Files
cms-onpageeditor/docs/styling.md
T
matsandClaude Sonnet 5.5 1af9ec3172 Add on-page editor for the go-graph-cms
Injectable React editor that is side-loaded onto the statically rendered
page and edits through the CMS HTTP API (POST /api/edit).

- Shadow-root UI over server HTML; patches morphed in, undo/redo from
  server inverses (reuses @k6n/cms-editor EditClient/morph/markdown editor)
- Sign-in via go-auth device grant as the CMS client (aud matches), token
  refresh + 401 retry; live preview swapped in on load and on tab focus
- Schema-driven property form (arrays, maps, variants, $ref, validation)
- Every string property editable where it is rendered (value-bound inline
  fields and attribute chips), markdown WYSIWYG for marked fields
- Block palette + drag & drop with ghost drop targets, insertion slots,
  sort snapping between siblings (Alt to nest)
- x-style annotations: on-page padding/margin/gap handles, Style panel,
  wrap-in-layout; x-graph label/group for titles and grouping
- Dev workflow: Vite proxy to the live site (token injected server-side)
  or an offline in-memory mock CMS; jsdom test suite

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-31 22:34:48 +01:00

73 lines
5.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Styling on the page: flow, padding, margin, gap
## Decision
**Keep the typed token object on the node; tag its fields with `x-style`; let on-page editors key off the tag.**
Not a free-form `style` object on every node, and not per-node CSS: the k6n design system (`data/k6n/design.md`) already says
*blocks own no outer margin, spacing comes from the parent layout, spacing values are tokens (`none xs sm md lg xl`) and never raw
lengths*. The `layout` node is that primitive: `flow`, `columns`, `gap`, `padding(Top|Bottom)`, `margin(Top|Bottom)`, `align`,
`justify`, `tone`, `border`, `reveal`, and `responsive.{mobile,tablet,desktop}`. The template turns those into `.k6n-gap-md` /
`.k6n-p-lg` helper classes. So:
| Option | Verdict |
|---|---|
| Generic `style: {…}` on every node | **No.** Every template would have to consume it, it re-opens raw CSS in class attributes, and it contradicts "blocks own no margin". |
| `style-class` nodes (named CSS class, linked by edge) | **Keep, but not for on-page tweaks.** Shared by design (blast radius = every layout that links it) and emitted into `<style>` in the head, which a per-node patch cannot refresh. Good for reusable surfaces (`tone`, `border`); edit them in the dialog. |
| Per-type editor code ("if layout then …") | **No.** Every new block type would need editor changes. |
| **`x-style` on schema properties** | **Yes.** The schema already is the source of truth (`x-graph`, `x-editor`); one `set` op per change (undo/redo, coalescing, 409 all free); any type opts in by annotating a field, zero editor code. |
Blocks that have no style fields (hero, text, …) get spacing by being **wrapped in a layout** — one atomic batch
(`insert` layout + `move` block into it), undone in one step. The wrapper type is the schema tagged `x-style.wrapper`.
## `x-style` (schema contract)
On a property:
```json
"paddingTop": {
"type": "string", "enum": ["none","xs","sm","md","lg","xl"],
"x-style": { "kind": "spacing", "box": "padding", "side": "top", "overrides": "padding", "token": "--k6n-sp-{value}" }
}
```
| key | meaning |
|---|---|
| `kind` | what the field *is*: `spacing` `gap` `flow` `columns` `align` `justify` `tone` `border` `reveal` `choice` |
| `box`, `side` | spacing only: `padding`/`margin` × `all top bottom left right x y` |
| `overrides` | sibling this field wins over (`paddingTop` over `padding`); drives the "inherited" display and which field a handle edits |
| `token` | CSS custom property template that resolves an enum value to a length. The editor measures real pixels from the page's own CSS (so fluid `clamp()` tokens are right) and snaps drags to the nearest token |
On the schema root: `"x-style": { "wrapper": true }` marks the type blocks can be wrapped in.
`responsive.<bp>.<field>` (the `$defs/breakpoint` properties) are matched to base fields by name: those fields get a breakpoint tab
and write to `responsive.<bp>.<field>`. Today only `flow` and `columns` are declared there - the template has classes for nothing else.
**Fallback.** For schemas without `x-style` (e.g. production before the annotated schemas deploy) the editor infers the same
descriptors from the property name + enum (`gap`, `padding|margin(Top|Bottom|Left|Right|X|Y)` with a `none…xl` scale, `flow`,
`columns`, `align`, `justify`, `tone`, `border`, `reveal`) and says so in the panel. The wrapper tag is never guessed.
Raw-length fields (`grid.gap: "1rem"`) are intentionally not inferred.
Annotated in `data/k6n/schemas`: `layout` (all fields, `$defs/breakpoint`, wrapper), `consulting-band` (`tone`, `align`, `width`),
and every block with a numeric `columns`.
## What the editor does with it
- **Handles on the page** (`src/style/BoxHandles.tsx`): padding (green), margin (orange) and gap (purple) bands from computed style,
each with a grip. Drag → live inline preview → snaps to a token on release → one coalesced `set`. Only sides the schema can express
get a grip (`left/right` fall back to the `all` field and say "(all sides)").
- **Style dialog** (`src/style/StylePanel.tsx`, toolbar ◐): flow / align / justify chips, columns stepper, a margin ⟶ padding box
diagram with one select per expressible side (override fields show what they inherit), gap chips with px, tone / border / reveal,
breakpoint tabs. Registered with `match: (info) => describeStyle(info.schema).length > 0`: **by schema, not by type**.
- **Wrap in layout** (toolbar ▣) for blocks without style fields.
- Pure, tested pieces: `model.ts` (descriptors, effective values, patches), `tokens.ts` (measure/snap), `handles.ts` (geometry, drag maths).
## Open points / suggestions for the CMS side
1. **Responsive spacing** is not possible today: no `gap/padding/margin` classes per breakpoint. If wanted: add `x-style` spacing
fields to `$defs/breakpoint` and emit e.g. `k6n-gap-md@t` classes from `layout.gohtml`; the editor picks them up unchanged.
2. **Left/right padding and margin** have no fields (`paddingTop/Bottom` only), so those grips edit the all-sides field. Adding
`paddingX/marginX` (`side: "x"`) gives them their own handles with no editor change.
3. `InsertOp` in `@k6n/cms-editor` lacks the optional `id` the server accepts (the editor casts for the wrap batch).
4. A layout linked to a `style-class` inherits `tone`/`border` from it; the panel shows the layout's own value only.