@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.
- package/.storybook/main.ts +9 -0
- package/.storybook/manager.ts +13 -0
- package/.storybook/preview.tsx +18 -0
- package/CLAUDE.md +30 -0
- package/LICENSE +21 -0
- package/README.md +7 -0
- package/bun.lock +947 -0
- package/bunfig.toml +2 -0
- package/eslint.config.mjs +106 -0
- package/index.ts +1 -0
- package/package.json +50 -0
- package/src/components/Chat/ChatComposer/ChatComposer.stories.tsx +68 -0
- package/src/components/Chat/ChatComposer/index.tsx +74 -0
- package/src/components/Chat/ChatComposer/styles.module.css +88 -0
- package/src/components/Chat/ChatComposer/types.ts +11 -0
- package/src/components/Chat/ChatMessage/ChatMessage.stories.tsx +111 -0
- package/src/components/Chat/ChatMessage/index.tsx +42 -0
- package/src/components/Chat/ChatMessage/styles.module.css +58 -0
- package/src/components/Chat/ChatMessage/types.ts +14 -0
- package/src/components/Chat/ChatRecipientsHeader/ChatRecipientsHeader.stories.tsx +145 -0
- package/src/components/Chat/ChatRecipientsHeader/index.tsx +29 -0
- package/src/components/Chat/ChatRecipientsHeader/styles.module.css +48 -0
- package/src/components/Chat/ChatRecipientsHeader/types.ts +26 -0
- package/src/components/Chat/ChatShell/ChatShell.stories.tsx +203 -0
- package/src/components/Chat/ChatShell/index.tsx +16 -0
- package/src/components/Chat/ChatShell/styles.module.css +27 -0
- package/src/components/Chat/ChatShell/types.ts +7 -0
- package/src/components/Chat/PillCombobox/PillCombobox.stories.tsx +59 -0
- package/src/components/Chat/PillCombobox/index.tsx +17 -0
- package/src/components/Chat/PillCombobox/styles.module.css +29 -0
- package/src/components/Chat/PillCombobox/types.ts +28 -0
- package/src/components/Chat/PillComboboxCore/Core.tsx +235 -0
- package/src/components/Chat/PillComboboxCore/styles.module.css +79 -0
- package/src/components/Chat/index.ts +12 -0
- package/src/components/Content/Badge/Badge.stories.tsx +31 -0
- package/src/components/Content/Badge/index.tsx +22 -0
- package/src/components/Content/Badge/styles.module.css +25 -0
- package/src/components/Content/Badge/types.ts +7 -0
- package/src/components/Content/Card/Card.stories.tsx +24 -0
- package/src/components/Content/Card/index.tsx +21 -0
- package/src/components/Content/Card/styles.module.css +13 -0
- package/src/components/Content/Card/types.ts +8 -0
- package/src/components/Content/EditableMarkdown/EditableMarkdown.stories.tsx +58 -0
- package/src/components/Content/EditableMarkdown/index.tsx +140 -0
- package/src/components/Content/EditableMarkdown/styles.module.css +221 -0
- package/src/components/Content/EditableMarkdown/types.ts +11 -0
- package/src/components/Content/Heading/Heading.stories.tsx +26 -0
- package/src/components/Content/Heading/index.tsx +20 -0
- package/src/components/Content/Heading/styles.module.css +19 -0
- package/src/components/Content/Heading/types.ts +8 -0
- package/src/components/Content/Link/Link.stories.tsx +21 -0
- package/src/components/Content/Link/index.tsx +19 -0
- package/src/components/Content/Link/styles.module.css +11 -0
- package/src/components/Content/Link/types.ts +8 -0
- package/src/components/Content/List/List.stories.tsx +62 -0
- package/src/components/Content/List/index.tsx +26 -0
- package/src/components/Content/List/styles.module.css +41 -0
- package/src/components/Content/List/types.ts +33 -0
- package/src/components/Content/LoadingContainer/LoadingContainer.stories.tsx +105 -0
- package/src/components/Content/LoadingContainer/index.tsx +36 -0
- package/src/components/Content/LoadingContainer/styles.module.css +54 -0
- package/src/components/Content/LoadingContainer/types.ts +8 -0
- package/src/components/Content/Markdown/Markdown.stories.tsx +39 -0
- package/src/components/Content/Markdown/index.tsx +28 -0
- package/src/components/Content/Markdown/styles.module.css +79 -0
- package/src/components/Content/Markdown/types.ts +8 -0
- package/src/components/Content/Menu/Menu.stories.tsx +186 -0
- package/src/components/Content/Menu/index.tsx +259 -0
- package/src/components/Content/Menu/styles.module.css +103 -0
- package/src/components/Content/Menu/types.ts +25 -0
- package/src/components/Content/Text/Text.stories.tsx +36 -0
- package/src/components/Content/Text/index.tsx +35 -0
- package/src/components/Content/Text/styles.module.css +30 -0
- package/src/components/Content/Text/types.ts +11 -0
- package/src/components/Forms/Button/Button.stories.tsx +40 -0
- package/src/components/Forms/Button/index.tsx +43 -0
- package/src/components/Forms/Button/styles.module.css +67 -0
- package/src/components/Forms/Button/types.ts +16 -0
- package/src/components/Forms/ColorInput/index.tsx +22 -0
- package/src/components/Forms/ColorInput/styles.module.css +19 -0
- package/src/components/Forms/ColorInput/types.ts +12 -0
- package/src/components/Forms/Field/Field.stories.tsx +35 -0
- package/src/components/Forms/Field/index.tsx +17 -0
- package/src/components/Forms/Field/styles.module.css +21 -0
- package/src/components/Forms/Field/types.ts +9 -0
- package/src/components/Forms/IconButton/IconButton.stories.tsx +91 -0
- package/src/components/Forms/IconButton/index.tsx +55 -0
- package/src/components/Forms/IconButton/styles.module.css +61 -0
- package/src/components/Forms/IconButton/types.ts +23 -0
- package/src/components/Forms/Input/Input.stories.tsx +22 -0
- package/src/components/Forms/Input/index.tsx +42 -0
- package/src/components/Forms/Input/styles.module.css +30 -0
- package/src/components/Forms/Input/types.ts +18 -0
- package/src/components/Forms/SearchInput/index.tsx +41 -0
- package/src/components/Forms/SearchInput/styles.module.css +30 -0
- package/src/components/Forms/SearchInput/types.ts +17 -0
- package/src/components/Forms/Select/MultiSelect/MultiSelect.stories.tsx +116 -0
- package/src/components/Forms/Select/MultiSelect/index.tsx +74 -0
- package/src/components/Forms/Select/MultiSelect/types.ts +15 -0
- package/src/components/Forms/Select/SingleSelect/SingleSelect.stories.tsx +174 -0
- package/src/components/Forms/Select/SingleSelect/index.tsx +62 -0
- package/src/components/Forms/Select/SingleSelect/types.ts +12 -0
- package/src/components/Forms/Select/index.ts +4 -0
- package/src/components/Forms/Select/internal/OptionList.tsx +124 -0
- package/src/components/Forms/Select/internal/SelectTrigger.tsx +60 -0
- package/src/components/Forms/Select/internal/styles.module.css +122 -0
- package/src/components/Forms/Textarea/Textarea.stories.tsx +25 -0
- package/src/components/Forms/Textarea/index.tsx +48 -0
- package/src/components/Forms/Textarea/styles.module.css +34 -0
- package/src/components/Forms/Textarea/types.ts +24 -0
- package/src/components/Json/Json/Json.stories.tsx +33 -0
- package/src/components/Json/Json/index.tsx +38 -0
- package/src/components/Json/Json/types.ts +21 -0
- package/src/components/Json/JsonTable/JsonLeafNode.tsx +31 -0
- package/src/components/Json/JsonTable/JsonTable.stories.tsx +52 -0
- package/src/components/Json/JsonTable/index.tsx +33 -0
- package/src/components/Json/JsonTable/types.ts +13 -0
- package/src/components/Json/JsonTable/utils.ts +6 -0
- package/src/components/Layout/Bar/Bar.stories.tsx +100 -0
- package/src/components/Layout/Bar/index.tsx +17 -0
- package/src/components/Layout/Bar/styles.module.css +34 -0
- package/src/components/Layout/Bar/types.ts +10 -0
- package/src/components/Layout/Debug/Debug.stories.tsx +86 -0
- package/src/components/Layout/Debug/index.tsx +41 -0
- package/src/components/Layout/Debug/styles.module.css +13 -0
- package/src/components/Layout/Debug/types.ts +12 -0
- package/src/components/Layout/Divider/Divider.stories.tsx +22 -0
- package/src/components/Layout/Divider/index.tsx +3 -0
- package/src/components/Layout/Divider/styles.module.css +6 -0
- package/src/components/Layout/Grid/Grid.stories.tsx +28 -0
- package/src/components/Layout/Grid/index.tsx +29 -0
- package/src/components/Layout/Grid/styles.module.css +12 -0
- package/src/components/Layout/Grid/types.ts +9 -0
- package/src/components/Layout/Panels/Panels.stories.tsx +287 -0
- package/src/components/Layout/Panels/index.tsx +119 -0
- package/src/components/Layout/Panels/styles.module.css +103 -0
- package/src/components/Layout/Panels/types.ts +36 -0
- package/src/components/Layout/Stack/Stack.stories.tsx +45 -0
- package/src/components/Layout/Stack/index.tsx +73 -0
- package/src/components/Layout/Stack/styles.module.css +41 -0
- package/src/components/Layout/Stack/types.ts +17 -0
- package/src/components/Modals/ConfirmModal/ConfirmModal.stories.tsx +73 -0
- package/src/components/Modals/ConfirmModal/index.tsx +72 -0
- package/src/components/Modals/ConfirmModal/styles.module.css +62 -0
- package/src/components/Modals/ConfirmModal/types.ts +14 -0
- package/src/components/Modals/LargeModal/LargeModal.stories.tsx +75 -0
- package/src/components/Modals/LargeModal/index.tsx +9 -0
- package/src/components/Modals/LargeModal/styles.module.css +6 -0
- package/src/components/Modals/LargeModal/types.ts +18 -0
- package/src/components/Modals/MediumModal/MediumModal.stories.tsx +121 -0
- package/src/components/Modals/MediumModal/MediumModal.test.tsx +48 -0
- package/src/components/Modals/MediumModal/index.tsx +9 -0
- package/src/components/Modals/MediumModal/styles.module.css +5 -0
- package/src/components/Modals/MediumModal/types.ts +18 -0
- package/src/components/Modals/index.ts +3 -0
- package/src/components/Modals/internal/ModalBody.tsx +21 -0
- package/src/components/Modals/internal/ModalFooter.tsx +12 -0
- package/src/components/Modals/internal/ModalHeader.tsx +27 -0
- package/src/components/Modals/internal/ModalShell.tsx +112 -0
- package/src/components/Modals/internal/styles.module.css +141 -0
- package/src/components/Navigation/TabBar/TabBar.stories.tsx +59 -0
- package/src/components/Navigation/TabBar/index.tsx +25 -0
- package/src/components/Navigation/TabBar/styles.module.css +32 -0
- package/src/components/Navigation/TabBar/types.ts +22 -0
- package/src/components/Navigation/VerticalNav/VerticalNav.stories.tsx +41 -0
- package/src/components/Navigation/VerticalNav/index.tsx +25 -0
- package/src/components/Navigation/VerticalNav/styles.module.css +28 -0
- package/src/components/Navigation/VerticalNav/types.ts +19 -0
- package/src/components/Overlays/Popover/Popover.stories.tsx +154 -0
- package/src/components/Overlays/Popover/index.tsx +175 -0
- package/src/components/Overlays/Popover/styles.module.css +59 -0
- package/src/components/Overlays/Popover/types.ts +34 -0
- package/src/components/Overlays/Tooltip/Tooltip.stories.tsx +41 -0
- package/src/components/Overlays/Tooltip/index.tsx +115 -0
- package/src/components/Overlays/Tooltip/styles.module.css +25 -0
- package/src/components/Overlays/Tooltip/types.ts +15 -0
- package/src/components/Primitives/EmptyValue/EmptyValue.stories.tsx +18 -0
- package/src/components/Primitives/EmptyValue/index.tsx +3 -0
- package/src/components/Primitives/EmptyValue/styles.module.css +3 -0
- package/src/components/Primitives/LinedStack/LinedStack.stories.tsx +101 -0
- package/src/components/Primitives/LinedStack/index.tsx +41 -0
- package/src/components/Primitives/LinedStack/styles.module.css +27 -0
- package/src/components/Primitives/LinedStack/types.ts +49 -0
- package/src/components/Primitives/LongText/LongText.stories.tsx +72 -0
- package/src/components/Primitives/LongText/index.tsx +67 -0
- package/src/components/Primitives/LongText/styles.module.css +30 -0
- package/src/components/Primitives/LongText/types.ts +4 -0
- package/src/components/Primitives/Num/Num.stories.tsx +51 -0
- package/src/components/Primitives/Num/index.tsx +37 -0
- package/src/components/Primitives/Num/types.ts +19 -0
- package/src/components/Primitives/Percent/Percent.stories.tsx +48 -0
- package/src/components/Primitives/Percent/index.tsx +15 -0
- package/src/components/Primitives/Percent/types.ts +10 -0
- package/src/components/Primitives/RelativeTime/RelativeTime.stories.tsx +57 -0
- package/src/components/Primitives/RelativeTime/index.tsx +31 -0
- package/src/components/Primitives/RelativeTime/types.ts +3 -0
- package/src/components/Tables/BigTable/BigTable.stories.tsx +367 -0
- package/src/components/Tables/BigTable/CLAUDE.md +118 -0
- package/src/components/Tables/BigTable/columnDefs.tsx +208 -0
- package/src/components/Tables/BigTable/index.tsx +104 -0
- package/src/components/Tables/BigTable/styles.module.css +83 -0
- package/src/components/Tables/BigTable/types.ts +20 -0
- package/src/components/Tables/QuickTable/CLAUDE.md +118 -0
- package/src/components/Tables/QuickTable/QuickTable.stories.tsx +121 -0
- package/src/components/Tables/QuickTable/index.tsx +86 -0
- package/src/components/Tables/QuickTable/internal.tsx +48 -0
- package/src/components/Tables/QuickTable/styles.module.css +65 -0
- package/src/components/Tables/QuickTable/types.ts +40 -0
- package/src/env.d.ts +4 -0
- package/src/index.ts +87 -0
- package/src/storybook/CLAUDE.md +35 -0
- package/src/storybook/Composition.stories.tsx +269 -0
- package/src/storybook/Lorem/index.tsx +54 -0
- package/src/storybook/Placeholder/index.tsx +27 -0
- package/src/storybook/Placeholder/styles.module.css +20 -0
- package/src/storybook/Repeat/index.tsx +23 -0
- package/src/storybook/Toggle/index.tsx +29 -0
- package/src/storybook/_StoryUtils.stories.tsx +58 -0
- package/src/storybook/index.ts +4 -0
- package/src/tokens.ts +31 -0
- package/src/utils.test.ts +24 -0
- package/src/utils.ts +2 -0
- package/test-setup.ts +3 -0
- package/tokens.css +323 -0
- package/tsconfig.json +16 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export { Text, type TextProps } from './components/Content/Text';
|
|
2
|
+
export { Heading, type HeadingProps } from './components/Content/Heading';
|
|
3
|
+
export { Link, type LinkProps } from './components/Content/Link';
|
|
4
|
+
export { Stack, type StackProps } from './components/Layout/Stack';
|
|
5
|
+
export { Grid, type GridProps } from './components/Layout/Grid';
|
|
6
|
+
export { Button, type ButtonProps } from './components/Forms/Button';
|
|
7
|
+
export {
|
|
8
|
+
IconButton,
|
|
9
|
+
type IconButtonProps,
|
|
10
|
+
type IconButtonVariant,
|
|
11
|
+
type IconButtonSize,
|
|
12
|
+
} from './components/Forms/IconButton';
|
|
13
|
+
export {
|
|
14
|
+
Tooltip,
|
|
15
|
+
type TooltipProps,
|
|
16
|
+
type TooltipPlacement,
|
|
17
|
+
} from './components/Overlays/Tooltip';
|
|
18
|
+
export { Input, type InputProps } from './components/Forms/Input';
|
|
19
|
+
export { SearchInput, type SearchInputProps } from './components/Forms/SearchInput';
|
|
20
|
+
export { ColorInput, type ColorInputProps } from './components/Forms/ColorInput';
|
|
21
|
+
export { Textarea, type TextareaProps } from './components/Forms/Textarea';
|
|
22
|
+
export { SingleSelect, type SingleSelectProps } from './components/Forms/Select';
|
|
23
|
+
export { MultiSelect, type MultiSelectProps } from './components/Forms/Select';
|
|
24
|
+
export type { SelectOption, SelectSize } from './components/Forms/Select';
|
|
25
|
+
export { Field, type FieldProps } from './components/Forms/Field';
|
|
26
|
+
export { MediumModal, LargeModal, ConfirmModal } from './components/Modals';
|
|
27
|
+
export type { MediumModalProps, LargeModalProps, ConfirmModalProps } from './components/Modals';
|
|
28
|
+
export { Divider } from './components/Layout/Divider';
|
|
29
|
+
export { Debug } from './components/Layout/Debug';
|
|
30
|
+
export { Panels, type PanelsProps } from './components/Layout/Panels';
|
|
31
|
+
export { Bar, type BarProps } from './components/Layout/Bar';
|
|
32
|
+
export { Card, type CardProps } from './components/Content/Card';
|
|
33
|
+
export { Badge, type BadgeProps } from './components/Content/Badge';
|
|
34
|
+
export { Markdown, type MarkdownProps } from './components/Content/Markdown';
|
|
35
|
+
export { EditableMarkdown, type EditableMarkdownProps } from './components/Content/EditableMarkdown';
|
|
36
|
+
export { LoadingContainer, type LoadingContainerProps } from './components/Content/LoadingContainer';
|
|
37
|
+
export { Menu, type MenuProps } from './components/Content/Menu';
|
|
38
|
+
export type { MenuItem, MenuLeaf, MenuBranch } from './components/Content/Menu/types';
|
|
39
|
+
export { Popover, type PopoverProps, type PopoverPlacement } from './components/Overlays/Popover';
|
|
40
|
+
export { List, ListItem, type ListProps, type ListItemProps } from './components/Content/List';
|
|
41
|
+
export { TabBar, type TabBarProps, type TabBarItem } from './components/Navigation/TabBar';
|
|
42
|
+
export { VerticalNav, type VerticalNavProps, type VerticalNavItem } from './components/Navigation/VerticalNav';
|
|
43
|
+
export { Json, type JsonProps } from './components/Json/Json';
|
|
44
|
+
export {
|
|
45
|
+
QuickTable,
|
|
46
|
+
QuickTableRow,
|
|
47
|
+
QuickTableCell,
|
|
48
|
+
QuickTableHeaderRow,
|
|
49
|
+
QuickTableHeaderCell,
|
|
50
|
+
type QuickTableProps,
|
|
51
|
+
type QuickTableRowProps,
|
|
52
|
+
type QuickTableCellProps,
|
|
53
|
+
type QuickTableHeaderRowProps,
|
|
54
|
+
type QuickTableHeaderCellProps,
|
|
55
|
+
type QuickTableCellAlign,
|
|
56
|
+
} from './components/Tables/QuickTable';
|
|
57
|
+
export {
|
|
58
|
+
BigTable,
|
|
59
|
+
textColumn,
|
|
60
|
+
numberColumn,
|
|
61
|
+
currencyColumn,
|
|
62
|
+
dateColumn,
|
|
63
|
+
relativeDateColumn,
|
|
64
|
+
badgeColumn,
|
|
65
|
+
booleanColumn,
|
|
66
|
+
linkColumn,
|
|
67
|
+
idColumn,
|
|
68
|
+
type BigTableProps,
|
|
69
|
+
type ColumnDef,
|
|
70
|
+
type ColumnAlign,
|
|
71
|
+
} from './components/Tables/BigTable';
|
|
72
|
+
export { ChatMessage, ChatShell, ChatComposer, PillCombobox } from './components/Chat';
|
|
73
|
+
export type {
|
|
74
|
+
ChatMessageProps,
|
|
75
|
+
ChatMessageRole,
|
|
76
|
+
ChatShellProps,
|
|
77
|
+
ChatComposerProps,
|
|
78
|
+
PillComboboxOption,
|
|
79
|
+
PillComboboxProps,
|
|
80
|
+
} from './components/Chat';
|
|
81
|
+
export { EmptyValue } from './components/Primitives/EmptyValue';
|
|
82
|
+
export { Num, type NumProps, type NumVariant } from './components/Primitives/Num';
|
|
83
|
+
export { Percent, type PercentProps } from './components/Primitives/Percent';
|
|
84
|
+
export { RelativeTime, type RelativeTimeProps } from './components/Primitives/RelativeTime';
|
|
85
|
+
export { LinedStack, type LinedStackProps } from './components/Primitives/LinedStack';
|
|
86
|
+
export { LongText, type LongTextProps } from './components/Primitives/LongText';
|
|
87
|
+
export { type BackgroundToken } from './tokens';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# _StoryUtils — dev-only helpers for stories
|
|
2
|
+
|
|
3
|
+
**These are not product primitives. They exist only to make story files shorter and clearer.** Do not add them to the root `src/index.ts` barrel. Consuming apps should not import from here.
|
|
4
|
+
|
|
5
|
+
## What lives here
|
|
6
|
+
|
|
7
|
+
- **`Lorem`** — deterministic text filler. `<Lorem paragraphs={3} />`, `<Lorem sentences={2} />`, `<Lorem words={20} />`. Same corpus every render (no hydration flicker, no jitter between renders). `as` takes either an intrinsic tag (`'p'`/`'span'`/`'div'`) or a component reference — `<Lorem paragraphs={20} as={Text} />` renders 20 `<Text>` paragraphs in one call, no `Repeat` needed. Defaults: `p` for paragraphs, `span` for sentences/words.
|
|
8
|
+
- **`Placeholder`** — sized rectangle with an optional label. `<Placeholder width={200} height={120} label="Sidebar" />`. Dashed border + subtle fill. For showing "something goes here" in layout stories when the content itself doesn't matter.
|
|
9
|
+
- **`Toggle`** — render-prop cheap boolean. `<Toggle>{({ isOn, on, off, toggle }) => …}</Toggle>`. Collapses the `useState(false)` + button + onClose preamble that every modal / menu / popover story repeats.
|
|
10
|
+
- **`Repeat`** — `<Repeat count={20}>{(i) => <Row />}</Repeat>`. Each child wrapped in a keyed `Fragment`. Sugar for `Array.from({length: n}, (_, i) => …)`.
|
|
11
|
+
|
|
12
|
+
## Relaxed rules
|
|
13
|
+
|
|
14
|
+
Real primitives follow strict rules in `plan.md` (no `className`, no HTML pass-through, no `aria-*` at call sites, `is*`/`can*`/`has*` booleans, CSS modules only, tokens only). These story utils **intentionally relax the rules** where it helps:
|
|
15
|
+
|
|
16
|
+
- **Render-prop children are fine** (`Toggle`, `Repeat`). Real primitives favor explicit props; these favor terse story code.
|
|
17
|
+
- **`as` prop is fine** (`Lorem`). Real primitives pick their semantic tag; story utils can defer.
|
|
18
|
+
- **Inline styles for dynamic sizing are fine** (`Placeholder`). Same rule as library code, just used more liberally here.
|
|
19
|
+
|
|
20
|
+
What still applies: CSS modules, `--ui-*` tokens, no HTML attribute pass-through, no `className` escape hatch. The relaxed rules are about *shape of the API*, not styling discipline.
|
|
21
|
+
|
|
22
|
+
## Don't graduate these into the public barrel
|
|
23
|
+
|
|
24
|
+
If a story util starts getting reached for by app code, that's a signal to build a real primitive, not to promote the story util. A real `Placeholder` for empty states would be a different primitive with proper typography, icon support, and action slots — not this dashed box. Keep the lanes separate.
|
|
25
|
+
|
|
26
|
+
## Adding a new util
|
|
27
|
+
|
|
28
|
+
- New folder under `src/_StoryUtils/<Name>/` with `index.tsx`.
|
|
29
|
+
- Export from `src/_StoryUtils/index.ts`.
|
|
30
|
+
- No story file for the util itself unless it's actually tricky to use. These are trivial by design.
|
|
31
|
+
- Document the use case in this CLAUDE.md.
|
|
32
|
+
|
|
33
|
+
## Where they're used
|
|
34
|
+
|
|
35
|
+
Stories under `src/**/*.stories.tsx` import from `'../_StoryUtils'` (relative) or the folder-local barrel. The ESLint boundary rule treats `_StoryUtils/` as a primitive; external imports go through `src/_StoryUtils/index.ts`. Stories and tests are the "dogfood" element type and are allowed to reach into their own primitive's index as usual.
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { useRef, useState } from 'react';
|
|
3
|
+
import { Button } from '../components/Forms/Button';
|
|
4
|
+
import { Field } from '../components/Forms/Field';
|
|
5
|
+
import { Input } from '../components/Forms/Input';
|
|
6
|
+
import { MediumModal } from '../components/Modals/MediumModal';
|
|
7
|
+
import { Menu } from '../components/Content/Menu';
|
|
8
|
+
import { Popover } from '../components/Overlays/Popover';
|
|
9
|
+
import { SingleSelect } from '../components/Forms/Select/SingleSelect';
|
|
10
|
+
import { Stack } from '../components/Layout/Stack';
|
|
11
|
+
import { Text } from '../components/Content/Text';
|
|
12
|
+
import { Tooltip } from '../components/Overlays/Tooltip';
|
|
13
|
+
import { Toggle } from '.';
|
|
14
|
+
|
|
15
|
+
const meta: Meta = {
|
|
16
|
+
title: 'Composition/Layered overlays',
|
|
17
|
+
parameters: { layout: 'padded' },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default meta;
|
|
21
|
+
type Story = StoryObj;
|
|
22
|
+
|
|
23
|
+
const PERIOD_OPTIONS = [
|
|
24
|
+
{ value: 'weekly', label: 'Weekly' },
|
|
25
|
+
{ value: 'monthly', label: 'Monthly' },
|
|
26
|
+
{ value: 'quarterly', label: 'Quarterly' },
|
|
27
|
+
{ value: 'annual', label: 'Annual' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const log = (label: string) => () => console.log('selected:', label);
|
|
31
|
+
|
|
32
|
+
const MENU_ITEMS = [
|
|
33
|
+
{ label: 'Refresh', onSelect: log('Refresh') },
|
|
34
|
+
{ label: 'Duplicate', onSelect: log('Duplicate') },
|
|
35
|
+
{
|
|
36
|
+
label: 'Export',
|
|
37
|
+
items: [
|
|
38
|
+
{ label: 'CSV', onSelect: log('CSV') },
|
|
39
|
+
{ label: 'JSON', onSelect: log('JSON') },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
{ label: 'Delete', onSelect: log('Delete') },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// Every layer kind opened from inside a modal. Confirms that Popover, Menu,
|
|
46
|
+
// and Tooltip all paint above the modal's backdrop+content because each is
|
|
47
|
+
// portaled to <body> later in mount order than the modal.
|
|
48
|
+
export const ModalContainsEverything: Story = {
|
|
49
|
+
render: () => {
|
|
50
|
+
const [period, setPeriod] = useState<string>('monthly');
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Toggle defaultOn>
|
|
54
|
+
{({ isOn, on, off }) => (
|
|
55
|
+
<>
|
|
56
|
+
<Button onClick={on}>Open modal</Button>
|
|
57
|
+
<MediumModal
|
|
58
|
+
isOpen={isOn}
|
|
59
|
+
onClose={off}
|
|
60
|
+
title="Layer composition test"
|
|
61
|
+
footer={
|
|
62
|
+
<>
|
|
63
|
+
<Button onClick={off}>Cancel</Button>
|
|
64
|
+
<Button variant="primary" onClick={off}>Save</Button>
|
|
65
|
+
</>
|
|
66
|
+
}
|
|
67
|
+
>
|
|
68
|
+
<Stack gap={3}>
|
|
69
|
+
<Text size="small" isMuted>
|
|
70
|
+
Each row is a different layer kind opened from inside the
|
|
71
|
+
modal. They should all sit above the modal's backdrop.
|
|
72
|
+
</Text>
|
|
73
|
+
|
|
74
|
+
<Field label="Popover-based select">
|
|
75
|
+
<SingleSelect
|
|
76
|
+
options={PERIOD_OPTIONS}
|
|
77
|
+
value={period}
|
|
78
|
+
onChange={setPeriod}
|
|
79
|
+
/>
|
|
80
|
+
</Field>
|
|
81
|
+
|
|
82
|
+
<Field label="Inline-edit popover">
|
|
83
|
+
<PopoverDemo />
|
|
84
|
+
</Field>
|
|
85
|
+
|
|
86
|
+
<Field label="Menu">
|
|
87
|
+
<Menu trigger="Actions ⋯" ariaLabel="Actions" items={MENU_ITEMS} />
|
|
88
|
+
</Field>
|
|
89
|
+
|
|
90
|
+
<Field label="Tooltip">
|
|
91
|
+
<Tooltip label="Tooltips also escape the modal stacking context">
|
|
92
|
+
<Button>Hover me</Button>
|
|
93
|
+
</Tooltip>
|
|
94
|
+
</Field>
|
|
95
|
+
</Stack>
|
|
96
|
+
</MediumModal>
|
|
97
|
+
</>
|
|
98
|
+
)}
|
|
99
|
+
</Toggle>
|
|
100
|
+
);
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const PopoverDemo = () => {
|
|
105
|
+
const anchorRef = useRef<HTMLButtonElement>(null);
|
|
106
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
107
|
+
const [value, setValue] = useState('150');
|
|
108
|
+
const [draft, setDraft] = useState(value);
|
|
109
|
+
return (
|
|
110
|
+
<>
|
|
111
|
+
<Button
|
|
112
|
+
ref={anchorRef}
|
|
113
|
+
size="small"
|
|
114
|
+
onClick={() => {
|
|
115
|
+
setDraft(value);
|
|
116
|
+
setIsOpen(true);
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
119
|
+
Edit ${value}…
|
|
120
|
+
</Button>
|
|
121
|
+
<Popover
|
|
122
|
+
anchorRef={anchorRef}
|
|
123
|
+
isOpen={isOpen}
|
|
124
|
+
onClose={() => setIsOpen(false)}
|
|
125
|
+
title="Edit amount"
|
|
126
|
+
actions={
|
|
127
|
+
<>
|
|
128
|
+
<Button variant="ghost" size="small" onClick={() => setIsOpen(false)}>
|
|
129
|
+
Cancel
|
|
130
|
+
</Button>
|
|
131
|
+
<Button
|
|
132
|
+
variant="primary"
|
|
133
|
+
size="small"
|
|
134
|
+
onClick={() => {
|
|
135
|
+
setValue(draft);
|
|
136
|
+
setIsOpen(false);
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
Save
|
|
140
|
+
</Button>
|
|
141
|
+
</>
|
|
142
|
+
}
|
|
143
|
+
>
|
|
144
|
+
<Field label="Amount">
|
|
145
|
+
<Input value={draft} onChange={(e) => setDraft(e.currentTarget.value)} autoFocus />
|
|
146
|
+
</Field>
|
|
147
|
+
</Popover>
|
|
148
|
+
</>
|
|
149
|
+
);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Each layer is opened on top of the previous one. Mount order alone should
|
|
153
|
+
// guarantee correct stacking: modal < first popover < menu < tooltip.
|
|
154
|
+
export const StackedDeep: Story = {
|
|
155
|
+
render: () => {
|
|
156
|
+
const popoverAnchorRef = useRef<HTMLButtonElement>(null);
|
|
157
|
+
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<Toggle defaultOn>
|
|
161
|
+
{({ isOn, on, off }) => (
|
|
162
|
+
<>
|
|
163
|
+
<Button onClick={on}>Open</Button>
|
|
164
|
+
<MediumModal
|
|
165
|
+
isOpen={isOn}
|
|
166
|
+
onClose={off}
|
|
167
|
+
title="Modal → Popover → Menu → Tooltip"
|
|
168
|
+
footer={<Button onClick={off}>Close</Button>}
|
|
169
|
+
>
|
|
170
|
+
<Stack gap={3}>
|
|
171
|
+
<Text>
|
|
172
|
+
Each layer is anchored inside the previous one. Open them in
|
|
173
|
+
order to confirm each new layer paints above its parent.
|
|
174
|
+
</Text>
|
|
175
|
+
<div>
|
|
176
|
+
<Button
|
|
177
|
+
ref={popoverAnchorRef}
|
|
178
|
+
onClick={() => setIsPopoverOpen(true)}
|
|
179
|
+
>
|
|
180
|
+
1. Open popover
|
|
181
|
+
</Button>
|
|
182
|
+
</div>
|
|
183
|
+
<Popover
|
|
184
|
+
anchorRef={popoverAnchorRef}
|
|
185
|
+
isOpen={isPopoverOpen}
|
|
186
|
+
onClose={() => setIsPopoverOpen(false)}
|
|
187
|
+
title="A popover"
|
|
188
|
+
>
|
|
189
|
+
<Stack gap={2}>
|
|
190
|
+
<Text size="small">
|
|
191
|
+
2. Open the menu (a layer above this popover):
|
|
192
|
+
</Text>
|
|
193
|
+
<Menu
|
|
194
|
+
trigger="Actions ⋯"
|
|
195
|
+
ariaLabel="Actions"
|
|
196
|
+
items={MENU_ITEMS}
|
|
197
|
+
/>
|
|
198
|
+
<Text size="small">3. Hover for a tooltip:</Text>
|
|
199
|
+
<Tooltip label="Tooltip lives above everything">
|
|
200
|
+
<Button size="small">Hover me</Button>
|
|
201
|
+
</Tooltip>
|
|
202
|
+
</Stack>
|
|
203
|
+
</Popover>
|
|
204
|
+
</Stack>
|
|
205
|
+
</MediumModal>
|
|
206
|
+
</>
|
|
207
|
+
)}
|
|
208
|
+
</Toggle>
|
|
209
|
+
);
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// Hostile container: overflow:hidden + transform on an ancestor. Inline-rendered
|
|
214
|
+
// floaters would clip or land in the wrong containing block. Portaled layers
|
|
215
|
+
// escape both.
|
|
216
|
+
export const InsideClippedTransformedContainer: Story = {
|
|
217
|
+
render: () => {
|
|
218
|
+
const [period, setPeriod] = useState<string>('monthly');
|
|
219
|
+
const popoverAnchorRef = useRef<HTMLButtonElement>(null);
|
|
220
|
+
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<div
|
|
224
|
+
style={{
|
|
225
|
+
width: 360,
|
|
226
|
+
height: 240,
|
|
227
|
+
padding: 16,
|
|
228
|
+
overflow: 'hidden',
|
|
229
|
+
transform: 'translateZ(0)',
|
|
230
|
+
border: '1px dashed var(--ui-border)',
|
|
231
|
+
borderRadius: 8,
|
|
232
|
+
}}
|
|
233
|
+
>
|
|
234
|
+
<Stack gap={2}>
|
|
235
|
+
<Text size="small" isMuted>
|
|
236
|
+
This box has overflow:hidden and a transform. Layers should still
|
|
237
|
+
escape.
|
|
238
|
+
</Text>
|
|
239
|
+
<SingleSelect
|
|
240
|
+
options={PERIOD_OPTIONS}
|
|
241
|
+
value={period}
|
|
242
|
+
onChange={setPeriod}
|
|
243
|
+
/>
|
|
244
|
+
<Menu trigger="Actions ⋯" ariaLabel="Actions" items={MENU_ITEMS} />
|
|
245
|
+
<Tooltip label="Escapes the clipped, transformed parent">
|
|
246
|
+
<Button size="small">Hover me</Button>
|
|
247
|
+
</Tooltip>
|
|
248
|
+
<Button
|
|
249
|
+
ref={popoverAnchorRef}
|
|
250
|
+
size="small"
|
|
251
|
+
onClick={() => setIsPopoverOpen(true)}
|
|
252
|
+
>
|
|
253
|
+
Open popover
|
|
254
|
+
</Button>
|
|
255
|
+
<Popover
|
|
256
|
+
anchorRef={popoverAnchorRef}
|
|
257
|
+
isOpen={isPopoverOpen}
|
|
258
|
+
onClose={() => setIsPopoverOpen(false)}
|
|
259
|
+
title="Escaped"
|
|
260
|
+
>
|
|
261
|
+
<Text size="small">
|
|
262
|
+
Rendered via portal-to-body, positioned in viewport coords.
|
|
263
|
+
</Text>
|
|
264
|
+
</Popover>
|
|
265
|
+
</Stack>
|
|
266
|
+
</div>
|
|
267
|
+
);
|
|
268
|
+
},
|
|
269
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ElementType, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export type LoremProps = {
|
|
4
|
+
paragraphs?: number;
|
|
5
|
+
sentences?: number;
|
|
6
|
+
words?: number;
|
|
7
|
+
/**
|
|
8
|
+
* Element (or component) to render into. Accepts intrinsic tags like `'p'`
|
|
9
|
+
* / `'span'` / `'div'`, or a component reference (e.g. `Text`) so paragraph
|
|
10
|
+
* output inherits the library's typography without a wrapping `Repeat`.
|
|
11
|
+
* Defaults: `'p'` for paragraphs, `'span'` for sentences/words.
|
|
12
|
+
*/
|
|
13
|
+
as?: ElementType;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const CORPUS = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus magna felis sollicitudin mauris. Integer in mauris eu nibh euismod gravida. Duis ac tellus et risus vulputate vehicula. Donec lobortis risus a elit. Etiam tempor. Ut ullamcorper, ligula eu tempor congue, eros est euismod turpis, id tincidunt sapien risus a quam. Maecenas fermentum consequat mi. Donec fermentum. Pellentesque malesuada nulla a mi. Duis sapien sem, aliquet nec, commodo eget, consequat quis, neque. Aliquam faucibus, elit ut dictum aliquet, felis nisl adipiscing sapien, sed malesuada diam lacus eget erat. Cras mollis scelerisque nunc. Nullam arcu.`;
|
|
17
|
+
|
|
18
|
+
const WORDS = CORPUS.replace(/[.,]/g, '').split(/\s+/);
|
|
19
|
+
const SENTENCES = CORPUS.match(/[^.]+\./g)?.map((s) => s.trim()) ?? [];
|
|
20
|
+
|
|
21
|
+
const makeParagraph = (index: number, sentencesPer: number): string => {
|
|
22
|
+
const start = (index * sentencesPer) % SENTENCES.length;
|
|
23
|
+
const out: string[] = [];
|
|
24
|
+
for (let i = 0; i < sentencesPer; i += 1) {
|
|
25
|
+
out.push(SENTENCES[(start + i) % SENTENCES.length]!);
|
|
26
|
+
}
|
|
27
|
+
return out.join(' ');
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const Lorem = (props: LoremProps): ReactNode => {
|
|
31
|
+
const { paragraphs, sentences, words, as } = props;
|
|
32
|
+
|
|
33
|
+
if (words != null) {
|
|
34
|
+
const Tag = as ?? 'span';
|
|
35
|
+
const picked = WORDS.slice(0, words).join(' ');
|
|
36
|
+
return <Tag>{picked.charAt(0).toUpperCase() + picked.slice(1) + '.'}</Tag>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (sentences != null) {
|
|
40
|
+
const Tag = as ?? 'span';
|
|
41
|
+
const picked = SENTENCES.slice(0, sentences).join(' ');
|
|
42
|
+
return <Tag>{picked}</Tag>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const count = paragraphs ?? 1;
|
|
46
|
+
const Tag = as ?? 'p';
|
|
47
|
+
return (
|
|
48
|
+
<>
|
|
49
|
+
{Array.from({ length: count }, (_, i) => (
|
|
50
|
+
<Tag key={i}>{makeParagraph(i, 4)}</Tag>
|
|
51
|
+
))}
|
|
52
|
+
</>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from 'react';
|
|
2
|
+
import styles from './styles.module.css';
|
|
3
|
+
|
|
4
|
+
export type PlaceholderProps = {
|
|
5
|
+
width?: string | number;
|
|
6
|
+
height?: string | number;
|
|
7
|
+
label?: ReactNode;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const toLength = (value: string | number | undefined): string | undefined => {
|
|
11
|
+
if (value == null) return undefined;
|
|
12
|
+
return typeof value === 'number' ? `${value}px` : value;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const Placeholder = (props: PlaceholderProps) => {
|
|
16
|
+
const { width, height, label } = props;
|
|
17
|
+
|
|
18
|
+
const style: CSSProperties = {
|
|
19
|
+
width: toLength(width),
|
|
20
|
+
height: toLength(height),
|
|
21
|
+
};
|
|
22
|
+
return (
|
|
23
|
+
<div className={styles.placeholder} style={style}>
|
|
24
|
+
{label ? <span className={styles.label}>{label}</span> : null}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
.placeholder {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: center;
|
|
5
|
+
min-width: 48px;
|
|
6
|
+
min-height: 32px;
|
|
7
|
+
background-color: var(--ui-background-3);
|
|
8
|
+
border: 1px dashed var(--ui-border);
|
|
9
|
+
border-radius: var(--ui-radius);
|
|
10
|
+
color: var(--ui-muted);
|
|
11
|
+
font-family: var(--ui-font-mono);
|
|
12
|
+
font-size: var(--ui-text-xsmall);
|
|
13
|
+
text-align: center;
|
|
14
|
+
padding: var(--ui-space-1) var(--ui-space-2);
|
|
15
|
+
box-sizing: border-box;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.label {
|
|
19
|
+
opacity: 0.85;
|
|
20
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Fragment, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export type RepeatProps = {
|
|
4
|
+
count: number;
|
|
5
|
+
children: (index: number) => ReactNode;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sugar for `Array.from({ length: n }, (_, i) => …)` in stories.
|
|
10
|
+
* Each child is wrapped in a keyed `Fragment`, so callers don't need to
|
|
11
|
+
* thread keys through.
|
|
12
|
+
*/
|
|
13
|
+
export const Repeat = (props: RepeatProps) => {
|
|
14
|
+
const { count, children } = props;
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<>
|
|
18
|
+
{Array.from({ length: count }, (_, i) => (
|
|
19
|
+
<Fragment key={i}>{children(i)}</Fragment>
|
|
20
|
+
))}
|
|
21
|
+
</>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useCallback, useState, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export type ToggleChildArgs = {
|
|
4
|
+
isOn: boolean;
|
|
5
|
+
on: () => void;
|
|
6
|
+
off: () => void;
|
|
7
|
+
toggle: () => void;
|
|
8
|
+
setOn: (next: boolean) => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type ToggleProps = {
|
|
12
|
+
defaultOn?: boolean;
|
|
13
|
+
children: (args: ToggleChildArgs) => ReactNode;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Render-prop boolean state for stories. Collapses the
|
|
18
|
+
* `useState(false)` + open button + onClose wiring that every modal,
|
|
19
|
+
* menu, or popover story would otherwise repeat.
|
|
20
|
+
*/
|
|
21
|
+
export const Toggle = (props: ToggleProps) => {
|
|
22
|
+
const { defaultOn = false, children } = props;
|
|
23
|
+
|
|
24
|
+
const [isOn, setOn] = useState(defaultOn);
|
|
25
|
+
const on = useCallback(() => setOn(true), []);
|
|
26
|
+
const off = useCallback(() => setOn(false), []);
|
|
27
|
+
const toggle = useCallback(() => setOn((prev) => !prev), []);
|
|
28
|
+
return <>{children({ isOn, on, off, toggle, setOn })}</>;
|
|
29
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Lorem, Placeholder, Toggle, Repeat } from './index';
|
|
3
|
+
import { Button } from '../components/Forms/Button';
|
|
4
|
+
import { Stack } from '../components/Layout/Stack';
|
|
5
|
+
import { Text } from '../components/Content/Text';
|
|
6
|
+
import { MediumModal } from '../components/Modals';
|
|
7
|
+
|
|
8
|
+
const meta: Meta = { title: 'StoryUtils/Showcase' };
|
|
9
|
+
export default meta;
|
|
10
|
+
type Story = StoryObj;
|
|
11
|
+
|
|
12
|
+
export const LoremSamples: Story = {
|
|
13
|
+
render: () => (
|
|
14
|
+
<Stack gap={4}>
|
|
15
|
+
<Text size="small" isMuted>paragraphs=3 (default as="p")</Text>
|
|
16
|
+
<Lorem paragraphs={3} />
|
|
17
|
+
<Text size="small" isMuted>sentences=2</Text>
|
|
18
|
+
<Lorem sentences={2} />
|
|
19
|
+
<Text size="small" isMuted>words=12</Text>
|
|
20
|
+
<Lorem words={12} />
|
|
21
|
+
</Stack>
|
|
22
|
+
),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const PlaceholderSamples: Story = {
|
|
26
|
+
render: () => (
|
|
27
|
+
<Stack direction="row" gap={2}>
|
|
28
|
+
<Placeholder width={120} height={80} label="Sidebar" />
|
|
29
|
+
<Placeholder width={240} height={80} label="Main" />
|
|
30
|
+
<Placeholder width={80} height={80} label="Aside" />
|
|
31
|
+
</Stack>
|
|
32
|
+
),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const ToggleWithModal: Story = {
|
|
36
|
+
render: () => (
|
|
37
|
+
<Toggle>
|
|
38
|
+
{({ isOn, on, off }) => (
|
|
39
|
+
<>
|
|
40
|
+
<Button onClick={on}>Open modal</Button>
|
|
41
|
+
<MediumModal isOpen={isOn} onClose={off} title="From a Toggle">
|
|
42
|
+
<Lorem paragraphs={2} />
|
|
43
|
+
</MediumModal>
|
|
44
|
+
</>
|
|
45
|
+
)}
|
|
46
|
+
</Toggle>
|
|
47
|
+
),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const RepeatWithPlaceholder: Story = {
|
|
51
|
+
render: () => (
|
|
52
|
+
<Stack gap={1}>
|
|
53
|
+
<Repeat count={6}>
|
|
54
|
+
{(i) => <Placeholder height={32} label={`row ${i + 1}`} />}
|
|
55
|
+
</Repeat>
|
|
56
|
+
</Stack>
|
|
57
|
+
),
|
|
58
|
+
};
|
package/src/tokens.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Background ramp level. Maps 1:1 to the `--ui-background-{0..3}` tokens
|
|
5
|
+
* defined in `tokens.css`. Each level is role-semantic, not brightness-ordered:
|
|
6
|
+
* 0 — primary interactive surface (content, inputs, modal)
|
|
7
|
+
* 1 — sidebar
|
|
8
|
+
* 2 — body / header / footer wall
|
|
9
|
+
* 3 — recessed or raised highlight (hover fills, badges)
|
|
10
|
+
* Each theme assigns a tone to each level. Brightness ordering between the
|
|
11
|
+
* four levels may differ between light and dark themes (e.g. in dark-warm,
|
|
12
|
+
* level 3 is lighter than level 0 because hover "lifts" on dark backgrounds).
|
|
13
|
+
*/
|
|
14
|
+
export type BackgroundToken = '0' | '1' | '2' | '3';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build an inline style that sets a named CSS custom property to the
|
|
18
|
+
* corresponding background token. Components consume the custom property
|
|
19
|
+
* in their stylesheet with a fallback to their default, e.g.
|
|
20
|
+
* background-color: var(--ui-bar-bg, transparent);
|
|
21
|
+
*
|
|
22
|
+
* `varName` is scoped per-component (e.g. `--ui-bar-bg`) so nested
|
|
23
|
+
* components don't inherit each other's overrides via the cascade.
|
|
24
|
+
*/
|
|
25
|
+
export const backgroundStyle = (
|
|
26
|
+
varName: string,
|
|
27
|
+
token: BackgroundToken | undefined,
|
|
28
|
+
): CSSProperties | undefined => {
|
|
29
|
+
if (token == null) return undefined;
|
|
30
|
+
return { [varName]: `var(--ui-background-${token})` } as CSSProperties;
|
|
31
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { cx } from './utils';
|
|
3
|
+
|
|
4
|
+
describe('cx', () => {
|
|
5
|
+
test('joins truthy strings with spaces', () => {
|
|
6
|
+
expect(cx('a', 'b', 'c')).toBe('a b c');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test('filters out false, null, and undefined', () => {
|
|
10
|
+
expect(cx('a', false, 'b', null, undefined, 'c')).toBe('a b c');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('returns empty string when all inputs are falsy', () => {
|
|
14
|
+
expect(cx(false, null, undefined)).toBe('');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('conditional class pattern', () => {
|
|
18
|
+
const active = true;
|
|
19
|
+
const disabled = false;
|
|
20
|
+
expect(
|
|
21
|
+
cx('btn', active && 'btn-active', disabled && 'btn-disabled'),
|
|
22
|
+
).toBe('btn btn-active');
|
|
23
|
+
});
|
|
24
|
+
});
|
package/src/utils.ts
ADDED