From 4c6adcf8dce72590716b9211d36787b605e49042 Mon Sep 17 00:00:00 2001 From: Ethan Graf Date: Thu, 7 May 2026 23:33:46 -0400 Subject: [PATCH] initialize settings section --- package.json | 2 +- src/App.tsx | 21 +++- src/components/AppTitleBar.tsx | 14 --- src/components/Modal.tsx | 70 +++++++++++++ src/components/SettingsModal.tsx | 166 +++++++++++++++++++++++++++++++ src/components/SettingsPane.tsx | 39 ++++++++ src/main.ts | 52 +++++++++- src/preload.ts | 7 +- src/vite-env.d.ts | 1 + 9 files changed, 350 insertions(+), 22 deletions(-) create mode 100644 src/components/Modal.tsx create mode 100644 src/components/SettingsModal.tsx create mode 100644 src/components/SettingsPane.tsx diff --git a/package.json b/package.json index 8f42ae5..f1faa06 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "textile", - "productName": "textile", + "productName": "Textile", "version": "1.0.0", "description": "My Electron application description", "main": ".vite/build/main.js", diff --git a/src/App.tsx b/src/App.tsx index e818cb5..6da6c10 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { AppSidebar } from './components/AppSidebar'; +import { SettingsModal } from './components/SettingsModal'; import { AppTitleBar } from './components/AppTitleBar'; import { getRendererPlatform } from './lib/platform'; import { AboutPage } from './pages/AboutPage'; @@ -12,6 +13,7 @@ export default function App() { return document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light'; }); const [sidebarOpen, setSidebarOpen] = useState(true); + const [settingsOpen, setSettingsOpen] = useState(false); // TODO: Remove this guard once non-macOS title bar behavior is implemented. const isMac = getRendererPlatform() === 'darwin'; @@ -24,6 +26,13 @@ export default function App() { } }, [theme]); + useEffect(() => { + const unsubscribe = window.textile?.onOpenSettings(() => { + setSettingsOpen(true); + }); + return () => unsubscribe?.(); + }, []); + return ( {/* TODO: Replace this unsupported screen with full non-macOS UI path. */} @@ -38,10 +47,6 @@ export default function App() { setSidebarOpen((o) => !o)} - theme={theme} - onToggleTheme={() => - setTheme((t) => (t === 'light' ? 'dark' : 'light')) - } />
+ setSettingsOpen(false)} + theme={theme} + onToggleTheme={() => + setTheme((t) => (t === 'light' ? 'dark' : 'light')) + } + /> )}
diff --git a/src/components/AppTitleBar.tsx b/src/components/AppTitleBar.tsx index d9ff9ff..b906f9e 100644 --- a/src/components/AppTitleBar.tsx +++ b/src/components/AppTitleBar.tsx @@ -3,8 +3,6 @@ import type { CSSProperties } from 'react'; type AppTitleBarProps = { sidebarOpen: boolean; onToggleSidebar: () => void; - theme: 'light' | 'dark'; - onToggleTheme: () => void; }; function SidebarToggleIcon({ className }: { className?: string }) { @@ -40,8 +38,6 @@ const macTitleBarStyle: CSSProperties & { WebkitAppRegion?: 'drag' } = { export function AppTitleBar({ sidebarOpen, onToggleSidebar, - theme, - onToggleTheme, }: AppTitleBarProps) { const noDrag = { WebkitAppRegion: 'no-drag' as const }; @@ -63,16 +59,6 @@ export function AppTitleBar({ -
- -
); } diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx new file mode 100644 index 0000000..f75e90f --- /dev/null +++ b/src/components/Modal.tsx @@ -0,0 +1,70 @@ +import type { ReactNode } from 'react'; + +type ModalProps = { + open: boolean; + onClose: () => void; + ariaLabel: string; + title?: string; + panelClassName?: string; + bodyClassName?: string; + children: ReactNode; +}; + +export function Modal({ + open, + onClose, + ariaLabel, + title, + panelClassName, + bodyClassName, + children, +}: ModalProps) { + if (!open) return null; + + return ( +
+
event.stopPropagation()} + > +
+
+ {title ? ( +
+

{title}

+
+ ) : null} + +
+
+ {children} +
+
+
+
+ ); +} diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx new file mode 100644 index 0000000..04b161e --- /dev/null +++ b/src/components/SettingsModal.tsx @@ -0,0 +1,166 @@ +import { useState, type ReactNode } from 'react'; +import { Modal } from './Modal'; +import { SettingsPaneLayout, SettingsPaneSection } from './SettingsPane'; + +type SettingsModalProps = { + open: boolean; + onClose: () => void; + theme: 'light' | 'dark'; + onToggleTheme: () => void; +}; + +type SettingsCategory = 'atmosphere' | 'appearance'; + +function AccountIcon() { + return ( + + + + + ); +} + +function AppearanceIcon() { + return ( + + + + + ); +} + +function SunIcon() { + return ( + + + + + ); +} + +function MoonIcon() { + return ( + + + + ); +} + +export function SettingsModal({ + open, + onClose, + theme, + onToggleTheme, +}: SettingsModalProps) { + const [selectedCategory, setSelectedCategory] = + useState('atmosphere'); + + if (!open) return null; + + const categories: { id: SettingsCategory; label: string; icon: ReactNode }[] = + [ + { + id: 'atmosphere', + label: 'Atmoshpere Account', + icon: , + }, + { + id: 'appearance', + label: 'Appearance', + icon: , + }, + ]; + + return ( + + + {categories.map((category) => ( + + ))} + + } + > + {selectedCategory === 'atmosphere' ? ( +

+ Connect your Atmoshpere account to unlock sync and profile settings. +

+ ) : ( + + + + )} +
+
+ ); +} diff --git a/src/components/SettingsPane.tsx b/src/components/SettingsPane.tsx new file mode 100644 index 0000000..da1d219 --- /dev/null +++ b/src/components/SettingsPane.tsx @@ -0,0 +1,39 @@ +import type { ReactNode } from 'react'; + +type SettingsPaneLayoutProps = { + sidebar: ReactNode; + children: ReactNode; +}; + +type SettingsPaneSectionProps = { + title: string; + children: ReactNode; +}; + +export function SettingsPaneLayout({ + sidebar, + children, +}: SettingsPaneLayoutProps) { + return ( +
+ +
{children}
+
+ ); +} + +export function SettingsPaneSection({ + title, + children, +}: SettingsPaneSectionProps) { + return ( +
+
+

{title}

+ {children} +
+
+ ); +} diff --git a/src/main.ts b/src/main.ts index 3897e38..cd4df4d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,4 @@ -import { app, BrowserWindow } from 'electron'; +import { app, BrowserWindow, Menu } from 'electron'; import path from 'node:path'; import started from 'electron-squirrel-startup'; @@ -7,6 +7,51 @@ if (started) { app.quit(); } +app.setName('Textile'); + +function installApplicationMenu() { + if (process.platform !== 'darwin') return; + + const template: Electron.MenuItemConstructorOptions[] = [ + { + label: app.getName(), + submenu: [ + { + label: 'Settings…', + accelerator: 'CommandOrControl+,', + click: () => { + const target = + BrowserWindow.getFocusedWindow() ?? + BrowserWindow.getAllWindows()[0]; + target?.webContents.send('app:open-settings'); + }, + }, + { type: 'separator' }, + { role: 'about' }, + { type: 'separator' }, + { role: 'services' }, + { type: 'separator' }, + { role: 'hide' }, + { role: 'hideOthers' }, + { role: 'unhide' }, + { type: 'separator' }, + { role: 'quit' }, + ], + }, + { + role: 'editMenu', + }, + { + role: 'viewMenu', + }, + { + role: 'windowMenu', + }, + ]; + + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); +} + const createWindow = () => { // TODO: Unify window chrome setup once non-macOS support is implemented. const isMac = process.platform === 'darwin'; @@ -44,7 +89,10 @@ const createWindow = () => { // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. -app.on('ready', createWindow); +app.on('ready', () => { + installApplicationMenu(); + createWindow(); +}); // Quit when all windows are closed, except on macOS. There, it's common // for applications and their menu bar to stay active until the user quits diff --git a/src/preload.ts b/src/preload.ts index ad66806..843cbe5 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -1,5 +1,10 @@ -import { contextBridge } from 'electron'; +import { contextBridge, ipcRenderer } from 'electron'; contextBridge.exposeInMainWorld('textile', { platform: process.platform, + onOpenSettings: (callback: () => void) => { + const listener = () => callback(); + ipcRenderer.on('app:open-settings', listener); + return () => ipcRenderer.off('app:open-settings', listener); + }, }); diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index babe3a4..420a3b8 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -4,6 +4,7 @@ declare global { interface Window { textile?: { platform: NodeJS.Platform; + onOpenSettings: (callback: () => void) => () => void; }; } } -- 2.51.2