Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

ac-electron: dedicated Notepat window + tray, v0.1.37

Bump to 0.1.37 and add a standalone Notepat entry point to the desktop
app:

- **Second menu-bar tray icon** (build/icons/notepatTrayTemplate*.png —
a 3-key piano silhouette) with a "notepat" title. Clicking it opens
the Notepat window.
- **Open Notepat 🎹 entry** in the main AC tray menu.
- **Dedicated notepat window** loading renderer/notepat-view.html —
frameless + transparent + always-on-top (same BrowserWindow opts as
the AC Pane) so the two windows feel like a matched set. The HTML
draws its own chrome: a B&W bordered frame with a small × close tab
protruding from the top-right edge.
- **flip-view.html parameterization**: initial webview src now derives
from ?piece= and ?base= query params (defaults unchanged) so the
same harness can host other pieces in future.
- **Keep tray alive on window-all-closed (macOS)** — Cmd+Q still quits
explicitly; closing all windows just hides them, the trays remain.
- **App icon squircle mask** — generate-icons.mjs now centers the
purple-pals foreground on a 1024px canvas and clips to a 228-radius
rounded rect so macOS displays the familiar squircle silhouette.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+332 -13
ac-electron/build/icon.png

This is a binary file and will not be displayed.

+13
ac-electron/build/icons/notepat-tray.svg
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <!-- Notepat tray icon: mini piano (3 white keys + 2 black keys between). 3 + Template image conventions: only black + alpha; macOS auto-inverts for dark menu bar. --> 4 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"> 5 + <!-- Outer piano body --> 6 + <rect x="1" y="3" width="14" height="10" fill="none" stroke="black" stroke-width="1"/> 7 + <!-- White-key separators (vertical strokes inside the body) --> 8 + <line x1="5.67" y1="3" x2="5.67" y2="13" stroke="black" stroke-width="1"/> 9 + <line x1="10.33" y1="3" x2="10.33" y2="13" stroke="black" stroke-width="1"/> 10 + <!-- Black keys (solid filled rects) --> 11 + <rect x="3.67" y="3" width="2.5" height="5" fill="black"/> 12 + <rect x="9.33" y="3" width="2.5" height="5" fill="black"/> 13 + </svg>
ac-electron/build/icons/notepatTrayTemplate.png

This is a binary file and will not be displayed.

ac-electron/build/icons/notepatTrayTemplate@2x.png

This is a binary file and will not be displayed.

+97 -2
ac-electron/main.js
··· 708 708 709 709 // ========== System Tray ========== 710 710 let tray = null; 711 + let notepatTray = null; 711 712 712 713 function createSystemTray() { 713 714 // Use template icon for proper macOS menu bar appearance ··· 833 834 label: 'New AC Pane', 834 835 accelerator: isMac ? 'Cmd+N' : 'Ctrl+N', 835 836 click: () => openAcPaneWindow() 837 + }); 838 + 839 + menuItems.push({ 840 + label: 'Open Notepat 🎹', 841 + click: () => openNotepatWindow() 836 842 }); 837 843 838 844 // Quick DevTools access (especially for Windows) ··· 1062 1068 tray.setContextMenu(contextMenu); 1063 1069 } 1064 1070 1071 + function createNotepatTray() { 1072 + if (notepatTray) return; 1073 + 1074 + let iconPath; 1075 + if (process.platform === 'darwin') { 1076 + iconPath = app.isPackaged 1077 + ? path.join(process.resourcesPath, 'notepatTrayTemplate.png') 1078 + : path.join(__dirname, 'build', 'icons', 'notepatTrayTemplate.png'); 1079 + } else { 1080 + iconPath = app.isPackaged 1081 + ? path.join(process.resourcesPath, 'notepatTrayTemplate.png') 1082 + : path.join(__dirname, 'build', 'icons', 'notepatTrayTemplate.png'); 1083 + } 1084 + 1085 + const icon = nativeImage.createFromPath(iconPath); 1086 + if (icon.isEmpty()) { 1087 + console.warn('[notepat-tray] Icon empty at', iconPath); 1088 + return; 1089 + } 1090 + if (process.platform === 'darwin') icon.setTemplateImage(true); 1091 + 1092 + notepatTray = new Tray(icon); 1093 + notepatTray.setToolTip('Notepat'); 1094 + if (process.platform === 'darwin') notepatTray.setTitle('notepat'); 1095 + 1096 + const open = () => openNotepatWindow(); 1097 + notepatTray.on('click', open); 1098 + 1099 + const menu = Menu.buildFromTemplate([ 1100 + { label: 'Open Notepat', click: open }, 1101 + { type: 'separator' }, 1102 + { label: 'Quit', accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Alt+F4', click: () => app.quit() }, 1103 + ]); 1104 + notepatTray.setContextMenu(menu); 1105 + } 1106 + 1065 1107 // ========== End System Tray ========== 1066 1108 1067 1109 // Update the tray title text (shown next to icon in menu bar) ··· 1144 1186 // Open a new AC Pane window 1145 1187 async function openAcPaneWindow(options = {}) { 1146 1188 return openAcPaneWindowInternal(options); 1189 + } 1190 + 1191 + // Open a standalone Notepat window — compact window dedicated to the /notepat piece 1192 + let notepatWindow = null; 1193 + function openNotepatWindow() { 1194 + if (notepatWindow && !notepatWindow.isDestroyed()) { 1195 + if (notepatWindow.isMinimized()) notepatWindow.restore(); 1196 + notepatWindow.show(); 1197 + notepatWindow.focus(); 1198 + return notepatWindow; 1199 + } 1200 + 1201 + const baseUrl = startInDevMode ? 'http://localhost:8888' : 'https://aesthetic.computer'; 1202 + 1203 + // Match the AC Pane (prompt) window chrome flags exactly so the two 1204 + // windows feel like a matched set — frameless, transparent, float-on-top. 1205 + const notepatOpts = { 1206 + width: 480, 1207 + height: 360, 1208 + minWidth: 320, 1209 + minHeight: 260, 1210 + title: 'Notepat', 1211 + frame: false, 1212 + transparent: !isPaperWM, 1213 + hasShadow: isPaperWM, 1214 + alwaysOnTop: !isPaperWM, 1215 + backgroundColor: isPaperWM ? '#000000' : '#00000000', 1216 + webPreferences: { 1217 + nodeIntegration: true, 1218 + contextIsolation: false, 1219 + webviewTag: true, 1220 + backgroundThrottling: false, 1221 + }, 1222 + }; 1223 + if (isPaperWM) notepatOpts.type = 'normal'; 1224 + notepatWindow = new BrowserWindow(notepatOpts); 1225 + 1226 + notepatWindow.loadFile( 1227 + getAppPath('renderer/notepat-view.html'), 1228 + { query: { piece: 'notepat', base: baseUrl } }, 1229 + ); 1230 + 1231 + const windowId = windowIdCounter++; 1232 + windows.set(windowId, { window: notepatWindow, mode: 'notepat' }); 1233 + 1234 + notepatWindow.on('closed', () => { 1235 + windows.delete(windowId); 1236 + notepatWindow = null; 1237 + }); 1238 + 1239 + return notepatWindow; 1147 1240 } 1148 1241 1149 1242 // Open KidLisp window (kidlisp.com) ··· 2422 2515 loadPreferences(); 2423 2516 createMenu(); 2424 2517 createSystemTray(); 2518 + createNotepatTray(); 2425 2519 2426 2520 // Start FF1 Bridge server for kidlisp.com integration 2427 2521 ff1Bridge.startBridge(); ··· 2602 2696 }); 2603 2697 2604 2698 app.on('window-all-closed', () => { 2605 - // Quit when all windows are closed, even on macOS 2606 - app.quit(); 2699 + // Keep tray icons (AC + Notepat) alive on macOS so the menu bar remains an entry point. 2700 + // Cmd+Q still quits explicitly. On Windows/Linux, close-all still quits. 2701 + if (process.platform !== 'darwin') app.quit(); 2607 2702 }); 2608 2703 2609 2704 app.on('will-quit', () => {
+2 -2
ac-electron/package-lock.json
··· 1 1 { 2 2 "name": "aesthetic-computer", 3 - "version": "0.1.36", 3 + "version": "0.1.37", 4 4 "lockfileVersion": 3, 5 5 "requires": true, 6 6 "packages": { 7 7 "": { 8 8 "name": "aesthetic-computer", 9 - "version": "0.1.36", 9 + "version": "0.1.37", 10 10 "license": "MIT", 11 11 "dependencies": { 12 12 "@electron/rebuild": "^4.0.2",
+11 -1
ac-electron/package.json
··· 1 1 { 2 2 "name": "aesthetic-computer", 3 - "version": "0.1.36", 3 + "version": "0.1.37", 4 4 "description": "Aesthetic Computer", 5 5 "homepage": "https://aesthetic.computer", 6 6 "author": "Aesthetic Computer <hi@aesthetic.computer>", ··· 53 53 "to": "trayTemplate@2x.png" 54 54 }, 55 55 { 56 + "from": "build/icons/notepatTrayTemplate.png", 57 + "to": "notepatTrayTemplate.png" 58 + }, 59 + { 60 + "from": "build/icons/notepatTrayTemplate@2x.png", 61 + "to": "notepatTrayTemplate@2x.png" 62 + }, 63 + { 56 64 "from": "build/icons/16x16.png", 57 65 "to": "tray-icon.png" 58 66 } ··· 94 102 "renderer/**/*", 95 103 "build/icons/trayTemplate.png", 96 104 "build/icons/trayTemplate@2x.png", 105 + "build/icons/notepatTrayTemplate.png", 106 + "build/icons/notepatTrayTemplate@2x.png", 97 107 "native/osr-gpu/build/Release/*.node", 98 108 "!node_modules/**/*", 99 109 "node_modules/@xterm/**/*",
+8
ac-electron/renderer/flip-view.html
··· 500 500 501 501 const flipCard = document.querySelector('.flip-card'); 502 502 const webviewEl = document.getElementById('front-webview'); 503 + 504 + // Initial src comes from ?piece= (defaults to 'prompt') and ?base= (defaults to prod). 505 + (function initWebviewSrc() { 506 + const params = new URLSearchParams(location.search); 507 + const piece = (params.get('piece') || 'prompt').replace(/^\/+/, ''); 508 + const base = params.get('base') || 'https://aesthetic.computer'; 509 + webviewEl.src = `${base}/${piece}?desktop`; 510 + })(); 503 511 const flipTabs = document.querySelectorAll('.flip-tab'); 504 512 const volumeSlider = document.getElementById('volume-slider'); 505 513 let showingTerminal = false;
+166
ac-electron/renderer/notepat-view.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8" /> 5 + <title>Notepat</title> 6 + <style> 7 + :root { 8 + /* B&W analogue of flip-view's purple border system. 9 + Dark mode: white-on-black. Light mode: black-on-white. */ 10 + --np-border: rgba(240, 240, 240, 0.25); 11 + --np-border-solid: rgba(240, 240, 240, 0.4); 12 + --np-border-hover: rgba(240, 240, 240, 0.55); 13 + --np-border-active: rgba(240, 240, 240, 0.75); 14 + --np-accent: rgba(240, 240, 240, 0.9); 15 + --np-accent-glow: rgba(240, 240, 240, 0.5); 16 + --np-chrome-bg: #000; 17 + } 18 + @media (prefers-color-scheme: light) { 19 + :root { 20 + --np-border: rgba(30, 30, 30, 0.25); 21 + --np-border-solid: rgba(30, 30, 30, 0.4); 22 + --np-border-hover: rgba(30, 30, 30, 0.55); 23 + --np-border-active: rgba(30, 30, 30, 0.8); 24 + --np-accent: rgba(30, 30, 30, 0.95); 25 + --np-accent-glow: rgba(30, 30, 30, 0.5); 26 + --np-chrome-bg: #fff; 27 + } 28 + } 29 + 30 + * { margin: 0; padding: 0; box-sizing: border-box; } 31 + html, body { 32 + width: 100%; 33 + height: 100%; 34 + overflow: hidden; 35 + background: transparent; 36 + } 37 + 38 + /* Rounded chrome frame — same geometry as flip-view's card, without 39 + the 3D flip machinery. We inset on the right so the close tab has 40 + room to protrude outside the border (matching the prompt pane's 41 + flip tabs on left/right). */ 42 + .frame { 43 + position: absolute; 44 + top: 0; 45 + bottom: 0; 46 + left: 0; 47 + right: 26px; 48 + border: 4px solid var(--np-border); 49 + border-radius: 10px; 50 + box-shadow: inset 0 0 0 1px var(--np-border-solid); 51 + overflow: hidden; 52 + background: var(--np-chrome-bg); 53 + } 54 + 55 + /* Top drag strip — lets you move the frameless window. */ 56 + #drag-strip { 57 + position: absolute; 58 + top: 0; 59 + left: 0; 60 + right: 0; 61 + height: 18px; 62 + -webkit-app-region: drag; 63 + z-index: 5; 64 + } 65 + 66 + /* Close tab — small square pocket on the top-right edge, protruding 67 + outside the frame. Shaped like a little tab (not a drag handle) 68 + with the × centered. Clicks close the window. */ 69 + .close-tab { 70 + position: absolute; 71 + right: 0; 72 + top: 10px; 73 + width: 22px; 74 + height: 22px; 75 + background: var(--np-border-solid); 76 + border-radius: 0 6px 6px 0; 77 + display: flex; 78 + align-items: center; 79 + justify-content: center; 80 + cursor: pointer; 81 + user-select: none; 82 + -webkit-app-region: no-drag; 83 + z-index: 250; 84 + transition: background 150ms ease; 85 + } 86 + .close-tab::after { 87 + content: '×'; 88 + color: var(--np-accent); 89 + font-family: "SF Mono", ui-monospace, monospace; 90 + font-size: 16px; 91 + line-height: 1; 92 + font-weight: 400; 93 + transition: color 150ms ease, text-shadow 150ms ease, transform 150ms ease; 94 + pointer-events: none; 95 + } 96 + .close-tab:hover { background: var(--np-border-hover); } 97 + .close-tab:hover::after { 98 + color: var(--np-border-active); 99 + text-shadow: 0 0 10px var(--np-accent-glow); 100 + transform: scale(1.15); 101 + } 102 + 103 + #note-webview { 104 + position: absolute; 105 + top: 0; 106 + left: 0; 107 + right: 0; 108 + bottom: 0; 109 + width: 100%; 110 + height: 100%; 111 + border: none; 112 + background: #111; 113 + } 114 + </style> 115 + </head> 116 + <body> 117 + <div class="frame"> 118 + <div id="drag-strip"></div> 119 + <webview 120 + id="note-webview" 121 + src="https://aesthetic.computer/notepat?desktop&nogap=true" 122 + allowpopups 123 + preload="../webview-preload.js" 124 + ></webview> 125 + </div> 126 + <div class="close-tab" title="Close (Cmd+W)"></div> 127 + <script> 128 + const { ipcRenderer } = require('electron'); 129 + const webviewEl = document.getElementById('note-webview'); 130 + const closeTab = document.querySelector('.close-tab'); 131 + 132 + // Apply ?piece=, ?base= overrides. Default src (set in HTML) keeps us 133 + // functional even if the webview element ignores a late src= assignment 134 + // on some Electron builds. 135 + const params = new URLSearchParams(location.search); 136 + const base = params.get('base') || 'https://aesthetic.computer'; 137 + const piece = (params.get('piece') || 'notepat').replace(/^\/+/, ''); 138 + const targetUrl = `${base}/${piece}?desktop&nogap=true`; 139 + if (webviewEl.src !== targetUrl) webviewEl.src = targetUrl; 140 + 141 + closeTab.addEventListener('click', () => window.close()); 142 + 143 + // Cmd+W / Ctrl+W also closes. 144 + window.addEventListener('keydown', (e) => { 145 + const mod = navigator.platform.toLowerCase().includes('mac') ? e.metaKey : e.ctrlKey; 146 + if (mod && e.key.toLowerCase() === 'w') { 147 + e.preventDefault(); 148 + window.close(); 149 + } 150 + }); 151 + 152 + // Keep the tray title in sync with whatever piece the webview lands on. 153 + webviewEl.addEventListener('did-navigate', (e) => { 154 + try { 155 + const pathname = new URL(e.url).pathname; 156 + const current = pathname.replace(/^\//, '').split('/')[0] || piece; 157 + ipcRenderer.invoke('set-current-piece', current); 158 + } catch (_) {} 159 + }); 160 + 161 + webviewEl.addEventListener('did-fail-load', (e) => { 162 + console.warn('[notepat] webview failed to load:', e.errorCode, e.errorDescription, e.validatedURL); 163 + }); 164 + </script> 165 + </body> 166 + </html>
+35 -8
ac-electron/scripts/generate-icons.mjs
··· 59 59 // The SVG has a transparent background, let's add a background color 60 60 // for the app icon (purple to match the pals theme) 61 61 const backgroundColor = '#1a1a2e'; // Dark purple/navy background 62 - 63 - // Generate main 1024x1024 icon for macOS 64 - console.log('📱 Generating macOS icon (1024x1024)...'); 65 - await sharp(Buffer.from(svgContent)) 66 - .resize(1024, 1024, { 62 + 63 + // macOS icon layout: 1024x1024 canvas, ~100px padding around a 824x824 foreground, 64 + // then masked by a squircle (rounded rect with rx~228) so the Dock renders it 65 + // with the familiar rounded-square silhouette. 66 + console.log('📱 Generating macOS icon (1024x1024, squircle-masked)...'); 67 + const ICON_SIZE = 1024; 68 + const FG_SIZE = 824; 69 + const FG_OFFSET = Math.round((ICON_SIZE - FG_SIZE) / 2); 70 + const MASK_RADIUS = 228; 71 + 72 + const foreground = await sharp(Buffer.from(svgContent)) 73 + .resize(FG_SIZE, FG_SIZE, { 67 74 fit: 'contain', 68 - background: backgroundColor 75 + background: { r: 0, g: 0, b: 0, alpha: 0 }, 69 76 }) 70 - .flatten({ background: backgroundColor }) 77 + .png() 78 + .toBuffer(); 79 + 80 + const squircleMask = Buffer.from( 81 + `<svg xmlns="http://www.w3.org/2000/svg" width="${ICON_SIZE}" height="${ICON_SIZE}">` + 82 + `<rect width="${ICON_SIZE}" height="${ICON_SIZE}" rx="${MASK_RADIUS}" ry="${MASK_RADIUS}" fill="white"/>` + 83 + `</svg>`, 84 + ); 85 + 86 + await sharp({ 87 + create: { 88 + width: ICON_SIZE, 89 + height: ICON_SIZE, 90 + channels: 4, 91 + background: backgroundColor, 92 + }, 93 + }) 94 + .composite([ 95 + { input: foreground, top: FG_OFFSET, left: FG_OFFSET }, 96 + { input: squircleMask, blend: 'dest-in' }, 97 + ]) 71 98 .png() 72 99 .toFile(path.join(buildDir, 'icon.png')); 73 - console.log(' ✅ build/icon.png'); 100 + console.log(' ✅ build/icon.png (squircle)'); 74 101 75 102 // Generate Linux icons at various sizes 76 103 console.log('\n🐧 Generating Linux icons...');