@aletheia-dev/collection-flow-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.
- package/CHANGELOG.md +17 -0
- package/LICENSE +202 -0
- package/README.md +17 -0
- package/dist/index.cjs +426 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +64 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +408 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +58 -0
- package/package.json +68 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/FieldInput.tsx","../src/FileField.tsx","../src/SubmissionScope.ts","../src/StepForm.tsx","../src/StepPreview.tsx"],"sourcesContent":["import type { Field } from '@aletheia-dev/core';\nimport { useContext } from 'react';\nimport { FileField } from './FileField.js';\nimport { SubmissionScopeContext } from './SubmissionScope.js';\n\nexport interface FieldInputProps {\n field: Field;\n value: unknown;\n error?: string;\n onChange: (value: unknown) => void;\n /** Renders the control disabled (previews, read-only views). */\n readOnly?: boolean;\n}\n\n/** Empty strings become undefined for every type except free text so optional fields validate. */\nfunction normalise(field: Field, raw: string): unknown {\n if (raw === '' && field.type !== 'text') return undefined;\n return raw;\n}\n\nfunction Control({ field, value, onChange, readOnly, id }: FieldInputProps & { id: string }) {\n const scope = useContext(SubmissionScopeContext);\n const text = typeof value === 'string' || typeof value === 'number' ? String(value) : '';\n switch (field.type) {\n case 'text':\n case 'email':\n case 'number':\n case 'date':\n return (\n <input\n id={id}\n type={field.type}\n value={text}\n placeholder={field.placeholder}\n min={field.type === 'number' ? field.validation?.min : undefined}\n max={field.type === 'number' ? field.validation?.max : undefined}\n disabled={readOnly}\n onChange={(e) => onChange(normalise(field, e.target.value))}\n />\n );\n case 'select':\n return (\n <select\n id={id}\n value={text}\n disabled={readOnly}\n onChange={(e) => onChange(normalise(field, e.target.value))}\n >\n <option value=\"\">Select…</option>\n {(field.options ?? []).map((o) => (\n <option key={o.value} value={o.value}>\n {o.label}\n </option>\n ))}\n </select>\n );\n case 'boolean':\n return (\n <input\n id={id}\n type=\"checkbox\"\n checked={value === true}\n disabled={readOnly}\n onChange={(e) => onChange(e.target.checked)}\n />\n );\n case 'file':\n // Uploads are scoped to a submission; without one (builder preview) the control is inert.\n if (!scope) {\n return (\n <div className=\"file-field\">\n <input id={id} type=\"file\" disabled />\n <p className=\"file-status muted\">File upload needs a submission</p>\n </div>\n );\n }\n return (\n <FileField id={id} field={field} value={value} onChange={onChange} disabled={readOnly} />\n );\n }\n}\n\nexport function FieldInput(props: FieldInputProps) {\n const { field, error } = props;\n const id = `field-${field.key}`;\n return (\n <div className=\"field\">\n <label htmlFor={id}>\n {field.label}\n {field.required && ' *'}\n </label>\n <Control {...props} id={id} />\n {field.help && <small>{field.help}</small>}\n {error && <span className=\"error\">{error}</span>}\n </div>\n );\n}\n","import {\n checkFileBeforeUpload,\n isTerminalDocumentStatus,\n pollDocument,\n uploadToStorage,\n type Document,\n type DocumentContentType,\n type FileFieldValue,\n} from '@aletheia-dev/api-client';\nimport type { Field } from '@aletheia-dev/core';\nimport { useEffect, useRef, useState, type ChangeEvent } from 'react';\nimport { useSubmissionScope } from './SubmissionScope.js';\n\n/** Formats an unknown thrown value for display. */\nfunction errorMessage(value: unknown): string {\n return value instanceof Error ? value.message : String(value);\n}\n\nconst ACCEPT = '.jpg,.jpeg,.png,.webp,.pdf,image/jpeg,image/png,image/webp,application/pdf';\n\ntype Phase =\n | { kind: 'empty' }\n /** A saved value exists; its status is being fetched. */\n | { kind: 'loading'; fileName?: string }\n | { kind: 'uploading'; fileName: string; fraction: number }\n | { kind: 'scanning'; fileName: string }\n | { kind: 'clean'; fileName: string }\n | { kind: 'rejected'; fileName: string; reason: string }\n | { kind: 'infected'; fileName: string }\n /** Pre-check failure (not retryable) or an upload/network error (retryable from `create`). */\n | { kind: 'error'; fileName: string; message: string; retryable: boolean };\n\ninterface FileFieldProps {\n id: string;\n field: Field;\n value: unknown;\n onChange: (value: FileFieldValue | undefined) => void;\n /** Blocks picking a file (read-only views); upload/scan progress still shows. */\n disabled?: boolean;\n}\n\nfunction savedValue(value: unknown): FileFieldValue | undefined {\n if (typeof value !== 'object' || value === null) return undefined;\n const { fileId, name } = value as { fileId?: unknown; name?: unknown };\n if (typeof fileId !== 'string') return undefined;\n return { fileId, name: typeof name === 'string' ? name : undefined };\n}\n\nfunction phaseFor(document: Document, fileName: string): Phase {\n switch (document.status) {\n case 'clean':\n return { kind: 'clean', fileName };\n case 'infected':\n return { kind: 'infected', fileName };\n case 'rejected':\n return {\n kind: 'rejected',\n fileName,\n reason: document.rejectionReason ?? 'The file was refused.',\n };\n default:\n return { kind: 'scanning', fileName };\n }\n}\n\n/**\n * A `file` field: pre-checks the pick, uploads straight to storage with progress, finalizes and\n * waits for the scan. The field value becomes `{ fileId, name }` only once the document is clean.\n */\nexport function FileField({ id, field, value, onChange, disabled }: FileFieldProps) {\n const { api, submissionId, upload = uploadToStorage, pollIntervalMs } = useSubmissionScope();\n const saved = savedValue(value);\n const [phase, setPhase] = useState<Phase>(() =>\n saved ? { kind: 'loading', fileName: saved.name } : { kind: 'empty' },\n );\n const inputRef = useRef<HTMLInputElement>(null);\n const lastFile = useRef<File | undefined>(undefined);\n const onChangeRef = useRef(onChange);\n // Attempt counter: results of a superseded attempt (or after unmount) are dropped.\n const attempt = useRef(0);\n const mounted = useRef(false);\n // Ids this field resolved itself, so a value it just set is not fetched again.\n const resolvedId = useRef<string | undefined>(undefined);\n\n useEffect(() => {\n onChangeRef.current = onChange;\n });\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n\n const savedId = saved?.fileId;\n const savedName = saved?.name;\n useEffect(() => {\n if (!savedId || savedId === resolvedId.current) return;\n const current = ++attempt.current;\n const alive = () => mounted.current && current === attempt.current;\n const controller = new AbortController();\n pollDocument(api, savedId, {\n submissionId,\n signal: controller.signal,\n intervalMs: pollIntervalMs,\n })\n .then((document) => {\n if (!alive()) return;\n resolvedId.current = savedId;\n const next = phaseFor(document, savedName ?? document.fileName);\n setPhase(next);\n if (next.kind !== 'clean') onChangeRef.current(undefined);\n })\n .catch((err: unknown) => {\n if (!alive() || controller.signal.aborted) return;\n setPhase({\n kind: 'error',\n fileName: savedName ?? '',\n message: errorMessage(err),\n retryable: false,\n });\n });\n return () => controller.abort();\n }, [api, submissionId, savedId, savedName, pollIntervalMs]);\n\n async function start(file: File) {\n const current = ++attempt.current;\n const alive = () => mounted.current && current === attempt.current;\n lastFile.current = file;\n const check = checkFileBeforeUpload(file);\n if (!check.ok) {\n setPhase({ kind: 'error', fileName: file.name, message: check.reason, retryable: false });\n return;\n }\n onChangeRef.current(undefined);\n setPhase({ kind: 'uploading', fileName: file.name, fraction: 0 });\n try {\n const ticket = await api.documents.create({\n fileName: file.name,\n contentType: file.type as DocumentContentType,\n sizeBytes: file.size,\n submissionId,\n fieldKey: field.key,\n });\n if (!alive()) return;\n await upload(ticket, file, {\n onProgress: (fraction) => {\n if (alive()) setPhase({ kind: 'uploading', fileName: file.name, fraction });\n },\n });\n if (!alive()) return;\n setPhase({ kind: 'scanning', fileName: file.name });\n let document = await api.documents.finalize(ticket.documentId, { submissionId });\n if (!alive()) return;\n if (!isTerminalDocumentStatus(document.status)) {\n document = await pollDocument(api, ticket.documentId, {\n submissionId,\n intervalMs: pollIntervalMs,\n });\n if (!alive()) return;\n }\n const next = phaseFor(document, file.name);\n setPhase(next);\n if (next.kind === 'clean') {\n resolvedId.current = document.id;\n onChangeRef.current({ fileId: document.id, name: file.name });\n }\n } catch (err) {\n if (!alive()) return;\n setPhase({ kind: 'error', fileName: file.name, message: errorMessage(err), retryable: true });\n }\n }\n\n function onPick(event: ChangeEvent<HTMLInputElement>) {\n const file = event.target.files?.[0];\n // Clear the control so picking the same file again fires `change`.\n event.target.value = '';\n if (file) void start(file);\n }\n\n const busy = phase.kind === 'uploading' || phase.kind === 'scanning' || phase.kind === 'loading';\n const retry = (\n <button\n type=\"button\"\n className=\"secondary inline\"\n onClick={() => {\n if (lastFile.current) void start(lastFile.current);\n }}\n >\n Retry\n </button>\n );\n\n return (\n <div className=\"file-field\">\n <input\n ref={inputRef}\n id={id}\n type=\"file\"\n accept={ACCEPT}\n disabled={busy || disabled}\n onChange={onPick}\n />\n {phase.kind === 'uploading' && (\n <>\n <progress value={phase.fraction} max={1} aria-label={`Uploading ${phase.fileName}`} />\n <p className=\"file-status\" role=\"status\">\n Uploading {phase.fileName}… {Math.round(phase.fraction * 100)}%\n </p>\n </>\n )}\n {phase.kind === 'loading' && (\n <p className=\"file-status muted\" role=\"status\">\n Checking {phase.fileName ?? 'the saved file'}…\n </p>\n )}\n {phase.kind === 'scanning' && (\n <p className=\"file-status muted\" role=\"status\">\n Scanning {phase.fileName}…\n </p>\n )}\n {phase.kind === 'clean' && (\n <p className=\"file-status ok\" role=\"status\">\n <span>{phase.fileName} uploaded and checked.</span>\n <button\n type=\"button\"\n className=\"secondary inline\"\n onClick={() => inputRef.current?.click()}\n >\n Replace\n </button>\n </p>\n )}\n {phase.kind === 'rejected' && (\n <p className=\"file-status bad\" role=\"status\">\n <span>\n {phase.fileName} was rejected: {phase.reason}\n </span>\n {retry}\n </p>\n )}\n {phase.kind === 'infected' && (\n <p className=\"file-status bad\" role=\"status\">\n {phase.fileName} failed the malware scan. Please choose another file.\n </p>\n )}\n {phase.kind === 'error' && (\n <p className=\"file-status bad\" role=\"status\">\n <span>{phase.message}</span>\n {phase.retryable && retry}\n </p>\n )}\n </div>\n );\n}\n","import type { ApiClient, uploadToStorage } from '@aletheia-dev/api-client';\nimport { createContext, useContext } from 'react';\n\n/** What fields that talk to the API (file uploads) need from the page rendering the flow. */\nexport interface SubmissionScope {\n api: ApiClient;\n submissionId: string;\n /** Storage uploader; injectable so tests need no XMLHttpRequest. */\n upload?: typeof uploadToStorage;\n /** Scan polling interval; defaults to the client's 750 ms. */\n pollIntervalMs?: number;\n}\n\nexport const SubmissionScopeContext = createContext<SubmissionScope | undefined>(undefined);\n\nexport function useSubmissionScope(): SubmissionScope {\n const scope = useContext(SubmissionScopeContext);\n if (!scope) throw new Error('useSubmissionScope must be used inside SubmissionScopeContext');\n return scope;\n}\n","import { visibleFields } from '@aletheia-dev/collection-flow';\nimport type { FlowStep } from '@aletheia-dev/core';\nimport { FieldInput } from './FieldInput.js';\n\nexport interface StepFormProps {\n step: FlowStep;\n /** Current answers, keyed by field key; drives `visibleWhen`. */\n value: Record<string, unknown>;\n onChange: (key: string, value: unknown) => void;\n /** Messages keyed by field key. */\n errors?: Record<string, string>;\n readOnly?: boolean;\n}\n\n/** Renders the currently visible fields of one flow step. */\nexport function StepForm({ step, value, onChange, errors, readOnly }: StepFormProps) {\n return (\n <>\n {visibleFields(step, value).map((field) => (\n <FieldInput\n key={field.key}\n field={field}\n value={value[field.key]}\n error={errors?.[field.key]}\n readOnly={readOnly}\n onChange={(next) => onChange(field.key, next)}\n />\n ))}\n </>\n );\n}\n","import { buildZodForStep } from '@aletheia-dev/collection-flow';\nimport type { FlowStep } from '@aletheia-dev/core';\nimport { useState } from 'react';\nimport { StepForm } from './StepForm.js';\n\nexport interface StepPreviewProps {\n step: FlowStep;\n /** Starting sample data; the preview keeps its own copy afterwards. */\n initialData?: Record<string, unknown>;\n}\n\ntype Result =\n | { kind: 'valid' }\n | { kind: 'invalid'; issues: Array<{ path: string; message: string }> }\n | { kind: 'broken'; message: string };\n\nfunction validate(step: FlowStep, data: Record<string, unknown>): Result {\n try {\n const parsed = buildZodForStep(step, data).safeParse(data);\n if (parsed.success) return { kind: 'valid' };\n return {\n kind: 'invalid',\n issues: parsed.error.issues.map((issue) => {\n const path = issue.path.map(String).join('.');\n const missing = issue.code === 'invalid_type' && data[path] === undefined;\n return { path, message: missing ? 'This field is required' : issue.message };\n }),\n };\n } catch (err) {\n // A step that cannot produce a schema (e.g. a select without options) is a definition error.\n return { kind: 'broken', message: err instanceof Error ? err.message : String(err) };\n }\n}\n\n/**\n * A step rendered as the applicant would see it, with sample data and a \"Validate\" button that\n * runs the step's validation. Needs no API: file fields render disabled outside a submission.\n */\nexport function StepPreview({ step, initialData }: StepPreviewProps) {\n const [data, setData] = useState<Record<string, unknown>>(() => ({ ...initialData }));\n const [result, setResult] = useState<Result | undefined>();\n\n const errors: Record<string, string> = {};\n if (result?.kind === 'invalid') {\n for (const issue of result.issues) {\n const key = issue.path.split('.')[0] ?? '';\n if (!(key in errors)) errors[key] = issue.message;\n }\n }\n\n return (\n <div className=\"step-preview\">\n <h3>{step.title}</h3>\n {step.description && <p className=\"muted\">{step.description}</p>}\n <form\n className=\"form\"\n onSubmit={(e) => {\n e.preventDefault();\n setResult(validate(step, data));\n }}\n >\n <StepForm\n step={step}\n value={data}\n errors={errors}\n onChange={(key, value) => {\n setData((d) => ({ ...d, [key]: value }));\n setResult(undefined);\n }}\n />\n <div className=\"actions\">\n <button type=\"submit\">Validate</button>\n </div>\n </form>\n {result?.kind === 'valid' && (\n <p className=\"preview-result ok\" role=\"status\">\n Valid: this step would pass.\n </p>\n )}\n {result?.kind === 'invalid' && (\n <div className=\"preview-result bad\" role=\"status\">\n <p>\n {result.issues.length} issue{result.issues.length === 1 ? '' : 's'}:\n </p>\n <ul>\n {result.issues.map((issue, i) => (\n <li key={`${issue.path}-${i}`}>\n <code>{issue.path || '(step)'}</code>: {issue.message}\n </li>\n ))}\n </ul>\n </div>\n )}\n {result?.kind === 'broken' && (\n <p className=\"preview-result bad\" role=\"status\">\n This step cannot be validated: {result.message}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["useContext","jsx","jsxs","useContext","Fragment","jsx","useState","jsx","jsxs","useState"]}
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Styles for @aletheia-dev/collection-flow-ui. Colours come from the host app's custom properties
|
|
3
|
+
* (--muted, --ok, --bad, --border, --panel, --text), with fallbacks so the components stand alone.
|
|
4
|
+
*/
|
|
5
|
+
.field small {
|
|
6
|
+
color: var(--muted, #6b7280);
|
|
7
|
+
}
|
|
8
|
+
.field .error {
|
|
9
|
+
color: var(--bad, #dc2626);
|
|
10
|
+
}
|
|
11
|
+
.field input[type='checkbox'] {
|
|
12
|
+
width: 1.1rem;
|
|
13
|
+
height: 1.1rem;
|
|
14
|
+
justify-self: start;
|
|
15
|
+
}
|
|
16
|
+
.file-field {
|
|
17
|
+
display: grid;
|
|
18
|
+
gap: 0.35rem;
|
|
19
|
+
}
|
|
20
|
+
.file-field progress {
|
|
21
|
+
width: 100%;
|
|
22
|
+
height: 0.5rem;
|
|
23
|
+
}
|
|
24
|
+
.file-field .file-status {
|
|
25
|
+
margin: 0;
|
|
26
|
+
font-size: 0.9rem;
|
|
27
|
+
display: flex;
|
|
28
|
+
gap: 0.5rem;
|
|
29
|
+
align-items: center;
|
|
30
|
+
flex-wrap: wrap;
|
|
31
|
+
}
|
|
32
|
+
.file-field .file-status.muted {
|
|
33
|
+
color: var(--muted, #6b7280);
|
|
34
|
+
}
|
|
35
|
+
.file-field .file-status.ok {
|
|
36
|
+
color: var(--ok, #16a34a);
|
|
37
|
+
}
|
|
38
|
+
.file-field .file-status.bad {
|
|
39
|
+
color: var(--bad, #dc2626);
|
|
40
|
+
}
|
|
41
|
+
.file-field button.inline {
|
|
42
|
+
padding: 0.2rem 0.5rem;
|
|
43
|
+
font-size: 0.85rem;
|
|
44
|
+
}
|
|
45
|
+
.step-preview .preview-result {
|
|
46
|
+
margin: 0.75rem 0 0;
|
|
47
|
+
font-size: 0.9rem;
|
|
48
|
+
}
|
|
49
|
+
.step-preview .preview-result p,
|
|
50
|
+
.step-preview .preview-result ul {
|
|
51
|
+
margin: 0.25rem 0;
|
|
52
|
+
}
|
|
53
|
+
.step-preview .preview-result.ok {
|
|
54
|
+
color: var(--ok, #16a34a);
|
|
55
|
+
}
|
|
56
|
+
.step-preview .preview-result.bad {
|
|
57
|
+
color: var(--bad, #dc2626);
|
|
58
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aletheia-dev/collection-flow-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React field inputs, step form and step preview for Aletheia collection flows, for embedding flows in your own site or back office.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/akhiljames/aletheia.git",
|
|
10
|
+
"directory": "packages/collection-flow-ui"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/akhiljames/aletheia/tree/main/packages/collection-flow-ui#readme",
|
|
13
|
+
"bugs": "https://github.com/akhiljames/aletheia/issues",
|
|
14
|
+
"sideEffects": [
|
|
15
|
+
"**/*.css"
|
|
16
|
+
],
|
|
17
|
+
"main": "./dist/index.cjs",
|
|
18
|
+
"module": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js",
|
|
24
|
+
"require": "./dist/index.cjs"
|
|
25
|
+
},
|
|
26
|
+
"./styles.css": "./dist/styles.css"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"README.md",
|
|
31
|
+
"CHANGELOG.md"
|
|
32
|
+
],
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@aletheia-dev/api-client": "0.1.0",
|
|
38
|
+
"@aletheia-dev/collection-flow": "0.1.0",
|
|
39
|
+
"@aletheia-dev/core": "0.1.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"react": "^19.3.0",
|
|
43
|
+
"react-dom": "^19.3.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@testing-library/dom": "^10.4.0",
|
|
47
|
+
"@testing-library/jest-dom": "^7.0.1",
|
|
48
|
+
"@testing-library/react": "^16.3.3",
|
|
49
|
+
"@types/node": "^22.20.4",
|
|
50
|
+
"@types/react": "^19.3.0",
|
|
51
|
+
"@types/react-dom": "^19.3.0",
|
|
52
|
+
"eslint": "^10.11.0",
|
|
53
|
+
"jsdom": "^30.1.1",
|
|
54
|
+
"react": "^19.3.0",
|
|
55
|
+
"react-dom": "^19.3.0",
|
|
56
|
+
"tsup": "^8.5.1",
|
|
57
|
+
"typescript": "5.9.3",
|
|
58
|
+
"vitest": "^5.0.3"
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "tsup",
|
|
62
|
+
"dev": "tsup --watch",
|
|
63
|
+
"lint": "eslint .",
|
|
64
|
+
"typecheck": "tsc --noEmit",
|
|
65
|
+
"test": "vitest run",
|
|
66
|
+
"clean": "rm -rf dist .turbo"
|
|
67
|
+
}
|
|
68
|
+
}
|