web based infinite canvas
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: titlebar with info modal

+210 -2
+2
apps/web/src/lib/canvas/Canvas.svelte
··· 1 1 <script lang="ts"> 2 2 import HistoryViewer from '$lib/components/HistoryViewer.svelte'; 3 3 import StatusBar from '$lib/components/StatusBar.svelte'; 4 + import TitleBar from '$lib/components/TitleBar.svelte'; 4 5 import Toolbar from '$lib/components/Toolbar.svelte'; 5 6 import { createInputAdapter, type InputAdapter } from '$lib/input'; 6 7 import { ··· 324 325 </script> 325 326 326 327 <div class="editor"> 328 + <TitleBar /> 327 329 <Toolbar currentTool={currentToolId} onToolChange={handleToolChange} onHistoryClick={handleHistoryClick} /> 328 330 <canvas bind:this={canvas}></canvas> 329 331 <HistoryViewer {store} bind:open={historyViewerOpen} onClose={handleHistoryClose} />
+173
apps/web/src/lib/components/TitleBar.svelte
··· 1 + <script lang="ts"> 2 + import Dialog from '$lib/components/Dialog.svelte'; 3 + 4 + const helpLinks = [ 5 + { label: 'Project README', href: 'https://github.com/stormlightlabs/inkfinite', external: true }, 6 + { label: 'Issue Tracker', href: 'https://github.com/stormlightlabs/inkfinite/issues', external: true } 7 + ]; 8 + 9 + const keyboardTips = [ 10 + '⌘/Ctrl + Z to undo, ⇧ + ⌘/Ctrl + Z to redo', 11 + 'Hold space to pan the canvas', 12 + 'Scroll to zoom, double-click to reset view' 13 + ]; 14 + 15 + let infoOpen = $state(false); 16 + function openInfo() { 17 + infoOpen = true; 18 + } 19 + function closeInfo() { 20 + infoOpen = false; 21 + } 22 + </script> 23 + 24 + <header class="titlebar"> 25 + <div class="titlebar__brand"> 26 + <div class="titlebar__logo">∞</div> 27 + <div> 28 + <div class="titlebar__name">Inkfinite</div> 29 + <div class="titlebar__tagline">Infinite canvas playground</div> 30 + </div> 31 + </div> 32 + <div class="titlebar__spacer"></div> 33 + <button class="titlebar__info" onclick={openInfo} aria-label="About Inkfinite"> 34 + <span aria-hidden="true">ℹ︎</span> 35 + <span class="titlebar__info-label">Info</span> 36 + </button> 37 + </header> 38 + 39 + <Dialog bind:open={infoOpen} onClose={closeInfo} title="About Inkfinite"> 40 + <section class="about"> 41 + <h1>About Inkfinite</h1> 42 + <p> 43 + Inkfinite is a Svelte-native infinite canvas prototype. The goal is to build a cross-platform editor with a 44 + framework-agnostic core so the same engine powers both the web and desktop apps. 45 + </p> 46 + 47 + <div class="about__section"> 48 + <h2>Quick Tips</h2> 49 + <ul> 50 + {#each keyboardTips as tip} 51 + <li>{tip}</li> 52 + {/each} 53 + </ul> 54 + </div> 55 + 56 + <div class="about__section"> 57 + <h2>Need help?</h2> 58 + <ul> 59 + {#each helpLinks as link} 60 + <li> 61 + <a href={link.href} target={link.external ? '_blank' : undefined} rel="noreferrer"> 62 + {link.label} 63 + </a> 64 + </li> 65 + {/each} 66 + </ul> 67 + </div> 68 + </section> 69 + </Dialog> 70 + 71 + <style> 72 + .titlebar { 73 + display: flex; 74 + align-items: center; 75 + padding: 8px 16px; 76 + gap: 12px; 77 + background: var(--surface-elevated); 78 + border-bottom: 1px solid var(--border); 79 + } 80 + 81 + .titlebar__brand { 82 + display: flex; 83 + align-items: center; 84 + gap: 12px; 85 + } 86 + 87 + .titlebar__logo { 88 + width: 36px; 89 + height: 36px; 90 + border-radius: 8px; 91 + background: var(--accent); 92 + color: var(--surface); 93 + font-weight: 600; 94 + display: flex; 95 + align-items: center; 96 + justify-content: center; 97 + font-size: 18px; 98 + } 99 + 100 + .titlebar__name { 101 + font-weight: 600; 102 + color: var(--text); 103 + } 104 + 105 + .titlebar__tagline { 106 + font-size: 12px; 107 + color: var(--text-muted); 108 + } 109 + 110 + .titlebar__spacer { 111 + flex: 1; 112 + } 113 + 114 + .titlebar__info { 115 + display: inline-flex; 116 + align-items: center; 117 + gap: 6px; 118 + border: 1px solid var(--border); 119 + background: var(--surface); 120 + color: var(--text); 121 + border-radius: 999px; 122 + padding: 4px 10px; 123 + cursor: pointer; 124 + font-size: 14px; 125 + } 126 + 127 + .titlebar__info:hover { 128 + background: var(--surface-elevated); 129 + } 130 + 131 + .titlebar__info-label { 132 + font-size: 12px; 133 + color: var(--text-secondary); 134 + } 135 + 136 + .about { 137 + padding: 24px; 138 + max-width: 480px; 139 + } 140 + 141 + .about h1 { 142 + margin-top: 0; 143 + font-size: 22px; 144 + } 145 + 146 + .about__section { 147 + margin-top: 20px; 148 + } 149 + 150 + .about__section h2 { 151 + margin-bottom: 8px; 152 + font-size: 16px; 153 + color: var(--text-secondary); 154 + } 155 + 156 + .about__section ul { 157 + margin: 0; 158 + padding-left: 20px; 159 + } 160 + 161 + .about__section li + li { 162 + margin-top: 4px; 163 + } 164 + 165 + .about__section a { 166 + color: var(--accent); 167 + text-decoration: none; 168 + } 169 + 170 + .about__section a:hover { 171 + text-decoration: underline; 172 + } 173 + </style>
+1 -1
apps/web/src/lib/tests/Canvas.history.test.ts
··· 53 53 update: () => {}, 54 54 }), 55 55 createSnapStore: () => ({ 56 - get: () => ({ snapEnabled: false, gridEnabled: false, gridSize: 10 }), 56 + get: () => ({ snapEnabled: false, gridEnabled: true, gridSize: 25 }), 57 57 subscribe: () => () => {}, 58 58 update: () => {}, 59 59 set: () => {},
+8 -1
apps/web/src/lib/tests/Canvas.svelte.test.ts
··· 23 23 update: () => {}, 24 24 }), 25 25 createSnapStore: () => ({ 26 - get: () => ({ snapEnabled: false, gridEnabled: false, gridSize: 10 }), 26 + get: () => ({ snapEnabled: false, gridEnabled: true, gridSize: 25 }), 27 27 subscribe: () => () => {}, 28 28 update: () => {}, 29 29 set: () => {}, ··· 102 102 const statusBar = container.querySelector(".status-bar"); 103 103 104 104 expect(statusBar).toBeTruthy(); 105 + }); 106 + 107 + it("should render the header with info button", () => { 108 + const { container } = render(Canvas); 109 + const titleBar = container.querySelector(".titlebar"); 110 + expect(titleBar).toBeTruthy(); 111 + expect(titleBar?.querySelector(".titlebar__info")).toBeTruthy(); 105 112 }); 106 113 107 114 it("should render all tool buttons in toolbar", () => {
+26
apps/web/src/lib/tests/TitleBar.svelte.test.ts
··· 1 + import { beforeEach, describe, expect, it } from "vitest"; 2 + import { cleanup, render } from "vitest-browser-svelte"; 3 + import TitleBar from "../components/TitleBar.svelte"; 4 + 5 + describe("TitleBar", () => { 6 + beforeEach(() => { 7 + cleanup(); 8 + }); 9 + 10 + it("renders title/logo and info button", () => { 11 + const { container } = render(TitleBar); 12 + expect(container.querySelector(".titlebar")).toBeTruthy(); 13 + expect(container.querySelector(".titlebar__logo")?.textContent).toContain("∞"); 14 + expect(container.querySelector(".titlebar__info")).toBeTruthy(); 15 + }); 16 + 17 + it("opens info dialog when button clicked", async () => { 18 + const { container } = render(TitleBar); 19 + const button = container.querySelector(".titlebar__info") as HTMLButtonElement; 20 + expect(button).toBeTruthy(); 21 + 22 + button.click(); 23 + await new Promise((resolve) => setTimeout(resolve, 0)); 24 + expect(container.querySelector(".about")).toBeTruthy(); 25 + }); 26 + });