lucent-ui 0.1.0

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.
Files changed (33) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +103 -0
  3. package/dist/index.cjs +112 -0
  4. package/dist/index.d.ts +687 -0
  5. package/dist/index.js +3227 -0
  6. package/dist-server/server/index.js +111 -0
  7. package/dist-server/server/registry.js +48 -0
  8. package/dist-server/src/components/atoms/Avatar/Avatar.manifest.js +29 -0
  9. package/dist-server/src/components/atoms/Badge/Badge.manifest.js +29 -0
  10. package/dist-server/src/components/atoms/Button/Button.manifest.js +2 -0
  11. package/dist-server/src/components/atoms/Checkbox/Checkbox.manifest.js +74 -0
  12. package/dist-server/src/components/atoms/Divider/Divider.manifest.js +25 -0
  13. package/dist-server/src/components/atoms/Icon/Icon.manifest.js +54 -0
  14. package/dist-server/src/components/atoms/Input/Input.manifest.js +36 -0
  15. package/dist-server/src/components/atoms/Radio/Radio.manifest.js +82 -0
  16. package/dist-server/src/components/atoms/Select/Select.manifest.js +103 -0
  17. package/dist-server/src/components/atoms/Spinner/Spinner.manifest.js +27 -0
  18. package/dist-server/src/components/atoms/Tag/Tag.manifest.js +62 -0
  19. package/dist-server/src/components/atoms/Text/Text.manifest.js +106 -0
  20. package/dist-server/src/components/atoms/Textarea/Textarea.manifest.js +35 -0
  21. package/dist-server/src/components/atoms/Toggle/Toggle.manifest.js +67 -0
  22. package/dist-server/src/components/atoms/Tooltip/Tooltip.manifest.js +54 -0
  23. package/dist-server/src/components/molecules/Alert/Alert.manifest.js +81 -0
  24. package/dist-server/src/components/molecules/Card/Card.manifest.js +92 -0
  25. package/dist-server/src/components/molecules/EmptyState/EmptyState.manifest.js +79 -0
  26. package/dist-server/src/components/molecules/FormField/FormField.manifest.js +93 -0
  27. package/dist-server/src/components/molecules/SearchInput/SearchInput.manifest.js +102 -0
  28. package/dist-server/src/components/molecules/Skeleton/Skeleton.manifest.js +93 -0
  29. package/dist-server/src/manifest/examples/button.manifest.js +116 -0
  30. package/dist-server/src/manifest/index.js +3 -0
  31. package/dist-server/src/manifest/types.js +1 -0
  32. package/dist-server/src/manifest/validate.js +102 -0
  33. package/package.json +58 -0
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { z } from 'zod';
5
+ import { ALL_MANIFESTS } from './registry.js';
6
+ // ─── Auth stub ───────────────────────────────────────────────────────────────
7
+ // LUCENT_API_KEY is reserved for the future paid tier.
8
+ // When set, the server acknowledges it but does not yet enforce it.
9
+ const apiKey = process.env['LUCENT_API_KEY'];
10
+ if (apiKey) {
11
+ process.stderr.write('[lucent-mcp] Auth mode active (LUCENT_API_KEY is set).\n');
12
+ }
13
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
14
+ function findManifest(nameOrId) {
15
+ const q = nameOrId.trim().toLowerCase();
16
+ return ALL_MANIFESTS.find((m) => m.id.toLowerCase() === q || m.name.toLowerCase() === q);
17
+ }
18
+ function scoreManifest(m, query) {
19
+ const q = query.toLowerCase();
20
+ let score = 0;
21
+ if (m.name.toLowerCase().includes(q))
22
+ score += 10;
23
+ if (m.id.toLowerCase().includes(q))
24
+ score += 8;
25
+ if (m.tier.toLowerCase().includes(q))
26
+ score += 5;
27
+ if (m.description.toLowerCase().includes(q))
28
+ score += 4;
29
+ if (m.designIntent.toLowerCase().includes(q))
30
+ score += 3;
31
+ for (const p of m.props) {
32
+ if (p.name.toLowerCase().includes(q))
33
+ score += 2;
34
+ if (p.description.toLowerCase().includes(q))
35
+ score += 1;
36
+ }
37
+ return score;
38
+ }
39
+ // ─── MCP Server ───────────────────────────────────────────────────────────────
40
+ const server = new McpServer({
41
+ name: 'lucent-mcp',
42
+ version: '0.1.0',
43
+ });
44
+ // Tool: list_components
45
+ server.tool('list_components', 'Lists all available Lucent UI components with their name, tier (atom/molecule), and one-line description.', {}, async () => {
46
+ const components = ALL_MANIFESTS.map((m) => ({
47
+ id: m.id,
48
+ name: m.name,
49
+ tier: m.tier,
50
+ description: m.description,
51
+ }));
52
+ return {
53
+ content: [
54
+ {
55
+ type: 'text',
56
+ text: JSON.stringify({ components }, null, 2),
57
+ },
58
+ ],
59
+ };
60
+ });
61
+ // Tool: get_component_manifest
62
+ server.tool('get_component_manifest', 'Returns the full manifest JSON for a Lucent UI component, including props, usage examples, design intent, and accessibility notes.', { componentName: z.string().describe('Component name or id, e.g. "Button" or "form-field"') }, async ({ componentName }) => {
63
+ const manifest = findManifest(componentName);
64
+ if (!manifest) {
65
+ return {
66
+ content: [
67
+ {
68
+ type: 'text',
69
+ text: JSON.stringify({
70
+ error: `Component "${componentName}" not found.`,
71
+ available: ALL_MANIFESTS.map((m) => m.name),
72
+ }),
73
+ },
74
+ ],
75
+ isError: true,
76
+ };
77
+ }
78
+ return {
79
+ content: [
80
+ {
81
+ type: 'text',
82
+ text: JSON.stringify(manifest, null, 2),
83
+ },
84
+ ],
85
+ };
86
+ });
87
+ // Tool: search_components
88
+ server.tool('search_components', 'Searches Lucent UI components by description or concept. Returns matching components ranked by relevance.', { query: z.string().describe('Natural language or keyword query, e.g. "loading indicator" or "form validation"') }, async ({ query }) => {
89
+ const results = ALL_MANIFESTS
90
+ .map((m) => ({ manifest: m, score: scoreManifest(m, query) }))
91
+ .filter(({ score }) => score > 0)
92
+ .sort((a, b) => b.score - a.score)
93
+ .map(({ manifest, score }) => ({
94
+ id: manifest.id,
95
+ name: manifest.name,
96
+ tier: manifest.tier,
97
+ description: manifest.description,
98
+ score,
99
+ }));
100
+ return {
101
+ content: [
102
+ {
103
+ type: 'text',
104
+ text: JSON.stringify({ query, results }, null, 2),
105
+ },
106
+ ],
107
+ };
108
+ });
109
+ // ─── Start ────────────────────────────────────────────────────────────────────
110
+ const transport = new StdioServerTransport();
111
+ await server.connect(transport);
@@ -0,0 +1,48 @@
1
+ // Atoms
2
+ import { COMPONENT_MANIFEST as Avatar } from '../src/components/atoms/Avatar/Avatar.manifest.js';
3
+ import { COMPONENT_MANIFEST as Badge } from '../src/components/atoms/Badge/Badge.manifest.js';
4
+ import { COMPONENT_MANIFEST as Button } from '../src/components/atoms/Button/Button.manifest.js';
5
+ import { COMPONENT_MANIFEST as Checkbox } from '../src/components/atoms/Checkbox/Checkbox.manifest.js';
6
+ import { COMPONENT_MANIFEST as Divider } from '../src/components/atoms/Divider/Divider.manifest.js';
7
+ import { COMPONENT_MANIFEST as Icon } from '../src/components/atoms/Icon/Icon.manifest.js';
8
+ import { COMPONENT_MANIFEST as Input } from '../src/components/atoms/Input/Input.manifest.js';
9
+ import { COMPONENT_MANIFEST as Radio } from '../src/components/atoms/Radio/Radio.manifest.js';
10
+ import { COMPONENT_MANIFEST as Select } from '../src/components/atoms/Select/Select.manifest.js';
11
+ import { COMPONENT_MANIFEST as Spinner } from '../src/components/atoms/Spinner/Spinner.manifest.js';
12
+ import { COMPONENT_MANIFEST as Tag } from '../src/components/atoms/Tag/Tag.manifest.js';
13
+ import { COMPONENT_MANIFEST as Text } from '../src/components/atoms/Text/Text.manifest.js';
14
+ import { COMPONENT_MANIFEST as Textarea } from '../src/components/atoms/Textarea/Textarea.manifest.js';
15
+ import { COMPONENT_MANIFEST as Toggle } from '../src/components/atoms/Toggle/Toggle.manifest.js';
16
+ import { COMPONENT_MANIFEST as Tooltip } from '../src/components/atoms/Tooltip/Tooltip.manifest.js';
17
+ // Molecules
18
+ import { COMPONENT_MANIFEST as Alert } from '../src/components/molecules/Alert/Alert.manifest.js';
19
+ import { COMPONENT_MANIFEST as Card } from '../src/components/molecules/Card/Card.manifest.js';
20
+ import { COMPONENT_MANIFEST as EmptyState } from '../src/components/molecules/EmptyState/EmptyState.manifest.js';
21
+ import { COMPONENT_MANIFEST as FormField } from '../src/components/molecules/FormField/FormField.manifest.js';
22
+ import { COMPONENT_MANIFEST as SearchInput } from '../src/components/molecules/SearchInput/SearchInput.manifest.js';
23
+ import { COMPONENT_MANIFEST as Skeleton } from '../src/components/molecules/Skeleton/Skeleton.manifest.js';
24
+ export const ALL_MANIFESTS = [
25
+ // Atoms
26
+ Avatar,
27
+ Badge,
28
+ Button,
29
+ Checkbox,
30
+ Divider,
31
+ Icon,
32
+ Input,
33
+ Radio,
34
+ Select,
35
+ Spinner,
36
+ Tag,
37
+ Text,
38
+ Textarea,
39
+ Toggle,
40
+ Tooltip,
41
+ // Molecules
42
+ Alert,
43
+ Card,
44
+ EmptyState,
45
+ FormField,
46
+ SearchInput,
47
+ Skeleton,
48
+ ];
@@ -0,0 +1,29 @@
1
+ export const COMPONENT_MANIFEST = {
2
+ id: 'avatar',
3
+ name: 'Avatar',
4
+ tier: 'atom',
5
+ domain: 'neutral',
6
+ specVersion: '0.1',
7
+ description: 'A circular user image with initials fallback.',
8
+ designIntent: 'Always provide alt for accessibility — it is used to derive initials automatically when src is absent or fails. ' +
9
+ 'Use initials prop to override auto-derived initials (e.g. for non-Latin names). ' +
10
+ 'Size xs/sm suit table rows and compact lists; md is the default for comment threads; lg/xl for profile headers.',
11
+ props: [
12
+ { name: 'src', type: 'string', required: false, description: 'Image URL. Falls back to initials if omitted or fails to load.' },
13
+ { name: 'alt', type: 'string', required: true, description: 'Alt text and source for auto-derived initials.' },
14
+ { name: 'size', type: 'enum', required: false, default: 'md', description: 'Diameter of the avatar.', enumValues: ['xs', 'sm', 'md', 'lg', 'xl'] },
15
+ { name: 'initials', type: 'string', required: false, description: 'Override auto-derived initials (max 2 characters).' },
16
+ ],
17
+ usageExamples: [
18
+ { title: 'With image', code: `<Avatar src="/avatars/jane.jpg" alt="Jane Doe" />` },
19
+ { title: 'Initials fallback', code: `<Avatar alt="Jane Doe" />` },
20
+ { title: 'Large profile', code: `<Avatar src={user.avatar} alt={user.name} size="lg" />` },
21
+ { title: 'Custom initials', code: `<Avatar alt="张伟" initials="张" size="md" />` },
22
+ ],
23
+ compositionGraph: [],
24
+ accessibility: {
25
+ role: 'img',
26
+ ariaAttributes: ['aria-label'],
27
+ notes: 'When src is present, renders as <img> with alt. When showing initials, renders as <span role="img" aria-label>.',
28
+ },
29
+ };
@@ -0,0 +1,29 @@
1
+ export const COMPONENT_MANIFEST = {
2
+ id: 'badge',
3
+ name: 'Badge',
4
+ tier: 'atom',
5
+ domain: 'neutral',
6
+ specVersion: '0.1',
7
+ description: 'A small inline label for status, count, or category.',
8
+ designIntent: 'Badges communicate status or category at a glance. Match variant to semantic meaning — ' +
9
+ 'never use "danger" for non-critical states or "success" for neutral counts. ' +
10
+ 'Use dot=true when a single colour indicator is enough context (e.g. online status). ' +
11
+ 'Keep badge text short: 1–3 words maximum.',
12
+ props: [
13
+ { name: 'variant', type: 'enum', required: false, default: 'neutral', description: 'Colour scheme conveying semantic meaning.', enumValues: ['neutral', 'success', 'warning', 'danger', 'info', 'accent'] },
14
+ { name: 'size', type: 'enum', required: false, default: 'md', description: 'Controls height and font size.', enumValues: ['sm', 'md'] },
15
+ { name: 'dot', type: 'boolean', required: false, default: 'false', description: 'Prepends a coloured dot indicator.' },
16
+ { name: 'children', type: 'ReactNode', required: true, description: 'Badge label.' },
17
+ ],
18
+ usageExamples: [
19
+ { title: 'Status', code: `<Badge variant="success" dot>Active</Badge>` },
20
+ { title: 'Count', code: `<Badge variant="danger">12</Badge>` },
21
+ { title: 'Category', code: `<Badge variant="info">Beta</Badge>` },
22
+ { title: 'Neutral tag', code: `<Badge>Draft</Badge>` },
23
+ ],
24
+ compositionGraph: [],
25
+ accessibility: {
26
+ role: 'status',
27
+ notes: 'Use aria-label on the parent element when badge meaning depends on context.',
28
+ },
29
+ };
@@ -0,0 +1,2 @@
1
+ // Re-export the canonical manifest from the manifest package
2
+ export { ButtonManifest as COMPONENT_MANIFEST } from '../../../manifest/examples/button.manifest.js';
@@ -0,0 +1,74 @@
1
+ export const COMPONENT_MANIFEST = {
2
+ id: 'checkbox',
3
+ name: 'Checkbox',
4
+ tier: 'atom',
5
+ domain: 'neutral',
6
+ specVersion: '0.1',
7
+ description: 'A binary selection control for boolean values or multi-select lists.',
8
+ designIntent: 'Checkboxes represent independent boolean choices — they do not affect each other. ' +
9
+ 'Use a Checkbox for settings that take effect immediately (e.g. "Remember me") or for ' +
10
+ 'selecting multiple items from a list. When only one option may be active at a time, use ' +
11
+ 'Radio instead. The indeterminate state communicates a "select all" parent whose children ' +
12
+ 'are partially checked — never use it for a third logical state.',
13
+ props: [
14
+ {
15
+ name: 'checked',
16
+ type: 'boolean',
17
+ required: false,
18
+ description: 'Controlled checked state. Pair with onChange for controlled usage.',
19
+ },
20
+ {
21
+ name: 'defaultChecked',
22
+ type: 'boolean',
23
+ required: false,
24
+ default: 'false',
25
+ description: 'Initial checked state for uncontrolled usage.',
26
+ },
27
+ {
28
+ name: 'onChange',
29
+ type: 'function',
30
+ required: false,
31
+ description: 'Called when the checked state changes.',
32
+ },
33
+ {
34
+ name: 'label',
35
+ type: 'string',
36
+ required: false,
37
+ description: 'Visible label rendered beside the checkbox.',
38
+ },
39
+ {
40
+ name: 'indeterminate',
41
+ type: 'boolean',
42
+ required: false,
43
+ default: 'false',
44
+ description: 'Displays a dash to indicate a partially-checked parent state.',
45
+ },
46
+ {
47
+ name: 'disabled',
48
+ type: 'boolean',
49
+ required: false,
50
+ default: 'false',
51
+ description: 'Prevents interaction and dims the control.',
52
+ },
53
+ {
54
+ name: 'size',
55
+ type: 'enum',
56
+ required: false,
57
+ default: 'md',
58
+ description: 'Size of the checkbox box.',
59
+ enumValues: ['sm', 'md'],
60
+ },
61
+ ],
62
+ usageExamples: [
63
+ { title: 'Controlled', code: `<Checkbox checked={agreed} onChange={e => setAgreed(e.target.checked)} label="I agree to the terms" />` },
64
+ { title: 'Uncontrolled', code: `<Checkbox defaultChecked label="Send me updates" />` },
65
+ { title: 'Indeterminate', code: `<Checkbox indeterminate label="Select all" />` },
66
+ { title: 'Disabled', code: `<Checkbox disabled label="Unavailable option" />` },
67
+ ],
68
+ compositionGraph: [],
69
+ accessibility: {
70
+ role: 'checkbox',
71
+ ariaAttributes: ['aria-checked', 'aria-disabled'],
72
+ keyboardInteractions: ['Space — toggles checked state'],
73
+ },
74
+ };
@@ -0,0 +1,25 @@
1
+ export const COMPONENT_MANIFEST = {
2
+ id: 'divider',
3
+ name: 'Divider',
4
+ tier: 'atom',
5
+ domain: 'neutral',
6
+ specVersion: '0.1',
7
+ description: 'A visual separator between content sections, horizontal or vertical.',
8
+ designIntent: 'Use horizontal Divider to separate sections in a layout. Use vertical Divider inline between sibling elements (e.g. nav links, toolbar buttons). ' +
9
+ 'Use the label prop for "OR" separators in auth flows or form sections — never use a plain text node next to a divider for this.',
10
+ props: [
11
+ { name: 'orientation', type: 'enum', required: false, default: 'horizontal', description: 'Direction of the divider line.', enumValues: ['horizontal', 'vertical'] },
12
+ { name: 'label', type: 'string', required: false, description: 'Optional centered label (horizontal only). Common use: "OR", "AND", section titles.' },
13
+ { name: 'spacing', type: 'string', required: false, default: 'var(--lucent-space-4)', description: 'Margin on the axis perpendicular to the line.' },
14
+ ],
15
+ usageExamples: [
16
+ { title: 'Section separator', code: `<Divider />` },
17
+ { title: 'With label', code: `<Divider label="OR" />` },
18
+ { title: 'Vertical in nav', code: `<nav style={{ display: 'flex', alignItems: 'center' }}><a>Home</a><Divider orientation="vertical" /><a>About</a></nav>` },
19
+ ],
20
+ compositionGraph: [],
21
+ accessibility: {
22
+ role: 'separator',
23
+ ariaAttributes: ['aria-orientation', 'aria-label'],
24
+ },
25
+ };
@@ -0,0 +1,54 @@
1
+ export const COMPONENT_MANIFEST = {
2
+ id: 'icon',
3
+ name: 'Icon',
4
+ tier: 'atom',
5
+ domain: 'neutral',
6
+ specVersion: '0.1',
7
+ description: 'A sized, accessible wrapper for any SVG or icon-set component.',
8
+ designIntent: 'Icon is intentionally icon-set agnostic — pass any SVG or third-party icon as children ' +
9
+ 'and it handles sizing and accessibility automatically. Use label for standalone icons that ' +
10
+ 'convey meaning without adjacent text (e.g. a close button). Omit label for decorative ' +
11
+ 'icons next to text, where the text already provides context. colour inherits from the ' +
12
+ 'parent by default via currentColor — override only when the icon must differ from its ' +
13
+ 'surrounding text.',
14
+ props: [
15
+ {
16
+ name: 'children',
17
+ type: 'ReactNode',
18
+ required: true,
19
+ description: 'The icon to render — any SVG element or icon-set component.',
20
+ },
21
+ {
22
+ name: 'size',
23
+ type: 'enum',
24
+ required: false,
25
+ default: 'md',
26
+ description: 'Constrains the icon to a fixed square (xs=12, sm=14, md=16, lg=20, xl=24).',
27
+ enumValues: ['xs', 'sm', 'md', 'lg', 'xl'],
28
+ },
29
+ {
30
+ name: 'label',
31
+ type: 'string',
32
+ required: false,
33
+ description: 'Accessible label (aria-label). Provide for meaningful standalone icons; omit for decorative ones.',
34
+ },
35
+ {
36
+ name: 'color',
37
+ type: 'string',
38
+ required: false,
39
+ description: 'CSS colour value. Defaults to currentColor (inherits from parent).',
40
+ },
41
+ ],
42
+ usageExamples: [
43
+ { title: 'Decorative (next to text)', code: `<Icon size="md"><SearchIcon /></Icon>` },
44
+ { title: 'Meaningful (standalone)', code: `<Icon size="lg" label="Close dialog"><XIcon /></Icon>` },
45
+ { title: 'Coloured', code: `<Icon color="var(--lucent-danger-default)"><AlertIcon /></Icon>` },
46
+ { title: 'Inside a button', code: `<Button leftIcon={<Icon size="sm"><PlusIcon /></Icon>}>Add item</Button>` },
47
+ ],
48
+ compositionGraph: [],
49
+ accessibility: {
50
+ role: 'img',
51
+ ariaAttributes: ['aria-label', 'aria-hidden'],
52
+ notes: 'aria-hidden="true" is applied automatically when no label is given, hiding the icon from screen readers.',
53
+ },
54
+ };
@@ -0,0 +1,36 @@
1
+ export const COMPONENT_MANIFEST = {
2
+ id: 'input',
3
+ name: 'Input',
4
+ tier: 'atom',
5
+ domain: 'neutral',
6
+ specVersion: '0.1',
7
+ description: 'A single-line text field with optional label, helper text, and error state.',
8
+ designIntent: 'Always pair with a visible label — never rely on placeholder text alone as it ' +
9
+ 'disappears on input and is inaccessible. Use errorText (not helperText) to surface ' +
10
+ 'validation failures; the component applies danger styling automatically. ' +
11
+ 'leftElement and rightElement accept icons or small controls (e.g. currency symbol, clear button).',
12
+ props: [
13
+ { name: 'type', type: 'enum', required: false, default: 'text', description: 'HTML input type.', enumValues: ['text', 'number', 'password', 'email', 'tel', 'url', 'search'] },
14
+ { name: 'label', type: 'string', required: false, description: 'Visible label rendered above the input.' },
15
+ { name: 'helperText', type: 'string', required: false, description: 'Supplementary hint shown below the input.' },
16
+ { name: 'errorText', type: 'string', required: false, description: 'Validation error message. When set, input renders in error state.' },
17
+ { name: 'leftElement', type: 'ReactNode', required: false, description: 'Icon or adornment rendered inside the left edge.' },
18
+ { name: 'rightElement', type: 'ReactNode', required: false, description: 'Icon or adornment rendered inside the right edge.' },
19
+ { name: 'placeholder', type: 'string', required: false, description: 'Placeholder text. Use as a hint, not a label.' },
20
+ { name: 'disabled', type: 'boolean', required: false, default: 'false', description: 'Disables the input.' },
21
+ { name: 'value', type: 'string', required: false, description: 'Controlled value.' },
22
+ { name: 'onChange', type: 'function', required: false, description: 'Change handler.' },
23
+ ],
24
+ usageExamples: [
25
+ { title: 'Basic', code: `<Input label="Email" type="email" placeholder="you@example.com" />` },
26
+ { title: 'With helper text', code: `<Input label="Username" helperText="3–20 characters, letters and numbers only" />` },
27
+ { title: 'Error state', code: `<Input label="Password" type="password" value={value} errorText="Must be at least 8 characters" />` },
28
+ { title: 'With icon', code: `<Input label="Search" leftElement={<SearchIcon />} placeholder="Search…" />` },
29
+ ],
30
+ compositionGraph: [],
31
+ accessibility: {
32
+ role: 'textbox',
33
+ ariaAttributes: ['aria-invalid', 'aria-describedby', 'aria-label'],
34
+ keyboardInteractions: ['Tab — focuses the input'],
35
+ },
36
+ };
@@ -0,0 +1,82 @@
1
+ export const COMPONENT_MANIFEST = {
2
+ id: 'radio',
3
+ name: 'Radio',
4
+ tier: 'atom',
5
+ domain: 'neutral',
6
+ specVersion: '0.1',
7
+ description: 'A mutually exclusive selection control. Use RadioGroup to manage a set of options.',
8
+ designIntent: 'Radio buttons enforce a single selection from a small set of options (typically 2–6). ' +
9
+ 'Always wrap Radio in a RadioGroup so name and selection state are shared automatically. ' +
10
+ 'For larger option sets (7+) prefer a Select. For independent true/false choices, use a ' +
11
+ 'Checkbox. RadioGroup orientation should match how the options relate — vertical for ' +
12
+ 'distinct choices, horizontal for brief inline options (e.g. Yes / No).',
13
+ props: [
14
+ {
15
+ name: 'value',
16
+ type: 'string',
17
+ required: true,
18
+ description: 'The value submitted when this radio is selected.',
19
+ },
20
+ {
21
+ name: 'label',
22
+ type: 'string',
23
+ required: false,
24
+ description: 'Visible label rendered beside the radio button.',
25
+ },
26
+ {
27
+ name: 'disabled',
28
+ type: 'boolean',
29
+ required: false,
30
+ default: 'false',
31
+ description: 'Prevents interaction. Inherits group-level disabled when inside RadioGroup.',
32
+ },
33
+ {
34
+ name: 'size',
35
+ type: 'enum',
36
+ required: false,
37
+ default: 'md',
38
+ description: 'Size of the radio button circle.',
39
+ enumValues: ['sm', 'md'],
40
+ },
41
+ ],
42
+ usageExamples: [
43
+ {
44
+ title: 'Controlled RadioGroup',
45
+ code: `
46
+ <RadioGroup name="plan" value={plan} onChange={setPlan}>
47
+ <Radio value="free" label="Free" />
48
+ <Radio value="pro" label="Pro" />
49
+ <Radio value="enterprise" label="Enterprise" />
50
+ </RadioGroup>`.trim(),
51
+ },
52
+ {
53
+ title: 'Horizontal orientation',
54
+ code: `
55
+ <RadioGroup name="size" value={size} onChange={setSize} orientation="horizontal">
56
+ <Radio value="s" label="S" />
57
+ <Radio value="m" label="M" />
58
+ <Radio value="l" label="L" />
59
+ </RadioGroup>`.trim(),
60
+ },
61
+ {
62
+ title: 'Group-level disabled',
63
+ code: `
64
+ <RadioGroup name="tier" value="basic" onChange={() => {}} disabled>
65
+ <Radio value="basic" label="Basic" />
66
+ <Radio value="advanced" label="Advanced" />
67
+ </RadioGroup>`.trim(),
68
+ },
69
+ ],
70
+ compositionGraph: [
71
+ { componentId: 'radio-group', componentName: 'RadioGroup', role: 'container', required: true },
72
+ { componentId: 'radio', componentName: 'Radio', role: 'item', required: true },
73
+ ],
74
+ accessibility: {
75
+ role: 'radio',
76
+ ariaAttributes: ['aria-checked', 'aria-disabled'],
77
+ keyboardInteractions: [
78
+ 'Arrow keys — move selection within the group',
79
+ 'Space — selects the focused radio',
80
+ ],
81
+ },
82
+ };
@@ -0,0 +1,103 @@
1
+ export const COMPONENT_MANIFEST = {
2
+ id: 'select',
3
+ name: 'Select',
4
+ tier: 'atom',
5
+ domain: 'neutral',
6
+ specVersion: '0.1',
7
+ description: 'A dropdown for choosing one option from a list.',
8
+ designIntent: 'Select is best for 5–15 options where showing all at once would be too noisy. ' +
9
+ 'For fewer than 5 options, prefer RadioGroup (always-visible, faster to scan). ' +
10
+ 'For search-filtered or async options, a Combobox (Wave 3) is more appropriate. ' +
11
+ 'Always provide a placeholder when no default is sensible — this communicates the ' +
12
+ 'field is required and prevents silent zero-state submissions.',
13
+ props: [
14
+ {
15
+ name: 'options',
16
+ type: 'array',
17
+ required: true,
18
+ description: 'Array of { value, label, disabled? } option objects.',
19
+ },
20
+ {
21
+ name: 'value',
22
+ type: 'string',
23
+ required: false,
24
+ description: 'Controlled selected value. Pair with onChange.',
25
+ },
26
+ {
27
+ name: 'onChange',
28
+ type: 'function',
29
+ required: false,
30
+ description: 'Called when the selected value changes.',
31
+ },
32
+ {
33
+ name: 'placeholder',
34
+ type: 'string',
35
+ required: false,
36
+ description: 'Disabled first option shown when no value is selected.',
37
+ },
38
+ {
39
+ name: 'label',
40
+ type: 'string',
41
+ required: false,
42
+ description: 'Visible label rendered above the select.',
43
+ },
44
+ {
45
+ name: 'helperText',
46
+ type: 'string',
47
+ required: false,
48
+ description: 'Supplementary hint shown below the select.',
49
+ },
50
+ {
51
+ name: 'errorText',
52
+ type: 'string',
53
+ required: false,
54
+ description: 'Validation error message. Replaces helperText and applies error styling.',
55
+ },
56
+ {
57
+ name: 'size',
58
+ type: 'enum',
59
+ required: false,
60
+ default: 'md',
61
+ description: 'Controls the height of the select control.',
62
+ enumValues: ['sm', 'md', 'lg'],
63
+ },
64
+ {
65
+ name: 'disabled',
66
+ type: 'boolean',
67
+ required: false,
68
+ default: 'false',
69
+ description: 'Prevents interaction.',
70
+ },
71
+ ],
72
+ usageExamples: [
73
+ {
74
+ title: 'Controlled',
75
+ code: `
76
+ <Select
77
+ label="Country"
78
+ placeholder="Choose a country"
79
+ options={countries}
80
+ value={country}
81
+ onChange={e => setCountry(e.target.value)}
82
+ />`.trim(),
83
+ },
84
+ {
85
+ title: 'With validation error',
86
+ code: `<Select label="Role" options={roles} errorText="Please select a role" />`,
87
+ },
88
+ {
89
+ title: 'With helper text',
90
+ code: `<Select label="Timezone" options={timezones} helperText="Used for scheduling notifications" />`,
91
+ },
92
+ ],
93
+ compositionGraph: [],
94
+ accessibility: {
95
+ role: 'combobox',
96
+ ariaAttributes: ['aria-invalid', 'aria-describedby'],
97
+ keyboardInteractions: [
98
+ 'Enter / Space — opens the option list',
99
+ 'Arrow keys — navigate options',
100
+ 'Escape — closes the list',
101
+ ],
102
+ },
103
+ };
@@ -0,0 +1,27 @@
1
+ export const COMPONENT_MANIFEST = {
2
+ id: 'spinner',
3
+ name: 'Spinner',
4
+ tier: 'atom',
5
+ domain: 'neutral',
6
+ specVersion: '0.1',
7
+ description: 'An animated loading indicator for async operations.',
8
+ designIntent: 'Use Spinner for indeterminate loading states of short duration (< 3s). ' +
9
+ 'For full-page or skeleton-level loading, prefer Skeleton instead. ' +
10
+ 'The label prop is visually hidden but read by screen readers — always set it to a meaningful description of what is loading.',
11
+ props: [
12
+ { name: 'size', type: 'enum', required: false, default: 'md', description: 'Spinner diameter.', enumValues: ['xs', 'sm', 'md', 'lg'] },
13
+ { name: 'label', type: 'string', required: false, default: 'Loading…', description: 'Visually hidden accessible label.' },
14
+ { name: 'color', type: 'string', required: false, description: 'Override colour (CSS value). Defaults to currentColor.' },
15
+ ],
16
+ usageExamples: [
17
+ { title: 'Default', code: `<Spinner />` },
18
+ { title: 'Inside button', code: `<Button loading><Spinner size="sm" label="Saving…" /></Button>` },
19
+ { title: 'Full-page overlay', code: `<div style={{ display: 'grid', placeItems: 'center', minHeight: '100vh' }}><Spinner size="lg" label="Loading dashboard…" /></div>` },
20
+ ],
21
+ compositionGraph: [],
22
+ accessibility: {
23
+ role: 'status',
24
+ ariaAttributes: ['aria-label'],
25
+ notes: 'The visible SVG is aria-hidden. The label is conveyed via a visually-hidden span inside role="status".',
26
+ },
27
+ };