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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "silvery",
3
- "version": "0.3.0",
4
- "description": "React terminal UI renderer for complex interactive apps layout-aware rendering, flexbox, scrolling, and incremental updates",
3
+ "version": "0.4.1",
4
+ "description": "React terminal UI renderer for complex interactive apps \u2014 layout-aware rendering, flexbox, scrolling, and incremental updates",
5
5
  "keywords": [
6
6
  "ansi",
7
7
  "chalk",
@@ -25,7 +25,7 @@
25
25
  "url": "https://github.com/beorn/silvery/issues"
26
26
  },
27
27
  "license": "MIT",
28
- "author": "Bjørn Stabell <bjorn@stabell.org>",
28
+ "author": "Bj\u00f8rn Stabell <bjorn@stabell.org>",
29
29
  "repository": {
30
30
  "type": "git",
31
31
  "url": "https://github.com/beorn/silvery.git"
@@ -38,8 +38,8 @@
38
38
  ],
39
39
  "files": [
40
40
  "src",
41
- "bin",
42
- "examples"
41
+ "dist",
42
+ "bin"
43
43
  ],
44
44
  "type": "module",
45
45
  "main": "src/index.ts",
@@ -49,20 +49,72 @@
49
49
  "types": "./src/index.ts",
50
50
  "import": "./src/index.ts"
51
51
  },
52
+ "./runtime": {
53
+ "types": "./src/runtime.ts",
54
+ "import": "./src/runtime.ts"
55
+ },
56
+ "./theme": {
57
+ "types": "./src/theme.ts",
58
+ "import": "./src/theme.ts"
59
+ },
60
+ "./ui": {
61
+ "types": "./src/ui.ts",
62
+ "import": "./src/ui.ts"
63
+ },
64
+ "./ui/cli": {
65
+ "types": "./src/ui/cli.ts",
66
+ "import": "./src/ui/cli.ts"
67
+ },
68
+ "./ui/react": {
69
+ "types": "./src/ui/react.ts",
70
+ "import": "./src/ui/react.ts"
71
+ },
72
+ "./ui/progress": {
73
+ "types": "./src/ui/progress.ts",
74
+ "import": "./src/ui/progress.ts"
75
+ },
76
+ "./ui/wrappers": {
77
+ "types": "./src/ui/wrappers.ts",
78
+ "import": "./src/ui/wrappers.ts"
79
+ },
80
+ "./ui/ansi": {
81
+ "types": "./src/ui/ansi.ts",
82
+ "import": "./src/ui/ansi.ts"
83
+ },
84
+ "./ui/display": {
85
+ "types": "./src/ui/display.ts",
86
+ "import": "./src/ui/display.ts"
87
+ },
88
+ "./ui/input": {
89
+ "types": "./src/ui/input.ts",
90
+ "import": "./src/ui/input.ts"
91
+ },
92
+ "./ui/animation": {
93
+ "types": "./src/ui/animation.ts",
94
+ "import": "./src/ui/animation.ts"
95
+ },
96
+ "./ui/image": {
97
+ "types": "./src/ui/image.ts",
98
+ "import": "./src/ui/image.ts"
99
+ },
100
+ "./ui/utils": {
101
+ "types": "./src/ui/utils.ts",
102
+ "import": "./src/ui/utils.ts"
103
+ },
52
104
  "./ink": {
53
- "types": "./packages/compat/src/ink.ts",
54
- "import": "./packages/compat/src/ink.ts"
105
+ "types": "./packages/ink/src/ink.ts",
106
+ "import": "./packages/ink/src/ink.ts"
55
107
  },
56
108
  "./chalk": {
57
- "types": "./packages/compat/src/chalk.ts",
58
- "import": "./packages/compat/src/chalk.ts"
109
+ "types": "./packages/ink/src/chalk.ts",
110
+ "import": "./packages/ink/src/chalk.ts"
59
111
  }
60
112
  },
61
113
  "publishConfig": {
62
114
  "access": "public"
63
115
  },
64
116
  "scripts": {
65
- "build": "bun build src/index.ts --outdir dist --target node",
117
+ "build": "bun run scripts/build.ts",
66
118
  "test": "bunx --bun vitest run",
67
119
  "test:fast": "bunx --bun vitest run --reporter=dot",
68
120
  "typecheck": "tsc --noEmit",
@@ -76,9 +128,9 @@
76
128
  "release": "changeset publish",
77
129
  "theme": "bun packages/theme/src/cli.ts",
78
130
  "demo": "bun examples/cli.ts",
79
- "compat": "bun packages/compat/scripts/compat-check.ts",
80
- "compat:ink": "bun packages/compat/scripts/compat-check.ts ink",
81
- "compat:chalk": "bun packages/compat/scripts/compat-check.ts chalk"
131
+ "compat": "bun packages/ink/scripts/compat-check.ts",
132
+ "compat:ink": "bun packages/ink/scripts/compat-check.ts ink",
133
+ "compat:chalk": "bun packages/ink/scripts/compat-check.ts chalk"
82
134
  },
83
135
  "dependencies": {
84
136
  "chalk": "^5.6.2",
@@ -101,7 +153,7 @@
101
153
  "playwright": "^1.58.2",
102
154
  "react": "^19.0.0",
103
155
  "typescript": "^5.5.0",
104
- "vimonkey": "github:beorn/vimonkey",
156
+ "vimonkey": "^0.2.0",
105
157
  "vitepress": "^1.5.0",
106
158
  "vitepress-plugin-llms": "^1.0.0",
107
159
  "vitest": "^4.0.18",
@@ -114,4 +166,4 @@
114
166
  "bun": ">=1.0",
115
167
  "node": ">=18"
116
168
  }
117
- }
169
+ }
package/src/index.ts CHANGED
@@ -3,4 +3,70 @@
3
3
 
4
4
  export const VERSION = "0.0.1"
5
5
 
6
- export * from "@silvery/react"
6
+ // Re-export everything from @silvery/ag-react — local `render` below shadows the re-exported one
7
+ export * from "@silvery/ag-react"
8
+
9
+ import type { ReactElement } from "react"
10
+ import { render as reactRender, type RenderOptions, type TermDef } from "@silvery/ag-react"
11
+ import type { Term } from "@silvery/ag-react"
12
+
13
+ /**
14
+ * Render a React element to the terminal.
15
+ *
16
+ * Zero-ceremony entry point — auto-detects the terminal and starts an
17
+ * interactive app when stdin is a TTY. No need to create a Term first.
18
+ *
19
+ * @example Hello World (2 lines)
20
+ * ```tsx
21
+ * import { render, Text } from "silvery"
22
+ * await render(<Text>Hello!</Text>).run()
23
+ * ```
24
+ *
25
+ * @example Interactive counter
26
+ * ```tsx
27
+ * import { useState } from "react"
28
+ * import { render, Box, Text, useInput } from "silvery"
29
+ *
30
+ * function Counter() {
31
+ * const [count, setCount] = useState(0)
32
+ * useInput((input) => {
33
+ * if (input === "j") setCount((c) => c + 1)
34
+ * })
35
+ * return (
36
+ * <Box borderStyle="round" padding={1}>
37
+ * <Text>Count: {count}</Text>
38
+ * </Box>
39
+ * )
40
+ * }
41
+ *
42
+ * await render(<Counter />).run()
43
+ * ```
44
+ *
45
+ * @example Static render (explicit)
46
+ * ```tsx
47
+ * import { render, Text } from "silvery"
48
+ * await render(<Text>Report</Text>, { width: 120 })
49
+ * ```
50
+ *
51
+ * When called without a Term or TermDef:
52
+ * - **TTY detected** → interactive mode (stdin + stdout auto-wired)
53
+ * - **No TTY** → static mode (renders once and returns)
54
+ *
55
+ * Pass a Term or TermDef explicitly to override auto-detection.
56
+ */
57
+ export function render(
58
+ element: ReactElement,
59
+ termOrDef?: Term | TermDef,
60
+ options?: RenderOptions,
61
+ ): ReturnType<typeof reactRender> {
62
+ // When no term/def is provided and we're in a TTY, auto-wire stdin/stdout
63
+ // so the app runs interactively (useInput works, app stays alive until exit).
64
+ if (!termOrDef && process.stdin?.isTTY && process.stdout?.isTTY) {
65
+ const ttyDef: TermDef = {
66
+ stdin: process.stdin,
67
+ stdout: process.stdout,
68
+ }
69
+ return reactRender(element, ttyDef, options)
70
+ }
71
+ return reactRender(element, termOrDef, options)
72
+ }
package/src/runtime.ts ADDED
@@ -0,0 +1,4 @@
1
+ // silvery/runtime — re-exports from @silvery/ag-term/runtime
2
+ // run(), useInput, useExit, createRuntime, and related runtime APIs
3
+
4
+ export * from "@silvery/ag-term/runtime"
package/src/theme.ts ADDED
@@ -0,0 +1,4 @@
1
+ // silvery/theme — re-exports from @silvery/theme
2
+ // Theme tokens, palettes, ThemeProvider, useTheme, color utilities
3
+
4
+ export * from "@silvery/theme"
@@ -0,0 +1,2 @@
1
+ // silvery/ui/animation — animation utilities and hooks
2
+ export * from "@silvery/ag-react/ui/animation"
package/src/ui/ansi.ts ADDED
@@ -0,0 +1,2 @@
1
+ // silvery/ui/ansi — ANSI rendering utilities
2
+ export * from "@silvery/ag-react/ui/ansi"
package/src/ui/cli.ts ADDED
@@ -0,0 +1,2 @@
1
+ // silvery/ui/cli — CLI progress indicators (no React)
2
+ export * from "@silvery/ag-react/ui/cli"
@@ -0,0 +1,2 @@
1
+ // silvery/ui/display — display components
2
+ export * from "@silvery/ag-react/ui/display"
@@ -0,0 +1,2 @@
1
+ // silvery/ui/image — image display (kitty, sixel)
2
+ export * from "@silvery/ag-react/ui/image"
@@ -0,0 +1,2 @@
1
+ // silvery/ui/input — input components
2
+ export * from "@silvery/ag-react/ui/input"
@@ -0,0 +1,2 @@
1
+ // silvery/ui/progress — task/progress wrappers
2
+ export * from "@silvery/ag-react/ui/progress"
@@ -0,0 +1,2 @@
1
+ // silvery/ui/react — React progress components
2
+ export * from "@silvery/ag-react/ui/react"
@@ -0,0 +1,2 @@
1
+ // silvery/ui/utils — UI utilities
2
+ export * from "@silvery/ag-react/ui/utils"
@@ -0,0 +1,2 @@
1
+ // silvery/ui/wrappers — task wrappers (fluent task API)
2
+ export * from "@silvery/ag-react/ui/wrappers"
package/src/ui.ts ADDED
@@ -0,0 +1,4 @@
1
+ // silvery/ui — re-exports from @silvery/ag-react/ui
2
+ // Component library (progress, CLI, wrappers)
3
+
4
+ export * from "@silvery/ag-react/ui"
@@ -1,75 +0,0 @@
1
- # Silvery Examples & Showcases
2
-
3
- ## Directory Structure
4
-
5
- | Directory | What |
6
- | -------------- | ---------------------------------------------------------- |
7
- | `interactive/` | Full apps — run with `bun examples/interactive/<name>.tsx` |
8
- | `inline/` | Inline mode examples (no alt screen) |
9
- | `kitty/` | Kitty protocol demos |
10
- | `layout/` | Layout engine examples |
11
- | `runtime/` | Runtime layer demos (run, createApp, createStore) |
12
- | `playground/` | Quick prototyping |
13
- | `web/` | Browser renderers (DOM, Canvas2D) |
14
- | `screenshots/` | Reference screenshots for visual regression |
15
-
16
- ## Making a Great Showcase
17
-
18
- ### Design Principles
19
-
20
- 1. **Show, don't tell.** A showcase should demonstrate Silvery features through working UI, not walls of text. Intro text is fine — but collapse it once the demo starts.
21
-
22
- 2. **Auto-size to content.** `ScrollbackView`/`ScrollbackList` auto-size to their content — no manual height management. The output phase caps output at terminal height independently. Content that exceeds terminal height causes natural terminal scrolling.
23
-
24
- 3. **Single status bar.** Keep the status bar to one line. Include: context bar, elapsed time, cost, and key hints. Remove anything that doesn't help the user interact.
25
-
26
- 4. **Conditional headers.** Show feature bullets before the demo starts (when there's space). Collapse to a one-liner once content fills the screen.
27
-
28
- 5. **Respect terminal width.** Boxes with borders at 120 cols should leave room for the border characters. Test at 80 and 120 cols.
29
-
30
- 6. **Streaming feels real.** For coding agent demos: thinking spinner (1-2s) → word-by-word text reveal → tool call spinner → output. Use `setInterval` at 50ms with 8-12% fraction increments.
31
-
32
- ### Scrollback Pattern
33
-
34
- Use `ScrollbackList` (or `ScrollbackView`) — they handle terminal height, footer pinning, and overflow automatically:
35
-
36
- ```tsx
37
- function App() {
38
- return (
39
- <ScrollbackList
40
- items={items}
41
- keyExtractor={(item) => item.id}
42
- isFrozen={(item) => item.done}
43
- markers={true}
44
- footer={<StatusBar />}
45
- >
46
- {(item) => <ItemView item={item} />}
47
- </ScrollbackList>
48
- )
49
- }
50
-
51
- await render(<App />, term, { mode: "inline" })
52
- ```
53
-
54
- `ScrollbackView` auto-sizes to its content — no manual height management. The output phase independently caps output at terminal height (via `inlineFullRender()`), so content that exceeds the terminal causes natural scrolling. The footer stays pinned at the bottom of the content.
55
-
56
- ### Theme Tokens
57
-
58
- Use semantic `$token` colors instead of hardcoded values:
59
-
60
- | Token | Use for |
61
- | ---------- | ------------------------------------- |
62
- | `$primary` | Active elements, progress bars, links |
63
- | `$success` | Completed items, checkmarks |
64
- | `$warning` | Caution, compaction |
65
- | `$error` | Failures, diff removals |
66
- | `$muted` | Secondary info, timestamps |
67
- | `$border` | Default border color |
68
-
69
- ### Testing Showcases
70
-
71
- 1. **Visual check**: Run in TTY and step through all states
72
- 2. **Resize**: Verify layout adapts to terminal resize
73
- 3. **Scrollback**: After frozen items, scroll up — verify colors/borders preserved
74
- 4. **Width**: Test at 80 and 120 columns
75
- 5. **Fast mode**: `--fast` flag should skip all animation for quick validation
@@ -1,60 +0,0 @@
1
- import React from "react"
2
- import { Box, Text, Strong, Muted, ThemeProvider, getThemeByName, type Theme } from "../src/index.js"
3
-
4
- export interface ExampleMeta {
5
- name: string
6
- description: string
7
- /** API features showcased, e.g. ["VirtualList", "useContentRect()"] */
8
- features?: string[]
9
- /** Curated demo — shown in CLI viewer (`bun examples`) and web showcase */
10
- demo?: boolean
11
- }
12
-
13
- interface Props {
14
- meta: ExampleMeta
15
- /** Short controls legend, e.g. "j/k navigate q quit" */
16
- controls?: string
17
- /** Override theme (from viewer). Falls back to SILVERY_THEME env var. */
18
- theme?: Theme
19
- children: React.ReactNode
20
- }
21
-
22
- /**
23
- * Compact header shown when examples run standalone.
24
- * Wraps children in ThemeProvider for consistent theming.
25
- */
26
- export function ExampleBanner({ meta, controls, theme, children }: Props) {
27
- const resolvedTheme = theme ?? getThemeByName(process.env.SILVERY_THEME)
28
-
29
- return (
30
- <ThemeProvider theme={resolvedTheme}>
31
- <Box flexDirection="column" flexGrow={1}>
32
- {/* One-line header: dimmed to not compete with example UI */}
33
- <Box paddingX={1} gap={1}>
34
- <Text dim color="$warning">
35
- {"▸ silvery"}
36
- </Text>
37
- <Strong>{meta.name}</Strong>
38
- <Muted>— {meta.description}</Muted>
39
- </Box>
40
- {meta.features && meta.features.length > 0 && (
41
- <Box paddingX={1}>
42
- <Muted>
43
- {" "}
44
- {meta.features.join(" · ")}
45
- </Muted>
46
- </Box>
47
- )}
48
- {controls && (
49
- <Box paddingX={1}>
50
- <Muted>
51
- {" "}
52
- {controls}
53
- </Muted>
54
- </Box>
55
- )}
56
- {children}
57
- </Box>
58
- </ThemeProvider>
59
- )
60
- }
package/examples/cli.ts DELETED
@@ -1,228 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * silvery demo CLI
4
- *
5
- * Lists and runs interactive demos from the examples/ directory.
6
- *
7
- * Usage:
8
- * bun demo — list all available demos grouped by category
9
- * bun demo <name> — run a specific demo by name (fuzzy match)
10
- * bun demo --list — same as no argument (list all)
11
- * bun demo --help — show usage help
12
- */
13
-
14
- import { resolve } from "node:path"
15
- import type { ExampleMeta } from "./_banner.js"
16
-
17
- // =============================================================================
18
- // Types
19
- // =============================================================================
20
-
21
- interface Example {
22
- name: string
23
- file: string
24
- description: string
25
- category: string
26
- features?: string[]
27
- }
28
-
29
- // =============================================================================
30
- // Auto-Discovery (matches viewer.tsx pattern)
31
- // =============================================================================
32
-
33
- const CATEGORY_DIRS = ["layout", "interactive", "runtime", "inline", "kitty"] as const
34
-
35
- const CATEGORY_DISPLAY: Record<string, string> = {
36
- kitty: "Kitty Protocol",
37
- }
38
-
39
- const CATEGORY_ORDER: Record<string, number> = {
40
- Layout: 0,
41
- Interactive: 1,
42
- Runtime: 2,
43
- Inline: 3,
44
- "Kitty Protocol": 4,
45
- }
46
-
47
- async function discoverExamples(): Promise<Example[]> {
48
- const baseDir = new URL(".", import.meta.url).pathname
49
- const results: Example[] = []
50
-
51
- for (const dir of CATEGORY_DIRS) {
52
- const category = CATEGORY_DISPLAY[dir] ?? dir.charAt(0).toUpperCase() + dir.slice(1)
53
- const dirPath = resolve(baseDir, dir)
54
- const files = [
55
- ...new Bun.Glob("*.tsx").scanSync({ cwd: dirPath }),
56
- ...new Bun.Glob("*/index.tsx").scanSync({ cwd: dirPath }),
57
- ]
58
-
59
- for (const file of files) {
60
- if (file.startsWith("_")) continue // skip internal files
61
-
62
- try {
63
- const mod = await import(resolve(dirPath, file))
64
- if (!mod.meta?.name) continue
65
-
66
- const meta: ExampleMeta = mod.meta
67
- results.push({
68
- name: meta.name,
69
- description: meta.description ?? "",
70
- file: resolve(dirPath, file),
71
- category,
72
- features: meta.features,
73
- })
74
- } catch {
75
- // Skip files that fail to import
76
- }
77
- }
78
- }
79
-
80
- results.sort((a, b) => {
81
- const catDiff = (CATEGORY_ORDER[a.category] ?? 99) - (CATEGORY_ORDER[b.category] ?? 99)
82
- if (catDiff !== 0) return catDiff
83
- return a.name.localeCompare(b.name)
84
- })
85
-
86
- return results
87
- }
88
-
89
- // =============================================================================
90
- // Formatting Helpers
91
- // =============================================================================
92
-
93
- // ANSI color codes for lightweight terminal output without importing silvery
94
- const RESET = "\x1b[0m"
95
- const BOLD = "\x1b[1m"
96
- const DIM = "\x1b[2m"
97
- const YELLOW = "\x1b[33m"
98
- const CYAN = "\x1b[36m"
99
- const GREEN = "\x1b[32m"
100
- const MAGENTA = "\x1b[35m"
101
- const BLUE = "\x1b[34m"
102
- const RED = "\x1b[31m"
103
- const WHITE = "\x1b[37m"
104
-
105
- const CATEGORY_COLOR_CODE: Record<string, string> = {
106
- Layout: MAGENTA,
107
- Interactive: CYAN,
108
- Runtime: GREEN,
109
- Inline: YELLOW,
110
- "Kitty Protocol": BLUE,
111
- }
112
-
113
- function printHelp(): void {
114
- console.log(`
115
- ${BOLD}${YELLOW}silvery demo${RESET} — browse and run interactive examples
116
-
117
- ${BOLD}Usage:${RESET}
118
- bun demo List all available demos
119
- bun demo ${DIM}<name>${RESET} Run a demo by name (case-insensitive, partial match)
120
- bun demo --list List all available demos
121
- bun demo --help Show this help
122
-
123
- ${BOLD}Examples:${RESET}
124
- bun demo dashboard Run the Dashboard demo
125
- bun demo kanban Run the Kanban Board demo
126
- bun demo scroll Run the first demo matching "scroll"
127
- `)
128
- }
129
-
130
- function printExampleList(examples: Example[]): void {
131
- console.log(`\n${BOLD}${YELLOW} silvery${RESET}${DIM} examples${RESET}\n`)
132
-
133
- let currentCategory = ""
134
-
135
- for (const ex of examples) {
136
- if (ex.category !== currentCategory) {
137
- currentCategory = ex.category
138
- const color = CATEGORY_COLOR_CODE[currentCategory] ?? WHITE
139
- console.log(` ${color}${BOLD}${currentCategory}${RESET}`)
140
- }
141
-
142
- const nameStr = `${BOLD}${WHITE}${ex.name}${RESET}`
143
- const descStr = `${DIM}${ex.description}${RESET}`
144
- console.log(` ${nameStr} ${descStr}`)
145
- }
146
-
147
- console.log(`\n ${DIM}Run a demo: bun demo <name>${RESET}\n`)
148
- }
149
-
150
- /** Find an example by name. Tries exact match first, then case-insensitive
151
- * prefix match, then case-insensitive substring match. */
152
- function findExample(examples: Example[], query: string): Example | undefined {
153
- const q = query.toLowerCase()
154
-
155
- // Exact match (case-insensitive)
156
- const exact = examples.find((ex) => ex.name.toLowerCase() === q)
157
- if (exact) return exact
158
-
159
- // Prefix match (case-insensitive)
160
- const prefix = examples.find((ex) => ex.name.toLowerCase().startsWith(q))
161
- if (prefix) return prefix
162
-
163
- // Substring match (case-insensitive)
164
- const substring = examples.find((ex) => ex.name.toLowerCase().includes(q))
165
- if (substring) return substring
166
-
167
- return undefined
168
- }
169
-
170
- function printNoMatch(query: string, examples: Example[]): void {
171
- console.error(`\n${RED}${BOLD}Error:${RESET} No demo matching "${query}"\n`)
172
- console.error(`${DIM}Available demos:${RESET}`)
173
-
174
- for (const ex of examples) {
175
- console.error(` ${WHITE}${ex.name}${RESET}`)
176
- }
177
-
178
- console.error(`\n${DIM}Run ${BOLD}bun demo${RESET}${DIM} for full list with descriptions.${RESET}\n`)
179
- }
180
-
181
- // =============================================================================
182
- // Main
183
- // =============================================================================
184
-
185
- async function main(): Promise<void> {
186
- const args = process.argv.slice(2)
187
-
188
- // Handle flags
189
- if (args.includes("--help") || args.includes("-h")) {
190
- printHelp()
191
- return
192
- }
193
-
194
- const examples = await discoverExamples()
195
-
196
- // No argument or --list: show the list
197
- if (args.length === 0 || args[0] === "--list") {
198
- printExampleList(examples)
199
- return
200
- }
201
-
202
- // Treat all non-flag arguments as the demo name query
203
- const query = args.filter((a) => !a.startsWith("--")).join(" ")
204
- if (!query) {
205
- printExampleList(examples)
206
- return
207
- }
208
-
209
- const match = findExample(examples, query)
210
- if (!match) {
211
- printNoMatch(query, examples)
212
- process.exit(1)
213
- }
214
-
215
- // Run the matched example
216
- console.log(`${DIM}Running ${BOLD}${match.name}${RESET}${DIM}...${RESET}\n`)
217
-
218
- const proc = Bun.spawn(["bun", "run", match.file], {
219
- stdio: ["inherit", "inherit", "inherit"],
220
- })
221
- const exitCode = await proc.exited
222
- process.exit(exitCode)
223
- }
224
-
225
- main().catch((err) => {
226
- console.error(err)
227
- process.exit(1)
228
- })