lucent-ui 0.14.2 → 0.16.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.
File without changes
File without changes
File without changes
@@ -34,6 +34,20 @@ export const COMPONENT_MANIFEST = {
34
34
  required: false,
35
35
  description: 'Array of hex color strings shown as clickable swatches. Defaults to a 16-color palette.',
36
36
  },
37
+ {
38
+ name: 'size',
39
+ type: 'string',
40
+ required: false,
41
+ default: '"md"',
42
+ description: 'Swatch trigger size. "sm" renders a 24px swatch, "md" renders 40px.',
43
+ },
44
+ {
45
+ name: 'inline',
46
+ type: 'boolean',
47
+ required: false,
48
+ default: 'false',
49
+ description: 'Places the label beside the swatch instead of above it. Useful for compact layouts.',
50
+ },
37
51
  {
38
52
  name: 'disabled',
39
53
  type: 'boolean',
@@ -44,6 +44,14 @@ export const COMPONENT_MANIFEST = {
44
44
  default: 'false',
45
45
  description: 'Disables interaction and renders muted text. Removes href and onClick.',
46
46
  },
47
+ {
48
+ name: 'inverse',
49
+ type: 'boolean',
50
+ required: false,
51
+ default: 'false',
52
+ description: 'Uses surface background with primary text instead of accent for the active state. ' +
53
+ 'Ideal for sidebar navigation on tinted chrome where the accent highlight would be too heavy.',
54
+ },
47
55
  {
48
56
  name: 'onClick',
49
57
  type: 'function',
@@ -4,21 +4,64 @@ export const COMPONENT_MANIFEST = {
4
4
  tier: 'molecule',
5
5
  domain: 'neutral',
6
6
  specVersion: '0.1',
7
- description: 'A surface container with optional header, body, and footer slots, configurable padding, shadow, and radius. Includes a CardBleed sub-component for edge-to-edge content.',
8
- designIntent: 'Card provides a consistent elevated surface for grouping related content. The header and footer slots ' +
9
- 'are separated from the body by a border-default divider, giving visual structure without requiring ' +
10
- 'the consumer to manage spacing. Padding, shadow, and radius are all configurable to accommodate ' +
11
- 'flat/ghost cards, modal-like surfaces, and compact data-dense layouts. The overflow: hidden ensures ' +
12
- 'children respect the border-radius without needing additional clipping.\n\n' +
13
- 'CardBleed is a companion component that allows specific children within the Card body to stretch ' +
14
- 'edge-to-edge, cancelling the horizontal padding via negative margins. Text inside CardBleed stays ' +
15
- 'aligned with the rest of the card content thanks to matching inner padding. Use it for dividers, ' +
16
- 'full-width lists, or bordered sections that need to reach the card edges.\n\n' +
17
- 'Token rule: Card uses surface for its background. Never use bgBase or bgSubtle on a Card — ' +
18
- 'those tokens are reserved for the page canvas (body, sidebar, layout regions). Content nested ' +
19
- 'inside a Card that needs a tinted fill (e.g. a footer, inset panel, or disabled input) should use ' +
20
- 'surfaceSecondary.',
7
+ description: 'A surface container with five elevation variants that form a visual importance hierarchy. ' +
8
+ 'Supports optional header, body, and footer slots with configurable padding, shadow, and radius. ' +
9
+ 'Includes a CardBleed sub-component for edge-to-edge content.',
10
+ designIntent: 'Card provides a configurable surface with an explicit elevation hierarchy. Each variant maps to ' +
11
+ 'a distinct level of visual prominence, giving consumers a single prop to express how much attention ' +
12
+ 'a surface should command relative to its surroundings.\n\n' +
13
+ '## Elevation hierarchy (lowest highest)\n\n' +
14
+ '1. **ghost** transparent background, no border, no shadow.\n' +
15
+ ' The card is invisible as a container content floats directly against the page or parent surface. ' +
16
+ 'Use for logical groupings that shouldn\'t compete visually: sidebar sections, form regions, or ' +
17
+ 'layout slots where structure exists conceptually but not visually. Header/footer dividers still ' +
18
+ 'render if those slots are used, providing minimal internal structure.\n\n' +
19
+ '2. **outline** (default) transparent background with `border-default` border, no shadow.\n' +
20
+ ' Like ghost, the card inherits its container\'s background — the border alone defines the boundary. ' +
21
+ 'This is the workhorse variant for lists of items, form sections, data panels, ' +
22
+ 'and any content that needs a visible container without drawing excessive attention. Header and footer ' +
23
+ 'slots are separated from the body by matching `border-default` dividers.\n\n' +
24
+ '3. **filled** — semi-transparent tinted background, no border, no shadow.\n' +
25
+ ' Differentiates the card from its surroundings by darkening (light mode) or lightening (dark mode) ' +
26
+ 'whatever surface it sits on. Uses `color-mix(in srgb, textPrimary 6%, transparent)` so the tint is ' +
27
+ 'always relative to the container — never a fixed color. Use for secondary content areas, inset panels, ' +
28
+ 'summary blocks, or anywhere a border would feel heavy but the card needs to be visually distinct. ' +
29
+ 'Effective for nesting (e.g., a filled card inside an elevated card creates a recessed region).\n\n' +
30
+ '4. **elevated** — `surface` background with medium shadow, no border.\n' +
31
+ ' The card lifts off the page through depth. The shadow creates a physical metaphor: this content ' +
32
+ 'sits above the surface it rests on. Use for primary content areas, feature highlights, pricing cards, ' +
33
+ 'or any surface that should feel physically elevated. The lack of border keeps the silhouette soft — ' +
34
+ 'the shadow alone defines the boundary.\n\n' +
35
+ '5. **combo** — transparent wrapper with an elevated body inset.\n' +
36
+ ' The header and footer are flat — they blend into the page background as if they were part of it. ' +
37
+ 'Only the body section is elevated: it gets `surface` background, border-radius, and shadow, appearing ' +
38
+ 'as a raised card sitting between the flat header/footer regions. This draws the eye to the primary ' +
39
+ 'content while keeping supporting info (header) and actions (footer) visually subordinate — they frame ' +
40
+ 'the elevated body without competing with it. No dividers are rendered — the elevation change IS the ' +
41
+ 'visual separator. Use for detail panels, profile cards, or settings forms where the body content is ' +
42
+ 'the focal point.\n\n' +
43
+ '## Shadow override\n' +
44
+ 'The `shadow` prop overrides whatever shadow the variant implies. This allows fine-tuning without ' +
45
+ 'changing the variant. For example, `variant="elevated" shadow="lg"` gives an elevated card with extra ' +
46
+ 'depth, while `variant="outline" shadow="none"` gives a flat bordered card.\n\n' +
47
+ '## Token rules\n' +
48
+ '- `elevated` and `combo` body use `surface` (the primary component surface — white in light mode).\n' +
49
+ '- `filled` uses a semi-transparent tint of `textPrimary` — contextually darker/lighter than its container.\n' +
50
+ '- `combo` wrapper is transparent (header/footer blend with page); only the body is elevated with `surface`.\n' +
51
+ '- `ghost` and `outline` use `transparent` — they inherit from whatever they\'re placed on. ' +
52
+ 'The border is the only visual differentiator for `outline`.\n' +
53
+ '- Never use `bgBase` or `bgSubtle` on a Card — those tokens are reserved for the page canvas.\n' +
54
+ '- Content nested inside a Card that needs a tinted fill should use `surfaceSecondary`.',
21
55
  props: [
56
+ {
57
+ name: 'variant',
58
+ type: 'enum',
59
+ required: false,
60
+ default: 'outline',
61
+ description: 'The elevation variant. Controls background color, border, and default shadow. ' +
62
+ 'Ordered from lowest to highest visual prominence: ghost → outline → filled → elevated → combo.',
63
+ enumValues: ['ghost', 'outline', 'filled', 'elevated', 'combo'],
64
+ },
22
65
  {
23
66
  name: 'children',
24
67
  type: 'ReactNode',
@@ -29,13 +72,15 @@ export const COMPONENT_MANIFEST = {
29
72
  name: 'header',
30
73
  type: 'ReactNode',
31
74
  required: false,
32
- description: 'Content rendered in the header slot, separated from the body by a divider.',
75
+ description: 'Content rendered in the header slot. Separated from the body by a divider in all variants except combo, ' +
76
+ 'where the background-color change provides the separation.',
33
77
  },
34
78
  {
35
79
  name: 'footer',
36
80
  type: 'ReactNode',
37
81
  required: false,
38
- description: 'Content rendered in the footer slot, separated from the body by a divider.',
82
+ description: 'Content rendered in the footer slot. Separated from the body by a divider in all variants except combo, ' +
83
+ 'where the background-color change provides the separation.',
39
84
  },
40
85
  {
41
86
  name: 'padding',
@@ -49,8 +94,8 @@ export const COMPONENT_MANIFEST = {
49
94
  name: 'shadow',
50
95
  type: 'enum',
51
96
  required: false,
52
- default: 'sm',
53
- description: 'Box shadow elevation.',
97
+ description: 'Box shadow elevation. When omitted, uses the variant\'s default: ghost=none, outline=none, filled=none, elevated=md, combo=md. ' +
98
+ 'When set explicitly, overrides the variant\'s default.',
54
99
  enumValues: ['none', 'sm', 'md', 'lg'],
55
100
  },
56
101
  {
@@ -67,27 +112,102 @@ export const COMPONENT_MANIFEST = {
67
112
  required: false,
68
113
  description: 'Inline style overrides for the card wrapper.',
69
114
  },
115
+ {
116
+ name: 'onClick',
117
+ type: 'function',
118
+ required: false,
119
+ description: 'Click handler. When provided, the card renders as a <button> with hover lift, focus ring, ' +
120
+ 'and active press states matching the Button component.',
121
+ },
122
+ {
123
+ name: 'href',
124
+ type: 'string',
125
+ required: false,
126
+ description: 'Link URL. When provided, the card renders as an <a>. Takes precedence over onClick for the element type, ' +
127
+ 'but onClick is still attached as a handler.',
128
+ },
129
+ {
130
+ name: 'target',
131
+ type: 'string',
132
+ required: false,
133
+ description: 'Passed to <a> when href is set (e.g. "_blank").',
134
+ },
135
+ {
136
+ name: 'rel',
137
+ type: 'string',
138
+ required: false,
139
+ description: 'Passed to <a> when href is set (e.g. "noopener noreferrer").',
140
+ },
141
+ {
142
+ name: 'disabled',
143
+ type: 'boolean',
144
+ required: false,
145
+ description: 'Disables interactive behavior. Reduces opacity, removes hover/focus/active states, ' +
146
+ 'and sets cursor to not-allowed. Only applies when onClick or href is set.',
147
+ },
148
+ {
149
+ name: 'status',
150
+ type: 'enum',
151
+ required: false,
152
+ description: 'Adds a 3px colored accent bar on the left edge of the card. Uses the corresponding status ' +
153
+ 'token (successDefault, warningDefault, dangerDefault, infoDefault). Works with all variants.',
154
+ enumValues: ['success', 'warning', 'danger', 'info'],
155
+ },
156
+ {
157
+ name: 'selected',
158
+ type: 'boolean',
159
+ required: false,
160
+ description: 'Adds an inset accent ring and subtle background tint to indicate selection. Used for card grids ' +
161
+ 'where cards act as radio/checkbox options. Pairs with onClick for toggle behavior. ' +
162
+ 'Sets aria-pressed on interactive cards. Disabled takes precedence — ring is hidden when disabled.',
163
+ },
164
+ {
165
+ name: 'media',
166
+ type: 'ReactNode',
167
+ required: false,
168
+ description: 'Full-bleed content rendered at the top of the card (before header). No padding is applied. ' +
169
+ 'Use for hero images, illustrations, or any edge-to-edge top content. The card\'s overflow:hidden ' +
170
+ 'clips media to the border-radius.',
171
+ },
70
172
  ],
71
173
  usageExamples: [
72
174
  {
73
- title: 'Simple card',
74
- code: `<Card>
75
- <Text>Some content here.</Text>
175
+ title: 'Ghost — invisible container',
176
+ code: `<Card variant="ghost">
177
+ <Text>Content sits directly on the page background.</Text>
76
178
  </Card>`,
77
179
  },
78
180
  {
79
- title: 'With header and footer',
181
+ title: 'Outline bordered card (default)',
80
182
  code: `<Card
81
183
  header={<Text weight="semibold">Card title</Text>}
82
184
  footer={<Button variant="primary">Save</Button>}
83
185
  >
84
- <Text color="secondary">Card body content goes here.</Text>
186
+ <Text color="secondary">The standard card treatment with dividers.</Text>
187
+ </Card>`,
188
+ },
189
+ {
190
+ title: 'Filled — color-differentiated region',
191
+ code: `<Card variant="filled" padding="lg">
192
+ <Text weight="semibold">Summary</Text>
193
+ <Text color="secondary" size="sm">A tinted panel that stands out through background color alone.</Text>
85
194
  </Card>`,
86
195
  },
87
196
  {
88
- title: 'Flat variant',
89
- code: `<Card shadow="none" radius="sm" padding="sm">
90
- <Text size="sm">Compact flat card</Text>
197
+ title: 'Elevated — shadow-lifted surface',
198
+ code: `<Card variant="elevated">
199
+ <Text weight="semibold">Featured</Text>
200
+ <Text color="secondary">This card floats above the page with shadow depth.</Text>
201
+ </Card>`,
202
+ },
203
+ {
204
+ title: 'Combo — two-tone card with header and footer',
205
+ code: `<Card
206
+ variant="combo"
207
+ header={<Text weight="semibold">Profile</Text>}
208
+ footer={<Button variant="primary">Update</Button>}
209
+ >
210
+ <Text>The bright body is framed by the muted header and footer.</Text>
91
211
  </Card>`,
92
212
  },
93
213
  {
@@ -97,13 +217,58 @@ export const COMPONENT_MANIFEST = {
97
217
  <CardBleed style={{ borderTop: '1px solid var(--lucent-border-default)', marginTop: 'var(--lucent-space-4)' }}>
98
218
  <Text color="secondary">This section stretches to the card edges.</Text>
99
219
  </CardBleed>
220
+ </Card>`,
221
+ },
222
+ {
223
+ title: 'Clickable card',
224
+ code: `<Card variant="elevated" onClick={() => navigate('/detail')}>
225
+ <Text weight="semibold">Dashboard tile</Text>
226
+ <Text color="secondary" size="sm">Click to view details</Text>
227
+ </Card>`,
228
+ },
229
+ {
230
+ title: 'Link card',
231
+ code: `<Card variant="elevated" href="/docs/getting-started" target="_blank" rel="noopener noreferrer">
232
+ <Text weight="semibold">Documentation</Text>
233
+ <Text color="secondary" size="sm">Opens in a new tab</Text>
234
+ </Card>`,
235
+ },
236
+ {
237
+ title: 'Status accent',
238
+ code: `<Card status="danger">
239
+ <Text weight="semibold">Payment failed</Text>
240
+ <Text color="secondary" size="sm">Check your card details and try again.</Text>
241
+ </Card>`,
242
+ },
243
+ {
244
+ title: 'Selectable card',
245
+ code: `<Card
246
+ variant="elevated"
247
+ selected={isSelected}
248
+ onClick={() => setIsSelected(!isSelected)}
249
+ >
250
+ <Text weight="semibold">Pro plan</Text>
251
+ <Text color="secondary" size="sm">$29/month</Text>
252
+ </Card>`,
253
+ },
254
+ {
255
+ title: 'Media card with hero image',
256
+ code: `<Card
257
+ variant="elevated"
258
+ media={<img src="/hero.jpg" alt="Hero" style={{ width: '100%', display: 'block' }} />}
259
+ >
260
+ <Text weight="semibold">Article title</Text>
261
+ <Text color="secondary" size="sm">A card with a full-bleed hero image.</Text>
100
262
  </Card>`,
101
263
  },
102
264
  ],
103
265
  compositionGraph: [],
104
266
  accessibility: {
105
- notes: 'Card has no implicit ARIA role. If the card represents a landmark, wrap it in a <section> or <article> ' +
106
- 'and provide an aria-label. For interactive cards (clickable), make the wrapper a <button> or <a> and ' +
107
- 'ensure focus styles are visible.',
267
+ notes: 'Non-interactive cards have no implicit ARIA role wrap in <section> or <article> if needed. ' +
268
+ 'Interactive cards with onClick render as <button> with focus ring. ' +
269
+ 'Interactive cards with href render as <a> with focus ring. ' +
270
+ 'Selected cards set aria-pressed on the interactive element. ' +
271
+ 'The status accent bar is aria-hidden (decorative). ' +
272
+ 'Media slot images should include alt text.',
108
273
  },
109
274
  };
@@ -5,7 +5,9 @@ export const COMPONENT_MANIFEST = {
5
5
  domain: 'neutral',
6
6
  specVersion: '0.1',
7
7
  description: 'Full-viewport shell layout with optional header, left sidebar, right panel, and footer slots arranged in a flex column/row structure.',
8
- designIntent: 'PageLayout owns the outermost chrome of an application page. The body row is a flex row containing ' +
8
+ designIntent: 'PageLayout owns the outermost chrome of an application page. Chrome regions (header, sidebar, footer) ' +
9
+ 'default to bgBase so the main content card feels elevated against the page canvas — especially ' +
10
+ 'noticeable with tinted bgBase values. The body row is a flex row containing ' +
9
11
  'an optional left sidebar, a bordered main content card, and an optional right panel — all as structural ' +
10
12
  'siblings so they share the same vertical space. The header and footer sit outside the body row as ' +
11
13
  'flex children of the outer column, ensuring they span the full width. Sidebars collapse to zero width ' +
@@ -87,6 +89,16 @@ export const COMPONENT_MANIFEST = {
87
89
  default: '28',
88
90
  description: 'Footer height in px (number) or any CSS value (string). Default: 28 — sized for a compact status bar.',
89
91
  },
92
+ {
93
+ name: 'chromeBackground',
94
+ type: 'string',
95
+ required: false,
96
+ default: '"bgBase"',
97
+ description: 'Background token for chrome regions (header, sidebar, footer). ' +
98
+ '"bgBase" uses the page canvas color so the main content card feels elevated; ' +
99
+ '"bgSubtle" uses a subtle shade of bgBase for chrome distinction; ' +
100
+ '"surface" matches the old behavior where chrome and content share the same background.',
101
+ },
90
102
  {
91
103
  name: 'mainStyle',
92
104
  type: 'object',
@@ -0,0 +1,28 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import { validateManifest } from './validate.js';
3
+ // Auto-discover all component manifests
4
+ const manifestModules = import.meta.glob('../components/**/*.manifest.ts', { eager: true });
5
+ const manifests = Object.entries(manifestModules).map(([path, mod]) => {
6
+ const m = mod;
7
+ const manifest = m['COMPONENT_MANIFEST'];
8
+ return { path, manifest };
9
+ });
10
+ describe('Component manifests', () => {
11
+ test('at least one manifest was discovered', () => {
12
+ expect(manifests.length).toBeGreaterThan(0);
13
+ });
14
+ for (const { path, manifest } of manifests) {
15
+ const label = path.replace('../components/', '').replace('.manifest.ts', '');
16
+ test(`${label} — exports COMPONENT_MANIFEST`, () => {
17
+ expect(manifest).toBeDefined();
18
+ });
19
+ test(`${label} — passes schema validation`, () => {
20
+ const result = validateManifest(manifest);
21
+ if (!result.valid) {
22
+ const messages = result.errors.map(e => ` ${e.field}: ${e.message}`).join('\n');
23
+ throw new Error(`Invalid manifest:\n${messages}`);
24
+ }
25
+ expect(result.valid).toBe(true);
26
+ });
27
+ }
28
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lucent-ui",
3
- "version": "0.14.2",
3
+ "version": "0.16.0",
4
4
  "description": "An AI-first React component library with machine-readable manifests.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -25,6 +25,18 @@
25
25
  "dist-server",
26
26
  "dist-cli"
27
27
  ],
28
+ "scripts": {
29
+ "dev": "vite --config vite.dev.config.ts",
30
+ "build": "vite build",
31
+ "build:server": "tsc -p server/tsconfig.json",
32
+ "build:cli": "tsc -p cli/tsconfig.json && cp cli/template.manifest.json dist-cli/cli/template.manifest.json",
33
+ "test": "vitest run",
34
+ "test:watch": "vitest",
35
+ "prepublishOnly": "tsc --noEmit && pnpm build && pnpm build:server && pnpm build:cli",
36
+ "changeset": "changeset",
37
+ "version-packages": "changeset version",
38
+ "release": "pnpm prepublishOnly && changeset publish"
39
+ },
28
40
  "keywords": [
29
41
  "react",
30
42
  "component-library",
@@ -39,6 +51,7 @@
39
51
  },
40
52
  "author": "Rozina Szogyenyi",
41
53
  "license": "MIT",
54
+ "packageManager": "pnpm@10.30.3",
42
55
  "peerDependencies": {
43
56
  "react": "^18.0.0 || ^19.0.0",
44
57
  "react-dom": "^18.0.0 || ^19.0.0"
@@ -60,16 +73,5 @@
60
73
  "dependencies": {
61
74
  "@modelcontextprotocol/sdk": "^1.27.1",
62
75
  "zod": "^4.3.6"
63
- },
64
- "scripts": {
65
- "dev": "vite --config vite.dev.config.ts",
66
- "build": "vite build",
67
- "build:server": "tsc -p server/tsconfig.json",
68
- "build:cli": "tsc -p cli/tsconfig.json && cp cli/template.manifest.json dist-cli/cli/template.manifest.json",
69
- "test": "vitest run",
70
- "test:watch": "vitest",
71
- "changeset": "changeset",
72
- "version-packages": "changeset version",
73
- "release": "pnpm prepublishOnly && changeset publish"
74
76
  }
75
- }
77
+ }