@marianmeres/stuic 3.66.1 → 3.67.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 (75) hide show
  1. package/dist/actions/autoscroll.d.ts +7 -0
  2. package/dist/actions/autoscroll.js +7 -0
  3. package/dist/actions/focus-trap.d.ts +7 -0
  4. package/dist/actions/focus-trap.js +8 -3
  5. package/dist/actions/typeahead.svelte.js +40 -4
  6. package/dist/components/Carousel/Carousel.svelte +9 -2
  7. package/dist/components/Carousel/README.md +8 -2
  8. package/dist/components/Cart/Cart.svelte +3 -0
  9. package/dist/components/Cart/README.md +18 -1
  10. package/dist/components/Checkout/CheckoutOrderReview.svelte +4 -14
  11. package/dist/components/Checkout/README.md +184 -0
  12. package/dist/components/Checkout/_internal/checkout-utils.d.ts +6 -0
  13. package/dist/components/Checkout/_internal/checkout-utils.js +24 -0
  14. package/dist/components/Checkout/index.d.ts +1 -1
  15. package/dist/components/Checkout/index.js +1 -1
  16. package/dist/components/CommandMenu/CommandMenu.svelte +23 -7
  17. package/dist/components/CommandMenu/CommandMenu.svelte.d.ts +2 -0
  18. package/dist/components/CronInput/CronInput.svelte +44 -9
  19. package/dist/components/CronInput/CronInput.svelte.d.ts +2 -0
  20. package/dist/components/CronInput/README.md +145 -0
  21. package/dist/components/CronInput/cron-next-run.svelte.d.ts +11 -0
  22. package/dist/components/CronInput/cron-next-run.svelte.js +11 -0
  23. package/dist/components/CronInput/index.css +0 -8
  24. package/dist/components/DataTable/DataTable.svelte +99 -62
  25. package/dist/components/DataTable/DataTable.svelte.d.ts +13 -3
  26. package/dist/components/DataTable/README.md +79 -25
  27. package/dist/components/DataTable/index.css +7 -0
  28. package/dist/components/DropdownMenu/DropdownMenu.svelte +43 -26
  29. package/dist/components/DropdownMenu/DropdownMenu.svelte.d.ts +5 -1
  30. package/dist/components/DropdownMenu/README.md +37 -9
  31. package/dist/components/Input/FieldAssets.svelte +9 -7
  32. package/dist/components/Input/FieldAssets.svelte.d.ts +3 -7
  33. package/dist/components/Input/FieldFile.svelte +13 -7
  34. package/dist/components/Input/FieldFile.svelte.d.ts +4 -7
  35. package/dist/components/Input/FieldInput.svelte +10 -8
  36. package/dist/components/Input/FieldInput.svelte.d.ts +3 -8
  37. package/dist/components/Input/FieldInputLocalized.svelte +8 -7
  38. package/dist/components/Input/FieldInputLocalized.svelte.d.ts +2 -7
  39. package/dist/components/Input/FieldKeyValues.svelte +8 -7
  40. package/dist/components/Input/FieldKeyValues.svelte.d.ts +2 -7
  41. package/dist/components/Input/FieldLikeButton.svelte +9 -7
  42. package/dist/components/Input/FieldLikeButton.svelte.d.ts +3 -7
  43. package/dist/components/Input/FieldObject.svelte +8 -7
  44. package/dist/components/Input/FieldObject.svelte.d.ts +2 -7
  45. package/dist/components/Input/FieldOptions.svelte +9 -7
  46. package/dist/components/Input/FieldOptions.svelte.d.ts +3 -7
  47. package/dist/components/Input/FieldPhoneNumber.svelte +7 -8
  48. package/dist/components/Input/FieldPhoneNumber.svelte.d.ts +3 -8
  49. package/dist/components/Input/FieldSelect.svelte +9 -8
  50. package/dist/components/Input/FieldSelect.svelte.d.ts +3 -8
  51. package/dist/components/Input/FieldSwitch.svelte +9 -7
  52. package/dist/components/Input/FieldSwitch.svelte.d.ts +3 -7
  53. package/dist/components/Input/FieldTextarea.svelte +7 -8
  54. package/dist/components/Input/FieldTextarea.svelte.d.ts +3 -8
  55. package/dist/components/Input/README.md +20 -0
  56. package/dist/components/Input/_internal/InputWrap.svelte +2 -10
  57. package/dist/components/Input/_internal/InputWrap.svelte.d.ts +2 -10
  58. package/dist/components/Input/types.d.ts +28 -0
  59. package/dist/components/Nav/Nav.svelte +5 -4
  60. package/dist/components/Nav/Nav.svelte.d.ts +2 -2
  61. package/dist/components/Nav/README.md +2 -2
  62. package/dist/components/Nav/index.css +4 -0
  63. package/dist/components/Tree/README.md +189 -0
  64. package/dist/components/Tree/Tree.svelte +46 -2
  65. package/dist/components/Tree/Tree.svelte.d.ts +5 -0
  66. package/dist/utils/input-history.svelte.d.ts +12 -0
  67. package/dist/utils/input-history.svelte.js +12 -0
  68. package/dist/utils/observe-exists.svelte.d.ts +1 -0
  69. package/dist/utils/observe-exists.svelte.js +11 -3
  70. package/dist/utils/switch.svelte.d.ts +12 -0
  71. package/dist/utils/switch.svelte.js +12 -1
  72. package/docs/architecture.md +0 -1
  73. package/docs/testing.md +72 -0
  74. package/docs/upgrading.md +281 -0
  75. package/package.json +12 -13
@@ -58,6 +58,8 @@
58
58
  classOption?: string;
59
59
  classOptionActive?: string;
60
60
  showAllOnEmptyQ?: boolean;
61
+ /** Skip stuic base classes (user provides their own styling) */
62
+ unstyled?: boolean;
61
63
  }
62
64
  </script>
63
65
 
@@ -81,6 +83,7 @@
81
83
  itemIdPropName = "id",
82
84
  searchPlaceholder,
83
85
  showAllOnEmptyQ,
86
+ unstyled = false,
84
87
  }: Props = $props();
85
88
 
86
89
  function _renderOptionLabel(item: Item): string {
@@ -138,10 +141,16 @@
138
141
  _optionsColl.clear().addMany(res);
139
142
  })
140
143
  .catch((e) => {
144
+ // Surface errors only for the latest in-flight request
145
+ if (currentRequest !== fetchRequestId) return;
141
146
  console.error(e);
142
147
  notifications?.error(`${e}`);
143
148
  })
144
- .finally(() => (isFetching = false));
149
+ .finally(() => {
150
+ // Only the latest request toggles the fetching state back off
151
+ // so the spinner doesn't flicker when a stale response arrives late.
152
+ if (currentRequest === fetchRequestId) isFetching = false;
153
+ });
145
154
  });
146
155
 
147
156
  //
@@ -263,14 +272,20 @@
263
272
  >
264
273
  {#snippet inputBefore()}
265
274
  <div
266
- class="flex flex-col items-center justify-center pl-3 stuic-command-menu-muted"
275
+ class={twMerge(
276
+ "flex flex-col items-center justify-center pl-3",
277
+ !unstyled && "stuic-command-menu-muted"
278
+ )}
267
279
  >
268
280
  {@html iconSearch({ size: 19 })}
269
281
  </div>
270
282
  {/snippet}
271
283
  {#snippet inputAfter()}
272
284
  <div
273
- class="flex pl-2 items-center justify-center stuic-command-menu-placeholder"
285
+ class={twMerge(
286
+ "flex pl-2 items-center justify-center",
287
+ !unstyled && "stuic-command-menu-placeholder"
288
+ )}
274
289
  >
275
290
  {#if isFetching}
276
291
  <Spinner class="w-4" />
@@ -302,7 +317,7 @@
302
317
  {#if options.size}
303
318
  <div
304
319
  class={twMerge(
305
- "stuic-command-menu-options",
320
+ !unstyled && "stuic-command-menu-options",
306
321
  "block space-y-1 p-1",
307
322
  "overflow-y-auto overflow-x-hidden mb-1",
308
323
  "border-t"
@@ -314,11 +329,13 @@
314
329
  aria-label="Search results"
315
330
  >
316
331
  {#each _normalize_and_group_options(options.items) as [_optgroup, _opts]}
317
- <!-- {console.log(11111, _optgroup, _opts)} -->
318
332
  <div class="p-1">
319
333
  {#if _optgroup}
320
334
  <div
321
- class="stuic-command-menu-group-header mb-1 p-1 font-semibold uppercase tracking-wide"
335
+ class={twMerge(
336
+ "mb-1 p-1 font-semibold uppercase tracking-wide",
337
+ !unstyled && "stuic-command-menu-group-header"
338
+ )}
322
339
  >
323
340
  {_optgroup}
324
341
  </div>
@@ -327,7 +344,6 @@
327
344
  {#each _opts as item (item[itemIdPropName])}
328
345
  {@const active =
329
346
  item[itemIdPropName] === options.active?.[itemIdPropName]}
330
- <!-- {@const isSelected = false} -->
331
347
  <li class:active role="presentation">
332
348
  <ListItemButton
333
349
  id={btn_id(item[itemIdPropName])}
@@ -16,6 +16,8 @@ export interface Props {
16
16
  classOption?: string;
17
17
  classOptionActive?: string;
18
18
  showAllOnEmptyQ?: boolean;
19
+ /** Skip stuic base classes (user provides their own styling) */
20
+ unstyled?: boolean;
19
21
  }
20
22
  declare const CommandMenu: import("svelte").Component<Props, {
21
23
  close: () => void;
@@ -17,6 +17,8 @@
17
17
  value?: string;
18
18
  el?: HTMLElement;
19
19
  id?: string;
20
+ /** Opt out of stuic base classes for full styling control */
21
+ unstyled?: boolean;
20
22
 
21
23
  // Mode toggle (overrides show* flags when defined)
22
24
  mode?: CronInputMode;
@@ -185,6 +187,7 @@
185
187
  value = $bindable("* * * * *"),
186
188
  el = $bindable(),
187
189
  id = getId(),
190
+ unstyled = false,
188
191
  //
189
192
  mode = $bindable<CronInputMode | undefined>("predefined"),
190
193
  //
@@ -435,14 +438,21 @@
435
438
  >
436
439
  <div class="w-full flex">
437
440
  <div
438
- class={twMerge("stuic-cron-input-content", error && "has-error")}
441
+ class={unstyled
442
+ ? error
443
+ ? "has-error"
444
+ : undefined
445
+ : twMerge("stuic-cron-input-content", error && "has-error")}
439
446
  data-presets-only={presetsOnly ? "" : undefined}
440
447
  >
441
448
  {#if _showPresets}
442
449
  <select
443
- class={twMerge("stuic-cron-input-preset", classPreset)}
450
+ class={unstyled
451
+ ? classPreset
452
+ : twMerge("stuic-cron-input-preset", classPreset)}
444
453
  bind:value={selectedPreset}
445
454
  onchange={onPresetChange}
455
+ aria-label="Cron schedule preset"
446
456
  {disabled}
447
457
  >
448
458
  <option value="">Custom</option>
@@ -453,18 +463,33 @@
453
463
  {/if}
454
464
 
455
465
  {#if _showFields}
456
- <div class={twMerge("stuic-cron-input-fields", classFields)}>
466
+ <div
467
+ class={unstyled
468
+ ? classFields
469
+ : twMerge("stuic-cron-input-fields", classFields)}
470
+ >
457
471
  {#each FIELD_DEFS as def}
458
- <div class={twMerge("stuic-cron-input-field", classField)}>
459
- <span class={twMerge("stuic-cron-input-field-label", classFieldLabel)}>
472
+ <div
473
+ class={unstyled
474
+ ? classField
475
+ : twMerge("stuic-cron-input-field", classField)}
476
+ >
477
+ <span
478
+ class={unstyled
479
+ ? classFieldLabel
480
+ : twMerge("stuic-cron-input-field-label", classFieldLabel)}
481
+ >
460
482
  {def.label}
461
483
  </span>
462
484
  <input
463
485
  type="text"
464
- class={twMerge("stuic-cron-input-field-input", classFieldInput)}
486
+ class={unstyled
487
+ ? classFieldInput
488
+ : twMerge("stuic-cron-input-field-input", classFieldInput)}
465
489
  bind:value={fields[def.key]}
466
490
  oninput={onFieldInput}
467
491
  placeholder={def.placeholder}
492
+ aria-label={`${def.label} (${def.placeholder})`}
468
493
  {disabled}
469
494
  autocomplete="off"
470
495
  spellcheck={false}
@@ -477,10 +502,13 @@
477
502
  {#if _showRawInput}
478
503
  <input
479
504
  type="text"
480
- class={twMerge("stuic-cron-input-raw", classRaw)}
505
+ class={unstyled
506
+ ? classRaw
507
+ : twMerge("stuic-cron-input-raw", classRaw)}
481
508
  bind:value={rawValue}
482
509
  oninput={onRawInput}
483
510
  placeholder="* * * * *"
511
+ aria-label="Raw cron expression"
484
512
  {disabled}
485
513
  autocomplete="off"
486
514
  spellcheck={false}
@@ -488,7 +516,11 @@
488
516
  {/if}
489
517
 
490
518
  {#if (_showDescription || _showNextRun) && humanDescription}
491
- <div class={twMerge("stuic-cron-input-summary", classSummary)}>
519
+ <div
520
+ class={unstyled
521
+ ? classSummary
522
+ : twMerge("stuic-cron-input-summary", classSummary)}
523
+ >
492
524
  {humanDescription}
493
525
  </div>
494
526
  {/if}
@@ -497,8 +529,11 @@
497
529
  {#if hasModeToggle}
498
530
  <button
499
531
  type="button"
500
- class={twMerge(BTN_CLS, classToggleButton)}
532
+ class={unstyled
533
+ ? classToggleButton
534
+ : twMerge(BTN_CLS, classToggleButton)}
501
535
  onclick={toggleMode}
536
+ aria-label={mode === "predefined" ? "Switch to manual input" : "Switch to presets"}
502
537
  {disabled}
503
538
  use:tooltip={() => ({
504
539
  enabled: true,
@@ -13,6 +13,8 @@ export interface Props {
13
13
  value?: string;
14
14
  el?: HTMLElement;
15
15
  id?: string;
16
+ /** Opt out of stuic base classes for full styling control */
17
+ unstyled?: boolean;
16
18
  mode?: CronInputMode;
17
19
  label?: SnippetWithId | THC;
18
20
  description?: SnippetWithId | THC;
@@ -0,0 +1,145 @@
1
+ # CronInput
2
+
3
+ A cron-expression editor with three progressive surfaces: **preset picker**, **5-field visual editor**, and **raw expression input** — plus a human-readable summary and the next-run preview. Backed by [`@marianmeres/cron`](https://www.npmjs.com/package/@marianmeres/cron).
4
+
5
+ The cron expression string is the single source of truth; field / preset / raw surfaces are all views over it. All three stay in sync automatically when the bound `value` changes.
6
+
7
+ ## Usage
8
+
9
+ ### Basic (preset picker + manual editor toggle)
10
+
11
+ ```svelte
12
+ <script lang="ts">
13
+ import { CronInput } from "@marianmeres/stuic";
14
+
15
+ let expression = $state("0 9 * * 1-5");
16
+ </script>
17
+
18
+ <CronInput bind:value={expression} label="Schedule" />
19
+ ```
20
+
21
+ Default `mode` is `"predefined"` — users see the preset picker with a toggle button to flip into `"manual"` (5 fields + raw + summary). Setting `mode={undefined}` removes the toggle and shows all surfaces that the `show*` flags enable.
22
+
23
+ ### Flat layout (no toggle, explicit surfaces)
24
+
25
+ ```svelte
26
+ <CronInput
27
+ bind:value={expression}
28
+ mode={undefined}
29
+ showPresets
30
+ showFields
31
+ showRawInput
32
+ showDescription
33
+ showNextRun
34
+ />
35
+ ```
36
+
37
+ ### Custom presets
38
+
39
+ ```svelte
40
+ <script lang="ts">
41
+ import { CronInput, type CronPreset } from "@marianmeres/stuic";
42
+
43
+ const presets: CronPreset[] = [
44
+ { label: "Top of every hour", value: "0 * * * *" },
45
+ { label: "Daily at 09:00", value: "0 9 * * *" },
46
+ { label: "Every 5 minutes", value: "*/5 * * * *" },
47
+ ];
48
+ </script>
49
+
50
+ <CronInput bind:value={expression} {presets} />
51
+ ```
52
+
53
+ ### Watching next-run externally
54
+
55
+ For places outside the CronInput that need the next-fire time (e.g. a dashboard card), use the exported `CronNextRun` helper:
56
+
57
+ ```svelte
58
+ <script lang="ts">
59
+ import { CronNextRun } from "@marianmeres/stuic";
60
+ import { onDestroy } from "svelte";
61
+
62
+ const nr = new CronNextRun("0 9 * * 1-5");
63
+ onDestroy(() => nr.destroy());
64
+ </script>
65
+
66
+ <p>Next run: {nr.nextRunFormatted}</p>
67
+ ```
68
+
69
+ > ⚠️ `CronNextRun` starts a 60-second interval in its constructor. **Always call `destroy()` on teardown**, or it will leak timers for the lifetime of the page.
70
+
71
+ ## Modes
72
+
73
+ The `mode` prop controls which surfaces are visible and whether the toggle button is shown:
74
+
75
+ | `mode` | Toggle shown | Visible surfaces |
76
+ | ------------------- | ------------ | ---------------------------------------------------- |
77
+ | `"predefined"` | yes | preset picker |
78
+ | `"manual"` | yes | fields + raw + description + next-run |
79
+ | `undefined` | no | whatever the `show*` flags enable (full control) |
80
+
81
+ When `mode` is defined, it **overrides** the individual `show*` flags. Set `mode={undefined}` if you want to pick surfaces yourself.
82
+
83
+ ## Expression ↔ fields sync
84
+
85
+ The `value` string is canonical. Internally:
86
+
87
+ - User edits a field → fields are composed back into an expression; `value` is set only if the result validates.
88
+ - User edits the raw input → fields are re-derived from it; invalid input surfaces as a validation message.
89
+ - User picks a preset → `value` is set directly.
90
+ - External `value` change → fields & raw mirror it (unless the change came from this component in the same tick).
91
+
92
+ Off-by-one conventions match standard cron:
93
+
94
+ | Field | Range | Notes |
95
+ | ------------ | ------ | ------------------------------------ |
96
+ | minute | 0-59 | |
97
+ | hour | 0-23 | |
98
+ | day of month | 1-31 | |
99
+ | month | 1-12 | 1 = January |
100
+ | day of week | 0-6 | 0 = Sunday |
101
+
102
+ ## Validation
103
+
104
+ The component uses `new CronParser(value)` to validate. When invalid:
105
+
106
+ - The visual invalid state comes from `InputWrap`'s standard validation wrapper (the same mechanism used by every `Field*` component — override via `classInputBoxWrapInvalid`).
107
+ - The content div gets a `has-error` class hook you can target with your own CSS. No built-in style is applied to it — it's purely an extension point.
108
+ - The `validate` prop (same shape as `FieldInput`'s) is supported; the component reports the parser's error message through the usual validation channel.
109
+
110
+ ## Timezone
111
+
112
+ All next-run calculations use the **host's local timezone**. There is no `timezone` prop today. DST transitions are handled correctly by `@marianmeres/cron`, but the displayed "next run" will naturally follow local-time semantics.
113
+
114
+ ## Props
115
+
116
+ | Prop | Type | Default | Description |
117
+ | --------------------- | ------------------------------------- | --------------- | ---------------------------------------------------- |
118
+ | `value` | `string` | `"* * * * *"` | Cron expression (bindable) |
119
+ | `mode` | `"predefined" \| "manual" \| undefined` | `"predefined"` | Editor mode; `undefined` hides the toggle |
120
+ | `presets` | `CronPreset[]` | `DEFAULT_PRESETS` | Preset options |
121
+ | `showPresets` | `boolean` | `true` | (Ignored when `mode` is defined) |
122
+ | `showFields` | `boolean` | `true` | (Ignored when `mode` is defined) |
123
+ | `showRawInput` | `boolean` | `true` | (Ignored when `mode` is defined) |
124
+ | `showDescription` | `boolean` | `true` | (Ignored when `mode` is defined) |
125
+ | `showNextRun` | `boolean` | `true` | (Ignored when `mode` is defined) |
126
+ | `onchange` | `(expr, valid) => void` | - | Fires on every valid or invalid change |
127
+ | `unstyled` | `boolean` | `false` | Skip stuic base classes (user provides all styling) |
128
+ | `class` | `string` | - | Wrapper class |
129
+ | `el` | `HTMLElement` | - | Bindable wrapper element |
130
+ | `classFields`/`classField`/`classFieldLabel`/`classFieldInput`/`classPreset`/`classRaw`/`classSummary`/`classToggleButton` | `string` | - | Per-element class overrides |
131
+
132
+ Plus all standard `InputWrap` wrapper class props (`classLabel`, `classLabelBox`, `classInputBox`, `classInputBoxWrap`, `classInputBoxWrapInvalid`, `classDescBox`, `classBelowBox`) — same pattern as every other `Field*` component.
133
+
134
+ ## Accessibility notes
135
+
136
+ - The preset select has `aria-label="Cron schedule preset"`.
137
+ - Each of the 5 field inputs has `aria-label` combining the short label and its range (e.g. `"Min (0-59)"`).
138
+ - The mode toggle button has a dynamic `aria-label` that reflects the target mode.
139
+ - Validation errors are announced via the underlying `InputWrap`'s validation message region.
140
+
141
+ ## Limitations
142
+
143
+ - **No timezone override** — uses host local time.
144
+ - **Validation happens on full expression** — no per-field pre-validation; a malformed single field makes the whole expression invalid.
145
+ - **Preset match is exact** — if a user's expression is semantically equivalent but formatted differently (e.g. `"0 0 * * 0"` vs `"0 0 * * 7"`) the preset picker shows "Custom".
@@ -1,6 +1,17 @@
1
1
  /**
2
2
  * A reactive helper that parses a cron expression and computes the next run time,
3
3
  * updating automatically every minute.
4
+ *
5
+ * ⚠️ **You must call `destroy()` when done** — the internal interval does not
6
+ * clean itself up. In a Svelte component:
7
+ *
8
+ * ```ts
9
+ * import { onDestroy } from "svelte";
10
+ * const nr = new CronNextRun("0 9 * * *");
11
+ * onDestroy(() => nr.destroy());
12
+ * ```
13
+ *
14
+ * Forgetting this will leak a 60s-interval timer for the lifetime of the page.
4
15
  */
5
16
  export declare class CronNextRun {
6
17
  #private;
@@ -2,6 +2,17 @@ import { CronParser } from "@marianmeres/cron";
2
2
  /**
3
3
  * A reactive helper that parses a cron expression and computes the next run time,
4
4
  * updating automatically every minute.
5
+ *
6
+ * ⚠️ **You must call `destroy()` when done** — the internal interval does not
7
+ * clean itself up. In a Svelte component:
8
+ *
9
+ * ```ts
10
+ * import { onDestroy } from "svelte";
11
+ * const nr = new CronNextRun("0 9 * * *");
12
+ * onDestroy(() => nr.destroy());
13
+ * ```
14
+ *
15
+ * Forgetting this will leak a 60s-interval timer for the lifetime of the page.
5
16
  */
6
17
  export class CronNextRun {
7
18
  #expression = $state("");
@@ -10,7 +10,6 @@
10
10
  --stuic-cron-input-section-gap: 0.25rem;
11
11
  --stuic-cron-input-field-label-text: var(--stuic-color-muted-foreground);
12
12
  --stuic-cron-input-summary-text: var(--stuic-color-muted-foreground);
13
- --stuic-cron-input-error-text: var(--stuic-color-destructive);
14
13
  --stuic-cron-input-field-bg: var(--stuic-color-background);
15
14
  --stuic-cron-input-field-border: var(--stuic-color-border);
16
15
  --stuic-cron-input-field-border-focus: var(--stuic-color-primary);
@@ -192,13 +191,6 @@
192
191
  line-height: 1.4;
193
192
  }
194
193
 
195
- /* Error message */
196
- .stuic-cron-input-error {
197
- color: var(--stuic-cron-input-error-text);
198
- font-size: 0.8125rem;
199
- line-height: 1.4;
200
- }
201
-
202
194
  /* Mode toggle button */
203
195
  .stuic-cron-input-content + .toggle-btn {
204
196
  color: var(--stuic-input-localized-toggle-text);