@human-kit/svelte-components 1.0.0-alpha.1
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/combobox/TODO.md +175 -0
- package/dist/combobox/button/combobox-button.svelte +57 -0
- package/dist/combobox/button/combobox-button.svelte.d.ts +9 -0
- package/dist/combobox/index.d.ts +14 -0
- package/dist/combobox/index.js +18 -0
- package/dist/combobox/index.parts.d.ts +10 -0
- package/dist/combobox/index.parts.js +11 -0
- package/dist/combobox/input/combobox-input.svelte +98 -0
- package/dist/combobox/input/combobox-input.svelte.d.ts +13 -0
- package/dist/combobox/item/combobox-item-implicit-text-test.svelte +21 -0
- package/dist/combobox/item/combobox-item-implicit-text-test.svelte.d.ts +3 -0
- package/dist/combobox/item/combobox-listboxitem.svelte +136 -0
- package/dist/combobox/item/combobox-listboxitem.svelte.d.ts +18 -0
- package/dist/combobox/item-indicator/combobox-item-indicator.svelte +63 -0
- package/dist/combobox/item-indicator/combobox-item-indicator.svelte.d.ts +17 -0
- package/dist/combobox/list/combobox-listbox.svelte +76 -0
- package/dist/combobox/list/combobox-listbox.svelte.d.ts +47 -0
- package/dist/combobox/popover/combobox-popover.svelte +69 -0
- package/dist/combobox/popover/combobox-popover.svelte.d.ts +12 -0
- package/dist/combobox/root/combobox-filtered-test.svelte +51 -0
- package/dist/combobox/root/combobox-filtered-test.svelte.d.ts +7 -0
- package/dist/combobox/root/combobox-multiselect-test.svelte +76 -0
- package/dist/combobox/root/combobox-multiselect-test.svelte.d.ts +13 -0
- package/dist/combobox/root/combobox-numeric-string-id-test.svelte +20 -0
- package/dist/combobox/root/combobox-numeric-string-id-test.svelte.d.ts +3 -0
- package/dist/combobox/root/combobox-test.svelte +43 -0
- package/dist/combobox/root/combobox-test.svelte.d.ts +9 -0
- package/dist/combobox/root/combobox.svelte +696 -0
- package/dist/combobox/root/combobox.svelte.d.ts +58 -0
- package/dist/combobox/root/context.d.ts +90 -0
- package/dist/combobox/root/context.js +15 -0
- package/dist/combobox/tag/combobox-tag.svelte +58 -0
- package/dist/combobox/tag/combobox-tag.svelte.d.ts +22 -0
- package/dist/combobox/tag/tag-context-provider.svelte +36 -0
- package/dist/combobox/tag/tag-context-provider.svelte.d.ts +14 -0
- package/dist/combobox/tag-remove/combobox-tag-remove.svelte +53 -0
- package/dist/combobox/tag-remove/combobox-tag-remove.svelte.d.ts +14 -0
- package/dist/combobox/tags/combobox-tags.svelte +50 -0
- package/dist/combobox/tags/combobox-tags.svelte.d.ts +20 -0
- package/dist/dialog/content/dialog-content.svelte +121 -0
- package/dist/dialog/content/dialog-content.svelte.d.ts +19 -0
- package/dist/dialog/index.d.ts +10 -0
- package/dist/dialog/index.js +15 -0
- package/dist/dialog/index.parts.d.ts +5 -0
- package/dist/dialog/index.parts.js +6 -0
- package/dist/dialog/overlay/dialog-overlay.svelte +39 -0
- package/dist/dialog/overlay/dialog-overlay.svelte.d.ts +12 -0
- package/dist/dialog/portal/dialog-portal.svelte +32 -0
- package/dist/dialog/portal/dialog-portal.svelte.d.ts +12 -0
- package/dist/dialog/root/context.d.ts +25 -0
- package/dist/dialog/root/context.js +8 -0
- package/dist/dialog/root/dialog-root.svelte +99 -0
- package/dist/dialog/root/dialog-root.svelte.d.ts +21 -0
- package/dist/dialog/root/dialog-stack.d.ts +32 -0
- package/dist/dialog/root/dialog-stack.js +55 -0
- package/dist/dialog/root/dialog-test.svelte +38 -0
- package/dist/dialog/root/dialog-test.svelte.d.ts +10 -0
- package/dist/dialog/root/dialog-with-combobox-test.svelte +61 -0
- package/dist/dialog/root/dialog-with-combobox-test.svelte.d.ts +7 -0
- package/dist/dialog/root/nested-dialog-test.svelte +63 -0
- package/dist/dialog/root/nested-dialog-test.svelte.d.ts +8 -0
- package/dist/dialog/root/types.d.ts +10 -0
- package/dist/dialog/root/types.js +1 -0
- package/dist/dialog/trigger/dialog-trigger.svelte +71 -0
- package/dist/dialog/trigger/dialog-trigger.svelte.d.ts +12 -0
- package/dist/hooks/use-virtual-focus.svelte.d.ts +55 -0
- package/dist/hooks/use-virtual-focus.svelte.js +201 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +19 -0
- package/dist/input/index.d.ts +3 -0
- package/dist/input/index.js +3 -0
- package/dist/input/input.svelte +19 -0
- package/dist/input/input.svelte.d.ts +8 -0
- package/dist/label/index.d.ts +3 -0
- package/dist/label/index.js +3 -0
- package/dist/label/label.svelte +21 -0
- package/dist/label/label.svelte.d.ts +8 -0
- package/dist/listbox/index.d.ts +6 -0
- package/dist/listbox/index.js +10 -0
- package/dist/listbox/index.parts.d.ts +2 -0
- package/dist/listbox/index.parts.js +3 -0
- package/dist/listbox/item/listbox-item.svelte +186 -0
- package/dist/listbox/item/listbox-item.svelte.d.ts +34 -0
- package/dist/listbox/root/context.d.ts +73 -0
- package/dist/listbox/root/context.js +249 -0
- package/dist/listbox/root/listbox-numeric-id-test.svelte +18 -0
- package/dist/listbox/root/listbox-numeric-id-test.svelte.d.ts +3 -0
- package/dist/listbox/root/listbox-test.svelte +27 -0
- package/dist/listbox/root/listbox-test.svelte.d.ts +8 -0
- package/dist/listbox/root/listbox.svelte +146 -0
- package/dist/listbox/root/listbox.svelte.d.ts +54 -0
- package/dist/popover/content/popover-content-test.svelte +43 -0
- package/dist/popover/content/popover-content-test.svelte.d.ts +12 -0
- package/dist/popover/content/popover-content.svelte +167 -0
- package/dist/popover/content/popover-content.svelte.d.ts +38 -0
- package/dist/popover/index.d.ts +8 -0
- package/dist/popover/index.js +14 -0
- package/dist/popover/index.parts.d.ts +4 -0
- package/dist/popover/index.parts.js +5 -0
- package/dist/popover/root/context.d.ts +24 -0
- package/dist/popover/root/context.js +10 -0
- package/dist/popover/root/popover-root.svelte +87 -0
- package/dist/popover/root/popover-root.svelte.d.ts +20 -0
- package/dist/popover/root/popover-test.svelte +40 -0
- package/dist/popover/root/popover-test.svelte.d.ts +11 -0
- package/dist/popover/trigger/popover-trigger-button.svelte +42 -0
- package/dist/popover/trigger/popover-trigger-button.svelte.d.ts +12 -0
- package/dist/popover/trigger/popover-trigger-in-dialog-test.svelte +29 -0
- package/dist/popover/trigger/popover-trigger-in-dialog-test.svelte.d.ts +18 -0
- package/dist/popover/trigger/popover-trigger.svelte +71 -0
- package/dist/popover/trigger/popover-trigger.svelte.d.ts +12 -0
- package/dist/portal/index.d.ts +1 -0
- package/dist/portal/index.js +1 -0
- package/dist/portal/portal.svelte +44 -0
- package/dist/portal/portal.svelte.d.ts +10 -0
- package/dist/primitives/aria-hide-outside.d.ts +38 -0
- package/dist/primitives/aria-hide-outside.js +152 -0
- package/dist/primitives/click-outside.d.ts +26 -0
- package/dist/primitives/click-outside.js +66 -0
- package/dist/primitives/floating.d.ts +57 -0
- package/dist/primitives/floating.js +179 -0
- package/dist/primitives/focus-trap.d.ts +19 -0
- package/dist/primitives/focus-trap.js +102 -0
- package/dist/primitives/index.d.ts +6 -0
- package/dist/primitives/index.js +7 -0
- package/dist/primitives/keyboard-navigation.d.ts +88 -0
- package/dist/primitives/keyboard-navigation.js +274 -0
- package/dist/primitives/scroll-lock.d.ts +19 -0
- package/dist/primitives/scroll-lock.js +62 -0
- package/dist/test-mocks/app-environment.d.ts +7 -0
- package/dist/test-mocks/app-environment.js +7 -0
- package/dist/test-mocks/app-navigation.d.ts +11 -0
- package/dist/test-mocks/app-navigation.js +11 -0
- package/dist/test-mocks/app-stores.d.ts +16 -0
- package/dist/test-mocks/app-stores.js +18 -0
- package/dist/utils/cn.d.ts +2 -0
- package/dist/utils/cn.js +5 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/package.json +99 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# ComboBox - Code Review & TODOs
|
|
2
|
+
|
|
3
|
+
Comprehensive review based on: **Accessibility**, **Scalability**, **Performance**, **Svelte 5 Runes Best Practices**.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🔊 Accesibilidad
|
|
8
|
+
|
|
9
|
+
### Completado ✅
|
|
10
|
+
|
|
11
|
+
- [x] ARIA pattern: `aria-activedescendant` para virtual focus
|
|
12
|
+
- [x] `aria-expanded`, `aria-haspopup`, `aria-controls` en input
|
|
13
|
+
- [x] `aria-label` en ListBox
|
|
14
|
+
- [x] `role="combobox"`, `role="listbox"`, `role="option"`
|
|
15
|
+
- [x] `aria-selected` en items seleccionados
|
|
16
|
+
- [x] `aria-disabled` en items/placeholder deshabilitados
|
|
17
|
+
- [x] Input soporta `aria-label` y `aria-labelledby` props
|
|
18
|
+
- [x] ListBox tiene ID para que `aria-controls` funcione correctamente
|
|
19
|
+
- [x] Button tiene `aria-controls` apuntando al listbox
|
|
20
|
+
- [x] Wrapper group soporta `aria-label` y `aria-labelledby`
|
|
21
|
+
- [x] Input soporta `aria-describedby` para instrucciones de uso
|
|
22
|
+
|
|
23
|
+
### Pendiente
|
|
24
|
+
|
|
25
|
+
- [ ] **Live regions para conteo de resultados**
|
|
26
|
+
- Agregar `<div aria-live="polite">` que anuncie "{N} resultados disponibles" al filtrar
|
|
27
|
+
- Importante para screen readers que no ven el cambio visual
|
|
28
|
+
|
|
29
|
+
- [ ] **Anuncio de selección**
|
|
30
|
+
- Anunciar "Item seleccionado: {label}" cuando se selecciona
|
|
31
|
+
- Usar `aria-live="assertive"` para cambios importantes
|
|
32
|
+
|
|
33
|
+
- [ ] **Soporte para grupos (sections)**
|
|
34
|
+
- Implementar `role="group"` con `aria-labelledby` para secciones
|
|
35
|
+
- Agregar `ComboBox.Section` component
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 📈 Escalabilidad
|
|
40
|
+
|
|
41
|
+
### Completado ✅
|
|
42
|
+
|
|
43
|
+
- [x] Hook `useVirtualFocus` reutilizable
|
|
44
|
+
- [x] Controlled/uncontrolled mode
|
|
45
|
+
- [x] Filtrado automático en items
|
|
46
|
+
- [x] `emptyPlaceholder` reactivo
|
|
47
|
+
|
|
48
|
+
### Pendiente
|
|
49
|
+
|
|
50
|
+
- [ ] **`filterFn` prop customizable**
|
|
51
|
+
- Actualmente filtrado es case-insensitive includes
|
|
52
|
+
- Permitir: fuzzy search, startsWith, exact match, async search
|
|
53
|
+
|
|
54
|
+
- [ ] **`allowCreate` prop**
|
|
55
|
+
- Permitir crear nuevos items cuando no hay match
|
|
56
|
+
- Callback `onCreate?: (value: string) => void`
|
|
57
|
+
|
|
58
|
+
- [x] **Multiple selection UI**
|
|
59
|
+
- Chips/tags para items seleccionados ✅ `ComboBox.Tags`, `ComboBox.Tag`, `ComboBox.TagRemove`
|
|
60
|
+
- Clear all button (disponible via `clearSelection()` en context)
|
|
61
|
+
- Contador de seleccionados (disponible via `selectedValue.size`)
|
|
62
|
+
- Navegación de tags con teclado (ArrowLeft/Right, Delete/Backspace)
|
|
63
|
+
- `ComboBox.ItemIndicator` para mostrar checks en items seleccionados
|
|
64
|
+
|
|
65
|
+
- [ ] **Form integration**
|
|
66
|
+
- `name` prop para `<form>` nativo
|
|
67
|
+
- Hidden input con valor serializado
|
|
68
|
+
- Validación con `required`, `aria-invalid`
|
|
69
|
+
|
|
70
|
+
- [ ] **Async data support**
|
|
71
|
+
- Props: `isLoading`, `loadingPlaceholder`
|
|
72
|
+
- Callback: `onLoadMore` para infinite scroll
|
|
73
|
+
- Debounce integrado para búsqueda async
|
|
74
|
+
|
|
75
|
+
- [ ] **Virtualization**
|
|
76
|
+
- Para listas grandes (>100 items)
|
|
77
|
+
- Integrar con `@tanstack/virtual` o similar
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## ⚡ Performance
|
|
82
|
+
|
|
83
|
+
### Completado ✅
|
|
84
|
+
|
|
85
|
+
- [x] Cache de DOM queries con invalidación (`cachedItemOrder`)
|
|
86
|
+
- [x] `untrack()` para evitar loops infinitos en effects
|
|
87
|
+
- [x] Subscription pattern para `itemCount` reactivo
|
|
88
|
+
- [x] Scoped queries via `containerRef`
|
|
89
|
+
|
|
90
|
+
### Pendiente
|
|
91
|
+
|
|
92
|
+
- [ ] **Memoización de `isVisible` en ListBoxItem**
|
|
93
|
+
- Actualmente se recalcula en cada render
|
|
94
|
+
- Considerar memoizar con `$derived` más granular
|
|
95
|
+
|
|
96
|
+
- [ ] **Batch registration**
|
|
97
|
+
- `registerItem` se llama por cada item individualmente
|
|
98
|
+
- Para listas grandes, batch notifications
|
|
99
|
+
|
|
100
|
+
- [ ] **Lazy itemLabels**
|
|
101
|
+
- El Map `itemLabels` crece con cada item
|
|
102
|
+
- Limpiar en unmount está implementado, pero considerar WeakMap
|
|
103
|
+
|
|
104
|
+
- [ ] **Effect cleanup optimizations**
|
|
105
|
+
- Revisar effects que podrían consolidarse
|
|
106
|
+
- `combobox-listboxitem.svelte` tiene 2 effects que podrían ser 1
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 🔧 Svelte 5 Runes Best Practices
|
|
111
|
+
|
|
112
|
+
### Completado ✅
|
|
113
|
+
|
|
114
|
+
- [x] `$state` para estado reactivo
|
|
115
|
+
- [x] `$derived` para valores computados
|
|
116
|
+
- [x] `$effect` con cleanup functions
|
|
117
|
+
- [x] `$bindable` para two-way binding
|
|
118
|
+
- [x] `$props()` para destructuring
|
|
119
|
+
- [x] `untrack()` para evitar re-runs innecesarios
|
|
120
|
+
- [x] `$derived(expression)` en vez de `$derived(() => ...)` - Simplificado en `combobox-listboxitem.svelte`
|
|
121
|
+
- [x] Effects consolidados - Usando 1 `$effect` + `onDestroy` en vez de 2 effects
|
|
122
|
+
|
|
123
|
+
### Revisado - No requiere cambios
|
|
124
|
+
|
|
125
|
+
- [x] **`$effect.pre`**: Revisado - no hay race conditions que lo requieran
|
|
126
|
+
- [x] **Context typing**: El type único es apropiado - tree-shaking no aplica a context objects
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 🧪 Testing
|
|
131
|
+
|
|
132
|
+
### Completado ✅
|
|
133
|
+
|
|
134
|
+
- [x] 291 tests unitarios pasando
|
|
135
|
+
- [x] Keyboard navigation tests
|
|
136
|
+
- [x] Selection tests
|
|
137
|
+
- [x] Filtering tests
|
|
138
|
+
- [x] Empty placeholder tests
|
|
139
|
+
- [x] ARIA accessibility tests (6 tests)
|
|
140
|
+
- [x] Edge cases: rapid typing, whitespace, backspace
|
|
141
|
+
- [x] Disabled/ReadOnly state tests
|
|
142
|
+
- [x] Trigger modes (focus, input, manual)
|
|
143
|
+
- [x] Selection behavior (Enter, click, Escape restoration)
|
|
144
|
+
- [x] Multi-select tests (12 tests)
|
|
145
|
+
- [x] Tags component tests (4 tests)
|
|
146
|
+
- [x] Tag component tests (13 tests) - incluye navegación por teclado
|
|
147
|
+
- [x] TagRemove component tests (6 tests)
|
|
148
|
+
- [x] ItemIndicator component tests (5 tests)
|
|
149
|
+
|
|
150
|
+
### Pendiente
|
|
151
|
+
|
|
152
|
+
- [ ] **Tests con muchos items (100+)** - performance tests
|
|
153
|
+
- [ ] **Visual regression tests** - screenshots de estados
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 📝 Documentación
|
|
158
|
+
|
|
159
|
+
- [ ] **JSDoc completo**
|
|
160
|
+
- Documentar todas las props públicas
|
|
161
|
+
- Ejemplos de uso en comments
|
|
162
|
+
|
|
163
|
+
- [ ] **Storybook/Demo page**
|
|
164
|
+
- Ejemplos interactivos de todos los casos de uso
|
|
165
|
+
- Estados: loading, error, disabled, readonly
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## 🎯 Próximos Pasos Priorizados
|
|
170
|
+
|
|
171
|
+
1. **Live regions** (accessibility - alto impacto)
|
|
172
|
+
2. **Form integration** (usabilidad - casos comunes)
|
|
173
|
+
3. **`filterFn` customizable** (escalabilidad)
|
|
174
|
+
4. **Consolidar effects** (performance/best practices)
|
|
175
|
+
5. **Async data support** (escalabilidad)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
4
|
+
import { useComboBoxContext } from '../root/context';
|
|
5
|
+
|
|
6
|
+
type ComboBoxButtonProps = HTMLButtonAttributes & {
|
|
7
|
+
class?: string;
|
|
8
|
+
children?: Snippet;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
let { class: className, children, tabindex = -1, ...restProps }: ComboBoxButtonProps = $props();
|
|
12
|
+
|
|
13
|
+
const ctx = useComboBoxContext();
|
|
14
|
+
|
|
15
|
+
// Use onmousedown with preventDefault to prevent blur from firing
|
|
16
|
+
// before the toggle. This prevents the race condition where:
|
|
17
|
+
// 1. Click button -> blur fires -> popover closes
|
|
18
|
+
// 2. Then onclick fires -> popover opens again
|
|
19
|
+
function handleMouseDown(e: MouseEvent) {
|
|
20
|
+
e.preventDefault();
|
|
21
|
+
if (!ctx.isDisabled && !ctx.isReadOnly) {
|
|
22
|
+
ctx.toggle();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<button
|
|
28
|
+
type="button"
|
|
29
|
+
{tabindex}
|
|
30
|
+
aria-label={ctx.isOpen ? 'Close menu' : 'Open menu'}
|
|
31
|
+
aria-expanded={ctx.isOpen}
|
|
32
|
+
aria-controls={`combobox-listbox-${ctx.instanceId}`}
|
|
33
|
+
disabled={ctx.isDisabled}
|
|
34
|
+
data-pressed={ctx.isOpen}
|
|
35
|
+
onmousedown={handleMouseDown}
|
|
36
|
+
class={className}
|
|
37
|
+
{...restProps}
|
|
38
|
+
>
|
|
39
|
+
{#if children}
|
|
40
|
+
{@render children()}
|
|
41
|
+
{:else}
|
|
42
|
+
<svg
|
|
43
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
44
|
+
width="16"
|
|
45
|
+
height="16"
|
|
46
|
+
viewBox="0 0 24 24"
|
|
47
|
+
fill="none"
|
|
48
|
+
stroke="currentColor"
|
|
49
|
+
stroke-width="2"
|
|
50
|
+
stroke-linecap="round"
|
|
51
|
+
stroke-linejoin="round"
|
|
52
|
+
class="transition-transform {ctx.isOpen ? 'rotate-180' : ''}"
|
|
53
|
+
>
|
|
54
|
+
<path d="m6 9 6 6 6-6" />
|
|
55
|
+
</svg>
|
|
56
|
+
{/if}
|
|
57
|
+
</button>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
3
|
+
type ComboBoxButtonProps = HTMLButtonAttributes & {
|
|
4
|
+
class?: string;
|
|
5
|
+
children?: Snippet;
|
|
6
|
+
};
|
|
7
|
+
declare const ComboboxButton: import("svelte").Component<ComboBoxButtonProps, {}, "">;
|
|
8
|
+
type ComboboxButton = ReturnType<typeof ComboboxButton>;
|
|
9
|
+
export default ComboboxButton;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * as ComboBox from './index.parts.ts';
|
|
2
|
+
export { default as ComboBoxRoot } from './root/combobox.svelte';
|
|
3
|
+
export { default as ComboBoxInput } from './input/combobox-input.svelte';
|
|
4
|
+
export { default as ComboBoxButton } from './button/combobox-button.svelte';
|
|
5
|
+
export { default as ComboBoxPopover } from './popover/combobox-popover.svelte';
|
|
6
|
+
export { default as ComboBoxList } from './list/combobox-listbox.svelte';
|
|
7
|
+
export { default as ComboBoxItem } from './item/combobox-listboxitem.svelte';
|
|
8
|
+
export { default as ComboBoxItemIndicator } from './item-indicator/combobox-item-indicator.svelte';
|
|
9
|
+
export { default as ComboBoxTags } from './tags/combobox-tags.svelte';
|
|
10
|
+
export { default as ComboBoxTag } from './tag/combobox-tag.svelte';
|
|
11
|
+
export { default as ComboBoxTagRemove } from './tag-remove/combobox-tag-remove.svelte';
|
|
12
|
+
export { getComboBoxContext, setComboBoxContext, useComboBoxContext, type ComboBoxContext } from './root/context.ts';
|
|
13
|
+
import * as ComboBoxParts from './index.parts.ts';
|
|
14
|
+
export default ComboBoxParts;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Namespace export for component composition: <ComboBox.Root>, <ComboBox.Input>, etc.
|
|
2
|
+
export * as ComboBox from './index.parts.ts';
|
|
3
|
+
// Direct named exports for individual imports
|
|
4
|
+
export { default as ComboBoxRoot } from './root/combobox.svelte';
|
|
5
|
+
export { default as ComboBoxInput } from './input/combobox-input.svelte';
|
|
6
|
+
export { default as ComboBoxButton } from './button/combobox-button.svelte';
|
|
7
|
+
export { default as ComboBoxPopover } from './popover/combobox-popover.svelte';
|
|
8
|
+
export { default as ComboBoxList } from './list/combobox-listbox.svelte';
|
|
9
|
+
export { default as ComboBoxItem } from './item/combobox-listboxitem.svelte';
|
|
10
|
+
export { default as ComboBoxItemIndicator } from './item-indicator/combobox-item-indicator.svelte';
|
|
11
|
+
export { default as ComboBoxTags } from './tags/combobox-tags.svelte';
|
|
12
|
+
export { default as ComboBoxTag } from './tag/combobox-tag.svelte';
|
|
13
|
+
export { default as ComboBoxTagRemove } from './tag-remove/combobox-tag-remove.svelte';
|
|
14
|
+
// Context and types
|
|
15
|
+
export { getComboBoxContext, setComboBoxContext, useComboBoxContext } from './root/context.ts';
|
|
16
|
+
// Default export as namespace object
|
|
17
|
+
import * as ComboBoxParts from './index.parts.ts';
|
|
18
|
+
export default ComboBoxParts;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { default as Root } from './root/combobox.svelte';
|
|
2
|
+
export { default as Input } from './input/combobox-input.svelte';
|
|
3
|
+
export { default as Button } from './button/combobox-button.svelte';
|
|
4
|
+
export { default as Popover } from './popover/combobox-popover.svelte';
|
|
5
|
+
export { default as List } from './list/combobox-listbox.svelte';
|
|
6
|
+
export { default as Item } from './item/combobox-listboxitem.svelte';
|
|
7
|
+
export { default as ItemIndicator } from './item-indicator/combobox-item-indicator.svelte';
|
|
8
|
+
export { default as Tags } from './tags/combobox-tags.svelte';
|
|
9
|
+
export { default as Tag } from './tag/combobox-tag.svelte';
|
|
10
|
+
export { default as TagRemove } from './tag-remove/combobox-tag-remove.svelte';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Short alias exports for namespace usage: ComboBox.Root, ComboBox.Input, etc.
|
|
2
|
+
export { default as Root } from './root/combobox.svelte';
|
|
3
|
+
export { default as Input } from './input/combobox-input.svelte';
|
|
4
|
+
export { default as Button } from './button/combobox-button.svelte';
|
|
5
|
+
export { default as Popover } from './popover/combobox-popover.svelte';
|
|
6
|
+
export { default as List } from './list/combobox-listbox.svelte';
|
|
7
|
+
export { default as Item } from './item/combobox-listboxitem.svelte';
|
|
8
|
+
export { default as ItemIndicator } from './item-indicator/combobox-item-indicator.svelte';
|
|
9
|
+
export { default as Tags } from './tags/combobox-tags.svelte';
|
|
10
|
+
export { default as Tag } from './tag/combobox-tag.svelte';
|
|
11
|
+
export { default as TagRemove } from './tag-remove/combobox-tag-remove.svelte';
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
3
|
+
import { useComboBoxContext } from '../root/context';
|
|
4
|
+
import { cn } from '../../utils/cn';
|
|
5
|
+
|
|
6
|
+
type ComboBoxInputProps = HTMLInputAttributes & {
|
|
7
|
+
/** Accessible label for the input */
|
|
8
|
+
'aria-label'?: string;
|
|
9
|
+
/** ID of element that labels this input */
|
|
10
|
+
'aria-labelledby'?: string;
|
|
11
|
+
/** ID of element that describes this input (e.g., usage instructions) */
|
|
12
|
+
'aria-describedby'?: string;
|
|
13
|
+
class?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
let {
|
|
17
|
+
'aria-label': ariaLabel,
|
|
18
|
+
'aria-labelledby': ariaLabelledby,
|
|
19
|
+
'aria-describedby': ariaDescribedby,
|
|
20
|
+
class: className,
|
|
21
|
+
...restProps
|
|
22
|
+
}: ComboBoxInputProps = $props();
|
|
23
|
+
|
|
24
|
+
let inputRef: HTMLInputElement | null = $state(null);
|
|
25
|
+
const ctx = useComboBoxContext();
|
|
26
|
+
|
|
27
|
+
$effect(() => {
|
|
28
|
+
if (inputRef) {
|
|
29
|
+
ctx.setInputRef(inputRef);
|
|
30
|
+
ctx.setTriggerRef(inputRef);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
function handleInput(event: Event) {
|
|
35
|
+
ctx.setFocusedTagId(null);
|
|
36
|
+
const target = event.target as HTMLInputElement;
|
|
37
|
+
ctx.setInputValue(target.value);
|
|
38
|
+
// Open on input for all trigger modes when user types
|
|
39
|
+
if (!ctx.isOpen && target.value.length > 0) {
|
|
40
|
+
ctx.open();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function handleFocus() {
|
|
45
|
+
// Open on focus if trigger is 'focus'
|
|
46
|
+
// Use a small delay to avoid opening immediately on programmatic focus
|
|
47
|
+
// (e.g., from a focus trap). This gives time for refs to be set up.
|
|
48
|
+
if (ctx.trigger === 'focus' && !ctx.isOpen) {
|
|
49
|
+
requestAnimationFrame(() => {
|
|
50
|
+
if (ctx.trigger === 'focus' && !ctx.isOpen) {
|
|
51
|
+
ctx.open();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function handleMouseDown() {
|
|
58
|
+
ctx.setFocusedTagId(null);
|
|
59
|
+
// Open on press if trigger is 'press'
|
|
60
|
+
if (ctx.trigger === 'press' && !ctx.isOpen && !ctx.isDisabled && !ctx.isReadOnly) {
|
|
61
|
+
ctx.open();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function handleBlur() {
|
|
66
|
+
// Restore selection label or deselect if empty
|
|
67
|
+
ctx.handleInputBlur();
|
|
68
|
+
}
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<input
|
|
72
|
+
bind:this={inputRef}
|
|
73
|
+
type="text"
|
|
74
|
+
role="combobox"
|
|
75
|
+
aria-autocomplete="list"
|
|
76
|
+
aria-expanded={ctx.isOpen}
|
|
77
|
+
aria-haspopup="listbox"
|
|
78
|
+
aria-controls={`combobox-listbox-${ctx.instanceId}`}
|
|
79
|
+
aria-activedescendant={ctx.focusedItemId !== null
|
|
80
|
+
? `combobox-item-${ctx.instanceId}-${ctx.focusedItemId}`
|
|
81
|
+
: undefined}
|
|
82
|
+
aria-label={ariaLabel}
|
|
83
|
+
aria-labelledby={ariaLabelledby}
|
|
84
|
+
aria-describedby={ariaDescribedby}
|
|
85
|
+
value={ctx.displayValue}
|
|
86
|
+
disabled={ctx.isDisabled}
|
|
87
|
+
readonly={ctx.isReadOnly}
|
|
88
|
+
oninput={handleInput}
|
|
89
|
+
onfocus={handleFocus}
|
|
90
|
+
onmousedown={handleMouseDown}
|
|
91
|
+
onblur={handleBlur}
|
|
92
|
+
onkeydown={ctx.handleKeydown}
|
|
93
|
+
class={cn(
|
|
94
|
+
'bg-depth-2 sunken placeholder:text-muted-foreground hover:bg-depth-1 focus:ring-border h-8 w-full rounded-xs border px-2 text-sm shadow-xs transition-all ease-out outline-none focus:ring focus:ring-offset-1 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
|
|
95
|
+
className
|
|
96
|
+
)}
|
|
97
|
+
{...restProps}
|
|
98
|
+
/>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
2
|
+
type ComboBoxInputProps = HTMLInputAttributes & {
|
|
3
|
+
/** Accessible label for the input */
|
|
4
|
+
'aria-label'?: string;
|
|
5
|
+
/** ID of element that labels this input */
|
|
6
|
+
'aria-labelledby'?: string;
|
|
7
|
+
/** ID of element that describes this input (e.g., usage instructions) */
|
|
8
|
+
'aria-describedby'?: string;
|
|
9
|
+
class?: string;
|
|
10
|
+
};
|
|
11
|
+
declare const ComboboxInput: import("svelte").Component<ComboBoxInputProps, {}, "">;
|
|
12
|
+
type ComboboxInput = ReturnType<typeof ComboboxInput>;
|
|
13
|
+
export default ComboboxInput;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ComboBox } from '../index';
|
|
3
|
+
|
|
4
|
+
let selected = $state<string | number | undefined>();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<ComboBox.Root bind:value={selected}>
|
|
8
|
+
<ComboBox.Input placeholder="Search countries..." />
|
|
9
|
+
<ComboBox.Button />
|
|
10
|
+
|
|
11
|
+
<ComboBox.Popover>
|
|
12
|
+
<ComboBox.List emptyPlaceholder="No countries found">
|
|
13
|
+
<!-- Intentionally omit textValue to validate implicit text extraction -->
|
|
14
|
+
<ComboBox.Item id="ar">Argentina</ComboBox.Item>
|
|
15
|
+
<ComboBox.Item id="br">Brazil</ComboBox.Item>
|
|
16
|
+
<ComboBox.Item id="ca">Canada</ComboBox.Item>
|
|
17
|
+
</ComboBox.List>
|
|
18
|
+
</ComboBox.Popover>
|
|
19
|
+
</ComboBox.Root>
|
|
20
|
+
|
|
21
|
+
<div data-testid="selected">{selected ?? ''}</div>
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export const COMBOBOX_ITEM_CONTEXT_KEY = Symbol.for('combobox-item');
|
|
3
|
+
|
|
4
|
+
export type ComboBoxItemContext = {
|
|
5
|
+
id: string | number;
|
|
6
|
+
};
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<script lang="ts">
|
|
10
|
+
import type { ComponentProps } from 'svelte';
|
|
11
|
+
import { untrack, onDestroy, setContext } from 'svelte';
|
|
12
|
+
import ListBoxItem from '../../listbox/item/listbox-item.svelte';
|
|
13
|
+
import { useComboBoxContext } from '../root/context';
|
|
14
|
+
import { cn } from '../../utils/cn';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* ComboBox.ListBoxItem wraps ListBox.Item and provides ComboBox-specific behavior:
|
|
18
|
+
* - Virtual focus (aria-activedescendant pattern)
|
|
19
|
+
* - Unique ID with instanceId for multiple comboboxes
|
|
20
|
+
* - Registration with ComboBox context for navigation
|
|
21
|
+
* - Scroll on focus for keyboard navigation
|
|
22
|
+
* - Automatic filtering based on inputValue
|
|
23
|
+
*/
|
|
24
|
+
type ComboBoxListBoxItemProps = Omit<
|
|
25
|
+
ComponentProps<typeof ListBoxItem>,
|
|
26
|
+
// Internal override props that ComboBox.ListBoxItem controls
|
|
27
|
+
| 'customId'
|
|
28
|
+
| 'disableFocusHandling'
|
|
29
|
+
| 'isFocusedOverride'
|
|
30
|
+
| 'onItemSelect'
|
|
31
|
+
| 'onResolvedTextValue'
|
|
32
|
+
| 'scrollOnFocus'
|
|
33
|
+
| 'isParentDisabled'
|
|
34
|
+
>;
|
|
35
|
+
|
|
36
|
+
let { id, class: className, ...props }: ComboBoxListBoxItemProps = $props();
|
|
37
|
+
|
|
38
|
+
const ctx = useComboBoxContext();
|
|
39
|
+
|
|
40
|
+
// Provide item context for child components like ItemIndicator
|
|
41
|
+
setContext<ComboBoxItemContext>(COMBOBOX_ITEM_CONTEXT_KEY, {
|
|
42
|
+
get id() {
|
|
43
|
+
return id;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Text value for filtering and display.
|
|
48
|
+
// If textValue prop is omitted, resolve it from rendered content on mount.
|
|
49
|
+
let resolvedTextValue = $state<string | null>(props.textValue ?? null);
|
|
50
|
+
const effectiveTextValue = $derived(resolvedTextValue ?? '');
|
|
51
|
+
|
|
52
|
+
$effect(() => {
|
|
53
|
+
if (props.textValue !== undefined) {
|
|
54
|
+
resolvedTextValue = props.textValue;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Normalized input for filtering comparison
|
|
59
|
+
const normalizedInput = $derived(ctx.inputValue.trim().toLowerCase());
|
|
60
|
+
|
|
61
|
+
// Automatic filtering: if text is not resolved yet, keep item visible until mount resolves it.
|
|
62
|
+
const isVisible = $derived(
|
|
63
|
+
!normalizedInput ||
|
|
64
|
+
!effectiveTextValue ||
|
|
65
|
+
effectiveTextValue.toLowerCase().includes(normalizedInput)
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Virtual focus from ComboBox context
|
|
69
|
+
const isFocused = $derived(ctx.focusedItemId === id);
|
|
70
|
+
|
|
71
|
+
// Generate unique ID using instanceId
|
|
72
|
+
const uniqueId = $derived(`combobox-item-${ctx.instanceId}-${id}`);
|
|
73
|
+
|
|
74
|
+
// Track registration state to avoid re-registering
|
|
75
|
+
let isRegistered = $state(false);
|
|
76
|
+
|
|
77
|
+
// Reactive registration: register when visible, unregister when hidden
|
|
78
|
+
$effect(() => {
|
|
79
|
+
const visible = isVisible;
|
|
80
|
+
const label = effectiveTextValue || String(id);
|
|
81
|
+
const itemId = id;
|
|
82
|
+
|
|
83
|
+
untrack(() => {
|
|
84
|
+
if (visible && !isRegistered) {
|
|
85
|
+
ctx.registerItem(itemId, label);
|
|
86
|
+
isRegistered = true;
|
|
87
|
+
} else if (!visible && isRegistered) {
|
|
88
|
+
ctx.unregisterItem(itemId);
|
|
89
|
+
isRegistered = false;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
function handleResolvedTextValue(label: string) {
|
|
95
|
+
if (!label) return;
|
|
96
|
+
resolvedTextValue = label;
|
|
97
|
+
// Update label map when already registered.
|
|
98
|
+
if (isRegistered) {
|
|
99
|
+
ctx.registerItem(id, label);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Cleanup on component destroy - use onDestroy for clearer semantics
|
|
104
|
+
onDestroy(() => {
|
|
105
|
+
if (isRegistered) {
|
|
106
|
+
ctx.unregisterItem(id);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Custom select handler that uses ComboBox context
|
|
111
|
+
function handleSelect(itemId: string | number, label: string) {
|
|
112
|
+
ctx.select(itemId, label);
|
|
113
|
+
}
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
{#if isVisible}
|
|
117
|
+
<ListBoxItem
|
|
118
|
+
{id}
|
|
119
|
+
{...props}
|
|
120
|
+
textValue={props.textValue}
|
|
121
|
+
customId={uniqueId}
|
|
122
|
+
disableFocusHandling={true}
|
|
123
|
+
isFocusedOverride={isFocused}
|
|
124
|
+
onItemSelect={handleSelect}
|
|
125
|
+
onResolvedTextValue={handleResolvedTextValue}
|
|
126
|
+
scrollOnFocus={true}
|
|
127
|
+
isParentDisabled={ctx.isDisabled}
|
|
128
|
+
class={cn(
|
|
129
|
+
'cursor-pointer px-3 py-2 transition-colors outline-none',
|
|
130
|
+
'data-focused:bg-accent data-hovered:bg-accent',
|
|
131
|
+
'data-selected:bg-primary/10 data-selected:font-medium',
|
|
132
|
+
'data-disabled:pointer-events-none data-disabled:opacity-50',
|
|
133
|
+
className
|
|
134
|
+
)}
|
|
135
|
+
/>
|
|
136
|
+
{/if}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const COMBOBOX_ITEM_CONTEXT_KEY: unique symbol;
|
|
2
|
+
export type ComboBoxItemContext = {
|
|
3
|
+
id: string | number;
|
|
4
|
+
};
|
|
5
|
+
import type { ComponentProps } from 'svelte';
|
|
6
|
+
import ListBoxItem from '../../listbox/item/listbox-item.svelte';
|
|
7
|
+
/**
|
|
8
|
+
* ComboBox.ListBoxItem wraps ListBox.Item and provides ComboBox-specific behavior:
|
|
9
|
+
* - Virtual focus (aria-activedescendant pattern)
|
|
10
|
+
* - Unique ID with instanceId for multiple comboboxes
|
|
11
|
+
* - Registration with ComboBox context for navigation
|
|
12
|
+
* - Scroll on focus for keyboard navigation
|
|
13
|
+
* - Automatic filtering based on inputValue
|
|
14
|
+
*/
|
|
15
|
+
type ComboBoxListBoxItemProps = Omit<ComponentProps<typeof ListBoxItem>, 'customId' | 'disableFocusHandling' | 'isFocusedOverride' | 'onItemSelect' | 'onResolvedTextValue' | 'scrollOnFocus' | 'isParentDisabled'>;
|
|
16
|
+
declare const ComboboxListboxitem: import("svelte").Component<ComboBoxListBoxItemProps, {}, "">;
|
|
17
|
+
type ComboboxListboxitem = ReturnType<typeof ComboboxListboxitem>;
|
|
18
|
+
export default ComboboxListboxitem;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
import { getContext } from 'svelte';
|
|
5
|
+
import { cn } from '../../utils/cn';
|
|
6
|
+
import { useComboBoxContext } from '../root/context';
|
|
7
|
+
import {
|
|
8
|
+
COMBOBOX_ITEM_CONTEXT_KEY,
|
|
9
|
+
type ComboBoxItemContext
|
|
10
|
+
} from '../item/combobox-listboxitem.svelte';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* ComboBox.ItemIndicator - Visual indicator shown when an item is selected.
|
|
14
|
+
* Must be used inside ComboBox.Item.
|
|
15
|
+
* Only renders when the parent item is selected.
|
|
16
|
+
*/
|
|
17
|
+
type ComboBoxItemIndicatorProps = {
|
|
18
|
+
/** Content to render when selected (defaults to checkmark icon) */
|
|
19
|
+
children?: Snippet;
|
|
20
|
+
/** Force show the indicator regardless of selection state */
|
|
21
|
+
forceMount?: boolean;
|
|
22
|
+
class?: string;
|
|
23
|
+
} & Omit<HTMLAttributes<HTMLSpanElement>, 'class' | 'children'>;
|
|
24
|
+
|
|
25
|
+
let {
|
|
26
|
+
children,
|
|
27
|
+
forceMount = false,
|
|
28
|
+
class: className,
|
|
29
|
+
...restProps
|
|
30
|
+
}: ComboBoxItemIndicatorProps = $props();
|
|
31
|
+
|
|
32
|
+
const comboboxCtx = useComboBoxContext();
|
|
33
|
+
const itemCtx = getContext<ComboBoxItemContext>(COMBOBOX_ITEM_CONTEXT_KEY);
|
|
34
|
+
|
|
35
|
+
const isSelected = $derived(comboboxCtx.selectedValue.has(itemCtx.id));
|
|
36
|
+
const shouldRender = $derived(forceMount || isSelected);
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
{#if shouldRender}
|
|
40
|
+
<span
|
|
41
|
+
aria-hidden="true"
|
|
42
|
+
data-state={isSelected ? 'checked' : 'unchecked'}
|
|
43
|
+
class={cn('inline-flex items-center justify-center', className)}
|
|
44
|
+
{...restProps}
|
|
45
|
+
>
|
|
46
|
+
{#if children}
|
|
47
|
+
{@render children()}
|
|
48
|
+
{:else}
|
|
49
|
+
<svg
|
|
50
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
51
|
+
viewBox="0 0 16 16"
|
|
52
|
+
fill="currentColor"
|
|
53
|
+
class="h-4 w-4"
|
|
54
|
+
>
|
|
55
|
+
<path
|
|
56
|
+
fill-rule="evenodd"
|
|
57
|
+
d="M12.416 3.376a.75.75 0 0 1 .208 1.04l-5 7.5a.75.75 0 0 1-1.154.114l-3-3a.75.75 0 0 1 1.06-1.06l2.353 2.353 4.493-6.74a.75.75 0 0 1 1.04-.207Z"
|
|
58
|
+
clip-rule="evenodd"
|
|
59
|
+
/>
|
|
60
|
+
</svg>
|
|
61
|
+
{/if}
|
|
62
|
+
</span>
|
|
63
|
+
{/if}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
/**
|
|
4
|
+
* ComboBox.ItemIndicator - Visual indicator shown when an item is selected.
|
|
5
|
+
* Must be used inside ComboBox.Item.
|
|
6
|
+
* Only renders when the parent item is selected.
|
|
7
|
+
*/
|
|
8
|
+
type ComboBoxItemIndicatorProps = {
|
|
9
|
+
/** Content to render when selected (defaults to checkmark icon) */
|
|
10
|
+
children?: Snippet;
|
|
11
|
+
/** Force show the indicator regardless of selection state */
|
|
12
|
+
forceMount?: boolean;
|
|
13
|
+
class?: string;
|
|
14
|
+
} & Omit<HTMLAttributes<HTMLSpanElement>, 'class' | 'children'>;
|
|
15
|
+
declare const ComboboxItemIndicator: import("svelte").Component<ComboBoxItemIndicatorProps, {}, "">;
|
|
16
|
+
type ComboboxItemIndicator = ReturnType<typeof ComboboxItemIndicator>;
|
|
17
|
+
export default ComboboxItemIndicator;
|