@llui/components 0.0.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.
Files changed (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -0
  3. package/dist/components/accordion.d.ts +115 -0
  4. package/dist/components/accordion.d.ts.map +1 -0
  5. package/dist/components/accordion.js +138 -0
  6. package/dist/components/alert-dialog.d.ts +45 -0
  7. package/dist/components/alert-dialog.d.ts.map +1 -0
  8. package/dist/components/alert-dialog.js +12 -0
  9. package/dist/components/angle-slider.d.ts +121 -0
  10. package/dist/components/angle-slider.d.ts.map +1 -0
  11. package/dist/components/angle-slider.js +145 -0
  12. package/dist/components/async-list.d.ts +104 -0
  13. package/dist/components/async-list.d.ts.map +1 -0
  14. package/dist/components/async-list.js +117 -0
  15. package/dist/components/avatar.d.ts +58 -0
  16. package/dist/components/avatar.d.ts.map +1 -0
  17. package/dist/components/avatar.js +43 -0
  18. package/dist/components/carousel.d.ts +128 -0
  19. package/dist/components/carousel.d.ts.map +1 -0
  20. package/dist/components/carousel.js +131 -0
  21. package/dist/components/cascade-select.d.ts +95 -0
  22. package/dist/components/cascade-select.d.ts.map +1 -0
  23. package/dist/components/cascade-select.js +100 -0
  24. package/dist/components/checkbox.d.ts +74 -0
  25. package/dist/components/checkbox.d.ts.map +1 -0
  26. package/dist/components/checkbox.js +73 -0
  27. package/dist/components/clipboard.d.ts +72 -0
  28. package/dist/components/clipboard.d.ts.map +1 -0
  29. package/dist/components/clipboard.js +73 -0
  30. package/dist/components/collapsible.d.ts +64 -0
  31. package/dist/components/collapsible.d.ts.map +1 -0
  32. package/dist/components/collapsible.js +51 -0
  33. package/dist/components/color-picker.d.ts +125 -0
  34. package/dist/components/color-picker.d.ts.map +1 -0
  35. package/dist/components/color-picker.js +169 -0
  36. package/dist/components/combobox.d.ts +163 -0
  37. package/dist/components/combobox.d.ts.map +1 -0
  38. package/dist/components/combobox.js +345 -0
  39. package/dist/components/context-menu.d.ts +105 -0
  40. package/dist/components/context-menu.d.ts.map +1 -0
  41. package/dist/components/context-menu.js +177 -0
  42. package/dist/components/date-input.d.ts +117 -0
  43. package/dist/components/date-input.d.ts.map +1 -0
  44. package/dist/components/date-input.js +149 -0
  45. package/dist/components/date-picker.d.ts +142 -0
  46. package/dist/components/date-picker.d.ts.map +1 -0
  47. package/dist/components/date-picker.js +294 -0
  48. package/dist/components/dialog.d.ts +152 -0
  49. package/dist/components/dialog.d.ts.map +1 -0
  50. package/dist/components/dialog.js +140 -0
  51. package/dist/components/drawer.d.ts +106 -0
  52. package/dist/components/drawer.d.ts.map +1 -0
  53. package/dist/components/drawer.js +136 -0
  54. package/dist/components/editable.d.ts +92 -0
  55. package/dist/components/editable.d.ts.map +1 -0
  56. package/dist/components/editable.js +112 -0
  57. package/dist/components/file-upload.d.ts +251 -0
  58. package/dist/components/file-upload.d.ts.map +1 -0
  59. package/dist/components/file-upload.js +324 -0
  60. package/dist/components/floating-panel.d.ts +171 -0
  61. package/dist/components/floating-panel.d.ts.map +1 -0
  62. package/dist/components/floating-panel.js +198 -0
  63. package/dist/components/hover-card.d.ts +85 -0
  64. package/dist/components/hover-card.d.ts.map +1 -0
  65. package/dist/components/hover-card.js +128 -0
  66. package/dist/components/image-cropper.d.ts +129 -0
  67. package/dist/components/image-cropper.d.ts.map +1 -0
  68. package/dist/components/image-cropper.js +208 -0
  69. package/dist/components/index.d.ts +109 -0
  70. package/dist/components/index.d.ts.map +1 -0
  71. package/dist/components/index.js +54 -0
  72. package/dist/components/listbox.d.ts +98 -0
  73. package/dist/components/listbox.d.ts.map +1 -0
  74. package/dist/components/listbox.js +174 -0
  75. package/dist/components/marquee.d.ts +84 -0
  76. package/dist/components/marquee.d.ts.map +1 -0
  77. package/dist/components/marquee.js +73 -0
  78. package/dist/components/menu.d.ts +131 -0
  79. package/dist/components/menu.d.ts.map +1 -0
  80. package/dist/components/menu.js +262 -0
  81. package/dist/components/navigation-menu.d.ts +111 -0
  82. package/dist/components/navigation-menu.d.ts.map +1 -0
  83. package/dist/components/navigation-menu.js +102 -0
  84. package/dist/components/number-input.d.ts +106 -0
  85. package/dist/components/number-input.d.ts.map +1 -0
  86. package/dist/components/number-input.js +178 -0
  87. package/dist/components/pagination.d.ts +113 -0
  88. package/dist/components/pagination.d.ts.map +1 -0
  89. package/dist/components/pagination.js +135 -0
  90. package/dist/components/password-input.d.ts +64 -0
  91. package/dist/components/password-input.d.ts.map +1 -0
  92. package/dist/components/password-input.js +52 -0
  93. package/dist/components/pin-input.d.ts +89 -0
  94. package/dist/components/pin-input.d.ts.map +1 -0
  95. package/dist/components/pin-input.js +139 -0
  96. package/dist/components/popover.d.ts +116 -0
  97. package/dist/components/popover.d.ts.map +1 -0
  98. package/dist/components/popover.js +146 -0
  99. package/dist/components/presence.d.ts +71 -0
  100. package/dist/components/presence.d.ts.map +1 -0
  101. package/dist/components/presence.js +57 -0
  102. package/dist/components/progress.d.ts +74 -0
  103. package/dist/components/progress.d.ts.map +1 -0
  104. package/dist/components/progress.js +80 -0
  105. package/dist/components/qr-code.d.ts +114 -0
  106. package/dist/components/qr-code.d.ts.map +1 -0
  107. package/dist/components/qr-code.js +108 -0
  108. package/dist/components/radio-group.d.ts +89 -0
  109. package/dist/components/radio-group.d.ts.map +1 -0
  110. package/dist/components/radio-group.js +161 -0
  111. package/dist/components/rating-group.d.ts +88 -0
  112. package/dist/components/rating-group.d.ts.map +1 -0
  113. package/dist/components/rating-group.js +122 -0
  114. package/dist/components/scroll-area.d.ts +124 -0
  115. package/dist/components/scroll-area.d.ts.map +1 -0
  116. package/dist/components/scroll-area.js +152 -0
  117. package/dist/components/select.d.ts +161 -0
  118. package/dist/components/select.d.ts.map +1 -0
  119. package/dist/components/select.js +333 -0
  120. package/dist/components/signature-pad.d.ts +138 -0
  121. package/dist/components/signature-pad.d.ts.map +1 -0
  122. package/dist/components/signature-pad.js +142 -0
  123. package/dist/components/slider.d.ts +117 -0
  124. package/dist/components/slider.d.ts.map +1 -0
  125. package/dist/components/slider.js +210 -0
  126. package/dist/components/splitter.d.ts +87 -0
  127. package/dist/components/splitter.d.ts.map +1 -0
  128. package/dist/components/splitter.js +119 -0
  129. package/dist/components/steps.d.ts +104 -0
  130. package/dist/components/steps.d.ts.map +1 -0
  131. package/dist/components/steps.js +133 -0
  132. package/dist/components/switch.d.ts +66 -0
  133. package/dist/components/switch.d.ts.map +1 -0
  134. package/dist/components/switch.js +59 -0
  135. package/dist/components/tabs.d.ts +146 -0
  136. package/dist/components/tabs.d.ts.map +1 -0
  137. package/dist/components/tabs.js +244 -0
  138. package/dist/components/tags-input.d.ts +118 -0
  139. package/dist/components/tags-input.d.ts.map +1 -0
  140. package/dist/components/tags-input.js +168 -0
  141. package/dist/components/time-picker.d.ts +121 -0
  142. package/dist/components/time-picker.d.ts.map +1 -0
  143. package/dist/components/time-picker.js +147 -0
  144. package/dist/components/timer.d.ts +131 -0
  145. package/dist/components/timer.d.ts.map +1 -0
  146. package/dist/components/timer.js +117 -0
  147. package/dist/components/toast.d.ts +119 -0
  148. package/dist/components/toast.d.ts.map +1 -0
  149. package/dist/components/toast.js +102 -0
  150. package/dist/components/toc.d.ts +119 -0
  151. package/dist/components/toc.d.ts.map +1 -0
  152. package/dist/components/toc.js +107 -0
  153. package/dist/components/toggle-group.d.ts +80 -0
  154. package/dist/components/toggle-group.d.ts.map +1 -0
  155. package/dist/components/toggle-group.js +93 -0
  156. package/dist/components/toggle.d.ts +47 -0
  157. package/dist/components/toggle.d.ts.map +1 -0
  158. package/dist/components/toggle.js +41 -0
  159. package/dist/components/tooltip.d.ts +92 -0
  160. package/dist/components/tooltip.d.ts.map +1 -0
  161. package/dist/components/tooltip.js +147 -0
  162. package/dist/components/tour.d.ts +145 -0
  163. package/dist/components/tour.d.ts.map +1 -0
  164. package/dist/components/tour.js +133 -0
  165. package/dist/components/tree-view.d.ts +216 -0
  166. package/dist/components/tree-view.d.ts.map +1 -0
  167. package/dist/components/tree-view.js +293 -0
  168. package/dist/index.d.ts +3 -0
  169. package/dist/index.d.ts.map +1 -0
  170. package/dist/index.js +4 -0
  171. package/dist/patterns/confirm-dialog.d.ts +92 -0
  172. package/dist/patterns/confirm-dialog.d.ts.map +1 -0
  173. package/dist/patterns/confirm-dialog.js +92 -0
  174. package/dist/patterns/index.d.ts +3 -0
  175. package/dist/patterns/index.d.ts.map +1 -0
  176. package/dist/patterns/index.js +1 -0
  177. package/dist/utils/anatomy.d.ts +40 -0
  178. package/dist/utils/anatomy.d.ts.map +1 -0
  179. package/dist/utils/anatomy.js +41 -0
  180. package/dist/utils/aria-hidden.d.ts +12 -0
  181. package/dist/utils/aria-hidden.d.ts.map +1 -0
  182. package/dist/utils/aria-hidden.js +72 -0
  183. package/dist/utils/dismissable.d.ts +25 -0
  184. package/dist/utils/dismissable.d.ts.map +1 -0
  185. package/dist/utils/dismissable.js +65 -0
  186. package/dist/utils/dom.d.ts +8 -0
  187. package/dist/utils/dom.d.ts.map +1 -0
  188. package/dist/utils/dom.js +21 -0
  189. package/dist/utils/floating.d.ts +44 -0
  190. package/dist/utils/floating.d.ts.map +1 -0
  191. package/dist/utils/floating.js +44 -0
  192. package/dist/utils/focus-trap.d.ts +18 -0
  193. package/dist/utils/focus-trap.d.ts.map +1 -0
  194. package/dist/utils/focus-trap.js +85 -0
  195. package/dist/utils/focusables.d.ts +6 -0
  196. package/dist/utils/focusables.d.ts.map +1 -0
  197. package/dist/utils/focusables.js +65 -0
  198. package/dist/utils/index.d.ts +18 -0
  199. package/dist/utils/index.d.ts.map +1 -0
  200. package/dist/utils/index.js +10 -0
  201. package/dist/utils/interact-outside.d.ts +26 -0
  202. package/dist/utils/interact-outside.d.ts.map +1 -0
  203. package/dist/utils/interact-outside.js +46 -0
  204. package/dist/utils/remove-scroll.d.ts +8 -0
  205. package/dist/utils/remove-scroll.d.ts.map +1 -0
  206. package/dist/utils/remove-scroll.js +37 -0
  207. package/dist/utils/tree-collection.d.ts +61 -0
  208. package/dist/utils/tree-collection.d.ts.map +1 -0
  209. package/dist/utils/tree-collection.js +137 -0
  210. package/dist/utils/typeahead.d.ts +49 -0
  211. package/dist/utils/typeahead.d.ts.map +1 -0
  212. package/dist/utils/typeahead.js +81 -0
  213. package/package.json +282 -0
@@ -0,0 +1,345 @@
1
+ import { show, portal, onMount, div } from '@llui/dom';
2
+ import { pushDismissable } from '../utils/dismissable';
3
+ import { attachFloating } from '../utils/floating';
4
+ export function init(opts = {}) {
5
+ const items = opts.items ?? [];
6
+ const disabledItems = opts.disabledItems ?? [];
7
+ const inputValue = opts.inputValue ?? '';
8
+ return {
9
+ open: false,
10
+ value: opts.value ?? [],
11
+ inputValue,
12
+ items,
13
+ disabledItems,
14
+ filteredItems: filterItems(items, inputValue),
15
+ highlightedIndex: null,
16
+ selectionMode: opts.selectionMode ?? 'single',
17
+ disabled: opts.disabled ?? false,
18
+ };
19
+ }
20
+ function filterItems(items, query) {
21
+ if (query === '')
22
+ return items;
23
+ const q = query.toLowerCase();
24
+ return items.filter((item) => item.toLowerCase().includes(q));
25
+ }
26
+ function nextEnabledIndex(items, disabled, from, delta) {
27
+ if (items.length === 0)
28
+ return null;
29
+ const start = from === null ? (delta === 1 ? -1 : items.length) : from;
30
+ const n = items.length;
31
+ for (let i = 1; i <= n; i++) {
32
+ const idx = (start + delta * i + n * n) % n;
33
+ if (!disabled.includes(items[idx]))
34
+ return idx;
35
+ }
36
+ return null;
37
+ }
38
+ function firstEnabledIndex(items, disabled) {
39
+ for (let i = 0; i < items.length; i++) {
40
+ if (!disabled.includes(items[i]))
41
+ return i;
42
+ }
43
+ return null;
44
+ }
45
+ function lastEnabledIndex(items, disabled) {
46
+ for (let i = items.length - 1; i >= 0; i--) {
47
+ if (!disabled.includes(items[i]))
48
+ return i;
49
+ }
50
+ return null;
51
+ }
52
+ function applySelection(state, value) {
53
+ if (state.disabledItems.includes(value))
54
+ return state.value;
55
+ if (state.selectionMode === 'single')
56
+ return [value];
57
+ const isActive = state.value.includes(value);
58
+ return isActive ? state.value.filter((v) => v !== value) : [...state.value, value];
59
+ }
60
+ export function update(state, msg) {
61
+ if (state.disabled && msg.type !== 'setItems')
62
+ return [state, []];
63
+ switch (msg.type) {
64
+ case 'open':
65
+ return [
66
+ {
67
+ ...state,
68
+ open: true,
69
+ highlightedIndex: firstEnabledIndex(state.filteredItems, state.disabledItems),
70
+ },
71
+ [],
72
+ ];
73
+ case 'close':
74
+ return [{ ...state, open: false, highlightedIndex: null }, []];
75
+ case 'setInputValue': {
76
+ const filteredItems = filterItems(state.items, msg.value);
77
+ return [
78
+ {
79
+ ...state,
80
+ inputValue: msg.value,
81
+ filteredItems,
82
+ open: true,
83
+ highlightedIndex: firstEnabledIndex(filteredItems, state.disabledItems),
84
+ },
85
+ [],
86
+ ];
87
+ }
88
+ case 'selectOption': {
89
+ const value = applySelection(state, msg.value);
90
+ const inputValue = state.selectionMode === 'single' ? msg.value : '';
91
+ const filteredItems = filterItems(state.items, inputValue);
92
+ const open = state.selectionMode === 'single' ? false : state.open;
93
+ return [
94
+ {
95
+ ...state,
96
+ value,
97
+ inputValue,
98
+ filteredItems,
99
+ open,
100
+ highlightedIndex: open ? state.highlightedIndex : null,
101
+ },
102
+ [],
103
+ ];
104
+ }
105
+ case 'setValue':
106
+ return [{ ...state, value: msg.value }, []];
107
+ case 'clear':
108
+ return [
109
+ { ...state, value: [], inputValue: '', filteredItems: state.items, highlightedIndex: null },
110
+ [],
111
+ ];
112
+ case 'highlight':
113
+ return [{ ...state, highlightedIndex: msg.index }, []];
114
+ case 'highlightNext':
115
+ return [
116
+ {
117
+ ...state,
118
+ highlightedIndex: nextEnabledIndex(state.filteredItems, state.disabledItems, state.highlightedIndex, 1),
119
+ },
120
+ [],
121
+ ];
122
+ case 'highlightPrev':
123
+ return [
124
+ {
125
+ ...state,
126
+ highlightedIndex: nextEnabledIndex(state.filteredItems, state.disabledItems, state.highlightedIndex, -1),
127
+ },
128
+ [],
129
+ ];
130
+ case 'highlightFirst':
131
+ return [
132
+ { ...state, highlightedIndex: firstEnabledIndex(state.filteredItems, state.disabledItems) },
133
+ [],
134
+ ];
135
+ case 'highlightLast':
136
+ return [
137
+ { ...state, highlightedIndex: lastEnabledIndex(state.filteredItems, state.disabledItems) },
138
+ [],
139
+ ];
140
+ case 'selectHighlighted': {
141
+ if (state.highlightedIndex === null)
142
+ return [state, []];
143
+ const v = state.filteredItems[state.highlightedIndex];
144
+ if (v === undefined)
145
+ return [state, []];
146
+ const value = applySelection(state, v);
147
+ const inputValue = state.selectionMode === 'single' ? v : '';
148
+ const filteredItems = filterItems(state.items, inputValue);
149
+ const open = state.selectionMode === 'single' ? false : state.open;
150
+ return [
151
+ {
152
+ ...state,
153
+ value,
154
+ inputValue,
155
+ filteredItems,
156
+ open,
157
+ highlightedIndex: open ? state.highlightedIndex : null,
158
+ },
159
+ [],
160
+ ];
161
+ }
162
+ case 'setItems': {
163
+ const disabled = msg.disabled ?? state.disabledItems;
164
+ const value = state.value.filter((v) => msg.items.includes(v) && !disabled.includes(v));
165
+ return [
166
+ {
167
+ ...state,
168
+ items: msg.items,
169
+ disabledItems: disabled,
170
+ filteredItems: filterItems(msg.items, state.inputValue),
171
+ value,
172
+ },
173
+ [],
174
+ ];
175
+ }
176
+ }
177
+ }
178
+ export function connect(get, send, opts) {
179
+ const base = opts.id;
180
+ const inputId = `${base}:input`;
181
+ const contentId = `${base}:content`;
182
+ const itemId = (index) => `${base}:item:${index}`;
183
+ const triggerLabel = opts.triggerLabel ?? 'Toggle options';
184
+ return {
185
+ root: {
186
+ role: 'combobox',
187
+ 'aria-expanded': (s) => get(s).open,
188
+ 'aria-controls': contentId,
189
+ 'aria-haspopup': 'listbox',
190
+ 'data-scope': 'combobox',
191
+ 'data-part': 'root',
192
+ 'data-state': (s) => (get(s).open ? 'open' : 'closed'),
193
+ },
194
+ input: {
195
+ type: 'text',
196
+ role: 'combobox',
197
+ autoComplete: 'off',
198
+ 'aria-autocomplete': 'list',
199
+ 'aria-expanded': (s) => get(s).open,
200
+ 'aria-controls': contentId,
201
+ 'aria-activedescendant': (s) => {
202
+ const idx = get(s).highlightedIndex;
203
+ return idx === null ? undefined : itemId(idx);
204
+ },
205
+ 'aria-disabled': (s) => (get(s).disabled ? 'true' : undefined),
206
+ id: inputId,
207
+ disabled: (s) => get(s).disabled,
208
+ value: (s) => get(s).inputValue,
209
+ 'data-scope': 'combobox',
210
+ 'data-part': 'input',
211
+ onInput: (e) => {
212
+ const value = e.target.value;
213
+ send({ type: 'setInputValue', value });
214
+ },
215
+ onKeyDown: (e) => {
216
+ switch (e.key) {
217
+ case 'ArrowDown':
218
+ e.preventDefault();
219
+ send({ type: 'open' });
220
+ send({ type: 'highlightNext' });
221
+ return;
222
+ case 'ArrowUp':
223
+ e.preventDefault();
224
+ send({ type: 'open' });
225
+ send({ type: 'highlightPrev' });
226
+ return;
227
+ case 'Home':
228
+ e.preventDefault();
229
+ send({ type: 'highlightFirst' });
230
+ return;
231
+ case 'End':
232
+ e.preventDefault();
233
+ send({ type: 'highlightLast' });
234
+ return;
235
+ case 'Enter':
236
+ e.preventDefault();
237
+ send({ type: 'selectHighlighted' });
238
+ return;
239
+ case 'Escape':
240
+ e.preventDefault();
241
+ send({ type: 'close' });
242
+ return;
243
+ }
244
+ },
245
+ onFocus: () => send({ type: 'open' }),
246
+ },
247
+ trigger: {
248
+ type: 'button',
249
+ 'aria-label': triggerLabel,
250
+ 'aria-expanded': (s) => get(s).open,
251
+ 'aria-controls': contentId,
252
+ tabIndex: -1,
253
+ 'data-scope': 'combobox',
254
+ 'data-part': 'trigger',
255
+ onClick: () => send({ type: 'open' }),
256
+ },
257
+ positioner: {
258
+ 'data-scope': 'combobox',
259
+ 'data-part': 'positioner',
260
+ style: 'position:absolute;top:0;left:0;',
261
+ },
262
+ content: {
263
+ role: 'listbox',
264
+ id: contentId,
265
+ 'aria-labelledby': inputId,
266
+ tabIndex: -1,
267
+ 'data-state': (s) => (get(s).open ? 'open' : 'closed'),
268
+ 'data-scope': 'combobox',
269
+ 'data-part': 'content',
270
+ },
271
+ item: (value, index) => ({
272
+ item: {
273
+ role: 'option',
274
+ id: itemId(index),
275
+ 'aria-selected': (s) => get(s).value.includes(value),
276
+ 'aria-disabled': (s) => (get(s).disabledItems.includes(value) ? 'true' : undefined),
277
+ 'data-state': (s) => (get(s).value.includes(value) ? 'selected' : undefined),
278
+ 'data-highlighted': (s) => (get(s).highlightedIndex === index ? '' : undefined),
279
+ 'data-disabled': (s) => (get(s).disabledItems.includes(value) ? '' : undefined),
280
+ 'data-scope': 'combobox',
281
+ 'data-part': 'item',
282
+ 'data-value': value,
283
+ 'data-index': String(index),
284
+ onClick: () => send({ type: 'selectOption', value }),
285
+ onPointerMove: () => send({ type: 'highlight', index }),
286
+ },
287
+ }),
288
+ empty: {
289
+ 'data-scope': 'combobox',
290
+ 'data-part': 'empty',
291
+ },
292
+ };
293
+ }
294
+ export function overlay(opts) {
295
+ const target = opts.target ?? 'body';
296
+ const placement = opts.placement ?? 'bottom-start';
297
+ const offset = opts.offset ?? 4;
298
+ const flip = opts.flip !== false;
299
+ const shift = opts.shift !== false;
300
+ const sameWidth = opts.sameWidth !== false;
301
+ const parts = opts.parts;
302
+ const contentId = parts.content.id;
303
+ const inputId = parts.input.id;
304
+ return show({
305
+ when: (s) => opts.get(s).open,
306
+ render: () => portal({
307
+ target,
308
+ render: () => {
309
+ onMount(() => {
310
+ const contentEl = document.getElementById(contentId);
311
+ const inputEl = document.getElementById(inputId);
312
+ if (!contentEl || !inputEl)
313
+ return;
314
+ const cleanups = [];
315
+ const positioner = contentEl.closest('[data-part="positioner"]');
316
+ const floatingEl = positioner ?? contentEl;
317
+ if (sameWidth) {
318
+ floatingEl.style.minWidth = `${inputEl.offsetWidth}px`;
319
+ }
320
+ cleanups.push(attachFloating({
321
+ anchor: inputEl,
322
+ floating: floatingEl,
323
+ placement,
324
+ offset,
325
+ flip,
326
+ shift,
327
+ }));
328
+ cleanups.push(pushDismissable({
329
+ element: contentEl,
330
+ ignore: () => [inputEl],
331
+ onDismiss: () => opts.send({ type: 'close' }),
332
+ }));
333
+ return () => {
334
+ for (let i = cleanups.length - 1; i >= 0; i--)
335
+ cleanups[i]();
336
+ };
337
+ });
338
+ return [div(parts.positioner, opts.content())];
339
+ },
340
+ }),
341
+ enter: opts.transition?.enter,
342
+ leave: opts.transition?.leave,
343
+ });
344
+ }
345
+ export const combobox = { init, update, connect, overlay };
@@ -0,0 +1,105 @@
1
+ import type { Send, TransitionOptions } from '@llui/dom';
2
+ /**
3
+ * Context menu — right-click (contextmenu) triggered menu positioned at
4
+ * the pointer. Unlike regular menu, it has no trigger button — the user
5
+ * right-clicks anywhere in the associated region.
6
+ *
7
+ * Uses raw x/y positioning instead of floating-ui (pointer is the anchor,
8
+ * not an element).
9
+ */
10
+ export interface ContextMenuState {
11
+ open: boolean;
12
+ x: number;
13
+ y: number;
14
+ items: string[];
15
+ disabledItems: string[];
16
+ highlighted: string | null;
17
+ }
18
+ export type ContextMenuMsg = {
19
+ type: 'openAt';
20
+ x: number;
21
+ y: number;
22
+ } | {
23
+ type: 'close';
24
+ } | {
25
+ type: 'highlight';
26
+ value: string | null;
27
+ } | {
28
+ type: 'highlightNext';
29
+ } | {
30
+ type: 'highlightPrev';
31
+ } | {
32
+ type: 'selectHighlighted';
33
+ } | {
34
+ type: 'select';
35
+ value: string;
36
+ } | {
37
+ type: 'setItems';
38
+ items: string[];
39
+ disabled?: string[];
40
+ };
41
+ export interface ContextMenuInit {
42
+ items?: string[];
43
+ disabledItems?: string[];
44
+ }
45
+ export declare function init(opts?: ContextMenuInit): ContextMenuState;
46
+ export declare function update(state: ContextMenuState, msg: ContextMenuMsg): [ContextMenuState, never[]];
47
+ export interface ContextMenuItemParts<S> {
48
+ item: {
49
+ role: 'menuitem';
50
+ id: string;
51
+ 'aria-disabled': (s: S) => 'true' | undefined;
52
+ 'data-state': (s: S) => 'highlighted' | undefined;
53
+ 'data-disabled': (s: S) => '' | undefined;
54
+ 'data-scope': 'context-menu';
55
+ 'data-part': 'item';
56
+ 'data-value': string;
57
+ tabIndex: -1;
58
+ onClick: (e: MouseEvent) => void;
59
+ onPointerMove: (e: PointerEvent) => void;
60
+ };
61
+ }
62
+ export interface ContextMenuParts<S> {
63
+ /** The element users right-click to open the menu. */
64
+ trigger: {
65
+ 'data-scope': 'context-menu';
66
+ 'data-part': 'trigger';
67
+ onContextMenu: (e: MouseEvent) => void;
68
+ };
69
+ positioner: {
70
+ 'data-scope': 'context-menu';
71
+ 'data-part': 'positioner';
72
+ style: (s: S) => string;
73
+ };
74
+ content: {
75
+ role: 'menu';
76
+ id: string;
77
+ tabIndex: -1;
78
+ 'data-state': (s: S) => 'open' | 'closed';
79
+ 'data-scope': 'context-menu';
80
+ 'data-part': 'content';
81
+ onKeyDown: (e: KeyboardEvent) => void;
82
+ };
83
+ item: (value: string) => ContextMenuItemParts<S>;
84
+ }
85
+ export interface ConnectOptions {
86
+ id: string;
87
+ onSelect?: (value: string) => void;
88
+ }
89
+ export declare function connect<S>(get: (s: S) => ContextMenuState, send: Send<ContextMenuMsg>, opts: ConnectOptions): ContextMenuParts<S>;
90
+ export interface OverlayOptions<S> {
91
+ get: (s: S) => ContextMenuState;
92
+ send: Send<ContextMenuMsg>;
93
+ parts: ContextMenuParts<S>;
94
+ content: () => Node[];
95
+ transition?: TransitionOptions;
96
+ target?: string | HTMLElement;
97
+ }
98
+ export declare function overlay<S>(opts: OverlayOptions<S>): Node[];
99
+ export declare const contextMenu: {
100
+ init: typeof init;
101
+ update: typeof update;
102
+ connect: typeof connect;
103
+ overlay: typeof overlay;
104
+ };
105
+ //# sourceMappingURL=context-menu.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-menu.d.ts","sourceRoot":"","sources":["../../src/components/context-menu.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,177 @@
1
+ import { show, portal, onMount, div } from '@llui/dom';
2
+ import { pushDismissable } from '../utils/dismissable';
3
+ export function init(opts = {}) {
4
+ return {
5
+ open: false,
6
+ x: 0,
7
+ y: 0,
8
+ items: opts.items ?? [],
9
+ disabledItems: opts.disabledItems ?? [],
10
+ highlighted: null,
11
+ };
12
+ }
13
+ function firstEnabled(items, disabled) {
14
+ for (const v of items)
15
+ if (!disabled.includes(v))
16
+ return v;
17
+ return null;
18
+ }
19
+ function nextEnabled(items, disabled, from, delta) {
20
+ if (items.length === 0)
21
+ return null;
22
+ const start = from === null ? -1 : items.indexOf(from);
23
+ const n = items.length;
24
+ for (let i = 1; i <= n; i++) {
25
+ const idx = start === -1 && delta === 1 ? i - 1 : (start + delta * i + n * n) % n;
26
+ const v = items[idx];
27
+ if (!disabled.includes(v))
28
+ return v;
29
+ }
30
+ return null;
31
+ }
32
+ export function update(state, msg) {
33
+ switch (msg.type) {
34
+ case 'openAt':
35
+ return [
36
+ {
37
+ ...state,
38
+ open: true,
39
+ x: msg.x,
40
+ y: msg.y,
41
+ highlighted: firstEnabled(state.items, state.disabledItems),
42
+ },
43
+ [],
44
+ ];
45
+ case 'close':
46
+ return [{ ...state, open: false, highlighted: null }, []];
47
+ case 'highlight':
48
+ if (msg.value !== null && state.disabledItems.includes(msg.value))
49
+ return [state, []];
50
+ return [{ ...state, highlighted: msg.value }, []];
51
+ case 'highlightNext':
52
+ return [
53
+ {
54
+ ...state,
55
+ highlighted: nextEnabled(state.items, state.disabledItems, state.highlighted, 1),
56
+ },
57
+ [],
58
+ ];
59
+ case 'highlightPrev':
60
+ return [
61
+ {
62
+ ...state,
63
+ highlighted: nextEnabled(state.items, state.disabledItems, state.highlighted, -1),
64
+ },
65
+ [],
66
+ ];
67
+ case 'selectHighlighted':
68
+ if (state.highlighted === null)
69
+ return [state, []];
70
+ return [{ ...state, open: false, highlighted: null }, []];
71
+ case 'select':
72
+ if (state.disabledItems.includes(msg.value))
73
+ return [state, []];
74
+ return [{ ...state, open: false, highlighted: null }, []];
75
+ case 'setItems': {
76
+ const disabled = msg.disabled ?? state.disabledItems;
77
+ return [{ ...state, items: msg.items, disabledItems: disabled }, []];
78
+ }
79
+ }
80
+ }
81
+ export function connect(get, send, opts) {
82
+ const contentId = `${opts.id}:content`;
83
+ const itemId = (v) => `${opts.id}:item:${v}`;
84
+ return {
85
+ trigger: {
86
+ 'data-scope': 'context-menu',
87
+ 'data-part': 'trigger',
88
+ onContextMenu: (e) => {
89
+ e.preventDefault();
90
+ send({ type: 'openAt', x: e.clientX, y: e.clientY });
91
+ },
92
+ },
93
+ positioner: {
94
+ 'data-scope': 'context-menu',
95
+ 'data-part': 'positioner',
96
+ style: (s) => {
97
+ const st = get(s);
98
+ return `position:fixed;top:${st.y}px;left:${st.x}px;`;
99
+ },
100
+ },
101
+ content: {
102
+ role: 'menu',
103
+ id: contentId,
104
+ tabIndex: -1,
105
+ 'data-state': (s) => (get(s).open ? 'open' : 'closed'),
106
+ 'data-scope': 'context-menu',
107
+ 'data-part': 'content',
108
+ onKeyDown: (e) => {
109
+ switch (e.key) {
110
+ case 'ArrowDown':
111
+ e.preventDefault();
112
+ send({ type: 'highlightNext' });
113
+ return;
114
+ case 'ArrowUp':
115
+ e.preventDefault();
116
+ send({ type: 'highlightPrev' });
117
+ return;
118
+ case 'Enter':
119
+ case ' ':
120
+ e.preventDefault();
121
+ send({ type: 'selectHighlighted' });
122
+ return;
123
+ case 'Escape':
124
+ e.preventDefault();
125
+ send({ type: 'close' });
126
+ return;
127
+ }
128
+ },
129
+ },
130
+ item: (value) => ({
131
+ item: {
132
+ role: 'menuitem',
133
+ id: itemId(value),
134
+ 'aria-disabled': (s) => (get(s).disabledItems.includes(value) ? 'true' : undefined),
135
+ 'data-state': (s) => (get(s).highlighted === value ? 'highlighted' : undefined),
136
+ 'data-disabled': (s) => (get(s).disabledItems.includes(value) ? '' : undefined),
137
+ 'data-scope': 'context-menu',
138
+ 'data-part': 'item',
139
+ 'data-value': value,
140
+ tabIndex: -1,
141
+ onClick: () => {
142
+ send({ type: 'select', value });
143
+ opts.onSelect?.(value);
144
+ },
145
+ onPointerMove: () => send({ type: 'highlight', value }),
146
+ },
147
+ }),
148
+ };
149
+ }
150
+ export function overlay(opts) {
151
+ const target = opts.target ?? 'body';
152
+ const parts = opts.parts;
153
+ const contentId = parts.content.id;
154
+ return show({
155
+ when: (s) => opts.get(s).open,
156
+ render: () => portal({
157
+ target,
158
+ render: () => {
159
+ onMount(() => {
160
+ const contentEl = document.getElementById(contentId);
161
+ if (!contentEl)
162
+ return;
163
+ contentEl.focus({ preventScroll: true });
164
+ const cleanup = pushDismissable({
165
+ element: contentEl,
166
+ onDismiss: () => opts.send({ type: 'close' }),
167
+ });
168
+ return cleanup;
169
+ });
170
+ return [div(parts.positioner, opts.content())];
171
+ },
172
+ }),
173
+ enter: opts.transition?.enter,
174
+ leave: opts.transition?.leave,
175
+ });
176
+ }
177
+ export const contextMenu = { init, update, connect, overlay };