@aiaiai-pt/design-system 0.33.0 → 0.35.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.
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Block / binding model for the portal rendering engine (#73, spec §5).
3
+ *
4
+ * A page is an ordered list of **blocks**; each block is a declarative
5
+ * *pointer*, never baked data. The block names a widget intent (`type`,
6
+ * authoring hint) and a `binding` describing WHAT to render (a list of entity
7
+ * Z, the detail of row Y, an action form, …). Data is fetched at render time
8
+ * by `resolveData(block)` (see `./resolve-data.ts`) and handed to the widget
9
+ * the dispatcher selects (see `./dispatch.ts`). Keeping only the pointer is
10
+ * what makes this a template, not a page.
11
+ *
12
+ * The ontology `portal_page` rows that emit these descriptors land in #79;
13
+ * here the descriptors are SEEDED (same staged pattern #70/#72 used).
14
+ */
15
+ /** The bounded, governed widget kinds (spec §5 catalog). */
16
+ export type WidgetKind = "list" | "detail" | "form" | "map" | "calendar" | "kpi" | "content" | "vote" | "status" | "forms" | "hero" | "lookup" | "feed" | "filters" | "actions" | "aggregate" | "subscriptions" | "consent" | "deliveries" | "owned-list" | "feature-grid" | "cta" | "media-text" | "steps" | "testimonial" | "faq" | "logo-strip" | "media-gallery";
17
+ /** The feed sub-views a `feed`/`filters` binding may toggle/scope (#308). */
18
+ export type FeedView = "list" | "map";
19
+ /** Declarative pointer — "I render <kind> of <entity|action>, scoped by filter". */
20
+ export interface Binding {
21
+ kind: WidgetKind;
22
+ /** Ontology entity type (list/detail/vote). */
23
+ entity?: string | null;
24
+ /** Ontology action type (form). */
25
+ action?: string | null;
26
+ /**
27
+ * Render-time selector / scope. For the single-resource kinds
28
+ * (detail/status/content) this is the captured route segment (id/ref/key).
29
+ * For list/map/calendar it is the (future) scope expression. Real
30
+ * `{{tenant.scope}}` expression evaluation lands with the ontology
31
+ * descriptors (#79); today it is a literal.
32
+ */
33
+ filter?: string | null;
34
+ /**
35
+ * `list` only (#75 M5): page-size cap + ordering, forwarded as
36
+ * `?limit=` / `?order_by=` (the BFF shape-sanitises order terms and drops
37
+ * anything malformed). The landing's recent-feed sets `limit: 6,
38
+ * order: "-created_at"`.
39
+ */
40
+ limit?: number | null;
41
+ order?: string | null;
42
+ /**
43
+ * `detail` only (#75 M5 slice 3): child collections to embed, forwarded as
44
+ * `?expand=` (the BFF resolves each through its own disclosure policy row
45
+ * — an undisclosed child is silently absent). Each aside block requests
46
+ * only what it renders (gallery → ["images"]).
47
+ */
48
+ expand?: string[] | null;
49
+ }
50
+ /**
51
+ * Fail-closed blast radius (§14.8). A `structural` slot must NOT silently
52
+ * vanish on misconfig (it surfaces a visible error); an `optional` widget
53
+ * fails soft to empty. The page template (#74) sets this from the slot
54
+ * definition; until then a block declares its own, defaulting to `optional`.
55
+ */
56
+ export type BlockImportance = "optional" | "structural";
57
+ /** Per-block structural lock override (#74 §2.1, Gutenberg per-block lock). */
58
+ export interface BlockLock {
59
+ move?: boolean;
60
+ remove?: boolean;
61
+ }
62
+ export interface Block {
63
+ /**
64
+ * Operator-authored widget hint. UNTRUSTED — it is NEVER interpolated into
65
+ * the DOM (TH-08). The dispatcher resolves a known-good registry `key` from
66
+ * the `binding` (+ a tester that MAY rank on this `type`); that literal is the
67
+ * only widget identifier that reaches `class`/`data-*`.
68
+ */
69
+ type: string;
70
+ /** Placement into the page-template slot (#74). */
71
+ slot: string;
72
+ /**
73
+ * Stable identifier within the slot (#74 §2.2). The override-map key — a
74
+ * `portal_page` (#79) addresses this block by `slot.name → block.name`.
75
+ * Stamped from the template's default-block `name`.
76
+ */
77
+ name?: string;
78
+ binding: Binding;
79
+ props?: Record<string, unknown>;
80
+ importance?: BlockImportance;
81
+ /**
82
+ * Single-H1 ownership (ARTE #2). Stamped from the owning slot — the widget
83
+ * renders the page's only `<h1>` here; every other heading is `<h2>`+.
84
+ */
85
+ ownsH1?: boolean;
86
+ /** Reserved error-summary slot (ARTE #5). Stamped from the slot. */
87
+ errorSummary?: boolean;
88
+ /** Per-block structural lock override (#74 §2.1). */
89
+ lock?: BlockLock;
90
+ /**
91
+ * Full-bleed band (#105 Phase 7 composition). A `bleed` block is an
92
+ * edge-to-edge, self-padding coloured band (a Hero / accent CallToAction). It
93
+ * keeps the template's vertical RHYTHM gap to/from a CONTENT band; the gap
94
+ * only collapses BETWEEN two adjacent bleed bands, so consecutive colour
95
+ * fields butt instead of leaving an awkward sliver. A template-author concern
96
+ * (set on the default block), not tenant content.
97
+ */
98
+ bleed?: boolean;
99
+ }
100
+ export interface PageDescriptor {
101
+ blocks: Block[];
102
+ }
103
+ /**
104
+ * Minimal ontology schema shape a tester / widget may consult. Widened in 73d
105
+ * when EntityList/DetailView read fields from the schema (no hardcoded
106
+ * entity-type unions — §14.6).
107
+ */
108
+ export interface OntologySchema {
109
+ entity: string;
110
+ fields: Array<{
111
+ key: string;
112
+ type?: string;
113
+ [extra: string]: unknown;
114
+ }>;
115
+ [extra: string]: unknown;
116
+ }
117
+ /**
118
+ * The uniform prop contract every catalog widget receives. `resolveData`
119
+ * produces `{ data, schema, actionDef }`; the page passes the block's `props`
120
+ * through verbatim (operator config — columns, labels, …). This single shape
121
+ * is what lets one dispatcher mount any widget, and what each DS-backed widget
122
+ * wrapper adapts to its component's props.
123
+ */
124
+ export interface WidgetProps {
125
+ data: unknown;
126
+ schema: OntologySchema | null;
127
+ actionDef: unknown | null;
128
+ props: Record<string, unknown>;
129
+ /**
130
+ * The vertical app segment (`occurrences`, …), threaded from the loader so a
131
+ * widget that POSTs back to the public surface (ActionForm → /{app}/public/
132
+ * submit, 73f) can build its path. Optional — read-only widgets ignore it.
133
+ */
134
+ app?: string;
135
+ /**
136
+ * Single-H1 ownership (ARTE #2), stamped from the owning slot via the
137
+ * block. A heading-rendering widget emits the page's `<h1>` only when this
138
+ * is true — otherwise `<h2>` — so composed pages (landing: hero + recent
139
+ * list) keep exactly one H1. Hardcoded routes that mount a widget
140
+ * standalone (my-reports) pass it explicitly.
141
+ */
142
+ ownsH1?: boolean;
143
+ /**
144
+ * The request's resolved locale (prefs cookie → portal default) — drives
145
+ * value FORMATTING only (dates). Copy localisation happens upstream
146
+ * (wuchale for code strings, $loc for page-row props).
147
+ */
148
+ locale?: string;
149
+ /**
150
+ * BFF path the block's data came from (#75 M5 slice 4) — paging widgets
151
+ * re-fetch it client-side with `?after={next_cursor}` through the same
152
+ * /bff proxy (graduated principal rides along for free).
153
+ */
154
+ dataPath?: string;
155
+ /**
156
+ * The request "now" as an ISO string, stamped once by the route loader
157
+ * (`new Date()`) — the SAME clock that feeds `resolveData` (calendar window).
158
+ * Threaded so time-relative widgets (PhaseTimeline open/closed/upcoming) are
159
+ * computed SSR-side and hydrate stable, instead of reading the browser clock
160
+ * (which would drift from SSR and corrupt hydration). Generic, not
161
+ * vertical-specific.
162
+ */
163
+ now?: string;
164
+ }
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Block / binding model for the portal rendering engine (#73, spec §5).
3
+ *
4
+ * A page is an ordered list of **blocks**; each block is a declarative
5
+ * *pointer*, never baked data. The block names a widget intent (`type`,
6
+ * authoring hint) and a `binding` describing WHAT to render (a list of entity
7
+ * Z, the detail of row Y, an action form, …). Data is fetched at render time
8
+ * by `resolveData(block)` (see `./resolve-data.ts`) and handed to the widget
9
+ * the dispatcher selects (see `./dispatch.ts`). Keeping only the pointer is
10
+ * what makes this a template, not a page.
11
+ *
12
+ * The ontology `portal_page` rows that emit these descriptors land in #79;
13
+ * here the descriptors are SEEDED (same staged pattern #70/#72 used).
14
+ */
15
+
16
+ /** The bounded, governed widget kinds (spec §5 catalog). */
17
+ export type WidgetKind =
18
+ | "list"
19
+ | "detail"
20
+ | "form"
21
+ | "map"
22
+ | "calendar"
23
+ | "kpi"
24
+ | "content"
25
+ | "vote"
26
+ | "status"
27
+ | "forms"
28
+ | "hero"
29
+ | "lookup"
30
+ // #105 Phase 1 (#308). A `feed` block pairs EntityList + MapView over one
31
+ // entity feed and toggles between them via the URL (`?view=`); resolveData
32
+ // resolves the ACTIVE view's underlying feed. A `filters` block renders the
33
+ // declared facets of a feed and writes the active filter state to the URL —
34
+ // the feed re-resolves server-side, so list + map share one filter channel.
35
+ | "feed"
36
+ | "filters"
37
+ // #105 Phase 2 (#313). An `actions` block resolves a detail row's CITIZEN
38
+ // ACTION availability — `/{app}/public/{entity}/{id}/actions` projects, per
39
+ // citizen-write placement, whether the action is OPEN on this row (the same
40
+ // `gate` criteria the submit enforces). SSR-resolved (no client fetch); the
41
+ // ActionPanel widget joins it with the operator's declared CTAs to render
42
+ // open links vs disabled+reason. Distinct from `detail` because it consumes
43
+ // the availability projection, not the row.
44
+ | "actions"
45
+ // #105 Phase 4. An `aggregate` block reads the ROWS of a public aggregate
46
+ // VIEW (`/{app}/public/{view}`, the #250 M3 public-view lane: self-gated by
47
+ // `group_by tenant_id`, no entity ACL) as PRIMARY data — distinct from a
48
+ // `filters` block reading a view as facet *options*. RankingBoard /
49
+ // ResultsChart consume the `{label, value}` projection of the rows. The view
50
+ // carries no per-type schema, so this never resolves a `/public/schema` path.
51
+ | "aggregate"
52
+ // #105 Phase 5. A `subscriptions` block reads the signed-in citizen's OWN
53
+ // notification subscriptions through the AUTHED account proxy
54
+ // (`/me/notifications/subscriptions`, require_auth + OWN-scoped) — NOT the
55
+ // public `/bff` surface. NotificationPrefs renders them as a toggle list and
56
+ // mutates them (POST/PATCH) through the same proxy. The first MUTATING widget
57
+ // kind: it carries no schema and its data path is the account lane, so
58
+ // resolveData handles it in a dedicated branch (never hits `bffPathForBinding`).
59
+ | "subscriptions"
60
+ // #105 Phase 5 slice 2. A `consent` block reads the signed-in citizen's OWN
61
+ // RGPD state through the AUTHED account proxy (`/me/consent`, require_auth +
62
+ // OWN-scoped): the tenant's active consent purposes joined with the caller's
63
+ // consent records. ConsentManager renders the toggle list and acts on it —
64
+ // GRANT reuses the public submit lane (the existing `grant_consent` action),
65
+ // REVOKE/ERASE/EXPORT ride the same `/me/consent` proxy. Like `subscriptions`
66
+ // it carries no schema; resolveData handles it in a dedicated branch.
67
+ | "consent"
68
+ // #252 Anexo 110 — a `deliveries` block reads the signed-in citizen's OWN
69
+ // notification HISTORY through the AUTHED account proxy
70
+ // (`/me/notifications/deliveries`, require_auth + OWN-scoped → recipient_user_id
71
+ // = caller sub): the `notification_delivery` ledger rows written AFTER each
72
+ // dispatch. Read-only (the citizen consults their history; nothing to mutate).
73
+ // Like `subscriptions`/`consent` it carries no schema; resolveData handles it
74
+ // in a dedicated branch.
75
+ | "deliveries"
76
+ // #105 — an `owned-list` block reads the signed-in citizen's OWN rows of an
77
+ // entity through the AUTHED account proxy (`/me/owned/{entity}`, require_auth +
78
+ // OWN-scoped). Unlike the my-reports public-list filter (which only works when
79
+ // the owner field STORES the sub), this handles a USER-ENTITY-REL owner
80
+ // (proposal.proponent) by resolving the caller's identity server-side. Reuses
81
+ // EntityListWidget (the data is `{items}` like a list); resolveData fetches the
82
+ // OWN lane + the public schema (for columns) in a dedicated branch. Auth-gated.
83
+ | "owned-list"
84
+ // #105 Phase 7 — the landing/content surface: presentational, marketing-grade
85
+ // section widgets that render PURELY from their block props (copy + media are
86
+ // tenant DATA). They have NO data path — resolveData short-circuits them all
87
+ // through the shared PRESENTATIONAL_KINDS branch (like `hero`/`lookup`), so a
88
+ // misconfigured one renders nothing rather than fetching. Each is its own
89
+ // kind (the `hero` precedent) so the registry dispatches the right widget;
90
+ // they drop into any vertical because they carry no entity/schema coupling.
91
+ | "feature-grid"
92
+ | "cta"
93
+ | "media-text"
94
+ | "steps"
95
+ | "testimonial"
96
+ | "faq"
97
+ | "logo-strip"
98
+ | "media-gallery";
99
+
100
+ /** The feed sub-views a `feed`/`filters` binding may toggle/scope (#308). */
101
+ export type FeedView = "list" | "map";
102
+
103
+ /** Declarative pointer — "I render <kind> of <entity|action>, scoped by filter". */
104
+ export interface Binding {
105
+ kind: WidgetKind;
106
+ /** Ontology entity type (list/detail/vote). */
107
+ entity?: string | null;
108
+ /** Ontology action type (form). */
109
+ action?: string | null;
110
+ /**
111
+ * Render-time selector / scope. For the single-resource kinds
112
+ * (detail/status/content) this is the captured route segment (id/ref/key).
113
+ * For list/map/calendar it is the (future) scope expression. Real
114
+ * `{{tenant.scope}}` expression evaluation lands with the ontology
115
+ * descriptors (#79); today it is a literal.
116
+ */
117
+ filter?: string | null;
118
+ /**
119
+ * `list` only (#75 M5): page-size cap + ordering, forwarded as
120
+ * `?limit=` / `?order_by=` (the BFF shape-sanitises order terms and drops
121
+ * anything malformed). The landing's recent-feed sets `limit: 6,
122
+ * order: "-created_at"`.
123
+ */
124
+ limit?: number | null;
125
+ order?: string | null;
126
+ /**
127
+ * `detail` only (#75 M5 slice 3): child collections to embed, forwarded as
128
+ * `?expand=` (the BFF resolves each through its own disclosure policy row
129
+ * — an undisclosed child is silently absent). Each aside block requests
130
+ * only what it renders (gallery → ["images"]).
131
+ */
132
+ expand?: string[] | null;
133
+ }
134
+
135
+ /**
136
+ * Fail-closed blast radius (§14.8). A `structural` slot must NOT silently
137
+ * vanish on misconfig (it surfaces a visible error); an `optional` widget
138
+ * fails soft to empty. The page template (#74) sets this from the slot
139
+ * definition; until then a block declares its own, defaulting to `optional`.
140
+ */
141
+ export type BlockImportance = "optional" | "structural";
142
+
143
+ /** Per-block structural lock override (#74 §2.1, Gutenberg per-block lock). */
144
+ export interface BlockLock {
145
+ move?: boolean;
146
+ remove?: boolean;
147
+ }
148
+
149
+ export interface Block {
150
+ /**
151
+ * Operator-authored widget hint. UNTRUSTED — it is NEVER interpolated into
152
+ * the DOM (TH-08). The dispatcher resolves a known-good registry `key` from
153
+ * the `binding` (+ a tester that MAY rank on this `type`); that literal is the
154
+ * only widget identifier that reaches `class`/`data-*`.
155
+ */
156
+ type: string;
157
+ /** Placement into the page-template slot (#74). */
158
+ slot: string;
159
+ /**
160
+ * Stable identifier within the slot (#74 §2.2). The override-map key — a
161
+ * `portal_page` (#79) addresses this block by `slot.name → block.name`.
162
+ * Stamped from the template's default-block `name`.
163
+ */
164
+ name?: string;
165
+ binding: Binding;
166
+ props?: Record<string, unknown>;
167
+ importance?: BlockImportance;
168
+ /**
169
+ * Single-H1 ownership (ARTE #2). Stamped from the owning slot — the widget
170
+ * renders the page's only `<h1>` here; every other heading is `<h2>`+.
171
+ */
172
+ ownsH1?: boolean;
173
+ /** Reserved error-summary slot (ARTE #5). Stamped from the slot. */
174
+ errorSummary?: boolean;
175
+ /** Per-block structural lock override (#74 §2.1). */
176
+ lock?: BlockLock;
177
+ /**
178
+ * Full-bleed band (#105 Phase 7 composition). A `bleed` block is an
179
+ * edge-to-edge, self-padding coloured band (a Hero / accent CallToAction). It
180
+ * keeps the template's vertical RHYTHM gap to/from a CONTENT band; the gap
181
+ * only collapses BETWEEN two adjacent bleed bands, so consecutive colour
182
+ * fields butt instead of leaving an awkward sliver. A template-author concern
183
+ * (set on the default block), not tenant content.
184
+ */
185
+ bleed?: boolean;
186
+ }
187
+
188
+ export interface PageDescriptor {
189
+ blocks: Block[];
190
+ }
191
+
192
+ /**
193
+ * Minimal ontology schema shape a tester / widget may consult. Widened in 73d
194
+ * when EntityList/DetailView read fields from the schema (no hardcoded
195
+ * entity-type unions — §14.6).
196
+ */
197
+ export interface OntologySchema {
198
+ entity: string;
199
+ fields: Array<{ key: string; type?: string; [extra: string]: unknown }>;
200
+ [extra: string]: unknown;
201
+ }
202
+
203
+ /**
204
+ * The uniform prop contract every catalog widget receives. `resolveData`
205
+ * produces `{ data, schema, actionDef }`; the page passes the block's `props`
206
+ * through verbatim (operator config — columns, labels, …). This single shape
207
+ * is what lets one dispatcher mount any widget, and what each DS-backed widget
208
+ * wrapper adapts to its component's props.
209
+ */
210
+ export interface WidgetProps {
211
+ data: unknown;
212
+ schema: OntologySchema | null;
213
+ actionDef: unknown | null;
214
+ props: Record<string, unknown>;
215
+ /**
216
+ * The vertical app segment (`occurrences`, …), threaded from the loader so a
217
+ * widget that POSTs back to the public surface (ActionForm → /{app}/public/
218
+ * submit, 73f) can build its path. Optional — read-only widgets ignore it.
219
+ */
220
+ app?: string;
221
+ /**
222
+ * Single-H1 ownership (ARTE #2), stamped from the owning slot via the
223
+ * block. A heading-rendering widget emits the page's `<h1>` only when this
224
+ * is true — otherwise `<h2>` — so composed pages (landing: hero + recent
225
+ * list) keep exactly one H1. Hardcoded routes that mount a widget
226
+ * standalone (my-reports) pass it explicitly.
227
+ */
228
+ ownsH1?: boolean;
229
+ /**
230
+ * The request's resolved locale (prefs cookie → portal default) — drives
231
+ * value FORMATTING only (dates). Copy localisation happens upstream
232
+ * (wuchale for code strings, $loc for page-row props).
233
+ */
234
+ locale?: string;
235
+ /**
236
+ * BFF path the block's data came from (#75 M5 slice 4) — paging widgets
237
+ * re-fetch it client-side with `?after={next_cursor}` through the same
238
+ * /bff proxy (graduated principal rides along for free).
239
+ */
240
+ dataPath?: string;
241
+ /**
242
+ * The request "now" as an ISO string, stamped once by the route loader
243
+ * (`new Date()`) — the SAME clock that feeds `resolveData` (calendar window).
244
+ * Threaded so time-relative widgets (PhaseTimeline open/closed/upcoming) are
245
+ * computed SSR-side and hydrate stable, instead of reading the browser clock
246
+ * (which would drift from SSR and corrupt hydration). Generic, not
247
+ * vertical-specific.
248
+ */
249
+ now?: string;
250
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Voting-ballot helpers (#105) — pure read-path builders + ballot mapping + the
3
+ * public-submit cast, kept OUT of the `.svelte` widget so (a) the HTTP verbs and
4
+ * header literals aren't mangled by Wuchale's `.svelte` string extraction, and
5
+ * (b) the shaping/cast is unit-testable without mounting a component.
6
+ *
7
+ * The ballot COMPOSES two existing PUBLIC reads (no new backend): the edition
8
+ * row — its `current_phase` decides open/closed — and the votable-entity list
9
+ * (the finalists, as radio options). The cast reuses the citizen public-submit
10
+ * lane (the vote action's placement) — the SAME auth-optional `/bff` surface
11
+ * `submit_proposal` uses, so the signed-in citizen's Bearer rides along and the
12
+ * server stamps the one-vote pseudonym (anonymous is refused server-side).
13
+ *
14
+ * Vertical-agnostic: every entity / field / status / phase literal is block
15
+ * DATA (the civic page supplies proposal / proposal_edition / in_voting /
16
+ * voting / title), so the widget drops into any vertical with a vote action.
17
+ */
18
+ /** Path-only read fetch (the resolve-data read shape). */
19
+ export type ReadFetch = (path: string) => Promise<{
20
+ ok: boolean;
21
+ status: number;
22
+ json: () => Promise<unknown>;
23
+ }>;
24
+ /** Write fetch for the cast POST (the browser `fetch` fits). */
25
+ export type WriteFetch = (path: string, init: {
26
+ method: string;
27
+ headers: Record<string, string>;
28
+ body: string;
29
+ }) => Promise<{
30
+ ok: boolean;
31
+ status: number;
32
+ }>;
33
+ export interface VoteOption {
34
+ id: string;
35
+ label: string;
36
+ }
37
+ /** The resolved ballot a `vote` block hands the widget. */
38
+ export interface Ballot {
39
+ options: VoteOption[];
40
+ /** Voting is NOT open on this edition (current_phase ≠ the open phase). */
41
+ closed: boolean;
42
+ /** The edition id the cast writes back (carried so the widget needn't re-derive). */
43
+ edition: string;
44
+ }
45
+ /**
46
+ * How to SOURCE the ballot from the public surface — all DATA (the votable
47
+ * entity type comes from the binding; the rest from props), so no civic literal
48
+ * lives in code. Generic facet/field names default to the platform conventions.
49
+ */
50
+ export interface VoteReadConfig {
51
+ /** Votable entity type (binding.entity), e.g. `proposal`. */
52
+ entity: string;
53
+ /** Edition entity type, e.g. `proposal_edition`. */
54
+ editionEntity: string;
55
+ /** Status value marking a votable option, e.g. `in_voting`. */
56
+ optionStatus: string;
57
+ /** Facet param carrying the status filter (default `status`). */
58
+ statusFacet: string;
59
+ /** Facet param carrying the edition id (default `edition.id`). */
60
+ editionFacet: string;
61
+ /** Option label field on the entity row (default `title`). */
62
+ labelField: string;
63
+ /** Edition phase field (default `current_phase`). */
64
+ phaseField: string;
65
+ /** Phase value that means voting is OPEN, e.g. `voting`. */
66
+ openPhase: string;
67
+ /** Max options to fetch (default 50). */
68
+ limit: number;
69
+ }
70
+ /** Where the cast POSTs — the placement + the vote entity's field names. DATA. */
71
+ export interface VoteCastConfig {
72
+ app: string;
73
+ /** Citizen public-submit placement key, e.g. `civic-cast-vote`. */
74
+ placementKey: string;
75
+ /** Envelope key = the created entity type, e.g. `proposal_vote`. */
76
+ voteModel: string;
77
+ /** Field on the vote entity pointing at the chosen option, e.g. `proposal`. */
78
+ voteField: string;
79
+ /** Field on the vote entity pointing at the edition, e.g. `edition`. */
80
+ editionField: string;
81
+ }
82
+ /**
83
+ * Build the read config from the block (entity on the binding, the rest in
84
+ * props). Returns null when a REQUIRED knob is missing — resolveData then fails
85
+ * the block closed rather than fetch a half-configured ballot.
86
+ */
87
+ export declare function voteReadConfig(entity: string | null | undefined, props: Record<string, unknown> | undefined): VoteReadConfig | null;
88
+ /** Build the cast config from props + the widget's app. Null if misconfigured. */
89
+ export declare function voteCastConfig(app: string | undefined, props: Record<string, unknown> | undefined): VoteCastConfig | null;
90
+ /** The edition read — its `current_phase` decides open/closed. */
91
+ export declare function editionReadPath(app: string, cfg: VoteReadConfig, editionId: string): string;
92
+ /** The votable-options read — the finalists for this edition, by status facet. */
93
+ export declare function optionsReadPath(app: string, cfg: VoteReadConfig, editionId: string): string;
94
+ /** Map the votable-entity list into ballot options (id + a non-empty label). */
95
+ export declare function toOptions(proposalsBody: unknown, cfg: VoteReadConfig): VoteOption[];
96
+ /** Voting is closed unless the edition's phase equals the configured open phase
97
+ * (an absent/null phase reads as CLOSED — fail-closed, matching the gate). */
98
+ export declare function isClosed(editionRow: unknown, cfg: VoteReadConfig): boolean;
99
+ /** The cast body: `{ <voteModel>: { <voteField>: choice, <editionField>: edition } }`. */
100
+ export declare function castBody(cfg: VoteCastConfig, choiceId: string, editionId: string): Record<string, unknown>;
101
+ export type CastOutcome = {
102
+ kind: "ok";
103
+ }
104
+ /** One-vote-per-edition already used (409) — a settled state, not an error. */
105
+ | {
106
+ kind: "conflict";
107
+ }
108
+ /** Identity missing/insufficient (401/403) — the cast needs a signed-in citizen. */
109
+ | {
110
+ kind: "auth";
111
+ } | {
112
+ kind: "error";
113
+ status: number;
114
+ };
115
+ /**
116
+ * Cast the vote through the citizen public-submit lane. The signed-in citizen's
117
+ * Bearer is forwarded by the same-origin `/bff` proxy, so the server stamps the
118
+ * pseudonym and enforces the one-vote constraint. Maps the response to a settled
119
+ * outcome the widget renders (never throws on a non-OK — a 409 is expected).
120
+ */
121
+ export declare function castVote(fetchImpl: WriteFetch, cfg: VoteCastConfig, choiceId: string, editionId: string): Promise<CastOutcome>;