@whiskeyjack-net/tauri 0.2.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/LICENSE +21 -0
- package/README.md +71 -0
- package/css/window-controls.css +39 -0
- package/dist/desktop.d.ts +9 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +358 -0
- package/dist/index.js.map +1 -0
- package/dist/open-external.d.ts +9 -0
- package/dist/update-banner.d.ts +15 -0
- package/dist/update-dialog.d.ts +18 -0
- package/dist/updater.d.ts +46 -0
- package/dist/use-system-accent.d.ts +5 -0
- package/dist/window-controls.d.ts +2 -0
- package/package.json +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 whiskeyjack.net
|
|
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,71 @@
|
|
|
1
|
+
# @whiskeyjack-net/tauri
|
|
2
|
+
|
|
3
|
+
The Tauri-native **app-shell layer** for the Whiskeyjack design system — the
|
|
4
|
+
window-chrome pieces the DS itself stays free of, so the design system runs
|
|
5
|
+
identically as a plain web app or inside a Tauri window.
|
|
6
|
+
|
|
7
|
+
*This README documents the published artifact (`@whiskeyjack-net/tauri`, staged
|
|
8
|
+
via `npm run build:package`). Inside the monorepo the workspace source export is
|
|
9
|
+
`@whiskeyjack/tauri`.*
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @whiskeyjack-net/tauri
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Peers: `react`/`react-dom` 18+, `@tauri-apps/api` 2+, `react-i18next`,
|
|
18
|
+
`@whiskeyjack-net/design-system` (the updater UI uses `Button`/`Toast`),
|
|
19
|
+
`@phosphor-icons/react`, and `@tauri-apps/plugin-opener` (optional — only for
|
|
20
|
+
`openExternal` / the updater's download action). Everything is inert (guarded)
|
|
21
|
+
off Tauri, so it is safe to ship in a plain web/PWA build.
|
|
22
|
+
|
|
23
|
+
## Exports
|
|
24
|
+
|
|
25
|
+
- **Guards** — `isTauri()`, `isMobileTauri()` (iPad-aware), `isDesktopTauri()`,
|
|
26
|
+
`isMacDesktop()`. The desktop shell must gate on **desktop** (mobile Tauri
|
|
27
|
+
also sets `__TAURI_INTERNALS__`), or its desktop-only CSS leaks onto phones.
|
|
28
|
+
- **`WindowControlsLeft` / `WindowControlsRight`** — CSD window-control buttons,
|
|
29
|
+
rendering the exact per-side buttons the backend reports (`data-wc-left` /
|
|
30
|
+
`data-wc-right` on `<html>`), so the app matches each desktop's convention.
|
|
31
|
+
Mount them in the DS `AppHeader`'s `chrome` slot.
|
|
32
|
+
- **`useSystemAccent()`** — on Tauri desktop, reads the OS accent color and
|
|
33
|
+
overrides the DS `--color-accent-*` variables, adds the `.tauri-desktop` /
|
|
34
|
+
`.tauri-platform-*` markers, and publishes the `--wc-left/right-px`
|
|
35
|
+
window-control footprint the header-clearance CSS consumes. No-op elsewhere.
|
|
36
|
+
- **`@whiskeyjack-net/tauri/css/window-controls`** — the window-control CSS
|
|
37
|
+
(import once); pairs with the components and the `AppHeader`
|
|
38
|
+
`chrome`/`rowClassName` extension points.
|
|
39
|
+
- **`openExternal(url)`** — open a URL in the system browser on Tauri (via the
|
|
40
|
+
opener plugin), a new tab on the web.
|
|
41
|
+
- **In-app updater** (direct-download desktop builds) — `createUpdater(config)`
|
|
42
|
+
binds the checker to one app's release channel; pair it with the UI:
|
|
43
|
+
- `createUpdater({ repo, tagPrefix, fallbackVersion })` → `{ getAppVersion,
|
|
44
|
+
checkForUpdates }`. Polls the GitHub repo's releases, matches `tagPrefix`,
|
|
45
|
+
compares semver, and resolves the best installer asset for the platform.
|
|
46
|
+
`fallbackVersion` is the web/PWA version (under Tauri the runtime reports it).
|
|
47
|
+
- `<UpdateBanner version releaseUrl onDismiss />` — a floating DS `Toast`
|
|
48
|
+
surfaced on launch when a newer release exists.
|
|
49
|
+
- `<UpdateDialog updater onClose />` — a "check for updates" body for a DS
|
|
50
|
+
`BottomDrawer` (Settings). Auto-runs the check on open.
|
|
51
|
+
|
|
52
|
+
Store-distributed builds (App Store, Snap, ...) update through the store —
|
|
53
|
+
gate the checker off for those.
|
|
54
|
+
|
|
55
|
+
The updater UI reads these translation keys from the app's locales:
|
|
56
|
+
`update.available` (`{{version}}`), `update.download`, `update.later`,
|
|
57
|
+
`update.checking`, `update.currentVersion` (`{{version}}`), `update.upToDate`,
|
|
58
|
+
`update.error`, `common.cancel`; the window controls read `window.close` /
|
|
59
|
+
`window.minimize` / `window.maximize`.
|
|
60
|
+
|
|
61
|
+
## Not included (by design)
|
|
62
|
+
|
|
63
|
+
- The **Rust** side (`src-tauri/`: the `get_system_accent_color` /
|
|
64
|
+
`get_window_controls_layout` commands, tray/menus, notifications) is an
|
|
65
|
+
**owned scaffold template**, seeded from a reference app — not an npm library.
|
|
66
|
+
- **Auth/sync** is app-coupled (Firebase) and lives in the app or a future
|
|
67
|
+
starter kit.
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
MIT
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/* Tauri desktop window-control chrome. Ported from the monorepo app; FINDING
|
|
2
|
+
(Stage B): this CSS is @whiskeyjack-net/tauri pack material -- it pairs with
|
|
3
|
+
the WindowControls component and the AppHeader `chrome`/`rowClassName`
|
|
4
|
+
extension points. Scoped so it is inert on web (no .tauri-desktop marker). */
|
|
5
|
+
.window-controls { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
|
|
6
|
+
|
|
7
|
+
.window-control {
|
|
8
|
+
width: 28px; height: 28px; display: flex; align-items: center; justify-content: center;
|
|
9
|
+
border: none; border-radius: 50%; font-size: 11px; line-height: 1; cursor: pointer;
|
|
10
|
+
background: var(--color-warm-100); color: var(--color-text-secondary-light);
|
|
11
|
+
transition: background 0.15s, color 0.15s;
|
|
12
|
+
}
|
|
13
|
+
.tauri-drag-zone .window-control { width: 20px; height: 20px; font-size: 9px; }
|
|
14
|
+
.dark .window-control { background: var(--color-neutral-800); color: var(--color-text-secondary-dark); }
|
|
15
|
+
.window-control:hover { background: var(--color-warm-200); color: var(--color-text-primary-light); }
|
|
16
|
+
.dark .window-control:hover { background: var(--color-neutral-700); color: var(--color-text-primary-dark); }
|
|
17
|
+
.window-control-close:hover { background: var(--color-error-500); color: white; }
|
|
18
|
+
|
|
19
|
+
/* Windows: rectangular, flush controls */
|
|
20
|
+
.tauri-platform-windows .window-controls { gap: 0; }
|
|
21
|
+
.tauri-platform-windows .window-control { width: 46px; height: 34px; border-radius: 0; background: transparent; font-size: 12px; }
|
|
22
|
+
.tauri-platform-windows .window-control:hover { background: rgba(0,0,0,0.06); }
|
|
23
|
+
.tauri-platform-windows.dark .window-control:hover { background: rgba(255,255,255,0.08); }
|
|
24
|
+
.tauri-platform-windows .window-control-close:hover { background: var(--color-error-500); color: white; }
|
|
25
|
+
.tauri-platform-windows .wc-right-container { right: 0; top: 0; align-items: flex-start; }
|
|
26
|
+
|
|
27
|
+
/* Header window-control clearance: constant footprint + 1rem, published as
|
|
28
|
+
--wc-*-px by useSystemAccent (not ported in Stage A). */
|
|
29
|
+
@media (min-width: 768px) {
|
|
30
|
+
.tauri-controls-left .tauri-pad-controls,
|
|
31
|
+
.tauri-controls-right .tauri-pad-controls,
|
|
32
|
+
.tauri-controls-both .tauri-pad-controls,
|
|
33
|
+
.tauri-controls-native .tauri-pad-controls {
|
|
34
|
+
padding-left: calc(var(--wc-left-px, 0px) + 1rem);
|
|
35
|
+
padding-right: calc(var(--wc-right-px, 0px) + 1rem);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.tauri-desktop .tauri-drag-zone { display: block; }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const isTauri: () => boolean;
|
|
2
|
+
/**
|
|
3
|
+
* Tauri mobile build (iOS/Android). iPads can report a "Macintosh" UA in
|
|
4
|
+
* WKWebView, so the touch-point check keeps them off the macOS/desktop path.
|
|
5
|
+
*/
|
|
6
|
+
export declare const isMobileTauri: () => boolean;
|
|
7
|
+
export declare const isDesktopTauri: () => boolean;
|
|
8
|
+
/** macOS desktop (draws OS traffic lights rather than custom window controls). */
|
|
9
|
+
export declare const isMacDesktop: () => boolean;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { isTauri, isMobileTauri, isDesktopTauri, isMacDesktop } from './desktop';
|
|
2
|
+
export { WindowControlsLeft, WindowControlsRight } from './window-controls';
|
|
3
|
+
export { useSystemAccent } from './use-system-accent';
|
|
4
|
+
export { openExternal } from './open-external';
|
|
5
|
+
export { createUpdater } from './updater';
|
|
6
|
+
export type { Updater, UpdaterConfig, UpdateResult } from './updater';
|
|
7
|
+
export { UpdateBanner } from './update-banner';
|
|
8
|
+
export { UpdateDialog } from './update-dialog';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
// src/desktop.ts
|
|
2
|
+
var isTauri = () => "__TAURI_INTERNALS__" in window;
|
|
3
|
+
var isMobileTauri = () => isTauri() && (/android|iphone|ipad|ipod/i.test(navigator.userAgent) || navigator.userAgent.includes("Mac") && navigator.maxTouchPoints > 1);
|
|
4
|
+
var isDesktopTauri = () => isTauri() && !isMobileTauri();
|
|
5
|
+
var isMacDesktop = () => navigator.userAgent.includes("Mac") && !isMobileTauri();
|
|
6
|
+
|
|
7
|
+
// src/window-controls.tsx
|
|
8
|
+
import { useState, useEffect } from "react";
|
|
9
|
+
import { useTranslation } from "react-i18next";
|
|
10
|
+
import { jsx } from "react/jsx-runtime";
|
|
11
|
+
function useWindowControls() {
|
|
12
|
+
const [left, setLeft] = useState([]);
|
|
13
|
+
const [right, setRight] = useState([]);
|
|
14
|
+
const [windowMod, setWindowMod] = useState(null);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!isTauri()) return;
|
|
17
|
+
const parse = (v) => (v ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
18
|
+
const read = () => {
|
|
19
|
+
const root = document.documentElement;
|
|
20
|
+
setLeft(parse(root.getAttribute("data-wc-left")));
|
|
21
|
+
setRight(parse(root.getAttribute("data-wc-right")));
|
|
22
|
+
};
|
|
23
|
+
read();
|
|
24
|
+
const observer = new MutationObserver(read);
|
|
25
|
+
observer.observe(document.documentElement, {
|
|
26
|
+
attributes: true,
|
|
27
|
+
attributeFilter: ["data-wc-left", "data-wc-right"]
|
|
28
|
+
});
|
|
29
|
+
return () => observer.disconnect();
|
|
30
|
+
}, []);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!isTauri()) return;
|
|
33
|
+
import("@tauri-apps/api/window").then(setWindowMod);
|
|
34
|
+
}, []);
|
|
35
|
+
return { left, right, windowMod };
|
|
36
|
+
}
|
|
37
|
+
function ControlButton({ kind, win }) {
|
|
38
|
+
const { t } = useTranslation();
|
|
39
|
+
switch (kind) {
|
|
40
|
+
case "close":
|
|
41
|
+
return /* @__PURE__ */ jsx(
|
|
42
|
+
"button",
|
|
43
|
+
{
|
|
44
|
+
onClick: () => win.close(),
|
|
45
|
+
"aria-label": t("window.close"),
|
|
46
|
+
title: t("window.close"),
|
|
47
|
+
className: "window-control window-control-close",
|
|
48
|
+
children: "\u2715"
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
case "minimize":
|
|
52
|
+
return /* @__PURE__ */ jsx(
|
|
53
|
+
"button",
|
|
54
|
+
{
|
|
55
|
+
onClick: () => win.minimize(),
|
|
56
|
+
"aria-label": t("window.minimize"),
|
|
57
|
+
title: t("window.minimize"),
|
|
58
|
+
className: "window-control",
|
|
59
|
+
children: "\u2212"
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
case "maximize":
|
|
63
|
+
return /* @__PURE__ */ jsx(
|
|
64
|
+
"button",
|
|
65
|
+
{
|
|
66
|
+
onClick: () => win.toggleMaximize(),
|
|
67
|
+
"aria-label": t("window.maximize"),
|
|
68
|
+
title: t("window.maximize"),
|
|
69
|
+
className: "window-control",
|
|
70
|
+
children: "\u25A2"
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
default:
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function WindowControlsLeft() {
|
|
78
|
+
const { left, windowMod } = useWindowControls();
|
|
79
|
+
if (!windowMod || left.length === 0) return null;
|
|
80
|
+
const win = windowMod.getCurrentWindow();
|
|
81
|
+
return /* @__PURE__ */ jsx("div", { className: "window-controls", children: left.map((kind) => /* @__PURE__ */ jsx(ControlButton, { kind, win }, kind)) });
|
|
82
|
+
}
|
|
83
|
+
function WindowControlsRight() {
|
|
84
|
+
const { right, windowMod } = useWindowControls();
|
|
85
|
+
if (!windowMod || right.length === 0) return null;
|
|
86
|
+
const win = windowMod.getCurrentWindow();
|
|
87
|
+
return /* @__PURE__ */ jsx("div", { className: "window-controls", children: right.map((kind) => /* @__PURE__ */ jsx(ControlButton, { kind, win }, kind)) });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/use-system-accent.ts
|
|
91
|
+
import { useEffect as useEffect2 } from "react";
|
|
92
|
+
function useSystemAccent() {
|
|
93
|
+
useEffect2(() => {
|
|
94
|
+
if (!isTauri()) return;
|
|
95
|
+
if (isMobileTauri()) return;
|
|
96
|
+
document.documentElement.classList.add("tauri-desktop");
|
|
97
|
+
if (navigator.userAgent.includes("Windows")) {
|
|
98
|
+
document.documentElement.classList.add("tauri-platform-windows");
|
|
99
|
+
}
|
|
100
|
+
let unlistenResized;
|
|
101
|
+
let disposed = false;
|
|
102
|
+
if (navigator.userAgent.includes("Linux")) {
|
|
103
|
+
document.documentElement.classList.add("tauri-platform-linux");
|
|
104
|
+
import("@tauri-apps/api/window").then(({ getCurrentWindow }) => {
|
|
105
|
+
const win = getCurrentWindow();
|
|
106
|
+
const syncMaximized = async () => {
|
|
107
|
+
try {
|
|
108
|
+
document.documentElement.classList.toggle(
|
|
109
|
+
"tauri-window-maximized",
|
|
110
|
+
await win.isMaximized()
|
|
111
|
+
);
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
syncMaximized();
|
|
116
|
+
win.onResized(syncMaximized).then((unlisten) => {
|
|
117
|
+
if (disposed) unlisten();
|
|
118
|
+
else unlistenResized = unlisten;
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
async function applyAccent() {
|
|
123
|
+
try {
|
|
124
|
+
const { invoke } = await import("@tauri-apps/api/core");
|
|
125
|
+
const hex = await invoke("get_system_accent_color");
|
|
126
|
+
if (!hex) return;
|
|
127
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
128
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
129
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
130
|
+
const root = document.documentElement;
|
|
131
|
+
root.style.setProperty("--color-accent-50", adjustBrightness(r, g, b, 0.95));
|
|
132
|
+
root.style.setProperty("--color-accent-100", adjustBrightness(r, g, b, 0.88));
|
|
133
|
+
root.style.setProperty("--color-accent-400", adjustBrightness(r, g, b, 0.15));
|
|
134
|
+
root.style.setProperty("--color-accent-500", hex);
|
|
135
|
+
root.style.setProperty("--color-accent-600", adjustBrightness(r, g, b, -0.12));
|
|
136
|
+
root.style.setProperty("--color-accent-700", adjustBrightness(r, g, b, -0.25));
|
|
137
|
+
} catch {
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
applyAccent();
|
|
141
|
+
async function applyWindowControls() {
|
|
142
|
+
try {
|
|
143
|
+
const { invoke } = await import("@tauri-apps/api/core");
|
|
144
|
+
const layout = await invoke(
|
|
145
|
+
"get_window_controls_layout"
|
|
146
|
+
);
|
|
147
|
+
const root = document.documentElement;
|
|
148
|
+
root.setAttribute("data-wc-left", layout.left.join(","));
|
|
149
|
+
root.setAttribute("data-wc-right", layout.right.join(","));
|
|
150
|
+
root.classList.remove("tauri-controls-left", "tauri-controls-right", "tauri-controls-both", "tauri-controls-native");
|
|
151
|
+
const side = layout.native ? "native" : layout.left.length && layout.right.length ? "both" : layout.left.length ? "left" : layout.right.length ? "right" : null;
|
|
152
|
+
if (side) root.classList.add(`tauri-controls-${side}`);
|
|
153
|
+
const isWindows = navigator.userAgent.includes("Windows");
|
|
154
|
+
const footprint = (count) => count === 0 ? 0 : isWindows ? count * 46 : 16 + count * 28 + (count - 1) * 6;
|
|
155
|
+
if (layout.native) {
|
|
156
|
+
root.style.setProperty("--wc-left-px", "64px");
|
|
157
|
+
root.style.setProperty("--wc-right-px", "0px");
|
|
158
|
+
} else {
|
|
159
|
+
root.style.setProperty("--wc-left-px", `${footprint(layout.left.length)}px`);
|
|
160
|
+
root.style.setProperty("--wc-right-px", `${footprint(layout.right.length)}px`);
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
applyWindowControls();
|
|
166
|
+
return () => {
|
|
167
|
+
disposed = true;
|
|
168
|
+
unlistenResized?.();
|
|
169
|
+
};
|
|
170
|
+
}, []);
|
|
171
|
+
}
|
|
172
|
+
function adjustBrightness(r, g, b, amount) {
|
|
173
|
+
const adjust = (c) => {
|
|
174
|
+
if (amount > 0) {
|
|
175
|
+
return Math.round(c + (255 - c) * amount);
|
|
176
|
+
}
|
|
177
|
+
return Math.round(c * (1 + amount));
|
|
178
|
+
};
|
|
179
|
+
const clamp = (v) => Math.max(0, Math.min(255, v));
|
|
180
|
+
const rr = clamp(adjust(r));
|
|
181
|
+
const gg = clamp(adjust(g));
|
|
182
|
+
const bb = clamp(adjust(b));
|
|
183
|
+
return `#${rr.toString(16).padStart(2, "0")}${gg.toString(16).padStart(2, "0")}${bb.toString(16).padStart(2, "0")}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/open-external.ts
|
|
187
|
+
async function openExternal(url) {
|
|
188
|
+
if ("__TAURI_INTERNALS__" in window) {
|
|
189
|
+
const { openUrl } = await import("@tauri-apps/plugin-opener");
|
|
190
|
+
await openUrl(url);
|
|
191
|
+
} else {
|
|
192
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/updater.ts
|
|
197
|
+
function compareSemver(a, b) {
|
|
198
|
+
const aParts = a.split(".").map(Number);
|
|
199
|
+
const bParts = b.split(".").map(Number);
|
|
200
|
+
const len = Math.max(aParts.length, bParts.length);
|
|
201
|
+
for (let i = 0; i < len; i++) {
|
|
202
|
+
const av = i < aParts.length ? aParts[i] : 0;
|
|
203
|
+
const bv = i < bParts.length ? bParts[i] : 0;
|
|
204
|
+
if (av !== bv) return av - bv;
|
|
205
|
+
}
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
function getDownloadUrlForPlatform(assets) {
|
|
209
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
210
|
+
let patterns;
|
|
211
|
+
if (ua.includes("mac")) {
|
|
212
|
+
patterns = [".dmg"];
|
|
213
|
+
} else if (ua.includes("win")) {
|
|
214
|
+
patterns = [".msi", "-setup.exe", ".exe"];
|
|
215
|
+
} else {
|
|
216
|
+
patterns = [".appimage", ".deb"];
|
|
217
|
+
}
|
|
218
|
+
for (const pattern of patterns) {
|
|
219
|
+
const match = assets.find((a) => a.name.toLowerCase().endsWith(pattern));
|
|
220
|
+
if (match) return match.browser_download_url;
|
|
221
|
+
}
|
|
222
|
+
return void 0;
|
|
223
|
+
}
|
|
224
|
+
function createUpdater(config) {
|
|
225
|
+
const releasesUrl = `https://api.github.com/repos/${config.repo}/releases?per_page=${config.perPage ?? 10}`;
|
|
226
|
+
async function getAppVersion() {
|
|
227
|
+
if ("__TAURI_INTERNALS__" in window) {
|
|
228
|
+
try {
|
|
229
|
+
const { getVersion } = await import("@tauri-apps/api/app");
|
|
230
|
+
return await getVersion();
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return config.fallbackVersion;
|
|
235
|
+
}
|
|
236
|
+
async function checkForUpdates() {
|
|
237
|
+
try {
|
|
238
|
+
const response = await fetch(releasesUrl, {
|
|
239
|
+
headers: { Accept: "application/vnd.github+json" },
|
|
240
|
+
signal: AbortSignal.timeout(15e3)
|
|
241
|
+
});
|
|
242
|
+
if (!response.ok) return { status: "error" };
|
|
243
|
+
const releases = await response.json();
|
|
244
|
+
const matching = releases.filter(
|
|
245
|
+
(r) => r.tag_name.startsWith(config.tagPrefix) && !r.draft
|
|
246
|
+
);
|
|
247
|
+
if (matching.length === 0) return { status: "up-to-date" };
|
|
248
|
+
const latest = matching[0];
|
|
249
|
+
const remoteVersion = latest.tag_name.slice(config.tagPrefix.length);
|
|
250
|
+
const currentVersion = await getAppVersion();
|
|
251
|
+
if (compareSemver(remoteVersion, currentVersion) > 0) {
|
|
252
|
+
return {
|
|
253
|
+
status: "update-available",
|
|
254
|
+
latestVersion: remoteVersion,
|
|
255
|
+
releaseUrl: latest.html_url,
|
|
256
|
+
downloadUrl: getDownloadUrlForPlatform(latest.assets) ?? latest.html_url
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return { status: "up-to-date" };
|
|
260
|
+
} catch {
|
|
261
|
+
return { status: "error" };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return { getAppVersion, checkForUpdates };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/update-banner.tsx
|
|
268
|
+
import { useTranslation as useTranslation2 } from "react-i18next";
|
|
269
|
+
import { ArrowSquareOut } from "@phosphor-icons/react";
|
|
270
|
+
import { Button, Toast } from "@whiskeyjack-net/design-system";
|
|
271
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
272
|
+
function UpdateBanner({ version, releaseUrl, onDismiss }) {
|
|
273
|
+
const { t } = useTranslation2();
|
|
274
|
+
return /* @__PURE__ */ jsx2(
|
|
275
|
+
Toast,
|
|
276
|
+
{
|
|
277
|
+
open: true,
|
|
278
|
+
icon: /* @__PURE__ */ jsx2(ArrowSquareOut, { size: 16, weight: "bold", className: "text-[var(--color-accent-500)]" }),
|
|
279
|
+
onDismiss,
|
|
280
|
+
dismissLabel: t("update.later"),
|
|
281
|
+
action: /* @__PURE__ */ jsx2("a", { href: releaseUrl, target: "_blank", rel: "noopener noreferrer", className: "shrink-0", children: /* @__PURE__ */ jsx2(Button, { variant: "accent", children: t("update.download") }) }),
|
|
282
|
+
children: t("update.available", { version })
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/update-dialog.tsx
|
|
288
|
+
import { useState as useState2, useCallback } from "react";
|
|
289
|
+
import { useTranslation as useTranslation3 } from "react-i18next";
|
|
290
|
+
import { ArrowSquareOut as ArrowSquareOut2, ArrowsClockwise, CheckCircle, Warning } from "@phosphor-icons/react";
|
|
291
|
+
import { Button as Button2 } from "@whiskeyjack-net/design-system";
|
|
292
|
+
import { Fragment, jsx as jsx3, jsxs } from "react/jsx-runtime";
|
|
293
|
+
function UpdateDialog({ updater, onClose }) {
|
|
294
|
+
const { t } = useTranslation3();
|
|
295
|
+
const [state, setState] = useState2("idle");
|
|
296
|
+
const [result, setResult] = useState2(null);
|
|
297
|
+
const [currentVersion, setCurrentVersion] = useState2("");
|
|
298
|
+
const handleCheck = useCallback(async () => {
|
|
299
|
+
setState("checking");
|
|
300
|
+
const [version, updateResult] = await Promise.all([
|
|
301
|
+
updater.getAppVersion(),
|
|
302
|
+
updater.checkForUpdates()
|
|
303
|
+
]);
|
|
304
|
+
setCurrentVersion(version);
|
|
305
|
+
setResult(updateResult);
|
|
306
|
+
setState("done");
|
|
307
|
+
}, [updater]);
|
|
308
|
+
if (state === "idle") {
|
|
309
|
+
handleCheck();
|
|
310
|
+
}
|
|
311
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-4 py-2", children: [
|
|
312
|
+
state === "checking" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
313
|
+
/* @__PURE__ */ jsx3(ArrowsClockwise, { size: 40, weight: "bold", className: "text-[var(--color-accent-500)] animate-spin" }),
|
|
314
|
+
/* @__PURE__ */ jsx3("p", { className: "text-sm text-[var(--color-text-secondary-light)] dark:text-[var(--color-text-secondary-dark)]", children: t("update.checking") })
|
|
315
|
+
] }),
|
|
316
|
+
state === "done" && result?.status === "update-available" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
317
|
+
/* @__PURE__ */ jsx3(ArrowsClockwise, { size: 40, weight: "bold", className: "text-[var(--color-accent-500)]" }),
|
|
318
|
+
/* @__PURE__ */ jsxs("div", { className: "text-center", children: [
|
|
319
|
+
/* @__PURE__ */ jsx3("p", { className: "text-sm font-medium text-[var(--color-text-primary-light)] dark:text-[var(--color-text-primary-dark)]", children: t("update.available", { version: result.latestVersion }) }),
|
|
320
|
+
/* @__PURE__ */ jsx3("p", { className: "text-xs text-[var(--color-text-muted-light)] dark:text-[var(--color-text-muted-dark)] mt-1", children: t("update.currentVersion", { version: currentVersion }) })
|
|
321
|
+
] }),
|
|
322
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-3", children: [
|
|
323
|
+
/* @__PURE__ */ jsxs(Button2, { variant: "accent", onClick: () => openExternal(result.downloadUrl || result.releaseUrl || ""), children: [
|
|
324
|
+
/* @__PURE__ */ jsx3(ArrowSquareOut2, { size: 16, weight: "bold" }),
|
|
325
|
+
t("update.download")
|
|
326
|
+
] }),
|
|
327
|
+
/* @__PURE__ */ jsx3(Button2, { variant: "outline", onClick: onClose, children: t("update.later") })
|
|
328
|
+
] })
|
|
329
|
+
] }),
|
|
330
|
+
state === "done" && result?.status === "up-to-date" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
331
|
+
/* @__PURE__ */ jsx3(CheckCircle, { size: 40, weight: "fill", className: "text-[var(--color-success-500)]" }),
|
|
332
|
+
/* @__PURE__ */ jsxs("div", { className: "text-center", children: [
|
|
333
|
+
/* @__PURE__ */ jsx3("p", { className: "text-sm font-medium text-[var(--color-text-primary-light)] dark:text-[var(--color-text-primary-dark)]", children: t("update.upToDate") }),
|
|
334
|
+
/* @__PURE__ */ jsx3("p", { className: "text-xs text-[var(--color-text-muted-light)] dark:text-[var(--color-text-muted-dark)] mt-1", children: t("update.currentVersion", { version: currentVersion }) })
|
|
335
|
+
] }),
|
|
336
|
+
/* @__PURE__ */ jsx3(Button2, { variant: "outline", onClick: onClose, children: t("common.cancel") })
|
|
337
|
+
] }),
|
|
338
|
+
state === "done" && result?.status === "error" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
339
|
+
/* @__PURE__ */ jsx3(Warning, { size: 40, weight: "fill", className: "text-[var(--color-warning-500)]" }),
|
|
340
|
+
/* @__PURE__ */ jsx3("p", { className: "text-sm text-[var(--color-text-secondary-light)] dark:text-[var(--color-text-secondary-dark)]", children: t("update.error") }),
|
|
341
|
+
/* @__PURE__ */ jsx3(Button2, { variant: "outline", onClick: onClose, children: t("common.cancel") })
|
|
342
|
+
] })
|
|
343
|
+
] });
|
|
344
|
+
}
|
|
345
|
+
export {
|
|
346
|
+
UpdateBanner,
|
|
347
|
+
UpdateDialog,
|
|
348
|
+
WindowControlsLeft,
|
|
349
|
+
WindowControlsRight,
|
|
350
|
+
createUpdater,
|
|
351
|
+
isDesktopTauri,
|
|
352
|
+
isMacDesktop,
|
|
353
|
+
isMobileTauri,
|
|
354
|
+
isTauri,
|
|
355
|
+
openExternal,
|
|
356
|
+
useSystemAccent
|
|
357
|
+
};
|
|
358
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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","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":";AAIO,IAAM,UAAU,MAAe,yBAAyB;AAMxD,IAAM,gBAAgB,MAC3B,QAAQ,MACP,4BAA4B,KAAK,UAAU,SAAS,KAClD,UAAU,UAAU,SAAS,KAAK,KAAK,UAAU,iBAAiB;AAEhE,IAAM,iBAAiB,MAAe,QAAQ,KAAK,CAAC,cAAc;AAGlE,IAAM,eAAe,MAC1B,UAAU,UAAU,SAAS,KAAK,KAAK,CAAC,cAAc;;;ACnBxD,SAAS,UAAU,iBAAiB;AACpC,SAAS,sBAAsB;AAmDvB;AAxCR,SAAS,oBAAoB;AAC3B,QAAM,CAAC,MAAM,OAAO,IAAI,SAAmB,CAAC,CAAC;AAC7C,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAmB,CAAC,CAAC;AAC/C,QAAM,CAAC,WAAW,YAAY,IAAI,SAAyD,IAAI;AAE/F,YAAU,MAAM;AACd,QAAI,CAAC,QAAQ,EAAG;AAEhB,UAAM,QAAQ,CAAC,OACZ,KAAK,IAAI,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,OAAO;AAE1D,UAAM,OAAO,MAAM;AACjB,YAAM,OAAO,SAAS;AACtB,cAAQ,MAAM,KAAK,aAAa,cAAc,CAAC,CAAC;AAChD,eAAS,MAAM,KAAK,aAAa,eAAe,CAAC,CAAC;AAAA,IACpD;AAEA,SAAK;AAEL,UAAM,WAAW,IAAI,iBAAiB,IAAI;AAC1C,aAAS,QAAQ,SAAS,iBAAiB;AAAA,MACzC,YAAY;AAAA,MACZ,iBAAiB,CAAC,gBAAgB,eAAe;AAAA,IACnD,CAAC;AACD,WAAO,MAAM,SAAS,WAAW;AAAA,EACnC,GAAG,CAAC,CAAC;AAEL,YAAU,MAAM;AACd,QAAI,CAAC,QAAQ,EAAG;AAChB,WAAO,wBAAwB,EAAE,KAAK,YAAY;AAAA,EACpD,GAAG,CAAC,CAAC;AAEL,SAAO,EAAE,MAAM,OAAO,UAAU;AAClC;AAEA,SAAS,cAAc,EAAE,MAAM,IAAI,GAAkC;AACnE,QAAM,EAAE,EAAE,IAAI,eAAe;AAC7B,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aACE;AAAA,QAAC;AAAA;AAAA,UACC,SAAS,MAAM,IAAI,MAAM;AAAA,UACzB,cAAY,EAAE,cAAc;AAAA,UAC5B,OAAO,EAAE,cAAc;AAAA,UACvB,WAAU;AAAA,UACX;AAAA;AAAA,MAED;AAAA,IAEJ,KAAK;AACH,aACE;AAAA,QAAC;AAAA;AAAA,UACC,SAAS,MAAM,IAAI,SAAS;AAAA,UAC5B,cAAY,EAAE,iBAAiB;AAAA,UAC/B,OAAO,EAAE,iBAAiB;AAAA,UAC1B,WAAU;AAAA,UACX;AAAA;AAAA,MAED;AAAA,IAEJ,KAAK;AACH,aACE;AAAA,QAAC;AAAA;AAAA,UACC,SAAS,MAAM,IAAI,eAAe;AAAA,UAClC,cAAY,EAAE,iBAAiB;AAAA,UAC/B,OAAO,EAAE,iBAAiB;AAAA,UAC1B,WAAU;AAAA,UACX;AAAA;AAAA,MAED;AAAA,IAEJ;AACE,aAAO;AAAA,EACX;AACF;AAEO,SAAS,qBAAqB;AACnC,QAAM,EAAE,MAAM,UAAU,IAAI,kBAAkB;AAC9C,MAAI,CAAC,aAAa,KAAK,WAAW,EAAG,QAAO;AAC5C,QAAM,MAAM,UAAU,iBAAiB;AACvC,SACE,oBAAC,SAAI,WAAU,mBACZ,eAAK,IAAI,CAAC,SACT,oBAAC,iBAAyB,MAAY,OAAlB,IAA4B,CACjD,GACH;AAEJ;AAEO,SAAS,sBAAsB;AACpC,QAAM,EAAE,OAAO,UAAU,IAAI,kBAAkB;AAC/C,MAAI,CAAC,aAAa,MAAM,WAAW,EAAG,QAAO;AAC7C,QAAM,MAAM,UAAU,iBAAiB;AACvC,SACE,oBAAC,SAAI,WAAU,mBACZ,gBAAM,IAAI,CAAC,SACV,oBAAC,iBAAyB,MAAY,OAAlB,IAA4B,CACjD,GACH;AAEJ;;;AChHA,SAAS,aAAAA,kBAAiB;AAOnB,SAAS,kBAAkB;AAChC,EAAAC,WAAU,MAAM;AACd,QAAI,CAAC,QAAQ,EAAG;AAOhB,QAAI,cAAc,EAAG;AAGrB,aAAS,gBAAgB,UAAU,IAAI,eAAe;AAGtD,QAAI,UAAU,UAAU,SAAS,SAAS,GAAG;AAC3C,eAAS,gBAAgB,UAAU,IAAI,wBAAwB;AAAA,IACjE;AAUA,QAAI;AACJ,QAAI,WAAW;AACf,QAAI,UAAU,UAAU,SAAS,OAAO,GAAG;AACzC,eAAS,gBAAgB,UAAU,IAAI,sBAAsB;AAC7D,aAAO,wBAAwB,EAAE,KAAK,CAAC,EAAE,iBAAiB,MAAM;AAC9D,cAAM,MAAM,iBAAiB;AAC7B,cAAM,gBAAgB,YAAY;AAChC,cAAI;AACF,qBAAS,gBAAgB,UAAU;AAAA,cACjC;AAAA,cACA,MAAM,IAAI,YAAY;AAAA,YACxB;AAAA,UACF,QAAQ;AAAA,UAER;AAAA,QACF;AACA,sBAAc;AAGd,YAAI,UAAU,aAAa,EAAE,KAAK,CAAC,aAAa;AAC9C,cAAI,SAAU,UAAS;AAAA,cAClB,mBAAkB;AAAA,QACzB,CAAC;AAAA,MACH,CAAC;AAAA,IACH;AAEA,mBAAe,cAAc;AAC3B,UAAI;AACF,cAAM,EAAE,OAAO,IAAI,MAAM,OAAO,sBAAsB;AACtD,cAAM,MAAM,MAAM,OAAsB,yBAAyB;AACjE,YAAI,CAAC,IAAK;AAEV,cAAM,IAAI,SAAS,IAAI,MAAM,GAAG,CAAC,GAAG,EAAE;AACtC,cAAM,IAAI,SAAS,IAAI,MAAM,GAAG,CAAC,GAAG,EAAE;AACtC,cAAM,IAAI,SAAS,IAAI,MAAM,GAAG,CAAC,GAAG,EAAE;AAEtC,cAAM,OAAO,SAAS;AAEtB,aAAK,MAAM,YAAY,qBAAqB,iBAAiB,GAAG,GAAG,GAAG,IAAI,CAAC;AAC3E,aAAK,MAAM,YAAY,sBAAsB,iBAAiB,GAAG,GAAG,GAAG,IAAI,CAAC;AAC5E,aAAK,MAAM,YAAY,sBAAsB,iBAAiB,GAAG,GAAG,GAAG,IAAI,CAAC;AAC5E,aAAK,MAAM,YAAY,sBAAsB,GAAG;AAChD,aAAK,MAAM,YAAY,sBAAsB,iBAAiB,GAAG,GAAG,GAAG,KAAK,CAAC;AAC7E,aAAK,MAAM,YAAY,sBAAsB,iBAAiB,GAAG,GAAG,GAAG,KAAK,CAAC;AAAA,MAC/E,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,gBAAY;AAEZ,mBAAe,sBAAsB;AACnC,UAAI;AACF,cAAM,EAAE,OAAO,IAAI,MAAM,OAAO,sBAAsB;AACtD,cAAM,SAAS,MAAM;AAAA,UACnB;AAAA,QACF;AACA,cAAM,OAAO,SAAS;AAGtB,aAAK,aAAa,gBAAgB,OAAO,KAAK,KAAK,GAAG,CAAC;AACvD,aAAK,aAAa,iBAAiB,OAAO,MAAM,KAAK,GAAG,CAAC;AAEzD,aAAK,UAAU,OAAO,uBAAuB,wBAAwB,uBAAuB,uBAAuB;AACnH,cAAM,OAAO,OAAO,SAChB,WACA,OAAO,KAAK,UAAU,OAAO,MAAM,SACjC,SACA,OAAO,KAAK,SACV,SACA,OAAO,MAAM,SACX,UACA;AACV,YAAI,KAAM,MAAK,UAAU,IAAI,kBAAkB,IAAI,EAAE;AAQrD,cAAM,YAAY,UAAU,UAAU,SAAS,SAAS;AACxD,cAAM,YAAY,CAAC,UACjB,UAAU,IAAI,IAAI,YAAY,QAAQ,KAAK,KAAK,QAAQ,MAAM,QAAQ,KAAK;AAC7E,YAAI,OAAO,QAAQ;AAMjB,eAAK,MAAM,YAAY,gBAAgB,MAAM;AAC7C,eAAK,MAAM,YAAY,iBAAiB,KAAK;AAAA,QAC/C,OAAO;AACL,eAAK,MAAM,YAAY,gBAAgB,GAAG,UAAU,OAAO,KAAK,MAAM,CAAC,IAAI;AAC3E,eAAK,MAAM,YAAY,iBAAiB,GAAG,UAAU,OAAO,MAAM,MAAM,CAAC,IAAI;AAAA,QAC/E;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,wBAAoB;AAEpB,WAAO,MAAM;AACX,iBAAW;AACX,wBAAkB;AAAA,IACpB;AAAA,EACF,GAAG,CAAC,CAAC;AACP;AAGA,SAAS,iBAAiB,GAAW,GAAW,GAAW,QAAwB;AACjF,QAAM,SAAS,CAAC,MAAc;AAC5B,QAAI,SAAS,GAAG;AAEd,aAAO,KAAK,MAAM,KAAK,MAAM,KAAK,MAAM;AAAA,IAC1C;AAEA,WAAO,KAAK,MAAM,KAAK,IAAI,OAAO;AAAA,EACpC;AAEA,QAAM,QAAQ,CAAC,MAAc,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,CAAC,CAAC;AACzD,QAAM,KAAK,MAAM,OAAO,CAAC,CAAC;AAC1B,QAAM,KAAK,MAAM,OAAO,CAAC,CAAC;AAC1B,QAAM,KAAK,MAAM,OAAO,CAAC,CAAC;AAE1B,SAAO,IAAI,GAAG,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,GAAG,GAAG,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,GAAG,GAAG,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC;AACnH;;;ACzJA,eAAsB,aAAa,KAAa;AAC9C,MAAI,yBAAyB,QAAQ;AACnC,UAAM,EAAE,QAAQ,IAAI,MAAM,OAAO,2BAA2B;AAC5D,UAAM,QAAQ,GAAG;AAAA,EACnB,OAAO;AACL,WAAO,KAAK,KAAK,UAAU,qBAAqB;AAAA,EAClD;AACF;;;ACmCA,SAAS,cAAc,GAAW,GAAmB;AACnD,QAAM,SAAS,EAAE,MAAM,GAAG,EAAE,IAAI,MAAM;AACtC,QAAM,SAAS,EAAE,MAAM,GAAG,EAAE,IAAI,MAAM;AACtC,QAAM,MAAM,KAAK,IAAI,OAAO,QAAQ,OAAO,MAAM;AAEjD,WAAS,IAAI,GAAG,IAAI,KAAK,KAAK;AAC5B,UAAM,KAAK,IAAI,OAAO,SAAS,OAAO,CAAC,IAAI;AAC3C,UAAM,KAAK,IAAI,OAAO,SAAS,OAAO,CAAC,IAAI;AAC3C,QAAI,OAAO,GAAI,QAAO,KAAK;AAAA,EAC7B;AACA,SAAO;AACT;AAGA,SAAS,0BAA0B,QAA2C;AAC5E,QAAM,KAAK,UAAU,UAAU,YAAY;AAC3C,MAAI;AAEJ,MAAI,GAAG,SAAS,KAAK,GAAG;AACtB,eAAW,CAAC,MAAM;AAAA,EACpB,WAAW,GAAG,SAAS,KAAK,GAAG;AAC7B,eAAW,CAAC,QAAQ,cAAc,MAAM;AAAA,EAC1C,OAAO;AACL,eAAW,CAAC,aAAa,MAAM;AAAA,EACjC;AAEA,aAAW,WAAW,UAAU;AAC9B,UAAM,QAAQ,OAAO,KAAK,CAAC,MAAM,EAAE,KAAK,YAAY,EAAE,SAAS,OAAO,CAAC;AACvE,QAAI,MAAO,QAAO,MAAM;AAAA,EAC1B;AACA,SAAO;AACT;AAcO,SAAS,cAAc,QAAgC;AAC5D,QAAM,cAAc,gCAAgC,OAAO,IAAI,sBAAsB,OAAO,WAAW,EAAE;AAEzG,iBAAe,gBAAiC;AAC9C,QAAI,yBAAyB,QAAQ;AACnC,UAAI;AACF,cAAM,EAAE,WAAW,IAAI,MAAM,OAAO,qBAAqB;AACzD,eAAO,MAAM,WAAW;AAAA,MAC1B,QAAQ;AAAA,MAER;AAAA,IACF;AACA,WAAO,OAAO;AAAA,EAChB;AAEA,iBAAe,kBAAyC;AACtD,QAAI;AACF,YAAM,WAAW,MAAM,MAAM,aAAa;AAAA,QACxC,SAAS,EAAE,QAAQ,8BAA8B;AAAA,QACjD,QAAQ,YAAY,QAAQ,IAAK;AAAA,MACnC,CAAC;AAED,UAAI,CAAC,SAAS,GAAI,QAAO,EAAE,QAAQ,QAAQ;AAE3C,YAAM,WAA4B,MAAM,SAAS,KAAK;AAGtD,YAAM,WAAW,SAAS;AAAA,QACxB,CAAC,MAAM,EAAE,SAAS,WAAW,OAAO,SAAS,KAAK,CAAC,EAAE;AAAA,MACvD;AAEA,UAAI,SAAS,WAAW,EAAG,QAAO,EAAE,QAAQ,aAAa;AAEzD,YAAM,SAAS,SAAS,CAAC;AACzB,YAAM,gBAAgB,OAAO,SAAS,MAAM,OAAO,UAAU,MAAM;AACnE,YAAM,iBAAiB,MAAM,cAAc;AAE3C,UAAI,cAAc,eAAe,cAAc,IAAI,GAAG;AACpD,eAAO;AAAA,UACL,QAAQ;AAAA,UACR,eAAe;AAAA,UACf,YAAY,OAAO;AAAA,UACnB,aAAa,0BAA0B,OAAO,MAAM,KAAK,OAAO;AAAA,QAClE;AAAA,MACF;AAEA,aAAO,EAAE,QAAQ,aAAa;AAAA,IAChC,QAAQ;AACN,aAAO,EAAE,QAAQ,QAAQ;AAAA,IAC3B;AAAA,EACF;AAEA,SAAO,EAAE,eAAe,gBAAgB;AAC1C;;;ACpJA,SAAS,kBAAAC,uBAAsB;AAC/B,SAAS,sBAAsB;AAC/B,SAAS,QAAQ,aAAa;AAsBlB,gBAAAC,YAAA;AANL,SAAS,aAAa,EAAE,SAAS,YAAY,UAAU,GAAsB;AAClF,QAAM,EAAE,EAAE,IAAID,gBAAe;AAE7B,SACE,gBAAAC;AAAA,IAAC;AAAA;AAAA,MACC,MAAI;AAAA,MACJ,MAAM,gBAAAA,KAAC,kBAAe,MAAM,IAAI,QAAO,QAAO,WAAU,kCAAiC;AAAA,MACzF;AAAA,MACA,cAAc,EAAE,cAAc;AAAA,MAC9B,QACE,gBAAAA,KAAC,OAAE,MAAM,YAAY,QAAO,UAAS,KAAI,uBAAsB,WAAU,YACvE,0BAAAA,KAAC,UAAO,SAAQ,UAAU,YAAE,iBAAiB,GAAE,GACjD;AAAA,MAGD,YAAE,oBAAoB,EAAE,QAAQ,CAAC;AAAA;AAAA,EACpC;AAEJ;;;ACpCA,SAAS,YAAAC,WAAU,mBAAmB;AACtC,SAAS,kBAAAC,uBAAsB;AAC/B,SAAS,kBAAAC,iBAAgB,iBAAiB,aAAa,eAAe;AACtE,SAAS,UAAAC,eAAc;AA6Cf,mBACE,OAAAC,MADF;AAzBD,SAAS,aAAa,EAAE,SAAS,QAAQ,GAAsB;AACpE,QAAM,EAAE,EAAE,IAAIC,gBAAe;AAC7B,QAAM,CAAC,OAAO,QAAQ,IAAIC,UAAuC,MAAM;AACvE,QAAM,CAAC,QAAQ,SAAS,IAAIA,UAA8B,IAAI;AAC9D,QAAM,CAAC,gBAAgB,iBAAiB,IAAIA,UAAiB,EAAE;AAE/D,QAAM,cAAc,YAAY,YAAY;AAC1C,aAAS,UAAU;AACnB,UAAM,CAAC,SAAS,YAAY,IAAI,MAAM,QAAQ,IAAI;AAAA,MAChD,QAAQ,cAAc;AAAA,MACtB,QAAQ,gBAAgB;AAAA,IAC1B,CAAC;AACD,sBAAkB,OAAO;AACzB,cAAU,YAAY;AACtB,aAAS,MAAM;AAAA,EACjB,GAAG,CAAC,OAAO,CAAC;AAGZ,MAAI,UAAU,QAAQ;AACpB,gBAAY;AAAA,EACd;AAEA,SACE,qBAAC,SAAI,WAAU,yCACZ;AAAA,cAAU,cACT,iCACE;AAAA,sBAAAF,KAAC,mBAAgB,MAAM,IAAI,QAAO,QAAO,WAAU,+CAA8C;AAAA,MACjG,gBAAAA,KAAC,OAAE,WAAU,iGACV,YAAE,iBAAiB,GACtB;AAAA,OACF;AAAA,IAGD,UAAU,UAAU,QAAQ,WAAW,sBACtC,iCACE;AAAA,sBAAAA,KAAC,mBAAgB,MAAM,IAAI,QAAO,QAAO,WAAU,kCAAiC;AAAA,MACpF,qBAAC,SAAI,WAAU,eACb;AAAA,wBAAAA,KAAC,OAAE,WAAU,yGACV,YAAE,oBAAoB,EAAE,SAAS,OAAO,cAAc,CAAC,GAC1D;AAAA,QACA,gBAAAA,KAAC,OAAE,WAAU,8FACV,YAAE,yBAAyB,EAAE,SAAS,eAAe,CAAC,GACzD;AAAA,SACF;AAAA,MACA,qBAAC,SAAI,WAAU,cACb;AAAA,6BAACG,SAAA,EAAO,SAAQ,UAAS,SAAS,MAAM,aAAa,OAAO,eAAe,OAAO,cAAc,EAAE,GAChG;AAAA,0BAAAH,KAACI,iBAAA,EAAe,MAAM,IAAI,QAAO,QAAO;AAAA,UACvC,EAAE,iBAAiB;AAAA,WACtB;AAAA,QACA,gBAAAJ,KAACG,SAAA,EAAO,SAAQ,WAAU,SAAS,SAChC,YAAE,cAAc,GACnB;AAAA,SACF;AAAA,OACF;AAAA,IAGD,UAAU,UAAU,QAAQ,WAAW,gBACtC,iCACE;AAAA,sBAAAH,KAAC,eAAY,MAAM,IAAI,QAAO,QAAO,WAAU,mCAAkC;AAAA,MACjF,qBAAC,SAAI,WAAU,eACb;AAAA,wBAAAA,KAAC,OAAE,WAAU,yGACV,YAAE,iBAAiB,GACtB;AAAA,QACA,gBAAAA,KAAC,OAAE,WAAU,8FACV,YAAE,yBAAyB,EAAE,SAAS,eAAe,CAAC,GACzD;AAAA,SACF;AAAA,MACA,gBAAAA,KAACG,SAAA,EAAO,SAAQ,WAAU,SAAS,SAChC,YAAE,eAAe,GACpB;AAAA,OACF;AAAA,IAGD,UAAU,UAAU,QAAQ,WAAW,WACtC,iCACE;AAAA,sBAAAH,KAAC,WAAQ,MAAM,IAAI,QAAO,QAAO,WAAU,mCAAkC;AAAA,MAC7E,gBAAAA,KAAC,OAAE,WAAU,iGACV,YAAE,cAAc,GACnB;AAAA,MACA,gBAAAA,KAACG,SAAA,EAAO,SAAQ,WAAU,SAAS,SAChC,YAAE,eAAe,GACpB;AAAA,OACF;AAAA,KAEJ;AAEJ;","names":["useEffect","useEffect","useTranslation","jsx","useState","useTranslation","ArrowSquareOut","Button","jsx","useTranslation","useState","Button","ArrowSquareOut"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open a URL outside the app: the system browser on Tauri (desktop and mobile,
|
|
3
|
+
* via the opener plugin -- window.open is a no-op in the Tauri WebView), a new
|
|
4
|
+
* tab on the web.
|
|
5
|
+
*
|
|
6
|
+
* Requires the `@tauri-apps/plugin-opener` peer plus its capability grant in the
|
|
7
|
+
* Tauri app (`opener:allow-open-url`).
|
|
8
|
+
*/
|
|
9
|
+
export declare function openExternal(url: string): Promise<void>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface UpdateBannerProps {
|
|
2
|
+
version: string;
|
|
3
|
+
releaseUrl: string;
|
|
4
|
+
onDismiss: () => void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* "Update available" notification for the desktop build. A persistent DS
|
|
8
|
+
* Toast (floating below the header) with a download action and a dismiss
|
|
9
|
+
* button -- the caller unmounts it on dismiss, so `open` is always true.
|
|
10
|
+
*
|
|
11
|
+
* Requires the app's locales to define `update.available` (interpolates
|
|
12
|
+
* `{{version}}`), `update.download`, and `update.later`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function UpdateBanner({ version, releaseUrl, onDismiss }: UpdateBannerProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Updater } from './updater';
|
|
2
|
+
interface UpdateDialogProps {
|
|
3
|
+
/** The app's updater (from `createUpdater`); drives the check. */
|
|
4
|
+
updater: Updater;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* "Check for updates" dialog body for the desktop build (mount inside a DS
|
|
9
|
+
* BottomDrawer). Auto-runs the check on first render, then shows the result
|
|
10
|
+
* with a download or up-to-date state.
|
|
11
|
+
*
|
|
12
|
+
* Requires the app's locales to define `update.checking`, `update.available`
|
|
13
|
+
* (interpolates `{{version}}`), `update.currentVersion` (interpolates
|
|
14
|
+
* `{{version}}`), `update.upToDate`, `update.download`, `update.later`,
|
|
15
|
+
* `update.error`, and `common.cancel`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function UpdateDialog({ updater, onClose }: UpdateDialogProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Desktop in-app update checker for direct-download Tauri builds. Polls a
|
|
3
|
+
* GitHub repo's releases, matches this app's tag prefix, compares semver
|
|
4
|
+
* against the running version, and resolves the best installer asset for the
|
|
5
|
+
* current platform. Store-distributed builds (App Store, Snap, ...) update
|
|
6
|
+
* through the store and should never call this.
|
|
7
|
+
*/
|
|
8
|
+
export interface UpdateResult {
|
|
9
|
+
status: 'update-available' | 'up-to-date' | 'error';
|
|
10
|
+
latestVersion?: string;
|
|
11
|
+
releaseUrl?: string;
|
|
12
|
+
downloadUrl?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface UpdaterConfig {
|
|
15
|
+
/** GitHub `owner/repo` whose releases list is polled. */
|
|
16
|
+
repo: string;
|
|
17
|
+
/** Tag prefix marking this app's releases, e.g. `chip-away-v`. */
|
|
18
|
+
tagPrefix: string;
|
|
19
|
+
/**
|
|
20
|
+
* Version used when the app is not running under Tauri (the web/PWA build,
|
|
21
|
+
* where `@tauri-apps/api/app` is unavailable) -- typically the build-time
|
|
22
|
+
* version constant. Under Tauri the real version comes from the runtime.
|
|
23
|
+
*/
|
|
24
|
+
fallbackVersion: string;
|
|
25
|
+
/** Releases page size to fetch (default 10). */
|
|
26
|
+
perPage?: number;
|
|
27
|
+
}
|
|
28
|
+
export interface Updater {
|
|
29
|
+
/** The running app's version (Tauri runtime, else `fallbackVersion`). */
|
|
30
|
+
getAppVersion(): Promise<string>;
|
|
31
|
+
/** Check the configured repo for a newer release. */
|
|
32
|
+
checkForUpdates(): Promise<UpdateResult>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Build an updater bound to one app's release config. Give it the app's repo,
|
|
36
|
+
* tag prefix, and build-time version:
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* export const updater = createUpdater({
|
|
40
|
+
* repo: 'whiskeyjack-net/downloads',
|
|
41
|
+
* tagPrefix: 'chip-away-v',
|
|
42
|
+
* fallbackVersion: __APP_VERSION__,
|
|
43
|
+
* })
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare function createUpdater(config: UpdaterConfig): Updater;
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@whiskeyjack-net/tauri",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Tauri-native app-shell layer for the Whiskeyjack design system: per-platform CSD window controls, OS accent integration, and desktop guards.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./css/window-controls": "./css/window-controls.css"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"@phosphor-icons/react": "^2.0.0",
|
|
20
|
+
"@tauri-apps/api": "^2.0.0",
|
|
21
|
+
"@tauri-apps/plugin-opener": "^2.0.0",
|
|
22
|
+
"@whiskeyjack-net/design-system": "^0.3.0",
|
|
23
|
+
"react": "^18.0.0",
|
|
24
|
+
"react-dom": "^18.0.0",
|
|
25
|
+
"react-i18next": "^14.0.0 || ^15.0.0"
|
|
26
|
+
},
|
|
27
|
+
"peerDependenciesMeta": {
|
|
28
|
+
"@tauri-apps/plugin-opener": {
|
|
29
|
+
"optional": true
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
}
|
|
35
|
+
}
|