Skip to content
ilinxa/pro-ui

Properties Form

alphav0.1.4

Schema-driven read and edit form for typed records — six field types, per-field permissions, sync validation, and a custom renderer slot.

Category: FormsUpdated: 2026-08-11Created: 2026-04-29Author: ilinxa

Context

Tier 1 pro-component for the graph-system. Pairs with detail-panel as the inline editing surface for entity properties; useful standalone wherever a settings page or properties drawer needs typed fields without pulling in a full form library. Generic over the entity shape; the host owns the data and persistence; permission resolution is layered (host predicate → field declaration → default editable). Sync-only validation in two layers; async deferred to v0.2.

Installation

Initialize shadcn (once per project)Seeds lib/utils.ts and components.json. Skip if you've already used any shadcn component.
pnpm dlx shadcn@latest init
Register the @ilinxa namespace (once per project)Add to your components.json. Merge with existing config.
"registries": {
  "@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}
Install the component
pnpm dlx shadcn@latest add @ilinxa/properties-form

Add -fixtures for dummy data:

pnpm dlx shadcn@latest add @ilinxa/properties-form-fixtures

Preview

Short description visible in lists.

Migrate auth middleware to v2 API
In progress
High
rina@ilinxa.dev
6
2026-05-12

Marks the task closed once done.

No
Compatible with both providers; needs review by the security team before rollout. Follow the migration checklist in the runbook.

The PRIORITY_OPTIONS list has 4 levels — see dummy-data.ts for the full schemas.

Demo source

demo.tsxtsx

Usage

When to use

Reach for PropertiesForm whenever you have a flat record of typed fields — a task, a settings page, a node-properties drawer — and want a controlled read/edit surface with built-in validation, permissions, and a small custom-renderer escape hatch. It is intentionally generic over T; the host owns the data shape.

Basic example

import {
  PropertiesForm,
  type PropertiesFormField,
} from "@/components/properties-form";

interface Task {
  title: string;
  done: boolean;
}

const SCHEMA: ReadonlyArray<PropertiesFormField> = [
  { key: "title", type: "string", label: "Title", required: true },
  { key: "done", type: "boolean", label: "Done" },
];

export function TaskCard() {
  const [values, setValues] = useState<Task>({ title: "", done: false });

  return (
    <PropertiesForm<Task>
      schema={SCHEMA}
      values={values}
      onChange={setValues}
      onSubmit={async (next) => ({ ok: true })}
      mode="edit"
    />
  );
}

Field types

  • string — single-line text via Input.
  • number — right-aligned monospaced input; onChange sees a JS number when parseable.
  • boolean — read renders Check/X; edit renders shadcn Switch.
  • date — native <input type="date"> for v0.1 (shadcn Calendar upgrade is non-breaking, planned for v0.2).
  • select — shadcn Select driven by field.options.
  • textarea — multi-line; preserves whitespace in read mode.

Permissions

Each field resolves to editable / read-only / hidden, in this order:

  1. resolvePermission(field, values) — host predicate, returning undefined defers.
  2. Declarative field.permission.
  3. Default editable.

Read-only fields show field.permissionReason in a tooltip. Hidden fields are omitted from the DOM and from the error summary, but their value is preserved in values — the host owns the shape.

Validation

Two layers, both synchronous in v0.1:

  • Per-field — field.validate(value, allValues) runs on every commit; throws are caught and logged.
  • Form-level — validate(values) on the form runs only on submit attempts.

Errors render after submit OR after a field is blurred-with-error. On submit failure, focus moves to the first invalid field. Expensive validators should be wrapped in useMemo or debounced — they run on every keystroke for text inputs.

Imperative handle

const formRef = useRef<PropertiesFormHandle>(null);
// ...
<PropertiesForm ref={formRef} ... />

formRef.current?.isDirty();        // boolean
formRef.current?.markClean();      // snapshot current values as clean
formRef.current?.reset();          // restore last cleanSnapshot
formRef.current?.focusField("title");
const result = await formRef.current?.submit();

Custom renderers

Set field.renderer to opt out of built-in rendering. The renderer receives FieldRendererProps: value, onChange, field, allValues, mode, error, disabled, fieldId, errorId. Wire fieldId on your input and errorId via aria-describedby so it participates in the same a11y graph as built-ins.

When renderer is set, field.type is advisory only — properties-form does NOT validate that value matches the declared type.

Schema reference stability

Inline schema={[...]} rebuilds field objects on every render and invalidates internal memoization. Hoist to module scope or wrap with useMemo:

const SCHEMA = [/* ... */] satisfies PropertiesFormField[];
<PropertiesForm schema={SCHEMA} ... />

// or, when derived:
const schema = useMemo(() => buildSchema(node), [node]);

In-repo, the React Compiler memoizes inline literals at the call site. The two patterns above matter most for the eventual NPM extraction where consumers may not have the Compiler enabled.

What ships in v0.2+

  • Async validation hook.
  • Conditional visible predicate (sibling of permission).
  • Sections / fieldsets and column layouts.
  • shadcn Calendar upgrade for the date field.
  • Slot-able submitActions with localized defaults.

Features

  • Six built-in field types — string, number, boolean, date, select, textarea
  • Three-state permissions per field — editable / read-only / hidden
  • Layered permission resolver (host predicate → declarative → default)
  • Sync per-field + form-level validation; first-error focus on submit failure
  • Counter-based dirty tracking with markClean / reset / isDirty
  • Async onSubmit with 200ms-delayed spinner and aria-busy
  • Custom renderer slot for non-built-in field types
  • Imperative handle (submit / reset / markClean / isDirty / focusField)
  • ARIA-complete: label, aria-required / -invalid / -describedby, error summary

Tags

properties-formformschemavalidationgraph-system

Dependencies

shadcn primitives: button, input, select, switch, textarea, tooltip
npm peer deps: lucide-react@^1.11.0