@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/README.md
CHANGED
|
@@ -47,6 +47,8 @@ the conventional shadcn path, and your bundler's `@` alias resolves it:
|
|
|
47
47
|
| `@/components/ui/number-field` | `NumberField` |
|
|
48
48
|
| `@/components/ui/slider` | `Slider` |
|
|
49
49
|
| `@/components/ui/color-field` | `ColorField` |
|
|
50
|
+
| `@/components/ui/path-field` | `PathField` (only for `file` fields) |
|
|
51
|
+
| `@/components/ui/tabs` | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` |
|
|
50
52
|
|
|
51
53
|
The last three are what make a panel feel like the rest of your app: a number
|
|
52
54
|
you drag, a square slider, your colour picker. They are the **minimum** facets
|
|
@@ -68,7 +70,18 @@ reads (typed in `dev/ui/` in this repo):
|
|
|
68
70
|
`presets`, `disabled`, `onPickStart` / `onPickEnd`. No `onChange` means
|
|
69
71
|
read-only.
|
|
70
72
|
|
|
71
|
-
|
|
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,
|
|
72
85
|
and Tailwind with shadcn's theme tokens (`muted-foreground`, `border` and
|
|
73
86
|
friends are used throughout).
|
|
74
87
|
|
|
@@ -206,7 +219,9 @@ Built-ins register through the same call you would use, with no special casing.
|
|
|
206
219
|
**`PropertyPanel`** — looks up the scope, calls `read`, walks the groups, hands
|
|
207
220
|
each field to its renderer. Props: `scopeKey`, `selection`, `ctx`, plus optional
|
|
208
221
|
`readOnly`, `emptyState`, `view`, `title` (see [Views](#views-inspector-card-collapsed)),
|
|
209
|
-
`labelLayout` and `labelWidth` (see [Label layout](#label-layout))
|
|
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)).
|
|
210
225
|
|
|
211
226
|
`ctx` is yours. The package treats it as opaque and passes it through to every
|
|
212
227
|
`read`, `write`, renderer and options provider. Put your app's API in it.
|
|
@@ -259,13 +274,136 @@ Eleven ship built in: nine that hold a value, and two that only show something.
|
|
|
259
274
|
| `select` | string | `options`, or `optionsProvider(ctx)` for dynamic lists |
|
|
260
275
|
| `color` | hex string | `presets`; the host's `ColorField` |
|
|
261
276
|
| `vector` | number array | N scalars in one row — Houdini's `float3` / `int2`. `components` sets arity and per-slot label, suffix and range |
|
|
262
|
-
| `file` |
|
|
277
|
+
| `file` | path string, or `null` | `recentKey`, `filters`, `title`, `placeholder`; editable on the host's `PathField`, see [The `file` field](#the-file-field) |
|
|
263
278
|
| `separator` | none | a horizontal rule, with `label` as an optional caption. No `path` |
|
|
264
279
|
| `label` | none | static text: a hint or a sub-heading. The text is `label`. No `path` |
|
|
265
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
|
+
|
|
266
335
|
Groups take `collapsible: true` to fold under their title, and
|
|
267
336
|
`defaultCollapsed: true` to start folded. The open or closed state belongs to
|
|
268
|
-
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.
|
|
269
407
|
|
|
270
408
|
---
|
|
271
409
|
|
|
@@ -351,8 +489,8 @@ plugin with a skill for writing schemas, scopes and custom renderers lives in
|
|
|
351
489
|
this repo:
|
|
352
490
|
|
|
353
491
|
```sh
|
|
354
|
-
claude plugin marketplace add preset-nz/
|
|
355
|
-
claude plugin install facets@preset-nz-
|
|
492
|
+
claude plugin marketplace add preset-nz/packages
|
|
493
|
+
claude plugin install facets@preset-nz-packages
|
|
356
494
|
```
|
|
357
495
|
|
|
358
496
|
---
|
|
@@ -380,6 +518,16 @@ The condition reads one other field's value. Set one operator:
|
|
|
380
518
|
|
|
381
519
|
If several are set, the first in that order wins.
|
|
382
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
|
+
|
|
383
531
|
---
|
|
384
532
|
|
|
385
533
|
## Custom field kinds
|
|
@@ -412,8 +560,11 @@ it does not take the rest of the panel down with it.
|
|
|
412
560
|
- **Registration is explicit.** `registerBuiltinRenderers()` is a call, not an
|
|
413
561
|
import side effect, and the package declares `sideEffects: false`. Call it
|
|
414
562
|
before rendering a panel.
|
|
415
|
-
- **Failures stay local.** A throwing `read` is
|
|
416
|
-
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.
|
|
417
568
|
- **Schemas are serialisable.** Keep them that way — no closures in a
|
|
418
569
|
`FieldDef` beyond `optionsProvider`. It keeps schemas versionable and lets them
|
|
419
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
|
}
|