@valyrianjs/terminal 0.2.1 → 0.2.3
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/dist/ansi.d.ts.map +1 -1
- package/dist/ansi.js +177 -17
- package/dist/ansi.js.map +1 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +4 -0
- package/dist/events.js.map +1 -1
- package/dist/frame-style.d.ts +7 -0
- package/dist/frame-style.d.ts.map +1 -0
- package/dist/frame-style.js +27 -0
- package/dist/frame-style.js.map +1 -0
- package/dist/layout.d.ts +5 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +53 -23
- package/dist/layout.js.map +1 -1
- package/dist/mouse.d.ts.map +1 -1
- package/dist/mouse.js +8 -1
- package/dist/mouse.js.map +1 -1
- package/dist/render-internal.d.ts +10 -0
- package/dist/render-internal.d.ts.map +1 -0
- package/dist/render-internal.js +1295 -0
- package/dist/render-internal.js.map +1 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +13 -1205
- package/dist/render.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +78 -4
- package/dist/session.js.map +1 -1
- package/dist/text.d.ts +7 -0
- package/dist/text.d.ts.map +1 -1
- package/dist/text.js +125 -0
- package/dist/text.js.map +1 -1
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +18 -2
- package/dist/theme.js.map +1 -1
- package/dist/types.d.ts +3 -2
- package/dist/types.d.ts.map +1 -1
- package/docs/api-reference.md +6 -3
- package/docs/cookbook.md +1 -1
- package/docs/interaction-model.md +5 -5
- package/docs/primitive-gallery.md +4 -4
- package/examples/basic.tsx +22 -0
- package/examples/cli.tsx +55 -0
- package/examples/demo.tsx +98 -0
- package/examples/docs/background-fill.tsx +107 -0
- package/examples/docs/component-composition.tsx +140 -0
- package/examples/docs/cursor.tsx +121 -0
- package/examples/docs/employees-list.tsx +138 -0
- package/examples/docs/hello.tsx +98 -0
- package/examples/docs/interactive-note.tsx +111 -0
- package/examples/docs/module-api-dashboard.tsx +307 -0
- package/examples/docs/module-flux-store.tsx +181 -0
- package/examples/docs/module-form-workflow.tsx +339 -0
- package/examples/docs/module-forms.tsx +218 -0
- package/examples/docs/module-money.tsx +175 -0
- package/examples/docs/module-native-store.tsx +188 -0
- package/examples/docs/module-pulses.tsx +142 -0
- package/examples/docs/module-query.tsx +209 -0
- package/examples/docs/module-request.tsx +194 -0
- package/examples/docs/module-state-workbench.tsx +283 -0
- package/examples/docs/module-tasks.tsx +223 -0
- package/examples/docs/module-translate.tsx +194 -0
- package/examples/docs/module-utils.tsx +168 -0
- package/examples/docs/module-valyrian-core.tsx +159 -0
- package/examples/docs/pizza-builder.tsx +463 -0
- package/examples/docs/primitive-activity-console.tsx +113 -0
- package/examples/docs/primitive-command-panel.tsx +186 -0
- package/examples/docs/primitive-data-explorer.tsx +155 -0
- package/examples/docs/primitive-input-workbench.tsx +128 -0
- package/examples/docs/primitive-layout-shell.tsx +115 -0
- package/examples/docs/responsive-split.tsx +186 -0
- package/examples/docs/style-system.tsx +209 -0
- package/examples/docs/theme-colors.tsx +225 -0
- package/examples/docs/virtualized-list-workbench.tsx +232 -0
- package/examples/opencode-dogfood-app.tsx +215 -0
- package/examples/opencode-dogfood-lifecycle.tsx +194 -0
- package/examples/opencode-dogfood.tsx +11 -0
- package/llms-full.txt +16 -13
- package/package.json +3 -2
- package/src/ansi.ts +207 -17
- package/src/events.ts +2 -0
- package/src/frame-style.ts +36 -0
- package/src/layout.ts +57 -24
- package/src/mouse.ts +10 -1
- package/src/render-internal.ts +1441 -0
- package/src/render.ts +14 -1324
- package/src/session.ts +99 -12
- package/src/text.ts +160 -0
- package/src/theme.ts +22 -2
- package/src/types.ts +3 -2
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { FormStore } from "valyrian.js/forms";
|
|
2
|
+
import { Money, formatMoney, parseMoneyInput } from "valyrian.js/money";
|
|
3
|
+
import { createNativeStore, StorageType } from "valyrian.js/native-store";
|
|
4
|
+
import { setLang, setLog, setStoreStrategy, setTranslations, t } from "valyrian.js/translate";
|
|
5
|
+
import { get, set } from "valyrian.js/utils";
|
|
6
|
+
import { Input, Pane, Screen, Split, Text, mountTerminal } from "@valyrianjs/terminal";
|
|
7
|
+
import type { TerminalMountOptions, TerminalSession } from "@valyrianjs/terminal";
|
|
8
|
+
|
|
9
|
+
type Language = "en" | "es";
|
|
10
|
+
|
|
11
|
+
type BillingForm = {
|
|
12
|
+
name: string;
|
|
13
|
+
seats: number;
|
|
14
|
+
budgetCents: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
interface SavedBilling {
|
|
18
|
+
name: string;
|
|
19
|
+
seats: number;
|
|
20
|
+
budgetCents: number;
|
|
21
|
+
workspace: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface FormWorkflowState {
|
|
25
|
+
language: Language;
|
|
26
|
+
savedWorkspace: string;
|
|
27
|
+
savedBilling: SavedBilling | null;
|
|
28
|
+
saveMessage: string;
|
|
29
|
+
resetMessage: string;
|
|
30
|
+
running: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface ModuleFormWorkflowDemo {
|
|
34
|
+
session: TerminalSession;
|
|
35
|
+
dispatchKey(key: string): string;
|
|
36
|
+
output(): string;
|
|
37
|
+
ansiOutput(): string;
|
|
38
|
+
isRunning(): boolean;
|
|
39
|
+
destroy(): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type FormWorkflowOptions = TerminalMountOptions & {
|
|
43
|
+
storeKey?: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const DEFAULT_STORE_KEY = "valyrian-terminal-docs.billing-settings-wizard";
|
|
47
|
+
const DEFAULT_WORKSPACE = "Northwind CLI";
|
|
48
|
+
|
|
49
|
+
const SHELL_STYLE = { color: "#f8fafc", background: "#111827", padding: { left: 1, right: 1 } };
|
|
50
|
+
const FORM_STYLE = { color: "#ffffff", background: "#0f3b3e", padding: { left: 1, right: 1 } };
|
|
51
|
+
const SUMMARY_STYLE = { color: "#ffffff", background: "#3b1f4f", padding: { left: 1, right: 1 } };
|
|
52
|
+
const FOOTER_STYLE = { color: "#e5e7eb", background: "#1f2937" };
|
|
53
|
+
const MUTED_STYLE = { color: "#cbd5e1" };
|
|
54
|
+
const ERROR_STYLE = { color: "#fecaca" };
|
|
55
|
+
|
|
56
|
+
function shouldRunSnapshot() {
|
|
57
|
+
return process.argv.includes("--snapshot") || process.env.VALYRIAN_TERMINAL_EXAMPLE_SNAPSHOT === "1" || !process.stdin.isTTY;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
function configureTranslations() {
|
|
62
|
+
setLog(false);
|
|
63
|
+
let storedLanguage = "en";
|
|
64
|
+
setStoreStrategy({
|
|
65
|
+
get: () => storedLanguage,
|
|
66
|
+
set: (language) => {
|
|
67
|
+
storedLanguage = language;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
setTranslations(
|
|
71
|
+
{
|
|
72
|
+
heading: "Billing/Settings Wizard",
|
|
73
|
+
language: "Language: English",
|
|
74
|
+
workspace: "Workspace",
|
|
75
|
+
customer: "Customer",
|
|
76
|
+
seats: "Seats",
|
|
77
|
+
budget: "Budget"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
es: {
|
|
81
|
+
heading: "Asistente de cobro/configuración",
|
|
82
|
+
language: "Idioma: español",
|
|
83
|
+
workspace: "Espacio",
|
|
84
|
+
customer: "Cliente",
|
|
85
|
+
seats: "Asientos",
|
|
86
|
+
budget: "Presupuesto"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseSeats(value: unknown) {
|
|
93
|
+
const seats = Number(String(value).trim());
|
|
94
|
+
return Number.isFinite(seats) ? seats : 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parseBudgetCents(value: unknown) {
|
|
98
|
+
try {
|
|
99
|
+
return parseMoneyInput(String(value), { decimalPlaces: 2 }).toCents();
|
|
100
|
+
} catch {
|
|
101
|
+
return -1;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function createBillingForm(onSaved: (billing: SavedBilling) => void) {
|
|
106
|
+
return new FormStore<BillingForm>({
|
|
107
|
+
state: { name: "Ada", seats: 12, budgetCents: 129950 },
|
|
108
|
+
schema: {
|
|
109
|
+
type: "object",
|
|
110
|
+
properties: {
|
|
111
|
+
name: { type: "string", minLength: 1 },
|
|
112
|
+
seats: { type: "number", minimum: 1 },
|
|
113
|
+
budgetCents: { type: "number", minimum: 100 }
|
|
114
|
+
},
|
|
115
|
+
required: ["name", "seats", "budgetCents"]
|
|
116
|
+
},
|
|
117
|
+
clean: {
|
|
118
|
+
name: (value) => String(value).trim(),
|
|
119
|
+
seats: parseSeats,
|
|
120
|
+
budgetCents: parseBudgetCents
|
|
121
|
+
},
|
|
122
|
+
format: {
|
|
123
|
+
seats: (value) => String(value),
|
|
124
|
+
budgetCents: (value) => (Number(value) / 100).toFixed(2)
|
|
125
|
+
},
|
|
126
|
+
onSubmit(values) {
|
|
127
|
+
onSaved({
|
|
128
|
+
name: values.name,
|
|
129
|
+
seats: values.seats,
|
|
130
|
+
budgetCents: values.budgetCents,
|
|
131
|
+
workspace: DEFAULT_WORKSPACE
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function formatBudget(cents: number, language: Language) {
|
|
138
|
+
const money = Money.fromCents(Number.isFinite(cents) ? cents : 0);
|
|
139
|
+
return formatMoney(money, { currency: "USD", locale: language === "es" ? "es-MX" : "en-US" });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function fieldError(form: FormStore<BillingForm>, field: keyof BillingForm) {
|
|
143
|
+
return String(form.validationErrors[field] || "not checked");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function savedSummary(saved: SavedBilling | null, language: Language) {
|
|
147
|
+
if (!saved) return "none";
|
|
148
|
+
return `${saved.name} / ${saved.seats} seats / ${formatBudget(saved.budgetCents, language)}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function App({ form, state, onSubmit }: { form: FormStore<BillingForm>; state: FormWorkflowState; onSubmit: () => void }) {
|
|
152
|
+
setLang(state.language);
|
|
153
|
+
const data = { billing: { name: form.state.name, seats: form.state.seats } };
|
|
154
|
+
set(data, "billing.workspace", DEFAULT_WORKSPACE);
|
|
155
|
+
set(data, "billing.total", formatBudget(form.state.budgetCents, state.language));
|
|
156
|
+
const workspace = String(get(data, "billing.workspace", DEFAULT_WORKSPACE));
|
|
157
|
+
const displayBudget = String(get(data, "billing.total", formatBudget(0, state.language)));
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<Screen title="Billing/Settings Wizard">
|
|
161
|
+
<Text style={{ color: "#ffffff", background: "#020617" }}>Valyrian.js modules in terminal apps: form workflow</Text>
|
|
162
|
+
<Split direction="row" gap={1} sizes={["1fr", "1fr"]}>
|
|
163
|
+
<Pane style={FORM_STYLE}>
|
|
164
|
+
<Text>{t("heading")}</Text>
|
|
165
|
+
<Text>{t("language")}</Text>
|
|
166
|
+
<Text>{`${t("workspace")}: ${workspace}`}</Text>
|
|
167
|
+
<Text>{t("customer")}</Text>
|
|
168
|
+
<Input id="billing-name" value={String(form.state.name)} placeholder="Customer name" onchange={(event) => form.setField("name", event.value)} onsubmit={onSubmit} />
|
|
169
|
+
<Text>{`Name: ${form.state.name || "missing"}`}</Text>
|
|
170
|
+
<Text style={ERROR_STYLE}>{`Name error: ${fieldError(form, "name")}`}</Text>
|
|
171
|
+
<Text>{t("seats")}</Text>
|
|
172
|
+
<Input id="billing-seats" value={String(form.formatValue("seats", form.state.seats))} placeholder="Seat count" onchange={(event) => form.setField("seats", event.value)} onsubmit={onSubmit} />
|
|
173
|
+
<Text>{`Seats: ${form.state.seats}`}</Text>
|
|
174
|
+
<Text style={ERROR_STYLE}>{`Seats error: ${fieldError(form, "seats")}`}</Text>
|
|
175
|
+
<Text>{t("budget")}</Text>
|
|
176
|
+
<Input id="billing-budget" value={String(form.formatValue("budgetCents", form.state.budgetCents))} placeholder="Monthly budget" onchange={(event) => form.setField("budgetCents", event.value)} onsubmit={onSubmit} />
|
|
177
|
+
<Text>{`Budget: ${displayBudget}`}</Text>
|
|
178
|
+
<Text style={ERROR_STYLE}>{`Budget error: ${fieldError(form, "budgetCents")}`}</Text>
|
|
179
|
+
</Pane>
|
|
180
|
+
<Pane style={SUMMARY_STYLE}>
|
|
181
|
+
<Text>Cleaned billing summary</Text>
|
|
182
|
+
<Text>{`Workspace: ${workspace}`}</Text>
|
|
183
|
+
<Text>{`Saved workspace: ${state.savedWorkspace || "none"}`}</Text>
|
|
184
|
+
<Text>{`Saved billing: ${savedSummary(state.savedBilling, state.language)}`}</Text>
|
|
185
|
+
<Text>{`Form dirty: ${form.isDirty}`}</Text>
|
|
186
|
+
<Text>{`Form success: ${form.success}`}</Text>
|
|
187
|
+
<Text>{`Save: ${state.saveMessage}`}</Text>
|
|
188
|
+
<Text>{`Reset: ${state.resetMessage}`}</Text>
|
|
189
|
+
<Text style={MUTED_STYLE}>Forms clean input, native-store persists workspace, translate changes labels, money formats totals, and utils shape summary data.</Text>
|
|
190
|
+
</Pane>
|
|
191
|
+
</Split>
|
|
192
|
+
<Pane style={SHELL_STYLE}>
|
|
193
|
+
<Text>N: fill name Tab: next field Shift+Tab: previous field L: language S/Enter: save R: reset form X: clear saved Ctrl+C: quit</Text>
|
|
194
|
+
<Text style={FOOTER_STYLE}>See the Valyrian.js documentation for forms, native-store, translate, money, and utils.</Text>
|
|
195
|
+
</Pane>
|
|
196
|
+
</Screen>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function createModuleFormWorkflowDemo(options: FormWorkflowOptions = {}): ModuleFormWorkflowDemo {
|
|
201
|
+
configureTranslations();
|
|
202
|
+
const settings = createNativeStore<{ workspace: string }>(options.storeKey ?? DEFAULT_STORE_KEY, {}, StorageType.Session, true);
|
|
203
|
+
const state: FormWorkflowState = {
|
|
204
|
+
language: "en",
|
|
205
|
+
savedWorkspace: String(settings.get("workspace") || ""),
|
|
206
|
+
savedBilling: null,
|
|
207
|
+
saveMessage: "not saved",
|
|
208
|
+
resetMessage: "not reset",
|
|
209
|
+
running: true
|
|
210
|
+
};
|
|
211
|
+
const form = createBillingForm((billing) => {
|
|
212
|
+
state.savedBilling = billing;
|
|
213
|
+
state.savedWorkspace = billing.workspace;
|
|
214
|
+
settings.set("workspace", billing.workspace);
|
|
215
|
+
state.saveMessage = `saved ${billing.name} for ${billing.seats} seats`;
|
|
216
|
+
});
|
|
217
|
+
let session: TerminalSession;
|
|
218
|
+
|
|
219
|
+
function quit() {
|
|
220
|
+
state.running = false;
|
|
221
|
+
settings.cleanup();
|
|
222
|
+
session.destroy();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function resetForm() {
|
|
226
|
+
form.reset();
|
|
227
|
+
state.savedBilling = null;
|
|
228
|
+
state.savedWorkspace = "";
|
|
229
|
+
state.saveMessage = "not saved";
|
|
230
|
+
state.resetMessage = "form reset";
|
|
231
|
+
settings.clear();
|
|
232
|
+
session.update();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function submitForm() {
|
|
236
|
+
const ok = await form.submit();
|
|
237
|
+
if (!ok) {
|
|
238
|
+
state.saveMessage = "blocked by validation";
|
|
239
|
+
}
|
|
240
|
+
session.update();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
session = mountTerminal(<App form={form} state={state} onSubmit={submitForm} />, {
|
|
244
|
+
...options,
|
|
245
|
+
cols: options.cols ?? 100,
|
|
246
|
+
rows: options.rows ?? 24,
|
|
247
|
+
keymap: {
|
|
248
|
+
...options.keymap,
|
|
249
|
+
bindings: [
|
|
250
|
+
...(options.keymap?.bindings || []),
|
|
251
|
+
{ key: "n", command: { id: "form.name" }, scope: "global" },
|
|
252
|
+
{ key: "N", command: { id: "form.name" }, scope: "global" },
|
|
253
|
+
{ key: "l", command: { id: "form.language" }, scope: "global" },
|
|
254
|
+
{ key: "L", command: { id: "form.language" }, scope: "global" },
|
|
255
|
+
{ key: "s", command: { id: "form.save" }, scope: "global" },
|
|
256
|
+
{ key: "S", command: { id: "form.save" }, scope: "global" },
|
|
257
|
+
{ key: "r", command: { id: "form.reset" }, scope: "global" },
|
|
258
|
+
{ key: "R", command: { id: "form.reset" }, scope: "global" },
|
|
259
|
+
{ key: "x", command: { id: "form.clear" }, scope: "global" },
|
|
260
|
+
{ key: "X", command: { id: "form.clear" }, scope: "global" },
|
|
261
|
+
{ key: "CTRL_C", command: { id: "quit" }, scope: "global" }
|
|
262
|
+
],
|
|
263
|
+
onCommand(command, context) {
|
|
264
|
+
if (command.id === "form.name") {
|
|
265
|
+
form.setField("name", "Ada Lovelace");
|
|
266
|
+
session.update();
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
if (command.id === "form.language") {
|
|
270
|
+
state.language = state.language === "en" ? "es" : "en";
|
|
271
|
+
setLang(state.language);
|
|
272
|
+
session.update();
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
if (command.id === "form.save") {
|
|
276
|
+
void submitForm();
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
if (command.id === "form.reset") {
|
|
280
|
+
resetForm();
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
if (command.id === "form.clear") {
|
|
284
|
+
state.savedWorkspace = "";
|
|
285
|
+
state.savedBilling = null;
|
|
286
|
+
state.saveMessage = "not saved";
|
|
287
|
+
settings.clear();
|
|
288
|
+
session.update();
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
if (command.id === "quit") {
|
|
292
|
+
quit();
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
return options.keymap?.onCommand?.(command, context);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
session.focus("billing-name");
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
session,
|
|
304
|
+
dispatchKey(key: string) {
|
|
305
|
+
return session.dispatchKey(key);
|
|
306
|
+
},
|
|
307
|
+
output() {
|
|
308
|
+
return session.output();
|
|
309
|
+
},
|
|
310
|
+
ansiOutput() {
|
|
311
|
+
return session.ansiOutput();
|
|
312
|
+
},
|
|
313
|
+
isRunning() {
|
|
314
|
+
return state.running;
|
|
315
|
+
},
|
|
316
|
+
destroy() {
|
|
317
|
+
state.running = false;
|
|
318
|
+
settings.cleanup();
|
|
319
|
+
session.destroy();
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (import.meta.main) {
|
|
325
|
+
if (shouldRunSnapshot()) {
|
|
326
|
+
const demo = createModuleFormWorkflowDemo({ runtime: "headless", cols: 100, rows: 24 });
|
|
327
|
+
demo.dispatchKey("S");
|
|
328
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
329
|
+
demo.dispatchKey("N");
|
|
330
|
+
demo.dispatchKey("L");
|
|
331
|
+
demo.dispatchKey("S");
|
|
332
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
333
|
+
process.stdout.write(demo.output());
|
|
334
|
+
process.stdout.write("\n");
|
|
335
|
+
demo.destroy();
|
|
336
|
+
} else {
|
|
337
|
+
createModuleFormWorkflowDemo();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { FormStore } from "valyrian.js/forms";
|
|
2
|
+
import { Input, Pane, Screen, Text, mountTerminal } from "@valyrianjs/terminal";
|
|
3
|
+
import type { TerminalMountOptions, TerminalSession } from "@valyrianjs/terminal";
|
|
4
|
+
|
|
5
|
+
interface ModuleDemo {
|
|
6
|
+
session: TerminalSession;
|
|
7
|
+
dispatchKey(key: string): string;
|
|
8
|
+
output(): string;
|
|
9
|
+
ansiOutput(): string;
|
|
10
|
+
isRunning(): boolean;
|
|
11
|
+
destroy(): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type ProfileForm = { name: string; email: string };
|
|
15
|
+
|
|
16
|
+
interface SavedProfile {
|
|
17
|
+
name: string;
|
|
18
|
+
email: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface FormDemoState {
|
|
22
|
+
form: FormStore<ProfileForm>;
|
|
23
|
+
valid: boolean;
|
|
24
|
+
savedProfile: SavedProfile | null;
|
|
25
|
+
submitMessage: string;
|
|
26
|
+
resetMessage: string;
|
|
27
|
+
running: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const PANEL_STYLE = { color: "#f8fafc", background: "#111827", padding: { left: 1, right: 1 } };
|
|
31
|
+
const FIELD_STYLE = { color: "#ffffff", background: "#0f3b3e", padding: { left: 1, right: 1 } };
|
|
32
|
+
const ERROR_STYLE = { color: "#fecaca" };
|
|
33
|
+
const FOOTER_STYLE = { color: "#dbeafe", background: "#1e293b" };
|
|
34
|
+
|
|
35
|
+
function shouldRunSnapshot() {
|
|
36
|
+
return process.argv.includes("--snapshot") || process.env.VALYRIAN_TERMINAL_EXAMPLE_SNAPSHOT === "1" || !process.stdin.isTTY;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createProfileForm(onSaved: (profile: SavedProfile) => void) {
|
|
40
|
+
return new FormStore<ProfileForm>({
|
|
41
|
+
state: { name: "", email: "" },
|
|
42
|
+
schema: {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {
|
|
45
|
+
name: { type: "string", minLength: 1 },
|
|
46
|
+
email: { type: "string", format: "email" }
|
|
47
|
+
},
|
|
48
|
+
required: ["name", "email"]
|
|
49
|
+
},
|
|
50
|
+
clean: {
|
|
51
|
+
name: (value) => String(value).trim(),
|
|
52
|
+
email: (value) => String(value).trim().toLowerCase()
|
|
53
|
+
},
|
|
54
|
+
onSubmit(values) {
|
|
55
|
+
onSaved({ name: values.name, email: values.email });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function fieldError(form: FormStore<ProfileForm>, field: keyof ProfileForm) {
|
|
61
|
+
return String(form.validationErrors[field] || "not checked");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function savedSummary(profile: SavedProfile | null) {
|
|
65
|
+
return profile ? `${profile.name} / ${profile.email}` : "none";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function App({ state, onSubmit }: { state: FormDemoState; onSubmit: () => void }) {
|
|
69
|
+
return (
|
|
70
|
+
<Screen title="Profile Editor">
|
|
71
|
+
<Text style={{ color: "#ffffff", background: "#020617" }}>Profile Editor</Text>
|
|
72
|
+
<Pane style={FIELD_STYLE}>
|
|
73
|
+
<Text>Name</Text>
|
|
74
|
+
<Input
|
|
75
|
+
id="profile-name"
|
|
76
|
+
value={state.form.state.name}
|
|
77
|
+
placeholder="Full name"
|
|
78
|
+
onchange={(event) => {
|
|
79
|
+
state.form.setField("name", event.value);
|
|
80
|
+
state.valid = false;
|
|
81
|
+
}}
|
|
82
|
+
onsubmit={onSubmit}
|
|
83
|
+
/>
|
|
84
|
+
<Text>{`Name: ${state.form.state.name || "blank"}`}</Text>
|
|
85
|
+
<Text style={ERROR_STYLE}>{`Name error: ${fieldError(state.form, "name")}`}</Text>
|
|
86
|
+
<Text>Email</Text>
|
|
87
|
+
<Input
|
|
88
|
+
id="profile-email"
|
|
89
|
+
value={state.form.state.email}
|
|
90
|
+
placeholder="name@example.com"
|
|
91
|
+
onchange={(event) => {
|
|
92
|
+
state.form.setField("email", event.value);
|
|
93
|
+
state.valid = false;
|
|
94
|
+
}}
|
|
95
|
+
onsubmit={onSubmit}
|
|
96
|
+
/>
|
|
97
|
+
<Text>{`Email: ${state.form.state.email || "blank"}`}</Text>
|
|
98
|
+
<Text style={ERROR_STYLE}>{`Email error: ${fieldError(state.form, "email")}`}</Text>
|
|
99
|
+
</Pane>
|
|
100
|
+
<Pane style={PANEL_STYLE}>
|
|
101
|
+
<Text>{`Form valid: ${state.valid}`}</Text>
|
|
102
|
+
<Text>{`Submit: ${state.submitMessage}`}</Text>
|
|
103
|
+
<Text>{`Reset: ${state.resetMessage}`}</Text>
|
|
104
|
+
<Text>{`Saved profile: ${savedSummary(state.savedProfile)}`}</Text>
|
|
105
|
+
</Pane>
|
|
106
|
+
<Text style={FOOTER_STYLE}>Type profile fields | Tab: next field | Shift+Tab: previous field | Enter: save profile | R: reset form | Ctrl+C: quit</Text>
|
|
107
|
+
</Screen>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function createModuleFormsDemo(options: TerminalMountOptions = {}): ModuleDemo {
|
|
112
|
+
const state: FormDemoState = {
|
|
113
|
+
form: null as unknown as FormStore<ProfileForm>,
|
|
114
|
+
valid: false,
|
|
115
|
+
savedProfile: null,
|
|
116
|
+
submitMessage: "not submitted",
|
|
117
|
+
resetMessage: "not reset",
|
|
118
|
+
running: true
|
|
119
|
+
};
|
|
120
|
+
state.form = createProfileForm((profile) => {
|
|
121
|
+
state.savedProfile = profile;
|
|
122
|
+
state.submitMessage = `saved ${profile.name} <${profile.email}>`;
|
|
123
|
+
});
|
|
124
|
+
let session: TerminalSession;
|
|
125
|
+
|
|
126
|
+
function resetForm() {
|
|
127
|
+
state.form.reset();
|
|
128
|
+
state.valid = false;
|
|
129
|
+
state.savedProfile = null;
|
|
130
|
+
state.submitMessage = "not submitted";
|
|
131
|
+
state.resetMessage = "form reset";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function submitForm() {
|
|
135
|
+
state.valid = state.form.validate();
|
|
136
|
+
if (!state.valid) {
|
|
137
|
+
state.submitMessage = "blocked by validation";
|
|
138
|
+
session.update();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
void state.form.submit().then((ok) => {
|
|
143
|
+
state.valid = ok;
|
|
144
|
+
if (!ok) state.submitMessage = "blocked by validation";
|
|
145
|
+
session.update();
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function quit() {
|
|
150
|
+
state.running = false;
|
|
151
|
+
session.destroy();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
session = mountTerminal(<App state={state} onSubmit={submitForm} />, {
|
|
155
|
+
...options,
|
|
156
|
+
cols: options.cols ?? 92,
|
|
157
|
+
rows: options.rows ?? 20,
|
|
158
|
+
keymap: {
|
|
159
|
+
...options.keymap,
|
|
160
|
+
bindings: [
|
|
161
|
+
...(options.keymap?.bindings || []),
|
|
162
|
+
{ key: "R", command: { id: "forms.reset" }, scope: "global" },
|
|
163
|
+
{ key: "CTRL_C", command: { id: "quit" }, scope: "global" }
|
|
164
|
+
],
|
|
165
|
+
onCommand(command, context) {
|
|
166
|
+
if (command.id === "forms.reset") {
|
|
167
|
+
resetForm();
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
if (command.id === "quit") {
|
|
171
|
+
quit();
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
return options.keymap?.onCommand?.(command, context);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
session.focus("profile-name");
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
session,
|
|
183
|
+
dispatchKey(key: string) {
|
|
184
|
+
return session.dispatchKey(key);
|
|
185
|
+
},
|
|
186
|
+
output() {
|
|
187
|
+
return session.output();
|
|
188
|
+
},
|
|
189
|
+
ansiOutput() {
|
|
190
|
+
return session.ansiOutput();
|
|
191
|
+
},
|
|
192
|
+
isRunning() {
|
|
193
|
+
return state.running;
|
|
194
|
+
},
|
|
195
|
+
destroy() {
|
|
196
|
+
state.running = false;
|
|
197
|
+
session.destroy();
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (import.meta.main) {
|
|
203
|
+
if (shouldRunSnapshot()) {
|
|
204
|
+
const demo = createModuleFormsDemo({ runtime: "headless", cols: 92, rows: 20 });
|
|
205
|
+
demo.dispatchKey("ENTER");
|
|
206
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
207
|
+
for (const key of "AdaLovelace") demo.dispatchKey(key);
|
|
208
|
+
demo.dispatchKey("TAB");
|
|
209
|
+
for (const key of "ada@example.com") demo.dispatchKey(key);
|
|
210
|
+
demo.dispatchKey("ENTER");
|
|
211
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
212
|
+
process.stdout.write(demo.output());
|
|
213
|
+
process.stdout.write("\n");
|
|
214
|
+
demo.destroy();
|
|
215
|
+
} else {
|
|
216
|
+
createModuleFormsDemo();
|
|
217
|
+
}
|
|
218
|
+
}
|