@pebble-software/bonsai 0.0.4 → 0.0.5

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 (4) hide show
  1. package/README.md +45 -21
  2. package/dist/index.d.ts +219 -3
  3. package/dist/index.js +1877 -1378
  4. package/package.json +17 -14
package/README.md CHANGED
@@ -1,7 +1,15 @@
1
1
  # @pebble-software/bonsai
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/%40pebble-software%2Fbonsai)](https://www.npmjs.com/package/@pebble-software/bonsai)
4
+ [![status: preview](https://img.shields.io/badge/status-preview-orange)](https://github.com/PebbleSoftwareInc/pebble-bonsai)
5
+ [![react](https://img.shields.io/badge/react-18%20%7C%2019-61dafb)](https://react.dev)
6
+
7
+ > ⚠️ **Active development / preview** — this package is pre-1.0 and evolving quickly. APIs, the configuration schema, and the component set may change between releases without a deprecation cycle. Pin an exact version and read release notes before upgrading. Feedback and issues are very welcome!
8
+
3
9
  The React runtime that turns a validated Bonsai configuration into a fully interactive, styled experience. It's built for UI that has to be delivered remotely (email, SMS, embedded experiences) but rendered locally with real React components, consistent styling, and predictable data flow.
4
10
 
11
+ Bonsai is the rendering engine behind [Pebble Bonsai](https://github.com/PebbleSoftwareInc/pebble-bonsai), an open-source form and signature service — but the renderer is useful anywhere you need to ship UI as data: define a screen as a JSON template, bind it to your data with `{{ }}` expressions, and let one component render it on the server (SSR, PDF) or in the browser (fully interactive).
12
+
5
13
  ## Table of contents
6
14
 
7
15
  - [Features](#features)
@@ -20,26 +28,31 @@ The React runtime that turns a validated Bonsai configuration into a fully inter
20
28
 
21
29
  ## Features
22
30
 
23
- - **Schema-driven UI** — every screen is a Zod-validated `Config` tree from `@bonsai/schemas`, so the renderer only ever processes known-safe structures.
24
- - **Universal rendering** — one component (`Bonsai`) picks server (SSR) or client (interactive) rendering automatically based on environment.
31
+ - **Schema-driven UI** — every screen is a Zod-validated `Config` tree, so the renderer only ever processes known-safe structures.
32
+ - **Universal rendering** — one component (`Bonsai`) picks server (SSR) or client (interactive) rendering automatically based on environment, with `web`, `pdf`, and `preview` render modes.
25
33
  - **Composable components** — schema component identifiers map to real React implementations via a `componentMap`, so the same schema can target different component sets.
26
34
  - **Isolated per-instance state** — each mounted `Bonsai` tree owns its own `valtio` store, scoped via React context. Nothing about which data renders lives in shared or module-level state.
27
35
  - **Fine-grained reactivity** — every leaf node reads the store itself and evaluates only its own bindings, so editing one field re-renders just that field and whatever depends on it.
28
36
  - **Structural nodes** — `each` and `if` handle looping and conditional rendering directly in the schema, with two-way binding preserved inside loops.
29
37
  - **Sandboxed expression language** — `{{ }}` bindings are evaluated by a small, fixed-surface formula language, never executable JavaScript.
30
- - **Tailwind CSS v4** theme with a `bonsai:` prefix to avoid collisions with a host app's own styles.
38
+ - **Tailwind CSS v4 design system** — a themeable token set (colors, typography, spacing, shadows) drives every component, and templates can layer on any Tailwind utility via `className`.
31
39
 
32
40
  ## Installation
33
41
 
34
42
  ```bash
43
+ npm install @pebble-software/bonsai
44
+ # or
35
45
  pnpm add @pebble-software/bonsai
46
+ # or
47
+ yarn add @pebble-software/bonsai
36
48
  ```
37
49
 
38
- Peer expectations:
50
+ Peer dependencies:
39
51
 
40
- - React 18 or newer
52
+ - `react` and `react-dom` 18 or 19
41
53
  - A bundler that understands ES modules (Vite, Next, Rspack, etc.)
42
- - Tailwind CSS support if you intend to merge or override Bonsai styles
54
+
55
+ > **Preview note:** while the package is pre-1.0, pin an exact version (`npm install @pebble-software/bonsai --save-exact`) — minor releases may contain breaking changes.
43
56
 
44
57
  ### Working in this monorepo
45
58
 
@@ -48,7 +61,7 @@ pnpm install
48
61
  pnpm --filter @pebble-software/bonsai run build
49
62
  ```
50
63
 
51
- The build script compiles the shared workspaces (`@bonsai/constants`, `@bonsai/schemas`) before bundling this package with Vite.
64
+ The shared workspaces (`@bonsai/constants`, `@bonsai/schemas`) are aliased to their sources and bundled directly into `dist/` by Vite, so downstream consumers only ever install this one package.
52
65
 
53
66
  ## Quick start
54
67
 
@@ -57,7 +70,7 @@ import { Bonsai, type Config } from "@pebble-software/bonsai";
57
70
 
58
71
  const template: Config = {
59
72
  type: "card",
60
- args: { className: "bonsai:p-6 bonsai:flex bonsai:flex-col bonsai:gap-4" },
73
+ args: { className: "p-6 flex flex-col gap-4" },
61
74
  children: [
62
75
  {
63
76
  type: "label",
@@ -98,7 +111,7 @@ export function Example() {
98
111
  | `useUpdateData()` | hook | Returns an `updateData(path, value)` function bound to the nearest `Bonsai` instance's store. Must be called from a component mounted inside it. |
99
112
  | `componentMap` | object | Maps schema component identifiers to React implementations. |
100
113
  | `controlledInputMap` | object | Defines how specific component types behave as controlled inputs (value prop, change handler, value extractor). |
101
- | `Config` | type | Runtime type describing a Bonsai node, re-exported from `@bonsai/schemas`. |
114
+ | `Config` | type | Runtime type describing a Bonsai node. |
102
115
  | `SignatureProps` | type | Props contract for the built-in signature capture component. |
103
116
 
104
117
  `Bonsai` accepts:
@@ -114,11 +127,14 @@ interface BonsaiProps {
114
127
  }
115
128
  ```
116
129
 
130
+ - `render: "web"` (default) mounts the tree inside a `<form>` whose submit handler POSTs the captured data to `captureUrl`. `"preview"` renders the same form UI flagged with `data-mode="preview"` for host styling. `"pdf"` renders a plain `<div>` wrapper with no form — the layout used for print/PDF output.
131
+ - `status` other than `"pending"` disables capture: the submit handler no-ops instead of POSTing.
132
+
117
133
  Each mounted instance owns its own store — there's no shared module-level state and no global `updateData`; `useUpdateData()` is the only way to reach a given instance's data, and only from within its subtree.
118
134
 
119
135
  ## Configuration schema
120
136
 
121
- The schema is defined in `@bonsai/schemas/src/bonsai.schema.ts` and exported as `Config`.
137
+ The schema is defined in [`library/schemas/src/bonsai.schema.ts`](https://github.com/PebbleSoftwareInc/pebble-bonsai/blob/main/library/schemas/src/bonsai.schema.ts) and exported as `Config`.
122
138
 
123
139
  - **Node shape**: each node is either a string literal (rendered directly, and itself eligible for `{{ }}` bindings) or an object with:
124
140
  - `type`: a value from `COMPONENT` (see [Component reference](#component-reference)), including the structural `each` and `if` types (see [Structural nodes](#structural-nodes-each--if)).
@@ -135,18 +151,21 @@ Example node:
135
151
  "args": {
136
152
  "id": "terms",
137
153
  "checked": "{{agreements.terms}}",
138
- "className": "bonsai:mt-4"
154
+ "className": "mt-4"
139
155
  },
140
156
  "children": ["I agree to the terms"]
141
157
  }
142
158
  ```
143
159
 
160
+ Complete example templates live in the repo's [`examples/`](https://github.com/PebbleSoftwareInc/pebble-bonsai/tree/main/examples) directory, including a kitchen-sink template that exercises every component type.
161
+
144
162
  ## Dynamic data binding
145
163
 
146
164
  - Use `{{ }}` to declare bindings. A binding must span the *entire* string value — `"{{a}}"` is evaluated, `"{{a}} of {{b}}"` is not (put each placeholder in its own child/string instead). Whitespace inside `{{ }}` is trimmed automatically.
147
165
  - Paths support dot notation (`user.email`) and array indexing (`addresses[0].street`).
148
166
  - Missing keys resolve to `undefined`; use the `default(value, fallback)` helper for a fallback inline, or handle defaults in host data.
149
167
  - Any `args` prop or string child can contain a binding.
168
+ - Besides your own `data`, the instance's top-level state fields — `status`, `expiry`, and `submitting` — are readable from bindings too (e.g. an `if` on `{{submitting}}`).
150
169
  - Inside an `each` row, an identifier resolves against the loop alias first (e.g. `applicant.name`), then falls back to the surrounding data for anything the alias doesn't shadow (e.g. `orgName` from outside the loop) — see [Structural nodes](#structural-nodes-each--if).
151
170
  - On the client, evaluating a node's bindings is also what subscribes that node to the store — see [Controlled inputs & state](#controlled-inputs--state).
152
171
 
@@ -263,7 +282,7 @@ Allowlisted methods: `map`, `filter`, `some`, `every`, `find`. Anything else —
263
282
 
264
283
  ## Component reference
265
284
 
266
- The full, authoritative list of component types is the `COMPONENT` enum in `@bonsai/constants` (`library/constants/src/components.ts`) — `componentMap` (`src/lib/ui/registry.ts`) has an entry for every value in it.
285
+ The full, authoritative list of component types is the `COMPONENT` enum in `@bonsai/constants` ([`library/constants/src/components.ts`](https://github.com/PebbleSoftwareInc/pebble-bonsai/blob/main/library/constants/src/components.ts)) — `componentMap` (`src/lib/ui/registry.ts`) has an entry for every value in it.
267
286
 
268
287
  | Category | Schema types |
269
288
  | --- | --- |
@@ -280,10 +299,12 @@ Controlled inputs (registered in `controlledInputMap` — `input`, `otp`, `check
280
299
 
281
300
  ## Styling
282
301
 
283
- - Tailwind CSS v4 is bundled with a `bonsai:` prefix to avoid class collisions. Host applications can safely include their own Tailwind build.
284
- - `src/index.css` registers all theme tokens (colors, spacing, shadows, fonts) and safelists commonly generated utility classes.
285
- - Custom fonts (e.g. for signatures) are declared locally and loaded via `@font-face`.
286
- - To override styling, supply a `className` through the schema or wrap Bonsai components in host containers.
302
+ Bonsai is styled with **Tailwind CSS v4**. Components emit plain Tailwind utility classes, and templates can pass any additional utilities through `className` on any node.
303
+
304
+ - The design system lives in [`src/index.css`](https://github.com/PebbleSoftwareInc/pebble-bonsai/blob/main/apps/bonsai/src/index.css): an `@theme` token set (an OKLCH color palette with swappable accent colors, typography, shadows) plus base and utility layers, custom fonts (Sora, Fredoka, and Great Vibes for signatures) loaded via `@font-face`, and style overrides for the embedded map (MapLibre) and chart (Recharts) widgets.
305
+ - Every rendered tree is wrapped in a `.bonsai-root` element carrying a `data-mode` attribute (`web` / `pdf` / `preview`), so hosts can scope their own overrides per render mode.
306
+ - **Getting the stylesheet**: inside the Pebble Bonsai platform, CSS is compiled per-template at form-creation time — only the classes a template actually uses are emitted. When consuming this package standalone, compile the styles with your own Tailwind v4 build: import the Bonsai theme and add the package (and your template sources) as content via `@source` so the utilities your templates reference are generated. A prebuilt, ready-to-import stylesheet export is planned while the packaging story stabilizes during the preview period.
307
+ - **Theming**: the theme is token-driven — redefine the CSS variables (e.g. `--color-main`, `--font-heading`) after the theme to restyle every component at once, or target `.bonsai-root` descendants for finer overrides.
287
308
 
288
309
  ## Extending Bonsai
289
310
 
@@ -295,14 +316,17 @@ Controlled inputs (registered in `controlledInputMap` — `input`, `otp`, `check
295
316
  2. **Custom data handling**:
296
317
  - Use `useUpdateData()` from a component mounted inside a `Bonsai` instance's subtree for simple mutations.
297
318
  3. **Alternate styling**:
298
- - Tailwind tokens can be overridden by redefining CSS variables before mounting Bonsai.
319
+ - Redefine the theme's CSS variables before mounting Bonsai — see [Styling](#styling).
299
320
 
300
321
  ## Development
301
322
 
323
+ Issues and pull requests are welcome at [PebbleSoftwareInc/pebble-bonsai](https://github.com/PebbleSoftwareInc/pebble-bonsai). This package lives at `apps/bonsai` in the monorepo.
324
+
302
325
  ```bash
303
- pnpm lint # Static analysis using ESLint
304
- pnpm test # Run the test suite (vitest)
305
- pnpm build # Builds dependencies then bundles dist/ via Vite
326
+ pnpm lint # Static analysis using ESLint
327
+ pnpm test # Run the test suite (vitest)
328
+ pnpm type-check # TypeScript without emit
329
+ pnpm build # Bundles dist/ (ESM + type declarations) via Vite
306
330
  ```
307
331
 
308
- `dist/` contains the ESM bundle and TypeScript declarations consumed by downstream applications.
332
+ `dist/` contains the ESM bundle and TypeScript declarations consumed by downstream applications. The shared `@bonsai/constants` and `@bonsai/schemas` workspaces are bundled in, so the published package has no workspace dependencies.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,9 @@
1
+ import { ComponentType } from 'react';
1
2
  import { JSX } from 'react';
2
3
  import { z } from 'zod';
4
+ import { ZodType } from 'zod';
5
+
6
+ export declare function assertNoFailingDrift(issues: DriftIssue[]): void;
3
7
 
4
8
  export declare function Bonsai(props: BonsaiProps): JSX.Element;
5
9
 
@@ -10,8 +14,22 @@ declare interface BonsaiProps {
10
14
  data?: any;
11
15
  status?: "pending" | "captured" | "completed" | "error" | "revoked";
12
16
  render?: "web" | "pdf" | "preview";
17
+ registry?: BonsaiRegistry;
18
+ }
19
+
20
+ export declare interface BonsaiRegistry {
21
+ id: string;
22
+ version: string;
23
+ namespace?: string;
24
+ manifestHash?: string;
25
+ components: Record<string, ComponentDefinition>;
26
+ fallback?: RegistryFallback;
13
27
  }
14
28
 
29
+ export declare function checkArtifactDrift(input: DriftCheckInput): DriftIssue[];
30
+
31
+ export declare function clearManifestCache(): void;
32
+
15
33
  declare enum COMPONENT {
16
34
  A = "a",
17
35
  P = "p",
@@ -72,11 +90,34 @@ declare enum COMPONENT {
72
90
  IF = "if",
73
91
  }
74
92
 
93
+ export declare type ComponentCapability = "children" | "bindings" | "events" | "formField" | "ssr" | "pdf";
94
+
95
+ export declare interface ComponentDefinition {
96
+ component: ComponentType<any> | string;
97
+ propsSchema?: ZodType<any>;
98
+ capabilities?: ComponentCapability[];
99
+ controlled?: ControlledFieldSpec | Record<string, ControlledFieldSpec>;
100
+ assets?: {
101
+ css?: string[];
102
+ fonts?: string[];
103
+ };
104
+ }
105
+
75
106
  export declare const componentMap: Record<COMPONENT[number], any>;
76
107
 
77
- export declare const Config = Node;
108
+ export declare function composePropsSchema(baseSchema: z.ZodType<unknown> | undefined, tenantSchema: z.ZodType<unknown> | undefined): z.ZodType<unknown> | undefined;
109
+
110
+ export declare function computeManifestHash(raw: string): Promise<string>;
111
+
112
+ export declare const Config = pebbleTemplateSchema.Node;
113
+
114
+ export declare type Config = z.infer<typeof pebbleTemplateSchema.Node>;
78
115
 
79
- export declare type Config = z.infer<typeof Node_2>;
116
+ export declare interface ControlledFieldSpec {
117
+ valueProp: string;
118
+ changeProp: string;
119
+ extract: (eventOrValue: unknown) => unknown;
120
+ }
80
121
 
81
122
  declare interface ControlledInput {
82
123
  valueProp: string;
@@ -86,7 +127,175 @@ declare interface ControlledInput {
86
127
 
87
128
  export declare const controlledInputMap: Record<COMPONENT[number], Record<string, ControlledInput> | ControlledInput>;
88
129
 
89
- declare const Node_2: z.ZodType<any> = z.lazy(() => z.union([z.string(), ComponentNode]));
130
+ export declare function defineBonsaiRegistry(registry: BonsaiRegistry): BonsaiRegistry;
131
+
132
+ export declare function defineComponent(def: ComponentDefinition): ComponentDefinition;
133
+
134
+ export declare interface DriftCheckInput {
135
+ artifactRegistries: RegistrySource[];
136
+ configRegistries: RegistrySource[];
137
+ artifactBaseUiVersion: string;
138
+ installedBaseUiVersion: string;
139
+ }
140
+
141
+ export declare interface DriftIssue {
142
+ check: "stale-artifact" | "baseui-drift";
143
+ level: "warn" | "fail";
144
+ message: string;
145
+ }
146
+
147
+ export declare const EXTRACTOR_ALLOWLIST: Record<string, (eventOrValue: unknown) => unknown>;
148
+
149
+ /**
150
+ * Compiles the JSON Schema subset a manifest's `propsSchema` is allowed to use into a
151
+ * Zod validator (REGISTRY_SPIKE.md, "Registry manifests": "JSON Schema on the wire;
152
+ * compiled to a Zod validator at load"). Deliberately a subset, not a general JSON
153
+ * Schema implementation — enough to express prop narrowing (type, enum, required,
154
+ * string/number bounds), which is the only thing a Case A manifest is allowed to do
155
+ * to a prop schema in the first place.
156
+ */
157
+ export declare type JsonSchema = Record<string, unknown>;
158
+
159
+ export declare function jsonSchemaToZod(schema: JsonSchema): z.ZodType<unknown>;
160
+
161
+ export declare interface LoadManifestOptions extends ResolveManifestOptions {
162
+ allowedOrigins?: string[];
163
+ fetchImpl?: typeof fetch;
164
+ }
165
+
166
+ export declare function loadRegistryManifest(source: RegistryManifest | string, options: LoadManifestOptions): Promise<BonsaiRegistry>;
167
+
168
+ export declare type ManifestComponent = z.infer<typeof ManifestComponentSchema>;
169
+
170
+ declare const ManifestComponentSchema: z.ZodObject<{
171
+ name: z.ZodString;
172
+ canonicalName: z.ZodOptional<z.ZodString>;
173
+ entry: z.ZodString;
174
+ propsSchema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
175
+ capabilities: z.ZodOptional<z.ZodArray<z.ZodEnum<{
176
+ children: "children";
177
+ pdf: "pdf";
178
+ bindings: "bindings";
179
+ events: "events";
180
+ formField: "formField";
181
+ ssr: "ssr";
182
+ }>>>;
183
+ controlled: z.ZodOptional<z.ZodObject<{
184
+ valueProp: z.ZodString;
185
+ changeProp: z.ZodString;
186
+ extract: z.ZodString;
187
+ }, z.core.$strict>>;
188
+ css: z.ZodOptional<z.ZodArray<z.ZodString>>;
189
+ }, z.core.$strict>;
190
+
191
+ export declare class ManifestLoadError extends Error {
192
+ constructor(message: string);
193
+ }
194
+
195
+ export declare function mergeGeneratedWithOverrides(generated: ObjectPropsSchema | undefined, overrides: Partial<ObjectPropsSchema> | undefined): ObjectPropsSchema;
196
+
197
+ export declare interface ObjectPropsSchema {
198
+ type?: "object";
199
+ properties: Record<string, JsonSchema>;
200
+ required?: string[];
201
+ }
202
+
203
+ export declare const pebbleRegistry: BonsaiRegistry;
204
+
205
+ declare const pebbleTemplateSchema = createTemplateSchema(PEBBLE_COMPONENT_SCHEMAS);
206
+
207
+ export declare type PinResolution = {
208
+ action: "render";
209
+ reason: "no-pin";
210
+ } | {
211
+ action: "render";
212
+ reason: "match";
213
+ } | {
214
+ action: "render";
215
+ reason: "version-skew";
216
+ warning: string;
217
+ } | {
218
+ action: "fail";
219
+ reason: "id-mismatch";
220
+ error: string;
221
+ };
222
+
223
+ declare type RegistryFallback = "error" | "div" | ComponentType<any>;
224
+
225
+ export declare interface RegistryIdentity {
226
+ id: string;
227
+ version: string;
228
+ manifestHash?: string;
229
+ }
230
+
231
+ export declare type RegistryManifest = z.infer<typeof RegistryManifestSchema>;
232
+
233
+ export declare const RegistryManifestSchema: z.ZodObject<{
234
+ $schema: z.ZodOptional<z.ZodString>;
235
+ id: z.ZodString;
236
+ namespace: z.ZodOptional<z.ZodString>;
237
+ version: z.ZodString;
238
+ engine: z.ZodObject<{
239
+ name: z.ZodLiteral<"bonsai">;
240
+ api: z.ZodString;
241
+ }, z.core.$strict>;
242
+ components: z.ZodArray<z.ZodObject<{
243
+ name: z.ZodString;
244
+ canonicalName: z.ZodOptional<z.ZodString>;
245
+ entry: z.ZodString;
246
+ propsSchema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
247
+ capabilities: z.ZodOptional<z.ZodArray<z.ZodEnum<{
248
+ children: "children";
249
+ pdf: "pdf";
250
+ bindings: "bindings";
251
+ events: "events";
252
+ formField: "formField";
253
+ ssr: "ssr";
254
+ }>>>;
255
+ controlled: z.ZodOptional<z.ZodObject<{
256
+ valueProp: z.ZodString;
257
+ changeProp: z.ZodString;
258
+ extract: z.ZodString;
259
+ }, z.core.$strict>>;
260
+ css: z.ZodOptional<z.ZodArray<z.ZodString>>;
261
+ }, z.core.$strict>>;
262
+ integrity: z.ZodOptional<z.ZodObject<{
263
+ algorithm: z.ZodLiteral<"sha256">;
264
+ manifestHash: z.ZodString;
265
+ }, z.core.$strict>>;
266
+ }, z.core.$strict>;
267
+
268
+ export declare interface RegistrySource {
269
+ url: string;
270
+ registryHash: string;
271
+ }
272
+
273
+ export declare interface RegistryValidationError {
274
+ path: string;
275
+ message: string;
276
+ componentType?: string;
277
+ }
278
+
279
+ export declare interface RegistryValidationResult {
280
+ valid: boolean;
281
+ errors: RegistryValidationError[];
282
+ warnings: string[];
283
+ }
284
+
285
+ export declare function resolveManifest(manifest: RegistryManifest, options: ResolveManifestOptions): BonsaiRegistry;
286
+
287
+ export declare interface ResolveManifestOptions {
288
+ base: BonsaiRegistry;
289
+ engineApi?: string;
290
+ maxComponents?: number;
291
+ maxManifestBytes?: number;
292
+ rawManifestSize?: number;
293
+ manifestHash?: string;
294
+ }
295
+
296
+ export declare function resolveRegistryPin(pin: RegistryIdentity | undefined, registry: Pick<BonsaiRegistry, "id" | "version"> & {
297
+ manifestHash?: string;
298
+ }): PinResolution;
90
299
 
91
300
  export declare type SignatureProps = {
92
301
  value?: string | null;
@@ -97,4 +306,11 @@ export declare type SignatureProps = {
97
306
 
98
307
  export declare function useUpdateData(): (path: string, value: any) => void;
99
308
 
309
+ export declare function validateTemplate(template: Config, registry: BonsaiRegistry, options?: ValidateTemplateOptions): RegistryValidationResult;
310
+
311
+ export declare interface ValidateTemplateOptions {
312
+ mode?: "web" | "pdf";
313
+ pin?: RegistryIdentity;
314
+ }
315
+
100
316
  export { }