From 10011e1907631a029518a73eed586efd87e68612 Mon Sep 17 00:00:00 2001 From: Loki Date: Wed, 14 Oct 2026 15:47:29 +0800 Subject: [PATCH] feat(claude-code): plugin list and search box; terminal draws round corners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Carrel install guide skipped picking the plugin after the marketplace. It needs the /plugins > Discover screen. - claude-code gains SearchBox and PluginList (descriptions cut to 60 cells, as Claude Code does). - terminal draws ╭╮╰╯ in the whole cell, like ─ and │. Co-Authored-By: Claude Opus 5.5 --- DESIGN.md | 2 +- README.md | 2 +- app/globals.css | 26 +++++ registry.json | 15 +++ .../winlab/blocks/claude-code/claude-code.tsx | 99 +++++++++++++++++++ registry/winlab/ui/terminal.tsx | 11 ++- 6 files changed, 152 insertions(+), 3 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index ad86f5c..c6b0737 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -173,6 +173,6 @@ Animation that keeps running, such as a terminal demo or a drawing that plays a ## Pictures of other programs -`browser` frames a picture of a web page and uses only the site's tokens. `terminal` shares its outer edge (`border`), corners and 2.5rem title bar, so a guide can show a terminal step and a browser step one after the other. `terminal` and `claude-code` draw another program's screen, so they keep that program's look instead of the page's: their own dark colors in both themes (`terminal-*` for the window and shell text, `claude-*` for Claude Code's screen, named and valued after Claude Code's dark theme), 1.25rem rows and two-cell East Asian characters. Use them only to show what a member will see in a terminal, such as a demo or an install guide, never as page chrome. Change what they say through props, not how they look; a color they need is a `terminal-*` or `claude-*` token, never a literal; check a new screen against a real `claude` run with an empty `CLAUDE_CONFIG_DIR`. Everything on a terminal screen is a character, rules (`─`), bars (`│`) and the Claude logo (`▐▛███▛█`) included: never draw them with borders or SVG. `terminal` fills block and box-drawing characters to the whole cell, the way a terminal does, so rows touch and the text still copies. +`browser` frames a picture of a web page and uses only the site's tokens. `terminal` shares its outer edge (`border`), corners and 2.5rem title bar, so a guide can show a terminal step and a browser step one after the other. `terminal` and `claude-code` draw another program's screen, so they keep that program's look instead of the page's: their own dark colors in both themes (`terminal-*` for the window and shell text, `claude-*` for Claude Code's screen, named and valued after Claude Code's dark theme), 1.25rem rows and two-cell East Asian characters. Use them only to show what a member will see in a terminal, such as a demo or an install guide, never as page chrome. Change what they say through props, not how they look; a color they need is a `terminal-*` or `claude-*` token, never a literal; check a new screen against a real `claude` run with an empty `CLAUDE_CONFIG_DIR`. Everything on a terminal screen is a character, rules (`─`), bars (`│`), round boxes (`╭╮╰╯`) and the Claude logo (`▐▛███▛█`) included: never draw them with borders or SVG. `terminal` fills block and box-drawing characters to the whole cell, the way a terminal does, so rows touch and the text still copies. A service that signs other programs in, such as an MCP server, asks the member on a `consent` page in the spotlight layout: the title names the service, the client is named as the server received it, and the two buttons are 取消 (outline) and 允許 (primary). A service that renders HTML outside React copies the block's markup and these tokens into its own page, so every WinLab consent page looks the same. diff --git a/README.md b/README.md index ed5264a..8f20623 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ npx shadcn@latest add @winlab/button | `focus` | `registry:block` | The one loud thing on a page, under its header: a label, a title with a highlight and a row of facts | | `avatar-stack` | `registry:block` | Several people in the space of one; the avatars spread apart when pointed at | | `countdown` | `registry:block` | Time left until a moment, to the minute, with rolling digits | -| `claude-code` | `registry:block` | Claude Code's screen in a `terminal` for demos and install guides: welcome, prompts, MCP calls, permission questions, the `/plugins` and `/mcp` menus; content through props; colors are `claude-*` tokens from Claude Code's dark theme | +| `claude-code` | `registry:block` | Claude Code's screen in a `terminal` for demos and install guides: welcome, prompts, MCP calls, permission questions, the `/plugins` menus (marketplaces, the plugin list with its search box, plugin details) and `/mcp`; content through props; colors are `claude-*` tokens from Claude Code's dark theme | | `typing` | `registry:lib` | Timed keystrokes for typing animations: a steady pace (100ms a key, about 120 WPM) with jitter; Chinese typed as zhuyin that turns into the character on its tone key; `InputBox` shows the zhuyin with `composing` | | `expand-row` | `registry:block` | A list row that opens in place to show its details | | `badge` | `registry:ui` | Status label; variants `default`, `muted`, `outline`, `destructive`, `success`, `warning` | diff --git a/app/globals.css b/app/globals.css index 87e51eb..240a826 100644 --- a/app/globals.css +++ b/app/globals.css @@ -414,6 +414,32 @@ @utility cell-br { --cell-br: currentColor; } +@utility cell-arc { + background-image: radial-gradient( + ellipse 50% 50% at var(--cell-arc-at), + transparent calc(100% - 1.5px), + currentColor calc(100% - 1px), + currentColor 100%, + transparent calc(100% + 0.5px) + ); +} + +@utility cell-arc-tl { + --cell-arc-at: 100% 100%; +} + +@utility cell-arc-tr { + --cell-arc-at: 0% 100%; +} + +@utility cell-arc-bl { + --cell-arc-at: 100% 0%; +} + +@utility cell-arc-br { + --cell-arc-at: 0% 0%; +} + @utility cell-hline { background-image: linear-gradient(currentColor 0 0); background-size: 100% 1px; diff --git a/registry.json b/registry.json index 4f229a8..62e550a 100644 --- a/registry.json +++ b/registry.json @@ -728,6 +728,21 @@ "background-image": "linear-gradient(currentColor 0 0)", "background-size": "1px 100%", "background-position": "center" + }, + "@utility cell-arc": { + "background-image": "radial-gradient(ellipse 50% 50% at var(--cell-arc-at), transparent calc(100% - 1.5px), currentColor calc(100% - 1px), currentColor 100%, transparent calc(100% + 0.5px))" + }, + "@utility cell-arc-tl": { + "--cell-arc-at": "100% 100%" + }, + "@utility cell-arc-tr": { + "--cell-arc-at": "0% 100%" + }, + "@utility cell-arc-bl": { + "--cell-arc-at": "100% 0%" + }, + "@utility cell-arc-br": { + "--cell-arc-at": "0% 0%" } } }, diff --git a/registry/winlab/blocks/claude-code/claude-code.tsx b/registry/winlab/blocks/claude-code/claude-code.tsx index 97fb7cc..1622b4e 100644 --- a/registry/winlab/blocks/claude-code/claude-code.tsx +++ b/registry/winlab/blocks/claude-code/claude-code.tsx @@ -735,6 +735,102 @@ function Marketplace({ ) } +// A search field in a menu, such as the one above /plugins > Discover: a +// round box drawn with box characters, as wide as the screen. +function SearchBox({ placeholder = "Search…" }: { placeholder?: string }) { + const edge = (left: string, right: string) => ( + + {left} + + {"─".repeat(240)} + + {right} + + ) + return ( +
+ {edge("╭", "╮")} + + │ + + {" ⌕ "} + {placeholder} + + │ + + {edge("╰", "╯")} +
+ ) +} + +type PluginItem = { + name: string + description: string + /** The version the marketplace lists, such as "v0002". */ + version: string + installed?: boolean +} + +// Claude Code cuts a description to the cells it has and ends it with "…". +function cut(text: string, width: number) { + return text.length > width ? `${text.slice(0, width - 1)}…` : text +} + +// The plugin list of /plugins > Discover: a count, then one row per plugin +// with its description cut to `width` cells and its version. `shown` rows +// are drawn; the rest sit under "more below". +function PluginList({ + items, + choice, + shown = items.length, + width = 60, +}: { + items: PluginItem[] + choice: number + shown?: number + width?: number +}) { + return ( + <> + + Install Plugins{" "} + + ({choice + 1}/{items.length}) + + + {items.slice(0, shown).map((item, index) => { + const on = index === choice + return ( +
+ + {on ? ❯ : " "} + {item.installed ? ✔ : "○ "} + + {item.name} + + {item.installed && (installed)} + + + + {cut(item.description, width)} · {item.version} + + +
+ ) + })} + {shown < items.length && ( + + ↓ more below + + )} + + ) +} + // A key: value row in a detail screen. function Field({ name, @@ -784,15 +880,18 @@ export { Menu, Notice, Output, + PluginList, PluginTabs, Prompt, Reply, Rule, Screen, + SearchBox, Spinner, ToolUse, ClaudeText, type ClaudeTone, + type PluginItem, doneVerb, spinnerVerb, Welcome, diff --git a/registry/winlab/ui/terminal.tsx b/registry/winlab/ui/terminal.tsx index e008540..809efc4 100644 --- a/registry/winlab/ui/terminal.tsx +++ b/registry/winlab/ui/terminal.tsx @@ -65,7 +65,15 @@ const QUADRANTS: Record = { "▚": "1001", "▞": "0110", } -const DRAWN = /([─│▘▝▖▗▌▐▀▄█▛▜▙▟▚▞])/ +const DRAWN = /([─│╭╮╰╯▘▝▖▗▌▐▀▄█▛▜▙▟▚▞])/ +// A round corner is a quarter of an ellipse centred on the cell corner its +// two lines point away from. +const ARCS: Record = { + "╭": "cell-arc-tl", + "╮": "cell-arc-tr", + "╰": "cell-arc-bl", + "╯": "cell-arc-br", +} // One drawn cell: the character stays in the text for copying and screen // readers; its glyph is hidden and the cell's background draws the shape in @@ -80,6 +88,7 @@ function DrawnCell({ char }: { char: string }) { "cell-drawn", char === "─" && "cell-hline", char === "│" && "cell-vline", + ARCS[char] && ["cell-arc", ARCS[char]], quarters && [ "cell-quad", ...QUARTERS.filter((_, index) => quarters[index] === "1"),