@sempods/apps 0.7.1 → 0.8.0-dev.1
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/CHANGELOG.md +190 -0
- package/README.md +6 -3
- package/bin/sempods-apps.mjs +1 -0
- package/instructions/app-workflow.md +254 -111
- package/instructions/publish.md +13 -7
- package/instructions/setup.md +33 -23
- package/instructions/visual-check.md +43 -0
- package/package.json +3 -3
- package/scripts/build-site.mjs +15 -3
- package/scripts/check.mjs +3 -1
- package/scripts/icon.mjs +47 -0
- package/scripts/lib/apps.mjs +56 -2
- package/scripts/lib/generate.mjs +55 -14
- package/scripts/lib/icon.mjs +192 -0
- package/scripts/lib/overview.mjs +5 -3
- package/scripts/lib/page.mjs +89 -0
- package/scripts/lib/themes.mjs +58 -0
- package/scripts/migrate.mjs +37 -14
- package/scripts/new-app.mjs +43 -10
- package/scripts/sdk-update.mjs +16 -1
- package/shared/files/{f2fb601d5ebc4ca72274ac9d3fcf7b7a96e27958bb92cd065a9165b501ee94d7 → 44c114d4d2c8b172f4757100f79e8a1a00d30c96ba2ad8f75c3373e52f6b849f} +2 -2
- package/shared/files/{31ede0d1a035d8531100ebfd751510c772ab0871309a0b9752a77fb96187eb53 → 843c078e84912158607ebb8ce0042589d676a742bcd3cf8b6c474d62ff3a6845} +3 -2
- package/shared/files/{1c29eed0a0297c742ca5814de9081ff8efce3b2da5d9e1bdf90a403873da4aa9 → f30ec6b7f9de16be54919382ab18cdcc33ee64938f7b907d855bcf6740e4c102} +8 -2
- package/shared/snapshot.json +4 -4
- package/skeleton/app/NOTES.md +11 -1
- package/skeleton/app/index.html +7 -3
- package/skeleton/app/package.json +6 -6
- package/skeleton/app/src/App.tsx +53 -21
- package/skeleton/app/src/NewVersionNotice.tsx +6 -11
- package/skeleton/app/src/examples/ListScreen.tsx +167 -0
- package/skeleton/app/src/index.css +86 -13
- package/skeleton/app/src/main.tsx +3 -0
- package/skeleton/app/src/pwa.ts +1 -1
- package/skeleton/app/src/routes.test.ts +19 -0
- package/skeleton/app/src/routes.ts +139 -0
- package/skeleton/app/src/texts.test.ts +28 -0
- package/skeleton/app/src/texts.ts +35 -0
- package/skeleton/app/src/themes/calm.css +11 -0
- package/skeleton/app/src/themes/fresh.css +11 -0
- package/skeleton/app/src/themes/playful.css +11 -0
- package/skeleton/app/src/themes/warm.css +11 -0
- package/skeleton/app/src/ui/Composer.tsx +18 -0
- package/skeleton/app/src/ui/ContextSwitch.tsx +85 -0
- package/skeleton/app/src/ui/Dialog.tsx +103 -0
- package/skeleton/app/src/ui/Menu.tsx +118 -0
- package/skeleton/app/src/ui/Tabs.tsx +39 -0
- package/skeleton/app/src/ui/dismiss.ts +30 -0
- package/skeleton/app/src/ui/icons.tsx +53 -0
- package/skeleton/app/src/ui/ui.css +240 -0
- package/skeleton/app/vite.config.ts +4 -4
- package/skills/update/SKILL.md +4 -2
- package/skeleton/app/public/favicon.svg +0 -1
- package/skeleton/app/public/icon-192.png +0 -0
- package/skeleton/app/public/icon-512.png +0 -0
- package/skeleton/app/src/start.test.ts +0 -15
- package/skeleton/app/src/start.ts +0 -9
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// The skeleton's themes: one CSS file per theme in src/themes/, which sets the
|
|
2
|
+
// --sempods-* tokens in light and dark mode. An app imports one of them in its
|
|
3
|
+
// src/main.tsx; its icon takes that theme's accent.
|
|
4
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { ifPresent } from './json.mjs';
|
|
7
|
+
import { SKELETON } from './paths.mjs';
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_THEME = 'calm';
|
|
10
|
+
const THEMES_DIR = join('src', 'themes');
|
|
11
|
+
const IMPORT = /^import '\.\/themes\/([a-z-]+)\.css';$/m;
|
|
12
|
+
|
|
13
|
+
/** The theme names the skeleton ships, the default first. */
|
|
14
|
+
export function themeNames(skeleton = SKELETON) {
|
|
15
|
+
const names = readdirSync(join(skeleton, THEMES_DIR))
|
|
16
|
+
.filter((file) => file.endsWith('.css'))
|
|
17
|
+
.map((file) => file.slice(0, -'.css'.length))
|
|
18
|
+
.sort();
|
|
19
|
+
return [DEFAULT_THEME, ...names.filter((name) => name !== DEFAULT_THEME)];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A token's light and dark colour, from `--name: light-dark(#light, #dark)`. */
|
|
23
|
+
export function tokenColours(css, token) {
|
|
24
|
+
const match = css.match(
|
|
25
|
+
new RegExp(
|
|
26
|
+
`--sempods-${token}:\\s*light-dark\\((#[0-9a-f]{6}),\\s*(#[0-9a-f]{6})\\)`,
|
|
27
|
+
'i',
|
|
28
|
+
),
|
|
29
|
+
);
|
|
30
|
+
if (!match) throw new Error(`the theme sets no --sempods-${token}`);
|
|
31
|
+
return { light: match[1].toLowerCase(), dark: match[2].toLowerCase() };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The icon's colours for a theme file: the accent as the square and the text
|
|
36
|
+
* on the accent as the letter, in light and dark mode.
|
|
37
|
+
*/
|
|
38
|
+
export function iconColours(css) {
|
|
39
|
+
const accent = tokenColours(css, 'accent');
|
|
40
|
+
const on = tokenColours(css, 'on-accent');
|
|
41
|
+
return {
|
|
42
|
+
light: { background: accent.light, letter: on.light },
|
|
43
|
+
dark: { background: accent.dark, letter: on.dark },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The theme an app imports in src/main.tsx, if it imports one of these. */
|
|
48
|
+
export function appTheme(appDir) {
|
|
49
|
+
const main = ifPresent(() =>
|
|
50
|
+
readFileSync(join(appDir, 'src', 'main.tsx'), 'utf8'),
|
|
51
|
+
);
|
|
52
|
+
return main?.match(IMPORT)?.[1];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The CSS of a theme file in an app or in the skeleton. */
|
|
56
|
+
export function themeCss(dir, name) {
|
|
57
|
+
return readFileSync(join(dir, THEMES_DIR, `${name}.css`), 'utf8');
|
|
58
|
+
}
|
package/scripts/migrate.mjs
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
replaceFile,
|
|
33
33
|
} from './lib/files.mjs';
|
|
34
34
|
import { GENERATED_FILES } from './lib/generate.mjs';
|
|
35
|
+
import { adoptPageTitle, PAGE, VITE_CONFIG } from './lib/page.mjs';
|
|
35
36
|
import { formatJson, ifPresent, readJson, replaceJson } from './lib/json.mjs';
|
|
36
37
|
import { mergeManifest, mergeText, withSection } from './lib/merge.mjs';
|
|
37
38
|
import { repositoryRoot, TOOLING } from './lib/paths.mjs';
|
|
@@ -73,21 +74,23 @@ export function refuseUnfinishedSdkUpdate(root) {
|
|
|
73
74
|
);
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
// A broken apps.json names no apps here; regenerating reports it later.
|
|
78
|
+
function appsOf(root) {
|
|
79
|
+
try {
|
|
80
|
+
return readApps(root).apps;
|
|
81
|
+
} catch {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
76
86
|
/**
|
|
77
87
|
* Symbolic links where an update writes: the command's own files, the apps'
|
|
78
|
-
* generated configuration and the given starter
|
|
79
|
-
* ancestors, and anything inside the migration folder.
|
|
88
|
+
* generated configuration, page and Vite configuration and the given starter
|
|
89
|
+
* paths, each with its ancestors, and anything inside the migration folder.
|
|
80
90
|
*/
|
|
81
91
|
export function linkedPaths(root, files = []) {
|
|
82
92
|
const found = new Set();
|
|
83
|
-
|
|
84
|
-
const apps = (() => {
|
|
85
|
-
try {
|
|
86
|
-
return readApps(root).apps;
|
|
87
|
-
} catch {
|
|
88
|
-
return [];
|
|
89
|
-
}
|
|
90
|
-
})();
|
|
93
|
+
const apps = appsOf(root);
|
|
91
94
|
for (const path of [
|
|
92
95
|
'package.json',
|
|
93
96
|
'pnpm-lock.yaml',
|
|
@@ -95,7 +98,9 @@ export function linkedPaths(root, files = []) {
|
|
|
95
98
|
BASELINE_FILE,
|
|
96
99
|
MIGRATION_DIR,
|
|
97
100
|
...apps.flatMap(({ id }) =>
|
|
98
|
-
GENERATED_FILES.map(
|
|
101
|
+
[...GENERATED_FILES, PAGE, VITE_CONFIG].map(
|
|
102
|
+
(file) => `apps/${id}/${file}`,
|
|
103
|
+
),
|
|
99
104
|
),
|
|
100
105
|
...files,
|
|
101
106
|
]) {
|
|
@@ -372,6 +377,8 @@ export function migrate(
|
|
|
372
377
|
if (baseline.revision === target.snapshot.revision) {
|
|
373
378
|
// A leftover of a completed migration.
|
|
374
379
|
rmSync(work, { recursive: true, force: true });
|
|
380
|
+
// The tooling may still have changed what it generates for apps.
|
|
381
|
+
updateApps(root, report);
|
|
375
382
|
return { ...report, unchanged: true };
|
|
376
383
|
}
|
|
377
384
|
}
|
|
@@ -479,7 +486,7 @@ export function migrate(
|
|
|
479
486
|
throw new Error(
|
|
480
487
|
`${unresolved.map(({ file }) => file).join(', ')} still contain an unresolved suggestion; resolve them, then run pnpm run migrate --done again`,
|
|
481
488
|
);
|
|
482
|
-
|
|
489
|
+
updateApps(root, report);
|
|
483
490
|
// A new starter can change the manifest or the workspace settings, so the
|
|
484
491
|
// lockfile and node_modules follow before the migration counts as done.
|
|
485
492
|
if (!install(root))
|
|
@@ -497,11 +504,27 @@ export function migrate(
|
|
|
497
504
|
return report;
|
|
498
505
|
}
|
|
499
506
|
|
|
507
|
+
/**
|
|
508
|
+
* The apps' part of a migration: files a release moves where that is
|
|
509
|
+
* unambiguous, then the generated configuration.
|
|
510
|
+
*/
|
|
511
|
+
function updateApps(root, report) {
|
|
512
|
+
for (const app of appsOf(root)) {
|
|
513
|
+
const dir = join(root, 'apps', app.id);
|
|
514
|
+
if (existsSync(dir)) report.updated.push(...adoptPageTitle(dir, app));
|
|
515
|
+
}
|
|
516
|
+
report.regenerated = regenerate(root);
|
|
517
|
+
}
|
|
518
|
+
|
|
500
519
|
export function formatMigration(report) {
|
|
501
|
-
if (report.unchanged)
|
|
502
|
-
return `The shared files already match ${PACKAGE} ${report.to}; nothing to migrate.`;
|
|
503
520
|
const list = (title, items) =>
|
|
504
521
|
items.length ? [`${title}:`, ...items.map((item) => ` ${item}`)] : [];
|
|
522
|
+
if (report.unchanged)
|
|
523
|
+
return [
|
|
524
|
+
`The shared files already match ${PACKAGE} ${report.to}; nothing to migrate.`,
|
|
525
|
+
...list('Updated', report.updated),
|
|
526
|
+
...list('Regenerated', report.regenerated),
|
|
527
|
+
].join('\n');
|
|
505
528
|
const lines = [
|
|
506
529
|
`Migration from ${PACKAGE} ${report.from} to ${report.to}${report.unfinished ? ' is open' : ' is complete'}.`,
|
|
507
530
|
...list('Updated', report.updated),
|
package/scripts/new-app.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Creates apps/<id> from the skeleton with the root's SDK version and adds it
|
|
3
3
|
// to apps.json.
|
|
4
|
-
// Usage: pnpm run new-app <id> [--title "<title>"] [--
|
|
4
|
+
// Usage: pnpm run new-app <id> [--title "<title>"] [--description "<text>"]
|
|
5
|
+
// [--language <auto|de|en>] [--theme <name>] [--no-pwa]
|
|
5
6
|
import {
|
|
6
7
|
cpSync,
|
|
7
8
|
existsSync,
|
|
9
|
+
mkdirSync,
|
|
8
10
|
readdirSync,
|
|
9
11
|
readFileSync,
|
|
10
12
|
rmSync,
|
|
@@ -14,14 +16,23 @@ import { extname, join, resolve } from 'node:path';
|
|
|
14
16
|
import { fileURLToPath } from 'node:url';
|
|
15
17
|
import { parseArgs } from 'node:util';
|
|
16
18
|
import {
|
|
19
|
+
APP_LANGUAGES,
|
|
20
|
+
AUTO,
|
|
21
|
+
invalidDescription,
|
|
17
22
|
invalidId,
|
|
18
|
-
LANGUAGES,
|
|
19
23
|
nextDevPort,
|
|
20
24
|
readApps,
|
|
21
25
|
writeApps,
|
|
22
26
|
} from './lib/apps.mjs';
|
|
23
27
|
import { writeGenerated } from './lib/generate.mjs';
|
|
24
28
|
import { escapeHtml } from './lib/html.mjs';
|
|
29
|
+
import { writeAppIcons } from './lib/icon.mjs';
|
|
30
|
+
import {
|
|
31
|
+
DEFAULT_THEME,
|
|
32
|
+
iconColours,
|
|
33
|
+
themeCss,
|
|
34
|
+
themeNames,
|
|
35
|
+
} from './lib/themes.mjs';
|
|
25
36
|
import { readJson, writeJson } from './lib/json.mjs';
|
|
26
37
|
import { repositoryRoot, SKELETON } from './lib/paths.mjs';
|
|
27
38
|
import { INSTALL, pnpm, scriptArgs } from './lib/pnpm.mjs';
|
|
@@ -29,14 +40,14 @@ import { EXACT_VERSION, SDK } from './lib/sdk.mjs';
|
|
|
29
40
|
|
|
30
41
|
const TEXT = new Set(['.json', '.html', '.md', '.ts', '.tsx', '.css']);
|
|
31
42
|
|
|
32
|
-
function fillTokens(dir, app) {
|
|
43
|
+
function fillTokens(dir, app, theme) {
|
|
33
44
|
// Directory entries carry their type, so no separate stat precedes the read
|
|
34
45
|
// and write of the same path.
|
|
35
46
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
36
47
|
const { name } = entry;
|
|
37
48
|
const path = join(dir, name);
|
|
38
49
|
if (entry.isDirectory()) {
|
|
39
|
-
fillTokens(path, app);
|
|
50
|
+
fillTokens(path, app, theme);
|
|
40
51
|
continue;
|
|
41
52
|
}
|
|
42
53
|
if (!TEXT.has(extname(name))) continue;
|
|
@@ -44,8 +55,13 @@ function fillTokens(dir, app) {
|
|
|
44
55
|
const source = readFileSync(path, 'utf8');
|
|
45
56
|
const filled = source
|
|
46
57
|
.replaceAll('__APP_ID__', app.id)
|
|
58
|
+
.replaceAll('__APP_THEME__', theme)
|
|
47
59
|
.replaceAll('__APP_TITLE__', title)
|
|
48
|
-
|
|
60
|
+
// The page's language until the app sets the one it resolved.
|
|
61
|
+
.replaceAll(
|
|
62
|
+
'__APP_LANGUAGE__',
|
|
63
|
+
app.language === AUTO ? 'en' : app.language,
|
|
64
|
+
);
|
|
49
65
|
if (filled !== source) writeFileSync(path, filled);
|
|
50
66
|
}
|
|
51
67
|
}
|
|
@@ -74,12 +90,23 @@ function rootSdk(root) {
|
|
|
74
90
|
*/
|
|
75
91
|
export function createApp(
|
|
76
92
|
root,
|
|
77
|
-
{
|
|
93
|
+
{
|
|
94
|
+
id,
|
|
95
|
+
title = id,
|
|
96
|
+
description,
|
|
97
|
+
language = AUTO,
|
|
98
|
+
theme = DEFAULT_THEME,
|
|
99
|
+
pwa = true,
|
|
100
|
+
},
|
|
78
101
|
) {
|
|
79
102
|
const reason = invalidId(id);
|
|
80
103
|
if (reason) throw new Error(reason);
|
|
81
|
-
if (!
|
|
82
|
-
throw new Error(`
|
|
104
|
+
if (!themeNames().includes(theme))
|
|
105
|
+
throw new Error(`theme must be one of ${themeNames().join(', ')}`);
|
|
106
|
+
const problem = invalidDescription(description);
|
|
107
|
+
if (problem) throw new Error(problem);
|
|
108
|
+
if (!APP_LANGUAGES.includes(language))
|
|
109
|
+
throw new Error(`language must be one of ${APP_LANGUAGES.join(', ')}`);
|
|
83
110
|
if (typeof title !== 'string' || title.trim() === '')
|
|
84
111
|
throw new Error('the title must not be empty');
|
|
85
112
|
const manifest = readApps(root);
|
|
@@ -91,6 +118,7 @@ export function createApp(
|
|
|
91
118
|
const app = {
|
|
92
119
|
id,
|
|
93
120
|
title: title.trim(),
|
|
121
|
+
...(description === undefined ? {} : { description: description.trim() }),
|
|
94
122
|
language,
|
|
95
123
|
path: `/${id}/`,
|
|
96
124
|
devPort: nextDevPort(manifest),
|
|
@@ -98,7 +126,8 @@ export function createApp(
|
|
|
98
126
|
};
|
|
99
127
|
try {
|
|
100
128
|
cpSync(SKELETON, appDir, { recursive: true, errorOnExist: true });
|
|
101
|
-
fillTokens(appDir, app);
|
|
129
|
+
fillTokens(appDir, app, theme);
|
|
130
|
+
writeAppIcons(appDir, app.title, iconColours(themeCss(appDir, theme)));
|
|
102
131
|
const pkgFile = join(appDir, 'package.json');
|
|
103
132
|
const pkg = readJson(pkgFile);
|
|
104
133
|
// Package managers keep dependency lists sorted.
|
|
@@ -123,14 +152,16 @@ function main() {
|
|
|
123
152
|
allowPositionals: true,
|
|
124
153
|
options: {
|
|
125
154
|
title: { type: 'string' },
|
|
155
|
+
description: { type: 'string' },
|
|
126
156
|
language: { type: 'string' },
|
|
157
|
+
theme: { type: 'string' },
|
|
127
158
|
'no-pwa': { type: 'boolean' },
|
|
128
159
|
'skip-install': { type: 'boolean' },
|
|
129
160
|
},
|
|
130
161
|
});
|
|
131
162
|
if (positionals.length !== 1) {
|
|
132
163
|
console.error(
|
|
133
|
-
'Usage: pnpm run new-app <id> [--title "<title>"] [--language <de|en>] [--no-pwa]',
|
|
164
|
+
'Usage: pnpm run new-app <id> [--title "<title>"] [--description "<text>"] [--language <auto|de|en>] [--theme <name>] [--no-pwa]',
|
|
134
165
|
);
|
|
135
166
|
process.exit(2);
|
|
136
167
|
}
|
|
@@ -140,6 +171,8 @@ function main() {
|
|
|
140
171
|
app = createApp(root, {
|
|
141
172
|
id: positionals[0],
|
|
142
173
|
title: values.title,
|
|
174
|
+
description: values.description,
|
|
175
|
+
theme: values.theme,
|
|
143
176
|
language: values.language,
|
|
144
177
|
pwa: !values['no-pwa'],
|
|
145
178
|
});
|
package/scripts/sdk-update.mjs
CHANGED
|
@@ -55,6 +55,20 @@ export function releaseLinks(version) {
|
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// Since 0.6.0 the SDK ships its release notes and migration guide; read the
|
|
59
|
+
// installed ones, which match the installed code.
|
|
60
|
+
const INSTALLED_DOCS = join('node_modules', '@sempods', 'app-sdk', 'docs');
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Where to read about `version` after it is installed: the installed notes
|
|
64
|
+
* and guide when the SDK ships them, otherwise the published ones.
|
|
65
|
+
*/
|
|
66
|
+
export function releaseDocs(root, version) {
|
|
67
|
+
const notes = join(INSTALLED_DOCS, 'releases', `${version}.md`);
|
|
68
|
+
if (!existsSync(join(root, notes))) return releaseLinks(version);
|
|
69
|
+
return { release: notes, migration: join(INSTALLED_DOCS, 'migration.md') };
|
|
70
|
+
}
|
|
71
|
+
|
|
58
72
|
/** Plan everything before writing. Registry injection keeps tests offline. */
|
|
59
73
|
export function planUpdate(
|
|
60
74
|
root,
|
|
@@ -202,8 +216,9 @@ function main() {
|
|
|
202
216
|
? `Updated SDK to ${plan.version}.`
|
|
203
217
|
: `Already at SDK ${plan.version}; nothing changed.`,
|
|
204
218
|
);
|
|
219
|
+
const docs = releaseDocs(root, plan.version);
|
|
205
220
|
console.log(
|
|
206
|
-
`SDK release: ${
|
|
221
|
+
`SDK release notes: ${docs.release}\nMigration guide: ${docs.migration}`,
|
|
207
222
|
);
|
|
208
223
|
}
|
|
209
224
|
|
|
@@ -47,9 +47,9 @@ it. Keep it free of credentials and private data.
|
|
|
47
47
|
- Created from (`.sempods-baseline.json`): not recorded
|
|
48
48
|
- SDK version at setup (`@sempods/app-sdk` in the root `package.json`): not recorded
|
|
49
49
|
- Owner and review identity: not recorded
|
|
50
|
-
-
|
|
50
|
+
- Owner's language for the app texts: not recorded
|
|
51
51
|
- First app ID and idea: not selected
|
|
52
|
-
- Test Pod
|
|
52
|
+
- Test Pod and contexts: not selected (optional; synthetic data only)
|
|
53
53
|
- sempods version PRs (Dependabot): not chosen
|
|
54
54
|
- Local URL and check result: not checked
|
|
55
55
|
- Pod exercise and remaining gaps: not tested
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"new-app": "sempods-apps new-app",
|
|
11
|
+
"icon": "sempods-apps icon",
|
|
11
12
|
"dev": "sempods-apps dev",
|
|
12
13
|
"regenerate": "sempods-apps regenerate",
|
|
13
14
|
"configure-site": "sempods-apps configure-site",
|
|
@@ -18,8 +19,8 @@
|
|
|
18
19
|
"migrate": "sempods-apps migrate"
|
|
19
20
|
},
|
|
20
21
|
"devDependencies": {
|
|
21
|
-
"@sempods/app-sdk": "0.
|
|
22
|
+
"@sempods/app-sdk": "0.6.0",
|
|
22
23
|
"@sempods/apps": "0.0.0",
|
|
23
|
-
"@sempods/client-sdk": "0.
|
|
24
|
+
"@sempods/client-sdk": "0.6.0"
|
|
24
25
|
}
|
|
25
26
|
}
|
|
@@ -14,7 +14,8 @@ to it by itself.
|
|
|
14
14
|
[Open this repository in your assistant and paste the starter prompt](../README.md#start-with-your-idea).
|
|
15
15
|
Describe one thing you want to do: “Add something to my shopping list.” Your
|
|
16
16
|
assistant sets up the app and gives you a local link to open.
|
|
17
|
-
It chooses the technical details and asks you
|
|
17
|
+
It chooses the technical details and asks you a few short questions: what the
|
|
18
|
+
app is for, which mood it should have, its name and icon.
|
|
18
19
|
Review rules and automatic updates can wait until you have tried the first
|
|
19
20
|
version.
|
|
20
21
|
|
|
@@ -29,7 +30,12 @@ Your existing apps stay in the same repository.
|
|
|
29
30
|
Use a test context with made-up data in your Pod. Sign in on the Pod's page,
|
|
30
31
|
then choose that context in the app. Ask your assistant to check a save and a
|
|
31
32
|
reload, and to tell you what it has not tested. Never paste passwords or tokens
|
|
32
|
-
into chat. Without a Pod
|
|
33
|
+
into chat. Without a Pod you only see the sign-in screen; the app's own
|
|
34
|
+
screens need a test context.
|
|
35
|
+
|
|
36
|
+
Before your assistant hands over a screen, it shows you a screenshot from a
|
|
37
|
+
phone-sized window and asks what you think. Say what you like and what feels
|
|
38
|
+
off; that is how the app gets its look.
|
|
33
39
|
|
|
34
40
|
## Keep apps up to date
|
|
35
41
|
|
package/shared/snapshot.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"format": 1,
|
|
3
|
-
"revision": "
|
|
3
|
+
"revision": "1dd60bc5a3e7e0ab995d0d6941e78f96239ee8979a6c1d3a529ae4f79b680e1b",
|
|
4
4
|
"files": {
|
|
5
5
|
".agents/skills/app-workflow/SKILL.md": "0221b70340152ba3cc615342254603b29b89c4d7d944d7b3b3e647873bdcefa0",
|
|
6
6
|
".agents/skills/update/SKILL.md": "d3ca3a13c6524a2ee9382977ddc0539790813225f366306024a58c957a4716ee",
|
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
".node-version": "b911698dc3cf3227d34d0ad6727f1c4408791203c9665de99ab959331a1fb07d",
|
|
15
15
|
"AGENTS.md": "393b09ecd18889d9e1fdf2a6df12d1f6c193fc7fcd89ba808ad94df521bad8db",
|
|
16
16
|
"CLAUDE.md": "f9e2544290ee8418484e47194444ee58c2ea675682871ae8a8727d1edb17978e",
|
|
17
|
-
"INIT.md": "
|
|
17
|
+
"INIT.md": "44c114d4d2c8b172f4757100f79e8a1a00d30c96ba2ad8f75c3373e52f6b849f",
|
|
18
18
|
"README.md": "2818c884d46d97ec14fe4921dbdc147769d29846c08a92b1d3225c5297cfed93",
|
|
19
19
|
"apps.json": "9945f9bd573e5af8aad2e65cc8b4284fbb1d7e8b96c4efdd42eeb49132fb4327",
|
|
20
20
|
"apps/.gitkeep": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
21
|
-
"docs/start.md": "
|
|
22
|
-
"package.json": "
|
|
21
|
+
"docs/start.md": "f30ec6b7f9de16be54919382ab18cdcc33ee64938f7b907d855bcf6740e4c102",
|
|
22
|
+
"package.json": "843c078e84912158607ebb8ce0042589d676a742bcd3cf8b6c474d62ff3a6845",
|
|
23
23
|
"pnpm-workspace.yaml": "4660ca1887261ae658c7f587190e2b71ac91d15b1b6c4cf04dec823fdd0da654"
|
|
24
24
|
}
|
|
25
25
|
}
|
package/skeleton/app/NOTES.md
CHANGED
|
@@ -6,6 +6,16 @@ Your assistant keeps these notes up to date so the next session can continue.
|
|
|
6
6
|
|
|
7
7
|
What the app is for and its first useful interaction.
|
|
8
8
|
|
|
9
|
+
## Design
|
|
10
|
+
|
|
11
|
+
Purpose and who uses it, the theme and how the icon was chosen, with later
|
|
12
|
+
changes the owner asked for.
|
|
13
|
+
|
|
14
|
+
## Data
|
|
15
|
+
|
|
16
|
+
How the app uses contexts (one, several one at a time, read all and write in
|
|
17
|
+
one, or write into several) and which contexts it reads and writes.
|
|
18
|
+
|
|
9
19
|
## Vocabulary
|
|
10
20
|
|
|
11
21
|
Types and properties (full IRIs) the app reads and writes.
|
|
@@ -16,4 +26,4 @@ Choices the owner made, with the date.
|
|
|
16
26
|
|
|
17
27
|
## Tested
|
|
18
28
|
|
|
19
|
-
What was tried, against which Pod and
|
|
29
|
+
What was tried, against which Pod and contexts, and what is still untested.
|
package/skeleton/app/index.html
CHANGED
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
|
-
|
|
7
|
-
<meta
|
|
6
|
+
<!-- The layout resizes with the on-screen keyboard and reaches the edges. -->
|
|
7
|
+
<meta
|
|
8
|
+
name="viewport"
|
|
9
|
+
content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content"
|
|
10
|
+
/>
|
|
11
|
+
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff" />
|
|
12
|
+
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#111315" />
|
|
8
13
|
<!-- The callback page must not send its code and state in Referer. -->
|
|
9
14
|
<meta name="referrer" content="strict-origin" />
|
|
10
|
-
<title>__APP_TITLE__</title>
|
|
11
15
|
</head>
|
|
12
16
|
<body>
|
|
13
17
|
<div id="root"></div>
|
|
@@ -11,18 +11,18 @@
|
|
|
11
11
|
"preview": "vite preview"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"react": "19.3.
|
|
15
|
-
"react-dom": "19.3.
|
|
14
|
+
"react": "19.3.1",
|
|
15
|
+
"react-dom": "19.3.1"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"@types/node": "24.19.
|
|
18
|
+
"@types/node": "24.19.2",
|
|
19
19
|
"@types/react": "19.3.0",
|
|
20
20
|
"@types/react-dom": "19.3.0",
|
|
21
21
|
"@vitejs/plugin-react": "6.1.2",
|
|
22
|
-
"oxlint": "1.
|
|
22
|
+
"oxlint": "1.89.0",
|
|
23
23
|
"typescript": "7.0.2",
|
|
24
|
-
"vite": "8.3.
|
|
24
|
+
"vite": "8.3.5",
|
|
25
25
|
"vite-plugin-pwa": "2.0.0",
|
|
26
|
-
"vitest": "5.0.
|
|
26
|
+
"vitest": "5.0.5"
|
|
27
27
|
}
|
|
28
28
|
}
|
package/skeleton/app/src/App.tsx
CHANGED
|
@@ -1,64 +1,96 @@
|
|
|
1
1
|
import { useRef, useState } from 'react';
|
|
2
2
|
import { createBrowserRuntime } from '@sempods/app-sdk';
|
|
3
3
|
import {
|
|
4
|
+
AccessNotice,
|
|
4
5
|
AppAccess,
|
|
5
6
|
SempodsProvider,
|
|
6
7
|
TargetScreen,
|
|
7
8
|
useAppState,
|
|
9
|
+
useMessages,
|
|
8
10
|
useSdkLocale,
|
|
9
11
|
} from '@sempods/app-sdk/react';
|
|
10
12
|
import { NewVersionNotice } from './NewVersionNotice.tsx';
|
|
13
|
+
import { replaceUrl } from './routes.ts';
|
|
11
14
|
import { app, runtimeOptions } from './sempods.generated.ts';
|
|
12
|
-
import {
|
|
15
|
+
import { texts, uiLocale } from './texts.ts';
|
|
16
|
+
import { MenuIcon } from './ui/icons.tsx';
|
|
17
|
+
import { Menu } from './ui/Menu.tsx';
|
|
13
18
|
|
|
14
|
-
// One runtime for the app's lifetime, created outside rendering.
|
|
15
|
-
|
|
19
|
+
// One runtime for the app's lifetime, created outside rendering. Its return
|
|
20
|
+
// after a sign-in goes through the router, which then shows that route.
|
|
21
|
+
const runtime = createBrowserRuntime({ ...runtimeOptions, replaceUrl });
|
|
22
|
+
|
|
23
|
+
// One UI language for the app's texts, the SDK components and the page.
|
|
24
|
+
const { locale, language } = uiLocale(app.language, navigator.languages);
|
|
25
|
+
document.documentElement.lang = language;
|
|
16
26
|
|
|
17
27
|
// Vite hot replacement ends this module's runtime lifetime.
|
|
18
28
|
if (import.meta.hot) import.meta.hot.dispose(() => runtime.dispose());
|
|
19
29
|
|
|
30
|
+
// The app icon (public/favicon.svg), decorative next to the app's name.
|
|
31
|
+
const icon = <img src={`${app.basePath}favicon.svg`} alt="" />;
|
|
32
|
+
|
|
20
33
|
export default function App({
|
|
21
34
|
registration,
|
|
22
35
|
}: {
|
|
23
36
|
readonly registration: Promise<ServiceWorkerRegistration | undefined>;
|
|
24
37
|
}) {
|
|
25
38
|
return (
|
|
26
|
-
<SempodsProvider runtime={runtime} language={
|
|
39
|
+
<SempodsProvider runtime={runtime} language={language} locale={locale}>
|
|
27
40
|
<NewVersionNotice registration={registration} />
|
|
28
41
|
<Frame />
|
|
29
42
|
</SempodsProvider>
|
|
30
43
|
);
|
|
31
44
|
}
|
|
32
45
|
|
|
33
|
-
// The app owns its layout
|
|
34
|
-
//
|
|
46
|
+
// The app owns its layout: a header with the app's icon and name, its icon
|
|
47
|
+
// actions and a menu; AppAccess beside the content for sign-in and recovery,
|
|
48
|
+
// and as a dialog for managing Pods and sharing from the menu. An app that
|
|
49
|
+
// switches between contexts (lists) puts ui/ContextSwitch in place of the
|
|
50
|
+
// name.
|
|
35
51
|
function Frame() {
|
|
36
|
-
const [
|
|
37
|
-
const
|
|
52
|
+
const [managing, setManaging] = useState(false);
|
|
53
|
+
const menuButton = useRef<HTMLButtonElement>(null);
|
|
38
54
|
const { connections } = useAppState();
|
|
39
55
|
const { messages } = useSdkLocale();
|
|
56
|
+
const t = useMessages(texts);
|
|
40
57
|
const header = connections.length > 0;
|
|
41
58
|
return (
|
|
42
59
|
<main>
|
|
43
60
|
{header && (
|
|
44
61
|
<header>
|
|
45
|
-
<h1>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
62
|
+
<h1>
|
|
63
|
+
{icon}
|
|
64
|
+
{app.title}
|
|
65
|
+
</h1>
|
|
66
|
+
<div className="header-actions">
|
|
67
|
+
{/* The app's own icon actions go here. */}
|
|
68
|
+
<Menu
|
|
69
|
+
label={t.menu}
|
|
70
|
+
trigger={<MenuIcon />}
|
|
71
|
+
button={menuButton}
|
|
72
|
+
items={[
|
|
73
|
+
{
|
|
74
|
+
label: messages.controls.dataAccess,
|
|
75
|
+
onSelect: () => setManaging(true),
|
|
76
|
+
},
|
|
77
|
+
]}
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
53
80
|
</header>
|
|
54
81
|
)}
|
|
55
|
-
{/* Below the header's h1, AppAccess uses h2.
|
|
82
|
+
{/* Below the header's h1, AppAccess uses h2. It explains sign-in and
|
|
83
|
+
lost access; AccessNotice adds only that a context is read-only. */}
|
|
56
84
|
<AppAccess
|
|
57
85
|
appName={app.title}
|
|
86
|
+
description={app.description}
|
|
87
|
+
icon={icon}
|
|
58
88
|
headingLevel={header ? 2 : 1}
|
|
59
|
-
open={
|
|
60
|
-
|
|
89
|
+
open={managing}
|
|
90
|
+
onClose={() => setManaging(false)}
|
|
91
|
+
focusTarget={menuButton}
|
|
61
92
|
/>
|
|
93
|
+
<AccessNotice only="write" />
|
|
62
94
|
<TargetScreen>
|
|
63
95
|
<Start />
|
|
64
96
|
</TargetScreen>
|
|
@@ -68,6 +100,6 @@ function Frame() {
|
|
|
68
100
|
|
|
69
101
|
// Replace this screen with your app. It renders once a Pod context is chosen.
|
|
70
102
|
function Start() {
|
|
71
|
-
const
|
|
72
|
-
return <p>{
|
|
103
|
+
const t = useMessages(texts);
|
|
104
|
+
return <p>{t.start}</p>;
|
|
73
105
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
// Adapted from the sempods TypeScript SDK v0.
|
|
1
|
+
// Adapted from the sempods TypeScript SDK v0.6.0 recipe
|
|
2
2
|
// examples/todo/pwa/register.tsx, licensed under Apache-2.0; see
|
|
3
3
|
// the LICENSE and NOTICE of the @sempods/app-sdk package. Changes: this header; the
|
|
4
|
-
// registration function moved to pwa.ts.
|
|
4
|
+
// registration function moved to pwa.ts; the text comes from texts.ts.
|
|
5
5
|
import { useEffect, useState } from 'react';
|
|
6
|
-
import {
|
|
6
|
+
import { useMessages } from '@sempods/app-sdk/react';
|
|
7
|
+
import { texts } from './texts.ts';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Render beneath the app's actual locale provider so notices follow its language.
|
|
@@ -18,7 +19,7 @@ export function NewVersionNotice({
|
|
|
18
19
|
readonly registration: Promise<ServiceWorkerRegistration | undefined>;
|
|
19
20
|
}) {
|
|
20
21
|
const [ready, setReady] = useState(false);
|
|
21
|
-
const
|
|
22
|
+
const t = useMessages(texts);
|
|
22
23
|
useEffect(() => {
|
|
23
24
|
let active = true;
|
|
24
25
|
let stop = () => {};
|
|
@@ -53,11 +54,5 @@ export function NewVersionNotice({
|
|
|
53
54
|
};
|
|
54
55
|
}, [registration]);
|
|
55
56
|
if (!ready) return null;
|
|
56
|
-
return
|
|
57
|
-
<p role="status">
|
|
58
|
-
{language === 'de'
|
|
59
|
-
? 'Eine neue Version ist bereit. Sie wird verwendet, sobald alle Fenster dieser App geschlossen sind.'
|
|
60
|
-
: 'A new version is ready. It is used once every window of this app has been closed.'}
|
|
61
|
-
</p>
|
|
62
|
-
);
|
|
57
|
+
return <p role="status">{t.newVersion}</p>;
|
|
63
58
|
}
|