@preset.nz/facets 0.1.3 → 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 +314 -11
- package/package.json +6 -3
- package/schema/v0.1.json +253 -121
- package/src/field-renderers.tsx +354 -206
- package/src/file-handlers.ts +158 -0
- package/src/index.ts +15 -6
- package/src/layout.ts +41 -0
- package/src/pages.ts +50 -0
- package/src/panel.tsx +611 -83
- package/src/registry.ts +8 -7
- package/src/types.ts +148 -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,15 +18,46 @@ 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
|
|
27
|
+
promote?: PromoteView[]
|
|
22
28
|
}
|
|
23
29
|
|
|
30
|
+
/**
|
|
31
|
+
* A view a field can be promoted to, like a parm promoted to a Houdini
|
|
32
|
+
* asset's interface. `promote` lists them, each chosen on its own:
|
|
33
|
+
* `"card"` shows the field on the card; `"collapsed"` in a closed group's
|
|
34
|
+
* header and in a folded scope. Leave `promote` out and the field is
|
|
35
|
+
* inspector-only.
|
|
36
|
+
*/
|
|
37
|
+
export type PromoteView = "card" | "collapsed"
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* How `PropertyPanel` draws a scope. `"inspector"` is every field in its
|
|
41
|
+
* groups; `"card"` the fields promoted to it, flat; `"collapsed"` the
|
|
42
|
+
* fields promoted to it, on one line.
|
|
43
|
+
*/
|
|
44
|
+
export type PanelView = "inspector" | "card" | "collapsed"
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Where a field's label sits, for the inspector and card views. `"auto"` (the default) is the
|
|
48
|
+
* original layout: number and vector fields carry their label inside the box as the scrub
|
|
49
|
+
* handle, other fields stack it above. `"column"` puts every label in one shared column to the
|
|
50
|
+
* left, so the controls start on one edge. `"stacked"` puts every label above its control.
|
|
51
|
+
* Paired rows (`[a, b]`) draw one field per row in `"column"`.
|
|
52
|
+
*/
|
|
53
|
+
export type LabelLayout = "auto" | "column" | "stacked"
|
|
54
|
+
|
|
24
55
|
/**
|
|
25
56
|
* Disables a field based on one other field's value. Exactly one operator is
|
|
26
|
-
* 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
|
|
27
58
|
* `equals`, `notEquals`, `in`, `notIn` wins. `in`/`notIn` match a list of
|
|
28
|
-
* values (e.g. "only these enum choices use this param")
|
|
29
|
-
*
|
|
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`).
|
|
30
61
|
*/
|
|
31
62
|
export interface DisabledWhen {
|
|
32
63
|
path: string
|
|
@@ -36,9 +67,15 @@ export interface DisabledWhen {
|
|
|
36
67
|
notIn?: readonly unknown[]
|
|
37
68
|
}
|
|
38
69
|
|
|
70
|
+
/** One condition, or a list: disabled when any of them holds. */
|
|
71
|
+
export type DisabledWhenAny = DisabledWhen | readonly DisabledWhen[]
|
|
72
|
+
|
|
39
73
|
// A custom field is any BaseField whose `kind` is not one of the built-ins.
|
|
40
74
|
// The host's registered renderer for that kind decides what extra props it reads.
|
|
41
|
-
|
|
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
|
|
42
79
|
[extra: string]: unknown
|
|
43
80
|
}
|
|
44
81
|
|
|
@@ -74,6 +111,10 @@ export interface NumberFieldDef extends BaseField {
|
|
|
74
111
|
min?: number
|
|
75
112
|
max?: number
|
|
76
113
|
step?: number
|
|
114
|
+
/** Whole numbers only: no fractional display, and a fine-step modifier never goes finer than `step`. */
|
|
115
|
+
integer?: boolean
|
|
116
|
+
/** A unit after the number, e.g. `px` or `°`. Shown when the panel's `labelLayout` is `"column"` or `"stacked"`. */
|
|
117
|
+
suffix?: string
|
|
77
118
|
}
|
|
78
119
|
|
|
79
120
|
export interface SliderFieldDef extends BaseField {
|
|
@@ -106,9 +147,36 @@ export interface VectorFieldDef extends BaseField {
|
|
|
106
147
|
precision?: number
|
|
107
148
|
}
|
|
108
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
|
+
*/
|
|
109
165
|
export interface FileFieldDef extends BaseField {
|
|
110
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
|
+
*/
|
|
111
178
|
accept?: string
|
|
179
|
+
/** @deprecated Use `help`, which every field has. Drawn as `help` when that's absent. */
|
|
112
180
|
helperText?: string
|
|
113
181
|
}
|
|
114
182
|
|
|
@@ -123,7 +191,8 @@ export interface SeparatorFieldDef {
|
|
|
123
191
|
kind: "separator"
|
|
124
192
|
id: string
|
|
125
193
|
label?: string
|
|
126
|
-
disabledWhen?:
|
|
194
|
+
disabledWhen?: DisabledWhenAny
|
|
195
|
+
promote?: PromoteView[]
|
|
127
196
|
}
|
|
128
197
|
|
|
129
198
|
/** Static text: a hint, a note, a sub-heading. The text is `label`. */
|
|
@@ -131,7 +200,8 @@ export interface LabelFieldDef {
|
|
|
131
200
|
kind: "label"
|
|
132
201
|
id: string
|
|
133
202
|
label: string
|
|
134
|
-
disabledWhen?:
|
|
203
|
+
disabledWhen?: DisabledWhenAny
|
|
204
|
+
promote?: PromoteView[]
|
|
135
205
|
}
|
|
136
206
|
|
|
137
207
|
export type BuiltinFieldDef =
|
|
@@ -157,10 +227,31 @@ export interface PropertyGroupDef {
|
|
|
157
227
|
description?: string
|
|
158
228
|
collapsible?: boolean
|
|
159
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
|
|
160
243
|
}
|
|
161
244
|
|
|
162
245
|
export interface PropertySchema {
|
|
163
246
|
version: number
|
|
247
|
+
/** What the scope is called, e.g. "Filter". Heads the card; `PropertyPanel`'s `title` overrides it. */
|
|
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[]
|
|
164
255
|
groups: PropertyGroupDef[]
|
|
165
256
|
}
|
|
166
257
|
|
|
@@ -168,15 +259,42 @@ export interface PropertySchema {
|
|
|
168
259
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
169
260
|
export type ScopeContext = any
|
|
170
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
|
+
|
|
171
272
|
export interface Scope<S = unknown, V = Record<string, unknown>> {
|
|
172
273
|
schema: PropertySchema
|
|
173
274
|
read: (selection: S, ctx: ScopeContext) => V
|
|
174
|
-
write?: (
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
275
|
+
write?: (path: string, value: unknown, selection: S, ctx: ScopeContext) => WriteResult
|
|
276
|
+
/**
|
|
277
|
+
* One drag as one undo step. A drag is a scrubbed number, a dragged slider
|
|
278
|
+
* thumb or an open colour picker; `begin` fires when it starts, every
|
|
279
|
+
* `write` until `end` is part of it, and `end` closes it. `cancel` fires
|
|
280
|
+
* instead of `end` when the user abandons it (Escape during a scrub): put
|
|
281
|
+
* the value back. Omit `gesture` and drags are plain repeated `write`s.
|
|
282
|
+
*/
|
|
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[]
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export interface ScopeGesture<S = unknown> {
|
|
295
|
+
begin: (path: string, selection: S, ctx: ScopeContext) => void
|
|
296
|
+
end: (path: string, selection: S, ctx: ScopeContext) => void
|
|
297
|
+
cancel?: (path: string, selection: S, ctx: ScopeContext) => void
|
|
180
298
|
}
|
|
181
299
|
|
|
182
300
|
export interface FieldRendererProps<F extends FieldDef = FieldDef> {
|
|
@@ -184,11 +302,25 @@ export interface FieldRendererProps<F extends FieldDef = FieldDef> {
|
|
|
184
302
|
value: unknown
|
|
185
303
|
disabled: boolean
|
|
186
304
|
onChange?: (next: unknown) => void
|
|
305
|
+
/**
|
|
306
|
+
* A drag began, ended, or was abandoned: see `Scope.gesture`. Set only when
|
|
307
|
+
* the scope declares `gesture` and the panel is editable. The built-in
|
|
308
|
+
* number, vector, slider and colour renderers call them around their drags;
|
|
309
|
+
* a custom renderer with a continuous control may too.
|
|
310
|
+
*/
|
|
311
|
+
onGestureBegin?: () => void
|
|
312
|
+
onGestureEnd?: () => void
|
|
313
|
+
onGestureCancel?: () => void
|
|
314
|
+
/** Why the host refused the field's last written value (see `WriteResult`). Absent once the value changes. */
|
|
315
|
+
error?: string
|
|
187
316
|
ctx: ScopeContext
|
|
317
|
+
/**
|
|
318
|
+
* The view the field is drawn in. In `"collapsed"` it shares a line with
|
|
319
|
+
* others, so built-ins draw compact. Optional: a renderer may ignore it.
|
|
320
|
+
*/
|
|
321
|
+
view?: PanelView
|
|
188
322
|
}
|
|
189
323
|
|
|
190
|
-
export type FieldRenderer<F extends FieldDef = FieldDef> = React.FC<
|
|
191
|
-
FieldRendererProps<F>
|
|
192
|
-
>
|
|
324
|
+
export type FieldRenderer<F extends FieldDef = FieldDef> = React.FC<FieldRendererProps<F>>
|
|
193
325
|
|
|
194
326
|
import type React from "react"
|