@whiskeyjack-net/tauri 0.4.4 → 0.5.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 CHANGED
@@ -35,6 +35,19 @@ off Tauri, so it is safe to ship in a plain web/PWA build.
35
35
  rendering the exact per-side buttons the backend reports (`data-wc-left` /
36
36
  `data-wc-right` on `<html>`), so the app matches each desktop's convention.
37
37
  Mount them in the DS `AppHeader`'s `chrome` slot.
38
+ - **`NarrowWindowChrome`** (+ `WJ_NARROW_CHROME_PX`) – the 28px strip a desktop
39
+ window narrower than `md` needs. The DS `AppHeader` is `hidden md:block`, so
40
+ below 768px the window loses its drag region and, on Windows and Linux, the
41
+ only way to close itself; width is not a proxy for platform once an app ships
42
+ as a resizable window. Render it as a child of the app shell, above the
43
+ header. The paired clearance is in `css/window-controls`: below `md` it
44
+ reserves the same height on the DS main scroller (`main[data-app-main]`), so
45
+ content starts under the strip rather than behind it. **macOS draws no
46
+ buttons here** – the traffic lights are the OS's own – but the clearance is
47
+ exactly what keeps them off the first line of content, which is what a page
48
+ whose first element happens to have generous padding hides until you look at
49
+ its siblings. Anything else that needs the same gap can take
50
+ `.tauri-narrow-clear`.
38
51
  - **`useSystemAccent()`** – on Tauri desktop, reads the OS accent color and
39
52
  overrides the DS `--color-accent-*` variables, adds the `.tauri-desktop` /
40
53
  `.tauri-platform-*` markers, and publishes the `--wc-left/right-px`
@@ -37,3 +37,18 @@
37
37
  }
38
38
 
39
39
  .tauri-desktop .tauri-drag-zone { display: block; }
40
+
41
+ /* Narrow desktop windows: reserve the strip `NarrowWindowChrome` occupies.
42
+ Below md the DS AppHeader hides itself, so nothing holds the window controls
43
+ off the content -- on macOS that is the OS's own traffic lights landing on
44
+ the first thing the page renders, and a page whose first element happens to
45
+ have generous padding escapes it while its siblings do not. Scoped to
46
+ .tauri-desktop (set by useSystemAccent only when Tauri AND not mobile), so
47
+ the web build and the real mobile builds, which use env() insets, are
48
+ untouched. */
49
+ @media (max-width: 767px) {
50
+ .tauri-desktop main[data-app-main],
51
+ .tauri-desktop .tauri-narrow-clear {
52
+ padding-top: var(--wj-narrow-chrome, 28px);
53
+ }
54
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { isTauri, isMobileTauri, isDesktopTauri, isMacDesktop, isLinuxDesktop } from './desktop';
2
2
  export { WindowControlsLeft, WindowControlsRight } from './window-controls';
3
+ export { NarrowWindowChrome, WJ_NARROW_CHROME_PX } from './narrow-window-chrome';
3
4
  export { useSystemAccent } from './use-system-accent';
4
5
  export { useWindowDrag } from './use-window-drag';
5
6
  export type { WindowDragProps } from './use-window-drag';
package/dist/index.js CHANGED
@@ -88,8 +88,8 @@ function WindowControlsRight() {
88
88
  return /* @__PURE__ */ jsx("div", { className: "window-controls", children: right.map((kind) => /* @__PURE__ */ jsx(ControlButton, { kind, win }, kind)) });
89
89
  }
90
90
 
91
- // src/use-system-accent.ts
92
- import { useEffect as useEffect2 } from "react";
91
+ // src/use-window-drag.ts
92
+ import { useCallback, useEffect as useEffect2 } from "react";
93
93
 
94
94
  // src/warn.ts
95
95
  var warned = /* @__PURE__ */ new Set();
@@ -109,9 +109,68 @@ function isPermissionError(err) {
109
109
  return String(err).includes("not allowed");
110
110
  }
111
111
 
112
+ // src/use-window-drag.ts
113
+ var windowModule = null;
114
+ function loadWindowModule() {
115
+ windowModule ?? (windowModule = import("@tauri-apps/api/window"));
116
+ return windowModule;
117
+ }
118
+ var INTERACTIVE = 'a, button, input, select, textarea, nav, [role="tab"], [role="button"], [contenteditable]';
119
+ function useWindowDrag() {
120
+ useEffect2(() => {
121
+ if (!isTauri() || isMobileTauri()) return;
122
+ void loadWindowModule().catch(() => {
123
+ });
124
+ }, []);
125
+ const onMouseDown = useCallback((event) => {
126
+ if (!isTauri() || isMobileTauri()) return;
127
+ if (event.button !== 0) return;
128
+ if (event.target?.closest(INTERACTIVE)) return;
129
+ event.preventDefault();
130
+ void loadWindowModule().then(({ getCurrentWindow }) => {
131
+ const win = getCurrentWindow();
132
+ const zoom = event.detail === 2;
133
+ return (zoom ? win.toggleMaximize() : win.startDragging()).catch((err) => {
134
+ const command = zoom ? "toggle-maximize" : "start-dragging";
135
+ if (isPermissionError(err)) {
136
+ warnOnce(
137
+ `window-drag-${command}`,
138
+ `dragging the window was refused. Add \`core:window:allow-${command}\` to src-tauri/capabilities/default.json -- \`core:default\` does not include it (see the package README).`,
139
+ err
140
+ );
141
+ } else {
142
+ warnOnce(`window-drag-${command}`, `dragging the window failed.`, err);
143
+ }
144
+ });
145
+ }).catch(() => {
146
+ });
147
+ }, []);
148
+ return { onMouseDown };
149
+ }
150
+
151
+ // src/narrow-window-chrome.tsx
152
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
153
+ function NarrowWindowChrome() {
154
+ const drag = useWindowDrag();
155
+ return /* @__PURE__ */ jsx2(
156
+ "div",
157
+ {
158
+ ...drag,
159
+ className: "tauri-drag-zone hidden fixed top-0 left-0 right-0 h-7 z-[60] md:!hidden",
160
+ children: /* @__PURE__ */ jsxs("div", { className: "flex items-center h-full px-1.5", children: [
161
+ /* @__PURE__ */ jsx2(WindowControlsLeft, {}),
162
+ /* @__PURE__ */ jsx2("div", { className: "flex-1" }),
163
+ /* @__PURE__ */ jsx2(WindowControlsRight, {})
164
+ ] })
165
+ }
166
+ );
167
+ }
168
+ var WJ_NARROW_CHROME_PX = 28;
169
+
112
170
  // src/use-system-accent.ts
171
+ import { useEffect as useEffect3 } from "react";
113
172
  function useSystemAccent() {
114
- useEffect2(() => {
173
+ useEffect3(() => {
115
174
  if (!isTauri()) return;
116
175
  if (isMobileTauri()) return;
117
176
  document.documentElement.classList.add("tauri-desktop");
@@ -221,46 +280,6 @@ function adjustBrightness(r, g, b, amount) {
221
280
  return `#${rr.toString(16).padStart(2, "0")}${gg.toString(16).padStart(2, "0")}${bb.toString(16).padStart(2, "0")}`;
222
281
  }
223
282
 
224
- // src/use-window-drag.ts
225
- import { useCallback, useEffect as useEffect3 } from "react";
226
- var windowModule = null;
227
- function loadWindowModule() {
228
- windowModule ?? (windowModule = import("@tauri-apps/api/window"));
229
- return windowModule;
230
- }
231
- var INTERACTIVE = 'a, button, input, select, textarea, nav, [role="tab"], [role="button"], [contenteditable]';
232
- function useWindowDrag() {
233
- useEffect3(() => {
234
- if (!isTauri() || isMobileTauri()) return;
235
- void loadWindowModule().catch(() => {
236
- });
237
- }, []);
238
- const onMouseDown = useCallback((event) => {
239
- if (!isTauri() || isMobileTauri()) return;
240
- if (event.button !== 0) return;
241
- if (event.target?.closest(INTERACTIVE)) return;
242
- event.preventDefault();
243
- void loadWindowModule().then(({ getCurrentWindow }) => {
244
- const win = getCurrentWindow();
245
- const zoom = event.detail === 2;
246
- return (zoom ? win.toggleMaximize() : win.startDragging()).catch((err) => {
247
- const command = zoom ? "toggle-maximize" : "start-dragging";
248
- if (isPermissionError(err)) {
249
- warnOnce(
250
- `window-drag-${command}`,
251
- `dragging the window was refused. Add \`core:window:allow-${command}\` to src-tauri/capabilities/default.json -- \`core:default\` does not include it (see the package README).`,
252
- err
253
- );
254
- } else {
255
- warnOnce(`window-drag-${command}`, `dragging the window failed.`, err);
256
- }
257
- });
258
- }).catch(() => {
259
- });
260
- }, []);
261
- return { onMouseDown };
262
- }
263
-
264
283
  // src/open-external.ts
265
284
  async function openExternal(url) {
266
285
  if ("__TAURI_INTERNALS__" in window) {
@@ -346,17 +365,17 @@ function createUpdater(config) {
346
365
  import { useTranslation as useTranslation2 } from "react-i18next";
347
366
  import { ArrowSquareOut } from "@phosphor-icons/react";
348
367
  import { Button, Toast } from "@whiskeyjack-net/design-system";
349
- import { jsx as jsx2 } from "react/jsx-runtime";
368
+ import { jsx as jsx3 } from "react/jsx-runtime";
350
369
  function UpdateBanner({ version, releaseUrl, onDismiss }) {
351
370
  const { t } = useTranslation2();
352
- return /* @__PURE__ */ jsx2(
371
+ return /* @__PURE__ */ jsx3(
353
372
  Toast,
354
373
  {
355
374
  open: true,
356
- icon: /* @__PURE__ */ jsx2(ArrowSquareOut, { size: 16, weight: "bold", className: "text-[var(--color-accent-500)]" }),
375
+ icon: /* @__PURE__ */ jsx3(ArrowSquareOut, { size: 16, weight: "bold", className: "text-[var(--color-accent-500)]" }),
357
376
  onDismiss,
358
377
  dismissLabel: t("update.later"),
359
- action: /* @__PURE__ */ jsx2("a", { href: releaseUrl, target: "_blank", rel: "noopener noreferrer", className: "shrink-0", children: /* @__PURE__ */ jsx2(Button, { variant: "accent", children: t("update.download") }) }),
378
+ action: /* @__PURE__ */ jsx3("a", { href: releaseUrl, target: "_blank", rel: "noopener noreferrer", className: "shrink-0", children: /* @__PURE__ */ jsx3(Button, { variant: "accent", children: t("update.download") }) }),
360
379
  children: t("update.available", { version })
361
380
  }
362
381
  );
@@ -367,7 +386,7 @@ import { useState as useState2, useCallback as useCallback2 } from "react";
367
386
  import { useTranslation as useTranslation3 } from "react-i18next";
368
387
  import { ArrowSquareOut as ArrowSquareOut2, ArrowsClockwise, CheckCircle, Warning } from "@phosphor-icons/react";
369
388
  import { Button as Button2 } from "@whiskeyjack-net/design-system";
370
- import { Fragment, jsx as jsx3, jsxs } from "react/jsx-runtime";
389
+ import { Fragment, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
371
390
  function UpdateDialog({ updater, onClose }) {
372
391
  const { t } = useTranslation3();
373
392
  const [state, setState] = useState2("idle");
@@ -386,43 +405,45 @@ function UpdateDialog({ updater, onClose }) {
386
405
  if (state === "idle") {
387
406
  handleCheck();
388
407
  }
389
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-4 py-2", children: [
390
- state === "checking" && /* @__PURE__ */ jsxs(Fragment, { children: [
391
- /* @__PURE__ */ jsx3(ArrowsClockwise, { size: 40, weight: "bold", className: "text-[var(--color-accent-500)] animate-spin" }),
392
- /* @__PURE__ */ jsx3("p", { className: "text-sm text-[var(--color-text-secondary-light)] dark:text-[var(--color-text-secondary-dark)]", children: t("update.checking") })
408
+ return /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center gap-4 py-2", children: [
409
+ state === "checking" && /* @__PURE__ */ jsxs2(Fragment, { children: [
410
+ /* @__PURE__ */ jsx4(ArrowsClockwise, { size: 40, weight: "bold", className: "text-[var(--color-accent-500)] animate-spin" }),
411
+ /* @__PURE__ */ jsx4("p", { className: "text-sm text-[var(--color-text-secondary-light)] dark:text-[var(--color-text-secondary-dark)]", children: t("update.checking") })
393
412
  ] }),
394
- state === "done" && result?.status === "update-available" && /* @__PURE__ */ jsxs(Fragment, { children: [
395
- /* @__PURE__ */ jsx3(ArrowsClockwise, { size: 40, weight: "bold", className: "text-[var(--color-accent-500)]" }),
396
- /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
397
- /* @__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 }) }),
398
- /* @__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 }) })
413
+ state === "done" && result?.status === "update-available" && /* @__PURE__ */ jsxs2(Fragment, { children: [
414
+ /* @__PURE__ */ jsx4(ArrowsClockwise, { size: 40, weight: "bold", className: "text-[var(--color-accent-500)]" }),
415
+ /* @__PURE__ */ jsxs2("div", { className: "text-center", children: [
416
+ /* @__PURE__ */ jsx4("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 }) }),
417
+ /* @__PURE__ */ jsx4("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 }) })
399
418
  ] }),
400
- /* @__PURE__ */ jsxs("div", { className: "flex gap-3", children: [
401
- /* @__PURE__ */ jsxs(Button2, { variant: "accent", onClick: () => openExternal(result.downloadUrl || result.releaseUrl || ""), children: [
402
- /* @__PURE__ */ jsx3(ArrowSquareOut2, { size: 16, weight: "bold" }),
419
+ /* @__PURE__ */ jsxs2("div", { className: "flex gap-3", children: [
420
+ /* @__PURE__ */ jsxs2(Button2, { variant: "accent", onClick: () => openExternal(result.downloadUrl || result.releaseUrl || ""), children: [
421
+ /* @__PURE__ */ jsx4(ArrowSquareOut2, { size: 16, weight: "bold" }),
403
422
  t("update.download")
404
423
  ] }),
405
- /* @__PURE__ */ jsx3(Button2, { variant: "outline", onClick: onClose, children: t("update.later") })
424
+ /* @__PURE__ */ jsx4(Button2, { variant: "outline", onClick: onClose, children: t("update.later") })
406
425
  ] })
407
426
  ] }),
408
- state === "done" && result?.status === "up-to-date" && /* @__PURE__ */ jsxs(Fragment, { children: [
409
- /* @__PURE__ */ jsx3(CheckCircle, { size: 40, weight: "fill", className: "text-[var(--color-success-500)]" }),
410
- /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
411
- /* @__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") }),
412
- /* @__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 }) })
427
+ state === "done" && result?.status === "up-to-date" && /* @__PURE__ */ jsxs2(Fragment, { children: [
428
+ /* @__PURE__ */ jsx4(CheckCircle, { size: 40, weight: "fill", className: "text-[var(--color-success-500)]" }),
429
+ /* @__PURE__ */ jsxs2("div", { className: "text-center", children: [
430
+ /* @__PURE__ */ jsx4("p", { className: "text-sm font-medium text-[var(--color-text-primary-light)] dark:text-[var(--color-text-primary-dark)]", children: t("update.upToDate") }),
431
+ /* @__PURE__ */ jsx4("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 }) })
413
432
  ] }),
414
- /* @__PURE__ */ jsx3(Button2, { variant: "outline", onClick: onClose, children: t("common.cancel") })
433
+ /* @__PURE__ */ jsx4(Button2, { variant: "outline", onClick: onClose, children: t("common.cancel") })
415
434
  ] }),
416
- state === "done" && result?.status === "error" && /* @__PURE__ */ jsxs(Fragment, { children: [
417
- /* @__PURE__ */ jsx3(Warning, { size: 40, weight: "fill", className: "text-[var(--color-warning-500)]" }),
418
- /* @__PURE__ */ jsx3("p", { className: "text-sm text-[var(--color-text-secondary-light)] dark:text-[var(--color-text-secondary-dark)]", children: t("update.error") }),
419
- /* @__PURE__ */ jsx3(Button2, { variant: "outline", onClick: onClose, children: t("common.cancel") })
435
+ state === "done" && result?.status === "error" && /* @__PURE__ */ jsxs2(Fragment, { children: [
436
+ /* @__PURE__ */ jsx4(Warning, { size: 40, weight: "fill", className: "text-[var(--color-warning-500)]" }),
437
+ /* @__PURE__ */ jsx4("p", { className: "text-sm text-[var(--color-text-secondary-light)] dark:text-[var(--color-text-secondary-dark)]", children: t("update.error") }),
438
+ /* @__PURE__ */ jsx4(Button2, { variant: "outline", onClick: onClose, children: t("common.cancel") })
420
439
  ] })
421
440
  ] });
422
441
  }
423
442
  export {
443
+ NarrowWindowChrome,
424
444
  UpdateBanner,
425
445
  UpdateDialog,
446
+ WJ_NARROW_CHROME_PX,
426
447
  WindowControlsLeft,
427
448
  WindowControlsRight,
428
449
  createUpdater,
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/warn.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'\nimport { warnOnce } from './warn'\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 command `get_system_accent_color` is not registered, so the app ' +\n 'keeps its theme accent instead of the OS one (see the package README).',\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. Falling back to what the platform almost\n // certainly wants makes a fresh scaffold correct on macOS and Windows\n // with no Rust at all; only Linux, where the button layout is a\n // per-desktop setting, genuinely needs the command.\n layout = platformDefaultLayout()\n warnOnce(\n 'get_window_controls_layout',\n 'the command `get_window_controls_layout` is not registered, so window ' +\n 'controls fall back to the platform default. Implement it to read the ' +\n 'desktop’s own button layout (see the package README).',\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\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","const warned = new Set<string>()\n\n/** Vite-style consumers expose this; anything else stays quiet rather than guess. */\nfunction isDev(): boolean {\n try {\n return Boolean((import.meta as ImportMeta & { env?: { DEV?: boolean } }).env?.DEV)\n } catch {\n return false\n }\n}\n\n/**\n * Say what went wrong, once, in development.\n *\n * This pack sits on top of a Rust side it does not own: commands the app has to\n * register, and capabilities the app has to grant. Both fail as rejected\n * promises, and a bare `catch {}` around either produces the same thing -- a\n * window that is subtly wrong with nothing anywhere to explain it. Production\n * stays quiet, because these are developer configuration errors and a user\n * cannot act on them.\n */\nexport function warnOnce(key: string, message: string, err: unknown): void {\n if (warned.has(key) || !isDev()) return\n warned.add(key)\n console.warn(`[@whiskeyjack-net/tauri] ${message}`, err)\n}\n\n/**\n * Whether a rejection is Tauri refusing a call the app has not been granted.\n *\n * Tauri v2 gates each window command behind its own capability, and\n * `core:default` covers fewer of them than the name suggests -- `startDragging`\n * is not in it. The rejection says so plainly, so the distinction is worth\n * drawing: a missing capability is a one-line fix in `capabilities/`, while any\n * other failure is not.\n */\nexport function isPermissionError(err: unknown): boolean {\n return String(err).includes('not allowed')\n}\n","import { useCallback, useEffect, type MouseEvent as ReactMouseEvent } from 'react'\nimport { isTauri, isMobileTauri } from './desktop'\nimport { isPermissionError, warnOnce } from './warn'\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\n * on mount and reusing the promise removes that.\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 * Spread the 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 const zoom = event.detail === 2\n return (zoom ? win.toggleMaximize() : win.startDragging()).catch((err: unknown) => {\n // Tauri refuses this call without the permission: `startDragging`\n // needs `core:window:allow-start-dragging`, which `core:default`\n // does NOT include, so a scaffolded app has a header that does\n // nothing and no hint anywhere as to why.\n const command = zoom ? 'toggle-maximize' : 'start-dragging'\n if (isPermissionError(err)) {\n warnOnce(\n `window-drag-${command}`,\n `dragging the window was refused. Add \\`core:window:allow-${command}\\` ` +\n `to src-tauri/capabilities/default.json -- \\`core:default\\` does not ` +\n `include it (see the package README).`,\n err,\n )\n } else {\n warnOnce(`window-drag-${command}`, `dragging the window failed.`, err)\n }\n })\n })\n .catch(() => {\n // The module itself would not load, which means this is not a Tauri\n // host after all. The gesture simply does nothing, correctly.\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"]}
1
+ {"version":3,"sources":["../src/desktop.ts","../src/window-controls.tsx","../src/use-window-drag.ts","../src/warn.ts","../src/narrow-window-chrome.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 { useCallback, useEffect, type MouseEvent as ReactMouseEvent } from 'react'\nimport { isTauri, isMobileTauri } from './desktop'\nimport { isPermissionError, warnOnce } from './warn'\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\n * on mount and reusing the promise removes that.\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 * Spread the 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 const zoom = event.detail === 2\n return (zoom ? win.toggleMaximize() : win.startDragging()).catch((err: unknown) => {\n // Tauri refuses this call without the permission: `startDragging`\n // needs `core:window:allow-start-dragging`, which `core:default`\n // does NOT include, so a scaffolded app has a header that does\n // nothing and no hint anywhere as to why.\n const command = zoom ? 'toggle-maximize' : 'start-dragging'\n if (isPermissionError(err)) {\n warnOnce(\n `window-drag-${command}`,\n `dragging the window was refused. Add \\`core:window:allow-${command}\\` ` +\n `to src-tauri/capabilities/default.json -- \\`core:default\\` does not ` +\n `include it (see the package README).`,\n err,\n )\n } else {\n warnOnce(`window-drag-${command}`, `dragging the window failed.`, err)\n }\n })\n })\n .catch(() => {\n // The module itself would not load, which means this is not a Tauri\n // host after all. The gesture simply does nothing, correctly.\n })\n }, [])\n\n return { onMouseDown }\n}\n","const warned = new Set<string>()\n\n/** Vite-style consumers expose this; anything else stays quiet rather than guess. */\nfunction isDev(): boolean {\n try {\n return Boolean((import.meta as ImportMeta & { env?: { DEV?: boolean } }).env?.DEV)\n } catch {\n return false\n }\n}\n\n/**\n * Say what went wrong, once, in development.\n *\n * This pack sits on top of a Rust side it does not own: commands the app has to\n * register, and capabilities the app has to grant. Both fail as rejected\n * promises, and a bare `catch {}` around either produces the same thing -- a\n * window that is subtly wrong with nothing anywhere to explain it. Production\n * stays quiet, because these are developer configuration errors and a user\n * cannot act on them.\n */\nexport function warnOnce(key: string, message: string, err: unknown): void {\n if (warned.has(key) || !isDev()) return\n warned.add(key)\n console.warn(`[@whiskeyjack-net/tauri] ${message}`, err)\n}\n\n/**\n * Whether a rejection is Tauri refusing a call the app has not been granted.\n *\n * Tauri v2 gates each window command behind its own capability, and\n * `core:default` covers fewer of them than the name suggests -- `startDragging`\n * is not in it. The rejection says so plainly, so the distinction is worth\n * drawing: a missing capability is a one-line fix in `capabilities/`, while any\n * other failure is not.\n */\nexport function isPermissionError(err: unknown): boolean {\n return String(err).includes('not allowed')\n}\n","import { WindowControlsLeft, WindowControlsRight } from './window-controls'\nimport { useWindowDrag } from './use-window-drag'\n\n/**\n * Window chrome for a desktop window narrower than `md`.\n *\n * The DS `AppHeader` is `hidden md:block`, so a desktop window dragged below\n * 768px loses the header -- and with it the drag region and, on Windows and\n * Linux, the only way to close the window. Width is not a proxy for platform\n * once an app ships as a resizable window. This is the strip that stands in:\n * `WJ_NARROW_CHROME_PX` tall, pinned to the top, with the platform's controls\n * at the edges.\n *\n * Inert everywhere else: the strip is `display: none` until `useSystemAccent`\n * marks the document `.tauri-desktop`, and hidden again from `md` up. Render it\n * as a child of the app shell, above the header.\n *\n * Pair it with the clearance -- `css/window-controls` reserves the same height\n * on the DS main scroller (`main[data-app-main]`) below `md`, so content starts\n * under the strip instead of behind it. On macOS the controls are the OS's own\n * and this component draws none, but the clearance is what keeps the traffic\n * lights off the first line of content.\n */\nexport function NarrowWindowChrome() {\n const drag = useWindowDrag()\n return (\n <div\n {...drag}\n className=\"tauri-drag-zone hidden fixed top-0 left-0 right-0 h-7 z-[60] md:!hidden\"\n >\n <div className=\"flex items-center h-full px-1.5\">\n <WindowControlsLeft />\n <div className=\"flex-1\" />\n <WindowControlsRight />\n </div>\n </div>\n )\n}\n\n/**\n * The strip's height in px, matching `h-7` and the CSS clearance. Exported for\n * an app that has to clear it somewhere the stylesheet cannot reach (a sticky\n * element inside the scroller sticks to the scrollport, not the padding edge).\n */\nexport const WJ_NARROW_CHROME_PX = 28\n","import { useEffect } from 'react'\nimport { isTauri, isMobileTauri } from './desktop'\nimport { warnOnce } from './warn'\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 command `get_system_accent_color` is not registered, so the app ' +\n 'keeps its theme accent instead of the OS one (see the package README).',\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. Falling back to what the platform almost\n // certainly wants makes a fresh scaffold correct on macOS and Windows\n // with no Rust at all; only Linux, where the button layout is a\n // per-desktop setting, genuinely needs the command.\n layout = platformDefaultLayout()\n warnOnce(\n 'get_window_controls_layout',\n 'the command `get_window_controls_layout` is not registered, so window ' +\n 'controls fall back to the platform default. Implement it to read the ' +\n 'desktop’s own button layout (see the package README).',\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\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","jsx","useEffect","useEffect","useTranslation","jsx","useState","useCallback","useTranslation","ArrowSquareOut","Button","jsx","jsxs","useTranslation","useState","useCallback","Button","ArrowSquareOut"]}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Window chrome for a desktop window narrower than `md`.
3
+ *
4
+ * The DS `AppHeader` is `hidden md:block`, so a desktop window dragged below
5
+ * 768px loses the header -- and with it the drag region and, on Windows and
6
+ * Linux, the only way to close the window. Width is not a proxy for platform
7
+ * once an app ships as a resizable window. This is the strip that stands in:
8
+ * `WJ_NARROW_CHROME_PX` tall, pinned to the top, with the platform's controls
9
+ * at the edges.
10
+ *
11
+ * Inert everywhere else: the strip is `display: none` until `useSystemAccent`
12
+ * marks the document `.tauri-desktop`, and hidden again from `md` up. Render it
13
+ * as a child of the app shell, above the header.
14
+ *
15
+ * Pair it with the clearance -- `css/window-controls` reserves the same height
16
+ * on the DS main scroller (`main[data-app-main]`) below `md`, so content starts
17
+ * under the strip instead of behind it. On macOS the controls are the OS's own
18
+ * and this component draws none, but the clearance is what keeps the traffic
19
+ * lights off the first line of content.
20
+ */
21
+ export declare function NarrowWindowChrome(): import("react/jsx-runtime").JSX.Element;
22
+ /**
23
+ * The strip's height in px, matching `h-7` and the CSS clearance. Exported for
24
+ * an app that has to clear it somewhere the stylesheet cannot reach (a sticky
25
+ * element inside the scroller sticks to the scrollport, not the padding edge).
26
+ */
27
+ export declare const WJ_NARROW_CHROME_PX = 28;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whiskeyjack-net/tauri",
3
- "version": "0.4.4",
3
+ "version": "0.5.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.18.0",
37
+ "@whiskeyjack-net/design-system": "^0.19.0",
38
38
  "react": "^18.0.0",
39
39
  "react-dom": "^18.0.0",
40
40
  "react-i18next": "^14.0.0 || ^15.0.0"