@tangle-network/blueprint-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +160 -0
  2. package/package.json +82 -0
  3. package/src/blueprints/registry.ts +107 -0
  4. package/src/components/forms/BlueprintJobForm.tsx +88 -0
  5. package/src/components/forms/FormField.tsx +206 -0
  6. package/src/components/forms/FormSummary.tsx +47 -0
  7. package/src/components/forms/JobExecutionDialog.tsx +203 -0
  8. package/src/components/layout/AppDocument.tsx +48 -0
  9. package/src/components/layout/AppFooter.tsx +38 -0
  10. package/src/components/layout/AppToaster.tsx +33 -0
  11. package/src/components/layout/ChainSwitcher.tsx +71 -0
  12. package/src/components/layout/ThemeToggle.tsx +18 -0
  13. package/src/components/layout/Web3Shell.tsx +28 -0
  14. package/src/components/motion/AnimatedPage.tsx +36 -0
  15. package/src/components/shared/Identicon.tsx +22 -0
  16. package/src/components/shared/TangleLogo.tsx +49 -0
  17. package/src/components/ui/badge.tsx +37 -0
  18. package/src/components/ui/button.tsx +61 -0
  19. package/src/components/ui/card.tsx +34 -0
  20. package/src/components/ui/dialog.tsx +59 -0
  21. package/src/components/ui/input.tsx +24 -0
  22. package/src/components/ui/select.tsx +80 -0
  23. package/src/components/ui/separator.tsx +25 -0
  24. package/src/components/ui/skeleton.tsx +13 -0
  25. package/src/components/ui/table.tsx +49 -0
  26. package/src/components/ui/tabs.tsx +39 -0
  27. package/src/components/ui/textarea.tsx +23 -0
  28. package/src/components/ui/toggle.tsx +35 -0
  29. package/src/components.ts +51 -0
  30. package/src/contracts/abi.ts +259 -0
  31. package/src/contracts/chains.ts +100 -0
  32. package/src/contracts/generic-encoder.ts +69 -0
  33. package/src/contracts/publicClient.ts +55 -0
  34. package/src/env.d.ts +14 -0
  35. package/src/hooks/useAuthenticatedFetch.ts +57 -0
  36. package/src/hooks/useJobForm.ts +78 -0
  37. package/src/hooks/useJobPrice.ts +283 -0
  38. package/src/hooks/useOperators.ts +141 -0
  39. package/src/hooks/useProvisionProgress.ts +125 -0
  40. package/src/hooks/useQuotes.ts +261 -0
  41. package/src/hooks/useServiceValidation.ts +113 -0
  42. package/src/hooks/useSessionAuth.ts +103 -0
  43. package/src/hooks/useSubmitJob.ts +115 -0
  44. package/src/hooks/useThemeValue.ts +6 -0
  45. package/src/index.ts +79 -0
  46. package/src/preset.ts +61 -0
  47. package/src/stores/infra.ts +43 -0
  48. package/src/stores/persistedAtom.ts +67 -0
  49. package/src/stores/session.ts +64 -0
  50. package/src/stores/theme.ts +28 -0
  51. package/src/stores/txHistory.ts +47 -0
  52. package/src/utils/resolveOperatorRpc.ts +20 -0
  53. package/src/utils/web3.ts +21 -0
  54. package/src/utils.ts +6 -0
  55. package/tsconfig.json +21 -0
package/README.md ADDED
@@ -0,0 +1,160 @@
1
+ ![Tangle Network Banner](https://raw.githubusercontent.com/tangle-network/tangle/refs/heads/main/assets/Tangle%20%20Banner.png)
2
+
3
+ <h1 align="center">@tangle-network/blueprint-ui</h1>
4
+
5
+ <p align="center"><em>Shared UI components, hooks, stores, and contract utilities for Tangle Blueprint applications.</em></p>
6
+
7
+ <p align="center">
8
+ <a href="https://discord.com/invite/cv8EfJu3Tn"><img src="https://img.shields.io/discord/833784453251596298?label=Discord" alt="Discord"></a>
9
+ <a href="https://t.me/tanglenet"><img src="https://img.shields.io/endpoint?color=neon&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Ftanglenet" alt="Telegram"></a>
10
+ </p>
11
+
12
+ ---
13
+
14
+ A TypeScript/React package that provides the building blocks for blueprint UIs on the Tangle Network. Designed to be consumed as a source dependency (no build step required) by apps using Vite or similar bundlers.
15
+
16
+ ## What's Included
17
+
18
+ ### Components
19
+
20
+ **UI Primitives** — Badge, Button, Card, Dialog, Input, Select, Separator, Skeleton, Table, Tabs, Textarea, Toggle
21
+
22
+ **Forms** — `FormField`, `BlueprintJobForm` (auto-generated from job definitions), `FormSummary`, `JobExecutionDialog`
23
+
24
+ **Layout / App Shell** — `AppDocument`, `AppFooter`, `AppToaster`, `Web3Shell`, `ChainSwitcher`, `ThemeToggle`
25
+
26
+ **Shared** — `Identicon` (blockie avatars), `TangleLogo`
27
+
28
+ **Motion** — `AnimatedPage`, `StaggerContainer`, `StaggerItem`
29
+
30
+ ### Hooks
31
+
32
+ | Hook | Purpose |
33
+ |------|---------|
34
+ | `useSubmitJob` | Submit on-chain jobs with TX lifecycle tracking |
35
+ | `useJobForm` | Generic form state derived from `JobDefinition` |
36
+ | `useJobPrice` / `useJobPrices` | Fetch job pricing from operators |
37
+ | `useQuotes` | Operator quote aggregation with PoW challenge solving |
38
+ | `useOperators` | Discover active operators for a blueprint |
39
+ | `useServiceValidation` | Check service status and permissions |
40
+ | `useProvisionProgress` | Track provision lifecycle (events + polling) |
41
+ | `useSessionAuth` | PASETO session management |
42
+ | `useAuthenticatedFetch` | Fetch wrapper with automatic token refresh |
43
+ | `useThemeValue` | Resolve theme-dependent values |
44
+
45
+ ### Stores (nanostores)
46
+
47
+ - **`infraStore`** — Blueprint ID, service ID, operator info
48
+ - **`sessionMapStore`** — PASETO sessions per operator
49
+ - **`txListStore`** — Transaction history tracking
50
+ - **`themeStore`** — Light/dark theme state
51
+ - **`persistedAtom`** — localStorage-backed atom with BigInt serialization
52
+
53
+ ### Contract Utilities
54
+
55
+ - **ABI exports** — `tangleJobsAbi`, `tangleServicesAbi`, `tangleOperatorsAbi`
56
+ - **Chain configs** — Tangle Local, Testnet, Mainnet with RPC resolution
57
+ - **`publicClient`** — Singleton viem public client tied to selected chain
58
+ - **`encodeJobArgs`** — ABI-encode job arguments from form values using job field metadata
59
+ - **Web3 helpers** — `createTangleTransports`, `defaultConnectKitOptions`, `tangleWalletChains`, `resolveOperatorRpc`
60
+
61
+ ## Package Boundaries
62
+
63
+ Use `@tangle-network/blueprint-ui` for:
64
+ - App-agnostic shell/layout primitives and design-system building blocks
65
+ - Shared chain/contract/provisioning hooks + stores
66
+ - Reusable cross-blueprint form and submission workflows
67
+
68
+ Keep in app-local code:
69
+ - Product-specific routes and copy
70
+ - Feature composition that is unique to a single app
71
+
72
+ ### Blueprint Registry
73
+
74
+ - **`registerBlueprint`** / **`getBlueprint`** — Register and look up blueprint definitions
75
+ - **`JobDefinition`** — Declarative job schema with field types, ABI metadata, and categories
76
+
77
+ ## Installation
78
+ ## Installation
79
+
80
+ ```bash
81
+ # As a git dependency (recommended for Tangle apps)
82
+ pnpm add github:tangle-network/blueprint-ui
83
+ ```
84
+
85
+ ## Publishing
86
+
87
+ Automated npm publishing is configured via GitHub Actions with npm Trusted Publishing (OIDC):
88
+ - Workflow: `.github/workflows/publish-npm.yml`
89
+ - Triggers:
90
+ - GitHub Release published (`vX.Y.Z`)
91
+ - Manual `workflow_dispatch`
92
+
93
+ No long-lived npm token is required once trusted publishing is configured.
94
+
95
+ Release flow:
96
+ 1. Bump `package.json` version.
97
+ 2. Create and publish a GitHub release tagged `v<same-version>`.
98
+ 3. Workflow typechecks and runs `npm publish --access public`.
99
+
100
+ Trusted publishing setup (one-time in npm):
101
+ 1. Open npm package settings for `@tangle-network/blueprint-ui`.
102
+ 2. Configure a trusted publisher:
103
+ - Provider: GitHub Actions
104
+ - Owner: `tangle-network`
105
+ - Repository: `blueprint-ui`
106
+ - Workflow file: `publish-npm.yml`
107
+ - Environment (if used): must match your workflow configuration
108
+
109
+ If npm does not allow configuring trusted publishing before first publish, do a one-time bootstrap publish with a short-lived token, then switch to trusted publishing and delete the token.
110
+
111
+ ## Usage
112
+
113
+ ```tsx
114
+ // Import hooks and utilities from the main entry
115
+ import { useSubmitJob, useJobForm, encodeJobArgs } from '@tangle-network/blueprint-ui';
116
+
117
+ // Import UI components from the /components entry
118
+ import { Button, Card, FormField } from '@tangle-network/blueprint-ui/components';
119
+ ```
120
+
121
+ The package uses source-level exports (`main` and `types` both point to `.ts` files). Your bundler must support TypeScript resolution — Vite works out of the box.
122
+
123
+ ## Peer Dependencies
124
+
125
+ React 19, wagmi 3.x, viem 2.x, nanostores, Radix UI primitives, framer-motion, sonner, tailwind-merge, class-variance-authority. See `package.json` for the full list and version ranges.
126
+
127
+ ## License
128
+
129
+ Licensed under either of [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) or [MIT license](http://opensource.org/licenses/MIT), at your option.
130
+
131
+ ---
132
+
133
+ ## Key Concepts
134
+
135
+ **Blueprint UI** is a TypeScript/React component library for building user interfaces that interact with Tangle Network blueprints. It provides pre-built components for operator management, service requests, job submission, and payment flows.
136
+
137
+ **Tangle Network** is a decentralized infrastructure protocol where operators stake economic collateral to run verifiable services called blueprints.
138
+
139
+ **x402** is an HTTP-native micropayment protocol that enables per-request payments for blueprint services, integrated into the UI through payment hooks and components.
140
+
141
+ **Blueprint** is a deployable service specification on Tangle that defines computation, verification, and payment in a single package.
142
+
143
+ ---
144
+
145
+ ## Frequently Asked Questions
146
+
147
+ **What is @tangle-network/blueprint-ui?**
148
+ A React component library providing UI primitives, hooks, and contract utilities for building applications on Tangle Network.
149
+
150
+ **Do I need to build this package before using it?**
151
+ No. It is designed as a source dependency consumed directly by Vite or similar bundlers with no build step required.
152
+
153
+ **What framework does blueprint-ui support?**
154
+ React with TypeScript. Components use Radix UI primitives and Tailwind CSS for styling.
155
+
156
+ **How do I connect to Tangle contracts?**
157
+ Use the provided contract hooks and ABI utilities. The package includes typed bindings for Tangle's on-chain service registry, operator staking, and job submission.
158
+
159
+ **Can I use blueprint-ui for x402 payment flows?**
160
+ Yes. The hooks and utilities support x402 payment header construction for per-request micropayments to blueprint operators.
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@tangle-network/blueprint-ui",
3
+ "version": "0.1.0",
4
+ "description": "Shared blueprint UI components, hooks, and contract utilities for Tangle Network apps",
5
+ "license": "MIT OR Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/tangle-network/blueprint-ui.git"
9
+ },
10
+ "homepage": "https://github.com/tangle-network/blueprint-ui",
11
+ "bugs": {
12
+ "url": "https://github.com/tangle-network/blueprint-ui/issues"
13
+ },
14
+ "type": "module",
15
+ "main": "./src/index.ts",
16
+ "types": "./src/index.ts",
17
+ "files": [
18
+ "src",
19
+ "README.md",
20
+ "package.json",
21
+ "tsconfig.json"
22
+ ],
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "exports": {
27
+ ".": "./src/index.ts",
28
+ "./components": "./src/components.ts",
29
+ "./preset": "./src/preset.ts"
30
+ },
31
+ "scripts": {
32
+ "typecheck": "tsc --noEmit"
33
+ },
34
+ "peerDependencies": {
35
+ "@tanstack/react-query": "^5.0.0",
36
+ "@nanostores/react": "^0.7.0",
37
+ "@radix-ui/react-dialog": "^1.1.0",
38
+ "@radix-ui/react-select": "^2.1.0",
39
+ "@radix-ui/react-separator": "^1.1.0",
40
+ "@radix-ui/react-slot": "^1.2.0",
41
+ "@radix-ui/react-tabs": "^1.1.0",
42
+ "@radix-ui/react-tooltip": "^1.2.0",
43
+ "blo": "^2.0.0",
44
+ "class-variance-authority": "^0.7.0",
45
+ "clsx": "^2.1.0",
46
+ "framer-motion": "^12.0.0",
47
+ "nanostores": "^0.10.0",
48
+ "react": "^19.0.0",
49
+ "react-dom": "^19.0.0",
50
+ "react-router": "^7.0.0",
51
+ "sonner": "^2.0.0",
52
+ "tailwind-merge": "^3.2.0",
53
+ "viem": "^2.31.0",
54
+ "wagmi": "^3.3.0"
55
+ },
56
+ "peerDependenciesMeta": {},
57
+ "devDependencies": {
58
+ "@tanstack/react-query": "^5.90.16",
59
+ "@nanostores/react": "^1.0.0",
60
+ "@radix-ui/react-dialog": "^1.1.15",
61
+ "@radix-ui/react-select": "^2.2.6",
62
+ "@radix-ui/react-separator": "^1.1.8",
63
+ "@radix-ui/react-slot": "^1.2.4",
64
+ "@radix-ui/react-tabs": "^1.1.13",
65
+ "@radix-ui/react-tooltip": "^1.2.8",
66
+ "@types/react": "18.3.1",
67
+ "@types/react-dom": "18.3.1",
68
+ "blo": "^2.0.0",
69
+ "class-variance-authority": "^0.7.1",
70
+ "clsx": "^2.1.1",
71
+ "framer-motion": "^12.34.3",
72
+ "nanostores": "^1.1.0",
73
+ "react": "^19.2.4",
74
+ "react-dom": "^19.2.4",
75
+ "react-router": "^7.13.0",
76
+ "sonner": "^2.0.7",
77
+ "tailwind-merge": "^3.5.0",
78
+ "typescript": "^5.5.2",
79
+ "viem": "^2.46.2",
80
+ "wagmi": "^3.5.0"
81
+ }
82
+ }
@@ -0,0 +1,107 @@
1
+ import type { Address } from 'viem';
2
+
3
+ /**
4
+ * Blueprint Registry — defines the metadata layer for Tangle blueprints.
5
+ *
6
+ * Each blueprint exposes a set of jobs. The registry maps on-chain job IDs
7
+ * to human-readable metadata: labels, descriptions, categories, form fields,
8
+ * and pricing info. This enables the UI to render appropriate forms for each
9
+ * job without procedurally generating UI from raw ABI data.
10
+ *
11
+ * Third-party blueprints can register here to appear in the wizard.
12
+ */
13
+
14
+ // ── Types ──
15
+
16
+ export type JobCategory = 'lifecycle' | 'execution' | 'batch' | 'workflow' | 'ssh' | 'management';
17
+
18
+ export interface JobFieldDef {
19
+ name: string;
20
+ label: string;
21
+ type: 'text' | 'textarea' | 'number' | 'boolean' | 'select' | 'json' | 'combobox';
22
+ placeholder?: string;
23
+ required?: boolean;
24
+ defaultValue?: string | number | boolean;
25
+ options?: { label: string; value: string }[];
26
+ helperText?: string;
27
+ /** Minimum allowed value for number fields */
28
+ min?: number;
29
+ /** Maximum allowed value for number fields */
30
+ max?: number;
31
+ /** Step increment for number fields */
32
+ step?: number;
33
+ /** Solidity ABI type for encoding (e.g. 'string', 'uint64', 'bool', 'uint8') */
34
+ abiType?: string;
35
+ /** ABI param name if different from `name` (e.g. 'agent_identifier' vs 'agentIdentifier') */
36
+ abiParam?: string;
37
+ /** Field is included in ABI encoding but never shown in form (e.g. sidecar_token) */
38
+ internal?: boolean;
39
+ }
40
+
41
+ /** ABI param injected from runtime context, not user input (e.g. sidecar_url, sandbox_id) */
42
+ export interface AbiContextParam {
43
+ abiName: string;
44
+ abiType: string;
45
+ }
46
+
47
+ export interface JobDefinition {
48
+ id: number;
49
+ name: string;
50
+ label: string;
51
+ description: string;
52
+ category: JobCategory;
53
+ icon: string;
54
+ pricingMultiplier: number;
55
+ /** Fields the user needs to fill for this job */
56
+ fields: JobFieldDef[];
57
+ /** Whether this job requires an existing sandbox to target */
58
+ requiresSandbox: boolean;
59
+ /** Optional warning shown before submission */
60
+ warning?: string;
61
+ /** ABI params prepended from runtime context (e.g. sidecar_url for sandbox jobs) */
62
+ contextParams?: AbiContextParam[];
63
+ /** Override for jobs with complex ABI encoding (e.g. nested structs) */
64
+ customEncoder?: (values: Record<string, unknown>, context?: Record<string, unknown>) => `0x${string}`;
65
+ }
66
+
67
+ export interface BlueprintDefinition {
68
+ id: string;
69
+ name: string;
70
+ version: string;
71
+ description: string;
72
+ icon: string;
73
+ color: string;
74
+ /** Contract address per chain ID — resolved at runtime */
75
+ contracts: Record<number, Address>;
76
+ /** Supported job definitions */
77
+ jobs: JobDefinition[];
78
+ /** Category ordering for the UI */
79
+ categories: { key: JobCategory; label: string; icon: string }[];
80
+ }
81
+
82
+ // ── Registry ──
83
+
84
+ const blueprintRegistry = new Map<string, BlueprintDefinition>();
85
+
86
+ export function registerBlueprint(bp: BlueprintDefinition) {
87
+ blueprintRegistry.set(bp.id, bp);
88
+ }
89
+
90
+ export function getBlueprint(id: string): BlueprintDefinition | undefined {
91
+ return blueprintRegistry.get(id);
92
+ }
93
+
94
+ export function getAllBlueprints(): BlueprintDefinition[] {
95
+ return Array.from(blueprintRegistry.values());
96
+ }
97
+
98
+ export function getBlueprintJobs(blueprintId: string, category?: JobCategory): JobDefinition[] {
99
+ const bp = blueprintRegistry.get(blueprintId);
100
+ if (!bp) return [];
101
+ return category ? bp.jobs.filter((j) => j.category === category) : bp.jobs;
102
+ }
103
+
104
+ export function getJobById(blueprintId: string, jobId: number): JobDefinition | undefined {
105
+ const bp = blueprintRegistry.get(blueprintId);
106
+ return bp?.jobs.find((j) => j.id === jobId);
107
+ }
@@ -0,0 +1,88 @@
1
+ import type { JobDefinition } from '../../blueprints/registry';
2
+ import { FormField } from './FormField';
3
+
4
+ export interface FormSection {
5
+ label: string;
6
+ fields: string[];
7
+ collapsed?: boolean;
8
+ }
9
+
10
+ interface BlueprintJobFormProps {
11
+ job: JobDefinition;
12
+ values: Record<string, unknown>;
13
+ onChange: (name: string, value: unknown) => void;
14
+ errors?: Record<string, string>;
15
+ sections?: FormSection[];
16
+ }
17
+
18
+ export function BlueprintJobForm({ job, values, onChange, errors, sections }: BlueprintJobFormProps) {
19
+ const visibleFields = job.fields.filter((f) => !f.internal);
20
+
21
+ if (sections) {
22
+ return (
23
+ <div className="space-y-6">
24
+ {sections.map((section) => {
25
+ const sectionFields = section.fields
26
+ .map((name) => visibleFields.find((f) => f.name === name))
27
+ .filter(Boolean);
28
+
29
+ if (sectionFields.length === 0) return null;
30
+
31
+ if (section.collapsed) {
32
+ return (
33
+ <details key={section.label} className="group">
34
+ <summary className="cursor-pointer text-sm font-display font-medium text-bp-elements-textTertiary hover:text-bp-elements-textSecondary transition-colors">
35
+ {section.label}
36
+ </summary>
37
+ <div className="mt-4 space-y-4">
38
+ {sectionFields.map((field) => (
39
+ <FormField
40
+ key={field!.name}
41
+ field={field!}
42
+ value={values[field!.name]}
43
+ onChange={onChange}
44
+ error={errors?.[field!.name]}
45
+ />
46
+ ))}
47
+ </div>
48
+ </details>
49
+ );
50
+ }
51
+
52
+ return (
53
+ <div key={section.label}>
54
+ <label className="block text-sm font-display font-medium text-bp-elements-textSecondary mb-3">
55
+ {section.label}
56
+ </label>
57
+ <div className="space-y-4">
58
+ {sectionFields.map((field) => (
59
+ <FormField
60
+ key={field!.name}
61
+ field={field!}
62
+ value={values[field!.name]}
63
+ onChange={onChange}
64
+ error={errors?.[field!.name]}
65
+ />
66
+ ))}
67
+ </div>
68
+ </div>
69
+ );
70
+ })}
71
+ </div>
72
+ );
73
+ }
74
+
75
+ return (
76
+ <div className="space-y-4">
77
+ {visibleFields.map((field) => (
78
+ <FormField
79
+ key={field.name}
80
+ field={field}
81
+ value={values[field.name]}
82
+ onChange={onChange}
83
+ error={errors?.[field.name]}
84
+ />
85
+ ))}
86
+ </div>
87
+ );
88
+ }
@@ -0,0 +1,206 @@
1
+ import { useState } from 'react';
2
+ import type { JobFieldDef } from '../../blueprints/registry';
3
+ import { Input } from '../ui/input';
4
+ import { Select } from '../ui/select';
5
+ import { Textarea } from '../ui/textarea';
6
+ import { Toggle } from '../ui/toggle';
7
+ import { cn } from '../../utils';
8
+
9
+ interface FormFieldProps {
10
+ field: JobFieldDef;
11
+ value: unknown;
12
+ onChange: (name: string, value: unknown) => void;
13
+ error?: string;
14
+ }
15
+
16
+ export function FormField({ field, value, onChange, error }: FormFieldProps) {
17
+ if (field.internal) return null;
18
+
19
+ const isBool = field.type === 'boolean';
20
+
21
+ return (
22
+ <div className={cn(isBool && 'flex items-center gap-3')}>
23
+ {!isBool && (
24
+ <label className="block text-sm font-display font-medium text-bp-elements-textSecondary mb-2">
25
+ {field.label}
26
+ {field.required && ' *'}
27
+ </label>
28
+ )}
29
+ <FieldInput field={field} value={value} onChange={onChange} />
30
+ {isBool && (
31
+ <span className="text-sm font-display text-bp-elements-textSecondary">{field.label}</span>
32
+ )}
33
+ {field.helperText && !error && (
34
+ <p className="text-xs text-bp-elements-textTertiary mt-1">{field.helperText}</p>
35
+ )}
36
+ {error && <p className="text-xs text-crimson-400 mt-1">{error}</p>}
37
+ </div>
38
+ );
39
+ }
40
+
41
+ function FieldInput({
42
+ field,
43
+ value,
44
+ onChange,
45
+ }: {
46
+ field: JobFieldDef;
47
+ value: unknown;
48
+ onChange: (name: string, value: unknown) => void;
49
+ }) {
50
+ switch (field.type) {
51
+ case 'text':
52
+ return (
53
+ <Input
54
+ value={String(value ?? '')}
55
+ onChange={(e) => onChange(field.name, e.target.value)}
56
+ placeholder={field.placeholder}
57
+ />
58
+ );
59
+ case 'number':
60
+ return <NumberInput field={field} value={value} onChange={onChange} />;
61
+ case 'textarea':
62
+ case 'json':
63
+ return (
64
+ <Textarea
65
+ value={String(value ?? '')}
66
+ onChange={(e) => onChange(field.name, e.target.value)}
67
+ placeholder={field.placeholder}
68
+ rows={field.type === 'json' ? 3 : 4}
69
+ className={field.type === 'json' ? 'font-data text-sm' : undefined}
70
+ />
71
+ );
72
+ case 'boolean':
73
+ return <Toggle checked={Boolean(value)} onChange={(v) => onChange(field.name, v)} />;
74
+ case 'select':
75
+ return (
76
+ <Select
77
+ value={String(value ?? '')}
78
+ onValueChange={(v) => onChange(field.name, v)}
79
+ options={field.options ?? []}
80
+ />
81
+ );
82
+ case 'combobox':
83
+ return <ComboboxInput field={field} value={value} onChange={onChange} />;
84
+ default:
85
+ return null;
86
+ }
87
+ }
88
+
89
+ /** Number input with inline stepper buttons */
90
+ function NumberInput({
91
+ field,
92
+ value,
93
+ onChange,
94
+ }: {
95
+ field: JobFieldDef;
96
+ value: unknown;
97
+ onChange: (name: string, value: unknown) => void;
98
+ }) {
99
+ const numVal = Number(value ?? field.min ?? 0);
100
+ const step = field.step ?? 1;
101
+
102
+ const clamp = (raw: number) => {
103
+ if (field.min != null && raw < field.min) return field.min;
104
+ if (field.max != null && raw > field.max) return field.max;
105
+ return raw;
106
+ };
107
+
108
+ const canDecrement = field.min == null || numVal > field.min;
109
+ const canIncrement = field.max == null || numVal < field.max;
110
+
111
+ return (
112
+ <div className="flex items-stretch h-11 rounded-lg border border-bp-elements-borderColor bg-bp-elements-background-depth-3 dark:bg-bp-elements-background-depth-4 transition-all duration-200 hover:border-bp-elements-borderColorActive/40 focus-within:border-violet-500/40 focus-within:ring-2 focus-within:ring-violet-500/10">
113
+ <button
114
+ type="button"
115
+ tabIndex={-1}
116
+ disabled={!canDecrement}
117
+ onClick={() => onChange(field.name, clamp(numVal - step))}
118
+ className="flex items-center justify-center w-10 shrink-0 text-bp-elements-textTertiary transition-colors hover:text-bp-elements-textPrimary hover:bg-white/[0.04] rounded-l-lg disabled:opacity-30 disabled:pointer-events-none"
119
+ >
120
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M3 7h8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /></svg>
121
+ </button>
122
+ <input
123
+ type="number"
124
+ value={numVal}
125
+ min={field.min}
126
+ max={field.max}
127
+ step={step}
128
+ onChange={(e) => {
129
+ if (e.target.value === '') {
130
+ onChange(field.name, field.min ?? 0);
131
+ return;
132
+ }
133
+ onChange(field.name, clamp(Number(e.target.value)));
134
+ }}
135
+ placeholder={field.placeholder}
136
+ className="flex-1 min-w-0 bg-transparent text-center text-base font-data text-bp-elements-textPrimary outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
137
+ />
138
+ <button
139
+ type="button"
140
+ tabIndex={-1}
141
+ disabled={!canIncrement}
142
+ onClick={() => onChange(field.name, clamp(numVal + step))}
143
+ className="flex items-center justify-center w-10 shrink-0 text-bp-elements-textTertiary transition-colors hover:text-bp-elements-textPrimary hover:bg-white/[0.04] rounded-r-lg disabled:opacity-30 disabled:pointer-events-none"
144
+ >
145
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M7 3v8M3 7h8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /></svg>
146
+ </button>
147
+ </div>
148
+ );
149
+ }
150
+
151
+ /** Dropdown with preset options + free-text custom input */
152
+ function ComboboxInput({
153
+ field,
154
+ value,
155
+ onChange,
156
+ }: {
157
+ field: JobFieldDef;
158
+ value: unknown;
159
+ onChange: (name: string, value: unknown) => void;
160
+ }) {
161
+ const strVal = String(value ?? '');
162
+ const options = field.options ?? [];
163
+ const isPreset = options.some((o) => o.value === strVal);
164
+ const [isCustom, setIsCustom] = useState(!isPreset && strVal !== '');
165
+
166
+ if (isCustom) {
167
+ return (
168
+ <div className="flex gap-2">
169
+ <Input
170
+ value={strVal}
171
+ onChange={(e) => onChange(field.name, e.target.value)}
172
+ placeholder={field.placeholder}
173
+ className="flex-1"
174
+ />
175
+ <button
176
+ type="button"
177
+ onClick={() => {
178
+ setIsCustom(false);
179
+ onChange(field.name, options[0]?.value ?? '');
180
+ }}
181
+ className="shrink-0 rounded-lg border border-bp-elements-borderColor bg-bp-elements-background-depth-3 px-3 py-2 text-xs font-display text-bp-elements-textTertiary transition-colors hover:border-bp-elements-borderColorActive/40 hover:text-bp-elements-textSecondary"
182
+ >
183
+ Presets
184
+ </button>
185
+ </div>
186
+ );
187
+ }
188
+
189
+ return (
190
+ <div className="flex gap-2 w-full">
191
+ <Select
192
+ value={strVal}
193
+ onValueChange={(v) => {
194
+ if (v === '__custom__') {
195
+ setIsCustom(true);
196
+ onChange(field.name, '');
197
+ } else {
198
+ onChange(field.name, v);
199
+ }
200
+ }}
201
+ options={[...options, { label: 'Custom...', value: '__custom__' }]}
202
+ className="flex-1"
203
+ />
204
+ </div>
205
+ );
206
+ }
@@ -0,0 +1,47 @@
1
+ import type { JobDefinition } from '../../blueprints/registry';
2
+ import { cn } from '../../utils';
3
+
4
+ interface FormSummaryProps {
5
+ job: JobDefinition;
6
+ values: Record<string, unknown>;
7
+ context?: Record<string, unknown>;
8
+ }
9
+
10
+ export function FormSummary({ job, values, context }: FormSummaryProps) {
11
+ return (
12
+ <div className="glass-card rounded-lg p-4 space-y-2.5">
13
+ {context &&
14
+ Object.entries(context).map(([key, val]) => (
15
+ <SummaryRow key={key} label={key} value={String(val)} mono />
16
+ ))}
17
+ {context && job.fields.length > 0 && (
18
+ <div className="border-t border-bp-elements-dividerColor my-2" />
19
+ )}
20
+ {job.fields
21
+ .filter((f) => !f.internal)
22
+ .map((field) => {
23
+ const v = values[field.name];
24
+ let display: string;
25
+ if (field.type === 'boolean') {
26
+ display = v ? 'Enabled' : 'Disabled';
27
+ } else if (field.type === 'select' && field.options) {
28
+ display = field.options.find((o) => o.value === String(v))?.label ?? String(v ?? '');
29
+ } else {
30
+ display = String(v ?? '--');
31
+ }
32
+ return <SummaryRow key={field.name} label={field.label} value={display} mono={field.type === 'json'} />;
33
+ })}
34
+ </div>
35
+ );
36
+ }
37
+
38
+ function SummaryRow({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
39
+ return (
40
+ <div className="flex justify-between text-sm">
41
+ <span className="text-bp-elements-textSecondary">{label}</span>
42
+ <span className={cn('text-bp-elements-textPrimary', mono ? 'font-data text-xs' : 'font-display')}>
43
+ {value || '--'}
44
+ </span>
45
+ </div>
46
+ );
47
+ }