Composition / foldkit-mixins-builder

Styling

BuilderSlots names every element; attach a Style as to any SlotView:

ts
import { Style } from 'foldkit-mixins'
import { BuilderSlots, BuilderView } from 'foldkit-mixins-builder'

const PageEditing = BuilderView.define(PageBuilder).pipe(
  Style.attach(
    Style.forSlots(BuilderSlots)({
      root: Style.class('builder'),
      row: Style.class('builder-row'),
      canvas: Style.class('builder-canvas'),
    }),
  ),
)

define draws the panels in four regions, so a layout places four things, and a new panel lands in its region without moving the rest:

root                 the container `builder`
├── panelTabs        a narrow editor's tabs (see below)
├── regions
│   ├── start        palette, layers
│   ├── bar          toolbar, crumbs, viewports, preview
│   ├── stage        alert, canvas
│   └── end          inspector
└── live

root is a container (container: builder / inline-size, the Builder's default Style), so the regions may be laid out by the editor's own width, @container builder (max-width: 64rem), rather than the window's: an editor in a narrow column stacks in a wide window. The example lays regions out as a grid of start, bar over stage, and end. A layout of your own (assemble) draws none of these. An inline-size container takes no width from what it holds, so give the editor one: as a flex item that does not grow, an auto grid column, inline-block or fit-content, it is 0 wide.

A node's props are drawn by its Block's settings form, a foldkit-mixins-form view the Builder makes. Give its Styles to define (or parts), and they are attached to every Block's settings form:

ts
import { FieldSlots, FormSlots } from 'foldkit-mixins-form'

const PageEditing = BuilderView.define(PageBuilder, {
  settings: {
    field: Style.attach(Style.forSlots(FieldSlots)({ label: Style.class('setting-label') })),
    form: Style.attach(Style.forSlots(FormSlots)({ root: Style.class('settings') })),
  },
})

A control kind of the application's own (Input.kind) is drawn by the renderers the application gives, the same ones its form views take. Each Block's settings form has Messages of its own, so they are given as a function of the Message, as Cms.controlRenderers is: BuilderView.define(PageBuilder, { settings: { renderers: Cms.controlRenderers } }). Without one, the inspector says which renderer is missing.

The page on the canvas is the site's own markup, so it is styled by the site's CSS, and the editor marks it by drawing over it: the selectionBox Slot sits over the selected node, with its Block's label in selectionLabel, and the hoverBox Slot over the node under the pointer. They are placed from where the nodes are measured to be (Measure, from foldkit-primitives/dom), scroll with the page, and take no pointer; the Builder places them, and a Style says how they look:

ts
Style.forSlots(BuilderSlots)({
  selectionBox: Style.self({ outline: '2px solid Highlight', outlineOffset: '3px' }),
  selectionLabel: Style.self({ background: 'Highlight', color: 'HighlightText', fontSize: '0.75rem' }),
  hoverBox: Style.self({ outline: '1px dashed GrayText', outlineOffset: '3px' }),
})

A node's wrapper still carries data-composition-mark (selected, or hovered; a node that is both is selected), which is what the boxes are measured from, and data-composition-drop (before, inside or after) on the node a drop would land at. A wrapper is display: contents and draws nothing, so style the element inside it. The layer rows carry data-builder-drop and data-builder-dragging the same way.

css
[data-composition-drop='before'] > * { box-shadow: 0 -3px 0 Highlight; }
[data-composition-drop='after'] > * { box-shadow: 0 3px 0 Highlight; }
[data-composition-drop='inside'] > * { outline: 2px dashed Highlight; }
[data-builder-dragging] { opacity: 0.5; }