Skip to content

refactor(dashboard): overlay, feedback and table primitives as kit adapters (KEA-7886, trilha D) - #17322

Merged
diegosouzapw merged 18 commits into
feat/new-uikitfrom
feat/new-uikit-D
Oct 20, 2026
Merged

diegosouzapw merged 18 commits into
feat/new-uikitfrom
feat/new-uikit-D

Conversation

@diegosouzapw

@diegosouzapw diegosouzapw commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Track D of the OmniRoute → Keak CI UI Kit migration (Linear KEA-7886): the overlay, feedback and table primitives become thin adapters over the vendored kit (packages/ui-kit). Every adapter keeps its file, default export, named exports and legacy props, so no importer is edited. Adapters import kit subpaths (@omniroute/ui-kit/<file>), never the barrel, which keeps react-day-picker and the rest of the kit out of every vitest worker.

Changes by task

Task 16: Modal / ConfirmModal over the kit Dialog (Radix) (603711ab, 831ab9c5, 7e6ae03d, 840c88e3, 9452dcd3)

  • src/shared/components/Modal.tsx: kit Dialog with aria-modal, Radix title wiring (sr-only common.dialogTitleFallback when there is no title), own focus restoration for controlled isOpen, TALL_MODAL_PROPS unchanged, outside-clicks on toasts ignored, one scroll owner (body) with the kit mobile gutter
  • src/i18n/messages/{en,pt-BR}.json: common.dialogTitleFallback

Task 17: Tooltip / InfoTooltip over the kit Tooltip (363f99d4, 7e6ae03d, 9452dcd3)

  • src/shared/components/Tooltip.tsx, InfoTooltip.tsx: Radix tooltip (hover + focus, Escape); a disabled trigger is wrapped in a focusable span; InfoTooltip is now a real, named <button>

Task 18: NotificationToast bridges the store into the kit Toaster (1e946c45, 7e6ae03d, 840c88e3)

  • src/shared/components/NotificationToast.tsx: <Toaster position="top-right"> + a store bridge; the toaster root stays a live region under an open dialog
  • config/quality/eslint-suppressions.json: stale suppression pruned

Task 19: EmptyState + Loading family (1a4d6234)

  • src/shared/components/EmptyState.tsx: kit EmptyState; Material ids render through <Icon> (Lucide), emoji keep the detection regex; the action is the Button adapter (variant="primary")
  • src/shared/components/Loading.tsx: Spinner keeps role="status" + aria-label (Lucide spinner), PageLoading is the kit LoadingState panel centred over the viewport, Skeleton/CardSkeleton are kit skeletons (ci-skeleton, aria-hidden)

Task 21: Breadcrumbs + Avatar (7dfadb7b)

  • src/shared/components/Breadcrumbs.tsx: kit Breadcrumb*; PATH_LABELS, getLabel, the /dashboard early return and the translated aria-label stay; aria-current="page" on the last crumb. The kit BreadcrumbLink has no asChild, so links stay next/link (client-side navigation) carrying the kit ci-breadcrumb__link class
  • src/shared/components/Avatar.tsx: kit Avatar (initials from name, same size names, ? fallback); the rainbow palette is gone

Task 22: DataTable family (2337142e)

  • src/shared/components/DataTable.tsx: DataTableShell loading + kit Table* + the EmptyState adapter; sticky header on the --table-header-bg token, selected row = aria-selected + data-state="selected", row click + Enter/Space activation and the nested-control guard kept
  • FilterBar.tsx: same signature, handleClear/hasActiveFilters kept; Input type="search" icon="search", one compact Select per filter (empty option = all), Button variant="ghost" icon="close"; colour styles removed
  • ColumnToggle.tsx: kit DropdownMenu (non-modal, like the legacy popover) + DropdownMenuCheckboxItem, menu stays open while toggling
  • FilterPills.tsx: role="group" of Button adapters (size="sm", aria-pressed), counts via Badge size="sm"
  • PresetSlider.tsx: kit Slider presets showValue
  • src/i18n/messages/{en,pt-BR}.json: common.clearFilters ("Clear filters" / "Limpar filtros")

Task 20: Collapsible / CollapsibleSection / SegmentedControl (106d77fe)

  • Collapsible.tsx: kit Accordion (bordered, inline → plain), the kit trigger is the single button with aria-expanded, trailing sits outside it, defaultOpen honoured, children mount only while open (as before)
  • CollapsibleSection.tsx: same, open by default, count as a kit Badge (variant="secondary"); // F1: swap for the Badge adapter once track C merges
  • SegmentedControl.tsx: kit capsule Tabs (role="tablist" with the aria-label, role="tab" + aria-selected, arrow keys), onChange(value) kept

Tests added or realigned

Added (adapter tests, all green):

  • tests/unit/ui/adapters/modal.test.tsx, tooltip.test.tsx, toast.test.tsx (D1)
  • tests/unit/ui/adapters/empty-loading.test.tsx 4/4, breadcrumbs-avatar.test.tsx 3/3, datatable.test.tsx 6/6 (the brief's 5 + FilterBar search/select/clear), collapsible-segmented.test.tsx 3/3

Realigned (same assertion, kit DOM):

  • D1: 33 files in 603711ab + 9 in 831ab9c5 query the modal through its portal (document.body / the dialog) instead of the render container; five mount checks became the stricter [role="dialog"]; modal-focus-restoration keeps the detached-opener focus spy (50e1b182); tooltip-portal-behavior.test.ts pins @omniroute/ui-kit/tooltip; analytics-token-hover-tooltip.test.tsx uses pointer events + unmounts its roots
  • tests/unit/dashboard-localization-contract.test.ts: EmptyState still detects Material ids (usesMaterialSymbol), now asserts they render through <Icon name={icon} instead of a material-symbols-outlined span
  • tests/unit/ui/data-table-loading-a11y.test.tsx: the busy status region is the kit BrandLoader (role="status", aria-busy); the decorative glyph is its aria-hidden SVG instead of ⏳; "layout/animation" asserts the kit overlay instead of inline styles; "no table while loading" asserts the retained rows are aria-hidden + inert; "no status region" checks look for [aria-busy="true"] (the kit renders aria-busy="false" when idle)
  • tests/unit/design-kit-bridge.test.ts (DataTable guard): header token kept; the zebra/border token assertions became "rows render through @omniroute/ui-kit/table" + "kit row selection via data-state"; no colour literal and no dead fallbacks still asserted

Commits 1a4d623 and 7dfadb7 carry the D1 adapter tests while they were still red; 840c88e and 9452dcd make them pass (history kept as-is, PR merges with --merge).

Validation

⚠️ base-red inherited: #17219

Command Where Result
npm run lint -- --concurrency=6 .113 rc 0
npm run typecheck:core .113 rc 0
npm run check:dashboard-typecheck .113 rc 0, 196 pre-existing errors within the frozen baseline (86 baselined errors no longer present)
npm run check:ui-icu .113 rc 0
npm run test:vitest:ui .113 498/505 files, 3425 passed / 22 failed / 3 skipped
the 7 red files, one at a time, --maxWorkers=1 --testTimeout=60000 .113 resilience-connections 18/18, modelsCombosCatalog 23/23, compressionPanelEnginesPartialWrite 3/3, manifestAdapter 12/12, proxySaveRefresh 1/1 → load/timeout only. Inherited: rtk-context-page-load-failure (3, known base red) and api-key-redis-auth-cache (hook timeout, fails identically on the base)
npm run check:tracked-artifacts local OK
npm run check:cycles (advisory) local cycles=14 = baseline
node scripts/i18n/check-key-completeness.mjs --warn local 65/66 locales miss 3 keys: common.clearFilters, common.dialogTitleFallback (this PR) and featureFlagUiKitShellDescription (track A); left to the i18n wave
focused adapter + pinned tests per task local all green (see the per-task lists above)
npm run build .113 success: next build + postbuild (standalone assembly, worker bundling) completed, rc 0 (_artifacts/D-pr-build-113.log). The tail -60 window starts after Turbopack's summary, so the warning count was not captured in this single build

Manual smoke

Dev server on port 20151 with an isolated DATA_DIR and dist dir, logged in through the e2e helper, Playwright captures light/dark at 1440 and 390 px (_artifacts/D-smoke.spec.ts):

  • /dashboard/providers (_artifacts/D-providers-{light,dark}-{1440,390}.png): kit breadcrumb, filter chips, empty-provider hero
  • /dashboard/settings → general (_artifacts/D-settings-*.png) and /dashboard/settings/routing (_artifacts/D-settings-routing-*.png, 2 kit accordions + a kit empty state)
  • /dashboard/translator (_artifacts/D-translator-*.png): 3 accordions (Collapsible), 2 capsule tab lists (SegmentedControl), 5 tooltip triggers
  • /dashboard/resilience/connections and /dashboard/costs (_artifacts/D-resilience-connections-*.png, _artifacts/D-costs-*.png, 4 kit skeletons)
  • Open modal: "Import providers from file" (_artifacts/D-modal-{light,dark}-1440.png); open tooltip on the translator ("The API format your app speaks…", _artifacts/D-tooltip-light-1280.png)
  • DataTable with rows at 390 px, light (_artifacts/D-datatable-390.png, fix round 1): /dashboard/conductor with the fleet endpoint fulfilled by a Playwright fixture (a fresh data dir has no hub). After min-w-0 the 3-column runners table fits the 306 px card (auto width, no sideways scroll); the 5-column tasks table is 484 px wide and scrolls inside its own container, as the legacy table did.

Fix round 1 (review of 57ee4c2c)

  • 50d1893d SegmentedControl: tabs no longer carry a dangling aria-controls (no panel is rendered); size maps onto the triggers (sm h-7 px-2 text-xs, lg h-10 px-4 text-base); the legacy phone layout is back on the tablist (w-full sm:w-auto, segments flex-1 on phones). A closed CollapsibleSection's content is inert, so it is out of the tab order as well as aria-hidden. The Collapsible/CollapsibleSection trigger's children wrapper can shrink, so long titles truncate.
  • 8bb7a526 DataTable: min-w-0 on the kit table, so narrow tables keep their auto width (the kit floors .ci-table at 40rem). EmptyState: an emoji icon is text-2xl and fits the kit's 2.5rem tile. view_column → Lucide Columns3 in iconMap.ts (icon-map-coverage was red without it).
  • 7a133bdc data-table-loading-a11y: the status-region test asserts the loading label (aria-label) and that no aria-hidden ancestor covers it, replacing a role check that could not fail.
  • Accepted kit behaviour: Avatar initials come from the kit (initials()): one word gives one letter ("Ada" → "A"), several words give the first and last initials. The legacy rule was the first two letters / the first two words. No app caller.

Pendências

Kit (from batch D1):

  1. The kit Toaster root should be a live region itself (aria-live="polite" on [data-slot="toaster"], toast.tsx:114). The adapter works around it in useToasterLiveRegion().
  2. toast() has no dismiss callback (no onDismiss/onAutoClose), so a kit-side close cannot update the store; it also blocks per-entry dismissible: false. Minor today: no caller uses duration: 0 or dismissible: false.
  3. The toast close label is hardcoded English (aria-label="Dismiss notification", toast.tsx:184); it needs a closeLabel prop.

Track D2:
4. CollapsibleSection renders the kit Badge directly until track C's Badge adapter lands (F1 checkpoint).
5. FilterPills / ColumnToggle / EmptyState sit on the legacy Button/Badge/Input/Select in this branch; they pick up track C's kit adapters on merge.
6. DataTableShell would print an English-only "N results" count, so the adapter does not pass resultCount.

…n, realign portal queries (KEA-7886, trilha D)
…stays live under a dialog (KEA-7886, trilha D)
…egmentedControl over Tabs (KEA-7886, trilha D)
…eeps the phone layout; closed CollapsibleSection is inert; Collapsible title truncates (KEA-7886, trilha D)
… tile; view_column mapped (KEA-7886, trilha D)
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.

1 participant