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