@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.
Files changed (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -0
  3. package/dist/index.css +1 -0
  4. package/dist/index.d.ts +969 -0
  5. package/dist/index.js +2623 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/r/accordion.json +79 -0
  8. package/dist/r/badge.json +69 -0
  9. package/dist/r/banner.json +90 -0
  10. package/dist/r/breadcrumb.json +70 -0
  11. package/dist/r/button-group.json +65 -0
  12. package/dist/r/button.json +137 -0
  13. package/dist/r/card.json +82 -0
  14. package/dist/r/checkbox.json +91 -0
  15. package/dist/r/divider.json +57 -0
  16. package/dist/r/empty-state.json +69 -0
  17. package/dist/r/error-boundary.json +66 -0
  18. package/dist/r/fade.json +65 -0
  19. package/dist/r/field.json +71 -0
  20. package/dist/r/header.json +67 -0
  21. package/dist/r/input.json +77 -0
  22. package/dist/r/loader.json +54 -0
  23. package/dist/r/menu.json +130 -0
  24. package/dist/r/modal.json +93 -0
  25. package/dist/r/pagination.json +80 -0
  26. package/dist/r/radio.json +85 -0
  27. package/dist/r/registry.json +1600 -0
  28. package/dist/r/scrollbar.json +68 -0
  29. package/dist/r/select.json +93 -0
  30. package/dist/r/sidebar.json +105 -0
  31. package/dist/r/skeleton.json +72 -0
  32. package/dist/r/slider.json +87 -0
  33. package/dist/r/surface.json +97 -0
  34. package/dist/r/tab-group.json +72 -0
  35. package/dist/r/tab.json +71 -0
  36. package/dist/r/table.json +110 -0
  37. package/dist/r/tag.json +110 -0
  38. package/dist/r/text.json +94 -0
  39. package/dist/r/toast.json +77 -0
  40. package/dist/r/toggle-button-group.json +65 -0
  41. package/dist/r/toggle-button.json +94 -0
  42. package/dist/r/toggle.json +65 -0
  43. package/dist/r/tooltip.json +86 -0
  44. package/package.json +80 -0
  45. package/src/components/Accordion/Accordion.module.css +66 -0
  46. package/src/components/Accordion/Accordion.stories.tsx +95 -0
  47. package/src/components/Accordion/Accordion.tsx +122 -0
  48. package/src/components/Accordion/index.ts +2 -0
  49. package/src/components/Badge/Badge.module.css +6 -0
  50. package/src/components/Badge/Badge.stories.tsx +99 -0
  51. package/src/components/Badge/Badge.tsx +23 -0
  52. package/src/components/Badge/index.ts +2 -0
  53. package/src/components/Banner/Banner.module.css +67 -0
  54. package/src/components/Banner/Banner.stories.tsx +81 -0
  55. package/src/components/Banner/Banner.tsx +109 -0
  56. package/src/components/Banner/index.ts +2 -0
  57. package/src/components/Breadcrumb/Breadcrumb.module.css +46 -0
  58. package/src/components/Breadcrumb/Breadcrumb.stories.tsx +72 -0
  59. package/src/components/Breadcrumb/Breadcrumb.tsx +68 -0
  60. package/src/components/Breadcrumb/index.ts +2 -0
  61. package/src/components/Button/Button.module.css +343 -0
  62. package/src/components/Button/Button.stories.tsx +575 -0
  63. package/src/components/Button/Button.tsx +201 -0
  64. package/src/components/Button/index.ts +10 -0
  65. package/src/components/ButtonGroup/ButtonGroup.module.css +157 -0
  66. package/src/components/ButtonGroup/ButtonGroup.stories.tsx +384 -0
  67. package/src/components/ButtonGroup/ButtonGroup.tsx +122 -0
  68. package/src/components/ButtonGroup/index.ts +2 -0
  69. package/src/components/Card/Card.module.css +36 -0
  70. package/src/components/Card/Card.stories.tsx +78 -0
  71. package/src/components/Card/Card.tsx +53 -0
  72. package/src/components/Card/index.ts +2 -0
  73. package/src/components/Checkbox/Checkbox.module.css +47 -0
  74. package/src/components/Checkbox/Checkbox.stories.tsx +129 -0
  75. package/src/components/Checkbox/Checkbox.tsx +68 -0
  76. package/src/components/Checkbox/index.ts +2 -0
  77. package/src/components/Divider/Divider.module.css +19 -0
  78. package/src/components/Divider/Divider.stories.tsx +55 -0
  79. package/src/components/Divider/Divider.tsx +28 -0
  80. package/src/components/Divider/index.ts +2 -0
  81. package/src/components/EmptyState/EmptyState.module.css +8 -0
  82. package/src/components/EmptyState/EmptyState.stories.tsx +53 -0
  83. package/src/components/EmptyState/EmptyState.tsx +31 -0
  84. package/src/components/EmptyState/index.ts +2 -0
  85. package/src/components/ErrorBoundary/ErrorBoundary.module.css +18 -0
  86. package/src/components/ErrorBoundary/ErrorBoundary.stories.tsx +38 -0
  87. package/src/components/ErrorBoundary/ErrorBoundary.tsx +50 -0
  88. package/src/components/ErrorBoundary/index.ts +2 -0
  89. package/src/components/Fade/Fade.module.css +31 -0
  90. package/src/components/Fade/Fade.stories.tsx +35 -0
  91. package/src/components/Fade/Fade.tsx +33 -0
  92. package/src/components/Fade/index.ts +2 -0
  93. package/src/components/Field/Field.module.css +5 -0
  94. package/src/components/Field/Field.stories.tsx +111 -0
  95. package/src/components/Field/Field.tsx +22 -0
  96. package/src/components/Field/index.ts +2 -0
  97. package/src/components/Header/Header.module.css +33 -0
  98. package/src/components/Header/Header.stories.tsx +52 -0
  99. package/src/components/Header/Header.tsx +42 -0
  100. package/src/components/Header/index.ts +2 -0
  101. package/src/components/Input/Input.module.css +71 -0
  102. package/src/components/Input/Input.stories.tsx +135 -0
  103. package/src/components/Input/Input.tsx +76 -0
  104. package/src/components/Input/index.ts +2 -0
  105. package/src/components/LabelWrap/LabelWrap.module.css +34 -0
  106. package/src/components/LabelWrap/LabelWrap.tsx +32 -0
  107. package/src/components/LabelWrap/index.ts +1 -0
  108. package/src/components/Loader/Loader.module.css +9 -0
  109. package/src/components/Loader/Loader.stories.tsx +44 -0
  110. package/src/components/Loader/Loader.tsx +43 -0
  111. package/src/components/Loader/index.ts +2 -0
  112. package/src/components/Menu/DestructiveMenuItem.module.css +57 -0
  113. package/src/components/Menu/DestructiveMenuItem.tsx +64 -0
  114. package/src/components/Menu/Menu.module.css +49 -0
  115. package/src/components/Menu/Menu.stories.tsx +129 -0
  116. package/src/components/Menu/Menu.tsx +269 -0
  117. package/src/components/Menu/MenuItem.module.css +84 -0
  118. package/src/components/Menu/MenuItem.tsx +101 -0
  119. package/src/components/Menu/index.ts +6 -0
  120. package/src/components/Modal/Modal.module.css +67 -0
  121. package/src/components/Modal/Modal.stories.tsx +71 -0
  122. package/src/components/Modal/Modal.tsx +122 -0
  123. package/src/components/Modal/index.ts +2 -0
  124. package/src/components/Pagination/Pagination.module.css +64 -0
  125. package/src/components/Pagination/Pagination.stories.tsx +51 -0
  126. package/src/components/Pagination/Pagination.tsx +103 -0
  127. package/src/components/Pagination/index.ts +2 -0
  128. package/src/components/Radio/Radio.module.css +61 -0
  129. package/src/components/Radio/Radio.stories.tsx +101 -0
  130. package/src/components/Radio/Radio.tsx +108 -0
  131. package/src/components/Radio/index.ts +2 -0
  132. package/src/components/Scrollbar/Scrollbar.module.css +81 -0
  133. package/src/components/Scrollbar/Scrollbar.stories.tsx +46 -0
  134. package/src/components/Scrollbar/Scrollbar.tsx +187 -0
  135. package/src/components/Scrollbar/index.ts +2 -0
  136. package/src/components/Select/Select.module.css +31 -0
  137. package/src/components/Select/Select.stories.tsx +59 -0
  138. package/src/components/Select/Select.tsx +92 -0
  139. package/src/components/Select/index.ts +2 -0
  140. package/src/components/Sidebar/Sidebar.module.css +161 -0
  141. package/src/components/Sidebar/Sidebar.stories.tsx +125 -0
  142. package/src/components/Sidebar/Sidebar.tsx +129 -0
  143. package/src/components/Sidebar/SidebarItem.tsx +57 -0
  144. package/src/components/Sidebar/SidebarSection.tsx +42 -0
  145. package/src/components/Sidebar/index.ts +6 -0
  146. package/src/components/Skeleton/Skeleton.module.css +53 -0
  147. package/src/components/Skeleton/Skeleton.stories.tsx +89 -0
  148. package/src/components/Skeleton/Skeleton.tsx +66 -0
  149. package/src/components/Skeleton/index.ts +2 -0
  150. package/src/components/Slider/Slider.module.css +62 -0
  151. package/src/components/Slider/Slider.stories.tsx +120 -0
  152. package/src/components/Slider/Slider.tsx +74 -0
  153. package/src/components/Slider/index.ts +2 -0
  154. package/src/components/Surface/Surface.module.css +222 -0
  155. package/src/components/Surface/Surface.stories.tsx +213 -0
  156. package/src/components/Surface/Surface.tsx +219 -0
  157. package/src/components/Surface/index.ts +12 -0
  158. package/src/components/Tab/Tab.module.css +66 -0
  159. package/src/components/Tab/Tab.stories.tsx +103 -0
  160. package/src/components/Tab/Tab.tsx +41 -0
  161. package/src/components/Tab/index.ts +2 -0
  162. package/src/components/TabGroup/TabGroup.module.css +26 -0
  163. package/src/components/TabGroup/TabGroup.stories.tsx +60 -0
  164. package/src/components/TabGroup/TabGroup.tsx +83 -0
  165. package/src/components/TabGroup/index.ts +2 -0
  166. package/src/components/Table/Table.module.css +130 -0
  167. package/src/components/Table/Table.stories.tsx +111 -0
  168. package/src/components/Table/Table.tsx +226 -0
  169. package/src/components/Table/index.ts +11 -0
  170. package/src/components/Tag/Tag.module.css +270 -0
  171. package/src/components/Tag/Tag.stories.tsx +399 -0
  172. package/src/components/Tag/Tag.tsx +162 -0
  173. package/src/components/Tag/index.ts +2 -0
  174. package/src/components/Text/Text.module.css +166 -0
  175. package/src/components/Text/Text.stories.tsx +212 -0
  176. package/src/components/Text/Text.tsx +181 -0
  177. package/src/components/Text/index.ts +13 -0
  178. package/src/components/Toast/Toast.module.css +91 -0
  179. package/src/components/Toast/Toast.stories.tsx +66 -0
  180. package/src/components/Toast/Toast.tsx +182 -0
  181. package/src/components/Toast/index.ts +8 -0
  182. package/src/components/Toggle/Toggle.module.css +87 -0
  183. package/src/components/Toggle/Toggle.stories.tsx +85 -0
  184. package/src/components/Toggle/Toggle.tsx +52 -0
  185. package/src/components/Toggle/index.ts +2 -0
  186. package/src/components/ToggleButton/ToggleButton.module.css +172 -0
  187. package/src/components/ToggleButton/ToggleButton.stories.tsx +299 -0
  188. package/src/components/ToggleButton/ToggleButton.tsx +118 -0
  189. package/src/components/ToggleButton/index.ts +2 -0
  190. package/src/components/ToggleButtonGroup/ToggleButtonGroup.module.css +163 -0
  191. package/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx +341 -0
  192. package/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx +111 -0
  193. package/src/components/ToggleButtonGroup/index.ts +2 -0
  194. package/src/components/Tooltip/Tooltip.module.css +45 -0
  195. package/src/components/Tooltip/Tooltip.stories.tsx +46 -0
  196. package/src/components/Tooltip/Tooltip.tsx +224 -0
  197. package/src/components/Tooltip/index.ts +2 -0
  198. package/src/css-modules.d.ts +4 -0
  199. package/src/docs/ColorUsage.mdx +195 -0
  200. package/src/docs/ElevationUsage.mdx +95 -0
  201. package/src/docs/Introduction.mdx +97 -0
  202. package/src/docs/OpticalSizing.mdx +178 -0
  203. package/src/docs/TypographyUsage.mdx +121 -0
  204. package/src/index.ts +154 -0
  205. package/src/stories/Colors.stories.tsx +528 -0
  206. package/src/stories/Dimensions.stories.tsx +200 -0
  207. package/src/stories/Effects.stories.tsx +231 -0
  208. package/src/stories/Icons.stories.tsx +159 -0
  209. package/src/stories/Typography.stories.tsx +157 -0
  210. package/src/types/icons.ts +15 -0
  211. package/src/types/index.ts +1 -0
  212. package/src/utils/cn.ts +6 -0
  213. package/src/utils/css-tokens.ts +22 -0
@@ -0,0 +1,181 @@
1
+ import React, { forwardRef } from 'react';
2
+ import styles from './Text.module.css';
3
+
4
+ /**
5
+ * Text variant options.
6
+ * Each variant maps to a TokoMo typography class with predefined font-size, line-height,
7
+ * font-weight, and letter-spacing.
8
+ */
9
+ export type TextVariant =
10
+ | 'text-display-medium'
11
+ | 'text-display-small'
12
+ | 'text-title-large'
13
+ | 'text-title-medium'
14
+ | 'text-title-small'
15
+ | 'text-body-large'
16
+ | 'text-body-large-emphasis'
17
+ | 'text-body-medium'
18
+ | 'text-body-medium-emphasis'
19
+ | 'text-body-small'
20
+ | 'text-body-small-emphasis'
21
+ | 'text-caption'
22
+ | 'text-caption-emphasis';
23
+
24
+ /** @deprecated Use `TextVariant` instead. */
25
+ export type TextStyle = TextVariant;
26
+
27
+ /**
28
+ * Semantic color tokens for text.
29
+ * Maps to TokoMo --color-foreground-* CSS custom properties.
30
+ */
31
+ export type TextColorToken =
32
+ | 'primary'
33
+ | 'secondary'
34
+ | 'tertiary'
35
+ | 'brand'
36
+ | 'negative'
37
+ | 'positive'
38
+ | 'warning'
39
+ | 'caution'
40
+ | 'ai'
41
+ | 'on-strong'
42
+ | 'on-bold'
43
+ | 'inherit';
44
+
45
+ /** Text color — semantic token or CSS variable escape hatch. */
46
+ export type TextColor = TextColorToken | `var(--${string})`;
47
+
48
+ export type TextDecoration = 'none' | 'underline' | 'dotted-underline';
49
+ export type TextAlign = 'left' | 'center' | 'right';
50
+ export type LineTruncation = 1 | 2 | 3 | 4 | 5 | 'none';
51
+ export type TextWrap = 'wrap' | 'nowrap' | 'balance' | 'pretty';
52
+ export type TextElement = 'p' | 'span' | 'div' | 'label' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
53
+
54
+ export interface TextProps {
55
+ /** Typography variant. Maps to a TokoMo typography class. Defaults to 'text-body-medium'. */
56
+ variant?: TextVariant;
57
+ children: React.ReactNode;
58
+
59
+ // Appearance
60
+ color?: TextColor;
61
+ decoration?: TextDecoration;
62
+ italic?: boolean;
63
+ align?: TextAlign;
64
+
65
+ // Truncation / wrap
66
+ /** Max lines before ellipsis. Ignored when wrap is 'nowrap'. Defaults to 'none'. */
67
+ lineTruncation?: LineTruncation;
68
+ /** 'nowrap' = single line, truncates with ellipsis when constrained. */
69
+ wrap?: TextWrap;
70
+
71
+ // Semantic HTML
72
+ as?: TextElement;
73
+ id?: string;
74
+ htmlFor?: string;
75
+
76
+ // Escape hatches
77
+ className?: string;
78
+ style?: React.CSSProperties;
79
+ }
80
+
81
+ const colorClassMap: Record<TextColorToken, string> = {
82
+ primary: styles.colorPrimary,
83
+ secondary: styles.colorSecondary,
84
+ tertiary: styles.colorTertiary,
85
+ brand: styles.colorBrand,
86
+ negative: styles.colorNegative,
87
+ positive: styles.colorPositive,
88
+ warning: styles.colorWarning,
89
+ caution: styles.colorCaution,
90
+ ai: styles.colorAi,
91
+ 'on-strong': styles.colorOnStrong,
92
+ 'on-bold': styles.colorOnBold,
93
+ inherit: styles.colorInherit,
94
+ };
95
+
96
+ const decorationClassMap: Record<TextDecoration, string> = {
97
+ none: styles.decorationNone,
98
+ underline: styles.decorationUnderline,
99
+ 'dotted-underline': styles.decorationDottedUnderline,
100
+ };
101
+
102
+ const alignClassMap: Record<TextAlign, string> = {
103
+ left: styles.alignLeft,
104
+ center: styles.alignCenter,
105
+ right: styles.alignRight,
106
+ };
107
+
108
+ const truncationClassMap: Record<LineTruncation, string> = {
109
+ 1: styles.truncate1Line,
110
+ 2: styles.truncate2Lines,
111
+ 3: styles.truncate3Lines,
112
+ 4: styles.truncate4Lines,
113
+ 5: styles.truncate5Lines,
114
+ none: styles.noTruncation,
115
+ };
116
+
117
+ const isCustomColor = (color: TextColor): color is `var(--${string})` =>
118
+ color.startsWith('var(--');
119
+
120
+ export const Text = forwardRef<HTMLElement, TextProps>(
121
+ (
122
+ {
123
+ variant = 'text-body-medium',
124
+ children,
125
+ color,
126
+ decoration,
127
+ italic,
128
+ align,
129
+ lineTruncation = 'none',
130
+ wrap,
131
+ as: Component = 'p',
132
+ id,
133
+ htmlFor,
134
+ className = '',
135
+ style,
136
+ },
137
+ ref
138
+ ) => {
139
+ const wrapClass =
140
+ wrap === 'nowrap' ? styles.wrapNowrap
141
+ : wrap === 'balance' ? styles.wrapBalance
142
+ : wrap === 'pretty' ? styles.wrapPretty
143
+ : '';
144
+ const truncationClass =
145
+ wrap === 'nowrap' ? '' : truncationClassMap[lineTruncation];
146
+
147
+ const classes = [
148
+ variant,
149
+ styles.text,
150
+ wrapClass,
151
+ truncationClass,
152
+ color && !isCustomColor(color) ? colorClassMap[color] : '',
153
+ decoration ? decorationClassMap[decoration] : '',
154
+ italic ? styles.italic : '',
155
+ align ? alignClassMap[align] : '',
156
+ className,
157
+ ]
158
+ .filter(Boolean)
159
+ .join(' ');
160
+
161
+ const computedStyle: React.CSSProperties = {
162
+ ...style,
163
+ ...(color && isCustomColor(color) ? { color } : {}),
164
+ };
165
+
166
+ const props: React.HTMLAttributes<HTMLElement> & { htmlFor?: string } = {
167
+ className: classes,
168
+ style: Object.keys(computedStyle).length > 0 ? computedStyle : undefined,
169
+ ...(id ? { id } : {}),
170
+ ...(Component === 'label' && htmlFor ? { htmlFor } : {}),
171
+ };
172
+
173
+ return (
174
+ <Component ref={ref as React.Ref<never>} {...props}>
175
+ {children}
176
+ </Component>
177
+ );
178
+ }
179
+ );
180
+
181
+ Text.displayName = 'Text';
@@ -0,0 +1,13 @@
1
+ export { Text } from './Text';
2
+ export type {
3
+ TextProps,
4
+ TextVariant,
5
+ TextStyle,
6
+ TextColor,
7
+ TextColorToken,
8
+ TextDecoration,
9
+ TextAlign,
10
+ LineTruncation,
11
+ TextWrap,
12
+ TextElement,
13
+ } from './Text';
@@ -0,0 +1,91 @@
1
+ /* ─── Container ─────────────────────────────────────────────────────────────── */
2
+
3
+ .container {
4
+ position: fixed;
5
+ z-index: 9999;
6
+ display: flex;
7
+ flex-direction: column;
8
+ gap: var(--dimension-space-075);
9
+ pointer-events: none;
10
+ padding: var(--dimension-space-100);
11
+ max-width: 480px;
12
+ width: 100%;
13
+ box-sizing: border-box;
14
+ }
15
+
16
+ .topCenter {
17
+ top: 0;
18
+ left: 50%;
19
+ transform: translateX(-50%);
20
+ align-items: center;
21
+ }
22
+
23
+ .topRight {
24
+ top: 0;
25
+ right: 0;
26
+ align-items: flex-end;
27
+ }
28
+
29
+ .bottomCenter {
30
+ bottom: 0;
31
+ left: 50%;
32
+ transform: translateX(-50%);
33
+ align-items: center;
34
+ }
35
+
36
+ .bottomRight {
37
+ bottom: 0;
38
+ right: 0;
39
+ align-items: flex-end;
40
+ }
41
+
42
+ /* ─── Toast ─────────────────────────────────────────────────────────────────── */
43
+
44
+ .toast {
45
+ display: flex;
46
+ align-items: center;
47
+ gap: var(--dimension-space-075);
48
+ padding: var(--dimension-space-075) var(--dimension-space-100);
49
+ min-height: var(--dimension-size-400);
50
+ pointer-events: auto;
51
+ animation: slideIn var(--effect-motion-short-3) ease-out;
52
+ }
53
+
54
+ .toastExit {
55
+ animation: fadeOut 200ms ease-in forwards;
56
+ }
57
+
58
+ .dismiss {
59
+ background: none;
60
+ border: none;
61
+ color: inherit;
62
+ cursor: pointer;
63
+ padding: var(--dimension-space-025);
64
+ margin-left: auto;
65
+ font-size: 16px;
66
+ line-height: 1;
67
+ opacity: 0.7;
68
+ border-radius: var(--dimension-radius-050);
69
+ transition: opacity var(--effect-motion-short-2);
70
+ flex-shrink: 0;
71
+ }
72
+
73
+ .dismiss:hover { opacity: 1; }
74
+
75
+ /* ─── Animations ────────────────────────────────────────────────────────────── */
76
+
77
+ @keyframes slideIn {
78
+ from {
79
+ opacity: 0;
80
+ transform: translateY(-8px);
81
+ }
82
+ to {
83
+ opacity: 1;
84
+ transform: translateY(0);
85
+ }
86
+ }
87
+
88
+ @keyframes fadeOut {
89
+ from { opacity: 1; }
90
+ to { opacity: 0; }
91
+ }
@@ -0,0 +1,66 @@
1
+ import React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react-vite';
3
+ import { ToastContainer, toast } from './Toast';
4
+ import type { ToastIntent } from './Toast';
5
+
6
+ const meta: Meta<typeof ToastContainer> = {
7
+ title: 'Classic/Toast',
8
+ component: ToastContainer,
9
+ args: {
10
+ position: 'top-center',
11
+ },
12
+ argTypes: {
13
+ position: { control: 'select', options: ['top-center', 'top-right', 'bottom-center', 'bottom-right'] },
14
+ },
15
+ };
16
+
17
+ export default meta;
18
+ type Story = StoryObj<typeof ToastContainer>;
19
+
20
+ const INTENTS: ToastIntent[] = ['neutral', 'brand', 'positive', 'negative', 'warning', 'caution'];
21
+
22
+ const row: React.CSSProperties = { display: 'flex', gap: 8, flexWrap: 'wrap' };
23
+
24
+ const ToastDemo = (props: React.ComponentProps<typeof ToastContainer>) => (
25
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
26
+ <div style={row}>
27
+ <button onClick={() => toast.info('This is an info message.')}>Info</button>
28
+ <button onClick={() => toast.success('Operation completed successfully.')}>Success</button>
29
+ <button onClick={() => toast.error('Something went wrong.')}>Error</button>
30
+ <button onClick={() => toast.warning('Please check your input.')}>Warning</button>
31
+ </div>
32
+ <div style={row}>
33
+ {INTENTS.map(intent => (
34
+ <button
35
+ key={intent}
36
+ onClick={() => toast.show({ message: `Toast with ${intent} intent`, intent })}
37
+ >
38
+ {intent}
39
+ </button>
40
+ ))}
41
+ </div>
42
+ <div style={row}>
43
+ <button onClick={() => toast.show({ message: 'Persistent toast (no auto-dismiss)', intent: 'brand', duration: 0 })}>
44
+ No auto-dismiss
45
+ </button>
46
+ <button onClick={() => toast.dismissAll()}>Dismiss all</button>
47
+ </div>
48
+ <ToastContainer {...props} />
49
+ </div>
50
+ );
51
+
52
+ export const Playground: Story = {
53
+ render: args => <ToastDemo {...args} />,
54
+ };
55
+
56
+ export const Default: Story = {
57
+ render: () => <ToastDemo />,
58
+ };
59
+
60
+ export const TopRight: Story = {
61
+ render: () => <ToastDemo position="top-right" />,
62
+ };
63
+
64
+ export const BottomCenter: Story = {
65
+ render: () => <ToastDemo position="bottom-center" />,
66
+ };
@@ -0,0 +1,182 @@
1
+ import React, { forwardRef, useEffect, useState, useCallback, useRef, useSyncExternalStore } from 'react';
2
+ import ReactDOM from 'react-dom';
3
+ import { cn } from '@/utils/cn';
4
+ import { Text } from '@/components/Text';
5
+ import { Surface } from '@/components/Surface';
6
+ import type { SurfaceIntent } from '@/components/Surface';
7
+ import styles from './Toast.module.css';
8
+
9
+ /* ─── Toast Types ───────────────────────────────────────────────────────────── */
10
+
11
+ export type ToastIntent = 'neutral' | 'brand' | 'positive' | 'negative' | 'warning' | 'caution';
12
+ export type ToastPosition = 'top-center' | 'top-right' | 'bottom-center' | 'bottom-right';
13
+
14
+ export interface ToastData {
15
+ id: string;
16
+ message: string;
17
+ intent?: ToastIntent;
18
+ duration?: number;
19
+ onDismiss?: () => void;
20
+ }
21
+
22
+ export interface ToastOptions {
23
+ message: string;
24
+ intent?: ToastIntent;
25
+ /** Auto-dismiss duration in ms. 0 = no auto-dismiss. Default 4000. */
26
+ duration?: number;
27
+ onDismiss?: () => void;
28
+ }
29
+
30
+ /* ─── Toast Store (external, framework-agnostic) ────────────────────────────── */
31
+
32
+ let toasts: ToastData[] = [];
33
+ const listeners = new Set<() => void>();
34
+
35
+ function emit() {
36
+ listeners.forEach(fn => fn());
37
+ }
38
+
39
+ function getSnapshot(): ToastData[] {
40
+ return toasts;
41
+ }
42
+
43
+ function subscribe(listener: () => void) {
44
+ listeners.add(listener);
45
+ return () => { listeners.delete(listener); };
46
+ }
47
+
48
+ let idCounter = 0;
49
+
50
+ export const toast = {
51
+ show(options: ToastOptions): string {
52
+ const id = `toast-${++idCounter}`;
53
+ toasts = [...toasts, { id, ...options }];
54
+ emit();
55
+ return id;
56
+ },
57
+ info(message: string, options?: Omit<ToastOptions, 'message'>): string {
58
+ return toast.show({ message, intent: 'neutral', ...options });
59
+ },
60
+ success(message: string, options?: Omit<ToastOptions, 'message'>): string {
61
+ return toast.show({ message, intent: 'positive', ...options });
62
+ },
63
+ error(message: string, options?: Omit<ToastOptions, 'message'>): string {
64
+ return toast.show({ message, intent: 'negative', ...options });
65
+ },
66
+ warning(message: string, options?: Omit<ToastOptions, 'message'>): string {
67
+ return toast.show({ message, intent: 'warning', ...options });
68
+ },
69
+ dismiss(id: string) {
70
+ toasts = toasts.filter(t => t.id !== id);
71
+ emit();
72
+ },
73
+ dismissAll() {
74
+ toasts = [];
75
+ emit();
76
+ },
77
+ };
78
+
79
+ /* ─── useToasts hook ────────────────────────────────────────────────────────── */
80
+
81
+ export function useToasts(): ToastData[] {
82
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
83
+ }
84
+
85
+ /* ─── Single Toast ──────────────────────────────────────────────────────────── */
86
+
87
+ const INTENT_MAP: Record<ToastIntent, SurfaceIntent> = {
88
+ neutral: 'neutral',
89
+ brand: 'brand',
90
+ positive: 'positive',
91
+ negative: 'negative',
92
+ warning: 'warning',
93
+ caution: 'caution',
94
+ };
95
+
96
+ interface ToastItemProps {
97
+ data: ToastData;
98
+ }
99
+
100
+ const ToastItem: React.FC<ToastItemProps> = ({ data }) => {
101
+ const [exiting, setExiting] = useState(false);
102
+ const timerRef = useRef<ReturnType<typeof setTimeout>>();
103
+
104
+ const dismiss = useCallback(() => {
105
+ setExiting(true);
106
+ setTimeout(() => {
107
+ toast.dismiss(data.id);
108
+ data.onDismiss?.();
109
+ }, 200);
110
+ }, [data]);
111
+
112
+ useEffect(() => {
113
+ const duration = data.duration ?? 4000;
114
+ if (duration > 0) {
115
+ timerRef.current = setTimeout(dismiss, duration);
116
+ }
117
+ return () => { if (timerRef.current) clearTimeout(timerRef.current); };
118
+ }, [data.duration, dismiss]);
119
+
120
+ return (
121
+ <Surface
122
+ intent={INTENT_MAP[data.intent ?? 'neutral']}
123
+ contrast="bold"
124
+ elevation="floating"
125
+ radius="lg"
126
+ className={cn(styles.toast, exiting && styles.toastExit)}
127
+ role="status"
128
+ aria-live="polite"
129
+ >
130
+ <Text variant="text-body-medium" as="span" color="inherit">
131
+ {data.message}
132
+ </Text>
133
+ <button
134
+ type="button"
135
+ className={styles.dismiss}
136
+ onClick={dismiss}
137
+ aria-label="Dismiss"
138
+ >
139
+ &times;
140
+ </button>
141
+ </Surface>
142
+ );
143
+ };
144
+
145
+ /* ─── ToastContainer ────────────────────────────────────────────────────────── */
146
+
147
+ export interface ToastContainerProps {
148
+ /** Where to render toasts on screen. */
149
+ position?: ToastPosition;
150
+ }
151
+
152
+ export const ToastContainer = forwardRef<HTMLDivElement, ToastContainerProps>(
153
+ ({ position = 'top-center' }, ref) => {
154
+ const items = useToasts();
155
+
156
+ if (typeof document === 'undefined') return null;
157
+
158
+ return ReactDOM.createPortal(
159
+ <div
160
+ ref={ref}
161
+ className={cn(styles.container, styles[positionClass(position)])}
162
+ aria-label="Notifications"
163
+ >
164
+ {items.map(item => (
165
+ <ToastItem key={item.id} data={item} />
166
+ ))}
167
+ </div>,
168
+ document.body
169
+ );
170
+ }
171
+ );
172
+
173
+ ToastContainer.displayName = 'ToastContainer';
174
+
175
+ function positionClass(position: ToastPosition): string {
176
+ switch (position) {
177
+ case 'top-center': return 'topCenter';
178
+ case 'top-right': return 'topRight';
179
+ case 'bottom-center': return 'bottomCenter';
180
+ case 'bottom-right': return 'bottomRight';
181
+ }
182
+ }
@@ -0,0 +1,8 @@
1
+ export { ToastContainer, toast, useToasts } from './Toast';
2
+ export type {
3
+ ToastContainerProps,
4
+ ToastData,
5
+ ToastOptions,
6
+ ToastIntent,
7
+ ToastPosition,
8
+ } from './Toast';
@@ -0,0 +1,87 @@
1
+ .toggle {
2
+ position: relative;
3
+ display: inline-flex;
4
+ align-items: center;
5
+ justify-content: flex-start;
6
+ width: var(--dimension-size-400);
7
+ height: var(--dimension-size-250);
8
+ padding: 0 var(--dimension-space-025);
9
+ border: none;
10
+ border-radius: var(--dimension-radius-half);
11
+ cursor: pointer;
12
+ -webkit-user-select: none;
13
+ user-select: none;
14
+ outline: none;
15
+ flex-shrink: 0;
16
+ background-color: var(--color-background-medium-neutral);
17
+ box-shadow: var(--effect-highlight-depressed-sm);
18
+ transition: background-color var(--effect-motion-short-2);
19
+ }
20
+
21
+ .toggle::before {
22
+ content: '';
23
+ position: absolute;
24
+ inset: 0;
25
+ border-radius: inherit;
26
+ box-shadow: var(--effect-shadow-depressed-sm);
27
+ pointer-events: none;
28
+ }
29
+
30
+ .toggle:focus-visible {
31
+ outline: var(--dimension-stroke-width-025) solid var(--color-interaction-focus);
32
+ outline-offset: var(--dimension-space-025);
33
+ }
34
+
35
+ .checked {
36
+ background-color: var(--color-foreground-bold-brand) !important;
37
+ }
38
+
39
+ .thumb {
40
+ position: relative;
41
+ flex-shrink: 0;
42
+ width: var(--dimension-size-200);
43
+ height: var(--dimension-size-200);
44
+ background-color: var(--color-background-primary);
45
+ border-radius: var(--dimension-radius-half);
46
+ box-shadow: var(--effect-elevation-elevated-sm);
47
+ transition: transform var(--effect-motion-short-2);
48
+ transform: translateX(0);
49
+ z-index: 1;
50
+ }
51
+
52
+ .thumb::after {
53
+ content: '';
54
+ position: absolute;
55
+ inset: 0;
56
+ background-color: transparent;
57
+ border-radius: inherit;
58
+ transition: var(--effect-transition-interaction-background-instant);
59
+ pointer-events: none;
60
+ }
61
+
62
+ .toggle:hover:not(.inactive) .thumb::after {
63
+ background-color: var(--color-interaction-hover);
64
+ }
65
+
66
+ .toggle:active:not(.inactive) .thumb::after {
67
+ background-color: var(--color-interaction-pressed);
68
+ }
69
+
70
+ .checked .thumb {
71
+ transform: translateX(var(--dimension-offset-150));
72
+ }
73
+
74
+ .toggle:active:not(.inactive) .thumb {
75
+ transform: translateX(0) scale(var(--dimension-scale-subtle)) !important;
76
+ transition: transform var(--effect-motion-instant);
77
+ }
78
+
79
+ .checked.toggle:active:not(.inactive) .thumb {
80
+ transform: translateX(var(--dimension-offset-150)) scale(var(--dimension-scale-subtle)) !important;
81
+ transition: transform var(--effect-motion-instant);
82
+ }
83
+
84
+ .inactive {
85
+ cursor: not-allowed;
86
+ opacity: 0.5;
87
+ }