@silvery/examples 0.4.2
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/bin/cli.ts +286 -0
- package/examples/apps/aichat/components.tsx +469 -0
- package/examples/apps/aichat/index.tsx +207 -0
- package/examples/apps/aichat/script.ts +460 -0
- package/examples/apps/aichat/state.ts +326 -0
- package/examples/apps/aichat/types.ts +19 -0
- package/examples/apps/app-todo.tsx +201 -0
- package/examples/apps/async-data.tsx +208 -0
- package/examples/apps/cli-wizard.tsx +332 -0
- package/examples/apps/clipboard.tsx +183 -0
- package/examples/apps/components.tsx +463 -0
- package/examples/apps/data-explorer.tsx +490 -0
- package/examples/apps/dev-tools.tsx +379 -0
- package/examples/apps/explorer.tsx +731 -0
- package/examples/apps/gallery.tsx +653 -0
- package/examples/apps/inline-bench.tsx +136 -0
- package/examples/apps/kanban.tsx +267 -0
- package/examples/apps/layout-ref.tsx +185 -0
- package/examples/apps/outline.tsx +171 -0
- package/examples/apps/panes/index.tsx +205 -0
- package/examples/apps/paste-demo.tsx +198 -0
- package/examples/apps/scroll.tsx +77 -0
- package/examples/apps/search-filter.tsx +240 -0
- package/examples/apps/task-list.tsx +271 -0
- package/examples/apps/terminal.tsx +800 -0
- package/examples/apps/textarea.tsx +103 -0
- package/examples/apps/theme.tsx +515 -0
- package/examples/apps/transform.tsx +242 -0
- package/examples/apps/virtual-10k.tsx +405 -0
- package/examples/components/counter.tsx +45 -0
- package/examples/components/hello.tsx +34 -0
- package/examples/components/progress-bar.tsx +48 -0
- package/examples/components/select-list.tsx +50 -0
- package/examples/components/spinner.tsx +40 -0
- package/examples/components/text-input.tsx +57 -0
- package/examples/components/virtual-list.tsx +52 -0
- package/package.json +27 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OSC 52 Clipboard Demo
|
|
3
|
+
*
|
|
4
|
+
* Shows copy/paste across terminal sessions using the OSC 52 protocol.
|
|
5
|
+
* Select items from a list, copy them to the system clipboard, and
|
|
6
|
+
* request clipboard contents back — all without native clipboard access.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Navigate a list of items with j/k
|
|
10
|
+
* - Press c to copy selected item via OSC 52
|
|
11
|
+
* - Press v to request clipboard contents
|
|
12
|
+
* - Status bar shows last copied/pasted text
|
|
13
|
+
*
|
|
14
|
+
* Run: bun vendor/silvery/examples/apps/clipboard.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
|
+
useStdout,
|
|
30
|
+
createTerm,
|
|
31
|
+
copyToClipboard,
|
|
32
|
+
requestClipboard,
|
|
33
|
+
parseClipboardResponse,
|
|
34
|
+
type Key,
|
|
35
|
+
} from "../../src/index.js"
|
|
36
|
+
import { ExampleBanner, type ExampleMeta } from "../_banner.js"
|
|
37
|
+
|
|
38
|
+
export const meta: ExampleMeta = {
|
|
39
|
+
name: "Clipboard (OSC 52)",
|
|
40
|
+
description: "Copy/paste via OSC 52 terminal protocol",
|
|
41
|
+
features: ["copyToClipboard()", "requestClipboard()", "parseClipboardResponse()", "useStdout"],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// Data
|
|
46
|
+
// ============================================================================
|
|
47
|
+
|
|
48
|
+
const items = [
|
|
49
|
+
{ category: "Colors", values: ["Crimson", "Cerulean", "Chartreuse", "Coral", "Cobalt", "Cyan"] },
|
|
50
|
+
{ category: "Languages", values: ["TypeScript", "Rust", "Elixir", "Haskell", "Zig", "OCaml"] },
|
|
51
|
+
{
|
|
52
|
+
category: "Fruits",
|
|
53
|
+
values: ["Mango", "Passionfruit", "Dragon fruit", "Starfruit", "Lychee", "Rambutan"],
|
|
54
|
+
},
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
const allItems = items.flatMap((group) => group.values.map((value) => ({ category: group.category, value })))
|
|
58
|
+
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// Components
|
|
61
|
+
// ============================================================================
|
|
62
|
+
|
|
63
|
+
function ListItem({ item, isSelected }: { item: (typeof allItems)[0]; isSelected: boolean }) {
|
|
64
|
+
return (
|
|
65
|
+
<Box paddingX={1}>
|
|
66
|
+
<Text
|
|
67
|
+
color={isSelected ? "$bg" : undefined}
|
|
68
|
+
backgroundColor={isSelected ? "$primary" : undefined}
|
|
69
|
+
bold={isSelected}
|
|
70
|
+
>
|
|
71
|
+
{isSelected ? " > " : " "}
|
|
72
|
+
{item.value}
|
|
73
|
+
</Text>
|
|
74
|
+
<Small> ({item.category})</Small>
|
|
75
|
+
</Box>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function StatusBar({ lastCopied, lastPasted }: { lastCopied: string | null; lastPasted: string | null }) {
|
|
80
|
+
return (
|
|
81
|
+
<Box flexDirection="column" borderStyle="round" borderColor="$border" paddingX={1}>
|
|
82
|
+
<Box gap={1}>
|
|
83
|
+
<Muted>Copied:</Muted>
|
|
84
|
+
{lastCopied ? <Text color="$success">{lastCopied}</Text> : <Lead>nothing yet</Lead>}
|
|
85
|
+
</Box>
|
|
86
|
+
<Box gap={1}>
|
|
87
|
+
<Muted>Pasted:</Muted>
|
|
88
|
+
{lastPasted ? <Text color="$warning">{lastPasted}</Text> : <Lead>nothing yet</Lead>}
|
|
89
|
+
</Box>
|
|
90
|
+
</Box>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function ClipboardDemo() {
|
|
95
|
+
const { exit } = useApp()
|
|
96
|
+
const { stdout } = useStdout()
|
|
97
|
+
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
98
|
+
const [lastCopied, setLastCopied] = useState<string | null>(null)
|
|
99
|
+
const [lastPasted, setLastPasted] = useState<string | null>(null)
|
|
100
|
+
|
|
101
|
+
useInput((input: string, key: Key) => {
|
|
102
|
+
if (input === "q" || key.escape) {
|
|
103
|
+
exit()
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Navigation
|
|
108
|
+
if (key.upArrow || input === "k") {
|
|
109
|
+
setSelectedIndex((prev) => Math.max(0, prev - 1))
|
|
110
|
+
}
|
|
111
|
+
if (key.downArrow || input === "j") {
|
|
112
|
+
setSelectedIndex((prev) => Math.min(allItems.length - 1, prev + 1))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Copy selected item
|
|
116
|
+
if (input === "c") {
|
|
117
|
+
const text = allItems[selectedIndex]!.value
|
|
118
|
+
copyToClipboard(stdout, text)
|
|
119
|
+
setLastCopied(text)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Request clipboard
|
|
123
|
+
if (input === "v") {
|
|
124
|
+
requestClipboard(stdout)
|
|
125
|
+
// Note: The terminal responds with an OSC 52 sequence containing
|
|
126
|
+
// the clipboard contents. In a real app you'd parse stdin for the
|
|
127
|
+
// response using parseClipboardResponse(). For this demo we just
|
|
128
|
+
// show that the request was sent.
|
|
129
|
+
setLastPasted("(request sent — check terminal)")
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Try to parse clipboard response from raw input
|
|
133
|
+
const parsed = parseClipboardResponse(input)
|
|
134
|
+
if (parsed) {
|
|
135
|
+
setLastPasted(parsed)
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<Box flexDirection="column" padding={1} gap={1}>
|
|
141
|
+
<Box flexDirection="column" borderStyle="round" borderColor="$primary" paddingX={1}>
|
|
142
|
+
<Box marginBottom={1}>
|
|
143
|
+
<H1>Items</H1>
|
|
144
|
+
<Small>
|
|
145
|
+
{" "}
|
|
146
|
+
— {selectedIndex + 1}/{allItems.length}
|
|
147
|
+
</Small>
|
|
148
|
+
</Box>
|
|
149
|
+
<Box flexDirection="column" overflow="scroll" scrollTo={selectedIndex} height={10}>
|
|
150
|
+
{allItems.map((item, index) => (
|
|
151
|
+
<ListItem key={`${item.category}-${item.value}`} item={item} isSelected={index === selectedIndex} />
|
|
152
|
+
))}
|
|
153
|
+
</Box>
|
|
154
|
+
</Box>
|
|
155
|
+
|
|
156
|
+
<StatusBar lastCopied={lastCopied} lastPasted={lastPasted} />
|
|
157
|
+
|
|
158
|
+
<Muted>
|
|
159
|
+
{" "}
|
|
160
|
+
<Kbd>j/k</Kbd> navigate <Kbd>c</Kbd> copy <Kbd>v</Kbd> paste <Kbd>Esc/q</Kbd> quit
|
|
161
|
+
</Muted>
|
|
162
|
+
</Box>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ============================================================================
|
|
167
|
+
// Main
|
|
168
|
+
// ============================================================================
|
|
169
|
+
|
|
170
|
+
async function main() {
|
|
171
|
+
using term = createTerm()
|
|
172
|
+
const { waitUntilExit } = await render(
|
|
173
|
+
<ExampleBanner meta={meta} controls="j/k navigate c copy v paste Esc/q quit">
|
|
174
|
+
<ClipboardDemo />
|
|
175
|
+
</ExampleBanner>,
|
|
176
|
+
term,
|
|
177
|
+
)
|
|
178
|
+
await waitUntilExit()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (import.meta.main) {
|
|
182
|
+
main().catch(console.error)
|
|
183
|
+
}
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Components Showcase
|
|
3
|
+
*
|
|
4
|
+
* A UI component gallery demonstrating silvery's built-in components:
|
|
5
|
+
* - Typography: H1-H3, Strong, Muted, Small, Lead, Code, Blockquote, lists
|
|
6
|
+
* - Inputs: TextInput, TextArea, SelectList, Toggle with focus cycling
|
|
7
|
+
* - Display: ProgressBar, Spinner, Badge, border styles, ModalDialog
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React, { useState, useCallback } from "react"
|
|
11
|
+
import {
|
|
12
|
+
render,
|
|
13
|
+
Box,
|
|
14
|
+
Text,
|
|
15
|
+
Muted,
|
|
16
|
+
useInput,
|
|
17
|
+
useApp,
|
|
18
|
+
createTerm,
|
|
19
|
+
// Typography
|
|
20
|
+
H1,
|
|
21
|
+
H2,
|
|
22
|
+
H3,
|
|
23
|
+
P,
|
|
24
|
+
Lead,
|
|
25
|
+
Small,
|
|
26
|
+
Strong,
|
|
27
|
+
Em,
|
|
28
|
+
Code,
|
|
29
|
+
Blockquote,
|
|
30
|
+
CodeBlock,
|
|
31
|
+
HR,
|
|
32
|
+
UL,
|
|
33
|
+
OL,
|
|
34
|
+
LI,
|
|
35
|
+
// Inputs
|
|
36
|
+
TextInput,
|
|
37
|
+
TextArea,
|
|
38
|
+
SelectList,
|
|
39
|
+
Toggle,
|
|
40
|
+
Button,
|
|
41
|
+
// Display
|
|
42
|
+
ProgressBar,
|
|
43
|
+
Spinner,
|
|
44
|
+
Badge,
|
|
45
|
+
Divider,
|
|
46
|
+
ModalDialog,
|
|
47
|
+
// Tabs
|
|
48
|
+
Tabs,
|
|
49
|
+
TabList,
|
|
50
|
+
Tab,
|
|
51
|
+
TabPanel,
|
|
52
|
+
type Key,
|
|
53
|
+
} from "../../src/index.js"
|
|
54
|
+
import { ExampleBanner, type ExampleMeta } from "../_banner.js"
|
|
55
|
+
|
|
56
|
+
export const meta: ExampleMeta = {
|
|
57
|
+
name: "Components",
|
|
58
|
+
description: "UI component gallery with typography, inputs, and dialogs",
|
|
59
|
+
demo: true,
|
|
60
|
+
features: ["Typography", "TextInput", "SelectList", "ModalDialog", "ProgressBar", "focus ring"],
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// Typography Tab
|
|
65
|
+
// ============================================================================
|
|
66
|
+
|
|
67
|
+
function TypographyTab() {
|
|
68
|
+
return (
|
|
69
|
+
<Box flexDirection="column" gap={1} paddingX={1} overflow="scroll" flexGrow={1}>
|
|
70
|
+
<H1>Getting Started with Silvery</H1>
|
|
71
|
+
<Lead>Build modern terminal UIs with React — layout feedback, semantic theming, and 30+ components.</Lead>
|
|
72
|
+
|
|
73
|
+
<HR />
|
|
74
|
+
|
|
75
|
+
<H2>Installation</H2>
|
|
76
|
+
<P>
|
|
77
|
+
Install silvery and its peer dependencies. The framework uses <Strong>React 19</Strong> with a custom reconciler
|
|
78
|
+
— no DOM required.
|
|
79
|
+
</P>
|
|
80
|
+
<CodeBlock>{"bun add silvery"}</CodeBlock>
|
|
81
|
+
|
|
82
|
+
<H2>Core Concepts</H2>
|
|
83
|
+
<P>
|
|
84
|
+
Silvery follows <Em>The Silvery Way</Em> — 10 principles that keep your TUI apps shiny. Here are the most
|
|
85
|
+
important ones:
|
|
86
|
+
</P>
|
|
87
|
+
|
|
88
|
+
<H3>Use Built-in Components</H3>
|
|
89
|
+
<P>
|
|
90
|
+
<Code>silvery/ui</Code> ships 30+ components. They handle keyboard navigation, theming,
|
|
91
|
+
mouse support, and dozens of edge cases.
|
|
92
|
+
</P>
|
|
93
|
+
<UL>
|
|
94
|
+
<LI>
|
|
95
|
+
<Strong>SelectList</Strong> — keyboard-navigable single-select with j/k, wrapping, and scroll
|
|
96
|
+
</LI>
|
|
97
|
+
<LI>
|
|
98
|
+
<Strong>TextInput</Strong> — full readline: Ctrl+A/E/K/U, Alt+B/F, kill ring, clipboard
|
|
99
|
+
</LI>
|
|
100
|
+
<LI>
|
|
101
|
+
<Strong>ModalDialog</Strong> — double-border dialog with title, footer, and input blocking
|
|
102
|
+
</LI>
|
|
103
|
+
<LI>
|
|
104
|
+
<Strong>ProgressBar</Strong> — determinate and indeterminate modes with auto-width
|
|
105
|
+
</LI>
|
|
106
|
+
</UL>
|
|
107
|
+
|
|
108
|
+
<H3>Semantic Theme Colors</H3>
|
|
109
|
+
<P>
|
|
110
|
+
Use <Code>$tokens</Code> instead of hardcoded colors. Your app adapts to 38 built-in palettes automatically:
|
|
111
|
+
</P>
|
|
112
|
+
<OL>
|
|
113
|
+
<LI>
|
|
114
|
+
<Text color="$primary">$primary</Text> — brand emphasis, active elements
|
|
115
|
+
</LI>
|
|
116
|
+
<LI>
|
|
117
|
+
<Text color="$accent">$accent</Text> — contrasting hue for attention
|
|
118
|
+
</LI>
|
|
119
|
+
<LI>
|
|
120
|
+
<Text color="$success">$success</Text> — completion, checkmarks
|
|
121
|
+
</LI>
|
|
122
|
+
<LI>
|
|
123
|
+
<Text color="$warning">$warning</Text> — caution signals
|
|
124
|
+
</LI>
|
|
125
|
+
<LI>
|
|
126
|
+
<Text color="$error">$error</Text> — failures, destructive actions
|
|
127
|
+
</LI>
|
|
128
|
+
</OL>
|
|
129
|
+
|
|
130
|
+
<Blockquote>
|
|
131
|
+
Less is more. The best color code is no color code — most components already use the right tokens.
|
|
132
|
+
</Blockquote>
|
|
133
|
+
|
|
134
|
+
<H3>Think in Flexbox</H3>
|
|
135
|
+
<P>
|
|
136
|
+
Silvery uses CSS flexbox via Flexily. Components know their size via <Code>useContentRect()</Code> —
|
|
137
|
+
synchronous, during render. No effects, no flash.
|
|
138
|
+
</P>
|
|
139
|
+
|
|
140
|
+
<Small>Last updated: silvery v0.0.1 — see silvery.dev for full documentation</Small>
|
|
141
|
+
</Box>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ============================================================================
|
|
146
|
+
// Inputs Tab
|
|
147
|
+
// ============================================================================
|
|
148
|
+
|
|
149
|
+
const frameworkItems = [
|
|
150
|
+
{ label: "Silvery", value: "silvery" },
|
|
151
|
+
{ label: "Ink", value: "ink" },
|
|
152
|
+
{ label: "Blessed", value: "blessed", disabled: true },
|
|
153
|
+
{ label: "Terminal Kit", value: "terminal-kit" },
|
|
154
|
+
{ label: "React Curse", value: "react-curse" },
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
function InputsTab() {
|
|
158
|
+
const [textValue, setTextValue] = useState("")
|
|
159
|
+
const [areaValue, setAreaValue] = useState("")
|
|
160
|
+
const [selectedFramework, setSelectedFramework] = useState(0)
|
|
161
|
+
const [darkMode, setDarkMode] = useState(true)
|
|
162
|
+
const [notifications, setNotifications] = useState(false)
|
|
163
|
+
const [autoSave, setAutoSave] = useState(true)
|
|
164
|
+
const [focusIndex, setFocusIndex] = useState(0)
|
|
165
|
+
|
|
166
|
+
const focusableCount = 5
|
|
167
|
+
|
|
168
|
+
useInput((_input: string, key: Key) => {
|
|
169
|
+
if (key.tab && !key.shift) {
|
|
170
|
+
setFocusIndex((prev) => (prev + 1) % focusableCount)
|
|
171
|
+
}
|
|
172
|
+
if (key.tab && key.shift) {
|
|
173
|
+
setFocusIndex((prev) => (prev - 1 + focusableCount) % focusableCount)
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const resetAll = useCallback(() => {
|
|
178
|
+
setTextValue("")
|
|
179
|
+
setAreaValue("")
|
|
180
|
+
setSelectedFramework(0)
|
|
181
|
+
setDarkMode(true)
|
|
182
|
+
setNotifications(false)
|
|
183
|
+
setAutoSave(true)
|
|
184
|
+
}, [])
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<Box flexDirection="column" gap={1} paddingX={1} overflow="scroll" flexGrow={1}>
|
|
188
|
+
<Box flexDirection="row" gap={2} flexGrow={1}>
|
|
189
|
+
{/* Left column: Input controls */}
|
|
190
|
+
<Box flexDirection="column" gap={1} flexGrow={1} flexBasis={0}>
|
|
191
|
+
<H2>Text Input</H2>
|
|
192
|
+
<TextInput
|
|
193
|
+
value={textValue}
|
|
194
|
+
onChange={setTextValue}
|
|
195
|
+
onSubmit={() => setTextValue("")}
|
|
196
|
+
placeholder="Type something..."
|
|
197
|
+
prompt="search: "
|
|
198
|
+
borderStyle="round"
|
|
199
|
+
isActive={focusIndex === 0}
|
|
200
|
+
/>
|
|
201
|
+
|
|
202
|
+
<H2>Text Area</H2>
|
|
203
|
+
<TextArea
|
|
204
|
+
value={areaValue}
|
|
205
|
+
onChange={setAreaValue}
|
|
206
|
+
placeholder="Write your thoughts..."
|
|
207
|
+
height={4}
|
|
208
|
+
borderStyle="round"
|
|
209
|
+
isActive={focusIndex === 1}
|
|
210
|
+
/>
|
|
211
|
+
|
|
212
|
+
<H2>Select List</H2>
|
|
213
|
+
<Box borderStyle="round" borderColor={focusIndex === 2 ? "$focusborder" : "$border"} paddingX={1}>
|
|
214
|
+
<SelectList
|
|
215
|
+
items={frameworkItems}
|
|
216
|
+
highlightedIndex={selectedFramework}
|
|
217
|
+
onHighlight={setSelectedFramework}
|
|
218
|
+
isActive={focusIndex === 2}
|
|
219
|
+
/>
|
|
220
|
+
</Box>
|
|
221
|
+
</Box>
|
|
222
|
+
|
|
223
|
+
{/* Right column: Toggles + Summary */}
|
|
224
|
+
<Box flexDirection="column" gap={1} flexGrow={1} flexBasis={0}>
|
|
225
|
+
<H2>Toggles</H2>
|
|
226
|
+
<Box
|
|
227
|
+
flexDirection="column"
|
|
228
|
+
borderStyle="round"
|
|
229
|
+
borderColor={focusIndex === 3 ? "$focusborder" : "$border"}
|
|
230
|
+
paddingX={1}
|
|
231
|
+
paddingY={1}
|
|
232
|
+
gap={1}
|
|
233
|
+
>
|
|
234
|
+
<Toggle value={darkMode} onChange={setDarkMode} label="Dark mode" isActive={focusIndex === 3} />
|
|
235
|
+
<Toggle value={notifications} onChange={setNotifications} label="Notifications" isActive={false} />
|
|
236
|
+
<Toggle value={autoSave} onChange={setAutoSave} label="Auto-save" isActive={false} />
|
|
237
|
+
</Box>
|
|
238
|
+
|
|
239
|
+
<H2>Button</H2>
|
|
240
|
+
<Button label="Reset All" onPress={resetAll} isActive={focusIndex === 4} />
|
|
241
|
+
|
|
242
|
+
<HR />
|
|
243
|
+
|
|
244
|
+
<H2>Current Values</H2>
|
|
245
|
+
<Box flexDirection="column" backgroundColor="$surfacebg" paddingX={1} paddingY={1} borderStyle="round">
|
|
246
|
+
<Text color="$surface">
|
|
247
|
+
<Strong>Text:</Strong> {textValue || <Muted>(empty)</Muted>}
|
|
248
|
+
</Text>
|
|
249
|
+
<Text color="$surface">
|
|
250
|
+
<Strong>Area:</Strong>{" "}
|
|
251
|
+
{areaValue ? areaValue.split("\n")[0] + (areaValue.includes("\n") ? "..." : "") : <Muted>(empty)</Muted>}
|
|
252
|
+
</Text>
|
|
253
|
+
<Text color="$surface">
|
|
254
|
+
<Strong>Framework:</Strong> {frameworkItems[selectedFramework]?.label}
|
|
255
|
+
</Text>
|
|
256
|
+
<Text color="$surface">
|
|
257
|
+
<Strong>Dark mode:</Strong> {darkMode ? "on" : "off"}
|
|
258
|
+
</Text>
|
|
259
|
+
<Text color="$surface">
|
|
260
|
+
<Strong>Notifications:</Strong> {notifications ? "on" : "off"}
|
|
261
|
+
</Text>
|
|
262
|
+
<Text color="$surface">
|
|
263
|
+
<Strong>Auto-save:</Strong> {autoSave ? "on" : "off"}
|
|
264
|
+
</Text>
|
|
265
|
+
</Box>
|
|
266
|
+
</Box>
|
|
267
|
+
</Box>
|
|
268
|
+
|
|
269
|
+
<Small>Tab/Shift+Tab to cycle focus — Space toggles — Enter submits</Small>
|
|
270
|
+
</Box>
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ============================================================================
|
|
275
|
+
// Display Tab
|
|
276
|
+
// ============================================================================
|
|
277
|
+
|
|
278
|
+
function DisplayTab() {
|
|
279
|
+
const [showModal, setShowModal] = useState(false)
|
|
280
|
+
const [selectedBorder, setSelectedBorder] = useState(0)
|
|
281
|
+
|
|
282
|
+
const borderStyles = ["round", "bold", "single", "double", "classic"] as const
|
|
283
|
+
|
|
284
|
+
useInput((input: string, key: Key) => {
|
|
285
|
+
if (key.return && !showModal) {
|
|
286
|
+
setShowModal(true)
|
|
287
|
+
}
|
|
288
|
+
if ((key.escape || input === "q") && showModal) {
|
|
289
|
+
setShowModal(false)
|
|
290
|
+
}
|
|
291
|
+
if (input === "j" && !showModal) {
|
|
292
|
+
setSelectedBorder((prev) => Math.min(prev + 1, borderStyles.length - 1))
|
|
293
|
+
}
|
|
294
|
+
if (input === "k" && !showModal) {
|
|
295
|
+
setSelectedBorder((prev) => Math.max(prev - 1, 0))
|
|
296
|
+
}
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<Box flexDirection="column" gap={1} paddingX={1} overflow="scroll" flexGrow={1}>
|
|
301
|
+
<Box flexDirection="row" gap={2} flexGrow={1}>
|
|
302
|
+
{/* Left column */}
|
|
303
|
+
<Box flexDirection="column" gap={1} flexGrow={1} flexBasis={0}>
|
|
304
|
+
<H2>Progress Bars</H2>
|
|
305
|
+
<Box flexDirection="column" gap={1}>
|
|
306
|
+
<Box>
|
|
307
|
+
<Text color="$muted">{"Build "}</Text>
|
|
308
|
+
<Box flexGrow={1}>
|
|
309
|
+
<ProgressBar value={1.0} label="✓" />
|
|
310
|
+
</Box>
|
|
311
|
+
</Box>
|
|
312
|
+
<Box>
|
|
313
|
+
<Text color="$muted">{"Test "}</Text>
|
|
314
|
+
<Box flexGrow={1}>
|
|
315
|
+
<ProgressBar value={0.73} />
|
|
316
|
+
</Box>
|
|
317
|
+
</Box>
|
|
318
|
+
<Box>
|
|
319
|
+
<Text color="$muted">{"Deploy "}</Text>
|
|
320
|
+
<Box flexGrow={1}>
|
|
321
|
+
<ProgressBar value={0.35} />
|
|
322
|
+
</Box>
|
|
323
|
+
</Box>
|
|
324
|
+
<Box>
|
|
325
|
+
<Text color="$muted">{"Install "}</Text>
|
|
326
|
+
<Box flexGrow={1}>
|
|
327
|
+
<ProgressBar />
|
|
328
|
+
</Box>
|
|
329
|
+
</Box>
|
|
330
|
+
</Box>
|
|
331
|
+
|
|
332
|
+
<H2>Spinners</H2>
|
|
333
|
+
<Box flexDirection="column">
|
|
334
|
+
<Spinner type="dots" label="Loading packages..." />
|
|
335
|
+
<Spinner type="line" label="Compiling..." />
|
|
336
|
+
<Spinner type="arc" label="Optimizing bundle..." />
|
|
337
|
+
<Spinner type="bounce" label="Connecting..." />
|
|
338
|
+
</Box>
|
|
339
|
+
|
|
340
|
+
<H2>Badges</H2>
|
|
341
|
+
<Box gap={1} flexWrap="wrap">
|
|
342
|
+
<Badge label="Stable" variant="success" />
|
|
343
|
+
<Badge label="Beta" variant="warning" />
|
|
344
|
+
<Badge label="Deprecated" variant="error" />
|
|
345
|
+
<Badge label="v0.0.1" variant="primary" />
|
|
346
|
+
<Badge label="MIT" />
|
|
347
|
+
</Box>
|
|
348
|
+
</Box>
|
|
349
|
+
|
|
350
|
+
{/* Right column */}
|
|
351
|
+
<Box flexDirection="column" gap={1} flexGrow={1} flexBasis={0}>
|
|
352
|
+
<H2>Border Styles</H2>
|
|
353
|
+
<Box flexDirection="column" gap={1}>
|
|
354
|
+
{borderStyles.map((style, i) => (
|
|
355
|
+
<Box
|
|
356
|
+
key={style}
|
|
357
|
+
borderStyle={style as any}
|
|
358
|
+
borderColor={i === selectedBorder ? "$primary" : "$border"}
|
|
359
|
+
paddingX={1}
|
|
360
|
+
>
|
|
361
|
+
<Text bold={i === selectedBorder}>
|
|
362
|
+
{i === selectedBorder ? "▸ " : " "}
|
|
363
|
+
{style}
|
|
364
|
+
</Text>
|
|
365
|
+
</Box>
|
|
366
|
+
))}
|
|
367
|
+
</Box>
|
|
368
|
+
|
|
369
|
+
<Divider title="Status" />
|
|
370
|
+
|
|
371
|
+
<Box flexDirection="column">
|
|
372
|
+
<Text color="$success">✓ All checks passed</Text>
|
|
373
|
+
<Text color="$warning">⚠ 2 deprecation warnings</Text>
|
|
374
|
+
<Text color="$error">✗ 1 vulnerability found</Text>
|
|
375
|
+
<Text color="$info">ℹ 47 packages installed</Text>
|
|
376
|
+
</Box>
|
|
377
|
+
|
|
378
|
+
<Small>j/k select border — Enter opens modal — q quits</Small>
|
|
379
|
+
</Box>
|
|
380
|
+
</Box>
|
|
381
|
+
|
|
382
|
+
{showModal && (
|
|
383
|
+
<Box position="absolute" display="flex" justifyContent="center" alignItems="center" width="100%" height="100%">
|
|
384
|
+
<ModalDialog title="Component Gallery" width={50} footer="ESC or q to close">
|
|
385
|
+
<Box flexDirection="column" gap={1}>
|
|
386
|
+
<P>
|
|
387
|
+
This gallery demonstrates <Strong>silvery</Strong>'s built-in UI components. Every component uses
|
|
388
|
+
semantic theme tokens — they adapt to any of the 38 built-in palettes automatically.
|
|
389
|
+
</P>
|
|
390
|
+
<HR />
|
|
391
|
+
<Box flexDirection="column">
|
|
392
|
+
<Text color="$success">✓ Typography presets (H1-H3, Lead, Muted, Code)</Text>
|
|
393
|
+
<Text color="$success">✓ Input components (TextInput, TextArea, SelectList)</Text>
|
|
394
|
+
<Text color="$success">✓ Display widgets (ProgressBar, Spinner, Badge)</Text>
|
|
395
|
+
<Text color="$success">✓ Layout primitives (Box, Divider, border styles)</Text>
|
|
396
|
+
<Text color="$success">✓ Dialog system (ModalDialog with input blocking)</Text>
|
|
397
|
+
</Box>
|
|
398
|
+
</Box>
|
|
399
|
+
</ModalDialog>
|
|
400
|
+
</Box>
|
|
401
|
+
)}
|
|
402
|
+
</Box>
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ============================================================================
|
|
407
|
+
// App
|
|
408
|
+
// ============================================================================
|
|
409
|
+
|
|
410
|
+
export function ComponentsApp() {
|
|
411
|
+
const { exit } = useApp()
|
|
412
|
+
const [activeTab, setActiveTab] = useState("typography")
|
|
413
|
+
|
|
414
|
+
useInput((input: string, key: Key) => {
|
|
415
|
+
// Only quit with q when not on the inputs tab (where user may be typing)
|
|
416
|
+
if (input === "q" && activeTab !== "inputs") {
|
|
417
|
+
exit()
|
|
418
|
+
}
|
|
419
|
+
if (key.escape && activeTab !== "display") {
|
|
420
|
+
exit()
|
|
421
|
+
}
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
return (
|
|
425
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
426
|
+
<Tabs defaultValue="typography" onChange={setActiveTab}>
|
|
427
|
+
<TabList>
|
|
428
|
+
<Tab value="typography">Typography</Tab>
|
|
429
|
+
<Tab value="inputs">Inputs</Tab>
|
|
430
|
+
<Tab value="display">Display</Tab>
|
|
431
|
+
</TabList>
|
|
432
|
+
<TabPanel value="typography">
|
|
433
|
+
<TypographyTab />
|
|
434
|
+
</TabPanel>
|
|
435
|
+
<TabPanel value="inputs">
|
|
436
|
+
<InputsTab />
|
|
437
|
+
</TabPanel>
|
|
438
|
+
<TabPanel value="display">
|
|
439
|
+
<DisplayTab />
|
|
440
|
+
</TabPanel>
|
|
441
|
+
</Tabs>
|
|
442
|
+
</Box>
|
|
443
|
+
)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ============================================================================
|
|
447
|
+
// Main
|
|
448
|
+
// ============================================================================
|
|
449
|
+
|
|
450
|
+
export async function main() {
|
|
451
|
+
using term = createTerm()
|
|
452
|
+
const { waitUntilExit } = await render(
|
|
453
|
+
<ExampleBanner meta={meta} controls="h/l tab Tab cycle inputs j/k navigate Enter modal Esc/q quit">
|
|
454
|
+
<ComponentsApp />
|
|
455
|
+
</ExampleBanner>,
|
|
456
|
+
term,
|
|
457
|
+
)
|
|
458
|
+
await waitUntilExit()
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (import.meta.main) {
|
|
462
|
+
main().catch(console.error)
|
|
463
|
+
}
|