@plitzi/sdk-server 0.37.0 → 0.37.2
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 +155 -0
- package/README.md +26 -0
- package/dist/actions.d.ts +14 -6
- package/dist/actions.js +7 -3
- package/dist/core/server/baseServer.d.ts +4 -1
- package/dist/core/server/baseServer.js +2 -1
- package/dist/core/server/closeOnSignals.d.ts +28 -0
- package/dist/core/server/closeOnSignals.js +46 -0
- package/dist/core/server/pageServer.js +3 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -1
- package/dist/modules/actions/index.d.ts +12 -3
- package/dist/modules/actions/index.js +9 -3
- package/dist/modules/actions/jobs/index.d.ts +32 -0
- package/dist/modules/actions/jobs/index.js +57 -0
- package/dist/modules/actions/jobs/memoryQueue.d.ts +30 -0
- package/dist/modules/actions/jobs/memoryQueue.js +178 -0
- package/dist/modules/actions/jobs/memoryStores.test.d.ts +1 -0
- package/dist/modules/actions/jobs/scheduler.d.ts +62 -0
- package/dist/modules/actions/jobs/scheduler.js +125 -0
- package/dist/modules/actions/jobs/scheduler.test.d.ts +1 -0
- package/dist/modules/actions/jobs/schedules.d.ts +23 -0
- package/dist/modules/actions/jobs/schedules.js +47 -0
- package/dist/modules/actions/jobs/storeClock.d.ts +16 -0
- package/dist/modules/actions/jobs/storeClock.js +32 -0
- package/dist/modules/actions/jobs/testing/jobQueueContract.d.ts +25 -0
- package/dist/modules/actions/jobs/worker.d.ts +62 -0
- package/dist/modules/actions/jobs/worker.js +262 -0
- package/dist/modules/actions/jobs/worker.test.d.ts +1 -0
- package/dist/modules/actions/moduleFor.js +1 -0
- package/dist/modules/actions/runtime/precheck.js +1 -1
- package/dist/modules/actions/runtime/renderResolver.js +2 -2
- package/dist/modules/actions/transport/callHandler.js +1 -1
- package/dist/modules/actions/transport/webhookHandler.js +1 -1
- package/dist/modules/actions/types.d.ts +76 -1
- package/dist/modules/mongo/index.d.ts +4 -0
- package/dist/modules/mongo/jobQueue.d.ts +38 -0
- package/dist/modules/mongo/jobQueue.js +335 -0
- package/dist/modules/mongo/kv.d.ts +15 -0
- package/dist/modules/mongo/kv.js +98 -0
- package/dist/modules/mongo/stores.integration.test.d.ts +1 -0
- package/dist/modules/mysql/jobs/index.d.ts +5 -0
- package/dist/modules/mysql/jobs/jobQueue.d.ts +40 -0
- package/dist/modules/mysql/jobs/jobQueue.js +375 -0
- package/dist/modules/mysql/jobs/kv.d.ts +18 -0
- package/dist/modules/mysql/jobs/kv.js +78 -0
- package/dist/modules/mysql/jobs/schema.d.ts +24 -0
- package/dist/modules/mysql/jobs/schema.js +96 -0
- package/dist/modules/mysql/jobs/stores.integration.test.d.ts +1 -0
- package/dist/modules/ssr/Component.d.ts +2 -5
- package/dist/modules/ssr/Component.js +1 -2
- package/dist/modules/ssr/prepareRender.js +1 -4
- package/dist/mongo.d.ts +25 -0
- package/dist/mongo.js +3 -0
- package/dist/mysql.d.ts +11 -0
- package/dist/mysql.js +4 -1
- package/package.json +16 -6
- package/skills/plitzi-authoring/SKILL.md +73 -267
- package/skills/plitzi-authoring/reference/data-and-visibility.md +114 -0
- package/skills/plitzi-authoring/reference/elements-and-styles.md +107 -0
- package/skills/plitzi-authoring/reference/flows.md +80 -0
- package/skills/plitzi-authoring/reference/layouts.md +140 -0
- package/skills/plitzi-authoring/reference/review-checklist.md +50 -0
- package/skills/plitzi-authoring/reference/structure.md +83 -0
- package/skills/plitzi-authoring/reference/templates-and-export.md +54 -0
- package/skills/plitzi-authoring/reference/templates.md +69 -0
- package/skills/plitzi-authoring/reference/testing.md +52 -0
- package/dist/modules/actions/runtime/schedule.d.ts +0 -35
- package/dist/modules/actions/runtime/schedule.js +0 -76
- /package/dist/{modules/actions/runtime/schedule.test.d.ts → core/server/closeOnSignals.test.d.ts} +0 -0
package/dist/mongo.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Job queue and key/value helpers for a deployment that runs Mongo. Import them as `@plitzi/sdk-server/mongo`.
|
|
2
|
+
*
|
|
3
|
+
* The scheduler and the workers are the package's; WHERE the jobs wait is the deployment's — `action.jobs.queue`
|
|
4
|
+
* and `action.kv` are adapters, and the defaults keep everything in one process's memory. That is right for one
|
|
5
|
+
* replica and wrong for two: each gets its own copy of every schedule, and the jobs a replica was holding go with it
|
|
6
|
+
* when it stops. These are those adapters, already written for Mongo, over a database the deployment connects to
|
|
7
|
+
* and owns:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { MongoClient } from 'mongodb';
|
|
11
|
+
* import { createServer } from '@plitzi/sdk-server';
|
|
12
|
+
* import { createMongoJobQueue, createMongoKv } from '@plitzi/sdk-server/mongo';
|
|
13
|
+
*
|
|
14
|
+
* const db = new MongoClient(process.env.MONGO_URL).db('app');
|
|
15
|
+
*
|
|
16
|
+
* createServer({
|
|
17
|
+
* adapters,
|
|
18
|
+
* action: { lookups, kv: createMongoKv({ db }), jobs: { queue: createMongoJobQueue({ db }) } }
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* The package holds no connection and keeps no data of its own: it creates the indexes its queries need, in the
|
|
23
|
+
* collections it is told to use. `mongodb` is an optional peer dependency, used for its types alone. */
|
|
24
|
+
export { createMongoJobQueue, createMongoKv } from './modules/mongo';
|
|
25
|
+
export type { MongoDbSource, MongoJobQueue, MongoJobQueueOptions, MongoKvOptions } from './modules/mongo';
|
package/dist/mongo.js
ADDED
package/dist/mysql.d.ts
CHANGED
|
@@ -22,5 +22,16 @@
|
|
|
22
22
|
* The tables are documented in `docs/auth/mysql-schema.md` — what each one is for, and what a deployment mapping
|
|
23
23
|
* its existing schema onto the adapters has to be able to answer. */
|
|
24
24
|
export { createMysqlStore, mysqlSchemaStatements } from './modules/mysql';
|
|
25
|
+
/**
|
|
26
|
+
* The job queue and the key/value store server actions run on, for a deployment that keeps them in MySQL rather than
|
|
27
|
+
* in one process's memory — which is what running more than one replica needs. They borrow connections from a pool
|
|
28
|
+
* the deployment opened (`store.pool` works) and create their three tables on first use:
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* createServer({ action: { lookups, kv: createMysqlKv({ pool }), jobs: { queue: createMysqlJobQueue({ pool }) } } });
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export { createMysqlJobQueue, createMysqlKv, mysqlJobSchemaStatements } from './modules/mysql/jobs';
|
|
35
|
+
export type { MysqlJobQueue, MysqlJobQueueOptions, MysqlKvOptions } from './modules/mysql/jobs';
|
|
25
36
|
export { SCHEMA_VERSION, TABLE_NAMES, tableNames } from './modules/mysql';
|
|
26
37
|
export type { AccountSeed, MysqlAdmin, MysqlConfig, IdentityStore, LinkedIdentity, MysqlStore, SessionStore, SessionSummary, SpaceTokenSeed, TableKey, Tables } from './modules/mysql';
|
package/dist/mysql.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { TABLE_NAMES, tableNames } from "./modules/mysql/config.js";
|
|
2
2
|
import { SCHEMA_VERSION } from "./modules/mysql/schema.js";
|
|
3
3
|
import { createMysqlStore, mysqlSchemaStatements } from "./modules/mysql/index.js";
|
|
4
|
-
|
|
4
|
+
import { mysqlJobSchemaStatements } from "./modules/mysql/jobs/schema.js";
|
|
5
|
+
import { createMysqlJobQueue } from "./modules/mysql/jobs/jobQueue.js";
|
|
6
|
+
import { createMysqlKv } from "./modules/mysql/jobs/kv.js";
|
|
7
|
+
export { SCHEMA_VERSION, TABLE_NAMES, createMysqlJobQueue, createMysqlKv, createMysqlStore, mysqlJobSchemaStatements, mysqlSchemaStatements, tableNames };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plitzi/sdk-server",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.2",
|
|
4
4
|
"license": "AGPL-3.0",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -35,6 +35,11 @@
|
|
|
35
35
|
"import": "./dist/mysql.js",
|
|
36
36
|
"default": "./dist/mysql.js"
|
|
37
37
|
},
|
|
38
|
+
"./mongo": {
|
|
39
|
+
"types": "./dist/mongo.d.ts",
|
|
40
|
+
"import": "./dist/mongo.js",
|
|
41
|
+
"default": "./dist/mongo.js"
|
|
42
|
+
},
|
|
38
43
|
"./handlers": {
|
|
39
44
|
"types": "./dist/handlers.d.ts",
|
|
40
45
|
"import": "./dist/handlers.js",
|
|
@@ -58,19 +63,23 @@
|
|
|
58
63
|
"type": "module",
|
|
59
64
|
"sideEffects": false,
|
|
60
65
|
"dependencies": {
|
|
61
|
-
"@plitzi/plitzi-sdk": "0.37.
|
|
62
|
-
"@plitzi/sdk-shared": "0.37.
|
|
66
|
+
"@plitzi/plitzi-sdk": "0.37.2",
|
|
67
|
+
"@plitzi/sdk-shared": "0.37.2",
|
|
63
68
|
"ejs": "^6.0.1",
|
|
64
69
|
"esbuild": "^0.28.2",
|
|
65
70
|
"jsonwebtoken": "^9.0.3",
|
|
66
71
|
"nodemailer": "^10.0.10"
|
|
67
72
|
},
|
|
68
73
|
"peerDependencies": {
|
|
74
|
+
"mongodb": "^6 || ^7",
|
|
69
75
|
"mysql2": "^3",
|
|
70
76
|
"react": "^19",
|
|
71
77
|
"react-dom": "^19"
|
|
72
78
|
},
|
|
73
79
|
"peerDependenciesMeta": {
|
|
80
|
+
"mongodb": {
|
|
81
|
+
"optional": true
|
|
82
|
+
},
|
|
74
83
|
"mysql2": {
|
|
75
84
|
"optional": true
|
|
76
85
|
}
|
|
@@ -78,20 +87,21 @@
|
|
|
78
87
|
"devDependencies": {
|
|
79
88
|
"@types/ejs": "^3.1.5",
|
|
80
89
|
"@types/jsonwebtoken": "^9.0.10",
|
|
81
|
-
"@types/node": "^26.6.
|
|
90
|
+
"@types/node": "^26.6.2",
|
|
82
91
|
"@types/nodemailer": "^8.0.2",
|
|
83
92
|
"@types/react": "^19.3.0",
|
|
84
93
|
"@vitejs/plugin-react": "^6.1.1",
|
|
85
94
|
"@vitest/coverage-v8": "^5.0.1",
|
|
86
95
|
"eslint": "^9.39.5",
|
|
87
96
|
"eslint-plugin-import": "^2.32.0",
|
|
97
|
+
"mongodb": "^7.6.0",
|
|
88
98
|
"mysql2": "^3.24.4",
|
|
89
99
|
"react": "^19.3.0",
|
|
90
100
|
"react-dom": "^19.3.0",
|
|
91
|
-
"tsx": "^4.23.
|
|
101
|
+
"tsx": "^4.23.15",
|
|
92
102
|
"typescript": "^6.0.3",
|
|
93
103
|
"vite": "^8.3.0",
|
|
94
|
-
"vite-plugin-dts": "^5.1.
|
|
104
|
+
"vite-plugin-dts": "^5.1.1",
|
|
95
105
|
"vitest": "^5.0.1"
|
|
96
106
|
},
|
|
97
107
|
"scripts": {
|
|
@@ -1,283 +1,89 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: plitzi-authoring
|
|
3
3
|
description: >-
|
|
4
|
-
Write a Plitzi space or
|
|
5
|
-
instead of hand-writing schema JSON. Use whenever the task is to
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
Write or change a Plitzi space or template in TypeScript with @plitzi/sdk-authoring — pages, layouts, elements,
|
|
5
|
+
CSS classes, data bindings, providers and flows — instead of hand-writing schema JSON. Use whenever the task is to
|
|
6
|
+
create, extend, restyle or fix a space: adding a page or a section, sharing a header across pages, binding an
|
|
7
|
+
element to data, showing or hiding something, wiring what happens on click, or turning an exported JSON into code.
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Authoring Plitzi spaces
|
|
11
11
|
|
|
12
|
-
A
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
A space is two JSON documents — a schema and a style — full of cross-referenced ids. **Never write them by hand.**
|
|
13
|
+
`@plitzi/sdk-authoring` derives every id, class name, parent link, breakpoint map and flow chain from a small
|
|
14
|
+
declaration, and refuses a declaration that would not render.
|
|
15
15
|
|
|
16
16
|
```ts
|
|
17
|
-
import { authorSpace, container, heading, text } from '@plitzi/sdk-authoring';
|
|
17
|
+
import { authorSpace, container, heading, styles, text } from '@plitzi/sdk-authoring';
|
|
18
18
|
|
|
19
|
-
const {
|
|
19
|
+
const page = styles('page', { display: 'flex', 'flex-direction': 'column', padding: '96px 24px', gap: '16px' });
|
|
20
|
+
|
|
21
|
+
export const space = {
|
|
20
22
|
name: 'My space',
|
|
21
23
|
permanentUrl: 'my-space',
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
{ name: 'Home', slug: '', class: 'page', body: [heading('Hello', { subType: 'h1' }), text('A paragraph.')] }
|
|
25
|
-
]
|
|
26
|
-
});
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Serve it with the adapters the page server already takes:
|
|
24
|
+
pages: [{ name: 'Home', slug: '', class: page, body: [heading('Hello', { subType: 'h1' }), text('A paragraph.')] }]
|
|
25
|
+
};
|
|
30
26
|
|
|
31
|
-
|
|
32
|
-
createServer({ port: 3001, adapters: createJsonAdapters({ offlineData: { schema, style } }) });
|
|
27
|
+
const { schema, style, warnings } = authorSpace(space);
|
|
33
28
|
```
|
|
34
29
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
30
|
+
The published `.d.ts` is the reference for every factory, field and step builder
|
|
31
|
+
(`node_modules/@plitzi/sdk-authoring/dist/index.d.ts`) — read it instead of guessing a name or a param.
|
|
32
|
+
|
|
33
|
+
## How to work
|
|
34
|
+
|
|
35
|
+
1. **Read before you write.** Open `src/space.ts` (and whatever it imports) and find the layout, the classes and the
|
|
36
|
+
helpers already there. Extend them; do not add a second way of doing something the space already does.
|
|
37
|
+
2. **Change the declaration, then author it.** `npm run author` (or restart the server) runs `authorSpace`. A refusal
|
|
38
|
+
is an error that names the element and the fix — fix the declaration, never work around the check.
|
|
39
|
+
3. **Read every warning.** Each one names something written that will not do what it says. Zero warnings is the bar.
|
|
40
|
+
4. **Look at it.** `npm run visual` opens the page in a browser. Check desktop, tablet and mobile, light and dark,
|
|
41
|
+
and the page while its data is still loading — not only the finished frame.
|
|
42
|
+
5. **Go through the [review checklist](reference/review-checklist.md) before saying it is done.** It is the feedback a
|
|
43
|
+
reviewer gives on every change, written down so you do not need to hear it.
|
|
44
|
+
|
|
45
|
+
## The rules that matter most
|
|
46
|
+
|
|
47
|
+
1. **Name what is referred to.** Give an `id` to every element a binding, a flow or a test addresses. Ids are ONE
|
|
48
|
+
namespace for the whole space — layouts and every page share it — so an element built by a helper that runs more
|
|
49
|
+
than once gets its id prefixed by what it is for: `` `${pageId}-foot` ``.
|
|
50
|
+
2. **Share with classes and layouts, never with copies.** A look used twice is a `styles()` class. Chrome shown on
|
|
51
|
+
several pages — a header, a sidebar, a footer — is a **layout** the pages name, written once. A menu, a card grid,
|
|
52
|
+
a list of steps is DATA mapped to elements (`entries.map(entry => link(…))`), not a block pasted per item. See
|
|
53
|
+
[layouts and duplication](reference/layouts.md).
|
|
54
|
+
3. **One element, one base selector.** An element takes a shared `class` OR its own `css`, never both — authoring
|
|
55
|
+
refuses the pair. A look that never changes is its own class.
|
|
56
|
+
4. **Visible by default; decide which way the logic flips.** An element is on screen unless something hides it. When
|
|
57
|
+
the logic REVEALS it (hidden → shown: an empty state, a "get started" card, an admin-only panel), it starts hidden —
|
|
58
|
+
`visible: 'source'`, or `visible: false` plus a binding for a computed condition — so it never flashes while data
|
|
59
|
+
loads (`condition-starts-visible` warns). When a flag HIDES it (shown → hidden: a sidebar label until the sidebar
|
|
60
|
+
is folded), it keeps the default and an absent flag must leave it shown. See
|
|
61
|
+
[data and visibility](reference/data-and-visibility.md).
|
|
62
|
+
5. **Know where a template is evaluated.** A binding's `twigTemplate` and a step's params evaluate full Twig. An
|
|
63
|
+
ATTRIBUTE only resolves a name with filters (`{{ post.slug|url_encode }}`); a condition there is used as written.
|
|
64
|
+
Inside any template a source is spelled in full — `apiContainer_stats`, `list_rows` — never by the element's short
|
|
65
|
+
id. See [templates](reference/templates.md).
|
|
66
|
+
6. **Tokens, not colours.** Every colour is a variable of the space with a `light` and a `dark` value; both themes are
|
|
67
|
+
checked from the first commit.
|
|
68
|
+
7. **Times in UTC, and say so.** Format with an explicit zone (`|date('j M · H:i', 'UTC')`) and print "UTC" beside it:
|
|
69
|
+
a page rendered on a server and hydrated in a browser in another zone must agree on the hour.
|
|
70
|
+
8. **Breakpoints are ranges.** `tablet` (48–64rem) and `mobile` (below 48rem) each inherit only from `desktop`; a rule
|
|
71
|
+
meant for both is written in both.
|
|
72
|
+
9. **Never invent data.** Numbers, names and states on screen come from a source. A panel with nothing true to say is
|
|
73
|
+
an empty state, not a placeholder figure.
|
|
74
|
+
10. **Never hand-write** `flat`, derived ids, `styleSelectors`, `beforeNode`/`afterNode`/`flowId`, or a
|
|
75
|
+
`styleVariant` binding's key — use the factories, `variantFrom` and `activeOn`.
|
|
76
|
+
|
|
77
|
+
## References
|
|
78
|
+
|
|
79
|
+
| Read | When |
|
|
58
80
|
| --- | --- |
|
|
59
|
-
|
|
|
60
|
-
|
|
|
61
|
-
|
|
|
62
|
-
| `
|
|
63
|
-
|
|
|
64
|
-
|
|
|
65
|
-
|
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
| `children` | the tree |
|
|
69
|
-
| `meta` | `meta.label` is the element's name in the builder's tree |
|
|
70
|
-
|
|
71
|
-
Anything else in the object is an **attribute**. `label` belongs to the attribute (a link and a form control both
|
|
72
|
-
have one); the builder's name for the element is `meta.label`.
|
|
73
|
-
|
|
74
|
-
A type this SDK does not ship — a plugin, one the deployment brings — is authored the same way:
|
|
75
|
-
|
|
76
|
-
```ts
|
|
77
|
-
const speciesStatus = defineElement<{ status?: string }>({ type: 'speciesStatus', content: { … } });
|
|
78
|
-
element<{ status?: string }>('speciesStatus', { status: 'vulnerable' }); // or one at a time
|
|
79
|
-
elementsFromManifest(manifest); // or a whole plugin manifest
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
## Style
|
|
83
|
-
|
|
84
|
-
Write CSS the way you write CSS, shorthands included — they are expanded before they reach the document, because
|
|
85
|
-
Plitzi's style editor reads a closed list of longhand properties:
|
|
86
|
-
|
|
87
|
-
```ts
|
|
88
|
-
css({ padding: '96px 24px', border: '1px solid var(--line)', 'border-radius': '12px' })
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
You rarely call `css` yourself: `authorSpace` runs every rule set through it. What you get is the refusal — a
|
|
92
|
-
property outside the vocabulary is an error naming the correct key (`paddingTop` → `padding-top`).
|
|
93
|
-
|
|
94
|
-
- `column(gap, extra?)`, `row(gap, extra?)`, `grid(columns, gap, extra?)` for the three layouts every space repeats.
|
|
95
|
-
- **Breakpoints are ranges, not a cascade.** `tablet` is 48–64rem and `mobile` is below 48rem, and each inherits
|
|
96
|
-
only from `desktop` — a rule written for `tablet` never reaches a phone. A grid that collapses to one column at
|
|
97
|
-
tablet and should stay collapsed says so in `mobile` too. `authorSpace` warns (`tablet-rule-skips-mobile`) when it
|
|
98
|
-
does not.
|
|
99
|
-
- **Fonts are declared, not assumed.** A `font-family` loads only if the space lists the face in `fonts`
|
|
100
|
-
(`{ source: 'google', family: 'Fraunces', fallback: 'Georgia, serif', weights: [400, 600], styles: ['normal'] }`);
|
|
101
|
-
the page server writes exactly that list into the document, and anything else renders in the fallback.
|
|
102
|
-
- Per element TYPE defaults go in `elements: { heading: { base: …, states: { … }, variants: { … }, slots: { … } } }`
|
|
103
|
-
— `slots` dresses the type's other selectors (a modal's `rootContainer`) for every element of the type.
|
|
104
|
-
- **`:hover` is part of the selector, not a rule in `customCss`.** `styles('card', { css: { … }, states: { hover: { … } },
|
|
105
|
-
variants: { active: { … } } })`, and `states` beside an element's own `css`. Written in `customCss` it renders and
|
|
106
|
-
then cannot be read back or edited per breakpoint.
|
|
107
|
-
|
|
108
|
-
**Share a rule as a class, never as a spread.** Writing a rule set once in a `const` and spreading it into each
|
|
109
|
-
element's `css` shares the source and duplicates the document — one selector per element, so re-theming the card
|
|
110
|
-
in the builder re-themes one card. `styles()` declares the class where it is used and writes it once:
|
|
111
|
-
|
|
112
|
-
```ts
|
|
113
|
-
const card = styles('card', { padding: '24px', 'border-radius': '12px', 'background-color': 'var(--surface)' });
|
|
114
|
-
|
|
115
|
-
container({ class: card, children: [ … ] });
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
Accepted anywhere a class name is — `class`, a `slot`, a page's `class` — and collected from wherever the tree
|
|
119
|
-
names it. One name declared twice with rules that disagree is refused. `classes` at the top of the space is the
|
|
120
|
-
same mechanism for what describes the space rather than one section of it.
|
|
121
|
-
|
|
122
|
-
An element has exactly ONE base selector, so a look that never changes is its own class over a shared plain object
|
|
123
|
-
(`styles('button-primary', { ...base, … })`), never two classes layered on top of each other. A class's `variants`
|
|
124
|
-
are for what the page switches while it runs — a binding with the `styleVariant` transformer picks one.
|
|
125
|
-
|
|
126
|
-
**Shells go in `layouts`.** A header or a sidebar every page shows is a layout the pages name —
|
|
127
|
-
`layout: { id: 'app-shell', slot: 'main' }`, the slot being the element in the shell where the body goes — never a
|
|
128
|
-
copy on each page. `visible: false` starts an element hidden for a flow to reveal.
|
|
129
|
-
|
|
130
|
-
## Data
|
|
131
|
-
|
|
132
|
-
```ts
|
|
133
|
-
heading({ bind: { content: 'posts.title' } }) // short form: attributes
|
|
134
|
-
paragraph({ visible: 'posts.hasPosts' }) // on screen only while this is true
|
|
135
|
-
paragraph({ visible: '!posts.hasPosts' }) // …and its inverse
|
|
136
|
-
container({ bind: [{ to: 'content', source: 'x.y', transformers: [ … ] }] }) // full form
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
**A source names the id you gave the element** — `'posts.title'`, `'postList.item.cover'` — and the prefix is
|
|
140
|
-
filled in. Only half of a source name is yours: the other half is the kind of source the ELEMENT publishes, and it
|
|
141
|
-
is not always the word you can see (a `form` publishes under `apiContainer`). The four globals — `variables`,
|
|
142
|
-
`navigation`, `auth`, `state` — are named as themselves. A full name still works and is checked the same way: a
|
|
143
|
-
prefix that does not match the element, or a name nothing answers to, is refused when the space is authored. It
|
|
144
|
-
is the quietest failure a space can carry.
|
|
145
|
-
|
|
146
|
-
Server-resolved sections are an `apiContainer` with `runtime: 'server'` naming a connector or an action, and a
|
|
147
|
-
space that has any needs `rsc: { enabled: true }`.
|
|
148
|
-
|
|
149
|
-
**A provider has no box unless you give it a tag.** An `apiContainer` renders no element of its own while its
|
|
150
|
-
`subType` is empty — the default, the builder's "Container Tag: None" — so its children sit directly in the parent's
|
|
151
|
-
layout. That is usually what you want (the provider stays invisible to the layout), and it means a `class` or `css`
|
|
152
|
-
on it styles nothing: the `gap` you wrote between its sections is simply not there. Put the layout on the parent, or
|
|
153
|
-
give the provider a tag (`attributes: { subType: 'div' }`) when it should be the box. `validateSpace` — and so
|
|
154
|
-
`authorSpace` and the MCP — warns with `STYLE_WITHOUT_TAG`.
|
|
155
|
-
|
|
156
|
-
**Never ask the data for a field's opposite.** Both sides of one question are `visible: 'x'` and `visible: '!x'` —
|
|
157
|
-
not an `x` and a `notX` beside it in the server's answer. The `!` is the `not` transformer, which reads a boolean
|
|
158
|
-
that travelled as text (`"false"`, `"0"`) and treats an empty array as false; an empty object is true. Only for a
|
|
159
|
-
real inverse — a three-state condition (`Boolean(post) && !canEdit`) still belongs where the data is made.
|
|
160
|
-
|
|
161
|
-
## Flows
|
|
162
|
-
|
|
163
|
-
```ts
|
|
164
|
-
button({
|
|
165
|
-
id: 'cta',
|
|
166
|
-
content: 'Get a quote',
|
|
167
|
-
flows: [[
|
|
168
|
-
onClick(),
|
|
169
|
-
named('quote', runServerAction({ actionId: 'shipping-quote', input: '{"city":"Berlin"}', mode: 'await' })),
|
|
170
|
-
setState({ key: 'quote', type: 'text', value: '{{quote.output.summary}}' })
|
|
171
|
-
]]
|
|
172
|
-
})
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
Use the step builders — they answer the three things that go wrong silently:
|
|
176
|
-
|
|
177
|
-
- **Where a step runs.** A global callback registers under its source MODULE (`state`, `auth`, `actions`), an
|
|
178
|
-
element callback under an element's id, a utility under nothing. The builders fill it in; a trigger and an
|
|
179
|
-
untargeted `updateElement` are filled with the element the flow was declared on.
|
|
180
|
-
- **Which `setState`.** `setState(…)` writes `runtime.state.<key>`; `updateElement(…)` changes one element's own
|
|
181
|
-
attribute or state. Different params, different node kind. Each has a flip-it counterpart storing the opposite
|
|
182
|
-
of what is there, which is how expand/collapse is ONE step on ONE trigger: `toggleState({ key })` for app state,
|
|
183
|
-
and `toggleElement({ category: 'state', key: 'visibility' }, 'panel')` to show/hide an element (the second
|
|
184
|
-
argument is the element it acts on; omitted, it acts on the one the flow is declared on). Never two branches
|
|
185
|
-
under opposite `when` guards — those read the state as it was when the flow STARTED, so they run a click behind.
|
|
186
|
-
- **What it takes.** Params are typed from the same declaration the builder's own panel is drawn from.
|
|
187
|
-
|
|
188
|
-
`named(id, step)` is how a later step reads an earlier one — the flow scope is keyed by node id, so
|
|
189
|
-
`{{quote.output.summary}}` resolves only when that step is called `quote`. `mode: 'await'` is what puts a run's
|
|
190
|
-
answer in the scope at all, and it is the default.
|
|
191
|
-
|
|
192
|
-
**Pass `input` as an object, not as a line of JSON text.** Both are accepted, and the text form fails silently: an
|
|
193
|
-
interpolated value carrying a quotation mark or a newline — a post body, a comment — makes it unparseable, and
|
|
194
|
-
unparseable input posts `{}` rather than refusing.
|
|
195
|
-
|
|
196
|
-
A flow is a list and never a tree, so "only if" is expressed on the step:
|
|
197
|
-
|
|
198
|
-
```ts
|
|
199
|
-
whenSucceeded('quote', navigate({ urlType: 'internal', url: '{{quote.output.url}}' })),
|
|
200
|
-
whenFailed('quote', setState({ key: 'notice', type: 'text', value: '{{quote.reason}}' })),
|
|
201
|
-
when({ field: 'state.count', operator: '>', value: 3 }, addNotification({ content: 'Enough' }))
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
`whenFailed` matches every outcome that is not `completed` — a run also comes back `skipped` or `aborted`, and
|
|
205
|
-
matching only `failed` is how those two end up doing nothing.
|
|
206
|
-
|
|
207
|
-
## What gets refused
|
|
208
|
-
|
|
209
|
-
`authorSpace` throws rather than hand back a space that would not render:
|
|
210
|
-
|
|
211
|
-
- a CSS property the style editor could not read back
|
|
212
|
-
- a `class` or a `slot` naming a class the space does not declare (the error names the one you probably meant)
|
|
213
|
-
- an element asking for a shared class AND rules of its own — an element has one base selector
|
|
214
|
-
- one class name declared twice with rules that disagree
|
|
215
|
-
- a binding source naming an element nothing answers to, or one whose prefix is not what that element publishes
|
|
216
|
-
- a name that shadows a global data source (`variables`, `navigation`, `auth`, `state`)
|
|
217
|
-
- a step target naming an element that is not there
|
|
218
|
-
- two elements answering to one name, a broken flow chain, an orphan, a cycle
|
|
219
|
-
- a global callback on the wrong module — or on none — and a utility given one. A global callback registers under
|
|
220
|
-
its SOURCE MODULE (`auth`, `state`, `actions`), and the pair is what the runtime resolves a step by, so naming
|
|
221
|
-
either half wrong is a control that does nothing at all with no error anywhere. An action no built-in source
|
|
222
|
-
declares comes back in `warnings` instead of throwing, because a plugin may register a module of its own.
|
|
223
|
-
|
|
224
|
-
Those last checks need the vocabulary, which only the composed package has. Import `authorSpace` from
|
|
225
|
-
`@plitzi/sdk-authoring` — not straight from `@plitzi/sdk-schema` — and you get them.
|
|
226
|
-
|
|
227
|
-
For documents you did not author here — an export from the builder, a JSON edited by hand — run the same gate
|
|
228
|
-
before serving it:
|
|
229
|
-
|
|
230
|
-
```ts
|
|
231
|
-
const { valid, errors, warnings } = validateSpace({ schema, style });
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
## Templates
|
|
235
|
-
|
|
236
|
-
A template is not a space: it is ONE subtree, the style that dresses it and a name, published as a JSON someone
|
|
237
|
-
fetches by URL and drags onto a canvas you will never see.
|
|
238
|
-
|
|
239
|
-
```ts
|
|
240
|
-
import { authorTemplate } from '@plitzi/sdk-authoring';
|
|
241
|
-
|
|
242
|
-
const { template, warnings } = authorTemplate({
|
|
243
|
-
name: 'Pricing card',
|
|
244
|
-
description: 'A price, a list of features and a call to action.',
|
|
245
|
-
classes: { card: { padding: '24px', 'border-radius': '12px', 'background-color': 'var(--surface)' } },
|
|
246
|
-
root: container({ class: 'card', children: [heading('$19', { subType: 'h3' }), button({ content: 'Start' })] })
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
await writeFile('pricing-card.json', JSON.stringify(template, null, 2));
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
Host that file and add it to a space's resources as an `application/json` — nothing else is required of you.
|
|
253
|
-
|
|
254
|
-
Everything a space is held to still applies, and two more things apply because a template LEAVES its space:
|
|
255
|
-
|
|
256
|
-
- **Everything it names, it carries.** A class declared in the space it was cut from does not travel; the element
|
|
257
|
-
keeps the class name, finds no rules wherever it is dropped, and renders unstyled. Declare in `classes` (or with
|
|
258
|
-
`styles()`) every rule the subtree names — `validateTemplate` warns about a name the manifest does not carry.
|
|
259
|
-
- **A binding may not point outside the subtree.** A source names an element by id, so binding to a provider
|
|
260
|
-
that stayed behind is dead on arrival — refused. Bring the provider (the `apiContainer`, the `form`) into the
|
|
261
|
-
template, or bind to one of the globals: `variables`, `navigation`, `auth`, `state`.
|
|
262
|
-
|
|
263
|
-
`validateTemplate(template)` runs the same gate over a manifest you did not author here — one exported by the
|
|
264
|
-
builder, or edited by hand — before you publish it.
|
|
265
|
-
|
|
266
|
-
## From an exported JSON
|
|
267
|
-
|
|
268
|
-
Never rewrite an exported space by hand. `specFromSpace({ schema, style })` reads it into the spec that authors it,
|
|
269
|
-
repairing what an older builder left behind and listing each repair in `corrections`; `specToSource(spec,
|
|
270
|
-
{ exportName })` writes that spec as factory calls; `compareSpaces(original, authorSpace(spec))` must list nothing but
|
|
271
|
-
what `corrections` explains. The builder's **Export** (and `POST /utils/transform-to-authoring`) does all three. Edit
|
|
272
|
-
the code it writes from then on, not the JSON.
|
|
273
|
-
|
|
274
|
-
## Rules
|
|
275
|
-
|
|
276
|
-
1. **Never hand-write `flat`, element ids, `styleSelectors`, `beforeNode`/`afterNode`/`flowId`.** They are derived.
|
|
277
|
-
Writing them is how a space half-renders with nothing reporting why.
|
|
278
|
-
2. **Name what is referred to.** `id` on any element a binding, a step or a pager addresses.
|
|
279
|
-
3. **Read the error.** Every refusal above names the element, the class or the property at fault; it is a bug in
|
|
280
|
-
the declaration, not a reason to work around the check.
|
|
281
|
-
4. **Ids are stable.** They are hashes of the path that produced them, so re-authoring an unchanged space writes
|
|
282
|
-
byte-identical documents — a seed can re-run and a diff stays readable.
|
|
283
|
-
5. **`warnings` is returned, not printed.** Read it if the space is generated in a build.
|
|
81
|
+
| [elements-and-styles.md](reference/elements-and-styles.md) | Any element or CSS: factories, fields, classes, states, variants, tokens, fonts, lists, links |
|
|
82
|
+
| [layouts.md](reference/layouts.md) | Anything shown on more than one page; menus; reducing duplication of elements and styles |
|
|
83
|
+
| [data-and-visibility.md](reference/data-and-visibility.md) | Bindings, providers, loading/empty/error states, live data, caching, showing and hiding |
|
|
84
|
+
| [templates.md](reference/templates.md) | Any `{{ … }}` or `{% … %}`: where it runs, naming sources, filters, tests, dates |
|
|
85
|
+
| [flows.md](reference/flows.md) | Clicks, submits, page loads, server actions, modals, state |
|
|
86
|
+
| [structure.md](reference/structure.md) | A space bigger than one screen: files, helpers, naming, keeping it short |
|
|
87
|
+
| [testing.md](reference/testing.md) | Handles, visual checks, catching a flash from the first frame |
|
|
88
|
+
| [templates-and-export.md](reference/templates-and-export.md) | Publishing a template; turning an exported JSON into code |
|
|
89
|
+
| [review-checklist.md](reference/review-checklist.md) | Before you say it is done |
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Data and visibility
|
|
2
|
+
|
|
3
|
+
## Bindings
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
heading({ bind: { content: 'posts.title' } }) // short form: attributes
|
|
7
|
+
text({ bind: [{ to: 'content', source: 'stats.data.total', // full form: anything else
|
|
8
|
+
transformers: [{ action: 'twigTemplate', params: { template: "{{ source|number_format(0, '.', ',') }} views" } }] }] })
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
**A source names the id you gave the element** — `'posts.title'`, `'postList.item.cover'` — and the prefix is
|
|
12
|
+
completed from what the element publishes (`apiContainer_posts`, `list_postList`; a `form` publishes under
|
|
13
|
+
`apiContainer`). A name nothing answers to, or a prefix that does not match, is refused.
|
|
14
|
+
|
|
15
|
+
**Inside a template the name is spelled in full**: `{{ apiContainer_stats.data.total }}`, `{{ list_rows.item.id }}`.
|
|
16
|
+
The binding's `source` is completed for you; its template is read as written, so the short name there is refused.
|
|
17
|
+
|
|
18
|
+
An element re-renders when anything it reads changes: the first part of each binding's `source` and every source its
|
|
19
|
+
templates name. The globals are `variables`, `navigation` (`routeParams`, `queryParams`, `origin`, `currentPageId`),
|
|
20
|
+
`auth` (who is signed in, `status` while that is being found out), `state` (what flows wrote) and `theme`
|
|
21
|
+
(`mode`, `resolved`).
|
|
22
|
+
|
|
23
|
+
## Providers
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
apiContainer({ id: 'orders', query: '{{apiUrl}}/orders', credentials: 'include', children: [ … ] })
|
|
27
|
+
apiContainer({ id: 'board', runtime: 'server', action: 'queue-board' }) // resolved before the HTML
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- A provider's source is readable by its **descendants** only. Wrap what reads it; a sibling cannot see it.
|
|
31
|
+
- It publishes `data` plus `isLoading`, `isEmpty`, `hasError`, `errorMessage` — bind a state to those, not to guesses.
|
|
32
|
+
- A query that depends on state is a binding on `query`; it answers `''` (and fetches nothing) until the state exists:
|
|
33
|
+
`"{{ source ? apiUrl ~ '/workspaces/' ~ source ~ '/stats' : '' }}"` with `source: 'state.workspace.id'`.
|
|
34
|
+
- A space with any `runtime: 'server'` provider needs `rsc: { enabled: true }`.
|
|
35
|
+
|
|
36
|
+
### Live, cached, refreshed
|
|
37
|
+
|
|
38
|
+
- **Live:** `refreshSeconds: 15` asks again on its own (either runtime); it pauses in a hidden tab and never
|
|
39
|
+
overlaps. Use it for queues, feeds, status boards — then mark the panel as live so the reader knows it moves.
|
|
40
|
+
- **Cached:** `cache: true` (and `staleTime` seconds, 30 by default) shares an answer between providers asking the
|
|
41
|
+
same thing and across page changes.
|
|
42
|
+
- **Refreshed by writes:** a `webHook` that is not a GET refreshes every request to its own site by default
|
|
43
|
+
(`invalidateQueries: 'origin'`); a `runServerAction` refreshes everything (`'all'`). Narrow it with
|
|
44
|
+
`'elements'` + `invalidateElements: ['orders']`, or stop it with `'none'`.
|
|
45
|
+
- **Say `'none'` when a refresh would move the page on.** A page that decides where to go from a provider's answer
|
|
46
|
+
("profile complete → leave") will act on the refreshed answer in the middle of a multi-step form that saves as it
|
|
47
|
+
goes, and skip the last step.
|
|
48
|
+
|
|
49
|
+
## Visibility
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
container({ visible: 'posts.hasPosts', … }) // shown while true
|
|
53
|
+
container({ visible: '!posts.hasPosts', … }) // its inverse
|
|
54
|
+
modalContainer({ visible: false, … }) // starts hidden; a flow opens it
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**An element is visible by default.** Which way its condition flips decides how it is written:
|
|
58
|
+
|
|
59
|
+
| The logic… | Example | Write |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| reveals it: hidden until the data says so | empty state, "get started", admin-only panel, error note | `visible: 'src'`, or `visible: false` + a computed binding |
|
|
62
|
+
| hides it: shown unless a flag says otherwise | sidebar labels until the sidebar is folded, a banner until dismissed | a binding with no `visible`; an absent flag must leave it shown |
|
|
63
|
+
|
|
64
|
+
`visible` starts the element **hidden**, and it appears when its data says so. A revealing condition you have to
|
|
65
|
+
compute is `visible: false` — so it waits hidden — plus a visibility binding with a template:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
container({
|
|
69
|
+
id: 'first-steps',
|
|
70
|
+
visible: false,
|
|
71
|
+
bind: [{
|
|
72
|
+
to: 'visibility',
|
|
73
|
+
source: 'stats.data.totals',
|
|
74
|
+
category: 'initialState',
|
|
75
|
+
transformers: [{ action: 'twigTemplate', params: {
|
|
76
|
+
template: "{{ source ? (source.spaces > 0 and source.published > 0 ? 'false' : 'true') : 'false' }}"
|
|
77
|
+
} }]
|
|
78
|
+
}]
|
|
79
|
+
})
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Rules for a condition's template:
|
|
83
|
+
|
|
84
|
+
- **A revealing condition starts hidden with `visible: false`.** A visibility binding alone keeps the element on
|
|
85
|
+
screen until it answers — right for a hiding flag, a flash for a condition on data. `authorSpace` warns
|
|
86
|
+
`condition-starts-visible` when a computed one on a provider starts on screen.
|
|
87
|
+
- **`'true'` shows, `'false'` hides, `''` writes nothing** and leaves the element as it is. Visibility hides only on
|
|
88
|
+
an explicit false.
|
|
89
|
+
- **Answer what "not known yet" should mean.** For a revealing condition that is `'false'` (`source ? … : 'false'`);
|
|
90
|
+
for a hiding flag it is `''` — nothing written, the default stays. A sidebar that answered `'false'` for a
|
|
91
|
+
"folded" state nobody had set yet hid every label of the menu for everybody.
|
|
92
|
+
- **Hide the element itself.** Never wrap it in a container that is shown while the inner one is hidden: an empty
|
|
93
|
+
visible wrapper still takes a slot in its parent's `gap`, and leaves a hole in the page.
|
|
94
|
+
|
|
95
|
+
### Loading, empty, error
|
|
96
|
+
|
|
97
|
+
Four states, and each has its own element:
|
|
98
|
+
|
|
99
|
+
| State | How to tell |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| Loading | `apiContainer_x.isLoading` — or simply nothing: every condition is hidden until data arrives |
|
|
102
|
+
| Empty | the answer ARRIVED and is empty: `{{ source is defined and source is empty ? 'true' : 'false' }}` over the list |
|
|
103
|
+
| Error | `apiContainer_x.hasError` |
|
|
104
|
+
| Data | the list itself |
|
|
105
|
+
|
|
106
|
+
`visible: '!provider.data.items'` is NOT an empty state: before the answer, `!undefined` is true and "Nothing here yet"
|
|
107
|
+
shows on every load. Test for "arrived and empty" (`is defined and … is empty`), or bind to `isEmpty` together with
|
|
108
|
+
`not isLoading`.
|
|
109
|
+
|
|
110
|
+
## Never ask the data for an opposite
|
|
111
|
+
|
|
112
|
+
Both sides of one question are `visible: 'x'` and `visible: '!x'`, not an `x` and a `notX` in the server's answer.
|
|
113
|
+
`!` reads a boolean that travelled as text (`"false"`, `"0"`) correctly. A three-state condition
|
|
114
|
+
(`Boolean(post) && !canEdit`) still belongs where the data is made.
|