fuma 2.1.5 → 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.
Files changed (84) 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 +25 -7
  7. package/dist/input/InputBoolean.svelte.d.ts +4 -0
  8. package/dist/input/InputCheckboxes.svelte +116 -0
  9. package/dist/input/InputCheckboxes.svelte.d.ts +16 -0
  10. package/dist/input/InputDateTime.svelte +194 -0
  11. package/dist/input/InputDateTime.svelte.d.ts +23 -0
  12. package/dist/input/InputMultiSelect.svelte +286 -0
  13. package/dist/input/{InputRelation.svelte.d.ts → InputMultiSelect.svelte.d.ts} +27 -13
  14. package/dist/input/InputNumber.svelte +36 -6
  15. package/dist/input/InputNumber.svelte.d.ts +5 -0
  16. package/dist/input/InputRadio.svelte +54 -23
  17. package/dist/input/InputRange.svelte +19 -4
  18. package/dist/input/InputRange.svelte.d.ts +6 -0
  19. package/dist/input/InputSelect.svelte +204 -62
  20. package/dist/input/InputSelect.svelte.d.ts +87 -6
  21. package/dist/input/InputSelectNative.svelte +44 -19
  22. package/dist/input/InputSelectNative.svelte.d.ts +5 -0
  23. package/dist/input/InputString.svelte +36 -6
  24. package/dist/input/InputString.svelte.d.ts +5 -0
  25. package/dist/input/InputTextarea.svelte +41 -12
  26. package/dist/input/InputTextarea.svelte.d.ts +5 -0
  27. package/dist/input/index.d.ts +4 -1
  28. package/dist/input/index.js +4 -1
  29. package/dist/input/useItems.svelte.d.ts +38 -0
  30. package/dist/input/useItems.svelte.js +142 -0
  31. package/dist/loading/Loading.svelte +3 -2
  32. package/dist/loading/Loading.svelte.d.ts +3 -2
  33. package/dist/popover/Popover.svelte +1 -2
  34. package/dist/popover/Popover.svelte.d.ts +13 -0
  35. package/dist/popover/index.d.ts +1 -1
  36. package/dist/popover/index.js +1 -1
  37. package/dist/popover/{popover.svelte.d.ts → usePopover.svelte.d.ts} +2 -1
  38. package/dist/popover/usePopover.svelte.js +145 -0
  39. package/dist/remote/useForm.d.ts +4 -5
  40. package/dist/remote/useForm.js +9 -4
  41. package/dist/state/index.d.ts +1 -1
  42. package/dist/state/index.js +1 -1
  43. package/dist/ui/button/ButtonDelete.svelte +4 -2
  44. package/dist/ui/dialog/Dialog.svelte +5 -5
  45. package/dist/ui/drawer/Drawer.svelte +74 -26
  46. package/dist/ui/drawer/Drawer.svelte.d.ts +10 -4
  47. package/dist/ui/drawer/index.d.ts +1 -0
  48. package/dist/ui/drawer/index.js +1 -0
  49. package/dist/ui/drawer/inertBackground.d.ts +13 -0
  50. package/dist/ui/drawer/inertBackground.js +59 -0
  51. package/dist/ui/drawer/useLayer.svelte.d.ts +1 -1
  52. package/dist/ui/drawer/useLayer.svelte.js +11 -7
  53. package/dist/ui/index.d.ts +1 -0
  54. package/dist/ui/index.js +1 -0
  55. package/dist/ui/menu/ContextMenu.svelte +30 -17
  56. package/dist/ui/menu/ContextMenu.svelte.d.ts +2 -3
  57. package/dist/ui/menu/DropDown.svelte +1 -1
  58. package/dist/ui/menu/DropDown.svelte.d.ts +1 -1
  59. package/dist/ui/menu/DropDownMenu.svelte +12 -10
  60. package/dist/ui/menu/DropDownMenu.svelte.d.ts +2 -3
  61. package/dist/ui/menu/index.d.ts +0 -1
  62. package/dist/ui/menu/index.js +0 -2
  63. package/dist/ui/range/RangePicker.svelte +25 -13
  64. package/dist/ui/range/RangePicker.svelte.d.ts +9 -25
  65. package/dist/ui/range/RangePickerButton.svelte +49 -32
  66. package/dist/ui/range/RangePickerButton.svelte.d.ts +1 -1
  67. package/dist/ui/table/Table.svelte +7 -10
  68. package/dist/ui/table/Table.svelte.d.ts +1 -1
  69. package/dist/ui/table/TableFieldsEdition.svelte +24 -22
  70. package/dist/ui/table/TableHead.svelte +5 -1
  71. package/dist/ui/table/TableViewSelect.svelte +52 -47
  72. package/dist/ui/table/cell/TableCellBoolean.svelte +2 -2
  73. package/dist/ui/table/head/TableHeadDate.svelte +62 -55
  74. package/dist/ui/table/head/TableHeadNumber.svelte +55 -19
  75. package/dist/ui/table/head/TableHeadSelect.svelte +30 -27
  76. package/dist/ui/table/head/TableHeadString.svelte +11 -7
  77. package/dist/ui/table/index.d.ts +1 -0
  78. package/dist/ui/table/index.js +1 -0
  79. package/package.json +7 -4
  80. package/dist/input/InputRelation.svelte +0 -200
  81. package/dist/popover/popover.svelte.js +0 -106
  82. /package/dist/command/{command.svelte.d.ts → useCommand.svelte.d.ts} +0 -0
  83. /package/dist/state/{param.svelte.d.ts → param.d.ts} +0 -0
  84. /package/dist/state/{param.svelte.js → param.js} +0 -0
@@ -4,14 +4,16 @@
4
4
  import z from 'zod'
5
5
  import { goto } from '$app/navigation'
6
6
  import { page } from '$app/state'
7
- import { urlParam } from '../../../state/param.svelte.js'
8
- import { DropDown } from '../../menu/index.js'
7
+ import { Popover } from '../../../popover/index.js'
8
+ import { urlParam } from '../../../state/param.js'
9
9
  import { zodCoerceJsonRecord } from '../../../validation/zod.js'
10
10
  import type { ItemBase, TableField } from '../field.js'
11
11
  import OrderButtons from './OrderButtons.svelte'
12
12
 
13
13
  let { field }: { field: TableField<Item> } = $props()
14
14
 
15
+ // `.catch`: le paramètre vient de l'URL, donc de l'utilisateur. Un contenu illisible retombe
16
+ // sur un filtre vide au lieu de faire échouer le rendu de toute la table.
15
17
  const paramModel = zodCoerceJsonRecord
16
18
  .pipe(
17
19
  z.object({
@@ -21,14 +23,24 @@
21
23
  })
22
24
  )
23
25
  .default({})
26
+ .catch({})
24
27
 
25
- let { min, max, order } = $derived(paramModel.parse(page.url.searchParams.get(field.key)))
28
+ // `?? undefined`: `searchParams.get` rend `null` quand le filtre est absent, et un
29
+ // `.default()` de zod ne couvre que `undefined`.
30
+ let { min, max, order } = $derived(
31
+ paramModel.parse(page.url.searchParams.get(field.key) ?? undefined)
32
+ )
33
+
34
+ /** Un `<input type="number">` vidé lie `null`: une borne effacée, pas une borne à zéro. */
35
+ function isSet(bound: number | null | undefined): bound is number {
36
+ return bound !== null && bound !== undefined
37
+ }
26
38
 
27
39
  const updateUrl = debounce(() => {
28
40
  const query: Record<string, string | number> = {}
29
41
  if (order) query.order = order
30
- if (min !== undefined) query.min = min
31
- if (max !== undefined) query.max = max
42
+ if (isSet(min)) query.min = min
43
+ if (isSet(max)) query.max = max
32
44
  if (!Object.keys(query).length) return resetFilter()
33
45
  goto(urlParam.with({ [field.key]: JSON.stringify(query) }, 'skip', 'take'), {
34
46
  noScroll: true,
@@ -37,6 +49,8 @@
37
49
  }, 250)
38
50
 
39
51
  function resetFilter() {
52
+ // Une frappe encore en attente réécrirait le filtre qu'on vient d'effacer.
53
+ updateUrl.clear()
40
54
  return goto(urlParam.without(field.key, 'skip', 'take'), {
41
55
  replaceState: true,
42
56
  noScroll: true,
@@ -46,23 +60,23 @@
46
60
  </script>
47
61
 
48
62
  <th class="p-1">
49
- <DropDown hideOnBlur hideOnNav={false} autofocus tippyProps={{ appendTo: () => document.body }}>
50
- {#snippet activator()}
51
- <button class="menu-item min-h-8 w-full flex-wrap gap-y-1">
63
+ <Popover class="p-1">
64
+ {#snippet trigger(popover)}
65
+ <button class="menu-item min-h-8 w-full flex-wrap gap-y-1" {...popover.trigger}>
52
66
  <div class="flex gap-2">
53
67
  <span>{field.label}</span>
54
- {#if min === undefined && max === undefined}
68
+ {#if !isSet(min) && !isSet(max)}
55
69
  <FunnelIcon size={15} class="opacity-50" />
56
70
  {/if}
57
71
  </div>
58
72
 
59
- {#if min !== undefined || max !== undefined}
73
+ {#if isSet(min) || isSet(max)}
60
74
  <span class="badge badge-xs text-[0.7rem] font-normal text-white badge-primary">
61
- {#if min !== undefined}
75
+ {#if isSet(min)}
62
76
  {min} ≤
63
77
  {/if}
64
78
  x
65
- {#if max !== undefined}
79
+ {#if isSet(max)}
66
80
  ≤ {max}
67
81
  {/if}
68
82
  </span>
@@ -75,13 +89,13 @@
75
89
  </button>
76
90
  {/snippet}
77
91
 
78
- {#snippet children({ tip })}
92
+ {#snippet children(popover)}
79
93
  {#if field.sortable !== false}
80
94
  <OrderButtons
81
95
  bind:order
82
96
  onChange={() => {
83
97
  updateUrl()
84
- tip?.hide()
98
+ popover.hide()
85
99
  }}
86
100
  />
87
101
  <div class="divider"></div>
@@ -91,11 +105,33 @@
91
105
  class="grid grid-cols-2 gap-2 p-1"
92
106
  onsubmit={(e) => {
93
107
  e.preventDefault()
94
- tip?.hide()
108
+ // Fermer sans purger la temporisation perdrait la dernière frappe.
109
+ updateUrl.flush()
110
+ popover.hide()
95
111
  }}
96
112
  >
97
- <input bind:value={min} oninput={updateUrl} placeholder="Min" />
98
- <input bind:value={max} oninput={updateUrl} placeholder="Max" />
113
+ <!-- `type="number"`: sans lui `bind:value` lie une chaîne, que ni le paramètre
114
+ d'URL ni le filtre serveur ne savent relire. -->
115
+ <!-- `autofocus`: le popover natif donne le focus à cet élément à l'ouverture. -->
116
+ <!-- svelte-ignore a11y_autofocus -->
117
+ <input
118
+ bind:value={min}
119
+ oninput={updateUrl}
120
+ type="number"
121
+ class="input w-full"
122
+ placeholder="Min"
123
+ aria-label="Minimum"
124
+ autofocus
125
+ onfocus={(e) => e.currentTarget.select()}
126
+ />
127
+ <input
128
+ bind:value={max}
129
+ oninput={updateUrl}
130
+ type="number"
131
+ class="input w-full"
132
+ placeholder="Max"
133
+ aria-label="Maximum"
134
+ />
99
135
 
100
136
  <div class="col-span-full flex justify-end gap-2">
101
137
  <button
@@ -104,7 +140,7 @@
104
140
  onclick={() => {
105
141
  min = undefined
106
142
  max = undefined
107
- tip?.hide()
143
+ popover.hide()
108
144
  resetFilter()
109
145
  }}
110
146
  >
@@ -114,5 +150,5 @@
114
150
  </div>
115
151
  </form>
116
152
  {/snippet}
117
- </DropDown>
153
+ </Popover>
118
154
  </th>
@@ -1,8 +1,8 @@
1
1
  <script lang="ts" generics="Item extends ItemBase">
2
2
  import { ListFilterIcon } from '@lucide/svelte'
3
3
  import { page } from '$app/state'
4
- import { param, urlParam } from '../../../state/param.svelte.js'
5
- import { DropDown } from '../../menu/index.js'
4
+ import { Popover } from '../../../popover/index.js'
5
+ import { param, urlParam } from '../../../state/param.js'
6
6
  import type { ItemBase, TableField } from '../index.js'
7
7
  import { jsonParse } from '../../../utils/jsonParse.js'
8
8
  import { type Options, parseOptions } from '../../../utils/options.js'
@@ -49,10 +49,10 @@
49
49
  </script>
50
50
 
51
51
  <th class="p-1">
52
- <DropDown hideOnBlur hideOnNav={!multiSelect} tippyProps={{ appendTo: () => document.body }}>
53
- {#snippet activator()}
54
- <button class="menu-item min-h-8 w-full flex-wrap gap-y-1">
55
- <div class="flex gap-2">
52
+ <Popover class="p-1">
53
+ {#snippet trigger(popover)}
54
+ <button class="menu-item min-h-8 w-full flex-wrap gap-y-1" {...popover.trigger}>
55
+ <div class="flex items-center gap-2">
56
56
  {field.label}
57
57
  {#if !optionsActive.length}
58
58
  <ListFilterIcon size={15} class="opacity-50" />
@@ -74,25 +74,28 @@
74
74
  </button>
75
75
  {/snippet}
76
76
 
77
- <div class="flex flex-col gap-1">
78
- {#each options as { isActive, icon: Icon, label, value } (value)}
79
- <a
80
- href={getHref(value)}
81
- class="menu-item px-3 py-2"
82
- class:active={isActive}
83
- data-sveltekit-noscroll
84
- data-sveltekit-replacestate
85
- >
86
- {#if Icon}
87
- <Icon size={18} class="opacity-60" />
88
- {/if}
89
- <span class="font-normal whitespace-nowrap">{label}</span>
90
- </a>
91
- {:else}
92
- <div class="menu-item disabled px-10">
93
- <span class="opacity-40">{placeholder}</span>
94
- </div>
95
- {/each}
96
- </div>
97
- </DropDown>
77
+ {#snippet children(popover)}
78
+ <div class="flex flex-col gap-1">
79
+ {#each options as { isActive, icon: Icon, label, value } (value)}
80
+ <a
81
+ href={getHref(value)}
82
+ class="menu-item px-3 py-2"
83
+ class:active={isActive}
84
+ data-sveltekit-noscroll
85
+ data-sveltekit-replacestate
86
+ onclick={multiSelect ? undefined : popover.hide}
87
+ >
88
+ {#if Icon}
89
+ <Icon size={18} class="opacity-60" />
90
+ {/if}
91
+ <span class="font-normal whitespace-nowrap">{label}</span>
92
+ </a>
93
+ {:else}
94
+ <div class="menu-item disabled px-10">
95
+ <span class="opacity-40">{placeholder}</span>
96
+ </div>
97
+ {/each}
98
+ </div>
99
+ {/snippet}
100
+ </Popover>
98
101
  </th>
@@ -1,7 +1,7 @@
1
1
  <script lang="ts" generics="Item extends ItemBase">
2
2
  import { SearchIcon } from '@lucide/svelte'
3
3
  import { page } from '$app/state'
4
- import { DropDown } from '../../menu/index.js'
4
+ import { Popover } from '../../../popover/index.js'
5
5
  import type { ItemBase, TableField } from '../index.js'
6
6
 
7
7
  let { field }: { field: TableField<Item> } = $props()
@@ -10,9 +10,9 @@
10
10
  </script>
11
11
 
12
12
  <th class="p-1">
13
- <DropDown hideOnBlur hideOnNav={false} autofocus tippyProps={{ appendTo: () => document.body }}>
14
- {#snippet activator()}
15
- <button class="menu-item min-h-8 w-full flex-wrap gap-y-1">
13
+ <Popover class="p-1">
14
+ {#snippet trigger(popover)}
15
+ <button class="menu-item min-h-8 w-full flex-wrap gap-y-1" {...popover.trigger}>
16
16
  <div class="flex items-center gap-2">
17
17
  <span>{field.label}</span>
18
18
  {#if !searchValue}
@@ -29,14 +29,18 @@
29
29
  </button>
30
30
  {/snippet}
31
31
 
32
- {#snippet children({ tip })}
32
+ {#snippet children(popover)}
33
+ <!-- `autofocus`: le popover natif donne le focus à cet élément à l'ouverture. -->
34
+ <!-- svelte-ignore a11y_autofocus -->
33
35
  <input
34
36
  name={field.key}
35
37
  type="search"
36
38
  class="m-1"
37
39
  value={searchValue}
38
- onkeydown={(e) => e.key === 'Enter' && tip?.hide()}
40
+ autofocus
41
+ onfocus={(e) => e.currentTarget.select()}
42
+ onkeydown={(e) => e.key === 'Enter' && popover.hide()}
39
43
  />
40
44
  {/snippet}
41
- </DropDown>
45
+ </Popover>
42
46
  </th>
@@ -1,3 +1,4 @@
1
+ export * from './cell/index.js';
1
2
  export * from './context.js';
2
3
  export type { TableField } from './field.js';
3
4
  export * from './field.js';
@@ -1,3 +1,4 @@
1
+ export * from './cell/index.js';
1
2
  export * from './context.js';
2
3
  export * from './field.js';
3
4
  export * from './head/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fuma",
3
- "version": "2.1.5",
3
+ "version": "2.2.7",
4
4
  "description": "My fullstack material build with sveltekit, daisyui, zod, prisma, lucia",
5
5
  "author": {
6
6
  "name": "Jonas Voisard",
@@ -11,6 +11,7 @@
11
11
  "build": "vite build && bun run package",
12
12
  "preview": "vite preview --host",
13
13
  "package": "svelte-kit sync && svelte-package && publint",
14
+ "package:watch": "svelte-package --watch",
14
15
  "prepublishOnly": "bun run package",
15
16
  "sync": "svelte-kit sync",
16
17
  "svelte-check": "svelte-check --tsconfig ./tsconfig.json",
@@ -44,7 +45,9 @@
44
45
  "!dist/**/*.spec.*"
45
46
  ],
46
47
  "peerDependencies": {
48
+ "@sveltejs/kit": "^2.70.2",
47
49
  "daisyui": "^5.5.17",
50
+ "svelte": "^5.55.5",
48
51
  "tailwindcss": "^4.1.18",
49
52
  "zod": "^4.3.6"
50
53
  },
@@ -52,12 +55,12 @@
52
55
  "@biomejs/biome": "2.4.2",
53
56
  "@eslint/compat": "^2.0.5",
54
57
  "@sveltejs/adapter-auto": "^7.0.1",
55
- "@sveltejs/kit": "^2.58.0",
58
+ "@sveltejs/kit": "^2.70.2",
56
59
  "@sveltejs/package": "^2.5.7",
57
60
  "@sveltejs/vite-plugin-svelte": "^6.2.4",
58
61
  "@tailwindcss/vite": "^4.2.4",
59
62
  "@types/node": "^25.6.0",
60
- "daisyui": "^5.5.19",
63
+ "daisyui": "^5.7.4",
61
64
  "eslint": "^9.39.4",
62
65
  "eslint-config-prettier": "^10.1.8",
63
66
  "eslint-plugin-svelte": "^3.17.1",
@@ -67,6 +70,7 @@
67
70
  "prettier-plugin-tailwindcss": "^0.7.4",
68
71
  "publint": "^0.1.16",
69
72
  "shiki": "^4.0.2",
73
+ "svelte": "^5.55.5",
70
74
  "svelte-check": "^4.4.6",
71
75
  "tailwindcss": "^4.2.4",
72
76
  "typescript": "^5.9.3",
@@ -86,7 +90,6 @@
86
90
  "litepicker": "^2.0.12",
87
91
  "perod": "^1.4.1",
88
92
  "runed": "^0.37.1",
89
- "svelte": "^5.55.5",
90
93
  "svelte-easy-crop": "^5.0.1",
91
94
  "svelte-sonner": "^1.1.1",
92
95
  "tippy.js": "^6.3.7",
@@ -1,200 +0,0 @@
1
- <script lang="ts" generics="Item">
2
- import { ChevronsUpDownIcon, SearchIcon, XIcon } from '@lucide/svelte'
3
- import type { RemoteFormField, RemoteQueryFunction } from '@sveltejs/kit'
4
- import type { Snippet } from 'svelte'
5
- import type { ClassValue } from 'svelte/elements'
6
- import { tip } from '../action/tip.js'
7
- import { useCommand } from '../command/command.svelte.js'
8
- import Loading from '../loading/Loading.svelte'
9
- import { type PopoverType, usePopover } from '../popover/popover.svelte.js'
10
- import Issues from './Issues.svelte'
11
-
12
- let {
13
- label,
14
- searchItems,
15
- getValue = defaultGetValue,
16
- selected = defaultSnippet,
17
- proposal = defaultSnippet,
18
- field,
19
- value = $bindable(),
20
- placeholder = 'Selectionner une valeur',
21
- class: klass,
22
- nullable,
23
- hint,
24
- onSelect,
25
- hotKey
26
- }: {
27
- searchItems: RemoteQueryFunction<{ search: string }, Item[]>
28
- label?: string
29
- getValue?: (item: NoInfer<Item>) => string
30
- selected?: Snippet<[Item]>
31
- proposal?: Snippet<[Item, { isSelected: boolean; isFocus: boolean }]>
32
- field?: RemoteFormField<string>
33
- value?: string
34
- placeholder?: string
35
- class?: ClassValue
36
- nullable?: boolean
37
- hint?: Snippet<[Item | undefined]>
38
- onSelect?: (item: NoInfer<Item> | undefined, popover: PopoverType) => void
39
- hotKey?: string
40
- } = $props()
41
-
42
- let search = $state('')
43
- const query = $derived.by(() => searchItems({ search }))
44
- const items = $derived(query.current || [])
45
-
46
- let selectedItem = $state<Item | undefined>(undefined)
47
- $effect(() => {
48
- const targetValue = (field?.value() ?? value)?.toString()
49
- if (!targetValue) {
50
- selectedItem = undefined
51
- command.selectedIndex = -1
52
- return
53
- }
54
- // TODO: Quelle est la meilleur facon de récupérer l'item initial ?
55
- command.selectedIndex = items.findIndex((item) => getValue(item) === targetValue)
56
- })
57
-
58
- export const popover = usePopover({
59
- listenFocus: false,
60
- onShow: () => command.focusTrigger(),
61
- hotKey: (() => hotKey)()
62
- })
63
- export const command = useCommand({
64
- isEnable: () => popover.isOpen,
65
- onSelect: (index) => {
66
- popover.hide()
67
- const item = items[index]
68
- onSelect?.(item, popover)
69
- if (!item) return
70
- selectedItem = item
71
- setValue(item)
72
- }
73
- })
74
-
75
- function setValue(item: Item | undefined) {
76
- const newValue = item && getValue(item)
77
- if (field) {
78
- field.set(newValue)
79
- } else {
80
- value = newValue
81
- }
82
- }
83
-
84
- function defaultGetValue(item: Item): string {
85
- if (item && typeof item === 'object') {
86
- if ('value' in item && typeof item.value === 'string') return item.value
87
- if ('rowid' in item && typeof item.rowid === 'string') return item.rowid
88
- }
89
- return JSON.stringify(item)
90
- }
91
-
92
- function getDefaultLabel(item: Item) {
93
- if (item && typeof item === 'object' && 'label' in item) {
94
- if (typeof item.label === 'string') return item.label
95
- }
96
- return getValue(item)
97
- }
98
-
99
- const isButtonSetNullVisible = $derived(nullable && (field?.value() || value))
100
- </script>
101
-
102
- {#snippet defaultSnippet(item: Item)}
103
- <span>{getDefaultLabel(item)}</span>
104
- {/snippet}
105
-
106
- {#snippet triggerButton()}
107
- <button
108
- type="button"
109
- class={['input', field?.issues.length && 'input-error', klass]}
110
- {...popover.trigger}
111
- >
112
- <div class="grow text-left">
113
- {#if !selectedItem}
114
- <span class="opacity-60">{placeholder}</span>
115
- {:else}
116
- <!-- item.icon not rerender if not wrapped in #key -->
117
- {#key selectedItem}{@render selected(selectedItem)}{/key}
118
- {/if}
119
- </div>
120
- <ChevronsUpDownIcon size={14} opacity={0.7} />
121
- </button>
122
- {/snippet}
123
-
124
- {#if field}
125
- <input {...field.as('hidden', field.value() || '')} />
126
- {/if}
127
-
128
- <div>
129
- {#if label}
130
- <label class="floating-label">
131
- <span>{label}</span>
132
- {@render triggerButton()}
133
- </label>
134
- {:else}
135
- {@render triggerButton()}
136
- {/if}
137
-
138
- <div
139
- {...popover.content}
140
- class={['my-2 rounded-box border bg-base-100 shadow-xl']}
141
- style="min-width: anchor-size(width);"
142
- tabindex="-1"
143
- >
144
- <div class="sticky top-0 z-10 flex gap-2 bg-base-100/10 p-2 backdrop-blur-md">
145
- <label class="input input-sm grow input-ghost">
146
- <SearchIcon opacity={0.6} size={20} />
147
- <input
148
- type="search"
149
- placeholder="Recherche"
150
- autocomplete="off"
151
- bind:value={search}
152
- {...command.trigger}
153
- />
154
- </label>
155
- {#if isButtonSetNullVisible}
156
- <button
157
- class="btn btn-square btn-soft btn-sm"
158
- type="button"
159
- onclick={() => {
160
- selectedItem = undefined
161
- setValue(undefined)
162
- onSelect?.(undefined, popover)
163
- popover.hide()
164
- }}
165
- use:tip={{ content: 'Pas de sélection' }}
166
- >
167
- <XIcon />
168
- </button>
169
- {/if}
170
- </div>
171
-
172
- <ul class="menu max-h-80 w-full flex-nowrap pt-0">
173
- {#each items as item, index (item)}
174
- {@const isSelected = index === command.selectedIndex}
175
- {@const isFocus = index === command.focusIndex}
176
- <li>
177
- <button
178
- {...command.item(index, '')}
179
- class={[isSelected && 'border', isFocus && 'menu-focus', 'scroll-mt-13 scroll-mb-2']}
180
- type="button"
181
- tabindex="-1"
182
- role="option"
183
- >
184
- {@render proposal(item, { isSelected, isFocus })}
185
- </button>
186
- </li>
187
- {/each}
188
- </ul>
189
-
190
- <Loading {query} />
191
- </div>
192
- <Issues {field} />
193
- {@render hint?.(selectedItem)}
194
- </div>
195
-
196
- <style>
197
- div[popover] {
198
- border-color: color-mix(in oklab, var(--color-base-content) 20%, #0000);
199
- }
200
- </style>
@@ -1,106 +0,0 @@
1
- import debounce from 'debounce';
2
- import { createAttachmentKey } from 'svelte/attachments';
3
- import { on } from 'svelte/events';
4
- const placements = {
5
- top: 'top',
6
- 'top-end': 'top span-left',
7
- 'top-start': 'top span-right',
8
- bottom: 'bottom',
9
- 'bottom-end': 'bottom span-left',
10
- 'bottom-start': 'bottom span-right',
11
- left: 'left',
12
- 'left-end': 'left span-top',
13
- 'left-start': 'left span-bottom',
14
- right: 'right',
15
- 'right-end': 'right span-top',
16
- 'right-start': 'right span-bottom'
17
- };
18
- let popoverInstanceCount = 0;
19
- export function usePopover({ mode = 'auto', placement = 'bottom-start', listenClick = true, listenFocus = true, listenHover = false, hideDelay = 400, onShow, onHide, hotKey } = {}) {
20
- const uid = popoverInstanceCount++;
21
- const anchorName = `--anchor-${uid}`;
22
- let popover = $state();
23
- let isOpen = $state(!!popover?.matches(':popover-open'));
24
- const hideDebounced = debounce(hide, hideDelay);
25
- function show() {
26
- popover?.showPopover();
27
- onShow?.();
28
- }
29
- function hide() {
30
- popover?.hidePopover();
31
- onHide?.();
32
- }
33
- function onMouseEnter() {
34
- hideDebounced.clear();
35
- show();
36
- }
37
- function onToggle(event) {
38
- isOpen = event.newState === 'open';
39
- }
40
- function attachTriggerListeners(activator) {
41
- const cleanups = [];
42
- if (listenClick) {
43
- cleanups.push(on(activator, 'click', show));
44
- }
45
- if (listenFocus) {
46
- cleanups.push(on(activator, 'focusin', show));
47
- // TODO: handle focusout ???
48
- }
49
- if (listenHover) {
50
- cleanups.push(on(activator, 'mouseenter', onMouseEnter));
51
- cleanups.push(on(activator, 'mouseleave', hideDebounced));
52
- }
53
- if (hotKey) {
54
- cleanups.push(on(window, 'keydown', handleHotKey));
55
- }
56
- return () => {
57
- for (const cleanup of cleanups)
58
- cleanup();
59
- };
60
- }
61
- function attachPopoverListeners(node) {
62
- const cleanups = [];
63
- cleanups.push(on(node, 'toggle', onToggle));
64
- if (listenHover) {
65
- cleanups.push(on(node, 'mouseenter', onMouseEnter));
66
- cleanups.push(on(node, 'mouseleave', hideDebounced));
67
- }
68
- return () => {
69
- for (const cleanup of cleanups)
70
- cleanup();
71
- };
72
- }
73
- async function handleHotKey(event) {
74
- const { metaKey, ctrlKey, key } = event;
75
- if ((metaKey || ctrlKey) && key === hotKey) {
76
- event.preventDefault();
77
- show();
78
- return;
79
- }
80
- }
81
- return {
82
- show,
83
- hide,
84
- get isOpen() {
85
- return isOpen;
86
- },
87
- content: {
88
- popover: mode,
89
- [createAttachmentKey()]: (node) => {
90
- popover = node;
91
- popover.style.positionAnchor = anchorName;
92
- popover.style.positionArea = placements[placement];
93
- popover.style.inset = 'auto';
94
- popover.style.positionTry = 'flip-x, flip-y';
95
- popover.style.position = 'relative';
96
- return attachPopoverListeners(node);
97
- }
98
- },
99
- trigger: {
100
- [createAttachmentKey()]: (node) => {
101
- node.style.anchorName = anchorName;
102
- return attachTriggerListeners(node);
103
- }
104
- }
105
- };
106
- }
File without changes
File without changes