Files
matsandClaude Sonnet 5.5 ec4212c113 Device preview + responsive spacing in the Style panel
Style panel gets a real-viewport iframe preview (390/820/1280px) tied to the
breakpoint tab, reloaded after each edit, and spacing/gap controls per
breakpoint. The editor does not boot inside the preview frame. Mock and
tests mirror the new k6n layout template.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-01 11:02:20 +01:00

77 lines
5.8 KiB
Markdown
Raw Permalink 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** (done): `$defs/breakpoint` declares `gap`, `padding(Top|Bottom)`, `margin(Top|Bottom)`, `align`, `justify`; the k6n
layout template emits `k6n-t-gap-md` / `k6n-m-pt-sm` classes (tablet ≤900px, mobile ≤620px). Columns are resolved in the template into
`--k6n-cols` / `--k6n-cols-t` / `--k6n-cols-m` (defaults: tablet ≤2, mobile 1); cards/quotes/cases/projects/people read the same variables.
**Device preview**: the Style panel's *Show … preview* opens the page in an iframe at 390/820/1280px (real viewport, so media queries apply),
reloaded after each edit; the editor does not boot inside it (`window.name`). Only the k6n content root has this so far; the shared
`data/templates/layout.gohtml` still uses the old class scheme.
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.