@xemahq/ui-kernel 0.9.0 → 0.9.1
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/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/biome-host/biome-registry.d.ts.map +1 -1
- package/dist/lib/biome-host/biome-registry.js +14 -9
- package/dist/lib/biome-host/biome-registry.js.map +1 -1
- package/dist/lib/contribution-registry/contribution-registry.d.ts +24 -0
- package/dist/lib/contribution-registry/contribution-registry.d.ts.map +1 -0
- package/dist/lib/contribution-registry/contribution-registry.js +67 -0
- package/dist/lib/contribution-registry/contribution-registry.js.map +1 -0
- package/dist/lib/contribution-registry/index.d.ts +3 -0
- package/dist/lib/contribution-registry/index.d.ts.map +1 -0
- package/dist/lib/contribution-registry/index.js +3 -0
- package/dist/lib/contribution-registry/index.js.map +1 -0
- package/dist/lib/contribution-registry/renderable-registry.d.ts +33 -0
- package/dist/lib/contribution-registry/renderable-registry.d.ts.map +1 -0
- package/dist/lib/contribution-registry/renderable-registry.js +51 -0
- package/dist/lib/contribution-registry/renderable-registry.js.map +1 -0
- package/dist/ui/capture/field-spec-to-zod.d.ts +1 -0
- package/dist/ui/capture/field-spec-to-zod.d.ts.map +1 -1
- package/dist/ui/capture/field-spec-to-zod.js +19 -0
- package/dist/ui/capture/field-spec-to-zod.js.map +1 -1
- package/dist/ui/capture/field-spec-validate.d.ts +4 -0
- package/dist/ui/capture/field-spec-validate.d.ts.map +1 -0
- package/dist/ui/capture/field-spec-validate.js +129 -0
- package/dist/ui/capture/field-spec-validate.js.map +1 -0
- package/dist/ui/capture/field-spec.d.ts +13 -1
- package/dist/ui/capture/field-spec.d.ts.map +1 -1
- package/dist/ui/capture/field-spec.js +25 -0
- package/dist/ui/capture/field-spec.js.map +1 -1
- package/dist/ui/capture/index.d.ts +4 -2
- package/dist/ui/capture/index.d.ts.map +1 -1
- package/dist/ui/capture/index.js +4 -2
- package/dist/ui/capture/index.js.map +1 -1
- package/dist/ui/capture/schema-field-input.d.ts.map +1 -1
- package/dist/ui/capture/schema-field-input.js +39 -12
- package/dist/ui/capture/schema-field-input.js.map +1 -1
- package/dist/ui/capture/submit-values.d.ts +6 -0
- package/dist/ui/capture/submit-values.d.ts.map +1 -0
- package/dist/ui/capture/submit-values.js +28 -0
- package/dist/ui/capture/submit-values.js.map +1 -0
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +1 -0
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/primitives/overflow-tabs.d.ts +5 -0
- package/dist/ui/primitives/overflow-tabs.d.ts.map +1 -1
- package/dist/ui/primitives/overflow-tabs.js +4 -4
- package/dist/ui/primitives/overflow-tabs.js.map +1 -1
- package/dist/ui/primitives/url-preview-chrome.d.ts +8 -0
- package/dist/ui/primitives/url-preview-chrome.d.ts.map +1 -0
- package/dist/ui/primitives/url-preview-chrome.js +40 -0
- package/dist/ui/primitives/url-preview-chrome.js.map +1 -0
- package/dist/vista/GenericCatalogueSurface.d.ts +10 -0
- package/dist/vista/GenericCatalogueSurface.d.ts.map +1 -0
- package/dist/vista/GenericCatalogueSurface.js +80 -0
- package/dist/vista/GenericCatalogueSurface.js.map +1 -0
- package/dist/vista/SurfaceFallbackCard.d.ts +9 -0
- package/dist/vista/SurfaceFallbackCard.d.ts.map +1 -0
- package/dist/vista/SurfaceFallbackCard.js +44 -0
- package/dist/vista/SurfaceFallbackCard.js.map +1 -0
- package/dist/vista/VistaPanel.d.ts +21 -0
- package/dist/vista/VistaPanel.d.ts.map +1 -0
- package/dist/vista/VistaPanel.js +31 -0
- package/dist/vista/VistaPanel.js.map +1 -0
- package/dist/vista/html/CustomUiFrame.d.ts +20 -0
- package/dist/vista/html/CustomUiFrame.d.ts.map +1 -0
- package/dist/vista/html/CustomUiFrame.js +69 -0
- package/dist/vista/html/CustomUiFrame.js.map +1 -0
- package/dist/vista/html/custom-ui-frame-bridge.d.ts +13 -0
- package/dist/vista/html/custom-ui-frame-bridge.d.ts.map +1 -0
- package/dist/vista/html/custom-ui-frame-bridge.js +30 -0
- package/dist/vista/html/custom-ui-frame-bridge.js.map +1 -0
- package/dist/vista/html/custom-ui-frame-contract.d.ts +29 -0
- package/dist/vista/html/custom-ui-frame-contract.d.ts.map +1 -0
- package/dist/vista/html/custom-ui-frame-contract.js +16 -0
- package/dist/vista/html/custom-ui-frame-contract.js.map +1 -0
- package/dist/vista/html/custom-ui-intents.d.ts +20 -0
- package/dist/vista/html/custom-ui-intents.d.ts.map +1 -0
- package/dist/vista/html/custom-ui-intents.js +57 -0
- package/dist/vista/html/custom-ui-intents.js.map +1 -0
- package/dist/vista/html/custom-ui-srcdoc.d.ts +17 -0
- package/dist/vista/html/custom-ui-srcdoc.d.ts.map +1 -0
- package/dist/vista/html/custom-ui-srcdoc.js +70 -0
- package/dist/vista/html/custom-ui-srcdoc.js.map +1 -0
- package/dist/vista/html/index.d.ts +6 -0
- package/dist/vista/html/index.d.ts.map +1 -0
- package/dist/vista/html/index.js +6 -0
- package/dist/vista/html/index.js.map +1 -0
- package/dist/vista/index.d.ts +18 -0
- package/dist/vista/index.d.ts.map +1 -0
- package/dist/vista/index.js +18 -0
- package/dist/vista/index.js.map +1 -0
- package/dist/vista/surface-catalogue.d.ts +5 -0
- package/dist/vista/surface-catalogue.d.ts.map +1 -0
- package/dist/vista/surface-catalogue.js +30 -0
- package/dist/vista/surface-catalogue.js.map +1 -0
- package/dist/vista/surface-icons.d.ts +7 -0
- package/dist/vista/surface-icons.d.ts.map +1 -0
- package/dist/vista/surface-icons.js +26 -0
- package/dist/vista/surface-icons.js.map +1 -0
- package/dist/vista/surface-registry.d.ts +7 -0
- package/dist/vista/surface-registry.d.ts.map +1 -0
- package/dist/vista/surface-registry.js +7 -0
- package/dist/vista/surface-registry.js.map +1 -0
- package/dist/vista/surfaces/ChartSurface.d.ts +4 -0
- package/dist/vista/surfaces/ChartSurface.d.ts.map +1 -0
- package/dist/vista/surfaces/ChartSurface.js +17 -0
- package/dist/vista/surfaces/ChartSurface.js.map +1 -0
- package/dist/vista/surfaces/FilesSurface.d.ts +15 -0
- package/dist/vista/surfaces/FilesSurface.d.ts.map +1 -0
- package/dist/vista/surfaces/FilesSurface.js +121 -0
- package/dist/vista/surfaces/FilesSurface.js.map +1 -0
- package/dist/vista/surfaces/FormSurface.d.ts +4 -0
- package/dist/vista/surfaces/FormSurface.d.ts.map +1 -0
- package/dist/vista/surfaces/FormSurface.js +97 -0
- package/dist/vista/surfaces/FormSurface.js.map +1 -0
- package/dist/vista/surfaces/HtmlSurface.d.ts +4 -0
- package/dist/vista/surfaces/HtmlSurface.d.ts.map +1 -0
- package/dist/vista/surfaces/HtmlSurface.js +11 -0
- package/dist/vista/surfaces/HtmlSurface.js.map +1 -0
- package/dist/vista/surfaces/TableSurface.d.ts +4 -0
- package/dist/vista/surfaces/TableSurface.d.ts.map +1 -0
- package/dist/vista/surfaces/TableSurface.js +42 -0
- package/dist/vista/surfaces/TableSurface.js.map +1 -0
- package/dist/vista/surfaces/UrlSurface.d.ts +4 -0
- package/dist/vista/surfaces/UrlSurface.d.ts.map +1 -0
- package/dist/vista/surfaces/UrlSurface.js +7 -0
- package/dist/vista/surfaces/UrlSurface.js.map +1 -0
- package/dist/vista/surfaces/blob-surfaces.d.ts +6 -0
- package/dist/vista/surfaces/blob-surfaces.d.ts.map +1 -0
- package/dist/vista/surfaces/blob-surfaces.js +38 -0
- package/dist/vista/surfaces/blob-surfaces.js.map +1 -0
- package/dist/vista/surfaces/elicitation-transport.d.ts +18 -0
- package/dist/vista/surfaces/elicitation-transport.d.ts.map +1 -0
- package/dist/vista/surfaces/elicitation-transport.js +100 -0
- package/dist/vista/surfaces/elicitation-transport.js.map +1 -0
- package/dist/vista/surfaces/index.d.ts +11 -0
- package/dist/vista/surfaces/index.d.ts.map +1 -0
- package/dist/vista/surfaces/index.js +89 -0
- package/dist/vista/surfaces/index.js.map +1 -0
- package/dist/vista/surfaces/text-surfaces.d.ts +8 -0
- package/dist/vista/surfaces/text-surfaces.d.ts.map +1 -0
- package/dist/vista/surfaces/text-surfaces.js +54 -0
- package/dist/vista/surfaces/text-surfaces.js.map +1 -0
- package/dist/vista/use-vista-tabs.d.ts +30 -0
- package/dist/vista/use-vista-tabs.d.ts.map +1 -0
- package/dist/vista/use-vista-tabs.js +77 -0
- package/dist/vista/use-vista-tabs.js.map +1 -0
- package/dist/vista/vista-code-renderer.d.ts +12 -0
- package/dist/vista/vista-code-renderer.d.ts.map +1 -0
- package/dist/vista/vista-code-renderer.js +10 -0
- package/dist/vista/vista-code-renderer.js.map +1 -0
- package/dist/vista/vista-file-source.d.ts +20 -0
- package/dist/vista/vista-file-source.d.ts.map +1 -0
- package/dist/vista/vista-file-source.js +69 -0
- package/dist/vista/vista-file-source.js.map +1 -0
- package/dist/vista/vista-form-host.d.ts +16 -0
- package/dist/vista/vista-form-host.d.ts.map +1 -0
- package/dist/vista/vista-form-host.js +10 -0
- package/dist/vista/vista-form-host.js.map +1 -0
- package/dist/vista/vista-model.d.ts +12 -0
- package/dist/vista/vista-model.d.ts.map +1 -0
- package/dist/vista/vista-model.js +72 -0
- package/dist/vista/vista-model.js.map +1 -0
- package/dist/vista/vista-open-context.d.ts +9 -0
- package/dist/vista/vista-open-context.d.ts.map +1 -0
- package/dist/vista/vista-open-context.js +10 -0
- package/dist/vista/vista-open-context.js.map +1 -0
- package/dist/vista/vista-payloads.d.ts +107 -0
- package/dist/vista/vista-payloads.d.ts.map +1 -0
- package/dist/vista/vista-payloads.js +305 -0
- package/dist/vista/vista-payloads.js.map +1 -0
- package/dist/vista/vista-tabs-manifest.d.ts +17 -0
- package/dist/vista/vista-tabs-manifest.d.ts.map +1 -0
- package/dist/vista/vista-tabs-manifest.js +60 -0
- package/dist/vista/vista-tabs-manifest.js.map +1 -0
- package/dist/vista/vista-tools.d.ts +41 -0
- package/dist/vista/vista-tools.d.ts.map +1 -0
- package/dist/vista/vista-tools.js +113 -0
- package/dist/vista/vista-tools.js.map +1 -0
- package/package.json +3 -3
- package/src/index.ts +5 -0
- package/src/lib/biome-host/biome-registry.ts +31 -11
- package/src/lib/contribution-registry/contribution-registry.spec.ts +141 -0
- package/src/lib/contribution-registry/contribution-registry.ts +202 -0
- package/src/lib/contribution-registry/index.ts +9 -0
- package/src/lib/contribution-registry/renderable-registry.ts +142 -0
- package/src/ui/capture/field-spec-to-zod.ts +22 -0
- package/src/ui/capture/field-spec-validate.ts +147 -0
- package/src/ui/capture/field-spec.ts +62 -0
- package/src/ui/capture/index.ts +9 -1
- package/src/ui/capture/schema-field-input.tsx +134 -14
- package/src/ui/capture/submit-values.test.ts +70 -0
- package/src/ui/capture/submit-values.ts +63 -0
- package/src/ui/index.ts +4 -0
- package/src/ui/primitives/overflow-tabs.tsx +24 -4
- package/src/ui/primitives/url-preview-chrome.tsx +102 -0
- package/src/vista/GenericCatalogueSurface.tsx +177 -0
- package/src/vista/SurfaceFallbackCard.tsx +109 -0
- package/src/vista/VistaPanel.tsx +155 -0
- package/src/vista/form-payload.spec.ts +93 -0
- package/src/vista/html/CustomUiFrame.spec.tsx +125 -0
- package/src/vista/html/CustomUiFrame.tsx +204 -0
- package/src/vista/html/custom-ui-frame-bridge.spec.ts +191 -0
- package/src/vista/html/custom-ui-frame-bridge.ts +92 -0
- package/src/vista/html/custom-ui-frame-contract.ts +88 -0
- package/src/vista/html/custom-ui-intents.spec.ts +133 -0
- package/src/vista/html/custom-ui-intents.ts +151 -0
- package/src/vista/html/custom-ui-srcdoc.spec.ts +151 -0
- package/src/vista/html/custom-ui-srcdoc.ts +193 -0
- package/src/vista/html/index.ts +13 -0
- package/src/vista/index.ts +48 -0
- package/src/vista/surface-catalogue.spec.ts +140 -0
- package/src/vista/surface-catalogue.ts +84 -0
- package/src/vista/surface-icons.ts +57 -0
- package/src/vista/surface-registry.ts +56 -0
- package/src/vista/surfaces/ChartSurface.tsx +34 -0
- package/src/vista/surfaces/FilesSurface.tsx +268 -0
- package/src/vista/surfaces/FormSurface.tsx +264 -0
- package/src/vista/surfaces/HtmlSurface.tsx +43 -0
- package/src/vista/surfaces/TableSurface.tsx +99 -0
- package/src/vista/surfaces/UrlSurface.tsx +40 -0
- package/src/vista/surfaces/blob-surfaces.tsx +129 -0
- package/src/vista/surfaces/elicitation-transport.spec.ts +187 -0
- package/src/vista/surfaces/elicitation-transport.ts +175 -0
- package/src/vista/surfaces/index.tsx +157 -0
- package/src/vista/surfaces/text-surfaces.tsx +119 -0
- package/src/vista/use-vista-tabs.ts +188 -0
- package/src/vista/vista-code-renderer.tsx +46 -0
- package/src/vista/vista-file-source.tsx +163 -0
- package/src/vista/vista-form-host.tsx +78 -0
- package/src/vista/vista-model.ts +130 -0
- package/src/vista/vista-open-context.tsx +42 -0
- package/src/vista/vista-payloads.ts +514 -0
- package/src/vista/vista-tabs-manifest.ts +123 -0
- package/src/vista/vista-tools.ts +220 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The RENDERABLE flavour of {@link createContributionRegistry}: a two-layer
|
|
3
|
+
* closed+open registry whose entries are versioned renderers with a narrowing
|
|
4
|
+
* payload validator, plus the fail-safe resolution every such plane needs.
|
|
5
|
+
*
|
|
6
|
+
* This is the shape the in-chat ChatWidget plane and the Vista render plane
|
|
7
|
+
* both want: an envelope `{ kind, version, payload }` arrives from an agent,
|
|
8
|
+
* and the frontend must turn it into something drawable WITHOUT ever throwing
|
|
9
|
+
* and WITHOUT ever silently drawing a coerced half-widget. Resolution returns a
|
|
10
|
+
* typed reason instead, which the caller renders as a safe fallback card.
|
|
11
|
+
*/
|
|
12
|
+
import {
|
|
13
|
+
createContributionRegistry,
|
|
14
|
+
type ContributionRegistry,
|
|
15
|
+
type ContributionRegistryOptions,
|
|
16
|
+
} from './contribution-registry';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Why an envelope fell back to the safe card. Populated on the resolution so
|
|
20
|
+
* the Studio debugger (and dev logs) can see WHY — mirrors the Model Resolution
|
|
21
|
+
* Matrix's `ResolvedModelDecision`. Closed set.
|
|
22
|
+
*/
|
|
23
|
+
export enum ContributionFallbackReason {
|
|
24
|
+
UnknownKind = 'unknown-kind',
|
|
25
|
+
InvalidPayload = 'invalid-payload',
|
|
26
|
+
UnsupportedVersion = 'unsupported-version',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The minimum an agent-emitted envelope must carry to be resolvable. */
|
|
30
|
+
export interface ContributionEnvelope {
|
|
31
|
+
readonly kind: string;
|
|
32
|
+
readonly version: number;
|
|
33
|
+
readonly payload: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A versioned renderer. `validate` narrows the wire payload to the renderer's
|
|
38
|
+
* own type (returning `null` routes to the fallback with `InvalidPayload`) and
|
|
39
|
+
* MUST NOT throw; `render` draws from the narrowed payload.
|
|
40
|
+
*/
|
|
41
|
+
export interface RenderableContributionEntry<TRendered, TPayload = unknown> {
|
|
42
|
+
/** Highest schema version this renderer supports. */
|
|
43
|
+
readonly version: number;
|
|
44
|
+
/** Narrowing validator. Returns the typed payload, or `null`. MUST NOT throw. */
|
|
45
|
+
validate(payload: unknown): TPayload | null;
|
|
46
|
+
/** Draw from the narrowed payload. */
|
|
47
|
+
render(payload: TPayload): TRendered;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The outcome of resolving an envelope — inspectable for the Studio debugger. */
|
|
51
|
+
export type RenderableResolution<TRendered> =
|
|
52
|
+
| {
|
|
53
|
+
readonly matched: true;
|
|
54
|
+
readonly kind: string;
|
|
55
|
+
readonly version: number;
|
|
56
|
+
readonly rendered: TRendered;
|
|
57
|
+
}
|
|
58
|
+
| {
|
|
59
|
+
readonly matched: false;
|
|
60
|
+
readonly kind: string;
|
|
61
|
+
readonly reason: ContributionFallbackReason;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** A {@link ContributionRegistry} of versioned renderers, plus `resolve`. */
|
|
65
|
+
export interface RenderableContributionRegistry<TRendered>
|
|
66
|
+
extends Omit<
|
|
67
|
+
ContributionRegistry<RenderableContributionEntry<TRendered>>,
|
|
68
|
+
'registerFirstParty' | 'register'
|
|
69
|
+
> {
|
|
70
|
+
registerFirstParty<TPayload>(
|
|
71
|
+
kind: string,
|
|
72
|
+
entry: RenderableContributionEntry<TRendered, TPayload>,
|
|
73
|
+
): void;
|
|
74
|
+
register<TPayload>(
|
|
75
|
+
kind: string,
|
|
76
|
+
entry: RenderableContributionEntry<TRendered, TPayload>,
|
|
77
|
+
): void;
|
|
78
|
+
/**
|
|
79
|
+
* Resolve an envelope. Pure + deterministic so it can run in render, and it
|
|
80
|
+
* NEVER throws — an unknown kind, a payload the entry rejects, or a `version`
|
|
81
|
+
* newer than the entry supports ALL yield `matched: false` with a typed
|
|
82
|
+
* reason the caller renders as the safe fallback card.
|
|
83
|
+
*/
|
|
84
|
+
resolve(envelope: ContributionEnvelope): RenderableResolution<TRendered>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createRenderableContributionRegistry<TRendered>(
|
|
88
|
+
options: ContributionRegistryOptions<RenderableContributionEntry<TRendered>>,
|
|
89
|
+
): RenderableContributionRegistry<TRendered> {
|
|
90
|
+
const registry =
|
|
91
|
+
createContributionRegistry<RenderableContributionEntry<TRendered>>(options);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
...registry,
|
|
95
|
+
|
|
96
|
+
registerFirstParty<TPayload>(
|
|
97
|
+
kind: string,
|
|
98
|
+
entry: RenderableContributionEntry<TRendered, TPayload>,
|
|
99
|
+
): void {
|
|
100
|
+
registry.registerFirstParty(kind, entry);
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
register<TPayload>(
|
|
104
|
+
kind: string,
|
|
105
|
+
entry: RenderableContributionEntry<TRendered, TPayload>,
|
|
106
|
+
): void {
|
|
107
|
+
registry.register(kind, entry);
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
resolve(envelope: ContributionEnvelope): RenderableResolution<TRendered> {
|
|
111
|
+
const entry = registry.get(envelope.kind);
|
|
112
|
+
if (entry === undefined) {
|
|
113
|
+
return {
|
|
114
|
+
matched: false,
|
|
115
|
+
kind: envelope.kind,
|
|
116
|
+
reason: ContributionFallbackReason.UnknownKind,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (envelope.version > entry.version) {
|
|
120
|
+
return {
|
|
121
|
+
matched: false,
|
|
122
|
+
kind: envelope.kind,
|
|
123
|
+
reason: ContributionFallbackReason.UnsupportedVersion,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const narrowed = entry.validate(envelope.payload);
|
|
127
|
+
if (narrowed === null) {
|
|
128
|
+
return {
|
|
129
|
+
matched: false,
|
|
130
|
+
kind: envelope.kind,
|
|
131
|
+
reason: ContributionFallbackReason.InvalidPayload,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
matched: true,
|
|
136
|
+
kind: envelope.kind,
|
|
137
|
+
version: entry.version,
|
|
138
|
+
rendered: entry.render(narrowed),
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -43,10 +43,30 @@ function scalarSchema(field: CaptureFieldSpec): z.ZodTypeAny {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/** True when `value` parses as JSON. Total — never throws. */
|
|
47
|
+
export function isParsableJson(value: string): boolean {
|
|
48
|
+
try {
|
|
49
|
+
JSON.parse(value);
|
|
50
|
+
return true;
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
46
56
|
function fieldSchema(field: CaptureFieldSpec): z.ZodTypeAny {
|
|
47
57
|
let base: z.ZodTypeAny;
|
|
48
58
|
if (field.type === CaptureFieldType.Object) {
|
|
49
59
|
base = objectSchema(field.fields ?? []);
|
|
60
|
+
} else if (field.type === CaptureFieldType.Json) {
|
|
61
|
+
// A JSON blob is carried as TEXT and validated as parseable. `''` passes
|
|
62
|
+
// the shape check so an optional blank field stays valid; requiredness is
|
|
63
|
+
// enforced by the `.min(1)` below, not by the parse rule.
|
|
64
|
+
const text = field.required
|
|
65
|
+
? z.string().min(1, { message: `${field.label} is required` })
|
|
66
|
+
: z.string();
|
|
67
|
+
base = text.refine((v) => v === '' || isParsableJson(v), {
|
|
68
|
+
message: `${field.label} must be valid JSON`,
|
|
69
|
+
});
|
|
50
70
|
} else if (field.type === CaptureFieldType.List) {
|
|
51
71
|
let arr = z.array(objectSchema(field.fields ?? []));
|
|
52
72
|
if (typeof field.min === 'number') arr = arr.min(field.min, { message: `${field.label} needs at least ${field.min} item(s)` });
|
|
@@ -58,6 +78,8 @@ function fieldSchema(field: CaptureFieldSpec): z.ZodTypeAny {
|
|
|
58
78
|
field.required &&
|
|
59
79
|
(field.type === CaptureFieldType.String ||
|
|
60
80
|
field.type === CaptureFieldType.Text ||
|
|
81
|
+
field.type === CaptureFieldType.Email ||
|
|
82
|
+
field.type === CaptureFieldType.Url ||
|
|
61
83
|
field.type === CaptureFieldType.User ||
|
|
62
84
|
field.type === CaptureFieldType.Reference ||
|
|
63
85
|
field.type === CaptureFieldType.Datetime ||
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// Total runtime validator for a `CaptureFieldSpec[]` arriving from the wire
|
|
3
|
+
//
|
|
4
|
+
// `fieldSpecToZod` validates the VALUES a form produces. This validates the
|
|
5
|
+
// SCHEMA itself, for the case where the field list is not authored in TypeScript
|
|
6
|
+
// but carried in an envelope (a Vista `form` surface payload, a biome
|
|
7
|
+
// contribution, an agent-authored capture step).
|
|
8
|
+
//
|
|
9
|
+
// Contract: TOTAL and NEVER throws — it returns the narrowed specs or `null`,
|
|
10
|
+
// exactly like the Vista payload validators, so a malformed schema degrades to
|
|
11
|
+
// a visible typed fallback instead of a half-rendered form.
|
|
12
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
13
|
+
import { CaptureFieldTransform, CaptureFieldType, type CaptureFieldSpec } from './field-spec';
|
|
14
|
+
|
|
15
|
+
const FIELD_TYPES: ReadonlySet<string> = new Set(Object.values(CaptureFieldType));
|
|
16
|
+
const TRANSFORMS: ReadonlySet<string> = new Set(Object.values(CaptureFieldTransform));
|
|
17
|
+
|
|
18
|
+
type UnknownRecord = Readonly<Record<string, unknown>>;
|
|
19
|
+
|
|
20
|
+
function asRecord(value: unknown): UnknownRecord | null {
|
|
21
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
22
|
+
? (value as UnknownRecord)
|
|
23
|
+
: null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `undefined` passes through; a present non-string is a rejection (`null`). */
|
|
27
|
+
function optionalString(value: unknown): string | undefined | null {
|
|
28
|
+
if (value === undefined) return undefined;
|
|
29
|
+
return typeof value === 'string' ? value : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `undefined` passes through; a present non-boolean is a rejection (`null`). */
|
|
33
|
+
function optionalBoolean(value: unknown): boolean | undefined | null {
|
|
34
|
+
if (value === undefined) return undefined;
|
|
35
|
+
return typeof value === 'boolean' ? value : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** `undefined` passes through; a present non-number is a rejection (`null`). */
|
|
39
|
+
function optionalNumber(value: unknown): number | undefined | null {
|
|
40
|
+
if (value === undefined) return undefined;
|
|
41
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Narrow ONE field spec. Returns `null` for anything malformed. */
|
|
45
|
+
export function validateCaptureField(raw: unknown): CaptureFieldSpec | null {
|
|
46
|
+
const record = asRecord(raw);
|
|
47
|
+
if (record === null) return null;
|
|
48
|
+
if (typeof record.key !== 'string' || record.key.length === 0) return null;
|
|
49
|
+
if (typeof record.label !== 'string' || record.label.length === 0) return null;
|
|
50
|
+
if (typeof record.type !== 'string' || !FIELD_TYPES.has(record.type)) return null;
|
|
51
|
+
|
|
52
|
+
const required = optionalBoolean(record.required);
|
|
53
|
+
if (required === null) return null;
|
|
54
|
+
const isSecret = optionalBoolean(record.isSecret);
|
|
55
|
+
if (isSecret === null) return null;
|
|
56
|
+
const description = optionalString(record.description);
|
|
57
|
+
if (description === null) return null;
|
|
58
|
+
const placeholder = optionalString(record.placeholder);
|
|
59
|
+
if (placeholder === null) return null;
|
|
60
|
+
const section = optionalString(record.section);
|
|
61
|
+
if (section === null) return null;
|
|
62
|
+
const referenceEntity = optionalString(record.referenceEntity);
|
|
63
|
+
if (referenceEntity === null) return null;
|
|
64
|
+
const accept = optionalString(record.accept);
|
|
65
|
+
if (accept === null) return null;
|
|
66
|
+
const min = optionalNumber(record.min);
|
|
67
|
+
if (min === null) return null;
|
|
68
|
+
const max = optionalNumber(record.max);
|
|
69
|
+
if (max === null) return null;
|
|
70
|
+
|
|
71
|
+
let transform: CaptureFieldTransform | undefined;
|
|
72
|
+
if (record.transform !== undefined) {
|
|
73
|
+
if (typeof record.transform !== 'string' || !TRANSFORMS.has(record.transform)) return null;
|
|
74
|
+
transform = record.transform as CaptureFieldTransform;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let enumValues: readonly string[] | undefined;
|
|
78
|
+
if (record.enumValues !== undefined) {
|
|
79
|
+
if (!Array.isArray(record.enumValues)) return null;
|
|
80
|
+
if (!record.enumValues.every((v) => typeof v === 'string')) return null;
|
|
81
|
+
enumValues = record.enumValues;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let capture: 'user' | 'environment' | undefined;
|
|
85
|
+
if (record.capture !== undefined) {
|
|
86
|
+
if (record.capture !== 'user' && record.capture !== 'environment') return null;
|
|
87
|
+
capture = record.capture;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let fields: readonly CaptureFieldSpec[] | undefined;
|
|
91
|
+
if (record.fields !== undefined) {
|
|
92
|
+
const nested = validateCaptureFields(record.fields);
|
|
93
|
+
if (nested === null) return null;
|
|
94
|
+
fields = nested;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const spec: {
|
|
98
|
+
key: string;
|
|
99
|
+
label: string;
|
|
100
|
+
type: CaptureFieldType;
|
|
101
|
+
required?: boolean;
|
|
102
|
+
isSecret?: boolean;
|
|
103
|
+
transform?: CaptureFieldTransform;
|
|
104
|
+
description?: string;
|
|
105
|
+
placeholder?: string;
|
|
106
|
+
section?: string;
|
|
107
|
+
referenceEntity?: string;
|
|
108
|
+
accept?: string;
|
|
109
|
+
capture?: 'user' | 'environment';
|
|
110
|
+
enumValues?: readonly string[];
|
|
111
|
+
fields?: readonly CaptureFieldSpec[];
|
|
112
|
+
min?: number;
|
|
113
|
+
max?: number;
|
|
114
|
+
} = { key: record.key, label: record.label, type: record.type as CaptureFieldType };
|
|
115
|
+
|
|
116
|
+
if (required !== undefined) spec.required = required;
|
|
117
|
+
if (isSecret !== undefined) spec.isSecret = isSecret;
|
|
118
|
+
if (transform !== undefined) spec.transform = transform;
|
|
119
|
+
if (description !== undefined) spec.description = description;
|
|
120
|
+
if (placeholder !== undefined) spec.placeholder = placeholder;
|
|
121
|
+
if (section !== undefined) spec.section = section;
|
|
122
|
+
if (referenceEntity !== undefined) spec.referenceEntity = referenceEntity;
|
|
123
|
+
if (accept !== undefined) spec.accept = accept;
|
|
124
|
+
if (capture !== undefined) spec.capture = capture;
|
|
125
|
+
if (enumValues !== undefined) spec.enumValues = enumValues;
|
|
126
|
+
if (fields !== undefined) spec.fields = fields;
|
|
127
|
+
if (min !== undefined) spec.min = min;
|
|
128
|
+
if (max !== undefined) spec.max = max;
|
|
129
|
+
|
|
130
|
+
return spec;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Narrow a whole field list. An EMPTY array is valid and narrows to `[]` — an
|
|
135
|
+
* empty schema is a real, meaningful state (the server is the authority on
|
|
136
|
+
* whether a form with no fields may be submitted), not a validation failure.
|
|
137
|
+
*/
|
|
138
|
+
export function validateCaptureFields(raw: unknown): readonly CaptureFieldSpec[] | null {
|
|
139
|
+
if (!Array.isArray(raw)) return null;
|
|
140
|
+
const out: CaptureFieldSpec[] = [];
|
|
141
|
+
for (const entry of raw) {
|
|
142
|
+
const spec = validateCaptureField(entry);
|
|
143
|
+
if (spec === null) return null;
|
|
144
|
+
out.push(spec);
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
@@ -50,6 +50,53 @@ export enum CaptureFieldType {
|
|
|
50
50
|
Image = 'image',
|
|
51
51
|
File = 'file',
|
|
52
52
|
Audio = 'audio',
|
|
53
|
+
// ── Scalar text refinements (absorbed from the connector credential form) ──
|
|
54
|
+
// These change only the NATIVE INPUT TYPE (keyboard + browser affordances);
|
|
55
|
+
// the value is a plain string. `Json` additionally renders as a mono textarea
|
|
56
|
+
// and is validated as parseable JSON.
|
|
57
|
+
Email = 'email',
|
|
58
|
+
Url = 'url',
|
|
59
|
+
Json = 'json',
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Normalization applied to a captured STRING value at the input boundary.
|
|
64
|
+
*
|
|
65
|
+
* Value-identical in meaning to the connector `CredentialFieldTransform`
|
|
66
|
+
* (`NONE`/`UPPERCASE`/`LOWERCASE`/`TRIM`) but named in this kit's lowercase
|
|
67
|
+
* vocabulary, because the capture kit is domain-agnostic and must not import a
|
|
68
|
+
* connector contract. The connector→capture adapter lives at the consuming
|
|
69
|
+
* seam, not here.
|
|
70
|
+
*
|
|
71
|
+
* This is the ONLY normalization the kit performs. There is deliberately no
|
|
72
|
+
* blanket trim on submit: an undeclared transform would silently override a
|
|
73
|
+
* field that declared {@link CaptureFieldTransform.None}.
|
|
74
|
+
*/
|
|
75
|
+
export enum CaptureFieldTransform {
|
|
76
|
+
None = 'none',
|
|
77
|
+
Uppercase = 'uppercase',
|
|
78
|
+
Lowercase = 'lowercase',
|
|
79
|
+
Trim = 'trim',
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Apply a declared transform to a string value. Total; absent = unchanged. */
|
|
83
|
+
export function applyCaptureTransform(
|
|
84
|
+
value: string,
|
|
85
|
+
transform: CaptureFieldTransform | `${CaptureFieldTransform}` | undefined,
|
|
86
|
+
): string {
|
|
87
|
+
switch (transform) {
|
|
88
|
+
case CaptureFieldTransform.Uppercase:
|
|
89
|
+
return value.toUpperCase();
|
|
90
|
+
case CaptureFieldTransform.Lowercase:
|
|
91
|
+
return value.toLowerCase();
|
|
92
|
+
case CaptureFieldTransform.Trim:
|
|
93
|
+
return value.trim();
|
|
94
|
+
case CaptureFieldTransform.None:
|
|
95
|
+
case undefined:
|
|
96
|
+
return value;
|
|
97
|
+
default:
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
53
100
|
}
|
|
54
101
|
|
|
55
102
|
/**
|
|
@@ -96,6 +143,21 @@ export interface CaptureFieldSpec {
|
|
|
96
143
|
* explicit). See {@link groupFieldsBySection}.
|
|
97
144
|
*/
|
|
98
145
|
readonly section?: string;
|
|
146
|
+
/**
|
|
147
|
+
* DECLARED write-only secrecy. When true the input is masked, never
|
|
148
|
+
* pre-filled from a read, and never echoed back by any read path.
|
|
149
|
+
*
|
|
150
|
+
* Secrecy is DECLARED, never inferred from {@link type}: a plain-`string`
|
|
151
|
+
* field can be a secret and a `password`-shaped field can be public. The
|
|
152
|
+
* renderer reads this flag and nothing else — do not re-introduce a
|
|
153
|
+
* `type === password` inference.
|
|
154
|
+
*/
|
|
155
|
+
readonly isSecret?: boolean;
|
|
156
|
+
/**
|
|
157
|
+
* Declared normalization applied to the string value at the input boundary.
|
|
158
|
+
* Absent = {@link CaptureFieldTransform.None}.
|
|
159
|
+
*/
|
|
160
|
+
readonly transform?: CaptureFieldTransform | `${CaptureFieldTransform}`;
|
|
99
161
|
}
|
|
100
162
|
|
|
101
163
|
/** A titled group of fields — the explicit sectioned schema shape. */
|
package/src/ui/capture/index.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
// ── field-spec model (aligned superset of app-forge's AppFieldType) ──
|
|
12
12
|
export {
|
|
13
13
|
CaptureFieldType,
|
|
14
|
+
CaptureFieldTransform,
|
|
15
|
+
applyCaptureTransform,
|
|
14
16
|
isSectioned,
|
|
15
17
|
toFieldList,
|
|
16
18
|
toSections,
|
|
@@ -21,8 +23,14 @@ export {
|
|
|
21
23
|
type CaptureValues,
|
|
22
24
|
} from './field-spec';
|
|
23
25
|
|
|
26
|
+
// ── total runtime validator for a wire-carried schema ──
|
|
27
|
+
export { validateCaptureField, validateCaptureFields } from './field-spec-validate';
|
|
28
|
+
|
|
24
29
|
// ── schema → zod adapter ──
|
|
25
|
-
export { fieldSpecToZod, isEmptyValue } from './field-spec-to-zod';
|
|
30
|
+
export { fieldSpecToZod, isEmptyValue, isParsableJson } from './field-spec-to-zod';
|
|
31
|
+
|
|
32
|
+
// ── values → wire payload ──
|
|
33
|
+
export { captureValuesToStringMap, captureValuesToSubmitPayload } from './submit-values';
|
|
26
34
|
|
|
27
35
|
// ── validation + "missing info" ──
|
|
28
36
|
export { useSchemaValidation, type SchemaValidationResult } from './use-schema-validation';
|
|
@@ -6,8 +6,14 @@
|
|
|
6
6
|
// items) composites. `reference` is source-agnostic here: it defaults to a text
|
|
7
7
|
// input, but a caller can inject a picker via `renderReference` (that is the
|
|
8
8
|
// seam app-forge uses to plug its record-backed `ReferenceSelect`).
|
|
9
|
+
//
|
|
10
|
+
// This is the ONE schema→input renderer in the tree. The connector-specific
|
|
11
|
+
// `CredentialFormRenderer` was folded in here: a DECLARED-secret field
|
|
12
|
+
// (`spec.isSecret`) renders the write-only masked input, and `email`/`url`/
|
|
13
|
+
// `json` are ordinary capture field types. Secrecy is never inferred from the
|
|
14
|
+
// field type.
|
|
9
15
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
10
|
-
import { Plus, Trash2 } from 'lucide-react';
|
|
16
|
+
import { Eye, EyeOff, Plus, Trash2 } from 'lucide-react';
|
|
11
17
|
import { useCallback, useRef, useState, type ReactNode } from 'react';
|
|
12
18
|
|
|
13
19
|
import { cn } from '../cn';
|
|
@@ -17,7 +23,7 @@ import { Input } from '../primitives/input';
|
|
|
17
23
|
import { Label } from '../primitives/label';
|
|
18
24
|
import { Textarea } from '../primitives/textarea';
|
|
19
25
|
|
|
20
|
-
import { CaptureFieldType, type CaptureFieldSpec, type CaptureValues } from './field-spec';
|
|
26
|
+
import { applyCaptureTransform, CaptureFieldType, type CaptureFieldSpec, type CaptureValues } from './field-spec';
|
|
21
27
|
import { asString } from './value-format';
|
|
22
28
|
|
|
23
29
|
const SELECT_CLASS =
|
|
@@ -44,42 +50,78 @@ export interface SchemaFieldInputProps {
|
|
|
44
50
|
readonly renderReference?: RenderReference;
|
|
45
51
|
}
|
|
46
52
|
|
|
53
|
+
/** Native `type` for the single-line text-ish capture types. */
|
|
54
|
+
function nativeInputType(type: CaptureFieldSpec['type']): 'text' | 'email' | 'url' {
|
|
55
|
+
switch (type) {
|
|
56
|
+
case CaptureFieldType.Email:
|
|
57
|
+
return 'email';
|
|
58
|
+
case CaptureFieldType.Url:
|
|
59
|
+
return 'url';
|
|
60
|
+
default:
|
|
61
|
+
return 'text';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
47
65
|
export function SchemaFieldInput(props: SchemaFieldInputProps) {
|
|
48
66
|
const { field, value, onChange, disabled, path, renderReference } = props;
|
|
49
67
|
const inputId = `field-${path}`;
|
|
50
68
|
|
|
69
|
+
// The ONE place a captured string is normalized. Composites (object/list)
|
|
70
|
+
// emit records/arrays and pass through untouched.
|
|
71
|
+
const emit = (next: unknown): void =>
|
|
72
|
+
onChange(typeof next === 'string' ? applyCaptureTransform(next, field.transform) : next);
|
|
73
|
+
|
|
74
|
+
// DECLARED secrecy wins over the field type: a secret is always write-only
|
|
75
|
+
// and masked, whatever shape it has. Never inferred from `type`.
|
|
76
|
+
if (field.isSecret === true) {
|
|
77
|
+
return <SecretFieldInput field={field} value={value} onChange={emit} disabled={disabled} inputId={inputId} />;
|
|
78
|
+
}
|
|
79
|
+
|
|
51
80
|
switch (field.type) {
|
|
52
81
|
case CaptureFieldType.Text:
|
|
53
|
-
return <Textarea id={inputId} value={asString(value)} onChange={(e) =>
|
|
82
|
+
return <Textarea id={inputId} value={asString(value)} onChange={(e) => emit(e.target.value)} disabled={disabled} rows={4} placeholder={field.placeholder} />;
|
|
83
|
+
case CaptureFieldType.Json:
|
|
84
|
+
return (
|
|
85
|
+
<Textarea
|
|
86
|
+
id={inputId}
|
|
87
|
+
value={asString(value)}
|
|
88
|
+
onChange={(e) => emit(e.target.value)}
|
|
89
|
+
disabled={disabled}
|
|
90
|
+
rows={6}
|
|
91
|
+
spellCheck={false}
|
|
92
|
+
className="min-h-[120px] font-mono"
|
|
93
|
+
placeholder={field.placeholder}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
54
96
|
case CaptureFieldType.Number:
|
|
55
97
|
return (
|
|
56
98
|
<Input
|
|
57
99
|
id={inputId}
|
|
58
100
|
type="number"
|
|
59
101
|
value={asString(value)}
|
|
60
|
-
onChange={(e) =>
|
|
102
|
+
onChange={(e) => emit(e.target.value === '' ? '' : Number(e.target.value))}
|
|
61
103
|
disabled={disabled}
|
|
62
104
|
placeholder={field.placeholder}
|
|
63
105
|
/>
|
|
64
106
|
);
|
|
65
107
|
case CaptureFieldType.Decimal:
|
|
66
108
|
// NUMERIC carried verbatim as text — never Number()-coerced (precision).
|
|
67
|
-
return <Input id={inputId} type="text" inputMode="decimal" value={asString(value)} onChange={(e) =>
|
|
109
|
+
return <Input id={inputId} type="text" inputMode="decimal" value={asString(value)} onChange={(e) => emit(e.target.value)} disabled={disabled} placeholder={field.placeholder ?? '0.00'} />;
|
|
68
110
|
case CaptureFieldType.User:
|
|
69
|
-
return <Input id={inputId} value={asString(value)} onChange={(e) =>
|
|
111
|
+
return <Input id={inputId} value={asString(value)} onChange={(e) => emit(e.target.value)} disabled={disabled} placeholder={field.placeholder ?? 'subject id'} />;
|
|
70
112
|
case CaptureFieldType.Boolean:
|
|
71
113
|
return (
|
|
72
114
|
<div className="mt-1">
|
|
73
|
-
<Checkbox id={inputId} checked={value === true} onCheckedChange={(checked) =>
|
|
115
|
+
<Checkbox id={inputId} checked={value === true} onCheckedChange={(checked) => emit(checked === true)} disabled={disabled} />
|
|
74
116
|
</div>
|
|
75
117
|
);
|
|
76
118
|
case CaptureFieldType.Date:
|
|
77
|
-
return <Input id={inputId} type="date" value={asString(value)} onChange={(e) =>
|
|
119
|
+
return <Input id={inputId} type="date" value={asString(value)} onChange={(e) => emit(e.target.value)} disabled={disabled} />;
|
|
78
120
|
case CaptureFieldType.Datetime:
|
|
79
|
-
return <Input id={inputId} type="datetime-local" value={asString(value)} onChange={(e) =>
|
|
121
|
+
return <Input id={inputId} type="datetime-local" value={asString(value)} onChange={(e) => emit(e.target.value)} disabled={disabled} />;
|
|
80
122
|
case CaptureFieldType.Enum:
|
|
81
123
|
return (
|
|
82
|
-
<select id={inputId} className={SELECT_CLASS} value={asString(value)} onChange={(e) =>
|
|
124
|
+
<select id={inputId} className={SELECT_CLASS} value={asString(value)} onChange={(e) => emit(e.target.value)} disabled={disabled}>
|
|
83
125
|
<option value="">— select —</option>
|
|
84
126
|
{(field.enumValues ?? []).map((v) => (
|
|
85
127
|
<option key={v} value={v}>
|
|
@@ -90,9 +132,9 @@ export function SchemaFieldInput(props: SchemaFieldInputProps) {
|
|
|
90
132
|
);
|
|
91
133
|
case CaptureFieldType.Reference:
|
|
92
134
|
return renderReference ? (
|
|
93
|
-
<>{renderReference({ field, value, onChange, disabled, inputId })}</>
|
|
135
|
+
<>{renderReference({ field, value, onChange: emit, disabled, inputId })}</>
|
|
94
136
|
) : (
|
|
95
|
-
<Input id={inputId} value={asString(value)} onChange={(e) =>
|
|
137
|
+
<Input id={inputId} value={asString(value)} onChange={(e) => emit(e.target.value)} disabled={disabled} placeholder={field.placeholder ?? 'reference id'} />
|
|
96
138
|
);
|
|
97
139
|
case CaptureFieldType.Object:
|
|
98
140
|
return <ObjectInput {...props} />;
|
|
@@ -103,10 +145,88 @@ export function SchemaFieldInput(props: SchemaFieldInputProps) {
|
|
|
103
145
|
return <MediaFieldInput {...props} />;
|
|
104
146
|
case CaptureFieldType.Audio:
|
|
105
147
|
return <AudioFieldInput {...props} />;
|
|
106
|
-
// string (and any unknown) —
|
|
148
|
+
// string / email / url (and any unknown) — single-line input.
|
|
107
149
|
default:
|
|
108
|
-
return <Input id={inputId} value={asString(value)} onChange={(e) =>
|
|
150
|
+
return <Input id={inputId} type={nativeInputType(field.type)} value={asString(value)} onChange={(e) => emit(e.target.value)} disabled={disabled} placeholder={field.placeholder} />;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Write-only input for a DECLARED-secret field.
|
|
156
|
+
*
|
|
157
|
+
* `autoComplete="off"` + `data-lpignore` keep password managers and the browser
|
|
158
|
+
* credential store from persisting the value; the reveal toggle is local and
|
|
159
|
+
* transient. The component holds no cache: it renders exactly the value the
|
|
160
|
+
* caller passes and nothing else, so a read path that returns no secret
|
|
161
|
+
* material (the only kind this kit is used with) can never repopulate it.
|
|
162
|
+
*/
|
|
163
|
+
function SecretFieldInput({
|
|
164
|
+
field,
|
|
165
|
+
value,
|
|
166
|
+
onChange,
|
|
167
|
+
disabled,
|
|
168
|
+
inputId,
|
|
169
|
+
}: Readonly<{
|
|
170
|
+
field: CaptureFieldSpec;
|
|
171
|
+
value: unknown;
|
|
172
|
+
onChange: (next: unknown) => void;
|
|
173
|
+
disabled?: boolean;
|
|
174
|
+
inputId: string;
|
|
175
|
+
}>) {
|
|
176
|
+
const [reveal, setReveal] = useState(false);
|
|
177
|
+
const multiline = field.type === CaptureFieldType.Json || field.type === CaptureFieldType.Text;
|
|
178
|
+
|
|
179
|
+
if (multiline && reveal) {
|
|
180
|
+
return (
|
|
181
|
+
<div className="relative">
|
|
182
|
+
<Textarea
|
|
183
|
+
id={inputId}
|
|
184
|
+
value={asString(value)}
|
|
185
|
+
onChange={(e) => onChange(e.target.value)}
|
|
186
|
+
disabled={disabled}
|
|
187
|
+
rows={6}
|
|
188
|
+
spellCheck={false}
|
|
189
|
+
autoComplete="off"
|
|
190
|
+
className="min-h-[120px] pr-9 font-mono"
|
|
191
|
+
placeholder={field.placeholder}
|
|
192
|
+
/>
|
|
193
|
+
<RevealToggle reveal={reveal} onToggle={() => setReveal((v) => !v)} />
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
109
196
|
}
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<div className="relative">
|
|
200
|
+
<Input
|
|
201
|
+
id={inputId}
|
|
202
|
+
type={reveal ? 'text' : 'password'}
|
|
203
|
+
value={asString(value)}
|
|
204
|
+
onChange={(e) => onChange(e.target.value)}
|
|
205
|
+
disabled={disabled}
|
|
206
|
+
autoComplete="off"
|
|
207
|
+
spellCheck={false}
|
|
208
|
+
data-lpignore="true"
|
|
209
|
+
className="pr-9 font-mono"
|
|
210
|
+
placeholder={field.placeholder}
|
|
211
|
+
/>
|
|
212
|
+
<RevealToggle reveal={reveal} onToggle={() => setReveal((v) => !v)} />
|
|
213
|
+
</div>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function RevealToggle({ reveal, onToggle }: Readonly<{ reveal: boolean; onToggle: () => void }>) {
|
|
218
|
+
return (
|
|
219
|
+
<Button
|
|
220
|
+
type="button"
|
|
221
|
+
size="sm"
|
|
222
|
+
variant="ghost"
|
|
223
|
+
className="absolute right-0 top-0 h-9 w-9 p-0"
|
|
224
|
+
onClick={onToggle}
|
|
225
|
+
aria-label={reveal ? 'Hide value' : 'Show value'}
|
|
226
|
+
>
|
|
227
|
+
{reveal ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
228
|
+
</Button>
|
|
229
|
+
);
|
|
110
230
|
}
|
|
111
231
|
|
|
112
232
|
const FILE_INPUT_CLASS =
|