@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,112 @@
1
+ import { useEffect, useState, type ReactNode } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { cx } from '../../../utils';
4
+ import { ModalHeader } from './ModalHeader';
5
+ import { ModalBody } from './ModalBody';
6
+ import { ModalFooter } from './ModalFooter';
7
+ import styles from './styles.module.css';
8
+
9
+ export type ModalShellProps = {
10
+ isOpen: boolean;
11
+ onClose: () => void;
12
+ title?: string;
13
+ /** Optional secondary header strips rendered between the title bar and
14
+ * the body. Each entry is wrapped in a div with the same divider style
15
+ * so they stack as distinct rows (recipients editor, breadcrumbs,
16
+ * filter bar, etc.). Each entry must carry a `key` prop —
17
+ * `react/jsx-key` will flag any array element without one. */
18
+ subHeaders?: ReactNode[];
19
+ children: ReactNode;
20
+ footer?: ReactNode;
21
+ /** Opt out of the default body padding for full-bleed content. */
22
+ disableContentPadding?: boolean;
23
+ /** Tints the modal body a half-step darker than the default surface. Bars stay put. */
24
+ contentBackgroundOffsetColor?: boolean;
25
+ dialogClassName?: string;
26
+ bodyClassName?: string;
27
+ };
28
+
29
+ // Matches the keyframe duration in styles.module.css. Kept in JS so the
30
+ // unmount-after-exit timer lines up with the end of the close animation.
31
+ const CLOSE_ANIMATION_MS = 200;
32
+
33
+ export const ModalShell = (props: ModalShellProps) => {
34
+ const {
35
+ isOpen,
36
+ onClose,
37
+ title,
38
+ subHeaders,
39
+ children,
40
+ footer,
41
+ disableContentPadding = false,
42
+ contentBackgroundOffsetColor = false,
43
+ dialogClassName,
44
+ bodyClassName,
45
+ } = props;
46
+
47
+ const [isMounted, setIsMounted] = useState(isOpen);
48
+ const [isClosing, setIsClosing] = useState(false);
49
+
50
+ useEffect(() => {
51
+ if (isOpen) {
52
+ setIsMounted(true);
53
+ setIsClosing(false);
54
+ return;
55
+ }
56
+ if (!isMounted) return;
57
+ setIsClosing(true);
58
+ const timer = setTimeout(() => {
59
+ setIsMounted(false);
60
+ setIsClosing(false);
61
+ }, CLOSE_ANIMATION_MS);
62
+ return () => clearTimeout(timer);
63
+ }, [isOpen, isMounted]);
64
+
65
+ useEffect(() => {
66
+ if (!isMounted) return;
67
+ const handleKey = (e: KeyboardEvent) => {
68
+ if (e.key === 'Escape') onClose();
69
+ };
70
+ window.addEventListener('keydown', handleKey);
71
+ return () => window.removeEventListener('keydown', handleKey);
72
+ }, [isMounted, onClose]);
73
+
74
+ if (!isMounted) return null;
75
+
76
+ return createPortal(
77
+ <div className={cx(styles.layer, isClosing && styles.closing)}>
78
+ <div
79
+ className={styles.backdrop}
80
+ onClick={onClose}
81
+ aria-hidden="true"
82
+ />
83
+ <div
84
+ role="dialog"
85
+ aria-modal="true"
86
+ aria-label={title}
87
+ className={cx(
88
+ styles.dialog,
89
+ contentBackgroundOffsetColor && styles.contentOffset,
90
+ dialogClassName,
91
+ )}
92
+ >
93
+ <div className={styles.content}>
94
+ <ModalHeader title={title} onClose={onClose} />
95
+ {subHeaders?.map((child, i) => (
96
+ <div key={i} className={styles.subHeader}>
97
+ {child}
98
+ </div>
99
+ ))}
100
+ <ModalBody
101
+ disableContentPadding={disableContentPadding}
102
+ className={bodyClassName}
103
+ >
104
+ {children}
105
+ </ModalBody>
106
+ {footer && <ModalFooter>{footer}</ModalFooter>}
107
+ </div>
108
+ </div>
109
+ </div>,
110
+ document.body,
111
+ );
112
+ };
@@ -0,0 +1,141 @@
1
+ /* Outer portal layer — fills the viewport, centers the dialog box. All
2
+ * portal-based layers use the same z-index so DOM-append order decides
3
+ * stacking. The last layer opened becomes the last child of <body> and
4
+ * paints on top. */
5
+ .layer {
6
+ position: fixed;
7
+ inset: 0;
8
+ display: flex;
9
+ align-items: center;
10
+ justify-content: center;
11
+ z-index: 100;
12
+ }
13
+
14
+ .backdrop {
15
+ position: absolute;
16
+ inset: 0;
17
+ background-color: var(--ui-backdrop);
18
+ animation: backdrop-in 200ms ease-out;
19
+ }
20
+
21
+ .dialog {
22
+ --modal-content-bg: var(--ui-background-0);
23
+ --modal-bar-bg: var(--ui-background-1);
24
+ position: relative;
25
+ border: 1px solid var(--ui-border);
26
+ border-radius: var(--ui-radius-large);
27
+ background-color: var(--modal-content-bg);
28
+ color: var(--ui-foreground);
29
+ box-shadow: 0 10px 40px var(--ui-shadow);
30
+ overflow: hidden;
31
+ display: flex;
32
+ flex-direction: column;
33
+ animation: modal-in 200ms ease-out;
34
+ }
35
+
36
+ .dialog.contentOffset {
37
+ --modal-content-bg: var(--ui-background-0-offset);
38
+ }
39
+
40
+ .layer.closing .backdrop {
41
+ animation: backdrop-out 200ms ease-in forwards;
42
+ }
43
+
44
+ .layer.closing .dialog {
45
+ animation: modal-out 200ms ease-in forwards;
46
+ }
47
+
48
+ @keyframes modal-in {
49
+ from {
50
+ opacity: 0;
51
+ transform: translateY(10px) scale(0.925);
52
+ }
53
+ to {
54
+ opacity: 1;
55
+ transform: translateY(0) scale(1);
56
+ }
57
+ }
58
+
59
+ @keyframes modal-out {
60
+ from {
61
+ opacity: 1;
62
+ transform: translateY(0) scale(1);
63
+ }
64
+ to {
65
+ opacity: 0;
66
+ transform: translateY(10px) scale(0.925);
67
+ }
68
+ }
69
+
70
+ @keyframes backdrop-in {
71
+ from { opacity: 0; }
72
+ to { opacity: 1; }
73
+ }
74
+
75
+ @keyframes backdrop-out {
76
+ from { opacity: 1; }
77
+ to { opacity: 0; }
78
+ }
79
+
80
+ @media (prefers-reduced-motion: reduce) {
81
+ .backdrop,
82
+ .dialog,
83
+ .layer.closing .backdrop,
84
+ .layer.closing .dialog {
85
+ animation: none;
86
+ }
87
+ }
88
+
89
+ .content {
90
+ display: flex;
91
+ flex-direction: column;
92
+ flex: 1 1 auto;
93
+ min-height: 0;
94
+ }
95
+
96
+ .header {
97
+ display: flex;
98
+ align-items: center;
99
+ justify-content: space-between;
100
+ gap: var(--ui-space-3);
101
+ padding: var(--ui-space-3);
102
+ padding-left: var(--ui-space-5);
103
+ border-bottom: 1px solid var(--ui-border-subtle);
104
+ background: var(--modal-bar-bg);
105
+ flex: 0 0 auto;
106
+ }
107
+
108
+ .title {
109
+ margin: 0;
110
+ font-size: var(--ui-heading-2);
111
+ font-weight: 600;
112
+ line-height: var(--ui-line-height-tight);
113
+ color: var(--ui-foreground);
114
+ }
115
+
116
+ .subHeader {
117
+ flex: 0 0 auto;
118
+ border-bottom: 1px solid var(--ui-border-subtle);
119
+ }
120
+
121
+ .body {
122
+ flex: 1 1 auto;
123
+ overflow-y: auto;
124
+ display: flex;
125
+ flex-direction: column;
126
+ }
127
+
128
+ .bodyPadded {
129
+ padding: var(--ui-space-5);
130
+ gap: var(--ui-space-3);
131
+ }
132
+
133
+ .footer {
134
+ display: flex;
135
+ justify-content: flex-end;
136
+ gap: var(--ui-space-2);
137
+ padding: var(--ui-space-3);
138
+ border-top: 1px solid var(--ui-border-subtle);
139
+ background: var(--modal-bar-bg);
140
+ flex: 0 0 auto;
141
+ }
@@ -0,0 +1,59 @@
1
+ import { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { TabBar } from './index';
4
+
5
+ const meta: Meta<typeof TabBar> = {
6
+ title: 'Navigation/TabBar',
7
+ component: TabBar,
8
+ };
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof TabBar>;
12
+
13
+ const sections = [
14
+ { id: 'tasks', label: 'Tasks' },
15
+ { id: 'workspaces', label: 'Workspaces' },
16
+ { id: 'linear', label: 'Linear' },
17
+ { id: 'settings', label: 'Settings' },
18
+ ];
19
+
20
+ export const Default: Story = {
21
+ render: () => {
22
+ const [active, setActive] = useState('workspaces');
23
+
24
+ return (
25
+ <TabBar
26
+ ariaLabel="Top-level sections"
27
+ items={sections.map(({ id, label }) => ({
28
+ id,
29
+ label,
30
+ href: `/${id}`,
31
+ isActive: active === id,
32
+ onClick: (e) => {
33
+ e.preventDefault();
34
+ setActive(id);
35
+ },
36
+ }))}
37
+ />
38
+ );
39
+ },
40
+ };
41
+
42
+ export const RouterIntegration: Story = {
43
+ render: () => (
44
+ <div style={{ display: 'grid', gap: 12 }}>
45
+ <p style={{ margin: 0, fontSize: 12, color: '#666' }}>
46
+ In a real app, compute <code>isActive</code> from <code>useLocation()</code>{' '}
47
+ and call <code>navigate(href)</code> inside <code>onClick</code> (after{' '}
48
+ <code>e.preventDefault()</code>).
49
+ </p>
50
+ <TabBar
51
+ items={sections.map((s) => ({
52
+ ...s,
53
+ href: `/${s.id}`,
54
+ isActive: s.id === 'tasks',
55
+ }))}
56
+ />
57
+ </div>
58
+ ),
59
+ };
@@ -0,0 +1,25 @@
1
+ import { cx } from '../../../utils';
2
+ import type { TabBarProps } from './types';
3
+ import styles from './styles.module.css';
4
+
5
+ export const TabBar = (props: TabBarProps) => {
6
+ const { items, ariaLabel = 'Sections' } = props;
7
+
8
+ return (
9
+ <nav className={styles.tabBar} aria-label={ariaLabel}>
10
+ {items.map(({ id, label, href, isActive, onClick }) => (
11
+ <a
12
+ key={id}
13
+ href={href}
14
+ onClick={onClick}
15
+ aria-current={isActive ? 'page' : undefined}
16
+ className={cx(styles.tab, isActive && styles.tabActive)}
17
+ >
18
+ {label}
19
+ </a>
20
+ ))}
21
+ </nav>
22
+ );
23
+ };
24
+
25
+ export type { TabBarProps, TabBarItem } from './types';
@@ -0,0 +1,32 @@
1
+ .tabBar {
2
+ display: flex;
3
+ gap: 2px;
4
+ }
5
+
6
+ .tab {
7
+ padding: 3px 10px;
8
+ border: 1px solid transparent;
9
+ border-radius: var(--ui-radius);
10
+ background-color: transparent;
11
+ color: var(--ui-muted);
12
+ font-size: var(--ui-text-small);
13
+ font-family: var(--ui-font);
14
+ /* Weight is constant across states. State change is carried by color +
15
+ background + border, not weight — a weight change would nudge the
16
+ baseline and shift surrounding layout. */
17
+ font-weight: 500;
18
+ text-decoration: none;
19
+ cursor: pointer;
20
+ }
21
+
22
+ .tab:hover {
23
+ color: var(--ui-foreground);
24
+ background-color: var(--ui-background-1);
25
+ }
26
+
27
+ .tabActive,
28
+ .tabActive:hover {
29
+ color: var(--ui-foreground);
30
+ background-color: var(--ui-background-0);
31
+ border-color: var(--ui-border);
32
+ }
@@ -0,0 +1,22 @@
1
+ import type { MouseEvent } from 'react';
2
+
3
+ export type TabBarItem = {
4
+ id: string;
5
+ label: string;
6
+ href: string;
7
+ isActive?: boolean;
8
+ onClick?: (e: MouseEvent<HTMLAnchorElement>) => void;
9
+ };
10
+
11
+ /**
12
+ * Horizontal row of navigation tabs. Router-agnostic: each item carries
13
+ * `href`, `isActive` (consumer computes from router location), and an
14
+ * optional `onClick` for SPA-style navigation (the consumer calls
15
+ * `e.preventDefault()` + `navigate(href)` there).
16
+ *
17
+ * Renders a <nav> landmark; pass `ariaLabel` to name it.
18
+ */
19
+ export type TabBarProps = {
20
+ items: TabBarItem[];
21
+ ariaLabel?: string;
22
+ };
@@ -0,0 +1,41 @@
1
+ import { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { VerticalNav } from './index';
4
+
5
+ const meta: Meta<typeof VerticalNav> = {
6
+ title: 'Navigation/VerticalNav',
7
+ component: VerticalNav,
8
+ };
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof VerticalNav>;
12
+
13
+ const sections = [
14
+ { id: 'prompts', label: 'System prompts' },
15
+ { id: 'config', label: 'Config' },
16
+ { id: 'field-notes', label: 'Field notes' },
17
+ ];
18
+
19
+ export const Default: Story = {
20
+ render: () => {
21
+ const [active, setActive] = useState('prompts');
22
+
23
+ return (
24
+ <div style={{ width: '14rem', border: '1px solid #ccc' }}>
25
+ <VerticalNav
26
+ ariaLabel="Settings sections"
27
+ items={sections.map(({ id, label }) => ({
28
+ id,
29
+ label,
30
+ href: `/settings/${id}`,
31
+ isActive: active === id,
32
+ onClick: (e) => {
33
+ e.preventDefault();
34
+ setActive(id);
35
+ },
36
+ }))}
37
+ />
38
+ </div>
39
+ );
40
+ },
41
+ };
@@ -0,0 +1,25 @@
1
+ import { cx } from '../../../utils';
2
+ import type { VerticalNavProps } from './types';
3
+ import styles from './styles.module.css';
4
+
5
+ export const VerticalNav = (props: VerticalNavProps) => {
6
+ const { items, ariaLabel = 'Sections' } = props;
7
+
8
+ return (
9
+ <nav className={styles.nav} aria-label={ariaLabel}>
10
+ {items.map(({ id, label, href, isActive, onClick }) => (
11
+ <a
12
+ key={id}
13
+ href={href}
14
+ onClick={onClick}
15
+ aria-current={isActive ? 'page' : undefined}
16
+ className={cx(styles.item, isActive && styles.itemActive)}
17
+ >
18
+ {label}
19
+ </a>
20
+ ))}
21
+ </nav>
22
+ );
23
+ };
24
+
25
+ export type { VerticalNavProps, VerticalNavItem } from './types';
@@ -0,0 +1,28 @@
1
+ .nav {
2
+ display: flex;
3
+ flex-direction: column;
4
+ padding: var(--ui-space-2);
5
+ gap: 2px;
6
+ }
7
+
8
+ .item {
9
+ padding: var(--ui-space-2) var(--ui-space-3);
10
+ border-radius: var(--ui-radius);
11
+ color: var(--ui-muted);
12
+ font-size: var(--ui-text-small);
13
+ font-weight: 500;
14
+ font-family: var(--ui-font);
15
+ text-decoration: none;
16
+ cursor: pointer;
17
+ }
18
+
19
+ .item:hover {
20
+ color: var(--ui-foreground);
21
+ background-color: var(--ui-background-1);
22
+ }
23
+
24
+ .itemActive,
25
+ .itemActive:hover {
26
+ color: var(--ui-foreground);
27
+ background-color: var(--ui-background-3);
28
+ }
@@ -0,0 +1,19 @@
1
+ import type { MouseEvent } from 'react';
2
+
3
+ export type VerticalNavItem = {
4
+ id: string;
5
+ label: string;
6
+ href: string;
7
+ isActive?: boolean;
8
+ onClick?: (e: MouseEvent<HTMLAnchorElement>) => void;
9
+ };
10
+
11
+ /**
12
+ * Vertical sidebar navigation. Same contract as `TabBar` but stacked and
13
+ * styled for sidebar contexts: larger click targets, no border on the
14
+ * active state, slightly deeper active background.
15
+ */
16
+ export type VerticalNavProps = {
17
+ items: VerticalNavItem[];
18
+ ariaLabel?: string;
19
+ };
@@ -0,0 +1,154 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { useRef, useState } from 'react';
3
+ import { Popover } from './index';
4
+ import { Button } from '../../Forms/Button';
5
+ import { Input } from '../../Forms/Input';
6
+ import { Field } from '../../Forms/Field';
7
+ import { Text } from '../../Content/Text';
8
+ const meta: Meta<typeof Popover> = {
9
+ title: 'Overlays/Popover',
10
+ component: Popover,
11
+ };
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof Popover>;
15
+
16
+ export const InlineEdit: Story = {
17
+ render: () => {
18
+ const anchorRef = useRef<HTMLButtonElement>(null);
19
+ const [isOpen, setIsOpen] = useState(false);
20
+ const [amount, setAmount] = useState('150');
21
+ const [draft, setDraft] = useState(amount);
22
+
23
+ const open = () => {
24
+ setDraft(amount);
25
+ setIsOpen(true);
26
+ };
27
+ const save = () => {
28
+ setAmount(draft);
29
+ setIsOpen(false);
30
+ };
31
+
32
+ return (
33
+ <div style={{ padding: 64 }}>
34
+ <Text size="small" isMuted>
35
+ Lunch budget: ${amount}
36
+ </Text>
37
+ <div style={{ marginTop: 8 }}>
38
+ <Button ref={anchorRef} size="small" onClick={open}>
39
+ Edit amount…
40
+ </Button>
41
+ </div>
42
+
43
+ <Popover
44
+ anchorRef={anchorRef}
45
+ isOpen={isOpen}
46
+ onClose={() => setIsOpen(false)}
47
+ title="Edit amount"
48
+ actions={
49
+ <>
50
+ <Button variant="ghost" size="small" onClick={() => setIsOpen(false)}>
51
+ Cancel
52
+ </Button>
53
+ <Button variant="primary" size="small" onClick={save}>
54
+ Save
55
+ </Button>
56
+ </>
57
+ }
58
+ >
59
+ <Field label="Amount (USD)">
60
+ <Input
61
+ value={draft}
62
+ onChange={(e) => setDraft(e.currentTarget.value)}
63
+ autoFocus
64
+ />
65
+ </Field>
66
+ </Popover>
67
+ </div>
68
+ );
69
+ },
70
+ };
71
+
72
+ export const NoChrome: Story = {
73
+ render: () => {
74
+ const anchorRef = useRef<HTMLButtonElement>(null);
75
+ const [isOpen, setIsOpen] = useState(false);
76
+
77
+ return (
78
+ <div style={{ padding: 64 }}>
79
+ <Button
80
+ ref={anchorRef}
81
+ size="small"
82
+ onClick={() => setIsOpen((v) => !v)}
83
+ >
84
+ Info
85
+ </Button>
86
+ <Popover
87
+ anchorRef={anchorRef}
88
+ isOpen={isOpen}
89
+ onClose={() => setIsOpen(false)}
90
+ ariaLabel="Info"
91
+ >
92
+ <Text size="small">
93
+ No title, no actions — a bare popover for inline tooltips or quick
94
+ notes. Dismisses on outside-click or Escape.
95
+ </Text>
96
+ </Popover>
97
+ </div>
98
+ );
99
+ },
100
+ };
101
+
102
+ // Placement clamping + animation origin: each corner trigger should grow
103
+ // its panel from the corner closest to the trigger.
104
+ export const Corners: Story = {
105
+ parameters: { layout: 'fullscreen' },
106
+ render: () => (
107
+ <div style={{ position: 'relative', height: '100vh', width: '100%' }}>
108
+ <CornerPopover corner="top-left" label="⋯ TL" />
109
+ <CornerPopover corner="top-right" label="⋯ TR" />
110
+ <CornerPopover corner="bottom-left" label="⋯ BL" />
111
+ <CornerPopover corner="bottom-right" label="⋯ BR" />
112
+ </div>
113
+ ),
114
+ };
115
+
116
+ type Corner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
117
+
118
+ const cornerStyle = (corner: Corner): React.CSSProperties => ({
119
+ position: 'absolute',
120
+ ...(corner.startsWith('top') ? { top: 16 } : { bottom: 16 }),
121
+ ...(corner.endsWith('left') ? { left: 16 } : { right: 16 }),
122
+ });
123
+
124
+ const CornerPopover = ({ corner, label }: { corner: Corner; label: string }) => {
125
+ const anchorRef = useRef<HTMLButtonElement>(null);
126
+ const [isOpen, setIsOpen] = useState(false);
127
+ return (
128
+ <div style={cornerStyle(corner)}>
129
+ <Button
130
+ ref={anchorRef}
131
+ size="small"
132
+ onClick={() => setIsOpen((v) => !v)}
133
+ >
134
+ {label}
135
+ </Button>
136
+ <Popover
137
+ anchorRef={anchorRef}
138
+ isOpen={isOpen}
139
+ onClose={() => setIsOpen(false)}
140
+ title={`Anchored: ${corner}`}
141
+ actions={
142
+ <Button variant="primary" size="small" onClick={() => setIsOpen(false)}>
143
+ Done
144
+ </Button>
145
+ }
146
+ >
147
+ <Text size="small">
148
+ Panel opens from the corner nearest the trigger. Placement auto-flips
149
+ to stay on-screen.
150
+ </Text>
151
+ </Popover>
152
+ </div>
153
+ );
154
+ };