Files
cms-onpageeditor/docs/styling.md
T
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

5.8 KiB
Raw Blame History

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:

"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.