@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.
Files changed (175) hide show
  1. package/README.md +98 -2
  2. package/fragments.json +1 -1
  3. package/package.json +11 -5
  4. package/src/components/Accordion/Accordion.fragment.tsx +186 -0
  5. package/src/components/Accordion/Accordion.module.scss +111 -0
  6. package/src/components/Accordion/index.tsx +271 -0
  7. package/src/components/Alert/Alert.fragment.tsx +67 -42
  8. package/src/components/Alert/Alert.module.scss +31 -21
  9. package/src/components/Alert/index.tsx +202 -73
  10. package/src/components/AppShell/AppShell.fragment.tsx +315 -0
  11. package/src/components/AppShell/AppShell.module.scss +213 -0
  12. package/src/components/AppShell/index.tsx +398 -0
  13. package/src/components/Avatar/Avatar.fragment.tsx +2 -2
  14. package/src/components/Avatar/index.tsx +8 -9
  15. package/src/components/Badge/Badge.fragment.tsx +2 -2
  16. package/src/components/Badge/Badge.module.scss +16 -10
  17. package/src/components/Badge/index.tsx +20 -6
  18. package/src/components/Box/Box.fragment.tsx +168 -0
  19. package/src/components/Box/Box.module.scss +84 -0
  20. package/src/components/Box/index.tsx +78 -0
  21. package/src/components/Button/Button.fragment.tsx +2 -2
  22. package/src/components/Button/Button.module.scss +42 -0
  23. package/src/components/Button/index.tsx +67 -33
  24. package/src/components/ButtonGroup/ButtonGroup.fragment.tsx +153 -0
  25. package/src/components/ButtonGroup/ButtonGroup.module.scss +37 -0
  26. package/src/components/ButtonGroup/index.tsx +40 -0
  27. package/src/components/Card/Card.fragment.tsx +52 -26
  28. package/src/components/Card/Card.module.scss +52 -5
  29. package/src/components/Card/index.tsx +154 -53
  30. package/src/components/Chart/Chart.fragment.tsx +213 -0
  31. package/src/components/Chart/Chart.module.scss +123 -0
  32. package/src/components/Chart/index.tsx +267 -0
  33. package/src/components/Checkbox/Checkbox.fragment.tsx +1 -1
  34. package/src/components/Checkbox/Checkbox.module.scss +4 -4
  35. package/src/components/Checkbox/index.tsx +3 -4
  36. package/src/components/CodeBlock/CodeBlock.fragment.tsx +460 -0
  37. package/src/components/CodeBlock/CodeBlock.module.scss +362 -0
  38. package/src/components/CodeBlock/index.tsx +599 -0
  39. package/src/components/Collapsible/Collapsible.fragment.tsx +199 -0
  40. package/src/components/Collapsible/Collapsible.module.scss +117 -0
  41. package/src/components/Collapsible/index.tsx +219 -0
  42. package/src/components/ColorPicker/ColorPicker.fragment.tsx +196 -0
  43. package/src/components/ColorPicker/ColorPicker.module.scss +119 -0
  44. package/src/components/ColorPicker/index.tsx +129 -0
  45. package/src/components/ConversationList/ConversationList.fragment.tsx +202 -0
  46. package/src/components/ConversationList/ConversationList.module.scss +160 -0
  47. package/src/components/ConversationList/index.tsx +254 -0
  48. package/src/components/Dialog/Dialog.fragment.tsx +12 -3
  49. package/src/components/Dialog/Dialog.module.scss +26 -7
  50. package/src/components/Dialog/index.tsx +12 -15
  51. package/src/components/EmptyState/EmptyState.fragment.tsx +55 -72
  52. package/src/components/EmptyState/EmptyState.module.scss +9 -9
  53. package/src/components/EmptyState/index.tsx +104 -69
  54. package/src/components/Field/Field.fragment.tsx +165 -0
  55. package/src/components/Field/Field.module.scss +31 -0
  56. package/src/components/Field/index.tsx +143 -0
  57. package/src/components/Fieldset/Fieldset.fragment.tsx +166 -0
  58. package/src/components/Fieldset/Fieldset.module.scss +22 -0
  59. package/src/components/Fieldset/index.tsx +47 -0
  60. package/src/components/Form/Form.fragment.tsx +286 -0
  61. package/src/components/Form/Form.module.scss +8 -0
  62. package/src/components/Form/index.tsx +53 -0
  63. package/src/components/Grid/Grid.fragment.tsx +18 -18
  64. package/src/components/Grid/index.tsx +6 -1
  65. package/src/components/Header/Header.fragment.tsx +192 -0
  66. package/src/components/Header/Header.module.scss +208 -0
  67. package/src/components/Header/index.tsx +363 -0
  68. package/src/components/Icon/Icon.fragment.tsx +138 -0
  69. package/src/components/Icon/Icon.module.scss +38 -0
  70. package/src/components/Icon/index.tsx +58 -0
  71. package/src/components/Image/Image.fragment.tsx +195 -0
  72. package/src/components/Image/Image.module.scss +77 -0
  73. package/src/components/Image/index.tsx +95 -0
  74. package/src/components/Input/Input.fragment.tsx +1 -1
  75. package/src/components/Input/Input.module.scss +75 -2
  76. package/src/components/Input/index.tsx +60 -21
  77. package/src/components/Link/Link.fragment.tsx +132 -0
  78. package/src/components/Link/Link.module.scss +67 -0
  79. package/src/components/Link/index.tsx +57 -0
  80. package/src/components/List/List.fragment.tsx +152 -0
  81. package/src/components/List/List.module.scss +71 -0
  82. package/src/components/List/index.tsx +106 -0
  83. package/src/components/Listbox/Listbox.fragment.tsx +191 -0
  84. package/src/components/Listbox/Listbox.module.scss +97 -0
  85. package/src/components/Listbox/index.tsx +121 -0
  86. package/src/components/Loading/Loading.fragment.tsx +153 -0
  87. package/src/components/Loading/Loading.module.scss +256 -0
  88. package/src/components/Loading/index.tsx +236 -0
  89. package/src/components/Menu/Menu.fragment.tsx +12 -3
  90. package/src/components/Menu/Menu.module.scss +17 -1
  91. package/src/components/Menu/index.tsx +3 -3
  92. package/src/components/Message/Message.fragment.tsx +200 -0
  93. package/src/components/Message/Message.module.scss +224 -0
  94. package/src/components/Message/index.tsx +278 -0
  95. package/src/components/Popover/Popover.fragment.tsx +13 -4
  96. package/src/components/Popover/Popover.module.scss +33 -10
  97. package/src/components/Popover/index.tsx +9 -11
  98. package/src/components/Progress/Progress.fragment.tsx +1 -1
  99. package/src/components/Progress/Progress.module.scss +11 -11
  100. package/src/components/Progress/index.tsx +34 -7
  101. package/src/components/Prompt/Prompt.fragment.tsx +231 -0
  102. package/src/components/Prompt/Prompt.module.scss +243 -0
  103. package/src/components/Prompt/index.tsx +439 -0
  104. package/src/components/RadioGroup/RadioGroup.fragment.tsx +1 -1
  105. package/src/components/RadioGroup/RadioGroup.module.scss +10 -7
  106. package/src/components/RadioGroup/index.tsx +3 -4
  107. package/src/components/Select/Select.fragment.tsx +10 -1
  108. package/src/components/Select/Select.module.scss +8 -0
  109. package/src/components/Select/index.tsx +91 -12
  110. package/src/components/Separator/Separator.fragment.tsx +1 -1
  111. package/src/components/Separator/index.tsx +7 -3
  112. package/src/components/Sidebar/Sidebar.fragment.tsx +11 -2
  113. package/src/components/Sidebar/Sidebar.module.scss +91 -47
  114. package/src/components/Sidebar/index.tsx +57 -14
  115. package/src/components/Skeleton/Skeleton.fragment.tsx +6 -6
  116. package/src/components/Skeleton/Skeleton.module.scss +11 -0
  117. package/src/components/Slider/Slider.fragment.tsx +201 -0
  118. package/src/components/Slider/Slider.module.scss +87 -0
  119. package/src/components/Slider/index.tsx +88 -0
  120. package/src/components/Stack/Stack.fragment.tsx +194 -0
  121. package/src/components/Stack/Stack.module.scss +120 -0
  122. package/src/components/Stack/index.tsx +148 -0
  123. package/src/components/Table/Table.fragment.tsx +10 -3
  124. package/src/components/Table/Table.module.scss +57 -0
  125. package/src/components/Table/index.tsx +44 -6
  126. package/src/components/Tabs/Tabs.fragment.tsx +10 -1
  127. package/src/components/Tabs/Tabs.module.scss +25 -10
  128. package/src/components/Tabs/index.tsx +11 -8
  129. package/src/components/Text/Text.fragment.tsx +188 -0
  130. package/src/components/Text/Text.module.scss +82 -0
  131. package/src/components/Text/index.tsx +58 -0
  132. package/src/components/Textarea/Textarea.fragment.tsx +1 -1
  133. package/src/components/Textarea/index.tsx +3 -7
  134. package/src/components/Theme/Theme.fragment.tsx +128 -0
  135. package/src/components/Theme/ThemeToggle.module.scss +82 -0
  136. package/src/components/Theme/index.tsx +343 -0
  137. package/src/components/ThinkingIndicator/ThinkingIndicator.fragment.tsx +182 -0
  138. package/src/components/ThinkingIndicator/ThinkingIndicator.module.scss +226 -0
  139. package/src/components/ThinkingIndicator/index.tsx +258 -0
  140. package/src/components/Toast/Toast.fragment.tsx +6 -6
  141. package/src/components/Toast/Toast.module.scss +16 -1
  142. package/src/components/Toast/index.tsx +27 -11
  143. package/src/components/Toggle/Toggle.fragment.tsx +1 -1
  144. package/src/components/Toggle/Toggle.module.scss +25 -10
  145. package/src/components/Toggle/index.tsx +12 -0
  146. package/src/components/ToggleGroup/ToggleGroup.fragment.tsx +207 -0
  147. package/src/components/ToggleGroup/ToggleGroup.module.scss +134 -0
  148. package/src/components/ToggleGroup/index.tsx +144 -0
  149. package/src/components/Tooltip/Tooltip.fragment.tsx +3 -3
  150. package/src/components/Tooltip/Tooltip.module.scss +4 -4
  151. package/src/components/Tooltip/index.tsx +4 -2
  152. package/src/components/VisuallyHidden/VisuallyHidden.fragment.tsx +134 -0
  153. package/src/components/VisuallyHidden/VisuallyHidden.module.scss +13 -0
  154. package/src/components/VisuallyHidden/index.tsx +29 -0
  155. package/src/index.ts +278 -3
  156. package/src/recipes/AIChat.recipe.ts +266 -0
  157. package/src/recipes/AppShell.recipe.ts +175 -0
  158. package/src/recipes/CardGrid.recipe.ts +6 -2
  159. package/src/recipes/ChatInterface.recipe.ts +87 -0
  160. package/src/recipes/CodeExamples.recipe.ts +66 -0
  161. package/src/recipes/DashboardLayout.recipe.ts +46 -12
  162. package/src/recipes/DashboardNav.recipe.ts +183 -0
  163. package/src/recipes/LoginForm.recipe.ts +8 -1
  164. package/src/recipes/SettingsPage.recipe.ts +37 -20
  165. package/src/styles/globals.scss +31 -0
  166. package/src/tokens/_computed.scss +212 -0
  167. package/src/tokens/_density.scss +171 -0
  168. package/src/tokens/_derive.scss +287 -0
  169. package/src/tokens/_index.scss +41 -0
  170. package/src/tokens/_mixins.scss +95 -1
  171. package/src/tokens/_palettes.scss +185 -0
  172. package/src/tokens/_radius.scss +107 -0
  173. package/src/tokens/_seeds.scss +59 -0
  174. package/src/tokens/_variables.scss +507 -101
  175. 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 './index.js';
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 title="Card Title" description="A brief description">
111
- Card content goes here.
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" title="Outlined Card">
120
- Content with border.
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" title="Featured Item">
129
- Important content.
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
- title="Click Me"
139
- description="This card is interactive"
140
- onClick={() => alert('Card clicked!')}
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 without header',
174
+ description: 'Card with just body content',
149
175
  render: () => (
150
176
  <Card>
151
- Just content, no title or description.
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-space-3, $fui-space-3);
33
+ padding: var(--fui-padding-container-sm, $fui-padding-container-sm);
33
34
  }
34
35
 
35
36
  .paddingMd {
36
- padding: var(--fui-space-4, $fui-space-4);
37
+ padding: var(--fui-padding-container-md, $fui-padding-container-md);
37
38
  }
38
39
 
39
40
  .paddingLg {
40
- padding: var(--fui-space-6, $fui-space-6);
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
- .content {
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
- export interface CardProps {
7
- children?: React.ReactNode;
8
- title?: string;
9
- description?: string;
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
- export const Card = React.forwardRef<HTMLDivElement, CardProps>(
24
- function Card(
25
- {
26
- children,
27
- title,
28
- description,
29
- variant = 'default',
30
- padding = 'md',
31
- onClick,
32
- className,
33
- },
34
- ref
35
- ) {
36
- const isInteractive = !!onClick;
37
-
38
- const classes = [
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
- if (isInteractive) {
61
- return (
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
- {content}
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
+ };