@servicetitan/json-render-react 0.0.0-anvil2-kits-poc-20260717210733
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 +335 -0
- package/dist/catalog/context/catalog-prompt.md +229 -0
- package/dist/catalog/context/schema.json +56 -0
- package/dist/catalog/core/catalog-prompt.md +306 -0
- package/dist/catalog/core/schema.json +64 -0
- package/dist/catalog/marketing/catalog-prompt.md +307 -0
- package/dist/catalog/marketing/schema.json +65 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2126 -0
- package/dist/index.js.map +1 -0
- package/dist/src/context/catalog/catalog-def.d.ts +50 -0
- package/dist/src/context/catalog/context-card.catalog.d.ts +19 -0
- package/dist/src/context/catalog/context-card.d.ts +8 -0
- package/dist/src/context/catalog/context-data-table.catalog.d.ts +34 -0
- package/dist/src/context/catalog/context-data-table.css.d.ts +2 -0
- package/dist/src/context/catalog/context-data-table.d.ts +6 -0
- package/dist/src/context/catalog/context-file-list.catalog.d.ts +11 -0
- package/dist/src/context/catalog/context-file-list.css.d.ts +16 -0
- package/dist/src/context/catalog/context-file-list.d.ts +6 -0
- package/dist/src/context/catalog/context-list.catalog.d.ts +48 -0
- package/dist/src/context/catalog/context-list.css.d.ts +16 -0
- package/dist/src/context/catalog/context-list.d.ts +6 -0
- package/dist/src/context/catalog/context-metric-grid.catalog.d.ts +22 -0
- package/dist/src/context/catalog/context-metric-grid.css.d.ts +13 -0
- package/dist/src/context/catalog/context-metric-grid.d.ts +6 -0
- package/dist/src/context/catalog/context-note-list.catalog.d.ts +15 -0
- package/dist/src/context/catalog/context-note-list.d.ts +6 -0
- package/dist/src/context/catalog/context-overview.catalog.d.ts +8 -0
- package/dist/src/context/catalog/context-overview.css.d.ts +2 -0
- package/dist/src/context/catalog/context-overview.d.ts +6 -0
- package/dist/src/context/catalog/context-panel.catalog.d.ts +6 -0
- package/dist/src/context/catalog/context-panel.css.d.ts +1 -0
- package/dist/src/context/catalog/context-panel.d.ts +6 -0
- package/dist/src/context/catalog/context-tabular-list.catalog.d.ts +19 -0
- package/dist/src/context/catalog/context-tabular-list.css.d.ts +17 -0
- package/dist/src/context/catalog/context-tabular-list.d.ts +6 -0
- package/dist/src/context/catalog/index.d.ts +10 -0
- package/dist/src/context/catalog/schemas.d.ts +151 -0
- package/dist/src/context/components/collapsible-card.css.d.ts +41 -0
- package/dist/src/context/components/collapsible-card.d.ts +14 -0
- package/dist/src/context/components/context-ai-summary.css.d.ts +7 -0
- package/dist/src/context/components/context-ai-summary.d.ts +4 -0
- package/dist/src/context/components/context-chip-tone.d.ts +10 -0
- package/dist/src/context/components/context-note-list.css.d.ts +10 -0
- package/dist/src/context/components/context-render-error-boundary.d.ts +21 -0
- package/dist/src/context/constants.d.ts +6 -0
- package/dist/src/context/index.d.ts +4 -0
- package/dist/src/context/validate-context-spec-deep.d.ts +9 -0
- package/dist/src/context/validate-context-spec.d.ts +11 -0
- package/dist/src/core/actions.d.ts +28 -0
- package/dist/src/core/catalog/alert.catalog.d.ts +17 -0
- package/dist/src/core/catalog/alert.d.ts +9 -0
- package/dist/src/core/catalog/bar-chart.catalog.d.ts +22 -0
- package/dist/src/core/catalog/bar-chart.d.ts +6 -0
- package/dist/src/core/catalog/button.catalog.d.ts +24 -0
- package/dist/src/core/catalog/button.d.ts +7 -0
- package/dist/src/core/catalog/card.catalog.d.ts +20 -0
- package/dist/src/core/catalog/card.d.ts +8 -0
- package/dist/src/core/catalog/catalog-def.d.ts +484 -0
- package/dist/src/core/catalog/chart-utils.d.ts +2 -0
- package/dist/src/core/catalog/chart.css.d.ts +1 -0
- package/dist/src/core/catalog/checkbox.catalog.d.ts +11 -0
- package/dist/src/core/catalog/checkbox.d.ts +8 -0
- package/dist/src/core/catalog/chip.catalog.d.ts +20 -0
- package/dist/src/core/catalog/chip.d.ts +7 -0
- package/dist/src/core/catalog/divider.catalog.d.ts +25 -0
- package/dist/src/core/catalog/divider.d.ts +6 -0
- package/dist/src/core/catalog/donut-chart.catalog.d.ts +19 -0
- package/dist/src/core/catalog/donut-chart.d.ts +6 -0
- package/dist/src/core/catalog/flex.catalog.d.ts +55 -0
- package/dist/src/core/catalog/flex.d.ts +8 -0
- package/dist/src/core/catalog/grid.catalog.d.ts +21 -0
- package/dist/src/core/catalog/grid.css.d.ts +4 -0
- package/dist/src/core/catalog/grid.d.ts +8 -0
- package/dist/src/core/catalog/guidance-card-column-width.d.ts +12 -0
- package/dist/src/core/catalog/guidance-card.catalog.d.ts +58 -0
- package/dist/src/core/catalog/guidance-card.d.ts +9 -0
- package/dist/src/core/catalog/icon.catalog.d.ts +19 -0
- package/dist/src/core/catalog/icon.d.ts +6 -0
- package/dist/src/core/catalog/index.d.ts +7 -0
- package/dist/src/core/catalog/input.catalog.d.ts +20 -0
- package/dist/src/core/catalog/input.d.ts +8 -0
- package/dist/src/core/catalog/line-chart.catalog.d.ts +18 -0
- package/dist/src/core/catalog/line-chart.d.ts +6 -0
- package/dist/src/core/catalog/link.catalog.d.ts +20 -0
- package/dist/src/core/catalog/link.d.ts +7 -0
- package/dist/src/core/catalog/listbox.catalog.d.ts +15 -0
- package/dist/src/core/catalog/listbox.css.d.ts +9 -0
- package/dist/src/core/catalog/listbox.d.ts +8 -0
- package/dist/src/core/catalog/progress-bar.catalog.d.ts +11 -0
- package/dist/src/core/catalog/progress-bar.d.ts +6 -0
- package/dist/src/core/catalog/radio-group.catalog.d.ts +15 -0
- package/dist/src/core/catalog/radio-group.d.ts +8 -0
- package/dist/src/core/catalog/schemas.d.ts +436 -0
- package/dist/src/core/catalog/switch.catalog.d.ts +11 -0
- package/dist/src/core/catalog/switch.d.ts +8 -0
- package/dist/src/core/catalog/text.catalog.d.ts +20 -0
- package/dist/src/core/catalog/text.d.ts +6 -0
- package/dist/src/core/catalog/textarea.catalog.d.ts +15 -0
- package/dist/src/core/catalog/textarea.d.ts +8 -0
- package/dist/src/generate.d.ts +9 -0
- package/dist/src/index.d.ts +9 -0
- package/dist/src/marketing/catalog/catalog.d.ts +1 -0
- package/dist/src/marketing/catalog/components/index.d.ts +14 -0
- package/dist/src/marketing/catalog/components/revenue-card.catalog.d.ts +15 -0
- package/dist/src/marketing/catalog/components/revenue-card.css.d.ts +15 -0
- package/dist/src/marketing/catalog/components/revenue-card.d.ts +6 -0
- package/dist/src/marketing/catalog/index.d.ts +6 -0
- package/dist/src/marketing/catalog/scoped-catalogs.d.ts +4 -0
- package/dist/src/registry-utils.d.ts +6 -0
- package/dist/src/registry.d.ts +9 -0
- package/dist/src/renderers/context-spec-renderer.d.ts +6 -0
- package/dist/src/renderers/internal/enrichments/button-actions.d.ts +7 -0
- package/dist/src/renderers/internal/enrichments/useEnrichedSpec.d.ts +3 -0
- package/dist/src/renderers/sectioned-context-renderer.css.d.ts +1 -0
- package/dist/src/renderers/sectioned-context-renderer.d.ts +36 -0
- package/dist/src/renderers/spec-renderer.d.ts +30 -0
- package/dist/src/renderers/types.d.ts +4 -0
- package/dist/src/renderers/wrap-context-registry.d.ts +3 -0
- package/dist/src/scoped-catalog.d.ts +64 -0
- package/dist/src/test/setup-dom.d.ts +1 -0
- package/dist/src/types.d.ts +17 -0
- package/dist/src/utils/cx.d.ts +2 -0
- package/dist/src/utils/defined-props.d.ts +6 -0
- package/dist/styles.css +1 -0
- package/package.json +92 -0
package/README.md
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# @servicetitan/json-render-react
|
|
2
|
+
|
|
3
|
+
Generative UI for ServiceTitan: Zod catalog schemas, React renderers, and LLM artifacts that turn JSON specs into live UI.
|
|
4
|
+
|
|
5
|
+
This package bridges [json-render](https://github.com/json-render/json-render) (spec format and React runtime) with ServiceTitan component libraries ([Anvil2](https://github.com/servicetitan/anvil2), [anvil2-ai-kit](../anvil2-ai-kit/)). Agents emit structured JSON; hosts render it with `SpecRenderer`.
|
|
6
|
+
|
|
7
|
+
> [!NOTE]
|
|
8
|
+
> **New here?** Read this file for architecture and concepts, then [CONTRIBUTING.md](./CONTRIBUTING.md) for workflows. Storybook [Guide](http://localhost:6007/?path=/docs/guide--docs) (`pnpm storybook`) is the interactive reference.
|
|
9
|
+
|
|
10
|
+
## What problem this solves
|
|
11
|
+
|
|
12
|
+
| Layer | Responsibility |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| **Agent backend** | Receives `catalog-prompt.md` + `schema.json`, emits a `Spec` JSON tree |
|
|
15
|
+
| **This package** | Validates catalog contracts, maps spec types to React, wires actions |
|
|
16
|
+
| **Host app (Atlas)** | Provides `SpecRenderer`, action handlers, theme providers |
|
|
17
|
+
| **End user** | Sees Anvil2 / Carto UI, submits forms, triggers navigation |
|
|
18
|
+
|
|
19
|
+
Without this package, every host would re-implement component schemas, renderers, prompt rules, and action wiring.
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
# From monorepo root
|
|
25
|
+
pnpm install
|
|
26
|
+
pnpm build --filter @servicetitan/json-render-react
|
|
27
|
+
pnpm storybook --filter @servicetitan/json-render-react # http://localhost:6007
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
import { AnvilProvider } from "@servicetitan/anvil2";
|
|
32
|
+
import { AiKitTheme } from "@servicetitan/anvil2-ai-kit";
|
|
33
|
+
import "@servicetitan/anvil2-ai-kit/styles.css";
|
|
34
|
+
import { getRegistry, SpecRenderer } from "@servicetitan/json-render-react";
|
|
35
|
+
|
|
36
|
+
const { registry, handlers } = getRegistry("chat");
|
|
37
|
+
|
|
38
|
+
<AnvilProvider themeData={{ mode: "light" }}>
|
|
39
|
+
<AiKitTheme appearance="light">
|
|
40
|
+
<SpecRenderer
|
|
41
|
+
spec={spec}
|
|
42
|
+
registry={registry}
|
|
43
|
+
handlers={handlers}
|
|
44
|
+
actions={({ setState, getSnapshot }) => ({
|
|
45
|
+
"atlas.submitUserAction": (params) => handleSubmit(params, getSnapshot()),
|
|
46
|
+
"atlas.navigate": (params) => handleNavigate((params as { url: string }).url),
|
|
47
|
+
})}
|
|
48
|
+
/>
|
|
49
|
+
</AiKitTheme>
|
|
50
|
+
</AnvilProvider>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
> [!IMPORTANT]
|
|
54
|
+
> Pin `appearance` to `"light"` or `"dark"` (not `"auto"`) and pass the same mode to `AnvilProvider` so Anvil2 and Carto resolve the same `light-dark()` branch.
|
|
55
|
+
|
|
56
|
+
## Architecture
|
|
57
|
+
|
|
58
|
+
### Three-layer model
|
|
59
|
+
|
|
60
|
+
```mermaid
|
|
61
|
+
flowchart TD
|
|
62
|
+
subgraph agent [Agent backend]
|
|
63
|
+
P[catalog-prompt.md]
|
|
64
|
+
S[schema.json]
|
|
65
|
+
LLM[LLM structured output]
|
|
66
|
+
P --> LLM
|
|
67
|
+
S --> LLM
|
|
68
|
+
LLM --> Spec[Spec JSON]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
subgraph jrr ["@servicetitan/json-render-react"]
|
|
72
|
+
GR[getRegistry surface]
|
|
73
|
+
SR[SpecRenderer]
|
|
74
|
+
REG[Component registry]
|
|
75
|
+
Spec --> SR
|
|
76
|
+
GR --> REG
|
|
77
|
+
REG --> SR
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
subgraph ui [Component libraries]
|
|
81
|
+
A[Anvil2 - core catalog]
|
|
82
|
+
C[anvil2-ai-kit - context panel]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
SR --> A
|
|
86
|
+
SR --> C
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Two-layer public API
|
|
90
|
+
|
|
91
|
+
| API | Purpose |
|
|
92
|
+
| --- | --- |
|
|
93
|
+
| `getRegistry("chat" \| "context" \| "panel" \| "app")` | Select which catalog components and actions are available on a surface |
|
|
94
|
+
| `SpecRenderer` | Context-agnostic host; pass `spec` + `registry` + `handlers` from `getRegistry` |
|
|
95
|
+
|
|
96
|
+
Default exports (`registry`, `handlers`, `executeAction`) use the **chat** registry for ti-assist / Atlas parity.
|
|
97
|
+
|
|
98
|
+
### Data flow (chat surface)
|
|
99
|
+
|
|
100
|
+
1. Agent receives generated `catalog-prompt.md` and `schema.json` from `pnpm build`.
|
|
101
|
+
2. Agent emits a `Spec` (`root`, `elements`, optional `state`).
|
|
102
|
+
3. Host calls `getRegistry("chat")` and renders with `SpecRenderer`.
|
|
103
|
+
4. `SpecRenderer` enriches the spec (e.g. auto-wires `Button` press → `atlas.submitUserAction`), creates a state store, and mounts json-render's `Renderer`.
|
|
104
|
+
5. User interaction fires catalog actions; host `actions` factory handles `atlas.submitUserAction`, `atlas.navigate`, `atlas.artifactToolCall`.
|
|
105
|
+
6. Backend returns an updated spec via `MessageUpdated` (Atlas).
|
|
106
|
+
|
|
107
|
+
See [Atlas gen-ui docs](https://atlas-docs.st.dev/#/gen-ui-docs) for host integration.
|
|
108
|
+
|
|
109
|
+
### Context panel flow
|
|
110
|
+
|
|
111
|
+
Context specs root at `ContextPanel` and render anvil2-ai-kit components. Use `getRegistry("context")`, `ContextSpecRenderer`, or `SectionedContextRenderer` for streaming multi-section layouts.
|
|
112
|
+
|
|
113
|
+
Validation: `validateContextSpec`, `validateContextSpecDeep`. Rules: `src/context/prompt/rules.md`.
|
|
114
|
+
|
|
115
|
+
## Package layout
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
packages/json-render-react/
|
|
119
|
+
├── README.md ← you are here
|
|
120
|
+
├── CONTRIBUTING.md ← contributor workflows
|
|
121
|
+
├── CLAUDE.md ← agent quick reference (synced to .cursor/rules)
|
|
122
|
+
├── src/
|
|
123
|
+
│ ├── index.ts ← public exports
|
|
124
|
+
│ ├── registry.ts ← getRegistry, surface filtering
|
|
125
|
+
│ ├── scoped-catalog.ts ← team catalog composition API
|
|
126
|
+
│ ├── core/
|
|
127
|
+
│ │ ├── catalog/ ← 20 core components (*.catalog.ts + *.tsx)
|
|
128
|
+
│ │ ├── actions.ts ← atlas.* action catalog
|
|
129
|
+
│ │ └── prompt/rules.md ← LLM layout/form rules (core)
|
|
130
|
+
│ ├── context/
|
|
131
|
+
│ │ ├── catalog/ ← ContextPanel shell + content primitives
|
|
132
|
+
│ │ ├── components/ ← CollapsibleCard, AISummary, error boundary
|
|
133
|
+
│ │ └── prompt/rules.md ← context-specific LLM rules
|
|
134
|
+
│ ├── marketing/catalog/ ← scoped catalog example (prefix `mrk`)
|
|
135
|
+
│ ├── renderers/
|
|
136
|
+
│ │ ├── spec-renderer.tsx ← main host
|
|
137
|
+
│ │ ├── context-spec-renderer.tsx
|
|
138
|
+
│ │ └── sectioned-context-renderer.tsx
|
|
139
|
+
│ ├── storybook/ ← Storybook infra (not published)
|
|
140
|
+
│ └── stories/ ← all Storybook content (CSF + MDX)
|
|
141
|
+
├── scripts/
|
|
142
|
+
│ ├── generate-schema.ts ← dist/catalog/* artifacts
|
|
143
|
+
│ └── generate-catalog-docs.ts
|
|
144
|
+
└── dist/ ← build output (not committed)
|
|
145
|
+
└── catalog/
|
|
146
|
+
├── core/ ← schema.json, catalog-prompt.md
|
|
147
|
+
├── context/
|
|
148
|
+
└── marketing/ ← scoped team artifacts
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Key concepts
|
|
152
|
+
|
|
153
|
+
### Spec
|
|
154
|
+
|
|
155
|
+
JSON document with `root` (element key), `elements` (map of nodes), and optional `state` (reactive values).
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"root": "root",
|
|
160
|
+
"state": { "name": "" },
|
|
161
|
+
"elements": {
|
|
162
|
+
"root": { "type": "Card", "props": { "title": "Hello" }, "children": ["field"] },
|
|
163
|
+
"field": { "type": "Input", "props": { "label": "Name", "value": { "$bindState": "/name" } }, "children": [] }
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
| Expression | Purpose |
|
|
169
|
+
| --- | --- |
|
|
170
|
+
| `$bindState` | Two-way binding on interactive props (`value`, `checked`) |
|
|
171
|
+
| `$state` | Read-only state reference |
|
|
172
|
+
| `$item` | Field access inside `repeat` blocks |
|
|
173
|
+
| `$cond` / `$then` / `$else` | Inline conditional prop values |
|
|
174
|
+
| `$computed` | Calls registered functions (`computeTotal` in `SpecRenderer`) |
|
|
175
|
+
| `visible` | Conditional element rendering |
|
|
176
|
+
| `repeat` | List rendering from a state array |
|
|
177
|
+
| `on.press` | Button event handlers (action objects) |
|
|
178
|
+
|
|
179
|
+
Full reference: Storybook Guide, skill [authoring-json-render-specs](../../.claude/skills/authoring-json-render-specs/SKILL.md).
|
|
180
|
+
|
|
181
|
+
### Catalog
|
|
182
|
+
|
|
183
|
+
Per-component Zod schema + metadata for LLMs:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
export const buttonCatalog = {
|
|
187
|
+
props: z.object({ label: z.string(), appearance: z.enum([...]).optional() }),
|
|
188
|
+
slots: [],
|
|
189
|
+
events: ["press"],
|
|
190
|
+
description: "Clickable button. ...",
|
|
191
|
+
};
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Paired with a **renderer** (`ButtonRenderer`) that maps props to Anvil2/Carto components.
|
|
195
|
+
|
|
196
|
+
### CatalogEntry and surfaces
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
interface CatalogEntry {
|
|
200
|
+
name: string;
|
|
201
|
+
catalog: ComponentCatalogDef;
|
|
202
|
+
renderer: unknown;
|
|
203
|
+
renderers: RendererContext[]; // "all" | "chat" | "context" | "panel" | "app"
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`getRegistry(renderer)` filters entries by surface before calling `defineCatalog` + `defineRegistry`.
|
|
208
|
+
|
|
209
|
+
| Surface | Components |
|
|
210
|
+
| --- | --- |
|
|
211
|
+
| `chat` | Core (`renderers: ["all"]`) + team chat components (e.g. Marketing `RevenueCard`) |
|
|
212
|
+
| `context` | Context shell + content primitives + curated core subset |
|
|
213
|
+
| `panel`, `app` | Reserved for future surfaces |
|
|
214
|
+
|
|
215
|
+
### Actions
|
|
216
|
+
|
|
217
|
+
Global action catalog in `src/core/actions.ts`:
|
|
218
|
+
|
|
219
|
+
| Action | Params | Host responsibility |
|
|
220
|
+
| --- | --- | --- |
|
|
221
|
+
| `atlas.submitUserAction` | `actionId?`, `data?` | Submit form snapshot to agent backend |
|
|
222
|
+
| `atlas.navigate` | `url` | Route internally or open external URL |
|
|
223
|
+
| `atlas.artifactToolCall` | `tool`, `args?` | Invoke MCP tool on artifact panel app |
|
|
224
|
+
| `setState` | `statePath`, `value` | Synchronous local state update (json-render built-in) |
|
|
225
|
+
|
|
226
|
+
`Button` elements without explicit `on.press` are auto-wired to `atlas.submitUserAction` using the element key as `actionId`.
|
|
227
|
+
|
|
228
|
+
### Build artifacts
|
|
229
|
+
|
|
230
|
+
`pnpm build` emits (not committed):
|
|
231
|
+
|
|
232
|
+
| Path | Content |
|
|
233
|
+
| --- | --- |
|
|
234
|
+
| `dist/catalog/core/schema.json` | JSON Schema for core chat catalog |
|
|
235
|
+
| `dist/catalog/core/catalog-prompt.md` | LLM prompt (catalog + `rules.md`) |
|
|
236
|
+
| `dist/catalog/context/*` | Context panel artifacts |
|
|
237
|
+
| `dist/catalog/<team>/*` | Per scoped catalog (e.g. `marketing`) |
|
|
238
|
+
|
|
239
|
+
Import: `@servicetitan/json-render-react/catalog/core/schema.json`
|
|
240
|
+
|
|
241
|
+
### UI library split
|
|
242
|
+
|
|
243
|
+
| Subsystem | Component library | Token source |
|
|
244
|
+
| --- | --- | --- |
|
|
245
|
+
| Core catalog (20 components) | Anvil2 + anvil2-ext-charts | Anvil2 theme |
|
|
246
|
+
| Context panel (9 components) | anvil2-ai-kit | `@servicetitan/anvil2-token` via AiKitTheme |
|
|
247
|
+
| Marketing example | Anvil2 + Carto tokens for accents | Mixed |
|
|
248
|
+
|
|
249
|
+
## Public exports
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
// Rendering
|
|
253
|
+
SpecRenderer, SpecRendererProps
|
|
254
|
+
ContextSpecRenderer, SectionedContextRenderer
|
|
255
|
+
|
|
256
|
+
// Registry
|
|
257
|
+
getRegistry, getContextRegistry, registry, handlers, executeAction, catalog
|
|
258
|
+
|
|
259
|
+
// Context validation
|
|
260
|
+
validateContextSpec, validateContextSpecDeep, postProcessContextSpec, ...
|
|
261
|
+
|
|
262
|
+
// Scoped catalogs (teams)
|
|
263
|
+
createScopedCatalog, mergeCatalogs, composeCatalogComponents
|
|
264
|
+
|
|
265
|
+
// Types
|
|
266
|
+
Spec, Catalog, RendererContext, CatalogEntry, ...
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Storybook
|
|
270
|
+
|
|
271
|
+
| Port | Package |
|
|
272
|
+
| --- | --- |
|
|
273
|
+
| 6006 | anvil2-ai-kit |
|
|
274
|
+
| 6007 | json-render-react (this package) |
|
|
275
|
+
| 6008 | carto-rn-kit |
|
|
276
|
+
|
|
277
|
+
```sh
|
|
278
|
+
pnpm storybook --filter @servicetitan/json-render-react
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
| Sidebar group | Content |
|
|
282
|
+
| --- | --- |
|
|
283
|
+
| Guide | Package narrative, spec format, Atlas integration |
|
|
284
|
+
| Spec Playground | Live JSON editor |
|
|
285
|
+
| Core Catalog | One story file per core component |
|
|
286
|
+
| Scoped Catalogs | Team-specific components |
|
|
287
|
+
| Context | Context panel components |
|
|
288
|
+
| Examples | Composed demos, tool-call wiring |
|
|
289
|
+
|
|
290
|
+
Catalog stories export exactly **Default** (Spec panel) + **StickerSheet** (Chromatic baseline).
|
|
291
|
+
|
|
292
|
+
## Scripts
|
|
293
|
+
|
|
294
|
+
| Command | Purpose |
|
|
295
|
+
| --- | --- |
|
|
296
|
+
| `pnpm build` | Library dist + `dist/catalog/*` artifacts |
|
|
297
|
+
| `pnpm schema:gen` | Regenerate catalog artifacts only |
|
|
298
|
+
| `pnpm docs:gen` | Scaffold MDX for new catalog components |
|
|
299
|
+
| `pnpm storybook` | Dev server on port 6007 |
|
|
300
|
+
| `pnpm test` | Unit tests |
|
|
301
|
+
| `pnpm test-storybook` | Browser tests via Storybook |
|
|
302
|
+
| `pnpm typecheck` / `pnpm lint` | CI checks |
|
|
303
|
+
|
|
304
|
+
## Agent skills
|
|
305
|
+
|
|
306
|
+
Claude/Cursor skills in `.claude/skills/`:
|
|
307
|
+
|
|
308
|
+
| Skill | When to use |
|
|
309
|
+
| --- | --- |
|
|
310
|
+
| [authoring-json-render-specs](../../.claude/skills/authoring-json-render-specs/SKILL.md) | Writing or reviewing JSON specs |
|
|
311
|
+
| [authoring-json-render-catalog-components](../../.claude/skills/authoring-json-render-catalog-components/SKILL.md) | New catalog entries, renderers, actions |
|
|
312
|
+
| [authoring-json-render-scoped-catalogs](../../.claude/skills/authoring-json-render-scoped-catalogs/SKILL.md) | Team-scoped catalogs |
|
|
313
|
+
| [authoring-json-render-context-panel](../../.claude/skills/authoring-json-render-context-panel/SKILL.md) | Context panel specs and components |
|
|
314
|
+
| [authoring-json-render-react-storybook-stories](../../.claude/skills/authoring-json-render-react-storybook-stories/SKILL.md) | Stories, MDX, Spec panel, Chromatic |
|
|
315
|
+
|
|
316
|
+
## External references
|
|
317
|
+
|
|
318
|
+
| Resource | URL |
|
|
319
|
+
| --- | --- |
|
|
320
|
+
| json-render (upstream) | https://github.com/json-render/json-render |
|
|
321
|
+
| Atlas gen-ui integration | https://atlas-docs.st.dev/#/gen-ui-docs |
|
|
322
|
+
| Carto monorepo contributing | [CONTRIBUTING.md](../../CONTRIBUTING.md) |
|
|
323
|
+
| Carto repo scaffolding (Confluence) | https://servicetitan.atlassian.net/wiki/spaces/ADS/pages/4510026867/ANV-4906+-+Carto+Repo+Scaffolding |
|
|
324
|
+
|
|
325
|
+
## Peer dependencies
|
|
326
|
+
|
|
327
|
+
Consumers must install:
|
|
328
|
+
|
|
329
|
+
- `react`, `react-dom` (`>=18 <20`)
|
|
330
|
+
- `@servicetitan/anvil2`, `@servicetitan/anvil2-ext-charts`
|
|
331
|
+
- `@servicetitan/anvil2-ai-kit`
|
|
332
|
+
- `@amcharts/amcharts5` (charts)
|
|
333
|
+
|
|
334
|
+
> [!WARNING]
|
|
335
|
+
> `@json-render/react@0.15` declares `react: ^19.2.3`; this package peers `>=18 <20` for Atlas/ti-assist compatibility. Hosts on React 18 may see upstream peer warnings until they upgrade or add `peerDependencyRules`.
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
You are a UI generator that outputs JSON.
|
|
2
|
+
|
|
3
|
+
OUTPUT FORMAT (JSONL, RFC 6902 JSON Patch):
|
|
4
|
+
Output JSONL (one JSON object per line) using RFC 6902 JSON Patch operations to build a UI tree.
|
|
5
|
+
Each line is a JSON patch operation (add, remove, replace). Start with /root, then stream /elements and /state patches interleaved so the UI fills in progressively as it streams.
|
|
6
|
+
|
|
7
|
+
Example output (each line is a separate JSON object):
|
|
8
|
+
|
|
9
|
+
{"op":"add","path":"/root","value":"main"}
|
|
10
|
+
{"op":"add","path":"/elements/main","value":{"type":"Text","props":{"content":"example"},"children":["child-1","list"]}}
|
|
11
|
+
{"op":"add","path":"/elements/child-1","value":{"type":"Chip","props":{"label":"example"},"children":[]}}
|
|
12
|
+
{"op":"add","path":"/elements/list","value":{"type":"Text","props":{"content":"example"},"repeat":{"statePath":"/items","key":"id"},"children":["item"]}}
|
|
13
|
+
{"op":"add","path":"/elements/item","value":{"type":"Chip","props":{"label":{"$item":"title"}},"children":[]}}
|
|
14
|
+
{"op":"add","path":"/state/items","value":[]}
|
|
15
|
+
{"op":"add","path":"/state/items/0","value":{"id":"1","title":"First Item"}}
|
|
16
|
+
{"op":"add","path":"/state/items/1","value":{"id":"2","title":"Second Item"}}
|
|
17
|
+
|
|
18
|
+
Note: state patches appear right after the elements that use them, so the UI fills in as it streams. ONLY use component types from the AVAILABLE COMPONENTS list below.
|
|
19
|
+
|
|
20
|
+
INITIAL STATE:
|
|
21
|
+
Specs include a /state field to seed the state model. Components with { $bindState } or { $bindItem } read from and write to this state, and $state expressions read from it.
|
|
22
|
+
CRITICAL: You MUST include state patches whenever your UI displays data via $state, $bindState, $bindItem, $item, or $index expressions, or uses repeat to iterate over arrays. Without state, these references resolve to nothing and repeat lists render zero items.
|
|
23
|
+
Output state patches right after the elements that reference them, so the UI fills in progressively as it streams.
|
|
24
|
+
Stream state progressively - output one patch per array item instead of one giant blob:
|
|
25
|
+
For arrays: {"op":"add","path":"/state/posts/0","value":{"id":"1","title":"First Post",...}} then /state/posts/1, /state/posts/2, etc.
|
|
26
|
+
For scalars: {"op":"add","path":"/state/newTodoText","value":""}
|
|
27
|
+
Initialize the array first if needed: {"op":"add","path":"/state/posts","value":[]}
|
|
28
|
+
When content comes from the state model, use { "$state": "/some/path" } dynamic props to display it instead of hardcoding the same value in both state and props. The state model is the single source of truth.
|
|
29
|
+
Include realistic sample data in state. For blogs: 3-4 posts with titles, excerpts, authors, dates. For product lists: 3-5 items with names, prices, descriptions. Never leave arrays empty.
|
|
30
|
+
|
|
31
|
+
DYNAMIC LISTS (repeat field):
|
|
32
|
+
Any element can have a top-level "repeat" field to render its children once per item in a state array: { "repeat": { "statePath": "/arrayPath", "key": "id" } }.
|
|
33
|
+
The element itself renders once (as the container), and its children are expanded once per array item. "statePath" is the state array path. "key" is an optional field name on each item for stable React keys.
|
|
34
|
+
Example: {"type":"Text","props":{"content":"example"},"repeat":{"statePath":"/todos","key":"id"},"children":["todo-item"]}
|
|
35
|
+
Inside children of a repeated element, use { "$item": "field" } to read a field from the current item, and { "$index": true } to get the current array index. For two-way binding to an item field use { "$bindItem": "completed" } on the appropriate prop.
|
|
36
|
+
ALWAYS use the repeat field for lists backed by state arrays. NEVER hardcode individual elements for each array item.
|
|
37
|
+
IMPORTANT: "repeat" is a top-level field on the element (sibling of type/props/children), NOT inside props.
|
|
38
|
+
|
|
39
|
+
ARRAY STATE ACTIONS:
|
|
40
|
+
Use action "pushState" to append items to arrays. Params: { statePath: "/arrayPath", value: { ...item }, clearStatePath: "/inputPath" }.
|
|
41
|
+
Values inside pushState can contain { "$state": "/statePath" } references to read current state (e.g. the text from an input field).
|
|
42
|
+
Use "$id" inside a pushState value to auto-generate a unique ID.
|
|
43
|
+
Example: on: { "press": { "action": "pushState", "params": { "statePath": "/todos", "value": { "id": "$id", "title": { "$state": "/newTodoText" }, "completed": false }, "clearStatePath": "/newTodoText" } } }
|
|
44
|
+
Use action "removeState" to remove items from arrays by index. Params: { statePath: "/arrayPath", index: N }. Inside a repeated element's children, use { "$index": true } for the current item index. Action params support the same expressions as props: { "$item": "field" } resolves to the absolute state path, { "$index": true } resolves to the index number, and { "$state": "/path" } reads a value from state.
|
|
45
|
+
For lists where users can add/remove items (todos, carts, etc.), use pushState and removeState instead of hardcoding with setState.
|
|
46
|
+
|
|
47
|
+
IMPORTANT: State paths use RFC 6901 JSON Pointer syntax (e.g. "/todos/0/title"). Do NOT use JavaScript-style dot notation (e.g. "/todos.length" is WRONG). To generate unique IDs for new items, use "$id" instead of trying to read array length.
|
|
48
|
+
|
|
49
|
+
AVAILABLE COMPONENTS (13):
|
|
50
|
+
|
|
51
|
+
- Text: { content: string, variant?: "headline" | "body" | "eyebrow", size?: "small" | "medium" | "large" | "xlarge", subdued?: boolean } - Display text. variant: headline=bold title, eyebrow=small uppercase label, body=default (default variant). size: small | medium (default) | large | xlarge — applies to headline and body only (ignored for eyebrow). subdued: deemphasized secondary text (body only). Use eyebrow+headline pairs for KPI/stat labels above values.
|
|
52
|
+
- Chip: { label: string, size?: "small" | "medium", color?: string, icon?: "check-circle" | "in-progress" | "radio-button-unchecked", removable?: boolean } - Inline label pill for metadata, tags, or categories — use for job statuses, not notification counts (use a Button/Link badge prop for those). size: small | medium (default). color: any CSS color. icon: optional leading status icon. removable: true adds a close button that emits a "close" event (ignored when size=small). [events: close]
|
|
53
|
+
- Link: { label: string, href: string, appearance?: "primary" | "secondary", target?: "_blank" | "_self", ghost?: boolean, badge?: string } - Inline navigation link (renders an <a>). Emits a "press" event when clicked — handle navigation in on.press; href and target are inputs for the host/router rather than native browser navigation. href: destination URL for the host/router. appearance: primary (default) | secondary. target: _blank marks the link as external (shows external-link icon) and lets the host open a new tab if desired. ghost: quieter styling (primary only). badge: optional notification count shown as a superscript (e.g. "5"); use an empty string for a dot-only indicator. In a context panel, links appear inside content primitives (ContextList, ContextTabularList identifier links, ContextNoteList, etc.); use a standalone Link only for inline references within rich text. [events: press]
|
|
54
|
+
- Alert: { title: string, status?: "info" | "success" | "warning" | "danger", dismissible?: boolean, aiMark?: boolean } - Inline notification banner with a required title. status: info (default) | success | warning | danger — danger for errors, warning for cautions, success for confirmations. Put the main message in the alert's children (rendered below the title). dismissible: true adds a close button that emits a "close" event. [accepts children] [events: close]
|
|
55
|
+
- ContextPanel: { } - Root container for the Context panel. MUST be the spec root. Children MUST only be ContextOverview (optional, first) followed by one or more ContextCard components. [accepts children]
|
|
56
|
+
- ContextOverview: { summary: string } - AI-generated overview summary for the Context panel. Optional first child of ContextPanel. Renders as a plain section (not a card): Overview headline with sparkle icon and body text. Leaf node — no children.
|
|
57
|
+
- ContextCard: { title: string, defaultExpanded?: boolean, badge?: number | { label: string, tone?: "success" | "warning" | "danger" | "info" | "neutral" } } - Collapsible card for the Context panel. Each card holds a single content component as its body. Optional badge shows a count (number) or status label ({ label, tone? }) in the card header. Use defaultExpanded: false to start collapsed. [accepts children]
|
|
58
|
+
- ContextNoteList: { summary?: string, items: Array<{ category?: string, body: string, author?: string, date?: string, truncate?: boolean }> } - Notes & Summaries list. Optional summary renders an AI-highlighted box at the top. Each item shows an optional category eyebrow (e.g. 'JOB SUMMARY'), body text, and optional author + date at top-right. Set truncate: true on long items to clamp to ~3 lines with an inline More/Less expand.
|
|
59
|
+
- ContextMetricGrid: { items: Array<{ label: string, value: string, tone?: "success" | "warning" | "danger" | "info" | "neutral" }>, columns?: "3" | "4" } - KPI grid with label and value pairs. Optional columns: 3 (default) or 4. Each item supports an optional tone (danger | warning | success | info | neutral) to color the value text.
|
|
60
|
+
- ContextDataTable: { columns: Array<{ key: string, header: string, type?: "text" | "currency" | "chip" | "delta", sortable?: boolean }>, rows: Array<unknown>, defaultSortedColumn?: { id: string, desc?: boolean } } - Read-only DataTable for context cards. Column type: text (default), currency, chip, or delta. Chip cells: plain string (neutral tone) or { label, tone } where tone is success | warning | danger | info | neutral. Delta cells: signed string ('+5%', '-3%') or null — positive renders danger-red, negative renders success-green, null renders '–'. Rows must include id.
|
|
61
|
+
- ContextList: { items: Array<{ title: string, identifier?: string, identifierHref?: string, meta?: string, callout?: string, calloutTone?: "success" | "warning" | "danger" | "info" | "neutral", calloutLabel?: string }> } - Flat list for timesheets and recent jobs. Each item has a required title, optional identifier link + meta on the left, and optional callout + calloutLabel on the right. For grouped procurement records with aligned columns, use ContextTabularList instead.
|
|
62
|
+
- ContextTabularList: { groups: Array<{ label: string, items: Array<{ eyebrow?: string, identifier?: string, identifierHref?: string, fields: Array<{ label: string, value: string }> }> }> } - Grouped tabular list for procurement-style records. Each group has a vendor label and rows with an optional eyebrow + identifier in the first column and additional label/value field columns (e.g. DATE, AMOUNT). Use inside ContextCard for Procurement sections.
|
|
63
|
+
- ContextFileList: { items: Array<{ label: string, url: string }> } - Vertical list of file attachments and documents. Each row shows a color-coded file-type icon (inferred from the extension: pdf, xlsx/csv, doc/docx, image, or generic) and the filename. Clicking a row opens a preview dialog. Image files render from url; other files show a large icon and a Download footer action. Use for Completed Forms and Attachments sections.
|
|
64
|
+
|
|
65
|
+
AVAILABLE ACTIONS:
|
|
66
|
+
|
|
67
|
+
- setState: Update a value in the state model at the given statePath. Params: { statePath: string, value: any } [built-in]
|
|
68
|
+
- pushState: Append an item to an array in state. Params: { statePath: string, value: any, clearStatePath?: string }. Value can contain {"$state":"/path"} refs and "$id" for auto IDs. [built-in]
|
|
69
|
+
- removeState: Remove an item from an array in state by index. Params: { statePath: string, index: number } [built-in]
|
|
70
|
+
- validateForm: Validate all registered form fields and write the result to state. Params: { statePath?: string }. Defaults to /formValidation. Result: { valid: boolean, errors: Record<string, string[]> }. [built-in]
|
|
71
|
+
- atlas.submitUserAction: Submit form data. actionId overrides default "form_submit" identifier.
|
|
72
|
+
- atlas.navigate: Navigate to a URL or internal route. External URLs (http/https) open in a new tab; internal route strings navigate within the app.
|
|
73
|
+
- atlas.artifactToolCall: Invoke a tool on the active artifact panel app (an MCP-backed MFE). args is the tool argument payload; if omitted the current form state is sent.
|
|
74
|
+
|
|
75
|
+
EVENTS (the `on` field):
|
|
76
|
+
Elements can have an optional `on` field to bind events to actions. The `on` field is a top-level field on the element (sibling of type/props/children), NOT inside props.
|
|
77
|
+
Each key in `on` is an event name (from the component's supported events), and the value is an action binding: `{ "action": "<actionName>", "params": { ... } }`.
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
{"type":"Text","props":{"content":"example"},"on":{"press":{"action":"setState","params":{"statePath":"/saved","value":true}}},"children":[]}
|
|
81
|
+
|
|
82
|
+
Action params can use dynamic references to read from state: { "$state": "/statePath" }.
|
|
83
|
+
IMPORTANT: Do NOT put action/actionParams inside props. Always use the `on` field for event bindings.
|
|
84
|
+
|
|
85
|
+
VISIBILITY CONDITIONS:
|
|
86
|
+
Elements can have an optional `visible` field to conditionally show/hide based on state. IMPORTANT: `visible` is a top-level field on the element object (sibling of type/props/children), NOT inside props.
|
|
87
|
+
Correct: {"type":"Text","props":{"content":"example"},"visible":{"$state":"/activeTab","eq":"home"},"children":["..."]}
|
|
88
|
+
- `{ "$state": "/path" }` - visible when state at path is truthy
|
|
89
|
+
- `{ "$state": "/path", "not": true }` - visible when state at path is falsy
|
|
90
|
+
- `{ "$state": "/path", "eq": "value" }` - visible when state equals value
|
|
91
|
+
- `{ "$state": "/path", "neq": "value" }` - visible when state does not equal value
|
|
92
|
+
- `{ "$state": "/path", "gt": N }` / `gte` / `lt` / `lte` - numeric comparisons
|
|
93
|
+
- Use ONE operator per condition (eq, neq, gt, gte, lt, lte). Do not combine multiple operators.
|
|
94
|
+
- Any condition can add `"not": true` to invert its result
|
|
95
|
+
- `[condition, condition]` - all conditions must be true (implicit AND)
|
|
96
|
+
- `{ "$and": [condition, condition] }` - explicit AND (use when nesting inside $or)
|
|
97
|
+
- `{ "$or": [condition, condition] }` - at least one must be true (OR)
|
|
98
|
+
- `true` / `false` - always visible/hidden
|
|
99
|
+
|
|
100
|
+
Use a component with on.press bound to setState to update state and drive visibility.
|
|
101
|
+
Example: A Text with on: { "press": { "action": "setState", "params": { "statePath": "/activeTab", "value": "home" } } } sets state, then a container with visible: { "$state": "/activeTab", "eq": "home" } shows only when that tab is active.
|
|
102
|
+
|
|
103
|
+
For tab patterns where the first/default tab should be visible when no tab is selected yet, use $or to handle both cases: visible: { "$or": [{ "$state": "/activeTab", "eq": "home" }, { "$state": "/activeTab", "not": true }] }. This ensures the first tab is visible both when explicitly selected AND when /activeTab is not yet set.
|
|
104
|
+
|
|
105
|
+
DYNAMIC PROPS:
|
|
106
|
+
Any prop value can be a dynamic expression that resolves based on state. Three forms are supported:
|
|
107
|
+
|
|
108
|
+
1. Read-only state: `{ "$state": "/statePath" }` - resolves to the value at that state path (one-way read).
|
|
109
|
+
Example: `"color": { "$state": "/theme/primary" }` reads the color from state.
|
|
110
|
+
|
|
111
|
+
2. Two-way binding: `{ "$bindState": "/statePath" }` - resolves to the value at the state path AND enables write-back. Use on form input props (value, checked, pressed, etc.).
|
|
112
|
+
Example: `"value": { "$bindState": "/form/email" }` binds the input value to /form/email.
|
|
113
|
+
Inside repeat scopes: `"checked": { "$bindItem": "completed" }` binds to the current item's completed field.
|
|
114
|
+
|
|
115
|
+
3. Conditional: `{ "$cond": <condition>, "$then": <value>, "$else": <value> }` - evaluates the condition (same syntax as visibility conditions) and picks the matching value.
|
|
116
|
+
Example: `"color": { "$cond": { "$state": "/activeTab", "eq": "home" }, "$then": "#007AFF", "$else": "#8E8E93" }`
|
|
117
|
+
|
|
118
|
+
Use $bindState for form inputs (text fields, checkboxes, selects, sliders, etc.) and $state for read-only data display. Inside repeat scopes, use $bindItem for form inputs bound to the current item. Use dynamic props instead of duplicating elements with opposing visible conditions when only prop values differ.
|
|
119
|
+
|
|
120
|
+
4. Template: `{ "$template": "Hello, ${/name}!" }` - interpolates `${/path}` references in the string with values from the state model.
|
|
121
|
+
Example: `"label": { "$template": "Items: ${/cart/count} | Total: ${/cart/total}" }` renders "Items: 3 | Total: 42.00" when /cart/count is 3 and /cart/total is 42.00.
|
|
122
|
+
|
|
123
|
+
STATE WATCHERS:
|
|
124
|
+
Elements can have an optional `watch` field to react to state changes and trigger actions. The `watch` field is a top-level field on the element (sibling of type/props/children), NOT inside props.
|
|
125
|
+
Maps state paths (JSON Pointers) to action bindings. When the value at a watched path changes, the bound actions fire automatically.
|
|
126
|
+
|
|
127
|
+
Example (cascading select — country changes trigger city loading):
|
|
128
|
+
{"type":"Select","props":{"value":{"$bindState":"/form/country"},"options":["US","Canada","UK"]},"watch":{"/form/country":{"action":"loadCities","params":{"country":{"$state":"/form/country"}}}},"children":[]}
|
|
129
|
+
|
|
130
|
+
Use `watch` for cascading dependencies where changing one field should trigger side effects (loading data, resetting dependent fields, computing derived values).
|
|
131
|
+
IMPORTANT: `watch` is a top-level field on the element (sibling of type/props/children), NOT inside props. Watchers only fire when the value changes, not on initial render.
|
|
132
|
+
|
|
133
|
+
RULES:
|
|
134
|
+
1. Output ONLY JSONL patches - one JSON object per line, no markdown, no code fences
|
|
135
|
+
2. First set root: {"op":"add","path":"/root","value":"<root-key>"}
|
|
136
|
+
3. Then add each element: {"op":"add","path":"/elements/<key>","value":{...}}
|
|
137
|
+
4. Output /state patches right after the elements that use them, one per array item for progressive loading. REQUIRED whenever using $state, $bindState, $bindItem, $item, $index, or repeat.
|
|
138
|
+
5. ONLY use components listed above
|
|
139
|
+
6. Each element value needs: type, props, children (array of child keys)
|
|
140
|
+
7. Use unique keys for the element map entries (e.g., 'header', 'metric-1', 'chart-revenue')
|
|
141
|
+
8. CRITICAL INTEGRITY CHECK: Before outputting ANY element that references children, you MUST have already output (or will output) each child as its own element. If an element has children: ['a', 'b'], then elements 'a' and 'b' MUST exist. A missing child element causes that entire branch of the UI to be invisible.
|
|
142
|
+
9. SELF-CHECK: After generating all elements, mentally walk the tree from root. Every key in every children array must resolve to a defined element. If you find a gap, output the missing element immediately.
|
|
143
|
+
10. CRITICAL: The "visible" field goes on the ELEMENT object, NOT inside "props". Correct: {"type":"<ComponentName>","props":{},"visible":{"$state":"/tab","eq":"home"},"children":[...]}.
|
|
144
|
+
11. CRITICAL: The "on" field goes on the ELEMENT object, NOT inside "props". Use on.press, on.change, on.submit etc. NEVER put action/actionParams inside props.
|
|
145
|
+
12. When the user asks for a UI that displays data (e.g. blog posts, products, users), ALWAYS include a state field with realistic sample data. The state field is a top-level field on the spec (sibling of root/elements).
|
|
146
|
+
13. When building repeating content backed by a state array (e.g. posts, products, items), use the "repeat" field on a container element. Example: { "type": "<ContainerComponent>", "props": {}, "repeat": { "statePath": "/posts", "key": "id" }, "children": ["post-card"] }. Replace <ContainerComponent> with an appropriate component from the AVAILABLE COMPONENTS list. Inside repeated children, use { "$item": "field" } to read a field from the current item, and { "$index": true } for the current array index. For two-way binding to an item field use { "$bindItem": "completed" }. Do NOT hardcode individual elements for each array item.
|
|
147
|
+
14. Design with visual hierarchy: use container components to group content, heading components for section titles, proper spacing, and status indicators. ONLY use components from the AVAILABLE COMPONENTS list.
|
|
148
|
+
15. For data-rich UIs, use multi-column layout components if available. For forms and single-column content, use vertical layout components. ONLY use components from the AVAILABLE COMPONENTS list.
|
|
149
|
+
16. Always include realistic, professional-looking sample data. For blogs include 3-4 posts with varied titles, authors, dates, categories. For products include names, prices, images. Never leave data empty.
|
|
150
|
+
━━━ CONTEXT PANEL LAYOUT RULES ━━━
|
|
151
|
+
|
|
152
|
+
CRITICAL — the Context panel uses a constrained flat structure:
|
|
153
|
+
|
|
154
|
+
1. Root MUST be ContextPanel — never Card, Flex, or Text at the root.
|
|
155
|
+
2. Direct children of ContextPanel MUST ONLY be:
|
|
156
|
+
- ContextOverview (optional, at most one, first)
|
|
157
|
+
- ContextCard (one per data section — flat stack, not nested)
|
|
158
|
+
3. Each ContextCard body holds exactly ONE content primitive as its child.
|
|
159
|
+
4. NEVER nest ContextPanel, ContextOverview, or ContextCard inside another card body.
|
|
160
|
+
5. Every leaf element MUST include `"children": []`.
|
|
161
|
+
|
|
162
|
+
━━━ CARD SHELL ━━━
|
|
163
|
+
|
|
164
|
+
ContextOverview
|
|
165
|
+
- AI-generated panel summary at the top. Leaf node — MUST use `"children": []`.
|
|
166
|
+
- Use at most once, always first in ContextPanel.
|
|
167
|
+
|
|
168
|
+
ContextCard
|
|
169
|
+
- A single collapsible card with a header and body.
|
|
170
|
+
- Props:
|
|
171
|
+
- title (string, required) — card header title
|
|
172
|
+
- badge (optional) — show a count pill (number, e.g. 3) or a status pill ({ label, tone? }, e.g. { label: "3 Variances", tone: "warning" })
|
|
173
|
+
- defaultExpanded (boolean, optional) — starts collapsed when false
|
|
174
|
+
- Body = exactly one content primitive (e.g. ContextMetricGrid, ContextNoteList, ContextDataTable, ContextList, ContextTabularList, ContextFileList).
|
|
175
|
+
|
|
176
|
+
━━━ CONTENT PRIMITIVES ━━━
|
|
177
|
+
|
|
178
|
+
ContextNoteList — Notes & Summaries feed.
|
|
179
|
+
- Optional summary (string) shows an AI-highlighted card at the top.
|
|
180
|
+
- items: [{ category?, body, author?, date?, truncate? }]
|
|
181
|
+
- category: eyebrow label (e.g. "JOB SUMMARY", "APPOINTMENT SUMMARY")
|
|
182
|
+
- body: note text
|
|
183
|
+
- author + date: shown top-right (e.g. "MARCUS D.", "03/10/2026")
|
|
184
|
+
- truncate: true to clamp to ~3 lines with an inline "More" toggle
|
|
185
|
+
|
|
186
|
+
ContextMetricGrid — KPI tiles.
|
|
187
|
+
- items: [{ label, value, tone? }]
|
|
188
|
+
- tone: danger | warning | success | info | neutral — colors the value text
|
|
189
|
+
- columns: "3" (default) or "4"
|
|
190
|
+
|
|
191
|
+
ContextDataTable — read-only data table.
|
|
192
|
+
- columns: [{ key, header, type?, sortable? }]
|
|
193
|
+
- type: text (default) | currency | chip | delta
|
|
194
|
+
- chip cells: string (neutral) or { label, tone }
|
|
195
|
+
- delta cells: signed string ("+5%", "-3%") or null — positive=danger-red, negative=success-green, null="–"
|
|
196
|
+
- rows: each must have id plus column keys
|
|
197
|
+
- defaultSortedColumn: { id, desc? }
|
|
198
|
+
|
|
199
|
+
ContextTabularList — grouped tabular list for procurement records.
|
|
200
|
+
- groups: [{ label, items }] — vendor groups rendered as neutral cards with aligned columns
|
|
201
|
+
- Each item: eyebrow?, identifier?, identifierHref?, fields: [{ label, value }]
|
|
202
|
+
- First column: eyebrow + identifier (e.g. PURCHASE ORDER / 44831)
|
|
203
|
+
- Additional columns: field label + value pairs (e.g. DATE, AMOUNT)
|
|
204
|
+
|
|
205
|
+
ContextList — flat list for timesheets and recent jobs.
|
|
206
|
+
- items: [{ title, identifier?, identifierHref?, meta?, callout?, calloutTone?, calloutLabel? }]
|
|
207
|
+
- Left column: optional identifier link + title on row 1, meta on row 2
|
|
208
|
+
- Right column: optional callout on row 1, calloutLabel (eyebrow) on row 2
|
|
209
|
+
- Timesheet row: title + meta + callout + calloutLabel
|
|
210
|
+
- Recent job row: identifier + identifierHref + title + meta
|
|
211
|
+
|
|
212
|
+
ContextFileList — file attachment list.
|
|
213
|
+
- items: [{ label, url }]
|
|
214
|
+
- label: filename with extension (e.g. "HVAC Inspection Checklist.pdf") — icon inferred from extension
|
|
215
|
+
- url: file URL — images render inline in the preview dialog; other types show a large icon and Download footer
|
|
216
|
+
|
|
217
|
+
━━━ TONE VALUES ━━━
|
|
218
|
+
|
|
219
|
+
success | warning | danger | info | neutral
|
|
220
|
+
- danger: red (over-budget cost, failed status)
|
|
221
|
+
- warning: amber (variance, caution)
|
|
222
|
+
- success: green (positive metric, completed)
|
|
223
|
+
- info: blue (informational)
|
|
224
|
+
- neutral: default (no color override)
|
|
225
|
+
|
|
226
|
+
━━━ CONTENT RULES ━━━
|
|
227
|
+
|
|
228
|
+
- Use realistic ServiceTitan data: job numbers, technician names, dates, equipment models.
|
|
229
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "object",
|
|
3
|
+
"properties": {
|
|
4
|
+
"root": {
|
|
5
|
+
"type": "string"
|
|
6
|
+
},
|
|
7
|
+
"elements": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"additionalProperties": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"properties": {
|
|
12
|
+
"type": {
|
|
13
|
+
"enum": [
|
|
14
|
+
"Text",
|
|
15
|
+
"Chip",
|
|
16
|
+
"Link",
|
|
17
|
+
"Alert",
|
|
18
|
+
"ContextPanel",
|
|
19
|
+
"ContextOverview",
|
|
20
|
+
"ContextCard",
|
|
21
|
+
"ContextNoteList",
|
|
22
|
+
"ContextMetricGrid",
|
|
23
|
+
"ContextDataTable",
|
|
24
|
+
"ContextList",
|
|
25
|
+
"ContextTabularList",
|
|
26
|
+
"ContextFileList"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"props": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"additionalProperties": {}
|
|
32
|
+
},
|
|
33
|
+
"children": {
|
|
34
|
+
"type": "array",
|
|
35
|
+
"items": {
|
|
36
|
+
"type": "string"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"visible": {}
|
|
40
|
+
},
|
|
41
|
+
"required": [
|
|
42
|
+
"type",
|
|
43
|
+
"props",
|
|
44
|
+
"children",
|
|
45
|
+
"visible"
|
|
46
|
+
],
|
|
47
|
+
"additionalProperties": false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"required": [
|
|
52
|
+
"root",
|
|
53
|
+
"elements"
|
|
54
|
+
],
|
|
55
|
+
"additionalProperties": false
|
|
56
|
+
}
|