@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.
- package/dist/chunk-RBYWGBQ2.js +2752 -0
- package/dist/chunk-RBYWGBQ2.js.map +1 -0
- package/dist/index.d.ts +6064 -80
- package/dist/index.js +21561 -9696
- package/dist/index.js.map +1 -1
- package/dist/rich-composer-impl-5NO443A6.js +1859 -0
- package/dist/rich-composer-impl-5NO443A6.js.map +1 -0
- package/dist/styles.css +1586 -2
- package/dist/theme-v2.css +228 -0
- package/dist/tokens.css +121 -0
- package/package.json +8 -1
- package/src/__tests__/anchor.test.tsx +422 -0
- package/src/__tests__/avatar.test.tsx +272 -0
- package/src/__tests__/bar-chart.test.tsx +809 -0
- package/src/__tests__/board.test.tsx +420 -0
- package/src/__tests__/chart-math.test.ts +922 -0
- package/src/__tests__/chart-series.test.ts +339 -0
- package/src/__tests__/code-block.test.tsx +134 -0
- package/src/__tests__/combobox.test.tsx +677 -0
- package/src/__tests__/display-popover.test.tsx +195 -0
- package/src/__tests__/display-store.test.tsx +208 -0
- package/src/__tests__/donut-chart.test.tsx +397 -0
- package/src/__tests__/dropdown-menu.test.tsx +572 -0
- package/src/__tests__/filter-menu.test.tsx +175 -0
- package/src/__tests__/gauge-ring.test.tsx +233 -0
- package/src/__tests__/helpers/geometry.ts +58 -0
- package/src/__tests__/layer-stack.test.tsx +228 -0
- package/src/__tests__/line-chart.test.tsx +612 -0
- package/src/__tests__/popover.test.tsx +460 -0
- package/src/__tests__/ranked-bars.test.tsx +343 -0
- package/src/__tests__/remark-underline.test.ts +194 -0
- package/src/__tests__/select.test.tsx +543 -0
- package/src/__tests__/sparkline.test.tsx +368 -0
- package/src/__tests__/stat-layer.test.tsx +271 -0
- package/src/__tests__/stat-strip.test.tsx +175 -0
- package/src/__tests__/status.test.tsx +293 -0
- package/src/__tests__/tabs.test.tsx +303 -0
- package/src/__tests__/token-shape.test.ts +132 -2
- package/src/__tests__/tooltip.test.tsx +355 -0
- package/src/avatar.tsx +352 -0
- package/src/bar-chart.tsx +1214 -0
- package/src/board.tsx +658 -0
- package/src/calculator-shell-v2.tsx +19 -39
- package/src/chart-frame.tsx +960 -0
- package/src/chart-legend.tsx +304 -0
- package/src/chart-tooltip.tsx +267 -0
- package/src/code-block.tsx +62 -8
- package/src/combobox.tsx +796 -0
- package/src/delta-chip.tsx +263 -0
- package/src/detail/__tests__/activity-pane.test.tsx +186 -0
- package/src/detail/__tests__/detail-chrome.test.tsx +134 -0
- package/src/detail/__tests__/detail-surface.test.tsx +529 -0
- package/src/detail/__tests__/field-row.test.tsx +357 -0
- package/src/detail/activity-pane.tsx +507 -0
- package/src/detail/calendar.tsx +355 -0
- package/src/detail/detail-divider.tsx +261 -0
- package/src/detail/detail-header.tsx +287 -0
- package/src/detail/detail-menu.tsx +254 -0
- package/src/detail/detail-surface.tsx +1110 -0
- package/src/detail/field-list.tsx +196 -0
- package/src/detail/field-row.tsx +1131 -0
- package/src/detail/index.ts +58 -0
- package/src/detail/segmented-choice.tsx +94 -0
- package/src/detail/types.ts +129 -0
- package/src/display-popover.tsx +487 -0
- package/src/display-store.tsx +242 -0
- package/src/donut-chart.tsx +988 -0
- package/src/dropdown-menu.tsx +412 -151
- package/src/filter-core/EXPORTS.md +85 -0
- package/src/filter-core/__tests__/columns.test.ts +159 -0
- package/src/filter-core/__tests__/faceting.test.ts +193 -0
- package/src/filter-core/__tests__/filter-fns.test.ts +519 -0
- package/src/filter-core/__tests__/operators.test.ts +235 -0
- package/src/filter-core/__tests__/state.test.ts +268 -0
- package/src/filter-core/__tests__/url.test.ts +350 -0
- package/src/filter-core/columns.ts +134 -0
- package/src/filter-core/date-utils.ts +38 -0
- package/src/filter-core/examples/task-filter-columns.ts +121 -0
- package/src/filter-core/faceting.ts +120 -0
- package/src/filter-core/filter-fns.ts +335 -0
- package/src/filter-core/index.ts +105 -0
- package/src/filter-core/operators.ts +433 -0
- package/src/filter-core/state.ts +280 -0
- package/src/filter-core/types.ts +247 -0
- package/src/filter-core/url.ts +261 -0
- package/src/filter-dropdown.tsx +12 -0
- package/src/filter-menu.tsx +649 -0
- package/src/floating-drawer.tsx +19 -1
- package/src/gauge-ring.tsx +435 -0
- package/src/hue.ts +52 -0
- package/src/index.ts +402 -0
- package/src/kbd.tsx +27 -4
- package/src/lib/anchor.ts +427 -0
- package/src/lib/chart.ts +866 -0
- package/src/lib/focus.ts +74 -0
- package/src/lib/layer-stack.ts +215 -0
- package/src/lib/refs.ts +31 -0
- package/src/lib/remark-underline.ts +443 -0
- package/src/lib/series.ts +169 -0
- package/src/line-chart.tsx +1176 -0
- package/src/markdown.tsx +26 -7
- package/src/metric-card.tsx +57 -22
- package/src/modal.tsx +73 -66
- package/src/popover.tsx +407 -0
- package/src/progress-ledger.tsx +304 -0
- package/src/ranked-bars.tsx +386 -0
- package/src/segmented-pill.tsx +32 -9
- package/src/select.tsx +646 -0
- package/src/sortable.tsx +315 -1
- package/src/sparkline.tsx +416 -0
- package/src/stat-card.tsx +376 -0
- package/src/stat-row.tsx +108 -70
- package/src/stat-strip.tsx +327 -0
- package/src/status.tsx +215 -0
- package/src/styles.css +1586 -2
- package/src/tabs.tsx +206 -25
- package/src/task-card.tsx +610 -0
- package/src/thread/__tests__/comment-body-hostile.test.tsx +331 -0
- package/src/thread/__tests__/comment-tree.test.ts +151 -0
- package/src/thread/__tests__/emoji.test.ts +187 -0
- package/src/thread/__tests__/fixtures/thread-fixture.ts +235 -0
- package/src/thread/__tests__/link-preview-source.test.ts +120 -0
- package/src/thread/__tests__/link-preview.test.tsx +600 -0
- package/src/thread/__tests__/markdown-format.test.ts +82 -0
- package/src/thread/__tests__/markdown-spec.test.ts +469 -0
- package/src/thread/__tests__/relative-time.test.ts +71 -0
- package/src/thread/__tests__/rich-composer.test.tsx +799 -0
- package/src/thread/__tests__/scroll-caret.test.ts +58 -0
- package/src/thread/__tests__/suggestion-list.test.tsx +648 -0
- package/src/thread/__tests__/thread-scroll-ownership.test.tsx +88 -0
- package/src/thread/__tests__/thread.test.tsx +662 -0
- package/src/thread/__tests__/use-attachments.test.tsx +679 -0
- package/src/thread/actions.tsx +196 -0
- package/src/thread/attachments.tsx +1071 -0
- package/src/thread/comment-body.tsx +148 -0
- package/src/thread/comment-tree.ts +182 -0
- package/src/thread/comment.tsx +915 -0
- package/src/thread/composer-footer.tsx +125 -0
- package/src/thread/composer.tsx +319 -0
- package/src/thread/emoji.ts +283 -0
- package/src/thread/index.ts +153 -0
- package/src/thread/link-preview.tsx +341 -0
- package/src/thread/markdown-format.ts +155 -0
- package/src/thread/markdown-spec.ts +754 -0
- package/src/thread/rail.tsx +372 -0
- package/src/thread/reactions.tsx +415 -0
- package/src/thread/relative-time.tsx +94 -0
- package/src/thread/rich-composer-impl.tsx +1601 -0
- package/src/thread/rich-composer.tsx +195 -0
- package/src/thread/scroll-caret.ts +37 -0
- package/src/thread/suggestion-list.tsx +182 -0
- package/src/thread/thread.tsx +718 -0
- package/src/thread/types.ts +218 -0
- package/src/thread/use-attachments.ts +598 -0
- package/src/thread/use-now.ts +73 -0
- package/src/thread/use-thread.ts +316 -0
- package/src/tokens.css +121 -0
- 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
|
+
}
|