fuma 2.5.1 → 2.5.4

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.
@@ -1,7 +1,7 @@
1
1
  <script lang="ts" generics="Item">
2
2
  import { CheckIcon, ChevronsUpDownIcon, SearchIcon, XIcon } from '@lucide/svelte'
3
3
  import type { RemoteFormField } from '@sveltejs/kit'
4
- import type { Snippet } from 'svelte'
4
+ import { untrack, type Snippet } from 'svelte'
5
5
  import type { ClassValue } from 'svelte/elements'
6
6
  import { slide } from 'svelte/transition'
7
7
  import { tip } from '../action/tip.js'
@@ -122,16 +122,25 @@
122
122
  value = selectedValues.includes(itemValue)
123
123
  ? value.filter((selectedItem) => getValue(selectedItem) !== itemValue)
124
124
  : [...value, item]
125
- commit()
125
+ onSelect?.(value, popover)
126
126
  }
127
127
 
128
128
  function remove(item: Item) {
129
129
  const itemValue = getValue(item)
130
130
  value = value.filter((selectedItem) => getValue(selectedItem) !== itemValue)
131
- commit()
131
+ onSelect?.(value, popover)
132
132
  }
133
133
 
134
- function commit() {
134
+ // L'annonce suit la valeur et non le geste: le parent peut l'écrire lui-même par `bind:`
135
+ // — un item créé dans un tiroir — et `toggle` n'est alors jamais appelé.
136
+ //
137
+ // La ligne de base est prise au montage: annoncer la sélection initiale l'écrirait dans
138
+ // l'état du champ, qui cesserait dès lors de consulter la valeur passée à `as()`.
139
+ let announced = untrack(() => selectedValues.join('\n'))
140
+ $effect(() => {
141
+ const signature = selectedValues.join('\n')
142
+ if (signature === announced) return
143
+ announced = signature
135
144
  // Les cases cachées suffisent à soumettre; `set` garde la validation à jour.
136
145
  field?.set(selectedValues)
137
146
  // Un choix n'émet aucun évènement de formulaire: les cases sont écrites par le code,
@@ -139,8 +148,7 @@
139
148
  // au select simple. Sans cette annonce, ce qui surveille le formulaire (une barre de
140
149
  // sauvegarde) ne voit la sélection qu'au choix suivant.
141
150
  trigger?.dispatchEvent(new Event('change', { bubbles: true }))
142
- onSelect?.(value, popover)
143
- }
151
+ })
144
152
  </script>
145
153
 
146
154
  {#snippet defaultSnippet(item: Item)}
@@ -262,7 +270,7 @@
262
270
  type="button"
263
271
  onclick={() => {
264
272
  value = []
265
- commit()
273
+ onSelect?.(value, popover)
266
274
  popover.hide()
267
275
  }}
268
276
  use:tip={{ content: 'Tout désélectionner' }}
@@ -1,5 +1,5 @@
1
1
  import type { RemoteFormField } from '@sveltejs/kit';
2
- import type { Snippet } from 'svelte';
2
+ import { type Snippet } from 'svelte';
3
3
  import type { ClassValue } from 'svelte/elements';
4
4
  import { type PopoverType } from '../popover/usePopover.svelte.js';
5
5
  import { type ItemsSource } from './useItems.svelte.js';
@@ -2,6 +2,7 @@
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 { InputProps } from './type.js'
5
6
  import type { Snippet } from 'svelte'
6
7
 
7
8
  let {
@@ -9,15 +10,20 @@
9
10
  label,
10
11
  value = $bindable(),
11
12
  options: optionsProp,
12
- labelAppend
13
+ labelAppend,
14
+ ...props
13
15
  }: {
14
16
  field?: RemoteFormField<string>
15
17
  label: string
18
+ /**
19
+ * Sans `field`, la valeur liée. Avec, la valeur initiale: `as('radio', …)` prend la
20
+ * valeur de l'option et ne sait donc pas amorcer la sélection.
21
+ */
16
22
  value?: string
17
23
  options: Options
18
24
  /** Rendu à droite du libellé. */
19
25
  labelAppend?: Snippet
20
- } = $props()
26
+ } & InputProps = $props()
21
27
 
22
28
  const options = $derived(parseOptions(optionsProp))
23
29
  </script>
@@ -29,8 +35,14 @@
29
35
  </span>
30
36
  <div class="join join-vertical">
31
37
  {#each options as option (option.value)}
32
- {@const { class: klass, ...props } = option}
33
- {@const checked = field?.value() === option.value || value === option.value}
38
+ {@const {
39
+ class: klass,
40
+ value: optionValue,
41
+ label: optionLabel,
42
+ icon: Icon,
43
+ ...optionProps
44
+ } = option}
45
+ {@const checked = (field ? (field.value() ?? value) : value) === optionValue}
34
46
  <label
35
47
  class={[
36
48
  'input join-item not-disabled:cursor-pointer not-disabled:hover:bg-base-200',
@@ -38,21 +50,22 @@
38
50
  ]}
39
51
  >
40
52
  <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
- />
53
+ {#if Icon}
54
+ <Icon size={18} opacity={0.8} />
55
55
  {/if}
56
+ <span class="grow">{optionLabel}</span>
57
+ <!-- Le `checked` explicite prime sur celui de `as()`, qui lit `field.value()`:
58
+ sans lui la valeur initiale ne cocherait rien, et un groupe décoché ne
59
+ part pas dans le `FormData`. -->
60
+ <input
61
+ {...field?.as('radio', optionValue)}
62
+ type="radio"
63
+ {checked}
64
+ onchange={() => (value = optionValue)}
65
+ class={['w-0']}
66
+ {...props}
67
+ {...optionProps}
68
+ />
56
69
  <div
57
70
  class={[
58
71
  'ml-auto h-5 w-5 rounded-full outline',
@@ -1,14 +1,19 @@
1
1
  import type { RemoteFormField } from '@sveltejs/kit';
2
2
  import { type Options } from '../utils/index.js';
3
+ import type { InputProps } from './type.js';
3
4
  import type { Snippet } from 'svelte';
4
5
  type $$ComponentProps = {
5
6
  field?: RemoteFormField<string>;
6
7
  label: string;
8
+ /**
9
+ * Sans `field`, la valeur liée. Avec, la valeur initiale: `as('radio', …)` prend la
10
+ * valeur de l'option et ne sait donc pas amorcer la sélection.
11
+ */
7
12
  value?: string;
8
13
  options: Options;
9
14
  /** Rendu à droite du libellé. */
10
15
  labelAppend?: Snippet;
11
- };
16
+ } & InputProps;
12
17
  declare const InputRadio: import("svelte").Component<$$ComponentProps, {}, "value">;
13
18
  type InputRadio = ReturnType<typeof InputRadio>;
14
19
  export default InputRadio;
@@ -1,7 +1,7 @@
1
1
  <script lang="ts" generics="Item">
2
2
  import { ChevronsUpDownIcon, SearchIcon, XIcon } from '@lucide/svelte'
3
3
  import type { RemoteFormField } from '@sveltejs/kit'
4
- import type { Snippet } from 'svelte'
4
+ import { untrack, type Snippet } from 'svelte'
5
5
  import type { ClassValue, HTMLLiAttributes } from 'svelte/elements'
6
6
  import { tip } from '../action/tip.js'
7
7
  import { useCommand } from '../command/useCommand.svelte.js'
@@ -82,6 +82,7 @@
82
82
  } = $props()
83
83
 
84
84
  const inputId = $props.id()
85
+ let trigger = $state<HTMLButtonElement>()
85
86
 
86
87
  let search = $state('')
87
88
  // Une énumération courte n'a que faire d'un champ de recherche; une fonction, elle, ne
@@ -116,15 +117,26 @@
116
117
  const item = options[index]
117
118
  onSelect?.(item, popover)
118
119
  if (!item) return
119
- setValue(item)
120
+ value = item
120
121
  }
121
122
  })
122
123
 
123
- function setValue(item: Item | undefined) {
124
- value = item
124
+ // L'annonce suit la valeur et non le geste: le parent peut l'écrire lui-même par `bind:`
125
+ // un item créé dans un tiroir — sans passer par la liste.
126
+ //
127
+ // La ligne de base est prise au montage: annoncer la valeur initiale l'écrirait dans
128
+ // l'état du champ, qui cesserait dès lors de consulter la valeur passée à `as()`.
129
+ let announced = untrack(() => submittedValue)
130
+ $effect(() => {
131
+ if (submittedValue === announced) return
132
+ announced = submittedValue
125
133
  // L'input caché suffit à soumettre; `set` garde la validation du formulaire à jour.
126
- field?.set(item && getValue(item))
127
- }
134
+ field?.set(submittedValue)
135
+ // Un choix n'émet aucun évènement de formulaire: l'input caché est écrit par le code.
136
+ // Sans cette annonce, ce qui surveille le formulaire (une barre de sauvegarde) ne voit
137
+ // pas une valeur que le parent a posée lui-même.
138
+ trigger?.dispatchEvent(new Event('change', { bubbles: true }))
139
+ })
128
140
 
129
141
  const isButtonSetNullVisible = $derived(nullable && value)
130
142
  </script>
@@ -135,6 +147,7 @@
135
147
 
136
148
  {#snippet triggerButton()}
137
149
  <button
150
+ bind:this={trigger}
138
151
  id={inputId}
139
152
  type="button"
140
153
  {disabled}
@@ -214,7 +227,7 @@
214
227
  class="btn btn-square btn-soft btn-sm"
215
228
  type="button"
216
229
  onclick={() => {
217
- setValue(undefined)
230
+ value = undefined
218
231
  onSelect?.(undefined, popover)
219
232
  popover.hide()
220
233
  }}
@@ -1,5 +1,5 @@
1
1
  import type { RemoteFormField } from '@sveltejs/kit';
2
- import type { Snippet } from 'svelte';
2
+ import { type Snippet } from 'svelte';
3
3
  import type { ClassValue, HTMLLiAttributes } from 'svelte/elements';
4
4
  import { type PopoverType } from '../popover/usePopover.svelte.js';
5
5
  import { type ItemsSource } from './useItems.svelte.js';
@@ -120,7 +120,7 @@
120
120
  "
121
121
  class={[
122
122
  'fixed top-0 right-0 bottom-0 z-10 flex',
123
- 'w-full flex-col overflow-y-scroll overscroll-contain bg-base-100',
123
+ 'w-full flex-col overflow-y-auto overscroll-contain bg-base-100',
124
124
  'transition-transform',
125
125
  klass
126
126
  ]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fuma",
3
- "version": "2.5.1",
3
+ "version": "2.5.4",
4
4
  "description": "My fullstack material build with sveltekit, daisyui, zod, prisma, lucia",
5
5
  "author": {
6
6
  "name": "Jonas Voisard",