zudoku 0.16.3 → 0.17.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.
Files changed (140) hide show
  1. package/dist/app/main.d.ts +1 -1
  2. package/dist/app/main.js +2 -2
  3. package/dist/app/main.js.map +1 -1
  4. package/dist/config/validators/validate.d.ts +44 -44
  5. package/dist/config/validators/validate.js.map +1 -1
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.js +1 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/lib/authentication/authentication.d.ts +2 -2
  10. package/dist/lib/authentication/state.d.ts +1 -1
  11. package/dist/lib/authentication/state.js +5 -3
  12. package/dist/lib/authentication/state.js.map +1 -1
  13. package/dist/lib/authentication/use-broadcast/shared.d.ts +48 -0
  14. package/dist/lib/authentication/use-broadcast/shared.js +243 -0
  15. package/dist/lib/authentication/use-broadcast/shared.js.map +1 -0
  16. package/dist/lib/authentication/use-broadcast/useBroadcast.d.ts +24 -0
  17. package/dist/lib/authentication/use-broadcast/useBroadcast.js +106 -0
  18. package/dist/lib/authentication/use-broadcast/useBroadcast.js.map +1 -0
  19. package/dist/lib/components/ClientOnly.d.ts +4 -2
  20. package/dist/lib/components/ClientOnly.js +1 -1
  21. package/dist/lib/components/ClientOnly.js.map +1 -1
  22. package/dist/lib/components/Header.js +3 -1
  23. package/dist/lib/components/Header.js.map +1 -1
  24. package/dist/lib/components/{DevPortal.d.ts → Zudoku.d.ts} +3 -3
  25. package/dist/lib/components/{DevPortal.js → Zudoku.js} +11 -11
  26. package/dist/lib/components/Zudoku.js.map +1 -0
  27. package/dist/lib/components/context/ZudokuContext.d.ts +4 -4
  28. package/dist/lib/components/context/ZudokuContext.js +1 -1
  29. package/dist/lib/components/context/ZudokuContext.js.map +1 -1
  30. package/dist/lib/components/context/ZudokuProvider.d.ts +2 -2
  31. package/dist/lib/components/context/ZudokuProvider.js.map +1 -1
  32. package/dist/lib/components/index.d.ts +4 -7
  33. package/dist/lib/components/index.js +2 -3
  34. package/dist/lib/components/index.js.map +1 -1
  35. package/dist/lib/core/{DevPortalContext.d.ts → ZudokuContext.d.ts} +5 -5
  36. package/dist/lib/core/{DevPortalContext.js → ZudokuContext.js} +2 -2
  37. package/dist/lib/core/ZudokuContext.js.map +1 -0
  38. package/dist/lib/core/plugins.d.ts +12 -12
  39. package/dist/lib/core/plugins.js.map +1 -1
  40. package/dist/lib/plugins/api-keys/index.d.ts +9 -9
  41. package/dist/lib/plugins/api-keys/index.js.map +1 -1
  42. package/dist/lib/plugins/custom-pages/index.d.ts +2 -2
  43. package/dist/lib/plugins/custom-pages/index.js.map +1 -1
  44. package/dist/lib/plugins/markdown/index.d.ts +2 -2
  45. package/dist/lib/plugins/markdown/index.js.map +1 -1
  46. package/dist/lib/plugins/openapi/OperationList.js +1 -1
  47. package/dist/lib/plugins/openapi/OperationList.js.map +1 -1
  48. package/dist/lib/plugins/openapi/Sidecar.js +28 -24
  49. package/dist/lib/plugins/openapi/Sidecar.js.map +1 -1
  50. package/dist/lib/plugins/openapi/index.d.ts +2 -2
  51. package/dist/lib/plugins/openapi/index.js.map +1 -1
  52. package/dist/lib/plugins/redirect/index.d.ts +2 -2
  53. package/dist/lib/plugins/redirect/index.js.map +1 -1
  54. package/dist/lib/plugins/search-inkeep/index.d.ts +2 -2
  55. package/dist/lib/plugins/search-inkeep/index.js.map +1 -1
  56. package/dist/lib/ui/ActionButton.d.ts +3 -1
  57. package/dist/lib/util/useOnScreen.d.ts +4 -0
  58. package/dist/lib/util/useOnScreen.js +19 -0
  59. package/dist/lib/util/useOnScreen.js.map +1 -0
  60. package/dist/vite/plugin-mdx.d.ts +0 -6
  61. package/dist/vite/plugin-mdx.js.map +1 -1
  62. package/lib/{AnchorLink-BbB2q-jx.js → AnchorLink-DYbUOP9U.js} +2 -2
  63. package/lib/{AnchorLink-BbB2q-jx.js.map → AnchorLink-DYbUOP9U.js.map} +1 -1
  64. package/lib/{AuthenticationPlugin-C9BHGXlE.js → AuthenticationPlugin-bqGAKfot.js} +3 -3
  65. package/lib/{AuthenticationPlugin-C9BHGXlE.js.map → AuthenticationPlugin-bqGAKfot.js.map} +1 -1
  66. package/lib/{ClientOnly-CVN6leDu.js → ClientOnly-E7hGysn1.js} +4 -4
  67. package/lib/ClientOnly-E7hGysn1.js.map +1 -0
  68. package/lib/{Markdown-BDcCAWwm.js → Markdown-D6UxMbZm.js} +2 -2
  69. package/lib/{Markdown-BDcCAWwm.js.map → Markdown-D6UxMbZm.js.map} +1 -1
  70. package/lib/{MdxPage-DKMH_t0f.js → MdxPage-DRKqyn2b.js} +5 -5
  71. package/lib/{MdxPage-DKMH_t0f.js.map → MdxPage-DRKqyn2b.js.map} +1 -1
  72. package/lib/{OperationList-BjppA5yM.js → OperationList-BHUBGM0c.js} +5 -5
  73. package/lib/{OperationList-BjppA5yM.js.map → OperationList-BHUBGM0c.js.map} +1 -1
  74. package/lib/{Route-D_djzMv3.js → Route-B0XuN1oC.js} +3 -3
  75. package/lib/{Route-D_djzMv3.js.map → Route-B0XuN1oC.js.map} +1 -1
  76. package/lib/{Select-Bagt3Bme.js → Select-DYKDahHt.js} +3 -3
  77. package/lib/{Select-Bagt3Bme.js.map → Select-DYKDahHt.js.map} +1 -1
  78. package/lib/{SlotletProvider-Da7eFgd2.js → SlotletProvider-mhjLPG44.js} +4 -4
  79. package/lib/{SlotletProvider-Da7eFgd2.js.map → SlotletProvider-mhjLPG44.js.map} +1 -1
  80. package/lib/{hook-sn0zMTkE.js → hook-CjQERPa7.js} +3 -3
  81. package/lib/{hook-sn0zMTkE.js.map → hook-CjQERPa7.js.map} +1 -1
  82. package/lib/{index-_gtpPhlu.js → index-BRg5pi5D.js} +1190 -1165
  83. package/lib/index-BRg5pi5D.js.map +1 -0
  84. package/lib/{index-CRo94sKK.js → index-DM9hrcCG.js} +4 -4
  85. package/lib/{index-CRo94sKK.js.map → index-DM9hrcCG.js.map} +1 -1
  86. package/lib/state-BsPrOUAh.js +252 -0
  87. package/lib/state-BsPrOUAh.js.map +1 -0
  88. package/lib/ui/ActionButton.js.map +1 -1
  89. package/lib/{useExposedProps-ChOIUaS4.js → useExposedProps-BxyHjPNN.js} +2 -2
  90. package/lib/{useExposedProps-ChOIUaS4.js.map → useExposedProps-BxyHjPNN.js.map} +1 -1
  91. package/lib/{ZudokuContext-BKXGJTmu.js → utils-DNAltzXc.js} +153 -152
  92. package/lib/utils-DNAltzXc.js.map +1 -0
  93. package/lib/zudoku.auth-auth0.js +1 -1
  94. package/lib/zudoku.auth-clerk.js +2 -2
  95. package/lib/zudoku.auth-openid.js +4 -4
  96. package/lib/zudoku.components.js +672 -650
  97. package/lib/zudoku.components.js.map +1 -1
  98. package/lib/zudoku.plugin-api-keys.js +5 -5
  99. package/lib/zudoku.plugin-api-keys.js.map +1 -1
  100. package/lib/zudoku.plugin-custom-pages.js +2 -2
  101. package/lib/zudoku.plugin-custom-pages.js.map +1 -1
  102. package/lib/zudoku.plugin-markdown.js +1 -1
  103. package/lib/zudoku.plugin-markdown.js.map +1 -1
  104. package/lib/zudoku.plugin-openapi.js +5 -5
  105. package/lib/zudoku.plugin-redirect.js.map +1 -1
  106. package/lib/zudoku.plugin-search-inkeep.js +1 -1
  107. package/lib/zudoku.plugin-search-inkeep.js.map +1 -1
  108. package/package.json +2 -2
  109. package/src/app/main.tsx +4 -4
  110. package/src/lib/authentication/authentication.ts +2 -2
  111. package/src/lib/authentication/state.ts +12 -5
  112. package/{LICENSE.md → src/lib/authentication/use-broadcast/LICENSE.md} +2 -2
  113. package/src/lib/authentication/use-broadcast/shared.ts +372 -0
  114. package/src/lib/authentication/use-broadcast/useBroadcast.ts +146 -0
  115. package/src/lib/components/ClientOnly.tsx +6 -3
  116. package/src/lib/components/Header.tsx +26 -20
  117. package/src/lib/components/Zudoku.tsx +113 -0
  118. package/src/lib/components/context/ZudokuContext.ts +3 -3
  119. package/src/lib/components/context/ZudokuProvider.tsx +2 -2
  120. package/src/lib/components/index.ts +2 -3
  121. package/src/lib/core/{DevPortalContext.ts → ZudokuContext.ts} +5 -5
  122. package/src/lib/core/plugins.ts +12 -16
  123. package/src/lib/plugins/api-keys/index.tsx +9 -9
  124. package/src/lib/plugins/custom-pages/index.tsx +2 -2
  125. package/src/lib/plugins/markdown/index.tsx +2 -2
  126. package/src/lib/plugins/openapi/OperationList.tsx +2 -1
  127. package/src/lib/plugins/openapi/Sidecar.tsx +65 -51
  128. package/src/lib/plugins/openapi/index.tsx +2 -4
  129. package/src/lib/plugins/redirect/index.tsx +2 -2
  130. package/src/lib/plugins/search-inkeep/index.tsx +2 -2
  131. package/src/lib/ui/ActionButton.tsx +1 -1
  132. package/src/lib/util/useOnScreen.ts +32 -0
  133. package/dist/lib/components/DevPortal.js.map +0 -1
  134. package/dist/lib/core/DevPortalContext.js.map +0 -1
  135. package/lib/ClientOnly-CVN6leDu.js.map +0 -1
  136. package/lib/ZudokuContext-BKXGJTmu.js.map +0 -1
  137. package/lib/index-_gtpPhlu.js.map +0 -1
  138. package/lib/state-CsuHT8ZO.js +0 -183
  139. package/lib/state-CsuHT8ZO.js.map +0 -1
  140. package/src/lib/components/DevPortal.tsx +0 -111
@@ -0,0 +1,372 @@
1
+ // https://github.com/Romainlg29/use-broadcast/
2
+
3
+ import { StateCreator, StoreMutatorIdentifier } from "zustand";
4
+
5
+ export type SharedOptions = {
6
+ /**
7
+ * The name of the broadcast channel
8
+ * It must be unique
9
+ */
10
+ name?: string;
11
+
12
+ /**
13
+ * Main timeout
14
+ * If the main tab / window doesn't respond in this time, this tab / window will become the main
15
+ * @default 100 ms
16
+ */
17
+ mainTimeout?: number;
18
+
19
+ /**
20
+ * If true, the store will only synchronize once with the main tab. After that, the store will be unsynchronized.
21
+ * @default false
22
+ */
23
+ unsync?: boolean;
24
+
25
+ /**
26
+ * Callback when this tab / window becomes the main tab / window
27
+ * Triggered only in the main tab / window
28
+ */
29
+ onBecomeMain?: (id: number) => void;
30
+
31
+ /**
32
+ * Callback when a new tab is opened / closed
33
+ * Triggered only in the main tab / window
34
+ */
35
+ onTabsChange?: (ids: number[]) => void;
36
+ };
37
+
38
+ /**
39
+ * The Shared type
40
+ */
41
+ export type Shared = <
42
+ T,
43
+ Mps extends [StoreMutatorIdentifier, unknown][] = [],
44
+ Mcs extends [StoreMutatorIdentifier, unknown][] = [],
45
+ >(
46
+ f: StateCreator<T, Mps, Mcs>,
47
+ options?: SharedOptions,
48
+ ) => StateCreator<T, Mps, Mcs>;
49
+
50
+ /**
51
+ * Type implementation of the Shared function
52
+ */
53
+ type SharedImpl = <T>(
54
+ f: StateCreator<T, [], []>,
55
+ options?: SharedOptions,
56
+ ) => StateCreator<T, [], []>;
57
+
58
+ /**
59
+ * Shared implementation
60
+ * @param f Zustand state creator
61
+ * @param options The options
62
+ */
63
+ const sharedImpl: SharedImpl = (f, options) => (set, get, store) => {
64
+ /**
65
+ * The broadcast channel is not supported in SSR
66
+ */
67
+ if (
68
+ typeof window === "undefined" &&
69
+ !(
70
+ typeof WorkerGlobalScope !== "undefined" &&
71
+ self instanceof WorkerGlobalScope
72
+ )
73
+ ) {
74
+ // eslint-disable-next-line no-console
75
+ console.warn(
76
+ "BroadcastChannel is not supported in this environment. The store will not be shared.",
77
+ );
78
+ return f(set, get, store);
79
+ }
80
+
81
+ /**
82
+ * If BroadcastChannel is not supported, return the basic store
83
+ */
84
+ if (typeof BroadcastChannel === "undefined") {
85
+ // eslint-disable-next-line no-console
86
+ console.warn(
87
+ "BroadcastChannel is not supported in this browser. The store will not be shared.",
88
+ );
89
+ return f(set, get, store);
90
+ }
91
+
92
+ /**
93
+ * Types
94
+ */
95
+ type Item = { [key: string]: unknown };
96
+ type Message =
97
+ | {
98
+ action: "sync";
99
+ }
100
+ | {
101
+ action: "change";
102
+ state: Item;
103
+ }
104
+ | {
105
+ action: "add_new_tab";
106
+ id: number;
107
+ }
108
+ | {
109
+ action: "close";
110
+ id: number;
111
+ }
112
+ | {
113
+ action: "change_main";
114
+ id: number;
115
+ tabs: number[];
116
+ };
117
+
118
+ /**
119
+ * Is the store synced with the other tabs
120
+ */
121
+ let isSynced = get() !== undefined;
122
+
123
+ /**
124
+ * Is this tab / window the main tab / window
125
+ * When a new tab / window is opened, it will be synced with the main
126
+ */
127
+ let isMain = false;
128
+
129
+ /**
130
+ * The broadcast channel name
131
+ */
132
+ const name = options?.name ?? f.toString();
133
+
134
+ /**
135
+ * The id of the tab / window
136
+ */
137
+ let id = 0;
138
+
139
+ /**
140
+ * Store a list of all the tabs / windows
141
+ * Only for the main tab / window
142
+ */
143
+ const tabs: number[] = [0];
144
+
145
+ /**
146
+ * Create the broadcast channel
147
+ */
148
+ const channel = new BroadcastChannel(name);
149
+
150
+ /**
151
+ * Handle the Zustand set function
152
+ * Trigger a postMessage to all the other tabs
153
+ */
154
+ const onSet: typeof set = (...args) => {
155
+ /**
156
+ * Get the previous states
157
+ */
158
+ const previous = get() as Item;
159
+
160
+ /**
161
+ * Update the states
162
+ */
163
+ set(...(args as Parameters<typeof set>));
164
+
165
+ /**
166
+ * If the stores should not be synced, return.
167
+ */
168
+ if (options?.unsync) {
169
+ return;
170
+ }
171
+
172
+ /**
173
+ * Get the fresh states
174
+ */
175
+ const updated = get() as Item;
176
+
177
+ /**
178
+ * Get the states that changed
179
+ */
180
+ const state = Object.entries(updated).reduce((obj, [key, val]) => {
181
+ if (previous[key] !== val) {
182
+ obj = { ...obj, [key]: val };
183
+ }
184
+ return obj;
185
+ }, {} as Item);
186
+
187
+ /**
188
+ * Send the states to all the other tabs
189
+ */
190
+ channel.postMessage({ action: "change", state } as Message);
191
+ };
192
+
193
+ /**
194
+ * Subscribe to the broadcast channel
195
+ */
196
+ channel.onmessage = (e) => {
197
+ if ((e.data as Message).action === "sync") {
198
+ /**
199
+ * If this tab / window is not the main, return
200
+ */
201
+ if (!isMain) {
202
+ return;
203
+ }
204
+
205
+ /**
206
+ * Remove all the functions and symbols from the store
207
+ */
208
+ const state = Object.entries(get() as Item).reduce((obj, [key, val]) => {
209
+ if (typeof val !== "function" && typeof val !== "symbol") {
210
+ obj = { ...obj, [key]: val };
211
+ }
212
+ return obj;
213
+ }, {});
214
+
215
+ /**
216
+ * Send the state to the other tabs
217
+ */
218
+ channel.postMessage({ action: "change", state } as Message);
219
+
220
+ /**
221
+ * Set the new tab / window id
222
+ */
223
+ const new_id = tabs[tabs.length - 1]! + 1;
224
+ tabs.push(new_id);
225
+
226
+ options?.onTabsChange?.(tabs);
227
+
228
+ channel.postMessage({ action: "add_new_tab", id: new_id } as Message);
229
+
230
+ return;
231
+ }
232
+
233
+ /**
234
+ * Set an id for the tab / window if it doesn't have one
235
+ */
236
+ if ((e.data as Message).action === "add_new_tab" && !isMain && id === 0) {
237
+ id = e.data.id;
238
+ return;
239
+ }
240
+
241
+ /**
242
+ * On receiving a new state, update the state
243
+ */
244
+ if ((e.data as Message).action === "change") {
245
+ /**
246
+ * Update the state
247
+ */
248
+ set(e.data.state);
249
+
250
+ /**
251
+ * Set the synced attribute
252
+ */
253
+ isSynced = true;
254
+ }
255
+
256
+ /**
257
+ * On receiving a close message, remove the tab / window id from the list
258
+ */
259
+ if ((e.data as Message).action === "close") {
260
+ if (!isMain) {
261
+ return;
262
+ }
263
+
264
+ const index = tabs.indexOf(e.data.id);
265
+ if (index !== -1) {
266
+ tabs.splice(index, 1);
267
+
268
+ options?.onTabsChange?.(tabs);
269
+ }
270
+ }
271
+
272
+ /**
273
+ * On receiving a change_main message, change the main tab / window
274
+ */
275
+ if ((e.data as Message).action === "change_main") {
276
+ if (e.data.id === id) {
277
+ isMain = true;
278
+ tabs.splice(0, tabs.length, ...e.data.tabs);
279
+
280
+ options?.onBecomeMain?.(id);
281
+ }
282
+ }
283
+ };
284
+
285
+ /**
286
+ * Synchronize with the main tab
287
+ */
288
+ const synchronize = (): void => {
289
+ channel.postMessage({ action: "sync" } as Message);
290
+
291
+ /**
292
+ * If isSynced is false after 100ms, this tab is the main tab
293
+ */
294
+ setTimeout(() => {
295
+ if (!isSynced) {
296
+ isMain = true;
297
+ isSynced = true;
298
+
299
+ options?.onBecomeMain?.(id);
300
+ }
301
+ }, options?.mainTimeout ?? 100);
302
+ };
303
+
304
+ /**
305
+ * Handle case when the tab / window is closed
306
+ */
307
+ const onClose = (): void => {
308
+ channel.postMessage({ action: "close", id } as Message);
309
+
310
+ /**
311
+ * If we're closing the main, make the second the new main
312
+ */
313
+ if (isMain) {
314
+ /**
315
+ * If there is only one tab left, close the channel and return
316
+ */
317
+ if (tabs.length === 1) {
318
+ /**
319
+ * Clean up
320
+ */
321
+ channel.close();
322
+ return;
323
+ }
324
+
325
+ const remaining_tabs = tabs.filter((tab) => tab !== id);
326
+ channel.postMessage({
327
+ action: "change_main",
328
+ id: remaining_tabs[0],
329
+ tabs: remaining_tabs,
330
+ } as Message);
331
+
332
+ return;
333
+ }
334
+ };
335
+
336
+ /**
337
+ * Add close event listener
338
+ */
339
+ if (typeof window !== "undefined") {
340
+ window.addEventListener("beforeunload", onClose);
341
+ }
342
+
343
+ /**
344
+ * Synchronize with the main tab
345
+ */
346
+ if (!isSynced) {
347
+ synchronize();
348
+ }
349
+
350
+ /**
351
+ * Modify and return the Zustand store
352
+ */
353
+ store.setState = onSet;
354
+
355
+ return f(onSet, get, store);
356
+ };
357
+
358
+ /**
359
+ * Shared middleware
360
+ *
361
+ * @example
362
+ * import { create } from 'zustand';
363
+ * import { shared } from 'use-broadcast-ts';
364
+ *
365
+ * const useStore = create(
366
+ * shared(
367
+ * (set) => ({ count: 0 }),
368
+ * { name: 'my-store' }
369
+ * )
370
+ * );
371
+ */
372
+ export const shared = sharedImpl as Shared;
@@ -0,0 +1,146 @@
1
+ // https://github.com/Romainlg29/use-broadcast/
2
+ import { useEffect, useRef, useState } from "react";
3
+
4
+ /**
5
+ * Our hook will return an object with three properties:
6
+ * - send: a function that will send a message to all other tabs
7
+ * - state: the current state of the broadcast
8
+ * - subscribe: a function that will subscribe to the broadcast (Only if options.subscribe is true)
9
+ */
10
+ export type UseBroadcastReturn<T> = {
11
+ send: (val: T) => void;
12
+ state: T | undefined;
13
+ subscribe: (callback: (e: T) => void) => () => void;
14
+ };
15
+
16
+ /**
17
+ * The options for the useBroadcast hook
18
+ */
19
+ export type UseBroadcastOptions = {
20
+ subscribe?: boolean;
21
+ };
22
+
23
+ /**
24
+ *
25
+ * @param name The name of the broadcast channel
26
+ * @param val The initial value of the broadcast
27
+ * @returns Returns an object with three properties: send, state and subscribe
28
+ */
29
+ export const useBroadcast = <T>(
30
+ name: string,
31
+ val?: T,
32
+ options?: UseBroadcastOptions,
33
+ ): UseBroadcastReturn<T> => {
34
+ /**
35
+ * Store the state of the broadcast
36
+ */
37
+ const [state, setState] = useState<T | undefined>(val);
38
+
39
+ /**
40
+ * Store the BroadcastChannel instance
41
+ */
42
+ const channel = useRef<BroadcastChannel | null>(null);
43
+
44
+ /**
45
+ * Store the listeners
46
+ */
47
+ const listeners = useRef<((e: T) => void)[]>([]);
48
+
49
+ /**
50
+ * This function send the value to all the other tabs
51
+ * @param val The value to send
52
+ */
53
+ const send = (val: T) => {
54
+ if (!channel.current) {
55
+ return;
56
+ }
57
+
58
+ /**
59
+ * Send the value to all the other tabs
60
+ */
61
+ channel.current.postMessage(val);
62
+
63
+ if (!options?.subscribe) {
64
+ setState(val);
65
+ }
66
+
67
+ /**
68
+ * Dispatch the event to the listeners
69
+ */
70
+ listeners.current.forEach((listener) => listener(val));
71
+ };
72
+
73
+ /**
74
+ * This function subscribe to the broadcast
75
+ * @param callback The callback function
76
+ * @returns Returns a function that unsubscribe the callback
77
+ */
78
+ const subscribe = (callback: (e: T) => void) => {
79
+ /**
80
+ * Add the callback to the listeners
81
+ */
82
+ listeners.current.push(callback);
83
+
84
+ /**
85
+ * Return a function that unsubscribe the callback
86
+ */
87
+ return () =>
88
+ listeners.current.splice(listeners.current.indexOf(callback), 1);
89
+ };
90
+
91
+ useEffect(() => {
92
+ /**
93
+ * If BroadcastChannel is not supported, we log an error and return
94
+ */
95
+ if (typeof window === "undefined") {
96
+ // eslint-disable-next-line no-console
97
+ console.error("Window is undefined!");
98
+ return;
99
+ }
100
+
101
+ if (!window.BroadcastChannel) {
102
+ // eslint-disable-next-line no-console
103
+ console.error("BroadcastChannel is not supported!");
104
+ return;
105
+ }
106
+
107
+ /**
108
+ * If the channel is null, we create a new one
109
+ */
110
+ if (!channel.current) {
111
+ channel.current = new BroadcastChannel(name);
112
+ }
113
+
114
+ /**
115
+ * Subscribe to the message event
116
+ * @param e The message event
117
+ */
118
+ channel.current.onmessage = (e) => {
119
+ /**
120
+ * Update the state
121
+ */
122
+ if (!options?.subscribe) {
123
+ setState(e.data);
124
+ }
125
+
126
+ /**
127
+ * Dispatch an event to the listeners
128
+ */
129
+ listeners.current.forEach((listener) => listener(e.data));
130
+ };
131
+
132
+ /**
133
+ * Cleanup
134
+ */
135
+ return () => {
136
+ if (!channel.current) {
137
+ return;
138
+ }
139
+
140
+ channel.current.close();
141
+ channel.current = null;
142
+ };
143
+ }, [name, options]);
144
+
145
+ return { send, state, subscribe };
146
+ };
@@ -1,13 +1,16 @@
1
- import { useSyncExternalStore } from "react";
1
+ import { type ReactNode, useSyncExternalStore } from "react";
2
2
 
3
3
  const noop = () => () => {};
4
4
 
5
- export const ClientOnly = (props: { children: React.ReactNode }) => {
5
+ export const ClientOnly = (props: {
6
+ children: ReactNode;
7
+ fallback?: ReactNode;
8
+ }) => {
6
9
  const value = useSyncExternalStore(
7
10
  noop,
8
11
  () => "client",
9
12
  () => "server",
10
13
  );
11
14
 
12
- return value === "client" ? props.children : null;
15
+ return value === "client" ? props.children : props.fallback;
13
16
  };
@@ -2,6 +2,7 @@ import { MoonStarIcon, SunIcon } from "lucide-react";
2
2
  import { memo } from "react";
3
3
  import { Link } from "react-router-dom";
4
4
  import { Button } from "zudoku/ui/Button.js";
5
+ import { Skeleton } from "zudoku/ui/Skeleton.js";
5
6
  import { useAuth } from "../authentication/hook.js";
6
7
  import { isProfileMenuPlugin, ProfileNavigationItem } from "../core/plugins.js";
7
8
  import {
@@ -19,6 +20,7 @@ import {
19
20
  import { cn } from "../util/cn.js";
20
21
  import { joinPath } from "../util/joinPath.js";
21
22
  import { Banner } from "./Banner.js";
23
+ import { ClientOnly } from "./ClientOnly.js";
22
24
  import { useTheme } from "./context/ThemeContext.js";
23
25
  import { useZudoku } from "./context/ZudokuContext.js";
24
26
  import { MobileTopNavigation } from "./MobileTopNavigation.js";
@@ -114,26 +116,30 @@ export const Header = memo(function HeaderInner() {
114
116
  <MobileTopNavigation />
115
117
  <div className="hidden lg:flex items-center justify-self-end text-sm gap-2">
116
118
  <Slotlet name="head-navigation-start" />
117
- {isAuthEnabled && !isAuthenticated ? (
118
- <Button variant="ghost" onClick={() => auth.login()}>
119
- Login
120
- </Button>
121
- ) : (
122
- accountItems.length > 0 && (
123
- <DropdownMenu modal={false}>
124
- <DropdownMenuTrigger asChild>
125
- <Button variant="ghost">
126
- {profile?.email ? `${profile.email}` : "My Account"}
127
- </Button>
128
- </DropdownMenuTrigger>
129
- <DropdownMenuContent className="w-56">
130
- <DropdownMenuLabel>My Account</DropdownMenuLabel>
131
- <DropdownMenuSeparator />
132
- {accountItems}
133
- </DropdownMenuContent>
134
- </DropdownMenu>
135
- )
136
- )}
119
+ <ClientOnly
120
+ fallback={<Skeleton className="rounded h-5 w-24 mr-4" />}
121
+ >
122
+ {isAuthEnabled && !isAuthenticated ? (
123
+ <Button variant="ghost" onClick={() => auth.login()}>
124
+ Login
125
+ </Button>
126
+ ) : (
127
+ accountItems.length > 0 && (
128
+ <DropdownMenu modal={false}>
129
+ <DropdownMenuTrigger asChild>
130
+ <Button variant="ghost">
131
+ {profile?.email ? `${profile.email}` : "My Account"}
132
+ </Button>
133
+ </DropdownMenuTrigger>
134
+ <DropdownMenuContent className="w-56">
135
+ <DropdownMenuLabel>My Account</DropdownMenuLabel>
136
+ <DropdownMenuSeparator />
137
+ {accountItems}
138
+ </DropdownMenuContent>
139
+ </DropdownMenu>
140
+ )
141
+ )}
142
+ </ClientOnly>
137
143
  <Button
138
144
  variant="ghost"
139
145
  aria-label={