@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,242 @@
1
+ /**
2
+ * createDisplayStore — one factory for "how should this view look" settings.
3
+ *
4
+ * `docs/design-briefs/tasks-board-header-primitives.md` §4 records two
5
+ * separate defects in the source, and this file exists to make both
6
+ * impossible:
7
+ *
8
+ * 1. THE COPY-PASTE. Their store shape (`Record<Key, boolean>` + `toggle` +
9
+ * `reset` + persist) is duplicated verbatim across FIVE stores. This is
10
+ * one factory called five times.
11
+ *
12
+ * 2. THE SINGLETON — the trap worth inverting. Their cards subscribe to a
13
+ * module-level zustand store directly (`issue-line.tsx:19`,
14
+ * `issue-grid.tsx:81`), so two boards on one page cannot hold different
15
+ * display settings, and a card cannot be rendered in a preview with
16
+ * settings of its own. Here the settings travel through REACT CONTEXT:
17
+ * the state lives in a `<Provider>` instance, so two providers are two
18
+ * independent panels, and a component that wants the settings takes them
19
+ * from the nearest one (or, for a preview, as a plain prop).
20
+ *
21
+ * PERSISTENCE IS PER SCOPE. `scope` is whatever partition the host needs —
22
+ * for a lens that is the brand id, because ADR-116 D10 requires brand A's
23
+ * preferences never to surface on brand B. The key is
24
+ * `workspace:<name>:<scope>:display`, matching CLAUDE.md §3's namespacing
25
+ * rule. Changing `scope` re-reads: the provider does not remount, so an
26
+ * in-flight draft elsewhere in the tree survives a brand switch.
27
+ *
28
+ * READING IS DEFENSIVE. `localStorage` is untrusted input — a hand-edited
29
+ * value, a key left behind by an older build, a private window that throws on
30
+ * access. Every persisted key is checked against the default's TYPE and, when
31
+ * the caller declares one, against an allow-list of values. Anything that
32
+ * fails falls back to the default rather than reaching a component as a
33
+ * setting nothing knows how to render.
34
+ */
35
+
36
+ import {
37
+ createContext,
38
+ useCallback,
39
+ useContext,
40
+ useEffect,
41
+ useMemo,
42
+ useRef,
43
+ useState,
44
+ type ReactNode,
45
+ } from 'react'
46
+
47
+ /**
48
+ * What a display setting may be. Enough for a select, a toggle — and a
49
+ * MEASUREMENT.
50
+ *
51
+ * `number` joined the union for a size the operator drags rather than picks
52
+ * (the item detail's panel width). It is validated harder than the other two,
53
+ * because `NaN` and `Infinity` are both `typeof 'number'` and both survive a
54
+ * `JSON.parse` round trip through `null`-free hand editing: a non-finite value
55
+ * reaching a layout is a pane with no width, so it is rejected on read like
56
+ * any other value that does not hold up.
57
+ */
58
+ export type DisplayValue = string | boolean | number
59
+
60
+ /**
61
+ * The settings object. A TYPE alias, not an interface, on purpose: an
62
+ * interface has no implicit index signature and would not satisfy this
63
+ * constraint at the call site.
64
+ */
65
+ export type DisplaySettings = Readonly<Record<string, DisplayValue>>
66
+
67
+ /** Allowed values per key, for the enumerated ones. Unlisted keys are open. */
68
+ export type DisplayAllowed<T extends DisplaySettings> = {
69
+ readonly [K in keyof T]?: readonly T[K][]
70
+ }
71
+
72
+ /**
73
+ * A per-key rewrite applied to the PERSISTED value before it is validated.
74
+ *
75
+ * The narrow case this exists for: a setting whose vocabulary changes, where
76
+ * the old value has an exact successor. Without it the allow-list rejects the
77
+ * stored value and the operator's preference is silently replaced by the
78
+ * default — which is a preference thrown away, not a preference migrated. The
79
+ * tasks lens widened `detailPresentation` from two values to three and its
80
+ * `drawer` became `panel`; nobody who had chosen the side panel should be put
81
+ * back on the default because of it.
82
+ *
83
+ * It runs on UNTRUSTED input, so it takes `unknown` and may return anything:
84
+ * whatever it produces still has to survive the type check and the allow-list
85
+ * afterwards. A migration cannot widen what is permitted, only translate into
86
+ * it.
87
+ */
88
+ export type DisplayMigrations<T extends DisplaySettings> = {
89
+ readonly [K in keyof T]?: (raw: unknown) => unknown
90
+ }
91
+
92
+ export interface DisplayController<T extends DisplaySettings> {
93
+ readonly settings: T
94
+ readonly set: <K extends keyof T>(key: K, value: T[K]) => void
95
+ readonly reset: () => void
96
+ /** True when nothing has been changed from the defaults. */
97
+ readonly isDefault: boolean
98
+ }
99
+
100
+ export interface DisplayProviderProps {
101
+ /**
102
+ * The persistence partition — a brand id in a lens. Omit for a panel that
103
+ * should not persist at all (a preview, a test).
104
+ */
105
+ scope?: string
106
+ children: ReactNode
107
+ }
108
+
109
+ export interface DisplayStore<T extends DisplaySettings> {
110
+ /** Holds the state. Two providers are two independent panels. */
111
+ readonly Provider: (props: DisplayProviderProps) => ReactNode
112
+ /** Reads the nearest provider. Throws outside one — a silent default here
113
+ * would render a board with settings the panel is not editing. */
114
+ readonly useDisplay: () => DisplayController<T>
115
+ readonly defaults: T
116
+ /** Exposed so a host can name the key it is clearing. */
117
+ readonly storageKey: (scope: string) => string
118
+ }
119
+
120
+ function isDisplayValue(value: unknown): value is DisplayValue {
121
+ if (typeof value === 'string' || typeof value === 'boolean') return true
122
+ return typeof value === 'number' && Number.isFinite(value)
123
+ }
124
+
125
+ export function createDisplayStore<T extends DisplaySettings>(
126
+ name: string,
127
+ defaults: T,
128
+ allowed?: DisplayAllowed<T>,
129
+ migrations?: DisplayMigrations<T>,
130
+ ): DisplayStore<T> {
131
+ const storageKey = (scope: string) => `workspace:${name}:${scope}:display`
132
+
133
+ /** One key's persisted value, or `null` when it does not hold up. */
134
+ function validate<K extends keyof T & string>(key: K, input: unknown): T[K] | null {
135
+ const fallback = defaults[key]
136
+ // The migration runs FIRST and on the raw value, then everything below
137
+ // judges its output exactly as it would judge a stored one.
138
+ const migrate = migrations?.[key]
139
+ const raw = migrate === undefined ? input : migrate(input)
140
+ if (!isDisplayValue(raw)) return null
141
+ if (typeof raw !== typeof fallback) return null
142
+ const permitted = allowed?.[key]
143
+ if (permitted && !permitted.includes(raw as T[K])) return null
144
+ return raw as T[K]
145
+ }
146
+
147
+ function read(scope: string | undefined): T {
148
+ if (!scope || typeof window === 'undefined') return defaults
149
+ let parsed: unknown
150
+ try {
151
+ const raw = window.localStorage.getItem(storageKey(scope))
152
+ if (!raw) return defaults
153
+ parsed = JSON.parse(raw)
154
+ } catch {
155
+ // A private window, blocked site data, or a truncated value. The panel
156
+ // opens at its defaults rather than not at all.
157
+ return defaults
158
+ }
159
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
160
+ return defaults
161
+ }
162
+ const record = parsed as Record<string, unknown>
163
+ const next: Record<string, DisplayValue> = { ...defaults }
164
+ for (const key of Object.keys(defaults)) {
165
+ if (!Object.hasOwn(record, key)) continue
166
+ const value = validate(key, record[key])
167
+ if (value !== null) next[key] = value
168
+ }
169
+ return next as unknown as T
170
+ }
171
+
172
+ function write(scope: string | undefined, settings: T): void {
173
+ if (!scope || typeof window === 'undefined') return
174
+ try {
175
+ window.localStorage.setItem(storageKey(scope), JSON.stringify(settings))
176
+ } catch {
177
+ // Never let a full or blocked store break the view.
178
+ }
179
+ }
180
+
181
+ const Ctx = createContext<DisplayController<T> | null>(null)
182
+
183
+ function Provider({ scope, children }: DisplayProviderProps): ReactNode {
184
+ const [settings, setSettings] = useState<T>(() => read(scope))
185
+
186
+ // A scope change RE-READS rather than remounting, so unsaved work
187
+ // elsewhere in the tree survives a brand switch (ADR-116 D10).
188
+ const previousScope = useRef(scope)
189
+ useEffect(() => {
190
+ if (previousScope.current === scope) return
191
+ previousScope.current = scope
192
+ setSettings(read(scope))
193
+ }, [scope])
194
+
195
+ const set = useCallback(
196
+ <K extends keyof T>(key: K, value: T[K]) => {
197
+ setSettings((current) => {
198
+ if (current[key] === value) return current
199
+ const next = { ...current, [key]: value }
200
+ write(scope, next)
201
+ return next
202
+ })
203
+ },
204
+ [scope],
205
+ )
206
+
207
+ const reset = useCallback(() => {
208
+ setSettings(() => {
209
+ write(scope, defaults)
210
+ return defaults
211
+ })
212
+ }, [scope])
213
+
214
+ const value = useMemo<DisplayController<T>>(
215
+ () => ({
216
+ settings,
217
+ set,
218
+ reset,
219
+ isDefault: Object.keys(defaults).every(
220
+ (key) => settings[key] === defaults[key],
221
+ ),
222
+ }),
223
+ [settings, set, reset],
224
+ )
225
+
226
+ return <Ctx.Provider value={value}>{children}</Ctx.Provider>
227
+ }
228
+
229
+ function useDisplay(): DisplayController<T> {
230
+ const ctx = useContext(Ctx)
231
+ if (!ctx) {
232
+ throw new Error(
233
+ `[display-store] useDisplay for "${name}" must be called inside its ` +
234
+ `Provider. Rendering with silent defaults would show a view the ` +
235
+ `display panel is not actually editing.`,
236
+ )
237
+ }
238
+ return ctx
239
+ }
240
+
241
+ return { Provider, useDisplay, defaults, storageKey }
242
+ }