lucent-ui 0.40.0 → 0.41.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/dist/LucentProvider-DluNb5H9.cjs +109 -0
- package/dist/{LucentProvider-Bm39MMvv.js → LucentProvider-lGSitrJV.js} +1019 -1001
- package/dist/devtools.cjs +1 -1
- package/dist/devtools.js +1 -1
- package/dist/index.cjs +47 -42
- package/dist/index.d.ts +33 -4
- package/dist/index.js +2252 -1998
- package/dist-server/server/pattern-registry.js +6 -0
- package/dist-server/server/recipe-registry.js +18 -0
- package/dist-server/src/components/molecules/PageLayout/PageLayout.manifest.js +65 -8
- package/dist-server/src/manifest/patterns/index.js +2 -0
- package/dist-server/src/manifest/patterns/multi-step-wizard.pattern.js +180 -0
- package/dist-server/src/manifest/patterns/search-filter-panel.pattern.js +188 -0
- package/dist-server/src/manifest/patterns/tab-page.pattern.js +152 -0
- package/dist-server/src/manifest/recipes/action-bar.recipe.js +91 -0
- package/dist-server/src/manifest/recipes/collapsible-card.recipe.js +100 -0
- package/dist-server/src/manifest/recipes/empty-state-card.recipe.js +72 -0
- package/dist-server/src/manifest/recipes/form-layout.recipe.js +98 -0
- package/dist-server/src/manifest/recipes/index.js +8 -0
- package/dist-server/src/manifest/recipes/profile-card.recipe.js +101 -0
- package/dist-server/src/manifest/recipes/search-filter-bar.recipe.js +122 -0
- package/dist-server/src/manifest/recipes/settings-panel.recipe.js +167 -0
- package/dist-server/src/manifest/recipes/stats-row.recipe.js +106 -0
- package/dist-server/src/manifest/validate.test.js +28 -0
- package/package.json +1 -1
- package/dist/LucentProvider-CzEDW5SL.cjs +0 -109
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
export const PATTERN = {
|
|
2
|
+
id: 'tab-page',
|
|
3
|
+
name: 'Tab Page',
|
|
4
|
+
description: 'Tabbed content sections with card bodies. Useful for detail views, settings pages, or any multi-section content.',
|
|
5
|
+
category: 'nav',
|
|
6
|
+
components: ['tabs', 'card', 'row', 'text', 'stack', 'button'],
|
|
7
|
+
structure: `
|
|
8
|
+
Stack gap="4"
|
|
9
|
+
├── Row gap="3" align="center" justify="between"
|
|
10
|
+
│ ├── Text (xl, bold) ← page title
|
|
11
|
+
│ └── Button (outline, sm) ← optional action
|
|
12
|
+
├── Tabs
|
|
13
|
+
│ └── Tab[] — each tab's content is wrapped in a Card
|
|
14
|
+
│ └── Card
|
|
15
|
+
│ └── Stack gap="4" ← tab body content
|
|
16
|
+
`.trim(),
|
|
17
|
+
code: `<Stack gap="4">
|
|
18
|
+
<Row gap="3" align="center" justify="between">
|
|
19
|
+
<Text as="h1" size="xl" weight="bold">Project settings</Text>
|
|
20
|
+
<Button variant="outline" size="sm">Save all</Button>
|
|
21
|
+
</Row>
|
|
22
|
+
<Tabs
|
|
23
|
+
defaultValue="general"
|
|
24
|
+
tabs={[
|
|
25
|
+
{
|
|
26
|
+
value: 'general',
|
|
27
|
+
label: 'General',
|
|
28
|
+
content: (
|
|
29
|
+
<Card>
|
|
30
|
+
<Stack gap="4">
|
|
31
|
+
<Text size="sm" weight="semibold">Project name</Text>
|
|
32
|
+
<Text size="sm" color="secondary">
|
|
33
|
+
Update your project name and description to help team members identify this project.
|
|
34
|
+
</Text>
|
|
35
|
+
</Stack>
|
|
36
|
+
</Card>
|
|
37
|
+
),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
value: 'members',
|
|
41
|
+
label: 'Members',
|
|
42
|
+
content: (
|
|
43
|
+
<Card>
|
|
44
|
+
<Stack gap="4">
|
|
45
|
+
<Row justify="between" align="center">
|
|
46
|
+
<Text size="sm" weight="semibold">Team members</Text>
|
|
47
|
+
<Button variant="outline" size="sm">Invite</Button>
|
|
48
|
+
</Row>
|
|
49
|
+
<Text size="sm" color="secondary">
|
|
50
|
+
Manage who has access to this project and their permissions.
|
|
51
|
+
</Text>
|
|
52
|
+
</Stack>
|
|
53
|
+
</Card>
|
|
54
|
+
),
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
value: 'billing',
|
|
58
|
+
label: 'Billing',
|
|
59
|
+
content: (
|
|
60
|
+
<Card>
|
|
61
|
+
<Stack gap="4">
|
|
62
|
+
<Text size="sm" weight="semibold">Plan & usage</Text>
|
|
63
|
+
<Text size="sm" color="secondary">
|
|
64
|
+
Review your current plan, usage limits, and payment history.
|
|
65
|
+
</Text>
|
|
66
|
+
</Stack>
|
|
67
|
+
</Card>
|
|
68
|
+
),
|
|
69
|
+
},
|
|
70
|
+
]}
|
|
71
|
+
/>
|
|
72
|
+
</Stack>`,
|
|
73
|
+
variants: [
|
|
74
|
+
{
|
|
75
|
+
title: 'Detail view with description and no action button',
|
|
76
|
+
code: `<Stack gap="4">
|
|
77
|
+
<Stack gap="1">
|
|
78
|
+
<Text as="h1" size="xl" weight="bold">Order #1042</Text>
|
|
79
|
+
<Text size="sm" color="secondary">Placed on March 15, 2025</Text>
|
|
80
|
+
</Stack>
|
|
81
|
+
<Tabs
|
|
82
|
+
defaultValue="items"
|
|
83
|
+
tabs={[
|
|
84
|
+
{
|
|
85
|
+
value: 'items',
|
|
86
|
+
label: 'Items',
|
|
87
|
+
content: (
|
|
88
|
+
<Card>
|
|
89
|
+
<Stack gap="3">
|
|
90
|
+
<Row justify="between">
|
|
91
|
+
<Text size="sm">Widget Pro × 2</Text>
|
|
92
|
+
<Text size="sm" weight="medium">$49.98</Text>
|
|
93
|
+
</Row>
|
|
94
|
+
<Row justify="between">
|
|
95
|
+
<Text size="sm">Adapter Cable</Text>
|
|
96
|
+
<Text size="sm" weight="medium">$12.00</Text>
|
|
97
|
+
</Row>
|
|
98
|
+
</Stack>
|
|
99
|
+
</Card>
|
|
100
|
+
),
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
value: 'shipping',
|
|
104
|
+
label: 'Shipping',
|
|
105
|
+
content: (
|
|
106
|
+
<Card>
|
|
107
|
+
<Stack gap="3">
|
|
108
|
+
<Text size="sm" weight="semibold">Tracking</Text>
|
|
109
|
+
<Text size="sm" color="secondary">
|
|
110
|
+
UPS Ground — 1Z999AA10123456784
|
|
111
|
+
</Text>
|
|
112
|
+
</Stack>
|
|
113
|
+
</Card>
|
|
114
|
+
),
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
value: 'history',
|
|
118
|
+
label: 'History',
|
|
119
|
+
content: (
|
|
120
|
+
<Card>
|
|
121
|
+
<Stack gap="3">
|
|
122
|
+
<Row gap="3" align="center">
|
|
123
|
+
<Text size="xs" color="secondary" style={{ width: 80 }}>Mar 15</Text>
|
|
124
|
+
<Text size="sm">Order placed</Text>
|
|
125
|
+
</Row>
|
|
126
|
+
<Row gap="3" align="center">
|
|
127
|
+
<Text size="xs" color="secondary" style={{ width: 80 }}>Mar 16</Text>
|
|
128
|
+
<Text size="sm">Payment confirmed</Text>
|
|
129
|
+
</Row>
|
|
130
|
+
<Row gap="3" align="center">
|
|
131
|
+
<Text size="xs" color="secondary" style={{ width: 80 }}>Mar 18</Text>
|
|
132
|
+
<Text size="sm">Shipped</Text>
|
|
133
|
+
</Row>
|
|
134
|
+
</Stack>
|
|
135
|
+
</Card>
|
|
136
|
+
),
|
|
137
|
+
},
|
|
138
|
+
]}
|
|
139
|
+
/>
|
|
140
|
+
</Stack>`,
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
designNotes: 'The page title and action button sit in a Row with justify="between" to push the action ' +
|
|
144
|
+
'to the far right, and align="center" to vertically center them regardless of text size. ' +
|
|
145
|
+
'Each tab\u2019s content is wrapped in a Card to give it a contained, elevated appearance that ' +
|
|
146
|
+
'visually separates the body from the tab strip. Stack gap="4" inside each Card provides ' +
|
|
147
|
+
'consistent vertical rhythm for the tab content. The Tabs component handles its own sliding ' +
|
|
148
|
+
'indicator and ARIA tablist semantics, so the pattern only needs to compose the surrounding ' +
|
|
149
|
+
'layout. The detail-view variant drops the action button and adds a subtitle via a nested ' +
|
|
150
|
+
'Stack gap="1" — a common pattern for entity detail pages where the header provides context ' +
|
|
151
|
+
'rather than actions.',
|
|
152
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
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';
|
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
};
|