Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ The system is intentionally minimal and bg-only. Chrome recedes; terminals are t

The system explicitly rejects: rounded SaaS cards, gradient accents, hacker-aesthetic green-on-black, "Slack-style" Electron chrome bloat, decorative animations, and any token that hardcodes a color. If a user installs a high-contrast theme, the chrome can look flatter than usual: that is accepted, not "fixed" with overrides.

**Scope.** This design system governs every product surface: the lib components, the VS Code webview, the standalone app, and the server-served Pocket app — auth screens included (`docs/specs/pocket-app.md`). The marketing website keeps its own separate "homepage" design system (`website/src/index.css`: Ubuntu Mono / Ubuntu Sans Mono, a fixed dark palette, the caramel accent), scoped to marketing pages only. The two share nothing except lib components embedded in the site's playgrounds, which bring this system's tokens with them; product surfaces never consume homepage tokens, and marketing chrome never consumes `--vscode-*`.

**Key Characteristics:**
- Host-theme-driven palette: every color is a `var(--vscode-*)` passthrough.
- Bg-only chrome: no decorative borders, no resting shadows, no accent stripes.
Expand Down
48 changes: 48 additions & 0 deletions docs/specs/pocket-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,51 @@ default, matching the homepage brand.
The server (`server/`) stays the only dynamic code: accounts, relay, and
static serving of the built Pocket bundle.

## Design system and theming

Pocket is a product surface, not a marketing page: all of it — the auth
screens included — renders on the shared themeable design system
(`--color-*` tokens over `--vscode-*`; [theme.md](./theme.md), `DESIGN.md`).
The website's separate "homepage" design system (`website/src/index.css`) is
never used by Pocket or anything else the server serves. There is no
Pocket-specific palette; changing the theme re-skins the auth screens and
the wall together.

Pocket has no VS Code host and boots into auth long before a Wall exists, so
the app restores the theme itself before first paint: `main.tsx` calls
`restorePocketTheme()` (`lib/src/remote/pocket-app/pocket-theme.ts`) before
the first render, with the Kimbie Dark default; `PocketWall` repeats it
idempotently via `usePocketTheme()` for isolated consumers (stories). The
default is one shared `POCKET_THEME_ID` constant that the website playground
imports, so the playground cannot drift from the real Pocket default.
Restoring also syncs document-level browser chrome that in-app hosts don't
need: `color-scheme` on the root element (native form controls, scrollbars)
and the `<meta name="theme-color">` address-bar tint, taken from the applied
theme's type and resolved `sideBar.background`. The static meta values in
`lib/pocket/index.html` are pre-boot placeholders only.

Phone-specific exceptions to the desktop chrome scale (`DESIGN.md`'s
Two-Step Rule), kept deliberately narrow:

* Form inputs use 16px text: smaller input text triggers iOS zoom-on-focus,
and 10–12px inputs are illegible at thumb distance.
* Chrome type runs a step larger than desktop (13px body, 11–12px
secondary), and touch targets are taller: 44px block actions, 36px row
actions.

The chrome itself follows theme.md's three-pair rule: the page is the app
pair, the header band is the active-header pair (the "titlebar", doubling as
the primary-action tone), and host rows are the inactive-header pair.
Secondary text is alpha on the owning pair's foreground, and presence is
intensity — an offline row drops to `opacity-55`; there is no online badge,
no border, no `surface-raised`, no `muted`. The one status color is
`text-error`, delineated by a red inset hairline for the error notice
(panel-border is transparent in many themes). Source of truth:
`lib/src/remote/pocket-app/App.tsx` (views + the `pkButton`/`PK`
vocabulary), `lib/src/remote/pocket-app/pocket-theme.ts` (theme boot +
browser-chrome sync), `lib/pocket/index.html` (structural viewport rules +
pre-boot color fallbacks).

## Deployment: same-origin, always

WebAuthn binds passkeys to the serving origin, and Chrome's Private Network
Expand All @@ -100,3 +145,6 @@ code.
`MobileTerminalUi` + `MobileWall` independently.
2. **CloudFlare routing** — the SaaS deployment above; deferred until SaaS.
Nothing in the shipped architecture needs rework for it.
3. **Theme picker in Pocket** — the app restores the persisted theme but
exposes no picker; add the shared `ThemePicker` (and its theme-debugger
entry) once its dropdown is phone-friendly.
5 changes: 5 additions & 0 deletions docs/specs/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,11 @@ Served by the server, built from `lib`:
straight away.
* Picker renders `directory.snapshot`; tapping a pane attaches with the
phone's cols/rows and reuses the existing mobile terminal UI (xterm).
* All of it — auth screens included — renders on the shared themeable design
system, with the theme restored before first paint
(`docs/specs/pocket-app.md`, Design system and theming). The server serves
no styling of its own; its only self-authored output is the plaintext
missing-build stub.

## Testing

Expand Down
7 changes: 6 additions & 1 deletion docs/specs/theme.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ theme-appropriate xterm.js colors.

VSCode extension mode gets `--vscode-*` variables from VSCode. Standalone and
website mode apply the same shape of variables to `document.body` with
`applyTheme()` from a bundled or installed Dormouse theme. Both paths run the
`applyTheme()` from a bundled or installed Dormouse theme. The Pocket app is
a fourth host: it restores the persisted/bundled theme with
`restoreActiveTheme()` before its first paint — auth screens included — and
additionally syncs document-level browser chrome (`color-scheme`,
`meta[name="theme-color"]`); see
[pocket-app.md](./pocket-app.md#design-system-and-theming). All paths run the
same consumed-token resolver from `lib/src/lib/themes/vscode-color-resolver.ts`
so omitted theme JSON keys behave like VSCode registry defaults before
Dormouse renders.
Expand Down
9 changes: 6 additions & 3 deletions lib/pocket/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover, maximum-scale=1.0, user-scalable=no"
/>
<meta name="color-scheme" content="light dark" />
<!-- Pre-boot defaults for Kimbie Dark (the default theme); pocket-theme.ts
re-syncs both to the applied theme at runtime, before first paint. -->
<meta name="color-scheme" content="dark" />
<meta name="theme-color" content="#362712" />
<title>Dormouse Pocket</title>
<style>
/* Theme-agnostic viewport plumbing. Colors and fonts come from the shared
Expand All @@ -18,8 +21,8 @@
height: 100%;
-webkit-text-size-adjust: 100%;
overscroll-behavior: none;
background: var(--color-app-bg, #1e1e1e);
color: var(--color-app-fg, #e6e8ec);
background: var(--color-app-bg, #362712);
color: var(--color-app-fg, #d3af86);
}
#pocket-root {
height: 100dvh;
Expand Down
7 changes: 5 additions & 2 deletions lib/src/components/MobileTerminalUi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -325,16 +325,19 @@ function SessionsPane({
'flex min-h-10 min-w-0 items-center gap-2 rounded px-2 text-left font-mono text-xs transition-colors',
'focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-focus-ring',
'disabled:pointer-events-none disabled:opacity-60',
// Rows sit on the header-inactive reserve, so the inactive row
// recesses to the app pair — the guaranteed app↔inactive delta
// (theme.md's three-pair rule); surface-raised is unreliable here.
active
? 'bg-header-active-bg text-header-active-fg shadow-[inset_0_0_0_1px_var(--color-focus-ring)]'
: 'bg-surface-raised text-foreground hover:bg-header-inactive-bg',
: 'bg-app-bg text-app-fg',
)}
>
<TerminalWindowIcon size={15} weight={active ? 'bold' : 'regular'} className="shrink-0" />
<span className="min-w-0 flex-1">
<span className="block truncate font-medium">{session.title}</span>
{session.secondary ? (
<span className={clsx('block truncate', active ? 'opacity-70' : 'text-muted')}>{session.secondary}</span>
<span className="block truncate opacity-70">{session.secondary}</span>
) : null}
</span>
{session.todo ? (
Expand Down
63 changes: 38 additions & 25 deletions lib/src/remote/pocket-app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,10 @@
* `MobileTerminalUi`/`MobileWall` (the same composition the website playground
* proves out with `FakePtyAdapter`). No bespoke terminal UI.
*
* Chrome is built from the same three VSCode list pairs as the rest of the app
* (docs/specs/theme.md): the page is `app-bg/fg`, the header band is the
* *active* pair `header-active-bg/fg`, and host rows are the *inactive* pair
* `header-inactive-bg/fg`. Hierarchy is background swaps between those pairs —
* never `surface-raised` or `panel-border`, which are near-black / transparent
* in themes like Kimbie. Secondary emphasis is foreground *intensity* (alpha on
* the pair's own fg), so no fourth color is introduced. The theme is applied to
* <body> by `restorePocketTheme()` in `main.tsx` before first paint.
* The whole shell — auth screens included — renders on the shared `--vscode-*`
* design tokens, restored to <body> before first paint by restorePocketTheme()
* in main.tsx. Chrome draws only on the three list pairs — see the vocabulary
* below and docs/specs/theme.md.
*/

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
Expand Down Expand Up @@ -68,9 +64,9 @@ const pkButton = tv({
lg: 'min-h-[44px] px-4 text-[13px]',
sm: 'min-h-9 px-3 text-[12px]',
},
block: { true: 'w-full' },
block: { true: 'w-full', false: '' },
},
defaultVariants: { tone: 'primary', size: 'lg' },
defaultVariants: { tone: 'primary', size: 'lg', block: false },
});

const PK = {
Expand Down Expand Up @@ -255,17 +251,7 @@ export default function App(): React.ReactElement {

if (phase === 'wall' && activeHost && adapterRef.current) {
return (
<div className={PK.app}>
<header className={PK.header}>
<button type="button" className={pkButton({ tone: 'ghost', size: 'sm' })} onClick={leaveWall}>
‹ Hosts
</button>
<h1 className={PK.headerTitle}>{activeHost.label || activeHost.hostId}</h1>
</header>
<div className={PK.wallHost}>
<PocketWall adapter={adapterRef.current} />
</div>
</div>
<ConnectedView host={activeHost} adapter={adapterRef.current} onLeave={leaveWall} />
);
}

Expand All @@ -276,6 +262,33 @@ export default function App(): React.ReactElement {
);
}

// --- ConnectedView ---------------------------------------------------------

/** The connected Pocket shell: host navigation chrome over the remote wall. */
export function ConnectedView({
host,
adapter,
onLeave,
}: {
host: HostView;
adapter: RemotePtyAdapter;
onLeave: () => void;
}): React.ReactElement {
return (
<div className={PK.app}>
<header className={PK.header}>
<button type="button" className={pkButton({ tone: 'ghost', size: 'sm' })} onClick={onLeave}>
‹ Hosts
</button>
<h1 className={PK.headerTitle}>{host.label || host.hostId}</h1>
</header>
<div className={PK.wallHost}>
<PocketWall adapter={adapter} />
</div>
</div>
);
}

// --- SetupOrSignin ---------------------------------------------------------

export function SetupOrSignin({
Expand Down Expand Up @@ -330,9 +343,9 @@ export function SetupOrSignin({
password.
</p>
<div className={PK.field}>
<label className={PK.fieldLabel} htmlFor="pk-pw">Setup password</label>
<label className={PK.fieldLabel} htmlFor="pocket-setup-password">Setup password</label>
<input
id="pk-pw"
id="pocket-setup-password"
className={PK.input}
type="password"
autoComplete="off"
Expand All @@ -341,9 +354,9 @@ export function SetupOrSignin({
/>
</div>
<div className={PK.field}>
<label className={PK.fieldLabel} htmlFor="pk-label">Passkey label</label>
<label className={PK.fieldLabel} htmlFor="pocket-passkey-label">Passkey label</label>
<input
id="pk-label"
id="pocket-passkey-label"
className={PK.input}
type="text"
value={label}
Expand Down
7 changes: 4 additions & 3 deletions lib/src/remote/pocket-app/PocketWall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
import { getTerminalInstance, refitSession } from '../../lib/terminal-registry';
import { doPaste } from '../../lib/clipboard';
import type { RemotePtyAdapter } from '../client/remote-adapter';
import { usePocketTheme } from './pocket-theme';
import {
activatePane,
attachableDirectoryEntries,
Expand All @@ -40,9 +41,9 @@ import {
} from './wall-model';

export function PocketWall({ adapter }: { adapter: RemotePtyAdapter }): React.ReactElement {
// The theme is applied to <body> once at boot by restorePocketTheme() in
// main.tsx (applyTheme writes --vscode-* outside React, so a CSR mount never
// reconciles it away); the wall just reads those variables.
// App restores the theme before this renders; repeat idempotently so isolated
// PocketWall consumers receive the same theme contract too.
usePocketTheme();
const [entries, setEntries] = useState<DirectoryEntry[]>(() => adapter.getDirectoryEntries());
const [activePaneId, setActivePaneId] = useState<string | null>(null);
const [touchMode, setTouchMode] = useState<MobileTerminalTouchMode>('gestures');
Expand Down
50 changes: 34 additions & 16 deletions lib/src/remote/pocket-app/pocket-theme.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,41 @@
/**
* Pocket's theme bootstrap. The whole Pocket app — the auth/hosts chrome *and*
* the terminal wall — is themed by the shared VSCode-token system
* (docs/specs/theme.md): applyTheme() writes `--vscode-*` onto <body>, and
* theme.css maps those to the `--color-*` tokens both surfaces consume.
* Theme boot for the Pocket shell. The whole app — auth screens included —
* runs on the shared `--vscode-*` token system (lib/src/theme.css;
* docs/specs/theme.md), so the theme must be restored before first paint,
* before any `--vscode-*` vars exist on body.
*
* Kimbie Dark is the brand default — dormouse's homepage is a Kimbie Dark
* clone — so an app-surface signin screen that renders in Kimbie Dark is also
* on-brand. restoreActiveTheme() honors a user's persisted choice first, then
* falls back to this default.
* The theme restoration never runs at module import time on purpose: Storybook
* imports these modules and manages its own themes.
*/
import { restoreActiveTheme } from '../../lib/themes';

const POCKET_THEME_ID = 'vscode.theme-kimbie-dark.kimbie-dark';
import { useEffect, useLayoutEffect, useRef } from 'react';
import { getAppliedThemeSnapshot, restoreActiveTheme } from '../../lib/themes';

/** Same default theme the website playground restores, unless the user picked one. */
export const POCKET_THEME_ID = 'vscode.theme-kimbie-dark.kimbie-dark';

const useBrowserLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;

/**
* Apply the persisted-or-default theme to <body>. Call before first paint so
* the auth screens (not just the wall) render with `--vscode-*` / `--color-*`
* present. Idempotent.
*/
export function restorePocketTheme(): void {
restoreActiveTheme(POCKET_THEME_ID);
const theme = restoreActiveTheme(POCKET_THEME_ID);
if (!theme || typeof document === 'undefined') return;
// Browser chrome outside the body: form-control palette and the
// address-bar / status-bar tint follow the applied theme.
document.documentElement.style.colorScheme = theme.type;
const appBg = getAppliedThemeSnapshot()?.resolvedVars['--vscode-sideBar-background'];
const meta = document.querySelector('meta[name="theme-color"]');
if (appBg && meta) meta.setAttribute('content', appBg);
}

export function usePocketTheme() {
const restoredRef = useRef(false);
if (!restoredRef.current) {
restorePocketTheme();
restoredRef.current = true;
}
// Repeat after hydration so the wall reads real theme variables even if
// React reconciled away render-time body styles.
useBrowserLayoutEffect(() => {
restorePocketTheme();
}, []);
}
Loading
Loading