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