@lovett/ui 0.0.10 → 0.0.11
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.
- package/dist/index.d.ts +666 -13
- package/dist/index.js +1835 -344
- package/dist/index.js.map +1 -1
- package/dist/styles.css +16 -2
- package/dist/theme-v2.css +228 -0
- package/dist/tokens.css +9 -0
- package/package.json +1 -1
- package/src/__tests__/anchor.test.tsx +422 -0
- package/src/__tests__/combobox.test.tsx +677 -0
- package/src/__tests__/dropdown-menu.test.tsx +418 -0
- package/src/__tests__/helpers/geometry.ts +58 -0
- package/src/__tests__/layer-stack.test.tsx +228 -0
- package/src/__tests__/popover.test.tsx +460 -0
- package/src/__tests__/select.test.tsx +543 -0
- package/src/__tests__/tooltip.test.tsx +355 -0
- package/src/calculator-shell-v2.tsx +19 -39
- package/src/combobox.tsx +796 -0
- package/src/dropdown-menu.tsx +142 -152
- package/src/index.ts +106 -0
- package/src/lib/anchor.ts +427 -0
- package/src/lib/focus.ts +32 -0
- package/src/lib/layer-stack.ts +188 -0
- package/src/lib/refs.ts +31 -0
- package/src/metric-card.tsx +57 -22
- package/src/modal.tsx +31 -48
- package/src/popover.tsx +407 -0
- package/src/select.tsx +646 -0
- package/src/stat-row.tsx +108 -70
- package/src/styles.css +16 -2
- package/src/tokens.css +9 -0
- package/src/tooltip.tsx +297 -0
package/src/dropdown-menu.tsx
CHANGED
|
@@ -8,45 +8,75 @@
|
|
|
8
8
|
* BUILD_FRESH per ADR-025 DEVIATION-3: source ships on
|
|
9
9
|
* `@base-ui/react/menu`, which is NOT in CLAUDE.md §1 allowed deps.
|
|
10
10
|
* The compound-component surface (Root / Trigger / Content / Item /
|
|
11
|
-
* CheckboxItem / Separator / Label / Group) carries over
|
|
12
|
-
* implementation is a lightweight React Context + Portal + outside-
|
|
13
|
-
* click + Escape handler.
|
|
11
|
+
* CheckboxItem / Separator / Label / Group) carries over.
|
|
14
12
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
13
|
+
* Rebuilt on the shared anchored-positioning kit per ADR-0030 Decision G
|
|
14
|
+
* (meta-ads-audit-dashboard task manager; the workspace app's 137 call
|
|
15
|
+
* sites are the other consumer). Same public API, same DOM roles and
|
|
16
|
+
* `data-slot`s; what changed underneath:
|
|
17
|
+
* • Positioning is `useAnchoredPosition` (lib/anchor.ts) — the menu now
|
|
18
|
+
* FLIPS to the other side when it cannot fit, instead of being clamped
|
|
19
|
+
* over its own trigger; `side` accepts `left` / `right` too.
|
|
20
|
+
* • Escape goes through the shared dismiss-layer stack (lib/layer-stack.ts)
|
|
21
|
+
* that Modal uses: a menu opened inside a Modal now closes on its own,
|
|
22
|
+
* leaving the Modal open. Before, both closed on one keypress.
|
|
23
|
+
* • Outside-click is `useOutsideClick`, layer-aware — a Select or Popover
|
|
24
|
+
* opened from a menu item is not "outside".
|
|
25
|
+
* • Trigger, Content, Item and CheckboxItem forward refs.
|
|
26
|
+
*
|
|
27
|
+
* Trigger styling per ADR-026 D4 (DQ-4 LIFT): the default trigger inherits
|
|
28
|
+
* the secondary Button visual (rounded-xl, `--bg-card` fill, 1px
|
|
29
|
+
* `--border`). Consumers can pass custom trigger content; the wrapper
|
|
30
|
+
* applies the button-style classes unless `unstyled` is set.
|
|
31
|
+
*
|
|
32
|
+
* Focus stays on the trigger when the menu opens (menus do not steal
|
|
33
|
+
* focus here; items are reached with Tab). Escape closes and refocuses the
|
|
34
|
+
* trigger. Roving arrow-key focus is a follow-up.
|
|
35
|
+
*
|
|
36
|
+
* Token discipline: surface chrome is `POPOVER_SURFACE_STYLE` (shared with
|
|
37
|
+
* Popover — internal `--popover` tokens are allowed inside packages/ui);
|
|
38
|
+
* item hover / focus use public `--surface-overlay`; motion is
|
|
39
|
+
* `.ds-enter-pop`, gated on `positioned` so it never plays from the parked
|
|
40
|
+
* off-screen frame.
|
|
20
41
|
*
|
|
21
42
|
* Workspace ADR: ADR-025 (port) + ADR-026 D4 (trigger spec)
|
|
22
|
-
* Ported 2026-05-18
|
|
43
|
+
* Ported 2026-05-18 · Rebuilt on lib/anchor 2026-09-05
|
|
23
44
|
*/
|
|
24
45
|
|
|
25
46
|
import {
|
|
26
47
|
createContext,
|
|
48
|
+
forwardRef,
|
|
27
49
|
useCallback,
|
|
28
50
|
useContext,
|
|
29
|
-
useEffect,
|
|
30
51
|
useId,
|
|
31
|
-
useLayoutEffect,
|
|
32
52
|
useMemo,
|
|
33
53
|
useRef,
|
|
34
54
|
useState,
|
|
35
55
|
type ButtonHTMLAttributes,
|
|
36
56
|
type HTMLAttributes,
|
|
37
57
|
type ReactNode,
|
|
58
|
+
type RefObject,
|
|
38
59
|
} from 'react'
|
|
39
60
|
import { createPortal } from 'react-dom'
|
|
40
61
|
import { Check } from 'lucide-react'
|
|
41
62
|
import { cn } from './lib/utils'
|
|
63
|
+
import { composeRefs } from './lib/refs'
|
|
64
|
+
import { useLayer } from './lib/layer-stack'
|
|
65
|
+
import {
|
|
66
|
+
useAnchoredPosition,
|
|
67
|
+
useOutsideClick,
|
|
68
|
+
type AnchorAlign,
|
|
69
|
+
type AnchorSide,
|
|
70
|
+
} from './lib/anchor'
|
|
71
|
+
import { POPOVER_SURFACE_STYLE } from './popover'
|
|
42
72
|
|
|
43
|
-
type Align =
|
|
44
|
-
type Side =
|
|
73
|
+
type Align = AnchorAlign
|
|
74
|
+
type Side = AnchorSide
|
|
45
75
|
|
|
46
76
|
interface DropdownMenuContextValue {
|
|
47
77
|
open: boolean
|
|
48
78
|
setOpen: (next: boolean) => void
|
|
49
|
-
triggerRef:
|
|
79
|
+
triggerRef: RefObject<HTMLButtonElement | null>
|
|
50
80
|
contentId: string
|
|
51
81
|
}
|
|
52
82
|
|
|
@@ -119,23 +149,28 @@ export interface DropdownMenuTriggerProps
|
|
|
119
149
|
* secondary Button visual (rounded-xl + `--bg-card` fill + 1px
|
|
120
150
|
* `--border`). Set `unstyled` to wrap your own button shape.
|
|
121
151
|
*/
|
|
122
|
-
export
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
children,
|
|
127
|
-
|
|
128
|
-
|
|
152
|
+
export const DropdownMenuTrigger = forwardRef<
|
|
153
|
+
HTMLButtonElement,
|
|
154
|
+
DropdownMenuTriggerProps
|
|
155
|
+
>(function DropdownMenuTrigger(
|
|
156
|
+
{ unstyled, className, onClick, children, ...rest },
|
|
157
|
+
forwardedRef,
|
|
158
|
+
) {
|
|
129
159
|
const { open, setOpen, triggerRef, contentId } =
|
|
130
160
|
useDropdownMenuCtx('DropdownMenuTrigger')
|
|
161
|
+
const ref = useMemo(
|
|
162
|
+
() => composeRefs<HTMLButtonElement>(forwardedRef, triggerRef),
|
|
163
|
+
[forwardedRef, triggerRef],
|
|
164
|
+
)
|
|
131
165
|
|
|
132
166
|
return (
|
|
133
167
|
<button
|
|
134
|
-
ref={
|
|
168
|
+
ref={ref}
|
|
135
169
|
type="button"
|
|
136
170
|
aria-haspopup="menu"
|
|
137
171
|
aria-expanded={open}
|
|
138
172
|
aria-controls={open ? contentId : undefined}
|
|
173
|
+
data-state={open ? 'open' : 'closed'}
|
|
139
174
|
className={cn(
|
|
140
175
|
!unstyled && 'btn btn-secondary',
|
|
141
176
|
className,
|
|
@@ -149,7 +184,7 @@ export function DropdownMenuTrigger({
|
|
|
149
184
|
{children}
|
|
150
185
|
</button>
|
|
151
186
|
)
|
|
152
|
-
}
|
|
187
|
+
})
|
|
153
188
|
|
|
154
189
|
export interface DropdownMenuContentProps
|
|
155
190
|
extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
@@ -158,142 +193,98 @@ export interface DropdownMenuContentProps
|
|
|
158
193
|
align?: Align
|
|
159
194
|
/** Pixel offset along the alignment axis. Default 0. */
|
|
160
195
|
alignOffset?: number
|
|
161
|
-
/**
|
|
196
|
+
/**
|
|
197
|
+
* Preferred side of the trigger. Default `bottom`. Flips to the opposite
|
|
198
|
+
* side when it cannot fit (before the 2026-09-05 rebuild it was clamped
|
|
199
|
+
* over its own trigger instead; only that no-room case differs).
|
|
200
|
+
*/
|
|
162
201
|
side?: Side
|
|
163
202
|
/** Pixel offset along the side axis. Default 6. */
|
|
164
203
|
sideOffset?: number
|
|
165
204
|
}
|
|
166
205
|
|
|
167
|
-
export
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
206
|
+
export const DropdownMenuContent = forwardRef<
|
|
207
|
+
HTMLDivElement,
|
|
208
|
+
DropdownMenuContentProps
|
|
209
|
+
>(function DropdownMenuContent(
|
|
210
|
+
{
|
|
211
|
+
children,
|
|
212
|
+
align = 'start',
|
|
213
|
+
alignOffset = 0,
|
|
214
|
+
side = 'bottom',
|
|
215
|
+
sideOffset = 6,
|
|
216
|
+
className,
|
|
217
|
+
style,
|
|
218
|
+
...rest
|
|
219
|
+
},
|
|
220
|
+
forwardedRef,
|
|
221
|
+
) {
|
|
177
222
|
const { open, setOpen, triggerRef, contentId } =
|
|
178
223
|
useDropdownMenuCtx('DropdownMenuContent')
|
|
179
224
|
const contentRef = useRef<HTMLDivElement | null>(null)
|
|
180
|
-
const [position, setPosition] = useState<{ left: number; top: number } | null>(
|
|
181
|
-
null,
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
// Position relative to the trigger viewport rect. We measure on
|
|
185
|
-
// open + on layout changes (scroll / resize) so the panel tracks.
|
|
186
|
-
useLayoutEffect(() => {
|
|
187
|
-
if (!open) {
|
|
188
|
-
setPosition(null)
|
|
189
|
-
return
|
|
190
|
-
}
|
|
191
|
-
const measure = () => {
|
|
192
|
-
const triggerEl = triggerRef.current
|
|
193
|
-
const contentEl = contentRef.current
|
|
194
|
-
if (!triggerEl || !contentEl) return
|
|
195
|
-
const triggerRect = triggerEl.getBoundingClientRect()
|
|
196
|
-
const contentRect = contentEl.getBoundingClientRect()
|
|
197
|
-
|
|
198
|
-
let left = triggerRect.left + alignOffset
|
|
199
|
-
if (align === 'end') left = triggerRect.right - contentRect.width - alignOffset
|
|
200
|
-
else if (align === 'center')
|
|
201
|
-
left =
|
|
202
|
-
triggerRect.left +
|
|
203
|
-
triggerRect.width / 2 -
|
|
204
|
-
contentRect.width / 2 +
|
|
205
|
-
alignOffset
|
|
206
|
-
|
|
207
|
-
let top =
|
|
208
|
-
side === 'bottom'
|
|
209
|
-
? triggerRect.bottom + sideOffset
|
|
210
|
-
: triggerRect.top - contentRect.height - sideOffset
|
|
211
225
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
226
|
+
const {
|
|
227
|
+
ref: positionRef,
|
|
228
|
+
style: positionStyle,
|
|
229
|
+
placement,
|
|
230
|
+
positioned,
|
|
231
|
+
} = useAnchoredPosition<HTMLDivElement>({
|
|
232
|
+
anchorRef: triggerRef,
|
|
233
|
+
side,
|
|
234
|
+
align,
|
|
235
|
+
offset: sideOffset,
|
|
236
|
+
alignOffset,
|
|
237
|
+
enabled: open,
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
// Topmost-wins Escape, shared with Modal and Popover.
|
|
241
|
+
const layer = useLayer({
|
|
242
|
+
enabled: open,
|
|
243
|
+
kind: 'popover',
|
|
244
|
+
elementRef: contentRef,
|
|
245
|
+
onEscape: () => {
|
|
246
|
+
setOpen(false)
|
|
247
|
+
triggerRef.current?.focus()
|
|
248
|
+
},
|
|
249
|
+
})
|
|
221
250
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
window.removeEventListener('resize', measure)
|
|
227
|
-
window.removeEventListener('scroll', measure, true)
|
|
228
|
-
}
|
|
229
|
-
}, [open, align, alignOffset, side, sideOffset, triggerRef])
|
|
251
|
+
useOutsideClick([triggerRef, contentRef], () => setOpen(false), {
|
|
252
|
+
enabled: open,
|
|
253
|
+
layer,
|
|
254
|
+
})
|
|
230
255
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const target = event.target as Node | null
|
|
236
|
-
if (!target) return
|
|
237
|
-
if (contentRef.current?.contains(target)) return
|
|
238
|
-
if (triggerRef.current?.contains(target)) return
|
|
239
|
-
setOpen(false)
|
|
240
|
-
}
|
|
241
|
-
const onKeyDown = (event: KeyboardEvent) => {
|
|
242
|
-
if (event.key === 'Escape') {
|
|
243
|
-
event.stopPropagation()
|
|
244
|
-
setOpen(false)
|
|
245
|
-
triggerRef.current?.focus()
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
document.addEventListener('pointerdown', onPointerDown)
|
|
249
|
-
document.addEventListener('keydown', onKeyDown)
|
|
250
|
-
return () => {
|
|
251
|
-
document.removeEventListener('pointerdown', onPointerDown)
|
|
252
|
-
document.removeEventListener('keydown', onKeyDown)
|
|
253
|
-
}
|
|
254
|
-
}, [open, setOpen, triggerRef])
|
|
256
|
+
const ref = useMemo(
|
|
257
|
+
() => composeRefs<HTMLDivElement>(forwardedRef, contentRef, positionRef),
|
|
258
|
+
[forwardedRef, positionRef],
|
|
259
|
+
)
|
|
255
260
|
|
|
256
261
|
if (!open || typeof document === 'undefined') return null
|
|
257
262
|
|
|
258
|
-
// Two-phase render:
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
// caused the "flash at top of viewport then jump under the trigger"
|
|
264
|
-
// glitch.
|
|
265
|
-
const isPositioned = position !== null
|
|
266
|
-
|
|
263
|
+
// Two-phase render (see lib/anchor.ts): the hook parks the first paint
|
|
264
|
+
// off-screen and hidden, measures, then applies real coordinates. The
|
|
265
|
+
// entrance animation is gated on `positioned` so it never fires from the
|
|
266
|
+
// parked location — that was the "flash at top of viewport then jump
|
|
267
|
+
// under the trigger" glitch.
|
|
267
268
|
return createPortal(
|
|
268
269
|
<div
|
|
269
|
-
ref={
|
|
270
|
+
ref={ref}
|
|
270
271
|
id={contentId}
|
|
271
272
|
role="menu"
|
|
272
273
|
data-slot="dropdown-menu-content"
|
|
273
274
|
data-state="open"
|
|
274
|
-
data-
|
|
275
|
+
data-side={placement?.side}
|
|
276
|
+
data-align={placement?.align}
|
|
277
|
+
data-positioned={positioned ? 'true' : 'false'}
|
|
275
278
|
className={cn(
|
|
276
279
|
'fixed z-[100] min-w-48 max-w-[min(28rem,calc(100vw-16px))]',
|
|
277
280
|
'p-1.5 overflow-y-auto',
|
|
278
281
|
'rounded-[var(--radius-lg)]',
|
|
279
|
-
|
|
282
|
+
positioned && 'ds-enter-pop',
|
|
280
283
|
className,
|
|
281
284
|
)}
|
|
282
285
|
style={{
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
// visibility:hidden during a same-frame double-render. If the
|
|
286
|
-
// first paint slips through, it's at (-99999, -99999), not at
|
|
287
|
-
// viewport (0, 0) where a left-edge flash would be visible.
|
|
288
|
-
left: position?.left ?? -99999,
|
|
289
|
-
top: position?.top ?? -99999,
|
|
290
|
-
opacity: isPositioned ? undefined : 0,
|
|
291
|
-
pointerEvents: isPositioned ? undefined : 'none',
|
|
292
|
-
visibility: isPositioned ? 'visible' : 'hidden',
|
|
293
|
-
background: 'rgb(var(--popover))',
|
|
294
|
-
color: 'rgb(var(--popover-foreground))',
|
|
295
|
-
border: '1px solid rgb(var(--border))',
|
|
296
|
-
boxShadow: 'var(--shadow-lg, 0 10px 32px rgba(0,0,0,0.18))',
|
|
286
|
+
...positionStyle,
|
|
287
|
+
...POPOVER_SURFACE_STYLE,
|
|
297
288
|
...style,
|
|
298
289
|
}}
|
|
299
290
|
{...rest}
|
|
@@ -302,7 +293,7 @@ export function DropdownMenuContent({
|
|
|
302
293
|
</div>,
|
|
303
294
|
document.body,
|
|
304
295
|
)
|
|
305
|
-
}
|
|
296
|
+
})
|
|
306
297
|
|
|
307
298
|
export interface DropdownMenuItemProps
|
|
308
299
|
extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
@@ -312,17 +303,17 @@ export interface DropdownMenuItemProps
|
|
|
312
303
|
inset?: boolean
|
|
313
304
|
}
|
|
314
305
|
|
|
315
|
-
export
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
onClick,
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}: DropdownMenuItemProps) {
|
|
306
|
+
export const DropdownMenuItem = forwardRef<
|
|
307
|
+
HTMLButtonElement,
|
|
308
|
+
DropdownMenuItemProps
|
|
309
|
+
>(function DropdownMenuItem(
|
|
310
|
+
{ variant = 'default', inset, className, onClick, children, ...rest },
|
|
311
|
+
forwardedRef,
|
|
312
|
+
) {
|
|
323
313
|
const { setOpen } = useDropdownMenuCtx('DropdownMenuItem')
|
|
324
314
|
return (
|
|
325
315
|
<button
|
|
316
|
+
ref={forwardedRef}
|
|
326
317
|
role="menuitem"
|
|
327
318
|
type="button"
|
|
328
319
|
data-slot="dropdown-menu-item"
|
|
@@ -348,7 +339,7 @@ export function DropdownMenuItem({
|
|
|
348
339
|
{children}
|
|
349
340
|
</button>
|
|
350
341
|
)
|
|
351
|
-
}
|
|
342
|
+
})
|
|
352
343
|
|
|
353
344
|
export interface DropdownMenuCheckboxItemProps
|
|
354
345
|
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'onChange'> {
|
|
@@ -356,17 +347,16 @@ export interface DropdownMenuCheckboxItemProps
|
|
|
356
347
|
onCheckedChange?: (checked: boolean) => void
|
|
357
348
|
}
|
|
358
349
|
|
|
359
|
-
export
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
children,
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
...rest
|
|
367
|
-
}: DropdownMenuCheckboxItemProps) {
|
|
350
|
+
export const DropdownMenuCheckboxItem = forwardRef<
|
|
351
|
+
HTMLButtonElement,
|
|
352
|
+
DropdownMenuCheckboxItemProps
|
|
353
|
+
>(function DropdownMenuCheckboxItem(
|
|
354
|
+
{ checked, onCheckedChange, className, children, onClick, disabled, ...rest },
|
|
355
|
+
forwardedRef,
|
|
356
|
+
) {
|
|
368
357
|
return (
|
|
369
358
|
<button
|
|
359
|
+
ref={forwardedRef}
|
|
370
360
|
role="menuitemcheckbox"
|
|
371
361
|
type="button"
|
|
372
362
|
data-slot="dropdown-menu-checkbox-item"
|
|
@@ -399,7 +389,7 @@ export function DropdownMenuCheckboxItem({
|
|
|
399
389
|
) : null}
|
|
400
390
|
</button>
|
|
401
391
|
)
|
|
402
|
-
}
|
|
392
|
+
})
|
|
403
393
|
|
|
404
394
|
export function DropdownMenuSeparator({
|
|
405
395
|
className,
|
package/src/index.ts
CHANGED
|
@@ -172,6 +172,112 @@ export {
|
|
|
172
172
|
type EditingCell,
|
|
173
173
|
} from './data-grid'
|
|
174
174
|
|
|
175
|
+
// Anchored-positioning kit + dismiss-layer stack — promoted per ADR-0030
|
|
176
|
+
// Decision G (meta-ads-audit-dashboard task manager: its pickers, sheets
|
|
177
|
+
// and palette need one positioning routine and one Escape order). The
|
|
178
|
+
// workspace app is the other consumer: DropdownMenu (137 call sites),
|
|
179
|
+
// Modal (51 files) and Popover are rebuilt on these in the same change.
|
|
180
|
+
// `useAnchoredPosition` measures / flips / clamps against an element ref
|
|
181
|
+
// OR a virtual rect (ContextMenu's right-click point); `useOutsideClick`
|
|
182
|
+
// is layer-aware; `useLayer` / `useEscapeKey` are the topmost-wins stack
|
|
183
|
+
// extracted from Modal's `openPanels`. Select / Combobox / Tooltip /
|
|
184
|
+
// ContextMenu build on them next; FilterDropdown / FolderTreePicker /
|
|
185
|
+
// TagChipInput still carry local copies of the positioning block
|
|
186
|
+
// (follow-up).
|
|
187
|
+
export {
|
|
188
|
+
useAnchoredPosition,
|
|
189
|
+
computeAnchoredPosition,
|
|
190
|
+
useOutsideClick,
|
|
191
|
+
type AnchorSide,
|
|
192
|
+
type AnchorAlign,
|
|
193
|
+
type AnchorRect,
|
|
194
|
+
type AnchorSize,
|
|
195
|
+
type AnchorPlacement,
|
|
196
|
+
type AnchoredPosition,
|
|
197
|
+
type AnchoredPositionOptions,
|
|
198
|
+
type UseAnchoredPositionOptions,
|
|
199
|
+
type UseAnchoredPositionResult,
|
|
200
|
+
type OutsideClickTarget,
|
|
201
|
+
type UseOutsideClickOptions,
|
|
202
|
+
} from './lib/anchor'
|
|
203
|
+
export {
|
|
204
|
+
useLayer,
|
|
205
|
+
useEscapeKey,
|
|
206
|
+
type LayerKind,
|
|
207
|
+
type LayerHandle,
|
|
208
|
+
type UseLayerOptions,
|
|
209
|
+
type UseEscapeKeyOptions,
|
|
210
|
+
} from './lib/layer-stack'
|
|
211
|
+
|
|
212
|
+
// Popover — anchored, non-modal surface: portal to body, `.ds-enter-pop`
|
|
213
|
+
// once positioned, focus into the content on open and back to the trigger
|
|
214
|
+
// on close, Escape closes the topmost layer only, outside-click closes.
|
|
215
|
+
// Compound: Popover + Popover.Trigger (`asChild` to wrap a <Button> or a
|
|
216
|
+
// cell) + Popover.Content (`role="dialog"` or `"none"`). Promoted per
|
|
217
|
+
// ADR-0030 Decision G; the workspace app's three hand-rolled popovers
|
|
218
|
+
// (sidebar user menu, group-settings colour / icon pickers, calculator
|
|
219
|
+
// AssumptionsPopover) are the migration targets. DropdownMenu shares its
|
|
220
|
+
// surface chrome (`POPOVER_SURFACE_STYLE`) and stack, but stays a sibling
|
|
221
|
+
// because a menu does not move focus into itself on open.
|
|
222
|
+
export {
|
|
223
|
+
Popover,
|
|
224
|
+
POPOVER_SURFACE_STYLE,
|
|
225
|
+
type PopoverProps,
|
|
226
|
+
type PopoverTriggerProps,
|
|
227
|
+
type PopoverContentProps,
|
|
228
|
+
} from './popover'
|
|
229
|
+
|
|
230
|
+
// Select — single-value picker: an `.input-shell` trigger (sm / md, sits
|
|
231
|
+
// flush beside <Input>) that opens a real listbox (APG select-only
|
|
232
|
+
// combobox: role=combobox trigger, aria-activedescendant, role=listbox /
|
|
233
|
+
// option, arrow / Home / End / Enter / Space / type-ahead, disabled
|
|
234
|
+
// options, optional icon + description). Promoted per ADR-0030 Decision G
|
|
235
|
+
// — the meta-ads-audit-dashboard task manager's status / priority / role
|
|
236
|
+
// pickers; its Users page hand-rolled a native <select> with a comment
|
|
237
|
+
// saying the package ships none. The workspace app is the second consumer
|
|
238
|
+
// (26 native <select>s, products/product-form.tsx says the same). Built on
|
|
239
|
+
// useAnchoredPosition + the layer stack; shares the popover surface chrome.
|
|
240
|
+
// `ListboxOption` / `LISTBOX_CLASS` are the row + popup chrome Combobox
|
|
241
|
+
// reuses, exported for the same reason POPOVER_SURFACE_STYLE is.
|
|
242
|
+
export {
|
|
243
|
+
Select,
|
|
244
|
+
ListboxOption,
|
|
245
|
+
LISTBOX_CLASS,
|
|
246
|
+
type SelectProps,
|
|
247
|
+
type SelectOption,
|
|
248
|
+
type SelectSize,
|
|
249
|
+
type ListboxOptionProps,
|
|
250
|
+
} from './select'
|
|
251
|
+
|
|
252
|
+
// Combobox — text input + filtered listbox (APG editable combobox with list
|
|
253
|
+
// autocomplete), single or multi-select with chips, optional grouped options
|
|
254
|
+
// with headings, a per-option live `count` slot and a `renderOption` slot.
|
|
255
|
+
// Async-friendly: `shouldFilter={false}` + `loading` + `onInputValueChange`;
|
|
256
|
+
// it never fetches. Promoted per ADR-0030 Decision G — the task manager's
|
|
257
|
+
// row / card / context-menu / palette pickers are "popover + search + live
|
|
258
|
+
// counts". Second consumer is the workspace app
|
|
259
|
+
// (sem-spec/location-autocomplete.tsx hand-rolls the role; TagChipInput
|
|
260
|
+
// carries the chip model this generalises).
|
|
261
|
+
export {
|
|
262
|
+
Combobox,
|
|
263
|
+
type ComboboxProps,
|
|
264
|
+
type ComboboxSingleProps,
|
|
265
|
+
type ComboboxMultipleProps,
|
|
266
|
+
type ComboboxOption,
|
|
267
|
+
type ComboboxGroup,
|
|
268
|
+
type ComboboxOptionState,
|
|
269
|
+
type ComboboxSize,
|
|
270
|
+
} from './combobox'
|
|
271
|
+
|
|
272
|
+
// Tooltip — hover (300 ms) / focus (immediate) triggered role=tooltip with
|
|
273
|
+
// aria-describedby wiring, Escape-dismissable through the layer stack, never
|
|
274
|
+
// focusable, pointer-events none, token-only inline surface like Kbd.
|
|
275
|
+
// Promoted per ADR-0030 Decision G (task manager: truncated cells, icon-only
|
|
276
|
+
// row actions, timestamps). Second consumer is the workspace app, which
|
|
277
|
+
// hand-rolls two today (tools/_shared/calc/info-tip.tsx,
|
|
278
|
+
// components/ui/sidebar/rail-tooltip.tsx).
|
|
279
|
+
export { Tooltip, type TooltipProps, type TooltipTriggerProps } from './tooltip'
|
|
280
|
+
|
|
175
281
|
// DropdownButton — Button-styled trigger + rich popover menu. Composed
|
|
176
282
|
// over <DropdownMenu> primitives. First consumer: Keywords dashboard's
|
|
177
283
|
// "New Keyword Plan" CTA (Set/Group picker). Reusable across other
|