@lovett/ui 0.0.11 → 0.2.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 (142) 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 +5574 -57
  4. package/dist/index.js +21650 -11206
  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 +307 -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__/sortable.test.tsx +394 -0
  27. package/src/__tests__/sparkline.test.tsx +368 -0
  28. package/src/__tests__/stat-layer.test.tsx +271 -0
  29. package/src/__tests__/stat-strip.test.tsx +175 -0
  30. package/src/__tests__/status.test.tsx +293 -0
  31. package/src/__tests__/tabs.test.tsx +303 -0
  32. package/src/__tests__/token-shape.test.ts +132 -2
  33. package/src/avatar.tsx +352 -0
  34. package/src/bar-chart.tsx +1214 -0
  35. package/src/board.tsx +658 -0
  36. package/src/chart-frame.tsx +960 -0
  37. package/src/chart-legend.tsx +304 -0
  38. package/src/chart-tooltip.tsx +267 -0
  39. package/src/code-block.tsx +62 -8
  40. package/src/delta-chip.tsx +263 -0
  41. package/src/detail/__tests__/activity-pane.test.tsx +369 -0
  42. package/src/detail/__tests__/detail-chrome.test.tsx +134 -0
  43. package/src/detail/__tests__/detail-surface.test.tsx +529 -0
  44. package/src/detail/__tests__/field-row.test.tsx +357 -0
  45. package/src/detail/activity-pane.tsx +611 -0
  46. package/src/detail/calendar.tsx +355 -0
  47. package/src/detail/detail-divider.tsx +261 -0
  48. package/src/detail/detail-header.tsx +287 -0
  49. package/src/detail/detail-menu.tsx +254 -0
  50. package/src/detail/detail-surface.tsx +1110 -0
  51. package/src/detail/field-list.tsx +196 -0
  52. package/src/detail/field-row.tsx +1131 -0
  53. package/src/detail/index.ts +58 -0
  54. package/src/detail/segmented-choice.tsx +94 -0
  55. package/src/detail/types.ts +129 -0
  56. package/src/display-popover.tsx +487 -0
  57. package/src/display-store.tsx +301 -0
  58. package/src/donut-chart.tsx +988 -0
  59. package/src/dropdown-menu.tsx +290 -19
  60. package/src/filter-core/EXPORTS.md +85 -0
  61. package/src/filter-core/__tests__/columns.test.ts +159 -0
  62. package/src/filter-core/__tests__/faceting.test.ts +193 -0
  63. package/src/filter-core/__tests__/filter-fns.test.ts +519 -0
  64. package/src/filter-core/__tests__/operators.test.ts +235 -0
  65. package/src/filter-core/__tests__/state.test.ts +268 -0
  66. package/src/filter-core/__tests__/url.test.ts +350 -0
  67. package/src/filter-core/columns.ts +134 -0
  68. package/src/filter-core/date-utils.ts +38 -0
  69. package/src/filter-core/examples/task-filter-columns.ts +121 -0
  70. package/src/filter-core/faceting.ts +120 -0
  71. package/src/filter-core/filter-fns.ts +335 -0
  72. package/src/filter-core/index.ts +105 -0
  73. package/src/filter-core/operators.ts +433 -0
  74. package/src/filter-core/state.ts +280 -0
  75. package/src/filter-core/types.ts +247 -0
  76. package/src/filter-core/url.ts +261 -0
  77. package/src/filter-dropdown.tsx +12 -0
  78. package/src/filter-menu.tsx +649 -0
  79. package/src/floating-drawer.tsx +19 -1
  80. package/src/gauge-ring.tsx +435 -0
  81. package/src/hue.ts +52 -0
  82. package/src/index.ts +303 -0
  83. package/src/kbd.tsx +27 -4
  84. package/src/lib/chart.ts +866 -0
  85. package/src/lib/focus.ts +43 -1
  86. package/src/lib/layer-stack.ts +30 -3
  87. package/src/lib/remark-underline.ts +443 -0
  88. package/src/lib/series.ts +169 -0
  89. package/src/line-chart.tsx +1176 -0
  90. package/src/markdown.tsx +26 -7
  91. package/src/modal.tsx +42 -18
  92. package/src/progress-ledger.tsx +304 -0
  93. package/src/ranked-bars.tsx +386 -0
  94. package/src/segmented-pill.tsx +32 -9
  95. package/src/sortable.tsx +520 -1
  96. package/src/sparkline.tsx +416 -0
  97. package/src/stat-card.tsx +376 -0
  98. package/src/stat-strip.tsx +327 -0
  99. package/src/status.tsx +215 -0
  100. package/src/styles.css +1570 -0
  101. package/src/tabs.tsx +206 -25
  102. package/src/task-card.tsx +610 -0
  103. package/src/thread/__tests__/comment-body-hostile.test.tsx +331 -0
  104. package/src/thread/__tests__/comment-tree.test.ts +151 -0
  105. package/src/thread/__tests__/emoji.test.ts +187 -0
  106. package/src/thread/__tests__/fixtures/thread-fixture.ts +252 -0
  107. package/src/thread/__tests__/link-preview-source.test.ts +120 -0
  108. package/src/thread/__tests__/link-preview.test.tsx +600 -0
  109. package/src/thread/__tests__/markdown-format.test.ts +82 -0
  110. package/src/thread/__tests__/markdown-spec.test.ts +469 -0
  111. package/src/thread/__tests__/relative-time.test.ts +71 -0
  112. package/src/thread/__tests__/rich-composer.test.tsx +799 -0
  113. package/src/thread/__tests__/scroll-caret.test.ts +58 -0
  114. package/src/thread/__tests__/suggestion-list.test.tsx +648 -0
  115. package/src/thread/__tests__/thread-scroll-ownership.test.tsx +88 -0
  116. package/src/thread/__tests__/thread.test.tsx +742 -0
  117. package/src/thread/__tests__/use-attachments.test.tsx +679 -0
  118. package/src/thread/actions.tsx +196 -0
  119. package/src/thread/attachments.tsx +1071 -0
  120. package/src/thread/comment-body.tsx +148 -0
  121. package/src/thread/comment-tree.ts +182 -0
  122. package/src/thread/comment.tsx +967 -0
  123. package/src/thread/composer-footer.tsx +125 -0
  124. package/src/thread/composer.tsx +319 -0
  125. package/src/thread/emoji.ts +283 -0
  126. package/src/thread/index.ts +153 -0
  127. package/src/thread/link-preview.tsx +341 -0
  128. package/src/thread/markdown-format.ts +155 -0
  129. package/src/thread/markdown-spec.ts +754 -0
  130. package/src/thread/rail.tsx +372 -0
  131. package/src/thread/reactions.tsx +415 -0
  132. package/src/thread/relative-time.tsx +94 -0
  133. package/src/thread/rich-composer-impl.tsx +1601 -0
  134. package/src/thread/rich-composer.tsx +195 -0
  135. package/src/thread/scroll-caret.ts +37 -0
  136. package/src/thread/suggestion-list.tsx +182 -0
  137. package/src/thread/thread.tsx +718 -0
  138. package/src/thread/types.ts +232 -0
  139. package/src/thread/use-attachments.ts +598 -0
  140. package/src/thread/use-now.ts +73 -0
  141. package/src/thread/use-thread.ts +316 -0
  142. package/src/tokens.css +112 -0
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @lovett/ui — the item-detail surface.
3
+ *
4
+ * `docs/design-briefs/tasks-item-detail-full.md`. A frame with three modes, a
5
+ * slotted header, the ellipsis menu, the Notion-style field rows, and the
6
+ * activity pane. Entity-agnostic by construction: nothing here fetches, and
7
+ * nothing here knows what a task is. The lens supplies the data and the
8
+ * behaviour; this package supplies the surface.
9
+ *
10
+ * `MonthCalendar` is deliberately NOT exported — it is the date row's editor,
11
+ * not a general calendar, and promoting it would freeze a shape that has had
12
+ * exactly one consumer.
13
+ */
14
+
15
+ export { DetailSurface, type DetailSurfaceProps } from './detail-surface'
16
+ export { DetailHeader, type DetailHeaderProps } from './detail-header'
17
+ export {
18
+ DetailMenu,
19
+ type DetailMenuProps,
20
+ type DetailMenuAction,
21
+ type DetailMenuItem,
22
+ } from './detail-menu'
23
+ export { DetailDivider, type DetailDividerProps } from './detail-divider'
24
+
25
+ export { FieldList, useFieldRowVisible, type FieldListProps } from './field-list'
26
+ export {
27
+ FieldRow,
28
+ FieldValueButton,
29
+ type FieldRowProps,
30
+ type FieldValueButtonProps,
31
+ type FieldRowTextProps,
32
+ type FieldRowDateProps,
33
+ type FieldRowSelectProps,
34
+ type FieldRowPeopleProps,
35
+ type FieldRowTagsProps,
36
+ } from './field-row'
37
+
38
+ export { ActivityPane, type ActivityPaneProps, type ActivityTab } from './activity-pane'
39
+
40
+ /** Date values are `YYYY-MM-DD` / `YYYY-MM-DDTHH:mm`, parsed as LOCAL. */
41
+ export { formatDateDisplay, parseDateValue, type DateParts } from './calendar'
42
+
43
+ export {
44
+ DETAIL_COLLAPSE_VALUES,
45
+ DETAIL_MODES,
46
+ DETAIL_MODE_LABELS,
47
+ DETAIL_PANEL_DEFAULT_SIZE,
48
+ FIELD_FILTERS,
49
+ FIELD_FILTER_LABELS,
50
+ detailPaneSide,
51
+ type ActivityEntry,
52
+ type DetailCollapse,
53
+ type DetailMode,
54
+ type DetailPaneSide,
55
+ type FieldFilter,
56
+ type FieldOption,
57
+ type FieldPerson,
58
+ } from './types'
@@ -0,0 +1,94 @@
1
+ /**
2
+ * SegmentedChoice — a one-of-N choice, internal to the detail surface.
3
+ *
4
+ * NOT `SegmentedPill`, and the reason is a11y rather than taste: that
5
+ * primitive renders `role="tablist"` with `role="tab"` segments, which is
6
+ * correct for a view switcher wired to tab panels and wrong for the two
7
+ * choices here — the mode row in the ellipsis menu, and the All / Filled /
8
+ * Empty filter chips. Neither controls a tab panel, and a screen reader that
9
+ * announces "tab 2 of 3" for a filter is being told something untrue.
10
+ * `role="group"` + `aria-pressed` says what these actually are.
11
+ *
12
+ * The selected state is the one CLAUDE.md §2 mandates: an opaque step UP
13
+ * (`--surface-card` on a `--surface-inset` track) with an accent border and
14
+ * `--shadow-sm`. Never an alpha tint on a translucent track — two alpha
15
+ * layers composite to a raised element that renders DARKER than the well it
16
+ * sits in.
17
+ *
18
+ * Not exported from the package barrel; it is a shape, not a contract.
19
+ */
20
+
21
+ import type { ReactNode } from 'react'
22
+ import { cn } from '../lib/utils'
23
+
24
+ export interface SegmentedChoiceItem<Id extends string> {
25
+ readonly id: Id
26
+ /** Visible text, or the accessible name when `icon` carries the meaning. */
27
+ readonly label: string
28
+ readonly icon?: ReactNode | undefined
29
+ /** Render the icon alone and keep `label` as the accessible name. */
30
+ readonly iconOnly?: boolean | undefined
31
+ }
32
+
33
+ export interface SegmentedChoiceProps<Id extends string> {
34
+ items: readonly SegmentedChoiceItem<Id>[]
35
+ value: Id
36
+ onChange: (next: Id) => void
37
+ /** Names the group. Required — a bare row of buttons has no name. */
38
+ ariaLabel: string
39
+ /** Stretch the segments to fill the track. Default false. */
40
+ stretch?: boolean | undefined
41
+ className?: string | undefined
42
+ }
43
+
44
+ export function SegmentedChoice<Id extends string>({
45
+ items,
46
+ value,
47
+ onChange,
48
+ ariaLabel,
49
+ stretch = false,
50
+ className,
51
+ }: SegmentedChoiceProps<Id>) {
52
+ return (
53
+ <div
54
+ role="group"
55
+ aria-label={ariaLabel}
56
+ data-slot="segmented-choice"
57
+ className={cn('inline-flex items-center gap-[var(--space-1)] border p-[var(--space-1)]', className)}
58
+ style={{
59
+ borderRadius: 'var(--radius-md)',
60
+ borderColor: 'rgb(var(--border))',
61
+ background: 'rgb(var(--surface-inset))',
62
+ }}
63
+ >
64
+ {items.map((item) => {
65
+ const active = item.id === value
66
+ return (
67
+ <button
68
+ key={item.id}
69
+ type="button"
70
+ data-choice={item.id}
71
+ aria-pressed={active}
72
+ {...(item.iconOnly ? { 'aria-label': item.label } : {})}
73
+ onClick={() => onChange(item.id)}
74
+ className={cn(
75
+ 'inline-flex h-[26px] items-center justify-center gap-[var(--space-1)]',
76
+ 'px-[var(--space-2)] text-[12px] font-semibold',
77
+ stretch && 'flex-1',
78
+ 'transition-[background-color,color,border-color,box-shadow] duration-[var(--dur-fast)] ease-[var(--ease-out)]',
79
+ 'motion-reduce:transition-none',
80
+ 'focus-visible:outline-none focus-visible:[box-shadow:var(--ring-focus)]',
81
+ active
82
+ ? 'border border-[rgb(var(--accent))] bg-[rgb(var(--surface-card))] text-[rgb(var(--accent-ink))] [box-shadow:var(--shadow-sm)]'
83
+ : 'border border-transparent text-[rgb(var(--text-tertiary))] hover:text-[rgb(var(--foreground))]',
84
+ )}
85
+ style={{ borderRadius: 'var(--radius-sm)' }}
86
+ >
87
+ {item.icon}
88
+ {item.iconOnly ? null : item.label}
89
+ </button>
90
+ )
91
+ })}
92
+ </div>
93
+ )
94
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Item detail — the shared vocabulary.
3
+ *
4
+ * `docs/design-briefs/tasks-item-detail-full.md` §1. Three modes, and the
5
+ * orientation of the activity pane FOLLOWS the mode rather than being a second
6
+ * setting that can disagree with it: side-by-side in the two centred modes,
7
+ * stacked in the side panel. `detailPaneSide()` is the only place that mapping
8
+ * exists, so a host cannot ship a `full` detail with a bottom pane.
9
+ *
10
+ * Nothing in this module renders. It is the type surface the lens imports.
11
+ */
12
+
13
+ import type { ReactNode } from 'react'
14
+
15
+ /** §1 — `full` is the default; the host persists the choice. */
16
+ export type DetailMode = 'full' | 'modal' | 'panel'
17
+
18
+ /** Menu order: the three mode icons read left to right in this order. */
19
+ export const DETAIL_MODES: readonly DetailMode[] = ['full', 'modal', 'panel']
20
+
21
+ /**
22
+ * Sentence case, and each names what happens rather than what it is
23
+ * (interface standards §7). These are accessible names for icon-only
24
+ * controls, so they have to survive being read on their own.
25
+ */
26
+ export const DETAIL_MODE_LABELS: Readonly<Record<DetailMode, string>> = {
27
+ full: 'Open full screen',
28
+ modal: 'Open centred',
29
+ panel: 'Open in side panel',
30
+ }
31
+
32
+ /**
33
+ * `panel` mode's default width, px — what the mode's `max-inline-size` was
34
+ * before its leading edge became draggable, so a host with nothing stored
35
+ * opens exactly where it always did, and `Home` / double-click on the edge
36
+ * come back to it.
37
+ *
38
+ * Exported because the HOST persists the width. A store that hard-codes its
39
+ * own copy of the default is one release away from disagreeing with the
40
+ * surface it is driving.
41
+ */
42
+ export const DETAIL_PANEL_DEFAULT_SIZE = 560
43
+
44
+ /** Where the activity pane sits relative to the fields. */
45
+ export type DetailPaneSide = 'side' | 'bottom'
46
+
47
+ /**
48
+ * §1 — "Orientation follows the mode." `full` and `modal` put activity to the
49
+ * inline end with a vertical divider; `panel` puts it below with a horizontal
50
+ * one.
51
+ */
52
+ export function detailPaneSide(mode: DetailMode): DetailPaneSide {
53
+ return mode === 'panel' ? 'bottom' : 'side'
54
+ }
55
+
56
+ /**
57
+ * Which pane of the split is collapsed, if either.
58
+ *
59
+ * A THIRD state rather than two booleans: "both collapsed" is not a layout,
60
+ * and a pair of flags is one bad write away from expressing it. `fields` and
61
+ * `aside` name the pane that is HIDDEN, not the one that is showing, because
62
+ * that is what the restore affordance has to say out loud.
63
+ *
64
+ * It is per-ORIENTATION for the same reason the pane's size is: a host that
65
+ * collapsed the activity pane beside the fields has said nothing about
66
+ * whether it wants it collapsed below them.
67
+ */
68
+ export type DetailCollapse = 'none' | 'fields' | 'aside'
69
+
70
+ /** For a host persisting the choice — the allow-list its store validates against. */
71
+ export const DETAIL_COLLAPSE_VALUES: readonly DetailCollapse[] = [
72
+ 'none',
73
+ 'fields',
74
+ 'aside',
75
+ ]
76
+
77
+ /** The chips above the field list. */
78
+ export type FieldFilter = 'all' | 'filled' | 'empty'
79
+
80
+ export const FIELD_FILTERS: readonly FieldFilter[] = ['all', 'filled', 'empty']
81
+
82
+ export const FIELD_FILTER_LABELS: Readonly<Record<FieldFilter, string>> = {
83
+ all: 'All',
84
+ filled: 'Filled',
85
+ empty: 'Empty',
86
+ }
87
+
88
+ /** One choice in a `FieldRow.Select`. */
89
+ export interface FieldOption<T extends string = string> {
90
+ readonly value: T
91
+ readonly label: string
92
+ /** A Lucide icon or a colour dot. Rendered before the label in both states. */
93
+ readonly icon?: ReactNode | undefined
94
+ readonly disabled?: boolean | undefined
95
+ }
96
+
97
+ /** One person in a `FieldRow.People`. Shaped like the thread's author (D5). */
98
+ export interface FieldPerson {
99
+ readonly id: string
100
+ /** Nullable by standing policy — never fabricate a display name. */
101
+ readonly name: string | null
102
+ readonly avatarUrl?: string | null | undefined
103
+ /** Identity colour as DATA. Omitted falls back to Avatar's hash. */
104
+ readonly tint?: string | null | undefined
105
+ /** A secondary line in the picker — an email, a role. */
106
+ readonly hint?: string | undefined
107
+ }
108
+
109
+ /**
110
+ * One row of the item's history (§5).
111
+ *
112
+ * The pane never derives these from the item on the client — §6 is explicit
113
+ * that the feed is written server-side in the same transaction as the change,
114
+ * "which is how the reference's activity tab stays truthful". This type is the
115
+ * shape that feed arrives in, and `summary` exists for the entries the
116
+ * standard `‹field› from ‹a› to ‹b›` sentence cannot say.
117
+ */
118
+ export interface ActivityEntry {
119
+ readonly id: string
120
+ /** Epoch milliseconds, like the thread (D16). */
121
+ readonly at: number
122
+ readonly actor?: FieldPerson | null | undefined
123
+ /** User-facing field name — "Status", never `column_id` (§7 copy). */
124
+ readonly field?: string | undefined
125
+ readonly from?: string | null | undefined
126
+ readonly to?: string | null | undefined
127
+ /** Overrides the derived sentence entirely. */
128
+ readonly summary?: ReactNode | undefined
129
+ }