@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
@@ -1,5 +1,4 @@
1
- import { pad } from '../box.js';
2
- import { measure } from '../measure.js';
1
+ import { padText, measureWidth } from '../text-grid.js';
3
2
  import { safeRead } from '../resolve.js';
4
3
  import { dataStateSuffix, failureToken } from '../failure-presentation.js';
5
4
  const CHART_KINDS = ['line', 'bar'];
@@ -30,7 +29,7 @@ function coercePoint(entry) {
30
29
  }
31
30
  return undefined;
32
31
  }
33
- /** Builds the numeric point series to plot, a bound `data` array taking priority over the static `values=` attribute. Ported from `@markii/html`'s `resolvePoints`. */
32
+ /** Builds the numeric point series to plot, a bound `data` array taking priority over the static `values=` attribute. */
34
33
  function resolvePoints(data, dataStatus, rawValues) {
35
34
  if (dataStatus !== 'missing' &&
36
35
  dataStatus !== 'error' &&
@@ -58,26 +57,25 @@ function resolvePoints(data, dataStatus, rawValues) {
58
57
  }
59
58
  return [];
60
59
  }
61
- /** `▁▂▃▄▅▆▇█`, the eight-step block-height ramp a line chart's sparkline row is built from. */
62
- const SPARKLINE_CHARS = '▁▂▃▄▅▆▇█';
63
60
  function formatPoint(value) {
64
61
  return Number.isFinite(value)
65
62
  ? String(Math.round(value * 100) / 100)
66
63
  : String(value);
67
64
  }
68
- function sparkline(points) {
65
+ /** One sparkline row over `ramp`, the render's eight-step `GlyphSet.sparkline`. */
66
+ function sparkline(points, ramp) {
69
67
  const min = Math.min(...points);
70
68
  const max = Math.max(...points);
71
69
  const range = max - min;
72
70
  return points
73
71
  .map((point) => {
74
72
  const normalized = range > 0 ? (point - min) / range : 0.5;
75
- const index = Math.min(SPARKLINE_CHARS.length - 1, Math.max(0, Math.round(normalized * (SPARKLINE_CHARS.length - 1))));
76
- return SPARKLINE_CHARS[index];
73
+ const index = Math.min(ramp.length - 1, Math.max(0, Math.round(normalized * (ramp.length - 1))));
74
+ return ramp[index];
77
75
  })
78
76
  .join('');
79
77
  }
80
- /** `██████` bar length for `point`, scaled against the series' largest magnitude and capped at `budget` columns. */
78
+ /** A bar's length for `point`, scaled against the series' largest magnitude and capped at `budget` columns. */
81
79
  function barLength(point, maxMagnitude, budget) {
82
80
  if (maxMagnitude <= 0)
83
81
  return 0;
@@ -85,18 +83,12 @@ function barLength(point, maxMagnitude, budget) {
85
83
  }
86
84
  /**
87
85
  * `::chart{kind=line|bar values="1,3,2,5"}` — a dependency-free, hand-rolled
88
- * chart. Data binding (§8) mirrors `@markii/html`'s `Chart`: a bound `data`
89
- * array of numbers (or `{value}` objects) takes priority over the static
90
- * `values=` attribute; non-numeric/non-finite entries are dropped and the
91
- * point count capped at `MAX_POINTS`. An empty or all-invalid series renders
92
- * a small neutral "no data" line rather than a broken chart.
93
- *
94
- * Terminal form: a `line` chart is a single sparkline row (`▁▂▃▄▅▆▇█`),
95
- * flanked by its minimum and maximum as plain numbers. A `bar` chart is one
96
- * horizontal `█` bar per point, its own value as the row's label, labels
97
- * right-aligned to the widest one so every bar starts at the same column.
86
+ * chart. Data binding (§8): a bound `data` array of numbers (or `{value}`
87
+ * objects) takes priority over the static `values=` attribute. Terminal
88
+ * form: a `line` chart is a single sparkline row, a `bar` chart is one
89
+ * horizontal bar per point.
98
90
  */
99
- export const Chart = (attributes, _children, ctx) => {
91
+ export const Chart = ({ attributes, ctx }) => {
100
92
  const { data, dataStatus, dataFailureKind } = ctx;
101
93
  const rawKind = attributes.kind ?? DEFAULT_KIND;
102
94
  const kind = isChartKind(rawKind) ? rawKind : DEFAULT_KIND;
@@ -115,15 +107,15 @@ export const Chart = (attributes, _children, ctx) => {
115
107
  if (kind === 'line') {
116
108
  const min = ctx.dim(formatPoint(Math.min(...points)));
117
109
  const max = ctx.dim(formatPoint(Math.max(...points)));
118
- return `${min} ${sparkline(points)} ${max}${styledSuffix}`;
110
+ return `${min} ${sparkline(points, ctx.glyphs.sparkline)} ${max}${styledSuffix}`;
119
111
  }
120
112
  const labels = points.map((point) => formatPoint(point));
121
- const labelWidth = Math.max(...labels.map((label) => measure(label)));
113
+ const labelWidth = Math.max(...labels.map((label) => measureWidth(label)));
122
114
  const budget = Math.max(1, ctx.width - labelWidth - 1);
123
115
  const maxMagnitude = Math.max(...points.map((point) => Math.abs(point)));
124
116
  const lines = points.map((point, index) => {
125
- const bar = '█'.repeat(barLength(point, maxMagnitude, budget));
126
- return `${pad(labels[index], labelWidth, 'right')} ${bar}`;
117
+ const bar = ctx.glyphs.barFilled.repeat(barLength(point, maxMagnitude, budget));
118
+ return `${padText(labels[index], labelWidth, 'right')} ${bar}`;
127
119
  });
128
120
  return `${lines.join('\n')}${styledSuffix}`;
129
121
  };
@@ -0,0 +1,26 @@
1
+ import { type AnsiComponent } from '../registry.js';
2
+ /** Columns the indent prefix (" ") consumes from the enclosing width. */
3
+ export declare const DETAILS_INDENT_WIDTH = 2;
4
+ /**
5
+ * The inner budget `details`'s body renders at, given only the outer width
6
+ * — never the body itself. Mirrors `card.ts`'s `resolveCardInnerWidth`:
7
+ * `render.tsx`'s walk calls this BEFORE building children (for both the
8
+ * STRING-mode fallback below and the real interactive element builder in
9
+ * `details.tsx`), so the body — and anything self-drawing nested inside it
10
+ * — is built exactly once, at the correct width, never re-wrapped
11
+ * afterward.
12
+ */
13
+ export declare function resolveDetailsInnerWidth(outerWidth: number): number;
14
+ /**
15
+ * The STRING-mode `details` registration (used by `render.tsx`'s registry
16
+ * lookups, and as the fallback rendering when a `details` directive is
17
+ * reached through STRING mode — nested inside a self-drawing container's
18
+ * own body, see `render.tsx`'s top comment). The real Ink/interactive
19
+ * version `render.tsx` builds directly for a `details` reached through
20
+ * ELEMENT mode is `InteractiveDetails` in `./details.tsx`; the two share no
21
+ * code because this one can only ever show the body (a plain string has no
22
+ * expand/collapse affordance), exactly like the pre-Ink engine. `children`
23
+ * arrives ALREADY built at exactly `resolveDetailsInnerWidth`'s width —
24
+ * this component never re-wraps.
25
+ */
26
+ export declare const Details: AnsiComponent;
@@ -0,0 +1,39 @@
1
+ import { indentBlock } from '../text-grid.js';
2
+ import { childrenText } from '../registry.js';
3
+ const DEFAULT_TITLE = 'Details';
4
+ /** Columns the indent prefix (" ") consumes from the enclosing width. */
5
+ export const DETAILS_INDENT_WIDTH = 2;
6
+ /**
7
+ * The inner budget `details`'s body renders at, given only the outer width
8
+ * — never the body itself. Mirrors `card.ts`'s `resolveCardInnerWidth`:
9
+ * `render.tsx`'s walk calls this BEFORE building children (for both the
10
+ * STRING-mode fallback below and the real interactive element builder in
11
+ * `details.tsx`), so the body — and anything self-drawing nested inside it
12
+ * — is built exactly once, at the correct width, never re-wrapped
13
+ * afterward.
14
+ */
15
+ export function resolveDetailsInnerWidth(outerWidth) {
16
+ return Math.max(1, outerWidth - DETAILS_INDENT_WIDTH);
17
+ }
18
+ /**
19
+ * The STRING-mode `details` registration (used by `render.tsx`'s registry
20
+ * lookups, and as the fallback rendering when a `details` directive is
21
+ * reached through STRING mode — nested inside a self-drawing container's
22
+ * own body, see `render.tsx`'s top comment). The real Ink/interactive
23
+ * version `render.tsx` builds directly for a `details` reached through
24
+ * ELEMENT mode is `InteractiveDetails` in `./details.tsx`; the two share no
25
+ * code because this one can only ever show the body (a plain string has no
26
+ * expand/collapse affordance), exactly like the pre-Ink engine. `children`
27
+ * arrives ALREADY built at exactly `resolveDetailsInnerWidth`'s width —
28
+ * this component never re-wraps.
29
+ */
30
+ export const Details = ({ attributes, children, ctx }) => {
31
+ const title = attributes.title ?? DEFAULT_TITLE;
32
+ const open = Object.hasOwn(attributes, 'open');
33
+ const glyph = open ? ctx.glyphs.foldOpen : ctx.glyphs.foldClosed;
34
+ const marker = ctx.dim(`${glyph} ${ctx.bold(ctx.text(title))} (collapsible section, shown expanded)`);
35
+ const body = childrenText(children);
36
+ if (!body)
37
+ return marker;
38
+ return `${marker}\n${indentBlock(body, ' ')}`;
39
+ };
@@ -1,12 +1,34 @@
1
- import type { AnsiComponent } from '../registry.js';
1
+ import type { GlyphSet } from '../glyphs.js';
2
+ import type { ReactNode } from 'react';
3
+ import type { ColorLevel } from '../ansi.js';
2
4
  /**
3
- * `:::details{title="..." open} ... :::` — a collapsible disclosure. A
4
- * terminal has no collapse/expand affordance (see `render.ts`'s top comment
5
- * on interactive components), so this ALWAYS shows the body: the bare
6
- * `open` attribute instead only changes the marker glyph (`▾` for `open`,
7
- * `▸` for folded-by-default), a quiet hint at the note's own authored
8
- * default rather than a control that does anything here. The marker line
9
- * also names the section a "collapsible section" so a reader understands
10
- * why they are seeing an indented block with no directive name attached.
5
+ * `:::details{title="..." open} ... :::` — a collapsible disclosure, built
6
+ * as a real Ink element (`render.tsx`'s `renderDetailsElement` calls this
7
+ * directly, the same way it calls `InteractiveTabs` for `tabs` — see
8
+ * `registry.ts`'s and `render.tsx`'s doc comments).
9
+ *
10
+ * Non-interactive (the default, and the render-once path): ALWAYS shows the
11
+ * body, exactly like the pre-Ink engine — a static render has no
12
+ * expand/collapse affordance. The bare `open` attribute only changes the
13
+ * marker glyph (`▾` for `open`, `▸` for folded-by-default), a quiet hint at
14
+ * the note's own authored default.
15
+ *
16
+ * Interactive: starts CLOSED unless `open` was given; Enter toggles it when
17
+ * this `details` instance is focused (`useIsFocused`). When focused, the
18
+ * summary line also carries a dim hint (`enter to open`/`enter to close`,
19
+ * matching the current state) and the whole line is drawn in the accent
20
+ * color with a leading `› ` glyph (`useFocusMarker`), so focus is visible
21
+ * even on a theme with no perceptible accent color; unfocused, the line
22
+ * gets two leading spaces instead, so nothing shifts horizontally when
23
+ * focus moves onto or off of this block.
11
24
  */
12
- export declare const Details: AnsiComponent;
25
+ export interface InteractiveDetailsProps {
26
+ title: string;
27
+ body: ReactNode;
28
+ defaultOpen: boolean;
29
+ color: ColorLevel;
30
+ glyphs: GlyphSet;
31
+ interactive: boolean;
32
+ focusId: number | undefined;
33
+ }
34
+ export declare function InteractiveDetails({ title, body, defaultOpen, color, glyphs, interactive, focusId, }: InteractiveDetailsProps): ReactNode;
@@ -1,25 +1,28 @@
1
- import { indentBlock } from '../box.js';
2
- const DEFAULT_TITLE = 'Details';
3
- /** Columns the indent prefix (" ") consumes from the enclosing width. */
4
- const INDENT_WIDTH = 2;
5
- /**
6
- * `:::details{title="..." open} ... :::` — a collapsible disclosure. A
7
- * terminal has no collapse/expand affordance (see `render.ts`'s top comment
8
- * on interactive components), so this ALWAYS shows the body: the bare
9
- * `open` attribute instead only changes the marker glyph (`▾` for `open`,
10
- * `▸` for folded-by-default), a quiet hint at the note's own authored
11
- * default rather than a control that does anything here. The marker line
12
- * also names the section a "collapsible section" so a reader understands
13
- * why they are seeing an indented block with no directive name attached.
14
- */
15
- export const Details = (attributes, children, ctx) => {
16
- const title = attributes.title ?? DEFAULT_TITLE;
17
- const open = Object.hasOwn(attributes, 'open');
18
- const glyph = open ? '▾' : '▸';
19
- const marker = ctx.dim(`${glyph} ${ctx.bold(ctx.text(title))} (collapsible section, shown expanded)`);
20
- const innerWidth = Math.max(1, ctx.width - INDENT_WIDTH);
21
- const childrenText = children({ width: innerWidth });
22
- if (!childrenText)
23
- return marker;
24
- return `${marker}\n${indentBlock(childrenText, ' ')}`;
25
- };
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import { bold, dim } from '../ansi.js';
5
+ import { useFocusAnchor, useFocusMarker, useIsFocused, } from '../interactive.js';
6
+ export function InteractiveDetails({ title, body, defaultOpen, color, glyphs, interactive, focusId, }) {
7
+ const [open, setOpen] = useState(defaultOpen);
8
+ const focused = useIsFocused(focusId);
9
+ const anchor = useFocusAnchor(focusId);
10
+ useInput((_input, key) => {
11
+ if (key.return)
12
+ setOpen((current) => !current);
13
+ }, { isActive: interactive && focused });
14
+ // Every hook this component calls runs BEFORE the non-interactive early
15
+ // return below, so the hook order is identical in both modes. The summary
16
+ // line is cheap to build and unused in the non-interactive branch; that
17
+ // waste is the price of never making a hook call conditional.
18
+ const openGlyph = open ? glyphs.foldOpen : glyphs.foldClosed;
19
+ const hint = focused ? (open ? ' enter to close' : ' enter to open') : '';
20
+ const summaryLine = dim(`${openGlyph} ${bold(title, color)}${hint}`, color);
21
+ const focusGlyph = focused ? glyphs.focus : ' ';
22
+ const headingLine = useFocusMarker(`${focusGlyph}${summaryLine}`, focusId);
23
+ if (!interactive) {
24
+ const glyph = defaultOpen ? glyphs.foldOpen : glyphs.foldClosed;
25
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: dim(`${glyph} ${bold(title, color)} (collapsible section, shown expanded)`, color) }), _jsx(Box, { marginLeft: 2, flexDirection: "column", children: body })] }));
26
+ }
27
+ return (_jsxs(Box, { ref: anchor, flexDirection: "column", children: [_jsx(Text, { children: headingLine }), open && (_jsx(Box, { marginLeft: 2, flexDirection: "column", children: body }))] }));
28
+ }
@@ -1,4 +1,4 @@
1
- import { measure } from '../measure.js';
1
+ import { measureWidth, padText, ruleLine } from '../text-grid.js';
2
2
  import { selfLayoutAlign, selfLayoutWidth } from '../layout.js';
3
3
  const DIVIDER_VARIANTS = [
4
4
  'line',
@@ -12,15 +12,13 @@ const LABEL_ALIGNS = ['left', 'center', 'right'];
12
12
  function isDividerLabelAlign(value) {
13
13
  return LABEL_ALIGNS.includes(value);
14
14
  }
15
- const ORNAMENT_GLYPH = '❖';
16
15
  /** The rule character each non-ornament variant draws with. */
17
- const RULE_CHAR = { line: '─', dots: '·' };
18
16
  /** How much of a labeled rule's rule fill sits on the SHORT side when the label is pushed left/right (the rest fills the long side). */
19
17
  const SHORT_SIDE_RULE = 2;
20
18
  /** Builds a full-`width` rule with `label` woven in at `align`, e.g. `── Part 2 ─────────`. */
21
19
  function labeledRule(ruleChar, label, width, align) {
22
20
  const labelText = ` ${label} `;
23
- const remaining = Math.max(0, width - measure(labelText));
21
+ const remaining = Math.max(0, width - measureWidth(labelText));
24
22
  let left;
25
23
  if (align === 'left')
26
24
  left = Math.min(SHORT_SIDE_RULE, remaining);
@@ -42,15 +40,13 @@ function labeledRule(ruleChar, label, width, align) {
42
40
  * within the width. Dimmed, matching the plain thematic-break rendering.
43
41
  *
44
42
  * Registered `selfLayout` (`registry.ts`'s `AnsiRegistryEntry.selfLayout`):
45
- * a full-width rule is ONE long run with no spaces in it, so a generic
43
+ * a full-width rule is ONE long run with no spaces in it, so the generic
46
44
  * post-render `applyLayout` narrowing would treat the whole rule as a
47
- * single over-long "word" and hard-break it into several shorter lines
48
- * (`../box.ts`'s `wrap`'s `breakLongWord`) instead of drawing one shorter
49
- * rule — this component instead reads `ctx.layout` and draws its own rule
50
- * at the resolved width from the start, exactly like `card`/`callout`.
45
+ * single over-long "word" and hard-break it — this component instead reads
46
+ * `ctx.layout` and draws its own rule at the resolved width from the start.
51
47
  */
52
48
  const FIT_DEFAULT_WIDTH = 10;
53
- export const Divider = (attributes, _children, ctx) => {
49
+ export const Divider = ({ attributes, ctx }) => {
54
50
  const rawVariant = attributes.variant ?? 'line';
55
51
  const variant = isDividerVariant(rawVariant)
56
52
  ? rawVariant
@@ -61,20 +57,20 @@ export const Divider = (attributes, _children, ctx) => {
61
57
  const labelAlign = rawLabelAlign && isDividerLabelAlign(rawLabelAlign)
62
58
  ? rawLabelAlign
63
59
  : 'center';
64
- const naturalWidth = label ? measure(label) + 4 : FIT_DEFAULT_WIDTH;
60
+ const naturalWidth = label ? measureWidth(label) + 4 : FIT_DEFAULT_WIDTH;
65
61
  const width = selfLayoutWidth(ctx.layout, ctx.width, naturalWidth);
66
62
  let line;
67
63
  if (variant === 'ornament') {
68
64
  const inner = label
69
- ? `${ORNAMENT_GLYPH} ${label} ${ORNAMENT_GLYPH}`
70
- : ORNAMENT_GLYPH;
71
- line = ctx.pad(inner, width, labelAlign);
65
+ ? `${ctx.glyphs.ornament} ${label} ${ctx.glyphs.ornament}`
66
+ : ctx.glyphs.ornament;
67
+ line = padText(inner, width, labelAlign);
72
68
  }
73
69
  else {
74
- const ruleChar = RULE_CHAR[variant];
70
+ const ruleChar = variant === 'dots' ? ctx.glyphs.ruleDots : ctx.glyphs.rule;
75
71
  line = label
76
72
  ? labeledRule(ruleChar, label, width, labelAlign)
77
- : ctx.rule(width, ruleChar);
73
+ : ruleLine(width, ruleChar);
78
74
  }
79
75
  return selfLayoutAlign(ctx.dim(line), ctx.layout, ctx.width);
80
76
  };
@@ -1,20 +1,9 @@
1
- import type { AnsiComponent } from '../registry.js';
1
+ import { type AnsiComponent } from '../registry.js';
2
2
  /**
3
3
  * `:::figure{src="..." alt="..."} caption markdown :::` — an image with a
4
- * rich (markdown) caption. `src` is required; a missing `src` shows only the
5
- * caption, matching the graceful-degradation spirit of the unknown-directive
6
- * fallback rather than throwing.
7
- *
8
- * Security: `src` bypasses `@markii/core`'s hast-level URL sanitizer (it is
9
- * a directive attribute, not a markdown image), so this component closes
10
- * that gap itself with `@markii/core`'s `isSafeUrl` — the same allowlist the
11
- * sanitizer uses — and drops the image line entirely on a refusal, replacing
12
- * it with a short labeled marker (a terminal has no tooltip to carry the
13
- * full sentence out of the text flow) plus the full sentence reported to
14
- * `onDiagnostic`; matches `@markii/html`'s `Figure` in behavior, not in the
15
- * inline wording.
16
- *
17
- * Terminal form: alt text, then the (possibly host-resolved) src, then the
18
- * caption, each on its own line, dimmed except the caption itself.
4
+ * rich (markdown) caption. `src` is required; a missing `src` shows only
5
+ * the caption. Security: `src` bypasses `@markii/core`'s hast-level URL
6
+ * sanitizer (it is a directive attribute, not a markdown image), so this
7
+ * component closes that gap itself with `isSafeUrl`.
19
8
  */
20
9
  export declare const Figure: AnsiComponent;
@@ -2,27 +2,17 @@ import { isSafeUrl } from '@markii/core';
2
2
  import { reportDiagnostic } from '@markii/stdlib';
3
3
  import { resolveImageAttribute } from '../image-resolve.js';
4
4
  import { unsafeImageSrcLabel, unsafeImageSrcTitle, } from '../failure-presentation.js';
5
+ import { childrenText } from '../registry.js';
5
6
  const DEFAULT_ALT = '';
6
7
  const DIRECTIVE_NAME = 'figure';
7
8
  /**
8
9
  * `:::figure{src="..." alt="..."} caption markdown :::` — an image with a
9
- * rich (markdown) caption. `src` is required; a missing `src` shows only the
10
- * caption, matching the graceful-degradation spirit of the unknown-directive
11
- * fallback rather than throwing.
12
- *
13
- * Security: `src` bypasses `@markii/core`'s hast-level URL sanitizer (it is
14
- * a directive attribute, not a markdown image), so this component closes
15
- * that gap itself with `@markii/core`'s `isSafeUrl` — the same allowlist the
16
- * sanitizer uses — and drops the image line entirely on a refusal, replacing
17
- * it with a short labeled marker (a terminal has no tooltip to carry the
18
- * full sentence out of the text flow) plus the full sentence reported to
19
- * `onDiagnostic`; matches `@markii/html`'s `Figure` in behavior, not in the
20
- * inline wording.
21
- *
22
- * Terminal form: alt text, then the (possibly host-resolved) src, then the
23
- * caption, each on its own line, dimmed except the caption itself.
10
+ * rich (markdown) caption. `src` is required; a missing `src` shows only
11
+ * the caption. Security: `src` bypasses `@markii/core`'s hast-level URL
12
+ * sanitizer (it is a directive attribute, not a markdown image), so this
13
+ * component closes that gap itself with `isSafeUrl`.
24
14
  */
25
- export const Figure = (attributes, children, ctx) => {
15
+ export const Figure = ({ attributes, children, ctx }) => {
26
16
  const rawSrc = attributes.src ?? null;
27
17
  const alt = attributes.alt ?? DEFAULT_ALT;
28
18
  const refused = Boolean(rawSrc) && !isSafeUrl(rawSrc);
@@ -46,8 +36,8 @@ export const Figure = (attributes, children, ctx) => {
46
36
  lines.push(ctx.dim(src));
47
37
  }
48
38
  }
49
- const childrenText = children();
50
- if (childrenText)
51
- lines.push(childrenText);
39
+ const body = childrenText(children);
40
+ if (body)
41
+ lines.push(body);
52
42
  return lines.join('\n');
53
43
  };
@@ -6,7 +6,8 @@ export type { CalloutType } from './callout.js';
6
6
  export { Card } from './card.js';
7
7
  export { Cell } from './cell.js';
8
8
  export { Chart } from './chart.js';
9
- export { Details } from './details.js';
9
+ export { Details } from './details-string.js';
10
+ export { InteractiveDetails } from './details.js';
10
11
  export { Divider } from './divider.js';
11
12
  export type { DividerVariant } from './divider.js';
12
13
  export { Figure } from './figure.js';
@@ -15,20 +16,23 @@ export { createLayoutWrapper, layoutWrapperPresetAxis, LAYOUT_WRAPPER_PRESETS, }
15
16
  export type { LayoutWrapperPreset } from './layout-wrapper.js';
16
17
  export { Progress } from './progress.js';
17
18
  export { Rating } from './rating.js';
18
- export { Row, ROW_COLUMN_THRESHOLD } from './row.js';
19
+ export { Row, ROW_COLUMN_THRESHOLD, resolveRowLayout } from './row.js';
19
20
  export { Stat } from './stat.js';
20
21
  export { Tab, DEFAULT_TAB_LABEL } from './tab.js';
21
- export { Tabs } from './tabs.js';
22
+ export { Tabs } from './tabs-string.js';
23
+ export { InteractiveTabs } from './tabs.js';
22
24
  export { Table } from './table.js';
23
25
  export { drawTableGrid, measureTableGridWidth, MIN_COLUMN_WIDTH, } from './table-grid.js';
24
26
  /**
25
- * The built-in standard components, pre-registered under their names —
26
- * matching `@markii/html`'s `defaultHtmlRegistry` and `@markii/react`'s
27
- * `defaultRegistry` in shape and coverage: the same 23 names. `card`,
28
- * `callout`, `divider`, and `table` are marked `selfLayout` (`registry.ts`'s
29
- * `AnsiRegistryEntry.selfLayout`) because they draw a single long
27
+ * The built-in standard components, pre-registered under their names: the
28
+ * same 23 names as `@markii/html`'s `defaultHtmlRegistry` and
29
+ * `@markii/react`'s `defaultRegistry`. `card`, `callout`, `divider`, and
30
+ * `table` are marked `selfLayout` because they draw a single long
30
31
  * frame/bar/rule/grid a generic post-render `applyLayout` narrow/pad would
31
- * corrupt or hard-break mid-glyph; see `card.ts`'s and `divider.ts`'s doc
32
- * comments.
32
+ * corrupt or hard-break mid-glyph. `row`, `tabs`, and `details` are the
33
+ * three directives `render.tsx` gives real Ink layout/state to directly
34
+ * (see `registry.ts`'s and `render.tsx`'s doc comments); their REGISTERED
35
+ * `component` here is the STRING-mode fallback, reached only when one of
36
+ * them is nested inside a self-drawing container's own body.
33
37
  */
34
38
  export declare const defaultAnsiRegistry: AnsiRegistry;
@@ -5,7 +5,7 @@ import { Callout } from './callout.js';
5
5
  import { Card } from './card.js';
6
6
  import { Cell } from './cell.js';
7
7
  import { Chart } from './chart.js';
8
- import { Details } from './details.js';
8
+ import { Details } from './details-string.js';
9
9
  import { Divider } from './divider.js';
10
10
  import { Figure } from './figure.js';
11
11
  import { Kbd } from './kbd.js';
@@ -15,31 +15,33 @@ import { Rating } from './rating.js';
15
15
  import { Row } from './row.js';
16
16
  import { Stat } from './stat.js';
17
17
  import { Tab } from './tab.js';
18
- import { Tabs } from './tabs.js';
18
+ import { Tabs } from './tabs-string.js';
19
19
  import { Table } from './table.js';
20
20
  export { Badge } from './badge.js';
21
21
  export { Callout } from './callout.js';
22
22
  export { Card } from './card.js';
23
23
  export { Cell } from './cell.js';
24
24
  export { Chart } from './chart.js';
25
- export { Details } from './details.js';
25
+ export { Details } from './details-string.js';
26
+ export { InteractiveDetails } from './details.js';
26
27
  export { Divider } from './divider.js';
27
28
  export { Figure } from './figure.js';
28
29
  export { Kbd } from './kbd.js';
29
30
  export { createLayoutWrapper, layoutWrapperPresetAxis, LAYOUT_WRAPPER_PRESETS, } from './layout-wrapper.js';
30
31
  export { Progress } from './progress.js';
31
32
  export { Rating } from './rating.js';
32
- export { Row, ROW_COLUMN_THRESHOLD } from './row.js';
33
+ export { Row, ROW_COLUMN_THRESHOLD, resolveRowLayout } from './row.js';
33
34
  export { Stat } from './stat.js';
34
35
  export { Tab, DEFAULT_TAB_LABEL } from './tab.js';
35
- export { Tabs } from './tabs.js';
36
+ export { Tabs } from './tabs-string.js';
37
+ export { InteractiveTabs } from './tabs.js';
36
38
  export { Table } from './table.js';
37
39
  export { drawTableGrid, measureTableGridWidth, MIN_COLUMN_WIDTH, } from './table-grid.js';
38
40
  /**
39
41
  * Derives a registry entry's `inline` flag from `@markii/stdlib`'s standard
40
- * component contract for `name`, matching `@markii/html`'s
41
- * `inlineFromContract`: `kind: 'inline'` -> `inline: true`, otherwise
42
- * `false`. Falls back to `false` if `name` has no standard contract.
42
+ * component contract for `name`: `kind: 'inline'` -> `inline: true`,
43
+ * otherwise `false`. Falls back to `false` if `name` has no standard
44
+ * contract.
43
45
  */
44
46
  function inlineFromContract(name) {
45
47
  return getContract(name)?.kind === 'inline';
@@ -47,7 +49,6 @@ function inlineFromContract(name) {
47
49
  /**
48
50
  * One layout-wrapper registration: the shared wrapper component bound to
49
51
  * `preset`, plus the `layout` axis that preset sets by its own name.
50
- * Matches `@markii/html`'s `layoutWrapperEntry`.
51
52
  */
52
53
  function layoutWrapperEntry(preset) {
53
54
  return {
@@ -57,14 +58,16 @@ function layoutWrapperEntry(preset) {
57
58
  };
58
59
  }
59
60
  /**
60
- * The built-in standard components, pre-registered under their names —
61
- * matching `@markii/html`'s `defaultHtmlRegistry` and `@markii/react`'s
62
- * `defaultRegistry` in shape and coverage: the same 23 names. `card`,
63
- * `callout`, `divider`, and `table` are marked `selfLayout` (`registry.ts`'s
64
- * `AnsiRegistryEntry.selfLayout`) because they draw a single long
61
+ * The built-in standard components, pre-registered under their names: the
62
+ * same 23 names as `@markii/html`'s `defaultHtmlRegistry` and
63
+ * `@markii/react`'s `defaultRegistry`. `card`, `callout`, `divider`, and
64
+ * `table` are marked `selfLayout` because they draw a single long
65
65
  * frame/bar/rule/grid a generic post-render `applyLayout` narrow/pad would
66
- * corrupt or hard-break mid-glyph; see `card.ts`'s and `divider.ts`'s doc
67
- * comments.
66
+ * corrupt or hard-break mid-glyph. `row`, `tabs`, and `details` are the
67
+ * three directives `render.tsx` gives real Ink layout/state to directly
68
+ * (see `registry.ts`'s and `render.tsx`'s doc comments); their REGISTERED
69
+ * `component` here is the STRING-mode fallback, reached only when one of
70
+ * them is nested inside a self-drawing container's own body.
68
71
  */
69
72
  export const defaultAnsiRegistry = createAnsiRegistry({
70
73
  callout: {
@@ -1,4 +1,4 @@
1
- import type { AnsiComponent } from '../registry.js';
1
+ import { type AnsiComponent } from '../registry.js';
2
2
  /**
3
3
  * `:kbd[Ctrl+S]` — a styled keycap for an inline text directive. Terminal
4
4
  * form: bracketed bold text, e.g. `[Ctrl+S]`. Takes no attributes; its inner
@@ -1,8 +1,9 @@
1
+ import { childrenText } from '../registry.js';
1
2
  /**
2
3
  * `:kbd[Ctrl+S]` — a styled keycap for an inline text directive. Terminal
3
4
  * form: bracketed bold text, e.g. `[Ctrl+S]`. Takes no attributes; its inner
4
5
  * content (already rendered plain text) is the key label.
5
6
  */
6
- export const Kbd = (_attributes, children, ctx) => {
7
- return `[${ctx.bold(children())}]`;
7
+ export const Kbd = ({ children, ctx }) => {
8
+ return `[${ctx.bold(childrenText(children))}]`;
8
9
  };
@@ -1,34 +1,19 @@
1
1
  import type { LayoutAxis } from '@markii/stdlib';
2
- import type { AnsiComponent } from '../registry.js';
2
+ import { type AnsiComponent } from '../registry.js';
3
3
  /**
4
4
  * The closed set of layout-wrapper container names (docs/format.md):
5
- * aliases of the one shared implementation below (`createLayoutWrapper`),
6
- * matching `@markii/html`'s `layout-wrapper.ts`. There is deliberately no
7
- * `normal` alias: the default needs no wrapper at all. Every name here is
8
- * ALSO one of `@markii/stdlib`'s own width/align preset values (`center`/
9
- * `left`/`right` are align presets, `wide`/`narrow`/`full`/`fit` are width
10
- * presets), which is what lets `createLayoutWrapper` use the preset name
11
- * itself as that axis's value with no separate lookup table.
5
+ * aliases of the one shared implementation below (`createLayoutWrapper`).
6
+ * There is deliberately no `normal` alias: the default needs no wrapper at
7
+ * all. Every name here is ALSO one of `@markii/stdlib`'s own width/align
8
+ * preset values.
12
9
  */
13
10
  export declare const LAYOUT_WRAPPER_PRESETS: readonly ["center", "left", "right", "wide", "narrow", "full", "fit"];
14
11
  export type LayoutWrapperPreset = (typeof LAYOUT_WRAPPER_PRESETS)[number];
15
12
  /**
16
13
  * Creates the registry component for one of docs/format.md's layout-wrapper
17
14
  * container names. One shared implementation, bound to `preset` at
18
- * registration time, matching `@markii/html`'s `createLayoutWrapper` in
19
- * spirit: it never reads `attributes` at all — `render.ts` already stripped
20
- * both reserved keys before this ever runs.
21
- *
22
- * A wrapper sets ONE axis by its own NAME (docs/spec.md §3): `preset` itself
23
- * IS that axis's value (`center` sets `align: 'center'`; `narrow` sets
24
- * `width: 'narrow'`), so it is applied unconditionally, whatever the author
25
- * wrote for that axis's own reserved attribute (already discarded — the
26
- * name always wins). The OTHER axis, when the author supplied it, arrives
27
- * as `ctx.layout` (`render.ts` resolved it on this wrapper's behalf, since
28
- * this wrapper is the directive's registered `layout` scope). Both are
29
- * merged into one `ResolvedLayoutPresets` and applied together via
30
- * `../layout.js`'s `applyLayout`, so `:::center{width=fit}` narrows AND
31
- * centers in one pass.
15
+ * registration time: it never reads `attributes` at all — `render.tsx`
16
+ * already stripped both reserved keys before this ever runs.
32
17
  */
33
18
  export declare function createLayoutWrapper(preset: LayoutWrapperPreset): AnsiComponent;
34
19
  /** The layout axis `preset` sets by its own name. Mirrors `@markii/html`'s `layoutWrapperPresetAxis`. */