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
@@ -0,0 +1,286 @@
1
+ <script lang="ts" generics="Item">
2
+ import { CheckIcon, ChevronsUpDownIcon, SearchIcon, XIcon } from '@lucide/svelte'
3
+ import type { RemoteFormField } from '@sveltejs/kit'
4
+ import type { Snippet } from 'svelte'
5
+ import type { ClassValue } from 'svelte/elements'
6
+ import { slide } from 'svelte/transition'
7
+ import { tip } from '../action/tip.js'
8
+ import { useCommand } from '../command/useCommand.svelte.js'
9
+ import Loading from '../loading/Loading.svelte'
10
+ import { type PopoverType, usePopover } from '../popover/usePopover.svelte.js'
11
+ import Issues from './Issues.svelte'
12
+ import {
13
+ defaultGetLabel,
14
+ defaultGetValue,
15
+ type ItemsSource,
16
+ useItems
17
+ } from './useItems.svelte.js'
18
+
19
+ let {
20
+ label,
21
+ items,
22
+ getValue = defaultGetValue,
23
+ getLabel = (item) => defaultGetLabel(item, getValue),
24
+ filter,
25
+ searchable,
26
+ debounceMs,
27
+ selected = defaultSnippet,
28
+ proposal = defaultSnippet,
29
+ field,
30
+ value = $bindable([]),
31
+ placeholder = 'Selectionner des valeurs',
32
+ class: klass,
33
+ hint,
34
+ append,
35
+ onSelect,
36
+ hotKey,
37
+ variant = 'block'
38
+ }: {
39
+ /** Un tableau déjà chargé, une fonction (synchrone ou non), ou une remote query. */
40
+ items: ItemsSource<Item>
41
+ label?: string
42
+ getValue?: (item: NoInfer<Item>) => string
43
+ /** Libellé affiché à défaut de snippet, et base du filtre d'une source tableau. */
44
+ getLabel?: (item: NoInfer<Item>) => string
45
+ /** Ne s'applique qu'à une source tableau: une fonction porte son propre filtre. */
46
+ filter?: (item: NoInfer<Item>, search: string) => boolean
47
+ /** Par défaut: toujours pour une fonction, au-delà de 7 entrées pour un tableau. */
48
+ searchable?: boolean
49
+ debounceMs?: number
50
+ selected?: Snippet<[Item]>
51
+ proposal?: Snippet<[Item, { isSelected: boolean; isFocus: boolean }]>
52
+ field?: RemoteFormField<string[]>
53
+ /**
54
+ * Les items, pas leurs valeurs soumises. Un parent qui les repilote après coup doit
55
+ * les lier par `bind:`: passé en simple prop, un `$bindable` cesse de suivre le
56
+ * parent dès que le composant y a écrit.
57
+ */
58
+ value?: NoInfer<Item>[]
59
+ placeholder?: string
60
+ class?: ClassValue
61
+ hint?: Snippet<[Item[]]>
62
+ /** Rendu à droite du champ de recherche: une action « créer », typiquement. */
63
+ append?: Snippet<[PopoverType]>
64
+ onSelect?: (items: NoInfer<Item>[], popover: PopoverType) => void
65
+ hotKey?: string
66
+ variant?: 'floating' | 'block'
67
+ } = $props()
68
+
69
+ const inputId = $props.id()
70
+ const listId = `${inputId}-list`
71
+
72
+ let search = $state('')
73
+ // Une énumération courte n'a que faire d'un champ de recherche; une fonction, elle, ne
74
+ // peut pas s'en passer, puisque le composant ne sait pas la filtrer.
75
+ const isSearchable = $derived(searchable ?? (Array.isArray(items) ? items.length > 7 : true))
76
+ const query = useItems({
77
+ source: () => items,
78
+ search: () => search,
79
+ getLabel: (item) => getLabel(item),
80
+ filter: () => filter,
81
+ debounceMs: (() => debounceMs)()
82
+ })
83
+ const options = $derived(query.current ?? [])
84
+
85
+ // `value` porte les items et non les valeurs soumises: celles-ci s'en dérivent par
86
+ // `getValue`, alors que l'inverse demanderait une requête. C'est aussi ce qui rend
87
+ // l'état initial gratuit — pas de `defaultValue` à réconcilier avec le champ.
88
+ const selectedValues = $derived(value.map(getValue))
89
+
90
+ export const popover = usePopover({
91
+ listenFocus: false,
92
+ listenFocusout: true,
93
+ onShow: () => command.focusTrigger(),
94
+ hotKey: (() => hotKey)()
95
+ })
96
+ // Un multi-select ne se referme pas à chaque choix: `popover.hide()` n'est appelé
97
+ // que par le bouton de fermeture.
98
+ export const command = useCommand({
99
+ isEnable: () => popover.isOpen,
100
+ onSelect: (index) => {
101
+ const item = options[index]
102
+ if (item) toggle(item)
103
+ }
104
+ })
105
+
106
+ function toggle(item: Item) {
107
+ const itemValue = getValue(item)
108
+ value = selectedValues.includes(itemValue)
109
+ ? value.filter((selectedItem) => getValue(selectedItem) !== itemValue)
110
+ : [...value, item]
111
+ commit()
112
+ }
113
+
114
+ function remove(item: Item) {
115
+ const itemValue = getValue(item)
116
+ value = value.filter((selectedItem) => getValue(selectedItem) !== itemValue)
117
+ commit()
118
+ }
119
+
120
+ function commit() {
121
+ // Les cases cachées suffisent à soumettre; `set` garde la validation à jour.
122
+ field?.set(selectedValues)
123
+ onSelect?.(value, popover)
124
+ }
125
+ </script>
126
+
127
+ {#snippet defaultSnippet(item: Item)}
128
+ <span>{getLabel(item)}</span>
129
+ {/snippet}
130
+
131
+ {#snippet triggerCombobox()}
132
+ <!-- Un `div` et non un `button`: les puces portent leur propre bouton de retrait, et
133
+ un bouton ne peut pas en contenir un autre. -->
134
+ <div
135
+ id={inputId}
136
+ role="combobox"
137
+ aria-controls={listId}
138
+ aria-expanded={popover.isOpen}
139
+ aria-haspopup="listbox"
140
+ tabindex="0"
141
+ class={[
142
+ 'input h-auto min-h-(--size) w-full flex-wrap items-center gap-1 py-1',
143
+ 'not-disabled:cursor-pointer',
144
+ field?.issues()?.length && 'input-error',
145
+ klass
146
+ ]}
147
+ onkeydown={(event) => {
148
+ if (event.key !== 'Enter' && event.key !== ' ') return
149
+ event.preventDefault()
150
+ popover.show()
151
+ }}
152
+ {...popover.trigger}
153
+ {...isSearchable ? {} : command.trigger}
154
+ >
155
+ {#if !value.length}
156
+ <span class="opacity-60">{placeholder}</span>
157
+ {:else}
158
+ {#each value as item (getValue(item))}
159
+ <span
160
+ class="badge items-center gap-1 badge-sm"
161
+ transition:slide={{ axis: 'x', duration: 150 }}
162
+ >
163
+ <!-- item.icon not rerender if not wrapped in #key -->
164
+ {#key item}{@render selected(item)}{/key}
165
+ <button
166
+ type="button"
167
+ class="btn btn-circle btn-ghost btn-xs"
168
+ aria-label="Retirer"
169
+ use:tip={{ content: 'Retirer' }}
170
+ onclick={(event) => {
171
+ // Sans quoi le clic remonte au combobox et rouvre le popover.
172
+ event.stopPropagation()
173
+ remove(item)
174
+ }}
175
+ >
176
+ <XIcon size={12} />
177
+ </button>
178
+ </span>
179
+ {/each}
180
+ {/if}
181
+ <ChevronsUpDownIcon class="ml-auto shrink-0" size={14} opacity={0.7} />
182
+ </div>
183
+ {/snippet}
184
+
185
+ {#if field}
186
+ <!-- Une case cochée par valeur: c'est ainsi qu'un champ `string[]` se soumet. Le
187
+ `checked` explicite prime sur celui de `as()`, qui lit `field.value()` — sans lui
188
+ la sélection initiale ne partirait pas tant que rien n'a été cliqué. -->
189
+ {#each selectedValues as selectedValue (selectedValue)}
190
+ <input {...field.as('checkbox', selectedValue)} checked class="hidden" />
191
+ {/each}
192
+ {/if}
193
+
194
+ <div class="contents">
195
+ {#if !label}
196
+ {@render triggerCombobox()}
197
+ {:else if variant === 'floating'}
198
+ <label class="floating-label">
199
+ <span>{label}</span>
200
+ {@render triggerCombobox()}
201
+ </label>
202
+ {:else}
203
+ <fieldset class="fieldset">
204
+ <label class="label text-wrap" for={inputId}>{label}</label>
205
+ {@render triggerCombobox()}
206
+ </fieldset>
207
+ {/if}
208
+
209
+ <div
210
+ {...popover.content}
211
+ class={['my-2 rounded-box border bg-base-100 shadow-xl']}
212
+ style="min-width: anchor-size(width);"
213
+ tabindex="-1"
214
+ >
215
+ {#if isSearchable || value.length || append}
216
+ <div class="sticky top-0 z-10 flex gap-2 bg-base-100/10 p-2 backdrop-blur-md">
217
+ {#if isSearchable}
218
+ <label class="input grow input-ghost input-sm">
219
+ <SearchIcon opacity={0.6} size={20} />
220
+ <input
221
+ type="search"
222
+ placeholder="Recherche"
223
+ autocomplete="off"
224
+ bind:value={search}
225
+ {...command.trigger}
226
+ />
227
+ </label>
228
+ {:else}
229
+ <div class="grow"></div>
230
+ {/if}
231
+ {#if value.length}
232
+ <button
233
+ class="btn btn-square btn-soft btn-sm"
234
+ type="button"
235
+ onclick={() => {
236
+ value = []
237
+ commit()
238
+ popover.hide()
239
+ }}
240
+ use:tip={{ content: 'Tout désélectionner' }}
241
+ >
242
+ <XIcon />
243
+ </button>
244
+ {/if}
245
+ {@render append?.(popover)}
246
+ </div>
247
+ {/if}
248
+
249
+ <ul
250
+ id={listId}
251
+ role="listbox"
252
+ aria-multiselectable="true"
253
+ class={['menu max-h-80 w-full flex-nowrap', (isSearchable || append) && 'pt-0']}
254
+ >
255
+ {#each options as item, index (item)}
256
+ {@const isSelected = selectedValues.includes(getValue(item))}
257
+ {@const isFocus = index === command.focusIndex}
258
+ <li>
259
+ <button
260
+ {...command.item(index, '')}
261
+ class={[isSelected && 'border', isFocus && 'menu-focus', 'scroll-mt-13 scroll-mb-2']}
262
+ type="button"
263
+ tabindex="-1"
264
+ role="option"
265
+ aria-selected={isSelected}
266
+ >
267
+ {@render proposal(item, { isSelected, isFocus })}
268
+ {#if proposal === defaultSnippet && isSelected}
269
+ <CheckIcon class="ml-auto" size={18} />
270
+ {/if}
271
+ </button>
272
+ </li>
273
+ {/each}
274
+ </ul>
275
+
276
+ <Loading {query} />
277
+ </div>
278
+ <Issues {field} />
279
+ {@render hint?.(value)}
280
+ </div>
281
+
282
+ <style>
283
+ div[popover] {
284
+ border-color: color-mix(in oklab, var(--color-base-content) 20%, #0000);
285
+ }
286
+ </style>
@@ -1,27 +1,41 @@
1
- import type { RemoteFormField, RemoteQueryFunction } from '@sveltejs/kit';
1
+ import type { RemoteFormField } from '@sveltejs/kit';
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { ClassValue } from 'svelte/elements';
4
- import { type PopoverType } from '../popover/popover.svelte.js';
4
+ import { type PopoverType } from '../popover/usePopover.svelte.js';
5
+ import { type ItemsSource } from './useItems.svelte.js';
5
6
  declare function $$render<Item>(): {
6
7
  props: {
7
- searchItems: RemoteQueryFunction<{
8
- search: string;
9
- }, Item[]>;
8
+ /** Un tableau déjà chargé, une fonction (synchrone ou non), ou une remote query. */
9
+ items: ItemsSource<Item>;
10
10
  label?: string;
11
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;
12
19
  selected?: Snippet<[Item]>;
13
20
  proposal?: Snippet<[Item, {
14
21
  isSelected: boolean;
15
22
  isFocus: boolean;
16
23
  }]>;
17
- field?: RemoteFormField<string>;
18
- value?: string;
24
+ field?: RemoteFormField<string[]>;
25
+ /**
26
+ * Les items, pas leurs valeurs soumises. Un parent qui les repilote après coup doit
27
+ * les lier par `bind:`: passé en simple prop, un `$bindable` cesse de suivre le
28
+ * parent dès que le composant y a écrit.
29
+ */
30
+ value?: NoInfer<Item>[];
19
31
  placeholder?: string;
20
32
  class?: ClassValue;
21
- nullable?: boolean;
22
- hint?: Snippet<[Item | undefined]>;
23
- onSelect?: (item: NoInfer<Item> | undefined, popover: PopoverType) => void;
33
+ hint?: Snippet<[Item[]]>;
34
+ /** Rendu à droite du champ de recherche: une action « créer », typiquement. */
35
+ append?: Snippet<[PopoverType]>;
36
+ onSelect?: (items: NoInfer<Item>[], popover: PopoverType) => void;
24
37
  hotKey?: string;
38
+ variant?: "floating" | "block";
25
39
  };
26
40
  exports: {
27
41
  popover: {
@@ -90,6 +104,6 @@ interface $$IsomorphicComponent {
90
104
  <Item>(internal: unknown, props: ReturnType<__sveltets_Render<Item>['props']> & {}): ReturnType<__sveltets_Render<Item>['exports']>;
91
105
  z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
92
106
  }
93
- declare const InputRelation: $$IsomorphicComponent;
94
- type InputRelation<Item> = InstanceType<typeof InputRelation<Item>>;
95
- export default InputRelation;
107
+ declare const InputMultiSelect: $$IsomorphicComponent;
108
+ type InputMultiSelect<Item> = InstanceType<typeof InputMultiSelect<Item>>;
109
+ export default InputMultiSelect;
@@ -8,23 +8,53 @@
8
8
  field,
9
9
  value = $bindable(),
10
10
  class: klass,
11
+ variant = 'block',
11
12
  ...props
12
13
  }: {
13
14
  label: string
14
15
  field?: RemoteFormField<number>
16
+ /**
17
+ * Sans `field`, la valeur liée. Avec, la valeur initiale du champ — celui-ci prend
18
+ * ensuite le relais et devient la source de vérité.
19
+ */
15
20
  value?: number
21
+ variant?: 'floating' | 'block'
16
22
  } & InputProps = $props()
23
+
24
+ const inputId = $props.id()
25
+ const inputProps = $derived({
26
+ id: inputId,
27
+ class: 'input w-full',
28
+ placeholder: variant === 'floating' ? label : '',
29
+ ...props
30
+ })
17
31
  </script>
18
32
 
19
- <label class="floating-label">
20
- <span class="label">{label}</span>
33
+ {#snippet snippetInput()}
21
34
  {#if field}
22
- <input placeholder={label} class={['input', klass]} {...field.as('number')} {...props} />
35
+ {#if value === undefined}
36
+ <input {...inputProps} {...field.as('number')} />
37
+ {:else}
38
+ <input {...inputProps} {...field.as('number', value)} />
39
+ {/if}
23
40
  {:else}
24
- <input placeholder={label} class={['input', klass]} type="number" bind:value {...props} />
41
+ <input {...inputProps} type="number" bind:value />
25
42
  {/if}
26
- <Issues {field} />
27
- </label>
43
+ {/snippet}
44
+
45
+ {#if variant === 'floating'}
46
+ <label class={['floating-label', klass]}>
47
+ <span>{label}</span>
48
+ {@render snippetInput()}
49
+ <Issues {field} />
50
+ </label>
51
+ {:else}
52
+ <fieldset class={['fieldset', klass]}>
53
+ <label class="label text-wrap" for={inputId}>{label}</label>
54
+ {@render snippetInput()}
55
+ <Issues {field} />
56
+ </fieldset>
57
+ {/if}
28
58
 
29
59
  <style>
30
60
  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<number>;
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?: number;
11
+ variant?: 'floating' | 'block';
7
12
  } & InputProps;
8
13
  declare const InputNumber: import("svelte").Component<$$ComponentProps, {}, "value">;
9
14
  type InputNumber = ReturnType<typeof InputNumber>;
@@ -19,30 +19,51 @@
19
19
  </script>
20
20
 
21
21
  <div class="flex flex-col gap-1">
22
- <span class="label px-3 text-sm">{label}</span>
22
+ <span class="label px-3 text-sm text-wrap">{label}</span>
23
23
  <div class="join join-vertical">
24
24
  {#each options as option (option.value)}
25
25
  {@const { class: klass, ...props } = option}
26
- <label class="input join-item not-disabled:cursor-pointer not-disabled:hover:bg-base-200">
27
- {#if option.icon}
28
- <option.icon size={18} opacity={0.8} />
29
- {/if}
30
- <span>{option.label}</span>
31
- {#if field}
32
- <input
33
- {...field.as('radio', option.value)}
34
- class={['radio ml-auto radio-sm', klass]}
35
- {...props}
36
- />
37
- {:else}
38
- <input
39
- type="radio"
40
- checked={value === option.value}
41
- onchange={() => (value = option.value)}
42
- class={['radio ml-auto radio-sm', klass]}
43
- {...props}
44
- />
45
- {/if}
26
+ {@const checked = field?.value() === option.value || value === option.value}
27
+ <label
28
+ class={[
29
+ 'input join-item not-disabled:cursor-pointer not-disabled:hover:bg-base-200',
30
+ 'group h-auto min-h-10'
31
+ ]}
32
+ >
33
+ <div class="flex h-(--size) grow items-center gap-2">
34
+ {#if option.icon}
35
+ <option.icon size={18} opacity={0.8} />
36
+ {/if}
37
+ <span class="grow">{option.label}</span>
38
+ {#if field}
39
+ <input {...field.as('radio', option.value)} class={['w-0']} {...props} />
40
+ {:else}
41
+ <input
42
+ type="radio"
43
+ {checked}
44
+ onchange={() => (value = option.value)}
45
+ class={['w-0']}
46
+ {...props}
47
+ />
48
+ {/if}
49
+ <div
50
+ class={[
51
+ 'ml-auto h-5 w-5 rounded-full outline',
52
+ 'bg-base-100',
53
+ !checked && 'outline-base-content/20',
54
+ klass
55
+ ]}
56
+ >
57
+ <div
58
+ class={[
59
+ 'grid h-5 w-5 place-content-center rounded-full',
60
+ 'bg-base-content',
61
+ 'ease scale-0 opacity-0',
62
+ checked && 'scale-[80%] opacity-100'
63
+ ]}
64
+ ></div>
65
+ </div>
66
+ </div>
46
67
  </label>
47
68
  {/each}
48
69
  </div>
@@ -50,7 +71,17 @@
50
71
  </div>
51
72
 
52
73
  <style>
53
- input {
54
- --input-color: var(--color-base-content);
74
+ .ease {
75
+ transition-property: translate background-color opacity;
76
+ transition-timing-function: cubic-bezier(0.275, 0.485, 0.515, 1.45);
77
+ transition-duration: 200ms;
78
+ }
79
+
80
+ label:has(input[aria-invalid='true']) {
81
+ --input-color: var(--color-error);
82
+ }
83
+ label:has(input:disabled) {
84
+ cursor: not-allowed;
85
+ border-color: var(--input-color);
55
86
  }
56
87
  </style>
@@ -7,24 +7,39 @@
7
7
  label,
8
8
  field,
9
9
  value = $bindable(),
10
+ name,
10
11
  class: klass,
11
12
  ...props
12
13
  }: {
13
14
  label: string
14
15
  field?: RemoteFormField<number>
16
+ /**
17
+ * Sans `field`, la valeur liée. Avec, la valeur initiale du champ — celui-ci prend
18
+ * ensuite le relais et devient la source de vérité.
19
+ */
15
20
  value?: number
21
+ /** Sans `field`, ce qui nomme la valeur soumise. Avec, `field.as()` s'en charge. */
22
+ name?: string
16
23
  } & InputProps = $props()
17
24
  </script>
18
25
 
19
26
  <label>
20
27
  <div class="flex gap-2 px-3 text-sm">
21
- <span class="label">{label}</span>
22
- <span class="ml-auto">{field?.value() ?? value}</span>
28
+ <span class="label text-wrap">{label}</span>
29
+ <span class="label ml-auto">{field?.value() ?? value}</span>
23
30
  </div>
24
31
  {#if field}
25
- <input class={['range range-sm', klass]} {...field.as('range')} {...props} />
32
+ <!-- `props` avant `field.as()`: le navigateur ramène la valeur d'un `range` sur la grille
33
+ `min`/`max`/`step` à chaque écriture. Posée en premier, elle est arrondie selon les
34
+ bornes par défaut (0–100, pas de 1), et poser les vraies bornes ensuite l'y recale une
35
+ seconde fois — au montage côté client, 0.75 finissait sur le minimum. -->
36
+ <input
37
+ class={['range w-full range-sm', klass]}
38
+ {...props}
39
+ {...value === undefined ? field.as('range') : field.as('range', value)}
40
+ />
26
41
  {:else}
27
- <input class={['range range-sm', klass]} type="range" bind:value {...props} />
42
+ <input class={['range w-full range-sm', klass]} type="range" {name} bind:value {...props} />
28
43
  {/if}
29
44
  <Issues {field} />
30
45
  </label>
@@ -3,7 +3,13 @@ import type { InputProps } from './type.js';
3
3
  type $$ComponentProps = {
4
4
  label: string;
5
5
  field?: RemoteFormField<number>;
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?: number;
11
+ /** Sans `field`, ce qui nomme la valeur soumise. Avec, `field.as()` s'en charge. */
12
+ name?: string;
7
13
  } & InputProps;
8
14
  declare const InputRange: import("svelte").Component<$$ComponentProps, {}, "value">;
9
15
  type InputRange = ReturnType<typeof InputRange>;