@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.
- package/dist/UPNG-ShUlaTDh.mjs +5074 -0
- package/dist/__vite-browser-external-2447137e-Bopa5BFR.mjs +4 -0
- package/dist/_banner-A70_y2Vi.mjs +43 -0
- package/dist/ansi-0VXlUmNn.mjs +16397 -0
- package/dist/apng-B0gRaDVT.mjs +3 -0
- package/dist/apng-BTRDTfDW.mjs +68 -0
- package/dist/apps/aichat/index.mjs +1298 -0
- package/dist/apps/app-todo.mjs +138 -0
- package/dist/apps/async-data.mjs +203 -0
- package/dist/apps/cli-wizard.mjs +338 -0
- package/dist/apps/clipboard.mjs +197 -0
- package/dist/apps/components.mjs +863 -0
- package/dist/apps/data-explorer.mjs +482 -0
- package/dist/apps/dev-tools.mjs +396 -0
- package/dist/apps/explorer.mjs +697 -0
- package/dist/apps/gallery.mjs +765 -0
- package/dist/apps/inline-bench.mjs +115 -0
- package/dist/apps/kanban.mjs +279 -0
- package/dist/apps/layout-ref.mjs +186 -0
- package/dist/apps/outline.mjs +202 -0
- package/dist/apps/paste-demo.mjs +188 -0
- package/dist/apps/scroll.mjs +85 -0
- package/dist/apps/search-filter.mjs +286 -0
- package/dist/apps/selection.mjs +354 -0
- package/dist/apps/spatial-focus-demo.mjs +387 -0
- package/dist/apps/task-list.mjs +257 -0
- package/dist/apps/terminal-caps-demo.mjs +314 -0
- package/dist/apps/terminal.mjs +871 -0
- package/dist/apps/text-selection-demo.mjs +253 -0
- package/dist/apps/textarea.mjs +177 -0
- package/dist/apps/theme.mjs +660 -0
- package/dist/apps/transform.mjs +214 -0
- package/dist/apps/virtual-10k.mjs +421 -0
- package/dist/assets/resvgjs.darwin-arm64-BtufyGW1.node +0 -0
- package/dist/backends-Dj-11kZF.mjs +1179 -0
- package/dist/backends-U3QwStfO.mjs +3 -0
- package/dist/{cli.mjs → bin/cli.mjs} +15 -19
- package/dist/chunk-BSw8zbkd.mjs +37 -0
- package/dist/components/counter.mjs +47 -0
- package/dist/components/hello.mjs +30 -0
- package/dist/components/progress-bar.mjs +58 -0
- package/dist/components/select-list.mjs +84 -0
- package/dist/components/spinner.mjs +56 -0
- package/dist/components/text-input.mjs +61 -0
- package/dist/components/virtual-list.mjs +50 -0
- package/dist/flexily-zero-adapter-ByVzLTFP.mjs +3374 -0
- package/dist/gif-B6NGH5gs.mjs +3 -0
- package/dist/gif-CfkOF-iG.mjs +71 -0
- package/dist/gifenc-BI4ihP_T.mjs +728 -0
- package/dist/key-mapping-5oYQdAQE.mjs +3 -0
- package/dist/key-mapping-D4LR1go6.mjs +130 -0
- package/dist/layout/dashboard.mjs +1203 -0
- package/dist/layout/live-resize.mjs +302 -0
- package/dist/layout/overflow.mjs +69 -0
- package/dist/layout/text-layout.mjs +334 -0
- package/dist/node-nsrAOjH4.mjs +1083 -0
- package/dist/plugins-CT0DdV_E.mjs +3056 -0
- package/dist/resvg-js-Cnk2o49d.mjs +201 -0
- package/dist/src-9ZhfQyzD.mjs +814 -0
- package/dist/src-CUUOuRH6.mjs +5322 -0
- package/dist/src-jO3Zuzjj.mjs +23538 -0
- package/dist/usingCtx-CsEf0xO3.mjs +57 -0
- package/dist/yoga-adapter-BSQHuMV9.mjs +237 -0
- package/package.json +21 -14
- package/_banner.tsx +0 -60
- package/apps/aichat/components.tsx +0 -469
- package/apps/aichat/index.tsx +0 -220
- package/apps/aichat/script.ts +0 -460
- package/apps/aichat/state.ts +0 -325
- package/apps/aichat/types.ts +0 -19
- package/apps/app-todo.tsx +0 -201
- package/apps/async-data.tsx +0 -196
- package/apps/cli-wizard.tsx +0 -332
- package/apps/clipboard.tsx +0 -183
- package/apps/components.tsx +0 -658
- package/apps/data-explorer.tsx +0 -490
- package/apps/dev-tools.tsx +0 -395
- package/apps/explorer.tsx +0 -731
- package/apps/gallery.tsx +0 -653
- package/apps/inline-bench.tsx +0 -138
- package/apps/kanban.tsx +0 -265
- package/apps/layout-ref.tsx +0 -173
- package/apps/outline.tsx +0 -160
- package/apps/panes/index.tsx +0 -203
- package/apps/paste-demo.tsx +0 -185
- package/apps/scroll.tsx +0 -80
- package/apps/search-filter.tsx +0 -240
- package/apps/selection.tsx +0 -346
- package/apps/spatial-focus-demo.tsx +0 -372
- package/apps/task-list.tsx +0 -271
- package/apps/terminal-caps-demo.tsx +0 -317
- package/apps/terminal.tsx +0 -784
- package/apps/text-selection-demo.tsx +0 -193
- package/apps/textarea.tsx +0 -155
- package/apps/theme.tsx +0 -515
- package/apps/transform.tsx +0 -229
- package/apps/virtual-10k.tsx +0 -405
- package/apps/vterm-demo/index.tsx +0 -216
- package/components/counter.tsx +0 -49
- package/components/hello.tsx +0 -38
- package/components/progress-bar.tsx +0 -52
- package/components/select-list.tsx +0 -54
- package/components/spinner.tsx +0 -44
- package/components/text-input.tsx +0 -61
- package/components/virtual-list.tsx +0 -56
- package/dist/cli.d.mts +0 -1
- package/dist/cli.mjs.map +0 -1
- package/layout/dashboard.tsx +0 -953
- package/layout/live-resize.tsx +0 -282
- package/layout/overflow.tsx +0 -51
- package/layout/text-layout.tsx +0 -283
package/apps/search-filter.tsx
DELETED
|
@@ -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
|
-
}
|
package/apps/selection.tsx
DELETED
|
@@ -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
|
-
}
|