@marianmeres/stuic 1.126.0 → 2.0.0-next.2

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 (252) hide show
  1. package/README.md +2 -8
  2. package/dist/_shared.css +2 -0
  3. package/dist/actions/autogrow.svelte.d.ts +6 -0
  4. package/dist/actions/autogrow.svelte.js +19 -0
  5. package/dist/actions/highlight-dragover.svelte.d.ts +7 -0
  6. package/dist/actions/highlight-dragover.svelte.js +38 -0
  7. package/dist/actions/index.d.ts +7 -0
  8. package/dist/actions/index.js +7 -0
  9. package/dist/actions/on-submit-validity-check.svelte.d.ts +15 -0
  10. package/dist/actions/on-submit-validity-check.svelte.js +58 -0
  11. package/dist/actions/tooltip/index.css +34 -0
  12. package/dist/actions/tooltip/tooltip.svelte.d.ts +13 -0
  13. package/dist/actions/tooltip/tooltip.svelte.js +203 -0
  14. package/dist/actions/trim.svelte.d.ts +4 -0
  15. package/dist/actions/trim.svelte.js +17 -0
  16. package/dist/actions/{validate.d.ts → validate.svelte.d.ts} +8 -8
  17. package/dist/actions/validate.svelte.js +90 -0
  18. package/dist/components/AlertConfirmPrompt/AlertConfirmPrompt.svelte +59 -385
  19. package/dist/components/AlertConfirmPrompt/AlertConfirmPrompt.svelte.d.ts +9 -101
  20. package/dist/components/AlertConfirmPrompt/Current.svelte +202 -0
  21. package/dist/components/AlertConfirmPrompt/Current.svelte.d.ts +22 -0
  22. package/dist/components/AlertConfirmPrompt/acp-icons.d.ts +7 -2
  23. package/dist/components/AlertConfirmPrompt/acp-icons.js +8 -8
  24. package/dist/components/AlertConfirmPrompt/alert-confirm-prompt-stack.svelte.d.ts +63 -0
  25. package/dist/components/AlertConfirmPrompt/alert-confirm-prompt-stack.svelte.js +144 -0
  26. package/dist/components/AlertConfirmPrompt/index.d.ts +2 -0
  27. package/dist/components/AlertConfirmPrompt/index.js +2 -0
  28. package/dist/components/AnimatedElipsis/AnimatedEllipsis.svelte +47 -0
  29. package/dist/components/AnimatedElipsis/AnimatedEllipsis.svelte.d.ts +7 -0
  30. package/dist/components/AnimatedElipsis/index.d.ts +1 -0
  31. package/dist/components/AnimatedElipsis/index.js +1 -0
  32. package/dist/components/AppShell/AppShell.svelte +188 -127
  33. package/dist/components/AppShell/AppShell.svelte.d.ts +62 -43
  34. package/dist/components/AppShell/index.d.ts +1 -0
  35. package/dist/components/AppShell/index.js +1 -0
  36. package/dist/components/Backdrop/Backdrop.svelte +149 -49
  37. package/dist/components/Backdrop/Backdrop.svelte.d.ts +22 -37
  38. package/dist/components/Backdrop/index.d.ts +1 -0
  39. package/dist/components/Backdrop/index.js +1 -0
  40. package/dist/components/Button/Button.svelte +122 -146
  41. package/dist/components/Button/Button.svelte.d.ts +22 -80
  42. package/dist/components/Button/index.css +16 -0
  43. package/dist/components/Button/index.d.ts +1 -0
  44. package/dist/components/Button/index.js +1 -0
  45. package/dist/components/ColResize/ColResize.svelte +0 -0
  46. package/dist/components/ColResize/ColResize.svelte.d.ts +26 -0
  47. package/dist/components/ColorScheme/{LocalColorScheme.svelte → ColorSchemeLocal.svelte} +2 -2
  48. package/dist/components/ColorScheme/ColorSchemeLocal.svelte.d.ts +26 -0
  49. package/dist/components/ColorScheme/{SystemAwareColorScheme.svelte → ColorSchemeSystemAware.svelte} +4 -4
  50. package/dist/components/ColorScheme/ColorSchemeSystemAware.svelte.d.ts +26 -0
  51. package/dist/components/ColorScheme/color-scheme.d.ts +26 -8
  52. package/dist/components/ColorScheme/color-scheme.js +40 -16
  53. package/dist/components/ColorScheme/index.d.ts +3 -0
  54. package/dist/components/ColorScheme/index.js +3 -0
  55. package/dist/components/DismissibleMessage/DismissibleMessage.svelte +76 -83
  56. package/dist/components/DismissibleMessage/DismissibleMessage.svelte.d.ts +16 -37
  57. package/dist/components/DismissibleMessage/index.css +13 -0
  58. package/dist/components/DismissibleMessage/index.d.ts +1 -0
  59. package/dist/components/DismissibleMessage/index.js +1 -0
  60. package/dist/components/Drawer/Drawer.svelte +155 -84
  61. package/dist/components/Drawer/Drawer.svelte.d.ts +24 -35
  62. package/dist/components/Drawer/index.d.ts +1 -0
  63. package/dist/components/Drawer/index.js +1 -0
  64. package/dist/components/HoverExpandableWidth/HoverExpandableWidth.svelte +150 -111
  65. package/dist/components/HoverExpandableWidth/HoverExpandableWidth.svelte.d.ts +16 -29
  66. package/dist/components/HoverExpandableWidth/index.d.ts +1 -0
  67. package/dist/components/HoverExpandableWidth/index.js +1 -0
  68. package/dist/components/Input/FieldCheckbox.svelte +174 -132
  69. package/dist/components/Input/FieldCheckbox.svelte.d.ts +28 -64
  70. package/dist/components/Input/FieldFile.svelte +166 -0
  71. package/dist/components/Input/FieldFile.svelte.d.ts +41 -0
  72. package/dist/components/Input/FieldInput.svelte +143 -0
  73. package/dist/components/Input/FieldInput.svelte.d.ts +41 -0
  74. package/dist/components/Input/FieldLikeButton.svelte +206 -0
  75. package/dist/components/Input/FieldLikeButton.svelte.d.ts +41 -0
  76. package/dist/components/Input/FieldOptions.svelte +646 -0
  77. package/dist/components/Input/FieldOptions.svelte.d.ts +58 -0
  78. package/dist/components/Input/FieldRadios.svelte +126 -77
  79. package/dist/components/Input/FieldRadios.svelte.d.ts +23 -61
  80. package/dist/components/Input/FieldSelect.svelte +160 -239
  81. package/dist/components/Input/FieldSelect.svelte.d.ts +40 -88
  82. package/dist/components/Input/FieldSwitch.svelte +132 -0
  83. package/dist/components/Input/FieldSwitch.svelte.d.ts +41 -0
  84. package/dist/components/Input/FieldTextarea.svelte +146 -0
  85. package/dist/components/Input/FieldTextarea.svelte.d.ts +44 -0
  86. package/dist/components/Input/Fieldset.svelte +21 -17
  87. package/dist/components/Input/Fieldset.svelte.d.ts +10 -27
  88. package/dist/components/Input/_internal/FieldRadioInternal.svelte +186 -0
  89. package/dist/components/Input/_internal/FieldRadioInternal.svelte.d.ts +30 -0
  90. package/dist/components/Input/_internal/InputWrap.svelte +216 -0
  91. package/dist/components/Input/_internal/InputWrap.svelte.d.ts +36 -0
  92. package/dist/components/Input/index.css +134 -0
  93. package/dist/components/Input/index.d.ts +11 -0
  94. package/dist/components/Input/index.js +11 -0
  95. package/dist/components/Input/types.d.ts +11 -0
  96. package/dist/components/KbdShortcut/KbdShortcut.svelte +89 -0
  97. package/dist/components/KbdShortcut/KbdShortcut.svelte.d.ts +17 -0
  98. package/dist/components/KbdShortcut/index.d.ts +1 -0
  99. package/dist/components/KbdShortcut/index.js +1 -0
  100. package/dist/components/Modal/Modal.svelte +127 -0
  101. package/dist/components/Modal/Modal.svelte.d.ts +32 -0
  102. package/dist/components/Modal/index.d.ts +1 -0
  103. package/dist/components/Modal/index.js +1 -0
  104. package/dist/components/ModalDialog/ModalDialog.svelte +137 -81
  105. package/dist/components/ModalDialog/ModalDialog.svelte.d.ts +17 -38
  106. package/dist/components/ModalDialog/index.d.ts +1 -0
  107. package/dist/components/ModalDialog/index.js +1 -0
  108. package/dist/components/Notifications/Notifications.svelte +259 -173
  109. package/dist/components/Notifications/Notifications.svelte.d.ts +32 -60
  110. package/dist/components/Notifications/index.css +12 -0
  111. package/dist/components/Notifications/index.d.ts +2 -0
  112. package/dist/components/Notifications/index.js +2 -0
  113. package/dist/components/Notifications/notifications-icons.d.ts +1 -1
  114. package/dist/components/Notifications/notifications-icons.js +4 -4
  115. package/dist/components/Notifications/notifications-stack.svelte.d.ts +89 -0
  116. package/dist/components/Notifications/notifications-stack.svelte.js +161 -0
  117. package/dist/components/Progress/Progress.svelte +26 -0
  118. package/dist/components/Progress/Progress.svelte.d.ts +10 -0
  119. package/dist/components/Progress/_internal/Bar.svelte +31 -0
  120. package/dist/components/Progress/_internal/Bar.svelte.d.ts +10 -0
  121. package/dist/components/Progress/_internal/Circle.svelte +10 -0
  122. package/dist/components/Progress/_internal/Circle.svelte.d.ts +7 -0
  123. package/dist/components/Progress/index.css +7 -0
  124. package/dist/components/Progress/index.d.ts +1 -0
  125. package/dist/components/Progress/index.js +1 -0
  126. package/dist/components/Spinner/Spinner.svelte +56 -41
  127. package/dist/components/Spinner/Spinner.svelte.d.ts +10 -22
  128. package/dist/components/Spinner/index.d.ts +1 -0
  129. package/dist/components/Spinner/index.js +1 -0
  130. package/dist/components/Switch/Switch.svelte +158 -118
  131. package/dist/components/Switch/Switch.svelte.d.ts +25 -66
  132. package/dist/components/Switch/SwitchButton.svelte +131 -0
  133. package/dist/components/Switch/SwitchButton.svelte.d.ts +21 -0
  134. package/dist/components/Switch/index.css +7 -0
  135. package/dist/components/Switch/index.d.ts +2 -0
  136. package/dist/components/Switch/index.js +2 -0
  137. package/dist/components/Thc/Thc.svelte +67 -10
  138. package/dist/components/Thc/Thc.svelte.d.ts +18 -22
  139. package/dist/components/Thc/index.d.ts +1 -0
  140. package/dist/components/Thc/index.js +1 -0
  141. package/dist/components/TwCheck/TwCheck.svelte +34 -0
  142. package/dist/components/TwCheck/TwCheck.svelte.d.ts +10 -0
  143. package/dist/components/TwCheck/index.css +5 -0
  144. package/dist/components/TwCheck/index.d.ts +1 -0
  145. package/dist/components/TwCheck/index.js +1 -0
  146. package/dist/components/X/X.svelte +12 -5
  147. package/dist/components/X/X.svelte.d.ts +6 -18
  148. package/dist/components/X/index.d.ts +1 -0
  149. package/dist/components/X/index.js +1 -0
  150. package/dist/index.css +26 -0
  151. package/dist/index.d.ts +21 -39
  152. package/dist/index.js +23 -54
  153. package/dist/types.d.ts +251 -2
  154. package/dist/types.js +248 -0
  155. package/dist/utils/breakpoint.svelte.d.ts +19 -0
  156. package/dist/utils/breakpoint.svelte.js +42 -0
  157. package/dist/utils/debounce.d.ts +13 -0
  158. package/dist/utils/debounce.js +22 -0
  159. package/dist/utils/device-pointer.svelte.d.ts +11 -0
  160. package/dist/utils/device-pointer.svelte.js +26 -0
  161. package/dist/utils/event-modifiers.d.ts +4 -0
  162. package/dist/utils/event-modifiers.js +29 -0
  163. package/dist/utils/get-id.d.ts +1 -1
  164. package/dist/utils/get-id.js +3 -1
  165. package/dist/utils/index.d.ts +21 -0
  166. package/dist/utils/index.js +21 -0
  167. package/dist/utils/is-browser.d.ts +1 -0
  168. package/dist/utils/is-browser.js +5 -0
  169. package/dist/utils/is-mac.d.ts +1 -0
  170. package/dist/utils/is-mac.js +11 -0
  171. package/dist/utils/maybe-json-parse.d.ts +1 -0
  172. package/dist/utils/maybe-json-parse.js +12 -0
  173. package/dist/utils/maybe-json-stringify.d.ts +1 -0
  174. package/dist/utils/maybe-json-stringify.js +11 -0
  175. package/dist/utils/move-array-item.d.ts +4 -0
  176. package/dist/utils/move-array-item.js +20 -0
  177. package/dist/utils/omit-pick.d.ts +2 -2
  178. package/dist/utils/omit-pick.js +10 -8
  179. package/dist/utils/paint.d.ts +18 -0
  180. package/dist/utils/paint.js +32 -0
  181. package/dist/utils/persistent-state.svelte.d.ts +23 -0
  182. package/dist/utils/persistent-state.svelte.js +48 -0
  183. package/dist/utils/prefers-reduced-motion.svelte.d.ts +2 -0
  184. package/dist/utils/prefers-reduced-motion.svelte.js +4 -0
  185. package/dist/utils/qsa.d.ts +1 -0
  186. package/dist/utils/qsa.js +3 -0
  187. package/dist/utils/sleep.d.ts +28 -0
  188. package/dist/utils/sleep.js +33 -0
  189. package/dist/utils/storage-abstraction.d.ts +35 -0
  190. package/dist/utils/storage-abstraction.js +136 -0
  191. package/dist/utils/str-hash.d.ts +7 -0
  192. package/dist/utils/str-hash.js +35 -0
  193. package/dist/utils/throttle.d.ts +1 -0
  194. package/dist/utils/throttle.js +47 -0
  195. package/dist/utils/to-integer.d.ts +1 -0
  196. package/dist/utils/to-integer.js +11 -0
  197. package/dist/utils/tr.d.ts +5 -0
  198. package/dist/utils/tr.js +13 -0
  199. package/dist/utils/tw-merge.d.ts +10 -0
  200. package/dist/utils/tw-merge.js +16 -0
  201. package/dist/utils/ucfirst.d.ts +1 -0
  202. package/dist/utils/ucfirst.js +6 -0
  203. package/package.json +66 -73
  204. package/dist/actions/autogrow.d.ts +0 -8
  205. package/dist/actions/autogrow.js +0 -22
  206. package/dist/actions/autoscroll.d.ts +0 -21
  207. package/dist/actions/autoscroll.js +0 -60
  208. package/dist/actions/drag-drop.d.ts +0 -28
  209. package/dist/actions/drag-drop.js +0 -152
  210. package/dist/actions/on-outside.d.ts +0 -9
  211. package/dist/actions/on-outside.js +0 -27
  212. package/dist/actions/pre-submit-validity-check.d.ts +0 -3
  213. package/dist/actions/pre-submit-validity-check.js +0 -21
  214. package/dist/actions/tooltip/_make-visible.d.ts +0 -3
  215. package/dist/actions/tooltip/_make-visible.js +0 -25
  216. package/dist/actions/tooltip/_maybe-pick-safe-placement.d.ts +0 -3
  217. package/dist/actions/tooltip/_maybe-pick-safe-placement.js +0 -86
  218. package/dist/actions/tooltip/_set-position.d.ts +0 -2
  219. package/dist/actions/tooltip/_set-position.js +0 -125
  220. package/dist/actions/tooltip/tooltip.d.ts +0 -42
  221. package/dist/actions/tooltip/tooltip.js +0 -299
  222. package/dist/actions/trim.d.ts +0 -4
  223. package/dist/actions/trim.js +0 -18
  224. package/dist/actions/validate.js +0 -80
  225. package/dist/components/AlertConfirmPrompt/alert-confirm-prompt.d.ts +0 -58
  226. package/dist/components/AlertConfirmPrompt/alert-confirm-prompt.js +0 -141
  227. package/dist/components/ColorScheme/LocalColorScheme.svelte.d.ts +0 -25
  228. package/dist/components/ColorScheme/SystemAwareColorScheme.svelte.d.ts +0 -25
  229. package/dist/components/Input/Field.svelte +0 -315
  230. package/dist/components/Input/Field.svelte.d.ts +0 -102
  231. package/dist/components/Input/PinInput.svelte +0 -151
  232. package/dist/components/Input/PinInput.svelte.d.ts +0 -51
  233. package/dist/components/Input/XFieldRadioInternal.svelte +0 -143
  234. package/dist/components/Input/XFieldRadioInternal.svelte.d.ts +0 -45
  235. package/dist/components/Notifications/notifications.d.ts +0 -78
  236. package/dist/components/Notifications/notifications.js +0 -215
  237. package/dist/components/Popover/Popover.svelte +0 -24
  238. package/dist/components/Popover/Popover.svelte.d.ts +0 -22
  239. package/dist/components/Spinner/Spinner.v5.svelte +0 -114
  240. package/dist/components/Spinner/Spinner.v5.svelte.d.ts +0 -16
  241. package/dist/utils/calculate-alignment.d.ts +0 -68
  242. package/dist/utils/calculate-alignment.js +0 -183
  243. package/dist/utils/device-pointer.d.ts +0 -5
  244. package/dist/utils/device-pointer.js +0 -10
  245. package/dist/utils/prefers-reduced-motion.d.ts +0 -6
  246. package/dist/utils/prefers-reduced-motion.js +0 -26
  247. package/dist/utils/tw-merge2.d.ts +0 -3
  248. package/dist/utils/tw-merge2.js +0 -9
  249. package/dist/utils/tw-types.d.ts +0 -1
  250. package/dist/utils/window-size.d.ts +0 -22
  251. package/dist/utils/window-size.js +0 -35
  252. /package/dist/{utils/tw-types.js → components/Input/types.js} +0 -0
@@ -0,0 +1,646 @@
1
+ <script lang="ts" module>
2
+ export interface Option {
3
+ label: string;
4
+ value: any;
5
+ }
6
+
7
+ // i18n ready
8
+ function t_default(k: string) {
9
+ const m: Record<string, string> = {
10
+ field_req_att: "This field requires attention. Please review and try again.",
11
+ cardinality_of: "of",
12
+ cardinality_selected: "selected",
13
+ submit: "Submit",
14
+ select_all: "Select all",
15
+ clear_all: "Clear all",
16
+ search_placeholder: "Type to search...",
17
+ cardinality_full: "Max selection reached",
18
+ select_from_list: "Please select from the list",
19
+ x_close: "Clear input or close [esc]",
20
+ unknown_allowed: "Select from the list or type and submit any value",
21
+ unknown_not_allowed: "Select values from the list only",
22
+ };
23
+ return m[k] ?? k;
24
+ }
25
+ </script>
26
+
27
+ <script lang="ts">
28
+ import { createClog } from "@marianmeres/clog";
29
+ import { iconBsSearch } from "@marianmeres/icons-fns/bootstrap/iconBsSearch.js";
30
+ import { iconLucideCheck } from "@marianmeres/icons-fns/lucide/iconLucideCheck.js";
31
+ import { iconLucideCircle } from "@marianmeres/icons-fns/lucide/iconLucideCircle.js";
32
+ import { iconLucideSquare } from "@marianmeres/icons-fns/lucide/iconLucideSquare.js";
33
+ import { ItemCollection, type Item } from "@marianmeres/item-collection";
34
+ import { Debounced, watch } from "runed";
35
+ import { type Snippet } from "svelte";
36
+ import { tooltip } from "../../actions/index.js";
37
+ import { type ValidateOptions } from "../../actions/validate.svelte.js";
38
+ import { getId } from "../../utils/get-id.js";
39
+ import { maybeJsonParse } from "../../utils/maybe-json-parse.js";
40
+ import { waitForNextRepaint } from "../../utils/paint.js";
41
+ import { qsa } from "../../utils/qsa.js";
42
+ import { strHash } from "../../utils/str-hash.js";
43
+ import { twMerge } from "../../utils/tw-merge.js";
44
+ import Button from "../Button/Button.svelte";
45
+ import Modal from "../Modal/Modal.svelte";
46
+ import { NotificationsStack } from "../Notifications/index.js";
47
+ import Spinner from "../Spinner/Spinner.svelte";
48
+ import type { THC } from "../Thc/Thc.svelte";
49
+ import X from "../X/X.svelte";
50
+ import InputWrap from "./_internal/InputWrap.svelte";
51
+ import FieldLikeButton from "./FieldLikeButton.svelte";
52
+
53
+ const clog = createClog("FieldOptions");
54
+
55
+ const iconCheckboxEmpty = iconLucideSquare;
56
+ const iconCheckboxCheck = iconLucideCheck;
57
+
58
+ const iconRadioEmpty = iconLucideCircle;
59
+ const iconRadioCheck = iconLucideCheck;
60
+
61
+ type SnippetWithId = Snippet<[{ id: string }]>;
62
+
63
+ interface Props extends Record<string, any> {
64
+ input?: HTMLInputElement;
65
+ value: string;
66
+ label?: SnippetWithId | THC;
67
+ type?: string;
68
+ description?: SnippetWithId | THC;
69
+ class?: string;
70
+ id?: string;
71
+ tabindex?: number; // tooShort
72
+ renderSize?: "sm" | "md" | "lg" | string;
73
+ useTrim?: boolean;
74
+ //
75
+ required?: boolean;
76
+ disabled?: boolean;
77
+ //
78
+ validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
79
+ // wrap snippets
80
+ labelAfter?: SnippetWithId | THC;
81
+ below?: SnippetWithId | THC;
82
+ //
83
+ labelLeft?: boolean;
84
+ labelLeftWidth?: "normal" | "wide";
85
+ labelLeftBreakpoint?: number;
86
+ //
87
+ classInput?: string;
88
+ classLabel?: string;
89
+ classLabelBox?: string;
90
+ classInputBox?: string;
91
+ classInputBoxWrap?: string;
92
+ classDescBox?: string;
93
+ classBelowBox?: string;
94
+ //
95
+ classOption?: string;
96
+ classOptionActive?: string;
97
+ //
98
+ classModalField?: string;
99
+ noScrollLock?: boolean;
100
+ //
101
+ style?: string;
102
+ t?: (key: string) => string;
103
+ //
104
+ renderValue?: (strigifiedItems: string) => string;
105
+ getOptions: (s: string, current: Item[]) => Promise<Item[]>;
106
+ notifications?: NotificationsStack;
107
+ // -1 no limit
108
+ // +n max selected limit
109
+ cardinality?: number;
110
+ renderOptionLabel?: (item: Item) => string;
111
+ // whether to allow adding unknown options
112
+ allowUnknown?: boolean;
113
+ showIcons?: boolean;
114
+ searchPlaceholder?: string;
115
+ name: string;
116
+ itemIdPropName?: string;
117
+ }
118
+
119
+ let {
120
+ input = $bindable(),
121
+ value = $bindable(), //
122
+ label = "",
123
+ id = getId(),
124
+ type = "text",
125
+ tabindex = 0,
126
+ description,
127
+ class: classProp,
128
+ renderSize = "md",
129
+ useTrim = true,
130
+ //
131
+ required = false,
132
+ disabled = false,
133
+ //
134
+ validate,
135
+ //
136
+ labelAfter,
137
+ below,
138
+ //
139
+ labelLeft = false,
140
+ labelLeftWidth = "normal",
141
+ labelLeftBreakpoint = 480,
142
+ //
143
+ classInput,
144
+ classLabel,
145
+ classLabelBox,
146
+ classInputBox,
147
+ classInputBoxWrap,
148
+ classDescBox,
149
+ classBelowBox,
150
+ //
151
+ classOption,
152
+ classOptionActive,
153
+ //
154
+ style,
155
+ //
156
+ classModalField,
157
+ noScrollLock = false,
158
+ t = t_default,
159
+ //
160
+ renderValue,
161
+ getOptions,
162
+ notifications,
163
+ cardinality: _cardinality = Infinity,
164
+ renderOptionLabel,
165
+ allowUnknown = false,
166
+ showIcons = true,
167
+ searchPlaceholder,
168
+ name,
169
+ itemIdPropName = "id",
170
+ ...rest
171
+ }: Props = $props();
172
+
173
+ let modal: Modal = $state()!;
174
+ let innerValue = $state("");
175
+ let isFetching = $state(false);
176
+ let cardinality = $derived(_cardinality === -1 ? Infinity : _cardinality);
177
+ let isMultiple = $derived(cardinality > 1);
178
+
179
+ //
180
+ let wrappedValidate: Omit<ValidateOptions, "setValidationResult"> = $derived({
181
+ enabled: true,
182
+ customValidator(value: any, context: Record<string, any> | undefined, el: any) {
183
+ // NOTE: the below error message code will be ignored, so it's just cosmetics.
184
+ // This, built-in JSON array validator cannot be bypassed. Strictly expecting array.
185
+ let selected = [];
186
+ try {
187
+ selected = JSON.parse(value);
188
+ if (!Array.isArray(selected)) return "typeMismatch";
189
+ } catch (e) {
190
+ return "typeMismatch";
191
+ }
192
+ // cardinality check
193
+ if (selected.length > cardinality) return "rangeOverflow";
194
+
195
+ // continue with provided validator
196
+ return (validate as any)?.customValidator?.(value, context, el) || "";
197
+ },
198
+ t(reason: keyof ValidityStateFlags, value: any, fallback: string) {
199
+ // Unfortunately, for hidden, everything is a `customError` reason. So, we must generalize...
200
+ return t("field_req_att");
201
+ },
202
+ });
203
+
204
+ function _renderOptionLabel(item: Item): string {
205
+ return renderOptionLabel?.(item) || `${item[itemIdPropName]}`;
206
+ }
207
+
208
+ function sortFn(a: Item, b: Item) {
209
+ return _renderOptionLabel(a).localeCompare(_renderOptionLabel(b), undefined, {
210
+ sensitivity: "base",
211
+ });
212
+ }
213
+
214
+ // let's have two distinct collections for the job, they are independent on each other
215
+ // first, the all available options
216
+ const _optionsColl = new ItemCollection([], {
217
+ allowNextPrevCycle: false,
218
+ sortFn,
219
+ idPropName: itemIdPropName,
220
+ searchable: { getContent: (item) => _renderOptionLabel(item) },
221
+ });
222
+
223
+ // second, the selected ones
224
+ const _selectedColl = new ItemCollection([], {
225
+ cardinality,
226
+ sortFn,
227
+ idPropName: itemIdPropName,
228
+ });
229
+
230
+ // now, create the reactive, subscribed variants
231
+ let options = $derived($_optionsColl);
232
+ let selected = $derived($_selectedColl);
233
+ // $inspect("options", options);
234
+ // $inspect("selected", selected);
235
+
236
+ let activeEl: HTMLButtonElement | undefined = $state();
237
+ let optionsBox: HTMLUListElement | undefined = $state();
238
+ let modalEl: HTMLDivElement | undefined = $state();
239
+
240
+ // set value on open
241
+ watch(
242
+ () => modal.visibility().visible,
243
+ (isVisible, wasVisible) => {
244
+ // modal was just opened
245
+ if (!wasVisible && isVisible) {
246
+ _selectedColl.clear().addMany(maybeJsonParse(value));
247
+ // IMPORTANT: focus first selected so it scrolls into view on open
248
+ if (_selectedColl.size) {
249
+ waitForNextRepaint().then(() => {
250
+ _optionsColl.setActive(_selectedColl.items[0]);
251
+ });
252
+ }
253
+ }
254
+ }
255
+ );
256
+
257
+ // scroll the active option into view
258
+ $effect(() => {
259
+ if (modal.visibility().visible && options.active?.[itemIdPropName]) {
260
+ activeEl = qsa(`#${btnId(options.active[itemIdPropName])}`, optionsBox)[0] as any;
261
+ activeEl?.scrollIntoView({ behavior: "smooth", block: "center" });
262
+ activeEl?.focus();
263
+ } else {
264
+ activeEl = undefined;
265
+ }
266
+ });
267
+
268
+ // suggest options as a typeahead feature
269
+ const debounced = new Debounced(() => innerValue, 150);
270
+ watch(
271
+ [() => modal.visibility().visible, () => debounced.current],
272
+ ([isVisible, currVal]) => {
273
+ if (!isVisible) return;
274
+ isFetching = true;
275
+ getOptions(currVal, selected.items)
276
+ .then((res) => {
277
+ // always update the existing with recent server data
278
+ _selectedColl.patchMany(res);
279
+ // continue normally, with (server) provided options...
280
+ _optionsColl.clear().addMany(res);
281
+ })
282
+ .catch((e) => {
283
+ console.error(e);
284
+ notifications?.error(`${e}`);
285
+ })
286
+ .finally(() => (isFetching = false));
287
+ }
288
+ );
289
+
290
+ // internal DRY
291
+ function btnId(id: string | number, prefix = "btn-") {
292
+ return prefix + strHash(`${id}`.repeat(3));
293
+ }
294
+
295
+ // this will set the outer bound value (always string) and close modal... further process is left on the consumer
296
+ function submit() {
297
+ // clog("modal submit", $state.snapshot(selected.items));
298
+ value = JSON.stringify(selected.items);
299
+ innerValue = "";
300
+ _optionsColl.clear();
301
+ modal.close();
302
+ }
303
+
304
+ // clears, closes, submits nothing
305
+ function escape() {
306
+ innerValue = "";
307
+ _optionsColl.clear();
308
+ modal?.close();
309
+ }
310
+ </script>
311
+
312
+ <!-- this must be on window as we're catching any typing anywhere -->
313
+ <svelte:window
314
+ onkeydown={(e) => {
315
+ if (modal.visibility().visible) {
316
+ // arrow navigation
317
+ if (["ArrowDown", "ArrowUp"].includes(e.key)) {
318
+ e.preventDefault();
319
+
320
+ if (e.key === "ArrowUp") {
321
+ e.metaKey ? _optionsColl.setActiveFirst() : _optionsColl.setActivePrevious();
322
+ } else if (e.key === "ArrowDown") {
323
+ e.metaKey ? _optionsColl.setActiveLast() : _optionsColl.setActiveNext();
324
+ }
325
+
326
+ // common UI convention: radios are selected by arrows
327
+ if (!isMultiple && _optionsColl.active) {
328
+ _selectedColl.clear().add(_optionsColl.active!);
329
+ }
330
+ }
331
+ // everything else (except controls) "forward" as an input search
332
+ else if (!["Tab", " ", "Enter"].includes(e.key)) {
333
+ input?.focus();
334
+ }
335
+ }
336
+ }}
337
+ />
338
+
339
+ <!-- must wrap both -->
340
+ <div>
341
+ <FieldLikeButton
342
+ bind:value
343
+ {name}
344
+ class={classProp}
345
+ {label}
346
+ {description}
347
+ {labelLeft}
348
+ {labelAfter}
349
+ {below}
350
+ {labelLeftWidth}
351
+ {labelLeftBreakpoint}
352
+ {classLabel}
353
+ {classLabelBox}
354
+ {classInputBox}
355
+ {classInputBoxWrap}
356
+ {classDescBox}
357
+ {classBelowBox}
358
+ {style}
359
+ validate={wrappedValidate}
360
+ {required}
361
+ {disabled}
362
+ renderValue={(v) => {
363
+ if (typeof renderValue === "function") return renderValue(v);
364
+ // console.log(123123, "renderValue", v);
365
+ // prettier-ignore
366
+ try {
367
+ // defensive
368
+ if (!v) v = "[]";
369
+
370
+ const limit = 5;
371
+ let vals: any[] = JSON.parse(v);
372
+ if (!Array.isArray(vals)) throw new Error('Expecting value to be an array');
373
+ const origLength = vals.length;
374
+ let extra = '';
375
+ if (vals.length > limit) {
376
+ vals = vals.slice(0, limit);
377
+ extra = `, ... <span class="text-sm opacity-50">(+${(origLength - limit)})</span>`;
378
+ }
379
+ return vals.map(_renderOptionLabel).join(", ") + extra;
380
+ } catch (e) {
381
+ clog.warn(e);
382
+ return `${e}`; // either invalid json or not array...
383
+ }
384
+ }}
385
+ onclick={modal?.open}
386
+ />
387
+
388
+ <Modal
389
+ bind:this={modal}
390
+ onEscape={escape}
391
+ class="bg-transparent dark:bg-transparent"
392
+ classInner="max-w-2xl"
393
+ bind:el={modalEl}
394
+ {noScrollLock}
395
+ >
396
+ <InputWrap
397
+ size={renderSize}
398
+ class={twMerge("m-4 mb-12 shadow-xl", classModalField)}
399
+ classInputBoxWrap={twMerge(
400
+ // always look like focused
401
+ `border border-input-accent dark:border-input-accent-dark`,
402
+ `ring-input-accent/20 dark:ring-input-accent-dark/20 ring-4`
403
+ )}
404
+ {id}
405
+ {required}
406
+ >
407
+ <input
408
+ bind:value={innerValue}
409
+ bind:this={input}
410
+ {type}
411
+ {id}
412
+ class={twMerge("form-input", renderSize, classInput)}
413
+ tabindex={1}
414
+ {required}
415
+ {disabled}
416
+ placeholder={searchPlaceholder ?? t("search_placeholder")}
417
+ onkeydown={(e) => {
418
+ if (e.key === "Enter") {
419
+ e.preventDefault();
420
+
421
+ if (innerValue) {
422
+ // doing label search, taking first result
423
+ let found = _optionsColl.search(innerValue)?.[0];
424
+ if (!found) {
425
+ if (!allowUnknown) {
426
+ return notifications?.error(t("select_from_list"), { ttl: 1000 });
427
+ }
428
+ found = { [itemIdPropName]: innerValue };
429
+ }
430
+
431
+ if (!isMultiple) _selectedColl.clear();
432
+
433
+ // actual selection addon
434
+ _selectedColl.add(found);
435
+
436
+ // we might have added a new one, so add it to options as well
437
+ // (will be noop if already exists)...
438
+ if (allowUnknown) {
439
+ _optionsColl.add(found);
440
+ _optionsColl.setActive(found);
441
+ }
442
+
443
+ // maybe submit
444
+ if (_selectedColl.isFull) submit();
445
+ }
446
+ // enter on empty input always submits
447
+ else {
448
+ submit();
449
+ }
450
+ }
451
+ }}
452
+ autocomplete="off"
453
+ name={`rand-${Math.random().toString(36).slice(2)}`}
454
+ {...rest}
455
+ />
456
+
457
+ {#snippet inputBelow()}
458
+ <div class="h-full border-t p-2 border-black/20">
459
+ <div class="text-sm -mt-1 flex items-center">
460
+ {#if isMultiple}
461
+ <button
462
+ type="button"
463
+ onclick={() => _selectedColl.addMany(options.items)}
464
+ class={twMerge(
465
+ "control flex items-center p-1 m-1 text-xs opacity-75 underline rounded",
466
+ "hover:opacity-100 focus-visible:outline-neutral-400 focus-visible:opacity-100"
467
+ )}
468
+ tabindex={4}
469
+ >
470
+ {@html t("select_all")}
471
+ </button>
472
+ {/if}
473
+ <button
474
+ type="button"
475
+ onclick={() => {
476
+ _selectedColl.clear();
477
+ input?.focus();
478
+ }}
479
+ class={twMerge(
480
+ "control flex items-center p-1 m-1 text-xs opacity-75 underline rounded",
481
+ "hover:opacity-100 focus-visible:outline-neutral-400 focus-visible:opacity-100"
482
+ )}
483
+ class:opacity-20={!selected.items.length}
484
+ tabindex={5}
485
+ disabled={!selected.items.length}
486
+ >
487
+ {@html t("clear_all")}
488
+ </button>
489
+
490
+ <span class="p-1 m-1 text-xs">&nbsp;</span>
491
+ <span class="flex-1 block justify-end opacity-50 text-right text-xs p-1 pr-2">
492
+ {selected.items.length}
493
+ {#if cardinality > 0 && cardinality < Infinity}
494
+ {@html t("cardinality_of")} {cardinality}
495
+ {/if}
496
+ {@html t("cardinality_selected")}
497
+ </span>
498
+ </div>
499
+
500
+ <!-- {#if options.items.length} -->
501
+ <ul
502
+ class={twMerge(
503
+ "options block h-[250px] max-h-[250px] overflow-y-auto overflow-x-hidden space-y-1"
504
+ )}
505
+ bind:this={optionsBox}
506
+ tabindex="-1"
507
+ >
508
+ {#if isFetching && !options.items.length}
509
+ <div class="p-4 opacity-50">
510
+ <Spinner class="w-4" />
511
+ </div>
512
+ {/if}
513
+ {#each options.items as item}
514
+ {@const active = item[itemIdPropName] === options.active?.[itemIdPropName]}
515
+ {@const isSelected =
516
+ selected.items && _selectedColl.exists(item[itemIdPropName])}
517
+ <li class:active class="px-2">
518
+ <button
519
+ type="button"
520
+ id={btnId(item[itemIdPropName])}
521
+ onclick={() => {
522
+ if (isMultiple) {
523
+ if (selected.isFull && !_selectedColl.exists(item)) {
524
+ return notifications?.error(t("cardinality_full"), {
525
+ ttl: 1000,
526
+ });
527
+ }
528
+ _selectedColl.toggleAdd(item);
529
+ } else {
530
+ _selectedColl.clear();
531
+ _selectedColl.add(item);
532
+ submit();
533
+ }
534
+ }}
535
+ class:active
536
+ class:selected={isSelected}
537
+ class={twMerge(
538
+ "no-focus-visible",
539
+ "w-full text-left rounded-md py-2 px-2.5 flex items-center space-x-2",
540
+ "text-ellipsis border border-transparent",
541
+ "focus:outline-0 focus:border-neutral-400 dark:focus:border-neutral-500",
542
+ "focus-visible:outline-0 focus-visible:ring-0",
543
+ "hover:border-neutral-400 dark:hover:border-neutral-500",
544
+ isSelected && "bg-neutral-200 dark:bg-neutral-800",
545
+ classOption,
546
+ // active && "border-neutral-400",
547
+ active && classOptionActive
548
+ )}
549
+ tabindex="-1"
550
+ role="checkbox"
551
+ aria-checked={isSelected}
552
+ >
553
+ {#if showIcons}
554
+ <span class={isSelected ? "opacity-100" : "opacity-25"}>
555
+ {#if isMultiple}
556
+ {#if isSelected}
557
+ {@html iconCheckboxCheck()}
558
+ {:else}
559
+ {@html iconCheckboxEmpty()}
560
+ {/if}
561
+ {:else if isSelected}
562
+ {@html iconRadioCheck()}
563
+ {:else}
564
+ {@html iconRadioEmpty()}
565
+ {/if}
566
+ </span>
567
+ {/if}
568
+ <span>{_renderOptionLabel(item)}</span>
569
+ </button>
570
+ </li>
571
+ {/each}
572
+ </ul>
573
+ <!-- {/if} -->
574
+ <div class="p-2 flex items-end justify-between">
575
+ <div class="text-xs opacity-50">
576
+ <!-- Use arrows to navigate. Spacebar and Enter to select and/or submit. -->
577
+ {#if allowUnknown}
578
+ {@html t("unknown_allowed")}
579
+ {:else}
580
+ {@html t("unknown_not_allowed")}
581
+ {/if}
582
+ </div>
583
+ <div>
584
+ <Button
585
+ class="control"
586
+ type="button"
587
+ variant="primary"
588
+ onclick={(e) => {
589
+ e.preventDefault();
590
+ submit();
591
+ }}
592
+ tabindex={3}
593
+ >
594
+ {@html t("submit")}
595
+ </Button>
596
+ </div>
597
+ </div>
598
+ </div>
599
+ {/snippet}
600
+
601
+ {#snippet inputAfter()}
602
+ <div class="flex pl-2 items-center justify-center opacity-50">
603
+ {#if isFetching}
604
+ <Spinner class="w-4" />
605
+ {/if}
606
+ </div>
607
+ <div class="flex pl-2 pr-1 items-center justify-center">
608
+ <button
609
+ type="button"
610
+ class={twMerge(
611
+ "opacity-50 rounded",
612
+ "hover:opacity-100 hover:bg-neutral-200 dark:hover:bg-neutral-800",
613
+ "focus-visible:opacity-100 focus-visible:outline-0",
614
+ " focus-visible:bg-neutral-200 dark:focus-visible:bg-neutral-800"
615
+ )}
616
+ use:tooltip
617
+ aria-label={t("x_close")}
618
+ onclick={(e) => {
619
+ e.preventDefault();
620
+ if (innerValue.trim() == "") {
621
+ return escape();
622
+ }
623
+ innerValue = "";
624
+ input?.focus();
625
+ }}
626
+ tabindex={2}
627
+ >
628
+ <X class="m-2 size-4 " />
629
+ </button>
630
+ </div>
631
+ {/snippet}
632
+
633
+ {#snippet inputBefore()}
634
+ <div class="flex flex-col items-center justify-center pl-3 opacity-50">
635
+ {@html iconBsSearch({ size: 14 })}
636
+ </div>
637
+ {/snippet}
638
+ </InputWrap>
639
+ </Modal>
640
+ </div>
641
+
642
+ <style>
643
+ ul.options {
644
+ scrollbar-width: thin;
645
+ }
646
+ </style>
@@ -0,0 +1,58 @@
1
+ export interface Option {
2
+ label: string;
3
+ value: any;
4
+ }
5
+ import { type Item } from "@marianmeres/item-collection";
6
+ import { type Snippet } from "svelte";
7
+ import { type ValidateOptions } from "../../actions/validate.svelte.js";
8
+ import { NotificationsStack } from "../Notifications/index.js";
9
+ import type { THC } from "../Thc/Thc.svelte";
10
+ type SnippetWithId = Snippet<[{
11
+ id: string;
12
+ }]>;
13
+ interface Props extends Record<string, any> {
14
+ input?: HTMLInputElement;
15
+ value: string;
16
+ label?: SnippetWithId | THC;
17
+ type?: string;
18
+ description?: SnippetWithId | THC;
19
+ class?: string;
20
+ id?: string;
21
+ tabindex?: number;
22
+ renderSize?: "sm" | "md" | "lg" | string;
23
+ useTrim?: boolean;
24
+ required?: boolean;
25
+ disabled?: boolean;
26
+ validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
27
+ labelAfter?: SnippetWithId | THC;
28
+ below?: SnippetWithId | THC;
29
+ labelLeft?: boolean;
30
+ labelLeftWidth?: "normal" | "wide";
31
+ labelLeftBreakpoint?: number;
32
+ classInput?: string;
33
+ classLabel?: string;
34
+ classLabelBox?: string;
35
+ classInputBox?: string;
36
+ classInputBoxWrap?: string;
37
+ classDescBox?: string;
38
+ classBelowBox?: string;
39
+ classOption?: string;
40
+ classOptionActive?: string;
41
+ classModalField?: string;
42
+ noScrollLock?: boolean;
43
+ style?: string;
44
+ t?: (key: string) => string;
45
+ renderValue?: (strigifiedItems: string) => string;
46
+ getOptions: (s: string, current: Item[]) => Promise<Item[]>;
47
+ notifications?: NotificationsStack;
48
+ cardinality?: number;
49
+ renderOptionLabel?: (item: Item) => string;
50
+ allowUnknown?: boolean;
51
+ showIcons?: boolean;
52
+ searchPlaceholder?: string;
53
+ name: string;
54
+ itemIdPropName?: string;
55
+ }
56
+ declare const FieldOptions: import("svelte").Component<Props, {}, "value" | "input">;
57
+ type FieldOptions = ReturnType<typeof FieldOptions>;
58
+ export default FieldOptions;