@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/README.md
CHANGED
|
@@ -44,8 +44,44 @@ the conventional shadcn path, and your bundler's `@` alias resolves it:
|
|
|
44
44
|
| `@/components/ui/checkbox` | `Checkbox` |
|
|
45
45
|
| `@/components/ui/separator` | `Separator` |
|
|
46
46
|
| `@/components/ui/select` | `Select`, `SelectTrigger`, `SelectValue`, `SelectContent`, `SelectItem` |
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
| `@/components/ui/number-field` | `NumberField` |
|
|
48
|
+
| `@/components/ui/slider` | `Slider` |
|
|
49
|
+
| `@/components/ui/color-field` | `ColorField` |
|
|
50
|
+
| `@/components/ui/path-field` | `PathField` (only for `file` fields) |
|
|
51
|
+
| `@/components/ui/tabs` | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` |
|
|
52
|
+
|
|
53
|
+
The last three are what make a panel feel like the rest of your app: a number
|
|
54
|
+
you drag, a square slider, your colour picker. They are the **minimum** facets
|
|
55
|
+
reads (typed in `dev/ui/` in this repo):
|
|
56
|
+
|
|
57
|
+
- `NumberField`: `value: number`, `onValueChange(value, { reason })`, `min`,
|
|
58
|
+
`max`, `step`, `integer`, `precision`, `label` (the scrub handle: the field
|
|
59
|
+
draws its own label, so the panel adds none), `disabled`, and
|
|
60
|
+
`onScrubStart` / `onScrubEnd` / `onScrubCancel`. Facets uses it for `number`
|
|
61
|
+
and for each slot of `vector`. A vector slot's `suffix` shows in read-only
|
|
62
|
+
mode only, unless the panel sets `labelLayout` (below). Two **optional** props
|
|
63
|
+
serve that: `labelPlacement` (`"inside"` default, `"column"`, `"above"`) and
|
|
64
|
+
`suffix` (a muted unit inside the box). A host that ignores them keeps the
|
|
65
|
+
label inside the box, so nothing doubles.
|
|
66
|
+
- `Slider`: `value: number`, `onValueChange(value)` (a number or an array, facets
|
|
67
|
+
takes the first), `onValueCommitted()`, `min`, `max`, `step`, `disabled`,
|
|
68
|
+
`aria-label`. One thumb.
|
|
69
|
+
- `ColorField`: `value: string | null`, `onChange(value | null)`, `label`,
|
|
70
|
+
`presets`, `disabled`, `onPickStart` / `onPickEnd`. No `onChange` means
|
|
71
|
+
read-only.
|
|
72
|
+
|
|
73
|
+
- `PathField`: `value: string | null`, `onCommit(path | null)` (Return or blur;
|
|
74
|
+
no `onCommit` means read-only), `recent: string[]`, `onBrowse(): Promise<string | null>`,
|
|
75
|
+
`status` (`"ok"`, `"missing"`, `"invalid"`), `message`, `id`, `placeholder`,
|
|
76
|
+
`disabled`, and the props of a `div` (facets passes `onContextMenu` and
|
|
77
|
+
`onMouseDownCapture`). With ux-kit: `export { PathField } from "@preset.nz/ux-kit"`.
|
|
78
|
+
|
|
79
|
+
`Tabs` draws a schema's pages. facets uses the shadcn API (`value`,
|
|
80
|
+
`onValueChange` on `Tabs`; `value` on `TabsTrigger` and `TabsContent`) and the
|
|
81
|
+
list's default variant, the segmented control. With ux-kit it is a one-liner:
|
|
82
|
+
`export { Tabs, TabsContent, TabsList, TabsTrigger } from "@preset.nz/ux-kit"`.
|
|
83
|
+
|
|
84
|
+
So you need an `@/*` alias pointing at your `src/`, those ten modules present,
|
|
49
85
|
and Tailwind with shadcn's theme tokens (`muted-foreground`, `border` and
|
|
50
86
|
friends are used throughout).
|
|
51
87
|
|
|
@@ -61,6 +97,18 @@ package in your CSS. From `src/index.css`:
|
|
|
61
97
|
Nothing fails when this line is missing. The build succeeds and the panel
|
|
62
98
|
renders unstyled.
|
|
63
99
|
|
|
100
|
+
Tell Vite to resolve one copy of React and of this package, so the package and
|
|
101
|
+
your app share one registry. In `vite.config.ts`:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
resolve: {
|
|
105
|
+
dedupe: ["react", "react-dom", "@preset.nz/facets"],
|
|
106
|
+
},
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Without it, a linked or nested install can register renderers in one copy and
|
|
110
|
+
look them up in another.
|
|
111
|
+
|
|
64
112
|
The upside is that `facets` inherits whatever primitive library you already
|
|
65
113
|
chose — Base UI, Radix, or your own — instead of dragging in a second one and
|
|
66
114
|
making your panels look foreign inside your own app.
|
|
@@ -122,7 +170,29 @@ Select a blur and the blur panel appears. Add a new filter type later by
|
|
|
122
170
|
registering another scope — the panel does not change.
|
|
123
171
|
|
|
124
172
|
A row holding an array renders as a two-column grid. Everything else is full
|
|
125
|
-
width. That is the whole layout system, on purpose
|
|
173
|
+
width. That is the whole layout system, on purpose, with one switch: where the
|
|
174
|
+
labels go.
|
|
175
|
+
|
|
176
|
+
### Label layout
|
|
177
|
+
|
|
178
|
+
`labelLayout` on `PropertyPanel` (inspector and card views; collapsed ignores it):
|
|
179
|
+
|
|
180
|
+
| Value | Labels | Numbers |
|
|
181
|
+
|---|---|---|
|
|
182
|
+
| `"auto"` (default) | Above their control; a checkbox's beside it | The label is the scrub handle, inside the box |
|
|
183
|
+
| `"column"` | One shared column to the left, truncated with the full text as a tooltip; controls start on one edge | Label in the column (still the scrub handle), number right-aligned, `suffix` as a muted unit |
|
|
184
|
+
| `"stacked"` | All above their control | Label above the box, `suffix` as a unit |
|
|
185
|
+
|
|
186
|
+
In `"column"` the label column's width is one CSS variable on the panel root
|
|
187
|
+
(`--facets-label-w`), set from the longest label (8ch to 16ch) or from the
|
|
188
|
+
`labelWidth` prop. A vector keeps its axis labels inside its slots, so its
|
|
189
|
+
fields share the column the scalars start on. A paired row (`[a, b]`) draws one
|
|
190
|
+
field per row, and a slider shows its value to the right. A custom renderer
|
|
191
|
+
follows the layout by wrapping its control in `FieldShell` (the built-ins'
|
|
192
|
+
shell; `top` aligns the label with the top of a tall control, `above` keeps it
|
|
193
|
+
above in every layout) and a read-only value in `ReadOnlyText`. For anything
|
|
194
|
+
else, `useLabelLayout()` and `COLUMN_ROW` give the parts. A renderer that draws
|
|
195
|
+
its own label ignores the layout.
|
|
126
196
|
|
|
127
197
|
---
|
|
128
198
|
|
|
@@ -148,11 +218,40 @@ Built-ins register through the same call you would use, with no special casing.
|
|
|
148
218
|
|
|
149
219
|
**`PropertyPanel`** — looks up the scope, calls `read`, walks the groups, hands
|
|
150
220
|
each field to its renderer. Props: `scopeKey`, `selection`, `ctx`, plus optional
|
|
151
|
-
`readOnly`
|
|
221
|
+
`readOnly`, `emptyState`, `view`, `title` (see [Views](#views-inspector-card-collapsed)),
|
|
222
|
+
`labelLayout` and `labelWidth` (see [Label layout](#label-layout)), and `page`,
|
|
223
|
+
`onPageChange`, `advancedOpen` and `onAdvancedOpenChange` (see
|
|
224
|
+
[Pages and advanced groups](#pages-and-advanced-groups)).
|
|
152
225
|
|
|
153
226
|
`ctx` is yours. The package treats it as opaque and passes it through to every
|
|
154
227
|
`read`, `write`, renderer and options provider. Put your app's API in it.
|
|
155
228
|
|
|
229
|
+
### Drags as one undo step
|
|
230
|
+
|
|
231
|
+
A scrubbed number, a dragged slider and an open colour picker produce many
|
|
232
|
+
`write`s. Say where they begin and end and your undo stack can fold them into
|
|
233
|
+
one step: add `gesture` to the scope.
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
registerScope("blur", {
|
|
237
|
+
schema, read, write,
|
|
238
|
+
gesture: {
|
|
239
|
+
begin: (path, sel, ctx) => ctx.beginGesture(sel.id, path),
|
|
240
|
+
end: (path, sel, ctx) => ctx.endGesture(),
|
|
241
|
+
cancel: (path, sel, ctx) => ctx.cancelGesture(), // optional: Escape during a scrub
|
|
242
|
+
},
|
|
243
|
+
})
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Every `write` between `begin` and `end` belongs to the gesture (apply them
|
|
247
|
+
live, close one undo step at `end`). `cancel` fires instead of `end` when the
|
|
248
|
+
user abandons a scrub; put the value back. Typed values and arrow-key steps are
|
|
249
|
+
single writes outside any gesture. Omit `gesture` and drags are plain repeated
|
|
250
|
+
`write`s, as before. The renderers receive the same hooks as optional
|
|
251
|
+
`onGestureBegin` / `onGestureEnd` / `onGestureCancel` props, set only when the
|
|
252
|
+
scope declares `gesture`; a custom renderer with a continuous control can call
|
|
253
|
+
them too.
|
|
254
|
+
|
|
156
255
|
### How `path` resolves
|
|
157
256
|
|
|
158
257
|
`read()` returns a record, and `path` is a **key into that record** — a flat
|
|
@@ -169,19 +268,195 @@ Eleven ship built in: nine that hold a value, and two that only show something.
|
|
|
169
268
|
|---|---|---|
|
|
170
269
|
| `text` | string | `placeholder` |
|
|
171
270
|
| `textarea` | string | `placeholder`, `rows` |
|
|
172
|
-
| `number` | number | `min`, `max`, `step` |
|
|
173
|
-
| `slider` | number | same,
|
|
271
|
+
| `number` | number | `min`, `max`, `step`, `integer`, `suffix` (a unit, with `labelLayout`); drag the label to scrub (host `NumberField`) |
|
|
272
|
+
| `slider` | number | same, drawn by the host's `Slider` |
|
|
174
273
|
| `checkbox` | boolean | |
|
|
175
274
|
| `select` | string | `options`, or `optionsProvider(ctx)` for dynamic lists |
|
|
176
|
-
| `color` | hex string | `presets`;
|
|
275
|
+
| `color` | hex string | `presets`; the host's `ColorField` |
|
|
177
276
|
| `vector` | number array | N scalars in one row — Houdini's `float3` / `int2`. `components` sets arity and per-slot label, suffix and range |
|
|
178
|
-
| `file` |
|
|
277
|
+
| `file` | path string, or `null` | `recentKey`, `filters`, `title`, `placeholder`; editable on the host's `PathField`, see [The `file` field](#the-file-field) |
|
|
179
278
|
| `separator` | none | a horizontal rule, with `label` as an optional caption. No `path` |
|
|
180
279
|
| `label` | none | static text: a hint or a sub-heading. The text is `label`. No `path` |
|
|
181
280
|
|
|
281
|
+
### The `file` field
|
|
282
|
+
|
|
283
|
+
`file` is a path to a file, on ux-kit's `PathField`: a text field, a ▾ list of
|
|
284
|
+
recent paths and a browse icon. The value is the path **as written**, relative
|
|
285
|
+
or absolute (facets never rewrites it), or `null`. Typing commits on Return or
|
|
286
|
+
blur through the scope's `write`, never per keystroke; Esc reverts.
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
{ kind: "file", id: "texture", path: "texture", label: "Texture Path",
|
|
290
|
+
recentKey: "texture", filters: [{ name: "Images", extensions: ["png", "jpg", "webp"] }] }
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
`recentKey` names the field kind whose last paths the ▾ lists, across documents.
|
|
294
|
+
`filters` and `title` go to the Open dialog. `accept` (as on `<input>`) is
|
|
295
|
+
deprecated: its `.ext` entries become one "Files" filter when `filters` is absent.
|
|
296
|
+
Before 0.3 the value was a read-only `{ name }`; that still draws, read-only.
|
|
297
|
+
|
|
298
|
+
**Refusing a value.** The host's `write` is the validator. Return `{ error }`
|
|
299
|
+
(or a promise that resolves to it or rejects with a message) and the field is
|
|
300
|
+
marked invalid with the message under it, keeping the typed text. The message
|
|
301
|
+
shows while the field's value stays as it was and clears on the next write. Any
|
|
302
|
+
`write` can refuse this way; fields other than `file` receive the message as
|
|
303
|
+
the `error` prop and ignore it.
|
|
304
|
+
|
|
305
|
+
**The native parts come from the host.** facets depends on neither Tauri nor
|
|
306
|
+
app-kit. Register the handlers once at startup; every `file` field then works
|
|
307
|
+
the same way. Without them it is a plain editable text field.
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
import { registerFileHandlers } from "@preset.nz/facets"
|
|
311
|
+
import { pathFieldMenu, pathsExist, pickFile, recentPaths, rememberPath, revealPath } from "@preset.nz/app-kit"
|
|
312
|
+
|
|
313
|
+
registerFileHandlers({
|
|
314
|
+
recent: recentPaths, // (key) => Promise<string[]>: the ▾ list
|
|
315
|
+
remember: rememberPath, // (key, path): after the host accepted a commit
|
|
316
|
+
pick: pickFile, // ({ filters, title }) => Promise<string | null>: the browse icon
|
|
317
|
+
exists: pathsExist, // (paths, base?) => Promise<boolean[]>: Missing, and gone recents left out
|
|
318
|
+
reveal: revealPath, // (path, base?)
|
|
319
|
+
contextMenu: pathFieldMenu, // ({ missing, canClear }) => "reveal" | "clear" | null: a native menu
|
|
320
|
+
base: () => currentDocumentFolder(), // relative paths resolve against it
|
|
321
|
+
})
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Every member is optional. A path is remembered when the field's value becomes
|
|
325
|
+
the path that was committed, which is the first moment a host that validates
|
|
326
|
+
asynchronously has accepted it; a refused commit is never remembered. Missing
|
|
327
|
+
and the ▾ list are re-checked when the window regains focus. Right-click opens
|
|
328
|
+
the native menu (Reveal in Finder, Clear) unless the text is being edited, when
|
|
329
|
+
the native text menu stays.
|
|
330
|
+
|
|
331
|
+
Every field that holds a value takes `help` (from 0.3.0): a sentence or two
|
|
332
|
+
drawn under the control in the inspector, and left out of the card and the
|
|
333
|
+
collapsed view. `file`'s `helperText` is its deprecated name.
|
|
334
|
+
|
|
182
335
|
Groups take `collapsible: true` to fold under their title, and
|
|
183
336
|
`defaultCollapsed: true` to start folded. The open or closed state belongs to
|
|
184
|
-
the
|
|
337
|
+
the panel, so it resets when the panel remounts.
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Pages and advanced groups
|
|
342
|
+
|
|
343
|
+
A long panel splits into pages, drawn as tabs. Declare them at the root, in
|
|
344
|
+
order, and name one on every group:
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
const SETTINGS: PropertySchema = {
|
|
348
|
+
version: 1,
|
|
349
|
+
pages: [
|
|
350
|
+
{ id: "general", label: "General" },
|
|
351
|
+
{ id: "export", label: "Export" },
|
|
352
|
+
],
|
|
353
|
+
groups: [
|
|
354
|
+
{ id: "appearance", title: "Appearance", page: "general", rows: [/* … */] },
|
|
355
|
+
{ id: "performance", title: "Performance", page: "general", advanced: true, rows: [/* … */] },
|
|
356
|
+
{ id: "format", title: "Format", page: "export", rows: [/* … */] },
|
|
357
|
+
],
|
|
358
|
+
};
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
- **No `pages` is one page and no tab row**, exactly as before. One declared
|
|
362
|
+
page draws no tab row either.
|
|
363
|
+
- **Pages are presentation only.** A `path` never names a page, so moving a
|
|
364
|
+
group to another page changes no values.
|
|
365
|
+
- **`advanced: true`** puts a group behind one closed *Advanced* row at the foot
|
|
366
|
+
of its page, with or without pages. The row folds like a collapsible group.
|
|
367
|
+
- **Every page needs a group that isn't advanced.** With pages, every group
|
|
368
|
+
names a declared page; without, none does. `schemaProblems(schema)` lists
|
|
369
|
+
what breaks these rules, and `registerScope` throws on any of them, so a bad
|
|
370
|
+
schema fails where it is registered, not as an empty tab.
|
|
371
|
+
- **The card and collapsed views ignore pages and `advanced`.** They draw
|
|
372
|
+
promoted fields, wherever they sit.
|
|
373
|
+
|
|
374
|
+
The open page and each page's Advanced row are the panel's own state: the first
|
|
375
|
+
page, every row closed. A host that remembers them, as a Settings window does,
|
|
376
|
+
passes them in:
|
|
377
|
+
|
|
378
|
+
```tsx
|
|
379
|
+
<PropertyPanel
|
|
380
|
+
scopeKey="settings" selection={prefs} ctx={ctx}
|
|
381
|
+
page={tab} onPageChange={setTab}
|
|
382
|
+
advancedOpen={openRows} onAdvancedOpenChange={setOpenRows}
|
|
383
|
+
/>
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
`advancedOpen` lists the pages whose Advanced row is open. A schema without
|
|
387
|
+
`pages` has one page, whose id is `""`. A `page` the schema doesn't declare
|
|
388
|
+
shows the first page. Group folds and Advanced rows survive switching tabs.
|
|
389
|
+
|
|
390
|
+
### Changed marks
|
|
391
|
+
|
|
392
|
+
A scope may say which of its values count as changed (from their defaults,
|
|
393
|
+
say). The panel then marks anything that hides one: a tab other than the open
|
|
394
|
+
one, a closed Advanced row, a closed group. The mark is a dot, read by screen
|
|
395
|
+
readers as "changed".
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
registerScope("settings", {
|
|
399
|
+
schema: SETTINGS,
|
|
400
|
+
read: (sel, ctx) => ctx.prefs(),
|
|
401
|
+
write: (path, value, sel, ctx) => ctx.setPref(path, value),
|
|
402
|
+
changed: (sel, ctx) => ctx.changedPrefs(), // paths, e.g. ["theme", "cacheSize"]
|
|
403
|
+
});
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
The field itself carries no mark from facets; a label or renderer does that.
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## Views: inspector, card, collapsed
|
|
411
|
+
|
|
412
|
+
One scope draws three ways. `view` on `PropertyPanel` picks one:
|
|
413
|
+
|
|
414
|
+
- **`"inspector"`**, the default: every group and field.
|
|
415
|
+
- **`"card"`**: the fields promoted to the card, with no group chrome. The
|
|
416
|
+
schema's `title` heads it. The panel's `title` prop overrides that, for an
|
|
417
|
+
instance name such as "Filter 2".
|
|
418
|
+
- **`"collapsed"`**: the fields promoted to collapsed, one compact row each.
|
|
419
|
+
This is a scope folded shut. The host draws the title bar that folds it and
|
|
420
|
+
holds the open or closed state.
|
|
421
|
+
|
|
422
|
+
A field lists the views it shows in besides the inspector, the way a Houdini
|
|
423
|
+
asset promotes a parm to its interface:
|
|
424
|
+
|
|
425
|
+
```ts
|
|
426
|
+
const FILTER: PropertySchema = {
|
|
427
|
+
version: 1,
|
|
428
|
+
title: "Filter",
|
|
429
|
+
groups: [
|
|
430
|
+
{
|
|
431
|
+
id: "filter",
|
|
432
|
+
title: "Filter",
|
|
433
|
+
collapsible: true,
|
|
434
|
+
rows: [
|
|
435
|
+
{ kind: "select", id: "type", path: "type", options: TYPES, promote: ["collapsed"] },
|
|
436
|
+
{ kind: "slider", id: "master", path: "master", promote: ["card", "collapsed"] },
|
|
437
|
+
{ kind: "slider", id: "resonance", path: "resonance" },
|
|
438
|
+
{ kind: "slider", id: "pan", path: "pan", promote: ["card"] },
|
|
439
|
+
],
|
|
440
|
+
},
|
|
441
|
+
],
|
|
442
|
+
};
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
```tsx
|
|
446
|
+
<PropertyPanel scopeKey="filter" selection={node} ctx={ctx} view="card" title={node.name} />
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
The card shows master and pan. Folded, the scope shows type and master.
|
|
450
|
+
Resonance is inspector-only. The views are independent: `["collapsed"]` is not
|
|
451
|
+
on the card.
|
|
452
|
+
|
|
453
|
+
A closed collapsible group shows its `collapsed` fields under its title, so
|
|
454
|
+
folding a group still leaves its main value in reach.
|
|
455
|
+
|
|
456
|
+
Renderers receive the view as an optional `view` prop. The built-ins draw a
|
|
457
|
+
compact row in `"collapsed"`. A custom renderer may ignore it.
|
|
458
|
+
`promotedFields(schema, view)` returns the fields a view draws, in schema
|
|
459
|
+
order, for a host that wants to skip an empty card.
|
|
185
460
|
|
|
186
461
|
---
|
|
187
462
|
|
|
@@ -207,6 +482,17 @@ code, so it has no JSON form.
|
|
|
207
482
|
One file covers every 0.1.x release. Patches may add kinds and props to it,
|
|
208
483
|
never remove or tighten them.
|
|
209
484
|
|
|
485
|
+
### For agents
|
|
486
|
+
|
|
487
|
+
`https://facets.preset.nz/llms.txt` is the contract in brief. A Claude Code
|
|
488
|
+
plugin with a skill for writing schemas, scopes and custom renderers lives in
|
|
489
|
+
this repo:
|
|
490
|
+
|
|
491
|
+
```sh
|
|
492
|
+
claude plugin marketplace add preset-nz/packages
|
|
493
|
+
claude plugin install facets@preset-nz-packages
|
|
494
|
+
```
|
|
495
|
+
|
|
210
496
|
---
|
|
211
497
|
|
|
212
498
|
## Read-only
|
|
@@ -232,6 +518,16 @@ The condition reads one other field's value. Set one operator:
|
|
|
232
518
|
|
|
233
519
|
If several are set, the first in that order wins.
|
|
234
520
|
|
|
521
|
+
A list of conditions disables the field when any of them holds, e.g. a width
|
|
522
|
+
that a hairline or a non-solid line both make moot:
|
|
523
|
+
|
|
524
|
+
```ts
|
|
525
|
+
disabledWhen: [
|
|
526
|
+
{ path: "hairline", equals: true },
|
|
527
|
+
{ path: "lineStyle", notEquals: "solid" },
|
|
528
|
+
]
|
|
529
|
+
```
|
|
530
|
+
|
|
235
531
|
---
|
|
236
532
|
|
|
237
533
|
## Custom field kinds
|
|
@@ -246,6 +542,10 @@ registerFieldRenderer("palette-swatches", ({ field, value, onChange, ctx }) => (
|
|
|
246
542
|
));
|
|
247
543
|
```
|
|
248
544
|
|
|
545
|
+
A renderer receives `field`, `value`, `disabled`, `onChange`, `ctx` and `view`.
|
|
546
|
+
Pick what it uses rather than spreading the props onto a DOM element, where
|
|
547
|
+
React warns about the unknown ones.
|
|
548
|
+
|
|
249
549
|
A field whose kind is not built in may carry any extra props it likes; the
|
|
250
550
|
renderer reads what it needs. **The type system will not check those props**, so
|
|
251
551
|
document them next to the renderer.
|
|
@@ -260,8 +560,11 @@ it does not take the rest of the panel down with it.
|
|
|
260
560
|
- **Registration is explicit.** `registerBuiltinRenderers()` is a call, not an
|
|
261
561
|
import side effect, and the package declares `sideEffects: false`. Call it
|
|
262
562
|
before rendering a panel.
|
|
263
|
-
- **Failures stay local.** A throwing `read` is
|
|
264
|
-
scope renders `emptyState`; an unknown kind
|
|
563
|
+
- **Failures stay local, except a bad page layout.** A throwing `read` is
|
|
564
|
+
caught and logged; an unknown scope renders `emptyState`; an unknown kind
|
|
565
|
+
skips that field. A schema whose pages don't add up throws from
|
|
566
|
+
`registerScope` (see [Pages](#pages-and-advanced-groups)), so the mistake
|
|
567
|
+
shows where the schema is registered rather than as an empty tab later.
|
|
265
568
|
- **Schemas are serialisable.** Keep them that way — no closures in a
|
|
266
569
|
`FieldDef` beyond `optionsProvider`. It keeps schemas versionable and lets them
|
|
267
570
|
cross a plugin boundary later.
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preset.nz/facets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Schema-as-data property/inspector panels for React: FieldDef schemas, a scope registry, an open field-renderer registry, and a generic PropertyPanel.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://facets.preset.nz",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/preset-nz/
|
|
9
|
+
"url": "git+https://github.com/preset-nz/packages.git",
|
|
10
|
+
"directory": "facets"
|
|
10
11
|
},
|
|
11
12
|
"type": "module",
|
|
12
13
|
"sideEffects": false,
|
|
@@ -25,8 +26,10 @@
|
|
|
25
26
|
"react": "^19"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
29
|
+
"@biomejs/biome": "^2.5.15",
|
|
28
30
|
"@types/react": "^19",
|
|
29
31
|
"ajv": "^8.20.0",
|
|
30
|
-
"
|
|
32
|
+
"ts5": "npm:typescript@5.9.3",
|
|
33
|
+
"typescript": "^7.0.2"
|
|
31
34
|
}
|
|
32
35
|
}
|