@marianmeres/stuic 3.180.0 → 3.181.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 (33) hide show
  1. package/AGENTS.md +2 -2
  2. package/dist/components/ColorPicker/ColorPicker.svelte +457 -0
  3. package/dist/components/ColorPicker/ColorPicker.svelte.d.ts +75 -0
  4. package/dist/components/ColorPicker/README.md +220 -0
  5. package/dist/components/ColorPicker/color-value.d.ts +32 -0
  6. package/dist/components/ColorPicker/color-value.js +55 -0
  7. package/dist/components/ColorPicker/i18n-sk.d.ts +17 -0
  8. package/dist/components/ColorPicker/i18n-sk.js +48 -0
  9. package/dist/components/ColorPicker/i18n.d.ts +64 -0
  10. package/dist/components/ColorPicker/i18n.js +75 -0
  11. package/dist/components/ColorPicker/index.css +275 -0
  12. package/dist/components/ColorPicker/index.d.ts +4 -0
  13. package/dist/components/ColorPicker/index.js +4 -0
  14. package/dist/components/ColorPicker/palettes.d.ts +41 -0
  15. package/dist/components/ColorPicker/palettes.js +49 -0
  16. package/dist/components/FieldsBuilder/README.md +3 -1
  17. package/dist/components/FieldsBuilder/types.d.ts +7 -2
  18. package/dist/components/FieldsBuilder/utils.d.ts +2 -1
  19. package/dist/components/FieldsBuilder/utils.js +4 -15
  20. package/dist/components/Nav/Nav.svelte +7 -2
  21. package/dist/components/Nav/Nav.svelte.d.ts +7 -2
  22. package/dist/components/Nav/README.md +2 -2
  23. package/dist/components/TabbedMenu/README.md +14 -13
  24. package/dist/components/TabbedMenu/TabbedMenu.svelte +7 -2
  25. package/dist/components/TabbedMenu/TabbedMenu.svelte.d.ts +7 -2
  26. package/dist/index.css +1 -0
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.js +1 -0
  29. package/dist/utils/tr.d.ts +26 -14
  30. package/dist/utils/tr.js +43 -19
  31. package/docs/_archive/maybe-todo.md +9 -2
  32. package/docs/domains/components.md +58 -1
  33. package/package.json +1 -1
@@ -1,25 +1,37 @@
1
1
  /**
2
- * A value that may be a plain string or a locale-keyed object of translations.
2
+ * A value that may be a plain string or a locale-keyed object of translations
3
+ * (`{ en: "Hello", sk: "Ahoj" }`).
3
4
  */
4
5
  export type MaybeLocalized = string | Record<string, string>;
5
6
  /**
6
- * Performs a locale-based translation lookup on a value.
7
+ * Resolves a `MaybeLocalized` value to the text to display.
7
8
  *
8
- * If the value is a plain string, returns it directly. If it's an object with
9
- * locale keys (e.g., `{ en: "Hello", sk: "Ahoj" }`), returns the value for the
10
- * given locale.
9
+ * A plain string is returned as-is. A locale-keyed record (or a JSON string
10
+ * encoding one — how string-valued inputs such as `FieldInputLocalized` carry
11
+ * it) is resolved in this order:
11
12
  *
12
- * @param val - A plain string or locale-keyed object (can also be a JSON string of the object)
13
- * @param locale - The locale key to look up (e.g., "en", "sk")
14
- * @param fallback - Fallback value if locale key not found
15
- * @returns The translated string
13
+ * 1. the first entry of `locale` (one locale, or a preference chain in order)
14
+ * that is non-empty — an empty entry counts as missing,
15
+ * 2. `fallback`, when one is given,
16
+ * 3. the first non-empty entry of the record,
17
+ * 4. `""`.
18
+ *
19
+ * So a record never renders as `[object Object]`, and a missing translation
20
+ * degrades to another language rather than to nothing. `null`/`undefined`
21
+ * yield `fallback ?? ""`.
22
+ *
23
+ * @param val - A plain string, a locale-keyed record, or a JSON string of one
24
+ * @param locale - The locale to look up, or a chain of locales in order of preference
25
+ * @param fallback - Text to use when none of `locale` has a non-empty entry
16
26
  *
17
27
  * @example
18
28
  * ```ts
19
- * tr('Hello', 'sk'); // 'Hello' (plain string returned as-is)
20
- * tr({ en: 'Hello', sk: 'Ahoj' }, 'sk'); // 'Ahoj'
21
- * tr({ en: 'Hello' }, 'sk', 'fallback'); // 'fallback'
22
- * tr('{"en":"Hello","sk":"Ahoj"}', 'sk'); // 'Ahoj' (JSON string parsed)
29
+ * tr("Hello", "sk"); // "Hello" (plain string returned as-is)
30
+ * tr({ en: "Hello", sk: "Ahoj" }, "sk"); // "Ahoj"
31
+ * tr({ en: "Hello", sk: "Ahoj" }, ["cs", "sk"]); // "Ahoj" (chain, in order)
32
+ * tr({ en: "Hello" }, "sk"); // "Hello" (first non-empty entry)
33
+ * tr({ en: "Hello" }, "sk", "—"); // "—" (explicit fallback wins)
34
+ * tr('{"en":"Hello","sk":"Ahoj"}', "sk"); // "Ahoj" (JSON string parsed)
23
35
  * ```
24
36
  */
25
- export declare function tr(val: MaybeLocalized | undefined | null, locale?: string, fallback?: string): string;
37
+ export declare function tr(val: MaybeLocalized | undefined | null, locale?: string | string[], fallback?: string): string;
package/dist/utils/tr.js CHANGED
@@ -1,30 +1,54 @@
1
+ import { isPlainObject } from "./is-plain-object.js";
1
2
  import { maybeJsonParse } from "./maybe-json-parse.js";
2
3
  /**
3
- * Performs a locale-based translation lookup on a value.
4
+ * Resolves a `MaybeLocalized` value to the text to display.
4
5
  *
5
- * If the value is a plain string, returns it directly. If it's an object with
6
- * locale keys (e.g., `{ en: "Hello", sk: "Ahoj" }`), returns the value for the
7
- * given locale.
6
+ * A plain string is returned as-is. A locale-keyed record (or a JSON string
7
+ * encoding one — how string-valued inputs such as `FieldInputLocalized` carry
8
+ * it) is resolved in this order:
8
9
  *
9
- * @param val - A plain string or locale-keyed object (can also be a JSON string of the object)
10
- * @param locale - The locale key to look up (e.g., "en", "sk")
11
- * @param fallback - Fallback value if locale key not found
12
- * @returns The translated string
10
+ * 1. the first entry of `locale` (one locale, or a preference chain in order)
11
+ * that is non-empty — an empty entry counts as missing,
12
+ * 2. `fallback`, when one is given,
13
+ * 3. the first non-empty entry of the record,
14
+ * 4. `""`.
15
+ *
16
+ * So a record never renders as `[object Object]`, and a missing translation
17
+ * degrades to another language rather than to nothing. `null`/`undefined`
18
+ * yield `fallback ?? ""`.
19
+ *
20
+ * @param val - A plain string, a locale-keyed record, or a JSON string of one
21
+ * @param locale - The locale to look up, or a chain of locales in order of preference
22
+ * @param fallback - Text to use when none of `locale` has a non-empty entry
13
23
  *
14
24
  * @example
15
25
  * ```ts
16
- * tr('Hello', 'sk'); // 'Hello' (plain string returned as-is)
17
- * tr({ en: 'Hello', sk: 'Ahoj' }, 'sk'); // 'Ahoj'
18
- * tr({ en: 'Hello' }, 'sk', 'fallback'); // 'fallback'
19
- * tr('{"en":"Hello","sk":"Ahoj"}', 'sk'); // 'Ahoj' (JSON string parsed)
26
+ * tr("Hello", "sk"); // "Hello" (plain string returned as-is)
27
+ * tr({ en: "Hello", sk: "Ahoj" }, "sk"); // "Ahoj"
28
+ * tr({ en: "Hello", sk: "Ahoj" }, ["cs", "sk"]); // "Ahoj" (chain, in order)
29
+ * tr({ en: "Hello" }, "sk"); // "Hello" (first non-empty entry)
30
+ * tr({ en: "Hello" }, "sk", "—"); // "—" (explicit fallback wins)
31
+ * tr('{"en":"Hello","sk":"Ahoj"}', "sk"); // "Ahoj" (JSON string parsed)
20
32
  * ```
21
33
  */
22
34
  export function tr(val, locale, fallback) {
23
- if (!locale)
24
- return `${val ?? fallback ?? ""}`;
25
- val = maybeJsonParse(val);
26
- // if string - no translation support
27
- if (typeof val === "string")
28
- return val;
29
- return `${val?.[locale] ?? fallback ?? val ?? ""}`;
35
+ if (val == null)
36
+ return fallback ?? "";
37
+ // only a plain object is a record; a string that parses to anything else
38
+ // ("2024", "null", "[1,2]") is just that string
39
+ const parsed = maybeJsonParse(val);
40
+ if (!isPlainObject(parsed))
41
+ return String(val);
42
+ const rec = parsed;
43
+ const chain = typeof locale === "string" ? [locale] : (locale ?? []);
44
+ for (const l of chain) {
45
+ if (l && Object.hasOwn(rec, l) && rec[l])
46
+ return String(rec[l]);
47
+ }
48
+ if (fallback !== undefined)
49
+ return fallback;
50
+ for (const v of Object.values(rec))
51
+ if (v)
52
+ return String(v);
53
+ return "";
30
54
  }
@@ -83,8 +83,15 @@ Checked first, to avoid false positives:
83
83
  feedback (icon + intent swap, localized label/name, sr live announcement),
84
84
  `onCopied` / `onError` callbacks; the write is the reusable `copyToClipboard()`
85
85
  util (async Clipboard API + `execCommand` fallback) in `utils/`.
86
- 13. **ColorPicker** — beyond native `type="color"`: swatch palette + custom input.
87
- Given stuic's theming/design-tokens focus, a swatch picker would be on-brand.
86
+ 13. ~~**ColorPicker**~~ — ✅ shipped (see `src/lib/components/ColorPicker/`): a
87
+ `role="radiogroup"` swatch palette (roving tabindex, wrapping arrows, optional
88
+ "no color" swatch, `columns` grid) plus a custom row of the native
89
+ `<input type="color">` and a hex field; hidden input + `validate` with `required`,
90
+ `t` texts with Slovak bundled. Swatch values are never parsed — any CSS color
91
+ string, so `COLOR_PICKER_PALETTE_THEME` can hold `var(--stuic-color-*)` tokens and
92
+ the picked value keeps following the theme. Mobile needed no special path: the
93
+ native input opens the platform picker, so there is no popover to fight the
94
+ on-screen keyboard.
88
95
 
89
96
  ## Deliberately out of scope (or close enough to covered)
90
97
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- 79 Svelte 5 component directories with consistent API patterns. All use runes-based reactivity.
5
+ 80 Svelte 5 component directories with consistent API patterns. All use runes-based reactivity.
6
6
 
7
7
  ## Component Categories
8
8
 
@@ -35,6 +35,7 @@
35
35
  | Slider | Fancy range input (fill + optional thumb) |
36
36
  | RangeSlider | Dual-thumb Slider: `start` ≤ `end` on one track, no crossing, `minRange`, two hidden inputs |
37
37
  | Rating | Star rating: radiogroup input (half steps, hover preview, form + validate) or read-only display |
38
+ | ColorPicker | Swatch palette (radiogroup) + native picker & hex field; any CSS color, form + validate |
38
39
  | TwCheck | Styled checkbox/radio |
39
40
  | DropdownMenu | Popover menu |
40
41
  | ContextMenu | Right-click / long-press menu at the cursor (the DropdownMenu engine + trigger semantics) |
@@ -882,6 +883,62 @@ Prefix: `--stuic-card-*`
882
883
 
883
884
  ---
884
885
 
886
+ ## ColorPicker
887
+
888
+ A swatch palette plus a custom-color escape hatch. The palette is a `role="radiogroup"` of swatch buttons (roving tabindex, wrapping Left/Right, Up/Down by one _rendered_ row — measured from the layout, so it survives wrapping — Home/End, Delete/Backspace to clear, an optional crossed-out "no color" swatch); the custom row is the native `<input type="color">` next to a hex text field. A hidden input carries `name`/value with the `validate` action and `required` enforced (hidden inputs skip native constraint validation).
889
+
890
+ `columns` caps swatches per row rather than forcing a grid — a phone too narrow for N wraps to fewer instead of scrolling the page sideways.
891
+
892
+ Swatch values are **never parsed** — they go to CSS as `--stuic-color-picker-swatch-color`, so a palette may hold hex, `oklch(...)`, `transparent`, or `var(--stuic-color-primary)` (`COLOR_PICKER_PALETTE_THEME` does exactly that, and the stored value keeps following the theme). Selection is a ring drawn _outside_ the swatch, which reads on any color without luminance math. Mobile needs no special path: the native input opens the platform picker, so there is no popover to fight the on-screen keyboard; swatches grow to ~44px on a coarse pointer and the hex field carries the iOS zoom guard.
893
+
894
+ ### Exports
895
+
896
+ | Export | Kind | Description |
897
+ | ---------------------------- | --------- | -------------------------------------------- |
898
+ | `ColorPicker` | component | Main component |
899
+ | `ColorPickerProps` | type | Props type |
900
+ | `ColorPickerCustom` | type | `"both" \| "native" \| "text" \| false` |
901
+ | `ColorPickerSwatch` | type | `string \| ColorPickerSwatchObject` |
902
+ | `ColorPickerSwatchObject` | type | `{ value, label? }` |
903
+ | `COLOR_PICKER_PALETTE` | constant | Default palette (12 hues + white/grey/black) |
904
+ | `COLOR_PICKER_PALETTE_THEME` | constant | Opt-in design-token palette |
905
+ | `createColorPickerT` | function | Builds the `t` prop from a (partial) catalog |
906
+ | `COLOR_PICKER_MESSAGES_EN` | constant | Built-in English catalog (also the fallback) |
907
+ | `COLOR_PICKER_MESSAGES_SK` | constant | Bundled Slovak catalog (opt-in) |
908
+ | `ColorPickerMessageKey` | type | Message key union |
909
+ | `ColorPickerMessages` | type | One locale's catalog |
910
+
911
+ ### Key Props
912
+
913
+ | Prop | Type | Default | Description |
914
+ | ----------------------------------------------------- | --------------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
915
+ | `value` | `string` | `""` | Bindable; any CSS color string, stored verbatim. `""` = no color |
916
+ | `palette` | `ColorPickerSwatch[]` | `COLOR_PICKER_PALETTE` | Color strings or `{ value, label }` objects; `[]` renders no palette |
917
+ | `columns` | `number` | — | Cap of N swatches per row (a narrower container wraps to fewer). Unset = as many as fit |
918
+ | `custom` | `"both" \| "native" \| "text" \| false` | `"both"` | Which custom-color controls render under the palette |
919
+ | `allowClear` | `boolean` | `true` | Clear swatch + Delete/Backspace + an emptied hex field |
920
+ | `name`, `required`, `validate`, `setValidationResult` | | | Form integration — same contract as the `Field*` components; imperative `validate()` / `clearValidation()` / `getValidation()` via `bind:this` |
921
+ | `onchange` | `(value: string) => void` | — | User **commits** only (see below) |
922
+ | `t` | `TranslateFn` | English | Group label, swatch names, "no color", custom-color labels, required message |
923
+
924
+ Class slots: `class`, `classSwatch`.
925
+
926
+ ### Preview vs commit
927
+
928
+ `value` updates live while the native OS picker is dragged and while a valid color is typed into the hex field (so `bind:value` previews); `onchange` — and the hidden input's `change`, which is what re-runs validation — fires only on a commit: a swatch click, an arrow onto a _different_ swatch (a radio does not re-fire for the checked one), the native picker's `change`, or Enter/blur in the hex field. An unparseable hex field commits nothing and snaps back.
929
+
930
+ The hex field normalizes hex in any spelling (`#0f0`, `3b82f6`) to lowercase `#rrggbb`; anything else `CSS.supports("color", …)` accepts is kept verbatim. Because a focused field must own its own DOM value, it is written explicitly rather than driven by a reactive `value=` — Svelte skips a write when the expression matches what it last wrote, which is exactly the snap-back case.
931
+
932
+ ### CSS Tokens
933
+
934
+ Prefix: `--stuic-color-picker-*`
935
+
936
+ `gap`, `custom-gap`, `swatch-size`, `swatch-size-touch`, `swatch-border`, `swatch-scale-hover`, `swatch-ring-width`, `swatch-ring-gap`, `swatch-ring-color`, `swatch-ring-gap-color`, `ring-width`, `ring-color`, `clear-color`, `clear-bg`, `text-width`, `text-bg`, `text-border`, `text-color`, `text-placeholder`, `text-font-family`, `text-font-size`, `text-font-size-touch-min`, `opacity-disabled`
937
+
938
+ Radius and border width use the shared-token fallback pattern (`swatch-radius` / `text-radius` / `*-border-width` → `--stuic-radius` / `--stuic-border-width`).
939
+
940
+ ---
941
+
885
942
  ## ContextMenu
886
943
 
887
944
  Right-click / long-press triggered menu positioned at the pointer. Wraps content into a context target area: `contextmenu` (right-click), long-press (touch/pen, via the `longPress` attachment), or Shift+F10 / the menu key open the menu anchored at the interaction point (an invisible 0×0 fixed anchor the underlying DropdownMenu positions against). The menu panel **is** a `DropdownMenu` — item model, keyboard navigation, search, overflow handling, and theming (`--stuic-dropdown-menu-*`) all come from there.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "3.180.0",
3
+ "version": "3.181.0",
4
4
  "packageManager": "pnpm@11.5.0",
5
5
  "scripts": {
6
6
  "dev": "vite dev",