@silvery/examples 0.17.3 → 0.17.5

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 (111) hide show
  1. package/dist/UPNG-ShUlaTDh.mjs +5074 -0
  2. package/dist/__vite-browser-external-2447137e-Bopa5BFR.mjs +4 -0
  3. package/dist/_banner-A70_y2Vi.mjs +43 -0
  4. package/dist/ansi-0VXlUmNn.mjs +16397 -0
  5. package/dist/apng-B0gRaDVT.mjs +3 -0
  6. package/dist/apng-BTRDTfDW.mjs +68 -0
  7. package/dist/apps/aichat/index.mjs +1298 -0
  8. package/dist/apps/app-todo.mjs +138 -0
  9. package/dist/apps/async-data.mjs +203 -0
  10. package/dist/apps/cli-wizard.mjs +338 -0
  11. package/dist/apps/clipboard.mjs +197 -0
  12. package/dist/apps/components.mjs +863 -0
  13. package/dist/apps/data-explorer.mjs +482 -0
  14. package/dist/apps/dev-tools.mjs +396 -0
  15. package/dist/apps/explorer.mjs +697 -0
  16. package/dist/apps/gallery.mjs +765 -0
  17. package/dist/apps/inline-bench.mjs +115 -0
  18. package/dist/apps/kanban.mjs +279 -0
  19. package/dist/apps/layout-ref.mjs +186 -0
  20. package/dist/apps/outline.mjs +202 -0
  21. package/dist/apps/paste-demo.mjs +188 -0
  22. package/dist/apps/scroll.mjs +85 -0
  23. package/dist/apps/search-filter.mjs +286 -0
  24. package/dist/apps/selection.mjs +354 -0
  25. package/dist/apps/spatial-focus-demo.mjs +387 -0
  26. package/dist/apps/task-list.mjs +257 -0
  27. package/dist/apps/terminal-caps-demo.mjs +314 -0
  28. package/dist/apps/terminal.mjs +871 -0
  29. package/dist/apps/text-selection-demo.mjs +253 -0
  30. package/dist/apps/textarea.mjs +177 -0
  31. package/dist/apps/theme.mjs +660 -0
  32. package/dist/apps/transform.mjs +214 -0
  33. package/dist/apps/virtual-10k.mjs +421 -0
  34. package/dist/assets/resvgjs.darwin-arm64-BtufyGW1.node +0 -0
  35. package/dist/backends-Dj-11kZF.mjs +1179 -0
  36. package/dist/backends-U3QwStfO.mjs +3 -0
  37. package/dist/{cli.mjs → bin/cli.mjs} +15 -19
  38. package/dist/chunk-BSw8zbkd.mjs +37 -0
  39. package/dist/components/counter.mjs +47 -0
  40. package/dist/components/hello.mjs +30 -0
  41. package/dist/components/progress-bar.mjs +58 -0
  42. package/dist/components/select-list.mjs +84 -0
  43. package/dist/components/spinner.mjs +56 -0
  44. package/dist/components/text-input.mjs +61 -0
  45. package/dist/components/virtual-list.mjs +50 -0
  46. package/dist/flexily-zero-adapter-ByVzLTFP.mjs +3374 -0
  47. package/dist/gif-B6NGH5gs.mjs +3 -0
  48. package/dist/gif-CfkOF-iG.mjs +71 -0
  49. package/dist/gifenc-BI4ihP_T.mjs +728 -0
  50. package/dist/key-mapping-5oYQdAQE.mjs +3 -0
  51. package/dist/key-mapping-D4LR1go6.mjs +130 -0
  52. package/dist/layout/dashboard.mjs +1203 -0
  53. package/dist/layout/live-resize.mjs +302 -0
  54. package/dist/layout/overflow.mjs +69 -0
  55. package/dist/layout/text-layout.mjs +334 -0
  56. package/dist/node-nsrAOjH4.mjs +1083 -0
  57. package/dist/plugins-CT0DdV_E.mjs +3056 -0
  58. package/dist/resvg-js-Cnk2o49d.mjs +201 -0
  59. package/dist/src-9ZhfQyzD.mjs +814 -0
  60. package/dist/src-CUUOuRH6.mjs +5322 -0
  61. package/dist/src-jO3Zuzjj.mjs +23538 -0
  62. package/dist/usingCtx-CsEf0xO3.mjs +57 -0
  63. package/dist/yoga-adapter-BSQHuMV9.mjs +237 -0
  64. package/package.json +21 -14
  65. package/_banner.tsx +0 -60
  66. package/apps/aichat/components.tsx +0 -469
  67. package/apps/aichat/index.tsx +0 -220
  68. package/apps/aichat/script.ts +0 -460
  69. package/apps/aichat/state.ts +0 -325
  70. package/apps/aichat/types.ts +0 -19
  71. package/apps/app-todo.tsx +0 -201
  72. package/apps/async-data.tsx +0 -196
  73. package/apps/cli-wizard.tsx +0 -332
  74. package/apps/clipboard.tsx +0 -183
  75. package/apps/components.tsx +0 -658
  76. package/apps/data-explorer.tsx +0 -490
  77. package/apps/dev-tools.tsx +0 -395
  78. package/apps/explorer.tsx +0 -731
  79. package/apps/gallery.tsx +0 -653
  80. package/apps/inline-bench.tsx +0 -138
  81. package/apps/kanban.tsx +0 -265
  82. package/apps/layout-ref.tsx +0 -173
  83. package/apps/outline.tsx +0 -160
  84. package/apps/panes/index.tsx +0 -203
  85. package/apps/paste-demo.tsx +0 -185
  86. package/apps/scroll.tsx +0 -80
  87. package/apps/search-filter.tsx +0 -240
  88. package/apps/selection.tsx +0 -346
  89. package/apps/spatial-focus-demo.tsx +0 -372
  90. package/apps/task-list.tsx +0 -271
  91. package/apps/terminal-caps-demo.tsx +0 -317
  92. package/apps/terminal.tsx +0 -784
  93. package/apps/text-selection-demo.tsx +0 -193
  94. package/apps/textarea.tsx +0 -155
  95. package/apps/theme.tsx +0 -515
  96. package/apps/transform.tsx +0 -229
  97. package/apps/virtual-10k.tsx +0 -405
  98. package/apps/vterm-demo/index.tsx +0 -216
  99. package/components/counter.tsx +0 -49
  100. package/components/hello.tsx +0 -38
  101. package/components/progress-bar.tsx +0 -52
  102. package/components/select-list.tsx +0 -54
  103. package/components/spinner.tsx +0 -44
  104. package/components/text-input.tsx +0 -61
  105. package/components/virtual-list.tsx +0 -56
  106. package/dist/cli.d.mts +0 -1
  107. package/dist/cli.mjs.map +0 -1
  108. package/layout/dashboard.tsx +0 -953
  109. package/layout/live-resize.tsx +0 -282
  110. package/layout/overflow.tsx +0 -51
  111. package/layout/text-layout.tsx +0 -283
@@ -1,240 +0,0 @@
1
- /**
2
- * Search Filter Example
3
- *
4
- * Demonstrates React concurrent features for responsive typing:
5
- * - useTransition for low-priority state updates
6
- * - useDeferredValue for deferred filtering
7
- * - Typing remains responsive even with heavy filtering
8
- */
9
-
10
- import React, { useState, useDeferredValue, useTransition } from "react"
11
- import { render, Box, Text, Kbd, Muted, Strong, Lead, useInput, useApp, createTerm, type Key } from "silvery"
12
- import { ExampleBanner, type ExampleMeta } from "../_banner.js"
13
-
14
- export const meta: ExampleMeta = {
15
- name: "Search Filter",
16
- description: "useTransition + useDeferredValue for responsive concurrent search",
17
- features: ["useDeferredValue", "useTransition", "useInput"],
18
- }
19
-
20
- // ============================================================================
21
- // Types
22
- // ============================================================================
23
-
24
- interface Item {
25
- id: number
26
- name: string
27
- category: string
28
- tags: string[]
29
- }
30
-
31
- // ============================================================================
32
- // Data
33
- // ============================================================================
34
-
35
- const items: Item[] = [
36
- {
37
- id: 1,
38
- name: "React Hooks Guide",
39
- category: "docs",
40
- tags: ["react", "hooks", "tutorial"],
41
- },
42
- {
43
- id: 2,
44
- name: "TypeScript Patterns",
45
- category: "docs",
46
- tags: ["typescript", "patterns"],
47
- },
48
- {
49
- id: 3,
50
- name: "Build Configuration",
51
- category: "config",
52
- tags: ["webpack", "vite", "build"],
53
- },
54
- {
55
- id: 4,
56
- name: "Testing Best Practices",
57
- category: "docs",
58
- tags: ["testing", "jest", "vitest"],
59
- },
60
- {
61
- id: 5,
62
- name: "API Documentation",
63
- category: "docs",
64
- tags: ["api", "rest", "graphql"],
65
- },
66
- {
67
- id: 6,
68
- name: "Database Schema",
69
- category: "config",
70
- tags: ["database", "sql", "migration"],
71
- },
72
- {
73
- id: 7,
74
- name: "Authentication Flow",
75
- category: "docs",
76
- tags: ["auth", "security", "jwt"],
77
- },
78
- {
79
- id: 8,
80
- name: "CI/CD Pipeline",
81
- category: "config",
82
- tags: ["ci", "deployment", "github"],
83
- },
84
- {
85
- id: 9,
86
- name: "Performance Tuning",
87
- category: "docs",
88
- tags: ["performance", "optimization"],
89
- },
90
- {
91
- id: 10,
92
- name: "Error Handling",
93
- category: "docs",
94
- tags: ["errors", "debugging", "logging"],
95
- },
96
- {
97
- id: 11,
98
- name: "State Management",
99
- category: "docs",
100
- tags: ["state", "redux", "zustand"],
101
- },
102
- {
103
- id: 12,
104
- name: "CSS Architecture",
105
- category: "docs",
106
- tags: ["css", "tailwind", "styled"],
107
- },
108
- {
109
- id: 13,
110
- name: "Security Guidelines",
111
- category: "docs",
112
- tags: ["security", "owasp", "audit"],
113
- },
114
- {
115
- id: 14,
116
- name: "Deployment Scripts",
117
- category: "config",
118
- tags: ["deploy", "docker", "k8s"],
119
- },
120
- {
121
- id: 15,
122
- name: "Monitoring Setup",
123
- category: "config",
124
- tags: ["monitoring", "metrics", "logs"],
125
- },
126
- ]
127
-
128
- // ============================================================================
129
- // Components
130
- // ============================================================================
131
-
132
- function SearchInput({ value, onChange }: { value: string; onChange: (v: string) => void }) {
133
- return (
134
- <Box>
135
- <Strong color="$primary">Search: </Strong>
136
- <Text>{value}</Text>
137
- <Text dim>|</Text>
138
- </Box>
139
- )
140
- }
141
-
142
- function FilteredList({ query, isPending }: { query: string; isPending: boolean }) {
143
- // Simulate expensive filtering (in real app this might be fuzzy search)
144
- const filtered = items.filter((item) => {
145
- const searchLower = query.toLowerCase()
146
- return (
147
- item.name.toLowerCase().includes(searchLower) ||
148
- item.category.toLowerCase().includes(searchLower) ||
149
- item.tags.some((tag) => tag.toLowerCase().includes(searchLower))
150
- )
151
- })
152
-
153
- return (
154
- <Box flexDirection="column" marginTop={1}>
155
- <Box marginBottom={1}>
156
- <Muted>
157
- {filtered.length} results
158
- {isPending && " (filtering...)"}
159
- </Muted>
160
- </Box>
161
- {filtered.map((item) => (
162
- <Box key={item.id} marginBottom={1}>
163
- <Text bold>{item.name}</Text>
164
- <Text dim> [{item.category}]</Text>
165
- <Text color="$muted"> {item.tags.join(", ")}</Text>
166
- </Box>
167
- ))}
168
- {filtered.length === 0 && <Lead>No matches found</Lead>}
169
- </Box>
170
- )
171
- }
172
-
173
- export function SearchApp() {
174
- const { exit } = useApp()
175
- const [query, setQuery] = useState("")
176
-
177
- // useDeferredValue: The filtered list uses a deferred version of the query
178
- // This keeps typing responsive while the list catches up
179
- const deferredQuery = useDeferredValue(query)
180
-
181
- // useTransition: Mark filtering as low-priority (optional, shows pending state)
182
- const [isPending, startTransition] = useTransition()
183
-
184
- useInput((input: string, key: Key) => {
185
- if (key.escape) {
186
- exit()
187
- return
188
- }
189
-
190
- if (key.backspace || key.delete) {
191
- // Backspace: remove last character
192
- startTransition(() => {
193
- setQuery((prev) => prev.slice(0, -1))
194
- })
195
- return
196
- }
197
-
198
- // Add printable characters
199
- if (input && !key.ctrl && !key.meta) {
200
- startTransition(() => {
201
- setQuery((prev) => prev + input)
202
- })
203
- }
204
- })
205
-
206
- return (
207
- <Box flexDirection="column" padding={1}>
208
- <SearchInput value={query} onChange={setQuery} />
209
-
210
- {/* List uses deferredQuery so typing stays responsive */}
211
- <Box flexGrow={1}>
212
- <FilteredList query={deferredQuery} isPending={isPending} />
213
- </Box>
214
-
215
- <Muted>
216
- {" "}
217
- <Kbd>type</Kbd> to search <Kbd>Esc/q</Kbd> quit
218
- </Muted>
219
- </Box>
220
- )
221
- }
222
-
223
- // ============================================================================
224
- // Main
225
- // ============================================================================
226
-
227
- export async function main() {
228
- using term = createTerm()
229
- const { waitUntilExit } = await render(
230
- <ExampleBanner meta={meta} controls="type to search Esc quit">
231
- <SearchApp />
232
- </ExampleBanner>,
233
- term,
234
- )
235
- await waitUntilExit()
236
- }
237
-
238
- if (import.meta.main) {
239
- await main()
240
- }
@@ -1,346 +0,0 @@
1
- /**
2
- * Selection Model Demo
3
- *
4
- * Demonstrates the silvery selection model from docs/design/selection-model.md:
5
- * - Node selection (click, j/k)
6
- * - Multi-select (Cmd+click toggle, Shift+j/k extend)
7
- * - Text editing (Enter → edit, Escape → node mode)
8
- * - Mode ladder: text ──Esc──► node ──Esc──► board
9
- * - Live status bar showing Selection state
10
- *
11
- * Run: bun examples/apps/selection.tsx
12
- */
13
-
14
- import React, { useState, useCallback } from "react"
15
- import { Box, Text, type SilveryMouseEvent } from "silvery"
16
- import { run, useInput, type Key } from "silvery/runtime"
17
-
18
- // ============================================================================
19
- // Selection Model (pure functions — the whole design doc in ~60 lines)
20
- // ============================================================================
21
-
22
- type ID = string
23
-
24
- type TextPoint = { nodeId: ID; offset: number }
25
-
26
- type Selection = {
27
- nodes: readonly [ID, ...ID[]]
28
- text?: readonly [TextPoint] | readonly [TextPoint, TextPoint]
29
- }
30
-
31
- const S = {
32
- // Read
33
- cursor: (sel: Selection): ID => sel.nodes[0],
34
- anchor: (sel: Selection): ID => sel.nodes.at(-1)!,
35
- ids: (sel: Selection): ReadonlySet<ID> => new Set(sel.nodes),
36
- includes: (sel: Selection, id: ID): boolean => sel.nodes.includes(id),
37
- isEditing: (sel: Selection): boolean => !!sel.text,
38
- inputMode: (sel: Selection | undefined): "board" | "node" | "text" => (!sel ? "board" : sel.text ? "text" : "node"),
39
-
40
- // Node mutations (clear text)
41
- select: (id: ID): Selection => ({ nodes: [id] }),
42
-
43
- toggle(sel: Selection, id: ID): Selection | undefined {
44
- if (sel.nodes.includes(id)) {
45
- const rest = sel.nodes.filter((n) => n !== id) as unknown as [ID, ...ID[]]
46
- return rest.length > 0 ? { nodes: rest } : undefined
47
- }
48
- return { nodes: [id, ...sel.nodes] }
49
- },
50
-
51
- extend(sel: Selection, id: ID, allIds: readonly ID[]): Selection {
52
- const anchorIdx = allIds.indexOf(S.anchor(sel))
53
- const targetIdx = allIds.indexOf(id)
54
- if (anchorIdx < 0 || targetIdx < 0) return sel
55
- const lo = Math.min(anchorIdx, targetIdx)
56
- const hi = Math.max(anchorIdx, targetIdx)
57
- const range = allIds.slice(lo, hi + 1)
58
- // cursor first, anchor last
59
- const nodes = targetIdx <= anchorIdx ? (range as [ID, ...ID[]]) : ([...range].reverse() as [ID, ...ID[]])
60
- return { nodes }
61
- },
62
-
63
- areaSelect(_sel: Selection | undefined, hitIds: readonly ID[], mode: "replace" | "xor"): Selection | undefined {
64
- if (mode === "replace") {
65
- return hitIds.length > 0 ? { nodes: hitIds as [ID, ...ID[]] } : undefined
66
- }
67
- // XOR: toggle each hit against current
68
- let result = _sel
69
- for (const id of hitIds) {
70
- result = result ? S.toggle(result, id) : S.select(id)
71
- }
72
- return result
73
- },
74
-
75
- clear: (): undefined => undefined,
76
-
77
- collapseToCursor(sel: Selection): Selection {
78
- return { nodes: [sel.nodes[0]] }
79
- },
80
-
81
- // Text mutations (don't touch nodes)
82
- edit(sel: Selection, offset: number): Selection {
83
- return { ...sel, text: [{ nodeId: sel.nodes[0], offset }] }
84
- },
85
-
86
- stopEditing(sel: Selection): Selection {
87
- const { text: _, ...rest } = sel
88
- return rest as Selection
89
- },
90
- }
91
-
92
- // ============================================================================
93
- // Demo Data
94
- // ============================================================================
95
-
96
- const ITEMS: { id: ID; label: string }[] = [
97
- { id: "inbox", label: "Inbox" },
98
- { id: "today", label: "Today" },
99
- { id: "next", label: "Next Actions" },
100
- { id: "projects", label: "Projects" },
101
- { id: "waiting", label: "Waiting For" },
102
- { id: "someday", label: "Someday / Maybe" },
103
- { id: "reference", label: "Reference" },
104
- { id: "calendar", label: "Calendar" },
105
- { id: "review", label: "Weekly Review" },
106
- { id: "done", label: "Done" },
107
- ]
108
- const ALL_IDS = ITEMS.map((i) => i.id)
109
-
110
- // ============================================================================
111
- // Components
112
- // ============================================================================
113
-
114
- function ItemRow({
115
- item,
116
- sel,
117
- onSelect,
118
- }: {
119
- item: { id: ID; label: string }
120
- sel: Selection | undefined
121
- onSelect: (id: ID, meta: boolean, shift: boolean) => void
122
- }) {
123
- const isCursor = sel ? S.cursor(sel) === item.id : false
124
- const isAnchor = sel ? S.anchor(sel) === item.id : false
125
- const isSelected = sel ? S.includes(sel, item.id) : false
126
- const isEditing = isCursor && sel ? S.isEditing(sel) : false
127
-
128
- const marker = isCursor ? "►" : isSelected ? "●" : " "
129
- const anchorMark = isAnchor && !isCursor ? " ⚓" : ""
130
-
131
- return (
132
- <Box
133
- onClick={(e: SilveryMouseEvent) => onSelect(item.id, e.metaKey, e.shiftKey)}
134
- onDoubleClick={() => onSelect(item.id, false, false)}
135
- >
136
- <Text color={isSelected ? "$primary" : "$muted"}>{marker} </Text>
137
- {isEditing ? (
138
- <Text backgroundColor="$surface" color="$text" bold>
139
- {" "}
140
- {item.label}
141
- <Text color="$primary">│</Text>{" "}
142
- </Text>
143
- ) : (
144
- <Text
145
- bold={isCursor}
146
- color={isCursor ? "$primary" : isSelected ? "$primary" : "$text"}
147
- dim={!isSelected && !isCursor}
148
- >
149
- {item.label}
150
- </Text>
151
- )}
152
- <Text color="$muted" dim>
153
- {anchorMark}
154
- </Text>
155
- </Box>
156
- )
157
- }
158
-
159
- function StatusBar({ sel }: { sel: Selection | undefined }) {
160
- const mode = S.inputMode(sel)
161
- const modeColor = mode === "text" ? "$success" : mode === "node" ? "$primary" : "$muted"
162
-
163
- return (
164
- <Box flexDirection="column" borderStyle="single" borderColor="$border" paddingX={1}>
165
- <Box gap={2}>
166
- <Text color={modeColor} bold>
167
- {mode.toUpperCase()}
168
- </Text>
169
- {sel && (
170
- <>
171
- <Text color="$muted">
172
- cursor=<Text color="$primary">{S.cursor(sel)}</Text>
173
- </Text>
174
- <Text color="$muted">
175
- anchor=<Text color="$text">{S.anchor(sel)}</Text>
176
- </Text>
177
- <Text color="$muted">
178
- selected=<Text color="$text">{sel.nodes.length}</Text>
179
- </Text>
180
- {sel.text && (
181
- <Text color="$muted">
182
- text=<Text color="$success">offset {sel.text[0].offset}</Text>
183
- </Text>
184
- )}
185
- </>
186
- )}
187
- </Box>
188
- <Text color="$muted" dim>
189
- j/k nav · Enter edit · Esc back · Shift+j/k extend · Cmd+click toggle · q quit
190
- </Text>
191
- </Box>
192
- )
193
- }
194
-
195
- function SelectionDemo() {
196
- const [sel, setSel] = useState<Selection | undefined>(undefined)
197
- const [editTexts, setEditTexts] = useState<Record<ID, string>>(() =>
198
- Object.fromEntries(ITEMS.map((i) => [i.id, i.label])),
199
- )
200
-
201
- const cursorIndex = sel ? ALL_IDS.indexOf(S.cursor(sel)) : -1
202
-
203
- const handleSelect = useCallback((id: ID, meta: boolean, shift: boolean) => {
204
- setSel((prev) => {
205
- if (meta && prev) return S.toggle(prev, id)
206
- if (shift && prev) return S.extend(prev, id, ALL_IDS)
207
- return S.select(id)
208
- })
209
- }, [])
210
-
211
- useInput((input: string, key: Key) => {
212
- if (input === "q" || (key.escape && !sel)) return "exit"
213
-
214
- const mode = S.inputMode(sel)
215
-
216
- // Text mode: handle typing
217
- if (mode === "text" && sel) {
218
- if (key.escape) {
219
- setSel(S.stopEditing(sel))
220
- return
221
- }
222
- if (key.backspace && sel.text) {
223
- const nodeId = S.cursor(sel)
224
- const offset = sel.text[0].offset
225
- if (offset > 0) {
226
- setEditTexts((prev) => ({
227
- ...prev,
228
- [nodeId]: prev[nodeId]!.slice(0, offset - 1) + prev[nodeId]!.slice(offset),
229
- }))
230
- setSel(S.edit(sel, offset - 1))
231
- }
232
- return
233
- }
234
- if (key.leftArrow && sel.text) {
235
- setSel(S.edit(sel, Math.max(0, sel.text[0].offset - 1)))
236
- return
237
- }
238
- if (key.rightArrow && sel.text) {
239
- const maxLen = editTexts[S.cursor(sel)]?.length ?? 0
240
- setSel(S.edit(sel, Math.min(maxLen, sel.text[0].offset + 1)))
241
- return
242
- }
243
- // Printable character
244
- if (input && !key.ctrl && !key.meta && sel.text) {
245
- const nodeId = S.cursor(sel)
246
- const offset = sel.text[0].offset
247
- setEditTexts((prev) => ({
248
- ...prev,
249
- [nodeId]: prev[nodeId]!.slice(0, offset) + input + prev[nodeId]!.slice(offset),
250
- }))
251
- setSel(S.edit(sel, offset + input.length))
252
- return
253
- }
254
- return
255
- }
256
-
257
- // Node mode
258
- if (mode === "node" && sel) {
259
- if (key.escape) {
260
- // Mode ladder: multi → single → board
261
- if (sel.nodes.length > 1) {
262
- setSel(S.collapseToCursor(sel))
263
- } else {
264
- setSel(S.clear())
265
- }
266
- return
267
- }
268
- if (key.return) {
269
- const text = editTexts[S.cursor(sel)] ?? ""
270
- setSel(S.edit(sel, text.length))
271
- return
272
- }
273
- if (input === "j" || key.downArrow) {
274
- const next = Math.min(ALL_IDS.length - 1, cursorIndex + 1)
275
- if (key.shift) {
276
- setSel(S.extend(sel, ALL_IDS[next]!, ALL_IDS))
277
- } else {
278
- setSel(S.select(ALL_IDS[next]!))
279
- }
280
- return
281
- }
282
- if (input === "k" || key.upArrow) {
283
- const next = Math.max(0, cursorIndex - 1)
284
- if (key.shift) {
285
- setSel(S.extend(sel, ALL_IDS[next]!, ALL_IDS))
286
- } else {
287
- setSel(S.select(ALL_IDS[next]!))
288
- }
289
- return
290
- }
291
- return
292
- }
293
-
294
- // Board mode — any nav enters node mode
295
- if (input === "j" || key.downArrow) setSel(S.select(ALL_IDS[0]!))
296
- if (input === "k" || key.upArrow) setSel(S.select(ALL_IDS.at(-1)!))
297
- })
298
-
299
- // Sync edit texts back to items for display
300
- const displayItems = ITEMS.map((item) => ({ ...item, label: editTexts[item.id] ?? item.label }))
301
-
302
- return (
303
- <Box flexDirection="column" padding={1} height="100%">
304
- <Box marginBottom={1}>
305
- <Text bold color="$primary">
306
- Selection Model Demo
307
- </Text>
308
- <Text color="$muted"> — silvery reactive selection</Text>
309
- </Box>
310
-
311
- <Box flexDirection="column" flexGrow={1} borderStyle="round" borderColor="$border" overflow="hidden">
312
- {displayItems.map((item) => (
313
- <ItemRow key={item.id} item={item} sel={sel} onSelect={handleSelect} />
314
- ))}
315
- </Box>
316
-
317
- <StatusBar sel={sel} />
318
-
319
- <Box marginTop={1} flexDirection="column">
320
- <Text color="$muted" dim>
321
- nodes=[{sel?.nodes.join(", ") ?? ""}]
322
- </Text>
323
- </Box>
324
- </Box>
325
- )
326
- }
327
-
328
- // ============================================================================
329
- // Main
330
- // ============================================================================
331
-
332
- export const meta = {
333
- name: "Selection",
334
- description: "Reactive selection model — node/text modes, multi-select, mode ladder",
335
- demo: true,
336
- features: ["Selection model", "mode ladder", "multi-select", "text editing"],
337
- }
338
-
339
- export async function main() {
340
- using handle = await run(<SelectionDemo />, { mode: "fullscreen" })
341
- await handle.waitUntilExit()
342
- }
343
-
344
- if (import.meta.main) {
345
- await main()
346
- }