@purestack/ts-components 1.1.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 (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -0
  3. package/dist/ts-components.d.mts +1662 -0
  4. package/dist/ts-components.d.mts.map +1 -0
  5. package/dist/ts-components.mjs +9200 -0
  6. package/dist/ts-components.mjs.map +1 -0
  7. package/package.json +130 -0
  8. package/src/defineComponents.ts +87 -0
  9. package/src/index.ts +270 -0
  10. package/src/registerStyles.ts +63 -0
  11. package/src/standard/alertBox/alertBox.ts +64 -0
  12. package/src/standard/autoId.ts +25 -0
  13. package/src/standard/badge/badge.ts +41 -0
  14. package/src/standard/badge/badgeStyle.ts +21 -0
  15. package/src/standard/barChart/barChart.ts +338 -0
  16. package/src/standard/barChart/barChartStyle.ts +81 -0
  17. package/src/standard/btn/btn.ts +191 -0
  18. package/src/standard/btn/btnStyle.ts +59 -0
  19. package/src/standard/btnGroup/btnGroup.ts +208 -0
  20. package/src/standard/btnGroup/btnGroupStyle.ts +85 -0
  21. package/src/standard/chart/cartesianChart.ts +99 -0
  22. package/src/standard/chart/chartDefaults.ts +25 -0
  23. package/src/standard/chart/chartUtils.ts +84 -0
  24. package/src/standard/classicLogo/classicLogo.ts +429 -0
  25. package/src/standard/classicLogo/classicLogoStyle.ts +292 -0
  26. package/src/standard/componentVariant.ts +153 -0
  27. package/src/standard/composer/composer.ts +1035 -0
  28. package/src/standard/composer/composerCanvasInitialStyle.ts +225 -0
  29. package/src/standard/composer/composerCanvasRevertStyle.ts +14 -0
  30. package/src/standard/composer/composerCanvasSharedStyle.ts +60 -0
  31. package/src/standard/composer/composerCanvasStyleTypes.ts +8 -0
  32. package/src/standard/composer/composerCanvasThemeStyle.ts +53 -0
  33. package/src/standard/composer/composerStyle.ts +156 -0
  34. package/src/standard/composer/emailHtmlPolicy.ts +474 -0
  35. package/src/standard/consent/consent.ts +173 -0
  36. package/src/standard/consent/consentStyle.ts +44 -0
  37. package/src/standard/contactForm/contactForm.ts +135 -0
  38. package/src/standard/contactForm/contactFormStyle.ts +116 -0
  39. package/src/standard/doughnutChart/doughnutChart.ts +527 -0
  40. package/src/standard/doughnutChart/doughnutChartStyle.ts +64 -0
  41. package/src/standard/dropFiles/dropFiles.ts +480 -0
  42. package/src/standard/dropFiles/dropFilesStyle.ts +171 -0
  43. package/src/standard/expandablePanel/expandablePanel.ts +106 -0
  44. package/src/standard/expandablePanel/expandablePanelStyle.ts +56 -0
  45. package/src/standard/flex/flex.ts +269 -0
  46. package/src/standard/flex/flexStyle.ts +556 -0
  47. package/src/standard/footer/footer.ts +93 -0
  48. package/src/standard/footer/footerStyle.ts +39 -0
  49. package/src/standard/form/autoCompleteInput.ts +567 -0
  50. package/src/standard/form/form.ts +205 -0
  51. package/src/standard/form/formCheck.ts +96 -0
  52. package/src/standard/form/formInputField.ts +184 -0
  53. package/src/standard/form/formSelectField.ts +152 -0
  54. package/src/standard/form/formStyle.ts +585 -0
  55. package/src/standard/form/multiAutoCompleteInput.ts +880 -0
  56. package/src/standard/grid/grid.ts +177 -0
  57. package/src/standard/grid/gridStyle.ts +56 -0
  58. package/src/standard/icon/icon.ts +96 -0
  59. package/src/standard/icon/iconStyle.ts +50 -0
  60. package/src/standard/landing/codeShowcase.ts +54 -0
  61. package/src/standard/landing/comparisonColumn.ts +30 -0
  62. package/src/standard/landing/comparisonFeature.ts +22 -0
  63. package/src/standard/landing/comparisonTable.ts +38 -0
  64. package/src/standard/landing/ctaSection.ts +59 -0
  65. package/src/standard/landing/featureCard.ts +41 -0
  66. package/src/standard/landing/landing.ts +52 -0
  67. package/src/standard/landing/landingBand.ts +217 -0
  68. package/src/standard/landing/landingSection.ts +43 -0
  69. package/src/standard/landing/landingTypes.ts +141 -0
  70. package/src/standard/landing/metricItem.ts +25 -0
  71. package/src/standard/landing/metricStrip.ts +20 -0
  72. package/src/standard/lineChart/lineChart.ts +462 -0
  73. package/src/standard/lineChart/lineChartStyle.ts +91 -0
  74. package/src/standard/logo/logo.ts +192 -0
  75. package/src/standard/logo/logoStyle.ts +164 -0
  76. package/src/standard/modal/modal.ts +171 -0
  77. package/src/standard/modal/modalStore.ts +154 -0
  78. package/src/standard/modal/modalStyle.ts +190 -0
  79. package/src/standard/navMenu/navMenu.ts +278 -0
  80. package/src/standard/navMenu/navMenuStyle.ts +323 -0
  81. package/src/standard/pageLinks/pageLinks.ts +90 -0
  82. package/src/standard/pageScript/pageScript.ts +155 -0
  83. package/src/standard/pageToc/pageToc.ts +154 -0
  84. package/src/standard/pageToc/pageTocStyle.ts +372 -0
  85. package/src/standard/panel/panel.ts +55 -0
  86. package/src/standard/panel/panelStyle.ts +15 -0
  87. package/src/standard/pricing/pricing.ts +146 -0
  88. package/src/standard/searchBox/searchBox.ts +54 -0
  89. package/src/standard/searchBox/searchBoxStyle.ts +180 -0
  90. package/src/standard/sectionHeader/sectionHeader.ts +99 -0
  91. package/src/standard/signIn/signIn.ts +167 -0
  92. package/src/standard/signIn/signInStyle.ts +70 -0
  93. package/src/standard/tabs/tabs.ts +193 -0
  94. package/src/standard/tabs/tabsStyle.ts +231 -0
  95. package/src/standard/themeToggle/themeToggle.ts +33 -0
  96. package/src/standard/themeToggle/themeToggleStyle.ts +88 -0
  97. package/src/standard/toast/toastHost.ts +42 -0
  98. package/src/standard/toast/toastStore.ts +50 -0
  99. package/src/standard/topBar/topBar.ts +101 -0
  100. package/src/standard/topBar/topBarStyle.ts +169 -0
  101. package/src/standard/virtualList/variableVirtualTable.ts +371 -0
  102. package/src/standard/virtualList/virtualList.ts +423 -0
  103. package/src/standard/virtualList/virtualListStyle.ts +34 -0
  104. package/src/standard/virtualList/virtualTable.ts +179 -0
  105. package/src/standard/virtualList/virtualTableStyle.ts +24 -0
  106. package/tsconfig.json +23 -0
@@ -0,0 +1,567 @@
1
+ import type { SemanticTone } from '@purestack/ts-style'
2
+ import {
3
+ type ComponentHead,
4
+ type ComputedRef,
5
+ computed,
6
+ defineComponent,
7
+ type Emits,
8
+ html,
9
+ isRef,
10
+ type Ref,
11
+ type RefOrValue,
12
+ ref,
13
+ unref,
14
+ } from 'regor'
15
+ import { createAutoId } from '../autoId'
16
+ import type { ComponentVariant } from '../componentVariant'
17
+ import type { FormInputFieldType } from './formInputField'
18
+
19
+ const resolveAutoCompleteInputId = createAutoId('auto-complete-input')
20
+
21
+ export type AutoCompleteValue = string | number
22
+
23
+ export interface AutoCompleteOption {
24
+ label: RefOrValue<string>
25
+ value?: RefOrValue<AutoCompleteValue>
26
+ disabled?: RefOrValue<boolean>
27
+ keywords?: RefOrValue<string | string[]>
28
+ }
29
+
30
+ export interface ResolvedAutoCompleteOption {
31
+ label: string
32
+ value: AutoCompleteValue
33
+ disabled?: boolean
34
+ keywords: string[]
35
+ }
36
+
37
+ export interface AutoCompleteRow {
38
+ id: string
39
+ index: number
40
+ option: ResolvedAutoCompleteOption
41
+ active: boolean
42
+ selected: boolean
43
+ disabled?: boolean
44
+ classes: string
45
+ }
46
+
47
+ export interface AutoCompleteInput {
48
+ id?: RefOrValue<string>
49
+ label?: RefOrValue<string>
50
+ tone?: RefOrValue<SemanticTone>
51
+ variant?: RefOrValue<ComponentVariant>
52
+ model?: Ref<string>
53
+ selectedValue?: Ref<AutoCompleteValue | null>
54
+ type?: RefOrValue<FormInputFieldType>
55
+ name?: RefOrValue<string>
56
+ required?: RefOrValue<boolean>
57
+ autocomplete?: RefOrValue<string>
58
+ placeholder?: RefOrValue<string>
59
+ disabled?: RefOrValue<boolean>
60
+ icon?: RefOrValue<string>
61
+ iconEnd?: RefOrValue<string>
62
+ options?: RefOrValue<AutoCompleteOption[]>
63
+ rowComponent?: RefOrValue<string>
64
+ minLength?: RefOrValue<number | string>
65
+ maxResults?: RefOrValue<number | string>
66
+ openOnFocus?: RefOrValue<boolean>
67
+ loading?: RefOrValue<boolean>
68
+ emptyText?: RefOrValue<string>
69
+ loadingText?: RefOrValue<string>
70
+ onSelect?: (option: ResolvedAutoCompleteOption) => void
71
+ onSearch?: (query: string) => void
72
+ emits?: Emits<'optionselect' | 'querychange'>
73
+ listboxId?: string
74
+ isOpen?: Ref<boolean>
75
+ activeIndex?: Ref<number>
76
+ query?: ComputedRef<string>
77
+ resolvedOptions?: ComputedRef<ResolvedAutoCompleteOption[]>
78
+ filteredOptions?: ComputedRef<ResolvedAutoCompleteOption[]>
79
+ rows?: ComputedRef<AutoCompleteRow[]>
80
+ activeOptionId?: ComputedRef<string | undefined>
81
+ shouldRenderPopup?: ComputedRef<boolean>
82
+ showLoading?: ComputedRef<boolean>
83
+ showEmpty?: ComputedRef<boolean>
84
+ resolvedRowComponent?: ComputedRef<string>
85
+ resolvedIconEnd?: ComputedRef<string | undefined>
86
+ handleFocusIn?: () => void
87
+ handleFocusOut?: () => void
88
+ handleInput?: () => void
89
+ handleKeydown?: (event: KeyboardEvent) => void
90
+ handleOptionMouseDown?: (event: MouseEvent) => void
91
+ selectRow?: (row: AutoCompleteRow) => void
92
+ }
93
+
94
+ export interface AutoCompleteOptionRow {
95
+ option: RefOrValue<ResolvedAutoCompleteOption | null>
96
+ active?: RefOrValue<boolean>
97
+ selected?: RefOrValue<boolean>
98
+ query?: RefOrValue<string>
99
+ }
100
+
101
+ const autoCompleteInputTemplate = html`<div
102
+ class="auto-complete-input"
103
+ @focusin="handleFocusIn"
104
+ @focusout="handleFocusOut"
105
+ >
106
+ <FormInputField
107
+ :id="id"
108
+ :label="label"
109
+ :tone="tone"
110
+ :variant="variant"
111
+ :model="model"
112
+ :type="type || 'search'"
113
+ :name="name"
114
+ :required="required"
115
+ :autocomplete="autocomplete || 'off'"
116
+ :placeholder="placeholder"
117
+ :disabled="disabled"
118
+ :icon="icon"
119
+ :iconEnd="resolvedIconEnd"
120
+ role="combobox"
121
+ aria-autocomplete="list"
122
+ :aria-controls="listboxId"
123
+ :aria-expanded="isOpen"
124
+ :aria-activedescendant="activeOptionId"
125
+ @input="handleInput"
126
+ @keydown="handleKeydown"
127
+ />
128
+ <div
129
+ class="auto-complete-input__popup"
130
+ r-if="shouldRenderPopup"
131
+ >
132
+ <div
133
+ class="auto-complete-input__message"
134
+ r-if="showLoading"
135
+ role="status"
136
+ aria-live="polite"
137
+ >
138
+ {{ loadingText || 'Loading suggestions' }}
139
+ </div>
140
+ <div
141
+ class="auto-complete-input__message"
142
+ r-if="showEmpty"
143
+ role="status"
144
+ aria-live="polite"
145
+ >
146
+ {{ emptyText || 'No suggestions' }}
147
+ </div>
148
+ <div
149
+ class="auto-complete-input__listbox"
150
+ :id="listboxId"
151
+ role="listbox"
152
+ r-if="!showLoading && !showEmpty"
153
+ >
154
+ <div
155
+ class="auto-complete-input__option"
156
+ :class="row.classes"
157
+ role="option"
158
+ :id="row.id"
159
+ :aria-selected="row.active"
160
+ :aria-disabled="row.disabled"
161
+ r-for="row in rows"
162
+ @mousedown="handleOptionMouseDown"
163
+ @click="selectRow(row)"
164
+ >
165
+ <div
166
+ :is="resolvedRowComponent"
167
+ :option="row.option"
168
+ :item="row.option"
169
+ :index="row.index"
170
+ :query="query"
171
+ :active="row.active"
172
+ :selected="row.selected"
173
+ :disabled="row.disabled"
174
+ ></div>
175
+ </div>
176
+ </div>
177
+ </div>
178
+ </div>`
179
+
180
+ const autoCompleteOptionRowTemplate = html`<div class="auto-complete-input__default-row">
181
+ <span class="auto-complete-input__default-label">{{ option?.label }}</span>
182
+ <Icon
183
+ class="auto-complete-input__default-check"
184
+ name="lucide:check"
185
+ r-if="selected"
186
+ />
187
+ </div>`
188
+
189
+ function defineAutoCompleteInputComponent() {
190
+ return defineComponent<AutoCompleteInput>(autoCompleteInputTemplate, {
191
+ props: [
192
+ 'id',
193
+ 'label',
194
+ 'tone',
195
+ 'variant',
196
+ 'model',
197
+ 'selectedValue',
198
+ 'type',
199
+ 'name',
200
+ 'required',
201
+ 'autocomplete',
202
+ 'placeholder',
203
+ 'disabled',
204
+ 'icon',
205
+ 'iconEnd',
206
+ 'options',
207
+ 'rowComponent',
208
+ 'minLength',
209
+ 'maxResults',
210
+ 'openOnFocus',
211
+ 'loading',
212
+ 'emptyText',
213
+ 'loadingText',
214
+ 'onSelect',
215
+ 'onSearch',
216
+ ],
217
+ context: (head) => new AutoCompleteInputContext(head),
218
+ })
219
+ }
220
+
221
+ function defineAutoCompleteOptionRowComponent() {
222
+ return defineComponent<AutoCompleteOptionRow>(autoCompleteOptionRowTemplate, {
223
+ props: ['option', 'active', 'selected', 'query'],
224
+ context: (head) => head.props,
225
+ })
226
+ }
227
+
228
+ export function defineAutoCompleteInputComponents() {
229
+ return {
230
+ autoCompleteInput: defineAutoCompleteInputComponent(),
231
+ autoCompleteOptionRow: defineAutoCompleteOptionRowComponent(),
232
+ }
233
+ }
234
+
235
+ class AutoCompleteInputContext implements AutoCompleteInput {
236
+ readonly id: string
237
+ readonly label?: RefOrValue<string>
238
+ readonly tone?: RefOrValue<SemanticTone>
239
+ readonly variant?: RefOrValue<ComponentVariant>
240
+ readonly model: Ref<string>
241
+ readonly selectedValue: Ref<AutoCompleteValue | null>
242
+ readonly type?: RefOrValue<FormInputFieldType>
243
+ readonly name?: RefOrValue<string>
244
+ readonly required?: RefOrValue<boolean>
245
+ readonly autocomplete?: RefOrValue<string>
246
+ readonly placeholder?: RefOrValue<string>
247
+ readonly disabled?: RefOrValue<boolean>
248
+ readonly icon?: RefOrValue<string>
249
+ readonly iconEnd?: RefOrValue<string>
250
+ readonly options?: RefOrValue<AutoCompleteOption[]>
251
+ readonly rowComponent?: RefOrValue<string>
252
+ readonly minLength?: RefOrValue<number | string>
253
+ readonly maxResults?: RefOrValue<number | string>
254
+ readonly openOnFocus?: RefOrValue<boolean>
255
+ readonly loading?: RefOrValue<boolean>
256
+ readonly emptyText?: RefOrValue<string>
257
+ readonly loadingText?: RefOrValue<string>
258
+ readonly onSelect?: (option: ResolvedAutoCompleteOption) => void
259
+ readonly onSearch?: (query: string) => void
260
+ readonly emits?: Emits<'optionselect' | 'querychange'>
261
+ readonly listboxId: string
262
+ readonly isOpen = ref(false)
263
+ readonly activeIndex = ref(-1)
264
+ readonly query: ComputedRef<string>
265
+ readonly resolvedOptions: ComputedRef<ResolvedAutoCompleteOption[]>
266
+ readonly filteredOptions: ComputedRef<ResolvedAutoCompleteOption[]>
267
+ readonly rows: ComputedRef<AutoCompleteRow[]>
268
+ readonly activeOptionId: ComputedRef<string | undefined>
269
+ readonly shouldRenderPopup: ComputedRef<boolean>
270
+ readonly showLoading: ComputedRef<boolean>
271
+ readonly showEmpty: ComputedRef<boolean>
272
+ readonly resolvedRowComponent: ComputedRef<string>
273
+ readonly resolvedIconEnd: ComputedRef<string | undefined>
274
+ private readonly emit: ComponentHead<AutoCompleteInput>['emit']
275
+ private closingTimer: ReturnType<typeof setTimeout> | undefined
276
+
277
+ constructor(head: ComponentHead<AutoCompleteInput>) {
278
+ const props = head.props
279
+ Object.assign(this, props)
280
+ this.emit = head.emit
281
+ this.id = resolveAutoCompleteInputId(props.id)
282
+ this.listboxId = `${this.id}-listbox`
283
+ this.model = isRef(props.model) ? props.model : ref('')
284
+ this.selectedValue = isRef(props.selectedValue)
285
+ ? props.selectedValue
286
+ : ref<AutoCompleteValue | null>(null)
287
+ this.query = computed(() => String(unref(this.model) ?? ''))
288
+ this.resolvedOptions = computed(() => {
289
+ const options = unref(props.options)
290
+ return Array.isArray(options)
291
+ ? options.map(resolveAutoCompleteOption)
292
+ : []
293
+ })
294
+ this.filteredOptions = computed(() => this.resolveFilteredOptions())
295
+ this.rows = computed(() => this.resolveRows())
296
+ this.activeOptionId = computed(() => this.rows()[this.activeIndex()]?.id)
297
+ this.showLoading = computed(() => this.isOpen() && !!unref(this.loading))
298
+ this.showEmpty = computed(
299
+ () =>
300
+ this.isOpen() &&
301
+ !this.showLoading() &&
302
+ this.meetsMinimumQueryLength() &&
303
+ this.filteredOptions().length === 0,
304
+ )
305
+ this.shouldRenderPopup = computed(
306
+ () =>
307
+ this.isOpen() &&
308
+ (this.showLoading() ||
309
+ this.showEmpty() ||
310
+ this.filteredOptions().length > 0),
311
+ )
312
+ this.resolvedRowComponent = computed(() => {
313
+ const component = unref(props.rowComponent)
314
+ return typeof component === 'string' && component.trim()
315
+ ? component
316
+ : 'AutoCompleteOptionRow'
317
+ })
318
+ this.resolvedIconEnd = computed(
319
+ () =>
320
+ unref(props.iconEnd) ||
321
+ (unref(props.loading) ? 'lucide:loader-circle' : 'lucide:chevron-down'),
322
+ )
323
+ }
324
+
325
+ handleFocusIn = () => {
326
+ this.clearClosingTimer()
327
+ if (!this.shouldOpenOnFocus()) return
328
+
329
+ this.open()
330
+ }
331
+
332
+ handleFocusOut = () => {
333
+ this.clearClosingTimer()
334
+ this.closingTimer = setTimeout(() => {
335
+ this.close()
336
+ }, 0)
337
+ }
338
+
339
+ handleInput = () => {
340
+ const query = this.query()
341
+ this.onSearch?.(query)
342
+ this.emit('querychange', { query })
343
+ this.selectedValue(null)
344
+ if (this.meetsMinimumQueryLength()) this.open()
345
+ else this.close()
346
+ }
347
+
348
+ handleKeydown = (event: KeyboardEvent) => {
349
+ if (unref(this.disabled)) return
350
+
351
+ if (event.key === 'ArrowDown') {
352
+ event.preventDefault()
353
+ if (!this.isOpen()) {
354
+ this.open()
355
+ return
356
+ }
357
+ this.open()
358
+ this.moveActive(1)
359
+ return
360
+ }
361
+ if (event.key === 'ArrowUp') {
362
+ event.preventDefault()
363
+ if (!this.isOpen()) {
364
+ this.open()
365
+ this.activateLast()
366
+ return
367
+ }
368
+ this.open()
369
+ this.moveActive(-1)
370
+ return
371
+ }
372
+ if (event.key === 'Home' && this.isOpen()) {
373
+ event.preventDefault()
374
+ this.activateFirst()
375
+ return
376
+ }
377
+ if (event.key === 'End' && this.isOpen()) {
378
+ event.preventDefault()
379
+ this.activateLast()
380
+ return
381
+ }
382
+ if (event.key === 'Enter' && this.isOpen()) {
383
+ const row = this.rows()[this.activeIndex()]
384
+ if (!row) return
385
+
386
+ event.preventDefault()
387
+ this.selectRow(row)
388
+ return
389
+ }
390
+ if (event.key === 'Escape') {
391
+ this.close()
392
+ }
393
+ }
394
+
395
+ handleOptionMouseDown = (event: MouseEvent) => {
396
+ event.preventDefault()
397
+ this.clearClosingTimer()
398
+ }
399
+
400
+ selectRow = (row: AutoCompleteRow) => {
401
+ if (row.disabled || unref(this.disabled)) return
402
+
403
+ const option = row.option
404
+ this.model(option.label)
405
+ this.selectedValue(option.value)
406
+ this.onSelect?.(option)
407
+ this.emit('optionselect', { option })
408
+ this.close()
409
+ }
410
+
411
+ private resolveFilteredOptions() {
412
+ const query = normalizeSearchText(this.query())
413
+ if (!this.meetsMinimumQueryLength()) return []
414
+
415
+ const options = this.resolvedOptions().filter(
416
+ (option) =>
417
+ !query ||
418
+ option.keywords.some((keyword) =>
419
+ normalizeSearchText(keyword).includes(query),
420
+ ),
421
+ )
422
+ return options.slice(0, this.readPositiveInteger(this.maxResults, 10))
423
+ }
424
+
425
+ private resolveRows() {
426
+ const activeIndex = this.activeIndex()
427
+ const selectedValue = this.selectedValue()
428
+ return this.filteredOptions().map(
429
+ (option, index): AutoCompleteRow => ({
430
+ id: `${this.listboxId}-option-${index}`,
431
+ index,
432
+ option,
433
+ active: index === activeIndex,
434
+ selected: selectedValue !== null && option.value === selectedValue,
435
+ disabled: option.disabled,
436
+ classes: [
437
+ index === activeIndex ? 'auto-complete-input__option--active' : '',
438
+ selectedValue !== null && option.value === selectedValue
439
+ ? 'auto-complete-input__option--selected'
440
+ : '',
441
+ option.disabled ? 'auto-complete-input__option--disabled' : '',
442
+ ]
443
+ .filter(Boolean)
444
+ .join(' '),
445
+ }),
446
+ )
447
+ }
448
+
449
+ private open() {
450
+ if (unref(this.disabled) || !this.meetsMinimumQueryLength()) return
451
+
452
+ this.isOpen(true)
453
+ this.ensureActiveIndex()
454
+ }
455
+
456
+ private close() {
457
+ this.isOpen(false)
458
+ this.activeIndex(-1)
459
+ }
460
+
461
+ private moveActive(direction: 1 | -1) {
462
+ const rows = this.rows()
463
+ if (rows.length === 0) {
464
+ this.activeIndex(-1)
465
+ return
466
+ }
467
+
468
+ const current = this.activeIndex()
469
+ for (let offset = 1; offset <= rows.length; offset += 1) {
470
+ const next = wrapIndex(current + direction * offset, rows.length)
471
+ if (!rows[next]?.disabled) {
472
+ this.activeIndex(next)
473
+ return
474
+ }
475
+ }
476
+ }
477
+
478
+ private activateFirst() {
479
+ const index = this.rows().findIndex((row) => !row.disabled)
480
+ this.activeIndex(index)
481
+ }
482
+
483
+ private activateLast() {
484
+ const rows = this.rows()
485
+ for (let index = rows.length - 1; index >= 0; index -= 1) {
486
+ if (rows[index]?.disabled) continue
487
+
488
+ this.activeIndex(index)
489
+ return
490
+ }
491
+ this.activeIndex(-1)
492
+ }
493
+
494
+ private ensureActiveIndex() {
495
+ const rows = this.rows()
496
+ const current = this.activeIndex()
497
+ if (current >= 0 && current < rows.length && !rows[current]?.disabled)
498
+ return
499
+
500
+ this.activateFirst()
501
+ }
502
+
503
+ private shouldOpenOnFocus() {
504
+ return unref(this.openOnFocus) !== false && this.meetsMinimumQueryLength()
505
+ }
506
+
507
+ private meetsMinimumQueryLength() {
508
+ return (
509
+ this.query().trim().length >= this.readPositiveInteger(this.minLength, 0)
510
+ )
511
+ }
512
+
513
+ private readPositiveInteger(
514
+ value: RefOrValue<number | string> | undefined,
515
+ fallback: number,
516
+ ) {
517
+ const raw = unref(value)
518
+ const parsed =
519
+ typeof raw === 'number' ? raw : Number.parseFloat(String(raw))
520
+ return Number.isFinite(parsed) && parsed >= 0
521
+ ? Math.trunc(parsed)
522
+ : fallback
523
+ }
524
+
525
+ private clearClosingTimer() {
526
+ if (this.closingTimer === undefined) return
527
+
528
+ clearTimeout(this.closingTimer)
529
+ this.closingTimer = undefined
530
+ }
531
+ }
532
+
533
+ function resolveAutoCompleteOption(
534
+ optionInput: RefOrValue<AutoCompleteOption>,
535
+ ): ResolvedAutoCompleteOption {
536
+ const option = unref(optionInput)
537
+ const label = String(unref(option.label) ?? '')
538
+ const value = unref(option.value) ?? label
539
+ return {
540
+ label,
541
+ value,
542
+ disabled: unref(option.disabled),
543
+ keywords: resolveKeywords(option, label, value),
544
+ }
545
+ }
546
+
547
+ function resolveKeywords(
548
+ option: AutoCompleteOption,
549
+ label: string,
550
+ value: AutoCompleteValue,
551
+ ) {
552
+ const keywords = unref(option.keywords)
553
+ const source: string[] = Array.isArray(keywords)
554
+ ? keywords.map((keyword) => String(unref(keyword) ?? ''))
555
+ : typeof keywords === 'string'
556
+ ? keywords.split(/\s+/)
557
+ : []
558
+ return [label, String(value), ...source].filter(Boolean)
559
+ }
560
+
561
+ function normalizeSearchText(value: string) {
562
+ return value.trim().toLocaleLowerCase()
563
+ }
564
+
565
+ function wrapIndex(index: number, length: number) {
566
+ return ((index % length) + length) % length
567
+ }