fuma 2.1.6 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/_doc/data.d.ts +3 -1
  2. package/dist/_doc/data.js +6 -2
  3. package/dist/command/index.d.ts +1 -1
  4. package/dist/command/index.js +1 -1
  5. package/dist/command/{command.svelte.js → useCommand.svelte.js} +1 -1
  6. package/dist/input/InputBoolean.svelte +41 -8
  7. package/dist/input/InputBoolean.svelte.d.ts +7 -0
  8. package/dist/input/InputCheckboxes.svelte +123 -0
  9. package/dist/input/InputCheckboxes.svelte.d.ts +19 -0
  10. package/dist/input/InputDateTime.svelte +199 -0
  11. package/dist/input/InputDateTime.svelte.d.ts +26 -0
  12. package/dist/input/InputMultiSelect.svelte +324 -0
  13. package/dist/input/{InputRelation.svelte.d.ts → InputMultiSelect.svelte.d.ts} +32 -13
  14. package/dist/input/InputNumber.svelte +43 -6
  15. package/dist/input/InputNumber.svelte.d.ts +8 -0
  16. package/dist/input/InputRadio.svelte +62 -24
  17. package/dist/input/InputRadio.svelte.d.ts +3 -0
  18. package/dist/input/InputRange.svelte +24 -4
  19. package/dist/input/InputRange.svelte.d.ts +9 -0
  20. package/dist/input/InputSelect.svelte +216 -62
  21. package/dist/input/InputSelect.svelte.d.ts +91 -6
  22. package/dist/input/InputSelectNative.svelte +51 -19
  23. package/dist/input/InputSelectNative.svelte.d.ts +8 -0
  24. package/dist/input/InputString.svelte +21 -6
  25. package/dist/input/InputString.svelte.d.ts +7 -0
  26. package/dist/input/InputTextarea.svelte +58 -11
  27. package/dist/input/InputTextarea.svelte.d.ts +14 -0
  28. package/dist/input/index.d.ts +4 -1
  29. package/dist/input/index.js +4 -1
  30. package/dist/input/useItems.svelte.d.ts +38 -0
  31. package/dist/input/useItems.svelte.js +142 -0
  32. package/dist/loading/Loading.svelte +3 -2
  33. package/dist/loading/Loading.svelte.d.ts +3 -2
  34. package/dist/popover/Popover.svelte +1 -2
  35. package/dist/popover/Popover.svelte.d.ts +13 -0
  36. package/dist/popover/index.d.ts +1 -1
  37. package/dist/popover/index.js +1 -1
  38. package/dist/popover/{popover.svelte.d.ts → usePopover.svelte.d.ts} +2 -1
  39. package/dist/popover/usePopover.svelte.js +145 -0
  40. package/dist/remote/useForm.d.ts +4 -5
  41. package/dist/remote/useForm.js +9 -4
  42. package/dist/state/index.d.ts +1 -1
  43. package/dist/state/index.js +1 -1
  44. package/dist/ui/button/ButtonDelete.svelte +6 -3
  45. package/dist/ui/button/ButtonDelete.svelte.d.ts +1 -1
  46. package/dist/ui/dialog/Dialog.svelte +5 -5
  47. package/dist/ui/drawer/Drawer.svelte +74 -26
  48. package/dist/ui/drawer/Drawer.svelte.d.ts +10 -4
  49. package/dist/ui/drawer/index.d.ts +1 -0
  50. package/dist/ui/drawer/index.js +1 -0
  51. package/dist/ui/drawer/inertBackground.d.ts +13 -0
  52. package/dist/ui/drawer/inertBackground.js +59 -0
  53. package/dist/ui/drawer/useLayer.svelte.d.ts +1 -1
  54. package/dist/ui/drawer/useLayer.svelte.js +11 -7
  55. package/dist/ui/index.d.ts +1 -0
  56. package/dist/ui/index.js +1 -0
  57. package/dist/ui/menu/ContextMenu.svelte +30 -17
  58. package/dist/ui/menu/ContextMenu.svelte.d.ts +2 -3
  59. package/dist/ui/menu/DropDown.svelte +1 -1
  60. package/dist/ui/menu/DropDown.svelte.d.ts +1 -1
  61. package/dist/ui/menu/DropDownMenu.svelte +12 -10
  62. package/dist/ui/menu/DropDownMenu.svelte.d.ts +2 -3
  63. package/dist/ui/menu/index.d.ts +0 -1
  64. package/dist/ui/menu/index.js +0 -2
  65. package/dist/ui/range/RangePicker.svelte +25 -13
  66. package/dist/ui/range/RangePicker.svelte.d.ts +9 -25
  67. package/dist/ui/range/RangePickerButton.svelte +49 -32
  68. package/dist/ui/range/RangePickerButton.svelte.d.ts +1 -1
  69. package/dist/ui/table/Table.svelte +16 -16
  70. package/dist/ui/table/Table.svelte.d.ts +1 -1
  71. package/dist/ui/table/TableFieldsEdition.svelte +25 -23
  72. package/dist/ui/table/TableHead.svelte +5 -1
  73. package/dist/ui/table/cell/TableCellBoolean.svelte +2 -2
  74. package/dist/ui/table/head/OrderButtons.svelte +21 -20
  75. package/dist/ui/table/head/OrderButtons.svelte.d.ts +2 -0
  76. package/dist/ui/table/head/TableHeadDate.svelte +66 -57
  77. package/dist/ui/table/head/TableHeadNumber.svelte +58 -22
  78. package/dist/ui/table/head/TableHeadSelect.svelte +31 -28
  79. package/dist/ui/table/head/TableHeadString.svelte +12 -8
  80. package/dist/ui/table/index.d.ts +1 -1
  81. package/dist/ui/table/index.js +1 -1
  82. package/package.json +7 -4
  83. package/dist/input/InputRelation.svelte +0 -200
  84. package/dist/popover/popover.svelte.js +0 -106
  85. package/dist/ui/table/TableViewSelect.svelte +0 -142
  86. package/dist/ui/table/TableViewSelect.svelte.d.ts +0 -11
  87. /package/dist/command/{command.svelte.d.ts → useCommand.svelte.d.ts} +0 -0
  88. /package/dist/state/{param.svelte.d.ts → param.d.ts} +0 -0
  89. /package/dist/state/{param.svelte.js → param.js} +0 -0
@@ -3,6 +3,7 @@
3
3
  import { TextareaAutosize } from 'runed'
4
4
  import Issues from './Issues.svelte'
5
5
  import type { TextareaProps } from './type.js'
6
+ import type { Snippet } from 'svelte'
6
7
 
7
8
  let {
8
9
  label,
@@ -10,12 +11,27 @@
10
11
  value = $bindable(),
11
12
  class: klass,
12
13
  maxHeight = 200,
14
+ variant = 'block',
15
+ labelAppend,
13
16
  ...props
14
17
  }: {
15
18
  label: string
16
19
  field?: RemoteFormField<string>
20
+ /**
21
+ * Sans `field`, la valeur liée. Avec, la valeur initiale du champ — celui-ci prend
22
+ * ensuite le relais et devient la source de vérité.
23
+ */
17
24
  value?: string
18
25
  maxHeight?: number
26
+ /**
27
+ * `bare` ne rend que le champ, sans enveloppe ni libellé visible: `label` devient le
28
+ * placeholder et le nom accessible, et `class` s'applique au `<textarea>`. C'est la
29
+ * variante d'un champ posé dans une barre de saisie, où l'appelant place lui-même ce qui
30
+ * l'accompagne.
31
+ */
32
+ variant?: 'floating' | 'block' | 'bare'
33
+ /** Rendu à droite du libellé. Sans effet sur les variantes `floating` et `bare`. */
34
+ labelAppend?: Snippet
19
35
  } & TextareaProps = $props()
20
36
 
21
37
  let textarea = $state<HTMLTextAreaElement>()
@@ -24,20 +40,51 @@
24
40
  input: () => field?.value() || value || '',
25
41
  maxHeight: (() => maxHeight)()
26
42
  })
43
+
44
+ const inputId = $props.id()
45
+ const textareaProps = $derived({
46
+ id: inputId,
47
+ placeholder: variant === 'block' ? '' : label,
48
+ 'aria-label': variant === 'bare' ? label : undefined,
49
+ ...props
50
+ })
51
+ const textareaClass = $derived(['textarea w-full', variant === 'bare' && klass])
27
52
  </script>
28
53
 
29
- <label class="floating-label">
30
- <span>{label}</span>
31
- <textarea
32
- placeholder={label}
33
- class={['textarea', klass]}
34
- bind:this={textarea}
35
- {...field?.as('text')}
36
- bind:value
37
- {...props}
38
- ></textarea>
54
+ {#snippet snippetInput()}
55
+ {#if field}
56
+ <!-- Pas de `bind:value` ici: avec un `field`, `value` n'est que la valeur initiale,
57
+ comme dans InputString et InputNumber. -->
58
+ <textarea
59
+ bind:this={textarea}
60
+ {...textareaProps}
61
+ class={textareaClass}
62
+ {...value === undefined ? field.as('text') : field.as('text', value)}
63
+ ></textarea>
64
+ {:else}
65
+ <textarea bind:this={textarea} {...textareaProps} class={textareaClass} bind:value></textarea>
66
+ {/if}
67
+ {/snippet}
68
+
69
+ {#if variant === 'floating'}
70
+ <label class={['floating-label', klass]}>
71
+ <span>{label}</span>
72
+ {@render snippetInput()}
73
+ <Issues {field} />
74
+ </label>
75
+ {:else if variant === 'bare'}
76
+ {@render snippetInput()}
39
77
  <Issues {field} />
40
- </label>
78
+ {:else}
79
+ <fieldset class={['fieldset', klass]}>
80
+ <label class="label text-wrap" for={inputId}>
81
+ <span>{label}</span>
82
+ {@render labelAppend?.()}
83
+ </label>
84
+ {@render snippetInput()}
85
+ <Issues {field} />
86
+ </fieldset>
87
+ {/if}
41
88
 
42
89
  <style>
43
90
  textarea[aria-invalid='true'] {
@@ -1,10 +1,24 @@
1
1
  import type { RemoteFormField } from '@sveltejs/kit';
2
2
  import type { TextareaProps } from './type.js';
3
+ import type { Snippet } from 'svelte';
3
4
  type $$ComponentProps = {
4
5
  label: string;
5
6
  field?: RemoteFormField<string>;
7
+ /**
8
+ * Sans `field`, la valeur liée. Avec, la valeur initiale du champ — celui-ci prend
9
+ * ensuite le relais et devient la source de vérité.
10
+ */
6
11
  value?: string;
7
12
  maxHeight?: number;
13
+ /**
14
+ * `bare` ne rend que le champ, sans enveloppe ni libellé visible: `label` devient le
15
+ * placeholder et le nom accessible, et `class` s'applique au `<textarea>`. C'est la
16
+ * variante d'un champ posé dans une barre de saisie, où l'appelant place lui-même ce qui
17
+ * l'accompagne.
18
+ */
19
+ variant?: 'floating' | 'block' | 'bare';
20
+ /** Rendu à droite du libellé. Sans effet sur les variantes `floating` et `bare`. */
21
+ labelAppend?: Snippet;
8
22
  } & TextareaProps;
9
23
  declare const InputTextarea: import("svelte").Component<$$ComponentProps, {}, "value">;
10
24
  type InputTextarea = ReturnType<typeof InputTextarea>;
@@ -1,11 +1,14 @@
1
1
  export { default as InputBoolean } from './InputBoolean.svelte';
2
+ export { default as InputCheckboxes } from './InputCheckboxes.svelte';
3
+ export { default as InputDateTime } from './InputDateTime.svelte';
4
+ export { default as InputMultiSelect } from './InputMultiSelect.svelte';
2
5
  export { default as InputNumber } from './InputNumber.svelte';
3
6
  export { default as InputRadio } from './InputRadio.svelte';
4
7
  export { default as InputRange } from './InputRange.svelte';
5
- export { default as InputRelation } from './InputRelation.svelte';
6
8
  export { default as InputSelect } from './InputSelect.svelte';
7
9
  export { default as InputSelectNative } from './InputSelectNative.svelte';
8
10
  export { default as InputString } from './InputString.svelte';
9
11
  export { default as InputTextarea } from './InputTextarea.svelte';
10
12
  export { default as Issues } from './Issues.svelte';
11
13
  export type * from './type.js';
14
+ export { type ItemsSource, type QueryLike, useItems } from './useItems.svelte.js';
@@ -1,10 +1,13 @@
1
1
  export { default as InputBoolean } from './InputBoolean.svelte';
2
+ export { default as InputCheckboxes } from './InputCheckboxes.svelte';
3
+ export { default as InputDateTime } from './InputDateTime.svelte';
4
+ export { default as InputMultiSelect } from './InputMultiSelect.svelte';
2
5
  export { default as InputNumber } from './InputNumber.svelte';
3
6
  export { default as InputRadio } from './InputRadio.svelte';
4
7
  export { default as InputRange } from './InputRange.svelte';
5
- export { default as InputRelation } from './InputRelation.svelte';
6
8
  export { default as InputSelect } from './InputSelect.svelte';
7
9
  export { default as InputSelectNative } from './InputSelectNative.svelte';
8
10
  export { default as InputString } from './InputString.svelte';
9
11
  export { default as InputTextarea } from './InputTextarea.svelte';
10
12
  export { default as Issues } from './Issues.svelte';
13
+ export { useItems } from './useItems.svelte.js';
@@ -0,0 +1,38 @@
1
+ import type { RemoteQueryFunction } from '@sveltejs/kit';
2
+ /**
3
+ * Ce que la liste d'un select et `Loading` lisent d'une source. `RemoteQuery` le satisfait
4
+ * structurellement, ce qui évite d'imposer une remote query à qui n'en a pas.
5
+ */
6
+ export type QueryLike<T> = {
7
+ readonly current: T | undefined;
8
+ readonly loading: boolean;
9
+ readonly error: unknown;
10
+ readonly ready: boolean;
11
+ };
12
+ /**
13
+ * Les trois façons d'alimenter un select:
14
+ * - un tableau déjà chargé — le filtre reste local;
15
+ * - une fonction, synchrone ou non — elle porte son propre filtre;
16
+ * - une remote query — le serveur filtre.
17
+ */
18
+ export type ItemsSource<Item> = Item[] | ((arg: {
19
+ search: string;
20
+ }) => Item[] | Promise<Item[]>) | RemoteQueryFunction<{
21
+ search: string;
22
+ }, Item[]>;
23
+ export type UseItemsOptions<Item> = {
24
+ source: () => ItemsSource<Item>;
25
+ search: () => string;
26
+ /** Base du filtre par défaut, et libellé affiché à défaut de snippet. */
27
+ getLabel: (item: Item) => string;
28
+ filter?: () => ((item: Item, search: string) => boolean) | undefined;
29
+ debounceMs?: number;
30
+ };
31
+ /** `item.id`, puis `item.value`: les deux formes que prennent les items en pratique. */
32
+ export declare function defaultGetValue<Item>(item: Item): string;
33
+ /** Un item porteur d'un `label` s'affiche par celui-ci, ce qui rend les options lisibles sans snippet. */
34
+ export declare function defaultGetLabel<Item>(item: Item, getValue: (item: Item) => string): string;
35
+ /**
36
+ * Ramène les trois formes de `ItemsSource` à la seule que lisent les selects et `Loading`.
37
+ */
38
+ export declare function useItems<Item>({ source, search, getLabel, filter, debounceMs }: UseItemsOptions<Item>): QueryLike<Item[]>;
@@ -0,0 +1,142 @@
1
+ import debounce from 'debounce';
2
+ /** `item.id`, puis `item.value`: les deux formes que prennent les items en pratique. */
3
+ export function defaultGetValue(item) {
4
+ if (item && typeof item === 'object') {
5
+ if ('id' in item && typeof item.id === 'string')
6
+ return item.id;
7
+ if ('value' in item && typeof item.value === 'string')
8
+ return item.value;
9
+ }
10
+ return JSON.stringify(item);
11
+ }
12
+ /** Un item porteur d'un `label` s'affiche par celui-ci, ce qui rend les options lisibles sans snippet. */
13
+ export function defaultGetLabel(item, getValue) {
14
+ if (item && typeof item === 'object' && 'label' in item) {
15
+ if (typeof item.label === 'string')
16
+ return item.label;
17
+ }
18
+ return getValue(item);
19
+ }
20
+ /** Casse et diacritiques ignorées: « Genève » se trouve en tapant « geneve ». */
21
+ function normalize(value) {
22
+ return value
23
+ .toLowerCase()
24
+ .normalize('NFD')
25
+ .replace(/\p{Diacritic}/gu, '');
26
+ }
27
+ /** `RemoteQuery` est une `Promise` augmentée: `ready` est ce qui l'en distingue. */
28
+ function isQueryLike(value) {
29
+ return !!value && typeof value === 'object' && 'ready' in value;
30
+ }
31
+ /**
32
+ * Ramène les trois formes de `ItemsSource` à la seule que lisent les selects et `Loading`.
33
+ */
34
+ export function useItems({ source, search, getLabel, filter, debounceMs = 150 }) {
35
+ // On temporise la valeur cherchée plutôt que l'appel: un seul mécanisme couvre les trois
36
+ // formes, et les remote queries cessent de partir à chaque frappe.
37
+ let debounced = $state(search());
38
+ const apply = debounce((value) => {
39
+ debounced = value;
40
+ }, debounceMs);
41
+ $effect(() => {
42
+ const value = search();
43
+ // Une recherche vide s'applique tout de suite: ouvrir le popover ne doit rien attendre.
44
+ if (!value) {
45
+ apply.clear();
46
+ debounced = value;
47
+ return;
48
+ }
49
+ apply(value);
50
+ });
51
+ const resolved = $derived.by(() => {
52
+ const items = source();
53
+ if (Array.isArray(items))
54
+ return { kind: 'static', value: items };
55
+ // Appel unique: c'est son retour, et lui seul, qui distingue les trois formes.
56
+ const result = items({ search: debounced });
57
+ if (Array.isArray(result))
58
+ return { kind: 'resolved', value: result };
59
+ if (isQueryLike(result))
60
+ return { kind: 'query', value: result };
61
+ return { kind: 'promise', value: result };
62
+ });
63
+ const pending = $state({
64
+ current: [],
65
+ loading: false,
66
+ error: undefined,
67
+ ready: false
68
+ });
69
+ let sequence = 0;
70
+ $effect(() => {
71
+ if (resolved.kind !== 'promise')
72
+ return;
73
+ // Une réponse en retard ne doit pas écraser une plus récente.
74
+ const current = ++sequence;
75
+ pending.loading = true;
76
+ resolved.value
77
+ .then((items) => {
78
+ if (current !== sequence)
79
+ return;
80
+ pending.current = items;
81
+ pending.error = undefined;
82
+ pending.ready = true;
83
+ })
84
+ .catch((error) => {
85
+ if (current !== sequence)
86
+ return;
87
+ pending.error = error;
88
+ })
89
+ .finally(() => {
90
+ if (current !== sequence)
91
+ return;
92
+ pending.loading = false;
93
+ });
94
+ });
95
+ function filterStatic(items) {
96
+ if (!debounced)
97
+ return items;
98
+ const match = filter?.();
99
+ if (match)
100
+ return items.filter((item) => match(item, debounced));
101
+ const needle = normalize(debounced);
102
+ return items.filter((item) => normalize(getLabel(item)).includes(needle));
103
+ }
104
+ return {
105
+ get current() {
106
+ switch (resolved.kind) {
107
+ case 'static':
108
+ return filterStatic(resolved.value);
109
+ case 'resolved':
110
+ return resolved.value;
111
+ case 'query':
112
+ return resolved.value.current ?? [];
113
+ case 'promise':
114
+ return pending.current;
115
+ }
116
+ },
117
+ get loading() {
118
+ // Sans quoi la fenêtre de temporisation afficherait un résultat périmé sans indicateur.
119
+ if (search() !== debounced)
120
+ return true;
121
+ if (resolved.kind === 'query')
122
+ return resolved.value.loading;
123
+ if (resolved.kind === 'promise')
124
+ return pending.loading;
125
+ return false;
126
+ },
127
+ get error() {
128
+ if (resolved.kind === 'query')
129
+ return resolved.value.error;
130
+ if (resolved.kind === 'promise')
131
+ return pending.error;
132
+ return undefined;
133
+ },
134
+ get ready() {
135
+ if (resolved.kind === 'query')
136
+ return resolved.value.ready;
137
+ if (resolved.kind === 'promise')
138
+ return pending.ready;
139
+ return true;
140
+ }
141
+ };
142
+ }
@@ -1,8 +1,8 @@
1
1
  <script lang="ts">
2
2
  import { GhostIcon, LoaderCircleIcon, TriangleAlertIcon } from '@lucide/svelte'
3
- import type { RemoteQuery } from '@sveltejs/kit'
4
3
  import type { ClassValue } from 'svelte/elements'
5
4
  import { fade } from 'svelte/transition'
5
+ import type { QueryLike } from '../input/useItems.svelte.js'
6
6
 
7
7
  let {
8
8
  query,
@@ -10,7 +10,8 @@
10
10
  error = 'Une erreur est survenue',
11
11
  class: klass
12
12
  }: {
13
- query: RemoteQuery<unknown>
13
+ /** Structurel plutôt que `RemoteQuery`: une source locale s'affiche de la même façon. */
14
+ query: QueryLike<unknown>
14
15
  empty?: string
15
16
  error?: string
16
17
  class?: ClassValue
@@ -1,7 +1,8 @@
1
- import type { RemoteQuery } from '@sveltejs/kit';
2
1
  import type { ClassValue } from 'svelte/elements';
2
+ import type { QueryLike } from '../input/useItems.svelte.js';
3
3
  type $$ComponentProps = {
4
- query: RemoteQuery<unknown>;
4
+ /** Structurel plutôt que `RemoteQuery`: une source locale s'affiche de la même façon. */
5
+ query: QueryLike<unknown>;
5
6
  empty?: string;
6
7
  error?: string;
7
8
  class?: ClassValue;
@@ -6,7 +6,7 @@
6
6
  type PopoverOptions,
7
7
  type PopoverType,
8
8
  usePopover
9
- } from './popover.svelte.js'
9
+ } from './usePopover.svelte.js'
10
10
 
11
11
  let {
12
12
  children,
@@ -37,7 +37,6 @@
37
37
  }
38
38
 
39
39
  div[popover]:popover-open {
40
- inset: 5px;
41
40
  transition-property: opacity, scale;
42
41
  transition-duration: 150ms;
43
42
  opacity: 1;
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { ClassValue } from 'svelte/elements';
3
+ import { type Placement, type PopoverOptions, type PopoverType } from './usePopover.svelte.js';
4
+ type $$ComponentProps = {
5
+ children: Snippet<[PopoverType]>;
6
+ trigger: Snippet<[PopoverType]>;
7
+ placement?: Placement;
8
+ class?: ClassValue;
9
+ style?: string;
10
+ } & PopoverOptions;
11
+ declare const Popover: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type Popover = ReturnType<typeof Popover>;
13
+ export default Popover;
@@ -1,2 +1,2 @@
1
1
  export { default as Popover } from './Popover.svelte';
2
- export * from './popover.svelte.js';
2
+ export * from './usePopover.svelte.js';
@@ -1,2 +1,2 @@
1
1
  export { default as Popover } from './Popover.svelte';
2
- export * from './popover.svelte.js';
2
+ export * from './usePopover.svelte.js';
@@ -7,6 +7,7 @@ export type PopoverOptions = {
7
7
  placement?: Placement;
8
8
  listenClick?: boolean;
9
9
  listenFocus?: boolean;
10
+ listenFocusout?: boolean;
10
11
  listenHover?: boolean;
11
12
  /** Only when listenHover={true} */
12
13
  hideDelay?: number;
@@ -14,7 +15,7 @@ export type PopoverOptions = {
14
15
  onHide?: () => void;
15
16
  hotKey?: string;
16
17
  };
17
- export declare function usePopover({ mode, placement, listenClick, listenFocus, listenHover, hideDelay, onShow, onHide, hotKey }?: PopoverOptions): {
18
+ export declare function usePopover({ mode, placement, listenClick, listenFocus, listenFocusout, listenHover, hideDelay, onShow, onHide, hotKey }?: PopoverOptions): {
18
19
  show: () => void;
19
20
  hide: () => void;
20
21
  readonly isOpen: boolean;
@@ -0,0 +1,145 @@
1
+ import debounce from 'debounce';
2
+ import { untrack } from 'svelte';
3
+ import { createAttachmentKey } from 'svelte/attachments';
4
+ import { on } from 'svelte/events';
5
+ const placements = {
6
+ top: 'top',
7
+ 'top-end': 'top span-left',
8
+ 'top-start': 'top span-right',
9
+ bottom: 'bottom',
10
+ 'bottom-end': 'bottom span-left',
11
+ 'bottom-start': 'bottom span-right',
12
+ left: 'left',
13
+ 'left-end': 'left span-top',
14
+ 'left-start': 'left span-bottom',
15
+ right: 'right',
16
+ 'right-end': 'right span-top',
17
+ 'right-start': 'right span-bottom'
18
+ };
19
+ let popoverInstanceCount = 0;
20
+ export function usePopover({ mode = 'auto', placement = 'bottom-start', listenClick = true, listenFocus = true, listenFocusout = false, listenHover = false, hideDelay = 400, onShow, onHide, hotKey } = {}) {
21
+ const uid = popoverInstanceCount++;
22
+ const anchorName = `--anchor-${uid}`;
23
+ let content = $state();
24
+ let activator = $state();
25
+ let isOpen = $state(false);
26
+ const hideDebounced = debounce(hide, hideDelay);
27
+ let isHiding = false;
28
+ function show() {
29
+ // En refermant, le navigateur rend le focus au déclencheur si le focus était dans le
30
+ // popover — un bouton « Ok » ou « Effacer », typiquement. Sans ce garde, le `focusin`
31
+ // qui s'ensuit rouvre aussitôt ce qu'on vient de fermer, et `hide()` ne fait rien.
32
+ if (isHiding)
33
+ return;
34
+ content?.showPopover();
35
+ onShow?.();
36
+ }
37
+ function hide() {
38
+ isHiding = true;
39
+ content?.hidePopover();
40
+ isHiding = false;
41
+ onHide?.();
42
+ }
43
+ function onMouseEnter() {
44
+ hideDebounced.clear();
45
+ show();
46
+ }
47
+ function onToggle(event) {
48
+ isOpen = event.newState === 'open';
49
+ }
50
+ function onFocusOut({ relatedTarget }) {
51
+ // Le focus ne va nulle part — `null`, ou `body` selon le navigateur: typiquement un
52
+ // `mousedown` sur un élément non focusable, un jour du calendrier par exemple. Fermer ici
53
+ // escamoterait la cible avant que le `click` ne l'atteigne; hors du popover, le
54
+ // light-dismiss natif s'en charge de toute façon.
55
+ if (!(relatedTarget instanceof Node) || relatedTarget === document.body)
56
+ return;
57
+ if (!content?.contains(relatedTarget) && relatedTarget !== activator)
58
+ return hide();
59
+ }
60
+ function attachTriggerListeners(node) {
61
+ const cleanups = [];
62
+ if (listenClick) {
63
+ cleanups.push(on(node, 'click', show));
64
+ }
65
+ if (listenFocus) {
66
+ cleanups.push(on(node, 'focusin', show));
67
+ }
68
+ if (listenFocus || listenFocusout) {
69
+ cleanups.push(on(node, 'focusout', onFocusOut));
70
+ }
71
+ if (listenHover) {
72
+ cleanups.push(on(node, 'mouseenter', onMouseEnter));
73
+ cleanups.push(on(node, 'mouseleave', hideDebounced));
74
+ }
75
+ if (hotKey) {
76
+ cleanups.push(on(window, 'keydown', handleHotKey));
77
+ }
78
+ return () => {
79
+ for (const cleanup of cleanups)
80
+ cleanup();
81
+ };
82
+ }
83
+ function attachPopoverListeners(node) {
84
+ const cleanups = [];
85
+ cleanups.push(on(node, 'toggle', onToggle));
86
+ if (listenHover) {
87
+ cleanups.push(on(node, 'mouseenter', onMouseEnter));
88
+ cleanups.push(on(node, 'mouseleave', hideDebounced));
89
+ }
90
+ if (listenFocus || listenFocusout) {
91
+ cleanups.push(on(node, 'focusout', onFocusOut));
92
+ }
93
+ return () => {
94
+ for (const cleanup of cleanups)
95
+ cleanup();
96
+ };
97
+ }
98
+ async function handleHotKey(event) {
99
+ const { metaKey, ctrlKey, key } = event;
100
+ if ((metaKey || ctrlKey) && key === hotKey) {
101
+ event.preventDefault();
102
+ show();
103
+ return;
104
+ }
105
+ }
106
+ return {
107
+ show,
108
+ hide,
109
+ get isOpen() {
110
+ return isOpen;
111
+ },
112
+ content: {
113
+ popover: mode,
114
+ [createAttachmentKey()]: (node) => {
115
+ untrack(() => (content = node));
116
+ node.style.positionAnchor = anchorName;
117
+ node.style.positionArea = placements[placement];
118
+ node.style.inset = 'auto';
119
+ node.style.positionTry = 'flip-x, flip-y';
120
+ // `fixed` et non `relative`: dans le top layer `relative` est absolutisé, le bloc
121
+ // conteneur devient alors le document et Chrome compense le défilement de page même
122
+ // quand l'ancre vit dans un sous-arbre `position: fixed` (tiroir, en-tête collant) qui,
123
+ // lui, ne défile pas — le popover dérivait d'exactement `window.scrollY`. `fixed` cale
124
+ // le bloc conteneur sur le viewport, ce qui rend aussi `position-try` viewport-relatif.
125
+ node.style.position = 'fixed';
126
+ const cleanup = attachPopoverListeners(node);
127
+ return () => {
128
+ untrack(() => (content = undefined));
129
+ cleanup();
130
+ };
131
+ }
132
+ },
133
+ trigger: {
134
+ [createAttachmentKey()]: (node) => {
135
+ untrack(() => (activator = node));
136
+ node.style.anchorName = anchorName;
137
+ const cleanup = attachTriggerListeners(node);
138
+ return () => {
139
+ untrack(() => (activator = undefined));
140
+ cleanup();
141
+ };
142
+ }
143
+ }
144
+ };
145
+ }
@@ -1,4 +1,4 @@
1
- import type { RemoteForm, RemoteFormInput } from '@sveltejs/kit';
1
+ import type { RemoteForm, RemoteFormEnhanceCallback, RemoteFormInput } from '@sveltejs/kit';
2
2
  import type { StandardSchemaV1 } from 'runed/kit';
3
3
  import { type Attachment } from 'svelte/attachments';
4
4
  /**
@@ -7,10 +7,9 @@ import { type Attachment } from 'svelte/attachments';
7
7
  */
8
8
  export declare function useForm<Input extends RemoteFormInput | undefined, Output>(remoteForm: RemoteForm<Input, Output>, schema: StandardSchemaV1<Input>, debounceMs?: number): RemoteForm<Input, Output>;
9
9
  export declare function validationOnInput<Input extends RemoteFormInput | undefined, Output>(remoteForm: RemoteForm<Input, Output>, debounceMs?: number): Attachment<HTMLFormElement>;
10
- type UseEnhanceCallbackOptions<Input extends RemoteFormInput | void> = {
11
- onSuccess?: (data: Input, form: HTMLFormElement) => void;
10
+ type UseEnhanceCallbackOptions<Output> = {
11
+ onSuccess?: (result: Output | undefined, element: HTMLFormElement) => void;
12
12
  onError?: (err: unknown) => void;
13
13
  };
14
- type EnhanceCallback<Input extends RemoteFormInput | void> = Parameters<RemoteForm<Input, unknown>['enhance']>[0];
15
- export declare function useEnhanceCallback<Input extends RemoteFormInput | void>({ onSuccess, onError }: UseEnhanceCallbackOptions<Input>): EnhanceCallback<Input>;
14
+ export declare function useEnhanceCallback<Input extends RemoteFormInput | void, Output>({ onSuccess, onError }: UseEnhanceCallbackOptions<Output>): RemoteFormEnhanceCallback<Input, Output>;
16
15
  export {};
@@ -29,10 +29,15 @@ export function validationOnInput(remoteForm, debounceMs = 1000) {
29
29
  return validation;
30
30
  }
31
31
  export function useEnhanceCallback({ onSuccess, onError }) {
32
- return ({ submit, data, form }) => {
33
- submit()
34
- .then(() => {
35
- onSuccess?.(data, form);
32
+ // L'instance passée au callback ne porte plus `data` ni `form`: la donnée soumise n'est plus
33
+ // exposée telle quelle, et l'élément s'appelle désormais `element`.
34
+ return (form) => {
35
+ form
36
+ .submit()
37
+ .then((succeeded) => {
38
+ // `submit()` résout `false` quand le préflight ou la validation serveur a échoué.
39
+ if (succeeded)
40
+ onSuccess?.(form.result, form.element);
36
41
  })
37
42
  .catch((err) => {
38
43
  console.error(err);
@@ -1 +1 @@
1
- export * from './param.svelte.js';
1
+ export * from './param.js';
@@ -1 +1 @@
1
- export * from './param.svelte.js';
1
+ export * from './param.js';
@@ -8,6 +8,7 @@
8
8
  formaction = '',
9
9
  onclick: onclickProp,
10
10
  onmouseleave: onmouseleaveProp,
11
+ class: klass,
11
12
  ...props
12
13
  }: {
13
14
  /** Form action URL triggered on confirmation */
@@ -16,7 +17,8 @@
16
17
  btn?: boolean
17
18
  /** Custom button content */
18
19
  children?: Snippet<[{ waitConfirmation: boolean }]>
19
- } & HTMLButtonAttributes = $props()
20
+ // `children` de `HTMLButtonAttributes` est un `Snippet<[]>`: l'intersection interdirait le paramètre.
21
+ } & Omit<HTMLButtonAttributes, 'children'> = $props()
20
22
 
21
23
  let button: HTMLButtonElement
22
24
  let waitConfirmation = $state(false)
@@ -47,9 +49,10 @@
47
49
  class={[
48
50
  btn && [
49
51
  'btn',
50
- waitConfirmation ? 'btn-outline btn-error' : 'text-error btn-ghost',
52
+ waitConfirmation ? 'btn-outline btn-error' : 'btn-ghost text-error',
51
53
  props.disabled && 'btn-disabled'
52
- ]
54
+ ],
55
+ klass
53
56
  ]}
54
57
  formaction={waitConfirmation ? formaction : undefined}
55
58
  {onclick}
@@ -9,7 +9,7 @@ type $$ComponentProps = {
9
9
  children?: Snippet<[{
10
10
  waitConfirmation: boolean;
11
11
  }]>;
12
- } & HTMLButtonAttributes;
12
+ } & Omit<HTMLButtonAttributes, 'children'>;
13
13
  declare const ButtonDelete: import("svelte").Component<$$ComponentProps, {}, "">;
14
14
  type ButtonDelete = ReturnType<typeof ButtonDelete>;
15
15
  export default ButtonDelete;