@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.
Files changed (126) hide show
  1. package/README.md +335 -0
  2. package/dist/catalog/context/catalog-prompt.md +229 -0
  3. package/dist/catalog/context/schema.json +56 -0
  4. package/dist/catalog/core/catalog-prompt.md +306 -0
  5. package/dist/catalog/core/schema.json +64 -0
  6. package/dist/catalog/marketing/catalog-prompt.md +307 -0
  7. package/dist/catalog/marketing/schema.json +65 -0
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.js +2126 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/src/context/catalog/catalog-def.d.ts +50 -0
  12. package/dist/src/context/catalog/context-card.catalog.d.ts +19 -0
  13. package/dist/src/context/catalog/context-card.d.ts +8 -0
  14. package/dist/src/context/catalog/context-data-table.catalog.d.ts +34 -0
  15. package/dist/src/context/catalog/context-data-table.css.d.ts +2 -0
  16. package/dist/src/context/catalog/context-data-table.d.ts +6 -0
  17. package/dist/src/context/catalog/context-file-list.catalog.d.ts +11 -0
  18. package/dist/src/context/catalog/context-file-list.css.d.ts +16 -0
  19. package/dist/src/context/catalog/context-file-list.d.ts +6 -0
  20. package/dist/src/context/catalog/context-list.catalog.d.ts +48 -0
  21. package/dist/src/context/catalog/context-list.css.d.ts +16 -0
  22. package/dist/src/context/catalog/context-list.d.ts +6 -0
  23. package/dist/src/context/catalog/context-metric-grid.catalog.d.ts +22 -0
  24. package/dist/src/context/catalog/context-metric-grid.css.d.ts +13 -0
  25. package/dist/src/context/catalog/context-metric-grid.d.ts +6 -0
  26. package/dist/src/context/catalog/context-note-list.catalog.d.ts +15 -0
  27. package/dist/src/context/catalog/context-note-list.d.ts +6 -0
  28. package/dist/src/context/catalog/context-overview.catalog.d.ts +8 -0
  29. package/dist/src/context/catalog/context-overview.css.d.ts +2 -0
  30. package/dist/src/context/catalog/context-overview.d.ts +6 -0
  31. package/dist/src/context/catalog/context-panel.catalog.d.ts +6 -0
  32. package/dist/src/context/catalog/context-panel.css.d.ts +1 -0
  33. package/dist/src/context/catalog/context-panel.d.ts +6 -0
  34. package/dist/src/context/catalog/context-tabular-list.catalog.d.ts +19 -0
  35. package/dist/src/context/catalog/context-tabular-list.css.d.ts +17 -0
  36. package/dist/src/context/catalog/context-tabular-list.d.ts +6 -0
  37. package/dist/src/context/catalog/index.d.ts +10 -0
  38. package/dist/src/context/catalog/schemas.d.ts +151 -0
  39. package/dist/src/context/components/collapsible-card.css.d.ts +41 -0
  40. package/dist/src/context/components/collapsible-card.d.ts +14 -0
  41. package/dist/src/context/components/context-ai-summary.css.d.ts +7 -0
  42. package/dist/src/context/components/context-ai-summary.d.ts +4 -0
  43. package/dist/src/context/components/context-chip-tone.d.ts +10 -0
  44. package/dist/src/context/components/context-note-list.css.d.ts +10 -0
  45. package/dist/src/context/components/context-render-error-boundary.d.ts +21 -0
  46. package/dist/src/context/constants.d.ts +6 -0
  47. package/dist/src/context/index.d.ts +4 -0
  48. package/dist/src/context/validate-context-spec-deep.d.ts +9 -0
  49. package/dist/src/context/validate-context-spec.d.ts +11 -0
  50. package/dist/src/core/actions.d.ts +28 -0
  51. package/dist/src/core/catalog/alert.catalog.d.ts +17 -0
  52. package/dist/src/core/catalog/alert.d.ts +9 -0
  53. package/dist/src/core/catalog/bar-chart.catalog.d.ts +22 -0
  54. package/dist/src/core/catalog/bar-chart.d.ts +6 -0
  55. package/dist/src/core/catalog/button.catalog.d.ts +24 -0
  56. package/dist/src/core/catalog/button.d.ts +7 -0
  57. package/dist/src/core/catalog/card.catalog.d.ts +20 -0
  58. package/dist/src/core/catalog/card.d.ts +8 -0
  59. package/dist/src/core/catalog/catalog-def.d.ts +484 -0
  60. package/dist/src/core/catalog/chart-utils.d.ts +2 -0
  61. package/dist/src/core/catalog/chart.css.d.ts +1 -0
  62. package/dist/src/core/catalog/checkbox.catalog.d.ts +11 -0
  63. package/dist/src/core/catalog/checkbox.d.ts +8 -0
  64. package/dist/src/core/catalog/chip.catalog.d.ts +20 -0
  65. package/dist/src/core/catalog/chip.d.ts +7 -0
  66. package/dist/src/core/catalog/divider.catalog.d.ts +25 -0
  67. package/dist/src/core/catalog/divider.d.ts +6 -0
  68. package/dist/src/core/catalog/donut-chart.catalog.d.ts +19 -0
  69. package/dist/src/core/catalog/donut-chart.d.ts +6 -0
  70. package/dist/src/core/catalog/flex.catalog.d.ts +55 -0
  71. package/dist/src/core/catalog/flex.d.ts +8 -0
  72. package/dist/src/core/catalog/grid.catalog.d.ts +21 -0
  73. package/dist/src/core/catalog/grid.css.d.ts +4 -0
  74. package/dist/src/core/catalog/grid.d.ts +8 -0
  75. package/dist/src/core/catalog/guidance-card-column-width.d.ts +12 -0
  76. package/dist/src/core/catalog/guidance-card.catalog.d.ts +58 -0
  77. package/dist/src/core/catalog/guidance-card.d.ts +9 -0
  78. package/dist/src/core/catalog/icon.catalog.d.ts +19 -0
  79. package/dist/src/core/catalog/icon.d.ts +6 -0
  80. package/dist/src/core/catalog/index.d.ts +7 -0
  81. package/dist/src/core/catalog/input.catalog.d.ts +20 -0
  82. package/dist/src/core/catalog/input.d.ts +8 -0
  83. package/dist/src/core/catalog/line-chart.catalog.d.ts +18 -0
  84. package/dist/src/core/catalog/line-chart.d.ts +6 -0
  85. package/dist/src/core/catalog/link.catalog.d.ts +20 -0
  86. package/dist/src/core/catalog/link.d.ts +7 -0
  87. package/dist/src/core/catalog/listbox.catalog.d.ts +15 -0
  88. package/dist/src/core/catalog/listbox.css.d.ts +9 -0
  89. package/dist/src/core/catalog/listbox.d.ts +8 -0
  90. package/dist/src/core/catalog/progress-bar.catalog.d.ts +11 -0
  91. package/dist/src/core/catalog/progress-bar.d.ts +6 -0
  92. package/dist/src/core/catalog/radio-group.catalog.d.ts +15 -0
  93. package/dist/src/core/catalog/radio-group.d.ts +8 -0
  94. package/dist/src/core/catalog/schemas.d.ts +436 -0
  95. package/dist/src/core/catalog/switch.catalog.d.ts +11 -0
  96. package/dist/src/core/catalog/switch.d.ts +8 -0
  97. package/dist/src/core/catalog/text.catalog.d.ts +20 -0
  98. package/dist/src/core/catalog/text.d.ts +6 -0
  99. package/dist/src/core/catalog/textarea.catalog.d.ts +15 -0
  100. package/dist/src/core/catalog/textarea.d.ts +8 -0
  101. package/dist/src/generate.d.ts +9 -0
  102. package/dist/src/index.d.ts +9 -0
  103. package/dist/src/marketing/catalog/catalog.d.ts +1 -0
  104. package/dist/src/marketing/catalog/components/index.d.ts +14 -0
  105. package/dist/src/marketing/catalog/components/revenue-card.catalog.d.ts +15 -0
  106. package/dist/src/marketing/catalog/components/revenue-card.css.d.ts +15 -0
  107. package/dist/src/marketing/catalog/components/revenue-card.d.ts +6 -0
  108. package/dist/src/marketing/catalog/index.d.ts +6 -0
  109. package/dist/src/marketing/catalog/scoped-catalogs.d.ts +4 -0
  110. package/dist/src/registry-utils.d.ts +6 -0
  111. package/dist/src/registry.d.ts +9 -0
  112. package/dist/src/renderers/context-spec-renderer.d.ts +6 -0
  113. package/dist/src/renderers/internal/enrichments/button-actions.d.ts +7 -0
  114. package/dist/src/renderers/internal/enrichments/useEnrichedSpec.d.ts +3 -0
  115. package/dist/src/renderers/sectioned-context-renderer.css.d.ts +1 -0
  116. package/dist/src/renderers/sectioned-context-renderer.d.ts +36 -0
  117. package/dist/src/renderers/spec-renderer.d.ts +30 -0
  118. package/dist/src/renderers/types.d.ts +4 -0
  119. package/dist/src/renderers/wrap-context-registry.d.ts +3 -0
  120. package/dist/src/scoped-catalog.d.ts +64 -0
  121. package/dist/src/test/setup-dom.d.ts +1 -0
  122. package/dist/src/types.d.ts +17 -0
  123. package/dist/src/utils/cx.d.ts +2 -0
  124. package/dist/src/utils/defined-props.d.ts +6 -0
  125. package/dist/styles.css +1 -0
  126. 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
+ }