@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/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
- disabledWhen?: DisabledWhen
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") and are still a
29
- * single-path condition — composable AND/OR belongs in a later DSL.
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
- export type CustomFieldDef = BaseField & {
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?: 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?: 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
- path: string,
176
- value: unknown,
177
- selection: S,
178
- ctx: ScopeContext,
179
- ) => void
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"