@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,111 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
|
+
import { Field } from './Field';
|
|
4
|
+
import { Input } from '@/components/Input';
|
|
5
|
+
import { Slider } from '@/components/Slider';
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof Field> = {
|
|
8
|
+
title: 'Primitives/Field',
|
|
9
|
+
component: Field,
|
|
10
|
+
args: {
|
|
11
|
+
label: 'Label',
|
|
12
|
+
},
|
|
13
|
+
argTypes: {
|
|
14
|
+
label: { control: 'text' },
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default meta;
|
|
19
|
+
type Story = StoryObj<typeof Field>;
|
|
20
|
+
|
|
21
|
+
export const Playground: Story = {
|
|
22
|
+
render: (args) => {
|
|
23
|
+
const [value, setValue] = useState('');
|
|
24
|
+
return (
|
|
25
|
+
<div style={{ width: 280 }}>
|
|
26
|
+
<Field {...args}>
|
|
27
|
+
<Input id={args.id} value={value} onChange={(e) => setValue(e.target.value)} placeholder="Enter value…" />
|
|
28
|
+
</Field>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// ─── Matrix ───────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
const col: React.CSSProperties = { display: 'flex', flexDirection: 'column', gap: 20 };
|
|
37
|
+
const section = (text: string) => (
|
|
38
|
+
<div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '0.05em', textTransform: 'uppercase', color: '#555' }}>
|
|
39
|
+
{text}
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
const divider = <hr style={{ border: 'none', borderTop: '1px solid #e0e0e0', margin: '4px 0 20px' }} />;
|
|
43
|
+
|
|
44
|
+
export const Matrix: Story = {
|
|
45
|
+
parameters: { layout: 'padded' },
|
|
46
|
+
render: () => {
|
|
47
|
+
const [textVal, setTextVal] = useState('');
|
|
48
|
+
const [emailVal, setEmailVal] = useState('');
|
|
49
|
+
const [searchVal, setSearchVal] = useState('');
|
|
50
|
+
const [sliderVal, setSliderVal] = useState(50);
|
|
51
|
+
const [opacityVal, setOpacityVal] = useState(0.8);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div style={{ ...col, fontFamily: 'sans-serif', maxWidth: 320 }}>
|
|
55
|
+
|
|
56
|
+
{section('With Input')}
|
|
57
|
+
{divider}
|
|
58
|
+
<Field label="Full name" id="name">
|
|
59
|
+
<Input id="name" value={textVal} onChange={(e) => setTextVal(e.target.value)} placeholder="e.g. Jane Smith" />
|
|
60
|
+
</Field>
|
|
61
|
+
<Field label="Email address" id="email">
|
|
62
|
+
<Input id="email" type="email" value={emailVal} onChange={(e) => setEmailVal(e.target.value)} placeholder="user@example.com" />
|
|
63
|
+
</Field>
|
|
64
|
+
<Field label="Search" id="search">
|
|
65
|
+
<Input id="search" type="search" value={searchVal} onChange={(e) => setSearchVal(e.target.value)} placeholder="Search…" />
|
|
66
|
+
</Field>
|
|
67
|
+
<Field label="Disabled field" id="disabled">
|
|
68
|
+
<Input id="disabled" value="Read-only value" disabled onChange={() => {}} />
|
|
69
|
+
</Field>
|
|
70
|
+
|
|
71
|
+
{section('With Slider')}
|
|
72
|
+
{divider}
|
|
73
|
+
<Field label="Volume">
|
|
74
|
+
<Slider label="Volume" value={sliderVal} onChange={setSliderVal} />
|
|
75
|
+
</Field>
|
|
76
|
+
<Field label="Opacity">
|
|
77
|
+
<Slider label="Opacity" value={opacityVal} onChange={setOpacityVal} min={0} max={1} step={0.05} />
|
|
78
|
+
</Field>
|
|
79
|
+
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// ─── Individual stories ───────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
export const WithInput: Story = {
|
|
88
|
+
render: () => {
|
|
89
|
+
const [value, setValue] = useState('');
|
|
90
|
+
return (
|
|
91
|
+
<div style={{ width: 280 }}>
|
|
92
|
+
<Field label="Name" id="name-field">
|
|
93
|
+
<Input id="name-field" value={value} onChange={(e) => setValue(e.target.value)} placeholder="Enter your name" />
|
|
94
|
+
</Field>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const WithSlider: Story = {
|
|
101
|
+
render: () => {
|
|
102
|
+
const [value, setValue] = useState(50);
|
|
103
|
+
return (
|
|
104
|
+
<div style={{ width: 280 }}>
|
|
105
|
+
<Field label="Volume">
|
|
106
|
+
<Slider label="Volume" value={value} onChange={setValue} />
|
|
107
|
+
</Field>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
import { cn } from '@/utils/cn';
|
|
3
|
+
import { Text } from '@/components/Text';
|
|
4
|
+
import styles from './Field.module.css';
|
|
5
|
+
|
|
6
|
+
export interface FieldProps {
|
|
7
|
+
label: string;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
id?: string;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const Field = forwardRef<HTMLDivElement, FieldProps>(
|
|
14
|
+
({ label, children, id, className }, ref) => (
|
|
15
|
+
<div ref={ref} className={cn(styles.field, className)}>
|
|
16
|
+
<Text variant="text-body-small-emphasis" as="label" htmlFor={id}>{label}</Text>
|
|
17
|
+
{children}
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
Field.displayName = 'Field';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
.header {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: space-between;
|
|
5
|
+
height: var(--dimension-size-600, 48px);
|
|
6
|
+
padding: 0 var(--dimension-space-100);
|
|
7
|
+
flex-shrink: 0;
|
|
8
|
+
gap: var(--dimension-space-100);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.left {
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
gap: var(--dimension-space-100);
|
|
15
|
+
flex-shrink: 0;
|
|
16
|
+
min-width: 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.center {
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
justify-content: center;
|
|
23
|
+
gap: var(--dimension-space-050);
|
|
24
|
+
flex: 1;
|
|
25
|
+
min-width: 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.right {
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: var(--dimension-space-050);
|
|
32
|
+
flex-shrink: 0;
|
|
33
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { Header } from './Header';
|
|
3
|
+
import { Button } from '../Button';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Header> = {
|
|
6
|
+
title: 'Layout/Header',
|
|
7
|
+
component: Header,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
type Story = StoryObj<typeof Header>;
|
|
12
|
+
|
|
13
|
+
export const Default: Story = {
|
|
14
|
+
args: {
|
|
15
|
+
title: 'Page Title',
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const WithSlots: Story = {
|
|
20
|
+
render: () => (
|
|
21
|
+
<Header
|
|
22
|
+
title="Dashboard"
|
|
23
|
+
left={<Button variant="tertiary" label="Back" size="sm" />}
|
|
24
|
+
right={
|
|
25
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
26
|
+
<Button variant="secondary" label="Export" size="sm" />
|
|
27
|
+
<Button variant="primary" label="New" size="sm" />
|
|
28
|
+
</div>
|
|
29
|
+
}
|
|
30
|
+
/>
|
|
31
|
+
),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const CenterContent: Story = {
|
|
35
|
+
render: () => (
|
|
36
|
+
<Header
|
|
37
|
+
left={<Button variant="tertiary" label="Menu" size="sm" />}
|
|
38
|
+
center={<span style={{ fontWeight: 600 }}>App Name</span>}
|
|
39
|
+
right={<Button variant="tertiary" label="Settings" size="sm" />}
|
|
40
|
+
/>
|
|
41
|
+
),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const Backgrounds: Story = {
|
|
45
|
+
render: () => (
|
|
46
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
47
|
+
{(['primary', 'secondary', 'transparent', 'translucent'] as const).map(bg => (
|
|
48
|
+
<Header key={bg} title={`${bg} background`} background={bg} />
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
),
|
|
52
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
import { cn } from '@/utils/cn';
|
|
3
|
+
import { Surface } from '@/components/Surface';
|
|
4
|
+
import { Text } from '@/components/Text';
|
|
5
|
+
import styles from './Header.module.css';
|
|
6
|
+
|
|
7
|
+
export type HeaderBackground = 'primary' | 'secondary' | 'transparent' | 'translucent';
|
|
8
|
+
|
|
9
|
+
export interface HeaderProps {
|
|
10
|
+
title?: string;
|
|
11
|
+
left?: React.ReactNode;
|
|
12
|
+
center?: React.ReactNode;
|
|
13
|
+
right?: React.ReactNode;
|
|
14
|
+
background?: HeaderBackground;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const Header = forwardRef<HTMLElement, HeaderProps>(
|
|
19
|
+
({ title, left, center, right, background = 'secondary', className }, ref) => (
|
|
20
|
+
<Surface
|
|
21
|
+
ref={ref}
|
|
22
|
+
as="header"
|
|
23
|
+
background={background}
|
|
24
|
+
elevation="none"
|
|
25
|
+
edge="bottom"
|
|
26
|
+
className={cn(styles.header, className)}
|
|
27
|
+
>
|
|
28
|
+
<div className={styles.left}>
|
|
29
|
+
{left}
|
|
30
|
+
{title && !center && (
|
|
31
|
+
<Text variant="text-title-small" as="h1" lineTruncation={1}>
|
|
32
|
+
{title}
|
|
33
|
+
</Text>
|
|
34
|
+
)}
|
|
35
|
+
</div>
|
|
36
|
+
{center && <div className={styles.center}>{center}</div>}
|
|
37
|
+
<div className={styles.right}>{right}</div>
|
|
38
|
+
</Surface>
|
|
39
|
+
)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
Header.displayName = 'Header';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
.wrapper {
|
|
2
|
+
width: 100%;
|
|
3
|
+
height: var(--dimension-size-400);
|
|
4
|
+
display: flex;
|
|
5
|
+
align-items: center;
|
|
6
|
+
background-color: var(--color-background-primary);
|
|
7
|
+
border-radius: var(--dimension-radius-075);
|
|
8
|
+
box-shadow: var(--effect-elevation-depressed-sm);
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.wrapper:focus-within {
|
|
13
|
+
box-shadow: inset 0 0 0 1.25px var(--color-foreground-bold-brand);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.row {
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
width: 100%;
|
|
20
|
+
height: 100%;
|
|
21
|
+
gap: var(--dimension-space-050);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.input {
|
|
25
|
+
flex: 1;
|
|
26
|
+
min-width: 0;
|
|
27
|
+
height: 100%;
|
|
28
|
+
padding: 0 var(--dimension-space-100);
|
|
29
|
+
background: transparent;
|
|
30
|
+
border: none;
|
|
31
|
+
outline: none;
|
|
32
|
+
font-family: inherit;
|
|
33
|
+
font-size: var(--typography-fontsize-md);
|
|
34
|
+
line-height: var(--typography-lineheight-md);
|
|
35
|
+
color: var(--color-foreground-primary);
|
|
36
|
+
border-radius: var(--dimension-radius-075);
|
|
37
|
+
-webkit-appearance: none;
|
|
38
|
+
appearance: none;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.input:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
42
|
+
.input::placeholder { color: var(--color-foreground-secondary); }
|
|
43
|
+
.input:focus::placeholder { color: var(--color-foreground-tertiary); }
|
|
44
|
+
|
|
45
|
+
/* Hide native search clear */
|
|
46
|
+
.input[type="search"]::-webkit-search-cancel-button,
|
|
47
|
+
.input[type="search"]::-webkit-search-decoration { display: none; }
|
|
48
|
+
|
|
49
|
+
.clearButton {
|
|
50
|
+
flex-shrink: 0;
|
|
51
|
+
margin-right: var(--dimension-space-050);
|
|
52
|
+
padding: var(--dimension-space-050);
|
|
53
|
+
border: none;
|
|
54
|
+
background: none;
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
color: var(--color-foreground-secondary);
|
|
57
|
+
border-radius: var(--dimension-radius-050);
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
justify-content: center;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.clearButton:hover { background-color: var(--color-interaction-hover); }
|
|
64
|
+
|
|
65
|
+
.suffix {
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
gap: var(--dimension-space-050);
|
|
69
|
+
flex-shrink: 0;
|
|
70
|
+
margin-right: var(--dimension-space-050);
|
|
71
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
|
+
import { Input } from './Input';
|
|
4
|
+
import type { InputType } from './Input';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Input> = {
|
|
7
|
+
title: 'Primitives/Input',
|
|
8
|
+
component: Input,
|
|
9
|
+
args: {
|
|
10
|
+
placeholder: 'Enter text…',
|
|
11
|
+
inactive: false,
|
|
12
|
+
type: 'text',
|
|
13
|
+
},
|
|
14
|
+
argTypes: {
|
|
15
|
+
type: { control: 'select', options: ['text', 'email', 'tel', 'url', 'search', 'password'] },
|
|
16
|
+
inactive: { control: 'boolean' },
|
|
17
|
+
placeholder: { control: 'text' },
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default meta;
|
|
22
|
+
type Story = StoryObj<typeof Input>;
|
|
23
|
+
|
|
24
|
+
const Controlled = (props: Omit<React.ComponentProps<typeof Input>, 'value' | 'onChange'> & { initialValue?: string }) => {
|
|
25
|
+
const [value, setValue] = useState(props.initialValue ?? '');
|
|
26
|
+
return <Input {...props} value={value} onChange={(e) => setValue(e.target.value)} />;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const Playground: Story = {
|
|
30
|
+
render: (args) => <Controlled {...args} />,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// ─── Matrix ───────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
const TYPES: InputType[] = ['text', 'email', 'search', 'password'];
|
|
36
|
+
|
|
37
|
+
const col: React.CSSProperties = { display: 'flex', flexDirection: 'column', gap: 10 };
|
|
38
|
+
const row: React.CSSProperties = { display: 'flex', gap: 16, alignItems: 'center' };
|
|
39
|
+
const labelStyle: React.CSSProperties = { fontSize: 10, fontFamily: 'monospace', color: '#888', minWidth: 72, flexShrink: 0 };
|
|
40
|
+
const inputCell: React.CSSProperties = { width: 200, flexShrink: 0 };
|
|
41
|
+
const section = (text: string) => (
|
|
42
|
+
<div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '0.05em', textTransform: 'uppercase', color: '#555', marginTop: 8 }}>
|
|
43
|
+
{text}
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
const divider = <hr style={{ border: 'none', borderTop: '1px solid #e0e0e0', margin: '16px 0' }} />;
|
|
47
|
+
|
|
48
|
+
export const Matrix: Story = {
|
|
49
|
+
parameters: { layout: 'padded' },
|
|
50
|
+
render: () => (
|
|
51
|
+
<div style={{ ...col, fontFamily: 'sans-serif', gap: 0 }}>
|
|
52
|
+
|
|
53
|
+
{section('Types × Empty / Filled')}
|
|
54
|
+
<div style={{ ...col, marginTop: 12 }}>
|
|
55
|
+
<div style={row}>
|
|
56
|
+
<span style={labelStyle}></span>
|
|
57
|
+
<span style={{ ...inputCell, fontSize: 10, fontFamily: 'monospace', color: '#888', textAlign: 'center' }}>empty</span>
|
|
58
|
+
<span style={{ ...inputCell, fontSize: 10, fontFamily: 'monospace', color: '#888', textAlign: 'center' }}>filled</span>
|
|
59
|
+
</div>
|
|
60
|
+
{TYPES.map(type => (
|
|
61
|
+
<div key={type} style={row}>
|
|
62
|
+
<span style={labelStyle}>{type}</span>
|
|
63
|
+
<div style={inputCell}>
|
|
64
|
+
<Controlled type={type} placeholder={`${type}…`} />
|
|
65
|
+
</div>
|
|
66
|
+
<div style={inputCell}>
|
|
67
|
+
<Controlled type={type} initialValue={type === 'email' ? 'user@example.com' : type === 'password' ? 'secret123' : 'Hello, world'} />
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
))}
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{divider}
|
|
74
|
+
|
|
75
|
+
{section('States')}
|
|
76
|
+
<div style={{ ...col, marginTop: 12 }}>
|
|
77
|
+
<div style={row}>
|
|
78
|
+
<span style={labelStyle}>default</span>
|
|
79
|
+
<div style={inputCell}><Controlled placeholder="Enter text…" /></div>
|
|
80
|
+
</div>
|
|
81
|
+
<div style={row}>
|
|
82
|
+
<span style={labelStyle}>with value</span>
|
|
83
|
+
<div style={inputCell}><Controlled initialValue="Some value" /></div>
|
|
84
|
+
</div>
|
|
85
|
+
<div style={row}>
|
|
86
|
+
<span style={labelStyle}>Inactive</span>
|
|
87
|
+
<div style={inputCell}><Input value="Disabled value" inactive onChange={() => {}} /></div>
|
|
88
|
+
</div>
|
|
89
|
+
<div style={row}>
|
|
90
|
+
<span style={labelStyle}>Inactive empty</span>
|
|
91
|
+
<div style={inputCell}><Input value="" placeholder="Disabled…" inactive onChange={() => {}} /></div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
</div>
|
|
96
|
+
),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// ─── Individual stories ───────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
export const Default: Story = {
|
|
102
|
+
render: () => <Controlled placeholder="Enter text…" style={{ width: 260 }} />,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const Search: Story = {
|
|
106
|
+
render: () => (
|
|
107
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, width: 260 }}>
|
|
108
|
+
<Controlled type="search" placeholder="Search…" />
|
|
109
|
+
<Controlled type="search" placeholder="Search…" initialValue="Some query" />
|
|
110
|
+
</div>
|
|
111
|
+
),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const Password: Story = {
|
|
115
|
+
render: () => <Controlled type="password" placeholder="Enter password…" style={{ width: 260 }} />,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export const Inactive: Story = {
|
|
119
|
+
render: () => (
|
|
120
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, width: 260 }}>
|
|
121
|
+
<Input value="" placeholder="Disabled empty" inactive onChange={() => {}} />
|
|
122
|
+
<Input value="Disabled with value" inactive onChange={() => {}} />
|
|
123
|
+
</div>
|
|
124
|
+
),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export const WithSuffix: Story = {
|
|
128
|
+
render: () => (
|
|
129
|
+
<Controlled
|
|
130
|
+
placeholder="Enter amount…"
|
|
131
|
+
suffix={<span style={{ fontSize: 12, color: '#888', paddingRight: 4 }}>USD</span>}
|
|
132
|
+
style={{ width: 260 }}
|
|
133
|
+
/>
|
|
134
|
+
),
|
|
135
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
import { cn } from '@/utils/cn';
|
|
3
|
+
import type { IconComponent } from '@/types/icons';
|
|
4
|
+
import styles from './Input.module.css';
|
|
5
|
+
|
|
6
|
+
export type InputType = 'text' | 'email' | 'tel' | 'url' | 'search' | 'password';
|
|
7
|
+
|
|
8
|
+
export interface InputProps {
|
|
9
|
+
value: string;
|
|
10
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
11
|
+
id?: string;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
autoFocus?: boolean;
|
|
14
|
+
className?: string;
|
|
15
|
+
type?: InputType;
|
|
16
|
+
inactive?: boolean;
|
|
17
|
+
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
|
|
18
|
+
/** Content rendered inside the field on the right. */
|
|
19
|
+
suffix?: React.ReactNode;
|
|
20
|
+
/** Icon for clear button (search type). */
|
|
21
|
+
clearIcon?: IconComponent;
|
|
22
|
+
'aria-label'?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const Input = forwardRef<HTMLDivElement, InputProps>(
|
|
26
|
+
(
|
|
27
|
+
{
|
|
28
|
+
value,
|
|
29
|
+
onChange,
|
|
30
|
+
id,
|
|
31
|
+
placeholder,
|
|
32
|
+
autoFocus = false,
|
|
33
|
+
className,
|
|
34
|
+
type = 'text',
|
|
35
|
+
inactive = false,
|
|
36
|
+
onKeyDown,
|
|
37
|
+
suffix,
|
|
38
|
+
clearIcon: ClearIcon,
|
|
39
|
+
'aria-label': ariaLabel,
|
|
40
|
+
},
|
|
41
|
+
ref
|
|
42
|
+
) => {
|
|
43
|
+
const showClear = type === 'search' && value.length > 0 && !inactive;
|
|
44
|
+
|
|
45
|
+
const handleClear = () => {
|
|
46
|
+
onChange({ target: { value: '' } } as React.ChangeEvent<HTMLInputElement>);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div ref={ref} className={cn(styles.wrapper, className)}>
|
|
51
|
+
<div className={styles.row}>
|
|
52
|
+
<input
|
|
53
|
+
type={type}
|
|
54
|
+
id={id}
|
|
55
|
+
value={value}
|
|
56
|
+
onChange={onChange}
|
|
57
|
+
onKeyDown={onKeyDown}
|
|
58
|
+
placeholder={placeholder}
|
|
59
|
+
className={styles.input}
|
|
60
|
+
autoFocus={autoFocus}
|
|
61
|
+
disabled={inactive}
|
|
62
|
+
aria-label={ariaLabel}
|
|
63
|
+
/>
|
|
64
|
+
{showClear && (
|
|
65
|
+
<button type="button" className={styles.clearButton} onClick={handleClear} aria-label="Clear">
|
|
66
|
+
{ClearIcon ? <ClearIcon size={16} /> : <span aria-hidden>×</span>}
|
|
67
|
+
</button>
|
|
68
|
+
)}
|
|
69
|
+
{suffix && <div className={styles.suffix}>{suffix}</div>}
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
Input.displayName = 'Input';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
LabelWrap — internal utility
|
|
3
|
+
Kills the inherited line-height strut so the component's fixed height
|
|
4
|
+
is not inflated by body { line-height: 1.5 }, then adds optical
|
|
5
|
+
horizontal padding around the text.
|
|
6
|
+
========================================================================== */
|
|
7
|
+
|
|
8
|
+
.labelWrap {
|
|
9
|
+
line-height: 0;
|
|
10
|
+
padding: 0 var(--dimension-space-050); /* 4px — body sm / md / lg */
|
|
11
|
+
white-space: nowrap;
|
|
12
|
+
flex-shrink: 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* Small scale: tighter optical padding */
|
|
16
|
+
.sizeSm { padding: 0 var(--dimension-space-025); } /* 2px */
|
|
17
|
+
|
|
18
|
+
/* Caption scale: tighter optical padding */
|
|
19
|
+
.sizeXs { padding: 0 var(--dimension-space-025); } /* 2px */
|
|
20
|
+
|
|
21
|
+
/* Truncation mode — used when parent is full-width and label must ellipsize */
|
|
22
|
+
.truncate {
|
|
23
|
+
flex-shrink: 1;
|
|
24
|
+
min-width: 0;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
white-space: nowrap;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.truncate > span {
|
|
30
|
+
display: block;
|
|
31
|
+
overflow: hidden;
|
|
32
|
+
text-overflow: ellipsis;
|
|
33
|
+
white-space: nowrap;
|
|
34
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { cn } from '@/utils/cn';
|
|
3
|
+
import styles from './LabelWrap.module.css';
|
|
4
|
+
|
|
5
|
+
interface LabelWrapProps {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
/**
|
|
8
|
+
* Maps to typography size — controls optical horizontal padding:
|
|
9
|
+
* xs | sm → 2px (tighter scale)
|
|
10
|
+
* md | lg → 4px (body scale, default)
|
|
11
|
+
*/
|
|
12
|
+
size?: 'xs' | 'sm' | 'md' | 'lg';
|
|
13
|
+
/** Enables truncation with ellipsis — use inside full-width containers. */
|
|
14
|
+
truncate?: boolean;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Internal utility wrapper used inside interactive components (Button, Tag,
|
|
20
|
+
* Input, Select, MenuItem, etc.) to:
|
|
21
|
+
* 1. Kill the inherited line-height strut from body { line-height: 1.5 }
|
|
22
|
+
* 2. Add optical horizontal padding around the text
|
|
23
|
+
*
|
|
24
|
+
* Not exported from the public barrel — internal use only.
|
|
25
|
+
*/
|
|
26
|
+
export const LabelWrap = ({ children, size = 'md', truncate = false, className }: LabelWrapProps) => (
|
|
27
|
+
<span className={cn(styles.labelWrap, size === 'sm' && styles.sizeSm, size === 'xs' && styles.sizeXs, truncate && styles.truncate, className)}>
|
|
28
|
+
{children}
|
|
29
|
+
</span>
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
LabelWrap.displayName = 'LabelWrap';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { LabelWrap } from './LabelWrap';
|