react-os-shell 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +242 -0
  3. package/dist/Calculator-BNBRNV4P.js +184 -0
  4. package/dist/Calculator-BNBRNV4P.js.map +1 -0
  5. package/dist/Calendar-5EYUVGUU.js +423 -0
  6. package/dist/Calendar-5EYUVGUU.js.map +1 -0
  7. package/dist/Checkers-MIAHIKJH.js +214 -0
  8. package/dist/Checkers-MIAHIKJH.js.map +1 -0
  9. package/dist/Chess-C5BY45NA.js +190 -0
  10. package/dist/Chess-C5BY45NA.js.map +1 -0
  11. package/dist/ConfirmDialog-ZP4AHVUD.js +3 -0
  12. package/dist/ConfirmDialog-ZP4AHVUD.js.map +1 -0
  13. package/dist/CurrencyConverter-TYPU2IRF.js +223 -0
  14. package/dist/CurrencyConverter-TYPU2IRF.js.map +1 -0
  15. package/dist/Email-JEYYJ3YV.js +1835 -0
  16. package/dist/Email-JEYYJ3YV.js.map +1 -0
  17. package/dist/Game2048-3RH3ELRD.js +191 -0
  18. package/dist/Game2048-3RH3ELRD.js.map +1 -0
  19. package/dist/GeminiChat-BXLBJFT4.js +184 -0
  20. package/dist/GeminiChat-BXLBJFT4.js.map +1 -0
  21. package/dist/Minesweeper-VQGLAZON.js +270 -0
  22. package/dist/Minesweeper-VQGLAZON.js.map +1 -0
  23. package/dist/Notepad-YTZRCAXX.js +389 -0
  24. package/dist/Notepad-YTZRCAXX.js.map +1 -0
  25. package/dist/PomodoroTimer-HARIJN4S.js +196 -0
  26. package/dist/PomodoroTimer-HARIJN4S.js.map +1 -0
  27. package/dist/Spreadsheet-IOKEDNS6.js +446 -0
  28. package/dist/Spreadsheet-IOKEDNS6.js.map +1 -0
  29. package/dist/Sudoku-XHLYCEVT.js +197 -0
  30. package/dist/Sudoku-XHLYCEVT.js.map +1 -0
  31. package/dist/Tetris-ZHCZYL24.js +243 -0
  32. package/dist/Tetris-ZHCZYL24.js.map +1 -0
  33. package/dist/Weather-ROZ7TRNW.js +310 -0
  34. package/dist/Weather-ROZ7TRNW.js.map +1 -0
  35. package/dist/apps/index.d.ts +55 -0
  36. package/dist/apps/index.js +48 -0
  37. package/dist/apps/index.js.map +1 -0
  38. package/dist/chunk-5O2KEISQ.js +155 -0
  39. package/dist/chunk-5O2KEISQ.js.map +1 -0
  40. package/dist/chunk-D7PYW2QS.js +265 -0
  41. package/dist/chunk-D7PYW2QS.js.map +1 -0
  42. package/dist/chunk-GP4Y3VCB.js +806 -0
  43. package/dist/chunk-GP4Y3VCB.js.map +1 -0
  44. package/dist/chunk-NSU7OHPC.js +39 -0
  45. package/dist/chunk-NSU7OHPC.js.map +1 -0
  46. package/dist/chunk-PDFQNHW7.js +24 -0
  47. package/dist/chunk-PDFQNHW7.js.map +1 -0
  48. package/dist/chunk-RFTLYCSF.js +144 -0
  49. package/dist/chunk-RFTLYCSF.js.map +1 -0
  50. package/dist/chunk-SVBID2P6.js +142 -0
  51. package/dist/chunk-SVBID2P6.js.map +1 -0
  52. package/dist/chunk-TFGOLXGD.js +41 -0
  53. package/dist/chunk-TFGOLXGD.js.map +1 -0
  54. package/dist/chunk-WIJ45SYD.js +120 -0
  55. package/dist/chunk-WIJ45SYD.js.map +1 -0
  56. package/dist/chunk-WQIS72NL.js +1470 -0
  57. package/dist/chunk-WQIS72NL.js.map +1 -0
  58. package/dist/index.d.ts +642 -0
  59. package/dist/index.js +3443 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/sounds-NT4DEZGD.js +3 -0
  62. package/dist/sounds-NT4DEZGD.js.map +1 -0
  63. package/dist/styles.css +174 -0
  64. package/dist/types-CFIZ1_xt.d.ts +67 -0
  65. package/package.json +76 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Victor Y. Mau
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,242 @@
1
+ # react-os-shell
2
+
3
+ A desktop-style React UI shell — windows, taskbar, start menu, sticky notes, frosted-glass theming — plus 16 bundled apps (utilities, games, Google integrations).
4
+
5
+ > **Status:** v0.1.0 — extracted from a production ERP where it's been running for a small team. Public API is stabilising; expect minor shape changes before 1.0.
6
+
7
+ ### → [Live demo](https://victorymau.github.io/react-os-shell/)
8
+
9
+ A backend-less playground hosted on GitHub Pages. Wallpapers, themes, sticky notes, the spreadsheet, the calendar, all wired to `localStorage` so the page survives a refresh. Source is in [`examples/demo/`](examples/demo/).
10
+
11
+ [![react-os-shell demo](docs/hero.png)](https://victorymau.github.io/react-os-shell/)
12
+
13
+ <sub>The screenshot is auto-captured against the deployed demo by [`.github/workflows/screenshot.yml`](.github/workflows/screenshot.yml). Run it manually: `gh workflow run "Capture hero screenshot"` (or use the Actions tab).</sub>
14
+
15
+ ## What's in the box
16
+
17
+ **Shell:** `<Layout>`, `<StartMenu>`, `<Desktop>` (with sticky notes + folders), `<WindowManager>`, `<Modal>` (standard / compact / widget styles), `<PopupMenu>`, `<ConfirmDialog>`, `<GlobalSearch>` (Cmd-K), `<ShortcutHelp>`, `<NotificationBell>`, `<BugReportDetail>`, `<StatusBadge>`, frosted-glass theming, `<GoogleConnectModal>`.
18
+
19
+ **Apps (16 ship in the package):**
20
+ - **Utilities (7):** Calculator, Notepad, Spreadsheet, Weather, CurrencyConverter, PomodoroTimer, WorldClock
21
+ - **Games (6):** Chess, Checkers, Minesweeper, Sudoku, Tetris, 2048
22
+ - **Google (3):** Calendar, Email (Gmail), GeminiChat
23
+
24
+ 15 of the 16 ship in the `bundledApps` registry today; the remaining one (WorldClock) is exported individually but needs consumer-supplied prefs wiring before slotting into `bundledApps`. The bundled `Customization` settings page is also exported separately for consumers to register at `/settings/customization`.
25
+
26
+ **Hooks:** `useWindowManager`, `useTheme`, full hotkey/nav system (`useNewHotkey`, `useEditHotkey`, `useModalNav`, `useModalSave`, `useModalDuplicate`, `useTableNav`, `useMultiModal`), `useGoogleAuth`, `useEmailUnread`.
27
+
28
+ **Themes:** light + dark (frosted-glass tinting; the package ships base styles, additional theme variants like pink/green/grey/blue can layer on top).
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ npm i react-os-shell
34
+ ```
35
+
36
+ Peer deps you should already have in a typical React + Tailwind v4 app:
37
+
38
+ ```bash
39
+ npm i react react-dom react-router-dom @tanstack/react-query react-hook-form \
40
+ tailwindcss @headlessui/react @heroicons/react
41
+ ```
42
+
43
+ ## Quick start (~50 lines)
44
+
45
+ ```tsx
46
+ // App.tsx
47
+ import { BrowserRouter, Routes, Route } from 'react-router-dom';
48
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
49
+ import {
50
+ Layout,
51
+ WindowManagerProvider,
52
+ ConfirmProvider,
53
+ ShellAuthProvider,
54
+ ShellPrefsProvider,
55
+ ShellEntityFetcherProvider,
56
+ StatusBadgeProvider,
57
+ setShellApiClient,
58
+ setShellAuthBridge,
59
+ setShellWindowRegistry,
60
+ createWindowRegistry,
61
+ useLocalStoragePrefs,
62
+ } from 'react-os-shell';
63
+ import { bundledApps } from 'react-os-shell/apps';
64
+ import 'react-os-shell/styles.css';
65
+ import axios from 'axios';
66
+
67
+ const apiClient = axios.create({ baseURL: '/api' });
68
+ setShellApiClient(apiClient);
69
+ setShellWindowRegistry(createWindowRegistry(bundledApps));
70
+ setShellAuthBridge({ user: { first_name: 'Demo' }, logout: () => {} });
71
+
72
+ const navSections = [
73
+ { to: '/', label: 'Home' },
74
+ { label: 'Games', items: bundledApps['/chess'] ? [
75
+ { to: '/chess', label: 'Chess' },
76
+ { to: '/tetris', label: 'Tetris' },
77
+ { to: '/2048', label: '2048' },
78
+ ] : [] },
79
+ ];
80
+
81
+ const queryClient = new QueryClient();
82
+
83
+ export default function App() {
84
+ const prefs = useLocalStoragePrefs('my-app');
85
+ return (
86
+ <QueryClientProvider client={queryClient}>
87
+ <ConfirmProvider>
88
+ <BrowserRouter>
89
+ <ShellAuthProvider value={{ hasAnyPerm: () => true }}>
90
+ <ShellPrefsProvider value={prefs}>
91
+ <ShellEntityFetcherProvider value={(endpoint, id) => apiClient.get(`${endpoint}${id}/`).then(r => r.data)}>
92
+ <StatusBadgeProvider groups={{}}>
93
+ <WindowManagerProvider>
94
+ <Routes>
95
+ <Route path="*" element={<Layout navSections={navSections} navIcons={{}} />} />
96
+ </Routes>
97
+ </WindowManagerProvider>
98
+ </StatusBadgeProvider>
99
+ </ShellEntityFetcherProvider>
100
+ </ShellPrefsProvider>
101
+ </ShellAuthProvider>
102
+ </BrowserRouter>
103
+ </ConfirmProvider>
104
+ </QueryClientProvider>
105
+ );
106
+ }
107
+ ```
108
+
109
+ That gives you the full desktop with all 12 utility/game/Google apps reachable through the start menu's Games / Utilities trays. Add your own entity windows by extending the registry, and wire the notification / bug-report / sticky-note systems through optional config callbacks when you want them.
110
+
111
+ ## Concepts
112
+
113
+ ### Window registry
114
+
115
+ Every window the shell can open lives in a `WindowRegistry` map. Two entry shapes:
116
+
117
+ - **Page** — `{ component: LazyExoticComponent, label, size?, widget?, … }`. Opened via `openPage(routeKey)`.
118
+ - **Entity** — `{ endpoint, render(entity, …), title(entity), footer?, … }`. Opened via `openEntity(typeKey, id)`. The shell GETs `${endpoint}${id}/` (via the consumer-supplied entity fetcher) and hands the result to `render`.
119
+
120
+ Compose multiple partial maps with `createWindowRegistry(...maps)`:
121
+
122
+ ```ts
123
+ import { bundledApps } from 'react-os-shell/apps';
124
+ import { erpEntities } from './shell-config/erpEntities';
125
+
126
+ const windows = createWindowRegistry(bundledApps, erpEntities);
127
+ setShellWindowRegistry(windows);
128
+ ```
129
+
130
+ ### Nav sections
131
+
132
+ `Layout` renders the start menu from a `(NavSection | NavItem)[]` you pass in:
133
+
134
+ ```ts
135
+ const navSections = [
136
+ { to: '/', label: 'Home' },
137
+ { label: 'Clients', items: [
138
+ { to: '/orders', label: 'Sales Orders', perms: ['view_order'] },
139
+ { to: '/clients', label: 'Clients' },
140
+ ]},
141
+ ];
142
+ ```
143
+
144
+ Items with `perms` are filtered through `<ShellAuthProvider value={{ hasAnyPerm }}>`.
145
+
146
+ ### useWindowManager
147
+
148
+ The hook every component uses to open / close / minimise windows:
149
+
150
+ ```ts
151
+ const { openPage, openEntity, closeEntity, openWindows } = useWindowManager();
152
+
153
+ openPage('/calculator');
154
+ openEntity('order', 'uuid-123');
155
+ ```
156
+
157
+ ## API reference
158
+
159
+ All exports are named — `import { Modal, ... } from 'react-os-shell'`.
160
+
161
+ ### Components
162
+
163
+ | Export | Purpose |
164
+ |---|---|
165
+ | `Layout` | Top-level shell — desktop + taskbar + start menu. Mount once inside your providers. |
166
+ | `StartMenu` / `Desktop` / `WindowManagerProvider` | Used internally by `Layout`; rarely instantiated directly. |
167
+ | `Modal`, `ModalActions`, `CopyButton`, `CancelButton` | Window primitive supporting standard / compact / widget styles. |
168
+ | `PopupMenu`, `PopupMenuItem`, `PopupMenuDivider`, `PopupMenuLabel` | Right-click / context-menu primitive. |
169
+ | `ConfirmProvider`, `confirm` | Imperative `confirm({ title, body })` returning a Promise<boolean>. |
170
+ | `GlobalSearch` | Cmd-K command palette. Pass `providers: SearchProvider[]` to add results. |
171
+ | `ShortcutHelp` | The keyboard cheatsheet shown on `?`. |
172
+ | `NotificationBell` | Taskbar bell — config via `<Layout notifications={…}>`. |
173
+ | `BugReportDetail` | Used inside an entity-window registry entry; reads from `<BugReportConfigProvider>`. |
174
+ | `StatusBadge` | Coloured pill rendering a status string. Map status→semantic group via `<StatusBadgeProvider groups={{...}}>`. |
175
+ | `GoogleConnectModal` | UI for entering Google OAuth client ID. |
176
+
177
+ ### Providers + setters
178
+
179
+ | Export | Use |
180
+ |---|---|
181
+ | `<ShellAuthProvider value={{ hasAnyPerm }}>` | Permission-filter nav items. |
182
+ | `<ShellPrefsProvider value={{ prefs, save }}>` | Where the shell reads/writes user prefs (theme, taskbar pos, sticky notes, …). Use `useLocalStoragePrefs(key)` for a backend-less default. |
183
+ | `<ShellEntityFetcherProvider value={(endpoint, id) => …}>` | How the modal stack fetches entity data. |
184
+ | `<BugReportConfigProvider value={{ submit, list?, resolve? }}>` | Wire the bug-report flow to your backend. |
185
+ | `<DesktopHostProvider value={{ stickyResolver?, saveShortcuts?, … }}>` | Sticky-note ref resolver + persistence callbacks. |
186
+ | `<StatusBadgeProvider groups={{ status: 'success' \| ... }}>` | Status string → semantic group. |
187
+ | `setShellApiClient(axios)` | Module-level: register your axios instance once. |
188
+ | `setShellAuthBridge({ user, logout })` | Module-level: register user identity / logout handler. |
189
+ | `setShellWindowRegistry(registry)` | Module-level: register your composed `WindowRegistry`. |
190
+
191
+ ### Hooks
192
+
193
+ | Export | Purpose |
194
+ |---|---|
195
+ | `useWindowManager()` | `{ openPage, openEntity, closeEntity, openWindows, … }` |
196
+ | `useTheme()` | `{ theme, resolved }` — current theme + system-resolved value. |
197
+ | `useNewHotkey(handler)` | Cmd/Ctrl+N — for "create new entity" buttons. |
198
+ | `useEditHotkey(handler)` | Alt+Shift+E — for "edit" toggle. |
199
+ | `useModalNav({ onPrev, onNext })` | ←/→ to step through siblings inside a modal. |
200
+ | `useModalSave(handler)` | Cmd-S inside a modal. |
201
+ | `useModalDuplicate(handler)` | Alt-D inside a modal. |
202
+ | `useTableNav({ rows, cols, onCell })` | Arrow-key cell navigation in editable grids. |
203
+ | `useMultiModal()` | Manages multi-window stacking + activate/blur. |
204
+ | `useGoogleAuth({ clientId? })` | Google Identity Services wrapper — token + scopes. |
205
+ | `useEmailUnread()` | Live unread-count for the Gmail badge. |
206
+ | `useShellAuth() / useShellPrefs() / useShellEntityFetcher() / useBugReport() / useDesktopHost()` | Context readers — the shell uses these internally; consumers may also call them. |
207
+
208
+ ### Apps barrel — `react-os-shell/apps`
209
+
210
+ | Export | Type |
211
+ |---|---|
212
+ | `bundledApps` | `WindowRegistry` — 12 ready-to-mount apps. |
213
+ | `utilityApps`, `gameApps`, `googleApps` | Subsets of `bundledApps`. |
214
+ | `Calculator`, `Spreadsheet`, `Weather`, `CurrencyConverter`, `PomodoroTimer`, `Chess`, `Checkers`, `Sudoku`, `Tetris`, `Game2048`, `Email`, `GeminiChat` | Lazy components — use directly in custom registry entries. |
215
+
216
+ ### Misc
217
+
218
+ | Export | Notes |
219
+ |---|---|
220
+ | `createWindowRegistry(...maps)` | Variadic merge — later partials override earlier on the same key. |
221
+ | `isPageEntry`, `isEntityEntry` | Type guards for `WindowRegistryEntry`. |
222
+ | `glassStyle()` | Returns the theme-aware frosted-glass `style` object. |
223
+ | `reportBug(submit)` | Captures a screenshot via `getDisplayMedia`, opens the dialog, hands the payload to your `submit`. |
224
+ | `formatDate(iso)` | Locale-aware date formatter. |
225
+ | `toast.success / .error / .info` | Toast notifications — auto-mounts container. |
226
+ | `Kbd` constants — `MOD`, `ALT`, `SHIFT`, `ENTER`, `ALT_SHIFT_E`, `CMD_K`, … | Symbol constants for rendering keyboard shortcuts. |
227
+
228
+ ## Why it exists
229
+
230
+ Most "desktop UI" demos on the web are toys with hardcoded windows and no escape hatch. This one was extracted from a working ERP where every entity (sales orders, invoices, vendors, …) opens as its own window with consistent header, footer, hotkeys, depth stacking, and split-view. The shell is **fully decoupled** from any specific backend — every subsystem that needs server data (notifications, bug reports, desktop shortcuts, search, entity fetching) takes its data through callback configs supplied by the consumer. Drop-in localStorage fallbacks ship for prefs and sticky notes so the package works out of the box without a backend.
231
+
232
+ ## Examples
233
+
234
+ - [`examples/demo`](examples/demo/) — small Vite app showcasing the shell + bundled apps with mock data. Live at [victorymau.github.io/react-os-shell](https://victorymau.github.io/react-os-shell/), deployed automatically by [`.github/workflows/pages.yml`](.github/workflows/pages.yml) on every push to `main`.
235
+
236
+ ## Contributing
237
+
238
+ PRs welcome. Open an issue first for non-trivial changes so we can align on shape.
239
+
240
+ ## License
241
+
242
+ [MIT](./LICENSE)
@@ -0,0 +1,184 @@
1
+ import { loadAppearance, WidgetSettingsModal } from './chunk-SVBID2P6.js';
2
+ import { useWidgetSettings } from './chunk-WQIS72NL.js';
3
+ import './chunk-RFTLYCSF.js';
4
+ import { useState, useCallback, useEffect } from 'react';
5
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
6
+
7
+ var CALC_SETTINGS_KEY = "calc_appearance";
8
+ function Calculator() {
9
+ const [display, setDisplay] = useState("0");
10
+ const [prev, setPrev] = useState(null);
11
+ const [op, setOp] = useState(null);
12
+ const [resetNext, setResetNext] = useState(false);
13
+ const [lastExpr, setLastExpr] = useState("");
14
+ const [history, setHistory] = useState([]);
15
+ const [copied, setCopied] = useState(false);
16
+ const [appearance, setAppearance] = useState(() => loadAppearance(CALC_SETTINGS_KEY));
17
+ const [settingsOpen, setSettingsOpen] = useState(false);
18
+ const [configAppearance, setConfigAppearance] = useState(appearance);
19
+ useWidgetSettings(useCallback(() => {
20
+ setConfigAppearance({ ...appearance });
21
+ setSettingsOpen(true);
22
+ }, [appearance]));
23
+ const append = useCallback((ch) => {
24
+ setLastExpr("");
25
+ setDisplay((d) => {
26
+ if (resetNext || d === "0") {
27
+ setResetNext(false);
28
+ return ch === "." ? "0." : ch;
29
+ }
30
+ if (ch === "." && d.includes(".")) return d;
31
+ return d + ch;
32
+ });
33
+ }, [resetNext]);
34
+ const clear = useCallback(() => {
35
+ setDisplay("0");
36
+ setPrev(null);
37
+ setOp(null);
38
+ setResetNext(false);
39
+ }, []);
40
+ const compute = useCallback((a, b, operator) => {
41
+ switch (operator) {
42
+ case "+":
43
+ return a + b;
44
+ case "-":
45
+ return a - b;
46
+ case "\xD7":
47
+ return a * b;
48
+ case "\xF7":
49
+ return b === 0 ? 0 : a / b;
50
+ default:
51
+ return b;
52
+ }
53
+ }, []);
54
+ const handleOp = useCallback((nextOp) => {
55
+ const current = parseFloat(display);
56
+ if (prev !== null && op && !resetNext) {
57
+ const result = compute(prev, current, op);
58
+ setDisplay(String(result));
59
+ setPrev(result);
60
+ setHistory((h) => [`${prev} ${op} ${current} = ${result}`, ...h].slice(0, 20));
61
+ } else {
62
+ setPrev(current);
63
+ }
64
+ setOp(nextOp);
65
+ setResetNext(true);
66
+ }, [display, prev, op, resetNext, compute]);
67
+ const equals = useCallback(() => {
68
+ if (prev === null || !op) return;
69
+ const current = parseFloat(display);
70
+ const result = compute(prev, current, op);
71
+ const expr = `${prev} ${op} ${current} =`;
72
+ setHistory((h) => [`${expr} ${result}`, ...h].slice(0, 20));
73
+ setLastExpr(expr);
74
+ setDisplay(String(result));
75
+ setPrev(null);
76
+ setOp(null);
77
+ setResetNext(true);
78
+ }, [display, prev, op, compute]);
79
+ const percent = useCallback(() => {
80
+ setDisplay((d) => String(parseFloat(d) / 100));
81
+ }, []);
82
+ const negate = useCallback(() => {
83
+ setDisplay((d) => d.startsWith("-") ? d.slice(1) : d === "0" ? d : "-" + d);
84
+ }, []);
85
+ useEffect(() => {
86
+ const handler = (e) => {
87
+ const tag = e.target.tagName;
88
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
89
+ if (e.key >= "0" && e.key <= "9") append(e.key);
90
+ else if (e.key === ".") append(".");
91
+ else if (e.key === "+") handleOp("+");
92
+ else if (e.key === "-") handleOp("-");
93
+ else if (e.key === "*") handleOp("\xD7");
94
+ else if (e.key === "/") {
95
+ e.preventDefault();
96
+ handleOp("\xF7");
97
+ } else if (e.key === "Enter" || e.key === "=") equals();
98
+ else if (e.key === "Escape") clear();
99
+ else if (e.key === "Backspace") setDisplay((d) => d.length > 1 ? d.slice(0, -1) : "0");
100
+ else if (e.key === "%") percent();
101
+ };
102
+ window.addEventListener("keydown", handler);
103
+ return () => window.removeEventListener("keydown", handler);
104
+ }, [append, handleOp, equals, clear, percent]);
105
+ const btn = "flex items-center justify-center rounded-lg text-sm font-medium transition-colors";
106
+ const numBtn = `${btn} bg-white border border-gray-200 text-gray-900 hover:bg-gray-50 active:bg-gray-100`;
107
+ const opBtn = `${btn} bg-blue-50 border border-blue-200 text-blue-700 hover:bg-blue-100 active:bg-blue-200`;
108
+ const fnBtn = `${btn} bg-gray-100 border border-gray-200 text-gray-600 hover:bg-gray-200 active:bg-gray-300`;
109
+ const eqBtn = `${btn} bg-blue-600 text-white hover:bg-blue-700 active:bg-blue-800`;
110
+ const formatDisplay = (val) => {
111
+ const num = parseFloat(val);
112
+ if (isNaN(num)) return val;
113
+ if (val.endsWith(".") || val.endsWith(".0")) return val;
114
+ if (Math.abs(num) < 1e15 && val.length < 16) return val;
115
+ return num.toExponential(6);
116
+ };
117
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
118
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", style: {
119
+ opacity: appearance.activeOpacity / 100,
120
+ backdropFilter: appearance.activeBlur > 0 ? `blur(${appearance.activeBlur}px)` : void 0
121
+ }, children: [
122
+ /* @__PURE__ */ jsxs("div", { className: "bg-slate-200 rounded-t-lg px-4 py-2 group/display relative flex flex-col justify-end min-h-[120px] border-b border-slate-300", children: [
123
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col justify-end flex-1 overflow-hidden", children: history.slice(0, 3).reverse().map((h, i) => /* @__PURE__ */ jsx("div", { className: "text-[11px] text-slate-400 text-right font-mono truncate", children: h }, i)) }),
124
+ (lastExpr || op && prev !== null) && /* @__PURE__ */ jsx("div", { className: "text-base text-slate-600 text-right font-mono", children: lastExpr || `${prev} ${op} ${!resetNext ? display : ""}` }),
125
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
126
+ /* @__PURE__ */ jsx(
127
+ "button",
128
+ {
129
+ onClick: () => {
130
+ navigator.clipboard.writeText(display);
131
+ setCopied(true);
132
+ setTimeout(() => setCopied(false), 1200);
133
+ },
134
+ title: "Copy result",
135
+ className: "shrink-0 text-slate-300 hover:text-slate-500 transition-colors opacity-0 group-hover/display:opacity-100",
136
+ children: copied ? /* @__PURE__ */ jsx("svg", { className: "h-5 w-5 text-green-500", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 13l4 4L19 7" }) }) : /* @__PURE__ */ jsx("svg", { className: "h-5 w-5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" }) })
137
+ }
138
+ ),
139
+ /* @__PURE__ */ jsx("div", { className: "text-right text-4xl font-mono font-bold text-slate-800 truncate flex-1", children: formatDisplay(display) })
140
+ ] })
141
+ ] }),
142
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-4 grid-rows-5 gap-1.5 flex-1 min-h-0 p-2", children: [
143
+ /* @__PURE__ */ jsx("button", { onClick: clear, className: fnBtn, children: "AC" }),
144
+ /* @__PURE__ */ jsx("button", { onClick: negate, className: fnBtn, children: "+/\u2212" }),
145
+ /* @__PURE__ */ jsx("button", { onClick: percent, className: fnBtn, children: "%" }),
146
+ /* @__PURE__ */ jsx("button", { onClick: () => handleOp("\xF7"), className: `${opBtn} ${op === "\xF7" ? "ring-2 ring-blue-400" : ""}`, children: "\xF7" }),
147
+ /* @__PURE__ */ jsx("button", { onClick: () => append("7"), className: numBtn, children: "7" }),
148
+ /* @__PURE__ */ jsx("button", { onClick: () => append("8"), className: numBtn, children: "8" }),
149
+ /* @__PURE__ */ jsx("button", { onClick: () => append("9"), className: numBtn, children: "9" }),
150
+ /* @__PURE__ */ jsx("button", { onClick: () => handleOp("\xD7"), className: `${opBtn} ${op === "\xD7" ? "ring-2 ring-blue-400" : ""}`, children: "\xD7" }),
151
+ /* @__PURE__ */ jsx("button", { onClick: () => append("4"), className: numBtn, children: "4" }),
152
+ /* @__PURE__ */ jsx("button", { onClick: () => append("5"), className: numBtn, children: "5" }),
153
+ /* @__PURE__ */ jsx("button", { onClick: () => append("6"), className: numBtn, children: "6" }),
154
+ /* @__PURE__ */ jsx("button", { onClick: () => handleOp("-"), className: `${opBtn} ${op === "-" ? "ring-2 ring-blue-400" : ""}`, children: "\u2212" }),
155
+ /* @__PURE__ */ jsx("button", { onClick: () => append("1"), className: numBtn, children: "1" }),
156
+ /* @__PURE__ */ jsx("button", { onClick: () => append("2"), className: numBtn, children: "2" }),
157
+ /* @__PURE__ */ jsx("button", { onClick: () => append("3"), className: numBtn, children: "3" }),
158
+ /* @__PURE__ */ jsx("button", { onClick: () => handleOp("+"), className: `${opBtn} ${op === "+" ? "ring-2 ring-blue-400" : ""}`, children: "+" }),
159
+ /* @__PURE__ */ jsx("button", { onClick: () => append("0"), className: `${numBtn} col-span-2`, children: "0" }),
160
+ /* @__PURE__ */ jsx("button", { onClick: () => append("."), className: numBtn, children: "." }),
161
+ /* @__PURE__ */ jsx("button", { onClick: equals, className: eqBtn, children: "=" })
162
+ ] })
163
+ ] }),
164
+ /* @__PURE__ */ jsx(
165
+ WidgetSettingsModal,
166
+ {
167
+ open: settingsOpen,
168
+ onClose: () => setSettingsOpen(false),
169
+ title: "Calculator Settings",
170
+ appearance: configAppearance,
171
+ onAppearanceChange: setConfigAppearance,
172
+ onSave: () => {
173
+ setAppearance(configAppearance);
174
+ localStorage.setItem(CALC_SETTINGS_KEY, JSON.stringify(configAppearance));
175
+ setSettingsOpen(false);
176
+ }
177
+ }
178
+ )
179
+ ] });
180
+ }
181
+
182
+ export { Calculator as default };
183
+ //# sourceMappingURL=Calculator-BNBRNV4P.js.map
184
+ //# sourceMappingURL=Calculator-BNBRNV4P.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/apps/Calculator.tsx"],"names":[],"mappings":"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","file":"Calculator-BNBRNV4P.js","sourcesContent":["import { useState, useCallback, useEffect } from 'react';\nimport { useWidgetSettings } from '../shell/Modal';\nimport WidgetSettingsModal, { loadAppearance, type WidgetAppearance } from '../shell/WidgetSettingsModal';\n\ntype Op = '+' | '-' | '×' | '÷' | null;\nconst CALC_SETTINGS_KEY = 'calc_appearance';\n\nexport default function Calculator() {\n const [display, setDisplay] = useState('0');\n const [prev, setPrev] = useState<number | null>(null);\n const [op, setOp] = useState<Op>(null);\n const [resetNext, setResetNext] = useState(false);\n const [lastExpr, setLastExpr] = useState('');\n const [history, setHistory] = useState<string[]>([]);\n const [copied, setCopied] = useState(false);\n const [appearance, setAppearance] = useState(() => loadAppearance(CALC_SETTINGS_KEY));\n const [settingsOpen, setSettingsOpen] = useState(false);\n const [configAppearance, setConfigAppearance] = useState<WidgetAppearance>(appearance);\n\n useWidgetSettings(useCallback(() => {\n setConfigAppearance({ ...appearance });\n setSettingsOpen(true);\n }, [appearance]));\n\n const append = useCallback((ch: string) => {\n setLastExpr('');\n setDisplay(d => {\n if (resetNext || d === '0') {\n setResetNext(false);\n return ch === '.' ? '0.' : ch;\n }\n if (ch === '.' && d.includes('.')) return d;\n return d + ch;\n });\n }, [resetNext]);\n\n const clear = useCallback(() => {\n setDisplay('0');\n setPrev(null);\n setOp(null);\n setResetNext(false);\n }, []);\n\n const compute = useCallback((a: number, b: number, operator: Op): number => {\n switch (operator) {\n case '+': return a + b;\n case '-': return a - b;\n case '×': return a * b;\n case '÷': return b === 0 ? 0 : a / b;\n default: return b;\n }\n }, []);\n\n const handleOp = useCallback((nextOp: Op) => {\n const current = parseFloat(display);\n if (prev !== null && op && !resetNext) {\n const result = compute(prev, current, op);\n setDisplay(String(result));\n setPrev(result);\n setHistory(h => [`${prev} ${op} ${current} = ${result}`, ...h].slice(0, 20));\n } else {\n setPrev(current);\n }\n setOp(nextOp);\n setResetNext(true);\n }, [display, prev, op, resetNext, compute]);\n\n const equals = useCallback(() => {\n if (prev === null || !op) return;\n const current = parseFloat(display);\n const result = compute(prev, current, op);\n const expr = `${prev} ${op} ${current} =`;\n setHistory(h => [`${expr} ${result}`, ...h].slice(0, 20));\n setLastExpr(expr);\n setDisplay(String(result));\n setPrev(null);\n setOp(null);\n setResetNext(true);\n }, [display, prev, op, compute]);\n\n const percent = useCallback(() => {\n setDisplay(d => String(parseFloat(d) / 100));\n }, []);\n\n const negate = useCallback(() => {\n setDisplay(d => d.startsWith('-') ? d.slice(1) : d === '0' ? d : '-' + d);\n }, []);\n\n // Keyboard support\n useEffect(() => {\n const handler = (e: KeyboardEvent) => {\n const tag = (e.target as HTMLElement).tagName;\n if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;\n if (e.key >= '0' && e.key <= '9') append(e.key);\n else if (e.key === '.') append('.');\n else if (e.key === '+') handleOp('+');\n else if (e.key === '-') handleOp('-');\n else if (e.key === '*') handleOp('×');\n else if (e.key === '/') { e.preventDefault(); handleOp('÷'); }\n else if (e.key === 'Enter' || e.key === '=') equals();\n else if (e.key === 'Escape') clear();\n else if (e.key === 'Backspace') setDisplay(d => d.length > 1 ? d.slice(0, -1) : '0');\n else if (e.key === '%') percent();\n };\n window.addEventListener('keydown', handler);\n return () => window.removeEventListener('keydown', handler);\n }, [append, handleOp, equals, clear, percent]);\n\n const btn = 'flex items-center justify-center rounded-lg text-sm font-medium transition-colors';\n const numBtn = `${btn} bg-white border border-gray-200 text-gray-900 hover:bg-gray-50 active:bg-gray-100`;\n const opBtn = `${btn} bg-blue-50 border border-blue-200 text-blue-700 hover:bg-blue-100 active:bg-blue-200`;\n const fnBtn = `${btn} bg-gray-100 border border-gray-200 text-gray-600 hover:bg-gray-200 active:bg-gray-300`;\n const eqBtn = `${btn} bg-blue-600 text-white hover:bg-blue-700 active:bg-blue-800`;\n\n // Format display for readability\n const formatDisplay = (val: string) => {\n const num = parseFloat(val);\n if (isNaN(num)) return val;\n if (val.endsWith('.') || val.endsWith('.0')) return val;\n if (Math.abs(num) < 1e15 && val.length < 16) return val;\n return num.toExponential(6);\n };\n\n return (\n <>\n <div className=\"flex flex-col h-full\" style={{\n opacity: appearance.activeOpacity / 100,\n backdropFilter: appearance.activeBlur > 0 ? `blur(${appearance.activeBlur}px)` : undefined,\n }}>\n {/* Display — 5-line area with history + current value */}\n <div className=\"bg-slate-200 rounded-t-lg px-4 py-2 group/display relative flex flex-col justify-end min-h-[120px] border-b border-slate-300\">\n {/* History lines (most recent at bottom, above current) */}\n <div className=\"flex flex-col justify-end flex-1 overflow-hidden\">\n {history.slice(0, 3).reverse().map((h, i) => (\n <div key={i} className=\"text-[11px] text-slate-400 text-right font-mono truncate\">{h}</div>\n ))}\n </div>\n {/* Current expression */}\n {(lastExpr || (op && prev !== null)) && (\n <div className=\"text-base text-slate-600 text-right font-mono\">\n {lastExpr || `${prev} ${op} ${!resetNext ? display : ''}`}\n </div>\n )}\n {/* Current value */}\n <div className=\"flex items-center gap-2\">\n <button onClick={() => { navigator.clipboard.writeText(display); setCopied(true); setTimeout(() => setCopied(false), 1200); }}\n title=\"Copy result\"\n className=\"shrink-0 text-slate-300 hover:text-slate-500 transition-colors opacity-0 group-hover/display:opacity-100\">\n {copied\n ? <svg className=\"h-5 w-5 text-green-500\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth={2}><path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M5 13l4 4L19 7\" /></svg>\n : <svg className=\"h-5 w-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth={2}><path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\" /></svg>\n }\n </button>\n <div className=\"text-right text-4xl font-mono font-bold text-slate-800 truncate flex-1\">\n {formatDisplay(display)}\n </div>\n </div>\n </div>\n\n {/* Buttons */}\n <div className=\"grid grid-cols-4 grid-rows-5 gap-1.5 flex-1 min-h-0 p-2\">\n <button onClick={clear} className={fnBtn}>AC</button>\n <button onClick={negate} className={fnBtn}>+/−</button>\n <button onClick={percent} className={fnBtn}>%</button>\n <button onClick={() => handleOp('÷')} className={`${opBtn} ${op === '÷' ? 'ring-2 ring-blue-400' : ''}`}>÷</button>\n\n <button onClick={() => append('7')} className={numBtn}>7</button>\n <button onClick={() => append('8')} className={numBtn}>8</button>\n <button onClick={() => append('9')} className={numBtn}>9</button>\n <button onClick={() => handleOp('×')} className={`${opBtn} ${op === '×' ? 'ring-2 ring-blue-400' : ''}`}>×</button>\n\n <button onClick={() => append('4')} className={numBtn}>4</button>\n <button onClick={() => append('5')} className={numBtn}>5</button>\n <button onClick={() => append('6')} className={numBtn}>6</button>\n <button onClick={() => handleOp('-')} className={`${opBtn} ${op === '-' ? 'ring-2 ring-blue-400' : ''}`}>−</button>\n\n <button onClick={() => append('1')} className={numBtn}>1</button>\n <button onClick={() => append('2')} className={numBtn}>2</button>\n <button onClick={() => append('3')} className={numBtn}>3</button>\n <button onClick={() => handleOp('+')} className={`${opBtn} ${op === '+' ? 'ring-2 ring-blue-400' : ''}`}>+</button>\n\n <button onClick={() => append('0')} className={`${numBtn} col-span-2`}>0</button>\n <button onClick={() => append('.')} className={numBtn}>.</button>\n <button onClick={equals} className={eqBtn}>=</button>\n </div>\n\n </div>\n <WidgetSettingsModal open={settingsOpen} onClose={() => setSettingsOpen(false)} title=\"Calculator Settings\"\n appearance={configAppearance} onAppearanceChange={setConfigAppearance}\n onSave={() => { setAppearance(configAppearance); localStorage.setItem(CALC_SETTINGS_KEY, JSON.stringify(configAppearance)); setSettingsOpen(false); }} />\n </>\n );\n}\n"]}