Skip to content
Merged
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
3 changes: 3 additions & 0 deletions lib/.storybook/tauri-window-mock.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
const mockWindow = {
isMaximized: () => Promise.resolve(false),
onResized: (_callback: () => void) => Promise.resolve(() => {}),
isFocused: () => Promise.resolve(true),
onFocusChanged: (_callback: (event: { payload: boolean }) => void) =>
Promise.resolve(() => {}),
minimize: () => Promise.resolve(),
toggleMaximize: () => Promise.resolve(),
close: () => Promise.resolve(),
Expand Down
13 changes: 8 additions & 5 deletions lib/src/components/Baseboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CaretLeftIcon, CaretRightIcon } from '@phosphor-icons/react';
import { Door } from './Door';
import { DoorElementsContext } from './wall/wall-context';
import type { DooredItem } from './wall/wall-types';
import { IS_MAC } from '../lib/platform';
import { DEFAULT_ACTIVITY_STATE, getActivitySnapshot, subscribeToActivity } from '../lib/terminal-registry';

export interface BaseboardProps {
Expand Down Expand Up @@ -60,7 +61,9 @@ export function Baseboard({ items, onReattach, notice }: BaseboardProps) {
}, [itemKey]);

// Keyboard shortcut hint — only show when there's enough space and no doors
const shortcutHint = 'LCmd → RCmd to enter command mode';
const shortcutHint = IS_MAC
? 'LCmd → RCmd to enter command mode'
: 'LShift → RShift to enter command mode';
const showHint = items.length === 0 && containerWidth > 350;

// Calculate which doors fit
Expand Down Expand Up @@ -148,19 +151,19 @@ export function Baseboard({ items, onReattach, notice }: BaseboardProps) {
);
})}
</div>
<button ref={arrowMeasureEl} className="absolute -left-[9999px] flex h-5 shrink-0 items-center gap-1 rounded px-1.5 pb-px text-sm font-medium font-mono tracking-[0.06em] text-muted" aria-hidden tabIndex={-1}>
<button ref={arrowMeasureEl} className="absolute -left-[9999px] flex h-5 shrink-0 items-center gap-1 rounded px-1.5 pb-px text-sm font-medium font-mono text-muted" aria-hidden tabIndex={-1}>
9 more <CaretRightIcon size={10} weight="bold" />
</button>

{items.length === 0 && showHint && (
<span className="truncate pb-1 text-sm font-mono tracking-[0.06em] text-muted">
<span className="truncate pb-1 text-sm font-mono text-muted">
{shortcutHint}
</span>
)}

{hiddenLeft > 0 && (
<button
className="flex h-5 shrink-0 items-center gap-1 rounded px-1.5 pb-px text-sm font-medium font-mono tracking-[0.06em] text-muted transition-colors hover:bg-surface-raised hover:text-foreground"
className="flex h-5 shrink-0 items-center gap-1 rounded px-1.5 pb-px text-sm font-medium font-mono text-muted transition-colors hover:bg-surface-raised hover:text-foreground"
onClick={scrollLeft}
>
<CaretLeftIcon size={10} weight="bold" />
Expand All @@ -184,7 +187,7 @@ export function Baseboard({ items, onReattach, notice }: BaseboardProps) {

{hiddenRight > 0 && (
<button
className="ml-auto flex h-5 shrink-0 items-center gap-1 rounded px-1.5 pb-px text-sm font-medium font-mono tracking-[0.06em] text-muted transition-colors hover:bg-surface-raised hover:text-foreground"
className="ml-auto flex h-5 shrink-0 items-center gap-1 rounded px-1.5 pb-px text-sm font-medium font-mono text-muted transition-colors hover:bg-surface-raised hover:text-foreground"
onClick={scrollRight}
>
{hiddenRight} more
Expand Down
6 changes: 3 additions & 3 deletions lib/src/components/Door.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { BellIcon } from '@phosphor-icons/react';
import type { SessionStatus, TodoState } from '../lib/terminal-registry';
import { useTodoPillContent } from './TodoPillBody';
import { bellIconClass } from './bell-icon-class';
import { TERMINAL_TOP_RADIUS_CLASS } from './design';
import { TERMINAL_TOP_RADIUS_CLASS, TODO_PILL_TRACKING_CLASS } from './design';

export interface DoorProps {
doorId?: string;
Expand Down Expand Up @@ -30,7 +30,7 @@ export function Door({
'relative flex h-6 max-w-[220px] min-w-[68px] items-center gap-2 overflow-hidden px-2.5',
TERMINAL_TOP_RADIUS_CLASS,
'bg-door-bg text-door-fg',
'text-sm font-medium font-mono tracking-[0.02em]',
'text-sm font-medium font-mono',
].join(' ')}
onClick={onClick}
title={title}
Expand All @@ -42,7 +42,7 @@ export function Door({
<span className="flex shrink-0 items-center gap-1.5">
{todoPill.visible && (
<span
className="todo-pill-shell text-xs font-semibold tracking-[0.08em]"
className={`todo-pill-shell text-xs font-semibold ${TODO_PILL_TRACKING_CLASS}`}
data-flourishing={todoPill.flourishing ? 'true' : 'false'}
>
{todoPill.body}
Expand Down
3 changes: 2 additions & 1 deletion lib/src/components/ThemePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ThemeSwatch } from './theme-picker/ThemeSwatch';
import { ThemeStoreDialog } from './theme-picker/ThemeStoreDialog';
import { useCloseOnOutsideAndEscape } from './theme-picker/use-close-on-outside';
import { themePickerStyles as styles } from './theme-picker/styles';
import { chromeButton } from './design';

export type ThemePickerVariant = 'playground-header' | 'standalone-appbar';

Expand Down Expand Up @@ -84,7 +85,7 @@ export function ThemePicker({ variant, className = '', defaultThemeId }: ThemePi
: 'relative flex items-center';
const triggerClass = isPlayground
? 'flex w-[116px] min-w-0 cursor-pointer items-baseline justify-end gap-1.5 rounded-md bg-[var(--color-header-inactive-bg)] text-right text-sm text-[var(--color-header-inactive-fg)] sm:w-40 md:w-56'
: 'flex h-6 cursor-pointer items-center gap-1 rounded px-2 text-xs text-muted transition-colors hover:bg-surface-raised hover:text-foreground';
: chromeButton({ kind: 'labeled' });
const menuClass = isPlayground
? 'fixed top-16 right-4 left-4 z-50 overflow-hidden rounded border font-mono shadow-2xl md:absolute md:top-full md:right-0 md:left-auto md:mt-2 md:w-[22rem]'
: 'absolute right-0 top-full z-50 mt-1 w-[280px] overflow-hidden rounded border font-mono shadow-2xl';
Expand Down
24 changes: 24 additions & 0 deletions lib/src/components/design.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export const TERMINAL_BOTTOM_RADIUS_CLASS = 'rounded-b-lg';
export const TERMINAL_SELECTION_BORDER_RADIUS = `${TERMINAL_BORDER_RADIUS_REM}rem`;
export const DOOR_SELECTION_BORDER_RADIUS = `${TERMINAL_BORDER_RADIUS_REM}rem ${TERMINAL_BORDER_RADIUS_REM}rem 0 0`;

// Letter-spacing for the small semibold TODO pill — wider tracking keeps the
// tiny label legible. Shared so both pill sites stay in sync.
export const TODO_PILL_TRACKING_CLASS = 'tracking-[0.08em]';

export function PopupButtonRow({
className,
...props
Expand Down Expand Up @@ -50,6 +54,26 @@ export const popupButton = tv({

export type PopupButtonVariants = VariantProps<typeof popupButton>;

// Chrome buttons: icon-only and labeled triggers used in the standalone app
// bar, plus the Windows/Linux native-style window controls. All inherit text
// color from the surrounding chrome so they tint with the active/inactive
// header palette — except `windowClose`, whose hover red matches the native
// OS close button across themes.
export const chromeButton = tv({
base: 'flex items-center transition-colors',
variants: {
kind: {
icon: 'h-5 min-w-5 justify-center rounded hover:bg-current/10',
labeled: 'h-5 min-w-5 gap-1 rounded px-1.5 text-xs text-inherit hover:bg-current/10',
window: 'w-11 justify-center text-inherit hover:bg-current/10',
windowClose: 'w-11 justify-center text-inherit hover:bg-[#b92a1b] hover:text-white',
},
},
defaultVariants: { kind: 'icon' },
});

export type ChromeButtonVariants = VariantProps<typeof chromeButton>;

/** Keyboard shortcut rendered as `[keys]` in muted color. Use everywhere key
* bindings appear in UI text so the bracket convention is consistent. */
export function Shortcut({
Expand Down
8 changes: 4 additions & 4 deletions lib/src/components/wall/TerminalPaneHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
} from '@phosphor-icons/react';
import { HeaderActionButton } from '../HeaderActionButton';
import { TodoAlertDialog } from '../TodoAlertDialog';
import { TERMINAL_TOP_RADIUS_CLASS } from '../design';
import { TERMINAL_TOP_RADIUS_CLASS, TODO_PILL_TRACKING_CLASS } from '../design';
import { bellIconClass } from '../bell-icon-class';
import { useTodoPillContent } from '../TodoPillBody';
import {
Expand Down Expand Up @@ -43,7 +43,7 @@ import {
import { MouseOverrideBanner } from './MouseOverrideBanner';

const tabVariant = tv({
base: `flex h-full w-full cursor-grab items-center gap-1.5 ${TERMINAL_TOP_RADIUS_CLASS} pl-2 pr-[5px] text-sm leading-none font-mono tracking-normal select-none active:cursor-grabbing`,
base: `flex h-full w-full cursor-grab items-center gap-1.5 ${TERMINAL_TOP_RADIUS_CLASS} pl-2 pr-[5px] text-sm leading-none font-mono select-none active:cursor-grabbing`,
variants: {
state: {
active: 'bg-header-active-bg text-header-active-fg',
Expand Down Expand Up @@ -127,7 +127,7 @@ export function TerminalPaneHeader({ api }: IDockviewPanelHeaderProps) {
<div className="flex flex-1 min-w-0 items-center gap-2">
{isRenaming ? (
<input
className="bg-transparent outline-none border-none text-inherit font-medium font-mono tracking-normal w-full min-w-0 p-0 m-0"
className="bg-transparent outline-none border-none text-inherit font-medium font-mono w-full min-w-0 p-0 m-0"
defaultValue={api.title}
autoFocus
ref={(el) => el?.select()}
Expand Down Expand Up @@ -194,7 +194,7 @@ export function TerminalPaneHeader({ api }: IDockviewPanelHeaderProps) {
type="button"
data-session-todo-for={api.id}
data-flourishing={todoPill.flourishing ? 'true' : 'false'}
className="todo-pill-shell shrink-0 rounded border border-current px-1.5 py-px text-xs font-semibold tracking-[0.08em] transition-colors hover:bg-current/10"
className={`todo-pill-shell shrink-0 rounded border border-current px-1.5 py-px text-xs font-semibold ${TODO_PILL_TRACKING_CLASS} transition-colors hover:bg-current/10`}
aria-label="Dismiss TODO"
aria-hidden={todoPill.flourishing ? true : undefined}
onMouseDown={(e) => e.stopPropagation()}
Expand Down
5 changes: 5 additions & 0 deletions standalone/src-tauri/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ fn main() {
println!("cargo:rerun-if-env-changed=NODE_BINARY");
println!("cargo:rerun-if-env-changed=PATH");

// tauri-build doesn't expand bundle.resources globs into rerun-if-changed
// entries, so edits to ../sidecar/*.js wouldn't rerun this script and the
// staged copy under target/<profile>/_up_/sidecar/ would go stale.
println!("cargo:rerun-if-changed=../sidecar");

bundle_node_runtime().expect("failed to prepare bundled Node.js runtime");
tauri_build::build()
}
Expand Down
38 changes: 27 additions & 11 deletions standalone/src/AppBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { useState, useEffect, useRef, useCallback } from 'react';
import { getCurrentWindow } from '@tauri-apps/api/window';
import { CaretDownIcon, MinusIcon, CornersOutIcon, CornersInIcon, XIcon, PlusIcon, CheckIcon } from '@phosphor-icons/react';
import { ThemePicker } from '../../lib/src/components/ThemePicker';
import { PopupButtonRow } from '../../lib/src/components/design';
import { PopupButtonRow, chromeButton } from '../../lib/src/components/design';
import { setDefaultShellOpts } from '../../lib/src/lib/shell-defaults';
import { IS_MAC } from '../../lib/src/lib/platform';

export interface ShellEntry {
name: string;
Expand All @@ -15,11 +16,20 @@ interface AppBarProps {
shells: ShellEntry[];
}

const IS_MAC = typeof (navigator as any).userAgentData?.platform === 'string'
? (navigator as any).userAgentData.platform === 'macOS'
: /Mac/.test(navigator.platform);
const appWindow = getCurrentWindow();

function useAppWindowFocused(): boolean {
const [focused, setFocused] = useState(() => document.hasFocus());

useEffect(() => {
appWindow.isFocused().then(setFocused);
const unlisten = appWindow.onFocusChanged(({ payload }) => setFocused(payload));
return () => { unlisten.then(fn => fn()); };
}, []);

return focused;
}

// ── Tooltip wrapper ────────────────────────────────────────────────────────

function Tip({ label, children }: { label: string; children: React.ReactNode }) {
Expand Down Expand Up @@ -52,7 +62,7 @@ function WinControls() {
<div className="flex items-stretch self-stretch">
<Tip label="Minimize">
<button
className="flex w-11 items-center justify-center text-muted transition-colors hover:bg-surface-raised hover:text-foreground"
className={chromeButton({ kind: 'window' })}
onClick={() => appWindow.minimize()}
aria-label="Minimize"
>
Expand All @@ -61,7 +71,7 @@ function WinControls() {
</Tip>
<Tip label={maximized ? 'Restore' : 'Maximize'}>
<button
className="flex w-11 items-center justify-center text-muted transition-colors hover:bg-surface-raised hover:text-foreground"
className={chromeButton({ kind: 'window' })}
onClick={() => { appWindow.toggleMaximize(); }}
aria-label={maximized ? 'Restore' : 'Maximize'}
>
Expand All @@ -72,7 +82,7 @@ function WinControls() {
</Tip>
<Tip label="Close">
<button
className="flex w-11 items-center justify-center text-muted transition-colors hover:bg-error/90 hover:text-white"
className={chromeButton({ kind: 'windowClose' })}
onClick={() => appWindow.close()}
aria-label="Close"
>
Expand Down Expand Up @@ -124,7 +134,7 @@ function ShellDropdown({ shells }: { shells: ShellEntry[] }) {
{/* Primary action: [+] spawns a new terminal with the selected shell */}
<Tip label={`New ${selected?.name ?? 'terminal'}`}>
<button
className="flex h-6 items-center rounded-l px-1.5 text-xs text-muted transition-colors hover:bg-surface-raised hover:text-foreground"
className={chromeButton({ kind: 'icon' })}
onClick={() => selected && spawn(selected)}
aria-label={`New ${selected?.name ?? 'terminal'}`}
>
Expand All @@ -134,7 +144,7 @@ function ShellDropdown({ shells }: { shells: ShellEntry[] }) {
{/* Selector: shows current shell name + caret; click to choose a different shell */}
<Tip label="Choose shell">
<button
className="flex h-6 items-center gap-1 rounded-r px-2 text-xs text-muted transition-colors hover:bg-surface-raised hover:text-foreground"
className={chromeButton({ kind: 'labeled' })}
onClick={() => setOpen(!open)}
aria-expanded={open}
aria-haspopup="menu"
Expand All @@ -155,7 +165,7 @@ function ShellDropdown({ shells }: { shells: ShellEntry[] }) {
key={shell.name}
role="menuitemradio"
aria-checked={isSelected}
className="flex w-full items-center gap-2 whitespace-nowrap px-3 py-1.5 text-left text-xs text-foreground transition-colors hover:bg-surface-raised"
className="flex w-full items-center gap-2 whitespace-nowrap px-3 py-1.5 text-left text-sm text-foreground transition-colors hover:bg-surface-raised"
onClick={() => {
setSelected(shell);
setOpen(false);
Expand All @@ -177,10 +187,16 @@ function ShellDropdown({ shells }: { shells: ShellEntry[] }) {
// ── AppBar ─────────────────────────────────────────────────────────────────

export function AppBar({ shells }: AppBarProps) {
const windowFocused = useAppWindowFocused();

return (
<div
data-tauri-drag-region
className={`flex h-[30px] shrink-0 select-none items-center border-b border-border bg-app-bg text-app-fg text-xs ${
className={`flex h-[30px] shrink-0 select-none items-center text-xs ${
windowFocused
? 'bg-header-active-bg text-header-active-fg'
: 'bg-header-inactive-bg text-header-inactive-fg'
} ${
IS_MAC ? 'pl-[78px]' : ''
}`}
>
Expand Down
2 changes: 1 addition & 1 deletion standalone/src/UpdateBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function UpdateBanner({ state, onDismiss, onOpenChangelog }: UpdateBanner
}

return (
<span className="flex items-center gap-1.5 pb-1 text-[9px] font-mono tracking-[0.06em] text-muted">
<span className="flex items-center gap-1.5 pb-1 text-sm font-mono text-muted">
<span className="truncate">{message}</span>
{showChangelog && (
<button
Expand Down
Loading