@xenide-io/the-old-ui-theme 0.6.12 → 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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  import {
4
4
  useCallback,
5
- useEffect,
5
+ useLayoutEffect,
6
6
  useState,
7
7
  type PointerEvent as ReactPointerEvent,
8
8
  } from "react";
@@ -13,6 +13,27 @@ export const SIDEBAR_MIN_EXPANDED_WIDTH = 180;
13
13
  export const SIDEBAR_MAX_WIDTH = 640;
14
14
  export const SIDEBAR_DEFAULT_WIDTH = 320;
15
15
 
16
+ /** Shared across Tides / TurtleTime / Kraken / ShellStack / Nakama. */
17
+ export const SUITE_SIDEBAR_WIDTH_KEY = "shellstack:sidebar-width";
18
+ export const SUITE_SIDEBAR_WIDTH_COOKIE = "shellstack_sidebar_width";
19
+
20
+ const LEGACY_SIDEBAR_WIDTH_KEYS = [
21
+ "tides-sidebar-width",
22
+ "tt-sidebar-width",
23
+ "kraken-sidebar-width",
24
+ "shellstack-sidebar-width",
25
+ "nakama-sidebar-width",
26
+ ] as const;
27
+
28
+ function isSuiteSidebarKey(storageKey: string): boolean {
29
+ return (
30
+ storageKey === SUITE_SIDEBAR_WIDTH_KEY ||
31
+ LEGACY_SIDEBAR_WIDTH_KEYS.includes(
32
+ storageKey as (typeof LEGACY_SIDEBAR_WIDTH_KEYS)[number],
33
+ )
34
+ );
35
+ }
36
+
16
37
  /** Desktop column width: icon rail when collapsed, otherwise the persisted width. */
17
38
  export function sidebarColumnWidth(
18
39
  width: number,
@@ -33,34 +54,149 @@ function snapWidth(width: number): number {
33
54
  return Math.min(SIDEBAR_MAX_WIDTH, width);
34
55
  }
35
56
 
57
+ function parseWidth(raw: string | null | undefined): number | null {
58
+ if (!raw) return null;
59
+ const parsed = Number.parseInt(raw, 10);
60
+ return Number.isNaN(parsed) ? null : parsed;
61
+ }
62
+
63
+ function suiteCookieDomainAttr(): string {
64
+ const host = window.location.hostname;
65
+ if (host === "localhost" || /^\d{1,3}(?:\.\d{1,3}){3}$/.test(host)) {
66
+ return "";
67
+ }
68
+ // tides.localhost / turtletime.localhost must share one cookie.
69
+ if (host.endsWith(".localhost")) {
70
+ return "; Domain=.localhost";
71
+ }
72
+ const known = new Set([
73
+ "tides",
74
+ "kraken",
75
+ "turtletime",
76
+ "time",
77
+ "nakama",
78
+ "app",
79
+ "shellstack",
80
+ "portal",
81
+ ]);
82
+ const parts = host.split(".");
83
+ if (parts.length >= 3 && known.has(parts[0] ?? "")) {
84
+ return `; Domain=.${parts.slice(1).join(".")}`;
85
+ }
86
+ if (parts.length >= 2) return `; Domain=.${parts.slice(1).join(".")}`;
87
+ return "";
88
+ }
89
+
90
+ function readCookie(name: string): string | null {
91
+ const prefix = `${name}=`;
92
+ for (const part of document.cookie.split(";")) {
93
+ const trimmed = part.trim();
94
+ if (trimmed.startsWith(prefix)) {
95
+ return decodeURIComponent(trimmed.slice(prefix.length));
96
+ }
97
+ }
98
+ return null;
99
+ }
100
+
101
+ function writeCookie(name: string, value: string): void {
102
+ const secure = window.location.protocol === "https:" ? "; Secure" : "";
103
+ document.cookie = `${name}=${encodeURIComponent(value)}; Path=/; Max-Age=31536000; SameSite=Lax${suiteCookieDomainAttr()}${secure}`;
104
+ }
105
+
106
+ function clearCookie(name: string): void {
107
+ const secure = window.location.protocol === "https:" ? "; Secure" : "";
108
+ document.cookie = `${name}=; Path=/; Max-Age=0; SameSite=Lax${suiteCookieDomainAttr()}${secure}`;
109
+ }
110
+
111
+ export function readSidebarWidth(storageKey: string): number | null {
112
+ try {
113
+ if (isSuiteSidebarKey(storageKey)) {
114
+ const fromCookie = parseWidth(readCookie(SUITE_SIDEBAR_WIDTH_COOKIE));
115
+ if (fromCookie != null) return fromCookie;
116
+ const shared = parseWidth(localStorage.getItem(SUITE_SIDEBAR_WIDTH_KEY));
117
+ if (shared != null) return shared;
118
+ for (const key of LEGACY_SIDEBAR_WIDTH_KEYS) {
119
+ const legacy = parseWidth(localStorage.getItem(key));
120
+ if (legacy != null) return legacy;
121
+ }
122
+ }
123
+ return parseWidth(localStorage.getItem(storageKey));
124
+ } catch {
125
+ return null;
126
+ }
127
+ }
128
+
129
+ export function persistSidebarWidth(storageKey: string, value: number): void {
130
+ try {
131
+ localStorage.setItem(storageKey, String(value));
132
+ if (isSuiteSidebarKey(storageKey)) {
133
+ localStorage.setItem(SUITE_SIDEBAR_WIDTH_KEY, String(value));
134
+ }
135
+ } catch {
136
+ // localStorage can be unavailable; the cookie still crosses apps.
137
+ }
138
+ if (isSuiteSidebarKey(storageKey)) {
139
+ try {
140
+ writeCookie(SUITE_SIDEBAR_WIDTH_COOKIE, String(value));
141
+ } catch {
142
+ // Cookie write can fail in restricted browser contexts.
143
+ }
144
+ }
145
+ }
146
+
147
+ export function resetSuiteSidebarWidth(): void {
148
+ try {
149
+ localStorage.removeItem(SUITE_SIDEBAR_WIDTH_KEY);
150
+ for (const key of LEGACY_SIDEBAR_WIDTH_KEYS) {
151
+ localStorage.removeItem(key);
152
+ }
153
+ localStorage.removeItem("kraken-sidebar-collapsed");
154
+ } catch {
155
+ // Ignore storage failures.
156
+ }
157
+ try {
158
+ clearCookie(SUITE_SIDEBAR_WIDTH_COOKIE);
159
+ } catch {
160
+ // Ignore cookie failures.
161
+ }
162
+ }
163
+
36
164
  /**
37
165
  * Shared suite sidebar resizing: drag below the threshold to collapse,
38
166
  * drag the rail edge out to expand, and double-click to reset.
167
+ * Suite app keys share one width via cookie so app-switch keeps it.
39
168
  */
40
169
  export function useSidebarWidth(
41
- storageKey: string,
170
+ storageKey = SUITE_SIDEBAR_WIDTH_KEY,
42
171
  defaultWidth = SIDEBAR_DEFAULT_WIDTH,
43
172
  ) {
44
173
  const fallbackWidth = snapWidth(clampWidth(defaultWidth));
174
+ // ponytail: always start at the default so SSR and the first client render
175
+ // match. Reading the cookie in useState() hydrates 320 vs 412 and React
176
+ // keeps the server width — TurtleTime / ShellStack / Nakama stay stuck.
45
177
  const [width, setWidth] = useState(fallbackWidth);
46
178
  const [narrowViewport, setNarrowViewport] = useState(false);
47
179
  const collapsed = narrowViewport || width <= SIDEBAR_COLLAPSE_THRESHOLD;
48
180
 
49
- useEffect(() => {
50
- let stored: string | null = null;
51
- try {
52
- stored = localStorage.getItem(storageKey);
53
- } catch {
54
- // Storage can be unavailable in private or restricted browser contexts.
55
- }
56
- if (!stored) return;
57
- const parsed = Number.parseInt(stored, 10);
58
- if (!Number.isNaN(parsed)) {
59
- queueMicrotask(() => setWidth(snapWidth(clampWidth(parsed))));
60
- }
181
+ useLayoutEffect(() => {
182
+ const sync = () => {
183
+ const stored = readSidebarWidth(storageKey);
184
+ if (stored == null) return;
185
+ setWidth(snapWidth(clampWidth(stored)));
186
+ };
187
+ sync();
188
+ // App switch can restore a bf-cached page. Re-read the shared cookie.
189
+ window.addEventListener("pageshow", sync);
190
+ window.addEventListener("focus", sync);
191
+ document.addEventListener("visibilitychange", sync);
192
+ return () => {
193
+ window.removeEventListener("pageshow", sync);
194
+ window.removeEventListener("focus", sync);
195
+ document.removeEventListener("visibilitychange", sync);
196
+ };
61
197
  }, [storageKey]);
62
198
 
63
- useEffect(() => {
199
+ useLayoutEffect(() => {
64
200
  const media = window.matchMedia("(max-width: 639px)");
65
201
  const sync = () => setNarrowViewport(media.matches);
66
202
  sync();
@@ -70,11 +206,7 @@ export function useSidebarWidth(
70
206
 
71
207
  const persist = useCallback(
72
208
  (value: number) => {
73
- try {
74
- localStorage.setItem(storageKey, String(value));
75
- } catch {
76
- // A persisted preference is optional; resizing should still work.
77
- }
209
+ persistSidebarWidth(storageKey, value);
78
210
  },
79
211
  [storageKey],
80
212
  );