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.
- package/LICENSE +21 -0
- package/README.md +103 -0
- package/dist/index.cjs +112 -0
- package/dist/index.d.ts +687 -0
- package/dist/index.js +3227 -0
- package/dist-server/server/index.js +111 -0
- package/dist-server/server/registry.js +48 -0
- package/dist-server/src/components/atoms/Avatar/Avatar.manifest.js +29 -0
- package/dist-server/src/components/atoms/Badge/Badge.manifest.js +29 -0
- package/dist-server/src/components/atoms/Button/Button.manifest.js +2 -0
- package/dist-server/src/components/atoms/Checkbox/Checkbox.manifest.js +74 -0
- package/dist-server/src/components/atoms/Divider/Divider.manifest.js +25 -0
- package/dist-server/src/components/atoms/Icon/Icon.manifest.js +54 -0
- package/dist-server/src/components/atoms/Input/Input.manifest.js +36 -0
- package/dist-server/src/components/atoms/Radio/Radio.manifest.js +82 -0
- package/dist-server/src/components/atoms/Select/Select.manifest.js +103 -0
- package/dist-server/src/components/atoms/Spinner/Spinner.manifest.js +27 -0
- package/dist-server/src/components/atoms/Tag/Tag.manifest.js +62 -0
- package/dist-server/src/components/atoms/Text/Text.manifest.js +106 -0
- package/dist-server/src/components/atoms/Textarea/Textarea.manifest.js +35 -0
- package/dist-server/src/components/atoms/Toggle/Toggle.manifest.js +67 -0
- package/dist-server/src/components/atoms/Tooltip/Tooltip.manifest.js +54 -0
- package/dist-server/src/components/molecules/Alert/Alert.manifest.js +81 -0
- package/dist-server/src/components/molecules/Card/Card.manifest.js +92 -0
- package/dist-server/src/components/molecules/EmptyState/EmptyState.manifest.js +79 -0
- package/dist-server/src/components/molecules/FormField/FormField.manifest.js +93 -0
- package/dist-server/src/components/molecules/SearchInput/SearchInput.manifest.js +102 -0
- package/dist-server/src/components/molecules/Skeleton/Skeleton.manifest.js +93 -0
- package/dist-server/src/manifest/examples/button.manifest.js +116 -0
- package/dist-server/src/manifest/index.js +3 -0
- package/dist-server/src/manifest/types.js +1 -0
- package/dist-server/src/manifest/validate.js +102 -0
- package/package.json +58 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export const COMPONENT_MANIFEST = {
|
|
2
|
+
id: 'form-field',
|
|
3
|
+
name: 'FormField',
|
|
4
|
+
tier: 'molecule',
|
|
5
|
+
domain: 'neutral',
|
|
6
|
+
specVersion: '0.1',
|
|
7
|
+
description: 'Wraps any form control (Input, Select, Textarea) with a label, helper text, and validation message.',
|
|
8
|
+
designIntent: 'FormField standardises the vertical rhythm around form controls. A label is linked to the control ' +
|
|
9
|
+
'via htmlFor so screen readers announce it correctly. The required asterisk is decorative (aria-hidden) ' +
|
|
10
|
+
'because the actual required state should be communicated on the input via aria-required. Helper text ' +
|
|
11
|
+
'provides proactive guidance; errorMessage replaces it when validation fails, using danger color to ' +
|
|
12
|
+
'draw attention. The gap between elements uses space-2 to create a tight but breathable stack.',
|
|
13
|
+
props: [
|
|
14
|
+
{
|
|
15
|
+
name: 'label',
|
|
16
|
+
type: 'string',
|
|
17
|
+
required: false,
|
|
18
|
+
description: 'Label text rendered above the control as a <label> element.',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'htmlFor',
|
|
22
|
+
type: 'string',
|
|
23
|
+
required: false,
|
|
24
|
+
description: 'ID of the form control this label describes. Forwarded to the label htmlFor attribute.',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'required',
|
|
28
|
+
type: 'boolean',
|
|
29
|
+
required: false,
|
|
30
|
+
default: 'false',
|
|
31
|
+
description: 'Appends a danger-colored asterisk after the label text.',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'helperText',
|
|
35
|
+
type: 'string',
|
|
36
|
+
required: false,
|
|
37
|
+
description: 'Secondary text below the control providing guidance. Hidden when errorMessage is set.',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'errorMessage',
|
|
41
|
+
type: 'string',
|
|
42
|
+
required: false,
|
|
43
|
+
description: 'Validation error shown in danger color below the control. Replaces helperText when set.',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'children',
|
|
47
|
+
type: 'ReactNode',
|
|
48
|
+
required: true,
|
|
49
|
+
description: 'The form control to wrap — typically Input, Select, or Textarea.',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'style',
|
|
53
|
+
type: 'object',
|
|
54
|
+
required: false,
|
|
55
|
+
description: 'Inline style overrides for the outer wrapper.',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
usageExamples: [
|
|
59
|
+
{
|
|
60
|
+
title: 'Basic field',
|
|
61
|
+
code: `<FormField label="Email" htmlFor="email">
|
|
62
|
+
<Input id="email" placeholder="you@example.com" />
|
|
63
|
+
</FormField>`,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
title: 'Required with helper',
|
|
67
|
+
code: `<FormField label="Username" htmlFor="username" required helperText="Letters and numbers only">
|
|
68
|
+
<Input id="username" />
|
|
69
|
+
</FormField>`,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
title: 'With validation error',
|
|
73
|
+
code: `<FormField label="Password" htmlFor="pw" errorMessage="Must be at least 8 characters">
|
|
74
|
+
<Input id="pw" type="password" />
|
|
75
|
+
</FormField>`,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
title: 'Wrapping a Select',
|
|
79
|
+
code: `<FormField label="Country" htmlFor="country">
|
|
80
|
+
<Select id="country" options={countryOptions} />
|
|
81
|
+
</FormField>`,
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
compositionGraph: [
|
|
85
|
+
{ componentId: 'text', componentName: 'Text', role: 'Label, helper text, and error message', required: false },
|
|
86
|
+
],
|
|
87
|
+
accessibility: {
|
|
88
|
+
ariaAttributes: ['aria-required', 'aria-describedby'],
|
|
89
|
+
notes: 'Link the wrapped control to an error message using aria-describedby on the control and a matching id ' +
|
|
90
|
+
'on the error element for full screen reader support. The required asterisk is aria-hidden; ' +
|
|
91
|
+
'set aria-required="true" on the control itself.',
|
|
92
|
+
},
|
|
93
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export const COMPONENT_MANIFEST = {
|
|
2
|
+
id: 'search-input',
|
|
3
|
+
name: 'SearchInput',
|
|
4
|
+
tier: 'molecule',
|
|
5
|
+
domain: 'neutral',
|
|
6
|
+
specVersion: '0.1',
|
|
7
|
+
description: 'A search field with a built-in magnifier icon, clear button, and an optional results dropdown.',
|
|
8
|
+
designIntent: 'SearchInput is intentionally dumb about filtering — the consumer passes already-filtered results ' +
|
|
9
|
+
'so the component stays stateless and flexible. The clear button appears only when the input has a ' +
|
|
10
|
+
'value, keeping the right side clean at rest. The results dropdown is rendered absolutely below the ' +
|
|
11
|
+
'input and closes after a 150ms delay on blur to allow result clicks to register before focus is lost. ' +
|
|
12
|
+
'Spinner replaces the clear button during loading to communicate async state without layout shift.',
|
|
13
|
+
props: [
|
|
14
|
+
{
|
|
15
|
+
name: 'value',
|
|
16
|
+
type: 'string',
|
|
17
|
+
required: true,
|
|
18
|
+
description: 'Controlled input value.',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'onChange',
|
|
22
|
+
type: 'function',
|
|
23
|
+
required: true,
|
|
24
|
+
description: 'Called with the new string value whenever the input changes.',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'placeholder',
|
|
28
|
+
type: 'string',
|
|
29
|
+
required: false,
|
|
30
|
+
default: '"Search…"',
|
|
31
|
+
description: 'Placeholder text for the input.',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'results',
|
|
35
|
+
type: 'array',
|
|
36
|
+
required: false,
|
|
37
|
+
default: '[]',
|
|
38
|
+
description: 'Pre-filtered list of SearchResult objects ({ id, label }) to display in the dropdown.',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'onResultSelect',
|
|
42
|
+
type: 'function',
|
|
43
|
+
required: false,
|
|
44
|
+
description: 'Called with the selected SearchResult when a dropdown item is clicked.',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'isLoading',
|
|
48
|
+
type: 'boolean',
|
|
49
|
+
required: false,
|
|
50
|
+
default: 'false',
|
|
51
|
+
description: 'Shows a spinner in the right slot to indicate async search in progress.',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'disabled',
|
|
55
|
+
type: 'boolean',
|
|
56
|
+
required: false,
|
|
57
|
+
default: 'false',
|
|
58
|
+
description: 'Disables the input.',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'id',
|
|
62
|
+
type: 'string',
|
|
63
|
+
required: false,
|
|
64
|
+
description: 'HTML id forwarded to the underlying input element.',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'style',
|
|
68
|
+
type: 'object',
|
|
69
|
+
required: false,
|
|
70
|
+
description: 'Inline style overrides for the outer wrapper.',
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
usageExamples: [
|
|
74
|
+
{
|
|
75
|
+
title: 'Basic controlled search',
|
|
76
|
+
code: `const [query, setQuery] = useState('');
|
|
77
|
+
const [results, setResults] = useState([]);
|
|
78
|
+
|
|
79
|
+
<SearchInput
|
|
80
|
+
value={query}
|
|
81
|
+
onChange={(v) => { setQuery(v); setResults(filter(v)); }}
|
|
82
|
+
results={results}
|
|
83
|
+
onResultSelect={(r) => console.log(r)}
|
|
84
|
+
/>`,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
title: 'Loading state',
|
|
88
|
+
code: `<SearchInput value={query} onChange={setQuery} isLoading={isFetching} results={[]} />`,
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
compositionGraph: [
|
|
92
|
+
{ componentId: 'input', componentName: 'Input', role: 'Search text field with icon slots', required: true },
|
|
93
|
+
{ componentId: 'spinner', componentName: 'Spinner', role: 'Loading indicator in the right slot', required: false },
|
|
94
|
+
],
|
|
95
|
+
accessibility: {
|
|
96
|
+
role: 'combobox',
|
|
97
|
+
ariaAttributes: ['aria-expanded', 'aria-haspopup', 'aria-label'],
|
|
98
|
+
keyboardInteractions: ['Enter to select focused result', 'Escape to close dropdown'],
|
|
99
|
+
notes: 'The results list uses role="listbox" with role="option" items. For full keyboard navigation ' +
|
|
100
|
+
'(arrow keys to move between results), wire up onKeyDown on the Input and manage an activeIndex state.',
|
|
101
|
+
},
|
|
102
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export const COMPONENT_MANIFEST = {
|
|
2
|
+
id: 'skeleton',
|
|
3
|
+
name: 'Skeleton',
|
|
4
|
+
tier: 'molecule',
|
|
5
|
+
domain: 'neutral',
|
|
6
|
+
specVersion: '0.1',
|
|
7
|
+
description: 'Animated placeholder that matches the shape of content while it loads.',
|
|
8
|
+
designIntent: 'Skeleton uses a shimmer animation to communicate that content is loading without showing a spinner. ' +
|
|
9
|
+
'The three variants (text, circle, rectangle) cover the most common content shapes: inline text, ' +
|
|
10
|
+
'avatars/thumbnails, and generic content blocks. The text variant with lines > 1 mimics a paragraph ' +
|
|
11
|
+
'by stacking multiple text skeletons and shortening the last line to 70%, which is a widely recognised ' +
|
|
12
|
+
'convention for body copy placeholders. The shimmer gradient uses bg-muted and bg-subtle so it adapts ' +
|
|
13
|
+
'correctly in both light and dark themes without hard-coded colors.',
|
|
14
|
+
props: [
|
|
15
|
+
{
|
|
16
|
+
name: 'variant',
|
|
17
|
+
type: 'enum',
|
|
18
|
+
required: false,
|
|
19
|
+
default: 'rectangle',
|
|
20
|
+
description: 'Shape of the skeleton — text (1em tall), circle (equal width/height), or rectangle.',
|
|
21
|
+
enumValues: ['text', 'circle', 'rectangle'],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'width',
|
|
25
|
+
type: 'string',
|
|
26
|
+
required: false,
|
|
27
|
+
default: '"100%"',
|
|
28
|
+
description: 'Width of the skeleton. Accepts any CSS value or a number (interpreted as px).',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'height',
|
|
32
|
+
type: 'string',
|
|
33
|
+
required: false,
|
|
34
|
+
description: 'Height of the skeleton. Defaults: text=1em, circle=40px, rectangle=40px.',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'lines',
|
|
38
|
+
type: 'number',
|
|
39
|
+
required: false,
|
|
40
|
+
default: '1',
|
|
41
|
+
description: 'Number of stacked text lines to render. Only applies to the text variant.',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'animate',
|
|
45
|
+
type: 'boolean',
|
|
46
|
+
required: false,
|
|
47
|
+
default: 'true',
|
|
48
|
+
description: 'Enables the shimmer animation. Set to false to render a static placeholder.',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'radius',
|
|
52
|
+
type: 'string',
|
|
53
|
+
required: false,
|
|
54
|
+
description: 'Override the default border-radius for the variant.',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'style',
|
|
58
|
+
type: 'object',
|
|
59
|
+
required: false,
|
|
60
|
+
description: 'Inline style overrides.',
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
usageExamples: [
|
|
64
|
+
{
|
|
65
|
+
title: 'Paragraph placeholder',
|
|
66
|
+
code: `<Skeleton variant="text" lines={3} />`,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
title: 'Avatar placeholder',
|
|
70
|
+
code: `<Skeleton variant="circle" width={40} height={40} />`,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
title: 'Card placeholder',
|
|
74
|
+
code: `<Card>
|
|
75
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
76
|
+
<Skeleton variant="rectangle" height={160} />
|
|
77
|
+
<Skeleton variant="text" lines={2} />
|
|
78
|
+
<Skeleton variant="text" width="40%" />
|
|
79
|
+
</div>
|
|
80
|
+
</Card>`,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
title: 'Static (no animation)',
|
|
84
|
+
code: `<Skeleton variant="rectangle" width={200} height={32} animate={false} />`,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
compositionGraph: [],
|
|
88
|
+
accessibility: {
|
|
89
|
+
ariaAttributes: ['aria-busy', 'aria-label'],
|
|
90
|
+
notes: 'Wrap loading regions with aria-busy="true" on the container so screen readers know content ' +
|
|
91
|
+
'is loading. Individual Skeleton elements are presentational and do not need ARIA attributes themselves.',
|
|
92
|
+
},
|
|
93
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
export const ButtonManifest = {
|
|
2
|
+
id: 'button',
|
|
3
|
+
name: 'Button',
|
|
4
|
+
tier: 'atom',
|
|
5
|
+
domain: 'neutral',
|
|
6
|
+
specVersion: '0.1',
|
|
7
|
+
description: 'A clickable control that triggers an action. The primary interactive primitive in Lucent UI.',
|
|
8
|
+
designIntent: 'Buttons communicate available actions. Variant conveys hierarchy: use "primary" for the ' +
|
|
9
|
+
'single most important action in a view, "secondary" for supporting actions, "ghost" for ' +
|
|
10
|
+
'low-emphasis actions in dense UIs, and "danger" exclusively for destructive or irreversible ' +
|
|
11
|
+
'operations. Size should match surrounding content density — prefer "md" as the default and ' +
|
|
12
|
+
'reserve "sm" for toolbars or tables.',
|
|
13
|
+
props: [
|
|
14
|
+
{
|
|
15
|
+
name: 'variant',
|
|
16
|
+
type: 'enum',
|
|
17
|
+
required: false,
|
|
18
|
+
default: 'primary',
|
|
19
|
+
description: 'Visual style conveying action hierarchy.',
|
|
20
|
+
enumValues: ['primary', 'secondary', 'ghost', 'danger'],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'size',
|
|
24
|
+
type: 'enum',
|
|
25
|
+
required: false,
|
|
26
|
+
default: 'md',
|
|
27
|
+
description: 'Controls height and padding.',
|
|
28
|
+
enumValues: ['sm', 'md', 'lg'],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'children',
|
|
32
|
+
type: 'ReactNode',
|
|
33
|
+
required: true,
|
|
34
|
+
description: 'Button label or content.',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'disabled',
|
|
38
|
+
type: 'boolean',
|
|
39
|
+
required: false,
|
|
40
|
+
default: 'false',
|
|
41
|
+
description: 'Prevents interaction and applies disabled styling.',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'loading',
|
|
45
|
+
type: 'boolean',
|
|
46
|
+
required: false,
|
|
47
|
+
default: 'false',
|
|
48
|
+
description: 'Shows a spinner and prevents interaction while an async action is in progress.',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'fullWidth',
|
|
52
|
+
type: 'boolean',
|
|
53
|
+
required: false,
|
|
54
|
+
default: 'false',
|
|
55
|
+
description: 'Stretches the button to fill its container width.',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'leftIcon',
|
|
59
|
+
type: 'ReactNode',
|
|
60
|
+
required: false,
|
|
61
|
+
description: 'Icon element rendered before the label.',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'rightIcon',
|
|
65
|
+
type: 'ReactNode',
|
|
66
|
+
required: false,
|
|
67
|
+
description: 'Icon element rendered after the label.',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'onClick',
|
|
71
|
+
type: 'function',
|
|
72
|
+
required: false,
|
|
73
|
+
description: 'Called when the button is clicked and not disabled or loading.',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'type',
|
|
77
|
+
type: 'enum',
|
|
78
|
+
required: false,
|
|
79
|
+
default: 'button',
|
|
80
|
+
description: 'Native button type attribute.',
|
|
81
|
+
enumValues: ['button', 'submit', 'reset'],
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
usageExamples: [
|
|
85
|
+
{
|
|
86
|
+
title: 'Primary action',
|
|
87
|
+
code: `<Button variant="primary" onClick={handleSave}>Save changes</Button>`,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
title: 'Destructive action',
|
|
91
|
+
code: `<Button variant="danger" onClick={handleDelete}>Delete account</Button>`,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
title: 'Loading state',
|
|
95
|
+
code: `<Button variant="primary" loading={isSaving}>Save changes</Button>`,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
title: 'With icon',
|
|
99
|
+
code: `<Button variant="secondary" leftIcon={<PlusIcon />}>Add member</Button>`,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
title: 'Ghost in toolbar',
|
|
103
|
+
code: `<Button variant="ghost" size="sm">Edit</Button>`,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
title: 'Full-width submit',
|
|
107
|
+
code: `<Button variant="primary" type="submit" fullWidth>Sign in</Button>`,
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
compositionGraph: [],
|
|
111
|
+
accessibility: {
|
|
112
|
+
role: 'button',
|
|
113
|
+
ariaAttributes: ['aria-disabled', 'aria-busy'],
|
|
114
|
+
keyboardInteractions: ['Enter — activates the button', 'Space — activates the button'],
|
|
115
|
+
},
|
|
116
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
function err(field, message) {
|
|
2
|
+
return { field, message };
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Validates a ComponentManifest object at runtime.
|
|
6
|
+
* Returns a result object — does not throw.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const result = validateManifest(ButtonManifest);
|
|
10
|
+
* if (!result.valid) console.error(result.errors);
|
|
11
|
+
*/
|
|
12
|
+
export function validateManifest(manifest) {
|
|
13
|
+
const errors = [];
|
|
14
|
+
if (typeof manifest !== 'object' || manifest === null) {
|
|
15
|
+
return { valid: false, errors: [err('manifest', 'Must be a non-null object')] };
|
|
16
|
+
}
|
|
17
|
+
const m = manifest;
|
|
18
|
+
// Required string fields
|
|
19
|
+
const requiredStrings = ['id', 'name', 'description', 'designIntent', 'specVersion'];
|
|
20
|
+
for (const field of requiredStrings) {
|
|
21
|
+
if (typeof m[field] !== 'string' || m[field].trim() === '') {
|
|
22
|
+
errors.push(err(field, `Must be a non-empty string`));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// id: kebab-case
|
|
26
|
+
if (typeof m['id'] === 'string' && !/^[a-z][a-z0-9-]*$/.test(m['id'])) {
|
|
27
|
+
errors.push(err('id', 'Must be kebab-case (e.g. "button", "form-field")'));
|
|
28
|
+
}
|
|
29
|
+
// tier
|
|
30
|
+
const validTiers = ['atom', 'molecule', 'block', 'flow', 'overlay'];
|
|
31
|
+
if (!validTiers.includes(m['tier'])) {
|
|
32
|
+
errors.push(err('tier', `Must be one of: ${validTiers.join(', ')}`));
|
|
33
|
+
}
|
|
34
|
+
// domain
|
|
35
|
+
if (typeof m['domain'] !== 'string' || m['domain'].trim() === '') {
|
|
36
|
+
errors.push(err('domain', 'Must be a non-empty string'));
|
|
37
|
+
}
|
|
38
|
+
// props
|
|
39
|
+
if (!Array.isArray(m['props'])) {
|
|
40
|
+
errors.push(err('props', 'Must be an array'));
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
m['props'].forEach((prop, i) => {
|
|
44
|
+
const p = prop;
|
|
45
|
+
const prefix = `props[${i}]`;
|
|
46
|
+
if (typeof p['name'] !== 'string' || p['name'] === '')
|
|
47
|
+
errors.push(err(`${prefix}.name`, 'Must be a non-empty string'));
|
|
48
|
+
if (typeof p['type'] !== 'string' || p['type'] === '')
|
|
49
|
+
errors.push(err(`${prefix}.type`, 'Must be a non-empty string'));
|
|
50
|
+
if (typeof p['required'] !== 'boolean')
|
|
51
|
+
errors.push(err(`${prefix}.required`, 'Must be a boolean'));
|
|
52
|
+
if (typeof p['description'] !== 'string' || p['description'] === '')
|
|
53
|
+
errors.push(err(`${prefix}.description`, 'Must be a non-empty string'));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
// usageExamples
|
|
57
|
+
if (!Array.isArray(m['usageExamples'])) {
|
|
58
|
+
errors.push(err('usageExamples', 'Must be an array'));
|
|
59
|
+
}
|
|
60
|
+
else if (m['usageExamples'].length === 0) {
|
|
61
|
+
errors.push(err('usageExamples', 'Must have at least one example'));
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
m['usageExamples'].forEach((ex, i) => {
|
|
65
|
+
const e = ex;
|
|
66
|
+
const prefix = `usageExamples[${i}]`;
|
|
67
|
+
if (typeof e['title'] !== 'string' || e['title'] === '')
|
|
68
|
+
errors.push(err(`${prefix}.title`, 'Must be a non-empty string'));
|
|
69
|
+
if (typeof e['code'] !== 'string' || e['code'] === '')
|
|
70
|
+
errors.push(err(`${prefix}.code`, 'Must be a non-empty string'));
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// compositionGraph
|
|
74
|
+
if (!Array.isArray(m['compositionGraph'])) {
|
|
75
|
+
errors.push(err('compositionGraph', 'Must be an array (empty array is fine for atoms)'));
|
|
76
|
+
}
|
|
77
|
+
// specVersion
|
|
78
|
+
if (typeof m['specVersion'] === 'string' && !/^\d+\.\d+$/.test(m['specVersion'])) {
|
|
79
|
+
errors.push(err('specVersion', 'Must be "MAJOR.MINOR" format, e.g. "0.1"'));
|
|
80
|
+
}
|
|
81
|
+
return { valid: errors.length === 0, errors };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Asserts a manifest is valid. Throws a descriptive error if not.
|
|
85
|
+
* Use in tests or at component module load time.
|
|
86
|
+
*/
|
|
87
|
+
export function assertManifest(manifest) {
|
|
88
|
+
const result = validateManifest(manifest);
|
|
89
|
+
if (!result.valid) {
|
|
90
|
+
const messages = result.errors.map(e => ` ${e.field}: ${e.message}`).join('\n');
|
|
91
|
+
throw new Error(`Invalid ComponentManifest:\n${messages}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export function isValidPropDescriptor(prop) {
|
|
95
|
+
if (typeof prop !== 'object' || prop === null)
|
|
96
|
+
return false;
|
|
97
|
+
const p = prop;
|
|
98
|
+
return (typeof p['name'] === 'string' &&
|
|
99
|
+
typeof p['type'] === 'string' &&
|
|
100
|
+
typeof p['required'] === 'boolean' &&
|
|
101
|
+
typeof p['description'] === 'string');
|
|
102
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lucent-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "An AI-first React component library with machine-readable manifests.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
},
|
|
15
|
+
"./styles": "./dist/index.css"
|
|
16
|
+
},
|
|
17
|
+
"bin": {
|
|
18
|
+
"lucent-mcp": "./dist-server/server/index.js"
|
|
19
|
+
},
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"dist-server"
|
|
24
|
+
],
|
|
25
|
+
"keywords": [
|
|
26
|
+
"react",
|
|
27
|
+
"component-library",
|
|
28
|
+
"design-system",
|
|
29
|
+
"mcp",
|
|
30
|
+
"ai"
|
|
31
|
+
],
|
|
32
|
+
"author": "Rozina Szogyenyi",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"react": "^18.0.0",
|
|
36
|
+
"react-dom": "^18.0.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^25.3.3",
|
|
40
|
+
"@types/react": "^18.3.28",
|
|
41
|
+
"@types/react-dom": "^18.3.7",
|
|
42
|
+
"@vitejs/plugin-react": "^4.7.0",
|
|
43
|
+
"react": "^18.3.1",
|
|
44
|
+
"react-dom": "^18.3.1",
|
|
45
|
+
"typescript": "^5.9.3",
|
|
46
|
+
"vite": "^6.4.1",
|
|
47
|
+
"vite-plugin-dts": "^4.5.4"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
51
|
+
"zod": "^4.3.6"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"dev": "vite --config vite.dev.config.ts",
|
|
55
|
+
"build": "vite build",
|
|
56
|
+
"build:server": "tsc -p server/tsconfig.json"
|
|
57
|
+
}
|
|
58
|
+
}
|