@petrarca/sonnet-playground 0.6.0 → 0.8.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 CHANGED
@@ -1,21 +1,30 @@
1
1
  # @petrarca/sonnet-playground
2
2
 
3
- Playground framework and built-in component demos for the Petrarca Sonnet
4
- component library.
3
+ The living reference for the Petrarca Sonnet component library: a registry of
4
+ demonstration pages and a factory that turns it into a `ShellModule`. It is a
5
+ library, not a hosted site -- you mount it in your own application and get a
6
+ working component reference inside your own build
7
+ ([ADR-0005](../../docs/adr/0005-the-playground-is-the-living-documentation.md)).
5
8
 
6
- ## What's included
9
+ ## What it provides
7
10
 
8
- **Playground framework** — Registry, overview tile grid, and `createPlaygroundModule()`
9
- factory. Wire a playground into any `sonnet-shell` app with a few lines.
11
+ **A registry.** `PlaygroundRegistry` and `createPlaygroundRegistry`, which
12
+ returns one pre-populated with the built-in entries. Register your own pages
13
+ beside them, grouped under a label you choose.
10
14
 
11
- **Built-in demos** — Pre-built showcase pages for all Sonnet components:
12
- Typography, Color Palette, Core UI, Advanced Components, JSON Editor, Forms,
13
- Form Components, Form Widgets, Data Components, Graph Visualization, Shell API.
15
+ **A module factory.** `createPlaygroundModule` turns a registry into a
16
+ `ShellModule` with an overview page, generated routes and navigation, ready to
17
+ pass to `createModuleRegistry`.
14
18
 
15
- **PlaygroundSection** — Shared section wrapper (`default` and `card` variants)
16
- used by all playground pages.
19
+ **Twelve built-in pages**, each lazily loaded: Typography and Color Palette
20
+ under Design System; Core UI Components, Advanced Components and JSON Editor
21
+ under Components; Form Components, Forms Renderer and Form Widgets under Forms;
22
+ Schema Designer under Schemas; Data Components under Data; Graph Visualization
23
+ under Graph; Shell API under Shell.
17
24
 
18
- ---
25
+ **Page building blocks.** `PlaygroundSection` for consistent section chrome,
26
+ `PlaygroundOverview` for the tile grid, and the playground settings provider,
27
+ hook and panel.
19
28
 
20
29
  ## Install
21
30
 
@@ -23,120 +32,73 @@ used by all playground pages.
23
32
  pnpm add @petrarca/sonnet-playground
24
33
  ```
25
34
 
26
- Peer dependencies: `react`, `react-dom`, `@petrarca/sonnet-shell`,
27
- `@petrarca/sonnet-ui`, `@petrarca/sonnet-forms`, `@petrarca/sonnet-graph`.
35
+ That single install pulls in all six other `@petrarca/sonnet-*` packages --
36
+ `sonnet-core`, `sonnet-ui`, `sonnet-forms`, `sonnet-shell`, `sonnet-graph` and
37
+ `sonnet-schema-designer` -- as regular dependencies, because the pages import
38
+ from every one of them.
28
39
 
29
- ---
30
-
31
- ## Basic usage
32
-
33
- ```ts
34
- // src/modules/playground/index.ts
35
- import {
36
- createPlaygroundRegistry,
37
- createPlaygroundModule,
38
- } from "@petrarca/sonnet-playground";
39
-
40
- const registry = createPlaygroundRegistry();
41
-
42
- // Optionally add app-specific pages:
43
- registry.register({
44
- id: "app.my-page",
45
- title: "My Page",
46
- description: "Custom demo page.",
47
- icon: SomeIcon,
48
- color: "teal",
49
- path: "my-page",
50
- component: React.lazy(() => import("./pages/MyPage")),
51
- group: "App",
52
- order: 0,
53
- });
54
-
55
- export default createPlaygroundModule(registry);
56
- ```
57
-
58
- ```ts
59
- // src/modules/registry.ts
60
- import { createModuleRegistry } from "@petrarca/sonnet-shell";
61
- import home from "./home";
62
- import playground from "./playground";
63
-
64
- export const registry = createModuleRegistry([
65
- home,
66
- playground, // pinBottom: true is set automatically by createPlaygroundModule
67
- ]);
68
- ```
69
-
70
- `createPlaygroundModule` returns a `ShellModule` — pass it directly to
71
- `createModuleRegistry` alongside your app's own modules.
72
-
73
- ---
40
+ | Peer | Range | Needed for |
41
+ |---|---|---|
42
+ | `react`, `react-dom` | `>=19` | Everything |
43
+ | `react-router-dom` | `>=7` | The generated routes and the layout outlet |
44
+ | `tailwindcss` | `>=3` | Every page's styling |
74
45
 
75
- ## Adding app-specific pages
46
+ Those dependencies carry further peers of their own, and some that are only
47
+ optional down there are required here -- the graph page needs every renderer
48
+ peer, and the JSON editor page needs CodeMirror. Getting that wrong shows up as
49
+ one page throwing rather than as an install error. See
50
+ [getting started](docs/usage/getting-started.md).
76
51
 
77
- Register your own pages alongside the built-in demos by calling
78
- `registry.register()` before passing the registry to `createPlaygroundModule`.
79
- Pages appear in the overview tile grid grouped under your chosen `group` label.
52
+ ## Usage
80
53
 
81
54
  ```ts
82
55
  // src/modules/playground/index.ts
83
56
  import { lazy } from "react";
84
- import { Search } from "lucide-react";
57
+ import { Boxes } from "lucide-react";
85
58
  import {
86
- createPlaygroundRegistry,
87
59
  createPlaygroundModule,
60
+ createPlaygroundRegistry,
88
61
  } from "@petrarca/sonnet-playground";
89
62
 
90
63
  const registry = createPlaygroundRegistry();
91
64
 
92
- // Register app-specific pages
93
65
  registry.register({
94
- id: "app.my-widget",
95
- title: "My Widget",
96
- description: "Interactive demo of MyWidget with live configuration.",
97
- icon: Search,
66
+ id: "app.widgets",
67
+ title: "Widgets",
68
+ description: "Interactive demo of the Widget components.",
69
+ icon: Boxes,
98
70
  color: "teal",
99
- path: "my-widget",
100
- component: lazy(() => import("./pages/PlayMyWidget")),
101
- group: "App Components", // appears as a group heading in the overview
71
+ path: "widgets",
72
+ component: lazy(() => import("./pages/PlayWidgets")),
73
+ group: "App Components",
102
74
  order: 0,
103
75
  });
104
76
 
105
77
  export default createPlaygroundModule(registry);
106
78
  ```
107
79
 
108
- Pages are lazy-loaded automatically — wrap the component in `React.lazy()`
109
- as shown above. The framework handles the `Suspense` boundary.
80
+ `createPlaygroundModule` returns a `ShellModule`, so it goes into
81
+ `createModuleRegistry` alongside your own modules. Its options are `icon`,
82
+ `label`, `basePath`, `overviewTitle`, `overviewSubtitle` and `grouped`; the
83
+ defaults are `FlaskConical`, `"Playground"`, `"/playground"`,
84
+ `"Developer Playground"`, `"Experiment with components, test features, and
85
+ explore capabilities."` and `true`.
110
86
 
111
- `group` is optional. Pages without a group appear under the default group in
112
- the overview. Pages with the same `group` string are grouped together.
87
+ Individual built-in pages are available from
88
+ `@petrarca/sonnet-playground/pages` when you want one on a route of your own.
113
89
 
114
- ---
115
-
116
- ## Pages subpath
117
-
118
- Individual built-in pages are available at `@petrarca/sonnet-playground/pages`
119
- for consumers who want to cherry-pick or re-export them:
120
-
121
- ```tsx
122
- import { PlayTypography, PlayColorPalette } from "@petrarca/sonnet-playground/pages";
123
- ```
124
-
125
- ---
126
-
127
- ## createPlaygroundModule options
128
-
129
- | Option | Type | Default |
130
- |---|---|---|
131
- | `icon` | `LucideIcon` | `FlaskConical` |
132
- | `label` | `string` | `"Playground"` |
133
- | `basePath` | `string` | `"/playground"` |
134
- | `overviewTitle` | `string` | `"Developer Playground"` |
135
- | `overviewSubtitle` | `string` | `""` |
136
- | `grouped` | `boolean` | `true` |
90
+ ## Documentation
137
91
 
138
- ---
92
+ - [Usage guides](docs/usage/index.md) -- getting started,
93
+ [adding pages](docs/usage/adding-pages.md) and
94
+ [embedding](docs/usage/embedding.md).
95
+ - [Design docs](docs/design/index.md) -- the registry model and the page
96
+ contract.
97
+ - [sonnet-shell: getting started](../sonnet-shell/docs/usage/getting-started.md)
98
+ -- the application frame this module plugs into.
99
+ - [Workspace documentation](../../docs/README.md) -- how the packages layer and
100
+ the architecture decision records.
139
101
 
140
102
  ## License
141
103
 
142
- See [LICENSE.md](../../LICENSE.md).
104
+ Apache 2.0. See [LICENSE.md](../../LICENSE.md).
@@ -0,0 +1,8 @@
1
+ import {
2
+ PlayAuth
3
+ } from "./chunk-RYCRENYO.js";
4
+ import "./chunk-C2EZ2ECG.js";
5
+ export {
6
+ PlayAuth as default
7
+ };
8
+ //# sourceMappingURL=PlayAuth-SOEYPOOL.js.map
@@ -0,0 +1,7 @@
1
+ import {
2
+ PlayForms
3
+ } from "./chunk-BRH3FFFT.js";
4
+ export {
5
+ PlayForms as default
6
+ };
7
+ //# sourceMappingURL=PlayForms-U2TWUX5L.js.map
@@ -0,0 +1,7 @@
1
+ import {
2
+ PlayGraph
3
+ } from "./chunk-E5X7GVLU.js";
4
+ export {
5
+ PlayGraph as default
6
+ };
7
+ //# sourceMappingURL=PlayGraph-M4KQSFI7.js.map
@@ -0,0 +1,7 @@
1
+ import {
2
+ PlaySchemaDesigner
3
+ } from "./chunk-2XMWOZ5O.js";
4
+ export {
5
+ PlaySchemaDesigner as default
6
+ };
7
+ //# sourceMappingURL=PlaySchemaDesigner-S4DBB53G.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -312,4 +312,4 @@ function PlaySchemaDesigner() {
312
312
  export {
313
313
  PlaySchemaDesigner
314
314
  };
315
- //# sourceMappingURL=chunk-6VQJFBTM.js.map
315
+ //# sourceMappingURL=chunk-2XMWOZ5O.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/pages/PlaySchemaDesigner.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport {\n SchemaDesigner,\n type SchemaDesignerCapabilities,\n} from \"@petrarca/sonnet-schema-designer\";\nimport { FormSelect } from \"@petrarca/sonnet-forms\";\nimport type { JsonSchema, UISchema } from \"@petrarca/sonnet-core/schema\";\n\nconst SAMPLE_SCHEMA: JsonSchema = {\n title: \"Patient\",\n type: \"object\",\n properties: {\n first_name: { type: \"string\", title: \"First name\", minLength: 1 },\n last_name: { type: \"string\", title: \"Last name\", minLength: 1 },\n date_of_birth: { type: \"string\", format: \"date\", title: \"Date of birth\" },\n gender: {\n type: \"string\",\n title: \"Gender\",\n enum: [\"male\", \"female\", \"other\", \"unknown\"],\n },\n active: { type: \"boolean\", title: \"Active\" },\n contact: {\n type: \"object\",\n title: \"Contact\",\n properties: {\n email: { type: \"string\", format: \"email\", title: \"Email\" },\n phone: { type: \"string\", title: \"Phone\" },\n },\n required: [\"email\"],\n },\n addresses: {\n type: \"array\",\n title: \"Addresses\",\n minItems: 1,\n maxItems: 3,\n items: {\n type: \"object\",\n properties: {\n street: { type: \"string\", title: \"Street\" },\n city: { type: \"string\", title: \"City\" },\n zip: { type: \"string\", title: \"ZIP\" },\n country: { type: \"string\", title: \"Country\" },\n },\n required: [\"street\", \"city\"],\n },\n },\n },\n required: [\"first_name\", \"last_name\"],\n};\n\n// An audit-event-like schema: a fixed envelope (actor, targets) with three\n// extensible metadata containers. Used to demo scoped editing.\nconst AUDIT_SCHEMA: JsonSchema = {\n title: \"AuditEvent\",\n type: \"object\",\n properties: {\n actor: {\n type: \"object\",\n title: \"Actor\",\n properties: {\n type: { type: \"string\", title: \"Type\" },\n id: { type: \"string\", title: \"ID\" },\n metadata: {\n type: \"object\",\n title: \"Actor metadata\",\n properties: { role: { type: \"string\", title: \"Role\" } },\n },\n },\n },\n targets: {\n type: \"array\",\n title: \"Targets\",\n items: {\n type: \"object\",\n properties: {\n type: { type: \"string\", title: \"Type\" },\n id: { type: \"string\", title: \"ID\" },\n metadata: {\n type: \"object\",\n title: \"Target metadata\",\n properties: { ward: { type: \"string\", title: \"Ward\" } },\n },\n },\n },\n },\n metadata: {\n type: \"object\",\n title: \"Metadata\",\n properties: { reason: { type: \"string\", title: \"Reason\" } },\n },\n },\n};\n\n// A schema exercising every logic construct the designer can author: a oneOf\n// discriminated variant (payment method), if/then/else conditionals (authored\n// as allOf members), a const-tagged field, and a nullable anyOf optional field.\n// x-ui-order places each conditional field right after the property it depends\n// on (see docs/design/logic-authoring.md in the schema-designer package,\n// ordering note).\nconst LOGIC_SCHEMA: JsonSchema = {\n title: \"Checkout\",\n type: \"object\",\n // A reusable definition referenced by $ref (shows read-only expansion).\n $defs: {\n Address: {\n type: \"object\",\n properties: {\n street: { type: \"string\", title: \"Street\" },\n city: { type: \"string\", title: \"City\" },\n zip: { type: \"string\", title: \"ZIP\" },\n },\n },\n },\n \"x-ui-order\": [\n \"schema_version\",\n \"amount\",\n \"requires_approval\",\n \"currency\",\n \"expedited\",\n \"delivery_window\",\n \"gift_wrap\",\n \"gift_message\",\n \"shipping_address\",\n \"note\",\n \"payment\",\n ],\n properties: {\n // A $ref to the shared Address definition (read-only in the tree).\n shipping_address: { $ref: \"#/$defs/Address\", title: \"Shipping address\" },\n // A fixed value carried by every document (const authoring).\n schema_version: {\n type: \"string\",\n title: \"Schema version\",\n const: \"checkout/v1\",\n },\n amount: { type: \"number\", title: \"Amount\", minimum: 0 },\n currency: {\n type: \"string\",\n title: \"Currency\",\n enum: [\"EUR\", \"USD\", \"GBP\"],\n },\n expedited: { type: \"boolean\", title: \"Expedited\" },\n gift_wrap: { type: \"boolean\", title: \"Gift wrap\" },\n // Declarative visibility rule (x-ui-rule): declared at its own position,\n // shown only when gift_wrap is checked. Contrast with the if/then/else\n // fields above, which are structurally added by their conditionals.\n gift_message: {\n type: \"string\",\n title: \"Gift message\",\n \"x-ui-rule\": {\n effect: \"show\",\n condition: { scope: \"/gift_wrap\", schema: { const: true } },\n },\n },\n // Nullable optional (anyOf [T, null]) -- the renderer unwraps to T.\n note: {\n title: \"Note (optional)\",\n anyOf: [{ type: \"string\" }, { type: \"null\" }],\n },\n // A discriminated union (oneOf): the payment method.\n payment: {\n type: \"object\",\n title: \"Payment method\",\n discriminator: { propertyName: \"kind\" },\n oneOf: [\n {\n title: \"Card\",\n properties: {\n kind: { const: \"card\" },\n card_number: { type: \"string\", title: \"Card number\" },\n cvc: { type: \"string\", title: \"CVC\" },\n },\n },\n {\n title: \"Bank transfer\",\n properties: {\n kind: { const: \"bank\" },\n iban: { type: \"string\", title: \"IBAN\" },\n },\n },\n {\n title: \"Cash\",\n properties: { kind: { const: \"cash\" } },\n },\n ],\n },\n },\n required: [\"amount\", \"currency\"],\n // Conditionals (if/then/else) authored as allOf members.\n allOf: [\n {\n // Large amounts require an approval reference.\n if: {\n properties: { amount: { exclusiveMinimum: 1000 } },\n required: [\"amount\"],\n },\n then: {\n properties: {\n requires_approval: {\n type: \"string\",\n title: \"Approval reference\",\n },\n },\n required: [\"requires_approval\"],\n },\n },\n {\n // Expedited orders ask for a delivery window; otherwise none.\n if: {\n properties: { expedited: { const: true } },\n required: [\"expedited\"],\n },\n then: {\n properties: {\n delivery_window: {\n type: \"string\",\n title: \"Delivery window\",\n enum: [\"morning\", \"afternoon\", \"evening\"],\n },\n },\n },\n },\n ],\n};\n\nconst AUDIT_EDITABLE_PATHS = [\n \"/properties/metadata/properties\",\n \"/properties/actor/properties/metadata/properties\",\n \"/properties/targets/items/properties/metadata/properties\",\n];\n\ntype ProfileKey = \"full\" | \"data\" | \"scoped\" | \"structure\" | \"logic\";\n\ninterface Profile {\n label: string;\n description: string;\n schema: JsonSchema;\n capabilities?: SchemaDesignerCapabilities;\n readOnly?: boolean;\n}\n\nconst PROFILES: Record<ProfileKey, Profile> = {\n full: {\n label: \"Full (schema + uiSchema)\",\n description: \"Default: author the schema and its form presentation hints.\",\n schema: SAMPLE_SCHEMA,\n capabilities: { previews: [\"form\", \"table\", \"json\", \"capabilities\"] },\n },\n data: {\n label: \"Data schema only\",\n description:\n \"Pure JSON Schema (audit, terminology): no UI/Combined tabs, JSON preview only.\",\n schema: SAMPLE_SCHEMA,\n capabilities: { mode: \"data\", previews: [\"json\", \"capabilities\"] },\n },\n scoped: {\n label: \"Scoped (audit event)\",\n description:\n \"Fixed envelope; only the three metadata containers are editable.\",\n schema: AUDIT_SCHEMA,\n capabilities: {\n mode: \"data\",\n editablePaths: AUDIT_EDITABLE_PATHS,\n previews: [\"json\", \"capabilities\"],\n },\n },\n structure: {\n label: \"Structure only (no preview)\",\n description: \"Tree + inspector only; the center preview pane is hidden.\",\n schema: SAMPLE_SCHEMA,\n capabilities: { mode: \"data\", previews: [] },\n },\n logic: {\n label: \"Logic (variants + conditionals)\",\n description:\n \"Shows every logic construct: a oneOf variant, if/then/else conditionals, a const field, and a nullable optional.\",\n schema: LOGIC_SCHEMA,\n capabilities: { previews: [\"form\", \"table\", \"json\", \"capabilities\"] },\n },\n};\n\n/**\n * PlaySchemaDesigner -- exercises SchemaDesigner: visually author a JsonSchema\n * (and its uiSchema presentation hints), with a live preview. The profile\n * switcher presets the capability profile (data/full triage, scoped editing,\n * read-only); the in-designer Capabilities tab tweaks it live.\n */\nexport default function PlaySchemaDesigner() {\n const [profileKey, setProfileKey] = useState<ProfileKey>(\"full\");\n const profile = PROFILES[profileKey];\n\n const [schema, setSchema] = useState<JsonSchema>(profile.schema);\n const [uiSchema, setUiSchema] = useState<Record<string, UISchema>>({});\n\n const selectProfile = (key: ProfileKey) => {\n setProfileKey(key);\n setSchema(PROFILES[key].schema);\n setUiSchema({});\n };\n\n return (\n <div className=\"w-full flex flex-col gap-3 h-[calc(100vh-8rem)]\">\n <div>\n <h1>Schema Designer</h1>\n <p className=\"text-sm text-muted-foreground mb-3\">\n Visually author a JSON Schema and its UI presentation hints. Edits\n flow through onChange as a valid schema + uiSchema.\n </p>\n </div>\n\n <div className=\"flex items-center gap-3\">\n <label\n htmlFor=\"capability-profile\"\n className=\"shrink-0 text-sm font-medium\"\n >\n Capability profile\n </label>\n <div className=\"w-72\">\n <FormSelect\n id=\"capability-profile\"\n name=\"profile\"\n value={profileKey}\n options={(Object.keys(PROFILES) as ProfileKey[]).map((k) => ({\n value: k,\n label: PROFILES[k].label,\n }))}\n onChange={(v) => selectProfile((v as ProfileKey) ?? \"full\")}\n clearable={false}\n />\n </div>\n <p className=\"text-sm text-muted-foreground\">{profile.description}</p>\n </div>\n\n <div style={{ flex: 1, minHeight: 0 }}>\n <SchemaDesigner\n key={profileKey}\n schema={schema}\n uiSchema={uiSchema}\n capabilities={profile.capabilities}\n readOnly={profile.readOnly}\n onChange={(nextSchema, nextUiSchema) => {\n setSchema(nextSchema);\n setUiSchema(nextUiSchema);\n }}\n />\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -21,7 +21,8 @@ import { JsonEditor } from "@petrarca/sonnet-ui/json-editor";
21
21
  import {
22
22
  ArrowRight,
23
23
  ChevronLeft,
24
- ChevronRight as ChevronRightIcon
24
+ ChevronRight as ChevronRightIcon,
25
+ ChevronDown
25
26
  } from "lucide-react";
26
27
  import { jsx, jsxs } from "react/jsx-runtime";
27
28
  var SAMPLE_SCHEMA = {
@@ -50,6 +51,64 @@ var SAMPLE_SCHEMA = {
50
51
  },
51
52
  required: ["name", "npi"]
52
53
  };
54
+ var VALIDATION_SCHEMA = {
55
+ title: "Validation showcase",
56
+ type: "object",
57
+ // Cross-field / form validator (registered by the app): confirmEmail == email.
58
+ "x-ui-validate": ["emailsMatch"],
59
+ properties: {
60
+ email: {
61
+ type: "string",
62
+ title: "Email",
63
+ format: "email",
64
+ description: "Built-in: format: email"
65
+ },
66
+ confirmEmail: {
67
+ type: "string",
68
+ title: "Confirm email",
69
+ format: "email",
70
+ description: "Cross-field: must match Email (x-ui-validate on the object)"
71
+ },
72
+ website: {
73
+ type: "string",
74
+ title: "Website",
75
+ format: "uri",
76
+ description: "Built-in: format: uri"
77
+ },
78
+ zip: {
79
+ type: "string",
80
+ title: "ZIP code",
81
+ pattern: "^[0-9]{5}$",
82
+ description: "Built-in: pattern (five digits)"
83
+ },
84
+ age: {
85
+ type: "integer",
86
+ title: "Age",
87
+ minimum: 18,
88
+ maximum: 120,
89
+ description: "Built-in: minimum / maximum"
90
+ },
91
+ plan: {
92
+ type: "string",
93
+ title: "Plan",
94
+ enum: ["free", "pro", "enterprise"],
95
+ description: "Built-in: enum"
96
+ },
97
+ slug: {
98
+ type: "string",
99
+ title: "Slug",
100
+ "x-ui-validate": "slug",
101
+ description: "Custom: x-ui-validate: 'slug' (registered by the app)"
102
+ },
103
+ handle: {
104
+ type: "string",
105
+ title: "Handle",
106
+ "x-ui-validate": "uniqueHandle",
107
+ description: "Async: remote uniqueness check -- try 'taken'"
108
+ }
109
+ },
110
+ required: ["email"]
111
+ };
53
112
  var NESTED_OBJECT_SCHEMA = {
54
113
  title: "Patient",
55
114
  type: "object",
@@ -402,6 +461,7 @@ var SECTIONS_LAYOUT_UISCHEMA = {
402
461
  };
403
462
  var sampleSchemas = {
404
463
  flat: SAMPLE_SCHEMA,
464
+ validation: VALIDATION_SCHEMA,
405
465
  nested_object: NESTED_OBJECT_SCHEMA,
406
466
  one_of: ONEOF_SCHEMA,
407
467
  array_of_objects: ARRAY_OF_OBJECTS_SCHEMA,
@@ -495,9 +555,16 @@ function usePlayFormsState() {
495
555
  const [dataError, setDataError] = useState("");
496
556
  const [activeTab, setActiveTab] = useState("schema");
497
557
  const [deferStateUpdates, setDeferStateUpdates] = useState(false);
558
+ const [validationDisplay, setValidationDisplay] = useState("onSubmit");
498
559
  const [dataCollapsed, setDataCollapsed] = useState(false);
560
+ const [errorsCollapsed, setErrorsCollapsed] = useState(false);
561
+ const [injectServerError, setInjectServerError] = useState(false);
499
562
  const [lastDiff, setLastDiff] = useState(null);
500
563
  const toggleDataPanel = useCallback(() => setDataCollapsed((v) => !v), []);
564
+ const toggleErrorsPanel = useCallback(
565
+ () => setErrorsCollapsed((v) => !v),
566
+ []
567
+ );
501
568
  const loadSchemaPreset = useCallback((value) => {
502
569
  if (!value || !sampleSchemas[value]) return;
503
570
  const text = JSON.stringify(sampleSchemas[value], null, 2);
@@ -533,7 +600,10 @@ function usePlayFormsState() {
533
600
  dataError,
534
601
  activeTab,
535
602
  deferStateUpdates,
603
+ validationDisplay,
536
604
  dataCollapsed,
605
+ errorsCollapsed,
606
+ injectServerError,
537
607
  lastDiff,
538
608
  setSchemaText,
539
609
  setUiSchemaText,
@@ -548,7 +618,10 @@ function usePlayFormsState() {
548
618
  setDataError,
549
619
  setActiveTab,
550
620
  setDeferStateUpdates,
621
+ setValidationDisplay,
551
622
  toggleDataPanel,
623
+ toggleErrorsPanel,
624
+ setInjectServerError,
552
625
  setLastDiff,
553
626
  loadSchemaPreset,
554
627
  loadUiSchemaPreset
@@ -725,8 +798,37 @@ function SchemaEditorColumn({
725
798
  }
726
799
  );
727
800
  }
801
+ var DEMO_SERVER_ERRORS = [
802
+ { pointer: "/name", message: "Server: this name is already taken" },
803
+ { pointer: "", message: "Server: could not save (example form-level error)" }
804
+ ];
805
+ var DEMO_VALIDATORS = {
806
+ field: {
807
+ slug: (value) => /^[a-z0-9-]+$/.test(String(value ?? "")) ? null : "Lowercase letters, digits and hyphens only"
808
+ },
809
+ form: {
810
+ // Cross-field: confirmEmail must equal email. Error attaches to the
811
+ // confirmEmail field by pointer.
812
+ emailsMatch: (data) => data.email && data.confirmEmail && data.email !== data.confirmEmail ? [{ pointer: "/confirmEmail", message: "Emails do not match" }] : []
813
+ },
814
+ // Declared in the `asyncField` namespace so the engine knows it is async and
815
+ // fires it only on value change. Demo: a simulated remote uniqueness check
816
+ // (200ms) where "taken" is unavailable.
817
+ asyncField: {
818
+ uniqueHandle: async (value) => {
819
+ await new Promise((r) => setTimeout(r, 200));
820
+ return String(value ?? "") === "taken" ? "Handle is already taken (async check)" : null;
821
+ }
822
+ }
823
+ };
824
+ var VALIDATION_DISPLAY_OPTIONS = [
825
+ { value: "onSubmit", label: "Errors: on submit" },
826
+ { value: "onTouched", label: "Errors: on blur" },
827
+ { value: "always", label: "Errors: always" }
828
+ ];
728
829
  var SCHEMA_PRESET_OPTIONS = [
729
830
  { value: "flat", label: "Practitioner (flat fields)" },
831
+ { value: "validation", label: "Validation showcase (built-in + custom)" },
730
832
  { value: "nested_object", label: "Patient (nested object)" },
731
833
  { value: "one_of", label: "Payment (oneOf variant)" },
732
834
  { value: "array_of_objects", label: "Medication Request (array)" },
@@ -791,9 +893,9 @@ function FormColumn({
791
893
  flexDirection: "column"
792
894
  },
793
895
  children: [
794
- /* @__PURE__ */ jsxs("div", { children: [
795
- /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold mb-1", children: "Rendered Form" }),
796
- /* @__PURE__ */ jsxs(SimpleGroup, { gap: "xs", align: "center", className: "mb-1", children: [
896
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
897
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
898
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold", children: "Rendered Form" }),
797
899
  /* @__PURE__ */ jsx(
798
900
  Badge,
799
901
  {
@@ -801,21 +903,53 @@ function FormColumn({
801
903
  className: "text-xs",
802
904
  children: state.isValid ? "Valid" : "Invalid"
803
905
  }
804
- ),
805
- /* @__PURE__ */ jsx(
806
- FormCheckbox,
807
- {
808
- label: "Defer state updates (change on blur)",
809
- checked: state.deferStateUpdates,
810
- onChange: (checked) => {
811
- if (checked !== "indeterminate") {
812
- state.setDeferStateUpdates(checked);
813
- }
814
- }
815
- }
816
906
  )
817
907
  ] }),
818
- state.validationErrors.length > 0 && /* @__PURE__ */ jsx("div", { style: { marginTop: "4px" }, children: state.validationErrors.map((err, idx) => /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: err }, idx)) })
908
+ /* @__PURE__ */ jsxs("div", { className: "rounded-md border border-border p-2", children: [
909
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] font-medium text-muted-foreground mb-1.5", children: "Options" }),
910
+ /* @__PURE__ */ jsxs(SimpleGroup, { gap: "md", align: "center", wrap: "wrap", children: [
911
+ /* @__PURE__ */ jsx(
912
+ FormSelect,
913
+ {
914
+ label: false,
915
+ "aria-label": "Validation display timing",
916
+ options: VALIDATION_DISPLAY_OPTIONS,
917
+ value: state.validationDisplay,
918
+ onChange: (value) => {
919
+ if (value) {
920
+ state.setValidationDisplay(value);
921
+ }
922
+ },
923
+ className: "text-xs",
924
+ wrapperClassName: "w-44"
925
+ }
926
+ ),
927
+ /* @__PURE__ */ jsx(
928
+ FormCheckbox,
929
+ {
930
+ label: "Change on blur",
931
+ checked: state.deferStateUpdates,
932
+ onChange: (checked) => {
933
+ if (checked !== "indeterminate") {
934
+ state.setDeferStateUpdates(checked);
935
+ }
936
+ }
937
+ }
938
+ ),
939
+ /* @__PURE__ */ jsx(
940
+ FormCheckbox,
941
+ {
942
+ label: "Inject server error",
943
+ checked: state.injectServerError,
944
+ onChange: (checked) => {
945
+ if (checked !== "indeterminate") {
946
+ state.setInjectServerError(checked);
947
+ }
948
+ }
949
+ }
950
+ )
951
+ ] })
952
+ ] })
819
953
  ] }),
820
954
  /* @__PURE__ */ jsxs(
821
955
  "div",
@@ -836,7 +970,11 @@ function FormColumn({
836
970
  onCancel: handleCancel,
837
971
  onValidationChange: handleValidationChange,
838
972
  showActions: true,
973
+ allowPristineSave: true,
839
974
  deferStateUpdates: state.deferStateUpdates,
975
+ validationDisplay: state.validationDisplay,
976
+ validators: DEMO_VALIDATORS,
977
+ extraErrors: state.injectServerError ? DEMO_SERVER_ERRORS : void 0,
840
978
  saveLabel: "Save Changes",
841
979
  cancelLabel: "Reset",
842
980
  showExtraProperties: true,
@@ -862,6 +1000,31 @@ function FormColumn({
862
1000
  }
863
1001
  );
864
1002
  }
1003
+ function ErrorsCard({
1004
+ state
1005
+ }) {
1006
+ if (state.validationErrors.length === 0) return null;
1007
+ return /* @__PURE__ */ jsxs(Card, { className: "border shadow-sm shrink-0 flex flex-col overflow-hidden", children: [
1008
+ /* @__PURE__ */ jsxs(
1009
+ "button",
1010
+ {
1011
+ type: "button",
1012
+ onClick: state.toggleErrorsPanel,
1013
+ className: "flex items-center gap-2 px-4 py-3 text-left rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
1014
+ children: [
1015
+ state.errorsCollapsed ? /* @__PURE__ */ jsx(ChevronRightIcon, { className: "h-4 w-4 text-muted-foreground" }) : /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4 text-muted-foreground" }),
1016
+ /* @__PURE__ */ jsxs("span", { className: "text-sm font-semibold text-destructive", children: [
1017
+ "Errors (",
1018
+ state.validationErrors.length,
1019
+ ")"
1020
+ ] }),
1021
+ /* @__PURE__ */ jsx("span", { className: "ml-auto text-[11px] font-medium text-muted-foreground", children: "via onValidationChange" })
1022
+ ]
1023
+ }
1024
+ ),
1025
+ !state.errorsCollapsed && /* @__PURE__ */ jsx("div", { className: "px-4 pb-4 space-y-0.5", children: state.validationErrors.map((err, idx) => /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: err }, idx)) })
1026
+ ] });
1027
+ }
865
1028
  function DataColumn({
866
1029
  state
867
1030
  }) {
@@ -881,6 +1044,7 @@ function DataColumn({
881
1044
  transition: "flex 200ms ease"
882
1045
  },
883
1046
  children: [
1047
+ !state.dataCollapsed && /* @__PURE__ */ jsx(ErrorsCard, { state }),
884
1048
  /* @__PURE__ */ jsxs(
885
1049
  Card,
886
1050
  {
@@ -974,4 +1138,4 @@ function PlayForms() {
974
1138
  export {
975
1139
  PlayForms
976
1140
  };
977
- //# sourceMappingURL=chunk-ZSUCFEB6.js.map
1141
+ //# sourceMappingURL=chunk-BRH3FFFT.js.map