ajo-ui 0.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 (103) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +126 -0
  3. package/dist/accordion.js +130 -0
  4. package/dist/avatar.js +54 -0
  5. package/dist/calendar.js +2 -0
  6. package/dist/carousel.js +239 -0
  7. package/dist/chart.js +790 -0
  8. package/dist/checkbox-group.js +70 -0
  9. package/dist/checkbox.js +77 -0
  10. package/dist/chunks/bar-CVafh6C1.js +99 -0
  11. package/dist/chunks/calendar-q8jZ8Cxr.js +1923 -0
  12. package/dist/chunks/collection-DtRB63U4.js +111 -0
  13. package/dist/chunks/data-table-DpkWv4y4.js +1039 -0
  14. package/dist/chunks/menu-B45IyHHC.js +704 -0
  15. package/dist/chunks/native-BJdhd9XJ.js +20 -0
  16. package/dist/chunks/popup-C8Bb3l_g.js +459 -0
  17. package/dist/chunks/popup-surface-BDCgtVU0.js +18 -0
  18. package/dist/chunks/position-D6_i_SRn.js +434 -0
  19. package/dist/chunks/virtual-list-B7hjGkjk.js +413 -0
  20. package/dist/collapsible.js +106 -0
  21. package/dist/command.js +263 -0
  22. package/dist/context-menu.js +112 -0
  23. package/dist/data-table.js +5 -0
  24. package/dist/dialog.js +207 -0
  25. package/dist/direction.js +22 -0
  26. package/dist/drawer.js +139 -0
  27. package/dist/field.js +26 -0
  28. package/dist/index.js +38 -0
  29. package/dist/input-date.js +994 -0
  30. package/dist/input-group.js +52 -0
  31. package/dist/input-otp.js +179 -0
  32. package/dist/menu.js +2 -0
  33. package/dist/menubar.js +236 -0
  34. package/dist/message-scroller.js +446 -0
  35. package/dist/navigation-menu.js +330 -0
  36. package/dist/popover.js +307 -0
  37. package/dist/progress.js +39 -0
  38. package/dist/radio-group.js +107 -0
  39. package/dist/resizable.js +172 -0
  40. package/dist/select.js +961 -0
  41. package/dist/sidebar.js +343 -0
  42. package/dist/slider.js +259 -0
  43. package/dist/switch.js +53 -0
  44. package/dist/tabs.js +182 -0
  45. package/dist/toast.js +492 -0
  46. package/dist/toggle-group.js +111 -0
  47. package/dist/toggle.js +52 -0
  48. package/dist/toolbar.js +127 -0
  49. package/dist/tooltip.js +196 -0
  50. package/dist/utils.js +104 -0
  51. package/dist/virtual-list.js +2 -0
  52. package/package.json +250 -0
  53. package/src/accordion.tsx +261 -0
  54. package/src/availability.ts +261 -0
  55. package/src/avatar.tsx +99 -0
  56. package/src/bar.ts +156 -0
  57. package/src/calendar.tsx +1441 -0
  58. package/src/carousel.tsx +424 -0
  59. package/src/chart.tsx +1194 -0
  60. package/src/checkbox-group.tsx +132 -0
  61. package/src/checkbox.tsx +130 -0
  62. package/src/collapsible.tsx +188 -0
  63. package/src/collection.ts +154 -0
  64. package/src/command.tsx +511 -0
  65. package/src/context-menu.tsx +233 -0
  66. package/src/data-table-contract.ts +143 -0
  67. package/src/data-table-model.ts +760 -0
  68. package/src/data-table.tsx +475 -0
  69. package/src/dialog.tsx +393 -0
  70. package/src/direction.tsx +45 -0
  71. package/src/drawer.tsx +251 -0
  72. package/src/field.tsx +61 -0
  73. package/src/index.ts +37 -0
  74. package/src/input-date.tsx +1539 -0
  75. package/src/input-group.tsx +142 -0
  76. package/src/input-otp.tsx +324 -0
  77. package/src/menu-cluster.ts +124 -0
  78. package/src/menu.tsx +1095 -0
  79. package/src/menubar.tsx +459 -0
  80. package/src/message-scroller.tsx +732 -0
  81. package/src/native.ts +26 -0
  82. package/src/navigation-menu.tsx +578 -0
  83. package/src/popover.tsx +519 -0
  84. package/src/popup-surface.tsx +31 -0
  85. package/src/popup.ts +569 -0
  86. package/src/position.ts +523 -0
  87. package/src/progress.tsx +70 -0
  88. package/src/radio-group.tsx +186 -0
  89. package/src/resizable.tsx +310 -0
  90. package/src/segments.ts +922 -0
  91. package/src/select.tsx +1501 -0
  92. package/src/sidebar.tsx +683 -0
  93. package/src/slider.tsx +424 -0
  94. package/src/switch.tsx +104 -0
  95. package/src/tabs.tsx +314 -0
  96. package/src/toast.tsx +923 -0
  97. package/src/toggle-group.tsx +249 -0
  98. package/src/toggle.tsx +91 -0
  99. package/src/toolbar.tsx +212 -0
  100. package/src/tooltip.tsx +359 -0
  101. package/src/utils.ts +204 -0
  102. package/src/virtual-list.tsx +205 -0
  103. package/src/virtual.ts +385 -0
@@ -0,0 +1,132 @@
1
+ import type { IntrinsicElements, Stateful, Stateless, WithChildren } from 'ajo'
2
+ import { selection, statefulRootAttrs as rootAttrs } from 'ajo-cloves'
3
+ import { context } from 'ajo/context'
4
+ import type { FixedArgs, OmitArg } from './utils'
5
+ import { flag, strings } from './utils'
6
+ import { Checkbox, type CheckboxArgs } from './checkbox'
7
+
8
+ /** Arguments for a group that coordinates checkbox values. */
9
+ export type CheckboxGroupArgs = WithChildren<OmitArg<IntrinsicElements['div'], 'defaultValue' | 'value'> & {
10
+ /** Controlled checked values. */
11
+ value?: string[]
12
+ /** Initial checked values for uncontrolled usage. */
13
+ defaultValue?: string[]
14
+ /** Called whenever the checked values change. */
15
+ onValueChange?: (value: string[], event: Event) => void
16
+ /** Disable every item in the group. */
17
+ disabled?: boolean
18
+ /** Shared checkbox input name. */
19
+ name?: string
20
+ }>
21
+
22
+ /** Arguments for one Checkbox registered with a CheckboxGroup. */
23
+ export type CheckboxGroupItemArgs = OmitArg<CheckboxArgs, 'checked' | 'defaultChecked' | 'onCheckedChange'> & {
24
+ /** Item value used by the parent checkbox group. */
25
+ value: string
26
+ } & FixedArgs<'checked' | 'defaultChecked' | 'onCheckedChange'>
27
+
28
+ type CheckboxGroupContextValue = {
29
+ checked: (value: string) => boolean
30
+ disabled?: boolean
31
+ name?: string
32
+ toggle: (value: string, checked: boolean, event: Event) => void
33
+ }
34
+
35
+ type CheckboxGroupRootArgs = WithChildren<{
36
+ defaultValue?: string[]
37
+ disabled?: boolean
38
+ name?: string
39
+ onValueChange?: (value: string[], event: Event) => void
40
+ value?: string[]
41
+ }>
42
+
43
+ const CheckboxGroupContext = context<CheckboxGroupContextValue | null>(null)
44
+
45
+ const CheckboxGroupRoot: Stateful<CheckboxGroupRootArgs> = function* ({ defaultValue }) {
46
+ let disabled = false
47
+ let name: string | undefined
48
+ let onValueChange: CheckboxGroupRootArgs['onValueChange']
49
+ const sel = selection(this, {
50
+ multiple: () => true,
51
+ fallback: strings(defaultValue),
52
+ onChange: (next, event) => onValueChange?.(next, event as Event),
53
+ })
54
+
55
+ const checked = (value: string) =>
56
+ sel.has(value)
57
+
58
+ const change = (value: string, _checked: boolean, event: Event) => {
59
+ if (disabled) return
60
+ sel.toggle(value, event)
61
+ }
62
+
63
+ for (const args of this) {
64
+ disabled = Boolean(args.disabled)
65
+ name = args.name
66
+ onValueChange = args.onValueChange
67
+ sel.sync(args.value != null ? strings(args.value) : undefined)
68
+
69
+ CheckboxGroupContext({ checked, disabled, name, toggle: change })
70
+
71
+ yield <>{args.children}</>
72
+ }
73
+ }
74
+
75
+
76
+ /** Unstyled checkbox group cascading checked values, name, and disabled state to its items. */
77
+ const CheckboxGroup: Stateless<CheckboxGroupArgs> = ({
78
+ children,
79
+ defaultValue,
80
+ disabled,
81
+ name,
82
+ onValueChange,
83
+ role = 'group',
84
+ value,
85
+ ...attrs
86
+ }) => {
87
+ const disabledFlag = Boolean(disabled)
88
+
89
+ return (
90
+ <CheckboxGroupRoot
91
+ {...rootAttrs(attrs)}
92
+ defaultValue={defaultValue}
93
+ disabled={disabledFlag}
94
+ name={name}
95
+ onValueChange={onValueChange}
96
+ value={value}
97
+ attr:aria-disabled={flag(disabledFlag)}
98
+ attr:data-disabled={flag(disabledFlag)}
99
+ attr:data-slot="checkbox-group"
100
+ attr:role={role}
101
+ >
102
+ {children}
103
+ </CheckboxGroupRoot>
104
+ )
105
+ }
106
+
107
+ /** Unstyled native checkbox item wired to the nearest group. */
108
+ const CheckboxGroupItem: Stateless<CheckboxGroupItemArgs> = ({
109
+ disabled,
110
+ name,
111
+ value,
112
+ ...attrs
113
+ }) => {
114
+ const group = CheckboxGroupContext()
115
+ const item = String(value)
116
+ const checked = group?.checked(item) ?? false
117
+
118
+ return (
119
+ <Checkbox
120
+ {...attrs}
121
+ checked={checked}
122
+ data-slot="checkbox-group-item"
123
+ disabled={disabled ?? group?.disabled}
124
+ name={name ?? group?.name}
125
+ set:checked={checked}
126
+ value={item}
127
+ onCheckedChange={(next, event) => group?.toggle(item, next, event)}
128
+ />
129
+ )
130
+ }
131
+
132
+ export { CheckboxGroup, CheckboxGroupItem }
@@ -0,0 +1,130 @@
1
+ import type { IntrinsicElements, Stateful, Stateless } from 'ajo'
2
+ import { callHandler, callRef, dom } from 'ajo-cloves'
3
+ import { ariaChecked, bool, flag, syncCheckedState, type CheckedState } from './utils'
4
+
5
+ /** Visual and ARIA state exposed by a Checkbox. */
6
+ export type CheckboxState = CheckedState
7
+
8
+ /** Arguments for the native checkbox and its visual companion. */
9
+ export type CheckboxArgs = IntrinsicElements['input'] & {
10
+ /** Classes for the checked indicator element. */
11
+ checkedIndicatorClass?: string
12
+ /** Classes for the native input element. */
13
+ inputClass?: string
14
+ /** Classes for the indeterminate indicator element. */
15
+ indeterminateIndicatorClass?: string
16
+ /** Called when the native checkbox state changes. */
17
+ onCheckedChange?: (checked: boolean, event: Event) => void
18
+ }
19
+
20
+ type CheckboxRootArgs = {
21
+ checkedIndicatorClass?: string
22
+ inputAttrs: IntrinsicElements['input']
23
+ inputClass?: string
24
+ indeterminateIndicatorClass?: string
25
+ onCheckedChange?: CheckboxArgs['onCheckedChange']
26
+ }
27
+
28
+ const stateFromAttrs = (attrs: IntrinsicElements['input']): CheckboxState => {
29
+ if (bool(attrs['set:indeterminate'])) return 'indeterminate'
30
+ return bool(attrs['set:checked'] ?? attrs.checked ?? attrs.defaultChecked) ? 'checked' : 'unchecked'
31
+ }
32
+
33
+ const CheckboxRoot: Stateful<CheckboxRootArgs, 'span'> = function* () {
34
+ let input: HTMLInputElement | null = null
35
+
36
+ const sync = () => {
37
+ if (input) syncCheckedState(input, dom(this) ? this : null)
38
+ }
39
+
40
+ for (const {
41
+ checkedIndicatorClass,
42
+ inputAttrs,
43
+ inputClass,
44
+ indeterminateIndicatorClass,
45
+ onCheckedChange,
46
+ } of this) {
47
+ const {
48
+ class: _class,
49
+ ref,
50
+ 'set:onchange': onChange,
51
+ 'set:oninput': onInput,
52
+ type: _type,
53
+ ...attrs
54
+ } = inputAttrs
55
+ const state = stateFromAttrs(inputAttrs)
56
+
57
+ if (dom(this)) this.dataset.state = state
58
+
59
+ yield (
60
+ <>
61
+ <input
62
+ {...attrs}
63
+ aria-checked={ariaChecked(state)}
64
+ class={inputClass}
65
+ data-slot="checkbox-input"
66
+ data-state={state}
67
+ ref={element => {
68
+ input = element
69
+ callRef(ref, element)
70
+ if (element) queueMicrotask(sync)
71
+ }}
72
+ set:onchange={(event: Event) => {
73
+ callHandler(onChange, event)
74
+ sync()
75
+ onCheckedChange?.((event.currentTarget as HTMLInputElement).checked, event)
76
+ }}
77
+ set:oninput={(event: Event) => {
78
+ callHandler(onInput, event)
79
+ sync()
80
+ }}
81
+ type="checkbox"
82
+ />
83
+ <span
84
+ aria-hidden="true"
85
+ class={checkedIndicatorClass}
86
+ data-slot="checkbox-indicator"
87
+ data-state="checked"
88
+ />
89
+ <span
90
+ aria-hidden="true"
91
+ class={indeterminateIndicatorClass}
92
+ data-slot="checkbox-indicator"
93
+ data-state="indeterminate"
94
+ />
95
+ </>
96
+ )
97
+ }
98
+ }
99
+
100
+ CheckboxRoot.is = 'span'
101
+
102
+ /** Unstyled native checkbox with state slots and form behavior. */
103
+ const Checkbox: Stateless<CheckboxArgs> = ({
104
+ checkedIndicatorClass,
105
+ class: classes,
106
+ disabled,
107
+ inputClass,
108
+ indeterminateIndicatorClass,
109
+ onCheckedChange,
110
+ ...attrs
111
+ }) => {
112
+ const inputAttrs = { ...attrs, disabled }
113
+ const state = stateFromAttrs(inputAttrs)
114
+
115
+ return (
116
+ <CheckboxRoot
117
+ checkedIndicatorClass={checkedIndicatorClass}
118
+ inputAttrs={inputAttrs}
119
+ inputClass={inputClass}
120
+ indeterminateIndicatorClass={indeterminateIndicatorClass}
121
+ onCheckedChange={onCheckedChange}
122
+ attr:class={classes}
123
+ attr:data-disabled={flag(disabled)}
124
+ attr:data-slot={attrs['data-slot'] ?? 'checkbox'}
125
+ attr:data-state={state}
126
+ />
127
+ )
128
+ }
129
+
130
+ export { Checkbox }
@@ -0,0 +1,188 @@
1
+ import type { IntrinsicElements, Stateful, Stateless, WithChildren } from 'ajo'
2
+ import type { OmitArg } from './utils'
3
+ import { callHandler, controlled, dom, id, statefulRootAttrs as rootAttrs } from 'ajo-cloves'
4
+ import { context } from 'ajo/context'
5
+ import { flag } from './utils'
6
+
7
+ /** Arguments for the controlled or uncontrolled disclosure root. */
8
+ export type CollapsibleArgs = WithChildren<OmitArg<IntrinsicElements['details'], 'open'> & {
9
+ /** Controlled open state. */
10
+ open?: boolean
11
+ /** Initial open state for uncontrolled usage. */
12
+ defaultOpen?: boolean
13
+ /** Disable the trigger. */
14
+ disabled?: boolean
15
+ /** Called whenever open state changes. */
16
+ onOpenChange?: (open: boolean, event?: Event) => void
17
+ }>
18
+
19
+ /** Arguments for the native summary that toggles a Collapsible. */
20
+ export type CollapsibleTriggerArgs = WithChildren<IntrinsicElements['summary'] & {
21
+ /** Disable the trigger. */
22
+ disabled?: boolean
23
+ /** Keep the trigger focusable but block toggling. */
24
+ locked?: boolean
25
+ }>
26
+
27
+ /** Arguments for the content revealed by a Collapsible. */
28
+ export type CollapsibleContentArgs = WithChildren<IntrinsicElements['div']>
29
+
30
+ type CollapsibleRootArgs = WithChildren<{
31
+ defaultOpen?: boolean
32
+ disabled?: boolean
33
+ onOpenChange?: (open: boolean, event?: Event) => void
34
+ open?: boolean
35
+ }>
36
+
37
+ /** Live disclosure state shared with composed Collapsible parts. */
38
+ export type CollapsibleContextValue = {
39
+ contentId: string
40
+ disabled: boolean
41
+ open: boolean
42
+ setOpen: (open: boolean, event?: Event) => void
43
+ triggerId: string
44
+ }
45
+
46
+ /** Composition context shared by Collapsible parts and derivative families such as Accordion. */
47
+ export const CollapsibleContext = context<CollapsibleContextValue | null>(null)
48
+
49
+ const CollapsibleRoot: Stateful<CollapsibleRootArgs, 'details'> = function* ({ defaultOpen, open }) {
50
+ const rootId = id('collapsible')
51
+ let disabled = false
52
+ let onOpenChange: CollapsibleRootArgs['onOpenChange']
53
+ const state = controlled<boolean>(this, {
54
+ fallback: Boolean(open ?? defaultOpen),
55
+ onChange: (next, event) => onOpenChange?.(next, event),
56
+ })
57
+
58
+ const syncRoot = () => {
59
+ if (!dom(this)) return
60
+
61
+ if (state.value) this.setAttribute('open', '')
62
+ else this.removeAttribute('open')
63
+
64
+ this.setAttribute('data-state', state.value ? 'open' : 'closed')
65
+ if (disabled) {
66
+ this.setAttribute('data-disabled', 'true')
67
+ this.setAttribute('aria-disabled', 'true')
68
+ } else {
69
+ this.removeAttribute('data-disabled')
70
+ this.removeAttribute('aria-disabled')
71
+ }
72
+ }
73
+
74
+ const setOpen = (next: boolean, event?: Event) => {
75
+ if (disabled || next === state.value) return
76
+ state.set(next, event)
77
+ }
78
+
79
+ for (const args of this) {
80
+ disabled = Boolean(args.disabled)
81
+ onOpenChange = args.onOpenChange
82
+ state.sync(args.open != null ? Boolean(args.open) : undefined)
83
+
84
+ syncRoot()
85
+
86
+ CollapsibleContext({
87
+ contentId: `${rootId}-content`,
88
+ disabled,
89
+ open: state.value,
90
+ setOpen,
91
+ triggerId: `${rootId}-trigger`,
92
+ })
93
+
94
+ yield <>{args.children}</>
95
+ }
96
+ }
97
+
98
+ CollapsibleRoot.is = 'details'
99
+
100
+ /** Unstyled collapsible disclosure rendered as a native details element. */
101
+ const Collapsible: Stateless<CollapsibleArgs> = ({
102
+ children,
103
+ defaultOpen,
104
+ disabled,
105
+ onOpenChange,
106
+ open,
107
+ ...attrs
108
+ }) => {
109
+ const opened = Boolean(open ?? defaultOpen)
110
+ const disabledFlag = Boolean(disabled)
111
+
112
+ return (
113
+ <CollapsibleRoot
114
+ attr:data-slot="collapsible"
115
+ {...rootAttrs(attrs)}
116
+ defaultOpen={defaultOpen}
117
+ disabled={disabledFlag}
118
+ onOpenChange={onOpenChange}
119
+ open={open}
120
+ attr:aria-disabled={flag(disabledFlag)}
121
+ attr:data-disabled={flag(disabledFlag)}
122
+ attr:data-state={opened ? 'open' : 'closed'}
123
+ attr:open={opened || undefined}
124
+ set:open={opened}
125
+ >
126
+ {children}
127
+ </CollapsibleRoot>
128
+ )
129
+ }
130
+
131
+ /** Unstyled disclosure trigger rendered as a native summary element. */
132
+ const CollapsibleTrigger: Stateless<CollapsibleTriggerArgs> = ({
133
+ children,
134
+ disabled,
135
+ id: idArg,
136
+ locked,
137
+ 'set:onclick': onClick,
138
+ ...attrs
139
+ }) => {
140
+ const collapsible = CollapsibleContext()
141
+ const disabledFlag = Boolean(disabled ?? collapsible?.disabled)
142
+ const unavailable = Boolean(disabledFlag || locked)
143
+
144
+ return (
145
+ <summary
146
+ data-slot="collapsible-trigger"
147
+ {...attrs}
148
+ aria-controls={collapsible?.contentId}
149
+ aria-disabled={unavailable ? 'true' : undefined}
150
+ aria-expanded={collapsible?.open ? 'true' : 'false'}
151
+ data-disabled={flag(disabledFlag)}
152
+ data-state={collapsible?.open ? 'open' : 'closed'}
153
+ id={idArg ?? collapsible?.triggerId}
154
+ set:onclick={(event: Event) => {
155
+ callHandler(onClick, event)
156
+ const prevented = event.defaultPrevented
157
+ event.preventDefault()
158
+ if (prevented || unavailable) return
159
+ collapsible?.setOpen(!collapsible.open, event)
160
+ }}
161
+ tabindex={disabledFlag ? -1 : undefined}
162
+ >
163
+ {children}
164
+ </summary>
165
+ )
166
+ }
167
+
168
+ /** Unstyled content panel natively shown or hidden by its details root. */
169
+ const CollapsibleContent: Stateless<CollapsibleContentArgs> = ({
170
+ children,
171
+ ...attrs
172
+ }) => {
173
+ const collapsible = CollapsibleContext()
174
+
175
+ return (
176
+ <div
177
+ data-slot="collapsible-content"
178
+ {...attrs}
179
+ aria-labelledby={collapsible?.triggerId}
180
+ data-state={collapsible?.open ? 'open' : 'closed'}
181
+ id={collapsible?.contentId}
182
+ >
183
+ {children}
184
+ </div>
185
+ )
186
+ }
187
+
188
+ export { Collapsible, CollapsibleContent, CollapsibleTrigger }
@@ -0,0 +1,154 @@
1
+ export type ItemsOptions = {
2
+ /** Exclude disabled items. Default: true. */
3
+ enabled?: boolean
4
+ /** Require rendered layout (an offsetParent). Default: true. */
5
+ rendered?: boolean
6
+ /** Exclude [hidden] items. Default: true. */
7
+ shown?: boolean
8
+ }
9
+
10
+ export type ItemAttrs = {
11
+ 'data-item': string
12
+ 'data-disabled'?: 'true'
13
+ 'data-label'?: string
14
+ 'data-value'?: string
15
+ }
16
+
17
+ type SweepOptions = {
18
+ empty?: string
19
+ group?: string
20
+ separator?: string
21
+ }
22
+
23
+ const enabledOf = (item: HTMLElement) =>
24
+ item.dataset.disabled !== 'true' && !(item as HTMLElement & { disabled?: boolean }).disabled
25
+
26
+ /**
27
+ * One item protocol for list-like families (menus, select, combobox, command):
28
+ * items are marked `data-item="<kind>"` and carry `data-value`, `data-label`,
29
+ * and `data-disabled`; the kind keeps nested families from cross-matching.
30
+ * Discovery is by DOM query (house pattern), highlight is `data-highlighted`.
31
+ */
32
+ export const collection = (kind: string, defaults: ItemsOptions = {}) => {
33
+ const selector = `[data-item="${kind}"]`
34
+ const sweepDefaults: Required<SweepOptions> = {
35
+ empty: `[data-slot="${kind}-empty"]`,
36
+ group: `[data-slot="${kind}-group"]`,
37
+ separator: `[data-slot="${kind}-separator"]`,
38
+ }
39
+
40
+ const all = (root: ParentNode | null | undefined) =>
41
+ root ? Array.from(root.querySelectorAll<HTMLElement>(selector)) : []
42
+
43
+ const items = (root: ParentNode | null | undefined, opts: ItemsOptions = {}) => {
44
+ const enabled = opts.enabled ?? defaults.enabled ?? true
45
+ const rendered = opts.rendered ?? defaults.rendered ?? true
46
+ const shown = opts.shown ?? defaults.shown ?? true
47
+
48
+ return all(root).filter(item =>
49
+ (!shown || !item.hidden) &&
50
+ (!rendered || item.offsetParent !== null) &&
51
+ (!enabled || enabledOf(item)))
52
+ }
53
+
54
+ const clearHighlight = (root: ParentNode | null | undefined) => {
55
+ for (const item of all(root)) delete item.dataset.highlighted
56
+ }
57
+
58
+ /** Marks one item highlighted and clears the rest; does not move focus. */
59
+ const highlight = (root: ParentNode | null | undefined, target: HTMLElement | undefined) => {
60
+ if (!target) return
61
+
62
+ target.dataset.highlighted = 'true'
63
+ for (const item of all(root)) {
64
+ if (item !== target) delete item.dataset.highlighted
65
+ }
66
+ }
67
+
68
+ /** Focus strategy: moves real focus and mirrors it in data-highlighted. */
69
+ const focusItem = (root: ParentNode | null | undefined, target: HTMLElement | undefined) => {
70
+ if (!target || !(root instanceof Node) || !root.contains(target)) return
71
+
72
+ target.focus()
73
+ highlight(root, target)
74
+ }
75
+
76
+ /** Focuses the first or last matching item. */
77
+ const edge = (root: ParentNode | null | undefined, which: 'first' | 'last', opts?: ItemsOptions) => {
78
+ const list = items(root, opts)
79
+ focusItem(root, which === 'first' ? list[0] : list[list.length - 1])
80
+ }
81
+
82
+ /** Resolves the item containing an event target. */
83
+ const item = (event: Event) =>
84
+ (event.target as HTMLElement | null)?.closest<HTMLElement>(selector)
85
+
86
+ /** Marker attrs for one item, meant for JSX spread. */
87
+ const attrs = (opts: { disabled?: boolean; label?: string; value?: string } = {}): ItemAttrs => ({
88
+ 'data-item': kind,
89
+ 'data-disabled': opts.disabled ? 'true' : undefined,
90
+ 'data-label': opts.label,
91
+ 'data-value': opts.value,
92
+ })
93
+
94
+ /**
95
+ * Hides empty groups (respecting data-force-mount) and toggles empty
96
+ * states against the current visible set; returns the visible items.
97
+ * Separators survive only between visible items: leading, trailing,
98
+ * and stacked separators hide as filtering empties their groups.
99
+ */
100
+ const sweep = (root: HTMLElement, opts?: SweepOptions) => {
101
+ const empty = opts?.empty ?? sweepDefaults.empty
102
+ const group = opts?.group ?? sweepDefaults.group
103
+ const separator = opts?.separator ?? sweepDefaults.separator
104
+
105
+ if (group) {
106
+ for (const element of root.querySelectorAll<HTMLElement>(group)) {
107
+ if (element.dataset.forceMount === 'true') {
108
+ element.hidden = false
109
+ continue
110
+ }
111
+ element.hidden = !element.querySelector<HTMLElement>(`${selector}:not([hidden])`)
112
+ }
113
+ }
114
+
115
+ // A group hidden by the previous filter makes its children non-rendered.
116
+ // Reconcile groups first so clearing the filter can measure them again.
117
+ const visible = items(root)
118
+
119
+ if (separator) {
120
+ const separators = new Set(root.querySelectorAll<HTMLElement>(separator))
121
+ const order = [...visible, ...separators].sort((a, b) =>
122
+ a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1)
123
+ const keep = new Set<HTMLElement>()
124
+
125
+ let afterItem = false
126
+ for (const element of order) {
127
+ if (!separators.has(element)) { afterItem = true; continue }
128
+ if (afterItem) keep.add(element)
129
+ afterItem = false
130
+ }
131
+
132
+ let beforeItem = false
133
+ for (let index = order.length - 1; index >= 0; index--) {
134
+ const element = order[index]
135
+ if (!separators.has(element)) { beforeItem = true; continue }
136
+ if (!beforeItem) keep.delete(element)
137
+ }
138
+
139
+ for (const separator of separators) separator.hidden = !keep.has(separator)
140
+ }
141
+
142
+ if (empty) {
143
+ for (const element of root.querySelectorAll<HTMLElement>(empty)) {
144
+ element.hidden = visible.length > 0
145
+ }
146
+ }
147
+
148
+ return visible
149
+ }
150
+
151
+ return { all, attrs, clearHighlight, edge, focusItem, highlight, item, items, selector, sweep }
152
+ }
153
+
154
+ export type Collection = ReturnType<typeof collection>