@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.
- package/components/EChart.svelte +266 -0
- package/components/EChart.svelte.d.ts +63 -0
- package/components/index.js +3 -0
- package/components/renderer/EChartWidget.svelte +78 -0
- package/components/renderer/EChartWidget.svelte.d.ts +4 -0
- package/components/renderer/ResultsChartWidget.svelte +77 -0
- package/components/renderer/ResultsChartWidget.svelte.d.ts +4 -0
- package/components/renderer/StatGridWidget.svelte +33 -0
- package/components/renderer/StatGridWidget.svelte.d.ts +4 -0
- package/components/renderer/aggregate.d.ts +34 -0
- package/components/renderer/aggregate.ts +72 -0
- package/components/renderer/data-provider.d.ts +82 -0
- package/components/renderer/data-provider.ts +162 -0
- package/components/renderer/dispatch.d.ts +63 -0
- package/components/renderer/dispatch.ts +99 -0
- package/components/renderer/registry.d.ts +48 -0
- package/components/renderer/registry.ts +101 -0
- package/components/renderer/resolve-data.d.ts +109 -0
- package/components/renderer/resolve-data.ts +619 -0
- package/components/renderer/types.d.ts +164 -0
- package/components/renderer/types.ts +250 -0
- package/components/renderer/vote.d.ts +121 -0
- package/components/renderer/vote.ts +253 -0
- package/package.json +17 -3
|
@@ -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>;
|