@plitzi/sdk-server 0.38.2 → 0.38.4
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 +325 -0
- package/README.md +17 -7
- package/dist/core/createServer.devReload.test.d.ts +1 -0
- package/dist/core/http/stages/devReload.d.ts +18 -0
- package/dist/core/http/stages/devReload.js +40 -0
- package/dist/core/http/stages/images.js +1 -1
- package/dist/core/server/baseServer.d.ts +2 -0
- package/dist/core/server/baseServer.js +1 -0
- package/dist/core/server/drain.d.ts +2 -2
- package/dist/core/server/drain.js +17 -2
- package/dist/core/server/pageServer.js +8 -2
- package/dist/modules/images/imageProxy.js +19 -1
- package/dist/modules/rsc/connectorRscData.js +1 -1
- package/dist/modules/rsc/publicFileResolver.d.ts +3 -0
- package/dist/modules/rsc/publicFileResolver.js +10 -4
- package/dist/modules/ssr/prepareRender.js +1 -0
- package/dist/modules/ssr/views/template.ejs +6 -0
- package/dist/plugins/manager.js +1 -1
- package/package.json +3 -3
- package/skills/plitzi-authoring/CHEATSHEET.md +9 -5
- package/skills/plitzi-authoring/SKILL.md +6 -2
- package/skills/plitzi-authoring/recipes/accordion.ts +58 -0
- package/skills/plitzi-authoring/recipes/carousel.ts +14 -9
- package/skills/plitzi-authoring/recipes/detail-page.ts +10 -6
- package/skills/plitzi-authoring/recipes/every-few-seconds.ts +13 -8
- package/skills/plitzi-authoring/recipes/filter-a-list.ts +2 -2
- package/skills/plitzi-authoring/recipes/forms-and-modals.ts +1 -2
- package/skills/plitzi-authoring/recipes/from-tailwind.ts +1 -1
- package/skills/plitzi-authoring/recipes/link-to-a-section.ts +3 -3
- package/skills/plitzi-authoring/recipes/link-with-data.ts +12 -7
- package/skills/plitzi-authoring/recipes/marquee.ts +2 -2
- package/skills/plitzi-authoring/recipes/scroll-a-row.ts +5 -6
- package/skills/plitzi-authoring/recipes/server-data.ts +65 -0
- package/skills/plitzi-authoring/recipes/show-data.ts +12 -7
- package/skills/plitzi-authoring/recipes/style-a-page.ts +1 -1
- package/skills/plitzi-authoring/recipes/typed-data.ts +9 -6
- package/skills/plitzi-authoring/recipes/usable-without-sight.ts +2 -2
- package/skills/plitzi-authoring/reference/accessibility.md +8 -4
- package/skills/plitzi-authoring/reference/authoring-errors.md +13 -3
- package/skills/plitzi-authoring/reference/colours-and-motion.md +7 -0
- package/skills/plitzi-authoring/reference/components.md +2 -1
- package/skills/plitzi-authoring/reference/data-and-visibility.md +26 -16
- package/skills/plitzi-authoring/reference/drawing.md +22 -0
- package/skills/plitzi-authoring/reference/efficiency.md +10 -1
- package/skills/plitzi-authoring/reference/elements-and-styles.md +7 -11
- package/skills/plitzi-authoring/reference/lists.md +12 -7
- package/skills/plitzi-authoring/reference/plugins.md +5 -0
- package/skills/plitzi-authoring/reference/review-checklist.md +14 -2
- package/skills/plitzi-authoring/reference/snippets-and-export.md +2 -2
- package/skills/plitzi-authoring/reference/structure.md +1 -1
- package/skills/plitzi-authoring/reference/templates.md +1 -1
- package/skills/plitzi-authoring/reference/testing.md +10 -5
- package/skills/plitzi-authoring/reference/typed-sources.md +1 -1
- package/skills/plitzi-authoring/reference/validation.md +3 -2
|
@@ -3,18 +3,17 @@
|
|
|
3
3
|
* Show data with no backend: a JSON file the project serves (`public/data/products.json`), read by a provider — one
|
|
4
4
|
* card per product, a skeleton while it loads, a count, a computed value, and an empty state that never flashes.
|
|
5
5
|
*/
|
|
6
|
-
import { apiContainer, container, heading, list, styles, text } from '@plitzi/sdk-authoring';
|
|
6
|
+
import { apiContainer, container, heading, list, styles, text, listItem } from '@plitzi/sdk-authoring';
|
|
7
7
|
|
|
8
8
|
import type { SpaceSpec } from '@plitzi/sdk-authoring';
|
|
9
9
|
|
|
10
|
-
// A list is a `<ul
|
|
10
|
+
// A list is a `<ul>`, its rows `<li>`s straight inside: the grid is all its class says, the indent taken off.
|
|
11
11
|
const grid = styles('product-grid', {
|
|
12
12
|
display: 'grid',
|
|
13
13
|
'grid-template-columns': 'repeat(auto-fill, minmax(220px, 1fr))',
|
|
14
14
|
gap: '16px',
|
|
15
15
|
margin: '0px',
|
|
16
|
-
padding: '0px'
|
|
17
|
-
'list-style-type': 'none'
|
|
16
|
+
padding: '0px'
|
|
18
17
|
});
|
|
19
18
|
|
|
20
19
|
const card = styles('product-card', { padding: '16px', 'border-radius': '12px', 'background-color': 'var(--card)' });
|
|
@@ -56,9 +55,15 @@ export const recipe: SpaceSpec = {
|
|
|
56
55
|
class: grid,
|
|
57
56
|
items: 'catalog.data.products',
|
|
58
57
|
children: [
|
|
59
|
-
// Inside a row, a binding names the row short (`products.item`), a template in
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
// One `<li>` per product. Inside a row, a binding names the row short (`products.item`), a template in
|
|
59
|
+
// full (`list_products`).
|
|
60
|
+
listItem({
|
|
61
|
+
class: card,
|
|
62
|
+
children: [
|
|
63
|
+
heading({ subType: 'h3', from: 'products.item.title' }),
|
|
64
|
+
text({ from: 'products.item.price', as: 'price' })
|
|
65
|
+
]
|
|
66
|
+
})
|
|
62
67
|
]
|
|
63
68
|
}),
|
|
64
69
|
// Revealed by the data, so it starts hidden: nothing flashes while the file loads.
|
|
@@ -10,7 +10,7 @@ import type { SpaceSpec } from '@plitzi/sdk-authoring';
|
|
|
10
10
|
const card = styles('card', {
|
|
11
11
|
css: {
|
|
12
12
|
desktop: { display: 'flex', 'flex-direction': 'column', gap: '8px', padding: '24px', 'border-radius': '16px' },
|
|
13
|
-
// Tablet (48–64rem) and mobile (
|
|
13
|
+
// Tablet (48–64rem) and mobile (up to 48rem) each inherit from desktop alone: `compact` is both.
|
|
14
14
|
compact: { padding: '16px' }
|
|
15
15
|
},
|
|
16
16
|
states: { hover: { 'border-color': 'var(--primary)' } }
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* `site.data.hero.titel` is a type error, not an empty heading. In a project the sample is the file itself:
|
|
5
5
|
* `import home from '../../public/data/home.json' with { type: 'json' }`.
|
|
6
6
|
*/
|
|
7
|
-
import { apiContainer, heading, list, source, styles, text, twig } from '@plitzi/sdk-authoring';
|
|
7
|
+
import { apiContainer, heading, list, source, styles, text, twig, listItem } from '@plitzi/sdk-authoring';
|
|
8
8
|
|
|
9
9
|
import type { SpaceSpec } from '@plitzi/sdk-authoring';
|
|
10
10
|
|
|
@@ -50,11 +50,14 @@ export const recipe: SpaceSpec = {
|
|
|
50
50
|
class: grid,
|
|
51
51
|
items: site.data.products,
|
|
52
52
|
// The row's item is typed as the sample's: every field any product has.
|
|
53
|
-
row: product =>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
row: product =>
|
|
54
|
+
listItem({
|
|
55
|
+
children: [
|
|
56
|
+
text({ from: product.item.title }),
|
|
57
|
+
text({ from: product.item.price, as: 'price' }),
|
|
58
|
+
text({ from: product.item.badge, visible: product.item.badge })
|
|
59
|
+
]
|
|
60
|
+
})
|
|
58
61
|
})
|
|
59
62
|
]
|
|
60
63
|
})
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* button, a field keeps its label out of sight rather than dropping it, a picture says what it shows or that it only
|
|
4
4
|
* decorates.
|
|
5
5
|
*/
|
|
6
|
-
import { button,
|
|
6
|
+
import { button, formControl, image, setState, styles, text, onClick } from '@plitzi/sdk-authoring';
|
|
7
7
|
|
|
8
8
|
import type { SpaceSpec } from '@plitzi/sdk-authoring';
|
|
9
9
|
|
|
@@ -23,7 +23,7 @@ export const recipe: SpaceSpec = {
|
|
|
23
23
|
name: 'Home',
|
|
24
24
|
slug: '',
|
|
25
25
|
body: [
|
|
26
|
-
button({ content: '', title: 'Search',
|
|
26
|
+
button({ content: '', title: 'Search', icon: 'fa-solid fa-magnifying-glass' }),
|
|
27
27
|
button({
|
|
28
28
|
content: '',
|
|
29
29
|
class: card,
|
|
@@ -4,7 +4,8 @@ A screen reader and a browser agent (Claude in Chrome) read a page the same way.
|
|
|
4
4
|
tree** the browser builds from the markup, where every control is a role and a name. If a control has no name, or a
|
|
5
5
|
click sits on a box that is not a control, those readers can't find it, even though the page looks fine. The linter
|
|
6
6
|
warns about each case below (`control-without-name`, `image-without-alt`, `click-on-static-element`,
|
|
7
|
-
`heading-level-skipped`, `label-ignored`
|
|
7
|
+
`dropdown-without-control`, `heading-level-skipped`, `label-ignored`, `control-in-decorative`, `embed-without-title`,
|
|
8
|
+
`controls-no-anchor`). Fix them the same way you fix any other warning.
|
|
8
9
|
|
|
9
10
|
## Controls have words
|
|
10
11
|
|
|
@@ -13,10 +14,10 @@ warns about each case below (`control-without-name`, `image-without-alt`, `click
|
|
|
13
14
|
button({ content: 'Save' })
|
|
14
15
|
|
|
15
16
|
// An icon only: `title` is its name, and also its tooltip. The icon is hidden from the tree by itself.
|
|
16
|
-
button({ content: '', title: 'Close',
|
|
17
|
+
button({ content: '', title: 'Close', icon: 'fa-solid fa-xmark' })
|
|
17
18
|
|
|
18
19
|
// Words that do not say what it does — a key hint, a count: `label` is read in their place.
|
|
19
|
-
button({ content: '', label: 'Select (V)', title: 'Select — V',
|
|
20
|
+
button({ content: 'V', label: 'Select (V)', title: 'Select — V', icon: 'fa-solid fa-arrow-pointer' })
|
|
20
21
|
|
|
21
22
|
// A link around a whole card: without `label` it is announced as every word inside the card.
|
|
22
23
|
link({ href: '/posts/field-notes', label: 'Read “Field notes”', children: [ /* cover, topic, title, byline */ ] })
|
|
@@ -75,7 +76,10 @@ button({ content: 'Grid', bind: { ariaPressed: 'computed.gridOn' }, class: chip
|
|
|
75
76
|
button({ content: 'Menu', bind: { ariaExpanded: 'state.menuOpen' }, flows: [[onClick(), toggleState({ key: 'menuOpen' })]] })
|
|
76
77
|
```
|
|
77
78
|
|
|
78
|
-
An "active" variant is for the eyes. `ariaPressed` and `ariaExpanded` are for everyone else. Use both.
|
|
79
|
+
An "active" variant is for the eyes. `ariaPressed` and `ariaExpanded` are for everyone else. Use both. A button that
|
|
80
|
+
opens a panel also names it — `controls: 'menu-panel'`, the panel's id (authoring gives the panel the anchor
|
|
81
|
+
`aria-controls` needs; [recipes/accordion.ts](../recipes/accordion.ts)). Words that change while somebody is on the page
|
|
82
|
+
— a count, a total, a status — sit in `container({ live: 'polite', … })`, so a screen reader says them when they change.
|
|
79
83
|
|
|
80
84
|
## The page has an outline
|
|
81
85
|
|
|
@@ -59,6 +59,7 @@ The space is not written until these are fixed.
|
|
|
59
59
|
| `computed-not-template` | a computed value that is not a template | `'{{ state.x * 2 }}'` |
|
|
60
60
|
| `computed-reads-element` | a computed value reading an element's source — no element is around the whole space | compute it from the globals and the variables, or bind it on the element |
|
|
61
61
|
| `computed-unknown` | a computed value read before it is declared, or never declared | declare it in `computed`, above the one that reads it |
|
|
62
|
+
| `controls-unknown` | a button's `controls` naming no element of the space | the id of the element it shows and hides: `controls: 'faq-answer'` |
|
|
62
63
|
| `css-property-twice` | one property written twice in a rule set — `paddingTop` beside `'padding-top'` — so one would silently win | keep one |
|
|
63
64
|
| `css-property-unknown` | a CSS property that does not exist | the property it suggests; a custom property starts with `--` |
|
|
64
65
|
| `css-value` | an empty CSS value, or one with `;` or `{}` | one value per property; leave a property out instead of writing it empty |
|
|
@@ -84,7 +85,7 @@ The space is not written until these are fixed.
|
|
|
84
85
|
| `global-callback-undeclared` | a global-callback step builder naming an action no source declares | one of the actions it lists |
|
|
85
86
|
| `id-invalid` | an id a binding, a template or a test cannot name | a letter first, then letters, digits, `-` and `_`: `'hero-title'` |
|
|
86
87
|
| `id-shadows-global` | an element named like a global data source (`state`, `navigation`, `auth`…) | another id |
|
|
87
|
-
| `id-taken` | two elements with one id — ids are one namespace for the whole space |
|
|
88
|
+
| `id-taken` | two elements with one id — ids are one namespace for the whole space | a name taken by a page or a layout: a name of its own; a helper called twice: `scope('promos', ref => …)`, every id inside prefixed |
|
|
88
89
|
| `layout-slot-unknown` | a layout `slot` that is not an element inside that layout | the id of the element in the shell where the body goes |
|
|
89
90
|
| `layout-undeclared` | a page or a layout inside a layout the space does not declare | the layout's id, or declare it in `layouts` |
|
|
90
91
|
| `list-items-ignored` | a list whose items nothing reads — `source: 'none'` renders its children once | `source: 'controlled'` |
|
|
@@ -92,6 +93,8 @@ The space is not written until these are fixed.
|
|
|
92
93
|
| `loading-slot-unknown` | an `apiContainer`'s `loadingSlot` that is not the id of one of its children | the id of the child to show until the first answer — `loadingSlot: 'catalog-skeleton'` beside that child |
|
|
93
94
|
| `modifier-count` | more than one set of rules in a class list | one set, after the classes: `class: [card, { opacity: '0.5', 'margin-top': '8px' }]` |
|
|
94
95
|
| `modifier-without-id` | rules on top of a class on an element with no `id` — the id names the class they become | `id: 'hero-bg'` |
|
|
96
|
+
| `motion-invalid` | a `motion` with a preset, a trigger or a timing the page cannot play | `motion: { enter: 'fade-up', on: 'view' }` — the message lists the presets |
|
|
97
|
+
| `motion-no-tag` | a `motion` on an element with no tag of its own — nothing to move | the motion on what it wraps, or a `subType` on it |
|
|
95
98
|
| `no-pages` | a space with no pages | `pages: [{ id: 'home', name: 'Home', slug: '', body: [] }]` |
|
|
96
99
|
| `notifications-shape` | `notifications` with a field it does not have, or a value that is not one CSS value | the fields it lists; a colour or a length, like 'var(--card)' or '12px' |
|
|
97
100
|
| `outside-ancestor` | an element that reads the state of an element it is not inside | nest it in the element the message names |
|
|
@@ -107,6 +110,7 @@ The space is not written until these are fixed.
|
|
|
107
110
|
| `prop-unknown` | a prop the component does not declare, handed in or read | the prop it suggests, or declare it: `props: { name: { type: 'text' } }` |
|
|
108
111
|
| `prop-value` | a prop handed in with a value its declaration does not take | a value of the declared type, or one of its options |
|
|
109
112
|
| `props-outside-component` | `props.x` read outside a component | read the source it would have come from |
|
|
113
|
+
| `quiet-unknown` | an element's `quiet` naming something that is not a suggestion's code — a problem is never quieted | the code of the suggestion it was offered: `quiet: ['repeated-shape']` |
|
|
110
114
|
| `redirect-target-unknown` | a page that sends visitors it is not for to a page the space does not have | a page's id or slug (`''` is the home page), or a full URL |
|
|
111
115
|
| `row-and-children` | a list with a `row` and `children` — the row is what it renders | one of the two |
|
|
112
116
|
| `row-component` | a list's `row` naming a component the space does not declare, or one with no prop to take the row | a declared component with an `item` prop — or a single prop — for the row |
|
|
@@ -115,6 +119,7 @@ The space is not written until these are fixed.
|
|
|
115
119
|
| `rule-set-mixed` | a style that writes its rules beside `states`, `variants` or `ancestors` instead of under `css` | `{ css: { desktop: { … } }, states: { hover: { … } } }` — the rules under `css`, each of the others beside it |
|
|
116
120
|
| `selector-invalid` | a `selector` that is not a CSS class name | letters, digits, `-` and `_` |
|
|
117
121
|
| `selector-taken` | a `selector` that is a declared class, or another element's | `class` to share rules; a selector of an element's own is its alone |
|
|
122
|
+
| `server-provider-in-component` | a `runtime: 'server'` element inside a component: the page server resolves a page's and its layouts', never a component's | put the provider on the page, around the instance, and hand the component its rows as a prop |
|
|
118
123
|
| `setting-misplaced` | `settings.computed` or `settings.channels` written inside `settings` | `computed` and `channels` at the top of the space |
|
|
119
124
|
| `slot-children` | children handed to an instance outside its slots | `children: { slotName: [ … ] }`; a component with no slots takes no children |
|
|
120
125
|
| `slot-unknown` | a component slot that is not an element of its tree | the id of an element inside the component — usually an empty container |
|
|
@@ -159,6 +164,7 @@ The space renders, and renders something you probably did not mean. Fix every on
|
|
|
159
164
|
| `condition-starts-visible` | a computed visibility that shows until its data answers | `visible: { source, template }` |
|
|
160
165
|
| `control-in-decorative` | a control inside a `decorative` container — Tab lands on something nothing announces | move it out of the illustration |
|
|
161
166
|
| `control-without-name` | a button or a link with no words, or a field nothing names | `title` on the button, `label` on the link, `label` (with `hideLabel: true`) on the field |
|
|
167
|
+
| `controls-no-anchor` | a button's `controls` naming no anchor — `aria-controls` points at nothing | an `anchor` on the element it shows and hides, and `controls` naming it |
|
|
162
168
|
| `default-content-beside-children` | a button printing its default "Button" beside its children | `content: ''` |
|
|
163
169
|
| `dropdown-without-control` | a dropdown opened from a box or an icon — no keyboard opens it | a `button` as what opens it (`title` if it is only an icon) |
|
|
164
170
|
| `embed-without-title` | an `embed` with no `title` — a frame a screen reader cannot describe | say what it shows in `title` |
|
|
@@ -172,13 +178,16 @@ The space renders, and renders something you probably did not mean. Fix every on
|
|
|
172
178
|
| `image-without-alt` | an image that is not `decorative` and has no `alt` | say what it shows, or `decorative: true` |
|
|
173
179
|
| `label-ignored` | a `label` on a container whose tag is named by what it holds (`li`, a heading) | the words inside, or a landmark tag (`nav`, `section`…) |
|
|
174
180
|
| `list-item-key-missing` | a list's `itemKey` that some of its items lack, or two of them share — the rows fall back to `id`, then position | a field every item has, once each |
|
|
181
|
+
| `list-row-not-li` | a row of a list with `items` that is not an `<li>` — that list is a `<ul>` (or `<ol>`), so the row is a box inside a list, no item to a screen reader | `container({ subType: 'li' })` — or the component's root one, or a wrapper for a link or a button |
|
|
175
182
|
| `overlay-never-opened` | a modal or a dialog that starts hidden and that no step opens | a flow with `openModal('id')` / `openDialog('id')` |
|
|
176
183
|
| `overlay-starts-open` | a modal or a dialog open when the page loads | `visible: false`, opened by `openModal` |
|
|
177
184
|
| `painted-state-without-keep-state` | `paintedState` without `keepState` — nothing is kept for the server to draw with | `settings.keepState: true`, or remove `paintedState` |
|
|
185
|
+
| `path-not-in-data` | a binding onto a provider whose answer the author could read (`data`), through a path that answer does not have | the path the message lists the keys for — `p.data.plans`, not `p.data.landing.plans` |
|
|
186
|
+
| `plugin-attribute-reserved` | a plugin attribute named as one of the element’s own fields — a factory never hands it to the plugin | rename it in the plugin (`variant` → `kind`) |
|
|
178
187
|
| `provider-without-source` | an `apiContainer` that asks nothing | a `query` (or `action`, `connector`, `resource`) |
|
|
179
188
|
| `route-param-undeclared` | `navigation.routeParams.x` read on a page whose slug has no `:x` — always empty | add `:x` to the slug, or read `navigation.queryParams.x` |
|
|
180
|
-
| `server-data-without-rsc` | a `runtime: 'server'` provider
|
|
181
|
-
| `span-holds-block` | a `container
|
|
189
|
+
| `server-data-without-rsc` | a `runtime: 'server'` provider — `connector`, `action` or `query` — in a space that turns server data off (`rsc: { enabled: false }`) | drop `rsc: { enabled: false }`: server data is on unless a space turns it off |
|
|
190
|
+
| `span-holds-block` | a `container` with `subType: 'span'` or `'p'` holding a heading, a paragraph, a list, a form or prose | a `div` (leave `subType` out), or words and inline elements: a `text`, a `link` |
|
|
182
191
|
| `state-key-has-runtime-prefix` | a state key written with `runtime.state.` in front — the state callbacks already write below it | `key: 'cart'`, not `'runtime.state.cart'` |
|
|
183
192
|
| `state-toggled-in-branches` | two `setState` of one key, each under a `when` on that key — the second flips back what the first wrote | `toggleState({ key })`; for something shown by default, a key named for hiding it |
|
|
184
193
|
| `STYLE_WITHOUT_TAG` | style on a provider that renders no element of its own | `subType: 'div'` on the provider, or style its parent |
|
|
@@ -189,6 +198,7 @@ The space renders, and renders something you probably did not mean. Fix every on
|
|
|
189
198
|
| `unknown-global-callback` | a global callback no built-in source declares — it runs only if something registers it | the built-in it suggests, or make sure a plugin or module of the space registers it |
|
|
190
199
|
| `unknown-utility` | a utility that is not one of the built-in ones | one of the built-in utilities |
|
|
191
200
|
| `unknown-variable` | a `var(--x)` nothing in the space declares — the property it is in is dropped | declare it under `variables`, fix the name, or give it a fallback: `var(--x, …)` |
|
|
201
|
+
| `unknown-variant` | a variant no class of the element and no style of its type declares — nothing applies | declare it (`styles(name, { variants: { … } })`), or name one that is |
|
|
192
202
|
|
|
193
203
|
## Suggested
|
|
194
204
|
|
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
Only `opacity` and `transform` (`translate`, `scale`, `rotate`) animate off the main thread: they stay smooth while
|
|
20
20
|
the page hydrates and on a slow phone. Everything else repaints, or lays the page out again, on every frame.
|
|
21
21
|
|
|
22
|
+
- **A preset first:** `motion: { enter: 'fade-up', on: 'view' }` on any element, `stagger: 60` for children one by one,
|
|
23
|
+
`loop: 'float'` — played by the SDK, cheap and stilled for reduced motion by construction (`npx plitzi explain motion` lists them; `MOTION_ENTERS` and `MOTION_LOOPS` in code).
|
|
24
|
+
`on: 'view'` plays once, the first time the element comes into view, and it stays; `on: 'scroll'` follows the scroll
|
|
25
|
+
both ways — it goes back out as the reader scrolls up past it — so keep it for an effect that is about the scroll.
|
|
26
|
+
|
|
22
27
|
- **Slide, grow, fade** with `transform` and `opacity` — never `left`, `top`, `width`, `height` or `margin`. Something
|
|
23
28
|
that travels across a box is `transform: translate(%)` on an element as large as the box.
|
|
24
29
|
- **No animated blur:** no `filter: blur()` in an entrance or a loop, no `backdrop-filter` over moving content. A soft
|
|
@@ -28,6 +33,8 @@ the page hydrates and on a slow phone. Everything else repaints, or lays the pag
|
|
|
28
33
|
- **Main-thread decoration waits for hydration:** a sweep on a custom property, a `background-position` seam — start
|
|
29
34
|
it paused, run it once the SDK's root says `data-hydrated`:
|
|
30
35
|
`'.glow { animation: glow 8s linear infinite paused; } [data-hydrated] .glow { animation-play-state: running; }'`.
|
|
36
|
+
That is for loops. An entrance of your own that plays once — `transform`/`opacity` only — plays from the first paint
|
|
37
|
+
with `backwards`, as the presets do: gated on `[data-hydrated]` it jumps when the page goes live.
|
|
31
38
|
- **Entrances:** `opacity` + a small `translateY`, under ~600ms, staggered by tens of ms.
|
|
32
39
|
- **One ambient loop per screen**, slow — not one per card. `infinite` runs on every visitor's battery for as long
|
|
33
40
|
as the page is open.
|
|
@@ -56,7 +56,8 @@ pages: [{ name: 'Shop', slug: 'shop', body: [
|
|
|
56
56
|
a `json` prop `item`, read as `{{ props.item.name }}`.
|
|
57
57
|
- **Slots** are elements of the component's tree that an instance fills. `children: [ … ]` fills a component with one
|
|
58
58
|
slot; `children: { '<slot>': [ … ] }` names the slot for each. What fills a slot is the instance's, not the
|
|
59
|
-
component's: it is authored on the page and sees the page's sources.
|
|
59
|
+
component's: it is authored on the page and sees the page's sources. A prop's value is read as a template on the
|
|
60
|
+
instance, so words that show `{{ … }}` as text (docs, a tutorial) go in a slot — a `markdown` child — not in a prop.
|
|
60
61
|
- **Closed.** Inside, nothing but `props` and the globals (`state`, `auth`, `navigation`, `theme`, `variables`,
|
|
61
62
|
`computed`). A binding inside a component onto a provider of the page is refused — "nothing in this component answers
|
|
62
63
|
to …" — because the component would render differently wherever it was placed. Put the provider inside the component,
|
|
@@ -50,7 +50,7 @@ text('', { bind: [bindTemplate('content', 'computed.level', 'Level {{ source }}'
|
|
|
50
50
|
|
|
51
51
|
A computed value reads the globals (`state`, `auth`, `navigation`, `variables`, `theme`, `host`) and the ones declared
|
|
52
52
|
above it — not an element's source: bind that on the element. One `{{ expression }}` gives its value (a number, a
|
|
53
|
-
list); anything else gives text. `authorSpace` refuses a name read before it is declared
|
|
53
|
+
list); anything else gives text. `authorSpace` refuses a name read before it is declared.
|
|
54
54
|
|
|
55
55
|
## Providers
|
|
56
56
|
|
|
@@ -61,12 +61,16 @@ apiContainer({ id: 'board', runtime: 'server', action: 'queue-board' })
|
|
|
61
61
|
|
|
62
62
|
- A provider's source is readable by its **descendants** only. Wrap what reads it; a sibling cannot see it.
|
|
63
63
|
- It publishes `data` plus `isLoading`, `isEmpty`, `hasError`, `errorMessage` — bind a state to those, not to guesses.
|
|
64
|
+
A `query`'s body is `data` in **either runtime** (`p.data.plans`), so moving a provider to the server changes no
|
|
65
|
+
binding. A connector publishes `records` (or `record`), an action its output.
|
|
64
66
|
`isEmpty` reads what arrived: a `query`'s body when it is missing, `null`, `''`, `[]` or `{}`; a connector list by
|
|
65
67
|
its `records`; a `singleRecord` provider by its `record`. It is also true before the first answer, so pair it with
|
|
66
68
|
`not isLoading` for an empty state.
|
|
67
69
|
- A query that depends on state is a binding on `query`; it answers `''` (and fetches nothing) until the state exists:
|
|
68
70
|
`"{{ source ? apiUrl ~ '/workspaces/' ~ source ~ '/stats' : '' }}"` with `source: 'state.workspace.id'`.
|
|
69
|
-
-
|
|
71
|
+
- `runtime: 'server'` is resolved by the page server — server data is on unless a space says `rsc: { enabled: false }`.
|
|
72
|
+
Only a page and its layouts are resolved — never inside a component: keep the provider on the page and hand the
|
|
73
|
+
component its rows as a prop ([recipes/server-data.ts](../recipes/server-data.ts)).
|
|
70
74
|
|
|
71
75
|
### Typed by a sample
|
|
72
76
|
|
|
@@ -81,11 +85,15 @@ An offline project (`offlineMode`) still never invents data: the content lives i
|
|
|
81
85
|
backend, so swapping the file for a real endpoint later changes one `query`. `mockData` is what the BUILDER shows while
|
|
82
86
|
editing; it is not a data source for the running page. Say on the page that demo content is demo content.
|
|
83
87
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
**`public/` is on the internet.** Every file in it is served to anyone who asks, as it is, once the project is deployed —
|
|
89
|
+
and so is everything the space's documents hold (pages, variables, attributes, `mockData`): they reach every visitor's
|
|
90
|
+
browser. Never put a secret, a key, a private document or data only some visitors may read in either. A secret is a
|
|
91
|
+
credential that an action or a connector names; data for some visitors only comes from a server action whose `access`
|
|
92
|
+
checks who is asking, or a `runtime: 'server'` provider behind one.
|
|
93
|
+
|
|
94
|
+
**In a server project (`create --mode server`), put the provider on the server** — `runtime: 'server'` — and the page
|
|
95
|
+
server reads the file from `public/` itself: the page arrives with those sections (and their anchors) in it. A `query`
|
|
96
|
+
with `{{tokens}}` is still read in the browser, against the visitor's route and state.
|
|
89
97
|
|
|
90
98
|
### Live, cached, refreshed
|
|
91
99
|
|
|
@@ -116,7 +124,7 @@ modalContainer({ visible: false, … }) // starts hidden; a flow opens
|
|
|
116
124
|
| The logic… | Example | Write |
|
|
117
125
|
| --- | --- | --- |
|
|
118
126
|
| 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 |
|
|
119
|
-
| hides it: shown unless a flag says otherwise | sidebar labels until the sidebar is folded, a banner until dismissed |
|
|
127
|
+
| hides it: shown unless a flag says otherwise | sidebar labels until the sidebar is folded, a banner until dismissed | `visible: '!state.folded'` — on screen while nobody has set it |
|
|
120
128
|
|
|
121
129
|
`visible` starts the element **hidden**, and it appears when its data says so. A revealing condition you have to
|
|
122
130
|
compute is `visible: { source, template }` — it starts hidden too, and the template's value is read as a yes or a
|
|
@@ -135,17 +143,19 @@ container({
|
|
|
135
143
|
|
|
136
144
|
Rules for a condition's template:
|
|
137
145
|
|
|
138
|
-
- **A
|
|
139
|
-
|
|
140
|
-
condition on data. `authorSpace` warns `condition-starts-visible` when a computed one on a provider starts on screen.
|
|
146
|
+
- **A visibility binding written by hand in `bind` keeps the element on screen until it answers** — a flash for a
|
|
147
|
+
condition on data (`condition-starts-visible` warns); `visible` starts it hidden.
|
|
141
148
|
- **A hidden element stays in the DOM** (`plitzi-component--hidden`), its subtree mounted. `loadStrategy: 'visible'`
|
|
142
149
|
mounts the subtree only while it is shown, and `'lazy'` from the first time it is shown (modals do that already).
|
|
143
150
|
Use it on heavy panels that are usually hidden; leave the default on what is usually on screen.
|
|
144
|
-
-
|
|
145
|
-
|
|
146
|
-
- **
|
|
147
|
-
|
|
148
|
-
|
|
151
|
+
- **Once its source has a value, the answer is a yes or a no**: `false`, `0`, `''` and an empty list hide it,
|
|
152
|
+
anything else shows it.
|
|
153
|
+
- **While its source has no value, only a definite answer is written.** `!source` is `true` — shown; a template's
|
|
154
|
+
`'true'` or `'false'` is written as it says. A plain source, or a template that answers nothing (`''`), writes
|
|
155
|
+
nothing: the element stays as it started — hidden for `visible`, shown for a binding written by hand.
|
|
156
|
+
- **So a hiding flag is `visible: '!state.folded'`** (or a binding with no `visible`): on screen before anybody has set
|
|
157
|
+
it. A condition that answers "not known yet" with `'false'` from a value that IS there — a "folded" read off
|
|
158
|
+
something always set — hid every label of a sidebar for everybody.
|
|
149
159
|
- **Hide the element itself.** Never wrap it in a container that is shown while the inner one is hidden: an empty
|
|
150
160
|
visible wrapper still takes a slot in its parent's `gap`, and leaves a hole in the page.
|
|
151
161
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Drawing and animating in a plugin
|
|
2
|
+
|
|
3
|
+
A plugin that draws on a canvas — a WebGL hero, a field of particles — takes the canvas from `useCanvas2d`, `useWebGL`
|
|
4
|
+
or `useWebGL2`, and writes only `setup` (once per context) and `draw` (each frame, in CSS pixels):
|
|
5
|
+
|
|
6
|
+
```tsx
|
|
7
|
+
import { createShaderProgram, RootElement, useWebGL } from '@plitzi/plitzi-sdk';
|
|
8
|
+
|
|
9
|
+
const { ref, ready, error } = useWebGL({
|
|
10
|
+
setup: gl => ({ program: createShaderProgram(gl, { vertex: VERTEX, fragment: FRAGMENT }) }),
|
|
11
|
+
draw: (gl, { time, state }) => { gl.useProgram(state.program); /* uniforms, drawArrays */ }
|
|
12
|
+
});
|
|
13
|
+
return <RootElement className={className}>{!ready && <img src={still} alt="" />}<canvas ref={ref} /></RootElement>;
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
It sizes the canvas to the device (at most 2× — `maxPixelRatio`), follows its size, and animates only while somebody
|
|
17
|
+
can see it move: on a live page (in the builder it draws one still frame), for a visitor who did not ask for less
|
|
18
|
+
motion, with the tab in front and the canvas on screen; `paused` holds it (bind it to a hover). A shader that does not
|
|
19
|
+
compile — GLSL ES 1.0 wants `196.0`, not `196` — throws a `ShaderError` with the driver's log, printed as
|
|
20
|
+
`[plugin <type> "<id>"] fragment shader failed: …` (`plitzi check` lists it under the console) and `error` says it;
|
|
21
|
+
show the still image meanwhile, as above. `useAnimationFrame(onFrame, { target })` is the same loop for motion that
|
|
22
|
+
is not a canvas.
|
|
@@ -19,6 +19,15 @@ const { warnings, suggestions } = authorSpace(space);
|
|
|
19
19
|
is a bug to fix; a suggestion is a shorter way to the same page — take it unless you have a reason not to, and the
|
|
20
20
|
reason is usually that the copies are about to diverge. `npx @plitzi/cli explain repeated-on-pages` explains any code.
|
|
21
21
|
|
|
22
|
+
Left on purpose, say so where it is written — `quiet` on an element it is about, and it is not offered again, in the
|
|
23
|
+
builder or over MCP either (it is kept in the document):
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
const includes = (words: string) => container({ class: row, quiet: ['repeated-shape'], children: [tick, text(words)] });
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Only a suggestion's code: a problem is never quieted (`quiet-unknown`).
|
|
30
|
+
|
|
22
31
|
## The long way, and the short one
|
|
23
32
|
|
|
24
33
|
| Written the long way | The short way | Suggestion |
|
|
@@ -26,7 +35,7 @@ reason is usually that the copies are about to diverge. `npx @plitzi/cli explain
|
|
|
26
35
|
| The same header, footer or sidebar in every page | A **layout** holding it once; each page names it (`layout: { id, slot }`) — [layouts](layouts.md) | `repeated-on-pages` |
|
|
27
36
|
| A header copied per page so one link can be styled "active" | The link marks its own page: `states: { current: { … } }` on its class; `activeOn` for an entry lit on several pages | `repeated-on-pages` |
|
|
28
37
|
| The same card, row or tile written again with other words | A **component** with props, placed with `component(id, { props })` — [components](components.md) | `repeated-shape` |
|
|
29
|
-
| Rows of data written one by one, side by side | One `list` over the rows — `items: [ … ]` fixed, or bound to a source — with its row written once — [lists](lists.md). Data is the test: three cards a person rewords on the canvas read better as three cards | `repeated-shape` |
|
|
38
|
+
| Rows of data written one by one, side by side | One `list` over the rows — `items: [ … ]` fixed, or bound to a source — with its row written once — [lists](lists.md). Data is the test: three cards a person rewords on the canvas read better as three cards. Copies that read different sources or write different state keys are controls written alike, and not offered | `repeated-shape` |
|
|
30
39
|
| `button({ children: [text('Save')] })`, `link({ children: [text('Docs'), fontAwesome({ icon })] })` | `button({ content: 'Save' })`, `link({ href, content: 'Docs', icon, iconPlacement: 'after' })` — the icon's class on the `icon` slot; what the text's class adds (`whiteSpace: 'nowrap'`) moves to the box's class — never the class itself, whose `inherit` would then point past the box and whose `pointerEvents: 'none'` would switch the box off | `content-attribute` |
|
|
31
40
|
| `.card:hover { … }`, `.card .icon { … }` in `customCss` | The class's own `states` and `ancestors` | `custom-css-class` |
|
|
32
41
|
| A reduced-motion reset, or rules showing one icon of the theme toggle, in `customCss` | Nothing: the SDK does both for every space | `custom-css-sdk-default` |
|
|
@@ -25,7 +25,7 @@ container([hero, grid]) // an array is the children
|
|
|
25
25
|
| `visible` | a condition (`'src'`, `'!src'`, `{ source, template }`), or `false` to start hidden |
|
|
26
26
|
| `flows` | what happens on click, on submit, on load |
|
|
27
27
|
| `runtime` | `'server'` resolves this provider's data on the server |
|
|
28
|
-
| `loadStrategy` | when the contents mount relative to visibility: `eager`
|
|
28
|
+
| `loadStrategy` | when the contents mount relative to visibility: `eager`, `lazy`, `visible`. Left out, the type decides — `eager`, `lazy` for a modal or a dialog |
|
|
29
29
|
| `children` | the tree |
|
|
30
30
|
| `meta` | `meta.label` is the element's name in the builder's tree |
|
|
31
31
|
|
|
@@ -44,7 +44,7 @@ another link — make the card the link, or the button, not both.
|
|
|
44
44
|
link to the page being shown is `aria-current="page"`; its class's `current` state styles it, so one header in a
|
|
45
45
|
layout lights the right item everywhere.
|
|
46
46
|
|
|
47
|
-
**An element's `id` is not its HTML id.** It reaches the DOM as `data-
|
|
47
|
+
**An element's `id` is not its HTML id.** It reaches the DOM as `data-plitzi-el`. For a section a URL can land on, give it
|
|
48
48
|
an `anchor` (`anchor: 'plans'` → `id="plans"`: lowercase, digits, `-`) and link to it with `hash`:
|
|
49
49
|
`link({ href: 'home', hash: 'plans' })` goes to `/#plans` and scrolls there, waiting for a section that renders once
|
|
50
50
|
its data arrives. One anchor per page, layouts included; never inside a list row or a component, which would repeat
|
|
@@ -66,8 +66,8 @@ says `content: ''` (`default-content-beside-children` warns otherwise). Its acce
|
|
|
66
66
|
keeps its own value and fires `onChange` with `{ value, name }` on every change — a search box or a select that
|
|
67
67
|
filters a screen needs no form. Bind `defaultValue` to the state it writes and a reset of that state reaches it.
|
|
68
68
|
|
|
69
|
-
**A `list` is a `<ul
|
|
70
|
-
`{ margin: '0px', padding: '0px'
|
|
69
|
+
**A `list` is a `<ul>`, its rows `<li>`s straight inside** — `listItem({ … })`, or `list-row-not-li` warns. It is
|
|
70
|
+
indented with space below it: `{ margin: '0px', padding: '0px' }` on its class takes that off.
|
|
71
71
|
|
|
72
72
|
**A provider has no box unless you give it a tag.** An `apiContainer` renders no element of its own by default, so its
|
|
73
73
|
children sit in the parent's layout and a `class` on it styles nothing (`STYLE_WITHOUT_TAG`). Put the layout on the
|
|
@@ -78,12 +78,8 @@ A type this SDK does not ship — a plugin — is authored the same way: `define
|
|
|
78
78
|
|
|
79
79
|
**What a type takes, answered by the package** instead of the 7,000-line `.d.ts`: every catalogue is exported, keyed by
|
|
80
80
|
type — `elementCatalog` (what it is for), `elementDefaultAttributes`, `elementAttributeNames`, `elementTriggers`,
|
|
81
|
-
`elementCallbacks`, `elementSlots`, `elementSourceTypes`, `elementLeafTypes`, plus `BUILTIN_TRANSFORMERS
|
|
82
|
-
prints
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
node --input-type=module -e "import * as a from '@plitzi/sdk-authoring'; const t = 'formControl'; console.log({ attributes: a.elementDefaultAttributes[t], triggers: a.elementTriggers[t], slots: a.elementSlots[t] })"
|
|
86
|
-
```
|
|
81
|
+
`elementCallbacks`, `elementSlots`, `elementSourceTypes`, `elementLeafTypes`, plus `BUILTIN_TRANSFORMERS` — and
|
|
82
|
+
`npx @plitzi/cli explain formControl` prints them for one type.
|
|
87
83
|
|
|
88
84
|
## CSS
|
|
89
85
|
|
|
@@ -92,7 +88,7 @@ kebab-case or camelCase (`paddingTop`, as a React style object), and a bare numb
|
|
|
92
88
|
`fontWeight: 800`, `opacity: 0.5` stay numbers). A property outside the vocabulary is refused.
|
|
93
89
|
|
|
94
90
|
- `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, `mobile`
|
|
91
|
+
- **Breakpoints are ranges, not a cascade.** `tablet` is 48–64rem, `mobile` up to 48rem, and each inherits only from
|
|
96
92
|
`desktop` — a rule for `tablet` never reaches a phone (`authorSpace` warns `tablet-rule-skips-mobile`). A rule for
|
|
97
93
|
everything narrower than a desktop is `compact`, written to both: `css: { desktop: { … }, compact: { 'grid-template-columns':
|
|
98
94
|
'1fr' }, mobile: { gap: '8px' } }` — what `tablet` or `mobile` say for themselves wins.
|
|
@@ -8,24 +8,29 @@ list bound to its records (see the provider in data-and-visibility).
|
|
|
8
8
|
|
|
9
9
|
```ts
|
|
10
10
|
list({ id: 'games', class: gameGrid, items: 'catalog.data.games', row: 'game-card' }) // a component per game
|
|
11
|
-
list({ id: 'games', items: 'catalog.data.games', row: r => text({ from: `${r.item}.title` }) })
|
|
12
|
-
list({ id: 'plans', items: ['Free', 'Pro'], children: [text({ from: 'plans.item' })] }) // fixed
|
|
11
|
+
list({ id: 'games', items: 'catalog.data.games', row: r => listItem({ children: [text({ from: `${r.item}.title` })] }) })
|
|
12
|
+
list({ id: 'plans', items: ['Free', 'Pro'], children: [listItem({ children: [text({ from: 'plans.item' })] })] }) // fixed
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
`items` makes a list controlled: an array of its own, or the source its rows come from. `row` is what it renders once
|
|
15
|
+
`items` (or `from`) makes a list controlled: an array of its own, or the source its rows come from. `row` is what it renders once
|
|
16
16
|
per item — a component's id (placed with the row bound to its `item` prop, or its only prop), or a function handed the
|
|
17
17
|
row's names: `r.item` and `r.index` for a binding or `from`, `r.inTemplate.item` for a template
|
|
18
18
|
(`` `/games/{{ ${r.inTemplate.item}.slug }}` ``). A function needs the list's `id`, which names those sources. With
|
|
19
19
|
`items` a typed source's path (`items: site.data.games`, see typed-sources.md), the row is typed too:
|
|
20
|
-
`row: g => text({ from: g.item.title })`, and a field the sample's games lack is a type error.
|
|
20
|
+
`row: g => listItem({ children: [text({ from: g.item.title })] })`, and a field the sample's games lack is a type error.
|
|
21
21
|
|
|
22
22
|
- Each row publishes **`list_<id>.item`** (the row) and **`list_<id>.index`** (its position, a number from 0 —
|
|
23
23
|
`index + 1` counts, and `==` compares it with text or a number alike). Inside the row,
|
|
24
24
|
bindings read the short form (`'games.item.title'`), templates and attribute tokens the full one
|
|
25
25
|
(`{{ list_games.item.slug }}`).
|
|
26
26
|
- A **nested list** sees the outer row: inside `list_features`, `list_games.item` is still the game.
|
|
27
|
-
-
|
|
28
|
-
|
|
27
|
+
- A list is a `<ul>` (`subType: 'ol'` for a numbered one) and its `class` styles that root; each row is rendered
|
|
28
|
+
straight into it, with no wrapper — so **a row is an `<li>`**: `listItem({ class, children })`, a
|
|
29
|
+
`container({ subType: 'li' })`, or a component whose root is one. Anything else is a box inside a list, no item to a
|
|
30
|
+
screen reader (`list-row-not-li`; `fixSpace` makes a plain container one). A link or a button is wrapped in a
|
|
31
|
+
`listItem`, which is then what the list lays out: a card that is a link fills its item with `display: 'grid'` on the
|
|
32
|
+
`listItem`. Every list is indented (40px) with 10px below it, and one without `items` is bulleted too:
|
|
33
|
+
`{ margin: '0px', padding: '0px' }` on the root takes the space off. A grid of cards is the root's class.
|
|
29
34
|
- Fixed data is `items: [ … ]` on the list itself — no provider needed. Worth it when the rows ARE data — written from
|
|
30
35
|
an array, or rows that come and go (`repeated-shape` points at sibling copies). Three cards a person rewords on the
|
|
31
36
|
canvas are easier to edit as three cards than as a JSON array.
|
|
@@ -68,7 +73,7 @@ apiContainer({ id: 'catalog', query: '/data/games.json', cache: true, children:
|
|
|
68
73
|
source: 'controlled',
|
|
69
74
|
bind: [bindTemplate('items', 'catalog.data.games',
|
|
70
75
|
'{{ source|filter(g => g.slug == navigation.routeParams.slug) }}', { returns: 'value' })],
|
|
71
|
-
children: [gameDossier()]
|
|
76
|
+
children: [listItem({ children: [gameDossier()] })] // reads list_game.item
|
|
72
77
|
}),
|
|
73
78
|
text('Signal lost — no game by that name.', {
|
|
74
79
|
id: 'game-missing',
|
|
@@ -107,6 +107,11 @@ const newChart = useFlag('newChart'); // false for a flag the space does not dec
|
|
|
107
107
|
Gating the whole plugin needs no code: give its element `flag: 'newChart'`. `useFlag` is for a plugin that changes
|
|
108
108
|
inside — a new mode, an extra panel.
|
|
109
109
|
|
|
110
|
+
## Drawing and animating
|
|
111
|
+
|
|
112
|
+
A canvas, WebGL or a loop of its own: `useCanvas2d`, `useWebGL`, `useWebGL2` and `useAnimationFrame` — see
|
|
113
|
+
[drawing](drawing.md).
|
|
114
|
+
|
|
110
115
|
## Registering
|
|
111
116
|
|
|
112
117
|
A project `plitzi create` wrote registers every folder of `src/plugins` by itself, under the folder's name in
|
|
@@ -7,7 +7,8 @@ of it; each line is something that has shipped broken before.
|
|
|
7
7
|
|
|
8
8
|
- [ ] `authorSpace` returns **zero warnings**. Each one is written code that will not do what it says.
|
|
9
9
|
- [ ] Every **suggestion** is taken, or left for a reason you can say (the copies are about to diverge; three cards a
|
|
10
|
-
person rewords on the canvas)
|
|
10
|
+
person rewords on the canvas) — and then quieted where it is written, `quiet: ['repeated-shape']`, so it stops
|
|
11
|
+
burying the ones that matter. The ones your change opened up first — [efficiency.md](efficiency.md).
|
|
11
12
|
- [ ] No console errors on any page you touched.
|
|
12
13
|
|
|
13
14
|
## Nothing flashes, nothing jumps
|
|
@@ -39,7 +40,8 @@ Screen readers and browser agents (Claude in Chrome) work a page through its acc
|
|
|
39
40
|
a `label` (`hideLabel: true` when the design shows what it is).
|
|
40
41
|
- [ ] Every image has an `alt` saying what it shows, or `decorative: true`.
|
|
41
42
|
- [ ] Every click is on a `button` or a `link`; the whole page can be worked from the keyboard.
|
|
42
|
-
- [ ] A toggle binds `ariaPressed`, a button that opens something `ariaExpanded
|
|
43
|
+
- [ ] A toggle binds `ariaPressed`, a button that opens something `ariaExpanded` and names it with `controls`; words
|
|
44
|
+
that change on their own are in a `live` container.
|
|
43
45
|
- [ ] Headings step down one level at a time; parts of the page a person jumps to are landmarks (`nav`, `main`, a
|
|
44
46
|
labelled `section`).
|
|
45
47
|
- [ ] What a canvas or a plugin draws is also there as elements, with a visible way to reach them.
|
|
@@ -66,6 +68,16 @@ Screen readers and browser agents (Claude in Chrome) work a page through its acc
|
|
|
66
68
|
- [ ] What only an administrator may read is shown only to administrators — and the check is a real boolean, not
|
|
67
69
|
a template left as text.
|
|
68
70
|
|
|
71
|
+
## Nothing left behind
|
|
72
|
+
|
|
73
|
+
- [ ] Nothing you made and no longer use is still there — a page, a component, a class, a token, a data file, an
|
|
74
|
+
import, a plugin folder. No commented-out code, no debug `console.log`, no copy of a file kept "just in case".
|
|
75
|
+
- [ ] Scratch work — a one-off script, a dump, a picture to look at — went in `tmp/` (never committed) and is gone from
|
|
76
|
+
everywhere else.
|
|
77
|
+
- [ ] Nothing secret, and nothing only some visitors may read, is in `public/` or in the space's documents: both reach
|
|
78
|
+
every visitor.
|
|
79
|
+
- [ ] `typecheck`, `lint` and `format` are clean.
|
|
80
|
+
|
|
69
81
|
## Proven
|
|
70
82
|
|
|
71
83
|
- [ ] You looked at it: the visual check ran, on the pages you changed, in both themes.
|
|
@@ -31,8 +31,8 @@ Never rewrite an exported space by hand. `specFromSpace({ schema, style })` read
|
|
|
31
31
|
A server action reads back the same way: `actionSpecFromEntry({ id, document })` gives the `defineAction` spec that writes
|
|
32
32
|
it — only when that spec writes the very same document, otherwise `reason` says why and it stays JSON — and
|
|
33
33
|
`actionToSource(spec)` writes the module. For the whole space as a project — pages, actions, functions, the source of
|
|
34
|
-
its plugins and runtime, its files — use `plitzi create --from <space>` (
|
|
35
|
-
the parts by hand.
|
|
34
|
+
its plugins and runtime, its files — use `plitzi create --from <space>` (then `plitzi pull` to bring the space's changes in, and
|
|
35
|
+
`plitzi push` to put the project's back) rather than exporting the parts by hand.
|
|
36
36
|
|
|
37
37
|
For a document you did not author — an export, a JSON edited by hand — run the gate before serving it:
|
|
38
38
|
`validateSpace({ schema, style })`.
|
|
@@ -39,7 +39,7 @@ export const panel = (params: { id: string; title: string; sub?: string; link?:
|
|
|
39
39
|
children: [
|
|
40
40
|
heading(params.title, { id: 'title', subType: 'h3', class: panelTitle }),
|
|
41
41
|
...(params.sub ? [text(params.sub, { id: 'sub', class: panelSub })] : []),
|
|
42
|
-
...(params.link ? [link({ id: 'link', href: params.link.href, class: panelLink,
|
|
42
|
+
...(params.link ? [link({ id: 'link', href: params.link.href, class: panelLink, content: params.link.label })] : [])
|
|
43
43
|
]
|
|
44
44
|
}),
|
|
45
45
|
...params.children
|
|
@@ -6,7 +6,7 @@ Plitzi's templates are Twig, run by Plitzi's own interpreter. What a `{{ … }}`
|
|
|
6
6
|
|
|
7
7
|
| Written in | What is evaluated | What it can read |
|
|
8
8
|
| --- | --- | --- |
|
|
9
|
-
| a binding's `twigTemplate` transformer | the whole template: conditions, filters, `{% set %}`, loops, tests | `source` (the bound value), `sourceTo
|
|
9
|
+
| a binding's `twigTemplate` transformer | the whole template: conditions, filters, `{% set %}`, loops, tests | `source` (the bound value — undefined until it has one: `{{ source|default('…') }}`), `sourceTo` (the attribute's value before it), the globals, the variables by bare name, and every source around the element |
|
|
10
10
|
| a flow step's params | the whole template | the trigger's payload and earlier steps by name, the globals, sources around the element |
|
|
11
11
|
| an ATTRIBUTE (`href`, `src`, `content`…) | only `{{ name }}` tokens (a filter after the name is allowed) | the globals, the variables and the page's route params by bare name (`{{ apiUrl }}`, `{{ slug }}`), and every source around the element (`{{ list_games.item.slug }}`) |
|
|
12
12
|
|