diff --git a/src/App.tsx b/src/App.tsx index 25a71be..f3aff56 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -52,6 +52,7 @@ import Gutter from "./lib/layout/Gutter"; import Notifications, { type Toast } from "./components/Notifications"; import Palette from "./components/Palette"; import HostManager from "./components/HostManager"; +import Help from "./components/Help"; import "./App.css"; import "./lib/layout/Gutter.css"; @@ -84,6 +85,7 @@ export default function App() { }); const [hosts, setHosts] = useState([]); const [hostManagerOpen, setHostManagerOpen] = useState(false); + const [helpOpen, setHelpOpen] = useState(false); const [ready, setReady] = useState(false); const [notifications, setNotifications] = useState([]); const [paletteOpen, setPaletteOpen] = useState(false); @@ -353,6 +355,14 @@ export default function App() { const key = e.key.toLowerCase(); const { activeLeafId, tree } = kbdStateRef.current; + // F1 — toggle help overlay + if (key === "f1") { + e.preventDefault(); + e.stopPropagation(); + setHelpOpen((v) => !v); + return; + } + // Ctrl+K — palette if (ctrl && !shift && !alt && key === "k") { e.preventDefault(); @@ -713,6 +723,14 @@ export default function App() { > 🔔 + {leafCount(tree)} pane{leafCount(tree) === 1 ? "" : "s"} @@ -774,6 +792,8 @@ export default function App() { onClose={closeHostManager} /> )} + + {helpOpen && setHelpOpen(false)} />} ); } diff --git a/src/components/Help.css b/src/components/Help.css new file mode 100644 index 0000000..50475d1 --- /dev/null +++ b/src/components/Help.css @@ -0,0 +1,132 @@ +.help { + position: fixed; + top: 8vh; + left: 50%; + transform: translateX(-50%); + width: min(720px, 92vw); + max-height: 84vh; + background: #161616; + color: #ccc; + border: 1px solid #2a2a2a; + border-radius: 8px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6); + z-index: 100; + display: flex; + flex-direction: column; + overflow: hidden; + font-family: "Cascadia Mono", "JetBrains Mono", "Consolas", monospace; +} + +.help-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 14px; + border-bottom: 1px solid #2a2a2a; + flex-shrink: 0; +} +.help-title { + font-weight: 600; + font-size: 13px; +} +.help-close { + background: transparent; + border: none; + color: #888; + font-size: 18px; + line-height: 1; + padding: 2px 8px; + cursor: pointer; + border-radius: 3px; +} +.help-close:hover { + background: #2a2a2a; + color: #ddd; +} + +.help-body { + padding: 14px 18px; + overflow-y: auto; + font-size: 12px; +} + +.help-body h3 { + margin: 18px 0 6px; + font-size: 13px; + color: #e6e6e6; + font-weight: 600; +} +.help-body h3:first-child { + margin-top: 0; +} + +.help-section { + margin-bottom: 10px; +} +.help-section h4 { + margin: 8px 0 4px; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #888; + font-weight: 500; +} + +.help-shortcuts { + width: 100%; + border-collapse: collapse; +} +.help-shortcuts td { + padding: 3px 4px; + vertical-align: top; +} +.help-shortcuts td.keys { + white-space: nowrap; + width: 260px; + padding-right: 12px; +} +.help-shortcuts td.desc { + color: #aaa; + line-height: 1.4; +} +.help-shortcuts kbd { + font-family: inherit; + font-size: 11px; + background: #222; + color: #cce6ff; + border: 1px solid #2a2a3a; + border-radius: 3px; + padding: 1px 6px; + white-space: nowrap; +} + +.help-tips { + list-style: none; + padding: 0; + margin: 4px 0 0; + display: flex; + flex-direction: column; + gap: 6px; +} +.help-tips li { + padding: 7px 10px; + background: #1c1c1c; + border: 1px solid #2a2a2a; + border-radius: 4px; + color: #aaa; + font-size: 11px; + line-height: 1.45; +} +.help-tips strong { + color: #e6e6e6; + font-weight: 600; +} + +.help-footer { + margin: 18px 0 0; + padding-top: 10px; + border-top: 1px solid #2a2a2a; + color: #666; + font-size: 11px; + line-height: 1.45; +} diff --git a/src/components/Help.tsx b/src/components/Help.tsx new file mode 100644 index 0000000..0e41779 --- /dev/null +++ b/src/components/Help.tsx @@ -0,0 +1,78 @@ +import { useEffect } from "react"; +import { SHORTCUT_SECTIONS, TIPS } from "../lib/shortcuts"; +import "./Help.css"; + +interface HelpProps { + onClose: () => void; +} + +export default function Help({ onClose }: HelpProps) { + useEffect(() => { + function onKey(e: KeyboardEvent) { + if (e.key === "Escape") { + e.preventDefault(); + onClose(); + } + } + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [onClose]); + + return ( + <> + + +
+
+ tiletopia — help + +
+
+

Keyboard shortcuts

+ {SHORTCUT_SECTIONS.map((section) => ( +
+

{section.title}

+ + + {section.items.map((item) => ( + + + + + ))} + +
+ {item.keys} + {item.description}
+
+ ))} + +

Tips

+
    + {TIPS.map((tip) => ( +
  • + {tip.title}. {tip.body} +
  • + ))} +
+ +

+ Shortcuts work while a terminal is focused — they capture the key + before xterm.js sees it. They don't fire while you're typing into + a label edit or the palette input. +

+
+
+ + ); +} diff --git a/src/lib/shortcuts.ts b/src/lib/shortcuts.ts new file mode 100644 index 0000000..1543457 --- /dev/null +++ b/src/lib/shortcuts.ts @@ -0,0 +1,111 @@ +/** + * Single source of truth for the keyboard shortcuts and inline tips shown + * in the help overlay. README has a hand-maintained shortcut table that + * mirrors this — keep them in sync until/unless we generate one from the + * other. + */ + +export interface ShortcutSpec { + /** Display string for the key combo, e.g. "Ctrl+Shift+E". */ + keys: string; + description: string; +} + +export interface ShortcutSection { + title: string; + items: ShortcutSpec[]; +} + +export const SHORTCUT_SECTIONS: ShortcutSection[] = [ + { + title: "Layout", + items: [ + { keys: "Ctrl+Shift+E", description: "Split active pane to the right" }, + { keys: "Ctrl+Shift+O", description: "Split active pane downward" }, + { keys: "Ctrl+Shift+W", description: "Close active pane" }, + { + keys: "Ctrl+Shift+P", + description: + "Promote active pane out one level (turns a nested pane into a full row/column; self-inverse)", + }, + ], + }, + { + title: "Navigation", + items: [ + { keys: "Ctrl+K", description: "Open jump-to-pane palette" }, + { + keys: "Ctrl+Shift+← / → / ↑ / ↓", + description: "Focus neighbour pane in that direction", + }, + ], + }, + { + title: "Broadcast", + items: [ + { keys: "Ctrl+Shift+B", description: "Toggle broadcast on active pane" }, + { + keys: "Ctrl+Shift+Alt+B", + description: "Toggle broadcast on ALL panes (same as titlebar 📡)", + }, + ], + }, + { + title: "Font size", + items: [ + { + keys: "Ctrl+= / Ctrl+- / Ctrl+0", + description: "Zoom active pane in / out / reset", + }, + { + keys: "Ctrl+Shift+= / Ctrl+Shift+- / Ctrl+Shift+0", + description: "Same, applied to every pane", + }, + ], + }, + { + title: "Terminal", + items: [ + { + keys: "Ctrl+Shift+C / Ctrl+Shift+V", + description: "Copy selection / paste in terminal", + }, + ], + }, + { + title: "Help", + items: [{ keys: "F1", description: "Show this help overlay" }], + }, +]; + +export interface TipSpec { + title: string; + body: string; +} + +export const TIPS: TipSpec[] = [ + { + title: "Per-pane shell picker", + body: "Click the distro chip in any pane's toolbar to switch between WSL distros, PowerShell, or a saved SSH host. The pane respawns with the new shell.", + }, + { + title: "SSH host manager", + body: "Titlebar 🔑 SSH hosts opens the manager. Add hostname / user / port / identity file / jump host / extra ssh args. Saved hosts appear in every pane's dropdown.", + }, + { + title: "Saved passwords", + body: "Optionally save a host's password — stored in Windows Credential Manager (DPAPI-encrypted), never written to hosts.json. When ssh prompts on connect it's typed automatically. Hosts with a saved password show 🔒 in the list.", + }, + { + title: "Clickable links", + body: "http and https URLs in terminal output get underlined and open in your default browser on click.", + }, + { + title: "Drag pane headers to swap", + body: "Grab a pane's title bar and drag it onto another pane to swap their tree positions. Useful for reorganizing without keyboard.", + }, + { + title: "Workspace persistence", + body: "Layout, labels, distro choices, and SSH hosts auto-save to %APPDATA%/com.megaproxy.tiletopia (debounced 500ms). Closed panes don't come back — only the structure is restored, shells spawn fresh on next launch.", + }, +];