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,90 @@
1
+ 'use client';
2
+
3
+ // Checkbox - checkbox primitive; on/off + indeterminate select-all.
4
+
5
+ import * as React from 'react';
6
+
7
+ export interface CheckboxProps extends Omit<
8
+ React.InputHTMLAttributes<HTMLInputElement>,
9
+ 'size' | 'type'
10
+ > {
11
+ /** Controlled checked state. Omit for uncontrolled (use `defaultChecked`). */
12
+ checked?: boolean;
13
+ /** Uncontrolled initial state. */
14
+ defaultChecked?: boolean;
15
+ /** Indeterminate - the "some, not all" select-all state; visually wins over `checked`. @default false */
16
+ indeterminate?: boolean;
17
+ /** Single error state for consent gates ("you must agree"); also sets `aria-invalid`. @default false */
18
+ invalid?: boolean;
19
+ /** Disabled - inert and de-emphasized (distinct fill when checked). */
20
+ disabled?: boolean;
21
+ /** Box size: `md` 18px - `sm` 16px for dense table rows. @default 'md' */
22
+ size?: 'sm' | 'md';
23
+ /** Label text beside the box. */
24
+ label?: React.ReactNode;
25
+ /** Optional secondary line under the label (settings rows). */
26
+ description?: React.ReactNode;
27
+ /** Fires on toggle - read `e.target.checked`. */
28
+ onChange?: React.ChangeEventHandler<HTMLInputElement>;
29
+ }
30
+
31
+ export function Checkbox({
32
+ checked,
33
+ defaultChecked,
34
+ indeterminate = false,
35
+ invalid = false,
36
+ disabled = false,
37
+ size = 'md',
38
+ label,
39
+ description,
40
+ className = '',
41
+ onChange,
42
+ ...rest
43
+ }: CheckboxProps) {
44
+ const ref = React.useRef<HTMLInputElement>(null);
45
+
46
+ // `indeterminate` is not an HTML attribute - push it onto the node directly.
47
+ React.useEffect(() => {
48
+ if (ref.current) ref.current.indeterminate = indeterminate;
49
+ }, [indeterminate, checked]);
50
+
51
+ const classes = [
52
+ 'cbx',
53
+ size === 'sm' ? 'cbx--sm' : '',
54
+ invalid ? 'cbx--invalid' : '',
55
+ disabled ? 'cbx--disabled' : '',
56
+ className,
57
+ ]
58
+ .filter(Boolean)
59
+ .join(' ');
60
+
61
+ const controlled = checked !== undefined;
62
+ const checkedProps = controlled ? { checked } : { defaultChecked };
63
+
64
+ return (
65
+ <label className={classes}>
66
+ <input
67
+ ref={ref}
68
+ type="checkbox"
69
+ className="cbx__input"
70
+ disabled={disabled}
71
+ aria-invalid={invalid || undefined}
72
+ onChange={onChange}
73
+ {...checkedProps}
74
+ {...rest}
75
+ />
76
+ <span className="cbx__box" aria-hidden="true">
77
+ <svg className="cbx__mark" viewBox="0 0 16 16" fill="none">
78
+ <path className="cbx__tick" d="M3.5 8.5 L6.75 11.5 L12.5 4.75" />
79
+ </svg>
80
+ <span className="cbx__dash" />
81
+ </span>
82
+ {label || description ? (
83
+ <span className="cbx__text">
84
+ {label ? <span className="cbx__label">{label}</span> : null}
85
+ {description ? <span className="cbx__desc">{description}</span> : null}
86
+ </span>
87
+ ) : null}
88
+ </label>
89
+ );
90
+ }
@@ -0,0 +1,179 @@
1
+ /* Checkbox - on/off + indeterminate; native input hidden, box painted from :has()-set flags read via calc(). */
2
+
3
+ .cbx {
4
+ --cbx-fill: linear-gradient(180deg, var(--accent), var(--accent-hover));
5
+ --cbx-on: var(--accent);
6
+ --cbx-off: var(--border-strong);
7
+ --cbx-ring: var(--ring-accent);
8
+ --cbx-size: var(--control-box);
9
+ --cbx-radius: var(--radius-sm);
10
+ --tick-len: 14px; /* needs a unit so the calc() dashoffset is a valid <length>; ~ the polyline path length */
11
+ --fill: 0;
12
+ --tick: 0;
13
+ --dash: 0;
14
+ --press: 1;
15
+
16
+ display: inline-flex;
17
+ align-items: flex-start;
18
+ gap: var(--space-2);
19
+ padding: var(--space-2);
20
+ border-radius: var(--radius-md);
21
+ cursor: pointer;
22
+ -webkit-tap-highlight-color: transparent;
23
+ transition: background-color var(--duration-fast) var(--ease-standard);
24
+
25
+ /* later rule wins, so indeterminate beats checked */
26
+ &:has(:checked) {
27
+ --fill: 1;
28
+ --tick: 1;
29
+ }
30
+ &:has(:indeterminate) {
31
+ --fill: 1;
32
+ --tick: 0;
33
+ --dash: 1;
34
+ }
35
+
36
+ &:not(.cbx--disabled):hover {
37
+ background: var(--bg-subtle);
38
+ }
39
+ /* accent the empty box on hover */
40
+ &:not(.cbx--disabled):hover:not(:has(:checked, :indeterminate)) {
41
+ --cbx-off: var(--cbx-on);
42
+ }
43
+ &:not(.cbx--disabled):active {
44
+ --press: 0.88;
45
+ }
46
+ &:has(:focus-visible) .cbx__box {
47
+ box-shadow: var(--cbx-ring);
48
+ }
49
+ }
50
+
51
+ .cbx__input {
52
+ position: absolute;
53
+ width: 1px;
54
+ height: 1px;
55
+ margin: 0;
56
+ padding: 0;
57
+ border: 0;
58
+ opacity: 0;
59
+ clip-path: inset(50%);
60
+ white-space: nowrap;
61
+ overflow: hidden;
62
+ }
63
+
64
+ .cbx__box {
65
+ position: relative;
66
+ flex: none;
67
+ display: grid;
68
+ place-items: center;
69
+ width: var(--cbx-size);
70
+ height: var(--cbx-size);
71
+ margin-top: calc((var(--leading-body) - var(--cbx-size)) / 2);
72
+ border: var(--border-hairline) solid
73
+ color-mix(in oklab, var(--cbx-on) calc(var(--fill) * 100%), var(--cbx-off));
74
+ border-radius: var(--cbx-radius);
75
+ background: var(--bg-inset);
76
+ transform: scale(var(--press));
77
+ transition:
78
+ border-color var(--duration-fast) var(--ease-standard),
79
+ background-color var(--duration-fast) var(--ease-standard),
80
+ box-shadow var(--duration-fast) var(--ease-standard),
81
+ transform var(--duration-base) var(--ease-spring);
82
+
83
+ &::before {
84
+ content: '';
85
+ position: absolute;
86
+ inset: 0;
87
+
88
+ /* inner corner = outer radius - border, so the fill reaches the corners (no --bg-inset showing) */
89
+ border-radius: calc(var(--cbx-radius) - var(--border-hairline));
90
+ background: var(--cbx-fill);
91
+ opacity: var(--fill);
92
+ transform: scale(calc(0.4 + 0.6 * var(--fill)));
93
+ transition:
94
+ transform var(--duration-base) var(--ease-spring),
95
+ opacity var(--duration-fast) var(--ease-standard);
96
+ }
97
+ }
98
+
99
+ /* stagger lives in the delay: draw-on waits a beat (calc - --tick), retract fires immediately */
100
+ .cbx__mark {
101
+ position: relative;
102
+ display: block;
103
+ width: 100%;
104
+ height: 100%;
105
+ color: var(--text-on-accent);
106
+ pointer-events: none;
107
+ }
108
+ .cbx__tick {
109
+ fill: none;
110
+ stroke: currentColor;
111
+ stroke-width: 2;
112
+ stroke-linecap: round;
113
+ stroke-linejoin: round;
114
+ stroke-dasharray: var(--tick-len);
115
+ stroke-dashoffset: calc(var(--tick-len) * (1 - var(--tick)));
116
+ transition: stroke-dashoffset var(--duration-base) var(--ease-entrance)
117
+ calc(var(--tick) * var(--duration-fast));
118
+ }
119
+
120
+ .cbx__dash {
121
+ position: absolute;
122
+ width: 56%;
123
+ height: 2px;
124
+ border-radius: var(--border-hairline);
125
+ background: var(--text-on-accent);
126
+ pointer-events: none;
127
+ opacity: var(--dash);
128
+ transform: scaleX(var(--dash));
129
+ transition:
130
+ transform var(--duration-base) var(--ease-spring),
131
+ opacity var(--duration-fast) var(--ease-standard);
132
+ }
133
+
134
+ .cbx__text {
135
+ display: grid;
136
+ gap: var(--space-1);
137
+ }
138
+ .cbx__label {
139
+ font: var(--type-label);
140
+ color: var(--text-strong);
141
+ }
142
+ .cbx__desc {
143
+ font: var(--type-caption);
144
+ color: var(--text-muted);
145
+ }
146
+
147
+ .cbx--sm {
148
+ --cbx-size: var(--space-4);
149
+ & .cbx__label {
150
+ font: var(--type-caption);
151
+ font-weight: var(--weight-medium);
152
+ color: var(--text-strong);
153
+ }
154
+ & .cbx__box {
155
+ margin-top: calc((var(--leading-caption) - var(--cbx-size)) / 2);
156
+ }
157
+ }
158
+
159
+ .cbx--invalid {
160
+ --cbx-fill: linear-gradient(180deg, var(--danger), var(--danger));
161
+ --cbx-on: var(--danger);
162
+ --cbx-off: var(--danger);
163
+ --cbx-ring: var(--ring-danger);
164
+ }
165
+
166
+ /* disabled leaves the state flags untouched, so the mark still matches checked state */
167
+ .cbx--disabled {
168
+ cursor: not-allowed;
169
+ --cbx-fill: var(--border-strong);
170
+ --cbx-on: var(--border-default);
171
+ --cbx-off: var(--border-subtle);
172
+ & .cbx__box {
173
+ background: var(--bg-muted);
174
+ }
175
+ & .cbx__label,
176
+ & .cbx__desc {
177
+ color: var(--text-disabled);
178
+ }
179
+ }
@@ -0,0 +1,362 @@
1
+ 'use client';
2
+
3
+ /* DateField - opinionated single-date picker: a .fld trigger opens a month-calendar popover, commit is live on day pick. */
4
+
5
+ import * as React from 'react';
6
+ import type { ReactNode } from 'react';
7
+ import { motion, animate } from 'motion/react';
8
+ import { UIMotion } from '../../tokens/motion-tokens';
9
+ import { Icon } from '../icon/Icon';
10
+ import { Overlay } from '../overlay/Overlay';
11
+ import { FieldShell, useControllable } from './field-shell';
12
+ import { GlidePill, useGlide } from './glide-pill';
13
+ import {
14
+ MONTHS as DTF_MONTHS,
15
+ DOW as DTF_DOW,
16
+ pad as dtfPad,
17
+ key as dtfKey,
18
+ parse as dtfParse,
19
+ today as dtfToday,
20
+ grid as dtfGrid,
21
+ tzLabel as dtfTzLabel,
22
+ } from './date-utils';
23
+
24
+ const { useState, useRef, useEffect, useId } = React;
25
+ const dtfMotion = motion;
26
+ const dtfAnimate = animate;
27
+ const dtfSM = UIMotion;
28
+
29
+ /* 'Jun 12' (year only when it isn't the current one) */
30
+ function dtfDisplay(key: string | null): string | null {
31
+ if (!key) return null;
32
+ const d = dtfParse(key);
33
+ const mon = DTF_MONTHS[d.getMonth()].slice(0, 3);
34
+ const year = d.getFullYear() === new Date().getFullYear() ? '' : ', ' + d.getFullYear();
35
+ return mon + ' ' + dtfPad(d.getDate()) + year;
36
+ }
37
+
38
+ export interface DtpPanelProps {
39
+ val: string | null;
40
+ commit: (key: string) => void;
41
+ min?: string;
42
+ max?: string;
43
+ timezone?: string;
44
+ label?: string;
45
+ close: () => void;
46
+ slot?: ReactNode;
47
+ }
48
+
49
+ /* the popover panel, mounted only while open - owns view + roving focus, so each open starts at the picked month. */
50
+ export function DtpPanel({ val, commit, min, max, timezone, label, close, slot }: DtpPanelProps) {
51
+ const seed = val ? dtfParse(val) : new Date();
52
+ const [view, setView] = useState<{ y: number; m: number }>({
53
+ y: seed.getFullYear(),
54
+ m: seed.getMonth(),
55
+ });
56
+ const [focusKey, setFocusKey] = useState<string>(val || dtfToday());
57
+
58
+ const daysRef = useRef<HTMLDivElement>(null);
59
+ const prevViewRef = useRef<number | null>(null);
60
+ const pendingFocusRef = useRef(false);
61
+ const uid = useId();
62
+ const pillId = 'dtp-pill-' + uid;
63
+
64
+ /* gliding hover: one persistent pill that travels to the hovered cell (see glide-pill). */
65
+ const glide = useGlide(daysRef);
66
+
67
+ const inRange = (key: string): boolean => (!min || key >= min) && (!max || key <= max);
68
+
69
+ function pickDay(key: string) {
70
+ if (!inRange(key)) return;
71
+ pendingFocusRef.current = true;
72
+ setFocusKey(key);
73
+ const d = dtfParse(key);
74
+ goToMonth(d.getFullYear(), d.getMonth());
75
+ commit(key);
76
+ }
77
+
78
+ function goToMonth(y: number, m: number) {
79
+ setView((v) => (v.y === y && v.m === m ? v : { y: y, m: m }));
80
+ }
81
+ function nav(dir: number) {
82
+ const d = new Date(view.y, view.m + dir, 1);
83
+ goToMonth(d.getFullYear(), d.getMonth());
84
+ }
85
+ /* today-jump is navigation, not commit: slides home + focuses today; disabled while viewing the current month. */
86
+ const dtfNow = new Date();
87
+ const viewIsCurrent = view.y === dtfNow.getFullYear() && view.m === dtfNow.getMonth();
88
+ function goToToday() {
89
+ const key = dtfToday();
90
+ const d = dtfParse(key);
91
+ pendingFocusRef.current = true;
92
+ setFocusKey(key);
93
+ goToMonth(d.getFullYear(), d.getMonth());
94
+ }
95
+ const viewIdx = view.y * 12 + view.m;
96
+ useEffect(() => {
97
+ const prev = prevViewRef.current;
98
+ prevViewRef.current = viewIdx;
99
+ const el = daysRef.current;
100
+ if (prev == null || prev === viewIdx || !el) return;
101
+ const dir = viewIdx > prev ? 1 : -1;
102
+ dtfAnimate(
103
+ el,
104
+ { x: [dir * 16, 0], opacity: [0, 1] },
105
+ { duration: dtfSM.t.enter.duration, ease: dtfSM.t.enter.ease },
106
+ );
107
+ }, [viewIdx]);
108
+
109
+ /* seed focus into the grid on open - the panel portals to <body>, unreachable from the trigger otherwise. */
110
+ useEffect(() => {
111
+ const el = daysRef.current;
112
+ if (!el) return;
113
+ const btn =
114
+ el.querySelector('[tabindex="0"]:not(:disabled)') ||
115
+ el.querySelector('.dtp__day:not(:disabled)');
116
+ if (btn) (btn as HTMLElement).focus({ preventScroll: true });
117
+ }, []);
118
+
119
+ /* focus follows keyboard travel across month boundaries */
120
+ useEffect(() => {
121
+ if (!pendingFocusRef.current || !daysRef.current) return;
122
+ pendingFocusRef.current = false;
123
+ const btn = daysRef.current.querySelector('[data-key="' + focusKey + '"]');
124
+ if (btn) (btn as HTMLElement).focus({ preventScroll: true });
125
+ }, [focusKey, viewIdx]);
126
+
127
+ function moveFocus(deltaDays: number, deltaMonths: number) {
128
+ const d = dtfParse(focusKey);
129
+ if (deltaMonths) d.setMonth(d.getMonth() + deltaMonths);
130
+ if (deltaDays) d.setDate(d.getDate() + deltaDays);
131
+ const key = dtfKey(d);
132
+ pendingFocusRef.current = true;
133
+ setFocusKey(key);
134
+ goToMonth(d.getFullYear(), d.getMonth());
135
+ }
136
+ function onGridKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
137
+ const k = e.key;
138
+ if (k === 'ArrowLeft') moveFocus(-1, 0);
139
+ else if (k === 'ArrowRight') moveFocus(1, 0);
140
+ else if (k === 'ArrowUp') moveFocus(-7, 0);
141
+ else if (k === 'ArrowDown') moveFocus(7, 0);
142
+ else if (k === 'PageUp') moveFocus(0, -1);
143
+ else if (k === 'PageDown') moveFocus(0, 1);
144
+ else if (k === 'Home') moveFocus(-((dtfParse(focusKey).getDay() + 6) % 7), 0);
145
+ else if (k === 'End') moveFocus(6 - ((dtfParse(focusKey).getDay() + 6) % 7), 0);
146
+ else if (k === 'Enter' || k === ' ') pickDay(focusKey);
147
+ else return;
148
+ e.preventDefault();
149
+ }
150
+
151
+ const days = dtfGrid(view.y, view.m);
152
+ const todayKey = dtfToday();
153
+ const selKey = val || null;
154
+ const gridKeys = days.map(dtfKey);
155
+ const tabKey =
156
+ gridKeys.indexOf(focusKey) >= 0
157
+ ? focusKey
158
+ : selKey && gridKeys.indexOf(selKey) >= 0
159
+ ? selKey
160
+ : gridKeys.indexOf(todayKey) >= 0
161
+ ? todayKey
162
+ : dtfKey(new Date(view.y, view.m, 1));
163
+
164
+ const prevEnd = dtfKey(new Date(view.y, view.m, 0));
165
+ const nextStart = dtfKey(new Date(view.y, view.m + 1, 1));
166
+ const canPrev = !min || prevEnd >= min;
167
+ const canNext = !max || nextStart <= max;
168
+
169
+ const weeks: Date[][] = [];
170
+ for (let w = 0; w < 6; w++) weeks.push(days.slice(w * 7, w * 7 + 7));
171
+
172
+ return (
173
+ <div className="dtp" role="dialog" aria-label={label || 'Pick a date'}>
174
+ <div className="dtp__cal">
175
+ <div className="dtp__head">
176
+ <span className="dtp__month" aria-live="polite">
177
+ {DTF_MONTHS[view.m]} <span className="dtp__year">{view.y}</span>
178
+ </span>
179
+ <div className="dtp__navs">
180
+ <button
181
+ type="button"
182
+ className="dtp__nav"
183
+ aria-label="Previous month"
184
+ disabled={!canPrev}
185
+ onClick={() => nav(-1)}
186
+ >
187
+ <Icon name="caret-left" size="sm" />
188
+ </button>
189
+ <button
190
+ type="button"
191
+ className="dtp__nav"
192
+ aria-label="Go to today"
193
+ disabled={viewIsCurrent}
194
+ onClick={goToToday}
195
+ >
196
+ <Icon name="calendar-dot" size="sm" />
197
+ </button>
198
+ <button
199
+ type="button"
200
+ className="dtp__nav"
201
+ aria-label="Next month"
202
+ disabled={!canNext}
203
+ onClick={() => nav(1)}
204
+ >
205
+ <Icon name="caret-right" size="sm" />
206
+ </button>
207
+ </div>
208
+ </div>
209
+ <div className="dtp__dow" aria-hidden="true">
210
+ {DTF_DOW.map((d) => (
211
+ <span key={d}>{d}</span>
212
+ ))}
213
+ </div>
214
+ <div
215
+ className="dtp__days"
216
+ ref={daysRef}
217
+ role="grid"
218
+ aria-label="Calendar"
219
+ onKeyDown={onGridKeyDown}
220
+ onPointerLeave={glide.leave}
221
+ >
222
+ {weeks.map((week, wi) => (
223
+ <div key={wi} role="row" className="dtp__row">
224
+ {week.map((d) => {
225
+ const key = dtfKey(d);
226
+ const out = d.getMonth() !== view.m;
227
+ const sel = key === selKey;
228
+ const cls = [
229
+ 'dtp__day',
230
+ out ? 'is-out' : '',
231
+ sel ? 'is-selected' : '',
232
+ key === todayKey ? 'is-today' : '',
233
+ ]
234
+ .filter(Boolean)
235
+ .join(' ');
236
+ return (
237
+ <button
238
+ key={key}
239
+ type="button"
240
+ role="gridcell"
241
+ data-key={key}
242
+ className={cls}
243
+ disabled={!inRange(key)}
244
+ tabIndex={key === tabKey ? 0 : -1}
245
+ aria-selected={sel || undefined}
246
+ aria-label={
247
+ DTF_MONTHS[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear()
248
+ }
249
+ onClick={() => pickDay(key)}
250
+ onPointerEnter={(e) => glide.enter(e.currentTarget)}
251
+ >
252
+ {sel ? (
253
+ <dtfMotion.span
254
+ className="dtp__pill"
255
+ layoutId={pillId}
256
+ transition={dtfSM.t.settle}
257
+ aria-hidden="true"
258
+ ></dtfMotion.span>
259
+ ) : null}
260
+ <span className="dtp__num">{d.getDate()}</span>
261
+ {key === todayKey ? (
262
+ <span className="dtp__dot" aria-hidden="true"></span>
263
+ ) : null}
264
+ </button>
265
+ );
266
+ })}
267
+ </div>
268
+ ))}
269
+ <GlidePill className="dtp__hover" rect={glide.rect} active={glide.active} />
270
+ </div>
271
+ </div>
272
+
273
+ {slot || null}
274
+
275
+ <div className="dtp__foot">
276
+ {timezone ? <span className="dtp__tz">{dtfTzLabel(timezone, selKey)}</span> : null}
277
+ <span className="dtp__footSpacer"></span>
278
+ <button type="button" className="btn btn--primary btn--sm" onClick={close}>
279
+ Done
280
+ </button>
281
+ </div>
282
+ </div>
283
+ );
284
+ }
285
+
286
+ export interface DateFieldProps {
287
+ /** Controlled value, 'YYYY-MM-DD'. */
288
+ value?: string | null;
289
+ defaultValue?: string | null;
290
+ onChange?: (value: string) => void;
291
+ label?: string;
292
+ placeholder?: string;
293
+ /** IANA timezone name (e.g. 'Europe/Riga') - display context only, shown with its GMT offset in the footer. */
294
+ timezone?: string;
295
+ /** Earliest pickable date, 'YYYY-MM-DD', inclusive. */
296
+ min?: string;
297
+ /** Latest pickable date, 'YYYY-MM-DD', inclusive. */
298
+ max?: string;
299
+ /** Asterisk on the label. */
300
+ required?: boolean;
301
+ /** Danger border + message color (.fld is-error). */
302
+ invalid?: boolean;
303
+ /** Helper / error text under the field. */
304
+ message?: string;
305
+ disabled?: boolean;
306
+ className?: string;
307
+ }
308
+
309
+ export function DateField({
310
+ value,
311
+ defaultValue = null,
312
+ onChange,
313
+ label,
314
+ placeholder = 'Pick a date',
315
+ timezone,
316
+ min,
317
+ max,
318
+ required = false,
319
+ invalid = false,
320
+ message,
321
+ disabled = false,
322
+ className = '',
323
+ }: DateFieldProps) {
324
+ const [val, commit] = useControllable(value, defaultValue, onChange);
325
+ const display = dtfDisplay(val);
326
+
327
+ const trigger = (
328
+ <button type="button" className="fld__input dtf__trigger" disabled={disabled}>
329
+ {display ? (
330
+ <span className="dtf__value">{display}</span>
331
+ ) : (
332
+ <span className="dtf__placeholder">{placeholder}</span>
333
+ )}
334
+ </button>
335
+ );
336
+
337
+ return (
338
+ <FieldShell
339
+ variant="dtf"
340
+ label={label}
341
+ required={required}
342
+ invalid={invalid}
343
+ message={message}
344
+ icon="calendar"
345
+ className={className}
346
+ >
347
+ <Overlay trigger={trigger} side="bottom" align="start">
348
+ {(api) => (
349
+ <DtpPanel
350
+ val={val}
351
+ commit={commit}
352
+ close={api.close}
353
+ min={min}
354
+ max={max}
355
+ timezone={timezone}
356
+ label={label}
357
+ />
358
+ )}
359
+ </Overlay>
360
+ </FieldShell>
361
+ );
362
+ }