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.
Files changed (89) 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 +41 -8
  7. package/dist/input/InputBoolean.svelte.d.ts +7 -0
  8. package/dist/input/InputCheckboxes.svelte +123 -0
  9. package/dist/input/InputCheckboxes.svelte.d.ts +19 -0
  10. package/dist/input/InputDateTime.svelte +199 -0
  11. package/dist/input/InputDateTime.svelte.d.ts +26 -0
  12. package/dist/input/InputMultiSelect.svelte +324 -0
  13. package/dist/input/{InputRelation.svelte.d.ts → InputMultiSelect.svelte.d.ts} +32 -13
  14. package/dist/input/InputNumber.svelte +43 -6
  15. package/dist/input/InputNumber.svelte.d.ts +8 -0
  16. package/dist/input/InputRadio.svelte +62 -24
  17. package/dist/input/InputRadio.svelte.d.ts +3 -0
  18. package/dist/input/InputRange.svelte +24 -4
  19. package/dist/input/InputRange.svelte.d.ts +9 -0
  20. package/dist/input/InputSelect.svelte +216 -62
  21. package/dist/input/InputSelect.svelte.d.ts +91 -6
  22. package/dist/input/InputSelectNative.svelte +51 -19
  23. package/dist/input/InputSelectNative.svelte.d.ts +8 -0
  24. package/dist/input/InputString.svelte +21 -6
  25. package/dist/input/InputString.svelte.d.ts +7 -0
  26. package/dist/input/InputTextarea.svelte +58 -11
  27. package/dist/input/InputTextarea.svelte.d.ts +14 -0
  28. package/dist/input/index.d.ts +4 -1
  29. package/dist/input/index.js +4 -1
  30. package/dist/input/useItems.svelte.d.ts +38 -0
  31. package/dist/input/useItems.svelte.js +142 -0
  32. package/dist/loading/Loading.svelte +3 -2
  33. package/dist/loading/Loading.svelte.d.ts +3 -2
  34. package/dist/popover/Popover.svelte +1 -2
  35. package/dist/popover/Popover.svelte.d.ts +13 -0
  36. package/dist/popover/index.d.ts +1 -1
  37. package/dist/popover/index.js +1 -1
  38. package/dist/popover/{popover.svelte.d.ts → usePopover.svelte.d.ts} +2 -1
  39. package/dist/popover/usePopover.svelte.js +145 -0
  40. package/dist/remote/useForm.d.ts +4 -5
  41. package/dist/remote/useForm.js +9 -4
  42. package/dist/state/index.d.ts +1 -1
  43. package/dist/state/index.js +1 -1
  44. package/dist/ui/button/ButtonDelete.svelte +6 -3
  45. package/dist/ui/button/ButtonDelete.svelte.d.ts +1 -1
  46. package/dist/ui/dialog/Dialog.svelte +5 -5
  47. package/dist/ui/drawer/Drawer.svelte +74 -26
  48. package/dist/ui/drawer/Drawer.svelte.d.ts +10 -4
  49. package/dist/ui/drawer/index.d.ts +1 -0
  50. package/dist/ui/drawer/index.js +1 -0
  51. package/dist/ui/drawer/inertBackground.d.ts +13 -0
  52. package/dist/ui/drawer/inertBackground.js +59 -0
  53. package/dist/ui/drawer/useLayer.svelte.d.ts +1 -1
  54. package/dist/ui/drawer/useLayer.svelte.js +11 -7
  55. package/dist/ui/index.d.ts +1 -0
  56. package/dist/ui/index.js +1 -0
  57. package/dist/ui/menu/ContextMenu.svelte +30 -17
  58. package/dist/ui/menu/ContextMenu.svelte.d.ts +2 -3
  59. package/dist/ui/menu/DropDown.svelte +1 -1
  60. package/dist/ui/menu/DropDown.svelte.d.ts +1 -1
  61. package/dist/ui/menu/DropDownMenu.svelte +12 -10
  62. package/dist/ui/menu/DropDownMenu.svelte.d.ts +2 -3
  63. package/dist/ui/menu/index.d.ts +0 -1
  64. package/dist/ui/menu/index.js +0 -2
  65. package/dist/ui/range/RangePicker.svelte +25 -13
  66. package/dist/ui/range/RangePicker.svelte.d.ts +9 -25
  67. package/dist/ui/range/RangePickerButton.svelte +49 -32
  68. package/dist/ui/range/RangePickerButton.svelte.d.ts +1 -1
  69. package/dist/ui/table/Table.svelte +16 -16
  70. package/dist/ui/table/Table.svelte.d.ts +1 -1
  71. package/dist/ui/table/TableFieldsEdition.svelte +25 -23
  72. package/dist/ui/table/TableHead.svelte +5 -1
  73. package/dist/ui/table/cell/TableCellBoolean.svelte +2 -2
  74. package/dist/ui/table/head/OrderButtons.svelte +21 -20
  75. package/dist/ui/table/head/OrderButtons.svelte.d.ts +2 -0
  76. package/dist/ui/table/head/TableHeadDate.svelte +66 -57
  77. package/dist/ui/table/head/TableHeadNumber.svelte +58 -22
  78. package/dist/ui/table/head/TableHeadSelect.svelte +31 -28
  79. package/dist/ui/table/head/TableHeadString.svelte +12 -8
  80. package/dist/ui/table/index.d.ts +1 -1
  81. package/dist/ui/table/index.js +1 -1
  82. package/package.json +7 -4
  83. package/dist/input/InputRelation.svelte +0 -200
  84. package/dist/popover/popover.svelte.js +0 -106
  85. package/dist/ui/table/TableViewSelect.svelte +0 -142
  86. package/dist/ui/table/TableViewSelect.svelte.d.ts +0 -11
  87. /package/dist/command/{command.svelte.d.ts → useCommand.svelte.d.ts} +0 -0
  88. /package/dist/state/{param.svelte.d.ts → param.d.ts} +0 -0
  89. /package/dist/state/{param.svelte.js → param.js} +0 -0
@@ -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
- isValided: z.literal(true, 'Validation requise'),
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
  });
@@ -1 +1 @@
1
- export * from './command.svelte.js';
1
+ export * from './useCommand.svelte.js';
@@ -1 +1 @@
1
- export * from './command.svelte.js';
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' || event.key === ' ') {
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
- <div class="flex h-(--size) items-center gap-2">
33
- <div class="grow">{label}</div>
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
- <input {...field.as('checkbox')} class={['peer w-0']} {...props} />
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 rounded-full outline', 'bg-base-100']} style="corner-shape: squircle;">
90
+ <div class={['squircle', 'h-5 w-5 outline', 'bg-base-100']}>
62
91
  <div
63
92
  class={[
64
- 'squircle grid h-5 w-5 place-content-center',
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;