@textui/widgets 0.6.1 → 0.8.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 (120) hide show
  1. package/dist/chart/line-chart.d.ts.map +1 -1
  2. package/dist/chart/line-chart.js +3 -2
  3. package/dist/control/select.d.ts.map +1 -1
  4. package/dist/control/select.js +4 -2
  5. package/dist/control/switch.d.ts.map +1 -1
  6. package/dist/control/switch.js +7 -2
  7. package/dist/control/text-area.d.ts +2 -0
  8. package/dist/control/text-area.d.ts.map +1 -1
  9. package/dist/control/text-area.js +16 -8
  10. package/dist/data/code-viewer.d.ts +2 -0
  11. package/dist/data/code-viewer.d.ts.map +1 -1
  12. package/dist/data/code-viewer.js +9 -1
  13. package/dist/data/index.d.ts +9 -0
  14. package/dist/data/index.d.ts.map +1 -1
  15. package/dist/data/index.js +11 -4
  16. package/dist/data/list.d.ts +9 -0
  17. package/dist/data/list.d.ts.map +1 -1
  18. package/dist/data/list.js +23 -5
  19. package/dist/data/log-viewer.d.ts.map +1 -1
  20. package/dist/data/log-viewer.js +7 -2
  21. package/dist/data/markdown-view.d.ts.map +1 -1
  22. package/dist/data/markdown-view.js +6 -2
  23. package/dist/data/pagination.d.ts.map +1 -1
  24. package/dist/data/pagination.js +6 -2
  25. package/dist/data/tree.d.ts +2 -0
  26. package/dist/data/tree.d.ts.map +1 -1
  27. package/dist/data/tree.js +8 -2
  28. package/dist/display/card.d.ts.map +1 -1
  29. package/dist/display/card.js +12 -0
  30. package/dist/display/color-text.d.ts +13 -1
  31. package/dist/display/color-text.d.ts.map +1 -1
  32. package/dist/display/color-text.js +28 -6
  33. package/dist/display/error-state.d.ts.map +1 -1
  34. package/dist/display/error-state.js +4 -3
  35. package/dist/display/font-text.d.ts +72 -0
  36. package/dist/display/font-text.d.ts.map +1 -0
  37. package/dist/display/font-text.js +35 -0
  38. package/dist/display/fonts.d.ts +138 -0
  39. package/dist/display/fonts.d.ts.map +1 -0
  40. package/dist/display/fonts.js +893 -0
  41. package/dist/display/gard.d.ts +26 -0
  42. package/dist/display/gard.d.ts.map +1 -0
  43. package/dist/display/gard.js +479 -0
  44. package/dist/display/index.d.ts +3 -0
  45. package/dist/display/index.d.ts.map +1 -1
  46. package/dist/display/index.js +7 -0
  47. package/dist/display/key-value.d.ts +25 -5
  48. package/dist/display/key-value.d.ts.map +1 -1
  49. package/dist/display/key-value.js +13 -3
  50. package/dist/display/pagga.d.ts +29 -0
  51. package/dist/display/pagga.d.ts.map +1 -0
  52. package/dist/display/pagga.js +101 -0
  53. package/dist/display/pattern.d.ts +117 -0
  54. package/dist/display/pattern.d.ts.map +1 -0
  55. package/dist/display/pattern.js +257 -0
  56. package/dist/display/spinner.d.ts +2 -0
  57. package/dist/display/spinner.d.ts.map +1 -1
  58. package/dist/display/spinner.js +3 -2
  59. package/dist/form/danger-zone.d.ts.map +1 -1
  60. package/dist/form/danger-zone.js +7 -3
  61. package/dist/form/form-actions.d.ts.map +1 -1
  62. package/dist/form/form-actions.js +3 -2
  63. package/dist/navigation/menu.d.ts +2 -0
  64. package/dist/navigation/menu.d.ts.map +1 -1
  65. package/dist/navigation/menu.js +22 -8
  66. package/dist/navigation/tabs.d.ts +2 -0
  67. package/dist/navigation/tabs.d.ts.map +1 -1
  68. package/dist/navigation/tabs.js +12 -2
  69. package/dist/overlay/command-palette.d.ts.map +1 -1
  70. package/dist/overlay/command-palette.js +55 -15
  71. package/dist/overlay/dialog.d.ts +11 -0
  72. package/dist/overlay/dialog.d.ts.map +1 -1
  73. package/dist/overlay/dialog.js +24 -1
  74. package/dist/overlay/index.d.ts.map +1 -1
  75. package/dist/overlay/index.js +11 -19
  76. package/dist/overlay/path-picker.d.ts.map +1 -1
  77. package/dist/overlay/path-picker.js +19 -11
  78. package/dist/overlay/prompt-dialog.d.ts.map +1 -1
  79. package/dist/overlay/prompt-dialog.js +5 -4
  80. package/dist/panel/index.d.ts.map +1 -1
  81. package/dist/panel/index.js +8 -7
  82. package/dist/panel/resource-panel.d.ts.map +1 -1
  83. package/dist/panel/resource-panel.js +9 -5
  84. package/dist/surface/mount-view.d.ts.map +1 -1
  85. package/dist/surface/mount-view.js +3 -2
  86. package/package.json +2 -2
  87. package/src/chart/line-chart.ts +3 -2
  88. package/src/control/select.ts +4 -2
  89. package/src/control/switch.ts +9 -2
  90. package/src/control/text-area.ts +18 -7
  91. package/src/data/code-viewer.ts +10 -1
  92. package/src/data/index.ts +12 -3
  93. package/src/data/list.ts +24 -4
  94. package/src/data/log-viewer.ts +7 -1
  95. package/src/data/markdown-view.ts +10 -1
  96. package/src/data/pagination.ts +6 -2
  97. package/src/data/tree.ts +9 -2
  98. package/src/display/card.ts +12 -0
  99. package/src/display/color-text.ts +33 -10
  100. package/src/display/error-state.ts +4 -3
  101. package/src/display/font-text.tsx +94 -0
  102. package/src/display/fonts.ts +990 -0
  103. package/src/display/gard.ts +475 -0
  104. package/src/display/index.ts +7 -0
  105. package/src/display/key-value.ts +44 -6
  106. package/src/display/pagga.ts +102 -0
  107. package/src/display/pattern.tsx +427 -0
  108. package/src/display/spinner.ts +5 -2
  109. package/src/form/danger-zone.ts +7 -3
  110. package/src/form/form-actions.ts +5 -2
  111. package/src/navigation/menu.ts +23 -8
  112. package/src/navigation/tabs.ts +13 -2
  113. package/src/overlay/command-palette.ts +66 -14
  114. package/src/overlay/dialog.ts +37 -1
  115. package/src/overlay/index.ts +11 -19
  116. package/src/overlay/path-picker.ts +23 -10
  117. package/src/overlay/prompt-dialog.ts +5 -4
  118. package/src/panel/index.ts +9 -7
  119. package/src/panel/resource-panel.ts +9 -4
  120. package/src/surface/mount-view.ts +3 -2
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The `pagga` table, transcribed.
3
+ *
4
+ * Three rows of half cells on a `░` ground - which is the difference between
5
+ * this and `half`, and the whole reason for it. `half` draws letters and
6
+ * leaves the space around them empty; this draws them *on* something, so the
7
+ * banner is a block of texture with the letters knocked out of it rather than
8
+ * strokes floating on the terminal's background.
9
+ *
10
+ * That ground is why the glyphs are four columns rather than three. The first
11
+ * column of every one is the `░` gutter, so the letters butt together and the
12
+ * ground runs unbroken between them - and why the space is a glyph of its own
13
+ * rather than a gap, because a gap would be a hole in it.
14
+ *
15
+ * One case: the source has one set of letters and lowercase folds to it.
16
+ *
17
+ * The punctuation comes from two places and the table says which is which. The
18
+ * marks above the divider are transcribed - and the brackets among them took a
19
+ * correction, because the source lists three pairs with nothing saying which is
20
+ * which and shape alone does not settle it: the square-cornered pair reads as
21
+ * `(` `)` just as happily as `[` `]`, and it was the wrong one of the two.
22
+ *
23
+ * The marks below it are drawn to match, because the source has none of them
24
+ * at all - no full stop, no comma, no slash. Which is worth knowing when one
25
+ * of them looks out of place: it is not a transcription error, it is a
26
+ * judgement, and a paste would replace it.
27
+ */
28
+ export const PAGGA: Record<string, string[]> = {
29
+ 0: ["░▄▀▄","░█/█","░░▀░"],
30
+ 1: ["░▀█░","░░█░","░▀▀▀"],
31
+ 2: ["░▀▀▄","░▄▀░","░▀▀▀"],
32
+ 3: ["░▀▀█","░░▀▄","░▀▀░"],
33
+ 4: ["░█░█","░░▀█","░░░▀"],
34
+ 5: ["░█▀▀","░▀▀▄","░▀▀░"],
35
+ 6: ["░▄▀▀","░█▀▄","░░▀░"],
36
+ 7: ["░▀▀█","░▄▀░","░▀░░"],
37
+ 8: ["░▄▀▄","░▄▀▄","░░▀░"],
38
+ 9: ["░▄▀▄","░░▀█","░▀▀░"],
39
+ A: ["░█▀█","░█▀█","░▀░▀"],
40
+ B: ["░█▀▄","░█▀▄","░▀▀░"],
41
+ C: ["░█▀▀","░█░░","░▀▀▀"],
42
+ D: ["░█▀▄","░█░█","░▀▀░"],
43
+ E: ["░█▀▀","░█▀▀","░▀▀▀"],
44
+ F: ["░█▀▀","░█▀▀","░▀░░"],
45
+ G: ["░█▀▀","░█░█","░▀▀▀"],
46
+ H: ["░█░█","░█▀█","░▀░▀"],
47
+ I: ["░▀█▀","░░█░","░▀▀▀"],
48
+ J: ["░▀▀█","░░░█","░▀▀░"],
49
+ K: ["░█░█","░█▀▄","░▀░▀"],
50
+ L: ["░█░░","░█░░","░▀▀▀"],
51
+ M: ["░█▄█","░█░█","░▀░▀"],
52
+ N: ["░█▀█","░█░█","░▀░▀"],
53
+ O: ["░█▀█","░█░█","░▀▀▀"],
54
+ P: ["░█▀█","░█▀▀","░▀░░"],
55
+ Q: ["░▄▀▄","░█\\█","░░▀\\"],
56
+ R: ["░█▀▄","░█▀▄","░▀░▀"],
57
+ S: ["░█▀▀","░▀▀█","░▀▀▀"],
58
+ T: ["░▀█▀","░░█░","░░▀░"],
59
+ U: ["░█░█","░█░█","░▀▀▀"],
60
+ V: ["░█░█","░▀▄▀","░░▀░"],
61
+ W: ["░█░█","░█▄█","░▀░▀"],
62
+ X: ["░█░█","░▄▀▄","░▀░▀"],
63
+ Y: ["░█░█","░░█░","░░▀░"],
64
+ Z: ["░▀▀█","░▄▀░","░▀▀▀"],
65
+ "!": ["░█","░▀","░▀"],
66
+ "?": ["░▄▀▄","░█▀▀","░░▀░"],
67
+ "#": ["░▄█▄█▄","░▄█▄█▄","░░▀░▀░"],
68
+ "$": ["░▄█▀","░▀██","░▀▀░"],
69
+ "%": ["░▀░█","░▄▀░","░▀░▀"],
70
+ "&": ["░▄▀░","░▄█▀","░░▀▀"],
71
+ "*": ["░▄░▄","░▄█▄","░▄▀▄"],
72
+ "[": ["░█▀░","░█░░","░▀▀░"],
73
+ "]": ["░▀█░","░░█░","░▀▀░"],
74
+ "_": ["░░░░","░░░░","░▀▀▀"],
75
+ "+": ["░░░░","░▄█▄","░░▀░"],
76
+ "-": ["░░░░","░▄▄▄","░░░░"],
77
+ "=": ["░░░░","░▀▀▀","░▀▀▀"],
78
+ "\"": ["░▀░▀","░░░░","░░░░"],
79
+ "'": ["░▀","░░","░░"],
80
+ "(": ["░▄▀░","░█░░","░░▀░"],
81
+ ")": ["░▀▄░","░░█░","░▀░░"],
82
+ "{": ["░░█▀","░▀▄░","░░▀▀"],
83
+ "}": ["░▀█░","░░▄▀","░▀▀░"],
84
+ "<": ["░░▄▀","░▀▄░","░░░▀"],
85
+ ">": ["░▀▄░","░░▄▀","░▀░░"],
86
+ " ": ["░░░░","░░░░","░░░░"],
87
+
88
+ // Drawn to match, not transcribed: the source has none of these. A period
89
+ // is the top half of the last cell, sitting on the line, and a comma is the
90
+ // bottom half of it, hanging below - which is what those two marks are.
91
+ ".": ["░░","░░","░▀"],
92
+ ",": ["░░","░░","░▄"],
93
+ ":": ["░░","░▀","░▀"],
94
+ ";": ["░░","░▀","░▄"],
95
+ "/": ["░░▄▀","░▄▀░","░▀░░"],
96
+ "\\": ["░▀▄░","░░▀▄","░░░▀"],
97
+ "@": ["░▄▀▄","░█▄█","░░▀▀"],
98
+ "^": ["░▄▀▄","░░░░","░░░░"],
99
+ "`": ["░▀▄","░░░","░░░"],
100
+ "|": ["░█","░█","░▀"],
101
+ "~": ["░░░░","░▄▀▄","░░░░"],
102
+ };
@@ -0,0 +1,427 @@
1
+ import { defineComponent, useCapabilities, useMeasure } from '@textui/core';
2
+ import type { BoxProps, PaintSurface, RenderContext, RenderOutput } from '@textui/core';
3
+ import { inkCellStyle, inkOwnStyle, painterOf } from './color-text.js';
4
+ import type { Ink } from './color-text.js';
5
+
6
+ /**
7
+ * A tile, repeated.
8
+ *
9
+ * A texture for a box, or a motif over one: the tile is stamped across and
10
+ * down until the box runs out, and `asBackground` (the default) or `asOverlay`
11
+ * says which side of the children it lands on.
12
+ *
13
+ * The two modes are document order and not much else. Painting walks the tree
14
+ * in the order components were written, so a layer emitted before the children
15
+ * ends up under them and one emitted after ends up over them. `zIndex` looks
16
+ * like it should decide this and does not: it sorts the layout boxes, while
17
+ * `paintTree` walks the instance children, so the property is inert for
18
+ * painting today. Relying on order is what actually works.
19
+ */
20
+
21
+ export interface PatternProps extends BoxProps {
22
+ /** The tile. One string per row, or one string with newlines in it. */
23
+ tile: string | string[];
24
+ /**
25
+ * The tile to draw on a terminal that cannot manage the first one.
26
+ *
27
+ * A pattern is the one component whose whole content is glyphs, so it is the
28
+ * one that breaks worst on an `unicode: 'ascii'` terminal - and unlike a
29
+ * border, the library cannot guess a fallback for a tile it has never seen.
30
+ * Whoever picks the glyphs picks the substitute.
31
+ */
32
+ ascii?: string | string[];
33
+ /**
34
+ * Repeats across.
35
+ *
36
+ * Unset or `0` draws the tile once. `-1` repeats until the box runs out.
37
+ * A positive number draws that many, and the box still clips it.
38
+ */
39
+ x?: number;
40
+ /** Repeats down. Same rules as `x`. */
41
+ y?: number;
42
+ /** Stop short of the box, in cells. Unset means the box decides. */
43
+ limit?: { width?: number; height?: number };
44
+ /**
45
+ * Cells added after each copy, before the next one starts.
46
+ *
47
+ * Unset is flush, which is what a texture wants. Spacing turns the same tile
48
+ * into a motif with air around it - the difference between wrapping paper
49
+ * and a wallpaper: `10 + 10 = 20`, and `spacing.x = 5` makes it `25`.
50
+ *
51
+ * Not `gap`. A box already has one, it means the space between *children*,
52
+ * and a component that used the same word for the space between copies of
53
+ * its own tile would be two different distances under one name.
54
+ */
55
+ spacing?: { x?: number; y?: number };
56
+ /**
57
+ * Up to this many extra cells between copies, chosen at random per step.
58
+ *
59
+ * The standard name for it: a jitter is a random displacement applied to
60
+ * positions that would otherwise be regular, and breaking a lattice is what
61
+ * it is for. Not a deviation, which in statistics is a spread about a mean,
62
+ * and not a factor, which is what it multiplied before it was a limit.
63
+ *
64
+ * A limit, not a factor: `jitter.x = 6` is "somewhere between flush and six
65
+ * cells further on", dealt again for every step. Unset or `0` is the grid,
66
+ * every copy exactly a tile from the last, which is what the default
67
+ * preserves.
68
+ *
69
+ * It stacks with `spacing` rather than replacing it. Spacing is the air you
70
+ * always want and this is how much more of it is left to chance, so
71
+ * `spacing.x = 4, jitter.x = 6` steps by a tile plus four to ten.
72
+ *
73
+ * A jitter on `x` also stops the copies lining up into columns - see
74
+ * `offsets`, and the screenshot that made it necessary.
75
+ */
76
+ jitter?: { x?: number; y?: number };
77
+ /**
78
+ * The deal, when there is a jitter.
79
+ *
80
+ * A pattern re-renders whenever its box changes, and one that reached for
81
+ * `Math.random()` while painting would shuffle itself on every frame - a
82
+ * texture that crawls. The seed makes the scatter a function of the props,
83
+ * so the same pattern is the same pattern until somebody changes the number.
84
+ */
85
+ seed?: number;
86
+ /**
87
+ * Cells holding this character are left unpainted, so whatever is behind
88
+ * shows through. `null` paints every cell, spaces included.
89
+ */
90
+ transparent?: string | null;
91
+ /**
92
+ * Colour the tile cell by cell, the way [`ColorText`](color-text.md) colours
93
+ * text: a ramp between stops, a palette walked in runs, or a function of the
94
+ * cell. The coordinates are the pattern's own - `col` and `line` count cells
95
+ * of the box it fills, not of one copy of the tile - so `{ gradient }` runs
96
+ * across the whole texture and `{ cycle, every }` walks it.
97
+ *
98
+ * Stated, the tile is painted on a canvas rather than drawn as text, and a
99
+ * canvas is not told what it was nested in: every cell takes this
100
+ * component's own `fg` where the ink declines one, not the colour of the row
101
+ * it happens to sit in. That is the trade `ColorText` makes for the same
102
+ * reason, and it is the only difference between an inked tile and a plain
103
+ * one.
104
+ */
105
+ ink?: Ink;
106
+ /** Paint under the children. The default, and what a texture wants. */
107
+ asBackground?: boolean;
108
+ /** Paint over the children instead. */
109
+ asOverlay?: boolean;
110
+ }
111
+
112
+ /**
113
+ * The tile as equal-length rows. A ragged tile pads rather than tears.
114
+ *
115
+ * Only the blank rows at the ends go - those are how a string written across
116
+ * several lines begins and ends. A blank row in the middle is a row of
117
+ * transparent cells somebody drew on purpose, and dropping it silently
118
+ * shortens their tile.
119
+ */
120
+ function rowsOf(tile: string | string[]): string[] {
121
+ const rows = Array.isArray(tile) ? [...tile] : tile.split('\n');
122
+ while (rows.length > 0 && rows[0] === '') rows.shift();
123
+ while (rows.length > 0 && rows[rows.length - 1] === '') rows.pop();
124
+
125
+ const width = Math.max(0, ...rows.map((row) => [...row].length));
126
+ return rows.map((row) => row.padEnd(width, ' '));
127
+ }
128
+
129
+ /**
130
+ * How many copies to draw along one axis.
131
+ *
132
+ * Unset and zero mean the same thing - once, no repetition - because "repeat
133
+ * it zero times" and "do not repeat it" are the same request, and treating
134
+ * zero as *nothing at all* makes an unset prop and a computed one behave
135
+ * differently for no reason anyone would want.
136
+ *
137
+ * `-1` is "as many as fit", which with a gap or a deviation is not arithmetic
138
+ * any more - it is however many the walk gets through. The count is an upper
139
+ * bound there rather than the answer.
140
+ */
141
+ function repeats(count: number | undefined, tile: number, room: number): number {
142
+ if (count === undefined || count === 0) return 1;
143
+ if (count < 0) return tile > 0 ? Math.ceil(room / tile) : 1;
144
+ return count;
145
+ }
146
+
147
+ /**
148
+ * A small deterministic generator, so a deviation is a deal rather than a
149
+ * shuffle. `mulberry32`: the point is repeatability, not statistics.
150
+ */
151
+ function random(seed: number): () => number {
152
+ let state = seed >>> 0;
153
+ return () => {
154
+ state = (state + 0x6d2b79f5) >>> 0;
155
+ let t = state;
156
+ t = Math.imul(t ^ (t >>> 15), t | 1);
157
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
158
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Where each copy starts along one axis.
164
+ *
165
+ * The step is a tile, plus whatever the gap and the deviation ask for:
166
+ *
167
+ * - neither - `tile`, which is the grid, and exactly what this drew before
168
+ * either of them existed
169
+ * - spacing - `tile + spacing`, the same grid with air in it
170
+ * - a deviation - `tile + rand * deviation * tile`, so `1` can skip a whole
171
+ * tile and land anywhere in the one after
172
+ * - both - `tile + rand * deviation * spacing`, because once there is a
173
+ * spacing it is the spacing being shared out rather than the tile
174
+ *
175
+ * A jitter also moves where the walk *starts*, backwards, by up to a step.
176
+ *
177
+ * Without that, a tile as wide as the box is drawn once at zero and the
178
+ * jitter has nothing to act on - there is no second copy for the step to
179
+ * be different from, so every seed deals the same picture. It is the case a
180
+ * sparse tile is most likely to be in: a big tile with two marks on it is
181
+ * somebody scattering by hand, and the whole point of the seed there is that
182
+ * the scatter moves.
183
+ *
184
+ * Backwards rather than forwards, so the phase shows as a different slice of
185
+ * the pattern rather than as a blank margin down the left. The walk then needs
186
+ * one more copy to reach the far edge, which is why `fill` extends the count.
187
+ */
188
+ function offsets(
189
+ count: number,
190
+ tile: number,
191
+ room: number,
192
+ spacing: number,
193
+ /** Up to this many extra cells per step. */
194
+ jitter: number,
195
+ next: () => number,
196
+ /** Repeating to fill, rather than drawing a stated number of copies. */
197
+ fill: boolean,
198
+ ): number[] {
199
+ if (tile <= 0) return [];
200
+ const out: number[] = [];
201
+ const step = tile + spacing;
202
+ // Only when filling. A caller who asked for three copies put them there;
203
+ // sliding those off the edge would be answering a different question.
204
+ const phase = fill && jitter > 0
205
+ ? -Math.round(next() * (step + jitter))
206
+ : 0;
207
+ let at = phase;
208
+ const total = phase < 0 ? count + 1 : count;
209
+ for (let i = 0; i < total && at < room; i++) {
210
+ out.push(at);
211
+ // A limit rather than a factor: the extra is somewhere in `0..jitter`,
212
+ // dealt again for every step.
213
+ at += step + (jitter > 0 ? Math.round(next() * jitter) : 0);
214
+ }
215
+ return out;
216
+ }
217
+
218
+ /** The runs of paintable cells in one row, so gaps stay see-through. */
219
+ function runs(row: string, transparent: string | null): { at: number; text: string }[] {
220
+ const cells = [...row];
221
+ if (transparent === null) return cells.length > 0 ? [{ at: 0, text: row }] : [];
222
+
223
+ const out: { at: number; text: string }[] = [];
224
+ let start = -1;
225
+ for (let i = 0; i <= cells.length; i++) {
226
+ const paint = i < cells.length && cells[i] !== transparent;
227
+ if (paint && start < 0) start = i;
228
+ else if (!paint && start >= 0) {
229
+ out.push({ at: start, text: cells.slice(start, i).join('') });
230
+ start = -1;
231
+ }
232
+ }
233
+ return out;
234
+ }
235
+
236
+ export const Pattern: (props: PatternProps) => RenderOutput = defineComponent<PatternProps>(
237
+ 'Pattern',
238
+ (props) => {
239
+ const {
240
+ tile, ascii, x, y, limit, transparent = ' ',
241
+ spacing, jitter, seed = 1, ink,
242
+ asOverlay, asBackground: _asBackground, children, ...rest
243
+ } = props;
244
+
245
+ const capabilities = useCapabilities();
246
+
247
+ // The box this pattern was given. Zero on the first pass, real on the
248
+ // second - which is why `-1` needs a frame to find its extent.
249
+ const measured = useMeasure();
250
+
251
+ const rows = rowsOf(capabilities.unicode === 'ascii' && ascii !== undefined ? ascii : tile);
252
+ const tileWidth = rows[0] ? [...rows[0]].length : 0;
253
+ const tileHeight = rows.length;
254
+
255
+ const room = {
256
+ width: Math.min(measured.width, limit?.width ?? Number.POSITIVE_INFINITY),
257
+ height: Math.min(measured.height, limit?.height ?? Number.POSITIVE_INFINITY),
258
+ };
259
+
260
+ const across = repeats(x, tileWidth, room.width);
261
+ const down = repeats(y, tileHeight, room.height);
262
+
263
+ // One generator for both axes, drawn from in a fixed order, so neither
264
+ // depends on the box's size.
265
+ const next = random(seed);
266
+ const lines = offsets(
267
+ down, tileHeight, room.height, spacing?.y ?? 0, jitter?.y ?? 0, next, (y ?? 0) < 0,
268
+ );
269
+
270
+ /*
271
+ * A walk across for *each* row of copies, not one shared by all of them.
272
+ *
273
+ * This is what the difference between "irregular" and "not a lattice"
274
+ * turns out to be. With one set of column positions, every row starts its
275
+ * copies at the same offsets - so the copies still line up into columns,
276
+ * and a sky of stars drawn that way still has visible rows and columns no
277
+ * matter how uneven the spacing along each axis is. You can draw the grid
278
+ * on a screenshot with a ruler, which is exactly what happened.
279
+ *
280
+ * Re-dealing the walk per row costs one pass each and breaks the
281
+ * alignment, because row two's copies are nowhere near row one's.
282
+ * Without a jitter there is nothing to re-deal, so the walk is
283
+ * computed once and shared - and the output is the grid it always was.
284
+ */
285
+ const shared = (jitter?.x ?? 0) === 0
286
+ ? offsets(across, tileWidth, room.width, spacing?.x ?? 0, 0, next, (x ?? 0) < 0)
287
+ : null;
288
+ const columnsFor = (): number[] => shared ?? offsets(
289
+ across, tileWidth, room.width, spacing?.x ?? 0, jitter?.x ?? 0, next, (x ?? 0) < 0,
290
+ );
291
+ /*
292
+ * Every copy placed on its own, not as a cell of a grid.
293
+ *
294
+ * Re-dealing the walk per row breaks the columns; it does not break the
295
+ * rows, because every copy in one row still shares a `top`. A sky drawn
296
+ * that way still has marks lined up in bands - which is visible the
297
+ * moment somebody puts a ruler on a screenshot.
298
+ *
299
+ * So a copy also wobbles up or down by its own amount, out of the same
300
+ * budget as the vertical step. That is the last thing holding the lattice
301
+ * together, and with it gone the placements are a scatter that happens to
302
+ * have been generated in reading order.
303
+ */
304
+ const wobble = jitter?.y ?? 0;
305
+ const placements = lines.flatMap((top) => columnsFor().map((left) => ({
306
+ left,
307
+ top: wobble > 0 ? top + Math.round(next() * wobble) - Math.round(wobble / 2) : top,
308
+ })));
309
+
310
+ const widest = Math.max(0, ...placements.map(({ left }) => left + tileWidth));
311
+ const last = {
312
+ x: Math.max(0, widest),
313
+ y: Math.max(0, (lines[lines.length - 1] ?? 0) + tileHeight),
314
+ };
315
+ const painted = {
316
+ width: Math.min(last.x, room.width),
317
+ height: Math.min(last.y, room.height),
318
+ };
319
+
320
+ /*
321
+ * Stamped into a grid, then split into runs.
322
+ *
323
+ * The rows used to be built by repeating the tile's own string, which is
324
+ * shorter and only works while every copy is exactly a tile from the last.
325
+ * With a gap or a deviation the copies are at arbitrary offsets, so the
326
+ * grid is composed first and read off afterwards - and the reading is the
327
+ * same `runs` split as before, which is what keeps a transparent cell
328
+ * transparent and adjacent cells in one `text` rather than one apiece.
329
+ *
330
+ * With neither prop set the offsets come out as `0, tile, 2*tile, ...` and
331
+ * the composed rows are identical to what the repeat produced.
332
+ */
333
+ const cells: RenderOutput[] = [];
334
+ if (tileWidth > 0 && painted.width > 0 && painted.height > 0) {
335
+ const blank = transparent ?? ' ';
336
+ const grid = Array.from(
337
+ { length: painted.height },
338
+ () => Array.from({ length: painted.width }, () => blank),
339
+ );
340
+
341
+ for (const { top, left } of placements) {
342
+ for (let r = 0; r < tileHeight; r++) {
343
+ // A phased or wobbled copy starts above and to the left of the box,
344
+ // so part of it is outside. Those cells are simply not written; the
345
+ // rest of the copy still lands.
346
+ const row = grid[top + r];
347
+ if (top + r < 0 || !row) continue;
348
+ const source = [...(rows[r] as string)];
349
+ for (let c = 0; c < tileWidth; c++) {
350
+ const at = left + c;
351
+ if (at >= 0 && at < painted.width) row[at] = source[c] as string;
352
+ }
353
+ }
354
+ }
355
+
356
+ if (ink === undefined) {
357
+ grid.forEach((row, y1) => {
358
+ for (const run of runs(row.join(''), transparent)) {
359
+ cells.push(
360
+ <text
361
+ key={`${y1}:${run.at}`}
362
+ position="absolute"
363
+ top={y1}
364
+ left={run.at}
365
+ content={run.text}
366
+ />,
367
+ );
368
+ }
369
+ });
370
+ } else {
371
+ // One cell at a time, because one colour at a time is the whole point
372
+ // of an ink. A cell the `transparent` character owns is skipped rather
373
+ // than painted over: it is not the tile's to draw.
374
+ cells.push(
375
+ <canvas
376
+ key="ink"
377
+ position="absolute"
378
+ top={0}
379
+ left={0}
380
+ width={painted.width}
381
+ height={painted.height}
382
+ draw={(surface: PaintSurface, ctx: RenderContext) => {
383
+ const ownProps = props as unknown as Record<string, unknown>;
384
+ const base = inkCellStyle(inkOwnStyle(ownProps, ctx), ctx, props.link);
385
+ const paint = painterOf(ink, ctx);
386
+ for (let y1 = 0; y1 < grid.length; y1++) {
387
+ const row = grid[y1] as string[];
388
+ for (let x1 = 0; x1 < row.length; x1++) {
389
+ const char = row[x1] as string;
390
+ if (transparent !== null && char === transparent) continue;
391
+ const style = paint({
392
+ char, col: x1, line: y1, index: x1,
393
+ offset: y1 * painted.width + x1,
394
+ width: painted.width, height: painted.height,
395
+ blockWidth: painted.width,
396
+ });
397
+ surface.put(x1, y1, char, style ? { ...base, ...style } : base);
398
+ }
399
+ }
400
+ }}
401
+ />,
402
+ );
403
+ }
404
+ }
405
+
406
+ // Absolute, so the pattern never pushes the children around, and hidden,
407
+ // so a partial tile at the edge is cut rather than allowed to overflow.
408
+ const layer = (
409
+ <box
410
+ position="absolute"
411
+ top={0}
412
+ left={0}
413
+ width={painted.width}
414
+ height={painted.height}
415
+ overflow="hidden"
416
+ >
417
+ {cells}
418
+ </box>
419
+ );
420
+
421
+ return (
422
+ <box position="relative" {...rest}>
423
+ {asOverlay === true ? [children, layer] : [layer, children]}
424
+ </box>
425
+ );
426
+ },
427
+ );
@@ -5,12 +5,15 @@ import { TONE as TONE_COLOR } from '../tone.js';
5
5
  export interface SpinnerProps extends BoxProps {
6
6
  label?: string;
7
7
  tone?: SemanticVariant;
8
+ frames?: string[];
8
9
  }
9
10
 
10
- export const Spinner = defineComponent<SpinnerProps>('Spinner', ({ label, tone = 'accent', ...rest }) => {
11
+ export const DEFAULT_FRAMES = ['-', '\\', '|', '/'];
12
+
13
+ export const Spinner = defineComponent<SpinnerProps>('Spinner', ({ label, tone = 'accent', frames: customFrames, ...rest }) => {
11
14
  const theme = useTheme();
12
15
  const frame = useFrame(10);
13
- const frames = theme.glyphs.spinner;
16
+ const frames = customFrames ?? theme.glyphs.spinner ?? DEFAULT_FRAMES;
14
17
  const glyph = frames[frame % frames.length] ?? frames[0] ?? '*';
15
18
 
16
19
  return h('box', { role: 'status', direction: 'row', gap: 1, ...rest },
@@ -1,5 +1,5 @@
1
1
  import type { BoxProps } from '@textui/core';
2
- import { defineComponent, h, useTheme } from '@textui/core';
2
+ import { defineComponent, h, useI18n, useTheme } from '@textui/core';
3
3
  import { Button } from '../control/index.js';
4
4
 
5
5
  export interface DangerZoneProps extends BoxProps {
@@ -13,8 +13,9 @@ export interface DangerZoneProps extends BoxProps {
13
13
 
14
14
  export const DangerZone = defineComponent<DangerZoneProps>('DangerZone', (props) => {
15
15
  const theme = useTheme();
16
+ const i18n = useI18n();
16
17
  const {
17
- title = 'Danger zone', description, actionLabel, onAction, confirmText, ...rest
18
+ title = i18n.t('textui.dangerZone.title', undefined, 'Danger zone'), description, actionLabel, onAction, confirmText, ...rest
18
19
  } = props;
19
20
 
20
21
  return h('box', {
@@ -26,7 +27,10 @@ export const DangerZone = defineComponent<DangerZoneProps>('DangerZone', (props)
26
27
  },
27
28
  description ? h('text', { content: description, fg: 'muted', wrap: 'word' }) : null,
28
29
  confirmText
29
- ? h('text', { content: `Type "${confirmText}" to confirm.`, fg: 'subtle' })
30
+ ? h('text', {
31
+ content: i18n.t('textui.dangerZone.confirm', { text: confirmText }, 'Type "{text}" to confirm.'),
32
+ fg: 'subtle',
33
+ })
30
34
  : null,
31
35
  h('box', { direction: 'row', justify: 'end' },
32
36
  h(Button, { label: actionLabel, tone: 'danger', variant: 'outline', onPress: onAction })),
@@ -1,5 +1,5 @@
1
1
  import type { BoxProps, SemanticVariant } from '@textui/core';
2
- import { defineComponent, h, useTheme } from '@textui/core';
2
+ import { defineComponent, h, useI18n, useTheme } from '@textui/core';
3
3
  import { Button } from '../control/index.js';
4
4
  import { useFormContext } from './shared.js';
5
5
 
@@ -14,9 +14,12 @@ export interface FormActionsProps extends BoxProps {
14
14
 
15
15
  export const FormActions = defineComponent<FormActionsProps>('FormActions', (props) => {
16
16
  const theme = useTheme();
17
+ const i18n = useI18n();
17
18
  const form = useFormContext();
18
19
  const {
19
- submitLabel = 'Submit', cancelLabel = 'Cancel', onCancel,
20
+ submitLabel = i18n.t('textui.formActions.submit', undefined, 'Submit'),
21
+ cancelLabel = i18n.t('textui.formActions.cancel', undefined, 'Cancel'),
22
+ onCancel,
20
23
  tone = 'primary', requireDirty = false, ...rest
21
24
  } = props;
22
25