@mosetta/react 0.0.11 → 0.0.13

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/AUTHORING.md ADDED
@@ -0,0 +1,212 @@
1
+ # Writing tiles
2
+
3
+ What a block of a mosetta site is made of, and the rules it lives by. Written for whoever writes one
4
+ — a developer, or an assistant writing a project's own blocks.
5
+
6
+ ## A tile palette
7
+
8
+ A tile palette is a folder: `package.json` (its version), `mosetta.json` (its brand), `tsconfig.json`
9
+ and `tiles/`. A tile is a folder under `tiles/`; its files are its **sides**:
10
+
11
+ ```
12
+ tiles/greeting/greeting.component.tsx what the visitor sees (required)
13
+ tiles/greeting/greeting.settings.ts what its settings are — a schema (strongly advised)
14
+ tiles/greeting/greeting.editor.tsx its settings form in the editor (optional)
15
+ tiles/greeting/greeting.server.ts its server actions (optional)
16
+ tiles/greeting/greeting.icon.svg its icon in the block picker (optional)
17
+ tiles/greeting/greetingCommon.ts what the sides share: types, tokens, constants (any name)
18
+ tiles/greeting/greeting.styles.css its own stylesheet, imported by the component (any name)
19
+ ```
20
+
21
+ A tile's full id is `_ns_<brand>_t_<side>_n_<name>_v_<version>`. A page names blocks by full id, so a
22
+ released version never changes: a change is a new version, and a page takes it when its author
23
+ updates the block. The tile's name is its folder's name: letters and digits, starting with a letter
24
+ (`greeting`, `bookingForm`).
25
+
26
+ ## The component side
27
+
28
+ ```tsx
29
+ import { defineComponentSide, useChildren, useSettings, useStyler } from '@mosetta/react';
30
+ import type { IGreetingSettings } from './greetingCommon';
31
+ import './greeting.styles.css';
32
+
33
+ export default defineComponentSide<IGreetingSettings>({
34
+ Component() {
35
+ const styler = useStyler();
36
+ const { text, tone } = useSettings<IGreetingSettings>();
37
+ return (
38
+ <section {...styler.part('root')} className={`greeting greeting--${tone}`}>
39
+ <h2 {...styler.part('title')}>{text}</h2>
40
+ {useChildren({ max: 'many' })}
41
+ </section>
42
+ );
43
+ },
44
+ defaultSettings() {
45
+ return { text: 'Hello', tone: 'warm' };
46
+ },
47
+ });
48
+ ```
49
+
50
+ - **Settings** are the node's data, filled in the editor. `defaultSettings()` is copied into a node
51
+ when it is placed — the runtime never merges defaults later, so give every field a default, and
52
+ read fields defensively (a node saved by an older version may lack a newer field).
53
+ - **Children**: a block holds other blocks only where it calls `useChildren`. `max` says how many fit
54
+ (`1`, `3`, `'many'`); a named slot (`useChildren({ slot: 'footer', max: 'many' })`) is a second place.
55
+ A block that calls none holds nothing.
56
+ - **Styling**: `useStyler().part('<name>')` lets the page's author restyle a part from the editor
57
+ (declare the parts and accept them in the settings schema with `styles(PARTS)`). Use the theme's
58
+ CSS variables for colours and fonts so the block follows the site's theme. Import your own CSS file;
59
+ the build ships it with the tile.
60
+ - **Translatable text**: `bindable: { text: 'text' }` in the definition lets a setting hold a reference
61
+ to a translation (`{"$ref": "i18n/<key>"}`) instead of the words; `useSettings` gives the words in
62
+ the visitor's language.
63
+ - **Other hooks** (all from `@mosetta/react`): `useRoute`, `useRouter`, `useHref`, `Link` (moves
64
+ without reloading), `useLanguages`, `useTheme`, `useAddToHead`, `useMediaUpload`,
65
+ `useIsPreviewMode`.
66
+ - **Rendering on the server**: pages are rendered on the server first. Do not touch `window`,
67
+ `document` or `localStorage` while rendering or at the top of a module — only in effects and event
68
+ handlers.
69
+
70
+ ## The settings schema
71
+
72
+ ```ts
73
+ // tiles/greeting/greeting.settings.ts
74
+ import { bindable, settingsOf, styles, z } from '@mosetta/settings';
75
+ import { GREETING_PARTS } from './greetingCommon';
76
+ import type { IGreetingSettings } from './greetingCommon';
77
+
78
+ export const settings = settingsOf<IGreetingSettings>()(z.object({
79
+ text: bindable(z.string().describe('the greeting, plain text')),
80
+ tone: z.enum(['warm', 'cool']).describe('the colour behind it'),
81
+ styles: styles(GREETING_PARTS).optional(),
82
+ }).describe('A greeting: a heading on a coloured band, with blocks under it'));
83
+ ```
84
+
85
+ The build checks the schema against the settings type and puts it into the tile's descriptor: the
86
+ editor and assistants read what each setting does and which values it allows, and settings are
87
+ checked where they are written. Describe every field and the block itself in plain words — that text
88
+ is how others learn the block without reading its code.
89
+
90
+ ## The editor side (optional)
91
+
92
+ Without one, a block's settings are edited by its schema. Write one when the form needs more than
93
+ fields:
94
+
95
+ ```tsx
96
+ import { defineEditorSide, useSettingsDraft } from '@mosetta/react';
97
+ import type { IGreetingSettings } from './greetingCommon';
98
+
99
+ export default defineEditorSide<IGreetingSettings>({
100
+ Component() {
101
+ const { draft, update } = useSettingsDraft<IGreetingSettings>();
102
+ return <input value={draft.text} data-field="text" onChange={(e) => update({ text: e.target.value })} />;
103
+ },
104
+ });
105
+ ```
106
+
107
+ `data-field` marks a bindable field: the editor offers a translation reference beside it. An editor
108
+ side may also declare `preview` (a small tree that shows the block in the picker) and `variants`
109
+ (states worth seeing side by side on the block's page).
110
+
111
+ ## Server actions
112
+
113
+ Code that must not run in the browser — reading a database, sending mail, using a secret — is a
114
+ server action. The token is shared by both sides; keep it in the common file:
115
+
116
+ ```ts
117
+ // tiles/booking/bookingCommon.ts
118
+ import { ServerActionToken } from '@mosetta/server-action/core';
119
+
120
+ export interface IBookingInput { name: string; email: string; slot: string }
121
+ export const bookToken = new ServerActionToken<IBookingInput, { ok: boolean }>({ actionName: 'book' });
122
+ ```
123
+
124
+ ```ts
125
+ // tiles/booking/booking.server.ts
126
+ import { DiInject, DiServerAction, defineServerSide, SECRETS_TOKEN, ServerAction } from '@mosetta/server-action/server';
127
+ import type { ICreateSchema, ISecrets, IServerActionPayload } from '@mosetta/server-action/server';
128
+ import { bookToken } from './bookingCommon';
129
+ import type { IBookingInput } from './bookingCommon';
130
+
131
+ @DiServerAction(bookToken)
132
+ class BookAction extends ServerAction<IBookingInput, { ok: boolean }> {
133
+ constructor(@DiInject(SECRETS_TOKEN) private readonly secrets: ISecrets) {
134
+ super();
135
+ }
136
+
137
+ authorize() {
138
+ return true; // a visitor's form: anyone may send it
139
+ }
140
+
141
+ createSchema({ builder }: ICreateSchema) {
142
+ return builder.object({ name: builder.string().min(1), email: builder.string().email(), slot: builder.string() });
143
+ }
144
+
145
+ async action({ input }: IServerActionPayload<IBookingInput>) {
146
+ const token = await this.secrets.get('D1_TOKEN');
147
+ // … write the booking with it
148
+ return { ok: Boolean(token && input.slot) };
149
+ }
150
+ }
151
+
152
+ export default defineServerSide({ services: [BookAction] });
153
+ ```
154
+
155
+ ```tsx
156
+ // in the component side
157
+ import { EServerActionStart } from '@mosetta/server-action/core';
158
+ import { useServerAction } from '@mosetta/react/server-action';
159
+
160
+ const booking = useServerAction({ token: bookToken, start: EServerActionStart.START_MANUAL });
161
+ // booking.start?.(input) sends it; booking.isLoading(), booking.isSuccess(), booking.serverData
162
+ ```
163
+
164
+ - **`authorize` is required.** It says who may call: `true` for anyone, or a check of the `caller` it is
165
+ given (what the host vouches for — on the platform, `roles: ['editor']` for the site's editors).
166
+ - **Input is validated** by `createSchema` before `authorize` and the action run.
167
+ - **Reading data** for a page: `useServerAction({ token, input })` starts as the block renders and
168
+ caches by input; `start: EServerActionStart.START_FROM_SERVER` runs it during server rendering, so
169
+ the page arrives with the data in it.
170
+ - **Secrets** are asked for by name (`UPPER_SNAKE_CASE`) and set in the site's settings; a value is
171
+ never in the code or the environment. The server side of a tile is named like its component
172
+ (`booking.server.ts` beside `booking.component.tsx`).
173
+
174
+ ### The block a call comes from
175
+
176
+ The input is whatever the browser sent — anybody can send anything. What the site's author set on
177
+ the block (the hours a booking form offers, the address a contact form writes to) is given to the
178
+ action separately, read by the host from the page itself: `block` beside `input`, in `action` and in
179
+ `authorize`.
180
+
181
+ ```ts
182
+ async action({ input, block }: IServerActionPayload<IBookingInput>) {
183
+ const hours = (block?.settings.hours ?? []) as string[];
184
+ if (!hours.includes(input.slot)) return { ok: false }; // not a time this form offers
185
+ // … write the booking
186
+ return { ok: true };
187
+ }
188
+ ```
189
+
190
+ - `block` is `{ page, node, reference, settings, draft }`: the page's name, the block's id on it, the
191
+ block's tile, its settings as stored, and whether it was read from a draft. A translated text in
192
+ the settings stays a reference (`{ $ref: … }`), as the page stores it.
193
+ - The component side does nothing for it: `useServerAction` says which block on which page calls.
194
+ The host finds that block on the page as published; in a draft's preview, in the draft — for the
195
+ site's editors only, everybody else gets the published page.
196
+ - `block` is `null` when the call comes from no page, when the block is not on the page as published
197
+ (a stale tab, a new block not published yet), or when it is not a tile of this palette at the
198
+ action's own version. An action that needs the block's settings refuses then: never fall back to
199
+ something the input says.
200
+ - Inside a prefab, the block's id is `<prefab's id on the page>/<id in the prefab>` and its settings
201
+ are as rendered, the instance's fields filled. A block inside a library's prefab is not read.
202
+
203
+ ## Rules of a project's palette on the platform
204
+
205
+ - Its brand is the project's own; it holds the project's tiles (and the packages they import), every
206
+ tile with its integrity — the build writes it. No prefabs: a project's prefabs are its content.
207
+ - It is built over the platform's tiles: the UI kit and the framework come from the platform, not
208
+ from the palette. Name them in `package.json` at the versions the template gives.
209
+ - Types are checked before a release: a type error in the palette's own files stops it, and the
210
+ error comes back as the checker's line.
211
+ - Every release is the next patch version. Pages keep the version they name; update a block on a
212
+ page to take the new one.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # @mosetta/react
2
2
 
3
+ ## 0.0.13
4
+
5
+ ### Patch Changes
6
+
7
+ - A server action learns the block it is called from: `block` — the page, the node, the tile and its
8
+ settings as the page stores them — beside `input`, in `action` and in `authorize` (and as
9
+ `BLOCK_TOKEN`). The browser only says which block on which page calls; the runner reads that block
10
+ off the published page itself (a draft's, for the site's editors), so the settings are the site
11
+ author's and can be trusted. `useServerAction` sends where the call comes from without the
12
+ component doing anything.
13
+ - Updated dependencies
14
+ - @mosetta/server-action@0.0.13
15
+ - @mosetta/server@0.0.13
16
+ - @mosetta/core@0.0.13
17
+ - @mosetta/di@0.0.13
18
+ - @mosetta/runtime@0.0.13
19
+
20
+ ## 0.0.12
21
+
22
+ ### Patch Changes
23
+
24
+ - `@mosetta/react` ships AUTHORING.md: what a tile is made of and the rules it lives by — sides,
25
+ settings and their schema, children and slots, styling, the editor side, server actions with their
26
+ access, inputs and secrets, and what a project's tile palette on a platform may hold. A catalog entry
27
+ (`@mosetta/content`) may name the public base of its tile host, for a block of a host other than the
28
+ site's primary.
29
+ - @mosetta/server@0.0.12
30
+ - @mosetta/core@0.0.12
31
+ - @mosetta/di@0.0.12
32
+ - @mosetta/runtime@0.0.12
33
+ - @mosetta/server-action@0.0.12
34
+
3
35
  ## 0.0.11
4
36
 
5
37
  ### Patch Changes
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # @mosetta/react
2
2
 
3
- The React binding of the mosetta protocol: what a tile's sides are written with.
3
+ The React binding of the mosetta protocol: what a tile's sides are written with. The whole of it —
4
+ sides, settings schemas, editor forms, server actions, the rules a palette lives by — is in
5
+ [AUTHORING.md](./AUTHORING.md), shipped with the package.
4
6
 
5
7
  A tile is a folder under `tiles/` whose files are its sides — `tiles/<name>/<name>.<side>.tsx`. The
6
8
  component side is what the visitor sees; it reads the settings a content manager filled in the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mosetta/react",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "React binding of the mosetta protocol: side definers, hooks, blueprint renderer, DI adapter, server-action hook, startAndMount. Subpath exports only (no barrel) — each entry ships as its own package tile; ./context is the internal state-singleton entry the others re-export from.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -13,11 +13,11 @@
13
13
  "dependencies": {
14
14
  "react": "*",
15
15
  "react-dom": "*",
16
- "@mosetta/core": "0.0.11",
17
- "@mosetta/di": "0.0.11",
18
- "@mosetta/runtime": "0.0.11",
19
- "@mosetta/server": "0.0.11",
20
- "@mosetta/server-action": "0.0.11"
16
+ "@mosetta/core": "0.0.13",
17
+ "@mosetta/runtime": "0.0.13",
18
+ "@mosetta/server": "0.0.13",
19
+ "@mosetta/server-action": "0.0.13",
20
+ "@mosetta/di": "0.0.13"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/react": "*",
@@ -91,6 +91,9 @@ export const ProvideNode = NodeContext.Provide;
91
91
 
92
92
  export const useNode = NodeContext.use;
93
93
 
94
+ /** the node a component renders in, or null outside a page's tree (a hosted mount, a tool) */
95
+ export const useNodeOptional = NodeContext.useOptional;
96
+
94
97
  function useNodeState(): INodeState {
95
98
  const { id } = useNode();
96
99
  const di = useDi(DI_CONTAINER_TOKEN);
@@ -19,8 +19,10 @@
19
19
  // conditional hooks (branches are value-level), no module-level shared state. Opt-in suspense throws
20
20
  // the waiter promise — the legacy protocol both runtimes (and preact's renderToStringAsync) support.
21
21
  import { useCallback, useState, useSyncExternalStore } from 'react';
22
- import { useComponent } from '@mosetta/react';
22
+ import { useComponent, useDiOptional, useNodeOptional } from '@mosetta/react';
23
23
  import { useDi } from '@mosetta/react';
24
+ import { PAGE_MANAGER_TOKEN } from '@mosetta/runtime/client';
25
+ import { blockOrigin } from '@mosetta/server-action/core';
24
26
  import {
25
27
  EServerActionStart,
26
28
  EXECUTION_ENV_TOKEN,
@@ -82,6 +84,13 @@ export function useServerAction<TIn extends {}, TOut>(
82
84
  ensureServerActionClient(di);
83
85
  const manager = useDi(SERVER_DATA_MANAGER);
84
86
  const { reference } = useComponent();
87
+ // where the call comes from — this node on the page shown now — for the host to read the
88
+ // block's settings itself (core.ts BLOCK_ORIGIN_HEADER); none outside a page. Asked again at
89
+ // every mutation: a block a move onto another page keeps is that page's block from then on
90
+ const pages = useDiOptional(PAGE_MANAGER_TOKEN);
91
+ const nodeId = useNodeOptional()?.id;
92
+ const originNow = (): string | null => blockOrigin(pages ? (pages.getCurrentPage() as { origin?: string }).origin : null, nodeId);
93
+ const origin = originNow();
85
94
 
86
95
  const env = useDi(EXECUTION_ENV_TOKEN);
87
96
  const recorder = useDi(SSR_ACTION_RECORDER_TOKEN);
@@ -116,18 +125,21 @@ export function useServerAction<TIn extends {}, TOut>(
116
125
  actionName: token.actionName,
117
126
  invalidates: mergeTags(token.invalidates, options.invalidates),
118
127
  customSerializer: token.customSerializer,
128
+ originOf: originNow,
119
129
  });
120
130
  }
121
131
  return null;
122
132
  });
123
133
 
124
- const key = localHandle ? null : `${fullId}/${token.actionName}#${stableStringify(options.input)}`;
134
+ // two blocks of one tile asking with one input are two questions: the host answers each by its
135
+ // own block's settings
136
+ const key = localHandle ? null : `${fullId}/${token.actionName}#${stableStringify(options.input)}${origin ? `@${origin}` : ''}`;
125
137
  if (key !== null) {
126
138
  // render-phase, but idempotent by key (start-on-first-read); a new input → a new key → a new query
127
139
  manager.ensureQuery({
128
140
  key,
129
141
  tags: mergeTags(token.tags, options.tags),
130
- request: { fullId, actionName: token.actionName, input: options.input },
142
+ request: { fullId, actionName: token.actionName, input: options.input, ...(origin ? { origin } : {}) },
131
143
  customSerializer: token.customSerializer,
132
144
  });
133
145
  // the head channel's counter waits for this query from the SAME render that started it