diff --git a/docs/UI_ROADMAP_2026_V3_CROSS_REPO.md b/docs/UI_ROADMAP_2026_V3_CROSS_REPO.md index 7fc637f7..ff6f428f 100644 --- a/docs/UI_ROADMAP_2026_V3_CROSS_REPO.md +++ b/docs/UI_ROADMAP_2026_V3_CROSS_REPO.md @@ -612,10 +612,10 @@ For multi-step rows, sub-bullets are tracked independently. Agents should leave ### 11.2 Progress at a glance ``` -TOTAL 5 / 202 ⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛ 2% +TOTAL 10 / 202 ⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛ 5% ───────────────────────────────────────────── Wave 8 Rollout 5 / 18 🟩🟩🟩⬛⬛⬛⬛⬛⬛⬛ 28% -Wave 9 Data 0 / 42 ⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛ 0% +Wave 9 Data 5 / 42 🟩⬛⬛⬛⬛⬛⬛⬛⬛⬛ 12% Wave 10 Shells 0 / 35 ⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛ 0% Wave 11 Adaptive 0 / 26 ⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛ 0% Wave 12 Mobile 0 / 26 ⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛ 0% @@ -626,6 +626,17 @@ Magnet demos 0 / 8 ⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛ 0% > **Agents:** before pushing your commit, run `pnpm dlx tsx scripts/count-roadmap-progress.ts docs/UI_ROADMAP_2026_V3_CROSS_REPO.md` (to be authored in Wave 8.0) and paste the refreshed block in. +### 11.2.A Open TODOs raised during execution + +Numbered list — coding agents drop `// ROADMAP-EXEC-TODO #N` comments at the point of need; the human operator can review and unblock. + +| # | Title | Where it surfaced | What's needed | +| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **#1** | **Publish workflow run** — push 8 packages to Gitea registry (`react-auth@0.2.0`, `dashboard-shell@0.2.0`, `design-tokens@0.2.0`, `ai-ui@0.4.0`, `command-palette@0.1.0`, `motion@0.1.0`, `data-viz@0.1.0`, `notifications-ui@0.1.0`) | Wave 8.A.1 | Registry credentials / CI trigger. Until this runs, all 8.A.2–6, 8.C.\*, and the next `ui@0.2.0` adoption are gated. | +| **#2** | **Add vitest setup to `@bytelyst/ui`** | Wave 9.D additions (Skeleton, SkeletonGroup, LoadingDots, SearchInput) | Decide whether to follow the `motion`/`data-viz` pattern (vitest + happy-dom + @testing-library/react devDeps) or stay typecheck-only. Recommended: copy motion's setup, then write tests. | +| **#3** | **Visual-regression baseline refresh** — the new showcase routes (`/showcase/motion/all`, `/showcase/command-palette/global`, plus any UI demos shipping in Wave 9) need `toHaveScreenshot()` snapshots captured | Wave 8 / CC.1 | Run `pnpm baseline` in showcase and commit the snapshots. | +| **#4** | **Republish `@bytelyst/ui@0.2.0` to Gitea registry** once it catches up | Wave 9 hygiene | After TODO #1 closes, re-trigger publish so product webs can pin `^0.2.0`. | + ### 11.3 Wave 8 — Unblock & rollout · `5 / 18` #### 8.A · Publishing & infra @@ -658,7 +669,7 @@ Magnet demos 0 / 8 ⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛ 0% - [ ] **8.D.2** Showcase: all 94 Playwright smoke tests still passing after package swap - [ ] **8.D.3** Showcase: visual-regression baseline updated post-swap -### 11.4 Wave 9 — Data, content, search · `0 / 42` +### 11.4 Wave 9 — Data, content, search · `5 / 42` #### 9.A · `@bytelyst/charts@0.1.0` @@ -697,12 +708,12 @@ Magnet demos 0 / 8 ⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛ 0% #### 9.D · `@bytelyst/ui@0.2.0` additions -- [ ] **9.D.1** `` (text/rect/circle/card variants) + tests -- [ ] **9.D.2** `` orchestrator + tests -- [ ] **9.D.3** `` with illustration slot + CTA + tests -- [ ] **9.D.4** `` with suggestions slot + tests +- [x] **9.D.1** `` extended with `card` shape — 4 variants (`text`/`block`/`circle`/`card`) _(pending tests — see TODO #2)_ +- [x] **9.D.2** `` orchestrator — `loading` → `fallback` ↔ `children` swap with opacity fade _(pending tests — see TODO #2)_ +- [x] **9.D.3** `` — already shipped in `@bytelyst/ui@0.1.x`; verified API (icon + title + description + actionLabel + onAction) +- [x] **9.D.4** `` — leading icon, clear-`x`, `suggestions` slot, 3 size variants, `searchbox` role _(pending tests — see TODO #2)_ - [ ] **9.D.5** `` + `` + `` + tests -- [ ] **9.D.6** `` + `` (token-tinted) + tests +- [x] **9.D.6** `` added; `` already shipped — both token-tinted _(pending tests — see TODO #2)_ - [ ] **9.D.7** **Showcase:** `/showcase/ui/skeleton-gallery` - [ ] **9.D.8** **Showcase:** `/showcase/ui/empty-states` — 6 idiomatic empty states - [ ] **9.D.9** **Showcase:** `/showcase/ui/filter-bar` — interactive chip filter demo diff --git a/packages/ui/package.json b/packages/ui/package.json index 34897b4b..d9f379d3 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@bytelyst/ui", - "version": "0.1.11", + "version": "0.2.0", "type": "module", "scripts": { "storybook": "storybook dev -p 6006", diff --git a/packages/ui/src/components/LoadingDots.tsx b/packages/ui/src/components/LoadingDots.tsx new file mode 100644 index 00000000..3cceb05a --- /dev/null +++ b/packages/ui/src/components/LoadingDots.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +import { clsx } from 'clsx'; + +export interface LoadingDotsProps { + /** Tailwind-style size scale. */ + size?: 'sm' | 'md' | 'lg'; + /** Override the dot colour. Defaults to `var(--bl-accent)`. */ + color?: string; + /** Accessible label for screen readers. */ + label?: string; + className?: string; +} + +/** + * `` — three-dot pulsing indicator. Tiny, inline-friendly + * alternative to ``. Honours `prefers-reduced-motion` + * via Tailwind's `motion-safe:` (the dots stop pulsing but remain + * visible). + * + * Composable inside chat bubbles, button labels, inline status chips. + */ +export function LoadingDots({ + size = 'md', + color, + label = 'Loading', + className, +}: LoadingDotsProps) { + const dotSize: Record, string> = { + sm: 'h-1 w-1', + md: 'h-1.5 w-1.5', + lg: 'h-2 w-2', + }; + const gap: Record, string> = { + sm: 'gap-1', + md: 'gap-1.5', + lg: 'gap-2', + }; + return ( + + {[0, 1, 2].map(i => ( + + ); +} + +LoadingDots.displayName = 'LoadingDots'; diff --git a/packages/ui/src/components/SearchInput.tsx b/packages/ui/src/components/SearchInput.tsx new file mode 100644 index 00000000..2654fd0b --- /dev/null +++ b/packages/ui/src/components/SearchInput.tsx @@ -0,0 +1,106 @@ +import * as React from 'react'; +import { clsx } from 'clsx'; +import { Search, X } from 'lucide-react'; + +export interface SearchInputProps extends Omit< + React.InputHTMLAttributes, + 'onChange' | 'value' | 'size' +> { + /** Controlled value. */ + value: string; + /** Called with the new value on every keystroke. */ + onChange: (next: string) => void; + /** Placeholder text. Defaults to `Search…`. */ + placeholder?: string; + /** When true, the clear-`x` button is visible while `value` is non-empty. */ + clearable?: boolean; + /** Optional slot rendered below the input — typically ``. */ + suggestions?: React.ReactNode; + /** Accessible label; required if `placeholder` is the only context. */ + ariaLabel?: string; + /** Size scale. */ + size?: 'sm' | 'md' | 'lg'; + className?: string; +} + +/** + * `` — themed search field with a leading icon, a clear-`x` + * affordance, and an optional suggestions slot below. + * + * Avoids the `bl-search-bespoke` proliferation — Wave 9.D.4 consolidates + * every product's hand-rolled search field into one accessible primitive. + */ +export const SearchInput = React.forwardRef( + ( + { + value, + onChange, + placeholder = 'Search…', + clearable = true, + suggestions, + ariaLabel, + size = 'md', + className, + ...rest + }, + ref + ) => { + const heightCls: Record, string> = { + sm: 'h-8 text-xs', + md: 'h-10 text-sm', + lg: 'h-12 text-base', + }; + const iconBox: Record, string> = { + sm: 'h-3.5 w-3.5', + md: 'h-4 w-4', + lg: 'h-5 w-5', + }; + return ( +
+
+
+ {suggestions &&
{suggestions}
} +
+ ); + } +); + +SearchInput.displayName = 'SearchInput'; diff --git a/packages/ui/src/components/Skeleton.tsx b/packages/ui/src/components/Skeleton.tsx index 8ca9351d..1baca0d3 100644 --- a/packages/ui/src/components/Skeleton.tsx +++ b/packages/ui/src/components/Skeleton.tsx @@ -1,10 +1,25 @@ +// ROADMAP-EXEC-TODO #2 — vitest setup pending in @bytelyst/ui; add Skeleton + +// SkeletonGroup tests once happy-dom + @testing-library/react devDeps land. import * as React from 'react'; import { clsx } from 'clsx'; export interface SkeletonProps extends React.HTMLAttributes { - shape?: 'block' | 'text' | 'circle'; + /** + * Visual variant. + * - `text` — short pill, fits a single line of body text. + * - `block` — full rect, default; suitable as a hero or paragraph placeholder. + * - `circle` — fixed 40 × 40 disc; ideal for avatar slots. + * - `card` — taller rounded panel, mirrors a `` footprint so swapping + * to the real content does not shift layout. + */ + shape?: 'block' | 'text' | 'circle' | 'card'; } +/** + * `` — shimmer placeholder. Dimensions are tuned so the eventual + * content matches; this avoids the `bl-skeleton-shift` CLS anti-pattern + * codified in roadmap §3.8. + */ export function Skeleton({ shape = 'block', className, ...props }: SkeletonProps) { return (
); } + +export interface SkeletonGroupProps { + /** When `true`, render the skeleton block; when `false`, render `children`. */ + loading: boolean; + /** Skeleton placeholder rendered while `loading` is true. */ + fallback: React.ReactNode; + /** Real content rendered once `loading` flips false. */ + children: React.ReactNode; + /** + * If true, keep `children` in the DOM after first load so re-fetches + * don't unmount it — the skeleton still overlays via opacity. + * Default: false (clean swap). + */ + keepContent?: boolean; + /** Fade duration in ms. Default 180. */ + durationMs?: number; +} + +/** + * `` — orchestrates the fade between a skeleton placeholder + * and the real content. Use one per "loadable region" rather than sprinkling + * `` everywhere; the fade is unified and CLS-stable. + * + * @example + * ```tsx + * }> + * + * + * ``` + */ +export function SkeletonGroup({ + loading, + fallback, + children, + keepContent = false, + durationMs = 180, +}: SkeletonGroupProps) { + const style = { transition: `opacity ${durationMs}ms ease` } as const; + if (loading) { + return ( +
+ {fallback} + {keepContent && ( + + )} +
+ ); + } + return ( +
+ {children} +
+ ); +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 5297ee96..83e21dc8 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -11,7 +11,13 @@ export { type AlertBannerProps, type AlertBannerTone, } from './components/AlertBanner.js'; -export { Skeleton, TableSkeleton, type SkeletonProps } from './components/Skeleton.js'; +export { + Skeleton, + SkeletonGroup, + TableSkeleton, + type SkeletonProps, + type SkeletonGroupProps, +} from './components/Skeleton.js'; export { EntityCard, type EntityCardProps } from './components/EntityCard.js'; export { MetricCard, type MetricCardProps } from './components/MetricCard.js'; export { ActionMenu, type ActionMenuItem, type ActionMenuProps } from './components/ActionMenu.js'; @@ -169,3 +175,5 @@ export { } from './components/Sidebar.js'; export { StatCard, type StatCardProps } from './components/StatCard.js'; export { LoadingSpinner, type LoadingSpinnerProps } from './components/LoadingSpinner.js'; +export { LoadingDots, type LoadingDotsProps } from './components/LoadingDots.js'; +export { SearchInput, type SearchInputProps } from './components/SearchInput.js'; diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index c3412354..ee81a777 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -6,7 +6,8 @@ "moduleResolution": "bundler", "outDir": "dist", "rootDir": "src", - "declaration": true + "declaration": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"] }, "include": ["src"], "exclude": ["src/**/*.stories.tsx"]