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 (
+
+ );
+}
+
+export function SettingsPaneSection({
+ title,
+ children,
+}: SettingsPaneSectionProps) {
+ return (
+
+ );
+}
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;
};
}
}