@whiskeyjack-net/tauri 0.3.9 → 0.4.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.
- package/README.md +42 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +69 -7
- package/dist/index.js.map +1 -1
- package/dist/use-window-drag.d.ts +22 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -39,6 +39,13 @@ off Tauri, so it is safe to ship in a plain web/PWA build.
|
|
|
39
39
|
overrides the DS `--color-accent-*` variables, adds the `.tauri-desktop` /
|
|
40
40
|
`.tauri-platform-*` markers, and publishes the `--wc-left/right-px`
|
|
41
41
|
window-control footprint the header-clearance CSS consumes. No-op elsewhere.
|
|
42
|
+
It **invokes two Rust commands** (below); without them it falls back to the
|
|
43
|
+
platform default and warns once in development.
|
|
44
|
+
- **`useWindowDrag()`** – returns `{ onMouseDown }` to spread onto whatever
|
|
45
|
+
should behave like a title bar, usually the DS `AppHeader`. Drags the window,
|
|
46
|
+
zooms on double-click, skips interactive descendants so the nav keeps its
|
|
47
|
+
clicks, and pre-caches the window module so the first drag does not stick.
|
|
48
|
+
Inert outside Tauri desktop, so one Layout serves the web build too.
|
|
42
49
|
- **`@whiskeyjack-net/tauri/css/window-controls`** – the window-control CSS
|
|
43
50
|
(import once); pairs with the components and the `AppHeader`
|
|
44
51
|
`chrome`/`rowClassName` extension points.
|
|
@@ -64,11 +71,43 @@ off Tauri, so it is safe to ship in a plain web/PWA build.
|
|
|
64
71
|
`update.error`, `common.cancel`; the window controls read `window.close` /
|
|
65
72
|
`window.minimize` / `window.maximize`.
|
|
66
73
|
|
|
74
|
+
## The two commands `useSystemAccent` expects
|
|
75
|
+
|
|
76
|
+
The Rust side is yours (see below), and these two are the part the pack
|
|
77
|
+
actually calls. **Neither is required for a correct window** – missing ones fall
|
|
78
|
+
back to the platform default and warn once in development – but implementing
|
|
79
|
+
them is what makes the OS accent and the per-desktop Linux button layout work.
|
|
80
|
+
|
|
81
|
+
| Command | Returns | Without it |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| `get_window_controls_layout` | `{ left: string[], right: string[], native: bool }` | macOS assumes native traffic lights, Windows and Linux assume a right-hand `minimize, maximize, close`. Only Linux genuinely loses something: its layout is a per-desktop setting that only the backend can read. |
|
|
84
|
+
| `get_system_accent_color` | `Option<String>`, `#rrggbb` | The app keeps its own theme accent. |
|
|
85
|
+
|
|
86
|
+
macOS needs only this much:
|
|
87
|
+
|
|
88
|
+
```rust
|
|
89
|
+
#[derive(serde::Serialize)]
|
|
90
|
+
pub struct WindowControlsLayout { left: Vec<String>, right: Vec<String>, native: bool }
|
|
91
|
+
|
|
92
|
+
#[tauri::command]
|
|
93
|
+
pub fn get_window_controls_layout() -> WindowControlsLayout {
|
|
94
|
+
WindowControlsLayout { left: vec![], right: vec![], native: true }
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Register it in `invoke_handler!`. Chip Away's `src-tauri/src/window_controls.rs`
|
|
99
|
+
is the full version, including reading GNOME and Pantheon button layouts.
|
|
100
|
+
|
|
101
|
+
Before the fallback existed, an app that skipped these got no
|
|
102
|
+
`.tauri-controls-*` class, so `.tauri-pad-controls` resolved to zero padding and
|
|
103
|
+
the header sat underneath the macOS traffic lights, with nothing logged
|
|
104
|
+
anywhere. That is the failure the defaults and the dev warning replace.
|
|
105
|
+
|
|
67
106
|
## Not included (by design)
|
|
68
107
|
|
|
69
|
-
- The **Rust** side (`src-tauri/`: the
|
|
70
|
-
|
|
71
|
-
|
|
108
|
+
- The **Rust** side (`src-tauri/`: the two commands above, tray/menus,
|
|
109
|
+
notifications) is an **owned scaffold template**, seeded from a reference app
|
|
110
|
+
– not an npm library.
|
|
72
111
|
- **Auth/sync** is app-coupled (Firebase) and lives in the app or a future
|
|
73
112
|
starter kit.
|
|
74
113
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { isTauri, isMobileTauri, isDesktopTauri, isMacDesktop, isLinuxDesktop } from './desktop';
|
|
2
2
|
export { WindowControlsLeft, WindowControlsRight } from './window-controls';
|
|
3
3
|
export { useSystemAccent } from './use-system-accent';
|
|
4
|
+
export { useWindowDrag } from './use-window-drag';
|
|
5
|
+
export type { WindowDragProps } from './use-window-drag';
|
|
4
6
|
export { openExternal } from './open-external';
|
|
5
7
|
export { createUpdater } from './updater';
|
|
6
8
|
export type { Updater, UpdaterConfig, UpdateResult } from './updater';
|
package/dist/index.js
CHANGED
|
@@ -135,16 +135,29 @@ function useSystemAccent() {
|
|
|
135
135
|
root.style.setProperty("--color-accent-500", hex);
|
|
136
136
|
root.style.setProperty("--color-accent-600", adjustBrightness(r, g, b, -0.12));
|
|
137
137
|
root.style.setProperty("--color-accent-700", adjustBrightness(r, g, b, -0.25));
|
|
138
|
-
} catch {
|
|
138
|
+
} catch (err) {
|
|
139
|
+
warnOnce(
|
|
140
|
+
"get_system_accent_color",
|
|
141
|
+
"the app keeps its theme accent instead of the OS one",
|
|
142
|
+
err
|
|
143
|
+
);
|
|
139
144
|
}
|
|
140
145
|
}
|
|
141
146
|
applyAccent();
|
|
142
147
|
async function applyWindowControls() {
|
|
148
|
+
let layout;
|
|
143
149
|
try {
|
|
144
150
|
const { invoke } = await import("@tauri-apps/api/core");
|
|
145
|
-
|
|
146
|
-
|
|
151
|
+
layout = await invoke("get_window_controls_layout");
|
|
152
|
+
} catch (err) {
|
|
153
|
+
layout = platformDefaultLayout();
|
|
154
|
+
warnOnce(
|
|
155
|
+
"get_window_controls_layout",
|
|
156
|
+
"window controls fall back to the platform default; implement the command to read the desktop\u2019s own button layout",
|
|
157
|
+
err
|
|
147
158
|
);
|
|
159
|
+
}
|
|
160
|
+
{
|
|
148
161
|
const root = document.documentElement;
|
|
149
162
|
root.setAttribute("data-wc-left", layout.left.join(","));
|
|
150
163
|
root.setAttribute("data-wc-right", layout.right.join(","));
|
|
@@ -160,7 +173,6 @@ function useSystemAccent() {
|
|
|
160
173
|
root.style.setProperty("--wc-left-px", `${footprint(layout.left.length)}px`);
|
|
161
174
|
root.style.setProperty("--wc-right-px", `${footprint(layout.right.length)}px`);
|
|
162
175
|
}
|
|
163
|
-
} catch {
|
|
164
176
|
}
|
|
165
177
|
}
|
|
166
178
|
applyWindowControls();
|
|
@@ -170,6 +182,27 @@ function useSystemAccent() {
|
|
|
170
182
|
};
|
|
171
183
|
}, []);
|
|
172
184
|
}
|
|
185
|
+
function platformDefaultLayout() {
|
|
186
|
+
const ua = navigator.userAgent;
|
|
187
|
+
if (ua.includes("Mac")) return { left: [], right: [], native: true };
|
|
188
|
+
return { left: [], right: ["minimize", "maximize", "close"], native: false };
|
|
189
|
+
}
|
|
190
|
+
var warned = /* @__PURE__ */ new Set();
|
|
191
|
+
function warnOnce(command, consequence, err) {
|
|
192
|
+
if (warned.has(command)) return;
|
|
193
|
+
warned.add(command);
|
|
194
|
+
let dev = false;
|
|
195
|
+
try {
|
|
196
|
+
dev = Boolean(import.meta.env?.DEV);
|
|
197
|
+
} catch {
|
|
198
|
+
}
|
|
199
|
+
if (!dev) return;
|
|
200
|
+
console.warn(
|
|
201
|
+
`[@whiskeyjack-net/tauri] the Tauri command \`${command}\` is not registered, so ${consequence}.
|
|
202
|
+
Add it to your src-tauri invoke_handler. See the package README.`,
|
|
203
|
+
err
|
|
204
|
+
);
|
|
205
|
+
}
|
|
173
206
|
function adjustBrightness(r, g, b, amount) {
|
|
174
207
|
const adjust = (c) => {
|
|
175
208
|
if (amount > 0) {
|
|
@@ -184,6 +217,34 @@ function adjustBrightness(r, g, b, amount) {
|
|
|
184
217
|
return `#${rr.toString(16).padStart(2, "0")}${gg.toString(16).padStart(2, "0")}${bb.toString(16).padStart(2, "0")}`;
|
|
185
218
|
}
|
|
186
219
|
|
|
220
|
+
// src/use-window-drag.ts
|
|
221
|
+
import { useCallback, useEffect as useEffect3 } from "react";
|
|
222
|
+
var windowModule = null;
|
|
223
|
+
function loadWindowModule() {
|
|
224
|
+
windowModule ?? (windowModule = import("@tauri-apps/api/window"));
|
|
225
|
+
return windowModule;
|
|
226
|
+
}
|
|
227
|
+
var INTERACTIVE = 'a, button, input, select, textarea, nav, [role="tab"], [role="button"], [contenteditable]';
|
|
228
|
+
function useWindowDrag() {
|
|
229
|
+
useEffect3(() => {
|
|
230
|
+
if (!isTauri() || isMobileTauri()) return;
|
|
231
|
+
void loadWindowModule().catch(() => {
|
|
232
|
+
});
|
|
233
|
+
}, []);
|
|
234
|
+
const onMouseDown = useCallback((event) => {
|
|
235
|
+
if (!isTauri() || isMobileTauri()) return;
|
|
236
|
+
if (event.button !== 0) return;
|
|
237
|
+
if (event.target?.closest(INTERACTIVE)) return;
|
|
238
|
+
event.preventDefault();
|
|
239
|
+
void loadWindowModule().then(({ getCurrentWindow }) => {
|
|
240
|
+
const win = getCurrentWindow();
|
|
241
|
+
return event.detail === 2 ? win.toggleMaximize() : win.startDragging();
|
|
242
|
+
}).catch(() => {
|
|
243
|
+
});
|
|
244
|
+
}, []);
|
|
245
|
+
return { onMouseDown };
|
|
246
|
+
}
|
|
247
|
+
|
|
187
248
|
// src/open-external.ts
|
|
188
249
|
async function openExternal(url) {
|
|
189
250
|
if ("__TAURI_INTERNALS__" in window) {
|
|
@@ -286,7 +347,7 @@ function UpdateBanner({ version, releaseUrl, onDismiss }) {
|
|
|
286
347
|
}
|
|
287
348
|
|
|
288
349
|
// src/update-dialog.tsx
|
|
289
|
-
import { useState as useState2, useCallback } from "react";
|
|
350
|
+
import { useState as useState2, useCallback as useCallback2 } from "react";
|
|
290
351
|
import { useTranslation as useTranslation3 } from "react-i18next";
|
|
291
352
|
import { ArrowSquareOut as ArrowSquareOut2, ArrowsClockwise, CheckCircle, Warning } from "@phosphor-icons/react";
|
|
292
353
|
import { Button as Button2 } from "@whiskeyjack-net/design-system";
|
|
@@ -296,7 +357,7 @@ function UpdateDialog({ updater, onClose }) {
|
|
|
296
357
|
const [state, setState] = useState2("idle");
|
|
297
358
|
const [result, setResult] = useState2(null);
|
|
298
359
|
const [currentVersion, setCurrentVersion] = useState2("");
|
|
299
|
-
const handleCheck =
|
|
360
|
+
const handleCheck = useCallback2(async () => {
|
|
300
361
|
setState("checking");
|
|
301
362
|
const [version, updateResult] = await Promise.all([
|
|
302
363
|
updater.getAppVersion(),
|
|
@@ -355,6 +416,7 @@ export {
|
|
|
355
416
|
isMobileTauri,
|
|
356
417
|
isTauri,
|
|
357
418
|
openExternal,
|
|
358
|
-
useSystemAccent
|
|
419
|
+
useSystemAccent,
|
|
420
|
+
useWindowDrag
|
|
359
421
|
};
|
|
360
422
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/desktop.ts","../src/window-controls.tsx","../src/use-system-accent.ts","../src/open-external.ts","../src/updater.ts","../src/update-banner.tsx","../src/update-dialog.tsx"],"sourcesContent":["// Tauri environment guards. `__TAURI_INTERNALS__` is present on mobile Tauri\n// too, so the desktop shell (window controls, CSD drag zone, OS-accent read)\n// must gate on DESKTOP explicitly -- otherwise its desktop-only CSS leaks onto\n// iOS/Android.\nexport const isTauri = (): boolean => '__TAURI_INTERNALS__' in window\n\n/**\n * Tauri mobile build (iOS/Android). iPads can report a \"Macintosh\" UA in\n * WKWebView, so the touch-point check keeps them off the macOS/desktop path.\n */\nexport const isMobileTauri = (): boolean =>\n isTauri() &&\n (/android|iphone|ipad|ipod/i.test(navigator.userAgent) ||\n (navigator.userAgent.includes('Mac') && navigator.maxTouchPoints > 1))\n\nexport const isDesktopTauri = (): boolean => isTauri() && !isMobileTauri()\n\n/** macOS desktop (draws OS traffic lights rather than custom window controls). */\nexport const isMacDesktop = (): boolean =>\n navigator.userAgent.includes('Mac') && !isMobileTauri()\n\n/**\n * Linux desktop -- the platform whose window is undecorated + `transparent`,\n * with rounded corners done purely in CSS (see `tauri-platform-linux` in\n * `use-system-accent.ts`). Anything that would make the canvas opaque has to\n * gate on this.\n *\n * Android's user agent also says \"Linux\", so `isMobileTauri()` has to be\n * excluded rather than assumed away.\n */\nexport const isLinuxDesktop = (): boolean =>\n navigator.userAgent.includes('Linux') && !isMobileTauri()\n","import { useState, useEffect } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport type { Window } from \"@tauri-apps/api/window\"\nimport { isTauri } from \"./desktop\"\n\n/**\n * Reads the per-side window-control button lists the backend detected (set as\n * `data-wc-left` / `data-wc-right` on <html> by useSystemAccent, comma-separated\n * close/minimize/maximize). Rendering the exact buttons per side lets the app\n * match the desktop's own convention (e.g. close-only on stock GNOME, the split\n * close:maximize on elementary, min/max/close on KDE/Windows).\n */\nfunction useWindowControls() {\n const [left, setLeft] = useState<string[]>([])\n const [right, setRight] = useState<string[]>([])\n const [windowMod, setWindowMod] = useState<typeof import('@tauri-apps/api/window') | null>(null)\n\n useEffect(() => {\n if (!isTauri()) return\n\n const parse = (v: string | null): string[] =>\n (v ?? '').split(',').map((s) => s.trim()).filter(Boolean)\n\n const read = () => {\n const root = document.documentElement\n setLeft(parse(root.getAttribute('data-wc-left')))\n setRight(parse(root.getAttribute('data-wc-right')))\n }\n\n read()\n // The attributes are set asynchronously after the backend responds.\n const observer = new MutationObserver(read)\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: ['data-wc-left', 'data-wc-right'],\n })\n return () => observer.disconnect()\n }, [])\n\n useEffect(() => {\n if (!isTauri()) return\n import('@tauri-apps/api/window').then(setWindowMod)\n }, [])\n\n return { left, right, windowMod }\n}\n\nfunction ControlButton({ kind, win }: { kind: string; win: Window }) {\n const { t } = useTranslation()\n switch (kind) {\n case 'close':\n return (\n <button\n onClick={() => win.close()}\n aria-label={t('window.close')}\n title={t('window.close')}\n className=\"window-control window-control-close\"\n >\n ✕\n </button>\n )\n case 'minimize':\n return (\n <button\n onClick={() => win.minimize()}\n aria-label={t('window.minimize')}\n title={t('window.minimize')}\n className=\"window-control\"\n >\n −\n </button>\n )\n case 'maximize':\n return (\n <button\n onClick={() => win.toggleMaximize()}\n aria-label={t('window.maximize')}\n title={t('window.maximize')}\n className=\"window-control\"\n >\n ▢\n </button>\n )\n default:\n return null\n }\n}\n\nexport function WindowControlsLeft() {\n const { left, windowMod } = useWindowControls()\n if (!windowMod || left.length === 0) return null\n const win = windowMod.getCurrentWindow()\n return (\n <div className=\"window-controls\">\n {left.map((kind) => (\n <ControlButton key={kind} kind={kind} win={win} />\n ))}\n </div>\n )\n}\n\nexport function WindowControlsRight() {\n const { right, windowMod } = useWindowControls()\n if (!windowMod || right.length === 0) return null\n const win = windowMod.getCurrentWindow()\n return (\n <div className=\"window-controls\">\n {right.map((kind) => (\n <ControlButton key={kind} kind={kind} win={win} />\n ))}\n </div>\n )\n}\n","import { useEffect } from 'react'\nimport { isTauri, isMobileTauri } from './desktop'\n\n/**\n * In Tauri desktop builds, fetch the system accent color and override\n * the design-system accent CSS variables so the app matches the OS theme.\n */\nexport function useSystemAccent() {\n useEffect(() => {\n if (!isTauri()) return\n // `__TAURI_INTERNALS__` is present on mobile Tauri too, so this hook -- the\n // window controls, the CSD drag zone, the OS-accent read, and the\n // `.tauri-desktop` marker class -- must gate on DESKTOP explicitly. Without\n // this, `.tauri-desktop` lands on iOS/Android and its desktop-only CSS (e.g.\n // the drag-zone tab-clearance padding) leaks onto mobile. Mobile accent is\n // handled in Layout.\n if (isMobileTauri()) return\n\n // Mark the document so CSS can target Tauri-specific styles\n document.documentElement.classList.add('tauri-desktop')\n\n // Add platform class for platform-specific control styling\n if (navigator.userAgent.includes('Windows')) {\n document.documentElement.classList.add('tauri-platform-windows')\n }\n\n // Linux desktop: the window is undecorated + transparent (decorations:false\n // + transparent:true in tauri.linux.conf.json) and its corners are rounded\n // entirely in CSS -- `.tauri-platform-linux #root` carries the border-radius\n // and clips to genuine window alpha (see index.css); no native GTK decoration\n // is involved. Mark the platform so that CSS applies, and track the maximized\n // state so the radius drops to 0 while maximized/tiled -- the app goes\n // edge-to-edge then, like every other windowed app (mobile Tauri is already\n // excluded above, so \"Linux\" here means desktop).\n let unlistenResized: (() => void) | undefined\n let disposed = false\n if (navigator.userAgent.includes('Linux')) {\n document.documentElement.classList.add('tauri-platform-linux')\n import('@tauri-apps/api/window').then(({ getCurrentWindow }) => {\n const win = getCurrentWindow()\n const syncMaximized = async () => {\n try {\n document.documentElement.classList.toggle(\n 'tauri-window-maximized',\n await win.isMaximized(),\n )\n } catch {\n // ignore -- corners just stay rounded\n }\n }\n syncMaximized()\n // onResized registers asynchronously; if the effect already tore down by\n // the time it resolves, unlisten immediately instead of leaking it.\n win.onResized(syncMaximized).then((unlisten) => {\n if (disposed) unlisten()\n else unlistenResized = unlisten\n })\n })\n }\n\n async function applyAccent() {\n try {\n const { invoke } = await import('@tauri-apps/api/core')\n const hex = await invoke<string | null>('get_system_accent_color')\n if (!hex) return\n\n const r = parseInt(hex.slice(1, 3), 16)\n const g = parseInt(hex.slice(3, 5), 16)\n const b = parseInt(hex.slice(5, 7), 16)\n\n const root = document.documentElement\n // Derive lighter/darker shades from the accent\n root.style.setProperty('--color-accent-50', adjustBrightness(r, g, b, 0.95))\n root.style.setProperty('--color-accent-100', adjustBrightness(r, g, b, 0.88))\n root.style.setProperty('--color-accent-400', adjustBrightness(r, g, b, 0.15))\n root.style.setProperty('--color-accent-500', hex)\n root.style.setProperty('--color-accent-600', adjustBrightness(r, g, b, -0.12))\n root.style.setProperty('--color-accent-700', adjustBrightness(r, g, b, -0.25))\n } catch {\n // Silently fall back to default accent\n }\n }\n\n applyAccent()\n\n async function applyWindowControls() {\n try {\n const { invoke } = await import('@tauri-apps/api/core')\n const layout = await invoke<{ left: string[]; right: string[]; native: boolean }>(\n 'get_window_controls_layout',\n )\n const root = document.documentElement\n // Per-side button lists drive which controls WindowControls renders, in\n // order (matches the desktop's own convention -- see window_controls.rs).\n root.setAttribute('data-wc-left', layout.left.join(','))\n root.setAttribute('data-wc-right', layout.right.join(','))\n // The side class drives the action-pill clearance padding (index.css).\n root.classList.remove('tauri-controls-left', 'tauri-controls-right', 'tauri-controls-both', 'tauri-controls-native')\n const side = layout.native\n ? 'native'\n : layout.left.length && layout.right.length\n ? 'both'\n : layout.left.length\n ? 'left'\n : layout.right.length\n ? 'right'\n : null\n if (side) root.classList.add(`tauri-controls-${side}`)\n\n // Publish each side's control footprint (px from the window edge to the\n // controls' inner edge) so the CSS clears them CONTINUOUSLY (index.css)\n // rather than via fixed width breakpoints -- exact for any layout and\n // any platform. Sizes mirror .window-control: Windows controls are 46px\n // wide flush to the edge; the custom (Linux) controls are 28px with a 6px\n // gap, inset 16px (left-4/right-4).\n const isWindows = navigator.userAgent.includes('Windows')\n const footprint = (count: number): number =>\n count === 0 ? 0 : isWindows ? count * 46 : 16 + count * 28 + (count - 1) * 6\n if (layout.native) {\n // macOS draws OS traffic lights (the app renders no custom controls, so\n // both button lists are empty). They sit on the LEFT and never vary in\n // count, so publish a fixed 64px left footprint (+1rem gap = the 80px\n // clearance used before) and let macOS ride the SAME continuous formula\n // as Linux/Windows -- fluid clearance instead of a width breakpoint.\n root.style.setProperty('--wc-left-px', '64px')\n root.style.setProperty('--wc-right-px', '0px')\n } else {\n root.style.setProperty('--wc-left-px', `${footprint(layout.left.length)}px`)\n root.style.setProperty('--wc-right-px', `${footprint(layout.right.length)}px`)\n }\n } catch {\n // Fall back to no custom controls / no extra padding\n }\n }\n\n applyWindowControls()\n\n return () => {\n disposed = true\n unlistenResized?.()\n }\n }, [])\n}\n\n/** Lighten (positive amount) or darken (negative amount) an RGB color. */\nfunction adjustBrightness(r: number, g: number, b: number, amount: number): string {\n const adjust = (c: number) => {\n if (amount > 0) {\n // Lighten: blend toward white\n return Math.round(c + (255 - c) * amount)\n }\n // Darken: blend toward black\n return Math.round(c * (1 + amount))\n }\n\n const clamp = (v: number) => Math.max(0, Math.min(255, v))\n const rr = clamp(adjust(r))\n const gg = clamp(adjust(g))\n const bb = clamp(adjust(b))\n\n return `#${rr.toString(16).padStart(2, '0')}${gg.toString(16).padStart(2, '0')}${bb.toString(16).padStart(2, '0')}`\n}\n","/**\n * Open a URL outside the app: the system browser on Tauri (desktop and mobile,\n * via the opener plugin -- window.open is a no-op in the Tauri WebView), a new\n * tab on the web.\n *\n * Requires the `@tauri-apps/plugin-opener` peer plus its capability grant in the\n * Tauri app (`opener:allow-open-url`).\n */\nexport async function openExternal(url: string) {\n if ('__TAURI_INTERNALS__' in window) {\n const { openUrl } = await import('@tauri-apps/plugin-opener')\n await openUrl(url)\n } else {\n window.open(url, '_blank', 'noopener,noreferrer')\n }\n}\n","/**\n * Desktop in-app update checker for direct-download Tauri builds. Polls a\n * GitHub repo's releases, matches this app's tag prefix, compares semver\n * against the running version, and resolves the best installer asset for the\n * current platform. Store-distributed builds (App Store, Snap, ...) update\n * through the store and should never call this.\n */\n\nexport interface UpdateResult {\n status: 'update-available' | 'up-to-date' | 'error'\n latestVersion?: string\n releaseUrl?: string\n downloadUrl?: string\n}\n\nexport interface UpdaterConfig {\n /** GitHub `owner/repo` whose releases list is polled. */\n repo: string\n /** Tag prefix marking this app's releases, e.g. `chip-away-v`. */\n tagPrefix: string\n /**\n * Version used when the app is not running under Tauri (the web/PWA build,\n * where `@tauri-apps/api/app` is unavailable) -- typically the build-time\n * version constant. Under Tauri the real version comes from the runtime.\n */\n fallbackVersion: string\n /** Releases page size to fetch (default 10). */\n perPage?: number\n}\n\nexport interface Updater {\n /** The running app's version (Tauri runtime, else `fallbackVersion`). */\n getAppVersion(): Promise<string>\n /** Check the configured repo for a newer release. */\n checkForUpdates(): Promise<UpdateResult>\n}\n\ninterface GitHubAsset {\n name: string\n browser_download_url: string\n}\n\ninterface GitHubRelease {\n tag_name: string\n html_url: string\n draft: boolean\n assets: GitHubAsset[]\n}\n\n/** Compare two semver strings. Positive if a > b, negative if a < b, 0 if equal. */\nfunction compareSemver(a: string, b: string): number {\n const aParts = a.split('.').map(Number)\n const bParts = b.split('.').map(Number)\n const len = Math.max(aParts.length, bParts.length)\n\n for (let i = 0; i < len; i++) {\n const av = i < aParts.length ? aParts[i] : 0\n const bv = i < bParts.length ? bParts[i] : 0\n if (av !== bv) return av - bv\n }\n return 0\n}\n\n/** Find the best download URL for the current platform. */\nfunction getDownloadUrlForPlatform(assets: GitHubAsset[]): string | undefined {\n const ua = navigator.userAgent.toLowerCase()\n let patterns: string[]\n\n if (ua.includes('mac')) {\n patterns = ['.dmg']\n } else if (ua.includes('win')) {\n patterns = ['.msi', '-setup.exe', '.exe']\n } else {\n patterns = ['.appimage', '.deb']\n }\n\n for (const pattern of patterns) {\n const match = assets.find((a) => a.name.toLowerCase().endsWith(pattern))\n if (match) return match.browser_download_url\n }\n return undefined\n}\n\n/**\n * Build an updater bound to one app's release config. Give it the app's repo,\n * tag prefix, and build-time version:\n *\n * ```ts\n * export const updater = createUpdater({\n * repo: 'whiskeyjack-net/downloads',\n * tagPrefix: 'chip-away-v',\n * fallbackVersion: __APP_VERSION__,\n * })\n * ```\n */\nexport function createUpdater(config: UpdaterConfig): Updater {\n const releasesUrl = `https://api.github.com/repos/${config.repo}/releases?per_page=${config.perPage ?? 10}`\n\n async function getAppVersion(): Promise<string> {\n if ('__TAURI_INTERNALS__' in window) {\n try {\n const { getVersion } = await import('@tauri-apps/api/app')\n return await getVersion()\n } catch {\n // Fall through to the configured fallback.\n }\n }\n return config.fallbackVersion\n }\n\n async function checkForUpdates(): Promise<UpdateResult> {\n try {\n const response = await fetch(releasesUrl, {\n headers: { Accept: 'application/vnd.github+json' },\n signal: AbortSignal.timeout(15000),\n })\n\n if (!response.ok) return { status: 'error' }\n\n const releases: GitHubRelease[] = await response.json()\n\n // Latest release matching our tag prefix, skipping drafts.\n const matching = releases.filter(\n (r) => r.tag_name.startsWith(config.tagPrefix) && !r.draft,\n )\n\n if (matching.length === 0) return { status: 'up-to-date' }\n\n const latest = matching[0]\n const remoteVersion = latest.tag_name.slice(config.tagPrefix.length)\n const currentVersion = await getAppVersion()\n\n if (compareSemver(remoteVersion, currentVersion) > 0) {\n return {\n status: 'update-available',\n latestVersion: remoteVersion,\n releaseUrl: latest.html_url,\n downloadUrl: getDownloadUrlForPlatform(latest.assets) ?? latest.html_url,\n }\n }\n\n return { status: 'up-to-date' }\n } catch {\n return { status: 'error' }\n }\n }\n\n return { getAppVersion, checkForUpdates }\n}\n","import { useTranslation } from 'react-i18next'\nimport { ArrowSquareOut } from '@phosphor-icons/react'\nimport { Button, Toast } from '@whiskeyjack-net/design-system'\n\ninterface UpdateBannerProps {\n version: string\n releaseUrl: string\n onDismiss: () => void\n}\n\n/**\n * \"Update available\" notification for the desktop build. A persistent DS\n * Toast (floating below the header) with a download action and a dismiss\n * button -- the caller unmounts it on dismiss, so `open` is always true.\n *\n * Requires the app's locales to define `update.available` (interpolates\n * `{{version}}`), `update.download`, and `update.later`.\n */\nexport function UpdateBanner({ version, releaseUrl, onDismiss }: UpdateBannerProps) {\n const { t } = useTranslation()\n\n return (\n <Toast\n open\n icon={<ArrowSquareOut size={16} weight=\"bold\" className=\"text-[var(--color-accent-500)]\" />}\n onDismiss={onDismiss}\n dismissLabel={t('update.later')}\n action={\n <a href={releaseUrl} target=\"_blank\" rel=\"noopener noreferrer\" className=\"shrink-0\">\n <Button variant=\"accent\">{t('update.download')}</Button>\n </a>\n }\n >\n {t('update.available', { version })}\n </Toast>\n )\n}\n","import { useState, useCallback } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { ArrowSquareOut, ArrowsClockwise, CheckCircle, Warning } from '@phosphor-icons/react'\nimport { Button } from '@whiskeyjack-net/design-system'\nimport { openExternal } from './open-external'\nimport type { Updater, UpdateResult } from './updater'\n\ninterface UpdateDialogProps {\n /** The app's updater (from `createUpdater`); drives the check. */\n updater: Updater\n onClose: () => void\n}\n\n/**\n * \"Check for updates\" dialog body for the desktop build (mount inside a DS\n * BottomDrawer). Auto-runs the check on first render, then shows the result\n * with a download or up-to-date state.\n *\n * Requires the app's locales to define `update.checking`, `update.available`\n * (interpolates `{{version}}`), `update.currentVersion` (interpolates\n * `{{version}}`), `update.upToDate`, `update.download`, `update.later`,\n * `update.error`, and `common.cancel`.\n */\nexport function UpdateDialog({ updater, onClose }: UpdateDialogProps) {\n const { t } = useTranslation()\n const [state, setState] = useState<'idle' | 'checking' | 'done'>('idle')\n const [result, setResult] = useState<UpdateResult | null>(null)\n const [currentVersion, setCurrentVersion] = useState<string>('')\n\n const handleCheck = useCallback(async () => {\n setState('checking')\n const [version, updateResult] = await Promise.all([\n updater.getAppVersion(),\n updater.checkForUpdates(),\n ])\n setCurrentVersion(version)\n setResult(updateResult)\n setState('done')\n }, [updater])\n\n // Auto-trigger check on first render\n if (state === 'idle') {\n handleCheck()\n }\n\n return (\n <div className=\"flex flex-col items-center gap-4 py-2\">\n {state === 'checking' && (\n <>\n <ArrowsClockwise size={40} weight=\"bold\" className=\"text-[var(--color-accent-500)] animate-spin\" />\n <p className=\"text-sm text-[var(--color-text-secondary-light)] dark:text-[var(--color-text-secondary-dark)]\">\n {t('update.checking')}\n </p>\n </>\n )}\n\n {state === 'done' && result?.status === 'update-available' && (\n <>\n <ArrowsClockwise size={40} weight=\"bold\" className=\"text-[var(--color-accent-500)]\" />\n <div className=\"text-center\">\n <p className=\"text-sm font-medium text-[var(--color-text-primary-light)] dark:text-[var(--color-text-primary-dark)]\">\n {t('update.available', { version: result.latestVersion })}\n </p>\n <p className=\"text-xs text-[var(--color-text-muted-light)] dark:text-[var(--color-text-muted-dark)] mt-1\">\n {t('update.currentVersion', { version: currentVersion })}\n </p>\n </div>\n <div className=\"flex gap-3\">\n <Button variant=\"accent\" onClick={() => openExternal(result.downloadUrl || result.releaseUrl || '')}>\n <ArrowSquareOut size={16} weight=\"bold\" />\n {t('update.download')}\n </Button>\n <Button variant=\"outline\" onClick={onClose}>\n {t('update.later')}\n </Button>\n </div>\n </>\n )}\n\n {state === 'done' && result?.status === 'up-to-date' && (\n <>\n <CheckCircle size={40} weight=\"fill\" className=\"text-[var(--color-success-500)]\" />\n <div className=\"text-center\">\n <p className=\"text-sm font-medium text-[var(--color-text-primary-light)] dark:text-[var(--color-text-primary-dark)]\">\n {t('update.upToDate')}\n </p>\n <p className=\"text-xs text-[var(--color-text-muted-light)] dark:text-[var(--color-text-muted-dark)] mt-1\">\n {t('update.currentVersion', { version: currentVersion })}\n </p>\n </div>\n <Button variant=\"outline\" onClick={onClose}>\n {t('common.cancel')}\n </Button>\n </>\n )}\n\n {state === 'done' && result?.status === 'error' && (\n <>\n <Warning size={40} weight=\"fill\" className=\"text-[var(--color-warning-500)]\" />\n <p className=\"text-sm text-[var(--color-text-secondary-light)] dark:text-[var(--color-text-secondary-dark)]\">\n {t('update.error')}\n </p>\n <Button variant=\"outline\" onClick={onClose}>\n {t('common.cancel')}\n </Button>\n </>\n )}\n </div>\n )\n}\n"],"mappings":"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","names":["useEffect","useEffect","useTranslation","jsx","useState","useTranslation","ArrowSquareOut","Button","jsx","useTranslation","useState","Button","ArrowSquareOut"]}
|
|
1
|
+
{"version":3,"sources":["../src/desktop.ts","../src/window-controls.tsx","../src/use-system-accent.ts","../src/use-window-drag.ts","../src/open-external.ts","../src/updater.ts","../src/update-banner.tsx","../src/update-dialog.tsx"],"sourcesContent":["// Tauri environment guards. `__TAURI_INTERNALS__` is present on mobile Tauri\n// too, so the desktop shell (window controls, CSD drag zone, OS-accent read)\n// must gate on DESKTOP explicitly -- otherwise its desktop-only CSS leaks onto\n// iOS/Android.\nexport const isTauri = (): boolean => '__TAURI_INTERNALS__' in window\n\n/**\n * Tauri mobile build (iOS/Android). iPads can report a \"Macintosh\" UA in\n * WKWebView, so the touch-point check keeps them off the macOS/desktop path.\n */\nexport const isMobileTauri = (): boolean =>\n isTauri() &&\n (/android|iphone|ipad|ipod/i.test(navigator.userAgent) ||\n (navigator.userAgent.includes('Mac') && navigator.maxTouchPoints > 1))\n\nexport const isDesktopTauri = (): boolean => isTauri() && !isMobileTauri()\n\n/** macOS desktop (draws OS traffic lights rather than custom window controls). */\nexport const isMacDesktop = (): boolean =>\n navigator.userAgent.includes('Mac') && !isMobileTauri()\n\n/**\n * Linux desktop -- the platform whose window is undecorated + `transparent`,\n * with rounded corners done purely in CSS (see `tauri-platform-linux` in\n * `use-system-accent.ts`). Anything that would make the canvas opaque has to\n * gate on this.\n *\n * Android's user agent also says \"Linux\", so `isMobileTauri()` has to be\n * excluded rather than assumed away.\n */\nexport const isLinuxDesktop = (): boolean =>\n navigator.userAgent.includes('Linux') && !isMobileTauri()\n","import { useState, useEffect } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport type { Window } from \"@tauri-apps/api/window\"\nimport { isTauri } from \"./desktop\"\n\n/**\n * Reads the per-side window-control button lists the backend detected (set as\n * `data-wc-left` / `data-wc-right` on <html> by useSystemAccent, comma-separated\n * close/minimize/maximize). Rendering the exact buttons per side lets the app\n * match the desktop's own convention (e.g. close-only on stock GNOME, the split\n * close:maximize on elementary, min/max/close on KDE/Windows).\n */\nfunction useWindowControls() {\n const [left, setLeft] = useState<string[]>([])\n const [right, setRight] = useState<string[]>([])\n const [windowMod, setWindowMod] = useState<typeof import('@tauri-apps/api/window') | null>(null)\n\n useEffect(() => {\n if (!isTauri()) return\n\n const parse = (v: string | null): string[] =>\n (v ?? '').split(',').map((s) => s.trim()).filter(Boolean)\n\n const read = () => {\n const root = document.documentElement\n setLeft(parse(root.getAttribute('data-wc-left')))\n setRight(parse(root.getAttribute('data-wc-right')))\n }\n\n read()\n // The attributes are set asynchronously after the backend responds.\n const observer = new MutationObserver(read)\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: ['data-wc-left', 'data-wc-right'],\n })\n return () => observer.disconnect()\n }, [])\n\n useEffect(() => {\n if (!isTauri()) return\n import('@tauri-apps/api/window').then(setWindowMod)\n }, [])\n\n return { left, right, windowMod }\n}\n\nfunction ControlButton({ kind, win }: { kind: string; win: Window }) {\n const { t } = useTranslation()\n switch (kind) {\n case 'close':\n return (\n <button\n onClick={() => win.close()}\n aria-label={t('window.close')}\n title={t('window.close')}\n className=\"window-control window-control-close\"\n >\n ✕\n </button>\n )\n case 'minimize':\n return (\n <button\n onClick={() => win.minimize()}\n aria-label={t('window.minimize')}\n title={t('window.minimize')}\n className=\"window-control\"\n >\n −\n </button>\n )\n case 'maximize':\n return (\n <button\n onClick={() => win.toggleMaximize()}\n aria-label={t('window.maximize')}\n title={t('window.maximize')}\n className=\"window-control\"\n >\n ▢\n </button>\n )\n default:\n return null\n }\n}\n\nexport function WindowControlsLeft() {\n const { left, windowMod } = useWindowControls()\n if (!windowMod || left.length === 0) return null\n const win = windowMod.getCurrentWindow()\n return (\n <div className=\"window-controls\">\n {left.map((kind) => (\n <ControlButton key={kind} kind={kind} win={win} />\n ))}\n </div>\n )\n}\n\nexport function WindowControlsRight() {\n const { right, windowMod } = useWindowControls()\n if (!windowMod || right.length === 0) return null\n const win = windowMod.getCurrentWindow()\n return (\n <div className=\"window-controls\">\n {right.map((kind) => (\n <ControlButton key={kind} kind={kind} win={win} />\n ))}\n </div>\n )\n}\n","import { useEffect } from 'react'\nimport { isTauri, isMobileTauri } from './desktop'\n\n/**\n * In Tauri desktop builds, fetch the system accent color and override\n * the design-system accent CSS variables so the app matches the OS theme.\n */\nexport function useSystemAccent() {\n useEffect(() => {\n if (!isTauri()) return\n // `__TAURI_INTERNALS__` is present on mobile Tauri too, so this hook -- the\n // window controls, the CSD drag zone, the OS-accent read, and the\n // `.tauri-desktop` marker class -- must gate on DESKTOP explicitly. Without\n // this, `.tauri-desktop` lands on iOS/Android and its desktop-only CSS (e.g.\n // the drag-zone tab-clearance padding) leaks onto mobile. Mobile accent is\n // handled in Layout.\n if (isMobileTauri()) return\n\n // Mark the document so CSS can target Tauri-specific styles\n document.documentElement.classList.add('tauri-desktop')\n\n // Add platform class for platform-specific control styling\n if (navigator.userAgent.includes('Windows')) {\n document.documentElement.classList.add('tauri-platform-windows')\n }\n\n // Linux desktop: the window is undecorated + transparent (decorations:false\n // + transparent:true in tauri.linux.conf.json) and its corners are rounded\n // entirely in CSS -- `.tauri-platform-linux #root` carries the border-radius\n // and clips to genuine window alpha (see index.css); no native GTK decoration\n // is involved. Mark the platform so that CSS applies, and track the maximized\n // state so the radius drops to 0 while maximized/tiled -- the app goes\n // edge-to-edge then, like every other windowed app (mobile Tauri is already\n // excluded above, so \"Linux\" here means desktop).\n let unlistenResized: (() => void) | undefined\n let disposed = false\n if (navigator.userAgent.includes('Linux')) {\n document.documentElement.classList.add('tauri-platform-linux')\n import('@tauri-apps/api/window').then(({ getCurrentWindow }) => {\n const win = getCurrentWindow()\n const syncMaximized = async () => {\n try {\n document.documentElement.classList.toggle(\n 'tauri-window-maximized',\n await win.isMaximized(),\n )\n } catch {\n // ignore -- corners just stay rounded\n }\n }\n syncMaximized()\n // onResized registers asynchronously; if the effect already tore down by\n // the time it resolves, unlisten immediately instead of leaking it.\n win.onResized(syncMaximized).then((unlisten) => {\n if (disposed) unlisten()\n else unlistenResized = unlisten\n })\n })\n }\n\n async function applyAccent() {\n try {\n const { invoke } = await import('@tauri-apps/api/core')\n const hex = await invoke<string | null>('get_system_accent_color')\n if (!hex) return\n\n const r = parseInt(hex.slice(1, 3), 16)\n const g = parseInt(hex.slice(3, 5), 16)\n const b = parseInt(hex.slice(5, 7), 16)\n\n const root = document.documentElement\n // Derive lighter/darker shades from the accent\n root.style.setProperty('--color-accent-50', adjustBrightness(r, g, b, 0.95))\n root.style.setProperty('--color-accent-100', adjustBrightness(r, g, b, 0.88))\n root.style.setProperty('--color-accent-400', adjustBrightness(r, g, b, 0.15))\n root.style.setProperty('--color-accent-500', hex)\n root.style.setProperty('--color-accent-600', adjustBrightness(r, g, b, -0.12))\n root.style.setProperty('--color-accent-700', adjustBrightness(r, g, b, -0.25))\n } catch (err) {\n // The theme accent is a fine outcome, so this one only needs saying in\n // development -- unlike the controls layout, nothing looks broken.\n warnOnce(\n 'get_system_accent_color',\n 'the app keeps its theme accent instead of the OS one',\n err,\n )\n }\n }\n\n applyAccent()\n\n async function applyWindowControls() {\n let layout: WindowControlsLayout\n try {\n const { invoke } = await import('@tauri-apps/api/core')\n layout = await invoke<WindowControlsLayout>('get_window_controls_layout')\n } catch (err) {\n // The command is the app's to implement, and a scaffolded app has not\n // implemented it yet. Doing nothing here used to mean no\n // `.tauri-controls-*` class and no `--wc-*-px`, so `.tauri-pad-controls`\n // resolved to zero padding and the header sat under the macOS traffic\n // lights -- silently, since this catch swallowed the reason. Falling\n // back to what the platform almost certainly wants makes a fresh\n // scaffold correct on macOS and Windows with no Rust at all; only\n // Linux, where the button layout is a per-desktop setting, genuinely\n // needs the command.\n layout = platformDefaultLayout()\n warnOnce(\n 'get_window_controls_layout',\n 'window controls fall back to the platform default; implement the command to read the desktop’s own button layout',\n err,\n )\n }\n\n {\n const root = document.documentElement\n // Per-side button lists drive which controls WindowControls renders, in\n // order (matches the desktop's own convention -- see window_controls.rs).\n root.setAttribute('data-wc-left', layout.left.join(','))\n root.setAttribute('data-wc-right', layout.right.join(','))\n // The side class drives the action-pill clearance padding (index.css).\n root.classList.remove('tauri-controls-left', 'tauri-controls-right', 'tauri-controls-both', 'tauri-controls-native')\n const side = layout.native\n ? 'native'\n : layout.left.length && layout.right.length\n ? 'both'\n : layout.left.length\n ? 'left'\n : layout.right.length\n ? 'right'\n : null\n if (side) root.classList.add(`tauri-controls-${side}`)\n\n // Publish each side's control footprint (px from the window edge to the\n // controls' inner edge) so the CSS clears them CONTINUOUSLY (index.css)\n // rather than via fixed width breakpoints -- exact for any layout and\n // any platform. Sizes mirror .window-control: Windows controls are 46px\n // wide flush to the edge; the custom (Linux) controls are 28px with a 6px\n // gap, inset 16px (left-4/right-4).\n const isWindows = navigator.userAgent.includes('Windows')\n const footprint = (count: number): number =>\n count === 0 ? 0 : isWindows ? count * 46 : 16 + count * 28 + (count - 1) * 6\n if (layout.native) {\n // macOS draws OS traffic lights (the app renders no custom controls, so\n // both button lists are empty). They sit on the LEFT and never vary in\n // count, so publish a fixed 64px left footprint (+1rem gap = the 80px\n // clearance used before) and let macOS ride the SAME continuous formula\n // as Linux/Windows -- fluid clearance instead of a width breakpoint.\n root.style.setProperty('--wc-left-px', '64px')\n root.style.setProperty('--wc-right-px', '0px')\n } else {\n root.style.setProperty('--wc-left-px', `${footprint(layout.left.length)}px`)\n root.style.setProperty('--wc-right-px', `${footprint(layout.right.length)}px`)\n }\n }\n }\n\n applyWindowControls()\n\n return () => {\n disposed = true\n unlistenResized?.()\n }\n }, [])\n}\n\ninterface WindowControlsLayout {\n left: string[]\n right: string[]\n native: boolean\n}\n\n/**\n * What the platform almost certainly wants, for an app that has not implemented\n * `get_window_controls_layout`.\n *\n * macOS draws its own traffic lights in an overlay title bar, so the app draws\n * none and only reserves room. Windows and the Linux fallback take the\n * conventional right-hand set. Linux is the one platform where this is a guess\n * rather than a rule -- the button layout is a desktop setting, and reading it\n * needs the Rust command.\n */\nfunction platformDefaultLayout(): WindowControlsLayout {\n const ua = navigator.userAgent\n if (ua.includes('Mac')) return { left: [], right: [], native: true }\n return { left: [], right: ['minimize', 'maximize', 'close'], native: false }\n}\n\nconst warned = new Set<string>()\n\n/**\n * Say what is missing, once, in development.\n *\n * A bare `catch {}` around a command an app is expected to implement is how a\n * scaffolded desktop app ends up with its navigation underneath the traffic\n * lights and nothing anywhere to explain it. Production stays quiet: the\n * fallbacks are correct, so there is nothing for a user to act on.\n */\nfunction warnOnce(command: string, consequence: string, err: unknown): void {\n if (warned.has(command)) return\n warned.add(command)\n let dev = false\n try {\n dev = Boolean((import.meta as ImportMeta & { env?: { DEV?: boolean } }).env?.DEV)\n } catch {\n // Not a Vite-style consumer; stay quiet rather than guess.\n }\n if (!dev) return\n console.warn(\n `[@whiskeyjack-net/tauri] the Tauri command \\`${command}\\` is not registered, so ${consequence}.\\n` +\n `Add it to your src-tauri invoke_handler. See the package README.`,\n err,\n )\n}\n\n/** Lighten (positive amount) or darken (negative amount) an RGB color. */\nfunction adjustBrightness(r: number, g: number, b: number, amount: number): string {\n const adjust = (c: number) => {\n if (amount > 0) {\n // Lighten: blend toward white\n return Math.round(c + (255 - c) * amount)\n }\n // Darken: blend toward black\n return Math.round(c * (1 + amount))\n }\n\n const clamp = (v: number) => Math.max(0, Math.min(255, v))\n const rr = clamp(adjust(r))\n const gg = clamp(adjust(g))\n const bb = clamp(adjust(b))\n\n return `#${rr.toString(16).padStart(2, '0')}${gg.toString(16).padStart(2, '0')}${bb.toString(16).padStart(2, '0')}`\n}\n","import { useCallback, useEffect, type MouseEvent as ReactMouseEvent } from 'react'\nimport { isTauri, isMobileTauri } from './desktop'\n\n/**\n * The window module, imported once and shared.\n *\n * Resolving the dynamic import inside the handler costs a frame or two on the\n * first drag, which reads as the window sticking before it moves. Warming it on\n * mount and reusing the promise removes that; Chip Away pre-cached it by hand\n * for the same reason, and carrying the trick into the hook is most of the\n * point of having one.\n */\nlet windowModule: Promise<typeof import('@tauri-apps/api/window')> | null = null\n\nfunction loadWindowModule() {\n windowModule ??= import('@tauri-apps/api/window')\n return windowModule\n}\n\n/**\n * Interactive descendants that must keep their click.\n *\n * `startDragging()` takes over the pointer, and the click that would have\n * followed never arrives -- so a header that drags from anywhere is a header\n * whose navigation does nothing. Anything focusable or role-bearing is excluded.\n */\nconst INTERACTIVE = 'a, button, input, select, textarea, nav, [role=\"tab\"], [role=\"button\"], [contenteditable]'\n\nexport interface WindowDragProps {\n onMouseDown: (event: ReactMouseEvent) => void\n}\n\n/**\n * Drag the window by an element, with the double-click zoom a title bar is\n * expected to have.\n *\n * The pack shipped the window-control components and their clearance CSS and\n * left this to the app, so every desktop consumer wrote the same twelve lines\n * (Chip Away and Glyph Stack had them character for character). Spread the\n * result onto whatever should behave like a title bar:\n *\n * ```tsx\n * const drag = useWindowDrag()\n * <AppHeader {...drag}>…</AppHeader>\n * ```\n *\n * Inert outside Tauri desktop, so the same Layout serves the web build with no\n * branch at the call site.\n */\nexport function useWindowDrag(): WindowDragProps {\n useEffect(() => {\n if (!isTauri() || isMobileTauri()) return\n void loadWindowModule().catch(() => {})\n }, [])\n\n const onMouseDown = useCallback((event: ReactMouseEvent) => {\n if (!isTauri() || isMobileTauri()) return\n // Only the primary button drags; a right-click belongs to the context menu.\n if (event.button !== 0) return\n if ((event.target as HTMLElement | null)?.closest(INTERACTIVE)) return\n\n event.preventDefault()\n void loadWindowModule()\n .then(({ getCurrentWindow }) => {\n const win = getCurrentWindow()\n // Double-click zooms, which is the platform convention for a title bar\n // on macOS and Windows alike.\n return event.detail === 2 ? win.toggleMaximize() : win.startDragging()\n })\n .catch(() => {\n // The window went away mid-gesture, or this is not a Tauri host after\n // all. Neither is worth surfacing: the gesture simply does nothing.\n })\n }, [])\n\n return { onMouseDown }\n}\n","/**\n * Open a URL outside the app: the system browser on Tauri (desktop and mobile,\n * via the opener plugin -- window.open is a no-op in the Tauri WebView), a new\n * tab on the web.\n *\n * Requires the `@tauri-apps/plugin-opener` peer plus its capability grant in the\n * Tauri app (`opener:allow-open-url`).\n */\nexport async function openExternal(url: string) {\n if ('__TAURI_INTERNALS__' in window) {\n const { openUrl } = await import('@tauri-apps/plugin-opener')\n await openUrl(url)\n } else {\n window.open(url, '_blank', 'noopener,noreferrer')\n }\n}\n","/**\n * Desktop in-app update checker for direct-download Tauri builds. Polls a\n * GitHub repo's releases, matches this app's tag prefix, compares semver\n * against the running version, and resolves the best installer asset for the\n * current platform. Store-distributed builds (App Store, Snap, ...) update\n * through the store and should never call this.\n */\n\nexport interface UpdateResult {\n status: 'update-available' | 'up-to-date' | 'error'\n latestVersion?: string\n releaseUrl?: string\n downloadUrl?: string\n}\n\nexport interface UpdaterConfig {\n /** GitHub `owner/repo` whose releases list is polled. */\n repo: string\n /** Tag prefix marking this app's releases, e.g. `chip-away-v`. */\n tagPrefix: string\n /**\n * Version used when the app is not running under Tauri (the web/PWA build,\n * where `@tauri-apps/api/app` is unavailable) -- typically the build-time\n * version constant. Under Tauri the real version comes from the runtime.\n */\n fallbackVersion: string\n /** Releases page size to fetch (default 10). */\n perPage?: number\n}\n\nexport interface Updater {\n /** The running app's version (Tauri runtime, else `fallbackVersion`). */\n getAppVersion(): Promise<string>\n /** Check the configured repo for a newer release. */\n checkForUpdates(): Promise<UpdateResult>\n}\n\ninterface GitHubAsset {\n name: string\n browser_download_url: string\n}\n\ninterface GitHubRelease {\n tag_name: string\n html_url: string\n draft: boolean\n assets: GitHubAsset[]\n}\n\n/** Compare two semver strings. Positive if a > b, negative if a < b, 0 if equal. */\nfunction compareSemver(a: string, b: string): number {\n const aParts = a.split('.').map(Number)\n const bParts = b.split('.').map(Number)\n const len = Math.max(aParts.length, bParts.length)\n\n for (let i = 0; i < len; i++) {\n const av = i < aParts.length ? aParts[i] : 0\n const bv = i < bParts.length ? bParts[i] : 0\n if (av !== bv) return av - bv\n }\n return 0\n}\n\n/** Find the best download URL for the current platform. */\nfunction getDownloadUrlForPlatform(assets: GitHubAsset[]): string | undefined {\n const ua = navigator.userAgent.toLowerCase()\n let patterns: string[]\n\n if (ua.includes('mac')) {\n patterns = ['.dmg']\n } else if (ua.includes('win')) {\n patterns = ['.msi', '-setup.exe', '.exe']\n } else {\n patterns = ['.appimage', '.deb']\n }\n\n for (const pattern of patterns) {\n const match = assets.find((a) => a.name.toLowerCase().endsWith(pattern))\n if (match) return match.browser_download_url\n }\n return undefined\n}\n\n/**\n * Build an updater bound to one app's release config. Give it the app's repo,\n * tag prefix, and build-time version:\n *\n * ```ts\n * export const updater = createUpdater({\n * repo: 'whiskeyjack-net/downloads',\n * tagPrefix: 'chip-away-v',\n * fallbackVersion: __APP_VERSION__,\n * })\n * ```\n */\nexport function createUpdater(config: UpdaterConfig): Updater {\n const releasesUrl = `https://api.github.com/repos/${config.repo}/releases?per_page=${config.perPage ?? 10}`\n\n async function getAppVersion(): Promise<string> {\n if ('__TAURI_INTERNALS__' in window) {\n try {\n const { getVersion } = await import('@tauri-apps/api/app')\n return await getVersion()\n } catch {\n // Fall through to the configured fallback.\n }\n }\n return config.fallbackVersion\n }\n\n async function checkForUpdates(): Promise<UpdateResult> {\n try {\n const response = await fetch(releasesUrl, {\n headers: { Accept: 'application/vnd.github+json' },\n signal: AbortSignal.timeout(15000),\n })\n\n if (!response.ok) return { status: 'error' }\n\n const releases: GitHubRelease[] = await response.json()\n\n // Latest release matching our tag prefix, skipping drafts.\n const matching = releases.filter(\n (r) => r.tag_name.startsWith(config.tagPrefix) && !r.draft,\n )\n\n if (matching.length === 0) return { status: 'up-to-date' }\n\n const latest = matching[0]\n const remoteVersion = latest.tag_name.slice(config.tagPrefix.length)\n const currentVersion = await getAppVersion()\n\n if (compareSemver(remoteVersion, currentVersion) > 0) {\n return {\n status: 'update-available',\n latestVersion: remoteVersion,\n releaseUrl: latest.html_url,\n downloadUrl: getDownloadUrlForPlatform(latest.assets) ?? latest.html_url,\n }\n }\n\n return { status: 'up-to-date' }\n } catch {\n return { status: 'error' }\n }\n }\n\n return { getAppVersion, checkForUpdates }\n}\n","import { useTranslation } from 'react-i18next'\nimport { ArrowSquareOut } from '@phosphor-icons/react'\nimport { Button, Toast } from '@whiskeyjack-net/design-system'\n\ninterface UpdateBannerProps {\n version: string\n releaseUrl: string\n onDismiss: () => void\n}\n\n/**\n * \"Update available\" notification for the desktop build. A persistent DS\n * Toast (floating below the header) with a download action and a dismiss\n * button -- the caller unmounts it on dismiss, so `open` is always true.\n *\n * Requires the app's locales to define `update.available` (interpolates\n * `{{version}}`), `update.download`, and `update.later`.\n */\nexport function UpdateBanner({ version, releaseUrl, onDismiss }: UpdateBannerProps) {\n const { t } = useTranslation()\n\n return (\n <Toast\n open\n icon={<ArrowSquareOut size={16} weight=\"bold\" className=\"text-[var(--color-accent-500)]\" />}\n onDismiss={onDismiss}\n dismissLabel={t('update.later')}\n action={\n <a href={releaseUrl} target=\"_blank\" rel=\"noopener noreferrer\" className=\"shrink-0\">\n <Button variant=\"accent\">{t('update.download')}</Button>\n </a>\n }\n >\n {t('update.available', { version })}\n </Toast>\n )\n}\n","import { useState, useCallback } from 'react'\nimport { useTranslation } from 'react-i18next'\nimport { ArrowSquareOut, ArrowsClockwise, CheckCircle, Warning } from '@phosphor-icons/react'\nimport { Button } from '@whiskeyjack-net/design-system'\nimport { openExternal } from './open-external'\nimport type { Updater, UpdateResult } from './updater'\n\ninterface UpdateDialogProps {\n /** The app's updater (from `createUpdater`); drives the check. */\n updater: Updater\n onClose: () => void\n}\n\n/**\n * \"Check for updates\" dialog body for the desktop build (mount inside a DS\n * BottomDrawer). Auto-runs the check on first render, then shows the result\n * with a download or up-to-date state.\n *\n * Requires the app's locales to define `update.checking`, `update.available`\n * (interpolates `{{version}}`), `update.currentVersion` (interpolates\n * `{{version}}`), `update.upToDate`, `update.download`, `update.later`,\n * `update.error`, and `common.cancel`.\n */\nexport function UpdateDialog({ updater, onClose }: UpdateDialogProps) {\n const { t } = useTranslation()\n const [state, setState] = useState<'idle' | 'checking' | 'done'>('idle')\n const [result, setResult] = useState<UpdateResult | null>(null)\n const [currentVersion, setCurrentVersion] = useState<string>('')\n\n const handleCheck = useCallback(async () => {\n setState('checking')\n const [version, updateResult] = await Promise.all([\n updater.getAppVersion(),\n updater.checkForUpdates(),\n ])\n setCurrentVersion(version)\n setResult(updateResult)\n setState('done')\n }, [updater])\n\n // Auto-trigger check on first render\n if (state === 'idle') {\n handleCheck()\n }\n\n return (\n <div className=\"flex flex-col items-center gap-4 py-2\">\n {state === 'checking' && (\n <>\n <ArrowsClockwise size={40} weight=\"bold\" className=\"text-[var(--color-accent-500)] animate-spin\" />\n <p className=\"text-sm text-[var(--color-text-secondary-light)] dark:text-[var(--color-text-secondary-dark)]\">\n {t('update.checking')}\n </p>\n </>\n )}\n\n {state === 'done' && result?.status === 'update-available' && (\n <>\n <ArrowsClockwise size={40} weight=\"bold\" className=\"text-[var(--color-accent-500)]\" />\n <div className=\"text-center\">\n <p className=\"text-sm font-medium text-[var(--color-text-primary-light)] dark:text-[var(--color-text-primary-dark)]\">\n {t('update.available', { version: result.latestVersion })}\n </p>\n <p className=\"text-xs text-[var(--color-text-muted-light)] dark:text-[var(--color-text-muted-dark)] mt-1\">\n {t('update.currentVersion', { version: currentVersion })}\n </p>\n </div>\n <div className=\"flex gap-3\">\n <Button variant=\"accent\" onClick={() => openExternal(result.downloadUrl || result.releaseUrl || '')}>\n <ArrowSquareOut size={16} weight=\"bold\" />\n {t('update.download')}\n </Button>\n <Button variant=\"outline\" onClick={onClose}>\n {t('update.later')}\n </Button>\n </div>\n </>\n )}\n\n {state === 'done' && result?.status === 'up-to-date' && (\n <>\n <CheckCircle size={40} weight=\"fill\" className=\"text-[var(--color-success-500)]\" />\n <div className=\"text-center\">\n <p className=\"text-sm font-medium text-[var(--color-text-primary-light)] dark:text-[var(--color-text-primary-dark)]\">\n {t('update.upToDate')}\n </p>\n <p className=\"text-xs text-[var(--color-text-muted-light)] dark:text-[var(--color-text-muted-dark)] mt-1\">\n {t('update.currentVersion', { version: currentVersion })}\n </p>\n </div>\n <Button variant=\"outline\" onClick={onClose}>\n {t('common.cancel')}\n </Button>\n </>\n )}\n\n {state === 'done' && result?.status === 'error' && (\n <>\n <Warning size={40} weight=\"fill\" className=\"text-[var(--color-warning-500)]\" />\n <p className=\"text-sm text-[var(--color-text-secondary-light)] dark:text-[var(--color-text-secondary-dark)]\">\n {t('update.error')}\n </p>\n <Button variant=\"outline\" onClick={onClose}>\n {t('common.cancel')}\n </Button>\n </>\n )}\n </div>\n )\n}\n"],"mappings":"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","names":["useEffect","useEffect","useEffect","useEffect","useTranslation","jsx","useState","useCallback","useTranslation","ArrowSquareOut","Button","jsx","useTranslation","useState","useCallback","Button","ArrowSquareOut"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type MouseEvent as ReactMouseEvent } from 'react';
|
|
2
|
+
export interface WindowDragProps {
|
|
3
|
+
onMouseDown: (event: ReactMouseEvent) => void;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Drag the window by an element, with the double-click zoom a title bar is
|
|
7
|
+
* expected to have.
|
|
8
|
+
*
|
|
9
|
+
* The pack shipped the window-control components and their clearance CSS and
|
|
10
|
+
* left this to the app, so every desktop consumer wrote the same twelve lines
|
|
11
|
+
* (Chip Away and Glyph Stack had them character for character). Spread the
|
|
12
|
+
* result onto whatever should behave like a title bar:
|
|
13
|
+
*
|
|
14
|
+
* ```tsx
|
|
15
|
+
* const drag = useWindowDrag()
|
|
16
|
+
* <AppHeader {...drag}>…</AppHeader>
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* Inert outside Tauri desktop, so the same Layout serves the web build with no
|
|
20
|
+
* branch at the call site.
|
|
21
|
+
*/
|
|
22
|
+
export declare function useWindowDrag(): WindowDragProps;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whiskeyjack-net/tauri",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Tauri-native app-shell layer for the Whiskeyjack design system: per-platform CSD window controls, OS accent integration, and desktop guards.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tauri",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"@phosphor-icons/react": "^2.0.0",
|
|
35
35
|
"@tauri-apps/api": "^2.0.0",
|
|
36
36
|
"@tauri-apps/plugin-opener": "^2.0.0",
|
|
37
|
-
"@whiskeyjack-net/design-system": "^0.
|
|
37
|
+
"@whiskeyjack-net/design-system": "^0.14.0",
|
|
38
38
|
"react": "^18.0.0",
|
|
39
39
|
"react-dom": "^18.0.0",
|
|
40
40
|
"react-i18next": "^14.0.0 || ^15.0.0"
|