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
package/dist/_doc/data.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export type User = {
|
|
|
12
12
|
subscribeAt: string;
|
|
13
13
|
gender: UserGender;
|
|
14
14
|
gender2: UserGender;
|
|
15
|
+
friendIds: string[];
|
|
15
16
|
loveId?: string | null;
|
|
16
17
|
};
|
|
17
18
|
export declare const userGenders: readonly ["male", "female", "other"];
|
|
@@ -21,7 +22,7 @@ export declare const schemaUser: z.ZodObject<{
|
|
|
21
22
|
age: z.ZodNumber;
|
|
22
23
|
level: z.ZodNumber;
|
|
23
24
|
cv: z.ZodString;
|
|
24
|
-
isValided: z.ZodLiteral<true
|
|
25
|
+
isValided: z.ZodPipe<z.ZodOptional<z.ZodBoolean>, z.ZodLiteral<true>>;
|
|
25
26
|
subscribeAt: z.z.ZodISODate;
|
|
26
27
|
gender: z.ZodEnum<{
|
|
27
28
|
other: "other";
|
|
@@ -33,6 +34,7 @@ export declare const schemaUser: z.ZodObject<{
|
|
|
33
34
|
female: "female";
|
|
34
35
|
male: "male";
|
|
35
36
|
}>;
|
|
37
|
+
friendIds: z.ZodArray<z.ZodString>;
|
|
36
38
|
loveId: z.ZodOptional<z.ZodString>;
|
|
37
39
|
}, z.z.core.$strip>;
|
|
38
40
|
export declare const userGenderOptions: {
|
package/dist/_doc/data.js
CHANGED
|
@@ -12,7 +12,8 @@ function createRandomUser() {
|
|
|
12
12
|
isValided: Math.random() < 0.5,
|
|
13
13
|
subscribeAt: faker.date.past().toISOString(),
|
|
14
14
|
gender: faker.person.sexType(),
|
|
15
|
-
gender2: faker.person.sexType()
|
|
15
|
+
gender2: faker.person.sexType(),
|
|
16
|
+
friendIds: []
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
19
|
export const users = faker.helpers.multiple(createRandomUser, {
|
|
@@ -25,10 +26,13 @@ export const schemaUser = z.object({
|
|
|
25
26
|
age: z.number().min(18).max(120),
|
|
26
27
|
level: z.number().min(0).max(100),
|
|
27
28
|
cv: z.string(),
|
|
28
|
-
|
|
29
|
+
// Une case non cochée n'envoie rien: `form()` de kit refuse au typage tout booléen non
|
|
30
|
+
// optionnel. L'entrée est donc optionnelle, et le `pipe` rétablit l'exigence à l'exécution.
|
|
31
|
+
isValided: z.boolean().optional().pipe(z.literal(true, 'Validation requise')),
|
|
29
32
|
subscribeAt: z.iso.date(),
|
|
30
33
|
gender: z.enum(userGenders),
|
|
31
34
|
gender2: z.enum(userGenders),
|
|
35
|
+
friendIds: z.array(z.string()),
|
|
32
36
|
loveId: z.string().optional()
|
|
33
37
|
// tags: z.array(z.enum(userTags)).default([]),
|
|
34
38
|
});
|
package/dist/command/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from './
|
|
1
|
+
export * from './useCommand.svelte.js';
|
package/dist/command/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from './
|
|
1
|
+
export * from './useCommand.svelte.js';
|
|
@@ -8,7 +8,7 @@ export function useCommand({ onSelect, onFocus, isEnable = () => true, hotKey }
|
|
|
8
8
|
function onKeydown(event) {
|
|
9
9
|
if (!isEnable())
|
|
10
10
|
return;
|
|
11
|
-
if (event.key === 'Enter'
|
|
11
|
+
if (event.key === 'Enter') {
|
|
12
12
|
event.preventDefault();
|
|
13
13
|
onSelect?.(focusIndex);
|
|
14
14
|
selectedIndex = focusIndex;
|
|
@@ -3,19 +3,29 @@
|
|
|
3
3
|
import type { RemoteFormField } from '@sveltejs/kit'
|
|
4
4
|
import Issues from './Issues.svelte'
|
|
5
5
|
import type { InputProps } from './type.js'
|
|
6
|
+
import type { Snippet } from 'svelte'
|
|
6
7
|
|
|
7
8
|
let {
|
|
8
9
|
field,
|
|
9
10
|
label,
|
|
10
11
|
checked = $bindable(),
|
|
11
12
|
hint,
|
|
13
|
+
labelAppend,
|
|
12
14
|
variant = 'checkbox',
|
|
15
|
+
class: klass,
|
|
16
|
+
disabled,
|
|
13
17
|
...props
|
|
14
18
|
}: {
|
|
15
19
|
label: string
|
|
16
20
|
field?: RemoteFormField<boolean>
|
|
21
|
+
/**
|
|
22
|
+
* Sans `field`, l'état lié. Avec, l'état initial du champ — celui-ci prend ensuite
|
|
23
|
+
* le relais et devient la source de vérité.
|
|
24
|
+
*/
|
|
17
25
|
checked?: boolean
|
|
18
26
|
hint?: string
|
|
27
|
+
/** Rendu à droite du libellé, dans le `label`: un clic dessus bascule la case. */
|
|
28
|
+
labelAppend?: Snippet
|
|
19
29
|
variant?: 'checkbox' | 'switch'
|
|
20
30
|
} & InputProps = $props()
|
|
21
31
|
|
|
@@ -26,15 +36,27 @@
|
|
|
26
36
|
<label
|
|
27
37
|
class={[
|
|
28
38
|
'input block not-disabled:cursor-pointer not-disabled:hover:bg-base-200',
|
|
29
|
-
'group h-auto min-h-10'
|
|
39
|
+
'group h-auto min-h-10 w-full',
|
|
40
|
+
klass
|
|
30
41
|
]}
|
|
31
42
|
>
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
<!-- `.input` de daisyUI pose `white-space: nowrap`: sans `text-wrap`, un libellé un peu
|
|
44
|
+
long déborde de la ligne au lieu de passer à la suivante. La hauteur suit donc le
|
|
45
|
+
contenu, `--size` n'étant plus qu'un plancher. -->
|
|
46
|
+
<div class="flex min-h-(--size) items-center gap-2 py-1.5">
|
|
47
|
+
<div class="grow text-wrap">{label}</div>
|
|
48
|
+
{@render labelAppend?.()}
|
|
34
49
|
{#if field}
|
|
35
|
-
|
|
50
|
+
<!-- `as('checkbox')` sans valeur pose `checked: undefined`: la case d'un champ
|
|
51
|
+
déjà à `true` partirait décochée, alors que la coche dessinée, elle, suit
|
|
52
|
+
`checked`. Les deux ne se contredisent que si l'état initial n'est pas transmis. -->
|
|
53
|
+
{#if checked === undefined}
|
|
54
|
+
<input {...field.as('checkbox')} class={['peer w-0']} {disabled} {...props} />
|
|
55
|
+
{:else}
|
|
56
|
+
<input {...field.as('checkbox', checked)} class={['peer w-0']} {disabled} {...props} />
|
|
57
|
+
{/if}
|
|
36
58
|
{:else}
|
|
37
|
-
<input type="checkbox" class={['peer w-0']} bind:checked {...props} />
|
|
59
|
+
<input type="checkbox" class={['peer w-0']} bind:checked {disabled} {...props} />
|
|
38
60
|
{/if}
|
|
39
61
|
{#if variant === 'checkbox'}
|
|
40
62
|
{@render variantCheckbox()}
|
|
@@ -53,20 +75,27 @@
|
|
|
53
75
|
</div>
|
|
54
76
|
{/if}
|
|
55
77
|
</label>
|
|
78
|
+
{#if field && !isChecked && !disabled}
|
|
79
|
+
<!-- Une case décochée ne transmet rien: sans ce relais, le serveur ne distinguerait pas
|
|
80
|
+
« décochée » de « champ non rendu », et une valeur ne pourrait jamais repasser à faux.
|
|
81
|
+
Il porte la clé de la case, d'où l'exclusion mutuelle — SvelteKit refuse un formulaire
|
|
82
|
+
où une clé apparaît deux fois. Un contrôle désactivé, lui, ne transmet toujours rien. -->
|
|
83
|
+
<input {...field.as('hidden', false)} />
|
|
84
|
+
{/if}
|
|
56
85
|
<Issues {field} />
|
|
57
86
|
</div>
|
|
58
87
|
|
|
59
88
|
{#snippet variantCheckbox()}
|
|
60
89
|
<!-- https://developer.mozilla.org/en-US/docs/Web/CSS/corner-shape -->
|
|
61
|
-
<div class={['h-5 w-5
|
|
90
|
+
<div class={['squircle', 'h-5 w-5 outline', 'bg-base-100']}>
|
|
62
91
|
<div
|
|
63
92
|
class={[
|
|
64
|
-
'squircle
|
|
93
|
+
'squircle',
|
|
94
|
+
'grid h-5 w-5 place-content-center',
|
|
65
95
|
'bg-base-content',
|
|
66
96
|
'ease scale-0 opacity-0',
|
|
67
97
|
isChecked && 'scale-[80%] opacity-100'
|
|
68
98
|
]}
|
|
69
|
-
style="corner-shape: squircle"
|
|
70
99
|
>
|
|
71
100
|
<CheckIcon size={18} class={['stroke-base-100']} strokeWidth={4} />
|
|
72
101
|
</div>
|
|
@@ -93,6 +122,10 @@
|
|
|
93
122
|
transition-timing-function: cubic-bezier(0.275, 0.485, 0.515, 1.45);
|
|
94
123
|
transition-duration: 200ms;
|
|
95
124
|
}
|
|
125
|
+
.squircle {
|
|
126
|
+
corner-shape: squircle;
|
|
127
|
+
border-radius: 100%;
|
|
128
|
+
}
|
|
96
129
|
|
|
97
130
|
label:has(input[aria-invalid='true']) {
|
|
98
131
|
--input-color: var(--color-error);
|
|
@@ -1,10 +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<boolean>;
|
|
7
|
+
/**
|
|
8
|
+
* Sans `field`, l'état lié. Avec, l'état initial du champ — celui-ci prend ensuite
|
|
9
|
+
* le relais et devient la source de vérité.
|
|
10
|
+
*/
|
|
6
11
|
checked?: boolean;
|
|
7
12
|
hint?: string;
|
|
13
|
+
/** Rendu à droite du libellé, dans le `label`: un clic dessus bascule la case. */
|
|
14
|
+
labelAppend?: Snippet;
|
|
8
15
|
variant?: 'checkbox' | 'switch';
|
|
9
16
|
} & InputProps;
|
|
10
17
|
declare const InputBoolean: import("svelte").Component<$$ComponentProps, {}, "checked">;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { CheckIcon } from '@lucide/svelte'
|
|
3
|
+
import type { RemoteFormField } from '@sveltejs/kit'
|
|
4
|
+
import { type Options, parseOptions } from '../utils/index.js'
|
|
5
|
+
import Issues from './Issues.svelte'
|
|
6
|
+
import type { InputProps } from './type.js'
|
|
7
|
+
import type { Snippet } from 'svelte'
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
field,
|
|
11
|
+
label,
|
|
12
|
+
value = $bindable([]),
|
|
13
|
+
options: optionsProp,
|
|
14
|
+
labelAppend,
|
|
15
|
+
...props
|
|
16
|
+
}: {
|
|
17
|
+
field?: RemoteFormField<string[]>
|
|
18
|
+
label: string
|
|
19
|
+
/**
|
|
20
|
+
* Sans `field`, les valeurs liées. Avec, les valeurs initiales: les cases restent
|
|
21
|
+
* pilotées par `value`, `field.as('checkbox', …)` ne servant qu'à les nommer.
|
|
22
|
+
*/
|
|
23
|
+
value?: string[]
|
|
24
|
+
options: Options
|
|
25
|
+
/** Rendu à droite du libellé. */
|
|
26
|
+
labelAppend?: Snippet
|
|
27
|
+
} & InputProps = $props()
|
|
28
|
+
|
|
29
|
+
const options = $derived(parseOptions(optionsProp))
|
|
30
|
+
|
|
31
|
+
function toggle(optionValue: string, checked: boolean) {
|
|
32
|
+
value = checked ? [...value, optionValue] : value.filter((v) => v !== optionValue)
|
|
33
|
+
// Les cases suffisent à soumettre; `set` garde la validation du formulaire à jour.
|
|
34
|
+
field?.set(value)
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<div class="flex flex-col gap-1">
|
|
39
|
+
<span class="label text-xs text-wrap">
|
|
40
|
+
<span>{label}</span>
|
|
41
|
+
{@render labelAppend?.()}
|
|
42
|
+
</span>
|
|
43
|
+
<div class="join join-vertical">
|
|
44
|
+
{#each options as option (option.value)}
|
|
45
|
+
{@const {
|
|
46
|
+
class: klass,
|
|
47
|
+
value: optionValue,
|
|
48
|
+
label: optionLabel,
|
|
49
|
+
icon: Icon,
|
|
50
|
+
...optionProps
|
|
51
|
+
} = option}
|
|
52
|
+
{@const checked = value.includes(optionValue)}
|
|
53
|
+
<label
|
|
54
|
+
class={[
|
|
55
|
+
'input join-item not-disabled:cursor-pointer not-disabled:hover:bg-base-200',
|
|
56
|
+
'group h-auto min-h-10'
|
|
57
|
+
]}
|
|
58
|
+
>
|
|
59
|
+
<div class="flex h-(--size) grow items-center gap-2">
|
|
60
|
+
{#if Icon}
|
|
61
|
+
<Icon size={18} opacity={0.8} />
|
|
62
|
+
{/if}
|
|
63
|
+
<span class="grow">{optionLabel}</span>
|
|
64
|
+
<!-- Le `checked` explicite prime sur celui de `as()`, qui lit `field.value()`:
|
|
65
|
+
sans lui les valeurs initiales ne partiraient pas tant que rien n'a été
|
|
66
|
+
coché, et le visuel ci-dessous s'en écarterait. -->
|
|
67
|
+
<input
|
|
68
|
+
{...field?.as('checkbox', optionValue)}
|
|
69
|
+
type="checkbox"
|
|
70
|
+
{checked}
|
|
71
|
+
onchange={({ currentTarget }) => toggle(optionValue, currentTarget.checked)}
|
|
72
|
+
class={['w-0']}
|
|
73
|
+
{...props}
|
|
74
|
+
{...optionProps}
|
|
75
|
+
/>
|
|
76
|
+
<!-- https://developer.mozilla.org/en-US/docs/Web/CSS/corner-shape -->
|
|
77
|
+
<div
|
|
78
|
+
class={[
|
|
79
|
+
'squircle',
|
|
80
|
+
'ml-auto h-5 w-5 outline',
|
|
81
|
+
'bg-base-100',
|
|
82
|
+
!checked && 'outline-base-content/20',
|
|
83
|
+
klass
|
|
84
|
+
]}
|
|
85
|
+
>
|
|
86
|
+
<div
|
|
87
|
+
class={[
|
|
88
|
+
'squircle',
|
|
89
|
+
'grid h-5 w-5 place-content-center',
|
|
90
|
+
'bg-base-content',
|
|
91
|
+
'ease scale-0 opacity-0',
|
|
92
|
+
checked && 'scale-[80%] opacity-100'
|
|
93
|
+
]}
|
|
94
|
+
>
|
|
95
|
+
<CheckIcon size={18} class={['stroke-base-100']} strokeWidth={4} />
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</label>
|
|
100
|
+
{/each}
|
|
101
|
+
</div>
|
|
102
|
+
<Issues {field} />
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<style>
|
|
106
|
+
.ease {
|
|
107
|
+
transition-property: translate background-color opacity;
|
|
108
|
+
transition-timing-function: cubic-bezier(0.275, 0.485, 0.515, 1.45);
|
|
109
|
+
transition-duration: 200ms;
|
|
110
|
+
}
|
|
111
|
+
.squircle {
|
|
112
|
+
corner-shape: squircle;
|
|
113
|
+
border-radius: 100%;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
label:has(input[aria-invalid='true']) {
|
|
117
|
+
--input-color: var(--color-error);
|
|
118
|
+
}
|
|
119
|
+
label:has(input:disabled) {
|
|
120
|
+
cursor: not-allowed;
|
|
121
|
+
border-color: var(--input-color);
|
|
122
|
+
}
|
|
123
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { RemoteFormField } from '@sveltejs/kit';
|
|
2
|
+
import { type Options } from '../utils/index.js';
|
|
3
|
+
import type { InputProps } from './type.js';
|
|
4
|
+
import type { Snippet } from 'svelte';
|
|
5
|
+
type $$ComponentProps = {
|
|
6
|
+
field?: RemoteFormField<string[]>;
|
|
7
|
+
label: string;
|
|
8
|
+
/**
|
|
9
|
+
* Sans `field`, les valeurs liées. Avec, les valeurs initiales: les cases restent
|
|
10
|
+
* pilotées par `value`, `field.as('checkbox', …)` ne servant qu'à les nommer.
|
|
11
|
+
*/
|
|
12
|
+
value?: string[];
|
|
13
|
+
options: Options;
|
|
14
|
+
/** Rendu à droite du libellé. */
|
|
15
|
+
labelAppend?: Snippet;
|
|
16
|
+
} & InputProps;
|
|
17
|
+
declare const InputCheckboxes: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
18
|
+
type InputCheckboxes = ReturnType<typeof InputCheckboxes>;
|
|
19
|
+
export default InputCheckboxes;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { MinusIcon, PlusIcon } from '@lucide/svelte'
|
|
3
|
+
import type { RemoteFormField } from '@sveltejs/kit'
|
|
4
|
+
import dayjs, { type Dayjs } from 'dayjs'
|
|
5
|
+
import timezonePlugin from 'dayjs/plugin/timezone.js'
|
|
6
|
+
import utcPlugin from 'dayjs/plugin/utc.js'
|
|
7
|
+
import Issues from './Issues.svelte'
|
|
8
|
+
import type { InputProps } from './type.js'
|
|
9
|
+
import type { Snippet } from 'svelte'
|
|
10
|
+
|
|
11
|
+
dayjs.extend(utcPlugin)
|
|
12
|
+
dayjs.extend(timezonePlugin)
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
label,
|
|
16
|
+
field,
|
|
17
|
+
name,
|
|
18
|
+
value = $bindable(),
|
|
19
|
+
timezone,
|
|
20
|
+
layout = 'split',
|
|
21
|
+
variant = 'block',
|
|
22
|
+
hint,
|
|
23
|
+
labelAppend,
|
|
24
|
+
transform = (v) => v,
|
|
25
|
+
onchange,
|
|
26
|
+
class: klass,
|
|
27
|
+
...props
|
|
28
|
+
}: {
|
|
29
|
+
label: string
|
|
30
|
+
/** La date part en ISO dans un champ caché: au schéma de la reconstruire. */
|
|
31
|
+
field?: RemoteFormField<string>
|
|
32
|
+
/** Sans `field`, le nom du champ caché. Sans l'un ni l'autre, rien n'est soumis. */
|
|
33
|
+
name?: string
|
|
34
|
+
value?: Date | null
|
|
35
|
+
/** Zone IANA d'affichage et de saisie; à défaut, celle du navigateur. */
|
|
36
|
+
timezone?: string
|
|
37
|
+
/** `split`: l'heure au-dessus de la date, avec ses boutons ±jour. */
|
|
38
|
+
layout?: 'split' | 'datetime'
|
|
39
|
+
/** Sans effet sur `split`, dont la ligne de label porte le `hint`. */
|
|
40
|
+
variant?: 'floating' | 'block'
|
|
41
|
+
hint?: string
|
|
42
|
+
/** Rendu à droite du libellé, après le `hint`. */
|
|
43
|
+
labelAppend?: Snippet
|
|
44
|
+
/** Ajuste la valeur avant qu'elle ne soit posée — borne, arrondi, décalage. */
|
|
45
|
+
transform?: (value: Date) => Date
|
|
46
|
+
onchange?: (value: Date) => void
|
|
47
|
+
} & InputProps = $props()
|
|
48
|
+
|
|
49
|
+
const inputId = $props.id()
|
|
50
|
+
const inputProps = $derived({ id: inputId, class: 'input w-full', ...props })
|
|
51
|
+
|
|
52
|
+
const current = $derived.by(() => {
|
|
53
|
+
if (!value) return null
|
|
54
|
+
const date = inZone(dayjs(value))
|
|
55
|
+
// Une date illisible se traite comme une absence de valeur: `format` rendrait
|
|
56
|
+
// « Invalid Date » dans les champs, et `toJSON` un `null` que le champ caché refuse.
|
|
57
|
+
return date.isValid() ? date : null
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
function inZone(date: Dayjs) {
|
|
61
|
+
return timezone ? date.tz(timezone) : date
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Une saisie partielle sur un champ vide se raccroche à maintenant. */
|
|
65
|
+
function base() {
|
|
66
|
+
return current ?? inZone(dayjs())
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function setTime(time: string) {
|
|
70
|
+
if (!time) return
|
|
71
|
+
const [h, m] = time.split(':').map(Number)
|
|
72
|
+
setValue(base().set('h', h).set('m', m))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function setDate(date: string) {
|
|
76
|
+
if (!date) return
|
|
77
|
+
const [y, m, d] = date.split('-').map(Number)
|
|
78
|
+
setValue(
|
|
79
|
+
base()
|
|
80
|
+
.set('y', y)
|
|
81
|
+
.set('M', m - 1)
|
|
82
|
+
.set('D', d)
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function setDateTime(datetime: string) {
|
|
87
|
+
if (!datetime) return
|
|
88
|
+
const [date, time] = datetime.split('T')
|
|
89
|
+
const [y, M, d] = date.split('-').map(Number)
|
|
90
|
+
const [h, m] = time.split(':').map(Number)
|
|
91
|
+
setValue(
|
|
92
|
+
base()
|
|
93
|
+
.set('y', y)
|
|
94
|
+
.set('M', M - 1)
|
|
95
|
+
.set('D', d)
|
|
96
|
+
.set('h', h)
|
|
97
|
+
.set('m', m)
|
|
98
|
+
.startOf('m')
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function setValue(newValue: Dayjs) {
|
|
103
|
+
const date = transform(newValue.toDate())
|
|
104
|
+
value = date
|
|
105
|
+
// Le champ caché suffit à soumettre; `set` garde la validation du formulaire à jour.
|
|
106
|
+
field?.set(date.toJSON())
|
|
107
|
+
onchange?.(date)
|
|
108
|
+
}
|
|
109
|
+
</script>
|
|
110
|
+
|
|
111
|
+
<!-- Par `current` et non `value`: la valeur passée n'est pas toujours la `Date` annoncée par le
|
|
112
|
+
type — une date qui a fait l'aller-retour par JSON revient en chaîne ISO. -->
|
|
113
|
+
{#if current}
|
|
114
|
+
{#if field}
|
|
115
|
+
<input {...field.as('hidden', current.toJSON())} />
|
|
116
|
+
{:else if name}
|
|
117
|
+
<input type="hidden" {name} value={current.toJSON()} />
|
|
118
|
+
{/if}
|
|
119
|
+
{/if}
|
|
120
|
+
|
|
121
|
+
{#snippet inputDateTime()}
|
|
122
|
+
<input
|
|
123
|
+
{...inputProps}
|
|
124
|
+
type="datetime-local"
|
|
125
|
+
value={current ? current.format('YYYY-MM-DDTHH:mm') : ''}
|
|
126
|
+
oninput={(event) => setDateTime(event.currentTarget.value)}
|
|
127
|
+
/>
|
|
128
|
+
{/snippet}
|
|
129
|
+
|
|
130
|
+
{#snippet inputSplit()}
|
|
131
|
+
<div>
|
|
132
|
+
<input
|
|
133
|
+
{...inputProps}
|
|
134
|
+
type="time"
|
|
135
|
+
value={current ? current.format('HH:mm') : ''}
|
|
136
|
+
oninput={(event) => setTime(event.currentTarget.value)}
|
|
137
|
+
/>
|
|
138
|
+
|
|
139
|
+
<div class="join mt-1 flex w-full">
|
|
140
|
+
<!-- `preventDefault` sur le `mousedown`: le bouton ne prend pas le focus, donc aucun
|
|
141
|
+
`focusout` n'est émis. Dans un popover, ce focus qui se déplace est ce qui déclenche
|
|
142
|
+
la fermeture; le focus reste sur le champ, ce qui permet aussi d'enchaîner les pas. -->
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
class="btn join-item btn-square bg-base-200/30 btn-xs"
|
|
146
|
+
disabled={!current}
|
|
147
|
+
onmousedown={(event) => event.preventDefault()}
|
|
148
|
+
onclick={() => current && setValue(current.add(-1, 'day'))}
|
|
149
|
+
>
|
|
150
|
+
<MinusIcon size={12} />
|
|
151
|
+
</button>
|
|
152
|
+
<input
|
|
153
|
+
type="date"
|
|
154
|
+
class="border-soft input join-item border-x-0 input-ghost input-xs"
|
|
155
|
+
value={current ? current.format('YYYY-MM-DD') : ''}
|
|
156
|
+
oninput={(event) => setDate(event.currentTarget.value)}
|
|
157
|
+
/>
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
class="btn join-item btn-square bg-base-200/30 btn-xs"
|
|
161
|
+
disabled={!current}
|
|
162
|
+
onmousedown={(event) => event.preventDefault()}
|
|
163
|
+
onclick={() => current && setValue(current.add(1, 'day'))}
|
|
164
|
+
>
|
|
165
|
+
<PlusIcon size={12} />
|
|
166
|
+
</button>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
{/snippet}
|
|
170
|
+
|
|
171
|
+
{#if layout === 'datetime' && variant === 'floating'}
|
|
172
|
+
<label class={['floating-label', klass]}>
|
|
173
|
+
<span>{label}</span>
|
|
174
|
+
{@render inputDateTime()}
|
|
175
|
+
<Issues {field} />
|
|
176
|
+
</label>
|
|
177
|
+
{:else}
|
|
178
|
+
<fieldset class={['fieldset', klass]}>
|
|
179
|
+
<label class="label text-wrap" for={inputId}>
|
|
180
|
+
<span>{label}</span>
|
|
181
|
+
{#if hint}
|
|
182
|
+
<span class="ml-auto">{hint}</span>
|
|
183
|
+
{/if}
|
|
184
|
+
{@render labelAppend?.()}
|
|
185
|
+
</label>
|
|
186
|
+
{#if layout === 'datetime'}
|
|
187
|
+
{@render inputDateTime()}
|
|
188
|
+
{:else}
|
|
189
|
+
{@render inputSplit()}
|
|
190
|
+
{/if}
|
|
191
|
+
<Issues {field} />
|
|
192
|
+
</fieldset>
|
|
193
|
+
{/if}
|
|
194
|
+
|
|
195
|
+
<style>
|
|
196
|
+
input[aria-invalid='true'] {
|
|
197
|
+
--input-color: var(--color-error);
|
|
198
|
+
}
|
|
199
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { RemoteFormField } from '@sveltejs/kit';
|
|
2
|
+
import type { InputProps } from './type.js';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
type $$ComponentProps = {
|
|
5
|
+
label: string;
|
|
6
|
+
/** La date part en ISO dans un champ caché: au schéma de la reconstruire. */
|
|
7
|
+
field?: RemoteFormField<string>;
|
|
8
|
+
/** Sans `field`, le nom du champ caché. Sans l'un ni l'autre, rien n'est soumis. */
|
|
9
|
+
name?: string;
|
|
10
|
+
value?: Date | null;
|
|
11
|
+
/** Zone IANA d'affichage et de saisie; à défaut, celle du navigateur. */
|
|
12
|
+
timezone?: string;
|
|
13
|
+
/** `split`: l'heure au-dessus de la date, avec ses boutons ±jour. */
|
|
14
|
+
layout?: 'split' | 'datetime';
|
|
15
|
+
/** Sans effet sur `split`, dont la ligne de label porte le `hint`. */
|
|
16
|
+
variant?: 'floating' | 'block';
|
|
17
|
+
hint?: string;
|
|
18
|
+
/** Rendu à droite du libellé, après le `hint`. */
|
|
19
|
+
labelAppend?: Snippet;
|
|
20
|
+
/** Ajuste la valeur avant qu'elle ne soit posée — borne, arrondi, décalage. */
|
|
21
|
+
transform?: (value: Date) => Date;
|
|
22
|
+
onchange?: (value: Date) => void;
|
|
23
|
+
} & InputProps;
|
|
24
|
+
declare const InputDateTime: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
25
|
+
type InputDateTime = ReturnType<typeof InputDateTime>;
|
|
26
|
+
export default InputDateTime;
|