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

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 +302 -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 +72 -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 +120 -0
  102. package/dist/lib/settingsClient.d.ts.map +1 -0
  103. package/dist/lib/settingsClient.js +140 -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 +465 -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 +140 -0
  156. package/src/lib/menuPlacement.ts +31 -0
  157. package/src/lib/selectMenus.ts +303 -0
  158. package/src/lib/settingsClient.ts +377 -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,23 @@
1
+ /**
2
+ * Where a popup (tooltip bubble, dropdown list) should be portalled.
3
+ *
4
+ * Theme tokens are scoped: HadokuThemeRoot puts `data-theme` on the app
5
+ * container, and a nested `[data-theme]` scope (a theme preview, an embedded
6
+ * board) can sit inside that. A popup must render inside the SAME scope as its
7
+ * anchor, or it paints in the wrong theme — portalled to <body>, it loses the
8
+ * container's tokens entirely.
9
+ *
10
+ * Rendering it in place keeps the theme but breaks on any ancestor with a
11
+ * `transform`, which becomes the containing block for `position: fixed` and
12
+ * drags the popup off its anchor. The nearest `[data-theme]` ancestor is the
13
+ * best of both: the right tokens, and above every transform between the
14
+ * anchor and the theme root. When the only match is <html>, <body> takes it —
15
+ * :root tokens reach body, and a portal into <html> itself would sit outside
16
+ * the document's body.
17
+ */
18
+ export function themeScopeOf(anchor: Element | null): HTMLElement | null {
19
+ if (!anchor) return null
20
+ const scope = anchor.closest<HTMLElement>('[data-theme]')
21
+ if (!scope || scope === document.documentElement) return document.body
22
+ return scope
23
+ }
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Card tooltips for every `title=` on a page, with no change to the markup.
3
+ *
4
+ * import { installTitleTooltips } from '@wolffm/task-ui-components/title-tooltips'
5
+ * import '@wolffm/task-ui-components/tooltip.css'
6
+ * installTitleTooltips()
7
+ *
8
+ * hadoku_site calls this once from its shell, which covers every
9
+ * micro-frontend at once; an app does not call it itself. Framework-free and
10
+ * React-free on purpose: the shell has no React, and this must not pull a
11
+ * second copy into the page.
12
+ *
13
+ * One delegated listener set on the document. On hover (mouse only — touch
14
+ * never showed a native title either) or keyboard focus, the nearest element
15
+ * carrying `title` lends it to the bubble: the attribute moves to
16
+ * `data-hdk-title` for as long as the bubble is up, which is what stops the
17
+ * browser drawing its own on top, and goes back the moment it closes. So
18
+ * outside a hover the DOM is exactly what the app rendered, and code or a
19
+ * framework that reads or re-sets `title` sees its own value. A title the
20
+ * app changes mid-hover (a MutationObserver watches the anchor) re-stashes and
21
+ * updates the open bubble.
22
+ *
23
+ * Same contract as <Tooltip>: hover or focus shows it, Escape dismisses it
24
+ * until the pointer leaves, the pointer can move onto the bubble, a press
25
+ * closes it, and the anchor gets `aria-describedby` while it is up (skipped
26
+ * when the title only repeats the aria-label). The bubble is portalled into
27
+ * the anchor's theme scope, so a nested `[data-theme]` gets its own colours.
28
+ *
29
+ * Opt out per subtree with `data-native-title`.
30
+ */
31
+
32
+ import { themeScopeOf } from './themeScope'
33
+ import { placeTooltip, type TooltipPlacement } from './tooltipPlacement'
34
+
35
+ export interface TitleTooltipOptions {
36
+ /** Hover delay before showing, ms. Focus shows immediately. */
37
+ delay?: number
38
+ placement?: TooltipPlacement
39
+ }
40
+
41
+ const HIDE_GRACE_MS = 120
42
+ const STASH = 'data-hdk-title'
43
+ const BUBBLE_ID = 'hdk-title-tooltip'
44
+
45
+ let uninstallCurrent: (() => void) | null = null
46
+
47
+ export function installTitleTooltips(options: TitleTooltipOptions = {}): () => void {
48
+ if (uninstallCurrent) return uninstallCurrent
49
+ const delay = options.delay ?? 300
50
+ const placement = options.placement ?? 'top'
51
+
52
+ let anchor: Element | null = null
53
+ let bubble: HTMLDivElement | null = null
54
+ let timer: ReturnType<typeof setTimeout> | undefined
55
+ // Escape closes the bubble until the pointer leaves that anchor (WCAG 1.4.13).
56
+ let dismissed: Element | null = null
57
+
58
+ const observer = new MutationObserver(() => {
59
+ if (!anchor || !anchor.hasAttribute('title')) return
60
+ stash(anchor)
61
+ if (bubble) {
62
+ bubble.textContent = anchor.getAttribute(STASH)
63
+ place()
64
+ }
65
+ })
66
+
67
+ function stash(el: Element) {
68
+ el.setAttribute(STASH, el.getAttribute('title') ?? '')
69
+ el.removeAttribute('title')
70
+ }
71
+
72
+ function restore(el: Element) {
73
+ const text = el.getAttribute(STASH)
74
+ if (text === null) return
75
+ el.removeAttribute(STASH)
76
+ if (!el.hasAttribute('title')) el.setAttribute('title', text)
77
+ }
78
+
79
+ function titledFrom(target: EventTarget | null): Element | null {
80
+ if (!(target instanceof Element)) return null
81
+ // The stash too: while the bubble is up the anchor carries data-hdk-title,
82
+ // not title, and a pointerover on its own icon must still find IT rather
83
+ // than a titled ancestor.
84
+ const el = target.closest(`[title], [${STASH}]`)
85
+ if (!el || el.tagName === 'IFRAME' || el.closest('[data-native-title]')) return null
86
+ return (el.getAttribute('title') ?? el.getAttribute(STASH) ?? '').trim() ? el : null
87
+ }
88
+
89
+ function place() {
90
+ if (!anchor || !bubble) return
91
+ if (!anchor.isConnected) {
92
+ hide(0)
93
+ return
94
+ }
95
+ const p = placeTooltip(
96
+ placement,
97
+ anchor.getBoundingClientRect(),
98
+ bubble.offsetWidth,
99
+ bubble.offsetHeight
100
+ )
101
+ bubble.style.left = `${p.left}px`
102
+ bubble.style.top = `${p.top}px`
103
+ bubble.style.setProperty('--hdk-tooltip-arrow', `${p.arrow}px`)
104
+ bubble.className = `hdk-tooltip hdk-tooltip--card hdk-tooltip--plain hdk-tooltip--${p.side} hdk-tooltip--visible`
105
+ }
106
+
107
+ /** Take the title off `el` now (before the browser's own ~1s delay runs
108
+ * out) and open the bubble after `after` ms. */
109
+ function arm(el: Element, after: number) {
110
+ clearTimeout(timer)
111
+ if (el !== anchor) {
112
+ release()
113
+ anchor = el
114
+ stash(el)
115
+ observer.observe(el, { attributes: true, attributeFilter: ['title'] })
116
+ }
117
+ if (dismissed === el) return
118
+ timer = setTimeout(open, after)
119
+ }
120
+
121
+ function open() {
122
+ if (!anchor) return
123
+ const scope = themeScopeOf(anchor)
124
+ if (!scope) return
125
+ if (!bubble) {
126
+ bubble = document.createElement('div')
127
+ bubble.id = BUBBLE_ID
128
+ bubble.setAttribute('role', 'tooltip')
129
+ bubble.addEventListener('pointerenter', () => clearTimeout(timer))
130
+ bubble.addEventListener('pointerleave', () => hide(HIDE_GRACE_MS))
131
+ }
132
+ const text = anchor.getAttribute(STASH) ?? ''
133
+ bubble.textContent = text
134
+ // Parked invisibly at the origin so it can be measured, then placed.
135
+ bubble.className = `hdk-tooltip hdk-tooltip--card hdk-tooltip--plain hdk-tooltip--${placement}`
136
+ bubble.style.left = '0px'
137
+ bubble.style.top = '0px'
138
+ if (bubble.parentNode !== scope) scope.appendChild(bubble)
139
+ if (text !== anchor.getAttribute('aria-label')) {
140
+ const ids = (anchor.getAttribute('aria-describedby') ?? '').split(/\s+/).filter(Boolean)
141
+ if (!ids.includes(BUBBLE_ID))
142
+ anchor.setAttribute('aria-describedby', [...ids, BUBBLE_ID].join(' '))
143
+ }
144
+ place()
145
+ window.addEventListener('scroll', place, true)
146
+ window.addEventListener('resize', place)
147
+ }
148
+
149
+ /** Close the bubble and hand the anchor its title back. */
150
+ function release() {
151
+ window.removeEventListener('scroll', place, true)
152
+ window.removeEventListener('resize', place)
153
+ bubble?.remove()
154
+ if (!anchor) return
155
+ observer.disconnect()
156
+ const ids = (anchor.getAttribute('aria-describedby') ?? '')
157
+ .split(/\s+/)
158
+ .filter(id => id && id !== BUBBLE_ID)
159
+ if (ids.length) anchor.setAttribute('aria-describedby', ids.join(' '))
160
+ else anchor.removeAttribute('aria-describedby')
161
+ restore(anchor)
162
+ anchor = null
163
+ }
164
+
165
+ function hide(after: number) {
166
+ clearTimeout(timer)
167
+ if (after === 0) release()
168
+ else timer = setTimeout(release, after)
169
+ }
170
+
171
+ const onPointerOver = (e: PointerEvent) => {
172
+ if (e.pointerType !== 'mouse') return
173
+ if (bubble && bubble.contains(e.target as Node)) return
174
+ // Back over the anchor of a bubble still up: just cancel the pending hide.
175
+ // Over it with no bubble (inside the hide grace, or after a dismissal
176
+ // that has since cleared), arm it again — otherwise it stays silent.
177
+ const el = titledFrom(e.target)
178
+ if (el && el === anchor && bubble?.isConnected) clearTimeout(timer)
179
+ else if (el) arm(el, delay)
180
+ }
181
+
182
+ const onPointerOut = (e: PointerEvent) => {
183
+ if (e.pointerType !== 'mouse') return
184
+ const to = e.relatedTarget as Node | null
185
+ // Checked before `anchor`: a dismissal has already released the anchor.
186
+ if (dismissed && !(to && dismissed.contains(to))) dismissed = null
187
+ if (!anchor || (to && (anchor.contains(to) || bubble?.contains(to)))) return
188
+ hide(HIDE_GRACE_MS)
189
+ }
190
+
191
+ // A press closes it, as it does a native title: the click usually opens
192
+ // something the bubble would sit on top of.
193
+ const onPointerDown = () => {
194
+ if (anchor) dismissed = anchor
195
+ hide(0)
196
+ }
197
+
198
+ const onFocusIn = (e: FocusEvent) => {
199
+ const target = e.target
200
+ if (!(target instanceof Element) || !target.matches(':focus-visible')) return
201
+ const el = titledFrom(target)
202
+ if (el) {
203
+ dismissed = null
204
+ arm(el, 0)
205
+ }
206
+ }
207
+
208
+ const onFocusOut = (e: FocusEvent) => {
209
+ if (anchor && anchor.contains(e.target as Node)) hide(0)
210
+ }
211
+
212
+ const onKeyDown = (e: KeyboardEvent) => {
213
+ if (e.key !== 'Escape' || !anchor || !bubble?.isConnected) return
214
+ dismissed = anchor
215
+ hide(0)
216
+ }
217
+
218
+ document.addEventListener('pointerover', onPointerOver)
219
+ document.addEventListener('pointerout', onPointerOut)
220
+ document.addEventListener('pointerdown', onPointerDown, true)
221
+ document.addEventListener('focusin', onFocusIn)
222
+ document.addEventListener('focusout', onFocusOut)
223
+ document.addEventListener('keydown', onKeyDown)
224
+
225
+ uninstallCurrent = () => {
226
+ hide(0)
227
+ document.removeEventListener('pointerover', onPointerOver)
228
+ document.removeEventListener('pointerout', onPointerOut)
229
+ document.removeEventListener('pointerdown', onPointerDown, true)
230
+ document.removeEventListener('focusin', onFocusIn)
231
+ document.removeEventListener('focusout', onFocusOut)
232
+ document.removeEventListener('keydown', onKeyDown)
233
+ uninstallCurrent = null
234
+ }
235
+ return uninstallCurrent
236
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Where a tooltip bubble goes, shared by <Tooltip> and installTitleTooltips so
3
+ * the React and framework-free bubbles sit, flip and clamp identically.
4
+ */
5
+
6
+ export type TooltipPlacement = 'top' | 'bottom' | 'left' | 'right'
7
+
8
+ export interface Placed {
9
+ side: TooltipPlacement
10
+ left: number
11
+ top: number
12
+ /** Arrow offset along the bubble edge, so it still points at the anchor after clamping. */
13
+ arrow: number
14
+ }
15
+
16
+ const GAP = 8
17
+ const EDGE = 8
18
+
19
+ const OPPOSITE: Record<TooltipPlacement, TooltipPlacement> = {
20
+ top: 'bottom',
21
+ bottom: 'top',
22
+ left: 'right',
23
+ right: 'left'
24
+ }
25
+
26
+ function fits(side: TooltipPlacement, a: DOMRect, w: number, h: number) {
27
+ if (side === 'top') return a.top - GAP - h >= EDGE
28
+ if (side === 'bottom') return a.bottom + GAP + h <= window.innerHeight - EDGE
29
+ if (side === 'left') return a.left - GAP - w >= EDGE
30
+ return a.right + GAP + w <= window.innerWidth - EDGE
31
+ }
32
+
33
+ /** Onto the device-pixel grid. A bubble at a fractional offset draws its 1px
34
+ * border antialiased across two rows of the screen — a blurred edge. */
35
+ function snap(v: number) {
36
+ const dpr = window.devicePixelRatio || 1
37
+ return Math.round(v * dpr) / dpr
38
+ }
39
+
40
+ export function placeTooltip(
41
+ preferred: TooltipPlacement,
42
+ a: DOMRect,
43
+ w: number,
44
+ h: number
45
+ ): Placed {
46
+ const side =
47
+ fits(preferred, a, w, h) || !fits(OPPOSITE[preferred], a, w, h)
48
+ ? preferred
49
+ : OPPOSITE[preferred]
50
+ const clamp = (v: number, max: number) => Math.min(Math.max(v, EDGE), max - EDGE)
51
+ if (side === 'top' || side === 'bottom') {
52
+ const left = snap(clamp(a.left + a.width / 2 - w / 2, window.innerWidth - w))
53
+ const top = snap(side === 'top' ? a.top - GAP - h : a.bottom + GAP)
54
+ return { side, left, top, arrow: a.left + a.width / 2 - left }
55
+ }
56
+ const top = snap(clamp(a.top + a.height / 2 - h / 2, window.innerHeight - h))
57
+ const left = snap(side === 'left' ? a.left - GAP - w : a.right + GAP)
58
+ return { side, left, top, arrow: a.top + a.height / 2 - top }
59
+ }
package/src/modal.css ADDED
@@ -0,0 +1,129 @@
1
+ /* Modal styles for @wolffm/task-ui-components.
2
+ *
3
+ * The component shipped without these, which is why it had one adopter: every
4
+ * other app hand-rolled a dialog rather than reproduce the stylesheet. Import
5
+ * alongside the other component CSS:
6
+ * import '@wolffm/task-ui-components/modal.css'
7
+ *
8
+ * Tokens only, so it themes with everything else across all 18 themes.
9
+ */
10
+
11
+ .modal-overlay {
12
+ position: fixed;
13
+ inset: 0;
14
+ background-color: var(--color-bg-overlay);
15
+ display: flex;
16
+ align-items: flex-start;
17
+ justify-content: center;
18
+ padding: 8vh 16px;
19
+ z-index: 50;
20
+ }
21
+
22
+ .modal-card {
23
+ background-color: var(--color-bg-card);
24
+ color: var(--color-text);
25
+ border: 1px solid var(--color-border);
26
+ border-radius: var(--hdk-radius-lg);
27
+ padding: 20px;
28
+ width: min(440px, 92vw);
29
+ max-height: 84vh;
30
+ overflow: auto;
31
+ box-shadow: var(--hdk-shadow-lg);
32
+ }
33
+
34
+ /* The card takes focus on open so Escape works before anything inside is
35
+ * focused; it must not draw a focus ring for that. Controls keep theirs. */
36
+ .modal-card:focus {
37
+ outline: none;
38
+ }
39
+
40
+ /* A dialog that needs room — a review step, a side-by-side. */
41
+ .modal-card--wide {
42
+ width: min(680px, 94vw);
43
+ }
44
+
45
+ .modal-head {
46
+ display: flex;
47
+ align-items: center;
48
+ gap: 10px;
49
+ margin: 0 0 14px;
50
+ }
51
+
52
+ .modal-card h3 {
53
+ margin: 0;
54
+ font-size: var(--hdk-text-lg);
55
+ font-weight: 600;
56
+ }
57
+
58
+ .modal-card label {
59
+ display: block;
60
+ color: var(--color-text-secondary);
61
+ font-size: var(--hdk-text-sm);
62
+ margin: 12px 0 5px;
63
+ }
64
+
65
+ .modal-input {
66
+ width: 100%;
67
+ font: inherit;
68
+ font-size: var(--hdk-text-sm);
69
+ color: var(--color-text);
70
+ background-color: var(--color-bg-alt);
71
+ border: 1px solid var(--color-border);
72
+ border-radius: var(--hdk-radius);
73
+ padding: 9px 11px;
74
+ }
75
+
76
+ .modal-input:focus {
77
+ outline: none;
78
+ border-color: var(--color-primary);
79
+ }
80
+
81
+ .modal-actions {
82
+ display: flex;
83
+ justify-content: flex-end;
84
+ gap: var(--hdk-space-sm);
85
+ margin-top: 18px;
86
+ }
87
+
88
+ .modal-button {
89
+ font: inherit;
90
+ font-size: var(--hdk-text-sm);
91
+ padding: 8px 14px;
92
+ border-radius: var(--hdk-radius);
93
+ border: 1px solid var(--color-border);
94
+ background: none;
95
+ color: var(--color-text);
96
+ cursor: pointer;
97
+ }
98
+
99
+ .modal-button:hover:not(:disabled) {
100
+ border-color: var(--color-primary);
101
+ }
102
+
103
+ .modal-button:disabled {
104
+ opacity: 0.5;
105
+ cursor: not-allowed;
106
+ }
107
+
108
+ .modal-button--primary {
109
+ background-color: var(--color-primary);
110
+ border-color: var(--color-primary);
111
+ color: var(--color-on-primary);
112
+ }
113
+
114
+ /* The destructive button carries its accent on the border, not the label.
115
+ * `--color-danger` as bare text on the card misses AA in 11 of 18 themes —
116
+ * the same unvalidated accent-over-surface pair that check-usage now gates. */
117
+ .modal-button--danger {
118
+ border-color: var(--color-danger);
119
+ color: var(--color-text);
120
+ }
121
+
122
+ /* Filled on hover, so the label takes `on-danger` — NOT `on-primary`, which is
123
+ * derived from primary's luminance and misses AA on a danger fill in 9 of 18
124
+ * themes. `on-danger` over `danger` is the pair check-contrast guarantees. */
125
+ .modal-button--danger:hover:not(:disabled) {
126
+ background-color: var(--color-danger);
127
+ border-color: var(--color-danger);
128
+ color: var(--color-on-danger);
129
+ }
@@ -0,0 +1,131 @@
1
+ /* NumberInput styles for @wolffm/task-ui-components.
2
+ *
3
+ * import '@wolffm/task-ui-components/number-input.css'
4
+ *
5
+ * One bordered box holding the field and a stacked up/down stepper, so the
6
+ * focus ring and the border belong to the whole control, as on <Select>.
7
+ */
8
+
9
+ .hdk-number {
10
+ display: inline-flex;
11
+ align-items: stretch;
12
+ min-width: 0;
13
+ vertical-align: middle;
14
+ color: var(--color-text);
15
+ background-color: var(--color-bg-card);
16
+ border: 1px solid var(--color-border);
17
+ border-radius: var(--hdk-radius);
18
+ overflow: hidden;
19
+ transition:
20
+ border-color var(--transition-fast),
21
+ box-shadow var(--transition-fast);
22
+ }
23
+
24
+ .hdk-number:hover:not(.hdk-number--disabled) {
25
+ border-color: var(--color-text-muted);
26
+ }
27
+
28
+ .hdk-number:focus-within {
29
+ border-color: var(--color-primary);
30
+ box-shadow: var(--hdk-shadow-focus-sm);
31
+ }
32
+
33
+ .hdk-number--disabled {
34
+ opacity: 0.5;
35
+ }
36
+
37
+ .hdk-number__input {
38
+ flex: 1;
39
+ width: 100%;
40
+ min-width: 0;
41
+ font: inherit;
42
+ color: inherit;
43
+ background: transparent;
44
+ border: 0;
45
+ appearance: textfield;
46
+ }
47
+
48
+ .hdk-number__input:focus {
49
+ outline: none;
50
+ }
51
+
52
+ .hdk-number__input::-webkit-inner-spin-button,
53
+ .hdk-number__input::-webkit-outer-spin-button {
54
+ appearance: none;
55
+ margin: 0;
56
+ }
57
+
58
+ .hdk-number__steps {
59
+ display: flex;
60
+ flex-direction: column;
61
+ flex-shrink: 0;
62
+ border-left: 1px solid var(--color-border);
63
+ }
64
+
65
+ .hdk-number__step {
66
+ display: flex;
67
+ flex: 1;
68
+ align-items: center;
69
+ justify-content: center;
70
+ padding: 0;
71
+ color: var(--color-text-secondary);
72
+ background: transparent;
73
+ border: 0;
74
+ cursor: pointer;
75
+ transition:
76
+ background-color var(--transition-fast),
77
+ color var(--transition-fast);
78
+ }
79
+
80
+ .hdk-number__step + .hdk-number__step {
81
+ border-top: 1px solid var(--color-border);
82
+ }
83
+
84
+ .hdk-number__step:hover:not(:disabled) {
85
+ color: var(--color-on-primary-bg);
86
+ background-color: var(--color-primary-bg);
87
+ }
88
+
89
+ .hdk-number__step:disabled {
90
+ opacity: 0.4;
91
+ cursor: not-allowed;
92
+ }
93
+
94
+ .hdk-number--sm {
95
+ font-size: var(--hdk-text-xs);
96
+ min-height: 28px;
97
+ }
98
+
99
+ .hdk-number--sm .hdk-number__input {
100
+ padding: 4px 8px;
101
+ }
102
+
103
+ .hdk-number--sm .hdk-number__step {
104
+ width: 18px;
105
+ }
106
+
107
+ .hdk-number--md {
108
+ font-size: var(--hdk-text-sm);
109
+ min-height: 34px;
110
+ }
111
+
112
+ .hdk-number--md .hdk-number__input {
113
+ padding: 7px 10px;
114
+ }
115
+
116
+ .hdk-number--md .hdk-number__step {
117
+ width: 22px;
118
+ }
119
+
120
+ .hdk-number--lg {
121
+ font-size: var(--hdk-text-base);
122
+ min-height: 42px;
123
+ }
124
+
125
+ .hdk-number--lg .hdk-number__input {
126
+ padding: 10px 12px;
127
+ }
128
+
129
+ .hdk-number--lg .hdk-number__step {
130
+ width: 26px;
131
+ }