@markii/ansi 0.15.0 → 0.17.0-next.0

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.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/dist/ansi.d.ts +26 -0
  3. package/dist/ansi.js +51 -0
  4. package/dist/components/badge.d.ts +1 -1
  5. package/dist/components/badge.js +5 -4
  6. package/dist/components/callout.d.ts +38 -13
  7. package/dist/components/callout.js +68 -31
  8. package/dist/components/card.d.ts +34 -2
  9. package/dist/components/card.js +55 -18
  10. package/dist/components/cell.d.ts +5 -11
  11. package/dist/components/cell.js +11 -15
  12. package/dist/components/chart.d.ts +4 -10
  13. package/dist/components/chart.js +16 -24
  14. package/dist/components/details-string.d.ts +26 -0
  15. package/dist/components/details-string.js +39 -0
  16. package/dist/components/details.d.ts +32 -10
  17. package/dist/components/details.js +28 -25
  18. package/dist/components/divider.js +12 -16
  19. package/dist/components/figure.d.ts +5 -16
  20. package/dist/components/figure.js +9 -19
  21. package/dist/components/index.d.ts +14 -10
  22. package/dist/components/index.js +19 -16
  23. package/dist/components/kbd.d.ts +1 -1
  24. package/dist/components/kbd.js +3 -2
  25. package/dist/components/layout-wrapper.d.ts +7 -22
  26. package/dist/components/layout-wrapper.js +9 -23
  27. package/dist/components/progress.d.ts +3 -11
  28. package/dist/components/progress.js +8 -17
  29. package/dist/components/rating.d.ts +1 -2
  30. package/dist/components/rating.js +3 -4
  31. package/dist/components/row.d.ts +35 -20
  32. package/dist/components/row.js +35 -51
  33. package/dist/components/stat.d.ts +3 -8
  34. package/dist/components/stat.js +5 -11
  35. package/dist/components/tab.d.ts +9 -8
  36. package/dist/components/tab.js +12 -10
  37. package/dist/components/table-grid.d.ts +2 -1
  38. package/dist/components/table-grid.js +14 -12
  39. package/dist/components/table.d.ts +5 -10
  40. package/dist/components/table.js +8 -13
  41. package/dist/components/tabs-string.d.ts +17 -0
  42. package/dist/components/tabs-string.js +30 -0
  43. package/dist/components/tabs.d.ts +31 -14
  44. package/dist/components/tabs.js +55 -30
  45. package/dist/failure-presentation.d.ts +15 -0
  46. package/dist/failure-presentation.js +22 -0
  47. package/dist/glyphs.d.ts +73 -0
  48. package/dist/glyphs.js +88 -0
  49. package/dist/index.d.ts +6 -4
  50. package/dist/index.js +10 -7
  51. package/dist/ink-string.d.ts +23 -0
  52. package/dist/ink-string.js +65 -0
  53. package/dist/interactive.d.ts +66 -0
  54. package/dist/interactive.js +207 -0
  55. package/dist/layout.js +1 -2
  56. package/dist/pack-loader.d.ts +64 -0
  57. package/dist/pack-loader.js +172 -0
  58. package/dist/pack-script.d.ts +44 -0
  59. package/dist/pack-script.js +135 -0
  60. package/dist/registry.d.ts +106 -100
  61. package/dist/registry.js +17 -9
  62. package/dist/render.d.ts +63 -25
  63. package/dist/render.js +1001 -380
  64. package/dist/text-grid.d.ts +90 -0
  65. package/dist/text-grid.js +300 -0
  66. package/package.json +9 -5
  67. package/dist/box.d.ts +0 -47
  68. package/dist/box.js +0 -209
  69. package/dist/measure.d.ts +0 -25
  70. package/dist/measure.js +0 -108
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sadigaxund
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/ansi.d.ts CHANGED
@@ -39,6 +39,32 @@ export declare const italic: (text: string, level: ColorLevel) => string;
39
39
  export declare const underline: (text: string, level: ColorLevel) => string;
40
40
  /** SGR inverse/reverse video (7). Returns `text` unchanged at `'none'`. */
41
41
  export declare const inverse: (text: string, level: ColorLevel) => string;
42
+ /**
43
+ * Repairs an SGR escape span that would otherwise cross an embedded `\n`
44
+ * inside `text`, so every physical LINE is independently self-contained:
45
+ * whatever SGR codes are still "open" (emitted since the last full reset)
46
+ * at a line boundary are closed with a reset before the `\n` and reopened
47
+ * at the start of the next line.
48
+ *
49
+ * This is the SYSTEMATIC fix for a real Ink behavior discovered during
50
+ * batch 10's post-phase-2 review: Ink's `<Text>` does not treat an embedded
51
+ * raw ANSI escape as transparent across an embedded newline the way a
52
+ * terminal receiving the same bytes directly would — it resets SGR state
53
+ * at every line boundary within one `<Text>` value and does not reopen it.
54
+ * Every multi-line string this engine hands to a single `<Text>` node
55
+ * (`render.tsx`'s `styledText` helper) MUST pass through this function
56
+ * first: a dim-wrapped multi-line frame, a bold-and-underlined heading that
57
+ * wrapped onto more than one line, a paragraph whose bold span happened to
58
+ * straddle a wrap point — anything this engine itself line-wrapped (with
59
+ * `text-grid.ts`'s `wrapText`) while carrying an open style. It is a no-op,
60
+ * fast-pathed, for a string with no `\n` at all.
61
+ *
62
+ * Every SGR code this engine's own helpers ever emit closes with a FULL
63
+ * reset (`\x1b[0m`), never a narrow one (`ansi.ts`'s `wrapSgr`), so
64
+ * tracking "what is currently open" only ever needs a flat list cleared by
65
+ * the next `\x1b[0m` (or a bare `\x1b[m`) — never per-attribute bookkeeping.
66
+ */
67
+ export declare function carrySgrAcrossLines(text: string): string;
42
68
  /**
43
69
  * OSC 8: `ESC ] 8 ; ; url BEL text ESC ] 8 ; ; BEL`. Emitted only when
44
70
  * `level` is not `'none'`; at `'none'` this returns `text` UNCHANGED, and
package/dist/ansi.js CHANGED
@@ -63,6 +63,57 @@ export const italic = sgrWrap(3);
63
63
  export const underline = sgrWrap(4);
64
64
  /** SGR inverse/reverse video (7). Returns `text` unchanged at `'none'`. */
65
65
  export const inverse = sgrWrap(7);
66
+ /** Matches one SGR sequence, capturing its raw parameter string (empty for a bare `ESC [ m`). */
67
+ const SGR_TOKEN = /\x1b\[([0-9;]*)m/g;
68
+ /**
69
+ * Repairs an SGR escape span that would otherwise cross an embedded `\n`
70
+ * inside `text`, so every physical LINE is independently self-contained:
71
+ * whatever SGR codes are still "open" (emitted since the last full reset)
72
+ * at a line boundary are closed with a reset before the `\n` and reopened
73
+ * at the start of the next line.
74
+ *
75
+ * This is the SYSTEMATIC fix for a real Ink behavior discovered during
76
+ * batch 10's post-phase-2 review: Ink's `<Text>` does not treat an embedded
77
+ * raw ANSI escape as transparent across an embedded newline the way a
78
+ * terminal receiving the same bytes directly would — it resets SGR state
79
+ * at every line boundary within one `<Text>` value and does not reopen it.
80
+ * Every multi-line string this engine hands to a single `<Text>` node
81
+ * (`render.tsx`'s `styledText` helper) MUST pass through this function
82
+ * first: a dim-wrapped multi-line frame, a bold-and-underlined heading that
83
+ * wrapped onto more than one line, a paragraph whose bold span happened to
84
+ * straddle a wrap point — anything this engine itself line-wrapped (with
85
+ * `text-grid.ts`'s `wrapText`) while carrying an open style. It is a no-op,
86
+ * fast-pathed, for a string with no `\n` at all.
87
+ *
88
+ * Every SGR code this engine's own helpers ever emit closes with a FULL
89
+ * reset (`\x1b[0m`), never a narrow one (`ansi.ts`'s `wrapSgr`), so
90
+ * tracking "what is currently open" only ever needs a flat list cleared by
91
+ * the next `\x1b[0m` (or a bare `\x1b[m`) — never per-attribute bookkeeping.
92
+ */
93
+ export function carrySgrAcrossLines(text) {
94
+ if (!text.includes('\n'))
95
+ return text;
96
+ const lines = text.split('\n');
97
+ let openCodes = [];
98
+ const result = [];
99
+ for (const line of lines) {
100
+ const prefix = openCodes.map((params) => `${ESC}[${params}m`).join('');
101
+ SGR_TOKEN.lastIndex = 0;
102
+ let match;
103
+ while ((match = SGR_TOKEN.exec(line)) !== null) {
104
+ const params = match[1] ?? '';
105
+ if (params === '' || params === '0') {
106
+ openCodes = [];
107
+ }
108
+ else {
109
+ openCodes = [...openCodes, params];
110
+ }
111
+ }
112
+ const suffix = openCodes.length > 0 ? RESET : '';
113
+ result.push(`${prefix}${line}${suffix}`);
114
+ }
115
+ return result.join('\n');
116
+ }
66
117
  /**
67
118
  * OSC 8: `ESC ] 8 ; ; url BEL text ESC ] 8 ; ; BEL`. Emitted only when
68
119
  * `level` is not `'none'`; at `'none'` this returns `text` UNCHANGED, and
@@ -1,4 +1,4 @@
1
- import type { AnsiComponent } from '../registry.js';
1
+ import { type AnsiComponent } from '../registry.js';
2
2
  export type BadgeVariant = 'neutral' | 'info' | 'success' | 'warning' | 'danger';
3
3
  /**
4
4
  * `:badge[New]{variant=success}` — a status pill for an inline text
@@ -1,3 +1,4 @@
1
+ import { childrenText } from '../registry.js';
1
2
  const BADGE_VARIANTS = [
2
3
  'neutral',
3
4
  'info',
@@ -31,14 +32,14 @@ const TOKEN_NAME = {
31
32
  * variant. At color level `'none'` there is no inverse video to distinguish
32
33
  * a badge from surrounding text, so it degrades to `[label]` instead.
33
34
  */
34
- export const Badge = (attributes, children, ctx) => {
35
+ export const Badge = ({ attributes, children, ctx }) => {
35
36
  const rawVariant = attributes.variant ?? DEFAULT_VARIANT;
36
37
  const variant = isBadgeVariant(rawVariant)
37
38
  ? rawVariant
38
39
  : DEFAULT_VARIANT;
39
- const childrenText = children();
40
+ const text = childrenText(children);
40
41
  if (ctx.color === 'none')
41
- return `[${childrenText}]`;
42
+ return `[${text}]`;
42
43
  const token = TOKEN_NAME[VARIANT_TOKEN[variant]];
43
- return ctx.inverse(ctx.style(` ${childrenText} `, token));
44
+ return ctx.inverse(ctx.style(` ${text} `, token));
44
45
  };
@@ -1,20 +1,45 @@
1
- import type { AnsiComponent } from '../registry.js';
1
+ import type { GlyphSet } from '../glyphs.js';
2
+ import type { ResolvedLayoutPresets } from '../layout.js';
3
+ import { type AnsiComponent, type DirectiveAttributes } from '../registry.js';
4
+ import type { Tier1Token } from '../theme.js';
2
5
  export type CalloutType = 'info' | 'warning' | 'danger';
6
+ export declare const CALLOUT_TOKENS: Record<CalloutType, Tier1Token>;
7
+ export declare function isCalloutType(value: string): value is CalloutType;
8
+ /** `headerText` for `type`, e.g. `ℹ Info` — needed by both `resolveCalloutInnerWidth` (before there is a component instance) and `Callout` itself. */
9
+ export declare function headerTextFor(type: CalloutType, glyphs: GlyphSet): string;
10
+ /** The resolved `type` of a callout: an unknown or missing value is `info`. */
11
+ export declare function resolveCalloutType(attributes: DirectiveAttributes): CalloutType;
12
+ /**
13
+ * The callout's own OUTER (bar-including) width: what `Callout` draws at
14
+ * and what the Ink element form in `render.tsx` sizes its `Box` to.
15
+ * `title` is expected already sanitized, as `Callout` passes it.
16
+ */
17
+ export declare function resolveCalloutBoxWidth(type: CalloutType, title: string | null, layout: ResolvedLayoutPresets | undefined, outerWidth: number, glyphs: GlyphSet): number;
18
+ /**
19
+ * The inner budget `callout`'s body renders at, given ONLY its own
20
+ * attributes and the outer width — never the body itself. Mirrors
21
+ * `card.ts`'s `resolveCardInnerWidth`: `render.tsx`'s walk calls this
22
+ * BEFORE building children, so a callout's body (and any self-drawing
23
+ * component nested inside it, like a `card`) is built exactly once, at the
24
+ * correct width, never built wide and re-wrapped narrower afterward — see
25
+ * `resolveCardInnerWidth`'s doc comment for why that re-wrap-afterward
26
+ * shape was a real, shipped regression.
27
+ */
28
+ export declare function resolveCalloutInnerWidth(attributes: DirectiveAttributes, layout: ResolvedLayoutPresets | undefined, outerWidth: number, glyphs: GlyphSet): number;
3
29
  /**
4
30
  * `:::callout{type=info|warning|danger title="..." text=left|center|right}` —
5
31
  * a colored aside/warning/danger box. Unknown/missing `type` falls back to
6
- * `info` (`render.ts`'s generic invalid-enum notice covers reporting that,
7
- * same as every other enum attribute). Terminal form: every line carries a
8
- * colored left bar; the first line is the icon plus the type label, an
9
- * optional bold title line follows, then the body wrapped to the remaining
10
- * width. `text` aligns the icon/title/body lines within that remaining
11
- * width; absent/invalid `text` behaves as `left` (no padding needed).
32
+ * `info`. Terminal form: every line carries a colored left bar; the first
33
+ * line is the icon plus the type label, an optional bold title line
34
+ * follows, then the body. `text` aligns the icon/title/body lines within
35
+ * the remaining width.
12
36
  *
13
- * Registered `selfLayout` (`registry.ts`'s `AnsiRegistryEntry.selfLayout`):
14
- * every line carries the colored bar prefix, which a generic post-render
15
- * `applyLayout` narrowing would re-wrap right through, losing the bar on any
16
- * continuation line. This component instead reads the resolved
17
- * `width`/`align` off `ctx.layout` itself and sizes/places its own bar
18
- * block, exactly like `card`/`table`/`chart`.
37
+ * Registered `selfLayout`: every line carries the colored bar prefix, which
38
+ * a generic post-render `applyLayout` narrowing would re-wrap right through,
39
+ * losing the bar on any continuation line. This component reads the
40
+ * resolved `width`/`align` off `ctx.layout` itself and sizes/places its own
41
+ * bar block, exactly like `card`/`table`/`divider`. `children` arrives
42
+ * ALREADY built at exactly this component's own inner width
43
+ * (`resolveCalloutInnerWidth` above) — this component never re-wraps.
19
44
  */
20
45
  export declare const Callout: AnsiComponent;
@@ -1,69 +1,106 @@
1
- import { measure } from '../measure.js';
1
+ import { measureWidth, padText } from '../text-grid.js';
2
2
  import { selfLayoutAlign, selfLayoutWidth } from '../layout.js';
3
+ import { childrenText, } from '../registry.js';
3
4
  const CALLOUT_TYPES = ['info', 'warning', 'danger'];
4
- const CALLOUT_ICONS = {
5
- info: 'ℹ',
6
- warning: '▲',
7
- danger: '✕',
8
- };
9
5
  const CALLOUT_LABELS = {
10
6
  info: 'Info',
11
7
  warning: 'Warning',
12
8
  danger: 'Danger',
13
9
  };
14
- const CALLOUT_TOKENS = {
10
+ export const CALLOUT_TOKENS = {
15
11
  info: '--mk-info',
16
12
  warning: '--mk-warning',
17
13
  danger: '--mk-danger',
18
14
  };
19
- function isCalloutType(value) {
15
+ export function isCalloutType(value) {
20
16
  return CALLOUT_TYPES.includes(value);
21
17
  }
22
18
  const TEXT_ALIGNS = ['left', 'center', 'right'];
23
19
  function isTextAlign(value) {
24
20
  return TEXT_ALIGNS.includes(value);
25
21
  }
26
- /** The left bar every line of a callout carries, marking it as one colored block at a glance (`box.ts` reserves `│`/`┆` for frames, so this uses the half-block glyph instead). */
27
- const BAR = '▌ ';
22
+ /** `headerText` for `type`, e.g. `ℹ Info` — needed by both `resolveCalloutInnerWidth` (before there is a component instance) and `Callout` itself. */
23
+ export function headerTextFor(type, glyphs) {
24
+ return `${glyphs.calloutIcons[type]} ${CALLOUT_LABELS[type]}`;
25
+ }
26
+ /** The resolved `type` of a callout: an unknown or missing value is `info`. */
27
+ export function resolveCalloutType(attributes) {
28
+ const raw = attributes.type ?? 'info';
29
+ return isCalloutType(raw) ? raw : 'info';
30
+ }
31
+ /**
32
+ * The callout's own OUTER (bar-including) width: what `Callout` draws at
33
+ * and what the Ink element form in `render.tsx` sizes its `Box` to.
34
+ * `title` is expected already sanitized, as `Callout` passes it.
35
+ */
36
+ export function resolveCalloutBoxWidth(type, title, layout, outerWidth, glyphs) {
37
+ const barWidth = measureWidth(glyphs.calloutBar);
38
+ const naturalWidth = barWidth +
39
+ Math.max(measureWidth(headerTextFor(type, glyphs)), measureWidth(title ?? ''));
40
+ return selfLayoutWidth(layout, outerWidth, naturalWidth);
41
+ }
42
+ /**
43
+ * The inner budget `callout`'s body renders at, given ONLY its own
44
+ * attributes and the outer width — never the body itself. Mirrors
45
+ * `card.ts`'s `resolveCardInnerWidth`: `render.tsx`'s walk calls this
46
+ * BEFORE building children, so a callout's body (and any self-drawing
47
+ * component nested inside it, like a `card`) is built exactly once, at the
48
+ * correct width, never built wide and re-wrapped narrower afterward — see
49
+ * `resolveCardInnerWidth`'s doc comment for why that re-wrap-afterward
50
+ * shape was a real, shipped regression.
51
+ */
52
+ export function resolveCalloutInnerWidth(attributes, layout, outerWidth, glyphs) {
53
+ // The left bar every line of a callout carries, marking it as one
54
+ // colored block at a glance.
55
+ const barWidth = measureWidth(glyphs.calloutBar);
56
+ const rawType = attributes.type ?? 'info';
57
+ const type = isCalloutType(rawType) ? rawType : 'info';
58
+ const title = attributes.title ?? null;
59
+ const naturalWidth = barWidth +
60
+ Math.max(measureWidth(headerTextFor(type, glyphs)), measureWidth(title ?? ''));
61
+ const boxWidth = selfLayoutWidth(layout, outerWidth, naturalWidth);
62
+ return Math.max(1, boxWidth - barWidth);
63
+ }
28
64
  /**
29
65
  * `:::callout{type=info|warning|danger title="..." text=left|center|right}` —
30
66
  * a colored aside/warning/danger box. Unknown/missing `type` falls back to
31
- * `info` (`render.ts`'s generic invalid-enum notice covers reporting that,
32
- * same as every other enum attribute). Terminal form: every line carries a
33
- * colored left bar; the first line is the icon plus the type label, an
34
- * optional bold title line follows, then the body wrapped to the remaining
35
- * width. `text` aligns the icon/title/body lines within that remaining
36
- * width; absent/invalid `text` behaves as `left` (no padding needed).
67
+ * `info`. Terminal form: every line carries a colored left bar; the first
68
+ * line is the icon plus the type label, an optional bold title line
69
+ * follows, then the body. `text` aligns the icon/title/body lines within
70
+ * the remaining width.
37
71
  *
38
- * Registered `selfLayout` (`registry.ts`'s `AnsiRegistryEntry.selfLayout`):
39
- * every line carries the colored bar prefix, which a generic post-render
40
- * `applyLayout` narrowing would re-wrap right through, losing the bar on any
41
- * continuation line. This component instead reads the resolved
42
- * `width`/`align` off `ctx.layout` itself and sizes/places its own bar
43
- * block, exactly like `card`/`table`/`chart`.
72
+ * Registered `selfLayout`: every line carries the colored bar prefix, which
73
+ * a generic post-render `applyLayout` narrowing would re-wrap right through,
74
+ * losing the bar on any continuation line. This component reads the
75
+ * resolved `width`/`align` off `ctx.layout` itself and sizes/places its own
76
+ * bar block, exactly like `card`/`table`/`divider`. `children` arrives
77
+ * ALREADY built at exactly this component's own inner width
78
+ * (`resolveCalloutInnerWidth` above) — this component never re-wraps.
44
79
  */
45
- export const Callout = (attributes, children, ctx) => {
80
+ export const Callout = ({ attributes, children, ctx }) => {
46
81
  const rawType = attributes.type ?? 'info';
47
82
  const type = isCalloutType(rawType) ? rawType : 'info';
48
83
  const token = CALLOUT_TOKENS[type];
49
84
  const title = attributes.title ?? null;
50
85
  const rawTextAlign = attributes.text;
51
86
  const align = rawTextAlign && isTextAlign(rawTextAlign) ? rawTextAlign : 'left';
52
- const headerText = `${CALLOUT_ICONS[type]} ${CALLOUT_LABELS[type]}`;
87
+ const headerText = headerTextFor(type, ctx.glyphs);
53
88
  const titleText = title ? ctx.text(title) : undefined;
54
- const naturalWidth = BAR.length + Math.max(measure(headerText), measure(titleText ?? ''));
89
+ const barWidth = measureWidth(ctx.glyphs.calloutBar);
90
+ const naturalWidth = barWidth +
91
+ Math.max(measureWidth(headerText), measureWidth(titleText ?? ''));
55
92
  const boxWidth = selfLayoutWidth(ctx.layout, ctx.width, naturalWidth);
56
- const bar = ctx.style(BAR, token);
57
- const innerWidth = Math.max(1, boxWidth - BAR.length);
58
- const placeLine = (line) => align === 'left' ? line : ctx.pad(line, innerWidth, align);
93
+ const bar = ctx.style(ctx.glyphs.calloutBar, token);
94
+ const innerWidth = Math.max(1, boxWidth - barWidth);
95
+ const placeLine = (line) => align === 'left' ? line : padText(line, innerWidth, align);
59
96
  const headerLine = `${bar}${placeLine(ctx.style(headerText, token))}`;
60
97
  const lines = [headerLine];
61
98
  if (titleText) {
62
99
  lines.push(`${bar}${placeLine(ctx.bold(titleText))}`);
63
100
  }
64
- const childrenText = children({ width: innerWidth });
65
- if (childrenText) {
66
- for (const line of childrenText.split('\n')) {
101
+ const body = childrenText(children);
102
+ if (body) {
103
+ for (const line of body.split('\n')) {
67
104
  lines.push(`${bar}${placeLine(line)}`);
68
105
  }
69
106
  }
@@ -1,4 +1,33 @@
1
- import type { AnsiComponent } from '../registry.js';
1
+ import type { ResolvedLayoutPresets } from '../layout.js';
2
+ import { type AnsiComponent, type DirectiveAttributes } from '../registry.js';
3
+ /** Frame border + one space of interior padding on each side. Exported: `render.tsx`'s Ink-`Box`-bordered ELEMENT builder needs the same overhead to go from a box width to an inner width. */
4
+ export declare const CARD_FRAME_OVERHEAD = 2;
5
+ /**
6
+ * `card`'s own natural/self-drawn OUTER (frame-including) width, given ONLY
7
+ * its own attributes, the resolved `width=`/`align=` layout preset, and the
8
+ * outer budget — never the body. Exported for `render.tsx`'s ELEMENT-mode
9
+ * builder, which needs the outer box width itself (to size its Ink `Box`)
10
+ * rather than only the inner budget `resolveCardInnerWidth` below returns.
11
+ */
12
+ export declare function resolveCardBoxWidth(attributes: DirectiveAttributes, layout: ResolvedLayoutPresets | undefined, outerWidth: number): number;
13
+ /**
14
+ * The inner budget `card`'s body renders at, given ONLY its own attributes
15
+ * and the outer width — never the body itself. This is deliberately a pure
16
+ * function callable BEFORE any children exist: `render.tsx`'s walk calls it
17
+ * to decide the width its OWN recursive children-builder uses, so `card`'s
18
+ * children are built ONCE, directly at the correct inner width, and never
19
+ * built wide and re-wrapped narrower afterward.
20
+ *
21
+ * That re-wrap-afterward shape was a real, shipped regression (caught
22
+ * post-phase-2 review): re-wrapping an ALREADY-RENDERED child block with a
23
+ * word-based wrapper corrupts anything in it that draws its own fixed-width
24
+ * glyphs (a nested `card`/`callout`'s frame, a nested `row`'s columns) — the
25
+ * word-wrapper has no notion that a run of box-drawing characters is not
26
+ * prose. Computing the inner width eagerly and building children exactly
27
+ * once at it is the fix, and it is what makes the deleted lazy-children API
28
+ * safe to have removed: this is its eager equivalent.
29
+ */
30
+ export declare function resolveCardInnerWidth(attributes: DirectiveAttributes, layout: ResolvedLayoutPresets | undefined, outerWidth: number): number;
2
31
  /**
3
32
  * `:::card{title="..." text=left|center|right} ... :::` — a titled panel.
4
33
  * `title` is optional; the title is woven into the frame's top edge, not a
@@ -7,6 +36,9 @@ import type { AnsiComponent } from '../registry.js';
7
36
  *
8
37
  * Registered `selfLayout` (see `callout.ts`'s doc comment for why): this
9
38
  * component draws a real box-drawn frame, which a generic post-render
10
- * narrow/pad would corrupt, so it reads `ctx.layout` and sizes its own frame.
39
+ * narrow/pad would corrupt, so it reads `ctx.layout` and sizes its own
40
+ * frame. `children` arrives ALREADY built at exactly this component's own
41
+ * inner width (`render.tsx`'s walk calls `resolveCardInnerWidth` above
42
+ * before building them) — this component never re-wraps.
11
43
  */
12
44
  export declare const Card: AnsiComponent;
@@ -1,13 +1,49 @@
1
- import { measure } from '../measure.js';
1
+ import { measureWidth, frameBlock, padText } from '../text-grid.js';
2
2
  import { selfLayoutAlign, selfLayoutWidth } from '../layout.js';
3
+ import { childrenText, } from '../registry.js';
3
4
  const TEXT_ALIGNS = ['left', 'center', 'right'];
4
5
  function isTextAlign(value) {
5
6
  return TEXT_ALIGNS.includes(value);
6
7
  }
7
- /** `width=fit` with no `title` and an unhelpfully-shaped body (nothing to measure a sensible box from) falls back to this column count rather than the full available width. */
8
+ /** `width=fit` with no `title` and an unhelpfully-shaped body falls back to this column count rather than the full available width. */
8
9
  const FIT_DEFAULT_WIDTH = 40;
9
- /** Frame border + one space of interior padding on each side. */
10
- const FRAME_OVERHEAD = 2;
10
+ /** Frame border + one space of interior padding on each side. Exported: `render.tsx`'s Ink-`Box`-bordered ELEMENT builder needs the same overhead to go from a box width to an inner width. */
11
+ export const CARD_FRAME_OVERHEAD = 2;
12
+ /**
13
+ * `card`'s own natural/self-drawn OUTER (frame-including) width, given ONLY
14
+ * its own attributes, the resolved `width=`/`align=` layout preset, and the
15
+ * outer budget — never the body. Exported for `render.tsx`'s ELEMENT-mode
16
+ * builder, which needs the outer box width itself (to size its Ink `Box`)
17
+ * rather than only the inner budget `resolveCardInnerWidth` below returns.
18
+ */
19
+ export function resolveCardBoxWidth(attributes, layout, outerWidth) {
20
+ const title = attributes.title ?? null;
21
+ const naturalWidth = title
22
+ ? measureWidth(title) + CARD_FRAME_OVERHEAD * 2
23
+ : FIT_DEFAULT_WIDTH;
24
+ return selfLayoutWidth(layout, outerWidth, naturalWidth);
25
+ }
26
+ /**
27
+ * The inner budget `card`'s body renders at, given ONLY its own attributes
28
+ * and the outer width — never the body itself. This is deliberately a pure
29
+ * function callable BEFORE any children exist: `render.tsx`'s walk calls it
30
+ * to decide the width its OWN recursive children-builder uses, so `card`'s
31
+ * children are built ONCE, directly at the correct inner width, and never
32
+ * built wide and re-wrapped narrower afterward.
33
+ *
34
+ * That re-wrap-afterward shape was a real, shipped regression (caught
35
+ * post-phase-2 review): re-wrapping an ALREADY-RENDERED child block with a
36
+ * word-based wrapper corrupts anything in it that draws its own fixed-width
37
+ * glyphs (a nested `card`/`callout`'s frame, a nested `row`'s columns) — the
38
+ * word-wrapper has no notion that a run of box-drawing characters is not
39
+ * prose. Computing the inner width eagerly and building children exactly
40
+ * once at it is the fix, and it is what makes the deleted lazy-children API
41
+ * safe to have removed: this is its eager equivalent.
42
+ */
43
+ export function resolveCardInnerWidth(attributes, layout, outerWidth) {
44
+ const boxWidth = resolveCardBoxWidth(attributes, layout, outerWidth);
45
+ return Math.max(1, boxWidth - CARD_FRAME_OVERHEAD);
46
+ }
11
47
  /**
12
48
  * `:::card{title="..." text=left|center|right} ... :::` — a titled panel.
13
49
  * `title` is optional; the title is woven into the frame's top edge, not a
@@ -16,29 +52,30 @@ const FRAME_OVERHEAD = 2;
16
52
  *
17
53
  * Registered `selfLayout` (see `callout.ts`'s doc comment for why): this
18
54
  * component draws a real box-drawn frame, which a generic post-render
19
- * narrow/pad would corrupt, so it reads `ctx.layout` and sizes its own frame.
55
+ * narrow/pad would corrupt, so it reads `ctx.layout` and sizes its own
56
+ * frame. `children` arrives ALREADY built at exactly this component's own
57
+ * inner width (`render.tsx`'s walk calls `resolveCardInnerWidth` above
58
+ * before building them) — this component never re-wraps.
20
59
  */
21
- export const Card = (attributes, children, ctx) => {
60
+ export const Card = ({ attributes, children, ctx }) => {
22
61
  const title = attributes.title ?? null;
23
62
  const titleText = title ? ctx.text(title) : undefined;
24
63
  const rawTextAlign = attributes.text;
25
64
  const align = rawTextAlign && isTextAlign(rawTextAlign) ? rawTextAlign : 'left';
26
- const naturalWidth = titleText
27
- ? measure(titleText) + FRAME_OVERHEAD * 2
28
- : FIT_DEFAULT_WIDTH;
29
- const boxWidth = selfLayoutWidth(ctx.layout, ctx.width, naturalWidth);
30
- const innerWidth = Math.max(1, boxWidth - FRAME_OVERHEAD);
31
- const childrenText = children({ width: innerWidth });
32
- const body = childrenText
33
- ? childrenText
65
+ const boxWidth = resolveCardBoxWidth({ ...attributes, title: titleText ?? null }, ctx.layout, ctx.width);
66
+ const innerWidth = Math.max(1, boxWidth - CARD_FRAME_OVERHEAD);
67
+ const bodyText = childrenText(children);
68
+ const body = align === 'left'
69
+ ? bodyText
70
+ : bodyText
34
71
  .split('\n')
35
- .map((line) => align === 'left' ? line : ctx.pad(line, innerWidth, align))
36
- .join('\n')
37
- : '';
38
- const framed = ctx.frame(body, {
72
+ .map((line) => padText(line, innerWidth, align))
73
+ .join('\n');
74
+ const framed = frameBlock(body, {
39
75
  style: 'solid',
40
76
  title: titleText,
41
77
  width: boxWidth,
78
+ glyphs: ctx.glyphs,
42
79
  });
43
80
  return selfLayoutAlign(framed, ctx.layout, ctx.width);
44
81
  };
@@ -1,18 +1,12 @@
1
- import type { AnsiComponent } from '../registry.js';
1
+ import { type AnsiComponent } from '../registry.js';
2
2
  /**
3
3
  * `:::cell ... :::` — a transparent grouping container whose only job is
4
4
  * letting several blocks count as ONE cell of `:::row`.
5
5
  *
6
- * `row.ts` no longer discovers its cells by splitting a flattened string on
7
- * a blank-line heuristic: it walks its own directive's top-level children
8
- * directly (`registry.ts`'s `AnsiChildren.parts`), so a `cell` grouping more
9
- * than one block renders those blocks exactly as it would standalone,
10
- * blank line and all, with no collapsing needed to keep a cell boundary
11
- * unambiguous for `row.ts`.
12
- *
13
6
  * `text` aligns this cell's own content when rendered STANDALONE (outside a
14
- * `row`); nested inside a `row`, the row's own `text` decides every cell's
15
- * alignment uniformly instead, for the same reason `row.ts` does not look
16
- * inside a cell's own attributes when it places columns.
7
+ * `row`) or nested inside one: `render.tsx`'s `row` handling still runs
8
+ * each cell through the ordinary directive pipeline (so this component's
9
+ * own alignment applies first), then applies `row`'s OWN `text` uniformly
10
+ * on top — see `components/row.ts`'s doc comment.
17
11
  */
18
12
  export declare const Cell: AnsiComponent;
@@ -1,3 +1,5 @@
1
+ import { padText } from '../text-grid.js';
2
+ import { childrenText } from '../registry.js';
1
3
  const TEXT_ALIGNS = ['left', 'center', 'right'];
2
4
  function isTextAlign(value) {
3
5
  return TEXT_ALIGNS.includes(value);
@@ -6,26 +8,20 @@ function isTextAlign(value) {
6
8
  * `:::cell ... :::` — a transparent grouping container whose only job is
7
9
  * letting several blocks count as ONE cell of `:::row`.
8
10
  *
9
- * `row.ts` no longer discovers its cells by splitting a flattened string on
10
- * a blank-line heuristic: it walks its own directive's top-level children
11
- * directly (`registry.ts`'s `AnsiChildren.parts`), so a `cell` grouping more
12
- * than one block renders those blocks exactly as it would standalone,
13
- * blank line and all, with no collapsing needed to keep a cell boundary
14
- * unambiguous for `row.ts`.
15
- *
16
11
  * `text` aligns this cell's own content when rendered STANDALONE (outside a
17
- * `row`); nested inside a `row`, the row's own `text` decides every cell's
18
- * alignment uniformly instead, for the same reason `row.ts` does not look
19
- * inside a cell's own attributes when it places columns.
12
+ * `row`) or nested inside one: `render.tsx`'s `row` handling still runs
13
+ * each cell through the ordinary directive pipeline (so this component's
14
+ * own alignment applies first), then applies `row`'s OWN `text` uniformly
15
+ * on top — see `components/row.ts`'s doc comment.
20
16
  */
21
- export const Cell = (attributes, children, ctx) => {
17
+ export const Cell = ({ attributes, children, ctx }) => {
22
18
  const rawTextAlign = attributes.text;
23
19
  const align = rawTextAlign && isTextAlign(rawTextAlign) ? rawTextAlign : undefined;
24
- const childrenText = children();
20
+ const text = childrenText(children);
25
21
  if (!align || align === 'left')
26
- return childrenText;
27
- return childrenText
22
+ return text;
23
+ return text
28
24
  .split('\n')
29
- .map((line) => ctx.pad(line, ctx.width, align))
25
+ .map((line) => padText(line, ctx.width, align))
30
26
  .join('\n');
31
27
  };
@@ -1,15 +1,9 @@
1
1
  import type { AnsiComponent } from '../registry.js';
2
2
  /**
3
3
  * `::chart{kind=line|bar values="1,3,2,5"}` — a dependency-free, hand-rolled
4
- * chart. Data binding (§8) mirrors `@markii/html`'s `Chart`: a bound `data`
5
- * array of numbers (or `{value}` objects) takes priority over the static
6
- * `values=` attribute; non-numeric/non-finite entries are dropped and the
7
- * point count capped at `MAX_POINTS`. An empty or all-invalid series renders
8
- * a small neutral "no data" line rather than a broken chart.
9
- *
10
- * Terminal form: a `line` chart is a single sparkline row (`▁▂▃▄▅▆▇█`),
11
- * flanked by its minimum and maximum as plain numbers. A `bar` chart is one
12
- * horizontal `█` bar per point, its own value as the row's label, labels
13
- * right-aligned to the widest one so every bar starts at the same column.
4
+ * chart. Data binding (§8): a bound `data` array of numbers (or `{value}`
5
+ * objects) takes priority over the static `values=` attribute. Terminal
6
+ * form: a `line` chart is a single sparkline row, a `bar` chart is one
7
+ * horizontal bar per point.
14
8
  */
15
9
  export declare const Chart: AnsiComponent;