@duro-app/ui 0.19.0 → 0.20.1

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 (49) hide show
  1. package/dist/components/DetailPanel/DetailPanel.d.ts +53 -0
  2. package/dist/components/DetailPanel/DetailPanel.d.ts.map +1 -0
  3. package/dist/components/DetailPanel/DetailPanel.meta.d.ts +3 -0
  4. package/dist/components/DetailPanel/DetailPanel.meta.d.ts.map +1 -0
  5. package/dist/components/DetailPanel/DetailPanel.stories.d.ts +9 -0
  6. package/dist/components/DetailPanel/DetailPanel.stories.d.ts.map +1 -0
  7. package/dist/components/DetailPanel/styles.css.d.ts +126 -0
  8. package/dist/components/DetailPanel/styles.css.d.ts.map +1 -0
  9. package/dist/components/Dialog/Dialog.d.ts.map +1 -1
  10. package/dist/components/Dialog/Dialog.meta.d.ts +3 -0
  11. package/dist/components/Dialog/Dialog.meta.d.ts.map +1 -0
  12. package/dist/components/Dialog/Dialog.stories.d.ts.map +1 -1
  13. package/dist/components/Drawer/Drawer.d.ts.map +1 -1
  14. package/dist/components/Drawer/Drawer.meta.d.ts +3 -0
  15. package/dist/components/Drawer/Drawer.meta.d.ts.map +1 -0
  16. package/dist/components/Drawer/Drawer.stories.d.ts.map +1 -1
  17. package/dist/components/Drawer/useSwipeDismiss.d.ts.map +1 -1
  18. package/dist/components/Panel/Panel.d.ts +27 -0
  19. package/dist/components/Panel/Panel.d.ts.map +1 -0
  20. package/dist/components/Panel/Panel.meta.d.ts +3 -0
  21. package/dist/components/Panel/Panel.meta.d.ts.map +1 -0
  22. package/dist/components/Panel/Panel.stories.d.ts +10 -0
  23. package/dist/components/Panel/Panel.stories.d.ts.map +1 -0
  24. package/dist/components/Panel/styles.css.d.ts +50 -0
  25. package/dist/components/Panel/styles.css.d.ts.map +1 -0
  26. package/dist/components/Table/Table.stories.d.ts.map +1 -1
  27. package/dist/index.css +1 -1
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +2417 -2034
  31. package/dist/index.js.map +1 -1
  32. package/package.json +2 -2
  33. package/src/components/DetailPanel/DetailPanel.meta.ts +41 -0
  34. package/src/components/DetailPanel/DetailPanel.stories.tsx +270 -0
  35. package/src/components/DetailPanel/DetailPanel.tsx +312 -0
  36. package/src/components/DetailPanel/styles.css.ts +187 -0
  37. package/src/components/Dialog/Dialog.meta.ts +44 -0
  38. package/src/components/Dialog/Dialog.stories.tsx +5 -7
  39. package/src/components/Dialog/Dialog.tsx +1 -5
  40. package/src/components/Drawer/Drawer.meta.ts +45 -0
  41. package/src/components/Drawer/Drawer.stories.tsx +24 -21
  42. package/src/components/Drawer/Drawer.tsx +2 -9
  43. package/src/components/Drawer/useSwipeDismiss.ts +13 -6
  44. package/src/components/Panel/Panel.meta.ts +37 -0
  45. package/src/components/Panel/Panel.stories.tsx +124 -0
  46. package/src/components/Panel/Panel.tsx +56 -0
  47. package/src/components/Panel/styles.css.ts +55 -0
  48. package/src/components/Table/Table.stories.tsx +1 -4
  49. package/src/index.ts +2 -0
@@ -0,0 +1,187 @@
1
+ import {css} from 'react-strict-dom'
2
+ import {colors} from '@duro-app/tokens/tokens/colors.css'
3
+ import {spacing} from '@duro-app/tokens/tokens/spacing.css'
4
+ import {shadows} from '@duro-app/tokens/tokens/shadows.css'
5
+
6
+ /** Apple-standard ease: aggressive deceleration, feels like "arriving" */
7
+ const APPLE_EASE_OUT = 'cubic-bezier(0.32, 0.72, 0, 1)'
8
+ const APPLE_EASE_IN = 'cubic-bezier(0.72, 0, 0.68, 0.28)'
9
+
10
+ const OPEN_DURATION = '280ms'
11
+ const CLOSE_DURATION = '220ms'
12
+
13
+ export const styles = css.create({
14
+ // --- Outer wrapper: animates width from 0 → target ---
15
+ wrapper: {
16
+ overflow: 'hidden',
17
+ flexShrink: 0,
18
+ position: 'relative',
19
+ },
20
+
21
+ // --- Wrapper open/close: SM (360px) ---
22
+ wrapperOpenSm: {
23
+ animationName: css.keyframes({
24
+ from: {width: 0},
25
+ to: {width: 360},
26
+ }),
27
+ animationDuration: OPEN_DURATION,
28
+ animationTimingFunction: APPLE_EASE_OUT,
29
+ animationFillMode: 'both',
30
+ },
31
+ wrapperCloseSm: {
32
+ animationName: css.keyframes({
33
+ from: {width: 360},
34
+ to: {width: 0},
35
+ }),
36
+ animationDuration: CLOSE_DURATION,
37
+ animationTimingFunction: APPLE_EASE_IN,
38
+ animationFillMode: 'both',
39
+ },
40
+
41
+ // --- Wrapper open/close: MD (480px) ---
42
+ wrapperOpenMd: {
43
+ animationName: css.keyframes({
44
+ from: {width: 0},
45
+ to: {width: 480},
46
+ }),
47
+ animationDuration: OPEN_DURATION,
48
+ animationTimingFunction: APPLE_EASE_OUT,
49
+ animationFillMode: 'both',
50
+ },
51
+ wrapperCloseMd: {
52
+ animationName: css.keyframes({
53
+ from: {width: 480},
54
+ to: {width: 0},
55
+ }),
56
+ animationDuration: CLOSE_DURATION,
57
+ animationTimingFunction: APPLE_EASE_IN,
58
+ animationFillMode: 'both',
59
+ },
60
+
61
+ // --- Content panel ---
62
+ content: {
63
+ height: '100%',
64
+ display: 'flex',
65
+ flexDirection: 'column',
66
+ backgroundColor: colors.bgCard,
67
+ borderLeftWidth: 1,
68
+ borderLeftStyle: 'solid',
69
+ borderLeftColor: colors.border,
70
+ boxShadow: shadows.sm,
71
+ position: 'relative',
72
+ },
73
+
74
+ // Content must maintain its target width even while wrapper is narrower
75
+ contentSm: {width: 360},
76
+ contentMd: {width: 480},
77
+
78
+ // --- Content slide + fade animations ---
79
+ slideIn: {
80
+ animationName: css.keyframes({
81
+ from: {
82
+ transform: 'translateX(40px)',
83
+ opacity: 0,
84
+ },
85
+ to: {
86
+ transform: 'translateX(0)',
87
+ opacity: 1,
88
+ },
89
+ }),
90
+ animationDuration: OPEN_DURATION,
91
+ animationTimingFunction: APPLE_EASE_OUT,
92
+ animationFillMode: 'both',
93
+ },
94
+ slideOut: {
95
+ animationName: css.keyframes({
96
+ from: {
97
+ transform: 'translateX(0)',
98
+ opacity: 1,
99
+ },
100
+ to: {
101
+ transform: 'translateX(40px)',
102
+ opacity: 0,
103
+ },
104
+ }),
105
+ animationDuration: CLOSE_DURATION,
106
+ animationTimingFunction: APPLE_EASE_IN,
107
+ animationFillMode: 'both',
108
+ },
109
+
110
+ // --- Header ---
111
+ header: {
112
+ display: 'flex',
113
+ alignItems: 'center',
114
+ justifyContent: 'space-between',
115
+ paddingTop: spacing.lg,
116
+ paddingLeft: spacing.lg,
117
+ paddingRight: spacing.lg,
118
+ paddingBottom: spacing.lg,
119
+ gap: spacing.md,
120
+ borderBottomWidth: 1,
121
+ borderBottomStyle: 'solid',
122
+ borderBottomColor: colors.border,
123
+ },
124
+
125
+ // --- Title ---
126
+ title: {
127
+ fontSize: 18,
128
+ fontWeight: 600,
129
+ lineHeight: 1.4,
130
+ color: colors.text,
131
+ margin: 0,
132
+ flex: 1,
133
+ minWidth: 0,
134
+ },
135
+
136
+ // --- Body ---
137
+ body: {
138
+ flex: 1,
139
+ overflowY: 'auto',
140
+ },
141
+ bodyPadded: {
142
+ paddingTop: spacing.lg,
143
+ paddingLeft: spacing.lg,
144
+ paddingRight: spacing.lg,
145
+ paddingBottom: spacing.lg,
146
+ },
147
+
148
+ // --- Footer ---
149
+ footer: {
150
+ display: 'flex',
151
+ alignItems: 'center',
152
+ justifyContent: 'flex-end',
153
+ gap: spacing.sm,
154
+ paddingTop: spacing.md,
155
+ paddingBottom: spacing.md,
156
+ paddingLeft: spacing.lg,
157
+ paddingRight: spacing.lg,
158
+ borderTopWidth: 1,
159
+ borderTopStyle: 'solid',
160
+ borderTopColor: colors.border,
161
+ },
162
+
163
+ // --- Close button ---
164
+ closeButton: {
165
+ display: 'flex',
166
+ alignItems: 'center',
167
+ justifyContent: 'center',
168
+ width: 32,
169
+ height: 32,
170
+ borderRadius: 8,
171
+ borderWidth: 0,
172
+ backgroundColor: {
173
+ default: 'transparent',
174
+ ':hover': colors.bgCardHover,
175
+ },
176
+ color: colors.textMuted,
177
+ cursor: 'pointer',
178
+ padding: 0,
179
+ flexShrink: 0,
180
+ transitionProperty: 'background-color',
181
+ transitionDuration: '150ms',
182
+ },
183
+
184
+ inlineWrapper: {
185
+ display: 'inline-flex',
186
+ },
187
+ })
@@ -0,0 +1,44 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Modal dialog with backdrop overlay. Centered on screen with scale+fade animation. Traps focus, dismisses on backdrop click or Escape. Use for confirmations, forms, or content that requires user attention before continuing.',
6
+ whenToUse: [
7
+ 'Confirmation prompts (delete, revoke, destructive actions)',
8
+ 'Short forms or data entry that blocks the main flow',
9
+ 'Viewing detail content in a focused overlay (e.g. certificate details)',
10
+ 'Any modal interaction that requires user acknowledgment',
11
+ ],
12
+ whenNotToUse: [
13
+ 'Non-modal side panels — use DetailPanel',
14
+ 'Sliding panels from screen edges — use Drawer',
15
+ 'Inline content expansion — use disclosure or accordion patterns',
16
+ 'Toast/notification messages — use Alert or a toast system',
17
+ ],
18
+ anatomy: {
19
+ required: ['Root', 'Portal'],
20
+ optional: ['Trigger', 'Header', 'Title', 'Description', 'Body', 'Footer', 'Close'],
21
+ },
22
+ relatedTo: [
23
+ {component: 'Drawer', relationship: 'Drawer slides from an edge; Dialog is centered with scale animation'},
24
+ {component: 'DetailPanel', relationship: 'DetailPanel is non-modal and in-flow; Dialog is modal with backdrop'},
25
+ ],
26
+ example: `<Dialog.Root open={open} onOpenChange={setOpen}>
27
+ <Dialog.Trigger>
28
+ <Button>Open dialog</Button>
29
+ </Dialog.Trigger>
30
+ <Dialog.Portal size="md">
31
+ <Dialog.Header>
32
+ <Dialog.Title>Confirm action</Dialog.Title>
33
+ <Dialog.Close />
34
+ </Dialog.Header>
35
+ <Dialog.Body>
36
+ <Text>Are you sure you want to proceed?</Text>
37
+ </Dialog.Body>
38
+ <Dialog.Footer>
39
+ <Button variant="secondary" onClick={() => setOpen(false)}>Cancel</Button>
40
+ <Button variant="danger" onClick={handleConfirm}>Confirm</Button>
41
+ </Dialog.Footer>
42
+ </Dialog.Portal>
43
+ </Dialog.Root>`,
44
+ }
@@ -177,9 +177,7 @@ function FormDialogDemo() {
177
177
  <Dialog.Title>Edit profile</Dialog.Title>
178
178
  <Dialog.Close />
179
179
  </Dialog.Header>
180
- <Dialog.Description>
181
- Update your display name and email address.
182
- </Dialog.Description>
180
+ <Dialog.Description>Update your display name and email address.</Dialog.Description>
183
181
  <Dialog.Body>
184
182
  <Stack gap="md">
185
183
  <Field.Root>
@@ -270,8 +268,8 @@ export const NonDismissable: Story = {
270
268
  <Dialog.Body>
271
269
  <Stack gap="md">
272
270
  <Text>
273
- You must accept the terms of service to continue. This dialog cannot be
274
- dismissed by clicking outside or pressing Escape.
271
+ You must accept the terms of service to continue. This dialog cannot be dismissed by
272
+ clicking outside or pressing Escape.
275
273
  </Text>
276
274
  <Text variant="bodySm" color="muted">
277
275
  Try clicking the backdrop or pressing Escape — the dialog stays open.
@@ -309,8 +307,8 @@ function BackdropDismissDemo() {
309
307
  </Dialog.Header>
310
308
  <Dialog.Body>
311
309
  <Text>
312
- Click the dark backdrop behind this dialog to dismiss it.
313
- You can also press Escape or use the close button.
310
+ Click the dark backdrop behind this dialog to dismiss it. You can also press Escape or
311
+ use the close button.
314
312
  </Text>
315
313
  </Dialog.Body>
316
314
  </Dialog.Portal>
@@ -278,11 +278,7 @@ function Close({children, 'aria-label': ariaLabel = 'Close'}: CloseProps) {
278
278
  }
279
279
 
280
280
  return (
281
- <html.button
282
- onClick={requestClose}
283
- aria-label={ariaLabel}
284
- style={styles.closeButton}
285
- >
281
+ <html.button onClick={requestClose} aria-label={ariaLabel} style={styles.closeButton}>
286
282
  <svg width={16} height={16} viewBox="0 0 16 16" fill="none">
287
283
  <path
288
284
  d="M4 4l8 8M12 4l-8 8"
@@ -0,0 +1,45 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Modal sliding panel from a screen edge (right, left, or bottom). Slides in with backdrop overlay. Supports swipe-to-dismiss gestures on mobile, backdrop click dismiss, and Escape key. Use for secondary workflows, settings panels, or mobile-friendly detail views.',
6
+ whenToUse: [
7
+ 'Settings or configuration panels that slide in from the side',
8
+ 'Mobile-friendly detail views (bottom sheet pattern)',
9
+ 'Secondary workflows that need more space than a dialog',
10
+ 'Filters, navigation drawers, or form panels',
11
+ ],
12
+ whenNotToUse: [
13
+ 'Centered modal confirmations — use Dialog',
14
+ 'Non-modal side inspection panels — use DetailPanel',
15
+ 'Inline content expansion — use disclosure patterns',
16
+ 'Primary navigation — use SideNav',
17
+ ],
18
+ anatomy: {
19
+ required: ['Root', 'Portal'],
20
+ optional: ['Trigger', 'Header', 'Title', 'Description', 'Body', 'Footer', 'Close'],
21
+ },
22
+ relatedTo: [
23
+ {component: 'Dialog', relationship: 'Dialog is centered modal; Drawer slides from an edge'},
24
+ {component: 'DetailPanel', relationship: 'DetailPanel is non-modal and in-flow; Drawer is modal with backdrop'},
25
+ ],
26
+ example: `<Drawer.Root anchor="right" open={open} onOpenChange={setOpen}>
27
+ <Drawer.Trigger>
28
+ <Button>Open drawer</Button>
29
+ </Drawer.Trigger>
30
+ <Drawer.Portal size="md">
31
+ <Drawer.Header>
32
+ <Drawer.Title>Settings</Drawer.Title>
33
+ <Drawer.Close />
34
+ </Drawer.Header>
35
+ <Drawer.Body>
36
+ <Stack gap="md">
37
+ <Text>Drawer content goes here.</Text>
38
+ </Stack>
39
+ </Drawer.Body>
40
+ <Drawer.Footer>
41
+ <Button variant="secondary" onClick={() => setOpen(false)}>Done</Button>
42
+ </Drawer.Footer>
43
+ </Drawer.Portal>
44
+ </Drawer.Root>`,
45
+ }
@@ -31,8 +31,8 @@ export const Default: Story = {
31
31
  </Drawer.Header>
32
32
  <Drawer.Body>
33
33
  <Text>
34
- This drawer slides in from the right edge. It's useful for showing details,
35
- settings panels, or supplementary content without leaving the current page.
34
+ This drawer slides in from the right edge. It's useful for showing details, settings
35
+ panels, or supplementary content without leaving the current page.
36
36
  </Text>
37
37
  </Drawer.Body>
38
38
  <Drawer.Footer>
@@ -93,9 +93,15 @@ export const BottomAnchor: Story = {
93
93
  </Drawer.Header>
94
94
  <Drawer.Body>
95
95
  <Stack gap="sm">
96
- <Button variant="secondary" fullWidth>Share</Button>
97
- <Button variant="secondary" fullWidth>Duplicate</Button>
98
- <Button variant="danger" fullWidth>Delete</Button>
96
+ <Button variant="secondary" fullWidth>
97
+ Share
98
+ </Button>
99
+ <Button variant="secondary" fullWidth>
100
+ Duplicate
101
+ </Button>
102
+ <Button variant="danger" fullWidth>
103
+ Delete
104
+ </Button>
99
105
  </Stack>
100
106
  </Drawer.Body>
101
107
  </Drawer.Portal>
@@ -125,7 +131,10 @@ function SizesDemo() {
125
131
  <Drawer.Close />
126
132
  </Drawer.Header>
127
133
  <Drawer.Body>
128
- <Text>This drawer uses the {s} size preset ({s === 'sm' ? '360' : s === 'md' ? '480' : '640'}px max-width).</Text>
134
+ <Text>
135
+ This drawer uses the {s} size preset (
136
+ {s === 'sm' ? '360' : s === 'md' ? '480' : '640'}px max-width).
137
+ </Text>
129
138
  </Drawer.Body>
130
139
  <Drawer.Footer>
131
140
  <Drawer.Close>
@@ -164,9 +173,7 @@ function FormDrawerDemo() {
164
173
  <Drawer.Title>Edit profile</Drawer.Title>
165
174
  <Drawer.Close />
166
175
  </Drawer.Header>
167
- <Drawer.Description>
168
- Update your account information below.
169
- </Drawer.Description>
176
+ <Drawer.Description>Update your account information below.</Drawer.Description>
170
177
  <Drawer.Body>
171
178
  <Stack gap="md">
172
179
  <Field.Root>
@@ -269,8 +276,8 @@ export const NonDismissable: Story = {
269
276
  <Drawer.Body>
270
277
  <Stack gap="md">
271
278
  <Text>
272
- This drawer cannot be dismissed by clicking the backdrop or pressing Escape.
273
- You must use the explicit close action below.
279
+ This drawer cannot be dismissed by clicking the backdrop or pressing Escape. You must
280
+ use the explicit close action below.
274
281
  </Text>
275
282
  <Text variant="bodySm" color="muted">
276
283
  Try clicking outside or pressing Escape — nothing happens.
@@ -302,8 +309,8 @@ export const NoSwipeDismiss: Story = {
302
309
  </Drawer.Header>
303
310
  <Drawer.Body>
304
311
  <Text>
305
- This bottom drawer has swipe-to-dismiss disabled. On mobile, you cannot
306
- swipe down to close it — use the close button or tap the backdrop instead.
312
+ This bottom drawer has swipe-to-dismiss disabled. On mobile, you cannot swipe down to
313
+ close it — use the close button or tap the backdrop instead.
307
314
  </Text>
308
315
  </Drawer.Body>
309
316
  </Drawer.Portal>
@@ -326,12 +333,10 @@ export const SwipeDismiss: Story = {
326
333
  </Drawer.Header>
327
334
  <Drawer.Body>
328
335
  <Stack gap="md">
329
- <Text>
330
- Drag this drawer downward to dismiss it. The backdrop fades as you drag.
331
- </Text>
336
+ <Text>Drag this drawer downward to dismiss it. The backdrop fades as you drag.</Text>
332
337
  <Text variant="bodySm" color="muted">
333
- Swipe past 30% of the panel height or flick quickly to dismiss.
334
- Otherwise it snaps back.
338
+ Swipe past 30% of the panel height or flick quickly to dismiss. Otherwise it snaps
339
+ back.
335
340
  </Text>
336
341
  </Stack>
337
342
  </Drawer.Body>
@@ -354,9 +359,7 @@ export const SwipeDismissRight: Story = {
354
359
  <Drawer.Close />
355
360
  </Drawer.Header>
356
361
  <Drawer.Body>
357
- <Text>
358
- Drag this panel to the right to dismiss it. Works with touch and mouse.
359
- </Text>
362
+ <Text>Drag this panel to the right to dismiss it. Works with touch and mouse.</Text>
360
363
  </Drawer.Body>
361
364
  </Drawer.Portal>
362
365
  </Drawer.Root>
@@ -252,10 +252,7 @@ function Portal({children, size = 'md'}: PortalProps) {
252
252
  />
253
253
 
254
254
  {/* Viewport */}
255
- <html.div
256
- style={[styles.viewport, viewportAnchorMap[anchor]]}
257
- data-drawer-viewport=""
258
- >
255
+ <html.div style={[styles.viewport, viewportAnchorMap[anchor]]} data-drawer-viewport="">
259
256
  <html.div
260
257
  ref={panelRef}
261
258
  role="dialog"
@@ -338,11 +335,7 @@ function Close({children, 'aria-label': ariaLabel = 'Close'}: CloseProps) {
338
335
  }
339
336
 
340
337
  return (
341
- <html.button
342
- onClick={requestClose}
343
- aria-label={ariaLabel}
344
- style={styles.closeButton}
345
- >
338
+ <html.button onClick={requestClose} aria-label={ariaLabel} style={styles.closeButton}>
346
339
  <svg width={16} height={16} viewBox="0 0 16 16" fill="none">
347
340
  <path
348
341
  d="M4 4l8 8M12 4l-8 8"
@@ -97,9 +97,7 @@ export function useSwipeDismiss({anchor, enabled, onDismiss, panelRef}: UseSwipe
97
97
 
98
98
  panelEl.style.transform = ''
99
99
  panelEl.style.boxShadow =
100
- shadowSpread > 0
101
- ? `0 0 ${shadowSpread * 2}px ${shadowSpread}px rgba(0, 0, 0, 0.15)`
102
- : ''
100
+ shadowSpread > 0 ? `0 0 ${shadowSpread * 2}px ${shadowSpread}px rgba(0, 0, 0, 0.15)` : ''
103
101
  }
104
102
  },
105
103
  [anchor, isHorizontal],
@@ -255,8 +253,7 @@ export function useSwipeDismiss({anchor, enabled, onDismiss, panelRef}: UseSwipe
255
253
  }
256
254
 
257
255
  const fraction = movement / state.panelSize
258
- const shouldDismiss =
259
- fraction > DISMISS_THRESHOLD || velocity > VELOCITY_THRESHOLD
256
+ const shouldDismiss = fraction > DISMISS_THRESHOLD || velocity > VELOCITY_THRESHOLD
260
257
 
261
258
  if (shouldDismiss) {
262
259
  animateDismiss(panel!)
@@ -291,5 +288,15 @@ export function useSwipeDismiss({anchor, enabled, onDismiss, panelRef}: UseSwipe
291
288
  panel.removeEventListener('pointercancel', handlePointerCancel)
292
289
  panel.removeEventListener('touchmove', handleTouchMove)
293
290
  }
294
- }, [enabled, panelRef, anchor, isHorizontal, getMovement, applyTransform, resetStyles, animateSnapBack, animateDismiss])
291
+ }, [
292
+ enabled,
293
+ panelRef,
294
+ anchor,
295
+ isHorizontal,
296
+ getMovement,
297
+ applyTransform,
298
+ resetStyles,
299
+ animateSnapBack,
300
+ animateDismiss,
301
+ ])
295
302
  }
@@ -0,0 +1,37 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Structural primitive for grouping content with header, body, and footer slots. A "workspace surface" for forms, settings sections, and inspectors. Sub-components are purely structural — no context required.',
6
+ whenToUse: [
7
+ 'Organizing content with distinct header/body/footer sections',
8
+ 'Settings panels, form containers, inspector views',
9
+ 'When you need more structure than Card but less than a full page layout',
10
+ ],
11
+ whenNotToUse: [
12
+ 'Repeating item cards — use Card',
13
+ 'Modal/overlay content — use Dialog or Drawer',
14
+ 'Page-level layout — use PageShell',
15
+ ],
16
+ anatomy: {
17
+ required: [],
18
+ optional: ['Root', 'Header', 'Body', 'Footer'],
19
+ },
20
+ relatedTo: [
21
+ {component: 'Card', relationship: 'Card is for shallow, repeated items; Panel is for deep, organized content'},
22
+ {component: 'DetailPanel', relationship: 'DetailPanel uses Panel internally for its structural layout'},
23
+ {component: 'PageShell', relationship: 'PageShell is for page-level layout; Panel is for section-level'},
24
+ ],
25
+ example: `<Panel.Root bordered>
26
+ <Panel.Header>
27
+ <Heading level={3}>Section Title</Heading>
28
+ <Button variant="secondary" size="small">Action</Button>
29
+ </Panel.Header>
30
+ <Panel.Body>
31
+ {/* Content */}
32
+ </Panel.Body>
33
+ <Panel.Footer>
34
+ <Button>Save</Button>
35
+ </Panel.Footer>
36
+ </Panel.Root>`,
37
+ }
@@ -0,0 +1,124 @@
1
+ import type {Meta, StoryObj} from '@storybook/react'
2
+ import {Panel} from './Panel'
3
+ import {Button} from '../Button/Button'
4
+ import {Text} from '../Text/Text'
5
+ import {Heading} from '../Heading/Heading'
6
+ import {Stack} from '../Stack/Stack'
7
+ import {Inline} from '../Inline/Inline'
8
+ import {Field} from '../Field/Field'
9
+ import {Input} from '../Input/Input'
10
+
11
+ const meta: Meta = {
12
+ title: 'Components/Panel',
13
+ }
14
+
15
+ export default meta
16
+ type Story = StoryObj
17
+
18
+ // --- Default ---
19
+
20
+ export const Default: Story = {
21
+ render: () => (
22
+ <Panel.Root>
23
+ <Panel.Header>
24
+ <Heading level={3}>Section Title</Heading>
25
+ </Panel.Header>
26
+ <Panel.Body>
27
+ <Text>
28
+ Panel is a structural primitive for grouping content with
29
+ header, body, and footer slots. It fills the gap between Card
30
+ and page-level layout.
31
+ </Text>
32
+ </Panel.Body>
33
+ <Panel.Footer>
34
+ <Button variant="secondary">Cancel</Button>
35
+ <Button>Save</Button>
36
+ </Panel.Footer>
37
+ </Panel.Root>
38
+ ),
39
+ }
40
+
41
+ // --- Bordered ---
42
+
43
+ export const Bordered: Story = {
44
+ render: () => (
45
+ <Panel.Root bordered>
46
+ <Panel.Header>
47
+ <Heading level={3}>Bordered Panel</Heading>
48
+ <Button variant="secondary" size="small">Action</Button>
49
+ </Panel.Header>
50
+ <Panel.Body>
51
+ <Text>
52
+ The bordered variant adds an outer border, useful when the panel
53
+ needs visual separation from its surroundings.
54
+ </Text>
55
+ </Panel.Body>
56
+ <Panel.Footer>
57
+ <Button>Confirm</Button>
58
+ </Panel.Footer>
59
+ </Panel.Root>
60
+ ),
61
+ }
62
+
63
+ // --- Body Only ---
64
+
65
+ export const BodyOnly: Story = {
66
+ render: () => (
67
+ <Panel.Root bordered>
68
+ <Panel.Body>
69
+ <Text>
70
+ Panel sub-components can be used independently. This panel only
71
+ has a Body — no Header or Footer.
72
+ </Text>
73
+ </Panel.Body>
74
+ </Panel.Root>
75
+ ),
76
+ }
77
+
78
+ // --- Unpadded Body ---
79
+
80
+ export const UnpaddedBody: Story = {
81
+ render: () => (
82
+ <Panel.Root bordered>
83
+ <Panel.Header>
84
+ <Heading level={3}>Full-bleed Content</Heading>
85
+ </Panel.Header>
86
+ <Panel.Body padded={false}>
87
+ <Text>
88
+ Setting padded=false removes body padding, useful for content
89
+ that needs to extend edge-to-edge (tables, images, etc).
90
+ </Text>
91
+ </Panel.Body>
92
+ </Panel.Root>
93
+ ),
94
+ }
95
+
96
+ // --- With Form ---
97
+
98
+ export const WithForm: Story = {
99
+ render: () => (
100
+ <Panel.Root bordered>
101
+ <Panel.Header>
102
+ <Heading level={3}>Profile Settings</Heading>
103
+ </Panel.Header>
104
+ <Panel.Body>
105
+ <Stack gap="md">
106
+ <Field.Root>
107
+ <Field.Label>Display name</Field.Label>
108
+ <Input placeholder="Enter your name" />
109
+ </Field.Root>
110
+ <Field.Root>
111
+ <Field.Label>Email</Field.Label>
112
+ <Input type="email" placeholder="you@example.com" />
113
+ </Field.Root>
114
+ </Stack>
115
+ </Panel.Body>
116
+ <Panel.Footer>
117
+ <Inline gap="sm">
118
+ <Button variant="secondary">Cancel</Button>
119
+ <Button>Save changes</Button>
120
+ </Inline>
121
+ </Panel.Footer>
122
+ </Panel.Root>
123
+ ),
124
+ }