@kolisachint/hoocode-tui 0.5.37 → 0.5.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,8 +31,10 @@ export declare class Box implements Component {
31
31
  * A terminal has no sub-pixel offsets, so the shadow is one extra row of
32
32
  * `▀` — an upper half-block, which paints solid color across the top half of
33
33
  * its cells and so hugs the box's bottom edge — indented one column to give
34
- * the offset. There is no matching right-hand column: these boxes render at
35
- * the full terminal width, and a column past the last cell has nowhere to go.
34
+ * the offset. An inset box gets a matching column of `▌` down its right edge
35
+ * and the bottom run reaches under it, so the two meet at the corner; with
36
+ * no inset the band already owns the last cell, so the bottom edge is all
37
+ * there is room for.
36
38
  *
37
39
  * Passing no function draws no shadow, which is the behavior of every theme
38
40
  * that does not opt in.
@@ -1 +1 @@
1
- {"version":3,"file":"box.d.ts","sourceRoot":"","sources":["../../src/components/box.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAgC3C;;GAEG;AACH,qBAAa,GAAI,YAAW,SAAS;IACpC,QAAQ,EAAE,SAAS,EAAE,CAAM;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,IAAI,CAAC,CAA2B;IACxC,OAAO,CAAC,QAAQ,CAAC,CAA2B;IAC5C,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,OAAO,CAAS;IAGxB,OAAO,CAAC,KAAK,CAAC,CAAc;IAE5B,YAAY,QAAQ,SAAI,EAAE,QAAQ,SAAI,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,EAItE;IAED;;;;;;OAMG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAIlC;IAED,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAGnC;IAED,WAAW,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAMtC;IAED,KAAK,IAAI,IAAI,CAGZ;IAED,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAG7C;IAED;;;;;;;;;;;;OAYG;IACH,WAAW,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAGrD;IAED;;;;;;;OAOG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAI5B;IAED;;;;;;;OAOG;IACH,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAIjC;IAED,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,UAAU;IAmBlB,UAAU,IAAI,IAAI,CAKjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAyE9B;IAED,OAAO,CAAC,OAAO;CAUf","sourcesContent":["import type { Component } from \"../tui.js\";\nimport { applyBackgroundToLine, visibleWidth } from \"../utils.js\";\n\ntype RenderCache = {\n\tchildLines: string[];\n\twidth: number;\n\tbgSample: string | undefined;\n\tshadowSample: string | undefined;\n\tinset: number;\n\tcutEdge: boolean;\n\tlines: string[];\n};\n\n/**\n * Roughly one row in this many takes the cut.\n *\n * A coin flip per row is not a cut edge, it is a sawtooth: the eye reads\n * alternating columns as a pattern, which is the opposite of hand-made. Scissors\n * leave a mostly straight line with the occasional nick, so the deviation has to\n * be rare enough to read as an accident.\n */\nconst CUT_EVERY = 5;\n\n/** djb2. Small, stable, and good enough to decide one column of a paper edge. */\nfunction hashString(value: string): number {\n\tlet hash = 5381;\n\tfor (let i = 0; i < value.length; i++) {\n\t\thash = ((hash << 5) + hash + value.charCodeAt(i)) >>> 0;\n\t}\n\treturn hash;\n}\n\n/**\n * Box component - a container that applies padding and background to all children\n */\nexport class Box implements Component {\n\tchildren: Component[] = [];\n\tprivate paddingX: number;\n\tprivate paddingY: number;\n\tprivate bgFn?: (text: string) => string;\n\tprivate shadowFn?: (text: string) => string;\n\tprivate inset = 0;\n\tprivate cutEdge = false;\n\n\t// Cache for rendered output\n\tprivate cache?: RenderCache;\n\n\tconstructor(paddingX = 1, paddingY = 1, bgFn?: (text: string) => string) {\n\t\tthis.paddingX = paddingX;\n\t\tthis.paddingY = paddingY;\n\t\tthis.bgFn = bgFn;\n\t}\n\n\t/**\n\t * Change the horizontal padding after construction.\n\t *\n\t * For a box whose padding exists to carry a background band: with no band to\n\t * draw, the padding is just an indent that pushes the content out of line\n\t * with everything around it.\n\t */\n\tsetPaddingX(paddingX: number): void {\n\t\tif (this.paddingX === paddingX) return;\n\t\tthis.paddingX = paddingX;\n\t\tthis.invalidateCache();\n\t}\n\n\taddChild(component: Component): void {\n\t\tthis.children.push(component);\n\t\tthis.invalidateCache();\n\t}\n\n\tremoveChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index !== -1) {\n\t\t\tthis.children.splice(index, 1);\n\t\t\tthis.invalidateCache();\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.children = [];\n\t\tthis.invalidateCache();\n\t}\n\n\tsetBgFn(bgFn?: (text: string) => string): void {\n\t\tthis.bgFn = bgFn;\n\t\t// Don't invalidate here - we'll detect bgFn changes by sampling output\n\t}\n\n\t/**\n\t * Draw an offset band under the box, so the block reads as a sheet laid on\n\t * the page rather than a color printed into it.\n\t *\n\t * A terminal has no sub-pixel offsets, so the shadow is one extra row of\n\t * `▀` — an upper half-block, which paints solid color across the top half of\n\t * its cells and so hugs the box's bottom edge — indented one column to give\n\t * the offset. There is no matching right-hand column: these boxes render at\n\t * the full terminal width, and a column past the last cell has nowhere to go.\n\t *\n\t * Passing no function draws no shadow, which is the behavior of every theme\n\t * that does not opt in.\n\t */\n\tsetShadowFn(shadowFn?: (text: string) => string): void {\n\t\tthis.shadowFn = shadowFn;\n\t\tthis.invalidateCache();\n\t}\n\n\t/**\n\t * Hold the band back from the right margin, leaving a gutter of page.\n\t *\n\t * A block that runs the full width of the terminal has no right edge to\n\t * show: it is a band of colour between two screen edges, not a sheet on a\n\t * page. Reserving a few columns gives it one, which is what makes both the\n\t * shadow's right-hand column and a cut edge possible at all.\n\t */\n\tsetInset(inset: number): void {\n\t\tif (this.inset === inset) return;\n\t\tthis.inset = Math.max(0, inset);\n\t\tthis.invalidateCache();\n\t}\n\n\t/**\n\t * Cut the right edge by hand rather than ruling it.\n\t *\n\t * The jitter is one column at most and it only ever eats padding, never\n\t * content, so a cut edge cannot cost a character. It is derived from the row\n\t * index and the block's own content, which keeps it identical between frames\n\t * — an edge that reshuffled on every render would read as noise, not paper.\n\t */\n\tsetCutEdge(cutEdge: boolean): void {\n\t\tif (this.cutEdge === cutEdge) return;\n\t\tthis.cutEdge = cutEdge;\n\t\tthis.invalidateCache();\n\t}\n\n\tprivate invalidateCache(): void {\n\t\tthis.cache = undefined;\n\t}\n\n\tprivate matchCache(\n\t\twidth: number,\n\t\tchildLines: string[],\n\t\tbgSample: string | undefined,\n\t\tshadowSample: string | undefined,\n\t): boolean {\n\t\tconst cache = this.cache;\n\t\treturn (\n\t\t\t!!cache &&\n\t\t\tcache.width === width &&\n\t\t\tcache.bgSample === bgSample &&\n\t\t\tcache.shadowSample === shadowSample &&\n\t\t\tcache.inset === this.inset &&\n\t\t\tcache.cutEdge === this.cutEdge &&\n\t\t\tcache.childLines.length === childLines.length &&\n\t\t\tcache.childLines.every((line, i) => line === childLines[i])\n\t\t);\n\t}\n\n\tinvalidate(): void {\n\t\tthis.invalidateCache();\n\t\tfor (const child of this.children) {\n\t\t\tchild.invalidate?.();\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.children.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// The band stops short of the right margin when inset, leaving a gutter of\n\t\t// page for the sheet's own edge and the shadow that follows it.\n\t\tconst bandWidth = Math.max(1, width - this.inset);\n\t\tconst contentWidth = Math.max(1, bandWidth - this.paddingX * 2);\n\t\tconst leftPad = \" \".repeat(this.paddingX);\n\n\t\t// Render all children\n\t\tconst childLines: string[] = [];\n\t\tfor (const child of this.children) {\n\t\t\tconst lines = child.render(contentWidth);\n\t\t\tfor (const line of lines) {\n\t\t\t\tchildLines.push(leftPad + line);\n\t\t\t}\n\t\t}\n\n\t\tif (childLines.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// Check if bgFn output changed by sampling\n\t\tconst bgSample = this.bgFn ? this.bgFn(\"test\") : undefined;\n\t\tconst shadowSample = this.shadowFn ? this.shadowFn(\"test\") : undefined;\n\n\t\t// Check cache validity\n\t\tif (this.matchCache(width, childLines, bgSample, shadowSample)) {\n\t\t\treturn this.cache!.lines;\n\t\t}\n\n\t\t// Apply background and padding\n\t\tconst rows: string[] = [];\n\t\tfor (let i = 0; i < this.paddingY; i++) rows.push(\"\");\n\t\trows.push(...childLines);\n\t\tfor (let i = 0; i < this.paddingY; i++) rows.push(\"\");\n\n\t\t// A cut edge needs a seed that is stable for this block but different\n\t\t// from its neighbours, or every sheet on screen is cut identically.\n\t\tconst seed = this.cutEdge ? hashString(childLines.join(\"\\n\")) : 0;\n\t\tconst result: string[] = [];\n\t\trows.forEach((line, index) => {\n\t\t\t// Never let the cut reach into the content: the jitter comes out of\n\t\t\t// the padding the row was going to draw anyway.\n\t\t\tconst slack = Math.max(0, bandWidth - visibleWidth(line));\n\t\t\tconst cut = this.cutEdge && slack > 0 && hashString(`${seed}:${index}`) % CUT_EVERY === 0 ? 1 : 0;\n\t\t\tconst rowWidth = bandWidth - cut;\n\t\t\tconst band = this.applyBg(line, rowWidth);\n\t\t\t// The shadow follows the edge it is cast by, so it jitters with it.\n\t\t\t// The first row has none: the offset is down *and* right.\n\t\t\tconst column = this.shadowFn && this.inset > 0 && index > 0 ? this.shadowFn(\"▌\") : \"\";\n\t\t\tresult.push(band + column);\n\t\t});\n\n\t\t// The shadow's bottom run, offset one column right of the band.\n\t\tif (this.shadowFn && bandWidth > 1) {\n\t\t\tresult.push(` ${this.shadowFn(\"▀\".repeat(bandWidth - 1))}`);\n\t\t}\n\n\t\t// Update cache\n\t\tthis.cache = {\n\t\t\tchildLines,\n\t\t\twidth,\n\t\t\tbgSample,\n\t\t\tshadowSample,\n\t\t\tinset: this.inset,\n\t\t\tcutEdge: this.cutEdge,\n\t\t\tlines: result,\n\t\t};\n\n\t\treturn result;\n\t}\n\n\tprivate applyBg(line: string, width: number): string {\n\t\tconst visLen = visibleWidth(line);\n\t\tconst padNeeded = Math.max(0, width - visLen);\n\t\tconst padded = line + \" \".repeat(padNeeded);\n\n\t\tif (this.bgFn) {\n\t\t\treturn applyBackgroundToLine(padded, width, this.bgFn);\n\t\t}\n\t\treturn padded;\n\t}\n}\n"]}
1
+ {"version":3,"file":"box.d.ts","sourceRoot":"","sources":["../../src/components/box.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAgC3C;;GAEG;AACH,qBAAa,GAAI,YAAW,SAAS;IACpC,QAAQ,EAAE,SAAS,EAAE,CAAM;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,IAAI,CAAC,CAA2B;IACxC,OAAO,CAAC,QAAQ,CAAC,CAA2B;IAC5C,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,OAAO,CAAS;IAGxB,OAAO,CAAC,KAAK,CAAC,CAAc;IAE5B,YAAY,QAAQ,SAAI,EAAE,QAAQ,SAAI,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,EAItE;IAED;;;;;;OAMG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAIlC;IAED,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAGnC;IAED,WAAW,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAMtC;IAED,KAAK,IAAI,IAAI,CAGZ;IAED,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAG7C;IAED;;;;;;;;;;;;;;OAcG;IACH,WAAW,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAGrD;IAED;;;;;;;OAOG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAI5B;IAED;;;;;;;OAOG;IACH,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAIjC;IAED,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,UAAU;IAmBlB,UAAU,IAAI,IAAI,CAKjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAqF9B;IAED,OAAO,CAAC,OAAO;CAUf","sourcesContent":["import type { Component } from \"../tui.js\";\nimport { applyBackgroundToLine, visibleWidth } from \"../utils.js\";\n\ntype RenderCache = {\n\tchildLines: string[];\n\twidth: number;\n\tbgSample: string | undefined;\n\tshadowSample: string | undefined;\n\tinset: number;\n\tcutEdge: boolean;\n\tlines: string[];\n};\n\n/**\n * Roughly one row in this many takes the cut.\n *\n * A coin flip per row is not a cut edge, it is a sawtooth: the eye reads\n * alternating columns as a pattern, which is the opposite of hand-made. Scissors\n * leave a mostly straight line with the occasional nick, so the deviation has to\n * be rare enough to read as an accident.\n */\nconst CUT_EVERY = 5;\n\n/** djb2. Small, stable, and good enough to decide one column of a paper edge. */\nfunction hashString(value: string): number {\n\tlet hash = 5381;\n\tfor (let i = 0; i < value.length; i++) {\n\t\thash = ((hash << 5) + hash + value.charCodeAt(i)) >>> 0;\n\t}\n\treturn hash;\n}\n\n/**\n * Box component - a container that applies padding and background to all children\n */\nexport class Box implements Component {\n\tchildren: Component[] = [];\n\tprivate paddingX: number;\n\tprivate paddingY: number;\n\tprivate bgFn?: (text: string) => string;\n\tprivate shadowFn?: (text: string) => string;\n\tprivate inset = 0;\n\tprivate cutEdge = false;\n\n\t// Cache for rendered output\n\tprivate cache?: RenderCache;\n\n\tconstructor(paddingX = 1, paddingY = 1, bgFn?: (text: string) => string) {\n\t\tthis.paddingX = paddingX;\n\t\tthis.paddingY = paddingY;\n\t\tthis.bgFn = bgFn;\n\t}\n\n\t/**\n\t * Change the horizontal padding after construction.\n\t *\n\t * For a box whose padding exists to carry a background band: with no band to\n\t * draw, the padding is just an indent that pushes the content out of line\n\t * with everything around it.\n\t */\n\tsetPaddingX(paddingX: number): void {\n\t\tif (this.paddingX === paddingX) return;\n\t\tthis.paddingX = paddingX;\n\t\tthis.invalidateCache();\n\t}\n\n\taddChild(component: Component): void {\n\t\tthis.children.push(component);\n\t\tthis.invalidateCache();\n\t}\n\n\tremoveChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index !== -1) {\n\t\t\tthis.children.splice(index, 1);\n\t\t\tthis.invalidateCache();\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.children = [];\n\t\tthis.invalidateCache();\n\t}\n\n\tsetBgFn(bgFn?: (text: string) => string): void {\n\t\tthis.bgFn = bgFn;\n\t\t// Don't invalidate here - we'll detect bgFn changes by sampling output\n\t}\n\n\t/**\n\t * Draw an offset band under the box, so the block reads as a sheet laid on\n\t * the page rather than a color printed into it.\n\t *\n\t * A terminal has no sub-pixel offsets, so the shadow is one extra row of\n\t * `▀` — an upper half-block, which paints solid color across the top half of\n\t * its cells and so hugs the box's bottom edge — indented one column to give\n\t * the offset. An inset box gets a matching column of `▌` down its right edge\n\t * and the bottom run reaches under it, so the two meet at the corner; with\n\t * no inset the band already owns the last cell, so the bottom edge is all\n\t * there is room for.\n\t *\n\t * Passing no function draws no shadow, which is the behavior of every theme\n\t * that does not opt in.\n\t */\n\tsetShadowFn(shadowFn?: (text: string) => string): void {\n\t\tthis.shadowFn = shadowFn;\n\t\tthis.invalidateCache();\n\t}\n\n\t/**\n\t * Hold the band back from the right margin, leaving a gutter of page.\n\t *\n\t * A block that runs the full width of the terminal has no right edge to\n\t * show: it is a band of colour between two screen edges, not a sheet on a\n\t * page. Reserving a few columns gives it one, which is what makes both the\n\t * shadow's right-hand column and a cut edge possible at all.\n\t */\n\tsetInset(inset: number): void {\n\t\tif (this.inset === inset) return;\n\t\tthis.inset = Math.max(0, inset);\n\t\tthis.invalidateCache();\n\t}\n\n\t/**\n\t * Cut the right edge by hand rather than ruling it.\n\t *\n\t * The jitter is one column at most and it only ever eats padding, never\n\t * content, so a cut edge cannot cost a character. It is derived from the row\n\t * index and the block's own content, which keeps it identical between frames\n\t * — an edge that reshuffled on every render would read as noise, not paper.\n\t */\n\tsetCutEdge(cutEdge: boolean): void {\n\t\tif (this.cutEdge === cutEdge) return;\n\t\tthis.cutEdge = cutEdge;\n\t\tthis.invalidateCache();\n\t}\n\n\tprivate invalidateCache(): void {\n\t\tthis.cache = undefined;\n\t}\n\n\tprivate matchCache(\n\t\twidth: number,\n\t\tchildLines: string[],\n\t\tbgSample: string | undefined,\n\t\tshadowSample: string | undefined,\n\t): boolean {\n\t\tconst cache = this.cache;\n\t\treturn (\n\t\t\t!!cache &&\n\t\t\tcache.width === width &&\n\t\t\tcache.bgSample === bgSample &&\n\t\t\tcache.shadowSample === shadowSample &&\n\t\t\tcache.inset === this.inset &&\n\t\t\tcache.cutEdge === this.cutEdge &&\n\t\t\tcache.childLines.length === childLines.length &&\n\t\t\tcache.childLines.every((line, i) => line === childLines[i])\n\t\t);\n\t}\n\n\tinvalidate(): void {\n\t\tthis.invalidateCache();\n\t\tfor (const child of this.children) {\n\t\t\tchild.invalidate?.();\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.children.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// The band stops short of the right margin when inset, leaving a gutter of\n\t\t// page for the sheet's own edge and the shadow that follows it.\n\t\tconst bandWidth = Math.max(1, width - this.inset);\n\t\tconst contentWidth = Math.max(1, bandWidth - this.paddingX * 2);\n\t\tconst leftPad = \" \".repeat(this.paddingX);\n\n\t\t// Render all children\n\t\tconst childLines: string[] = [];\n\t\tfor (const child of this.children) {\n\t\t\tconst lines = child.render(contentWidth);\n\t\t\tfor (const line of lines) {\n\t\t\t\tchildLines.push(leftPad + line);\n\t\t\t}\n\t\t}\n\n\t\tif (childLines.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// Check if bgFn output changed by sampling\n\t\tconst bgSample = this.bgFn ? this.bgFn(\"test\") : undefined;\n\t\tconst shadowSample = this.shadowFn ? this.shadowFn(\"test\") : undefined;\n\n\t\t// Check cache validity\n\t\tif (this.matchCache(width, childLines, bgSample, shadowSample)) {\n\t\t\treturn this.cache!.lines;\n\t\t}\n\n\t\t// Apply background and padding\n\t\tconst rows: string[] = [];\n\t\tfor (let i = 0; i < this.paddingY; i++) rows.push(\"\");\n\t\trows.push(...childLines);\n\t\tfor (let i = 0; i < this.paddingY; i++) rows.push(\"\");\n\n\t\t// A cut edge needs a seed that is stable for this block but different\n\t\t// from its neighbours, or every sheet on screen is cut identically.\n\t\tconst seed = this.cutEdge ? hashString(childLines.join(\"\\n\")) : 0;\n\t\tconst shadowFn = this.shadowFn;\n\t\t// The right-hand column only exists when a gutter was reserved for it.\n\t\tconst hasColumn = shadowFn !== undefined && this.inset > 0;\n\t\tconst result: string[] = [];\n\t\trows.forEach((line, index) => {\n\t\t\t// Never let the cut reach into the content: the jitter comes out of\n\t\t\t// the padding the row was going to draw anyway.\n\t\t\tconst slack = Math.max(0, bandWidth - visibleWidth(line));\n\t\t\tconst cut = this.cutEdge && slack > 0 && hashString(`${seed}:${index}`) % CUT_EVERY === 0 ? 1 : 0;\n\t\t\tconst rowWidth = bandWidth - cut;\n\t\t\tconst band = this.applyBg(line, rowWidth);\n\t\t\t// The shadow holds one column whatever the cut does to the edge in\n\t\t\t// front of it. Following the cut was the first attempt and it broke\n\t\t\t// the shadow: `▌` paints half a cell, so a one-column step leaves no\n\t\t\t// overlap at all between one row's mark and the next, and what the\n\t\t\t// eye gets is a dashed staircase rather than an edge. A cut row\n\t\t\t// shows its nick as a column of page between sheet and shadow —\n\t\t\t// which is what a nick is — and the shadow stays a single line.\n\t\t\t// The first row has none: the offset is down *and* right.\n\t\t\tconst column = hasColumn && index > 0 ? \" \".repeat(cut) + shadowFn(\"▌\") : \"\";\n\t\t\tresult.push(band + column);\n\t\t});\n\n\t\t// The shadow's bottom run, offset one column right of the band. It ends\n\t\t// under the right-hand column so the two close the corner; with no\n\t\t// gutter there is no such column, and the run stops one cell short of\n\t\t// the margin instead of wrapping past it.\n\t\tif (shadowFn && bandWidth > 1) {\n\t\t\tresult.push(` ${shadowFn(\"▀\".repeat(hasColumn ? bandWidth : bandWidth - 1))}`);\n\t\t}\n\n\t\t// Update cache\n\t\tthis.cache = {\n\t\t\tchildLines,\n\t\t\twidth,\n\t\t\tbgSample,\n\t\t\tshadowSample,\n\t\t\tinset: this.inset,\n\t\t\tcutEdge: this.cutEdge,\n\t\t\tlines: result,\n\t\t};\n\n\t\treturn result;\n\t}\n\n\tprivate applyBg(line: string, width: number): string {\n\t\tconst visLen = visibleWidth(line);\n\t\tconst padNeeded = Math.max(0, width - visLen);\n\t\tconst padded = line + \" \".repeat(padNeeded);\n\n\t\tif (this.bgFn) {\n\t\t\treturn applyBackgroundToLine(padded, width, this.bgFn);\n\t\t}\n\t\treturn padded;\n\t}\n}\n"]}
@@ -73,8 +73,10 @@ export class Box {
73
73
  * A terminal has no sub-pixel offsets, so the shadow is one extra row of
74
74
  * `▀` — an upper half-block, which paints solid color across the top half of
75
75
  * its cells and so hugs the box's bottom edge — indented one column to give
76
- * the offset. There is no matching right-hand column: these boxes render at
77
- * the full terminal width, and a column past the last cell has nowhere to go.
76
+ * the offset. An inset box gets a matching column of `▌` down its right edge
77
+ * and the bottom run reaches under it, so the two meet at the corner; with
78
+ * no inset the band already owns the last cell, so the bottom edge is all
79
+ * there is room for.
78
80
  *
79
81
  * Passing no function draws no shadow, which is the behavior of every theme
80
82
  * that does not opt in.
@@ -168,6 +170,9 @@ export class Box {
168
170
  // A cut edge needs a seed that is stable for this block but different
169
171
  // from its neighbours, or every sheet on screen is cut identically.
170
172
  const seed = this.cutEdge ? hashString(childLines.join("\n")) : 0;
173
+ const shadowFn = this.shadowFn;
174
+ // The right-hand column only exists when a gutter was reserved for it.
175
+ const hasColumn = shadowFn !== undefined && this.inset > 0;
171
176
  const result = [];
172
177
  rows.forEach((line, index) => {
173
178
  // Never let the cut reach into the content: the jitter comes out of
@@ -176,14 +181,23 @@ export class Box {
176
181
  const cut = this.cutEdge && slack > 0 && hashString(`${seed}:${index}`) % CUT_EVERY === 0 ? 1 : 0;
177
182
  const rowWidth = bandWidth - cut;
178
183
  const band = this.applyBg(line, rowWidth);
179
- // The shadow follows the edge it is cast by, so it jitters with it.
184
+ // The shadow holds one column whatever the cut does to the edge in
185
+ // front of it. Following the cut was the first attempt and it broke
186
+ // the shadow: `▌` paints half a cell, so a one-column step leaves no
187
+ // overlap at all between one row's mark and the next, and what the
188
+ // eye gets is a dashed staircase rather than an edge. A cut row
189
+ // shows its nick as a column of page between sheet and shadow —
190
+ // which is what a nick is — and the shadow stays a single line.
180
191
  // The first row has none: the offset is down *and* right.
181
- const column = this.shadowFn && this.inset > 0 && index > 0 ? this.shadowFn("▌") : "";
192
+ const column = hasColumn && index > 0 ? " ".repeat(cut) + shadowFn("▌") : "";
182
193
  result.push(band + column);
183
194
  });
184
- // The shadow's bottom run, offset one column right of the band.
185
- if (this.shadowFn && bandWidth > 1) {
186
- result.push(` ${this.shadowFn("▀".repeat(bandWidth - 1))}`);
195
+ // The shadow's bottom run, offset one column right of the band. It ends
196
+ // under the right-hand column so the two close the corner; with no
197
+ // gutter there is no such column, and the run stops one cell short of
198
+ // the margin instead of wrapping past it.
199
+ if (shadowFn && bandWidth > 1) {
200
+ result.push(` ${shadowFn("▀".repeat(hasColumn ? bandWidth : bandWidth - 1))}`);
187
201
  }
188
202
  // Update cache
189
203
  this.cache = {
@@ -1 +1 @@
1
- {"version":3,"file":"box.js","sourceRoot":"","sources":["../../src/components/box.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Component } from \"../tui.js\";\nimport { applyBackgroundToLine, visibleWidth } from \"../utils.js\";\n\ntype RenderCache = {\n\tchildLines: string[];\n\twidth: number;\n\tbgSample: string | undefined;\n\tshadowSample: string | undefined;\n\tinset: number;\n\tcutEdge: boolean;\n\tlines: string[];\n};\n\n/**\n * Roughly one row in this many takes the cut.\n *\n * A coin flip per row is not a cut edge, it is a sawtooth: the eye reads\n * alternating columns as a pattern, which is the opposite of hand-made. Scissors\n * leave a mostly straight line with the occasional nick, so the deviation has to\n * be rare enough to read as an accident.\n */\nconst CUT_EVERY = 5;\n\n/** djb2. Small, stable, and good enough to decide one column of a paper edge. */\nfunction hashString(value: string): number {\n\tlet hash = 5381;\n\tfor (let i = 0; i < value.length; i++) {\n\t\thash = ((hash << 5) + hash + value.charCodeAt(i)) >>> 0;\n\t}\n\treturn hash;\n}\n\n/**\n * Box component - a container that applies padding and background to all children\n */\nexport class Box implements Component {\n\tchildren: Component[] = [];\n\tprivate paddingX: number;\n\tprivate paddingY: number;\n\tprivate bgFn?: (text: string) => string;\n\tprivate shadowFn?: (text: string) => string;\n\tprivate inset = 0;\n\tprivate cutEdge = false;\n\n\t// Cache for rendered output\n\tprivate cache?: RenderCache;\n\n\tconstructor(paddingX = 1, paddingY = 1, bgFn?: (text: string) => string) {\n\t\tthis.paddingX = paddingX;\n\t\tthis.paddingY = paddingY;\n\t\tthis.bgFn = bgFn;\n\t}\n\n\t/**\n\t * Change the horizontal padding after construction.\n\t *\n\t * For a box whose padding exists to carry a background band: with no band to\n\t * draw, the padding is just an indent that pushes the content out of line\n\t * with everything around it.\n\t */\n\tsetPaddingX(paddingX: number): void {\n\t\tif (this.paddingX === paddingX) return;\n\t\tthis.paddingX = paddingX;\n\t\tthis.invalidateCache();\n\t}\n\n\taddChild(component: Component): void {\n\t\tthis.children.push(component);\n\t\tthis.invalidateCache();\n\t}\n\n\tremoveChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index !== -1) {\n\t\t\tthis.children.splice(index, 1);\n\t\t\tthis.invalidateCache();\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.children = [];\n\t\tthis.invalidateCache();\n\t}\n\n\tsetBgFn(bgFn?: (text: string) => string): void {\n\t\tthis.bgFn = bgFn;\n\t\t// Don't invalidate here - we'll detect bgFn changes by sampling output\n\t}\n\n\t/**\n\t * Draw an offset band under the box, so the block reads as a sheet laid on\n\t * the page rather than a color printed into it.\n\t *\n\t * A terminal has no sub-pixel offsets, so the shadow is one extra row of\n\t * `▀` — an upper half-block, which paints solid color across the top half of\n\t * its cells and so hugs the box's bottom edge — indented one column to give\n\t * the offset. There is no matching right-hand column: these boxes render at\n\t * the full terminal width, and a column past the last cell has nowhere to go.\n\t *\n\t * Passing no function draws no shadow, which is the behavior of every theme\n\t * that does not opt in.\n\t */\n\tsetShadowFn(shadowFn?: (text: string) => string): void {\n\t\tthis.shadowFn = shadowFn;\n\t\tthis.invalidateCache();\n\t}\n\n\t/**\n\t * Hold the band back from the right margin, leaving a gutter of page.\n\t *\n\t * A block that runs the full width of the terminal has no right edge to\n\t * show: it is a band of colour between two screen edges, not a sheet on a\n\t * page. Reserving a few columns gives it one, which is what makes both the\n\t * shadow's right-hand column and a cut edge possible at all.\n\t */\n\tsetInset(inset: number): void {\n\t\tif (this.inset === inset) return;\n\t\tthis.inset = Math.max(0, inset);\n\t\tthis.invalidateCache();\n\t}\n\n\t/**\n\t * Cut the right edge by hand rather than ruling it.\n\t *\n\t * The jitter is one column at most and it only ever eats padding, never\n\t * content, so a cut edge cannot cost a character. It is derived from the row\n\t * index and the block's own content, which keeps it identical between frames\n\t * — an edge that reshuffled on every render would read as noise, not paper.\n\t */\n\tsetCutEdge(cutEdge: boolean): void {\n\t\tif (this.cutEdge === cutEdge) return;\n\t\tthis.cutEdge = cutEdge;\n\t\tthis.invalidateCache();\n\t}\n\n\tprivate invalidateCache(): void {\n\t\tthis.cache = undefined;\n\t}\n\n\tprivate matchCache(\n\t\twidth: number,\n\t\tchildLines: string[],\n\t\tbgSample: string | undefined,\n\t\tshadowSample: string | undefined,\n\t): boolean {\n\t\tconst cache = this.cache;\n\t\treturn (\n\t\t\t!!cache &&\n\t\t\tcache.width === width &&\n\t\t\tcache.bgSample === bgSample &&\n\t\t\tcache.shadowSample === shadowSample &&\n\t\t\tcache.inset === this.inset &&\n\t\t\tcache.cutEdge === this.cutEdge &&\n\t\t\tcache.childLines.length === childLines.length &&\n\t\t\tcache.childLines.every((line, i) => line === childLines[i])\n\t\t);\n\t}\n\n\tinvalidate(): void {\n\t\tthis.invalidateCache();\n\t\tfor (const child of this.children) {\n\t\t\tchild.invalidate?.();\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.children.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// The band stops short of the right margin when inset, leaving a gutter of\n\t\t// page for the sheet's own edge and the shadow that follows it.\n\t\tconst bandWidth = Math.max(1, width - this.inset);\n\t\tconst contentWidth = Math.max(1, bandWidth - this.paddingX * 2);\n\t\tconst leftPad = \" \".repeat(this.paddingX);\n\n\t\t// Render all children\n\t\tconst childLines: string[] = [];\n\t\tfor (const child of this.children) {\n\t\t\tconst lines = child.render(contentWidth);\n\t\t\tfor (const line of lines) {\n\t\t\t\tchildLines.push(leftPad + line);\n\t\t\t}\n\t\t}\n\n\t\tif (childLines.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// Check if bgFn output changed by sampling\n\t\tconst bgSample = this.bgFn ? this.bgFn(\"test\") : undefined;\n\t\tconst shadowSample = this.shadowFn ? this.shadowFn(\"test\") : undefined;\n\n\t\t// Check cache validity\n\t\tif (this.matchCache(width, childLines, bgSample, shadowSample)) {\n\t\t\treturn this.cache!.lines;\n\t\t}\n\n\t\t// Apply background and padding\n\t\tconst rows: string[] = [];\n\t\tfor (let i = 0; i < this.paddingY; i++) rows.push(\"\");\n\t\trows.push(...childLines);\n\t\tfor (let i = 0; i < this.paddingY; i++) rows.push(\"\");\n\n\t\t// A cut edge needs a seed that is stable for this block but different\n\t\t// from its neighbours, or every sheet on screen is cut identically.\n\t\tconst seed = this.cutEdge ? hashString(childLines.join(\"\\n\")) : 0;\n\t\tconst result: string[] = [];\n\t\trows.forEach((line, index) => {\n\t\t\t// Never let the cut reach into the content: the jitter comes out of\n\t\t\t// the padding the row was going to draw anyway.\n\t\t\tconst slack = Math.max(0, bandWidth - visibleWidth(line));\n\t\t\tconst cut = this.cutEdge && slack > 0 && hashString(`${seed}:${index}`) % CUT_EVERY === 0 ? 1 : 0;\n\t\t\tconst rowWidth = bandWidth - cut;\n\t\t\tconst band = this.applyBg(line, rowWidth);\n\t\t\t// The shadow follows the edge it is cast by, so it jitters with it.\n\t\t\t// The first row has none: the offset is down *and* right.\n\t\t\tconst column = this.shadowFn && this.inset > 0 && index > 0 ? this.shadowFn(\"▌\") : \"\";\n\t\t\tresult.push(band + column);\n\t\t});\n\n\t\t// The shadow's bottom run, offset one column right of the band.\n\t\tif (this.shadowFn && bandWidth > 1) {\n\t\t\tresult.push(` ${this.shadowFn(\"▀\".repeat(bandWidth - 1))}`);\n\t\t}\n\n\t\t// Update cache\n\t\tthis.cache = {\n\t\t\tchildLines,\n\t\t\twidth,\n\t\t\tbgSample,\n\t\t\tshadowSample,\n\t\t\tinset: this.inset,\n\t\t\tcutEdge: this.cutEdge,\n\t\t\tlines: result,\n\t\t};\n\n\t\treturn result;\n\t}\n\n\tprivate applyBg(line: string, width: number): string {\n\t\tconst visLen = visibleWidth(line);\n\t\tconst padNeeded = Math.max(0, width - visLen);\n\t\tconst padded = line + \" \".repeat(padNeeded);\n\n\t\tif (this.bgFn) {\n\t\t\treturn applyBackgroundToLine(padded, width, this.bgFn);\n\t\t}\n\t\treturn padded;\n\t}\n}\n"]}
1
+ {"version":3,"file":"box.js","sourceRoot":"","sources":["../../src/components/box.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Component } from \"../tui.js\";\nimport { applyBackgroundToLine, visibleWidth } from \"../utils.js\";\n\ntype RenderCache = {\n\tchildLines: string[];\n\twidth: number;\n\tbgSample: string | undefined;\n\tshadowSample: string | undefined;\n\tinset: number;\n\tcutEdge: boolean;\n\tlines: string[];\n};\n\n/**\n * Roughly one row in this many takes the cut.\n *\n * A coin flip per row is not a cut edge, it is a sawtooth: the eye reads\n * alternating columns as a pattern, which is the opposite of hand-made. Scissors\n * leave a mostly straight line with the occasional nick, so the deviation has to\n * be rare enough to read as an accident.\n */\nconst CUT_EVERY = 5;\n\n/** djb2. Small, stable, and good enough to decide one column of a paper edge. */\nfunction hashString(value: string): number {\n\tlet hash = 5381;\n\tfor (let i = 0; i < value.length; i++) {\n\t\thash = ((hash << 5) + hash + value.charCodeAt(i)) >>> 0;\n\t}\n\treturn hash;\n}\n\n/**\n * Box component - a container that applies padding and background to all children\n */\nexport class Box implements Component {\n\tchildren: Component[] = [];\n\tprivate paddingX: number;\n\tprivate paddingY: number;\n\tprivate bgFn?: (text: string) => string;\n\tprivate shadowFn?: (text: string) => string;\n\tprivate inset = 0;\n\tprivate cutEdge = false;\n\n\t// Cache for rendered output\n\tprivate cache?: RenderCache;\n\n\tconstructor(paddingX = 1, paddingY = 1, bgFn?: (text: string) => string) {\n\t\tthis.paddingX = paddingX;\n\t\tthis.paddingY = paddingY;\n\t\tthis.bgFn = bgFn;\n\t}\n\n\t/**\n\t * Change the horizontal padding after construction.\n\t *\n\t * For a box whose padding exists to carry a background band: with no band to\n\t * draw, the padding is just an indent that pushes the content out of line\n\t * with everything around it.\n\t */\n\tsetPaddingX(paddingX: number): void {\n\t\tif (this.paddingX === paddingX) return;\n\t\tthis.paddingX = paddingX;\n\t\tthis.invalidateCache();\n\t}\n\n\taddChild(component: Component): void {\n\t\tthis.children.push(component);\n\t\tthis.invalidateCache();\n\t}\n\n\tremoveChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index !== -1) {\n\t\t\tthis.children.splice(index, 1);\n\t\t\tthis.invalidateCache();\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.children = [];\n\t\tthis.invalidateCache();\n\t}\n\n\tsetBgFn(bgFn?: (text: string) => string): void {\n\t\tthis.bgFn = bgFn;\n\t\t// Don't invalidate here - we'll detect bgFn changes by sampling output\n\t}\n\n\t/**\n\t * Draw an offset band under the box, so the block reads as a sheet laid on\n\t * the page rather than a color printed into it.\n\t *\n\t * A terminal has no sub-pixel offsets, so the shadow is one extra row of\n\t * `▀` — an upper half-block, which paints solid color across the top half of\n\t * its cells and so hugs the box's bottom edge — indented one column to give\n\t * the offset. An inset box gets a matching column of `▌` down its right edge\n\t * and the bottom run reaches under it, so the two meet at the corner; with\n\t * no inset the band already owns the last cell, so the bottom edge is all\n\t * there is room for.\n\t *\n\t * Passing no function draws no shadow, which is the behavior of every theme\n\t * that does not opt in.\n\t */\n\tsetShadowFn(shadowFn?: (text: string) => string): void {\n\t\tthis.shadowFn = shadowFn;\n\t\tthis.invalidateCache();\n\t}\n\n\t/**\n\t * Hold the band back from the right margin, leaving a gutter of page.\n\t *\n\t * A block that runs the full width of the terminal has no right edge to\n\t * show: it is a band of colour between two screen edges, not a sheet on a\n\t * page. Reserving a few columns gives it one, which is what makes both the\n\t * shadow's right-hand column and a cut edge possible at all.\n\t */\n\tsetInset(inset: number): void {\n\t\tif (this.inset === inset) return;\n\t\tthis.inset = Math.max(0, inset);\n\t\tthis.invalidateCache();\n\t}\n\n\t/**\n\t * Cut the right edge by hand rather than ruling it.\n\t *\n\t * The jitter is one column at most and it only ever eats padding, never\n\t * content, so a cut edge cannot cost a character. It is derived from the row\n\t * index and the block's own content, which keeps it identical between frames\n\t * — an edge that reshuffled on every render would read as noise, not paper.\n\t */\n\tsetCutEdge(cutEdge: boolean): void {\n\t\tif (this.cutEdge === cutEdge) return;\n\t\tthis.cutEdge = cutEdge;\n\t\tthis.invalidateCache();\n\t}\n\n\tprivate invalidateCache(): void {\n\t\tthis.cache = undefined;\n\t}\n\n\tprivate matchCache(\n\t\twidth: number,\n\t\tchildLines: string[],\n\t\tbgSample: string | undefined,\n\t\tshadowSample: string | undefined,\n\t): boolean {\n\t\tconst cache = this.cache;\n\t\treturn (\n\t\t\t!!cache &&\n\t\t\tcache.width === width &&\n\t\t\tcache.bgSample === bgSample &&\n\t\t\tcache.shadowSample === shadowSample &&\n\t\t\tcache.inset === this.inset &&\n\t\t\tcache.cutEdge === this.cutEdge &&\n\t\t\tcache.childLines.length === childLines.length &&\n\t\t\tcache.childLines.every((line, i) => line === childLines[i])\n\t\t);\n\t}\n\n\tinvalidate(): void {\n\t\tthis.invalidateCache();\n\t\tfor (const child of this.children) {\n\t\t\tchild.invalidate?.();\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.children.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// The band stops short of the right margin when inset, leaving a gutter of\n\t\t// page for the sheet's own edge and the shadow that follows it.\n\t\tconst bandWidth = Math.max(1, width - this.inset);\n\t\tconst contentWidth = Math.max(1, bandWidth - this.paddingX * 2);\n\t\tconst leftPad = \" \".repeat(this.paddingX);\n\n\t\t// Render all children\n\t\tconst childLines: string[] = [];\n\t\tfor (const child of this.children) {\n\t\t\tconst lines = child.render(contentWidth);\n\t\t\tfor (const line of lines) {\n\t\t\t\tchildLines.push(leftPad + line);\n\t\t\t}\n\t\t}\n\n\t\tif (childLines.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// Check if bgFn output changed by sampling\n\t\tconst bgSample = this.bgFn ? this.bgFn(\"test\") : undefined;\n\t\tconst shadowSample = this.shadowFn ? this.shadowFn(\"test\") : undefined;\n\n\t\t// Check cache validity\n\t\tif (this.matchCache(width, childLines, bgSample, shadowSample)) {\n\t\t\treturn this.cache!.lines;\n\t\t}\n\n\t\t// Apply background and padding\n\t\tconst rows: string[] = [];\n\t\tfor (let i = 0; i < this.paddingY; i++) rows.push(\"\");\n\t\trows.push(...childLines);\n\t\tfor (let i = 0; i < this.paddingY; i++) rows.push(\"\");\n\n\t\t// A cut edge needs a seed that is stable for this block but different\n\t\t// from its neighbours, or every sheet on screen is cut identically.\n\t\tconst seed = this.cutEdge ? hashString(childLines.join(\"\\n\")) : 0;\n\t\tconst shadowFn = this.shadowFn;\n\t\t// The right-hand column only exists when a gutter was reserved for it.\n\t\tconst hasColumn = shadowFn !== undefined && this.inset > 0;\n\t\tconst result: string[] = [];\n\t\trows.forEach((line, index) => {\n\t\t\t// Never let the cut reach into the content: the jitter comes out of\n\t\t\t// the padding the row was going to draw anyway.\n\t\t\tconst slack = Math.max(0, bandWidth - visibleWidth(line));\n\t\t\tconst cut = this.cutEdge && slack > 0 && hashString(`${seed}:${index}`) % CUT_EVERY === 0 ? 1 : 0;\n\t\t\tconst rowWidth = bandWidth - cut;\n\t\t\tconst band = this.applyBg(line, rowWidth);\n\t\t\t// The shadow holds one column whatever the cut does to the edge in\n\t\t\t// front of it. Following the cut was the first attempt and it broke\n\t\t\t// the shadow: `▌` paints half a cell, so a one-column step leaves no\n\t\t\t// overlap at all between one row's mark and the next, and what the\n\t\t\t// eye gets is a dashed staircase rather than an edge. A cut row\n\t\t\t// shows its nick as a column of page between sheet and shadow —\n\t\t\t// which is what a nick is — and the shadow stays a single line.\n\t\t\t// The first row has none: the offset is down *and* right.\n\t\t\tconst column = hasColumn && index > 0 ? \" \".repeat(cut) + shadowFn(\"▌\") : \"\";\n\t\t\tresult.push(band + column);\n\t\t});\n\n\t\t// The shadow's bottom run, offset one column right of the band. It ends\n\t\t// under the right-hand column so the two close the corner; with no\n\t\t// gutter there is no such column, and the run stops one cell short of\n\t\t// the margin instead of wrapping past it.\n\t\tif (shadowFn && bandWidth > 1) {\n\t\t\tresult.push(` ${shadowFn(\"▀\".repeat(hasColumn ? bandWidth : bandWidth - 1))}`);\n\t\t}\n\n\t\t// Update cache\n\t\tthis.cache = {\n\t\t\tchildLines,\n\t\t\twidth,\n\t\t\tbgSample,\n\t\t\tshadowSample,\n\t\t\tinset: this.inset,\n\t\t\tcutEdge: this.cutEdge,\n\t\t\tlines: result,\n\t\t};\n\n\t\treturn result;\n\t}\n\n\tprivate applyBg(line: string, width: number): string {\n\t\tconst visLen = visibleWidth(line);\n\t\tconst padNeeded = Math.max(0, width - visLen);\n\t\tconst padded = line + \" \".repeat(padNeeded);\n\n\t\tif (this.bgFn) {\n\t\t\treturn applyBackgroundToLine(padded, width, this.bgFn);\n\t\t}\n\t\treturn padded;\n\t}\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-tui",
3
- "version": "0.5.37",
3
+ "version": "0.5.38",
4
4
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",