@myelophone/nuxt 0.18.2

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 (259) hide show
  1. package/.devcontainer/Dockerfile +7 -0
  2. package/.devcontainer/devcontainer.json +21 -0
  3. package/.editorconfig +43 -0
  4. package/.github/workflows/package.yml +73 -0
  5. package/.nuxtignore +6 -0
  6. package/.releaserc.js +132 -0
  7. package/.yarn/releases/yarn-4.18.0.cjs +1000 -0
  8. package/.yarnrc.yml +8 -0
  9. package/CHANGELOG.md +141 -0
  10. package/LICENSE +77 -0
  11. package/README.md +3004 -0
  12. package/app/app.vue +455 -0
  13. package/app/assets/css/animation.css +81 -0
  14. package/app/assets/css/backgrounds.css +57 -0
  15. package/app/assets/css/base.css +182 -0
  16. package/app/assets/css/index.css +9 -0
  17. package/app/assets/css/links.css +12 -0
  18. package/app/assets/css/multimedia.css +102 -0
  19. package/app/assets/css/preloader.css +68 -0
  20. package/app/assets/css/print.css +40 -0
  21. package/app/assets/css/tailwind.css +20 -0
  22. package/app/assets/css/theme.css +29 -0
  23. package/app/components/consent/GoogleMap.vue +59 -0
  24. package/app/components/consent/Youtube.vue +27 -0
  25. package/app/components/cookie/Banner.vue +70 -0
  26. package/app/components/cookie/ConsentWrapper.vue +71 -0
  27. package/app/components/cookie/PrivacyPolicy.vue +179 -0
  28. package/app/components/cookie/SettingsModal.vue +387 -0
  29. package/app/components/grid/Col.vue +262 -0
  30. package/app/components/grid/Container.vue +236 -0
  31. package/app/components/grid/Row.vue +248 -0
  32. package/app/components/grid/Stack.vue +233 -0
  33. package/app/components/myelophone/Copyright.vue +5 -0
  34. package/app/components/myelophone/Welcome.vue +107 -0
  35. package/app/components/page/FullscreenPreloader.vue +177 -0
  36. package/app/components/page/Preloader.vue +18 -0
  37. package/app/components/page/ReadyLocalized.vue +19 -0
  38. package/app/components/ready/.gitkeep +0 -0
  39. package/app/components/safe/Email.vue +52 -0
  40. package/app/components/safe/ImgWithLoader.vue +180 -0
  41. package/app/components/safe/NuxtImg.vue +25 -0
  42. package/app/components/safe/NuxtPicture.vue +29 -0
  43. package/app/components/section/.gitkeep +0 -0
  44. package/app/components/seo/BrandInfo.vue +76 -0
  45. package/app/components/seo/ContentNoIndex.vue +35 -0
  46. package/app/components/seo/NoIndex.vue +22 -0
  47. package/app/components/seo/OrgInfo.vue +130 -0
  48. package/app/components/seo/ProfileInfo.vue +137 -0
  49. package/app/components/ui/Accordion.vue +163 -0
  50. package/app/components/ui/Alert.vue +179 -0
  51. package/app/components/ui/Avatar.vue +88 -0
  52. package/app/components/ui/Badge.vue +150 -0
  53. package/app/components/ui/Banner.vue +133 -0
  54. package/app/components/ui/Button.vue +175 -0
  55. package/app/components/ui/Card.vue +66 -0
  56. package/app/components/ui/Checkbox.vue +134 -0
  57. package/app/components/ui/Chip.vue +136 -0
  58. package/app/components/ui/CommandPalette.vue +434 -0
  59. package/app/components/ui/Console.vue +583 -0
  60. package/app/components/ui/CursorCreative.vue +148 -0
  61. package/app/components/ui/FeatureAccordion.vue +165 -0
  62. package/app/components/ui/FileUpload.vue +705 -0
  63. package/app/components/ui/GeoDependent.vue +72 -0
  64. package/app/components/ui/Heading.vue +51 -0
  65. package/app/components/ui/Icon.vue +42 -0
  66. package/app/components/ui/Icon8.vue +39 -0
  67. package/app/components/ui/Input.vue +88 -0
  68. package/app/components/ui/LangLink.vue +34 -0
  69. package/app/components/ui/LangVisible.vue +33 -0
  70. package/app/components/ui/LanguageSelect.vue +202 -0
  71. package/app/components/ui/LightBox.vue +155 -0
  72. package/app/components/ui/LocalTime.vue +170 -0
  73. package/app/components/ui/Modal.vue +161 -0
  74. package/app/components/ui/Popup.vue +200 -0
  75. package/app/components/ui/PostContent.server.vue +23 -0
  76. package/app/components/ui/ProtectedEmail.vue +52 -0
  77. package/app/components/ui/RelativeTime.vue +141 -0
  78. package/app/components/ui/ScheduledContent.vue +150 -0
  79. package/app/components/ui/ScrollToTop.client.vue +48 -0
  80. package/app/components/ui/SegmentedControl.vue +148 -0
  81. package/app/components/ui/SiteSearch.vue +1191 -0
  82. package/app/components/ui/Skeleton.vue +91 -0
  83. package/app/components/ui/SmartContrast.vue +74 -0
  84. package/app/components/ui/SnapContainer.vue +222 -0
  85. package/app/components/ui/SnapSection.vue +294 -0
  86. package/app/components/ui/SonnerStyle.vue +7 -0
  87. package/app/components/ui/SplitSection.vue +99 -0
  88. package/app/components/ui/Steps.vue +858 -0
  89. package/app/components/ui/StickyWrapper.vue +46 -0
  90. package/app/components/ui/Table.vue +251 -0
  91. package/app/components/ui/Tabs.vue +451 -0
  92. package/app/components/ui/TextColumn.vue +36 -0
  93. package/app/components/ui/Textarea.vue +251 -0
  94. package/app/components/ui/Toggler.vue +40 -0
  95. package/app/components/ui/Tooltip.vue +524 -0
  96. package/app/components/ui/TruncateText.vue +105 -0
  97. package/app/components/ui/ViewportSpacer.vue +72 -0
  98. package/app/components/view/AnimatedCounter.vue +121 -0
  99. package/app/components/view/AnnouncementBar.vue +104 -0
  100. package/app/components/view/AvatarGroup.vue +104 -0
  101. package/app/components/view/Breadcrumbs.vue +97 -0
  102. package/app/components/view/ContentWithToc.vue +152 -0
  103. package/app/components/view/Copyright.vue +68 -0
  104. package/app/components/view/CountdownTimer.vue +184 -0
  105. package/app/components/view/CreativeCTA.vue +143 -0
  106. package/app/components/view/CurrencySelect.vue +254 -0
  107. package/app/components/view/FeatureBoxGrid.vue +142 -0
  108. package/app/components/view/HorizontalMenu.vue +97 -0
  109. package/app/components/view/ImageCompare.vue +141 -0
  110. package/app/components/view/ImgSlider.vue +333 -0
  111. package/app/components/view/InfiniteMarquee.vue +147 -0
  112. package/app/components/view/InfoBar.vue +86 -0
  113. package/app/components/view/LogoGrid.vue +106 -0
  114. package/app/components/view/LogoSlider.vue +83 -0
  115. package/app/components/view/NotFound.vue +81 -0
  116. package/app/components/view/PartialBackground.vue +313 -0
  117. package/app/components/view/QuoteBig.vue +111 -0
  118. package/app/components/view/ResponsiveMenu.vue +219 -0
  119. package/app/components/view/ReviewsSlider.vue +230 -0
  120. package/app/components/view/SocialBar.vue +78 -0
  121. package/app/components/view/StickyHeadingContent.vue +158 -0
  122. package/app/components/view/TallyForm.vue +295 -0
  123. package/app/components/view/WarningOutdated.vue +113 -0
  124. package/app/composables/useApi.ts +88 -0
  125. package/app/composables/useAppSeo.ts +78 -0
  126. package/app/composables/useBreadcrumbs.ts +106 -0
  127. package/app/composables/useChangeUrl.ts +73 -0
  128. package/app/composables/useCommand.ts +28 -0
  129. package/app/composables/useCookieControl.ts +111 -0
  130. package/app/composables/useCookieModal.ts +9 -0
  131. package/app/composables/useDevice.ts +109 -0
  132. package/app/composables/useGeoFetch.ts +139 -0
  133. package/app/composables/useLoadScript.ts +36 -0
  134. package/app/composables/useModalState.ts +37 -0
  135. package/app/composables/useMultiLang.ts +278 -0
  136. package/app/composables/useNetwork.ts +49 -0
  137. package/app/composables/useNuxtVersion.ts +5 -0
  138. package/app/composables/usePiniaStore.ts +5 -0
  139. package/app/composables/usePopupControl.ts +57 -0
  140. package/app/composables/useQuery.ts +307 -0
  141. package/app/composables/useReadyPage.ts +182 -0
  142. package/app/composables/useSafeList.ts +90 -0
  143. package/app/composables/useScriptLoader.ts +221 -0
  144. package/app/composables/useSiteSearch.ts +32 -0
  145. package/app/composables/useSnapScroll.ts +24 -0
  146. package/app/composables/useStatic.ts +5 -0
  147. package/app/composables/useStorage.ts +252 -0
  148. package/app/composables/useStoreBroadcast.ts +146 -0
  149. package/app/composables/useThemeSync.ts +81 -0
  150. package/app/constants/layout.ts +109 -0
  151. package/app/constants/predefinedCookieScripts.ts +1222 -0
  152. package/app/constants/ui-events.ts +1 -0
  153. package/app/error.vue +169 -0
  154. package/app/layouts/default-blog.vue +5 -0
  155. package/app/layouts/default.vue +3 -0
  156. package/app/locales/en/commands.json +5 -0
  157. package/app/locales/en/cookies.json +182 -0
  158. package/app/locales/en/error.json +4 -0
  159. package/app/locales/en/interface.json +33 -0
  160. package/app/locales/en/myelophone.json +8 -0
  161. package/app/locales/en/outdated.json +5 -0
  162. package/app/locales/en/times.json +18 -0
  163. package/app/locales/pl/commands.json +5 -0
  164. package/app/locales/pl/cookies.json +182 -0
  165. package/app/locales/pl/error.json +4 -0
  166. package/app/locales/pl/interface.json +33 -0
  167. package/app/locales/pl/myelophone.json +8 -0
  168. package/app/locales/pl/outdated.json +5 -0
  169. package/app/locales/pl/times.json +18 -0
  170. package/app/locales/ru/commands.json +5 -0
  171. package/app/locales/ru/cookies.json +182 -0
  172. package/app/locales/ru/error.json +4 -0
  173. package/app/locales/ru/interface.json +33 -0
  174. package/app/locales/ru/myelophone.json +8 -0
  175. package/app/locales/ru/outdated.json +5 -0
  176. package/app/locales/ru/times.json +18 -0
  177. package/app/middleware/blog-layout.ts +28 -0
  178. package/app/middleware/i18n.global.ts +17 -0
  179. package/app/middleware/not-found-status.ts +11 -0
  180. package/app/modules/default-blog/index.ts +39 -0
  181. package/app/modules/default-blog/posts.ts +80 -0
  182. package/app/modules/default-sitemap/index.ts +116 -0
  183. package/app/modules/default-sitemap/runtime/sitemap.get.ts +42 -0
  184. package/app/modules/fullscreen-preloader/module.ts +43 -0
  185. package/app/modules/htaccess/.htaccess +38 -0
  186. package/app/modules/htaccess/index.ts +23 -0
  187. package/app/modules/i18n-shaker.mjs +221 -0
  188. package/app/modules/multi18n/module.ts +71 -0
  189. package/app/modules/myelophone-config/module.ts +241 -0
  190. package/app/pages/[...all].vue +9 -0
  191. package/app/pages/index.vue +3 -0
  192. package/app/pages/post/[...slug].vue +30 -0
  193. package/app/plugins/bfcache-refresh.client.ts +9 -0
  194. package/app/plugins/canonical.ts +30 -0
  195. package/app/plugins/csp-nonce.server.ts +41 -0
  196. package/app/plugins/multi18n.client.ts +21 -0
  197. package/app/plugins/multi18n.server.ts +15 -0
  198. package/app/plugins/nuxt-version.client.ts +4 -0
  199. package/app/plugins/restore.client.ts +21 -0
  200. package/app/plugins/reveal.ts +89 -0
  201. package/app/plugins/url-helper.client.ts +66 -0
  202. package/app/server/api/exchange-rates.get.ts +216 -0
  203. package/app/server/api/healthz.ts +1 -0
  204. package/app/server/api/logout.post.ts +6 -0
  205. package/app/server/middleware/normalize-url.ts +14 -0
  206. package/app/server/middleware/security-headers.ts +147 -0
  207. package/app/server/middleware/url-helper.ts +43 -0
  208. package/app/server/plugins/clean-html-spaces.ts +15 -0
  209. package/app/server/plugins/clean-styles.ts +94 -0
  210. package/app/server/plugins/compression.ts +122 -0
  211. package/app/server/plugins/css-preload.ts +26 -0
  212. package/app/server/plugins/early-hints.ts +31 -0
  213. package/app/server/plugins/force-noindex.ts +43 -0
  214. package/app/server/plugins/html-one-line.ts +17 -0
  215. package/app/server/plugins/inline-style.ts +7 -0
  216. package/app/server/routes/healthz.get.ts +1 -0
  217. package/app/server/tsconfig.json +3 -0
  218. package/app/stores/cart.ts +945 -0
  219. package/app/stores/commands.ts +43 -0
  220. package/app/stores/device.ts +156 -0
  221. package/app/stores/querySync.ts +5 -0
  222. package/app/stores/settings.ts +145 -0
  223. package/app/stores/user.ts +607 -0
  224. package/app/types/cookie.ts +89 -0
  225. package/app/types/myelophone.d.ts +87 -0
  226. package/app/types/site-search.ts +110 -0
  227. package/app/utils/asyncRefresh.ts +4 -0
  228. package/app/utils/blog.server.ts +142 -0
  229. package/app/utils/jsonLd.ts +56 -0
  230. package/app/utils/myelophoneConfig.ts +4 -0
  231. package/app/utils/sortBy.ts +18 -0
  232. package/app/utils/transliterate.ts +75 -0
  233. package/app/utils/ytVideo.ts +37 -0
  234. package/app/workers/site-search.worker.ts +661 -0
  235. package/biome.json +94 -0
  236. package/commitlint.config.js +3 -0
  237. package/nuxt.config.ts +539 -0
  238. package/package.json +101 -0
  239. package/playground/content/.gitkeep +0 -0
  240. package/playground/locales/.gitkeep +0 -0
  241. package/playground/myelophone.dev.ts +45 -0
  242. package/playground/nuxt.config.ts +105 -0
  243. package/playground/package.json +5 -0
  244. package/playground/pages/.gitkeep +0 -0
  245. package/playground/tsconfig.json +3 -0
  246. package/playwright.config.ts +23 -0
  247. package/public/browsers/chrome.svg +77 -0
  248. package/public/browsers/edge.svg +98 -0
  249. package/public/browsers/firefox.svg +300 -0
  250. package/public/favicon.ico +0 -0
  251. package/public/myelophone_eng.png +0 -0
  252. package/public/myelophone_eng_white.png +0 -0
  253. package/spa-loading-template.html +72 -0
  254. package/tests/home.spec.ts +9 -0
  255. package/tests/utils.ts +23 -0
  256. package/tsconfig.json +16 -0
  257. package/vite/plugins/clean-css.ts +190 -0
  258. package/vite/plugins/postcss-add-viewportunits.ts +33 -0
  259. package/vite/plugins/postcss-viewport-fallback.ts +49 -0
@@ -0,0 +1,221 @@
1
+ import type {
2
+ CookieScriptsConfig,
3
+ CookieScriptDef,
4
+ CookieCategory,
5
+ CookiePreferences,
6
+ } from '~/types/cookie';
7
+ import { useMyelophoneConfig } from '~/utils/myelophoneConfig';
8
+
9
+ type CookieScriptEntry = {
10
+ category: CookieCategory;
11
+ script: CookieScriptDef;
12
+ };
13
+
14
+ export const useScriptLoader = () => {
15
+ const config = useMyelophoneConfig();
16
+ const scriptsConfig = config.cookieScripts as CookieScriptsConfig;
17
+ const loadedScripts = reactive(new Set<string>());
18
+ const initializedScripts = reactive(new Set<string>());
19
+ const executedInitializers = reactive(new Set<string>());
20
+ const appliedCategorySettings = reactive(new Set<string>());
21
+
22
+ const getScriptLoadKey = (script: CookieScriptDef) =>
23
+ script.loadKey || script.src || `inline:${script.id}`;
24
+
25
+ const getScriptSettingsKey = (
26
+ script: CookieScriptDef,
27
+ category: CookieCategory,
28
+ ) =>
29
+ `${script.id}:${category}:${script.src || script.code || script.loadKey || ''}`;
30
+
31
+ const getAllScriptEntries = (): CookieScriptEntry[] =>
32
+ Object.entries(scriptsConfig || {}).flatMap(([category, scripts]) =>
33
+ (scripts || []).map((script) => ({
34
+ category: category as CookieCategory,
35
+ script,
36
+ })),
37
+ );
38
+
39
+ const getAllowedScriptEntries = (
40
+ allowedCategories: Set<CookieCategory>,
41
+ ) => {
42
+ const seen = new Set<string>();
43
+
44
+ return getAllScriptEntries().filter(({ category, script }) => {
45
+ const settingsKey = getScriptSettingsKey(script, category);
46
+ if (!allowedCategories.has(category) || seen.has(settingsKey)) {
47
+ return false;
48
+ }
49
+
50
+ seen.add(settingsKey);
51
+ return true;
52
+ });
53
+ };
54
+
55
+ const getAllowedCategories = (prefs?: CookiePreferences) => {
56
+ const allowed = new Set<CookieCategory>(['necessary']);
57
+
58
+ if (prefs?.status === 'accepted' || prefs?.status === 'declined') {
59
+ if (prefs.analytics) allowed.add('analytics');
60
+ if (prefs.marketing) allowed.add('marketing');
61
+ if (prefs.functional) allowed.add('functional');
62
+ }
63
+
64
+ return allowed;
65
+ };
66
+
67
+ const createExecutionContext = (
68
+ script: CookieScriptDef,
69
+ allowedCategories: Set<CookieCategory>,
70
+ ) => ({
71
+ script,
72
+ allowedCategories: Array.from(allowedCategories),
73
+ categories: {
74
+ necessary: allowedCategories.has('necessary'),
75
+ analytics: allowedCategories.has('analytics'),
76
+ marketing: allowedCategories.has('marketing'),
77
+ functional: allowedCategories.has('functional'),
78
+ },
79
+ isAllowed: (category: CookieCategory) =>
80
+ allowedCategories.has(category),
81
+ });
82
+
83
+ const runClientCode = (
84
+ code: string | undefined,
85
+ script: CookieScriptDef,
86
+ allowedCategories: Set<CookieCategory>,
87
+ ) => {
88
+ if (!code || !import.meta.client) return;
89
+
90
+ try {
91
+ Function(
92
+ 'context',
93
+ code,
94
+ )(createExecutionContext(script, allowedCategories));
95
+ } catch (e) {
96
+ console.error(`Error running cookie script hook ${script.id}:`, e);
97
+ }
98
+ };
99
+
100
+ const runInitializers = (
101
+ def: CookieScriptDef,
102
+ allowedCategories: Set<CookieCategory>,
103
+ initializers = def.initializers || [],
104
+ ) => {
105
+ initializers.forEach((initializer) => {
106
+ if (executedInitializers.has(initializer.key)) return;
107
+
108
+ runClientCode(initializer.code, def, allowedCategories);
109
+ executedInitializers.add(initializer.key);
110
+ });
111
+ };
112
+
113
+ const applyScriptSettings = (
114
+ def: CookieScriptDef,
115
+ category: CookieCategory,
116
+ allowedCategories: Set<CookieCategory>,
117
+ ) => {
118
+ runInitializers(def, allowedCategories);
119
+
120
+ if (!initializedScripts.has(def.id)) {
121
+ runClientCode(def.beforeLoad, def, allowedCategories);
122
+ initializedScripts.add(def.id);
123
+ }
124
+
125
+ const settings = def.categorySettings?.[category];
126
+ const settingsKey = getScriptSettingsKey(def, category);
127
+ if (!settings || appliedCategorySettings.has(settingsKey)) {
128
+ runClientCode(def.onConsentChange, def, allowedCategories);
129
+ return;
130
+ }
131
+
132
+ runInitializers(def, allowedCategories, settings.initializers);
133
+ runClientCode(settings.beforeLoad, def, allowedCategories);
134
+ runClientCode(settings.onConsentChange, def, allowedCategories);
135
+ appliedCategorySettings.add(settingsKey);
136
+
137
+ runClientCode(def.onConsentChange, def, allowedCategories);
138
+ };
139
+
140
+ const loadScriptGroup = (
141
+ loadKey: string,
142
+ entries: CookieScriptEntry[],
143
+ allowedCategories: Set<CookieCategory>,
144
+ ) => {
145
+ entries.forEach(({ category, script }) => {
146
+ applyScriptSettings(script, category, allowedCategories);
147
+ });
148
+
149
+ if (loadedScripts.has(loadKey)) return;
150
+
151
+ const def = entries.find(
152
+ ({ script }) => script.src || script.code,
153
+ )?.script;
154
+ if (!def) return;
155
+
156
+ if (import.meta.dev) {
157
+ console.log(`[DEV-MOCK] Loading script: [${def.id}] ${loadKey}`);
158
+ loadedScripts.add(loadKey);
159
+ return;
160
+ }
161
+
162
+ try {
163
+ if (def.src) {
164
+ useScript(def.src, {
165
+ ...def.options,
166
+ scriptAttributes: {
167
+ 'data-cookie-script-key': loadKey,
168
+ 'data-cookie-categories':
169
+ Array.from(allowedCategories).join(','),
170
+ ...def.options?.scriptAttributes,
171
+ },
172
+ });
173
+ } else if (def.code) {
174
+ useScript({
175
+ innerHTML: def.code,
176
+ ...def.options,
177
+ });
178
+ }
179
+ loadedScripts.add(loadKey);
180
+ } catch (e) {
181
+ console.error(`Error loading script ${def.id}:`, e);
182
+ }
183
+ };
184
+
185
+ const loadAllowedScripts = (allowedCategories: Set<CookieCategory>) => {
186
+ const scriptGroups = new Map<string, CookieScriptEntry[]>();
187
+
188
+ getAllowedScriptEntries(allowedCategories).forEach((entry) => {
189
+ const loadKey = getScriptLoadKey(entry.script);
190
+ scriptGroups.set(loadKey, [
191
+ ...(scriptGroups.get(loadKey) || []),
192
+ entry,
193
+ ]);
194
+ });
195
+
196
+ scriptGroups.forEach((entries, loadKey) => {
197
+ loadScriptGroup(loadKey, entries, allowedCategories);
198
+ });
199
+ };
200
+
201
+ const initScripts = () => {
202
+ if (!import.meta.client) return;
203
+
204
+ const { cookiePreferences } = useCookieControl();
205
+
206
+ onNuxtReady(() => {
207
+ watch(
208
+ () => cookiePreferences.value,
209
+ (prefs) => {
210
+ loadAllowedScripts(getAllowedCategories(prefs));
211
+ },
212
+ { deep: true, immediate: true },
213
+ );
214
+ });
215
+ };
216
+
217
+ return {
218
+ initScripts,
219
+ allScriptsConfig: scriptsConfig,
220
+ };
221
+ };
@@ -0,0 +1,32 @@
1
+ import { UI_SITE_SEARCH_OPEN_EVENT } from '~/constants/ui-events';
2
+
3
+ export function openSiteSearch(preset = '', immediate = true) {
4
+ if (typeof window !== 'undefined') {
5
+ window.dispatchEvent(
6
+ new CustomEvent(UI_SITE_SEARCH_OPEN_EVENT, {
7
+ detail: { preset, immediate },
8
+ }),
9
+ );
10
+ }
11
+ }
12
+
13
+ export function closeSiteSearch() {
14
+ if (typeof window !== 'undefined') {
15
+ window.dispatchEvent(new CustomEvent('myelophone:site-search-close'));
16
+ }
17
+ }
18
+
19
+ export function useSiteSearch() {
20
+ const open = (preset = '', immediate = true) => {
21
+ openSiteSearch(preset, immediate);
22
+ };
23
+
24
+ const close = () => {
25
+ closeSiteSearch();
26
+ };
27
+
28
+ return {
29
+ open,
30
+ close,
31
+ };
32
+ }
@@ -0,0 +1,24 @@
1
+ const resetRegistry = new Set<() => void>();
2
+
3
+ export const useSnapScroll = () => {
4
+ const registerContainer = (resetFn: () => void) => {
5
+ onMounted(() => {
6
+ resetRegistry.add(resetFn);
7
+ });
8
+ onUnmounted(() => {
9
+ resetRegistry.delete(resetFn);
10
+ });
11
+ };
12
+
13
+ const scrollToTop = () => {
14
+ window.scrollTo({ top: 0, behavior: 'smooth' });
15
+
16
+ resetRegistry.forEach((reset) => {
17
+ try {
18
+ reset();
19
+ } catch (e) {}
20
+ });
21
+ };
22
+
23
+ return { registerContainer, scrollToTop };
24
+ };
@@ -0,0 +1,5 @@
1
+ export const useStatic = () => {
2
+ return computed(() => {
3
+ return process.env.NUXT_STATIC === 'true';
4
+ });
5
+ };
@@ -0,0 +1,252 @@
1
+ type StorageKind = 'local' | 'session' | 'cookie';
2
+
3
+ interface Serializer<T> {
4
+ read: (value: string) => T;
5
+ write: (value: T) => string;
6
+ }
7
+
8
+ interface StorageOptions<T> {
9
+ storage?: StorageKind;
10
+ fallbackStorage?: Exclude<StorageKind, 'cookie'>;
11
+ expires?: number;
12
+ path?: string;
13
+ deep?: boolean;
14
+ syncTabs?: boolean;
15
+ writeDefault?: boolean;
16
+ serializer?: Serializer<T>;
17
+ canUseCookie?: () => boolean;
18
+ shouldPersist?: (value: T) => boolean;
19
+ }
20
+
21
+ const jsonSerializer = <T>(): Serializer<T> => ({
22
+ read: (value) => JSON.parse(value) as T,
23
+ write: (value) => JSON.stringify(toRaw(value)),
24
+ });
25
+
26
+ const cloneValue = <T>(value: T): T => {
27
+ if (typeof structuredClone === 'function') return structuredClone(value);
28
+ return JSON.parse(JSON.stringify(value)) as T;
29
+ };
30
+
31
+ const isStorageAvailable = (kind: Exclude<StorageKind, 'cookie'>) => {
32
+ if (!import.meta.client) return false;
33
+
34
+ try {
35
+ const storage = kind === 'session' ? sessionStorage : localStorage;
36
+ const key = '__storage_test__';
37
+ storage.setItem(key, key);
38
+ storage.removeItem(key);
39
+ return true;
40
+ } catch {
41
+ return false;
42
+ }
43
+ };
44
+
45
+ export function useStorage<T>(
46
+ key: string,
47
+ defaultValue: T,
48
+ options: StorageOptions<T> = {},
49
+ ): { value: Ref<T>; remove: () => void; refresh: () => void } {
50
+ const {
51
+ storage: preferred = 'local',
52
+ fallbackStorage = 'local',
53
+ expires = 365,
54
+ path = '/',
55
+ deep = true,
56
+ syncTabs = true,
57
+ writeDefault = true,
58
+ serializer = jsonSerializer<T>(),
59
+ canUseCookie = () => true,
60
+ shouldPersist = () => true,
61
+ } = options;
62
+
63
+ const getDefaultValue = () => cloneValue(defaultValue);
64
+ const state = ref<T>(getDefaultValue()) as Ref<T>;
65
+ let broadcast: BroadcastChannel | null = null;
66
+ let isApplyingExternalUpdate = false;
67
+ let isRemoving = false;
68
+
69
+ const canUsePreferredCookie = () =>
70
+ preferred === 'cookie' && canUseCookie();
71
+
72
+ const getEffectiveStorage = (): StorageKind => {
73
+ if (preferred !== 'cookie') return preferred;
74
+ return canUsePreferredCookie() ? 'cookie' : fallbackStorage;
75
+ };
76
+
77
+ const getWebStorage = (kind = getEffectiveStorage()) => {
78
+ if (kind === 'cookie' || !isStorageAvailable(kind)) return null;
79
+ return kind === 'session' ? sessionStorage : localStorage;
80
+ };
81
+
82
+ const read = (): T => {
83
+ const storageKind = getEffectiveStorage();
84
+
85
+ if (storageKind === 'cookie') {
86
+ const cookie = useCookie<T | null>(key, {
87
+ path,
88
+ maxAge: expires * 86400,
89
+ });
90
+ return cookie.value ?? getDefaultValue();
91
+ }
92
+
93
+ const storage = getWebStorage();
94
+ if (!storage) return getDefaultValue();
95
+
96
+ try {
97
+ const raw = storage.getItem(key);
98
+ return raw === null ? getDefaultValue() : serializer.read(raw);
99
+ } catch (error) {
100
+ console.warn(`[useStorage:${key}] failed to read`, error);
101
+ return getDefaultValue();
102
+ }
103
+ };
104
+
105
+ const write = (value: T) => {
106
+ if (!shouldPersist(value)) {
107
+ clearStoredValue();
108
+ return;
109
+ }
110
+
111
+ const storageKind = getEffectiveStorage();
112
+
113
+ if (storageKind === 'cookie') {
114
+ const cookie = useCookie<T>(key, {
115
+ path,
116
+ maxAge: expires * 86400,
117
+ });
118
+ cookie.value = value;
119
+ return;
120
+ }
121
+
122
+ if (preferred === 'cookie') {
123
+ const cookie = useCookie<T | null>(key, { path });
124
+ cookie.value = null;
125
+ }
126
+
127
+ const storage = getWebStorage();
128
+ if (!storage) return;
129
+
130
+ try {
131
+ storage.setItem(key, serializer.write(value));
132
+ } catch (error) {
133
+ console.warn(`[useStorage:${key}] failed to write`, error);
134
+ }
135
+ };
136
+
137
+ const clearStoredValue = () => {
138
+ if (preferred === 'cookie') {
139
+ const cookie = useCookie<T | null>(key, { path });
140
+ cookie.value = null;
141
+ }
142
+
143
+ const storage = getWebStorage(
144
+ preferred === 'cookie' ? fallbackStorage : preferred,
145
+ );
146
+ storage?.removeItem(key);
147
+ };
148
+
149
+ const remove = () => {
150
+ try {
151
+ isRemoving = true;
152
+ state.value = getDefaultValue();
153
+ } finally {
154
+ isRemoving = false;
155
+ }
156
+
157
+ clearStoredValue();
158
+ broadcast?.postMessage({ type: 'remove' });
159
+ };
160
+
161
+ const refresh = () => {
162
+ state.value = read();
163
+ };
164
+
165
+ const shouldDeferClientRead =
166
+ import.meta.client && getEffectiveStorage() !== 'cookie';
167
+
168
+ if (!shouldDeferClientRead) {
169
+ state.value = read();
170
+ if (writeDefault) write(state.value);
171
+ }
172
+
173
+ if (import.meta.client && syncTabs && 'BroadcastChannel' in window) {
174
+ broadcast = new BroadcastChannel(`useStorage:${key}`);
175
+ broadcast.onmessage = (event) => {
176
+ if (!['update', 'remove'].includes(event.data?.type)) return;
177
+
178
+ try {
179
+ isApplyingExternalUpdate = true;
180
+ state.value =
181
+ event.data.type === 'remove'
182
+ ? getDefaultValue()
183
+ : (event.data.value as T);
184
+ if (event.data.type === 'remove') clearStoredValue();
185
+ } finally {
186
+ isApplyingExternalUpdate = false;
187
+ }
188
+ };
189
+ }
190
+
191
+ const stop = watch(
192
+ state,
193
+ (value) => {
194
+ write(value);
195
+
196
+ if (!isApplyingExternalUpdate && !isRemoving) {
197
+ broadcast?.postMessage({
198
+ type: 'update',
199
+ value: cloneValue(toRaw(value) as T),
200
+ });
201
+ }
202
+ },
203
+ { deep, flush: 'sync' },
204
+ );
205
+
206
+ const handleStorage = (event: StorageEvent) => {
207
+ if (event.key !== key) return;
208
+
209
+ try {
210
+ isApplyingExternalUpdate = true;
211
+ state.value =
212
+ event.newValue === null
213
+ ? getDefaultValue()
214
+ : serializer.read(event.newValue);
215
+ } catch (error) {
216
+ console.warn(
217
+ `[useStorage:${key}] failed to sync storage event`,
218
+ error,
219
+ );
220
+ } finally {
221
+ isApplyingExternalUpdate = false;
222
+ }
223
+ };
224
+
225
+ if (import.meta.client && syncTabs && getEffectiveStorage() === 'local') {
226
+ window.addEventListener('storage', handleStorage);
227
+ }
228
+
229
+ if (shouldDeferClientRead) {
230
+ window.setTimeout(() => {
231
+ state.value = read();
232
+ if (writeDefault) write(state.value);
233
+ }, 0);
234
+ }
235
+
236
+ const cleanup = () => {
237
+ stop();
238
+ broadcast?.close();
239
+
240
+ if (
241
+ import.meta.client &&
242
+ syncTabs &&
243
+ getEffectiveStorage() === 'local'
244
+ ) {
245
+ window.removeEventListener('storage', handleStorage);
246
+ }
247
+ };
248
+
249
+ if (getCurrentScope()) onScopeDispose(cleanup);
250
+
251
+ return { value: state, remove, refresh };
252
+ }
@@ -0,0 +1,146 @@
1
+ type StoreBroadcastValue =
2
+ | string
3
+ | number
4
+ | boolean
5
+ | null
6
+ | StoreBroadcastValue[]
7
+ | { [key: string]: StoreBroadcastValue };
8
+
9
+ type StoreBroadcastPayload = Record<string, StoreBroadcastValue>;
10
+
11
+ interface BroadcastableStore {
12
+ $id?: string;
13
+ $patch: (value: Record<string, unknown>) => void;
14
+ [key: string]: unknown;
15
+ }
16
+
17
+ interface StoreBroadcastOptions<TStore extends BroadcastableStore> {
18
+ keys: readonly (Extract<keyof TStore, string> | string)[];
19
+ channel?: string;
20
+ }
21
+
22
+ interface StoreBroadcastMessage {
23
+ source: string;
24
+ store: string;
25
+ payload: StoreBroadcastPayload;
26
+ createdAt: number;
27
+ }
28
+
29
+ const sourceId =
30
+ typeof crypto !== 'undefined' && 'randomUUID' in crypto
31
+ ? crypto.randomUUID()
32
+ : `${Date.now()}:${Math.random()}`;
33
+
34
+ const toBroadcastValue = (value: unknown): StoreBroadcastValue => {
35
+ const seen = new WeakSet<object>();
36
+
37
+ const normalize = (input: unknown): StoreBroadcastValue => {
38
+ if (
39
+ input === null ||
40
+ typeof input === 'string' ||
41
+ typeof input === 'number' ||
42
+ typeof input === 'boolean'
43
+ ) {
44
+ return input;
45
+ }
46
+
47
+ if (input instanceof Date) return input.toISOString();
48
+ if (typeof input !== 'object') return null;
49
+ if (seen.has(input)) return null;
50
+
51
+ seen.add(input);
52
+
53
+ if (Array.isArray(input)) {
54
+ return input.map((item) => normalize(item));
55
+ }
56
+
57
+ return Object.fromEntries(
58
+ Object.entries(input).map(([key, item]) => [key, normalize(item)]),
59
+ );
60
+ };
61
+
62
+ return normalize(value);
63
+ };
64
+
65
+ export function useStoreBroadcast<TStore extends BroadcastableStore>(
66
+ store: TStore,
67
+ options: StoreBroadcastOptions<TStore>,
68
+ ) {
69
+ if (!import.meta.client || !options.keys.length) {
70
+ return () => {};
71
+ }
72
+
73
+ const storeId = store.$id || 'store';
74
+ const channelName = options.channel || `pinia:${storeId}`;
75
+ const keys = [...options.keys].map(String);
76
+ const channel =
77
+ 'BroadcastChannel' in window ? new BroadcastChannel(channelName) : null;
78
+
79
+ let isApplyingRemote = false;
80
+ let lastSerialized = '';
81
+
82
+ const snapshot = () =>
83
+ Object.fromEntries(
84
+ keys.map((key) => [key, toBroadcastValue(store[key])]),
85
+ ) as StoreBroadcastPayload;
86
+
87
+ const post = (payload: StoreBroadcastPayload) => {
88
+ const message: StoreBroadcastMessage = {
89
+ source: sourceId,
90
+ store: storeId,
91
+ payload,
92
+ createdAt: Date.now(),
93
+ };
94
+
95
+ channel?.postMessage(message);
96
+ };
97
+
98
+ const apply = (message: StoreBroadcastMessage) => {
99
+ if (
100
+ message.source === sourceId ||
101
+ message.store !== storeId ||
102
+ !message.payload
103
+ ) {
104
+ return;
105
+ }
106
+
107
+ isApplyingRemote = true;
108
+ try {
109
+ store.$patch(message.payload);
110
+ lastSerialized = JSON.stringify(snapshot());
111
+ } finally {
112
+ isApplyingRemote = false;
113
+ }
114
+ };
115
+
116
+ channel?.addEventListener('message', (event: MessageEvent) => {
117
+ apply(event.data as StoreBroadcastMessage);
118
+ });
119
+
120
+ const stopWatch = watch(
121
+ snapshot,
122
+ (payload) => {
123
+ if (isApplyingRemote) return;
124
+
125
+ const serialized = JSON.stringify(payload);
126
+ if (serialized === lastSerialized) return;
127
+
128
+ lastSerialized = serialized;
129
+ post(payload);
130
+ },
131
+ { deep: true, flush: 'post' },
132
+ );
133
+
134
+ lastSerialized = JSON.stringify(snapshot());
135
+
136
+ const stop = () => {
137
+ stopWatch();
138
+ channel?.close();
139
+ };
140
+
141
+ if (getCurrentInstance()) {
142
+ onScopeDispose(stop);
143
+ }
144
+
145
+ return stop;
146
+ }