@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,270 @@
1
+ /* ==========================================================================
2
+ Tag — hover/pressed via direct background-color + color-mix().
3
+ No pseudo-element overlays.
4
+ ========================================================================== */
5
+
6
+ .tag {
7
+ display: inline-flex;
8
+ align-items: center;
9
+ justify-content: center;
10
+ border-radius: var(--dimension-radius-050);
11
+ box-sizing: border-box;
12
+ color: var(--color-foreground-primary);
13
+ position: relative;
14
+
15
+ /* Interaction — ghost defaults */
16
+ --_hover-bg: var(--color-interaction-hover);
17
+ --_pressed-bg: var(--color-interaction-pressed);
18
+ }
19
+
20
+ /* ==========================================================================
21
+ Sizes
22
+ md 28px — no size-350 token, expressed as 7/2 × base
23
+ sm 20px — size-250
24
+ xs 12px — size-150
25
+ ========================================================================== */
26
+
27
+ .sizeMd { height: calc(var(--dimension-size-base) * 7 / 2); padding: 0 var(--dimension-space-075); gap: 0; }
28
+ .sizeSm { height: var(--dimension-size-250); padding: 0 var(--dimension-space-050); gap: 0; }
29
+ .sizeXs { height: var(--dimension-size-150); padding: 0 var(--dimension-space-012); gap: 0; }
30
+
31
+ /* ==========================================================================
32
+ Rounded
33
+ ========================================================================== */
34
+
35
+ .rounded { border-radius: var(--dimension-radius-half); }
36
+
37
+ /*
38
+ * Pill padding correction — add space-050 to sides that have no content anchor.
39
+ * Applied by Tag.tsx based on whether a leading icon or trailing remove is present:
40
+ * no leading icon → roundedLeft{Size} (left side is bare label text)
41
+ * no trailing icon → roundedRight{Size} (right side is bare label text)
42
+ * Placed after size rules so same-specificity cascade wins correctly.
43
+ */
44
+
45
+ /* MD — base: space-075 */
46
+ .roundedLeftMD { padding-left: calc(var(--dimension-space-075) + var(--dimension-space-050)); }
47
+ .roundedRightMD { padding-right: calc(var(--dimension-space-075) + var(--dimension-space-050)); }
48
+
49
+ /* SM — base: space-050 */
50
+ .roundedLeftSM { padding-left: calc(var(--dimension-space-050) + var(--dimension-space-050)); }
51
+ .roundedRightSM { padding-right: calc(var(--dimension-space-050) + var(--dimension-space-050)); }
52
+
53
+ /* XS — base: space-012 (1px) */
54
+ .roundedLeftXS { padding-left: calc(var(--dimension-space-012) + var(--dimension-space-050)); }
55
+ .roundedRightXS { padding-right: calc(var(--dimension-space-012) + var(--dimension-space-050)); }
56
+
57
+ /*
58
+ * Icon-edge padding correction — when an icon (leading or remove) sits on a
59
+ * side, that side's padding should match the top/bottom optical gap:
60
+ * (height − icon_size) / 2
61
+ *
62
+ * md: (28 − 20) / 2 = 4px → space-050
63
+ * sm: (20 − 16) / 2 = 2px → space-025
64
+ * xs: (12 − 12) / 2 = 0px → 0
65
+ *
66
+ * Applied by Tag.tsx: iconLeft when Icon present, iconRight when removable.
67
+ * Placed after size + rounded rules so same-specificity cascade wins.
68
+ */
69
+ .iconLeftMD { padding-left: var(--dimension-space-050); }
70
+ .iconRightMD { padding-right: var(--dimension-space-050); }
71
+
72
+ .iconLeftSM { padding-left: var(--dimension-space-025); }
73
+ .iconRightSM { padding-right: var(--dimension-space-025); }
74
+
75
+ .iconLeftXS { padding-left: 0; }
76
+ .iconRightXS { padding-right: 0; }
77
+
78
+ /* ==========================================================================
79
+ Background context — parent surface awareness
80
+ ========================================================================== */
81
+
82
+ .onMedium {
83
+ --_hover-bg: var(--color-interaction-on-medium-background-hover);
84
+ --_pressed-bg: var(--color-interaction-on-medium-background-pressed);
85
+ }
86
+
87
+ .onBold {
88
+ --_hover-bg: var(--color-interaction-on-bold-background-hover);
89
+ --_pressed-bg: var(--color-interaction-on-bold-background-pressed);
90
+ }
91
+
92
+ .onStrong {
93
+ --_hover-bg: var(--color-interaction-on-strong-background-hover);
94
+ --_pressed-bg: var(--color-interaction-on-strong-background-pressed);
95
+ }
96
+
97
+ .onAlwaysDark {
98
+ --_hover-bg: var(--color-always-dark-interaction-hover);
99
+ --_pressed-bg: var(--color-always-dark-interaction-pressed);
100
+ }
101
+
102
+ /* ==========================================================================
103
+ Elevation
104
+ none — filled intent bg, no border, no shadow (default)
105
+ flat — transparent bg + intent-coloured border ring
106
+ elevated — filled intent bg + elevated shadow
107
+ ========================================================================== */
108
+
109
+ /* elevationNone — no extra chrome needed, intent+contrast classes handle fill */
110
+
111
+ .elevationFlat {
112
+ background-color: transparent !important;
113
+ box-shadow: inset 0 0 0 var(--dimension-stroke-width-015) currentColor;
114
+ }
115
+
116
+ /* Flat: no background — use on-surface fg tokens matched to the contrast level.
117
+ The border ring is currentColor so it follows text color automatically.
118
+ 3-class specificity beats the 2-class intent+contrast filled rules above. */
119
+
120
+ .elevationFlat.intentNeutral.contrastBold { color: var(--color-foreground-bold-neutral); }
121
+ .elevationFlat.intentBrand.contrastBold { color: var(--color-foreground-bold-brand); }
122
+ .elevationFlat.intentAi.contrastBold { color: var(--color-foreground-bold-ai); }
123
+ .elevationFlat.intentNegative.contrastBold { color: var(--color-foreground-bold-negative); }
124
+ .elevationFlat.intentWarning.contrastBold { color: var(--color-foreground-bold-warning); }
125
+ .elevationFlat.intentCaution.contrastBold { color: var(--color-foreground-bold-caution); }
126
+ .elevationFlat.intentPositive.contrastBold { color: var(--color-foreground-bold-positive); }
127
+
128
+ .elevationFlat.intentNeutral.contrastStrong { color: var(--color-foreground-strong-neutral); }
129
+ .elevationFlat.intentBrand.contrastStrong { color: var(--color-foreground-strong-brand); }
130
+ .elevationFlat.intentAi.contrastStrong { color: var(--color-foreground-strong-ai); }
131
+ .elevationFlat.intentNegative.contrastStrong { color: var(--color-foreground-strong-negative); }
132
+ .elevationFlat.intentWarning.contrastStrong { color: var(--color-foreground-strong-warning); }
133
+ .elevationFlat.intentCaution.contrastStrong { color: var(--color-foreground-strong-caution); }
134
+ .elevationFlat.intentPositive.contrastStrong { color: var(--color-foreground-strong-positive); }
135
+
136
+ .elevationFlat.intentNeutral.contrastMedium { color: var(--color-foreground-medium-neutral); }
137
+ .elevationFlat.intentBrand.contrastMedium { color: var(--color-foreground-medium-brand); }
138
+ .elevationFlat.intentAi.contrastMedium { color: var(--color-foreground-medium-ai); }
139
+ .elevationFlat.intentNegative.contrastMedium { color: var(--color-foreground-medium-negative); }
140
+ .elevationFlat.intentWarning.contrastMedium { color: var(--color-foreground-medium-warning); }
141
+ .elevationFlat.intentCaution.contrastMedium { color: var(--color-foreground-medium-caution); }
142
+ .elevationFlat.intentPositive.contrastMedium { color: var(--color-foreground-medium-positive); }
143
+
144
+ .elevationFlat.intentNeutral.contrastFaint { color: var(--color-foreground-faint-neutral); }
145
+ .elevationFlat.intentBrand.contrastFaint { color: var(--color-foreground-faint-brand); }
146
+ .elevationFlat.intentAi.contrastFaint { color: var(--color-foreground-faint-ai); }
147
+ .elevationFlat.intentNegative.contrastFaint { color: var(--color-foreground-faint-negative); }
148
+ .elevationFlat.intentWarning.contrastFaint { color: var(--color-foreground-faint-warning); }
149
+ .elevationFlat.intentCaution.contrastFaint { color: var(--color-foreground-faint-caution); }
150
+ .elevationFlat.intentPositive.contrastFaint { color: var(--color-foreground-faint-positive); }
151
+
152
+ .elevationElevated { box-shadow: var(--effect-elevation-elevated-sm); }
153
+
154
+ /* ==========================================================================
155
+ Intent + contrast — strong
156
+ ========================================================================== */
157
+
158
+ .intentNeutral.contrastStrong { background-color: var(--color-background-strong-neutral); color: var(--color-foreground-medium-neutral); }
159
+ .intentBrand.contrastStrong { background-color: var(--color-background-strong-brand); color: var(--color-foreground-medium-brand); }
160
+ .intentAi.contrastStrong { background-color: var(--color-background-strong-ai); color: var(--color-foreground-medium-ai); }
161
+ .intentNegative.contrastStrong { background-color: var(--color-background-strong-negative); color: var(--color-foreground-medium-negative); }
162
+ .intentWarning.contrastStrong { background-color: var(--color-background-strong-warning); color: var(--color-foreground-medium-warning); }
163
+ .intentCaution.contrastStrong { background-color: var(--color-background-strong-caution); color: var(--color-foreground-medium-caution); }
164
+ .intentPositive.contrastStrong { background-color: var(--color-background-strong-positive); color: var(--color-foreground-medium-positive); }
165
+
166
+ /* ==========================================================================
167
+ Intent + contrast — bold
168
+ ========================================================================== */
169
+
170
+ .intentNeutral.contrastBold { background-color: var(--color-background-bold-neutral); color: var(--color-foreground-on-bold-background-primary); }
171
+ .intentBrand.contrastBold { background-color: var(--color-background-bold-brand); color: var(--color-foreground-on-bold-background-primary); }
172
+ .intentAi.contrastBold { background-color: var(--color-background-bold-ai); color: var(--color-foreground-on-bold-background-primary); }
173
+ .intentNegative.contrastBold { background-color: var(--color-background-bold-negative); color: var(--color-foreground-on-bold-background-primary); }
174
+ .intentWarning.contrastBold { background-color: var(--color-background-bold-warning); color: var(--color-foreground-on-bold-background-primary); }
175
+ .intentCaution.contrastBold { background-color: var(--color-background-bold-caution); color: var(--color-foreground-on-bold-background-primary); }
176
+ .intentPositive.contrastBold { background-color: var(--color-background-bold-positive); color: var(--color-foreground-on-bold-background-primary); }
177
+
178
+ /* ==========================================================================
179
+ Intent + contrast — medium
180
+ ========================================================================== */
181
+
182
+ .intentNeutral.contrastMedium { background-color: var(--color-background-medium-neutral); color: var(--color-foreground-strong-neutral); }
183
+ .intentBrand.contrastMedium { background-color: var(--color-background-medium-brand); color: var(--color-foreground-strong-brand); }
184
+ .intentAi.contrastMedium { background-color: var(--color-background-medium-ai); color: var(--color-foreground-strong-ai); }
185
+ .intentNegative.contrastMedium { background-color: var(--color-background-medium-negative); color: var(--color-foreground-strong-negative); }
186
+ .intentWarning.contrastMedium { background-color: var(--color-background-medium-warning); color: var(--color-foreground-strong-warning); }
187
+ .intentCaution.contrastMedium { background-color: var(--color-background-medium-caution); color: var(--color-foreground-strong-caution); }
188
+ .intentPositive.contrastMedium { background-color: var(--color-background-medium-positive); color: var(--color-foreground-strong-positive); }
189
+
190
+ /* ==========================================================================
191
+ Intent + contrast — faint
192
+ ========================================================================== */
193
+
194
+ .intentNeutral.contrastFaint { background-color: var(--color-background-faint-neutral); color: var(--color-foreground-bold-neutral); }
195
+ .intentBrand.contrastFaint { background-color: var(--color-background-faint-brand); color: var(--color-foreground-bold-brand); }
196
+ .intentAi.contrastFaint { background-color: var(--color-background-faint-ai); color: var(--color-foreground-bold-ai); }
197
+ .intentNegative.contrastFaint { background-color: var(--color-background-faint-negative); color: var(--color-foreground-bold-negative); }
198
+ .intentWarning.contrastFaint { background-color: var(--color-background-faint-warning); color: var(--color-foreground-bold-warning); }
199
+ .intentCaution.contrastFaint { background-color: var(--color-background-faint-caution); color: var(--color-foreground-bold-caution); }
200
+ .intentPositive.contrastFaint { background-color: var(--color-background-faint-positive); color: var(--color-foreground-bold-positive); }
201
+
202
+ /* ==========================================================================
203
+ Interactive (clickable / filter-chip)
204
+ ========================================================================== */
205
+
206
+ .interactive {
207
+ cursor: pointer;
208
+ -webkit-user-select: none;
209
+ user-select: none;
210
+ outline: none;
211
+ }
212
+
213
+ .interactive:hover { background-color: var(--_hover-bg); }
214
+ .interactive:active { background-color: var(--_pressed-bg); }
215
+
216
+ .interactive:focus-visible {
217
+ outline: var(--dimension-stroke-width-025) solid var(--color-interaction-focus);
218
+ outline-offset: var(--dimension-space-025);
219
+ }
220
+
221
+ /* ==========================================================================
222
+ Pressed / selected (filter-chip active state)
223
+ Always brand — same language as ToggleButton
224
+ ========================================================================== */
225
+
226
+ .tag.pressed {
227
+ background-color: var(--color-background-faint-brand) !important;
228
+ color: var(--color-foreground-bold-brand) !important;
229
+ border-color: transparent !important;
230
+ }
231
+
232
+ /* ==========================================================================
233
+ Remove button — keeps ::before overlay for its own isolated hit area
234
+ ========================================================================== */
235
+
236
+ .removable { gap: var(--dimension-space-025); }
237
+ .sizeXs.removable { gap: 0; }
238
+
239
+ .removeButton {
240
+ padding: 0; margin: 0; border: none; background: none;
241
+ display: flex; align-items: center; justify-content: center;
242
+ cursor: pointer; color: inherit; flex-shrink: 0;
243
+ position: relative; border-radius: var(--dimension-radius-050);
244
+ }
245
+
246
+ .removeButton::before {
247
+ content: ''; position: absolute; inset: -2px;
248
+ border-radius: var(--dimension-radius-025);
249
+ background-color: transparent;
250
+ transition: var(--effect-transition-interaction-background-short-2);
251
+ pointer-events: none;
252
+ }
253
+
254
+ .removeButton:hover::before { background-color: var(--color-interaction-hover); }
255
+ .removeButton:active::before { background-color: var(--color-interaction-pressed); }
256
+
257
+ /* SM — 1px gap, inset -1px → 18×18 hover area */
258
+ .sizeSm .removeButton::before { inset: -1px; border-radius: 3px; }
259
+
260
+ /* XS — flush, inset 0 → 12×12 hover area matching tag radius */
261
+ .sizeXs .removeButton::before { inset: 0; border-radius: var(--dimension-radius-050); }
262
+
263
+ .rounded .removeButton,
264
+ .rounded .removeButton::before { border-radius: var(--dimension-radius-half); }
265
+
266
+ /* ==========================================================================
267
+ Disabled
268
+ ========================================================================== */
269
+
270
+ .inactive { opacity: 0.5; pointer-events: none; cursor: not-allowed; }
@@ -0,0 +1,399 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import React, { useState } from 'react';
3
+ import { Tag } from './Tag';
4
+ import { Surface } from '@/components/Surface';
5
+ import type { TagIntent, TagContrast, TagElevation, TagSize, TagBackground } from './Tag';
6
+ import type { IconComponent } from '@/types/icons';
7
+
8
+ const PlaceholderIcon: IconComponent = ({ size = 16 }) => (
9
+ <svg width={size} height={size} viewBox="0 0 20 20" fill="currentColor">
10
+ <circle cx="10" cy="10" r="6" />
11
+ </svg>
12
+ );
13
+
14
+ const meta: Meta<typeof Tag> = {
15
+ title: 'Primitives (Reviewed)/Tag',
16
+ component: Tag,
17
+ args: {
18
+ label: 'Tag',
19
+ intent: 'neutral',
20
+ contrast: 'faint',
21
+ elevation: 'none',
22
+ size: 'md',
23
+ rounded: false,
24
+ inactive: false,
25
+ },
26
+ argTypes: {
27
+ intent: { control: 'select', options: ['neutral', 'brand', 'ai', 'negative', 'warning', 'caution', 'positive'] },
28
+ contrast: { control: 'select', options: ['strong', 'bold', 'medium', 'faint'] },
29
+ elevation: { control: 'select', options: ['none', 'flat', 'elevated'] },
30
+ size: { control: 'select', options: ['md', 'sm', 'xs'] },
31
+ label: { control: 'text' },
32
+ rounded: { control: 'boolean' },
33
+ removable: { control: 'boolean' },
34
+ inactive: { control: 'boolean' },
35
+ icon: {
36
+ control: 'boolean',
37
+ mapping: { true: PlaceholderIcon, false: undefined },
38
+ },
39
+ },
40
+ };
41
+
42
+ export default meta;
43
+ type Story = StoryObj<typeof Tag>;
44
+
45
+ export const Playground: Story = {};
46
+
47
+ // ─── Matrix helpers ───────────────────────────────────────────────────────────
48
+
49
+ const INTENTS: TagIntent[] = ['neutral', 'brand', 'ai', 'negative', 'warning', 'caution', 'positive'];
50
+ const CONTRASTS: TagContrast[] = ['strong', 'bold', 'medium', 'faint'];
51
+ const ELEVATIONS: TagElevation[] = ['none', 'flat', 'elevated'];
52
+ const SIZES: TagSize[] = ['md', 'sm', 'xs'];
53
+
54
+ const col: React.CSSProperties = { display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'flex-start' };
55
+ const row: React.CSSProperties = { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' };
56
+ const lbl = (): React.CSSProperties => ({ fontSize: 10, fontFamily: 'monospace', color: '#888', minWidth: 80, flexShrink: 0 });
57
+ const section = (text: string) => (
58
+ <div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '0.05em', textTransform: 'uppercase', color: '#555', marginTop: 8 }}>
59
+ {text}
60
+ </div>
61
+ );
62
+ const divider = <hr style={{ border: 'none', borderTop: '1px solid #e0e0e0', margin: '16px 0' }} />;
63
+
64
+ export const Matrix: Story = {
65
+ parameters: { layout: 'padded' },
66
+ render: () => (
67
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 0, fontFamily: 'sans-serif' }}>
68
+
69
+ {/* ── Intents × Contrasts — none ── */}
70
+ {section('Intents × Contrasts — elevation: none')}
71
+ <div style={{ ...col, marginTop: 12 }}>
72
+ <div style={row}>
73
+ <span style={lbl()}></span>
74
+ {CONTRASTS.map(c => <span key={c} style={{ ...lbl(), minWidth: 90 }}>{c}</span>)}
75
+ </div>
76
+ {INTENTS.map(intent => (
77
+ <div key={intent} style={row}>
78
+ <span style={lbl()}>{intent}</span>
79
+ {CONTRASTS.map(contrast => (
80
+ <Tag key={contrast} label={intent.charAt(0).toUpperCase() + intent.slice(1)} intent={intent} contrast={contrast} />
81
+ ))}
82
+ </div>
83
+ ))}
84
+ </div>
85
+
86
+ {divider}
87
+
88
+ {/* ── Intents × Contrasts — flat ── */}
89
+ {section('Intents × Contrasts — elevation: flat')}
90
+ <div style={{ ...col, marginTop: 12 }}>
91
+ {INTENTS.map(intent => (
92
+ <div key={intent} style={row}>
93
+ <span style={lbl()}>{intent}</span>
94
+ {CONTRASTS.map(contrast => (
95
+ <Tag key={contrast} label={intent.charAt(0).toUpperCase() + intent.slice(1)} intent={intent} contrast={contrast} elevation="flat" />
96
+ ))}
97
+ </div>
98
+ ))}
99
+ </div>
100
+
101
+ {divider}
102
+
103
+ {/* ── Sizes ── */}
104
+ {section('Sizes')}
105
+ <div style={{ ...col, marginTop: 12 }}>
106
+ {SIZES.map(size => (
107
+ <div key={size} style={row}>
108
+ <span style={lbl()}>{size}</span>
109
+ <Tag label="Label" size={size} intent="brand" />
110
+ <Tag label="Label" size={size} intent="brand" icon={PlaceholderIcon} />
111
+ <Tag label="Label" size={size} intent="brand" removable onRemove={() => {}} />
112
+ <Tag label="Label" size={size} intent="brand" icon={PlaceholderIcon} removable onRemove={() => {}} />
113
+ </div>
114
+ ))}
115
+ </div>
116
+
117
+ {divider}
118
+
119
+ {/* ── Sizes × Elevation ── */}
120
+ {section('Sizes × Elevation')}
121
+ <div style={{ ...col, marginTop: 12 }}>
122
+ {SIZES.map(size => (
123
+ <div key={size} style={row}>
124
+ <span style={lbl()}>{size}</span>
125
+ {ELEVATIONS.map(elevation => (
126
+ <Tag key={elevation} label={elevation} size={size} intent="brand" elevation={elevation} />
127
+ ))}
128
+ </div>
129
+ ))}
130
+ </div>
131
+
132
+ {divider}
133
+
134
+ {/* ── Rounded — content variants ── */}
135
+ {section('Rounded — content variants')}
136
+ <div style={{ ...col, marginTop: 12 }}>
137
+ {([
138
+ { combo: 'label only', icon: false, removable: false },
139
+ { combo: 'icon + label', icon: true, removable: false },
140
+ { combo: 'removable', icon: false, removable: true },
141
+ { combo: 'icon + removable', icon: true, removable: true },
142
+ ] as const).map(({ combo, icon, removable }) => (
143
+ <div key={combo} style={row}>
144
+ <span style={lbl()}>{combo}</span>
145
+ {SIZES.map(size => (
146
+ <Tag
147
+ key={size}
148
+ label="Label"
149
+ size={size}
150
+ intent="brand"
151
+ rounded
152
+ icon={icon ? PlaceholderIcon : undefined}
153
+ removable={removable}
154
+ onRemove={removable ? () => {} : undefined}
155
+ />
156
+ ))}
157
+ </div>
158
+ ))}
159
+ </div>
160
+
161
+ {divider}
162
+
163
+ {/* ── Content variants × Intents ── */}
164
+ {section('Content variants × Intents')}
165
+ <div style={{ ...col, marginTop: 12 }}>
166
+ {([
167
+ { combo: 'label only', icon: false, removable: false },
168
+ { combo: 'icon + label', icon: true, removable: false },
169
+ { combo: 'removable', icon: false, removable: true },
170
+ { combo: 'icon + removable', icon: true, removable: true },
171
+ ] as const).map(({ combo, icon, removable }) => (
172
+ <div key={combo} style={row}>
173
+ <span style={lbl()}>{combo}</span>
174
+ {INTENTS.map(intent => (
175
+ <Tag
176
+ key={intent}
177
+ label={intent.charAt(0).toUpperCase() + intent.slice(1)}
178
+ intent={intent}
179
+ icon={icon ? PlaceholderIcon : undefined}
180
+ removable={removable}
181
+ onRemove={removable ? () => {} : undefined}
182
+ />
183
+ ))}
184
+ </div>
185
+ ))}
186
+ </div>
187
+
188
+ {divider}
189
+
190
+ {/* ── Content variants × Elevations ── */}
191
+ {section('Content variants × Elevations')}
192
+ <div style={{ ...col, marginTop: 12 }}>
193
+ {([
194
+ { combo: 'label only', icon: false, removable: false },
195
+ { combo: 'icon + label', icon: true, removable: false },
196
+ { combo: 'removable', icon: false, removable: true },
197
+ { combo: 'icon + removable', icon: true, removable: true },
198
+ ] as const).map(({ combo, icon, removable }) => (
199
+ <div key={combo} style={row}>
200
+ <span style={lbl()}>{combo}</span>
201
+ {ELEVATIONS.map(elevation => (
202
+ <Tag
203
+ key={elevation}
204
+ label="Label"
205
+ elevation={elevation}
206
+ intent="brand"
207
+ icon={icon ? PlaceholderIcon : undefined}
208
+ removable={removable}
209
+ onRemove={removable ? () => {} : undefined}
210
+ />
211
+ ))}
212
+ </div>
213
+ ))}
214
+ </div>
215
+
216
+ {divider}
217
+
218
+ {/* ── States ── */}
219
+ {section('States')}
220
+ <div style={{ ...col, marginTop: 12 }}>
221
+ {([
222
+ { label: 'default', props: {} },
223
+ { label: 'rounded', props: { rounded: true } },
224
+ { label: 'Inactive', props: { inactive: true } },
225
+ { label: 'elevated', props: { elevation: 'elevated' as const } },
226
+ { label: 'flat', props: { elevation: 'flat' as const } },
227
+ { label: 'pressed', props: { pressed: true } },
228
+ ]).map(({ label: stateLabel, props }) => (
229
+ <div key={stateLabel} style={row}>
230
+ <span style={lbl()}>{stateLabel}</span>
231
+ <Tag label="Label" intent="brand" {...(props as object)} />
232
+ <Tag label="Label" intent="negative" {...(props as object)} />
233
+ </div>
234
+ ))}
235
+ </div>
236
+
237
+ </div>
238
+ ),
239
+ };
240
+
241
+ // ─── Individual stories ───────────────────────────────────────────────────────
242
+
243
+ export const Intents: Story = {
244
+ render: () => (
245
+ <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
246
+ {INTENTS.map(intent => <Tag key={intent} label={intent.charAt(0).toUpperCase() + intent.slice(1)} intent={intent} />)}
247
+ </div>
248
+ ),
249
+ };
250
+
251
+ export const Contrasts: Story = {
252
+ render: () => (
253
+ <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
254
+ <Tag label="Strong" contrast="strong" intent="brand" />
255
+ <Tag label="Bold" contrast="bold" intent="brand" />
256
+ <Tag label="Medium" contrast="medium" intent="brand" />
257
+ <Tag label="Faint" contrast="faint" intent="brand" />
258
+ </div>
259
+ ),
260
+ };
261
+
262
+ export const Sizes: Story = {
263
+ render: () => (
264
+ <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
265
+ <Tag label="Medium" size="md" intent="brand" />
266
+ <Tag label="Small" size="sm" intent="brand" />
267
+ <Tag label="Extra Small" size="xs" intent="brand" />
268
+ </div>
269
+ ),
270
+ };
271
+
272
+ export const WithIcon: Story = {
273
+ render: () => (
274
+ <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
275
+ {INTENTS.map(intent => <Tag key={intent} label={intent.charAt(0).toUpperCase() + intent.slice(1)} intent={intent} icon={PlaceholderIcon} />)}
276
+ </div>
277
+ ),
278
+ };
279
+
280
+ export const Removable: Story = {
281
+ render: () => (
282
+ <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
283
+ <Tag label="Remove me" intent="brand" removable onRemove={() => {}} />
284
+ <Tag label="Remove me" intent="negative" removable onRemove={() => {}} size="sm" />
285
+ <Tag label="Remove me" intent="positive" removable onRemove={() => {}} size="xs" />
286
+ <Tag label="With icon" intent="brand" removable onRemove={() => {}} icon={PlaceholderIcon} />
287
+ </div>
288
+ ),
289
+ };
290
+
291
+ export const Flat: Story = {
292
+ render: () => (
293
+ <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
294
+ {INTENTS.map(intent => <Tag key={intent} label={intent.charAt(0).toUpperCase() + intent.slice(1)} intent={intent} elevation="flat" />)}
295
+ </div>
296
+ ),
297
+ };
298
+
299
+ export const Elevation: Story = {
300
+ render: () => (
301
+ <div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
302
+ <Tag label="none" intent="brand" elevation="none" />
303
+ <Tag label="flat" intent="brand" elevation="flat" />
304
+ <Tag label="elevated" intent="brand" elevation="elevated" />
305
+ </div>
306
+ ),
307
+ };
308
+
309
+ export const Interactive: Story = {
310
+ render: () => {
311
+ const [pressed, setPressed] = useState(false);
312
+ const [filters, setFilters] = useState<Record<string, boolean>>({
313
+ positive: false, negative: false, warning: false, brand: false, ai: false,
314
+ });
315
+ const toggle = (key: string) => setFilters(f => ({ ...f, [key]: !f[key] }));
316
+
317
+ return (
318
+ <div style={{ ...col, gap: 16 }}>
319
+ <div>
320
+ <div style={{ ...lbl(), marginBottom: 8 }}>Single toggle</div>
321
+ <Tag
322
+ label={pressed ? 'Selected' : 'Click me'}
323
+ intent="brand"
324
+ pressed={pressed}
325
+ onPressedChange={setPressed}
326
+ />
327
+ </div>
328
+ <div>
329
+ <div style={{ ...lbl(), marginBottom: 8 }}>Filter chips</div>
330
+ <div style={row}>
331
+ {(Object.keys(filters) as TagIntent[]).map(intent => (
332
+ <Tag
333
+ key={intent}
334
+ label={intent.charAt(0).toUpperCase() + intent.slice(1)}
335
+ intent={intent}
336
+ pressed={filters[intent]}
337
+ onPressedChange={() => toggle(intent)}
338
+ />
339
+ ))}
340
+ </div>
341
+ </div>
342
+ <div>
343
+ <div style={{ ...lbl(), marginBottom: 8 }}>Click only (no toggle)</div>
344
+ <Tag label="Click me" intent="neutral" onClick={() => alert('clicked')} />
345
+ </div>
346
+ </div>
347
+ );
348
+ },
349
+ };
350
+
351
+ export const Inactive: Story = {
352
+ render: () => (
353
+ <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
354
+ <Tag label="Inactive" intent="brand" inactive />
355
+ <Tag label="Inactive remove" intent="brand" inactive removable onRemove={() => {}} />
356
+ <Tag label="Inactive flat" intent="brand" inactive elevation="flat" />
357
+ </div>
358
+ ),
359
+ };
360
+
361
+ // ─── Background Context ──────────────────────────────────────────────────────
362
+
363
+ const TAG_BG_CONTEXTS: { bg: TagBackground; contrast: 'faint' | 'medium' | 'bold' | 'strong' }[] = [
364
+ { bg: 'faint', contrast: 'faint' },
365
+ { bg: 'medium', contrast: 'medium' },
366
+ { bg: 'bold', contrast: 'bold' },
367
+ { bg: 'strong', contrast: 'strong' },
368
+ ];
369
+
370
+ export const BackgroundContext: Story = {
371
+ parameters: { layout: 'padded' },
372
+ render: () => (
373
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
374
+ {TAG_BG_CONTEXTS.map(({ bg, contrast }) => (
375
+ <Surface key={bg} intent="neutral" contrast={contrast} radius="md" style={{ padding: 20 }}>
376
+ <span style={{ fontSize: 10, fontFamily: 'monospace', opacity: 0.7, display: 'block', marginBottom: 12 }}>
377
+ background="{bg}" — interactive tags, hover to test
378
+ </span>
379
+ <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
380
+ <Tag label="Brand" intent="brand" background={bg} onClick={() => {}} />
381
+ <Tag label="Neutral" intent="neutral" background={bg} onClick={() => {}} />
382
+ <Tag label="Negative" intent="negative" background={bg} onClick={() => {}} />
383
+ <Tag label="With Icon" intent="brand" background={bg} onClick={() => {}} icon={PlaceholderIcon} />
384
+ <Tag label="Removable" intent="brand" background={bg} removable onRemove={() => {}} />
385
+ </div>
386
+ </Surface>
387
+ ))}
388
+ <div style={{ background: '#1a1a2e', borderRadius: 8, padding: 20 }}>
389
+ <span style={{ fontSize: 10, fontFamily: 'monospace', color: 'rgba(255,255,255,0.5)', display: 'block', marginBottom: 12 }}>
390
+ background="always-dark"
391
+ </span>
392
+ <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
393
+ <Tag label="Brand" intent="brand" background="always-dark" onClick={() => {}} />
394
+ <Tag label="Neutral" intent="neutral" background="always-dark" onClick={() => {}} />
395
+ </div>
396
+ </div>
397
+ </div>
398
+ ),
399
+ };