@silvery/examples 0.5.6 → 0.17.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.
Files changed (112) hide show
  1. package/dist/UPNG-Cy7ViL8f.mjs +5074 -0
  2. package/dist/__vite-browser-external-2447137e-BML7CYau.mjs +4 -0
  3. package/dist/_banner-DLPxCqVy.mjs +44 -0
  4. package/dist/ansi-CCE2pVS0.mjs +16397 -0
  5. package/dist/apng-HhhBjRGt.mjs +68 -0
  6. package/dist/apng-mwUQbTTF.mjs +3 -0
  7. package/dist/apps/aichat/index.mjs +1299 -0
  8. package/dist/apps/app-todo.mjs +139 -0
  9. package/dist/apps/async-data.mjs +204 -0
  10. package/dist/apps/cli-wizard.mjs +339 -0
  11. package/dist/apps/clipboard.mjs +198 -0
  12. package/dist/apps/components.mjs +864 -0
  13. package/dist/apps/data-explorer.mjs +483 -0
  14. package/dist/apps/dev-tools.mjs +397 -0
  15. package/dist/apps/explorer.mjs +698 -0
  16. package/dist/apps/gallery.mjs +766 -0
  17. package/dist/apps/inline-bench.mjs +115 -0
  18. package/dist/apps/kanban.mjs +280 -0
  19. package/dist/apps/layout-ref.mjs +187 -0
  20. package/dist/apps/outline.mjs +203 -0
  21. package/dist/apps/paste-demo.mjs +189 -0
  22. package/dist/apps/scroll.mjs +86 -0
  23. package/dist/apps/search-filter.mjs +287 -0
  24. package/dist/apps/selection.mjs +355 -0
  25. package/dist/apps/spatial-focus-demo.mjs +388 -0
  26. package/dist/apps/task-list.mjs +258 -0
  27. package/dist/apps/terminal-caps-demo.mjs +315 -0
  28. package/dist/apps/terminal.mjs +872 -0
  29. package/dist/apps/text-selection-demo.mjs +254 -0
  30. package/dist/apps/textarea.mjs +178 -0
  31. package/dist/apps/theme.mjs +661 -0
  32. package/dist/apps/transform.mjs +215 -0
  33. package/dist/apps/virtual-10k.mjs +422 -0
  34. package/dist/assets/resvgjs.darwin-arm64-BtufyGW1.node +0 -0
  35. package/dist/backends-Bahh9mKN.mjs +1179 -0
  36. package/dist/backends-CCtCDQ94.mjs +3 -0
  37. package/dist/{cli.mjs → bin/cli.mjs} +21 -25
  38. package/dist/chunk-BSw8zbkd.mjs +37 -0
  39. package/dist/components/counter.mjs +48 -0
  40. package/dist/components/hello.mjs +31 -0
  41. package/dist/components/progress-bar.mjs +59 -0
  42. package/dist/components/select-list.mjs +85 -0
  43. package/dist/components/spinner.mjs +57 -0
  44. package/dist/components/text-input.mjs +62 -0
  45. package/dist/components/virtual-list.mjs +51 -0
  46. package/dist/flexily-zero-adapter-UB-ra8fR.mjs +3374 -0
  47. package/dist/gif-BZaqPPVX.mjs +3 -0
  48. package/dist/gif-BtnXuxLF.mjs +71 -0
  49. package/dist/gifenc-CLRW41dk.mjs +728 -0
  50. package/dist/jsx-runtime-dMs_8fNu.mjs +241 -0
  51. package/dist/key-mapping-5oYQdAQE.mjs +3 -0
  52. package/dist/key-mapping-D4LR1go6.mjs +130 -0
  53. package/dist/layout/dashboard.mjs +1204 -0
  54. package/dist/layout/live-resize.mjs +303 -0
  55. package/dist/layout/overflow.mjs +70 -0
  56. package/dist/layout/text-layout.mjs +335 -0
  57. package/dist/node-NuJ94BWl.mjs +1083 -0
  58. package/dist/plugins-D1KtkT4a.mjs +3057 -0
  59. package/dist/resvg-js-C_8Wps1F.mjs +201 -0
  60. package/dist/src-BTEVGpd9.mjs +23538 -0
  61. package/dist/src-CUUOuRH6.mjs +5322 -0
  62. package/dist/src-CzfRafCQ.mjs +814 -0
  63. package/dist/usingCtx-CsEf0xO3.mjs +57 -0
  64. package/dist/yoga-adapter-BVtQ5OJR.mjs +237 -0
  65. package/package.json +19 -14
  66. package/_banner.tsx +0 -60
  67. package/apps/aichat/components.tsx +0 -469
  68. package/apps/aichat/index.tsx +0 -220
  69. package/apps/aichat/script.ts +0 -460
  70. package/apps/aichat/state.ts +0 -325
  71. package/apps/aichat/types.ts +0 -19
  72. package/apps/app-todo.tsx +0 -201
  73. package/apps/async-data.tsx +0 -196
  74. package/apps/cli-wizard.tsx +0 -332
  75. package/apps/clipboard.tsx +0 -183
  76. package/apps/components.tsx +0 -658
  77. package/apps/data-explorer.tsx +0 -490
  78. package/apps/dev-tools.tsx +0 -395
  79. package/apps/explorer.tsx +0 -731
  80. package/apps/gallery.tsx +0 -653
  81. package/apps/inline-bench.tsx +0 -138
  82. package/apps/kanban.tsx +0 -265
  83. package/apps/layout-ref.tsx +0 -173
  84. package/apps/outline.tsx +0 -160
  85. package/apps/panes/index.tsx +0 -203
  86. package/apps/paste-demo.tsx +0 -185
  87. package/apps/scroll.tsx +0 -77
  88. package/apps/search-filter.tsx +0 -240
  89. package/apps/selection.tsx +0 -342
  90. package/apps/spatial-focus-demo.tsx +0 -368
  91. package/apps/task-list.tsx +0 -271
  92. package/apps/terminal-caps-demo.tsx +0 -334
  93. package/apps/terminal.tsx +0 -800
  94. package/apps/text-selection-demo.tsx +0 -189
  95. package/apps/textarea.tsx +0 -155
  96. package/apps/theme.tsx +0 -515
  97. package/apps/transform.tsx +0 -229
  98. package/apps/virtual-10k.tsx +0 -405
  99. package/apps/vterm-demo/index.tsx +0 -216
  100. package/components/counter.tsx +0 -45
  101. package/components/hello.tsx +0 -34
  102. package/components/progress-bar.tsx +0 -48
  103. package/components/select-list.tsx +0 -50
  104. package/components/spinner.tsx +0 -40
  105. package/components/text-input.tsx +0 -57
  106. package/components/virtual-list.tsx +0 -52
  107. package/dist/cli.d.mts +0 -1
  108. package/dist/cli.mjs.map +0 -1
  109. package/layout/dashboard.tsx +0 -953
  110. package/layout/live-resize.tsx +0 -282
  111. package/layout/overflow.tsx +0 -51
  112. package/layout/text-layout.tsx +0 -283
@@ -1,325 +0,0 @@
1
- /**
2
- * TEA state machine for the AI coding agent demo.
3
- *
4
- * Pure (state, msg) → [state, effects] — all side effects are timer-based.
5
- * The update function is created via factory to close over script/mode config.
6
- */
7
-
8
- import { fx } from "silvery"
9
- import type { TeaResult, TimerEffect } from "silvery"
10
- import type { Exchange, ScriptEntry } from "./types.js"
11
- import { RANDOM_AGENT_RESPONSES, INPUT_COST_PER_M, OUTPUT_COST_PER_M, CONTEXT_WINDOW } from "./script.js"
12
-
13
- // ============================================================================
14
- // Types
15
- // ============================================================================
16
-
17
- /** Streaming phases: thinking -> streaming text -> tool calls -> done */
18
- export type StreamPhase = "thinking" | "streaming" | "tools" | "done"
19
-
20
- export type DemoState = {
21
- exchanges: Exchange[]
22
- scriptIdx: number
23
- streamPhase: StreamPhase
24
- revealFraction: number
25
- done: boolean
26
- compacting: boolean
27
- pulse: boolean
28
- ctrlDPending: boolean
29
- contextBaseline: number
30
- offScript: boolean
31
- nextId: number
32
- autoTyping: { full: string; revealed: number } | null
33
- }
34
-
35
- export type DemoMsg =
36
- | { type: "mount" }
37
- | { type: "advance" }
38
- | { type: "endThinking" }
39
- | { type: "streamTick" }
40
- | { type: "endTools" }
41
- | { type: "submit"; text: string }
42
- | { type: "compact" }
43
- | { type: "compactDone" }
44
- | { type: "pulse" }
45
- | { type: "autoAdvance" }
46
- | { type: "typingTick" }
47
- | { type: "autoTypingDone" }
48
- | { type: "respondRandom" }
49
- | { type: "setCtrlDPending"; pending: boolean }
50
-
51
- export type DemoEffect = TimerEffect<DemoMsg>
52
- export type DemoResult = TeaResult<DemoState, DemoEffect>
53
-
54
- // ============================================================================
55
- // Constants
56
- // ============================================================================
57
-
58
- const INTRO_TEXT = [
59
- "Coding agent simulation showcasing ListView:",
60
- " • ListView — unified virtualized list with cache",
61
- " • Cache mode — completed exchanges cached for performance",
62
- " • OSC 8 hyperlinks — clickable file paths and URLs",
63
- " • $token theme colors — semantic color tokens",
64
- ].join("\n")
65
-
66
- export const INIT_STATE: DemoState = {
67
- exchanges: [{ id: 0, role: "system", content: INTRO_TEXT }],
68
- scriptIdx: 0,
69
- streamPhase: "done",
70
- revealFraction: 1,
71
- done: false,
72
- compacting: false,
73
- pulse: false,
74
- ctrlDPending: false,
75
- contextBaseline: 0,
76
- offScript: false,
77
- nextId: 1,
78
- autoTyping: null,
79
- }
80
-
81
- // ============================================================================
82
- // Token & Cost Utilities
83
- // ============================================================================
84
-
85
- export function formatTokens(n: number): string {
86
- if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
87
- if (n >= 1000) return `${(n / 1000).toFixed(1)}K`
88
- return String(n)
89
- }
90
-
91
- export function formatCost(inputTokens: number, outputTokens: number): string {
92
- const cost = (inputTokens * INPUT_COST_PER_M + outputTokens * OUTPUT_COST_PER_M) / 1_000_000
93
- if (cost < 0.01) return `$${cost.toFixed(4)}`
94
- return `$${cost.toFixed(2)}`
95
- }
96
-
97
- /**
98
- * Compute token stats for display and compaction.
99
- *
100
- * Token values in the script are CUMULATIVE — each exchange's `input` represents
101
- * the total context consumed at that point. So:
102
- * - `currentContext`: the LAST exchange's input tokens (= current context window usage)
103
- * - `totalCost`: sum of all (input + output) for cost calculation (each API call costs)
104
- */
105
- export function computeCumulativeTokens(exchanges: Exchange[]): {
106
- input: number
107
- output: number
108
- currentContext: number
109
- } {
110
- let input = 0
111
- let output = 0
112
- let currentContext = 0
113
- for (const ex of exchanges) {
114
- if (ex.tokens) {
115
- input += ex.tokens.input
116
- output += ex.tokens.output
117
- if (ex.tokens.input > currentContext) currentContext = ex.tokens.input
118
- }
119
- }
120
- return { input, output, currentContext }
121
- }
122
-
123
- /** Next scripted user message for footer placeholder. */
124
- export function getNextMessage(state: DemoState, script: ScriptEntry[], autoMode: boolean): string {
125
- if (autoMode || state.done || state.offScript || state.streamPhase !== "done" || state.exchanges.length === 0)
126
- return ""
127
- const entry = script[state.scriptIdx]
128
- return entry?.role === "user" ? entry.content : ""
129
- }
130
-
131
- // ============================================================================
132
- // Update Factory
133
- // ============================================================================
134
-
135
- export function createDemoUpdate(script: ScriptEntry[], fastMode: boolean, autoMode: boolean) {
136
- function addExchange(state: DemoState, entry: ScriptEntry): DemoState {
137
- const exchange: Exchange = { ...entry, id: state.nextId }
138
- return { ...state, exchanges: [...state.exchanges, exchange], nextId: state.nextId + 1 }
139
- }
140
-
141
- function startStreaming(state: DemoState, entry: ScriptEntry): [DemoState, DemoEffect[]] {
142
- const s = addExchange(state, entry)
143
- if (entry.role !== "agent" || fastMode) {
144
- return [{ ...s, streamPhase: "done", revealFraction: 1 }, []]
145
- }
146
- if (entry.thinking) {
147
- return [{ ...s, streamPhase: "thinking", revealFraction: 0 }, [fx.delay(1200, { type: "endThinking" })]]
148
- }
149
- return [{ ...s, streamPhase: "streaming", revealFraction: 0 }, [fx.interval(50, { type: "streamTick" }, "reveal")]]
150
- }
151
-
152
- function autoAdvanceEffects(state: DemoState): DemoEffect[] {
153
- if (state.done || state.compacting || state.streamPhase !== "done") return []
154
- const next = script[state.scriptIdx]
155
- if (!next) return autoMode ? [fx.delay(0, { type: "autoAdvance" })] : []
156
- if (autoMode || next.role !== "user") return [fx.delay(fastMode ? 100 : 400, { type: "autoAdvance" })]
157
- return []
158
- }
159
-
160
- function doAdvance(state: DemoState, extraEffects: DemoEffect[] = []): DemoResult {
161
- if (state.done || state.compacting || state.streamPhase !== "done") return state
162
- if (state.scriptIdx >= script.length) {
163
- return autoMode ? { ...state, done: true } : state
164
- }
165
-
166
- const entry = script[state.scriptIdx]!
167
- let s: DemoState = {
168
- ...state,
169
- scriptIdx: state.scriptIdx + 1,
170
- }
171
- const effects = [...extraEffects]
172
- let streamFx: DemoEffect[]
173
-
174
- ;[s, streamFx] = startStreaming(s, entry)
175
- effects.push(...streamFx)
176
-
177
- if (fastMode) {
178
- while (s.scriptIdx < script.length && script[s.scriptIdx]!.role !== "user") {
179
- ;[s, streamFx] = startStreaming({ ...s, scriptIdx: s.scriptIdx + 1 }, script[s.scriptIdx]!)
180
- effects.push(...streamFx)
181
- }
182
- effects.push(...autoAdvanceEffects(s))
183
- } else if (entry.role === "user") {
184
- if (s.scriptIdx < script.length && script[s.scriptIdx]!.role === "agent") {
185
- ;[s, streamFx] = startStreaming({ ...s, scriptIdx: s.scriptIdx + 1 }, script[s.scriptIdx]!)
186
- effects.push(...streamFx)
187
- }
188
- }
189
-
190
- return [s, effects]
191
- }
192
-
193
- return function update(state: DemoState, msg: DemoMsg): DemoResult {
194
- switch (msg.type) {
195
- case "mount":
196
- return doAdvance(state, [fx.interval(400, { type: "pulse" }, "pulse")])
197
-
198
- case "advance":
199
- case "autoAdvance": {
200
- if (autoMode && !fastMode && state.streamPhase === "done" && !state.done && !state.compacting) {
201
- const next = script[state.scriptIdx]
202
- if (next?.role === "user") {
203
- return [
204
- { ...state, autoTyping: { full: next.content, revealed: 0 } },
205
- [fx.interval(30, { type: "typingTick" }, "typing")],
206
- ]
207
- }
208
- }
209
- if (autoMode && state.scriptIdx >= script.length && state.streamPhase === "done") {
210
- return { ...state, done: true }
211
- }
212
- return doAdvance(state)
213
- }
214
-
215
- case "typingTick": {
216
- if (!state.autoTyping) return state
217
- const next = state.autoTyping.revealed + 1
218
- if (next >= state.autoTyping.full.length) {
219
- return [
220
- {
221
- ...state,
222
- autoTyping: { ...state.autoTyping, revealed: state.autoTyping.full.length },
223
- },
224
- [fx.cancel("typing"), fx.delay(300, { type: "autoTypingDone" })],
225
- ]
226
- }
227
- return { ...state, autoTyping: { ...state.autoTyping, revealed: next } }
228
- }
229
-
230
- case "autoTypingDone":
231
- return doAdvance({ ...state, autoTyping: null })
232
-
233
- case "endThinking":
234
- return [
235
- { ...state, streamPhase: "streaming", revealFraction: 0 },
236
- [fx.interval(50, { type: "streamTick" }, "reveal")],
237
- ]
238
-
239
- case "streamTick": {
240
- const last = state.exchanges[state.exchanges.length - 1]
241
- const rate = last?.thinking ? 0.08 : 0.12
242
- const frac = Math.min(state.revealFraction + rate, 1)
243
- if (frac < 1) return { ...state, revealFraction: frac }
244
-
245
- const tools = last?.toolCalls ?? []
246
- if (tools.length > 0) {
247
- const s = { ...state, streamPhase: "tools" as StreamPhase, revealFraction: 1 }
248
- return [s, [fx.cancel("reveal"), fx.delay(600 * tools.length, { type: "endTools" })]]
249
- }
250
- const s = { ...state, streamPhase: "done" as StreamPhase, revealFraction: 1 }
251
- return [s, [fx.cancel("reveal"), ...autoAdvanceEffects(s)]]
252
- }
253
-
254
- case "endTools": {
255
- const s = { ...state, streamPhase: "done" as StreamPhase }
256
- return [s, autoAdvanceEffects(s)]
257
- }
258
-
259
- case "submit": {
260
- // Fast-forward streaming if still animating
261
- const base =
262
- state.streamPhase !== "done"
263
- ? { ...state, streamPhase: "done" as StreamPhase, revealFraction: 1, autoTyping: null }
264
- : state.autoTyping
265
- ? { ...state, autoTyping: null }
266
- : state
267
- const cancelEffects: DemoEffect[] =
268
- state.streamPhase !== "done" ? [fx.cancel("reveal"), fx.cancel("typing")] : [fx.cancel("typing")]
269
-
270
- // Empty submit just fast-forwards (no text to queue)
271
- if (!msg.text.trim()) return [base, cancelEffects]
272
- if (base.done) return base
273
-
274
- const s = addExchange(base, {
275
- role: "user",
276
- content: msg.text,
277
- tokens: { input: msg.text.length * 4, output: 0 },
278
- })
279
-
280
- if (s.scriptIdx < script.length) {
281
- let nextIdx = s.scriptIdx
282
- while (nextIdx < script.length && script[nextIdx]!.role === "user") nextIdx++
283
- return [{ ...s, scriptIdx: nextIdx }, [...cancelEffects, fx.delay(150, { type: "autoAdvance" })]]
284
- }
285
-
286
- return [{ ...s, offScript: true }, [...cancelEffects, fx.delay(150, { type: "respondRandom" })]]
287
- }
288
-
289
- case "respondRandom": {
290
- const resp = RANDOM_AGENT_RESPONSES[Math.floor(Math.random() * RANDOM_AGENT_RESPONSES.length)]!
291
- const [s, effects] = startStreaming(state, resp)
292
- return [{ ...s, offScript: true }, effects]
293
- }
294
-
295
- case "compact": {
296
- if (state.done || state.compacting) return state
297
- const cumulative = computeCumulativeTokens(state.exchanges)
298
- return [
299
- {
300
- ...state,
301
- streamPhase: "done",
302
- revealFraction: 1,
303
- compacting: true,
304
- contextBaseline: cumulative.currentContext,
305
- exchanges: state.exchanges,
306
- autoTyping: null,
307
- },
308
- [fx.cancel("reveal"), fx.cancel("typing"), fx.delay(fastMode ? 300 : 3000, { type: "compactDone" })],
309
- ]
310
- }
311
-
312
- case "compactDone":
313
- return doAdvance({ ...state, compacting: false })
314
-
315
- case "pulse":
316
- return { ...state, pulse: !state.pulse }
317
-
318
- case "setCtrlDPending":
319
- return { ...state, ctrlDPending: msg.pending }
320
-
321
- default:
322
- return state
323
- }
324
- }
325
- }
@@ -1,19 +0,0 @@
1
- /** Tool call in an exchange (Read, Edit, Bash, etc.). */
2
- export interface ToolCall {
3
- tool: string
4
- args: string
5
- output: string[]
6
- }
7
-
8
- /** A single exchange in the conversation. */
9
- export interface Exchange {
10
- id: number
11
- role: "user" | "agent" | "system"
12
- content: string
13
- thinking?: string
14
- toolCalls?: ToolCall[]
15
- tokens?: { input: number; output: number }
16
- }
17
-
18
- /** Script entry — exchange data before id is assigned. */
19
- export type ScriptEntry = Omit<Exchange, "id">
package/apps/app-todo.tsx DELETED
@@ -1,201 +0,0 @@
1
- /**
2
- * App Todo - Layer 3 Example
3
- *
4
- * Demonstrates pipe() composition with createApp(), withReact(), and
5
- * withTerminal() — the canonical pattern for building full apps.
6
- *
7
- * The plugin system separates concerns:
8
- * - createApp() — store + event handlers (what the app does)
9
- * - withReact() — element binding (what the app renders)
10
- * - withTerminal() — I/O binding (where the app runs)
11
- *
12
- * pipe() composes them left-to-right: each plugin enhances the
13
- * app object, wrapping run() so the final call needs no arguments.
14
- *
15
- * Usage: bun examples/apps/app-todo.tsx
16
- *
17
- * Controls:
18
- * j/k - Move cursor down/up
19
- * a - Add new todo
20
- * x - Toggle completed
21
- * d - Delete todo
22
- * Esc/q - Quit
23
- */
24
-
25
- import React from "react"
26
- import { Box, Text, Muted, Kbd } from "silvery"
27
- import { createApp, useApp, type AppHandle } from "@silvery/create/create-app"
28
- import { pipe, withReact, withTerminal } from "@silvery/create/plugins"
29
- import { ExampleBanner, type ExampleMeta } from "../_banner.js"
30
-
31
- export const meta: ExampleMeta = {
32
- name: "Todo App",
33
- description: "Layer 3: pipe() + createApp() + withReact() + withTerminal()",
34
- features: ["pipe()", "createApp()", "withReact()", "withTerminal()"],
35
- }
36
-
37
- // ============================================================================
38
- // Types
39
- // ============================================================================
40
-
41
- type Todo = {
42
- id: number
43
- text: string
44
- completed: boolean
45
- }
46
-
47
- type State = {
48
- todos: Todo[]
49
- cursor: number
50
- nextId: number
51
- addTodo: (text: string) => void
52
- toggleTodo: () => void
53
- deleteTodo: () => void
54
- moveCursor: (delta: number) => void
55
- }
56
-
57
- // ============================================================================
58
- // Components
59
- // ============================================================================
60
-
61
- function TodoItem({ todo, isCursor }: { todo: Todo; isCursor: boolean }) {
62
- return (
63
- <Box>
64
- <Text color={isCursor ? "$primary" : undefined}>{isCursor ? "› " : " "}</Text>
65
- <Text color={todo.completed ? "$success" : undefined} strikethrough={todo.completed}>
66
- {todo.completed ? "✓" : "○"} {todo.text}
67
- </Text>
68
- </Box>
69
- )
70
- }
71
-
72
- function TodoList() {
73
- const todos = useApp((s: State) => s.todos)
74
- const cursor = useApp((s: State) => s.cursor)
75
-
76
- return (
77
- <Box flexDirection="column">
78
- {todos.map((todo, i) => (
79
- <TodoItem key={todo.id} todo={todo} isCursor={i === cursor} />
80
- ))}
81
- {todos.length === 0 && <Muted>No todos. Press 'a' to add one.</Muted>}
82
- </Box>
83
- )
84
- }
85
-
86
- function TodoApp() {
87
- return (
88
- <Box flexDirection="column" padding={1}>
89
- <TodoList />
90
- <Text> </Text>
91
- <Muted>
92
- <Kbd>j/k</Kbd> move <Kbd>x</Kbd> toggle <Kbd>a</Kbd> add <Kbd>d</Kbd> delete <Kbd>Esc/q</Kbd> quit
93
- </Muted>
94
- </Box>
95
- )
96
- }
97
-
98
- // ============================================================================
99
- // App — pipe() composition
100
- // ============================================================================
101
-
102
- // 1. createApp() defines the store and event handlers
103
- const baseApp = createApp<Record<string, unknown>, State>(
104
- () => (set, get) => ({
105
- todos: [
106
- { id: 1, text: "Learn silvery plugin composition", completed: true },
107
- { id: 2, text: "Build an app with pipe()", completed: false },
108
- { id: 3, text: "Ship to production", completed: false },
109
- ],
110
- cursor: 0,
111
- nextId: 4,
112
-
113
- addTodo: (text: string) =>
114
- set((s) => ({
115
- todos: [...s.todos, { id: s.nextId, text, completed: false }],
116
- nextId: s.nextId + 1,
117
- })),
118
-
119
- toggleTodo: () =>
120
- set((s) => ({
121
- todos: s.todos.map((t, i) => (i === s.cursor ? { ...t, completed: !t.completed } : t)),
122
- })),
123
-
124
- deleteTodo: () =>
125
- set((s) => {
126
- const newTodos = s.todos.filter((_, i) => i !== s.cursor)
127
- return {
128
- todos: newTodos,
129
- cursor: Math.min(s.cursor, newTodos.length - 1),
130
- }
131
- }),
132
-
133
- moveCursor: (delta: number) =>
134
- set((s) => ({
135
- cursor: Math.max(0, Math.min(s.cursor + delta, s.todos.length - 1)),
136
- })),
137
- }),
138
-
139
- {
140
- "term:key": (data, ctx) => {
141
- const { input: k, key } = data as {
142
- input: string
143
- key: { escape: boolean }
144
- }
145
- const state = ctx.get() as State
146
- if (key.escape) return "exit"
147
- switch (k) {
148
- case "j":
149
- state.moveCursor(1)
150
- break
151
- case "k":
152
- state.moveCursor(-1)
153
- break
154
- case "x":
155
- state.toggleTodo()
156
- break
157
- case "d":
158
- if (state.todos.length > 0) state.deleteTodo()
159
- break
160
- case "a":
161
- state.addTodo(`New todo ${state.nextId}`)
162
- break
163
- case "q":
164
- return "exit"
165
- }
166
- },
167
- },
168
- )
169
-
170
- // 2. pipe() composes plugins left-to-right:
171
- // - withReact() binds the element, so run() needs no JSX argument
172
- // - withTerminal() binds stdin/stdout, so run() needs no options
173
- // Note: pipe() type composition requires casts at plugin boundaries
174
- // because AppDefinition's typed run() doesn't structurally match
175
- // the generic RunnableApp constraint used by plugins.
176
- const app = pipe(
177
- baseApp as any,
178
- withReact(
179
- <ExampleBanner meta={meta} controls="j/k move x toggle a add d delete Esc/q quit">
180
- <TodoApp />
181
- </ExampleBanner>,
182
- ),
183
- withTerminal(process as any),
184
- )
185
-
186
- // ============================================================================
187
- // Main
188
- // ============================================================================
189
-
190
- async function main() {
191
- // 3. run() needs no arguments — element and terminal are already bound
192
- const handle = (await app.run()) as AppHandle<State>
193
-
194
- await handle.waitUntilExit()
195
-
196
- console.log("\nFinal state:", handle.store.getState().todos.length, "todos")
197
- }
198
-
199
- if (import.meta.main) {
200
- main().catch(console.error)
201
- }