@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,17 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export type StackDirection = 'row' | 'column';
4
+ export type StackGap = 0 | 1 | 2 | 3 | 4 | 5 | 6;
5
+ export type StackPadding = 0 | 1 | 2 | 3 | 4 | 5 | 6;
6
+ export type StackAlign = 'start' | 'center' | 'end' | 'stretch';
7
+ export type StackJustify = 'start' | 'center' | 'end' | 'between';
8
+
9
+ export type StackProps = {
10
+ children: ReactNode;
11
+ direction?: StackDirection;
12
+ gap?: StackGap;
13
+ padding?: StackPadding;
14
+ align?: StackAlign;
15
+ justify?: StackJustify;
16
+ isWrap?: boolean;
17
+ };
@@ -0,0 +1,73 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { ConfirmModal } from './index';
3
+ import { Toggle } from '../../../storybook';
4
+
5
+ const meta: Meta<typeof ConfirmModal> = {
6
+ title: 'Modals/ConfirmModal',
7
+ component: ConfirmModal,
8
+ };
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof ConfirmModal>;
12
+
13
+ type HarnessProps = {
14
+ isDanger?: boolean;
15
+ title: string;
16
+ description?: string;
17
+ confirmLabel?: string;
18
+ initialOpen?: boolean;
19
+ };
20
+
21
+ const Harness = ({
22
+ isDanger,
23
+ title,
24
+ description,
25
+ confirmLabel,
26
+ initialOpen = false,
27
+ }: HarnessProps) => (
28
+ <Toggle defaultOn={initialOpen}>
29
+ {({ isOn, on, off }) => (
30
+ <>
31
+ <button type="button" onClick={on}>Open</button>
32
+ <ConfirmModal
33
+ isOpen={isOn}
34
+ onCancel={off}
35
+ onConfirm={async () => {
36
+ await new Promise((r) => setTimeout(r, 500));
37
+ off();
38
+ }}
39
+ title={title}
40
+ description={description}
41
+ confirmLabel={confirmLabel}
42
+ isDanger={isDanger}
43
+ />
44
+ </>
45
+ )}
46
+ </Toggle>
47
+ );
48
+
49
+ export const Default: Story = {
50
+ render: () => (
51
+ <Harness
52
+ initialOpen
53
+ title="Archive this work item?"
54
+ description="It will move to the archive and stop appearing in the active board."
55
+ confirmLabel="Archive"
56
+ />
57
+ ),
58
+ };
59
+
60
+ export const Danger: Story = {
61
+ render: () => (
62
+ <Harness
63
+ isDanger
64
+ title="Delete this actor?"
65
+ description="Their agent runs will be orphaned. This cannot be undone."
66
+ confirmLabel="Delete"
67
+ />
68
+ ),
69
+ };
70
+
71
+ export const NoDescription: Story = {
72
+ render: () => <Harness title="Continue?" />,
73
+ };
@@ -0,0 +1,72 @@
1
+ import { useState } from 'react';
2
+ import { cx } from '../../../utils';
3
+ import { ModalShell } from '../internal/ModalShell';
4
+ import type { ConfirmModalProps } from './types';
5
+ import styles from './styles.module.css';
6
+
7
+ export const ConfirmModal = (props: ConfirmModalProps) => {
8
+ const {
9
+ isOpen,
10
+ onConfirm,
11
+ onCancel,
12
+ title,
13
+ description,
14
+ confirmLabel = 'Confirm',
15
+ cancelLabel = 'Cancel',
16
+ isDanger = false,
17
+ } = props;
18
+
19
+ const [isWorking, setIsWorking] = useState(false);
20
+
21
+ const handleConfirm = async () => {
22
+ try {
23
+ setIsWorking(true);
24
+ await onConfirm();
25
+ } finally {
26
+ setIsWorking(false);
27
+ }
28
+ };
29
+
30
+ // Block backdrop, Esc, and the close X from cancelling while in-flight.
31
+ const handleClose = () => {
32
+ if (!isWorking) onCancel();
33
+ };
34
+
35
+ return (
36
+ <ModalShell
37
+ isOpen={isOpen}
38
+ onClose={handleClose}
39
+ title={title}
40
+ dialogClassName={styles.dialog}
41
+ bodyClassName={styles.body}
42
+ footer={
43
+ <>
44
+ <button
45
+ type="button"
46
+ className={cx(styles.button, styles.cancelButton)}
47
+ onClick={onCancel}
48
+ disabled={isWorking}
49
+ autoFocus
50
+ >
51
+ {cancelLabel}
52
+ </button>
53
+ <button
54
+ type="button"
55
+ className={cx(
56
+ styles.button,
57
+ isDanger ? styles.dangerButton : styles.confirmButton,
58
+ )}
59
+ onClick={handleConfirm}
60
+ disabled={isWorking}
61
+ >
62
+ {isWorking ? 'Working…' : confirmLabel}
63
+ </button>
64
+ </>
65
+ }
66
+ >
67
+ {description && <div className={styles.description}>{description}</div>}
68
+ </ModalShell>
69
+ );
70
+ };
71
+
72
+ export type { ConfirmModalProps };
@@ -0,0 +1,62 @@
1
+ .dialog {
2
+ min-width: 360px;
3
+ max-width: 480px;
4
+ }
5
+
6
+ .body {
7
+ min-height: 120px;
8
+ }
9
+
10
+ .description {
11
+ margin: 0;
12
+ font-size: var(--ui-text-medium);
13
+ line-height: var(--ui-line-height);
14
+ color: var(--ui-muted);
15
+ }
16
+
17
+ .button {
18
+ font-family: var(--ui-font);
19
+ font-size: var(--ui-text-medium);
20
+ font-weight: 500;
21
+ line-height: 1;
22
+ border: 1px solid var(--ui-border);
23
+ border-radius: var(--ui-radius);
24
+ padding: 6px 14px;
25
+ cursor: pointer;
26
+ }
27
+
28
+ .button:disabled {
29
+ opacity: 0.5;
30
+ cursor: not-allowed;
31
+ }
32
+
33
+ .cancelButton {
34
+ background-color: var(--ui-background-0);
35
+ color: var(--ui-foreground);
36
+ }
37
+
38
+ .cancelButton:hover:not(:disabled) {
39
+ background-color: var(--ui-background-1);
40
+ }
41
+
42
+ .confirmButton {
43
+ background-color: var(--ui-accent);
44
+ color: var(--ui-on-accent);
45
+ border-color: var(--ui-accent);
46
+ }
47
+
48
+ .confirmButton:hover:not(:disabled) {
49
+ background-color: var(--ui-accent-hover);
50
+ border-color: var(--ui-accent-hover);
51
+ }
52
+
53
+ .dangerButton {
54
+ background-color: var(--ui-danger);
55
+ color: var(--ui-on-danger);
56
+ border-color: var(--ui-danger);
57
+ }
58
+
59
+ .dangerButton:hover:not(:disabled) {
60
+ background-color: var(--ui-danger-hover);
61
+ border-color: var(--ui-danger-hover);
62
+ }
@@ -0,0 +1,14 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export type ConfirmModalProps = {
4
+ isOpen: boolean;
5
+ onConfirm: () => void | Promise<void>;
6
+ onCancel: () => void;
7
+ title: string;
8
+ /** Body content. String for simple prose, ReactNode when formatting or emphasis is needed. */
9
+ description?: ReactNode;
10
+ confirmLabel?: string;
11
+ cancelLabel?: string;
12
+ /** Styles the confirm button as destructive (danger palette). */
13
+ isDanger?: boolean;
14
+ };
@@ -0,0 +1,75 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { LargeModal } from './index';
3
+ import { Button } from '../../Forms/Button';
4
+ import { Stack } from '../../Layout/Stack';
5
+ import { Text } from '../../Content/Text';
6
+ import { Lorem, Toggle } from '../../../storybook';
7
+
8
+ const meta: Meta<typeof LargeModal> = {
9
+ title: 'Modals/LargeModal',
10
+ component: LargeModal,
11
+ parameters: { layout: 'centered' },
12
+ };
13
+
14
+ export default meta;
15
+ type Story = StoryObj<typeof LargeModal>;
16
+
17
+ export const Default: Story = {
18
+ render: () => (
19
+ <Toggle defaultOn>
20
+ {({ isOn, on, off }) => (
21
+ <>
22
+ <Button onClick={on}>Open large modal</Button>
23
+ <LargeModal
24
+ isOpen={isOn}
25
+ onClose={off}
26
+ title="Workspace detail"
27
+ footer={
28
+ <Button variant="primary" onClick={off}>
29
+ Done
30
+ </Button>
31
+ }
32
+ >
33
+ <Stack gap={3}>
34
+ <Text>
35
+ Large modals take up 90% of the viewport width and height — suited to
36
+ immersive content: long forms, detail views, editors.
37
+ </Text>
38
+ <Lorem paragraphs={20} as={Text} />
39
+ </Stack>
40
+ </LargeModal>
41
+ </>
42
+ )}
43
+ </Toggle>
44
+ ),
45
+ };
46
+
47
+ export const FullBleed: Story = {
48
+ render: () => (
49
+ <Toggle>
50
+ {({ isOn, on, off }) => (
51
+ <>
52
+ <Button onClick={on}>Open full-bleed large modal</Button>
53
+ <LargeModal
54
+ isOpen={isOn}
55
+ onClose={off}
56
+ disableContentPadding
57
+ title="Custom layout"
58
+ footer={
59
+ <Button variant="primary" onClick={off}>
60
+ Close
61
+ </Button>
62
+ }
63
+ >
64
+ <div style={{ padding: 32, background: 'var(--ui-background-2)', flex: 1 }}>
65
+ <Text>
66
+ With `disableContentPadding`, children sit flush — useful when the
67
+ callee brings its own layout (split panes, tables, full-bleed media).
68
+ </Text>
69
+ </div>
70
+ </LargeModal>
71
+ </>
72
+ )}
73
+ </Toggle>
74
+ ),
75
+ };
@@ -0,0 +1,9 @@
1
+ import type { LargeModalProps } from './types';
2
+ import { ModalShell } from '../internal/ModalShell';
3
+ import styles from './styles.module.css';
4
+
5
+ export const LargeModal = (props: LargeModalProps) => (
6
+ <ModalShell {...props} dialogClassName={styles.dialog} />
7
+ );
8
+
9
+ export type { LargeModalProps };
@@ -0,0 +1,6 @@
1
+ .dialog {
2
+ width: 90vw;
3
+ height: 90vh;
4
+ max-width: none;
5
+ max-height: none;
6
+ }
@@ -0,0 +1,18 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export type LargeModalProps = {
4
+ isOpen: boolean;
5
+ onClose: () => void;
6
+ title?: string;
7
+ /** Optional secondary header strips rendered between the title bar and
8
+ * the body. Each entry is wrapped in a div with the same divider style
9
+ * so they stack as distinct rows (recipients editor, breadcrumbs,
10
+ * filter bar, etc.). Each entry must carry a `key` prop —
11
+ * `react/jsx-key` will flag any array element without one. */
12
+ subHeaders?: ReactNode[];
13
+ children: ReactNode;
14
+ footer?: ReactNode;
15
+ /** Opt out of the default body padding for full-bleed content. */
16
+ disableContentPadding?: boolean;
17
+ contentBackgroundOffsetColor?: boolean;
18
+ };
@@ -0,0 +1,121 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { MediumModal } from './index';
3
+ import { Button } from '../../Forms/Button';
4
+ import { Field } from '../../Forms/Field';
5
+ import { Input } from '../../Forms/Input';
6
+ import { Stack } from '../../Layout/Stack';
7
+ import { Text } from '../../Content/Text';
8
+ import { Lorem, Toggle } from '../../../storybook';
9
+
10
+ const meta: Meta<typeof MediumModal> = {
11
+ title: 'Modals/MediumModal',
12
+ component: MediumModal,
13
+ parameters: { layout: 'centered' },
14
+ };
15
+
16
+ export default meta;
17
+ type Story = StoryObj<typeof MediumModal>;
18
+
19
+ export const Default: Story = {
20
+ render: () => (
21
+ <Toggle defaultOn>
22
+ {({ isOn, on, off }) => (
23
+ <>
24
+ <Button onClick={on}>Open modal</Button>
25
+ <MediumModal
26
+ isOpen={isOn}
27
+ onClose={off}
28
+ title="Confirm action"
29
+ footer={
30
+ <>
31
+ <Button onClick={off}>Cancel</Button>
32
+ <Button variant="primary" onClick={off}>Confirm</Button>
33
+ </>
34
+ }
35
+ >
36
+ <Text>Are you sure you want to proceed? This action cannot be undone.</Text>
37
+ </MediumModal>
38
+ </>
39
+ )}
40
+ </Toggle>
41
+ ),
42
+ };
43
+
44
+ export const WithForm: Story = {
45
+ render: () => (
46
+ <Toggle>
47
+ {({ isOn, on, off }) => (
48
+ <>
49
+ <Button onClick={on}>New item…</Button>
50
+ <MediumModal
51
+ isOpen={isOn}
52
+ onClose={off}
53
+ contentBackgroundOffsetColor
54
+ title="Create item"
55
+ footer={
56
+ <>
57
+ <Button onClick={off}>Cancel</Button>
58
+ <Button variant="primary" onClick={off}>Create</Button>
59
+ </>
60
+ }
61
+ >
62
+ <Stack gap={3}>
63
+ <Field label="Title">
64
+ <Input placeholder="Title" />
65
+ </Field>
66
+ <Field label="Description" hint="Optional">
67
+ <Input placeholder="Short description" />
68
+ </Field>
69
+ </Stack>
70
+ </MediumModal>
71
+ </>
72
+ )}
73
+ </Toggle>
74
+ ),
75
+ };
76
+
77
+ export const LongContent: Story = {
78
+ render: () => (
79
+ <Toggle>
80
+ {({ isOn, on, off }) => (
81
+ <>
82
+ <Button onClick={on}>View release notes</Button>
83
+ <MediumModal
84
+ isOpen={isOn}
85
+ onClose={off}
86
+ title="Release notes"
87
+ footer={
88
+ <>
89
+ <Button onClick={off}>Close</Button>
90
+ <Button variant="primary" onClick={off}>Acknowledge</Button>
91
+ </>
92
+ }
93
+ >
94
+ <Stack gap={3}>
95
+ <Lorem paragraphs={30} as={Text} />
96
+ </Stack>
97
+ </MediumModal>
98
+ </>
99
+ )}
100
+ </Toggle>
101
+ ),
102
+ };
103
+
104
+ export const NoTitle: Story = {
105
+ render: () => (
106
+ <Toggle>
107
+ {({ isOn, on, off }) => (
108
+ <>
109
+ <Button onClick={on}>Open untitled modal</Button>
110
+ <MediumModal
111
+ isOpen={isOn}
112
+ onClose={off}
113
+ footer={<Button variant="primary" onClick={off}>Done</Button>}
114
+ >
115
+ <Text>A modal without a title still shows the close affordance in the header.</Text>
116
+ </MediumModal>
117
+ </>
118
+ )}
119
+ </Toggle>
120
+ ),
121
+ };
@@ -0,0 +1,48 @@
1
+ import { describe, test, expect, mock } from 'bun:test';
2
+ import { render, fireEvent, cleanup } from '@testing-library/react';
3
+ import { MediumModal } from './index';
4
+
5
+ // happy-dom implements showMediumModal/close but we call cleanup between tests
6
+ // manually (bun:test has no afterEach-by-default in this setup).
7
+ const setup = (props: Partial<React.ComponentProps<typeof MediumModal>> = {}) => {
8
+ const onClose = mock(() => {});
9
+ const utils = render(
10
+ <MediumModal isOpen onClose={onClose} title="Settings" {...props}>
11
+ <button type="button">inside</button>
12
+ </MediumModal>,
13
+ );
14
+ const dialog = utils.container.ownerDocument.querySelector('dialog');
15
+ if (!dialog) throw new Error('dialog not rendered');
16
+ return { ...utils, onClose, dialog };
17
+ };
18
+
19
+ describe('MediumModal', () => {
20
+ test('renders title and children when open', () => {
21
+ const { getByText } = setup();
22
+ expect(getByText('Settings')).toBeTruthy();
23
+ expect(getByText('inside')).toBeTruthy();
24
+ cleanup();
25
+ });
26
+
27
+ test('calls onClose when the backdrop (the dialog itself) is clicked', () => {
28
+ const { onClose, dialog } = setup();
29
+ fireEvent.click(dialog);
30
+ expect(onClose).toHaveBeenCalledTimes(1);
31
+ cleanup();
32
+ });
33
+
34
+ test('does not call onClose when clicking inside the content', () => {
35
+ const { onClose, getByText } = setup();
36
+ fireEvent.click(getByText('inside'));
37
+ expect(onClose).not.toHaveBeenCalled();
38
+ cleanup();
39
+ });
40
+
41
+ test('calls onClose on the native cancel event (Escape)', () => {
42
+ const { onClose, dialog } = setup();
43
+ const cancel = new Event('cancel', { cancelable: true });
44
+ dialog.dispatchEvent(cancel);
45
+ expect(onClose).toHaveBeenCalledTimes(1);
46
+ cleanup();
47
+ });
48
+ });
@@ -0,0 +1,9 @@
1
+ import type { MediumModalProps } from './types';
2
+ import { ModalShell } from '../internal/ModalShell';
3
+ import styles from './styles.module.css';
4
+
5
+ export const MediumModal = (props: MediumModalProps) => (
6
+ <ModalShell {...props} dialogClassName={styles.dialog} />
7
+ );
8
+
9
+ export type { MediumModalProps };
@@ -0,0 +1,5 @@
1
+ .dialog {
2
+ width: min(90vw, 768px);
3
+ min-height: 576px;
4
+ max-height: 85vh;
5
+ }
@@ -0,0 +1,18 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export type MediumModalProps = {
4
+ isOpen: boolean;
5
+ onClose: () => void;
6
+ title?: string;
7
+ /** Optional secondary header strips rendered between the title bar and
8
+ * the body. Each entry is wrapped in a div with the same divider style
9
+ * so they stack as distinct rows (recipients editor, breadcrumbs,
10
+ * filter bar, etc.). Each entry must carry a `key` prop —
11
+ * `react/jsx-key` will flag any array element without one. */
12
+ subHeaders?: ReactNode[];
13
+ children: ReactNode;
14
+ footer?: ReactNode;
15
+ /** Opt out of the default body padding for full-bleed content. */
16
+ disableContentPadding?: boolean;
17
+ contentBackgroundOffsetColor?: boolean;
18
+ };
@@ -0,0 +1,3 @@
1
+ export { MediumModal, type MediumModalProps } from './MediumModal';
2
+ export { LargeModal, type LargeModalProps } from './LargeModal';
3
+ export { ConfirmModal, type ConfirmModalProps } from './ConfirmModal';
@@ -0,0 +1,21 @@
1
+ import type { ReactNode } from 'react';
2
+ import { cx } from '../../../utils';
3
+ import styles from './styles.module.css';
4
+
5
+ type Props = {
6
+ children: ReactNode;
7
+ /** Opt out of the default body padding for full-bleed content
8
+ * (split panes, tables, custom layouts that bring their own). */
9
+ disableContentPadding?: boolean;
10
+ className?: string;
11
+ };
12
+
13
+ export const ModalBody = (props: Props) => {
14
+ const { children, disableContentPadding = false, className } = props;
15
+
16
+ return (
17
+ <div className={cx(styles.body, !disableContentPadding && styles.bodyPadded, className)}>
18
+ {children}
19
+ </div>
20
+ );
21
+ };
@@ -0,0 +1,12 @@
1
+ import type { ReactNode } from 'react';
2
+ import styles from './styles.module.css';
3
+
4
+ type Props = {
5
+ children: ReactNode;
6
+ };
7
+
8
+ export const ModalFooter = (props: Props) => {
9
+ const { children } = props;
10
+
11
+ return <footer className={styles.footer}>{children}</footer>;
12
+ };
@@ -0,0 +1,27 @@
1
+ import { IconButton } from '../../Forms/IconButton';
2
+ import styles from './styles.module.css';
3
+
4
+ type Props = {
5
+ title?: string;
6
+ onClose: () => void;
7
+ };
8
+
9
+ export const ModalHeader = (props: Props) => {
10
+ const { title, onClose } = props;
11
+
12
+ return (
13
+ <header className={styles.header}>
14
+ {title ? <h2 className={styles.title}>{title}</h2> : <span />}
15
+ <IconButton ariaLabel="Close" size="small" onClick={onClose}>
16
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
17
+ <path
18
+ d="M4 4 L12 12 M12 4 L4 12"
19
+ stroke="currentColor"
20
+ strokeWidth="1.5"
21
+ strokeLinecap="round"
22
+ />
23
+ </svg>
24
+ </IconButton>
25
+ </header>
26
+ );
27
+ };