seblify 0.2.1 → 0.3.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 (47) hide show
  1. package/README.md +118 -45
  2. package/dist/components/action/input/select/checkbox/Checkbox.svelte +378 -16
  3. package/dist/components/action/input/select/checkbox/Checkbox.svelte.d.ts +7 -0
  4. package/dist/components/action/input/select/checkbox/checkbox-relations.svelte.d.ts +47 -0
  5. package/dist/components/action/input/select/checkbox/checkbox-relations.svelte.js +154 -0
  6. package/dist/components/action/input/select/switch/Switch.svelte +349 -40
  7. package/dist/components/action/input/select/switch/Switch.svelte.d.ts +8 -3
  8. package/dist/components/display/annotation/tag/Tag.svelte +19 -0
  9. package/dist/components/display/data-display/metric/duration/Duration.svelte +64 -0
  10. package/dist/components/display/data-display/metric/duration/Duration.svelte.d.ts +16 -0
  11. package/dist/components/display/data-display/metric/relative-time/RelativeTime.svelte +104 -0
  12. package/dist/components/display/data-display/metric/relative-time/RelativeTime.svelte.d.ts +16 -0
  13. package/dist/components/display/data-display/visual/timeline/TimelineItem.svelte +19 -0
  14. package/dist/components/display/feedback/loading/loading-dots/LoadingDots.svelte +19 -0
  15. package/dist/components/display/feedback/loading/spinner/Spinner.svelte +19 -0
  16. package/dist/components/display/motion/collapse/Collapse.svelte +115 -0
  17. package/dist/components/display/motion/collapse/Collapse.svelte.d.ts +17 -0
  18. package/dist/components/display/typography/accordion/Accordion.svelte +347 -0
  19. package/dist/components/display/typography/accordion/Accordion.svelte.d.ts +13 -0
  20. package/dist/components/display/typography/accordion/AccordionGroup.svelte +132 -0
  21. package/dist/components/display/typography/accordion/AccordionGroup.svelte.d.ts +11 -0
  22. package/dist/components/display/typography/accordion/context.d.ts +8 -0
  23. package/dist/components/display/typography/accordion/context.js +8 -0
  24. package/dist/components/framework/overlay/tooltip/Tooltip.svelte +1012 -0
  25. package/dist/components/framework/overlay/tooltip/Tooltip.svelte.d.ts +31 -0
  26. package/dist/components/utility/format/duration/formatDuration.d.ts +15 -0
  27. package/dist/components/utility/format/duration/formatDuration.js +218 -0
  28. package/dist/components/utility/format/relative-time/formatRelativeTime.d.ts +35 -0
  29. package/dist/components/utility/format/relative-time/formatRelativeTime.js +343 -0
  30. package/dist/design-system/reserved-props/allowed-options.d.ts +6 -3
  31. package/dist/design-system/theme/tokens.js +48 -0
  32. package/dist/icons/generated/check-thick.svelte +16 -0
  33. package/dist/icons/generated/check-thick.svelte.d.ts +7 -0
  34. package/dist/icons/generated/check.svelte +16 -0
  35. package/dist/icons/generated/check.svelte.d.ts +7 -0
  36. package/dist/icons/generated/close-thick.svelte +16 -0
  37. package/dist/icons/generated/close-thick.svelte.d.ts +7 -0
  38. package/dist/icons/generated/close.svelte +16 -0
  39. package/dist/icons/generated/close.svelte.d.ts +7 -0
  40. package/dist/icons/generated/index.d.ts +4 -0
  41. package/dist/icons/generated/index.js +4 -0
  42. package/dist/icons/generated/manifest.js +28 -0
  43. package/dist/index.d.ts +9 -0
  44. package/dist/index.js +9 -0
  45. package/dist/styles/tone-context.css +19 -0
  46. package/dist/theme.css +22 -0
  47. package/package.json +15 -3
@@ -1,39 +1,277 @@
1
1
  <script lang="ts">
2
2
  import type { SeblifyProps } from 'seblify';
3
3
 
4
+ type CheckboxTone =
5
+ | 'neutral'
6
+ | 'primary'
7
+ | 'secondary'
8
+ | 'success'
9
+ | 'danger'
10
+ | 'warning'
11
+ | 'info'
12
+ | 'inverse';
13
+ type CheckboxVisualState =
14
+ | 'default'
15
+ | 'derived'
16
+ | 'indirect'
17
+ | 'direct-and-derived'
18
+ | 'direct-and-indirect';
19
+
4
20
  type Props = SeblifyProps<{
21
+ ariaLabel?: string;
5
22
  checked?: boolean;
6
23
  disabled?: boolean;
7
24
  label?: string;
8
25
  description?: string;
26
+ indeterminate?: boolean;
9
27
  name?: string;
28
+ onclick?: (event: MouseEvent) => void;
29
+ tone?: CheckboxTone;
30
+ visualState?: CheckboxVisualState;
10
31
  value?: string;
11
32
  }>;
12
33
 
13
34
  let {
35
+ ariaLabel,
14
36
  checked = $bindable(false),
15
37
  disabled = false,
16
38
  label,
17
39
  description,
40
+ indeterminate = false,
18
41
  name,
42
+ onclick,
43
+ tone,
44
+ visualState = 'default',
19
45
  value,
20
46
  }: Props = $props();
47
+ const checkboxId = $props.id();
48
+ let inputElement: HTMLInputElement | undefined;
49
+
50
+ const labelId = $derived(label ? `${checkboxId}-label` : undefined);
51
+ const descriptionId = $derived(
52
+ description ? `${checkboxId}-description` : undefined,
53
+ );
54
+
55
+ const dataState = $derived(
56
+ disabled && indeterminate
57
+ ? 'disabled-indeterminate'
58
+ : disabled && checked
59
+ ? 'disabled-checked'
60
+ : disabled
61
+ ? 'disabled-unchecked'
62
+ : indeterminate
63
+ ? 'indeterminate'
64
+ : checked
65
+ ? 'checked'
66
+ : 'unchecked',
67
+ );
68
+
69
+ $effect(() => {
70
+ if (inputElement) {
71
+ inputElement.checked = checked;
72
+ inputElement.indeterminate = indeterminate;
73
+ }
74
+ });
75
+
76
+ function handleInputChange(event: Event) {
77
+ if (disabled) {
78
+ const input = event.currentTarget as HTMLInputElement;
79
+
80
+ input.checked = checked;
81
+ input.indeterminate = indeterminate;
82
+ return;
83
+ }
84
+
85
+ checked = (event.currentTarget as HTMLInputElement).checked;
86
+ }
87
+
88
+ function handleInputClick(event: MouseEvent) {
89
+ if (disabled) {
90
+ event.preventDefault();
91
+ event.stopPropagation();
92
+
93
+ if (inputElement) {
94
+ inputElement.checked = checked;
95
+ inputElement.indeterminate = indeterminate;
96
+ }
97
+
98
+ return;
99
+ }
100
+
101
+ onclick?.(event);
102
+ }
21
103
  </script>
22
104
 
23
- <label class:disabled class="checkbox">
24
- <input bind:checked {disabled} {name} type="checkbox" {value} />
25
- <span class="content">
105
+ <label
106
+ class:disabled
107
+ class="seblify-checkbox seblify-tone-context"
108
+ data-indeterminate={indeterminate}
109
+ data-seblify-tone={tone ?? 'inherit'}
110
+ data-state={dataState}
111
+ data-visual-state={visualState}
112
+ >
113
+ <input
114
+ aria-checked={indeterminate ? 'mixed' : undefined}
115
+ aria-describedby={descriptionId}
116
+ aria-label={labelId ? undefined : ariaLabel}
117
+ aria-labelledby={labelId}
118
+ bind:this={inputElement}
119
+ checked={checked}
120
+ {disabled}
121
+ {name}
122
+ data-state={dataState}
123
+ onchange={handleInputChange}
124
+ onclick={handleInputClick}
125
+ type="checkbox"
126
+ {value}
127
+ />
128
+ <span class="seblify-checkbox__control" aria-hidden="true">
129
+ <span class="seblify-checkbox__mark"></span>
130
+ </span>
131
+ <span class="seblify-checkbox__content">
26
132
  {#if label}
27
- <span class="label">{label}</span>
133
+ <span class="seblify-checkbox__label" id={labelId}>{label}</span>
28
134
  {/if}
29
135
  {#if description}
30
- <span class="description">{description}</span>
136
+ <span class="seblify-checkbox__description" id={descriptionId}
137
+ >{description}</span
138
+ >
31
139
  {/if}
32
140
  </span>
33
141
  </label>
34
142
 
35
- <style>
36
- .checkbox {
143
+ <style>:where(.seblify-tone-context) {
144
+ --seblify-tone-fill: var(--seblify-color-fill-neutral, #516176);
145
+ --seblify-tone-fill-text: var(--seblify-color-fill-neutral-text, #ffffff);
146
+ --seblify-tone-surface: var(--seblify-color-surface-muted, #d9e0e8);
147
+ --seblify-tone-text: var(--seblify-color-text-muted, #516176);
148
+ --seblify-tone-border: var(--seblify-color-border, #c8d2df);
149
+ }
150
+
151
+ :where(.seblify-tone-context[data-seblify-tone='inherit']) {
152
+ --seblify-tone-fill: var(
153
+ --seblify-current-tone-fill,
154
+ var(--seblify-color-fill-neutral, #516176)
155
+ );
156
+ --seblify-tone-fill-text: var(
157
+ --seblify-current-tone-fill-text,
158
+ var(--seblify-color-fill-neutral-text, #ffffff)
159
+ );
160
+ --seblify-tone-surface: var(
161
+ --seblify-current-tone-surface,
162
+ var(--seblify-color-surface-muted, #d9e0e8)
163
+ );
164
+ --seblify-tone-text: var(
165
+ --seblify-current-tone-text,
166
+ var(--seblify-color-text-muted, #516176)
167
+ );
168
+ --seblify-tone-border: var(
169
+ --seblify-current-tone-border,
170
+ var(--seblify-color-border, #c8d2df)
171
+ );
172
+ }
173
+
174
+ :where(.seblify-tone-context[data-seblify-tone='neutral']) {
175
+ --seblify-tone-fill: var(--seblify-color-fill-neutral, #516176);
176
+ --seblify-tone-fill-text: var(--seblify-color-fill-neutral-text, #ffffff);
177
+ --seblify-tone-surface: var(--seblify-color-surface-muted, #d9e0e8);
178
+ --seblify-tone-text: var(--seblify-color-text-muted, #516176);
179
+ --seblify-tone-border: var(--seblify-color-border, #c8d2df);
180
+ }
181
+
182
+ :where(.seblify-tone-context[data-seblify-tone='strong']) {
183
+ --seblify-tone-fill: var(--seblify-color-fill-strong, #172033);
184
+ --seblify-tone-fill-text: var(--seblify-color-fill-strong-text, #ffffff);
185
+ --seblify-tone-surface: var(--seblify-color-surface-raised, #ffffff);
186
+ --seblify-tone-text: var(--seblify-color-text, #172033);
187
+ --seblify-tone-border: var(--seblify-color-border-strong, #aab7c5);
188
+ }
189
+
190
+ :where(.seblify-tone-context[data-seblify-tone='primary']) {
191
+ --seblify-tone-fill: var(--seblify-color-fill-primary, #146a6f);
192
+ --seblify-tone-fill-text: var(--seblify-color-fill-primary-text, #ffffff);
193
+ --seblify-tone-surface: var(--seblify-color-surface-accent, #f7fbfb);
194
+ --seblify-tone-text: var(--seblify-color-fill-primary, #146a6f);
195
+ --seblify-tone-border: var(--seblify-color-border-accent, #9ab7ba);
196
+ }
197
+
198
+ :where(.seblify-tone-context[data-seblify-tone='secondary']) {
199
+ --seblify-tone-fill: var(--seblify-color-fill-secondary, #5b5fc7);
200
+ --seblify-tone-fill-text: var(
201
+ --seblify-color-fill-secondary-text,
202
+ #ffffff
203
+ );
204
+ --seblify-tone-surface: var(--seblify-color-secondary-surface, #f0efff);
205
+ --seblify-tone-text: var(--seblify-color-secondary-text, #2f316f);
206
+ --seblify-tone-border: var(--seblify-color-secondary-border, #c8c6f4);
207
+ }
208
+
209
+ :where(.seblify-tone-context[data-seblify-tone='success']) {
210
+ --seblify-tone-fill: var(--seblify-color-fill-success, #1f7a3f);
211
+ --seblify-tone-fill-text: var(--seblify-color-fill-success-text, #ffffff);
212
+ --seblify-tone-surface: var(--seblify-color-success-surface, #e8f8ee);
213
+ --seblify-tone-text: var(--seblify-color-success-text, #143d25);
214
+ --seblify-tone-border: var(--seblify-color-success-border, #b8ddc7);
215
+ }
216
+
217
+ :where(.seblify-tone-context[data-seblify-tone='danger']) {
218
+ --seblify-tone-fill: var(--seblify-color-fill-danger, #c93434);
219
+ --seblify-tone-fill-text: var(--seblify-color-fill-danger-text, #ffffff);
220
+ --seblify-tone-surface: var(--seblify-color-error-surface, #fff0f0);
221
+ --seblify-tone-text: var(--seblify-color-error-text, #5a1717);
222
+ --seblify-tone-border: var(--seblify-color-error-border, #efb6b6);
223
+ }
224
+
225
+ :where(.seblify-tone-context[data-seblify-tone='warning']) {
226
+ --seblify-tone-fill: var(--seblify-color-fill-warning, #8a5a00);
227
+ --seblify-tone-fill-text: var(--seblify-color-fill-warning-text, #ffffff);
228
+ --seblify-tone-surface: var(--seblify-color-warning-surface, #fff6d8);
229
+ --seblify-tone-text: var(--seblify-color-warning-text, #4d3900);
230
+ --seblify-tone-border: var(--seblify-color-warning-border, #e8d28f);
231
+ }
232
+
233
+ :where(.seblify-tone-context[data-seblify-tone='info']) {
234
+ --seblify-tone-fill: var(--seblify-color-fill-info, #1f6fb2);
235
+ --seblify-tone-fill-text: var(--seblify-color-fill-info-text, #ffffff);
236
+ --seblify-tone-surface: var(--seblify-color-info-surface, #eaf3ff);
237
+ --seblify-tone-text: var(--seblify-color-info-text, #102f55);
238
+ --seblify-tone-border: var(--seblify-color-info-border, #bfd1ea);
239
+ }
240
+
241
+ :where(.seblify-tone-context[data-seblify-tone='inverse']) {
242
+ --seblify-tone-fill: var(--seblify-color-fill-inverse, #172033);
243
+ --seblify-tone-fill-text: var(--seblify-color-fill-inverse-text, #ffffff);
244
+ --seblify-tone-surface: var(--seblify-color-surface-inverse-hover, #263043);
245
+ --seblify-tone-text: var(--seblify-color-fill-inverse-text, #ffffff);
246
+ --seblify-tone-border: var(--seblify-color-border-inverse, #263043);
247
+ }
248
+
249
+ :where(.seblify-tone-publisher) {
250
+ --seblify-current-tone-fill: var(--seblify-tone-fill);
251
+ --seblify-current-tone-fill-text: var(--seblify-tone-fill-text);
252
+ --seblify-current-tone-surface: var(--seblify-tone-surface);
253
+ --seblify-current-tone-text: var(--seblify-tone-text);
254
+ --seblify-current-tone-border: var(--seblify-tone-border);
255
+ }
256
+
257
+ .seblify-checkbox {
258
+ --checkbox-size: 1.125rem;
259
+ --checkbox-radius: 0.25rem;
260
+ --checkbox-border: var(--seblify-color-border-strong, #aab7c5);
261
+ --checkbox-surface: var(--seblify-color-surface, #ffffff);
262
+ --checkbox-active-border: var(--seblify-tone-fill);
263
+ --checkbox-active-surface: var(--seblify-tone-fill);
264
+ --checkbox-active-mark: var(--seblify-tone-fill-text);
265
+ --checkbox-focus-ring: color-mix(
266
+ in srgb,
267
+ var(--seblify-tone-fill) 54%,
268
+ transparent
269
+ );
270
+ --checkbox-derived-border: var(--seblify-color-border-strong, #aab7c5);
271
+ --checkbox-derived-surface: var(--seblify-color-surface-muted, #d9e0e8);
272
+ --checkbox-derived-mark: var(--seblify-color-text-muted, #516176);
273
+
274
+ position: relative;
37
275
  display: inline-flex;
38
276
  gap: 10px;
39
277
  align-items: flex-start;
@@ -41,30 +279,154 @@
41
279
  cursor: pointer;
42
280
  }
43
281
 
44
- input {
45
- width: 18px;
46
- height: 18px;
47
- margin: 2px 0 0;
48
- accent-color: var(--seblify-color-fill-accent, #146a6f);
282
+ .seblify-checkbox[data-seblify-tone='inherit'] {
283
+ --seblify-tone-fill: var(
284
+ --seblify-current-tone-fill,
285
+ var(--seblify-color-fill-primary, #146a6f)
286
+ );
287
+ --seblify-tone-fill-text: var(
288
+ --seblify-current-tone-fill-text,
289
+ var(--seblify-color-fill-primary-text, #ffffff)
290
+ );
291
+ }
292
+
293
+ .seblify-checkbox > input {
294
+ position: absolute;
295
+ width: 1px !important;
296
+ height: 1px !important;
297
+ padding: 0 !important;
298
+ border: 0 !important;
299
+ margin: -1px !important;
300
+ overflow: hidden;
301
+ background: transparent !important;
302
+ clip: rect(0 0 0 0);
303
+ clip-path: inset(50%);
304
+ white-space: nowrap;
305
+ }
306
+
307
+ .seblify-checkbox__control {
308
+ position: relative;
309
+ display: inline-flex;
310
+ width: var(--checkbox-size);
311
+ height: var(--checkbox-size);
312
+ flex: 0 0 auto;
313
+ align-items: center;
314
+ justify-content: center;
315
+ border: 1px solid var(--checkbox-border);
316
+ border-radius: var(--checkbox-radius);
317
+ margin-top: 0.125rem;
318
+ background: var(--checkbox-surface);
319
+ box-sizing: border-box;
320
+ color: var(--checkbox-active-mark);
321
+ transition:
322
+ border-color 140ms ease,
323
+ background 140ms ease,
324
+ box-shadow 140ms ease;
325
+ }
326
+
327
+ .seblify-checkbox__mark {
328
+ position: relative;
329
+ display: block;
330
+ width: 0.625rem;
331
+ height: 0.625rem;
332
+ opacity: 0;
333
+ transform: scale(0.86);
334
+ transition:
335
+ opacity 120ms ease,
336
+ transform 120ms ease;
337
+ }
338
+
339
+ .seblify-checkbox[data-state='checked'] .seblify-checkbox__control,
340
+ .seblify-checkbox[data-state='indeterminate'] .seblify-checkbox__control,
341
+ .seblify-checkbox[data-state='disabled-checked'] .seblify-checkbox__control,
342
+ .seblify-checkbox[data-state='disabled-indeterminate']
343
+ .seblify-checkbox__control {
344
+ border-color: var(--checkbox-active-border);
345
+ background: var(--checkbox-active-surface);
346
+ }
347
+
348
+ .seblify-checkbox[data-state='checked'] .seblify-checkbox__mark,
349
+ .seblify-checkbox[data-state='indeterminate'] .seblify-checkbox__mark,
350
+ .seblify-checkbox[data-state='disabled-checked'] .seblify-checkbox__mark,
351
+ .seblify-checkbox[data-state='disabled-indeterminate']
352
+ .seblify-checkbox__mark {
353
+ opacity: 1;
354
+ transform: scale(1);
355
+ }
356
+
357
+ .seblify-checkbox[data-state='checked'] .seblify-checkbox__mark::before,
358
+ .seblify-checkbox[data-state='disabled-checked']
359
+ .seblify-checkbox__mark::before {
360
+ position: absolute;
361
+ left: 50%;
362
+ top: 50%;
363
+ width: 0.25rem;
364
+ height: 0.55rem;
365
+ border: solid currentColor;
366
+ border-width: 0 0.125rem 0.125rem 0;
367
+ content: '';
368
+ transform: translate(-50%, -58%) rotate(45deg);
369
+ }
370
+
371
+ .seblify-checkbox[data-state='indeterminate']
372
+ .seblify-checkbox__mark::before,
373
+ .seblify-checkbox[data-state='disabled-indeterminate']
374
+ .seblify-checkbox__mark::before {
375
+ position: absolute;
376
+ left: 0.0625rem;
377
+ right: 0.0625rem;
378
+ top: 50%;
379
+ height: 0.125rem;
380
+ border-radius: 999px;
381
+ background: currentColor;
382
+ content: '';
383
+ transform: translateY(-50%);
384
+ }
385
+
386
+ .seblify-checkbox > input:focus-visible + .seblify-checkbox__control {
387
+ outline: 3px solid var(--checkbox-focus-ring);
388
+ outline-offset: 2px;
389
+ }
390
+
391
+ .seblify-checkbox[data-visual-state='derived']
392
+ .seblify-checkbox__control,
393
+ .seblify-checkbox[data-visual-state='indirect']
394
+ .seblify-checkbox__control {
395
+ --checkbox-active-border: var(--checkbox-derived-border);
396
+ --checkbox-active-surface: var(--checkbox-derived-surface);
397
+ --checkbox-active-mark: var(--checkbox-derived-mark);
398
+
399
+ border-color: var(--checkbox-derived-border);
400
+ background: var(--checkbox-derived-surface);
401
+ color: var(--checkbox-derived-mark);
402
+ }
403
+
404
+ .seblify-checkbox[data-visual-state='direct-and-derived']
405
+ .seblify-checkbox__control,
406
+ .seblify-checkbox[data-visual-state='direct-and-indirect']
407
+ .seblify-checkbox__control {
408
+ box-shadow: 0 0 0 2px
409
+ color-mix(in srgb, var(--seblify-tone-fill) 22%, transparent);
49
410
  }
50
411
 
51
- .content {
412
+ .seblify-checkbox__content {
52
413
  display: grid;
53
414
  gap: 2px;
415
+ min-width: 0;
54
416
  }
55
417
 
56
- .label {
418
+ .seblify-checkbox__label {
57
419
  font-weight: 700;
58
420
  line-height: 1.35;
59
421
  }
60
422
 
61
- .description {
423
+ .seblify-checkbox__description {
62
424
  color: var(--seblify-color-text-muted, #516176);
63
425
  font-size: 0.875rem;
64
426
  line-height: 1.45;
65
427
  }
66
428
 
67
- .disabled {
429
+ .seblify-checkbox.disabled {
68
430
  cursor: not-allowed;
69
431
  opacity: var(--seblify-opacity-disabled, 0.56);
70
432
  }
@@ -1,9 +1,16 @@
1
+ type CheckboxTone = 'neutral' | 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'inverse';
2
+ type CheckboxVisualState = 'default' | 'derived' | 'indirect' | 'direct-and-derived' | 'direct-and-indirect';
1
3
  declare const Checkbox: import("svelte").Component<{
4
+ ariaLabel?: string | undefined;
2
5
  checked?: boolean | undefined;
3
6
  disabled?: boolean | undefined;
4
7
  label?: string | undefined;
5
8
  description?: string | undefined;
9
+ indeterminate?: boolean | undefined;
6
10
  name?: string | undefined;
11
+ onclick?: ((event: MouseEvent) => void) | undefined;
12
+ tone?: CheckboxTone | undefined;
13
+ visualState?: CheckboxVisualState | undefined;
7
14
  value?: string | undefined;
8
15
  }, {}, "checked">;
9
16
  type Checkbox = ReturnType<typeof Checkbox>;
@@ -0,0 +1,47 @@
1
+ export type CheckboxRelationsId = string;
2
+ export interface CheckboxRelationsItemConfig {
3
+ checked?: boolean;
4
+ disabled?: boolean;
5
+ requires?: CheckboxRelationsId[];
6
+ selects?: CheckboxRelationsId[];
7
+ }
8
+ export type CheckboxRelationsGroupConfig = CheckboxRelationsId[] | {
9
+ itemIds: CheckboxRelationsId[];
10
+ };
11
+ export interface CheckboxRelationsConfig {
12
+ groups?: Record<CheckboxRelationsId, CheckboxRelationsGroupConfig>;
13
+ items: Record<CheckboxRelationsId, CheckboxRelationsItemConfig>;
14
+ }
15
+ export type CheckboxRelationsVisualState = 'default' | 'indirect';
16
+ export interface CheckboxRelationsItemState {
17
+ blockedBy: CheckboxRelationsId[];
18
+ checked: boolean;
19
+ directChecked: boolean;
20
+ disabled: boolean;
21
+ indirectChecked: boolean;
22
+ requires: CheckboxRelationsId[];
23
+ selects: CheckboxRelationsId[];
24
+ setChecked: (checked: boolean) => void;
25
+ toggle: () => void;
26
+ visualState: CheckboxRelationsVisualState;
27
+ }
28
+ export interface CheckboxRelationsGroupState {
29
+ checked: boolean;
30
+ directSelectedCount: number;
31
+ indeterminate: boolean;
32
+ indirectSelectedCount: number;
33
+ itemIds: CheckboxRelationsId[];
34
+ selectedCount: number;
35
+ selectableCount: number;
36
+ setChecked: (checked: boolean) => void;
37
+ toggle: () => void;
38
+ visualState: CheckboxRelationsVisualState;
39
+ }
40
+ export interface CheckboxRelations {
41
+ group: (id: CheckboxRelationsId) => CheckboxRelationsGroupState;
42
+ item: (id: CheckboxRelationsId) => CheckboxRelationsItemState;
43
+ setItemChecked: (id: CheckboxRelationsId, checked: boolean) => void;
44
+ toggleGroup: (id: CheckboxRelationsId) => void;
45
+ toggleItem: (id: CheckboxRelationsId) => void;
46
+ }
47
+ export declare function createCheckboxRelations(config: CheckboxRelationsConfig): CheckboxRelations;
@@ -0,0 +1,154 @@
1
+ export function createCheckboxRelations(config) {
2
+ const items = config.items;
3
+ const groups = normalizeGroups(config.groups ?? {});
4
+ let directChecked = $state(Object.fromEntries(Object.entries(items).map(([id, item]) => [id, item.checked === true])));
5
+ const effectiveChecked = $derived(resolveEffectiveChecked(items, groups, directChecked));
6
+ function item(id) {
7
+ const config = getItemConfig(items, id);
8
+ const blockedBy = getBlockedBy(items, id, effectiveChecked);
9
+ const checked = effectiveChecked.has(id);
10
+ const direct = directChecked[id] === true && checked;
11
+ const indirect = checked && !direct;
12
+ return {
13
+ blockedBy,
14
+ checked,
15
+ directChecked: direct,
16
+ disabled: config.disabled === true || blockedBy.length > 0,
17
+ indirectChecked: indirect,
18
+ requires: config.requires ?? [],
19
+ selects: config.selects ?? [],
20
+ setChecked: (checked) => setItemChecked(id, checked),
21
+ toggle: () => toggleItem(id),
22
+ visualState: indirect ? 'indirect' : 'default',
23
+ };
24
+ }
25
+ function group(id) {
26
+ const itemIds = getGroupItemIds(groups, id);
27
+ const selectableItems = itemIds
28
+ .map((itemId) => item(itemId))
29
+ .filter((itemState) => !itemState.disabled);
30
+ const selectedCount = selectableItems.filter((itemState) => itemState.checked).length;
31
+ const directSelectedCount = selectableItems.filter((itemState) => itemState.directChecked).length;
32
+ const indirectSelectedCount = selectableItems.filter((itemState) => itemState.indirectChecked).length;
33
+ const checked = selectableItems.length > 0 && selectedCount === selectableItems.length;
34
+ const visualState = selectedCount > 0 && directSelectedCount === 0 ? 'indirect' : 'default';
35
+ return {
36
+ checked,
37
+ directSelectedCount,
38
+ indeterminate: selectedCount > 0 && !checked,
39
+ indirectSelectedCount,
40
+ itemIds,
41
+ selectedCount,
42
+ selectableCount: selectableItems.length,
43
+ setChecked: (checked) => setGroupChecked(id, checked),
44
+ toggle: () => toggleGroup(id),
45
+ visualState,
46
+ };
47
+ }
48
+ function setItemChecked(id, checked) {
49
+ const currentItem = item(id);
50
+ if (currentItem.disabled && checked) {
51
+ return;
52
+ }
53
+ directChecked[id] = checked;
54
+ }
55
+ function toggleItem(id) {
56
+ const currentItem = item(id);
57
+ if (currentItem.disabled) {
58
+ return;
59
+ }
60
+ directChecked[id] = !currentItem.directChecked;
61
+ }
62
+ function setGroupChecked(id, checked) {
63
+ const groupItemIds = getGroupItemIds(groups, id);
64
+ if (!checked) {
65
+ const selectableItemIds = groupItemIds.filter((itemId) => !item(itemId).disabled);
66
+ for (const itemId of selectableItemIds) {
67
+ directChecked[itemId] = false;
68
+ }
69
+ return;
70
+ }
71
+ let changed = true;
72
+ while (changed) {
73
+ changed = false;
74
+ for (const itemId of groupItemIds) {
75
+ const currentItem = item(itemId);
76
+ if (!currentItem.disabled && directChecked[itemId] !== true) {
77
+ directChecked[itemId] = true;
78
+ changed = true;
79
+ }
80
+ }
81
+ }
82
+ }
83
+ function toggleGroup(id) {
84
+ const currentGroup = group(id);
85
+ setGroupChecked(id, !(currentGroup.checked && currentGroup.directSelectedCount > 0));
86
+ }
87
+ return {
88
+ group,
89
+ item,
90
+ setItemChecked,
91
+ toggleGroup,
92
+ toggleItem,
93
+ };
94
+ }
95
+ function normalizeGroups(groups) {
96
+ return Object.fromEntries(Object.entries(groups).map(([id, group]) => [
97
+ id,
98
+ Array.isArray(group) ? group : group.itemIds,
99
+ ]));
100
+ }
101
+ function getItemConfig(items, id) {
102
+ const item = items[id];
103
+ if (!item) {
104
+ throw new Error(`Unknown checkbox relations item "${id}".`);
105
+ }
106
+ return item;
107
+ }
108
+ function getGroupItemIds(groups, id) {
109
+ const itemIds = groups[id];
110
+ if (!itemIds) {
111
+ throw new Error(`Unknown checkbox relations group "${id}".`);
112
+ }
113
+ return itemIds;
114
+ }
115
+ function resolveEffectiveChecked(items, groups, directChecked) {
116
+ const effectiveChecked = new Set();
117
+ let changed = true;
118
+ while (changed) {
119
+ changed = false;
120
+ for (const [id, item] of Object.entries(items)) {
121
+ if (directChecked[id] === true &&
122
+ item.disabled !== true &&
123
+ hasRequiredItems(item, effectiveChecked) &&
124
+ !effectiveChecked.has(id)) {
125
+ effectiveChecked.add(id);
126
+ changed = true;
127
+ }
128
+ }
129
+ for (const id of Array.from(effectiveChecked)) {
130
+ for (const selectedId of getSelectedItemIds(items, groups, id)) {
131
+ const selectedItem = getItemConfig(items, selectedId);
132
+ if (selectedItem.disabled !== true &&
133
+ hasRequiredItems(selectedItem, effectiveChecked) &&
134
+ !effectiveChecked.has(selectedId)) {
135
+ effectiveChecked.add(selectedId);
136
+ changed = true;
137
+ }
138
+ }
139
+ }
140
+ }
141
+ return effectiveChecked;
142
+ }
143
+ function getSelectedItemIds(items, groups, id) {
144
+ return (items[id]?.selects ?? []).flatMap((selectedId) => [
145
+ ...(items[selectedId] ? [selectedId] : []),
146
+ ...(groups[selectedId] ?? []),
147
+ ]);
148
+ }
149
+ function hasRequiredItems(item, effectiveChecked) {
150
+ return (item.requires ?? []).every((id) => effectiveChecked.has(id));
151
+ }
152
+ function getBlockedBy(items, id, effectiveChecked) {
153
+ return (getItemConfig(items, id).requires ?? []).filter((requiredId) => !effectiveChecked.has(requiredId));
154
+ }