@servicetitan/json-render-react 0.3.1 → 0.3.2-anv-5204-0ef51ea

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 ADDED
@@ -0,0 +1,334 @@
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), [carto-react-kit](../carto-react-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 { CartoTheme } from "@servicetitan/carto-react-kit";
33
+ import "@servicetitan/carto-react-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
+ <CartoTheme 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
+ </CartoTheme>
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[carto-react-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.toolCall`.
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 carto-react-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.toolCall` | `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) | carto-react-kit | `@servicetitan/carto-tokens` via CartoTheme |
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
+ | 6007 | json-render-react (this package) |
274
+ | 6006 | carto-react-kit |
275
+
276
+ ```sh
277
+ pnpm storybook --filter @servicetitan/json-render-react
278
+ ```
279
+
280
+ | Sidebar group | Content |
281
+ | --- | --- |
282
+ | Guide | Package narrative, spec format, Atlas integration |
283
+ | Spec Playground | Live JSON editor |
284
+ | Core Catalog | One story file per core component |
285
+ | Scoped Catalogs | Team-specific components |
286
+ | Context | Context panel components |
287
+ | Examples | Composed demos, tool-call wiring |
288
+
289
+ Catalog stories export exactly **Default** (Spec panel) + **StickerSheet** (Chromatic baseline).
290
+
291
+ ## Scripts
292
+
293
+ | Command | Purpose |
294
+ | --- | --- |
295
+ | `pnpm build` | Library dist + `dist/catalog/*` artifacts |
296
+ | `pnpm schema:gen` | Regenerate catalog artifacts only |
297
+ | `pnpm docs:gen` | Scaffold MDX for new catalog components |
298
+ | `pnpm storybook` | Dev server on port 6007 |
299
+ | `pnpm test` | Unit tests |
300
+ | `pnpm test-storybook` | Browser tests via Storybook |
301
+ | `pnpm typecheck` / `pnpm lint` | CI checks |
302
+
303
+ ## Agent skills
304
+
305
+ Claude/Cursor skills in `.claude/skills/`:
306
+
307
+ | Skill | When to use |
308
+ | --- | --- |
309
+ | [authoring-json-render-specs](../../.claude/skills/authoring-json-render-specs/SKILL.md) | Writing or reviewing JSON specs |
310
+ | [authoring-json-render-catalog-components](../../.claude/skills/authoring-json-render-catalog-components/SKILL.md) | New catalog entries, renderers, actions |
311
+ | [authoring-json-render-scoped-catalogs](../../.claude/skills/authoring-json-render-scoped-catalogs/SKILL.md) | Team-scoped catalogs |
312
+ | [authoring-json-render-context-panel](../../.claude/skills/authoring-json-render-context-panel/SKILL.md) | Context panel specs and components |
313
+ | [authoring-json-render-react-storybook-stories](../../.claude/skills/authoring-json-render-react-storybook-stories/SKILL.md) | Stories, MDX, Spec panel, Chromatic |
314
+
315
+ ## External references
316
+
317
+ | Resource | URL |
318
+ | --- | --- |
319
+ | json-render (upstream) | https://github.com/json-render/json-render |
320
+ | Atlas gen-ui integration | https://atlas-docs.st.dev/#/gen-ui-docs |
321
+ | Carto monorepo contributing | [CONTRIBUTING.md](../../CONTRIBUTING.md) |
322
+ | Carto repo scaffolding (Confluence) | https://servicetitan.atlassian.net/wiki/spaces/ADS/pages/4510026867/ANV-4906+-+Carto+Repo+Scaffolding |
323
+
324
+ ## Peer dependencies
325
+
326
+ Consumers must install:
327
+
328
+ - `react`, `react-dom` (`>=18 <20`)
329
+ - `@servicetitan/anvil2`, `@servicetitan/anvil2-ext-charts`
330
+ - `@servicetitan/carto-react-kit`
331
+ - `@amcharts/amcharts5` (charts)
332
+
333
+ > [!WARNING]
334
+ > `@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`.
@@ -46,7 +46,7 @@ For lists where users can add/remove items (todos, carts, etc.), use pushState a
46
46
 
47
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
48
 
49
- AVAILABLE COMPONENTS (20):
49
+ AVAILABLE COMPONENTS (21):
50
50
 
51
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
52
  - Card: { title?: string, description?: string, background?: "strong" | "stronger", padding?: "0" | "xsmall" | "small" | "medium" | "large" } - White surface that groups a logical section of related content. title: optional headline rendered at the top of the card. description: optional subdued line below the title. Children render below the header and are stacked vertically with consistent spacing — you do not need to wrap them in a Flex. Prefer the built-in title/description over standalone Text children for the heading. background: strong | stronger for nested/elevated surfaces. padding: 0 | xsmall | small | medium (default) | large. Do not nest a Card inside another Card. [accepts children]
@@ -68,6 +68,7 @@ AVAILABLE COMPONENTS (20):
68
68
  - Listbox: { label: string, options: Array<{ value: string, label: string }>, value?: string, disabled?: boolean } - Single-select dropdown for choosing one value from a fixed list of known options. options: [{ value, label }] — value is stored in state, label is shown. Bind the selection with $bindState on value; emits a "change" event on selection. Use only when options are predefined; for free-text user input use Input, and for 2–7 visible mutually-exclusive choices prefer RadioGroup. disabled: true prevents selection. [events: change]
69
69
  - RadioGroup: { label: string, options: Array<{ value: string, label: string }>, value?: string, disabled?: boolean } - Group of radio buttons for picking one option from 2–7 visible mutually-exclusive choices. options: [{ value, label }]. Bind the selection with $bindState on value; emits a "change" event on selection. For longer option lists use Listbox. disabled: true prevents selection. [events: change]
70
70
  - Switch: { label: string, checked?: boolean, disabled?: boolean } - Toggle switch for a single binary on/off setting with immediate effect (no form submit). Prefer over Checkbox for enabling/disabling features. Bind state with $bindState on checked; emits a "change" event when toggled. disabled: true prevents toggling. [events: change]
71
+ - GuidanceCard: { state?: "default" | "accepted" | "rejected" | "skipped", referenceLabel: string, referenceHref?: string, changeSections: Array<{ type: undefined, changes: Array<{ id: string, label: string, from?: string, to?: string, reason?: string }> } | { type: undefined, label: string, changeTables: Array<{ label: string, columns: Array<{ key: string, header: string, minWidth?: "compact" | "standard" | "wide" }>, data: Array<unknown>, reason?: string }> }>, appliedChangeIds?: Array<string>, defaultExpanded?: boolean } - Suggested-change review card for Atlas agent chat. Use in the default state while the user can accept, edit, or dismiss AI-proposed updates to an estimate/invoice. state: default (selectable diffs + actions) | accepted (collapsible accepted summary) | rejected | skipped (collapsible read-only summaries). Bind state and appliedChangeIds with $bindState so apply/reject can update lifecycle state locally. Checkbox selection is managed internally and defaults to all single-change and table-row ids selected. changeSections[] defines ordered body content: type single (changes[] with label/from/to/reason) or type table (label + changeTables[] with editable labels, columns[{ key, header, minWidth? }], data, optional reason). Column minWidth presets: compact (codes/SKUs/quantities) | standard (names/dates) | wide (descriptions/diffs) — omit when flexible. Table cell rendering is defined per cell via string (plain text), { type: text | diff | addition, ... }, or legacy { from, to } diff shorthand. appliedChangeIds filters accepted-state content. referenceHref opens the linked record in a new tab. Emits apply, edit, and dismiss. [events: apply, edit, dismiss]
71
72
 
72
73
  AVAILABLE ACTIONS:
73
74
 
@@ -295,3 +296,11 @@ LINECHART
295
296
  - series optionally names which keys to plot (inferred from the data keys if omitted, max 6). theme: "monochrome" (1–2 series) | "categorical" (3–6 series). showLegend (default true for 2+ series); showDots toggles point markers; unit sets the tooltip suffix.
296
297
  - Do NOT wrap in Card or Flex — use as the root element.
297
298
 
299
+ GUIDANCECARD
300
+ - Use when Atlas proposes suggested changes to an estimate or invoice inline in chat. GuidanceCard is a standalone root — do NOT wrap it in Card or Flex.
301
+ - state: default (selectable diffs + apply/edit/dismiss actions) | accepted (collapsible accepted summary) | rejected | skipped (collapsible read-only summaries). Default is default.
302
+ - Bind `state` and `appliedChangeIds` with $bindState (e.g. `/cardState`, `/appliedChangeIds`) so Storybook and local previews can flip lifecycle state on apply/reject. On apply, chain setState to `"accepted"` and set `/appliedChangeIds` to the ids being applied before atlas.submitUserAction.
303
+ - changeSections[] defines ordered body content: type single (changes[] with id, label, optional from/to/reason) or type table (label + changeTables[] with editable labels, columns, and data rows with id). Table cell rendering is defined per cell: plain string, `{ type: "text" | "diff" | "addition", ... }`, or legacy `{ from, to }` diff shorthand — not on the column.
304
+ - Table column `minWidth` presets (optional): **compact** — codes, SKUs, quantities, unit prices; **standard** — names, categories, dates, single-line labels; **wide** — descriptions, notes, scope text, or any column with diff/addition cells. Omit when the column can flex.
305
+ - Emits apply, edit, and dismiss. Wire actions via on.apply/on.edit/on.dismiss; use atlas.submitUserAction with descriptive actionIds.
306
+
@@ -30,7 +30,8 @@
30
30
  "Textarea",
31
31
  "Listbox",
32
32
  "RadioGroup",
33
- "Switch"
33
+ "Switch",
34
+ "GuidanceCard"
34
35
  ]
35
36
  },
36
37
  "props": {
@@ -46,7 +46,7 @@ For lists where users can add/remove items (todos, carts, etc.), use pushState a
46
46
 
47
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
48
 
49
- AVAILABLE COMPONENTS (21):
49
+ AVAILABLE COMPONENTS (22):
50
50
 
51
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
52
  - Card: { title?: string, description?: string, background?: "strong" | "stronger", padding?: "0" | "xsmall" | "small" | "medium" | "large" } - White surface that groups a logical section of related content. title: optional headline rendered at the top of the card. description: optional subdued line below the title. Children render below the header and are stacked vertically with consistent spacing — you do not need to wrap them in a Flex. Prefer the built-in title/description over standalone Text children for the heading. background: strong | stronger for nested/elevated surfaces. padding: 0 | xsmall | small | medium (default) | large. Do not nest a Card inside another Card. [accepts children]
@@ -68,6 +68,7 @@ AVAILABLE COMPONENTS (21):
68
68
  - Listbox: { label: string, options: Array<{ value: string, label: string }>, value?: string, disabled?: boolean } - Single-select dropdown for choosing one value from a fixed list of known options. options: [{ value, label }] — value is stored in state, label is shown. Bind the selection with $bindState on value; emits a "change" event on selection. Use only when options are predefined; for free-text user input use Input, and for 2–7 visible mutually-exclusive choices prefer RadioGroup. disabled: true prevents selection. [events: change]
69
69
  - RadioGroup: { label: string, options: Array<{ value: string, label: string }>, value?: string, disabled?: boolean } - Group of radio buttons for picking one option from 2–7 visible mutually-exclusive choices. options: [{ value, label }]. Bind the selection with $bindState on value; emits a "change" event on selection. For longer option lists use Listbox. disabled: true prevents selection. [events: change]
70
70
  - Switch: { label: string, checked?: boolean, disabled?: boolean } - Toggle switch for a single binary on/off setting with immediate effect (no form submit). Prefer over Checkbox for enabling/disabling features. Bind state with $bindState on checked; emits a "change" event when toggled. disabled: true prevents toggling. [events: change]
71
+ - GuidanceCard: { state?: "default" | "accepted" | "rejected" | "skipped", referenceLabel: string, referenceHref?: string, changeSections: Array<{ type: undefined, changes: Array<{ id: string, label: string, from?: string, to?: string, reason?: string }> } | { type: undefined, label: string, changeTables: Array<{ label: string, columns: Array<{ key: string, header: string, minWidth?: "compact" | "standard" | "wide" }>, data: Array<unknown>, reason?: string }> }>, appliedChangeIds?: Array<string>, defaultExpanded?: boolean } - Suggested-change review card for Atlas agent chat. Use in the default state while the user can accept, edit, or dismiss AI-proposed updates to an estimate/invoice. state: default (selectable diffs + actions) | accepted (collapsible accepted summary) | rejected | skipped (collapsible read-only summaries). Bind state and appliedChangeIds with $bindState so apply/reject can update lifecycle state locally. Checkbox selection is managed internally and defaults to all single-change and table-row ids selected. changeSections[] defines ordered body content: type single (changes[] with label/from/to/reason) or type table (label + changeTables[] with editable labels, columns[{ key, header, minWidth? }], data, optional reason). Column minWidth presets: compact (codes/SKUs/quantities) | standard (names/dates) | wide (descriptions/diffs) — omit when flexible. Table cell rendering is defined per cell via string (plain text), { type: text | diff | addition, ... }, or legacy { from, to } diff shorthand. appliedChangeIds filters accepted-state content. referenceHref opens the linked record in a new tab. Emits apply, edit, and dismiss. [events: apply, edit, dismiss]
71
72
  - RevenueCard: { title: string, value: string, trend?: "up" | "down" | "flat", delta?: string } - KPI metric card. trend: up=green↑ down=red↓ flat=grey→. delta e.g. "+12% vs last month". Leaf node — no children.
72
73
 
73
74
  AVAILABLE ACTIONS:
@@ -296,3 +297,11 @@ LINECHART
296
297
  - series optionally names which keys to plot (inferred from the data keys if omitted, max 6). theme: "monochrome" (1–2 series) | "categorical" (3–6 series). showLegend (default true for 2+ series); showDots toggles point markers; unit sets the tooltip suffix.
297
298
  - Do NOT wrap in Card or Flex — use as the root element.
298
299
 
300
+ GUIDANCECARD
301
+ - Use when Atlas proposes suggested changes to an estimate or invoice inline in chat. GuidanceCard is a standalone root — do NOT wrap it in Card or Flex.
302
+ - state: default (selectable diffs + apply/edit/dismiss actions) | accepted (collapsible accepted summary) | rejected | skipped (collapsible read-only summaries). Default is default.
303
+ - Bind `state` and `appliedChangeIds` with $bindState (e.g. `/cardState`, `/appliedChangeIds`) so Storybook and local previews can flip lifecycle state on apply/reject. On apply, chain setState to `"accepted"` and set `/appliedChangeIds` to the ids being applied before atlas.submitUserAction.
304
+ - changeSections[] defines ordered body content: type single (changes[] with id, label, optional from/to/reason) or type table (label + changeTables[] with editable labels, columns, and data rows with id). Table cell rendering is defined per cell: plain string, `{ type: "text" | "diff" | "addition", ... }`, or legacy `{ from, to }` diff shorthand — not on the column.
305
+ - Table column `minWidth` presets (optional): **compact** — codes, SKUs, quantities, unit prices; **standard** — names, categories, dates, single-line labels; **wide** — descriptions, notes, scope text, or any column with diff/addition cells. Omit when the column can flex.
306
+ - Emits apply, edit, and dismiss. Wire actions via on.apply/on.edit/on.dismiss; use atlas.submitUserAction with descriptive actionIds.
307
+
@@ -31,6 +31,7 @@
31
31
  "Listbox",
32
32
  "RadioGroup",
33
33
  "Switch",
34
+ "GuidanceCard",
34
35
  "RevenueCard"
35
36
  ]
36
37
  },
@@ -402,6 +402,65 @@ export declare const catalog: import('@json-render/core').Catalog<{
402
402
  events: string[];
403
403
  description: string;
404
404
  };
405
+ GuidanceCard: {
406
+ props: import('zod').ZodObject<{
407
+ state: import('zod').ZodOptional<import('zod').ZodEnum<{
408
+ default: "default";
409
+ accepted: "accepted";
410
+ rejected: "rejected";
411
+ skipped: "skipped";
412
+ }>>;
413
+ referenceLabel: import('zod').ZodString;
414
+ referenceHref: import('zod').ZodOptional<import('zod').ZodString>;
415
+ changeSections: import('zod').ZodArray<import('zod').ZodDiscriminatedUnion<[import('zod').ZodObject<{
416
+ type: import('zod').ZodLiteral<"single">;
417
+ changes: import('zod').ZodArray<import('zod').ZodObject<{
418
+ id: import('zod').ZodString;
419
+ label: import('zod').ZodString;
420
+ from: import('zod').ZodOptional<import('zod').ZodNullable<import('zod').ZodString>>;
421
+ to: import('zod').ZodOptional<import('zod').ZodNullable<import('zod').ZodString>>;
422
+ reason: import('zod').ZodOptional<import('zod').ZodString>;
423
+ }, import('zod/v4/core').$strip>>;
424
+ }, import('zod/v4/core').$strip>, import('zod').ZodObject<{
425
+ type: import('zod').ZodLiteral<"table">;
426
+ label: import('zod').ZodString;
427
+ changeTables: import('zod').ZodArray<import('zod').ZodObject<{
428
+ label: import('zod').ZodString;
429
+ columns: import('zod').ZodArray<import('zod').ZodObject<{
430
+ key: import('zod').ZodString;
431
+ header: import('zod').ZodString;
432
+ minWidth: import('zod').ZodOptional<import('zod').ZodEnum<{
433
+ compact: "compact";
434
+ standard: "standard";
435
+ wide: "wide";
436
+ }>>;
437
+ }, import('zod/v4/core').$strip>>;
438
+ data: import('zod').ZodArray<import('zod').ZodIntersection<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodUnion<readonly [import('zod').ZodString, import('zod').ZodDiscriminatedUnion<[import('zod').ZodObject<{
439
+ type: import('zod').ZodLiteral<"text">;
440
+ value: import('zod').ZodString;
441
+ }, import('zod/v4/core').$strip>, import('zod').ZodObject<{
442
+ type: import('zod').ZodLiteral<"diff">;
443
+ from: import('zod').ZodOptional<import('zod').ZodNullable<import('zod').ZodString>>;
444
+ to: import('zod').ZodOptional<import('zod').ZodNullable<import('zod').ZodString>>;
445
+ }, import('zod/v4/core').$strip>, import('zod').ZodObject<{
446
+ type: import('zod').ZodLiteral<"addition">;
447
+ value: import('zod').ZodString;
448
+ }, import('zod/v4/core').$strip>], "type">, import('zod').ZodObject<{
449
+ from: import('zod').ZodOptional<import('zod').ZodNullable<import('zod').ZodString>>;
450
+ to: import('zod').ZodOptional<import('zod').ZodNullable<import('zod').ZodString>>;
451
+ }, import('zod/v4/core').$strip>]>>, import('zod').ZodObject<{
452
+ id: import('zod').ZodString;
453
+ }, import('zod/v4/core').$strip>>>;
454
+ reason: import('zod').ZodOptional<import('zod').ZodString>;
455
+ }, import('zod/v4/core').$strip>>;
456
+ }, import('zod/v4/core').$strip>], "type">>;
457
+ appliedChangeIds: import('zod').ZodOptional<import('zod').ZodArray<import('zod').ZodString>>;
458
+ defaultExpanded: import('zod').ZodOptional<import('zod').ZodBoolean>;
459
+ }, import('zod/v4/core').$strip>;
460
+ slots: never[];
461
+ events: string[];
462
+ description: string;
463
+ };
405
464
  };
406
465
  actions: {
407
466
  "atlas.submitUserAction": {
@@ -0,0 +1,11 @@
1
+ export declare const GUIDANCE_CARD_COLUMN_WIDTHS: {
2
+ /** Short codes, SKUs, quantities, unit prices, status values. */
3
+ readonly compact: 120;
4
+ /** Names, categories, dates, single-line labels. */
5
+ readonly standard: 180;
6
+ /** Descriptions, notes, scope text, diff/addition cells. */
7
+ readonly wide: 280;
8
+ };
9
+ export type GuidanceCardColumnWidth = keyof typeof GUIDANCE_CARD_COLUMN_WIDTHS;
10
+ export declare function resolveGuidanceCardColumnMinWidth(width: GuidanceCardColumnWidth): number;
11
+ export declare function resolveGuidanceCardColumnMinWidth(width: GuidanceCardColumnWidth | undefined): number | undefined;
@@ -0,0 +1,61 @@
1
+ import { z } from 'zod';
2
+ export declare const guidanceCardCatalog: {
3
+ props: z.ZodObject<{
4
+ state: z.ZodOptional<z.ZodEnum<{
5
+ default: "default";
6
+ accepted: "accepted";
7
+ rejected: "rejected";
8
+ skipped: "skipped";
9
+ }>>;
10
+ referenceLabel: z.ZodString;
11
+ referenceHref: z.ZodOptional<z.ZodString>;
12
+ changeSections: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
13
+ type: z.ZodLiteral<"single">;
14
+ changes: z.ZodArray<z.ZodObject<{
15
+ id: z.ZodString;
16
+ label: z.ZodString;
17
+ from: z.ZodOptional<z.ZodNullable<z.ZodString>>;
18
+ to: z.ZodOptional<z.ZodNullable<z.ZodString>>;
19
+ reason: z.ZodOptional<z.ZodString>;
20
+ }, z.core.$strip>>;
21
+ }, z.core.$strip>, z.ZodObject<{
22
+ type: z.ZodLiteral<"table">;
23
+ label: z.ZodString;
24
+ changeTables: z.ZodArray<z.ZodObject<{
25
+ label: z.ZodString;
26
+ columns: z.ZodArray<z.ZodObject<{
27
+ key: z.ZodString;
28
+ header: z.ZodString;
29
+ minWidth: z.ZodOptional<z.ZodEnum<{
30
+ compact: "compact";
31
+ standard: "standard";
32
+ wide: "wide";
33
+ }>>;
34
+ }, z.core.$strip>>;
35
+ data: z.ZodArray<z.ZodIntersection<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
36
+ type: z.ZodLiteral<"text">;
37
+ value: z.ZodString;
38
+ }, z.core.$strip>, z.ZodObject<{
39
+ type: z.ZodLiteral<"diff">;
40
+ from: z.ZodOptional<z.ZodNullable<z.ZodString>>;
41
+ to: z.ZodOptional<z.ZodNullable<z.ZodString>>;
42
+ }, z.core.$strip>, z.ZodObject<{
43
+ type: z.ZodLiteral<"addition">;
44
+ value: z.ZodString;
45
+ }, z.core.$strip>], "type">, z.ZodObject<{
46
+ from: z.ZodOptional<z.ZodNullable<z.ZodString>>;
47
+ to: z.ZodOptional<z.ZodNullable<z.ZodString>>;
48
+ }, z.core.$strip>]>>, z.ZodObject<{
49
+ id: z.ZodString;
50
+ }, z.core.$strip>>>;
51
+ reason: z.ZodOptional<z.ZodString>;
52
+ }, z.core.$strip>>;
53
+ }, z.core.$strip>], "type">>;
54
+ appliedChangeIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
55
+ defaultExpanded: z.ZodOptional<z.ZodBoolean>;
56
+ }, z.core.$strip>;
57
+ slots: never[];
58
+ events: string[];
59
+ description: string;
60
+ };
61
+ export type GuidanceCardCatalogProps = z.infer<typeof guidanceCardCatalog.props>;
@@ -0,0 +1 @@
1
+ export declare const guidanceLineItemsTableWrap: string;
@@ -0,0 +1,7 @@
1
+ import { z } from 'zod';
2
+ import { guidanceCardCatalog } from './guidance-card.catalog';
3
+ export { guidanceCardCatalog };
4
+ export declare const GuidanceCardRenderer: ({ props, emit, }: {
5
+ props: z.infer<typeof guidanceCardCatalog.props>;
6
+ emit: (event: string) => void;
7
+ }) => import("react/jsx-runtime").JSX.Element;
@@ -377,4 +377,63 @@ export declare const baseComponents: {
377
377
  events: string[];
378
378
  description: string;
379
379
  };
380
+ GuidanceCard: {
381
+ props: import('zod').ZodObject<{
382
+ state: import('zod').ZodOptional<import('zod').ZodEnum<{
383
+ default: "default";
384
+ accepted: "accepted";
385
+ rejected: "rejected";
386
+ skipped: "skipped";
387
+ }>>;
388
+ referenceLabel: import('zod').ZodString;
389
+ referenceHref: import('zod').ZodOptional<import('zod').ZodString>;
390
+ changeSections: import('zod').ZodArray<import('zod').ZodDiscriminatedUnion<[import('zod').ZodObject<{
391
+ type: import('zod').ZodLiteral<"single">;
392
+ changes: import('zod').ZodArray<import('zod').ZodObject<{
393
+ id: import('zod').ZodString;
394
+ label: import('zod').ZodString;
395
+ from: import('zod').ZodOptional<import('zod').ZodNullable<import('zod').ZodString>>;
396
+ to: import('zod').ZodOptional<import('zod').ZodNullable<import('zod').ZodString>>;
397
+ reason: import('zod').ZodOptional<import('zod').ZodString>;
398
+ }, import('zod/v4/core').$strip>>;
399
+ }, import('zod/v4/core').$strip>, import('zod').ZodObject<{
400
+ type: import('zod').ZodLiteral<"table">;
401
+ label: import('zod').ZodString;
402
+ changeTables: import('zod').ZodArray<import('zod').ZodObject<{
403
+ label: import('zod').ZodString;
404
+ columns: import('zod').ZodArray<import('zod').ZodObject<{
405
+ key: import('zod').ZodString;
406
+ header: import('zod').ZodString;
407
+ minWidth: import('zod').ZodOptional<import('zod').ZodEnum<{
408
+ compact: "compact";
409
+ standard: "standard";
410
+ wide: "wide";
411
+ }>>;
412
+ }, import('zod/v4/core').$strip>>;
413
+ data: import('zod').ZodArray<import('zod').ZodIntersection<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodUnion<readonly [import('zod').ZodString, import('zod').ZodDiscriminatedUnion<[import('zod').ZodObject<{
414
+ type: import('zod').ZodLiteral<"text">;
415
+ value: import('zod').ZodString;
416
+ }, import('zod/v4/core').$strip>, import('zod').ZodObject<{
417
+ type: import('zod').ZodLiteral<"diff">;
418
+ from: import('zod').ZodOptional<import('zod').ZodNullable<import('zod').ZodString>>;
419
+ to: import('zod').ZodOptional<import('zod').ZodNullable<import('zod').ZodString>>;
420
+ }, import('zod/v4/core').$strip>, import('zod').ZodObject<{
421
+ type: import('zod').ZodLiteral<"addition">;
422
+ value: import('zod').ZodString;
423
+ }, import('zod/v4/core').$strip>], "type">, import('zod').ZodObject<{
424
+ from: import('zod').ZodOptional<import('zod').ZodNullable<import('zod').ZodString>>;
425
+ to: import('zod').ZodOptional<import('zod').ZodNullable<import('zod').ZodString>>;
426
+ }, import('zod/v4/core').$strip>]>>, import('zod').ZodObject<{
427
+ id: import('zod').ZodString;
428
+ }, import('zod/v4/core').$strip>>>;
429
+ reason: import('zod').ZodOptional<import('zod').ZodString>;
430
+ }, import('zod/v4/core').$strip>>;
431
+ }, import('zod/v4/core').$strip>], "type">>;
432
+ appliedChangeIds: import('zod').ZodOptional<import('zod').ZodArray<import('zod').ZodString>>;
433
+ defaultExpanded: import('zod').ZodOptional<import('zod').ZodBoolean>;
434
+ }, import('zod/v4/core').$strip>;
435
+ slots: never[];
436
+ events: string[];
437
+ description: string;
438
+ };
380
439
  };