fuma 2.1.5 → 2.2.7

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 (84) 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 +25 -7
  7. package/dist/input/InputBoolean.svelte.d.ts +4 -0
  8. package/dist/input/InputCheckboxes.svelte +116 -0
  9. package/dist/input/InputCheckboxes.svelte.d.ts +16 -0
  10. package/dist/input/InputDateTime.svelte +194 -0
  11. package/dist/input/InputDateTime.svelte.d.ts +23 -0
  12. package/dist/input/InputMultiSelect.svelte +286 -0
  13. package/dist/input/{InputRelation.svelte.d.ts → InputMultiSelect.svelte.d.ts} +27 -13
  14. package/dist/input/InputNumber.svelte +36 -6
  15. package/dist/input/InputNumber.svelte.d.ts +5 -0
  16. package/dist/input/InputRadio.svelte +54 -23
  17. package/dist/input/InputRange.svelte +19 -4
  18. package/dist/input/InputRange.svelte.d.ts +6 -0
  19. package/dist/input/InputSelect.svelte +204 -62
  20. package/dist/input/InputSelect.svelte.d.ts +87 -6
  21. package/dist/input/InputSelectNative.svelte +44 -19
  22. package/dist/input/InputSelectNative.svelte.d.ts +5 -0
  23. package/dist/input/InputString.svelte +36 -6
  24. package/dist/input/InputString.svelte.d.ts +5 -0
  25. package/dist/input/InputTextarea.svelte +41 -12
  26. package/dist/input/InputTextarea.svelte.d.ts +5 -0
  27. package/dist/input/index.d.ts +4 -1
  28. package/dist/input/index.js +4 -1
  29. package/dist/input/useItems.svelte.d.ts +38 -0
  30. package/dist/input/useItems.svelte.js +142 -0
  31. package/dist/loading/Loading.svelte +3 -2
  32. package/dist/loading/Loading.svelte.d.ts +3 -2
  33. package/dist/popover/Popover.svelte +1 -2
  34. package/dist/popover/Popover.svelte.d.ts +13 -0
  35. package/dist/popover/index.d.ts +1 -1
  36. package/dist/popover/index.js +1 -1
  37. package/dist/popover/{popover.svelte.d.ts → usePopover.svelte.d.ts} +2 -1
  38. package/dist/popover/usePopover.svelte.js +145 -0
  39. package/dist/remote/useForm.d.ts +4 -5
  40. package/dist/remote/useForm.js +9 -4
  41. package/dist/state/index.d.ts +1 -1
  42. package/dist/state/index.js +1 -1
  43. package/dist/ui/button/ButtonDelete.svelte +4 -2
  44. package/dist/ui/dialog/Dialog.svelte +5 -5
  45. package/dist/ui/drawer/Drawer.svelte +74 -26
  46. package/dist/ui/drawer/Drawer.svelte.d.ts +10 -4
  47. package/dist/ui/drawer/index.d.ts +1 -0
  48. package/dist/ui/drawer/index.js +1 -0
  49. package/dist/ui/drawer/inertBackground.d.ts +13 -0
  50. package/dist/ui/drawer/inertBackground.js +59 -0
  51. package/dist/ui/drawer/useLayer.svelte.d.ts +1 -1
  52. package/dist/ui/drawer/useLayer.svelte.js +11 -7
  53. package/dist/ui/index.d.ts +1 -0
  54. package/dist/ui/index.js +1 -0
  55. package/dist/ui/menu/ContextMenu.svelte +30 -17
  56. package/dist/ui/menu/ContextMenu.svelte.d.ts +2 -3
  57. package/dist/ui/menu/DropDown.svelte +1 -1
  58. package/dist/ui/menu/DropDown.svelte.d.ts +1 -1
  59. package/dist/ui/menu/DropDownMenu.svelte +12 -10
  60. package/dist/ui/menu/DropDownMenu.svelte.d.ts +2 -3
  61. package/dist/ui/menu/index.d.ts +0 -1
  62. package/dist/ui/menu/index.js +0 -2
  63. package/dist/ui/range/RangePicker.svelte +25 -13
  64. package/dist/ui/range/RangePicker.svelte.d.ts +9 -25
  65. package/dist/ui/range/RangePickerButton.svelte +49 -32
  66. package/dist/ui/range/RangePickerButton.svelte.d.ts +1 -1
  67. package/dist/ui/table/Table.svelte +7 -10
  68. package/dist/ui/table/Table.svelte.d.ts +1 -1
  69. package/dist/ui/table/TableFieldsEdition.svelte +24 -22
  70. package/dist/ui/table/TableHead.svelte +5 -1
  71. package/dist/ui/table/TableViewSelect.svelte +52 -47
  72. package/dist/ui/table/cell/TableCellBoolean.svelte +2 -2
  73. package/dist/ui/table/head/TableHeadDate.svelte +62 -55
  74. package/dist/ui/table/head/TableHeadNumber.svelte +55 -19
  75. package/dist/ui/table/head/TableHeadSelect.svelte +30 -27
  76. package/dist/ui/table/head/TableHeadString.svelte +11 -7
  77. package/dist/ui/table/index.d.ts +1 -0
  78. package/dist/ui/table/index.js +1 -0
  79. package/package.json +7 -4
  80. package/dist/input/InputRelation.svelte +0 -200
  81. package/dist/popover/popover.svelte.js +0 -106
  82. /package/dist/command/{command.svelte.d.ts → useCommand.svelte.d.ts} +0 -0
  83. /package/dist/state/{param.svelte.d.ts → param.d.ts} +0 -0
  84. /package/dist/state/{param.svelte.js → param.js} +0 -0
@@ -1,109 +1,251 @@
1
1
  <script lang="ts" generics="Item">
2
- import { ChevronsUpDownIcon } from '@lucide/svelte'
2
+ import { ChevronsUpDownIcon, SearchIcon, XIcon } from '@lucide/svelte'
3
3
  import type { RemoteFormField } from '@sveltejs/kit'
4
4
  import type { Snippet } from 'svelte'
5
- import { useCommand } from '../command/command.svelte.js'
6
- import { usePopover } from '../popover/popover.svelte.js'
5
+ import type { ClassValue, HTMLLiAttributes } from 'svelte/elements'
6
+ import { tip } from '../action/tip.js'
7
+ import { useCommand } from '../command/useCommand.svelte.js'
8
+ import Loading from '../loading/Loading.svelte'
9
+ import { type PopoverType, usePopover } from '../popover/usePopover.svelte.js'
7
10
  import Issues from './Issues.svelte'
11
+ import {
12
+ defaultGetLabel,
13
+ defaultGetValue,
14
+ type ItemsSource,
15
+ useItems
16
+ } from './useItems.svelte.js'
8
17
 
9
18
  let {
10
19
  label,
11
20
  items,
12
- getValue,
13
- selected,
14
- proposal,
21
+ getValue = defaultGetValue,
22
+ getLabel = (item) => defaultGetLabel(item, getValue),
23
+ filter,
24
+ searchable,
25
+ debounceMs,
26
+ selected = defaultSnippet,
27
+ proposal = defaultSnippet,
15
28
  field,
16
- value = $bindable()
29
+ value = $bindable(),
30
+ placeholder = 'Selectionner une valeur',
31
+ class: klass,
32
+ nullable,
33
+ hint,
34
+ append,
35
+ header,
36
+ proposalAppend,
37
+ onSelect,
38
+ hotKey,
39
+ variant = 'block',
40
+ propsLi
17
41
  }: {
18
- label: string
19
- items: Item[]
20
- getValue: (item: Item) => string
42
+ /** Un tableau déjà chargé, une fonction (synchrone ou non), ou une remote query. */
43
+ items: ItemsSource<Item>
44
+ label?: string
45
+ getValue?: (item: NoInfer<Item>) => string
46
+ /** Libellé affiché à défaut de snippet, et base du filtre d'une source tableau. */
47
+ getLabel?: (item: NoInfer<Item>) => string
48
+ /** Ne s'applique qu'à une source tableau: une fonction porte son propre filtre. */
49
+ filter?: (item: NoInfer<Item>, search: string) => boolean
50
+ /** Par défaut: toujours pour une fonction, au-delà de 7 entrées pour un tableau. */
51
+ searchable?: boolean
52
+ debounceMs?: number
21
53
  selected?: Snippet<[Item]>
22
- proposal?: Snippet<[Item, { isSelected: boolean; isFocus: boolean }]>
54
+ proposal?: Snippet<[Item, { popover: PopoverType; isSelected: boolean; isFocus: boolean }]>
55
+ proposalAppend?: Snippet<
56
+ [Item, { popover: PopoverType; isSelected: boolean; isFocus: boolean }]
57
+ >
23
58
  field?: RemoteFormField<string>
24
- value?: string
59
+ /**
60
+ * L'item, pas sa valeur soumise. Un parent qui le repilote après coup — remise à
61
+ * zéro, changement de jeu d'options — doit le lier par `bind:`: passé en simple
62
+ * prop, un `$bindable` cesse de suivre le parent dès que le composant y a écrit.
63
+ */
64
+ value?: NoInfer<Item>
65
+ placeholder?: string
66
+ class?: ClassValue
67
+ nullable?: boolean
68
+ hint?: Snippet<[Item | undefined]>
69
+ /** Rendu à droite du champ de recherche: une action « créer », typiquement. */
70
+ append?: Snippet<[PopoverType]>
71
+ header?: Snippet<[PopoverType]>
72
+ onSelect?: (item: NoInfer<Item> | undefined, popover: PopoverType) => void
73
+ hotKey?: string
74
+ variant?: 'floating' | 'block'
75
+ propsLi?: HTMLLiAttributes
25
76
  } = $props()
26
77
 
27
- let selectedItem = $state<Item | undefined>(undefined)
78
+ const inputId = $props.id()
28
79
 
29
- $effect(() => {
30
- const targetValue = field?.value() ?? value
31
- selectedItem = items.find((item) => getValue(item) === targetValue)
80
+ let search = $state('')
81
+ // Une énumération courte n'a que faire d'un champ de recherche; une fonction, elle, ne
82
+ // peut pas s'en passer, puisque le composant ne sait pas la filtrer.
83
+ const isSearchable = $derived(searchable ?? (Array.isArray(items) ? items.length > 7 : true))
84
+ const query = useItems({
85
+ source: () => items,
86
+ search: () => search,
87
+ getLabel: (item) => getLabel(item),
88
+ filter: () => filter,
89
+ debounceMs: (() => debounceMs)()
32
90
  })
91
+ const options = $derived(query.current ?? [])
92
+
93
+ // `value` porte l'item et non sa valeur soumise: celle-ci s'en dérive par `getValue`,
94
+ // alors que l'inverse demanderait une requête. C'est aussi ce qui rend l'état initial
95
+ // gratuit — pas de `defaultValue` à réconcilier avec la valeur du champ.
96
+ const submittedValue = $derived(value ? getValue(value) : undefined)
33
97
 
34
- const popover = usePopover({ listenFocus: false })
35
- const command = useCommand({
98
+ export const popover = usePopover({
99
+ listenFocus: false,
100
+ listenFocusout: true,
101
+ onShow: () => command.focusTrigger(),
102
+ hotKey: (() => hotKey)()
103
+ })
104
+ export const command = useCommand({
36
105
  isEnable: () => popover.isOpen,
37
106
  onSelect: (index) => {
38
107
  popover.hide()
39
- const item = items[index]
108
+ const item = options[index]
109
+ onSelect?.(item, popover)
40
110
  if (!item) return
41
- selectedItem = item
42
- if (field) {
43
- field.set(getValue(item))
44
- } else {
45
- value = getValue(item)
46
- }
111
+ setValue(item)
47
112
  }
48
113
  })
114
+
115
+ function setValue(item: Item | undefined) {
116
+ value = item
117
+ // L'input caché suffit à soumettre; `set` garde la validation du formulaire à jour.
118
+ field?.set(item && getValue(item))
119
+ }
120
+
121
+ const isButtonSetNullVisible = $derived(nullable && value)
49
122
  </script>
50
123
 
51
- {#if field?.value() !== undefined}
52
- <input {...field.as('hidden', field.value() as string)} />
124
+ {#snippet defaultSnippet(item: Item)}
125
+ <span>{getLabel(item)}</span>
126
+ {/snippet}
127
+
128
+ {#snippet triggerButton()}
129
+ <button
130
+ id={inputId}
131
+ type="button"
132
+ class={['input', field?.issues()?.length && 'input-error', klass]}
133
+ {...popover.trigger}
134
+ {...isSearchable ? {} : command.trigger}
135
+ >
136
+ <div class="grow text-left">
137
+ {#if !value}
138
+ <span class="opacity-60">{placeholder}</span>
139
+ {:else}
140
+ <!-- item.icon not rerender if not wrapped in #key -->
141
+ {#key value}{@render selected(value)}{/key}
142
+ {/if}
143
+ </div>
144
+ <ChevronsUpDownIcon size={14} opacity={0.7} />
145
+ </button>
146
+ {/snippet}
147
+
148
+ <!-- Rien à soumettre tant qu'aucune valeur n'est choisie: `as('hidden')` refuse une
149
+ valeur vide. -->
150
+ {#if field && submittedValue}
151
+ <input {...field.as('hidden', submittedValue)} />
53
152
  {/if}
54
153
 
55
- <div>
56
- <label class="floating-label">
57
- <span>{label}</span>
58
- <button
59
- type="button"
60
- class={['input', field?.issues.length && 'input-error']}
61
- {...popover.trigger}
62
- {...command.trigger}
63
- >
64
- <div class="grow text-left">
65
- {#if !selectedItem}
66
- <span class="opacity-60">Select a value</span>
67
- {:else if selected}
68
- <!-- item.icon not rerender if not wrapped in #key -->
69
- {#key selectedItem}{@render selected(selectedItem)}{/key}
70
- {:else}
71
- {getValue(selectedItem)}
72
- {/if}
73
- </div>
74
- <ChevronsUpDownIcon size={14} />
75
- </button>
76
- </label>
154
+ <div class="contents">
155
+ {#if !label}
156
+ {@render triggerButton()}
157
+ <Issues {field} />
158
+ {@render hint?.(value)}
159
+ {:else if variant === 'floating'}
160
+ <label class="floating-label">
161
+ <span>{label}</span>
162
+ {@render triggerButton()}
163
+ <Issues {field} />
164
+ {@render hint?.(value)}
165
+ </label>
166
+ {:else}
167
+ <fieldset class="fieldset">
168
+ <label class="label text-wrap" for={inputId}>{label}</label>
169
+ {@render triggerButton()}
170
+ <Issues {field} />
171
+ {@render hint?.(value)}
172
+ </fieldset>
173
+ {/if}
77
174
 
78
175
  <div
79
176
  {...popover.content}
80
177
  class={['my-2 rounded-box border bg-base-100 shadow-xl']}
81
- style="width: anchor-size(width);"
178
+ style="min-width: anchor-size(width);"
82
179
  tabindex="-1"
83
180
  >
84
- <ul class="menu max-h-80 w-full flex-nowrap">
85
- {#each items as item, index (item)}
86
- {@const isSelected = index === command.selectedIndex}
181
+ {#if isSearchable || isButtonSetNullVisible || append || header}
182
+ <div class="sticky top-0 z-10 flex gap-2 bg-base-100/10 p-1 backdrop-blur-md">
183
+ {#if header}
184
+ {@render header(popover)}
185
+ {:else}
186
+ {#if isSearchable}
187
+ <label class="input grow input-ghost input-sm">
188
+ <SearchIcon opacity={0.6} size={20} />
189
+ <input
190
+ type="search"
191
+ placeholder="Recherche"
192
+ autocomplete="off"
193
+ bind:value={search}
194
+ {...command.trigger}
195
+ />
196
+ </label>
197
+ {:else}
198
+ <div class="grow"></div>
199
+ {/if}
200
+ {#if isButtonSetNullVisible}
201
+ <button
202
+ class="btn btn-square btn-soft btn-sm"
203
+ type="button"
204
+ onclick={() => {
205
+ setValue(undefined)
206
+ onSelect?.(undefined, popover)
207
+ popover.hide()
208
+ }}
209
+ use:tip={{ content: 'Pas de sélection' }}
210
+ >
211
+ <XIcon />
212
+ </button>
213
+ {/if}
214
+ {@render append?.(popover)}
215
+ {/if}
216
+ </div>
217
+ {/if}
218
+
219
+ <ul
220
+ class={[
221
+ 'menu max-h-80 w-full flex-nowrap gap-1 p-1',
222
+ (isSearchable || append || header) && 'pt-0'
223
+ ]}
224
+ >
225
+ {#each options as item, index (item)}
226
+ {@const isSelected = submittedValue === getValue(item)}
87
227
  {@const isFocus = index === command.focusIndex}
88
- <li>
228
+ <li {...propsLi}>
89
229
  <button
90
- {...command.item(index)}
91
- class={[isFocus && 'menu-focus']}
230
+ {...command.item(index, '')}
231
+ class={[
232
+ isSelected && 'border-hard border',
233
+ isFocus && 'menu-focus',
234
+ 'grow scroll-mt-13 scroll-mb-2'
235
+ ]}
92
236
  type="button"
93
237
  tabindex="-1"
94
238
  role="option"
95
239
  >
96
- {#if proposal}
97
- {@render proposal(item, { isSelected, isFocus })}
98
- {:else}
99
- {getValue(item)}
100
- {/if}
240
+ {@render proposal(item, { popover, isSelected, isFocus })}
101
241
  </button>
242
+ {@render proposalAppend?.(item, { popover, isSelected, isFocus })}
102
243
  </li>
103
244
  {/each}
104
245
  </ul>
246
+
247
+ <Loading {query} />
105
248
  </div>
106
- <Issues {field} />
107
249
  </div>
108
250
 
109
251
  <style>
@@ -1,19 +1,76 @@
1
1
  import type { RemoteFormField } from '@sveltejs/kit';
2
2
  import type { Snippet } from 'svelte';
3
+ import type { ClassValue, HTMLLiAttributes } from 'svelte/elements';
4
+ import { type PopoverType } from '../popover/usePopover.svelte.js';
5
+ import { type ItemsSource } from './useItems.svelte.js';
3
6
  declare function $$render<Item>(): {
4
7
  props: {
5
- label: string;
6
- items: Item[];
7
- getValue: (item: Item) => string;
8
+ /** Un tableau déjà chargé, une fonction (synchrone ou non), ou une remote query. */
9
+ items: ItemsSource<Item>;
10
+ label?: string;
11
+ getValue?: (item: NoInfer<Item>) => string;
12
+ /** Libellé affiché à défaut de snippet, et base du filtre d'une source tableau. */
13
+ getLabel?: (item: NoInfer<Item>) => string;
14
+ /** Ne s'applique qu'à une source tableau: une fonction porte son propre filtre. */
15
+ filter?: (item: NoInfer<Item>, search: string) => boolean;
16
+ /** Par défaut: toujours pour une fonction, au-delà de 7 entrées pour un tableau. */
17
+ searchable?: boolean;
18
+ debounceMs?: number;
8
19
  selected?: Snippet<[Item]>;
9
20
  proposal?: Snippet<[Item, {
21
+ popover: PopoverType;
22
+ isSelected: boolean;
23
+ isFocus: boolean;
24
+ }]>;
25
+ proposalAppend?: Snippet<[Item, {
26
+ popover: PopoverType;
10
27
  isSelected: boolean;
11
28
  isFocus: boolean;
12
29
  }]>;
13
30
  field?: RemoteFormField<string>;
14
- value?: string;
31
+ /**
32
+ * L'item, pas sa valeur soumise. Un parent qui le repilote après coup — remise à
33
+ * zéro, changement de jeu d'options — doit le lier par `bind:`: passé en simple
34
+ * prop, un `$bindable` cesse de suivre le parent dès que le composant y a écrit.
35
+ */
36
+ value?: NoInfer<Item>;
37
+ placeholder?: string;
38
+ class?: ClassValue;
39
+ nullable?: boolean;
40
+ hint?: Snippet<[Item | undefined]>;
41
+ /** Rendu à droite du champ de recherche: une action « créer », typiquement. */
42
+ append?: Snippet<[PopoverType]>;
43
+ header?: Snippet<[PopoverType]>;
44
+ onSelect?: (item: NoInfer<Item> | undefined, popover: PopoverType) => void;
45
+ hotKey?: string;
46
+ variant?: "floating" | "block";
47
+ propsLi?: HTMLLiAttributes;
48
+ };
49
+ exports: {
50
+ popover: {
51
+ show: () => void;
52
+ hide: () => void;
53
+ readonly isOpen: boolean;
54
+ content: {
55
+ [x: symbol]: (node: HTMLElement) => () => void;
56
+ popover: "auto" | "hint" | "manual";
57
+ };
58
+ trigger: {
59
+ [x: symbol]: (node: HTMLElement) => () => void;
60
+ };
61
+ };
62
+ command: {
63
+ readonly focusIndex: number;
64
+ selectedIndex: number;
65
+ trigger: {
66
+ [x: symbol]: (node: HTMLElement) => () => void;
67
+ };
68
+ focusTrigger: () => void;
69
+ item: (index: number, scrollMargin?: string) => {
70
+ [x: symbol]: (node: HTMLElement) => () => void;
71
+ };
72
+ };
15
73
  };
16
- exports: {};
17
74
  bindings: "value";
18
75
  slots: {};
19
76
  events: {};
@@ -23,7 +80,31 @@ declare class __sveltets_Render<Item> {
23
80
  events(): ReturnType<typeof $$render<Item>>['events'];
24
81
  slots(): ReturnType<typeof $$render<Item>>['slots'];
25
82
  bindings(): "value";
26
- exports(): {};
83
+ exports(): {
84
+ popover: {
85
+ show: () => void;
86
+ hide: () => void;
87
+ readonly isOpen: boolean;
88
+ content: {
89
+ [x: symbol]: (node: HTMLElement) => () => void;
90
+ popover: "auto" | "hint" | "manual";
91
+ };
92
+ trigger: {
93
+ [x: symbol]: (node: HTMLElement) => () => void;
94
+ };
95
+ };
96
+ command: {
97
+ readonly focusIndex: number;
98
+ selectedIndex: number;
99
+ trigger: {
100
+ [x: symbol]: (node: HTMLElement) => () => void;
101
+ };
102
+ focusTrigger: () => void;
103
+ item: (index: number, scrollMargin?: string) => {
104
+ [x: symbol]: (node: HTMLElement) => () => void;
105
+ };
106
+ };
107
+ };
27
108
  }
28
109
  interface $$IsomorphicComponent {
29
110
  new <Item>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<Item>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<Item>['props']>, ReturnType<__sveltets_Render<Item>['events']>, ReturnType<__sveltets_Render<Item>['slots']>> & {
@@ -11,38 +11,63 @@
11
11
  options: optionsProp,
12
12
  class: klass,
13
13
  multiple,
14
+ variant = 'block',
14
15
  ...props
15
16
  }: {
16
17
  label: string
17
18
  options: Options
18
19
  field?: RemoteFormField<string | 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
+ */
19
24
  value?: string | string[]
25
+ variant?: 'floating' | 'block'
20
26
  } & SelectProps = $props()
21
27
 
22
28
  const options = $derived(parseOptions(optionsProp))
29
+ const inputId = $props.id()
30
+
31
+ // Sans valeur initiale, `as()` rend `value: undefined` tant que le champ n'a rien reçu: le
32
+ // moindre re-rendu du spread — l'`aria-invalid` d'une soumission refusée, typiquement —
33
+ // désélectionne alors tout. La liaison, elle, ne rattrape rien: sa source n'a pas bougé.
34
+ const fieldProps = $derived.by(() => {
35
+ if (!field) return undefined
36
+ if (multiple === true) {
37
+ const initial = Array.isArray(value) ? value : undefined
38
+ return initial ? field.as('select multiple', initial) : field.as('select multiple')
39
+ }
40
+ const initial = typeof value === 'string' ? value : undefined
41
+ return initial === undefined ? field.as('select') : field.as('select', initial)
42
+ })
23
43
  </script>
24
44
 
25
- <div>
26
- <label class="floating-label">
45
+ {#snippet snippetSelect()}
46
+ <select id={inputId} class="select w-full" {...fieldProps} bind:value {...props}>
47
+ {#each options as option (option.value)}
48
+ <option value={option.value} class={['', option.class]} disabled={option.disabled}>
49
+ {#if option.icon}
50
+ <option.icon size={18} opacity={0.8} />
51
+ {/if}
52
+ <span class="grow">{option.label}</span>
53
+ </option>
54
+ {/each}
55
+ </select>
56
+ {/snippet}
57
+
58
+ {#if variant === 'floating'}
59
+ <label class={['floating-label', klass]}>
27
60
  <span>{label}</span>
28
- <select
29
- class={['select', klass]}
30
- {...field?.as(multiple !== true ? 'select' : 'select multiple')}
31
- bind:value
32
- {...props}
33
- >
34
- {#each options as option (option.value)}
35
- <option value={option.value} class={['', option.class]} disabled={option.disabled}>
36
- {#if option.icon}
37
- <option.icon size={18} opacity={0.8} />
38
- {/if}
39
- <span class="grow">{option.label}</span>
40
- </option>
41
- {/each}
42
- </select>
61
+ {@render snippetSelect()}
62
+ <Issues {field} />
43
63
  </label>
44
- <Issues {field} />
45
- </div>
64
+ {:else}
65
+ <fieldset class={['fieldset', klass]}>
66
+ <label class="label text-wrap" for={inputId}>{label}</label>
67
+ {@render snippetSelect()}
68
+ <Issues {field} />
69
+ </fieldset>
70
+ {/if}
46
71
 
47
72
  <style>
48
73
  option::checkmark {
@@ -5,7 +5,12 @@ type $$ComponentProps = {
5
5
  label: string;
6
6
  options: Options;
7
7
  field?: RemoteFormField<string | string[]>;
8
+ /**
9
+ * Sans `field`, la valeur liée. Avec, la valeur initiale du champ — celui-ci prend
10
+ * ensuite le relais et devient la source de vérité.
11
+ */
8
12
  value?: string | string[];
13
+ variant?: 'floating' | 'block';
9
14
  } & SelectProps;
10
15
  declare const InputSelectNative: import("svelte").Component<$$ComponentProps, {}, "value">;
11
16
  type InputSelectNative = ReturnType<typeof InputSelectNative>;
@@ -9,11 +9,17 @@
9
9
  value = $bindable(),
10
10
  type = 'text',
11
11
  class: klass,
12
+ variant = 'block',
12
13
  ...props
13
14
  }: {
14
15
  label: string
15
16
  field?: RemoteFormField<string>
17
+ /**
18
+ * Sans `field`, la valeur liée. Avec, la valeur initiale du champ — celui-ci prend
19
+ * ensuite le relais et devient la source de vérité.
20
+ */
16
21
  value?: string
22
+ variant?: 'floating' | 'block'
17
23
  type?:
18
24
  | 'text'
19
25
  | 'email'
@@ -29,17 +35,41 @@
29
35
  | 'color'
30
36
  | 'select'
31
37
  } & InputProps = $props()
38
+
39
+ const inputId = $props.id()
40
+ const inputProps = $derived({
41
+ id: inputId,
42
+ class: 'input w-full',
43
+ placeholder: variant === 'floating' ? label : '',
44
+ ...props
45
+ })
32
46
  </script>
33
47
 
34
- <label class="floating-label">
35
- <span>{label}</span>
48
+ {#snippet snippetInput()}
36
49
  {#if field}
37
- <input placeholder={label} class={['input', klass]} {...field.as(type)} {...props} />
50
+ {#if value === undefined}
51
+ <input {...inputProps} {...field.as(type)} />
52
+ {:else}
53
+ <input {...inputProps} {...field.as(type, value)} />
54
+ {/if}
38
55
  {:else}
39
- <input placeholder={label} class={['input', klass]} {type} bind:value {...props} />
56
+ <input {...inputProps} {type} bind:value />
40
57
  {/if}
41
- <Issues {field} />
42
- </label>
58
+ {/snippet}
59
+
60
+ {#if variant === 'floating'}
61
+ <label class={['floating-label', klass]}>
62
+ <span>{label}</span>
63
+ {@render snippetInput()}
64
+ <Issues {field} />
65
+ </label>
66
+ {:else}
67
+ <fieldset class={['fieldset', klass]}>
68
+ <label class="label text-wrap" for={inputId}>{label}</label>
69
+ {@render snippetInput()}
70
+ <Issues {field} />
71
+ </fieldset>
72
+ {/if}
43
73
 
44
74
  <style>
45
75
  input[aria-invalid='true'] {
@@ -3,7 +3,12 @@ import type { InputProps } from './type.js';
3
3
  type $$ComponentProps = {
4
4
  label: string;
5
5
  field?: RemoteFormField<string>;
6
+ /**
7
+ * Sans `field`, la valeur liée. Avec, la valeur initiale du champ — celui-ci prend
8
+ * ensuite le relais et devient la source de vérité.
9
+ */
6
10
  value?: string;
11
+ variant?: 'floating' | 'block';
7
12
  type?: 'text' | 'email' | 'password' | 'url' | 'tel' | 'search' | 'date' | 'datetime-local' | 'time' | 'month' | 'week' | 'color' | 'select';
8
13
  } & InputProps;
9
14
  declare const InputString: import("svelte").Component<$$ComponentProps, {}, "value">;
@@ -10,12 +10,18 @@
10
10
  value = $bindable(),
11
11
  class: klass,
12
12
  maxHeight = 200,
13
+ variant = 'block',
13
14
  ...props
14
15
  }: {
15
16
  label: string
16
17
  field?: RemoteFormField<string>
18
+ /**
19
+ * Sans `field`, la valeur liée. Avec, la valeur initiale du champ — celui-ci prend
20
+ * ensuite le relais et devient la source de vérité.
21
+ */
17
22
  value?: string
18
23
  maxHeight?: number
24
+ variant?: 'floating' | 'block'
19
25
  } & TextareaProps = $props()
20
26
 
21
27
  let textarea = $state<HTMLTextAreaElement>()
@@ -24,20 +30,43 @@
24
30
  input: () => field?.value() || value || '',
25
31
  maxHeight: (() => maxHeight)()
26
32
  })
33
+
34
+ const inputId = $props.id()
35
+ const textareaProps = $derived({
36
+ id: inputId,
37
+ class: 'textarea w-full',
38
+ placeholder: variant === 'floating' ? label : '',
39
+ ...props
40
+ })
27
41
  </script>
28
42
 
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>
39
- <Issues {field} />
40
- </label>
43
+ {#snippet snippetInput()}
44
+ {#if field}
45
+ <!-- Pas de `bind:value` ici: avec un `field`, `value` n'est que la valeur initiale,
46
+ comme dans InputString et InputNumber. -->
47
+ <textarea
48
+ bind:this={textarea}
49
+ {...textareaProps}
50
+ {...value === undefined ? field.as('text') : field.as('text', value)}
51
+ ></textarea>
52
+ {:else}
53
+ <textarea bind:this={textarea} {...textareaProps} bind:value></textarea>
54
+ {/if}
55
+ {/snippet}
56
+
57
+ {#if variant === 'floating'}
58
+ <label class={['floating-label', klass]}>
59
+ <span>{label}</span>
60
+ {@render snippetInput()}
61
+ <Issues {field} />
62
+ </label>
63
+ {:else}
64
+ <fieldset class={['fieldset', klass]}>
65
+ <label class="label text-wrap" for={inputId}>{label}</label>
66
+ {@render snippetInput()}
67
+ <Issues {field} />
68
+ </fieldset>
69
+ {/if}
41
70
 
42
71
  <style>
43
72
  textarea[aria-invalid='true'] {