@structuralists/scaffolding 0.0.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 (225) hide show
  1. package/.storybook/main.ts +9 -0
  2. package/.storybook/manager.ts +13 -0
  3. package/.storybook/preview.tsx +18 -0
  4. package/CLAUDE.md +30 -0
  5. package/LICENSE +21 -0
  6. package/README.md +7 -0
  7. package/bun.lock +947 -0
  8. package/bunfig.toml +2 -0
  9. package/eslint.config.mjs +106 -0
  10. package/index.ts +1 -0
  11. package/package.json +50 -0
  12. package/src/components/Chat/ChatComposer/ChatComposer.stories.tsx +68 -0
  13. package/src/components/Chat/ChatComposer/index.tsx +74 -0
  14. package/src/components/Chat/ChatComposer/styles.module.css +88 -0
  15. package/src/components/Chat/ChatComposer/types.ts +11 -0
  16. package/src/components/Chat/ChatMessage/ChatMessage.stories.tsx +111 -0
  17. package/src/components/Chat/ChatMessage/index.tsx +42 -0
  18. package/src/components/Chat/ChatMessage/styles.module.css +58 -0
  19. package/src/components/Chat/ChatMessage/types.ts +14 -0
  20. package/src/components/Chat/ChatRecipientsHeader/ChatRecipientsHeader.stories.tsx +145 -0
  21. package/src/components/Chat/ChatRecipientsHeader/index.tsx +29 -0
  22. package/src/components/Chat/ChatRecipientsHeader/styles.module.css +48 -0
  23. package/src/components/Chat/ChatRecipientsHeader/types.ts +26 -0
  24. package/src/components/Chat/ChatShell/ChatShell.stories.tsx +203 -0
  25. package/src/components/Chat/ChatShell/index.tsx +16 -0
  26. package/src/components/Chat/ChatShell/styles.module.css +27 -0
  27. package/src/components/Chat/ChatShell/types.ts +7 -0
  28. package/src/components/Chat/PillCombobox/PillCombobox.stories.tsx +59 -0
  29. package/src/components/Chat/PillCombobox/index.tsx +17 -0
  30. package/src/components/Chat/PillCombobox/styles.module.css +29 -0
  31. package/src/components/Chat/PillCombobox/types.ts +28 -0
  32. package/src/components/Chat/PillComboboxCore/Core.tsx +235 -0
  33. package/src/components/Chat/PillComboboxCore/styles.module.css +79 -0
  34. package/src/components/Chat/index.ts +12 -0
  35. package/src/components/Content/Badge/Badge.stories.tsx +31 -0
  36. package/src/components/Content/Badge/index.tsx +22 -0
  37. package/src/components/Content/Badge/styles.module.css +25 -0
  38. package/src/components/Content/Badge/types.ts +7 -0
  39. package/src/components/Content/Card/Card.stories.tsx +24 -0
  40. package/src/components/Content/Card/index.tsx +21 -0
  41. package/src/components/Content/Card/styles.module.css +13 -0
  42. package/src/components/Content/Card/types.ts +8 -0
  43. package/src/components/Content/EditableMarkdown/EditableMarkdown.stories.tsx +58 -0
  44. package/src/components/Content/EditableMarkdown/index.tsx +140 -0
  45. package/src/components/Content/EditableMarkdown/styles.module.css +221 -0
  46. package/src/components/Content/EditableMarkdown/types.ts +11 -0
  47. package/src/components/Content/Heading/Heading.stories.tsx +26 -0
  48. package/src/components/Content/Heading/index.tsx +20 -0
  49. package/src/components/Content/Heading/styles.module.css +19 -0
  50. package/src/components/Content/Heading/types.ts +8 -0
  51. package/src/components/Content/Link/Link.stories.tsx +21 -0
  52. package/src/components/Content/Link/index.tsx +19 -0
  53. package/src/components/Content/Link/styles.module.css +11 -0
  54. package/src/components/Content/Link/types.ts +8 -0
  55. package/src/components/Content/List/List.stories.tsx +62 -0
  56. package/src/components/Content/List/index.tsx +26 -0
  57. package/src/components/Content/List/styles.module.css +41 -0
  58. package/src/components/Content/List/types.ts +33 -0
  59. package/src/components/Content/LoadingContainer/LoadingContainer.stories.tsx +105 -0
  60. package/src/components/Content/LoadingContainer/index.tsx +36 -0
  61. package/src/components/Content/LoadingContainer/styles.module.css +54 -0
  62. package/src/components/Content/LoadingContainer/types.ts +8 -0
  63. package/src/components/Content/Markdown/Markdown.stories.tsx +39 -0
  64. package/src/components/Content/Markdown/index.tsx +28 -0
  65. package/src/components/Content/Markdown/styles.module.css +79 -0
  66. package/src/components/Content/Markdown/types.ts +8 -0
  67. package/src/components/Content/Menu/Menu.stories.tsx +186 -0
  68. package/src/components/Content/Menu/index.tsx +259 -0
  69. package/src/components/Content/Menu/styles.module.css +103 -0
  70. package/src/components/Content/Menu/types.ts +25 -0
  71. package/src/components/Content/Text/Text.stories.tsx +36 -0
  72. package/src/components/Content/Text/index.tsx +35 -0
  73. package/src/components/Content/Text/styles.module.css +30 -0
  74. package/src/components/Content/Text/types.ts +11 -0
  75. package/src/components/Forms/Button/Button.stories.tsx +40 -0
  76. package/src/components/Forms/Button/index.tsx +43 -0
  77. package/src/components/Forms/Button/styles.module.css +67 -0
  78. package/src/components/Forms/Button/types.ts +16 -0
  79. package/src/components/Forms/ColorInput/index.tsx +22 -0
  80. package/src/components/Forms/ColorInput/styles.module.css +19 -0
  81. package/src/components/Forms/ColorInput/types.ts +12 -0
  82. package/src/components/Forms/Field/Field.stories.tsx +35 -0
  83. package/src/components/Forms/Field/index.tsx +17 -0
  84. package/src/components/Forms/Field/styles.module.css +21 -0
  85. package/src/components/Forms/Field/types.ts +9 -0
  86. package/src/components/Forms/IconButton/IconButton.stories.tsx +91 -0
  87. package/src/components/Forms/IconButton/index.tsx +55 -0
  88. package/src/components/Forms/IconButton/styles.module.css +61 -0
  89. package/src/components/Forms/IconButton/types.ts +23 -0
  90. package/src/components/Forms/Input/Input.stories.tsx +22 -0
  91. package/src/components/Forms/Input/index.tsx +42 -0
  92. package/src/components/Forms/Input/styles.module.css +30 -0
  93. package/src/components/Forms/Input/types.ts +18 -0
  94. package/src/components/Forms/SearchInput/index.tsx +41 -0
  95. package/src/components/Forms/SearchInput/styles.module.css +30 -0
  96. package/src/components/Forms/SearchInput/types.ts +17 -0
  97. package/src/components/Forms/Select/MultiSelect/MultiSelect.stories.tsx +116 -0
  98. package/src/components/Forms/Select/MultiSelect/index.tsx +74 -0
  99. package/src/components/Forms/Select/MultiSelect/types.ts +15 -0
  100. package/src/components/Forms/Select/SingleSelect/SingleSelect.stories.tsx +174 -0
  101. package/src/components/Forms/Select/SingleSelect/index.tsx +62 -0
  102. package/src/components/Forms/Select/SingleSelect/types.ts +12 -0
  103. package/src/components/Forms/Select/index.ts +4 -0
  104. package/src/components/Forms/Select/internal/OptionList.tsx +124 -0
  105. package/src/components/Forms/Select/internal/SelectTrigger.tsx +60 -0
  106. package/src/components/Forms/Select/internal/styles.module.css +122 -0
  107. package/src/components/Forms/Textarea/Textarea.stories.tsx +25 -0
  108. package/src/components/Forms/Textarea/index.tsx +48 -0
  109. package/src/components/Forms/Textarea/styles.module.css +34 -0
  110. package/src/components/Forms/Textarea/types.ts +24 -0
  111. package/src/components/Json/Json/Json.stories.tsx +33 -0
  112. package/src/components/Json/Json/index.tsx +38 -0
  113. package/src/components/Json/Json/types.ts +21 -0
  114. package/src/components/Json/JsonTable/JsonLeafNode.tsx +31 -0
  115. package/src/components/Json/JsonTable/JsonTable.stories.tsx +52 -0
  116. package/src/components/Json/JsonTable/index.tsx +33 -0
  117. package/src/components/Json/JsonTable/types.ts +13 -0
  118. package/src/components/Json/JsonTable/utils.ts +6 -0
  119. package/src/components/Layout/Bar/Bar.stories.tsx +100 -0
  120. package/src/components/Layout/Bar/index.tsx +17 -0
  121. package/src/components/Layout/Bar/styles.module.css +34 -0
  122. package/src/components/Layout/Bar/types.ts +10 -0
  123. package/src/components/Layout/Debug/Debug.stories.tsx +86 -0
  124. package/src/components/Layout/Debug/index.tsx +41 -0
  125. package/src/components/Layout/Debug/styles.module.css +13 -0
  126. package/src/components/Layout/Debug/types.ts +12 -0
  127. package/src/components/Layout/Divider/Divider.stories.tsx +22 -0
  128. package/src/components/Layout/Divider/index.tsx +3 -0
  129. package/src/components/Layout/Divider/styles.module.css +6 -0
  130. package/src/components/Layout/Grid/Grid.stories.tsx +28 -0
  131. package/src/components/Layout/Grid/index.tsx +29 -0
  132. package/src/components/Layout/Grid/styles.module.css +12 -0
  133. package/src/components/Layout/Grid/types.ts +9 -0
  134. package/src/components/Layout/Panels/Panels.stories.tsx +287 -0
  135. package/src/components/Layout/Panels/index.tsx +119 -0
  136. package/src/components/Layout/Panels/styles.module.css +103 -0
  137. package/src/components/Layout/Panels/types.ts +36 -0
  138. package/src/components/Layout/Stack/Stack.stories.tsx +45 -0
  139. package/src/components/Layout/Stack/index.tsx +73 -0
  140. package/src/components/Layout/Stack/styles.module.css +41 -0
  141. package/src/components/Layout/Stack/types.ts +17 -0
  142. package/src/components/Modals/ConfirmModal/ConfirmModal.stories.tsx +73 -0
  143. package/src/components/Modals/ConfirmModal/index.tsx +72 -0
  144. package/src/components/Modals/ConfirmModal/styles.module.css +62 -0
  145. package/src/components/Modals/ConfirmModal/types.ts +14 -0
  146. package/src/components/Modals/LargeModal/LargeModal.stories.tsx +75 -0
  147. package/src/components/Modals/LargeModal/index.tsx +9 -0
  148. package/src/components/Modals/LargeModal/styles.module.css +6 -0
  149. package/src/components/Modals/LargeModal/types.ts +18 -0
  150. package/src/components/Modals/MediumModal/MediumModal.stories.tsx +121 -0
  151. package/src/components/Modals/MediumModal/MediumModal.test.tsx +48 -0
  152. package/src/components/Modals/MediumModal/index.tsx +9 -0
  153. package/src/components/Modals/MediumModal/styles.module.css +5 -0
  154. package/src/components/Modals/MediumModal/types.ts +18 -0
  155. package/src/components/Modals/index.ts +3 -0
  156. package/src/components/Modals/internal/ModalBody.tsx +21 -0
  157. package/src/components/Modals/internal/ModalFooter.tsx +12 -0
  158. package/src/components/Modals/internal/ModalHeader.tsx +27 -0
  159. package/src/components/Modals/internal/ModalShell.tsx +112 -0
  160. package/src/components/Modals/internal/styles.module.css +141 -0
  161. package/src/components/Navigation/TabBar/TabBar.stories.tsx +59 -0
  162. package/src/components/Navigation/TabBar/index.tsx +25 -0
  163. package/src/components/Navigation/TabBar/styles.module.css +32 -0
  164. package/src/components/Navigation/TabBar/types.ts +22 -0
  165. package/src/components/Navigation/VerticalNav/VerticalNav.stories.tsx +41 -0
  166. package/src/components/Navigation/VerticalNav/index.tsx +25 -0
  167. package/src/components/Navigation/VerticalNav/styles.module.css +28 -0
  168. package/src/components/Navigation/VerticalNav/types.ts +19 -0
  169. package/src/components/Overlays/Popover/Popover.stories.tsx +154 -0
  170. package/src/components/Overlays/Popover/index.tsx +175 -0
  171. package/src/components/Overlays/Popover/styles.module.css +59 -0
  172. package/src/components/Overlays/Popover/types.ts +34 -0
  173. package/src/components/Overlays/Tooltip/Tooltip.stories.tsx +41 -0
  174. package/src/components/Overlays/Tooltip/index.tsx +115 -0
  175. package/src/components/Overlays/Tooltip/styles.module.css +25 -0
  176. package/src/components/Overlays/Tooltip/types.ts +15 -0
  177. package/src/components/Primitives/EmptyValue/EmptyValue.stories.tsx +18 -0
  178. package/src/components/Primitives/EmptyValue/index.tsx +3 -0
  179. package/src/components/Primitives/EmptyValue/styles.module.css +3 -0
  180. package/src/components/Primitives/LinedStack/LinedStack.stories.tsx +101 -0
  181. package/src/components/Primitives/LinedStack/index.tsx +41 -0
  182. package/src/components/Primitives/LinedStack/styles.module.css +27 -0
  183. package/src/components/Primitives/LinedStack/types.ts +49 -0
  184. package/src/components/Primitives/LongText/LongText.stories.tsx +72 -0
  185. package/src/components/Primitives/LongText/index.tsx +67 -0
  186. package/src/components/Primitives/LongText/styles.module.css +30 -0
  187. package/src/components/Primitives/LongText/types.ts +4 -0
  188. package/src/components/Primitives/Num/Num.stories.tsx +51 -0
  189. package/src/components/Primitives/Num/index.tsx +37 -0
  190. package/src/components/Primitives/Num/types.ts +19 -0
  191. package/src/components/Primitives/Percent/Percent.stories.tsx +48 -0
  192. package/src/components/Primitives/Percent/index.tsx +15 -0
  193. package/src/components/Primitives/Percent/types.ts +10 -0
  194. package/src/components/Primitives/RelativeTime/RelativeTime.stories.tsx +57 -0
  195. package/src/components/Primitives/RelativeTime/index.tsx +31 -0
  196. package/src/components/Primitives/RelativeTime/types.ts +3 -0
  197. package/src/components/Tables/BigTable/BigTable.stories.tsx +367 -0
  198. package/src/components/Tables/BigTable/CLAUDE.md +118 -0
  199. package/src/components/Tables/BigTable/columnDefs.tsx +208 -0
  200. package/src/components/Tables/BigTable/index.tsx +104 -0
  201. package/src/components/Tables/BigTable/styles.module.css +83 -0
  202. package/src/components/Tables/BigTable/types.ts +20 -0
  203. package/src/components/Tables/QuickTable/CLAUDE.md +118 -0
  204. package/src/components/Tables/QuickTable/QuickTable.stories.tsx +121 -0
  205. package/src/components/Tables/QuickTable/index.tsx +86 -0
  206. package/src/components/Tables/QuickTable/internal.tsx +48 -0
  207. package/src/components/Tables/QuickTable/styles.module.css +65 -0
  208. package/src/components/Tables/QuickTable/types.ts +40 -0
  209. package/src/env.d.ts +4 -0
  210. package/src/index.ts +87 -0
  211. package/src/storybook/CLAUDE.md +35 -0
  212. package/src/storybook/Composition.stories.tsx +269 -0
  213. package/src/storybook/Lorem/index.tsx +54 -0
  214. package/src/storybook/Placeholder/index.tsx +27 -0
  215. package/src/storybook/Placeholder/styles.module.css +20 -0
  216. package/src/storybook/Repeat/index.tsx +23 -0
  217. package/src/storybook/Toggle/index.tsx +29 -0
  218. package/src/storybook/_StoryUtils.stories.tsx +58 -0
  219. package/src/storybook/index.ts +4 -0
  220. package/src/tokens.ts +31 -0
  221. package/src/utils.test.ts +24 -0
  222. package/src/utils.ts +2 -0
  223. package/test-setup.ts +3 -0
  224. package/tokens.css +323 -0
  225. package/tsconfig.json +16 -0
@@ -0,0 +1,287 @@
1
+ import { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { Panels } from './index';
4
+ import { Bar } from '../Bar';
5
+ import { Button } from '../../Forms/Button';
6
+ import { Text } from '../../Content/Text';
7
+ import { Heading } from '../../Content/Heading';
8
+ import { Stack } from '../Stack';
9
+ import { Lorem } from '../../../storybook';
10
+
11
+ const meta: Meta<typeof Panels> = {
12
+ title: 'Layout/Panels',
13
+ component: Panels,
14
+ parameters: { layout: 'fullscreen' },
15
+ decorators: [
16
+ (Story) => (
17
+ <div style={{ height: '100vh' }}>
18
+ <Story />
19
+ </div>
20
+ ),
21
+ ],
22
+ };
23
+
24
+ export default meta;
25
+ type Story = StoryObj<typeof Panels>;
26
+
27
+ const LeftSidebarContent = () => (
28
+ <Stack gap={2}>
29
+ {['Inbox', 'Today', 'Upcoming', 'Archive'].map((label) => (
30
+ <Text key={label}>{label}</Text>
31
+ ))}
32
+ </Stack>
33
+ );
34
+
35
+ const RightSidebarContent = () => (
36
+ <Stack gap={2}>
37
+ <Heading level={3}>Details</Heading>
38
+ <Text isMuted size="small">
39
+ Select something to inspect.
40
+ </Text>
41
+ </Stack>
42
+ );
43
+
44
+ const LongMain = () => (
45
+ <Stack gap={3}>
46
+ <Heading level={2}>Main content</Heading>
47
+ <Lorem paragraphs={40} as={Text} />
48
+ </Stack>
49
+ );
50
+
51
+ // Inline padding for main/sidebar slot content; Bar handles its own padding
52
+ // so header/footer slots pass a Bar directly instead of going through this.
53
+ const slot = (children: React.ReactNode, width?: number) => (
54
+ <div style={{ padding: 16, ...(width ? { width } : {}) }}>{children}</div>
55
+ );
56
+
57
+ export const Full: Story = {
58
+ render: () => (
59
+ <Panels
60
+ header={
61
+ <Bar
62
+ title="Workbench"
63
+ right={
64
+ <Text isMuted size="small">
65
+ v0.1
66
+ </Text>
67
+ }
68
+ />
69
+ }
70
+ footer={<Bar title="Ready." />}
71
+ leftSidebar={slot(<LeftSidebarContent />, 200)}
72
+ rightSidebar={slot(<RightSidebarContent />, 260)}
73
+ >
74
+ {slot(<LongMain />)}
75
+ </Panels>
76
+ ),
77
+ };
78
+
79
+ export const HeaderAndMain: Story = {
80
+ render: () => (
81
+ <Panels header={<Bar title="Workbench" />}>{slot(<LongMain />)}</Panels>
82
+ ),
83
+ };
84
+
85
+ export const LeftSidebarOnly: Story = {
86
+ render: () => (
87
+ <Panels leftSidebar={slot(<LeftSidebarContent />, 200)}>
88
+ {slot(<LongMain />)}
89
+ </Panels>
90
+ ),
91
+ };
92
+
93
+ export const MainOnly: Story = {
94
+ render: () => <Panels>{slot(<LongMain />)}</Panels>,
95
+ };
96
+
97
+ // rightOverlay slides in from the right, bounded by header/footer so the
98
+ // app nav and footer remain visible and interactive. Open state lives in
99
+ // the parent — passing `null` triggers the slide-out animation.
100
+ export const RightOverlay: Story = {
101
+ render: () => {
102
+ const RightOverlayDemo = () => {
103
+ const [selected, setSelected] = useState<string | null>(null);
104
+
105
+ return (
106
+ <Panels
107
+ header={
108
+ <Bar
109
+ title="Workbench"
110
+ right={
111
+ <Text isMuted size="small">
112
+ v0.1
113
+ </Text>
114
+ }
115
+ />
116
+ }
117
+ footer={<Bar title="Ready." />}
118
+ leftSidebar={slot(<LeftSidebarContent />, 200)}
119
+ rightOverlay={
120
+ selected
121
+ ? slot(
122
+ <Stack gap={3}>
123
+ <Heading level={3}>{selected}</Heading>
124
+ <Text isMuted size="small">
125
+ Detail for {selected}. Header and left nav remain
126
+ interactive while this is open.
127
+ </Text>
128
+ <Button onClick={() => setSelected(null)}>Close</Button>
129
+ </Stack>,
130
+ )
131
+ : null
132
+ }
133
+ >
134
+ {slot(
135
+ <Stack gap={3}>
136
+ <Heading level={2}>Items</Heading>
137
+ {['Alpha', 'Beta', 'Gamma'].map((name) => (
138
+ <Button
139
+ key={name}
140
+ onClick={() =>
141
+ setSelected((prev) => (prev === name ? null : name))
142
+ }
143
+ >
144
+ {selected === name ? `Close ${name}` : `Open ${name}`}
145
+ </Button>
146
+ ))}
147
+ <Lorem paragraphs={20} as={Text} />
148
+ </Stack>,
149
+ )}
150
+ </Panels>
151
+ );
152
+ };
153
+
154
+ return <RightOverlayDemo />;
155
+ },
156
+ };
157
+
158
+ // The overlay's content is itself a Panels — header/footer stay pinned
159
+ // while the body scrolls. The header carries the title and an X close
160
+ // button; the outer layout is intentionally minimal (no sidebars) to
161
+ // keep the focus on the overlay shape.
162
+ const CloseX = ({ onClick }: { onClick: () => void }) => (
163
+ <button
164
+ type="button"
165
+ aria-label="Close"
166
+ onClick={onClick}
167
+ style={{
168
+ display: 'inline-flex',
169
+ alignItems: 'center',
170
+ justifyContent: 'center',
171
+ width: 28,
172
+ height: 28,
173
+ padding: 0,
174
+ background: 'transparent',
175
+ border: 'none',
176
+ borderRadius: 4,
177
+ color: 'var(--ui-muted)',
178
+ cursor: 'pointer',
179
+ }}
180
+ >
181
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
182
+ <path
183
+ d="M4 4 L12 12 M12 4 L4 12"
184
+ stroke="currentColor"
185
+ strokeWidth="1.5"
186
+ strokeLinecap="round"
187
+ />
188
+ </svg>
189
+ </button>
190
+ );
191
+
192
+ export const RightOverlayWithNestedPanels: Story = {
193
+ render: () => {
194
+ const Demo = () => {
195
+ const [isOpen, setIsOpen] = useState(false);
196
+
197
+ return (
198
+ <Panels
199
+ header={<Bar title="Workbench" />}
200
+ rightOverlayWidth={420}
201
+ rightOverlay={
202
+ isOpen ? (
203
+ <Panels
204
+ header={
205
+ <Bar
206
+ title="Detail"
207
+ right={<CloseX onClick={() => setIsOpen(false)} />}
208
+ />
209
+ }
210
+ footer={
211
+ <Bar
212
+ right={
213
+ <Button variant="primary" onClick={() => setIsOpen(false)}>
214
+ Done
215
+ </Button>
216
+ }
217
+ />
218
+ }
219
+ >
220
+ {slot(
221
+ <Stack gap={3}>
222
+ <Heading level={3}>Long content</Heading>
223
+ <Text isMuted size="small">
224
+ Body scrolls; header and footer stay pinned.
225
+ </Text>
226
+ <Lorem paragraphs={30} as={Text} />
227
+ </Stack>,
228
+ )}
229
+ </Panels>
230
+ ) : null
231
+ }
232
+ >
233
+ {slot(
234
+ <Stack gap={3}>
235
+ <Heading level={2}>Main</Heading>
236
+ <Button onClick={() => setIsOpen((prev) => !prev)}>
237
+ {isOpen ? 'Close detail' : 'Open detail'}
238
+ </Button>
239
+ </Stack>,
240
+ )}
241
+ </Panels>
242
+ );
243
+ };
244
+
245
+ return <Demo />;
246
+ },
247
+ };
248
+
249
+ // The "fixed strip above a nested Panels" recipe. The outer Panels' main
250
+ // area hosts a flex column: an intro strip on top (intrinsic height) and
251
+ // a flex-1 area that contains another Panels. The column's `min-height: 0`
252
+ // is what lets the inner Panels resolve its own `height: 100%` without
253
+ // spilling past the outer scroll region.
254
+ //
255
+ // Copy this layout when a detail view needs a non-scrolling header above
256
+ // a list/detail pair that scrolls independently.
257
+ export const NestedFullHeight: Story = {
258
+ render: () => (
259
+ <Panels header={<Bar title="Outer" />}>
260
+ <div
261
+ style={{
262
+ display: 'flex',
263
+ flexDirection: 'column',
264
+ height: '100%',
265
+ minHeight: 0,
266
+ }}
267
+ >
268
+ <div style={{ flex: '0 0 auto', padding: 16 }}>
269
+ <Heading level={3}>Intro strip</Heading>
270
+ <Text size="small" isMuted>
271
+ Fixed height above an inner Panels. Does not scroll.
272
+ </Text>
273
+ </div>
274
+ <div style={{ flex: '1 1 0', minHeight: 0, display: 'flex' }}>
275
+ <div style={{ width: '100%' }}>
276
+ <Panels
277
+ header={<Bar title="Inner" />}
278
+ leftSidebar={slot(<LeftSidebarContent />, 180)}
279
+ >
280
+ {slot(<LongMain />)}
281
+ </Panels>
282
+ </div>
283
+ </div>
284
+ </div>
285
+ </Panels>
286
+ ),
287
+ };
@@ -0,0 +1,119 @@
1
+ import { useEffect, useState, type CSSProperties, type ReactNode } from 'react';
2
+ import { backgroundStyle } from '../../../tokens';
3
+ import { cx } from '../../../utils';
4
+ import type { PanelsProps } from './types';
5
+ import styles from './styles.module.css';
6
+
7
+ // Matches the keyframe duration in styles.module.css. Kept in JS so the
8
+ // overlay stays mounted long enough for the slide-out animation to play.
9
+ const OVERLAY_CLOSE_ANIMATION_MS = 200;
10
+
11
+ export const Panels = (props: PanelsProps) => {
12
+ const {
13
+ header,
14
+ subHeaders,
15
+ footer,
16
+ leftSidebar,
17
+ rightSidebar,
18
+ rightOverlay,
19
+ leftSidebarWidth,
20
+ rightSidebarWidth,
21
+ rightOverlayWidth,
22
+ headerBackground,
23
+ footerBackground,
24
+ leftSidebarBackground,
25
+ rightSidebarBackground,
26
+ mainContentBackground,
27
+ children,
28
+ } = props;
29
+
30
+ const leftSidebarStyle: CSSProperties | undefined =
31
+ leftSidebarWidth != null || leftSidebarBackground != null
32
+ ? {
33
+ ...(leftSidebarWidth != null ? { width: leftSidebarWidth } : null),
34
+ ...(backgroundStyle('--ui-panels-left-bg', leftSidebarBackground) ?? null),
35
+ }
36
+ : undefined;
37
+
38
+ const rightSidebarStyle: CSSProperties | undefined =
39
+ rightSidebarWidth != null || rightSidebarBackground != null
40
+ ? {
41
+ ...(rightSidebarWidth != null ? { width: rightSidebarWidth } : null),
42
+ ...(backgroundStyle('--ui-panels-right-bg', rightSidebarBackground) ?? null),
43
+ }
44
+ : undefined;
45
+
46
+ const [renderedOverlay, setRenderedOverlay] = useState<ReactNode>(rightOverlay ?? null);
47
+ const [isOverlayClosing, setIsOverlayClosing] = useState(false);
48
+
49
+ useEffect(() => {
50
+ if (rightOverlay != null) {
51
+ setRenderedOverlay(rightOverlay);
52
+ setIsOverlayClosing(false);
53
+ return;
54
+ }
55
+ setIsOverlayClosing(true);
56
+ const timer = setTimeout(() => {
57
+ setRenderedOverlay(null);
58
+ setIsOverlayClosing(false);
59
+ }, OVERLAY_CLOSE_ANIMATION_MS);
60
+ return () => clearTimeout(timer);
61
+ }, [rightOverlay]);
62
+
63
+ const rightOverlayStyle: CSSProperties | undefined =
64
+ rightOverlayWidth != null ? { width: rightOverlayWidth } : undefined;
65
+
66
+ return (
67
+ <div className={styles.panels}>
68
+ {header && (
69
+ <div
70
+ className={styles.header}
71
+ style={backgroundStyle('--ui-panels-header-bg', headerBackground)}
72
+ >
73
+ {header}
74
+ </div>
75
+ )}
76
+ {subHeaders?.map((child, i) => (
77
+ <div key={i} className={styles.subHeader}>
78
+ {child}
79
+ </div>
80
+ ))}
81
+ <div className={styles.middle}>
82
+ {leftSidebar && (
83
+ <div className={styles.leftSidebar} style={leftSidebarStyle}>
84
+ {leftSidebar}
85
+ </div>
86
+ )}
87
+ <div
88
+ className={styles.main}
89
+ style={backgroundStyle('--ui-panels-main-bg', mainContentBackground)}
90
+ >
91
+ {children}
92
+ </div>
93
+ {rightSidebar && (
94
+ <div className={styles.rightSidebar} style={rightSidebarStyle}>
95
+ {rightSidebar}
96
+ </div>
97
+ )}
98
+ {renderedOverlay != null && (
99
+ <div
100
+ className={cx(styles.rightOverlay, isOverlayClosing && styles.closing)}
101
+ style={rightOverlayStyle}
102
+ >
103
+ {renderedOverlay}
104
+ </div>
105
+ )}
106
+ </div>
107
+ {footer && (
108
+ <div
109
+ className={styles.footer}
110
+ style={backgroundStyle('--ui-panels-footer-bg', footerBackground)}
111
+ >
112
+ {footer}
113
+ </div>
114
+ )}
115
+ </div>
116
+ );
117
+ };
118
+
119
+ export type { PanelsProps };
@@ -0,0 +1,103 @@
1
+ .panels {
2
+ display: flex;
3
+ flex-direction: column;
4
+ height: 100%;
5
+ width: 100%;
6
+ }
7
+
8
+ .header {
9
+ flex-shrink: 0;
10
+ background-color: var(--ui-panels-header-bg, var(--ui-background-2));
11
+ border-bottom: 1px solid var(--ui-border);
12
+ }
13
+
14
+ .subHeader {
15
+ flex-shrink: 0;
16
+ background-color: var(--ui-background-1);
17
+ border-bottom: 1px solid var(--ui-border);
18
+ }
19
+
20
+ .footer {
21
+ flex-shrink: 0;
22
+ background-color: var(--ui-panels-footer-bg, var(--ui-background-2));
23
+ border-top: 1px solid var(--ui-border);
24
+ }
25
+
26
+ .middle {
27
+ display: flex;
28
+ flex-direction: row;
29
+ flex: 1 1 0;
30
+ /* min-height: 0 lets flex children establish their own scroll contexts
31
+ instead of stretching the row to their intrinsic content height. */
32
+ min-height: 0;
33
+ /* Anchor for the absolutely-positioned rightOverlay. */
34
+ position: relative;
35
+ /* Clip the rightOverlay during its slide animation so its off-screen
36
+ start/end position can't induce a scrollbar on the page. Inner regions
37
+ (main, sidebars) manage their own overflow. */
38
+ overflow: hidden;
39
+ }
40
+
41
+ .leftSidebar {
42
+ flex-shrink: 0;
43
+ overflow: auto;
44
+ background-color: var(--ui-panels-left-bg, var(--ui-background-1));
45
+ border-right: 1px solid var(--ui-border);
46
+ }
47
+
48
+ .rightSidebar {
49
+ flex-shrink: 0;
50
+ overflow: auto;
51
+ background-color: var(--ui-panels-right-bg, var(--ui-background-1));
52
+ border-left: 1px solid var(--ui-border);
53
+ }
54
+
55
+ .main {
56
+ flex: 1 1 0;
57
+ overflow: auto;
58
+ background-color: var(--ui-panels-main-bg, var(--ui-background-0));
59
+ /* min-width: 0 is the row-direction counterpart — without it, overflowing
60
+ main content pushes the layout wider than the viewport. */
61
+ min-width: 0;
62
+ /* Contain z-indexes from descendants (e.g. sticky table headers) so they
63
+ can't paint above the rightOverlay or other chrome that lives in the
64
+ parent stacking context. */
65
+ isolation: isolate;
66
+ }
67
+
68
+ .rightOverlay {
69
+ position: absolute;
70
+ top: 0;
71
+ bottom: 0;
72
+ right: 0;
73
+ width: 360px;
74
+ display: flex;
75
+ flex-direction: column;
76
+ overflow: auto;
77
+ background-color: var(--ui-background-0);
78
+ border-left: 1px solid var(--ui-border);
79
+ box-shadow: -4px 0 20px var(--ui-shadow);
80
+ z-index: 1;
81
+ animation: panels-overlay-in 200ms ease-out;
82
+ }
83
+
84
+ .rightOverlay.closing {
85
+ animation: panels-overlay-out 200ms ease-in forwards;
86
+ }
87
+
88
+ @keyframes panels-overlay-in {
89
+ from { transform: translateX(100%); }
90
+ to { transform: translateX(0); }
91
+ }
92
+
93
+ @keyframes panels-overlay-out {
94
+ from { transform: translateX(0); }
95
+ to { transform: translateX(100%); }
96
+ }
97
+
98
+ @media (prefers-reduced-motion: reduce) {
99
+ .rightOverlay,
100
+ .rightOverlay.closing {
101
+ animation: none;
102
+ }
103
+ }
@@ -0,0 +1,36 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { BackgroundToken } from '../../../tokens';
3
+
4
+ export type PanelsProps = {
5
+ header?: ReactNode;
6
+ /** Optional secondary header strips rendered directly below `header`.
7
+ * Each entry is wrapped in a div with the same divider style so they
8
+ * stack as distinct rows (recipients editor, breadcrumbs, filter bar,
9
+ * etc.). Each entry must carry a `key` prop — `react/jsx-key` will
10
+ * flag any array element without one. */
11
+ subHeaders?: ReactNode[];
12
+ footer?: ReactNode;
13
+ leftSidebar?: ReactNode;
14
+ rightSidebar?: ReactNode;
15
+ /** Transient overlay anchored to the right edge of the middle region,
16
+ * bounded vertically by header/footer so app nav stays visible. Open
17
+ * when truthy, animates out when set back to `null`/`undefined`.
18
+ * Parent owns the open state and renders its own close affordance —
19
+ * there is no backdrop scrim or built-in dismissal. */
20
+ rightOverlay?: ReactNode;
21
+ /** Fixed width for the left sidebar. Numbers → px, strings pass through
22
+ * (`"22rem"`, `"25%"`, etc). Defaults to content-sized. */
23
+ leftSidebarWidth?: number | string;
24
+ /** Fixed width for the right sidebar. Same shape as leftSidebarWidth. */
25
+ rightSidebarWidth?: number | string;
26
+ /** Width of the right overlay. Same shape as the sidebar widths.
27
+ * Defaults to 360px. */
28
+ rightOverlayWidth?: number | string;
29
+ /** Per-region background ramp. Each defaults to the region's stock tone. */
30
+ headerBackground?: BackgroundToken;
31
+ footerBackground?: BackgroundToken;
32
+ leftSidebarBackground?: BackgroundToken;
33
+ rightSidebarBackground?: BackgroundToken;
34
+ mainContentBackground?: BackgroundToken;
35
+ children?: ReactNode;
36
+ };
@@ -0,0 +1,45 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Stack } from './index';
3
+ import { Button } from '../../Forms/Button';
4
+
5
+ const meta: Meta<typeof Stack> = {
6
+ title: 'Layout/Stack',
7
+ component: Stack,
8
+ argTypes: {
9
+ direction: { control: 'inline-radio', options: ['row', 'column'] },
10
+ gap: { control: { type: 'range', min: 0, max: 6, step: 1 } },
11
+ align: { control: 'inline-radio', options: ['start', 'center', 'end', 'stretch'] },
12
+ justify: { control: 'inline-radio', options: ['start', 'center', 'end', 'between'] },
13
+ },
14
+ };
15
+
16
+ export default meta;
17
+ type Story = StoryObj<typeof Stack>;
18
+
19
+ const demoChildren = (
20
+ <>
21
+ <Button>One</Button>
22
+ <Button>Two</Button>
23
+ <Button>Three</Button>
24
+ </>
25
+ );
26
+
27
+ export const Column: Story = {
28
+ args: { direction: 'column', gap: 3 },
29
+ render: (args) => <Stack {...args}>{demoChildren}</Stack>,
30
+ };
31
+
32
+ export const Row: Story = {
33
+ args: { direction: 'row', gap: 2 },
34
+ render: (args) => <Stack {...args}>{demoChildren}</Stack>,
35
+ };
36
+
37
+ export const RowSpaceBetween: Story = {
38
+ args: { direction: 'row', justify: 'between' },
39
+ render: (args) => (
40
+ <Stack {...args}>
41
+ <Button>Left</Button>
42
+ <Button>Right</Button>
43
+ </Stack>
44
+ ),
45
+ };
@@ -0,0 +1,73 @@
1
+ import type {
2
+ StackAlign,
3
+ StackGap,
4
+ StackJustify,
5
+ StackPadding,
6
+ StackProps,
7
+ } from './types';
8
+ import { cx } from '../../../utils';
9
+ import styles from './styles.module.css';
10
+
11
+ const GAP_MAP: Record<StackGap, string> = {
12
+ 0: styles.gap0,
13
+ 1: styles.gap1,
14
+ 2: styles.gap2,
15
+ 3: styles.gap3,
16
+ 4: styles.gap4,
17
+ 5: styles.gap5,
18
+ 6: styles.gap6,
19
+ };
20
+
21
+ const PADDING_MAP: Record<StackPadding, string> = {
22
+ 0: styles.padding0,
23
+ 1: styles.padding1,
24
+ 2: styles.padding2,
25
+ 3: styles.padding3,
26
+ 4: styles.padding4,
27
+ 5: styles.padding5,
28
+ 6: styles.padding6,
29
+ };
30
+
31
+ const ALIGN_MAP: Record<StackAlign, string> = {
32
+ start: styles.alignStart,
33
+ center: styles.alignCenter,
34
+ end: styles.alignEnd,
35
+ stretch: styles.alignStretch,
36
+ };
37
+
38
+ const JUSTIFY_MAP: Record<StackJustify, string> = {
39
+ start: styles.justifyStart,
40
+ center: styles.justifyCenter,
41
+ end: styles.justifyEnd,
42
+ between: styles.justifyBetween,
43
+ };
44
+
45
+ export const Stack = (props: StackProps) => {
46
+ const {
47
+ children,
48
+ direction = 'column',
49
+ gap = 2,
50
+ padding,
51
+ align,
52
+ justify,
53
+ isWrap,
54
+ } = props;
55
+
56
+ return (
57
+ <div
58
+ className={cx(
59
+ styles.stack,
60
+ direction === 'row' ? styles.row : styles.column,
61
+ GAP_MAP[gap],
62
+ padding !== undefined && PADDING_MAP[padding],
63
+ align && ALIGN_MAP[align],
64
+ justify && JUSTIFY_MAP[justify],
65
+ isWrap && styles.wrap,
66
+ )}
67
+ >
68
+ {children}
69
+ </div>
70
+ );
71
+ };
72
+
73
+ export type { StackProps };
@@ -0,0 +1,41 @@
1
+ .stack {
2
+ display: flex;
3
+ }
4
+
5
+ .row {
6
+ flex-direction: row;
7
+ }
8
+
9
+ .column {
10
+ flex-direction: column;
11
+ }
12
+
13
+ .wrap {
14
+ flex-wrap: wrap;
15
+ }
16
+
17
+ .gap0 { gap: 0; }
18
+ .gap1 { gap: var(--ui-space-1); }
19
+ .gap2 { gap: var(--ui-space-2); }
20
+ .gap3 { gap: var(--ui-space-3); }
21
+ .gap4 { gap: var(--ui-space-4); }
22
+ .gap5 { gap: var(--ui-space-5); }
23
+ .gap6 { gap: var(--ui-space-6); }
24
+
25
+ .padding0 { padding: 0; }
26
+ .padding1 { padding: var(--ui-space-1); }
27
+ .padding2 { padding: var(--ui-space-2); }
28
+ .padding3 { padding: var(--ui-space-3); }
29
+ .padding4 { padding: var(--ui-space-4); }
30
+ .padding5 { padding: var(--ui-space-5); }
31
+ .padding6 { padding: var(--ui-space-6); }
32
+
33
+ .alignStart { align-items: flex-start; }
34
+ .alignCenter { align-items: center; }
35
+ .alignEnd { align-items: flex-end; }
36
+ .alignStretch { align-items: stretch; }
37
+
38
+ .justifyStart { justify-content: flex-start; }
39
+ .justifyCenter { justify-content: center; }
40
+ .justifyEnd { justify-content: flex-end; }
41
+ .justifyBetween { justify-content: space-between; }