lucent-ui 0.32.1 → 0.33.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/README.md +1 -1
- package/dist-cli/cli/entry.js +0 -0
- package/dist-cli/cli/index.js +0 -0
- package/dist-server/server/design-rules.js +197 -0
- package/dist-server/server/index.js +48 -0
- package/package.json +13 -15
- package/dist-server/server/recipe-registry.js +0 -18
- package/dist-server/src/manifest/recipes/action-bar.recipe.js +0 -91
- package/dist-server/src/manifest/recipes/collapsible-card.recipe.js +0 -100
- package/dist-server/src/manifest/recipes/empty-state-card.recipe.js +0 -72
- package/dist-server/src/manifest/recipes/form-layout.recipe.js +0 -98
- package/dist-server/src/manifest/recipes/index.js +0 -8
- package/dist-server/src/manifest/recipes/profile-card.recipe.js +0 -101
- package/dist-server/src/manifest/recipes/search-filter-bar.recipe.js +0 -122
- package/dist-server/src/manifest/recipes/settings-panel.recipe.js +0 -167
- package/dist-server/src/manifest/recipes/stats-row.recipe.js +0 -106
- package/dist-server/src/manifest/validate.test.js +0 -28
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Lucent UI
|
|
2
2
|
|
|
3
|
-
[](https://github.com/rozina-hudson/lucent-ui/actions/workflows/ci.yml)
|
|
4
4
|
[](https://www.npmjs.com/package/lucent-ui)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
|
package/dist-cli/cli/entry.js
CHANGED
|
File without changes
|
package/dist-cli/cli/index.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// ─── Design Rules ────────────────────────────────────────────────────────────
|
|
2
|
+
// Structured layout guidance injected into the MCP system prompt so AI agents
|
|
3
|
+
// produce aesthetically consistent Lucent UI layouts, not just valid props.
|
|
4
|
+
export const DESIGN_RULES = [
|
|
5
|
+
{
|
|
6
|
+
id: 'spacing',
|
|
7
|
+
title: 'Spacing scale',
|
|
8
|
+
body: `
|
|
9
|
+
Use Stack and Row components for all layout — never raw flexbox.
|
|
10
|
+
Gap values map to the spacing token scale (default density, 14px base):
|
|
11
|
+
|
|
12
|
+
| Gap | Token | Rem | ~px | Use for |
|
|
13
|
+
|-----|----------|---------|-----|-----------------------------------------------------|
|
|
14
|
+
| 0 | space-0 | 0 | 0 | Stacked label + description (no visible gap) |
|
|
15
|
+
| 1 | space-1 | 0.25rem | 3.5 | Tight inline pairs: icon + label, badge + name |
|
|
16
|
+
| 2 | space-2 | 0.5rem | 7 | Related items in a row: chip gap, button-group gap |
|
|
17
|
+
| 3 | space-3 | 0.75rem | 10 | Label-to-control gap, rows in a settings list |
|
|
18
|
+
| 4 | space-4 | 1rem | 14 | Sections within a card, standard Stack gap (default) |
|
|
19
|
+
| 5 | space-5 | 1.25rem | 17 | Between content blocks inside a page section |
|
|
20
|
+
| 6 | space-6 | 1.5rem | 21 | Card-to-card gap, major section separation |
|
|
21
|
+
| 8 | space-8 | 2rem | 28 | Page-level section separation |
|
|
22
|
+
| 10+ | space-10+| 2.5rem+ | 35+ | Hero spacing, page margins (rare in components) |
|
|
23
|
+
|
|
24
|
+
Card padding sizes:
|
|
25
|
+
- sm: py=space-2, px=space-3 — compact lists, dense data
|
|
26
|
+
- md: py=space-4, px=space-5 — forms, standard content (default)
|
|
27
|
+
- lg: py=space-6, px=space-8 — hero content, featured cards
|
|
28
|
+
`.trim(),
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'typography',
|
|
32
|
+
title: 'Typography hierarchy',
|
|
33
|
+
body: `
|
|
34
|
+
Always use the Text component — never raw HTML tags with inline font styles.
|
|
35
|
+
Three font families: base (DM Sans), mono (DM Mono), display (Georama).
|
|
36
|
+
Use display family for large numeric values (stats, prices, counters).
|
|
37
|
+
|
|
38
|
+
| Role | Size | Weight | Color | Element | Family |
|
|
39
|
+
|--------------|------|----------|-----------|---------|---------|
|
|
40
|
+
| Page title | 2xl | bold | primary | h1 | base |
|
|
41
|
+
| Section head | lg | semibold | primary | h2 | base |
|
|
42
|
+
| Card title | md | semibold | primary | h3 | base |
|
|
43
|
+
| Body text | sm | regular | primary | p | base |
|
|
44
|
+
| Label | xs | medium | secondary | label | base |
|
|
45
|
+
| Caption | xs | regular | secondary | span | base |
|
|
46
|
+
| Stat value | 2xl | bold | primary | span | display |
|
|
47
|
+
| Code/mono | sm | regular | primary | code | mono |
|
|
48
|
+
|
|
49
|
+
Font size scale (14px base):
|
|
50
|
+
- xs: 0.75rem (10.5px) — labels, captions, metadata
|
|
51
|
+
- sm: 0.875rem (12.25px) — body text, descriptions
|
|
52
|
+
- md: 1rem (14px) — card titles, emphasized text
|
|
53
|
+
- lg: 1.125rem (15.75px) — section headings
|
|
54
|
+
- xl: 1.25rem (17.5px) — page subtitles
|
|
55
|
+
- 2xl: 1.5rem (21px) — page titles, stat values
|
|
56
|
+
- 3xl: 1.875rem (26.25px) — hero headings (rare)
|
|
57
|
+
`.trim(),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'buttons',
|
|
61
|
+
title: 'Button pairing rules',
|
|
62
|
+
body: `
|
|
63
|
+
Button variants: primary, secondary (filled), outline (bordered), ghost, danger.
|
|
64
|
+
|
|
65
|
+
Pairing guidelines:
|
|
66
|
+
- Primary + Outline: balanced dual actions (Save / Cancel, Confirm / Back)
|
|
67
|
+
- Primary + Ghost: dominant + dismiss (Submit / Reset, Continue / Skip)
|
|
68
|
+
- Single Primary: sole call-to-action (Sign up, Follow, Add to cart)
|
|
69
|
+
- Outline only: equal-weight options in a row (Edit, Share, Export)
|
|
70
|
+
- Danger + Outline: destructive confirmation (Delete / Cancel)
|
|
71
|
+
- Never place two primary buttons in the same Row
|
|
72
|
+
|
|
73
|
+
Size pairing:
|
|
74
|
+
- Buttons in the same Row should use the same size
|
|
75
|
+
- Form action buttons: md (default)
|
|
76
|
+
- Card inline actions: sm
|
|
77
|
+
- Toolbar/compact actions: xs
|
|
78
|
+
- Button heights (border-box): sm=34px, md=42px, lg=48px — these align with input total heights
|
|
79
|
+
|
|
80
|
+
Icon usage:
|
|
81
|
+
- leftIcon for semantic context (+ Add, ↓ Download)
|
|
82
|
+
- rightIcon for directional flow (Next →)
|
|
83
|
+
- chevron prop for dropdown triggers (built-in chevron-down SVG)
|
|
84
|
+
`.trim(),
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'layout',
|
|
88
|
+
title: 'Common layout patterns',
|
|
89
|
+
body: `
|
|
90
|
+
Always use Stack (vertical) and Row (horizontal) — never raw div with flex styles.
|
|
91
|
+
Stack default gap: 4. Row default gap: 3.
|
|
92
|
+
|
|
93
|
+
Toggle / checkbox row:
|
|
94
|
+
Row gap="3" align="center"
|
|
95
|
+
├── Toggle or Checkbox
|
|
96
|
+
└── Stack gap="0"
|
|
97
|
+
├── Text size="sm" weight="medium" ← label
|
|
98
|
+
└── Text size="xs" color="secondary" ← description
|
|
99
|
+
|
|
100
|
+
Stats block:
|
|
101
|
+
Stack gap="0" align="center"
|
|
102
|
+
├── Text size="2xl" weight="bold" family="display" ← value
|
|
103
|
+
└── Text size="xs" color="secondary" ← label
|
|
104
|
+
|
|
105
|
+
Form layout:
|
|
106
|
+
Stack gap="4"
|
|
107
|
+
└── FormField[] ← each field includes its own label + error
|
|
108
|
+
|
|
109
|
+
Action bar (form actions):
|
|
110
|
+
Row gap="2" justify="end"
|
|
111
|
+
├── Button variant="outline" ← secondary action
|
|
112
|
+
└── Button variant="primary" ← primary action
|
|
113
|
+
|
|
114
|
+
Card content:
|
|
115
|
+
Card padding="md"
|
|
116
|
+
└── Stack gap="4"
|
|
117
|
+
├── Text size="md" weight="semibold" ← card title
|
|
118
|
+
├── Stack gap="3" ← card body
|
|
119
|
+
└── Row gap="2" justify="end" ← card actions
|
|
120
|
+
|
|
121
|
+
Page section:
|
|
122
|
+
Stack gap="5"
|
|
123
|
+
├── Row justify="between" align="center"
|
|
124
|
+
│ ├── Text size="2xl" weight="bold" ← page title
|
|
125
|
+
│ └── Button variant="outline" ← page action
|
|
126
|
+
└── Stack gap="6" ← section content
|
|
127
|
+
`.trim(),
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 'color',
|
|
131
|
+
title: 'Color usage',
|
|
132
|
+
body: `
|
|
133
|
+
Lucent uses CSS custom properties for all colors. Never use raw hex values.
|
|
134
|
+
|
|
135
|
+
Text colors (via Text color prop):
|
|
136
|
+
- primary: default text, headings, values
|
|
137
|
+
- secondary: labels, captions, metadata, descriptions
|
|
138
|
+
- disabled: disabled state text (neutral gray, never accent-tinted)
|
|
139
|
+
- inverse: text on dark backgrounds
|
|
140
|
+
- onAccent: text on accent-colored backgrounds (auto-computed for contrast)
|
|
141
|
+
- success / warning / danger / info: semantic status text
|
|
142
|
+
|
|
143
|
+
Surface usage:
|
|
144
|
+
- Card variant="elevated": raised cards with shadow
|
|
145
|
+
- Card variant="outline": bordered cards, lower emphasis
|
|
146
|
+
- Card variant="filled": subtle background fill, no border
|
|
147
|
+
|
|
148
|
+
Accent color:
|
|
149
|
+
- Applied automatically by LucentProvider to primary buttons, toggles, checkboxes, radio
|
|
150
|
+
- textOnAccent is auto-computed via APCA contrast algorithm
|
|
151
|
+
- Light mode default accent: near-black (#111827)
|
|
152
|
+
- Dark mode default accent: near-white (#f9fafb)
|
|
153
|
+
- 12 palette presets available: default, brand, indigo, violet, emerald, teal, rose, coral, amber, ocean, slate, sage
|
|
154
|
+
|
|
155
|
+
Status colors (Chip/Badge/Alert variants):
|
|
156
|
+
- success: positive trends, confirmations, online status
|
|
157
|
+
- warning: attention needed, approaching limits
|
|
158
|
+
- danger: errors, destructive actions, critical alerts
|
|
159
|
+
- info: neutral information, tips, updates
|
|
160
|
+
|
|
161
|
+
Disabled state:
|
|
162
|
+
- Always use neutral gray — never accent-tinted surfaces
|
|
163
|
+
- Applied via color-mix with neutral gray, not opacity
|
|
164
|
+
`.trim(),
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: 'density',
|
|
168
|
+
title: 'Density and responsive patterns',
|
|
169
|
+
body: `
|
|
170
|
+
Three density presets scale all spacing tokens proportionally:
|
|
171
|
+
- compact: tighter spacing (~80% of default) — data-dense dashboards, admin panels
|
|
172
|
+
- default: standard spacing — general purpose
|
|
173
|
+
- spacious: generous spacing (~125% of default) — marketing, onboarding, hero sections
|
|
174
|
+
|
|
175
|
+
Responsive wrapping:
|
|
176
|
+
- Use Row with wrap prop for responsive card grids
|
|
177
|
+
- Set minWidth on flex children (e.g. style={{ flex: 1, minWidth: 180 }})
|
|
178
|
+
- This creates equal-width cards that wrap gracefully without media queries
|
|
179
|
+
|
|
180
|
+
Card grids:
|
|
181
|
+
Row gap="4" wrap
|
|
182
|
+
└── Card[] style={{ flex: 1, minWidth: 240 }}
|
|
183
|
+
|
|
184
|
+
Compact list items:
|
|
185
|
+
Stack gap="2"
|
|
186
|
+
└── Row[] gap="3" align="center" (each row is an item)
|
|
187
|
+
`.trim(),
|
|
188
|
+
},
|
|
189
|
+
];
|
|
190
|
+
/** Full design rules as a single markdown string for system prompt injection. */
|
|
191
|
+
export const DESIGN_RULES_TEXT = DESIGN_RULES.map((s) => `## ${s.title}\n\n${s.body}`).join('\n\n');
|
|
192
|
+
/** Compact summary for the system prompt (keeps token budget reasonable). */
|
|
193
|
+
export const DESIGN_RULES_SUMMARY = `# Lucent UI Design Rules
|
|
194
|
+
|
|
195
|
+
These rules ensure AI-generated layouts are aesthetically consistent, not just technically valid.
|
|
196
|
+
|
|
197
|
+
${DESIGN_RULES_TEXT}`;
|
|
@@ -5,6 +5,7 @@ import { z } from 'zod';
|
|
|
5
5
|
import { ALL_MANIFESTS } from './registry.js';
|
|
6
6
|
import { ALL_PATTERNS } from './pattern-registry.js';
|
|
7
7
|
import { PALETTES, SHAPES, DENSITIES, SHADOWS, COMBINED, generatePresetConfig } from './presets.js';
|
|
8
|
+
import { DESIGN_RULES, DESIGN_RULES_SUMMARY } from './design-rules.js';
|
|
8
9
|
// ─── Auth stub ───────────────────────────────────────────────────────────────
|
|
9
10
|
// LUCENT_API_KEY is reserved for the future paid tier.
|
|
10
11
|
// When set, the server acknowledges it but does not yet enforce it.
|
|
@@ -65,6 +66,8 @@ function scorePattern(r, query) {
|
|
|
65
66
|
const server = new McpServer({
|
|
66
67
|
name: 'lucent-mcp',
|
|
67
68
|
version: '0.1.0',
|
|
69
|
+
}, {
|
|
70
|
+
instructions: DESIGN_RULES_SUMMARY,
|
|
68
71
|
});
|
|
69
72
|
// Tool: list_components
|
|
70
73
|
server.tool('list_components', 'Lists all available Lucent UI components with their name, tier (atom/molecule), and one-line description.', {}, async () => {
|
|
@@ -260,6 +263,51 @@ server.tool('get_preset_config', 'Returns the LucentProvider configuration code
|
|
|
260
263
|
],
|
|
261
264
|
};
|
|
262
265
|
});
|
|
266
|
+
// Tool: get_design_rules
|
|
267
|
+
server.tool('get_design_rules', 'Returns Lucent UI design rules for spacing, typography, button pairing, layout patterns, color usage, and density. These rules ensure AI-generated layouts are aesthetically consistent. Query a specific section or get all rules.', {
|
|
268
|
+
section: z
|
|
269
|
+
.string()
|
|
270
|
+
.optional()
|
|
271
|
+
.describe('Optional section id: "spacing", "typography", "buttons", "layout", "color", or "density". Omit to get all rules.'),
|
|
272
|
+
}, async ({ section }) => {
|
|
273
|
+
if (section) {
|
|
274
|
+
const s = section.trim().toLowerCase();
|
|
275
|
+
const rule = DESIGN_RULES.find((r) => r.id === s);
|
|
276
|
+
if (!rule) {
|
|
277
|
+
return {
|
|
278
|
+
content: [
|
|
279
|
+
{
|
|
280
|
+
type: 'text',
|
|
281
|
+
text: JSON.stringify({
|
|
282
|
+
error: `Section "${section}" not found.`,
|
|
283
|
+
availableSections: DESIGN_RULES.map((r) => ({
|
|
284
|
+
id: r.id,
|
|
285
|
+
title: r.title,
|
|
286
|
+
})),
|
|
287
|
+
}),
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
isError: true,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
content: [
|
|
295
|
+
{
|
|
296
|
+
type: 'text',
|
|
297
|
+
text: `## ${rule.title}\n\n${rule.body}`,
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
content: [
|
|
304
|
+
{
|
|
305
|
+
type: 'text',
|
|
306
|
+
text: DESIGN_RULES_SUMMARY,
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
};
|
|
310
|
+
});
|
|
263
311
|
// ─── Start ────────────────────────────────────────────────────────────────────
|
|
264
312
|
const transport = new StdioServerTransport();
|
|
265
313
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lucent-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "An AI-first React component library with machine-readable manifests.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -30,18 +30,6 @@
|
|
|
30
30
|
"dist-server",
|
|
31
31
|
"dist-cli"
|
|
32
32
|
],
|
|
33
|
-
"scripts": {
|
|
34
|
-
"dev": "vite --config vite.dev.config.ts",
|
|
35
|
-
"build": "vite build",
|
|
36
|
-
"build:server": "tsc -p server/tsconfig.json",
|
|
37
|
-
"build:cli": "tsc -p cli/tsconfig.json && cp cli/template.manifest.json dist-cli/cli/template.manifest.json",
|
|
38
|
-
"test": "vitest run",
|
|
39
|
-
"test:watch": "vitest",
|
|
40
|
-
"prepublishOnly": "tsc --noEmit && pnpm build && pnpm build:server && pnpm build:cli",
|
|
41
|
-
"changeset": "changeset",
|
|
42
|
-
"version-packages": "changeset version",
|
|
43
|
-
"release": "pnpm prepublishOnly && changeset publish"
|
|
44
|
-
},
|
|
45
33
|
"keywords": [
|
|
46
34
|
"react",
|
|
47
35
|
"component-library",
|
|
@@ -56,7 +44,6 @@
|
|
|
56
44
|
},
|
|
57
45
|
"author": "Rozina Szogyenyi",
|
|
58
46
|
"license": "MIT",
|
|
59
|
-
"packageManager": "pnpm@10.30.3",
|
|
60
47
|
"peerDependencies": {
|
|
61
48
|
"react": "^18.0.0 || ^19.0.0",
|
|
62
49
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
@@ -78,5 +65,16 @@
|
|
|
78
65
|
"dependencies": {
|
|
79
66
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
80
67
|
"zod": "^4.3.6"
|
|
68
|
+
},
|
|
69
|
+
"scripts": {
|
|
70
|
+
"dev": "vite --config vite.dev.config.ts",
|
|
71
|
+
"build": "vite build",
|
|
72
|
+
"build:server": "tsc -p server/tsconfig.json",
|
|
73
|
+
"build:cli": "tsc -p cli/tsconfig.json && cp cli/template.manifest.json dist-cli/cli/template.manifest.json",
|
|
74
|
+
"test": "vitest run",
|
|
75
|
+
"test:watch": "vitest",
|
|
76
|
+
"changeset": "changeset",
|
|
77
|
+
"version-packages": "changeset version",
|
|
78
|
+
"release": "pnpm prepublishOnly && changeset publish"
|
|
81
79
|
}
|
|
82
|
-
}
|
|
80
|
+
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { RECIPE as ProfileCard } from '../src/manifest/recipes/profile-card.recipe.js';
|
|
2
|
-
import { RECIPE as SettingsPanel } from '../src/manifest/recipes/settings-panel.recipe.js';
|
|
3
|
-
import { RECIPE as StatsRow } from '../src/manifest/recipes/stats-row.recipe.js';
|
|
4
|
-
import { RECIPE as ActionBar } from '../src/manifest/recipes/action-bar.recipe.js';
|
|
5
|
-
import { RECIPE as FormLayout } from '../src/manifest/recipes/form-layout.recipe.js';
|
|
6
|
-
import { RECIPE as EmptyStateCard } from '../src/manifest/recipes/empty-state-card.recipe.js';
|
|
7
|
-
import { RECIPE as CollapsibleCard } from '../src/manifest/recipes/collapsible-card.recipe.js';
|
|
8
|
-
import { RECIPE as SearchFilterBar } from '../src/manifest/recipes/search-filter-bar.recipe.js';
|
|
9
|
-
export const ALL_RECIPES = [
|
|
10
|
-
ProfileCard,
|
|
11
|
-
SettingsPanel,
|
|
12
|
-
StatsRow,
|
|
13
|
-
ActionBar,
|
|
14
|
-
FormLayout,
|
|
15
|
-
EmptyStateCard,
|
|
16
|
-
CollapsibleCard,
|
|
17
|
-
SearchFilterBar,
|
|
18
|
-
];
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
export const RECIPE = {
|
|
2
|
-
id: 'action-bar',
|
|
3
|
-
name: 'Action Bar',
|
|
4
|
-
description: 'Page-level or card-level header pairing a title with action buttons. Page headers use breadcrumb, large display title, and a divider below; card headers are compact with small text.',
|
|
5
|
-
category: 'action',
|
|
6
|
-
components: ['text', 'button', 'row', 'stack', 'breadcrumb', 'divider', 'card'],
|
|
7
|
-
structure: `
|
|
8
|
-
Page header:
|
|
9
|
-
Stack gap="4"
|
|
10
|
-
├── Breadcrumb ← navigation context
|
|
11
|
-
├── Row justify="between" align="end"
|
|
12
|
-
│ ├── Stack gap="1"
|
|
13
|
-
│ │ ├── Text (h1, 3xl, bold, display) ← page title
|
|
14
|
-
│ │ └── Text (sm, secondary) ← subtitle
|
|
15
|
-
│ └── Row gap="2" ← actions
|
|
16
|
-
│ ├── Button (outline, sm)
|
|
17
|
-
│ └── Button (primary, sm)
|
|
18
|
-
└── Divider
|
|
19
|
-
|
|
20
|
-
Card header:
|
|
21
|
-
Row justify="between" align="start"
|
|
22
|
-
├── Stack gap="1"
|
|
23
|
-
│ ├── Text (xs, secondary, uppercase, tight) ← category label
|
|
24
|
-
│ └── Text (md, semibold, tight) ← section title
|
|
25
|
-
└── Row gap="1" ← compact actions (top-aligned)
|
|
26
|
-
└── Button[] (ghost, xs)
|
|
27
|
-
`.trim(),
|
|
28
|
-
code: `<Stack gap="4">
|
|
29
|
-
<Breadcrumb items={[
|
|
30
|
-
{ label: 'Home', href: '#' },
|
|
31
|
-
{ label: 'Projects', href: '#' },
|
|
32
|
-
{ label: 'Acme Corp' },
|
|
33
|
-
]} />
|
|
34
|
-
<Row justify="between" align="end">
|
|
35
|
-
<Stack gap="1">
|
|
36
|
-
<Text as="h1" size="3xl" weight="bold" family="display">Acme Corp</Text>
|
|
37
|
-
<Text size="sm" color="secondary">Last updated 5 minutes ago</Text>
|
|
38
|
-
</Stack>
|
|
39
|
-
<Row gap="2">
|
|
40
|
-
<Button variant="outline" size="sm">Export</Button>
|
|
41
|
-
<Button variant="primary" size="sm">New report</Button>
|
|
42
|
-
</Row>
|
|
43
|
-
</Row>
|
|
44
|
-
<Divider />
|
|
45
|
-
</Stack>`,
|
|
46
|
-
variants: [
|
|
47
|
-
{
|
|
48
|
-
title: 'Page header — danger zone',
|
|
49
|
-
code: `<Stack gap="4">
|
|
50
|
-
<Breadcrumb items={[
|
|
51
|
-
{ label: 'Home', href: '#' },
|
|
52
|
-
{ label: 'Settings', href: '#' },
|
|
53
|
-
{ label: 'Danger zone' },
|
|
54
|
-
]} />
|
|
55
|
-
<Row justify="between" align="end">
|
|
56
|
-
<Stack gap="1">
|
|
57
|
-
<Text as="h1" size="3xl" weight="bold" family="display">Danger zone</Text>
|
|
58
|
-
<Text size="sm" color="secondary">These actions are irreversible.</Text>
|
|
59
|
-
</Stack>
|
|
60
|
-
<Button variant="danger" size="sm">Delete project</Button>
|
|
61
|
-
</Row>
|
|
62
|
-
<Divider />
|
|
63
|
-
</Stack>`,
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
title: 'Card header (compact with label)',
|
|
67
|
-
code: `<Card variant="outline" padding="md" style={{ width: 400 }}>
|
|
68
|
-
<Stack gap="4">
|
|
69
|
-
<Row justify="between" align="start">
|
|
70
|
-
<Stack gap="1">
|
|
71
|
-
<Text size="xs" color="secondary" weight="medium" style={{ letterSpacing: 'var(--lucent-letter-spacing-tight)', textTransform: 'uppercase' }}>Activity</Text>
|
|
72
|
-
<Text size="md" weight="semibold" style={{ letterSpacing: 'var(--lucent-letter-spacing-base)' }}>Recent activity</Text>
|
|
73
|
-
</Stack>
|
|
74
|
-
<Row gap="1">
|
|
75
|
-
<Button variant="ghost" size="xs">Filter</Button>
|
|
76
|
-
<Button variant="ghost" size="xs">Export</Button>
|
|
77
|
-
</Row>
|
|
78
|
-
</Row>
|
|
79
|
-
<Text size="xs" color="secondary">No activity to show yet.</Text>
|
|
80
|
-
</Stack>
|
|
81
|
-
</Card>`,
|
|
82
|
-
},
|
|
83
|
-
],
|
|
84
|
-
designNotes: 'Page headers and card headers have very different scales. Page headers use 3xl ' +
|
|
85
|
-
'display font for the title, Breadcrumb for navigation context, and a Divider to ' +
|
|
86
|
-
'separate the header from page content below. align="end" on the Row anchors the ' +
|
|
87
|
-
'buttons to the baseline of the title block so they sit level with the subtitle. ' +
|
|
88
|
-
'Card headers are compact: sm semibold text with xs/ghost buttons that recede ' +
|
|
89
|
-
'visually. The primary action is always rightmost following natural reading order. ' +
|
|
90
|
-
'Both patterns use justify="between" to push title and actions to opposite edges.',
|
|
91
|
-
};
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
export const RECIPE = {
|
|
2
|
-
id: 'collapsible-card',
|
|
3
|
-
name: 'Collapsible Card',
|
|
4
|
-
description: 'Card with an expandable/collapsible section using smooth height animation, available in all card variants.',
|
|
5
|
-
category: 'card',
|
|
6
|
-
components: ['card', 'collapsible', 'text'],
|
|
7
|
-
structure: `
|
|
8
|
-
Card (padding="none", hoverable)
|
|
9
|
-
└── Collapsible
|
|
10
|
-
├── trigger: Text (sm, semibold) ← clickable header
|
|
11
|
-
└── children ← collapsible body
|
|
12
|
-
└── Text (sm, secondary) ← content
|
|
13
|
-
`.trim(),
|
|
14
|
-
code: `<Card variant="outline" padding="none" hoverable style={{ width: 360 }}>
|
|
15
|
-
<Collapsible
|
|
16
|
-
trigger={<Text as="span" weight="semibold" size="sm">Details</Text>}
|
|
17
|
-
defaultOpen
|
|
18
|
-
>
|
|
19
|
-
<Text size="sm" color="secondary">
|
|
20
|
-
The Collapsible auto-detects its Card parent and bleeds the trigger
|
|
21
|
-
full-width. Content inherits the card's padding.
|
|
22
|
-
</Text>
|
|
23
|
-
</Collapsible>
|
|
24
|
-
</Card>`,
|
|
25
|
-
variants: [
|
|
26
|
-
{
|
|
27
|
-
title: 'Ghost variant',
|
|
28
|
-
code: `<Card variant="ghost" padding="none" hoverable style={{ width: 360 }}>
|
|
29
|
-
<Collapsible trigger={<Text as="span" weight="semibold" size="sm">FAQ item</Text>}>
|
|
30
|
-
<Text size="sm" color="secondary">
|
|
31
|
-
Transparent container — content floats directly on the page surface.
|
|
32
|
-
</Text>
|
|
33
|
-
</Collapsible>
|
|
34
|
-
</Card>`,
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
title: 'Elevated variant',
|
|
38
|
-
code: `<Card variant="elevated" padding="none" hoverable style={{ width: 360 }}>
|
|
39
|
-
<Collapsible trigger={<Text as="span" weight="semibold" size="sm">Advanced options</Text>}>
|
|
40
|
-
<Text size="sm" color="secondary">
|
|
41
|
-
Elevated cards cast a shadow — use for prominent collapsible sections.
|
|
42
|
-
</Text>
|
|
43
|
-
</Collapsible>
|
|
44
|
-
</Card>`,
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
title: 'Combo (two-tone nested cards)',
|
|
48
|
-
code: `<Card variant="filled" padding="none" hoverable style={{ width: 360 }}>
|
|
49
|
-
<Collapsible
|
|
50
|
-
padded={false}
|
|
51
|
-
trigger={<Text as="span" weight="semibold" size="sm">Combo layout</Text>}
|
|
52
|
-
>
|
|
53
|
-
<Card
|
|
54
|
-
variant="elevated"
|
|
55
|
-
padding="sm"
|
|
56
|
-
style={{ margin: 'var(--lucent-space-1) var(--lucent-space-2) var(--lucent-space-2)' }}
|
|
57
|
-
>
|
|
58
|
-
<Text size="sm" color="secondary">
|
|
59
|
-
Two-tone layout — flat trigger surface, elevated body.
|
|
60
|
-
</Text>
|
|
61
|
-
</Card>
|
|
62
|
-
</Collapsible>
|
|
63
|
-
</Card>`,
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
title: 'With localStorage persistence',
|
|
67
|
-
code: `function CollapsibleSection({ id, title, children }) {
|
|
68
|
-
const storageKey = \`collapsible-\${id}\`;
|
|
69
|
-
const [open, setOpen] = React.useState(() => {
|
|
70
|
-
const saved = localStorage.getItem(storageKey);
|
|
71
|
-
return saved !== null ? saved === 'true' : true;
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
const handleChange = (next) => {
|
|
75
|
-
setOpen(next);
|
|
76
|
-
localStorage.setItem(storageKey, String(next));
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
return (
|
|
80
|
-
<Card variant="outline" padding="none" hoverable>
|
|
81
|
-
<Collapsible
|
|
82
|
-
open={open}
|
|
83
|
-
onOpenChange={handleChange}
|
|
84
|
-
trigger={<Text as="span" weight="semibold" size="sm">{title}</Text>}
|
|
85
|
-
>
|
|
86
|
-
{children}
|
|
87
|
-
</Collapsible>
|
|
88
|
-
</Card>
|
|
89
|
-
);
|
|
90
|
-
}`,
|
|
91
|
-
},
|
|
92
|
-
],
|
|
93
|
-
designNotes: 'Card uses padding="none" because the Collapsible auto-detects its Card parent ' +
|
|
94
|
-
'via CardPaddingContext and bleeds the trigger full-width while applying card ' +
|
|
95
|
-
'padding to the body content. The hoverable prop gives hover lift feedback ' +
|
|
96
|
-
'without making the card itself interactive — the Collapsible trigger handles ' +
|
|
97
|
-
'clicks. For the combo variant, padded={false} on Collapsible removes its built-in ' +
|
|
98
|
-
'content padding so the nested elevated Card can control its own spacing with ' +
|
|
99
|
-
'custom margin. This creates a two-tone visual: flat trigger + elevated body.',
|
|
100
|
-
};
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
export const RECIPE = {
|
|
2
|
-
id: 'empty-state-card',
|
|
3
|
-
name: 'Empty State Card',
|
|
4
|
-
description: 'Centered empty state with illustration icon, heading, description, and call-to-action button inside a card.',
|
|
5
|
-
category: 'card',
|
|
6
|
-
components: ['card', 'empty-state', 'icon', 'button'],
|
|
7
|
-
structure: `
|
|
8
|
-
Card (outline, padding="lg")
|
|
9
|
-
└── EmptyState
|
|
10
|
-
├── illustration: Icon (xl) ← decorative SVG
|
|
11
|
-
├── title: string ← heading
|
|
12
|
-
├── description: string ← explanatory text
|
|
13
|
-
└── action: Button (secondary / primary) ← CTA
|
|
14
|
-
`.trim(),
|
|
15
|
-
code: `<Card variant="outline" padding="lg" style={{ width: 400 }}>
|
|
16
|
-
<EmptyState
|
|
17
|
-
illustration={
|
|
18
|
-
<Icon size="xl">
|
|
19
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round">
|
|
20
|
-
<circle cx={11} cy={11} r={8} />
|
|
21
|
-
<path d="M21 21l-4.35-4.35" />
|
|
22
|
-
</svg>
|
|
23
|
-
</Icon>
|
|
24
|
-
}
|
|
25
|
-
title="No results found"
|
|
26
|
-
description="Try adjusting your search or filters to find what you're looking for."
|
|
27
|
-
action={<Button variant="secondary" size="sm">Clear filters</Button>}
|
|
28
|
-
/>
|
|
29
|
-
</Card>`,
|
|
30
|
-
variants: [
|
|
31
|
-
{
|
|
32
|
-
title: 'Getting started empty state',
|
|
33
|
-
code: `<Card variant="elevated" padding="lg" style={{ width: 400 }}>
|
|
34
|
-
<EmptyState
|
|
35
|
-
illustration={
|
|
36
|
-
<Icon size="xl">
|
|
37
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round">
|
|
38
|
-
<path d="M12 5v14M5 12h14" />
|
|
39
|
-
</svg>
|
|
40
|
-
</Icon>
|
|
41
|
-
}
|
|
42
|
-
title="No projects yet"
|
|
43
|
-
description="Create your first project to get started."
|
|
44
|
-
action={<Button variant="primary" size="sm">Create project</Button>}
|
|
45
|
-
/>
|
|
46
|
-
</Card>`,
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
title: 'Error empty state with retry',
|
|
50
|
-
code: `<Card variant="outline" padding="lg" style={{ width: 400 }}>
|
|
51
|
-
<EmptyState
|
|
52
|
-
illustration={
|
|
53
|
-
<Icon size="xl">
|
|
54
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round">
|
|
55
|
-
<circle cx={12} cy={12} r={10} />
|
|
56
|
-
<path d="M12 8v4M12 16h.01" />
|
|
57
|
-
</svg>
|
|
58
|
-
</Icon>
|
|
59
|
-
}
|
|
60
|
-
title="Something went wrong"
|
|
61
|
-
description="We couldn't load your data. Please try again."
|
|
62
|
-
action={<Button variant="outline" size="sm">Retry</Button>}
|
|
63
|
-
/>
|
|
64
|
-
</Card>`,
|
|
65
|
-
},
|
|
66
|
-
],
|
|
67
|
-
designNotes: 'EmptyState handles internal centering and spacing — no need for manual Stack/Row ' +
|
|
68
|
-
'layout inside it. The illustration uses Icon at xl size for visual weight without ' +
|
|
69
|
-
'overwhelming the text. Card variant should match the context: outline for inline ' +
|
|
70
|
-
'empty states (e.g. a table with no rows), elevated for standalone pages. The ' +
|
|
71
|
-
'action button uses size="sm" to avoid competing with the heading for attention.',
|
|
72
|
-
};
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
export const RECIPE = {
|
|
2
|
-
id: 'form-layout',
|
|
3
|
-
name: 'Form Layout',
|
|
4
|
-
description: 'Stacked form with grouped sections, FormField labels, validation hints, and a submit/cancel footer.',
|
|
5
|
-
category: 'form',
|
|
6
|
-
components: ['card', 'text', 'input', 'select', 'textarea', 'checkbox', 'button', 'stack', 'row', 'divider', 'form-field'],
|
|
7
|
-
structure: `
|
|
8
|
-
Card (elevated, padding="lg")
|
|
9
|
-
└── Stack as="form" gap="6"
|
|
10
|
-
├── Stack gap="1" ← section header
|
|
11
|
-
│ ├── Text (lg, semibold) ← form title
|
|
12
|
-
│ └── Text (sm, secondary) ← description
|
|
13
|
-
├── Stack gap="4" ← field group 1
|
|
14
|
-
│ ├── Row gap="4" ← side-by-side fields
|
|
15
|
-
│ │ ├── FormField (label, required)
|
|
16
|
-
│ │ │ └── Input
|
|
17
|
-
│ │ └── FormField (label, required)
|
|
18
|
-
│ │ └── Input
|
|
19
|
-
│ ├── FormField (label)
|
|
20
|
-
│ │ └── Input
|
|
21
|
-
│ └── FormField (label)
|
|
22
|
-
│ └── Select
|
|
23
|
-
├── Divider
|
|
24
|
-
├── Stack gap="4" ← field group 2
|
|
25
|
-
│ ├── FormField (label)
|
|
26
|
-
│ │ └── Textarea
|
|
27
|
-
│ └── Checkbox (contained)
|
|
28
|
-
└── Row gap="2" justify="end" ← actions
|
|
29
|
-
├── Button (ghost)
|
|
30
|
-
└── Button (primary)
|
|
31
|
-
`.trim(),
|
|
32
|
-
code: `<Card variant="elevated" padding="lg" style={{ width: 480 }}>
|
|
33
|
-
<Stack as="form" gap="6">
|
|
34
|
-
<Stack gap="1">
|
|
35
|
-
<Text as="h2" size="lg" weight="semibold">Create project</Text>
|
|
36
|
-
<Text size="sm" color="secondary">
|
|
37
|
-
Fill in the details to set up your new project.
|
|
38
|
-
</Text>
|
|
39
|
-
</Stack>
|
|
40
|
-
<Stack gap="4">
|
|
41
|
-
<Row gap="4">
|
|
42
|
-
<FormField label="First name" htmlFor="fname" required style={{ flex: 1 }}>
|
|
43
|
-
<Input id="fname" placeholder="Jane" />
|
|
44
|
-
</FormField>
|
|
45
|
-
<FormField label="Last name" htmlFor="lname" required style={{ flex: 1 }}>
|
|
46
|
-
<Input id="lname" placeholder="Doe" />
|
|
47
|
-
</FormField>
|
|
48
|
-
</Row>
|
|
49
|
-
<FormField label="Email" htmlFor="email" helperText="We'll never share your email.">
|
|
50
|
-
<Input id="email" type="email" placeholder="jane@example.com" />
|
|
51
|
-
</FormField>
|
|
52
|
-
<FormField label="Role" htmlFor="role">
|
|
53
|
-
<Select
|
|
54
|
-
id="role"
|
|
55
|
-
placeholder="Select a role..."
|
|
56
|
-
options={[
|
|
57
|
-
{ value: 'admin', label: 'Admin' },
|
|
58
|
-
{ value: 'editor', label: 'Editor' },
|
|
59
|
-
{ value: 'viewer', label: 'Viewer' },
|
|
60
|
-
]}
|
|
61
|
-
/>
|
|
62
|
-
</FormField>
|
|
63
|
-
</Stack>
|
|
64
|
-
<Divider />
|
|
65
|
-
<Stack gap="4">
|
|
66
|
-
<FormField label="Bio" htmlFor="bio">
|
|
67
|
-
<Textarea id="bio" placeholder="Tell us about yourself..." rows={3} />
|
|
68
|
-
</FormField>
|
|
69
|
-
<Checkbox label="I agree to the terms" contained />
|
|
70
|
-
</Stack>
|
|
71
|
-
<Row gap="2" justify="end">
|
|
72
|
-
<Button variant="ghost">Cancel</Button>
|
|
73
|
-
<Button variant="primary">Create</Button>
|
|
74
|
-
</Row>
|
|
75
|
-
</Stack>
|
|
76
|
-
</Card>`,
|
|
77
|
-
variants: [
|
|
78
|
-
{
|
|
79
|
-
title: 'Inline form (no card)',
|
|
80
|
-
code: `<Stack as="form" gap="4" style={{ maxWidth: 400 }}>
|
|
81
|
-
<FormField label="Username" htmlFor="user" required helperText="Letters and numbers only, 3–20 chars.">
|
|
82
|
-
<Input id="user" placeholder="yourname" />
|
|
83
|
-
</FormField>
|
|
84
|
-
<FormField label="Password" htmlFor="pass" required>
|
|
85
|
-
<Input id="pass" type="password" />
|
|
86
|
-
</FormField>
|
|
87
|
-
<Button variant="primary">Sign in</Button>
|
|
88
|
-
</Stack>`,
|
|
89
|
-
},
|
|
90
|
-
],
|
|
91
|
-
designNotes: 'The form uses Stack as="form" to render a semantic <form> element while keeping ' +
|
|
92
|
-
'token-based vertical spacing. gap="6" between sections provides clear visual ' +
|
|
93
|
-
'grouping; gap="4" within a section keeps fields tightly related. Side-by-side ' +
|
|
94
|
-
'fields use Row with flex: 1 on each FormField so they share width equally. ' +
|
|
95
|
-
'The Divider separates logical sections (identity fields vs. profile fields). ' +
|
|
96
|
-
'Actions are right-aligned (justify="end") following the convention that the ' +
|
|
97
|
-
'primary action is the rightmost button.',
|
|
98
|
-
};
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export { RECIPE as ProfileCard } from './profile-card.recipe.js';
|
|
2
|
-
export { RECIPE as SettingsPanel } from './settings-panel.recipe.js';
|
|
3
|
-
export { RECIPE as StatsRow } from './stats-row.recipe.js';
|
|
4
|
-
export { RECIPE as ActionBar } from './action-bar.recipe.js';
|
|
5
|
-
export { RECIPE as FormLayout } from './form-layout.recipe.js';
|
|
6
|
-
export { RECIPE as EmptyStateCard } from './empty-state-card.recipe.js';
|
|
7
|
-
export { RECIPE as CollapsibleCard } from './collapsible-card.recipe.js';
|
|
8
|
-
export { RECIPE as SearchFilterBar } from './search-filter-bar.recipe.js';
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
export const RECIPE = {
|
|
2
|
-
id: 'profile-card',
|
|
3
|
-
name: 'Profile Card',
|
|
4
|
-
description: 'User profile card with avatar, name/role, bio, tag chips, stats row, and action buttons.',
|
|
5
|
-
category: 'card',
|
|
6
|
-
components: ['card', 'avatar', 'text', 'chip', 'button', 'stack', 'row', 'divider'],
|
|
7
|
-
structure: `
|
|
8
|
-
Card (elevated, equal padding via style)
|
|
9
|
-
└── Stack gap="5"
|
|
10
|
-
├── Row gap="3" align="center" ← avatar + name block
|
|
11
|
-
│ ├── Avatar (lg)
|
|
12
|
-
│ └── Stack gap="1"
|
|
13
|
-
│ ├── Row gap="2" align="center"
|
|
14
|
-
│ │ ├── Text (lg, semibold, display) ← full name
|
|
15
|
-
│ │ └── Chip (success, sm) ← status badge
|
|
16
|
-
│ └── Text (sm, secondary) ← role / subtitle
|
|
17
|
-
├── Text (sm) ← bio paragraph
|
|
18
|
-
├── Row gap="2" wrap ← skill/tag chips
|
|
19
|
-
│ └── Chip[] (neutral, borderless, clickable)
|
|
20
|
-
├── Divider
|
|
21
|
-
├── Row gap="6" justify="around" ← stats
|
|
22
|
-
│ └── Stack[] gap="0" align="center"
|
|
23
|
-
│ ├── Text (2xl, bold, display) ← stat value
|
|
24
|
-
│ └── Text (xs, secondary) ← stat label
|
|
25
|
-
└── Row gap="3" ← actions
|
|
26
|
-
├── Button (primary, full width)
|
|
27
|
-
└── Button (outline, full width)
|
|
28
|
-
`.trim(),
|
|
29
|
-
code: `<Card variant="elevated" padding="none" style={{ width: 340, padding: 'var(--lucent-space-6)' }}>
|
|
30
|
-
<Stack gap="5">
|
|
31
|
-
<Row gap="3" align="center">
|
|
32
|
-
<Avatar src="/avatars/jane.jpg" alt="Jane Doe" size="lg" />
|
|
33
|
-
<Stack gap="1">
|
|
34
|
-
<Row gap="2" align="center">
|
|
35
|
-
<Text size="lg" weight="semibold" family="display">Jane Doe</Text>
|
|
36
|
-
<Chip variant="success" size="sm" dot>Pro</Chip>
|
|
37
|
-
</Row>
|
|
38
|
-
<Text size="sm" color="secondary">Software Engineer</Text>
|
|
39
|
-
</Stack>
|
|
40
|
-
</Row>
|
|
41
|
-
<Text size="sm">
|
|
42
|
-
Building design systems and component libraries.
|
|
43
|
-
Passionate about accessible, token-driven UI.
|
|
44
|
-
</Text>
|
|
45
|
-
<Row gap="2" wrap>
|
|
46
|
-
<Chip variant="neutral" borderless onClick={() => {}}>React</Chip>
|
|
47
|
-
<Chip variant="neutral" borderless onClick={() => {}}>TypeScript</Chip>
|
|
48
|
-
<Chip variant="neutral" borderless onClick={() => {}}>Design Systems</Chip>
|
|
49
|
-
</Row>
|
|
50
|
-
<Divider />
|
|
51
|
-
<Row gap="6" justify="around">
|
|
52
|
-
<Stack gap="0" align="center">
|
|
53
|
-
<Text size="2xl" weight="bold" family="display">128</Text>
|
|
54
|
-
<Text size="xs" color="secondary">Posts</Text>
|
|
55
|
-
</Stack>
|
|
56
|
-
<Stack gap="0" align="center">
|
|
57
|
-
<Text size="2xl" weight="bold" family="display">4.2k</Text>
|
|
58
|
-
<Text size="xs" color="secondary">Followers</Text>
|
|
59
|
-
</Stack>
|
|
60
|
-
<Stack gap="0" align="center">
|
|
61
|
-
<Text size="2xl" weight="bold" family="display">312</Text>
|
|
62
|
-
<Text size="xs" color="secondary">Following</Text>
|
|
63
|
-
</Stack>
|
|
64
|
-
</Row>
|
|
65
|
-
<Row gap="3">
|
|
66
|
-
<Button variant="primary" style={{ flex: 1 }}>Follow</Button>
|
|
67
|
-
<Button variant="outline" style={{ flex: 1 }}>Message</Button>
|
|
68
|
-
</Row>
|
|
69
|
-
</Stack>
|
|
70
|
-
</Card>`,
|
|
71
|
-
variants: [
|
|
72
|
-
{
|
|
73
|
-
title: 'Compact collapsible profile',
|
|
74
|
-
code: `<Card variant="filled" padding="none" hoverable style={{ width: 280 }}>
|
|
75
|
-
<Collapsible
|
|
76
|
-
trigger={
|
|
77
|
-
<Row gap="3" align="center">
|
|
78
|
-
<Avatar alt="Jane Doe" size="md" />
|
|
79
|
-
<Stack gap="1">
|
|
80
|
-
<Text as="span" size="sm" weight="semibold">Jane Doe</Text>
|
|
81
|
-
<Text as="span" size="xs" color="secondary">Software Engineer</Text>
|
|
82
|
-
</Stack>
|
|
83
|
-
</Row>
|
|
84
|
-
}
|
|
85
|
-
defaultOpen
|
|
86
|
-
>
|
|
87
|
-
<Row gap="2" justify="end">
|
|
88
|
-
<Button variant="outline" size="sm">Message</Button>
|
|
89
|
-
<Button variant="primary" size="sm">Follow</Button>
|
|
90
|
-
</Row>
|
|
91
|
-
</Collapsible>
|
|
92
|
-
</Card>`,
|
|
93
|
-
},
|
|
94
|
-
],
|
|
95
|
-
designNotes: 'Avatar and name are grouped in a Row with center alignment so the text block ' +
|
|
96
|
-
'vertically centers against the avatar regardless of line count. The name row ' +
|
|
97
|
-
'uses gap="2" for tight coupling between name and status chip. Stats use ' +
|
|
98
|
-
'justify="around" to distribute evenly without fixed widths. Action buttons ' +
|
|
99
|
-
'use flex: 1 to split the row equally. The Divider separates informational ' +
|
|
100
|
-
'content (above) from interactive content (below).',
|
|
101
|
-
};
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
export const RECIPE = {
|
|
2
|
-
id: 'search-filter-bar',
|
|
3
|
-
name: 'Search / Filter Bar',
|
|
4
|
-
description: 'Compact toolbar of filter molecules — FilterSearch, FilterSelect, FilterMultiSelect, FilterDateRange — with sort control and view toggle. Designed to sit above a DataTable or list.',
|
|
5
|
-
category: 'dashboard',
|
|
6
|
-
components: ['filter-search', 'filter-select', 'filter-multi-select', 'filter-date-range', 'segmented-control', 'row', 'text', 'button'],
|
|
7
|
-
structure: `
|
|
8
|
-
Row gap="2" align="center" wrap
|
|
9
|
-
├── FilterSearch ← icon button, expands to Input
|
|
10
|
-
├── FilterSelect (label="Availability") ← single-select dropdown
|
|
11
|
-
├── FilterMultiSelect (label="Status") ← multi-select, stays open, count badge
|
|
12
|
-
├── FilterMultiSelect (label="Tags") ← multi-select with swatch chips
|
|
13
|
-
├── FilterDateRange (label="Date range") ← button trigger → calendar popover
|
|
14
|
-
├── Text + Button (ghost, "Clear all") ← conditional result count row
|
|
15
|
-
├── flex spacer
|
|
16
|
-
└── Row gap="2" align="center"
|
|
17
|
-
├── FilterSelect (icon=sort, "Newest") ← sort control
|
|
18
|
-
└── SegmentedControl (grid/list icons) ← view toggle
|
|
19
|
-
`.trim(),
|
|
20
|
-
code: `const [search, setSearch] = useState('');
|
|
21
|
-
const [statuses, setStatuses] = useState<string[]>([]);
|
|
22
|
-
const [tags, setTags] = useState<string[]>([]);
|
|
23
|
-
const [dateRange, setDateRange] = useState<DateRange | undefined>();
|
|
24
|
-
|
|
25
|
-
const hasFilters = statuses.length > 0 || tags.length > 0 || dateRange !== undefined;
|
|
26
|
-
const clearAll = () => { setStatuses([]); setTags([]); setDateRange(undefined); setSearch(''); };
|
|
27
|
-
|
|
28
|
-
<Row gap="2" align="center" wrap>
|
|
29
|
-
<FilterSearch placeholder="Search…" value={search} onChange={setSearch} />
|
|
30
|
-
|
|
31
|
-
<FilterSelect label="Availability" options={[
|
|
32
|
-
{ value: 'available', label: 'Available' },
|
|
33
|
-
{ value: 'notice', label: 'On notice' },
|
|
34
|
-
{ value: 'unavailable', label: 'Unavailable' },
|
|
35
|
-
]} />
|
|
36
|
-
|
|
37
|
-
<FilterMultiSelect label="Status" options={[
|
|
38
|
-
{ value: 'active', label: 'Active' },
|
|
39
|
-
{ value: 'archived', label: 'Archived' },
|
|
40
|
-
{ value: 'on-hold', label: 'On hold' },
|
|
41
|
-
]} value={statuses} onChange={setStatuses} />
|
|
42
|
-
|
|
43
|
-
<FilterMultiSelect label="Tags" options={[
|
|
44
|
-
{ value: 'react', label: 'React', swatch: '#3b82f6' },
|
|
45
|
-
{ value: 'devops', label: 'DevOps', swatch: '#10b981' },
|
|
46
|
-
{ value: 'data', label: 'Data Science', swatch: '#6366f1' },
|
|
47
|
-
]} value={tags} onChange={setTags} />
|
|
48
|
-
|
|
49
|
-
<FilterDateRange label="Date range" value={dateRange} onChange={setDateRange} />
|
|
50
|
-
|
|
51
|
-
<div style={{ flex: 1 }} />
|
|
52
|
-
|
|
53
|
-
<Row gap="2" align="center">
|
|
54
|
-
<FilterSelect label="Newest first" icon={<SortIcon />} options={[
|
|
55
|
-
{ value: 'newest', label: 'Newest first' },
|
|
56
|
-
{ value: 'oldest', label: 'Oldest first' },
|
|
57
|
-
{ value: 'name', label: 'Name A–Z' },
|
|
58
|
-
]} />
|
|
59
|
-
<SegmentedControl size="sm" defaultValue="grid" options={[
|
|
60
|
-
{ value: 'grid', label: <GridIcon /> },
|
|
61
|
-
{ value: 'list', label: <ListIcon /> },
|
|
62
|
-
]} />
|
|
63
|
-
</Row>
|
|
64
|
-
</Row>
|
|
65
|
-
|
|
66
|
-
{/* Result count between filter bar and DataTable */}
|
|
67
|
-
{hasFilters && (
|
|
68
|
-
<Row gap="2" align="center">
|
|
69
|
-
<Text size="sm" color="secondary">
|
|
70
|
-
Showing {filteredRows.length} of {allRows.length} candidates
|
|
71
|
-
</Text>
|
|
72
|
-
<Button variant="ghost" size="xs" onClick={clearAll}>Clear all</Button>
|
|
73
|
-
</Row>
|
|
74
|
-
)}`,
|
|
75
|
-
variants: [
|
|
76
|
-
{
|
|
77
|
-
title: 'Minimal — search + sort only',
|
|
78
|
-
code: `<Row gap="2" align="center">
|
|
79
|
-
<FilterSearch placeholder="Search…" />
|
|
80
|
-
<div style={{ flex: 1 }} />
|
|
81
|
-
<FilterSelect label="Newest first" icon={<SortIcon />} options={sortOptions} />
|
|
82
|
-
</Row>`,
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
title: 'With DataTable',
|
|
86
|
-
code: `const [search, setSearch] = useState('');
|
|
87
|
-
const [statuses, setStatuses] = useState<string[]>([]);
|
|
88
|
-
|
|
89
|
-
const filtered = candidates.filter(c => {
|
|
90
|
-
if (search && !c.name.toLowerCase().includes(search.toLowerCase())) return false;
|
|
91
|
-
if (statuses.length > 0 && !statuses.includes(c.status.toLowerCase())) return false;
|
|
92
|
-
return true;
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
<Stack gap="3">
|
|
96
|
-
<Row gap="2" align="center" wrap>
|
|
97
|
-
<FilterSearch placeholder="Search…" value={search} onChange={setSearch} />
|
|
98
|
-
<FilterMultiSelect label="Status" options={statusOptions}
|
|
99
|
-
value={statuses} onChange={setStatuses} />
|
|
100
|
-
</Row>
|
|
101
|
-
{statuses.length > 0 && (
|
|
102
|
-
<Row gap="2" align="center">
|
|
103
|
-
<Text size="sm" color="secondary">
|
|
104
|
-
Showing {filtered.length} of {candidates.length}
|
|
105
|
-
</Text>
|
|
106
|
-
<Button variant="ghost" size="xs" onClick={() => setStatuses([])}>Clear all</Button>
|
|
107
|
-
</Row>
|
|
108
|
-
)}
|
|
109
|
-
<DataTable columns={columns} rows={filtered} pageSize={5} />
|
|
110
|
-
</Stack>`,
|
|
111
|
-
},
|
|
112
|
-
],
|
|
113
|
-
designNotes: 'Four dedicated filter molecules replace the previous boilerplate of manually wiring Menu open state, ' +
|
|
114
|
-
'keepOpen hacks, count badges, and clear footers. FilterSearch collapses to a square icon-only button ' +
|
|
115
|
-
'and expands on click. FilterSelect wraps Menu for single-select with a "Clear" footer. ' +
|
|
116
|
-
'FilterMultiSelect builds its own portal dropdown (not Menu) so items toggle without closing — ' +
|
|
117
|
-
'it renders Checkbox + optional Chip (with swatch) per option and an always-visible "Clear all" footer. ' +
|
|
118
|
-
'FilterDateRange wraps DateRangePicker with the trigger prop to render a secondary Button. ' +
|
|
119
|
-
'A flex spacer pushes sort and view controls to the right edge. ' +
|
|
120
|
-
'Between the filter bar and DataTable, a conditional result count row shows ' +
|
|
121
|
-
'"Showing X of Y" with a ghost "Clear all" button when filters are active.',
|
|
122
|
-
};
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
export const RECIPE = {
|
|
2
|
-
id: 'settings-panel',
|
|
3
|
-
name: 'Settings Panel',
|
|
4
|
-
description: 'Settings card with section header, toggle rows with descriptions, select dropdown, and action buttons.',
|
|
5
|
-
category: 'settings',
|
|
6
|
-
components: ['card', 'text', 'badge', 'toggle', 'select', 'button', 'stack', 'row', 'divider', 'nav-menu', 'form-field', 'input', 'textarea'],
|
|
7
|
-
structure: `
|
|
8
|
-
Card (elevated, padding="lg")
|
|
9
|
-
└── Stack gap="5"
|
|
10
|
-
├── Row gap="2" align="center" ← section header
|
|
11
|
-
│ ├── Text (lg, semibold) ← title
|
|
12
|
-
│ └── Badge (accent) ← version/plan badge
|
|
13
|
-
├── Divider
|
|
14
|
-
├── Stack gap="4" ← toggle settings
|
|
15
|
-
│ ├── Row justify="between" ← setting row
|
|
16
|
-
│ │ ├── Stack gap="1"
|
|
17
|
-
│ │ │ ├── Text (sm, medium) ← setting label
|
|
18
|
-
│ │ │ └── Text (xs, secondary) ← description
|
|
19
|
-
│ │ └── Toggle
|
|
20
|
-
│ ├── Row justify="between"
|
|
21
|
-
│ │ ├── Stack gap="1"
|
|
22
|
-
│ │ │ ├── Text (sm, medium)
|
|
23
|
-
│ │ │ └── Text (xs, secondary)
|
|
24
|
-
│ │ └── Toggle
|
|
25
|
-
│ └── Row justify="between"
|
|
26
|
-
│ ├── Stack gap="1"
|
|
27
|
-
│ │ ├── Text (sm, medium)
|
|
28
|
-
│ │ └── Text (xs, secondary)
|
|
29
|
-
│ └── Select
|
|
30
|
-
├── Divider
|
|
31
|
-
└── Row gap="2" justify="end" ← actions
|
|
32
|
-
├── Button (ghost, sm)
|
|
33
|
-
└── Button (primary, sm)
|
|
34
|
-
`.trim(),
|
|
35
|
-
code: `<Card variant="elevated" padding="lg" style={{ width: 400 }}>
|
|
36
|
-
<Stack gap="5">
|
|
37
|
-
<Row gap="2" align="center">
|
|
38
|
-
<Text size="lg" weight="semibold">Notifications</Text>
|
|
39
|
-
<Badge variant="accent">Pro</Badge>
|
|
40
|
-
</Row>
|
|
41
|
-
<Divider />
|
|
42
|
-
<Stack gap="4">
|
|
43
|
-
<Row justify="between">
|
|
44
|
-
<Stack gap="1">
|
|
45
|
-
<Text size="sm" weight="medium">Email alerts</Text>
|
|
46
|
-
<Text size="xs" color="secondary">
|
|
47
|
-
Get notified when someone mentions you.
|
|
48
|
-
</Text>
|
|
49
|
-
</Stack>
|
|
50
|
-
<Toggle defaultChecked />
|
|
51
|
-
</Row>
|
|
52
|
-
<Row justify="between">
|
|
53
|
-
<Stack gap="1">
|
|
54
|
-
<Text size="sm" weight="medium">Push notifications</Text>
|
|
55
|
-
<Text size="xs" color="secondary">
|
|
56
|
-
Receive push notifications on your device.
|
|
57
|
-
</Text>
|
|
58
|
-
</Stack>
|
|
59
|
-
<Toggle />
|
|
60
|
-
</Row>
|
|
61
|
-
<Row justify="between">
|
|
62
|
-
<Stack gap="1">
|
|
63
|
-
<Text size="sm" weight="medium">Digest frequency</Text>
|
|
64
|
-
<Text size="xs" color="secondary">
|
|
65
|
-
How often to send summary emails.
|
|
66
|
-
</Text>
|
|
67
|
-
</Stack>
|
|
68
|
-
<Select
|
|
69
|
-
defaultValue="weekly"
|
|
70
|
-
options={[
|
|
71
|
-
{ value: 'daily', label: 'Daily' },
|
|
72
|
-
{ value: 'weekly', label: 'Weekly' },
|
|
73
|
-
{ value: 'monthly', label: 'Monthly' },
|
|
74
|
-
]}
|
|
75
|
-
size="sm"
|
|
76
|
-
style={{ width: 120 }}
|
|
77
|
-
/>
|
|
78
|
-
</Row>
|
|
79
|
-
</Stack>
|
|
80
|
-
<Divider />
|
|
81
|
-
<Row gap="2" justify="end">
|
|
82
|
-
<Button variant="ghost" size="sm">Reset</Button>
|
|
83
|
-
<Button variant="primary" size="sm">Save changes</Button>
|
|
84
|
-
</Row>
|
|
85
|
-
</Stack>
|
|
86
|
-
</Card>`,
|
|
87
|
-
variants: [
|
|
88
|
-
{
|
|
89
|
-
title: 'Drill-down with NavMenu',
|
|
90
|
-
code: `const [tab, setTab] = useState('profile');
|
|
91
|
-
|
|
92
|
-
<Card variant="elevated" padding="none" style={{ width: 560 }}>
|
|
93
|
-
<Row gap="0" align="stretch">
|
|
94
|
-
<div style={{ width: 160, borderRight: '1px solid var(--lucent-border-default)', padding: 'var(--lucent-space-3)', flexShrink: 0 }}>
|
|
95
|
-
<NavMenu orientation="vertical" size="sm">
|
|
96
|
-
<NavMenu.Item as="button" isActive={tab === 'profile'} onClick={() => setTab('profile')}>Profile</NavMenu.Item>
|
|
97
|
-
<NavMenu.Item as="button" isActive={tab === 'notifications'} onClick={() => setTab('notifications')}>Notifications</NavMenu.Item>
|
|
98
|
-
<NavMenu.Item as="button" isActive={tab === 'security'} onClick={() => setTab('security')}>Security</NavMenu.Item>
|
|
99
|
-
</NavMenu>
|
|
100
|
-
</div>
|
|
101
|
-
<div style={{ flex: 1, padding: 'var(--lucent-space-5)' }}>
|
|
102
|
-
{tab === 'profile' && (
|
|
103
|
-
<Stack gap="4">
|
|
104
|
-
<Text size="sm" weight="semibold">Profile</Text>
|
|
105
|
-
<FormField label="Display name" htmlFor="s-name">
|
|
106
|
-
<Input id="s-name" placeholder="Jane Doe" size="sm" />
|
|
107
|
-
</FormField>
|
|
108
|
-
<FormField label="Bio" htmlFor="s-bio">
|
|
109
|
-
<Textarea id="s-bio" placeholder="Tell us about yourself..." rows={2} />
|
|
110
|
-
</FormField>
|
|
111
|
-
<Row gap="2" justify="end">
|
|
112
|
-
<Button variant="primary" size="sm">Save</Button>
|
|
113
|
-
</Row>
|
|
114
|
-
</Stack>
|
|
115
|
-
)}
|
|
116
|
-
{tab === 'notifications' && (
|
|
117
|
-
<Stack gap="4">
|
|
118
|
-
<Text size="sm" weight="semibold">Notifications</Text>
|
|
119
|
-
<Row justify="between">
|
|
120
|
-
<Stack gap="1">
|
|
121
|
-
<Text size="sm" weight="medium">Email alerts</Text>
|
|
122
|
-
<Text size="xs" color="secondary">Get notified on mentions.</Text>
|
|
123
|
-
</Stack>
|
|
124
|
-
<Toggle defaultChecked />
|
|
125
|
-
</Row>
|
|
126
|
-
<Row justify="between">
|
|
127
|
-
<Stack gap="1">
|
|
128
|
-
<Text size="sm" weight="medium">Push notifications</Text>
|
|
129
|
-
<Text size="xs" color="secondary">Receive push on your device.</Text>
|
|
130
|
-
</Stack>
|
|
131
|
-
<Toggle />
|
|
132
|
-
</Row>
|
|
133
|
-
</Stack>
|
|
134
|
-
)}
|
|
135
|
-
{tab === 'security' && (
|
|
136
|
-
<Stack gap="4">
|
|
137
|
-
<Text size="sm" weight="semibold">Security</Text>
|
|
138
|
-
<Row justify="between">
|
|
139
|
-
<Stack gap="1">
|
|
140
|
-
<Text size="sm" weight="medium">Two-factor auth</Text>
|
|
141
|
-
<Text size="xs" color="secondary">Add an extra layer of security.</Text>
|
|
142
|
-
</Stack>
|
|
143
|
-
<Toggle />
|
|
144
|
-
</Row>
|
|
145
|
-
<FormField label="Current password" htmlFor="s-pass">
|
|
146
|
-
<Input id="s-pass" type="password" size="sm" />
|
|
147
|
-
</FormField>
|
|
148
|
-
<Row gap="2" justify="end">
|
|
149
|
-
<Button variant="primary" size="sm">Update password</Button>
|
|
150
|
-
</Row>
|
|
151
|
-
</Stack>
|
|
152
|
-
)}
|
|
153
|
-
</div>
|
|
154
|
-
</Row>
|
|
155
|
-
</Card>`,
|
|
156
|
-
},
|
|
157
|
-
],
|
|
158
|
-
designNotes: 'Each setting row uses Row with justify="between" to push the control to the far ' +
|
|
159
|
-
'right. The label + description pair is a Stack with gap="1" for tight coupling. ' +
|
|
160
|
-
'Dividers separate the header, settings body, and action footer into visual ' +
|
|
161
|
-
'sections. The Select is constrained to a fixed width (120px) to prevent the row ' +
|
|
162
|
-
'from shifting when option labels vary in length. Actions are right-aligned with ' +
|
|
163
|
-
'a ghost Reset so it visually recedes next to the primary Save. ' +
|
|
164
|
-
'The drill-down variant uses NavMenu as a left sidebar with a border-right separator. ' +
|
|
165
|
-
'Card uses padding="none" so the nav and content pane each control their own padding. ' +
|
|
166
|
-
'Row with align="stretch" ensures the nav border extends full height.',
|
|
167
|
-
};
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
export const RECIPE = {
|
|
2
|
-
id: 'stats-row',
|
|
3
|
-
name: 'Stats Row',
|
|
4
|
-
description: 'Row of individual stat cards with label, large display-font value, and trend chip with comparison.',
|
|
5
|
-
category: 'dashboard',
|
|
6
|
-
components: ['card', 'text', 'chip', 'avatar', 'stack', 'row'],
|
|
7
|
-
structure: `
|
|
8
|
-
Row gap="3" wrap
|
|
9
|
-
├── Card (outline, padding="md", flex=1)
|
|
10
|
-
│ └── Stack gap="3"
|
|
11
|
-
│ ├── Text (xs, secondary, medium) ← metric label
|
|
12
|
-
│ ├── Text (2xl, bold, display) ← value
|
|
13
|
-
│ └── Row gap="2" align="center"
|
|
14
|
-
│ ├── Chip (success/danger, sm) ← trend %
|
|
15
|
-
│ └── Text (xs, secondary) ← comparison
|
|
16
|
-
├── Card ...
|
|
17
|
-
└── Card ...
|
|
18
|
-
`.trim(),
|
|
19
|
-
code: `<Row gap="3" wrap>
|
|
20
|
-
<Card variant="outline" padding="md" style={{ flex: 1, minWidth: 180 }}>
|
|
21
|
-
<Stack gap="3">
|
|
22
|
-
<Text size="xs" color="secondary" weight="medium">Total Events</Text>
|
|
23
|
-
<Text size="2xl" weight="bold" family="display">32</Text>
|
|
24
|
-
<Row gap="2" align="center">
|
|
25
|
-
<Chip variant="success" size="sm" borderless>+20%</Chip>
|
|
26
|
-
<Text size="xs" color="secondary">25 last week</Text>
|
|
27
|
-
</Row>
|
|
28
|
-
</Stack>
|
|
29
|
-
</Card>
|
|
30
|
-
<Card variant="outline" padding="md" style={{ flex: 1, minWidth: 180 }}>
|
|
31
|
-
<Stack gap="3">
|
|
32
|
-
<Text size="xs" color="secondary" weight="medium">Total Hours</Text>
|
|
33
|
-
<Text size="2xl" weight="bold" family="display">38.2 hr</Text>
|
|
34
|
-
<Row gap="2" align="center">
|
|
35
|
-
<Chip variant="danger" size="sm" borderless>-8%</Chip>
|
|
36
|
-
<Text size="xs" color="secondary">42.0 hr last week</Text>
|
|
37
|
-
</Row>
|
|
38
|
-
</Stack>
|
|
39
|
-
</Card>
|
|
40
|
-
<Card variant="outline" padding="md" style={{ flex: 1, minWidth: 180 }}>
|
|
41
|
-
<Stack gap="3">
|
|
42
|
-
<Text size="xs" color="secondary" weight="medium">Focus Time</Text>
|
|
43
|
-
<Text size="2xl" weight="bold" family="display">16.8 hr</Text>
|
|
44
|
-
<Row gap="2" align="center">
|
|
45
|
-
<Chip variant="success" size="sm" borderless>+12%</Chip>
|
|
46
|
-
<Text size="xs" color="secondary">14.4 hr last week</Text>
|
|
47
|
-
</Row>
|
|
48
|
-
</Stack>
|
|
49
|
-
</Card>
|
|
50
|
-
</Row>`,
|
|
51
|
-
variants: [
|
|
52
|
-
{
|
|
53
|
-
title: 'Revenue cards with avatar header',
|
|
54
|
-
code: `<Row gap="3" wrap>
|
|
55
|
-
<Card variant="filled" padding="md" style={{ flex: 1, minWidth: 180 }}>
|
|
56
|
-
<Stack gap="3">
|
|
57
|
-
<Row gap="2" align="center">
|
|
58
|
-
<Avatar alt="Airbnb" size="sm" />
|
|
59
|
-
<Stack gap="0">
|
|
60
|
-
<Text size="sm" weight="semibold">Airbnb</Text>
|
|
61
|
-
<Text size="xs" color="secondary">Travel and tourism</Text>
|
|
62
|
-
</Stack>
|
|
63
|
-
</Row>
|
|
64
|
-
<Row justify="between" align="end">
|
|
65
|
-
<Stack gap="1">
|
|
66
|
-
<Row gap="2" align="baseline">
|
|
67
|
-
<Text size="lg" weight="bold" family="display">$33.2k</Text>
|
|
68
|
-
<Chip variant="success" size="sm" borderless>+37%</Chip>
|
|
69
|
-
</Row>
|
|
70
|
-
<Text size="xs" color="secondary">Recurring Revenue</Text>
|
|
71
|
-
</Stack>
|
|
72
|
-
</Row>
|
|
73
|
-
</Stack>
|
|
74
|
-
</Card>
|
|
75
|
-
<Card variant="filled" padding="md" style={{ flex: 1, minWidth: 180 }}>
|
|
76
|
-
<Stack gap="3">
|
|
77
|
-
<Row gap="2" align="center">
|
|
78
|
-
<Avatar alt="MailChimp" size="sm" />
|
|
79
|
-
<Stack gap="0">
|
|
80
|
-
<Text size="sm" weight="semibold">MailChimp</Text>
|
|
81
|
-
<Text size="xs" color="secondary">Email Marketing</Text>
|
|
82
|
-
</Stack>
|
|
83
|
-
</Row>
|
|
84
|
-
<Row justify="between" align="end">
|
|
85
|
-
<Stack gap="1">
|
|
86
|
-
<Row gap="2" align="baseline">
|
|
87
|
-
<Text size="lg" weight="bold" family="display">$3.2k</Text>
|
|
88
|
-
<Chip variant="danger" size="sm" borderless>-23%</Chip>
|
|
89
|
-
</Row>
|
|
90
|
-
<Text size="xs" color="secondary">Recurring Revenue</Text>
|
|
91
|
-
</Stack>
|
|
92
|
-
</Row>
|
|
93
|
-
</Stack>
|
|
94
|
-
</Card>
|
|
95
|
-
</Row>`,
|
|
96
|
-
},
|
|
97
|
-
],
|
|
98
|
-
designNotes: 'Each stat lives in its own Card so it reads as a discrete metric. flex: 1 with ' +
|
|
99
|
-
'minWidth ensures equal sizing that wraps gracefully. gap="3" in the Stack creates ' +
|
|
100
|
-
'three clear visual tiers: label (top), value (middle), trend context (bottom). ' +
|
|
101
|
-
'Display font on the value creates instant hierarchy. The trend row pairs a ' +
|
|
102
|
-
'color-coded Chip (success/danger) with a secondary comparison value so the user ' +
|
|
103
|
-
'gets both relative change and absolute context. The revenue variant adds an ' +
|
|
104
|
-
'Avatar + name header for entity-scoped metrics — align="baseline" on the ' +
|
|
105
|
-
'value + chip row keeps the trend visually anchored to the number.',
|
|
106
|
-
};
|
|
@@ -1,28 +0,0 @@
|
|
|
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
|
-
});
|