lucent-ui 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +106 -19
- package/dist/index.d.ts +166 -1
- package/dist/index.js +2940 -1002
- package/dist-server/src/components/molecules/CommandPalette/CommandPalette.manifest.js +89 -0
- package/dist-server/src/components/molecules/DataTable/DataTable.manifest.js +97 -0
- package/dist-server/src/components/molecules/DatePicker/DatePicker.manifest.js +91 -0
- package/dist-server/src/components/molecules/DateRangePicker/DateRangePicker.manifest.js +85 -0
- package/dist-server/src/components/molecules/FileUpload/FileUpload.manifest.js +104 -0
- package/dist-server/src/components/molecules/MultiSelect/MultiSelect.manifest.js +100 -0
- package/dist-server/src/components/molecules/PageLayout/PageLayout.manifest.js +160 -0
- package/dist-server/src/components/molecules/Timeline/Timeline.manifest.js +60 -0
- package/package.json +1 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export const COMPONENT_MANIFEST = {
|
|
2
|
+
id: 'command-palette',
|
|
3
|
+
name: 'CommandPalette',
|
|
4
|
+
tier: 'overlay',
|
|
5
|
+
domain: 'neutral',
|
|
6
|
+
specVersion: '1.0',
|
|
7
|
+
description: 'A keyboard-driven modal command palette with fuzzy search, grouped results, and a built-in ⌘K / Ctrl+K global shortcut.',
|
|
8
|
+
designIntent: 'CommandPalette renders as a portal-style overlay (fixed, full-viewport) with a centred panel that animates in with a subtle scale+fade. ' +
|
|
9
|
+
'The ⌘K shortcut is registered as a global window listener so it works regardless of focus — consumers can override the key via shortcutKey prop. ' +
|
|
10
|
+
'Clicking the backdrop dismisses the palette; Escape also closes it. ' +
|
|
11
|
+
'Results are filtered client-side against label and description using simple substring match — no fuzzy library needed. ' +
|
|
12
|
+
'Navigation is purely keyboard-driven: ↑↓ move the active index, Enter selects, mouse hover syncs the active index so mouse and keyboard stay in sync. ' +
|
|
13
|
+
'Groups are derived from the group field on each CommandItem; the order of groups follows the order they first appear in the commands array. ' +
|
|
14
|
+
'The component is controlled-or-uncontrolled: pass open + onOpenChange for controlled use, or omit both to use internal state.',
|
|
15
|
+
props: [
|
|
16
|
+
{
|
|
17
|
+
name: 'commands',
|
|
18
|
+
type: 'array',
|
|
19
|
+
required: true,
|
|
20
|
+
description: 'Array of CommandItem objects. Each has id, label, onSelect, and optional description, icon, group, disabled.',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'placeholder',
|
|
24
|
+
type: 'string',
|
|
25
|
+
required: false,
|
|
26
|
+
default: '"Search commands…"',
|
|
27
|
+
description: 'Placeholder text for the search input.',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'shortcutKey',
|
|
31
|
+
type: 'string',
|
|
32
|
+
required: false,
|
|
33
|
+
default: '"k"',
|
|
34
|
+
description: 'Key that opens the palette when pressed with Meta (Mac) or Ctrl (Windows). Defaults to "k" for ⌘K.',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'open',
|
|
38
|
+
type: 'boolean',
|
|
39
|
+
required: false,
|
|
40
|
+
description: 'Controlled open state. When provided, the component is fully controlled.',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'onOpenChange',
|
|
44
|
+
type: 'function',
|
|
45
|
+
required: false,
|
|
46
|
+
description: 'Called when the palette requests an open/close state change.',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'style',
|
|
50
|
+
type: 'object',
|
|
51
|
+
required: false,
|
|
52
|
+
description: 'Inline style overrides for the backdrop element.',
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
usageExamples: [
|
|
56
|
+
{
|
|
57
|
+
title: 'Uncontrolled with ⌘K',
|
|
58
|
+
code: `<CommandPalette
|
|
59
|
+
commands={[
|
|
60
|
+
{ id: 'new', label: 'New document', icon: <PlusIcon />, onSelect: () => router.push('/new') },
|
|
61
|
+
{ id: 'settings', label: 'Settings', description: 'Open app settings', onSelect: () => router.push('/settings') },
|
|
62
|
+
{ id: 'logout', label: 'Log out', group: 'Account', onSelect: handleLogout },
|
|
63
|
+
]}
|
|
64
|
+
/>`,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
title: 'Controlled with custom shortcut',
|
|
68
|
+
code: `const [open, setOpen] = useState(false);
|
|
69
|
+
<>
|
|
70
|
+
<Button onClick={() => setOpen(true)}>Open palette</Button>
|
|
71
|
+
<CommandPalette
|
|
72
|
+
commands={commands}
|
|
73
|
+
open={open}
|
|
74
|
+
onOpenChange={setOpen}
|
|
75
|
+
shortcutKey="p"
|
|
76
|
+
/>
|
|
77
|
+
</>`,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
compositionGraph: [
|
|
81
|
+
{ componentId: 'text', componentName: 'Text', role: 'Group labels, item labels, descriptions, and empty state', required: true },
|
|
82
|
+
],
|
|
83
|
+
accessibility: {
|
|
84
|
+
role: 'dialog',
|
|
85
|
+
ariaAttributes: ['aria-label', 'aria-modal', 'aria-expanded', 'aria-selected', 'aria-disabled', 'aria-controls', 'aria-autocomplete'],
|
|
86
|
+
keyboardInteractions: ['⌘K / Ctrl+K to open', '↑↓ to navigate', 'Enter to select', 'Escape to close'],
|
|
87
|
+
notes: 'The backdrop and panel use role="dialog" with aria-modal="true". The input is role="searchbox". The result list is role="listbox" with role="option" items. Focus is moved to the search input on open.',
|
|
88
|
+
},
|
|
89
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export const COMPONENT_MANIFEST = {
|
|
2
|
+
id: 'data-table',
|
|
3
|
+
name: 'DataTable',
|
|
4
|
+
tier: 'molecule',
|
|
5
|
+
domain: 'neutral',
|
|
6
|
+
specVersion: '1.0',
|
|
7
|
+
description: 'A sortable, paginated data table with configurable columns, custom cell renderers, and keyboard-accessible pagination controls.',
|
|
8
|
+
designIntent: 'DataTable is generic over row type T so TypeScript consumers get full type safety on column keys and renderers. ' +
|
|
9
|
+
'Sorting is client-side and composable — each column opts in via sortable:true; clicking a sorted column cycles asc → desc → unsorted. ' +
|
|
10
|
+
'Pagination is either controlled (page prop + onPageChange) or uncontrolled (internal state). ' +
|
|
11
|
+
'A pageSize of 0 disables pagination entirely, useful when the parent manages windowing. ' +
|
|
12
|
+
'Column filtering is intentionally excluded here (see DataTable Filter issue #52) to keep the API focused. ' +
|
|
13
|
+
'Row hover uses bg-subtle, not a border change, so the visual weight stays low for dense data views.',
|
|
14
|
+
props: [
|
|
15
|
+
{
|
|
16
|
+
name: 'columns',
|
|
17
|
+
type: 'array',
|
|
18
|
+
required: true,
|
|
19
|
+
description: 'Column definitions. Each column has a key, header, optional render function, optional sortable flag, optional width, and optional text align.',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'rows',
|
|
23
|
+
type: 'array',
|
|
24
|
+
required: true,
|
|
25
|
+
description: 'Array of data objects to display. The generic type T is inferred from this prop.',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'pageSize',
|
|
29
|
+
type: 'number',
|
|
30
|
+
required: false,
|
|
31
|
+
default: '10',
|
|
32
|
+
description: 'Number of rows per page. Set to 0 to disable pagination.',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'page',
|
|
36
|
+
type: 'number',
|
|
37
|
+
required: false,
|
|
38
|
+
description: 'Controlled current page (0-indexed). When provided, the component is fully controlled.',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'onPageChange',
|
|
42
|
+
type: 'function',
|
|
43
|
+
required: false,
|
|
44
|
+
description: 'Called with the new page index whenever the page changes (from pagination controls or after a sort reset).',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'emptyState',
|
|
48
|
+
type: 'ReactNode',
|
|
49
|
+
required: false,
|
|
50
|
+
description: 'Content to render when rows is empty. Defaults to a "No data" text.',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'style',
|
|
54
|
+
type: 'object',
|
|
55
|
+
required: false,
|
|
56
|
+
description: 'Inline style overrides for the outer wrapper.',
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
usageExamples: [
|
|
60
|
+
{
|
|
61
|
+
title: 'Basic sortable table',
|
|
62
|
+
code: `<DataTable
|
|
63
|
+
columns={[
|
|
64
|
+
{ key: 'name', header: 'Name', sortable: true },
|
|
65
|
+
{ key: 'role', header: 'Role', sortable: true },
|
|
66
|
+
{ key: 'status', header: 'Status', render: (row) => <Badge>{row.status}</Badge> },
|
|
67
|
+
]}
|
|
68
|
+
rows={users}
|
|
69
|
+
/>`,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
title: 'Controlled pagination',
|
|
73
|
+
code: `const [page, setPage] = useState(0);
|
|
74
|
+
<DataTable
|
|
75
|
+
columns={columns}
|
|
76
|
+
rows={rows}
|
|
77
|
+
pageSize={20}
|
|
78
|
+
page={page}
|
|
79
|
+
onPageChange={setPage}
|
|
80
|
+
/>`,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
title: 'No pagination',
|
|
84
|
+
code: `<DataTable columns={columns} rows={rows} pageSize={0} />`,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
compositionGraph: [
|
|
88
|
+
{ componentId: 'text', componentName: 'Text', role: 'Row count and empty state labels', required: false },
|
|
89
|
+
{ componentId: 'badge', componentName: 'Badge', role: 'Typical cell content for status columns', required: false },
|
|
90
|
+
],
|
|
91
|
+
accessibility: {
|
|
92
|
+
role: 'table',
|
|
93
|
+
ariaAttributes: ['aria-label', 'aria-sort', 'aria-current'],
|
|
94
|
+
keyboardInteractions: ['Tab to pagination controls', 'Enter/Space to activate buttons'],
|
|
95
|
+
notes: 'Column headers with sortable:true are interactive buttons with aria-sort reflecting the current sort direction. Pagination buttons include aria-label and aria-current="page" for the active page.',
|
|
96
|
+
},
|
|
97
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export const COMPONENT_MANIFEST = {
|
|
2
|
+
id: 'date-picker',
|
|
3
|
+
name: 'DatePicker',
|
|
4
|
+
tier: 'molecule',
|
|
5
|
+
domain: 'neutral',
|
|
6
|
+
specVersion: '1.0',
|
|
7
|
+
description: 'A single-date picker with a calendar popover, month navigation, today highlight, min/max constraints, and controlled or uncontrolled modes.',
|
|
8
|
+
designIntent: 'DatePicker deliberately avoids native <input type="date"> to guarantee consistent cross-browser appearance that matches the Lucent token system. ' +
|
|
9
|
+
'The trigger button shows the selected date in YYYY-MM-DD format (ISO-sortable, unambiguous locale-wise) or a placeholder. ' +
|
|
10
|
+
'The calendar popover renders as a role="dialog" and closes on outside click. ' +
|
|
11
|
+
'Today is outlined rather than filled so it doesn\'t compete visually with the selected date. ' +
|
|
12
|
+
'Disabled dates (outside min/max) are grayed out and non-interactive. ' +
|
|
13
|
+
'The Calendar primitive is exported separately so DateRangePicker can compose two calendars side by side.',
|
|
14
|
+
props: [
|
|
15
|
+
{
|
|
16
|
+
name: 'value',
|
|
17
|
+
type: 'object',
|
|
18
|
+
required: false,
|
|
19
|
+
description: 'Controlled selected Date. When provided the component is fully controlled.',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'defaultValue',
|
|
23
|
+
type: 'object',
|
|
24
|
+
required: false,
|
|
25
|
+
description: 'Initial selected Date for uncontrolled usage.',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'onChange',
|
|
29
|
+
type: 'function',
|
|
30
|
+
required: false,
|
|
31
|
+
description: 'Called with the newly selected Date when the user picks a day.',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'placeholder',
|
|
35
|
+
type: 'string',
|
|
36
|
+
required: false,
|
|
37
|
+
default: '"Pick a date"',
|
|
38
|
+
description: 'Trigger button text when no date is selected.',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'disabled',
|
|
42
|
+
type: 'boolean',
|
|
43
|
+
required: false,
|
|
44
|
+
default: 'false',
|
|
45
|
+
description: 'Disables the trigger button and all interaction.',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'min',
|
|
49
|
+
type: 'object',
|
|
50
|
+
required: false,
|
|
51
|
+
description: 'Earliest selectable Date. Days before this are grayed out.',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'max',
|
|
55
|
+
type: 'object',
|
|
56
|
+
required: false,
|
|
57
|
+
description: 'Latest selectable Date. Days after this are grayed out.',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'style',
|
|
61
|
+
type: 'object',
|
|
62
|
+
required: false,
|
|
63
|
+
description: 'Inline style overrides for the outer wrapper.',
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
usageExamples: [
|
|
67
|
+
{
|
|
68
|
+
title: 'Uncontrolled',
|
|
69
|
+
code: `<DatePicker onChange={(d) => console.log(d)} />`,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
title: 'Controlled with constraints',
|
|
73
|
+
code: `const [date, setDate] = useState<Date>();
|
|
74
|
+
<DatePicker
|
|
75
|
+
value={date}
|
|
76
|
+
onChange={setDate}
|
|
77
|
+
min={new Date()}
|
|
78
|
+
placeholder="Select a future date"
|
|
79
|
+
/>`,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
compositionGraph: [
|
|
83
|
+
{ componentId: 'text', componentName: 'Text', role: 'Month/year header and weekday labels', required: true },
|
|
84
|
+
],
|
|
85
|
+
accessibility: {
|
|
86
|
+
role: 'dialog',
|
|
87
|
+
ariaAttributes: ['aria-haspopup', 'aria-expanded', 'aria-label', 'aria-pressed'],
|
|
88
|
+
keyboardInteractions: ['Enter/Space to open calendar', 'Click day to select', 'Escape closes popover (click outside)'],
|
|
89
|
+
notes: 'The calendar popover is role="dialog". Each day button has aria-label with the full date and aria-pressed for selected state. Full arrow-key navigation within the calendar grid is a planned enhancement.',
|
|
90
|
+
},
|
|
91
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export const COMPONENT_MANIFEST = {
|
|
2
|
+
id: 'date-range-picker',
|
|
3
|
+
name: 'DateRangePicker',
|
|
4
|
+
tier: 'molecule',
|
|
5
|
+
domain: 'neutral',
|
|
6
|
+
specVersion: '1.0',
|
|
7
|
+
description: 'A two-calendar date range picker. First click sets the start date, second click sets the end; the selected interval is highlighted across both calendars.',
|
|
8
|
+
designIntent: 'DateRangePicker composes two Calendar primitives from DatePicker side by side, advancing in lockstep (left = current month, right = next month). ' +
|
|
9
|
+
'Selection is a two-click flow: first click anchors the start, a hint appears ("Now pick the end date"), second click resolves the range with automatic start/end ordering so users can click in either direction. ' +
|
|
10
|
+
'The highlight range (accent-subtle background) spans both calendars to give clear visual feedback for the selected interval. ' +
|
|
11
|
+
'Navigation (prev/next month) advances both calendars together to maintain the one-month-apart constraint.',
|
|
12
|
+
props: [
|
|
13
|
+
{
|
|
14
|
+
name: 'value',
|
|
15
|
+
type: 'object',
|
|
16
|
+
required: false,
|
|
17
|
+
description: 'Controlled DateRange { start: Date; end: Date }. When provided the component is fully controlled.',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'defaultValue',
|
|
21
|
+
type: 'object',
|
|
22
|
+
required: false,
|
|
23
|
+
description: 'Initial DateRange for uncontrolled usage.',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'onChange',
|
|
27
|
+
type: 'function',
|
|
28
|
+
required: false,
|
|
29
|
+
description: 'Called with the completed DateRange after the user picks both start and end.',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'placeholder',
|
|
33
|
+
type: 'string',
|
|
34
|
+
required: false,
|
|
35
|
+
default: '"Pick a date range"',
|
|
36
|
+
description: 'Trigger button text when no range is selected.',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'disabled',
|
|
40
|
+
type: 'boolean',
|
|
41
|
+
required: false,
|
|
42
|
+
default: 'false',
|
|
43
|
+
description: 'Disables the trigger button and all interaction.',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'min',
|
|
47
|
+
type: 'object',
|
|
48
|
+
required: false,
|
|
49
|
+
description: 'Earliest selectable Date.',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'max',
|
|
53
|
+
type: 'object',
|
|
54
|
+
required: false,
|
|
55
|
+
description: 'Latest selectable Date.',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'style',
|
|
59
|
+
type: 'object',
|
|
60
|
+
required: false,
|
|
61
|
+
description: 'Inline style overrides for the outer wrapper.',
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
usageExamples: [
|
|
65
|
+
{
|
|
66
|
+
title: 'Uncontrolled',
|
|
67
|
+
code: `<DateRangePicker onChange={({ start, end }) => console.log(start, end)} />`,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
title: 'Controlled',
|
|
71
|
+
code: `const [range, setRange] = useState<DateRange>();
|
|
72
|
+
<DateRangePicker value={range} onChange={setRange} min={new Date()} />`,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
compositionGraph: [
|
|
76
|
+
{ componentId: 'date-picker', componentName: 'DatePicker', role: 'Calendar primitive (two instances, left and right)', required: true },
|
|
77
|
+
{ componentId: 'text', componentName: 'Text', role: 'Mid-selection hint and calendar headers', required: true },
|
|
78
|
+
],
|
|
79
|
+
accessibility: {
|
|
80
|
+
role: 'dialog',
|
|
81
|
+
ariaAttributes: ['aria-haspopup', 'aria-expanded', 'aria-label', 'aria-pressed'],
|
|
82
|
+
keyboardInteractions: ['Enter/Space to open', 'Click first day to set start', 'Click second day to set end', 'Escape/click outside to cancel'],
|
|
83
|
+
notes: 'Inherits Calendar accessibility from DatePicker. The two-step selection flow is reinforced with a visible "Now pick the end date" hint.',
|
|
84
|
+
},
|
|
85
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export const COMPONENT_MANIFEST = {
|
|
2
|
+
id: 'file-upload',
|
|
3
|
+
name: 'FileUpload',
|
|
4
|
+
tier: 'molecule',
|
|
5
|
+
domain: 'neutral',
|
|
6
|
+
specVersion: '1.0',
|
|
7
|
+
description: 'A drag-and-drop file upload zone with a file list, per-file progress bars, error display, and size/type validation.',
|
|
8
|
+
designIntent: 'FileUpload separates concerns between the drop zone (entry point) and the file list (status display). ' +
|
|
9
|
+
'The drop zone uses a dashed border and an upload arrow icon to communicate droppability without words. ' +
|
|
10
|
+
'Progress is modelled as a field on UploadFile rather than as a callback so the parent controls upload logic — this component is purely presentational for the upload state. ' +
|
|
11
|
+
'The progress bar turns success-green at 100% to give clear completion feedback. ' +
|
|
12
|
+
'Errors are shown inline on each file row (not as a toast) so the user knows exactly which file failed and why. ' +
|
|
13
|
+
'The hidden <input type="file"> is triggered programmatically on click/keyboard so the drop zone can have a fully custom appearance.',
|
|
14
|
+
props: [
|
|
15
|
+
{
|
|
16
|
+
name: 'accept',
|
|
17
|
+
type: 'string',
|
|
18
|
+
required: false,
|
|
19
|
+
description: 'Accepted MIME types or extensions passed to the file input, e.g. "image/*,.pdf".',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'multiple',
|
|
23
|
+
type: 'boolean',
|
|
24
|
+
required: false,
|
|
25
|
+
default: 'false',
|
|
26
|
+
description: 'Allow selecting multiple files at once.',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'maxSize',
|
|
30
|
+
type: 'number',
|
|
31
|
+
required: false,
|
|
32
|
+
description: 'Maximum file size in bytes. Files exceeding this trigger onError and are not added.',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'value',
|
|
36
|
+
type: 'array',
|
|
37
|
+
required: false,
|
|
38
|
+
description: 'Controlled array of UploadFile objects. Each has id, file (File), optional progress (0–100), and optional error string.',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'onChange',
|
|
42
|
+
type: 'function',
|
|
43
|
+
required: false,
|
|
44
|
+
description: 'Called with the updated UploadFile array after files are added or removed.',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'onError',
|
|
48
|
+
type: 'function',
|
|
49
|
+
required: false,
|
|
50
|
+
description: 'Called with an error message string when a file fails validation (e.g. exceeds maxSize).',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'disabled',
|
|
54
|
+
type: 'boolean',
|
|
55
|
+
required: false,
|
|
56
|
+
default: 'false',
|
|
57
|
+
description: 'Disables the drop zone and all file interaction.',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'style',
|
|
61
|
+
type: 'object',
|
|
62
|
+
required: false,
|
|
63
|
+
description: 'Inline style overrides for the outer wrapper.',
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
usageExamples: [
|
|
67
|
+
{
|
|
68
|
+
title: 'Uncontrolled multi-file upload',
|
|
69
|
+
code: `<FileUpload
|
|
70
|
+
multiple
|
|
71
|
+
accept="image/*,.pdf"
|
|
72
|
+
maxSize={5 * 1024 * 1024}
|
|
73
|
+
onError={(msg) => toast.error(msg)}
|
|
74
|
+
onChange={(files) => console.log(files)}
|
|
75
|
+
/>`,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
title: 'Controlled with progress',
|
|
79
|
+
code: `const [files, setFiles] = useState<UploadFile[]>([]);
|
|
80
|
+
|
|
81
|
+
const handleChange = async (updated: UploadFile[]) => {
|
|
82
|
+
setFiles(updated);
|
|
83
|
+
for (const f of updated.filter(f => f.progress === undefined)) {
|
|
84
|
+
// simulate upload
|
|
85
|
+
for (let p = 0; p <= 100; p += 10) {
|
|
86
|
+
await delay(100);
|
|
87
|
+
setFiles(prev => prev.map(x => x.id === f.id ? { ...x, progress: p } : x));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
<FileUpload value={files} onChange={handleChange} multiple />`,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
compositionGraph: [
|
|
96
|
+
{ componentId: 'text', componentName: 'Text', role: 'Drop zone label, file name, file size, and error messages', required: true },
|
|
97
|
+
],
|
|
98
|
+
accessibility: {
|
|
99
|
+
role: 'button',
|
|
100
|
+
ariaAttributes: ['aria-label', 'aria-disabled'],
|
|
101
|
+
keyboardInteractions: ['Enter/Space to open file picker', 'Tab to focus drop zone'],
|
|
102
|
+
notes: 'The drop zone has role="button" with tabIndex=0 and responds to Enter/Space. Remove buttons on file rows have aria-label including the filename.',
|
|
103
|
+
},
|
|
104
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export const COMPONENT_MANIFEST = {
|
|
2
|
+
id: 'multi-select',
|
|
3
|
+
name: 'MultiSelect',
|
|
4
|
+
tier: 'molecule',
|
|
5
|
+
domain: 'neutral',
|
|
6
|
+
specVersion: '1.0',
|
|
7
|
+
description: 'A tag-based multi-option selector with inline search, keyboard navigation, and an optional selection cap.',
|
|
8
|
+
designIntent: 'MultiSelect renders selected values as removable tags inside the trigger area, giving immediate visual feedback on what is selected. ' +
|
|
9
|
+
'The search input sits inline with the tags so there is no separate search field to discover. ' +
|
|
10
|
+
'Backspace with an empty query removes the last selected tag — a standard UX pattern in multi-select inputs. ' +
|
|
11
|
+
'The max prop caps selection without disabling the entire component: unselected options beyond the cap are grayed out and show a hint. ' +
|
|
12
|
+
'The dropdown closes on outside click, Escape, or when focus leaves. ' +
|
|
13
|
+
'Checkbox indicators in the dropdown make the selected state scannable at a glance without relying solely on background color.',
|
|
14
|
+
props: [
|
|
15
|
+
{
|
|
16
|
+
name: 'options',
|
|
17
|
+
type: 'array',
|
|
18
|
+
required: true,
|
|
19
|
+
description: 'Array of { value, label, disabled? } option objects.',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'value',
|
|
23
|
+
type: 'array',
|
|
24
|
+
required: false,
|
|
25
|
+
description: 'Controlled array of selected values.',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'defaultValue',
|
|
29
|
+
type: 'array',
|
|
30
|
+
required: false,
|
|
31
|
+
default: '[]',
|
|
32
|
+
description: 'Initial selected values for uncontrolled usage.',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'onChange',
|
|
36
|
+
type: 'function',
|
|
37
|
+
required: false,
|
|
38
|
+
description: 'Called with the updated array of selected values on each change.',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'placeholder',
|
|
42
|
+
type: 'string',
|
|
43
|
+
required: false,
|
|
44
|
+
default: '"Select…"',
|
|
45
|
+
description: 'Placeholder shown when nothing is selected.',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'disabled',
|
|
49
|
+
type: 'boolean',
|
|
50
|
+
required: false,
|
|
51
|
+
default: 'false',
|
|
52
|
+
description: 'Disables interaction and dims the component.',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'max',
|
|
56
|
+
type: 'number',
|
|
57
|
+
required: false,
|
|
58
|
+
description: 'Maximum number of selectable options. Unselected options beyond the cap are grayed out.',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'style',
|
|
62
|
+
type: 'object',
|
|
63
|
+
required: false,
|
|
64
|
+
description: 'Inline style overrides for the outer wrapper.',
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
usageExamples: [
|
|
68
|
+
{
|
|
69
|
+
title: 'Uncontrolled',
|
|
70
|
+
code: `<MultiSelect
|
|
71
|
+
options={[
|
|
72
|
+
{ value: 'react', label: 'React' },
|
|
73
|
+
{ value: 'vue', label: 'Vue' },
|
|
74
|
+
{ value: 'svelte', label: 'Svelte' },
|
|
75
|
+
]}
|
|
76
|
+
placeholder="Select frameworks…"
|
|
77
|
+
/>`,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
title: 'Controlled with max',
|
|
81
|
+
code: `const [tags, setTags] = useState<string[]>([]);
|
|
82
|
+
<MultiSelect
|
|
83
|
+
options={allTags}
|
|
84
|
+
value={tags}
|
|
85
|
+
onChange={setTags}
|
|
86
|
+
max={3}
|
|
87
|
+
placeholder="Up to 3 tags"
|
|
88
|
+
/>`,
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
compositionGraph: [
|
|
92
|
+
{ componentId: 'text', componentName: 'Text', role: 'Option labels, empty state, and max hint', required: true },
|
|
93
|
+
],
|
|
94
|
+
accessibility: {
|
|
95
|
+
role: 'combobox',
|
|
96
|
+
ariaAttributes: ['aria-autocomplete', 'aria-controls', 'aria-expanded', 'aria-multiselectable', 'aria-selected', 'aria-disabled'],
|
|
97
|
+
keyboardInteractions: ['↑↓ to navigate options', 'Enter to toggle selection', 'Escape to close', 'Backspace to remove last tag'],
|
|
98
|
+
notes: 'The input carries role="combobox" with aria-expanded and aria-controls pointing to the listbox. Each option has role="option" with aria-selected. Remove buttons on tags have descriptive aria-label.',
|
|
99
|
+
},
|
|
100
|
+
};
|