@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,55 @@
1
+ import { cx } from '../../../utils';
2
+ import { Tooltip } from '../../Overlays/Tooltip';
3
+ import type {
4
+ IconButtonProps,
5
+ IconButtonSize,
6
+ IconButtonVariant,
7
+ } from './types';
8
+ import styles from './styles.module.css';
9
+
10
+ const VARIANT_MAP: Record<IconButtonVariant, string> = {
11
+ ghost: styles.ghost,
12
+ solid: styles.solid,
13
+ danger: styles.danger,
14
+ };
15
+
16
+ const SIZE_MAP: Record<IconButtonSize, string> = {
17
+ small: styles.small,
18
+ medium: styles.medium,
19
+ };
20
+
21
+ export const IconButton = (props: IconButtonProps) => {
22
+ const {
23
+ children,
24
+ ariaLabel,
25
+ tooltip,
26
+ variant = 'ghost',
27
+ size = 'medium',
28
+ type = 'button',
29
+ onClick,
30
+ disabled,
31
+ autoFocus,
32
+ ref,
33
+ } = props;
34
+
35
+ const button = (
36
+ <button
37
+ ref={ref}
38
+ type={type}
39
+ aria-label={ariaLabel}
40
+ className={cx(styles.button, VARIANT_MAP[variant], SIZE_MAP[size])}
41
+ onClick={onClick}
42
+ disabled={disabled}
43
+ autoFocus={autoFocus}
44
+ >
45
+ {children}
46
+ </button>
47
+ );
48
+
49
+ if (tooltip) {
50
+ return <Tooltip label={tooltip}>{button}</Tooltip>;
51
+ }
52
+ return button;
53
+ };
54
+
55
+ export type { IconButtonProps, IconButtonVariant, IconButtonSize };
@@ -0,0 +1,61 @@
1
+ .button {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ padding: 0;
6
+ border: 1px solid transparent;
7
+ border-radius: var(--ui-radius);
8
+ cursor: pointer;
9
+ color: var(--ui-foreground);
10
+ }
11
+
12
+ .button:disabled {
13
+ opacity: 0.5;
14
+ cursor: not-allowed;
15
+ }
16
+
17
+ .button:focus-visible {
18
+ outline: 2px solid var(--ui-accent);
19
+ outline-offset: 1px;
20
+ }
21
+
22
+ .small {
23
+ width: 24px;
24
+ height: 24px;
25
+ }
26
+
27
+ .medium {
28
+ width: 32px;
29
+ height: 32px;
30
+ }
31
+
32
+ .ghost {
33
+ background-color: transparent;
34
+ border-color: transparent;
35
+ color: var(--ui-muted);
36
+ }
37
+
38
+ .ghost:hover:not(:disabled) {
39
+ background-color: var(--ui-background-3);
40
+ color: var(--ui-foreground);
41
+ }
42
+
43
+ .solid {
44
+ background-color: var(--ui-background-0);
45
+ border-color: var(--ui-border);
46
+ color: var(--ui-foreground);
47
+ }
48
+
49
+ .solid:hover:not(:disabled) {
50
+ background-color: var(--ui-background-1);
51
+ }
52
+
53
+ .danger {
54
+ background-color: transparent;
55
+ border-color: transparent;
56
+ color: var(--ui-danger);
57
+ }
58
+
59
+ .danger:hover:not(:disabled) {
60
+ background-color: var(--ui-background-3);
61
+ }
@@ -0,0 +1,23 @@
1
+ import type { MouseEventHandler, ReactNode, Ref } from 'react';
2
+
3
+ export type IconButtonVariant = 'ghost' | 'solid' | 'danger';
4
+ export type IconButtonSize = 'small' | 'medium';
5
+ export type IconButtonType = 'button' | 'submit' | 'reset';
6
+
7
+ export type IconButtonProps = {
8
+ /** The icon to render. Typically an inline SVG sized to ~16px. */
9
+ children: ReactNode;
10
+ /** Required. Icon-only buttons have no text, so an accessible name must
11
+ * be supplied explicitly. */
12
+ ariaLabel: string;
13
+ /** Optional tooltip text shown on hover/focus. Independent of `ariaLabel`
14
+ * so screen-reader text and visual hint can differ. */
15
+ tooltip?: string;
16
+ variant?: IconButtonVariant;
17
+ size?: IconButtonSize;
18
+ type?: IconButtonType;
19
+ onClick?: MouseEventHandler<HTMLButtonElement>;
20
+ disabled?: boolean;
21
+ autoFocus?: boolean;
22
+ ref?: Ref<HTMLButtonElement>;
23
+ };
@@ -0,0 +1,22 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Input } from './index';
3
+
4
+ const meta: Meta<typeof Input> = {
5
+ title: 'Forms/Input',
6
+ component: Input,
7
+ argTypes: {
8
+ size: { control: 'inline-radio', options: ['small', 'medium'] },
9
+ },
10
+ args: { placeholder: 'Enter text...', size: 'medium' },
11
+ };
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof Input>;
15
+
16
+ export const Default: Story = {};
17
+
18
+ export const Small: Story = { args: { size: 'small' } };
19
+
20
+ export const Disabled: Story = {
21
+ args: { disabled: true, defaultValue: 'Some value' },
22
+ };
@@ -0,0 +1,42 @@
1
+ import type { InputProps, InputSize } from './types';
2
+ import { cx } from '../../../utils';
3
+ import styles from './styles.module.css';
4
+
5
+ const SIZE_MAP: Record<InputSize, string> = {
6
+ small: styles.small,
7
+ medium: styles.medium,
8
+ };
9
+
10
+ export const Input = (props: InputProps) => {
11
+ const {
12
+ type = 'text',
13
+ size = 'medium',
14
+ value,
15
+ defaultValue,
16
+ onChange,
17
+ placeholder,
18
+ required,
19
+ autoFocus,
20
+ disabled,
21
+ id,
22
+ name,
23
+ } = props;
24
+
25
+ return (
26
+ <input
27
+ type={type}
28
+ className={cx(styles.input, SIZE_MAP[size])}
29
+ value={value}
30
+ defaultValue={defaultValue}
31
+ onChange={onChange}
32
+ placeholder={placeholder}
33
+ required={required}
34
+ autoFocus={autoFocus}
35
+ disabled={disabled}
36
+ id={id}
37
+ name={name}
38
+ />
39
+ );
40
+ };
41
+
42
+ export type { InputProps };
@@ -0,0 +1,30 @@
1
+ .input {
2
+ font-family: var(--ui-font);
3
+ border: 1px solid var(--ui-border);
4
+ border-radius: var(--ui-radius);
5
+ background-color: var(--ui-background-0);
6
+ color: var(--ui-foreground);
7
+ width: 100%;
8
+ line-height: var(--ui-line-height);
9
+ }
10
+
11
+ .input:focus {
12
+ outline: 2px solid var(--ui-accent);
13
+ outline-offset: -1px;
14
+ border-color: var(--ui-accent);
15
+ }
16
+
17
+ .input:disabled {
18
+ opacity: 0.5;
19
+ cursor: not-allowed;
20
+ }
21
+
22
+ .small {
23
+ font-size: var(--ui-text-small);
24
+ padding: 4px 8px;
25
+ }
26
+
27
+ .medium {
28
+ font-size: var(--ui-text-medium);
29
+ padding: 6px 10px;
30
+ }
@@ -0,0 +1,18 @@
1
+ import type { ChangeEventHandler } from 'react';
2
+
3
+ export type InputSize = 'small' | 'medium';
4
+ export type InputType = 'text' | 'email' | 'password';
5
+
6
+ export type InputProps = {
7
+ type?: InputType;
8
+ size?: InputSize;
9
+ value?: string;
10
+ defaultValue?: string;
11
+ onChange?: ChangeEventHandler<HTMLInputElement>;
12
+ placeholder?: string;
13
+ required?: boolean;
14
+ autoFocus?: boolean;
15
+ disabled?: boolean;
16
+ id?: string;
17
+ name?: string;
18
+ };
@@ -0,0 +1,41 @@
1
+ import type { SearchInputProps, SearchInputSize } from './types';
2
+ import { cx } from '../../../utils';
3
+ import styles from './styles.module.css';
4
+
5
+ const SIZE_MAP: Record<SearchInputSize, string> = {
6
+ small: styles.small,
7
+ medium: styles.medium,
8
+ };
9
+
10
+ export const SearchInput = (props: SearchInputProps) => {
11
+ const {
12
+ label,
13
+ size = 'medium',
14
+ value,
15
+ defaultValue,
16
+ onChange,
17
+ placeholder,
18
+ disabled,
19
+ autoFocus,
20
+ id,
21
+ name,
22
+ } = props;
23
+
24
+ return (
25
+ <input
26
+ type="search"
27
+ aria-label={label}
28
+ className={cx(styles.input, SIZE_MAP[size])}
29
+ value={value}
30
+ defaultValue={defaultValue}
31
+ onChange={onChange}
32
+ placeholder={placeholder}
33
+ disabled={disabled}
34
+ autoFocus={autoFocus}
35
+ id={id}
36
+ name={name}
37
+ />
38
+ );
39
+ };
40
+
41
+ export type { SearchInputProps };
@@ -0,0 +1,30 @@
1
+ .input {
2
+ font-family: var(--ui-font);
3
+ border: 1px solid var(--ui-border);
4
+ border-radius: var(--ui-radius);
5
+ background-color: var(--ui-background-0);
6
+ color: var(--ui-foreground);
7
+ line-height: var(--ui-line-height);
8
+ width: 100%;
9
+ }
10
+
11
+ .input:focus {
12
+ outline: 2px solid var(--ui-accent);
13
+ outline-offset: -1px;
14
+ border-color: var(--ui-accent);
15
+ }
16
+
17
+ .input:disabled {
18
+ opacity: 0.5;
19
+ cursor: not-allowed;
20
+ }
21
+
22
+ .small {
23
+ font-size: var(--ui-text-small);
24
+ padding: 4px 8px;
25
+ }
26
+
27
+ .medium {
28
+ font-size: var(--ui-text-medium);
29
+ padding: 6px 10px;
30
+ }
@@ -0,0 +1,17 @@
1
+ import type { ChangeEventHandler } from 'react';
2
+
3
+ export type SearchInputSize = 'small' | 'medium';
4
+
5
+ export type SearchInputProps = {
6
+ /** Accessible name for the unlabeled search input. Becomes the aria-label. */
7
+ label: string;
8
+ size?: SearchInputSize;
9
+ value?: string;
10
+ defaultValue?: string;
11
+ onChange?: ChangeEventHandler<HTMLInputElement>;
12
+ placeholder?: string;
13
+ disabled?: boolean;
14
+ autoFocus?: boolean;
15
+ id?: string;
16
+ name?: string;
17
+ };
@@ -0,0 +1,116 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { useState } from 'react';
3
+ import { MultiSelect } from './index';
4
+ import { Field } from '../../Field';
5
+ import { Stack } from '../../../Layout/Stack';
6
+ import { Text } from '../../../Content/Text';
7
+
8
+ const meta: Meta<typeof MultiSelect> = {
9
+ title: 'Forms/MultiSelect',
10
+ component: MultiSelect,
11
+ };
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof MultiSelect>;
15
+
16
+ type Tag = 'design' | 'engineering' | 'product' | 'ops' | 'legal';
17
+
18
+ const TAG_OPTIONS: { value: Tag; label: string }[] = [
19
+ { value: 'design', label: 'Design' },
20
+ { value: 'engineering', label: 'Engineering' },
21
+ { value: 'product', label: 'Product' },
22
+ { value: 'ops', label: 'Ops' },
23
+ { value: 'legal', label: 'Legal' },
24
+ ];
25
+
26
+ export const Basic: Story = {
27
+ render: () => {
28
+ const [values, setValues] = useState<Tag[]>(['engineering']);
29
+ return (
30
+ <div style={{ padding: 32, maxWidth: 360 }}>
31
+ <Field label="Tags">
32
+ <MultiSelect<Tag>
33
+ options={TAG_OPTIONS}
34
+ values={values}
35
+ onChange={setValues}
36
+ placeholder="Pick some tags…"
37
+ />
38
+ </Field>
39
+ <div style={{ marginTop: 16 }}>
40
+ <Text size="small" isMuted>
41
+ Selected: {values.join(', ') || '—'}
42
+ </Text>
43
+ </div>
44
+ </div>
45
+ );
46
+ },
47
+ };
48
+
49
+ export const Empty: Story = {
50
+ render: () => {
51
+ const [values, setValues] = useState<Tag[]>([]);
52
+ return (
53
+ <div style={{ padding: 32, maxWidth: 360 }}>
54
+ <MultiSelect<Tag>
55
+ options={TAG_OPTIONS}
56
+ values={values}
57
+ onChange={setValues}
58
+ placeholder="Pick some tags…"
59
+ />
60
+ </div>
61
+ );
62
+ },
63
+ };
64
+
65
+ export const CustomSummary: Story = {
66
+ render: () => {
67
+ const [values, setValues] = useState<Tag[]>(['design', 'product']);
68
+ return (
69
+ <div style={{ padding: 32, maxWidth: 360 }}>
70
+ <Stack direction="column" gap={2}>
71
+ <Text size="small" isMuted>
72
+ `renderSummary` replaces the "N selected" default.
73
+ </Text>
74
+ <MultiSelect<Tag>
75
+ options={TAG_OPTIONS}
76
+ values={values}
77
+ onChange={setValues}
78
+ renderSummary={(selected) =>
79
+ selected.map((s) => s.label).join(', ')
80
+ }
81
+ />
82
+ </Stack>
83
+ </div>
84
+ );
85
+ },
86
+ };
87
+
88
+ export const Small: Story = {
89
+ render: () => {
90
+ const [values, setValues] = useState<Tag[]>([]);
91
+ return (
92
+ <div style={{ padding: 32, maxWidth: 240 }}>
93
+ <MultiSelect<Tag>
94
+ options={TAG_OPTIONS}
95
+ values={values}
96
+ onChange={setValues}
97
+ size="small"
98
+ placeholder="Tags…"
99
+ />
100
+ </div>
101
+ );
102
+ },
103
+ };
104
+
105
+ export const Disabled: Story = {
106
+ render: () => (
107
+ <div style={{ padding: 32, maxWidth: 360 }}>
108
+ <MultiSelect<Tag>
109
+ options={TAG_OPTIONS}
110
+ values={['design']}
111
+ onChange={() => {}}
112
+ isDisabled
113
+ />
114
+ </div>
115
+ ),
116
+ };
@@ -0,0 +1,74 @@
1
+ import { useRef, useState } from 'react';
2
+ import { Popover } from '../../../Overlays/Popover';
3
+ import { OptionList } from '../internal/OptionList';
4
+ import { SelectTrigger } from '../internal/SelectTrigger';
5
+ import type { MultiSelectProps } from './types';
6
+
7
+ const defaultSummary = <T extends string>(selected: { value: T; label: string }[]) =>
8
+ selected.length === 1 ? selected[0]!.label : `${selected.length} selected`;
9
+
10
+ export const MultiSelect = <T extends string,>({
11
+ options,
12
+ values,
13
+ onChange,
14
+ size = 'medium',
15
+ isDisabled,
16
+ placeholder = 'Select…',
17
+ renderSummary = defaultSummary,
18
+ ariaLabel,
19
+ }: MultiSelectProps<T>) => {
20
+ const triggerRef = useRef<HTMLButtonElement>(null);
21
+ const [isOpen, setIsOpen] = useState(false);
22
+
23
+ const selectedSet = new Set(values);
24
+ const selectedOptions = options.filter((o) => selectedSet.has(o.value));
25
+ const hasSelection = selectedOptions.length > 0;
26
+ const firstSelectedIndex = hasSelection
27
+ ? options.findIndex((o) => selectedSet.has(o.value))
28
+ : 0;
29
+
30
+ const close = () => setIsOpen(false);
31
+ const toggle = () => setIsOpen((v) => !v);
32
+ const commitToggle = (next: T) => {
33
+ const isCurrentlySelected = selectedSet.has(next);
34
+ const nextValues = isCurrentlySelected
35
+ ? values.filter((v) => v !== next)
36
+ : [...values, next];
37
+ onChange(nextValues);
38
+ };
39
+
40
+ return (
41
+ <>
42
+ <SelectTrigger
43
+ ref={triggerRef}
44
+ size={size}
45
+ isOpen={isOpen}
46
+ isDisabled={isDisabled}
47
+ isPlaceholder={!hasSelection}
48
+ onToggle={toggle}
49
+ ariaLabel={ariaLabel}
50
+ >
51
+ {hasSelection ? renderSummary(selectedOptions) : placeholder}
52
+ </SelectTrigger>
53
+ <Popover
54
+ anchorRef={triggerRef}
55
+ isOpen={isOpen}
56
+ onClose={close}
57
+ ariaLabel={ariaLabel ?? 'Select options'}
58
+ matchAnchorWidth
59
+ hasBodyPadding={false}
60
+ >
61
+ <OptionList<T>
62
+ options={options}
63
+ isSelected={(v) => selectedSet.has(v)}
64
+ onCommit={commitToggle}
65
+ initialFocusIndex={firstSelectedIndex}
66
+ indicator="checkbox"
67
+ ariaMultiSelectable
68
+ />
69
+ </Popover>
70
+ </>
71
+ );
72
+ };
73
+
74
+ export type { MultiSelectProps };
@@ -0,0 +1,15 @@
1
+ import type { SelectOption } from '../internal/OptionList';
2
+ import type { SelectSize } from '../internal/SelectTrigger';
3
+
4
+ export type MultiSelectProps<T extends string = string> = {
5
+ options: SelectOption<T>[];
6
+ values: T[];
7
+ onChange: (values: T[]) => void;
8
+ size?: SelectSize;
9
+ isDisabled?: boolean;
10
+ /** Displayed when `values` is empty. */
11
+ placeholder?: string;
12
+ /** Custom summary builder when selections exist. Defaults to "N selected". */
13
+ renderSummary?: (selected: SelectOption<T>[]) => string;
14
+ ariaLabel?: string;
15
+ };