@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,179 @@
1
+ <script setup lang="ts">
2
+ import type {
3
+ CookieCategory,
4
+ CookieScriptDef,
5
+ CookieScriptsConfig,
6
+ } from "~/types/cookie";
7
+ import { useMyelophoneConfig } from "~/utils/myelophoneConfig";
8
+
9
+ type PolicyCategory = CookieCategory | "notices";
10
+
11
+ const props = withDefaults(
12
+ defineProps<{
13
+ title?: string;
14
+ showSources?: boolean;
15
+ }>(),
16
+ {
17
+ title: "Cookies and Similar Technologies",
18
+ showSources: true,
19
+ },
20
+ );
21
+
22
+ const { t } = useMultiLang(["cookies"]);
23
+ const config = useMyelophoneConfig();
24
+
25
+ const cookieScripts = computed(
26
+ () => (config.cookieScripts || {}) as CookieScriptsConfig,
27
+ );
28
+
29
+ const categories = computed<
30
+ Array<{
31
+ key: PolicyCategory;
32
+ title: string;
33
+ description: string;
34
+ scripts: CookieScriptDef[];
35
+ }>
36
+ >(() =>
37
+ [
38
+ {
39
+ key: "notices" as const,
40
+ title: t("cookies.cookielessTitle"),
41
+ description: t("cookies.cookielessDesc"),
42
+ },
43
+ {
44
+ key: "necessary" as const,
45
+ title: t("cookies.necessaryTitle"),
46
+ description: t("cookies.necessaryDesc"),
47
+ },
48
+ {
49
+ key: "analytics" as const,
50
+ title: t("cookies.analyticsTitle"),
51
+ description: t("cookies.analyticsDesc"),
52
+ },
53
+ {
54
+ key: "marketing" as const,
55
+ title: t("cookies.marketingTitle"),
56
+ description: t("cookies.marketingDesc"),
57
+ },
58
+ {
59
+ key: "functional" as const,
60
+ title: t("cookies.functionalTitle"),
61
+ description: t("cookies.functionalDesc"),
62
+ },
63
+ ]
64
+ .map((category) => ({
65
+ ...category,
66
+ scripts: cookieScripts.value[category.key] || [],
67
+ }))
68
+ .filter((category) => category.scripts.length),
69
+ );
70
+
71
+ const scriptName = (script: CookieScriptDef) => t(script.nameKey!);
72
+ const scriptDescription = (script: CookieScriptDef) =>
73
+ t(script.descriptionKey!);
74
+ const legalPurpose = (script: CookieScriptDef) =>
75
+ script.legal?.purposeKey ? t(script.legal.purposeKey) : "";
76
+ const cookiesDescription = (script: CookieScriptDef) =>
77
+ script.legal?.cookiesDescriptionKey
78
+ ? t(script.legal.cookiesDescriptionKey)
79
+ : "";
80
+ const legalNotes = (script: CookieScriptDef) =>
81
+ script.legal?.notesKey ? t(script.legal.notesKey) : "";
82
+ </script>
83
+
84
+ <template>
85
+ <section
86
+ class="max-w-3xl text-sm leading-7 text-neutral-700 dark:text-neutral-300"
87
+ >
88
+ <h2
89
+ class="text-xl font-semibold tracking-normal text-neutral-950 dark:text-white"
90
+ >
91
+ {{ props.title }}
92
+ </h2>
93
+
94
+ <p class="mt-3">
95
+ {{ t("cookies.cookieNote") }}
96
+ </p>
97
+
98
+ <div class="mt-6 space-y-6">
99
+ <section v-for="category in categories" :key="category.key">
100
+ <h3
101
+ class="text-base font-semibold text-neutral-950 dark:text-white"
102
+ >
103
+ {{ category.title }}
104
+ </h3>
105
+
106
+ <p class="mt-1 text-neutral-600 dark:text-neutral-400">
107
+ {{ category.description }}
108
+ </p>
109
+
110
+ <ul class="mt-3 space-y-3">
111
+ <li
112
+ v-for="script in category.scripts"
113
+ :key="`${category.key}-${script.id}-${script.loadKey || script.src || 'inline'}`"
114
+ class="border-l border-neutral-200 dark:border-neutral-800"
115
+ >
116
+ <p class="font-medium text-neutral-900 dark:text-white">
117
+ {{ scriptName(script) }}
118
+ </p>
119
+
120
+ <p
121
+ class="mt-0.5 text-neutral-600 dark:text-neutral-400"
122
+ >
123
+ {{ scriptDescription(script) }}
124
+ </p>
125
+
126
+ <p
127
+ v-if="legalPurpose(script)"
128
+ class="mt-1 text-neutral-600 dark:text-neutral-400"
129
+ >
130
+ {{ legalPurpose(script) }}
131
+ </p>
132
+
133
+ <p
134
+ v-if="cookiesDescription(script)"
135
+ class="mt-1 text-neutral-600 dark:text-neutral-400"
136
+ >
137
+ {{ cookiesDescription(script) }}
138
+ </p>
139
+
140
+ <p
141
+ v-if="legalNotes(script)"
142
+ class="mt-1 text-neutral-600 dark:text-neutral-400"
143
+ >
144
+ {{ legalNotes(script) }}
145
+ </p>
146
+
147
+ <p
148
+ v-if="
149
+ showSources &&
150
+ (script.legal?.privacyPolicyUrl ||
151
+ script.legal?.officialDocsUrl)
152
+ "
153
+ class="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs"
154
+ >
155
+ <a
156
+ v-if="script.legal?.privacyPolicyUrl"
157
+ :href="script.legal.privacyPolicyUrl"
158
+ rel="noopener noreferrer"
159
+ target="_blank"
160
+ class="font-medium text-neutral-900 underline underline-offset-2 dark:text-white"
161
+ >
162
+ Privacy policy
163
+ </a>
164
+ <a
165
+ v-if="script.legal?.officialDocsUrl"
166
+ :href="script.legal.officialDocsUrl"
167
+ rel="noopener noreferrer"
168
+ target="_blank"
169
+ class="font-medium text-neutral-900 underline underline-offset-2 dark:text-white"
170
+ >
171
+ Official documentation
172
+ </a>
173
+ </p>
174
+ </li>
175
+ </ul>
176
+ </section>
177
+ </div>
178
+ </section>
179
+ </template>
@@ -0,0 +1,387 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ COOKIE_CATEGORIES,
4
+ type CookieCategory,
5
+ type CookieSelection,
6
+ type CookieScriptDef,
7
+ } from "~/types/cookie";
8
+
9
+ const { t } = useMultiLang(["cookies"]);
10
+
11
+ const { isOpen, close } = useCookieModal();
12
+ const { cookiePreferences, savePreferences, declineAll, acceptAll } =
13
+ useCookieControl();
14
+ const { allScriptsConfig } = useScriptLoader();
15
+
16
+ const visibleCookieCategories = computed(() =>
17
+ COOKIE_CATEGORIES.filter(
18
+ (cat) =>
19
+ cat === "necessary" ||
20
+ cat === "functional" ||
21
+ (allScriptsConfig[cat]?.length ?? 0) > 0,
22
+ ),
23
+ );
24
+
25
+ const localSelection = reactive<CookieSelection>({
26
+ necessary: true,
27
+ analytics: false,
28
+ marketing: false,
29
+ functional: false,
30
+ });
31
+
32
+ const detectedCookies = ref<{ key: string; value: string }[]>([]);
33
+
34
+ const noticeScripts = computed(() =>
35
+ (allScriptsConfig.notices || []).filter(
36
+ (script: CookieScriptDef) => script.legalBasis === "cookieless",
37
+ ),
38
+ );
39
+
40
+ const categoryInfo = computed<
41
+ Record<CookieCategory, { title: string; desc: string }>
42
+ >(() => {
43
+ return {
44
+ necessary: {
45
+ title: t("cookies.necessaryTitle"),
46
+ desc: t("cookies.necessaryDesc"),
47
+ },
48
+ analytics: {
49
+ title: t("cookies.analyticsTitle"),
50
+ desc: t("cookies.analyticsDesc"),
51
+ },
52
+ marketing: {
53
+ title: t("cookies.marketingTitle"),
54
+ desc: t("cookies.marketingDesc"),
55
+ },
56
+ functional: {
57
+ title: t("cookies.functionalTitle"),
58
+ desc: t("cookies.functionalDesc"),
59
+ },
60
+ };
61
+ });
62
+
63
+ watch(isOpen, (val) => {
64
+ if (val) {
65
+ const currentPrefs = cookiePreferences.value;
66
+ COOKIE_CATEGORIES.forEach((cat) => {
67
+ localSelection[cat] =
68
+ cat === "necessary"
69
+ ? true
70
+ : currentPrefs.status === "pending"
71
+ ? false
72
+ : currentPrefs[cat];
73
+ });
74
+ parseCookies();
75
+ }
76
+ });
77
+
78
+ const parseCookies = () => {
79
+ if (typeof document === "undefined") return;
80
+ const cookies = document.cookie.split(";");
81
+
82
+ detectedCookies.value = cookies
83
+ .map((c: string) => {
84
+ const [key, ...v] = c.trim().split("=");
85
+ return { key: key || "", value: v.join("=") };
86
+ })
87
+ .filter((c) => c.key !== "");
88
+ };
89
+
90
+ const handleSave = () => {
91
+ savePreferences(localSelection);
92
+ close();
93
+ };
94
+ const handleRejectAll = () => {
95
+ declineAll();
96
+ close();
97
+ };
98
+ const handleAcceptAll = () => {
99
+ acceptAll();
100
+ close();
101
+ };
102
+ </script>
103
+
104
+ <template>
105
+ <UiModal
106
+ v-model="isOpen"
107
+ :title="t('cookies.privacyTitle')"
108
+ width="max-w-3xl"
109
+ >
110
+ <div class="space-y-6">
111
+ <p
112
+ class="text-sm text-neutral-600 dark:text-neutral-400 leading-relaxed"
113
+ >
114
+ {{ t("cookies.cookieNote") }}
115
+ </p>
116
+
117
+ <div class="space-y-3">
118
+ <div
119
+ v-for="cat in visibleCookieCategories"
120
+ :key="cat"
121
+ class="group rounded-xl border border-neutral-200 bg-white p-4 transition-all hover:border-neutral-300 hover:shadow-sm dark:border-neutral-800 dark:bg-neutral-900 dark:hover:border-neutral-700"
122
+ >
123
+ <div class="flex items-start justify-between gap-4">
124
+ <div class="flex-1 min-w-0">
125
+ <div
126
+ class="flex items-center flex-wrap gap-2 min-h-6"
127
+ >
128
+ <p
129
+ class="text-base font-semibold text-neutral-900 dark:text-white leading-none"
130
+ >
131
+ {{ categoryInfo[cat].title }}
132
+ </p>
133
+ <span
134
+ v-if="cat === 'necessary'"
135
+ class="text-base text-neutral-400 dark:text-neutral-500 font-normal leading-none"
136
+ >
137
+ {{ t("cookies.alwaysOn") }}
138
+ </span>
139
+ </div>
140
+
141
+ <p
142
+ class="mt-1.5 text-sm text-neutral-500 dark:text-neutral-400 leading-snug"
143
+ >
144
+ {{ categoryInfo[cat].desc }}
145
+ </p>
146
+
147
+ <div class="mt-3">
148
+ <details
149
+ v-if="
150
+ (allScriptsConfig[cat]?.length ?? 0) > 0
151
+ "
152
+ >
153
+ <summary
154
+ class="inline-flex cursor-pointer items-center gap-1 text-xs font-medium text-(--ui-text) opacity-70 hover:opacity-100 transition-opacity select-none"
155
+ >
156
+ <span
157
+ >{{ t("cookies.usedServices") }} ({{
158
+ allScriptsConfig[cat]!.length
159
+ }})</span
160
+ >
161
+ <svg
162
+ class="h-3 w-3 transition-transform group-open:rotate-180"
163
+ fill="none"
164
+ viewBox="0 0 24 24"
165
+ stroke="currentColor"
166
+ >
167
+ <path
168
+ stroke-linecap="round"
169
+ stroke-linejoin="round"
170
+ stroke-width="2"
171
+ d="M19 9l-7 7-7-7"
172
+ />
173
+ </svg>
174
+ </summary>
175
+
176
+ <div
177
+ class="mt-2 pl-3 border-l-2 border-neutral-100 dark:border-neutral-800 space-y-2"
178
+ >
179
+ <div
180
+ v-for="script in allScriptsConfig[
181
+ cat
182
+ ]"
183
+ :key="script.id"
184
+ class="text-xs"
185
+ >
186
+ <p
187
+ class="font-medium text-neutral-700 dark:text-neutral-300 text-xs"
188
+ >
189
+ {{ t(script.nameKey!) }}
190
+ </p>
191
+ <p
192
+ v-if="script.descriptionKey"
193
+ class="text-neutral-500 dark:text-neutral-500 text-xs"
194
+ >
195
+ {{ t(script.descriptionKey!) }}
196
+ </p>
197
+ </div>
198
+ </div>
199
+ </details>
200
+
201
+ <p
202
+ v-else-if="cat !== 'necessary'"
203
+ class="text-xs text-neutral-400 dark:text-neutral-600 italic select-none"
204
+ >
205
+ {{ t("cookies.usedNotServices") }}
206
+ </p>
207
+ </div>
208
+ </div>
209
+
210
+ <div class="shrink-0">
211
+ <UiToggler
212
+ v-model="localSelection[cat]"
213
+ :disabled="cat === 'necessary'"
214
+ />
215
+ </div>
216
+ </div>
217
+ </div>
218
+ </div>
219
+
220
+ <details
221
+ class="rounded-lg border border-neutral-200 dark:border-neutral-800 open:bg-neutral-50 dark:open:bg-white/5"
222
+ >
223
+ <summary
224
+ class="flex cursor-pointer items-center justify-between px-4 py-3 text-sm font-medium text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-200 outline-none select-none"
225
+ >
226
+ <span
227
+ >🍪 {{ t("cookies.cookiesList") }} ({{
228
+ detectedCookies.length
229
+ }})</span
230
+ >
231
+ <svg
232
+ class="h-4 w-4"
233
+ fill="none"
234
+ viewBox="0 0 24 24"
235
+ stroke="currentColor"
236
+ >
237
+ <path
238
+ stroke-linecap="round"
239
+ stroke-linejoin="round"
240
+ stroke-width="2"
241
+ d="M19 9l-7 7-7-7"
242
+ />
243
+ </svg>
244
+ </summary>
245
+ <div
246
+ class="px-4 pb-4 pt-2 border-t border-neutral-200 dark:border-neutral-800"
247
+ >
248
+ <div
249
+ v-if="detectedCookies.length > 0"
250
+ class="overflow-x-auto max-h-40 overflow-y-auto custom-scrollbar"
251
+ >
252
+ <table class="w-full text-left text-xs">
253
+ <tbody
254
+ class="divide-y divide-neutral-200/50 dark:divide-neutral-700/50"
255
+ >
256
+ <tr v-for="c in detectedCookies" :key="c.key">
257
+ <td
258
+ class="py-1.5 pr-4 font-mono font-medium text-neutral-700 dark:text-neutral-300"
259
+ >
260
+ {{ c.key }}
261
+ </td>
262
+ <td
263
+ class="py-1.5 font-mono text-neutral-500 truncate max-w-50"
264
+ :title="c.value"
265
+ >
266
+ {{ c.value }}
267
+ </td>
268
+ </tr>
269
+ </tbody>
270
+ </table>
271
+ </div>
272
+ <p v-else class="text-xs text-neutral-400 italic">
273
+ {{ t("cookies.emptyList") }}
274
+ </p>
275
+ </div>
276
+ </details>
277
+ </div>
278
+
279
+ <div
280
+ v-if="noticeScripts.length"
281
+ class="group rounded-xl border border-neutral-200 bg-white p-4 transition-all hover:border-neutral-300 hover:shadow-sm dark:border-neutral-800 dark:bg-neutral-900 dark:hover:border-neutral-700 mt-6"
282
+ >
283
+ <div class="flex items-start justify-between gap-4">
284
+ <div class="min-w-0 flex-1">
285
+ <div class="flex min-h-6 flex-wrap items-center gap-2">
286
+ <p
287
+ class="text-base font-semibold leading-none text-neutral-900 dark:text-white"
288
+ >
289
+ {{ t("cookies.cookielessTitle") }}
290
+ </p>
291
+ </div>
292
+
293
+ <p
294
+ class="mt-1.5 text-sm leading-snug text-neutral-500 dark:text-neutral-400"
295
+ >
296
+ {{ t("cookies.cookielessDesc") }}
297
+ </p>
298
+
299
+ <div class="mt-3">
300
+ <details>
301
+ <summary
302
+ class="inline-flex cursor-pointer select-none items-center gap-1 text-xs font-medium text-(--ui-text) opacity-70 transition-opacity hover:opacity-100"
303
+ >
304
+ <span
305
+ >Services used ({{
306
+ noticeScripts.length
307
+ }})</span
308
+ >
309
+ <svg
310
+ class="h-3 w-3 transition-transform group-open:rotate-180"
311
+ fill="none"
312
+ viewBox="0 0 24 24"
313
+ stroke="currentColor"
314
+ >
315
+ <path
316
+ stroke-linecap="round"
317
+ stroke-linejoin="round"
318
+ stroke-width="2"
319
+ d="M19 9l-7 7-7-7"
320
+ />
321
+ </svg>
322
+ </summary>
323
+
324
+ <div
325
+ class="mt-2 space-y-2 border-l-2 border-neutral-100 pl-3 dark:border-neutral-800"
326
+ >
327
+ <div
328
+ v-for="script in noticeScripts"
329
+ :key="script.id"
330
+ class="text-xs"
331
+ >
332
+ <p
333
+ class="text-xs font-medium text-neutral-700 dark:text-neutral-300"
334
+ >
335
+ {{ t(script.nameKey!) }}
336
+ </p>
337
+ <p
338
+ class="text-xs text-neutral-500 dark:text-neutral-500"
339
+ >
340
+ {{ t(script.descriptionKey!) }}
341
+ </p>
342
+ </div>
343
+ </div>
344
+ </details>
345
+ </div>
346
+ </div>
347
+
348
+ <div class="shrink-0">
349
+ <span
350
+ class="inline-flex rounded-full bg-neutral-100 px-2.5 py-1 text-xs font-medium text-neutral-600 dark:bg-neutral-800 dark:text-neutral-300"
351
+ >
352
+ Notice
353
+ </span>
354
+ </div>
355
+ </div>
356
+ </div>
357
+
358
+ <template #footer>
359
+ <div
360
+ class="flex flex-col-reverse sm:flex-row w-full gap-3 sm:items-center sm:justify-between"
361
+ >
362
+ <button
363
+ @click="handleRejectAll"
364
+ class="w-full sm:w-auto px-4 py-2.5 text-sm font-medium text-neutral-600 hover:text-neutral-900 hover:bg-neutral-100 rounded-lg transition-colors dark:text-neutral-400 dark:hover:text-white dark:hover:bg-white/10"
365
+ >
366
+ {{ t("cookies.declineAll") }}
367
+ </button>
368
+
369
+ <div class="flex flex-col sm:flex-row gap-3 w-full sm:w-auto">
370
+ <button
371
+ @click="handleSave"
372
+ class="w-full sm:w-auto px-5 py-2.5 rounded-lg text-sm font-semibold transition-all border shadow-sm bg-white text-neutral-900 border-neutral-300 hover:bg-neutral-50 hover:border-neutral-400 dark:bg-transparent dark:text-white dark:border-neutral-600 dark:hover:border-neutral-500 dark:hover:text-black"
373
+ >
374
+ {{ t("cookies.savePrefs") }}
375
+ </button>
376
+
377
+ <button
378
+ @click="handleAcceptAll"
379
+ class="w-full sm:w-auto px-6 py-2.5 rounded-lg text-sm font-bold shadow-md transition-transform active:scale-95 bg-neutral-900 text-white hover:bg-black dark:bg-white dark:text-neutral-900 dark:hover:bg-neutral-100"
380
+ >
381
+ {{ t("cookies.acceptAll") }}
382
+ </button>
383
+ </div>
384
+ </div>
385
+ </template>
386
+ </UiModal>
387
+ </template>