premium-ds 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 (257) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +113 -0
  3. package/dist/alert.d.ts +31 -0
  4. package/dist/alert.js +6 -0
  5. package/dist/alert.js.map +1 -0
  6. package/dist/avatar-group.d.ts +13 -0
  7. package/dist/avatar-group.js +3 -0
  8. package/dist/avatar-group.js.map +1 -0
  9. package/dist/avatar.d.ts +25 -0
  10. package/dist/avatar.js +3 -0
  11. package/dist/avatar.js.map +1 -0
  12. package/dist/badge.d.ts +23 -0
  13. package/dist/badge.js +3 -0
  14. package/dist/badge.js.map +1 -0
  15. package/dist/button.d.ts +20 -0
  16. package/dist/button.js +3 -0
  17. package/dist/button.js.map +1 -0
  18. package/dist/checkbox.d.ts +25 -0
  19. package/dist/checkbox.js +3 -0
  20. package/dist/checkbox.js.map +1 -0
  21. package/dist/chunk-2OWHZ4JT.js +36 -0
  22. package/dist/chunk-2OWHZ4JT.js.map +1 -0
  23. package/dist/chunk-34SIXSYL.js +64 -0
  24. package/dist/chunk-34SIXSYL.js.map +1 -0
  25. package/dist/chunk-37O2ZXD6.js +55 -0
  26. package/dist/chunk-37O2ZXD6.js.map +1 -0
  27. package/dist/chunk-4AZL76UJ.js +89 -0
  28. package/dist/chunk-4AZL76UJ.js.map +1 -0
  29. package/dist/chunk-4HSCN5TZ.js +86 -0
  30. package/dist/chunk-4HSCN5TZ.js.map +1 -0
  31. package/dist/chunk-5DDOOT33.js +258 -0
  32. package/dist/chunk-5DDOOT33.js.map +1 -0
  33. package/dist/chunk-5FVHWIMY.js +117 -0
  34. package/dist/chunk-5FVHWIMY.js.map +1 -0
  35. package/dist/chunk-5K6KRJGX.js +147 -0
  36. package/dist/chunk-5K6KRJGX.js.map +1 -0
  37. package/dist/chunk-5PQMQBQC.js +74 -0
  38. package/dist/chunk-5PQMQBQC.js.map +1 -0
  39. package/dist/chunk-7OCTVQ7C.js +95 -0
  40. package/dist/chunk-7OCTVQ7C.js.map +1 -0
  41. package/dist/chunk-7OPMOET7.js +39 -0
  42. package/dist/chunk-7OPMOET7.js.map +1 -0
  43. package/dist/chunk-BXXS7YRC.js +270 -0
  44. package/dist/chunk-BXXS7YRC.js.map +1 -0
  45. package/dist/chunk-CV2Q4YXX.js +272 -0
  46. package/dist/chunk-CV2Q4YXX.js.map +1 -0
  47. package/dist/chunk-EIMMDWIW.js +282 -0
  48. package/dist/chunk-EIMMDWIW.js.map +1 -0
  49. package/dist/chunk-EZ2CWTBE.js +230 -0
  50. package/dist/chunk-EZ2CWTBE.js.map +1 -0
  51. package/dist/chunk-FGHDG3Y4.js +89 -0
  52. package/dist/chunk-FGHDG3Y4.js.map +1 -0
  53. package/dist/chunk-FPP2XLKX.js +127 -0
  54. package/dist/chunk-FPP2XLKX.js.map +1 -0
  55. package/dist/chunk-G6OY35DI.js +295 -0
  56. package/dist/chunk-G6OY35DI.js.map +1 -0
  57. package/dist/chunk-H6KWJNOE.js +65 -0
  58. package/dist/chunk-H6KWJNOE.js.map +1 -0
  59. package/dist/chunk-HGILYGY3.js +45 -0
  60. package/dist/chunk-HGILYGY3.js.map +1 -0
  61. package/dist/chunk-I3BCB4Z5.js +88 -0
  62. package/dist/chunk-I3BCB4Z5.js.map +1 -0
  63. package/dist/chunk-KBWNUUWM.js +582 -0
  64. package/dist/chunk-KBWNUUWM.js.map +1 -0
  65. package/dist/chunk-KN7JFAZ6.js +113 -0
  66. package/dist/chunk-KN7JFAZ6.js.map +1 -0
  67. package/dist/chunk-MEF7PI6U.js +16 -0
  68. package/dist/chunk-MEF7PI6U.js.map +1 -0
  69. package/dist/chunk-NKGMQL6I.js +310 -0
  70. package/dist/chunk-NKGMQL6I.js.map +1 -0
  71. package/dist/chunk-NMFQRGLL.js +127 -0
  72. package/dist/chunk-NMFQRGLL.js.map +1 -0
  73. package/dist/chunk-OUBWD6CX.js +433 -0
  74. package/dist/chunk-OUBWD6CX.js.map +1 -0
  75. package/dist/chunk-PFNXVBLU.js +96 -0
  76. package/dist/chunk-PFNXVBLU.js.map +1 -0
  77. package/dist/chunk-PUPZ4HME.js +165 -0
  78. package/dist/chunk-PUPZ4HME.js.map +1 -0
  79. package/dist/chunk-QFS52OK5.js +690 -0
  80. package/dist/chunk-QFS52OK5.js.map +1 -0
  81. package/dist/chunk-QNC6O3PG.js +45 -0
  82. package/dist/chunk-QNC6O3PG.js.map +1 -0
  83. package/dist/chunk-QUHOXWBK.js +82 -0
  84. package/dist/chunk-QUHOXWBK.js.map +1 -0
  85. package/dist/chunk-UIQGSTBJ.js +106 -0
  86. package/dist/chunk-UIQGSTBJ.js.map +1 -0
  87. package/dist/chunk-UJQKVP6V.js +193 -0
  88. package/dist/chunk-UJQKVP6V.js.map +1 -0
  89. package/dist/chunk-VVPGEAC6.js +11 -0
  90. package/dist/chunk-VVPGEAC6.js.map +1 -0
  91. package/dist/chunk-XA3T5KWA.js +58 -0
  92. package/dist/chunk-XA3T5KWA.js.map +1 -0
  93. package/dist/chunk-YSHJHSJM.js +19 -0
  94. package/dist/chunk-YSHJHSJM.js.map +1 -0
  95. package/dist/chunk-YVHOAVSM.js +182 -0
  96. package/dist/chunk-YVHOAVSM.js.map +1 -0
  97. package/dist/collapse.d.ts +16 -0
  98. package/dist/collapse.js +3 -0
  99. package/dist/collapse.js.map +1 -0
  100. package/dist/count-badge.d.ts +11 -0
  101. package/dist/count-badge.js +4 -0
  102. package/dist/count-badge.js.map +1 -0
  103. package/dist/date-field.d.ts +39 -0
  104. package/dist/date-field.js +8 -0
  105. package/dist/date-field.js.map +1 -0
  106. package/dist/date-range-field.d.ts +30 -0
  107. package/dist/date-range-field.js +8 -0
  108. package/dist/date-range-field.js.map +1 -0
  109. package/dist/datetime-field.d.ts +28 -0
  110. package/dist/datetime-field.js +10 -0
  111. package/dist/datetime-field.js.map +1 -0
  112. package/dist/dialog.d.ts +26 -0
  113. package/dist/dialog.js +7 -0
  114. package/dist/dialog.js.map +1 -0
  115. package/dist/index.d.ts +35 -0
  116. package/dist/index.js +40 -0
  117. package/dist/index.js.map +1 -0
  118. package/dist/motion-tokens.d.ts +29 -0
  119. package/dist/motion-tokens.js +3 -0
  120. package/dist/motion-tokens.js.map +1 -0
  121. package/dist/multi-select.d.ts +25 -0
  122. package/dist/multi-select.js +7 -0
  123. package/dist/multi-select.js.map +1 -0
  124. package/dist/number-field.d.ts +24 -0
  125. package/dist/number-field.js +4 -0
  126. package/dist/number-field.js.map +1 -0
  127. package/dist/otp-field.d.ts +20 -0
  128. package/dist/otp-field.js +3 -0
  129. package/dist/otp-field.js.map +1 -0
  130. package/dist/overlay.d.ts +31 -0
  131. package/dist/overlay.js +4 -0
  132. package/dist/overlay.js.map +1 -0
  133. package/dist/pagination.d.ts +24 -0
  134. package/dist/pagination.js +5 -0
  135. package/dist/pagination.js.map +1 -0
  136. package/dist/radio-group.d.ts +46 -0
  137. package/dist/radio-group.js +6 -0
  138. package/dist/radio-group.js.map +1 -0
  139. package/dist/select-core-SAyS-8w0.d.ts +16 -0
  140. package/dist/select.d.ts +27 -0
  141. package/dist/select.js +7 -0
  142. package/dist/select.js.map +1 -0
  143. package/dist/status-badge.d.ts +17 -0
  144. package/dist/status-badge.js +5 -0
  145. package/dist/status-badge.js.map +1 -0
  146. package/dist/table.d.ts +65 -0
  147. package/dist/table.js +5 -0
  148. package/dist/table.js.map +1 -0
  149. package/dist/tabs.d.ts +44 -0
  150. package/dist/tabs.js +5 -0
  151. package/dist/tabs.js.map +1 -0
  152. package/dist/tag.d.ts +28 -0
  153. package/dist/tag.js +5 -0
  154. package/dist/tag.js.map +1 -0
  155. package/dist/text-field.d.ts +30 -0
  156. package/dist/text-field.js +6 -0
  157. package/dist/text-field.js.map +1 -0
  158. package/dist/textarea.d.ts +33 -0
  159. package/dist/textarea.js +5 -0
  160. package/dist/textarea.js.map +1 -0
  161. package/dist/time-field.d.ts +27 -0
  162. package/dist/time-field.js +6 -0
  163. package/dist/time-field.js.map +1 -0
  164. package/dist/toast-store.d.ts +75 -0
  165. package/dist/toast-store.js +3 -0
  166. package/dist/toast-store.js.map +1 -0
  167. package/dist/toast.d.ts +3 -0
  168. package/dist/toast.js +6 -0
  169. package/dist/toast.js.map +1 -0
  170. package/dist/toggle-tag.d.ts +24 -0
  171. package/dist/toggle-tag.js +4 -0
  172. package/dist/toggle-tag.js.map +1 -0
  173. package/dist/toggle.d.ts +21 -0
  174. package/dist/toggle.js +3 -0
  175. package/dist/toggle.js.map +1 -0
  176. package/dist/tooltip.d.ts +27 -0
  177. package/dist/tooltip.js +4 -0
  178. package/dist/tooltip.js.map +1 -0
  179. package/llms.txt +165 -0
  180. package/package.json +205 -0
  181. package/src/components/alert/Alert.tsx +118 -0
  182. package/src/components/alert/alert.css +136 -0
  183. package/src/components/avatar/Avatar.tsx +128 -0
  184. package/src/components/avatar/AvatarGroup.tsx +50 -0
  185. package/src/components/avatar/avatar.css +200 -0
  186. package/src/components/badge/Badge.tsx +66 -0
  187. package/src/components/badge/CountBadge.tsx +46 -0
  188. package/src/components/badge/StatusBadge.tsx +132 -0
  189. package/src/components/badge/badge.css +243 -0
  190. package/src/components/button/Button.tsx +68 -0
  191. package/src/components/button/button.css +222 -0
  192. package/src/components/checkbox/Checkbox.tsx +90 -0
  193. package/src/components/checkbox/checkbox.css +179 -0
  194. package/src/components/date-picker/DateField.tsx +362 -0
  195. package/src/components/date-picker/DateRangeField.tsx +533 -0
  196. package/src/components/date-picker/DateTimeField.tsx +177 -0
  197. package/src/components/date-picker/TimeField.tsx +100 -0
  198. package/src/components/date-picker/date-picker.css +591 -0
  199. package/src/components/date-picker/date-utils.ts +55 -0
  200. package/src/components/date-picker/field-shell.tsx +78 -0
  201. package/src/components/date-picker/glide-pill.tsx +81 -0
  202. package/src/components/date-picker/time-core.tsx +305 -0
  203. package/src/components/dialog/Dialog.tsx +181 -0
  204. package/src/components/dialog/dialog.css +170 -0
  205. package/src/components/glass/glass.css +100 -0
  206. package/src/components/icon/Icon.tsx +76 -0
  207. package/src/components/icon/IconSlot.tsx +11 -0
  208. package/src/components/icon/icon.css +33 -0
  209. package/src/components/input/NumberField.tsx +117 -0
  210. package/src/components/input/OtpField.tsx +118 -0
  211. package/src/components/input/TextField.tsx +123 -0
  212. package/src/components/input/input.css +335 -0
  213. package/src/components/motion/Collapse.tsx +33 -0
  214. package/src/components/motion/collapse.css +41 -0
  215. package/src/components/overlay/Overlay.tsx +239 -0
  216. package/src/components/overlay/overlay-core.tsx +565 -0
  217. package/src/components/overlay/overlay.css +119 -0
  218. package/src/components/overlay/sheet-drag.tsx +146 -0
  219. package/src/components/pagination/Pagination.tsx +140 -0
  220. package/src/components/pagination/pagination.css +48 -0
  221. package/src/components/radio-group/RadioGroup.tsx +182 -0
  222. package/src/components/radio-group/radio-group.css +277 -0
  223. package/src/components/select/MultiSelect.tsx +251 -0
  224. package/src/components/select/Select.tsx +235 -0
  225. package/src/components/select/select-core.tsx +417 -0
  226. package/src/components/select/select.css +386 -0
  227. package/src/components/table/Table.tsx +433 -0
  228. package/src/components/table/table.css +348 -0
  229. package/src/components/tabs/Tabs.tsx +371 -0
  230. package/src/components/tabs/tabs.css +228 -0
  231. package/src/components/tag/Tag.tsx +145 -0
  232. package/src/components/tag/ToggleTag.tsx +125 -0
  233. package/src/components/tag/tag.css +248 -0
  234. package/src/components/textarea/Textarea.tsx +197 -0
  235. package/src/components/textarea/textarea.css +219 -0
  236. package/src/components/toast/Toast.tsx +349 -0
  237. package/src/components/toast/toast-store.ts +266 -0
  238. package/src/components/toast/toast.css +233 -0
  239. package/src/components/toggle/Toggle.tsx +94 -0
  240. package/src/components/toggle/toggle.css +152 -0
  241. package/src/components/tooltip/Tooltip.tsx +365 -0
  242. package/src/components/tooltip/tooltip.css +86 -0
  243. package/src/index.ts +42 -0
  244. package/src/styles.css +39 -0
  245. package/src/tokens/avatar.css +20 -0
  246. package/src/tokens/color.css +56 -0
  247. package/src/tokens/elevation.css +20 -0
  248. package/src/tokens/fonts.css +3 -0
  249. package/src/tokens/glass.css +21 -0
  250. package/src/tokens/icons.css +7 -0
  251. package/src/tokens/layers.css +6 -0
  252. package/src/tokens/motion-tokens.ts +72 -0
  253. package/src/tokens/motion.css +49 -0
  254. package/src/tokens/radius.css +11 -0
  255. package/src/tokens/semantic.css +75 -0
  256. package/src/tokens/spacing.css +26 -0
  257. package/src/tokens/typography.css +54 -0
@@ -0,0 +1,233 @@
1
+ /* toast.css - the glass notification card, tinted per tone (a draining timer ring). */
2
+
3
+ .toast-viewport {
4
+ position: fixed;
5
+ bottom: var(--space-6);
6
+ right: var(--space-6);
7
+ width: min(22.5rem, calc(100vw - 2 * var(--space-6)));
8
+ z-index: var(--layer-toast);
9
+ margin: 0;
10
+ padding: 0;
11
+ list-style: none;
12
+ pointer-events: none; /* only the cards themselves are live */
13
+ }
14
+
15
+ /* The card - a glass consumer (.glass is set in JSX). */
16
+ .toast {
17
+ /* tone hooks - set by [data-tone] below */
18
+ --toast-hue: var(--text-subtle);
19
+ --glass-fg: var(--text-strong);
20
+ --glass-tint: color-mix(in oklab, var(--glass-tint-neutral) 75%, transparent);
21
+ --glass-sheen-rest: 0.45;
22
+
23
+ position: absolute;
24
+ bottom: 0;
25
+ right: 0;
26
+ width: 100%;
27
+ box-sizing: border-box;
28
+ overflow: hidden; /* custom content stays in the pane */
29
+ pointer-events: auto;
30
+ transform-origin: center bottom; /* the stack recedes upward */
31
+ border-radius: var(--radius-lg);
32
+
33
+ /* Motion owns transform - keep only the color/shadow transitions (never smooth Motion's writes) */
34
+ transition:
35
+ background-color var(--duration-base) var(--ease-standard),
36
+ box-shadow var(--duration-base) var(--ease-standard);
37
+ }
38
+
39
+ /* rich panes for genuine status, diluted to stay glass; neutral otherwise */
40
+ .toast[data-tone='success'] {
41
+ --toast-hue: var(--success);
42
+ --glass-fg: var(--success-text);
43
+ --glass-tint: color-mix(in oklab, var(--glass-tint-success) 70%, transparent);
44
+ }
45
+ .toast[data-tone='error'] {
46
+ --toast-hue: var(--danger);
47
+ --glass-fg: var(--danger-text);
48
+ --glass-tint: color-mix(in oklab, var(--glass-tint-danger) 70%, transparent);
49
+ }
50
+ .toast[data-tone='warning'] {
51
+ --toast-hue: var(--warning);
52
+ --glass-fg: var(--warning-text);
53
+ --glass-tint: color-mix(in oklab, var(--glass-tint-warning) 70%, transparent);
54
+ }
55
+ .toast[data-tone='info'] {
56
+ --toast-hue: var(--info);
57
+ --glass-fg: var(--info-text);
58
+ --glass-tint: color-mix(in oklab, var(--glass-tint-info) 70%, transparent);
59
+ }
60
+
61
+ /* receding cards fall quiet: tone drains out, sheen dims, content fades */
62
+ .toast[data-behind] {
63
+ --glass-tint: color-mix(in oklab, var(--glass-tint-neutral) 75%, transparent);
64
+ --glass-sheen-rest: 0.3;
65
+ }
66
+ .toast[data-behind] .toast__inner,
67
+ .toast[data-behind] .toast__custom {
68
+ opacity: 0;
69
+ }
70
+
71
+ .toast__inner {
72
+ display: flex;
73
+ align-items: center;
74
+ gap: var(--space-3);
75
+ padding: var(--space-3) var(--space-4);
76
+ transition: var(--transition-opacity);
77
+ }
78
+
79
+ /* the dial - glyph centered in the timer ring; one instrument, not two */
80
+ .toast__icon {
81
+ position: relative;
82
+ flex: none;
83
+ display: grid;
84
+ place-items: center;
85
+ width: calc(var(--icon-md) + 2 * var(--space-2));
86
+ height: calc(var(--icon-md) + 2 * var(--space-2));
87
+ color: var(--toast-hue);
88
+ }
89
+ .toast__icon-glyph {
90
+ display: grid;
91
+ place-items: center;
92
+ }
93
+
94
+ /* the ring - a draining arc; linear by design, animation-duration set at runtime to the toast's lifetime */
95
+ .toast__ring {
96
+ position: absolute;
97
+ inset: 0;
98
+ width: 100%;
99
+ height: 100%;
100
+ transform: rotate(-90deg); /* arc starts at 12 o'clock */
101
+ pointer-events: none;
102
+ }
103
+ .toast__ring circle {
104
+ fill: none;
105
+ stroke-width: var(--border-emphasis);
106
+ }
107
+ .toast__ring-track {
108
+ stroke: color-mix(in oklab, var(--toast-hue) 16%, transparent);
109
+ }
110
+ .toast__ring-arc {
111
+ stroke: var(--toast-hue);
112
+ stroke-linecap: round;
113
+ stroke-dasharray: 1; /* pathLength=1 - offset is a fraction */
114
+ animation-name: toast-ring;
115
+ animation-timing-function: linear;
116
+ animation-fill-mode: both;
117
+ }
118
+ .toast-viewport.is-paused .toast__ring-arc {
119
+ animation-play-state: paused;
120
+ }
121
+ @keyframes toast-ring {
122
+ from {
123
+ stroke-dashoffset: 0;
124
+ }
125
+ to {
126
+ stroke-dashoffset: 1;
127
+ }
128
+ }
129
+
130
+ .toast__text {
131
+ flex: 1 1 auto;
132
+ min-width: 0;
133
+ }
134
+ .toast__message {
135
+ margin: 0;
136
+ font: var(--type-label);
137
+ color: var(--glass-fg);
138
+ text-wrap: pretty;
139
+ }
140
+ .toast__desc {
141
+ margin: var(--space-1) 0 0;
142
+ font: var(--type-caption);
143
+ color: color-mix(in oklab, var(--glass-fg) 76%, transparent);
144
+ text-wrap: pretty;
145
+ }
146
+
147
+ /* coalesce counter - same toast refired becomes a xN counter (mono) */
148
+ .toast__count {
149
+ flex: none;
150
+ font-family: var(--font-mono);
151
+ font-size: var(--size-micro);
152
+ line-height: var(--leading-micro);
153
+ font-variant-numeric: tabular-nums;
154
+ color: var(--glass-fg);
155
+ background: color-mix(in oklab, var(--glass-fg) 9%, transparent);
156
+ border-radius: var(--radius-sm);
157
+ padding: 0 var(--space-1);
158
+ }
159
+
160
+ .toast__action {
161
+ flex: none;
162
+ }
163
+
164
+ /* dismiss - appears only while the card is hovered or holds focus */
165
+ .toast__close {
166
+ flex: none;
167
+ appearance: none;
168
+ display: grid;
169
+ place-items: center;
170
+ width: var(--space-6);
171
+ height: var(--space-6);
172
+ margin: 0;
173
+ padding: 0;
174
+ border: none;
175
+ border-radius: var(--radius-sm);
176
+ background: transparent;
177
+ color: color-mix(in oklab, var(--glass-fg) 64%, transparent);
178
+ cursor: pointer;
179
+ opacity: 0;
180
+ transition:
181
+ var(--transition-control),
182
+ opacity var(--duration-fast) var(--ease-standard);
183
+ }
184
+ .toast:hover .toast__close,
185
+ .toast:focus-within .toast__close {
186
+ opacity: 1;
187
+ }
188
+ .toast__close:hover {
189
+ background: color-mix(in oklab, var(--glass-fg) 9%, transparent);
190
+ color: var(--glass-fg);
191
+ }
192
+ .toast__close:active {
193
+ background: color-mix(in oklab, var(--glass-fg) 14%, transparent);
194
+ }
195
+ .toast__close:focus-visible {
196
+ outline: none;
197
+ box-shadow: var(--focus-ring);
198
+ opacity: 1;
199
+ }
200
+
201
+ /* Loading spinner - continuous --duration-spin (linear is allowed for loops; reduced motion slows, never freezes) */
202
+ .toast__spinner {
203
+ display: block;
204
+ box-sizing: border-box;
205
+ width: var(--icon-sm);
206
+ height: var(--icon-sm);
207
+ border-radius: var(--radius-full);
208
+ border: var(--border-emphasis) solid currentColor;
209
+ border-right-color: transparent;
210
+ color: var(--text-muted);
211
+ animation: toast-spin var(--duration-spin) linear infinite;
212
+ }
213
+ @keyframes toast-spin {
214
+ to {
215
+ transform: rotate(1turn);
216
+ }
217
+ }
218
+
219
+ /* Custom slot - bare neutral glass; the consumer owns the interior. */
220
+ .toast__custom {
221
+ padding: var(--space-3) var(--space-4);
222
+ transition: var(--transition-opacity);
223
+ }
224
+
225
+ @media (prefers-reduced-motion: reduce) {
226
+ /* travel/gestures collapse via the tokens; the ring is data, so it keeps running */
227
+ .toast,
228
+ .toast__close,
229
+ .toast__inner,
230
+ .toast__custom {
231
+ transition: none;
232
+ }
233
+ }
@@ -0,0 +1,94 @@
1
+ 'use client';
2
+
3
+ // Toggle - binary on/off switch for an immediate setting (vs. Checkbox, which stages a choice).
4
+
5
+ import * as React from 'react';
6
+
7
+ const { useState } = React;
8
+
9
+ export interface ToggleProps extends Omit<
10
+ React.InputHTMLAttributes<HTMLInputElement>,
11
+ 'size' | 'type'
12
+ > {
13
+ /** Controlled checked state. Omit for uncontrolled (use `defaultChecked`). */
14
+ checked?: boolean;
15
+ /** Uncontrolled initial state. */
16
+ defaultChecked?: boolean;
17
+ /** Disabled - inert and de-emphasized (faded track, retains its position). */
18
+ disabled?: boolean;
19
+ /** Track size: `md` 36x20 - `sm` 28x16 for dense settings/table rows. @default 'md' */
20
+ size?: 'sm' | 'md';
21
+ /** Label text beside the track. */
22
+ label?: React.ReactNode;
23
+ /** Optional secondary line under the label (settings rows). */
24
+ description?: React.ReactNode;
25
+ /** Fires on flip - read `e.target.checked`. */
26
+ onChange?: React.ChangeEventHandler<HTMLInputElement>;
27
+ }
28
+
29
+ export function Toggle({
30
+ checked,
31
+ defaultChecked = false,
32
+ disabled = false,
33
+ size = 'md',
34
+ label,
35
+ description,
36
+ className = '',
37
+ onChange,
38
+ ...rest
39
+ }: ToggleProps) {
40
+ const controlled = checked !== undefined;
41
+ const [internal, setInternal] = useState(!!defaultChecked);
42
+ const isOn = controlled ? !!checked : internal;
43
+
44
+ const classes = ['sw', size === 'sm' ? 'sw--sm' : '', disabled ? 'sw--disabled' : '', className]
45
+ .filter(Boolean)
46
+ .join(' ');
47
+
48
+ const [pressed, setPressed] = useState(false);
49
+ function press(on: boolean) {
50
+ return () => {
51
+ if (!disabled) setPressed(on);
52
+ };
53
+ }
54
+
55
+ function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
56
+ if (!controlled) setInternal(e.target.checked);
57
+ if (onChange) onChange(e);
58
+ }
59
+
60
+ return (
61
+ <label
62
+ className={classes}
63
+ onPointerDown={press(true)}
64
+ onPointerUp={press(false)}
65
+ onPointerLeave={press(false)}
66
+ onPointerCancel={press(false)}
67
+ >
68
+ <input
69
+ type="checkbox"
70
+ role="switch"
71
+ className="sw__input"
72
+ disabled={disabled}
73
+ checked={isOn}
74
+ onChange={handleChange}
75
+ {...rest}
76
+ defaultChecked={undefined}
77
+ />
78
+ <span
79
+ className="sw__track"
80
+ data-on={isOn ? 'true' : undefined}
81
+ data-pressed={pressed ? 'true' : undefined}
82
+ aria-hidden="true"
83
+ >
84
+ <span className="sw__thumb"></span>
85
+ </span>
86
+ {label || description ? (
87
+ <span className="sw__text">
88
+ {label ? <span className="sw__label">{label}</span> : null}
89
+ {description ? <span className="sw__desc">{description}</span> : null}
90
+ </span>
91
+ ) : null}
92
+ </label>
93
+ );
94
+ }
@@ -0,0 +1,152 @@
1
+ /* Toggle (switch) - immediate on/off; native input hidden, track + thumb painted, thumb travels on a CSS spring. */
2
+
3
+ .sw {
4
+ --sw-on-fill: linear-gradient(180deg, var(--accent), var(--accent-hover));
5
+ --sw-off-fill: var(--bg-muted);
6
+ --sw-on-border: var(--accent);
7
+ --sw-off-border: var(--border-strong);
8
+ --sw-ring: var(--ring-accent);
9
+ /* track size is the only input; thumb + travel derive from it */
10
+ --sw-w: var(--control-height);
11
+ --sw-h: var(--control-switch);
12
+ --sw-inset: calc(2 * var(--space-px));
13
+ /* inner track height minus both borders and the gap each side, so the inset is uniform on all edges */
14
+ --sw-thumb: calc(var(--sw-h) - 2 * var(--border-hairline) - 2 * var(--sw-inset));
15
+ /* colors read this flag; the thumb is moved by transform, not by it */
16
+ --on: 0;
17
+
18
+ display: inline-flex;
19
+ align-items: flex-start;
20
+ gap: var(--space-3);
21
+ padding: var(--space-2);
22
+ border-radius: var(--radius-md);
23
+ cursor: pointer;
24
+ -webkit-tap-highlight-color: transparent;
25
+ transition: background-color var(--duration-fast) var(--ease-standard);
26
+
27
+ &:has(:checked) {
28
+ --on: 1;
29
+ }
30
+
31
+ &:not(.sw--disabled):hover {
32
+ background: var(--bg-muted);
33
+ }
34
+ &:not(.sw--disabled):hover:not(:has(:checked)) {
35
+ --sw-off-border: var(--text-subtle);
36
+ }
37
+ &:has(:focus-visible) .sw__track {
38
+ box-shadow: var(--sw-ring);
39
+ }
40
+ }
41
+
42
+ .sw__input {
43
+ position: absolute;
44
+ width: 1px;
45
+ height: 1px;
46
+ margin: 0;
47
+ padding: 0;
48
+ border: 0;
49
+ opacity: 0;
50
+ clip-path: inset(50%);
51
+ white-space: nowrap;
52
+ overflow: hidden;
53
+ }
54
+
55
+ .sw__track {
56
+ position: relative;
57
+ flex: none;
58
+ display: flex;
59
+ align-items: center;
60
+ justify-content: flex-start;
61
+ padding: var(--sw-inset);
62
+ width: var(--sw-w);
63
+ height: var(--sw-h);
64
+ margin-top: calc((var(--leading-body) - var(--sw-h)) / 2);
65
+ border: var(--border-hairline) solid
66
+ color-mix(in oklab, var(--sw-on-border) calc(var(--on) * 100%), var(--sw-off-border));
67
+ border-radius: var(--radius-full);
68
+ background: var(--sw-off-fill);
69
+ transform: scale(1);
70
+ transition:
71
+ border-color var(--duration-base) var(--ease-standard),
72
+ background-color var(--duration-base) var(--ease-standard),
73
+ box-shadow var(--duration-fast) var(--ease-standard),
74
+ transform var(--duration-fast) var(--ease-spring);
75
+
76
+ &::before {
77
+ content: '';
78
+ position: absolute;
79
+ inset: 0;
80
+ border-radius: inherit;
81
+ background: var(--sw-on-fill);
82
+ opacity: var(--on);
83
+ transition: opacity var(--duration-base) var(--ease-standard);
84
+ }
85
+ }
86
+ /* --tx (travel) + --sx (press-stretch) compose one transform, so press and travel never fight over it */
87
+ .sw__thumb {
88
+ --tx: 0px;
89
+ --sx: 1;
90
+ position: relative;
91
+ z-index: 1;
92
+ flex: none;
93
+ width: var(--sw-thumb);
94
+ height: var(--sw-thumb);
95
+ border-radius: var(--radius-full);
96
+ background: var(--bg-surface);
97
+ box-shadow: var(--shadow-sm);
98
+ transform: translateX(var(--tx)) scaleX(var(--sx));
99
+ transition: transform var(--duration-base) var(--ease-spring);
100
+ }
101
+ /* React-set [data-on] drives travel - no :has() twin to move the thumb out from under the spring and snap it */
102
+ .sw__track[data-on='true'] .sw__thumb {
103
+ --tx: calc(var(--sw-w) - var(--sw-h));
104
+ }
105
+ .sw__track[data-pressed='true'] {
106
+ transform: scale(0.96);
107
+ }
108
+ .sw__track[data-pressed='true'] .sw__thumb {
109
+ --sx: 1.16;
110
+ }
111
+
112
+ .sw__text {
113
+ display: grid;
114
+ gap: var(--space-1);
115
+ }
116
+ .sw__label {
117
+ font: var(--type-label);
118
+ color: var(--text-strong);
119
+ }
120
+ .sw__desc {
121
+ font: var(--type-caption);
122
+ color: var(--text-muted);
123
+ }
124
+
125
+ .sw--sm {
126
+ --sw-w: var(--control-height-sm);
127
+ --sw-h: var(--space-4);
128
+ & .sw__label {
129
+ font: var(--type-caption);
130
+ font-weight: var(--weight-medium);
131
+ color: var(--text-strong);
132
+ }
133
+ & .sw__track {
134
+ margin-top: calc((var(--leading-caption) - var(--sw-h)) / 2);
135
+ }
136
+ }
137
+
138
+ /* disabled leaves the flag untouched, so the thumb keeps its correct position */
139
+ .sw--disabled {
140
+ cursor: not-allowed;
141
+ --sw-on-fill: var(--accent-disabled);
142
+ --sw-off-fill: var(--bg-muted);
143
+ --sw-on-border: var(--accent-disabled);
144
+ --sw-off-border: var(--border-default);
145
+ & .sw__thumb {
146
+ box-shadow: var(--shadow-xs);
147
+ }
148
+ & .sw__label,
149
+ & .sw__desc {
150
+ color: var(--text-disabled);
151
+ }
152
+ }