fix(breaking): bordered elements should reserve layout space for border sides - #116
Conversation
Clay doesn't account for border widths in layout — borders were drawn as visual overlays, so any element with border width > padding had its content collapsed behind the border glyphs (or, for fit-height boxes, collapsed to a single row with top and bottom glyphs overlapping and children invisible). Fix: at pack time, compute effective padding per side as max(userPadding, borderWidth). Border glyphs draw in the same cells as before; only the Clay layout values change so the engine reserves those cells. Semantics of the max rule: - No explicit padding: border width becomes the effective padding; content lands inside the border, not behind it. - padding == borderWidth (prior workaround): max evaluates to the same value, no double-reservation; these elements render identically. - padding > borderWidth: extra padding provides breathing room inside the border, measured from the border edge inward. Callers who set padding == borderWidth as a workaround are unaffected. Downstream compensators (e.g. lgtm.shop Panel) will render identically until they drop the manual compensation on their next pin bump. Resolves Open Decision #4 in specs/renderer-spec.md.
commit: |
|
Size Increased — +0.1 KB 118.9 KB unpacked |
Border presence implies padding on that side equal to the border width. Callers who compensated by setting `padding == borderWidth` now receive double-reservation and must remove the workaround padding. `padding: 1` with `border: 1` → effective 2; `border: 1` alone → effective 1.
cowboyd
left a comment
There was a problem hiding this comment.
@natemoo-re It'll be sooo nice to bury this footgun in the ground once and for all.
I do have one thought: what would it look like to put this in C? It isn't a hard requirement, but I'm thinking towards the day when we get uplift from a TUI framework in a totally different language using TTY (similar to the way I'm hoping we get uplift by being a shared tool amongst JS web framework TUIS).
If it isn't feasible, no worries, just wanted to put that there as something to think about.
The additive effective-padding rule (userPadding + borderWidth per side) was applied in pack() on the TypeScript side, so the packed layout word carried a pre-computed value. It now happens in clayterm.c when the PROP_BORDER block decodes border widths: decl is zero-initialized and PROP_LAYOUT decodes first, so border-without-layout and layout-without-border both fall out naturally. The wire format's padding field now carries raw user padding; the renderer owns the reservation. Behavior is unchanged — all existing border tests pass as-is.
|
@natemoo-re a rare case where it seems to actually add clarity to have moved something into C! Its seems to me that we said the next thing was to dream up a bunch of use-cases to expose lurking dragons? |
|
@cowboyd yes, moving to C was a great call! yep, I think dogfooding will expose plenty more rough edges but this feels like one big footgun down! |
Cover everything landed since v0.8.0 that affects the published package; resolves to a 0.9.0 minor bump. - #116 border sides reserve layout space (minor, breaking within 0.x) - #103 hardware cursor via text() caret (minor) - #89 compile out Clay debug-tools, ~35% smaller wasm (patch) - #49 correct width for non-printable/surrogate codepoints, Unicode 17 (patch) Dependabot/CI bumps, the 2048 example, and docs-only commits get no changeset.
* feat: adopt changesets for versioning and releases Replace the tag-triggered publish workflow with a changesets-driven flow and seed the historical changelog so future releases build on it. - package.json becomes the version source of truth (0.8.0, private); the publishable artifact remains build/npm produced by dnt. - .changeset config uses @changesets/changelog-github against bombshell-dev/tty. - release.yml runs changesets/action on main: it opens a "Version Packages" PR from pending changesets, and on merge runs the publish command (npm run release). - tasks/release.ts wraps the Deno/dnt flow: build:npm, publish build/npm, then tag v<version> and push. It no-ops when the version is already on npm, because changesets/action runs the publish command on every changeset-less push to main. Tags keep the existing v<version> format for continuity with v0.0.0..v0.8.0. - CHANGELOG.md seeds all 14 releases in changelog-github format, with PRs attributed by the tag range their merge commit falls in and authors credited. - deno.json fmt excludes CHANGELOG.md/.changeset so deno fmt does not rewrap changesets output. * build(changeset): upgrade to changesets v3 Bump @changesets/cli ^2.29.7 -> ^3.0.1 and @changesets/changelog-github ^0.5.1 -> ^1.0.0, tracking the transitive @changesets/config@4.0.0 in the config schema URL. v3 stops versioning private packages by default. @bomb.sh/tty is `private: true` (root is a build source; publish happens from build/npm via tasks/release.ts), so opt back in with privatePackages.version. Leave privatePackages.tag off because release.ts owns git tagging. * chore(changeset): backfill changesets for unreleased main changes Cover everything landed since v0.8.0 that affects the published package; resolves to a 0.9.0 minor bump. - #116 border sides reserve layout space (minor, breaking within 0.x) - #103 hardware cursor via text() caret (minor) - #89 compile out Clay debug-tools, ~35% smaller wasm (patch) - #49 correct width for non-printable/surrogate codepoints, Unicode 17 (patch) Dependabot/CI bumps, the 2048 example, and docs-only commits get no changeset. * docs(changeset): retrofit CHANGELOG to Astro conventions Rework the backfilled history to match the changeset message style used for new entries: - Third-person present verbs throughout (Adds/Fixes/Renames), so future changeset-generated entries read consistently against the history. - Reframe entries toward user-visible impact, dropping wire-format and build-mechanism detail that consumers can't act on. - Drop non-user-facing entries (CI, tooling, tests, benchmarks, examples, internal docs); keep LICENSE since it ships in the package. - Hoist the breaking Op->directive/name->id rename to the top of its Patch section (bump stays patch; it already shipped). * build(changeset): refresh deno.lock for changesets v3 * fix(release): tag before publish and backfill missing tags * fix(release): use changesets action v2.1.1 input names * fix(release): disable action GH releases, drop hardcoded pkg name * fix(release): keep publish.yml filename to preserve npm trusted publisher * chore(changeset): tighten backfilled messages to match detail scale Trim to the level of detail each bump warrants: patches to a single sentence, the caret minor to one paragraph. The breaking border change keeps its two paragraphs plus migration diff. * fix(release): parse npm view --json for publish check Rely on structured --json output instead of plain-text stdout when checking whether the version is already on npm.
previously, bordered elements collapsed when no padding was set. Clay treats borders as visual overlays and does not account for them in layout, so text in the terminal was rendered behind the border glyph
now, tty intercepts borders at
pack()time and passes an effective padding to Clay that equalsuserPadding + borderWidthper sidebreaking change: border presence now implies a minimum padding equal to the border width. callers who compensated by setting
padding == borderWidthnow get double-reservation and should remove the workaround padding!