@keemakr/ui-sdk 1.5.0 → 1.6.0-dev-20261026112734

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 (73) hide show
  1. package/README.md +118 -0
  2. package/dist/badges.d.ts +65 -0
  3. package/dist/badges.js +96 -0
  4. package/dist/build.js +2 -2
  5. package/dist/cells.d.ts +31 -0
  6. package/dist/cells.js +45 -0
  7. package/dist/chart.d.ts +20 -0
  8. package/dist/chart.js +38 -0
  9. package/dist/conform.d.ts +21 -0
  10. package/dist/conform.js +310 -0
  11. package/dist/dataset.d.ts +6 -6
  12. package/dist/date-format.d.ts +64 -0
  13. package/dist/date-format.js +132 -0
  14. package/dist/drawer.d.ts +22 -0
  15. package/dist/drawer.js +21 -0
  16. package/dist/edit.d.ts +60 -0
  17. package/dist/edit.js +60 -0
  18. package/dist/filters.d.ts +54 -0
  19. package/dist/filters.js +81 -0
  20. package/dist/form.d.ts +45 -1
  21. package/dist/form.js +39 -0
  22. package/dist/heatmap.d.ts +36 -0
  23. package/dist/heatmap.js +56 -0
  24. package/dist/icon.d.ts +20 -0
  25. package/dist/icon.js +43 -0
  26. package/dist/index.d.ts +37 -4
  27. package/dist/index.js +21 -2
  28. package/dist/markdown-links.d.ts +29 -0
  29. package/dist/markdown-links.js +99 -0
  30. package/dist/media.js +7 -1
  31. package/dist/negotiate.d.ts +302 -0
  32. package/dist/negotiate.js +1092 -0
  33. package/dist/nodes.d.ts +1 -1
  34. package/dist/nodes.js +3 -0
  35. package/dist/pager.d.ts +2 -2
  36. package/dist/player.d.ts +46 -0
  37. package/dist/player.js +86 -0
  38. package/dist/published/index.d.ts +4 -0
  39. package/dist/published/index.js +18 -0
  40. package/dist/published/v1.1.0/schema.d.ts +439 -0
  41. package/dist/published/v1.1.0/schema.js +405 -0
  42. package/dist/published/v1.4.0/dataset-view.d.ts +160 -0
  43. package/dist/published/v1.4.0/dataset-view.js +702 -0
  44. package/dist/published/v1.4.0/dataset.d.ts +331 -0
  45. package/dist/published/v1.4.0/dataset.js +628 -0
  46. package/dist/published/v1.4.0/schema.d.ts +1132 -0
  47. package/dist/published/v1.4.0/schema.js +1410 -0
  48. package/dist/schema.d.ts +1638 -20
  49. package/dist/schema.js +1275 -72
  50. package/dist/section.d.ts +65 -0
  51. package/dist/section.js +81 -0
  52. package/dist/stacked-bar.d.ts +34 -0
  53. package/dist/stacked-bar.js +20 -0
  54. package/dist/stat.d.ts +24 -0
  55. package/dist/stat.js +27 -0
  56. package/dist/table-row.d.ts +24 -0
  57. package/dist/table-row.js +23 -0
  58. package/dist/tabs.d.ts +36 -0
  59. package/dist/tabs.js +83 -0
  60. package/dist/testkit.d.ts +38 -0
  61. package/dist/testkit.js +68 -0
  62. package/dist/timeline.d.ts +5 -0
  63. package/dist/timeline.js +35 -0
  64. package/examples/campaigns/README.md +22 -1
  65. package/examples/campaigns/approvals/index.ts +23 -0
  66. package/examples/campaigns/approvals/launch.ts +38 -0
  67. package/examples/campaigns/brief/index.ts +23 -0
  68. package/examples/campaigns/brief/ready.ts +43 -0
  69. package/examples/campaigns/demo-approvals.ts +35 -0
  70. package/examples/campaigns/demo-brief.ts +23 -0
  71. package/examples/campaigns/entry.json +71 -1
  72. package/examples/campaigns/tsconfig.json +8 -1
  73. package/package.json +6 -1
@@ -0,0 +1,405 @@
1
+ // FROZEN COPY of the published @keemakr/ui-sdk@1.1.0 src/schema.ts (git tag @keemakr/ui-sdk@1.1.0).
2
+ // Core checks the tree it sends an older app against the schema that app really parses with.
3
+ // Do not edit: tests/sdk/ui-published-parsers.test.ts pins the bytes below this header.
4
+ /**
5
+ * The server-driven UI contract between a module runtime and keemakr-core.
6
+ *
7
+ * A module's `load` handler returns a ScreenTree; core validates it against
8
+ * this schema BEFORE rendering — that validation is the security boundary.
9
+ * Nothing here ever becomes raw HTML: every node maps to a platform-owned
10
+ * component, so module output is data, not code.
11
+ *
12
+ * Versioning: `version` is a literal. A breaking change to any node shape
13
+ * bumps UI_SCHEMA_VERSION and adds a new literal; core keeps rendering old
14
+ * versions until modules migrate.
15
+ */
16
+ import { z } from 'zod';
17
+ export const UI_SCHEMA_VERSION = 1;
18
+ const slug = z
19
+ .string()
20
+ .regex(/^[a-z][a-z0-9_-]*$/, 'lowercase slug (a-z, 0-9, -, _)')
21
+ .max(64);
22
+ /** What a button (or table row) does when pressed. */
23
+ export const pressSchema = z.discriminatedUnion('kind', [
24
+ z.object({
25
+ kind: z.literal('action'),
26
+ /** Key into the screen's `actions` map on the module runtime. */
27
+ action: slug,
28
+ input: z.record(z.string(), z.unknown()).optional(),
29
+ /** If set, core shows this confirmation before dispatching. */
30
+ confirm: z.string().max(500).optional(),
31
+ }),
32
+ z.object({
33
+ kind: z.literal('navigate'),
34
+ /** Another screen key of the same module. */
35
+ screen: slug,
36
+ params: z.record(z.string(), z.string()).optional(),
37
+ }),
38
+ ]);
39
+ const tone = z.enum(['default', 'success', 'warning', 'destructive']);
40
+ export const buttonSchema = z.object({
41
+ type: z.literal('button'),
42
+ label: z.string().max(80),
43
+ onPress: pressSchema,
44
+ variant: z.enum(['primary', 'secondary', 'destructive']).default('secondary'),
45
+ });
46
+ /** A table cell. Dates are ISO 8601; core formats them tenant-locale-aware. */
47
+ export const cellSchema = z.discriminatedUnion('kind', [
48
+ z.object({ kind: z.literal('text'), value: z.string().max(500) }),
49
+ z.object({ kind: z.literal('badge'), value: z.string().max(40), tone: tone.default('default') }),
50
+ z.object({ kind: z.literal('date'), value: z.iso.datetime() }),
51
+ ]);
52
+ const choiceCardsSchema = z.object({
53
+ type: z.literal('choice_cards'),
54
+ choices: z
55
+ .array(z.object({
56
+ id: z.string().min(1).max(64),
57
+ title: z.string().min(1).max(120),
58
+ description: z.string().max(500).optional(),
59
+ meta: z.string().max(120).optional(),
60
+ selected: z.boolean().default(false),
61
+ onPress: pressSchema,
62
+ }))
63
+ .min(1)
64
+ .max(20),
65
+ });
66
+ const progressSchema = z.object({
67
+ type: z.literal('progress'),
68
+ steps: z
69
+ .array(z.object({
70
+ label: z.string().min(1).max(200),
71
+ status: z.enum(['pending', 'active', 'done', 'error']),
72
+ detail: z.string().max(1000).optional(),
73
+ }))
74
+ .min(1)
75
+ .max(100),
76
+ });
77
+ const chartSchema = z
78
+ .object({
79
+ type: z.literal('chart'),
80
+ chartType: z.enum(['bar', 'line']),
81
+ title: z.string().max(120).optional(),
82
+ categories: z.array(z.string().max(80)).min(1).max(50),
83
+ series: z
84
+ .array(z.object({
85
+ name: z.string().min(1).max(80),
86
+ values: z.array(z.number().finite()).min(1).max(50),
87
+ }))
88
+ .min(1)
89
+ .max(8),
90
+ })
91
+ .superRefine((chart, ctx) => {
92
+ for (let i = 0; i < chart.series.length; i += 1) {
93
+ const series = chart.series[i];
94
+ if (series && series.values.length !== chart.categories.length) {
95
+ ctx.addIssue({
96
+ code: 'custom',
97
+ path: ['series', i, 'values'],
98
+ message: 'must contain one value per category',
99
+ });
100
+ }
101
+ }
102
+ });
103
+ const headingSchema = z.object({ type: z.literal('heading'), text: z.string().max(120) });
104
+ const textSchema = z.object({
105
+ type: z.literal('text'),
106
+ value: z.string().max(5000),
107
+ muted: z.boolean().default(false),
108
+ });
109
+ /** Rendered by core's sanitizing markdown renderer — never raw HTML. */
110
+ const markdownSchema = z.object({ type: z.literal('markdown'), value: z.string().max(20000) });
111
+ const statSchema = z.object({
112
+ type: z.literal('stat'),
113
+ label: z.string().max(80),
114
+ value: z.string().max(40),
115
+ hint: z.string().max(120).optional(),
116
+ });
117
+ /**
118
+ * What a table row may reveal in place. Deliberately NOT the leaf union and NOT
119
+ * `nodeSchema`: `table` is itself a leaf, so either reference re-opens the
120
+ * recursion — both were measured accepting a 200-level `table -> expand -> table`
121
+ * chain, escaping MAX_SECTION_DEPTH entirely, and both collapsed type inference
122
+ * (TS7022 on the leaf union, TS7024 on the lazy callback). Naming the five
123
+ * content branches explicitly keeps this non-recursive by construction, which is
124
+ * why the depth test asserts REJECTION rather than slowness: the validator
125
+ * handles 200 levels in tens of milliseconds, so nothing upstream would catch it.
126
+ *
127
+ * The general rule: any leaf that references the leaf union re-opens the recursion.
128
+ */
129
+ const expandContentSchema = z.discriminatedUnion('type', [
130
+ headingSchema,
131
+ textSchema,
132
+ markdownSchema,
133
+ statSchema,
134
+ buttonSchema,
135
+ ]);
136
+ const leafNodeSchema = z.discriminatedUnion('type', [
137
+ headingSchema,
138
+ textSchema,
139
+ markdownSchema,
140
+ statSchema,
141
+ buttonSchema,
142
+ z.object({
143
+ type: z.literal('form'),
144
+ key: slug,
145
+ fields: z
146
+ .array(z.object({
147
+ kind: z.enum(['text', 'textarea', 'select']),
148
+ name: slug,
149
+ label: z.string().max(80),
150
+ value: z.string().max(20000).optional(),
151
+ options: z.array(z.object({ value: z.string(), label: z.string() })).optional(),
152
+ required: z.boolean().default(false),
153
+ }))
154
+ .max(30),
155
+ /** Field values are posted as the action's `input`, keyed by field name. */
156
+ submit: z.object({ label: z.string().max(80), action: slug }),
157
+ }),
158
+ z.object({
159
+ type: z.literal('table'),
160
+ columns: z.array(z.object({ key: slug, label: z.string().max(80) })).max(12),
161
+ rows: z
162
+ .array(z.object({
163
+ key: z.string().max(128),
164
+ cells: z.record(slug, cellSchema),
165
+ onPress: pressSchema.optional(),
166
+ /**
167
+ * Content the row reveals in place, no navigation hop. Content nodes
168
+ * only — no nested `table`, no `section`.
169
+ *
170
+ * A row declaring `expand` WITH CONTENT toggles disclosure when
171
+ * activated, and its `onPress` is IGNORED. The renderer already gives a
172
+ * pressable row `role="button"` with an Enter/Space handler, so a second
173
+ * focusable control inside it would be invalid ARIA with two handlers on
174
+ * one key. Expansion is local view state; a press means "call my
175
+ * runtime". They stay apart deliberately.
176
+ *
177
+ * Zero children is allowed and means NO disclosure — the row behaves
178
+ * exactly as if `expand` were absent, `onPress` included. A body built
179
+ * from data legitimately drops to nothing, and rejecting that would fail
180
+ * the whole screen load rather than degrade one row (the same cliff the
181
+ * tab strip's minimum had). The maximum stays: an oversize body has no
182
+ * safe degradation — truncating it would be silent data loss.
183
+ */
184
+ expand: z.object({ children: z.array(expandContentSchema).max(20) }).optional(),
185
+ }))
186
+ .max(500),
187
+ /** Shown when `rows` is empty — every data-bound surface needs an empty state. */
188
+ empty: z.string().max(200),
189
+ }),
190
+ z.object({
191
+ type: z.literal('timeline'),
192
+ items: z
193
+ .array(z.object({
194
+ label: z.string().max(200),
195
+ status: z.enum(['pending', 'active', 'done', 'error']),
196
+ at: z.iso.datetime().optional(),
197
+ detail: z.string().max(1000).optional(),
198
+ }))
199
+ .max(200),
200
+ }),
201
+ /**
202
+ * A HITL approval slot — a REFERENCE, never content. The module places the
203
+ * request id where the card should appear; core resolves it at render time
204
+ * (tenant + this-install + still-pending checks) and renders the platform
205
+ * card wired to the respond endpoint. The module cannot author the card
206
+ * (a forged prompt/options here is ignored — this node carries no fields
207
+ * for them) and the tree transports no PHI, only the opaque id.
208
+ */
209
+ z.object({ type: z.literal('pending_request'), requestId: z.string().min(1).max(128) }),
210
+ /**
211
+ * The zero-bookkeeping HITL slot: "render ALL of this module's still-pending
212
+ * approvals here." Core fills it from its own pending store — the module
213
+ * needs no request-id tracking (which the id-form above requires). Requests
214
+ * already placed individually via `pending_request` elsewhere in the same
215
+ * tree are not repeated here.
216
+ */
217
+ z.object({ type: z.literal('pending_requests') }),
218
+ choiceCardsSchema,
219
+ progressSchema,
220
+ chartSchema,
221
+ ]);
222
+ const MAX_SECTION_DEPTH = 8;
223
+ const nodeSchemas = new Map();
224
+ /** Shared by the chain and by the root union, so the two cannot drift apart. */
225
+ const sectionBranch = (children) => z.object({
226
+ type: z.literal('section'),
227
+ title: z.string().max(120).optional(),
228
+ description: z.string().max(300).optional(),
229
+ layout: z.enum(['stack', 'grid']),
230
+ columns: z.number().int().min(1).max(4),
231
+ children: z.array(children).max(100),
232
+ });
233
+ const tabsBranch = (children) => z
234
+ .object({
235
+ type: z.literal('tabs'),
236
+ key: slug,
237
+ /** A hint only — the renderer owns selection, and it survives no reload. */
238
+ active: slug.optional(),
239
+ // One tab is legitimate: a strip generated per live queue drops to one when
240
+ // only one queue exists, and rejecting that fails the WHOLE screen load.
241
+ tabs: z
242
+ .array(z.object({
243
+ key: slug,
244
+ label: z.string().min(1).max(40),
245
+ /** Sits beside the label — a count, not prose. */
246
+ badge: z.string().max(12).optional(),
247
+ children: z.array(children).max(100),
248
+ }))
249
+ .min(1)
250
+ .max(6),
251
+ })
252
+ .superRefine((node, ctx) => {
253
+ const keys = new Set();
254
+ for (const [index, tab] of node.tabs.entries()) {
255
+ if (keys.has(tab.key)) {
256
+ ctx.addIssue({
257
+ code: 'custom',
258
+ path: ['tabs', index, 'key'],
259
+ message: `duplicate tab key "${tab.key}"`,
260
+ });
261
+ }
262
+ keys.add(tab.key);
263
+ }
264
+ if (node.active !== undefined && !keys.has(node.active)) {
265
+ ctx.addIssue({
266
+ code: 'custom',
267
+ path: ['active'],
268
+ message: `active tab "${node.active}" is not one of the declared tabs`,
269
+ });
270
+ }
271
+ });
272
+ function nodeSchemaAtDepth(depth) {
273
+ const cached = nodeSchemas.get(depth);
274
+ if (cached)
275
+ return cached;
276
+ if (depth === MAX_SECTION_DEPTH)
277
+ return leafNodeSchema;
278
+ const schema = z.discriminatedUnion('type', [
279
+ leafNodeSchema,
280
+ sectionBranch(z.lazy(() => nodeSchemaAtDepth(depth + 1))),
281
+ ]);
282
+ nodeSchemas.set(depth, schema);
283
+ return schema;
284
+ }
285
+ /**
286
+ * The root union: the chain's own membership plus `tabs`. Both container
287
+ * branches descend into the chain at depth 1, so nesting stays bounded by
288
+ * MAX_SECTION_DEPTH exactly as it was before `tabs` existed.
289
+ */
290
+ export const nodeSchema = z.discriminatedUnion('type', [
291
+ leafNodeSchema,
292
+ sectionBranch(z.lazy(() => nodeSchemaAtDepth(1))),
293
+ tabsBranch(z.lazy(() => nodeSchemaAtDepth(1))),
294
+ ]);
295
+ export const screenTreeSchema = z.object({
296
+ version: z.literal(UI_SCHEMA_VERSION),
297
+ title: z.string().max(120),
298
+ description: z.string().max(300).optional(),
299
+ /** Page-level actions, rendered in the header (e.g. "New draft"). */
300
+ actions: z.array(buttonSchema).max(4).optional(),
301
+ children: z.array(nodeSchema).max(100),
302
+ // ponytail: polling keeps live views (agent activity) simple; switch to the
303
+ // existing run-event NDJSON stream when polling measurably falls short.
304
+ refreshSeconds: z.number().int().min(2).max(300).optional(),
305
+ });
306
+ /** What an action handler returns. `refresh` re-runs `load` after the action. */
307
+ export const actionResultSchema = z.object({
308
+ toast: z.string().max(200).optional(),
309
+ navigate: z
310
+ .object({ screen: slug, params: z.record(z.string(), z.string()).optional() })
311
+ .optional(),
312
+ refresh: z.boolean().default(true),
313
+ });
314
+ /* --------------------------------------------------------------------- */
315
+ /* Wire protocol: core proxy <-> module runtime (grant in Authorization). */
316
+ /* eve runtimes serve these from agent/channels/screens.ts (custom channel */
317
+ /* routes mount at the runtime root): */
318
+ /* POST {runtime}/screens/{screen}/load -> ScreenLoadResponse */
319
+ /* POST {runtime}/screens/{screen}/actions/{action} -> ActionResult */
320
+ /* --------------------------------------------------------------------- */
321
+ export const screenLoadRequestSchema = z.object({
322
+ params: z.record(z.string(), z.string()).default({}),
323
+ });
324
+ export const screenLoadResponseSchema = z.object({ tree: screenTreeSchema });
325
+ export const screenActionRequestSchema = z.object({
326
+ params: z.record(z.string(), z.string()).default({}),
327
+ input: z.record(z.string(), z.unknown()).default({}),
328
+ /** Core-generated, forwarded so the handler can be idempotent on retries too
329
+ * (core already dedupes via command_log; this is the belt-and-suspenders
330
+ * half of at-least-once). Optional for runtime version-skew tolerance. */
331
+ idempotencyKey: z.string().max(200).optional(),
332
+ });
333
+ export const interfaceFamilySchema = z.enum(['dashboard', 'client']);
334
+ export const surfaceDefinitionSchema = z.object({
335
+ label: z.string().min(1).max(60),
336
+ /** Lucide icon name, same convention as entry.json `icon`. */
337
+ icon: z.string().max(60).optional(),
338
+ description: z.string().max(200).optional(),
339
+ availableOn: z
340
+ .array(interfaceFamilySchema)
341
+ .min(1)
342
+ .max(2)
343
+ .refine((families) => new Set(families).size === families.length, {
344
+ message: 'interface families must be unique',
345
+ }),
346
+ });
347
+ /**
348
+ * Manifest addition: `manifest.surfaces[]` in entry.json. Each surface is a
349
+ * side-menu entry pointing at an entry screen. Core's marketplace
350
+ * validateSubmission composes this in (wired in the proxy phase).
351
+ */
352
+ export const surfaceSchema = surfaceDefinitionSchema.extend({
353
+ key: slug,
354
+ screen: slug,
355
+ });
356
+ export const screenRegistrationSchema = z.object({
357
+ screen: slug,
358
+ surface: slug,
359
+ });
360
+ /** The complete generated UI portion of a module manifest. */
361
+ export const uiManifestSchema = z
362
+ .object({
363
+ surfaces: z.array(surfaceSchema).min(1),
364
+ screenRegistry: z.array(screenRegistrationSchema).min(1).max(256),
365
+ })
366
+ .superRefine((manifest, ctx) => {
367
+ const surfaceKeys = new Set();
368
+ for (const [index, surface] of manifest.surfaces.entries()) {
369
+ if (surfaceKeys.has(surface.key)) {
370
+ ctx.addIssue({
371
+ code: 'custom',
372
+ path: ['surfaces', index, 'key'],
373
+ message: `duplicate surface key "${surface.key}"`,
374
+ });
375
+ }
376
+ surfaceKeys.add(surface.key);
377
+ }
378
+ const screens = new Map();
379
+ for (const [index, registration] of manifest.screenRegistry.entries()) {
380
+ if (!surfaceKeys.has(registration.surface)) {
381
+ ctx.addIssue({
382
+ code: 'custom',
383
+ path: ['screenRegistry', index, 'surface'],
384
+ message: `unknown surface "${registration.surface}"`,
385
+ });
386
+ }
387
+ if (screens.has(registration.screen)) {
388
+ ctx.addIssue({
389
+ code: 'custom',
390
+ path: ['screenRegistry', index, 'screen'],
391
+ message: `duplicate screen identity "${registration.screen}"`,
392
+ });
393
+ }
394
+ screens.set(registration.screen, registration.surface);
395
+ }
396
+ for (const [index, surface] of manifest.surfaces.entries()) {
397
+ if (screens.get(surface.screen) !== surface.key) {
398
+ ctx.addIssue({
399
+ code: 'custom',
400
+ path: ['surfaces', index, 'screen'],
401
+ message: `root screen "${surface.screen}" must be registered to surface "${surface.key}"`,
402
+ });
403
+ }
404
+ }
405
+ });
@@ -0,0 +1,160 @@
1
+ /**
2
+ * The `dataset` node, and how a dataset becomes blocks.
3
+ *
4
+ * A screen places one of its module's datasets with a single node:
5
+ *
6
+ * { type: 'dataset', dataset: 'leads', layout: 'table' }
7
+ *
8
+ * Core expands the node before any renderer sees it: it queries the dataset
9
+ * with the viewer's grant and replaces the node with blocks every app already
10
+ * draws. So a new layout never needs a new client. The layouts:
11
+ *
12
+ * - `table`: a table with sort and filter headers and a pager.
13
+ * - `board`: cards in columns, grouped by a stage or a status.
14
+ * - `list`: a table with filter chips over its stage or status. Pressing a
15
+ * row opens it as a record beside the list, in the screen's right rail.
16
+ * - `record`: one row, named by a screen param, as a page: its title, its
17
+ * status, and its other fields as facts. This file is the pure
18
+ * half of that: the node's schema, how a screen's params become a query, and
19
+ * how one page of rows becomes blocks. It runs on the server and in tests.
20
+ *
21
+ * Interaction is navigation. A sort header, a filter menu and the pager each
22
+ * navigate back to the same screen with params (`sort`, `direction`, a field
23
+ * name, the page param), and the next expansion reads them. One interactive
24
+ * dataset node per screen: two would share those param names.
25
+ */
26
+ import { z } from 'zod';
27
+ import { type DatasetDeclaration, type DatasetField, type DatasetQueryRequestInput, type DatasetQueryResponse, type DatasetRow } from './dataset.js';
28
+ export declare const DATASET_LAYOUTS: readonly ["table", "board", "list", "record", "chart"];
29
+ export type DatasetLayout = (typeof DATASET_LAYOUTS)[number];
30
+ /** Deepest page a table reaches. Paging walks cursors from page 1. */
31
+ export declare const DATASET_MAX_PAGE = 20;
32
+ export declare const datasetNodeSchema: z.ZodObject<{
33
+ type: z.ZodLiteral<"dataset">;
34
+ dataset: z.ZodString;
35
+ layout: z.ZodDefault<z.ZodEnum<{
36
+ record: "record";
37
+ table: "table";
38
+ board: "board";
39
+ list: "list";
40
+ chart: "chart";
41
+ }>>;
42
+ fields: z.ZodOptional<z.ZodArray<z.ZodString>>;
43
+ filters: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
44
+ sort: z.ZodOptional<z.ZodObject<{
45
+ field: z.ZodString;
46
+ direction: z.ZodDefault<z.ZodEnum<{
47
+ asc: "asc";
48
+ desc: "desc";
49
+ }>>;
50
+ }, z.core.$strip>>;
51
+ limit: z.ZodDefault<z.ZodNumber>;
52
+ groupBy: z.ZodOptional<z.ZodString>;
53
+ chart: z.ZodOptional<z.ZodObject<{
54
+ form: z.ZodDefault<z.ZodEnum<{
55
+ bars: "bars";
56
+ funnel: "funnel";
57
+ }>>;
58
+ measure: z.ZodOptional<z.ZodString>;
59
+ }, z.core.$strip>>;
60
+ open: z.ZodOptional<z.ZodObject<{
61
+ screen: z.ZodString;
62
+ param: z.ZodDefault<z.ZodString>;
63
+ }, z.core.$strip>>;
64
+ param: z.ZodDefault<z.ZodString>;
65
+ empty: z.ZodDefault<z.ZodString>;
66
+ create: z.ZodDefault<z.ZodBoolean>;
67
+ }, z.core.$strip>;
68
+ export type DatasetNode = z.infer<typeof datasetNodeSchema>;
69
+ export type DatasetNodeInput = z.input<typeof datasetNodeSchema>;
70
+ /** The screen a dataset node sits on: where its presses navigate back to. */
71
+ export interface DatasetScreenContext {
72
+ screen: string;
73
+ params: Record<string, string>;
74
+ }
75
+ /** Problems with a node against its dataset. Core shows these, never a broken view. */
76
+ export declare function datasetNodeIssues(node: DatasetNode, declaration: DatasetDeclaration): string[];
77
+ /** Which page of a table the params ask for, 1 when none. */
78
+ export declare function datasetPage(node: DatasetNode, params: Record<string, string>): number;
79
+ /**
80
+ * The key of the row a record shows, or a list opens beside it. Undefined when
81
+ * the node shows no single row, or the param is empty.
82
+ */
83
+ export declare function datasetRecordKey(node: DatasetNode, params: Record<string, string>): string | undefined;
84
+ /** The query for the one row a record shows. */
85
+ export declare function datasetRecordQuery(key: string): DatasetQueryRequestInput;
86
+ /**
87
+ * The query a node and the screen's params make. The node's own filters win
88
+ * over a param of the same name; a param for a field the dataset does not let
89
+ * a person filter is ignored, never forwarded.
90
+ */
91
+ export declare function datasetQueryFor(node: DatasetNode, declaration: DatasetDeclaration, params: Record<string, string>): DatasetQueryRequestInput;
92
+ type Cell = {
93
+ kind: 'text';
94
+ value: string;
95
+ } | {
96
+ kind: 'badge';
97
+ value: string;
98
+ tone: 'default' | 'success' | 'warning' | 'destructive';
99
+ } | {
100
+ kind: 'date';
101
+ value: string;
102
+ };
103
+ /** One value as a table cell, or null when the row has none. */
104
+ export declare function datasetCell(field: DatasetField, value: DatasetRow[string]): Cell | null;
105
+ /**
106
+ * One row as a record: its title, its subtitle, a tag for its status and its
107
+ * stage, and its other fields as facts. `row` undefined means the module did
108
+ * not find the key, and the record says so.
109
+ */
110
+ export declare function compileDatasetRecord(opts: {
111
+ declaration: DatasetDeclaration;
112
+ row: DatasetRow | undefined;
113
+ }): Record<string, unknown>[];
114
+ /** The key of the root section that holds the record a list opened. */
115
+ export declare const DATASET_RECORD_SECTION = "dataset-record";
116
+ /**
117
+ * The record a list opened, as the section the screen's right rail shows,
118
+ * with a button that closes it.
119
+ */
120
+ export declare function compileDatasetRecordPanel(opts: {
121
+ node: DatasetNode;
122
+ declaration: DatasetDeclaration;
123
+ row: DatasetRow | undefined;
124
+ screen: DatasetScreenContext;
125
+ }): Record<string, unknown>;
126
+ /**
127
+ * One page of rows as screen blocks. `rows` is the page to show; `total` and
128
+ * `hasMore` drive the pager. The output is plain screen nodes, ready to put in
129
+ * the node's place.
130
+ */
131
+ export declare function compileDatasetNode(opts: {
132
+ node: DatasetNode;
133
+ declaration: DatasetDeclaration;
134
+ response: DatasetQueryResponse;
135
+ screen: DatasetScreenContext;
136
+ page: number;
137
+ hasMore: boolean;
138
+ }): Record<string, unknown>[];
139
+ /**
140
+ * The screen param that opens a dataset's create form: its value is the
141
+ * dataset's name. Reserved on every screen that shows a dataset.
142
+ */
143
+ export declare const DATASET_NEW_PARAM = "new";
144
+ /**
145
+ * The action a create form submits. Core answers it itself: it checks the input
146
+ * against the declaration and calls the module's `create`. A module never
147
+ * receives it, so no module screen may name an action this.
148
+ */
149
+ export declare const DATASET_CREATE_ACTION = "kee-dataset-create";
150
+ /**
151
+ * The create affordance for a dataset node: a button that opens the form, or,
152
+ * once the `new` param names this dataset, the form itself with Cancel. Empty
153
+ * when the dataset takes no new rows, the node turns it off, or it is a record.
154
+ */
155
+ export declare function compileDatasetCreate(opts: {
156
+ node: DatasetNode;
157
+ declaration: DatasetDeclaration;
158
+ screen: DatasetScreenContext;
159
+ }): Record<string, unknown>[];
160
+ export {};