@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
package/bunfig.toml ADDED
@@ -0,0 +1,2 @@
1
+ [test]
2
+ preload = "./test-setup.ts"
@@ -0,0 +1,106 @@
1
+ import boundaries from 'eslint-plugin-boundaries';
2
+ import tsParser from '@typescript-eslint/parser';
3
+
4
+ // Component primitives live under src/components/<Section>/<Component>/.
5
+ // "Umbrella sections" (Modals, Chat) are themselves the primitive — their
6
+ // sub-folders share internals and must be reached through the section barrel.
7
+ // Plain sections (Layout, Content, Forms, ...) treat each component folder as
8
+ // its own primitive.
9
+ //
10
+ // Three element types so rules can distinguish roles inside a primitive:
11
+ // - primitive-entry → the barrel (`<primitive>/index.{ts,tsx}`)
12
+ // - primitive-dogfood → stories and tests; allowed to use their own barrel
13
+ // because they demonstrate / exercise the public API
14
+ // - primitive → any other file inside the primitive
15
+ //
16
+ // Ordering matters; more specific patterns first so they win the match.
17
+ // `!(Modals|Chat)` is an extglob negation that excludes umbrella sections from
18
+ // the depth-3 patterns, so files inside Modals/Chat fall through to the
19
+ // depth-2 patterns and resolve to the umbrella as one primitive.
20
+ export default [
21
+ {
22
+ files: ['src/**/*.{ts,tsx}'],
23
+ plugins: { boundaries },
24
+ languageOptions: {
25
+ parser: tsParser,
26
+ parserOptions: { ecmaVersion: 'latest', sourceType: 'module', ecmaFeatures: { jsx: true } },
27
+ },
28
+ settings: {
29
+ 'import/resolver': {
30
+ node: { extensions: ['.js', '.jsx', '.ts', '.tsx'] },
31
+ },
32
+ 'boundaries/include': ['src/**/*'],
33
+ 'boundaries/elements': [
34
+ // Plain-section component primitives (depth-3 under src/components)
35
+ {
36
+ type: 'primitive-entry',
37
+ pattern: 'src/components/!(Modals|Chat)/*/index.{ts,tsx}',
38
+ mode: 'full',
39
+ capture: ['_section', 'name'],
40
+ },
41
+ {
42
+ type: 'primitive-dogfood',
43
+ pattern: 'src/components/!(Modals|Chat)/*/**/*.{stories,test}.{ts,tsx}',
44
+ mode: 'full',
45
+ capture: ['_section', 'name'],
46
+ },
47
+ {
48
+ type: 'primitive',
49
+ pattern: 'src/components/!(Modals|Chat)/*',
50
+ mode: 'folder',
51
+ capture: ['_section', 'name'],
52
+ },
53
+ // Umbrella-section primitives (depth-2): Modals, Chat
54
+ {
55
+ type: 'primitive-entry',
56
+ pattern: 'src/components/*/index.{ts,tsx}',
57
+ mode: 'full',
58
+ capture: ['name'],
59
+ },
60
+ {
61
+ type: 'primitive-dogfood',
62
+ pattern: 'src/components/*/**/*.{stories,test}.{ts,tsx}',
63
+ mode: 'full',
64
+ capture: ['name'],
65
+ },
66
+ {
67
+ type: 'primitive',
68
+ pattern: 'src/components/*',
69
+ mode: 'folder',
70
+ capture: ['name'],
71
+ },
72
+ ],
73
+ },
74
+ rules: {
75
+ 'boundaries/no-unknown': 'off',
76
+ 'boundaries/no-unknown-files': 'off',
77
+ 'boundaries/dependencies': [
78
+ 'error',
79
+ {
80
+ default: 'allow',
81
+ rules: [
82
+ // External imports into a primitive must go through its barrel.
83
+ // Importers that aren't in the same primitive can't reach internals.
84
+ {
85
+ from: [
86
+ { type: 'primitive', captured: { name: '!{{ to.captured.name }}' } },
87
+ { type: 'primitive-entry', captured: { name: '!{{ to.captured.name }}' } },
88
+ { type: 'primitive-dogfood', captured: { name: '!{{ to.captured.name }}' } },
89
+ ],
90
+ disallow: [{ to: { type: 'primitive' } }],
91
+ message: "External imports must go through the folder's index barrel.",
92
+ },
93
+ // Inside the same primitive, files must not import their own barrel
94
+ // (cycle risk). Reach the neighbor directly.
95
+ {
96
+ from: [{ type: 'primitive', captured: { name: '{{ to.captured.name }}' } }],
97
+ disallow: [{ to: { type: 'primitive-entry' } }],
98
+ message:
99
+ "Files inside a primitive folder must not import their own index barrel; import the neighbor directly.",
100
+ },
101
+ ],
102
+ },
103
+ ],
104
+ },
105
+ },
106
+ ];
package/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./src";
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@structuralists/scaffolding",
3
+ "version": "0.0.1",
4
+ "main": "./index.ts",
5
+ "types": "./index.ts",
6
+ "exports": {
7
+ ".": "./index.ts",
8
+ "./tokens.css": "./tokens.css"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "scripts": {
14
+ "dev": "storybook dev -p 6015 --ci",
15
+ "storybook": "storybook dev -p 6015",
16
+ "build-storybook": "storybook build",
17
+ "test": "bun test",
18
+ "typecheck": "tsc --noEmit",
19
+ "lint": "eslint src"
20
+ },
21
+ "dependencies": {
22
+ "@uiw/react-json-view": "^2.0.0-alpha.41",
23
+ "react-markdown": "^10.1.0"
24
+ },
25
+ "peerDependencies": {
26
+ "react": "^19.0.0",
27
+ "react-dom": "^19.0.0",
28
+ "react-router": "^7.0.0"
29
+ },
30
+ "devDependencies": {
31
+ "@happy-dom/global-registrator": "^15.0.0",
32
+ "@storybook/addon-essentials": "^8.6.0",
33
+ "@storybook/addon-storysource": "^8.6.0",
34
+ "@storybook/react": "^8.6.0",
35
+ "@storybook/react-vite": "^8.6.0",
36
+ "@testing-library/dom": "^10.0.0",
37
+ "@testing-library/react": "^16.0.0",
38
+ "@types/bun": "latest",
39
+ "@types/react": "^19.0.0",
40
+ "@types/react-dom": "^19.0.0",
41
+ "@typescript-eslint/parser": "^8.58.2",
42
+ "@vitejs/plugin-react": "^4.0.0",
43
+ "eslint": "^10.2.1",
44
+ "eslint-plugin-boundaries": "^6.0.2",
45
+ "react-router": "^7.0.0",
46
+ "storybook": "^8.6.0",
47
+ "typescript": "^5.0.0",
48
+ "vite": "^6.0.0"
49
+ }
50
+ }
@@ -0,0 +1,68 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { ChatComposer } from './index';
3
+
4
+ const meta: Meta<typeof ChatComposer> = {
5
+ title: 'Chat/ChatComposer',
6
+ component: ChatComposer,
7
+ decorators: [
8
+ (Story) => (
9
+ <div style={{ width: 720, border: '1px solid var(--ui-border)' }}>
10
+ <Story />
11
+ </div>
12
+ ),
13
+ ],
14
+ };
15
+
16
+ export default meta;
17
+ type Story = StoryObj<typeof ChatComposer>;
18
+
19
+ export const Default: Story = {
20
+ args: {
21
+ onSubmit: (text) => alert(`submit: ${text}`),
22
+ },
23
+ };
24
+
25
+ export const WithInitialValue: Story = {
26
+ args: {
27
+ initialValue: 'What does BackgroundToken accept again?',
28
+ onSubmit: (text) => alert(`submit: ${text}`),
29
+ },
30
+ };
31
+
32
+ export const Disabled: Story = {
33
+ args: {
34
+ isDisabled: true,
35
+ initialValue: 'Waiting on the last response…',
36
+ onSubmit: () => {},
37
+ },
38
+ };
39
+
40
+ export const WithLeftControls: Story = {
41
+ args: {
42
+ onSubmit: (text) => alert(`submit: ${text}`),
43
+ left: (
44
+ <button
45
+ type="button"
46
+ style={{
47
+ font: 'inherit',
48
+ padding: '4px 8px',
49
+ border: '1px solid var(--ui-border)',
50
+ borderRadius: 'var(--ui-radius)',
51
+ background: 'var(--ui-background-1)',
52
+ color: 'var(--ui-foreground)',
53
+ cursor: 'pointer',
54
+ }}
55
+ >
56
+ 📎 Attach
57
+ </button>
58
+ ),
59
+ },
60
+ };
61
+
62
+ export const CustomSubmitLabel: Story = {
63
+ args: {
64
+ submitLabel: 'Run',
65
+ placeholder: 'Type a command…',
66
+ onSubmit: (text) => alert(`run: ${text}`),
67
+ },
68
+ };
@@ -0,0 +1,74 @@
1
+ import { useState, useRef } from 'react';
2
+ import type { KeyboardEvent, ChangeEvent } from 'react';
3
+ import type { ChatComposerProps } from './types';
4
+ import styles from './styles.module.css';
5
+
6
+ export const ChatComposer = (props: ChatComposerProps) => {
7
+ const {
8
+ onSubmit,
9
+ placeholder = 'Message…',
10
+ initialValue = '',
11
+ isDisabled = false,
12
+ submitLabel = 'Send',
13
+ left,
14
+ right,
15
+ } = props;
16
+
17
+ const [value, setValue] = useState(initialValue);
18
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
19
+
20
+ const canSubmit = !isDisabled && value.trim().length > 0;
21
+
22
+ const submit = () => {
23
+ if (!canSubmit) return;
24
+ onSubmit(value);
25
+ setValue('');
26
+ };
27
+
28
+ const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
29
+ if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
30
+ e.preventDefault();
31
+ submit();
32
+ return;
33
+ }
34
+ if (e.key === 'Escape') {
35
+ textareaRef.current?.blur();
36
+ }
37
+ };
38
+
39
+ const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
40
+ setValue(e.target.value);
41
+ };
42
+
43
+ return (
44
+ <div className={styles.composer}>
45
+ <textarea
46
+ ref={textareaRef}
47
+ className={styles.textarea}
48
+ value={value}
49
+ onChange={handleChange}
50
+ onKeyDown={handleKeyDown}
51
+ placeholder={placeholder}
52
+ disabled={isDisabled}
53
+ rows={2}
54
+ />
55
+ <div className={styles.controls}>
56
+ {left !== undefined && <div className={styles.controlsLeft}>{left}</div>}
57
+ <span className={styles.hint}>Enter to send · Shift+Enter for newline</span>
58
+ <div className={styles.controlsRight}>
59
+ {right}
60
+ <button
61
+ type="button"
62
+ className={styles.submit}
63
+ onClick={submit}
64
+ disabled={!canSubmit}
65
+ >
66
+ {submitLabel}
67
+ </button>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ );
72
+ };
73
+
74
+ export type { ChatComposerProps };
@@ -0,0 +1,88 @@
1
+ .composer {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--ui-space-2);
5
+ padding: var(--ui-space-3) var(--ui-space-4);
6
+ }
7
+
8
+ .textarea {
9
+ font-family: var(--ui-font);
10
+ font-size: var(--ui-text-medium);
11
+ line-height: var(--ui-line-height);
12
+ color: var(--ui-foreground);
13
+ background-color: var(--ui-background-0);
14
+ border: 1px solid var(--ui-border);
15
+ border-radius: var(--ui-radius);
16
+ padding: var(--ui-space-2) var(--ui-space-3);
17
+ width: 100%;
18
+ resize: none;
19
+ /* field-sizing grows the textarea with content; floor via min-height below. */
20
+ field-sizing: content;
21
+ min-height: calc(var(--ui-text-medium) * var(--ui-line-height) * 2 + var(--ui-space-2) * 2 + 2px);
22
+ max-height: 240px;
23
+ overflow-y: auto;
24
+ }
25
+
26
+ .textarea:focus {
27
+ outline: 2px solid var(--ui-accent);
28
+ outline-offset: -1px;
29
+ border-color: var(--ui-accent);
30
+ }
31
+
32
+ .textarea:disabled {
33
+ opacity: 0.5;
34
+ cursor: not-allowed;
35
+ }
36
+
37
+ .controls {
38
+ display: flex;
39
+ align-items: center;
40
+ gap: var(--ui-space-2);
41
+ }
42
+
43
+ .controlsLeft {
44
+ display: flex;
45
+ align-items: center;
46
+ gap: var(--ui-space-2);
47
+ }
48
+
49
+ .controlsRight {
50
+ display: flex;
51
+ align-items: center;
52
+ gap: var(--ui-space-2);
53
+ margin-left: auto;
54
+ }
55
+
56
+ .hint {
57
+ font-size: var(--ui-text-xsmall);
58
+ color: var(--ui-muted);
59
+ line-height: var(--ui-line-height-tight);
60
+ }
61
+
62
+ .submit {
63
+ font-family: var(--ui-font);
64
+ font-size: var(--ui-text-small);
65
+ font-weight: 600;
66
+ color: var(--ui-on-accent);
67
+ background-color: var(--ui-accent);
68
+ border: 1px solid var(--ui-accent);
69
+ border-radius: var(--ui-radius);
70
+ padding: var(--ui-space-1) var(--ui-space-3);
71
+ cursor: pointer;
72
+ line-height: var(--ui-line-height-tight);
73
+ }
74
+
75
+ .submit:hover:not(:disabled) {
76
+ background-color: var(--ui-accent-hover);
77
+ border-color: var(--ui-accent-hover);
78
+ }
79
+
80
+ .submit:focus-visible {
81
+ outline: 2px solid var(--ui-accent);
82
+ outline-offset: 2px;
83
+ }
84
+
85
+ .submit:disabled {
86
+ opacity: 0.5;
87
+ cursor: not-allowed;
88
+ }
@@ -0,0 +1,11 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export type ChatComposerProps = {
4
+ onSubmit: (text: string) => void;
5
+ placeholder?: string;
6
+ initialValue?: string;
7
+ isDisabled?: boolean;
8
+ submitLabel?: string;
9
+ left?: ReactNode;
10
+ right?: ReactNode;
11
+ };
@@ -0,0 +1,111 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { ChatMessage } from './index';
3
+ import { Stack } from '../../Layout/Stack';
4
+ import { Badge } from '../../Content/Badge';
5
+ import { Json } from '../../Json/Json';
6
+
7
+ const meta: Meta<typeof ChatMessage> = {
8
+ title: 'Chat/ChatMessage',
9
+ component: ChatMessage,
10
+ };
11
+
12
+ export default meta;
13
+ type Story = StoryObj<typeof ChatMessage>;
14
+
15
+ const now = new Date(2026, 3, 23, 15, 42);
16
+
17
+ export const User: Story = {
18
+ args: {
19
+ role: 'user',
20
+ md: 'Can you summarize the last three commits on this branch?',
21
+ timestamp: now,
22
+ },
23
+ };
24
+
25
+ export const Assistant: Story = {
26
+ args: {
27
+ role: 'assistant',
28
+ timestamp: now,
29
+ md: `Sure — here's what landed:
30
+
31
+ 1. **\`feat:\` add ChatMessage primitive** — role-driven chrome, \`md | element\` content.
32
+ 2. **\`chore:\` bump storybook** — 8.6.14 → 8.6.18.
33
+ 3. **\`fix:\` Popover placement off-by-one** — corner math now uses \`getBoundingClientRect\`.
34
+
35
+ Want me to open a PR?`,
36
+ },
37
+ };
38
+
39
+ export const System: Story = {
40
+ args: {
41
+ role: 'system',
42
+ md: 'Conversation started. Model: claude-opus-4-7. Tools: read, grep, bash.',
43
+ },
44
+ };
45
+
46
+ export const WithAuthorOverride: Story = {
47
+ args: {
48
+ role: 'user',
49
+ author: 'william.lubelski',
50
+ timestamp: now,
51
+ md: 'Override the default role label when you have a real identity to show.',
52
+ },
53
+ };
54
+
55
+ export const WithFooter: Story = {
56
+ args: {
57
+ role: 'assistant',
58
+ timestamp: now,
59
+ md: 'Footer slot is open for actions, token counts, tool-call summaries, whatever.',
60
+ footer: '42 tokens · 1.2s',
61
+ },
62
+ };
63
+
64
+ export const ElementContent: Story = {
65
+ args: {
66
+ role: 'assistant',
67
+ timestamp: now,
68
+ element: (
69
+ <Json
70
+ value={{ branch: 'main', ahead: 3, behind: 0, dirty: false }}
71
+ isCollapsed={false}
72
+ />
73
+ ),
74
+ },
75
+ };
76
+
77
+ export const Conversation: Story = {
78
+ render: () => (
79
+ <Stack direction="column" gap={0}>
80
+ <ChatMessage
81
+ role="system"
82
+ md="Conversation started. Model: claude-opus-4-7."
83
+ />
84
+ <ChatMessage
85
+ role="user"
86
+ timestamp={new Date(2026, 3, 23, 15, 40)}
87
+ md="What does `BackgroundToken` accept?"
88
+ />
89
+ <ChatMessage
90
+ role="assistant"
91
+ timestamp={new Date(2026, 3, 23, 15, 40)}
92
+ 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.`}
93
+ />
94
+ <ChatMessage
95
+ role="user"
96
+ timestamp={new Date(2026, 3, 23, 15, 41)}
97
+ md="And which primitives expose it?"
98
+ />
99
+ <ChatMessage
100
+ role="assistant"
101
+ timestamp={new Date(2026, 3, 23, 15, 41)}
102
+ md={`\`Bar\`, \`List\`, and \`Panels\` (per-region, via \`headerBackground\` / \`footerBackground\` / etc.).`}
103
+ footer={
104
+ <>
105
+ <Badge isMono>plan.md#L43</Badge> · 2 sources
106
+ </>
107
+ }
108
+ />
109
+ </Stack>
110
+ ),
111
+ };
@@ -0,0 +1,42 @@
1
+ import type { ChatMessageProps, ChatMessageRole } from './types';
2
+ import { Markdown } from '../../Content/Markdown';
3
+ import { cx } from '../../../utils';
4
+ import styles from './styles.module.css';
5
+
6
+ const ROLE_STYLE: Record<ChatMessageRole, string> = {
7
+ user: styles.roleUser,
8
+ assistant: styles.roleAssistant,
9
+ system: styles.roleSystem,
10
+ };
11
+
12
+ const ROLE_LABEL: Record<ChatMessageRole, string> = {
13
+ user: 'You',
14
+ assistant: 'Assistant',
15
+ system: 'System',
16
+ };
17
+
18
+ const formatTimestamp = (timestamp: Date | string): string => {
19
+ if (typeof timestamp === 'string') return timestamp;
20
+ return timestamp.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
21
+ };
22
+
23
+ export const ChatMessage = (props: ChatMessageProps) => {
24
+ const { role, author, timestamp, footer } = props;
25
+ const label = author ?? ROLE_LABEL[role];
26
+ const ts = timestamp !== undefined ? formatTimestamp(timestamp) : null;
27
+
28
+ return (
29
+ <div className={cx(styles.message, ROLE_STYLE[role])}>
30
+ <div className={styles.header}>
31
+ <span className={styles.author}>{label}</span>
32
+ {ts !== null && <span className={styles.timestamp}>{ts}</span>}
33
+ </div>
34
+ <div className={styles.content}>
35
+ {'md' in props ? <Markdown>{props.md}</Markdown> : props.element}
36
+ </div>
37
+ {footer !== undefined && <div className={styles.footer}>{footer}</div>}
38
+ </div>
39
+ );
40
+ };
41
+
42
+ export type { ChatMessageProps, ChatMessageRole };
@@ -0,0 +1,58 @@
1
+ .message {
2
+ padding: var(--ui-space-2) var(--ui-space-3);
3
+ color: var(--ui-foreground);
4
+ line-height: var(--ui-line-height);
5
+ margin: var(--ui-space-2);
6
+ border-radius: var(--ui-radius);
7
+ border: 1px solid var(--ui-border-subtle);
8
+ }
9
+
10
+ .roleUser {
11
+ background-color: var(--ui-background-2);
12
+ }
13
+
14
+ .roleAssistant {
15
+ background-color: var(--ui-background-0);
16
+ }
17
+
18
+ .roleSystem {
19
+ background-color: transparent;
20
+ color: var(--ui-muted);
21
+ font-style: italic;
22
+ border: none;
23
+ max-width: none;
24
+ width: auto;
25
+ text-align: center;
26
+ }
27
+
28
+ .header {
29
+ display: flex;
30
+ align-items: baseline;
31
+ justify-content: space-between;
32
+ gap: var(--ui-space-2);
33
+ margin-bottom: var(--ui-space-1);
34
+ font-size: var(--ui-text-xsmall);
35
+ line-height: var(--ui-line-height-tight);
36
+ color: var(--ui-muted);
37
+ }
38
+
39
+ .author {
40
+ font-weight: 600;
41
+ letter-spacing: 0.02em;
42
+ text-transform: uppercase;
43
+ }
44
+
45
+ .timestamp {
46
+ font-variant-numeric: tabular-nums;
47
+ }
48
+
49
+ .content {
50
+ font-size: var(--ui-text-medium);
51
+ }
52
+
53
+ .footer {
54
+ margin-top: var(--ui-space-2);
55
+ font-size: var(--ui-text-xsmall);
56
+ line-height: var(--ui-line-height-tight);
57
+ color: var(--ui-muted);
58
+ }
@@ -0,0 +1,14 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export type ChatMessageRole = 'user' | 'assistant' | 'system';
4
+
5
+ type ChatMessageContent = { md: string } | { element: ReactNode };
6
+
7
+ type ChatMessageBase = {
8
+ role: ChatMessageRole;
9
+ author?: string;
10
+ timestamp?: Date | string;
11
+ footer?: ReactNode;
12
+ };
13
+
14
+ export type ChatMessageProps = ChatMessageBase & ChatMessageContent;