From 260e09ab0bf6ed8a819f60c8f6ffedaf053e242c Mon Sep 17 00:00:00 2001 From: Marlon Marcello Date: Thu, 11 Jun 2026 16:47:55 -0700 Subject: [PATCH 01/20] chore: solid refactor plans and fix tokens --- AGENTS.md | 2 +- plans/PLAN.md | 14 +- plans/SOLID_TUI_REFACTOR.md | 517 ++++++++++++++++++++++++++++++++++++ src/tui/tokens.ts | 48 ++++ 4 files changed, 576 insertions(+), 5 deletions(-) create mode 100644 plans/SOLID_TUI_REFACTOR.md diff --git a/AGENTS.md b/AGENTS.md index 2ecdba3..53b1432 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,7 +18,7 @@ Before making changes, read these files for context: | Concern | Choice | | ----------------- | ------------------------------------------------ | | Runtime | Bun | -| TUI | `@opentui/core` (functional API, no React/Solid) | +| TUI | `@opentui/solid` + `solid-js` | | CLI parser | yargs | | Linter | oxlint | | Formatter | oxfmt | diff --git a/plans/PLAN.md b/plans/PLAN.md index 7064e21..1d23545 100644 --- a/plans/PLAN.md +++ b/plans/PLAN.md @@ -175,14 +175,20 @@ See `MVP.md` for detailed deliverables. ### Phase 2 — TUI Refactor to Solid.js -- Add `@opentui/solid` + `solid-js` as dependencies -- Rewrite `src/tui/app.ts` → Solid root component with `` -- Rewrite `src/tui/components/modal.ts` → `Dialog` component + `useDialog` context (matching OpenCode's dialog pattern) +See `SOLID_TUI_REFACTOR.md` for the detailed implementation plan, UX direction, design tokens, dialog/status bar/command palette architecture, testing boundaries, and migration sequence. + +- Add `@opentui/solid` + `solid-js` + `@opentui/keymap` as dependencies +- Configure TSX (`jsxImportSource`), Bun preload, and Solid build plugin +- Expand `tokens.ts` into full palette + semantic tokens +- Rewrite `src/tui/app.ts` → Solid root component with ``, ``, `` +- Rewrite `src/tui/components/modal.ts` → `DialogProvider` + `UpdateDialog` (OpenCode-inspired dialog pattern) - Rewrite `src/tui/pages/dashboard.ts` → Solid JSX with reactive state - Create keymap module (`useBindings`-style) for keyboard handling - Create theme context (`useTheme`) consuming `tokens.ts` +- Add bottom status bar (mandatory — shows active hotkeys per context) +- Add command palette (mandatory — `ctrl+p` overlay for quick navigation) - Remove all `findDescendantById` patterns -- Update test setup for Solid-based TUI +- Update test setup to cover logic only, not TUI rendering ### Phase 3 — GitHub Repo Creation diff --git a/plans/SOLID_TUI_REFACTOR.md b/plans/SOLID_TUI_REFACTOR.md new file mode 100644 index 0000000..c7cdb43 --- /dev/null +++ b/plans/SOLID_TUI_REFACTOR.md @@ -0,0 +1,517 @@ +# Phase 2 — Solid.js TUI Refactor + +## Status + +- **Started**: — +- **Branch**: `chore/solid-tui-refactor` (suggested) +- **TUI Stack**: `@opentui/solid` + `solid-js` + `@opentui/keymap` + +--- + +## Rationale + +The MVP TUI was built with the functional `@opentui/core` API (`Box`, `Text`, `Select`, imperative callbacks). +This works but does not scale to multi-page navigation, reusable dialogs, keyboard-driven UX, or reactive state. +Moving to Solid.js brings: + +- **Declarative JSX components** — no more manual renderable assembly +- **Fine-grained reactivity** — signals/memos instead of manual `content =` assignments +- **OpenCode-inspired patterns** — `DialogProvider`, `KeymapProvider`, contextual `useBindings`, status bar, command palette +- **Correct keyboard handling** — layered keymaps via `@opentui/keymap`, no ad-hoc `renderer.keyInput.on()` + +--- + +## UX Priorities (In Order) + +1. **Bottom status bar** (mandatory — OpenCode-inspired) + - Always visible at terminal bottom + - Shows available hotkeys for the current context (e.g., `↑↓ navigate · enter select · esc back · q quit`) + - Updates reactively as focus or key layers change +2. **Command palette** (mandatory — VSCode/Slack/OpenCode-style) + - Triggered by a key chord (e.g., `ctrl+p` or `cmd+p`) + - Overlay list of navigable commands + - Cross-cuts all pages and actions +3. **Dialog provider** — stack-based overlay dialogs for alerts, prompts, confirmations, the update notification +4. **Update dialog** — converts current imperative modal into a Solid component within the dialog provider + +--- + +## Token System + +### Palette + +Expand `src/tui/tokens.ts` to include all commented-out website colors as real palette entries. + +```ts +export const palette = { + // Brand core + black: "#101820", + pink50: "#fc6f83", + yellow: "#f8ea36", + teal50: "#9ad9e9", + teal75: "#2daccc", + green: "#8dc975", + white: "#ffffff", + + // Extended + black10: "#e4e4e5", + black25: "#c9c9cb", + black50: "#939497", + black75: "#5e5f61", + blue: "#8599f8", + blue10: "#eef0fe", + blue100: "#081c81", + brown: "#734400", + green10: "#eff7eb", + green100: "#2e5120", + greyBlue: "#334251", + maroon: "#7f0315", + orange: "#f7a836", + purple: "#c98bdb", + red: "#e40526", + skyblue: "#9ad9e9", + teal10: "#e9f7fa", + teal25: "#cdecf4", + teal75: "#2daccc", + yellow10: "#fefce2", + yellow75: "#f7a836", + pink10: "#ffeaed", + pink25: "#fecbd3", +} as const; +``` + +### Semantic Tokens + +Add a `tokens` object that references the palette. This keeps palette as the raw source and tokens as the theme contract so the rest of the codebase never reaches into palette directly. + +```ts +export const tokens = { + // Surfaces + bg: palette.black, + surface: palette.black75, + surfaceRaised: palette.greyBlue, + surfaceOverlay: palette.black, + + // Text + text: palette.white, + textDim: palette.black50, + textMuted: palette.black50, + textAccent: palette.teal75, + textInverse: palette.black, + + // Brand accent + accent: palette.teal75, + accentSoft: palette.teal50, + + // Semantic + success: palette.green, + warning: palette.yellow, + danger: palette.pink50, + info: palette.teal50, + + // Interactive + selectionBg: palette.teal50, + selectionText: palette.black, + + // Borders + border: palette.black75, + borderFocus: palette.teal75, +} as const; +``` + +### Design Direction + +| Role | Color | Notes | +| ----------------- | ----------------------------- | ------------------------------ | +| Background | `#101820` (black) | Dark base | +| Surface | `#5e5f61` (black75) | Cards, dialog panels | +| Primary text | `#ffffff` (white) | High contrast | +| Dim text | `#939497` (black50) | Labels, hints, version strings | +| Accent / focus | `#2daccc` (teal75) | Borders, highlights, CTAs | +| Accent soft | `#9ad9e9` (teal50) | Selection background | +| Callout / brand | `#fc6f83` (pink50) | Titles, warnings, personality | +| Success | `#8dc975` (green) | Confirmations | +| Warning | `#f8ea36` (yellow) | Attention states | + +No ASCII art logos unless they are genuinely brand-relevant. The WTC tiny-font logo from the MVP can stay as a lightweight branding element on the dashboard. + +--- + +## File Structure Changes + +``` +src/tui/ +├── app.tsx # Solid root — renderer creation, keymap init, providers +├── theme.tsx # ThemeProvider + useTheme context +├── keymap.tsx # KeymapProvider, re-export useBindings/useKeymapSelector +├── tokens.ts # Palette + semantic tokens (expand existing) +├── components/ +│ ├── dialog.tsx # DialogProvider + useDialog + base Dialog overlay +│ ├── dialog-alert.tsx # Alert dialog (simplified from OpenCode pattern) +│ ├── update-dialog.tsx# Update available notification dialog +│ ├── status-bar.tsx # Bottom bar showing active hotkeys +│ └── command-palette.tsx # ctrl+p overlay command list +└── pages/ + └── dashboard.tsx # Solid JSX version of the dashboard +``` + +--- + +## Migration Steps + +### Step 1 — Add Dependencies and Config + +Package manager additions in `package.json`: + +- `solid-js` (dependency) +- `@opentui/solid` (dependency) +- `@opentui/keymap` (dependency) + +`tsconfig.json` additions: + +```json +"jsx": "preserve", +"jsxImportSource": "@opentui/solid" +``` + +`bunfig.toml` creation (or addition) — needed for Solid JSX runtime: + +```toml +preload = ["@opentui/solid/preload"] +``` + +`scripts/build.ts` — add the Solid Bun plugin: + +```ts +import solidPlugin from "@opentui/solid/bun-plugin" +// … in Bun.build config: +plugins: [solidPlugin], +``` + +### Step 2 — Expand Tokens + +Edit `src/tui/tokens.ts` to include the full `palette` and the updated `tokens` object described above. + +### Step 3 — Create Theme Context + +File: `src/tui/theme.tsx` + +```ts +import { createContext, useContext, type ParentProps } from "solid-js" +import { tokens } from "./tokens" +import { useRenderer } from "@opentui/solid" +import { onMount } from "solid-js" + +export type Theme = typeof tokens + +const ThemeContext = createContext() + +export function ThemeProvider(props: ParentProps) { + const renderer = useRenderer() + onMount(() => renderer.setBackgroundColor(tokens.bg)) + return ( + + {props.children} + + ) +} + +export function useTheme(): Theme { + const value = useContext(ThemeContext) + if (!value) throw new Error("useTheme must be used within a ThemeProvider") + return value +} +``` + +This stays minimal — no external theme loading, no mode switching. + +### Step 4 — Create Keymap Module + +File: `src/tui/keymap.tsx` + +```ts +import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui" +import { KeymapProvider, useBindings, useKeymapSelector } from "@opentui/keymap/solid" +import type { CliRenderer } from "@opentui/core" + +export function createKeymap(renderer: CliRenderer) { + return createDefaultOpenTuiKeymap(renderer) +} + +export { KeymapProvider, useBindings, useKeymapSelector } +``` + +Global bindings (quit, status bar update) are registered inside `app.tsx` via `useBindings`. + +### Step 5 — Create Dialog Provider + +File: `src/tui/components/dialog.tsx` + +Simplified from OpenCode's pattern: + +- `DialogProvider` wraps children + a portal overlay +- `useDialog()` returns `{ show, replace, clear }` +- `show(element, onClose?)` pushes onto a stack +- `replace(element, onClose?)` replaces the stack (for single-dialog mode) +- `clear()` closes all +- Escape key dispatches `clear` via `useBindings` +- Base `` component renders a full-screen semi-transparent overlay with a centered raised panel + +The implementation: + +- Uses `createStore` with a `stack` array +- `Portal` from `@opentui/solid` to mount the overlay outside the main tree root +- The overlay box has `position: "absolute"`, `zIndex: 3000`, `backgroundColor: RGBA.fromInts(...)` for the dimming effect +- Dialog panel width is capped at 60 columns by default (or configurable) + +### Step 6 — Create Update Dialog + +File: `src/tui/components/update-dialog.tsx` + +Replaces the imperative `createModal` from MVP. + +Props: + +```ts +interface UpdateDialogProps { + currentVersion: string + latestVersion: string + repo: string +} +``` + +Layout: + +``` +┌─────────────────────────────────────┐ +│ Update Available │ +│ │ +│ v0.1.9 → v0.2.0 │ +│ │ +│ curl -fsSL https://...install.sh │ +│ | bash │ +│ │ +│ [enter] close │ +└─────────────────────────────────────┘ +``` + +Behavior: + +- `enter` or `escape` closes via `dialog.clear()` +- Uses `useBindings` for keyboard handling, not `renderer.keyInput.on()` +- Text colors from tokens: title in `pink50`, version in `teal75`, command on `surfaceRaised` background + +### Step 7 — Convert Dashboard to Solid JSX + +File: `src/tui/pages/dashboard.tsx` + +Replace `createDashboard()` with: + +```tsx +import { ascii_font, box, select, text } from "@opentui/solid" +import { useTheme } from "../theme" + +export function Dashboard(props: { version: string }) { + const theme = useTheme() + + return ( + + + What will you build? + ` with navigable commands (future: page navigation, feature actions, upgrade check, etc.) +- On select, dispatches the command and closes +- Escape closes without action + +```tsx +export function CommandPalette(props: { onClose: () => void }) { + const theme = useTheme() + const [query, setQuery] = createSignal("") + const [selectedIndex, setSelectedIndex] = createSignal(0) + + // filtered commands based on query + // keybinding: enter to select, esc to close, up/down to navigate + + return ( + + + + + ) +} +``` + +The palette is rendered through `DialogProvider` via `dialog.replace()`. + +### Step 10 — Rewrite App Root + +File: `src/tui/app.tsx` + +```tsx +import { render } from "@opentui/solid" +import { createKeymap, KeymapProvider } from "./keymap" +import { ThemeProvider } from "./theme" +import { DialogProvider } from "./components/dialog" +import { StatusBar } from "./components/status-bar" +import { Dashboard } from "./pages/dashboard" +import { UpdateDialog } from "./components/update-dialog" +import { checkForUpdate } from "../utils/update-check" +import { useBindings } from "./keymap" +import { onMount } from "solid-js" +import { APP_VERSION } from "../version" + +const REPO = "wethegit/wtc" + +export async function launchDashboard(version = APP_VERSION): Promise { + const renderer = await createCliRenderer({ exitOnCtrlC: true, backgroundColor: tokens.bg }) + const keymap = createKeymap(renderer) + + function Root() { + const dialog = useDialog() + + useBindings(() => ({ + bindings: [ + { key: "ctrl+p", cmd: "command-palette.show", desc: "Command palette" }, + { key: "q", cmd: "quit", run: () => renderer.destroy() }, + ], + })) + + onMount(() => { + checkForUpdate(version).then((info) => { + if (info.updateAvailable) { + dialog.replace( + () => , + ) + } + }) + }) + + return ( + + + + + ) + } + + await render( + () => ( + + + + + + + + ), + renderer, + ) +} +``` + +--- + +## Testing Strategy + +Per the testing philosophy in `AGENTS.md`: + +- **Do not test TUI rendering.** No tests for box dimensions, text positions, ASCII art, or styling tokens. +- **No mocks of `@opentui/solid` or `@opentui/core`.** If a function delegates to OpenTUI, trust it. +- **Remove existing dashboard tests** that assert text content (`expect(frame).toContain(...)`) — those test the renderer's output, not our logic. +- **Test pure logic and state transformations:** + - `UpdateDialog` — test that correct version strings and install command are constructed + - `CommandPalette` — test command filtering against a query string (pure function) + - `StatusBar` — test key metadata formatting (pure function that maps binding arrays to display strings) + - `tokens.ts` — verify palette values are readonly and token keys map to palette keys (structural contract) + - `update-check.ts` — existing tests already cover this well; keep them +- **Future tests:** dialogs' `onClose` callbacks, command dispatch behavior (if factored as testable functions). + +--- + +## Future-Proofing Notes + +- The keymap module location (`src/tui/keymap.tsx`) can later grow command registration helpers when Phase 3+ adds real features. +- The status bar is intentionally simple — it can become richer (git branch, AWS profile, timer status) in Phase 5-6. +- The command palette is a shell — its command list is populated as real commands are built. +- DialogProvider supports a stack but we only use single-dialog mode for now. Stack semantics are ready for multi-dialog flows like wizards. +- No router is introduced yet. The MVP dashboard nav leads to "coming soon" placeholders; real page routing is added per feature phase. + +--- + +## Verification Checklist + +Before committing: + +- [ ] `bun run fmt:check` passes +- [ ] `bun run lint` passes +- [ ] `bun run check` passes (tsc) +- [ ] `bun test` passes +- [ ] `bun run build` produces working binary +- [ ] Old dashboard tests removed / replaced with logic-based tests +- [ ] AGENTS.md updated (already done above if linked) +- [ ] PLANS.md Phase 2 updated to reference this doc diff --git a/src/tui/tokens.ts b/src/tui/tokens.ts index 7d60b64..bcc4fc1 100644 --- a/src/tui/tokens.ts +++ b/src/tui/tokens.ts @@ -1,3 +1,51 @@ +/** + --color-black: #101820; + --color-black-10: #e4e4e5; + --color-black-25: #c9c9cb; + --color-black-50: #939497; + --color-black-75: #5e5f61; + --color-blue: #8599f8; + --color-blue-10: #eef0fe; + --color-blue-100: #081c81; + --color-blue-25: #cbd3fc; + --color-blue-75: #3958f3; + --color-blue-50: #8599f8; + --color-brown: #734400; + --color-green: #8dc975; + --color-green-10: #eff7eb; + --color-green-100: #2e5120; + --color-green-25: #d6eccd; + --color-green-75: #498233; + --color-grey-blue: #334251; + --color-grey-blue-10: #e9eef4; + --color-grey-blue-25: #becede; + --color-grey-blue-50: #658bb1; + --color-grey-blue-75: #526b83; + --color-maroon: #7f0315; + --color-orange: #f7a836; + --color-pink: #fc6f83; + --color-pink-10: #ffeaed; + --color-pink-25: #fecbd3; + --color-pink-50: #fc6f83; + --color-purple: #c98bdb; + --color-purple-10: #f0def5; + --color-purple-100: #742a89; + --color-purple-25: #e4c4ed; + --color-purple-75: #a63cc3; + --color-purple-50: #c98bdb; + --color-red: #e40526; + --color-skyblue: #9ad9e9; + --color-teal: #144d5b; + --color-teal-50: #9ad9e9; + --color-teal-10: #e9f7fa; + --color-teal-75: #2daccc; + --color-teal-25: #cdecf4; + --color-white: #fff; + --color-yellow: #f8ea36; + --color-yellow-10: #fefce2; + --color-yellow-25: #fcf7af; + --color-yellow-75: #f7a836; + */ export const palette = { black: "#101820", teal50: "#9ad9e9", From daf1d9a564768ca2b7013bb7973be6e4977fb9ae Mon Sep 17 00:00:00 2001 From: Marlon Marcello Date: Thu, 11 Jun 2026 16:49:18 -0700 Subject: [PATCH 02/20] chore: add solid-js, @opentui/solid, @opentui/keymap deps and JSX config - Add dependency packages - Configure tsconfig.json with jsxImportSource - Create bunfig.toml with Solid preload - Add Solid bun-plugin to build script - Update AGENTS.md tech stack --- AGENTS.md | 24 ++-- bun.lock | 217 ++++++++++++++++++++++++++++++++++++ bunfig.toml | 1 + package.json | 3 + plans/SOLID_TUI_REFACTOR.md | 116 +++++++++---------- scripts/build.ts | 3 + tsconfig.json | 2 + 7 files changed, 297 insertions(+), 69 deletions(-) create mode 100644 bunfig.toml diff --git a/AGENTS.md b/AGENTS.md index 53b1432..e2982bd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,18 +15,18 @@ Before making changes, read these files for context: ## Tech Stack -| Concern | Choice | -| ----------------- | ------------------------------------------------ | -| Runtime | Bun | -| TUI | `@opentui/solid` + `solid-js` | -| CLI parser | yargs | -| Linter | oxlint | -| Formatter | oxfmt | -| Test runner | bun test | -| Pre-commit | husky + lint-staged | -| Release versions | Changesets | -| Encryption | Web Crypto (AES-256-GCM + PBKDF2) | -| Config validation | zod | +| Concern | Choice | +| ----------------- | --------------------------------- | +| Runtime | Bun | +| TUI | `@opentui/solid` + `solid-js` | +| CLI parser | yargs | +| Linter | oxlint | +| Formatter | oxfmt | +| Test runner | bun test | +| Pre-commit | husky + lint-staged | +| Release versions | Changesets | +| Encryption | Web Crypto (AES-256-GCM + PBKDF2) | +| Config validation | zod | ## Conventions diff --git a/bun.lock b/bun.lock index a49e1a0..57ae187 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,9 @@ "name": "my-opentui-project", "dependencies": { "@opentui/core": "^0.4.0", + "@opentui/keymap": "^0.4.1", + "@opentui/solid": "^0.4.1", + "solid-js": "^1.9.13", "yargs": "^18.0.0", "zod": "^4.4.3", }, @@ -25,8 +28,66 @@ }, }, "packages": { + "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], + + "@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], + + "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], + + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], + + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], + + "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], + + "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], + + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.29.7", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="], + + "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], + + "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], + + "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="], + + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA=="], + + "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.29.7", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ=="], + + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-syntax-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw=="], + + "@babel/preset-typescript": ["@babel/preset-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="], + "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], + "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], + + "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], + + "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + "@changesets/apply-release-plan": ["@changesets/apply-release-plan@7.1.1", "", { "dependencies": { "@changesets/config": "^3.1.4", "@changesets/get-version-range-type": "^0.4.0", "@changesets/git": "^3.0.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "detect-indent": "^6.0.0", "fs-extra": "^7.0.1", "lodash.startcase": "^4.4.0", "outdent": "^0.5.0", "prettier": "^2.7.1", "resolve-from": "^5.0.0", "semver": "^7.5.3" } }, "sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA=="], "@changesets/assemble-release-plan": ["@changesets/assemble-release-plan@6.0.10", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "semver": "^7.5.3" } }, "sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A=="], @@ -67,6 +128,14 @@ "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@manypkg/find-root": ["@manypkg/find-root@1.1.0", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@types/node": "^12.7.1", "find-up": "^4.1.0", "fs-extra": "^8.1.0" } }, "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA=="], "@manypkg/get-packages": ["@manypkg/get-packages@1.1.3", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@changesets/types": "^4.0.1", "@manypkg/find-root": "^1.1.0", "fs-extra": "^8.1.0", "globby": "^11.0.0", "read-yaml-file": "^1.1.0" } }, "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A=="], @@ -95,6 +164,10 @@ "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.4.0", "", { "os": "win32", "cpu": "x64" }, "sha512-3uHjGfgjhw+tyxDoF6bs11Gbrt+8VP7k6vAMm9Qyvd8cExDFaNcR1E5Tf/6yR7oxGfh9E951ALue8M7h6l/lLQ=="], + "@opentui/keymap": ["@opentui/keymap@0.4.1", "", { "dependencies": { "@opentui/core": "0.4.1" }, "peerDependencies": { "@opentui/react": "0.4.1", "@opentui/solid": "0.4.1", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-4Wds3HynfTPURawkdlO8CjrzfSvxSF5RO2jlpv6etT+JTQjKSKdVrBcza6N77qWqKy0Qi0D/u2yuN6aTUJ0Z7Q=="], + + "@opentui/solid": ["@opentui/solid@0.4.1", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.4.1", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-CD3M3mlUFwy2r8fd43MFY8kR7bFO+CfL1LHjV6+adp7T4jpadb9zuhfKKJpMzSdgAqQof+F5ifGMx6RljiCmwQ=="], + "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.54.0", "", { "os": "android", "cpu": "arm" }, "sha512-NAtpl/SiaeU103e7/OmZw0MvUnsUUopW7hEm/ecegJg7YM0skQaA0IXEZoyTV6NUdiNPupdIUreRqUZTShbn/g=="], "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.54.0", "", { "os": "android", "cpu": "arm64" }, "sha512-B4VZfBUlKK1rmMChsssNZbkZjE8+FzG3avMjGgMDwbGxXRoXkoeXiAZ+78Oa+eyDPHvDCiUb4zH/vmCOUSafLQ=="], @@ -191,14 +264,30 @@ "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.7", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-/O6JWUmjv03OI9lL2ry9bUjpD5S3PclM55RRJEyCdcFZ5W2SEA/59d+l2hNsk3gI6kiWRdRPdOtqZmsQzFN1pQ=="], + + "babel-plugin-module-resolver": ["babel-plugin-module-resolver@5.0.2", "", { "dependencies": { "find-babel-config": "^2.1.1", "glob": "^9.3.3", "pkg-up": "^3.1.0", "reselect": "^4.1.7", "resolve": "^1.22.8" } }, "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg=="], + + "babel-preset-solid": ["babel-preset-solid@1.9.12", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.6" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.12" }, "optionalPeers": ["solid-js"] }, "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.36", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lVq/Df7LXlO79MVaaUHztSwWiG9oXoWHlgvNS51v8Dpd4+G4/VIy6qYePTw31nAVls33nUtnfezYeLkYAak9dg=="], + "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="], + "brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], + "bun-ffi-structs": ["bun-ffi-structs@0.2.3", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-pgJiXP+hEgFo9qG51J6ItfY4ocs3vniwNzJ9WhoakB3QB2GdzQxX2EXssentPYlB2hOfJrTjO6iIQkWYzUodpg=="], "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "caniuse-lite": ["caniuse-lite@1.0.30001799", "", {}, "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw=="], + "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], @@ -207,10 +296,16 @@ "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "dataloader": ["dataloader@1.4.0", "", {}, "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="], "diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="], @@ -219,12 +314,18 @@ "dotenv": ["dotenv@8.6.0", "", {}, "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g=="], + "electron-to-chromium": ["electron-to-chromium@1.5.371", "", {}, "sha512-e9htk9mAYL6AzmkEhSvVVw7IWGSBJ/Bqdn2eRyRLrj1g6sncN4WbFt5qnILYoCktktr45pyjIrOiRvBThQ808w=="], + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], + "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], @@ -239,20 +340,34 @@ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="], + "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], "fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + "glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + + "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], + "human-id": ["human-id@4.2.0", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-K3GbkIWqyvvlpfhBPlbEvD97TtqBpAYA4kt+cn2lD2x2HuohzZCibcA2nOlnJT6exqvJLggoB5nv2dNf192nEA=="], "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], @@ -261,6 +376,8 @@ "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], @@ -275,8 +392,14 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], "lint-staged": ["lint-staged@17.0.7", "", { "dependencies": { "listr2": "^10.2.1", "picomatch": "^4.0.4", "string-argv": "^0.3.2", "tinyexec": "^1.2.4" }, "optionalDependencies": { "yaml": "^2.9.0" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-JrSobt+tW3rH8IOMi8tDZd3foorM5yPEkLD/V2NxobgHrFfHWGee4MOLVuZeScgxftEwbHrPHIFA/ZL+nUJeuA=="], @@ -289,6 +412,8 @@ "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], @@ -297,10 +422,18 @@ "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + "minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="], + + "minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + "node-releases": ["node-releases@2.0.47", "", {}, "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og=="], + "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], "outdent": ["outdent@0.5.0", "", {}, "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q=="], @@ -321,10 +454,16 @@ "package-manager-detector": ["package-manager-detector@0.2.11", "", { "dependencies": { "quansync": "^0.2.7" } }, "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ=="], + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], @@ -333,6 +472,8 @@ "pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], + "pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="], + "prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], @@ -341,6 +482,10 @@ "read-yaml-file": ["read-yaml-file@1.1.0", "", { "dependencies": { "graceful-fs": "^4.1.5", "js-yaml": "^3.6.1", "pify": "^4.0.1", "strip-bom": "^3.0.0" } }, "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA=="], + "reselect": ["reselect@4.1.8", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="], + + "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], @@ -351,10 +496,16 @@ "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "s-js": ["s-js@0.4.9", "", {}, "sha512-RtpOm+cM6O0sHg6IA70wH+UC3FZcND+rccBZpBAHzlUgNO2Bm5BN+FnM8+OBxzXdwpKWFwX11JGF0MFRkhSoIQ=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "semver": ["semver@7.8.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-wnilbGyMxzbY7dNOl7jpKbLSjcfeweJWU5j4+u5qW+6/wuGD9KzIGOyZnQVSBM9E7DtWaaH3CyHkppYrKYoxwg=="], + "seroval": ["seroval@1.5.4", "", {}, "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw=="], + + "seroval-plugins": ["seroval-plugins@1.5.4", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], @@ -365,6 +516,8 @@ "slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], + "solid-js": ["solid-js@1.9.13", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-6hJeJMOcEX8ktqjpDoJZEmld3ijvcvWBDtiXBm7f4332SiFN66QeAQI1REQshvyUoISsSeJ4PHDauKYbwao9JQ=="], + "spawndamnit": ["spawndamnit@3.0.1", "", { "dependencies": { "cross-spawn": "^7.0.5", "signal-exit": "^4.0.1" } }, "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg=="], "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], @@ -377,6 +530,8 @@ "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="], "tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], @@ -393,6 +548,8 @@ "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + "web-tree-sitter": ["web-tree-sitter@0.25.10", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="], "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], @@ -405,6 +562,8 @@ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], "yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], @@ -415,6 +574,12 @@ "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@manypkg/find-root/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], "@manypkg/find-root/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], @@ -423,6 +588,12 @@ "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + "@opentui/keymap/@opentui/core": ["@opentui/core@0.4.1", "", { "dependencies": { "bun-ffi-structs": "0.2.3", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.1", "@opentui/core-darwin-x64": "0.4.1", "@opentui/core-linux-arm64": "0.4.1", "@opentui/core-linux-arm64-musl": "0.4.1", "@opentui/core-linux-x64": "0.4.1", "@opentui/core-linux-x64-musl": "0.4.1", "@opentui/core-win32-arm64": "0.4.1", "@opentui/core-win32-x64": "0.4.1" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-ejlunoFCGLcghYGdfamI/DlWHsgCTLbuoL2JeOmFuLsN+DM5phje3CQbGR4tpl24cadCgHJQFomjoQ9Htvin+Q=="], + + "@opentui/solid/@opentui/core": ["@opentui/core@0.4.1", "", { "dependencies": { "bun-ffi-structs": "0.2.3", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.1", "@opentui/core-darwin-x64": "0.4.1", "@opentui/core-linux-arm64": "0.4.1", "@opentui/core-linux-arm64-musl": "0.4.1", "@opentui/core-linux-x64": "0.4.1", "@opentui/core-linux-x64-musl": "0.4.1", "@opentui/core-win32-arm64": "0.4.1", "@opentui/core-win32-x64": "0.4.1" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-ejlunoFCGLcghYGdfamI/DlWHsgCTLbuoL2JeOmFuLsN+DM5phje3CQbGR4tpl24cadCgHJQFomjoQ9Htvin+Q=="], + + "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], + "cli-truncate/string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], "cliui/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], @@ -435,12 +606,58 @@ "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "path-scurry/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "pkg-up/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], + "read-yaml-file/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], "wrap-ansi/string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], + "@opentui/keymap/@opentui/core/@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.4.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ocs73hj9n0zLArOTpUWIXCWU6ERThG+3wQzO78EvfaR4hb5FRrDHGKWTzXpr6ukSKsUtKdztK5XYTPsJ5e3vww=="], + + "@opentui/keymap/@opentui/core/@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.4.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-YogRtDBfxGeOkcSHDMsGkKFBIt3cWPMPGNu2AmEN6a5KKjDYwAZCudwbDJaUbZDCJjfAUHz9iXjhJVXJBXs9vQ=="], + + "@opentui/keymap/@opentui/core/@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-sBZTS1eEGeVSQ8fAmDALKQcT7FckrhK64oHfEO7W0lJ+lXapfJuOKtTM33na54V56GAM9guk4RD4cbPeTXEh4g=="], + + "@opentui/keymap/@opentui/core/@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Eps9qB+vQ/Lel4ZYqMH87Um9oiU17Vu4oWzvRi40Yf+69vA1a3R4D7KUCeY3OxKWnRnwAHkMU9TxNnjKngPH/w=="], + + "@opentui/keymap/@opentui/core/@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-9/xjYGzX5RdUl0qmGQY0OCayjJ4VffDhsBmApQdseUkMT6LGL3RumI4zPK3Y9vo1fuy6ffLnriLFOktOgutXDg=="], + + "@opentui/keymap/@opentui/core/@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-UYcp8XGX4DZXN+VYUVuCrJkbFMJ0L+VUVu0t5KqqaeJ74fI4NZ+DmwNqPPg1+C+EIzoW4QChlEUdhlZRdiEQiA=="], + + "@opentui/keymap/@opentui/core/@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.4.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-hkIbpUJcKd4iLetTygPlFS45teOBTto49aXuxNeafYQUNd3ehiSwJENNBlAGULLfq+KP3dMJoWUiOcbuPVOQRA=="], + + "@opentui/keymap/@opentui/core/@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.4.1", "", { "os": "win32", "cpu": "x64" }, "sha512-s1kGBcloy4ksl3wFCMqqOUFtXWlTTpzxe6pkLFhFhzgqKsMXHE9pwebiS3pzJkkFUxah5MYG+kbcn2Dw2Gdncg=="], + + "@opentui/solid/@opentui/core/@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.4.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ocs73hj9n0zLArOTpUWIXCWU6ERThG+3wQzO78EvfaR4hb5FRrDHGKWTzXpr6ukSKsUtKdztK5XYTPsJ5e3vww=="], + + "@opentui/solid/@opentui/core/@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.4.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-YogRtDBfxGeOkcSHDMsGkKFBIt3cWPMPGNu2AmEN6a5KKjDYwAZCudwbDJaUbZDCJjfAUHz9iXjhJVXJBXs9vQ=="], + + "@opentui/solid/@opentui/core/@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-sBZTS1eEGeVSQ8fAmDALKQcT7FckrhK64oHfEO7W0lJ+lXapfJuOKtTM33na54V56GAM9guk4RD4cbPeTXEh4g=="], + + "@opentui/solid/@opentui/core/@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Eps9qB+vQ/Lel4ZYqMH87Um9oiU17Vu4oWzvRi40Yf+69vA1a3R4D7KUCeY3OxKWnRnwAHkMU9TxNnjKngPH/w=="], + + "@opentui/solid/@opentui/core/@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-9/xjYGzX5RdUl0qmGQY0OCayjJ4VffDhsBmApQdseUkMT6LGL3RumI4zPK3Y9vo1fuy6ffLnriLFOktOgutXDg=="], + + "@opentui/solid/@opentui/core/@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-UYcp8XGX4DZXN+VYUVuCrJkbFMJ0L+VUVu0t5KqqaeJ74fI4NZ+DmwNqPPg1+C+EIzoW4QChlEUdhlZRdiEQiA=="], + + "@opentui/solid/@opentui/core/@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.4.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-hkIbpUJcKd4iLetTygPlFS45teOBTto49aXuxNeafYQUNd3ehiSwJENNBlAGULLfq+KP3dMJoWUiOcbuPVOQRA=="], + + "@opentui/solid/@opentui/core/@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.4.1", "", { "os": "win32", "cpu": "x64" }, "sha512-s1kGBcloy4ksl3wFCMqqOUFtXWlTTpzxe6pkLFhFhzgqKsMXHE9pwebiS3pzJkkFUxah5MYG+kbcn2Dw2Gdncg=="], + "enquirer/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], + "read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], + + "pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], } } diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..7693482 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1 @@ +preload = ["@opentui/solid/preload"] diff --git a/package.json b/package.json index 6fd5221..01be441 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,9 @@ }, "dependencies": { "@opentui/core": "^0.4.0", + "@opentui/keymap": "^0.4.1", + "@opentui/solid": "^0.4.1", + "solid-js": "^1.9.13", "yargs": "^18.0.0", "zod": "^4.4.3" }, diff --git a/plans/SOLID_TUI_REFACTOR.md b/plans/SOLID_TUI_REFACTOR.md index c7cdb43..9d52100 100644 --- a/plans/SOLID_TUI_REFACTOR.md +++ b/plans/SOLID_TUI_REFACTOR.md @@ -2,8 +2,8 @@ ## Status -- **Started**: — -- **Branch**: `chore/solid-tui-refactor` (suggested) +- **Started**: — +- **Branch**: `chore/solid-tui-refactor` (suggested) - **TUI Stack**: `@opentui/solid` + `solid-js` + `@opentui/keymap` --- @@ -121,17 +121,17 @@ export const tokens = { ### Design Direction -| Role | Color | Notes | -| ----------------- | ----------------------------- | ------------------------------ | -| Background | `#101820` (black) | Dark base | -| Surface | `#5e5f61` (black75) | Cards, dialog panels | -| Primary text | `#ffffff` (white) | High contrast | -| Dim text | `#939497` (black50) | Labels, hints, version strings | -| Accent / focus | `#2daccc` (teal75) | Borders, highlights, CTAs | -| Accent soft | `#9ad9e9` (teal50) | Selection background | -| Callout / brand | `#fc6f83` (pink50) | Titles, warnings, personality | -| Success | `#8dc975` (green) | Confirmations | -| Warning | `#f8ea36` (yellow) | Attention states | +| Role | Color | Notes | +| --------------- | ------------------- | ------------------------------ | +| Background | `#101820` (black) | Dark base | +| Surface | `#5e5f61` (black75) | Cards, dialog panels | +| Primary text | `#ffffff` (white) | High contrast | +| Dim text | `#939497` (black50) | Labels, hints, version strings | +| Accent / focus | `#2daccc` (teal75) | Borders, highlights, CTAs | +| Accent soft | `#9ad9e9` (teal50) | Selection background | +| Callout / brand | `#fc6f83` (pink50) | Titles, warnings, personality | +| Success | `#8dc975` (green) | Confirmations | +| Warning | `#f8ea36` (yellow) | Attention states | No ASCII art logos unless they are genuinely brand-relevant. The WTC tiny-font logo from the MVP can stay as a lightweight branding element on the dashboard. @@ -230,15 +230,15 @@ This stays minimal — no external theme loading, no mode switching. File: `src/tui/keymap.tsx` ```ts -import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui" -import { KeymapProvider, useBindings, useKeymapSelector } from "@opentui/keymap/solid" -import type { CliRenderer } from "@opentui/core" +import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"; +import { KeymapProvider, useBindings, useKeymapSelector } from "@opentui/keymap/solid"; +import type { CliRenderer } from "@opentui/core"; export function createKeymap(renderer: CliRenderer) { - return createDefaultOpenTuiKeymap(renderer) + return createDefaultOpenTuiKeymap(renderer); } -export { KeymapProvider, useBindings, useKeymapSelector } +export { KeymapProvider, useBindings, useKeymapSelector }; ``` Global bindings (quit, status bar update) are registered inside `app.tsx` via `useBindings`. @@ -274,9 +274,9 @@ Props: ```ts interface UpdateDialogProps { - currentVersion: string - latestVersion: string - repo: string + currentVersion: string; + latestVersion: string; + repo: string; } ``` @@ -308,11 +308,11 @@ File: `src/tui/pages/dashboard.tsx` Replace `createDashboard()` with: ```tsx -import { ascii_font, box, select, text } from "@opentui/solid" -import { useTheme } from "../theme" +import { ascii_font, box, select, text } from "@opentui/solid"; +import { useTheme } from "../theme"; export function Dashboard(props: { version: string }) { - const theme = useTheme() + const theme = useTheme(); return ( @@ -331,7 +331,7 @@ export function Dashboard(props: { version: string }) { /> v{props.version} · Press Ctrl+C to exit - ) + ); } ``` @@ -344,14 +344,14 @@ File: `src/tui/components/status-bar.tsx` A bottom-pinned bar that shows active keybinding hints for the current context. This is the **mandatory** UX pattern from OpenCode. ```tsx -import { useTheme } from "../theme" -import { useKeymapSelector } from "../keymap" -import { useDialog } from "./dialog" +import { useTheme } from "../theme"; +import { useKeymapSelector } from "../keymap"; +import { useDialog } from "./dialog"; export function StatusBar() { - const theme = useTheme() - const dialog = useDialog() - const activeKeys = useKeymapSelector((km) => km.getActiveKeys({ includeMetadata: true })) + const theme = useTheme(); + const dialog = useDialog(); + const activeKeys = useKeymapSelector((km) => km.getActiveKeys({ includeMetadata: true })); return ( {activeKeys().length > 0 - ? activeKeys().map((k) => k.display ?? k.key).join(" · ") + ? activeKeys() + .map((k) => k.display ?? k.key) + .join(" · ") : "↑↓ navigate · enter select · esc back · q quit"} - ) + ); } ``` @@ -413,50 +415,50 @@ The palette is rendered through `DialogProvider` via `dialog.replace()`. File: `src/tui/app.tsx` ```tsx -import { render } from "@opentui/solid" -import { createKeymap, KeymapProvider } from "./keymap" -import { ThemeProvider } from "./theme" -import { DialogProvider } from "./components/dialog" -import { StatusBar } from "./components/status-bar" -import { Dashboard } from "./pages/dashboard" -import { UpdateDialog } from "./components/update-dialog" -import { checkForUpdate } from "../utils/update-check" -import { useBindings } from "./keymap" -import { onMount } from "solid-js" -import { APP_VERSION } from "../version" - -const REPO = "wethegit/wtc" +import { render } from "@opentui/solid"; +import { createKeymap, KeymapProvider } from "./keymap"; +import { ThemeProvider } from "./theme"; +import { DialogProvider } from "./components/dialog"; +import { StatusBar } from "./components/status-bar"; +import { Dashboard } from "./pages/dashboard"; +import { UpdateDialog } from "./components/update-dialog"; +import { checkForUpdate } from "../utils/update-check"; +import { useBindings } from "./keymap"; +import { onMount } from "solid-js"; +import { APP_VERSION } from "../version"; + +const REPO = "wethegit/wtc"; export async function launchDashboard(version = APP_VERSION): Promise { - const renderer = await createCliRenderer({ exitOnCtrlC: true, backgroundColor: tokens.bg }) - const keymap = createKeymap(renderer) + const renderer = await createCliRenderer({ exitOnCtrlC: true, backgroundColor: tokens.bg }); + const keymap = createKeymap(renderer); function Root() { - const dialog = useDialog() + const dialog = useDialog(); useBindings(() => ({ bindings: [ { key: "ctrl+p", cmd: "command-palette.show", desc: "Command palette" }, { key: "q", cmd: "quit", run: () => renderer.destroy() }, ], - })) + })); onMount(() => { checkForUpdate(version).then((info) => { if (info.updateAvailable) { - dialog.replace( - () => , - ) + dialog.replace(() => ( + + )); } - }) - }) + }); + }); return ( - ) + ); } await render( @@ -470,7 +472,7 @@ export async function launchDashboard(version = APP_VERSION): Promise { ), renderer, - ) + ); } ``` diff --git a/scripts/build.ts b/scripts/build.ts index bfb3fad..2cc5375 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -29,8 +29,11 @@ await Bun.$`mkdir -p ${outdir}`.quiet(); console.log(`Building ${outfilePath} (v${version})...`); +import solidPlugin from "@opentui/solid/bun-plugin"; + const result = await Bun.build({ entrypoints: ["./src/index.ts"], + plugins: [solidPlugin], compile: { target: bunTarget, outfile: `./${outfilePath}`, diff --git a/tsconfig.json b/tsconfig.json index d4467c8..d0a0c3c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,8 @@ "module": "Preserve", "moduleDetection": "force", "allowJs": true, + "jsx": "preserve", + "jsxImportSource": "@opentui/solid", // Bundler mode "moduleResolution": "bundler", From 8a71dd481934d5b1e0ae3887d5bedc9ec711b3f2 Mon Sep 17 00:00:00 2001 From: Marlon Marcello Date: Thu, 11 Jun 2026 16:49:52 -0700 Subject: [PATCH 03/20] feat: expand tokens.ts with full palette and semantic tokens - Add all website colors to palette (includes brand colors) - Add new semantic keys: surface, surfaceRaised, surfaceOverlay, textMuted, textInverse, accent, accentSoft, info - Fix selectionText contrast (black on teal50 instead of teal75) - Rename bgRaised to surface for clarity --- src/tui/components/modal.ts | 2 +- src/tui/tokens.ts | 68 +++++++++++++++++++++++++++++++------ 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/src/tui/components/modal.ts b/src/tui/components/modal.ts index eb58368..51c2872 100644 --- a/src/tui/components/modal.ts +++ b/src/tui/components/modal.ts @@ -32,7 +32,7 @@ export function createModal( padding: 2, flexDirection: "column", gap: 1, - backgroundColor: tokens.bgRaised, + backgroundColor: tokens.surface, }, Text({ content: opts.title, attributes: TextAttributes.BOLD }), bodyText, diff --git a/src/tui/tokens.ts b/src/tui/tokens.ts index bcc4fc1..7e9d0a4 100644 --- a/src/tui/tokens.ts +++ b/src/tui/tokens.ts @@ -47,34 +47,82 @@ --color-yellow-75: #f7a836; */ export const palette = { + // Brand core black: "#101820", - teal50: "#9ad9e9", - teal75: "#2daccc", pink50: "#fc6f83", yellow: "#f8ea36", + teal50: "#9ad9e9", + teal75: "#2daccc", green: "#8dc975", - white: "#ffffff", + + // Extended + black10: "#e4e4e5", + black25: "#c9c9cb", black50: "#939497", black75: "#5e5f61", + blue: "#8599f8", + blue10: "#eef0fe", + blue100: "#081c81", + brown: "#734400", + green10: "#eff7eb", + green100: "#2e5120", + green25: "#d6eccd", + green75: "#498233", greyBlue: "#334251", + greyBlue10: "#e9eef4", + greyBlue25: "#becede", + greyBlue50: "#658bb1", + greyBlue75: "#526b83", + maroon: "#7f0315", + orange: "#f7a836", + pink10: "#ffeaed", + pink25: "#fecbd3", + purple: "#c98bdb", + purple10: "#f0def5", + purple100: "#742a89", + purple25: "#e4c4ed", + purple75: "#a63cc3", + purple50: "#c98bdb", + red: "#e40526", + skyblue: "#9ad9e9", + teal: "#144d5b", + teal10: "#e9f7fa", + teal25: "#cdecf4", + yellow10: "#fefce2", + yellow25: "#fcf7af", + yellow75: "#f7a836", } as const; export const tokens = { + // Surfaces bg: palette.black, - bgRaised: palette.black75, + surface: palette.black75, + surfaceRaised: palette.greyBlue, + surfaceOverlay: palette.black, + // Text text: palette.white, textDim: palette.black50, + textMuted: palette.black50, textAccent: palette.teal75, + textInverse: palette.black, - border: palette.black75, - borderFocus: palette.teal75, - selectionBg: palette.teal50, - selectionText: palette.teal75, + // Brand accent + accent: palette.teal75, + accentSoft: palette.teal50, - primary: palette.teal75, - danger: palette.pink50, + // Semantic success: palette.green, warning: palette.yellow, + danger: palette.pink50, + info: palette.teal50, + + // Interactive + selectionBg: palette.teal50, + selectionText: palette.black, + + // Borders + border: palette.black75, + borderFocus: palette.teal75, } as const; From 9cefbd979f19084f0ec7f25c046f2b2bbc4e822e Mon Sep 17 00:00:00 2001 From: Marlon Marcello Date: Thu, 11 Jun 2026 16:50:28 -0700 Subject: [PATCH 04/20] feat: create ThemeProvider and useTheme context - Add ThemeProvider component that sets renderer background color - Export Tokens type from tokens.ts for type-safe consumption - UseTheme hook with error boundary for misplaced usage --- src/tui/theme.tsx | 25 +++++++++++++++++++++++++ src/tui/tokens.ts | 2 ++ 2 files changed, 27 insertions(+) create mode 100644 src/tui/theme.tsx diff --git a/src/tui/theme.tsx b/src/tui/theme.tsx new file mode 100644 index 0000000..20b5660 --- /dev/null +++ b/src/tui/theme.tsx @@ -0,0 +1,25 @@ +import { createContext, useContext, onMount, type ParentProps } from "solid-js"; +import { useRenderer } from "@opentui/solid"; +import { tokens, type Tokens } from "./tokens.ts"; + +type Theme = Tokens; + +const ThemeContext = createContext(); + +export function ThemeProvider(props: ParentProps) { + const renderer = useRenderer(); + + onMount(() => { + renderer.setBackgroundColor(tokens.bg); + }); + + return {props.children}; +} + +export function useTheme(): Theme { + const value = useContext(ThemeContext); + if (!value) { + throw new Error("useTheme must be used within a ThemeProvider"); + } + return value; +} diff --git a/src/tui/tokens.ts b/src/tui/tokens.ts index 7e9d0a4..f2392de 100644 --- a/src/tui/tokens.ts +++ b/src/tui/tokens.ts @@ -126,3 +126,5 @@ export const tokens = { border: palette.black75, borderFocus: palette.teal75, } as const; + +export type Tokens = typeof tokens; From cda63da9ac43ff0527195cb36e480377bcb635f3 Mon Sep 17 00:00:00 2001 From: Marlon Marcello Date: Thu, 11 Jun 2026 16:52:02 -0700 Subject: [PATCH 05/20] feat: create keymap module and upgrade @opentui/core to v0.4.1 - Add keymap.tsx wrapping KeymapProvider and useBindings from @opentui/keymap - Upgrade @opentui/core to ^0.4.1 to resolve nested dependency type mismatch - Export createKeymap helper that creates a default OpenTUI keymap --- bun.lock | 58 ++++++++-------------------------------------- package.json | 2 +- src/tui/keymap.tsx | 11 +++++++++ 3 files changed, 22 insertions(+), 49 deletions(-) create mode 100644 src/tui/keymap.tsx diff --git a/bun.lock b/bun.lock index 57ae187..6a233f4 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "my-opentui-project", "dependencies": { - "@opentui/core": "^0.4.0", + "@opentui/core": "^0.4.1", "@opentui/keymap": "^0.4.1", "@opentui/solid": "^0.4.1", "solid-js": "^1.9.13", @@ -146,23 +146,23 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "@opentui/core": ["@opentui/core@0.4.0", "", { "dependencies": { "bun-ffi-structs": "0.2.3", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.0", "@opentui/core-darwin-x64": "0.4.0", "@opentui/core-linux-arm64": "0.4.0", "@opentui/core-linux-arm64-musl": "0.4.0", "@opentui/core-linux-x64": "0.4.0", "@opentui/core-linux-x64-musl": "0.4.0", "@opentui/core-win32-arm64": "0.4.0", "@opentui/core-win32-x64": "0.4.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-G3TmJmaPoxD6SadwevZNE30H/pMZsr/qneVaKc7bmFBxA+uHgxFSAsMoFYaEqcFJM1dGt22kMJb+YY2ZahzqVw=="], + "@opentui/core": ["@opentui/core@0.4.1", "", { "dependencies": { "bun-ffi-structs": "0.2.3", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.1", "@opentui/core-darwin-x64": "0.4.1", "@opentui/core-linux-arm64": "0.4.1", "@opentui/core-linux-arm64-musl": "0.4.1", "@opentui/core-linux-x64": "0.4.1", "@opentui/core-linux-x64-musl": "0.4.1", "@opentui/core-win32-arm64": "0.4.1", "@opentui/core-win32-x64": "0.4.1" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-ejlunoFCGLcghYGdfamI/DlWHsgCTLbuoL2JeOmFuLsN+DM5phje3CQbGR4tpl24cadCgHJQFomjoQ9Htvin+Q=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.4.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-t7VGLn2LyCGWSrhYdQ2xnq0L0sjg6VOdM6OyFxrZC3HU69m+PXqIhWMpGw/m8V6W7//uM1RX2GQrB/mgJ1Ce0Q=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.4.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ocs73hj9n0zLArOTpUWIXCWU6ERThG+3wQzO78EvfaR4hb5FRrDHGKWTzXpr6ukSKsUtKdztK5XYTPsJ5e3vww=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.4.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-/3goEuLFxn9df4fsLw9MSzbp8T3Rcs0NltCS3k73LcCb27pBmHH2FFNDKIj96m5ktK2JIWglEXcX85/i1qy4ug=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.4.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-YogRtDBfxGeOkcSHDMsGkKFBIt3cWPMPGNu2AmEN6a5KKjDYwAZCudwbDJaUbZDCJjfAUHz9iXjhJVXJBXs9vQ=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.4.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-KBv05c0e+FRUpPKRqrE5bDmmIhiJsho43EDXJuYOUYJAuGsTii8J8ws0Q4GYBIxZa/atG1Wv3rEKR7+RSd29lA=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-sBZTS1eEGeVSQ8fAmDALKQcT7FckrhK64oHfEO7W0lJ+lXapfJuOKtTM33na54V56GAM9guk4RD4cbPeTXEh4g=="], - "@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.4.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-8SL+HCpXc1Ott4MYfPzBdNM6XUhknBlqAKOmdDnTxkFSnnadJxhBJo+JYX1jxQP/lkd3jKUpEKDc51k9wud4tg=="], + "@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Eps9qB+vQ/Lel4ZYqMH87Um9oiU17Vu4oWzvRi40Yf+69vA1a3R4D7KUCeY3OxKWnRnwAHkMU9TxNnjKngPH/w=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.4.0", "", { "os": "linux", "cpu": "x64" }, "sha512-n2aZdF2vkzXyV0C0JL9Ok3EDImtWqewZVcFutMkCRK+EQk3a4oa+bl2Y07XpxGN8FZyJxS+R/HWSoDbKBNKDAg=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-9/xjYGzX5RdUl0qmGQY0OCayjJ4VffDhsBmApQdseUkMT6LGL3RumI4zPK3Y9vo1fuy6ffLnriLFOktOgutXDg=="], - "@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.4.0", "", { "os": "linux", "cpu": "x64" }, "sha512-EpBHP5S8O83VgI4YIM/CPZTUVIGJjYyk+dcYJl76phtRYF4CwZTvomQqyHb0lzXGZ7LwSDoQxsWtLs/hEOVyPA=="], + "@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-UYcp8XGX4DZXN+VYUVuCrJkbFMJ0L+VUVu0t5KqqaeJ74fI4NZ+DmwNqPPg1+C+EIzoW4QChlEUdhlZRdiEQiA=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.4.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-nUoKrHTHDMyqDGSqN7XfKvjaevgS460Pd99tzLCY4rTZcyDTdEUDfnLgEQrkURRCfUsBbwB1lNnq1aP4u+GeZQ=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.4.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-hkIbpUJcKd4iLetTygPlFS45teOBTto49aXuxNeafYQUNd3ehiSwJENNBlAGULLfq+KP3dMJoWUiOcbuPVOQRA=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.4.0", "", { "os": "win32", "cpu": "x64" }, "sha512-3uHjGfgjhw+tyxDoF6bs11Gbrt+8VP7k6vAMm9Qyvd8cExDFaNcR1E5Tf/6yR7oxGfh9E951ALue8M7h6l/lLQ=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.4.1", "", { "os": "win32", "cpu": "x64" }, "sha512-s1kGBcloy4ksl3wFCMqqOUFtXWlTTpzxe6pkLFhFhzgqKsMXHE9pwebiS3pzJkkFUxah5MYG+kbcn2Dw2Gdncg=="], "@opentui/keymap": ["@opentui/keymap@0.4.1", "", { "dependencies": { "@opentui/core": "0.4.1" }, "peerDependencies": { "@opentui/react": "0.4.1", "@opentui/solid": "0.4.1", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-4Wds3HynfTPURawkdlO8CjrzfSvxSF5RO2jlpv6etT+JTQjKSKdVrBcza6N77qWqKy0Qi0D/u2yuN6aTUJ0Z7Q=="], @@ -570,8 +570,6 @@ "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], - "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], - "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -588,10 +586,6 @@ "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], - "@opentui/keymap/@opentui/core": ["@opentui/core@0.4.1", "", { "dependencies": { "bun-ffi-structs": "0.2.3", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.1", "@opentui/core-darwin-x64": "0.4.1", "@opentui/core-linux-arm64": "0.4.1", "@opentui/core-linux-arm64-musl": "0.4.1", "@opentui/core-linux-x64": "0.4.1", "@opentui/core-linux-x64-musl": "0.4.1", "@opentui/core-win32-arm64": "0.4.1", "@opentui/core-win32-x64": "0.4.1" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-ejlunoFCGLcghYGdfamI/DlWHsgCTLbuoL2JeOmFuLsN+DM5phje3CQbGR4tpl24cadCgHJQFomjoQ9Htvin+Q=="], - - "@opentui/solid/@opentui/core": ["@opentui/core@0.4.1", "", { "dependencies": { "bun-ffi-structs": "0.2.3", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.1", "@opentui/core-darwin-x64": "0.4.1", "@opentui/core-linux-arm64": "0.4.1", "@opentui/core-linux-arm64-musl": "0.4.1", "@opentui/core-linux-x64": "0.4.1", "@opentui/core-linux-x64-musl": "0.4.1", "@opentui/core-win32-arm64": "0.4.1", "@opentui/core-win32-x64": "0.4.1" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-ejlunoFCGLcghYGdfamI/DlWHsgCTLbuoL2JeOmFuLsN+DM5phje3CQbGR4tpl24cadCgHJQFomjoQ9Htvin+Q=="], - "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], "cli-truncate/string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], @@ -618,38 +612,6 @@ "wrap-ansi/string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], - "@opentui/keymap/@opentui/core/@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.4.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ocs73hj9n0zLArOTpUWIXCWU6ERThG+3wQzO78EvfaR4hb5FRrDHGKWTzXpr6ukSKsUtKdztK5XYTPsJ5e3vww=="], - - "@opentui/keymap/@opentui/core/@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.4.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-YogRtDBfxGeOkcSHDMsGkKFBIt3cWPMPGNu2AmEN6a5KKjDYwAZCudwbDJaUbZDCJjfAUHz9iXjhJVXJBXs9vQ=="], - - "@opentui/keymap/@opentui/core/@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-sBZTS1eEGeVSQ8fAmDALKQcT7FckrhK64oHfEO7W0lJ+lXapfJuOKtTM33na54V56GAM9guk4RD4cbPeTXEh4g=="], - - "@opentui/keymap/@opentui/core/@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Eps9qB+vQ/Lel4ZYqMH87Um9oiU17Vu4oWzvRi40Yf+69vA1a3R4D7KUCeY3OxKWnRnwAHkMU9TxNnjKngPH/w=="], - - "@opentui/keymap/@opentui/core/@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-9/xjYGzX5RdUl0qmGQY0OCayjJ4VffDhsBmApQdseUkMT6LGL3RumI4zPK3Y9vo1fuy6ffLnriLFOktOgutXDg=="], - - "@opentui/keymap/@opentui/core/@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-UYcp8XGX4DZXN+VYUVuCrJkbFMJ0L+VUVu0t5KqqaeJ74fI4NZ+DmwNqPPg1+C+EIzoW4QChlEUdhlZRdiEQiA=="], - - "@opentui/keymap/@opentui/core/@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.4.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-hkIbpUJcKd4iLetTygPlFS45teOBTto49aXuxNeafYQUNd3ehiSwJENNBlAGULLfq+KP3dMJoWUiOcbuPVOQRA=="], - - "@opentui/keymap/@opentui/core/@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.4.1", "", { "os": "win32", "cpu": "x64" }, "sha512-s1kGBcloy4ksl3wFCMqqOUFtXWlTTpzxe6pkLFhFhzgqKsMXHE9pwebiS3pzJkkFUxah5MYG+kbcn2Dw2Gdncg=="], - - "@opentui/solid/@opentui/core/@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.4.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ocs73hj9n0zLArOTpUWIXCWU6ERThG+3wQzO78EvfaR4hb5FRrDHGKWTzXpr6ukSKsUtKdztK5XYTPsJ5e3vww=="], - - "@opentui/solid/@opentui/core/@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.4.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-YogRtDBfxGeOkcSHDMsGkKFBIt3cWPMPGNu2AmEN6a5KKjDYwAZCudwbDJaUbZDCJjfAUHz9iXjhJVXJBXs9vQ=="], - - "@opentui/solid/@opentui/core/@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-sBZTS1eEGeVSQ8fAmDALKQcT7FckrhK64oHfEO7W0lJ+lXapfJuOKtTM33na54V56GAM9guk4RD4cbPeTXEh4g=="], - - "@opentui/solid/@opentui/core/@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Eps9qB+vQ/Lel4ZYqMH87Um9oiU17Vu4oWzvRi40Yf+69vA1a3R4D7KUCeY3OxKWnRnwAHkMU9TxNnjKngPH/w=="], - - "@opentui/solid/@opentui/core/@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-9/xjYGzX5RdUl0qmGQY0OCayjJ4VffDhsBmApQdseUkMT6LGL3RumI4zPK3Y9vo1fuy6ffLnriLFOktOgutXDg=="], - - "@opentui/solid/@opentui/core/@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-UYcp8XGX4DZXN+VYUVuCrJkbFMJ0L+VUVu0t5KqqaeJ74fI4NZ+DmwNqPPg1+C+EIzoW4QChlEUdhlZRdiEQiA=="], - - "@opentui/solid/@opentui/core/@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.4.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-hkIbpUJcKd4iLetTygPlFS45teOBTto49aXuxNeafYQUNd3ehiSwJENNBlAGULLfq+KP3dMJoWUiOcbuPVOQRA=="], - - "@opentui/solid/@opentui/core/@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.4.1", "", { "os": "win32", "cpu": "x64" }, "sha512-s1kGBcloy4ksl3wFCMqqOUFtXWlTTpzxe6pkLFhFhzgqKsMXHE9pwebiS3pzJkkFUxah5MYG+kbcn2Dw2Gdncg=="], - "enquirer/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], diff --git a/package.json b/package.json index 01be441..58b13d6 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "changeset:version": "changeset version" }, "dependencies": { - "@opentui/core": "^0.4.0", + "@opentui/core": "^0.4.1", "@opentui/keymap": "^0.4.1", "@opentui/solid": "^0.4.1", "solid-js": "^1.9.13", diff --git a/src/tui/keymap.tsx b/src/tui/keymap.tsx new file mode 100644 index 0000000..edba9dd --- /dev/null +++ b/src/tui/keymap.tsx @@ -0,0 +1,11 @@ +import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"; +import { KeymapProvider, useBindings, useKeymapSelector } from "@opentui/keymap/solid"; + +type KeymapRenderer = Parameters[0]; + +export function createKeymap(renderer: KeymapRenderer) { + return createDefaultOpenTuiKeymap(renderer); +} + +export { KeymapProvider, useBindings, useKeymapSelector }; +export type { KeymapRenderer }; From b8594d50394da5644af640c6399c19842df0970b Mon Sep 17 00:00:00 2001 From: Marlon Marcello Date: Thu, 11 Jun 2026 16:52:44 -0700 Subject: [PATCH 06/20] feat: create DialogProvider with stack-based overlay dialogs - Add DialogProvider component wrapping children with Portal overlay - useDialog hook exposing show/replace/clear for dialog management - Escape key closes current dialog via useBindings - Responsive overlay with RGBA dimming and centered panel --- src/tui/components/dialog.tsx | 110 ++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 src/tui/components/dialog.tsx diff --git a/src/tui/components/dialog.tsx b/src/tui/components/dialog.tsx new file mode 100644 index 0000000..a01d26c --- /dev/null +++ b/src/tui/components/dialog.tsx @@ -0,0 +1,110 @@ +import { createContext, useContext, type ParentProps, type JSX, Show } from "solid-js"; +import { createStore } from "solid-js/store"; +import { Portal, useRenderer, useTerminalDimensions } from "@opentui/solid"; +import { RGBA } from "@opentui/core"; +import { useTheme } from "../theme.tsx"; +import { useBindings } from "../keymap.tsx"; + +interface DialogItem { + element: JSX.Element; + onClose?: () => void; +} + +interface DialogContextValue { + show(element: JSX.Element, onClose?: () => void): void; + replace(element: JSX.Element, onClose?: () => void): void; + clear(): void; +} + +const DialogContext = createContext(); + +function DialogOverlay(props: ParentProps<{ onClose: () => void }>) { + const dimensions = useTerminalDimensions(); + const theme = useTheme(); + + return ( + + + {props.children} + + + ); +} + +export function DialogProvider(props: ParentProps) { + const [store, setStore] = createStore({ + stack: [] as DialogItem[], + }); + + const renderer = useRenderer(); + + useBindings(() => ({ + enabled: store.stack.length > 0, + bindings: [ + { + key: "escape", + desc: "Close dialog", + group: "Dialog", + cmd: () => { + const current = store.stack.at(-1); + current?.onClose?.(); + setStore("stack", store.stack.slice(0, -1)); + }, + }, + ], + })); + + const value: DialogContextValue = { + show(element: JSX.Element, onClose?: () => void) { + setStore("stack", [...store.stack, { element, onClose }]); + }, + replace(element: JSX.Element, onClose?: () => void) { + for (const item of store.stack) { + item.onClose?.(); + } + setStore("stack", [{ element, onClose }]); + }, + clear() { + for (const item of store.stack) { + item.onClose?.(); + } + setStore("stack", []); + }, + }; + + return ( + + {props.children} + + 0}> + value.clear()}>{store.stack.at(-1)?.element} + + + + ); +} + +export function useDialog(): DialogContextValue { + const value = useContext(DialogContext); + if (!value) { + throw new Error("useDialog must be used within a DialogProvider"); + } + return value; +} From f914f76f47ffeae6496a3f0a8f0e1e72ba8f2129 Mon Sep 17 00:00:00 2001 From: Marlon Marcello Date: Thu, 11 Jun 2026 16:53:16 -0700 Subject: [PATCH 07/20] feat: create UpdateDialog component - Solid component replacing imperative modal for update notifications - Uses useBindings for keyboard handling (enter/escape to close) - Brand-styled: pink title, teal version highlight, OK button - Shows install command on a dim background - Mouse click support on the close indicator and OK button --- src/tui/components/update-dialog.tsx | 59 ++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/tui/components/update-dialog.tsx diff --git a/src/tui/components/update-dialog.tsx b/src/tui/components/update-dialog.tsx new file mode 100644 index 0000000..663c433 --- /dev/null +++ b/src/tui/components/update-dialog.tsx @@ -0,0 +1,59 @@ +import { TextAttributes } from "@opentui/core"; +import { useTheme } from "../theme.tsx"; +import { useDialog } from "./dialog.tsx"; +import { useBindings } from "../keymap.tsx"; + +export interface UpdateDialogProps { + currentVersion: string; + latestVersion: string; + repo: string; +} + +export function UpdateDialog(props: UpdateDialogProps) { + const dialog = useDialog(); + const theme = useTheme(); + + useBindings(() => ({ + bindings: [ + { + key: "return", + desc: "Close", + group: "Dialog", + cmd: () => dialog.clear(), + }, + ], + })); + + const installCmd = `curl -fsSL https://raw.githubusercontent.com/${props.repo}/main/install.sh | bash`; + + return ( + + + + Update Available + + dialog.clear()}> + esc + + + + v{props.currentVersion} + + v{props.latestVersion} + + + {installCmd} + + + dialog.clear()} + > + ok + + + + ); +} From 450018e612d02355a400c85c2a6b7835fb6c1971 Mon Sep 17 00:00:00 2001 From: Marlon Marcello Date: Thu, 11 Jun 2026 16:57:44 -0700 Subject: [PATCH 08/20] feat: convert TUI to Solid JSX with dialog, status bar, and command palette - Rewrite app root as Solid component tree with ThemeProvider, DialogProvider, KeymapProvider - Convert dashboard to Solid JSX (ascii_font, box, text, select) - Add StatusBar component showing active key bindings at bottom - Add CommandPalette component for ctrl+p overlay navigation - Add UpdateDialog replacing old imperative modal - Remove old functional dashboard.ts, modal.ts, and rendering tests - app.ts now delegates to app.tsx launchSolidApp --- src/tui/app.ts | 37 +--------- src/tui/app.tsx | 74 ++++++++++++++++++++ src/tui/components/command-palette.tsx | 95 ++++++++++++++++++++++++++ src/tui/components/modal.ts | 82 ---------------------- src/tui/components/status-bar.tsx | 28 ++++++++ src/tui/pages/dashboard.ts | 41 ----------- src/tui/pages/dashboard.tsx | 35 ++++++++++ tests/tui/dashboard.test.ts | 79 --------------------- 8 files changed, 234 insertions(+), 237 deletions(-) create mode 100644 src/tui/app.tsx create mode 100644 src/tui/components/command-palette.tsx delete mode 100644 src/tui/components/modal.ts create mode 100644 src/tui/components/status-bar.tsx delete mode 100644 src/tui/pages/dashboard.ts create mode 100644 src/tui/pages/dashboard.tsx delete mode 100644 tests/tui/dashboard.test.ts diff --git a/src/tui/app.ts b/src/tui/app.ts index b67abbf..bdcd72f 100644 --- a/src/tui/app.ts +++ b/src/tui/app.ts @@ -1,39 +1,6 @@ -import { Box, createCliRenderer, isRenderable, t } from "@opentui/core"; -import { checkForUpdate } from "../utils/update-check.ts"; import { APP_VERSION } from "../version.ts"; -import { createDashboard } from "./pages/dashboard.ts"; -import { createModal } from "./components/modal.ts"; -import { tokens } from "./tokens.ts"; - -const REPO = "wethegit/wtc"; +import { launchSolidApp } from "./app.tsx"; export async function launchDashboard(version = APP_VERSION): Promise { - const renderer = await createCliRenderer({ exitOnCtrlC: true, backgroundColor: tokens.bg }); - - renderer.root.add(Box({ flexDirection: "column", flexGrow: 1 }, createDashboard(version))); - - const nav = renderer.root.findDescendantById("dashboard-nav"); - if (nav && isRenderable(nav)) { - nav.focus(); - } - - const updateModal = createModal(renderer, { - id: "update-modal", - title: "Update Available", - }); - - checkForUpdate(version).then((info) => { - if (info.updateAvailable) { - updateModal.setBody( - t` v${version} \u2192 ${info.latestVersion} - curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | bash -`, - ); - updateModal.show(); - } - }); - - await new Promise((resolve) => { - renderer.on("destroy", () => resolve()); - }); + await launchSolidApp(version); } diff --git a/src/tui/app.tsx b/src/tui/app.tsx new file mode 100644 index 0000000..e0c56c3 --- /dev/null +++ b/src/tui/app.tsx @@ -0,0 +1,74 @@ +import { onMount } from "solid-js"; +import { render, useRenderer } from "@opentui/solid"; +import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"; +import { KeymapProvider, useBindings } from "@opentui/keymap/solid"; +import { checkForUpdate } from "../utils/update-check.ts"; +import { APP_VERSION } from "../version.ts"; +import { ThemeProvider } from "./theme.tsx"; +import { DialogProvider, useDialog } from "./components/dialog.tsx"; +import { UpdateDialog } from "./components/update-dialog.tsx"; +import { Dashboard } from "./pages/dashboard.tsx"; +import { StatusBar } from "./components/status-bar.tsx"; +import { tokens } from "./tokens.ts"; + +const REPO = "wethegit/wtc"; + +function AppContent(props: { version: string }) { + const dialog = useDialog(); + const renderer = useRenderer(); + + useBindings(() => ({ + bindings: [ + { + key: "q", + cmd: "quit", + run: () => renderer.destroy(), + }, + ], + })); + + onMount(() => { + checkForUpdate(props.version).then((info) => { + if (info.updateAvailable) { + dialog.replace(() => ( + + )); + } + }); + }); + + return ( + + + + + ); +} + +function AppShell(props: { version: string }) { + const renderer = useRenderer(); + const keymap = createDefaultOpenTuiKeymap(renderer); + + return ( + + + + ); +} + +export async function launchSolidApp(version = APP_VERSION): Promise { + await render( + () => ( + + + + + + ), + { exitOnCtrlC: true, backgroundColor: tokens.bg }, + ); +} diff --git a/src/tui/components/command-palette.tsx b/src/tui/components/command-palette.tsx new file mode 100644 index 0000000..cd611f4 --- /dev/null +++ b/src/tui/components/command-palette.tsx @@ -0,0 +1,95 @@ +import { createSignal } from "solid-js"; +import { TextAttributes } from "@opentui/core"; +import { useTheme } from "../theme.tsx"; +import { useDialog } from "./dialog.tsx"; +import { useBindings } from "../keymap.tsx"; + +export interface CommandEntry { + id: string; + title: string; + description?: string; + onSelect: () => void; +} + +export function showCommandPalette(entries: () => CommandEntry[]) { + const dialog = useDialog(); + + dialog.replace(() => dialog.clear()} />); +} + +function CommandPalette(props: { entries: CommandEntry[]; onClose: () => void }) { + const theme = useTheme(); + const [query, setQuery] = createSignal(""); + const [selectedIndex, setSelectedIndex] = createSignal(0); + + const filtered = () => { + const q = query().toLowerCase(); + if (!q) return props.entries; + return props.entries.filter( + (e) => e.title.toLowerCase().includes(q) || e.description?.toLowerCase().includes(q), + ); + }; + + useBindings(() => ({ + bindings: [ + { + key: "escape", + desc: "Close palette", + group: "CommandPalette", + cmd: () => props.onClose(), + }, + { + key: "up", + cmd: "palette.up", + run: () => setSelectedIndex((i) => Math.max(0, i - 1)), + }, + { + key: "down", + cmd: "palette.down", + run: () => setSelectedIndex((i) => Math.min(filtered().length - 1, i + 1)), + }, + { + key: "return", + desc: "Select command", + group: "CommandPalette", + cmd: () => { + const entry = filtered()[selectedIndex()]; + if (entry) { + entry.onSelect(); + } + }, + }, + ], + })); + + return ( + + + Command Palette + + { + setQuery(val); + setSelectedIndex(0); + }} + placeholder="Type to filter..." + /> + + {filtered().map((entry, i) => ( + { + entry.onSelect(); + }} + > + {entry.title} + {entry.description && — {entry.description}} + + ))} + {filtered().length === 0 && No matching commands} + + ↑↓ navigate · enter select · esc close + + ); +} diff --git a/src/tui/components/modal.ts b/src/tui/components/modal.ts deleted file mode 100644 index 51c2872..0000000 --- a/src/tui/components/modal.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { - BoxRenderable, - TextRenderable, - Box, - Text, - TextAttributes, - type CliRenderer, - type KeyEvent, - type StyledText, -} from "@opentui/core"; -import { tokens } from "../tokens.ts"; - -export interface ModalController { - show(): void; - hide(): void; - setBody(content: string | StyledText): void; - destroy(): void; -} - -export function createModal( - renderer: CliRenderer, - opts: { id: string; title: string }, -): ModalController { - const bodyText = new TextRenderable(renderer, { - id: `${opts.id}-body`, - content: "", - }); - - const content = Box( - { - id: `${opts.id}-content`, - padding: 2, - flexDirection: "column", - gap: 1, - backgroundColor: tokens.surface, - }, - Text({ content: opts.title, attributes: TextAttributes.BOLD }), - bodyText, - Text({ content: "Press ESC to close", attributes: TextAttributes.DIM }), - ); - - const root = new BoxRenderable(renderer, { - id: opts.id, - position: "absolute", - top: 0, - left: 0, - width: "100%", - height: "100%", - flexDirection: "column", - justifyContent: "center", - alignItems: "center", - zIndex: 1000, - visible: false, - }); - - root.add(content); - renderer.root.add(root); - - renderer.keyInput.on("keypress", (key: KeyEvent) => { - if (root.visible && key.name === "escape") { - root.visible = false; - } - }); - - function show() { - root.visible = true; - } - - function hide() { - root.visible = false; - } - - function setBody(content: string | StyledText) { - bodyText.content = content; - } - - function destroy() { - root.destroyRecursively(); - } - - return { show, hide, setBody, destroy }; -} diff --git a/src/tui/components/status-bar.tsx b/src/tui/components/status-bar.tsx new file mode 100644 index 0000000..a99057f --- /dev/null +++ b/src/tui/components/status-bar.tsx @@ -0,0 +1,28 @@ +import { useTheme } from "../theme.tsx"; +import { useKeymapSelector } from "../keymap.tsx"; + +export function StatusBar() { + const theme = useTheme(); + const activeKeys = useKeymapSelector((km) => km.getActiveKeys({ includeMetadata: true })); + + const hint = () => { + const keys = activeKeys(); + if (keys.length > 0) { + return keys.map((k) => k.display).join(" · "); + } + return "↑↓ navigate · enter select · esc back · q quit"; + }; + + return ( + + {hint()} + + ); +} diff --git a/src/tui/pages/dashboard.ts b/src/tui/pages/dashboard.ts deleted file mode 100644 index 23eb3ba..0000000 --- a/src/tui/pages/dashboard.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { ASCIIFont, Box, Select, Text, TextAttributes } from "@opentui/core"; -import { tokens } from "../tokens.ts"; - -const navItems = [ - { name: "GitHub (coming soon)", description: "Repository workflows" }, - { name: "Amplify (coming soon)", description: "Hosting setup" }, - { name: "Teamwork (coming soon)", description: "Tasks and timers" }, - { name: "Settings (coming soon)", description: "Configuration" }, -]; - -export function createDashboard(version = "0.1.0") { - return Box( - { - flexDirection: "column", - alignItems: "center", - justifyContent: "center", - flexGrow: 1, - gap: 1, - }, - Box( - { flexDirection: "column", justifyContent: "center", alignItems: "center", gap: 1 }, - ASCIIFont({ font: "tiny", text: "WTC" }), - Text({ content: "What will you build?", attributes: TextAttributes.DIM }), - ), - Select({ - id: "dashboard-nav", - width: 34, - height: 4, - options: navItems, - selectedIndex: 0, - showDescription: false, - wrapSelection: true, - selectedTextColor: tokens.selectionText, - selectedBackgroundColor: tokens.selectionBg, - }), - Text({ - content: `v${version} | Press Ctrl+C to exit`, - attributes: TextAttributes.DIM, - }), - ); -} diff --git a/src/tui/pages/dashboard.tsx b/src/tui/pages/dashboard.tsx new file mode 100644 index 0000000..4ac349b --- /dev/null +++ b/src/tui/pages/dashboard.tsx @@ -0,0 +1,35 @@ +import { TextAttributes } from "@opentui/core"; +import { useTheme } from "../theme.tsx"; + +const navItems = [ + { name: "GitHub (coming soon)", description: "Repository workflows" }, + { name: "Amplify (coming soon)", description: "Hosting setup" }, + { name: "Teamwork (coming soon)", description: "Tasks and timers" }, + { name: "Settings (coming soon)", description: "Configuration" }, +]; + +export function Dashboard(props: { version?: string }) { + const theme = useTheme(); + const version = props.version ?? "0.1.0"; + + return ( + + + + What will you build? + + void }) {filtered().map((entry, i) => ( { entry.onSelect(); }} > - {entry.title} - {entry.description && — {entry.description}} + + {entry.title} + + {entry.description && — {entry.description}} ))} - {filtered().length === 0 && No matching commands} + {filtered().length === 0 && No matching commands} - ↑↓ navigate · enter select · esc close + ↑↓ navigate · enter select · esc close ); } diff --git a/src/tui/components/dialog.tsx b/src/tui/components/dialog.tsx index a01d26c..f852d1a 100644 --- a/src/tui/components/dialog.tsx +++ b/src/tui/components/dialog.tsx @@ -2,8 +2,8 @@ import { createContext, useContext, type ParentProps, type JSX, Show } from "sol import { createStore } from "solid-js/store"; import { Portal, useRenderer, useTerminalDimensions } from "@opentui/solid"; import { RGBA } from "@opentui/core"; -import { useTheme } from "../theme.tsx"; import { useBindings } from "../keymap.tsx"; +import { tokens } from "../tokens.ts"; interface DialogItem { element: JSX.Element; @@ -20,7 +20,6 @@ const DialogContext = createContext(); function DialogOverlay(props: ParentProps<{ onClose: () => void }>) { const dimensions = useTerminalDimensions(); - const theme = useTheme(); return ( void }>) { km.getActiveKeys({ includeMetadata: true })); const hint = () => { @@ -20,9 +19,9 @@ export function StatusBar() { left={0} width="100%" height={1} - backgroundColor={theme.surface} + backgroundColor={tokens.surface} > - {hint()} + {hint()} ); } diff --git a/src/tui/components/update-dialog.tsx b/src/tui/components/update-dialog.tsx index 663c433..d21aa49 100644 --- a/src/tui/components/update-dialog.tsx +++ b/src/tui/components/update-dialog.tsx @@ -1,5 +1,5 @@ import { TextAttributes } from "@opentui/core"; -import { useTheme } from "../theme.tsx"; +import { tokens } from "../tokens.ts"; import { useDialog } from "./dialog.tsx"; import { useBindings } from "../keymap.tsx"; @@ -11,7 +11,6 @@ export interface UpdateDialogProps { export function UpdateDialog(props: UpdateDialogProps) { const dialog = useDialog(); - const theme = useTheme(); useBindings(() => ({ bindings: [ @@ -29,29 +28,29 @@ export function UpdateDialog(props: UpdateDialogProps) { return ( - + Update Available - dialog.clear()}> + dialog.clear()}> esc - v{props.currentVersion} - - v{props.latestVersion} + v{props.currentVersion} + + v{props.latestVersion} - {installCmd} + {installCmd} dialog.clear()} > - ok + ok diff --git a/src/tui/pages/dashboard.tsx b/src/tui/pages/dashboard.tsx index 4ac349b..7748267 100644 --- a/src/tui/pages/dashboard.tsx +++ b/src/tui/pages/dashboard.tsx @@ -1,5 +1,5 @@ import { TextAttributes } from "@opentui/core"; -import { useTheme } from "../theme.tsx"; +import { tokens } from "../tokens.ts"; const navItems = [ { name: "GitHub (coming soon)", description: "Repository workflows" }, @@ -9,7 +9,6 @@ const navItems = [ ]; export function Dashboard(props: { version?: string }) { - const theme = useTheme(); const version = props.version ?? "0.1.0"; return ( @@ -26,8 +25,8 @@ export function Dashboard(props: { version?: string }) { selectedIndex={0} showDescription={false} wrapSelection={true} - selectedTextColor={theme.selectionText} - selectedBackgroundColor={theme.selectionBg} + selectedTextColor={tokens.selectionText} + selectedBackgroundColor={tokens.selectionBg} /> v{version} · Press Ctrl+C to exit diff --git a/src/tui/theme.tsx b/src/tui/theme.tsx deleted file mode 100644 index 20b5660..0000000 --- a/src/tui/theme.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { createContext, useContext, onMount, type ParentProps } from "solid-js"; -import { useRenderer } from "@opentui/solid"; -import { tokens, type Tokens } from "./tokens.ts"; - -type Theme = Tokens; - -const ThemeContext = createContext(); - -export function ThemeProvider(props: ParentProps) { - const renderer = useRenderer(); - - onMount(() => { - renderer.setBackgroundColor(tokens.bg); - }); - - return {props.children}; -} - -export function useTheme(): Theme { - const value = useContext(ThemeContext); - if (!value) { - throw new Error("useTheme must be used within a ThemeProvider"); - } - return value; -} From 037b330005af9c5b92b589e1fe8539f4078aabc9 Mon Sep 17 00:00:00 2001 From: Marlon Marcello Date: Thu, 11 Jun 2026 17:17:15 -0700 Subject: [PATCH 11/20] refactor: remove unnecessary keymap wrapper --- plans/PLAN.md | 2 +- plans/SOLID_TUI_REFACTOR.md | 26 ++++++++++---------------- src/tui/components/command-palette.tsx | 2 +- src/tui/components/dialog.tsx | 2 +- src/tui/components/status-bar.tsx | 2 +- src/tui/components/update-dialog.tsx | 2 +- src/tui/keymap.tsx | 11 ----------- 7 files changed, 15 insertions(+), 32 deletions(-) delete mode 100644 src/tui/keymap.tsx diff --git a/plans/PLAN.md b/plans/PLAN.md index 2882e4d..5c4660a 100644 --- a/plans/PLAN.md +++ b/plans/PLAN.md @@ -183,7 +183,7 @@ See `SOLID_TUI_REFACTOR.md` for the detailed implementation plan, UX direction, - Rewrite `src/tui/app.ts` → Solid root component with `` + `` - Rewrite `src/tui/components/modal.ts` → `DialogProvider` + `UpdateDialog` (OpenCode-inspired dialog pattern) - Rewrite `src/tui/pages/dashboard.ts` → Solid JSX with reactive state -- Create keymap module (`useBindings`-style) for keyboard handling +- Use `@opentui/keymap` directly for `KeymapProvider`, `useBindings`, and `useKeymapSelector` - Import `tokens.ts` directly from components; do not add a theme provider unless runtime theming becomes necessary - Add bottom status bar (mandatory — shows active hotkeys per context) - Add command palette (mandatory — `ctrl+p` overlay for quick navigation) diff --git a/plans/SOLID_TUI_REFACTOR.md b/plans/SOLID_TUI_REFACTOR.md index 1c3cf92..1e94792 100644 --- a/plans/SOLID_TUI_REFACTOR.md +++ b/plans/SOLID_TUI_REFACTOR.md @@ -142,7 +142,6 @@ No ASCII art logos unless they are genuinely brand-relevant. The WTC tiny-font l ``` src/tui/ ├── app.tsx # Solid root — renderer creation, keymap init, providers -├── keymap.tsx # KeymapProvider, re-export useBindings/useKeymapSelector ├── tokens.ts # Palette + semantic tokens (expand existing) ├── components/ │ ├── dialog.tsx # DialogProvider + useDialog + base Dialog overlay @@ -201,23 +200,18 @@ import { tokens } from "../tokens.ts"; Do not add a `ThemeProvider` or `useTheme` hook while the app has one fixed brand theme. Add a provider only if runtime theming becomes necessary. -### Step 4 — Create Keymap Module +### Step 4 — Use OpenTUI Keymap Directly -File: `src/tui/keymap.tsx` +Use `@opentui/keymap` imports directly until the app needs custom keymap behavior. ```ts import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"; import { KeymapProvider, useBindings, useKeymapSelector } from "@opentui/keymap/solid"; -import type { CliRenderer } from "@opentui/core"; - -export function createKeymap(renderer: CliRenderer) { - return createDefaultOpenTuiKeymap(renderer); -} - -export { KeymapProvider, useBindings, useKeymapSelector }; ``` -Global bindings (quit, status bar update) are registered inside `app.tsx` via `useBindings`. +Do not add a local `keymap.tsx` wrapper while it only re-exports library functions. Add one later if we introduce app-specific behavior such as user-configurable keybinds, command formatting, mode stacks, leader keys, or shared command registry helpers. + +Global bindings (quit, status bar update, command palette) are registered inside Solid components via `useBindings`. ### Step 5 — Create Dialog Provider @@ -318,7 +312,7 @@ File: `src/tui/components/status-bar.tsx` A bottom-pinned bar that shows active keybinding hints for the current context. This is the **mandatory** UX pattern from OpenCode. ```tsx -import { useKeymapSelector } from "../keymap"; +import { useKeymapSelector } from "@opentui/keymap/solid"; import { tokens } from "../tokens"; export function StatusBar() { @@ -386,13 +380,13 @@ File: `src/tui/app.tsx` ```tsx import { render } from "@opentui/solid"; -import { createKeymap, KeymapProvider } from "./keymap"; +import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"; +import { KeymapProvider, useBindings } from "@opentui/keymap/solid"; import { DialogProvider } from "./components/dialog"; import { StatusBar } from "./components/status-bar"; import { Dashboard } from "./pages/dashboard"; import { UpdateDialog } from "./components/update-dialog"; import { checkForUpdate } from "../utils/update-check"; -import { useBindings } from "./keymap"; import { onMount } from "solid-js"; import { APP_VERSION } from "../version"; @@ -400,7 +394,7 @@ const REPO = "wethegit/wtc"; export async function launchDashboard(version = APP_VERSION): Promise { const renderer = await createCliRenderer({ exitOnCtrlC: true, backgroundColor: tokens.bg }); - const keymap = createKeymap(renderer); + const keymap = createDefaultOpenTuiKeymap(renderer); function Root() { const dialog = useDialog(); @@ -464,7 +458,7 @@ Per the testing philosophy in `AGENTS.md`: ## Future-Proofing Notes -- The keymap module location (`src/tui/keymap.tsx`) can later grow command registration helpers when Phase 3+ adds real features. +- Add a local keymap module only when Phase 3+ needs app-specific command registration, key formatting, mode stacks, leader keys, or configurable user bindings. - The status bar is intentionally simple — it can become richer (git branch, AWS profile, timer status) in Phase 5-6. - The command palette is a shell — its command list is populated as real commands are built. - DialogProvider supports a stack but we only use single-dialog mode for now. Stack semantics are ready for multi-dialog flows like wizards. diff --git a/src/tui/components/command-palette.tsx b/src/tui/components/command-palette.tsx index 38f146f..2f35617 100644 --- a/src/tui/components/command-palette.tsx +++ b/src/tui/components/command-palette.tsx @@ -1,8 +1,8 @@ import { createSignal } from "solid-js"; import { TextAttributes } from "@opentui/core"; +import { useBindings } from "@opentui/keymap/solid"; import { tokens } from "../tokens.ts"; import { useDialog } from "./dialog.tsx"; -import { useBindings } from "../keymap.tsx"; export interface CommandEntry { id: string; diff --git a/src/tui/components/dialog.tsx b/src/tui/components/dialog.tsx index f852d1a..333bb6b 100644 --- a/src/tui/components/dialog.tsx +++ b/src/tui/components/dialog.tsx @@ -2,7 +2,7 @@ import { createContext, useContext, type ParentProps, type JSX, Show } from "sol import { createStore } from "solid-js/store"; import { Portal, useRenderer, useTerminalDimensions } from "@opentui/solid"; import { RGBA } from "@opentui/core"; -import { useBindings } from "../keymap.tsx"; +import { useBindings } from "@opentui/keymap/solid"; import { tokens } from "../tokens.ts"; interface DialogItem { diff --git a/src/tui/components/status-bar.tsx b/src/tui/components/status-bar.tsx index 963badd..77cb96f 100644 --- a/src/tui/components/status-bar.tsx +++ b/src/tui/components/status-bar.tsx @@ -1,4 +1,4 @@ -import { useKeymapSelector } from "../keymap.tsx"; +import { useKeymapSelector } from "@opentui/keymap/solid"; import { tokens } from "../tokens.ts"; export function StatusBar() { diff --git a/src/tui/components/update-dialog.tsx b/src/tui/components/update-dialog.tsx index d21aa49..df1d1b2 100644 --- a/src/tui/components/update-dialog.tsx +++ b/src/tui/components/update-dialog.tsx @@ -1,7 +1,7 @@ import { TextAttributes } from "@opentui/core"; +import { useBindings } from "@opentui/keymap/solid"; import { tokens } from "../tokens.ts"; import { useDialog } from "./dialog.tsx"; -import { useBindings } from "../keymap.tsx"; export interface UpdateDialogProps { currentVersion: string; diff --git a/src/tui/keymap.tsx b/src/tui/keymap.tsx deleted file mode 100644 index edba9dd..0000000 --- a/src/tui/keymap.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"; -import { KeymapProvider, useBindings, useKeymapSelector } from "@opentui/keymap/solid"; - -type KeymapRenderer = Parameters[0]; - -export function createKeymap(renderer: KeymapRenderer) { - return createDefaultOpenTuiKeymap(renderer); -} - -export { KeymapProvider, useBindings, useKeymapSelector }; -export type { KeymapRenderer }; From e75b3a28b8208e35754195f931912a1c11fef1b7 Mon Sep 17 00:00:00 2001 From: Marlon Marcello Date: Thu, 11 Jun 2026 17:31:04 -0700 Subject: [PATCH 12/20] feature: adds 'routing' --- plans/PLAN.md | 5 +- plans/SOLID_TUI_REFACTOR.md | 69 ++++++++++++-------------- src/tui/app.tsx | 48 +++++++++++++++++- src/tui/components/command-palette.tsx | 36 +++++++------- src/tui/components/status-bar.tsx | 13 +---- src/tui/pages/dashboard.tsx | 21 +------- src/tui/pages/github.tsx | 14 ++++++ src/tui/pages/settings.tsx | 14 ++++++ tests/tui/command-palette.test.ts | 33 ++++++++++++ 9 files changed, 162 insertions(+), 91 deletions(-) create mode 100644 src/tui/pages/github.tsx create mode 100644 src/tui/pages/settings.tsx create mode 100644 tests/tui/command-palette.test.ts diff --git a/plans/PLAN.md b/plans/PLAN.md index 5c4660a..9177bee 100644 --- a/plans/PLAN.md +++ b/plans/PLAN.md @@ -182,11 +182,12 @@ See `SOLID_TUI_REFACTOR.md` for the detailed implementation plan, UX direction, - Expand `tokens.ts` into full palette + semantic tokens - Rewrite `src/tui/app.ts` → Solid root component with `` + `` - Rewrite `src/tui/components/modal.ts` → `DialogProvider` + `UpdateDialog` (OpenCode-inspired dialog pattern) -- Rewrite `src/tui/pages/dashboard.ts` → Solid JSX with reactive state +- Rewrite `src/tui/pages/dashboard.ts` → Solid JSX intro screen without dashboard navigation select - Use `@opentui/keymap` directly for `KeymapProvider`, `useBindings`, and `useKeymapSelector` - Import `tokens.ts` directly from components; do not add a theme provider unless runtime theming becomes necessary - Add bottom status bar (mandatory — shows active hotkeys per context) -- Add command palette (mandatory — `ctrl+p` overlay for quick navigation) +- Add command palette (mandatory — `ctrl/cmd+p` overlay for quick navigation) +- Add initial routes for GitHub and Settings, navigable through the command palette - Remove all `findDescendantById` patterns - Update test setup to cover logic only, not TUI rendering diff --git a/plans/SOLID_TUI_REFACTOR.md b/plans/SOLID_TUI_REFACTOR.md index 1e94792..edbd9bd 100644 --- a/plans/SOLID_TUI_REFACTOR.md +++ b/plans/SOLID_TUI_REFACTOR.md @@ -25,7 +25,7 @@ Moving to Solid.js brings: 1. **Bottom status bar** (mandatory — OpenCode-inspired) - Always visible at terminal bottom - - Shows available hotkeys for the current context (e.g., `↑↓ navigate · enter select · esc back · q quit`) + - Shows available hotkeys for the current context (initially `ctrl/cmd+p commands · q quit`) - Updates reactively as focus or key layers change 2. **Command palette** (mandatory — VSCode/Slack/OpenCode-style) - Triggered by a key chord (e.g., `ctrl+p` or `cmd+p`) @@ -148,9 +148,11 @@ src/tui/ │ ├── dialog-alert.tsx # Alert dialog (simplified from OpenCode pattern) │ ├── update-dialog.tsx# Update available notification dialog │ ├── status-bar.tsx # Bottom bar showing active hotkeys -│ └── command-palette.tsx # ctrl+p overlay command list +│ └── command-palette.tsx # ctrl/cmd+p overlay command list └── pages/ - └── dashboard.tsx # Solid JSX version of the dashboard + ├── dashboard.tsx # Intro screen with WTC logo + ├── github.tsx # Placeholder GitHub page + └── settings.tsx # Placeholder Settings page ``` --- @@ -275,10 +277,10 @@ Behavior: File: `src/tui/pages/dashboard.tsx` -Replace `createDashboard()` with: +Replace `createDashboard()` with an intro-only screen. Do not include a navigation ` + Press ctrl/cmd+p to open the command palette. v{props.version} · Press Ctrl+C to exit ); } ``` -No `findDescendantById` — focus is either automatic or set via a ref. +No `findDescendantById`, no dashboard select, and no rendering tests for this visual-only screen. + +### Step 7.5 — Add Initial Pages + +Add placeholder pages used by the first command palette commands: + +- `src/tui/pages/github.tsx` +- `src/tui/pages/settings.tsx` + +These pages are intentionally lightweight until their real feature phases begin. ### Step 8 — Create Status Bar @@ -312,12 +313,9 @@ File: `src/tui/components/status-bar.tsx` A bottom-pinned bar that shows active keybinding hints for the current context. This is the **mandatory** UX pattern from OpenCode. ```tsx -import { useKeymapSelector } from "@opentui/keymap/solid"; import { tokens } from "../tokens"; export function StatusBar() { - const activeKeys = useKeymapSelector((km) => km.getActiveKeys({ includeMetadata: true })); - return ( - - {activeKeys().length > 0 - ? activeKeys() - .map((k) => k.display) - .join(" · ") - : "↑↓ navigate · enter select · esc back · q quit"} - + ctrl/cmd+p commands · q quit ); } ``` -The status bar is rendered inside the root app layout, below the main content area, using absolute positioning. +The status bar is rendered inside the root app layout, below the main content area, using absolute positioning. It can become fully dynamic later, but the initial UX should reliably advertise the command palette shortcut. ### Step 9 — Create Command Palette @@ -347,9 +339,9 @@ File: `src/tui/components/command-palette.tsx` A **mandatory** keyboard-driven overlay for quick navigation and actions. -- Triggered by `ctrl+p` via a global binding in `app.tsx` +- Triggered by `mod+p` plus explicit `ctrl+p` fallback via a global binding in `app.tsx` - Registers its own layer via `useBindings` that activates when the palette is open -- Renders a ` - + {/* filtered command list */} ) } @@ -374,6 +364,11 @@ export function CommandPalette(props: { onClose: () => void }) { The palette is rendered through `DialogProvider` via `dialog.replace()`. +Initial commands: + +- `Open GitHub` → route to `github` +- `Open Settings` → route to `settings` + ### Step 10 — Rewrite App Root File: `src/tui/app.tsx` @@ -401,6 +396,7 @@ export async function launchDashboard(version = APP_VERSION): Promise { useBindings(() => ({ bindings: [ + { key: "mod+p", cmd: "command-palette.show", desc: "Command palette" }, { key: "ctrl+p", cmd: "command-palette.show", desc: "Command palette" }, { key: "q", cmd: "quit", run: () => renderer.destroy() }, ], @@ -448,8 +444,7 @@ Per the testing philosophy in `AGENTS.md`: - **Remove existing dashboard tests** that assert text content (`expect(frame).toContain(...)`) — those test the renderer's output, not our logic. - **Test pure logic and state transformations:** - `UpdateDialog` — test that correct version strings and install command are constructed - - `CommandPalette` — test command filtering against a query string (pure function) - - `StatusBar` — test key metadata formatting (pure function that maps binding arrays to display strings) +- `CommandPalette` — test command filtering against a query string (pure function) - `tokens.ts` — verify palette values are readonly and token keys map to palette keys (structural contract) - `update-check.ts` — existing tests already cover this well; keep them - **Future tests:** dialogs' `onClose` callbacks, command dispatch behavior (if factored as testable functions). @@ -460,9 +455,9 @@ Per the testing philosophy in `AGENTS.md`: - Add a local keymap module only when Phase 3+ needs app-specific command registration, key formatting, mode stacks, leader keys, or configurable user bindings. - The status bar is intentionally simple — it can become richer (git branch, AWS profile, timer status) in Phase 5-6. -- The command palette is a shell — its command list is populated as real commands are built. +- The command palette starts with GitHub and Settings navigation commands; add real commands as feature pages mature. - DialogProvider supports a stack but we only use single-dialog mode for now. Stack semantics are ready for multi-dialog flows like wizards. -- No router is introduced yet. The MVP dashboard nav leads to "coming soon" placeholders; real page routing is added per feature phase. +- Routing is intentionally local app state for now (`home`, `github`, `settings`). Add a larger router only when route complexity requires it. --- diff --git a/src/tui/app.tsx b/src/tui/app.tsx index b611c2e..41f47ce 100644 --- a/src/tui/app.tsx +++ b/src/tui/app.tsx @@ -1,4 +1,4 @@ -import { onMount } from "solid-js"; +import { createSignal, onMount } from "solid-js"; import { render, useRenderer } from "@opentui/solid"; import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"; import { KeymapProvider, useBindings } from "@opentui/keymap/solid"; @@ -6,20 +6,58 @@ import { checkForUpdate } from "../utils/update-check.ts"; import { APP_VERSION } from "../version.ts"; import { DialogProvider, useDialog } from "./components/dialog.tsx"; import { UpdateDialog } from "./components/update-dialog.tsx"; +import { CommandPalette, type CommandEntry } from "./components/command-palette.tsx"; import { Dashboard } from "./pages/dashboard.tsx"; +import { GitHubPage } from "./pages/github.tsx"; +import { SettingsPage } from "./pages/settings.tsx"; import { StatusBar } from "./components/status-bar.tsx"; import { tokens } from "./tokens.ts"; const REPO = "wethegit/wtc"; +type Route = "home" | "github" | "settings"; function AppContent(props: { version: string }) { const dialog = useDialog(); const renderer = useRenderer(); + const [route, setRoute] = createSignal("home"); + + const commands = (): CommandEntry[] => [ + { + id: "github.open", + title: "Open GitHub", + description: "Repository workflows", + onSelect: () => setRoute("github"), + }, + { + id: "settings.open", + title: "Open Settings", + description: "Configuration and setup", + onSelect: () => setRoute("settings"), + }, + ]; + + const openCommandPalette = () => { + dialog.replace(() => dialog.clear()} />); + }; useBindings(() => ({ bindings: [ + { + key: "mod+p", + desc: "Command palette", + group: "Global", + cmd: openCommandPalette, + }, + { + key: "ctrl+p", + desc: "Command palette", + group: "Global", + cmd: openCommandPalette, + }, { key: "q", + desc: "Quit", + group: "Global", cmd: "quit", run: () => renderer.destroy(), }, @@ -42,7 +80,13 @@ function AppContent(props: { version: string }) { return ( - + {route() === "github" ? ( + + ) : route() === "settings" ? ( + + ) : ( + + )} ); diff --git a/src/tui/components/command-palette.tsx b/src/tui/components/command-palette.tsx index 2f35617..4b14a40 100644 --- a/src/tui/components/command-palette.tsx +++ b/src/tui/components/command-palette.tsx @@ -2,7 +2,6 @@ import { createSignal } from "solid-js"; import { TextAttributes } from "@opentui/core"; import { useBindings } from "@opentui/keymap/solid"; import { tokens } from "../tokens.ts"; -import { useDialog } from "./dialog.tsx"; export interface CommandEntry { id: string; @@ -11,22 +10,25 @@ export interface CommandEntry { onSelect: () => void; } -export function showCommandPalette(entries: () => CommandEntry[]) { - const dialog = useDialog(); - - dialog.replace(() => dialog.clear()} />); +export function filterCommands(entries: readonly CommandEntry[], query: string): CommandEntry[] { + const q = query.trim().toLowerCase(); + if (!q) return [...entries]; + return entries.filter( + (entry) => + entry.title.toLowerCase().includes(q) || entry.description?.toLowerCase().includes(q), + ); } -function CommandPalette(props: { entries: CommandEntry[]; onClose: () => void }) { +export function CommandPalette(props: { entries: CommandEntry[]; onClose: () => void }) { const [query, setQuery] = createSignal(""); const [selectedIndex, setSelectedIndex] = createSignal(0); + const filtered = () => filterCommands(props.entries, query()); - const filtered = () => { - const q = query().toLowerCase(); - if (!q) return props.entries; - return props.entries.filter( - (e) => e.title.toLowerCase().includes(q) || e.description?.toLowerCase().includes(q), - ); + const selectCurrent = () => { + const entry = filtered()[selectedIndex()]; + if (!entry) return; + entry.onSelect(); + props.onClose(); }; useBindings(() => ({ @@ -45,18 +47,13 @@ function CommandPalette(props: { entries: CommandEntry[]; onClose: () => void }) { key: "down", cmd: "palette.down", - run: () => setSelectedIndex((i) => Math.min(filtered().length - 1, i + 1)), + run: () => setSelectedIndex((i) => Math.min(Math.max(0, filtered().length - 1), i + 1)), }, { key: "return", desc: "Select command", group: "CommandPalette", - cmd: () => { - const entry = filtered()[selectedIndex()]; - if (entry) { - entry.onSelect(); - } - }, + cmd: selectCurrent, }, ], })); @@ -80,6 +77,7 @@ function CommandPalette(props: { entries: CommandEntry[]; onClose: () => void }) backgroundColor={i === selectedIndex() ? tokens.selectionBg : undefined} onMouseUp={() => { entry.onSelect(); + props.onClose(); }} > diff --git a/src/tui/components/status-bar.tsx b/src/tui/components/status-bar.tsx index 77cb96f..59ff81b 100644 --- a/src/tui/components/status-bar.tsx +++ b/src/tui/components/status-bar.tsx @@ -1,17 +1,6 @@ -import { useKeymapSelector } from "@opentui/keymap/solid"; import { tokens } from "../tokens.ts"; export function StatusBar() { - const activeKeys = useKeymapSelector((km) => km.getActiveKeys({ includeMetadata: true })); - - const hint = () => { - const keys = activeKeys(); - if (keys.length > 0) { - return keys.map((k) => k.display).join(" · "); - } - return "↑↓ navigate · enter select · esc back · q quit"; - }; - return ( - {hint()} + ctrl/cmd+p commands · q quit ); } diff --git a/src/tui/pages/dashboard.tsx b/src/tui/pages/dashboard.tsx index 7748267..e0caef8 100644 --- a/src/tui/pages/dashboard.tsx +++ b/src/tui/pages/dashboard.tsx @@ -1,33 +1,16 @@ import { TextAttributes } from "@opentui/core"; import { tokens } from "../tokens.ts"; -const navItems = [ - { name: "GitHub (coming soon)", description: "Repository workflows" }, - { name: "Amplify (coming soon)", description: "Hosting setup" }, - { name: "Teamwork (coming soon)", description: "Tasks and timers" }, - { name: "Settings (coming soon)", description: "Configuration" }, -]; - export function Dashboard(props: { version?: string }) { const version = props.version ?? "0.1.0"; return ( - + What will you build? - { - setQuery(val); - setSelectedIndex(0); - }} - placeholder="Type to filter..." - /> - - {filtered().map((entry, i) => ( - { - entry.onSelect(); - props.onClose(); - }} - > - - {entry.title} - - {entry.description && — {entry.description}} - - ))} - {filtered().length === 0 && No matching commands} - - ↑↓ navigate · enter select · esc close - + const options = createMemo[]>(() => + entries() + .filter(isVisiblePaletteCommand) + .map((entry) => ({ + title: typeof entry.command.title === "string" ? entry.command.title : entry.command.name, + description: typeof entry.command.desc === "string" ? entry.command.desc : undefined, + category: typeof entry.command.category === "string" ? entry.command.category : undefined, + value: entry.command.name, + onSelect: () => { + dialog.clear(); + keymap.dispatchCommand(entry.command.name); + }, + })), ); + + return ; } diff --git a/src/tui/components/dialog-select.tsx b/src/tui/components/dialog-select.tsx new file mode 100644 index 0000000..ba63a0d --- /dev/null +++ b/src/tui/components/dialog-select.tsx @@ -0,0 +1,123 @@ +import { createMemo, createSignal } from "solid-js"; +import { InputRenderable, TextAttributes } from "@opentui/core"; +import { useBindings } from "@opentui/keymap/solid"; +import { tokens } from "../tokens.ts"; +import { useDialog } from "./dialog.tsx"; + +export interface DialogSelectOption { + title: string; + value: T; + description?: string; + category?: string; + footer?: string; + onSelect?: () => void; +} + +export function filterDialogSelectOptions( + options: readonly DialogSelectOption[], + query: string, +): DialogSelectOption[] { + const q = query.trim().toLowerCase(); + if (!q) return [...options]; + return options.filter( + (option) => + option.title.toLowerCase().includes(q) || + option.description?.toLowerCase().includes(q) || + option.category?.toLowerCase().includes(q), + ); +} + +export function DialogSelect(props: { title: string; options: DialogSelectOption[] }) { + const dialog = useDialog(); + const [query, setQuery] = createSignal(""); + const [selectedIndex, setSelectedIndex] = createSignal(0); + const filtered = createMemo(() => filterDialogSelectOptions(props.options, query())); + let input: InputRenderable | undefined; + + const move = (direction: 1 | -1) => { + if (filtered().length === 0) return; + setSelectedIndex((index) => { + const next = index + direction; + if (next < 0) return filtered().length - 1; + if (next >= filtered().length) return 0; + return next; + }); + }; + + const submit = () => { + const option = filtered()[selectedIndex()]; + if (!option) return; + option.onSelect?.(); + }; + + useBindings(() => ({ + bindings: [ + { + key: "escape", + desc: "Close dialog", + group: "Dialog", + cmd: () => dialog.clear(), + }, + { + key: "up", + desc: "Previous item", + group: "Dialog", + cmd: () => move(-1), + }, + { + key: "down", + desc: "Next item", + group: "Dialog", + cmd: () => move(1), + }, + { + key: "return", + desc: "Select item", + group: "Dialog", + cmd: submit, + }, + ], + })); + + return ( + + + + {props.title} + + dialog.clear()}> + esc + + + { + setQuery(value); + setSelectedIndex(0); + }} + placeholder="Search" + ref={(renderable) => { + input = renderable; + setTimeout(() => { + if (!input || input.isDestroyed) return; + input.focus(); + }, 1); + }} + /> + + {filtered().map((option, index) => ( + option.onSelect?.()} + > + + {option.title} + + {option.description && — {option.description}} + + ))} + {filtered().length === 0 && No matching commands} + + ↑↓ navigate · enter select · esc close + + ); +} diff --git a/src/tui/components/dialog.tsx b/src/tui/components/dialog.tsx index 333bb6b..47fdc2c 100644 --- a/src/tui/components/dialog.tsx +++ b/src/tui/components/dialog.tsx @@ -1,23 +1,30 @@ import { createContext, useContext, type ParentProps, type JSX, Show } from "solid-js"; import { createStore } from "solid-js/store"; -import { Portal, useRenderer, useTerminalDimensions } from "@opentui/solid"; +import { useTerminalDimensions } from "@opentui/solid"; import { RGBA } from "@opentui/core"; import { useBindings } from "@opentui/keymap/solid"; import { tokens } from "../tokens.ts"; +type DialogElement = JSX.Element | (() => JSX.Element); + interface DialogItem { - element: JSX.Element; + element: DialogElement; onClose?: () => void; } -interface DialogContextValue { - show(element: JSX.Element, onClose?: () => void): void; - replace(element: JSX.Element, onClose?: () => void): void; +export interface DialogContextValue { + show(element: DialogElement, onClose?: () => void): void; + replace(element: DialogElement, onClose?: () => void): void; clear(): void; } const DialogContext = createContext(); +function renderDialogElement(element: DialogElement | undefined) { + if (typeof element === "function") return element(); + return element; +} + function DialogOverlay(props: ParentProps<{ onClose: () => void }>) { const dimensions = useTerminalDimensions(); @@ -52,8 +59,6 @@ export function DialogProvider(props: ParentProps) { stack: [] as DialogItem[], }); - const renderer = useRenderer(); - useBindings(() => ({ enabled: store.stack.length > 0, bindings: [ @@ -71,10 +76,10 @@ export function DialogProvider(props: ParentProps) { })); const value: DialogContextValue = { - show(element: JSX.Element, onClose?: () => void) { + show(element: DialogElement, onClose?: () => void) { setStore("stack", [...store.stack, { element, onClose }]); }, - replace(element: JSX.Element, onClose?: () => void) { + replace(element: DialogElement, onClose?: () => void) { for (const item of store.stack) { item.onClose?.(); } @@ -91,11 +96,11 @@ export function DialogProvider(props: ParentProps) { return ( {props.children} - - 0}> - value.clear()}>{store.stack.at(-1)?.element} - - + 0}> + value.clear()}> + {renderDialogElement(store.stack.at(-1)?.element)} + + ); } diff --git a/tests/tui/command-palette.test.ts b/tests/tui/command-palette.test.ts deleted file mode 100644 index e6dfff4..0000000 --- a/tests/tui/command-palette.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { filterCommands, type CommandEntry } from "../../src/tui/components/command-palette.tsx"; - -function command(id: string, title: string, description?: string): CommandEntry { - return { - id, - title, - description, - onSelect() {}, - }; -} - -describe("filterCommands", () => { - const commands = [ - command("github.open", "Open GitHub", "Repository workflows"), - command("settings.open", "Open Settings", "Configuration and setup"), - ]; - - test("returns all commands for empty query", () => { - expect(filterCommands(commands, "").map((entry) => entry.id)).toEqual([ - "github.open", - "settings.open", - ]); - }); - - test("filters by title", () => { - expect(filterCommands(commands, "git").map((entry) => entry.id)).toEqual(["github.open"]); - }); - - test("filters by description", () => { - expect(filterCommands(commands, "config").map((entry) => entry.id)).toEqual(["settings.open"]); - }); -}); diff --git a/tests/tui/dialog-select.test.ts b/tests/tui/dialog-select.test.ts new file mode 100644 index 0000000..a53c263 --- /dev/null +++ b/tests/tui/dialog-select.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, test } from "bun:test"; +import { + filterDialogSelectOptions, + type DialogSelectOption, +} from "../../src/tui/components/dialog-select.tsx"; + +function option(value: string, title: string, description?: string): DialogSelectOption { + return { value, title, description }; +} + +describe("filterDialogSelectOptions", () => { + const options = [ + option("github.open", "Open GitHub", "Repository workflows"), + option("settings.open", "Open Settings", "Configuration and setup"), + ]; + + test("returns all options for empty query", () => { + expect(filterDialogSelectOptions(options, "").map((entry) => entry.value)).toEqual([ + "github.open", + "settings.open", + ]); + }); + + test("filters by title", () => { + expect(filterDialogSelectOptions(options, "git").map((entry) => entry.value)).toEqual([ + "github.open", + ]); + }); + + test("filters by description", () => { + expect(filterDialogSelectOptions(options, "config").map((entry) => entry.value)).toEqual([ + "settings.open", + ]); + }); +}); From f563583d7155659b21ffb277f9173db4bf0d174c Mon Sep 17 00:00:00 2001 From: Marlon Marcello Date: Thu, 11 Jun 2026 18:01:59 -0700 Subject: [PATCH 14/20] fix: PR template note --- .github/pull_request_template.md | 10 +++++----- src/tui/app.tsx | 5 +---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index eb72184..bed13e2 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,6 +1,6 @@ ## Summary -- +Description ## Type of Change @@ -12,11 +12,11 @@ ## Local Verification +- [ ] `bun run fmt` - [ ] `bun run lint` -- [ ] `bun run fmt:check` - [ ] `bun run check` - [ ] `bun test` -- [ ] `bun run build` if this affects CLI, install, release, update, or native TUI packaging behavior +- [ ] `bun run build` ## Release Impact @@ -27,8 +27,8 @@ - [ ] Updates build/release packaging - [ ] Breaking change -## Notes - + diff --git a/src/tui/app.tsx b/src/tui/app.tsx index 1857c7b..3d4f1bf 100644 --- a/src/tui/app.tsx +++ b/src/tui/app.tsx @@ -6,10 +6,7 @@ import { checkForUpdate } from "../utils/update-check.ts"; import { APP_VERSION } from "../version.ts"; import { DialogProvider, useDialog } from "./components/dialog.tsx"; import { UpdateDialog } from "./components/update-dialog.tsx"; -import { - COMMAND_PALETTE_COMMAND, - CommandPaletteDialog, -} from "./components/command-palette.tsx"; +import { COMMAND_PALETTE_COMMAND, CommandPaletteDialog } from "./components/command-palette.tsx"; import { Dashboard } from "./pages/dashboard.tsx"; import { GitHubPage } from "./pages/github.tsx"; import { SettingsPage } from "./pages/settings.tsx"; From 743152d8581ea216aa48294aa254ad67e5cc49c3 Mon Sep 17 00:00:00 2001 From: Marlon Marcello Date: Thu, 11 Jun 2026 18:02:45 -0700 Subject: [PATCH 15/20] chore: changeset --- .changeset/five-aliens-thank.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/five-aliens-thank.md diff --git a/.changeset/five-aliens-thank.md b/.changeset/five-aliens-thank.md new file mode 100644 index 0000000..e9c47bc --- /dev/null +++ b/.changeset/five-aliens-thank.md @@ -0,0 +1,5 @@ +--- +"wtc": minor +--- + +refactor: solidJS bindings From 27d67204daac11fefd7d60eb3f97b0595d1150d5 Mon Sep 17 00:00:00 2001 From: Marlon Marcello Date: Fri, 12 Jun 2026 14:12:00 -0700 Subject: [PATCH 16/20] fix: removes prop drilling for version --- .zed/settings.json | 236 +++++++++++++++++++++++++++ src/index.ts | 4 +- src/tui/app.tsx | 25 ++- src/tui/components/update-dialog.tsx | 4 +- src/tui/pages/dashboard.tsx | 9 +- src/utils/update-check.ts | 20 ++- 6 files changed, 265 insertions(+), 33 deletions(-) create mode 100644 .zed/settings.json diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..4ba8ef4 --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,236 @@ +{ + "lsp": { + "oxlint": { + "initialization_options": { + "settings": { + "configPath": null, + "run": "onType", + "disableNestedConfig": false, + "fixKind": "safe_fix", + "unusedDisableDirectives": "deny" + } + } + }, + "oxfmt": { + "initialization_options": { + "settings": { + "fmt.configPath": null, + "run": "onSave" + } + } + } + }, + "languages": { + "CSS": { + "format_on_save": "on", + "prettier": { + "allowed": false + }, + "formatter": [ + { + "language_server": { + "name": "oxfmt" + } + } + ] + }, + "GraphQL": { + "format_on_save": "on", + "prettier": { + "allowed": false + }, + "formatter": [ + { + "language_server": { + "name": "oxfmt" + } + } + ] + }, + "Handlebars": { + "format_on_save": "on", + "prettier": { + "allowed": false + }, + "formatter": [ + { + "language_server": { + "name": "oxfmt" + } + } + ] + }, + "HTML": { + "format_on_save": "on", + "prettier": { + "allowed": false + }, + "formatter": [ + { + "language_server": { + "name": "oxfmt" + } + } + ] + }, + "JavaScript": { + "format_on_save": "on", + "prettier": { + "allowed": false + }, + "formatter": [ + { + "language_server": { + "name": "oxfmt" + } + }, + { + "code_action": "source.fixAll.oxc" + } + ] + }, + "JSON": { + "format_on_save": "on", + "prettier": { + "allowed": false + }, + "formatter": [ + { + "language_server": { + "name": "oxfmt" + } + } + ] + }, + "JSON5": { + "format_on_save": "on", + "prettier": { + "allowed": false + }, + "formatter": [ + { + "language_server": { + "name": "oxfmt" + } + } + ] + }, + "JSONC": { + "format_on_save": "on", + "prettier": { + "allowed": false + }, + "formatter": [ + { + "language_server": { + "name": "oxfmt" + } + } + ] + }, + "Less": { + "format_on_save": "on", + "prettier": { + "allowed": false + }, + "formatter": [ + { + "language_server": { + "name": "oxfmt" + } + } + ] + }, + "Markdown": { + "format_on_save": "on", + "prettier": { + "allowed": false + }, + "formatter": [ + { + "language_server": { + "name": "oxfmt" + } + } + ] + }, + "MDX": { + "format_on_save": "on", + "prettier": { + "allowed": false + }, + "formatter": [ + { + "language_server": { + "name": "oxfmt" + } + } + ] + }, + "SCSS": { + "format_on_save": "on", + "prettier": { + "allowed": false + }, + "formatter": [ + { + "language_server": { + "name": "oxfmt" + } + } + ] + }, + "TypeScript": { + "format_on_save": "on", + "prettier": { + "allowed": false + }, + "formatter": [ + { + "language_server": { + "name": "oxfmt" + } + } + ] + }, + "TSX": { + "format_on_save": "on", + "prettier": { + "allowed": false + }, + "formatter": [ + { + "language_server": { + "name": "oxfmt" + } + } + ] + }, + "Vue.js": { + "format_on_save": "on", + "prettier": { + "allowed": false + }, + "formatter": [ + { + "language_server": { + "name": "oxfmt" + } + } + ] + }, + "YAML": { + "format_on_save": "on", + "prettier": { + "allowed": false + }, + "formatter": [ + { + "language_server": { + "name": "oxfmt" + } + } + ] + } + } +} diff --git a/src/index.ts b/src/index.ts index aaf18c3..0ad034f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,8 @@ -import { APP_VERSION } from "./version.ts"; - const args = Bun.argv.slice(2); if (args.length === 0) { const { runTUI } = await import("./tui/app.tsx"); - await runTUI(APP_VERSION); + await runTUI(); } else { const { runCli } = await import("./cli/parser.ts"); await runCli(); diff --git a/src/tui/app.tsx b/src/tui/app.tsx index 3d4f1bf..2695603 100644 --- a/src/tui/app.tsx +++ b/src/tui/app.tsx @@ -2,8 +2,9 @@ import { createSignal, onMount } from "solid-js"; import { render, useKeyboard, useRenderer } from "@opentui/solid"; import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"; import { KeymapProvider, useBindings, useKeymap } from "@opentui/keymap/solid"; + import { checkForUpdate } from "../utils/update-check.ts"; -import { APP_VERSION } from "../version.ts"; + import { DialogProvider, useDialog } from "./components/dialog.tsx"; import { UpdateDialog } from "./components/update-dialog.tsx"; import { COMMAND_PALETTE_COMMAND, CommandPaletteDialog } from "./components/command-palette.tsx"; @@ -16,7 +17,7 @@ import { tokens } from "./tokens.ts"; const REPO = "wethegit/wtc"; type Route = "home" | "github" | "settings"; -function AppContent(props: { version: string }) { +function Home() { const dialog = useDialog(); const keymap = useKeymap(); const renderer = useRenderer(); @@ -110,15 +111,9 @@ function AppContent(props: { version: string }) { }); onMount(() => { - checkForUpdate(props.version).then((info) => { + checkForUpdate().then((info) => { if (info.updateAvailable) { - dialog.replace(() => ( - - )); + dialog.replace(() => ); } }); }); @@ -130,28 +125,28 @@ function AppContent(props: { version: string }) { ) : route() === "settings" ? ( ) : ( - + )} ); } -function AppShell(props: { version: string }) { +function App() { const renderer = useRenderer(); const keymap = createDefaultOpenTuiKeymap(renderer); return ( - + ); } -export async function runTUI(version = APP_VERSION): Promise { - await render(() => , { +export async function runTUI(): Promise { + await render(() => , { exitOnCtrlC: false, backgroundColor: tokens.bg, useKittyKeyboard: {}, diff --git a/src/tui/components/update-dialog.tsx b/src/tui/components/update-dialog.tsx index df1d1b2..1e41d76 100644 --- a/src/tui/components/update-dialog.tsx +++ b/src/tui/components/update-dialog.tsx @@ -2,9 +2,9 @@ import { TextAttributes } from "@opentui/core"; import { useBindings } from "@opentui/keymap/solid"; import { tokens } from "../tokens.ts"; import { useDialog } from "./dialog.tsx"; +import { APP_VERSION } from "../../version.ts"; export interface UpdateDialogProps { - currentVersion: string; latestVersion: string; repo: string; } @@ -36,7 +36,7 @@ export function UpdateDialog(props: UpdateDialogProps) { - v{props.currentVersion} + v{APP_VERSION} v{props.latestVersion} diff --git a/src/tui/pages/dashboard.tsx b/src/tui/pages/dashboard.tsx index e0caef8..ab29bab 100644 --- a/src/tui/pages/dashboard.tsx +++ b/src/tui/pages/dashboard.tsx @@ -1,9 +1,10 @@ import { TextAttributes } from "@opentui/core"; -import { tokens } from "../tokens.ts"; -export function Dashboard(props: { version?: string }) { - const version = props.version ?? "0.1.0"; +import { APP_VERSION } from "../../version.ts"; + +import { tokens } from "../tokens.ts"; +export function Dashboard() { return ( @@ -11,7 +12,7 @@ export function Dashboard(props: { version?: string }) { What will you build? Press ctrl/cmd+p to open the command palette. - v{version} · Press Ctrl+C to exit + v{APP_VERSION} · Press Ctrl+C to exit ); } diff --git a/src/utils/update-check.ts b/src/utils/update-check.ts index be1b7be..4b6b7e3 100644 --- a/src/utils/update-check.ts +++ b/src/utils/update-check.ts @@ -1,3 +1,5 @@ +import { APP_VERSION } from "../version"; + const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours const REPO = "wethegit/wtc"; @@ -71,15 +73,15 @@ async function fetchLatestVersion(): Promise { return data.tag_name; } -export async function checkForUpdate(currentVersion: string): Promise { +export async function checkForUpdate(): Promise { const cached = await readCache(); const now = Date.now(); if (cached && now - cached.checkedAt < CACHE_TTL_MS) { return { - currentVersion, + currentVersion: APP_VERSION, latestVersion: cached.latestVersion, - updateAvailable: compareVersions(cached.latestVersion, currentVersion) > 0, + updateAvailable: compareVersions(cached.latestVersion, APP_VERSION) > 0, }; } @@ -88,22 +90,22 @@ export async function checkForUpdate(currentVersion: string): Promise 0, + updateAvailable: compareVersions(latestVersion, APP_VERSION) > 0, }; } catch { if (cached) { return { - currentVersion, + currentVersion: APP_VERSION, latestVersion: cached.latestVersion, - updateAvailable: compareVersions(cached.latestVersion, currentVersion) > 0, + updateAvailable: compareVersions(cached.latestVersion, APP_VERSION) > 0, }; } return { - currentVersion, - latestVersion: currentVersion, + currentVersion: APP_VERSION, + latestVersion: APP_VERSION, updateAvailable: false, }; } From c1a83107da1c26098c1ad9fb6b944fceeac90ddd Mon Sep 17 00:00:00 2001 From: Marlon Marcello Date: Fri, 12 Jun 2026 15:16:04 -0700 Subject: [PATCH 17/20] refactor: simplify consts and props --- bunfig.toml | 2 ++ src/cli/commands/upgrade.ts | 9 +++--- src/cli/parser.ts | 2 +- src/{version.ts => config/consts.ts} | 2 ++ src/tui/app.tsx | 5 ++-- src/tui/components/dialog.tsx | 9 +++--- src/tui/components/status-bar.tsx | 4 +-- src/tui/components/update-dialog.tsx | 33 +++++++++----------- src/tui/pages/dashboard.tsx | 2 +- src/tui/tokens.ts | 45 ++++++++++++++++------------ src/utils/update-check.ts | 2 +- 11 files changed, 60 insertions(+), 55 deletions(-) rename src/{version.ts => config/consts.ts} (63%) diff --git a/bunfig.toml b/bunfig.toml index 711a08b..1af0ede 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -1,3 +1,5 @@ +preload = ["@opentui/solid/preload"] + [install] exact = true # Only install newly resolved package versions published at least 3 days ago. diff --git a/src/cli/commands/upgrade.ts b/src/cli/commands/upgrade.ts index de21903..f247192 100644 --- a/src/cli/commands/upgrade.ts +++ b/src/cli/commands/upgrade.ts @@ -1,17 +1,16 @@ +import { APP_VERSION } from "../../config/consts.ts"; import { checkForUpdate } from "../../utils/update-check.ts"; -import { APP_VERSION } from "../../version.ts"; const REPO = "wethegit/wtc"; export async function upgrade(_args: { check: boolean }): Promise { - const currentVersion = APP_VERSION; - const info = await checkForUpdate(currentVersion); + const info = await checkForUpdate(); if (!info.updateAvailable) { - console.log(`You're up to date (v${currentVersion}).`); + console.log(`You're up to date (v${APP_VERSION}).`); return; } - console.log(`Update available: v${currentVersion} \u2192 ${info.latestVersion}`); + console.log(`Update available: v${APP_VERSION} \u2192 ${info.latestVersion}`); console.log(` curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | bash`); } diff --git a/src/cli/parser.ts b/src/cli/parser.ts index b99e9bb..6c9fb8b 100644 --- a/src/cli/parser.ts +++ b/src/cli/parser.ts @@ -1,7 +1,7 @@ import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { upgrade } from "./commands/upgrade.ts"; -import { APP_VERSION } from "../version.ts"; +import { APP_VERSION } from "../config/consts.ts"; export async function runCli(): Promise { const currentVersion = APP_VERSION; diff --git a/src/version.ts b/src/config/consts.ts similarity index 63% rename from src/version.ts rename to src/config/consts.ts index 670751d..042a918 100644 --- a/src/version.ts +++ b/src/config/consts.ts @@ -1 +1,3 @@ export const APP_VERSION = process.env.APP_VERSION ?? "0.1.0"; + +export const REPO = "wethegit/wtc"; diff --git a/src/tui/app.tsx b/src/tui/app.tsx index 2695603..c51725d 100644 --- a/src/tui/app.tsx +++ b/src/tui/app.tsx @@ -14,7 +14,6 @@ import { SettingsPage } from "./pages/settings.tsx"; import { StatusBar } from "./components/status-bar.tsx"; import { tokens } from "./tokens.ts"; -const REPO = "wethegit/wtc"; type Route = "home" | "github" | "settings"; function Home() { @@ -113,13 +112,13 @@ function Home() { onMount(() => { checkForUpdate().then((info) => { if (info.updateAvailable) { - dialog.replace(() => ); + dialog.replace(() => ); } }); }); return ( - + {route() === "github" ? ( ) : route() === "settings" ? ( diff --git a/src/tui/components/dialog.tsx b/src/tui/components/dialog.tsx index 47fdc2c..7aba12b 100644 --- a/src/tui/components/dialog.tsx +++ b/src/tui/components/dialog.tsx @@ -1,8 +1,8 @@ import { createContext, useContext, type ParentProps, type JSX, Show } from "solid-js"; import { createStore } from "solid-js/store"; import { useTerminalDimensions } from "@opentui/solid"; -import { RGBA } from "@opentui/core"; import { useBindings } from "@opentui/keymap/solid"; + import { tokens } from "../tokens.ts"; type DialogElement = JSX.Element | (() => JSX.Element); @@ -38,13 +38,14 @@ function DialogOverlay(props: ParentProps<{ onClose: () => void }>) { zIndex={3000} left={0} top={0} - backgroundColor={RGBA.fromInts(0, 0, 0, 150)} + backgroundColor={tokens.surfaceOverlay} > diff --git a/src/tui/components/status-bar.tsx b/src/tui/components/status-bar.tsx index 59ff81b..9aedbe0 100644 --- a/src/tui/components/status-bar.tsx +++ b/src/tui/components/status-bar.tsx @@ -7,8 +7,8 @@ export function StatusBar() { bottom={0} left={0} width="100%" - height={1} - backgroundColor={tokens.surface} + paddingX={1} + backgroundColor={tokens.surfaceOverlay} > ctrl/cmd+p commands · q quit diff --git a/src/tui/components/update-dialog.tsx b/src/tui/components/update-dialog.tsx index 1e41d76..858830b 100644 --- a/src/tui/components/update-dialog.tsx +++ b/src/tui/components/update-dialog.tsx @@ -1,12 +1,14 @@ import { TextAttributes } from "@opentui/core"; import { useBindings } from "@opentui/keymap/solid"; + +import { APP_VERSION, REPO } from "../../config/consts.ts"; + import { tokens } from "../tokens.ts"; + import { useDialog } from "./dialog.tsx"; -import { APP_VERSION } from "../../version.ts"; export interface UpdateDialogProps { latestVersion: string; - repo: string; } export function UpdateDialog(props: UpdateDialogProps) { @@ -23,33 +25,26 @@ export function UpdateDialog(props: UpdateDialogProps) { ], })); - const installCmd = `curl -fsSL https://raw.githubusercontent.com/${props.repo}/main/install.sh | bash`; + const installCmd = `curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | bash`; return ( - + - - Update Available - + Update Available dialog.clear()}> esc - - v{APP_VERSION} + + v{APP_VERSION} - v{props.latestVersion} + {props.latestVersion} - - {installCmd} + + {installCmd} - - dialog.clear()} - > + + dialog.clear()}> ok diff --git a/src/tui/pages/dashboard.tsx b/src/tui/pages/dashboard.tsx index ab29bab..e4fae97 100644 --- a/src/tui/pages/dashboard.tsx +++ b/src/tui/pages/dashboard.tsx @@ -1,6 +1,6 @@ import { TextAttributes } from "@opentui/core"; -import { APP_VERSION } from "../../version.ts"; +import { APP_VERSION } from "../../config/consts.ts"; import { tokens } from "../tokens.ts"; diff --git a/src/tui/tokens.ts b/src/tui/tokens.ts index 0d64459..8e000c1 100644 --- a/src/tui/tokens.ts +++ b/src/tui/tokens.ts @@ -1,3 +1,5 @@ +import { RGBA } from "@opentui/core"; + /** * These were taken from our website CSS styles */ @@ -51,35 +53,40 @@ export const palette = { export const tokens = { // Surfaces - bg: palette.black, - surface: palette.black75, - surfaceRaised: palette.greyBlue, - surfaceOverlay: palette.black, + bg: RGBA.fromHex(palette.black), + surface: RGBA.fromHex(palette.black75), + surfaceRaised: RGBA.fromHex(palette.black50), + surfaceOverlay: RGBA.fromValues( + RGBA.fromHex(palette.black75).r, + RGBA.fromHex(palette.black75).g, + RGBA.fromHex(palette.black75).b, + 0.2, + ), // Text - text: palette.white, - textDim: palette.black50, - textMuted: palette.black50, - textAccent: palette.teal75, - textInverse: palette.black, + text: RGBA.fromHex(palette.white), + textDim: RGBA.fromHex(palette.black50), + textMuted: RGBA.fromHex(palette.black50), + textAccent: RGBA.fromHex(palette.teal75), + textInverse: RGBA.fromHex(palette.black), // Brand accent - accent: palette.teal75, - accentSoft: palette.teal50, + accent: RGBA.fromHex(palette.teal75), + accentSoft: RGBA.fromHex(palette.teal50), // Semantic - success: palette.green, - warning: palette.yellow, - danger: palette.pink50, - info: palette.teal50, + success: RGBA.fromHex(palette.green), + warning: RGBA.fromHex(palette.yellow), + danger: RGBA.fromHex(palette.pink50), + info: RGBA.fromHex(palette.teal50), // Interactive - selectionBg: palette.teal50, - selectionText: palette.black, + selectionBg: RGBA.fromHex(palette.teal50), + selectionText: RGBA.fromHex(palette.black), // Borders - border: palette.black75, - borderFocus: palette.teal75, + border: RGBA.fromHex(palette.black75), + borderFocus: RGBA.fromHex(palette.teal75), } as const; export type Tokens = typeof tokens; diff --git a/src/utils/update-check.ts b/src/utils/update-check.ts index 4b6b7e3..dce3697 100644 --- a/src/utils/update-check.ts +++ b/src/utils/update-check.ts @@ -1,4 +1,4 @@ -import { APP_VERSION } from "../version"; +import { APP_VERSION } from "../config/consts"; const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours const REPO = "wethegit/wtc"; From a3a3340a86d6f73eddd0dc8e09b2f68cec66ec66 Mon Sep 17 00:00:00 2001 From: Marlon Marcello Date: Fri, 12 Jun 2026 15:25:59 -0700 Subject: [PATCH 18/20] chore: documentation --- CONTRIBUTING.md | 13 ++++--- scripts/build.ts | 7 ++++ src/cli/commands/upgrade.ts | 6 ++++ src/cli/parser.ts | 7 ++++ src/config/consts.ts | 2 ++ src/config/crypto.ts | 20 +++++++++++ src/config/manager.ts | 24 +++++++++++++ src/config/schema.ts | 7 ++++ src/index.ts | 3 ++ src/tui/app.tsx | 21 ++++++++++++ src/tui/components/command-palette.tsx | 24 +++++++++++++ src/tui/components/dialog-select.tsx | 31 +++++++++++++++++ src/tui/components/dialog.tsx | 37 ++++++++++++++++++++ src/tui/components/status-bar.tsx | 9 +++++ src/tui/components/update-dialog.tsx | 11 ++++++ src/tui/pages/dashboard.tsx | 7 ++++ src/tui/pages/github.tsx | 1 + src/tui/pages/settings.tsx | 1 + src/tui/tokens.ts | 14 +++++++- src/utils/update-check.ts | 47 ++++++++++++++++++++------ 20 files changed, 276 insertions(+), 16 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 48ecf6b..bbb0c5e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,10 @@ - [Bun](https://bun.com/) +Useful resources: + +- [OpenTUI Docs](https://opentui.com/) + ## Development Setup ```bash @@ -72,15 +76,14 @@ bun install --os="*" --cpu="*" @opentui/core Run the generated binary directly from the repository root: ```bash -./dist/wtc-linux-x64 --version -./dist/wtc-linux-x64 --help -./dist/wtc-linux-x64 +cd ./dist +wtc-linux-x64 --version +wtc-linux-x64 --help +wtc-linux-x64 ``` Use the filename that matches your platform. The dashboard starts with no arguments. Press `Ctrl+C` to exit. -Generated binaries live in `dist/`, which is ignored by git. - ## CI/CD and Releases CI runs automatically on pull requests targeting `main`. diff --git a/scripts/build.ts b/scripts/build.ts index 2cc5375..4fdcbdd 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -7,6 +7,8 @@ const arch = process.env.WTC_TARGET_ARCH ?? process.env.arch ?? process.arch; let targetSuffix: string; let bunTarget: "bun-darwin-arm64" | "bun-darwin-x64" | "bun-linux-x64"; +// The release workflow can override platform/arch to build named targets. Local +// builds default to the current machine so `bun run build` remains ergonomic. if (platform === "darwin" && arch === "arm64") { targetSuffix = "darwin-arm64"; bunTarget = "bun-darwin-arm64"; @@ -33,12 +35,17 @@ import solidPlugin from "@opentui/solid/bun-plugin"; const result = await Bun.build({ entrypoints: ["./src/index.ts"], + // Solid TSX must be compiled into OpenTUI render calls before Bun creates the + // standalone executable. Avoid using a top-level bunfig preload here because + // compiled binaries would try to resolve it at runtime. plugins: [solidPlugin], compile: { target: bunTarget, outfile: `./${outfilePath}`, }, define: { + // Inject the package version into the binary so `wtc --version`, CLI update + // checks, and TUI update dialogs all report the release version. "process.env.APP_VERSION": JSON.stringify(version), "process.env.OPENTUI_LIBC": platform === "linux" ? JSON.stringify("glibc") : "undefined", }, diff --git a/src/cli/commands/upgrade.ts b/src/cli/commands/upgrade.ts index f247192..fbbe7d3 100644 --- a/src/cli/commands/upgrade.ts +++ b/src/cli/commands/upgrade.ts @@ -3,6 +3,12 @@ import { checkForUpdate } from "../../utils/update-check.ts"; const REPO = "wethegit/wtc"; +/** + * Checks GitHub Releases and prints the install command when an update exists. + * + * The `_args` shape is already wired for future upgrade modes. Today only + * `--check` is accepted, so this command never mutates the local installation. + */ export async function upgrade(_args: { check: boolean }): Promise { const info = await checkForUpdate(); diff --git a/src/cli/parser.ts b/src/cli/parser.ts index 6c9fb8b..189d1d1 100644 --- a/src/cli/parser.ts +++ b/src/cli/parser.ts @@ -3,6 +3,13 @@ import { hideBin } from "yargs/helpers"; import { upgrade } from "./commands/upgrade.ts"; import { APP_VERSION } from "../config/consts.ts"; +/** + * Runs the yargs-powered CLI parser for non-interactive commands. + * + * The top-level entrypoint only imports this module when arguments are present, + * which keeps simple CLI commands independent from TUI startup cost and OpenTUI + * renderer initialization. + */ export async function runCli(): Promise { const currentVersion = APP_VERSION; diff --git a/src/config/consts.ts b/src/config/consts.ts index 042a918..f0d0e85 100644 --- a/src/config/consts.ts +++ b/src/config/consts.ts @@ -1,3 +1,5 @@ +/** Current application version injected by the build script for releases. */ export const APP_VERSION = process.env.APP_VERSION ?? "0.1.0"; +/** GitHub repository slug used by update and install flows. */ export const REPO = "wethegit/wtc"; diff --git a/src/config/crypto.ts b/src/config/crypto.ts index a251812..18bd806 100644 --- a/src/config/crypto.ts +++ b/src/config/crypto.ts @@ -6,14 +6,21 @@ const IV_LENGTH = 16; const SALT_LENGTH = 32; const DIGEST = "sha512"; +/** Hex-encoded encrypted payload persisted inside the WTC config file. */ export interface EncryptedPayload { + /** Random salt used to derive the encryption key. */ salt: string; + /** Random initialization vector used for AES-GCM. */ iv: string; + /** AES-GCM authentication tag used to verify ciphertext integrity. */ authTag: string; + /** Hex-encoded ciphertext. */ data: string; } function deriveKey(password: string, salt: Buffer): Buffer { + // Keep derivation deterministic for decrypting existing config payloads. If + // this changes later, add config versioning/migration before shipping it. return Buffer.from( createHash(DIGEST) .update(Buffer.concat([Buffer.from(password, "utf-8"), salt])) @@ -22,6 +29,12 @@ function deriveKey(password: string, salt: Buffer): Buffer { ); } +/** + * Encrypts a UTF-8 string with a password-derived AES-256-GCM key. + * + * A fresh salt and IV are generated for every call, so encrypting the same input + * twice should produce different payloads. + */ export function encrypt(plaintext: string, password: string): EncryptedPayload { const salt = randomBytes(SALT_LENGTH); const key = deriveKey(password, salt); @@ -40,6 +53,13 @@ export function encrypt(plaintext: string, password: string): EncryptedPayload { }; } +/** + * Decrypts a payload produced by `encrypt()`. + * + * Throws when the password is wrong, the auth tag fails, or the payload is + * malformed. Callers should surface those errors as invalid credentials rather + * than silently returning empty secrets. + */ export function decrypt(payload: EncryptedPayload, password: string): string { const salt = Buffer.from(payload.salt, "hex"); const key = deriveKey(password, salt); diff --git a/src/config/manager.ts b/src/config/manager.ts index 9408180..b5e97c6 100644 --- a/src/config/manager.ts +++ b/src/config/manager.ts @@ -10,6 +10,7 @@ import type { EncryptedPayload } from "./crypto.ts"; const CONFIG_DIR = join(homedir(), ".config", "wtc"); const CONFIG_PATH = join(CONFIG_DIR, "config.json"); +/** Default config written on first run. */ const defaultConfig: Config = { version: 1, encrypted: { @@ -25,11 +26,20 @@ const defaultConfig: Config = { }, }; +/** Decrypted secret values stored in the encrypted config payload. */ export interface DecryptedSecrets { + /** GitHub personal access token or compatible API token. */ github?: { token: string }; + /** Teamwork API key. */ teamwork?: { apiKey: string }; } +/** + * Ensures the WTC config directory and config file exist. + * + * This is safe to call before every read/write operation because it only creates + * missing filesystem entries. + */ export async function initConfig(): Promise { if (!existsSync(CONFIG_DIR)) { await mkdir(CONFIG_DIR, { recursive: true }); @@ -40,6 +50,7 @@ export async function initConfig(): Promise { } } +/** Loads and validates the persisted WTC config file. */ export async function loadConfig(): Promise { await initConfig(); @@ -49,11 +60,18 @@ export async function loadConfig(): Promise { return ConfigSchema.parse(parsed); } +/** Writes a validated config object to disk. */ export async function saveConfig(config: Config): Promise { await initConfig(); await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8"); } +/** + * Encrypts and saves secret values into the config file. + * + * Non-secret `plain` config is preserved. The caller owns password collection; + * this module only receives the password long enough to derive an encryption key. + */ export async function saveSecrets(decrypted: DecryptedSecrets, password: string): Promise { const config = await loadConfig(); const encryptedPayload: EncryptedPayload = encrypt(JSON.stringify(decrypted), password); @@ -62,6 +80,12 @@ export async function saveSecrets(decrypted: DecryptedSecrets, password: string) await saveConfig(config); } +/** + * Loads and decrypts saved secret values. + * + * Returns `null` when no encrypted payload has been saved yet. Throws if a + * payload exists but cannot be decrypted with the provided password. + */ export async function loadSecrets(password: string): Promise { const config = await loadConfig(); diff --git a/src/config/schema.ts b/src/config/schema.ts index 3532c5b..3d8db94 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -1,5 +1,11 @@ import { z } from "zod"; +/** + * Persisted config file schema for `~/.config/wtc/config.json`. + * + * `plain` stores non-secret preferences that can be edited directly. `encrypted` + * stores the encrypted secrets payload produced by `saveSecrets()`. + */ export const ConfigSchema = z.object({ version: z.literal(1), encrypted: z.object({ @@ -15,4 +21,5 @@ export const ConfigSchema = z.object({ }), }); +/** Parsed and validated WTC config shape. */ export type Config = z.infer; diff --git a/src/index.ts b/src/index.ts index 0ad034f..27f3f3f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,8 @@ const args = Bun.argv.slice(2); +// OpenCode-style entrypoint split: no arguments enters the interactive TUI, +// while any explicit argument stays in the CLI parser path. Dynamic imports keep +// TUI dependencies out of simple commands such as `wtc --version`. if (args.length === 0) { const { runTUI } = await import("./tui/app.tsx"); await runTUI(); diff --git a/src/tui/app.tsx b/src/tui/app.tsx index c51725d..5f24202 100644 --- a/src/tui/app.tsx +++ b/src/tui/app.tsx @@ -16,6 +16,7 @@ import { tokens } from "./tokens.ts"; type Route = "home" | "github" | "settings"; +/** Main TUI screen controller rendered inside the app providers. */ function Home() { const dialog = useDialog(); const keymap = useKeymap(); @@ -61,10 +62,15 @@ function Home() { ...command, })); + // Palette commands are registered as keymap commands instead of local arrays + // so other UI can query the same action registry and stay in sync with active + // route/focus conditions later. useBindings(() => ({ commands: commands(), })); + // Global bindings live at the app shell level. Feature screens should add + // their own scoped bindings with `useBindings()` when they need local actions. useBindings(() => ({ bindings: [ { @@ -94,6 +100,9 @@ function Home() { ], })); + // `useBindings()` is the primary key path. This fallback handles terminals or + // keyboard protocols that report modifier keys differently and guarantees that + // Ctrl+C tears down the renderer even when focus is inside an input. useKeyboard((key) => { if (key.name === "c" && key.ctrl) { key.preventDefault(); @@ -110,6 +119,8 @@ function Home() { }); onMount(() => { + // The update check is non-blocking. If a newer release exists, it enters the + // same dialog stack used by app actions so modal behavior stays consistent. checkForUpdate().then((info) => { if (info.updateAvailable) { dialog.replace(() => ); @@ -131,11 +142,14 @@ function Home() { ); } +/** Root provider tree for the Solid OpenTUI app. */ function App() { const renderer = useRenderer(); const keymap = createDefaultOpenTuiKeymap(renderer); return ( + // Provider order matters: the keymap needs the OpenTUI renderer, and dialogs + // need the keymap so they can register modal Escape/Return bindings. @@ -144,6 +158,13 @@ function App() { ); } +/** + * Starts the interactive TUI. + * + * The CLI entrypoint calls this when `wtc` is executed without arguments. The + * renderer disables OpenTUI's default Ctrl+C handling because the app shell owns + * graceful teardown through global key bindings. + */ export async function runTUI(): Promise { await render(() => , { exitOnCtrlC: false, diff --git a/src/tui/components/command-palette.tsx b/src/tui/components/command-palette.tsx index e358605..ad223a2 100644 --- a/src/tui/components/command-palette.tsx +++ b/src/tui/components/command-palette.tsx @@ -3,6 +3,14 @@ import { useKeymap, useKeymapSelector } from "@opentui/keymap/solid"; import { DialogSelect, type DialogSelectOption } from "./dialog-select.tsx"; import { useDialog } from "./dialog.tsx"; +/** + * Command name used to open the global command palette. + * + * Register a keybinding that points at this command, or dispatch it directly via + * `keymap.dispatchCommand(COMMAND_PALETTE_COMMAND)`. The app shell owns the + * command implementation so feature modules can stay decoupled from the dialog + * implementation. + */ export const COMMAND_PALETTE_COMMAND = "command.palette.show"; type PaletteEntry = ReturnType["getCommandEntries"]>[number]; @@ -11,9 +19,23 @@ function isVisiblePaletteCommand(entry: PaletteEntry) { return entry.command.hidden !== true && entry.command.name !== COMMAND_PALETTE_COMMAND; } +/** + * Dialog that lists reachable keymap commands for quick navigation and actions. + * + * To make a command appear here, register it with `useBindings()` and add + * `namespace: "palette"` to the command object. The palette queries only + * reachable commands, so commands can still be hidden by keymap conditions, + * focus targets, or `hidden: true`. + * + * Open this dialog through the keymap command registered by the app shell: + * `keymap.dispatchCommand(COMMAND_PALETTE_COMMAND)`. If a component already has + * dialog context, it can also call `dialog.replace(() => )`. + */ export function CommandPaletteDialog() { const dialog = useDialog(); const keymap = useKeymap(); + // `useKeymapSelector` tracks keymap state changes, so the palette refreshes + // when commands become reachable or unreachable while it is open. const entries = useKeymapSelector((keymap) => keymap.getCommandEntries({ namespace: "palette", @@ -31,6 +53,8 @@ export function CommandPaletteDialog() { category: typeof entry.command.category === "string" ? entry.command.category : undefined, value: entry.command.name, onSelect: () => { + // Close the palette before dispatching so route-changing commands do + // not leave stale dialog UI on top of the next screen. dialog.clear(); keymap.dispatchCommand(entry.command.name); }, diff --git a/src/tui/components/dialog-select.tsx b/src/tui/components/dialog-select.tsx index ba63a0d..dbcc41d 100644 --- a/src/tui/components/dialog-select.tsx +++ b/src/tui/components/dialog-select.tsx @@ -4,15 +4,33 @@ import { useBindings } from "@opentui/keymap/solid"; import { tokens } from "../tokens.ts"; import { useDialog } from "./dialog.tsx"; +/** + * Selectable option rendered by `DialogSelect`. + * + * `value` is owned by the caller and can be any stable identifier. `onSelect` + * should perform the action and close the dialog when appropriate. + */ export interface DialogSelectOption { + /** Primary label shown in the list. */ title: string; + /** Caller-owned identifier for the option. */ value: T; + /** Optional secondary text shown beside the title. */ description?: string; + /** Optional grouping metadata, currently used by filtering. */ category?: string; + /** Reserved for future contextual footer text. */ footer?: string; + /** Action to run when the option is selected by keyboard or mouse. */ onSelect?: () => void; } +/** + * Filters dialog select options using the title, description, and category. + * + * This helper is intentionally pure so command palette filtering stays covered + * by unit tests without rendering OpenTUI components. + */ export function filterDialogSelectOptions( options: readonly DialogSelectOption[], query: string, @@ -27,6 +45,14 @@ export function filterDialogSelectOptions( ); } +/** + * Searchable list dialog used by the command palette and future pickers. + * + * The component assumes it is rendered inside `DialogProvider` and + * `KeymapProvider`. It registers local dialog navigation bindings with + * `useBindings()` for Escape, Up, Down, and Return. Callers provide the options + * and decide what each option does in `onSelect`. + */ export function DialogSelect(props: { title: string; options: DialogSelectOption[] }) { const dialog = useDialog(); const [query, setQuery] = createSignal(""); @@ -50,6 +76,8 @@ export function DialogSelect(props: { title: string; options: DialogSelectOpt option.onSelect?.(); }; + // These bindings live with the dialog component so they are automatically + // disposed when the dialog is removed from the stack. useBindings(() => ({ bindings: [ { @@ -97,6 +125,9 @@ export function DialogSelect(props: { title: string; options: DialogSelectOpt placeholder="Search" ref={(renderable) => { input = renderable; + // OpenTUI creates the underlying input renderable during reconciliation. + // Delay focus until the renderable exists and has not been destroyed by + // a fast close/reopen cycle. setTimeout(() => { if (!input || input.isDestroyed) return; input.focus(); diff --git a/src/tui/components/dialog.tsx b/src/tui/components/dialog.tsx index 7aba12b..7cfc27f 100644 --- a/src/tui/components/dialog.tsx +++ b/src/tui/components/dialog.tsx @@ -5,6 +5,14 @@ import { useBindings } from "@opentui/keymap/solid"; import { tokens } from "../tokens.ts"; +/** + * Renderable dialog content. + * + * Prefer passing a factory, for example `dialog.replace(() => )`, + * when the dialog component uses Solid hooks or context. The provider renders the + * factory inside the active dialog owner so hooks like `useDialog()` and + * `useBindings()` resolve correctly. + */ type DialogElement = JSX.Element | (() => JSX.Element); interface DialogItem { @@ -12,9 +20,19 @@ interface DialogItem { onClose?: () => void; } +/** + * Dialog stack controller exposed to components below `DialogProvider`. + * + * The stack lets temporary UI layers compose without each caller needing to know + * who opened the previous layer. `show()` pushes a new layer, `replace()` closes + * all current layers and opens one layer, and `clear()` closes everything. + */ export interface DialogContextValue { + /** Pushes a dialog on top of the current stack. */ show(element: DialogElement, onClose?: () => void): void; + /** Closes existing dialogs and opens a single replacement dialog. */ replace(element: DialogElement, onClose?: () => void): void; + /** Closes all dialogs and runs their close callbacks. */ clear(): void; } @@ -25,6 +43,7 @@ function renderDialogElement(element: DialogElement | undefined) { return element; } +/** Full-screen overlay that centers the active dialog content. */ function DialogOverlay(props: ParentProps<{ onClose: () => void }>) { const dimensions = useTerminalDimensions(); @@ -55,11 +74,21 @@ function DialogOverlay(props: ParentProps<{ onClose: () => void }>) { ); } +/** + * Provides dialog stack state and keyboard behavior for modal overlays. + * + * `DialogProvider` must be rendered below `KeymapProvider` because it registers + * an Escape binding with `useBindings()`. That binding is enabled only while the + * stack has entries, so normal app-level Escape handling can remain inactive + * until a dialog is actually open. + */ export function DialogProvider(props: ParentProps) { const [store, setStore] = createStore({ stack: [] as DialogItem[], }); + // Dialogs are stack based so future flows can open a confirmation dialog from + // another dialog without losing the original layer underneath it. useBindings(() => ({ enabled: store.stack.length > 0, bindings: [ @@ -70,6 +99,8 @@ export function DialogProvider(props: ParentProps) { cmd: () => { const current = store.stack.at(-1); current?.onClose?.(); + // Escape only pops the active layer. Call `clear()` from dialog actions + // when the whole modal workflow should be dismissed. setStore("stack", store.stack.slice(0, -1)); }, }, @@ -106,6 +137,12 @@ export function DialogProvider(props: ParentProps) { ); } +/** + * Returns the active dialog controller. + * + * Use this inside components rendered below `DialogProvider` to open or close + * modal UI. Calling it outside the provider is a programmer error and throws. + */ export function useDialog(): DialogContextValue { const value = useContext(DialogContext); if (!value) { diff --git a/src/tui/components/status-bar.tsx b/src/tui/components/status-bar.tsx index 9aedbe0..10964ba 100644 --- a/src/tui/components/status-bar.tsx +++ b/src/tui/components/status-bar.tsx @@ -1,5 +1,14 @@ import { tokens } from "../tokens.ts"; +/** + * Bottom status strip for global hints and contextual state. + * + * Today this is intentionally static because the MVP has only global actions. + * Future feature screens should make this component data-driven, for example by + * passing status segments and active key hints from the current route. Keep this + * component as the single place that owns status bar styling so pages only + * describe what should be shown. + */ export function StatusBar() { return ( ({ bindings: [ { diff --git a/src/tui/pages/dashboard.tsx b/src/tui/pages/dashboard.tsx index e4fae97..a66881c 100644 --- a/src/tui/pages/dashboard.tsx +++ b/src/tui/pages/dashboard.tsx @@ -4,6 +4,13 @@ import { APP_VERSION } from "../../config/consts.ts"; import { tokens } from "../tokens.ts"; +/** + * Intro screen shown when the TUI starts. + * + * The dashboard intentionally stays navigation-light: global movement happens + * through the command palette, while this page establishes brand, version, and + * the primary keyboard affordance. + */ export function Dashboard() { return ( diff --git a/src/tui/pages/github.tsx b/src/tui/pages/github.tsx index ef0700e..7541970 100644 --- a/src/tui/pages/github.tsx +++ b/src/tui/pages/github.tsx @@ -1,6 +1,7 @@ import { TextAttributes } from "@opentui/core"; import { tokens } from "../tokens.ts"; +/** Placeholder route for upcoming GitHub repository workflows. */ export function GitHubPage() { return ( diff --git a/src/tui/pages/settings.tsx b/src/tui/pages/settings.tsx index d2d8401..326153a 100644 --- a/src/tui/pages/settings.tsx +++ b/src/tui/pages/settings.tsx @@ -1,6 +1,7 @@ import { TextAttributes } from "@opentui/core"; import { tokens } from "../tokens.ts"; +/** Placeholder route for configuration and setup workflows. */ export function SettingsPage() { return ( diff --git a/src/tui/tokens.ts b/src/tui/tokens.ts index 8e000c1..f7c0bbb 100644 --- a/src/tui/tokens.ts +++ b/src/tui/tokens.ts @@ -1,7 +1,11 @@ import { RGBA } from "@opentui/core"; /** - * These were taken from our website CSS styles + * Raw WTC brand color palette copied from the website design system. + * + * Prefer using `tokens` for component styling so UI code depends on semantic + * roles instead of raw brand names. Reach for `palette` only when defining new + * semantic tokens. */ export const palette = { // Brand core @@ -51,6 +55,13 @@ export const palette = { yellow75: "#f7a836", } as const; +/** + * Semantic TUI design tokens expressed as OpenTUI `RGBA` values. + * + * These tokens are the stable styling contract for components. Keeping them in + * one file makes it easier to evolve contrast, overlays, and focus states + * without touching every screen. + */ export const tokens = { // Surfaces bg: RGBA.fromHex(palette.black), @@ -89,4 +100,5 @@ export const tokens = { borderFocus: RGBA.fromHex(palette.teal75), } as const; +/** Type helper for consumers that need the full token object shape. */ export type Tokens = typeof tokens; diff --git a/src/utils/update-check.ts b/src/utils/update-check.ts index dce3697..4c903a7 100644 --- a/src/utils/update-check.ts +++ b/src/utils/update-check.ts @@ -1,18 +1,27 @@ import { APP_VERSION } from "../config/consts"; -const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours +/** How long a successful GitHub release lookup remains fresh. */ +const CACHE_TTL_MS = 24 * 60 * 60 * 1000; const REPO = "wethegit/wtc"; +/** Returns the update cache location, allowing tests to override the directory. */ function getCachePaths(): { cacheDir: string; cachePath: string } { const homeDir = Bun.env.HOME ?? process.env.HOME ?? "."; const cacheDir = process.env.WTC_CACHE_DIR ?? `${homeDir}/.cache/wtc`; return { cacheDir, cachePath: `${cacheDir}/update-check.json` }; } +/** Normalizes release tags so `v1.2.3` and `1.2.3` compare equally. */ function normalizeVersion(version: string): string { return version.trim().replace(/^v/, ""); } +/** + * Compares semantic-looking versions from GitHub release tags. + * + * Falls back to string comparison when either side contains non-numeric parts so + * pre-releases or unexpected tags remain deterministic instead of throwing. + */ function compareVersions(left: string, right: string): number { const leftParts = normalizeVersion(left).split(".").map(Number); const rightParts = normalizeVersion(right).split(".").map(Number); @@ -32,17 +41,25 @@ function compareVersions(left: string, right: string): number { return 0; } +/** Cached latest release lookup. */ interface UpdateCache { + /** Latest version tag returned by GitHub. */ latestVersion: string; + /** Unix timestamp in milliseconds for when the lookup was cached. */ checkedAt: number; } +/** Result returned by the update checker. */ export interface UpdateInfo { + /** Version currently running. */ currentVersion: string; + /** Latest known release version. */ latestVersion: string; + /** Whether `latestVersion` is newer than `currentVersion`. */ updateAvailable: boolean; } +/** Reads the cached release lookup, returning null when missing or invalid. */ async function readCache(): Promise { try { const { cachePath } = getCachePaths(); @@ -53,12 +70,14 @@ async function readCache(): Promise { } } +/** Writes the latest successful release lookup to the local cache. */ async function writeCache(cache: UpdateCache): Promise { const { cachePath } = getCachePaths(); await Bun.write(cachePath, JSON.stringify(cache)); } +/** Fetches the latest GitHub release tag for the WTC repository. */ async function fetchLatestVersion(): Promise { const response = await fetch(`https://api.github.com/repos/${REPO}/releases/latest`, { headers: { "User-Agent": "wtc" }, @@ -73,15 +92,23 @@ async function fetchLatestVersion(): Promise { return data.tag_name; } -export async function checkForUpdate(): Promise { +/** + * Checks whether a newer WTC release exists. + * + * The checker prefers cached data for fast startup, refreshes from GitHub when + * the cache is stale, and falls back to stale cache data if the network request + * fails. If no cache exists and GitHub is unavailable, it reports no update so + * startup remains quiet and non-blocking. + */ +export async function checkForUpdate(currentVersion = APP_VERSION): Promise { const cached = await readCache(); const now = Date.now(); if (cached && now - cached.checkedAt < CACHE_TTL_MS) { return { - currentVersion: APP_VERSION, + currentVersion, latestVersion: cached.latestVersion, - updateAvailable: compareVersions(cached.latestVersion, APP_VERSION) > 0, + updateAvailable: compareVersions(cached.latestVersion, currentVersion) > 0, }; } @@ -90,22 +117,22 @@ export async function checkForUpdate(): Promise { await writeCache({ latestVersion, checkedAt: now }); return { - currentVersion: APP_VERSION, + currentVersion, latestVersion, - updateAvailable: compareVersions(latestVersion, APP_VERSION) > 0, + updateAvailable: compareVersions(latestVersion, currentVersion) > 0, }; } catch { if (cached) { return { - currentVersion: APP_VERSION, + currentVersion, latestVersion: cached.latestVersion, - updateAvailable: compareVersions(cached.latestVersion, APP_VERSION) > 0, + updateAvailable: compareVersions(cached.latestVersion, currentVersion) > 0, }; } return { - currentVersion: APP_VERSION, - latestVersion: APP_VERSION, + currentVersion, + latestVersion: currentVersion, updateAvailable: false, }; } From c305409ca150954c04e23b656acfd35d8cfa4a5c Mon Sep 17 00:00:00 2001 From: Marlon Marcello Date: Fri, 12 Jun 2026 15:34:57 -0700 Subject: [PATCH 19/20] chore: wip notes --- src/config/crypto.ts | 6 ++++++ src/config/manager.ts | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/src/config/crypto.ts b/src/config/crypto.ts index 18bd806..c72ff10 100644 --- a/src/config/crypto.ts +++ b/src/config/crypto.ts @@ -1,3 +1,9 @@ +/** + * + * + * Work in progress + * This will store TUI secretes + */ import { randomBytes, createHash, createCipheriv, createDecipheriv } from "node:crypto"; const ALGORITHM = "aes-256-gcm"; diff --git a/src/config/manager.ts b/src/config/manager.ts index b5e97c6..6983e07 100644 --- a/src/config/manager.ts +++ b/src/config/manager.ts @@ -1,3 +1,11 @@ +/** + * + * + * + * Work in progress + * This will manage TUI config + * + */ import { mkdir, writeFile, readFile } from "node:fs/promises"; import { existsSync } from "node:fs"; import { homedir } from "node:os"; From 8bcbe76f5e2e60db3e5fda969ab0784d13761794 Mon Sep 17 00:00:00 2001 From: Marlon Marcello Date: Fri, 12 Jun 2026 15:38:21 -0700 Subject: [PATCH 20/20] chore: clean up plan --- plans/SOLID_TUI_REFACTOR.md | 157 +----------------------------------- 1 file changed, 1 insertion(+), 156 deletions(-) diff --git a/plans/SOLID_TUI_REFACTOR.md b/plans/SOLID_TUI_REFACTOR.md index 332426c..0224880 100644 --- a/plans/SOLID_TUI_REFACTOR.md +++ b/plans/SOLID_TUI_REFACTOR.md @@ -3,7 +3,7 @@ ## Status - **Started**: — -- **Branch**: `chore/solid-tui-refactor` (suggested) +- **Branch**: `feature/solid-js-integration` (suggested) - **TUI Stack**: `@opentui/solid` + `solid-js` + `@opentui/keymap` --- @@ -121,42 +121,10 @@ export const tokens = { ### Design Direction -| Role | Color | Notes | -| --------------- | ------------------- | ------------------------------ | -| Background | `#101820` (black) | Dark base | -| Surface | `#5e5f61` (black75) | Cards, dialog panels | -| Primary text | `#ffffff` (white) | High contrast | -| Dim text | `#939497` (black50) | Labels, hints, version strings | -| Accent / focus | `#2daccc` (teal75) | Borders, highlights, CTAs | -| Accent soft | `#9ad9e9` (teal50) | Selection background | -| Callout / brand | `#fc6f83` (pink50) | Titles, warnings, personality | -| Success | `#8dc975` (green) | Confirmations | -| Warning | `#f8ea36` (yellow) | Attention states | - No ASCII art logos unless they are genuinely brand-relevant. The WTC tiny-font logo from the MVP can stay as a lightweight branding element on the dashboard. --- -## File Structure Changes - -``` -src/tui/ -├── app.tsx # Solid root — renderer creation, keymap init, providers -├── tokens.ts # Palette + semantic tokens (expand existing) -├── components/ -│ ├── dialog.tsx # DialogProvider + useDialog + base Dialog overlay -│ ├── dialog-alert.tsx # Alert dialog (simplified from OpenCode pattern) -│ ├── update-dialog.tsx# Update available notification dialog -│ ├── status-bar.tsx # Bottom bar showing active hotkeys -│ └── command-palette.tsx # ctrl/cmd+p overlay command list -└── pages/ - ├── dashboard.tsx # Intro screen with WTC logo - ├── github.tsx # Placeholder GitHub page - └── settings.tsx # Placeholder Settings page -``` - ---- - ## Migration Steps ### Step 1 — Add Dependencies and Config @@ -277,22 +245,6 @@ File: `src/tui/pages/dashboard.tsx` Replace `createDashboard()` with an intro-only screen. Do not include a navigation ` - {/* filtered command list */} - - ) -} -``` - -The palette is rendered through `DialogProvider` via `dialog.replace()`. - -Initial commands: - -- `Open GitHub` → route to `github` -- `Open Settings` → route to `settings` - -### Step 10 — Rewrite App Root - -File: `src/tui/app.tsx` - -```tsx -import { render } from "@opentui/solid"; -import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"; -import { KeymapProvider, useBindings } from "@opentui/keymap/solid"; -import { DialogProvider } from "./components/dialog"; -import { StatusBar } from "./components/status-bar"; -import { Dashboard } from "./pages/dashboard"; -import { UpdateDialog } from "./components/update-dialog"; -import { checkForUpdate } from "../utils/update-check"; -import { onMount } from "solid-js"; -import { APP_VERSION } from "../version"; - -const REPO = "wethegit/wtc"; - -export async function launchDashboard(version = APP_VERSION): Promise { - const renderer = await createCliRenderer({ exitOnCtrlC: true, backgroundColor: tokens.bg }); - const keymap = createDefaultOpenTuiKeymap(renderer); - - function Root() { - const dialog = useDialog(); - - useBindings(() => ({ - bindings: [ - { key: "mod+p", cmd: "command-palette.show", desc: "Command palette" }, - { key: "ctrl+p", cmd: "command-palette.show", desc: "Command palette" }, - { key: "q", cmd: "quit", run: () => renderer.destroy() }, - ], - })); - - onMount(() => { - checkForUpdate(version).then((info) => { - if (info.updateAvailable) { - dialog.replace(() => ( - - )); - } - }); - }); - - return ( - - - - - ); - } - - await render( - () => ( - - - - - - ), - renderer, - ); -} -``` - --- ## Testing Strategy