@marianmeres/stuic 2.61.0 → 2.63.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.
@@ -3,7 +3,7 @@
3
3
  import { iconSearch, iconCheck, iconCircle, iconSquare } from "../../icons/index.js";
4
4
  import { ItemCollection, type Item } from "@marianmeres/item-collection";
5
5
  import { Debounced, watch } from "runed";
6
- import { tick, type Snippet } from "svelte";
6
+ import { onDestroy, tick, type Snippet } from "svelte";
7
7
  import { tooltip } from "../../actions/index.js";
8
8
  import { type ValidateOptions } from "../../actions/validate.svelte.js";
9
9
  import type { TranslateFn } from "../../types.js";
@@ -16,7 +16,7 @@
16
16
  import { strHash } from "../../utils/str-hash.js";
17
17
  import { twMerge } from "../../utils/tw-merge.js";
18
18
  import Button from "../Button/Button.svelte";
19
- import Modal from "../Modal/Modal.svelte";
19
+ import { ModalDialog } from "../ModalDialog/index.js";
20
20
  import { NotificationsStack } from "../Notifications/index.js";
21
21
  import Spinner from "../Spinner/Spinner.svelte";
22
22
  import type { THC } from "../Thc/Thc.svelte";
@@ -32,7 +32,7 @@
32
32
  type SnippetWithId = Snippet<[{ id: string }]>;
33
33
 
34
34
  export interface Props extends Record<string, any> {
35
- trigger?: Snippet<[{ value: string; modal: Modal }]>;
35
+ trigger?: Snippet<[{ value: string; modal: ModalDialog }]>;
36
36
  input?: HTMLInputElement;
37
37
  value: string;
38
38
  label?: SnippetWithId | THC;
@@ -65,11 +65,8 @@
65
65
  noScrollLock?: boolean;
66
66
  style?: string;
67
67
  t?: TranslateFn;
68
- renderValue?: (strigifiedItems: string) => string;
69
- getOptions: (
70
- q: string,
71
- current: Item[]
72
- ) => Promise<{ coll?: ItemCollection<Item>; found: Item[] }>;
68
+ renderValue?: (stringifiedItems: string) => string;
69
+ getOptions: (q: string, current: Item[]) => Promise<{ found: Item[] }>;
73
70
  notifications?: NotificationsStack;
74
71
  cardinality?: number;
75
72
  renderOptionLabel?: (item: Item) => string;
@@ -134,7 +131,7 @@
134
131
  tabindex = 0,
135
132
  description,
136
133
  class: classProp,
137
- renderSize = "md",
134
+ renderSize = "lg",
138
135
  useTrim = true,
139
136
  //
140
137
  required = false,
@@ -183,9 +180,13 @@
183
180
  ...rest
184
181
  }: Props = $props();
185
182
 
186
- let modal: Modal = $state()!;
183
+ let modalDialog: ModalDialog = $state()!;
187
184
  let innerValue = $state("");
188
185
  let isFetching = $state(false);
186
+ let isUnmounted = false;
187
+ onDestroy(() => {
188
+ isUnmounted = true;
189
+ });
189
190
  let cardinality = $derived(_cardinality === -1 ? Infinity : _cardinality);
190
191
  let isMultiple = $derived(cardinality > 1);
191
192
  let showIcons = $derived(isMultiple ? showIconsCheckbox : showIconsRadio);
@@ -246,6 +247,11 @@
246
247
  // reconfigure if the prop ever changes during runtime (most likely will NOT)
247
248
  $effect(() => {
248
249
  _selectedColl.configure({ cardinality });
250
+ // trim excess selections if cardinality was reduced
251
+ if (_selectedColl.size > cardinality) {
252
+ const trimmed = _selectedColl.items.slice(0, cardinality);
253
+ _selectedColl.clear().addMany(trimmed);
254
+ }
249
255
  });
250
256
 
251
257
  // now, create the reactive, subscribed variants
@@ -259,54 +265,32 @@
259
265
  );
260
266
  }
261
267
 
262
- // $inspect("options", options);
263
- // $inspect("selected", selected);
264
- // $inspect("lastQuery", lastQuery, innerValue);
265
-
266
268
  // hidden input which holds the final value (upon which validation happens)
267
269
  let parentHiddenInputEl: HTMLInputElement | undefined = $state();
268
270
 
269
271
  let activeEl: HTMLButtonElement | undefined = $state();
270
272
  let optionsBox: HTMLDivElement | undefined = $state();
271
- let modalEl: HTMLDivElement | undefined = $state();
272
273
 
273
274
  // add_new dance...
274
275
  let addNewBtn: HTMLButtonElement | undefined = $state();
275
276
  let isAddNewBtnActive = $state(false);
276
277
  let touch = $state(new Date());
277
278
 
278
- // set value on open
279
- // watch(
280
- // () => modal.visibility().visible,
281
- // (isVisible, wasVisible) => {
282
- // // modal was just opened
283
- // if (isVisible) {
284
- // _selectedColl.clear().addMany(maybeJsonParse(value));
285
- // console.log(_selectedColl.dump());
286
- // // IMPORTANT: focus first selected so it scrolls into view on open
287
- // if (_selectedColl.size) {
288
- // console.log(1111);
289
- // waitForNextRepaint().then(() => {
290
- // _optionsColl.setActive(_selectedColl.items[0]);
291
- // waitForNextRepaint().then(() => {
292
- // scrollIntoViewTrigger = new Date();
293
- // });
294
- // });
295
- // }
296
- // }
297
- // }
298
- // );
299
-
300
279
  // suggest options as a typeahead feature
301
280
  const debounced = new Debounced(() => innerValue, 150);
281
+ let fetchRequestId = 0;
302
282
  watch(
303
- [() => modal.visibility().visible, () => debounced.current],
283
+ [() => modalDialog.visibility().visible, () => debounced.current],
304
284
  ([isVisible, currVal]) => {
305
285
  if (!isVisible) return;
306
286
  isFetching = true;
287
+ const currentRequest = ++fetchRequestId;
307
288
  getOptions(currVal, selected.items)
308
289
  .then((res) => {
309
- const { found, coll } = res;
290
+ // ignore stale responses
291
+ if (currentRequest !== fetchRequestId) return;
292
+
293
+ const { found } = res;
310
294
 
311
295
  // continue normally, with (server) provided options...
312
296
  _optionsColl.clear().addMany(found);
@@ -325,12 +309,12 @@
325
309
  );
326
310
 
327
311
  $effect(() => {
328
- if (modal.visibility().visible && touch) {
312
+ if (modalDialog.visibility().visible && touch) {
329
313
  _selectedColl.clear().addMany(maybeJsonParse(value) as Item[]);
330
314
  // IMPORTANT: focus first selected so it scrolls into view on open
331
315
  if (_selectedColl.size) {
332
316
  waitForNextRepaint().then(() => {
333
- _optionsColl.setActive(_selectedColl.items[0]);
317
+ if (!isUnmounted) _optionsColl.setActive(_selectedColl.items[0]);
334
318
  });
335
319
  }
336
320
  }
@@ -409,16 +393,15 @@
409
393
  value = JSON.stringify(selected.items);
410
394
  innerValue = "";
411
395
  _optionsColl.clear();
412
- modal.close();
396
+ modalDialog.close();
413
397
  _dispatch_change_to_owner();
414
398
  onChange?.(value);
415
399
  }
416
400
 
417
- // clears, closes, submits nothing
401
+ // clears state and dispatches change; close is handled by ModalDialog's preEscapeClose
418
402
  function escape() {
419
403
  innerValue = "";
420
404
  _optionsColl.clear();
421
- modal?.close();
422
405
  _dispatch_change_to_owner();
423
406
  }
424
407
 
@@ -433,6 +416,8 @@
433
416
  return groupped;
434
417
  }
435
418
 
419
+ let groupedOptions = $derived(_normalize_and_group_options(options.items));
420
+
436
421
  const BTN_CLS = [
437
422
  "no-focus-visible",
438
423
  "text-left rounded-md py-2 px-2.5 flex items-center space-x-2",
@@ -483,7 +468,7 @@
483
468
  <!-- this must be on window as we're catching any typing anywhere -->
484
469
  <svelte:window
485
470
  onkeydown={(e) => {
486
- if (modal.visibility().visible) {
471
+ if (modalDialog.visibility().visible) {
487
472
  // arrow navigation
488
473
  if (["ArrowDown", "ArrowUp"].includes(e.key)) {
489
474
  e.preventDefault();
@@ -514,7 +499,7 @@
514
499
  <!-- must wrap both -->
515
500
  <div>
516
501
  {#if trigger}
517
- {@render trigger({ value, modal })}
502
+ {@render trigger({ value, modal: modalDialog })}
518
503
  {:else}
519
504
  <FieldLikeButton
520
505
  bind:value
@@ -553,7 +538,7 @@
553
538
  let extra = '';
554
539
  if (vals.length > limit) {
555
540
  vals = vals.slice(0, limit);
556
- extra = `, ... <span class="text-sm opacity-50">(+${(origLength - limit)})</span>`;
541
+ extra = `, ... <span class="text-sm opacity-75">(+${(origLength - limit)})</span>`;
557
542
  }
558
543
  return vals.filter(v => v != null).map(_renderOptionLabel).join(", ") + extra;
559
544
  } catch (e) {
@@ -561,274 +546,282 @@
561
546
  return `${e}`; // either invalid json or not array...
562
547
  }
563
548
  }}
564
- onclick={modal?.open}
549
+ onclick={modalDialog?.open}
565
550
  />
566
551
  {/if}
567
552
 
568
- <Modal
569
- bind:this={modal}
570
- onEscape={escape}
571
- class="bg-transparent dark:bg-transparent"
572
- classInner="max-w-2xl"
573
- bind:el={modalEl}
553
+ <ModalDialog
554
+ bind:this={modalDialog}
555
+ preEscapeClose={escape}
556
+ classDialog="items-start"
557
+ class="w-full max-w-2xl bg-transparent pointer-events-none"
558
+ ariaLabelledby={id}
574
559
  {noScrollLock}
575
560
  >
576
- <InputWrap
577
- size={renderSize}
578
- class={twMerge("m-2 mb-12 shadow-xl", classModalField)}
579
- classInputBoxWrap={twMerge(
580
- // always look like focused
581
- `border border-input-accent dark:border-input-accent-dark`,
582
- `ring-input-accent/20 dark:ring-input-accent-dark/20 ring-4`
583
- )}
584
- {id}
585
- {required}
586
- >
587
- <input
588
- bind:value={innerValue}
589
- bind:this={input}
590
- {type}
591
- {id}
592
- class={twMerge("form-input", renderSize, classInput)}
593
- tabindex={1}
594
- {required}
595
- {disabled}
596
- placeholder={searchPlaceholder ??
597
- t(allowUnknown ? "search_submit_placeholder" : "search_placeholder")}
598
- onkeydown={(e) => {
599
- if (e.key === "Enter") {
600
- e.preventDefault();
601
- try_submit();
602
- }
603
- }}
604
- autocomplete="off"
605
- name={`rand-${Math.random().toString(36).slice(2)}`}
606
- {...rest}
607
- />
608
-
609
- {#snippet inputBelow()}
610
- <div class="h-full border-t p-2 border-black/20">
611
- <div class="text-sm -mt-1 flex items-center">
612
- {#if isMultiple}
613
- <button
614
- type="button"
615
- onclick={() => _selectedColl.addMany(options.items)}
616
- class={twMerge(
617
- "control flex items-center p-1 m-1 text-sm opacity-75 underline rounded",
618
- "hover:opacity-100 focus-visible:outline-neutral-400 focus-visible:opacity-100"
619
- )}
620
- tabindex={4}
621
- disabled={!options.size}
622
- >
623
- {@html t("select_all")}
624
- </button>
625
- {/if}
626
- <button
627
- type="button"
628
- onclick={() => {
629
- _selectedColl.clear();
630
- input?.focus();
631
- }}
632
- class={twMerge(
633
- "control flex items-center p-1 m-1 text-sm opacity-75 underline rounded",
634
- "hover:opacity-100 focus-visible:outline-neutral-400 focus-visible:opacity-100"
635
- )}
636
- class:opacity-20={!selected.items.length}
637
- tabindex={5}
638
- disabled={!selected.items.length}
639
- >
640
- {@html t(cardinality === 1 ? "clear" : "clear_all")}
641
- </button>
642
-
643
- <span class="p-1 m-1 text-sm">&nbsp;</span>
644
- <span class="flex-1 block justify-end opacity-50 text-right text-sm p-1 pr-2">
645
- {selected.items.length}
646
- {#if cardinality > 0 && cardinality < Infinity}
647
- {@html t("cardinality_of")} {cardinality}
648
- {/if}
649
- {@html t("cardinality_selected")}
650
- </span>
651
- </div>
652
-
653
- <!-- {#if options.items.length} -->
654
- <div
655
- class={[
656
- "options overflow-y-auto overflow-x-hidden space-y-1 scrollbar-thin",
657
- "h-55 max-h-55",
658
- ]}
659
- bind:this={optionsBox}
660
- tabindex="-1"
661
- >
662
- {#if isFetching && !options.items.length}
663
- <!-- <div class="p-4 opacity-50"> -->
664
- <div class="flex opacity-50 text-sm h-full items-center justify-center">
665
- <Spinner class="w-4" />
666
- </div>
667
- {:else if !options.items.length && !allowUnknown}
668
- <div class="flex opacity-50 text-sm h-full items-center justify-center">
669
- {@html t("no_results")}
670
- </div>
671
- {/if}
672
-
673
- {#if !isFetching && allowUnknown && innerValue && !have_option_label_like(options.items, innerValue)}
674
- <div class="px-1">
561
+ <div class="pt-0 md:pt-[20vh] w-full">
562
+ <div class="pointer-events-auto">
563
+ <InputWrap
564
+ size={renderSize}
565
+ class={twMerge("m-2 mb-12 shadow-xl", classModalField)}
566
+ classInputBoxWrap={twMerge(
567
+ // always look like focused
568
+ `border border-input-accent dark:border-input-accent-dark`,
569
+ `ring-input-accent/20 dark:ring-input-accent-dark/20 ring-4`
570
+ )}
571
+ {id}
572
+ {required}
573
+ >
574
+ <input
575
+ bind:value={innerValue}
576
+ bind:this={input}
577
+ {type}
578
+ {id}
579
+ class={twMerge("form-input", renderSize, classInput)}
580
+ tabindex={1}
581
+ {required}
582
+ {disabled}
583
+ placeholder={searchPlaceholder ??
584
+ t(allowUnknown ? "search_submit_placeholder" : "search_placeholder")}
585
+ onkeydown={(e) => {
586
+ if (e.key === "Enter") {
587
+ e.preventDefault();
588
+ try_submit();
589
+ }
590
+ }}
591
+ autocomplete="off"
592
+ aria-controls={`${id}-options`}
593
+ name={`field-${id}`}
594
+ {...rest}
595
+ />
596
+
597
+ {#snippet inputBelow()}
598
+ <div class="h-full border-t p-2 border-black/20">
599
+ <div class="text-sm -mt-1 flex items-center">
600
+ {#if isMultiple}
601
+ <button
602
+ type="button"
603
+ onclick={() => _selectedColl.addMany(options.items)}
604
+ class={twMerge(
605
+ "control flex items-center p-1 m-1 text-sm opacity-75 underline rounded",
606
+ "hover:opacity-100 focus-visible:outline-neutral-400 focus-visible:opacity-100"
607
+ )}
608
+ tabindex={4}
609
+ disabled={!options.size}
610
+ >
611
+ {@html t("select_all")}
612
+ </button>
613
+ {/if}
675
614
  <button
676
615
  type="button"
677
- bind:this={addNewBtn}
678
- onclick={add_new}
616
+ onclick={() => {
617
+ _selectedColl.clear();
618
+ input?.focus();
619
+ }}
679
620
  class={twMerge(
680
- BTN_CLS,
681
- classOption,
682
- isAddNewBtnActive && classOptionActive
621
+ "control flex items-center p-1 m-1 text-sm opacity-75 underline rounded",
622
+ "hover:opacity-100 focus-visible:outline-neutral-400 focus-visible:opacity-100"
683
623
  )}
624
+ class:opacity-20={!selected.items.length}
625
+ tabindex={5}
626
+ disabled={!selected.items.length}
684
627
  >
685
- {t("add_new", { value: innerValue })}
628
+ {@html t(cardinality === 1 ? "clear" : "clear_all")}
686
629
  </button>
687
- </div>
688
- {/if}
689
630
 
690
- {#each _normalize_and_group_options(options.items) as [_optgroup, _opts]}
691
- {#if _optgroup}
692
- <div
693
- class={twMerge(
694
- "text-sm capitalize opacity-50 border-b border-black/10 mb-0.5 p-1 mx-1",
695
- classOptgroup
696
- )}
631
+ <span class="p-1 m-1 text-sm">&nbsp;</span>
632
+ <span
633
+ class="flex-1 block justify-end opacity-75 text-right text-xs p-1 pr-2"
697
634
  >
698
- {_optgroup}
699
- </div>
700
- {/if}
701
- <ul class="space-y-0.5">
702
- <!-- {#each options.items as item} -->
703
- {#each _opts as item (item[itemIdPropName])}
704
- {@const active =
705
- item[itemIdPropName] === options.active?.[itemIdPropName]}
706
- {@const isSelected =
707
- selected.items && _selectedColl.exists(item[itemIdPropName])}
708
- <li class:active class="px-1">
635
+ {selected.items.length}
636
+ {#if cardinality > 0 && cardinality < Infinity}
637
+ {@html t("cardinality_of")} {cardinality}
638
+ {/if}
639
+ {@html t("cardinality_selected")}
640
+ </span>
641
+ </div>
642
+
643
+ <!-- {#if options.items.length} -->
644
+ <div
645
+ id={`${id}-options`}
646
+ class={[
647
+ "options overflow-y-auto overflow-x-hidden space-y-1 scrollbar-thin",
648
+ "h-55 max-h-55",
649
+ ]}
650
+ bind:this={optionsBox}
651
+ tabindex="-1"
652
+ >
653
+ {#if isFetching && !options.items.length}
654
+ <div class="flex opacity-50 text-sm h-full items-center justify-center">
655
+ <Spinner class="w-4" />
656
+ </div>
657
+ {:else if !options.items.length && !allowUnknown}
658
+ <div class="flex opacity-50 text-sm h-full items-center justify-center">
659
+ {@html t("no_results")}
660
+ </div>
661
+ {/if}
662
+
663
+ {#if !isFetching && allowUnknown && innerValue && !have_option_label_like(options.items, innerValue)}
664
+ <div class="px-1">
709
665
  <button
710
666
  type="button"
711
- id={btn_id(item[itemIdPropName])}
712
- onclick={() => {
713
- if (isMultiple) {
714
- if (selected.isFull && !_selectedColl.exists(item)) {
715
- return notifications?.error(t("cardinality_full"), {
716
- ttl: 1000,
717
- });
718
- }
719
- _selectedColl.toggleAdd(item);
720
- } else {
721
- _selectedColl.clear();
722
- _selectedColl.add(item);
723
- submit();
724
- }
725
- }}
726
- class:active
727
- class:selected={isSelected}
667
+ bind:this={addNewBtn}
668
+ onclick={add_new}
728
669
  class={twMerge(
729
670
  BTN_CLS,
730
- isSelected && "bg-neutral-200 dark:bg-neutral-800",
731
671
  classOption,
732
- // active && "border-neutral-400",
733
- active && classOptionActive
672
+ isAddNewBtnActive && classOptionActive
734
673
  )}
735
- tabindex="-1"
736
- role="checkbox"
737
- aria-checked={isSelected}
738
674
  >
739
- {#if showIcons}
740
- <span class={isSelected ? "opacity-100" : "opacity-25"}>
741
- {#if isMultiple}
742
- {#if isSelected}
743
- {@html iconCheckboxCheck()}
744
- {:else}
745
- {@html iconCheckboxEmpty()}
746
- {/if}
747
- {:else if isSelected}
748
- {@html iconRadioCheck()}
749
- {:else}
750
- {@html iconRadioEmpty()}
751
- {/if}
752
- </span>
753
- {/if}
754
- <span
755
- class={twMerge(
756
- "min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap"
757
- )}>{_renderOptionLabel(item)}</span
758
- >
675
+ {t("add_new", { value: innerValue })}
759
676
  </button>
760
- </li>
677
+ </div>
678
+ {/if}
679
+
680
+ {#each groupedOptions as [_optgroup, _opts]}
681
+ {#if _optgroup}
682
+ <div
683
+ class={twMerge(
684
+ "text-sm capitalize opacity-50 border-b border-black/10 mb-0.5 p-1 mx-1",
685
+ classOptgroup
686
+ )}
687
+ >
688
+ {_optgroup}
689
+ </div>
690
+ {/if}
691
+ <ul class="space-y-0.5">
692
+ <!-- {#each options.items as item} -->
693
+ {#each _opts as item (item[itemIdPropName])}
694
+ {@const active =
695
+ item[itemIdPropName] === options.active?.[itemIdPropName]}
696
+ {@const isSelected =
697
+ selected.items && _selectedColl.exists(item[itemIdPropName])}
698
+ <li class:active class="px-1">
699
+ <button
700
+ type="button"
701
+ id={btn_id(item[itemIdPropName])}
702
+ onclick={() => {
703
+ if (isMultiple) {
704
+ if (selected.isFull && !_selectedColl.exists(item)) {
705
+ return notifications?.error(t("cardinality_full"), {
706
+ ttl: 1000,
707
+ });
708
+ }
709
+ _selectedColl.toggleAdd(item);
710
+ } else {
711
+ _selectedColl.clear();
712
+ _selectedColl.add(item);
713
+ submit();
714
+ }
715
+ }}
716
+ class:active
717
+ class:selected={isSelected}
718
+ class={twMerge(
719
+ BTN_CLS,
720
+ isSelected && "bg-neutral-200 dark:bg-neutral-800",
721
+ classOption,
722
+ // active && "border-neutral-400",
723
+ active && classOptionActive
724
+ )}
725
+ tabindex="-1"
726
+ role={isMultiple ? "checkbox" : "radio"}
727
+ aria-checked={isSelected}
728
+ >
729
+ {#if showIcons}
730
+ <span class={isSelected ? "opacity-100" : "opacity-25"}>
731
+ {#if isMultiple}
732
+ {#if isSelected}
733
+ {@html iconCheckboxCheck()}
734
+ {:else}
735
+ {@html iconCheckboxEmpty()}
736
+ {/if}
737
+ {:else if isSelected}
738
+ {@html iconRadioCheck()}
739
+ {:else}
740
+ {@html iconRadioEmpty()}
741
+ {/if}
742
+ </span>
743
+ {/if}
744
+ <span
745
+ class={twMerge(
746
+ "min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap"
747
+ )}>{_renderOptionLabel(item)}</span
748
+ >
749
+ </button>
750
+ </li>
751
+ {/each}
752
+ </ul>
761
753
  {/each}
762
- </ul>
763
- {/each}
764
- </div>
765
- <!-- {/if} -->
766
- <div class="p-2 px-3 flex items-end justify-between">
767
- <div class="text-sm opacity-50">
768
- <!-- Use arrows to navigate. Spacebar and Enter to select and/or submit. -->
769
- {#if allowUnknown}
770
- {@html t("unknown_allowed")}
771
- {:else}
772
- {@html t("unknown_not_allowed")}
754
+ </div>
755
+ <!-- {/if} -->
756
+ <div class="p-2 px-3 flex items-end justify-between">
757
+ <div class="text-xs opacity-75">
758
+ <!-- Use arrows to navigate. Spacebar and Enter to select and/or submit. -->
759
+ {#if allowUnknown}
760
+ {@html t("unknown_allowed")}
761
+ {:else}
762
+ {@html t("unknown_not_allowed")}
763
+ {/if}
764
+ </div>
765
+ <div>
766
+ <Button
767
+ class="control"
768
+ type="button"
769
+ variant="primary"
770
+ onclick={async (e) => {
771
+ e.preventDefault();
772
+ try_submit(true);
773
+ }}
774
+ tabindex={3}
775
+ >
776
+ {@html t("submit")}
777
+ </Button>
778
+ </div>
779
+ </div>
780
+ </div>
781
+ {/snippet}
782
+
783
+ {#snippet inputAfter()}
784
+ <div class="flex pl-2 items-center justify-center opacity-50">
785
+ {#if isFetching}
786
+ <Spinner class="w-4" />
773
787
  {/if}
774
788
  </div>
775
- <div>
776
- <Button
777
- class="control"
789
+ <div class="flex pl-2 pr-1 items-center justify-center">
790
+ <button
778
791
  type="button"
779
- variant="primary"
780
- onclick={async (e) => {
792
+ class={twMerge(
793
+ "opacity-75 rounded",
794
+ "hover:opacity-100 hover:bg-neutral-200 dark:hover:bg-neutral-800",
795
+ "focus-visible:opacity-100 focus-visible:outline-0",
796
+ "focus-visible:bg-neutral-200 dark:focus-visible:bg-neutral-800"
797
+ )}
798
+ use:tooltip
799
+ aria-label={t("x_close")}
800
+ onclick={(e) => {
781
801
  e.preventDefault();
782
- try_submit(true);
802
+ if (innerValue.trim() == "") {
803
+ escape();
804
+ return modalDialog.close();
805
+ }
806
+ innerValue = "";
807
+ input?.focus();
783
808
  }}
784
- tabindex={3}
809
+ tabindex={2}
785
810
  >
786
- {@html t("submit")}
787
- </Button>
811
+ <X class="m-2 size-6" />
812
+ </button>
788
813
  </div>
789
- </div>
790
- </div>
791
- {/snippet}
792
-
793
- {#snippet inputAfter()}
794
- <div class="flex pl-2 items-center justify-center opacity-50">
795
- {#if isFetching}
796
- <Spinner class="w-4" />
797
- {/if}
798
- </div>
799
- <div class="flex pl-2 pr-1 items-center justify-center">
800
- <button
801
- type="button"
802
- class={twMerge(
803
- "opacity-50 rounded",
804
- "hover:opacity-100 hover:bg-neutral-200 dark:hover:bg-neutral-800",
805
- "focus-visible:opacity-100 focus-visible:outline-0",
806
- "focus-visible:bg-neutral-200 dark:focus-visible:bg-neutral-800"
807
- )}
808
- use:tooltip
809
- aria-label={t("x_close")}
810
- onclick={(e) => {
811
- e.preventDefault();
812
- if (innerValue.trim() == "") {
813
- return escape();
814
- }
815
- innerValue = "";
816
- input?.focus();
817
- }}
818
- tabindex={2}
819
- >
820
- <X class="m-2 size-4 " />
821
- </button>
822
- </div>
823
- {/snippet}
824
-
825
- {#snippet inputBefore()}
826
- <div class="flex flex-col items-center justify-center pl-3 opacity-50">
827
- {@html iconSearch({ size: 14 })}
828
- </div>
829
- {/snippet}
830
- </InputWrap>
831
- </Modal>
814
+ {/snippet}
815
+
816
+ {#snippet inputBefore()}
817
+ <div class="flex flex-col items-center justify-center pl-3 opacity-75">
818
+ {@html iconSearch({ size: 19, strokeWidth: 3 })}
819
+ </div>
820
+ {/snippet}
821
+ </InputWrap>
822
+ </div>
823
+ </div>
824
+ </ModalDialog>
832
825
  </div>
833
826
 
834
827
  <style>