@preset.nz/facets 0.2.0 → 0.3.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/README.md +159 -8
- package/package.json +6 -3
- package/schema/v0.1.json +198 -125
- package/src/field-renderers.tsx +130 -103
- package/src/file-handlers.ts +158 -0
- package/src/index.ts +15 -7
- package/src/layout.ts +5 -1
- package/src/pages.ts +50 -0
- package/src/panel.tsx +434 -87
- package/src/registry.ts +8 -7
- package/src/types.ts +86 -16
package/src/registry.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
+
import { schemaProblems } from "./pages"
|
|
1
2
|
import type { FieldRenderer, Scope } from "./types"
|
|
2
3
|
|
|
3
4
|
const scopes = new Map<string, Scope<unknown, Record<string, unknown>>>()
|
|
4
5
|
const fieldRenderers = new Map<string, FieldRenderer>()
|
|
5
6
|
|
|
7
|
+
/** Throws when the schema's pages don't add up: see `schemaProblems`. */
|
|
6
8
|
export function registerScope<S, V extends Record<string, unknown>>(
|
|
7
9
|
key: string,
|
|
8
10
|
scope: Scope<S, V>,
|
|
9
11
|
): void {
|
|
12
|
+
const problems = schemaProblems(scope.schema)
|
|
13
|
+
if (problems.length > 0) {
|
|
14
|
+
throw new Error(`[properties] scope "${key}": ${problems.join("; ")}`)
|
|
15
|
+
}
|
|
10
16
|
scopes.set(key, scope as Scope<unknown, Record<string, unknown>>)
|
|
11
17
|
}
|
|
12
18
|
|
|
@@ -14,16 +20,11 @@ export function unregisterScope(key: string): void {
|
|
|
14
20
|
scopes.delete(key)
|
|
15
21
|
}
|
|
16
22
|
|
|
17
|
-
export function getScope(
|
|
18
|
-
key: string,
|
|
19
|
-
): Scope<unknown, Record<string, unknown>> | undefined {
|
|
23
|
+
export function getScope(key: string): Scope<unknown, Record<string, unknown>> | undefined {
|
|
20
24
|
return scopes.get(key)
|
|
21
25
|
}
|
|
22
26
|
|
|
23
|
-
export function registerFieldRenderer(
|
|
24
|
-
kind: string,
|
|
25
|
-
Component: FieldRenderer,
|
|
26
|
-
): void {
|
|
27
|
+
export function registerFieldRenderer(kind: string, Component: FieldRenderer): void {
|
|
27
28
|
fieldRenderers.set(kind, Component)
|
|
28
29
|
}
|
|
29
30
|
|
package/src/types.ts
CHANGED
|
@@ -18,7 +18,12 @@ export interface BaseField {
|
|
|
18
18
|
id: string
|
|
19
19
|
label?: string
|
|
20
20
|
path: string
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* One or two sentences on what the field does, drawn under the control in the
|
|
23
|
+
* inspector view. The card and collapsed views leave it out.
|
|
24
|
+
*/
|
|
25
|
+
help?: string
|
|
26
|
+
disabledWhen?: DisabledWhenAny
|
|
22
27
|
promote?: PromoteView[]
|
|
23
28
|
}
|
|
24
29
|
|
|
@@ -49,10 +54,10 @@ export type LabelLayout = "auto" | "column" | "stacked"
|
|
|
49
54
|
|
|
50
55
|
/**
|
|
51
56
|
* Disables a field based on one other field's value. Exactly one operator is
|
|
52
|
-
* meant to be set; if several are, the first present in the order
|
|
57
|
+
* meant to be set; if several are set, the first present in the order
|
|
53
58
|
* `equals`, `notEquals`, `in`, `notIn` wins. `in`/`notIn` match a list of
|
|
54
|
-
* values (e.g. "only these enum choices use this param")
|
|
55
|
-
*
|
|
59
|
+
* values (e.g. "only these enum choices use this param"). A field takes a list
|
|
60
|
+
* of these to be disabled when any of them holds (`DisabledWhenAny`).
|
|
56
61
|
*/
|
|
57
62
|
export interface DisabledWhen {
|
|
58
63
|
path: string
|
|
@@ -62,9 +67,15 @@ export interface DisabledWhen {
|
|
|
62
67
|
notIn?: readonly unknown[]
|
|
63
68
|
}
|
|
64
69
|
|
|
70
|
+
/** One condition, or a list: disabled when any of them holds. */
|
|
71
|
+
export type DisabledWhenAny = DisabledWhen | readonly DisabledWhen[]
|
|
72
|
+
|
|
65
73
|
// A custom field is any BaseField whose `kind` is not one of the built-ins.
|
|
66
74
|
// The host's registered renderer for that kind decides what extra props it reads.
|
|
67
|
-
|
|
75
|
+
// `path` is optional here: a custom field may hold no value (a host section the
|
|
76
|
+
// renderer draws and owns), like the display-only kinds.
|
|
77
|
+
export type CustomFieldDef = Omit<BaseField, "path"> & {
|
|
78
|
+
path?: string
|
|
68
79
|
[extra: string]: unknown
|
|
69
80
|
}
|
|
70
81
|
|
|
@@ -136,9 +147,36 @@ export interface VectorFieldDef extends BaseField {
|
|
|
136
147
|
precision?: number
|
|
137
148
|
}
|
|
138
149
|
|
|
150
|
+
/** One file type in the host's Open dialog. Extensions have no dot: `["png", "jpg"]`. */
|
|
151
|
+
export interface FileFilter {
|
|
152
|
+
name: string
|
|
153
|
+
extensions: string[]
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* A path to a file, edited on ux-kit's `PathField`. The value is the path string as written,
|
|
158
|
+
* relative or absolute, or `null`/`""` when empty. Typing commits on Return or blur through the
|
|
159
|
+
* scope's `write`; the host validates there (see `WriteResult`). The ▾ list, the browse icon, the
|
|
160
|
+
* Missing check and the context menu need the host's `registerFileHandlers`; without them the
|
|
161
|
+
* field is a plain editable text field.
|
|
162
|
+
*
|
|
163
|
+
* Before 0.3 the value was a read-only `{ name }`. That value still draws, read-only.
|
|
164
|
+
*/
|
|
139
165
|
export interface FileFieldDef extends BaseField {
|
|
140
166
|
kind: "file"
|
|
167
|
+
/** Keys the recent paths: fields with the same key share one ▾ list, across documents (`"texture"`). */
|
|
168
|
+
recentKey?: string
|
|
169
|
+
/** File types in the Open dialog. */
|
|
170
|
+
filters?: FileFilter[]
|
|
171
|
+
/** The Open dialog's title. */
|
|
172
|
+
title?: string
|
|
173
|
+
placeholder?: string
|
|
174
|
+
/**
|
|
175
|
+
* @deprecated Use `filters`. File types as on `<input accept>`; the extensions in it (`.png,.jpg`)
|
|
176
|
+
* become one "Files" filter when `filters` is absent.
|
|
177
|
+
*/
|
|
141
178
|
accept?: string
|
|
179
|
+
/** @deprecated Use `help`, which every field has. Drawn as `help` when that's absent. */
|
|
142
180
|
helperText?: string
|
|
143
181
|
}
|
|
144
182
|
|
|
@@ -153,7 +191,7 @@ export interface SeparatorFieldDef {
|
|
|
153
191
|
kind: "separator"
|
|
154
192
|
id: string
|
|
155
193
|
label?: string
|
|
156
|
-
disabledWhen?:
|
|
194
|
+
disabledWhen?: DisabledWhenAny
|
|
157
195
|
promote?: PromoteView[]
|
|
158
196
|
}
|
|
159
197
|
|
|
@@ -162,7 +200,7 @@ export interface LabelFieldDef {
|
|
|
162
200
|
kind: "label"
|
|
163
201
|
id: string
|
|
164
202
|
label: string
|
|
165
|
-
disabledWhen?:
|
|
203
|
+
disabledWhen?: DisabledWhenAny
|
|
166
204
|
promote?: PromoteView[]
|
|
167
205
|
}
|
|
168
206
|
|
|
@@ -189,12 +227,31 @@ export interface PropertyGroupDef {
|
|
|
189
227
|
description?: string
|
|
190
228
|
collapsible?: boolean
|
|
191
229
|
defaultCollapsed?: boolean
|
|
230
|
+
/** The id of the page the group sits on. Required when the schema declares `pages`, absent otherwise. */
|
|
231
|
+
page?: string
|
|
232
|
+
/** Sits behind the page's one closed Advanced row, at the foot of the page. */
|
|
233
|
+
advanced?: boolean
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* A tab in the inspector. Pages are presentation only: a field's `path` never
|
|
238
|
+
* names one, so moving a group to another page changes no values.
|
|
239
|
+
*/
|
|
240
|
+
export interface PropertyPageDef {
|
|
241
|
+
id: string
|
|
242
|
+
label: string
|
|
192
243
|
}
|
|
193
244
|
|
|
194
245
|
export interface PropertySchema {
|
|
195
246
|
version: number
|
|
196
247
|
/** What the scope is called, e.g. "Filter". Heads the card; `PropertyPanel`'s `title` overrides it. */
|
|
197
248
|
title?: string
|
|
249
|
+
/**
|
|
250
|
+
* Splits the inspector into tabs, in this order. Omit it for one page and no
|
|
251
|
+
* tab row. Each page needs at least one group that isn't `advanced`; see
|
|
252
|
+
* `schemaProblems`.
|
|
253
|
+
*/
|
|
254
|
+
pages?: PropertyPageDef[]
|
|
198
255
|
groups: PropertyGroupDef[]
|
|
199
256
|
}
|
|
200
257
|
|
|
@@ -202,15 +259,20 @@ export interface PropertySchema {
|
|
|
202
259
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
203
260
|
export type ScopeContext = any
|
|
204
261
|
|
|
262
|
+
/**
|
|
263
|
+
* What a scope's `write` may return. Nothing is the usual case: the write was taken. `{ error }`,
|
|
264
|
+
* or a promise that resolves to it or rejects, refuses the value: the panel hands the message to
|
|
265
|
+
* the field as `error` for as long as the field's value stays as it was, and a field that can
|
|
266
|
+
* show it (the `file` field marks itself invalid) does. An accepted write needs no signal: the
|
|
267
|
+
* host's `read` returns the new value.
|
|
268
|
+
*/
|
|
269
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: an async write that returns nothing is a Promise<void>
|
|
270
|
+
export type WriteResult = void | { error: string } | Promise<void | { error: string }>
|
|
271
|
+
|
|
205
272
|
export interface Scope<S = unknown, V = Record<string, unknown>> {
|
|
206
273
|
schema: PropertySchema
|
|
207
274
|
read: (selection: S, ctx: ScopeContext) => V
|
|
208
|
-
write?: (
|
|
209
|
-
path: string,
|
|
210
|
-
value: unknown,
|
|
211
|
-
selection: S,
|
|
212
|
-
ctx: ScopeContext,
|
|
213
|
-
) => void
|
|
275
|
+
write?: (path: string, value: unknown, selection: S, ctx: ScopeContext) => WriteResult
|
|
214
276
|
/**
|
|
215
277
|
* One drag as one undo step. A drag is a scrubbed number, a dragged slider
|
|
216
278
|
* thumb or an open colour picker; `begin` fires when it starts, every
|
|
@@ -219,6 +281,14 @@ export interface Scope<S = unknown, V = Record<string, unknown>> {
|
|
|
219
281
|
* the value back. Omit `gesture` and drags are plain repeated `write`s.
|
|
220
282
|
*/
|
|
221
283
|
gesture?: ScopeGesture<S>
|
|
284
|
+
/**
|
|
285
|
+
* The paths whose values differ from their defaults, or whatever "changed"
|
|
286
|
+
* means to the host. The panel marks a tab, a closed Advanced row or a
|
|
287
|
+
* closed group that hides one of them, so a change is never out of sight
|
|
288
|
+
* without a trace. It doesn't mark the field itself: a renderer or label
|
|
289
|
+
* does that. Called like `read`, when the selection or ctx changes.
|
|
290
|
+
*/
|
|
291
|
+
changed?: (selection: S, ctx: ScopeContext) => readonly string[]
|
|
222
292
|
}
|
|
223
293
|
|
|
224
294
|
export interface ScopeGesture<S = unknown> {
|
|
@@ -241,6 +311,8 @@ export interface FieldRendererProps<F extends FieldDef = FieldDef> {
|
|
|
241
311
|
onGestureBegin?: () => void
|
|
242
312
|
onGestureEnd?: () => void
|
|
243
313
|
onGestureCancel?: () => void
|
|
314
|
+
/** Why the host refused the field's last written value (see `WriteResult`). Absent once the value changes. */
|
|
315
|
+
error?: string
|
|
244
316
|
ctx: ScopeContext
|
|
245
317
|
/**
|
|
246
318
|
* The view the field is drawn in. In `"collapsed"` it shares a line with
|
|
@@ -249,8 +321,6 @@ export interface FieldRendererProps<F extends FieldDef = FieldDef> {
|
|
|
249
321
|
view?: PanelView
|
|
250
322
|
}
|
|
251
323
|
|
|
252
|
-
export type FieldRenderer<F extends FieldDef = FieldDef> = React.FC<
|
|
253
|
-
FieldRendererProps<F>
|
|
254
|
-
>
|
|
324
|
+
export type FieldRenderer<F extends FieldDef = FieldDef> = React.FC<FieldRendererProps<F>>
|
|
255
325
|
|
|
256
326
|
import type React from "react"
|