@hadoku/task-ui-components 0.0.0-stage → 4.20.4

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 (169) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +917 -2
  3. package/dist/app-header.css +288 -0
  4. package/dist/bento.css +143 -0
  5. package/dist/button.css +227 -0
  6. package/dist/components/AppHeader.d.ts +61 -0
  7. package/dist/components/AppHeader.d.ts.map +1 -0
  8. package/dist/components/AppHeader.js +23 -0
  9. package/dist/components/AppHeader.js.map +1 -0
  10. package/dist/components/BentoCard.d.ts +23 -0
  11. package/dist/components/BentoCard.d.ts.map +1 -0
  12. package/dist/components/BentoCard.js +49 -0
  13. package/dist/components/BentoCard.js.map +1 -0
  14. package/dist/components/BentoGrid.d.ts +24 -0
  15. package/dist/components/BentoGrid.d.ts.map +1 -0
  16. package/dist/components/BentoGrid.js +22 -0
  17. package/dist/components/BentoGrid.js.map +1 -0
  18. package/dist/components/Button.d.ts +28 -0
  19. package/dist/components/Button.d.ts.map +1 -0
  20. package/dist/components/Button.js +51 -0
  21. package/dist/components/Button.js.map +1 -0
  22. package/dist/components/ConnectedSettings.d.ts +53 -0
  23. package/dist/components/ConnectedSettings.d.ts.map +1 -0
  24. package/dist/components/ConnectedSettings.js +368 -0
  25. package/dist/components/ConnectedSettings.js.map +1 -0
  26. package/dist/components/ConnectedThemePicker.d.ts +52 -0
  27. package/dist/components/ConnectedThemePicker.d.ts.map +1 -0
  28. package/dist/components/ConnectedThemePicker.js +32 -0
  29. package/dist/components/ConnectedThemePicker.js.map +1 -0
  30. package/dist/components/ContextMenu.d.ts +22 -0
  31. package/dist/components/ContextMenu.d.ts.map +1 -0
  32. package/dist/components/ContextMenu.js +23 -0
  33. package/dist/components/ContextMenu.js.map +1 -0
  34. package/dist/components/HadokuThemePicker.d.ts +27 -0
  35. package/dist/components/HadokuThemePicker.d.ts.map +1 -0
  36. package/dist/components/HadokuThemePicker.js +19 -0
  37. package/dist/components/HadokuThemePicker.js.map +1 -0
  38. package/dist/components/LoadingSkeleton.d.ts +14 -0
  39. package/dist/components/LoadingSkeleton.d.ts.map +1 -0
  40. package/dist/components/LoadingSkeleton.js +62 -0
  41. package/dist/components/LoadingSkeleton.js.map +1 -0
  42. package/dist/components/Modal.d.ts +39 -0
  43. package/dist/components/Modal.d.ts.map +1 -0
  44. package/dist/components/Modal.js +106 -0
  45. package/dist/components/Modal.js.map +1 -0
  46. package/dist/components/NumberInput.d.ts +26 -0
  47. package/dist/components/NumberInput.d.ts.map +1 -0
  48. package/dist/components/NumberInput.js +131 -0
  49. package/dist/components/NumberInput.js.map +1 -0
  50. package/dist/components/Select.d.ts +52 -0
  51. package/dist/components/Select.d.ts.map +1 -0
  52. package/dist/components/Select.js +281 -0
  53. package/dist/components/Select.js.map +1 -0
  54. package/dist/components/SettingsModal.d.ts +69 -0
  55. package/dist/components/SettingsModal.d.ts.map +1 -0
  56. package/dist/components/SettingsModal.js +141 -0
  57. package/dist/components/SettingsModal.js.map +1 -0
  58. package/dist/components/ThemeIcons.d.ts +26 -0
  59. package/dist/components/ThemeIcons.d.ts.map +1 -0
  60. package/dist/components/ThemeIcons.js +74 -0
  61. package/dist/components/ThemeIcons.js.map +1 -0
  62. package/dist/components/ThemePicker.d.ts +28 -0
  63. package/dist/components/ThemePicker.d.ts.map +1 -0
  64. package/dist/components/ThemePicker.js +101 -0
  65. package/dist/components/ThemePicker.js.map +1 -0
  66. package/dist/components/Toast.d.ts +13 -0
  67. package/dist/components/Toast.d.ts.map +1 -0
  68. package/dist/components/Toast.js +70 -0
  69. package/dist/components/Toast.js.map +1 -0
  70. package/dist/components/Toaster.d.ts +19 -0
  71. package/dist/components/Toaster.d.ts.map +1 -0
  72. package/dist/components/Toaster.js +19 -0
  73. package/dist/components/Toaster.js.map +1 -0
  74. package/dist/components/Tooltip.d.ts +49 -0
  75. package/dist/components/Tooltip.d.ts.map +1 -0
  76. package/dist/components/Tooltip.js +143 -0
  77. package/dist/components/Tooltip.js.map +1 -0
  78. package/dist/context-menu.css +11 -0
  79. package/dist/controls.css +137 -0
  80. package/dist/hooks/useReducedMotion.d.ts +7 -0
  81. package/dist/hooks/useReducedMotion.d.ts.map +1 -0
  82. package/dist/hooks/useReducedMotion.js +25 -0
  83. package/dist/hooks/useReducedMotion.js.map +1 -0
  84. package/dist/hooks/useToast.d.ts +18 -0
  85. package/dist/hooks/useToast.d.ts.map +1 -0
  86. package/dist/hooks/useToast.js +28 -0
  87. package/dist/hooks/useToast.js.map +1 -0
  88. package/dist/icon-btn.css +64 -0
  89. package/dist/index.d.ts +49 -0
  90. package/dist/index.d.ts.map +1 -0
  91. package/dist/index.js +75 -0
  92. package/dist/index.js.map +1 -0
  93. package/dist/lib/menuPlacement.d.ts +16 -0
  94. package/dist/lib/menuPlacement.d.ts.map +1 -0
  95. package/dist/lib/menuPlacement.js +19 -0
  96. package/dist/lib/menuPlacement.js.map +1 -0
  97. package/dist/lib/selectMenus.d.ts +30 -0
  98. package/dist/lib/selectMenus.d.ts.map +1 -0
  99. package/dist/lib/selectMenus.js +236 -0
  100. package/dist/lib/selectMenus.js.map +1 -0
  101. package/dist/lib/settingsClient.d.ts +144 -0
  102. package/dist/lib/settingsClient.d.ts.map +1 -0
  103. package/dist/lib/settingsClient.js +157 -0
  104. package/dist/lib/settingsClient.js.map +1 -0
  105. package/dist/lib/themeScope.d.ts +19 -0
  106. package/dist/lib/themeScope.d.ts.map +1 -0
  107. package/dist/lib/themeScope.js +10 -0
  108. package/dist/lib/themeScope.js.map +1 -0
  109. package/dist/lib/titleTooltips.d.ts +38 -0
  110. package/dist/lib/titleTooltips.d.ts.map +1 -0
  111. package/dist/lib/titleTooltips.js +166 -0
  112. package/dist/lib/titleTooltips.js.map +1 -0
  113. package/dist/lib/tooltipPlacement.d.ts +14 -0
  114. package/dist/lib/tooltipPlacement.d.ts.map +1 -0
  115. package/dist/lib/tooltipPlacement.js +34 -0
  116. package/dist/lib/tooltipPlacement.js.map +1 -0
  117. package/dist/modal.css +129 -0
  118. package/dist/number-input.css +131 -0
  119. package/dist/scrollbar.css +165 -0
  120. package/dist/select.css +251 -0
  121. package/dist/styles.css +1866 -0
  122. package/dist/task-ui-components.css +1866 -0
  123. package/dist/theme-picker.css +219 -0
  124. package/dist/toaster.css +159 -0
  125. package/dist/tooltip.css +103 -0
  126. package/dist/types/index.d.ts +37 -0
  127. package/dist/types/index.d.ts.map +1 -0
  128. package/package.json +60 -4
  129. package/src/app-header.css +288 -0
  130. package/src/bento.css +143 -0
  131. package/src/button.css +227 -0
  132. package/src/components/AppHeader.tsx +87 -0
  133. package/src/components/BentoCard.tsx +72 -0
  134. package/src/components/BentoGrid.tsx +48 -0
  135. package/src/components/Button.tsx +81 -0
  136. package/src/components/ConnectedSettings.tsx +545 -0
  137. package/src/components/ConnectedThemePicker.tsx +79 -0
  138. package/src/components/ContextMenu.tsx +48 -0
  139. package/src/components/HadokuThemePicker.tsx +39 -0
  140. package/src/components/LoadingSkeleton.tsx +84 -0
  141. package/src/components/Modal.tsx +171 -0
  142. package/src/components/NumberInput.tsx +161 -0
  143. package/src/components/Select.tsx +359 -0
  144. package/src/components/SettingsModal.tsx +235 -0
  145. package/src/components/ThemeIcons.tsx +127 -0
  146. package/src/components/ThemePicker.tsx +149 -0
  147. package/src/components/Toast.tsx +92 -0
  148. package/src/components/Toaster.tsx +43 -0
  149. package/src/components/Tooltip.tsx +212 -0
  150. package/src/context-menu.css +11 -0
  151. package/src/controls.css +137 -0
  152. package/src/hooks/useReducedMotion.ts +36 -0
  153. package/src/hooks/useToast.ts +57 -0
  154. package/src/icon-btn.css +64 -0
  155. package/src/index.ts +153 -0
  156. package/src/lib/menuPlacement.ts +31 -0
  157. package/src/lib/selectMenus.ts +303 -0
  158. package/src/lib/settingsClient.ts +425 -0
  159. package/src/lib/themeScope.ts +23 -0
  160. package/src/lib/titleTooltips.ts +236 -0
  161. package/src/lib/tooltipPlacement.ts +59 -0
  162. package/src/modal.css +129 -0
  163. package/src/number-input.css +131 -0
  164. package/src/scrollbar.css +165 -0
  165. package/src/select.css +251 -0
  166. package/src/theme-picker.css +219 -0
  167. package/src/toaster.css +159 -0
  168. package/src/tooltip.css +103 -0
  169. package/src/types/index.ts +43 -0
@@ -0,0 +1,303 @@
1
+ /**
2
+ * The themed list for every native <select> on a page, with no change to the
3
+ * markup.
4
+ *
5
+ * import { installSelectMenus } from '@wolffm/task-ui-components/select-menus'
6
+ * import '@wolffm/task-ui-components/select.css'
7
+ * installSelectMenus()
8
+ *
9
+ * hadoku_site calls this once from its shell, which covers every
10
+ * micro-frontend; an app does not call it itself. Framework-free and
11
+ * React-free, like installTitleTooltips, for the same reason.
12
+ *
13
+ * A native select's closed box takes CSS, but its open list is drawn by the
14
+ * OS and takes almost none. So a mouse press or an opening key on the select
15
+ * is intercepted (preventDefault on mousedown is what stops the OS list) and
16
+ * the same list <Select> renders opens instead, built from the select's own
17
+ * <option>s and <optgroup>s at that moment.
18
+ *
19
+ * The <select> stays the source of truth. Choosing sets its selectedIndex and
20
+ * fires `input` and `change` exactly as the OS list would, so every framework
21
+ * (React's onChange included) and every plain listener sees an ordinary
22
+ * change, and a form still submits the select's value.
23
+ *
24
+ * Left native on purpose:
25
+ * - touch: a phone's own picker is the better control there
26
+ * - `multiple` or `size > 1`, which render as an inline list, not a popup
27
+ * - anything under `data-native-select`
28
+ */
29
+
30
+ import { themeScopeOf } from './themeScope'
31
+ import { placeMenu } from './menuPlacement'
32
+
33
+ const TYPEAHEAD_RESET_MS = 600
34
+ const LIST_ID = 'hdk-select-menu'
35
+ // `check` from the @wolffm/themes registry (Lucide circle-check), inlined so
36
+ // this entry does not pull the whole registry into the shell.
37
+ const CHECK_SVG =
38
+ '<svg class="hdk-icon hdk-select__check" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10" /><path d="m9 12 2 2 4-4" /></svg>'
39
+
40
+ const OPEN_KEYS = new Set(['ArrowDown', 'ArrowUp', ' ', 'F4'])
41
+
42
+ let uninstallCurrent: (() => void) | null = null
43
+
44
+ function eligible(el: Element | null): el is HTMLSelectElement {
45
+ return (
46
+ el instanceof HTMLSelectElement &&
47
+ !el.multiple &&
48
+ el.size <= 1 &&
49
+ !el.disabled &&
50
+ !el.closest('[data-native-select]')
51
+ )
52
+ }
53
+
54
+ export function installSelectMenus(): () => void {
55
+ if (uninstallCurrent) return uninstallCurrent
56
+
57
+ let lastPointer = 'mouse'
58
+ let select: HTMLSelectElement | null = null
59
+ let list: HTMLUListElement | null = null
60
+ let options: HTMLOptionElement[] = []
61
+ let items: HTMLLIElement[] = []
62
+ let active = -1
63
+ const typeahead = { text: '', at: 0 }
64
+
65
+ function setActive(i: number) {
66
+ if (items[active]) items[active].classList.remove('hdk-select__option--active')
67
+ active = i
68
+ const item = items[i]
69
+ if (!item || !list) return
70
+ item.classList.add('hdk-select__option--active')
71
+ list.setAttribute('aria-activedescendant', item.id)
72
+ item.scrollIntoView({ block: 'nearest' })
73
+ }
74
+
75
+ function step(from: number, dir: 1 | -1) {
76
+ for (let i = 1; i <= options.length; i++) {
77
+ const next = (from + dir * i + options.length) % options.length
78
+ if (!isOff(next)) return next
79
+ }
80
+ return from
81
+ }
82
+
83
+ const edge = (dir: 1 | -1) => step(dir === 1 ? -1 : options.length, dir)
84
+
85
+ function matchTypeahead(key: string) {
86
+ const now = Date.now()
87
+ typeahead.text = now - typeahead.at > TYPEAHEAD_RESET_MS ? key : typeahead.text + key
88
+ typeahead.at = now
89
+ const needle = typeahead.text.toLowerCase()
90
+ const start = Math.max(active, 0)
91
+ for (let i = 0; i < options.length; i++) {
92
+ // A single repeated key cycles; a longer prefix stays on the current match.
93
+ const idx = (start + (typeahead.text.length === 1 ? i + 1 : i)) % options.length
94
+ const o = options[idx]
95
+ if (!isOff(idx) && labelOf(o).toLowerCase().startsWith(needle)) return idx
96
+ }
97
+ return -1
98
+ }
99
+
100
+ const labelOf = (o: HTMLOptionElement) => o.label || o.text
101
+ // A disabled <optgroup> disables its options too.
102
+ const isOff = (i: number) =>
103
+ options[i].disabled ||
104
+ (options[i].parentElement as HTMLOptGroupElement | null)?.disabled === true
105
+
106
+ function place() {
107
+ if (!select || !list) return
108
+ if (!select.isConnected) {
109
+ close(false)
110
+ return
111
+ }
112
+ const p = placeMenu(select.getBoundingClientRect(), list.scrollHeight)
113
+ list.style.left = `${p.left}px`
114
+ list.style.minWidth = `${p.width}px`
115
+ list.style.top = p.top === undefined ? '' : `${p.top}px`
116
+ list.style.bottom = p.bottom === undefined ? '' : `${p.bottom}px`
117
+ list.style.maxHeight = `${p.maxHeight}px`
118
+ list.style.visibility = ''
119
+ }
120
+
121
+ function open(el: HTMLSelectElement, start?: 'last') {
122
+ close(false)
123
+ const scope = themeScopeOf(el)
124
+ if (!scope) return
125
+ select = el
126
+ options = Array.from(el.options).filter(o => !o.hidden)
127
+ if (options.length === 0) return
128
+
129
+ list = document.createElement('ul')
130
+ list.id = LIST_ID
131
+ list.setAttribute('role', 'listbox')
132
+ list.tabIndex = -1
133
+ list.className = 'hdk-select__menu hdk-scroll hdk-scroll--thin'
134
+ const name = el.getAttribute('aria-label') ?? el.labels?.[0]?.textContent?.trim()
135
+ if (name) list.setAttribute('aria-label', name)
136
+ // The list is portalled, so it cannot inherit the select's type size.
137
+ const cs = getComputedStyle(el)
138
+ list.style.fontSize = cs.fontSize
139
+ list.style.fontFamily = cs.fontFamily
140
+ list.style.visibility = 'hidden'
141
+
142
+ let group: HTMLOptGroupElement | null = null
143
+ items = options.map((o, i) => {
144
+ const g = o.parentElement instanceof HTMLOptGroupElement ? o.parentElement : null
145
+ if (g && g !== group) {
146
+ const head = document.createElement('li')
147
+ head.setAttribute('role', 'presentation')
148
+ head.className = 'hdk-select__group'
149
+ head.textContent = g.label
150
+ list?.append(head)
151
+ }
152
+ group = g
153
+ const item = document.createElement('li')
154
+ item.id = `${LIST_ID}-${i}`
155
+ item.setAttribute('role', 'option')
156
+ const selected = o.index === el.selectedIndex
157
+ item.setAttribute('aria-selected', String(selected))
158
+ if (o.disabled || g?.disabled) item.setAttribute('aria-disabled', 'true')
159
+ item.className = `hdk-select__option${selected ? ' hdk-select__option--selected' : ''}`
160
+ const text = document.createElement('span')
161
+ text.className = 'hdk-select__option-label'
162
+ text.textContent = labelOf(o)
163
+ const wrap = document.createElement('span')
164
+ wrap.className = 'hdk-select__option-text'
165
+ wrap.append(text)
166
+ item.append(wrap)
167
+ if (selected) item.insertAdjacentHTML('beforeend', CHECK_SVG)
168
+ item.addEventListener('pointermove', () => {
169
+ if (!isOff(i) && i !== active) setActive(i)
170
+ })
171
+ item.addEventListener('click', () => choose(i))
172
+ list?.append(item)
173
+ return item
174
+ })
175
+ list.addEventListener('keydown', onListKeyDown)
176
+ scope.appendChild(list)
177
+ el.setAttribute('aria-expanded', 'true')
178
+ el.setAttribute('aria-controls', LIST_ID)
179
+
180
+ const current = options.findIndex(o => o.index === el.selectedIndex)
181
+ setActive(start === 'last' ? edge(-1) : current >= 0 && !isOff(current) ? current : edge(1))
182
+ place()
183
+ list.focus({ preventScroll: true })
184
+ window.addEventListener('scroll', onScroll, true)
185
+ window.addEventListener('resize', place)
186
+ }
187
+
188
+ function close(refocus: boolean) {
189
+ window.removeEventListener('scroll', onScroll, true)
190
+ window.removeEventListener('resize', place)
191
+ list?.remove()
192
+ list = null
193
+ if (!select) return
194
+ select.removeAttribute('aria-expanded')
195
+ select.removeAttribute('aria-controls')
196
+ if (refocus) select.focus({ preventScroll: true })
197
+ select = null
198
+ items = []
199
+ options = []
200
+ active = -1
201
+ }
202
+
203
+ function choose(i: number) {
204
+ const el = select
205
+ const o = options[i]
206
+ if (!el || !o || isOff(i)) return
207
+ const changed = o.index !== el.selectedIndex
208
+ close(true)
209
+ if (!changed) return
210
+ el.selectedIndex = o.index
211
+ el.dispatchEvent(new Event('input', { bubbles: true }))
212
+ el.dispatchEvent(new Event('change', { bubbles: true }))
213
+ }
214
+
215
+ // Scrolling the list itself must not re-place (or close) it.
216
+ const onScroll = (e: Event) => {
217
+ if (list && e.target instanceof Node && list.contains(e.target)) return
218
+ place()
219
+ }
220
+
221
+ function onListKeyDown(e: KeyboardEvent) {
222
+ switch (e.key) {
223
+ case 'ArrowDown':
224
+ e.preventDefault()
225
+ setActive(step(active, 1))
226
+ break
227
+ case 'ArrowUp':
228
+ e.preventDefault()
229
+ setActive(step(active, -1))
230
+ break
231
+ case 'Home':
232
+ case 'PageUp':
233
+ e.preventDefault()
234
+ setActive(edge(1))
235
+ break
236
+ case 'End':
237
+ case 'PageDown':
238
+ e.preventDefault()
239
+ setActive(edge(-1))
240
+ break
241
+ case 'Enter':
242
+ case ' ':
243
+ e.preventDefault()
244
+ choose(active)
245
+ break
246
+ case 'Escape':
247
+ // Stop here so an enclosing modal does not close along with the list.
248
+ e.preventDefault()
249
+ e.stopPropagation()
250
+ close(true)
251
+ break
252
+ case 'Tab':
253
+ close(true)
254
+ break
255
+ default:
256
+ if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
257
+ const idx = matchTypeahead(e.key)
258
+ if (idx >= 0) setActive(idx)
259
+ }
260
+ }
261
+ }
262
+
263
+ const onPointerDown = (e: PointerEvent) => {
264
+ lastPointer = e.pointerType
265
+ const target = e.target as Node
266
+ if (list && !list.contains(target) && target !== select) close(false)
267
+ }
268
+
269
+ const onMouseDown = (e: MouseEvent) => {
270
+ if (e.button !== 0 || lastPointer === 'touch') return
271
+ const el = (e.target as Element | null)?.closest?.('select') ?? null
272
+ if (!eligible(el)) return
273
+ // This is what keeps the OS list from opening.
274
+ e.preventDefault()
275
+ if (select === el) {
276
+ close(true)
277
+ return
278
+ }
279
+ el.focus({ preventScroll: true })
280
+ open(el)
281
+ }
282
+
283
+ const onKeyDown = (e: KeyboardEvent) => {
284
+ const el = e.target as Element | null
285
+ if (!eligible(el) || select === el) return
286
+ if (!OPEN_KEYS.has(e.key) || e.ctrlKey || e.metaKey) return
287
+ e.preventDefault()
288
+ open(el, e.key === 'ArrowUp' && !e.altKey ? 'last' : undefined)
289
+ }
290
+
291
+ document.addEventListener('pointerdown', onPointerDown, true)
292
+ document.addEventListener('mousedown', onMouseDown, true)
293
+ document.addEventListener('keydown', onKeyDown, true)
294
+
295
+ uninstallCurrent = () => {
296
+ close(false)
297
+ document.removeEventListener('pointerdown', onPointerDown, true)
298
+ document.removeEventListener('mousedown', onMouseDown, true)
299
+ document.removeEventListener('keydown', onKeyDown, true)
300
+ uninstallCurrent = null
301
+ }
302
+ return uninstallCurrent
303
+ }