@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/{ai-message-blocknote-DIUICX3G.mjs → ai-message-blocknote-WABTHFY7.mjs} +1 -1
- package/dist/{chunk-VIWW7AW2.mjs → chunk-NOI42JNZ.mjs} +47 -3
- package/dist/suite.d.mts +9 -2
- package/dist/suite.d.ts +9 -2
- package/dist/suite.js +267 -90
- package/dist/suite.mjs +149 -27
- package/package.json +1 -1
- package/src/styles/suite-skin.css +8 -0
- package/src/suite/components/suite-notification-bell.tsx +9 -7
- package/src/suite/components/suite-sidebar.tsx +4 -1
- package/src/suite/components/theme-provider.test.tsx +15 -0
- package/src/suite/components/theme-provider.tsx +3 -0
- package/src/suite/index.ts +5 -0
- package/src/suite/lib/use-lock-mobile-page-zoom.test.tsx +65 -0
- package/src/suite/lib/use-lock-mobile-page-zoom.ts +51 -0
- package/src/suite/lib/use-sidebar-width-hook.test.tsx +73 -0
- package/src/suite/lib/use-sidebar-width.test.ts +77 -0
- package/src/suite/lib/use-sidebar-width.ts +152 -20
package/dist/suite.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
import {
|
|
7
7
|
SuiteThemeProvider,
|
|
8
8
|
useSuiteTheme
|
|
9
|
-
} from "./chunk-
|
|
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__ */
|
|
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
|
|
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-
|
|
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
|
-
|
|
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
|
|
2860
|
-
|
|
2861
|
-
const
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
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
|
-
|
|
2949
|
+
writeCookie(SUITE_SIDEBAR_WIDTH_COOKIE, String(value));
|
|
2868
2950
|
} catch (e) {
|
|
2869
2951
|
}
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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="
|
|
182
|
-
<
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
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;
|
package/src/suite/index.ts
CHANGED
|
@@ -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
|
+
});
|