fuma 2.1.6 → 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.
- 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 +25 -7
- package/dist/input/InputBoolean.svelte.d.ts +4 -0
- package/dist/input/InputCheckboxes.svelte +116 -0
- package/dist/input/InputCheckboxes.svelte.d.ts +16 -0
- package/dist/input/InputDateTime.svelte +194 -0
- package/dist/input/InputDateTime.svelte.d.ts +23 -0
- package/dist/input/InputMultiSelect.svelte +286 -0
- package/dist/input/{InputRelation.svelte.d.ts → InputMultiSelect.svelte.d.ts} +27 -13
- package/dist/input/InputNumber.svelte +36 -6
- package/dist/input/InputNumber.svelte.d.ts +5 -0
- package/dist/input/InputRadio.svelte +54 -23
- package/dist/input/InputRange.svelte +19 -4
- package/dist/input/InputRange.svelte.d.ts +6 -0
- package/dist/input/InputSelect.svelte +204 -62
- package/dist/input/InputSelect.svelte.d.ts +87 -6
- package/dist/input/InputSelectNative.svelte +44 -19
- package/dist/input/InputSelectNative.svelte.d.ts +5 -0
- package/dist/input/InputString.svelte +14 -6
- package/dist/input/InputString.svelte.d.ts +4 -0
- package/dist/input/InputTextarea.svelte +41 -12
- package/dist/input/InputTextarea.svelte.d.ts +5 -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 +4 -2
- 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 +7 -10
- package/dist/ui/table/Table.svelte.d.ts +1 -1
- package/dist/ui/table/TableFieldsEdition.svelte +24 -22
- package/dist/ui/table/TableHead.svelte +5 -1
- package/dist/ui/table/TableViewSelect.svelte +52 -47
- package/dist/ui/table/cell/TableCellBoolean.svelte +2 -2
- package/dist/ui/table/head/TableHeadDate.svelte +62 -55
- package/dist/ui/table/head/TableHeadNumber.svelte +55 -19
- package/dist/ui/table/head/TableHeadSelect.svelte +30 -27
- package/dist/ui/table/head/TableHeadString.svelte +11 -7
- package/dist/ui/table/index.d.ts +1 -0
- package/dist/ui/table/index.js +1 -0
- package/package.json +7 -4
- package/dist/input/InputRelation.svelte +0 -200
- package/dist/popover/popover.svelte.js +0 -106
- /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,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
|
|
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;
|
|
20
32
|
class?: ClassValue;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
94
|
-
type
|
|
95
|
-
export default
|
|
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
|
-
|
|
20
|
-
<span class="label">{label}</span>
|
|
33
|
+
{#snippet snippetInput()}
|
|
21
34
|
{#if field}
|
|
22
|
-
|
|
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
|
|
41
|
+
<input {...inputProps} type="number" bind:value />
|
|
25
42
|
{/if}
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
{
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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>;
|