@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,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
- 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
- main().catch(console.error)
240
- }
@@ -1,342 +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
- if (import.meta.main) {
340
- using handle = await run(<SelectionDemo />, { mode: "fullscreen" })
341
- await handle.waitUntilExit()
342
- }