@preset.nz/facets 0.1.1 → 0.1.2

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 CHANGED
@@ -14,6 +14,10 @@ switch statement over your types.
14
14
  Built for image and map editors, where the same panel has to render a layer, a
15
15
  filter, a document or a tool depending on what is selected.
16
16
 
17
+ The playground at [facets.preset.nz](https://facets.preset.nz) shows the field
18
+ kinds, the schema as JSON, and the panel it draws, side by side. Its source is
19
+ in `playground/`.
20
+
17
21
  ---
18
22
 
19
23
  ## Installing
@@ -159,7 +163,7 @@ keep the mapping inside `read`, where your domain types are still in scope.
159
163
 
160
164
  ## Field kinds
161
165
 
162
- Nine ship built in.
166
+ Eleven ship built in: nine that hold a value, and two that only show something.
163
167
 
164
168
  | Kind | Value | Notes |
165
169
  |---|---|---|
@@ -172,6 +176,12 @@ Nine ship built in.
172
176
  | `color` | hex string | `presets`; shows a swatch |
173
177
  | `vector` | number array | N scalars in one row — Houdini's `float3` / `int2`. `components` sets arity and per-slot label, suffix and range |
174
178
  | `file` | `{ name }` | display only |
179
+ | `separator` | none | a horizontal rule, with `label` as an optional caption. No `path` |
180
+ | `label` | none | static text: a hint or a sub-heading. The text is `label`. No `path` |
181
+
182
+ Groups take `collapsible: true` to fold under their title, and
183
+ `defaultCollapsed: true` to start folded. The open or closed state belongs to
184
+ the group, so it resets when the panel remounts.
175
185
 
176
186
  ---
177
187
 
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@preset.nz/facets",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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
+ "homepage": "https://facets.preset.nz",
6
7
  "repository": {
7
8
  "type": "git",
8
9
  "url": "git+https://github.com/preset-nz/facets.git"
@@ -14,6 +14,7 @@ import {
14
14
  SelectValue,
15
15
  } from "@/components/ui/select"
16
16
  import { Checkbox } from "@/components/ui/checkbox"
17
+ import { Separator } from "@/components/ui/separator"
17
18
  import { registerFieldRenderer } from "./registry"
18
19
  import type {
19
20
  CheckboxFieldDef,
@@ -26,6 +27,8 @@ import type {
26
27
  TextFieldDef,
27
28
  TextareaFieldDef,
28
29
  FileFieldDef,
30
+ LabelFieldDef,
31
+ SeparatorFieldDef,
29
32
  VectorFieldDef,
30
33
  } from "./types"
31
34
 
@@ -226,7 +229,9 @@ const SelectRenderer: FieldRenderer<SelectFieldDef> = ({
226
229
  disabled={disabled}
227
230
  >
228
231
  <SelectTrigger>
229
- <SelectValue />
232
+ {/* Children, not the primitive's own lookup: Base UI's Value shows
233
+ the raw value unless it's told the label. */}
234
+ <SelectValue>{matched ? matched.label : current}</SelectValue>
230
235
  </SelectTrigger>
231
236
  <SelectContent>
232
237
  {options.map((o) => (
@@ -425,7 +430,39 @@ const VectorRenderer: FieldRenderer<VectorFieldDef> = ({
425
430
  )
426
431
  }
427
432
 
433
+ const SeparatorRenderer: FieldRenderer<SeparatorFieldDef> = ({ field }) =>
434
+ field.label ? (
435
+ <div className="my-1 flex items-center gap-2">
436
+ <span className="text-[10px] uppercase tracking-wide text-muted-foreground">
437
+ {field.label}
438
+ </span>
439
+ <Separator className="flex-1" />
440
+ </div>
441
+ ) : (
442
+ <Separator className="my-1" />
443
+ )
444
+
445
+ // The same in every mode: there is nothing to edit. Greys out under
446
+ // disabledWhen so a hint can follow the field it describes.
447
+ const LabelRenderer: FieldRenderer<LabelFieldDef> = ({ field, disabled }) => (
448
+ <p
449
+ className={
450
+ "text-xs text-muted-foreground" + (disabled ? " opacity-50" : "")
451
+ }
452
+ >
453
+ {field.label}
454
+ </p>
455
+ )
456
+
428
457
  export function registerBuiltinRenderers(): void {
458
+ registerFieldRenderer(
459
+ "separator",
460
+ SeparatorRenderer as FieldRenderer<import("./types").FieldDef>,
461
+ )
462
+ registerFieldRenderer(
463
+ "label",
464
+ LabelRenderer as FieldRenderer<import("./types").FieldDef>,
465
+ )
429
466
  registerFieldRenderer(
430
467
  "text",
431
468
  TextRenderer as FieldRenderer<import("./types").FieldDef>,
package/src/panel.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useMemo } from "react"
1
+ import { useId, useMemo, useState } from "react"
2
2
  import { Separator } from "@/components/ui/separator"
3
3
  import { getFieldRenderer, getScope } from "./registry"
4
4
  import type {
@@ -69,23 +69,54 @@ function PropertyGroup({
69
69
  onChange?: (path: string, val: unknown) => void
70
70
  isLast: boolean
71
71
  }) {
72
+ // Collapsing needs a title to click. Open or closed is the group's own
73
+ // state; a host that wants it remembered across selections keys the panel.
74
+ const collapsible = Boolean(group.collapsible && group.title)
75
+ const [collapsed, setCollapsed] = useState(
76
+ collapsible && Boolean(group.defaultCollapsed),
77
+ )
78
+ const bodyId = useId()
79
+ const titleClass =
80
+ "text-[11px] font-semibold uppercase tracking-wider text-muted-foreground"
81
+
72
82
  return (
73
83
  <section className="flex flex-col gap-3 px-3 py-3">
74
84
  {(group.title || group.description) && (
75
85
  <header>
76
- {group.title && (
77
- <h3 className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
78
- {group.title}
79
- </h3>
80
- )}
81
- {group.description && (
86
+ {group.title &&
87
+ (collapsible ? (
88
+ <h3 className={titleClass}>
89
+ <button
90
+ type="button"
91
+ aria-expanded={!collapsed}
92
+ aria-controls={bodyId}
93
+ onClick={() => setCollapsed((c) => !c)}
94
+ className="-mx-1 flex w-[calc(100%+0.5rem)] items-center gap-1 px-1 text-left uppercase hover:text-foreground"
95
+ >
96
+ <svg
97
+ aria-hidden
98
+ viewBox="0 0 16 16"
99
+ className={
100
+ "size-3 shrink-0 transition-transform " +
101
+ (collapsed ? "-rotate-90" : "")
102
+ }
103
+ >
104
+ <path d="M4 6l4 4 4-4" fill="none" stroke="currentColor" strokeWidth="1.5" />
105
+ </svg>
106
+ {group.title}
107
+ </button>
108
+ </h3>
109
+ ) : (
110
+ <h3 className={titleClass}>{group.title}</h3>
111
+ ))}
112
+ {group.description && !(collapsible && collapsed) && (
82
113
  <p className="mt-1 text-xs text-muted-foreground">
83
114
  {group.description}
84
115
  </p>
85
116
  )}
86
117
  </header>
87
118
  )}
88
- <div className="flex flex-col gap-3">
119
+ <div id={bodyId} hidden={collapsible && collapsed} className="flex flex-col gap-3">
89
120
  {group.rows.map((row, idx) => {
90
121
  const fields = Array.isArray(row) ? row : [row]
91
122
  return (
@@ -142,11 +173,14 @@ function FieldSlot({
142
173
  return false
143
174
  })()
144
175
 
176
+ // Display-only kinds (separator, label) have no path: no value, no writes.
177
+ const path = "path" in field ? field.path : undefined
145
178
  return renderer({
146
179
  field,
147
- value: values[field.path],
180
+ value: path === undefined ? undefined : values[path],
148
181
  disabled,
149
- onChange: onChange ? (val) => onChange(field.path, val) : undefined,
182
+ onChange:
183
+ onChange && path !== undefined ? (val) => onChange(path, val) : undefined,
150
184
  ctx,
151
185
  })
152
186
  }
package/src/types.ts CHANGED
@@ -9,6 +9,9 @@ export type BuiltinFieldKind =
9
9
  | "color"
10
10
  | "checkbox"
11
11
  | "file"
12
+ | "vector"
13
+ | "separator"
14
+ | "label"
12
15
 
13
16
  export interface BaseField {
14
17
  kind: string
@@ -109,6 +112,28 @@ export interface FileFieldDef extends BaseField {
109
112
  helperText?: string
110
113
  }
111
114
 
115
+ // Display-only rows: they show something but hold no value, so they have no
116
+ // `path`. Houdini's Separator and Label parameter types.
117
+
118
+ /**
119
+ * A horizontal rule between rows, with an optional caption. Every kind keeps
120
+ * `label` so hosts can read `field.label` on any FieldDef without narrowing.
121
+ */
122
+ export interface SeparatorFieldDef {
123
+ kind: "separator"
124
+ id: string
125
+ label?: string
126
+ disabledWhen?: DisabledWhen
127
+ }
128
+
129
+ /** Static text: a hint, a note, a sub-heading. The text is `label`. */
130
+ export interface LabelFieldDef {
131
+ kind: "label"
132
+ id: string
133
+ label: string
134
+ disabledWhen?: DisabledWhen
135
+ }
136
+
112
137
  export type BuiltinFieldDef =
113
138
  | SelectFieldDef
114
139
  | ColorFieldDef
@@ -119,6 +144,8 @@ export type BuiltinFieldDef =
119
144
  | CheckboxFieldDef
120
145
  | FileFieldDef
121
146
  | VectorFieldDef
147
+ | SeparatorFieldDef
148
+ | LabelFieldDef
122
149
 
123
150
  export type FieldDef = BuiltinFieldDef | CustomFieldDef
124
151