@fragments-sdk/ui 0.3.0 → 0.5.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 +98 -2
- package/fragments.json +1 -1
- package/package.json +11 -5
- package/src/components/Accordion/Accordion.fragment.tsx +186 -0
- package/src/components/Accordion/Accordion.module.scss +111 -0
- package/src/components/Accordion/index.tsx +271 -0
- package/src/components/Alert/Alert.fragment.tsx +67 -42
- package/src/components/Alert/Alert.module.scss +31 -21
- package/src/components/Alert/index.tsx +202 -73
- package/src/components/AppShell/AppShell.fragment.tsx +315 -0
- package/src/components/AppShell/AppShell.module.scss +213 -0
- package/src/components/AppShell/index.tsx +398 -0
- package/src/components/Avatar/Avatar.fragment.tsx +2 -2
- package/src/components/Avatar/index.tsx +8 -9
- package/src/components/Badge/Badge.fragment.tsx +2 -2
- package/src/components/Badge/Badge.module.scss +16 -10
- package/src/components/Badge/index.tsx +20 -6
- package/src/components/Box/Box.fragment.tsx +168 -0
- package/src/components/Box/Box.module.scss +84 -0
- package/src/components/Box/index.tsx +78 -0
- package/src/components/Button/Button.fragment.tsx +2 -2
- package/src/components/Button/Button.module.scss +42 -0
- package/src/components/Button/index.tsx +67 -33
- package/src/components/ButtonGroup/ButtonGroup.fragment.tsx +153 -0
- package/src/components/ButtonGroup/ButtonGroup.module.scss +37 -0
- package/src/components/ButtonGroup/index.tsx +40 -0
- package/src/components/Card/Card.fragment.tsx +52 -26
- package/src/components/Card/Card.module.scss +52 -5
- package/src/components/Card/index.tsx +154 -53
- package/src/components/Chart/Chart.fragment.tsx +213 -0
- package/src/components/Chart/Chart.module.scss +123 -0
- package/src/components/Chart/index.tsx +267 -0
- package/src/components/Checkbox/Checkbox.fragment.tsx +1 -1
- package/src/components/Checkbox/Checkbox.module.scss +4 -4
- package/src/components/Checkbox/index.tsx +3 -4
- package/src/components/CodeBlock/CodeBlock.fragment.tsx +460 -0
- package/src/components/CodeBlock/CodeBlock.module.scss +362 -0
- package/src/components/CodeBlock/index.tsx +599 -0
- package/src/components/Collapsible/Collapsible.fragment.tsx +199 -0
- package/src/components/Collapsible/Collapsible.module.scss +117 -0
- package/src/components/Collapsible/index.tsx +219 -0
- package/src/components/ColorPicker/ColorPicker.fragment.tsx +196 -0
- package/src/components/ColorPicker/ColorPicker.module.scss +119 -0
- package/src/components/ColorPicker/index.tsx +129 -0
- package/src/components/ConversationList/ConversationList.fragment.tsx +202 -0
- package/src/components/ConversationList/ConversationList.module.scss +160 -0
- package/src/components/ConversationList/index.tsx +254 -0
- package/src/components/Dialog/Dialog.fragment.tsx +12 -3
- package/src/components/Dialog/Dialog.module.scss +26 -7
- package/src/components/Dialog/index.tsx +12 -15
- package/src/components/EmptyState/EmptyState.fragment.tsx +55 -72
- package/src/components/EmptyState/EmptyState.module.scss +9 -9
- package/src/components/EmptyState/index.tsx +104 -69
- package/src/components/Field/Field.fragment.tsx +165 -0
- package/src/components/Field/Field.module.scss +31 -0
- package/src/components/Field/index.tsx +143 -0
- package/src/components/Fieldset/Fieldset.fragment.tsx +166 -0
- package/src/components/Fieldset/Fieldset.module.scss +22 -0
- package/src/components/Fieldset/index.tsx +47 -0
- package/src/components/Form/Form.fragment.tsx +286 -0
- package/src/components/Form/Form.module.scss +8 -0
- package/src/components/Form/index.tsx +53 -0
- package/src/components/Grid/Grid.fragment.tsx +18 -18
- package/src/components/Grid/index.tsx +6 -1
- package/src/components/Header/Header.fragment.tsx +192 -0
- package/src/components/Header/Header.module.scss +208 -0
- package/src/components/Header/index.tsx +363 -0
- package/src/components/Icon/Icon.fragment.tsx +138 -0
- package/src/components/Icon/Icon.module.scss +38 -0
- package/src/components/Icon/index.tsx +58 -0
- package/src/components/Image/Image.fragment.tsx +195 -0
- package/src/components/Image/Image.module.scss +77 -0
- package/src/components/Image/index.tsx +95 -0
- package/src/components/Input/Input.fragment.tsx +1 -1
- package/src/components/Input/Input.module.scss +75 -2
- package/src/components/Input/index.tsx +60 -21
- package/src/components/Link/Link.fragment.tsx +132 -0
- package/src/components/Link/Link.module.scss +67 -0
- package/src/components/Link/index.tsx +57 -0
- package/src/components/List/List.fragment.tsx +152 -0
- package/src/components/List/List.module.scss +71 -0
- package/src/components/List/index.tsx +106 -0
- package/src/components/Listbox/Listbox.fragment.tsx +191 -0
- package/src/components/Listbox/Listbox.module.scss +97 -0
- package/src/components/Listbox/index.tsx +121 -0
- package/src/components/Loading/Loading.fragment.tsx +153 -0
- package/src/components/Loading/Loading.module.scss +256 -0
- package/src/components/Loading/index.tsx +236 -0
- package/src/components/Menu/Menu.fragment.tsx +12 -3
- package/src/components/Menu/Menu.module.scss +17 -1
- package/src/components/Menu/index.tsx +3 -3
- package/src/components/Message/Message.fragment.tsx +200 -0
- package/src/components/Message/Message.module.scss +224 -0
- package/src/components/Message/index.tsx +278 -0
- package/src/components/Popover/Popover.fragment.tsx +13 -4
- package/src/components/Popover/Popover.module.scss +33 -10
- package/src/components/Popover/index.tsx +9 -11
- package/src/components/Progress/Progress.fragment.tsx +1 -1
- package/src/components/Progress/Progress.module.scss +11 -11
- package/src/components/Progress/index.tsx +34 -7
- package/src/components/Prompt/Prompt.fragment.tsx +231 -0
- package/src/components/Prompt/Prompt.module.scss +243 -0
- package/src/components/Prompt/index.tsx +439 -0
- package/src/components/RadioGroup/RadioGroup.fragment.tsx +1 -1
- package/src/components/RadioGroup/RadioGroup.module.scss +10 -7
- package/src/components/RadioGroup/index.tsx +3 -4
- package/src/components/Select/Select.fragment.tsx +10 -1
- package/src/components/Select/Select.module.scss +8 -0
- package/src/components/Select/index.tsx +91 -12
- package/src/components/Separator/Separator.fragment.tsx +1 -1
- package/src/components/Separator/index.tsx +7 -3
- package/src/components/Sidebar/Sidebar.fragment.tsx +11 -2
- package/src/components/Sidebar/Sidebar.module.scss +91 -47
- package/src/components/Sidebar/index.tsx +57 -14
- package/src/components/Skeleton/Skeleton.fragment.tsx +6 -6
- package/src/components/Skeleton/Skeleton.module.scss +11 -0
- package/src/components/Slider/Slider.fragment.tsx +201 -0
- package/src/components/Slider/Slider.module.scss +87 -0
- package/src/components/Slider/index.tsx +88 -0
- package/src/components/Stack/Stack.fragment.tsx +194 -0
- package/src/components/Stack/Stack.module.scss +120 -0
- package/src/components/Stack/index.tsx +148 -0
- package/src/components/Table/Table.fragment.tsx +10 -3
- package/src/components/Table/Table.module.scss +57 -0
- package/src/components/Table/index.tsx +44 -6
- package/src/components/Tabs/Tabs.fragment.tsx +10 -1
- package/src/components/Tabs/Tabs.module.scss +25 -10
- package/src/components/Tabs/index.tsx +11 -8
- package/src/components/Text/Text.fragment.tsx +188 -0
- package/src/components/Text/Text.module.scss +82 -0
- package/src/components/Text/index.tsx +58 -0
- package/src/components/Textarea/Textarea.fragment.tsx +1 -1
- package/src/components/Textarea/index.tsx +3 -7
- package/src/components/Theme/Theme.fragment.tsx +128 -0
- package/src/components/Theme/ThemeToggle.module.scss +82 -0
- package/src/components/Theme/index.tsx +343 -0
- package/src/components/ThinkingIndicator/ThinkingIndicator.fragment.tsx +182 -0
- package/src/components/ThinkingIndicator/ThinkingIndicator.module.scss +226 -0
- package/src/components/ThinkingIndicator/index.tsx +258 -0
- package/src/components/Toast/Toast.fragment.tsx +6 -6
- package/src/components/Toast/Toast.module.scss +16 -1
- package/src/components/Toast/index.tsx +27 -11
- package/src/components/Toggle/Toggle.fragment.tsx +1 -1
- package/src/components/Toggle/Toggle.module.scss +25 -10
- package/src/components/Toggle/index.tsx +12 -0
- package/src/components/ToggleGroup/ToggleGroup.fragment.tsx +207 -0
- package/src/components/ToggleGroup/ToggleGroup.module.scss +134 -0
- package/src/components/ToggleGroup/index.tsx +144 -0
- package/src/components/Tooltip/Tooltip.fragment.tsx +3 -3
- package/src/components/Tooltip/Tooltip.module.scss +4 -4
- package/src/components/Tooltip/index.tsx +4 -2
- package/src/components/VisuallyHidden/VisuallyHidden.fragment.tsx +134 -0
- package/src/components/VisuallyHidden/VisuallyHidden.module.scss +13 -0
- package/src/components/VisuallyHidden/index.tsx +29 -0
- package/src/index.ts +278 -3
- package/src/recipes/AIChat.recipe.ts +266 -0
- package/src/recipes/AppShell.recipe.ts +175 -0
- package/src/recipes/CardGrid.recipe.ts +6 -2
- package/src/recipes/ChatInterface.recipe.ts +87 -0
- package/src/recipes/CodeExamples.recipe.ts +66 -0
- package/src/recipes/DashboardLayout.recipe.ts +46 -12
- package/src/recipes/DashboardNav.recipe.ts +183 -0
- package/src/recipes/LoginForm.recipe.ts +8 -1
- package/src/recipes/SettingsPage.recipe.ts +37 -20
- package/src/styles/globals.scss +31 -0
- package/src/tokens/_computed.scss +212 -0
- package/src/tokens/_density.scss +171 -0
- package/src/tokens/_derive.scss +287 -0
- package/src/tokens/_index.scss +41 -0
- package/src/tokens/_mixins.scss +95 -1
- package/src/tokens/_palettes.scss +185 -0
- package/src/tokens/_radius.scss +107 -0
- package/src/tokens/_seeds.scss +59 -0
- package/src/tokens/_variables.scss +507 -101
- package/src/utils/a11y.tsx +439 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
@use '../../tokens/variables' as *;
|
|
2
|
+
|
|
3
|
+
.group {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: row;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Gap sizes
|
|
9
|
+
.gap-xs {
|
|
10
|
+
gap: var(--fui-space-1, $fui-space-1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.gap-sm {
|
|
14
|
+
gap: var(--fui-space-2, $fui-space-2);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.gap-md {
|
|
18
|
+
gap: var(--fui-space-3, $fui-space-3);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Wrap
|
|
22
|
+
.wrap {
|
|
23
|
+
flex-wrap: wrap;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Alignment
|
|
27
|
+
.align-start {
|
|
28
|
+
justify-content: flex-start;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.align-center {
|
|
32
|
+
justify-content: center;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.align-end {
|
|
36
|
+
justify-content: flex-end;
|
|
37
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import styles from './ButtonGroup.module.scss';
|
|
3
|
+
import '../../styles/globals.scss';
|
|
4
|
+
|
|
5
|
+
export interface ButtonGroupProps {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
gap?: 'none' | 'xs' | 'sm' | 'md';
|
|
8
|
+
wrap?: boolean;
|
|
9
|
+
align?: 'start' | 'center' | 'end';
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ButtonGroup = React.forwardRef<HTMLDivElement, ButtonGroupProps>(
|
|
14
|
+
function ButtonGroup(
|
|
15
|
+
{
|
|
16
|
+
children,
|
|
17
|
+
gap = 'sm',
|
|
18
|
+
wrap = false,
|
|
19
|
+
align,
|
|
20
|
+
className,
|
|
21
|
+
},
|
|
22
|
+
ref
|
|
23
|
+
) {
|
|
24
|
+
const classes = [
|
|
25
|
+
styles.group,
|
|
26
|
+
gap !== 'none' && styles[`gap-${gap}`],
|
|
27
|
+
wrap && styles.wrap,
|
|
28
|
+
align && styles[`align-${align}`],
|
|
29
|
+
className,
|
|
30
|
+
]
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.join(' ');
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div ref={ref} className={classes}>
|
|
36
|
+
{children}
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { defineSegment } from '@fragments/core';
|
|
3
|
-
import { Card } from '
|
|
3
|
+
import { Card } from '.';
|
|
4
4
|
|
|
5
5
|
export default defineSegment({
|
|
6
6
|
component: Card,
|
|
@@ -40,15 +40,7 @@ export default defineSegment({
|
|
|
40
40
|
props: {
|
|
41
41
|
children: {
|
|
42
42
|
type: 'node',
|
|
43
|
-
description: 'Card content',
|
|
44
|
-
},
|
|
45
|
-
title: {
|
|
46
|
-
type: 'string',
|
|
47
|
-
description: 'Card header title',
|
|
48
|
-
},
|
|
49
|
-
description: {
|
|
50
|
-
type: 'string',
|
|
51
|
-
description: 'Card header description/subtitle',
|
|
43
|
+
description: 'Card content - use Card.Header, Card.Body, Card.Footer sub-components',
|
|
52
44
|
},
|
|
53
45
|
variant: {
|
|
54
46
|
type: 'enum',
|
|
@@ -86,9 +78,8 @@ export default defineSegment({
|
|
|
86
78
|
propsSummary: [
|
|
87
79
|
'variant: default|outlined|elevated (default: default)',
|
|
88
80
|
'padding: none|sm|md|lg (default: md)',
|
|
89
|
-
'title: string - header title',
|
|
90
|
-
'description: string - header subtitle',
|
|
91
81
|
'onClick: () => void - makes card interactive',
|
|
82
|
+
'Sub-components: Card.Header, Card.Title, Card.Description, Card.Body, Card.Footer',
|
|
92
83
|
],
|
|
93
84
|
scenarioTags: [
|
|
94
85
|
'layout.container',
|
|
@@ -102,13 +93,28 @@ export default defineSegment({
|
|
|
102
93
|
],
|
|
103
94
|
},
|
|
104
95
|
|
|
96
|
+
ai: {
|
|
97
|
+
compositionPattern: 'compound',
|
|
98
|
+
subComponents: ['Header', 'Title', 'Description', 'Body', 'Footer'],
|
|
99
|
+
requiredChildren: ['Body'],
|
|
100
|
+
commonPatterns: [
|
|
101
|
+
'<Card><Card.Body>{content}</Card.Body></Card>',
|
|
102
|
+
'<Card><Card.Header><Card.Title>{title}</Card.Title></Card.Header><Card.Body>{content}</Card.Body></Card>',
|
|
103
|
+
'<Card><Card.Header><Card.Title>{title}</Card.Title><Card.Description>{desc}</Card.Description></Card.Header><Card.Body>{content}</Card.Body><Card.Footer>{actions}</Card.Footer></Card>',
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
|
|
105
107
|
variants: [
|
|
106
108
|
{
|
|
107
109
|
name: 'Default',
|
|
108
110
|
description: 'Standard card with subtle shadow',
|
|
109
111
|
render: () => (
|
|
110
|
-
<Card
|
|
111
|
-
Card
|
|
112
|
+
<Card>
|
|
113
|
+
<Card.Header>
|
|
114
|
+
<Card.Title>Card Title</Card.Title>
|
|
115
|
+
<Card.Description>A brief description</Card.Description>
|
|
116
|
+
</Card.Header>
|
|
117
|
+
<Card.Body>Card content goes here.</Card.Body>
|
|
112
118
|
</Card>
|
|
113
119
|
),
|
|
114
120
|
},
|
|
@@ -116,8 +122,11 @@ export default defineSegment({
|
|
|
116
122
|
name: 'Outlined',
|
|
117
123
|
description: 'Card with border instead of shadow',
|
|
118
124
|
render: () => (
|
|
119
|
-
<Card variant="outlined"
|
|
120
|
-
|
|
125
|
+
<Card variant="outlined">
|
|
126
|
+
<Card.Header>
|
|
127
|
+
<Card.Title>Outlined Card</Card.Title>
|
|
128
|
+
</Card.Header>
|
|
129
|
+
<Card.Body>Content with border.</Card.Body>
|
|
121
130
|
</Card>
|
|
122
131
|
),
|
|
123
132
|
},
|
|
@@ -125,8 +134,11 @@ export default defineSegment({
|
|
|
125
134
|
name: 'Elevated',
|
|
126
135
|
description: 'Card with prominent shadow for emphasis',
|
|
127
136
|
render: () => (
|
|
128
|
-
<Card variant="elevated"
|
|
129
|
-
|
|
137
|
+
<Card variant="elevated">
|
|
138
|
+
<Card.Header>
|
|
139
|
+
<Card.Title>Featured Item</Card.Title>
|
|
140
|
+
</Card.Header>
|
|
141
|
+
<Card.Body>Important content.</Card.Body>
|
|
130
142
|
</Card>
|
|
131
143
|
),
|
|
132
144
|
},
|
|
@@ -134,21 +146,35 @@ export default defineSegment({
|
|
|
134
146
|
name: 'Interactive',
|
|
135
147
|
description: 'Clickable card',
|
|
136
148
|
render: () => (
|
|
137
|
-
<Card
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
Click anywhere on this card.
|
|
149
|
+
<Card onClick={() => alert('Card clicked!')}>
|
|
150
|
+
<Card.Header>
|
|
151
|
+
<Card.Title>Click Me</Card.Title>
|
|
152
|
+
<Card.Description>This card is interactive</Card.Description>
|
|
153
|
+
</Card.Header>
|
|
154
|
+
<Card.Body>Click anywhere on this card.</Card.Body>
|
|
155
|
+
</Card>
|
|
156
|
+
),
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'With Footer',
|
|
160
|
+
description: 'Card with header, body, and footer',
|
|
161
|
+
render: () => (
|
|
162
|
+
<Card>
|
|
163
|
+
<Card.Header>
|
|
164
|
+
<Card.Title>Card with Footer</Card.Title>
|
|
165
|
+
<Card.Description>Complete card layout</Card.Description>
|
|
166
|
+
</Card.Header>
|
|
167
|
+
<Card.Body>Main content area.</Card.Body>
|
|
168
|
+
<Card.Footer>Footer actions go here</Card.Footer>
|
|
143
169
|
</Card>
|
|
144
170
|
),
|
|
145
171
|
},
|
|
146
172
|
{
|
|
147
173
|
name: 'Content Only',
|
|
148
|
-
description: 'Card
|
|
174
|
+
description: 'Card with just body content',
|
|
149
175
|
render: () => (
|
|
150
176
|
<Card>
|
|
151
|
-
Just content, no
|
|
177
|
+
<Card.Body>Just content, no header or footer.</Card.Body>
|
|
152
178
|
</Card>
|
|
153
179
|
),
|
|
154
180
|
},
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
font-family: var(--fui-font-sans, $fui-font-sans);
|
|
7
7
|
transition:
|
|
8
8
|
box-shadow var(--fui-transition-fast, $fui-transition-fast),
|
|
9
|
-
border-color var(--fui-transition-fast, $fui-transition-fast)
|
|
9
|
+
border-color var(--fui-transition-fast, $fui-transition-fast),
|
|
10
|
+
transform var(--fui-transition-fast, $fui-transition-fast);
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
// Variants
|
|
@@ -29,15 +30,15 @@
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
.paddingSm {
|
|
32
|
-
padding: var(--fui-
|
|
33
|
+
padding: var(--fui-padding-container-sm, $fui-padding-container-sm);
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
.paddingMd {
|
|
36
|
-
padding: var(--fui-
|
|
37
|
+
padding: var(--fui-padding-container-md, $fui-padding-container-md);
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
.paddingLg {
|
|
40
|
-
padding: var(--fui-
|
|
41
|
+
padding: var(--fui-padding-container-lg, $fui-padding-container-lg);
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
// Interactive card (when onClick is provided)
|
|
@@ -53,10 +54,12 @@
|
|
|
53
54
|
&:hover {
|
|
54
55
|
border-color: var(--fui-border-strong, $fui-border-strong);
|
|
55
56
|
box-shadow: var(--fui-shadow-md, $fui-shadow-md);
|
|
57
|
+
transform: translateY(-2px);
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
&:active {
|
|
59
61
|
box-shadow: var(--fui-shadow-sm, $fui-shadow-sm);
|
|
62
|
+
transform: translateY(0);
|
|
60
63
|
}
|
|
61
64
|
}
|
|
62
65
|
|
|
@@ -79,8 +82,52 @@
|
|
|
79
82
|
line-height: var(--fui-line-height-normal, $fui-line-height-normal);
|
|
80
83
|
}
|
|
81
84
|
|
|
82
|
-
.
|
|
85
|
+
.body {
|
|
83
86
|
color: var(--fui-text-primary, $fui-text-primary);
|
|
84
87
|
font-size: var(--fui-font-size-sm, $fui-font-size-sm);
|
|
85
88
|
line-height: var(--fui-line-height-normal, $fui-line-height-normal);
|
|
86
89
|
}
|
|
90
|
+
|
|
91
|
+
.footer {
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
gap: var(--fui-space-2, $fui-space-2);
|
|
95
|
+
margin-top: var(--fui-space-4, $fui-space-4);
|
|
96
|
+
padding-top: var(--fui-space-4, $fui-space-4);
|
|
97
|
+
border-top: 1px solid var(--fui-border, $fui-border);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ============================================
|
|
101
|
+
// Accessibility: Reduced Motion
|
|
102
|
+
// ============================================
|
|
103
|
+
|
|
104
|
+
@media (prefers-reduced-motion: reduce) {
|
|
105
|
+
.card {
|
|
106
|
+
transition: none;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.interactive {
|
|
110
|
+
&:hover {
|
|
111
|
+
transform: none;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
&:active {
|
|
115
|
+
transform: none;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ============================================
|
|
121
|
+
// Accessibility: High Contrast Mode
|
|
122
|
+
// ============================================
|
|
123
|
+
|
|
124
|
+
@media (prefers-contrast: more) {
|
|
125
|
+
.card {
|
|
126
|
+
border-width: 2px;
|
|
127
|
+
border-color: var(--fui-text-primary, $fui-text-primary);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.footer {
|
|
131
|
+
border-top-width: 2px;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -3,16 +3,61 @@ import styles from './Card.module.scss';
|
|
|
3
3
|
// Import globals to ensure CSS variables are defined
|
|
4
4
|
import '../../styles/globals.scss';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
// ============================================
|
|
7
|
+
// Types
|
|
8
|
+
// ============================================
|
|
9
|
+
|
|
10
|
+
export interface CardProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onClick'> {
|
|
11
|
+
children: React.ReactNode;
|
|
10
12
|
variant?: 'default' | 'outlined' | 'elevated';
|
|
11
13
|
padding?: 'none' | 'sm' | 'md' | 'lg';
|
|
12
14
|
onClick?: () => void;
|
|
13
|
-
className?: string;
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
export interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
18
|
+
children: React.ReactNode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CardTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
|
|
22
|
+
children: React.ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CardDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {
|
|
26
|
+
children: React.ReactNode;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CardBodyProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
30
|
+
children: React.ReactNode;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CardFooterProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
34
|
+
children: React.ReactNode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ============================================
|
|
38
|
+
// Context
|
|
39
|
+
// ============================================
|
|
40
|
+
|
|
41
|
+
interface CardContextValue {
|
|
42
|
+
variant: 'default' | 'outlined' | 'elevated';
|
|
43
|
+
padding: 'none' | 'sm' | 'md' | 'lg';
|
|
44
|
+
isInteractive: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const CardContext = React.createContext<CardContextValue | null>(null);
|
|
48
|
+
|
|
49
|
+
function useCardContext() {
|
|
50
|
+
const context = React.useContext(CardContext);
|
|
51
|
+
if (!context) {
|
|
52
|
+
throw new Error('Card compound components must be used within a Card');
|
|
53
|
+
}
|
|
54
|
+
return context;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ============================================
|
|
58
|
+
// Padding Map
|
|
59
|
+
// ============================================
|
|
60
|
+
|
|
16
61
|
const paddingMap = {
|
|
17
62
|
none: styles.paddingNone,
|
|
18
63
|
sm: styles.paddingSm,
|
|
@@ -20,60 +65,116 @@ const paddingMap = {
|
|
|
20
65
|
lg: styles.paddingLg,
|
|
21
66
|
};
|
|
22
67
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
styles.card,
|
|
40
|
-
styles[variant],
|
|
41
|
-
paddingMap[padding],
|
|
42
|
-
isInteractive && styles.interactive,
|
|
43
|
-
className,
|
|
44
|
-
]
|
|
45
|
-
.filter(Boolean)
|
|
46
|
-
.join(' ');
|
|
47
|
-
|
|
48
|
-
const content = (
|
|
49
|
-
<>
|
|
50
|
-
{(title || description) && (
|
|
51
|
-
<div className={children ? styles.header : undefined}>
|
|
52
|
-
{title && <h3 className={styles.title}>{title}</h3>}
|
|
53
|
-
{description && <p className={styles.description}>{description}</p>}
|
|
54
|
-
</div>
|
|
55
|
-
)}
|
|
56
|
-
{children && <div className={styles.content}>{children}</div>}
|
|
57
|
-
</>
|
|
58
|
-
);
|
|
68
|
+
// ============================================
|
|
69
|
+
// Components
|
|
70
|
+
// ============================================
|
|
71
|
+
|
|
72
|
+
function CardRoot({
|
|
73
|
+
children,
|
|
74
|
+
variant = 'default',
|
|
75
|
+
padding = 'md',
|
|
76
|
+
onClick,
|
|
77
|
+
className,
|
|
78
|
+
style,
|
|
79
|
+
'aria-label': ariaLabel,
|
|
80
|
+
'aria-describedby': ariaDescribedBy,
|
|
81
|
+
...htmlProps
|
|
82
|
+
}: CardProps) {
|
|
83
|
+
const isInteractive = !!onClick;
|
|
59
84
|
|
|
60
|
-
|
|
61
|
-
|
|
85
|
+
const classes = [
|
|
86
|
+
styles.card,
|
|
87
|
+
styles[variant],
|
|
88
|
+
paddingMap[padding],
|
|
89
|
+
isInteractive && styles.interactive,
|
|
90
|
+
className,
|
|
91
|
+
]
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
.join(' ');
|
|
94
|
+
|
|
95
|
+
const contextValue: CardContextValue = {
|
|
96
|
+
variant,
|
|
97
|
+
padding,
|
|
98
|
+
isInteractive,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
if (isInteractive) {
|
|
102
|
+
return (
|
|
103
|
+
<CardContext.Provider value={contextValue}>
|
|
62
104
|
<button
|
|
63
|
-
ref={ref as React.Ref<HTMLButtonElement>}
|
|
64
105
|
type="button"
|
|
106
|
+
{...(htmlProps as React.ButtonHTMLAttributes<HTMLButtonElement>)}
|
|
65
107
|
onClick={onClick}
|
|
66
108
|
className={classes}
|
|
109
|
+
style={style}
|
|
110
|
+
aria-label={ariaLabel}
|
|
111
|
+
aria-describedby={ariaDescribedBy}
|
|
67
112
|
>
|
|
68
|
-
{
|
|
113
|
+
{children}
|
|
69
114
|
</button>
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return (
|
|
74
|
-
<div ref={ref} className={classes}>
|
|
75
|
-
{content}
|
|
76
|
-
</div>
|
|
115
|
+
</CardContext.Provider>
|
|
77
116
|
);
|
|
78
117
|
}
|
|
79
|
-
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<CardContext.Provider value={contextValue}>
|
|
121
|
+
<article
|
|
122
|
+
{...htmlProps}
|
|
123
|
+
className={classes}
|
|
124
|
+
style={style}
|
|
125
|
+
aria-label={ariaLabel}
|
|
126
|
+
aria-describedby={ariaDescribedBy}
|
|
127
|
+
>
|
|
128
|
+
{children}
|
|
129
|
+
</article>
|
|
130
|
+
</CardContext.Provider>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function CardHeader({ children, className, ...htmlProps }: CardHeaderProps) {
|
|
135
|
+
const classes = [styles.header, className].filter(Boolean).join(' ');
|
|
136
|
+
return <div {...htmlProps} className={classes}>{children}</div>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function CardTitle({ children, className, ...htmlProps }: CardTitleProps) {
|
|
140
|
+
const classes = [styles.title, className].filter(Boolean).join(' ');
|
|
141
|
+
return <h3 {...htmlProps} className={classes}>{children}</h3>;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function CardDescription({ children, className, ...htmlProps }: CardDescriptionProps) {
|
|
145
|
+
const classes = [styles.description, className].filter(Boolean).join(' ');
|
|
146
|
+
return <p {...htmlProps} className={classes}>{children}</p>;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function CardBody({ children, className, ...htmlProps }: CardBodyProps) {
|
|
150
|
+
const classes = [styles.body, className].filter(Boolean).join(' ');
|
|
151
|
+
return <div {...htmlProps} className={classes}>{children}</div>;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function CardFooter({ children, className, ...htmlProps }: CardFooterProps) {
|
|
155
|
+
const classes = [styles.footer, className].filter(Boolean).join(' ');
|
|
156
|
+
return <div {...htmlProps} className={classes}>{children}</div>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ============================================
|
|
160
|
+
// Export compound component
|
|
161
|
+
// ============================================
|
|
162
|
+
|
|
163
|
+
export const Card = Object.assign(CardRoot, {
|
|
164
|
+
Header: CardHeader,
|
|
165
|
+
Title: CardTitle,
|
|
166
|
+
Description: CardDescription,
|
|
167
|
+
Body: CardBody,
|
|
168
|
+
Footer: CardFooter,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Re-export individual components for tree-shaking
|
|
172
|
+
export {
|
|
173
|
+
CardRoot,
|
|
174
|
+
CardHeader,
|
|
175
|
+
CardTitle,
|
|
176
|
+
CardDescription,
|
|
177
|
+
CardBody,
|
|
178
|
+
CardFooter,
|
|
179
|
+
useCardContext,
|
|
180
|
+
};
|