@xenide-io/the-old-ui-theme 0.6.11 → 0.6.13

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/dist/suite.mjs CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  import {
7
7
  SuiteThemeProvider,
8
8
  useSuiteTheme
9
- } from "./chunk-VIWW7AW2.mjs";
9
+ } from "./chunk-NOI42JNZ.mjs";
10
10
  import {
11
11
  __spreadProps,
12
12
  __spreadValues
@@ -1286,10 +1286,10 @@ function SuiteNotificationBell({
1286
1286
  panelClassName: "w-80 overflow-hidden p-0",
1287
1287
  open,
1288
1288
  onOpenChange,
1289
- trigger: /* @__PURE__ */ jsxs7("span", { className: "relative inline-flex h-11 w-11 items-center justify-center rounded-full text-ph-mutedtext transition hover:bg-ph-muted hover:text-ph-ink", children: [
1289
+ trigger: /* @__PURE__ */ jsx7("span", { className: "inline-flex h-11 w-11 items-center justify-center rounded-full text-ph-mutedtext transition hover:bg-ph-muted hover:text-ph-ink", children: /* @__PURE__ */ jsxs7("span", { className: "relative inline-flex", children: [
1290
1290
  /* @__PURE__ */ jsx7(Bell, { className: "h-5 w-5", strokeWidth: 1.75, "aria-hidden": true }),
1291
- unread > 0 ? /* @__PURE__ */ jsx7("span", { className: "absolute -right-0.5 -top-0.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-ph-brand px-1 text-[10px] font-semibold leading-none text-white ring-2 ring-ph-surface", children: unread > 9 ? "9+" : unread }) : null
1292
- ] }),
1291
+ unread > 0 ? /* @__PURE__ */ jsx7("span", { className: "absolute right-0 top-0 flex h-3.5 min-w-3.5 translate-x-0.5 -translate-y-0.5 items-center justify-center rounded-full bg-ph-brand px-1 text-[9px] font-semibold leading-none text-white ring-1 ring-ph-surface", children: unread > 9 ? "9+" : unread }) : null
1292
+ ] }) }),
1293
1293
  children: [
1294
1294
  /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between border-b border-ph-border px-3 py-2", children: [
1295
1295
  /* @__PURE__ */ jsx7("span", { className: "text-sm font-semibold text-ph-ink", children: "Notifications" }),
@@ -2182,7 +2182,7 @@ import {
2182
2182
  WarningTriangle
2183
2183
  } from "iconoir-react";
2184
2184
  import { Fragment as Fragment5, jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
2185
- var SuiteAiBlockNoteMessage = lazy(() => import("./ai-message-blocknote-DIUICX3G.mjs"));
2185
+ var SuiteAiBlockNoteMessage = lazy(() => import("./ai-message-blocknote-WABTHFY7.mjs"));
2186
2186
  var SUITE_OPEN_ASK_AI_EVENT = "shellstack:open-ask-ai";
2187
2187
  var SUITE_ASK_AI_OPEN_KEY = "shellstack:shelly-open";
2188
2188
  var SUITE_ASK_AI_OPEN_QUERY = "shelly";
@@ -2836,7 +2836,7 @@ function SuiteWorkspaceSwitcher({
2836
2836
  // src/suite/lib/use-sidebar-width.ts
2837
2837
  import {
2838
2838
  useCallback as useCallback3,
2839
- useEffect as useEffect7,
2839
+ useLayoutEffect as useLayoutEffect2,
2840
2840
  useState as useState9
2841
2841
  } from "react";
2842
2842
  var SIDEBAR_RAIL_WIDTH = 56;
@@ -2844,6 +2844,20 @@ var SIDEBAR_COLLAPSE_THRESHOLD = 80;
2844
2844
  var SIDEBAR_MIN_EXPANDED_WIDTH = 180;
2845
2845
  var SIDEBAR_MAX_WIDTH = 640;
2846
2846
  var SIDEBAR_DEFAULT_WIDTH = 320;
2847
+ var SUITE_SIDEBAR_WIDTH_KEY = "shellstack:sidebar-width";
2848
+ var SUITE_SIDEBAR_WIDTH_COOKIE = "shellstack_sidebar_width";
2849
+ var LEGACY_SIDEBAR_WIDTH_KEYS = [
2850
+ "tides-sidebar-width",
2851
+ "tt-sidebar-width",
2852
+ "kraken-sidebar-width",
2853
+ "shellstack-sidebar-width",
2854
+ "nakama-sidebar-width"
2855
+ ];
2856
+ function isSuiteSidebarKey(storageKey) {
2857
+ return storageKey === SUITE_SIDEBAR_WIDTH_KEY || LEGACY_SIDEBAR_WIDTH_KEYS.includes(
2858
+ storageKey
2859
+ );
2860
+ }
2847
2861
  function sidebarColumnWidth(width, collapsed, _chatOpen = false) {
2848
2862
  if (collapsed) return SIDEBAR_RAIL_WIDTH;
2849
2863
  return width;
@@ -2856,24 +2870,123 @@ function snapWidth(width) {
2856
2870
  if (width < SIDEBAR_MIN_EXPANDED_WIDTH) return SIDEBAR_MIN_EXPANDED_WIDTH;
2857
2871
  return Math.min(SIDEBAR_MAX_WIDTH, width);
2858
2872
  }
2859
- function useSidebarWidth(storageKey, defaultWidth = SIDEBAR_DEFAULT_WIDTH) {
2860
- const fallbackWidth = snapWidth(clampWidth(defaultWidth));
2861
- const [width, setWidth] = useState9(fallbackWidth);
2862
- const [narrowViewport, setNarrowViewport] = useState9(false);
2863
- const collapsed = narrowViewport || width <= SIDEBAR_COLLAPSE_THRESHOLD;
2864
- useEffect7(() => {
2865
- let stored = null;
2873
+ function parseWidth(raw) {
2874
+ if (!raw) return null;
2875
+ const parsed = Number.parseInt(raw, 10);
2876
+ return Number.isNaN(parsed) ? null : parsed;
2877
+ }
2878
+ function suiteCookieDomainAttr() {
2879
+ var _a;
2880
+ const host = window.location.hostname;
2881
+ if (host === "localhost" || /^\d{1,3}(?:\.\d{1,3}){3}$/.test(host)) {
2882
+ return "";
2883
+ }
2884
+ if (host.endsWith(".localhost")) {
2885
+ return "; Domain=.localhost";
2886
+ }
2887
+ const known = /* @__PURE__ */ new Set([
2888
+ "tides",
2889
+ "kraken",
2890
+ "turtletime",
2891
+ "time",
2892
+ "nakama",
2893
+ "app",
2894
+ "shellstack",
2895
+ "portal"
2896
+ ]);
2897
+ const parts = host.split(".");
2898
+ if (parts.length >= 3 && known.has((_a = parts[0]) != null ? _a : "")) {
2899
+ return `; Domain=.${parts.slice(1).join(".")}`;
2900
+ }
2901
+ if (parts.length >= 2) return `; Domain=.${parts.slice(1).join(".")}`;
2902
+ return "";
2903
+ }
2904
+ function readCookie(name) {
2905
+ const prefix = `${name}=`;
2906
+ for (const part of document.cookie.split(";")) {
2907
+ const trimmed = part.trim();
2908
+ if (trimmed.startsWith(prefix)) {
2909
+ return decodeURIComponent(trimmed.slice(prefix.length));
2910
+ }
2911
+ }
2912
+ return null;
2913
+ }
2914
+ function writeCookie(name, value) {
2915
+ const secure = window.location.protocol === "https:" ? "; Secure" : "";
2916
+ document.cookie = `${name}=${encodeURIComponent(value)}; Path=/; Max-Age=31536000; SameSite=Lax${suiteCookieDomainAttr()}${secure}`;
2917
+ }
2918
+ function clearCookie(name) {
2919
+ const secure = window.location.protocol === "https:" ? "; Secure" : "";
2920
+ document.cookie = `${name}=; Path=/; Max-Age=0; SameSite=Lax${suiteCookieDomainAttr()}${secure}`;
2921
+ }
2922
+ function readSidebarWidth(storageKey) {
2923
+ try {
2924
+ if (isSuiteSidebarKey(storageKey)) {
2925
+ const fromCookie = parseWidth(readCookie(SUITE_SIDEBAR_WIDTH_COOKIE));
2926
+ if (fromCookie != null) return fromCookie;
2927
+ const shared = parseWidth(localStorage.getItem(SUITE_SIDEBAR_WIDTH_KEY));
2928
+ if (shared != null) return shared;
2929
+ for (const key of LEGACY_SIDEBAR_WIDTH_KEYS) {
2930
+ const legacy = parseWidth(localStorage.getItem(key));
2931
+ if (legacy != null) return legacy;
2932
+ }
2933
+ }
2934
+ return parseWidth(localStorage.getItem(storageKey));
2935
+ } catch (e) {
2936
+ return null;
2937
+ }
2938
+ }
2939
+ function persistSidebarWidth(storageKey, value) {
2940
+ try {
2941
+ localStorage.setItem(storageKey, String(value));
2942
+ if (isSuiteSidebarKey(storageKey)) {
2943
+ localStorage.setItem(SUITE_SIDEBAR_WIDTH_KEY, String(value));
2944
+ }
2945
+ } catch (e) {
2946
+ }
2947
+ if (isSuiteSidebarKey(storageKey)) {
2866
2948
  try {
2867
- stored = localStorage.getItem(storageKey);
2949
+ writeCookie(SUITE_SIDEBAR_WIDTH_COOKIE, String(value));
2868
2950
  } catch (e) {
2869
2951
  }
2870
- if (!stored) return;
2871
- const parsed = Number.parseInt(stored, 10);
2872
- if (!Number.isNaN(parsed)) {
2873
- queueMicrotask(() => setWidth(snapWidth(clampWidth(parsed))));
2952
+ }
2953
+ }
2954
+ function resetSuiteSidebarWidth() {
2955
+ try {
2956
+ localStorage.removeItem(SUITE_SIDEBAR_WIDTH_KEY);
2957
+ for (const key of LEGACY_SIDEBAR_WIDTH_KEYS) {
2958
+ localStorage.removeItem(key);
2874
2959
  }
2960
+ localStorage.removeItem("kraken-sidebar-collapsed");
2961
+ } catch (e) {
2962
+ }
2963
+ try {
2964
+ clearCookie(SUITE_SIDEBAR_WIDTH_COOKIE);
2965
+ } catch (e) {
2966
+ }
2967
+ }
2968
+ function useSidebarWidth(storageKey = SUITE_SIDEBAR_WIDTH_KEY, defaultWidth = SIDEBAR_DEFAULT_WIDTH) {
2969
+ const fallbackWidth = snapWidth(clampWidth(defaultWidth));
2970
+ const [width, setWidth] = useState9(fallbackWidth);
2971
+ const [narrowViewport, setNarrowViewport] = useState9(false);
2972
+ const collapsed = narrowViewport || width <= SIDEBAR_COLLAPSE_THRESHOLD;
2973
+ useLayoutEffect2(() => {
2974
+ const sync = () => {
2975
+ const stored = readSidebarWidth(storageKey);
2976
+ if (stored == null) return;
2977
+ setWidth(snapWidth(clampWidth(stored)));
2978
+ };
2979
+ sync();
2980
+ window.addEventListener("pageshow", sync);
2981
+ window.addEventListener("focus", sync);
2982
+ document.addEventListener("visibilitychange", sync);
2983
+ return () => {
2984
+ window.removeEventListener("pageshow", sync);
2985
+ window.removeEventListener("focus", sync);
2986
+ document.removeEventListener("visibilitychange", sync);
2987
+ };
2875
2988
  }, [storageKey]);
2876
- useEffect7(() => {
2989
+ useLayoutEffect2(() => {
2877
2990
  const media = window.matchMedia("(max-width: 639px)");
2878
2991
  const sync = () => setNarrowViewport(media.matches);
2879
2992
  sync();
@@ -2882,10 +2995,7 @@ function useSidebarWidth(storageKey, defaultWidth = SIDEBAR_DEFAULT_WIDTH) {
2882
2995
  }, []);
2883
2996
  const persist = useCallback3(
2884
2997
  (value) => {
2885
- try {
2886
- localStorage.setItem(storageKey, String(value));
2887
- } catch (e) {
2888
- }
2998
+ persistSidebarWidth(storageKey, value);
2889
2999
  },
2890
3000
  [storageKey]
2891
3001
  );
@@ -3156,7 +3266,7 @@ function SuiteSettingsMobileNav({
3156
3266
 
3157
3267
  // src/suite/components/suite-sidebar.tsx
3158
3268
  import Link2 from "next/link";
3159
- import { useCallback as useCallback4, useEffect as useEffect8 } from "react";
3269
+ import { useCallback as useCallback4, useEffect as useEffect7 } from "react";
3160
3270
  import { NavArrowLeft, Sparks as Sparks2 } from "iconoir-react";
3161
3271
  import { Fragment as Fragment7, jsx as jsx22, jsxs as jsxs19 } from "react/jsx-runtime";
3162
3272
  function renderNode(node, collapsed) {
@@ -3188,7 +3298,7 @@ function SuiteSidebar({
3188
3298
  },
3189
3299
  [onAskAiOpenChange]
3190
3300
  );
3191
- useEffect8(() => {
3301
+ useEffect7(() => {
3192
3302
  if (!onAskAiOpenChange) return;
3193
3303
  function onOpen() {
3194
3304
  onRequestExpand == null ? void 0 : onRequestExpand();
@@ -3197,7 +3307,7 @@ function SuiteSidebar({
3197
3307
  window.addEventListener(SUITE_OPEN_ASK_AI_EVENT, onOpen);
3198
3308
  return () => window.removeEventListener(SUITE_OPEN_ASK_AI_EVENT, onOpen);
3199
3309
  }, [onAskAiOpenChange, onRequestExpand, setAskAi]);
3200
- useEffect8(() => {
3310
+ useEffect7(() => {
3201
3311
  if (!chatOpen) return;
3202
3312
  function onKey(event) {
3203
3313
  if (event.key === "Escape") setAskAi(false);
@@ -3373,7 +3483,14 @@ function SuiteSidebar({
3373
3483
  );
3374
3484
  return collapsed ? /* @__PURE__ */ jsx22(Tooltip, { content: item.label, side: "right", children: link }, item.label) : link;
3375
3485
  }) }) }),
3376
- secondaryNav ? /* @__PURE__ */ jsx22("div", { className: "shrink-0", "data-test": "suite-sidebar-secondary-nav", children: secondaryNav }) : null
3486
+ secondaryNav ? /* @__PURE__ */ jsx22(
3487
+ "div",
3488
+ {
3489
+ className: "min-h-0 flex-1",
3490
+ "data-test": "suite-sidebar-secondary-nav",
3491
+ children: secondaryNav
3492
+ }
3493
+ ) : null
3377
3494
  ] })
3378
3495
  ]
3379
3496
  }
@@ -3518,6 +3635,8 @@ export {
3518
3635
  SUITE_ICON_NAMES,
3519
3636
  SUITE_MUTATED_EVENT,
3520
3637
  SUITE_OPEN_ASK_AI_EVENT,
3638
+ SUITE_SIDEBAR_WIDTH_COOKIE,
3639
+ SUITE_SIDEBAR_WIDTH_KEY,
3521
3640
  SUITE_SPRINGS,
3522
3641
  SourceAppGlyph,
3523
3642
  SuiteAiPanel,
@@ -3570,8 +3689,11 @@ export {
3570
3689
  m,
3571
3690
  openSuiteAskAi,
3572
3691
  pathWithSuiteAskAiOpen,
3692
+ persistSidebarWidth,
3573
3693
  persistSuiteAskAiOpen,
3694
+ readSidebarWidth,
3574
3695
  readSuiteAskAiOpen,
3696
+ resetSuiteSidebarWidth,
3575
3697
  resolveSuiteNotificationHref,
3576
3698
  sidebarColumnWidth,
3577
3699
  sourceToSuiteApp,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenide-io/the-old-ui-theme",
3
- "version": "0.6.11",
3
+ "version": "0.6.13",
4
4
  "description": "Props-driven React components and theme tokens inspired by classic interface design \u2014 optimized for Next.js apps and internal tools.",
5
5
  "author": "Xenide",
6
6
  "license": "MIT",
@@ -41,6 +41,14 @@ html {
41
41
  --suite-reading-max: 48rem; /* ~max-w-3xl / 65ch */
42
42
  }
43
43
 
44
+ /* Phones/tablets only: pan/scroll, no pinch-zoom of the page. Desktop zoom stays. */
45
+ @media (hover: none) and (pointer: coarse) {
46
+ html,
47
+ body {
48
+ touch-action: pan-x pan-y;
49
+ }
50
+ }
51
+
44
52
  @media (min-width: 1536px) {
45
53
  html {
46
54
  --suite-page-pad-x-lg: var(--suite-space-7);
@@ -178,13 +178,15 @@ export function SuiteNotificationBell({
178
178
  open={open}
179
179
  onOpenChange={onOpenChange}
180
180
  trigger={
181
- <span className="relative inline-flex h-11 w-11 items-center justify-center rounded-full text-ph-mutedtext transition hover:bg-ph-muted hover:text-ph-ink">
182
- <Bell className="h-5 w-5" strokeWidth={1.75} aria-hidden />
183
- {unread > 0 ? (
184
- <span className="absolute -right-0.5 -top-0.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-ph-brand px-1 text-[10px] font-semibold leading-none text-white ring-2 ring-ph-surface">
185
- {unread > 9 ? "9+" : unread}
186
- </span>
187
- ) : null}
181
+ <span className="inline-flex h-11 w-11 items-center justify-center rounded-full text-ph-mutedtext transition hover:bg-ph-muted hover:text-ph-ink">
182
+ <span className="relative inline-flex">
183
+ <Bell className="h-5 w-5" strokeWidth={1.75} aria-hidden />
184
+ {unread > 0 ? (
185
+ <span className="absolute right-0 top-0 flex h-3.5 min-w-3.5 translate-x-0.5 -translate-y-0.5 items-center justify-center rounded-full bg-ph-brand px-1 text-[9px] font-semibold leading-none text-white ring-1 ring-ph-surface">
186
+ {unread > 9 ? "9+" : unread}
187
+ </span>
188
+ ) : null}
189
+ </span>
188
190
  </span>
189
191
  }
190
192
  >
@@ -303,7 +303,10 @@ export function SuiteSidebar({
303
303
  </nav>
304
304
 
305
305
  {secondaryNav ? (
306
- <div className="shrink-0" data-test="suite-sidebar-secondary-nav">
306
+ <div
307
+ className="min-h-0 flex-1"
308
+ data-test="suite-sidebar-secondary-nav"
309
+ >
307
310
  {secondaryNav}
308
311
  </div>
309
312
  ) : null}
@@ -17,12 +17,27 @@ beforeEach(() => {
17
17
  getItem: () => "dark",
18
18
  setItem: vi.fn(),
19
19
  });
20
+ vi.stubGlobal(
21
+ "matchMedia",
22
+ (query: string) =>
23
+ ({
24
+ matches: query.includes("prefers-color-scheme: dark"),
25
+ media: query,
26
+ addEventListener: vi.fn(),
27
+ removeEventListener: vi.fn(),
28
+ addListener: vi.fn(),
29
+ removeListener: vi.fn(),
30
+ dispatchEvent: vi.fn(),
31
+ onchange: null,
32
+ }) satisfies MediaQueryList,
33
+ );
20
34
  });
21
35
 
22
36
  afterEach(() => {
23
37
  cleanup();
24
38
  vi.unstubAllGlobals();
25
39
  document.documentElement.removeAttribute("data-theme");
40
+ document.documentElement.removeAttribute("data-suite-lock-page-zoom");
26
41
  document.documentElement.classList.remove("dark");
27
42
  document.getElementById("theme-color-meta")?.remove();
28
43
  });
@@ -10,6 +10,8 @@ import {
10
10
  type ReactNode,
11
11
  } from 'react';
12
12
 
13
+ import { useLockMobilePageZoom } from '../lib/use-lock-mobile-page-zoom';
14
+
13
15
  export type SuiteTheme = 'system' | 'light' | 'dark';
14
16
  export type SuiteResolvedTheme = 'light' | 'dark';
15
17
 
@@ -104,6 +106,7 @@ export function SuiteThemeProvider({
104
106
  config: SuiteThemeConfig;
105
107
  children: ReactNode;
106
108
  }) {
109
+ useLockMobilePageZoom();
107
110
  const [state, setState] = useState<{
108
111
  theme: SuiteTheme;
109
112
  resolvedTheme: SuiteResolvedTheme;
@@ -103,6 +103,11 @@ export {
103
103
  SIDEBAR_MAX_WIDTH,
104
104
  SIDEBAR_MIN_EXPANDED_WIDTH,
105
105
  SIDEBAR_RAIL_WIDTH,
106
+ SUITE_SIDEBAR_WIDTH_COOKIE,
107
+ SUITE_SIDEBAR_WIDTH_KEY,
108
+ persistSidebarWidth,
109
+ readSidebarWidth,
110
+ resetSuiteSidebarWidth,
106
111
  sidebarColumnWidth,
107
112
  useSidebarWidth,
108
113
  } from "./lib/use-sidebar-width";
@@ -0,0 +1,65 @@
1
+ import { cleanup, render } from "@testing-library/react";
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+
4
+ import { useLockMobilePageZoom } from "./use-lock-mobile-page-zoom";
5
+
6
+ function Probe() {
7
+ useLockMobilePageZoom();
8
+ return null;
9
+ }
10
+
11
+ function stubMedia(matches: boolean) {
12
+ vi.stubGlobal(
13
+ "matchMedia",
14
+ (query: string) =>
15
+ ({
16
+ matches: query.includes("pointer: coarse") ? matches : false,
17
+ media: query,
18
+ addEventListener: vi.fn(),
19
+ removeEventListener: vi.fn(),
20
+ addListener: vi.fn(),
21
+ removeListener: vi.fn(),
22
+ dispatchEvent: vi.fn(),
23
+ onchange: null,
24
+ }) satisfies MediaQueryList,
25
+ );
26
+ }
27
+
28
+ afterEach(() => {
29
+ cleanup();
30
+ vi.unstubAllGlobals();
31
+ document.documentElement.removeAttribute("data-suite-lock-page-zoom");
32
+ document.head.innerHTML = "";
33
+ });
34
+
35
+ describe("mobile page zoom lock", () => {
36
+ it("locks the viewport on phones and tablets", () => {
37
+ document.head.innerHTML =
38
+ '<meta name="viewport" content="width=device-width, initial-scale=1">';
39
+ stubMedia(true);
40
+
41
+ render(<Probe />);
42
+
43
+ expect(document.documentElement).toHaveAttribute(
44
+ "data-suite-lock-page-zoom",
45
+ );
46
+ expect(
47
+ document.querySelector('meta[name="viewport"]')?.getAttribute("content"),
48
+ ).toContain("user-scalable=no");
49
+ });
50
+
51
+ it("leaves desktop page zoom alone", () => {
52
+ document.head.innerHTML =
53
+ '<meta name="viewport" content="width=device-width, initial-scale=1">';
54
+ stubMedia(false);
55
+
56
+ render(<Probe />);
57
+
58
+ expect(document.documentElement).not.toHaveAttribute(
59
+ "data-suite-lock-page-zoom",
60
+ );
61
+ expect(
62
+ document.querySelector('meta[name="viewport"]')?.getAttribute("content"),
63
+ ).toBe("width=device-width, initial-scale=1");
64
+ });
65
+ });
@@ -0,0 +1,51 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+
5
+ const MOBILE_PAGE_ZOOM_QUERY = "(hover: none) and (pointer: coarse)";
6
+ const LOCKED_VIEWPORT =
7
+ "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover";
8
+
9
+ function preventGesture(event: Event) {
10
+ event.preventDefault();
11
+ }
12
+
13
+ /**
14
+ * App-like mobile chrome: no pinch/page zoom. Desktop zoom is unchanged.
15
+ * CSS `touch-action` covers Chromium; viewport + gesture events cover iOS.
16
+ */
17
+ export function useLockMobilePageZoom() {
18
+ useEffect(() => {
19
+ if (typeof window.matchMedia !== "function") return;
20
+ const media = window.matchMedia(MOBILE_PAGE_ZOOM_QUERY);
21
+ const meta = document.querySelector('meta[name="viewport"]');
22
+ const original = meta?.getAttribute("content");
23
+
24
+ const apply = () => {
25
+ const lock = media.matches;
26
+ document.documentElement.toggleAttribute(
27
+ "data-suite-lock-page-zoom",
28
+ lock,
29
+ );
30
+ document.removeEventListener("gesturestart", preventGesture);
31
+ document.removeEventListener("gesturechange", preventGesture);
32
+ if (lock) {
33
+ meta?.setAttribute("content", LOCKED_VIEWPORT);
34
+ document.addEventListener("gesturestart", preventGesture);
35
+ document.addEventListener("gesturechange", preventGesture);
36
+ return;
37
+ }
38
+ if (original) meta?.setAttribute("content", original);
39
+ };
40
+
41
+ apply();
42
+ media.addEventListener("change", apply);
43
+ return () => {
44
+ media.removeEventListener("change", apply);
45
+ document.documentElement.removeAttribute("data-suite-lock-page-zoom");
46
+ if (original) meta?.setAttribute("content", original);
47
+ document.removeEventListener("gesturestart", preventGesture);
48
+ document.removeEventListener("gesturechange", preventGesture);
49
+ };
50
+ }, []);
51
+ }
@@ -0,0 +1,73 @@
1
+ import { cleanup, render, waitFor } from "@testing-library/react";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+
4
+ import {
5
+ persistSidebarWidth,
6
+ SUITE_SIDEBAR_WIDTH_KEY,
7
+ useSidebarWidth,
8
+ } from "./use-sidebar-width";
9
+
10
+ function WidthProbe() {
11
+ const { width } = useSidebarWidth();
12
+ return <div data-testid="width">{width}</div>;
13
+ }
14
+
15
+ const store = new Map<string, string>();
16
+
17
+ beforeEach(() => {
18
+ store.clear();
19
+ vi.stubGlobal("localStorage", {
20
+ getItem: (key: string) => store.get(key) ?? null,
21
+ setItem: (key: string, value: string) => {
22
+ store.set(key, value);
23
+ },
24
+ removeItem: (key: string) => {
25
+ store.delete(key);
26
+ },
27
+ clear: () => {
28
+ store.clear();
29
+ },
30
+ });
31
+ vi.stubGlobal(
32
+ "matchMedia",
33
+ (query: string) =>
34
+ ({
35
+ matches: false,
36
+ media: query,
37
+ addEventListener: vi.fn(),
38
+ removeEventListener: vi.fn(),
39
+ addListener: vi.fn(),
40
+ removeListener: vi.fn(),
41
+ dispatchEvent: vi.fn(),
42
+ onchange: null,
43
+ }) satisfies MediaQueryList,
44
+ );
45
+ document.cookie.split(";").forEach((part) => {
46
+ const name = part.split("=")[0]?.trim();
47
+ if (name) document.cookie = `${name}=; Path=/; Max-Age=0`;
48
+ });
49
+ });
50
+
51
+ afterEach(() => {
52
+ cleanup();
53
+ vi.unstubAllGlobals();
54
+ });
55
+
56
+ describe("useSidebarWidth app-switch sync", () => {
57
+ it("picks up a cookie written by another app after pageshow", async () => {
58
+ persistSidebarWidth(SUITE_SIDEBAR_WIDTH_KEY, 280);
59
+ render(<WidthProbe />);
60
+ expect(document.querySelector("[data-testid=width]")?.textContent).toBe(
61
+ "280",
62
+ );
63
+
64
+ persistSidebarWidth("tides-sidebar-width", 480);
65
+ window.dispatchEvent(new Event("pageshow"));
66
+
67
+ await waitFor(() =>
68
+ expect(document.querySelector("[data-testid=width]")?.textContent).toBe(
69
+ "480",
70
+ ),
71
+ );
72
+ });
73
+ });
@@ -0,0 +1,77 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import {
4
+ persistSidebarWidth,
5
+ readSidebarWidth,
6
+ resetSuiteSidebarWidth,
7
+ SUITE_SIDEBAR_WIDTH_COOKIE,
8
+ SUITE_SIDEBAR_WIDTH_KEY,
9
+ } from "./use-sidebar-width";
10
+
11
+ const store = new Map<string, string>();
12
+
13
+ beforeEach(() => {
14
+ store.clear();
15
+ vi.stubGlobal("localStorage", {
16
+ getItem: (key: string) => store.get(key) ?? null,
17
+ setItem: (key: string, value: string) => {
18
+ store.set(key, value);
19
+ },
20
+ removeItem: (key: string) => {
21
+ store.delete(key);
22
+ },
23
+ clear: () => {
24
+ store.clear();
25
+ },
26
+ });
27
+ document.cookie.split(";").forEach((part) => {
28
+ const name = part.split("=")[0]?.trim();
29
+ if (name) document.cookie = `${name}=; Path=/; Max-Age=0`;
30
+ });
31
+ });
32
+
33
+ afterEach(() => {
34
+ vi.unstubAllGlobals();
35
+ });
36
+
37
+ describe("suite sidebar width", () => {
38
+ it("shares one width across app keys via cookie", () => {
39
+ persistSidebarWidth("tides-sidebar-width", 412);
40
+
41
+ expect(readSidebarWidth("kraken-sidebar-width")).toBe(412);
42
+ expect(readSidebarWidth("tt-sidebar-width")).toBe(412);
43
+ expect(readSidebarWidth(SUITE_SIDEBAR_WIDTH_KEY)).toBe(412);
44
+ expect(localStorage.getItem(SUITE_SIDEBAR_WIDTH_KEY)).toBe("412");
45
+ expect(document.cookie).toContain(`${SUITE_SIDEBAR_WIDTH_COOKIE}=412`);
46
+ });
47
+
48
+ it("prefers the cookie over a stale per-app localStorage value", () => {
49
+ localStorage.setItem("kraken-sidebar-width", "240");
50
+ persistSidebarWidth("tides-sidebar-width", 480);
51
+
52
+ expect(readSidebarWidth("kraken-sidebar-width")).toBe(480);
53
+ });
54
+
55
+ it("migrates a legacy per-app width when nothing shared is stored", () => {
56
+ localStorage.setItem("tt-sidebar-width", "360");
57
+
58
+ expect(readSidebarWidth("tides-sidebar-width")).toBe(360);
59
+ });
60
+
61
+ it("does not write the suite cookie for a demo-only key", () => {
62
+ persistSidebarWidth("the-old-ui-demo-sidebar-width", 220);
63
+
64
+ expect(document.cookie).not.toContain(SUITE_SIDEBAR_WIDTH_COOKIE);
65
+ expect(localStorage.getItem(SUITE_SIDEBAR_WIDTH_KEY)).toBeNull();
66
+ expect(readSidebarWidth("tides-sidebar-width")).toBeNull();
67
+ });
68
+
69
+ it("clears the shared cookie and legacy keys", () => {
70
+ persistSidebarWidth("kraken-sidebar-width", 400);
71
+ resetSuiteSidebarWidth();
72
+
73
+ expect(readSidebarWidth("tides-sidebar-width")).toBeNull();
74
+ expect(localStorage.getItem("kraken-sidebar-width")).toBeNull();
75
+ expect(document.cookie).not.toContain(`${SUITE_SIDEBAR_WIDTH_COOKIE}=400`);
76
+ });
77
+ });