@ds-mo/ui 0.1.0
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/LICENSE +21 -0
- package/README.md +81 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +969 -0
- package/dist/index.js +2623 -0
- package/dist/index.js.map +1 -0
- package/dist/r/accordion.json +79 -0
- package/dist/r/badge.json +69 -0
- package/dist/r/banner.json +90 -0
- package/dist/r/breadcrumb.json +70 -0
- package/dist/r/button-group.json +65 -0
- package/dist/r/button.json +137 -0
- package/dist/r/card.json +82 -0
- package/dist/r/checkbox.json +91 -0
- package/dist/r/divider.json +57 -0
- package/dist/r/empty-state.json +69 -0
- package/dist/r/error-boundary.json +66 -0
- package/dist/r/fade.json +65 -0
- package/dist/r/field.json +71 -0
- package/dist/r/header.json +67 -0
- package/dist/r/input.json +77 -0
- package/dist/r/loader.json +54 -0
- package/dist/r/menu.json +130 -0
- package/dist/r/modal.json +93 -0
- package/dist/r/pagination.json +80 -0
- package/dist/r/radio.json +85 -0
- package/dist/r/registry.json +1600 -0
- package/dist/r/scrollbar.json +68 -0
- package/dist/r/select.json +93 -0
- package/dist/r/sidebar.json +105 -0
- package/dist/r/skeleton.json +72 -0
- package/dist/r/slider.json +87 -0
- package/dist/r/surface.json +97 -0
- package/dist/r/tab-group.json +72 -0
- package/dist/r/tab.json +71 -0
- package/dist/r/table.json +110 -0
- package/dist/r/tag.json +110 -0
- package/dist/r/text.json +94 -0
- package/dist/r/toast.json +77 -0
- package/dist/r/toggle-button-group.json +65 -0
- package/dist/r/toggle-button.json +94 -0
- package/dist/r/toggle.json +65 -0
- package/dist/r/tooltip.json +86 -0
- package/package.json +80 -0
- package/src/components/Accordion/Accordion.module.css +66 -0
- package/src/components/Accordion/Accordion.stories.tsx +95 -0
- package/src/components/Accordion/Accordion.tsx +122 -0
- package/src/components/Accordion/index.ts +2 -0
- package/src/components/Badge/Badge.module.css +6 -0
- package/src/components/Badge/Badge.stories.tsx +99 -0
- package/src/components/Badge/Badge.tsx +23 -0
- package/src/components/Badge/index.ts +2 -0
- package/src/components/Banner/Banner.module.css +67 -0
- package/src/components/Banner/Banner.stories.tsx +81 -0
- package/src/components/Banner/Banner.tsx +109 -0
- package/src/components/Banner/index.ts +2 -0
- package/src/components/Breadcrumb/Breadcrumb.module.css +46 -0
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +72 -0
- package/src/components/Breadcrumb/Breadcrumb.tsx +68 -0
- package/src/components/Breadcrumb/index.ts +2 -0
- package/src/components/Button/Button.module.css +343 -0
- package/src/components/Button/Button.stories.tsx +575 -0
- package/src/components/Button/Button.tsx +201 -0
- package/src/components/Button/index.ts +10 -0
- package/src/components/ButtonGroup/ButtonGroup.module.css +157 -0
- package/src/components/ButtonGroup/ButtonGroup.stories.tsx +384 -0
- package/src/components/ButtonGroup/ButtonGroup.tsx +122 -0
- package/src/components/ButtonGroup/index.ts +2 -0
- package/src/components/Card/Card.module.css +36 -0
- package/src/components/Card/Card.stories.tsx +78 -0
- package/src/components/Card/Card.tsx +53 -0
- package/src/components/Card/index.ts +2 -0
- package/src/components/Checkbox/Checkbox.module.css +47 -0
- package/src/components/Checkbox/Checkbox.stories.tsx +129 -0
- package/src/components/Checkbox/Checkbox.tsx +68 -0
- package/src/components/Checkbox/index.ts +2 -0
- package/src/components/Divider/Divider.module.css +19 -0
- package/src/components/Divider/Divider.stories.tsx +55 -0
- package/src/components/Divider/Divider.tsx +28 -0
- package/src/components/Divider/index.ts +2 -0
- package/src/components/EmptyState/EmptyState.module.css +8 -0
- package/src/components/EmptyState/EmptyState.stories.tsx +53 -0
- package/src/components/EmptyState/EmptyState.tsx +31 -0
- package/src/components/EmptyState/index.ts +2 -0
- package/src/components/ErrorBoundary/ErrorBoundary.module.css +18 -0
- package/src/components/ErrorBoundary/ErrorBoundary.stories.tsx +38 -0
- package/src/components/ErrorBoundary/ErrorBoundary.tsx +50 -0
- package/src/components/ErrorBoundary/index.ts +2 -0
- package/src/components/Fade/Fade.module.css +31 -0
- package/src/components/Fade/Fade.stories.tsx +35 -0
- package/src/components/Fade/Fade.tsx +33 -0
- package/src/components/Fade/index.ts +2 -0
- package/src/components/Field/Field.module.css +5 -0
- package/src/components/Field/Field.stories.tsx +111 -0
- package/src/components/Field/Field.tsx +22 -0
- package/src/components/Field/index.ts +2 -0
- package/src/components/Header/Header.module.css +33 -0
- package/src/components/Header/Header.stories.tsx +52 -0
- package/src/components/Header/Header.tsx +42 -0
- package/src/components/Header/index.ts +2 -0
- package/src/components/Input/Input.module.css +71 -0
- package/src/components/Input/Input.stories.tsx +135 -0
- package/src/components/Input/Input.tsx +76 -0
- package/src/components/Input/index.ts +2 -0
- package/src/components/LabelWrap/LabelWrap.module.css +34 -0
- package/src/components/LabelWrap/LabelWrap.tsx +32 -0
- package/src/components/LabelWrap/index.ts +1 -0
- package/src/components/Loader/Loader.module.css +9 -0
- package/src/components/Loader/Loader.stories.tsx +44 -0
- package/src/components/Loader/Loader.tsx +43 -0
- package/src/components/Loader/index.ts +2 -0
- package/src/components/Menu/DestructiveMenuItem.module.css +57 -0
- package/src/components/Menu/DestructiveMenuItem.tsx +64 -0
- package/src/components/Menu/Menu.module.css +49 -0
- package/src/components/Menu/Menu.stories.tsx +129 -0
- package/src/components/Menu/Menu.tsx +269 -0
- package/src/components/Menu/MenuItem.module.css +84 -0
- package/src/components/Menu/MenuItem.tsx +101 -0
- package/src/components/Menu/index.ts +6 -0
- package/src/components/Modal/Modal.module.css +67 -0
- package/src/components/Modal/Modal.stories.tsx +71 -0
- package/src/components/Modal/Modal.tsx +122 -0
- package/src/components/Modal/index.ts +2 -0
- package/src/components/Pagination/Pagination.module.css +64 -0
- package/src/components/Pagination/Pagination.stories.tsx +51 -0
- package/src/components/Pagination/Pagination.tsx +103 -0
- package/src/components/Pagination/index.ts +2 -0
- package/src/components/Radio/Radio.module.css +61 -0
- package/src/components/Radio/Radio.stories.tsx +101 -0
- package/src/components/Radio/Radio.tsx +108 -0
- package/src/components/Radio/index.ts +2 -0
- package/src/components/Scrollbar/Scrollbar.module.css +81 -0
- package/src/components/Scrollbar/Scrollbar.stories.tsx +46 -0
- package/src/components/Scrollbar/Scrollbar.tsx +187 -0
- package/src/components/Scrollbar/index.ts +2 -0
- package/src/components/Select/Select.module.css +31 -0
- package/src/components/Select/Select.stories.tsx +59 -0
- package/src/components/Select/Select.tsx +92 -0
- package/src/components/Select/index.ts +2 -0
- package/src/components/Sidebar/Sidebar.module.css +161 -0
- package/src/components/Sidebar/Sidebar.stories.tsx +125 -0
- package/src/components/Sidebar/Sidebar.tsx +129 -0
- package/src/components/Sidebar/SidebarItem.tsx +57 -0
- package/src/components/Sidebar/SidebarSection.tsx +42 -0
- package/src/components/Sidebar/index.ts +6 -0
- package/src/components/Skeleton/Skeleton.module.css +53 -0
- package/src/components/Skeleton/Skeleton.stories.tsx +89 -0
- package/src/components/Skeleton/Skeleton.tsx +66 -0
- package/src/components/Skeleton/index.ts +2 -0
- package/src/components/Slider/Slider.module.css +62 -0
- package/src/components/Slider/Slider.stories.tsx +120 -0
- package/src/components/Slider/Slider.tsx +74 -0
- package/src/components/Slider/index.ts +2 -0
- package/src/components/Surface/Surface.module.css +222 -0
- package/src/components/Surface/Surface.stories.tsx +213 -0
- package/src/components/Surface/Surface.tsx +219 -0
- package/src/components/Surface/index.ts +12 -0
- package/src/components/Tab/Tab.module.css +66 -0
- package/src/components/Tab/Tab.stories.tsx +103 -0
- package/src/components/Tab/Tab.tsx +41 -0
- package/src/components/Tab/index.ts +2 -0
- package/src/components/TabGroup/TabGroup.module.css +26 -0
- package/src/components/TabGroup/TabGroup.stories.tsx +60 -0
- package/src/components/TabGroup/TabGroup.tsx +83 -0
- package/src/components/TabGroup/index.ts +2 -0
- package/src/components/Table/Table.module.css +130 -0
- package/src/components/Table/Table.stories.tsx +111 -0
- package/src/components/Table/Table.tsx +226 -0
- package/src/components/Table/index.ts +11 -0
- package/src/components/Tag/Tag.module.css +270 -0
- package/src/components/Tag/Tag.stories.tsx +399 -0
- package/src/components/Tag/Tag.tsx +162 -0
- package/src/components/Tag/index.ts +2 -0
- package/src/components/Text/Text.module.css +166 -0
- package/src/components/Text/Text.stories.tsx +212 -0
- package/src/components/Text/Text.tsx +181 -0
- package/src/components/Text/index.ts +13 -0
- package/src/components/Toast/Toast.module.css +91 -0
- package/src/components/Toast/Toast.stories.tsx +66 -0
- package/src/components/Toast/Toast.tsx +182 -0
- package/src/components/Toast/index.ts +8 -0
- package/src/components/Toggle/Toggle.module.css +87 -0
- package/src/components/Toggle/Toggle.stories.tsx +85 -0
- package/src/components/Toggle/Toggle.tsx +52 -0
- package/src/components/Toggle/index.ts +2 -0
- package/src/components/ToggleButton/ToggleButton.module.css +172 -0
- package/src/components/ToggleButton/ToggleButton.stories.tsx +299 -0
- package/src/components/ToggleButton/ToggleButton.tsx +118 -0
- package/src/components/ToggleButton/index.ts +2 -0
- package/src/components/ToggleButtonGroup/ToggleButtonGroup.module.css +163 -0
- package/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx +341 -0
- package/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx +111 -0
- package/src/components/ToggleButtonGroup/index.ts +2 -0
- package/src/components/Tooltip/Tooltip.module.css +45 -0
- package/src/components/Tooltip/Tooltip.stories.tsx +46 -0
- package/src/components/Tooltip/Tooltip.tsx +224 -0
- package/src/components/Tooltip/index.ts +2 -0
- package/src/css-modules.d.ts +4 -0
- package/src/docs/ColorUsage.mdx +195 -0
- package/src/docs/ElevationUsage.mdx +95 -0
- package/src/docs/Introduction.mdx +97 -0
- package/src/docs/OpticalSizing.mdx +178 -0
- package/src/docs/TypographyUsage.mdx +121 -0
- package/src/index.ts +154 -0
- package/src/stories/Colors.stories.tsx +528 -0
- package/src/stories/Dimensions.stories.tsx +200 -0
- package/src/stories/Effects.stories.tsx +231 -0
- package/src/stories/Icons.stories.tsx +159 -0
- package/src/stories/Typography.stories.tsx +157 -0
- package/src/types/icons.ts +15 -0
- package/src/types/index.ts +1 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/css-tokens.ts +22 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
|
+
import { Accordion } from './Accordion';
|
|
4
|
+
import { Text } from '@/components/Text';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Accordion> = {
|
|
7
|
+
title: 'Classic/Accordion',
|
|
8
|
+
component: Accordion,
|
|
9
|
+
args: {
|
|
10
|
+
multiple: false,
|
|
11
|
+
},
|
|
12
|
+
argTypes: {
|
|
13
|
+
multiple: { control: 'boolean' },
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default meta;
|
|
18
|
+
type Story = StoryObj<typeof Accordion>;
|
|
19
|
+
|
|
20
|
+
const ITEMS = [
|
|
21
|
+
{
|
|
22
|
+
id: 'shipping',
|
|
23
|
+
label: 'Shipping Information',
|
|
24
|
+
content: (
|
|
25
|
+
<Text variant="text-body-medium" as="p" color="secondary">
|
|
26
|
+
We offer free standard shipping on all orders over $50. Express shipping is available for $9.99.
|
|
27
|
+
International shipping rates are calculated at checkout based on destination.
|
|
28
|
+
</Text>
|
|
29
|
+
),
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'returns',
|
|
33
|
+
label: 'Returns & Exchanges',
|
|
34
|
+
content: (
|
|
35
|
+
<Text variant="text-body-medium" as="p" color="secondary">
|
|
36
|
+
Items can be returned within 30 days of purchase. Items must be in original condition with tags attached.
|
|
37
|
+
Refunds are processed within 5-7 business days.
|
|
38
|
+
</Text>
|
|
39
|
+
),
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'warranty',
|
|
43
|
+
label: 'Warranty',
|
|
44
|
+
content: (
|
|
45
|
+
<Text variant="text-body-medium" as="p" color="secondary">
|
|
46
|
+
All products come with a 1-year manufacturer warranty against defects in materials and workmanship.
|
|
47
|
+
</Text>
|
|
48
|
+
),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'contact',
|
|
52
|
+
label: 'Contact Support',
|
|
53
|
+
content: (
|
|
54
|
+
<Text variant="text-body-medium" as="p" color="secondary">
|
|
55
|
+
Email us at support@example.com or call 1-800-EXAMPLE. Our support hours are Mon-Fri 9am-5pm EST.
|
|
56
|
+
</Text>
|
|
57
|
+
),
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const Controlled = (props: Partial<React.ComponentProps<typeof Accordion>>) => {
|
|
62
|
+
const [expanded, setExpanded] = useState<string[]>([]);
|
|
63
|
+
return <Accordion items={ITEMS} expandedIds={expanded} onExpandedChange={setExpanded} {...props} />;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const Playground: Story = {
|
|
67
|
+
render: args => <Controlled {...args} />,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const Default: Story = {
|
|
71
|
+
render: () => <Controlled />,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const Multiple: Story = {
|
|
75
|
+
render: () => <Controlled multiple />,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const WithInactive: Story = {
|
|
79
|
+
render: () => (
|
|
80
|
+
<Controlled
|
|
81
|
+
items={[
|
|
82
|
+
...ITEMS.slice(0, 2),
|
|
83
|
+
{ ...ITEMS[2], inactive: true },
|
|
84
|
+
ITEMS[3],
|
|
85
|
+
]}
|
|
86
|
+
/>
|
|
87
|
+
),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const PreExpanded: Story = {
|
|
91
|
+
render: () => {
|
|
92
|
+
const [expanded, setExpanded] = useState<string[]>(['shipping']);
|
|
93
|
+
return <Accordion items={ITEMS} expandedIds={expanded} onExpandedChange={setExpanded} />;
|
|
94
|
+
},
|
|
95
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import React, { forwardRef, useState, useRef, useEffect, useCallback } from 'react';
|
|
2
|
+
import { cn } from '@/utils/cn';
|
|
3
|
+
import { Text } from '@/components/Text';
|
|
4
|
+
import styles from './Accordion.module.css';
|
|
5
|
+
|
|
6
|
+
export interface AccordionItemData {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
content: React.ReactNode;
|
|
10
|
+
inactive?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AccordionProps {
|
|
14
|
+
/** Items to render. */
|
|
15
|
+
items: AccordionItemData[];
|
|
16
|
+
/** Allow multiple items open at once. When false, behaves as an accordion. */
|
|
17
|
+
multiple?: boolean;
|
|
18
|
+
/** Controlled: which item IDs are expanded. */
|
|
19
|
+
expandedIds?: string[];
|
|
20
|
+
/** Called when expansion state changes. */
|
|
21
|
+
onExpandedChange?: (ids: string[]) => void;
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const Accordion = forwardRef<HTMLDivElement, AccordionProps>(
|
|
26
|
+
({ items, multiple = false, expandedIds: controlledIds, onExpandedChange, className }, ref) => {
|
|
27
|
+
const [internalIds, setInternalIds] = useState<string[]>([]);
|
|
28
|
+
const expandedIds = controlledIds ?? internalIds;
|
|
29
|
+
|
|
30
|
+
const toggle = (id: string) => {
|
|
31
|
+
let next: string[];
|
|
32
|
+
if (expandedIds.includes(id)) {
|
|
33
|
+
next = expandedIds.filter(x => x !== id);
|
|
34
|
+
} else {
|
|
35
|
+
next = multiple ? [...expandedIds, id] : [id];
|
|
36
|
+
}
|
|
37
|
+
setInternalIds(next);
|
|
38
|
+
onExpandedChange?.(next);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div ref={ref} className={cn(styles.accordion, className)}>
|
|
43
|
+
{items.map(item => (
|
|
44
|
+
<AccordionItem
|
|
45
|
+
key={item.id}
|
|
46
|
+
label={item.label}
|
|
47
|
+
isExpanded={expandedIds.includes(item.id)}
|
|
48
|
+
inactive={item.inactive}
|
|
49
|
+
onToggle={() => toggle(item.id)}
|
|
50
|
+
>
|
|
51
|
+
{item.content}
|
|
52
|
+
</AccordionItem>
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
Accordion.displayName = 'Accordion';
|
|
60
|
+
|
|
61
|
+
/* ─── AccordionItem ─────────────────────────────────────────────────────────── */
|
|
62
|
+
|
|
63
|
+
export interface AccordionItemProps {
|
|
64
|
+
label: string;
|
|
65
|
+
children: React.ReactNode;
|
|
66
|
+
isExpanded?: boolean;
|
|
67
|
+
onToggle?: () => void;
|
|
68
|
+
inactive?: boolean;
|
|
69
|
+
className?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const AccordionItem = forwardRef<HTMLDivElement, AccordionItemProps>(
|
|
73
|
+
({ label, children, isExpanded = false, onToggle, inactive = false, className }, ref) => {
|
|
74
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
75
|
+
const [height, setHeight] = useState<number>(0);
|
|
76
|
+
|
|
77
|
+
const measure = useCallback(() => {
|
|
78
|
+
if (contentRef.current) {
|
|
79
|
+
setHeight(contentRef.current.scrollHeight);
|
|
80
|
+
}
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
measure();
|
|
85
|
+
const el = contentRef.current;
|
|
86
|
+
if (!el) return;
|
|
87
|
+
const ro = new ResizeObserver(measure);
|
|
88
|
+
ro.observe(el);
|
|
89
|
+
return () => ro.disconnect();
|
|
90
|
+
}, [measure]);
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div ref={ref} className={cn(styles.item, inactive && styles.inactive, className)}>
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
className={styles.trigger}
|
|
97
|
+
onClick={() => !inactive && onToggle?.()}
|
|
98
|
+
aria-expanded={isExpanded}
|
|
99
|
+
tabIndex={inactive ? -1 : 0}
|
|
100
|
+
>
|
|
101
|
+
<Text variant="text-body-medium-emphasis" as="span" color="inherit">
|
|
102
|
+
{label}
|
|
103
|
+
</Text>
|
|
104
|
+
<span className={cn(styles.chevron, isExpanded && styles.chevronOpen)}>
|
|
105
|
+
›
|
|
106
|
+
</span>
|
|
107
|
+
</button>
|
|
108
|
+
<div
|
|
109
|
+
className={styles.body}
|
|
110
|
+
style={{ height: isExpanded ? height : 0 }}
|
|
111
|
+
aria-hidden={!isExpanded}
|
|
112
|
+
>
|
|
113
|
+
<div ref={contentRef} className={styles.bodyInner}>
|
|
114
|
+
{children}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
AccordionItem.displayName = 'AccordionItem';
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Badge } from './Badge';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Badge> = {
|
|
6
|
+
title: 'Primitives/Badge',
|
|
7
|
+
component: Badge,
|
|
8
|
+
args: {
|
|
9
|
+
count: 3,
|
|
10
|
+
isSelected: false,
|
|
11
|
+
},
|
|
12
|
+
argTypes: {
|
|
13
|
+
count: { control: 'number' },
|
|
14
|
+
isSelected: { control: 'boolean' },
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default meta;
|
|
19
|
+
type Story = StoryObj<typeof Badge>;
|
|
20
|
+
|
|
21
|
+
export const Playground: Story = {};
|
|
22
|
+
|
|
23
|
+
// ─── Matrix ───────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
const COUNTS = [1, 5, 9, 10, 99];
|
|
26
|
+
|
|
27
|
+
const row: React.CSSProperties = { display: 'flex', gap: 16, alignItems: 'center' };
|
|
28
|
+
const col: React.CSSProperties = { display: 'flex', flexDirection: 'column', gap: 12 };
|
|
29
|
+
const labelStyle: React.CSSProperties = { fontSize: 10, fontFamily: 'monospace', color: '#888', minWidth: 72, flexShrink: 0 };
|
|
30
|
+
|
|
31
|
+
export const Matrix: Story = {
|
|
32
|
+
parameters: { layout: 'padded' },
|
|
33
|
+
render: () => (
|
|
34
|
+
<div style={{ ...col, fontFamily: 'sans-serif' }}>
|
|
35
|
+
{/* Header row */}
|
|
36
|
+
<div style={row}>
|
|
37
|
+
<span style={labelStyle}></span>
|
|
38
|
+
{COUNTS.map(c => (
|
|
39
|
+
<span key={c} style={{ ...labelStyle, textAlign: 'center' }}>{c}</span>
|
|
40
|
+
))}
|
|
41
|
+
</div>
|
|
42
|
+
{/* Normal */}
|
|
43
|
+
<div style={row}>
|
|
44
|
+
<span style={labelStyle}>normal</span>
|
|
45
|
+
{COUNTS.map(c => (
|
|
46
|
+
<div key={c} style={{ minWidth: 72, display: 'flex', justifyContent: 'center' }}>
|
|
47
|
+
<Badge count={c} />
|
|
48
|
+
</div>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
{/* Selected */}
|
|
52
|
+
<div style={row}>
|
|
53
|
+
<span style={labelStyle}>selected</span>
|
|
54
|
+
{COUNTS.map(c => (
|
|
55
|
+
<div key={c} style={{ minWidth: 72, display: 'flex', justifyContent: 'center' }}>
|
|
56
|
+
<Badge count={c} isSelected />
|
|
57
|
+
</div>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
{/* Zero renders nothing */}
|
|
61
|
+
<div style={row}>
|
|
62
|
+
<span style={labelStyle}>zero</span>
|
|
63
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
64
|
+
<Badge count={0} />
|
|
65
|
+
<span style={{ fontSize: 10, color: '#aaa', fontFamily: 'monospace' }}>(renders null)</span>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ─── Individual stories ───────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
export const Counts: Story = {
|
|
75
|
+
render: () => (
|
|
76
|
+
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
|
|
77
|
+
{COUNTS.map(c => <Badge key={c} count={c} />)}
|
|
78
|
+
</div>
|
|
79
|
+
),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const Selected: Story = {
|
|
83
|
+
render: () => (
|
|
84
|
+
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
|
|
85
|
+
<Badge count={5} />
|
|
86
|
+
<Badge count={5} isSelected />
|
|
87
|
+
</div>
|
|
88
|
+
),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const ZeroCount: Story = {
|
|
92
|
+
render: () => (
|
|
93
|
+
<div style={{ display: 'flex', gap: 8, alignItems: 'center', fontFamily: 'sans-serif', fontSize: 12 }}>
|
|
94
|
+
<span>count=0 renders null:</span>
|
|
95
|
+
<Badge count={0} />
|
|
96
|
+
<span style={{ color: '#888' }}>(nothing between these spans)</span>
|
|
97
|
+
</div>
|
|
98
|
+
),
|
|
99
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text } from '@/components/Text';
|
|
3
|
+
import styles from './Badge.module.css';
|
|
4
|
+
|
|
5
|
+
export interface BadgeProps {
|
|
6
|
+
count: number;
|
|
7
|
+
isSelected?: boolean;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const Badge: React.FC<BadgeProps> = ({ count, isSelected = false, className }) => {
|
|
12
|
+
if (count === 0) return null;
|
|
13
|
+
const displayText = count > 9 ? '+' : count.toString();
|
|
14
|
+
return (
|
|
15
|
+
<span className={`${styles.badge} ${className || ''}`} aria-label={`${count} unread`}>
|
|
16
|
+
<Text variant="text-caption-emphasis" as="span" color={isSelected ? 'primary' : 'secondary'}>
|
|
17
|
+
{displayText}
|
|
18
|
+
</Text>
|
|
19
|
+
</span>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
Badge.displayName = 'Badge';
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
.bannerWrapper {}
|
|
2
|
+
|
|
3
|
+
.banner {
|
|
4
|
+
position: relative;
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
overflow: hidden;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.bannerHeaderSurface {
|
|
11
|
+
width: 100%;
|
|
12
|
+
height: var(--dimension-space-100);
|
|
13
|
+
flex-shrink: 0;
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
border-bottom: var(--dimension-stroke-width-015) solid var(--color-border-primary);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.bannerHeader {
|
|
19
|
+
width: 100%;
|
|
20
|
+
height: 100%;
|
|
21
|
+
background: repeating-linear-gradient(
|
|
22
|
+
-45deg,
|
|
23
|
+
var(--banner-content),
|
|
24
|
+
var(--banner-content) var(--dimension-size-100),
|
|
25
|
+
transparent var(--dimension-size-100),
|
|
26
|
+
transparent var(--dimension-size-200)
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.bannerBody {
|
|
31
|
+
padding: var(--dimension-space-075);
|
|
32
|
+
min-height: var(--dimension-size-400);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.message {
|
|
36
|
+
color: var(--banner-content);
|
|
37
|
+
line-height: var(--typography-lineheight-md);
|
|
38
|
+
display: block;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.floatingPosition {
|
|
42
|
+
position: fixed;
|
|
43
|
+
top: var(--dimension-space-200);
|
|
44
|
+
left: 50%;
|
|
45
|
+
transform: translateX(-50%);
|
|
46
|
+
pointer-events: none;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.floatingToast {
|
|
50
|
+
backface-visibility: hidden;
|
|
51
|
+
-webkit-font-smoothing: subpixel-antialiased;
|
|
52
|
+
animation: toastFadeIn var(--effect-motion-short-2) forwards;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.floatingToast.closing {
|
|
56
|
+
animation: toastFadeOut var(--effect-motion-short-3) forwards;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@keyframes toastFadeIn {
|
|
60
|
+
from { opacity: 0; transform: translateY(-8px); }
|
|
61
|
+
to { opacity: 1; transform: translateY(0); }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@keyframes toastFadeOut {
|
|
65
|
+
from { opacity: 1; transform: translateY(0); }
|
|
66
|
+
to { opacity: 0; transform: translateY(-4px); }
|
|
67
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
|
+
import { Banner } from './Banner';
|
|
4
|
+
import { Button } from '../Button';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Banner> = {
|
|
7
|
+
title: 'Components/Banner',
|
|
8
|
+
component: Banner,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default meta;
|
|
12
|
+
type Story = StoryObj<typeof Banner>;
|
|
13
|
+
|
|
14
|
+
export const Intents: Story = {
|
|
15
|
+
render: () => (
|
|
16
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, padding: 24 }}>
|
|
17
|
+
{(['brand', 'positive', 'negative', 'warning', 'caution', 'ai', 'neutral'] as const).map(intent => (
|
|
18
|
+
<Banner key={intent} intent={intent} contrast="faint" message={`This is a ${intent} banner.`} />
|
|
19
|
+
))}
|
|
20
|
+
</div>
|
|
21
|
+
),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const Contrasts: Story = {
|
|
25
|
+
render: () => (
|
|
26
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, padding: 24 }}>
|
|
27
|
+
{(['faint', 'medium', 'bold', 'strong'] as const).map(contrast => (
|
|
28
|
+
<Banner key={contrast} intent="brand" contrast={contrast} message={`Brand banner — ${contrast} contrast`} />
|
|
29
|
+
))}
|
|
30
|
+
</div>
|
|
31
|
+
),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const WithHeader: Story = {
|
|
35
|
+
args: {
|
|
36
|
+
intent: 'positive',
|
|
37
|
+
contrast: 'faint',
|
|
38
|
+
message: 'Your changes have been saved successfully.',
|
|
39
|
+
header: true,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const Dismissible: Story = {
|
|
44
|
+
render: () => {
|
|
45
|
+
const [visible, setVisible] = useState(true);
|
|
46
|
+
return (
|
|
47
|
+
<div style={{ padding: 24 }}>
|
|
48
|
+
{visible ? (
|
|
49
|
+
<Banner
|
|
50
|
+
intent="warning"
|
|
51
|
+
contrast="faint"
|
|
52
|
+
message="This banner can be dismissed."
|
|
53
|
+
onDismiss={() => setVisible(false)}
|
|
54
|
+
/>
|
|
55
|
+
) : (
|
|
56
|
+
<Button variant="secondary" label="Show Banner" onClick={() => setVisible(true)} />
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const FloatingToast: Story = {
|
|
64
|
+
render: () => {
|
|
65
|
+
const [show, setShow] = useState(false);
|
|
66
|
+
return (
|
|
67
|
+
<div style={{ padding: 24 }}>
|
|
68
|
+
<Button variant="primary" label="Show Toast" onClick={() => setShow(true)} />
|
|
69
|
+
{show && (
|
|
70
|
+
<Banner
|
|
71
|
+
intent="positive"
|
|
72
|
+
contrast="faint"
|
|
73
|
+
message="Operation completed!"
|
|
74
|
+
floating
|
|
75
|
+
onDismiss={() => setShow(false)}
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
},
|
|
81
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { Surface } from '@/components/Surface';
|
|
4
|
+
import { Text } from '@/components/Text';
|
|
5
|
+
import { getCssTimeMs } from '@/utils/css-tokens';
|
|
6
|
+
import styles from './Banner.module.css';
|
|
7
|
+
|
|
8
|
+
const TOAST_DURATION_FALLBACK_MS = 4000;
|
|
9
|
+
const FADE_OUT_DURATION_MS = 200;
|
|
10
|
+
|
|
11
|
+
export type BannerIntent =
|
|
12
|
+
| 'brand' | 'positive' | 'negative' | 'warning' | 'caution'
|
|
13
|
+
| 'ai' | 'neutral' | 'walkthrough' | 'guide';
|
|
14
|
+
|
|
15
|
+
export type BannerContrast = 'faint' | 'medium' | 'bold' | 'strong';
|
|
16
|
+
|
|
17
|
+
export interface BannerProps {
|
|
18
|
+
intent: BannerIntent;
|
|
19
|
+
contrast: BannerContrast;
|
|
20
|
+
message: string;
|
|
21
|
+
header?: boolean;
|
|
22
|
+
className?: string;
|
|
23
|
+
floating?: boolean;
|
|
24
|
+
onDismiss?: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const getContentColorVar = (intent: BannerIntent, contrast: BannerContrast): string => {
|
|
28
|
+
if (contrast === 'bold') return 'var(--color-foreground-on-bold-background-primary)';
|
|
29
|
+
const contentContrast = contrast === 'faint' ? 'bold' : contrast === 'medium' ? 'strong' : 'medium';
|
|
30
|
+
return `var(--color-foreground-${contentContrast}-${intent})`;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const Banner: React.FC<BannerProps> = ({
|
|
34
|
+
intent,
|
|
35
|
+
contrast,
|
|
36
|
+
message,
|
|
37
|
+
header = false,
|
|
38
|
+
className = '',
|
|
39
|
+
floating = false,
|
|
40
|
+
onDismiss,
|
|
41
|
+
}) => {
|
|
42
|
+
const toastDurationMs = getCssTimeMs('--effect-animation-delay-long-2', TOAST_DURATION_FALLBACK_MS);
|
|
43
|
+
const messageColor = getContentColorVar(intent, contrast);
|
|
44
|
+
const [isClosing, setIsClosing] = useState(false);
|
|
45
|
+
const dismissTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
46
|
+
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
47
|
+
|
|
48
|
+
const scheduleDismiss = useCallback(() => {
|
|
49
|
+
if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current);
|
|
50
|
+
if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
|
|
51
|
+
dismissTimerRef.current = setTimeout(() => {
|
|
52
|
+
dismissTimerRef.current = null;
|
|
53
|
+
setIsClosing(true);
|
|
54
|
+
closeTimerRef.current = setTimeout(() => {
|
|
55
|
+
closeTimerRef.current = null;
|
|
56
|
+
onDismiss?.();
|
|
57
|
+
}, FADE_OUT_DURATION_MS);
|
|
58
|
+
}, toastDurationMs);
|
|
59
|
+
}, [onDismiss, toastDurationMs]);
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (floating && message) scheduleDismiss();
|
|
63
|
+
return () => {
|
|
64
|
+
if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current);
|
|
65
|
+
if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
|
|
66
|
+
};
|
|
67
|
+
}, [floating, message, scheduleDismiss]);
|
|
68
|
+
|
|
69
|
+
const content = (
|
|
70
|
+
<div
|
|
71
|
+
className={`${styles.bannerWrapper} ${className}`.trim()}
|
|
72
|
+
style={{ '--banner-content': messageColor } as React.CSSProperties}
|
|
73
|
+
>
|
|
74
|
+
<Surface
|
|
75
|
+
elevation={floating ? 'floating' : 'elevated'}
|
|
76
|
+
intent={intent}
|
|
77
|
+
contrast={contrast}
|
|
78
|
+
radius="sm"
|
|
79
|
+
className={styles.banner}
|
|
80
|
+
>
|
|
81
|
+
{header && (
|
|
82
|
+
<div className={styles.bannerHeaderSurface}>
|
|
83
|
+
<div className={styles.bannerHeader} aria-hidden />
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
<div className={styles.bannerBody}>
|
|
87
|
+
<Text variant="text-body-medium" as="span" className={styles.message}>
|
|
88
|
+
{message}
|
|
89
|
+
</Text>
|
|
90
|
+
</div>
|
|
91
|
+
</Surface>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (floating) {
|
|
96
|
+
return createPortal(
|
|
97
|
+
<div className={styles.floatingPosition} style={{ zIndex: 9998 }}>
|
|
98
|
+
<div className={`${styles.floatingToast} ${isClosing ? styles.closing : ''}`}>
|
|
99
|
+
{content}
|
|
100
|
+
</div>
|
|
101
|
+
</div>,
|
|
102
|
+
document.body
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return content;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
Banner.displayName = 'Banner';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
.breadcrumb {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.list {
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
list-style: none;
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
gap: var(--dimension-space-050);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.item {
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
gap: var(--dimension-space-050);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.link {
|
|
22
|
+
color: var(--color-foreground-secondary);
|
|
23
|
+
text-decoration: none;
|
|
24
|
+
background: none;
|
|
25
|
+
border: none;
|
|
26
|
+
padding: var(--dimension-space-025) var(--dimension-space-050);
|
|
27
|
+
border-radius: var(--dimension-radius-050);
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
transition: background-color var(--effect-motion-short-2), color var(--effect-motion-short-2);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.link:hover {
|
|
33
|
+
background-color: var(--color-interaction-hover);
|
|
34
|
+
color: var(--color-foreground-primary);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.link:active {
|
|
38
|
+
background-color: var(--color-interaction-pressed);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.separator {
|
|
42
|
+
color: var(--color-foreground-tertiary);
|
|
43
|
+
font-size: 12px;
|
|
44
|
+
-webkit-user-select: none;
|
|
45
|
+
user-select: none;
|
|
46
|
+
}
|