@lovett/ui 0.0.11 → 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 (141) 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 +5402 -71
  4. package/dist/index.js +21992 -11618
  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 +1570 -0
  9. package/dist/tokens.css +112 -0
  10. package/package.json +8 -1
  11. package/src/__tests__/avatar.test.tsx +272 -0
  12. package/src/__tests__/bar-chart.test.tsx +809 -0
  13. package/src/__tests__/board.test.tsx +420 -0
  14. package/src/__tests__/chart-math.test.ts +922 -0
  15. package/src/__tests__/chart-series.test.ts +339 -0
  16. package/src/__tests__/code-block.test.tsx +134 -0
  17. package/src/__tests__/display-popover.test.tsx +195 -0
  18. package/src/__tests__/display-store.test.tsx +208 -0
  19. package/src/__tests__/donut-chart.test.tsx +397 -0
  20. package/src/__tests__/dropdown-menu.test.tsx +156 -2
  21. package/src/__tests__/filter-menu.test.tsx +175 -0
  22. package/src/__tests__/gauge-ring.test.tsx +233 -0
  23. package/src/__tests__/line-chart.test.tsx +612 -0
  24. package/src/__tests__/ranked-bars.test.tsx +343 -0
  25. package/src/__tests__/remark-underline.test.ts +194 -0
  26. package/src/__tests__/sparkline.test.tsx +368 -0
  27. package/src/__tests__/stat-layer.test.tsx +271 -0
  28. package/src/__tests__/stat-strip.test.tsx +175 -0
  29. package/src/__tests__/status.test.tsx +293 -0
  30. package/src/__tests__/tabs.test.tsx +303 -0
  31. package/src/__tests__/token-shape.test.ts +132 -2
  32. package/src/avatar.tsx +352 -0
  33. package/src/bar-chart.tsx +1214 -0
  34. package/src/board.tsx +658 -0
  35. package/src/chart-frame.tsx +960 -0
  36. package/src/chart-legend.tsx +304 -0
  37. package/src/chart-tooltip.tsx +267 -0
  38. package/src/code-block.tsx +62 -8
  39. package/src/delta-chip.tsx +263 -0
  40. package/src/detail/__tests__/activity-pane.test.tsx +186 -0
  41. package/src/detail/__tests__/detail-chrome.test.tsx +134 -0
  42. package/src/detail/__tests__/detail-surface.test.tsx +529 -0
  43. package/src/detail/__tests__/field-row.test.tsx +357 -0
  44. package/src/detail/activity-pane.tsx +507 -0
  45. package/src/detail/calendar.tsx +355 -0
  46. package/src/detail/detail-divider.tsx +261 -0
  47. package/src/detail/detail-header.tsx +287 -0
  48. package/src/detail/detail-menu.tsx +254 -0
  49. package/src/detail/detail-surface.tsx +1110 -0
  50. package/src/detail/field-list.tsx +196 -0
  51. package/src/detail/field-row.tsx +1131 -0
  52. package/src/detail/index.ts +58 -0
  53. package/src/detail/segmented-choice.tsx +94 -0
  54. package/src/detail/types.ts +129 -0
  55. package/src/display-popover.tsx +487 -0
  56. package/src/display-store.tsx +242 -0
  57. package/src/donut-chart.tsx +988 -0
  58. package/src/dropdown-menu.tsx +290 -19
  59. package/src/filter-core/EXPORTS.md +85 -0
  60. package/src/filter-core/__tests__/columns.test.ts +159 -0
  61. package/src/filter-core/__tests__/faceting.test.ts +193 -0
  62. package/src/filter-core/__tests__/filter-fns.test.ts +519 -0
  63. package/src/filter-core/__tests__/operators.test.ts +235 -0
  64. package/src/filter-core/__tests__/state.test.ts +268 -0
  65. package/src/filter-core/__tests__/url.test.ts +350 -0
  66. package/src/filter-core/columns.ts +134 -0
  67. package/src/filter-core/date-utils.ts +38 -0
  68. package/src/filter-core/examples/task-filter-columns.ts +121 -0
  69. package/src/filter-core/faceting.ts +120 -0
  70. package/src/filter-core/filter-fns.ts +335 -0
  71. package/src/filter-core/index.ts +105 -0
  72. package/src/filter-core/operators.ts +433 -0
  73. package/src/filter-core/state.ts +280 -0
  74. package/src/filter-core/types.ts +247 -0
  75. package/src/filter-core/url.ts +261 -0
  76. package/src/filter-dropdown.tsx +12 -0
  77. package/src/filter-menu.tsx +649 -0
  78. package/src/floating-drawer.tsx +19 -1
  79. package/src/gauge-ring.tsx +435 -0
  80. package/src/hue.ts +52 -0
  81. package/src/index.ts +296 -0
  82. package/src/kbd.tsx +27 -4
  83. package/src/lib/chart.ts +866 -0
  84. package/src/lib/focus.ts +43 -1
  85. package/src/lib/layer-stack.ts +30 -3
  86. package/src/lib/remark-underline.ts +443 -0
  87. package/src/lib/series.ts +169 -0
  88. package/src/line-chart.tsx +1176 -0
  89. package/src/markdown.tsx +26 -7
  90. package/src/modal.tsx +42 -18
  91. package/src/progress-ledger.tsx +304 -0
  92. package/src/ranked-bars.tsx +386 -0
  93. package/src/segmented-pill.tsx +32 -9
  94. package/src/sortable.tsx +315 -1
  95. package/src/sparkline.tsx +416 -0
  96. package/src/stat-card.tsx +376 -0
  97. package/src/stat-strip.tsx +327 -0
  98. package/src/status.tsx +215 -0
  99. package/src/styles.css +1570 -0
  100. package/src/tabs.tsx +206 -25
  101. package/src/task-card.tsx +610 -0
  102. package/src/thread/__tests__/comment-body-hostile.test.tsx +331 -0
  103. package/src/thread/__tests__/comment-tree.test.ts +151 -0
  104. package/src/thread/__tests__/emoji.test.ts +187 -0
  105. package/src/thread/__tests__/fixtures/thread-fixture.ts +235 -0
  106. package/src/thread/__tests__/link-preview-source.test.ts +120 -0
  107. package/src/thread/__tests__/link-preview.test.tsx +600 -0
  108. package/src/thread/__tests__/markdown-format.test.ts +82 -0
  109. package/src/thread/__tests__/markdown-spec.test.ts +469 -0
  110. package/src/thread/__tests__/relative-time.test.ts +71 -0
  111. package/src/thread/__tests__/rich-composer.test.tsx +799 -0
  112. package/src/thread/__tests__/scroll-caret.test.ts +58 -0
  113. package/src/thread/__tests__/suggestion-list.test.tsx +648 -0
  114. package/src/thread/__tests__/thread-scroll-ownership.test.tsx +88 -0
  115. package/src/thread/__tests__/thread.test.tsx +662 -0
  116. package/src/thread/__tests__/use-attachments.test.tsx +679 -0
  117. package/src/thread/actions.tsx +196 -0
  118. package/src/thread/attachments.tsx +1071 -0
  119. package/src/thread/comment-body.tsx +148 -0
  120. package/src/thread/comment-tree.ts +182 -0
  121. package/src/thread/comment.tsx +915 -0
  122. package/src/thread/composer-footer.tsx +125 -0
  123. package/src/thread/composer.tsx +319 -0
  124. package/src/thread/emoji.ts +283 -0
  125. package/src/thread/index.ts +153 -0
  126. package/src/thread/link-preview.tsx +341 -0
  127. package/src/thread/markdown-format.ts +155 -0
  128. package/src/thread/markdown-spec.ts +754 -0
  129. package/src/thread/rail.tsx +372 -0
  130. package/src/thread/reactions.tsx +415 -0
  131. package/src/thread/relative-time.tsx +94 -0
  132. package/src/thread/rich-composer-impl.tsx +1601 -0
  133. package/src/thread/rich-composer.tsx +195 -0
  134. package/src/thread/scroll-caret.ts +37 -0
  135. package/src/thread/suggestion-list.tsx +182 -0
  136. package/src/thread/thread.tsx +718 -0
  137. package/src/thread/types.ts +218 -0
  138. package/src/thread/use-attachments.ts +598 -0
  139. package/src/thread/use-now.ts +73 -0
  140. package/src/thread/use-thread.ts +316 -0
  141. package/src/tokens.css +112 -0
package/src/sortable.tsx CHANGED
@@ -9,15 +9,22 @@
9
9
  * - <DragHandle> is a Lucide-grip handle pre-wired to the listeners
10
10
  */
11
11
 
12
+ import { useRef, useState } from 'react'
12
13
  import type { CSSProperties, HTMLAttributes, ReactNode } from 'react'
13
14
  import {
14
15
  DndContext,
16
+ DragOverlay,
17
+ MeasuringStrategy,
15
18
  closestCenter,
19
+ closestCorners,
20
+ useDroppable,
16
21
  KeyboardSensor,
17
22
  PointerSensor,
18
23
  useSensor,
19
24
  useSensors,
20
25
  type DragEndEvent,
26
+ type DragOverEvent,
27
+ type DragStartEvent,
21
28
  } from '@dnd-kit/core'
22
29
  import {
23
30
  arrayMove,
@@ -86,13 +93,51 @@ export function SortableItem({
86
93
  }: {
87
94
  id: string
88
95
  children: (props: {
96
+ /** attributes + listeners merged — the single-node case, where the whole
97
+ * node IS the handle (a `<DragHandle>`, a settings row's grip). */
89
98
  dragHandleProps: HTMLAttributes<HTMLElement>
99
+ /**
100
+ * The ACTIVATORS — `onPointerDown` from the PointerSensor, `onKeyDown` from
101
+ * the KeyboardSensor. Whatever element receives these is what starts a drag.
102
+ *
103
+ * Split from `dragAttributes` so a card can be a drop target and a
104
+ * transform target (`setNodeRef` + `style`) WITHOUT being a drag handle.
105
+ * Spread these onto the card root and a pointer-down anywhere on it starts
106
+ * a drag, which is the bug this split exists to prevent: the card's own
107
+ * buttons, links and text selection all stop working as themselves.
108
+ */
109
+ dragListeners: HTMLAttributes<HTMLElement>
110
+ /**
111
+ * `role="button"`, `tabIndex`, `aria-roledescription`, `aria-describedby`.
112
+ *
113
+ * These belong on the SAME element as `dragListeners` — they describe the
114
+ * handle to assistive tech, and dnd-kit's instruction text is reached
115
+ * through the `aria-describedby` here. Putting them on a card that holds
116
+ * its own buttons makes it an interactive element wrapping interactive
117
+ * elements, and `role="button"`'s presentational children erase the
118
+ * buttons inside it.
119
+ */
120
+ dragAttributes: HTMLAttributes<HTMLElement>
121
+ /** The draggable NODE — measured, transformed, and used as the drop rect. */
90
122
  setNodeRef: (node: HTMLElement | null) => void
123
+ /**
124
+ * The HANDLE node. dnd-kit restores focus here when a keyboard drag ends;
125
+ * without it focus lands on the card and the next arrow key does nothing.
126
+ */
127
+ setActivatorNodeRef: (node: HTMLElement | null) => void
91
128
  style: CSSProperties
92
129
  isDragging: boolean
93
130
  }) => ReactNode
94
131
  }) {
95
- const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
132
+ const {
133
+ attributes,
134
+ listeners,
135
+ setNodeRef,
136
+ setActivatorNodeRef,
137
+ transform,
138
+ transition,
139
+ isDragging,
140
+ } = useSortable({
96
141
  id,
97
142
  })
98
143
 
@@ -108,7 +153,14 @@ export function SortableItem({
108
153
  <>
109
154
  {children({
110
155
  dragHandleProps: { ...attributes, ...listeners } as HTMLAttributes<HTMLElement>,
156
+ dragListeners: { ...listeners } as HTMLAttributes<HTMLElement>,
157
+ // Attributes ONLY. This was `{ ...attributes, ...listeners }` — byte
158
+ // identical to `dragHandleProps` — so the "split" was not a split, and
159
+ // every consumer that took `dragAttributes` for the aria wiring got the
160
+ // pointer activators with it.
161
+ dragAttributes: { ...attributes } as HTMLAttributes<HTMLElement>,
111
162
  setNodeRef,
163
+ setActivatorNodeRef,
112
164
  style,
113
165
  isDragging,
114
166
  })}
@@ -135,3 +187,265 @@ export function DragHandle({
135
187
  </button>
136
188
  )
137
189
  }
190
+
191
+ /* ============================================================================
192
+ * Multi-container drag — ADR-146 slice A.
193
+ *
194
+ * <SortableList> above is single-container: one list, reorder within it. A
195
+ * kanban needs a card to LEAVE its column, which dnd-kit models as a droppable
196
+ * per container plus a DragOverlay that follows the pointer while the source
197
+ * item stays in place.
198
+ *
199
+ * Kept from the single-container wrapper on purpose:
200
+ * - the KeyboardSensor, so a card can cross columns without a pointer
201
+ * (interface-standards §6 — non-negotiable, not polish)
202
+ * - the 4px pointer activation constraint, so a click on a card still reads
203
+ * as a click and not a 1px drag
204
+ *
205
+ * Added:
206
+ * - `closestCorners` rather than `closestCenter`. Center-based collision
207
+ * mis-resolves when the dragged card is taller than the row it is over,
208
+ * which is the normal case on a board with variable-height cards.
209
+ * - `MeasuringStrategy.Always`, because a container's height changes as
210
+ * cards enter and leave it and a cached rect drops the item in the wrong
211
+ * lane.
212
+ * - screen-reader announcements naming the destination container, not its id.
213
+ * ========================================================================== */
214
+
215
+ /** containerId -> the ids it holds, in order. */
216
+ export type SortableContainers = Readonly<Record<string, readonly string[]>>
217
+
218
+ export interface MultiSortableMove {
219
+ itemId: string
220
+ fromContainerId: string
221
+ toContainerId: string
222
+ /** Destination index within `toContainerId`, after the move. */
223
+ toIndex: number
224
+ }
225
+
226
+ export interface MultiSortableListProps {
227
+ containers: SortableContainers
228
+ /** Container ids in board order. Also the set treated as drop targets. */
229
+ containerOrder: readonly string[]
230
+ /**
231
+ * Commit the move. Called synchronously from the drop handler, so a
232
+ * `setState` here batches with the primitive's own reset and the board does
233
+ * not flash the pre-drag arrangement for a frame.
234
+ */
235
+ onMove: (move: MultiSortableMove, next: SortableContainers) => void
236
+ /**
237
+ * What follows the pointer. Without this the card appears to vanish.
238
+ *
239
+ * Receives the item's container in the LIVE arrangement, which during a
240
+ * cross-column drag is the DESTINATION, not the origin. Without it the
241
+ * overlay renders in the origin's colour while the placeholder underneath
242
+ * already shows the destination's — one card, on screen twice, in two
243
+ * different hues.
244
+ */
245
+ renderOverlay?: ((itemId: string, containerId: string | null) => ReactNode) | undefined
246
+ /** Human name for an id, for both the overlay's a11y text and announcements. */
247
+ labelForItem?: ((itemId: string) => string) | undefined
248
+ labelForContainer?: ((containerId: string) => string) | undefined
249
+ /**
250
+ * Receives the live arrangement, which differs from `containers` mid-drag,
251
+ * and the container the drag is currently resolved INTO.
252
+ *
253
+ * The second argument exists because `useDroppable().isOver` cannot answer
254
+ * it on a board: `closestCorners` resolves `over` to a sortable ITEM
255
+ * whenever a column holds one, so the column's own droppable never wins and
256
+ * its `isOver` measured `false` on every column during real pointer AND
257
+ * keyboard drags. The container has to be derived from the resolved `over`
258
+ * id — the same `containerOf` the announcements already use.
259
+ */
260
+ children: (
261
+ arrangement: SortableContainers,
262
+ overContainerId: string | null,
263
+ ) => ReactNode
264
+ }
265
+
266
+ function moveBetween(
267
+ arrangement: SortableContainers,
268
+ itemId: string,
269
+ from: string,
270
+ to: string,
271
+ index: number,
272
+ ): SortableContainers {
273
+ const next: Record<string, readonly string[]> = { ...arrangement }
274
+ const fromItems = (next[from] ?? []).filter((id) => id !== itemId)
275
+ const toItems = from === to ? [...fromItems] : [...(next[to] ?? [])]
276
+ const clamped = Math.max(0, Math.min(index, toItems.length))
277
+ toItems.splice(clamped, 0, itemId)
278
+ next[from] = fromItems
279
+ next[to] = toItems
280
+ return next
281
+ }
282
+
283
+ export function MultiSortableList({
284
+ containers,
285
+ containerOrder,
286
+ onMove,
287
+ renderOverlay,
288
+ labelForItem,
289
+ labelForContainer,
290
+ children,
291
+ }: MultiSortableListProps) {
292
+ const sensors = useSensors(
293
+ useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
294
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
295
+ )
296
+
297
+ const [activeId, setActiveId] = useState<string | null>(null)
298
+ const [overContainerId, setOverContainerId] = useState<string | null>(null)
299
+ const [preview, setPreview] = useState<SortableContainers | null>(null)
300
+ // The arrangement the drag started from, so a cancel restores it exactly.
301
+ const originRef = useRef<SortableContainers | null>(null)
302
+
303
+ const arrangement = preview ?? containers
304
+
305
+ const containerOf = (id: string): string | null => {
306
+ if (containerOrder.includes(id)) return id
307
+ for (const containerId of containerOrder) {
308
+ if ((arrangement[containerId] ?? []).includes(id)) return containerId
309
+ }
310
+ return null
311
+ }
312
+
313
+ const nameItem = (id: string) => labelForItem?.(id) ?? id
314
+ const nameContainer = (id: string) => labelForContainer?.(id) ?? id
315
+
316
+ const handleDragStart = (event: DragStartEvent) => {
317
+ const id = String(event.active.id)
318
+ setActiveId(id)
319
+ originRef.current = containers
320
+ setPreview(containers)
321
+ // The card starts over its own column, so the drop target is lit from the
322
+ // first frame rather than only once the pointer crosses a boundary.
323
+ setOverContainerId(containerOf(id))
324
+ }
325
+
326
+ const handleDragOver = (event: DragOverEvent) => {
327
+ const { active, over } = event
328
+ if (!over) {
329
+ setOverContainerId(null)
330
+ return
331
+ }
332
+ const itemId = String(active.id)
333
+ const overId = String(over.id)
334
+ const from = containerOf(itemId)
335
+ const to = containerOf(overId)
336
+ // Set BEFORE the same-container early return: a reorder inside one column
337
+ // still has a drop target, and it is that column.
338
+ setOverContainerId(to)
339
+ if (!from || !to || from === to) return
340
+
341
+ const overItems = arrangement[to] ?? []
342
+ const overIndex = overItems.indexOf(overId)
343
+ const index = overIndex === -1 ? overItems.length : overIndex
344
+ setPreview(moveBetween(arrangement, itemId, from, to, index))
345
+ }
346
+
347
+ const handleDragEnd = (event: DragEndEvent) => {
348
+ const { active, over } = event
349
+ const itemId = String(active.id)
350
+ const origin = originRef.current ?? containers
351
+ const from = Object.keys(origin).find((key) =>
352
+ (origin[key] ?? []).includes(itemId),
353
+ )
354
+ setActiveId(null)
355
+ setOverContainerId(null)
356
+ originRef.current = null
357
+
358
+ if (!over || !from) {
359
+ setPreview(null)
360
+ return
361
+ }
362
+
363
+ const overId = String(over.id)
364
+ const to = containerOf(overId) ?? from
365
+ const overItems = (arrangement[to] ?? []).filter((id) => id !== itemId)
366
+ const overIndex = overItems.indexOf(overId)
367
+ const index = overIndex === -1 ? overItems.length : overIndex
368
+ const next = moveBetween(origin, itemId, from, to, index)
369
+
370
+ setPreview(null)
371
+ if (from === to && (origin[from] ?? []).indexOf(itemId) === index) return
372
+ onMove({ itemId, fromContainerId: from, toContainerId: to, toIndex: index }, next)
373
+ }
374
+
375
+ const handleDragCancel = () => {
376
+ setActiveId(null)
377
+ setOverContainerId(null)
378
+ originRef.current = null
379
+ setPreview(null)
380
+ }
381
+
382
+ return (
383
+ <DndContext
384
+ sensors={sensors}
385
+ collisionDetection={closestCorners}
386
+ measuring={{ droppable: { strategy: MeasuringStrategy.Always } }}
387
+ onDragStart={handleDragStart}
388
+ onDragOver={handleDragOver}
389
+ onDragEnd={handleDragEnd}
390
+ onDragCancel={handleDragCancel}
391
+ accessibility={{
392
+ announcements: {
393
+ onDragStart: ({ active }) => `Picked up ${nameItem(String(active.id))}.`,
394
+ onDragOver: ({ active, over }) =>
395
+ over
396
+ ? `${nameItem(String(active.id))} is over ${nameContainer(
397
+ containerOf(String(over.id)) ?? String(over.id),
398
+ )}.`
399
+ : `${nameItem(String(active.id))} is no longer over a column.`,
400
+ onDragEnd: ({ active, over }) =>
401
+ over
402
+ ? `Moved ${nameItem(String(active.id))} to ${nameContainer(
403
+ containerOf(String(over.id)) ?? String(over.id),
404
+ )}.`
405
+ : `${nameItem(String(active.id))} was dropped outside a column and returned.`,
406
+ onDragCancel: ({ active }) =>
407
+ `Cancelled. ${nameItem(String(active.id))} returned to where it started.`,
408
+ },
409
+ }}
410
+ >
411
+ {children(arrangement, overContainerId)}
412
+ <DragOverlay>
413
+ {activeId && renderOverlay ? renderOverlay(activeId, containerOf(activeId)) : null}
414
+ </DragOverlay>
415
+ </DndContext>
416
+ )
417
+ }
418
+
419
+ /**
420
+ * One droppable container inside a <MultiSortableList>.
421
+ *
422
+ * Renders through a prop so the consumer owns the container's chrome — the
423
+ * board column's drag-over state steps its BORDER rather than tinting with the
424
+ * accent (design brief §4), and only the consumer knows what its border is.
425
+ *
426
+ * `isOver` here is `useDroppable`'s raw answer and is NOT the board's drag-over
427
+ * signal: with `closestCorners`, `over` resolves to a sortable item whenever
428
+ * the column holds one, so this reads `false` for the column the card is
429
+ * actually over. It is kept because the droppable registration is what makes an
430
+ * EMPTY column a drop target at all. The state a consumer should paint comes
431
+ * from `MultiSortableList`'s `overContainerId`.
432
+ */
433
+ export function SortableDropZone({
434
+ id,
435
+ items,
436
+ children,
437
+ }: {
438
+ id: string
439
+ items: readonly string[]
440
+ children: (props: {
441
+ setNodeRef: (node: HTMLElement | null) => void
442
+ isOver: boolean
443
+ }) => ReactNode
444
+ }) {
445
+ const { setNodeRef, isOver } = useDroppable({ id })
446
+ return (
447
+ <SortableContext items={[...items]} strategy={verticalListSortingStrategy}>
448
+ {children({ setNodeRef, isOver })}
449
+ </SortableContext>
450
+ )
451
+ }