@preset.nz/facets 0.1.3 → 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/README.md +159 -7
- package/package.json +1 -1
- package/schema/v0.1.json +59 -0
- package/src/field-renderers.tsx +232 -111
- package/src/index.ts +3 -2
- package/src/layout.ts +37 -0
- package/src/panel.tsx +206 -25
- package/src/types.ts +62 -0
package/README.md
CHANGED
|
@@ -44,8 +44,31 @@ 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
|
+
|
|
51
|
+
The last three are what make a panel feel like the rest of your app: a number
|
|
52
|
+
you drag, a square slider, your colour picker. They are the **minimum** facets
|
|
53
|
+
reads (typed in `dev/ui/` in this repo):
|
|
54
|
+
|
|
55
|
+
- `NumberField`: `value: number`, `onValueChange(value, { reason })`, `min`,
|
|
56
|
+
`max`, `step`, `integer`, `precision`, `label` (the scrub handle: the field
|
|
57
|
+
draws its own label, so the panel adds none), `disabled`, and
|
|
58
|
+
`onScrubStart` / `onScrubEnd` / `onScrubCancel`. Facets uses it for `number`
|
|
59
|
+
and for each slot of `vector`. A vector slot's `suffix` shows in read-only
|
|
60
|
+
mode only, unless the panel sets `labelLayout` (below). Two **optional** props
|
|
61
|
+
serve that: `labelPlacement` (`"inside"` default, `"column"`, `"above"`) and
|
|
62
|
+
`suffix` (a muted unit inside the box). A host that ignores them keeps the
|
|
63
|
+
label inside the box, so nothing doubles.
|
|
64
|
+
- `Slider`: `value: number`, `onValueChange(value)` (a number or an array, facets
|
|
65
|
+
takes the first), `onValueCommitted()`, `min`, `max`, `step`, `disabled`,
|
|
66
|
+
`aria-label`. One thumb.
|
|
67
|
+
- `ColorField`: `value: string | null`, `onChange(value | null)`, `label`,
|
|
68
|
+
`presets`, `disabled`, `onPickStart` / `onPickEnd`. No `onChange` means
|
|
69
|
+
read-only.
|
|
70
|
+
|
|
71
|
+
So you need an `@/*` alias pointing at your `src/`, those eight modules present,
|
|
49
72
|
and Tailwind with shadcn's theme tokens (`muted-foreground`, `border` and
|
|
50
73
|
friends are used throughout).
|
|
51
74
|
|
|
@@ -61,6 +84,18 @@ package in your CSS. From `src/index.css`:
|
|
|
61
84
|
Nothing fails when this line is missing. The build succeeds and the panel
|
|
62
85
|
renders unstyled.
|
|
63
86
|
|
|
87
|
+
Tell Vite to resolve one copy of React and of this package, so the package and
|
|
88
|
+
your app share one registry. In `vite.config.ts`:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
resolve: {
|
|
92
|
+
dedupe: ["react", "react-dom", "@preset.nz/facets"],
|
|
93
|
+
},
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Without it, a linked or nested install can register renderers in one copy and
|
|
97
|
+
look them up in another.
|
|
98
|
+
|
|
64
99
|
The upside is that `facets` inherits whatever primitive library you already
|
|
65
100
|
chose — Base UI, Radix, or your own — instead of dragging in a second one and
|
|
66
101
|
making your panels look foreign inside your own app.
|
|
@@ -122,7 +157,29 @@ Select a blur and the blur panel appears. Add a new filter type later by
|
|
|
122
157
|
registering another scope — the panel does not change.
|
|
123
158
|
|
|
124
159
|
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
|
|
160
|
+
width. That is the whole layout system, on purpose, with one switch: where the
|
|
161
|
+
labels go.
|
|
162
|
+
|
|
163
|
+
### Label layout
|
|
164
|
+
|
|
165
|
+
`labelLayout` on `PropertyPanel` (inspector and card views; collapsed ignores it):
|
|
166
|
+
|
|
167
|
+
| Value | Labels | Numbers |
|
|
168
|
+
|---|---|---|
|
|
169
|
+
| `"auto"` (default) | Above their control; a checkbox's beside it | The label is the scrub handle, inside the box |
|
|
170
|
+
| `"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 |
|
|
171
|
+
| `"stacked"` | All above their control | Label above the box, `suffix` as a unit |
|
|
172
|
+
|
|
173
|
+
In `"column"` the label column's width is one CSS variable on the panel root
|
|
174
|
+
(`--facets-label-w`), set from the longest label (8ch to 16ch) or from the
|
|
175
|
+
`labelWidth` prop. A vector keeps its axis labels inside its slots, so its
|
|
176
|
+
fields share the column the scalars start on. A paired row (`[a, b]`) draws one
|
|
177
|
+
field per row, and a slider shows its value to the right. A custom renderer
|
|
178
|
+
follows the layout by wrapping its control in `FieldShell` (the built-ins'
|
|
179
|
+
shell; `top` aligns the label with the top of a tall control, `above` keeps it
|
|
180
|
+
above in every layout) and a read-only value in `ReadOnlyText`. For anything
|
|
181
|
+
else, `useLabelLayout()` and `COLUMN_ROW` give the parts. A renderer that draws
|
|
182
|
+
its own label ignores the layout.
|
|
126
183
|
|
|
127
184
|
---
|
|
128
185
|
|
|
@@ -148,11 +205,38 @@ Built-ins register through the same call you would use, with no special casing.
|
|
|
148
205
|
|
|
149
206
|
**`PropertyPanel`** — looks up the scope, calls `read`, walks the groups, hands
|
|
150
207
|
each field to its renderer. Props: `scopeKey`, `selection`, `ctx`, plus optional
|
|
151
|
-
`readOnly`
|
|
208
|
+
`readOnly`, `emptyState`, `view`, `title` (see [Views](#views-inspector-card-collapsed)),
|
|
209
|
+
`labelLayout` and `labelWidth` (see [Label layout](#label-layout)).
|
|
152
210
|
|
|
153
211
|
`ctx` is yours. The package treats it as opaque and passes it through to every
|
|
154
212
|
`read`, `write`, renderer and options provider. Put your app's API in it.
|
|
155
213
|
|
|
214
|
+
### Drags as one undo step
|
|
215
|
+
|
|
216
|
+
A scrubbed number, a dragged slider and an open colour picker produce many
|
|
217
|
+
`write`s. Say where they begin and end and your undo stack can fold them into
|
|
218
|
+
one step: add `gesture` to the scope.
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
registerScope("blur", {
|
|
222
|
+
schema, read, write,
|
|
223
|
+
gesture: {
|
|
224
|
+
begin: (path, sel, ctx) => ctx.beginGesture(sel.id, path),
|
|
225
|
+
end: (path, sel, ctx) => ctx.endGesture(),
|
|
226
|
+
cancel: (path, sel, ctx) => ctx.cancelGesture(), // optional: Escape during a scrub
|
|
227
|
+
},
|
|
228
|
+
})
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Every `write` between `begin` and `end` belongs to the gesture (apply them
|
|
232
|
+
live, close one undo step at `end`). `cancel` fires instead of `end` when the
|
|
233
|
+
user abandons a scrub; put the value back. Typed values and arrow-key steps are
|
|
234
|
+
single writes outside any gesture. Omit `gesture` and drags are plain repeated
|
|
235
|
+
`write`s, as before. The renderers receive the same hooks as optional
|
|
236
|
+
`onGestureBegin` / `onGestureEnd` / `onGestureCancel` props, set only when the
|
|
237
|
+
scope declares `gesture`; a custom renderer with a continuous control can call
|
|
238
|
+
them too.
|
|
239
|
+
|
|
156
240
|
### How `path` resolves
|
|
157
241
|
|
|
158
242
|
`read()` returns a record, and `path` is a **key into that record** — a flat
|
|
@@ -169,11 +253,11 @@ Eleven ship built in: nine that hold a value, and two that only show something.
|
|
|
169
253
|
|---|---|---|
|
|
170
254
|
| `text` | string | `placeholder` |
|
|
171
255
|
| `textarea` | string | `placeholder`, `rows` |
|
|
172
|
-
| `number` | number | `min`, `max`, `step` |
|
|
173
|
-
| `slider` | number | same,
|
|
256
|
+
| `number` | number | `min`, `max`, `step`, `integer`, `suffix` (a unit, with `labelLayout`); drag the label to scrub (host `NumberField`) |
|
|
257
|
+
| `slider` | number | same, drawn by the host's `Slider` |
|
|
174
258
|
| `checkbox` | boolean | |
|
|
175
259
|
| `select` | string | `options`, or `optionsProvider(ctx)` for dynamic lists |
|
|
176
|
-
| `color` | hex string | `presets`;
|
|
260
|
+
| `color` | hex string | `presets`; the host's `ColorField` |
|
|
177
261
|
| `vector` | number array | N scalars in one row — Houdini's `float3` / `int2`. `components` sets arity and per-slot label, suffix and range |
|
|
178
262
|
| `file` | `{ name }` | display only |
|
|
179
263
|
| `separator` | none | a horizontal rule, with `label` as an optional caption. No `path` |
|
|
@@ -185,6 +269,59 @@ the group, so it resets when the panel remounts.
|
|
|
185
269
|
|
|
186
270
|
---
|
|
187
271
|
|
|
272
|
+
## Views: inspector, card, collapsed
|
|
273
|
+
|
|
274
|
+
One scope draws three ways. `view` on `PropertyPanel` picks one:
|
|
275
|
+
|
|
276
|
+
- **`"inspector"`**, the default: every group and field.
|
|
277
|
+
- **`"card"`**: the fields promoted to the card, with no group chrome. The
|
|
278
|
+
schema's `title` heads it. The panel's `title` prop overrides that, for an
|
|
279
|
+
instance name such as "Filter 2".
|
|
280
|
+
- **`"collapsed"`**: the fields promoted to collapsed, one compact row each.
|
|
281
|
+
This is a scope folded shut. The host draws the title bar that folds it and
|
|
282
|
+
holds the open or closed state.
|
|
283
|
+
|
|
284
|
+
A field lists the views it shows in besides the inspector, the way a Houdini
|
|
285
|
+
asset promotes a parm to its interface:
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
const FILTER: PropertySchema = {
|
|
289
|
+
version: 1,
|
|
290
|
+
title: "Filter",
|
|
291
|
+
groups: [
|
|
292
|
+
{
|
|
293
|
+
id: "filter",
|
|
294
|
+
title: "Filter",
|
|
295
|
+
collapsible: true,
|
|
296
|
+
rows: [
|
|
297
|
+
{ kind: "select", id: "type", path: "type", options: TYPES, promote: ["collapsed"] },
|
|
298
|
+
{ kind: "slider", id: "master", path: "master", promote: ["card", "collapsed"] },
|
|
299
|
+
{ kind: "slider", id: "resonance", path: "resonance" },
|
|
300
|
+
{ kind: "slider", id: "pan", path: "pan", promote: ["card"] },
|
|
301
|
+
],
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
};
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
```tsx
|
|
308
|
+
<PropertyPanel scopeKey="filter" selection={node} ctx={ctx} view="card" title={node.name} />
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
The card shows master and pan. Folded, the scope shows type and master.
|
|
312
|
+
Resonance is inspector-only. The views are independent: `["collapsed"]` is not
|
|
313
|
+
on the card.
|
|
314
|
+
|
|
315
|
+
A closed collapsible group shows its `collapsed` fields under its title, so
|
|
316
|
+
folding a group still leaves its main value in reach.
|
|
317
|
+
|
|
318
|
+
Renderers receive the view as an optional `view` prop. The built-ins draw a
|
|
319
|
+
compact row in `"collapsed"`. A custom renderer may ignore it.
|
|
320
|
+
`promotedFields(schema, view)` returns the fields a view draws, in schema
|
|
321
|
+
order, for a host that wants to skip an empty card.
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
188
325
|
## JSON Schema
|
|
189
326
|
|
|
190
327
|
A schema for editors and agents is published at
|
|
@@ -207,6 +344,17 @@ code, so it has no JSON form.
|
|
|
207
344
|
One file covers every 0.1.x release. Patches may add kinds and props to it,
|
|
208
345
|
never remove or tighten them.
|
|
209
346
|
|
|
347
|
+
### For agents
|
|
348
|
+
|
|
349
|
+
`https://facets.preset.nz/llms.txt` is the contract in brief. A Claude Code
|
|
350
|
+
plugin with a skill for writing schemas, scopes and custom renderers lives in
|
|
351
|
+
this repo:
|
|
352
|
+
|
|
353
|
+
```sh
|
|
354
|
+
claude plugin marketplace add preset-nz/facets
|
|
355
|
+
claude plugin install facets@preset-nz-facets
|
|
356
|
+
```
|
|
357
|
+
|
|
210
358
|
---
|
|
211
359
|
|
|
212
360
|
## Read-only
|
|
@@ -246,6 +394,10 @@ registerFieldRenderer("palette-swatches", ({ field, value, onChange, ctx }) => (
|
|
|
246
394
|
));
|
|
247
395
|
```
|
|
248
396
|
|
|
397
|
+
A renderer receives `field`, `value`, `disabled`, `onChange`, `ctx` and `view`.
|
|
398
|
+
Pick what it uses rather than spreading the props onto a DOM element, where
|
|
399
|
+
React warns about the unknown ones.
|
|
400
|
+
|
|
249
401
|
A field whose kind is not built in may carry any extra props it likes; the
|
|
250
402
|
renderer reads what it needs. **The type system will not check those props**, so
|
|
251
403
|
document them next to the renderer.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preset.nz/facets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|
package/schema/v0.1.json
CHANGED
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
"type": "number",
|
|
19
19
|
"description": "The host's own version of this schema. Bump it when the shape changes."
|
|
20
20
|
},
|
|
21
|
+
"title": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "What the scope is called, e.g. \"Filter\". Heads the card; the host can override it."
|
|
24
|
+
},
|
|
21
25
|
"groups": {
|
|
22
26
|
"type": "array",
|
|
23
27
|
"description": "Rendered top to bottom, with a rule between groups.",
|
|
@@ -102,6 +106,17 @@
|
|
|
102
106
|
}
|
|
103
107
|
}
|
|
104
108
|
},
|
|
109
|
+
"promote": {
|
|
110
|
+
"type": "array",
|
|
111
|
+
"description": "The views the field shows in besides the inspector, like a parm promoted to a Houdini asset's interface. \"card\": on the card. \"collapsed\": in a closed group's header and in a folded scope. Omit for inspector-only.",
|
|
112
|
+
"items": {
|
|
113
|
+
"enum": [
|
|
114
|
+
"card",
|
|
115
|
+
"collapsed"
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
"uniqueItems": true
|
|
119
|
+
},
|
|
105
120
|
"selectOption": {
|
|
106
121
|
"type": "object",
|
|
107
122
|
"required": [
|
|
@@ -403,6 +418,9 @@
|
|
|
403
418
|
"placeholder": {
|
|
404
419
|
"type": "string",
|
|
405
420
|
"description": "Shown while empty."
|
|
421
|
+
},
|
|
422
|
+
"promote": {
|
|
423
|
+
"$ref": "#/definitions/promote"
|
|
406
424
|
}
|
|
407
425
|
},
|
|
408
426
|
"additionalProperties": false
|
|
@@ -441,6 +459,9 @@
|
|
|
441
459
|
"rows": {
|
|
442
460
|
"type": "number",
|
|
443
461
|
"description": "Visible lines. Default 3."
|
|
462
|
+
},
|
|
463
|
+
"promote": {
|
|
464
|
+
"$ref": "#/definitions/promote"
|
|
444
465
|
}
|
|
445
466
|
},
|
|
446
467
|
"additionalProperties": false
|
|
@@ -483,6 +504,17 @@
|
|
|
483
504
|
"step": {
|
|
484
505
|
"type": "number",
|
|
485
506
|
"description": "Increment. Default 1."
|
|
507
|
+
},
|
|
508
|
+
"integer": {
|
|
509
|
+
"type": "boolean",
|
|
510
|
+
"description": "Whole numbers only: no fractional display, and a fine-step modifier never goes finer than step."
|
|
511
|
+
},
|
|
512
|
+
"suffix": {
|
|
513
|
+
"type": "string",
|
|
514
|
+
"description": "Unit after the number, e.g. px or °. Shown when the panel sets labelLayout to column or stacked."
|
|
515
|
+
},
|
|
516
|
+
"promote": {
|
|
517
|
+
"$ref": "#/definitions/promote"
|
|
486
518
|
}
|
|
487
519
|
},
|
|
488
520
|
"additionalProperties": false
|
|
@@ -525,6 +557,9 @@
|
|
|
525
557
|
"step": {
|
|
526
558
|
"type": "number",
|
|
527
559
|
"description": "Increment. Default 1."
|
|
560
|
+
},
|
|
561
|
+
"promote": {
|
|
562
|
+
"$ref": "#/definitions/promote"
|
|
528
563
|
}
|
|
529
564
|
},
|
|
530
565
|
"additionalProperties": false
|
|
@@ -562,6 +597,9 @@
|
|
|
562
597
|
"items": {
|
|
563
598
|
"$ref": "#/definitions/selectOption"
|
|
564
599
|
}
|
|
600
|
+
},
|
|
601
|
+
"promote": {
|
|
602
|
+
"$ref": "#/definitions/promote"
|
|
565
603
|
}
|
|
566
604
|
},
|
|
567
605
|
"additionalProperties": false
|
|
@@ -599,6 +637,9 @@
|
|
|
599
637
|
"type": "string"
|
|
600
638
|
},
|
|
601
639
|
"description": "Hex strings. Declared in 0.1; the built-in renderer does not draw them."
|
|
640
|
+
},
|
|
641
|
+
"promote": {
|
|
642
|
+
"$ref": "#/definitions/promote"
|
|
602
643
|
}
|
|
603
644
|
},
|
|
604
645
|
"additionalProperties": false
|
|
@@ -629,6 +670,9 @@
|
|
|
629
670
|
},
|
|
630
671
|
"disabledWhen": {
|
|
631
672
|
"$ref": "#/definitions/disabledWhen"
|
|
673
|
+
},
|
|
674
|
+
"promote": {
|
|
675
|
+
"$ref": "#/definitions/promote"
|
|
632
676
|
}
|
|
633
677
|
},
|
|
634
678
|
"additionalProperties": false
|
|
@@ -667,6 +711,9 @@
|
|
|
667
711
|
"helperText": {
|
|
668
712
|
"type": "string",
|
|
669
713
|
"description": "A hint. Declared in 0.1; the built-in renderer does not draw it."
|
|
714
|
+
},
|
|
715
|
+
"promote": {
|
|
716
|
+
"$ref": "#/definitions/promote"
|
|
670
717
|
}
|
|
671
718
|
},
|
|
672
719
|
"additionalProperties": false
|
|
@@ -713,6 +760,9 @@
|
|
|
713
760
|
"precision": {
|
|
714
761
|
"type": "number",
|
|
715
762
|
"description": "Decimals shown when read-only."
|
|
763
|
+
},
|
|
764
|
+
"promote": {
|
|
765
|
+
"$ref": "#/definitions/promote"
|
|
716
766
|
}
|
|
717
767
|
},
|
|
718
768
|
"additionalProperties": false
|
|
@@ -738,6 +788,9 @@
|
|
|
738
788
|
},
|
|
739
789
|
"disabledWhen": {
|
|
740
790
|
"$ref": "#/definitions/disabledWhen"
|
|
791
|
+
},
|
|
792
|
+
"promote": {
|
|
793
|
+
"$ref": "#/definitions/promote"
|
|
741
794
|
}
|
|
742
795
|
},
|
|
743
796
|
"additionalProperties": false
|
|
@@ -764,6 +817,9 @@
|
|
|
764
817
|
},
|
|
765
818
|
"disabledWhen": {
|
|
766
819
|
"$ref": "#/definitions/disabledWhen"
|
|
820
|
+
},
|
|
821
|
+
"promote": {
|
|
822
|
+
"$ref": "#/definitions/promote"
|
|
767
823
|
}
|
|
768
824
|
},
|
|
769
825
|
"additionalProperties": false
|
|
@@ -794,6 +850,9 @@
|
|
|
794
850
|
},
|
|
795
851
|
"disabledWhen": {
|
|
796
852
|
"$ref": "#/definitions/disabledWhen"
|
|
853
|
+
},
|
|
854
|
+
"promote": {
|
|
855
|
+
"$ref": "#/definitions/promote"
|
|
797
856
|
}
|
|
798
857
|
}
|
|
799
858
|
}
|