@duro-app/ui 4.2.0 → 4.4.0
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/components/ActionBar/ActionBar.d.ts +7 -0
- package/dist/components/ActionBar/ActionBar.d.ts.map +1 -1
- package/dist/components/ActionBar/ActionBar.meta.d.ts.map +1 -1
- package/dist/components/ActionBar/ActionBar.stories.d.ts +3 -0
- package/dist/components/ActionBar/ActionBar.stories.d.ts.map +1 -1
- package/dist/components/ActionBar/styles.css.d.ts +25 -2
- package/dist/components/ActionBar/styles.css.d.ts.map +1 -1
- package/dist/components/DetailPanel/DetailPanel.meta.d.ts.map +1 -1
- package/dist/components/Dialog/Dialog.meta.d.ts.map +1 -1
- package/dist/components/Drawer/Drawer.meta.d.ts.map +1 -1
- package/dist/components/Menu/Menu.meta.d.ts.map +1 -1
- package/dist/components/Popover/Popover.d.ts +41 -0
- package/dist/components/Popover/Popover.d.ts.map +1 -0
- package/dist/components/Popover/Popover.meta.d.ts +3 -0
- package/dist/components/Popover/Popover.meta.d.ts.map +1 -0
- package/dist/components/Popover/Popover.stories.d.ts +11 -0
- package/dist/components/Popover/Popover.stories.d.ts.map +1 -0
- package/dist/components/Popover/outside.d.ts +2 -0
- package/dist/components/Popover/outside.d.ts.map +1 -0
- package/dist/components/Popover/position.d.ts +27 -0
- package/dist/components/Popover/position.d.ts.map +1 -0
- package/dist/components/Popover/styles.css.d.ts +61 -0
- package/dist/components/Popover/styles.css.d.ts.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2021 -1720
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/ActionBar/ActionBar.meta.ts +7 -0
- package/src/components/ActionBar/ActionBar.stories.tsx +95 -3
- package/src/components/ActionBar/ActionBar.tsx +12 -1
- package/src/components/ActionBar/styles.css.ts +34 -2
- package/src/components/DetailPanel/DetailPanel.meta.ts +12 -0
- package/src/components/Dialog/Dialog.meta.ts +6 -0
- package/src/components/Drawer/Drawer.meta.ts +6 -0
- package/src/components/Menu/Menu.meta.ts +5 -1
- package/src/components/Popover/Popover.meta.ts +69 -0
- package/src/components/Popover/Popover.stories.tsx +250 -0
- package/src/components/Popover/Popover.tsx +293 -0
- package/src/components/Popover/outside.ts +9 -0
- package/src/components/Popover/position.ts +48 -0
- package/src/components/Popover/styles.css.ts +75 -0
- package/src/components/Tooltip/Tooltip.meta.ts +1 -1
- package/src/index.ts +2 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ReactNode,
|
|
3
|
+
type Ref,
|
|
4
|
+
createContext,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useId,
|
|
9
|
+
useImperativeHandle,
|
|
10
|
+
useLayoutEffect,
|
|
11
|
+
useRef,
|
|
12
|
+
useState,
|
|
13
|
+
} from 'react'
|
|
14
|
+
import {createPortal} from 'react-dom'
|
|
15
|
+
import {html} from 'react-strict-dom'
|
|
16
|
+
import {usePortalMount} from '../ThemeProvider/ThemeProvider'
|
|
17
|
+
import {styles} from './styles.css'
|
|
18
|
+
import {computePopoverPosition, type PopoverAlign, type PopoverSide} from './position'
|
|
19
|
+
import {isOutsidePress} from './outside'
|
|
20
|
+
|
|
21
|
+
// --- Context ---
|
|
22
|
+
|
|
23
|
+
type IgnoreEntry = Element | null | (() => Element | null)
|
|
24
|
+
|
|
25
|
+
interface PopoverContextValue {
|
|
26
|
+
open: boolean
|
|
27
|
+
setOpen: (open: boolean) => void
|
|
28
|
+
popoverId: string
|
|
29
|
+
side: PopoverSide
|
|
30
|
+
align: PopoverAlign
|
|
31
|
+
anchor?: () => DOMRect
|
|
32
|
+
triggerRef: React.RefObject<HTMLButtonElement | null>
|
|
33
|
+
popupRef: React.RefObject<HTMLDivElement | null>
|
|
34
|
+
repositionRef: React.RefObject<(() => void) | null>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const PopoverContext = createContext<PopoverContextValue | null>(null)
|
|
38
|
+
|
|
39
|
+
function usePopover(part: string) {
|
|
40
|
+
const ctx = useContext(PopoverContext)
|
|
41
|
+
if (!ctx) throw new Error(`Popover.${part} must be used within Popover.Root`)
|
|
42
|
+
return ctx
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const FOCUSABLE =
|
|
46
|
+
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
47
|
+
|
|
48
|
+
const OFFSET = 4 // `xs` token
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Whether a nested popup control owns this Escape: a Select, Menu or
|
|
52
|
+
* Combobox inside the popup closes on Esc without preventing default, and
|
|
53
|
+
* that Esc must close only it. Only controls that open a popup count — an
|
|
54
|
+
* expanded trigger that declares one (`aria-haspopup`), an expanded combobox,
|
|
55
|
+
* or focus inside a listbox or menu — never a plain disclosure, and never
|
|
56
|
+
* this popover's own Trigger.
|
|
57
|
+
*
|
|
58
|
+
* holds-until: Duro's Select, Menu and Combobox call preventDefault on the
|
|
59
|
+
* Escape they consume; the defaultPrevented check alone then suffices and
|
|
60
|
+
* this selector goes.
|
|
61
|
+
*/
|
|
62
|
+
const NESTED_POPUP_OWNER =
|
|
63
|
+
'[aria-expanded="true"][aria-haspopup], [role="combobox"][aria-expanded="true"], [role="listbox"], [role="menu"]'
|
|
64
|
+
|
|
65
|
+
function nestedOwnsEscape(target: EventTarget | null, ownTrigger: Element | null): boolean {
|
|
66
|
+
if (!(target instanceof Element)) return false
|
|
67
|
+
const owner = target.closest(NESTED_POPUP_OWNER)
|
|
68
|
+
return owner !== null && owner !== ownTrigger
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --- Root ---
|
|
72
|
+
|
|
73
|
+
export interface PopoverHandle {
|
|
74
|
+
/** Re-measure the anchor and re-place the popup. */
|
|
75
|
+
reposition: () => void
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface RootProps {
|
|
79
|
+
children: ReactNode
|
|
80
|
+
open?: boolean
|
|
81
|
+
defaultOpen?: boolean
|
|
82
|
+
onOpenChange?: (open: boolean) => void
|
|
83
|
+
side?: PopoverSide
|
|
84
|
+
align?: PopoverAlign
|
|
85
|
+
/** Virtual anchor: a viewport rect. Replaces the Trigger as the anchor. */
|
|
86
|
+
anchor?: () => DOMRect
|
|
87
|
+
/** Elements (or getters) whose pointerdown does not count as an outside press. */
|
|
88
|
+
ignore?: ReadonlyArray<IgnoreEntry>
|
|
89
|
+
ref?: Ref<PopoverHandle>
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function Root({
|
|
93
|
+
children,
|
|
94
|
+
open: openProp,
|
|
95
|
+
defaultOpen = false,
|
|
96
|
+
onOpenChange,
|
|
97
|
+
side = 'bottom',
|
|
98
|
+
align = 'start',
|
|
99
|
+
anchor,
|
|
100
|
+
ignore,
|
|
101
|
+
ref,
|
|
102
|
+
}: RootProps) {
|
|
103
|
+
const [inner, setInner] = useState(defaultOpen)
|
|
104
|
+
const controlled = openProp !== undefined
|
|
105
|
+
const open = controlled ? openProp : inner
|
|
106
|
+
const popoverId = useId()
|
|
107
|
+
const triggerRef = useRef<HTMLButtonElement | null>(null)
|
|
108
|
+
const popupRef = useRef<HTMLDivElement | null>(null)
|
|
109
|
+
const repositionRef = useRef<(() => void) | null>(null)
|
|
110
|
+
const previousFocusRef = useRef<HTMLElement | null>(null)
|
|
111
|
+
const ignoreRef = useRef(ignore)
|
|
112
|
+
ignoreRef.current = ignore
|
|
113
|
+
const openRef = useRef(open)
|
|
114
|
+
openRef.current = open
|
|
115
|
+
|
|
116
|
+
const setOpen = useCallback(
|
|
117
|
+
(next: boolean) => {
|
|
118
|
+
if (!controlled) setInner(next)
|
|
119
|
+
if (next !== openRef.current) onOpenChange?.(next)
|
|
120
|
+
},
|
|
121
|
+
[controlled, onOpenChange],
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
useImperativeHandle(ref, () => ({reposition: () => repositionRef.current?.()}), [])
|
|
125
|
+
|
|
126
|
+
// Focus: into the popup on open, back to the trigger (or the previously
|
|
127
|
+
// focused element when there is no trigger) on close.
|
|
128
|
+
const wasOpenRef = useRef(false)
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (open && !wasOpenRef.current) {
|
|
131
|
+
previousFocusRef.current = document.activeElement as HTMLElement | null
|
|
132
|
+
const popup = popupRef.current
|
|
133
|
+
if (popup) {
|
|
134
|
+
const first = popup.querySelector<HTMLElement>(FOCUSABLE)
|
|
135
|
+
;(first ?? popup).focus({preventScroll: true})
|
|
136
|
+
}
|
|
137
|
+
} else if (!open && wasOpenRef.current) {
|
|
138
|
+
const target = triggerRef.current ?? previousFocusRef.current
|
|
139
|
+
target?.focus?.()
|
|
140
|
+
previousFocusRef.current = null
|
|
141
|
+
}
|
|
142
|
+
wasOpenRef.current = open
|
|
143
|
+
}, [open])
|
|
144
|
+
|
|
145
|
+
// Esc and outside press.
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (!open) return
|
|
148
|
+
|
|
149
|
+
function onKeyDown(e: KeyboardEvent) {
|
|
150
|
+
if (e.key !== 'Escape' || e.defaultPrevented) return
|
|
151
|
+
if (nestedOwnsEscape(e.target, triggerRef.current)) return
|
|
152
|
+
e.preventDefault()
|
|
153
|
+
setOpen(false)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function onPointerDown(e: PointerEvent) {
|
|
157
|
+
const ignored = (ignoreRef.current ?? []).map((entry) =>
|
|
158
|
+
typeof entry === 'function' ? entry() : entry,
|
|
159
|
+
)
|
|
160
|
+
const inside = [popupRef.current, triggerRef.current, ...ignored]
|
|
161
|
+
if (isOutsidePress(e.target as Node | null, inside)) setOpen(false)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
document.addEventListener('keydown', onKeyDown)
|
|
165
|
+
document.addEventListener('pointerdown', onPointerDown, true)
|
|
166
|
+
return () => {
|
|
167
|
+
document.removeEventListener('keydown', onKeyDown)
|
|
168
|
+
document.removeEventListener('pointerdown', onPointerDown, true)
|
|
169
|
+
}
|
|
170
|
+
}, [open, setOpen])
|
|
171
|
+
|
|
172
|
+
const ctx: PopoverContextValue = {
|
|
173
|
+
open,
|
|
174
|
+
setOpen,
|
|
175
|
+
popoverId,
|
|
176
|
+
side,
|
|
177
|
+
align,
|
|
178
|
+
anchor,
|
|
179
|
+
triggerRef,
|
|
180
|
+
popupRef,
|
|
181
|
+
repositionRef,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return <PopoverContext.Provider value={ctx}>{children}</PopoverContext.Provider>
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// --- Trigger ---
|
|
188
|
+
// Trigger IS the button (like Menu.Trigger): give it a label, not a Button.
|
|
189
|
+
|
|
190
|
+
function Trigger({children}: {children: ReactNode}) {
|
|
191
|
+
const {open, setOpen, popoverId, triggerRef} = usePopover('Trigger')
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<html.button
|
|
195
|
+
ref={triggerRef}
|
|
196
|
+
type="button"
|
|
197
|
+
onClick={() => setOpen(!open)}
|
|
198
|
+
aria-haspopup="dialog"
|
|
199
|
+
aria-expanded={open}
|
|
200
|
+
aria-controls={open ? popoverId : undefined}
|
|
201
|
+
style={styles.trigger}
|
|
202
|
+
>
|
|
203
|
+
{children}
|
|
204
|
+
</html.button>
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// --- Popup ---
|
|
209
|
+
|
|
210
|
+
interface PopupProps {
|
|
211
|
+
children: ReactNode
|
|
212
|
+
/** Accessible name for the dialog. */
|
|
213
|
+
label: string
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function Popup({children, label}: PopupProps) {
|
|
217
|
+
const {open, popoverId, side, align, anchor, triggerRef, popupRef, repositionRef} =
|
|
218
|
+
usePopover('Popup')
|
|
219
|
+
const [coords, setCoords] = useState({top: 0, left: 0})
|
|
220
|
+
// Inside ThemeProvider's portal layer, like Select/Dialog/Drawer: theme
|
|
221
|
+
// tokens cascade into the popup and it stacks above dialogs.
|
|
222
|
+
const mount = usePortalMount()
|
|
223
|
+
|
|
224
|
+
// Measure before paint on open; re-measure on scroll/resize (capture phase
|
|
225
|
+
// catches any scrolling ancestor) and when Root.reposition() is called.
|
|
226
|
+
useLayoutEffect(() => {
|
|
227
|
+
if (!open) {
|
|
228
|
+
repositionRef.current = null
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
const update = () => {
|
|
232
|
+
const popup = popupRef.current
|
|
233
|
+
if (!popup) return
|
|
234
|
+
const rect = anchor ? anchor() : triggerRef.current?.getBoundingClientRect()
|
|
235
|
+
if (!rect) return
|
|
236
|
+
const next = computePopoverPosition({
|
|
237
|
+
anchor: {top: rect.top, left: rect.left, width: rect.width, height: rect.height},
|
|
238
|
+
popup: {width: popup.offsetWidth, height: popup.offsetHeight},
|
|
239
|
+
viewport: {width: window.innerWidth, height: window.innerHeight},
|
|
240
|
+
side,
|
|
241
|
+
align,
|
|
242
|
+
offset: OFFSET,
|
|
243
|
+
})
|
|
244
|
+
setCoords((prev) =>
|
|
245
|
+
prev.top === next.top && prev.left === next.left ? prev : {top: next.top, left: next.left},
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
repositionRef.current = update
|
|
249
|
+
update()
|
|
250
|
+
window.addEventListener('resize', update)
|
|
251
|
+
window.addEventListener('scroll', update, true)
|
|
252
|
+
return () => {
|
|
253
|
+
repositionRef.current = null
|
|
254
|
+
window.removeEventListener('resize', update)
|
|
255
|
+
window.removeEventListener('scroll', update, true)
|
|
256
|
+
}
|
|
257
|
+
}, [open, side, align, anchor, triggerRef, popupRef, repositionRef])
|
|
258
|
+
|
|
259
|
+
if (!open || typeof document === 'undefined') return null
|
|
260
|
+
|
|
261
|
+
return createPortal(
|
|
262
|
+
<html.div
|
|
263
|
+
ref={popupRef}
|
|
264
|
+
id={popoverId}
|
|
265
|
+
role="dialog"
|
|
266
|
+
aria-label={label}
|
|
267
|
+
tabIndex={-1}
|
|
268
|
+
style={[styles.popup, styles.popupPosition(coords.top, coords.left)]}
|
|
269
|
+
>
|
|
270
|
+
{children}
|
|
271
|
+
</html.div>,
|
|
272
|
+
mount ?? document.body,
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// --- Close ---
|
|
277
|
+
|
|
278
|
+
function Close({children}: {children: ReactNode}) {
|
|
279
|
+
const {setOpen} = usePopover('Close')
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<html.button type="button" onClick={() => setOpen(false)} style={styles.close}>
|
|
283
|
+
{children}
|
|
284
|
+
</html.button>
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export const Popover = {
|
|
289
|
+
Root,
|
|
290
|
+
Trigger,
|
|
291
|
+
Popup,
|
|
292
|
+
Close,
|
|
293
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// True when a pointerdown target lies outside the popup, the trigger and every
|
|
2
|
+
// ignored element. `inside` may hold nulls (refs not yet attached).
|
|
3
|
+
export function isOutsidePress(
|
|
4
|
+
target: Node | null,
|
|
5
|
+
inside: ReadonlyArray<Element | null>,
|
|
6
|
+
): boolean {
|
|
7
|
+
if (!target) return true
|
|
8
|
+
return !inside.some((el) => el != null && el.contains(target))
|
|
9
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface PopoverRect {
|
|
2
|
+
top: number
|
|
3
|
+
left: number
|
|
4
|
+
width: number
|
|
5
|
+
height: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface PopoverSize {
|
|
9
|
+
width: number
|
|
10
|
+
height: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type PopoverSide = 'top' | 'bottom'
|
|
14
|
+
export type PopoverAlign = 'start' | 'center' | 'end'
|
|
15
|
+
|
|
16
|
+
interface Input {
|
|
17
|
+
anchor: PopoverRect
|
|
18
|
+
popup: PopoverSize
|
|
19
|
+
viewport: PopoverSize
|
|
20
|
+
side: PopoverSide
|
|
21
|
+
align: PopoverAlign
|
|
22
|
+
offset: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Pure placement: puts the popup on the requested side of the anchor, flips to
|
|
26
|
+
// the opposite side when it would overflow the viewport there (and the other
|
|
27
|
+
// side fits), and clamps horizontally into the viewport.
|
|
28
|
+
export function computePopoverPosition({anchor, popup, viewport, side, align, offset}: Input) {
|
|
29
|
+
const below = anchor.top + anchor.height + offset
|
|
30
|
+
const above = anchor.top - offset - popup.height
|
|
31
|
+
const fitsBelow = below + popup.height <= viewport.height
|
|
32
|
+
const fitsAbove = above >= 0
|
|
33
|
+
|
|
34
|
+
let resolved: PopoverSide = side
|
|
35
|
+
if (side === 'bottom' && !fitsBelow && fitsAbove) resolved = 'top'
|
|
36
|
+
else if (side === 'top' && !fitsAbove && fitsBelow) resolved = 'bottom'
|
|
37
|
+
|
|
38
|
+
const top = resolved === 'bottom' ? below : above
|
|
39
|
+
|
|
40
|
+
let left = anchor.left
|
|
41
|
+
if (align === 'center') left = anchor.left + (anchor.width - popup.width) / 2
|
|
42
|
+
else if (align === 'end') left = anchor.left + anchor.width - popup.width
|
|
43
|
+
|
|
44
|
+
const maxLeft = Math.max(0, viewport.width - popup.width)
|
|
45
|
+
left = Math.min(Math.max(0, left), maxLeft)
|
|
46
|
+
|
|
47
|
+
return {top, left, side: resolved}
|
|
48
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {css} from 'react-strict-dom'
|
|
2
|
+
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
3
|
+
import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
|
|
4
|
+
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
5
|
+
import {shadows} from '@duro-app/tokens/tokens/shadows.css'
|
|
6
|
+
import {duration} from '@duro-app/tokens/tokens/motion.css'
|
|
7
|
+
|
|
8
|
+
export const styles = css.create({
|
|
9
|
+
trigger: {
|
|
10
|
+
display: 'inline-flex',
|
|
11
|
+
alignItems: 'center',
|
|
12
|
+
gap: spacing.sm,
|
|
13
|
+
paddingTop: spacing.sm,
|
|
14
|
+
paddingBottom: spacing.sm,
|
|
15
|
+
paddingLeft: spacing.md,
|
|
16
|
+
paddingRight: spacing.md,
|
|
17
|
+
fontFamily: typography.fontFamily,
|
|
18
|
+
fontSize: typography.fontSizeSm,
|
|
19
|
+
color: colors.text,
|
|
20
|
+
backgroundColor: {
|
|
21
|
+
default: 'transparent',
|
|
22
|
+
':hover': colors.bgCardHover,
|
|
23
|
+
},
|
|
24
|
+
borderWidth: 1,
|
|
25
|
+
borderStyle: 'solid',
|
|
26
|
+
borderColor: colors.border,
|
|
27
|
+
borderRadius: radii.sm,
|
|
28
|
+
cursor: 'pointer',
|
|
29
|
+
transitionProperty: 'background-color, border-color',
|
|
30
|
+
transitionDuration: duration.fast,
|
|
31
|
+
},
|
|
32
|
+
popup: {
|
|
33
|
+
position: 'fixed',
|
|
34
|
+
top: 0,
|
|
35
|
+
left: 0,
|
|
36
|
+
boxSizing: 'border-box',
|
|
37
|
+
backgroundColor: colors.bgCard,
|
|
38
|
+
borderWidth: 1,
|
|
39
|
+
borderStyle: 'solid',
|
|
40
|
+
borderColor: colors.border,
|
|
41
|
+
borderRadius: radii.sm,
|
|
42
|
+
boxShadow: shadows.md,
|
|
43
|
+
padding: spacing.md,
|
|
44
|
+
fontFamily: typography.fontFamily,
|
|
45
|
+
fontSize: typography.fontSizeSm,
|
|
46
|
+
color: colors.text,
|
|
47
|
+
zIndex: 50,
|
|
48
|
+
// The portal layer is pointer-events: none so clicks fall through it;
|
|
49
|
+
// the popup takes them back.
|
|
50
|
+
pointerEvents: 'auto',
|
|
51
|
+
},
|
|
52
|
+
close: {
|
|
53
|
+
display: 'inline-flex',
|
|
54
|
+
alignItems: 'center',
|
|
55
|
+
paddingTop: spacing.xs,
|
|
56
|
+
paddingBottom: spacing.xs,
|
|
57
|
+
paddingLeft: spacing.sm,
|
|
58
|
+
paddingRight: spacing.sm,
|
|
59
|
+
fontFamily: typography.fontFamily,
|
|
60
|
+
fontSize: typography.fontSizeSm,
|
|
61
|
+
color: colors.text,
|
|
62
|
+
backgroundColor: {
|
|
63
|
+
default: 'transparent',
|
|
64
|
+
':hover': colors.bgCardHover,
|
|
65
|
+
},
|
|
66
|
+
borderWidth: 0,
|
|
67
|
+
borderRadius: radii.sm,
|
|
68
|
+
cursor: 'pointer',
|
|
69
|
+
},
|
|
70
|
+
// Dynamic position — applied at runtime from the measured anchor.
|
|
71
|
+
popupPosition: (top: number, left: number) => ({
|
|
72
|
+
top,
|
|
73
|
+
left,
|
|
74
|
+
}),
|
|
75
|
+
})
|
|
@@ -9,7 +9,7 @@ export const meta: ComponentMeta = {
|
|
|
9
9
|
],
|
|
10
10
|
whenNotToUse: [
|
|
11
11
|
'Critical information that must always be visible — use Text or Callout',
|
|
12
|
-
'Interactive content (links, buttons) — tooltips are not interactive',
|
|
12
|
+
'Interactive content (links, buttons) — tooltips are not interactive — use Popover',
|
|
13
13
|
],
|
|
14
14
|
anatomy: {
|
|
15
15
|
required: ['Root', 'Trigger'],
|
package/src/index.ts
CHANGED
|
@@ -90,6 +90,8 @@ export {Text, type TextVariant, type TextColor} from './components/Text/Text'
|
|
|
90
90
|
export {Toggle, type ToggleSize} from './components/Toggle/Toggle'
|
|
91
91
|
export {ToggleGroup} from './components/ToggleGroup/ToggleGroup'
|
|
92
92
|
export {Tooltip} from './components/Tooltip/Tooltip'
|
|
93
|
+
export {Popover} from './components/Popover/Popover'
|
|
94
|
+
export type {PopoverHandle} from './components/Popover/Popover'
|
|
93
95
|
export {Switch} from './components/Switch/Switch'
|
|
94
96
|
|
|
95
97
|
// Layout primitives
|