@marianmeres/stuic 2.0.0-next.5 → 2.0.3

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 (61) hide show
  1. package/dist/actions/file-dropzone.svelte.d.ts +8 -0
  2. package/dist/actions/file-dropzone.svelte.js +43 -0
  3. package/dist/actions/highlight-dragover.svelte.js +16 -3
  4. package/dist/actions/index.d.ts +2 -0
  5. package/dist/actions/index.js +2 -0
  6. package/dist/actions/resizable-width.svelte.d.ts +21 -0
  7. package/dist/actions/resizable-width.svelte.js +162 -0
  8. package/dist/actions/validate.svelte.js +13 -13
  9. package/dist/components/Backdrop/Backdrop.svelte +1 -1
  10. package/dist/components/Button/Button.svelte +1 -1
  11. package/dist/components/Button/Button.svelte.d.ts +1 -1
  12. package/dist/components/ButtonGroupRadio/ButtonGroupRadio.svelte +47 -12
  13. package/dist/components/ButtonGroupRadio/ButtonGroupRadio.svelte.d.ts +3 -2
  14. package/dist/components/ButtonGroupRadio/index.css +11 -2
  15. package/dist/components/ButtonGroupRadio/index.d.ts +1 -0
  16. package/dist/components/ButtonGroupRadio/index.js +1 -0
  17. package/dist/components/CommandMenu/CommandMenu.svelte +365 -0
  18. package/dist/components/CommandMenu/CommandMenu.svelte.d.ts +25 -0
  19. package/dist/components/CommandMenu/index.d.ts +1 -0
  20. package/dist/components/CommandMenu/index.js +1 -0
  21. package/dist/components/Input/FieldInput.svelte +1 -0
  22. package/dist/components/Input/FieldLikeButton.svelte +16 -7
  23. package/dist/components/Input/FieldLikeButton.svelte.d.ts +1 -1
  24. package/dist/components/Input/FieldOptions.svelte +278 -120
  25. package/dist/components/Input/FieldOptions.svelte.d.ts +15 -8
  26. package/dist/components/Input/_internal/InputWrap.svelte +7 -6
  27. package/dist/components/Modal/Modal.svelte +10 -5
  28. package/dist/components/ModalDialog/ModalDialog.svelte +25 -0
  29. package/dist/components/Notifications/Notifications.svelte +1 -1
  30. package/dist/components/Progress/_internal/Bar.svelte +1 -1
  31. package/dist/components/Spinner/SpinnerUnicode.svelte +130 -0
  32. package/dist/components/Spinner/SpinnerUnicode.svelte.d.ts +12 -0
  33. package/dist/components/Spinner/index.d.ts +1 -0
  34. package/dist/components/Spinner/index.js +1 -0
  35. package/dist/components/TypeaheadInput/TypeaheadInput.svelte +261 -0
  36. package/dist/components/TypeaheadInput/TypeaheadInput.svelte.d.ts +40 -0
  37. package/dist/components/TypeaheadInput/index.d.ts +1 -0
  38. package/dist/components/TypeaheadInput/index.js +1 -0
  39. package/dist/index.css +1 -0
  40. package/dist/index.d.ts +3 -0
  41. package/dist/index.js +3 -0
  42. package/dist/types.d.ts +1 -0
  43. package/dist/utils/escape-regex.d.ts +1 -0
  44. package/dist/utils/escape-regex.js +1 -0
  45. package/dist/utils/event-emitter.d.ts +18 -0
  46. package/dist/utils/event-emitter.js +40 -0
  47. package/dist/utils/index.d.ts +5 -0
  48. package/dist/utils/index.js +5 -0
  49. package/dist/utils/is-plain-object.d.ts +2 -0
  50. package/dist/utils/is-plain-object.js +4 -0
  51. package/dist/utils/replace-map.d.ts +5 -0
  52. package/dist/utils/replace-map.js +22 -0
  53. package/dist/utils/seconds.d.ts +7 -0
  54. package/dist/utils/seconds.js +35 -0
  55. package/dist/utils/tw-merge.d.ts +2 -0
  56. package/dist/utils/tw-merge.js +4 -0
  57. package/dist/utils/unaccent.d.ts +6 -0
  58. package/dist/utils/unaccent.js +8 -0
  59. package/package.json +70 -66
  60. package/dist/components/ColResize/ColResize.svelte +0 -0
  61. package/dist/components/ColResize/ColResize.svelte.d.ts +0 -26
@@ -1,32 +1,4 @@
1
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
- clear: "Clear",
17
- search_placeholder: "Type to search...",
18
- search_submit_placeholder: "Type to search and/or submit...",
19
- cardinality_full: "Max selection reached",
20
- select_from_list: "Please select from the list",
21
- x_close: "Clear input or close [esc]",
22
- unknown_allowed: "Select from the list or type and submit any value",
23
- unknown_not_allowed: "Select values from the list only",
24
- };
25
- return m[k] ?? k;
26
- }
27
- </script>
28
-
29
- <script lang="ts">
30
2
  import { createClog } from "@marianmeres/clog";
31
3
  import { iconBsSearch } from "@marianmeres/icons-fns/bootstrap/iconBsSearch.js";
32
4
  import { iconLucideCheck } from "@marianmeres/icons-fns/lucide/iconLucideCheck.js";
@@ -34,7 +6,7 @@
34
6
  import { iconLucideSquare } from "@marianmeres/icons-fns/lucide/iconLucideSquare.js";
35
7
  import { ItemCollection, type Item } from "@marianmeres/item-collection";
36
8
  import { Debounced, watch } from "runed";
37
- import { type Snippet } from "svelte";
9
+ import { tick, type Snippet } from "svelte";
38
10
  import { tooltip } from "../../actions/index.js";
39
11
  import { type ValidateOptions } from "../../actions/validate.svelte.js";
40
12
  import { getId } from "../../utils/get-id.js";
@@ -51,7 +23,48 @@
51
23
  import X from "../X/X.svelte";
52
24
  import InputWrap from "./_internal/InputWrap.svelte";
53
25
  import FieldLikeButton from "./FieldLikeButton.svelte";
26
+ import { replaceMap } from "../../utils/replace-map.js";
27
+ import { isPlainObject } from "../../utils/is-plain-object.js";
28
+ import type { TranslateFn } from "../../types.js";
29
+
30
+ export interface Option {
31
+ label: string;
32
+ value: any;
33
+ }
54
34
 
35
+ // i18n ready
36
+ function t_default(
37
+ k: string,
38
+ values: false | null | undefined | Record<string, string | number> = null,
39
+ fallback: string | boolean = "",
40
+ i18nSpanWrap: boolean = true
41
+ ) {
42
+ const m: Record<string, string> = {
43
+ field_req_att: "This field requires attention. Please review and try again.",
44
+ cardinality_of: "of max",
45
+ cardinality_selected: "selected",
46
+ submit: "Submit",
47
+ select_all: "Select results",
48
+ clear_all: "Clear selected",
49
+ clear: "Clear",
50
+ search_placeholder: "Type to search...",
51
+ search_submit_placeholder: "Type to search and/or submit...",
52
+ cardinality_full: "Max selection reached",
53
+ select_from_list: "Please select from the list only",
54
+ x_close: "Clear input or close [esc]",
55
+ unknown_allowed: "Select or type and submit",
56
+ unknown_not_allowed: "Select from the list",
57
+ no_results: "No results found.",
58
+ add_new: 'Add "{{value}}"...',
59
+ click_add_new: "You must add the value to continue",
60
+ };
61
+ let out = m[k] ?? fallback ?? k;
62
+
63
+ return isPlainObject(values) ? replaceMap(out, values as any) : out;
64
+ }
65
+ </script>
66
+
67
+ <script lang="ts">
55
68
  const clog = createClog("FieldOptions");
56
69
 
57
70
  const iconCheckboxEmpty = iconLucideSquare;
@@ -96,23 +109,29 @@
96
109
  //
97
110
  classOption?: string;
98
111
  classOptionActive?: string;
112
+ classOptgroup?: string;
99
113
  //
100
114
  classModalField?: string;
101
115
  noScrollLock?: boolean;
102
116
  //
103
117
  style?: string;
104
- t?: (key: string) => string;
118
+ t?: TranslateFn;
105
119
  //
106
120
  renderValue?: (strigifiedItems: string) => string;
107
- getOptions: (s: string, current: Item[]) => Promise<Item[]>;
121
+ getOptions: (
122
+ q: string,
123
+ current: Item[]
124
+ ) => Promise<{ coll?: ItemCollection<Item>; found: Item[] }>;
108
125
  notifications?: NotificationsStack;
109
126
  // -1 no limit
110
127
  // +n max selected limit
111
128
  cardinality?: number;
112
129
  renderOptionLabel?: (item: Item) => string;
130
+ renderOptionGroup?: (s: string) => string;
113
131
  // whether to allow adding unknown options
114
132
  allowUnknown?: boolean;
115
- showIcons?: boolean;
133
+ showIconsCheckbox?: boolean;
134
+ showIconsRadio?: boolean;
116
135
  searchPlaceholder?: string;
117
136
  name: string;
118
137
  itemIdPropName?: string;
@@ -152,6 +171,7 @@
152
171
  //
153
172
  classOption,
154
173
  classOptionActive,
174
+ classOptgroup,
155
175
  //
156
176
  style,
157
177
  //
@@ -164,8 +184,10 @@
164
184
  notifications,
165
185
  cardinality: _cardinality = Infinity,
166
186
  renderOptionLabel,
187
+ renderOptionGroup = (s: string) => `${s}`.replaceAll("_", " "),
167
188
  allowUnknown = false,
168
- showIcons = true,
189
+ showIconsCheckbox = true,
190
+ showIconsRadio = false,
169
191
  searchPlaceholder,
170
192
  name,
171
193
  itemIdPropName = "id",
@@ -177,6 +199,7 @@
177
199
  let isFetching = $state(false);
178
200
  let cardinality = $derived(_cardinality === -1 ? Infinity : _cardinality);
179
201
  let isMultiple = $derived(cardinality > 1);
202
+ let showIcons = $derived(isMultiple ? showIconsCheckbox : showIconsRadio);
180
203
 
181
204
  //
182
205
  let wrappedValidate: Omit<ValidateOptions, "setValidationResult"> = $derived({
@@ -208,7 +231,8 @@
208
231
  }
209
232
 
210
233
  function sortFn(a: Item, b: Item) {
211
- return _renderOptionLabel(a).localeCompare(_renderOptionLabel(b), undefined, {
234
+ const withOptGroup = (i: Item) => `${i.optgroup || ""}__${_renderOptionLabel(i)}`;
235
+ return withOptGroup(a).localeCompare(withOptGroup(b), undefined, {
212
236
  sensitivity: "base",
213
237
  });
214
238
  }
@@ -238,13 +262,29 @@
238
262
  // now, create the reactive, subscribed variants
239
263
  let options = $derived($_optionsColl);
240
264
  let selected = $derived($_selectedColl);
265
+
266
+ // we need to know whether to show "Add xyz"...
267
+ function have_option_label_like(items: Item[], s: string) {
268
+ return items.some(
269
+ (item) => _renderOptionLabel(item).toLowerCase() === `${s}`.toLowerCase()
270
+ );
271
+ }
272
+
241
273
  // $inspect("options", options);
242
274
  // $inspect("selected", selected);
275
+ // $inspect("lastQuery", lastQuery, innerValue);
276
+
277
+ // hidden input which holds the final value (upon which validation happens)
278
+ let parentHiddenInputEl: HTMLInputElement | undefined = $state();
243
279
 
244
280
  let activeEl: HTMLButtonElement | undefined = $state();
245
- let optionsBox: HTMLUListElement | undefined = $state();
281
+ let optionsBox: HTMLDivElement | undefined = $state();
246
282
  let modalEl: HTMLDivElement | undefined = $state();
247
283
 
284
+ // add_new dance...
285
+ let addNewBtn: HTMLButtonElement | undefined = $state();
286
+ let isAddNewBtnActive = $state(false);
287
+
248
288
  // set value on open
249
289
  watch(
250
290
  () => modal.visibility().visible,
@@ -265,7 +305,7 @@
265
305
  // scroll the active option into view
266
306
  $effect(() => {
267
307
  if (modal.visibility().visible && options.active?.[itemIdPropName]) {
268
- activeEl = qsa(`#${btnId(options.active[itemIdPropName])}`, optionsBox)[0] as any;
308
+ activeEl = qsa(`#${btn_id(options.active[itemIdPropName])}`, optionsBox)[0] as any;
269
309
  activeEl?.scrollIntoView({ behavior: "smooth", block: "center" });
270
310
  activeEl?.focus();
271
311
  } else {
@@ -282,10 +322,12 @@
282
322
  isFetching = true;
283
323
  getOptions(currVal, selected.items)
284
324
  .then((res) => {
325
+ const { found, coll } = res;
326
+
285
327
  // always update the existing with recent server data
286
- _selectedColl.patchMany(res);
328
+ _selectedColl.patchMany(found);
287
329
  // continue normally, with (server) provided options...
288
- _optionsColl.clear().addMany(res);
330
+ _optionsColl.clear().addMany(found);
289
331
  })
290
332
  .catch((e) => {
291
333
  console.error(e);
@@ -296,32 +338,23 @@
296
338
  );
297
339
 
298
340
  // internal DRY
299
- function btnId(id: string | number, prefix = "btn-") {
341
+ function btn_id(id: string | number, prefix = "btn-") {
300
342
  return prefix + strHash(`${id}`.repeat(3));
301
343
  }
302
344
 
303
345
  // "inner" submit
304
346
  function try_submit(force = false) {
347
+ clog("try_submit", innerValue);
305
348
  if (innerValue) {
306
- // doing label search, taking first result
307
- let found = _optionsColl.search(innerValue)?.[0];
308
- if (!found) {
309
- if (!allowUnknown) {
310
- return notifications?.error(t("select_from_list"), { ttl: 1000 });
311
- }
312
- found = { [itemIdPropName]: innerValue };
349
+ let found = have_option_label_like(_optionsColl.items, innerValue);
350
+ if (!found && !allowUnknown) {
351
+ return notifications?.error(t("select_from_list"), { ttl: 1000 });
313
352
  }
314
353
 
315
- if (!isMultiple) _selectedColl.clear();
316
-
317
- // actual selection addon
318
- _selectedColl.add(found);
319
-
320
- // we might have added a new one, so add it to options as well
321
- // (will be noop if already exists)...
322
- if (allowUnknown) {
323
- _optionsColl.add(found);
324
- _optionsColl.setActive(found);
354
+ if (!found && !_optionsColl.size) {
355
+ return notifications?.error(t("click_add_new", { value: innerValue }), {
356
+ ttl: 1000,
357
+ });
325
358
  }
326
359
 
327
360
  // maybe submit
@@ -333,6 +366,27 @@
333
366
  }
334
367
  }
335
368
 
369
+ function add_new() {
370
+ // should be noop if called multiple times with same value
371
+ if (allowUnknown && innerValue) {
372
+ const item = { [itemIdPropName]: innerValue };
373
+ if (!isMultiple) _selectedColl.clear();
374
+ // actual selection addon
375
+ _selectedColl.add(item);
376
+ // we might have added a new one, so add it to options as well
377
+ // (will be noop if already exists)...
378
+ _optionsColl.add(item);
379
+ _optionsColl.setActive(item);
380
+ }
381
+ }
382
+
383
+ function _dispatch_change_to_owner() {
384
+ // trigger validation on the parent on each submit (emulate typical browser behaviour)
385
+ tick().then(() => {
386
+ parentHiddenInputEl?.dispatchEvent(new Event("change", { bubbles: true }));
387
+ });
388
+ }
389
+
336
390
  // "outer" submit - will set the outer bound value (always string) and close modal...
337
391
  // further process is left on the consumer
338
392
  function submit() {
@@ -341,6 +395,7 @@
341
395
  innerValue = "";
342
396
  _optionsColl.clear();
343
397
  modal.close();
398
+ _dispatch_change_to_owner();
344
399
  }
345
400
 
346
401
  // clears, closes, submits nothing
@@ -348,6 +403,64 @@
348
403
  innerValue = "";
349
404
  _optionsColl.clear();
350
405
  modal?.close();
406
+ _dispatch_change_to_owner();
407
+ }
408
+
409
+ function _normalize_and_group_options(opts: Item[]): Map<string, Item[]> {
410
+ const groupped = new Map<string, Item[]>();
411
+ opts.forEach((o) => {
412
+ const optgLabel = renderOptionGroup(o.optgroup || "");
413
+ if (!groupped.has(optgLabel)) groupped.set(optgLabel, []);
414
+ const optgroup = groupped.get(optgLabel);
415
+ optgroup!.push(o);
416
+ });
417
+ return groupped;
418
+ }
419
+
420
+ const BTN_CLS = [
421
+ "no-focus-visible",
422
+ "text-left rounded-md py-2 px-2.5 flex items-center space-x-2",
423
+ "w-full",
424
+ "border border-transparent",
425
+ "focus:outline-0 focus:border-neutral-400 dark:focus:border-neutral-500",
426
+ "focus-visible:outline-0 focus-visible:ring-0",
427
+ "hover:border-neutral-400 dark:hover:border-neutral-500",
428
+ ];
429
+
430
+ // add new dance
431
+ $effect(() => {
432
+ if (addNewBtn && isAddNewBtnActive) {
433
+ addNewBtn?.focus();
434
+ _optionsColl.unsetActive(); // make sure to reset
435
+ }
436
+ if (!addNewBtn) {
437
+ isAddNewBtnActive = false;
438
+ }
439
+ });
440
+
441
+ function maybe_activate_add_new(isDown: boolean, isMeta: boolean) {
442
+ // no button, no activation
443
+ if (!addNewBtn) return false;
444
+ const isUp = !isDown;
445
+
446
+ // separating below into distinct ifs, so it's easily readable
447
+
448
+ // if first arrow down
449
+ if (!isAddNewBtnActive && isDown && _optionsColl.activeIndex === undefined) {
450
+ return true;
451
+ }
452
+
453
+ // isActive and isUp (this is a noop, but we must break)
454
+ if (isAddNewBtnActive && isUp) {
455
+ return true;
456
+ }
457
+
458
+ // isUp from first, or is metaUp
459
+ if (!isAddNewBtnActive && isUp && (_optionsColl.activeIndex === 0 || isMeta)) {
460
+ return true;
461
+ }
462
+
463
+ return false;
351
464
  }
352
465
  </script>
353
466
 
@@ -359,10 +472,14 @@
359
472
  if (["ArrowDown", "ArrowUp"].includes(e.key)) {
360
473
  e.preventDefault();
361
474
 
362
- if (e.key === "ArrowUp") {
363
- e.metaKey ? _optionsColl.setActiveFirst() : _optionsColl.setActivePrevious();
364
- } else if (e.key === "ArrowDown") {
365
- e.metaKey ? _optionsColl.setActiveLast() : _optionsColl.setActiveNext();
475
+ isAddNewBtnActive = maybe_activate_add_new(e.key === "ArrowDown", e.metaKey);
476
+
477
+ if (!isAddNewBtnActive) {
478
+ if (e.key === "ArrowUp") {
479
+ e.metaKey ? _optionsColl.setActiveFirst() : _optionsColl.setActivePrevious();
480
+ } else if (e.key === "ArrowDown") {
481
+ e.metaKey ? _optionsColl.setActiveLast() : _optionsColl.setActiveNext();
482
+ }
366
483
  }
367
484
 
368
485
  // common UI convention: radios are selected by arrows
@@ -382,6 +499,7 @@
382
499
  <div>
383
500
  <FieldLikeButton
384
501
  bind:value
502
+ bind:input={parentHiddenInputEl}
385
503
  {name}
386
504
  class={classProp}
387
505
  {label}
@@ -437,7 +555,7 @@
437
555
  >
438
556
  <InputWrap
439
557
  size={renderSize}
440
- class={twMerge("m-4 mb-12 shadow-xl", classModalField)}
558
+ class={twMerge("m-2 mb-12 shadow-xl", classModalField)}
441
559
  classInputBoxWrap={twMerge(
442
560
  // always look like focused
443
561
  `border border-input-accent dark:border-input-accent-dark`,
@@ -480,6 +598,7 @@
480
598
  "hover:opacity-100 focus-visible:outline-neutral-400 focus-visible:opacity-100"
481
599
  )}
482
600
  tabindex={4}
601
+ disabled={!options.size}
483
602
  >
484
603
  {@html t("select_all")}
485
604
  </button>
@@ -512,80 +631,119 @@
512
631
  </div>
513
632
 
514
633
  <!-- {#if options.items.length} -->
515
- <ul
516
- class={twMerge(
517
- "options block h-[250px] max-h-[250px] overflow-y-auto overflow-x-hidden space-y-1"
518
- )}
634
+ <div
635
+ class={[
636
+ "options overflow-y-auto overflow-x-hidden space-y-1",
637
+ "h-[220px] max-h-[220px]",
638
+ ]}
519
639
  bind:this={optionsBox}
520
640
  tabindex="-1"
521
641
  >
522
642
  {#if isFetching && !options.items.length}
523
- <div class="p-4 opacity-50">
643
+ <!-- <div class="p-4 opacity-50"> -->
644
+ <div class="flex opacity-50 text-sm h-full items-center justify-center">
524
645
  <Spinner class="w-4" />
525
646
  </div>
647
+ {:else if !options.items.length && !allowUnknown}
648
+ <div class="flex opacity-50 text-sm h-full items-center justify-center">
649
+ {@html t("no_results")}
650
+ </div>
526
651
  {/if}
527
- {#each options.items as item}
528
- {@const active = item[itemIdPropName] === options.active?.[itemIdPropName]}
529
- {@const isSelected =
530
- selected.items && _selectedColl.exists(item[itemIdPropName])}
531
- <li class:active class="px-2">
652
+
653
+ {#if !isFetching && allowUnknown && innerValue && !have_option_label_like(options.items, innerValue)}
654
+ <div class="px-1">
532
655
  <button
533
656
  type="button"
534
- id={btnId(item[itemIdPropName])}
535
- onclick={() => {
536
- if (isMultiple) {
537
- if (selected.isFull && !_selectedColl.exists(item)) {
538
- return notifications?.error(t("cardinality_full"), {
539
- ttl: 1000,
540
- });
541
- }
542
- _selectedColl.toggleAdd(item);
543
- } else {
544
- _selectedColl.clear();
545
- _selectedColl.add(item);
546
- submit();
547
- }
548
- }}
549
- class:active
550
- class:selected={isSelected}
657
+ bind:this={addNewBtn}
658
+ onclick={add_new}
551
659
  class={twMerge(
552
- "no-focus-visible",
553
- "w-full text-left rounded-md py-2 px-2.5 flex items-center space-x-2",
554
- "text-ellipsis border border-transparent",
555
- "focus:outline-0 focus:border-neutral-400 dark:focus:border-neutral-500",
556
- "focus-visible:outline-0 focus-visible:ring-0",
557
- "hover:border-neutral-400 dark:hover:border-neutral-500",
558
- isSelected && "bg-neutral-200 dark:bg-neutral-800",
660
+ BTN_CLS,
559
661
  classOption,
560
- // active && "border-neutral-400",
561
- active && classOptionActive
662
+ isAddNewBtnActive && classOptionActive
562
663
  )}
563
- tabindex="-1"
564
- role="checkbox"
565
- aria-checked={isSelected}
566
664
  >
567
- {#if showIcons}
568
- <span class={isSelected ? "opacity-100" : "opacity-25"}>
569
- {#if isMultiple}
570
- {#if isSelected}
571
- {@html iconCheckboxCheck()}
572
- {:else}
573
- {@html iconCheckboxEmpty()}
574
- {/if}
575
- {:else if isSelected}
576
- {@html iconRadioCheck()}
577
- {:else}
578
- {@html iconRadioEmpty()}
579
- {/if}
580
- </span>
581
- {/if}
582
- <span>{_renderOptionLabel(item)}</span>
665
+ {t("add_new", { value: innerValue })}
583
666
  </button>
584
- </li>
667
+ </div>
668
+ {/if}
669
+
670
+ {#each _normalize_and_group_options(options.items) as [_optgroup, _opts]}
671
+ {#if _optgroup}
672
+ <div
673
+ class={twMerge(
674
+ "text-xs capitalize opacity-50 border-b border-black/10 mb-0.5 p-1 mx-1",
675
+ classOptgroup
676
+ )}
677
+ >
678
+ {_optgroup}
679
+ </div>
680
+ {/if}
681
+ <ul class="space-y-0.5">
682
+ <!-- {#each options.items as item} -->
683
+ {#each _opts as item (item[itemIdPropName])}
684
+ {@const active =
685
+ item[itemIdPropName] === options.active?.[itemIdPropName]}
686
+ {@const isSelected =
687
+ selected.items && _selectedColl.exists(item[itemIdPropName])}
688
+ <li class:active class="px-1">
689
+ <button
690
+ type="button"
691
+ id={btn_id(item[itemIdPropName])}
692
+ onclick={() => {
693
+ if (isMultiple) {
694
+ if (selected.isFull && !_selectedColl.exists(item)) {
695
+ return notifications?.error(t("cardinality_full"), {
696
+ ttl: 1000,
697
+ });
698
+ }
699
+ _selectedColl.toggleAdd(item);
700
+ } else {
701
+ _selectedColl.clear();
702
+ _selectedColl.add(item);
703
+ submit();
704
+ }
705
+ }}
706
+ class:active
707
+ class:selected={isSelected}
708
+ class={twMerge(
709
+ BTN_CLS,
710
+ isSelected && "bg-neutral-200 dark:bg-neutral-800",
711
+ classOption,
712
+ // active && "border-neutral-400",
713
+ active && classOptionActive
714
+ )}
715
+ tabindex="-1"
716
+ role="checkbox"
717
+ aria-checked={isSelected}
718
+ >
719
+ {#if showIcons}
720
+ <span class={isSelected ? "opacity-100" : "opacity-25"}>
721
+ {#if isMultiple}
722
+ {#if isSelected}
723
+ {@html iconCheckboxCheck()}
724
+ {:else}
725
+ {@html iconCheckboxEmpty()}
726
+ {/if}
727
+ {:else if isSelected}
728
+ {@html iconRadioCheck()}
729
+ {:else}
730
+ {@html iconRadioEmpty()}
731
+ {/if}
732
+ </span>
733
+ {/if}
734
+ <span
735
+ class={twMerge(
736
+ "min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap"
737
+ )}>{_renderOptionLabel(item)}</span
738
+ >
739
+ </button>
740
+ </li>
741
+ {/each}
742
+ </ul>
585
743
  {/each}
586
- </ul>
744
+ </div>
587
745
  <!-- {/if} -->
588
- <div class="p-2 flex items-end justify-between">
746
+ <div class="p-2 px-3 flex items-end justify-between">
589
747
  <div class="text-xs opacity-50">
590
748
  <!-- Use arrows to navigate. Spacebar and Enter to select and/or submit. -->
591
749
  {#if allowUnknown}
@@ -625,7 +783,7 @@
625
783
  "opacity-50 rounded",
626
784
  "hover:opacity-100 hover:bg-neutral-200 dark:hover:bg-neutral-800",
627
785
  "focus-visible:opacity-100 focus-visible:outline-0",
628
- " focus-visible:bg-neutral-200 dark:focus-visible:bg-neutral-800"
786
+ "focus-visible:bg-neutral-200 dark:focus-visible:bg-neutral-800"
629
787
  )}
630
788
  use:tooltip
631
789
  aria-label={t("x_close")}
@@ -1,12 +1,13 @@
1
- export interface Option {
2
- label: string;
3
- value: any;
4
- }
5
- import { type Item } from "@marianmeres/item-collection";
1
+ import { ItemCollection, type Item } from "@marianmeres/item-collection";
6
2
  import { type Snippet } from "svelte";
7
3
  import { type ValidateOptions } from "../../actions/validate.svelte.js";
8
4
  import { NotificationsStack } from "../Notifications/index.js";
9
5
  import type { THC } from "../Thc/Thc.svelte";
6
+ import type { TranslateFn } from "../../types.js";
7
+ export interface Option {
8
+ label: string;
9
+ value: any;
10
+ }
10
11
  type SnippetWithId = Snippet<[{
11
12
  id: string;
12
13
  }]>;
@@ -38,17 +39,23 @@ interface Props extends Record<string, any> {
38
39
  classBelowBox?: string;
39
40
  classOption?: string;
40
41
  classOptionActive?: string;
42
+ classOptgroup?: string;
41
43
  classModalField?: string;
42
44
  noScrollLock?: boolean;
43
45
  style?: string;
44
- t?: (key: string) => string;
46
+ t?: TranslateFn;
45
47
  renderValue?: (strigifiedItems: string) => string;
46
- getOptions: (s: string, current: Item[]) => Promise<Item[]>;
48
+ getOptions: (q: string, current: Item[]) => Promise<{
49
+ coll?: ItemCollection<Item>;
50
+ found: Item[];
51
+ }>;
47
52
  notifications?: NotificationsStack;
48
53
  cardinality?: number;
49
54
  renderOptionLabel?: (item: Item) => string;
55
+ renderOptionGroup?: (s: string) => string;
50
56
  allowUnknown?: boolean;
51
- showIcons?: boolean;
57
+ showIconsCheckbox?: boolean;
58
+ showIconsRadio?: boolean;
52
59
  searchPlaceholder?: string;
53
60
  name: string;
54
61
  itemIdPropName?: string;
@@ -112,9 +112,10 @@
112
112
  class={twMerge(
113
113
  "stuic-input",
114
114
  _classCommon,
115
- "mb-8 grid",
116
- hasLabel && labelLeft && labelLeftWidth === "normal" && "width-normal grid-cols-4",
117
- hasLabel && labelLeft && labelLeftWidth === "wide" && "width-wide grid-cols-3",
115
+ "mb-8",
116
+ hasLabel && labelLeft && "flex",
117
+ hasLabel && labelLeft && labelLeftWidth === "normal" && "width-normal",
118
+ hasLabel && labelLeft && labelLeftWidth === "wide" && "width-wide",
118
119
  classProp
119
120
  )}
120
121
  bind:clientWidth={width}
@@ -124,7 +125,7 @@
124
125
  class={twMerge(
125
126
  "label-box",
126
127
  _classCommon,
127
- "flex",
128
+ "flex flex-1",
128
129
  labelLeft ? "left items-start mt-2" : "items-end",
129
130
  classLabelBox
130
131
  )}
@@ -152,8 +153,8 @@
152
153
  class={twMerge(
153
154
  "input-box",
154
155
  _classCommon,
155
- hasLabel && labelLeft && labelLeftWidth === "normal" && "col-span-3",
156
- hasLabel && labelLeft && labelLeftWidth === "wide" && "col-span-2",
156
+ hasLabel && labelLeft && labelLeftWidth === "normal" && "flex-3",
157
+ hasLabel && labelLeft && labelLeftWidth === "wide" && "flex-2",
157
158
  classInputBox
158
159
  )}
159
160
  >