gbs-add-block 1.2.14 → 1.2.16

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 (169) hide show
  1. package/README.md +2 -2
  2. package/index.cjs +219 -21
  3. package/package.json +1 -1
  4. package/source/beta-components/breadcrumb/README.md +34 -0
  5. package/source/beta-components/breadcrumb/__tests__/core.test.ts +58 -0
  6. package/source/beta-components/breadcrumb/core/index.ts +4 -0
  7. package/source/beta-components/breadcrumb/core/trail.ts +42 -0
  8. package/source/beta-components/breadcrumb/core/types.ts +20 -0
  9. package/source/beta-components/breadcrumb/index.ts +5 -0
  10. package/source/beta-components/breadcrumb/react/Breadcrumb.tsx +177 -0
  11. package/source/beta-components/breadcrumb/react/locale.ts +6 -0
  12. package/source/beta-components/breadcrumb/react/props.ts +14 -0
  13. package/source/beta-components/breadcrumb/styles.css +139 -0
  14. package/source/beta-components/button/README.md +32 -0
  15. package/source/beta-components/button/__tests__/core.test.ts +31 -0
  16. package/source/beta-components/button/core/index.ts +3 -0
  17. package/source/beta-components/button/core/state.ts +16 -0
  18. package/source/beta-components/button/core/types.ts +28 -0
  19. package/source/beta-components/button/index.ts +5 -0
  20. package/source/beta-components/button/react/Button.tsx +173 -0
  21. package/source/beta-components/button/react/locale.ts +5 -0
  22. package/source/beta-components/button/react/props.ts +23 -0
  23. package/source/beta-components/button/styles.css +233 -0
  24. package/source/beta-components/checkbox/README.md +36 -0
  25. package/source/beta-components/checkbox/__tests__/core.test.ts +40 -0
  26. package/source/beta-components/checkbox/core/group.ts +30 -0
  27. package/source/beta-components/checkbox/core/index.ts +3 -0
  28. package/source/beta-components/checkbox/core/types.ts +17 -0
  29. package/source/beta-components/checkbox/index.ts +9 -0
  30. package/source/beta-components/checkbox/react/Checkbox.tsx +147 -0
  31. package/source/beta-components/checkbox/react/CheckboxGroup.tsx +149 -0
  32. package/source/beta-components/checkbox/react/context.ts +16 -0
  33. package/source/beta-components/checkbox/react/locale.ts +5 -0
  34. package/source/beta-components/checkbox/react/props.ts +6 -0
  35. package/source/beta-components/checkbox/styles.css +212 -0
  36. package/source/beta-components/combobox/README.md +3 -3
  37. package/source/beta-components/combobox/react/MultiSelect.tsx +265 -263
  38. package/source/beta-components/combobox/react/Popover.tsx +38 -70
  39. package/source/beta-components/combobox/react/Select.tsx +200 -198
  40. package/source/beta-components/combobox/react/icons.tsx +6 -29
  41. package/source/beta-components/combobox/react/props.ts +4 -2
  42. package/source/beta-components/combobox/styles.css +18 -18
  43. package/source/beta-components/data-grid/react/Popover.tsx +35 -0
  44. package/source/beta-components/{datagrid → data-grid}/react/context.ts +2 -2
  45. package/source/beta-components/{datagrid → data-grid}/react/icons.tsx +8 -35
  46. package/source/beta-components/{datagrid → data-grid}/styles.css +24 -24
  47. package/source/beta-components/{datepicker → date-picker}/README.md +3 -3
  48. package/source/beta-components/{datepicker → date-picker}/react/DatePicker.tsx +249 -249
  49. package/source/beta-components/{datepicker → date-picker}/react/DateRangePicker.tsx +291 -291
  50. package/source/beta-components/date-picker/react/Popover.tsx +45 -0
  51. package/source/beta-components/date-picker/react/icons.tsx +15 -0
  52. package/source/beta-components/{datepicker → date-picker}/react/props.ts +2 -2
  53. package/source/beta-components/{datepicker → date-picker}/styles.css +18 -18
  54. package/source/beta-components/dialog/react/icons.tsx +16 -47
  55. package/source/beta-components/dialog/react/props.ts +2 -2
  56. package/source/beta-components/dialog/styles.css +21 -21
  57. package/source/beta-components/{fileuploader → file-uploader}/react/FileUploader.tsx +511 -515
  58. package/source/beta-components/{fileuploader → file-uploader}/react/icons.tsx +7 -27
  59. package/source/beta-components/{fileuploader → file-uploader}/react/props.ts +2 -2
  60. package/source/beta-components/{fileuploader → file-uploader}/styles.css +17 -17
  61. package/source/beta-components/input/core/count.ts +3 -12
  62. package/source/beta-components/input/react/Input.tsx +219 -219
  63. package/source/beta-components/input/react/OtpInput.tsx +256 -256
  64. package/source/beta-components/input/react/icons.tsx +3 -21
  65. package/source/beta-components/input/react/props.ts +2 -2
  66. package/source/beta-components/input/styles.css +15 -15
  67. package/source/beta-components/modal/react/Modal.tsx +242 -238
  68. package/source/beta-components/modal/react/icons.tsx +1 -19
  69. package/source/beta-components/modal/react/props.ts +2 -2
  70. package/source/beta-components/modal/styles.css +17 -13
  71. package/source/beta-components/shared/README.md +54 -0
  72. package/source/beta-components/shared/__tests__/boundaries.test.ts +95 -0
  73. package/source/beta-components/shared/__tests__/core.test.ts +55 -0
  74. package/source/beta-components/shared/core/cx.ts +9 -0
  75. package/source/beta-components/shared/core/field.ts +35 -0
  76. package/source/beta-components/shared/core/text.ts +14 -0
  77. package/source/beta-components/shared/index.ts +7 -0
  78. package/source/beta-components/shared/react/Popover.tsx +147 -0
  79. package/source/beta-components/shared/react/icons.tsx +74 -0
  80. package/source/beta-components/shared/react/useControllableState.ts +42 -0
  81. package/source/beta-components/shared/version.json +4 -0
  82. package/source/beta-components/shared/version.ts +6 -0
  83. package/source/beta-components/spinner/README.md +29 -0
  84. package/source/beta-components/spinner/__tests__/core.test.ts +48 -0
  85. package/source/beta-components/spinner/core/index.ts +3 -0
  86. package/source/beta-components/spinner/core/types.ts +28 -0
  87. package/source/beta-components/spinner/core/visibility.ts +34 -0
  88. package/source/beta-components/spinner/index.ts +6 -0
  89. package/source/beta-components/spinner/react/Spinner.tsx +96 -0
  90. package/source/beta-components/spinner/react/locale.ts +5 -0
  91. package/source/beta-components/spinner/react/props.ts +4 -0
  92. package/source/beta-components/spinner/react/useDelayedLoading.ts +39 -0
  93. package/source/beta-components/spinner/styles.css +167 -0
  94. package/source/beta-components/tabs/README.md +32 -0
  95. package/source/beta-components/tabs/__tests__/core.test.ts +51 -0
  96. package/source/beta-components/tabs/core/index.ts +3 -0
  97. package/source/beta-components/tabs/core/navigation.ts +54 -0
  98. package/source/beta-components/tabs/core/types.ts +16 -0
  99. package/source/beta-components/tabs/index.ts +5 -0
  100. package/source/beta-components/tabs/react/Tabs.tsx +258 -0
  101. package/source/beta-components/tabs/react/context.ts +21 -0
  102. package/source/beta-components/tabs/styles.css +257 -0
  103. package/source/beta-components/textarea/core/count.ts +3 -12
  104. package/source/beta-components/textarea/react/Textarea.tsx +208 -209
  105. package/source/beta-components/textarea/react/props.ts +2 -2
  106. package/source/beta-components/textarea/styles.css +12 -12
  107. package/source/beta-components/toaster/react/icons.tsx +19 -52
  108. package/source/beta-components/toaster/react/props.ts +2 -2
  109. package/source/beta-components/toaster/styles.css +18 -18
  110. package/source/beta-components/datagrid/react/Popover.tsx +0 -79
  111. package/source/beta-components/datepicker/react/Popover.tsx +0 -87
  112. package/source/beta-components/datepicker/react/icons.tsx +0 -32
  113. /package/source/beta-components/{datagrid → data-grid}/README.md +0 -0
  114. /package/source/beta-components/{datagrid → data-grid}/__tests__/core.test.ts +0 -0
  115. /package/source/beta-components/{datagrid → data-grid}/__tests__/export.test.ts +0 -0
  116. /package/source/beta-components/{datagrid → data-grid}/core/columnHelper.ts +0 -0
  117. /package/source/beta-components/{datagrid → data-grid}/core/columns.ts +0 -0
  118. /package/source/beta-components/{datagrid → data-grid}/core/filtering.ts +0 -0
  119. /package/source/beta-components/{datagrid → data-grid}/core/grid.ts +0 -0
  120. /package/source/beta-components/{datagrid → data-grid}/core/index.ts +0 -0
  121. /package/source/beta-components/{datagrid → data-grid}/core/rows.ts +0 -0
  122. /package/source/beta-components/{datagrid → data-grid}/core/sorting.ts +0 -0
  123. /package/source/beta-components/{datagrid → data-grid}/core/state.ts +0 -0
  124. /package/source/beta-components/{datagrid → data-grid}/core/store.ts +0 -0
  125. /package/source/beta-components/{datagrid → data-grid}/core/types.ts +0 -0
  126. /package/source/beta-components/{datagrid → data-grid}/core/values.ts +0 -0
  127. /package/source/beta-components/{datagrid → data-grid}/core/virtual.ts +0 -0
  128. /package/source/beta-components/{datagrid → data-grid}/export/csv.ts +0 -0
  129. /package/source/beta-components/{datagrid → data-grid}/export/download.ts +0 -0
  130. /package/source/beta-components/{datagrid → data-grid}/export/pdf.ts +0 -0
  131. /package/source/beta-components/{datagrid → data-grid}/export/table.ts +0 -0
  132. /package/source/beta-components/{datagrid → data-grid}/export/xlsx.ts +0 -0
  133. /package/source/beta-components/{datagrid → data-grid}/export/zip.ts +0 -0
  134. /package/source/beta-components/{datagrid → data-grid}/index.ts +0 -0
  135. /package/source/beta-components/{datagrid → data-grid}/react/Cell.tsx +0 -0
  136. /package/source/beta-components/{datagrid → data-grid}/react/ColumnMenu.tsx +0 -0
  137. /package/source/beta-components/{datagrid → data-grid}/react/DataGrid.tsx +0 -0
  138. /package/source/beta-components/{datagrid → data-grid}/react/FilterForm.tsx +0 -0
  139. /package/source/beta-components/{datagrid → data-grid}/react/HeaderRow.tsx +0 -0
  140. /package/source/beta-components/{datagrid → data-grid}/react/Pagination.tsx +0 -0
  141. /package/source/beta-components/{datagrid → data-grid}/react/Row.tsx +0 -0
  142. /package/source/beta-components/{datagrid → data-grid}/react/Toolbar.tsx +0 -0
  143. /package/source/beta-components/{datagrid → data-grid}/react/Viewport.tsx +0 -0
  144. /package/source/beta-components/{datagrid → data-grid}/react/hooks.ts +0 -0
  145. /package/source/beta-components/{datagrid → data-grid}/react/keyboard.ts +0 -0
  146. /package/source/beta-components/{datagrid → data-grid}/react/locale.ts +0 -0
  147. /package/source/beta-components/{datepicker → date-picker}/__tests__/core.test.ts +0 -0
  148. /package/source/beta-components/{datepicker → date-picker}/core/calendar.ts +0 -0
  149. /package/source/beta-components/{datepicker → date-picker}/core/format.ts +0 -0
  150. /package/source/beta-components/{datepicker → date-picker}/core/index.ts +0 -0
  151. /package/source/beta-components/{datepicker → date-picker}/core/types.ts +0 -0
  152. /package/source/beta-components/{datepicker → date-picker}/index.ts +0 -0
  153. /package/source/beta-components/{datepicker → date-picker}/react/Calendar.tsx +0 -0
  154. /package/source/beta-components/{datepicker → date-picker}/react/CalendarPanel.tsx +0 -0
  155. /package/source/beta-components/{datepicker → date-picker}/react/MonthYearPanels.tsx +0 -0
  156. /package/source/beta-components/{datepicker → date-picker}/react/locale.ts +0 -0
  157. /package/source/beta-components/{datepicker → date-picker}/react/useDatePicker.ts +0 -0
  158. /package/source/beta-components/{fileuploader → file-uploader}/README.md +0 -0
  159. /package/source/beta-components/{fileuploader → file-uploader}/__tests__/core.test.ts +0 -0
  160. /package/source/beta-components/{fileuploader → file-uploader}/core/format.ts +0 -0
  161. /package/source/beta-components/{fileuploader → file-uploader}/core/index.ts +0 -0
  162. /package/source/beta-components/{fileuploader → file-uploader}/core/store.ts +0 -0
  163. /package/source/beta-components/{fileuploader → file-uploader}/core/transport.ts +0 -0
  164. /package/source/beta-components/{fileuploader → file-uploader}/core/types.ts +0 -0
  165. /package/source/beta-components/{fileuploader → file-uploader}/core/validate.ts +0 -0
  166. /package/source/beta-components/{fileuploader → file-uploader}/index.ts +0 -0
  167. /package/source/beta-components/{fileuploader → file-uploader}/react/FileRow.tsx +0 -0
  168. /package/source/beta-components/{fileuploader → file-uploader}/react/locale.ts +0 -0
  169. /package/source/beta-components/{fileuploader → file-uploader}/react/useFileUploader.ts +0 -0
@@ -0,0 +1,233 @@
1
+ /*
2
+ * Button styles.
3
+ *
4
+ * Tokens read the shared --gbs-* variables when they are set on an ancestor
5
+ * (set them on :root to theme every component at once) and fall back to the
6
+ * same default palette otherwise. Rules live in the `components` layer, so utility classes passed
7
+ * through `className` / `classNames` override them.
8
+ */
9
+ @layer theme, base, components, utilities;
10
+
11
+ @layer components {
12
+ .bt-root {
13
+ --bt-bg: var(--gbs-bg, var(--dg-bg, light-dark(#ffffff, #0b0b0e)));
14
+ --bt-fg: var(--gbs-fg, var(--dg-fg, light-dark(#18181b, #f4f4f5)));
15
+ --bt-border: var(--gbs-border, var(--dg-border, light-dark(#e4e4e7, #27272a)));
16
+ --bt-hover: var(--gbs-hover, var(--dg-hover, light-dark(#f4f4f5, #1f1f23)));
17
+ --bt-subtle: var(--gbs-subtle, light-dark(#f4f4f5, #1c1c20));
18
+ --bt-accent: var(--gbs-accent, var(--dg-accent, light-dark(#2563eb, #60a5fa)));
19
+ --bt-accent-fg: var(--gbs-accent-fg, var(--dg-accent-fg, light-dark(#ffffff, #0b1220)));
20
+ --bt-danger: var(--gbs-danger, var(--dg-danger, light-dark(#dc2626, #f87171)));
21
+ --bt-danger-fg: var(--gbs-danger-fg, light-dark(#ffffff, #1c0606));
22
+ --bt-focus: var(--gbs-focus, var(--dg-focus, light-dark(#2563eb, #60a5fa)));
23
+ --bt-radius: var(--gbs-radius, var(--dg-radius, 8px));
24
+ --bt-font-size: var(--gbs-font-size, var(--dg-font-size, 13px));
25
+ --bt-height: 36px;
26
+ --bt-px: 14px;
27
+ --bt-gap: 8px;
28
+
29
+ /* Per variant. */
30
+ --bt-fill: var(--bt-accent);
31
+ --bt-fill-hover: color-mix(in oklab, var(--bt-accent), var(--bt-fg) 12%);
32
+ --bt-ink: var(--bt-accent-fg);
33
+ --bt-line: var(--bt-accent);
34
+
35
+ color-scheme: inherit;
36
+ box-sizing: border-box;
37
+ position: relative;
38
+ display: inline-grid;
39
+ place-items: center;
40
+ height: var(--bt-height);
41
+ padding: 0 var(--bt-px);
42
+ border: 1px solid var(--bt-line);
43
+ border-radius: var(--bt-radius);
44
+ background: var(--bt-fill);
45
+ color: var(--bt-ink);
46
+ font: inherit;
47
+ font-size: var(--bt-font-size);
48
+ font-weight: 500;
49
+ line-height: 1;
50
+ white-space: nowrap;
51
+ text-decoration: none;
52
+ vertical-align: middle;
53
+ cursor: pointer;
54
+ user-select: none;
55
+ -webkit-tap-highlight-color: transparent;
56
+ transition:
57
+ background-color 120ms ease,
58
+ border-color 120ms ease,
59
+ color 120ms ease,
60
+ scale 120ms ease;
61
+ }
62
+
63
+ :where(.dark, [data-theme="dark"]) .bt-root {
64
+ color-scheme: dark;
65
+ }
66
+ :where(.light, [data-theme="light"]) .bt-root {
67
+ color-scheme: light;
68
+ }
69
+
70
+ :where(.bt-root) *,
71
+ :where(.bt-root) *::before,
72
+ :where(.bt-root) *::after {
73
+ box-sizing: border-box;
74
+ }
75
+
76
+ /* ------------------------------------------------------------ variants */
77
+
78
+ .bt-root[data-variant="secondary"] {
79
+ --bt-fill: var(--bt-subtle);
80
+ --bt-fill-hover: color-mix(in oklab, var(--bt-subtle), var(--bt-fg) 8%);
81
+ --bt-ink: var(--bt-fg);
82
+ --bt-line: transparent;
83
+ }
84
+ .bt-root[data-variant="outline"] {
85
+ --bt-fill: var(--bt-bg);
86
+ --bt-fill-hover: var(--bt-hover);
87
+ --bt-ink: var(--bt-fg);
88
+ --bt-line: var(--bt-border);
89
+ }
90
+ .bt-root[data-variant="ghost"] {
91
+ --bt-fill: transparent;
92
+ --bt-fill-hover: var(--bt-hover);
93
+ --bt-ink: var(--bt-fg);
94
+ --bt-line: transparent;
95
+ }
96
+ .bt-root[data-variant="danger"] {
97
+ --bt-fill: var(--bt-danger);
98
+ --bt-fill-hover: color-mix(in oklab, var(--bt-danger), var(--bt-fg) 12%);
99
+ --bt-ink: var(--bt-danger-fg);
100
+ --bt-line: var(--bt-danger);
101
+ }
102
+ .bt-root[data-variant="link"] {
103
+ --bt-fill: transparent;
104
+ --bt-fill-hover: transparent;
105
+ --bt-ink: var(--bt-accent);
106
+ --bt-line: transparent;
107
+ --bt-px: 0px;
108
+ height: auto;
109
+ text-underline-offset: 3px;
110
+ }
111
+ .bt-root[data-variant="link"]:hover:not([aria-disabled="true"], :disabled) {
112
+ text-decoration: underline;
113
+ }
114
+
115
+ /* --------------------------------------------------------------- sizes */
116
+
117
+ .bt-root[data-size="sm"] {
118
+ --bt-height: 30px;
119
+ --bt-px: 10px;
120
+ --bt-gap: 6px;
121
+ --bt-font-size: 12px;
122
+ }
123
+ .bt-root[data-size="lg"] {
124
+ --bt-height: 44px;
125
+ --bt-px: 18px;
126
+ --bt-gap: 10px;
127
+ --bt-font-size: 14px;
128
+ }
129
+
130
+ /* -------------------------------------------------------------- states */
131
+
132
+ .bt-root:hover:not([aria-disabled="true"], :disabled) {
133
+ background: var(--bt-fill-hover);
134
+ }
135
+ .bt-root:active:not([aria-disabled="true"], :disabled) {
136
+ scale: 0.98;
137
+ }
138
+ .bt-root:focus-visible {
139
+ outline: 2px solid var(--bt-focus);
140
+ outline-offset: 2px;
141
+ }
142
+ .bt-root:disabled,
143
+ .bt-root[aria-disabled="true"]:not([data-busy]) {
144
+ opacity: 0.5;
145
+ cursor: not-allowed;
146
+ }
147
+ .bt-root[data-busy] {
148
+ cursor: progress;
149
+ }
150
+
151
+ .bt-root[data-icon-only] {
152
+ width: var(--bt-height);
153
+ padding: 0;
154
+ }
155
+ .bt-root[data-full-width] {
156
+ width: 100%;
157
+ }
158
+
159
+ /* ------------------------------------------------------------- content */
160
+
161
+ .bt-content,
162
+ .bt-spinner {
163
+ grid-area: 1 / 1;
164
+ display: inline-flex;
165
+ align-items: center;
166
+ justify-content: center;
167
+ gap: var(--bt-gap);
168
+ min-width: 0;
169
+ }
170
+ /* The label keeps its space while the spinner sits on top… */
171
+ .bt-content[data-hidden] {
172
+ visibility: hidden;
173
+ }
174
+ /* …unless loading text replaces it. */
175
+ .bt-root[data-loading-text] .bt-content {
176
+ display: none;
177
+ }
178
+
179
+ .bt-icon {
180
+ display: inline-flex;
181
+ flex: none;
182
+ }
183
+ .bt-icon > svg {
184
+ width: 1.15em;
185
+ height: 1.15em;
186
+ }
187
+
188
+ .bt-spinner-ring {
189
+ width: 1.15em;
190
+ height: 1.15em;
191
+ fill: none;
192
+ stroke: currentColor;
193
+ stroke-width: 2.5;
194
+ stroke-linecap: round;
195
+ stroke-dasharray: 30 100;
196
+ animation: bt-rotate 0.8s linear infinite;
197
+ }
198
+ @keyframes bt-rotate {
199
+ to {
200
+ transform: rotate(360deg);
201
+ }
202
+ }
203
+
204
+ .bt-sr-only {
205
+ position: absolute;
206
+ width: 1px;
207
+ height: 1px;
208
+ margin: -1px;
209
+ padding: 0;
210
+ overflow: hidden;
211
+ clip-path: inset(50%);
212
+ white-space: nowrap;
213
+ border: 0;
214
+ }
215
+
216
+ @media (prefers-reduced-motion: reduce) {
217
+ .bt-root {
218
+ transition: none;
219
+ }
220
+ .bt-root:active:not([aria-disabled="true"], :disabled) {
221
+ scale: none;
222
+ }
223
+ .bt-spinner-ring {
224
+ animation-duration: 2.4s;
225
+ }
226
+ }
227
+
228
+ @media (forced-colors: active) {
229
+ .bt-root {
230
+ border-color: ButtonText;
231
+ }
232
+ }
233
+ }
@@ -0,0 +1,36 @@
1
+ # Checkbox and CheckboxGroup
2
+
3
+ Checkboxes for React 19, styled to match the rest of the library. The box is a
4
+ real `<input type="checkbox">`, so forms, form libraries and assistive
5
+ technology work as they do natively. No runtime dependencies besides React.
6
+
7
+ ```ts
8
+ import { Checkbox, CheckboxGroup } from "@/components/checkbox";
9
+ import "@/components/checkbox/styles.css";
10
+ ```
11
+
12
+ ```tsx
13
+ <Checkbox label="Remember me" name="remember" checked={remember} onCheckedChange={setRemember} />
14
+
15
+ <CheckboxGroup
16
+ label="Notify me by"
17
+ name="channels"
18
+ options={[{ value: "email", label: "Email" }, { value: "sms", label: "SMS" }]}
19
+ value={channels}
20
+ onValueChange={setChannels}
21
+ selectAll
22
+ />
23
+ ```
24
+
25
+ **Checkbox:** `checked` / `defaultChecked` (`true`, `false`, `"indeterminate"`),
26
+ `onCheckedChange`, `value`, `label`, `description`, `error`, `size`, `classNames`
27
+ (`root` `control` `input` `label` `description` `error`), `ref` (the `<input>`), and
28
+ every `<input>` attribute.
29
+
30
+ **CheckboxGroup:** `value` / `defaultValue` / `onValueChange` (string arrays),
31
+ `options` or `<Checkbox value>` children, `label` (legend), `description`,
32
+ `error`, `required`, `disabled`, `name`, `size`, `orientation`, `selectAll`,
33
+ `classNames` (`root` `legend` `description` `items` `error`), `localeText`.
34
+
35
+ `toggleValue`, `groupState` and `toggleAll` are exported from the
36
+ framework-free `core`.
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { groupState, toggleAll, toggleValue } from "../core/group";
3
+
4
+ const options = [
5
+ { value: "email" },
6
+ { value: "sms" },
7
+ { value: "push", disabled: true },
8
+ ];
9
+
10
+ describe("toggling one value", () => {
11
+ it("adds and removes without duplicates", () => {
12
+ expect(toggleValue(["email"], "sms", true)).toEqual(["email", "sms"]);
13
+ expect(toggleValue(["email"], "email", true)).toEqual(["email"]);
14
+ expect(toggleValue(["email", "sms"], "email", false)).toEqual(["sms"]);
15
+ expect(toggleValue([], "email", false)).toEqual([]);
16
+ });
17
+ });
18
+
19
+ describe("select all", () => {
20
+ it("reports none, some or all", () => {
21
+ expect(groupState([], options)).toBe(false);
22
+ expect(groupState(["sms"], options)).toBe("indeterminate");
23
+ expect(groupState(["email", "sms", "push"], options)).toBe(true);
24
+ expect(groupState(["email"], [])).toBe(false);
25
+ });
26
+
27
+ it("selects every enabled option, leaving disabled ones alone", () => {
28
+ expect(toggleAll(["sms"], options)).toEqual(["sms", "email"]);
29
+ expect(toggleAll([], options)).toEqual(["email", "sms"]);
30
+ });
31
+
32
+ it("clears enabled options once they are all selected", () => {
33
+ expect(toggleAll(["email", "sms"], options)).toEqual([]);
34
+ expect(toggleAll(["push", "email", "sms"], options)).toEqual(["push"]);
35
+ });
36
+
37
+ it("keeps values that aren't options", () => {
38
+ expect(toggleAll(["legacy"], options)).toEqual(["legacy", "email", "sms"]);
39
+ });
40
+ });
@@ -0,0 +1,30 @@
1
+ import type { CheckedState } from "./types";
2
+
3
+ /** Adds or removes one value, keeping the order values were selected in. */
4
+ export function toggleValue(values: readonly string[], value: string, checked: boolean): string[] {
5
+ const has = values.includes(value);
6
+ if (checked) return has ? [...values] : [...values, value];
7
+ return values.filter((item) => item !== value);
8
+ }
9
+
10
+ /** A "select all" box's state for the options in a group. */
11
+ export function groupState(values: readonly string[], options: readonly { value: string }[]): CheckedState {
12
+ if (options.length === 0) return false;
13
+ const selected = options.filter((option) => values.includes(option.value)).length;
14
+ if (selected === 0) return false;
15
+ return selected === options.length ? true : "indeterminate";
16
+ }
17
+
18
+ /**
19
+ * What "select all" does: when every enabled option is already selected, clear
20
+ * them; otherwise select them all. Disabled options keep their state either way.
21
+ */
22
+ export function toggleAll(
23
+ values: readonly string[],
24
+ options: readonly { value: string; disabled?: boolean }[],
25
+ ): string[] {
26
+ const enabled = options.filter((option) => !option.disabled).map((option) => option.value);
27
+ const allOn = enabled.every((value) => values.includes(value));
28
+ if (allOn) return values.filter((value) => !enabled.includes(value));
29
+ return [...values, ...enabled.filter((value) => !values.includes(value))];
30
+ }
@@ -0,0 +1,3 @@
1
+ // Framework-free: selection rules for checkbox groups.
2
+ export { groupState, toggleAll, toggleValue } from "./group";
3
+ export type * from "./types";
@@ -0,0 +1,17 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export type CheckboxSize = "sm" | "md" | "lg";
4
+
5
+ /** `true`, `false`, or partly checked. */
6
+ export type CheckedState = boolean | "indeterminate";
7
+
8
+ export interface CheckboxOption {
9
+ value: string;
10
+ label: ReactNode;
11
+ description?: ReactNode;
12
+ disabled?: boolean;
13
+ }
14
+
15
+ export interface CheckboxLocaleText {
16
+ selectAll: string;
17
+ }
@@ -0,0 +1,9 @@
1
+ export { Checkbox } from "./react/Checkbox";
2
+ export type { CheckboxProps } from "./react/Checkbox";
3
+ export { CheckboxGroup } from "./react/CheckboxGroup";
4
+ export type { CheckboxGroupProps } from "./react/CheckboxGroup";
5
+ export { CheckboxGroupContext } from "./react/context";
6
+ export type { CheckboxGroupContextValue } from "./react/context";
7
+ export { defaultCheckboxText } from "./react/locale";
8
+ export type { CheckboxGroupSlot, CheckboxSlot } from "./react/props";
9
+ export * from "./core";
@@ -0,0 +1,147 @@
1
+ "use client";
2
+
3
+ import {
4
+ use,
5
+ useId,
6
+ useImperativeHandle,
7
+ useLayoutEffect,
8
+ useRef,
9
+ type ChangeEvent,
10
+ type ChangeEventHandler,
11
+ type InputHTMLAttributes,
12
+ type ReactNode,
13
+ type Ref,
14
+ } from "react";
15
+ import { useControllableState } from "../../shared/react/useControllableState";
16
+ import { describeField } from "../../shared/core/field";
17
+ import type { CheckboxSize, CheckedState } from "../core/types";
18
+ import { CheckboxGroupContext } from "./context";
19
+ import { cx, type CheckboxSlot } from "./props";
20
+
21
+ export interface CheckboxProps
22
+ extends Omit<
23
+ InputHTMLAttributes<HTMLInputElement>,
24
+ "type" | "size" | "checked" | "defaultChecked" | "onChange" | "value"
25
+ > {
26
+ /** `true`, `false` or `"indeterminate"` (controlled). */
27
+ checked?: CheckedState;
28
+ defaultChecked?: CheckedState;
29
+ onCheckedChange?(checked: boolean): void;
30
+ onChange?: ChangeEventHandler<HTMLInputElement>;
31
+ /** Posted with the form when checked. Inside a CheckboxGroup, the value this box adds. */
32
+ value?: string;
33
+ label?: ReactNode;
34
+ description?: ReactNode;
35
+ error?: ReactNode;
36
+ size?: CheckboxSize;
37
+ classNames?: Partial<Record<CheckboxSlot, string>>;
38
+ /** The `<input>` element. */
39
+ ref?: Ref<HTMLInputElement>;
40
+ }
41
+
42
+ /**
43
+ * A native checkbox, styled. It posts with forms, works with form libraries and
44
+ * reads as a checkbox to every assistive technology, including the mixed state.
45
+ */
46
+ export function Checkbox(props: CheckboxProps) {
47
+ const {
48
+ ref,
49
+ checked: checkedProp,
50
+ defaultChecked = false,
51
+ onCheckedChange,
52
+ onChange,
53
+ value,
54
+ label,
55
+ description,
56
+ error,
57
+ size,
58
+ className,
59
+ classNames,
60
+ style,
61
+ id: idProp,
62
+ disabled,
63
+ required,
64
+ name,
65
+ "aria-describedby": describedByProp,
66
+ ...inputProps
67
+ } = props;
68
+
69
+ const group = use(CheckboxGroupContext);
70
+ const reactId = useId();
71
+ const id = idProp ?? reactId;
72
+ const inputRef = useRef<HTMLInputElement>(null);
73
+ useImperativeHandle(ref, () => inputRef.current as HTMLInputElement, []);
74
+
75
+ const [own, setOwn] = useControllableState<CheckedState>(checkedProp, defaultChecked);
76
+ const inGroup = group !== null && value !== undefined;
77
+ // Inside a group the group owns the state, whoever owns it outside one.
78
+ const state: CheckedState = inGroup ? group.values.includes(value) : own;
79
+
80
+ // "Indeterminate" exists only as a DOM property, not an attribute.
81
+ useLayoutEffect(() => {
82
+ if (inputRef.current) inputRef.current.indeterminate = state === "indeterminate";
83
+ }, [state]);
84
+
85
+ const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
86
+ const next = event.target.checked;
87
+ if (inGroup) group.toggle(value, next);
88
+ else setOwn(next);
89
+ onChange?.(event);
90
+ onCheckedChange?.(next);
91
+ };
92
+
93
+ const invalid = Boolean(error) || (group?.invalid ?? false);
94
+ const describedBy = describeField(id, { extra: describedByProp, description, error });
95
+
96
+ return (
97
+ <div
98
+ className={cx("ck-root", classNames?.root, className)}
99
+ style={style}
100
+ data-size={size ?? group?.size ?? "md"}
101
+ data-disabled={disabled || group?.disabled || undefined}
102
+ data-invalid={invalid ? "" : undefined}
103
+ >
104
+ <span className={cx("ck-control", classNames?.control)}>
105
+ <input
106
+ {...inputProps}
107
+ ref={inputRef}
108
+ id={id}
109
+ type="checkbox"
110
+ className={cx("ck-input", classNames?.input)}
111
+ checked={state === true}
112
+ value={value}
113
+ name={name ?? group?.name}
114
+ disabled={disabled}
115
+ required={required}
116
+ aria-invalid={invalid || undefined}
117
+ aria-describedby={describedBy}
118
+ onChange={handleChange}
119
+ />
120
+ <svg className="ck-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
121
+ <path className="ck-check" d="M3.5 8.5 6.5 11.5 12.5 4.5" pathLength={1} />
122
+ <path className="ck-dash" d="M4 8h8" />
123
+ </svg>
124
+ </span>
125
+
126
+ {(label || description || error) && (
127
+ <span className="ck-text">
128
+ {label && (
129
+ <label htmlFor={id} className={cx("ck-label", classNames?.label)} data-required={required || undefined}>
130
+ {label}
131
+ </label>
132
+ )}
133
+ {description && (
134
+ <span id={`${id}-description`} className={cx("ck-description", classNames?.description)}>
135
+ {description}
136
+ </span>
137
+ )}
138
+ {error && (
139
+ <span id={`${id}-error`} className={cx("ck-error", classNames?.error)} role="alert">
140
+ {error}
141
+ </span>
142
+ )}
143
+ </span>
144
+ )}
145
+ </div>
146
+ );
147
+ }
@@ -0,0 +1,149 @@
1
+ "use client";
2
+
3
+ import { useId, useMemo, type CSSProperties, type ReactNode } from "react";
4
+ import { useControllableState } from "../../shared/react/useControllableState";
5
+ import { describeField } from "../../shared/core/field";
6
+ import { groupState, toggleAll, toggleValue } from "../core/group";
7
+ import type { CheckboxLocaleText, CheckboxOption, CheckboxSize } from "../core/types";
8
+ import { Checkbox } from "./Checkbox";
9
+ import { CheckboxGroupContext, type CheckboxGroupContextValue } from "./context";
10
+ import { defaultCheckboxText } from "./locale";
11
+ import { cx, type CheckboxGroupSlot } from "./props";
12
+
13
+ export interface CheckboxGroupProps {
14
+ /** Checked values (controlled). */
15
+ value?: string[];
16
+ defaultValue?: string[];
17
+ onValueChange?(values: string[]): void;
18
+ /** Render these options, or pass `<Checkbox value="…">` children. */
19
+ options?: readonly CheckboxOption[];
20
+ children?: ReactNode;
21
+ /** The group's name, shown as its legend. */
22
+ label?: ReactNode;
23
+ description?: ReactNode;
24
+ error?: ReactNode;
25
+ required?: boolean;
26
+ disabled?: boolean;
27
+ /** Form field name for every checkbox in the group. */
28
+ name?: string;
29
+ size?: CheckboxSize;
30
+ /** Default `vertical`. */
31
+ orientation?: "vertical" | "horizontal";
32
+ /** With `options`, a parent box that checks or clears them all. `true` uses the default label. */
33
+ selectAll?: boolean | ReactNode;
34
+ id?: string;
35
+ className?: string;
36
+ classNames?: Partial<Record<CheckboxGroupSlot, string>>;
37
+ style?: CSSProperties;
38
+ localeText?: Partial<CheckboxLocaleText>;
39
+ }
40
+
41
+ /** A labelled set of checkboxes sharing one array of values. */
42
+ export function CheckboxGroup(props: CheckboxGroupProps) {
43
+ const {
44
+ value,
45
+ defaultValue,
46
+ onValueChange,
47
+ options,
48
+ children,
49
+ label,
50
+ description,
51
+ error,
52
+ required = false,
53
+ disabled = false,
54
+ name,
55
+ size,
56
+ orientation = "vertical",
57
+ selectAll,
58
+ id: idProp,
59
+ className,
60
+ classNames,
61
+ style,
62
+ localeText,
63
+ } = props;
64
+
65
+ const reactId = useId();
66
+ const id = idProp ?? reactId;
67
+ const text = useMemo(() => ({ ...defaultCheckboxText, ...localeText }), [localeText]);
68
+ const [values, setValues] = useControllableState<string[]>(value, defaultValue ?? []);
69
+
70
+ const context = useMemo<CheckboxGroupContextValue>(() => {
71
+ const commit = (next: string[]) => {
72
+ setValues(next);
73
+ onValueChange?.(next);
74
+ };
75
+ return {
76
+ values,
77
+ toggle: (item, checked) => commit(toggleValue(values, item, checked)),
78
+ name,
79
+ disabled,
80
+ invalid: Boolean(error),
81
+ size,
82
+ };
83
+ }, [values, setValues, onValueChange, name, disabled, error, size]);
84
+
85
+ const showSelectAll = Boolean(selectAll) && options !== undefined && options.length > 0;
86
+ const describedBy = describeField(id, { description, error });
87
+
88
+ return (
89
+ <fieldset
90
+ id={id}
91
+ className={cx("ck-group", classNames?.root, className)}
92
+ style={style}
93
+ // A disabled fieldset disables every control inside it natively.
94
+ disabled={disabled}
95
+ aria-describedby={describedBy}
96
+ data-orientation={orientation}
97
+ data-select-all={showSelectAll || undefined}
98
+ data-invalid={error ? "" : undefined}
99
+ >
100
+ {label && (
101
+ <legend className={cx("ck-legend", classNames?.legend)} data-required={required || undefined}>
102
+ {label}
103
+ </legend>
104
+ )}
105
+ {description && (
106
+ <span id={`${id}-description`} className={cx("ck-description", classNames?.description)}>
107
+ {description}
108
+ </span>
109
+ )}
110
+
111
+ {showSelectAll && (
112
+ <Checkbox
113
+ className="ck-select-all"
114
+ label={selectAll === true ? text.selectAll : selectAll}
115
+ size={size}
116
+ disabled={disabled}
117
+ checked={groupState(values, options)}
118
+ onCheckedChange={() => {
119
+ const next = toggleAll(values, options);
120
+ setValues(next);
121
+ onValueChange?.(next);
122
+ }}
123
+ />
124
+ )}
125
+
126
+ <CheckboxGroupContext value={context}>
127
+ <div className={cx("ck-items", classNames?.items)}>
128
+ {options
129
+ ? options.map((option) => (
130
+ <Checkbox
131
+ key={option.value}
132
+ value={option.value}
133
+ label={option.label}
134
+ description={option.description}
135
+ disabled={option.disabled}
136
+ />
137
+ ))
138
+ : children}
139
+ </div>
140
+ </CheckboxGroupContext>
141
+
142
+ {error && (
143
+ <span id={`${id}-error`} className={cx("ck-error", classNames?.error)} role="alert">
144
+ {error}
145
+ </span>
146
+ )}
147
+ </fieldset>
148
+ );
149
+ }
@@ -0,0 +1,16 @@
1
+ "use client";
2
+
3
+ import { createContext } from "react";
4
+ import type { CheckboxSize } from "../core/types";
5
+
6
+ export interface CheckboxGroupContextValue {
7
+ values: readonly string[];
8
+ toggle(value: string, checked: boolean): void;
9
+ name?: string;
10
+ disabled: boolean;
11
+ invalid: boolean;
12
+ size?: CheckboxSize;
13
+ }
14
+
15
+ /** Lets each Checkbox inside a CheckboxGroup read and change the group's values. */
16
+ export const CheckboxGroupContext = createContext<CheckboxGroupContextValue | null>(null);
@@ -0,0 +1,5 @@
1
+ import type { CheckboxLocaleText } from "../core/types";
2
+
3
+ export const defaultCheckboxText: CheckboxLocaleText = {
4
+ selectAll: "Select all",
5
+ };