@lovett/ui 0.0.10 → 0.1.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.
Files changed (158) hide show
  1. package/dist/chunk-RBYWGBQ2.js +2752 -0
  2. package/dist/chunk-RBYWGBQ2.js.map +1 -0
  3. package/dist/index.d.ts +6064 -80
  4. package/dist/index.js +21561 -9696
  5. package/dist/index.js.map +1 -1
  6. package/dist/rich-composer-impl-5NO443A6.js +1859 -0
  7. package/dist/rich-composer-impl-5NO443A6.js.map +1 -0
  8. package/dist/styles.css +1586 -2
  9. package/dist/theme-v2.css +228 -0
  10. package/dist/tokens.css +121 -0
  11. package/package.json +8 -1
  12. package/src/__tests__/anchor.test.tsx +422 -0
  13. package/src/__tests__/avatar.test.tsx +272 -0
  14. package/src/__tests__/bar-chart.test.tsx +809 -0
  15. package/src/__tests__/board.test.tsx +420 -0
  16. package/src/__tests__/chart-math.test.ts +922 -0
  17. package/src/__tests__/chart-series.test.ts +339 -0
  18. package/src/__tests__/code-block.test.tsx +134 -0
  19. package/src/__tests__/combobox.test.tsx +677 -0
  20. package/src/__tests__/display-popover.test.tsx +195 -0
  21. package/src/__tests__/display-store.test.tsx +208 -0
  22. package/src/__tests__/donut-chart.test.tsx +397 -0
  23. package/src/__tests__/dropdown-menu.test.tsx +572 -0
  24. package/src/__tests__/filter-menu.test.tsx +175 -0
  25. package/src/__tests__/gauge-ring.test.tsx +233 -0
  26. package/src/__tests__/helpers/geometry.ts +58 -0
  27. package/src/__tests__/layer-stack.test.tsx +228 -0
  28. package/src/__tests__/line-chart.test.tsx +612 -0
  29. package/src/__tests__/popover.test.tsx +460 -0
  30. package/src/__tests__/ranked-bars.test.tsx +343 -0
  31. package/src/__tests__/remark-underline.test.ts +194 -0
  32. package/src/__tests__/select.test.tsx +543 -0
  33. package/src/__tests__/sparkline.test.tsx +368 -0
  34. package/src/__tests__/stat-layer.test.tsx +271 -0
  35. package/src/__tests__/stat-strip.test.tsx +175 -0
  36. package/src/__tests__/status.test.tsx +293 -0
  37. package/src/__tests__/tabs.test.tsx +303 -0
  38. package/src/__tests__/token-shape.test.ts +132 -2
  39. package/src/__tests__/tooltip.test.tsx +355 -0
  40. package/src/avatar.tsx +352 -0
  41. package/src/bar-chart.tsx +1214 -0
  42. package/src/board.tsx +658 -0
  43. package/src/calculator-shell-v2.tsx +19 -39
  44. package/src/chart-frame.tsx +960 -0
  45. package/src/chart-legend.tsx +304 -0
  46. package/src/chart-tooltip.tsx +267 -0
  47. package/src/code-block.tsx +62 -8
  48. package/src/combobox.tsx +796 -0
  49. package/src/delta-chip.tsx +263 -0
  50. package/src/detail/__tests__/activity-pane.test.tsx +186 -0
  51. package/src/detail/__tests__/detail-chrome.test.tsx +134 -0
  52. package/src/detail/__tests__/detail-surface.test.tsx +529 -0
  53. package/src/detail/__tests__/field-row.test.tsx +357 -0
  54. package/src/detail/activity-pane.tsx +507 -0
  55. package/src/detail/calendar.tsx +355 -0
  56. package/src/detail/detail-divider.tsx +261 -0
  57. package/src/detail/detail-header.tsx +287 -0
  58. package/src/detail/detail-menu.tsx +254 -0
  59. package/src/detail/detail-surface.tsx +1110 -0
  60. package/src/detail/field-list.tsx +196 -0
  61. package/src/detail/field-row.tsx +1131 -0
  62. package/src/detail/index.ts +58 -0
  63. package/src/detail/segmented-choice.tsx +94 -0
  64. package/src/detail/types.ts +129 -0
  65. package/src/display-popover.tsx +487 -0
  66. package/src/display-store.tsx +242 -0
  67. package/src/donut-chart.tsx +988 -0
  68. package/src/dropdown-menu.tsx +412 -151
  69. package/src/filter-core/EXPORTS.md +85 -0
  70. package/src/filter-core/__tests__/columns.test.ts +159 -0
  71. package/src/filter-core/__tests__/faceting.test.ts +193 -0
  72. package/src/filter-core/__tests__/filter-fns.test.ts +519 -0
  73. package/src/filter-core/__tests__/operators.test.ts +235 -0
  74. package/src/filter-core/__tests__/state.test.ts +268 -0
  75. package/src/filter-core/__tests__/url.test.ts +350 -0
  76. package/src/filter-core/columns.ts +134 -0
  77. package/src/filter-core/date-utils.ts +38 -0
  78. package/src/filter-core/examples/task-filter-columns.ts +121 -0
  79. package/src/filter-core/faceting.ts +120 -0
  80. package/src/filter-core/filter-fns.ts +335 -0
  81. package/src/filter-core/index.ts +105 -0
  82. package/src/filter-core/operators.ts +433 -0
  83. package/src/filter-core/state.ts +280 -0
  84. package/src/filter-core/types.ts +247 -0
  85. package/src/filter-core/url.ts +261 -0
  86. package/src/filter-dropdown.tsx +12 -0
  87. package/src/filter-menu.tsx +649 -0
  88. package/src/floating-drawer.tsx +19 -1
  89. package/src/gauge-ring.tsx +435 -0
  90. package/src/hue.ts +52 -0
  91. package/src/index.ts +402 -0
  92. package/src/kbd.tsx +27 -4
  93. package/src/lib/anchor.ts +427 -0
  94. package/src/lib/chart.ts +866 -0
  95. package/src/lib/focus.ts +74 -0
  96. package/src/lib/layer-stack.ts +215 -0
  97. package/src/lib/refs.ts +31 -0
  98. package/src/lib/remark-underline.ts +443 -0
  99. package/src/lib/series.ts +169 -0
  100. package/src/line-chart.tsx +1176 -0
  101. package/src/markdown.tsx +26 -7
  102. package/src/metric-card.tsx +57 -22
  103. package/src/modal.tsx +73 -66
  104. package/src/popover.tsx +407 -0
  105. package/src/progress-ledger.tsx +304 -0
  106. package/src/ranked-bars.tsx +386 -0
  107. package/src/segmented-pill.tsx +32 -9
  108. package/src/select.tsx +646 -0
  109. package/src/sortable.tsx +315 -1
  110. package/src/sparkline.tsx +416 -0
  111. package/src/stat-card.tsx +376 -0
  112. package/src/stat-row.tsx +108 -70
  113. package/src/stat-strip.tsx +327 -0
  114. package/src/status.tsx +215 -0
  115. package/src/styles.css +1586 -2
  116. package/src/tabs.tsx +206 -25
  117. package/src/task-card.tsx +610 -0
  118. package/src/thread/__tests__/comment-body-hostile.test.tsx +331 -0
  119. package/src/thread/__tests__/comment-tree.test.ts +151 -0
  120. package/src/thread/__tests__/emoji.test.ts +187 -0
  121. package/src/thread/__tests__/fixtures/thread-fixture.ts +235 -0
  122. package/src/thread/__tests__/link-preview-source.test.ts +120 -0
  123. package/src/thread/__tests__/link-preview.test.tsx +600 -0
  124. package/src/thread/__tests__/markdown-format.test.ts +82 -0
  125. package/src/thread/__tests__/markdown-spec.test.ts +469 -0
  126. package/src/thread/__tests__/relative-time.test.ts +71 -0
  127. package/src/thread/__tests__/rich-composer.test.tsx +799 -0
  128. package/src/thread/__tests__/scroll-caret.test.ts +58 -0
  129. package/src/thread/__tests__/suggestion-list.test.tsx +648 -0
  130. package/src/thread/__tests__/thread-scroll-ownership.test.tsx +88 -0
  131. package/src/thread/__tests__/thread.test.tsx +662 -0
  132. package/src/thread/__tests__/use-attachments.test.tsx +679 -0
  133. package/src/thread/actions.tsx +196 -0
  134. package/src/thread/attachments.tsx +1071 -0
  135. package/src/thread/comment-body.tsx +148 -0
  136. package/src/thread/comment-tree.ts +182 -0
  137. package/src/thread/comment.tsx +915 -0
  138. package/src/thread/composer-footer.tsx +125 -0
  139. package/src/thread/composer.tsx +319 -0
  140. package/src/thread/emoji.ts +283 -0
  141. package/src/thread/index.ts +153 -0
  142. package/src/thread/link-preview.tsx +341 -0
  143. package/src/thread/markdown-format.ts +155 -0
  144. package/src/thread/markdown-spec.ts +754 -0
  145. package/src/thread/rail.tsx +372 -0
  146. package/src/thread/reactions.tsx +415 -0
  147. package/src/thread/relative-time.tsx +94 -0
  148. package/src/thread/rich-composer-impl.tsx +1601 -0
  149. package/src/thread/rich-composer.tsx +195 -0
  150. package/src/thread/scroll-caret.ts +37 -0
  151. package/src/thread/suggestion-list.tsx +182 -0
  152. package/src/thread/thread.tsx +718 -0
  153. package/src/thread/types.ts +218 -0
  154. package/src/thread/use-attachments.ts +598 -0
  155. package/src/thread/use-now.ts +73 -0
  156. package/src/thread/use-thread.ts +316 -0
  157. package/src/tokens.css +121 -0
  158. package/src/tooltip.tsx +297 -0
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Tooltip — a short, non-interactive label that appears on hover or focus.
3
+ *
4
+ * Wraps ONE trigger element (always "asChild": the child is cloned with the
5
+ * ref, the hover / focus handlers and `aria-describedby`; no wrapper node is
6
+ * added). The tip is `role="tooltip"`, portals to `document.body`, positions
7
+ * through `useAnchoredPosition` (default: above, centred; flips below when
8
+ * there is no room), enters with `.ds-enter-pop` once positioned, and is
9
+ * `pointer-events: none` so it never blocks the thing under it.
10
+ *
11
+ * Timing: hover opens after `openDelay` (300 ms) and closes after
12
+ * `closeDelay` (0 ms) — the delay is what keeps a mouse sweeping across a
13
+ * toolbar from flashing every label. Keyboard focus opens IMMEDIATELY (a
14
+ * keyboard user has already committed to the control); a focus that arrives
15
+ * by pointer (clicking a button) does not open it, and pressing the trigger
16
+ * dismisses it. Escape dismisses through the shared layer stack, so it never
17
+ * steals the keypress from a Modal underneath — a tooltip is pushed above
18
+ * the modal only while it is showing.
19
+ *
20
+ * Never traps or moves focus: the tip is not focusable and the trigger keeps
21
+ * whatever focus it had. Only non-interactive content belongs in it — the
22
+ * trigger should still have its own accessible name (`aria-label`) when it
23
+ * is icon-only; the tooltip DESCRIBES, it does not name.
24
+ *
25
+ * Promoted per ADR-0030 Decision G (meta-ads-audit-dashboard task manager:
26
+ * truncated cells, icon-only row actions, relative timestamps). Second
27
+ * consumer is the workspace app, which hand-rolls two today —
28
+ * `tools/_shared/calc/info-tip.tsx` and
29
+ * `components/ui/sidebar/rail-tooltip.tsx` — and leans on 900+ `title`
30
+ * attributes elsewhere.
31
+ *
32
+ * Token discipline: token-only inline surface like Kbd — INTERNAL
33
+ * `--popover` / `--popover-foreground` (allowed inside packages/ui) with
34
+ * public `--border`, `--shadow-md`, `--radius-sm`, `--space-*`. The same
35
+ * chrome family as Popover / DropdownMenu, so a tip and a menu read as one
36
+ * system. Geometry from the hook; motion from `.ds-enter-pop`, which
37
+ * honours prefers-reduced-motion in styles.css.
38
+ *
39
+ * Usage:
40
+ *
41
+ * <Tooltip content="Archive task">
42
+ * <Button variant="secondary" aria-label="Archive task"><Archive /></Button>
43
+ * </Tooltip>
44
+ *
45
+ * <Tooltip content={fullTitle} side="bottom" align="start">
46
+ * <span className="truncate">{fullTitle}</span>
47
+ * </Tooltip>
48
+ */
49
+
50
+ import {
51
+ cloneElement,
52
+ isValidElement,
53
+ useCallback,
54
+ useEffect,
55
+ useId,
56
+ useMemo,
57
+ useRef,
58
+ useState,
59
+ type CSSProperties,
60
+ type FocusEvent as ReactFocusEvent,
61
+ type HTMLAttributes,
62
+ type PointerEvent as ReactPointerEvent,
63
+ type ReactElement,
64
+ type ReactNode,
65
+ type Ref,
66
+ } from 'react'
67
+ import { createPortal } from 'react-dom'
68
+ import { cn } from './lib/utils'
69
+ import { composeRefs } from './lib/refs'
70
+ import { useLayer } from './lib/layer-stack'
71
+ import { useAnchoredPosition, type AnchorAlign, type AnchorSide } from './lib/anchor'
72
+
73
+ export interface TooltipProps {
74
+ /** The tip's content. Nothing renders (children pass through untouched) when null. */
75
+ content: ReactNode
76
+ /** Exactly one element — it becomes the trigger. */
77
+ children: ReactElement<TooltipTriggerProps>
78
+ /** Default `top`. Flips when it cannot fit. */
79
+ side?: AnchorSide
80
+ /** Default `center`. */
81
+ align?: AnchorAlign
82
+ /** Gap from the trigger, px. Default 6. */
83
+ offset?: number
84
+ /** Hover-open delay, ms. Default 300. Focus opens immediately. */
85
+ openDelay?: number
86
+ /** Close delay, ms. Default 0. */
87
+ closeDelay?: number
88
+ /** Controlled open state. Omit for internal state. */
89
+ open?: boolean
90
+ defaultOpen?: boolean
91
+ onOpenChange?: (open: boolean) => void
92
+ /** Extra classes on the tip surface. */
93
+ className?: string
94
+ }
95
+
96
+ /** What the cloned trigger element must accept. */
97
+ export type TooltipTriggerProps = HTMLAttributes<HTMLElement> & {
98
+ ref?: Ref<HTMLElement>
99
+ }
100
+
101
+ const TOOLTIP_SURFACE_STYLE: CSSProperties = {
102
+ background: 'rgb(var(--popover))',
103
+ color: 'rgb(var(--popover-foreground))',
104
+ border: '1px solid rgb(var(--border))',
105
+ boxShadow: 'var(--shadow-md)',
106
+ borderRadius: 'var(--radius-sm)',
107
+ padding: 'var(--space-1) var(--space-2)',
108
+ }
109
+
110
+ export function Tooltip({
111
+ content,
112
+ children,
113
+ side = 'top',
114
+ align = 'center',
115
+ offset = 6,
116
+ openDelay = 300,
117
+ closeDelay = 0,
118
+ open: openProp,
119
+ defaultOpen,
120
+ onOpenChange,
121
+ className,
122
+ }: TooltipProps) {
123
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(Boolean(defaultOpen))
124
+ const isControlled = openProp !== undefined
125
+ const open = isControlled ? Boolean(openProp) : uncontrolledOpen
126
+ const setOpen = useCallback(
127
+ (next: boolean) => {
128
+ if (!isControlled) setUncontrolledOpen(next)
129
+ onOpenChange?.(next)
130
+ },
131
+ [isControlled, onOpenChange],
132
+ )
133
+
134
+ const triggerRef = useRef<HTMLElement | null>(null)
135
+ const tipRef = useRef<HTMLDivElement | null>(null)
136
+ const timerRef = useRef<number | null>(null)
137
+ // Set on pointerdown, cleared on the next pointerup / pointercancel
138
+ // anywhere: a focus that arrives during that window came from the pointer,
139
+ // not the keyboard. The document listener is held in a ref so an
140
+ // interrupted press (unmount mid-press, a second pointerdown) removes it
141
+ // rather than leaving a stale closure on document.
142
+ const pointerDownRef = useRef(false)
143
+ const pressEndListenerRef = useRef<(() => void) | null>(null)
144
+ const clearPressEndListener = useCallback(() => {
145
+ const listener = pressEndListenerRef.current
146
+ if (listener === null) return
147
+ pressEndListenerRef.current = null
148
+ document.removeEventListener('pointerup', listener)
149
+ document.removeEventListener('pointercancel', listener)
150
+ }, [])
151
+ useEffect(() => clearPressEndListener, [clearPressEndListener])
152
+
153
+ const tipId = `tooltip-${useId().replace(/[^a-zA-Z0-9_-]/g, '')}`
154
+
155
+ const clearTimer = useCallback(() => {
156
+ if (timerRef.current !== null) {
157
+ window.clearTimeout(timerRef.current)
158
+ timerRef.current = null
159
+ }
160
+ }, [])
161
+ useEffect(() => clearTimer, [clearTimer])
162
+
163
+ const scheduleOpen = useCallback(
164
+ (delay: number) => {
165
+ clearTimer()
166
+ if (delay <= 0) {
167
+ setOpen(true)
168
+ return
169
+ }
170
+ timerRef.current = window.setTimeout(() => {
171
+ timerRef.current = null
172
+ setOpen(true)
173
+ }, delay)
174
+ },
175
+ [clearTimer, setOpen],
176
+ )
177
+ const scheduleClose = useCallback(
178
+ (delay: number) => {
179
+ clearTimer()
180
+ if (delay <= 0) {
181
+ setOpen(false)
182
+ return
183
+ }
184
+ timerRef.current = window.setTimeout(() => {
185
+ timerRef.current = null
186
+ setOpen(false)
187
+ }, delay)
188
+ },
189
+ [clearTimer, setOpen],
190
+ )
191
+
192
+ const {
193
+ ref: positionRef,
194
+ style: positionStyle,
195
+ placement,
196
+ positioned,
197
+ } = useAnchoredPosition<HTMLDivElement>({
198
+ anchorRef: triggerRef,
199
+ side,
200
+ align,
201
+ offset,
202
+ enabled: open,
203
+ })
204
+
205
+ useLayer({
206
+ enabled: open,
207
+ kind: 'popover',
208
+ elementRef: tipRef,
209
+ onEscape: () => {
210
+ clearTimer()
211
+ setOpen(false)
212
+ },
213
+ })
214
+
215
+ const tipRefs = useMemo(
216
+ () => composeRefs<HTMLDivElement>(tipRef, positionRef),
217
+ [positionRef],
218
+ )
219
+
220
+ if (!isValidElement<TooltipTriggerProps>(children)) {
221
+ throw new Error('Tooltip expects exactly one element child to use as its trigger.')
222
+ }
223
+ if (content === null || content === undefined || content === false) return children
224
+
225
+ const childProps = children.props
226
+ const describedBy = open
227
+ ? [childProps['aria-describedby'], tipId].filter(Boolean).join(' ')
228
+ : childProps['aria-describedby']
229
+
230
+ const trigger = cloneElement(children, {
231
+ ref: composeRefs<HTMLElement>(childProps.ref, triggerRef),
232
+ 'aria-describedby': describedBy,
233
+ onPointerEnter: (event: ReactPointerEvent<HTMLElement>) => {
234
+ childProps.onPointerEnter?.(event)
235
+ // Touch has no hover; a long-press tooltip is a different pattern.
236
+ if (event.pointerType === 'touch') return
237
+ scheduleOpen(openDelay)
238
+ },
239
+ onPointerLeave: (event: ReactPointerEvent<HTMLElement>) => {
240
+ childProps.onPointerLeave?.(event)
241
+ scheduleClose(closeDelay)
242
+ },
243
+ onPointerDown: (event: ReactPointerEvent<HTMLElement>) => {
244
+ childProps.onPointerDown?.(event)
245
+ pointerDownRef.current = true
246
+ clearPressEndListener()
247
+ const onPressEnd = () => {
248
+ pointerDownRef.current = false
249
+ clearPressEndListener()
250
+ }
251
+ pressEndListenerRef.current = onPressEnd
252
+ document.addEventListener('pointerup', onPressEnd)
253
+ document.addEventListener('pointercancel', onPressEnd)
254
+ // Pressing the trigger is the user acting on it; the label is in the way.
255
+ clearTimer()
256
+ setOpen(false)
257
+ },
258
+ onFocus: (event: ReactFocusEvent<HTMLElement>) => {
259
+ childProps.onFocus?.(event)
260
+ if (pointerDownRef.current) return
261
+ scheduleOpen(0)
262
+ },
263
+ onBlur: (event: ReactFocusEvent<HTMLElement>) => {
264
+ childProps.onBlur?.(event)
265
+ scheduleClose(0)
266
+ },
267
+ })
268
+
269
+ return (
270
+ <>
271
+ {trigger}
272
+ {open && typeof document !== 'undefined'
273
+ ? createPortal(
274
+ <div
275
+ ref={tipRefs}
276
+ id={tipId}
277
+ role="tooltip"
278
+ data-slot="tooltip"
279
+ data-state="open"
280
+ data-side={placement?.side}
281
+ data-align={placement?.align}
282
+ data-positioned={positioned ? 'true' : 'false'}
283
+ className={cn(
284
+ 'pointer-events-none fixed z-[110] max-w-xs text-xs font-medium leading-snug',
285
+ positioned && 'ds-enter-pop',
286
+ className,
287
+ )}
288
+ style={{ ...positionStyle, ...TOOLTIP_SURFACE_STYLE }}
289
+ >
290
+ {content}
291
+ </div>,
292
+ document.body,
293
+ )
294
+ : null}
295
+ </>
296
+ )
297
+ }