@sempods/apps 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +200 -0
- package/README.md +6 -3
- package/bin/sempods-apps.mjs +1 -0
- package/instructions/app-workflow.md +322 -111
- package/instructions/publish.md +13 -7
- package/instructions/setup.md +38 -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 +137 -0
- package/scripts/lib/themes.mjs +58 -0
- package/scripts/migrate.mjs +44 -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 → e1c798ec8536cf8cff6c19caf622fa9076ae210b0d1733a6e82ea2c1f4104c43} +17 -2
- package/shared/snapshot.json +4 -4
- package/skeleton/app/NOTES.md +20 -2
- 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 +176 -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 +94 -0
- package/skeleton/app/src/ui/Dialog.tsx +120 -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 +249 -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
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,10 +14,20 @@ 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
|
|
17
|
+
It chooses the technical details and proposes a fitting look. It asks a few
|
|
18
|
+
short questions about what the app is for, whether people use their own data
|
|
19
|
+
or work on the same data, and where new entries and changes should go. You can
|
|
20
|
+
describe these choices as lists or shared areas; no technical terms are needed.
|
|
18
21
|
Review rules and automatic updates can wait until you have tried the first
|
|
19
22
|
version.
|
|
20
23
|
|
|
24
|
+
Already have data for this app? If you want, connect your Pod to the assistant
|
|
25
|
+
through MCP, its connection to external tools and data. Agree which data it
|
|
26
|
+
may inspect so it can make the app fit your existing tasks, contacts or events.
|
|
27
|
+
This optional look only reads; it does not change your records. You can also
|
|
28
|
+
provide a made-up example or start without it. You still sign in to the app
|
|
29
|
+
separately when trying it.
|
|
30
|
+
|
|
21
31
|
## Change
|
|
22
32
|
|
|
23
33
|
Ask for the next small improvement: “Let me mark an item as bought.” Keep trying
|
|
@@ -29,7 +39,12 @@ Your existing apps stay in the same repository.
|
|
|
29
39
|
Use a test context with made-up data in your Pod. Sign in on the Pod's page,
|
|
30
40
|
then choose that context in the app. Ask your assistant to check a save and a
|
|
31
41
|
reload, and to tell you what it has not tested. Never paste passwords or tokens
|
|
32
|
-
into chat. Without a Pod
|
|
42
|
+
into chat. Without a Pod you only see the sign-in screen; the app's own
|
|
43
|
+
screens need a test context.
|
|
44
|
+
|
|
45
|
+
Before your assistant hands over a screen, it shows you a screenshot from a
|
|
46
|
+
phone-sized window and asks what you think. Say what you like and what feels
|
|
47
|
+
off; that is how the app gets its look.
|
|
33
48
|
|
|
34
49
|
## Keep apps up to date
|
|
35
50
|
|
package/shared/snapshot.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"format": 1,
|
|
3
|
-
"revision": "
|
|
3
|
+
"revision": "b39743fd5d824bb8bd37eaa9298b23ffe281dfd41799d652b5295f96191fdc02",
|
|
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": "e1c798ec8536cf8cff6c19caf622fa9076ae210b0d1733a6e82ea2c1f4104c43",
|
|
22
|
+
"package.json": "843c078e84912158607ebb8ce0042589d676a742bcd3cf8b6c474d62ff3a6845",
|
|
23
23
|
"pnpm-workspace.yaml": "4660ca1887261ae658c7f587190e2b71ac91d15b1b6c4cf04dec823fdd0da654"
|
|
24
24
|
}
|
|
25
25
|
}
|
package/skeleton/app/NOTES.md
CHANGED
|
@@ -6,9 +6,27 @@ 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
|
+
Who uses their own data or shared data; who reads, writes and deletes, and
|
|
17
|
+
which limits the Pod can enforce. Which contexts are read, where new records
|
|
18
|
+
go, and where edits to existing records go (the selected context or each
|
|
19
|
+
row's source). What deletion means for other apps using the same records.
|
|
20
|
+
If existing data was inspected, the agreed Pod, contexts and read scope;
|
|
21
|
+
otherwise, what remains unknown. No credentials or private sample values.
|
|
22
|
+
|
|
9
23
|
## Vocabulary
|
|
10
24
|
|
|
11
|
-
|
|
25
|
+
Existing data or standard vocabulary it follows, types and properties (full
|
|
26
|
+
IRIs), text languages and status values. How subject identities and unknown
|
|
27
|
+
fields survive other apps' edits; the reason for any custom extension.
|
|
28
|
+
Which mappings were observed through optional MCP inspection or a supplied
|
|
29
|
+
example, and which are assumptions still to verify.
|
|
12
30
|
|
|
13
31
|
## Decisions
|
|
14
32
|
|
|
@@ -16,4 +34,4 @@ Choices the owner made, with the date.
|
|
|
16
34
|
|
|
17
35
|
## Tested
|
|
18
36
|
|
|
19
|
-
What was tried, against which Pod and
|
|
37
|
+
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
|
+
// h1.
|
|
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
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// An example, not mounted: the default screen of a list app, built from the
|
|
2
|
+
// SDK's quickstart screen with the kit. For a list app, copy it next to
|
|
3
|
+
// App.tsx, render <ListScreen /> in place of <Start />, and rename the type,
|
|
4
|
+
// fields, collection and texts to the app's own. Keep App.tsx's runtime,
|
|
5
|
+
// provider and frame; take behaviour from the SDK's examples, never their
|
|
6
|
+
// runtime, provider or styles.
|
|
7
|
+
import {
|
|
8
|
+
LoadNotice,
|
|
9
|
+
RemoveButton,
|
|
10
|
+
ResourceEditor,
|
|
11
|
+
UnresolvedNotices,
|
|
12
|
+
UpdateNotice,
|
|
13
|
+
useCreation,
|
|
14
|
+
useFieldUpdate,
|
|
15
|
+
useList,
|
|
16
|
+
useMessages,
|
|
17
|
+
useResourceEditor,
|
|
18
|
+
useSelection,
|
|
19
|
+
useWorkflowAccess,
|
|
20
|
+
} from '@sempods/app-sdk/react';
|
|
21
|
+
import { fields, flag, text } from '@sempods/client-sdk/edit';
|
|
22
|
+
import { Composer } from '../ui/Composer.tsx';
|
|
23
|
+
import { Dialog } from '../ui/Dialog.tsx';
|
|
24
|
+
import { AddIcon } from '../ui/icons.tsx';
|
|
25
|
+
|
|
26
|
+
const schema = 'https://schema.org/';
|
|
27
|
+
|
|
28
|
+
/** What one row is: a name and whether it is done. */
|
|
29
|
+
const item = fields(
|
|
30
|
+
{
|
|
31
|
+
name: text(`${schema}name`, { language: null, trim: true }),
|
|
32
|
+
done: flag(`${schema}actionStatus`, {
|
|
33
|
+
on: `${schema}CompletedActionStatus`,
|
|
34
|
+
off: `${schema}PotentialActionStatus`,
|
|
35
|
+
}),
|
|
36
|
+
},
|
|
37
|
+
{ type: `${schema}Action` },
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// In an app these belong in src/texts.ts.
|
|
41
|
+
const en = {
|
|
42
|
+
empty: 'Nothing here yet. Add the first item below.',
|
|
43
|
+
emptyReadOnly: 'Nothing in this list.',
|
|
44
|
+
done: (name: string) => `Done: ${name}`,
|
|
45
|
+
remove: (name: string) => `Delete: ${name}`,
|
|
46
|
+
edit: 'Edit item',
|
|
47
|
+
close: 'Close',
|
|
48
|
+
name: 'Name',
|
|
49
|
+
newItem: 'New item',
|
|
50
|
+
add: 'Add',
|
|
51
|
+
};
|
|
52
|
+
const listTexts: { readonly en: typeof en; readonly de: typeof en } = {
|
|
53
|
+
en,
|
|
54
|
+
de: {
|
|
55
|
+
empty: 'Noch nichts hier. Füge unten den ersten Eintrag hinzu.',
|
|
56
|
+
emptyReadOnly: 'Nichts in dieser Liste.',
|
|
57
|
+
done: (name) => `Erledigt: ${name}`,
|
|
58
|
+
remove: (name) => `Löschen: ${name}`,
|
|
59
|
+
edit: 'Eintrag bearbeiten',
|
|
60
|
+
close: 'Schließen',
|
|
61
|
+
name: 'Name',
|
|
62
|
+
newItem: 'Neuer Eintrag',
|
|
63
|
+
add: 'Hinzufügen',
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export function ListScreen() {
|
|
68
|
+
const t = useMessages(listTexts);
|
|
69
|
+
// A list that can only be read gets no input and no invitation to add.
|
|
70
|
+
const { write } = useWorkflowAccess();
|
|
71
|
+
const list = useList(item);
|
|
72
|
+
const items = list.data?.items;
|
|
73
|
+
// One hook for the list; each row gets its part with `mutate.for(row)`.
|
|
74
|
+
const mutate = useFieldUpdate(item);
|
|
75
|
+
const { selected, select } = useSelection();
|
|
76
|
+
const editor = useResourceEditor(selected, item);
|
|
77
|
+
const creation = useCreation(item, {
|
|
78
|
+
initial: { name: '', done: false },
|
|
79
|
+
collection: 'items',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<>
|
|
84
|
+
{/* Loading, a failed read with "Try again", never a failure as empty. */}
|
|
85
|
+
<LoadNotice {...list} />
|
|
86
|
+
{(list.state.kind === 'ready' || list.state.kind === 'loading') &&
|
|
87
|
+
items?.length === 0 &&
|
|
88
|
+
list.data?.skipped === 0 && (
|
|
89
|
+
<p className="empty">{write ? t.empty : t.emptyReadOnly}</p>
|
|
90
|
+
)}
|
|
91
|
+
<ul className="rows">
|
|
92
|
+
{items?.map((entry) => {
|
|
93
|
+
const row = mutate.for(entry);
|
|
94
|
+
return (
|
|
95
|
+
<li key={entry.iri} className="row">
|
|
96
|
+
{/* The label is the checkbox's 44px touch target. */}
|
|
97
|
+
<label>
|
|
98
|
+
<input
|
|
99
|
+
type="checkbox"
|
|
100
|
+
checked={row.data.done}
|
|
101
|
+
disabled={!row.canMutate}
|
|
102
|
+
onChange={() => void row.update({ done: !row.data.done })}
|
|
103
|
+
/>
|
|
104
|
+
<span className="visually-hidden">{t.done(row.data.name)}</span>
|
|
105
|
+
</label>
|
|
106
|
+
<button
|
|
107
|
+
className="row-name"
|
|
108
|
+
onClick={() => void select(entry.iri)}
|
|
109
|
+
>
|
|
110
|
+
{row.data.name}
|
|
111
|
+
</button>
|
|
112
|
+
{/* Asks in the SDK's dialog before it deletes. */}
|
|
113
|
+
<RemoveButton
|
|
114
|
+
disabled={!row.canMutate}
|
|
115
|
+
onRemove={row.remove}
|
|
116
|
+
label={t.remove(row.data.name)}
|
|
117
|
+
/>
|
|
118
|
+
{/* The changed row is the confirmation; problems show here. */}
|
|
119
|
+
<UpdateNotice {...row.notice} quiet />
|
|
120
|
+
</li>
|
|
121
|
+
);
|
|
122
|
+
})}
|
|
123
|
+
</ul>
|
|
124
|
+
{/* Recovery for a row with an open write that is no longer listed. */}
|
|
125
|
+
<UnresolvedNotices of={mutate} shown={items} />
|
|
126
|
+
{/* Outside the rows, so the draft stays reachable if its row goes.
|
|
127
|
+
The SDK's Escape in a field cancels like Cancel and discards the
|
|
128
|
+
draft; × and a click beside the dialog ask first. */}
|
|
129
|
+
{selected && (
|
|
130
|
+
<Dialog
|
|
131
|
+
label={t.edit}
|
|
132
|
+
closeLabel={t.close}
|
|
133
|
+
onClose={() => void select(null)}
|
|
134
|
+
>
|
|
135
|
+
<ResourceEditor editor={editor} onClose={() => select(null)}>
|
|
136
|
+
{(draft, change) => (
|
|
137
|
+
<label>
|
|
138
|
+
{t.name}
|
|
139
|
+
<input
|
|
140
|
+
value={draft.name}
|
|
141
|
+
onChange={(event) => change({ name: event.target.value })}
|
|
142
|
+
/>
|
|
143
|
+
</label>
|
|
144
|
+
)}
|
|
145
|
+
</ResourceEditor>
|
|
146
|
+
</Dialog>
|
|
147
|
+
)}
|
|
148
|
+
<UpdateNotice {...creation.notice} quiet />
|
|
149
|
+
{write && (
|
|
150
|
+
<Composer>
|
|
151
|
+
<form
|
|
152
|
+
onSubmit={(event) => {
|
|
153
|
+
event.preventDefault();
|
|
154
|
+
void creation.create();
|
|
155
|
+
}}
|
|
156
|
+
>
|
|
157
|
+
<input
|
|
158
|
+
aria-label={t.newItem}
|
|
159
|
+
placeholder={t.newItem}
|
|
160
|
+
value={creation.draft.name}
|
|
161
|
+
disabled={!creation.canEdit}
|
|
162
|
+
onChange={(event) => creation.change({ name: event.target.value })}
|
|
163
|
+
/>
|
|
164
|
+
<button
|
|
165
|
+
aria-label={t.add}
|
|
166
|
+
title={t.add}
|
|
167
|
+
disabled={!creation.canCreate}
|
|
168
|
+
>
|
|
169
|
+
<AddIcon />
|
|
170
|
+
</button>
|
|
171
|
+
</form>
|
|
172
|
+
</Composer>
|
|
173
|
+
)}
|
|
174
|
+
</>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
:root {
|
|
2
|
-
/* Follow the system's light or dark mode
|
|
3
|
-
|
|
2
|
+
/* Follow the system's light or dark mode. The colours are the --sempods-*
|
|
3
|
+
tokens of the app's theme (src/themes/, imported in main.tsx), which
|
|
4
|
+
colour the app and the SDK components alike. */
|
|
4
5
|
color-scheme: light dark;
|
|
5
6
|
font-family: system-ui, sans-serif;
|
|
6
|
-
color:
|
|
7
|
-
background:
|
|
7
|
+
color: var(--sempods-text);
|
|
8
|
+
background: var(--sempods-bg);
|
|
8
9
|
}
|
|
9
10
|
body {
|
|
10
11
|
margin: 0;
|
|
@@ -14,24 +15,96 @@ main {
|
|
|
14
15
|
margin: auto;
|
|
15
16
|
padding: 16px;
|
|
16
17
|
}
|
|
17
|
-
header
|
|
18
|
+
/* The app's own header; AppAccess brings its own. */
|
|
19
|
+
main > header {
|
|
18
20
|
display: flex;
|
|
19
21
|
flex-wrap: wrap;
|
|
20
22
|
align-items: center;
|
|
21
23
|
justify-content: space-between;
|
|
22
24
|
gap: 0.5rem;
|
|
23
25
|
}
|
|
24
|
-
header h1 {
|
|
26
|
+
main > header h1 {
|
|
27
|
+
display: flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
gap: 0.5rem;
|
|
25
30
|
margin: 0;
|
|
26
31
|
font-size: 1.4rem;
|
|
27
32
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
main > header h1 img {
|
|
34
|
+
width: 28px;
|
|
35
|
+
height: 28px;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* The rules below style the app's own markup only. SDK components
|
|
39
|
+
([data-sempods-ui]) bring their own styles and are left alone. :where()
|
|
40
|
+
keeps these rules at zero specificity, so any class overrides them. */
|
|
41
|
+
|
|
42
|
+
/* Actions read as text, like the SDK's secondary actions: accent colour, no
|
|
43
|
+
frame, a calm hover and a visible focus, with 44px touch targets. */
|
|
44
|
+
:where(button):not(:where([data-sempods-ui] *, [data-sempods-button])) {
|
|
45
|
+
min-width: 44px;
|
|
46
|
+
min-height: 44px;
|
|
47
|
+
padding: 10px 12px;
|
|
48
|
+
border: 0;
|
|
49
|
+
border-radius: 6px;
|
|
50
|
+
background: transparent;
|
|
51
|
+
color: var(--sempods-accent);
|
|
31
52
|
font: inherit;
|
|
32
|
-
|
|
33
|
-
|
|
53
|
+
font-weight: 500;
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
}
|
|
56
|
+
:where(button):not(:where([data-sempods-ui] *, [data-sempods-button])):where(
|
|
57
|
+
:hover:not(:disabled)
|
|
58
|
+
) {
|
|
59
|
+
background: color-mix(in srgb, var(--sempods-accent) 8%, transparent);
|
|
60
|
+
}
|
|
61
|
+
:where(button):not(:where([data-sempods-ui] *, [data-sempods-button])):where(
|
|
62
|
+
:disabled
|
|
63
|
+
) {
|
|
64
|
+
color: var(--sempods-muted);
|
|
65
|
+
cursor: default;
|
|
66
|
+
}
|
|
67
|
+
:where(input, select, textarea):not(
|
|
68
|
+
:where([data-sempods-ui] *, [type='checkbox'], [type='radio'])
|
|
69
|
+
) {
|
|
70
|
+
box-sizing: border-box;
|
|
71
|
+
min-height: 44px;
|
|
72
|
+
padding: 8px 10px;
|
|
73
|
+
border: 1px solid var(--sempods-line);
|
|
74
|
+
border-radius: 6px;
|
|
75
|
+
background: var(--sempods-bg);
|
|
76
|
+
color: var(--sempods-text);
|
|
77
|
+
font: inherit;
|
|
78
|
+
}
|
|
79
|
+
/* A compact box; its wrapping <label> is the 44px touch target, as in the
|
|
80
|
+
SDK. Margins are not clickable, so wrap every checkbox in its label. */
|
|
81
|
+
:where(input[type='checkbox'], input[type='radio']):not(
|
|
82
|
+
:where([data-sempods-ui] *)
|
|
83
|
+
) {
|
|
84
|
+
flex: none;
|
|
85
|
+
width: 24px;
|
|
86
|
+
height: 24px;
|
|
87
|
+
margin: 0;
|
|
88
|
+
margin-inline-end: 10px;
|
|
89
|
+
accent-color: var(--sempods-accent);
|
|
90
|
+
}
|
|
91
|
+
:where(label:has(input[type='checkbox'], input[type='radio'])):not(
|
|
92
|
+
:where([data-sempods-ui] *)
|
|
93
|
+
) {
|
|
94
|
+
display: inline-flex;
|
|
95
|
+
align-items: center;
|
|
96
|
+
min-width: 44px;
|
|
97
|
+
min-height: 44px;
|
|
98
|
+
cursor: pointer;
|
|
99
|
+
}
|
|
100
|
+
:where(button, input, select, textarea, a):not(
|
|
101
|
+
:where([data-sempods-ui] *, [data-sempods-button])
|
|
102
|
+
):where(:focus-visible) {
|
|
103
|
+
outline: 2px solid var(--sempods-accent);
|
|
104
|
+
outline-offset: 2px;
|
|
34
105
|
}
|
|
35
|
-
|
|
36
|
-
|
|
106
|
+
/* Lists of items, without the browser's indentation and bullets. */
|
|
107
|
+
:where(ul):not(:where([data-sempods-ui] *)) {
|
|
108
|
+
padding-inline-start: 0;
|
|
109
|
+
list-style: none;
|
|
37
110
|
}
|