silvery 0.3.0 → 0.4.1

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 (120) hide show
  1. package/README.md +41 -145
  2. package/dist/chalk.js +3 -0
  3. package/dist/chalk.js.map +11 -0
  4. package/dist/index.js +340 -0
  5. package/dist/index.js.map +282 -0
  6. package/dist/ink.js +129 -0
  7. package/dist/ink.js.map +140 -0
  8. package/dist/runtime.js +394 -0
  9. package/dist/runtime.js.map +286 -0
  10. package/dist/theme.js +343 -0
  11. package/dist/theme.js.map +286 -0
  12. package/dist/ui/animation.js +3 -0
  13. package/dist/ui/animation.js.map +15 -0
  14. package/dist/ui/ansi.js +3 -0
  15. package/dist/ui/ansi.js.map +10 -0
  16. package/dist/ui/cli.js +8 -0
  17. package/dist/ui/cli.js.map +14 -0
  18. package/dist/ui/display.js +4 -0
  19. package/dist/ui/display.js.map +10 -0
  20. package/dist/ui/image.js +4 -0
  21. package/dist/ui/image.js.map +15 -0
  22. package/dist/ui/input.js +3 -0
  23. package/dist/ui/input.js.map +11 -0
  24. package/dist/ui/progress.js +8 -0
  25. package/dist/ui/progress.js.map +20 -0
  26. package/dist/ui/react.js +3 -0
  27. package/dist/ui/react.js.map +15 -0
  28. package/dist/ui/utils.js +3 -0
  29. package/dist/ui/utils.js.map +10 -0
  30. package/dist/ui/wrappers.js +14 -0
  31. package/dist/ui/wrappers.js.map +19 -0
  32. package/dist/ui.js +17 -0
  33. package/dist/ui.js.map +20 -0
  34. package/package.json +67 -15
  35. package/src/index.ts +67 -1
  36. package/src/runtime.ts +4 -0
  37. package/src/theme.ts +4 -0
  38. package/src/ui/animation.ts +2 -0
  39. package/src/ui/ansi.ts +2 -0
  40. package/src/ui/cli.ts +2 -0
  41. package/src/ui/display.ts +2 -0
  42. package/src/ui/image.ts +2 -0
  43. package/src/ui/input.ts +2 -0
  44. package/src/ui/progress.ts +2 -0
  45. package/src/ui/react.ts +2 -0
  46. package/src/ui/utils.ts +2 -0
  47. package/src/ui/wrappers.ts +2 -0
  48. package/src/ui.ts +4 -0
  49. package/examples/CLAUDE.md +0 -75
  50. package/examples/_banner.tsx +0 -60
  51. package/examples/cli.ts +0 -228
  52. package/examples/index.md +0 -101
  53. package/examples/inline/inline-nontty.tsx +0 -98
  54. package/examples/inline/inline-progress.tsx +0 -79
  55. package/examples/inline/inline-simple.tsx +0 -63
  56. package/examples/inline/scrollback.tsx +0 -185
  57. package/examples/interactive/_input-debug.tsx +0 -110
  58. package/examples/interactive/_stdin-test.ts +0 -71
  59. package/examples/interactive/_textarea-bare.tsx +0 -45
  60. package/examples/interactive/aichat/components.tsx +0 -468
  61. package/examples/interactive/aichat/index.tsx +0 -207
  62. package/examples/interactive/aichat/script.ts +0 -460
  63. package/examples/interactive/aichat/state.ts +0 -326
  64. package/examples/interactive/aichat/types.ts +0 -19
  65. package/examples/interactive/app-todo.tsx +0 -198
  66. package/examples/interactive/async-data.tsx +0 -208
  67. package/examples/interactive/cli-wizard.tsx +0 -332
  68. package/examples/interactive/clipboard.tsx +0 -183
  69. package/examples/interactive/components.tsx +0 -463
  70. package/examples/interactive/data-explorer.tsx +0 -506
  71. package/examples/interactive/dev-tools.tsx +0 -379
  72. package/examples/interactive/explorer.tsx +0 -747
  73. package/examples/interactive/gallery.tsx +0 -652
  74. package/examples/interactive/inline-bench.tsx +0 -136
  75. package/examples/interactive/kanban.tsx +0 -267
  76. package/examples/interactive/layout-ref.tsx +0 -185
  77. package/examples/interactive/outline.tsx +0 -171
  78. package/examples/interactive/paste-demo.tsx +0 -198
  79. package/examples/interactive/scroll.tsx +0 -77
  80. package/examples/interactive/search-filter.tsx +0 -240
  81. package/examples/interactive/task-list.tsx +0 -279
  82. package/examples/interactive/terminal.tsx +0 -798
  83. package/examples/interactive/textarea.tsx +0 -103
  84. package/examples/interactive/theme.tsx +0 -336
  85. package/examples/interactive/transform.tsx +0 -256
  86. package/examples/interactive/virtual-10k.tsx +0 -413
  87. package/examples/kitty/canvas.tsx +0 -519
  88. package/examples/kitty/generate-samples.ts +0 -236
  89. package/examples/kitty/image-component.tsx +0 -273
  90. package/examples/kitty/images.tsx +0 -604
  91. package/examples/kitty/input.tsx +0 -371
  92. package/examples/kitty/keys.tsx +0 -378
  93. package/examples/kitty/paint.tsx +0 -1017
  94. package/examples/layout/dashboard.tsx +0 -551
  95. package/examples/layout/live-resize.tsx +0 -290
  96. package/examples/layout/overflow.tsx +0 -51
  97. package/examples/playground/README.md +0 -69
  98. package/examples/playground/build.ts +0 -61
  99. package/examples/playground/index.html +0 -420
  100. package/examples/playground/playground-app.tsx +0 -416
  101. package/examples/runtime/elm-counter.tsx +0 -206
  102. package/examples/runtime/hello-runtime.tsx +0 -73
  103. package/examples/runtime/pipe-composition.tsx +0 -184
  104. package/examples/runtime/run-counter.tsx +0 -78
  105. package/examples/runtime/runtime-counter.tsx +0 -197
  106. package/examples/screenshots/generate.tsx +0 -563
  107. package/examples/scrollback-perf.tsx +0 -230
  108. package/examples/viewer.tsx +0 -654
  109. package/examples/web/build.ts +0 -365
  110. package/examples/web/canvas-app.tsx +0 -80
  111. package/examples/web/canvas.html +0 -89
  112. package/examples/web/dom-app.tsx +0 -81
  113. package/examples/web/dom.html +0 -113
  114. package/examples/web/showcase-app.tsx +0 -107
  115. package/examples/web/showcase.html +0 -34
  116. package/examples/web/showcases/index.tsx +0 -56
  117. package/examples/web/viewer-app.tsx +0 -555
  118. package/examples/web/viewer.html +0 -30
  119. package/examples/web/xterm-app.tsx +0 -105
  120. package/examples/web/xterm.html +0 -118
@@ -1,171 +0,0 @@
1
- /**
2
- * Outline vs Border Comparison
3
- *
4
- * Side-by-side demonstration of outlineStyle vs borderStyle.
5
- * Borders push content inward (adding to layout dimensions), while
6
- * outlines overlap the content edge without affecting layout.
7
- *
8
- * Features:
9
- * - Left panel: Box with borderStyle — content area is smaller
10
- * - Right panel: Box with outlineStyle — content starts at edge
11
- * - Toggle between styles with Tab
12
- * - Live content dimensions via useContentRect()
13
- *
14
- * Run: bun vendor/silvery/examples/interactive/outline.tsx
15
- */
16
-
17
- import React, { useState } from "react"
18
- import {
19
- render,
20
- Box,
21
- Text,
22
- Kbd,
23
- Muted,
24
- useInput,
25
- useApp,
26
- useContentRect,
27
- createTerm,
28
- type Key,
29
- } from "../../src/index.js"
30
- import { ExampleBanner, type ExampleMeta } from "../_banner.js"
31
-
32
- export const meta: ExampleMeta = {
33
- name: "Outline vs Border",
34
- description: "Side-by-side comparison showing outline (no layout impact) vs border",
35
- features: ["outlineStyle", "borderStyle", "useContentRect()", "layout dimensions"],
36
- }
37
-
38
- // ============================================================================
39
- // Types
40
- // ============================================================================
41
-
42
- type StyleVariant = "single" | "double" | "round" | "bold"
43
-
44
- const STYLES: StyleVariant[] = ["single", "double", "round", "bold"]
45
-
46
- // ============================================================================
47
- // Components
48
- // ============================================================================
49
-
50
- function ContentWithSize({ label }: { label: string }): JSX.Element {
51
- const { width, height } = useContentRect()
52
-
53
- return (
54
- <Box flexDirection="column">
55
- <Text bold>{label}</Text>
56
- <Text>
57
- Content area:{" "}
58
- <Text color="$success" bold>
59
- {width}
60
- </Text>
61
- x
62
- <Text color="$success" bold>
63
- {height}
64
- </Text>
65
- </Text>
66
- <Text dim>The quick brown fox</Text>
67
- <Text dim>jumps over the lazy</Text>
68
- <Text dim>dog on a sunny day.</Text>
69
- </Box>
70
- )
71
- }
72
-
73
- function BorderPanel({ style, highlight }: { style: StyleVariant; highlight: boolean }): JSX.Element {
74
- return (
75
- <Box flexDirection="column" flexGrow={1} gap={1}>
76
- <Text bold color={highlight ? "$primary" : undefined}>
77
- borderStyle="{style}"
78
- </Text>
79
- <Box borderStyle={style} borderColor={highlight ? "$primary" : "$border"} width={30} height={9}>
80
- <ContentWithSize label="Border Box" />
81
- </Box>
82
- <Muted>Border adds to layout.</Muted>
83
- <Muted>Content is pushed inward.</Muted>
84
- </Box>
85
- )
86
- }
87
-
88
- function OutlinePanel({ style, highlight }: { style: StyleVariant; highlight: boolean }): JSX.Element {
89
- return (
90
- <Box flexDirection="column" flexGrow={1} gap={1}>
91
- <Text bold color={highlight ? "$warning" : undefined}>
92
- outlineStyle="{style}"
93
- </Text>
94
- <Box outlineStyle={style} outlineColor={highlight ? "$warning" : "$border"} width={30} height={9}>
95
- <ContentWithSize label="Outline Box" />
96
- </Box>
97
- <Muted>Outline overlaps content.</Muted>
98
- <Muted>No layout impact at all.</Muted>
99
- </Box>
100
- )
101
- }
102
-
103
- export function OutlineDemo(): JSX.Element {
104
- const { exit } = useApp()
105
- const [styleIndex, setStyleIndex] = useState(0)
106
- const [focusedSide, setFocusedSide] = useState<"border" | "outline">("border")
107
-
108
- const currentStyle = STYLES[styleIndex]!
109
-
110
- useInput((input: string, key: Key) => {
111
- if (input === "q" || key.escape) {
112
- exit()
113
- return
114
- }
115
-
116
- // Toggle focus between panels
117
- if (key.tab || input === "\t") {
118
- setFocusedSide((prev) => (prev === "border" ? "outline" : "border"))
119
- }
120
-
121
- // Cycle through border/outline styles
122
- if (key.rightArrow || input === "l") {
123
- setStyleIndex((prev) => (prev + 1) % STYLES.length)
124
- }
125
- if (key.leftArrow || input === "h") {
126
- setStyleIndex((prev) => (prev - 1 + STYLES.length) % STYLES.length)
127
- }
128
- })
129
-
130
- return (
131
- <Box flexDirection="column" padding={1} gap={1}>
132
- <Box gap={1}>
133
- <Text bold>Style:</Text>
134
- {STYLES.map((s, i) => (
135
- <Text key={s} color={i === styleIndex ? "$primary" : "$muted"} bold={i === styleIndex}>
136
- {i === styleIndex ? `[${s}]` : s}
137
- </Text>
138
- ))}
139
- </Box>
140
-
141
- <Box flexDirection="row" gap={2}>
142
- <BorderPanel style={currentStyle} highlight={focusedSide === "border"} />
143
- <OutlinePanel style={currentStyle} highlight={focusedSide === "outline"} />
144
- </Box>
145
-
146
- <Muted>
147
- {" "}
148
- <Kbd>Tab</Kbd> toggle focus <Kbd>h/l</Kbd> change style <Kbd>Esc/q</Kbd> quit
149
- </Muted>
150
- </Box>
151
- )
152
- }
153
-
154
- // ============================================================================
155
- // Main
156
- // ============================================================================
157
-
158
- async function main() {
159
- using term = createTerm()
160
- const { waitUntilExit } = await render(
161
- <ExampleBanner meta={meta} controls="Tab toggle h/l style Esc/q quit">
162
- <OutlineDemo />
163
- </ExampleBanner>,
164
- term,
165
- )
166
- await waitUntilExit()
167
- }
168
-
169
- if (import.meta.main) {
170
- main().catch(console.error)
171
- }
@@ -1,198 +0,0 @@
1
- /**
2
- * Bracketed Paste Demo
3
- *
4
- * Demonstrates bracketed paste mode — when text is pasted into the terminal,
5
- * it arrives as a single event rather than individual keystrokes. This prevents
6
- * pasted text from being interpreted as commands.
7
- *
8
- * Features:
9
- * - Shows paste mode status (always enabled with render())
10
- * - Displays pasted text as a single block event
11
- * - Shows character count and line count of pasted text
12
- * - Maintains a history of paste events
13
- *
14
- * Run: bun vendor/silvery/examples/interactive/paste-demo.tsx
15
- */
16
-
17
- import React, { useState } from "react"
18
- import {
19
- render,
20
- Box,
21
- Text,
22
- H1,
23
- Small,
24
- Kbd,
25
- Muted,
26
- Lead,
27
- useInput,
28
- useApp,
29
- createTerm,
30
- type Key,
31
- } from "../../src/index.js"
32
- import { ExampleBanner, type ExampleMeta } from "../_banner.js"
33
-
34
- export const meta: ExampleMeta = {
35
- name: "Bracketed Paste",
36
- description: "Receive pasted text as a single event via bracketed paste mode",
37
- features: ["onPaste", "useInput", "bracketed paste mode"],
38
- }
39
-
40
- // ============================================================================
41
- // Types
42
- // ============================================================================
43
-
44
- interface PasteEvent {
45
- id: number
46
- text: string
47
- charCount: number
48
- lineCount: number
49
- timestamp: string
50
- }
51
-
52
- // ============================================================================
53
- // Components
54
- // ============================================================================
55
-
56
- function PasteIndicator(): JSX.Element {
57
- return (
58
- <Box gap={1} paddingX={1}>
59
- <Text color="$success" bold>
60
- {"●"}
61
- </Text>
62
- <Text>Paste mode:</Text>
63
- <Text color="$success" bold>
64
- ENABLED
65
- </Text>
66
- <Muted>(bracketed paste is automatic with render())</Muted>
67
- </Box>
68
- )
69
- }
70
-
71
- function PasteEventCard({ event, isLatest }: { event: PasteEvent; isLatest: boolean }): JSX.Element {
72
- const preview = event.text.length > 60 ? event.text.slice(0, 57) + "..." : event.text
73
- const displayText = preview.replace(/\n/g, "\\n").replace(/\t/g, "\\t")
74
-
75
- return (
76
- <Box
77
- flexDirection="column"
78
- borderStyle="round"
79
- borderColor={isLatest ? "$primary" : "$border"}
80
- paddingX={1}
81
- marginBottom={0}
82
- >
83
- <Box justifyContent="space-between">
84
- <H1 color={isLatest ? "$primary" : "white"}>Paste #{event.id}</H1>
85
- <Small>{event.timestamp}</Small>
86
- </Box>
87
- <Box gap={2}>
88
- <Small>
89
- {event.charCount} char{event.charCount !== 1 ? "s" : ""}
90
- </Small>
91
- <Small>
92
- {event.lineCount} line{event.lineCount !== 1 ? "s" : ""}
93
- </Small>
94
- </Box>
95
- <Box marginTop={1}>
96
- <Text color="yellow">{displayText}</Text>
97
- </Box>
98
- </Box>
99
- )
100
- }
101
-
102
- function EmptyState(): JSX.Element {
103
- return (
104
- <Box flexDirection="column" padding={2} alignItems="center">
105
- <Muted>No paste events yet.</Muted>
106
- <Lead>Try pasting some text from your clipboard!</Lead>
107
- <Lead>(Cmd+V on macOS, Ctrl+Shift+V on Linux)</Lead>
108
- </Box>
109
- )
110
- }
111
-
112
- export function PasteDemo(): JSX.Element {
113
- const { exit } = useApp()
114
- const [pasteHistory, setPasteHistory] = useState<PasteEvent[]>([])
115
- const [nextId, setNextId] = useState(1)
116
-
117
- useInput(
118
- (input: string, key: Key) => {
119
- if (input === "q" || key.escape) {
120
- exit()
121
- return
122
- }
123
-
124
- // Clear history
125
- if (input === "x") {
126
- setPasteHistory([])
127
- setNextId(1)
128
- }
129
- },
130
- {
131
- onPaste: (text: string) => {
132
- const now = new Date()
133
- const timestamp = `${now.getHours().toString().padStart(2, "0")}:${now.getMinutes().toString().padStart(2, "0")}:${now.getSeconds().toString().padStart(2, "0")}`
134
-
135
- const event: PasteEvent = {
136
- id: nextId,
137
- text,
138
- charCount: text.length,
139
- lineCount: text.split("\n").length,
140
- timestamp,
141
- }
142
-
143
- setPasteHistory((prev) => [event, ...prev].slice(0, 10))
144
- setNextId((prev) => prev + 1)
145
- },
146
- },
147
- )
148
-
149
- return (
150
- <Box flexDirection="column" padding={1} gap={1}>
151
- <PasteIndicator />
152
-
153
- <Box flexDirection="column" borderStyle="round" borderColor="$primary" paddingX={1}>
154
- <Box marginBottom={1}>
155
- <H1>Paste History</H1>
156
- <Small>
157
- {" "}
158
- — {pasteHistory.length} event{pasteHistory.length !== 1 ? "s" : ""}
159
- </Small>
160
- </Box>
161
-
162
- {pasteHistory.length === 0 ? (
163
- <EmptyState />
164
- ) : (
165
- <Box flexDirection="column" overflow="scroll" height={12} gap={1}>
166
- {pasteHistory.map((event, index) => (
167
- <PasteEventCard key={event.id} event={event} isLatest={index === 0} />
168
- ))}
169
- </Box>
170
- )}
171
- </Box>
172
-
173
- <Muted>
174
- {" "}
175
- <Kbd>Paste text</Kbd> to see events <Kbd>x</Kbd> clear <Kbd>Esc/q</Kbd> quit
176
- </Muted>
177
- </Box>
178
- )
179
- }
180
-
181
- // ============================================================================
182
- // Main
183
- // ============================================================================
184
-
185
- async function main() {
186
- using term = createTerm()
187
- const { waitUntilExit } = await render(
188
- <ExampleBanner meta={meta} controls="Paste text to see events x clear Esc/q quit">
189
- <PasteDemo />
190
- </ExampleBanner>,
191
- term,
192
- )
193
- await waitUntilExit()
194
- }
195
-
196
- if (import.meta.main) {
197
- main().catch(console.error)
198
- }
@@ -1,77 +0,0 @@
1
- /**
2
- * Scroll Example
3
- *
4
- * Demonstrates overflow="scroll" with keyboard navigation.
5
- */
6
-
7
- import React, { useState } from "react"
8
- import { Box, Text, Kbd, Muted, render, useInput, useApp, createTerm, type Key } from "../../src/index.js"
9
- import { ExampleBanner, type ExampleMeta } from "../_banner.js"
10
-
11
- export const meta: ExampleMeta = {
12
- name: "Scroll",
13
- description: 'Native overflow="scroll" with automatic scroll-to-selected',
14
- features: ['overflow="scroll"', "scrollTo", "useInput"],
15
- }
16
-
17
- // Generate sample items
18
- const items = Array.from({ length: 50 }, (_, i) => ({
19
- id: i,
20
- title: `Item ${i + 1}`,
21
- description: `This is the description for item number ${i + 1}`,
22
- }))
23
-
24
- export function ScrollExample() {
25
- const { exit } = useApp()
26
- const [selectedIndex, setSelectedIndex] = useState(0)
27
-
28
- useInput((input: string, key: Key) => {
29
- if (input === "q" || key.escape) {
30
- exit()
31
- }
32
- if (key.upArrow || input === "k") {
33
- setSelectedIndex((prev) => Math.max(0, prev - 1))
34
- }
35
- if (key.downArrow || input === "j") {
36
- setSelectedIndex((prev) => Math.min(items.length - 1, prev + 1))
37
- }
38
- })
39
-
40
- return (
41
- <Box flexDirection="column" width={60} height={20}>
42
- <Box
43
- flexGrow={1}
44
- flexDirection="column"
45
- borderStyle="round"
46
- borderColor="$primary"
47
- overflow="scroll"
48
- scrollTo={selectedIndex}
49
- height={10}
50
- >
51
- {items.map((item, index) => (
52
- <Box key={item.id} paddingX={1} backgroundColor={index === selectedIndex ? "$primary" : undefined}>
53
- <Text color={index === selectedIndex ? "black" : "white"} bold={index === selectedIndex}>
54
- {item.title}
55
- </Text>
56
- </Box>
57
- ))}
58
- </Box>
59
-
60
- <Muted>
61
- {" "}
62
- <Kbd>j/k</Kbd> navigate <Kbd>Esc/q</Kbd> quit | Selected: {selectedIndex + 1}/{items.length}
63
- </Muted>
64
- </Box>
65
- )
66
- }
67
-
68
- // Run the app
69
- if (import.meta.main) {
70
- using term = createTerm()
71
- await render(
72
- <ExampleBanner meta={meta} controls="j/k navigate Esc/q quit">
73
- <ScrollExample />
74
- </ExampleBanner>,
75
- term,
76
- )
77
- }
@@ -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 "../../src/index.js"
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="gray"> {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(): JSX.Element {
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
- }