@particle-academy/fancy-cms-ui 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +395 -0
- package/LICENSE +21 -0
- package/agent-guide/professional-pages.md +107 -0
- package/dist/{chunk-SYRU2X45.js → chunk-IOMIYI45.js} +6 -4
- package/dist/{chunk-SYRU2X45.js.map → chunk-IOMIYI45.js.map} +1 -1
- package/dist/{chunk-LY6ID2J2.js → chunk-SJYKJ6SG.js} +282 -78
- package/dist/chunk-SJYKJ6SG.js.map +1 -0
- package/dist/{chunk-R7HDS4VG.js → chunk-V6QIOJI7.js} +3 -3
- package/dist/{chunk-R7HDS4VG.js.map → chunk-V6QIOJI7.js.map} +1 -1
- package/dist/editor.cjs +274 -70
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.d.cts +2 -2
- package/dist/editor.d.ts +2 -2
- package/dist/editor.js +3 -3
- package/dist/index.cjs +283 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +4 -4
- package/dist/react.cjs +253 -52
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +2 -2
- package/dist/{registry-CUNaBqTs.d.cts → registry-B4FaQDip.d.cts} +26 -0
- package/dist/{registry-CUNaBqTs.d.ts → registry-B4FaQDip.d.ts} +26 -0
- package/dist/{useEditor-wdlYQMHg.d.cts → useEditor-CIRm2oi-.d.cts} +1 -1
- package/dist/{useEditor-aPH5falI.d.ts → useEditor-DZDD9qpS.d.ts} +1 -1
- package/package.json +107 -105
- package/dist/chunk-LY6ID2J2.js.map +0 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
> **Pre-1.0:** breaking changes land in MINOR releases. Read the entry, not the
|
|
9
|
+
> version number.
|
|
10
|
+
|
|
11
|
+
> **0.2.0 and 0.3.0 are missing below.** This file was last updated at 0.1.1 and
|
|
12
|
+
> the two releases after it were never written up; `git log` is the record for
|
|
13
|
+
> those. Noted rather than quietly back-filled from memory.
|
|
14
|
+
|
|
15
|
+
## [Unreleased]
|
|
16
|
+
|
|
17
|
+
## [0.8.0] - 2026-10-23
|
|
18
|
+
|
|
19
|
+
What a professional landing page needs from the renderer, found by mapping
|
|
20
|
+
the stack for the lab's website-builder agent.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **BREAKING (markup): the `[data-cms]` element is now the semantic element.**
|
|
25
|
+
`heading` renders `<h1>`-`<h6>` (`props.level`, default 2), `text` renders
|
|
26
|
+
`<p>`, `section` renders `<section>`, `button` and `link` with a safe `href`
|
|
27
|
+
render `<a>`, and `list` renders `<ul>`/`<ol>`. Previously every node was a
|
|
28
|
+
`<div>`. **What to do:** nothing, unless your own CSS targets
|
|
29
|
+
`div[data-cms]` or `[data-cms] > a` (the inner `<a>` of a `link` is gone;
|
|
30
|
+
the link IS the element now). Select by `[data-cms="<id>"]`, which is
|
|
31
|
+
unchanged.
|
|
32
|
+
- **BREAKING (security, CSS): a style value that could escape its declaration
|
|
33
|
+
is dropped.** A value containing `{`, `}` or `<`, or a `;` outside
|
|
34
|
+
parentheses or quotes, emits nothing. **What to do:** nothing for any real
|
|
35
|
+
colour, gradient, font stack or `url(data:...;base64,...)`, which all still
|
|
36
|
+
pass. Only a value that was trying to close the rule changes.
|
|
37
|
+
- `direction`, `columns` and the new container knobs are read **per
|
|
38
|
+
breakpoint**. A grid can now go 1 column -> 2 -> 3, and a stack column ->
|
|
39
|
+
row. Base output is unchanged.
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- Style properties: `paddingX` / `paddingY`, `marginX` (`"auto"` centres) /
|
|
44
|
+
`marginY`, `maxWidth`, `minHeight`, `aspectRatio`, `sticky`, `zIndex`,
|
|
45
|
+
`overflow`, `hidden` (per breakpoint), `textTransform`, `backgroundSize`,
|
|
46
|
+
`backgroundPosition`, `wrap`, and `columnMin` for an auto-fit grid that is
|
|
47
|
+
responsive with no breakpoints at all.
|
|
48
|
+
- **Theme tokens are emitted** as `:root { --cms-* }` custom properties
|
|
49
|
+
(`theme.color.primary` -> `--cms-color-primary`), so styles can reference
|
|
50
|
+
`var(--cms-color-primary)`.
|
|
51
|
+
- `props.as` on containers: `section`, `header`, `nav`, `main`, `footer`,
|
|
52
|
+
`article`, `aside`, `div`. Anything else is ignored, never used as a tag.
|
|
53
|
+
- `list` element (`props.items`, `props.ordered`), and List in the editor's add menu.
|
|
54
|
+
- `props.anchor` on any node renders as its `id`, so an in-page link
|
|
55
|
+
(`href="#pricing"`) has somewhere to land. Only a lowercase slug of up to 64
|
|
56
|
+
characters is rendered (`ANCHOR_PATTERN`, `anchorFor` exported); anything
|
|
57
|
+
else is ignored. The PHP renderer follows in fancy-cms 0.5.0.
|
|
58
|
+
- `image`: `props.fit` (`cover` default, `contain`, `fill`, `none`,
|
|
59
|
+
`scale-down`) and `props.loading` (lazy by default).
|
|
60
|
+
- **`agent-guide/professional-pages.md`**, shipped in the tarball: how an agent
|
|
61
|
+
builds a professional page with these primitives (the bar, scales, batching
|
|
62
|
+
ops, deferring to a person's edits). It is what the lab's website-builder
|
|
63
|
+
agent is measured with, so its numbers are tuned by lab passes, not by taste.
|
|
64
|
+
- `safeHref`, `elementFor`, `LANDMARK_TAGS` and `safeCssValue` are exported.
|
|
65
|
+
Hrefs pass for relative URLs and `http`/`https`/`mailto`/`tel`; every other
|
|
66
|
+
scheme (`javascript:`, `data:` ...) is dropped. External http(s) links get
|
|
67
|
+
`rel="noopener noreferrer"`.
|
|
68
|
+
- **Security: nothing a document holds can escape the inline stylesheet.**
|
|
69
|
+
Two routes carried raw strings into `<style>`. A node id went into
|
|
70
|
+
`[data-cms="<id>"]` unescaped, so an id containing `</style><script>` ended
|
|
71
|
+
the stylesheet and injected a script; ids now pass through `cssString`
|
|
72
|
+
(`\` and `"` escaped, control characters and `<` as hex escapes), and the
|
|
73
|
+
selector still matches the attribute. And `align`, `justify`, `direction`,
|
|
74
|
+
`textAlign`, a Length's unit and the numeric fields were written without a
|
|
75
|
+
check; now a Length needs a finite number and a known unit, a keyword must be
|
|
76
|
+
on its allowlist, and a number must be finite, or the declaration is dropped.
|
|
77
|
+
**Nothing to do:** every valid document emits the same CSS as before.
|
|
78
|
+
`parity/canonical-doc-hostile.json` holds the PHP emitter to the same rules.
|
|
79
|
+
Found by the PHP port's source review.
|
|
80
|
+
- **Security / Fixed: node ids and breakpoint names that are also
|
|
81
|
+
`Object.prototype` names.** Ids arrive in ops, which an agent writes. The node
|
|
82
|
+
map was read as a plain object, so `constructor`, `toString` and friends
|
|
83
|
+
resolved to built-ins: inserting a node called `constructor` was refused as a
|
|
84
|
+
duplicate, removing an absent `toString` "succeeded" and bumped `seq`, and a
|
|
85
|
+
node called `__proto__` replaced the map's prototype instead of being added.
|
|
86
|
+
Every lookup and write in the reducer, renderer, CSS emitter and migration now
|
|
87
|
+
uses own properties only (`src/document/own.ts`). **Nothing to do:** a page
|
|
88
|
+
whose ids are ordinary strings behaves exactly as before. Found by the PHP
|
|
89
|
+
port, which has no prototype to see.
|
|
90
|
+
- **Fixed:** `reorder_roots` accepted a list that repeated a root as long as
|
|
91
|
+
its length matched (`["s1","s1"]` for roots s1, s2), rewrote that root's key
|
|
92
|
+
twice and left the other's alone, so the page order became arbitrary. It is
|
|
93
|
+
now refused as not a permutation, like a list with an unknown id. Found while
|
|
94
|
+
porting the reducer to PHP.
|
|
95
|
+
- Parity: `parity/run.mjs` checks every `parity/canonical*.json`;
|
|
96
|
+
`canonical-doc-0.8.json` covers the new properties and the values that must
|
|
97
|
+
be dropped.
|
|
98
|
+
|
|
99
|
+
### Fixed
|
|
100
|
+
|
|
101
|
+
- **`CHANGELOG.md` is now in the published tarball.** `files` did not whitelist it, so npm never shipped it — and this package puts breaking changes in MINOR releases and tells you in the README to read the entry before taking one. The instruction existed for the author, who has the file, and not for the consumer, who is the only one being instructed. Nothing for you to do; the file simply arrives from this release on.
|
|
102
|
+
- Dropped `docs` from `files`: it was declared and never existed, so the package claimed to ship it and did not.
|
|
103
|
+
|
|
104
|
+
### Security
|
|
105
|
+
|
|
106
|
+
- `source-map-js` is pinned forward to `^1.2.2` via `overrides`. Versions up to
|
|
107
|
+
1.2.1 allow an event-loop denial of service through indexed source-map section
|
|
108
|
+
offsets, and it arrives here transitively through the build toolchain.
|
|
109
|
+
**Nothing for a consumer to do, and no runtime change**: an npm package does
|
|
110
|
+
not ship a lockfile, so this governs builds OF this repo, not anything
|
|
111
|
+
installed FROM it. Recorded rather than left silent because the override it
|
|
112
|
+
sits beside — `shell-quote` `^1.9.0`, added for an earlier advisory — was
|
|
113
|
+
carried with no note of why, and had drifted back inside the vulnerable range
|
|
114
|
+
before anyone looked.
|
|
115
|
+
- **Dev toolchain only — no runtime change, nothing for consumers to do.** The
|
|
116
|
+
lockfile now resolves `vite` 8.3.5 (was 8.3.0), whose bundled
|
|
117
|
+
`launch-editor` fixes GHSA-587h-7wmj-h5pm (HIGH, `fs.existsSync()` check
|
|
118
|
+
bypassable on Windows), GHSA-698v-4r3x-3vcv (`escapeCmdArgs()` omits `%`
|
|
119
|
+
escaping on Windows) and GHSA-r825-w8mm-5pxw (editor CLI flag
|
|
120
|
+
misinterpretation). `vite` is only reached through the test/dev toolchain
|
|
121
|
+
and the published package does not ship our lockfile, so the exposure was
|
|
122
|
+
this repo's own dev server.
|
|
123
|
+
|
|
124
|
+
## [0.7.1] — 2026-09-17
|
|
125
|
+
|
|
126
|
+
### Fixed
|
|
127
|
+
|
|
128
|
+
- **Malformed authored documents now render empty instead of throwing during
|
|
129
|
+
React render.** `CmsPage` normalizes a missing, null, array, or otherwise
|
|
130
|
+
invalid `nodes` map at its migration boundary, while direct `emitDocCss()`
|
|
131
|
+
and `rootIds()` calls defensively return empty results. A bad agent-authored
|
|
132
|
+
value can no longer unmount the host subtree around the CMS page.
|
|
133
|
+
|
|
134
|
+
**What you must do:** nothing. Well-formed documents render unchanged; the
|
|
135
|
+
fallback only applies when `nodes` is not a non-null object.
|
|
136
|
+
|
|
137
|
+
## [0.7.0] — 2026-08-09
|
|
138
|
+
|
|
139
|
+
### Added
|
|
140
|
+
|
|
141
|
+
- **`$source` on a binding — where its data comes from, in the Live Contract's
|
|
142
|
+
vocabulary.** Story #171, AC2.
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
{ $bind: "products.0.name", $source: ["catalog", "products"] }
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`{ $bind: "profile.coins" }` is a path into whatever blob the host passes as
|
|
149
|
+
`data`. It says nothing about which query produced that blob, so nothing could
|
|
150
|
+
know what to re-render when the underlying data changed — a host wiring live
|
|
151
|
+
data had to work it out per page, by hand.
|
|
152
|
+
|
|
153
|
+
`$source` names it in the same `[namespace, resource, …]` shape
|
|
154
|
+
`fancy-query`'s `liveKey()` produces and every backend twin declares its
|
|
155
|
+
broadcast events against. This is exactly why the work was gated behind the
|
|
156
|
+
Live Contract: had the substrate invented its own name for "where this data
|
|
157
|
+
lives", that vocabulary would have been baked into every **saved document**.
|
|
158
|
+
|
|
159
|
+
- `bindingSources(doc)` answers *what live queries does this page read?* —
|
|
160
|
+
walking props and a repeater's `each`, which is the densest data dependency
|
|
161
|
+
in the model.
|
|
162
|
+
- A malformed `$source` reads as absent rather than throwing. Documents arrive
|
|
163
|
+
from databases and from agents; a renderer that throws takes the page down.
|
|
164
|
+
- Deliberately a plain array — structurally a TanStack `QueryKey` without the
|
|
165
|
+
dependency, and it survives JSON.
|
|
166
|
+
|
|
167
|
+
**What you must do:** nothing. `$source` is optional, and every document saved
|
|
168
|
+
before this has none.
|
|
169
|
+
|
|
170
|
+
- **Asserts `fancy-doc-commons`' `CANONICAL_WALKS`** — the shared fixture
|
|
171
|
+
`fancy-screens` asserts too, so both surfaces are checked against one tree
|
|
172
|
+
rather than each being inspected separately (AC1).
|
|
173
|
+
|
|
174
|
+
### Changed
|
|
175
|
+
|
|
176
|
+
- `@particle-academy/fancy-doc-commons` to `>=0.4 <2.0`.
|
|
177
|
+
|
|
178
|
+
## [0.6.0] — 2026-08-09
|
|
179
|
+
|
|
180
|
+
### Changed
|
|
181
|
+
|
|
182
|
+
- **BREAKING — the `set_props` op is now `set_node_props`.**
|
|
183
|
+
|
|
184
|
+
`@particle-academy/fancy-doc-commons`' `TreeOp` uses the same discriminator
|
|
185
|
+
`t` and also had a `set_props` variant, with a byte-identical shape. So a
|
|
186
|
+
stored `{ t: "set_props", id, patch }` was **valid under two vocabularies and
|
|
187
|
+
reduced by two different reducers**, with nothing in the document able to say
|
|
188
|
+
which one it meant. Now that `PageDoc` IS a `DocTree`, both reducers can
|
|
189
|
+
plausibly be handed the same document.
|
|
190
|
+
|
|
191
|
+
**What you must do:** if you emit this op directly, rename the tag. If you
|
|
192
|
+
only use `Editor`, `EditablePage`, `Inspector` or `useEditor`, nothing — they
|
|
193
|
+
emit it for you. Stored documents are unaffected: this is an op vocabulary,
|
|
194
|
+
not a document field, so nothing persisted contains it.
|
|
195
|
+
|
|
196
|
+
Every other variant was already disjoint (`insert_node` vs `insert`,
|
|
197
|
+
`move_node` vs `move`), so closing the collision cost exactly one rename. The
|
|
198
|
+
alternative on the table was a versioned envelope (`{v:"tree/1", t:…}`) on
|
|
199
|
+
every op across the kit; that would have been the right answer if the overlap
|
|
200
|
+
were wide, and it is one variant.
|
|
201
|
+
|
|
202
|
+
### Added
|
|
203
|
+
|
|
204
|
+
- A test asserting the two vocabularies stay **disjoint**, so the next
|
|
205
|
+
collision fails the build rather than shipping. Keeping tag spaces disjoint
|
|
206
|
+
is the cheap version of a versioned envelope — but only while something
|
|
207
|
+
checks.
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
### Added
|
|
211
|
+
|
|
212
|
+
- The migration is now tested against a **real saved document** — the showcase's
|
|
213
|
+
own CMS home page as it was actually persisted before 0.5.0, 62 nodes and
|
|
214
|
+
seven sections, extracted from `px-ui-sandbox` at the commit before the
|
|
215
|
+
migration landed. A hand-built fixture only contains the cases its author
|
|
216
|
+
already thought of.
|
|
217
|
+
|
|
218
|
+
That document's `sections` happens to agree with its order keys, because
|
|
219
|
+
nobody ever reordered it — so a further test permutes `sections` and leaves
|
|
220
|
+
the keys alone, which is exactly what the old `reorder_sections` op did, and
|
|
221
|
+
asserts the migration recovers the dragged order.
|
|
222
|
+
|
|
223
|
+
## [0.5.1] — 2026-08-09
|
|
224
|
+
|
|
225
|
+
### Fixed
|
|
226
|
+
|
|
227
|
+
- **`NodeTransform` is exported from the `/editor` entry.** It appears in two of
|
|
228
|
+
`EditablePage`'s props — `transforms?: Record<string, NodeTransform>` and
|
|
229
|
+
`onNodeTransform?: (id, transform: NodeTransform) => void` — but had never
|
|
230
|
+
been exported, so a consumer writing an `onNodeTransform` handler had no way
|
|
231
|
+
to name its second argument. The showcase imported it anyway and carried a
|
|
232
|
+
permanent type error in the app that exists to demonstrate this package.
|
|
233
|
+
|
|
234
|
+
**What you must do:** nothing. This only adds an export.
|
|
235
|
+
|
|
236
|
+
The gap was invisible from inside the package: it built and tested clean, and
|
|
237
|
+
only a CONSUMER could discover it. There is now a type-only fixture
|
|
238
|
+
(`tests/editor-exports.types.ts`) that imports every public prop type through
|
|
239
|
+
the same barrel a consumer uses, so the next one fails the build here rather
|
|
240
|
+
than in someone else's app.
|
|
241
|
+
|
|
242
|
+
### Added
|
|
243
|
+
|
|
244
|
+
- `tsconfig.test.json`, wired into `npm run lint`. `tests/` was excluded from
|
|
245
|
+
type checking entirely, so test files were unchecked TypeScript — turning it
|
|
246
|
+
on surfaced four real type errors that had been sitting in the suite.
|
|
247
|
+
|
|
248
|
+
## [0.5.0] — 2026-08-09
|
|
249
|
+
|
|
250
|
+
### Added
|
|
251
|
+
|
|
252
|
+
- `migrateDoc(doc)` / `migrateDocVerbose(doc)` / `needsMigration(doc)` — forward
|
|
253
|
+
migration for documents saved before this release. Idempotent, and returns an
|
|
254
|
+
already-migrated document by reference, so it is safe to call unconditionally
|
|
255
|
+
on load. `migrateDocVerbose` additionally reports `danglingIds` (ids listed in
|
|
256
|
+
`sections[]` with no matching node) and `unlistedRootIds` (roots the array
|
|
257
|
+
never mentioned, which previously did not render at all).
|
|
258
|
+
- `rootIds(doc)` and `lastRootId(doc)` on `document/reduce` — the two things
|
|
259
|
+
callers actually wanted from `doc.sections`.
|
|
260
|
+
|
|
261
|
+
### Changed
|
|
262
|
+
|
|
263
|
+
- **BREAKING — `PageDoc.sections[]` is gone; roots are ordered by their `order`
|
|
264
|
+
key like every other sibling group.** `PageDoc` is now a `DocTree` from
|
|
265
|
+
`@particle-academy/fancy-doc-commons`, so `childrenOf`, `roots` and
|
|
266
|
+
`descendantsOf` from that package work on a CMS document directly.
|
|
267
|
+
|
|
268
|
+
**What you must do:**
|
|
269
|
+
|
|
270
|
+
- **Reading `doc.sections`** → use `rootIds(doc)`.
|
|
271
|
+
- **Emitting the `reorder_sections` op** → it is now `reorder_roots`, and it
|
|
272
|
+
rewrites the roots' `order` keys rather than permuting an array. The payload
|
|
273
|
+
shape (`{ t, order: NodeId[] }`) is unchanged.
|
|
274
|
+
- **Loading documents you persisted before this release** → nothing, if you
|
|
275
|
+
go through `CmsPage` or `initEditor`; both migrate on the way in. If you
|
|
276
|
+
read a stored document yourself — to index it, diff it, or render it on a
|
|
277
|
+
server — call `migrateDoc` first.
|
|
278
|
+
|
|
279
|
+
This one genuinely bites, so it is worth being precise about why: the old
|
|
280
|
+
`reorder_sections` permuted `sections[]` and left every node's `order` key
|
|
281
|
+
untouched. On any page whose sections were ever dragged into a new order, the
|
|
282
|
+
keys are stale and the array is the **only** record of the real order. So a
|
|
283
|
+
consumer who drops the array and falls back to the keys does not lose a
|
|
284
|
+
redundant field — that page silently reverts to the order it was first
|
|
285
|
+
authored in. Nothing throws, nothing logs, and the layout is wrong. That is
|
|
286
|
+
the case `migrateDoc` exists for, and the reason both entry points call it
|
|
287
|
+
for you.
|
|
288
|
+
|
|
289
|
+
- Top-level order used to live in its own array while every other level used
|
|
290
|
+
fractional keys — two ordering mechanisms that could disagree. A node could be
|
|
291
|
+
a root by `parent === null` and absent from `sections[]`, and then simply not
|
|
292
|
+
render. That is now unrepresentable.
|
|
293
|
+
|
|
294
|
+
### Removed
|
|
295
|
+
|
|
296
|
+
- **BREAKING — `src/document/fractional.ts` is deleted.** `keyBetween` is still
|
|
297
|
+
exported from the package root, now re-exported from
|
|
298
|
+
`@particle-academy/fancy-doc-commons` (which also exports it as
|
|
299
|
+
`fractionalKey`).
|
|
300
|
+
|
|
301
|
+
**What you must do:** if you imported `keyBetween` from the package root,
|
|
302
|
+
nothing — it is the same function and produces byte-identical keys, which the
|
|
303
|
+
test suite pins against golden values captured from the deleted
|
|
304
|
+
implementation. If you deep-imported `@particle-academy/fancy-cms-ui/dist/document/fractional`,
|
|
305
|
+
import from the package root instead.
|
|
306
|
+
|
|
307
|
+
## [0.4.0] — 2026-08-07
|
|
308
|
+
|
|
309
|
+
### Changed
|
|
310
|
+
|
|
311
|
+
- **BREAKING — Node 18 is no longer supported.** `engines.node` moves from `>=18` to `>=22`.
|
|
312
|
+
|
|
313
|
+
**What you must do:** on Node 22 or newer, nothing. Note npm only *warns* on an `engines` mismatch while **pnpm fails the install**, so this surfaces differently depending on your package manager. Node 18 is end-of-life and 20 is maintenance-only.
|
|
314
|
+
|
|
315
|
+
- **BREAKING — React 18 is no longer supported.** `peerDependencies.react` / `react-dom` are now `^19.0.0`.
|
|
316
|
+
|
|
317
|
+
**What you must do:** on React 19, nothing. On React 18, stay on the previous release, or upgrade your app to 19 first.
|
|
318
|
+
|
|
319
|
+
React 18 support was a claim nothing tested — every build and test in this package ran against 19, so the 18 half of the old range was never executed. An untested compatibility claim is worse than an absent one, because it reads as support.
|
|
320
|
+
|
|
321
|
+
### Why
|
|
322
|
+
|
|
323
|
+
These are the kit 0.5 platform floors, applied across every package at once so a consumer never has to resolve a mix. **No API changed, nothing was removed, nothing was renamed** — only what the package requires.
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
## [0.3.1] — 2026-07-27
|
|
327
|
+
|
|
328
|
+
### Fixed
|
|
329
|
+
|
|
330
|
+
- **`Inspector`'s labels were attached to nothing.** Every one was a plain
|
|
331
|
+
sibling of its control — `<label>Background</label><input/>` — so clicking a
|
|
332
|
+
label focused nothing and a screen reader announced the style panel as a
|
|
333
|
+
column of unlabelled boxes.
|
|
334
|
+
|
|
335
|
+
Worth distinguishing: `NodeInspector`'s labels **wrap** their inputs, which
|
|
336
|
+
associates them implicitly and was always correct. This was seven fields in
|
|
337
|
+
one file, not a package-wide defect.
|
|
338
|
+
|
|
339
|
+
Each control now has an `id` its label points at, plus a `data-cms-field`
|
|
340
|
+
handle keyed by the field — the stable identity the Human+ contract asks for,
|
|
341
|
+
so an agent addresses a field by name instead of guessing at the DOM.
|
|
342
|
+
|
|
343
|
+
- **The element palette was hardcoded dark inside a light-themed host.** The
|
|
344
|
+
editor ships a `--fcms-*` light/dark token layer and then set
|
|
345
|
+
`background: "#0b1220"`, `color: "#e2e8f0"` and a slate border directly, so a
|
|
346
|
+
host on the light theme got a permanently dark flyout. The overlay's selection
|
|
347
|
+
outlines, the drop hint and several `NodeInspector` colours bypassed the layer
|
|
348
|
+
the same way.
|
|
349
|
+
|
|
350
|
+
All of them now resolve through `--fcms-*` **with the previous values as
|
|
351
|
+
fallbacks**, so default rendering is unchanged and only a host that themes the
|
|
352
|
+
editor sees a difference. `--fcms-drop` and `--fcms-danger` were added to the
|
|
353
|
+
light and dark blocks, since a token nothing defines cannot be themed from one
|
|
354
|
+
place.
|
|
355
|
+
|
|
356
|
+
**Nothing to do.** No prop or markup changed.
|
|
357
|
+
|
|
358
|
+
### Changed
|
|
359
|
+
|
|
360
|
+
- **A stale note in `Editor.tsx` said the chrome "graduates to react-fancy
|
|
361
|
+
next".** It dates from the Phase 0+1 commit and the `--fcms-*` token layer
|
|
362
|
+
landed three weeks after it, superseding it — react-fancy's primitives are
|
|
363
|
+
hardcoded Tailwind palette classes that read no custom properties, so adopting
|
|
364
|
+
them would ignore `--fcms-accent` and strand a themed host with a half-themed
|
|
365
|
+
editor. The comment now records why the chrome stays plain markup, and that
|
|
366
|
+
the old plan is no longer the plan. It is the kind of note that gets acted on
|
|
367
|
+
long after it stopped being true.
|
|
368
|
+
|
|
369
|
+
### Added
|
|
370
|
+
|
|
371
|
+
- **11 tests** covering label association, the field handles, per-instance ids,
|
|
372
|
+
the select → deselect → reselect transition, and that every `--fcms-*` token
|
|
373
|
+
referenced anywhere is actually defined. Ten of them fail against the previous
|
|
374
|
+
code.
|
|
375
|
+
|
|
376
|
+
## 0.1.1 — 2026-07-06
|
|
377
|
+
|
|
378
|
+
### Fixed
|
|
379
|
+
- **`Editor`: the notify effect no longer depends on the `onChange` prop's
|
|
380
|
+
identity** (#1). The latest `onChange` is held in a ref (synced in an effect)
|
|
381
|
+
and the notify effect depends only on the document — so a consumer passing an
|
|
382
|
+
inline closure that also sets state (the most natural usage) can no longer
|
|
383
|
+
loop the notify (notify → setState → render → new closure identity → effect
|
|
384
|
+
re-fires → notify → …; 91k fires observed for one op). Each op now notifies
|
|
385
|
+
exactly once, guarded by a regression test.
|
|
386
|
+
- The same hygiene applied to `EditablePage`'s `onSelect` notify and
|
|
387
|
+
`onProgress` scroll-reporting effects — the identical latent loop class.
|
|
388
|
+
|
|
389
|
+
## 0.1.0 — 2026-07-05
|
|
390
|
+
|
|
391
|
+
First beta. The editor (`Editor`, `EditablePage`, `Canvas`, `LayersPanel`,
|
|
392
|
+
inspectors, `useEditor` + snapshot undo/redo) and the isomorphic renderer
|
|
393
|
+
(`CmsPage`, `CmsRegion`) exported from the main entry, over the Stages document
|
|
394
|
+
model, the `PageOp` op-spine, and the JS style→CSS emitter (byte-parity with the
|
|
395
|
+
PHP emitter in `particle-academy/fancy-cms`).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Particle Academy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Building professional pages with fancy-cms
|
|
2
|
+
|
|
3
|
+
You are building one page as a fancy-cms document, edited through ops. A person
|
|
4
|
+
may be watching it take shape, and may edit it while you work. The bar is a page
|
|
5
|
+
a design studio would ship: clear, specific, consistent, and good on a phone.
|
|
6
|
+
|
|
7
|
+
## The bar
|
|
8
|
+
|
|
9
|
+
1. **The brief's facts, all of them, exactly.** Every name, price, time, figure
|
|
10
|
+
and link the brief gives appears on the page as given. Nothing is invented:
|
|
11
|
+
no extra features, customers, awards, quotes or statistics. If a section
|
|
12
|
+
would need a fact the brief does not give, leave the section out.
|
|
13
|
+
2. **One message per section.** A visitor can say what each section is for in
|
|
14
|
+
five words.
|
|
15
|
+
3. **The call to action is obvious.** The primary CTA appears in the hero, uses
|
|
16
|
+
the brief's exact label and link, and is visible without scrolling at 375 px.
|
|
17
|
+
4. **Good on a phone first.** Design at 375 px, then widen. Nothing scrolls
|
|
18
|
+
sideways. Tap targets are at least 44 px tall.
|
|
19
|
+
5. **Consistent.** One type scale, one spacing scale, two or three colours from
|
|
20
|
+
the theme. No one-off values.
|
|
21
|
+
|
|
22
|
+
## Never ship
|
|
23
|
+
|
|
24
|
+
- A fact that is not in the brief, or a brief fact that is missing or changed.
|
|
25
|
+
- Lorem ipsum, "Feature 1", placeholder prices or `#` links where the brief gave
|
|
26
|
+
a link.
|
|
27
|
+
- Body text over a busy image, or text that fails contrast (body 4.5:1, large
|
|
28
|
+
text 3:1).
|
|
29
|
+
- More than one `h1`, or heading levels that skip (h2 straight to h4).
|
|
30
|
+
- An image with empty `alt` unless it is purely decorative.
|
|
31
|
+
- Absolute positioning for page layout. Use stack and grid; reserve `free` for
|
|
32
|
+
genuine overlays.
|
|
33
|
+
|
|
34
|
+
## Before you build
|
|
35
|
+
|
|
36
|
+
1. Call `cms_describe` once. It lists every element type, op and style property.
|
|
37
|
+
Do not guess names.
|
|
38
|
+
2. Read the brief and list its facts. Sort them into sections, in this order
|
|
39
|
+
unless the brief says otherwise:
|
|
40
|
+
header, hero, proof or features, the details (menu, pricing, timetable,
|
|
41
|
+
work), trust (quote, numbers, disclaimer), final CTA, footer.
|
|
42
|
+
3. Set the theme first, in one `set_theme` op, so every later style can
|
|
43
|
+
reference it.
|
|
44
|
+
|
|
45
|
+
## The theme
|
|
46
|
+
|
|
47
|
+
Set tokens once and reference them as `var(--cms-<group>-<name>)`:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{ "t": "set_theme", "patch": {
|
|
51
|
+
"color": { "primary": "#0f766e", "ink": "#0f172a", "muted": "#475569", "surface": "#fbfaf7", "line": "#e5e7eb" },
|
|
52
|
+
"font": { "body": "Inter, system-ui, sans-serif", "display": "Inter, system-ui, sans-serif" }
|
|
53
|
+
} }
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Use the brand colour from the brief as `primary`. Ink on surface must clear 4.5:1.
|
|
57
|
+
|
|
58
|
+
## Scales (use these values, not others)
|
|
59
|
+
|
|
60
|
+
- **Type** (px, base / md and up): display 40 / 56, h2 28 / 36, h3 20 / 22,
|
|
61
|
+
body 17 / 18, small 14. Line height 1.15 for display and h2, 1.6 for body.
|
|
62
|
+
Weight 700 for display and h2, 600 for h3, 400 for body.
|
|
63
|
+
- **Spacing** (px): 4, 8, 12, 16, 24, 32, 48, 64, 96. Section padding: `paddingY`
|
|
64
|
+
64 at base, 96 at `md`; `paddingX` 20 at base, 32 at `md`.
|
|
65
|
+
- **Content width**: wrap each section's content in a stack with `maxWidth` 1120,
|
|
66
|
+
`marginX: "auto"` and `paddingX` as above. Text columns no wider than 680.
|
|
67
|
+
- **Radius**: 8 for buttons, 12 for cards.
|
|
68
|
+
|
|
69
|
+
## Building the page
|
|
70
|
+
|
|
71
|
+
- **Batch your ops.** Send one `cms_apply` per section (10-30 ops), not one op
|
|
72
|
+
per call. A person watching sees the page build section by section, and you
|
|
73
|
+
spend a fraction of the tokens.
|
|
74
|
+
- **Structure**: each section is a root `section` node (`props.as: "header"` for
|
|
75
|
+
the header, `"footer"` for the footer). Inside it, one container `stack`, then
|
|
76
|
+
content.
|
|
77
|
+
- **Grids that respond**: for cards, use a `grid` with `columnMin` 260 px; it
|
|
78
|
+
goes from one column on a phone to three on a desktop with no breakpoints. For
|
|
79
|
+
an exact count per width, set `columns` 1 at base, 2 at `md`, 3 at `lg`.
|
|
80
|
+
- **Rows that collapse**: a stack with `direction: "column"` at base and
|
|
81
|
+
`"row"` at `md`, plus `wrap: true` when items may not fit.
|
|
82
|
+
- **Headings**: `heading` with `props.level` 1 for the page title, 2 for each
|
|
83
|
+
section title, 3 inside cards.
|
|
84
|
+
- **Buttons**: `button` with `props.label` and `props.href`; `variant` primary
|
|
85
|
+
for the main CTA, `outline` for the secondary. At least 44 px tall: `paddingY`
|
|
86
|
+
12 and `fontSize` 16.
|
|
87
|
+
- **Lists**: a `list` node with `props.items` for features, inclusions and
|
|
88
|
+
hours, not one text node per line.
|
|
89
|
+
- **Contact**: phone as `tel:+15550104477`, email as `mailto:...`.
|
|
90
|
+
- **Images**: `image` with `props.src`, a descriptive `props.alt`, and
|
|
91
|
+
`aspectRatio` (for example `"4 / 3"`) so it never stretches.
|
|
92
|
+
- **Meta**: `set_meta` with a title under 60 characters and a description under
|
|
93
|
+
155 characters, both from the brief's own words.
|
|
94
|
+
|
|
95
|
+
## When a person edits the page
|
|
96
|
+
|
|
97
|
+
Every result carries a `revision`. If you get `surface_changed_under_you`, a
|
|
98
|
+
person changed the page. Call `cms_changes_since`, read what they did, and keep
|
|
99
|
+
it: their edit is the new intent. Never put back something a person changed. If
|
|
100
|
+
their change conflicts with the brief, build around it and say so in your final
|
|
101
|
+
message.
|
|
102
|
+
|
|
103
|
+
## Finishing
|
|
104
|
+
|
|
105
|
+
Before `cms_finish`, call `cms_get` once and check against the bar: every brief
|
|
106
|
+
fact present and exact, one h1, the CTA in the hero, nothing invented. Fix what
|
|
107
|
+
fails in one more `cms_apply`, then finish with a two-sentence summary.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { migrateDoc, reduce, CmsPage, rootIds, childrenOf, lastRootId, emptyDoc } from './chunk-
|
|
1
|
+
import { migrateDoc, reduce, CmsPage, rootIds, childrenOf, lastRootId, emptyDoc } from './chunk-SJYKJ6SG.js';
|
|
2
2
|
import { useReducer, useCallback, useMemo, useRef, useState, useLayoutEffect, Fragment, useId, useEffect } from 'react';
|
|
3
3
|
import { jsxs, jsx, Fragment as Fragment$1 } from 'react/jsx-runtime';
|
|
4
4
|
import { fractionalKey } from '@particle-academy/fancy-doc-commons';
|
|
@@ -371,7 +371,8 @@ var ADD_DEFAULTS = {
|
|
|
371
371
|
divider: { type: "divider", props: {}, style: {} },
|
|
372
372
|
code: { type: "code", props: { content: "npm install @particle-academy/react-fancy", lang: "bash" }, style: {} },
|
|
373
373
|
richtext: { type: "richtext", props: { html: "<p>Rich <strong>text</strong> with <em>inline</em> formatting.</p>" }, style: {} },
|
|
374
|
-
repeater: { type: "repeater", props: { items: "" }, style: { gap: { value: 12, unit: "px" } }, layout: "stack" }
|
|
374
|
+
repeater: { type: "repeater", props: { items: "" }, style: { gap: { value: 12, unit: "px" } }, layout: "stack" },
|
|
375
|
+
list: { type: "list", props: { items: ["First point", "Second point"], ordered: false }, style: {} }
|
|
375
376
|
};
|
|
376
377
|
var ADD_MENU = [
|
|
377
378
|
{ kind: "heading", label: "Heading" },
|
|
@@ -387,6 +388,7 @@ var ADD_MENU = [
|
|
|
387
388
|
{ kind: "divider", label: "Divider" },
|
|
388
389
|
{ kind: "code", label: "Code" },
|
|
389
390
|
{ kind: "richtext", label: "Rich text" },
|
|
391
|
+
{ kind: "list", label: "List" },
|
|
390
392
|
{ kind: "repeater", label: "Repeater" }
|
|
391
393
|
];
|
|
392
394
|
function resolveInsertParent(doc, selectedId) {
|
|
@@ -574,5 +576,5 @@ var LIGHT_VARS = "color-scheme:light;--fcms-bg:#ffffff;--fcms-fg:#0f172a;--fcms-
|
|
|
574
576
|
var CHROME_CSS = `.fancy-cms-editor{--fcms-accent:#8b5cf6;${LIGHT_VARS}}@media (prefers-color-scheme:dark){.fancy-cms-editor{${DARK_VARS}}}:where(.light,[data-theme="light"]) .fancy-cms-editor{${LIGHT_VARS}}:where(.dark,[data-theme="dark"]) .fancy-cms-editor{${DARK_VARS}}`;
|
|
575
577
|
|
|
576
578
|
export { ADD_DEFAULTS, ADD_MENU, CONTAINER_TYPES, Canvas, Editor, Inspector, LayersPanel, buildInsertOp, editorReduce, initEditor, resolveInsertParent, useEditor, useLatestRef };
|
|
577
|
-
//# sourceMappingURL=chunk-
|
|
578
|
-
//# sourceMappingURL=chunk-
|
|
579
|
+
//# sourceMappingURL=chunk-IOMIYI45.js.map
|
|
580
|
+
//# sourceMappingURL=chunk-IOMIYI45.js.map
|