diff --git a/libs/ui/src/index.ts b/libs/ui/src/index.ts index f2b8a15ec..ea4a2af2d 100644 --- a/libs/ui/src/index.ts +++ b/libs/ui/src/index.ts @@ -1,3 +1,8 @@ +// Primitives (U0.5) +export { Badge } from './primitives/Badge'; +export type { BadgeProps } from './primitives/Badge'; +export { Kbd } from './primitives/Kbd'; +export type { KbdProps } from './primitives/Kbd'; export { ThemeProvider, useTheme } from './theme/ThemeProvider'; export type { Theme, ResolvedTheme, ThemeProviderProps } from './theme/ThemeProvider'; export { AppShell } from './shell/AppShell'; diff --git a/libs/ui/src/primitives/Badge.css b/libs/ui/src/primitives/Badge.css new file mode 100644 index 000000000..6f89e07fc --- /dev/null +++ b/libs/ui/src/primitives/Badge.css @@ -0,0 +1,42 @@ +.ac-badge { + border-radius: 6px; + white-space: nowrap; +} + +/* Dense card chip (was .ac-kanban__badge). */ +.ac-badge--sm { + display: inline-flex; + align-items: center; + min-height: 22px; + padding: 0 7px; + font-size: 10.5px; + font-weight: 780; +} + +/* Detail / label badge (was .ac-task-detail__badge). */ +.ac-badge--md { + padding: 2px 8px; + font-size: 11px; + font-weight: 600; +} + +.ac-badge--good { + color: var(--green); + background: var(--green-soft); +} +.ac-badge--info { + color: var(--blue); + background: var(--blue-soft); +} +.ac-badge--warn { + color: var(--amber); + background: var(--amber-soft); +} +.ac-badge--bad { + color: var(--red); + background: var(--red-soft); +} +.ac-badge--neutral { + color: var(--muted); + background: var(--rail); +} diff --git a/libs/ui/src/primitives/Badge.tsx b/libs/ui/src/primitives/Badge.tsx new file mode 100644 index 000000000..76fe73669 --- /dev/null +++ b/libs/ui/src/primitives/Badge.tsx @@ -0,0 +1,33 @@ +import type { HTMLAttributes } from 'react'; +import type { BadgeTone } from '../client/types'; +import './Badge.css'; + +export interface BadgeProps extends HTMLAttributes { + /** Semantic color; defaults to neutral. */ + tone?: BadgeTone; + /** 'sm' for dense card chips, 'md' for detail/label badges. */ + size?: 'sm' | 'md'; +} + +/** + * Tonal pill used across the board and detail screens (status chips, outcome + * badges). The tone colors are shared design tokens; `size` preserves the two + * historical sizings (dense card chip vs. detail badge). Remaining span + * attributes (className, aria-*, data-*, handlers) are forwarded. + */ +export function Badge({ + tone = 'neutral', + size = 'md', + className, + children, + ...rest +}: Readonly) { + const classes = `ac-badge ac-badge--${size} ac-badge--${tone}${ + className ? ` ${className}` : '' + }`; + return ( + + {children} + + ); +} diff --git a/libs/ui/src/primitives/Kbd.css b/libs/ui/src/primitives/Kbd.css new file mode 100644 index 000000000..75b386e28 --- /dev/null +++ b/libs/ui/src/primitives/Kbd.css @@ -0,0 +1,5 @@ +.ac-kbd { + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 10.5px; + color: var(--quiet); +} diff --git a/libs/ui/src/primitives/Kbd.tsx b/libs/ui/src/primitives/Kbd.tsx new file mode 100644 index 000000000..1e8bbebb4 --- /dev/null +++ b/libs/ui/src/primitives/Kbd.tsx @@ -0,0 +1,16 @@ +import type { HTMLAttributes } from 'react'; +import './Kbd.css'; + +export type KbdProps = HTMLAttributes; + +/** + * Inline keyboard-hint text (e.g. a "⌘K" shortcut label), rendered as the + * semantic element. Remaining attributes are forwarded. + */ +export function Kbd({ className, children, ...rest }: Readonly) { + return ( + + {children} + + ); +} diff --git a/libs/ui/src/screens/BoardToolbar.css b/libs/ui/src/screens/BoardToolbar.css index 33c6a5fa9..f42044982 100644 --- a/libs/ui/src/screens/BoardToolbar.css +++ b/libs/ui/src/screens/BoardToolbar.css @@ -44,11 +44,7 @@ color: var(--muted); } -.ac-toolbar__kbd { - font-family: ui-monospace, SFMono-Regular, Menlo, monospace; - font-size: 10.5px; - color: var(--quiet); -} +/* The ⌘K hint uses the shared primitive. */ /* The groups are fieldsets for semantics; reset the UA chrome. */ .ac-toolbar__filters { diff --git a/libs/ui/src/screens/BoardToolbar.tsx b/libs/ui/src/screens/BoardToolbar.tsx index ebb5dc93a..bf39a3b32 100644 --- a/libs/ui/src/screens/BoardToolbar.tsx +++ b/libs/ui/src/screens/BoardToolbar.tsx @@ -1,4 +1,5 @@ import type { ChangeEvent } from 'react'; +import { Kbd } from '../primitives/Kbd'; import './BoardToolbar.css'; export interface BoardToolbarFilter { @@ -70,9 +71,7 @@ export function BoardToolbar({ aria-label={searchLabel ?? searchPlaceholder} onChange={handleSearch} /> - {searchShortcut != null && ( - {searchShortcut} - )} + {searchShortcut != null && {searchShortcut}} )} {filters != null && filters.length > 0 && ( diff --git a/libs/ui/src/screens/KanbanBoard.css b/libs/ui/src/screens/KanbanBoard.css index b33a6af94..c539335d4 100644 --- a/libs/ui/src/screens/KanbanBoard.css +++ b/libs/ui/src/screens/KanbanBoard.css @@ -101,22 +101,7 @@ flex-wrap: wrap; } -.ac-kanban__badge { - display: inline-flex; - align-items: center; - min-height: 22px; - padding: 0 7px; - border-radius: 6px; - font-size: 10.5px; - font-weight: 780; - white-space: nowrap; -} - -.ac-kanban__badge--good { color: var(--green); background: var(--green-soft); } -.ac-kanban__badge--info { color: var(--blue); background: var(--blue-soft); } -.ac-kanban__badge--warn { color: var(--amber); background: var(--amber-soft); } -.ac-kanban__badge--bad { color: var(--red); background: var(--red-soft); } -.ac-kanban__badge--neutral { color: var(--muted); background: var(--rail); } +/* Status chip + outcome badges use the shared primitive. */ .ac-kanban__card-meta { display: flex; diff --git a/libs/ui/src/screens/KanbanBoard.tsx b/libs/ui/src/screens/KanbanBoard.tsx index 33d550482..110f458ce 100644 --- a/libs/ui/src/screens/KanbanBoard.tsx +++ b/libs/ui/src/screens/KanbanBoard.tsx @@ -1,5 +1,6 @@ import { Fragment } from 'react'; import type { KeyboardEvent } from 'react'; +import { Badge } from '../primitives/Badge'; import type { TaskStatus, UiTask } from '../client/types'; import './KanbanBoard.css'; @@ -81,11 +82,9 @@ function KanbanCard({ task, onSelect }: KanbanCardProps) {
{task.id} {task.statusChip != null && ( - + {task.statusChip.label} - + )}

{task.title}

@@ -100,12 +99,13 @@ function KanbanCard({ task, onSelect }: KanbanCardProps) { {task.badges != null && task.badges.length > 0 && (
{task.badges.map((badge) => ( - {badge.label} - + ))}
)} diff --git a/libs/ui/src/screens/TaskDetail.css b/libs/ui/src/screens/TaskDetail.css index 7c4cd26ae..933132a05 100644 --- a/libs/ui/src/screens/TaskDetail.css +++ b/libs/ui/src/screens/TaskDetail.css @@ -80,17 +80,7 @@ flex-wrap: wrap; gap: 6px; } -.ac-task-detail__badge { - font-size: 11px; - font-weight: 600; - padding: 2px 8px; - border-radius: 6px; -} -.ac-task-detail__badge--good { color: var(--green); background: var(--green-soft); } -.ac-task-detail__badge--info { color: var(--blue); background: var(--blue-soft); } -.ac-task-detail__badge--warn { color: var(--amber); background: var(--amber-soft); } -.ac-task-detail__badge--bad { color: var(--red); background: var(--red-soft); } -.ac-task-detail__badge--neutral { color: var(--muted); background: var(--rail); } +/* Badges use the shared primitive. */ .ac-task-detail__desc { margin: 0; diff --git a/libs/ui/src/screens/TaskDetail.tsx b/libs/ui/src/screens/TaskDetail.tsx index de8046dd2..9dc40661d 100644 --- a/libs/ui/src/screens/TaskDetail.tsx +++ b/libs/ui/src/screens/TaskDetail.tsx @@ -1,3 +1,4 @@ +import { Badge } from '../primitives/Badge'; import type { TaskStatus, UiTaskDetail } from '../client/types'; import './TaskDetail.css'; @@ -95,12 +96,13 @@ function TaskDetailBody({ task, statusLabels }: Readonly) { {task.badges != null && task.badges.length > 0 && (
{task.badges.map((badge) => ( - {badge.label} - + ))}
)}