@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.
- package/dist/chunk-RBYWGBQ2.js +2752 -0
- package/dist/chunk-RBYWGBQ2.js.map +1 -0
- package/dist/index.d.ts +5574 -57
- package/dist/index.js +21650 -11206
- 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 +1570 -0
- package/dist/tokens.css +112 -0
- package/package.json +8 -1
- 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__/display-popover.test.tsx +195 -0
- package/src/__tests__/display-store.test.tsx +307 -0
- package/src/__tests__/donut-chart.test.tsx +397 -0
- package/src/__tests__/dropdown-menu.test.tsx +156 -2
- package/src/__tests__/filter-menu.test.tsx +175 -0
- package/src/__tests__/gauge-ring.test.tsx +233 -0
- package/src/__tests__/line-chart.test.tsx +612 -0
- package/src/__tests__/ranked-bars.test.tsx +343 -0
- package/src/__tests__/remark-underline.test.ts +194 -0
- package/src/__tests__/sortable.test.tsx +394 -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/avatar.tsx +352 -0
- package/src/bar-chart.tsx +1214 -0
- package/src/board.tsx +658 -0
- 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/delta-chip.tsx +263 -0
- package/src/detail/__tests__/activity-pane.test.tsx +369 -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 +611 -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 +301 -0
- package/src/donut-chart.tsx +988 -0
- package/src/dropdown-menu.tsx +290 -19
- 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 +303 -0
- package/src/kbd.tsx +27 -4
- package/src/lib/chart.ts +866 -0
- package/src/lib/focus.ts +43 -1
- package/src/lib/layer-stack.ts +30 -3
- 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/modal.tsx +42 -18
- package/src/progress-ledger.tsx +304 -0
- package/src/ranked-bars.tsx +386 -0
- package/src/segmented-pill.tsx +32 -9
- package/src/sortable.tsx +520 -1
- package/src/sparkline.tsx +416 -0
- package/src/stat-card.tsx +376 -0
- package/src/stat-strip.tsx +327 -0
- package/src/status.tsx +215 -0
- package/src/styles.css +1570 -0
- 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 +252 -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 +742 -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 +967 -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 +232 -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 +112 -0
|
@@ -0,0 +1,301 @@
|
|
|
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
|
+
* `<prefix>:<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
|
+
* THE PREFIX IS THE PRODUCT, AND ITS DEFAULT IS FROZEN. This package is a
|
|
29
|
+
* dependency of more than one app on more than one origin, and `workspace` is
|
|
30
|
+
* only the right first segment for one of them: a host whose own convention is
|
|
31
|
+
* `lightwork:<area>:<id>:<key>` was getting the one key on its board that read
|
|
32
|
+
* as somebody else's. So `prefix` is an option — and its default is `workspace`
|
|
33
|
+
* FOREVER, not because that name is good but because these keys already exist
|
|
34
|
+
* in operators' browsers. A store holds a preference the operator set; changing
|
|
35
|
+
* the default segment would not move that preference to a new key, it would
|
|
36
|
+
* walk past it and open at the defaults. The suite pins the unprefixed key as a
|
|
37
|
+
* literal for that reason.
|
|
38
|
+
*
|
|
39
|
+
* READING IS DEFENSIVE. `localStorage` is untrusted input — a hand-edited
|
|
40
|
+
* value, a key left behind by an older build, a private window that throws on
|
|
41
|
+
* access. Every persisted key is checked against the default's TYPE and, when
|
|
42
|
+
* the caller declares one, against an allow-list of values. Anything that
|
|
43
|
+
* fails falls back to the default rather than reaching a component as a
|
|
44
|
+
* setting nothing knows how to render.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import {
|
|
48
|
+
createContext,
|
|
49
|
+
useCallback,
|
|
50
|
+
useContext,
|
|
51
|
+
useEffect,
|
|
52
|
+
useMemo,
|
|
53
|
+
useRef,
|
|
54
|
+
useState,
|
|
55
|
+
type ReactNode,
|
|
56
|
+
} from 'react'
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* What a display setting may be. Enough for a select, a toggle — and a
|
|
60
|
+
* MEASUREMENT.
|
|
61
|
+
*
|
|
62
|
+
* `number` joined the union for a size the operator drags rather than picks
|
|
63
|
+
* (the item detail's panel width). It is validated harder than the other two,
|
|
64
|
+
* because `NaN` and `Infinity` are both `typeof 'number'` and both survive a
|
|
65
|
+
* `JSON.parse` round trip through `null`-free hand editing: a non-finite value
|
|
66
|
+
* reaching a layout is a pane with no width, so it is rejected on read like
|
|
67
|
+
* any other value that does not hold up.
|
|
68
|
+
*/
|
|
69
|
+
export type DisplayValue = string | boolean | number
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The settings object. A TYPE alias, not an interface, on purpose: an
|
|
73
|
+
* interface has no implicit index signature and would not satisfy this
|
|
74
|
+
* constraint at the call site.
|
|
75
|
+
*/
|
|
76
|
+
export type DisplaySettings = Readonly<Record<string, DisplayValue>>
|
|
77
|
+
|
|
78
|
+
/** Allowed values per key, for the enumerated ones. Unlisted keys are open. */
|
|
79
|
+
export type DisplayAllowed<T extends DisplaySettings> = {
|
|
80
|
+
readonly [K in keyof T]?: readonly T[K][]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A per-key rewrite applied to the PERSISTED value before it is validated.
|
|
85
|
+
*
|
|
86
|
+
* The narrow case this exists for: a setting whose vocabulary changes, where
|
|
87
|
+
* the old value has an exact successor. Without it the allow-list rejects the
|
|
88
|
+
* stored value and the operator's preference is silently replaced by the
|
|
89
|
+
* default — which is a preference thrown away, not a preference migrated. The
|
|
90
|
+
* tasks lens widened `detailPresentation` from two values to three and its
|
|
91
|
+
* `drawer` became `panel`; nobody who had chosen the side panel should be put
|
|
92
|
+
* back on the default because of it.
|
|
93
|
+
*
|
|
94
|
+
* It runs on UNTRUSTED input, so it takes `unknown` and may return anything:
|
|
95
|
+
* whatever it produces still has to survive the type check and the allow-list
|
|
96
|
+
* afterwards. A migration cannot widen what is permitted, only translate into
|
|
97
|
+
* it.
|
|
98
|
+
*/
|
|
99
|
+
export type DisplayMigrations<T extends DisplaySettings> = {
|
|
100
|
+
readonly [K in keyof T]?: (raw: unknown) => unknown
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface DisplayController<T extends DisplaySettings> {
|
|
104
|
+
readonly settings: T
|
|
105
|
+
readonly set: <K extends keyof T>(key: K, value: T[K]) => void
|
|
106
|
+
readonly reset: () => void
|
|
107
|
+
/** True when nothing has been changed from the defaults. */
|
|
108
|
+
readonly isDefault: boolean
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface DisplayProviderProps {
|
|
112
|
+
/**
|
|
113
|
+
* The persistence partition — a brand id in a lens. Omit for a panel that
|
|
114
|
+
* should not persist at all (a preview, a test).
|
|
115
|
+
*/
|
|
116
|
+
scope?: string
|
|
117
|
+
children: ReactNode
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface DisplayStore<T extends DisplaySettings> {
|
|
121
|
+
/** Holds the state. Two providers are two independent panels. */
|
|
122
|
+
readonly Provider: (props: DisplayProviderProps) => ReactNode
|
|
123
|
+
/** Reads the nearest provider. Throws outside one — a silent default here
|
|
124
|
+
* would render a board with settings the panel is not editing. */
|
|
125
|
+
readonly useDisplay: () => DisplayController<T>
|
|
126
|
+
readonly defaults: T
|
|
127
|
+
/** Exposed so a host can name the key it is clearing. */
|
|
128
|
+
readonly storageKey: (scope: string) => string
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Everything about the store that is not its settings.
|
|
133
|
+
*
|
|
134
|
+
* An OBJECT rather than a fifth positional string, because `allowed` and
|
|
135
|
+
* `migrations` are both optional and already both objects: a call reading
|
|
136
|
+
* `createDisplayStore('board', DEFAULTS, undefined, undefined, 'lightwork')`
|
|
137
|
+
* puts two bare strings at opposite ends of an argument list and names
|
|
138
|
+
* neither. `{ prefix: 'lightwork' }` says what it is at the call site, and the
|
|
139
|
+
* next option that earns its place goes beside it rather than becoming a sixth
|
|
140
|
+
* positional.
|
|
141
|
+
*/
|
|
142
|
+
export interface DisplayStoreOptions {
|
|
143
|
+
/**
|
|
144
|
+
* The key's first segment — the PRODUCT, above the store's own `name`.
|
|
145
|
+
*
|
|
146
|
+
* Omit it and the key is `workspace:<name>:<scope>:display`, exactly as it
|
|
147
|
+
* has always been. Pass one and the store moves wholesale onto it, so two
|
|
148
|
+
* apps sharing this primitive on one origin under the same `name` cannot
|
|
149
|
+
* read or overwrite each other.
|
|
150
|
+
*
|
|
151
|
+
* It is not a migration seam. A store that changes prefix does not carry its
|
|
152
|
+
* stored settings across; it opens at its defaults on the new key and leaves
|
|
153
|
+
* the old one behind. Choose it once, when the store is written.
|
|
154
|
+
*/
|
|
155
|
+
prefix?: string
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isDisplayValue(value: unknown): value is DisplayValue {
|
|
159
|
+
if (typeof value === 'string' || typeof value === 'boolean') return true
|
|
160
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function createDisplayStore<T extends DisplaySettings>(
|
|
164
|
+
name: string,
|
|
165
|
+
defaults: T,
|
|
166
|
+
allowed?: DisplayAllowed<T>,
|
|
167
|
+
migrations?: DisplayMigrations<T>,
|
|
168
|
+
options?: DisplayStoreOptions,
|
|
169
|
+
): DisplayStore<T> {
|
|
170
|
+
// Resolved once, at construction, so the key cannot differ between the read
|
|
171
|
+
// on mount and the write on the next change.
|
|
172
|
+
const prefix = options?.prefix ?? 'workspace'
|
|
173
|
+
// An EMPTY prefix is refused rather than defaulted, and loudly, because both
|
|
174
|
+
// quieter options are worse. Falling back to `workspace` would hand a caller
|
|
175
|
+
// who asked for isolation the shared namespace instead — silently, and
|
|
176
|
+
// discoverable only as two products reading each other's settings. Accepting
|
|
177
|
+
// it produces `:board:ws_1:display`, whose empty first segment collides with
|
|
178
|
+
// every other empty-prefix caller: precisely the cross-product this option
|
|
179
|
+
// exists to prevent, reintroduced by the option itself.
|
|
180
|
+
//
|
|
181
|
+
// A throw at CONSTRUCTION is safe where a throw on read would not be: the
|
|
182
|
+
// store is built once, at module scope, so this fires before any UI exists
|
|
183
|
+
// rather than under a user mid-session. `useDisplay` outside a Provider
|
|
184
|
+
// already throws for the same reason.
|
|
185
|
+
if (prefix === '') {
|
|
186
|
+
throw new Error(
|
|
187
|
+
'createDisplayStore: `prefix` cannot be empty — omit the option to use the default.',
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
const storageKey = (scope: string) => `${prefix}:${name}:${scope}:display`
|
|
191
|
+
|
|
192
|
+
/** One key's persisted value, or `null` when it does not hold up. */
|
|
193
|
+
function validate<K extends keyof T & string>(key: K, input: unknown): T[K] | null {
|
|
194
|
+
const fallback = defaults[key]
|
|
195
|
+
// The migration runs FIRST and on the raw value, then everything below
|
|
196
|
+
// judges its output exactly as it would judge a stored one.
|
|
197
|
+
const migrate = migrations?.[key]
|
|
198
|
+
const raw = migrate === undefined ? input : migrate(input)
|
|
199
|
+
if (!isDisplayValue(raw)) return null
|
|
200
|
+
if (typeof raw !== typeof fallback) return null
|
|
201
|
+
const permitted = allowed?.[key]
|
|
202
|
+
if (permitted && !permitted.includes(raw as T[K])) return null
|
|
203
|
+
return raw as T[K]
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function read(scope: string | undefined): T {
|
|
207
|
+
if (!scope || typeof window === 'undefined') return defaults
|
|
208
|
+
let parsed: unknown
|
|
209
|
+
try {
|
|
210
|
+
const raw = window.localStorage.getItem(storageKey(scope))
|
|
211
|
+
if (!raw) return defaults
|
|
212
|
+
parsed = JSON.parse(raw)
|
|
213
|
+
} catch {
|
|
214
|
+
// A private window, blocked site data, or a truncated value. The panel
|
|
215
|
+
// opens at its defaults rather than not at all.
|
|
216
|
+
return defaults
|
|
217
|
+
}
|
|
218
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
219
|
+
return defaults
|
|
220
|
+
}
|
|
221
|
+
const record = parsed as Record<string, unknown>
|
|
222
|
+
const next: Record<string, DisplayValue> = { ...defaults }
|
|
223
|
+
for (const key of Object.keys(defaults)) {
|
|
224
|
+
if (!Object.hasOwn(record, key)) continue
|
|
225
|
+
const value = validate(key, record[key])
|
|
226
|
+
if (value !== null) next[key] = value
|
|
227
|
+
}
|
|
228
|
+
return next as unknown as T
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function write(scope: string | undefined, settings: T): void {
|
|
232
|
+
if (!scope || typeof window === 'undefined') return
|
|
233
|
+
try {
|
|
234
|
+
window.localStorage.setItem(storageKey(scope), JSON.stringify(settings))
|
|
235
|
+
} catch {
|
|
236
|
+
// Never let a full or blocked store break the view.
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const Ctx = createContext<DisplayController<T> | null>(null)
|
|
241
|
+
|
|
242
|
+
function Provider({ scope, children }: DisplayProviderProps): ReactNode {
|
|
243
|
+
const [settings, setSettings] = useState<T>(() => read(scope))
|
|
244
|
+
|
|
245
|
+
// A scope change RE-READS rather than remounting, so unsaved work
|
|
246
|
+
// elsewhere in the tree survives a brand switch (ADR-116 D10).
|
|
247
|
+
const previousScope = useRef(scope)
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
if (previousScope.current === scope) return
|
|
250
|
+
previousScope.current = scope
|
|
251
|
+
setSettings(read(scope))
|
|
252
|
+
}, [scope])
|
|
253
|
+
|
|
254
|
+
const set = useCallback(
|
|
255
|
+
<K extends keyof T>(key: K, value: T[K]) => {
|
|
256
|
+
setSettings((current) => {
|
|
257
|
+
if (current[key] === value) return current
|
|
258
|
+
const next = { ...current, [key]: value }
|
|
259
|
+
write(scope, next)
|
|
260
|
+
return next
|
|
261
|
+
})
|
|
262
|
+
},
|
|
263
|
+
[scope],
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
const reset = useCallback(() => {
|
|
267
|
+
setSettings(() => {
|
|
268
|
+
write(scope, defaults)
|
|
269
|
+
return defaults
|
|
270
|
+
})
|
|
271
|
+
}, [scope])
|
|
272
|
+
|
|
273
|
+
const value = useMemo<DisplayController<T>>(
|
|
274
|
+
() => ({
|
|
275
|
+
settings,
|
|
276
|
+
set,
|
|
277
|
+
reset,
|
|
278
|
+
isDefault: Object.keys(defaults).every(
|
|
279
|
+
(key) => settings[key] === defaults[key],
|
|
280
|
+
),
|
|
281
|
+
}),
|
|
282
|
+
[settings, set, reset],
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
return <Ctx.Provider value={value}>{children}</Ctx.Provider>
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function useDisplay(): DisplayController<T> {
|
|
289
|
+
const ctx = useContext(Ctx)
|
|
290
|
+
if (!ctx) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
`[display-store] useDisplay for "${name}" must be called inside its ` +
|
|
293
|
+
`Provider. Rendering with silent defaults would show a view the ` +
|
|
294
|
+
`display panel is not actually editing.`,
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
return ctx
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return { Provider, useDisplay, defaults, storageKey }
|
|
301
|
+
}
|