diff --git a/AGENTS.md b/AGENTS.md index 5b6129f..f1cd309 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,7 +33,8 @@ The primary job of a spec is to be an accurate reference for the current state o - **`docs/specs/layout.md`** — Tiling layout, pane/door containers, dockview configuration, modes (passthrough/command), keyboard shortcuts, selection overlay, spatial navigation, detach/reattach, inline rename, session lifecycle, session persistence, and theming. Read this when touching: `Pond.tsx`, `Baseboard.tsx`, `Door.tsx`, `TerminalPane.tsx`, `spatial-nav.ts`, `layout-snapshot.ts`, `terminal-registry.ts`, `session-save.ts`, `session-restore.ts`, `reconnect.ts`, `index.css`, `theme.css`, or any keyboard/navigation/mode behavior. - **`docs/specs/alarm.md`** — Activity monitoring state machine, alarm trigger/clearing rules, attention model, TODO lifecycle (soft/hard), bell button visual states and interaction, door alarm indicators, and hardening (a11y, motion, i18n, overflow). Read this when touching: `activity-monitor.ts`, `alarm-manager.ts`, the alarm bell or TODO pill in `Pond.tsx` (TerminalPaneHeader), alarm indicators in `Door.tsx`, or the `a`/`t` keyboard shortcuts. Layout.md defers to this spec for all alarm/TODO behavior. - **`docs/specs/vscode.md`** — VS Code extension architecture: hosting modes (WebviewView + WebviewPanel), PTY lifecycle and buffering, message protocol between webview and extension host, session persistence flow, reconnection protocol, theme integration, CSP, build pipeline, and invariants (save-before-kill ordering, PTY ownership, alarm state merging). Read this when touching: `extension.ts`, `webview-view-provider.ts`, `message-router.ts`, `message-types.ts`, `pty-manager.ts`, `pty-host.js`, `session-state.ts`, `webview-html.ts`, `vscode-adapter.ts`, or `pty-core.js`. -- **`docs/specs/tutorial.md`** — Playground tutorial on the website: 3-pane initial layout, `tut` command and TutorialShell, 6-step progressive tutorial with detection logic, theme picker, FakePtyAdapter extensions, and Pond event hooks. Read this when touching: `website/src/pages/Playground.tsx`, `website/src/lib/tutorial-shell.ts`, `website/src/lib/tutorial-detection.ts`, `website/src/components/ThemePicker.tsx`, `website/src/lib/playground-themes.ts`, `lib/src/lib/platform/fake-scenarios.ts` (tutorial scenarios), or the `onApiReady`/`onEvent`/`initialPaneIds` props on Pond. +- **`docs/specs/tutorial.md`** — Playground tutorial on the website: 3-pane initial layout, `tut` command and TutorialShell, 6-step progressive tutorial with detection logic, theme picker, FakePtyAdapter extensions, and Pond event hooks. Read this when touching: `website/src/pages/Playground.tsx`, `website/src/lib/tutorial-shell.ts`, `website/src/lib/tutorial-detection.ts`, `lib/src/components/ThemePicker.tsx`, `lib/src/lib/themes/`, `lib/src/lib/platform/fake-scenarios.ts` (tutorial scenarios), or the `onApiReady`/`onEvent`/`initialPaneIds` props on Pond. +- **`docs/specs/theme.md`** — Theme system: two-layer CSS variable strategy, theme data model, conversion pipeline, bundled themes, localStorage store, shared ThemePicker component, standalone AppBar picker, runtime OpenVSX installer. Read this when touching: `lib/src/lib/themes/`, `lib/src/components/ThemePicker.tsx`, `lib/src/theme.css`, `lib/scripts/bundle-themes.mjs`, `standalone/src/AppBar.tsx` (theme picker), `standalone/src/main.tsx` (theme restore), or `website/src/components/SiteHeader.tsx` (themeAware mode). When updating code covered by a spec, update the spec to match. When the two specs overlap (e.g. pane header elements appear in both), layout.md documents placement and sizing while alarm.md documents behavior and visual states. diff --git a/docs/specs/TODO.md b/docs/specs/TODO.md deleted file mode 100644 index 9fe4ca0..0000000 --- a/docs/specs/TODO.md +++ /dev/null @@ -1,5 +0,0 @@ -- [ ] fix headerbar color and size / font size -- [ ] fix selection size / glow -- [ ] desaturate selection on blur -- [ ] alarm right-click popup and TODO/soft-TODO popup -- [ ] dynamic shrinking on soft-TODO during typing \ No newline at end of file diff --git a/docs/specs/deploy.md b/docs/specs/deploy.md index b254636..331a057 100644 --- a/docs/specs/deploy.md +++ b/docs/specs/deploy.md @@ -18,6 +18,7 @@ Human-driven steps, in order: 1. **Update dependencies page** — run `node website/scripts/generate-deps.js` and review the diff in `website/src/data/dependencies.json`. Commit if changed. 2. **Finalize changelog** — promote the `[Unreleased]` section in `CHANGELOG.md` to `[X.Y.Z]` with today's date. Write release notes covering both standalone and VSCode changes. 3. **Bump versions** — update `version` in all three places: + - [standalone/src-tauri/Cargo.toml](../../standalone/src-tauri/Cargo.toml) - [standalone/src-tauri/tauri.conf.json](../../standalone/src-tauri/tauri.conf.json) - [vscode-ext/package.json](../../vscode-ext/package.json) - [lib/package.json](../../lib/package.json) diff --git a/docs/specs/layout.md b/docs/specs/layout.md index f2f2ad8..d0345d0 100644 --- a/docs/specs/layout.md +++ b/docs/specs/layout.md @@ -68,7 +68,7 @@ The content area is a tiling layout of panes, powered by dockview. Each pane occ ### Pane header -Each pane has a 30px header that doubles as a drag handle. The header uses `cursor-grab` / `active:cursor-grabbing` and `select-none`. Background uses `--mt-tab-*` theme tokens (adapts to VSCode host theme). Dockview's default close button and right-actions container are hidden via CSS. +Each pane has a 30px header that doubles as a drag handle. The header uses `cursor-grab` / `active:cursor-grabbing` and `select-none`. Background uses `--color-tab-*` theme tokens (adapts to VSCode host theme). Dockview's default close button and right-actions container are hidden via CSS. Elements from left to right: @@ -279,9 +279,9 @@ Custom `mousetermTheme` extends dockview's `themeAbyss`: - Pane header height: `--dv-tabs-and-actions-container-height: 30px` - 6px padding around the dockview area (`p-1.5` on wrapper, `inset-1.5` on container) -Colors use a two-layer CSS variable strategy: `--mt-*` semantic tokens → `var(--vscode-*, )`. In VSCode, host theme variables take precedence. In standalone mode, fallback values apply (Dark+ defaults with `prefers-color-scheme: light` overrides). Tailwind v4 `@theme` block registers `--mt-*` tokens as Tailwind colors (e.g., `bg-surface`, `text-foreground`, `border-border`). See `theme.css` for the full token map. +Colors use a two-layer CSS variable strategy: `@theme --color-*` tokens → `var(--vscode-*, )`. In VSCode, host theme variables take precedence. In standalone mode, fallback values apply with `prefers-color-scheme: light` overrides. Tailwind v4 `@theme` block registers `--color-*` tokens as Tailwind colors (e.g., `bg-surface`, `text-foreground`, `border-border`). See `theme.css` for the full token map. -Dockview's separator borders, sash handles, and groupview borders are all set to transparent/none — the 6px gap is the only visual separator between panes. All dockview container backgrounds are flattened to `var(--mt-surface)`. +Dockview's separator borders, sash handles, and groupview borders are all set to transparent/none — the 6px gap is the only visual separator between panes. All dockview container backgrounds are flattened to `var(--color-surface)`. ## Corner cases @@ -315,4 +315,4 @@ Dockview's separator borders, sash handles, and groupview borders are all set to | `lib/src/lib/reconnect.ts` | Priority-based recovery: live PTYs first, then saved session, then empty | | `lib/src/lib/resume-patterns.ts` | Detects resumable commands (`claude --resume`, etc.) in scrollback | | `lib/src/index.css` | Dockview theme overrides — separator/sash/border removal, background flattening | -| `lib/src/theme.css` | Two-layer VSCode theme token system (`--mt-*` → `--vscode-*`) and Tailwind v4 `@theme` integration | +| `lib/src/theme.css` | Two-layer VSCode theme token system (`@theme --color-*` → `--vscode-*`) and Tailwind v4 `@theme` integration | diff --git a/docs/specs/theme.md b/docs/specs/theme.md new file mode 100644 index 0000000..d0a9395 --- /dev/null +++ b/docs/specs/theme.md @@ -0,0 +1,230 @@ +# Theme Spec + +MouseTerm uses real VSCode themes in standalone and website mode. Bundled themes are extracted from actual VSCode theme extensions at build time. Users can also install additional themes from [OpenVSX](https://open-vsx.org/) at runtime. + +## How it works + +MouseTerm has a two-layer CSS variable theme system: + +1. **`--vscode-*`** — the theme data. In VSCode extension mode, the editor injects these automatically. In standalone/website mode, `applyTheme()` sets them on `document.body`. +2. **`@theme --color-*`** — Tailwind tokens with fallbacks. Defined in `theme.css` as `--color-surface: var(--vscode-editor-background, #1e1e1e)`. Powers utility classes like `bg-surface`, `text-foreground`, `border-border`. + +``` +VSCode theme JSON CSS variables Tailwind +───────────────── ────────────── ──────── +colors: { --vscode-editor-background @theme --color-surface + "editor.background": "#282a36" → set on body.style → → bg-surface + "terminal.ansiRed": "#ff5555" --vscode-terminal-ansiRed (read by getTerminalTheme()) + ... ... +} +``` + +Two consumers read `--vscode-*` variables: +- **`@theme` fallbacks** in `theme.css` — for UI colors (surfaces, tabs, badges, buttons, text) +- **`getTerminalTheme()`** in `terminal-registry.ts` — reads ANSI colors, cursor, and selection directly as `--vscode-*` for xterm.js + +A MutationObserver on `document.body` (in `terminal-registry.ts`) detects style changes and re-reads the theme for all xterm.js terminals. Dockview overrides and Tailwind classes update automatically because they reference `--color-*`. + +### Cleanup from previous `--mt-*` layer + +The old three-layer system (`--vscode-*` → `--mt-*` → `--color-*`) had a redundant middle layer. The `--mt-*` variables were a pure passthrough — every one was immediately re-exported as `--color-*` with no transformation. The cleanup: + +- **Collapsed `--mt-*` into `@theme`**: `--color-surface: var(--vscode-editor-background, #1e1e1e)` directly, no intermediate variable. +- **Deleted 38 dead variables** (114 lines of CSS): `--mt-ansi-*` (32 colors), `--mt-terminal-cursor`, `--mt-terminal-selection`, `--mt-gutter`, `--mt-gutter-active`, `--mt-editor-font-size`, `--mt-editor-font-family`, `--mt-selection-workspace` were defined 3x each (dark/light/prefers-color-scheme) but never consumed. The ANSI colors are read directly as `--vscode-*` by `getTerminalTheme()`. +- **Eliminated duplicate mappings**: `--vscode-focusBorder` was aliased as `--mt-accent`, `--mt-gutter-active`, and `--mt-selection-terminal` — three names for one token. Now just `--color-accent`. +- **Dropped `testing.iconPassed` mapping**: `--mt-success-fg` stole `testing.iconPassed` as a generic success color. Most themes don't define this key, and it's the wrong semantic. `--color-success` now uses a hardcoded green. +- **Kept defensible cross-domain mappings**: `--color-tab-selected-bg/fg` ← `list.activeSelectionBackground/Foreground` (closest VSCode approximation of our command-mode tab selection). `--color-accent` ← `focusBorder` (most themes treat this as their brand/accent color). + +### Light theme body class + +`applyTheme()` adds `vscode-light` to `document.body.classList` for light themes and removes it for dark themes. `theme.css` has a `body.vscode-light` selector that switches all `--color-*` fallback values to the light fallback palette. Without this class, a light theme that doesn't explicitly define every key would get dark fallbacks for missing keys. + +## Theme data model + +```typescript +interface MouseTermTheme { + id: string; // "GitHub.github-vscode-theme.github-dark-default" + label: string; // "GitHub Dark Default" + type: 'dark' | 'light'; + swatch: string; // editor.background — used for picker preview + accent: string; // focusBorder — used for picker accent dot + vars: Record; // --vscode-* CSS variable overrides + origin: BundledOrigin | InstalledOrigin; +} + +interface BundledOrigin { kind: 'bundled' } +interface InstalledOrigin { + kind: 'installed'; + extensionId: string; // "publisher/theme-extension" + installedAt: string; // ISO date +} +``` + +This replaced the old `PlaygroundTheme` interface (previously in `website/src/lib/playground-themes.ts`, now deleted). + +## Conversion pipeline + +### `CONSUMED_VSCODE_KEYS` + +Not all VSCode theme color keys matter to MouseTerm — only the ~45 keys that are actually read. The conversion function filters to this set and drops the rest. The keys come from two consumers: + +**Read by `@theme` fallbacks** (UI colors, ~25 keys): +- **Surfaces**: `editor.background`, `editorGroupHeader.tabsBackground`, `sideBar.background`, `editorWidget.background` +- **Text**: `editor.foreground`, `descriptionForeground` +- **Accent/borders**: `focusBorder`, `panel.border` +- **Tabs**: `tab.activeBackground`, `tab.inactiveBackground`, `tab.activeForeground`, `tab.inactiveForeground`, `list.activeSelectionBackground`, `list.activeSelectionForeground` +- **Terminal**: `terminal.background`, `terminal.foreground` +- **Status**: `badge.background`, `badge.foreground`, `errorForeground`, `editorWarning.foreground` +- **Inputs**: `input.background`, `input.border` +- **Buttons**: `button.background`, `button.foreground`, `button.hoverBackground` +- **Links**: `textLink.foreground` + +**Read by `getTerminalTheme()` directly** (terminal colors, ~20 keys): +- `terminal.background`, `terminal.foreground` (also in `@theme`) +- `terminalCursor.foreground`, `terminal.selectionBackground` +- `terminal.ansiBlack` through `terminal.ansiBrightWhite` (16 ANSI colors) + +### Conversion rule + +For each key in the VSCode theme's `colors` object: if it's in `CONSUMED_VSCODE_KEYS`, emit `--vscode-${key.replace(/\./g, '-')}` → value. Keys not consumed by MouseTerm are silently dropped. Missing keys fall through to the `@theme` fallbacks in `theme.css`, which is the same behavior as VSCode itself. + +## Bundled themes + +Bundled themes are extracted at build time by a Node.js script (`lib/scripts/bundle-themes.mjs`) and written to `lib/src/lib/themes/bundled.json`. This file is checked into git so builds don't require network access. + +### Source extensions + +| Extension | OpenVSX ID | Variants | +|-----------|-----------|----------| +| GitHub VSCode Theme | `GitHub/github-vscode-theme` | Dark Default, Light Default, Dark Dimmed, Dark High Contrast, Light High Contrast, Dark Colorblind, Light Colorblind, etc. | + +### Build script flow + +``` +lib/scripts/bundle-themes.mjs + | + +- for each extension in EXTENSIONS list: + | +- fetch /api/{ns}/{name}/latest from OpenVSX + | +- download VSIX from files.download URL + | +- unzip (Node.js zlib + ZIP reader) + | +- read extension/package.json -> contributes.themes + | +- for each theme variant: + | +- read theme JSON from ZIP (parse with jsonc-parser for comments) + | +- convertVscodeThemeColors(colors) -> vars + | +- emit MouseTermTheme object + | + +- write lib/src/lib/themes/bundled.json +``` + +Run manually: `pnpm bundle-themes`. Output is committed. + +## Theme store (localStorage) + +| Key | Value | +|-----|-------| +| `mouseterm:installed-themes` | JSON array of `MouseTermTheme` objects (user-installed only) | +| `mouseterm:active-theme` | Theme ID string | + +The store module provides: +- `getAllThemes()` — bundled themes (from `bundled.json`) + installed themes (from localStorage) +- `getActiveThemeId()` / `setActiveThemeId(id)` — persists choice across sessions +- `addInstalledTheme(theme)` / `removeInstalledTheme(id)` — manages user-installed themes + +## Shared theme picker + +`lib/src/components/ThemePicker.tsx` exports a shared `ThemePicker` with two variants: + +- `playground-header` — used only on `/playground`, passed through `SiteHeader`'s `controls` slot. It renders a visible `Theme:` label and uses a mobile fixed dropdown so the menu stays inside the viewport. +- `standalone-appbar` — used only by the Tauri standalone `AppBar`. It uses a compact trigger for the 30px AppBar and an anchored dropdown. + +Both variants use the same state and behavior: restore the persisted active theme on mount, list bundled themes first, append installed themes, persist selected themes to `mouseterm:active-theme`, apply selections immediately, show an `X` for installed themes, confirm before deletion, and fall back to the first remaining theme when deleting the active installed theme. The dropdown footer is always `Install theme from OpenVSX`; it opens the shared runtime installer dialog. + +The picker is mounted only by the website playground page and the standalone AppBar. It is not mounted on non-playground website routes, and it is not mounted from `mouseterm-lib/App`, `Pond`, or any VS Code extension entry point. + +## Standalone AppBar picker + +The standalone Tauri app renders the shared theme picker in `AppBar`, not in `mouseterm-lib/App` or `Pond`, so the VS Code extension entry point does not mount it. On macOS it sits in the right-side AppBar action group next to the shell dropdown; on Windows/Linux it sits before the native window controls. The AppBar already uses `bg-surface-alt`, `text-foreground`, and related theme tokens, so changing the active theme updates the AppBar chrome as well as Dockview and terminals. + +`standalone/src/main.tsx` restores the persisted active theme before reconnecting/restoring Pond. This prevents the first terminal render from briefly using fallback colors. The picker itself also restores and refreshes theme state on mount. + +## Runtime OpenVSX installer + +Users can browse and install themes from OpenVSX directly in the app. + +### OpenVSX API + +OpenVSX has permissive CORS (`Access-Control-Allow-Origin: *`) — no proxy needed. + +- **Search**: `GET https://open-vsx.org/api/-/search?category=Themes&query=...&size=...&offset=...` +- **Extension details**: `GET https://open-vsx.org/api/{namespace}/{name}/latest` +- **VSIX download**: URL in the response's `files.download` field + +### In-browser extraction + +VSIX files are ZIP archives. Extraction uses `fflate` (~8 KB gzipped) via dynamic import — only loaded when the user opens the theme store, so no impact on initial bundle. + +``` +user searches OpenVSX + | + +- fetch /api/-/search?category=Themes&query=... + +- display results (name, icon, download count) + | + user clicks "Install" on an extension + | + +- fetch /api/{ns}/{name}/latest -> get VSIX download URL + +- fetch VSIX as ArrayBuffer + +- fflate.unzipSync() -> all files + +- read extension/package.json -> contributes.themes + +- for each theme variant: + | +- parse theme JSON (jsonc-parser) + | +- convertVscodeThemeColors(colors) -> MouseTermTheme + | +- addInstalledTheme(theme) -> persists to localStorage + | + +- theme immediately available in picker +``` + +## Files + +| File | Role | +|------|------| +| [`lib/src/lib/themes/types.ts`](../../lib/src/lib/themes/types.ts) | `MouseTermTheme` interface and origin types | +| [`lib/src/lib/themes/convert.ts`](../../lib/src/lib/themes/convert.ts) | `CONSUMED_VSCODE_KEYS`, `convertVscodeThemeColors()`, `uiThemeToType()` | +| [`lib/src/lib/themes/apply.ts`](../../lib/src/lib/themes/apply.ts) | `applyTheme()` — sets CSS vars on body, manages body classes | +| [`lib/src/lib/themes/store.ts`](../../lib/src/lib/themes/store.ts) | Theme registry combining bundled + installed, localStorage persistence | +| [`lib/src/lib/themes/openvsx.ts`](../../lib/src/lib/themes/openvsx.ts) | OpenVSX search API, VSIX download + extraction | +| [`lib/src/lib/themes/bundled.json`](../../lib/src/lib/themes/bundled.json) | Pre-converted bundled themes (generated, checked in) | +| [`lib/src/lib/themes/index.ts`](../../lib/src/lib/themes/index.ts) | Barrel export | +| [`lib/scripts/bundle-themes.mjs`](../../lib/scripts/bundle-themes.mjs) | Build-time script to download and convert themes from OpenVSX | +| [`lib/src/theme.css`](../../lib/src/theme.css) | `@theme` tokens with `var(--vscode-*, fallback)` + light mode overrides | +| [`lib/src/lib/terminal-registry.ts`](../../lib/src/lib/terminal-registry.ts) | MutationObserver + `getTerminalTheme()` — no changes needed | +| [`lib/src/components/ThemePicker.tsx`](../../lib/src/components/ThemePicker.tsx) | Shared website/standalone dropdown and OpenVSX dialog for selecting, installing, and deleting themes | +| [`website/src/components/SiteHeader.tsx`](../../website/src/components/SiteHeader.tsx) | Shared site header; playground enables `themeAware` so header chrome follows the active theme | +| [`standalone/src/AppBar.tsx`](../../standalone/src/AppBar.tsx) | Mounts the standalone theme picker in the Tauri AppBar, outside the VS Code extension path | +| [`standalone/src/main.tsx`](../../standalone/src/main.tsx) | Restores the persisted standalone theme before Pond reconnects | + +## Dependencies + +- `jsonc-parser` — parses JSONC (JSON with comments/trailing commas), already a transitive dependency via Storybook +- `fflate` — ~8 KB gzipped ZIP library for in-browser VSIX extraction, dynamically imported + +## Design decisions + +**Why two layers, not three?** The old `--mt-*` middle layer was a pure passthrough — every variable was immediately re-exported as `--color-*`. Collapsing it into `@theme` eliminates 114 lines of dead CSS and removes a layer of indirection with no loss of functionality. + +**Why keep semantic names (`surface`, `accent`) instead of VSCode names (`editor-background`, `focusBorder`)?** `bg-surface` reads better in JSX than `bg-editor-background`, and some mappings are genuinely semantic (`surface-alt` communicates intent better than `editorGroupHeader-tabsBackground`). The mapping is documented in one place (`theme.css`). + +**Why hardcode `success` instead of mapping to `testing.iconPassed`?** Most VSCode themes don't define `testing.iconPassed` — it's an optional, domain-specific key. Using it as generic "success green" means imported themes either get our fallback (fine) or get a color chosen for test runner icons (wrong semantic). A hardcoded green is more reliable. + +**Why extract from real VSIX files instead of manually copying colors?** Manual copying is error-prone — colors drift, coverage is incomplete, and adding new themes requires hunting through source repos. Extracting from the actual published extension guarantees accuracy and makes adding themes trivial (add one line to the extensions list, re-run the script). + +**Why bundle pre-converted themes instead of fetching at runtime?** Bundled themes work offline, load instantly, and don't depend on OpenVSX availability. The bundled JSON is small (~1 KB per theme). Runtime fetching is an opt-in addition for users who want more themes. + +**Why `fflate` over `JSZip`?** JSZip is ~45 KB gzipped. `fflate` is ~8 KB, tree-shakeable, and faster. We only need to read ZIPs, not create them. + +**Why `localStorage` over IndexedDB?** Theme data is small (~1-2 KB per theme). Even with 50 installed themes, that's well under the 5 MB localStorage limit. The project already uses localStorage for state persistence in both standalone and website. IndexedDB would add complexity with no benefit at this scale. + +**Why filter to `CONSUMED_VSCODE_KEYS` instead of passing all colors through?** VSCode themes can define 500+ color keys. Setting all of them as CSS variables would be wasteful (most are never read) and could cause unexpected interactions if VSCode adds new keys that happen to match future `--color-*` variables. + +**Why set the `vscode-light` body class?** `theme.css` uses `body.vscode-light` to switch all `--color-*` fallback values to the light fallback palette. Without this class, a light theme that doesn't explicitly define every key would get dark fallbacks for the missing ones, creating a broken mixed appearance. + +**Why not use OpenVSX's direct file access instead of downloading the full VSIX?** OpenVSX doesn't expose individual theme files via API — you have to download the full VSIX. However, theme-only extensions are typically small (50-200 KB), so this is fine. The build script and runtime installer share the same extraction logic. diff --git a/docs/specs/tutorial.md b/docs/specs/tutorial.md index ba398a1..628333b 100644 --- a/docs/specs/tutorial.md +++ b/docs/specs/tutorial.md @@ -4,15 +4,15 @@ At the `/playground` route on the website. **Status: Implemented** (Epics 14, 15 ## Layout -- `SiteHeader` at top (with Playground as active nav item). -- `PlaygroundToolbar` below the header: a dedicated toolbar with a **theme picker** dead center (mouse-based, not keyboard). Visually distinct from the site nav — belongs to the sandbox, not the website. -- Below the toolbar: MouseTerm `Pond` embedded fullscreen using `FakePtyAdapter`. +- `SiteHeader` at top (with Playground as active nav item). On `/playground`, the header renders the **Theme:** dropdown as an optional header control; other routes do not render it. +- Below the header: MouseTerm `Pond` embedded fullscreen using `FakePtyAdapter`. The page-level `
` is a flex container so Pond's `flex-1 min-h-0` root receives a real height. +- The playground header uses the active `--vscode-*` theme variables for its background, border, text, and banner colors so theme changes affect the header as well as Pond. ### Implementation - `website/src/pages/Playground.tsx` — Page component. Dynamically imports Pond (SSR-safe). Initializes `FakePtyAdapter`, `TutorialShell`, and `TutorialDetector`. Passes `onApiReady` to set up the 3-pane layout and `onEvent` for step detection. -- `website/src/components/PlaygroundToolbar.tsx` — Toolbar shell with centered slot. -- `website/src/components/ThemePicker.tsx` — Color dot swatches for 5 themes. +- `website/src/components/SiteHeader.tsx` — Shared header. Accepts an optional playground-only `controls` slot and a `themeAware` mode that reads the active VSCode theme variables. +- `mouseterm-lib/components/ThemePicker` — Shared header dropdown for bundled and installed themes. The playground passes `variant="playground-header"` and the footer action opens the OpenVSX installer. - `website/vite.config.ts` — Vite alias `mouseterm-lib` → `../lib/src` for workspace imports. ## Initial State @@ -125,16 +125,18 @@ The sandbox stays fully functional after completion. Running `tut` shows "Tutori ## Theme Picker -Implemented in `website/src/lib/playground-themes.ts` and `website/src/components/ThemePicker.tsx`. +Implemented in `mouseterm-lib/lib/themes` and `mouseterm-lib/components/ThemePicker`. -5 themes available: **Dark** (default), **Monokai**, **Solarized**, **Nord**, **Dracula**. +Bundled themes are provided by `mouseterm-lib/lib/themes` and include only GitHub variants. Users can install additional themes from OpenVSX through the dropdown footer action. -Each theme is defined as a map of `--vscode-*` CSS variable overrides. `applyTheme()` sets these on `document.body`, which: -1. Cascades into `--mt-*` variables (via `var(--vscode-*, fallback)` in `theme.css`) +The picker appears only on `/playground`, inside `SiteHeader`, labeled `Theme:`. The trigger opens a dropdown of bundled and installed themes. The dropdown footer is always `Install theme from OpenVSX`, which opens the theme store dialog. Installed theme rows include an `X` delete control; deletion requires browser confirmation before removing the theme from localStorage. If the active installed theme is deleted, the picker falls back to the first bundled theme and applies it immediately. + +Each theme is defined as a map of `--vscode-*` CSS variable overrides. `applyTheme()` applies the active theme, which: +1. Cascades into `--color-*` variables (via `var(--vscode-*, fallback)` in `theme.css`) 2. Triggers the `MutationObserver` in `terminal-registry.ts` to re-read `getTerminalTheme()` for all xterm.js terminals 3. Updates Dockview/Tailwind token colors -The Dark theme uses an empty vars map (relies on the existing CSS fallback values). +The picker restores the persisted active theme on mount. The playground header is `themeAware`, so the same active theme also affects the site header chrome while the picker remains hidden on non-playground routes. ## Technical Notes @@ -142,4 +144,3 @@ The Dark theme uses an empty vars map (relies on the existing CSS fallback value - `FakePtyAdapter` extensions: `setInputHandler(id, fn)` routes `writePty` calls to a custom handler; `sendOutput(id, data)` writes to a terminal's output stream. - `Pond` extensions: `initialPaneIds` prop seeds the first pane(s); `onApiReady` callback prop exposes `DockviewApi`; `onEvent` callback prop fires `PondEvent` for mode/zoom/detach/selection/split changes (types: `modeChange`, `zoomChange`, `detachChange`, `split`, `selectionChange`). - `SCENARIO_TUTORIAL_MOTD` scenario added to `lib/src/lib/platform/fake-scenarios.ts`. - diff --git a/docs/specs/vscode.md b/docs/specs/vscode.md index d80ec75..82528d3 100644 --- a/docs/specs/vscode.md +++ b/docs/specs/vscode.md @@ -231,7 +231,7 @@ Example of the pattern: --color-surface: var(--mt-surface); ``` -Full mapping in `lib/src/theme.css` covers: surfaces (3), text (2), accent/borders (4), tabs (6), terminal bg/fg/cursor/selection (4), all 16 ANSI colors + bright variants, badges (2), semantic status (3), inputs (2), buttons (3), and selection (2). Dark mode fallbacks (VS Code Dark+ defaults) are in `:root`; light mode overrides (VS Code Light+ defaults) are in `body.vscode-light`; a standalone fallback uses `@media (prefers-color-scheme: light)` for non-VS Code contexts. +Full mapping in `lib/src/theme.css` covers: surfaces (3), text (2), accent/borders (4), tabs (6), terminal bg/fg/cursor/selection (4), all 16 ANSI colors + bright variants, badges (2), semantic status (3), inputs (2), buttons (3), and selection (2). Dark mode fallbacks are in `:root`; light mode overrides are in `body.vscode-light`; a standalone fallback uses `@media (prefers-color-scheme: light)` for non-VS Code contexts. A `MutationObserver` in `terminal-registry.ts` watches for VS Code theme changes on `body`/`html` (class and style attribute mutations) and live-updates all xterm.js instances. diff --git a/lib/.storybook/preview.ts b/lib/.storybook/preview.ts index 57f87ae..993bcec 100644 --- a/lib/.storybook/preview.ts +++ b/lib/.storybook/preview.ts @@ -43,7 +43,7 @@ const preview: Preview = { }, }, initialGlobals: { - theme: 'Dark+', + theme: 'GitHub Dark Default', }, decorators: [ // Theme switcher: inject --vscode-* CSS variables diff --git a/lib/.storybook/themes.ts b/lib/.storybook/themes.ts index a953dbc..51297c9 100644 --- a/lib/.storybook/themes.ts +++ b/lib/.storybook/themes.ts @@ -1,193 +1,13 @@ /** VSCode theme color maps for Storybook theme switcher. - * Each entry maps --vscode-* CSS variable names to their values in that theme. - * When applied, these override the --mt-* fallbacks in theme.css. + * Derived from bundled themes. When applied, these override + * the @theme --color-* fallbacks in theme.css. */ -export const VSCODE_THEMES: Record> = { - 'Dark+': { - '--vscode-editor-background': '#1e1e1e', - '--vscode-editor-foreground': '#cccccc', - '--vscode-sideBar-background': '#252526', - '--vscode-editorWidget-background': '#252526', - '--vscode-descriptionForeground': '#858585', - '--vscode-focusBorder': '#007fd4', - '--vscode-panel-border': '#2b2b2b', - '--vscode-tab-activeBackground': '#1e1e1e', - '--vscode-tab-inactiveBackground': '#2d2d2d', - '--vscode-tab-activeForeground': '#ffffff', - '--vscode-tab-inactiveForeground': '#969696', - '--vscode-list-activeSelectionBackground': '#094771', - '--vscode-list-activeSelectionForeground': '#ffffff', - '--vscode-list-inactiveSelectionBackground': '#2f3f52', - '--vscode-list-inactiveSelectionForeground': '#cccccc', - '--vscode-terminal-background': '#1e1e1e', - '--vscode-terminal-foreground': '#cccccc', - '--vscode-badge-background': '#007acc', - '--vscode-badge-foreground': '#ffffff', - '--vscode-errorForeground': '#f48771', - '--vscode-input-background': '#3c3c3c', - '--vscode-input-border': '#3c3c3c', - '--vscode-button-background': '#0e639c', - '--vscode-button-foreground': '#ffffff', - '--vscode-button-hoverBackground': '#1177bb', - '--vscode-textLink-foreground': '#3794ff', - '--vscode-terminal-ansiBlack': '#000000', - '--vscode-terminal-ansiRed': '#cd3131', - '--vscode-terminal-ansiGreen': '#0dbc79', - '--vscode-terminal-ansiYellow': '#e5e510', - '--vscode-terminal-ansiBlue': '#2472c8', - '--vscode-terminal-ansiMagenta': '#bc3fbc', - '--vscode-terminal-ansiCyan': '#11a8cd', - '--vscode-terminal-ansiWhite': '#e5e5e5', - '--vscode-terminal-ansiBrightBlack': '#666666', - '--vscode-terminal-ansiBrightRed': '#f14c4c', - '--vscode-terminal-ansiBrightGreen': '#23d18b', - '--vscode-terminal-ansiBrightYellow': '#f5f543', - '--vscode-terminal-ansiBrightBlue': '#3b8eea', - '--vscode-terminal-ansiBrightMagenta': '#d670d6', - '--vscode-terminal-ansiBrightCyan': '#29b8db', - '--vscode-terminal-ansiBrightWhite': '#e5e5e5', - '--vscode-terminalCursor-foreground': '#aeafad', - '--vscode-terminal-selectionBackground': '#264f7840', - }, +import _bundled from '../src/lib/themes/bundled.json'; +import type { MouseTermTheme } from '../src/lib/themes/types'; - 'Light+': { - '--vscode-editor-background': '#ffffff', - '--vscode-editor-foreground': '#333333', - '--vscode-sideBar-background': '#f3f3f3', - '--vscode-editorWidget-background': '#f3f3f3', - '--vscode-descriptionForeground': '#717171', - '--vscode-focusBorder': '#0090f1', - '--vscode-panel-border': '#e5e5e5', - '--vscode-tab-activeBackground': '#ffffff', - '--vscode-tab-inactiveBackground': '#ececec', - '--vscode-tab-activeForeground': '#333333', - '--vscode-tab-inactiveForeground': '#8e8e8e', - '--vscode-list-activeSelectionBackground': '#cce6ff', - '--vscode-list-activeSelectionForeground': '#000000', - '--vscode-list-inactiveSelectionBackground': '#d6e5f8', - '--vscode-list-inactiveSelectionForeground': '#333333', - '--vscode-terminal-background': '#ffffff', - '--vscode-terminal-foreground': '#333333', - '--vscode-badge-background': '#007acc', - '--vscode-badge-foreground': '#ffffff', - '--vscode-errorForeground': '#a1260d', - '--vscode-input-background': '#ffffff', - '--vscode-input-border': '#cecece', - '--vscode-button-background': '#007acc', - '--vscode-button-foreground': '#ffffff', - '--vscode-button-hoverBackground': '#0062a3', - '--vscode-textLink-foreground': '#006ab1', - '--vscode-terminal-ansiBlack': '#000000', - '--vscode-terminal-ansiRed': '#cd3131', - '--vscode-terminal-ansiGreen': '#00bc00', - '--vscode-terminal-ansiYellow': '#949800', - '--vscode-terminal-ansiBlue': '#0451a5', - '--vscode-terminal-ansiMagenta': '#bc05bc', - '--vscode-terminal-ansiCyan': '#0598bc', - '--vscode-terminal-ansiWhite': '#555555', - '--vscode-terminal-ansiBrightBlack': '#666666', - '--vscode-terminal-ansiBrightRed': '#cd3131', - '--vscode-terminal-ansiBrightGreen': '#14ce14', - '--vscode-terminal-ansiBrightYellow': '#b5ba00', - '--vscode-terminal-ansiBrightBlue': '#0451a5', - '--vscode-terminal-ansiBrightMagenta': '#bc05bc', - '--vscode-terminal-ansiBrightCyan': '#0598bc', - '--vscode-terminal-ansiBrightWhite': '#a5a5a5', - '--vscode-terminalCursor-foreground': '#000000', - '--vscode-terminal-selectionBackground': '#add6ff80', - }, +const bundled = _bundled as unknown as MouseTermTheme[]; - 'High Contrast Dark': { - '--vscode-editor-background': '#000000', - '--vscode-editor-foreground': '#ffffff', - '--vscode-sideBar-background': '#000000', - '--vscode-editorWidget-background': '#0c141f', - '--vscode-descriptionForeground': '#ffffff', - '--vscode-focusBorder': '#f38518', - '--vscode-panel-border': '#6fc3df', - '--vscode-tab-activeBackground': '#000000', - '--vscode-tab-inactiveBackground': '#000000', - '--vscode-tab-activeForeground': '#ffffff', - '--vscode-tab-inactiveForeground': '#ffffff', - '--vscode-list-activeSelectionBackground': '#000000', - '--vscode-list-activeSelectionForeground': '#ffffff', - '--vscode-list-inactiveSelectionBackground': '#000000', - '--vscode-list-inactiveSelectionForeground': '#ffffff', - '--vscode-terminal-background': '#000000', - '--vscode-terminal-foreground': '#ffffff', - '--vscode-badge-background': '#000000', - '--vscode-badge-foreground': '#ffffff', - '--vscode-errorForeground': '#f48771', - '--vscode-input-background': '#000000', - '--vscode-input-border': '#6fc3df', - '--vscode-button-background': '#000000', - '--vscode-button-foreground': '#ffffff', - '--vscode-button-hoverBackground': '#000000', - '--vscode-textLink-foreground': '#3794ff', - '--vscode-terminal-ansiBlack': '#000000', - '--vscode-terminal-ansiRed': '#cd3131', - '--vscode-terminal-ansiGreen': '#0dbc79', - '--vscode-terminal-ansiYellow': '#e5e510', - '--vscode-terminal-ansiBlue': '#2472c8', - '--vscode-terminal-ansiMagenta': '#bc3fbc', - '--vscode-terminal-ansiCyan': '#11a8cd', - '--vscode-terminal-ansiWhite': '#e5e5e5', - '--vscode-terminal-ansiBrightBlack': '#666666', - '--vscode-terminal-ansiBrightRed': '#f14c4c', - '--vscode-terminal-ansiBrightGreen': '#23d18b', - '--vscode-terminal-ansiBrightYellow': '#f5f543', - '--vscode-terminal-ansiBrightBlue': '#3b8eea', - '--vscode-terminal-ansiBrightMagenta': '#d670d6', - '--vscode-terminal-ansiBrightCyan': '#29b8db', - '--vscode-terminal-ansiBrightWhite': '#e5e5e5', - '--vscode-terminalCursor-foreground': '#ffffff', - '--vscode-terminal-selectionBackground': '#ffffff40', - }, - - 'High Contrast Light': { - '--vscode-editor-background': '#ffffff', - '--vscode-editor-foreground': '#292929', - '--vscode-sideBar-background': '#ffffff', - '--vscode-editorWidget-background': '#f0f0f0', - '--vscode-descriptionForeground': '#292929', - '--vscode-focusBorder': '#0090f1', - '--vscode-panel-border': '#292929', - '--vscode-tab-activeBackground': '#ffffff', - '--vscode-tab-inactiveBackground': '#ffffff', - '--vscode-tab-activeForeground': '#292929', - '--vscode-tab-inactiveForeground': '#292929', - '--vscode-list-activeSelectionBackground': '#0f4a85', - '--vscode-list-activeSelectionForeground': '#ffffff', - '--vscode-list-inactiveSelectionBackground': '#c8ddf3', - '--vscode-list-inactiveSelectionForeground': '#292929', - '--vscode-terminal-background': '#ffffff', - '--vscode-terminal-foreground': '#292929', - '--vscode-badge-background': '#0f4a85', - '--vscode-badge-foreground': '#ffffff', - '--vscode-errorForeground': '#b5200d', - '--vscode-input-background': '#ffffff', - '--vscode-input-border': '#292929', - '--vscode-button-background': '#0f4a85', - '--vscode-button-foreground': '#ffffff', - '--vscode-button-hoverBackground': '#264f78', - '--vscode-textLink-foreground': '#0f4a85', - '--vscode-terminal-ansiBlack': '#292929', - '--vscode-terminal-ansiRed': '#b5200d', - '--vscode-terminal-ansiGreen': '#1a7f37', - '--vscode-terminal-ansiYellow': '#6c6c00', - '--vscode-terminal-ansiBlue': '#0451a5', - '--vscode-terminal-ansiMagenta': '#8b2252', - '--vscode-terminal-ansiCyan': '#0598bc', - '--vscode-terminal-ansiWhite': '#d6d6d6', - '--vscode-terminal-ansiBrightBlack': '#666666', - '--vscode-terminal-ansiBrightRed': '#cd3131', - '--vscode-terminal-ansiBrightGreen': '#14ce14', - '--vscode-terminal-ansiBrightYellow': '#b5ba00', - '--vscode-terminal-ansiBrightBlue': '#0451a5', - '--vscode-terminal-ansiBrightMagenta': '#bc05bc', - '--vscode-terminal-ansiBrightCyan': '#0598bc', - '--vscode-terminal-ansiBrightWhite': '#a5a5a5', - '--vscode-terminalCursor-foreground': '#292929', - '--vscode-terminal-selectionBackground': '#0f4a8540', - }, -}; +export const VSCODE_THEMES: Record> = {}; +for (const theme of bundled) { + VSCODE_THEMES[theme.label] = theme.vars; +} diff --git a/lib/package.json b/lib/package.json index b28769a..3e9eda4 100644 --- a/lib/package.json +++ b/lib/package.json @@ -21,6 +21,8 @@ "dockview-react": "^5.1.0", "react": "^19.2.0", "react-dom": "^19.2.0", + "fflate": "0.8.2", + "jsonc-parser": "3.3.1", "tailwind-merge": "^3.5.0", "tailwind-variants": "^3.2.2" }, diff --git a/lib/scripts/bundle-themes.mjs b/lib/scripts/bundle-themes.mjs new file mode 100644 index 0000000..d1e6f00 --- /dev/null +++ b/lib/scripts/bundle-themes.mjs @@ -0,0 +1,175 @@ +#!/usr/bin/env node +/** + * Download VSCode theme extensions from OpenVSX, extract theme JSONs, + * and write lib/src/lib/themes/bundled.json. + * + * Usage: node scripts/bundle-themes.mjs + * + * Output is checked into git so builds don't require network access. + */ + +import { writeFileSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { unzipSync } from 'fflate'; +import { parse as parseJsonc } from 'jsonc-parser'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const OUTPUT = resolve(__dirname, '../src/lib/themes/bundled.json'); +const OUTPUT_EXTENSIONS = resolve(__dirname, '../src/lib/themes/bundled-extensions.json'); + +/** Extensions to download from OpenVSX. */ +const EXTENSIONS = [ + { namespace: 'GitHub', name: 'github-vscode-theme' }, + { namespace: 'santoso-wijaya', name: 'helios-selene' }, + { namespace: 'jdinhlife', name: 'gruvbox' }, + { namespace: 'cocopon', name: 'iceberg-theme' }, +]; + +const PREFERRED_THEME_ORDER = [ + 'GitHub.github-vscode-theme.github-dark-default', +]; + +/** Theme IDs to exclude (e.g. legacy/classic variants). */ +const EXCLUDED_THEMES = new Set([ + 'GitHub.github-vscode-theme.github-light', + 'GitHub.github-vscode-theme.github-dark', +]); + +/** + * VSCode theme color keys consumed by MouseTerm. + * Keep in sync with lib/src/lib/themes/convert.ts. + */ +const CONSUMED_KEYS = new Set([ + 'editor.background', 'editorGroupHeader.tabsBackground', 'sideBar.background', + 'editorWidget.background', 'editor.foreground', 'descriptionForeground', + 'focusBorder', 'panel.border', + 'tab.activeBackground', 'tab.inactiveBackground', 'tab.activeForeground', + 'tab.inactiveForeground', 'list.activeSelectionBackground', 'list.activeSelectionForeground', + 'terminal.background', 'terminal.foreground', 'badge.background', 'badge.foreground', + 'errorForeground', 'editorWarning.foreground', + 'input.background', 'input.border', + 'button.background', 'button.foreground', 'button.hoverBackground', + 'textLink.foreground', + 'terminalCursor.foreground', 'terminal.selectionBackground', + 'terminal.ansiBlack', 'terminal.ansiRed', 'terminal.ansiGreen', 'terminal.ansiYellow', + 'terminal.ansiBlue', 'terminal.ansiMagenta', 'terminal.ansiCyan', 'terminal.ansiWhite', + 'terminal.ansiBrightBlack', 'terminal.ansiBrightRed', 'terminal.ansiBrightGreen', + 'terminal.ansiBrightYellow', 'terminal.ansiBrightBlue', 'terminal.ansiBrightMagenta', + 'terminal.ansiBrightCyan', 'terminal.ansiBrightWhite', +]); + +function convertColors(colors) { + const vars = {}; + for (const [key, value] of Object.entries(colors)) { + if (CONSUMED_KEYS.has(key)) { + vars[`--vscode-${key.replace(/\./g, '-')}`] = value; + } + } + return vars; +} + +function uiThemeToType(uiTheme) { + return uiTheme === 'vs' || uiTheme === 'hc-light' ? 'light' : 'dark'; +} + +function slugify(label) { + return label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); +} + +/** Read a UTF-8 file from the unzipped VSIX entries. */ +function readEntry(entries, path) { + // VSIX entries are prefixed with "extension/" + const key = `extension/${path}`; + const data = entries[key]; + if (!data) throw new Error(`Missing ${key} in VSIX`); + return new TextDecoder().decode(data); +} + +async function fetchExtensionThemes(namespace, name) { + console.log(`Fetching ${namespace}/${name} from OpenVSX...`); + + // Get latest version metadata + const metaRes = await fetch(`https://open-vsx.org/api/${namespace}/${name}/latest`); + if (!metaRes.ok) throw new Error(`OpenVSX metadata failed: ${metaRes.status}`); + const meta = await metaRes.json(); + + const downloadUrl = meta.files?.download; + if (!downloadUrl) throw new Error(`No download URL for ${namespace}/${name}`); + + console.log(` Downloading VSIX (v${meta.version})...`); + const vsixRes = await fetch(downloadUrl); + if (!vsixRes.ok) throw new Error(`VSIX download failed: ${vsixRes.status}`); + const vsixBuf = new Uint8Array(await vsixRes.arrayBuffer()); + + console.log(` Extracting...`); + const entries = unzipSync(vsixBuf); + + // Read package.json to find theme contributions + const pkgJson = JSON.parse(readEntry(entries, 'package.json')); + const themeContribs = pkgJson.contributes?.themes ?? []; + + const themes = []; + for (const contrib of themeContribs) { + const themePath = contrib.path.replace(/^\.\//, ''); + console.log(` Converting ${contrib.label} (${themePath})...`); + + const themeJson = parseJsonc(readEntry(entries, themePath)); + const colors = themeJson.colors ?? {}; + const vars = convertColors(colors); + const type = uiThemeToType(contrib.uiTheme ?? themeJson.type ?? 'vs-dark'); + + themes.push({ + id: `${namespace}.${name}.${slugify(contrib.label)}`, + label: contrib.label, + type, + swatch: colors['editor.background'] ?? (type === 'light' ? '#ffffff' : '#1e1e1e'), + accent: colors['focusBorder'] ?? (type === 'light' ? '#0090f1' : '#007fd4'), + vars, + origin: { kind: 'bundled' }, + }); + } + + console.log(` Found ${themes.length} theme(s).`); + return { + themes, + extension: { + name: meta.displayName ?? `${namespace}/${name}`, + version: meta.version, + license: meta.license ?? null, + author: meta.publishedBy?.loginName ?? null, + homepage: meta.homepage ?? meta.repository ?? null, + }, + }; +} + +async function main() { + const allThemes = []; + const allExtensions = []; + + for (const ext of EXTENSIONS) { + const { themes, extension } = await fetchExtensionThemes(ext.namespace, ext.name); + allThemes.push(...themes.filter(t => !EXCLUDED_THEMES.has(t.id))); + allExtensions.push(extension); + } + allThemes.sort((a, b) => { + const aIndex = PREFERRED_THEME_ORDER.indexOf(a.id); + const bIndex = PREFERRED_THEME_ORDER.indexOf(b.id); + if (aIndex === -1 && bIndex === -1) return 0; + if (aIndex === -1) return 1; + if (bIndex === -1) return -1; + return aIndex - bIndex; + }); + + console.log(`\nWriting ${allThemes.length} themes to ${OUTPUT}`); + writeFileSync(OUTPUT, JSON.stringify(allThemes, null, 2) + '\n'); + + console.log(`Writing ${allExtensions.length} extensions to ${OUTPUT_EXTENSIONS}`); + writeFileSync(OUTPUT_EXTENSIONS, JSON.stringify(allExtensions, null, 2) + '\n'); + console.log('Done.'); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/lib/src/components/Door.tsx b/lib/src/components/Door.tsx index 95b1b68..9c2a8ad 100644 --- a/lib/src/components/Door.tsx +++ b/lib/src/components/Door.tsx @@ -40,7 +40,7 @@ export function Door({ 'transition-colors hover:bg-surface-raised', ].join(' ')} style={{ - border: '2px solid var(--mt-border)', + border: '2px solid var(--color-border)', borderBottom: '2px solid transparent', }} onClick={onClick} diff --git a/lib/src/components/Pond.tsx b/lib/src/components/Pond.tsx index 124bdc3..5622f44 100644 --- a/lib/src/components/Pond.tsx +++ b/lib/src/components/Pond.tsx @@ -704,7 +704,7 @@ function useWindowFocused(): boolean { } function readSelectionColor() { - return getComputedStyle(document.documentElement).getPropertyValue('--mt-selection-terminal').trim(); + return getComputedStyle(document.documentElement).getPropertyValue('--color-accent').trim(); } function useSelectionColor() { diff --git a/lib/src/components/ThemePicker.tsx b/lib/src/components/ThemePicker.tsx new file mode 100644 index 0000000..6cd02fe --- /dev/null +++ b/lib/src/components/ThemePicker.tsx @@ -0,0 +1,444 @@ +import { useCallback, useEffect, useId, useRef, useState, type RefObject } from 'react'; +import { CaretDownIcon } from '@phosphor-icons/react'; +import type { MouseTermTheme, OpenVSXExtension } from '../lib/themes'; +import { + addInstalledTheme, + applyTheme, + fetchExtensionThemes, + getActiveThemeId, + getAllThemes, + getInstalledThemes, + getTheme, + removeInstalledTheme, + searchThemes, + setActiveThemeId, +} from '../lib/themes'; + +export type ThemePickerVariant = 'playground-header' | 'standalone-appbar'; + +export interface ThemePickerProps { + variant: ThemePickerVariant; + className?: string; +} + +const styles = { + muted: { color: 'var(--vscode-descriptionForeground, #858585)' }, + foreground: { color: 'var(--vscode-editor-foreground, #cccccc)' }, + trigger: (open: boolean) => ({ + backgroundColor: 'var(--vscode-input-background, #3c3c3c)', + borderColor: open ? 'var(--vscode-focusBorder, #007fd4)' : 'var(--vscode-input-border, #3c3c3c)', + color: 'var(--vscode-editor-foreground, #cccccc)', + }), + panel: { + backgroundColor: 'var(--vscode-editorWidget-background, #252526)', + borderColor: 'var(--vscode-panel-border, #2b2b2b)', + color: 'var(--vscode-editor-foreground, #cccccc)', + boxShadow: '0 12px 32px rgba(0, 0, 0, 0.35)', + }, + border: { borderColor: 'var(--vscode-panel-border, #2b2b2b)' }, + activeRow: { + backgroundColor: 'var(--vscode-list-activeSelectionBackground, #094771)', + color: 'var(--vscode-list-activeSelectionForeground, #ffffff)', + }, + link: { color: 'var(--vscode-textLink-foreground, var(--vscode-focusBorder, #3794ff))' }, + error: { color: 'var(--vscode-errorForeground, #f48771)' }, + button: { + backgroundColor: 'var(--vscode-button-background, #0e639c)', + color: 'var(--vscode-button-foreground, #ffffff)', + }, +}; + +function applyActiveThemeFallback(): MouseTermTheme | null { + const allThemes = getAllThemes(); + const theme = getTheme(getActiveThemeId()) ?? allThemes[0]; + if (!theme) return null; + setActiveThemeId(theme.id); + applyTheme(theme); + return theme; +} + +function ThemeSwatch({ theme, size }: { theme: MouseTermTheme; size: 'sm' | 'md' }) { + const swatchClass = size === 'sm' ? 'h-3.5 w-3.5' : 'h-4 w-4'; + return ( + + + + + ); +} + +function useCloseOnOutsideAndEscape(open: boolean, ref: RefObject, onClose: () => void) { + useEffect(() => { + if (!open) return; + + const closeOnPointerDown = (event: PointerEvent) => { + if (ref.current && !ref.current.contains(event.target as Node)) onClose(); + }; + const closeOnEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') onClose(); + }; + + window.addEventListener('pointerdown', closeOnPointerDown, true); + window.addEventListener('keydown', closeOnEscape); + return () => { + window.removeEventListener('pointerdown', closeOnPointerDown, true); + window.removeEventListener('keydown', closeOnEscape); + }; + }, [open, ref, onClose]); +} + +function ThemeStoreDialog({ + open, + onClose, + onThemesChanged, +}: { + open: boolean; + onClose: () => void; + onThemesChanged: () => void; +}) { + const [query, setQuery] = useState(''); + const [results, setResults] = useState([]); + const [loading, setLoading] = useState(false); + const [installing, setInstalling] = useState(null); + const [error, setError] = useState(null); + const debounceRef = useRef | null>(null); + const dialogRef = useRef(null); + + useEffect(() => { + const dialog = dialogRef.current; + if (!dialog) return; + if (open && !dialog.open) dialog.showModal(); + if (!open && dialog.open) dialog.close(); + }, [open]); + + useEffect(() => { + return () => { + if (debounceRef.current) clearTimeout(debounceRef.current); + }; + }, []); + + const doSearch = useCallback(async (value: string) => { + if (!value.trim()) { + setResults([]); + return; + } + setLoading(true); + setError(null); + try { + const response = await searchThemes(value, 0, 20); + setResults(response.extensions); + } catch (reason) { + setError(reason instanceof Error ? reason.message : 'Search failed'); + } finally { + setLoading(false); + } + }, []); + + const handleInput = (value: string) => { + setQuery(value); + if (debounceRef.current) clearTimeout(debounceRef.current); + debounceRef.current = setTimeout(() => doSearch(value), 300); + }; + + const handleInstall = async (extension: OpenVSXExtension) => { + const key = `${extension.namespace}/${extension.name}`; + setInstalling(key); + setError(null); + try { + const themes = await fetchExtensionThemes(extension.namespace, extension.name); + for (const theme of themes) addInstalledTheme(theme); + if (themes[0]) { + setActiveThemeId(themes[0].id); + applyTheme(themes[0]); + } + onThemesChanged(); + } catch (reason) { + setError(reason instanceof Error ? reason.message : 'Install failed'); + } finally { + setInstalling(null); + } + }; + + const handleRemoveExtension = (extensionId: string) => { + const confirmed = window.confirm(`Remove installed themes from ${extensionId}?`); + if (!confirmed) return; + + for (const theme of getInstalledThemes()) { + if (theme.origin.kind === 'installed' && theme.origin.extensionId === extensionId) { + removeInstalledTheme(theme.id); + } + } + applyActiveThemeFallback(); + onThemesChanged(); + }; + + const isInstalled = (extension: OpenVSXExtension) => { + const key = `${extension.namespace}/${extension.name}`; + return getInstalledThemes().some( + (theme) => theme.origin.kind === 'installed' && theme.origin.extensionId === key, + ); + }; + + if (!open) return null; + + return ( + +
+
+ Install theme from OpenVSX + +
+ +
+ handleInput(event.target.value)} + placeholder="Search themes..." + autoFocus + className="w-full rounded border px-3 py-1.5 text-xs outline-none placeholder:opacity-65" + style={styles.trigger(false)} + /> +
+ +
+ {error ? ( +
+ {error} +
+ ) : null} + {loading ?
Searching...
: null} + {!loading && results.length === 0 && query.trim() ? ( +
No themes found
+ ) : null} + {!loading && !query.trim() ? ( +
+ Search for a VS Code theme to install +
+ ) : null} + {results.map((extension) => { + const key = `${extension.namespace}/${extension.name}`; + const installed = isInstalled(extension); + const isInstallingThis = installing === key; + return ( +
+ {extension.files?.icon ? ( + + ) : ( +
+ VS +
+ )} +
+
{extension.displayName || extension.name}
+
+ {extension.namespace} - {extension.downloadCount.toLocaleString()} downloads +
+
+ {installed ? ( + + ) : ( + + )} +
+ ); + })} +
+
+
+ ); +} + +export function ThemePicker({ variant, className = '' }: ThemePickerProps) { + const labelId = useId(); + const currentId = useId(); + const [themes, setThemes] = useState(getAllThemes); + const [activeId, setActiveId] = useState(() => getAllThemes()[0]?.id ?? ''); + const [open, setOpen] = useState(false); + const [storeOpen, setStoreOpen] = useState(false); + const rootRef = useRef(null); + + const isPlayground = variant === 'playground-header'; + const activeTheme = themes.find((theme) => theme.id === activeId) ?? themes[0]; + + useEffect(() => { + const theme = applyActiveThemeFallback(); + if (theme) setActiveId(theme.id); + setThemes(getAllThemes()); + }, []); + + const closeDropdown = useCallback(() => setOpen(false), []); + useCloseOnOutsideAndEscape(open, rootRef, closeDropdown); + + const refreshThemes = useCallback(() => { + setThemes(getAllThemes()); + const theme = applyActiveThemeFallback(); + if (theme) setActiveId(theme.id); + }, []); + + const selectTheme = (id: string) => { + const theme = getTheme(id); + if (!theme) return; + setActiveThemeId(id); + setActiveId(id); + applyTheme(theme); + setOpen(false); + }; + + const deleteTheme = (theme: MouseTermTheme) => { + if (theme.origin.kind !== 'installed') return; + const confirmed = window.confirm(`Delete "${theme.label}"?`); + if (!confirmed) return; + + removeInstalledTheme(theme.id); + setThemes(getAllThemes()); + + if (theme.id === activeId) { + const fallback = applyActiveThemeFallback(); + if (fallback) setActiveId(fallback.id); + } + }; + + const rootClass = isPlayground + ? 'relative flex min-w-0 items-center gap-1.5 text-xs' + : 'relative flex items-center'; + const triggerClass = isPlayground + ? 'flex h-8 w-[116px] min-w-0 items-center gap-2 rounded border px-2 text-left text-[12px] transition-colors sm:w-40 md:w-56' + : 'flex h-6 max-w-[190px] items-center gap-1.5 rounded border border-transparent px-2 text-xs transition-colors hover:opacity-85'; + const menuClass = isPlayground + ? 'fixed top-16 right-4 left-4 z-50 overflow-hidden rounded border shadow-2xl md:absolute md:top-full md:right-0 md:left-auto md:mt-2 md:w-[22rem]' + : 'absolute right-0 top-full z-50 mt-1 w-[280px] overflow-hidden rounded border shadow-2xl'; + const rowButtonClass = isPlayground + ? 'flex min-w-0 flex-1 items-center gap-2 px-3 py-2 text-left text-xs' + : 'flex min-w-0 flex-1 items-center gap-2 px-3 py-1.5 text-left text-xs'; + const swatchSize = isPlayground ? 'md' : 'sm'; + + return ( +
+ {isPlayground ? ( + + Theme: + + ) : null} + + + + {open ? ( +
+
+ {themes.map((theme) => { + const isActive = theme.id === activeId; + const isInstalled = theme.origin.kind === 'installed'; + return ( +
+ + {isInstalled ? ( + + ) : null} +
+ ); + })} +
+ +
+ +
+
+ ) : null} + + setStoreOpen(false)} onThemesChanged={refreshThemes} /> +
+ ); +} diff --git a/lib/src/index.css b/lib/src/index.css index dd0a8ba..33022a3 100644 --- a/lib/src/index.css +++ b/lib/src/index.css @@ -26,17 +26,17 @@ body { .dockview-theme-abyss { --dv-tabs-and-actions-container-height: 30px !important; --dv-tabs-and-actions-container-font-size: var(--mt-font-size) !important; - --dv-group-view-background-color: var(--mt-surface) !important; - --dv-tabs-and-actions-container-background-color: var(--mt-surface-alt) !important; - --dv-activegroup-visiblepanel-tab-background-color: var(--mt-tab-active-bg) !important; - --dv-activegroup-hiddenpanel-tab-background-color: var(--mt-tab-inactive-bg) !important; - --dv-inactivegroup-visiblepanel-tab-background-color: var(--mt-tab-active-bg) !important; - --dv-inactivegroup-hiddenpanel-tab-background-color: var(--mt-tab-inactive-bg) !important; - --dv-activegroup-visiblepanel-tab-color: var(--mt-tab-active-fg) !important; - --dv-activegroup-hiddenpanel-tab-color: var(--mt-tab-inactive-fg) !important; - --dv-inactivegroup-visiblepanel-tab-color: var(--mt-tab-active-fg) !important; - --dv-inactivegroup-hiddenpanel-tab-color: var(--mt-tab-inactive-fg) !important; - --dv-tab-divider-color: var(--mt-border) !important; + --dv-group-view-background-color: var(--color-surface) !important; + --dv-tabs-and-actions-container-background-color: var(--color-surface-alt) !important; + --dv-activegroup-visiblepanel-tab-background-color: var(--color-tab-active-bg) !important; + --dv-activegroup-hiddenpanel-tab-background-color: var(--color-tab-inactive-bg) !important; + --dv-inactivegroup-visiblepanel-tab-background-color: var(--color-tab-active-bg) !important; + --dv-inactivegroup-hiddenpanel-tab-background-color: var(--color-tab-inactive-bg) !important; + --dv-activegroup-visiblepanel-tab-color: var(--color-tab-active-fg) !important; + --dv-activegroup-hiddenpanel-tab-color: var(--color-tab-inactive-fg) !important; + --dv-inactivegroup-visiblepanel-tab-color: var(--color-tab-active-fg) !important; + --dv-inactivegroup-hiddenpanel-tab-color: var(--color-tab-inactive-fg) !important; + --dv-tab-divider-color: var(--color-border) !important; --dv-separator-border: transparent !important; --dv-active-sash-color: transparent !important; } @@ -45,7 +45,7 @@ body { .dockview-theme-abyss .dv-groupview, .dockview-theme-abyss .dv-split-view-container, .dockview-theme-abyss .dv-split-view-container .dv-view-container { - background-color: var(--mt-surface) !important; + background-color: var(--color-surface) !important; } /* Remove tab-bar background/border — the tab itself provides the bg */ diff --git a/lib/src/lib/themes/apply.ts b/lib/src/lib/themes/apply.ts new file mode 100644 index 0000000..c720b8f --- /dev/null +++ b/lib/src/lib/themes/apply.ts @@ -0,0 +1,37 @@ +import type { MouseTermTheme } from './types'; + +/** Previously applied variable names — tracked for cleanup. */ +let appliedVarNames: string[] = []; + +/** + * Apply a theme by setting --vscode-* CSS variables on document.body. + * + * Also manages body classes (vscode-light / vscode-dark) so that + * theme.css fallback selectors activate correctly. + * + * The MutationObserver in terminal-registry.ts detects the style change + * and re-reads the theme for all xterm.js terminals. + */ +export function applyTheme(theme: MouseTermTheme): void { + if (typeof document === 'undefined') return; + + // Clear previously applied variables + for (const name of appliedVarNames) { + document.body.style.removeProperty(name); + } + + // Apply new variables + appliedVarNames = Object.keys(theme.vars); + for (const [name, value] of Object.entries(theme.vars)) { + document.body.style.setProperty(name, value); + } + + // Set body class for light/dark so theme.css fallbacks work + if (theme.type === 'light') { + document.body.classList.add('vscode-light'); + document.body.classList.remove('vscode-dark'); + } else { + document.body.classList.add('vscode-dark'); + document.body.classList.remove('vscode-light'); + } +} diff --git a/lib/src/lib/themes/bundled-extensions.json b/lib/src/lib/themes/bundled-extensions.json new file mode 100644 index 0000000..af3ffc6 --- /dev/null +++ b/lib/src/lib/themes/bundled-extensions.json @@ -0,0 +1,30 @@ +[ + { + "name": "GitHub Theme", + "version": "6.3.5", + "license": "MIT", + "author": "open-vsx", + "homepage": "https://github.com/primer/github-vscode-theme#readme" + }, + { + "name": "Solarized & Selenized", + "version": "0.3.18", + "license": null, + "author": "santoso-wijaya", + "homepage": "https://github.com/santoso-wijaya/vscode-helios-selene" + }, + { + "name": "Gruvbox Theme", + "version": "1.29.1", + "license": "MIT", + "author": "jdinhify", + "homepage": "https://github.com/jdinhify/vscode-theme-gruvbox" + }, + { + "name": "Iceberg Theme", + "version": "2.0.5", + "license": "MIT", + "author": "cocopon", + "homepage": "https://cocopon.github.io/iceberg.vim/" + } +] diff --git a/lib/src/lib/themes/bundled.json b/lib/src/lib/themes/bundled.json new file mode 100644 index 0000000..d25f869 --- /dev/null +++ b/lib/src/lib/themes/bundled.json @@ -0,0 +1,984 @@ +[ + { + "id": "GitHub.github-vscode-theme.github-dark-default", + "label": "GitHub Dark Default", + "type": "dark", + "swatch": "#0d1117", + "accent": "#1f6feb", + "vars": { + "--vscode-focusBorder": "#1f6feb", + "--vscode-descriptionForeground": "#7d8590", + "--vscode-errorForeground": "#f85149", + "--vscode-textLink-foreground": "#2f81f7", + "--vscode-button-background": "#238636", + "--vscode-button-foreground": "#ffffff", + "--vscode-button-hoverBackground": "#2ea043", + "--vscode-input-background": "#0d1117", + "--vscode-input-border": "#30363d", + "--vscode-badge-foreground": "#ffffff", + "--vscode-badge-background": "#1f6feb", + "--vscode-sideBar-background": "#010409", + "--vscode-list-activeSelectionForeground": "#e6edf3", + "--vscode-list-activeSelectionBackground": "#6e768166", + "--vscode-editorGroupHeader-tabsBackground": "#010409", + "--vscode-tab-activeForeground": "#e6edf3", + "--vscode-tab-inactiveForeground": "#7d8590", + "--vscode-tab-inactiveBackground": "#010409", + "--vscode-tab-activeBackground": "#0d1117", + "--vscode-editor-foreground": "#e6edf3", + "--vscode-editor-background": "#0d1117", + "--vscode-editorWidget-background": "#161b22", + "--vscode-panel-border": "#30363d", + "--vscode-terminal-foreground": "#e6edf3", + "--vscode-terminal-ansiBlack": "#484f58", + "--vscode-terminal-ansiRed": "#ff7b72", + "--vscode-terminal-ansiGreen": "#3fb950", + "--vscode-terminal-ansiYellow": "#d29922", + "--vscode-terminal-ansiBlue": "#58a6ff", + "--vscode-terminal-ansiMagenta": "#bc8cff", + "--vscode-terminal-ansiCyan": "#39c5cf", + "--vscode-terminal-ansiWhite": "#b1bac4", + "--vscode-terminal-ansiBrightBlack": "#6e7681", + "--vscode-terminal-ansiBrightRed": "#ffa198", + "--vscode-terminal-ansiBrightGreen": "#56d364", + "--vscode-terminal-ansiBrightYellow": "#e3b341", + "--vscode-terminal-ansiBrightBlue": "#79c0ff", + "--vscode-terminal-ansiBrightMagenta": "#d2a8ff", + "--vscode-terminal-ansiBrightCyan": "#56d4dd", + "--vscode-terminal-ansiBrightWhite": "#ffffff" + }, + "origin": { + "kind": "bundled" + } + }, + { + "id": "GitHub.github-vscode-theme.github-light-default", + "label": "GitHub Light Default", + "type": "light", + "swatch": "#ffffff", + "accent": "#0969da", + "vars": { + "--vscode-focusBorder": "#0969da", + "--vscode-descriptionForeground": "#656d76", + "--vscode-errorForeground": "#cf222e", + "--vscode-textLink-foreground": "#0969da", + "--vscode-button-background": "#1f883d", + "--vscode-button-foreground": "#ffffff", + "--vscode-button-hoverBackground": "#1a7f37", + "--vscode-input-background": "#ffffff", + "--vscode-input-border": "#d0d7de", + "--vscode-badge-foreground": "#ffffff", + "--vscode-badge-background": "#0969da", + "--vscode-sideBar-background": "#f6f8fa", + "--vscode-list-activeSelectionForeground": "#1f2328", + "--vscode-list-activeSelectionBackground": "#afb8c133", + "--vscode-editorGroupHeader-tabsBackground": "#f6f8fa", + "--vscode-tab-activeForeground": "#1f2328", + "--vscode-tab-inactiveForeground": "#656d76", + "--vscode-tab-inactiveBackground": "#f6f8fa", + "--vscode-tab-activeBackground": "#ffffff", + "--vscode-editor-foreground": "#1f2328", + "--vscode-editor-background": "#ffffff", + "--vscode-editorWidget-background": "#ffffff", + "--vscode-panel-border": "#d0d7de", + "--vscode-terminal-foreground": "#1f2328", + "--vscode-terminal-ansiBlack": "#24292f", + "--vscode-terminal-ansiRed": "#cf222e", + "--vscode-terminal-ansiGreen": "#116329", + "--vscode-terminal-ansiYellow": "#4d2d00", + "--vscode-terminal-ansiBlue": "#0969da", + "--vscode-terminal-ansiMagenta": "#8250df", + "--vscode-terminal-ansiCyan": "#1b7c83", + "--vscode-terminal-ansiWhite": "#6e7781", + "--vscode-terminal-ansiBrightBlack": "#57606a", + "--vscode-terminal-ansiBrightRed": "#a40e26", + "--vscode-terminal-ansiBrightGreen": "#1a7f37", + "--vscode-terminal-ansiBrightYellow": "#633c01", + "--vscode-terminal-ansiBrightBlue": "#218bff", + "--vscode-terminal-ansiBrightMagenta": "#a475f9", + "--vscode-terminal-ansiBrightCyan": "#3192aa", + "--vscode-terminal-ansiBrightWhite": "#8c959f" + }, + "origin": { + "kind": "bundled" + } + }, + { + "id": "GitHub.github-vscode-theme.github-light-high-contrast", + "label": "GitHub Light High Contrast", + "type": "light", + "swatch": "#ffffff", + "accent": "#0349b4", + "vars": { + "--vscode-focusBorder": "#0349b4", + "--vscode-descriptionForeground": "#0e1116", + "--vscode-errorForeground": "#a0111f", + "--vscode-textLink-foreground": "#0349b4", + "--vscode-button-background": "#055d20", + "--vscode-button-foreground": "#ffffff", + "--vscode-button-hoverBackground": "#024c1a", + "--vscode-input-background": "#ffffff", + "--vscode-input-border": "#20252c", + "--vscode-badge-foreground": "#ffffff", + "--vscode-badge-background": "#0349b4", + "--vscode-sideBar-background": "#ffffff", + "--vscode-list-activeSelectionForeground": "#0e1116", + "--vscode-list-activeSelectionBackground": "#acb6c033", + "--vscode-editorGroupHeader-tabsBackground": "#ffffff", + "--vscode-tab-activeForeground": "#0e1116", + "--vscode-tab-inactiveForeground": "#0e1116", + "--vscode-tab-inactiveBackground": "#ffffff", + "--vscode-tab-activeBackground": "#ffffff", + "--vscode-editor-foreground": "#0e1116", + "--vscode-editor-background": "#ffffff", + "--vscode-editorWidget-background": "#ffffff", + "--vscode-panel-border": "#20252c", + "--vscode-terminal-foreground": "#0e1116", + "--vscode-terminal-ansiBlack": "#0e1116", + "--vscode-terminal-ansiRed": "#a0111f", + "--vscode-terminal-ansiGreen": "#024c1a", + "--vscode-terminal-ansiYellow": "#3f2200", + "--vscode-terminal-ansiBlue": "#0349b4", + "--vscode-terminal-ansiMagenta": "#622cbc", + "--vscode-terminal-ansiCyan": "#1b7c83", + "--vscode-terminal-ansiWhite": "#66707b", + "--vscode-terminal-ansiBrightBlack": "#4b535d", + "--vscode-terminal-ansiBrightRed": "#86061d", + "--vscode-terminal-ansiBrightGreen": "#055d20", + "--vscode-terminal-ansiBrightYellow": "#4e2c00", + "--vscode-terminal-ansiBrightBlue": "#1168e3", + "--vscode-terminal-ansiBrightMagenta": "#844ae7", + "--vscode-terminal-ansiBrightCyan": "#3192aa", + "--vscode-terminal-ansiBrightWhite": "#88929d" + }, + "origin": { + "kind": "bundled" + } + }, + { + "id": "GitHub.github-vscode-theme.github-light-colorblind-beta", + "label": "GitHub Light Colorblind (Beta)", + "type": "light", + "swatch": "#ffffff", + "accent": "#0969da", + "vars": { + "--vscode-focusBorder": "#0969da", + "--vscode-descriptionForeground": "#57606a", + "--vscode-errorForeground": "#b35900", + "--vscode-textLink-foreground": "#0969da", + "--vscode-button-background": "#218bff", + "--vscode-button-foreground": "#ffffff", + "--vscode-button-hoverBackground": "#0969da", + "--vscode-input-background": "#ffffff", + "--vscode-input-border": "#d0d7de", + "--vscode-badge-foreground": "#ffffff", + "--vscode-badge-background": "#0969da", + "--vscode-sideBar-background": "#f6f8fa", + "--vscode-list-activeSelectionForeground": "#24292f", + "--vscode-list-activeSelectionBackground": "#afb8c133", + "--vscode-editorGroupHeader-tabsBackground": "#f6f8fa", + "--vscode-tab-activeForeground": "#24292f", + "--vscode-tab-inactiveForeground": "#57606a", + "--vscode-tab-inactiveBackground": "#f6f8fa", + "--vscode-tab-activeBackground": "#ffffff", + "--vscode-editor-foreground": "#24292f", + "--vscode-editor-background": "#ffffff", + "--vscode-editorWidget-background": "#ffffff", + "--vscode-panel-border": "#d0d7de", + "--vscode-terminal-foreground": "#24292f", + "--vscode-terminal-ansiBlack": "#24292f", + "--vscode-terminal-ansiRed": "#b35900", + "--vscode-terminal-ansiGreen": "#0550ae", + "--vscode-terminal-ansiYellow": "#4d2d00", + "--vscode-terminal-ansiBlue": "#0969da", + "--vscode-terminal-ansiMagenta": "#8250df", + "--vscode-terminal-ansiCyan": "#1b7c83", + "--vscode-terminal-ansiWhite": "#6e7781", + "--vscode-terminal-ansiBrightBlack": "#57606a", + "--vscode-terminal-ansiBrightRed": "#8a4600", + "--vscode-terminal-ansiBrightGreen": "#0969da", + "--vscode-terminal-ansiBrightYellow": "#633c01", + "--vscode-terminal-ansiBrightBlue": "#218bff", + "--vscode-terminal-ansiBrightMagenta": "#a475f9", + "--vscode-terminal-ansiBrightCyan": "#3192aa", + "--vscode-terminal-ansiBrightWhite": "#8c959f" + }, + "origin": { + "kind": "bundled" + } + }, + { + "id": "GitHub.github-vscode-theme.github-dark-high-contrast", + "label": "GitHub Dark High Contrast", + "type": "dark", + "swatch": "#0a0c10", + "accent": "#409eff", + "vars": { + "--vscode-focusBorder": "#409eff", + "--vscode-descriptionForeground": "#f0f3f6", + "--vscode-errorForeground": "#ff6a69", + "--vscode-textLink-foreground": "#71b7ff", + "--vscode-button-background": "#09b43a", + "--vscode-button-foreground": "#0a0c10", + "--vscode-button-hoverBackground": "#26cd4d", + "--vscode-input-background": "#0a0c10", + "--vscode-input-border": "#7a828e", + "--vscode-badge-foreground": "#0a0c10", + "--vscode-badge-background": "#409eff", + "--vscode-sideBar-background": "#010409", + "--vscode-list-activeSelectionForeground": "#f0f3f6", + "--vscode-list-activeSelectionBackground": "#9ea7b366", + "--vscode-editorGroupHeader-tabsBackground": "#010409", + "--vscode-tab-activeForeground": "#f0f3f6", + "--vscode-tab-inactiveForeground": "#f0f3f6", + "--vscode-tab-inactiveBackground": "#010409", + "--vscode-tab-activeBackground": "#0a0c10", + "--vscode-editor-foreground": "#f0f3f6", + "--vscode-editor-background": "#0a0c10", + "--vscode-editorWidget-background": "#272b33", + "--vscode-panel-border": "#7a828e", + "--vscode-terminal-foreground": "#f0f3f6", + "--vscode-terminal-ansiBlack": "#7a828e", + "--vscode-terminal-ansiRed": "#ff9492", + "--vscode-terminal-ansiGreen": "#26cd4d", + "--vscode-terminal-ansiYellow": "#f0b72f", + "--vscode-terminal-ansiBlue": "#71b7ff", + "--vscode-terminal-ansiMagenta": "#cb9eff", + "--vscode-terminal-ansiCyan": "#39c5cf", + "--vscode-terminal-ansiWhite": "#d9dee3", + "--vscode-terminal-ansiBrightBlack": "#9ea7b3", + "--vscode-terminal-ansiBrightRed": "#ffb1af", + "--vscode-terminal-ansiBrightGreen": "#4ae168", + "--vscode-terminal-ansiBrightYellow": "#f7c843", + "--vscode-terminal-ansiBrightBlue": "#91cbff", + "--vscode-terminal-ansiBrightMagenta": "#dbb7ff", + "--vscode-terminal-ansiBrightCyan": "#56d4dd", + "--vscode-terminal-ansiBrightWhite": "#ffffff" + }, + "origin": { + "kind": "bundled" + } + }, + { + "id": "GitHub.github-vscode-theme.github-dark-colorblind-beta", + "label": "GitHub Dark Colorblind (Beta)", + "type": "dark", + "swatch": "#0d1117", + "accent": "#1f6feb", + "vars": { + "--vscode-focusBorder": "#1f6feb", + "--vscode-descriptionForeground": "#8b949e", + "--vscode-errorForeground": "#d47616", + "--vscode-textLink-foreground": "#58a6ff", + "--vscode-button-background": "#1f6feb", + "--vscode-button-foreground": "#ffffff", + "--vscode-button-hoverBackground": "#388bfd", + "--vscode-input-background": "#0d1117", + "--vscode-input-border": "#30363d", + "--vscode-badge-foreground": "#ffffff", + "--vscode-badge-background": "#1f6feb", + "--vscode-sideBar-background": "#010409", + "--vscode-list-activeSelectionForeground": "#c9d1d9", + "--vscode-list-activeSelectionBackground": "#6e768166", + "--vscode-editorGroupHeader-tabsBackground": "#010409", + "--vscode-tab-activeForeground": "#c9d1d9", + "--vscode-tab-inactiveForeground": "#8b949e", + "--vscode-tab-inactiveBackground": "#010409", + "--vscode-tab-activeBackground": "#0d1117", + "--vscode-editor-foreground": "#c9d1d9", + "--vscode-editor-background": "#0d1117", + "--vscode-editorWidget-background": "#161b22", + "--vscode-panel-border": "#30363d", + "--vscode-terminal-foreground": "#c9d1d9", + "--vscode-terminal-ansiBlack": "#484f58", + "--vscode-terminal-ansiRed": "#ec8e2c", + "--vscode-terminal-ansiGreen": "#58a6ff", + "--vscode-terminal-ansiYellow": "#d29922", + "--vscode-terminal-ansiBlue": "#58a6ff", + "--vscode-terminal-ansiMagenta": "#bc8cff", + "--vscode-terminal-ansiCyan": "#39c5cf", + "--vscode-terminal-ansiWhite": "#b1bac4", + "--vscode-terminal-ansiBrightBlack": "#6e7681", + "--vscode-terminal-ansiBrightRed": "#fdac54", + "--vscode-terminal-ansiBrightGreen": "#79c0ff", + "--vscode-terminal-ansiBrightYellow": "#e3b341", + "--vscode-terminal-ansiBrightBlue": "#79c0ff", + "--vscode-terminal-ansiBrightMagenta": "#d2a8ff", + "--vscode-terminal-ansiBrightCyan": "#56d4dd", + "--vscode-terminal-ansiBrightWhite": "#ffffff" + }, + "origin": { + "kind": "bundled" + } + }, + { + "id": "GitHub.github-vscode-theme.github-dark-dimmed", + "label": "GitHub Dark Dimmed", + "type": "dark", + "swatch": "#22272e", + "accent": "#316dca", + "vars": { + "--vscode-focusBorder": "#316dca", + "--vscode-descriptionForeground": "#768390", + "--vscode-errorForeground": "#e5534b", + "--vscode-textLink-foreground": "#539bf5", + "--vscode-button-background": "#347d39", + "--vscode-button-foreground": "#ffffff", + "--vscode-button-hoverBackground": "#46954a", + "--vscode-input-background": "#22272e", + "--vscode-input-border": "#444c56", + "--vscode-badge-foreground": "#cdd9e5", + "--vscode-badge-background": "#316dca", + "--vscode-sideBar-background": "#1c2128", + "--vscode-list-activeSelectionForeground": "#adbac7", + "--vscode-list-activeSelectionBackground": "#636e7b66", + "--vscode-editorGroupHeader-tabsBackground": "#1c2128", + "--vscode-tab-activeForeground": "#adbac7", + "--vscode-tab-inactiveForeground": "#768390", + "--vscode-tab-inactiveBackground": "#1c2128", + "--vscode-tab-activeBackground": "#22272e", + "--vscode-editor-foreground": "#adbac7", + "--vscode-editor-background": "#22272e", + "--vscode-editorWidget-background": "#2d333b", + "--vscode-panel-border": "#444c56", + "--vscode-terminal-foreground": "#adbac7", + "--vscode-terminal-ansiBlack": "#545d68", + "--vscode-terminal-ansiRed": "#f47067", + "--vscode-terminal-ansiGreen": "#57ab5a", + "--vscode-terminal-ansiYellow": "#c69026", + "--vscode-terminal-ansiBlue": "#539bf5", + "--vscode-terminal-ansiMagenta": "#b083f0", + "--vscode-terminal-ansiCyan": "#39c5cf", + "--vscode-terminal-ansiWhite": "#909dab", + "--vscode-terminal-ansiBrightBlack": "#636e7b", + "--vscode-terminal-ansiBrightRed": "#ff938a", + "--vscode-terminal-ansiBrightGreen": "#6bc46d", + "--vscode-terminal-ansiBrightYellow": "#daaa3f", + "--vscode-terminal-ansiBrightBlue": "#6cb6ff", + "--vscode-terminal-ansiBrightMagenta": "#dcbdfb", + "--vscode-terminal-ansiBrightCyan": "#56d4dd", + "--vscode-terminal-ansiBrightWhite": "#cdd9e5" + }, + "origin": { + "kind": "bundled" + } + }, + { + "id": "santoso-wijaya.helios-selene.selenized-light", + "label": "Selenized Light", + "type": "light", + "swatch": "#fef3da", + "accent": "#c75d2099", + "vars": { + "--vscode-focusBorder": "#c75d2099", + "--vscode-errorForeground": "#d4212b", + "--vscode-button-background": "#b38800", + "--vscode-input-background": "#d6cbb4", + "--vscode-input-border": "#8f9894", + "--vscode-badge-background": "#b38800AA", + "--vscode-list-activeSelectionBackground": "#b3880088", + "--vscode-sideBar-background": "#f0e4cc", + "--vscode-editorGroupHeader-tabsBackground": "#d6cbb4", + "--vscode-tab-activeBackground": "#fef3da", + "--vscode-tab-activeForeground": "#384c52", + "--vscode-tab-inactiveForeground": "#52666d", + "--vscode-tab-inactiveBackground": "#d6cbb4", + "--vscode-editor-background": "#fef3da", + "--vscode-editor-foreground": "#384c52", + "--vscode-editorWidget-background": "#f0e4cc", + "--vscode-panel-border": "#8f9894", + "--vscode-terminal-background": "#fef3da", + "--vscode-terminal-foreground": "#384c52", + "--vscode-terminal-selectionBackground": "#f0e4cc", + "--vscode-terminalCursor-foreground": "#52666d", + "--vscode-terminal-ansiBlack": "#f0e4cc", + "--vscode-terminal-ansiRed": "#d4212b", + "--vscode-terminal-ansiGreen": "#539100", + "--vscode-terminal-ansiYellow": "#b38800", + "--vscode-terminal-ansiBlue": "#0073d2", + "--vscode-terminal-ansiMagenta": "#cb4c99", + "--vscode-terminal-ansiCyan": "#009c8f", + "--vscode-terminal-ansiWhite": "#52666d", + "--vscode-terminal-ansiBrightBlack": "#fef3da", + "--vscode-terminal-ansiBrightRed": "#c75d20", + "--vscode-terminal-ansiBrightGreen": "#539100", + "--vscode-terminal-ansiBrightYellow": "#b38800", + "--vscode-terminal-ansiBrightBlue": "#0073d2", + "--vscode-terminal-ansiBrightMagenta": "#7d64c5", + "--vscode-terminal-ansiBrightCyan": "#0073d2", + "--vscode-terminal-ansiBrightWhite": "#384c52" + }, + "origin": { + "kind": "bundled" + } + }, + { + "id": "santoso-wijaya.helios-selene.selenized-dark", + "label": "Selenized Dark", + "type": "dark", + "swatch": "#053d48", + "accent": "#39c7b999", + "vars": { + "--vscode-focusBorder": "#39c7b999", + "--vscode-errorForeground": "#fd564e", + "--vscode-button-background": "#0096f5", + "--vscode-input-background": "#275b69", + "--vscode-input-border": "#718b90", + "--vscode-badge-background": "#0096f5AA", + "--vscode-list-activeSelectionBackground": "#0096f588", + "--vscode-sideBar-background": "#0e4956", + "--vscode-editorGroupHeader-tabsBackground": "#275b69", + "--vscode-tab-activeBackground": "#053d48", + "--vscode-tab-activeForeground": "#c8d7d8", + "--vscode-tab-inactiveForeground": "#adbcbc", + "--vscode-tab-inactiveBackground": "#275b69", + "--vscode-editor-background": "#053d48", + "--vscode-editor-foreground": "#c8d7d8", + "--vscode-editorWidget-background": "#0e4956", + "--vscode-panel-border": "#718b90", + "--vscode-terminal-background": "#053d48", + "--vscode-terminal-foreground": "#c8d7d8", + "--vscode-terminal-selectionBackground": "#0e4956", + "--vscode-terminalCursor-foreground": "#adbcbc", + "--vscode-terminal-ansiBlack": "#0e4956", + "--vscode-terminal-ansiRed": "#fd564e", + "--vscode-terminal-ansiGreen": "#80b83c", + "--vscode-terminal-ansiYellow": "#e3b230", + "--vscode-terminal-ansiBlue": "#0096f5", + "--vscode-terminal-ansiMagenta": "#f176bd", + "--vscode-terminal-ansiCyan": "#39c7b9", + "--vscode-terminal-ansiWhite": "#adbcbc", + "--vscode-terminal-ansiBrightBlack": "#053d48", + "--vscode-terminal-ansiBrightRed": "#f38649", + "--vscode-terminal-ansiBrightGreen": "#80b83c", + "--vscode-terminal-ansiBrightYellow": "#e3b230", + "--vscode-terminal-ansiBrightBlue": "#0096f5", + "--vscode-terminal-ansiBrightMagenta": "#a58cec", + "--vscode-terminal-ansiBrightCyan": "#0096f5", + "--vscode-terminal-ansiBrightWhite": "#c8d7d8" + }, + "origin": { + "kind": "bundled" + } + }, + { + "id": "santoso-wijaya.helios-selene.solarized-light", + "label": "Solarized Light", + "type": "light", + "swatch": "#fff6e3", + "accent": "#cf4b1599", + "vars": { + "--vscode-focusBorder": "#cf4b1599", + "--vscode-errorForeground": "#e0332e", + "--vscode-button-background": "#bb8801", + "--vscode-input-background": "#d7cebc", + "--vscode-input-border": "#92a1a1", + "--vscode-badge-background": "#bb8801AA", + "--vscode-list-activeSelectionBackground": "#bb880188", + "--vscode-sideBar-background": "#f0e7d5", + "--vscode-editorGroupHeader-tabsBackground": "#d7cebc", + "--vscode-tab-activeBackground": "#fff6e3", + "--vscode-tab-activeForeground": "#566e76", + "--vscode-tab-inactiveForeground": "#637b82", + "--vscode-tab-inactiveBackground": "#d7cebc", + "--vscode-editor-background": "#fff6e3", + "--vscode-editor-foreground": "#566e76", + "--vscode-editorWidget-background": "#f0e7d5", + "--vscode-panel-border": "#92a1a1", + "--vscode-terminal-background": "#fff6e3", + "--vscode-terminal-foreground": "#566e76", + "--vscode-terminal-selectionBackground": "#f0e7d5", + "--vscode-terminalCursor-foreground": "#637b82", + "--vscode-terminal-ansiBlack": "#f0e7d5", + "--vscode-terminal-ansiRed": "#e0332e", + "--vscode-terminal-ansiGreen": "#8d9800", + "--vscode-terminal-ansiYellow": "#bb8801", + "--vscode-terminal-ansiBlue": "#008dd1", + "--vscode-terminal-ansiMagenta": "#f2579c", + "--vscode-terminal-ansiCyan": "#1fa198", + "--vscode-terminal-ansiWhite": "#637b82", + "--vscode-terminal-ansiBrightBlack": "#fff6e3", + "--vscode-terminal-ansiBrightRed": "#cf4b15", + "--vscode-terminal-ansiBrightGreen": "#8d9800", + "--vscode-terminal-ansiBrightYellow": "#bb8801", + "--vscode-terminal-ansiBrightBlue": "#008dd1", + "--vscode-terminal-ansiBrightMagenta": "#5c73c4", + "--vscode-terminal-ansiBrightCyan": "#008dd1", + "--vscode-terminal-ansiBrightWhite": "#566e76" + }, + "origin": { + "kind": "bundled" + } + }, + { + "id": "santoso-wijaya.helios-selene.solarized-dark", + "label": "Solarized Dark", + "type": "dark", + "swatch": "#002b36", + "accent": "#1fa19899", + "vars": { + "--vscode-focusBorder": "#1fa19899", + "--vscode-errorForeground": "#e0332e", + "--vscode-button-background": "#008dd1", + "--vscode-input-background": "#164854", + "--vscode-input-border": "#566e76", + "--vscode-badge-background": "#008dd1AA", + "--vscode-list-activeSelectionBackground": "#008dd188", + "--vscode-sideBar-background": "#003641", + "--vscode-editorGroupHeader-tabsBackground": "#164854", + "--vscode-tab-activeBackground": "#002b36", + "--vscode-tab-activeForeground": "#92a1a1", + "--vscode-tab-inactiveForeground": "#829496", + "--vscode-tab-inactiveBackground": "#164854", + "--vscode-editor-background": "#002b36", + "--vscode-editor-foreground": "#92a1a1", + "--vscode-editorWidget-background": "#003641", + "--vscode-panel-border": "#566e76", + "--vscode-terminal-background": "#002b36", + "--vscode-terminal-foreground": "#92a1a1", + "--vscode-terminal-selectionBackground": "#003641", + "--vscode-terminalCursor-foreground": "#829496", + "--vscode-terminal-ansiBlack": "#003641", + "--vscode-terminal-ansiRed": "#e0332e", + "--vscode-terminal-ansiGreen": "#8d9800", + "--vscode-terminal-ansiYellow": "#bb8801", + "--vscode-terminal-ansiBlue": "#008dd1", + "--vscode-terminal-ansiMagenta": "#f2579c", + "--vscode-terminal-ansiCyan": "#1fa198", + "--vscode-terminal-ansiWhite": "#829496", + "--vscode-terminal-ansiBrightBlack": "#002b36", + "--vscode-terminal-ansiBrightRed": "#cf4b15", + "--vscode-terminal-ansiBrightGreen": "#8d9800", + "--vscode-terminal-ansiBrightYellow": "#bb8801", + "--vscode-terminal-ansiBrightBlue": "#008dd1", + "--vscode-terminal-ansiBrightMagenta": "#5c73c4", + "--vscode-terminal-ansiBrightCyan": "#008dd1", + "--vscode-terminal-ansiBrightWhite": "#92a1a1" + }, + "origin": { + "kind": "bundled" + } + }, + { + "id": "jdinhlife.gruvbox.gruvbox-dark-medium", + "label": "Gruvbox Dark Medium", + "type": "dark", + "swatch": "#282828", + "accent": "#3c3836", + "vars": { + "--vscode-focusBorder": "#3c3836", + "--vscode-errorForeground": "#fb4934", + "--vscode-button-background": "#45858880", + "--vscode-button-foreground": "#ebdbb2", + "--vscode-button-hoverBackground": "#45858860", + "--vscode-input-background": "#282828", + "--vscode-input-border": "#3c3836", + "--vscode-badge-background": "#b16286", + "--vscode-badge-foreground": "#ebdbb2", + "--vscode-list-activeSelectionBackground": "#3c383680", + "--vscode-list-activeSelectionForeground": "#8ec07c", + "--vscode-sideBar-background": "#282828", + "--vscode-editorGroupHeader-tabsBackground": "#282828", + "--vscode-tab-activeBackground": "#3c3836", + "--vscode-tab-activeForeground": "#ebdbb2", + "--vscode-tab-inactiveForeground": "#a89984", + "--vscode-tab-inactiveBackground": "#282828", + "--vscode-editor-background": "#282828", + "--vscode-editor-foreground": "#ebdbb2", + "--vscode-editorWarning-foreground": "#d79921", + "--vscode-editorWidget-background": "#282828", + "--vscode-panel-border": "#3c3836", + "--vscode-terminal-ansiBlack": "#3c3836", + "--vscode-terminal-ansiBrightBlack": "#928374", + "--vscode-terminal-ansiRed": "#cc241d", + "--vscode-terminal-ansiBrightRed": "#fb4934", + "--vscode-terminal-ansiGreen": "#98971a", + "--vscode-terminal-ansiBrightGreen": "#b8bb26", + "--vscode-terminal-ansiYellow": "#d79921", + "--vscode-terminal-ansiBrightYellow": "#fabd2f", + "--vscode-terminal-ansiBlue": "#458588", + "--vscode-terminal-ansiBrightBlue": "#83a598", + "--vscode-terminal-ansiMagenta": "#b16286", + "--vscode-terminal-ansiBrightMagenta": "#d3869b", + "--vscode-terminal-ansiCyan": "#689d6a", + "--vscode-terminal-ansiBrightCyan": "#8ec07c", + "--vscode-terminal-ansiWhite": "#a89984", + "--vscode-terminal-ansiBrightWhite": "#ebdbb2", + "--vscode-terminal-foreground": "#ebdbb2", + "--vscode-terminal-background": "#282828", + "--vscode-textLink-foreground": "#83a598" + }, + "origin": { + "kind": "bundled" + } + }, + { + "id": "jdinhlife.gruvbox.gruvbox-dark-hard", + "label": "Gruvbox Dark Hard", + "type": "dark", + "swatch": "#1d2021", + "accent": "#3c3836", + "vars": { + "--vscode-focusBorder": "#3c3836", + "--vscode-errorForeground": "#fb4934", + "--vscode-button-background": "#45858880", + "--vscode-button-foreground": "#ebdbb2", + "--vscode-button-hoverBackground": "#45858860", + "--vscode-input-background": "#1d2021", + "--vscode-input-border": "#3c3836", + "--vscode-badge-background": "#b16286", + "--vscode-badge-foreground": "#ebdbb2", + "--vscode-list-activeSelectionBackground": "#3c383680", + "--vscode-list-activeSelectionForeground": "#8ec07c", + "--vscode-sideBar-background": "#1d2021", + "--vscode-editorGroupHeader-tabsBackground": "#1d2021", + "--vscode-tab-activeBackground": "#3c3836", + "--vscode-tab-activeForeground": "#ebdbb2", + "--vscode-tab-inactiveForeground": "#a89984", + "--vscode-tab-inactiveBackground": "#1d2021", + "--vscode-editor-background": "#1d2021", + "--vscode-editor-foreground": "#ebdbb2", + "--vscode-editorWarning-foreground": "#d79921", + "--vscode-editorWidget-background": "#1d2021", + "--vscode-panel-border": "#3c3836", + "--vscode-terminal-ansiBlack": "#3c3836", + "--vscode-terminal-ansiBrightBlack": "#928374", + "--vscode-terminal-ansiRed": "#cc241d", + "--vscode-terminal-ansiBrightRed": "#fb4934", + "--vscode-terminal-ansiGreen": "#98971a", + "--vscode-terminal-ansiBrightGreen": "#b8bb26", + "--vscode-terminal-ansiYellow": "#d79921", + "--vscode-terminal-ansiBrightYellow": "#fabd2f", + "--vscode-terminal-ansiBlue": "#458588", + "--vscode-terminal-ansiBrightBlue": "#83a598", + "--vscode-terminal-ansiMagenta": "#b16286", + "--vscode-terminal-ansiBrightMagenta": "#d3869b", + "--vscode-terminal-ansiCyan": "#689d6a", + "--vscode-terminal-ansiBrightCyan": "#8ec07c", + "--vscode-terminal-ansiWhite": "#a89984", + "--vscode-terminal-ansiBrightWhite": "#ebdbb2", + "--vscode-terminal-foreground": "#ebdbb2", + "--vscode-terminal-background": "#1d2021", + "--vscode-textLink-foreground": "#83a598" + }, + "origin": { + "kind": "bundled" + } + }, + { + "id": "jdinhlife.gruvbox.gruvbox-dark-soft", + "label": "Gruvbox Dark Soft", + "type": "dark", + "swatch": "#32302f", + "accent": "#3c3836", + "vars": { + "--vscode-focusBorder": "#3c3836", + "--vscode-errorForeground": "#fb4934", + "--vscode-button-background": "#45858880", + "--vscode-button-foreground": "#ebdbb2", + "--vscode-button-hoverBackground": "#45858860", + "--vscode-input-background": "#32302f", + "--vscode-input-border": "#3c3836", + "--vscode-badge-background": "#b16286", + "--vscode-badge-foreground": "#ebdbb2", + "--vscode-list-activeSelectionBackground": "#3c383680", + "--vscode-list-activeSelectionForeground": "#8ec07c", + "--vscode-sideBar-background": "#32302f", + "--vscode-editorGroupHeader-tabsBackground": "#32302f", + "--vscode-tab-activeBackground": "#3c3836", + "--vscode-tab-activeForeground": "#ebdbb2", + "--vscode-tab-inactiveForeground": "#a89984", + "--vscode-tab-inactiveBackground": "#32302f", + "--vscode-editor-background": "#32302f", + "--vscode-editor-foreground": "#ebdbb2", + "--vscode-editorWarning-foreground": "#d79921", + "--vscode-editorWidget-background": "#32302f", + "--vscode-panel-border": "#3c3836", + "--vscode-terminal-ansiBlack": "#3c3836", + "--vscode-terminal-ansiBrightBlack": "#928374", + "--vscode-terminal-ansiRed": "#cc241d", + "--vscode-terminal-ansiBrightRed": "#fb4934", + "--vscode-terminal-ansiGreen": "#98971a", + "--vscode-terminal-ansiBrightGreen": "#b8bb26", + "--vscode-terminal-ansiYellow": "#d79921", + "--vscode-terminal-ansiBrightYellow": "#fabd2f", + "--vscode-terminal-ansiBlue": "#458588", + "--vscode-terminal-ansiBrightBlue": "#83a598", + "--vscode-terminal-ansiMagenta": "#b16286", + "--vscode-terminal-ansiBrightMagenta": "#d3869b", + "--vscode-terminal-ansiCyan": "#689d6a", + "--vscode-terminal-ansiBrightCyan": "#8ec07c", + "--vscode-terminal-ansiWhite": "#a89984", + "--vscode-terminal-ansiBrightWhite": "#ebdbb2", + "--vscode-terminal-foreground": "#ebdbb2", + "--vscode-terminal-background": "#32302f", + "--vscode-textLink-foreground": "#83a598" + }, + "origin": { + "kind": "bundled" + } + }, + { + "id": "jdinhlife.gruvbox.gruvbox-light-medium", + "label": "Gruvbox Light Medium", + "type": "light", + "swatch": "#fbf1c7", + "accent": "#ebdbb2", + "vars": { + "--vscode-focusBorder": "#ebdbb2", + "--vscode-errorForeground": "#9d0006", + "--vscode-button-background": "#45858880", + "--vscode-button-foreground": "#3c3836", + "--vscode-button-hoverBackground": "#45858860", + "--vscode-input-background": "#fbf1c7", + "--vscode-input-border": "#ebdbb2", + "--vscode-badge-background": "#b16286", + "--vscode-badge-foreground": "#ebdbb2", + "--vscode-list-activeSelectionBackground": "#ebdbb280", + "--vscode-list-activeSelectionForeground": "#427b58", + "--vscode-sideBar-background": "#fbf1c7", + "--vscode-editorGroupHeader-tabsBackground": "#fbf1c7", + "--vscode-tab-activeBackground": "#ebdbb2", + "--vscode-tab-activeForeground": "#3c3836", + "--vscode-tab-inactiveForeground": "#7c6f64", + "--vscode-tab-inactiveBackground": "#fbf1c7", + "--vscode-editor-background": "#fbf1c7", + "--vscode-editor-foreground": "#3c3836", + "--vscode-editorWarning-foreground": "#d79921", + "--vscode-editorWidget-background": "#fbf1c7", + "--vscode-panel-border": "#ebdbb2", + "--vscode-terminal-ansiBlack": "#ebdbb2", + "--vscode-terminal-ansiBrightBlack": "#928374", + "--vscode-terminal-ansiRed": "#cc241d", + "--vscode-terminal-ansiBrightRed": "#9d0006", + "--vscode-terminal-ansiGreen": "#98971a", + "--vscode-terminal-ansiBrightGreen": "#79740e", + "--vscode-terminal-ansiYellow": "#d79921", + "--vscode-terminal-ansiBrightYellow": "#b57614", + "--vscode-terminal-ansiBlue": "#458588", + "--vscode-terminal-ansiBrightBlue": "#076678", + "--vscode-terminal-ansiMagenta": "#b16286", + "--vscode-terminal-ansiBrightMagenta": "#8f3f71", + "--vscode-terminal-ansiCyan": "#689d6a", + "--vscode-terminal-ansiBrightCyan": "#427b58", + "--vscode-terminal-ansiWhite": "#7c6f64", + "--vscode-terminal-ansiBrightWhite": "#3c3836", + "--vscode-terminal-foreground": "#3c3836", + "--vscode-terminal-background": "#fbf1c7", + "--vscode-textLink-foreground": "#076678" + }, + "origin": { + "kind": "bundled" + } + }, + { + "id": "jdinhlife.gruvbox.gruvbox-light-hard", + "label": "Gruvbox Light Hard", + "type": "light", + "swatch": "#f9f5d7", + "accent": "#ebdbb2", + "vars": { + "--vscode-focusBorder": "#ebdbb2", + "--vscode-errorForeground": "#9d0006", + "--vscode-button-background": "#45858880", + "--vscode-button-foreground": "#3c3836", + "--vscode-button-hoverBackground": "#45858860", + "--vscode-input-background": "#f9f5d7", + "--vscode-input-border": "#ebdbb2", + "--vscode-badge-background": "#b16286", + "--vscode-badge-foreground": "#ebdbb2", + "--vscode-list-activeSelectionBackground": "#ebdbb280", + "--vscode-list-activeSelectionForeground": "#427b58", + "--vscode-sideBar-background": "#f9f5d7", + "--vscode-editorGroupHeader-tabsBackground": "#f9f5d7", + "--vscode-tab-activeBackground": "#ebdbb2", + "--vscode-tab-activeForeground": "#3c3836", + "--vscode-tab-inactiveForeground": "#7c6f64", + "--vscode-tab-inactiveBackground": "#f9f5d7", + "--vscode-editor-background": "#f9f5d7", + "--vscode-editor-foreground": "#3c3836", + "--vscode-editorWarning-foreground": "#d79921", + "--vscode-editorWidget-background": "#f9f5d7", + "--vscode-panel-border": "#ebdbb2", + "--vscode-terminal-ansiBlack": "#ebdbb2", + "--vscode-terminal-ansiBrightBlack": "#928374", + "--vscode-terminal-ansiRed": "#cc241d", + "--vscode-terminal-ansiBrightRed": "#9d0006", + "--vscode-terminal-ansiGreen": "#98971a", + "--vscode-terminal-ansiBrightGreen": "#79740e", + "--vscode-terminal-ansiYellow": "#d79921", + "--vscode-terminal-ansiBrightYellow": "#b57614", + "--vscode-terminal-ansiBlue": "#458588", + "--vscode-terminal-ansiBrightBlue": "#076678", + "--vscode-terminal-ansiMagenta": "#b16286", + "--vscode-terminal-ansiBrightMagenta": "#8f3f71", + "--vscode-terminal-ansiCyan": "#689d6a", + "--vscode-terminal-ansiBrightCyan": "#427b58", + "--vscode-terminal-ansiWhite": "#7c6f64", + "--vscode-terminal-ansiBrightWhite": "#3c3836", + "--vscode-terminal-foreground": "#3c3836", + "--vscode-terminal-background": "#f9f5d7", + "--vscode-textLink-foreground": "#076678" + }, + "origin": { + "kind": "bundled" + } + }, + { + "id": "jdinhlife.gruvbox.gruvbox-light-soft", + "label": "Gruvbox Light Soft", + "type": "light", + "swatch": "#f2e5bc", + "accent": "#ebdbb2", + "vars": { + "--vscode-focusBorder": "#ebdbb2", + "--vscode-errorForeground": "#9d0006", + "--vscode-button-background": "#45858880", + "--vscode-button-foreground": "#3c3836", + "--vscode-button-hoverBackground": "#45858860", + "--vscode-input-background": "#f2e5bc", + "--vscode-input-border": "#ebdbb2", + "--vscode-badge-background": "#b16286", + "--vscode-badge-foreground": "#ebdbb2", + "--vscode-list-activeSelectionBackground": "#ebdbb280", + "--vscode-list-activeSelectionForeground": "#427b58", + "--vscode-sideBar-background": "#f2e5bc", + "--vscode-editorGroupHeader-tabsBackground": "#f2e5bc", + "--vscode-tab-activeBackground": "#ebdbb2", + "--vscode-tab-activeForeground": "#3c3836", + "--vscode-tab-inactiveForeground": "#7c6f64", + "--vscode-tab-inactiveBackground": "#f2e5bc", + "--vscode-editor-background": "#f2e5bc", + "--vscode-editor-foreground": "#3c3836", + "--vscode-editorWarning-foreground": "#d79921", + "--vscode-editorWidget-background": "#f2e5bc", + "--vscode-panel-border": "#ebdbb2", + "--vscode-terminal-ansiBlack": "#ebdbb2", + "--vscode-terminal-ansiBrightBlack": "#928374", + "--vscode-terminal-ansiRed": "#cc241d", + "--vscode-terminal-ansiBrightRed": "#9d0006", + "--vscode-terminal-ansiGreen": "#98971a", + "--vscode-terminal-ansiBrightGreen": "#79740e", + "--vscode-terminal-ansiYellow": "#d79921", + "--vscode-terminal-ansiBrightYellow": "#b57614", + "--vscode-terminal-ansiBlue": "#458588", + "--vscode-terminal-ansiBrightBlue": "#076678", + "--vscode-terminal-ansiMagenta": "#b16286", + "--vscode-terminal-ansiBrightMagenta": "#8f3f71", + "--vscode-terminal-ansiCyan": "#689d6a", + "--vscode-terminal-ansiBrightCyan": "#427b58", + "--vscode-terminal-ansiWhite": "#7c6f64", + "--vscode-terminal-ansiBrightWhite": "#3c3836", + "--vscode-terminal-foreground": "#3c3836", + "--vscode-terminal-background": "#f2e5bc", + "--vscode-textLink-foreground": "#076678" + }, + "origin": { + "kind": "bundled" + } + }, + { + "id": "cocopon.iceberg-theme.iceberg", + "label": "Iceberg", + "type": "dark", + "swatch": "#161821", + "accent": "#242940", + "vars": { + "--vscode-badge-background": "#6b708920", + "--vscode-badge-foreground": "#6b7089", + "--vscode-button-background": "#c6c8d1", + "--vscode-button-foreground": "#161821", + "--vscode-button-hoverBackground": "#d2d4de", + "--vscode-descriptionForeground": "#6b7089", + "--vscode-editor-background": "#161821", + "--vscode-editor-foreground": "#c6c8d1", + "--vscode-editorGroupHeader-tabsBackground": "#0e1015", + "--vscode-editorWidget-background": "#1e2132", + "--vscode-editorWarning-foreground": "#e2a478", + "--vscode-focusBorder": "#242940", + "--vscode-input-background": "#0f1117", + "--vscode-list-activeSelectionBackground": "#1e2132", + "--vscode-list-activeSelectionForeground": "#c6c8d1", + "--vscode-panel-border": "#0e1015", + "--vscode-sideBar-background": "#161821", + "--vscode-tab-activeBackground": "#161821", + "--vscode-tab-activeForeground": "#c6c8d1", + "--vscode-tab-inactiveBackground": "#0e1015", + "--vscode-tab-inactiveForeground": "#6b7089", + "--vscode-terminal-ansiBlack": "#1e2132", + "--vscode-terminal-ansiBlue": "#84a0c6", + "--vscode-terminal-ansiBrightBlack": "#6b7089", + "--vscode-terminal-ansiBrightBlue": "#91acd1", + "--vscode-terminal-ansiBrightCyan": "#95c4ce", + "--vscode-terminal-ansiBrightGreen": "#c0ca8e", + "--vscode-terminal-ansiBrightMagenta": "#ada0d3", + "--vscode-terminal-ansiBrightRed": "#e98989", + "--vscode-terminal-ansiBrightWhite": "#d2d4de", + "--vscode-terminal-ansiBrightYellow": "#e9b189", + "--vscode-terminal-ansiCyan": "#89b8c2", + "--vscode-terminal-ansiGreen": "#b4be82", + "--vscode-terminal-ansiMagenta": "#a093c7", + "--vscode-terminal-ansiRed": "#e27878", + "--vscode-terminal-ansiWhite": "#c6c8d1", + "--vscode-terminal-ansiYellow": "#e2a478", + "--vscode-terminal-foreground": "#c6c8d1", + "--vscode-terminal-selectionBackground": "#4a548266", + "--vscode-textLink-foreground": "#84a0c6" + }, + "origin": { + "kind": "bundled" + } + }, + { + "id": "cocopon.iceberg-theme.iceberg-light", + "label": "Iceberg Light", + "type": "light", + "swatch": "#e8e9ec", + "accent": "#cbcfda", + "vars": { + "--vscode-badge-background": "#8389a320", + "--vscode-badge-foreground": "#8389a3", + "--vscode-button-background": "#33374c", + "--vscode-button-foreground": "#e8e9ec", + "--vscode-button-hoverBackground": "#262a3f", + "--vscode-descriptionForeground": "#8389a3", + "--vscode-editor-background": "#e8e9ec", + "--vscode-editor-foreground": "#33374c", + "--vscode-editorGroupHeader-tabsBackground": "#c8cfdd", + "--vscode-editorWidget-background": "#dcdfe7", + "--vscode-editorWarning-foreground": "#c57339", + "--vscode-focusBorder": "#cbcfda", + "--vscode-input-background": "#cad0de", + "--vscode-list-activeSelectionBackground": "#dcdfe7", + "--vscode-list-activeSelectionForeground": "#33374c", + "--vscode-panel-border": "#c8cfdd", + "--vscode-sideBar-background": "#e8e9ec", + "--vscode-tab-activeBackground": "#e8e9ec", + "--vscode-tab-activeForeground": "#33374c", + "--vscode-tab-inactiveBackground": "#c8cfdd", + "--vscode-tab-inactiveForeground": "#8389a3", + "--vscode-terminal-ansiBlack": "#dcdfe7", + "--vscode-terminal-ansiBlue": "#2d539e", + "--vscode-terminal-ansiBrightBlack": "#8389a3", + "--vscode-terminal-ansiBrightBlue": "#22478e", + "--vscode-terminal-ansiBrightCyan": "#327698", + "--vscode-terminal-ansiBrightGreen": "#598030", + "--vscode-terminal-ansiBrightMagenta": "#6845ad", + "--vscode-terminal-ansiBrightRed": "#cc3768", + "--vscode-terminal-ansiBrightWhite": "#262a3f", + "--vscode-terminal-ansiBrightYellow": "#b6662d", + "--vscode-terminal-ansiCyan": "#3f83a6", + "--vscode-terminal-ansiGreen": "#668e3d", + "--vscode-terminal-ansiMagenta": "#7759b4", + "--vscode-terminal-ansiRed": "#cc517a", + "--vscode-terminal-ansiWhite": "#33374c", + "--vscode-terminal-ansiYellow": "#c57339", + "--vscode-terminal-foreground": "#33374c", + "--vscode-terminal-selectionBackground": "#aeb2c666", + "--vscode-textLink-foreground": "#2d539e" + }, + "origin": { + "kind": "bundled" + } + } +] diff --git a/lib/src/lib/themes/convert.test.ts b/lib/src/lib/themes/convert.test.ts new file mode 100644 index 0000000..56bbe28 --- /dev/null +++ b/lib/src/lib/themes/convert.test.ts @@ -0,0 +1,74 @@ +import { describe, it, expect } from 'vitest'; +import { convertVscodeThemeColors, uiThemeToType, CONSUMED_VSCODE_KEYS } from './convert'; + +describe('convertVscodeThemeColors', () => { + it('converts consumed keys to --vscode-* CSS variables', () => { + const result = convertVscodeThemeColors({ + 'editor.background': '#282a36', + 'editor.foreground': '#f8f8f2', + 'terminal.ansiRed': '#ff5555', + }); + expect(result).toEqual({ + '--vscode-editor-background': '#282a36', + '--vscode-editor-foreground': '#f8f8f2', + '--vscode-terminal-ansiRed': '#ff5555', + }); + }); + + it('drops keys not in CONSUMED_VSCODE_KEYS', () => { + const result = convertVscodeThemeColors({ + 'editor.background': '#282a36', + 'activityBar.background': '#21222c', // not consumed + 'statusBar.background': '#191a21', // not consumed + }); + expect(result).toEqual({ + '--vscode-editor-background': '#282a36', + }); + }); + + it('returns empty object for empty input', () => { + expect(convertVscodeThemeColors({})).toEqual({}); + }); + + it('handles all consumed keys without error', () => { + const colors: Record = {}; + for (const key of CONSUMED_VSCODE_KEYS) { + colors[key] = '#000000'; + } + const result = convertVscodeThemeColors(colors); + expect(Object.keys(result)).toHaveLength(CONSUMED_VSCODE_KEYS.length); + }); + + it('preserves camelCase in key conversion', () => { + const result = convertVscodeThemeColors({ + 'editorGroupHeader.tabsBackground': '#252526', + 'terminal.ansiBrightMagenta': '#d670d6', + }); + expect(result).toEqual({ + '--vscode-editorGroupHeader-tabsBackground': '#252526', + '--vscode-terminal-ansiBrightMagenta': '#d670d6', + }); + }); +}); + +describe('uiThemeToType', () => { + it('maps vs to light', () => { + expect(uiThemeToType('vs')).toBe('light'); + }); + + it('maps hc-light to light', () => { + expect(uiThemeToType('hc-light')).toBe('light'); + }); + + it('maps vs-dark to dark', () => { + expect(uiThemeToType('vs-dark')).toBe('dark'); + }); + + it('maps hc-black to dark', () => { + expect(uiThemeToType('hc-black')).toBe('dark'); + }); + + it('defaults unknown values to dark', () => { + expect(uiThemeToType('something-else')).toBe('dark'); + }); +}); diff --git a/lib/src/lib/themes/convert.ts b/lib/src/lib/themes/convert.ts new file mode 100644 index 0000000..dc0c208 --- /dev/null +++ b/lib/src/lib/themes/convert.ts @@ -0,0 +1,97 @@ +/** + * Conversion from VSCode theme JSON `colors` to --vscode-* CSS variables. + * + * Two consumers read --vscode-* variables: + * 1. @theme fallbacks in theme.css — UI colors (surfaces, tabs, etc.) + * 2. getTerminalTheme() in terminal-registry.ts — ANSI, cursor, selection + */ + +/** VSCode theme color keys consumed by MouseTerm. Derived from theme.css and terminal-registry.ts. */ +export const CONSUMED_VSCODE_KEYS: readonly string[] = [ + // Surfaces (theme.css @theme) + 'editor.background', + 'editorGroupHeader.tabsBackground', + 'sideBar.background', + 'editorWidget.background', + // Text + 'editor.foreground', + 'descriptionForeground', + // Accent & borders + 'focusBorder', + 'panel.border', + // Tabs + 'tab.activeBackground', + 'tab.inactiveBackground', + 'tab.activeForeground', + 'tab.inactiveForeground', + 'list.activeSelectionBackground', + 'list.activeSelectionForeground', + // Terminal + 'terminal.background', + 'terminal.foreground', + // Badges + 'badge.background', + 'badge.foreground', + // Status + 'errorForeground', + 'editorWarning.foreground', + // Inputs + 'input.background', + 'input.border', + // Buttons + 'button.background', + 'button.foreground', + 'button.hoverBackground', + // Links + 'textLink.foreground', + // Terminal (read directly by getTerminalTheme()) + 'terminalCursor.foreground', + 'terminal.selectionBackground', + 'terminal.ansiBlack', + 'terminal.ansiRed', + 'terminal.ansiGreen', + 'terminal.ansiYellow', + 'terminal.ansiBlue', + 'terminal.ansiMagenta', + 'terminal.ansiCyan', + 'terminal.ansiWhite', + 'terminal.ansiBrightBlack', + 'terminal.ansiBrightRed', + 'terminal.ansiBrightGreen', + 'terminal.ansiBrightYellow', + 'terminal.ansiBrightBlue', + 'terminal.ansiBrightMagenta', + 'terminal.ansiBrightCyan', + 'terminal.ansiBrightWhite', +] as const; + +const consumedSet = new Set(CONSUMED_VSCODE_KEYS); + +/** + * Convert a VSCode theme `colors` object to --vscode-* CSS variable entries. + * Only keys in CONSUMED_VSCODE_KEYS are included; the rest are dropped. + * + * Conversion rule: `editor.background` → `--vscode-editor-background` + */ +export function convertVscodeThemeColors( + colors: Record, +): Record { + const vars: Record = {}; + for (const [key, value] of Object.entries(colors)) { + if (consumedSet.has(key)) { + vars[`--vscode-${key.replace(/\./g, '-')}`] = value; + } + } + return vars; +} + +/** Map package.json contributes.themes[].uiTheme to our type field. */ +export function uiThemeToType(uiTheme: string): 'dark' | 'light' { + switch (uiTheme) { + case 'vs': + case 'hc-light': + return 'light'; + default: + return 'dark'; + } +} diff --git a/lib/src/lib/themes/index.ts b/lib/src/lib/themes/index.ts new file mode 100644 index 0000000..178f3f3 --- /dev/null +++ b/lib/src/lib/themes/index.ts @@ -0,0 +1,15 @@ +export type { MouseTermTheme, BundledOrigin, InstalledOrigin } from './types'; +export { CONSUMED_VSCODE_KEYS, convertVscodeThemeColors, uiThemeToType } from './convert'; +export { applyTheme } from './apply'; +export { + getBundledThemes, + getInstalledThemes, + getAllThemes, + getTheme, + addInstalledTheme, + removeInstalledTheme, + getActiveThemeId, + setActiveThemeId, +} from './store'; +export { searchThemes, fetchExtensionThemes } from './openvsx'; +export type { OpenVSXSearchResult, OpenVSXExtension } from './openvsx'; diff --git a/lib/src/lib/themes/openvsx.ts b/lib/src/lib/themes/openvsx.ts new file mode 100644 index 0000000..8eb712a --- /dev/null +++ b/lib/src/lib/themes/openvsx.ts @@ -0,0 +1,120 @@ +/** + * Runtime OpenVSX theme installer. + * + * Searches for theme extensions, downloads VSIX files, extracts theme + * JSONs in the browser, and converts them to MouseTermTheme objects. + * + * fflate is dynamically imported so it doesn't affect initial bundle size. + */ + +import type { MouseTermTheme } from './types'; +import { convertVscodeThemeColors, uiThemeToType } from './convert'; + +const OPENVSX_API = 'https://open-vsx.org/api'; + +export interface OpenVSXSearchResult { + extensions: OpenVSXExtension[]; + totalSize: number; + offset: number; +} + +export interface OpenVSXExtension { + namespace: string; + name: string; + displayName: string; + description: string; + version: string; + averageRating?: number; + downloadCount: number; + files?: { icon?: string }; +} + +export async function searchThemes( + query: string, + offset = 0, + size = 20, +): Promise { + const params = new URLSearchParams({ + category: 'Themes', + query, + size: String(size), + offset: String(offset), + sortBy: 'relevance', + sortOrder: 'desc', + }); + const res = await fetch(`${OPENVSX_API}/-/search?${params}`); + if (!res.ok) throw new Error(`OpenVSX search failed: ${res.status}`); + return res.json(); +} + +function slugify(label: string): string { + return label + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, ''); +} + +/** + * Download a theme extension from OpenVSX and return all theme variants + * as MouseTermTheme objects ready for installation. + */ +export async function fetchExtensionThemes( + namespace: string, + name: string, +): Promise { + // 1. Get latest version metadata + const metaRes = await fetch(`${OPENVSX_API}/${namespace}/${name}/latest`); + if (!metaRes.ok) throw new Error(`OpenVSX metadata failed: ${metaRes.status}`); + const meta = await metaRes.json(); + + const downloadUrl = meta.files?.download; + if (!downloadUrl) throw new Error(`No download URL for ${namespace}/${name}`); + + // 2. Download VSIX + const vsixRes = await fetch(downloadUrl); + if (!vsixRes.ok) throw new Error(`VSIX download failed: ${vsixRes.status}`); + const vsixBuf = new Uint8Array(await vsixRes.arrayBuffer()); + + // 3. Extract (dynamic import — fflate only loaded when needed) + const { unzipSync } = await import('fflate'); + const entries = unzipSync(vsixBuf); + + // 4. Read package.json + const pkgData = entries['extension/package.json']; + if (!pkgData) throw new Error('No package.json in VSIX'); + const pkgJson = JSON.parse(new TextDecoder().decode(pkgData)); + const themeContribs: Array<{ label: string; uiTheme?: string; path: string }> = + pkgJson.contributes?.themes ?? []; + + // 5. Parse JSONC (dynamic import to avoid loading at startup) + const { parse: parseJsonc } = await import('jsonc-parser'); + + // 6. Convert each theme variant + const themes: MouseTermTheme[] = []; + for (const contrib of themeContribs) { + const themePath = `extension/${contrib.path.replace(/^\.\//, '')}`; + const themeData = entries[themePath]; + if (!themeData) continue; + + const themeJson = parseJsonc(new TextDecoder().decode(themeData)); + const colors: Record = themeJson.colors ?? {}; + const vars = convertVscodeThemeColors(colors); + const type = uiThemeToType(contrib.uiTheme ?? themeJson.type ?? 'vs-dark'); + + themes.push({ + id: `${namespace}.${name}.${slugify(contrib.label)}`, + label: contrib.label, + type, + swatch: colors['editor.background'] ?? (type === 'light' ? '#ffffff' : '#1e1e1e'), + accent: colors['focusBorder'] ?? (type === 'light' ? '#0090f1' : '#007fd4'), + vars, + origin: { + kind: 'installed', + extensionId: `${namespace}/${name}`, + installedAt: new Date().toISOString(), + }, + }); + } + + return themes; +} diff --git a/lib/src/lib/themes/store.ts b/lib/src/lib/themes/store.ts new file mode 100644 index 0000000..a088f51 --- /dev/null +++ b/lib/src/lib/themes/store.ts @@ -0,0 +1,54 @@ +import type { MouseTermTheme } from './types'; +// JSON import types are inferred too narrowly — cast at the boundary. +import _bundledThemes from './bundled.json'; +const bundledThemes = _bundledThemes as unknown as MouseTermTheme[]; + +const INSTALLED_KEY = 'mouseterm:installed-themes'; +const ACTIVE_KEY = 'mouseterm:active-theme'; + +const hasStorage = typeof localStorage !== 'undefined'; + +export function getBundledThemes(): MouseTermTheme[] { + return bundledThemes; +} + +export function getInstalledThemes(): MouseTermTheme[] { + if (!hasStorage) return []; + try { + const raw = localStorage.getItem(INSTALLED_KEY); + return raw ? (JSON.parse(raw) as MouseTermTheme[]) : []; + } catch { + return []; + } +} + +export function getAllThemes(): MouseTermTheme[] { + return [...getBundledThemes(), ...getInstalledThemes()]; +} + +export function getTheme(id: string): MouseTermTheme | undefined { + return getAllThemes().find((t) => t.id === id); +} + +export function addInstalledTheme(theme: MouseTermTheme): void { + if (!hasStorage) return; + const installed = getInstalledThemes().filter((t) => t.id !== theme.id); + installed.push(theme); + localStorage.setItem(INSTALLED_KEY, JSON.stringify(installed)); +} + +export function removeInstalledTheme(id: string): void { + if (!hasStorage) return; + const installed = getInstalledThemes().filter((t) => t.id !== id); + localStorage.setItem(INSTALLED_KEY, JSON.stringify(installed)); +} + +export function getActiveThemeId(): string { + if (!hasStorage) return getBundledThemes()[0]?.id ?? ''; + return localStorage.getItem(ACTIVE_KEY) ?? getBundledThemes()[0]?.id ?? ''; +} + +export function setActiveThemeId(id: string): void { + if (!hasStorage) return; + localStorage.setItem(ACTIVE_KEY, id); +} diff --git a/lib/src/lib/themes/types.ts b/lib/src/lib/themes/types.ts new file mode 100644 index 0000000..7b4511e --- /dev/null +++ b/lib/src/lib/themes/types.ts @@ -0,0 +1,28 @@ +export interface MouseTermTheme { + /** Stable unique ID, e.g. "GitHub.github-vscode-theme.github-dark-default" */ + id: string; + /** Human-readable label from the VSCode theme */ + label: string; + /** Theme base type */ + type: 'dark' | 'light'; + /** Background color for picker swatch (editor.background) */ + swatch: string; + /** Accent color for picker dot (focusBorder) */ + accent: string; + /** --vscode-* CSS variable overrides */ + vars: Record; + /** Where this theme came from */ + origin: BundledOrigin | InstalledOrigin; +} + +export interface BundledOrigin { + kind: 'bundled'; +} + +export interface InstalledOrigin { + kind: 'installed'; + /** OpenVSX namespace/name, e.g. "publisher/theme-extension" */ + extensionId: string; + /** ISO date string */ + installedAt: string; +} diff --git a/lib/src/stories/Pond.stories.tsx b/lib/src/stories/Pond.stories.tsx index 9525d77..663984b 100644 --- a/lib/src/stories/Pond.stories.tsx +++ b/lib/src/stories/Pond.stories.tsx @@ -73,13 +73,13 @@ export const MultiPane: Story = { export const MultiPaneDark: Story = { parameters: { fakePty: { scenario: flattenScenario(SCENARIO_LS_OUTPUT) } }, - globals: { theme: 'Dark+' }, + globals: { theme: 'GitHub Dark Default' }, play: splitPanes, }; export const MultiPaneLight: Story = { parameters: { fakePty: { scenario: flattenScenario(SCENARIO_LS_OUTPUT) } }, - globals: { theme: 'Light+' }, + globals: { theme: 'GitHub Light Default' }, play: splitPanes, }; diff --git a/lib/src/stories/SelectionOverlay.stories.tsx b/lib/src/stories/SelectionOverlay.stories.tsx index 26f5717..4d2cd94 100644 --- a/lib/src/stories/SelectionOverlay.stories.tsx +++ b/lib/src/stories/SelectionOverlay.stories.tsx @@ -18,7 +18,7 @@ function SelectionOverlayDemo({ initialMode = 'command' as PondMode }) { return () => ro.disconnect(); }, []); - const color = getComputedStyle(document.documentElement).getPropertyValue('--mt-selection-terminal').trim() || '#007fd4'; + const color = getComputedStyle(document.documentElement).getPropertyValue('--color-accent').trim() || '#007fd4'; const overlayStyle: React.CSSProperties = { position: 'absolute', diff --git a/lib/src/theme.css b/lib/src/theme.css index d4eca89..306931a 100644 --- a/lib/src/theme.css +++ b/lib/src/theme.css @@ -1,231 +1,137 @@ /* MouseTerm Theme System * * Two-layer CSS variable strategy: - * Tailwind tokens -> --mt-* (our semantic layer) -> var(--vscode-*, ) + * @theme --color-* tokens → var(--vscode-*, ) * * In VSCode: --vscode-* variables are auto-injected and take precedence. - * VSCode adds body classes: vscode-light, vscode-dark, vscode-high-contrast. - * In Neutralino/standalone: fallback values apply, with prefers-color-scheme - * used to pick dark vs light defaults. + * In standalone/website: applyTheme() sets --vscode-* on document.body. + * Also sets body class vscode-light for light themes. + * For standalone without explicit theme: prefers-color-scheme picks + * dark vs light defaults. + * + * Two consumers read --vscode-* variables: + * 1. @theme fallbacks below — for UI colors (surfaces, tabs, etc.) + * 2. getTerminalTheme() in terminal-registry.ts — reads ANSI colors, + * cursor, and selection directly as --vscode-* for xterm.js */ -/* --- Dark mode fallbacks (VSCode Dark+ defaults) --- */ +/* --- Font tokens (not Tailwind colors) --- */ :root { - /* Typography */ --mt-font-size: var(--vscode-font-size, 13px); --mt-font-family: var(--vscode-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif); - --mt-editor-font-size: var(--vscode-editor-font-size, 12px); - --mt-editor-font-family: var(--vscode-editor-font-family, 'SF Mono', Menlo, Monaco, monospace); +} +/* --- Dark mode fallback defaults --- */ +@theme { /* Surfaces */ - --mt-surface: var(--vscode-editor-background, #1e1e1e); - --mt-surface-alt: var(--vscode-editorGroupHeader-tabsBackground, var(--vscode-sideBar-background, #252526)); - --mt-surface-raised: var(--vscode-editorWidget-background, #252526); + --color-surface: var(--vscode-editor-background, #1e1e1e); + --color-surface-alt: var(--vscode-editorGroupHeader-tabsBackground, var(--vscode-sideBar-background, #252526)); + --color-surface-raised: var(--vscode-editorWidget-background, #252526); /* Text */ - --mt-foreground: var(--vscode-editor-foreground, #cccccc); - --mt-muted: var(--vscode-descriptionForeground, #858585); + --color-foreground: var(--vscode-editor-foreground, #cccccc); + --color-muted: var(--vscode-descriptionForeground, #858585); /* Accent & borders */ - --mt-accent: var(--vscode-focusBorder, #007fd4); - --mt-border: var(--vscode-panel-border, #2b2b2b); - --mt-gutter: var(--vscode-panel-border, #2b2b2b); - --mt-gutter-active: var(--vscode-focusBorder, #007fd4); + --color-accent: var(--vscode-focusBorder, #007fd4); + --color-border: var(--vscode-panel-border, #2b2b2b); /* Tabs */ - --mt-tab-active-bg: var(--vscode-tab-activeBackground, #1e1e1e); - --mt-tab-inactive-bg: var(--vscode-tab-inactiveBackground, #2d2d2d); - --mt-tab-active-fg: var(--vscode-tab-activeForeground, #ffffff); - --mt-tab-inactive-fg: var(--vscode-tab-inactiveForeground, #969696); - --mt-tab-selected-bg: var(--vscode-list-activeSelectionBackground, #094771); - --mt-tab-selected-fg: var(--vscode-list-activeSelectionForeground, #ffffff); + --color-tab-active-bg: var(--vscode-tab-activeBackground, #1e1e1e); + --color-tab-inactive-bg: var(--vscode-tab-inactiveBackground, #2d2d2d); + --color-tab-active-fg: var(--vscode-tab-activeForeground, #ffffff); + --color-tab-inactive-fg: var(--vscode-tab-inactiveForeground, #969696); + --color-tab-selected-bg: var(--vscode-list-activeSelectionBackground, #094771); + --color-tab-selected-fg: var(--vscode-list-activeSelectionForeground, #ffffff); /* Terminal */ - --mt-terminal-bg: var(--vscode-terminal-background, #1e1e1e); - --mt-terminal-fg: var(--vscode-terminal-foreground, #cccccc); + --color-terminal-bg: var(--vscode-terminal-background, #1e1e1e); + --color-terminal-fg: var(--vscode-terminal-foreground, #cccccc); /* Badges */ - --mt-badge-bg: var(--vscode-badge-background, #007acc); - --mt-badge-fg: var(--vscode-badge-foreground, #ffffff); + --color-badge-bg: var(--vscode-badge-background, #007acc); + --color-badge-fg: var(--vscode-badge-foreground, #ffffff); /* Semantic status */ - --mt-error-fg: var(--vscode-errorForeground, #f48771); - --mt-success-fg: var(--vscode-testing-iconPassed, #73c991); - --mt-warning-fg: var(--vscode-editorWarning-foreground, #cca700); + --color-error: var(--vscode-errorForeground, #f48771); + --color-success: #73c991; + --color-warning: var(--vscode-editorWarning-foreground, #cca700); /* Inputs */ - --mt-input-bg: var(--vscode-input-background, #3c3c3c); - --mt-input-border: var(--vscode-input-border, #3c3c3c); + --color-input-bg: var(--vscode-input-background, #3c3c3c); + --color-input-border: var(--vscode-input-border, #3c3c3c); /* Buttons */ - --mt-button-bg: var(--vscode-button-background, #0e639c); - --mt-button-fg: var(--vscode-button-foreground, #ffffff); - --mt-button-hover-bg: var(--vscode-button-hoverBackground, #1177bb); + --color-button-bg: var(--vscode-button-background, #0e639c); + --color-button-fg: var(--vscode-button-foreground, #ffffff); + --color-button-hover-bg: var(--vscode-button-hoverBackground, #1177bb); - /* Selection overlay */ - --mt-selection-terminal: var(--vscode-focusBorder, #007fd4); - --mt-selection-workspace: var(--vscode-textLink-foreground, #3794ff); - - /* Terminal ANSI colors */ - --mt-ansi-black: var(--vscode-terminal-ansiBlack, #000000); - --mt-ansi-red: var(--vscode-terminal-ansiRed, #cd3131); - --mt-ansi-green: var(--vscode-terminal-ansiGreen, #0dbc79); - --mt-ansi-yellow: var(--vscode-terminal-ansiYellow, #e5e510); - --mt-ansi-blue: var(--vscode-terminal-ansiBlue, #2472c8); - --mt-ansi-magenta: var(--vscode-terminal-ansiMagenta, #bc3fbc); - --mt-ansi-cyan: var(--vscode-terminal-ansiCyan, #11a8cd); - --mt-ansi-white: var(--vscode-terminal-ansiWhite, #e5e5e5); - --mt-ansi-bright-black: var(--vscode-terminal-ansiBrightBlack, #666666); - --mt-ansi-bright-red: var(--vscode-terminal-ansiBrightRed, #f14c4c); - --mt-ansi-bright-green: var(--vscode-terminal-ansiBrightGreen, #23d18b); - --mt-ansi-bright-yellow: var(--vscode-terminal-ansiBrightYellow, #f5f543); - --mt-ansi-bright-blue: var(--vscode-terminal-ansiBrightBlue, #3b8eea); - --mt-ansi-bright-magenta: var(--vscode-terminal-ansiBrightMagenta, #d670d6); - --mt-ansi-bright-cyan: var(--vscode-terminal-ansiBrightCyan, #29b8db); - --mt-ansi-bright-white: var(--vscode-terminal-ansiBrightWhite, #e5e5e5); - --mt-terminal-cursor: var(--vscode-terminalCursor-foreground, #aeafad); - --mt-terminal-selection: var(--vscode-terminal-selectionBackground, #264f7840); + /* Animation */ + --animate-alarm-dot: alarm-dot 2s ease-in-out infinite; } -/* --- Light mode fallbacks (VSCode Light+ defaults) --- +/* --- Light mode fallback defaults --- * VSCode adds body.vscode-light for light themes. - * Also respect prefers-color-scheme for Neutralino/standalone. */ + * applyTheme() also sets this class for imported light themes. */ body.vscode-light { - --mt-surface: var(--vscode-editor-background, #ffffff); - --mt-surface-alt: var(--vscode-editorGroupHeader-tabsBackground, var(--vscode-sideBar-background, #f3f3f3)); - --mt-surface-raised: var(--vscode-editorWidget-background, #f3f3f3); - --mt-foreground: var(--vscode-editor-foreground, #333333); - --mt-muted: var(--vscode-descriptionForeground, #717171); - --mt-accent: var(--vscode-focusBorder, #0090f1); - --mt-border: var(--vscode-panel-border, #e5e5e5); - --mt-gutter: var(--vscode-panel-border, #d9d9d9); - --mt-gutter-active: var(--vscode-focusBorder, #0090f1); - --mt-tab-active-bg: var(--vscode-tab-activeBackground, #ffffff); - --mt-tab-inactive-bg: var(--vscode-tab-inactiveBackground, #ececec); - --mt-tab-active-fg: var(--vscode-tab-activeForeground, #333333); - --mt-tab-inactive-fg: var(--vscode-tab-inactiveForeground, #8e8e8e); - --mt-tab-selected-bg: var(--vscode-list-activeSelectionBackground, #cce6ff); - --mt-tab-selected-fg: var(--vscode-list-activeSelectionForeground, #000000); - --mt-terminal-bg: var(--vscode-terminal-background, #ffffff); - --mt-terminal-fg: var(--vscode-terminal-foreground, #333333); - --mt-badge-bg: var(--vscode-badge-background, #007acc); - --mt-badge-fg: var(--vscode-badge-foreground, #ffffff); - --mt-error-fg: var(--vscode-errorForeground, #a1260d); - --mt-success-fg: var(--vscode-testing-iconPassed, #388a34); - --mt-warning-fg: var(--vscode-editorWarning-foreground, #bf8803); - --mt-input-bg: var(--vscode-input-background, #ffffff); - --mt-input-border: var(--vscode-input-border, #cecece); - --mt-button-bg: var(--vscode-button-background, #007acc); - --mt-button-fg: var(--vscode-button-foreground, #ffffff); - --mt-button-hover-bg: var(--vscode-button-hoverBackground, #0062a3); - --mt-selection-terminal: var(--vscode-focusBorder, #0090f1); - --mt-selection-workspace: var(--vscode-textLink-foreground, #006ab1); - - --mt-ansi-black: var(--vscode-terminal-ansiBlack, #000000); - --mt-ansi-red: var(--vscode-terminal-ansiRed, #cd3131); - --mt-ansi-green: var(--vscode-terminal-ansiGreen, #00bc00); - --mt-ansi-yellow: var(--vscode-terminal-ansiYellow, #949800); - --mt-ansi-blue: var(--vscode-terminal-ansiBlue, #0451a5); - --mt-ansi-magenta: var(--vscode-terminal-ansiMagenta, #bc05bc); - --mt-ansi-cyan: var(--vscode-terminal-ansiCyan, #0598bc); - --mt-ansi-white: var(--vscode-terminal-ansiWhite, #555555); - --mt-ansi-bright-black: var(--vscode-terminal-ansiBrightBlack, #666666); - --mt-ansi-bright-red: var(--vscode-terminal-ansiBrightRed, #cd3131); - --mt-ansi-bright-green: var(--vscode-terminal-ansiBrightGreen, #14ce14); - --mt-ansi-bright-yellow: var(--vscode-terminal-ansiBrightYellow, #b5ba00); - --mt-ansi-bright-blue: var(--vscode-terminal-ansiBrightBlue, #0451a5); - --mt-ansi-bright-magenta: var(--vscode-terminal-ansiBrightMagenta, #bc05bc); - --mt-ansi-bright-cyan: var(--vscode-terminal-ansiBrightCyan, #0598bc); - --mt-ansi-bright-white: var(--vscode-terminal-ansiBrightWhite, #a5a5a5); - --mt-terminal-cursor: var(--vscode-terminalCursor-foreground, #000000); - --mt-terminal-selection: var(--vscode-terminal-selectionBackground, #add6ff80); + --color-surface: var(--vscode-editor-background, #ffffff); + --color-surface-alt: var(--vscode-editorGroupHeader-tabsBackground, var(--vscode-sideBar-background, #f3f3f3)); + --color-surface-raised: var(--vscode-editorWidget-background, #f3f3f3); + --color-foreground: var(--vscode-editor-foreground, #333333); + --color-muted: var(--vscode-descriptionForeground, #717171); + --color-accent: var(--vscode-focusBorder, #0090f1); + --color-border: var(--vscode-panel-border, #e5e5e5); + --color-tab-active-bg: var(--vscode-tab-activeBackground, #ffffff); + --color-tab-inactive-bg: var(--vscode-tab-inactiveBackground, #ececec); + --color-tab-active-fg: var(--vscode-tab-activeForeground, #333333); + --color-tab-inactive-fg: var(--vscode-tab-inactiveForeground, #8e8e8e); + --color-tab-selected-bg: var(--vscode-list-activeSelectionBackground, #cce6ff); + --color-tab-selected-fg: var(--vscode-list-activeSelectionForeground, #000000); + --color-terminal-bg: var(--vscode-terminal-background, #ffffff); + --color-terminal-fg: var(--vscode-terminal-foreground, #333333); + --color-badge-bg: var(--vscode-badge-background, #007acc); + --color-badge-fg: var(--vscode-badge-foreground, #ffffff); + --color-error: var(--vscode-errorForeground, #a1260d); + --color-success: #388a34; + --color-warning: var(--vscode-editorWarning-foreground, #bf8803); + --color-input-bg: var(--vscode-input-background, #ffffff); + --color-input-border: var(--vscode-input-border, #cecece); + --color-button-bg: var(--vscode-button-background, #007acc); + --color-button-fg: var(--vscode-button-foreground, #ffffff); + --color-button-hover-bg: var(--vscode-button-hoverBackground, #0062a3); } -/* Neutralino/standalone: use OS preference when no VSCode body class */ +/* Standalone: use OS preference when no VSCode body class */ @media (prefers-color-scheme: light) { body:not(.vscode-light):not(.vscode-dark) { - --mt-surface: var(--vscode-editor-background, #ffffff); - --mt-surface-alt: var(--vscode-editorGroupHeader-tabsBackground, var(--vscode-sideBar-background, #f3f3f3)); - --mt-surface-raised: var(--vscode-editorWidget-background, #f3f3f3); - --mt-foreground: var(--vscode-editor-foreground, #333333); - --mt-muted: var(--vscode-descriptionForeground, #717171); - --mt-accent: var(--vscode-focusBorder, #0090f1); - --mt-border: var(--vscode-panel-border, #e5e5e5); - --mt-gutter: var(--vscode-panel-border, #d9d9d9); - --mt-gutter-active: var(--vscode-focusBorder, #0090f1); - --mt-tab-active-bg: var(--vscode-tab-activeBackground, #ffffff); - --mt-tab-inactive-bg: var(--vscode-tab-inactiveBackground, #ececec); - --mt-tab-active-fg: var(--vscode-tab-activeForeground, #333333); - --mt-tab-inactive-fg: var(--vscode-tab-inactiveForeground, #8e8e8e); - --mt-tab-selected-bg: var(--vscode-list-activeSelectionBackground, #cce6ff); - --mt-tab-selected-fg: var(--vscode-list-activeSelectionForeground, #000000); - --mt-terminal-bg: var(--vscode-terminal-background, #ffffff); - --mt-terminal-fg: var(--vscode-terminal-foreground, #333333); - --mt-badge-bg: var(--vscode-badge-background, #007acc); - --mt-badge-fg: var(--vscode-badge-foreground, #ffffff); - --mt-error-fg: var(--vscode-errorForeground, #a1260d); - --mt-success-fg: var(--vscode-testing-iconPassed, #388a34); - --mt-warning-fg: var(--vscode-editorWarning-foreground, #bf8803); - --mt-input-bg: var(--vscode-input-background, #ffffff); - --mt-input-border: var(--vscode-input-border, #cecece); - --mt-button-bg: var(--vscode-button-background, #007acc); - --mt-button-fg: var(--vscode-button-foreground, #ffffff); - --mt-button-hover-bg: var(--vscode-button-hoverBackground, #0062a3); - --mt-selection-terminal: var(--vscode-focusBorder, #0090f1); - --mt-selection-workspace: var(--vscode-textLink-foreground, #006ab1); - - --mt-ansi-black: var(--vscode-terminal-ansiBlack, #000000); - --mt-ansi-red: var(--vscode-terminal-ansiRed, #cd3131); - --mt-ansi-green: var(--vscode-terminal-ansiGreen, #00bc00); - --mt-ansi-yellow: var(--vscode-terminal-ansiYellow, #949800); - --mt-ansi-blue: var(--vscode-terminal-ansiBlue, #0451a5); - --mt-ansi-magenta: var(--vscode-terminal-ansiMagenta, #bc05bc); - --mt-ansi-cyan: var(--vscode-terminal-ansiCyan, #0598bc); - --mt-ansi-white: var(--vscode-terminal-ansiWhite, #555555); - --mt-ansi-bright-black: var(--vscode-terminal-ansiBrightBlack, #666666); - --mt-ansi-bright-red: var(--vscode-terminal-ansiBrightRed, #cd3131); - --mt-ansi-bright-green: var(--vscode-terminal-ansiBrightGreen, #14ce14); - --mt-ansi-bright-yellow: var(--vscode-terminal-ansiBrightYellow, #b5ba00); - --mt-ansi-bright-blue: var(--vscode-terminal-ansiBrightBlue, #0451a5); - --mt-ansi-bright-magenta: var(--vscode-terminal-ansiBrightMagenta, #bc05bc); - --mt-ansi-bright-cyan: var(--vscode-terminal-ansiBrightCyan, #0598bc); - --mt-ansi-bright-white: var(--vscode-terminal-ansiBrightWhite, #a5a5a5); - --mt-terminal-cursor: var(--vscode-terminalCursor-foreground, #000000); - --mt-terminal-selection: var(--vscode-terminal-selectionBackground, #add6ff80); + --color-surface: var(--vscode-editor-background, #ffffff); + --color-surface-alt: var(--vscode-editorGroupHeader-tabsBackground, var(--vscode-sideBar-background, #f3f3f3)); + --color-surface-raised: var(--vscode-editorWidget-background, #f3f3f3); + --color-foreground: var(--vscode-editor-foreground, #333333); + --color-muted: var(--vscode-descriptionForeground, #717171); + --color-accent: var(--vscode-focusBorder, #0090f1); + --color-border: var(--vscode-panel-border, #e5e5e5); + --color-tab-active-bg: var(--vscode-tab-activeBackground, #ffffff); + --color-tab-inactive-bg: var(--vscode-tab-inactiveBackground, #ececec); + --color-tab-active-fg: var(--vscode-tab-activeForeground, #333333); + --color-tab-inactive-fg: var(--vscode-tab-inactiveForeground, #8e8e8e); + --color-tab-selected-bg: var(--vscode-list-activeSelectionBackground, #cce6ff); + --color-tab-selected-fg: var(--vscode-list-activeSelectionForeground, #000000); + --color-terminal-bg: var(--vscode-terminal-background, #ffffff); + --color-terminal-fg: var(--vscode-terminal-foreground, #333333); + --color-badge-bg: var(--vscode-badge-background, #007acc); + --color-badge-fg: var(--vscode-badge-foreground, #ffffff); + --color-error: var(--vscode-errorForeground, #a1260d); + --color-success: #388a34; + --color-warning: var(--vscode-editorWarning-foreground, #bf8803); + --color-input-bg: var(--vscode-input-background, #ffffff); + --color-input-border: var(--vscode-input-border, #cecece); + --color-button-bg: var(--vscode-button-background, #007acc); + --color-button-fg: var(--vscode-button-foreground, #ffffff); + --color-button-hover-bg: var(--vscode-button-hoverBackground, #0062a3); } } -/* --- Register semantic tokens with Tailwind v4 --- */ -@theme { - --color-surface: var(--mt-surface); - --color-surface-alt: var(--mt-surface-alt); - --color-surface-raised: var(--mt-surface-raised); - --color-foreground: var(--mt-foreground); - --color-muted: var(--mt-muted); - --color-accent: var(--mt-accent); - --color-border: var(--mt-border); - --color-tab-active-bg: var(--mt-tab-active-bg); - --color-tab-inactive-bg: var(--mt-tab-inactive-bg); - --color-tab-active-fg: var(--mt-tab-active-fg); - --color-tab-inactive-fg: var(--mt-tab-inactive-fg); - --color-tab-selected-bg: var(--mt-tab-selected-bg); - --color-tab-selected-fg: var(--mt-tab-selected-fg); - --color-terminal-bg: var(--mt-terminal-bg); - --color-terminal-fg: var(--mt-terminal-fg); - --color-badge-bg: var(--mt-badge-bg); - --color-badge-fg: var(--mt-badge-fg); - --color-error: var(--mt-error-fg); - --color-success: var(--mt-success-fg); - --color-warning: var(--mt-warning-fg); - --color-input-bg: var(--mt-input-bg); - --color-input-border: var(--mt-input-border); - --color-button-bg: var(--mt-button-bg); - --color-button-fg: var(--mt-button-fg); - --color-button-hover-bg: var(--mt-button-hover-bg); - - --animate-alarm-dot: alarm-dot 2s ease-in-out infinite; -} - @keyframes alarm-dot { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } diff --git a/package.json b/package.json index bcde8fb..d7d2e01 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "build:website": "pnpm --filter mouseterm-website build", "dogfood:vscode": "pnpm run build:vscode && pnpm --filter mouseterm dogfood", "dogfood:standalone": "bash standalone/scripts/dogfood.sh", - "storybook": "pnpm --filter mouseterm-lib storybook" + "storybook": "pnpm --filter mouseterm-lib storybook", + "bundle-themes": "node lib/scripts/bundle-themes.mjs" }, "pnpm": { "onlyBuiltDependencies": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 889448a..5777c9f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,6 +25,12 @@ importers: dockview-react: specifier: ^5.1.0 version: 5.1.0(react@19.2.4) + fflate: + specifier: 0.8.2 + version: 0.8.2 + jsonc-parser: + specifier: 3.3.1 + version: 3.3.1 react: specifier: ^19.2.0 version: 19.2.4 @@ -2095,6 +2101,9 @@ packages: picomatch: optional: true + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -5175,6 +5184,8 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fflate@0.8.2: {} + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 diff --git a/standalone/scripts/dogfood.sh b/standalone/scripts/dogfood.sh index 943fac9..e264cd2 100755 --- a/standalone/scripts/dogfood.sh +++ b/standalone/scripts/dogfood.sh @@ -12,8 +12,8 @@ # # Install mode (--install): # Copies the built files over the system-installed copy, bypassing the slow -# bundling/installer step. Requires a one-time install via the NSIS installer -# so that registry entries, shortcuts, etc. are in place. Currently Windows only. +# installer step. Requires a one-time install first (NSIS installer on Windows, +# DMG on macOS) so that the install location exists. # set -euo pipefail @@ -23,9 +23,14 @@ set -euo pipefail RELEASE_DIR="standalone/src-tauri/target/release" if [[ "${1:-}" == "--install" ]]; then - # Full build with bundling, but disable updater artifact signing + # Full build with bundling, but disable updater artifact signing. + # On macOS, build only the .app bundle (skip DMG creation). + BUNDLE_ARGS=() + case "$(uname -s)" in + Darwin) BUNDLE_ARGS=(--bundles app) ;; + esac pnpm --filter mouseterm-standalone tauri build \ - -c '{"bundle":{"createUpdaterArtifacts":false}}' + -c '{"bundle":{"createUpdaterArtifacts":false}}' "${BUNDLE_ARGS[@]}" else # Fast build: skip bundling entirely since we just need the exe pnpm --filter mouseterm-standalone tauri build --no-bundle @@ -56,6 +61,20 @@ if [[ "${1:-}" == "--install" ]]; then cp -r "$RELEASE_DIR/_up_/" "$INSTALL_DIR/_up_/" echo "✦ Installed to $INSTALL_DIR" ;; + Darwin) + INSTALL_DIR="/Applications/MouseTerm.app" + if [[ ! -d "$INSTALL_DIR" ]]; then + echo "MouseTerm is not installed yet." + echo "Install via the DMG first:" + echo " open $RELEASE_DIR/bundle/dmg/MouseTerm_*.dmg" + echo "" + echo "After that, 'dogfood:standalone --install' will work from then on." + exit 1 + fi + rm -rf "$INSTALL_DIR" + cp -r "$RELEASE_DIR/bundle/macos/MouseTerm.app" "$INSTALL_DIR" + echo "✦ Installed to $INSTALL_DIR" + ;; *) echo "--install is not yet implemented for this platform." exit 1 diff --git a/standalone/src-tauri/Cargo.lock b/standalone/src-tauri/Cargo.lock index 74c181a..35fcf0c 100644 --- a/standalone/src-tauri/Cargo.lock +++ b/standalone/src-tauri/Cargo.lock @@ -1938,7 +1938,7 @@ dependencies = [ [[package]] name = "mouseterm" -version = "0.1.0" +version = "0.6.2" dependencies = [ "libc", "serde", diff --git a/standalone/src-tauri/Cargo.toml b/standalone/src-tauri/Cargo.toml index 6c11a13..58b89aa 100644 --- a/standalone/src-tauri/Cargo.toml +++ b/standalone/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mouseterm" -version = "0.1.0" +version = "0.6.2" description = "Mouse-friendly multitasking terminal" authors = ["DiffPlug"] license = "FSL-1.1-MIT" diff --git a/standalone/src-tauri/icons/128x128.png b/standalone/src-tauri/icons/128x128.png index e436e5f..0300c0c 100644 Binary files a/standalone/src-tauri/icons/128x128.png and b/standalone/src-tauri/icons/128x128.png differ diff --git a/standalone/src-tauri/icons/128x128@2x.png b/standalone/src-tauri/icons/128x128@2x.png index 0087a35..33d8cab 100644 Binary files a/standalone/src-tauri/icons/128x128@2x.png and b/standalone/src-tauri/icons/128x128@2x.png differ diff --git a/standalone/src-tauri/icons/32x32.png b/standalone/src-tauri/icons/32x32.png index 65ee27c..e377fd3 100644 Binary files a/standalone/src-tauri/icons/32x32.png and b/standalone/src-tauri/icons/32x32.png differ diff --git a/standalone/src-tauri/icons/icon.icns b/standalone/src-tauri/icons/icon.icns index b1af88b..a95aee7 100644 Binary files a/standalone/src-tauri/icons/icon.icns and b/standalone/src-tauri/icons/icon.icns differ diff --git a/standalone/src-tauri/icons/icon.ico b/standalone/src-tauri/icons/icon.ico index 0156111..ac1a3cb 100644 Binary files a/standalone/src-tauri/icons/icon.ico and b/standalone/src-tauri/icons/icon.ico differ diff --git a/standalone/src-tauri/tauri.conf.json b/standalone/src-tauri/tauri.conf.json index f8d6d27..9e78e3e 100644 --- a/standalone/src-tauri/tauri.conf.json +++ b/standalone/src-tauri/tauri.conf.json @@ -2,7 +2,7 @@ "$schema": "https://schema.tauri.app/config/2", "productName": "MouseTerm", "version": "0.6.2", - "identifier": "com.mouseterm.app", + "identifier": "com.mouseterm.standalone", "build": { "beforeDevCommand": "pnpm dev", "devUrl": "http://localhost:1420", diff --git a/standalone/src/AppBar.tsx b/standalone/src/AppBar.tsx index b37a50e..47e5dac 100644 --- a/standalone/src/AppBar.tsx +++ b/standalone/src/AppBar.tsx @@ -1,6 +1,7 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import { getCurrentWindow } from '@tauri-apps/api/window'; import { CaretDownIcon, MinusIcon, CornersOutIcon, CornersInIcon, XIcon, TerminalWindowIcon, PlusIcon } from '@phosphor-icons/react'; +import { ThemePicker } from '../../lib/src/components/ThemePicker'; export interface ShellEntry { name: string; @@ -208,11 +209,17 @@ export function AppBar({ projectDir, homeDir, shells }: AppBarProps) { {/* Shell dropdown on the right (macOS) or window controls (Windows/Linux) */} {IS_MAC ? ( -
+
+
) : ( - +
+
+ +
+ +
)}
); diff --git a/standalone/src/main.tsx b/standalone/src/main.tsx index 3c326f7..d85ec2c 100644 --- a/standalone/src/main.tsx +++ b/standalone/src/main.tsx @@ -3,6 +3,13 @@ import { createRoot } from "react-dom/client"; import { invoke } from "@tauri-apps/api/core"; import { setPlatform } from "mouseterm-lib/lib/platform"; import { reconnectFromInit } from "mouseterm-lib/lib/reconnect"; +import { + applyTheme, + getActiveThemeId, + getAllThemes, + getTheme, + setActiveThemeId, +} from "mouseterm-lib/lib/themes"; import App from "mouseterm-lib/App"; import "mouseterm-lib/index.css"; import { TauriAdapter } from "./tauri-adapter"; @@ -14,6 +21,14 @@ import { startUpdateCheck, useUpdateState, dismissBanner, openChangelog } from " const platform = new TauriAdapter(); setPlatform(platform); +function restoreStandaloneTheme() { + const allThemes = getAllThemes(); + const theme = getTheme(getActiveThemeId()) ?? allThemes[0]; + if (!theme) return; + setActiveThemeId(theme.id); + applyTheme(theme); +} + function ConnectedUpdateBanner() { const state = useUpdateState(); return ; @@ -24,6 +39,7 @@ async function bootstrap() { await platform.init(); const { initAlarmStateReceiver } = await import("mouseterm-lib/lib/terminal-registry"); initAlarmStateReceiver(); + restoreStandaloneTheme(); const result = await reconnectFromInit(platform); startUpdateCheck(); diff --git a/website/public/favicon.svg b/website/public/favicon.svg index 030be86..34476e1 100644 --- a/website/public/favicon.svg +++ b/website/public/favicon.svg @@ -6,12 +6,11 @@ sodipodi:docname="favicon.svg" xml:space="preserve" inkscape:version="1.4.3 (0d15f75, 2025-12-25)" - inkscape:export-filename="../icon-1024.png" - inkscape:export-xdpi="96" - inkscape:export-ydpi="96" + inkscape:export-filename="/Users/ntwigg/Downloads/256.png" + inkscape:export-xdpi="24" + inkscape:export-ydpi="24" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:bx="https://boxy-svg.com">Tool Angle