···5353 return o;
5454}
55555656+function sideWidth(side: BorderSide | undefined): number {
5757+ return typeof side === "number" ? side : side?.width ?? 0;
5858+}
5959+6060+function sideFg(side: BorderSide | undefined, shared: number): number {
6161+ let color = typeof side === "object" && side.color !== undefined
6262+ ? side.color
6363+ : shared;
6464+ return color & 0x00FFFFFF;
6565+}
6666+6767+function sideBg(
6868+ side: BorderSide | undefined,
6969+ shared: number | undefined,
7070+): number {
7171+ let bg = typeof side === "object" && side.bg !== undefined ? side.bg : shared;
7272+ // ATTR_DEFAULT sentinel (bit 31 set) means "keep the existing cell bg"
7373+ return bg === undefined ? 0x80000000 : bg & 0x00FFFFFF;
7474+}
7575+5676function packString(
5777 view: DataView,
5878 bytes: Uint8Array,
···162182163183 if (op.border) {
164184 let b = op.border;
165165- view.setUint32(o, b.color, true);
166166- o += 4;
167167-168168- // ATTR_DEFAULT sentinel (bit 31 set) means "use terminal default bg"
169169- let bg = b.bg === undefined ? 0x80000000 : b.bg & 0x00FFFFFF;
170170- view.setUint32(o, bg, true);
171171- o += 4;
172172-173185 view.setUint32(
174186 o,
175175- (b.left ?? 0) | ((b.right ?? 0) << 8) | ((b.top ?? 0) << 16) |
176176- ((b.bottom ?? 0) << 24),
187187+ sideWidth(b.left) | (sideWidth(b.right) << 8) |
188188+ (sideWidth(b.top) << 16) | (sideWidth(b.bottom) << 24),
177189 true,
178190 );
179191 o += 4;
192192+193193+ // Resolved per-side attributes (CSS-like fallback expansion done
194194+ // here, not in C): fg/bg word pairs in top, right, bottom, left
195195+ // order. The C renderer consumes these as explicit values.
196196+ for (let side of [b.top, b.right, b.bottom, b.left]) {
197197+ view.setUint32(o, sideFg(side, b.color), true);
198198+ o += 4;
199199+ view.setUint32(o, sideBg(side, b.bg), true);
200200+ o += 4;
201201+ }
180202 }
181203182204 if (op.clip) {
···300322 directive: typeof OP_CLOSE_ELEMENT;
301323}
302324325325+export type BorderSide =
326326+ | number
327327+ | {
328328+ width: number;
329329+ color?: number;
330330+ bg?: number;
331331+ };
332332+303333export interface OpenElement {
304334 directive: typeof OP_OPEN_ELEMENT;
305335 id: string;
···317347 border?: {
318348 color: number;
319349 bg?: number;
320320- left?: number;
321321- right?: number;
322322- top?: number;
323323- bottom?: number;
350350+ left?: BorderSide;
351351+ right?: BorderSide;
352352+ top?: BorderSide;
353353+ bottom?: BorderSide;
324354 };
325355 clip?: { horizontal?: boolean; vertical?: boolean };
326356 floating?: {
···453483 if (op.layout) n += 6 * 4 + 4 + 4 + 4; // 2 axes (3 words each) + pad + gap + align
454484 if (op.bg !== undefined) n += 4;
455485 if (op.cornerRadius) n += 4;
456456- if (op.border) n += 12;
486486+ if (op.border) n += 36; // widths word + 4 sides × (fg + bg)
457487 if (op.clip) n += 4;
458488 // x, y, expand width/height, parent, attach/pointer, clip/z
459489 if (op.floating) n += 7 * 4;
+46-8
specs/renderer-spec.md
···644644 padding (per-side), alignment (`alignX`: `"left"` | `"center"` | `"right"`;
645645 `alignY`: `"top"` | `"center"` | `"bottom"`, defaulting to left/top when
646646 omitted), direction (top-to-bottom or left-to-right), and gap
647647-- **`border`** — per-side border widths, border color, and border background
648648- color
647647+- **`border`** — per-side border configuration. Each side field (`top`, `right`,
648648+ `bottom`, `left`) accepts either a scalar width or a structured object
649649+ `{ width, color?, bg? }`. The shared `color` field is required and is the
650650+ fallback foreground for every side; the optional shared `bg` field is the
651651+ fallback border-cell background for every side
649652- **`cornerRadius`** — per-corner radius values, producing rounded box-drawing
650653 characters
651654- **`clip`** — clip region configuration for scroll containers
···709712These property groups represent the current implementation surface. New groups
710713and fields have been added incrementally and more may follow.
711714712712-**Border background.** When `border.bg` is provided, the renderer MUST apply
713713-that background color to all cells occupied by border glyphs (corners,
714714-horizontal edges, and vertical edges). When `border.bg` is omitted, border
715715-rendering MUST NOT override the background already present in each border cell;
716716-element backgrounds established by `open({ bg })` remain in effect, and the
717717-terminal default remains in effect where no element background applies.
715715+**Border sides.** Each border side is declared independently as either a scalar
716716+width (`top: 1`) or a structured object (`top: { width: 1, color?, bg? }`). The
717717+two forms are equivalent when the object form provides only `width`. A side is
718718+enabled when its resolved width is greater than zero; an omitted side or a side
719719+with width `0` MUST NOT be drawn. Scalar side declarations MUST keep their
720720+pre-existing behavior.
721721+722722+**Border side colors (fallback resolution).** Side attributes resolve in a
723723+CSS-like shorthand/longhand fashion before rendering:
724724+725725+- A structured side with `color` MUST render with that foreground color. A
726726+ scalar side, or a structured side that omits `color`, MUST fall back to the
727727+ shared `border.color`. The shared `color` remains required.
728728+- A structured side with `bg` MUST render border cells of that side with that
729729+ background color. A scalar side, or a structured side that omits `bg`, MUST
730730+ fall back to the shared `border.bg` when it is provided.
731731+- When neither the side nor the shared border provides `bg`, border rendering
732732+ MUST NOT override the background already present in each border cell of that
733733+ side; element backgrounds established by `open({ bg })` remain in effect, and
734734+ the terminal default remains in effect where no element background applies.
735735+736736+Fallback resolution is performed on the TypeScript side before the frame is
737737+transferred; the WASM renderer consumes explicit per-side attributes and does
738738+not implement the public fallback rules.
739739+740740+**Independent sides and corners.** Each enabled side renders as a straight edge
741741+(`─` for horizontal sides, `│` for vertical sides). A corner glyph MUST be
742742+rendered only when both adjacent sides for that corner are enabled; when either
743743+adjacent side is absent, the present side continues straight through the
744744+endpoint with no corner glyph. A left-only border is therefore a plain vertical
745745+line, and a top-plus-bottom border is two plain horizontal rules.
746746+747747+**Corner styling approximation.** A terminal cell carries a single glyph,
748748+foreground, and background, so CSS-style diagonally split corners cannot be
749749+represented. When corners are rendered: top corners (`┌`, `┐`, and their rounded
750750+variants) MUST use the resolved attributes of the `top` side, and bottom corners
751751+(`└`, `┘`, and their rounded variants) MUST use the resolved attributes of the
752752+`bottom` side. Left and right side attributes apply to vertical edge cells
753753+excluding joined corner cells. Per-side attributes affect only the styling of
754754+corner cells; corner glyph shape selection (including rounded corners via
755755+`cornerRadius`) is unchanged.
718756719757**Border width and layout interaction.** In the underlying layout engine (Clay),
720758border configuration does not affect layout computation. This is Clay's intended
+31-17
src/clayterm.c
···303303static void render_border(struct Clayterm *ct, int x0, int y0, int x1, int y1,
304304 Clay_RenderCommand *cmd) {
305305 Clay_BorderRenderData *b = &cmd->renderData.border;
306306- uint32_t fg = color(b->color);
307307- /* userData is currently exclusively the packed border-bg word. */
308308- uint32_t bg = (uint32_t)(uintptr_t)cmd->userData;
306306+ /* userData points at eight words in the command buffer carrying resolved
307307+ * per-side attributes as fg/bg pairs in top, right, bottom, left order.
308308+ * Fallback resolution (shared color/bg vs side overrides) happens on the
309309+ * TypeScript side; this renderer consumes explicit values only. The
310310+ * command buffer outlives the render pass within reduce(). */
311311+ const uint32_t *s = (const uint32_t *)cmd->userData;
312312+ uint32_t deffg = color(b->color);
313313+ uint32_t top_fg = s ? s[0] : deffg, top_bg = s ? s[1] : ATTR_DEFAULT;
314314+ uint32_t right_fg = s ? s[2] : deffg, right_bg = s ? s[3] : ATTR_DEFAULT;
315315+ uint32_t bot_fg = s ? s[4] : deffg, bot_bg = s ? s[5] : ATTR_DEFAULT;
316316+ uint32_t left_fg = s ? s[6] : deffg, left_bg = s ? s[7] : ATTR_DEFAULT;
309317 int top = b->width.top > 0;
310318 int bot = b->width.bottom > 0;
311319 int left = b->width.left > 0;
312320 int right = b->width.right > 0;
313321314314- /* corners — rounded when corner radius > 0 */
322322+ /* corners — rounded when corner radius > 0. Drawn only when both adjacent
323323+ * sides are enabled; a terminal cell holds a single fg/bg, so top corners
324324+ * take the top side attributes and bottom corners take the bottom side
325325+ * attributes (deterministic approximation of CSS split corners). */
315326 uint32_t tl = b->cornerRadius.topLeft > 0 ? 0x256d : 0x250c;
316327 uint32_t tr = b->cornerRadius.topRight > 0 ? 0x256e : 0x2510;
317328 uint32_t bl = b->cornerRadius.bottomLeft > 0 ? 0x2570 : 0x2514;
318329 uint32_t br = b->cornerRadius.bottomRight > 0 ? 0x256f : 0x2518;
319330320331 if (top && left)
321321- setcell(ct, x0, y0, tl, fg, bg);
332332+ setcell(ct, x0, y0, tl, top_fg, top_bg);
322333 if (top && right)
323323- setcell(ct, x1 - 1, y0, tr, fg, bg);
334334+ setcell(ct, x1 - 1, y0, tr, top_fg, top_bg);
324335 if (bot && left)
325325- setcell(ct, x0, y1 - 1, bl, fg, bg);
336336+ setcell(ct, x0, y1 - 1, bl, bot_fg, bot_bg);
326337 if (bot && right)
327327- setcell(ct, x1 - 1, y1 - 1, br, fg, bg);
338338+ setcell(ct, x1 - 1, y1 - 1, br, bot_fg, bot_bg);
328339329340 /* horizontal edges */
330341 if (top)
331342 for (int x = x0 + left; x < x1 - right; x++)
332332- setcell(ct, x, y0, 0x2500, fg, bg);
343343+ setcell(ct, x, y0, 0x2500, top_fg, top_bg);
333344 if (bot)
334345 for (int x = x0 + left; x < x1 - right; x++)
335335- setcell(ct, x, y1 - 1, 0x2500, fg, bg);
346346+ setcell(ct, x, y1 - 1, 0x2500, bot_fg, bot_bg);
336347337337- /* vertical edges */
348348+ /* vertical edges — excluding joined corner cells owned by top/bottom */
338349 if (left)
339350 for (int y = y0 + top; y < y1 - bot; y++)
340340- setcell(ct, x0, y, 0x2502, fg, bg);
351351+ setcell(ct, x0, y, 0x2502, left_fg, left_bg);
341352 if (right)
342353 for (int y = y0 + top; y < y1 - bot; y++)
343343- setcell(ct, x1 - 1, y, 0x2502, fg, bg);
354354+ setcell(ct, x1 - 1, y, 0x2502, right_fg, right_bg);
344355}
345356346357/* ── Command buffer helpers ───────────────────────────────────────── */
···533544 }
534545535546 if (mask & PROP_BORDER) {
536536- decl.border.color = unpack_color(rd(buf, len, &i));
537537-538538- decl.userData = (void *)(uintptr_t)rd(buf, len, &i);
539539-540547 uint32_t bw = rd(buf, len, &i);
541548 decl.border.width.left = bw & 0xff;
542549 decl.border.width.right = (bw >> 8) & 0xff;
543550 decl.border.width.top = (bw >> 16) & 0xff;
544551 decl.border.width.bottom = (bw >> 24) & 0xff;
552552+553553+ /* Resolved per-side fg/bg attribute words (top, right, bottom,
554554+ * left). Routed to render_border via userData; the command buffer
555555+ * remains valid for the whole render pass. */
556556+ if (i + 8 <= len)
557557+ decl.userData = (void *)&buf[i];
558558+ i += 8;
545559 }
546560547561 if (mask & PROP_CLIP) {