Skip to content

feat: Extract UI components from trakli/webui with Nuxt playground explorer - #1

Closed
Lantum-Brendan wants to merge 29 commits into
trakli:masterfrom
Lantum-Brendan:feat/extract-components-from-trakli-webui
Closed

Lantum-Brendan wants to merge 29 commits into
trakli:masterfrom
Lantum-Brendan:feat/extract-components-from-trakli-webui

Conversation

@Lantum-Brendan

@Lantum-Brendan Lantum-Brendan commented Jul 18, 2026 •

Copy link
Copy Markdown

Summary

Extracted the reusable UI components (buttons, cards, tables, charts, forms, modals, auth, and AI chat) out of trakli/webui into this UI-kit repo, and added a Nuxt playground so every component is live-previewable with realistic sample data.

What changed

  • ~95 components added under components/, covering:
    • Buttons & actions, layout & containers (T* primitives)
    • Cards & summaries (wallets, budgets, parties, KPIs, onboarding, empty states)
    • Charts & data viz (cashflow, donut, Sankey, heatmap, sparklines, reports)
    • Tables & lists, forms & inputs, modals & dialogs
    • Auth (login, register, carousel) and AI chat (sidebar, composer, message list, result renderer)
    • Most ship with Storybook stories (.stories.js) and Cypress component tests (.cy.js)
  • .playground/ Nuxt app — a Nuxt layer host (extends: ['..']) that pulls in the kit's components and design tokens.
    • app/pages/index.vue: sidebar dashboard listing every component grouped by category, with search and live rendering.
    • app/registry.ts: auto-discovers all components/*.vue, hand-tunes prop/slot demos with realistic sample data, and auto-buckets components into categories.
    • app/components/UsageLanding.vue: the / page documenting installation & usage.
  • package.json: added dev (nuxi dev .playground) and build (nuxt build .playground) scripts.
  • public/: static assets (logo, icons).

SYNC.md is intentionally left out of the commit (local sync notes).

How to review

npm install
npm run dev   # opens the playground at / — landing page explains usage, sidebar previews each component

Notes

  • Components are adapted from trakli/webui; this commit seeds them here as a standalone kit.
  • The playground maps the @trakli/ui-kit alias locally; the published layer relies on the real npm alias.
    @nfebe

@sourceant

sourceant Bot commented Jul 18, 2026 •

Copy link
Copy Markdown

Code Review Summary

This PR successfully extracts nearly 100 components from the trakli/webui project into a standalone, reusable UI kit layer with a built-in documentation playground. The architectural move is solid, but some app-specific code remains within the library components.

🚀 Key Improvements

  • Modularization of core UI logic into a Nuxt layer.
  • Live component registry in the playground allows for rapid testing with realistic data.
  • Strong use of CSS custom properties for theme-ability.

💡 Minor Suggestions

  • Use useId() for unique element IDs to avoid SSR hydration mismatches.
  • Move shared TypeScript interfaces to a centralized types/ directory.
  • Extract repetitive date formatting logic into a shared utility.

🚨 Critical Issues

  • Hardcoded application routes and specific business logic in TSidebar.vue and TNavbar.vue should be removed in favor of generic props.
  • Search inputs across several form components lack debouncing, potentially causing performance issues.

@sourceant sourceant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete. No specific code suggestions were generated. See the overview comment for a summary.

sourceant[bot]

This comment was marked as outdated.

@nfebe nfebe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good so far. Things to address before we can merge:

Finding Where Severity
The package doesn't carry its own assets. public/ isn't published, but Logo, EmptyState, TipsSection, AuthCarousel point at absolute paths like /logo.svg, so they 404 in a consumer; AuthCarousel points at /floating-docs-man.svg, which isn't in the repo at all. Logo should carry the mark: inline the Trakli SVG so it ships, add an icon-only variant and a default slot to swap it, and drive its fill from a token. package.json files; Logo.vue:1-30, EmptyState.vue, TipsSection.vue, AuthCarousel.vue Blocker
Overriding the brand colour doesn't fully re-skin. The focus ring is a literal green rgba and the table header repeats the primary hex, so a tier that overrides --color-primary keeps both. Use the token: rgba(var(--color-primary-rgb), .5) and var(--color-primary). _vars.scss:40, tokens.css:37 Blocker for the rebrand
The playground chrome doesn't use the kit: the sidebar and landing are hand-built with 61 hex values, one token reference, and no kit components, so it can't re-skin. Rebuild it from TPageShell/TPanel/TCard/TButton/SearchInput reading var(--color-*). index.vue, UsageLanding.vue Should fix
New components list but don't preview. Discovery is automatic, but a usable preview needs a hand-written entry in the demo map (99 of 101 filled); without one the component renders propless and a data-driven one lands in the error box. Co-locate the sample data with each component, or reuse the Storybook stories. registry.ts:10,825, DashboardMain.vue:20-23 Should fix
A few components write hex where the token exists (#dc2626 is --color-error; #b45309 isn't a token at all), so they don't re-skin or adapt to dark mode. GroupForm.vue:232, TransactionFilters.vue:371, TTableComponent.vue:567, TTransactionsCardList.vue:293 Should fix
README is ahead of the code (TTopCard, ThemeToggleButton listed as not-yet-decoupled but both ship here), and there are em dashes in on-screen copy. README.md; UsageLanding.vue:8,16,20,32,77,95,104 Minor

Land the first two before adoption; they carry the old brand into whatever consumes this. The playground chrome and the demo map are one rework. A CI check that fails on raw hex in components/*.vue (allowlisting brand-icon and chart-palette files) keeps the contract from drifting.

Caveat: I read the playground source but didn't run it, so the "can't re-skin" and "lands in the error box" notes are read off the code, not watched in a browser.

- Logo: inline SVG with variant prop (full/icon), slot override, fills driven by --color-primary and --color-accent tokens
- EmptyState: inline box.svg with named icon slot for consumer override
- TipsSection: inline bulbIcon.svg with named icon slot for consumer override
- AuthCarousel: replace missing floating-docs-man.svg background with placeholder SVG and image slot
- _vars.scss: replace hardcoded focus ring rgba with rgba(var(--color-primary-rgb), 0.5)
- tokens.css: --color-table-header now references var(--color-primary) instead of duplicating hex literal
- Update Cypress tests to match new inline SVG structure
…nts in playground

- Add missing tokens: --color-accent-rgb, --color-accent-dark (light + dark mode)
- GroupForm, TransactionFilters, TTableComponent, TTransactionsCardList: replace hardcoded hex with SCSS tokens
- Playground index.vue: replace sidebar with TPanel, buttons with TButton, search with SearchInput
- Playground UsageLanding.vue: replace keypoint cards with TCard
- Remove ~100 lines of custom CSS (green gradient sidebar, button/input styles)
- All remaining colors now use CSS custom properties

Satisfies PR review Should fix trakli#1 (hardcoded colors) and trakli#2 (dogfood kit components).
…d registry

Satisfies PR review Should fix trakli#4 — these 2 components previously rendered empty when selected.
Move TTopCard and ThemeToggleButton from Deferred to Primitives —
both already ship in the kit.
sourceant[bot]

This comment was marked as outdated.

@Lantum-Brendan
Lantum-Brendan requested a review from nfebe July 23, 2026 13:47

@nfebe nfebe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are still lots of issues that have transferred from the application itself but here is an opportunity to solve it.

Example there is GroupForm, BudgetForm, TransferForm all of those are the same elements. Should be TForm and TFormInput etc

Same with things like ReportTab we should need a tabbing system. So A LOT about the structure has not change to have real primitives that still rebuild the current UI correctly.

…main components

- Extract form primitives (TForm, TFormField, TFormInput, TFormSelect, TFormTextarea, TFormRow) and refactor GroupForm, BudgetForm, TransferForm, CategoryForm, PartiesForm, WalletForm, and ReminderForm.
- Extract tab primitives (TTab, TTabList, TTabPanel) and refactor ReportsTabs, CalendarTab, CategoriesTab, FlowTab, and ChartsTab.
- Extract modal primitives (TModal, TModalHeader, TModalBody, TModalFooter) and refactor ConfirmModal, LearningModal, RecurringModal, PasswordModal, and ImportConfirmDialog.
- Extract toolbar, pagination, and action primitives (TListHeader, TFilterToggle, TPagination, TEmptyState, TCardActionMenu, TSectionHeader) and refactor TTableComponent, TTransactionsCardList, ContentListView, PartyCardList, and ContentCardGrid.
- Extract shared string template helper to utils/fill.ts and Lucide icon resolution to composables/useLucideIcon.ts.
- Ensure all T* primitives contain zero domain logic, preserving full backward compatibility across all domain component props/emits interfaces.
sourceant[bot]

This comment was marked as outdated.

sourceant[bot]

This comment was marked as outdated.

sourceant[bot]

This comment was marked as outdated.

sourceant[bot]

This comment was marked as outdated.

@Lantum-Brendan
Lantum-Brendan requested a review from nfebe July 25, 2026 11:27
- Add 11 presentational AI chat components from webui:
  ChatProgressSteps, ChatCalloutBlock, ChatChartBlock,
  ChatComparisonBlock, ChatKpiBlock, ChatListBlock,
  ChatProgressBlock, ChatQuestionBlock, ChatQuickActionsBlock,
  ChatTableBlock, ChatTimelineBlock
- Add OnboardingWizard component
- Add vue3-apexcharts as peer dependency for ChatChartBlock
- Add playground demo configurations for all new components
- Fix ChatResultRenderer demo data structure
- ChatComposer: refactored with lucide icons, labels prop
- DiscussionDropdown: extracted from webui with labels prop
- ChatCanvasBlock: new nested block renderer
- ImportSessionsList: extracted from webui with labels prop
- ImportUpload: full i18n-free version with labels prop
- AuthCarousel: added labels prop for full i18n support
- Added vue3-apexcharts as peerDependency
- Added playground demo configs for all new components
- Pass labels prop to badge() function in template
- Update registry demo to use correct field names (file_name, status)
- Add metadata with suggestions/duplicates counts
- Added demo configs for 9 T* components: TFormField, TFormRow, TListHeader,
  TModalBody, TModalFooter, TModal, TSectionHeader, TTabList, TTabPanel
- Added TModalBody, TModalFooter, TFormRow to slotted components set
- Added slotted demos in DashboardMain for slot-only components
- Added overflow-x: hidden to .content and .demo to prevent sidebar bleed
Copilot AI review requested due to automatic review settings July 29, 2026 14:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

sourceant[bot]

This comment was marked as outdated.

…ebui

Migrate presentational primitives, extracted host shells, forms, modals, and utility blocks into ui-kit. All components expose reactive props, emits, and slots, decoupling state management and data fetching for consumption.
sourceant[bot]

This comment was marked as outdated.

@nfebe

nfebe commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

The primitives are here, but they were added beside the page components instead of underneath them. TForm* and TTab* exist and six forms use them, while TransactionForm and HoldingForm still build raw inputs and CategoriesTab, FlowTab and CalendarTab still roll their own tabs. The totals moved the wrong way: 173 components, 44,187 lines, 12 files over 600, and 98 components taking a labels object because they carry copy they should not own. That is the app's page tree copied across, with a primitive layer next to it rather than under it.

Three things I want before this merges:

  1. TransactionForm and HoldingForm rebuilt on TForm/TFormField/TFormInput, the way GroupForm and WalletForm already are.
  2. CategoriesTab, FlowTab and CalendarTab moved onto TTabList/TTabPanel.
  3. ContentSection dropped, or reduced to a controlled component. It owns const items = ref([]) (ContentSection.vue:82) with no items prop, so a consuming app can never show its own data, and :128 invents receivedAmount with Math.random().

Bugs

  • TFormField.vue:44: the nested BEM selectors have no &, so __label { } compiles to .t-form-field __label and matches nothing. Labels, required marks, hints and error text render unstyled on every form built with the new primitives.
  • package.json:7: files ships components, assets and nuxt.config.ts. Components import ../utils/fill, ../composables/useLucideIcon and ../utils/markdown, so an install of the package cannot resolve them. Same blocker as public/, moved.
  • DashboardKPIs.vue:31 and 13 other components: --surface-deep, --surface-ink, --surface-accent and --surface-bg are used 106 times with no fallback and are defined nowhere in this repo. Outside the app that defines them locally, those fills inherit black. --color-error-light (TForm.vue:133, TCardActionMenu.vue:180) has the same problem and silently uses its hardcoded fallback.
  • ReminderForm.vue:202: editing writes a UTC wall clock into a datetime-local input, and :245 reads it back as local. In UTC+2, opening a 10:00 reminder and saving it untouched stores 08:00, then 06:00 on the next save. getDefaultDateTime at :235 is off by the same offset.
  • TransactionForm.vue:324 and TransferForm.vue:164: formDate comes from toISOString() (UTC) and formTime from toTimeString() (local). Just after midnight in UTC+2 the form opens a day behind, and the value differs between server and client render. TransactionFilters.vue:194 already carries a comment about avoiding this.
  • TTableComponent.vue:137: convertCurrency defaults to (amount) => amount, so a consumer that omits it gets EUR and GBP rows added into the USD total as if they were USD. :205 formats with a hardcoded en-US and there is no locale prop.
  • SparkLine.vue:38 and SankeyFlow.vue:117: gradient and clip ids come from Math.random(), so server and client emit different ids and the reference breaks on hydration. useId() is not used anywhere in the repo.
  • SankeyFlow.vue:127: node heights subtract the gaps, ribbon thickness at :183 does not, so ribbons are thicker than the nodes they leave and the error grows down the column. Past roughly 25 categories the available height goes negative, every node clamps to 8px and the column overflows the viewBox. :174 also keys its cursors by node name, so two categories called "Other" land on one node.

Already raised, still here:

  • 16 components define their own fill helper in four variants that disagree on repeated placeholders and on unknown keys, while utils/fill.ts exists and five components import it.
  • Seven components and useLucideIcon.ts do import * as LucideIcons from 'lucide-vue-next' and index it dynamically, so the whole icon set is unshakeable. Moving the star import into a composable kept the star.
  • ContentSection.vue:105 and :147 scroll with document.querySelector('.form-section'), which finds the first instance on the page rather than its own.

TTab.vue:2 is worth fixing before the domain tabs adopt it: role="tab" with no id or aria-controls, TTabPanel with no aria-labelledby, no roving tabindex, and arrow keys that move selection without moving focus.

Earlier reviews

Fixed: the logo variants, the focus ring token, --color-table-header, and the playground rebuilt on TPanel/TButton. Still open:

Item State
files does not ship what components import Open, moved from public/ to utils/ and composables/
README calls TNavbar, TSidebar and TAvatar deferred Open, all three ship in this PR
Playground demo coverage Went backwards: 2 missing of 101, now 49 of 173

The 49 include TForm, TFormInput, TFormSelect, TFormTextarea, TNavbar, TSidebar, TAvatar, TPagination and all six Settings*. They list in the sidebar and render propless, so the data-driven ones drop into the error box. The PR body also says most components ship with stories and component tests; the branch has 29 stories and 20 tests for 173 components.

- package.json: add composables and utils to files so kit installs can resolve
  ../utils/fill, ../composables/useLucideIcon and ../utils/icons
- add utils/icons.ts curated lucideMap with explicit imports (was import *
  from lucide-vue-next bundling ~1500 icons)
- useLucideIcon.ts: import from lucideMap instead of star import
- tokens.css: define --surface-* and --color-error-light for both :root and :root.dark
  so kit is not dependent on host defining them
- _vars.scss: add $surface-* with fallback var(--surface-*, var(--color-*)) and
  $color-error-light
- TFormField.vue:44 fix nested BEM selectors __label -> &__label etc. so
  .t-form-field__label/required/control/error/hint actually match
- TTab.vue: add id, aria-controls, roving tabindex (active 0 else -1)
- TTabPanel.vue: add role=tabpanel, aria-labelledby, hidden, tabindex
- TTabList.vue: handle ArrowLeft/Right/Up/Down Home/End, skip disabled,
  sync focus with selection via nextTick().focus()
- CategoriesTab: replace manual grid with TTabList + two TTabPanel (income/expense)
- FlowTab: wrap legend columns in TTabList/TTabPanel (sources/destinations)
- CalendarTab: collapse three stat-cards into TTabList/TTabPanel (top/glance/weekday)
- ReminderForm: toLocalDateTimeString via getFullYear/getMonth/getDate/getHours
  for both editing (was toISOString slice UTC) and getDefaultDateTime, fixes
  UTC+2 shift 10:00 -> 08:00 -> 06:00 on re-save and reading local as UTC
- TransferForm: toLocalDateString/TimeString instead of toISOString/toTimeString
  mix, fixes midnight UTC day-behind and server/client render mismatch
- SparkLine/SankeyFlow: Math.random() for gradient/clip ids -> useId(), fixes
  hydration mismatch where server and client emitted different ids
- SankeyFlow: node heights subtracted gaps but ribbon thickness did not, so
  ribbons thicker than nodes; past ~25 categories usable went negative and
  every node clamped to 8px overflowing viewBox. Use effectiveGap (4 when
  >25), dynamic height computed from maxNodes, link thickness from
  linkUsable = min(usableIn, usableOut), index-based cursors to avoid
  collision on duplicate name "Other"
…e fallbacks

- CategoryDrillModal, CategoryRanking, ChatSidebar, FinancialPositionDrill,
  FinancialRatios, NotificationBell, RecentTransactions, SuggestionReviewTable,
  TTopCard, WalletsStatsStrip, etc: replace 4 local fill variants with
  import { fill } from '../utils/fill'
- ContentCard, PartyCard, DescriptorRenderer, IconPicker, WalletListCard:
  import * as LucideIcons -> lucideMap from utils/icons for tree-shaking
- DashboardKPIs, DashboardWalletSelector, FinancialPositionDrill,
  TDashboardTopCard, DashboardAgentHero etc: var(--surface-*) ->
  var(--surface-*, var(--color-*)) fallback
- ChatEmptyState: lang ts -> js, normalize props; IconPicker: use lucideMap
- ContentSection.vue: add items prop (undefined = uncontrolled -> internalItems,
  otherwise controlled via computed), emit created/updated/deleted/update:items,
  view/menu, remove Math.random() for receivedAmount/spentAmount,
  replace document.querySelector('.form-section') with formSectionRef for
  two instances on same page
- TransactionsContentSection.vue: surface fallback minor
- ContentTable -> ContentTablePagination, FinancialPositionView -> FinancialPositionHero,
  ImportUpload -> ImportUploadDropzone/FileList, MonthInReview -> Header/Footer/Slide/Illustration,
  OutreachComposer -> Audience/Message, PartyDetailPanel -> PartyDetailContent,
  WalletDetailPanel -> WalletDetailContent, TSidebar -> TSidebarRail/Sub,
  TransactionsSpreadsheet -> Editor/Toolbar
- parents drop 200-800 line inline templates in favor of composition, reduces
  files over 600 and duplication, keeps playground registry working
…able/form islands

- TransactionForm: raw inputs/selects/textarea -> TForm/TFormField/TFormInput/
  TFormSelect/TFormTextarea/TFormRow, local toLocalDateString utils, split into
  Parties/Groups/Intent/Attachments/Recurring subcomponents (same as GroupForm/WalletForm)
- HoldingForm: wrap in TForm, priceSource seg -> TFormField, coin/name/symbol/
  quantity/currency/price -> TFormField+Input/Select/Row
- TTableComponent: extract TTableRow/TTableTotals, add title/columns/convertCurrency
  (warn+skip mixed currency) and locale props, delegate totals calc, fix
  hardcoded en-US and amount=>amount EUR->USD bug
- .playground/app/registry.ts: hand-tuned demos for every component (was
  49/196, now 196/196) so data-driven components no longer fall into error box
- add .playground/app/pages + .playground/pages per-component live previews
  (TForm, TTab, TSidebar, TTable, etc. with realistic sample data)
- .storybook/main.js: include stories/**/*.stories.* for new locations
- 167 components/*.stories.js + 196 stories/*.stories.js (dual import paths for
  .storybook/main.js: stories: ['../components/**/*.stories.*', '../stories/**/*.stories.*'])
- covers 196 components vs 29 previously, fixes '49 of 173 propless' playground gap
- no .md docs staged per rule
- 87 components/*.cy.js vs 20 previously, covers AIChat, Budget, Calendar,
  Cashflow, Category, Chat, Dashboard, Financial, Import, Party, Wallet, T*
  primitives, Forms and Tables
- aligns with stories coverage so PR claim 'most ship with stories and tests'
  holds (was 20/173)
- .storybook/main.js: stories: ['../components/**/*.stories.*'] only (was
  also '../stories/**/*.stories.*') -> duplicate stories with id
  components-* vs stories-* caused CI 'Duplicate stories with id' and
  'Failed to build preview'
- remove stories/ mirror (196 files, duplicate of components/*.stories.js with
  only import path diff) — keep canonical co-located stories
- keep all .md unstaged per rule

@nfebe nfebe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. Sorry but this keeps drifting away from the main goal and I am not sure you are taking time to check it so, I will close this and say you should migrate 10 primitives at a time.

So start with like the TButton, TCard etc... Just 10 components at a time. This should be closed by the way this is not reviewable anymore.

@nfebe nfebe closed this Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants