@keemakr/ui-sdk 0.1.1 → 1.0.0-dev-20260807074219
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/README.md +79 -0
- package/dist/build.d.ts +9 -0
- package/dist/build.js +264 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +116 -0
- package/dist/define.d.ts +3 -16
- package/dist/define.js +21 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/schema.d.ts +45 -2
- package/dist/schema.js +65 -5
- package/examples/campaigns/README.md +17 -0
- package/examples/campaigns/demo.ts +13 -0
- package/examples/campaigns/entry.json +53 -0
- package/examples/campaigns/tsconfig.json +11 -0
- package/examples/campaigns/ui/campaign-settings/screen.ts +9 -0
- package/examples/campaigns/ui/campaign-settings/surface.ts +7 -0
- package/examples/campaigns/ui/campaigns/campaign/screen.ts +29 -0
- package/examples/campaigns/ui/campaigns/screen.ts +27 -0
- package/examples/campaigns/ui/campaigns/surface.ts +7 -0
- package/examples/campaigns/ui/data.ts +9 -0
- package/examples/campaigns/ui/screen-registry.ts +10 -0
- package/package.json +14 -3
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Keemakr UI SDK
|
|
2
|
+
|
|
3
|
+
Build server-driven module screens that Keemakr renders in Dashboard and Kee clients. Your module
|
|
4
|
+
returns structured UI data. It does not send HTML or browser code.
|
|
5
|
+
|
|
6
|
+
## 1. Organize UI by surface
|
|
7
|
+
|
|
8
|
+
Keep `ui/` at the module root, separate from agents:
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
ui/
|
|
12
|
+
campaigns/
|
|
13
|
+
surface.ts
|
|
14
|
+
screen.ts
|
|
15
|
+
campaign/
|
|
16
|
+
screen.ts
|
|
17
|
+
campaign-settings/
|
|
18
|
+
surface.ts
|
|
19
|
+
screen.ts
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
A surface is one complete navigable flow. Every nested `screen.ts` inherits its surface's
|
|
23
|
+
availability. Put shared helpers under `ui/_shared/`; the generator ignores underscore-prefixed
|
|
24
|
+
directories.
|
|
25
|
+
|
|
26
|
+
## 2. Declare where a surface is available
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { defineSurface } from '@keemakr/ui-sdk';
|
|
30
|
+
|
|
31
|
+
export default defineSurface({
|
|
32
|
+
label: 'Campaigns',
|
|
33
|
+
availableOn: ['dashboard', 'client'],
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Use `['dashboard']` for tenant administration screens that should not appear in Kee clients. The
|
|
38
|
+
array is required and may contain `dashboard`, `client`, or both.
|
|
39
|
+
|
|
40
|
+
## 3. Define screens
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { defineScreen } from '@keemakr/ui-sdk';
|
|
44
|
+
|
|
45
|
+
export default defineScreen({
|
|
46
|
+
load: async () => ({
|
|
47
|
+
version: 1,
|
|
48
|
+
title: 'Campaigns',
|
|
49
|
+
children: [{ type: 'text', value: 'Your campaigns appear here.' }],
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Folder names are screen identities. For example, `ui/campaigns/campaign/screen.ts` is the
|
|
55
|
+
`campaign` screen. Screen folder names must be unique across the module.
|
|
56
|
+
|
|
57
|
+
## 4. Generate before Eve runs
|
|
58
|
+
|
|
59
|
+
Use the SDK wrapper in the module scripts:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"scripts": {
|
|
64
|
+
"dev": "keemakr-ui dev -- eve dev",
|
|
65
|
+
"build": "keemakr-ui build && eve build"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The generator validates `ui/`, writes `ui/screen-registry.ts`, and synchronizes
|
|
71
|
+
`manifest.surfaces` plus `manifest.screenRegistry` in `entry.json`. Do not edit those generated
|
|
72
|
+
fields or the registry by hand. A module with no surfaces omits both fields. Removing a screen
|
|
73
|
+
records its identity so later builds keep warning about the breaking change.
|
|
74
|
+
|
|
75
|
+
Core uses the installed manifest to filter navigation and to gate every load and action before it
|
|
76
|
+
calls the module runtime. Availability controls UI placement. Existing authentication and roles
|
|
77
|
+
still control who may enter Dashboard or a Kee client.
|
|
78
|
+
|
|
79
|
+
See [`examples/campaigns`](./examples/campaigns) for one complete in-memory example.
|
package/dist/build.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type UiManifest } from './schema.js';
|
|
2
|
+
export interface BuildModuleUiResult {
|
|
3
|
+
manifest: UiManifest;
|
|
4
|
+
registryPath: string;
|
|
5
|
+
entryPath: string;
|
|
6
|
+
removedScreens: string[];
|
|
7
|
+
}
|
|
8
|
+
/** Validate root ui/, generate its runtime registry, and synchronize entry.json. */
|
|
9
|
+
export declare function buildModuleUi(projectRoot?: string): Promise<BuildModuleUiResult>;
|
package/dist/build.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { readdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, join, relative, sep } from 'node:path';
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
4
|
+
import ts from 'typescript';
|
|
5
|
+
import { surfaceDefinitionSchema, uiManifestSchema, } from './schema.js';
|
|
6
|
+
const SLUG = /^[a-z][a-z0-9_-]*$/;
|
|
7
|
+
const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
8
|
+
const REMOVED_SCREENS_META_KEY = 'uiSdkRemovedScreens';
|
|
9
|
+
function unwrap(expression) {
|
|
10
|
+
if (ts.isParenthesizedExpression(expression) ||
|
|
11
|
+
ts.isAsExpression(expression) ||
|
|
12
|
+
ts.isSatisfiesExpression(expression)) {
|
|
13
|
+
return unwrap(expression.expression);
|
|
14
|
+
}
|
|
15
|
+
return expression;
|
|
16
|
+
}
|
|
17
|
+
function literalValue(expression) {
|
|
18
|
+
const value = unwrap(expression);
|
|
19
|
+
if (ts.isStringLiteralLike(value))
|
|
20
|
+
return value.text;
|
|
21
|
+
if (value.kind === ts.SyntaxKind.TrueKeyword)
|
|
22
|
+
return true;
|
|
23
|
+
if (value.kind === ts.SyntaxKind.FalseKeyword)
|
|
24
|
+
return false;
|
|
25
|
+
if (ts.isArrayLiteralExpression(value))
|
|
26
|
+
return value.elements.map(literalValue);
|
|
27
|
+
throw new Error('surface.ts values must be string or array literals');
|
|
28
|
+
}
|
|
29
|
+
async function readSurfaceDefinition(path) {
|
|
30
|
+
const source = ts.createSourceFile(path, await readFile(path, 'utf8'), ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
31
|
+
let object;
|
|
32
|
+
for (const statement of source.statements) {
|
|
33
|
+
if (!ts.isExportAssignment(statement))
|
|
34
|
+
continue;
|
|
35
|
+
const exported = unwrap(statement.expression);
|
|
36
|
+
if (!ts.isCallExpression(exported) || exported.arguments.length !== 1)
|
|
37
|
+
continue;
|
|
38
|
+
if (!ts.isIdentifier(exported.expression) || exported.expression.text !== 'defineSurface')
|
|
39
|
+
continue;
|
|
40
|
+
const argument = unwrap(exported.arguments[0]);
|
|
41
|
+
if (ts.isObjectLiteralExpression(argument))
|
|
42
|
+
object = argument;
|
|
43
|
+
}
|
|
44
|
+
if (!object) {
|
|
45
|
+
throw new Error(`${path}: default export must be defineSurface({ ... })`);
|
|
46
|
+
}
|
|
47
|
+
const raw = {};
|
|
48
|
+
for (const property of object.properties) {
|
|
49
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
50
|
+
throw new Error(`${path}: surface properties must use key: value syntax`);
|
|
51
|
+
}
|
|
52
|
+
const name = property.name;
|
|
53
|
+
if (!ts.isIdentifier(name) && !ts.isStringLiteral(name)) {
|
|
54
|
+
throw new Error(`${path}: surface property names must be static`);
|
|
55
|
+
}
|
|
56
|
+
raw[name.text] = literalValue(property.initializer);
|
|
57
|
+
}
|
|
58
|
+
const allowed = new Set(['label', 'icon', 'description', 'availableOn']);
|
|
59
|
+
for (const key of Object.keys(raw)) {
|
|
60
|
+
if (!allowed.has(key))
|
|
61
|
+
throw new Error(`${path}: unknown surface property "${key}"`);
|
|
62
|
+
}
|
|
63
|
+
const parsed = surfaceDefinitionSchema.safeParse(raw);
|
|
64
|
+
if (!parsed.success) {
|
|
65
|
+
const issue = parsed.error.issues[0];
|
|
66
|
+
throw new Error(`${path}: ${issue?.path.join('.') || 'surface'} ${issue?.message ?? 'is invalid'}`);
|
|
67
|
+
}
|
|
68
|
+
return parsed.data;
|
|
69
|
+
}
|
|
70
|
+
async function isFile(path) {
|
|
71
|
+
return stat(path)
|
|
72
|
+
.then((value) => value.isFile())
|
|
73
|
+
.catch(() => false);
|
|
74
|
+
}
|
|
75
|
+
async function collectScreenFiles(directory) {
|
|
76
|
+
const out = [];
|
|
77
|
+
if (await isFile(join(directory, 'screen.ts')))
|
|
78
|
+
out.push(join(directory, 'screen.ts'));
|
|
79
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
80
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
81
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name.startsWith('_'))
|
|
82
|
+
continue;
|
|
83
|
+
const child = join(directory, entry.name);
|
|
84
|
+
if (await isFile(join(child, 'surface.ts'))) {
|
|
85
|
+
throw new Error(`${join(child, 'surface.ts')}: nested surfaces are not allowed`);
|
|
86
|
+
}
|
|
87
|
+
out.push(...(await collectScreenFiles(child)));
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
function moduleSpecifier(fromDirectory, file) {
|
|
92
|
+
const value = relative(fromDirectory, file).split(sep).join('/').replace(/\.ts$/, '.js');
|
|
93
|
+
return value.startsWith('.') ? value : `./${value}`;
|
|
94
|
+
}
|
|
95
|
+
function propertyKey(key) {
|
|
96
|
+
return IDENTIFIER.test(key) ? key : `'${key}'`;
|
|
97
|
+
}
|
|
98
|
+
async function readIfPresent(path) {
|
|
99
|
+
return readFile(path, 'utf8').catch((error) => {
|
|
100
|
+
if (error.code === 'ENOENT')
|
|
101
|
+
return undefined;
|
|
102
|
+
throw error;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function temporaryPath(path, index) {
|
|
106
|
+
return `${path}.keemakr-ui-${process.pid}-${Date.now()}-${index}.tmp`;
|
|
107
|
+
}
|
|
108
|
+
async function restoreOutput(output) {
|
|
109
|
+
if (output.previous === undefined) {
|
|
110
|
+
await rm(output.path, { force: true });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const restorePath = temporaryPath(output.path, -1);
|
|
114
|
+
await writeFile(restorePath, output.previous);
|
|
115
|
+
await rename(restorePath, output.path);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Prepare every generated file before replacing either live output. A process
|
|
119
|
+
* can still die between two renames, but ordinary write/rename failures restore
|
|
120
|
+
* the previous pair rather than leaving one generated contract half-updated.
|
|
121
|
+
*/
|
|
122
|
+
async function replaceGeneratedOutputs(outputs) {
|
|
123
|
+
const changed = [];
|
|
124
|
+
for (const output of outputs) {
|
|
125
|
+
const previous = await readIfPresent(output.path);
|
|
126
|
+
if (previous !== output.contents)
|
|
127
|
+
changed.push({ ...output, previous });
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
for (const [index, output] of changed.entries()) {
|
|
131
|
+
if (output.contents === undefined)
|
|
132
|
+
continue;
|
|
133
|
+
output.temporaryPath = temporaryPath(output.path, index);
|
|
134
|
+
await writeFile(output.temporaryPath, output.contents);
|
|
135
|
+
}
|
|
136
|
+
const committed = [];
|
|
137
|
+
try {
|
|
138
|
+
for (const output of changed) {
|
|
139
|
+
if (output.temporaryPath)
|
|
140
|
+
await rename(output.temporaryPath, output.path);
|
|
141
|
+
else
|
|
142
|
+
await rm(output.path, { force: true });
|
|
143
|
+
committed.push(output);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
const restores = await Promise.allSettled(committed.reverse().map(restoreOutput));
|
|
148
|
+
const restoreFailure = restores.find((result) => result.status === 'rejected');
|
|
149
|
+
if (restoreFailure?.status === 'rejected') {
|
|
150
|
+
throw new AggregateError([error, restoreFailure.reason], 'generated UI outputs could not be committed or restored');
|
|
151
|
+
}
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
await Promise.all(changed.flatMap((output) => output.temporaryPath ? [rm(output.temporaryPath, { force: true })] : []));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function stringArray(value) {
|
|
160
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string') ? value : [];
|
|
161
|
+
}
|
|
162
|
+
/** Validate root ui/, generate its runtime registry, and synchronize entry.json. */
|
|
163
|
+
export async function buildModuleUi(projectRoot = process.cwd()) {
|
|
164
|
+
const uiRoot = join(projectRoot, 'ui');
|
|
165
|
+
const entryPath = join(projectRoot, 'entry.json');
|
|
166
|
+
const entries = await readdir(uiRoot, { withFileTypes: true }).catch((error) => {
|
|
167
|
+
if (error.code === 'ENOENT')
|
|
168
|
+
return [];
|
|
169
|
+
throw error;
|
|
170
|
+
});
|
|
171
|
+
const surfaces = [];
|
|
172
|
+
const screenRegistry = [];
|
|
173
|
+
const registryImports = [];
|
|
174
|
+
const seenScreens = new Map();
|
|
175
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
176
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name.startsWith('_'))
|
|
177
|
+
continue;
|
|
178
|
+
if (!SLUG.test(entry.name))
|
|
179
|
+
throw new Error(`surface directory "${entry.name}" is not a slug`);
|
|
180
|
+
const surfaceDirectory = join(uiRoot, entry.name);
|
|
181
|
+
const definitionPath = join(surfaceDirectory, 'surface.ts');
|
|
182
|
+
const rootScreenPath = join(surfaceDirectory, 'screen.ts');
|
|
183
|
+
if (!(await isFile(definitionPath)))
|
|
184
|
+
throw new Error(`${definitionPath}: file is required`);
|
|
185
|
+
if (!(await isFile(rootScreenPath)))
|
|
186
|
+
throw new Error(`${rootScreenPath}: file is required`);
|
|
187
|
+
const definition = await readSurfaceDefinition(definitionPath);
|
|
188
|
+
surfaces.push({ key: entry.name, screen: entry.name, ...definition });
|
|
189
|
+
for (const file of await collectScreenFiles(surfaceDirectory)) {
|
|
190
|
+
const screen = basename(dirname(file));
|
|
191
|
+
if (!SLUG.test(screen))
|
|
192
|
+
throw new Error(`${file}: screen directory "${screen}" is not a slug`);
|
|
193
|
+
const previous = seenScreens.get(screen);
|
|
194
|
+
if (previous) {
|
|
195
|
+
throw new Error(`duplicate screen identity "${screen}" in ${previous} and ${file}`);
|
|
196
|
+
}
|
|
197
|
+
seenScreens.set(screen, file);
|
|
198
|
+
screenRegistry.push({ screen, surface: entry.name });
|
|
199
|
+
registryImports.push({ screen, file });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
screenRegistry.sort((a, b) => a.screen.localeCompare(b.screen));
|
|
203
|
+
registryImports.sort((a, b) => a.screen.localeCompare(b.screen));
|
|
204
|
+
const manifest = surfaces.length === 0
|
|
205
|
+
? { surfaces, screenRegistry }
|
|
206
|
+
: uiManifestSchema.parse({ surfaces, screenRegistry });
|
|
207
|
+
const entrySource = await readFile(entryPath, 'utf8');
|
|
208
|
+
const originalEntryManifest = JSON.parse(entrySource);
|
|
209
|
+
const entryManifest = JSON.parse(entrySource);
|
|
210
|
+
if (!entryManifest.manifest || typeof entryManifest.manifest !== 'object') {
|
|
211
|
+
throw new Error(`${entryPath}: manifest object is required`);
|
|
212
|
+
}
|
|
213
|
+
const registryPath = join(uiRoot, 'screen-registry.ts');
|
|
214
|
+
const imports = registryImports
|
|
215
|
+
.map((item, index) => `import screen${index} from '${moduleSpecifier(uiRoot, item.file)}';`)
|
|
216
|
+
.join('\n');
|
|
217
|
+
const properties = registryImports
|
|
218
|
+
.map((item, index) => ` ${propertyKey(item.screen)}: screen${index},`)
|
|
219
|
+
.join('\n');
|
|
220
|
+
const registryContents = surfaces.length === 0
|
|
221
|
+
? undefined
|
|
222
|
+
: `// Generated by @keemakr/ui-sdk. Do not edit.\n${imports}\n\nexport const SCREENS = {\n${properties}\n} as const;\n`;
|
|
223
|
+
const previousScreens = Array.isArray(entryManifest.manifest.screenRegistry)
|
|
224
|
+
? entryManifest.manifest.screenRegistry.flatMap((item) => item && typeof item === 'object' && 'screen' in item && typeof item.screen === 'string'
|
|
225
|
+
? [item.screen]
|
|
226
|
+
: [])
|
|
227
|
+
: [];
|
|
228
|
+
const currentScreens = new Set(manifest.screenRegistry.map((item) => item.screen));
|
|
229
|
+
const meta = entryManifest.manifest.meta && typeof entryManifest.manifest.meta === 'object'
|
|
230
|
+
? entryManifest.manifest.meta
|
|
231
|
+
: {};
|
|
232
|
+
const removedScreens = [
|
|
233
|
+
...new Set([
|
|
234
|
+
...stringArray(meta[REMOVED_SCREENS_META_KEY]),
|
|
235
|
+
...previousScreens.filter((screen) => !currentScreens.has(screen)),
|
|
236
|
+
]),
|
|
237
|
+
]
|
|
238
|
+
.filter((screen) => !currentScreens.has(screen))
|
|
239
|
+
.sort();
|
|
240
|
+
if (surfaces.length === 0) {
|
|
241
|
+
delete entryManifest.manifest.surfaces;
|
|
242
|
+
delete entryManifest.manifest.screenRegistry;
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
entryManifest.manifest.surfaces = manifest.surfaces;
|
|
246
|
+
entryManifest.manifest.screenRegistry = manifest.screenRegistry;
|
|
247
|
+
}
|
|
248
|
+
if (removedScreens.length > 0) {
|
|
249
|
+
entryManifest.manifest.meta = { ...meta, [REMOVED_SCREENS_META_KEY]: removedScreens };
|
|
250
|
+
}
|
|
251
|
+
else if (REMOVED_SCREENS_META_KEY in meta) {
|
|
252
|
+
const remainingMeta = { ...meta };
|
|
253
|
+
delete remainingMeta[REMOVED_SCREENS_META_KEY];
|
|
254
|
+
entryManifest.manifest.meta = remainingMeta;
|
|
255
|
+
}
|
|
256
|
+
const entryContents = isDeepStrictEqual(entryManifest, originalEntryManifest)
|
|
257
|
+
? entrySource
|
|
258
|
+
: `${JSON.stringify(entryManifest, null, 2)}\n`;
|
|
259
|
+
await replaceGeneratedOutputs([
|
|
260
|
+
{ path: registryPath, contents: registryContents },
|
|
261
|
+
{ path: entryPath, contents: entryContents },
|
|
262
|
+
]);
|
|
263
|
+
return { manifest, registryPath, entryPath, removedScreens };
|
|
264
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { watch } from 'node:fs';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { buildModuleUi } from './build.js';
|
|
6
|
+
async function build() {
|
|
7
|
+
const result = await buildModuleUi();
|
|
8
|
+
if (result.removedScreens.length > 0) {
|
|
9
|
+
console.warn(`Warning: removed screen identities: ${result.removedScreens.join(', ')}. Treat path renames as breaking changes.`);
|
|
10
|
+
}
|
|
11
|
+
console.log(`Built ${result.manifest.surfaces.length} UI surface(s) and ${result.manifest.screenRegistry.length} screen(s).`);
|
|
12
|
+
}
|
|
13
|
+
function createRebuildScheduler() {
|
|
14
|
+
let buildInFlight;
|
|
15
|
+
let dirty = false;
|
|
16
|
+
return () => {
|
|
17
|
+
if (buildInFlight) {
|
|
18
|
+
dirty = true;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
buildInFlight = (async () => {
|
|
22
|
+
do {
|
|
23
|
+
dirty = false;
|
|
24
|
+
try {
|
|
25
|
+
await build();
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
console.error(error instanceof Error ? error.message : error);
|
|
29
|
+
}
|
|
30
|
+
} while (dirty);
|
|
31
|
+
})();
|
|
32
|
+
void buildInFlight.finally(() => {
|
|
33
|
+
buildInFlight = undefined;
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function createUiWatcher(onChange) {
|
|
38
|
+
const directory = join(process.cwd(), 'ui');
|
|
39
|
+
try {
|
|
40
|
+
return watch(directory, { recursive: true }, (_event, filename) => {
|
|
41
|
+
if (!filename || filename.endsWith('screen-registry.ts'))
|
|
42
|
+
return;
|
|
43
|
+
onChange();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
48
|
+
throw new Error(`Unable to recursively watch ${directory}. keemakr-ui dev requires Node >=20.9 with recursive fs.watch support on this platform. Upgrade Node or run \"keemakr-ui build\" after UI changes. (${detail})`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function dev(command) {
|
|
52
|
+
await build();
|
|
53
|
+
let timer;
|
|
54
|
+
const requestRebuild = createRebuildScheduler();
|
|
55
|
+
const watcher = createUiWatcher(() => {
|
|
56
|
+
if (timer)
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
timer = setTimeout(() => {
|
|
59
|
+
timer = undefined;
|
|
60
|
+
requestRebuild();
|
|
61
|
+
}, 50);
|
|
62
|
+
});
|
|
63
|
+
let watcherClosed = false;
|
|
64
|
+
const closeWatcher = () => {
|
|
65
|
+
if (timer) {
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
timer = undefined;
|
|
68
|
+
}
|
|
69
|
+
if (watcherClosed)
|
|
70
|
+
return;
|
|
71
|
+
watcherClosed = true;
|
|
72
|
+
watcher.close();
|
|
73
|
+
};
|
|
74
|
+
let child;
|
|
75
|
+
watcher.once('error', (error) => {
|
|
76
|
+
closeWatcher();
|
|
77
|
+
console.error(`UI watcher stopped: ${error.message}`);
|
|
78
|
+
child?.kill('SIGTERM');
|
|
79
|
+
process.exitCode = 1;
|
|
80
|
+
});
|
|
81
|
+
const childCommand = command[0] ?? 'eve';
|
|
82
|
+
const childArgs = command.length > 0 ? command.slice(1) : ['dev'];
|
|
83
|
+
child = spawn(childCommand, childArgs, { stdio: 'inherit' });
|
|
84
|
+
const stop = (signal) => {
|
|
85
|
+
closeWatcher();
|
|
86
|
+
child?.kill(signal);
|
|
87
|
+
};
|
|
88
|
+
process.once('SIGINT', () => stop('SIGINT'));
|
|
89
|
+
process.once('SIGTERM', () => stop('SIGTERM'));
|
|
90
|
+
child.once('error', (error) => {
|
|
91
|
+
closeWatcher();
|
|
92
|
+
console.error(`Unable to start ${childCommand}: ${error.message}`);
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
});
|
|
95
|
+
child.once('exit', (code, signal) => {
|
|
96
|
+
closeWatcher();
|
|
97
|
+
process.exitCode = signal ? 1 : (code ?? 1);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
async function main() {
|
|
101
|
+
const [command = 'build', ...args] = process.argv.slice(2);
|
|
102
|
+
if (command === 'build') {
|
|
103
|
+
await build();
|
|
104
|
+
}
|
|
105
|
+
else if (command === 'dev') {
|
|
106
|
+
const separator = args.indexOf('--');
|
|
107
|
+
await dev(separator === -1 ? args : args.slice(separator + 1));
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
throw new Error('Usage: keemakr-ui build | keemakr-ui dev -- eve dev');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
main().catch((error) => {
|
|
114
|
+
console.error(error instanceof Error ? error.message : error);
|
|
115
|
+
process.exitCode = 1;
|
|
116
|
+
});
|
package/dist/define.d.ts
CHANGED
|
@@ -1,19 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* defineScreen — the module-dev half of the contract.
|
|
3
|
-
*
|
|
4
|
-
* A screen is two things: a `load` that returns a ScreenTree, and named
|
|
5
|
-
* `actions` the tree's buttons/forms reference by key. Handlers run
|
|
6
|
-
* server-side in the module's own eve deployment; keemakr-core calls them
|
|
7
|
-
* through the grant-authed screens channel and validates every response
|
|
8
|
-
* against the schemas in ./schema before rendering.
|
|
9
|
-
*
|
|
10
|
-
* `Ctx` is generic on purpose: this package depends on nothing. The eve
|
|
11
|
-
* channel adapter (shipped alongside the reference module) binds Ctx to the
|
|
12
|
-
* eve tool ctx, so inside handlers `useKee(ctx)` works exactly as it does in
|
|
13
|
-
* agent tools — tenant and scopes come from the verified grant, never from
|
|
14
|
-
* params.
|
|
15
|
-
*/
|
|
16
|
-
import type { ActionResultInput, ScreenTreeInput } from './schema.js';
|
|
1
|
+
import type { ActionResultInput, ScreenTreeInput, SurfaceDefinition } from './schema.js';
|
|
17
2
|
export type ScreenParams = Record<string, string>;
|
|
18
3
|
export type LoadHandler<Ctx> = (ctx: Ctx, params: ScreenParams) => ScreenTreeInput | Promise<ScreenTreeInput>;
|
|
19
4
|
export type ActionHandler<Ctx> = (ctx: Ctx, input: Record<string, unknown>, params: ScreenParams) => ActionResultInput | Promise<ActionResultInput>;
|
|
@@ -26,3 +11,5 @@ export interface ScreenDefinition<Ctx = unknown> {
|
|
|
26
11
|
* cheap authoring-time validation, not runtime behavior.
|
|
27
12
|
*/
|
|
28
13
|
export declare function defineScreen<Ctx = unknown>(screen: ScreenDefinition<Ctx>): ScreenDefinition<Ctx>;
|
|
14
|
+
/** Define surface metadata. The filesystem supplies its key, root screen, and child ownership. */
|
|
15
|
+
export declare function defineSurface(surface: SurfaceDefinition): SurfaceDefinition;
|
package/dist/define.js
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* defineScreen — the module-dev half of the contract.
|
|
3
|
+
*
|
|
4
|
+
* A screen is two things: a `load` that returns a ScreenTree, and named
|
|
5
|
+
* `actions` the tree's buttons/forms reference by key. Handlers run
|
|
6
|
+
* server-side in the module's own eve deployment; keemakr-core calls them
|
|
7
|
+
* through the grant-authed screens channel and validates every response
|
|
8
|
+
* against the schemas in ./schema before rendering.
|
|
9
|
+
*
|
|
10
|
+
* `Ctx` is generic on purpose: this package depends on nothing. The eve
|
|
11
|
+
* channel adapter (shipped alongside the reference module) binds Ctx to the
|
|
12
|
+
* eve tool ctx, so inside handlers `useKee(ctx)` works exactly as it does in
|
|
13
|
+
* agent tools — tenant and scopes come from the verified grant, never from
|
|
14
|
+
* params.
|
|
15
|
+
*/
|
|
16
|
+
import { surfaceDefinitionSchema } from './schema.js';
|
|
1
17
|
const ACTION_KEY = /^[a-z][a-z0-9_-]*$/;
|
|
2
18
|
/**
|
|
3
19
|
* Identity function (the defineProvider pattern): exists for inference and
|
|
@@ -11,3 +27,8 @@ export function defineScreen(screen) {
|
|
|
11
27
|
}
|
|
12
28
|
return screen;
|
|
13
29
|
}
|
|
30
|
+
/** Define surface metadata. The filesystem supplies its key, root screen, and child ownership. */
|
|
31
|
+
export function defineSurface(surface) {
|
|
32
|
+
surfaceDefinitionSchema.parse(surface);
|
|
33
|
+
return surface;
|
|
34
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { UI_SCHEMA_VERSION, pressSchema, buttonSchema, cellSchema, nodeSchema, screenTreeSchema, actionResultSchema, screenLoadRequestSchema, screenLoadResponseSchema, screenActionRequestSchema, surfaceSchema, } from './schema.js';
|
|
2
|
-
export type { Press, ScreenNode, ScreenTree, ActionResult, Surface, ScreenTreeInput, ActionResultInput, } from './schema.js';
|
|
3
|
-
export { defineScreen } from './define.js';
|
|
1
|
+
export { UI_SCHEMA_VERSION, pressSchema, buttonSchema, cellSchema, nodeSchema, screenTreeSchema, actionResultSchema, screenLoadRequestSchema, screenLoadResponseSchema, screenActionRequestSchema, interfaceFamilySchema, surfaceDefinitionSchema, surfaceSchema, screenRegistrationSchema, uiManifestSchema, } from './schema.js';
|
|
2
|
+
export type { Press, ScreenNode, ScreenTree, ActionResult, InterfaceFamily, SurfaceDefinition, Surface, ScreenRegistration, UiManifest, ScreenTreeInput, ActionResultInput, } from './schema.js';
|
|
3
|
+
export { defineScreen, defineSurface } from './define.js';
|
|
4
4
|
export type { ScreenDefinition, ScreenParams, LoadHandler, ActionHandler } from './define.js';
|
package/dist/index.js
CHANGED
|
@@ -6,5 +6,5 @@
|
|
|
6
6
|
// platform components, proxies load/action with a grant
|
|
7
7
|
//
|
|
8
8
|
// Module code never ships to the browser. UI is data, not code.
|
|
9
|
-
export { UI_SCHEMA_VERSION, pressSchema, buttonSchema, cellSchema, nodeSchema, screenTreeSchema, actionResultSchema, screenLoadRequestSchema, screenLoadResponseSchema, screenActionRequestSchema, surfaceSchema, } from './schema.js';
|
|
10
|
-
export { defineScreen } from './define.js';
|
|
9
|
+
export { UI_SCHEMA_VERSION, pressSchema, buttonSchema, cellSchema, nodeSchema, screenTreeSchema, actionResultSchema, screenLoadRequestSchema, screenLoadResponseSchema, screenActionRequestSchema, interfaceFamilySchema, surfaceDefinitionSchema, surfaceSchema, screenRegistrationSchema, uiManifestSchema, } from './schema.js';
|
|
10
|
+
export { defineScreen, defineSurface } from './define.js';
|
package/dist/schema.d.ts
CHANGED
|
@@ -302,22 +302,65 @@ export declare const screenActionRequestSchema: z.ZodObject<{
|
|
|
302
302
|
input: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
303
303
|
idempotencyKey: z.ZodOptional<z.ZodString>;
|
|
304
304
|
}, z.core.$strip>;
|
|
305
|
+
export declare const interfaceFamilySchema: z.ZodEnum<{
|
|
306
|
+
dashboard: "dashboard";
|
|
307
|
+
client: "client";
|
|
308
|
+
}>;
|
|
309
|
+
export declare const surfaceDefinitionSchema: z.ZodObject<{
|
|
310
|
+
label: z.ZodString;
|
|
311
|
+
icon: z.ZodOptional<z.ZodString>;
|
|
312
|
+
description: z.ZodOptional<z.ZodString>;
|
|
313
|
+
availableOn: z.ZodArray<z.ZodEnum<{
|
|
314
|
+
dashboard: "dashboard";
|
|
315
|
+
client: "client";
|
|
316
|
+
}>>;
|
|
317
|
+
}, z.core.$strip>;
|
|
305
318
|
/**
|
|
306
319
|
* Manifest addition: `manifest.surfaces[]` in entry.json. Each surface is a
|
|
307
320
|
* side-menu entry pointing at an entry screen. Core's marketplace
|
|
308
321
|
* validateSubmission composes this in (wired in the proxy phase).
|
|
309
322
|
*/
|
|
310
323
|
export declare const surfaceSchema: z.ZodObject<{
|
|
311
|
-
key: z.ZodString;
|
|
312
324
|
label: z.ZodString;
|
|
313
325
|
icon: z.ZodOptional<z.ZodString>;
|
|
314
|
-
screen: z.ZodString;
|
|
315
326
|
description: z.ZodOptional<z.ZodString>;
|
|
327
|
+
availableOn: z.ZodArray<z.ZodEnum<{
|
|
328
|
+
dashboard: "dashboard";
|
|
329
|
+
client: "client";
|
|
330
|
+
}>>;
|
|
331
|
+
key: z.ZodString;
|
|
332
|
+
screen: z.ZodString;
|
|
333
|
+
}, z.core.$strip>;
|
|
334
|
+
export declare const screenRegistrationSchema: z.ZodObject<{
|
|
335
|
+
screen: z.ZodString;
|
|
336
|
+
surface: z.ZodString;
|
|
337
|
+
}, z.core.$strip>;
|
|
338
|
+
/** The complete generated UI portion of a module manifest. */
|
|
339
|
+
export declare const uiManifestSchema: z.ZodObject<{
|
|
340
|
+
surfaces: z.ZodArray<z.ZodObject<{
|
|
341
|
+
label: z.ZodString;
|
|
342
|
+
icon: z.ZodOptional<z.ZodString>;
|
|
343
|
+
description: z.ZodOptional<z.ZodString>;
|
|
344
|
+
availableOn: z.ZodArray<z.ZodEnum<{
|
|
345
|
+
dashboard: "dashboard";
|
|
346
|
+
client: "client";
|
|
347
|
+
}>>;
|
|
348
|
+
key: z.ZodString;
|
|
349
|
+
screen: z.ZodString;
|
|
350
|
+
}, z.core.$strip>>;
|
|
351
|
+
screenRegistry: z.ZodArray<z.ZodObject<{
|
|
352
|
+
screen: z.ZodString;
|
|
353
|
+
surface: z.ZodString;
|
|
354
|
+
}, z.core.$strip>>;
|
|
316
355
|
}, z.core.$strip>;
|
|
317
356
|
export type Press = z.infer<typeof pressSchema>;
|
|
318
357
|
export type ScreenTree = z.infer<typeof screenTreeSchema>;
|
|
319
358
|
export type ActionResult = z.infer<typeof actionResultSchema>;
|
|
359
|
+
export type InterfaceFamily = z.infer<typeof interfaceFamilySchema>;
|
|
360
|
+
export type SurfaceDefinition = z.infer<typeof surfaceDefinitionSchema>;
|
|
320
361
|
export type Surface = z.infer<typeof surfaceSchema>;
|
|
362
|
+
export type ScreenRegistration = z.infer<typeof screenRegistrationSchema>;
|
|
363
|
+
export type UiManifest = z.infer<typeof uiManifestSchema>;
|
|
321
364
|
export type ScreenTreeInput = z.input<typeof screenTreeSchema>;
|
|
322
365
|
export type ActionResultInput = z.input<typeof actionResultSchema>;
|
|
323
366
|
export {};
|
package/dist/schema.js
CHANGED
|
@@ -235,16 +235,76 @@ export const screenActionRequestSchema = z.object({
|
|
|
235
235
|
* half of at-least-once). Optional for runtime version-skew tolerance. */
|
|
236
236
|
idempotencyKey: z.string().max(200).optional(),
|
|
237
237
|
});
|
|
238
|
+
export const interfaceFamilySchema = z.enum(['dashboard', 'client']);
|
|
239
|
+
export const surfaceDefinitionSchema = z.object({
|
|
240
|
+
label: z.string().min(1).max(60),
|
|
241
|
+
/** Lucide icon name, same convention as entry.json `icon`. */
|
|
242
|
+
icon: z.string().max(60).optional(),
|
|
243
|
+
description: z.string().max(200).optional(),
|
|
244
|
+
availableOn: z
|
|
245
|
+
.array(interfaceFamilySchema)
|
|
246
|
+
.min(1)
|
|
247
|
+
.max(2)
|
|
248
|
+
.refine((families) => new Set(families).size === families.length, {
|
|
249
|
+
message: 'interface families must be unique',
|
|
250
|
+
}),
|
|
251
|
+
});
|
|
238
252
|
/**
|
|
239
253
|
* Manifest addition: `manifest.surfaces[]` in entry.json. Each surface is a
|
|
240
254
|
* side-menu entry pointing at an entry screen. Core's marketplace
|
|
241
255
|
* validateSubmission composes this in (wired in the proxy phase).
|
|
242
256
|
*/
|
|
243
|
-
export const surfaceSchema =
|
|
257
|
+
export const surfaceSchema = surfaceDefinitionSchema.extend({
|
|
244
258
|
key: slug,
|
|
245
|
-
label: z.string().max(60),
|
|
246
|
-
/** Lucide icon name, same convention as entry.json `icon`. */
|
|
247
|
-
icon: z.string().max(60).optional(),
|
|
248
259
|
screen: slug,
|
|
249
|
-
|
|
260
|
+
});
|
|
261
|
+
export const screenRegistrationSchema = z.object({
|
|
262
|
+
screen: slug,
|
|
263
|
+
surface: slug,
|
|
264
|
+
});
|
|
265
|
+
/** The complete generated UI portion of a module manifest. */
|
|
266
|
+
export const uiManifestSchema = z
|
|
267
|
+
.object({
|
|
268
|
+
surfaces: z.array(surfaceSchema).min(1),
|
|
269
|
+
screenRegistry: z.array(screenRegistrationSchema).min(1).max(256),
|
|
270
|
+
})
|
|
271
|
+
.superRefine((manifest, ctx) => {
|
|
272
|
+
const surfaceKeys = new Set();
|
|
273
|
+
for (const [index, surface] of manifest.surfaces.entries()) {
|
|
274
|
+
if (surfaceKeys.has(surface.key)) {
|
|
275
|
+
ctx.addIssue({
|
|
276
|
+
code: 'custom',
|
|
277
|
+
path: ['surfaces', index, 'key'],
|
|
278
|
+
message: `duplicate surface key "${surface.key}"`,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
surfaceKeys.add(surface.key);
|
|
282
|
+
}
|
|
283
|
+
const screens = new Map();
|
|
284
|
+
for (const [index, registration] of manifest.screenRegistry.entries()) {
|
|
285
|
+
if (!surfaceKeys.has(registration.surface)) {
|
|
286
|
+
ctx.addIssue({
|
|
287
|
+
code: 'custom',
|
|
288
|
+
path: ['screenRegistry', index, 'surface'],
|
|
289
|
+
message: `unknown surface "${registration.surface}"`,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
if (screens.has(registration.screen)) {
|
|
293
|
+
ctx.addIssue({
|
|
294
|
+
code: 'custom',
|
|
295
|
+
path: ['screenRegistry', index, 'screen'],
|
|
296
|
+
message: `duplicate screen identity "${registration.screen}"`,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
screens.set(registration.screen, registration.surface);
|
|
300
|
+
}
|
|
301
|
+
for (const [index, surface] of manifest.surfaces.entries()) {
|
|
302
|
+
if (screens.get(surface.screen) !== surface.key) {
|
|
303
|
+
ctx.addIssue({
|
|
304
|
+
code: 'custom',
|
|
305
|
+
path: ['surfaces', index, 'screen'],
|
|
306
|
+
message: `root screen "${surface.screen}" must be registered to surface "${surface.key}"`,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
250
310
|
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Campaigns example
|
|
2
|
+
|
|
3
|
+
This example has:
|
|
4
|
+
|
|
5
|
+
- a Campaigns surface available in Dashboard and Kee clients;
|
|
6
|
+
- a nested campaign detail screen that inherits that availability;
|
|
7
|
+
- a separate Campaign settings surface available only in Dashboard;
|
|
8
|
+
- one in-memory pause action.
|
|
9
|
+
|
|
10
|
+
From this directory, generate the manifest and registry, then run the demo:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx keemakr-ui build
|
|
14
|
+
npx tsx demo.ts
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
No database or external service is required.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { SCREENS } from './ui/screen-registry.js';
|
|
2
|
+
|
|
3
|
+
async function main() {
|
|
4
|
+
const params = { id: 'summer-launch' };
|
|
5
|
+
console.log(await SCREENS.campaign.load({}, params));
|
|
6
|
+
console.log(await SCREENS.campaign.actions?.pause?.({}, {}, params));
|
|
7
|
+
console.log(await SCREENS.campaign.load({}, params));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
void main().catch((error: unknown) => {
|
|
11
|
+
console.error(error);
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"slug": "campaigns-example",
|
|
3
|
+
"display_name": "Campaigns example",
|
|
4
|
+
"visibility": "public",
|
|
5
|
+
"runtime": {
|
|
6
|
+
"kind": "eve",
|
|
7
|
+
"url": "http://localhost:3210",
|
|
8
|
+
"status": "dev"
|
|
9
|
+
},
|
|
10
|
+
"manifest": {
|
|
11
|
+
"department": {
|
|
12
|
+
"key": "sales",
|
|
13
|
+
"label": "Sales"
|
|
14
|
+
},
|
|
15
|
+
"agents": [
|
|
16
|
+
{
|
|
17
|
+
"key": "campaign-manager",
|
|
18
|
+
"display_name": "Campaign manager"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"surfaces": [
|
|
22
|
+
{
|
|
23
|
+
"label": "Campaign settings",
|
|
24
|
+
"description": "Configure tenant campaign defaults.",
|
|
25
|
+
"availableOn": ["dashboard"],
|
|
26
|
+
"key": "campaign-settings",
|
|
27
|
+
"screen": "campaign-settings"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"label": "Campaigns",
|
|
31
|
+
"description": "Create and monitor campaigns.",
|
|
32
|
+
"availableOn": ["dashboard", "client"],
|
|
33
|
+
"key": "campaigns",
|
|
34
|
+
"screen": "campaigns"
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"screenRegistry": [
|
|
38
|
+
{
|
|
39
|
+
"screen": "campaign",
|
|
40
|
+
"surface": "campaigns"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"screen": "campaign-settings",
|
|
44
|
+
"surface": "campaign-settings"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"screen": "campaigns",
|
|
48
|
+
"surface": "campaigns"
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
"dependencies": []
|
|
53
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { defineScreen } from '@keemakr/ui-sdk';
|
|
2
|
+
import { campaigns } from '../../data.js';
|
|
3
|
+
|
|
4
|
+
export default defineScreen({
|
|
5
|
+
load: async (_ctx, params) => {
|
|
6
|
+
const campaign = campaigns.find((item) => item.id === params.id);
|
|
7
|
+
return {
|
|
8
|
+
version: 1,
|
|
9
|
+
title: campaign?.name ?? 'Campaign not found',
|
|
10
|
+
children: campaign
|
|
11
|
+
? [
|
|
12
|
+
{ type: 'text' as const, value: `Status: ${campaign.status}` },
|
|
13
|
+
{
|
|
14
|
+
type: 'button' as const,
|
|
15
|
+
label: 'Pause campaign',
|
|
16
|
+
onPress: { kind: 'action' as const, action: 'pause' },
|
|
17
|
+
},
|
|
18
|
+
]
|
|
19
|
+
: [{ type: 'text' as const, value: 'This campaign does not exist.' }],
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
actions: {
|
|
23
|
+
pause: async (_ctx, _input, params) => {
|
|
24
|
+
const campaign = campaigns.find((item) => item.id === params.id);
|
|
25
|
+
if (campaign) campaign.status = 'paused';
|
|
26
|
+
return { toast: campaign ? 'Campaign paused.' : 'Campaign not found.', refresh: true };
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { defineScreen } from '@keemakr/ui-sdk';
|
|
2
|
+
import { campaigns } from '../data.js';
|
|
3
|
+
|
|
4
|
+
export default defineScreen({
|
|
5
|
+
load: async () => ({
|
|
6
|
+
version: 1,
|
|
7
|
+
title: 'Campaigns',
|
|
8
|
+
children: [
|
|
9
|
+
{
|
|
10
|
+
type: 'table',
|
|
11
|
+
columns: [
|
|
12
|
+
{ key: 'name', label: 'Campaign' },
|
|
13
|
+
{ key: 'status', label: 'Status' },
|
|
14
|
+
],
|
|
15
|
+
rows: campaigns.map((campaign) => ({
|
|
16
|
+
key: campaign.id,
|
|
17
|
+
cells: {
|
|
18
|
+
name: { kind: 'text', value: campaign.name },
|
|
19
|
+
status: { kind: 'badge', value: campaign.status },
|
|
20
|
+
},
|
|
21
|
+
onPress: { kind: 'navigate', screen: 'campaign', params: { id: campaign.id } },
|
|
22
|
+
})),
|
|
23
|
+
empty: 'No campaigns yet.',
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
}),
|
|
27
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Generated by @keemakr/ui-sdk. Do not edit.
|
|
2
|
+
import screen0 from './campaigns/campaign/screen.js';
|
|
3
|
+
import screen1 from './campaign-settings/screen.js';
|
|
4
|
+
import screen2 from './campaigns/screen.js';
|
|
5
|
+
|
|
6
|
+
export const SCREENS = {
|
|
7
|
+
campaign: screen0,
|
|
8
|
+
'campaign-settings': screen1,
|
|
9
|
+
campaigns: screen2,
|
|
10
|
+
} as const;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keemakr/ui-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-dev-20260807074219",
|
|
4
4
|
"description": "Product screens for keemakr marketplace modules: a versioned, server-driven UI tree schema plus defineScreen() handlers. Module code never ships to the browser — keemakr-core validates the tree and renders it with platform components.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -11,11 +11,20 @@
|
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
12
|
"import": "./dist/index.js",
|
|
13
13
|
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./build": {
|
|
16
|
+
"types": "./dist/build.d.ts",
|
|
17
|
+
"import": "./dist/build.js",
|
|
18
|
+
"default": "./dist/build.js"
|
|
14
19
|
}
|
|
15
20
|
},
|
|
21
|
+
"bin": {
|
|
22
|
+
"keemakr-ui": "./dist/cli.js"
|
|
23
|
+
},
|
|
16
24
|
"files": [
|
|
17
25
|
"dist",
|
|
18
|
-
"README.md"
|
|
26
|
+
"README.md",
|
|
27
|
+
"examples"
|
|
19
28
|
],
|
|
20
29
|
"publishConfig": {
|
|
21
30
|
"access": "public"
|
|
@@ -33,9 +42,11 @@
|
|
|
33
42
|
"peerDependencies": {
|
|
34
43
|
"zod": "^4.4.3"
|
|
35
44
|
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"typescript": "^5"
|
|
47
|
+
},
|
|
36
48
|
"devDependencies": {
|
|
37
49
|
"@types/node": "^20.19.43",
|
|
38
|
-
"typescript": "^5",
|
|
39
50
|
"zod": "^4.4.3"
|
|
40
51
|
},
|
|
41
52
|
"engines": {
|