@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,294 @@
1
+ function pad(n) {
2
+ return n.toString().padStart(2, '0');
3
+ }
4
+ function parseIso(iso) {
5
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
6
+ if (!match)
7
+ return null;
8
+ return { y: Number(match[1]), m: Number(match[2]), d: Number(match[3]) };
9
+ }
10
+ function toIso(y, m, d) {
11
+ return `${y}-${pad(m)}-${pad(d)}`;
12
+ }
13
+ function daysInMonth(year, month) {
14
+ return new Date(year, month, 0).getDate();
15
+ }
16
+ function todayIso() {
17
+ const now = new Date();
18
+ return toIso(now.getFullYear(), now.getMonth() + 1, now.getDate());
19
+ }
20
+ function addDays(iso, days) {
21
+ const p = parseIso(iso);
22
+ if (!p)
23
+ return iso;
24
+ const d = new Date(p.y, p.m - 1, p.d);
25
+ d.setDate(d.getDate() + days);
26
+ return toIso(d.getFullYear(), d.getMonth() + 1, d.getDate());
27
+ }
28
+ function isInRange(iso, min, max) {
29
+ if (min && iso < min)
30
+ return false;
31
+ if (max && iso > max)
32
+ return false;
33
+ return true;
34
+ }
35
+ export function init(opts = {}) {
36
+ const today = todayIso();
37
+ const parsed = opts.value ? parseIso(opts.value) : null;
38
+ const visibleMonth = opts.visibleMonth ?? parsed?.m ?? new Date().getMonth() + 1;
39
+ const visibleYear = opts.visibleYear ?? parsed?.y ?? new Date().getFullYear();
40
+ return {
41
+ value: opts.value ?? null,
42
+ visibleMonth,
43
+ visibleYear,
44
+ focused: opts.value ?? today,
45
+ min: opts.min ?? null,
46
+ max: opts.max ?? null,
47
+ weekStartsOn: opts.weekStartsOn ?? 0,
48
+ disabled: opts.disabled ?? false,
49
+ };
50
+ }
51
+ function normalizeMonth(year, month) {
52
+ let y = year;
53
+ let m = month;
54
+ while (m > 12) {
55
+ m -= 12;
56
+ y += 1;
57
+ }
58
+ while (m < 1) {
59
+ m += 12;
60
+ y -= 1;
61
+ }
62
+ return { year: y, month: m };
63
+ }
64
+ function syncVisibleMonth(state, date) {
65
+ const p = parseIso(date);
66
+ if (!p)
67
+ return state;
68
+ if (p.y === state.visibleYear && p.m === state.visibleMonth)
69
+ return state;
70
+ return { ...state, visibleYear: p.y, visibleMonth: p.m };
71
+ }
72
+ export function update(state, msg) {
73
+ if (state.disabled)
74
+ return [state, []];
75
+ switch (msg.type) {
76
+ case 'setValue':
77
+ return [{ ...state, value: msg.value, focused: msg.value ?? state.focused }, []];
78
+ case 'setFocused':
79
+ return [syncVisibleMonth({ ...state, focused: msg.date }, msg.date), []];
80
+ case 'prevMonth': {
81
+ const n = normalizeMonth(state.visibleYear, state.visibleMonth - 1);
82
+ return [{ ...state, visibleYear: n.year, visibleMonth: n.month }, []];
83
+ }
84
+ case 'nextMonth': {
85
+ const n = normalizeMonth(state.visibleYear, state.visibleMonth + 1);
86
+ return [{ ...state, visibleYear: n.year, visibleMonth: n.month }, []];
87
+ }
88
+ case 'prevYear':
89
+ return [{ ...state, visibleYear: state.visibleYear - 1 }, []];
90
+ case 'nextYear':
91
+ return [{ ...state, visibleYear: state.visibleYear + 1 }, []];
92
+ case 'selectFocused':
93
+ if (!isInRange(state.focused, state.min, state.max))
94
+ return [state, []];
95
+ return [{ ...state, value: state.focused }, []];
96
+ case 'moveFocus': {
97
+ const next = addDays(state.focused, msg.days);
98
+ return [syncVisibleMonth({ ...state, focused: next }, next), []];
99
+ }
100
+ case 'focusStartOfWeek': {
101
+ const p = parseIso(state.focused);
102
+ if (!p)
103
+ return [state, []];
104
+ const d = new Date(p.y, p.m - 1, p.d);
105
+ const delta = (d.getDay() - state.weekStartsOn + 7) % 7;
106
+ return [update(state, { type: 'moveFocus', days: -delta })[0], []];
107
+ }
108
+ case 'focusEndOfWeek': {
109
+ const p = parseIso(state.focused);
110
+ if (!p)
111
+ return [state, []];
112
+ const d = new Date(p.y, p.m - 1, p.d);
113
+ const delta = 6 - ((d.getDay() - state.weekStartsOn + 7) % 7);
114
+ return [update(state, { type: 'moveFocus', days: delta })[0], []];
115
+ }
116
+ case 'focusToday': {
117
+ const today = todayIso();
118
+ return [syncVisibleMonth({ ...state, focused: today }, today), []];
119
+ }
120
+ case 'clear':
121
+ return [{ ...state, value: null }, []];
122
+ }
123
+ }
124
+ /**
125
+ * Compute the grid of days visible in the current month view. Always returns
126
+ * full weeks: leading days from previous month and trailing from next month
127
+ * to fill the grid.
128
+ */
129
+ export function monthGrid(state) {
130
+ const y = state.visibleYear;
131
+ const m = state.visibleMonth;
132
+ const first = new Date(y, m - 1, 1);
133
+ const firstDay = first.getDay();
134
+ const leadDays = (firstDay - state.weekStartsOn + 7) % 7;
135
+ const totalDays = daysInMonth(y, m);
136
+ const today = todayIso();
137
+ const cells = [];
138
+ // Leading: previous month's trailing days
139
+ for (let i = leadDays; i > 0; i--) {
140
+ const d = new Date(y, m - 1, 1 - i);
141
+ const iso = toIso(d.getFullYear(), d.getMonth() + 1, d.getDate());
142
+ cells.push({
143
+ iso,
144
+ day: d.getDate(),
145
+ inMonth: false,
146
+ isToday: iso === today,
147
+ isSelected: iso === state.value,
148
+ isFocused: iso === state.focused,
149
+ isDisabled: !isInRange(iso, state.min, state.max),
150
+ });
151
+ }
152
+ // Current month
153
+ for (let d = 1; d <= totalDays; d++) {
154
+ const iso = toIso(y, m, d);
155
+ cells.push({
156
+ iso,
157
+ day: d,
158
+ inMonth: true,
159
+ isToday: iso === today,
160
+ isSelected: iso === state.value,
161
+ isFocused: iso === state.focused,
162
+ isDisabled: !isInRange(iso, state.min, state.max),
163
+ });
164
+ }
165
+ // Trailing: next month's leading days to fill to multiple of 7
166
+ const remaining = (7 - (cells.length % 7)) % 7;
167
+ for (let d = 1; d <= remaining; d++) {
168
+ const next = new Date(y, m, d);
169
+ const iso = toIso(next.getFullYear(), next.getMonth() + 1, next.getDate());
170
+ cells.push({
171
+ iso,
172
+ day: d,
173
+ inMonth: false,
174
+ isToday: iso === today,
175
+ isSelected: iso === state.value,
176
+ isFocused: iso === state.focused,
177
+ isDisabled: !isInRange(iso, state.min, state.max),
178
+ });
179
+ }
180
+ return cells;
181
+ }
182
+ const MONTH_NAMES = [
183
+ 'January',
184
+ 'February',
185
+ 'March',
186
+ 'April',
187
+ 'May',
188
+ 'June',
189
+ 'July',
190
+ 'August',
191
+ 'September',
192
+ 'October',
193
+ 'November',
194
+ 'December',
195
+ ];
196
+ export function connect(get, send, opts = {}) {
197
+ const prevLabel = opts.prevLabel ?? 'Previous month';
198
+ const nextLabel = opts.nextLabel ?? 'Next month';
199
+ const gridLabel = opts.gridLabel ?? ((y, m) => `${MONTH_NAMES[m - 1]} ${y}`);
200
+ return {
201
+ root: {
202
+ 'data-scope': 'date-picker',
203
+ 'data-part': 'root',
204
+ 'data-disabled': (s) => (get(s).disabled ? '' : undefined),
205
+ },
206
+ grid: {
207
+ role: 'grid',
208
+ 'aria-label': (s) => gridLabel(get(s).visibleYear, get(s).visibleMonth),
209
+ 'data-scope': 'date-picker',
210
+ 'data-part': 'grid',
211
+ },
212
+ prevMonthTrigger: {
213
+ type: 'button',
214
+ 'aria-label': prevLabel,
215
+ disabled: (s) => get(s).disabled,
216
+ 'data-scope': 'date-picker',
217
+ 'data-part': 'prev-month-trigger',
218
+ onClick: () => send({ type: 'prevMonth' }),
219
+ },
220
+ nextMonthTrigger: {
221
+ type: 'button',
222
+ 'aria-label': nextLabel,
223
+ disabled: (s) => get(s).disabled,
224
+ 'data-scope': 'date-picker',
225
+ 'data-part': 'next-month-trigger',
226
+ onClick: () => send({ type: 'nextMonth' }),
227
+ },
228
+ dayCell: (cell) => ({
229
+ cell: {
230
+ role: 'gridcell',
231
+ 'aria-selected': cell.isSelected,
232
+ 'aria-disabled': cell.isDisabled ? 'true' : undefined,
233
+ tabIndex: cell.isFocused ? 0 : -1,
234
+ 'data-scope': 'date-picker',
235
+ 'data-part': 'day-cell',
236
+ 'data-date': cell.iso,
237
+ 'data-in-month': cell.inMonth ? '' : undefined,
238
+ 'data-today': cell.isToday ? '' : undefined,
239
+ 'data-selected': cell.isSelected ? '' : undefined,
240
+ 'data-focused': cell.isFocused ? '' : undefined,
241
+ 'data-disabled': cell.isDisabled ? '' : undefined,
242
+ onClick: () => {
243
+ if (cell.isDisabled)
244
+ return;
245
+ send({ type: 'setFocused', date: cell.iso });
246
+ send({ type: 'selectFocused' });
247
+ },
248
+ onFocus: () => send({ type: 'setFocused', date: cell.iso }),
249
+ onKeyDown: (e) => {
250
+ switch (e.key) {
251
+ case 'ArrowLeft':
252
+ e.preventDefault();
253
+ send({ type: 'moveFocus', days: -1 });
254
+ return;
255
+ case 'ArrowRight':
256
+ e.preventDefault();
257
+ send({ type: 'moveFocus', days: 1 });
258
+ return;
259
+ case 'ArrowUp':
260
+ e.preventDefault();
261
+ send({ type: 'moveFocus', days: -7 });
262
+ return;
263
+ case 'ArrowDown':
264
+ e.preventDefault();
265
+ send({ type: 'moveFocus', days: 7 });
266
+ return;
267
+ case 'PageUp':
268
+ e.preventDefault();
269
+ send({ type: 'prevMonth' });
270
+ return;
271
+ case 'PageDown':
272
+ e.preventDefault();
273
+ send({ type: 'nextMonth' });
274
+ return;
275
+ case 'Home':
276
+ e.preventDefault();
277
+ send({ type: 'focusStartOfWeek' });
278
+ return;
279
+ case 'End':
280
+ e.preventDefault();
281
+ send({ type: 'focusEndOfWeek' });
282
+ return;
283
+ case 'Enter':
284
+ case ' ':
285
+ e.preventDefault();
286
+ send({ type: 'selectFocused' });
287
+ return;
288
+ }
289
+ },
290
+ },
291
+ }),
292
+ };
293
+ }
294
+ export const datePicker = { init, update, connect, monthGrid };
@@ -0,0 +1,152 @@
1
+ import type { Send, TransitionOptions } from '@llui/dom';
2
+ /**
3
+ * Dialog — modal / non-modal overlay. Ties together focus-trap, dismissable,
4
+ * body scroll lock, sibling aria-hidden, and portal-to-body rendering into
5
+ * a single view helper.
6
+ *
7
+ * Two layers:
8
+ * - **state machine** (`init`, `update`, `connect`) — pure, minimal.
9
+ * - **`overlay()` view helper** — opens the dialog's DOM tree inside a
10
+ * body portal, wires up all accessibility utilities on mount, tears
11
+ * them down on close, restores focus to the trigger.
12
+ *
13
+ * ```ts
14
+ * const parts = dialog.connect<State>(s => s.confirm, sendDialog, { id: 'confirm' })
15
+ *
16
+ * view: (send) => [
17
+ * button({ ...parts.trigger, class: 'btn' }, [text('Delete')]),
18
+ * ...dialog.overlay({
19
+ * get: s => s.confirm,
20
+ * send: sendDialog,
21
+ * parts,
22
+ * content: () => [
23
+ * div({ ...parts.content, class: 'dialog' }, [
24
+ * h2({ ...parts.title }, [text('Are you sure?')]),
25
+ * button({ ...parts.closeTrigger, class: 'btn' }, [text('Cancel')]),
26
+ * ]),
27
+ * ],
28
+ * transition: fade({ duration: 150 }),
29
+ * }),
30
+ * ]
31
+ * ```
32
+ */
33
+ export interface DialogState {
34
+ open: boolean;
35
+ }
36
+ export type DialogMsg = {
37
+ type: 'open';
38
+ } | {
39
+ type: 'close';
40
+ } | {
41
+ type: 'toggle';
42
+ } | {
43
+ type: 'setOpen';
44
+ open: boolean;
45
+ };
46
+ export interface DialogInit {
47
+ open?: boolean;
48
+ }
49
+ export declare function init(opts?: DialogInit): DialogState;
50
+ export declare function update(state: DialogState, msg: DialogMsg): [DialogState, never[]];
51
+ export interface DialogParts<S> {
52
+ trigger: {
53
+ type: 'button';
54
+ 'aria-haspopup': 'dialog';
55
+ 'aria-expanded': (s: S) => boolean;
56
+ 'aria-controls': string;
57
+ id: string;
58
+ 'data-state': (s: S) => 'open' | 'closed';
59
+ 'data-scope': 'dialog';
60
+ 'data-part': 'trigger';
61
+ onClick: (e: MouseEvent) => void;
62
+ };
63
+ backdrop: {
64
+ 'data-state': (s: S) => 'open' | 'closed';
65
+ 'data-scope': 'dialog';
66
+ 'data-part': 'backdrop';
67
+ 'aria-hidden': 'true';
68
+ };
69
+ positioner: {
70
+ 'data-scope': 'dialog';
71
+ 'data-part': 'positioner';
72
+ };
73
+ content: {
74
+ role: 'dialog' | 'alertdialog';
75
+ id: string;
76
+ 'aria-modal': 'true' | undefined;
77
+ 'aria-labelledby': string;
78
+ 'aria-describedby': string;
79
+ tabIndex: -1;
80
+ 'data-state': (s: S) => 'open' | 'closed';
81
+ 'data-scope': 'dialog';
82
+ 'data-part': 'content';
83
+ };
84
+ title: {
85
+ id: string;
86
+ 'data-scope': 'dialog';
87
+ 'data-part': 'title';
88
+ };
89
+ description: {
90
+ id: string;
91
+ 'data-scope': 'dialog';
92
+ 'data-part': 'description';
93
+ };
94
+ closeTrigger: {
95
+ type: 'button';
96
+ 'aria-label': string;
97
+ 'data-scope': 'dialog';
98
+ 'data-part': 'close-trigger';
99
+ onClick: (e: MouseEvent) => void;
100
+ };
101
+ }
102
+ export interface ConnectOptions {
103
+ /** Unique id per dialog instance (used for ARIA wiring). */
104
+ id: string;
105
+ /** ARIA role (default: 'dialog'). Use 'alertdialog' for destructive confirmations. */
106
+ role?: 'dialog' | 'alertdialog';
107
+ /** Modal dialogs trap focus and lock scroll (default: true). */
108
+ modal?: boolean;
109
+ /** Accessible label for the close button (default: 'Close'). */
110
+ closeLabel?: string;
111
+ }
112
+ export declare function connect<S>(get: (s: S) => DialogState, send: Send<DialogMsg>, opts: ConnectOptions): DialogParts<S>;
113
+ export interface OverlayOptions<S> {
114
+ /** State accessor. */
115
+ get: (s: S) => DialogState;
116
+ /** Send dispatcher for dialog messages. */
117
+ send: Send<DialogMsg>;
118
+ /** Parts from `connect()` — used to locate the content element by id. */
119
+ parts: DialogParts<S>;
120
+ /** Content rendering. */
121
+ content: () => Node[];
122
+ /** Optional transition to apply on open/close (from `@llui/transitions`). */
123
+ transition?: TransitionOptions;
124
+ /** Close on Escape key (default: true). */
125
+ closeOnEscape?: boolean;
126
+ /** Close on click outside content (default: true). */
127
+ closeOnOutsideClick?: boolean;
128
+ /** Trap focus inside the dialog while open (default: true for modal). */
129
+ trapFocus?: boolean;
130
+ /** Lock body scroll while open (default: true for modal). */
131
+ lockScroll?: boolean;
132
+ /** Apply aria-hidden to sibling trees (default: true for modal). */
133
+ hideSiblings?: boolean;
134
+ /** Target element / selector for the portal (default: 'body'). */
135
+ target?: string | HTMLElement;
136
+ /** Element to focus initially (default: first focusable inside content). */
137
+ initialFocus?: Element | (() => Element | null);
138
+ /** Restore focus on close (default: true). */
139
+ restoreFocus?: boolean;
140
+ }
141
+ /**
142
+ * Build the dialog's DOM tree and wire up all accessibility utilities.
143
+ * Returns a `show()` structural block that tracks `get(state).open`.
144
+ */
145
+ export declare function overlay<S>(opts: OverlayOptions<S>): Node[];
146
+ export declare const dialog: {
147
+ init: typeof init;
148
+ update: typeof update;
149
+ connect: typeof connect;
150
+ overlay: typeof overlay;
151
+ };
152
+ //# sourceMappingURL=dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dialog.d.ts","sourceRoot":"","sources":["../../src/components/dialog.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,140 @@
1
+ import { show, portal, onMount, div } from '@llui/dom';
2
+ import { pushFocusTrap } from '../utils/focus-trap';
3
+ import { pushDismissable } from '../utils/dismissable';
4
+ import { setAriaHiddenOutside } from '../utils/aria-hidden';
5
+ import { lockBodyScroll } from '../utils/remove-scroll';
6
+ export function init(opts = {}) {
7
+ return { open: opts.open ?? false };
8
+ }
9
+ export function update(state, msg) {
10
+ switch (msg.type) {
11
+ case 'open':
12
+ return [{ ...state, open: true }, []];
13
+ case 'close':
14
+ return [{ ...state, open: false }, []];
15
+ case 'toggle':
16
+ return [{ ...state, open: !state.open }, []];
17
+ case 'setOpen':
18
+ return [{ ...state, open: msg.open }, []];
19
+ }
20
+ }
21
+ export function connect(get, send, opts) {
22
+ const base = opts.id;
23
+ const contentId = `${base}:content`;
24
+ const titleId = `${base}:title`;
25
+ const descId = `${base}:description`;
26
+ const triggerId = `${base}:trigger`;
27
+ const role = opts.role ?? 'dialog';
28
+ const modal = opts.modal !== false;
29
+ const closeLabel = opts.closeLabel ?? 'Close';
30
+ return {
31
+ trigger: {
32
+ type: 'button',
33
+ 'aria-haspopup': 'dialog',
34
+ 'aria-expanded': (s) => get(s).open,
35
+ 'aria-controls': contentId,
36
+ id: triggerId,
37
+ 'data-state': (s) => (get(s).open ? 'open' : 'closed'),
38
+ 'data-scope': 'dialog',
39
+ 'data-part': 'trigger',
40
+ onClick: () => send({ type: 'open' }),
41
+ },
42
+ backdrop: {
43
+ 'data-state': (s) => (get(s).open ? 'open' : 'closed'),
44
+ 'data-scope': 'dialog',
45
+ 'data-part': 'backdrop',
46
+ 'aria-hidden': 'true',
47
+ },
48
+ positioner: {
49
+ 'data-scope': 'dialog',
50
+ 'data-part': 'positioner',
51
+ },
52
+ content: {
53
+ role,
54
+ id: contentId,
55
+ 'aria-modal': modal ? 'true' : undefined,
56
+ 'aria-labelledby': titleId,
57
+ 'aria-describedby': descId,
58
+ tabIndex: -1,
59
+ 'data-state': (s) => (get(s).open ? 'open' : 'closed'),
60
+ 'data-scope': 'dialog',
61
+ 'data-part': 'content',
62
+ },
63
+ title: {
64
+ id: titleId,
65
+ 'data-scope': 'dialog',
66
+ 'data-part': 'title',
67
+ },
68
+ description: {
69
+ id: descId,
70
+ 'data-scope': 'dialog',
71
+ 'data-part': 'description',
72
+ },
73
+ closeTrigger: {
74
+ type: 'button',
75
+ 'aria-label': closeLabel,
76
+ 'data-scope': 'dialog',
77
+ 'data-part': 'close-trigger',
78
+ onClick: () => send({ type: 'close' }),
79
+ },
80
+ };
81
+ }
82
+ /**
83
+ * Build the dialog's DOM tree and wire up all accessibility utilities.
84
+ * Returns a `show()` structural block that tracks `get(state).open`.
85
+ */
86
+ export function overlay(opts) {
87
+ const target = opts.target ?? 'body';
88
+ const closeOnEscape = opts.closeOnEscape !== false;
89
+ const closeOnOutsideClick = opts.closeOnOutsideClick !== false;
90
+ const trapFocus = opts.trapFocus !== false;
91
+ const lockScroll = opts.lockScroll !== false;
92
+ const hideSiblings = opts.hideSiblings !== false;
93
+ const restoreFocus = opts.restoreFocus !== false;
94
+ const parts = opts.parts;
95
+ const contentId = parts.content.id;
96
+ const triggerId = parts.trigger.id;
97
+ return show({
98
+ when: (s) => opts.get(s).open,
99
+ render: () => portal({
100
+ target,
101
+ render: () => {
102
+ onMount(() => {
103
+ const contentEl = document.getElementById(contentId);
104
+ if (!contentEl)
105
+ return;
106
+ const triggerEl = document.getElementById(triggerId);
107
+ const cleanups = [];
108
+ if (lockScroll)
109
+ cleanups.push(lockBodyScroll());
110
+ if (hideSiblings)
111
+ cleanups.push(setAriaHiddenOutside(contentEl));
112
+ if (trapFocus) {
113
+ cleanups.push(pushFocusTrap({
114
+ container: contentEl,
115
+ initialFocus: opts.initialFocus,
116
+ restoreFocus,
117
+ }));
118
+ }
119
+ if (closeOnEscape || closeOnOutsideClick) {
120
+ cleanups.push(pushDismissable({
121
+ element: contentEl,
122
+ ignore: () => (triggerEl ? [triggerEl] : []),
123
+ disableEscape: !closeOnEscape,
124
+ disableOutside: !closeOnOutsideClick,
125
+ onDismiss: () => opts.send({ type: 'close' }),
126
+ }));
127
+ }
128
+ return () => {
129
+ for (let i = cleanups.length - 1; i >= 0; i--)
130
+ cleanups[i]();
131
+ };
132
+ });
133
+ return [div(parts.positioner, opts.content())];
134
+ },
135
+ }),
136
+ enter: opts.transition?.enter,
137
+ leave: opts.transition?.leave,
138
+ });
139
+ }
140
+ export const dialog = { init, update, connect, overlay };