@marianmeres/stuic 3.169.0 → 3.171.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.
package/AGENTS.md CHANGED
@@ -23,7 +23,7 @@
23
23
 
24
24
  ```
25
25
  src/lib/
26
- ├── components/ # 73 component directories
26
+ ├── components/ # 74 component directories
27
27
  ├── actions/ # 15 Svelte actions (use: directives)
28
28
  ├── attachments/ # Svelte attachments ({@attach} — preferred for new DOM helpers)
29
29
  ├── utils/ # 45 utility modules
@@ -129,7 +129,7 @@ Global tokens that control cross-component visual properties. Defined in `src/li
129
129
 
130
130
  ### Domain Docs
131
131
 
132
- - [Components](./docs/domains/components.md) — 73 component directories, Props pattern, snippets
132
+ - [Components](./docs/domains/components.md) — 74 component directories, Props pattern, snippets
133
133
  - [Theming](./docs/domains/theming.md) — CSS tokens, dark mode, themes
134
134
  - [CSS presets](./docs/domains/css-presets.md) — ratio-locked frame (letterbox), safe-area, scrollbar
135
135
  - [Actions](./docs/domains/actions.md) — 15 Svelte directives
@@ -47,7 +47,7 @@
47
47
  search?: boolean | ContextMenuSearchConfig;
48
48
  /** Show backdrop in fallback mode (default: true) */
49
49
  showBackdrop?: boolean;
50
- /** Reserve scrollbar space to prevent layout shift on open (see DropdownMenu) */
50
+ /** Reserve scrollbar space; auto-detected once the menu scrolls (see DropdownMenu) */
51
51
  scrollbarGutter?: boolean;
52
52
  /** Skip the body scroll lock in fallback mode */
53
53
  noScrollLock?: boolean;
@@ -39,7 +39,7 @@ export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children">
39
39
  search?: boolean | ContextMenuSearchConfig;
40
40
  /** Show backdrop in fallback mode (default: true) */
41
41
  showBackdrop?: boolean;
42
- /** Reserve scrollbar space to prevent layout shift on open (see DropdownMenu) */
42
+ /** Reserve scrollbar space; auto-detected once the menu scrolls (see DropdownMenu) */
43
43
  scrollbarGutter?: boolean;
44
44
  /** Skip the body scroll lock in fallback mode */
45
45
  noScrollLock?: boolean;
@@ -29,7 +29,7 @@ trigger semantics and the cursor anchoring.
29
29
  | `closeOnEscape` | `boolean` | `true` | Close on Escape |
30
30
  | `search` | `boolean \| ContextMenuSearchConfig` | - | Search/filter input inside the menu (see DropdownMenu) |
31
31
  | `showBackdrop` | `boolean` | `true` | Backdrop in fallback (centered modal) mode |
32
- | `scrollbarGutter` | `boolean` | auto | Reserve scrollbar space (auto-enables at 7 items) |
32
+ | `scrollbarGutter` | `boolean` | auto | Reserve scrollbar space (auto: once the menu actually scrolls, kept until close) |
33
33
  | `noScrollLock` | `boolean` | - | Skip the body scroll lock in fallback mode |
34
34
  | `forceFallback` | `boolean` | `false` | Force the centered-modal fallback (testing) |
35
35
  | `onOpen` / `onClose` | `() => void` | - | Open/close callbacks |
@@ -194,8 +194,10 @@
194
194
  onClose?: () => void;
195
195
  /** Called when any action item is selected (fallback if item has no onSelect) */
196
196
  onSelect?: (item: DropdownMenuActionItem) => void | boolean | Promise<void | boolean>;
197
- /** Reserve scrollbar space to prevent layout shift on open (useful for long lists).
198
- * When undefined, auto-enables if items count >= 7. */
197
+ /** Reserve scrollbar space (`scrollbar-gutter: stable`) so the menu width does not
198
+ * jump when the scrollbar comes and goes while open (search filtering, expandable
199
+ * sections). `true`/`false` force it. When undefined, the gutter is reserved only
200
+ * once the content actually scrolls, and kept until the menu closes. */
199
201
  scrollbarGutter?: boolean;
200
202
  /** Reference to trigger element */
201
203
  triggerEl?: HTMLButtonElement;
@@ -684,9 +686,58 @@
684
686
  // Computed transition duration
685
687
  let transitionDuration = $derived(reducedMotion.current ? 0 : 100);
686
688
 
689
+ // Scrollbar gutter.
690
+ // `scrollbar-gutter: stable` keeps the dropdown width from jumping when the vertical
691
+ // scrollbar comes and goes while the menu is open (search filtering, expanding or
692
+ // collapsing sections). Reserving it up front for a menu that never scrolls leaves a
693
+ // dead strip along the right edge (with classic, space-taking scrollbars), so unless
694
+ // the `scrollbarGutter` prop decides, the gutter is reserved only once a space-taking
695
+ // scrollbar is actually present — and then kept until the menu closes.
696
+ let hasScrollbar = $state(false);
697
+
698
+ /** Whether `el` currently renders a vertical scrollbar that takes up layout space.
699
+ * Overlay scrollbars take none (and `scrollbar-gutter` reserves nothing for them).
700
+ * Both halves are needed: an already reserved gutter takes the same space as a
701
+ * scrollbar (so width alone is fooled by a stale gutter from the previous session),
702
+ * and a mid-transition frame (`overflow: hidden`, reduced height) "overflows"
703
+ * without any scrollbar (so height alone is fooled by the slide animation). */
704
+ function hasVerticalScrollbar(el: HTMLElement): boolean {
705
+ const cs = getComputedStyle(el);
706
+ const borders =
707
+ (parseFloat(cs.borderLeftWidth) || 0) + (parseFloat(cs.borderRightWidth) || 0);
708
+ // offsetWidth - clientWidth = horizontal borders + vertical scrollbar (or gutter)
709
+ const takesSpace = el.offsetWidth - el.clientWidth - borders > 1;
710
+ return takesSpace && el.scrollHeight > el.clientHeight;
711
+ }
712
+
713
+ function measureScrollbar() {
714
+ if (hasScrollbar || !dropdownEl) return;
715
+ if (hasVerticalScrollbar(dropdownEl)) hasScrollbar = true;
716
+ }
717
+
718
+ // Each open session measures afresh. Reset on open rather than on close, so the
719
+ // gutter survives the closing slide-out (no width jump mid-outro).
720
+ $effect(() => {
721
+ if (isOpen) hasScrollbar = false;
722
+ });
723
+
724
+ // Measure on open and whenever the rendered content can grow (search results,
725
+ // expanded sections, items, positioning mode). Effects run before the slide intro
726
+ // applies its first zero-height frame, so this reads the settled layout; the
727
+ // `onintrostart` / `onintroend` handlers in the template re-check around transitions
728
+ // (a re-open that interrupts the closing slide-out is measured mid-animation here,
729
+ // and only settles by `introend`).
730
+ $effect(() => {
731
+ if (!isOpen || !dropdownEl) return;
732
+ void filteredItems;
733
+ void expandedSections;
734
+ void isSupported;
735
+ untrack(measureScrollbar);
736
+ });
737
+
687
738
  // Position styles for CSS Anchor Positioning
688
739
  let dropdownStyle = $derived.by(() => {
689
- const useGutter = scrollbarGutter ?? items.length >= 7;
740
+ const useGutter = scrollbarGutter ?? hasScrollbar;
690
741
  const gutterStyle = useGutter ? "scrollbar-gutter: stable;" : "";
691
742
  if (isSupported) {
692
743
  // Use fixed height when search is enabled AND position is a "top" variant
@@ -878,6 +929,8 @@
878
929
  )}
879
930
  style={dropdownStyle}
880
931
  transition:slide={{ duration: transitionDuration }}
932
+ onintrostart={measureScrollbar}
933
+ onintroend={measureScrollbar}
881
934
  >
882
935
  <!-- Close button (fallback mode only) -->
883
936
  {#if !isSupported}
@@ -1017,6 +1070,7 @@
1017
1070
  classExpandableContent
1018
1071
  )}
1019
1072
  transition:slide={{ duration: transitionDuration }}
1073
+ onintroend={measureScrollbar}
1020
1074
  >
1021
1075
  {#each item.items as childItem}
1022
1076
  {#if childItem.type === "action"}
@@ -163,8 +163,10 @@ export interface Props extends Omit<HTMLButtonAttributes, "children"> {
163
163
  onClose?: () => void;
164
164
  /** Called when any action item is selected (fallback if item has no onSelect) */
165
165
  onSelect?: (item: DropdownMenuActionItem) => void | boolean | Promise<void | boolean>;
166
- /** Reserve scrollbar space to prevent layout shift on open (useful for long lists).
167
- * When undefined, auto-enables if items count >= 7. */
166
+ /** Reserve scrollbar space (`scrollbar-gutter: stable`) so the menu width does not
167
+ * jump when the scrollbar comes and goes while open (search filtering, expandable
168
+ * sections). `true`/`false` force it. When undefined, the gutter is reserved only
169
+ * once the content actually scrolls, and kept until the menu closes. */
168
170
  scrollbarGutter?: boolean;
169
171
  /** Reference to trigger element */
170
172
  triggerEl?: HTMLButtonElement;
@@ -11,6 +11,7 @@ A feature-rich dropdown menu component with CSS Anchor Positioning (with fallbac
11
11
  | `position` | `DropdownMenuPosition` | `"bottom-span-right"` | Popover position relative to trigger |
12
12
  | `offset` | `string` | `"0.25rem"` | Offset from trigger element (CSS value) |
13
13
  | `maxHeight` | `string` | `"300px"` | Max height of dropdown |
14
+ | `scrollbarGutter` | `boolean` | auto | Reserve scrollbar space (auto: once it scrolls) |
14
15
  | `closeOnSelect` | `boolean` | `true` | Close menu when action item is selected |
15
16
  | `closeOnClickOutside` | `boolean` | `true` | Close on click outside |
16
17
  | `closeOnEscape` | `boolean` | `true` | Close on Escape key |
@@ -22,11 +22,9 @@
22
22
  import type { TranslateFn } from "../../types.js";
23
23
  import type { IntentColorKey } from "../../utils/design-tokens.js";
24
24
  import { getId } from "../../utils/get-id.js";
25
- import { isPlainObject } from "../../utils/is-plain-object.js";
26
25
  import { maybeJsonParse } from "../../utils/maybe-json-parse.js";
27
26
  import { waitForNextRepaint } from "../../utils/paint.js";
28
27
  import { qsa } from "../../utils/qsa.js";
29
- import { replaceMap } from "../../utils/replace-map.js";
30
28
  import { strHash } from "../../utils/str-hash.js";
31
29
  import { twMerge } from "../../utils/tw-merge.js";
32
30
  import Button from "../Button/Button.svelte";
@@ -40,6 +38,7 @@
40
38
  import FieldLikeChips, { type FieldLikeChip } from "./_internal/FieldLikeChips.svelte";
41
39
  import ListItemButton from "../ListItemButton/ListItemButton.svelte";
42
40
  import type { InputWrapClassProps } from "./types.js";
41
+ import { t_default } from "./field-options-i18n.js";
43
42
 
44
43
  export interface Option {
45
44
  label: string;
@@ -111,60 +110,6 @@
111
110
  itemIdPropName?: string;
112
111
  onChange?: (value: string) => void;
113
112
  }
114
-
115
- // i18n ready
116
- function t_default(
117
- k: string,
118
- values: false | null | undefined | Record<string, string | number> = null,
119
- fallback: string | boolean = "",
120
- i18nSpanWrap: boolean = true
121
- ) {
122
- const m: Record<string, string> = {
123
- field_req_att: "This field requires attention. Please review and try again.",
124
- cardinality_of: "of max",
125
- cardinality_selected: "selected",
126
- submit: "Submit",
127
- select_all: "Select all",
128
- clear_all: "Clear selected",
129
- clear: "Clear",
130
- search_placeholder: "Type to search...",
131
- search_submit_placeholder: "Type to search and submit...",
132
- cardinality_full: "Max selection reached",
133
- select_from_list: "Please select from the list only",
134
- x_close: "Clear input or close [esc]",
135
- close: "Close [esc]",
136
- unknown_allowed: "Select or type and submit",
137
- unknown_not_allowed: "Select from the list",
138
- no_results: "No results found.",
139
- add_new: 'Add "{{value}}"...',
140
- click_add_new: "You must add the value to continue",
141
- // chips display mode
142
- chips_placeholder: "Nothing selected",
143
- chips_open: "Choose...",
144
- chips_remove: "Remove {{value}}",
145
- //
146
- pick_tab: "Pick",
147
- arrange_tab: "Arrange ({{value}})",
148
- arrange_help: "Reorder the selected items. Use the buttons to move them.",
149
- sort_az: "Sort A–Z",
150
- reverse: "Reverse",
151
- shuffle: "Shuffle",
152
- move_up: "Move up",
153
- move_down: "Move down",
154
- move_to_top: "Move to top",
155
- move_to_bottom: "Move to bottom",
156
- remove_item: "Remove",
157
- moved_up: "Moved {{value}} up",
158
- moved_down: "Moved {{value}} down",
159
- removed_item: "Removed {{value}}",
160
- sorted_az: "Sorted A to Z",
161
- reversed: "Order reversed",
162
- shuffled: "Order shuffled",
163
- };
164
- let out = m[k] ?? fallback ?? k;
165
-
166
- return isPlainObject(values) ? replaceMap(out, values as any) : out;
167
- }
168
113
  </script>
169
114
 
170
115
  <script lang="ts">
@@ -538,6 +538,84 @@ Typing directly into the field (an inline combobox) is deliberately not part of
538
538
  modal remains the single place where options are searched and picked, which is what keeps the
539
539
  field usable under a soft keyboard.
540
540
 
541
+ ### i18n
542
+
543
+ All built-in texts go through the `t` prop. English is the built-in default; Slovak ships
544
+ bundled and opt-in (importing it is what pulls it into your bundle — English-only consumers
545
+ pay nothing).
546
+
547
+ ```svelte
548
+ <script>
549
+ import {
550
+ FieldOptions,
551
+ createFieldOptionsT,
552
+ FIELD_OPTIONS_MESSAGES_SK,
553
+ } from "@marianmeres/stuic";
554
+
555
+ const t = createFieldOptionsT(FIELD_OPTIONS_MESSAGES_SK);
556
+ </script>
557
+
558
+ <FieldOptions name="tags" bind:value {getOptions} {t} />
559
+ ```
560
+
561
+ `createFieldOptionsT(messages, fallbackMessages?)` falls back to
562
+ `FIELD_OPTIONS_MESSAGES_EN` for any key the catalog does not define, so a partial catalog is
563
+ fine and a raw key is never rendered — pass your own object to translate into a language
564
+ that is not bundled, or to override individual texts. This matters most with `chips`, where
565
+ `chips_placeholder`, `chips_open` and `chips_remove` end up in the **closed field**, on the
566
+ page, next to your own copy (a filter with nothing selected is not "nothing selected", it is
567
+ _everything_):
568
+
569
+ ```ts
570
+ // reword two strings, keep English everywhere else
571
+ const t = createFieldOptionsT({ chips_placeholder: "Any tag", chips_open: "Edit tags" });
572
+
573
+ // or: full Slovak, plus a field specific tweak
574
+ const t = createFieldOptionsT({
575
+ ...FIELD_OPTIONS_MESSAGES_SK,
576
+ chips_placeholder: "Bez štítkov",
577
+ });
578
+ ```
579
+
580
+ Placeholders are mustache-style (`{{value}}`).
581
+
582
+ | Key | English | Used by |
583
+ | --------------------------- | ----------------------------------------------------------- | ----------------------------------- |
584
+ | `field_req_att` | This field requires attention. Please review and try again. | Validation message |
585
+ | `chips_placeholder` | Nothing selected | Empty `chips` row |
586
+ | `chips_open` | Choose... | `chips` trailing button (+ tooltip) |
587
+ | `chips_remove` | Remove {{value}} | Each chip's × accessible name |
588
+ | `search_placeholder` | Type to search... | Modal search input |
589
+ | `search_submit_placeholder` | Type to search and submit... | Modal search input (`allowUnknown`) |
590
+ | `x_close` | Clear input or close [esc] | Search input's × tooltip |
591
+ | `close` | Close [esc] | Modal close button tooltip |
592
+ | `submit` | Submit | Modal submit button |
593
+ | `select_all` | Select all | Modal control |
594
+ | `clear_all` / `clear` | Clear selected / Clear | Modal control (multi / single) |
595
+ | `cardinality_of` | of max | Selection counter |
596
+ | `cardinality_selected` | selected | Selection counter |
597
+ | `cardinality_full` | Max selection reached | Notification |
598
+ | `no_results` | No results found. | Empty options list |
599
+ | `add_new` | Add "{{value}}"... | `allowUnknown` add row |
600
+ | `click_add_new` | You must add the value to continue | Notification |
601
+ | `select_from_list` | Please select from the list only | Notification |
602
+ | `unknown_allowed` | Select or type and submit | Modal footer hint |
603
+ | `unknown_not_allowed` | Select from the list | Modal footer hint |
604
+ | `pick_tab` / `arrange_tab` | Pick / Arrange ({{value}}) | `ordered` tab header |
605
+ | `arrange_help` | Reorder the selected items. Use the buttons to move them. | `ordered` screen hint |
606
+ | `sort_az` | Sort A–Z | `ordered` shortcut |
607
+ | `reverse` | Reverse | `ordered` shortcut |
608
+ | `shuffle` | Shuffle | `ordered` shortcut |
609
+ | `move_up` / `move_down` | Move up / Move down | `ordered` row buttons |
610
+ | `move_to_top` | Move to top | `ordered` row button |
611
+ | `move_to_bottom` | Move to bottom | `ordered` row button |
612
+ | `remove_item` | Remove | `ordered` row button |
613
+ | `moved_up` / `moved_down` | Moved {{value}} up / Moved {{value}} down | Screen reader announcement |
614
+ | `removed_item` | Removed {{value}} | Screen reader announcement |
615
+ | `sorted_az` | Sorted A to Z | Screen reader announcement |
616
+ | `reversed` | Order reversed | Screen reader announcement |
617
+ | `shuffled` | Order shuffled | Screen reader announcement |
618
+
541
619
  ### Customization Examples
542
620
 
543
621
  ```css
@@ -0,0 +1,21 @@
1
+ import type { FieldOptionsMessages } from "./field-options-i18n.js";
2
+ /**
3
+ * Slovak message catalog for `FieldOptions`. Opt-in — English stays the built-in
4
+ * default, and this module is only pulled into a bundle when it is actually imported
5
+ * (the component itself never references it).
6
+ *
7
+ * @example
8
+ * ```svelte
9
+ * <script>
10
+ * import {
11
+ * FieldOptions,
12
+ * createFieldOptionsT,
13
+ * FIELD_OPTIONS_MESSAGES_SK,
14
+ * } from "@marianmeres/stuic";
15
+ * const t = createFieldOptionsT(FIELD_OPTIONS_MESSAGES_SK);
16
+ * </script>
17
+ *
18
+ * <FieldOptions name="tags" bind:value {getOptions} {t} />
19
+ * ```
20
+ */
21
+ export declare const FIELD_OPTIONS_MESSAGES_SK: FieldOptionsMessages;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Slovak message catalog for `FieldOptions`. Opt-in — English stays the built-in
3
+ * default, and this module is only pulled into a bundle when it is actually imported
4
+ * (the component itself never references it).
5
+ *
6
+ * @example
7
+ * ```svelte
8
+ * <script>
9
+ * import {
10
+ * FieldOptions,
11
+ * createFieldOptionsT,
12
+ * FIELD_OPTIONS_MESSAGES_SK,
13
+ * } from "@marianmeres/stuic";
14
+ * const t = createFieldOptionsT(FIELD_OPTIONS_MESSAGES_SK);
15
+ * </script>
16
+ *
17
+ * <FieldOptions name="tags" bind:value {getOptions} {t} />
18
+ * ```
19
+ */
20
+ export const FIELD_OPTIONS_MESSAGES_SK = {
21
+ field_req_att: "Toto pole vyžaduje pozornosť. Skontrolujte ho a skúste to znova.",
22
+ cardinality_of: "z max.",
23
+ cardinality_selected: "vybraných",
24
+ submit: "Potvrdiť",
25
+ select_all: "Vybrať všetko",
26
+ clear_all: "Zrušiť výber",
27
+ clear: "Zrušiť výber",
28
+ search_placeholder: "Píšte pre vyhľadávanie...",
29
+ search_submit_placeholder: "Píšte pre vyhľadanie a potvrdenie...",
30
+ cardinality_full: "Dosiahnutý maximálny počet výberov",
31
+ select_from_list: "Vyberte, prosím, iba zo zoznamu",
32
+ x_close: "Vymazať vstup alebo zavrieť [esc]",
33
+ close: "Zavrieť [esc]",
34
+ unknown_allowed: "Vyberte alebo napíšte a potvrďte",
35
+ unknown_not_allowed: "Vyberte zo zoznamu",
36
+ no_results: "Nenašli sa žiadne výsledky.",
37
+ add_new: 'Pridať "{{value}}"...',
38
+ click_add_new: "Ak chcete pokračovať, musíte hodnotu pridať",
39
+ // chips display mode
40
+ chips_placeholder: "Nič nie je vybrané",
41
+ chips_open: "Vybrať...",
42
+ chips_remove: "Odstrániť {{value}}",
43
+ //
44
+ pick_tab: "Výber",
45
+ arrange_tab: "Poradie ({{value}})",
46
+ arrange_help: "Zmeňte poradie vybraných položiek. Na presun použite tlačidlá.",
47
+ sort_az: "Zoradiť A–Z",
48
+ reverse: "Obrátiť",
49
+ shuffle: "Zamiešať",
50
+ move_up: "Posunúť vyššie",
51
+ move_down: "Posunúť nižšie",
52
+ move_to_top: "Presunúť na začiatok",
53
+ move_to_bottom: "Presunúť na koniec",
54
+ remove_item: "Odstrániť",
55
+ moved_up: "Posunuté vyššie: {{value}}",
56
+ moved_down: "Posunuté nižšie: {{value}}",
57
+ removed_item: "Odstránené: {{value}}",
58
+ sorted_az: "Zoradené od A po Z",
59
+ reversed: "Poradie obrátené",
60
+ shuffled: "Poradie zamiešané",
61
+ };
@@ -0,0 +1,82 @@
1
+ import type { TranslateFn } from "../../types.js";
2
+ /**
3
+ * The built-in (English) message catalog of `FieldOptions`. Also the fallback of every
4
+ * other bundled locale, so a locale missing a key still renders text.
5
+ *
6
+ * Placeholders are mustache-style (`{{value}}`).
7
+ */
8
+ export declare const FIELD_OPTIONS_MESSAGES_EN: {
9
+ field_req_att: string;
10
+ cardinality_of: string;
11
+ cardinality_selected: string;
12
+ submit: string;
13
+ select_all: string;
14
+ clear_all: string;
15
+ clear: string;
16
+ search_placeholder: string;
17
+ search_submit_placeholder: string;
18
+ cardinality_full: string;
19
+ select_from_list: string;
20
+ x_close: string;
21
+ close: string;
22
+ unknown_allowed: string;
23
+ unknown_not_allowed: string;
24
+ no_results: string;
25
+ add_new: string;
26
+ click_add_new: string;
27
+ chips_placeholder: string;
28
+ chips_open: string;
29
+ chips_remove: string;
30
+ pick_tab: string;
31
+ arrange_tab: string;
32
+ arrange_help: string;
33
+ sort_az: string;
34
+ reverse: string;
35
+ shuffle: string;
36
+ move_up: string;
37
+ move_down: string;
38
+ move_to_top: string;
39
+ move_to_bottom: string;
40
+ remove_item: string;
41
+ moved_up: string;
42
+ moved_down: string;
43
+ removed_item: string;
44
+ sorted_az: string;
45
+ reversed: string;
46
+ shuffled: string;
47
+ };
48
+ /** Every message key `FieldOptions` may look up. */
49
+ export type FieldOptionsMessageKey = keyof typeof FIELD_OPTIONS_MESSAGES_EN;
50
+ /** A (possibly partial) catalog for one locale. */
51
+ export type FieldOptionsMessages = Record<FieldOptionsMessageKey, string>;
52
+ /**
53
+ * Builds the `t` prop of `FieldOptions` from a message catalog. Unknown or untranslated
54
+ * keys fall back to `fallbackMessages` (English by default), so a catalog may safely be
55
+ * partial and never renders a raw key.
56
+ *
57
+ * Handy with the `chips` display mode, where a few of the texts (`chips_placeholder`,
58
+ * `chips_open`, `chips_remove`) end up in the closed field, on the page, next to your own
59
+ * copy — reworded per field, without restating the whole catalog.
60
+ *
61
+ * @example
62
+ * ```svelte
63
+ * <script>
64
+ * import {
65
+ * FieldOptions,
66
+ * createFieldOptionsT,
67
+ * FIELD_OPTIONS_MESSAGES_SK,
68
+ * } from "@marianmeres/stuic";
69
+ *
70
+ * // full locale, plus a field specific tweak
71
+ * const t = createFieldOptionsT({
72
+ * ...FIELD_OPTIONS_MESSAGES_SK,
73
+ * chips_placeholder: "Bez štítkov",
74
+ * });
75
+ * </script>
76
+ *
77
+ * <FieldOptions chips name="tags" bind:value {getOptions} {t} />
78
+ * ```
79
+ */
80
+ export declare function createFieldOptionsT(messages: Partial<FieldOptionsMessages> | Record<string, string>, fallbackMessages?: Partial<FieldOptionsMessages> | Record<string, string>): TranslateFn;
81
+ /** The component's built-in English `t`. */
82
+ export declare const t_default: TranslateFn;
@@ -0,0 +1,90 @@
1
+ import { isPlainObject } from "../../utils/is-plain-object.js";
2
+ import { replaceMap } from "../../utils/replace-map.js";
3
+ /**
4
+ * The built-in (English) message catalog of `FieldOptions`. Also the fallback of every
5
+ * other bundled locale, so a locale missing a key still renders text.
6
+ *
7
+ * Placeholders are mustache-style (`{{value}}`).
8
+ */
9
+ export const FIELD_OPTIONS_MESSAGES_EN = {
10
+ field_req_att: "This field requires attention. Please review and try again.",
11
+ cardinality_of: "of max",
12
+ cardinality_selected: "selected",
13
+ submit: "Submit",
14
+ select_all: "Select all",
15
+ clear_all: "Clear selected",
16
+ clear: "Clear",
17
+ search_placeholder: "Type to search...",
18
+ search_submit_placeholder: "Type to search and submit...",
19
+ cardinality_full: "Max selection reached",
20
+ select_from_list: "Please select from the list only",
21
+ x_close: "Clear input or close [esc]",
22
+ close: "Close [esc]",
23
+ unknown_allowed: "Select or type and submit",
24
+ unknown_not_allowed: "Select from the list",
25
+ no_results: "No results found.",
26
+ add_new: 'Add "{{value}}"...',
27
+ click_add_new: "You must add the value to continue",
28
+ // chips display mode
29
+ chips_placeholder: "Nothing selected",
30
+ chips_open: "Choose...",
31
+ chips_remove: "Remove {{value}}",
32
+ //
33
+ pick_tab: "Pick",
34
+ arrange_tab: "Arrange ({{value}})",
35
+ arrange_help: "Reorder the selected items. Use the buttons to move them.",
36
+ sort_az: "Sort A–Z",
37
+ reverse: "Reverse",
38
+ shuffle: "Shuffle",
39
+ move_up: "Move up",
40
+ move_down: "Move down",
41
+ move_to_top: "Move to top",
42
+ move_to_bottom: "Move to bottom",
43
+ remove_item: "Remove",
44
+ moved_up: "Moved {{value}} up",
45
+ moved_down: "Moved {{value}} down",
46
+ removed_item: "Removed {{value}}",
47
+ sorted_az: "Sorted A to Z",
48
+ reversed: "Order reversed",
49
+ shuffled: "Order shuffled",
50
+ };
51
+ /**
52
+ * Builds the `t` prop of `FieldOptions` from a message catalog. Unknown or untranslated
53
+ * keys fall back to `fallbackMessages` (English by default), so a catalog may safely be
54
+ * partial and never renders a raw key.
55
+ *
56
+ * Handy with the `chips` display mode, where a few of the texts (`chips_placeholder`,
57
+ * `chips_open`, `chips_remove`) end up in the closed field, on the page, next to your own
58
+ * copy — reworded per field, without restating the whole catalog.
59
+ *
60
+ * @example
61
+ * ```svelte
62
+ * <script>
63
+ * import {
64
+ * FieldOptions,
65
+ * createFieldOptionsT,
66
+ * FIELD_OPTIONS_MESSAGES_SK,
67
+ * } from "@marianmeres/stuic";
68
+ *
69
+ * // full locale, plus a field specific tweak
70
+ * const t = createFieldOptionsT({
71
+ * ...FIELD_OPTIONS_MESSAGES_SK,
72
+ * chips_placeholder: "Bez štítkov",
73
+ * });
74
+ * </script>
75
+ *
76
+ * <FieldOptions chips name="tags" bind:value {getOptions} {t} />
77
+ * ```
78
+ */
79
+ export function createFieldOptionsT(messages, fallbackMessages = FIELD_OPTIONS_MESSAGES_EN) {
80
+ return (k, values = null, fallback = "") => {
81
+ const out = messages[k] ??
82
+ fallbackMessages[k] ??
83
+ (typeof fallback === "string" ? fallback : k);
84
+ return isPlainObject(values)
85
+ ? replaceMap(out, values)
86
+ : out;
87
+ };
88
+ }
89
+ /** The component's built-in English `t`. */
90
+ export const t_default = createFieldOptionsT(FIELD_OPTIONS_MESSAGES_EN);
@@ -8,6 +8,8 @@ export { default as FieldInput, type Props as FieldInputProps, } from "./FieldIn
8
8
  export { default as FieldMoney, type Props as FieldMoneyProps, } from "./FieldMoney.svelte";
9
9
  export { default as FieldLikeButton, type Props as FieldLikeButtonProps, } from "./FieldLikeButton.svelte";
10
10
  export { default as FieldOptions, type Props as FieldOptionsProps, type Option as FieldOption, } from "./FieldOptions.svelte";
11
+ export { createFieldOptionsT, FIELD_OPTIONS_MESSAGES_EN, type FieldOptionsMessageKey, type FieldOptionsMessages, } from "./field-options-i18n.js";
12
+ export { FIELD_OPTIONS_MESSAGES_SK } from "./field-options-i18n-sk.js";
11
13
  export { default as FieldRadios, type Props as FieldRadiosProps, } from "./FieldRadios.svelte";
12
14
  export { default as FieldSelect, type Props as FieldSelectProps, } from "./FieldSelect.svelte";
13
15
  export { default as Fieldset, type Props as FieldsetProps } from "./Fieldset.svelte";
@@ -8,6 +8,8 @@ export { default as FieldInput, } from "./FieldInput.svelte";
8
8
  export { default as FieldMoney, } from "./FieldMoney.svelte";
9
9
  export { default as FieldLikeButton, } from "./FieldLikeButton.svelte";
10
10
  export { default as FieldOptions, } from "./FieldOptions.svelte";
11
+ export { createFieldOptionsT, FIELD_OPTIONS_MESSAGES_EN, } from "./field-options-i18n.js";
12
+ export { FIELD_OPTIONS_MESSAGES_SK } from "./field-options-i18n-sk.js";
11
13
  export { default as FieldRadios, } from "./FieldRadios.svelte";
12
14
  export { default as FieldSelect, } from "./FieldSelect.svelte";
13
15
  export { default as Fieldset } from "./Fieldset.svelte";