@peripheries/renderer 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +90 -0
  2. package/dist/catalog.js +2 -2
  3. package/dist/chunks/forms-CCAE7sCu.js +342 -0
  4. package/dist/chunks/forms-CCAE7sCu.js.map +1 -0
  5. package/dist/chunks/{query-BPuCJC1C.js → query-LnTbQghF.js} +167 -7
  6. package/dist/chunks/query-LnTbQghF.js.map +1 -0
  7. package/dist/chunks/{structure-o9pYoDjD.js → structure-BrKSgzRJ.js} +2 -2
  8. package/dist/chunks/{structure-o9pYoDjD.js.map → structure-BrKSgzRJ.js.map} +1 -1
  9. package/dist/chunks/{validate-4LgH8QMJ.js → validate-B_g7719l.js} +21 -6
  10. package/dist/chunks/validate-B_g7719l.js.map +1 -0
  11. package/dist/core.js +134 -27
  12. package/dist/core.js.map +1 -1
  13. package/dist/forms.js +2 -0
  14. package/dist/react.js +353 -5
  15. package/dist/react.js.map +1 -1
  16. package/dist/testing.js +213 -2
  17. package/dist/testing.js.map +1 -1
  18. package/dist/types/core/data-context.d.ts +5 -0
  19. package/dist/types/core/diagnostics.d.ts +19 -3
  20. package/dist/types/core/graph.d.ts +17 -1
  21. package/dist/types/core/index.d.ts +4 -3
  22. package/dist/types/core/load-page.d.ts +71 -18
  23. package/dist/types/core/query.d.ts +18 -0
  24. package/dist/types/core/release.d.ts +21 -0
  25. package/dist/types/core/sources.d.ts +9 -3
  26. package/dist/types/core/types.d.ts +2 -0
  27. package/dist/types/forms/conditions.d.ts +55 -0
  28. package/dist/types/forms/index.d.ts +3 -0
  29. package/dist/types/forms/model.d.ts +134 -0
  30. package/dist/types/forms/port.d.ts +69 -0
  31. package/dist/types/react/cell-view.d.ts +2 -0
  32. package/dist/types/react/index.d.ts +1 -0
  33. package/dist/types/react/page-renderer.d.ts +5 -0
  34. package/dist/types/react/use-forms-runtime.d.ts +201 -0
  35. package/dist/types/testing/form-workbook.d.ts +106 -0
  36. package/dist/types/testing/index.d.ts +1 -0
  37. package/package.json +6 -2
  38. package/dist/chunks/query-BPuCJC1C.js.map +0 -1
  39. package/dist/chunks/validate-4LgH8QMJ.js.map +0 -1
@@ -1,3 +1,4 @@
1
+ import { type VersionContext } from '@peripheries/runtime';
1
2
  import type { CellSource, ResolvedCell } from './cells.js';
2
3
  import type { PageRuntime } from './load-page.js';
3
4
  import type { StateOrigin } from './types.js';
@@ -42,7 +43,7 @@ export declare const STRUCTURE_FIELDS: readonly ["title", "applicable", "startsP
42
43
  * The new value (and, for a page edit, the version seen) is the caller's. `fn: null` says why the
43
44
  * value can't be edited here; the write functions still decide everything that can.
44
45
  */
45
- export type EditTarget = {
46
+ export type EditTarget = ({
46
47
  fn: 'doc_edit_content';
47
48
  graphNodeId: string;
48
49
  path: string[];
@@ -59,6 +60,9 @@ export type EditTarget = {
59
60
  } | {
60
61
  fn: 'page_edit_data';
61
62
  path: string[];
63
+ }) & {
64
+ /** The branch the edit writes (`p_version_id`, versions.md §4.3); absent on main. */
65
+ versionId?: string;
62
66
  } | {
63
67
  fn: null;
64
68
  reason: EditRefusal;
@@ -83,7 +87,9 @@ export type EditRefusal =
83
87
  * (`jsonb_set_path`). The host no longer refuses list paths itself: the functions write at a
84
88
  * path into a list (DF-SCH-9), and their dry run says whether the element exists.
85
89
  */
86
- | 'LIST_ELEMENT';
90
+ | 'LIST_ELEMENT'
91
+ /** Read at a release or a channel (or another scope's published version): releases are read-only (versions.md §5.3). */
92
+ | 'VERSION_IS_RELEASE';
87
93
  /** The cell a value is in, and how its content was resolved (ResolvedCell.source). */
88
94
  export type EditCell = {
89
95
  id: string;
@@ -99,7 +105,7 @@ export type EditCell = {
99
105
  * - everything else nowhere, with the reason.
100
106
  * Paths may run through list indices: the functions write at a path into a list (DF-SCH-9).
101
107
  */
102
- export declare function editTarget(source: PropSource, cell: EditCell): EditTarget;
108
+ export declare function editTarget(source: PropSource, cell: EditCell, version?: VersionContext): EditTarget;
103
109
  /** A value at a path, as the nested object a patch is (`['props', 'caption']` → `{ props: { caption } }`). */
104
110
  export declare function patchAt(path: readonly string[], value: unknown): unknown;
105
111
  /**
@@ -24,6 +24,8 @@ export type StateOrigin = {
24
24
  systemId: string;
25
25
  name?: string;
26
26
  stateId: string | null;
27
+ versionId?: string | null;
28
+ outOfScope?: boolean;
27
29
  };
28
30
  /**
29
31
  * Where a label's value came from (INV-DOC-6), so an edit can go back to it (design/data.md §9.5).
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Conditions on pages, sections, questions and content blocks (design/forms.md §3.1).
3
+ *
4
+ * One shape: `{ all: [{ q: '<question key>', op, value }], not?: boolean }`, operators
5
+ * in lower case only. Ported from the PWA's `lib/forms/evaluate-conditional.ts` and
6
+ * `lib/forms/normalize-conditional-logic.ts`; the semantics are theirs:
7
+ *
8
+ * - every clause must hold (AND); `not: true` negates the result;
9
+ * - absent or empty conditions show the target;
10
+ * - an unknown operator, or an error while evaluating, SHOWS the target. Hiding would
11
+ * silently remove a question from a workbook; an extra one is visible and correctable.
12
+ *
13
+ * The PWA had two stored shapes and mixed operator casing (`equals`, `not_equals`,
14
+ * `notEquals`, `is_not_empty`, `greaterThan` …). `normalizeCondition` maps all of them
15
+ * to this shape once, at migration and validation time, so the evaluator reads only
16
+ * the canonical operators.
17
+ */
18
+ export declare const CONDITION_OPS: readonly ["eq", "ne", "in", "not_in", "gt", "lt", "gte", "lte", "contains", "not_contains", "empty", "not_empty"];
19
+ export type ConditionOp = (typeof CONDITION_OPS)[number];
20
+ export interface ConditionClause {
21
+ /** The question key the clause reads from the answers. */
22
+ q: string;
23
+ /** A `ConditionOp`; anything else shows the target and is reported. */
24
+ op: ConditionOp | (string & {});
25
+ value?: unknown;
26
+ }
27
+ export interface Condition {
28
+ all: ConditionClause[];
29
+ not?: boolean;
30
+ }
31
+ export declare function isConditionOp(op: unknown): op is ConditionOp;
32
+ /**
33
+ * True when the condition passes and the target should render.
34
+ *
35
+ * @param answers Answers keyed by question key, unwrapped (`unwrapAnswerValue`).
36
+ * @param onProblem Called for an unknown operator or an evaluation error; the target
37
+ * is shown in both cases.
38
+ */
39
+ export declare function evaluateCondition(condition: Condition | null | undefined, answers: Record<string, unknown>, onProblem?: (problem: string) => void): boolean;
40
+ /** The canonical operator for a legacy spelling, or null when it is not one we know. */
41
+ export declare function normalizeOperator(op: unknown): ConditionOp | null;
42
+ /**
43
+ * Map a stored condition to the one shape.
44
+ *
45
+ * Accepts the new shape, the PWA's current `{ conditions: [{ questionId, operator, value }],
46
+ * not? }`, its legacy `[{ when, operator, is }]`, and any of them as a JSON string (the
47
+ * column is jsonb and some seeding wrote it stringified).
48
+ *
49
+ * `condition: null` means UNGATED, i.e. the target shows: the safe direction for input
50
+ * that is absent or unusable. Unusable input and dropped clauses are listed in `problems`.
51
+ */
52
+ export declare function normalizeCondition(legacy: unknown): {
53
+ condition: Condition | null;
54
+ problems: string[];
55
+ };
@@ -0,0 +1,3 @@
1
+ export { CONDITION_OPS, evaluateCondition, isConditionOp, normalizeCondition, normalizeOperator, type Condition, type ConditionClause, type ConditionOp } from './conditions.js';
2
+ export { defaultMode, enterAdvances, fromPageSteps, goTo, goToQuestion, isReadOnlyStatus, linkTo, missing, move, positionFromLink, positionsOf, progress, questionsOf, settle, stepKind, visibleSteps, type DisplayMode, type FormChapter, type FormItem, type FormItemKind, type FormLink, type FormModel, type FormQuestion, type FormQuestionDefinition, type FormStep, type Position, type StepKind, type StepProgress, type SubmissionStatus, } from './model.js';
3
+ export type { AnswerToSave, FileToUpload, FormsPort, FormsRefusal, FormsResult } from './port.js';
@@ -0,0 +1,134 @@
1
+ import { type Condition } from './conditions.js';
2
+ /**
3
+ * A form as the forms runtime reads it (forms-as-pages.md §4.2–§4.3): steps, an overlay on the
4
+ * page's flat cells, each holding items (cells) and the questions they ask. Where it comes from
5
+ * is an adapter's business: a page's `layout.steps` (`fromPageSteps`) or, until forms are pages
6
+ * (SCH M3b), the legacy definition (the host's adapter). Pure: no React, no network.
7
+ */
8
+ export type FormQuestion = {
9
+ /** The answer's key: what conditions, progress and the port name. */
10
+ key: string;
11
+ label: string;
12
+ required: boolean;
13
+ /** Enter may move on from it (forms-as-pages.md §4.3): a one-line question. */
14
+ singleLine: boolean;
15
+ when?: Condition | null;
16
+ /** The question node an answer is saved against (answer_save's p_question_node), when the form is a page. */
17
+ node?: string;
18
+ /** How to ask it, from the question node's state: what a cell renders its control from. */
19
+ definition?: FormQuestionDefinition;
20
+ };
21
+ /**
22
+ * A question's definition as its node's state holds it (forms-as-pages.md §4.2): the control's
23
+ * type, its options, validation and help. The runtime passes it through to the cell untouched.
24
+ */
25
+ export type FormQuestionDefinition = {
26
+ type: string;
27
+ options?: unknown[];
28
+ validation?: Record<string, unknown>;
29
+ description?: string;
30
+ helpText?: string;
31
+ };
32
+ export type FormItemKind = 'question' | 'content' | 'document' | 'review' | 'upload' | 'other';
33
+ /** One cell of a step: its questions, if any. */
34
+ export type FormItem = {
35
+ id: string;
36
+ kind: FormItemKind;
37
+ questions: FormQuestion[];
38
+ when?: Condition | null;
39
+ };
40
+ /** `label`: an authored word for the step shown in place of its derived kind's (advisor review, status); the kind stays derived. */
41
+ export type FormStep = {
42
+ id: string;
43
+ title: string;
44
+ chapter?: string;
45
+ label?: string;
46
+ items: FormItem[];
47
+ when?: Condition | null;
48
+ };
49
+ export type FormChapter = {
50
+ id: string;
51
+ title: string;
52
+ };
53
+ export type FormModel = {
54
+ title: string;
55
+ chapters: FormChapter[];
56
+ steps: FormStep[];
57
+ /** On submit: `warn` lists what's missing and submits; `block` refuses until it's answered (owner, Q-F7; default warn). */
58
+ submitRequired: 'warn' | 'block';
59
+ /** Back is offered (the template's `allow_back`, default true). */
60
+ allowBack: boolean;
61
+ /** The form takes a submission (false for a legacy form, which has none); default true. */
62
+ submittable?: boolean;
63
+ };
64
+ /** A step's kind, derived from its cells (§4.2): for labels and progress. */
65
+ export type StepKind = 'answer' | 'read' | 'review' | 'upload' | 'submit';
66
+ /** Where a submission stands (the `submission` layer's status, schema-g2.md §2.7). */
67
+ export type SubmissionStatus = 'in_progress' | 'submitted' | 'changes_requested' | 'completed';
68
+ export type DisplayMode = 'step' | 'question';
69
+ /** Steps, cells and questions whose conditions hold for these answers; a step left with nothing visible is skipped. */
70
+ export declare function visibleSteps(model: FormModel, values: Record<string, unknown>): FormStep[];
71
+ export declare const questionsOf: (step: FormStep) => FormQuestion[];
72
+ export declare function stepKind(step: FormStep, isLast: boolean): StepKind;
73
+ /** Where a person is: a step, and in question mode one of its questions. */
74
+ export type Position = {
75
+ step: string;
76
+ question?: string;
77
+ };
78
+ /** The places Next and Back move through: a step each, or in question mode each question (a step with none counts once). */
79
+ export declare function positionsOf(steps: FormStep[], mode: DisplayMode): Position[];
80
+ /** A position in these steps: the same, else its step's first, else the first. */
81
+ export declare function settle(steps: FormStep[], mode: DisplayMode, at: Position | null): Position | null;
82
+ /** One move on, or back; null at either end. A move across a step boundary is a step change (it pushes history). */
83
+ export declare function move(steps: FormStep[], mode: DisplayMode, at: Position, by: 1 | -1): Position | null;
84
+ /** The contents click (§4.3): that step, in question mode at its first unanswered question, else its first. */
85
+ export declare function goTo(steps: FormStep[], mode: DisplayMode, stepId: string, values: Record<string, unknown>): Position | null;
86
+ /** A question's place (§4.3 question mode, `?question=`): its step, settled for the mode. Null when no visible step has it. */
87
+ export declare function goToQuestion(steps: FormStep[], mode: DisplayMode, key: string): Position | null;
88
+ /** Enter moves on (§4.3): a step with exactly one visible one-line question, or a one-line question in question mode. */
89
+ export declare function enterAdvances(step: FormStep, mode: DisplayMode, question?: string): boolean;
90
+ /** Required questions not yet answered, in order: in one step, or (no step) in the whole form. */
91
+ export declare function missing(steps: FormStep[], values: Record<string, unknown>, stepId?: string): FormQuestion[];
92
+ export type StepProgress = {
93
+ answered: number;
94
+ total: number;
95
+ complete: boolean;
96
+ };
97
+ /**
98
+ * Progress (§4.3): overall, required answered of required visible; per step, answered of visible
99
+ * questions, complete when all its required ones are answered. A read step counts once visited.
100
+ */
101
+ export declare function progress(steps: FormStep[], values: Record<string, unknown>, visited: ReadonlySet<string>): {
102
+ answered: number;
103
+ required: number;
104
+ ratio: number;
105
+ total: {
106
+ answered: number;
107
+ count: number;
108
+ };
109
+ perStep: Record<string, StepProgress>;
110
+ };
111
+ /** Answers can't change: submitted or completed (Edit answers reopens a submitted form). */
112
+ export declare const isReadOnlyStatus: (status: SubmissionStatus | null | undefined) => status is "submitted" | "completed";
113
+ /** Step mode on desktop, one question at a time on a phone (§4.3). */
114
+ export declare const defaultMode: (phone: boolean) => DisplayMode;
115
+ /**
116
+ * Steps from a page's overlay (§4.2): `layout.steps: [{ id, title, chapter?, cell_ids, when? }]`
117
+ * and `layout.chapters`, each cell's questions from `questionsOfCell` (the page knows its
118
+ * question cells' bindings). Cells the overlay doesn't name aren't in the form.
119
+ */
120
+ export declare function fromPageSteps(page: {
121
+ name?: string | null;
122
+ layout?: Record<string, unknown> | null;
123
+ }, itemOf: (cellId: string) => FormItem | null): FormModel;
124
+ /** A deep link (§4.3, the URL contract `?mode=&page=&section=&question=`): `section` a step id, `page` a chapter id, `question` a question key. */
125
+ export type FormLink = {
126
+ mode?: 'question';
127
+ page?: string;
128
+ section?: string;
129
+ question?: string;
130
+ };
131
+ /** Where a link lands: the step it names (and question), else its chapter's first step, else the first. */
132
+ export declare function positionFromLink(steps: FormStep[], mode: DisplayMode, link: FormLink): Position | null;
133
+ /** The link to a position, as an assistant reply carries it. */
134
+ export declare function linkTo(steps: FormStep[], mode: DisplayMode, at: Position): FormLink;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * How the forms runtime saves (joint-releases.md §2.4): an injected port, so the runtime never
3
+ * names a table or a function. The legacy adapter (the host's, over form_answer_save) implements
4
+ * it in G2; answer_save, form_submit, form_visit and form_reopen (SCH M3b; schema-g2.md §2.7) in
5
+ * G3, with no renderer release. A refusal is a code, never a throw, so it's said in words like an edit's.
6
+ */
7
+ import type { FileAnswer } from '@peripheries/runtime';
8
+ /** The codes a port answers with (schema-g2.md §2.7), and NETWORK when the call didn't arrive. */
9
+ export type FormsRefusal = 'CONFLICT' | 'ANSWER_TYPE' | 'ANSWER_OPTION' | 'ANSWER_REQUIRED' | 'ANSWER_FILE_PATH' | 'ANSWER_VALIDATION' | 'FORM_INCOMPLETE' | 'FORM_NOT_OPEN' | 'FORM_COMPLETED' | 'FORM_STEP_UNKNOWN' | 'NO_CAPABILITY' | 'NETWORK' | (string & {});
10
+ export type FormsResult<T extends object = object> = ({
11
+ ok: true;
12
+ } & T) | {
13
+ ok: false;
14
+ code: FormsRefusal;
15
+ message?: string;
16
+ detail?: unknown;
17
+ };
18
+ /** A file to upload for an answer: any Blob with a name (a File in the browser). */
19
+ export type FileToUpload = Blob & {
20
+ readonly name: string;
21
+ };
22
+ /** One answer to save: its key, the question node when the form is a page, and the state it replaces. */
23
+ export type AnswerToSave = {
24
+ key: string;
25
+ node?: string;
26
+ value: unknown;
27
+ expectedStateId?: string | null;
28
+ };
29
+ export interface FormsPort {
30
+ /** Save one answer; the state it wrote is what the next save of that key expects (CONFLICT otherwise). */
31
+ save(answer: AnswerToSave): Promise<FormsResult<{
32
+ stateId?: string | null;
33
+ }>>;
34
+ /** Submit: `missing` is what the runtime found unanswered (recorded under `warn`; `block` refuses FORM_INCOMPLETE). */
35
+ submit(input: {
36
+ missing: string[];
37
+ }): Promise<FormsResult<{
38
+ status: 'submitted';
39
+ missing: string[];
40
+ }>>;
41
+ /**
42
+ * Edit answers (forms-as-pages.md §4.3): a submitted form back to `in_progress`, recorded in the
43
+ * submission. Refused once staff have completed it (FORM_COMPLETED).
44
+ */
45
+ reopen(): Promise<FormsResult<{
46
+ status: 'in_progress';
47
+ }>>;
48
+ /** A step was seen (`submission.visited_steps`): read steps count once visited. A repeat is a no-op. */
49
+ visit(stepId: string): Promise<FormsResult>;
50
+ /**
51
+ * Upload a file for a file question, as the viewer: the adapter stores it where answer_save
52
+ * accepts it and returns the answer to save (`{ v: 1, file }`); saving it is `save`'s. Absent
53
+ * where the host takes no files.
54
+ */
55
+ upload?(input: {
56
+ key: string;
57
+ node?: string;
58
+ file: FileToUpload;
59
+ }): Promise<FormsResult<{
60
+ answer: {
61
+ v: 1;
62
+ file: FileAnswer;
63
+ };
64
+ }>>;
65
+ /** A short-lived URL to open a saved file answer, as the viewer (storage decides who may). */
66
+ openFile?(file: FileAnswer): Promise<FormsResult<{
67
+ url: string;
68
+ }>>;
69
+ }
@@ -51,6 +51,8 @@ export type CellEnvironment = {
51
51
  onDiagnostic?: (d: Diagnostic) => void;
52
52
  /** Extra attributes and style for a cell's frame (RendererEnvironment.frameProps). */
53
53
  frameProps?: (cell: PageCell, contract: ComponentContract | undefined) => FrameProps | undefined;
54
+ /** Props from the page's runtime, merged last (RendererEnvironment.cellProps). */
55
+ cellProps?: (cell: PageCell) => Record<string, unknown> | undefined;
54
56
  };
55
57
  export type FrameProps = {
56
58
  attributes?: Record<string, string>;
@@ -5,3 +5,4 @@ export { StructureView, useStructureFacts } from './structure-view.js';
5
5
  export { useCellStore } from './use-cell-store.js';
6
6
  export { useContainerSize, type Size } from './use-container-size.js';
7
7
  export { cellLabels, usePaging, type LabelPaging, type LoadPage } from './use-paging.js';
8
+ export { answerInputId, FORM_SAVE_DEBOUNCE_MS, useFormsRuntime, type FormAnswer, type FormSaveState, type FormSlotProps, type FormsRuntimeOptions } from './use-forms-runtime.js';
@@ -48,6 +48,11 @@ export type RendererEnvironment = {
48
48
  retry?: () => void;
49
49
  /** Extra attributes and style for each cell's frame, e.g. a print policy the host doesn't apply yet. */
50
50
  frameProps?: (cell: PageCell, contract: ComponentContract | undefined) => FrameProps | undefined;
51
+ /**
52
+ * Props a cell gets from the page's runtime, merged over everything else: on a form page, the
53
+ * forms runtime's `form`, `answers` and `answer` (useFormsRuntime's `cellProps`).
54
+ */
55
+ cellProps?: (cell: PageCell) => Record<string, unknown> | undefined;
51
56
  };
52
57
  export type PageRendererProps = {
53
58
  runtime: PageRuntime;
@@ -0,0 +1,201 @@
1
+ import { type KeyboardEvent } from 'react';
2
+ import { type DisplayMode, type FileToUpload, type FormLink, type FormModel, type FormQuestionDefinition, type FormsPort, type FormsRefusal, type Position, type StepKind, type SubmissionStatus } from '../forms/index.js';
3
+ /** How long after the last change an answer is saved (forms-as-pages.md §4.3: 3 s). */
4
+ export declare const FORM_SAVE_DEBOUNCE_MS = 3000;
5
+ export type FormSaveState = 'idle' | 'saving' | 'saved' | 'error';
6
+ /**
7
+ * What the form's shell slots read (forms-as-pages.md §4.4; DS M1's `host:Form*` defaults and an
8
+ * org's replacements): the contents, the header, the footer, the phone app bar and step bar.
9
+ * Frozen in 0.23.0. The names an org's workbook components bind (`forms.sections`,
10
+ * `forms.progress`, `forms.goToSection`, `forms.advance`, `forms.setDisplayMode`) are these.
11
+ */
12
+ /**
13
+ * One question as a cell gets it (forms-as-pages.md §4.3; agreed with DS for 0.23.0): the same
14
+ * words as the slot props. `answers` holds only the questions its conditions show.
15
+ */
16
+ export type FormAnswer = {
17
+ /** The answer's key (`?question=`, conditions, the port). */
18
+ key: string;
19
+ label: string;
20
+ /** How to ask it, from the question node (FormQuestion.definition); null when the page gives none. */
21
+ question: FormQuestionDefinition | null;
22
+ /** As saved, or as typed while its save is pending. */
23
+ value: unknown;
24
+ /** Saves through the port, debounced and flushed on every move; a no-op while read-only. */
25
+ change(value: unknown): void;
26
+ readOnly: boolean;
27
+ required: boolean;
28
+ /** Required and unanswered, once the person has moved past its step or tried to submit. */
29
+ missing: boolean;
30
+ /** Its last refusal from the port, in words; null once a save of it succeeds. */
31
+ refusal: string | null;
32
+ /** The DOM id the runtime moves focus to (goToQuestion, Review's Edit links): unique on the page; put it on the control. */
33
+ inputId: string;
34
+ /** A file question: upload, then save the file answer. Absent where the port takes no files. */
35
+ upload?(file: FileToUpload): Promise<{
36
+ ok: true;
37
+ } | {
38
+ ok: false;
39
+ refusal: string;
40
+ }>;
41
+ /** A short-lived URL for the saved file; null when there is none or it can't be opened. */
42
+ openFile?(): Promise<string | null>;
43
+ };
44
+ /** The id a question's control carries (FormAnswer.inputId). */
45
+ export declare const answerInputId: (key: string) => string;
46
+ export type FormSlotProps = {
47
+ title: string;
48
+ chapter: {
49
+ id: string;
50
+ title: string;
51
+ } | null;
52
+ /** `label`: the step's authored word, shown in place of its derived kind's when present. */
53
+ step: {
54
+ id: string;
55
+ title: string;
56
+ index: number;
57
+ count: number;
58
+ kind: StepKind;
59
+ label?: string;
60
+ };
61
+ sections: Array<{
62
+ id: string;
63
+ title: string;
64
+ chapter?: string;
65
+ kind: StepKind;
66
+ label?: string;
67
+ complete: boolean;
68
+ answered: number;
69
+ total: number;
70
+ current: boolean;
71
+ }>;
72
+ /**
73
+ * Required answered of required (what "complete" and the bar mean; `ratio`), and `total`: every
74
+ * visible question, answered of all ("X of Y questions answered").
75
+ */
76
+ progress: {
77
+ answered: number;
78
+ required: number;
79
+ ratio: number;
80
+ total: {
81
+ answered: number;
82
+ count: number;
83
+ };
84
+ };
85
+ /**
86
+ * In question mode, the question shown: its place in the step, its key (`?question=`), and the
87
+ * next question in the step ("Next question"). Null in step mode, or on a step without questions.
88
+ */
89
+ question: {
90
+ index: number;
91
+ count: number;
92
+ key: string;
93
+ next: {
94
+ label: string;
95
+ } | null; /** The question shown, as a cell gets it. */
96
+ answer?: FormAnswer;
97
+ } | null;
98
+ /** Go to a question by key: its step, and in question mode the question (review's Edit links, deep links). */
99
+ goToQuestion(key: string): void;
100
+ displayMode: DisplayMode;
101
+ setDisplayMode(mode: DisplayMode): void;
102
+ goToSection(stepId: string): void;
103
+ advance(): void;
104
+ back(): void;
105
+ canBack: boolean;
106
+ next: {
107
+ title: string;
108
+ } | null;
109
+ /** The last step's action; absent elsewhere. `blocked` under `submit.required: 'block'` while anything is missing. */
110
+ submit?: {
111
+ label: string;
112
+ run(): void;
113
+ blocked: boolean;
114
+ } | null;
115
+ /** Edit answers, on a submitted form (forms-as-pages.md §4.3); absent otherwise. */
116
+ reopen?: {
117
+ label: string;
118
+ run(): void;
119
+ } | null;
120
+ save: {
121
+ state: FormSaveState;
122
+ at?: string;
123
+ error?: string;
124
+ };
125
+ /** Required questions still to answer here (warn while moving, never block). */
126
+ missing: Array<{
127
+ key: string;
128
+ label: string;
129
+ }>;
130
+ /** Every chapter, in order, so a nav can group its rows under each (`sections[].chapter` names one). */
131
+ chapters?: Array<{
132
+ id: string;
133
+ title: string;
134
+ }>;
135
+ /** The submission's status (null for a form that takes none), and whether answers can be changed now. */
136
+ status?: SubmissionStatus | null;
137
+ readOnly?: boolean;
138
+ /** The newest staff review (form_review): what they asked for, or that they completed it. Null when none. */
139
+ review?: {
140
+ status: SubmissionStatus;
141
+ note: string;
142
+ at: string;
143
+ by: string | null;
144
+ } | null;
145
+ };
146
+ export type FormsRuntimeOptions = {
147
+ model: FormModel;
148
+ port: FormsPort;
149
+ /** Answers as loaded, and the state each was read at (what the next save expects). */
150
+ initial: Record<string, {
151
+ value: unknown;
152
+ stateId?: string | null;
153
+ }>;
154
+ status: SubmissionStatus | null;
155
+ visited?: string[];
156
+ /** The deep link the route was opened with. */
157
+ link?: FormLink;
158
+ /** A phone opens in question mode. */
159
+ phone?: boolean;
160
+ /** Tell the route where the person is: a step change pushes history, a move within a step replaces it. */
161
+ onNavigate?(link: FormLink, how: {
162
+ replace: boolean;
163
+ }): void;
164
+ /** A refusal's words (the host's text for a code). */
165
+ describe?(code: FormsRefusal): string;
166
+ /**
167
+ * The submission's staff notes as loaded (`SubmissionData.notes`), `by` as the host can name the
168
+ * writer to this viewer (else null): the newest review is the `review` slot.
169
+ */
170
+ notes?: Array<{
171
+ at: string;
172
+ text: string;
173
+ status?: SubmissionStatus;
174
+ by?: string | null;
175
+ }>;
176
+ };
177
+ /**
178
+ * The forms runtime (forms-as-pages.md §4.3) for a form page: modes, Next / Back, the contents
179
+ * click, Enter, required (warn while moving; warn or block on submit), history, scroll and focus,
180
+ * saving through the port (debounced per question, flushed on every move, on submit, on
181
+ * `pagehide` and on unmount, in order, retried with the next change or flush), progress (read
182
+ * steps once visited), read-only states and deep links. The route renders the cells; the shell's
183
+ * slots render `slots`.
184
+ */
185
+ export declare function useFormsRuntime(o: FormsRuntimeOptions): {
186
+ slots: FormSlotProps | null;
187
+ cellProps: (cell: {
188
+ id: string;
189
+ }) => Record<string, unknown> | undefined;
190
+ steps: import("../forms/index.js").FormStep[];
191
+ step: import("../forms/index.js").FormStep;
192
+ position: Position | null;
193
+ values: Record<string, unknown>;
194
+ change: (key: string, value: unknown) => void;
195
+ flush: () => Promise<void>;
196
+ readOnly: boolean;
197
+ status: SubmissionStatus | null;
198
+ headingRef: import("react").RefObject<HTMLElement | null>;
199
+ contentRef: import("react").RefObject<HTMLElement | null>;
200
+ onKeyDown: (e: KeyboardEvent) => void;
201
+ };
@@ -0,0 +1,106 @@
1
+ import { type FormItem, type FormModel } from '../forms/index.js';
2
+ /** The fixture page, as `project_page` holds it. */
3
+ export declare const formWorkbookPage: {
4
+ id: string;
5
+ name: string;
6
+ org_id: string;
7
+ project_id: string;
8
+ process_id: null;
9
+ graph_node_id: null;
10
+ page_type: string;
11
+ status: string;
12
+ is_template: boolean;
13
+ metadata: null;
14
+ content: {
15
+ cells: ({
16
+ id: string;
17
+ component: string;
18
+ props: {
19
+ blocks: {
20
+ text: string;
21
+ }[];
22
+ question?: undefined;
23
+ };
24
+ when?: undefined;
25
+ } | {
26
+ id: string;
27
+ component: string;
28
+ props: {
29
+ question: {
30
+ key: string;
31
+ label: string;
32
+ type: string;
33
+ required: boolean;
34
+ options?: undefined;
35
+ };
36
+ blocks?: undefined;
37
+ };
38
+ when?: undefined;
39
+ } | {
40
+ id: string;
41
+ component: string;
42
+ props: {
43
+ question: {
44
+ key: string;
45
+ label: string;
46
+ type: string;
47
+ required: boolean;
48
+ options: {
49
+ value: string;
50
+ label: string;
51
+ }[];
52
+ };
53
+ blocks?: undefined;
54
+ };
55
+ when?: undefined;
56
+ } | {
57
+ id: string;
58
+ component: string;
59
+ props: {
60
+ question: {
61
+ key: string;
62
+ label: string;
63
+ type: string;
64
+ required: boolean;
65
+ options?: undefined;
66
+ };
67
+ blocks?: undefined;
68
+ };
69
+ when: {
70
+ all: {
71
+ q: string;
72
+ op: string;
73
+ value: string;
74
+ }[];
75
+ };
76
+ } | {
77
+ id: string;
78
+ component: string;
79
+ props: {
80
+ blocks?: undefined;
81
+ question?: undefined;
82
+ };
83
+ when?: undefined;
84
+ })[];
85
+ };
86
+ layout: {
87
+ type: string;
88
+ chapters: {
89
+ id: string;
90
+ title: string;
91
+ }[];
92
+ steps: {
93
+ id: string;
94
+ title: string;
95
+ chapter: string;
96
+ cell_ids: string[];
97
+ }[];
98
+ submit: {
99
+ required: string;
100
+ };
101
+ };
102
+ };
103
+ /** A cell's place in the form: its question, if it asks one. */
104
+ export declare function formWorkbookItem(cellId: string): FormItem | null;
105
+ /** The fixture as the forms runtime reads it. */
106
+ export declare const formWorkbookModel: () => FormModel;
@@ -3,3 +3,4 @@ export * as fixtureComponents from './components.js';
3
3
  export { dashboardPage, documentPage, GOLDEN_PAGES, mixedPage, thinRefPage, walkPage, type GoldenBundle, type GoldenPage } from './golden.js';
4
4
  export { ADVISOR_ORG, CLIENT_ORG, FIXTURE_EMAIL_DOMAIN, FIXTURE_ID, FIXTURE_ORG_SLUGS, fixtureId } from './ids.js';
5
5
  export { goldenBundleRow, goldenBundles, goldenGraphSource, goldenScope, type SourceCall } from './sources.js';
6
+ export { formWorkbookItem, formWorkbookModel, formWorkbookPage } from './form-workbook.js';