@zaly/tui 0.0.4
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.
- package/LICENSE +21 -0
- package/README.md +59 -0
- package/assets/schemas/theme.schema.json +899 -0
- package/assets/themes/LICENSE-tokyonight.txt +201 -0
- package/assets/themes/NOTICES.md +69 -0
- package/assets/themes/ayu-dark.json +119 -0
- package/assets/themes/catppuccin-frappe.json +124 -0
- package/assets/themes/catppuccin-latte.json +124 -0
- package/assets/themes/catppuccin-macchiato.json +124 -0
- package/assets/themes/catppuccin-mocha.json +124 -0
- package/assets/themes/dracula-soft.json +119 -0
- package/assets/themes/dracula.json +119 -0
- package/assets/themes/everforest-dark.json +118 -0
- package/assets/themes/everforest-light.json +118 -0
- package/assets/themes/github-dark.json +123 -0
- package/assets/themes/github-light.json +123 -0
- package/assets/themes/gruvbox-dark-medium.json +112 -0
- package/assets/themes/kanagawa-dragon.json +115 -0
- package/assets/themes/kanagawa-wave.json +115 -0
- package/assets/themes/material-theme-palenight.json +114 -0
- package/assets/themes/min-dark.json +55 -0
- package/assets/themes/min-light.json +59 -0
- package/assets/themes/monokai.json +116 -0
- package/assets/themes/night-owl.json +116 -0
- package/assets/themes/nord.json +101 -0
- package/assets/themes/one-dark-pro.json +110 -0
- package/assets/themes/one-light.json +108 -0
- package/assets/themes/poimandres.json +123 -0
- package/assets/themes/rose-pine.json +98 -0
- package/assets/themes/solarized-dark.json +116 -0
- package/assets/themes/solarized-light.json +116 -0
- package/assets/themes/synthwave-84.json +100 -0
- package/assets/themes/tokyonight-day.json +157 -0
- package/assets/themes/tokyonight-moon.json +157 -0
- package/assets/themes/tokyonight-night.json +157 -0
- package/assets/themes/tokyonight-storm.json +157 -0
- package/assets/themes/vesper.json +103 -0
- package/assets/themes/vitesse-dark.json +126 -0
- package/assets/themes/vitesse-light.json +126 -0
- package/dist/ansi-Clq0ozyA.mjs +206 -0
- package/dist/ansi-Clq0ozyA.mjs.map +1 -0
- package/dist/ansi.d.mts +48 -0
- package/dist/ansi.d.mts.map +1 -0
- package/dist/ansi.mjs +2 -0
- package/dist/autocomplete-Dfw-yQDa.d.mts +100 -0
- package/dist/autocomplete-Dfw-yQDa.d.mts.map +1 -0
- package/dist/box-DyaByEiG.mjs +386 -0
- package/dist/box-DyaByEiG.mjs.map +1 -0
- package/dist/builder-D49ECBEf.mjs +385 -0
- package/dist/builder-D49ECBEf.mjs.map +1 -0
- package/dist/callbacks-CNjGNxVw.mjs +279 -0
- package/dist/callbacks-CNjGNxVw.mjs.map +1 -0
- package/dist/client-D6C_uczF.mjs +142 -0
- package/dist/client-D6C_uczF.mjs.map +1 -0
- package/dist/clipboard.d.mts +104 -0
- package/dist/clipboard.d.mts.map +1 -0
- package/dist/clipboard.mjs +404 -0
- package/dist/clipboard.mjs.map +1 -0
- package/dist/flex-Bguo-fUe.mjs +202 -0
- package/dist/flex-Bguo-fUe.mjs.map +1 -0
- package/dist/image-Cm5T-uw-.mjs +108 -0
- package/dist/image-Cm5T-uw-.mjs.map +1 -0
- package/dist/image.d.mts +123 -0
- package/dist/image.d.mts.map +1 -0
- package/dist/image.mjs +2 -0
- package/dist/index.d.mts +64 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +222 -0
- package/dist/index.mjs.map +1 -0
- package/dist/input-B5Vy-iaa.d.mts +141 -0
- package/dist/input-B5Vy-iaa.d.mts.map +1 -0
- package/dist/inspect-D1d9t-ni.mjs +101 -0
- package/dist/inspect-D1d9t-ni.mjs.map +1 -0
- package/dist/inspect-D6FW_QUo.d.mts +67 -0
- package/dist/inspect-D6FW_QUo.d.mts.map +1 -0
- package/dist/kitty-JbCJC0iQ.mjs +673 -0
- package/dist/kitty-JbCJC0iQ.mjs.map +1 -0
- package/dist/markdown.d.mts +24 -0
- package/dist/markdown.d.mts.map +1 -0
- package/dist/markdown.mjs +2 -0
- package/dist/md.bun.d.mts +14 -0
- package/dist/md.bun.d.mts.map +1 -0
- package/dist/md.bun.mjs +28 -0
- package/dist/md.bun.mjs.map +1 -0
- package/dist/md.node.d.mts +14 -0
- package/dist/md.node.d.mts.map +1 -0
- package/dist/md.node.mjs +126 -0
- package/dist/md.node.mjs.map +1 -0
- package/dist/node-BZ7f6HzU.mjs +316 -0
- package/dist/node-BZ7f6HzU.mjs.map +1 -0
- package/dist/picker-D8OLb6kZ.d.mts +36 -0
- package/dist/picker-D8OLb6kZ.d.mts.map +1 -0
- package/dist/picker-ndVVt6qF.mjs +621 -0
- package/dist/picker-ndVVt6qF.mjs.map +1 -0
- package/dist/queries-DH3bj74K.d.mts +1963 -0
- package/dist/queries-DH3bj74K.d.mts.map +1 -0
- package/dist/reactive-f7NpAi69.mjs +623 -0
- package/dist/reactive-f7NpAi69.mjs.map +1 -0
- package/dist/registry-U654j61Y.mjs +356 -0
- package/dist/registry-U654j61Y.mjs.map +1 -0
- package/dist/render-DxVLqqB9.mjs +74 -0
- package/dist/render-DxVLqqB9.mjs.map +1 -0
- package/dist/renderer-CdLCkJB-.mjs +42 -0
- package/dist/renderer-CdLCkJB-.mjs.map +1 -0
- package/dist/renderer-CxL4xbkl.mjs +2644 -0
- package/dist/renderer-CxL4xbkl.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/search-FvGnswRl.d.mts +49 -0
- package/dist/search-FvGnswRl.d.mts.map +1 -0
- package/dist/select-CbmAZdpt.d.mts +116 -0
- package/dist/select-CbmAZdpt.d.mts.map +1 -0
- package/dist/services/logger.d.mts +2 -0
- package/dist/services/logger.mjs +71 -0
- package/dist/services/logger.mjs.map +1 -0
- package/dist/services/notifier.d.mts +17 -0
- package/dist/services/notifier.d.mts.map +1 -0
- package/dist/services/notifier.mjs +71 -0
- package/dist/services/notifier.mjs.map +1 -0
- package/dist/services/picker.d.mts +36 -0
- package/dist/services/picker.d.mts.map +1 -0
- package/dist/services/picker.mjs +137 -0
- package/dist/services/picker.mjs.map +1 -0
- package/dist/services/prompt.d.mts +30 -0
- package/dist/services/prompt.d.mts.map +1 -0
- package/dist/services/prompt.mjs +83 -0
- package/dist/services/prompt.mjs.map +1 -0
- package/dist/shiki-BrjVUkeh.mjs +423 -0
- package/dist/shiki-BrjVUkeh.mjs.map +1 -0
- package/dist/size-D8JogYLy.mjs +34 -0
- package/dist/size-D8JogYLy.mjs.map +1 -0
- package/dist/style.d.mts +10 -0
- package/dist/style.d.mts.map +1 -0
- package/dist/style.mjs +4 -0
- package/dist/text.d.mts +33 -0
- package/dist/text.d.mts.map +1 -0
- package/dist/text.mjs +111 -0
- package/dist/text.mjs.map +1 -0
- package/dist/theme.config-CU3B50HN.mjs +832 -0
- package/dist/theme.config-CU3B50HN.mjs.map +1 -0
- package/dist/themes.d.mts +113 -0
- package/dist/themes.d.mts.map +1 -0
- package/dist/themes.mjs +2 -0
- package/dist/tree-C28QOVIK.d.mts +23 -0
- package/dist/tree-C28QOVIK.d.mts.map +1 -0
- package/dist/types-B9Eh7k2v.d.mts +212 -0
- package/dist/types-B9Eh7k2v.d.mts.map +1 -0
- package/dist/types-D3jk1rtk.d.mts +140 -0
- package/dist/types-D3jk1rtk.d.mts.map +1 -0
- package/dist/utils-3M4D_AXZ.mjs +40 -0
- package/dist/utils-3M4D_AXZ.mjs.map +1 -0
- package/dist/widgets/autocomplete.d.mts +2 -0
- package/dist/widgets/autocomplete.mjs +165 -0
- package/dist/widgets/autocomplete.mjs.map +1 -0
- package/dist/widgets/box.d.mts +2 -0
- package/dist/widgets/box.mjs +2 -0
- package/dist/widgets/code.d.mts +48 -0
- package/dist/widgets/code.d.mts.map +1 -0
- package/dist/widgets/code.mjs +59 -0
- package/dist/widgets/code.mjs.map +1 -0
- package/dist/widgets/completions/actions.d.mts +41 -0
- package/dist/widgets/completions/actions.d.mts.map +1 -0
- package/dist/widgets/completions/actions.mjs +48 -0
- package/dist/widgets/completions/actions.mjs.map +1 -0
- package/dist/widgets/completions/files.d.mts +29 -0
- package/dist/widgets/completions/files.d.mts.map +1 -0
- package/dist/widgets/completions/files.mjs +42 -0
- package/dist/widgets/completions/files.mjs.map +1 -0
- package/dist/widgets/completions/github.d.mts +64 -0
- package/dist/widgets/completions/github.d.mts.map +1 -0
- package/dist/widgets/completions/github.mjs +137 -0
- package/dist/widgets/completions/github.mjs.map +1 -0
- package/dist/widgets/completions/index.d.mts +4 -0
- package/dist/widgets/completions/index.mjs +4 -0
- package/dist/widgets/diff.d.mts +57 -0
- package/dist/widgets/diff.d.mts.map +1 -0
- package/dist/widgets/diff.mjs +199 -0
- package/dist/widgets/diff.mjs.map +1 -0
- package/dist/widgets/divider.d.mts +11 -0
- package/dist/widgets/divider.d.mts.map +1 -0
- package/dist/widgets/divider.mjs +9 -0
- package/dist/widgets/divider.mjs.map +1 -0
- package/dist/widgets/image.d.mts +37 -0
- package/dist/widgets/image.d.mts.map +1 -0
- package/dist/widgets/image.mjs +77 -0
- package/dist/widgets/image.mjs.map +1 -0
- package/dist/widgets/input.d.mts +2 -0
- package/dist/widgets/input.mjs +497 -0
- package/dist/widgets/input.mjs.map +1 -0
- package/dist/widgets/log.d.mts +2 -0
- package/dist/widgets/log.mjs +130 -0
- package/dist/widgets/log.mjs.map +1 -0
- package/dist/widgets/markdown.d.mts +45 -0
- package/dist/widgets/markdown.d.mts.map +1 -0
- package/dist/widgets/markdown.mjs +124 -0
- package/dist/widgets/markdown.mjs.map +1 -0
- package/dist/widgets/overlay.d.mts +2 -0
- package/dist/widgets/overlay.mjs +18 -0
- package/dist/widgets/overlay.mjs.map +1 -0
- package/dist/widgets/picker.d.mts +2 -0
- package/dist/widgets/picker.mjs +2 -0
- package/dist/widgets/progress.d.mts +48 -0
- package/dist/widgets/progress.d.mts.map +1 -0
- package/dist/widgets/progress.mjs +48 -0
- package/dist/widgets/progress.mjs.map +1 -0
- package/dist/widgets/select.d.mts +2 -0
- package/dist/widgets/select.mjs +195 -0
- package/dist/widgets/select.mjs.map +1 -0
- package/dist/widgets/show.d.mts +55 -0
- package/dist/widgets/show.d.mts.map +1 -0
- package/dist/widgets/show.mjs +85 -0
- package/dist/widgets/show.mjs.map +1 -0
- package/dist/widgets/spinner.d.mts +65 -0
- package/dist/widgets/spinner.d.mts.map +1 -0
- package/dist/widgets/spinner.mjs +147 -0
- package/dist/widgets/spinner.mjs.map +1 -0
- package/dist/widgets/text.d.mts +2 -0
- package/dist/widgets/text.mjs +32 -0
- package/dist/widgets/text.mjs.map +1 -0
- package/dist/widgets/tree.d.mts +2 -0
- package/dist/widgets/tree.mjs +73 -0
- package/dist/widgets/tree.mjs.map +1 -0
- package/dist/widgets/widget.d.mts +49 -0
- package/dist/widgets/widget.d.mts.map +1 -0
- package/dist/widgets/widget.mjs +40 -0
- package/dist/widgets/widget.mjs.map +1 -0
- package/dist/worker-DjHuQSbY.mjs +27 -0
- package/dist/worker-DjHuQSbY.mjs.map +1 -0
- package/package.json +117 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { S as unwrap, f as effect } from "../reactive-f7NpAi69.mjs";
|
|
2
|
+
import { t as Node } from "../node-BZ7f6HzU.mjs";
|
|
3
|
+
import { n as resolveSize } from "../size-D8JogYLy.mjs";
|
|
4
|
+
import { fitAnsi, stringWidth } from "@zaly/shared/ansi";
|
|
5
|
+
//#region src/widgets/select.ts
|
|
6
|
+
/**
|
|
7
|
+
* Selectable list. Items are rendered one per row with an active-row
|
|
8
|
+
* highlight; navigation actions (`next`, `prev`, `first`, `last`,
|
|
9
|
+
* `accept`, `complete`, `cancel`) live on `this.actions`, same pattern as
|
|
10
|
+
* `Input`.
|
|
11
|
+
*
|
|
12
|
+
* Used standalone for simple lists and as the underlying list primitive for
|
|
13
|
+
* `picker`, `tree`, and `autocomplete`. Doesn't open/close itself — callers
|
|
14
|
+
* control visibility via `state.visible`.
|
|
15
|
+
*/
|
|
16
|
+
var Select = class Select extends Node {
|
|
17
|
+
static type = "select";
|
|
18
|
+
type = Select.type;
|
|
19
|
+
actions = {
|
|
20
|
+
"select.accept": () => {
|
|
21
|
+
const items = this.#items;
|
|
22
|
+
if (items.length === 0) return;
|
|
23
|
+
this.emit("accept", { item: items[this.active] });
|
|
24
|
+
},
|
|
25
|
+
"select.close": () => {
|
|
26
|
+
this.emit("close");
|
|
27
|
+
},
|
|
28
|
+
"select.complete": () => {
|
|
29
|
+
const items = this.#items;
|
|
30
|
+
if (items.length === 0) return;
|
|
31
|
+
this.emit("complete", { item: items[this.active] });
|
|
32
|
+
},
|
|
33
|
+
"select.first": () => {
|
|
34
|
+
if (this.#items.length === 0) return;
|
|
35
|
+
this.active = 0;
|
|
36
|
+
},
|
|
37
|
+
"select.last": () => {
|
|
38
|
+
const n = this.#items.length;
|
|
39
|
+
if (n === 0) return;
|
|
40
|
+
this.active = n - 1;
|
|
41
|
+
},
|
|
42
|
+
"select.next": () => {
|
|
43
|
+
if (this.#items.length === 0) return;
|
|
44
|
+
this.active += this.#direction;
|
|
45
|
+
},
|
|
46
|
+
"select.page-down": () => {
|
|
47
|
+
if (this.#items.length === 0) return;
|
|
48
|
+
const page = Math.max(this.pageSize - 1, 1);
|
|
49
|
+
this.state.active = this.#clamp(this.active + this.#direction * page);
|
|
50
|
+
},
|
|
51
|
+
"select.page-up": () => {
|
|
52
|
+
if (this.#items.length === 0) return;
|
|
53
|
+
const page = Math.max(this.pageSize - 1, 1);
|
|
54
|
+
this.state.active = this.#clamp(this.active - this.#direction * page);
|
|
55
|
+
},
|
|
56
|
+
"select.prev": () => {
|
|
57
|
+
if (this.#items.length === 0) return;
|
|
58
|
+
this.active -= this.#direction;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
/** Top of the currently-rendered window, in absolute item indices.
|
|
62
|
+
* Updated per render using the pin-until-leave rule: stays put
|
|
63
|
+
* while `active` remains in `[start, start + visible)`, slides just
|
|
64
|
+
* enough to re-admit it otherwise. */
|
|
65
|
+
#windowStart = 0;
|
|
66
|
+
constructor(initial) {
|
|
67
|
+
super({
|
|
68
|
+
active: 0,
|
|
69
|
+
...initial
|
|
70
|
+
});
|
|
71
|
+
let active = this.item;
|
|
72
|
+
effect(() => {
|
|
73
|
+
const next = this.item;
|
|
74
|
+
if (next === active) return;
|
|
75
|
+
active = next;
|
|
76
|
+
this.emit("changed", {
|
|
77
|
+
active: this.active,
|
|
78
|
+
item: next
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
get #direction() {
|
|
83
|
+
return this.state.reverse ? -1 : 1;
|
|
84
|
+
}
|
|
85
|
+
get #items() {
|
|
86
|
+
return unwrap(this.state.items);
|
|
87
|
+
}
|
|
88
|
+
#clamp(i) {
|
|
89
|
+
const n = this.#items.length;
|
|
90
|
+
return n === 0 ? 0 : Math.max(0, Math.min(n - 1, i));
|
|
91
|
+
}
|
|
92
|
+
set active(i) {
|
|
93
|
+
const n = this.#items.length;
|
|
94
|
+
this.state.active = n === 0 ? 0 : (i % n + n) % n;
|
|
95
|
+
}
|
|
96
|
+
get active() {
|
|
97
|
+
const a = this.state.active ?? 0;
|
|
98
|
+
const n = this.#items.length;
|
|
99
|
+
if (n === 0) return 0;
|
|
100
|
+
return Math.max(0, Math.min(n - 1, a));
|
|
101
|
+
}
|
|
102
|
+
bind(node) {
|
|
103
|
+
let n;
|
|
104
|
+
const getn = () => n = node instanceof Node ? node : node();
|
|
105
|
+
this.on("mount", () => getn().addActionTarget(this));
|
|
106
|
+
this.on("unmount", () => n?.removeActionTarget(this));
|
|
107
|
+
if (this.mounted) getn().addActionTarget(this);
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
get count() {
|
|
111
|
+
return this.#items.length;
|
|
112
|
+
}
|
|
113
|
+
get pageSize() {
|
|
114
|
+
return unwrap(this.state.maxHeight) ?? Math.max(this.#items.length, 1);
|
|
115
|
+
}
|
|
116
|
+
get item() {
|
|
117
|
+
return this.#items[this.active];
|
|
118
|
+
}
|
|
119
|
+
_render(ctx) {
|
|
120
|
+
const items = this.#items;
|
|
121
|
+
if (items.length === 0) return [];
|
|
122
|
+
const width = resolveSize(this.state.width ?? "fill", ctx.width) ?? ctx.width;
|
|
123
|
+
const active = this.active;
|
|
124
|
+
const height = Math.min(this.pageSize, items.length);
|
|
125
|
+
if (height === 0) return [];
|
|
126
|
+
let start = this.#windowStart;
|
|
127
|
+
if (active < start) start = active;
|
|
128
|
+
else if (active >= start + height) start = active - height + 1;
|
|
129
|
+
start = Math.max(0, Math.min(Math.max(0, items.length - height), start));
|
|
130
|
+
this.#windowStart = start;
|
|
131
|
+
const visible = items.slice(start, start + height);
|
|
132
|
+
const renderer = this.renderer;
|
|
133
|
+
const rows = [];
|
|
134
|
+
for (let i = start; i < start + height; i++) {
|
|
135
|
+
const item = items[i];
|
|
136
|
+
let row = renderer(item, {
|
|
137
|
+
...ctx,
|
|
138
|
+
active: i === active,
|
|
139
|
+
items,
|
|
140
|
+
visible
|
|
141
|
+
}).replace(/\n/g, " ");
|
|
142
|
+
row = fitAnsi(row, width);
|
|
143
|
+
rows.push(i === active ? ctx.style.optionActive(row) : row);
|
|
144
|
+
}
|
|
145
|
+
if (this.state.reverse) rows.reverse();
|
|
146
|
+
if (this.state.counter ?? items.length > height) {
|
|
147
|
+
const label = `(${items.length === 0 ? 0 : active + 1}/${items.length}${this.state.more ? "+" : ""})`;
|
|
148
|
+
rows.push(ctx.style.gutter(fitAnsi(label, width)));
|
|
149
|
+
}
|
|
150
|
+
return rows;
|
|
151
|
+
}
|
|
152
|
+
get renderer() {
|
|
153
|
+
return unwrap(this.state.render) ?? this.defaultRenderer();
|
|
154
|
+
}
|
|
155
|
+
extendRenderer(fn) {
|
|
156
|
+
const prev = this.renderer;
|
|
157
|
+
this.state.render = fn(prev);
|
|
158
|
+
return this;
|
|
159
|
+
}
|
|
160
|
+
defaultRenderer() {
|
|
161
|
+
let visible = [];
|
|
162
|
+
let labelWidth = 0;
|
|
163
|
+
const update = (ctx) => {
|
|
164
|
+
if (ctx.visible === visible) return;
|
|
165
|
+
visible = ctx.visible;
|
|
166
|
+
if (!visible.some((i) => i.desc)) return labelWidth = ctx.width;
|
|
167
|
+
labelWidth = this.state.labelWidth ?? 0;
|
|
168
|
+
labelWidth ||= visible.map(itemLabel).reduce((max, label) => Math.max(max, stringWidth(label)), 0);
|
|
169
|
+
labelWidth = Math.min(labelWidth, Math.floor(ctx.width / 2));
|
|
170
|
+
};
|
|
171
|
+
return (item, ctx) => {
|
|
172
|
+
update(ctx);
|
|
173
|
+
const label = fitAnsi(itemLabel(item), labelWidth);
|
|
174
|
+
if (!item.desc) return ctx.style.optionName(label);
|
|
175
|
+
const desc = fitAnsi(item.desc ?? "", Math.max(0, ctx.width - labelWidth - 2));
|
|
176
|
+
return `${ctx.style.optionName(label)} ${ctx.style.optionDesc(desc)}`;
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
const itemLabel = (item) => (item.name ?? item.text).replace(/\s+/g, " ").trim();
|
|
181
|
+
/**
|
|
182
|
+
* Factory for a selectable list.
|
|
183
|
+
*
|
|
184
|
+
* ```ts
|
|
185
|
+
* const s = select({ items: [{ text: "/help" }, { text: "/quit" }] })
|
|
186
|
+
* s.on("accept", ({ item }) => console.log("picked", item.text))
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
function select(state) {
|
|
190
|
+
return new Select(state);
|
|
191
|
+
}
|
|
192
|
+
//#endregion
|
|
193
|
+
export { Select, select };
|
|
194
|
+
|
|
195
|
+
//# sourceMappingURL=select.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"select.mjs","names":["#items","#direction","#clamp","#windowStart"],"sources":["../../src/widgets/select.ts"],"sourcesContent":["import type { RenderCtx } from \"../core/ctx.ts\"\nimport type { BaseEvents } from \"../core/node.ts\"\nimport type { Reactive, Ref } from \"../core/reactive.ts\"\nimport type { State } from \"../core/state.ts\"\nimport type { NodeActionMap } from \"../input/actions.ts\"\nimport type { Size } from \"../layout/size.ts\"\nimport type { Style } from \"../style/types.ts\"\n\nimport { fitAnsi, stringWidth } from \"@zaly/shared/ansi\"\nimport { Node } from \"../core/node.ts\"\nimport { effect, unwrap } from \"../core/reactive.ts\"\nimport { resolveSize } from \"../layout/size.ts\"\n\n/** Default shape for selectable options. `text` is the canonical\n * searchable/fallback label. `name` overrides the displayed label in the\n * default renderer, and `desc` is shown as a dim right-column description. */\nexport type Option = {\n name?: string\n desc?: string\n text: string\n}\n\nexport type OptionRenderCtx<T> = RenderCtx & {\n visible: readonly T[]\n items: readonly T[]\n active?: boolean\n prefixWidth?: number\n}\n\n/** Per-row rendering hook. `ctx.active` lets callers branch on selection\n * state (e.g. swap an icon or pick a different fg) — but Select still\n * applies the `optionActive` theme slot uniformly so the highlight stays\n * visually consistent. The returned string is clipped/padded to the row\n * width by Select. */\nexport type OptionRender<T> = (item: T, ctx: OptionRenderCtx<T>) => string\n\nexport interface SelectState<T extends Option = Option> extends Style {\n /** Items to show. Accepts a signal accessor so the list can be\n * driven from reactive state (filtered results, search, etc.). */\n items: Reactive<readonly T[]>\n /** Index of the highlighted row. Defaults to 0; clamped on render. */\n active?: number\n /** Max item rows visible at once. The counter (when shown) is an\n * extra row on top of this, not counted against the cap. Window\n * slides to keep `active` in view (pin-until-leave). */\n maxHeight?: Reactive<number>\n /** Show a `(active+1 / total)` footer as an extra row when items\n * don't fit. `undefined` (default) auto-shows when needed;\n * `false` disables, `true` forces on. */\n counter?: boolean\n /** Render width. Defaults to `fill`. */\n width?: Size\n /** Width of the label column for the default renderer. Defaults to the\n * widest visible label, capped to half the row width. Ignored when\n * `render` is set. */\n labelWidth?: number\n /** Per-item renderer. When omitted, the default two-column layout uses\n * `name ?? text` as the label and `desc` as the right-column hint. */\n render?: Reactive<OptionRender<T> | undefined>\n reverse?: boolean\n /** Whether there are more items available than are currently shown. This is a hint for the\n * default renderer to show a \"more\" indicator in the counter. */\n more?: boolean\n}\n\nexport interface SelectEvents<T extends Option = Option> extends BaseEvents {\n /** Fired when the user completes the active item. Payload is the item. */\n complete: { item: T }\n /** Fired when the user picks the active item. Payload is the item. */\n accept: { item: T }\n /** Fired when the user closes the select (esc). */\n close: {}\n changed: { active: number; item?: T }\n}\n\n/**\n * Selectable list. Items are rendered one per row with an active-row\n * highlight; navigation actions (`next`, `prev`, `first`, `last`,\n * `accept`, `complete`, `cancel`) live on `this.actions`, same pattern as\n * `Input`.\n *\n * Used standalone for simple lists and as the underlying list primitive for\n * `picker`, `tree`, and `autocomplete`. Doesn't open/close itself — callers\n * control visibility via `state.visible`.\n */\nexport class Select<T extends Option = Option> extends Node<SelectState<T>, SelectEvents<T>> {\n static readonly type = \"select\"\n override readonly type = Select.type\n\n override actions = {\n \"select.accept\": (): void => {\n const items = this.#items\n if (items.length === 0) return\n void this.emit(\"accept\", { item: items[this.active] })\n },\n \"select.close\": (): void => {\n void this.emit(\"close\")\n },\n \"select.complete\": (): void => {\n const items = this.#items\n if (items.length === 0) return\n void this.emit(\"complete\", { item: items[this.active] })\n },\n \"select.first\": (): void => {\n if (this.#items.length === 0) return\n this.active = 0\n },\n \"select.last\": (): void => {\n const n = this.#items.length\n if (n === 0) return\n this.active = n - 1\n },\n \"select.next\": (): void => {\n const n = this.#items.length\n if (n === 0) return\n this.active += this.#direction\n },\n \"select.page-down\": (): void => {\n const n = this.#items.length\n if (n === 0) return\n const page = Math.max(this.pageSize - 1, 1)\n this.state.active = this.#clamp(this.active + this.#direction * page)\n },\n \"select.page-up\": (): void => {\n const n = this.#items.length\n if (n === 0) return\n const page = Math.max(this.pageSize - 1, 1)\n this.state.active = this.#clamp(this.active - this.#direction * page)\n },\n \"select.prev\": (): void => {\n const n = this.#items.length\n if (n === 0) return\n this.active -= this.#direction\n },\n } satisfies NodeActionMap\n\n /** Top of the currently-rendered window, in absolute item indices.\n * Updated per render using the pin-until-leave rule: stays put\n * while `active` remains in `[start, start + visible)`, slides just\n * enough to re-admit it otherwise. */\n #windowStart = 0\n\n constructor(initial: SelectState<T>) {\n super({ active: 0, ...initial } as SelectState<T>)\n\n let active = this.item\n effect(() => {\n const next = this.item\n if (next === active) return\n active = next\n void this.emit(\"changed\", { active: this.active, item: next })\n })\n }\n\n get #direction(): 1 | -1 {\n return this.state.reverse ? -1 : 1\n }\n\n get #items() {\n return unwrap(this.state.items)\n }\n\n #clamp(i: number): number {\n const n = this.#items.length\n return n === 0 ? 0 : Math.max(0, Math.min(n - 1, i))\n }\n\n set active(i: number) {\n const n = this.#items.length\n this.state.active = n === 0 ? 0 : ((i % n) + n) % n\n }\n\n get active(): number {\n const a = this.state.active ?? 0\n const n = this.#items.length\n if (n === 0) return 0\n return Math.max(0, Math.min(n - 1, a))\n }\n\n bind(node: Node | Ref<Node>): this {\n let n: Node | undefined\n const getn = () => (n = node instanceof Node ? node : node())\n this.on(\"mount\", () => getn().addActionTarget(this))\n this.on(\"unmount\", () => n?.removeActionTarget(this))\n if (this.mounted) getn().addActionTarget(this)\n return this\n }\n\n get count(): number {\n return this.#items.length\n }\n\n get pageSize(): number {\n return unwrap(this.state.maxHeight) ?? Math.max(this.#items.length, 1)\n }\n\n get item(): T | undefined {\n return this.#items[this.active]\n }\n\n protected _render(ctx: RenderCtx): string[] {\n const items = this.#items\n if (items.length === 0) return []\n\n const width = resolveSize(this.state.width ?? \"fill\", ctx.width) ?? ctx.width\n const active = this.active\n const height = Math.min(this.pageSize, items.length)\n if (height === 0) return []\n\n // Pin-until-leave window. `#windowStart` is kept across renders so\n // the list doesn't recenter while the user nudges up/down inside\n let start = this.#windowStart\n if (active < start) start = active\n else if (active >= start + height) start = active - height + 1\n start = Math.max(0, Math.min(Math.max(0, items.length - height), start))\n this.#windowStart = start\n\n const visible = items.slice(start, start + height)\n const renderer = this.renderer\n\n const rows: string[] = []\n for (let i = start; i < start + height; i++) {\n const item = items[i]\n let row = renderer(item, {\n ...ctx,\n active: i === active,\n items,\n visible,\n }).replace(/\\n/g, \" \")\n row = fitAnsi(row, width)\n rows.push(i === active ? ctx.style.optionActive(row) : row)\n }\n if (this.state.reverse) rows.reverse()\n\n if (this.state.counter ?? items.length > height) {\n const shown = items.length === 0 ? 0 : active + 1\n const label = `(${shown}/${items.length}${this.state.more ? \"+\" : \"\"})`\n rows.push(ctx.style.gutter(fitAnsi(label, width)))\n }\n\n return rows\n }\n\n get renderer(): OptionRender<T> {\n return unwrap(this.state.render) ?? this.defaultRenderer()\n }\n\n extendRenderer(fn: (prev: OptionRender<T>) => OptionRender<T>): this {\n const prev = this.renderer\n this.state.render = fn(prev)\n return this\n }\n\n defaultRenderer(): OptionRender<Option> {\n let visible: readonly Option[] = []\n let labelWidth = 0\n\n const update = (ctx: OptionRenderCtx<Option>) => {\n if (ctx.visible === visible) return\n visible = ctx.visible\n if (!visible.some((i) => i.desc)) return (labelWidth = ctx.width) // No desc, so full width\n labelWidth = this.state.labelWidth ?? 0\n labelWidth ||= visible\n .map(itemLabel)\n .reduce((max, label) => Math.max(max, stringWidth(label)), 0)\n labelWidth = Math.min(labelWidth, Math.floor(ctx.width / 2))\n }\n\n return (item: Option, ctx): string => {\n update(ctx)\n const label = fitAnsi(itemLabel(item), labelWidth)\n if (!item.desc) return ctx.style.optionName(label)\n const desc = fitAnsi(item.desc ?? \"\", Math.max(0, ctx.width - labelWidth - 2))\n return `${ctx.style.optionName(label)} ${ctx.style.optionDesc(desc)}`\n }\n }\n}\n\nconst itemLabel = (item: Option): string => (item.name ?? item.text).replace(/\\s+/g, \" \").trim()\n\n/**\n * Factory for a selectable list.\n *\n * ```ts\n * const s = select({ items: [{ text: \"/help\" }, { text: \"/quit\" }] })\n * s.on(\"accept\", ({ item }) => console.log(\"picked\", item.text))\n * ```\n */\nexport function select<T extends Option = Option>(state: State<SelectState<T>>): Select<T> {\n return new Select<T>(state)\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { P as RenderCtx, nt as Reactive, p as Node } from "../queries-DH3bj74K.mjs";
|
|
2
|
+
//#region src/widgets/show.d.ts
|
|
3
|
+
type ShowGate = {
|
|
4
|
+
when: Reactive<unknown>;
|
|
5
|
+
};
|
|
6
|
+
type ShowBranch = {
|
|
7
|
+
when: Reactive<unknown>;
|
|
8
|
+
use: ShowChildren;
|
|
9
|
+
};
|
|
10
|
+
type ShowChildren = () => Node | Node[];
|
|
11
|
+
type ShowSpec = (ShowGate | ShowBranch)[] | [...(ShowGate | ShowBranch)[], ShowChildren];
|
|
12
|
+
/** Conditional fragment backing `show()`. Branches are created lazily:
|
|
13
|
+
* a `use` factory only runs the first time its condition wins. Once
|
|
14
|
+
* created, the branch remains mounted and owned; inactive branches simply
|
|
15
|
+
* don't participate in layout/rendering and contribute zero rows.
|
|
16
|
+
*
|
|
17
|
+
* Lazy factories run under the owner captured at `show()` creation time,
|
|
18
|
+
* so context reads behave as if the branch had been constructed eagerly.
|
|
19
|
+
*/
|
|
20
|
+
declare class Show extends Node {
|
|
21
|
+
#private;
|
|
22
|
+
constructor(spec: ShowSpec);
|
|
23
|
+
layoutChildren(): readonly Node[];
|
|
24
|
+
/** Standalone-render fallback. Box parents go through
|
|
25
|
+
* `layoutChildren` and bypass this; only fires when `Show` sits
|
|
26
|
+
* outside a layout container (root, overlay, etc.). */
|
|
27
|
+
protected _render(ctx: RenderCtx): Promise<string[]>;
|
|
28
|
+
}
|
|
29
|
+
/** Lazy conditional rendering.
|
|
30
|
+
*
|
|
31
|
+
* Specs are evaluated from left to right:
|
|
32
|
+
* - `{ when }` is a guard. When false, rendering stops with no rows; when
|
|
33
|
+
* true, evaluation continues.
|
|
34
|
+
* - `{ when, use }` is a conditional branch. The first branch whose
|
|
35
|
+
* condition is truthy is rendered.
|
|
36
|
+
* - A final bare factory is the fallback branch and must be the last
|
|
37
|
+
* argument.
|
|
38
|
+
*
|
|
39
|
+
* Branch factories are lazy and run at most once. Created branches stay
|
|
40
|
+
* mounted across toggles, preserving state, effects, and async resources.
|
|
41
|
+
*
|
|
42
|
+
* ```ts
|
|
43
|
+
* show({ when: isLoading }, () => spinner({ color: "accent" }))
|
|
44
|
+
*
|
|
45
|
+
* show(
|
|
46
|
+
* { when: full },
|
|
47
|
+
* { when: isError, use: () => errorView() },
|
|
48
|
+
* () => resultView()
|
|
49
|
+
* )
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
declare function show(...spec: ShowSpec): Show;
|
|
53
|
+
//#endregion
|
|
54
|
+
export { type Show, ShowBranch, ShowChildren, ShowGate, ShowSpec, show };
|
|
55
|
+
//# sourceMappingURL=show.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"show.d.mts","names":[],"sources":["../../src/widgets/show.ts"],"mappings":";;KAMY;EAAa,MAAM;;KACnB;EAAe,MAAM;EAAmB,KAAK;;KAC7C,qBAAqB,OAAO;KAC5B,YAAY,WAAW,qBAAqB,WAAW,eAAe;;;;;;;;;cAkB5E,aAAa;;EAIjB,YAAY,MAAM;EAqBT,2BAA2B;;;;YAepB,QAAQ,KAAK,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;iBA8B3B,QAAQ,MAAM,WAAW"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { C as useActiveOwner, E as withOwner, S as unwrap, x as untrack } from "../reactive-f7NpAi69.mjs";
|
|
2
|
+
import { t as Node } from "../node-BZ7f6HzU.mjs";
|
|
3
|
+
//#region src/widgets/show.ts
|
|
4
|
+
/** Conditional fragment backing `show()`. Branches are created lazily:
|
|
5
|
+
* a `use` factory only runs the first time its condition wins. Once
|
|
6
|
+
* created, the branch remains mounted and owned; inactive branches simply
|
|
7
|
+
* don't participate in layout/rendering and contribute zero rows.
|
|
8
|
+
*
|
|
9
|
+
* Lazy factories run under the owner captured at `show()` creation time,
|
|
10
|
+
* so context reads behave as if the branch had been constructed eagerly.
|
|
11
|
+
*/
|
|
12
|
+
var Show = class extends Node {
|
|
13
|
+
#state = [];
|
|
14
|
+
#owner = useActiveOwner();
|
|
15
|
+
constructor(spec) {
|
|
16
|
+
super({});
|
|
17
|
+
let fallback = false;
|
|
18
|
+
for (const s of spec) {
|
|
19
|
+
if (fallback) throw new Error("Show: `fallback` must be the final argument");
|
|
20
|
+
if (typeof s === "function") {
|
|
21
|
+
this.#state.push({
|
|
22
|
+
use: s,
|
|
23
|
+
when: true
|
|
24
|
+
});
|
|
25
|
+
fallback = true;
|
|
26
|
+
} else this.#state.push(s);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
#nodes(s, use) {
|
|
30
|
+
if (s.children) return s.children;
|
|
31
|
+
const create = () => untrack(use);
|
|
32
|
+
const nodes = this.#owner ? withOwner(this.#owner, create) : create();
|
|
33
|
+
s.children = Array.isArray(nodes) ? nodes : [nodes];
|
|
34
|
+
for (const c of s.children) this.add(c);
|
|
35
|
+
return s.children;
|
|
36
|
+
}
|
|
37
|
+
layoutChildren() {
|
|
38
|
+
for (const s of this.#state) {
|
|
39
|
+
const matches = unwrap(s.when);
|
|
40
|
+
if (s.use === void 0) {
|
|
41
|
+
if (!matches) return [];
|
|
42
|
+
continue;
|
|
43
|
+
} else if (!matches) continue;
|
|
44
|
+
return this.#nodes(s, s.use);
|
|
45
|
+
}
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
/** Standalone-render fallback. Box parents go through
|
|
49
|
+
* `layoutChildren` and bypass this; only fires when `Show` sits
|
|
50
|
+
* outside a layout container (root, overlay, etc.). */
|
|
51
|
+
async _render(ctx) {
|
|
52
|
+
const targets = this.layoutChildren();
|
|
53
|
+
return (await Promise.all(targets.map((c) => c.render(ctx)))).flat();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
/** Lazy conditional rendering.
|
|
57
|
+
*
|
|
58
|
+
* Specs are evaluated from left to right:
|
|
59
|
+
* - `{ when }` is a guard. When false, rendering stops with no rows; when
|
|
60
|
+
* true, evaluation continues.
|
|
61
|
+
* - `{ when, use }` is a conditional branch. The first branch whose
|
|
62
|
+
* condition is truthy is rendered.
|
|
63
|
+
* - A final bare factory is the fallback branch and must be the last
|
|
64
|
+
* argument.
|
|
65
|
+
*
|
|
66
|
+
* Branch factories are lazy and run at most once. Created branches stay
|
|
67
|
+
* mounted across toggles, preserving state, effects, and async resources.
|
|
68
|
+
*
|
|
69
|
+
* ```ts
|
|
70
|
+
* show({ when: isLoading }, () => spinner({ color: "accent" }))
|
|
71
|
+
*
|
|
72
|
+
* show(
|
|
73
|
+
* { when: full },
|
|
74
|
+
* { when: isError, use: () => errorView() },
|
|
75
|
+
* () => resultView()
|
|
76
|
+
* )
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
function show(...spec) {
|
|
80
|
+
return new Show(spec);
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
export { show };
|
|
84
|
+
|
|
85
|
+
//# sourceMappingURL=show.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"show.mjs","names":["#state","#owner","#nodes"],"sources":["../../src/widgets/show.ts"],"sourcesContent":["import type { RenderCtx } from \"../core/ctx.ts\"\nimport type { Reactive } from \"../core/reactive.ts\"\n\nimport { Node } from \"../core/node.ts\"\nimport { untrack, unwrap, useActiveOwner, withOwner } from \"../core/reactive.ts\"\n\nexport type ShowGate = { when: Reactive<unknown> }\nexport type ShowBranch = { when: Reactive<unknown>; use: ShowChildren }\nexport type ShowChildren = () => Node | Node[]\nexport type ShowSpec = (ShowGate | ShowBranch)[] | [...(ShowGate | ShowBranch)[], ShowChildren]\n\ntype ShowState = {\n when: Reactive<unknown>\n use?: ShowChildren\n children?: Node[]\n}\n\nexport type { Show }\n\n/** Conditional fragment backing `show()`. Branches are created lazily:\n * a `use` factory only runs the first time its condition wins. Once\n * created, the branch remains mounted and owned; inactive branches simply\n * don't participate in layout/rendering and contribute zero rows.\n *\n * Lazy factories run under the owner captured at `show()` creation time,\n * so context reads behave as if the branch had been constructed eagerly.\n */\nclass Show extends Node {\n #state: ShowState[] = []\n #owner = useActiveOwner()\n\n constructor(spec: ShowSpec) {\n super({})\n let fallback = false\n for (const s of spec) {\n if (fallback) throw new Error(\"Show: `fallback` must be the final argument\")\n if (typeof s === \"function\") {\n this.#state.push({ use: s, when: true })\n fallback = true\n } else this.#state.push(s)\n }\n }\n\n #nodes(s: ShowState, use: ShowChildren): Node[] {\n if (s.children) return s.children\n const create = () => untrack(use)\n const nodes = this.#owner ? withOwner(this.#owner, create) : create()\n s.children = Array.isArray(nodes) ? nodes : [nodes]\n for (const c of s.children) this.add(c)\n return s.children\n }\n\n override layoutChildren(): readonly Node[] {\n for (const s of this.#state) {\n const matches = unwrap(s.when)\n if (s.use === undefined) {\n if (!matches) return []\n continue\n } else if (!matches) continue\n return this.#nodes(s, s.use)\n }\n return []\n }\n\n /** Standalone-render fallback. Box parents go through\n * `layoutChildren` and bypass this; only fires when `Show` sits\n * outside a layout container (root, overlay, etc.). */\n protected async _render(ctx: RenderCtx): Promise<string[]> {\n const targets = this.layoutChildren()\n const rows = await Promise.all(targets.map((c) => c.render(ctx)))\n return rows.flat()\n }\n}\n\n/** Lazy conditional rendering.\n *\n * Specs are evaluated from left to right:\n * - `{ when }` is a guard. When false, rendering stops with no rows; when\n * true, evaluation continues.\n * - `{ when, use }` is a conditional branch. The first branch whose\n * condition is truthy is rendered.\n * - A final bare factory is the fallback branch and must be the last\n * argument.\n *\n * Branch factories are lazy and run at most once. Created branches stay\n * mounted across toggles, preserving state, effects, and async resources.\n *\n * ```ts\n * show({ when: isLoading }, () => spinner({ color: \"accent\" }))\n *\n * show(\n * { when: full },\n * { when: isError, use: () => errorView() },\n * () => resultView()\n * )\n * ```\n */\nexport function show(...spec: ShowSpec): Show {\n return new Show(spec)\n}\n"],"mappings":";;;;;;;;;;;AA2BA,IAAM,OAAN,cAAmB,KAAK;CACtB,SAAsB,CAAC;CACvB,SAAS,eAAe;CAExB,YAAY,MAAgB;EAC1B,MAAM,CAAC,CAAC;EACR,IAAI,WAAW;EACf,KAAK,MAAM,KAAK,MAAM;GACpB,IAAI,UAAU,MAAM,IAAI,MAAM,6CAA6C;GAC3E,IAAI,OAAO,MAAM,YAAY;IAC3B,KAAKA,OAAO,KAAK;KAAE,KAAK;KAAG,MAAM;IAAK,CAAC;IACvC,WAAW;GACb,OAAO,KAAKA,OAAO,KAAK,CAAC;EAC3B;CACF;CAEA,OAAO,GAAc,KAA2B;EAC9C,IAAI,EAAE,UAAU,OAAO,EAAE;EACzB,MAAM,eAAe,QAAQ,GAAG;EAChC,MAAM,QAAQ,KAAKC,SAAS,UAAU,KAAKA,QAAQ,MAAM,IAAI,OAAO;EACpE,EAAE,WAAW,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK;EAClD,KAAK,MAAM,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;EACtC,OAAO,EAAE;CACX;CAEA,iBAA2C;EACzC,KAAK,MAAM,KAAK,KAAKD,QAAQ;GAC3B,MAAM,UAAU,OAAO,EAAE,IAAI;GAC7B,IAAI,EAAE,QAAQ,KAAA,GAAW;IACvB,IAAI,CAAC,SAAS,OAAO,CAAC;IACtB;GACF,OAAO,IAAI,CAAC,SAAS;GACrB,OAAO,KAAKE,OAAO,GAAG,EAAE,GAAG;EAC7B;EACA,OAAO,CAAC;CACV;;;;CAKA,MAAgB,QAAQ,KAAmC;EACzD,MAAM,UAAU,KAAK,eAAe;EAEpC,QAAO,MADY,QAAQ,IAAI,QAAQ,KAAK,MAAM,EAAE,OAAO,GAAG,CAAC,CAAC,EAAA,CACpD,KAAK;CACnB;AACF;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,KAAK,GAAG,MAAsB;CAC5C,OAAO,IAAI,KAAK,IAAI;AACtB"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { P as RenderCtx, X as State, nt as Reactive, p as Node } from "../queries-DH3bj74K.mjs";
|
|
2
|
+
import { o as AnyStyle } from "../types-B9Eh7k2v.mjs";
|
|
3
|
+
//#region src/widgets/spinner.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Frame sets from the common terminal-spinner vocabulary. Pick one to taste.
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
declare const spinnerFrames: {
|
|
9
|
+
readonly arrow: readonly ["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"];
|
|
10
|
+
readonly bouncingBar: readonly ["[ ]", "[= ]", "[== ]", "[=== ]", "[ ===]", "[ ==]", "[ =]", "[ ]"];
|
|
11
|
+
readonly circle: readonly ["◐", "◓", "◑", "◒"];
|
|
12
|
+
readonly dots: readonly ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
13
|
+
readonly line: readonly ["-", "\\", "|", "/"];
|
|
14
|
+
};
|
|
15
|
+
type SpinnerStyle = keyof typeof spinnerFrames;
|
|
16
|
+
interface SpinnerState {
|
|
17
|
+
/** Frame glyphs, cycled in order. Defaults to `dots`. */
|
|
18
|
+
frames?: SpinnerStyle | readonly string[];
|
|
19
|
+
/** Foreground theme slot or explicit color. Defaults to `primary`. */
|
|
20
|
+
color?: Reactive<AnyStyle>;
|
|
21
|
+
/** Whether the animation is ticking. Defaults to `true`. Accepts a
|
|
22
|
+
* signal accessor so callers can drive the spinner from shared
|
|
23
|
+
* reactive state. Setting `false` stops the interval; setting
|
|
24
|
+
* `true` restarts it. */
|
|
25
|
+
running?: Reactive<boolean>;
|
|
26
|
+
idle?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* An animated spinner. Its *frame* is always a pure function of wall
|
|
30
|
+
* time and `speed`:
|
|
31
|
+
*
|
|
32
|
+
* ```ts
|
|
33
|
+
* const idx = Spinner.tick(speed) % frames.length
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* That means the visible frame only depends on how much time has
|
|
37
|
+
* actually elapsed, never on how often you happen to render. Two
|
|
38
|
+
* spinners with the same `speed` stay in lockstep even if only one of
|
|
39
|
+
* them gets invalidated.
|
|
40
|
+
*
|
|
41
|
+
* Each spinner owns an `unref()`'d interval while it is mounted,
|
|
42
|
+
* visible, and running. The interval is reconciled from reactive
|
|
43
|
+
* state/lifecycle, not from `_render`, so hiding a spinner stops it
|
|
44
|
+
* even though invisible nodes skip rendering. Forgetting to `.stop()`
|
|
45
|
+
* is harmless — the unref'd timer doesn't pin the event loop.
|
|
46
|
+
*/
|
|
47
|
+
declare class Spinner extends Node<SpinnerState> {
|
|
48
|
+
#private;
|
|
49
|
+
constructor(state: SpinnerState);
|
|
50
|
+
/** Global tick index for a given `speed`. Shared across all callers. */
|
|
51
|
+
static tick(speed: number): number;
|
|
52
|
+
/** Shorthand for `state.set({ running: true })`. */
|
|
53
|
+
start(): this;
|
|
54
|
+
/** Shorthand for `state.set({ running: false })`. */
|
|
55
|
+
stop(): this;
|
|
56
|
+
protected _render(ctx: RenderCtx): string[];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Factory for `Spinner`. All options are optional — a bare `spinner()`
|
|
60
|
+
* gives you the braille `dots` set at 80ms in the theme's `primary` color.
|
|
61
|
+
*/
|
|
62
|
+
declare function spinner(state?: State<SpinnerState>): Spinner;
|
|
63
|
+
//#endregion
|
|
64
|
+
export { Spinner, SpinnerState, SpinnerStyle, spinner, spinnerFrames };
|
|
65
|
+
//# sourceMappingURL=spinner.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spinner.d.mts","names":[],"sources":["../../src/widgets/spinner.ts"],"mappings":";;;;;;;cAaa;WACX;WACA;WACA;WACA;WACA;;KAGU,4BAA4B;UAEvB;;EAEf,SAAS;;EAET,QAAQ,SAAS;;;;;EAKjB,UAAU;EACV;;;;;;;;;;;;;;;;;;;;;cA4DW,gBAAgB,KAAK;;EAChC,YAAY,OAAO;;SAYZ,KAAK;;EAKZ;;EAMA;YAcU,QAAQ,KAAK;;;;;;iBAoBT,QAAQ,QAAO,MAAM,gBAAqB"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { S as unwrap, f as effect, x as untrack } from "../reactive-f7NpAi69.mjs";
|
|
2
|
+
import { t as Node } from "../node-BZ7f6HzU.mjs";
|
|
3
|
+
import { stringWidth } from "@zaly/shared/ansi";
|
|
4
|
+
//#region src/widgets/spinner.ts
|
|
5
|
+
/**
|
|
6
|
+
* Frame sets from the common terminal-spinner vocabulary. Pick one to taste.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
const spinnerFrames = {
|
|
10
|
+
arrow: [
|
|
11
|
+
"←",
|
|
12
|
+
"↖",
|
|
13
|
+
"↑",
|
|
14
|
+
"↗",
|
|
15
|
+
"→",
|
|
16
|
+
"↘",
|
|
17
|
+
"↓",
|
|
18
|
+
"↙"
|
|
19
|
+
],
|
|
20
|
+
bouncingBar: [
|
|
21
|
+
"[ ]",
|
|
22
|
+
"[= ]",
|
|
23
|
+
"[== ]",
|
|
24
|
+
"[=== ]",
|
|
25
|
+
"[ ===]",
|
|
26
|
+
"[ ==]",
|
|
27
|
+
"[ =]",
|
|
28
|
+
"[ ]"
|
|
29
|
+
],
|
|
30
|
+
circle: [
|
|
31
|
+
"◐",
|
|
32
|
+
"◓",
|
|
33
|
+
"◑",
|
|
34
|
+
"◒"
|
|
35
|
+
],
|
|
36
|
+
dots: [
|
|
37
|
+
"⠋",
|
|
38
|
+
"⠙",
|
|
39
|
+
"⠹",
|
|
40
|
+
"⠸",
|
|
41
|
+
"⠼",
|
|
42
|
+
"⠴",
|
|
43
|
+
"⠦",
|
|
44
|
+
"⠧",
|
|
45
|
+
"⠇",
|
|
46
|
+
"⠏"
|
|
47
|
+
],
|
|
48
|
+
line: [
|
|
49
|
+
"-",
|
|
50
|
+
"\\",
|
|
51
|
+
"|",
|
|
52
|
+
"/"
|
|
53
|
+
]
|
|
54
|
+
};
|
|
55
|
+
const SPEED = 80;
|
|
56
|
+
var Animator = class {
|
|
57
|
+
#timer;
|
|
58
|
+
#spinners = /* @__PURE__ */ new Set();
|
|
59
|
+
#start() {
|
|
60
|
+
if (this.#timer !== void 0) return;
|
|
61
|
+
this.#timer = untrack(() => setInterval(() => this.#spinners.forEach((s) => s.invalidate()), SPEED));
|
|
62
|
+
this.#timer.unref();
|
|
63
|
+
}
|
|
64
|
+
#stop() {
|
|
65
|
+
if (this.#timer === void 0) return;
|
|
66
|
+
clearInterval(this.#timer);
|
|
67
|
+
this.#timer = void 0;
|
|
68
|
+
}
|
|
69
|
+
add(node) {
|
|
70
|
+
if (this.#spinners.has(node)) return;
|
|
71
|
+
this.#spinners.add(node);
|
|
72
|
+
this.#start();
|
|
73
|
+
}
|
|
74
|
+
del(node) {
|
|
75
|
+
if (!this.#spinners.has(node)) return;
|
|
76
|
+
this.#spinners.delete(node);
|
|
77
|
+
if (this.#spinners.size === 0) this.#stop();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const animator = new Animator();
|
|
81
|
+
/**
|
|
82
|
+
* An animated spinner. Its *frame* is always a pure function of wall
|
|
83
|
+
* time and `speed`:
|
|
84
|
+
*
|
|
85
|
+
* ```ts
|
|
86
|
+
* const idx = Spinner.tick(speed) % frames.length
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* That means the visible frame only depends on how much time has
|
|
90
|
+
* actually elapsed, never on how often you happen to render. Two
|
|
91
|
+
* spinners with the same `speed` stay in lockstep even if only one of
|
|
92
|
+
* them gets invalidated.
|
|
93
|
+
*
|
|
94
|
+
* Each spinner owns an `unref()`'d interval while it is mounted,
|
|
95
|
+
* visible, and running. The interval is reconciled from reactive
|
|
96
|
+
* state/lifecycle, not from `_render`, so hiding a spinner stops it
|
|
97
|
+
* even though invisible nodes skip rendering. Forgetting to `.stop()`
|
|
98
|
+
* is harmless — the unref'd timer doesn't pin the event loop.
|
|
99
|
+
*/
|
|
100
|
+
var Spinner = class Spinner extends Node {
|
|
101
|
+
constructor(state) {
|
|
102
|
+
super(state);
|
|
103
|
+
this.on("mount", () => this.#check());
|
|
104
|
+
this.on("unmount", () => animator.del(this));
|
|
105
|
+
effect(() => this.#check());
|
|
106
|
+
}
|
|
107
|
+
/** Global tick index for a given `speed`. Shared across all callers. */
|
|
108
|
+
static tick(speed) {
|
|
109
|
+
return Math.floor(performance.now() / speed);
|
|
110
|
+
}
|
|
111
|
+
/** Shorthand for `state.set({ running: true })`. */
|
|
112
|
+
start() {
|
|
113
|
+
this.state.set({ running: true });
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
/** Shorthand for `state.set({ running: false })`. */
|
|
117
|
+
stop() {
|
|
118
|
+
this.state.set({ running: false });
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
121
|
+
/** Sync the interval to the current `running` state. Reading through
|
|
122
|
+
* `unwrap` during a render subscribes the spinner to a signal
|
|
123
|
+
* accessor so flips from elsewhere in the app retrigger this. */
|
|
124
|
+
#check() {
|
|
125
|
+
if (!(unwrap(this.state.running ?? true) && this.visible && this.mounted)) return animator.del(this);
|
|
126
|
+
animator.add(this);
|
|
127
|
+
}
|
|
128
|
+
_render(ctx) {
|
|
129
|
+
const f = this.state.frames;
|
|
130
|
+
const frames = (typeof f === "string" ? spinnerFrames[f] : f) ?? spinnerFrames.dots;
|
|
131
|
+
let frame = frames[Spinner.tick(SPEED) % frames.length];
|
|
132
|
+
if (!unwrap(this.state.running ?? true)) frame = (this.state.idle ?? " ").repeat(stringWidth(frame));
|
|
133
|
+
const color = this.state.color ?? "primary";
|
|
134
|
+
return [ctx.style.add(unwrap(color))(frame)];
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Factory for `Spinner`. All options are optional — a bare `spinner()`
|
|
139
|
+
* gives you the braille `dots` set at 80ms in the theme's `primary` color.
|
|
140
|
+
*/
|
|
141
|
+
function spinner(state = {}) {
|
|
142
|
+
return new Spinner(state);
|
|
143
|
+
}
|
|
144
|
+
//#endregion
|
|
145
|
+
export { Spinner, spinner, spinnerFrames };
|
|
146
|
+
|
|
147
|
+
//# sourceMappingURL=spinner.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spinner.mjs","names":["#timer","#spinners","#start","#stop","#check"],"sources":["../../src/widgets/spinner.ts"],"sourcesContent":["import type { RenderCtx } from \"../core/ctx.ts\"\nimport type { Reactive } from \"../core/reactive.ts\"\nimport type { State } from \"../core/state.ts\"\nimport type { AnyStyle } from \"../style/types.ts\"\n\nimport { stringWidth } from \"@zaly/shared/ansi\"\nimport { Node } from \"../core/node.ts\"\nimport { effect, untrack, unwrap } from \"../core/reactive.ts\"\n\n/**\n * Frame sets from the common terminal-spinner vocabulary. Pick one to taste.\n * @internal\n */\nexport const spinnerFrames = {\n arrow: [\"←\", \"↖\", \"↑\", \"↗\", \"→\", \"↘\", \"↓\", \"↙\"],\n bouncingBar: [\"[ ]\", \"[= ]\", \"[== ]\", \"[=== ]\", \"[ ===]\", \"[ ==]\", \"[ =]\", \"[ ]\"],\n circle: [\"◐\", \"◓\", \"◑\", \"◒\"],\n dots: [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"],\n line: [\"-\", \"\\\\\", \"|\", \"/\"],\n} as const\n\nexport type SpinnerStyle = keyof typeof spinnerFrames\n\nexport interface SpinnerState {\n /** Frame glyphs, cycled in order. Defaults to `dots`. */\n frames?: SpinnerStyle | readonly string[]\n /** Foreground theme slot or explicit color. Defaults to `primary`. */\n color?: Reactive<AnyStyle>\n /** Whether the animation is ticking. Defaults to `true`. Accepts a\n * signal accessor so callers can drive the spinner from shared\n * reactive state. Setting `false` stops the interval; setting\n * `true` restarts it. */\n running?: Reactive<boolean>\n idle?: string\n}\n\nconst SPEED = 80\n\nclass Animator {\n #timer?: ReturnType<typeof setInterval>\n #spinners = new Set<Spinner>()\n\n #start(): void {\n if (this.#timer !== undefined) return\n // `untracked` so the interval callback doesn't inherit the render's tracking ctx.\n this.#timer = untrack(() =>\n setInterval(() => this.#spinners.forEach((s) => s.invalidate()), SPEED)\n )\n // Don't pin the event loop — a forgotten spinner should never\n // prevent the process from exiting.\n this.#timer.unref()\n }\n\n #stop(): void {\n if (this.#timer === undefined) return\n clearInterval(this.#timer)\n this.#timer = undefined\n }\n\n add(node: Spinner) {\n if (this.#spinners.has(node)) return\n this.#spinners.add(node)\n this.#start()\n }\n\n del(node: Spinner) {\n if (!this.#spinners.has(node)) return\n this.#spinners.delete(node)\n if (this.#spinners.size === 0) this.#stop()\n }\n}\n\nconst animator = new Animator()\n\n/**\n * An animated spinner. Its *frame* is always a pure function of wall\n * time and `speed`:\n *\n * ```ts\n * const idx = Spinner.tick(speed) % frames.length\n * ```\n *\n * That means the visible frame only depends on how much time has\n * actually elapsed, never on how often you happen to render. Two\n * spinners with the same `speed` stay in lockstep even if only one of\n * them gets invalidated.\n *\n * Each spinner owns an `unref()`'d interval while it is mounted,\n * visible, and running. The interval is reconciled from reactive\n * state/lifecycle, not from `_render`, so hiding a spinner stops it\n * even though invisible nodes skip rendering. Forgetting to `.stop()`\n * is harmless — the unref'd timer doesn't pin the event loop.\n */\nexport class Spinner extends Node<SpinnerState> {\n constructor(state: SpinnerState) {\n super(state)\n // Timer lifecycle is driven by mount + reactive state, not `_render`:\n // invisible nodes skip rendering, but still need their interval stopped.\n // Unmount always clears.\n this.on(\"mount\", () => this.#check())\n this.on(\"unmount\", () => animator.del(this))\n\n effect(() => this.#check())\n }\n\n /** Global tick index for a given `speed`. Shared across all callers. */\n static tick(speed: number): number {\n return Math.floor(performance.now() / speed)\n }\n\n /** Shorthand for `state.set({ running: true })`. */\n start(): this {\n this.state.set({ running: true })\n return this\n }\n\n /** Shorthand for `state.set({ running: false })`. */\n stop(): this {\n this.state.set({ running: false })\n return this\n }\n\n /** Sync the interval to the current `running` state. Reading through\n * `unwrap` during a render subscribes the spinner to a signal\n * accessor so flips from elsewhere in the app retrigger this. */\n #check(): void {\n const enabled = unwrap(this.state.running ?? true) && this.visible && this.mounted // track\n if (!enabled) return animator.del(this)\n animator.add(this)\n }\n\n protected _render(ctx: RenderCtx): string[] {\n const f = this.state.frames\n const frames = (typeof f === \"string\" ? spinnerFrames[f] : f) ?? spinnerFrames.dots\n let frame = frames[Spinner.tick(SPEED) % frames.length]\n // When stopped, hold the slot open with blank space of the same\n // cell width so surrounding layout doesn't jump as the spinner\n // toggles. Stable-width frame sets (dots, line, circle) look the\n // same; variable sets (bouncingBar) pick the longest frame's\n // width so stops after a short frame still hold full space.\n if (!unwrap(this.state.running ?? true))\n frame = (this.state.idle ?? \" \").repeat(stringWidth(frame))\n const color = this.state.color ?? \"primary\"\n return [ctx.style.add(unwrap(color))(frame)]\n }\n}\n\n/**\n * Factory for `Spinner`. All options are optional — a bare `spinner()`\n * gives you the braille `dots` set at 80ms in the theme's `primary` color.\n */\nexport function spinner(state: State<SpinnerState> = {}): Spinner {\n return new Spinner(state)\n}\n"],"mappings":";;;;;;;;AAaA,MAAa,gBAAgB;CAC3B,OAAO;EAAC;EAAK;EAAK;EAAK;EAAK;EAAK;EAAK;EAAK;CAAG;CAC9C,aAAa;EAAC;EAAU;EAAU;EAAU;EAAU;EAAU;EAAU;EAAU;CAAQ;CAC5F,QAAQ;EAAC;EAAK;EAAK;EAAK;CAAG;CAC3B,MAAM;EAAC;EAAK;EAAK;EAAK;EAAK;EAAK;EAAK;EAAK;EAAK;EAAK;CAAG;CACvD,MAAM;EAAC;EAAK;EAAM;EAAK;CAAG;AAC5B;AAiBA,MAAM,QAAQ;AAEd,IAAM,WAAN,MAAe;CACb;CACA,4BAAY,IAAI,IAAa;CAE7B,SAAe;EACb,IAAI,KAAKA,WAAW,KAAA,GAAW;EAE/B,KAAKA,SAAS,cACZ,kBAAkB,KAAKC,UAAU,SAAS,MAAM,EAAE,WAAW,CAAC,GAAG,KAAK,CACxE;EAGA,KAAKD,OAAO,MAAM;CACpB;CAEA,QAAc;EACZ,IAAI,KAAKA,WAAW,KAAA,GAAW;EAC/B,cAAc,KAAKA,MAAM;EACzB,KAAKA,SAAS,KAAA;CAChB;CAEA,IAAI,MAAe;EACjB,IAAI,KAAKC,UAAU,IAAI,IAAI,GAAG;EAC9B,KAAKA,UAAU,IAAI,IAAI;EACvB,KAAKC,OAAO;CACd;CAEA,IAAI,MAAe;EACjB,IAAI,CAAC,KAAKD,UAAU,IAAI,IAAI,GAAG;EAC/B,KAAKA,UAAU,OAAO,IAAI;EAC1B,IAAI,KAAKA,UAAU,SAAS,GAAG,KAAKE,MAAM;CAC5C;AACF;AAEA,MAAM,WAAW,IAAI,SAAS;;;;;;;;;;;;;;;;;;;;AAqB9B,IAAa,UAAb,MAAa,gBAAgB,KAAmB;CAC9C,YAAY,OAAqB;EAC/B,MAAM,KAAK;EAIX,KAAK,GAAG,eAAe,KAAKC,OAAO,CAAC;EACpC,KAAK,GAAG,iBAAiB,SAAS,IAAI,IAAI,CAAC;EAE3C,aAAa,KAAKA,OAAO,CAAC;CAC5B;;CAGA,OAAO,KAAK,OAAuB;EACjC,OAAO,KAAK,MAAM,YAAY,IAAI,IAAI,KAAK;CAC7C;;CAGA,QAAc;EACZ,KAAK,MAAM,IAAI,EAAE,SAAS,KAAK,CAAC;EAChC,OAAO;CACT;;CAGA,OAAa;EACX,KAAK,MAAM,IAAI,EAAE,SAAS,MAAM,CAAC;EACjC,OAAO;CACT;;;;CAKA,SAAe;EAEb,IAAI,EADY,OAAO,KAAK,MAAM,WAAW,IAAI,KAAK,KAAK,WAAW,KAAK,UAC7D,OAAO,SAAS,IAAI,IAAI;EACtC,SAAS,IAAI,IAAI;CACnB;CAEA,QAAkB,KAA0B;EAC1C,MAAM,IAAI,KAAK,MAAM;EACrB,MAAM,UAAU,OAAO,MAAM,WAAW,cAAc,KAAK,MAAM,cAAc;EAC/E,IAAI,QAAQ,OAAO,QAAQ,KAAK,KAAK,IAAI,OAAO;EAMhD,IAAI,CAAC,OAAO,KAAK,MAAM,WAAW,IAAI,GACpC,SAAS,KAAK,MAAM,QAAQ,IAAA,CAAK,OAAO,YAAY,KAAK,CAAC;EAC5D,MAAM,QAAQ,KAAK,MAAM,SAAS;EAClC,OAAO,CAAC,IAAI,MAAM,IAAI,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;CAC7C;AACF;;;;;AAMA,SAAgB,QAAQ,QAA6B,CAAC,GAAY;CAChE,OAAO,IAAI,QAAQ,KAAK;AAC1B"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { S as unwrap } from "../reactive-f7NpAi69.mjs";
|
|
2
|
+
import { t as Node } from "../node-BZ7f6HzU.mjs";
|
|
3
|
+
import { calcLayout, formatText } from "../text.mjs";
|
|
4
|
+
//#region src/widgets/text.ts
|
|
5
|
+
var Text = class extends Node {
|
|
6
|
+
async _render(ctx) {
|
|
7
|
+
return formatText(await textContent(this.state.content, ctx), {
|
|
8
|
+
style: ctx.style.add(this.state),
|
|
9
|
+
width: ctx.width,
|
|
10
|
+
wrap: this.state.wrap
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
async layout(ctx) {
|
|
14
|
+
return calcLayout(await textContent(this.state.content, ctx), { wrap: this.state.wrap });
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
async function textContent(content, ctx) {
|
|
18
|
+
if (content === void 0) return;
|
|
19
|
+
const raw = unwrap(content);
|
|
20
|
+
return unwrap(typeof raw === "string" ? raw : await raw(ctx));
|
|
21
|
+
}
|
|
22
|
+
function text(first, style) {
|
|
23
|
+
if (typeof first === "string" || typeof first === "function") return new Text({
|
|
24
|
+
content: first,
|
|
25
|
+
...style
|
|
26
|
+
});
|
|
27
|
+
return new Text(first);
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { Text, text, textContent };
|
|
31
|
+
|
|
32
|
+
//# sourceMappingURL=text.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text.mjs","names":[],"sources":["../../src/widgets/text.ts"],"sourcesContent":["import type { MaybePromise } from \"@zaly/shared\"\nimport type { RenderCtx } from \"../core/ctx.ts\"\nimport type { Reactive } from \"../core/reactive.ts\"\nimport type { State } from \"../core/state.ts\"\nimport type { Style } from \"../style/types.ts\"\n\nimport { Node } from \"../core/node.ts\"\nimport { unwrap } from \"../core/reactive.ts\"\nimport { calcLayout, formatText } from \"../layout/text.ts\"\n\n/**\n * Text content — three forms, resolved at render time:\n *\n * 1. Plain string — static content.\n * 2. Reactive accessor (`signal()` / `memo()`) — re-reads on each\n * render and subscribes the node so signal writes invalidate.\n * 3. `(ctx) => string` — computed from the render context. Use this\n * for inline-styled spans via `ctx.style`.\n *\n * Branded accessors are picked up via `isAccessor`; any other function\n * is treated as the ctx-aware form. Plain strings pass through.\n *\n * ```ts\n * text(\"hello\")\n * text(name) // signal accessor\n * text(({ style }) => style.ok(`✓ ${count}`)) // ctx-aware\n * ```\n */\nexport type TextContent = Reactive<string> | ((ctx: RenderCtx) => MaybePromise<Reactive<string>>)\n\nexport interface TextStyle extends Style {\n content: TextContent\n wrap?: \"word\" | \"char\" | \"none\"\n}\n\nexport class Text extends Node<TextStyle> {\n protected async _render(ctx: RenderCtx): Promise<string[]> {\n const content = await textContent(this.state.content, ctx)\n return formatText(content, {\n style: ctx.style.add(this.state),\n width: ctx.width,\n wrap: this.state.wrap,\n })\n }\n\n override async layout(ctx: RenderCtx) {\n return calcLayout(await textContent(this.state.content, ctx), { wrap: this.state.wrap })\n }\n}\n\nexport function textContent(content: TextContent, ctx: RenderCtx): Promise<string>\nexport function textContent(content: TextContent | undefined, ctx: RenderCtx): Promise<undefined>\nexport async function textContent(\n content: TextContent | undefined,\n ctx: RenderCtx\n): Promise<string | undefined> {\n if (content === undefined) return\n const raw = unwrap(content)\n return unwrap(typeof raw === \"string\" ? raw : await raw(ctx))\n}\n\n/**\n * Factory for `Text`. Content can be a plain string or a `(ctx) => string`\n * function that's called at render time — use the function form when you\n * want inline-styled spans via `ctx.style`.\n *\n * ```ts\n * text(\"hello\")\n * text(\"hello\", { fg: \"primary\", bold: true })\n * text({ content: \"hello\", fg: \"primary\" })\n * text(({ style }) => `ok: ${style.ok(\"yes\")}`)\n * ```\n */\nexport function text(content: TextContent, style?: Omit<State<TextStyle>, \"content\">): Text\nexport function text(style: State<TextStyle>): Text\nexport function text(\n first: TextContent | State<TextStyle>,\n style?: Omit<State<TextStyle>, \"content\">\n): Text {\n // TextContent: string | function (incl. reactive accessor) | …\n // TextStyle: plain object with a `content` field.\n // The TextStyle branch is picked only when `first` is a non-function\n // object — accessors are functions, so they go through the content path.\n if (typeof first === \"string\" || typeof first === \"function\") {\n return new Text({ content: first, ...style })\n }\n return new Text(first)\n}\n"],"mappings":";;;;AAmCA,IAAa,OAAb,cAA0B,KAAgB;CACxC,MAAgB,QAAQ,KAAmC;EAEzD,OAAO,WAAW,MADI,YAAY,KAAK,MAAM,SAAS,GAAG,GAC9B;GACzB,OAAO,IAAI,MAAM,IAAI,KAAK,KAAK;GAC/B,OAAO,IAAI;GACX,MAAM,KAAK,MAAM;EACnB,CAAC;CACH;CAEA,MAAe,OAAO,KAAgB;EACpC,OAAO,WAAW,MAAM,YAAY,KAAK,MAAM,SAAS,GAAG,GAAG,EAAE,MAAM,KAAK,MAAM,KAAK,CAAC;CACzF;AACF;AAIA,eAAsB,YACpB,SACA,KAC6B;CAC7B,IAAI,YAAY,KAAA,GAAW;CAC3B,MAAM,MAAM,OAAO,OAAO;CAC1B,OAAO,OAAO,OAAO,QAAQ,WAAW,MAAM,MAAM,IAAI,GAAG,CAAC;AAC9D;AAgBA,SAAgB,KACd,OACA,OACM;CAKN,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,YAChD,OAAO,IAAI,KAAK;EAAE,SAAS;EAAO,GAAG;CAAM,CAAC;CAE9C,OAAO,IAAI,KAAK,KAAK;AACvB"}
|