Build layout from four parts.
Box decorates. Stack orders. Spacer distributes. Text measures.
Add paint and padding
A box has zero or one child. It supplies the child's available width after padding.
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.
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.