Editful
DocsLayout and sizing

Build layout from four parts.

Box decorates. Stack orders. Spacer distributes. Text measures.

  • Block and inline
  • Logical padding
  • Fit-content

Add paint and padding

A box has zero or one child. It supplies the child's available width after padding.

360 × 0
canvas-ui-box.ts
import { TextFont, definePlugin, hexColor } from '@editful/canvas-sdk';

export const CANVAS_UI_BOX_KIND = 'docs:padded-box';

export default definePlugin({
  register(context) {
    const kind = context.kind(CANVAS_UI_BOX_KIND);
    const ui = kind.ui;

    kind.hit('rect');
    kind.view({
      root: ui.box({
        fill: hexColor('#161b22'),
        stroke: hexColor('#30363d'),
        strokeWidth: 1,
        cornerRadius: 8,
        padding: { inline: 24, block: 18 },
        child: ui.text('One child. Paint and padding belong to the box.', {
          font: {
            family: TextFont.Sans,
            size: 16,
            weight: 400,
            lineHeight: 1.35,
          },
          color: hexColor('#f0f6fc'),
        }),
      }),
      width: { mode: 'user', min: 280 },
      height: { mode: 'fit-content' },
    });
  },
});

Combine stack and spacer

The block stack owns vertical rhythm. The inline stack places a status pill at the far edge.

380 × 0
canvas-ui-layout.ts
import { TextFont, definePlugin, hexColor } from '@editful/canvas-sdk';

export const CANVAS_UI_LAYOUT_KIND = 'docs:metric-card';

export default definePlugin({
  register(context) {
    const kind = context.kind(CANVAS_UI_LAYOUT_KIND);
    const label = kind.field.string('label');
    const value = kind.field.string('value');
    const change = kind.field.string('change');
    const changeFill = kind.field.f64('change-fill');
    const ui = kind.ui;

    const smallText = (text: typeof label, color: number) => ui.text(text, {
      font: { family: TextFont.Sans, size: 12, weight: 400, lineHeight: 1.25 },
      color,
      wrap: false,
    });

    kind.hit('rect');
    kind.view({
      root: ui.box({
        fill: hexColor('#0d1117'),
        stroke: hexColor('#30363d'),
        strokeWidth: 1,
        cornerRadius: 8,
        padding: 20,
        child: ui.stack({
          axis: 'block',
          gap: 14,
          children: [
            ui.stack({
              axis: 'inline',
              align: 'center',
              children: [
                smallText(label, hexColor('#9198a1')),
                ui.spacer(),
                ui.box({
                  fill: changeFill,
                  cornerRadius: 'full',
                  padding: { inline: 10, block: 4 },
                  child: smallText(change, hexColor('#ffffff')),
                }),
              ],
            }),
            ui.text(value, {
              font: {
                family: TextFont.Sans,
                size: 32,
                weight: 400,
                lineHeight: 1.1,
              },
              color: hexColor('#f0f6fc'),
              wrap: false,
            }),
          ],
        }),
      }),
      width: { mode: 'user', min: 280 },
      height: { mode: 'fit-content' },
    });
  },
});

Give each primitive one job

box
One optional child, plus padding, fill, stroke, and corners.
stack · block
Places children top to bottom and stretches them to available width.
stack · inline
Measures children intrinsically and aligns them on the cross axis.
spacer
Consumes remaining inline width. Multiple spacers share it.

Width in. Height out.

Register a user-resizable width and fit-content height. The host recomputes height after width, field, or style changes.

Padding, gaps, and wrapped text contribute to the root height. Vertical resize handles stay disabled.