@xemahq/ui-kernel 0.11.1 → 0.11.2
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/package.json +1 -2
- package/src/index.ts +0 -17
- package/src/lib/biome-host/agent-validation.ts +0 -239
- package/src/lib/biome-host/biome-builders.ts +0 -112
- package/src/lib/biome-host/biome-mode.ts +0 -6
- package/src/lib/biome-host/biome-navigation.ts +0 -37
- package/src/lib/biome-host/biome-registry.ts +0 -299
- package/src/lib/biome-host/biome-scope-hooks.ts +0 -41
- package/src/lib/biome-host/biome-scope.tsx +0 -52
- package/src/lib/biome-host/biome-scoped-query.ts +0 -134
- package/src/lib/biome-host/create-biome-orval-config.ts +0 -82
- package/src/lib/biome-host/define-web-biome.spec.ts +0 -221
- package/src/lib/biome-host/define-web-biome.ts +0 -222
- package/src/lib/biome-host/errors.ts +0 -220
- package/src/lib/biome-host/frontend-biome.ts +0 -256
- package/src/lib/biome-host/host-bridge.ts +0 -281
- package/src/lib/biome-host/host-sources.ts +0 -54
- package/src/lib/biome-host/i18n-hooks.ts +0 -51
- package/src/lib/biome-host/i18n-port.test.ts +0 -50
- package/src/lib/biome-host/i18n-port.ts +0 -96
- package/src/lib/biome-host/index.ts +0 -35
- package/src/lib/biome-host/realtime-hooks.ts +0 -75
- package/src/lib/biome-host/realtime-port.ts +0 -115
- package/src/lib/biome-host/session-contributions.ts +0 -296
- package/src/lib/biome-host/use-mutation-with-error-toast.ts +0 -90
- package/src/lib/biome-host/use-page-state.ts +0 -248
- package/src/lib/capabilities/capability-context.ts +0 -25
- package/src/lib/capabilities/capability-provider.tsx +0 -73
- package/src/lib/capabilities/index.ts +0 -17
- package/src/lib/capabilities/types.ts +0 -69
- package/src/lib/capabilities/use-capability.ts +0 -79
- package/src/lib/contribution-registry/contribution-registry.spec.ts +0 -141
- package/src/lib/contribution-registry/contribution-registry.ts +0 -204
- package/src/lib/contribution-registry/index.ts +0 -9
- package/src/lib/contribution-registry/renderable-registry.ts +0 -142
- package/src/lib/system-bus/capability-invoke.ts +0 -92
- package/src/lib/system-bus/deeplink.ts +0 -210
- package/src/lib/system-bus/enums.ts +0 -86
- package/src/lib/system-bus/host-ports.ts +0 -93
- package/src/lib/system-bus/index.ts +0 -16
- package/src/lib/system-bus/intent-registry.ts +0 -109
- package/src/lib/system-bus/intents.ts +0 -113
- package/src/lib/system-bus/palette.ts +0 -78
- package/src/lib/system-bus/system-bus-builder.ts +0 -159
- package/src/lib/system-bus/system-bus.ts +0 -37
- package/src/lib/system-bus/windows.ts +0 -51
- package/src/offline/id.ts +0 -16
- package/src/offline/index.ts +0 -54
- package/src/offline/mutation.ts +0 -75
- package/src/offline/offline-status-badge.tsx +0 -51
- package/src/offline/online-controller.ts +0 -105
- package/src/offline/outbox.test.ts +0 -172
- package/src/offline/outbox.ts +0 -348
- package/src/offline/react-hooks.ts +0 -40
- package/src/offline/react.tsx +0 -32
- package/src/offline/storage.ts +0 -112
- package/src/offline/types.ts +0 -77
- package/src/org-db/components/DbResultTable.tsx +0 -176
- package/src/org-db/index.ts +0 -1
- package/src/pdf/index.ts +0 -11
- package/src/registry/index.ts +0 -9
- package/src/registry/lib/agent-validation-host.ts +0 -36
- package/src/registry/lib/biome-slot.tsx +0 -50
- package/src/registry/lib/biomes-enabled-context.ts +0 -20
- package/src/registry/lib/extension-points.ts +0 -134
- package/src/registry/lib/primitives/SessionEventCard.tsx +0 -164
- package/src/registry/lib/primitives/SessionEventTimeline.tsx +0 -95
- package/src/registry/lib/primitives/SessionMutationBar.tsx +0 -76
- package/src/registry/lib/primitives/SessionTabbedDrawer.tsx +0 -165
- package/src/registry/lib/primitives/index.ts +0 -18
- package/src/registry/lib/session-context-builder.ts +0 -84
- package/src/registry/lib/session-context-provider.tsx +0 -33
- package/src/registry/lib/session-context.ts +0 -21
- package/src/registry/lib/session-selectors.ts +0 -272
- package/src/session/comments/CommentRail.tsx +0 -164
- package/src/session/index.ts +0 -3
- package/src/session/lib/cn.ts +0 -11
- package/src/session/shell/SessionWorkspaceShell.tsx +0 -147
- package/src/session-kit/approvals/ApprovalButton.tsx +0 -89
- package/src/session-kit/approvals/ApprovalCard.tsx +0 -341
- package/src/session-kit/approvals/ApprovalsCenter.tsx +0 -336
- package/src/session-kit/approvals/CapabilityApprovalStickyBar.tsx +0 -118
- package/src/session-kit/approvals/Obligation.tsx +0 -111
- package/src/session-kit/approvals/ScopedBody.tsx +0 -392
- package/src/session-kit/approvals/approval-icons.ts +0 -31
- package/src/session-kit/approvals/approval-model.ts +0 -204
- package/src/session-kit/approvals/index.ts +0 -22
- package/src/session-kit/approvals/obligation-display.ts +0 -100
- package/src/session-kit/approvals/risk-accent.ts +0 -47
- package/src/session-kit/approvals/scope-icons.ts +0 -19
- package/src/session-kit/combobox/Combobox.tsx +0 -358
- package/src/session-kit/combobox/use-click-outside.ts +0 -21
- package/src/session-kit/display/ContextHeader.tsx +0 -148
- package/src/session-kit/display/FileDiffCard.tsx +0 -140
- package/src/session-kit/display/MD.tsx +0 -162
- package/src/session-kit/display/MessageTurn.tsx +0 -157
- package/src/session-kit/display/ThinkingPanel.tsx +0 -81
- package/src/session-kit/display/TodoChecklist.tsx +0 -120
- package/src/session-kit/display/TokenMeter.tsx +0 -89
- package/src/session-kit/display/ToolStrip.tsx +0 -278
- package/src/session-kit/display/TypingDots.tsx +0 -24
- package/src/session-kit/index.ts +0 -44
- package/src/session-kit/lib/enums.ts +0 -66
- package/src/session-kit/lib/portal-accent.ts +0 -30
- package/src/session-kit/lib/status-dot.ts +0 -68
- package/src/session-kit/primitives/Avatar.tsx +0 -44
- package/src/session-kit/primitives/PortalGlyph.tsx +0 -51
- package/src/session-kit/primitives/RiskPill.tsx +0 -95
- package/src/session-kit/primitives/ScopeDot.tsx +0 -47
- package/src/session-kit/primitives/Segmented.tsx +0 -71
- package/src/ui/branding/app-branding-context.ts +0 -20
- package/src/ui/branding/app-branding-provider.tsx +0 -123
- package/src/ui/branding/sanitize.ts +0 -89
- package/src/ui/breakpoints.ts +0 -14
- package/src/ui/capture/document-preview.tsx +0 -166
- package/src/ui/capture/field-spec-to-zod.test.ts +0 -81
- package/src/ui/capture/field-spec-to-zod.ts +0 -174
- package/src/ui/capture/field-spec-validate.ts +0 -236
- package/src/ui/capture/field-spec.test.ts +0 -49
- package/src/ui/capture/field-spec.ts +0 -274
- package/src/ui/capture/index.ts +0 -51
- package/src/ui/capture/schema-field-input.tsx +0 -620
- package/src/ui/capture/schema-form.tsx +0 -149
- package/src/ui/capture/submit-values.test.ts +0 -70
- package/src/ui/capture/submit-values.ts +0 -63
- package/src/ui/capture/use-prefill.ts +0 -186
- package/src/ui/capture/use-schema-validation.ts +0 -145
- package/src/ui/capture/use-wizard.ts +0 -180
- package/src/ui/capture/value-format.ts +0 -32
- package/src/ui/capture/wizard.tsx +0 -105
- package/src/ui/charts/bar-chart.tsx +0 -139
- package/src/ui/charts/chart-frame.tsx +0 -125
- package/src/ui/charts/chart-types.ts +0 -52
- package/src/ui/charts/chart-utils.ts +0 -190
- package/src/ui/charts/charts.test.tsx +0 -204
- package/src/ui/charts/donut-chart.tsx +0 -129
- package/src/ui/charts/index.ts +0 -27
- package/src/ui/charts/line-chart.tsx +0 -202
- package/src/ui/charts/sparkline.tsx +0 -109
- package/src/ui/chrome/AsyncBoundary.tsx +0 -74
- package/src/ui/chrome/EmptyState.tsx +0 -257
- package/src/ui/chrome/ErrorCard.tsx +0 -63
- package/src/ui/chrome/LoadingState.tsx +0 -61
- package/src/ui/chrome/PageHeader.tsx +0 -137
- package/src/ui/chrome/RecordCardList.tsx +0 -98
- package/src/ui/chrome/StateCard.tsx +0 -150
- package/src/ui/chrome/confirm-delete-dialog.tsx +0 -59
- package/src/ui/chrome/page-shell.spec.tsx +0 -117
- package/src/ui/chrome/page-shell.tsx +0 -221
- package/src/ui/chrome/scope-badge.tsx +0 -40
- package/src/ui/chrome/status-badge.tsx +0 -79
- package/src/ui/cn.ts +0 -32
- package/src/ui/design-tokens.ts +0 -372
- package/src/ui/hooks/use-debounced-value.ts +0 -16
- package/src/ui/hooks/use-is-mobile.ts +0 -28
- package/src/ui/index.ts +0 -110
- package/src/ui/pdf/pdf-preview.tsx +0 -122
- package/src/ui/primitives/alert-dialog.tsx +0 -104
- package/src/ui/primitives/async-combobox.tsx +0 -182
- package/src/ui/primitives/badge-variants.ts +0 -21
- package/src/ui/primitives/badge.tsx +0 -25
- package/src/ui/primitives/button-variants.ts +0 -31
- package/src/ui/primitives/button.tsx +0 -29
- package/src/ui/primitives/card.tsx +0 -43
- package/src/ui/primitives/checkbox.tsx +0 -26
- package/src/ui/primitives/collapsible.tsx +0 -9
- package/src/ui/primitives/dialog.tsx +0 -103
- package/src/ui/primitives/dropdown-menu.tsx +0 -179
- package/src/ui/primitives/form-stepper.tsx +0 -94
- package/src/ui/primitives/form-stepper.types.ts +0 -15
- package/src/ui/primitives/input.tsx +0 -22
- package/src/ui/primitives/label.tsx +0 -17
- package/src/ui/primitives/overflow-tabs.tsx +0 -331
- package/src/ui/primitives/popover.tsx +0 -33
- package/src/ui/primitives/preview-device-frame.tsx +0 -213
- package/src/ui/primitives/radio-group.tsx +0 -36
- package/src/ui/primitives/resizable.tsx +0 -67
- package/src/ui/primitives/scroll-area.tsx +0 -38
- package/src/ui/primitives/select.tsx +0 -143
- package/src/ui/primitives/separator.tsx +0 -20
- package/src/ui/primitives/sheet.tsx +0 -107
- package/src/ui/primitives/skeleton.tsx +0 -99
- package/src/ui/primitives/switch.tsx +0 -27
- package/src/ui/primitives/table.tsx +0 -72
- package/src/ui/primitives/tabs.tsx +0 -53
- package/src/ui/primitives/tag-multi-select.tsx +0 -261
- package/src/ui/primitives/textarea.tsx +0 -21
- package/src/ui/primitives/tooltip.tsx +0 -30
- package/src/ui/primitives/url-preview-chrome.tsx +0 -103
- package/src/vista/GenericCatalogueSurface.tsx +0 -179
- package/src/vista/SurfaceFallbackCard.tsx +0 -109
- package/src/vista/VistaPanel.tsx +0 -163
- package/src/vista/form-payload.spec.ts +0 -139
- package/src/vista/html/CustomUiFrame.spec.tsx +0 -127
- package/src/vista/html/CustomUiFrame.tsx +0 -206
- package/src/vista/html/custom-ui-frame-bridge.spec.ts +0 -191
- package/src/vista/html/custom-ui-frame-bridge.ts +0 -98
- package/src/vista/html/custom-ui-frame-contract.ts +0 -88
- package/src/vista/html/custom-ui-intents.spec.ts +0 -135
- package/src/vista/html/custom-ui-intents.ts +0 -155
- package/src/vista/html/custom-ui-srcdoc.spec.ts +0 -151
- package/src/vista/html/custom-ui-srcdoc.ts +0 -193
- package/src/vista/html/index.ts +0 -13
- package/src/vista/index.ts +0 -52
- package/src/vista/surface-catalogue.spec.ts +0 -140
- package/src/vista/surface-catalogue.ts +0 -90
- package/src/vista/surface-icons.ts +0 -57
- package/src/vista/surface-registry.ts +0 -56
- package/src/vista/surfaces/ChartSurface.tsx +0 -34
- package/src/vista/surfaces/FilesSurface.tsx +0 -202
- package/src/vista/surfaces/FormSurface.tsx +0 -380
- package/src/vista/surfaces/HtmlSurface.tsx +0 -43
- package/src/vista/surfaces/TableSurface.tsx +0 -100
- package/src/vista/surfaces/UrlSurface.tsx +0 -40
- package/src/vista/surfaces/blob-surfaces.tsx +0 -139
- package/src/vista/surfaces/elicitation-transport.spec.ts +0 -369
- package/src/vista/surfaces/elicitation-transport.ts +0 -367
- package/src/vista/surfaces/file-surface-utils.ts +0 -69
- package/src/vista/surfaces/index.ts +0 -24
- package/src/vista/surfaces/register-surfaces.tsx +0 -139
- package/src/vista/surfaces/text-surfaces.tsx +0 -132
- package/src/vista/use-vista-tabs.ts +0 -196
- package/src/vista/vista-code-renderer-context.ts +0 -16
- package/src/vista/vista-code-renderer.tsx +0 -35
- package/src/vista/vista-file-context.ts +0 -14
- package/src/vista/vista-file-source.tsx +0 -141
- package/src/vista/vista-form-context.ts +0 -20
- package/src/vista/vista-form-host.tsx +0 -48
- package/src/vista/vista-model.ts +0 -137
- package/src/vista/vista-open-context.tsx +0 -27
- package/src/vista/vista-open-state.ts +0 -11
- package/src/vista/vista-payloads.ts +0 -635
- package/src/vista/vista-tabs-manifest.ts +0 -138
- package/src/vista/vista-tools.ts +0 -232
|
@@ -1,620 +0,0 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
-
// SchemaFieldInput — one typed input per CaptureFieldType
|
|
3
|
-
//
|
|
4
|
-
// Mirrors app-forge-web's `FieldInput` (the 10 typed inputs) using ui-kernel
|
|
5
|
-
// primitives, and ADDS the `object` (nested group) and `list` (repeatable
|
|
6
|
-
// items) composites. `reference` is source-agnostic here: it defaults to a text
|
|
7
|
-
// input, but a caller can inject a picker via `renderReference` (that is the
|
|
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.
|
|
15
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
16
|
-
import { Eye, EyeOff, Plus, Trash2 } from 'lucide-react';
|
|
17
|
-
import { useCallback, useRef, useState, type ReactNode } from 'react';
|
|
18
|
-
|
|
19
|
-
import { cn } from '../cn';
|
|
20
|
-
import {
|
|
21
|
-
applyCaptureTransform,
|
|
22
|
-
CaptureFieldType,
|
|
23
|
-
type CaptureFieldSpec,
|
|
24
|
-
type CaptureValues,
|
|
25
|
-
} from './field-spec';
|
|
26
|
-
import { asString } from './value-format';
|
|
27
|
-
import { Button } from '../primitives/button';
|
|
28
|
-
import { Checkbox } from '../primitives/checkbox';
|
|
29
|
-
import { Input } from '../primitives/input';
|
|
30
|
-
import { Label } from '../primitives/label';
|
|
31
|
-
import { Textarea } from '../primitives/textarea';
|
|
32
|
-
|
|
33
|
-
const SELECT_CLASS =
|
|
34
|
-
'mt-1 flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-body-1 text-foreground ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50';
|
|
35
|
-
|
|
36
|
-
/** Optional override for the `reference` field type (record-backed picker). */
|
|
37
|
-
export type RenderReference = (args: {
|
|
38
|
-
readonly field: CaptureFieldSpec;
|
|
39
|
-
readonly value: unknown;
|
|
40
|
-
readonly onChange: (value: unknown) => void;
|
|
41
|
-
readonly disabled?: boolean;
|
|
42
|
-
readonly inputId: string;
|
|
43
|
-
}) => ReactNode;
|
|
44
|
-
|
|
45
|
-
export interface SchemaFieldInputProps {
|
|
46
|
-
readonly field: CaptureFieldSpec;
|
|
47
|
-
readonly value: unknown;
|
|
48
|
-
readonly onChange: (value: unknown) => void;
|
|
49
|
-
readonly disabled?: boolean;
|
|
50
|
-
/** Dot-path prefix for nested/list item fields (labels/ids stay unique). */
|
|
51
|
-
readonly path: string;
|
|
52
|
-
/** Per-field errors keyed by dot-path (from `useSchemaValidation` or server). */
|
|
53
|
-
readonly errors?: Readonly<Record<string, string>>;
|
|
54
|
-
readonly renderReference?: RenderReference;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Native `type` for the single-line text-ish capture types. */
|
|
58
|
-
function nativeInputType(
|
|
59
|
-
type: CaptureFieldSpec['type'],
|
|
60
|
-
): 'text' | 'email' | 'url' {
|
|
61
|
-
switch (type) {
|
|
62
|
-
case CaptureFieldType.Email:
|
|
63
|
-
return 'email';
|
|
64
|
-
case CaptureFieldType.Url:
|
|
65
|
-
return 'url';
|
|
66
|
-
default:
|
|
67
|
-
return 'text';
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function SchemaFieldInput(props: SchemaFieldInputProps) {
|
|
72
|
-
const { field, value, onChange, disabled, path, renderReference } = props;
|
|
73
|
-
const inputId = `field-${path}`;
|
|
74
|
-
|
|
75
|
-
// The ONE place a captured string is normalized. Composites (object/list)
|
|
76
|
-
// emit records/arrays and pass through untouched.
|
|
77
|
-
const emit = (next: unknown): void =>
|
|
78
|
-
onChange(
|
|
79
|
-
typeof next === 'string'
|
|
80
|
-
? applyCaptureTransform(next, field.transform)
|
|
81
|
-
: next,
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
// DECLARED secrecy wins over the field type: a secret is always write-only
|
|
85
|
-
// and masked, whatever shape it has. Never inferred from `type`.
|
|
86
|
-
if (field.isSecret === true) {
|
|
87
|
-
return (
|
|
88
|
-
<SecretFieldInput
|
|
89
|
-
field={field}
|
|
90
|
-
value={value}
|
|
91
|
-
onChange={emit}
|
|
92
|
-
disabled={disabled}
|
|
93
|
-
inputId={inputId}
|
|
94
|
-
/>
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
switch (field.type) {
|
|
99
|
-
case CaptureFieldType.Text:
|
|
100
|
-
return (
|
|
101
|
-
<Textarea
|
|
102
|
-
id={inputId}
|
|
103
|
-
value={asString(value)}
|
|
104
|
-
onChange={(e) => emit(e.target.value)}
|
|
105
|
-
disabled={disabled}
|
|
106
|
-
rows={4}
|
|
107
|
-
placeholder={field.placeholder}
|
|
108
|
-
/>
|
|
109
|
-
);
|
|
110
|
-
case CaptureFieldType.Json:
|
|
111
|
-
return (
|
|
112
|
-
<Textarea
|
|
113
|
-
id={inputId}
|
|
114
|
-
value={asString(value)}
|
|
115
|
-
onChange={(e) => emit(e.target.value)}
|
|
116
|
-
disabled={disabled}
|
|
117
|
-
rows={6}
|
|
118
|
-
spellCheck={false}
|
|
119
|
-
className="min-h-[120px] font-mono"
|
|
120
|
-
placeholder={field.placeholder}
|
|
121
|
-
/>
|
|
122
|
-
);
|
|
123
|
-
case CaptureFieldType.Number:
|
|
124
|
-
return (
|
|
125
|
-
<Input
|
|
126
|
-
id={inputId}
|
|
127
|
-
type="number"
|
|
128
|
-
value={asString(value)}
|
|
129
|
-
onChange={(e) =>
|
|
130
|
-
emit(e.target.value === '' ? '' : Number(e.target.value))
|
|
131
|
-
}
|
|
132
|
-
disabled={disabled}
|
|
133
|
-
placeholder={field.placeholder}
|
|
134
|
-
/>
|
|
135
|
-
);
|
|
136
|
-
case CaptureFieldType.Decimal:
|
|
137
|
-
// NUMERIC carried verbatim as text — never Number()-coerced (precision).
|
|
138
|
-
return (
|
|
139
|
-
<Input
|
|
140
|
-
id={inputId}
|
|
141
|
-
type="text"
|
|
142
|
-
inputMode="decimal"
|
|
143
|
-
value={asString(value)}
|
|
144
|
-
onChange={(e) => emit(e.target.value)}
|
|
145
|
-
disabled={disabled}
|
|
146
|
-
placeholder={field.placeholder ?? '0.00'}
|
|
147
|
-
/>
|
|
148
|
-
);
|
|
149
|
-
case CaptureFieldType.User:
|
|
150
|
-
return (
|
|
151
|
-
<Input
|
|
152
|
-
id={inputId}
|
|
153
|
-
value={asString(value)}
|
|
154
|
-
onChange={(e) => emit(e.target.value)}
|
|
155
|
-
disabled={disabled}
|
|
156
|
-
placeholder={field.placeholder ?? 'subject id'}
|
|
157
|
-
/>
|
|
158
|
-
);
|
|
159
|
-
case CaptureFieldType.Boolean:
|
|
160
|
-
return (
|
|
161
|
-
<div className="mt-1">
|
|
162
|
-
<Checkbox
|
|
163
|
-
id={inputId}
|
|
164
|
-
checked={value === true}
|
|
165
|
-
onCheckedChange={(checked) => emit(checked === true)}
|
|
166
|
-
disabled={disabled}
|
|
167
|
-
/>
|
|
168
|
-
</div>
|
|
169
|
-
);
|
|
170
|
-
case CaptureFieldType.Date:
|
|
171
|
-
return (
|
|
172
|
-
<Input
|
|
173
|
-
id={inputId}
|
|
174
|
-
type="date"
|
|
175
|
-
value={asString(value)}
|
|
176
|
-
onChange={(e) => emit(e.target.value)}
|
|
177
|
-
disabled={disabled}
|
|
178
|
-
/>
|
|
179
|
-
);
|
|
180
|
-
case CaptureFieldType.Datetime:
|
|
181
|
-
return (
|
|
182
|
-
<Input
|
|
183
|
-
id={inputId}
|
|
184
|
-
type="datetime-local"
|
|
185
|
-
value={asString(value)}
|
|
186
|
-
onChange={(e) => emit(e.target.value)}
|
|
187
|
-
disabled={disabled}
|
|
188
|
-
/>
|
|
189
|
-
);
|
|
190
|
-
case CaptureFieldType.Enum:
|
|
191
|
-
return (
|
|
192
|
-
<select
|
|
193
|
-
id={inputId}
|
|
194
|
-
className={SELECT_CLASS}
|
|
195
|
-
value={asString(value)}
|
|
196
|
-
onChange={(e) => emit(e.target.value)}
|
|
197
|
-
disabled={disabled}
|
|
198
|
-
>
|
|
199
|
-
<option value="">— select —</option>
|
|
200
|
-
{(field.enumValues ?? []).map((v) => (
|
|
201
|
-
<option key={v} value={v}>
|
|
202
|
-
{v}
|
|
203
|
-
</option>
|
|
204
|
-
))}
|
|
205
|
-
</select>
|
|
206
|
-
);
|
|
207
|
-
case CaptureFieldType.Reference:
|
|
208
|
-
return renderReference ? (
|
|
209
|
-
<>
|
|
210
|
-
{renderReference({ field, value, onChange: emit, disabled, inputId })}
|
|
211
|
-
</>
|
|
212
|
-
) : (
|
|
213
|
-
<Input
|
|
214
|
-
id={inputId}
|
|
215
|
-
value={asString(value)}
|
|
216
|
-
onChange={(e) => emit(e.target.value)}
|
|
217
|
-
disabled={disabled}
|
|
218
|
-
placeholder={field.placeholder ?? 'reference id'}
|
|
219
|
-
/>
|
|
220
|
-
);
|
|
221
|
-
case CaptureFieldType.Object:
|
|
222
|
-
return <ObjectInput {...props} />;
|
|
223
|
-
case CaptureFieldType.List:
|
|
224
|
-
return <ListInput {...props} />;
|
|
225
|
-
case CaptureFieldType.Image:
|
|
226
|
-
case CaptureFieldType.File:
|
|
227
|
-
return <MediaFieldInput {...props} />;
|
|
228
|
-
case CaptureFieldType.Audio:
|
|
229
|
-
return <AudioFieldInput {...props} />;
|
|
230
|
-
// string / email / url (and any unknown) — single-line input.
|
|
231
|
-
default:
|
|
232
|
-
return (
|
|
233
|
-
<Input
|
|
234
|
-
id={inputId}
|
|
235
|
-
type={nativeInputType(field.type)}
|
|
236
|
-
value={asString(value)}
|
|
237
|
-
onChange={(e) => emit(e.target.value)}
|
|
238
|
-
disabled={disabled}
|
|
239
|
-
placeholder={field.placeholder}
|
|
240
|
-
/>
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Write-only input for a DECLARED-secret field.
|
|
247
|
-
*
|
|
248
|
-
* `autoComplete="off"` + `data-lpignore` keep password managers and the browser
|
|
249
|
-
* credential store from persisting the value; the reveal toggle is local and
|
|
250
|
-
* transient. The component holds no cache: it renders exactly the value the
|
|
251
|
-
* caller passes and nothing else, so a read path that returns no secret
|
|
252
|
-
* material (the only kind this kit is used with) can never repopulate it.
|
|
253
|
-
*/
|
|
254
|
-
function SecretFieldInput({
|
|
255
|
-
field,
|
|
256
|
-
value,
|
|
257
|
-
onChange,
|
|
258
|
-
disabled,
|
|
259
|
-
inputId,
|
|
260
|
-
}: Readonly<{
|
|
261
|
-
field: CaptureFieldSpec;
|
|
262
|
-
value: unknown;
|
|
263
|
-
onChange: (next: unknown) => void;
|
|
264
|
-
disabled?: boolean;
|
|
265
|
-
inputId: string;
|
|
266
|
-
}>) {
|
|
267
|
-
const [reveal, setReveal] = useState(false);
|
|
268
|
-
const multiline =
|
|
269
|
-
field.type === CaptureFieldType.Json ||
|
|
270
|
-
field.type === CaptureFieldType.Text;
|
|
271
|
-
|
|
272
|
-
if (multiline && reveal) {
|
|
273
|
-
return (
|
|
274
|
-
<div className="relative">
|
|
275
|
-
<Textarea
|
|
276
|
-
id={inputId}
|
|
277
|
-
value={asString(value)}
|
|
278
|
-
onChange={(e) => onChange(e.target.value)}
|
|
279
|
-
disabled={disabled}
|
|
280
|
-
rows={6}
|
|
281
|
-
spellCheck={false}
|
|
282
|
-
autoComplete="off"
|
|
283
|
-
className="min-h-[120px] pr-9 font-mono"
|
|
284
|
-
placeholder={field.placeholder}
|
|
285
|
-
/>
|
|
286
|
-
<RevealToggle reveal={reveal} onToggle={() => setReveal((v) => !v)} />
|
|
287
|
-
</div>
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return (
|
|
292
|
-
<div className="relative">
|
|
293
|
-
<Input
|
|
294
|
-
id={inputId}
|
|
295
|
-
type={reveal ? 'text' : 'password'}
|
|
296
|
-
value={asString(value)}
|
|
297
|
-
onChange={(e) => onChange(e.target.value)}
|
|
298
|
-
disabled={disabled}
|
|
299
|
-
autoComplete="off"
|
|
300
|
-
spellCheck={false}
|
|
301
|
-
data-lpignore="true"
|
|
302
|
-
className="pr-9 font-mono"
|
|
303
|
-
placeholder={field.placeholder}
|
|
304
|
-
/>
|
|
305
|
-
<RevealToggle reveal={reveal} onToggle={() => setReveal((v) => !v)} />
|
|
306
|
-
</div>
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function RevealToggle({
|
|
311
|
-
reveal,
|
|
312
|
-
onToggle,
|
|
313
|
-
}: Readonly<{ reveal: boolean; onToggle: () => void }>) {
|
|
314
|
-
return (
|
|
315
|
-
<Button
|
|
316
|
-
type="button"
|
|
317
|
-
size="sm"
|
|
318
|
-
variant="ghost"
|
|
319
|
-
className="absolute right-0 top-0 h-9 w-9 p-0"
|
|
320
|
-
onClick={onToggle}
|
|
321
|
-
aria-label={reveal ? 'Hide value' : 'Show value'}
|
|
322
|
-
>
|
|
323
|
-
{reveal ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
324
|
-
</Button>
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const FILE_INPUT_CLASS =
|
|
329
|
-
'block w-full text-body-2 text-foreground file:mr-3 file:rounded-md file:border-0 file:bg-secondary file:px-3 file:py-1.5 file:text-body-2 file:text-secondary-foreground hover:file:bg-secondary/80 disabled:cursor-not-allowed disabled:opacity-50';
|
|
330
|
-
|
|
331
|
-
/** Read a File/Blob as a base64 data-URL (JSON-serializable, offline-safe). */
|
|
332
|
-
function readAsDataUrl(file: Blob, onDone: (dataUrl: string) => void): void {
|
|
333
|
-
const reader = new FileReader();
|
|
334
|
-
reader.onload = () =>
|
|
335
|
-
onDone(typeof reader.result === 'string' ? reader.result : '');
|
|
336
|
-
reader.readAsDataURL(file);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* `image`/`file` capture. On mobile, `field.capture` makes an image field open
|
|
341
|
-
* the camera directly. The value is a data-URL so it stays JSON-serializable and
|
|
342
|
-
* flows unchanged through the record model + offline outbox.
|
|
343
|
-
*/
|
|
344
|
-
function MediaFieldInput({
|
|
345
|
-
field,
|
|
346
|
-
value,
|
|
347
|
-
onChange,
|
|
348
|
-
disabled,
|
|
349
|
-
path,
|
|
350
|
-
}: SchemaFieldInputProps) {
|
|
351
|
-
const inputId = `field-${path}`;
|
|
352
|
-
const current = asString(value);
|
|
353
|
-
const isImage = field.type === CaptureFieldType.Image;
|
|
354
|
-
const accept = field.accept ?? (isImage ? 'image/*' : undefined);
|
|
355
|
-
return (
|
|
356
|
-
<div className="mt-1 space-y-2">
|
|
357
|
-
<input
|
|
358
|
-
id={inputId}
|
|
359
|
-
type="file"
|
|
360
|
-
className={FILE_INPUT_CLASS}
|
|
361
|
-
disabled={disabled}
|
|
362
|
-
{...(accept ? { accept } : {})}
|
|
363
|
-
{...(field.capture ? { capture: field.capture } : {})}
|
|
364
|
-
onChange={(e) => {
|
|
365
|
-
const file = e.target.files?.[0];
|
|
366
|
-
if (file) {
|
|
367
|
-
readAsDataUrl(file, onChange);
|
|
368
|
-
}
|
|
369
|
-
}}
|
|
370
|
-
/>
|
|
371
|
-
{current ? (
|
|
372
|
-
<div className="flex items-center gap-2">
|
|
373
|
-
{isImage && current.startsWith('data:image') ? (
|
|
374
|
-
<img
|
|
375
|
-
src={current}
|
|
376
|
-
alt={field.label}
|
|
377
|
-
className="max-h-32 rounded-md border border-input"
|
|
378
|
-
/>
|
|
379
|
-
) : (
|
|
380
|
-
<span className="truncate text-body-2 text-muted-foreground">
|
|
381
|
-
Attached
|
|
382
|
-
</span>
|
|
383
|
-
)}
|
|
384
|
-
<Button
|
|
385
|
-
type="button"
|
|
386
|
-
variant="ghost"
|
|
387
|
-
size="sm"
|
|
388
|
-
onClick={() => onChange('')}
|
|
389
|
-
disabled={disabled}
|
|
390
|
-
>
|
|
391
|
-
Clear
|
|
392
|
-
</Button>
|
|
393
|
-
</div>
|
|
394
|
-
) : null}
|
|
395
|
-
</div>
|
|
396
|
-
);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* `audio` capture — records a voice memo via MediaRecorder and stores it as a
|
|
401
|
-
* data-URL. Browser APIs are touched only inside the record handler (SSR-safe).
|
|
402
|
-
*/
|
|
403
|
-
function AudioFieldInput({
|
|
404
|
-
value,
|
|
405
|
-
onChange,
|
|
406
|
-
disabled,
|
|
407
|
-
path,
|
|
408
|
-
}: SchemaFieldInputProps) {
|
|
409
|
-
const inputId = `field-${path}`;
|
|
410
|
-
const current = asString(value);
|
|
411
|
-
const [recording, setRecording] = useState(false);
|
|
412
|
-
const [error, setError] = useState<string | null>(null);
|
|
413
|
-
const recorderRef = useRef<MediaRecorder | null>(null);
|
|
414
|
-
const chunksRef = useRef<Blob[]>([]);
|
|
415
|
-
|
|
416
|
-
const start = useCallback(async () => {
|
|
417
|
-
setError(null);
|
|
418
|
-
try {
|
|
419
|
-
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
420
|
-
const recorder = new MediaRecorder(stream);
|
|
421
|
-
chunksRef.current = [];
|
|
422
|
-
recorder.ondataavailable = (e) => {
|
|
423
|
-
if (e.data.size > 0) {
|
|
424
|
-
chunksRef.current.push(e.data);
|
|
425
|
-
}
|
|
426
|
-
};
|
|
427
|
-
recorder.onstop = () => {
|
|
428
|
-
for (const track of stream.getTracks()) {
|
|
429
|
-
track.stop();
|
|
430
|
-
}
|
|
431
|
-
readAsDataUrl(
|
|
432
|
-
new Blob(chunksRef.current, {
|
|
433
|
-
type: recorder.mimeType || 'audio/webm',
|
|
434
|
-
}),
|
|
435
|
-
onChange,
|
|
436
|
-
);
|
|
437
|
-
};
|
|
438
|
-
recorderRef.current = recorder;
|
|
439
|
-
recorder.start();
|
|
440
|
-
setRecording(true);
|
|
441
|
-
} catch {
|
|
442
|
-
setError('Microphone unavailable or permission denied.');
|
|
443
|
-
}
|
|
444
|
-
}, [onChange]);
|
|
445
|
-
|
|
446
|
-
const stop = useCallback(() => {
|
|
447
|
-
recorderRef.current?.stop();
|
|
448
|
-
recorderRef.current = null;
|
|
449
|
-
setRecording(false);
|
|
450
|
-
}, []);
|
|
451
|
-
|
|
452
|
-
return (
|
|
453
|
-
<div className="mt-1 space-y-2">
|
|
454
|
-
<div className="flex items-center gap-2">
|
|
455
|
-
{recording ? (
|
|
456
|
-
<Button
|
|
457
|
-
type="button"
|
|
458
|
-
variant="secondary"
|
|
459
|
-
size="sm"
|
|
460
|
-
onClick={stop}
|
|
461
|
-
disabled={disabled}
|
|
462
|
-
>
|
|
463
|
-
Stop
|
|
464
|
-
</Button>
|
|
465
|
-
) : (
|
|
466
|
-
<Button
|
|
467
|
-
type="button"
|
|
468
|
-
variant="secondary"
|
|
469
|
-
size="sm"
|
|
470
|
-
onClick={() => void start()}
|
|
471
|
-
disabled={disabled}
|
|
472
|
-
>
|
|
473
|
-
{current ? 'Re-record' : 'Record'}
|
|
474
|
-
</Button>
|
|
475
|
-
)}
|
|
476
|
-
{current ? (
|
|
477
|
-
<Button
|
|
478
|
-
type="button"
|
|
479
|
-
variant="ghost"
|
|
480
|
-
size="sm"
|
|
481
|
-
onClick={() => onChange('')}
|
|
482
|
-
disabled={disabled}
|
|
483
|
-
>
|
|
484
|
-
Clear
|
|
485
|
-
</Button>
|
|
486
|
-
) : null}
|
|
487
|
-
</div>
|
|
488
|
-
{error ? <p className="text-body-2 text-destructive">{error}</p> : null}
|
|
489
|
-
{current && !recording ? (
|
|
490
|
-
<Button asChild variant="outline" size="sm">
|
|
491
|
-
<a id={inputId} href={current} target="_blank" rel="noreferrer">
|
|
492
|
-
Open recorded audio
|
|
493
|
-
</a>
|
|
494
|
-
</Button>
|
|
495
|
-
) : null}
|
|
496
|
-
</div>
|
|
497
|
-
);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
/** Renders one label + input + error block for a nested field. */
|
|
501
|
-
function NestedField(props: SchemaFieldInputProps) {
|
|
502
|
-
const { field, path, errors } = props;
|
|
503
|
-
const error = errors?.[path];
|
|
504
|
-
return (
|
|
505
|
-
<div>
|
|
506
|
-
<Label htmlFor={`field-${path}`}>
|
|
507
|
-
{field.label}
|
|
508
|
-
{field.required ? <span className="text-destructive"> *</span> : null}
|
|
509
|
-
</Label>
|
|
510
|
-
{field.description ? (
|
|
511
|
-
<p className="mb-1 text-caption text-ink-3">{field.description}</p>
|
|
512
|
-
) : null}
|
|
513
|
-
<SchemaFieldInput {...props} />
|
|
514
|
-
{error ? (
|
|
515
|
-
<p className="mt-1 text-caption text-destructive">{error}</p>
|
|
516
|
-
) : null}
|
|
517
|
-
</div>
|
|
518
|
-
);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
function ObjectInput({
|
|
522
|
-
field,
|
|
523
|
-
value,
|
|
524
|
-
onChange,
|
|
525
|
-
disabled,
|
|
526
|
-
path,
|
|
527
|
-
errors,
|
|
528
|
-
renderReference,
|
|
529
|
-
}: SchemaFieldInputProps) {
|
|
530
|
-
const record = (
|
|
531
|
-
value && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
|
532
|
-
) as CaptureValues;
|
|
533
|
-
const fields = field.fields ?? [];
|
|
534
|
-
return (
|
|
535
|
-
<div className="mt-1 space-y-3 rounded-lg border border-rule bg-paper-elev/40 p-3">
|
|
536
|
-
{fields.map((child) => (
|
|
537
|
-
<NestedField
|
|
538
|
-
key={child.key}
|
|
539
|
-
field={child}
|
|
540
|
-
value={record[child.key]}
|
|
541
|
-
onChange={(v) => onChange({ ...record, [child.key]: v })}
|
|
542
|
-
disabled={disabled}
|
|
543
|
-
path={`${path}.${child.key}`}
|
|
544
|
-
errors={errors}
|
|
545
|
-
renderReference={renderReference}
|
|
546
|
-
/>
|
|
547
|
-
))}
|
|
548
|
-
</div>
|
|
549
|
-
);
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
function ListInput({
|
|
553
|
-
field,
|
|
554
|
-
value,
|
|
555
|
-
onChange,
|
|
556
|
-
disabled,
|
|
557
|
-
path,
|
|
558
|
-
errors,
|
|
559
|
-
renderReference,
|
|
560
|
-
}: SchemaFieldInputProps) {
|
|
561
|
-
const items = (Array.isArray(value) ? value : []) as CaptureValues[];
|
|
562
|
-
const fields = field.fields ?? [];
|
|
563
|
-
const atMax = typeof field.max === 'number' && items.length >= field.max;
|
|
564
|
-
|
|
565
|
-
const setItem = (index: number, next: CaptureValues) =>
|
|
566
|
-
onChange(items.map((it, i) => (i === index ? next : it)));
|
|
567
|
-
const removeItem = (index: number) =>
|
|
568
|
-
onChange(items.filter((_, i) => i !== index));
|
|
569
|
-
const addItem = () => onChange([...items, {}]);
|
|
570
|
-
|
|
571
|
-
return (
|
|
572
|
-
<div className="mt-1 space-y-2">
|
|
573
|
-
{items.map((item, index) => (
|
|
574
|
-
<div
|
|
575
|
-
key={index}
|
|
576
|
-
className="relative space-y-3 rounded-lg border border-rule bg-paper-elev/40 p-3"
|
|
577
|
-
>
|
|
578
|
-
<div className="flex items-center justify-between">
|
|
579
|
-
<span className="text-caption font-medium text-ink-3">
|
|
580
|
-
{field.label} #{index + 1}
|
|
581
|
-
</span>
|
|
582
|
-
<Button
|
|
583
|
-
type="button"
|
|
584
|
-
variant="ghost"
|
|
585
|
-
size="icon"
|
|
586
|
-
onClick={() => removeItem(index)}
|
|
587
|
-
disabled={disabled}
|
|
588
|
-
aria-label={`Remove ${field.label} ${index + 1}`}
|
|
589
|
-
>
|
|
590
|
-
<Trash2 className="h-4 w-4" />
|
|
591
|
-
</Button>
|
|
592
|
-
</div>
|
|
593
|
-
{fields.map((child) => (
|
|
594
|
-
<NestedField
|
|
595
|
-
key={child.key}
|
|
596
|
-
field={child}
|
|
597
|
-
value={item[child.key]}
|
|
598
|
-
onChange={(v) => setItem(index, { ...item, [child.key]: v })}
|
|
599
|
-
disabled={disabled}
|
|
600
|
-
path={`${path}.${index}.${child.key}`}
|
|
601
|
-
errors={errors}
|
|
602
|
-
renderReference={renderReference}
|
|
603
|
-
/>
|
|
604
|
-
))}
|
|
605
|
-
</div>
|
|
606
|
-
))}
|
|
607
|
-
<Button
|
|
608
|
-
type="button"
|
|
609
|
-
variant="outline"
|
|
610
|
-
size="sm"
|
|
611
|
-
onClick={addItem}
|
|
612
|
-
disabled={disabled || atMax}
|
|
613
|
-
className={cn(items.length === 0 && 'w-full')}
|
|
614
|
-
>
|
|
615
|
-
<Plus className="mr-1.5 h-3.5 w-3.5" />
|
|
616
|
-
Add {field.label}
|
|
617
|
-
</Button>
|
|
618
|
-
</div>
|
|
619
|
-
);
|
|
620
|
-
}
|