@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,145 @@
1
+ import { useState, type ComponentType } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { ChatRecipientsHeader } from './index';
4
+ import type { PillComboboxOption } from '../PillCombobox/types';
5
+ import { Stack } from '../../Layout/Stack';
6
+ import { Textarea } from '../../Forms/Textarea';
7
+ import { Input } from '../../Forms/Input';
8
+ import { Field } from '../../Forms/Field';
9
+ import { Button } from '../../Forms/Button';
10
+ import { MediumModal } from '../../Modals';
11
+ import { Toggle } from '../../../storybook';
12
+
13
+ const meta: Meta<typeof ChatRecipientsHeader> = {
14
+ title: 'Chat/ChatRecipientsHeader',
15
+ component: ChatRecipientsHeader,
16
+ };
17
+
18
+ export default meta;
19
+ type Story = StoryObj<typeof ChatRecipientsHeader>;
20
+
21
+ const actorOptions: PillComboboxOption<string>[] = [
22
+ { value: 'alice', label: 'Alice' },
23
+ { value: 'bob', label: 'Bob' },
24
+ { value: 'carol', label: 'Carol' },
25
+ { value: 'dave', label: 'Dave' },
26
+ { value: 'eve', label: 'Eve' },
27
+ { value: 'mallory', label: 'Mallory' },
28
+ ];
29
+
30
+ const Harness = ({
31
+ initial = [],
32
+ isDisabled,
33
+ label,
34
+ }: {
35
+ initial?: string[];
36
+ isDisabled?: boolean;
37
+ label?: string;
38
+ }) => {
39
+ const [values, setValues] = useState<string[]>(initial);
40
+ return (
41
+ <ChatRecipientsHeader
42
+ options={actorOptions}
43
+ values={values}
44
+ onChange={setValues}
45
+ ariaLabel="Recipients"
46
+ placeholder="Add people…"
47
+ isDisabled={isDisabled}
48
+ label={label}
49
+ />
50
+ );
51
+ };
52
+
53
+ const widthDecorator = (Story: ComponentType) => (
54
+ <div style={{ width: 560 }}>
55
+ <Story />
56
+ </div>
57
+ );
58
+
59
+ // Plain — flush header in a fixed-width container so the bottom-rule and
60
+ // inset focus highlight read clearly.
61
+ export const Default: Story = {
62
+ decorators: [widthDecorator],
63
+ render: () => <Harness />,
64
+ };
65
+
66
+ export const WithSelections: Story = {
67
+ decorators: [widthDecorator],
68
+ render: () => <Harness initial={['alice', 'bob']} />,
69
+ };
70
+
71
+ export const CustomLabel: Story = {
72
+ decorators: [widthDecorator],
73
+ render: () => <Harness label="Cc" initial={['carol']} />,
74
+ };
75
+
76
+ export const Disabled: Story = {
77
+ decorators: [widthDecorator],
78
+ render: () => <Harness initial={['alice']} isDisabled />,
79
+ };
80
+
81
+ // Canonical use: dropped into the modal's `subHeaders` slot, sitting flush
82
+ // under the title bar with the slot's divider separating it from the body.
83
+ // The body keeps its default padding for the rest of the form.
84
+ export const InComposeModal: Story = {
85
+ parameters: { layout: 'centered' },
86
+ render: () => (
87
+ <Toggle defaultOn>
88
+ {({ isOn, on, off }) => {
89
+ const [recipients, setRecipients] = useState<string[]>(['alice']);
90
+ const [title, setTitle] = useState('');
91
+ const [body, setBody] = useState('');
92
+ return (
93
+ <>
94
+ <Button onClick={on}>Open compose…</Button>
95
+ <MediumModal
96
+ isOpen={isOn}
97
+ onClose={off}
98
+ title="New conversation"
99
+ subHeaders={[
100
+ <ChatRecipientsHeader
101
+ key="recipients"
102
+ options={actorOptions}
103
+ values={recipients}
104
+ onChange={setRecipients}
105
+ ariaLabel="Recipients"
106
+ placeholder="Add people…"
107
+ />,
108
+ ]}
109
+ footer={
110
+ <Stack direction="row" justify="end" gap={2}>
111
+ <Button variant="ghost" onClick={off}>
112
+ Cancel
113
+ </Button>
114
+ <Button variant="primary" onClick={off}>
115
+ Send
116
+ </Button>
117
+ </Stack>
118
+ }
119
+ >
120
+ <Stack gap={3}>
121
+ <Field label="Title" htmlFor="story-title">
122
+ <Input
123
+ id="story-title"
124
+ value={title}
125
+ onChange={(e) => setTitle(e.target.value)}
126
+ placeholder="What's this about?"
127
+ />
128
+ </Field>
129
+ <Field label="Message" htmlFor="story-body">
130
+ <Textarea
131
+ id="story-body"
132
+ value={body}
133
+ onChange={(e) => setBody(e.target.value)}
134
+ placeholder="Type your first message…"
135
+ rows={4}
136
+ />
137
+ </Field>
138
+ </Stack>
139
+ </MediumModal>
140
+ </>
141
+ );
142
+ }}
143
+ </Toggle>
144
+ ),
145
+ };
@@ -0,0 +1,29 @@
1
+ import { cx } from '../../../utils';
2
+ import { PillComboboxCore } from '../PillComboboxCore/Core';
3
+ import type {
4
+ ChatRecipientsHeaderProps,
5
+ PillComboboxOption,
6
+ } from './types';
7
+ import styles from './styles.module.css';
8
+
9
+ // Flush header packaging of the chat combobox. Same engine as
10
+ // PillCombobox, but renders without a bordered shell — meant to sit edge-
11
+ // to-edge at the top of a compose modal or panel, with an inline "To"
12
+ // label.
13
+ export const ChatRecipientsHeader = <T extends string,>({
14
+ label = 'To',
15
+ ...rest
16
+ }: ChatRecipientsHeaderProps<T>) => (
17
+ <PillComboboxCore
18
+ {...rest}
19
+ shellClassName={cx(styles.shell, rest.isDisabled && styles.shellDisabled)}
20
+ rowClassName={cx(styles.row, rest.isDisabled && styles.rowDisabled)}
21
+ leadingSlot={
22
+ <span className={styles.label} aria-hidden="true">
23
+ {label}
24
+ </span>
25
+ }
26
+ />
27
+ );
28
+
29
+ export type { ChatRecipientsHeaderProps, PillComboboxOption };
@@ -0,0 +1,48 @@
1
+ /* Outer carries the resting "header zone" tint and lays out the leading
2
+ label beside the focusable row. The container's subHeader slot is
3
+ responsible for the divider above/below. */
4
+ .shell {
5
+ display: flex;
6
+ align-items: center;
7
+ gap: var(--ui-space-1);
8
+ padding: var(--ui-space-1);
9
+ padding-left: calc(var(--ui-space-5) + var(--ui-space-1));
10
+ background-color: var(--ui-background-1);
11
+ }
12
+
13
+ .shellDisabled {
14
+ opacity: 0.5;
15
+ }
16
+
17
+ .label {
18
+ font-size: var(--ui-text-medium);
19
+ line-height: var(--ui-line-height);
20
+ color: var(--ui-muted);
21
+ user-select: none;
22
+ }
23
+
24
+ /* Inner: the focusable row. Transparent at rest (inherits the outer's
25
+ tint); on focus, lifts to the content surface and shows an accent ring
26
+ around the entire row. */
27
+ .row {
28
+ flex: 1 1 0;
29
+ display: flex;
30
+ flex-wrap: wrap;
31
+ align-items: center;
32
+ gap: var(--ui-space-1);
33
+ padding: 2px var(--ui-space-2);
34
+ border-radius: var(--ui-radius);
35
+ cursor: text;
36
+ min-height: calc(var(--ui-text-medium) * var(--ui-line-height) + var(--ui-space-1) * 2);
37
+ transition: background-color 80ms ease;
38
+ }
39
+
40
+ .shell:focus-within .row {
41
+ background-color: var(--ui-background-0);
42
+ outline: 2px solid var(--ui-accent);
43
+ outline-offset: -1px;
44
+ }
45
+
46
+ .rowDisabled {
47
+ cursor: not-allowed;
48
+ }
@@ -0,0 +1,26 @@
1
+ import type { PillComboboxOption } from '../PillCombobox/types';
2
+
3
+ /**
4
+ * Header-shaped recipients editor: flush with its container, leading inline
5
+ * label ("To" by default), bottom rule. Designed to sit at the top of a
6
+ * compose modal or panel — not inside a Field. Shares the underlying
7
+ * combobox with PillCombobox; differs only in chrome.
8
+ *
9
+ * The flush look assumes the parent suppresses its own gutter padding —
10
+ * inside a Modal, that means `disableContentPadding` on the body. The
11
+ * header then bleeds edge-to-edge under the modal title.
12
+ */
13
+ export type ChatRecipientsHeaderProps<T extends string> = {
14
+ options: PillComboboxOption<T>[];
15
+ values: T[];
16
+ onChange: (next: T[]) => void;
17
+ /** Accessible name for the underlying combobox input. */
18
+ ariaLabel: string;
19
+ /** Inline label rendered to the left of the pills. Defaults to "To". */
20
+ label?: string;
21
+ placeholder?: string;
22
+ isDisabled?: boolean;
23
+ emptyHint?: string;
24
+ };
25
+
26
+ export type { PillComboboxOption };
@@ -0,0 +1,203 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { useState } from 'react';
3
+ import { ChatShell } from './index';
4
+ import { ChatMessage } from '../ChatMessage';
5
+ import { ChatComposer } from '../ChatComposer';
6
+ import { Bar } from '../../Layout/Bar';
7
+ import { Heading } from '../../Content/Heading';
8
+ import { Text } from '../../Content/Text';
9
+ import { Badge } from '../../Content/Badge';
10
+ import { Panels } from '../../Layout/Panels';
11
+ import { Stack } from '../../Layout/Stack';
12
+
13
+ const meta: Meta<typeof ChatShell> = {
14
+ title: 'Chat/ChatShell',
15
+ component: ChatShell,
16
+ parameters: { layout: 'fullscreen' },
17
+ decorators: [
18
+ (Story) => (
19
+ <div style={{ height: '100vh' }}>
20
+ <Story />
21
+ </div>
22
+ ),
23
+ ],
24
+ };
25
+
26
+ export default meta;
27
+ type Story = StoryObj<typeof ChatShell>;
28
+
29
+ type Msg = { id: string; role: 'user' | 'assistant' | 'system'; md: string; timestamp: Date };
30
+
31
+ const seedConversation = (): Msg[] => [
32
+ {
33
+ id: '0',
34
+ role: 'system',
35
+ md: 'Conversation started. Model: claude-opus-4-7.',
36
+ timestamp: new Date(2026, 3, 23, 15, 39),
37
+ },
38
+ {
39
+ id: '1',
40
+ role: 'user',
41
+ md: 'What does `BackgroundToken` accept?',
42
+ timestamp: new Date(2026, 3, 23, 15, 40),
43
+ },
44
+ {
45
+ id: '2',
46
+ role: 'assistant',
47
+ md: `Four values: \`"0" | "1" | "2" | "3"\`. They map to \`--ui-background-0\` through \`--ui-background-3\`. Components consume them via a \`backgroundStyle(varName, token)\` helper that sets a component-scoped CSS custom property so nested overrides don't bleed through the cascade.`,
48
+ timestamp: new Date(2026, 3, 23, 15, 40),
49
+ },
50
+ {
51
+ id: '3',
52
+ role: 'user',
53
+ md: 'And which primitives expose it?',
54
+ timestamp: new Date(2026, 3, 23, 15, 41),
55
+ },
56
+ {
57
+ id: '4',
58
+ role: 'assistant',
59
+ md: `\`Bar\`, \`List\`, and \`Panels\` (per-region, via \`headerBackground\` / \`footerBackground\` / \`leftSidebarBackground\` / \`rightSidebarBackground\` / \`mainContentBackground\`).`,
60
+ timestamp: new Date(2026, 3, 23, 15, 41),
61
+ },
62
+ ];
63
+
64
+ const useChatState = () => {
65
+ const [messages, setMessages] = useState<Msg[]>(seedConversation());
66
+
67
+ const append = (text: string) => {
68
+ setMessages((prev) => [
69
+ ...prev,
70
+ { id: String(prev.length), role: 'user', md: text, timestamp: new Date() },
71
+ {
72
+ id: String(prev.length + 1),
73
+ role: 'assistant',
74
+ md: `_(echo)_ ${text}`,
75
+ timestamp: new Date(),
76
+ },
77
+ ]);
78
+ };
79
+
80
+ return { messages, append };
81
+ };
82
+
83
+ export const Default: Story = {
84
+ render: () => {
85
+ const { messages, append } = useChatState();
86
+
87
+ return (
88
+ <ChatShell
89
+ header={<Bar title="Conversation" />}
90
+ composer={<ChatComposer onSubmit={append} placeholder="Reply…" />}
91
+ >
92
+ {messages.map((m) => (
93
+ <ChatMessage key={m.id} role={m.role} md={m.md} timestamp={m.timestamp} />
94
+ ))}
95
+ </ChatShell>
96
+ );
97
+ },
98
+ };
99
+
100
+ export const NoHeader: Story = {
101
+ render: () => {
102
+ const { messages, append } = useChatState();
103
+
104
+ return (
105
+ <ChatShell composer={<ChatComposer onSubmit={append} placeholder="Reply…" />}>
106
+ {messages.map((m) => (
107
+ <ChatMessage key={m.id} role={m.role} md={m.md} timestamp={m.timestamp} />
108
+ ))}
109
+ </ChatShell>
110
+ );
111
+ },
112
+ };
113
+
114
+ export const RichHeader: Story = {
115
+ render: () => {
116
+ const { messages, append } = useChatState();
117
+
118
+ return (
119
+ <ChatShell
120
+ header={
121
+ <Bar
122
+ title="Planning session"
123
+ right={
124
+ <Stack direction="row" gap={2} align="center">
125
+ <Badge isMono>claude-opus-4-7</Badge>
126
+ <Text isMuted size="small">
127
+ 1,842 tokens
128
+ </Text>
129
+ </Stack>
130
+ }
131
+ />
132
+ }
133
+ composer={<ChatComposer onSubmit={append} placeholder="Reply…" />}
134
+ >
135
+ {messages.map((m) => (
136
+ <ChatMessage key={m.id} role={m.role} md={m.md} timestamp={m.timestamp} />
137
+ ))}
138
+ </ChatShell>
139
+ );
140
+ },
141
+ };
142
+
143
+ export const LongTranscript: Story = {
144
+ render: () => {
145
+ const { append } = useChatState();
146
+
147
+ const manyMessages: Msg[] = Array.from({ length: 30 }, (_, i) => ({
148
+ id: String(i),
149
+ role: i % 2 === 0 ? 'user' : 'assistant',
150
+ md:
151
+ i % 2 === 0
152
+ ? `Turn ${i}: a question about the library.`
153
+ : `Turn ${i}: a reply with some **markdown** and \`code\`.`,
154
+ timestamp: new Date(2026, 3, 23, 14, i),
155
+ }));
156
+
157
+ return (
158
+ <ChatShell
159
+ header={<Bar title="Long scroll test" />}
160
+ composer={<ChatComposer onSubmit={append} placeholder="Reply…" />}
161
+ >
162
+ {manyMessages.map((m) => (
163
+ <ChatMessage key={m.id} role={m.role} md={m.md} timestamp={m.timestamp} />
164
+ ))}
165
+ </ChatShell>
166
+ );
167
+ },
168
+ };
169
+
170
+ export const InPanels: Story = {
171
+ render: () => {
172
+ const { messages, append } = useChatState();
173
+
174
+ return (
175
+ <Panels
176
+ header={<Bar title="Workbench" />}
177
+ leftSidebar={
178
+ <Stack gap={2} padding={3}>
179
+ <Heading level={3}>Threads</Heading>
180
+ <Text isMuted size="small">
181
+ Planning session
182
+ </Text>
183
+ <Text isMuted size="small">
184
+ Bug triage
185
+ </Text>
186
+ <Text isMuted size="small">
187
+ Design review
188
+ </Text>
189
+ </Stack>
190
+ }
191
+ leftSidebarWidth={200}
192
+ >
193
+ <ChatShell
194
+ composer={<ChatComposer onSubmit={append} placeholder="Reply…" />}
195
+ >
196
+ {messages.map((m) => (
197
+ <ChatMessage key={m.id} role={m.role} md={m.md} timestamp={m.timestamp} />
198
+ ))}
199
+ </ChatShell>
200
+ </Panels>
201
+ );
202
+ },
203
+ };
@@ -0,0 +1,16 @@
1
+ import type { ChatShellProps } from './types';
2
+ import styles from './styles.module.css';
3
+
4
+ export const ChatShell = (props: ChatShellProps) => {
5
+ const { header, composer, children } = props;
6
+
7
+ return (
8
+ <div className={styles.shell}>
9
+ {header !== undefined && <div className={styles.header}>{header}</div>}
10
+ <div className={styles.transcript}>{children}</div>
11
+ <div className={styles.composer}>{composer}</div>
12
+ </div>
13
+ );
14
+ };
15
+
16
+ export type { ChatShellProps };
@@ -0,0 +1,27 @@
1
+ .shell {
2
+ display: flex;
3
+ flex-direction: column;
4
+ height: 100%;
5
+ width: 100%;
6
+ background-color: var(--ui-background-0);
7
+ }
8
+
9
+ .header {
10
+ flex-shrink: 0;
11
+ background-color: var(--ui-background-2);
12
+ border-bottom: 1px solid var(--ui-border);
13
+ }
14
+
15
+ .transcript {
16
+ flex: 1 1 0;
17
+ overflow: auto;
18
+ /* min-height: 0 lets the flex child establish its own scroll context
19
+ instead of stretching to intrinsic content height. */
20
+ min-height: 0;
21
+ }
22
+
23
+ .composer {
24
+ flex-shrink: 0;
25
+ border-top: 1px solid var(--ui-border);
26
+ background-color: var(--ui-background-0);
27
+ }
@@ -0,0 +1,7 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export type ChatShellProps = {
4
+ header?: ReactNode;
5
+ composer: ReactNode;
6
+ children: ReactNode;
7
+ };
@@ -0,0 +1,59 @@
1
+ import { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { PillCombobox, type PillComboboxOption } from './index';
4
+
5
+ const meta: Meta<typeof PillCombobox> = {
6
+ title: 'Chat/PillCombobox',
7
+ component: PillCombobox,
8
+ decorators: [
9
+ (Story) => (
10
+ <div style={{ width: 480, padding: 16 }}>
11
+ <Story />
12
+ </div>
13
+ ),
14
+ ],
15
+ };
16
+
17
+ export default meta;
18
+ type Story = StoryObj<typeof PillCombobox>;
19
+
20
+ const actorOptions: PillComboboxOption<string>[] = [
21
+ { value: 'alice', label: 'Alice' },
22
+ { value: 'bob', label: 'Bob' },
23
+ { value: 'carol', label: 'Carol' },
24
+ { value: 'dave', label: 'Dave' },
25
+ { value: 'eve', label: 'Eve' },
26
+ { value: 'mallory', label: 'Mallory' },
27
+ ];
28
+
29
+ const Harness = ({
30
+ initial = [],
31
+ isDisabled,
32
+ }: {
33
+ initial?: string[];
34
+ isDisabled?: boolean;
35
+ }) => {
36
+ const [values, setValues] = useState<string[]>(initial);
37
+ return (
38
+ <PillCombobox
39
+ options={actorOptions}
40
+ values={values}
41
+ onChange={setValues}
42
+ ariaLabel="Recipients"
43
+ placeholder="Type a name…"
44
+ isDisabled={isDisabled}
45
+ />
46
+ );
47
+ };
48
+
49
+ export const Empty: Story = {
50
+ render: () => <Harness />,
51
+ };
52
+
53
+ export const WithSelections: Story = {
54
+ render: () => <Harness initial={['alice', 'bob']} />,
55
+ };
56
+
57
+ export const Disabled: Story = {
58
+ render: () => <Harness initial={['alice']} isDisabled />,
59
+ };
@@ -0,0 +1,17 @@
1
+ import { cx } from '../../../utils';
2
+ import { PillComboboxCore } from '../PillComboboxCore/Core';
3
+ import type { PillComboboxOption, PillComboboxProps } from './types';
4
+ import styles from './styles.module.css';
5
+
6
+ // Bordered, form-element variant. Drops into a Field like a fancy Select.
7
+ // For a flush, modal-header packaging of the same combobox, see
8
+ // `ChatRecipientsHeader`.
9
+ export const PillCombobox = <T extends string,>(props: PillComboboxProps<T>) => (
10
+ <PillComboboxCore
11
+ {...props}
12
+ shellClassName={cx(styles.shell, props.isDisabled && styles.shellDisabled)}
13
+ rowClassName={cx(styles.row, props.isDisabled && styles.rowDisabled)}
14
+ />
15
+ );
16
+
17
+ export type { PillComboboxOption, PillComboboxProps };
@@ -0,0 +1,29 @@
1
+ .shell {
2
+ border: 1px solid var(--ui-border);
3
+ border-radius: var(--ui-radius);
4
+ background-color: var(--ui-background-0);
5
+ }
6
+
7
+ .shell:focus-within {
8
+ outline: 2px solid var(--ui-accent);
9
+ outline-offset: -1px;
10
+ border-color: var(--ui-accent);
11
+ }
12
+
13
+ .shellDisabled {
14
+ opacity: 0.5;
15
+ }
16
+
17
+ .row {
18
+ display: flex;
19
+ flex-wrap: wrap;
20
+ align-items: center;
21
+ gap: var(--ui-space-1);
22
+ padding: var(--ui-space-1) var(--ui-space-2);
23
+ cursor: text;
24
+ min-height: calc(var(--ui-text-medium) * var(--ui-line-height) + var(--ui-space-2) * 2);
25
+ }
26
+
27
+ .rowDisabled {
28
+ cursor: not-allowed;
29
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * A single selectable option. `value` is the stable key (e.g. an actor id
3
+ * stringified, a slug); `label` is what the user sees in pills and the
4
+ * dropdown.
5
+ */
6
+ export type PillComboboxOption<T extends string> = {
7
+ value: T;
8
+ label: string;
9
+ };
10
+
11
+ /**
12
+ * Gmail-style "type to search, select adds a pill" combobox. Generic by
13
+ * value type. Lives in the Chat namespace for now; built to general
14
+ * ui-component standards so it can graduate when a second consumer
15
+ * arrives.
16
+ */
17
+ export type PillComboboxProps<T extends string> = {
18
+ options: PillComboboxOption<T>[];
19
+ values: T[];
20
+ onChange: (next: T[]) => void;
21
+ /** Accessible name for the underlying combobox input. */
22
+ ariaLabel: string;
23
+ /** Shown when no pills are selected and the input is empty. */
24
+ placeholder?: string;
25
+ isDisabled?: boolean;
26
+ /** Shown in the dropdown when the typed query has no matches. */
27
+ emptyHint?: string;
28
+ };