@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
package/README.md ADDED
@@ -0,0 +1,3004 @@
1
+ # @myelophone/nuxt
2
+
3
+ Production-oriented Nuxt 4 + Tailwind CSS application framework and Nuxt layer with SSR/SSG, multilingual routing, consent-aware integrations, Pinia stores, SEO, security headers, performance tooling, and reusable responsive components library.
4
+
5
+ > [!TIP]
6
+ > **Need a ready-to-run frontend instead of assembling a project from scratch?**
7
+ > [`@myelophone/nuxt-template`](https://github.com/myelophone/nuxt-template) is the official frontend boilerplate built on `@myelophone/nuxt`. It provides a prepared application structure and starting point for quickly developing a new service or web site.
8
+ >
9
+ > **[Open template repository](https://github.com/myelophone/nuxt-template)** · **[Create a repository from this template](https://github.com/myelophone/nuxt-template/generate)**
10
+ >
11
+ > ```bash
12
+ > git clone https://github.com/myelophone/nuxt-template.git my-frontend
13
+ > cd my-frontend
14
+ > ```
15
+
16
+ ## What is @myelophone/nuxt
17
+
18
+ @myelophone/nuxt is an extendable Nuxt framework layer that supplies application shell behavior, modules, components, composables, stores, styles, middleware, server routes, and build optimizations.
19
+
20
+ This repository contains the framework source and its `playground/` test application used exclusively while developing the layer, following the Nuxt convention for module/layer development. New applications should start from the separate [@myelophone/nuxt-template](https://github.com/MyelophOne/nuxt-template), which consumes this layer and provides the application-facing project structure.
21
+
22
+ It is intended for content sites, landing pages, multilingual corporate sites, product interfaces, dashboards, catalogues, and commerce frontends that need a common production baseline instead of assembling the same infrastructure for every project.
23
+
24
+ The layer includes:
25
+
26
+ - Nuxt 4 and Vue 3 with TypeScript;
27
+ - Tailwind CSS 4 and a light/dark CSS-variable theme;
28
+ - SSR builds with Nitro's `node-cluster` preset by default, plus an explicit static generation mode;
29
+ - localized routes and lazy namespace-based translations without external dependencies;
30
+ - SEO metadata, canonical URLs, alternate-language links, robots, OG images, breadcrumbs JSON-LD, and FAQ JSON-LD;
31
+ - cookie consent with necessary, analytics, marketing, and functional categories;
32
+ - presets for 30+ analytics, advertising, CRM, chat, and form providers;
33
+ - built-in cross-tab synchronization for theme and consent settings, optional user and cart stores, and selected fields from custom Pinia stores;
34
+ - currency conversion and an exchange-rate proxy;
35
+ - reusable UI, layout, media, navigation, and content components;
36
+ - CSP and security headers, request size limits, rate limiting, URL normalization, compression, asset caching, CSS cleanup, and build-time i18n tree shaking;
37
+ - reduced-motion, slow-network, low-battery, bot, and legacy-browser fallbacks;
38
+ - health endpoints, Playwright smoke testing, Biome, Commitlint, and semantic-release.
39
+
40
+ ## Requirements
41
+
42
+ - Node.js 24+ for local development. CI currently uses Node.js 26.
43
+ - Yarn 4.18.0 through Corepack.
44
+ - A modern browser for the complete interactive experience. Static and reduced-motion fallbacks cover constrained clients.
45
+
46
+ ```bash
47
+ corepack enable
48
+ yarn install
49
+ yarn prepare
50
+ ```
51
+
52
+ ## Quick start
53
+
54
+ ### Start a new application from the template
55
+
56
+ Use the dedicated template repository for application development:
57
+
58
+ ```bash
59
+ git clone https://github.com/MyelophOne/nuxt-template.git my-app
60
+ cd my-app
61
+ corepack enable
62
+ yarn install
63
+ yarn dev
64
+ ```
65
+
66
+ The template owns project-specific pages, components, assets, locales, and configuration while inheriting the framework from `@myelophone/nuxt`. Follow the template README for its exact file layout and bootstrap process.
67
+
68
+ Inside this framework repository, maintainers test layer behavior and configuration overrides through `playground/myelophone.ts`:
69
+
70
+ ```ts
71
+ export default defineNuxtConfig({
72
+ app: {
73
+ head: {
74
+ title: "Acme",
75
+ },
76
+ },
77
+ multi18n: {
78
+ defaultLocale: "en",
79
+ locales: ["en", "pl", "de"],
80
+ },
81
+ runtimeConfig: {
82
+ apiBaseServer: "https://api.internal.example.com",
83
+ public: {
84
+ apiBase: "https://api.example.com",
85
+ },
86
+ },
87
+ myelophone: {
88
+ siteDomain: "https://example.com",
89
+ bundleTranslations: false,
90
+ splitCss: false,
91
+ stores: { cart: true, user: true },
92
+ frankfurterBaseCurrency: "EUR",
93
+ frankfurterCurrencies: ["USD", "PLN", "GBP"],
94
+ creativeCursor: false,
95
+ },
96
+ });
97
+ ```
98
+
99
+ `playground/nuxt.config.ts` deep-merges this file over the base layer. Objects are merged recursively and arrays are appended with duplicate primitive values removed. This playground exists only for framework development and testing; application scaffolding belongs to `nuxt-template`.
100
+
101
+ ### Extend it as a Nuxt layer
102
+
103
+ The layer can be installed directly from GitHub without registry configuration, access tokens, or GitHub Packages authentication. Add the dependency to the consuming application's `package.json`:
104
+
105
+ ```json
106
+ {
107
+ "devDependencies": {
108
+ "@myelophone/nuxt": "git+https://github.com/myelophone/nuxt.git"
109
+ }
110
+ }
111
+ ```
112
+
113
+ Install dependencies and prepare Nuxt types:
114
+
115
+ ```bash
116
+ yarn install
117
+ yarn nuxt prepare
118
+ ```
119
+
120
+ The dependency name from `package.json` is then used by `extends`:
121
+
122
+ ```ts
123
+ // nuxt.config.ts
124
+ export default defineNuxtConfig({
125
+ extends: ["@myelophone/nuxt"],
126
+
127
+ multi18n: {
128
+ defaultLocale: "en",
129
+ locales: ["en", "pl"],
130
+ },
131
+ });
132
+ ```
133
+
134
+ ## Commands
135
+
136
+ | Command | Purpose |
137
+ | ------------------- | -------------------------------------------------------------- |
138
+ | `yarn dev` | Start the playground in development mode. |
139
+ | `yarn build` | Build the playground for SSR using the selected Nitro preset. |
140
+ | `yarn server` | Run the built Node server. |
141
+ | `yarn generate` | Generate a static site into `playground/.output/public`. |
142
+ | `yarn preview` | Preview a completed build. |
143
+ | `yarn analyze` | Analyze the Nuxt bundle. |
144
+ | `yarn test:types` | Type-check the layer and playground. |
145
+ | `yarn playwright` | Run browser tests; it builds and starts the app automatically. |
146
+ | `yarn test` | Run type checks, then Playwright tests. |
147
+ | `yarn biome:check` | Apply Biome lint fixes to `app/` and `playground/`. |
148
+ | `yarn biome:format` | Format `app/` and `playground/`. |
149
+ | `yarn audit` | Audit installed packages. |
150
+ | `yarn upgrade` | Upgrade Nuxt and regenerate prepared types. |
151
+ | `yarn update` | Interactively update dependencies. |
152
+ | `yarn lock-update` | Deduplicate the lockfile using the highest versions. |
153
+
154
+ The scripts use POSIX-style environment assignments and are expected to run in Linux, macOS, WSL, Git Bash, or CI. On native PowerShell, set the variables first or use a compatible shell.
155
+
156
+ ## Configuration reference
157
+
158
+ ### Environment variables
159
+
160
+ | Variable | Effect |
161
+ | ------------------------------------- | ---------------------------------------------------------------------------------------------------- |
162
+ | `NUXT_STATIC=true` | Select the `static` Nitro preset, disable IPX, and copy the included `.htaccess` into static output. |
163
+ | `NUXT_SSR_STREAMING=true` | Enable experimental streaming SSR for non-static server builds; disabled by default. |
164
+ | `NITRO_PRESET=<preset>` | Override the SSR preset; defaults to `node-cluster`. |
165
+ | `NUXT_PLAYGROUND=true` | Include playground sources/locales while developing or building this repository. |
166
+ | `PROD_DIST=true` | Exclude playground-only locale globs from `useMultiLang`. |
167
+ | `JSON_PLACEHOLDER_API_BASE_URL=<url>` | Replace the default `jsonPlaceholder` endpoint used by `nuxt-api-party`. |
168
+ | `NODE_ENV=<mode>` | Select `development`, `production`, or `test` behavior. |
169
+
170
+ Nuxt runtime values can also be supplied with standard `NUXT_*` environment-variable mapping, for example `NUXT_API_BASE_SERVER` and `NUXT_PUBLIC_API_BASE`.
171
+
172
+ ### Runtime configuration
173
+
174
+ | Key | Default | Purpose |
175
+ | ---------------- | ------- | -------------------------------------------------------- |
176
+ | `apiBaseServer` | unset | Private server-side base URL used by `useApi`. |
177
+ | `public.apiBase` | unset | Public API base fallback used by `useApi` on the server. |
178
+
179
+ ### Myelophone build-time configuration
180
+
181
+ Project-level framework options belong in the top-level `myelophone` section of `nuxt.config.ts` or `playground/myelophone.ts`. These values are resolved during Nuxt setup/build instead of being serialized through `runtimeConfig.public` in the HTML payload. The layer supplies defaults, and local Nuxt config files can override them through normal Nuxt config merging.
182
+
183
+ | Key | Default | Purpose |
184
+ | ------------------------------------ | ----------------------- | ---------------------------------------------------------------------------------- |
185
+ | `myelophone.siteDomain` | `http://localhost:3000` | Absolute production origin for SEO URLs and `/sitemap.xml`. |
186
+ | `myelophone.blog.blogEnabled` | `true` | Enables Markdown post routes, prerendering, and sitemap entries. |
187
+ | `myelophone.blog.postsLayout` | `default-blog` | Nuxt layout used by Markdown post pages. |
188
+ | `myelophone.cookieControl.enabled` | `true` | Enables the consent banner and preferences flow. |
189
+ | `myelophone.cookieScripts` | empty categories | Consent-aware integrations and legal notices. |
190
+ | `myelophone.bundleTranslations` | `true` | Bundle locale data together; `false` enables per-locale chunk grouping. |
191
+ | `myelophone.splitCss` | `false` | Controls Vite CSS code splitting. |
192
+ | `myelophone.stores.cart` | `false` | Initialize the cart store, exchange rates, persistence, and tab sync. |
193
+ | `myelophone.stores.user` | `false` | Initialize the user store, session extension, persistence, and tab sync. |
194
+ | `myelophone.frankfurterCurrencies` | `[]` | Default quote currencies for `/api/exchange-rates`. Empty means provider defaults. |
195
+ | `myelophone.frankfurterBaseCurrency` | `USD` | Base currency for the exchange-rate endpoint. |
196
+ | `myelophone.creativeCursor` | `false` | Enable the custom cursor for fine pointers without reduced motion. |
197
+ | `myelophone.siteSearch` | built-in defaults | Site-search strategy, query params, limits, and enablement. |
198
+ | `myelophone.pageFullscreenPreloader` | `{}` / disabled | Optional global full-screen route preloader component and overlay configuration. |
199
+ | `myelophone.tally.domain` | `tally.so` | Default host used by `ViewTallyForm`. |
200
+ | `myelophone.noindex` | `false` | Hide whole site from search engines. |
201
+ | `myelophone.ssrStream` | `false` | Enable experimental streaming SSR when not building static output. |
202
+
203
+ ## Rendering and deployment
204
+
205
+ ### SSR / Node cluster
206
+
207
+ ```bash
208
+ yarn build
209
+ yarn server
210
+ ```
211
+
212
+ > [!IMPORTANT]
213
+ > A regular production build uses Nitro's `node-cluster` preset by default. Running `yarn build` without `NUXT_STATIC` or `NITRO_PRESET` therefore produces a clustered Node.js server build.
214
+
215
+ The effective preset selection is:
216
+
217
+ ```ts
218
+ process.env.NUXT_STATIC === "true"
219
+ ? "static"
220
+ : process.env.NITRO_PRESET || "node-cluster";
221
+ ```
222
+
223
+ Start the generated cluster build with `yarn server`, which runs `playground/.output/server/index.mjs` in this repository. In an application based on `@myelophone/nuxt-template`, run its corresponding production server command.
224
+
225
+ The default cluster preset lets Nitro run the application through Node's cluster model. This is the ready-to-use default for a dedicated Node.js host or container where the application owns its process model. HTML responses are compressed with Brotli/Gzip, public assets are precompressed, and hashed `/_nuxt/**` assets receive a one-year immutable cache header.
226
+
227
+ No configuration is required to keep `node-cluster`. Set `NITRO_PRESET` only when the deployment platform expects another Nitro target. For example, use a single Node server when Kubernetes, Docker orchestration, systemd, PM2, or the hosting platform manages process replication externally:
228
+
229
+ ```bash
230
+ NITRO_PRESET=node-server yarn build
231
+ ```
232
+
233
+ `NUXT_STATIC=true` takes precedence over `NITRO_PRESET` and selects the static preset instead of a Node server build.
234
+
235
+ ### Streaming SSR
236
+
237
+ The layer can use Nuxt's experimental streaming renderer in non-static builds. Enable it in `myelophone.ts`:
238
+
239
+ ```ts
240
+ export default defineNuxtConfig({
241
+ myelophone: {
242
+ ssrStream: true,
243
+ },
244
+ });
245
+ ```
246
+
247
+ The environment variable is also supported for CI or deployment-specific overrides:
248
+
249
+ ```bash
250
+ NUXT_SSR_STREAMING=true yarn build
251
+ yarn server
252
+ ```
253
+
254
+ On native PowerShell:
255
+
256
+ ```powershell
257
+ $env:NUXT_SSR_STREAMING = "true"
258
+ yarn build
259
+ yarn server
260
+ ```
261
+
262
+ Streaming SSR is disabled by default. The framework maps `myelophone.ssrStream: true` or `NUXT_SSR_STREAMING=true` to `experimental.ssrStreaming.enabled`. Static generation always keeps streaming disabled because prerendered routes are emitted as complete HTML files.
263
+
264
+ With streaming enabled, Nuxt sends the document shell first and streams the rendered route body afterwards. Server-rendered data is still part of the final HTML response; streaming changes when chunks are sent, not whether the route content is rendered on the server. Bots, prerendered routes, SPA routes, redirects, and routes with incompatible cache rules can automatically use buffered rendering.
265
+
266
+ #### Server data that must be present in page source
267
+
268
+ Await `useFetch` or `useAsyncData` during setup. Nuxt renders the resolved value into the HTML and serializes it in the hydration payload, so the browser does not repeat the initial request:
269
+
270
+ ```vue
271
+ <script setup lang="ts">
272
+ interface Post {
273
+ id: number;
274
+ title: string;
275
+ body: string;
276
+ }
277
+
278
+ const { data: posts, error } = await useFetch<Post[]>(
279
+ "https://jsonplaceholder.typicode.com/posts",
280
+ {
281
+ key: "posts-stream",
282
+ query: { _limit: 6 },
283
+ server: true,
284
+ default: () => [],
285
+ },
286
+ );
287
+ </script>
288
+
289
+ <template>
290
+ <p v-if="error">Could not load posts.</p>
291
+ <article v-for="post in posts" v-else :key="post.id">
292
+ <h2>{{ post.title }}</h2>
293
+ <p>{{ post.body }}</p>
294
+ </article>
295
+ </template>
296
+ ```
297
+
298
+ Do not move SEO or no-JavaScript content into `onMounted`, and do not wrap it in `ClientOnly`: neither approach can place the fetched result in the server HTML. Prefer `useFetch`/`useAsyncData` over a bare universal `$fetch`, because the Nuxt composables transfer the resolved value through the payload and avoid a duplicate hydration request.
299
+
300
+ #### Deliberately client-only or heavy data
301
+
302
+ Use a lazy client component when its code and data are intentionally excluded from SSR. A `.client.vue` suffix prevents server rendering, `Lazy` keeps the component in a separate async chunk, and `server: false` documents that the request must run in the browser:
303
+
304
+ ```vue
305
+ <template>
306
+ <ClientOnly>
307
+ <LazyExamplesHeavyPosts v-if="showHeavyComponent" />
308
+ <template #fallback>Posts are not part of the server HTML.</template>
309
+ </ClientOnly>
310
+ </template>
311
+ ```
312
+
313
+ ```vue
314
+ <!-- components/examples/HeavyPosts.client.vue -->
315
+ <script setup lang="ts">
316
+ const { data: posts, status } = useFetch(
317
+ "https://jsonplaceholder.typicode.com/posts",
318
+ {
319
+ key: "posts-client",
320
+ query: { _limit: 6 },
321
+ server: false,
322
+ lazy: true,
323
+ },
324
+ );
325
+ </script>
326
+ ```
327
+
328
+ In this pattern the request starts only after the application renders the lazy client component, and the returned posts are therefore absent from View Source. Combine the component with an explicit user action, visibility condition, or another application-specific trigger when its download should be deferred further.
329
+
330
+ #### Streaming compatibility requirements
331
+
332
+ - The server and the hydrating client must initially select the same elements, attributes, IDs, and text. Do not derive initial markup independently from `Date.now()`, `new Date()`, `Math.random()`, browser timezone, viewport size, or browser-only storage.
333
+ - Use `useState`, `useFetch`, or `useAsyncData` to serialize request-specific SSR state. Use Vue's `useId()` for stable component IDs. Browser APIs belong in `onMounted` or `.client.vue` components.
334
+ - HTTP status, redirects, response headers, and cookie writes must be decided before the first response chunk is committed. If the result of an awaited route/component query determines those values, use buffered SSR for that route:
335
+
336
+ ```ts
337
+ export default defineNuxtConfig({
338
+ routeRules: {
339
+ "/account/**": { streaming: false },
340
+ "/products/**": { streaming: false }, // dynamic 404/redirect after lookup
341
+ },
342
+ });
343
+ ```
344
+
345
+ - Keep above-the-fold paint-critical styles in global CSS. Deeply nested async components with their own scoped styles can briefly render before their late stylesheet chunk arrives.
346
+ - Test a production `build` + `server`, not only `nuxt dev`. Verify the response status and headers, inspect View Source after the stream completes, and check the browser console for hydration warnings.
347
+
348
+ `UiScheduledContent` follows these rules. It computes the initial active/upcoming/hidden branch on the server, stores that exact branch in the Nuxt payload with an instance-specific `useId()` key, and keeps using it until mount. The live browser clock starts only after hydration, preventing streamed HTML from selecting a different slot because of timing or server/browser timezone differences.
349
+
350
+ ### Static generation
351
+
352
+ ```bash
353
+ yarn generate
354
+ ```
355
+
356
+ Static mode disables runtime image transformation and payload extraction, crawls links, and includes an Apache `.htaccess` with clean HTML routing, compression, security headers, and cache rules.
357
+
358
+ ### High-load topology
359
+
360
+ The repository provides an application-level baseline, not a complete distributed architecture. For high traffic, place the app behind a CDN/reverse proxy, terminate TLS there, cache immutable assets and suitable HTML/API responses, run multiple stateless app instances, centralize logs/metrics, and use external durable storage for shared state.
361
+
362
+ The configured rate limiter uses an in-memory LRU driver. Its quota is local to each process, worker, or replica; use a shared driver or enforce global limits at the gateway before treating it as a distributed abuse-control mechanism.
363
+
364
+ Health checks are available at:
365
+
366
+ - `GET /healthz` → `{ "status": "ok" }`;
367
+ - `GET /api/healthz` → `ok`.
368
+
369
+ ## Markdown blog posts
370
+
371
+ The layer includes an optional server-rendered Markdown blog. Add application posts to the root `content/` directory. The layer can provide default posts from `app/content/`; an application post with the same relative path overrides the layer post.
372
+
373
+ ### Create a post
374
+
375
+ The file path determines the URL recursively:
376
+
377
+ | File | URL |
378
+ | ------------------------------ | ------------------------- |
379
+ | `content/hello-world.md` | `/post/hello-world` |
380
+ | `content/guides/deployment.md` | `/post/guides/deployment` |
381
+
382
+ For example:
383
+
384
+ ```md
385
+ ---
386
+ title: "Hello world"
387
+ description: "A short description used by search engines and social previews."
388
+ image: "/images/posts/hello-world.jpg"
389
+ ---
390
+
391
+ # Hello world
392
+
393
+ This is a **Markdown** post.
394
+ ```
395
+
396
+ `title`, `description`, and `image` are optional string frontmatter fields. They populate the page title, meta description, and Open Graph image. Raw HTML in Markdown is disabled; plain URLs are linkified and typographic substitutions are enabled.
397
+
398
+ Posts are rendered on the server. The resulting post fragment is safely minified before it is inserted into the page source: structural whitespace is removed while significant inline whitespace and the contents of `<pre>` blocks are preserved.
399
+
400
+ Adding, removing, or renaming a post requires a rebuild. During a build, post files are discovered once, nested directories are scanned recursively, and the result is reused by prerender and sitemap generation. Markdown modules are loaded lazily and each parsed post is cached for the lifetime of the server process.
401
+
402
+ ### Configure the blog
403
+
404
+ ```ts
405
+ export default defineNuxtConfig({
406
+ myelophone: {
407
+ siteDomain: "https://example.com",
408
+ blog: {
409
+ blogEnabled: true,
410
+ postsLayout: "default-blog",
411
+ },
412
+ },
413
+ });
414
+ ```
415
+
416
+ - `blogEnabled: false` removes the catch-all post page and excludes posts from prerendering and the sitemap. Set it before building because changing it after deployment cannot recreate a route omitted from the build.
417
+ - `postsLayout` selects the Nuxt layout used for every post.
418
+ - `siteDomain` supplies the absolute sitemap origin. When it is not configured, the sitemap uses the current request origin.
419
+
420
+ Every discovered post is added to `/sitemap.xml` and the Nitro prerender route list. A missing post returns a real HTTP 404 response. In a regular production build, the initial payload is embedded in the HTML while extracted `_payload.json` files remain available for client navigation without an unused preload. `NUXT_STATIC=true` disables payload extraction and prerenders the post pages as static HTML.
421
+
422
+ ## Styling and themes
423
+
424
+ Global styles are loaded by the base `app.vue`. Tailwind scans Vue files across the layer and consumer project. The theme is driven by `data-theme="light|dark"` and these CSS variables:
425
+
426
+ ```css
427
+ :root {
428
+ --ui-bg: #f7f7f7;
429
+ --ui-text: #1a1a1b;
430
+ --ui-border: #374151;
431
+ --loader-fill-color: #0082e6;
432
+ }
433
+ ```
434
+
435
+ Use the settings store to switch or synchronize themes:
436
+
437
+ ```vue
438
+ <script setup lang="ts">
439
+ const settings = useSettingsStore();
440
+ </script>
441
+
442
+ <template>
443
+ <UiButton label="Toggle theme" @click="settings.toggleTheme()" />
444
+ </template>
445
+ ```
446
+
447
+ The choice is stored locally, synchronized across tabs, reflected in the browser theme color, and initialized before paint to reduce theme flashing.
448
+
449
+ ## Ready page components
450
+
451
+ A ready page component is a complete, reusable page composition built from the layer's grid, UI, view, media, and content components. It owns its default content, markup, and visual preset, so a Nuxt page can render a finished page with a single component:
452
+
453
+ ```vue
454
+ <template>
455
+ <ReadyProductPage />
456
+ </template>
457
+ ```
458
+
459
+ Keep ready page components in `app/components/ready/`. Nuxt derives the component name from the directory and filename: `components/ready/ProductPage.vue` becomes `<ReadyProductPage />`.
460
+
461
+ Ready components should contain only page-specific concerns:
462
+
463
+ - the content contract and other page-specific TypeScript interfaces;
464
+ - complete default content for every supported locale;
465
+ - the composition of existing components;
466
+ - named slots that are useful for exceptional site-specific replacements;
467
+ - the page's default CSS variables and the selectors that consume them.
468
+
469
+ Reusable behavior belongs to the layer rather than an individual ready component:
470
+
471
+ | API | Responsibility |
472
+ | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
473
+ | `useReadyPage()` | Resolves configured locales, applies fallbacks and overrides, stays reactive, and updates title, description, Open Graph title, and Open Graph description for the active language. |
474
+ | `mergeReadyContent()` | Deeply merges objects without mutating the defaults. Arrays are replaced as complete collections. |
475
+ | `readyCssVariables()` | Converts a typed appearance object into inheritable `--ready-*` CSS variables. |
476
+ | `PageReadyLocalized` | Renders the resolved language variant through `UiLangVisible`. |
477
+
478
+ Marketing and editorial content belongs in the ready component, not in locale JSON files. Keep JSON namespaces for shared interface or service strings used by independent components, such as email, telephone, consent, navigation controls, validation messages, and common actions.
479
+
480
+ ### Minimal ready component
481
+
482
+ The following is a complete minimal `app/components/ready/ProductPage.vue`. Its specific types stay in the SFC and can be imported by a consuming page when type annotations are useful.
483
+
484
+ ```vue
485
+ <script lang="ts">
486
+ import type {
487
+ DeepPartial,
488
+ ReadyContentByLocale,
489
+ } from "../../composables/useReadyPage";
490
+
491
+ export interface ProductPageContent {
492
+ seo: {
493
+ title: string;
494
+ description: string;
495
+ ogTitle?: string;
496
+ ogDescription?: string;
497
+ };
498
+ brand: string;
499
+ hero: {
500
+ title: string;
501
+ description: string;
502
+ actionLabel: string;
503
+ actionTo: string;
504
+ };
505
+ }
506
+
507
+ export interface ProductPageAppearance {
508
+ background: string;
509
+ text: string;
510
+ primary: string;
511
+ primaryContrast: string;
512
+ buttonRadius: string;
513
+ }
514
+
515
+ export type ProductPageContentOverride = DeepPartial<ProductPageContent>;
516
+ export type ProductPageContentByLocale =
517
+ ReadyContentByLocale<ProductPageContent>;
518
+ export type ProductPageAppearanceOverride = DeepPartial<ProductPageAppearance>;
519
+ </script>
520
+
521
+ <script setup lang="ts">
522
+ import {
523
+ mergeReadyContent,
524
+ readyCssVariables,
525
+ useReadyPage,
526
+ } from "../../composables/useReadyPage";
527
+
528
+ defineOptions({ inheritAttrs: false });
529
+
530
+ const props = withDefaults(
531
+ defineProps<{
532
+ content?: ProductPageContentOverride;
533
+ contentByLocale?: ProductPageContentByLocale;
534
+ appearance?: ProductPageAppearanceOverride;
535
+ }>(),
536
+ {
537
+ content: () => ({}),
538
+ contentByLocale: () => ({}),
539
+ appearance: () => ({}),
540
+ },
541
+ );
542
+
543
+ const english: ProductPageContent = {
544
+ seo: {
545
+ title: "Acme — a clearer way to work",
546
+ description: "A short description of the complete product page.",
547
+ },
548
+ brand: "Acme",
549
+ hero: {
550
+ title: "A complete page from one component",
551
+ description: "The component provides useful content and layout immediately.",
552
+ actionLabel: "Get started",
553
+ actionTo: "/contact",
554
+ },
555
+ };
556
+
557
+ const defaultContentByLocale: Record<string, ProductPageContent> = {
558
+ en: english,
559
+ pl: mergeReadyContent(english, {
560
+ seo: {
561
+ title: "Acme — prostszy sposób pracy",
562
+ description: "Krótki opis kompletnej strony produktu.",
563
+ },
564
+ hero: {
565
+ title: "Kompletna strona z jednego komponentu",
566
+ description: "Komponent od razu udostępnia treść i układ.",
567
+ actionLabel: "Zacznij",
568
+ },
569
+ }),
570
+ };
571
+
572
+ const defaultAppearance: ProductPageAppearance = {
573
+ background: "var(--ui-bg)",
574
+ text: "var(--ui-text)",
575
+ primary: "#526dff",
576
+ primaryContrast: "#ffffff",
577
+ buttonRadius: "0.75rem",
578
+ };
579
+
580
+ const appearance = computed(() =>
581
+ mergeReadyContent(defaultAppearance, props.appearance),
582
+ );
583
+ const rootStyle = computed(() => readyCssVariables(appearance.value));
584
+
585
+ const { resolvedContentByLocale } = useReadyPage({
586
+ contentByLocale: defaultContentByLocale,
587
+ content: () => props.content,
588
+ overridesByLocale: () => props.contentByLocale,
589
+ fallbackLocale: "en",
590
+ });
591
+ </script>
592
+
593
+ <template>
594
+ <PageReadyLocalized
595
+ v-slot="{ content: page }"
596
+ :content-by-locale="resolvedContentByLocale"
597
+ >
598
+ <section
599
+ v-bind="$attrs"
600
+ :style="[rootStyle, $attrs.style]"
601
+ class="ready-product-page"
602
+ >
603
+ <GridContainer size="boxed" class="py-20">
604
+ <p>{{ page.brand }}</p>
605
+ <UiHeading :level="1">{{ page.hero.title }}</UiHeading>
606
+ <p>{{ page.hero.description }}</p>
607
+ <UiButton
608
+ as="nuxt-link"
609
+ :to="page.hero.actionTo"
610
+ :label="page.hero.actionLabel"
611
+ class="ready-product-page__button"
612
+ />
613
+ </GridContainer>
614
+ </section>
615
+ </PageReadyLocalized>
616
+ </template>
617
+
618
+ <style scoped>
619
+ .ready-product-page {
620
+ background: var(--ready-background);
621
+ color: var(--ready-text);
622
+ min-height: 100dvh;
623
+ }
624
+
625
+ .ready-product-page :deep(.ready-product-page__button) {
626
+ background: var(--ready-primary);
627
+ border-color: var(--ready-primary);
628
+ border-radius: var(--ready-button-radius);
629
+ color: var(--ready-primary-contrast);
630
+ }
631
+ </style>
632
+ ```
633
+
634
+ `useReadyPage()` reads the active language used by the built-in multi-language system. `PageReadyLocalized` delegates visible content to `UiLangVisible`; ready components should not manually inspect the route or duplicate locale-selection logic.
635
+
636
+ Each locale entry must describe a complete usable page after merging with its fallback. The example creates Polish content by deeply overriding the English fallback. Because arrays represent ordered page collections, `mergeReadyContent()` replaces an overridden array instead of merging items by index.
637
+
638
+ ### Use and customize a ready page
639
+
640
+ No props are required when the defaults are suitable. A concrete site can change any nested value without editing or copying the ready component:
641
+
642
+ ```vue
643
+ <script setup lang="ts">
644
+ import type { ProductPageContentByLocale } from "~/components/ready/ProductPage.vue";
645
+
646
+ const contentByLocale: ProductPageContentByLocale = {
647
+ en: {
648
+ brand: "Example Cloud",
649
+ hero: {
650
+ title: "Infrastructure without busywork",
651
+ },
652
+ },
653
+ pl: {
654
+ brand: "Example Cloud",
655
+ hero: {
656
+ title: "Infrastruktura bez zbędnej pracy",
657
+ },
658
+ },
659
+ };
660
+ </script>
661
+
662
+ <template>
663
+ <ReadyProductPage
664
+ :content="{
665
+ hero: {
666
+ actionTo: 'mailto:hello@example.com',
667
+ },
668
+ }"
669
+ :content-by-locale="contentByLocale"
670
+ :appearance="{
671
+ primary: '#7c3aed',
672
+ buttonRadius: '999px',
673
+ }"
674
+ class="site-product-page"
675
+ />
676
+ </template>
677
+ ```
678
+
679
+ Overrides are applied in this order:
680
+
681
+ 1. the ready component's fallback content;
682
+ 2. the ready component's content for the active language;
683
+ 3. common site overrides from `content`;
684
+ 4. active-language site overrides from `contentByLocale`.
685
+
686
+ Use `content` for values shared by every language, such as URLs, email addresses, feature flags, IDs, and asset paths. Use `contentByLocale` for language-dependent editorial content. Both objects are reactive, so replacing either prop updates the rendered page and its SEO metadata.
687
+
688
+ The `appearance` prop exposes the variables intentionally supported by that ready component. Normal root attributes are also forwarded. This permits site-specific classes, `data-*`, `aria-*`, and additional or emergency CSS-variable overrides:
689
+
690
+ ```vue
691
+ <ReadyProductPage
692
+ data-campaign="autumn"
693
+ style="--ready-primary: #e11d48; --ready-button-radius: 0.4rem"
694
+ />
695
+ ```
696
+
697
+ Define every documented appearance variable on the ready component's root and consume it inside the component with `var(--ready-...)`. CSS custom properties inherit through nested Vue components, while `:deep()` lets scoped ready styles target the roots of reused UI components. Prefer `appearance` for the stable public styling API and direct `style` variables for exceptional additions.
698
+
699
+ ### Ready component checklist
700
+
701
+ When adding another complete page or large ready page fragment:
702
+
703
+ 1. Create a descriptive file under `app/components/ready/`.
704
+ 2. Keep its content and appearance interfaces in that `.vue` file; do not add ready-specific contracts to the global `app/types/` directory.
705
+ 3. Provide a complete fallback locale and all useful built-in locale variants inside the component.
706
+ 4. Include `seo.title` and `seo.description`; add localized `ogTitle` and `ogDescription` only when they should differ.
707
+ 5. Accept `content`, `contentByLocale`, and `appearance` as deep partial overrides.
708
+ 6. Call `useReadyPage()` instead of implementing merging, locale selection, fallback, or SEO again.
709
+ 7. Render the result through `PageReadyLocalized` so visibility continues to use `UiLangVisible`.
710
+ 8. Put visual defaults in a typed appearance object, convert it with `readyCssVariables()`, and bind the result to the root element.
711
+ 9. Forward `$attrs` and merge `$attrs.style` after the default root variables.
712
+ 10. Add named slots only where arbitrary site markup is genuinely useful; prefer typed content overrides for normal changes.
713
+ 11. Add a separate route under `playground/pages/` for development. Do not replace framework or application pages merely to demonstrate the component.
714
+ 12. Run type checking and a production build before considering the ready component complete.
715
+
716
+ ## Reveal animations
717
+
718
+ `v-reveal` uses IntersectionObserver and automatically disables motion for bots, slow connections, low battery, old browsers, and `prefers-reduced-motion` users.
719
+
720
+ ```vue
721
+ <section v-reveal>Default slide reveal</section>
722
+ <section v-reveal:fade.fast>Fast fade</section>
723
+ <section
724
+ v-reveal:zoom.slow.repeat="150"
725
+ >Repeated zoom; 150 ms queue step</section>
726
+ ```
727
+
728
+ Supported style arguments include `slide`, `fade`, and `zoom`; modifiers are `fast`, `slow`, and `repeat`.
729
+
730
+ ## Internationalization
731
+
732
+ The built-in `multi18n` module provides localized routes, lazy namespace loading, interpolation, pluralization, compact numbers, locale-aware links and visibility, language switching, fallback translations, SEO integration, and build-time removal of unused keys. No separate i18n module is required.
733
+
734
+ ### Configuration
735
+
736
+ Configure the supported languages and the unprefixed default language:
737
+
738
+ ```ts
739
+ export default defineNuxtConfig({
740
+ multi18n: {
741
+ defaultLocale: "en",
742
+ locales: ["en", "pl", "ru"],
743
+ },
744
+ });
745
+ ```
746
+
747
+ `defaultLocale` defaults to `en`; `locales` defaults to `['en']`. Duplicate locale values are removed in public runtime configuration. Use lowercase two-letter locale codes such as `en`, `pl`, and `ru` for consistent route and SEO handling.
748
+
749
+ ### Localized routes
750
+
751
+ Every regular Nuxt page receives a route for each non-default locale:
752
+
753
+ | Source page | English, the default locale | Polish | Russian |
754
+ | ------------------ | --------------------------- | -------------------- | -------------------- |
755
+ | `/` | `/` | `/pl` | `/ru` |
756
+ | `/about` | `/about` | `/pl/about` | `/ru/about` |
757
+ | `/products/[slug]` | `/products/:slug` | `/pl/products/:slug` | `/ru/products/:slug` |
758
+
759
+ The default language never needs a URL prefix. A prefixed default-language URL such as `/en/about` is permanently redirected to `/about`.
760
+
761
+ Exclude a page from route localization when it must have only one URL:
762
+
763
+ ```vue
764
+ <script setup lang="ts">
765
+ definePageMeta({
766
+ i18n: false,
767
+ });
768
+ </script>
769
+ ```
770
+
771
+ Catch-all pages such as `[...all].vue` are not duplicated. An explicitly defined localized path is also preserved instead of being generated a second time.
772
+
773
+ The active locale is resolved from the first URL segment during SSR and updated after client navigation. The framework synchronizes `settingsStore.currentLocale` and the document's `<html lang>` attribute with that route.
774
+
775
+ ### Translation files and namespaces
776
+
777
+ Translation files use namespace paths:
778
+
779
+ ```text
780
+ app/locales/en/common.json
781
+ app/locales/pl/common.json
782
+ app/locales/en/products.json
783
+ app/locales/pl/products.json
784
+ ```
785
+
786
+ The filename is the namespace: `products.json` is loaded with `useMultiLang('products')`, and its keys are addressed as `products.*`. Namespace paths can also be nested, for example `locales/en/account/profile.json` → `account/profile.*`.
787
+
788
+ Translation files are discovered in both the framework and application locale directories. Keep application-specific translations in their own namespaces. Inside a loaded JSON file, nested objects and dotted keys are expanded and can be mixed:
789
+
790
+ ```json
791
+ {
792
+ "title": "Products",
793
+ "hello": "Hello, {name}!",
794
+ "filters.empty": "No matching products",
795
+ "cart": {
796
+ "title": "Your cart"
797
+ },
798
+ "items_zero": "No items",
799
+ "items_one": "{count} item",
800
+ "items_other": "{count} items"
801
+ }
802
+ ```
803
+
804
+ ### Loading and translating namespaces
805
+
806
+ ```vue
807
+ <script setup lang="ts">
808
+ const { t, tn, loadPromise, refresh } = useMultiLang(["products"]);
809
+
810
+ // `loadPromise` is the reactive pending state returned by useAsyncData.
811
+ // Call refresh() when translation files must be reloaded explicitly.
812
+ </script>
813
+
814
+ <template>
815
+ <div :aria-busy="loadPromise">
816
+ <h1>{{ t("products.title") }}</h1>
817
+ <p>{{ t("products.hello", { name: "Ada" }) }}</p>
818
+ <p>{{ t("products.items", 0) }}</p>
819
+ <p>{{ t("products.items", 12) }}</p>
820
+ <p>{{ tn("products.items", 12500) }}</p>
821
+ </div>
822
+ </template>
823
+ ```
824
+
825
+ `useMultiLang` accepts one namespace or an array and returns:
826
+
827
+ | Member | Purpose |
828
+ | ----------------------------- | ------------------------------------------------------------ |
829
+ | `t(key)` | Translate a string |
830
+ | `t(key, params)` | Replace `{placeholder}` values |
831
+ | `t(key, count)` | Select a plural form and format `{count}` |
832
+ | `t(key, params, count)` | Combine interpolation and pluralization |
833
+ | `t(key, params, count, rule)` | Force a plural suffix such as `few` or `many` |
834
+ | `tn(key, count, params?)` | Use compact number formatting such as `12K` for large counts |
835
+ | `loadPromise` | Reactive namespace-loading state |
836
+ | `refresh()` | Reload the selected namespaces through Nuxt async data |
837
+
838
+ Plural keys use the suffixes returned by `Intl.PluralRules`, such as `_one`, `_few`, `_many`, and `_other`. For zero, `_zero` is checked first. `_other` is the final plural fallback.
839
+
840
+ ```ts
841
+ const { t, tn } = useMultiLang("products");
842
+
843
+ t("products.hello", { name: "Ada" });
844
+ t("products.items", 0); // products.items_zero
845
+ t("products.items", { category: "books" }, 3);
846
+ t("products.items", {}, 3, "few"); // explicitly use products.items_few
847
+ tn("products.items", 12_500); // compact, locale-aware {count}
848
+ ```
849
+
850
+ Missing active-locale translations fall back to the default locale when that namespace is available, and unresolved values return the full translation key. Interpolated numbers use locale-aware `Intl.NumberFormat` formatting.
851
+
852
+ Namespaces are loaded lazily with `import.meta.glob`, cached in shared Nuxt state, loaded again when the active locale changes, and SSR-serialized for hydration. Set `myelophone.bundleTranslations: false` to group translation assets into separate per-locale chunks; keep it `true` to use the normal bundle grouping.
853
+
854
+ ### Dynamic keys and tree shaking
855
+
856
+ Production builds scan `.vue`, `.ts`, `.js`, and `.mjs` sources for literal keys used by `t()`, `tn()`, and `$t()`. Unused JSON entries are removed, while all plural variants belonging to a used base key are retained.
857
+
858
+ Dynamic translation keys cannot always be found statically. Preserve them with either method:
859
+
860
+ ```ts
861
+ // @i18n-keep products.dynamic_title
862
+
863
+ useSafeList(
864
+ {
865
+ titleKey: "products.dynamic_title",
866
+ emptyStateKeys: ["products.empty.search", "products.empty.category"],
867
+ sections: [{ headingKey: "products.sections.featured" }],
868
+ },
869
+ { safelistPath: "i18n-safelist.json" },
870
+ );
871
+ ```
872
+
873
+ `useSafeList` recursively collects:
874
+
875
+ - direct translation-key strings;
876
+ - object properties whose names end in `Key`, except `loadKey`;
877
+ - arrays stored in properties whose names end in `Keys`;
878
+ - matching values inside nested arrays and objects.
879
+
880
+ It is intended for server/build-time code because it uses the Node filesystem. Without options it writes sorted keys to `.nuxt/i18n-safelist.generated.json`. To feed collected keys directly into the configured tree-shaker, set `safelistPath: 'i18n-safelist.json'` as above. The same root file can also be maintained manually:
881
+
882
+ ```json
883
+ ["products.dynamic_title", "products.empty.search", "products.empty.category"]
884
+ ```
885
+
886
+ Literal calls, `@i18n-keep` comments, the configured root safelist, and keys registered by built-in framework configuration are combined during the build.
887
+
888
+ ### Locale-aware navigation and content
889
+
890
+ Language-aware components:
891
+
892
+ ```vue
893
+ <UiLanguageSelect show-full />
894
+ <UiLangLink to="/pricing">Pricing</UiLangLink>
895
+ <UiLangVisible only="pl">Polish-only content</UiLangVisible>
896
+ <UiLangVisible :except="['pl', 'ru']">All other languages</UiLangVisible>
897
+ ```
898
+
899
+ `UiLanguageSelect` lists configured locales, keeps the current path, query, and hash when switching, removes the prefix for the default locale, and automatically opens upward or right-aligned when viewport space is limited. Use its slots and class props for a custom presentation:
900
+
901
+ ```vue
902
+ <UiLanguageSelect
903
+ show-full
904
+ container-class="relative"
905
+ trigger-class="rounded-lg px-4 py-2"
906
+ dropdown-class="rounded-lg"
907
+ option-class="text-sm"
908
+ >
909
+ <template #trigger>
910
+ <span class="uppercase">Choose language</span>
911
+ </template>
912
+ </UiLanguageSelect>
913
+ ```
914
+
915
+ Full language names use keys such as `common.en`, `common.pl`, and `common.ru`. Add them to every configured locale's `common.json` when `show-full` is enabled.
916
+
917
+ `UiLangLink` accepts `to`, `exact`, and `replace`. It prefixes internal paths only when the active locale is not the default and avoids adding the same prefix twice:
918
+
919
+ ```vue
920
+ <UiLangLink to="/products">Products</UiLangLink>
921
+ <UiLangLink to="/checkout" replace>Checkout</UiLangLink>
922
+ ```
923
+
924
+ `UiLangVisible` accepts a locale string or array through either `only` or `except`. If both are omitted, its slot is always rendered.
925
+
926
+ The settings store can also drive language changes directly:
927
+
928
+ ```vue
929
+ <script setup lang="ts">
930
+ const settings = useSettingsStore();
931
+
932
+ const selectPolish = () => settings.setLocale("pl");
933
+ </script>
934
+
935
+ <template>
936
+ <UiButton @click="selectPolish">Polski</UiButton>
937
+ </template>
938
+ ```
939
+
940
+ `setLocale()` ignores unsupported locale values and navigates to the equivalent localized route while retaining the current query and hash.
941
+
942
+ ### i18n and SEO
943
+
944
+ For localized pages, the application shell updates `<html lang>`, creates alternate `hreflang` links and an `x-default` link, and keeps localized breadcrumb paths and labels in sync. Set `myelophone.siteDomain` to the canonical production origin so SSR can produce absolute URLs.
945
+
946
+ ## SEO and URL behavior
947
+
948
+ ### Page metadata
949
+
950
+ ```vue
951
+ <script setup lang="ts">
952
+ const product = await fetchProduct();
953
+
954
+ useAppSeo({
955
+ title: () => product.name,
956
+ descriptionKey: "products.seo_description",
957
+ params: { name: computed(() => product.name) },
958
+ noIndex: !product.isPublished,
959
+ });
960
+ </script>
961
+ ```
962
+
963
+ `useAppSeo` sets title, Open Graph title, description, Open Graph description, robots, and the current breadcrumb title. A static `titleKey` can be used instead of a value/function.
964
+
965
+ Other built-in SEO behavior:
966
+
967
+ - canonical URLs preserve only the `page` query parameter;
968
+ - URL paths are normalized to lowercase, duplicate/trailing slashes are removed, and production traffic is redirected away from `www` and HTTP;
969
+ - alternate-language and `x-default` links are generated for localized pages;
970
+ - breadcrumb JSON-LD is generated from route metadata and translation keys;
971
+ - `UiAccordion faq` emits FAQPage JSON-LD;
972
+ - `SeoProfileInfo`, `SeoOrgInfo`, and `SeoBrandInfo` emit reactive SSR JSON-LD records;
973
+ - `SeoNoIndex` marks 4xx pages as `noindex, nofollow`;
974
+ - `SeoContentNoIndex` wraps content in `data-nosnippet` and renders it client-side;
975
+ - `nuxt-og-image`, robots, link checking, and SEO utilities are included.
976
+
977
+ ```vue
978
+ <ViewBreadcrumbs :max-chars="40" separator="›" />
979
+
980
+ <SeoContentNoIndex fallback-height="160px">
981
+ Private or volatile text
982
+ <template #placeholder><span aria-hidden="true" /></template>
983
+ </SeoContentNoIndex>
984
+ ```
985
+
986
+ Set `myelophone.siteDomain` in production. Without it, server-rendered absolute alternate and breadcrumb URLs do not have a reliable origin.
987
+
988
+ ### Profile, organization, and brand JSON-LD
989
+
990
+ `SeoProfileInfo`, `SeoOrgInfo`, and `SeoBrandInfo` are head-only components: they render no visible element and add an SSR-accessible `<script type="application/ld+json">` record to `<head>`. Values remain reactive during client navigation. JSON is escaped so supplied text cannot prematurely close the script element.
991
+
992
+ Use `SeoProfileInfo` only when the page primarily describes one person or organization. Google requires `mainEntity` and a `name` or `alternateName` on that entity for ProfilePage eligibility. The shorthand entity props cover Google's common fields; `entity` accepts every additional `Person` or `Organization` property:
993
+
994
+ ```vue
995
+ <SeoProfileInfo
996
+ id="https://example.com/team/ada#profile-page"
997
+ url="https://example.com/team/ada"
998
+ name="Ada Lovelace — profile"
999
+ date-created="2024-01-10T09:00:00Z"
1000
+ date-modified="2026-08-16T12:00:00Z"
1001
+ entity-id="https://example.com/team/ada#person"
1002
+ entity-name="Ada Lovelace"
1003
+ alternate-name="@ada"
1004
+ entity-description="Lead platform engineer"
1005
+ entity-image="https://example.com/images/ada.webp"
1006
+ :same-as="['https://github.com/ada', 'https://www.linkedin.com/in/ada']"
1007
+ :interaction-statistic="[
1008
+ {
1009
+ '@type': 'InteractionCounter',
1010
+ interactionType: 'https://schema.org/FollowAction',
1011
+ userInteractionCount: 1250,
1012
+ },
1013
+ ]"
1014
+ :entity="{
1015
+ jobTitle: 'Lead platform engineer',
1016
+ worksFor: { '@id': 'https://example.com/#organization' },
1017
+ knowsAbout: ['Nuxt', 'Accessibility'],
1018
+ }"
1019
+ :data="{
1020
+ isPartOf: { '@id': 'https://example.com/#website' },
1021
+ }"
1022
+ />
1023
+ ```
1024
+
1025
+ If a complete entity object already exists, pass it as `main-entity`; it replaces the generated entity:
1026
+
1027
+ ```vue
1028
+ <SeoProfileInfo
1029
+ :main-entity="{
1030
+ '@type': 'Organization',
1031
+ '@id': 'https://example.com/#organization',
1032
+ name: 'Example Studio',
1033
+ url: 'https://example.com/',
1034
+ }"
1035
+ />
1036
+ ```
1037
+
1038
+ For a single JSON-LD script with an `@graph`, set `entity-only` to make the profile's root node a `Person` or `Organization`, then pass every related node through `graph`. This covers graph structures containing a person, company, nested brand, WebSite, FAQPage, and any other Schema.org records:
1039
+
1040
+ ```vue
1041
+ <script setup lang="ts">
1042
+ const personId = "https://aleksivanov.me/#person";
1043
+ const organizationId = "https://myeloph.one/#organization";
1044
+
1045
+ const relatedGraph = [
1046
+ {
1047
+ "@type": "Organization",
1048
+ "@id": organizationId,
1049
+ name: "MyelophOne",
1050
+ url: "https://myeloph.one/",
1051
+ legalName: "Aliaksandr Ivanou",
1052
+ founder: { "@id": personId },
1053
+ brand: {
1054
+ "@type": "Brand",
1055
+ name: "MyelophOne",
1056
+ },
1057
+ },
1058
+ {
1059
+ "@type": "WebSite",
1060
+ "@id": "https://aleksivanov.me/#website",
1061
+ name: "Aleks Ivanou",
1062
+ url: "https://aleksivanov.me/",
1063
+ publisher: { "@id": personId },
1064
+ },
1065
+ {
1066
+ "@type": "FAQPage",
1067
+ "@id": "https://aleksivanov.me/#faq",
1068
+ mainEntity: [
1069
+ {
1070
+ "@type": "Question",
1071
+ name: "Who is Aleks Ivanou?",
1072
+ acceptedAnswer: {
1073
+ "@type": "Answer",
1074
+ text: "A full-stack developer and founder of MyelophOne.",
1075
+ },
1076
+ },
1077
+ ],
1078
+ },
1079
+ ];
1080
+ </script>
1081
+
1082
+ <template>
1083
+ <SeoProfileInfo
1084
+ entity-only
1085
+ :id="personId"
1086
+ entity-type="Person"
1087
+ name="Aliaksandr Ivanou"
1088
+ :alternate-name="['Aleks Ivanou', 'Alex Ivanou', '@aleksivanou']"
1089
+ url="https://aleksivanov.me/"
1090
+ image="https://aleksivanov.me/assets/img/portrait.jpg"
1091
+ job-title="Full-stack developer"
1092
+ email="mailto:aleks@example.com"
1093
+ description="Full-stack developer and founder of MyelophOne."
1094
+ :same-as="['https://github.com/aleksivanou']"
1095
+ :owns="{ '@id': organizationId }"
1096
+ :knows-about="['Nuxt.js', 'Vue.js', 'TypeScript', 'Go']"
1097
+ :graph="relatedGraph"
1098
+ />
1099
+ </template>
1100
+ ```
1101
+
1102
+ The result is one script shaped as `{ "@context": "https://schema.org", "@graph": [...] }`. The generated profile/entity is the first graph node, followed by the supplied nodes. The `graph` prop is also available on `SeoOrgInfo` and `SeoBrandInfo`, so any of the three record types can be the primary node. When `graph` is omitted, the component continues to emit its normal standalone JSON-LD record.
1103
+
1104
+ Use `SeoOrgInfo` once on the home page or a page that describes the organization. Prefer the most specific applicable Schema.org subtype through `type`, such as `Corporation`, `OnlineStore`, or a suitable `LocalBusiness` subtype:
1105
+
1106
+ ```vue
1107
+ <SeoOrgInfo
1108
+ type="Corporation"
1109
+ id="https://example.com/#organization"
1110
+ name="Example Studio"
1111
+ alternate-name="Example"
1112
+ legal-name="Example Studio sp. z o.o."
1113
+ url="https://example.com/"
1114
+ description="Product design and engineering studio"
1115
+ telephone="+48-12-345-67-89"
1116
+ email="hello@example.com"
1117
+ :logo="{
1118
+ '@type': 'ImageObject',
1119
+ url: 'https://example.com/logo.png',
1120
+ width: 512,
1121
+ height: 512,
1122
+ }"
1123
+ :address="{
1124
+ '@type': 'PostalAddress',
1125
+ streetAddress: '1 Example Street',
1126
+ addressLocality: 'Warsaw',
1127
+ postalCode: '00-001',
1128
+ addressCountry: 'PL',
1129
+ }"
1130
+ :contact-point="{
1131
+ '@type': 'ContactPoint',
1132
+ contactType: 'customer support',
1133
+ telephone: '+48-12-345-67-89',
1134
+ availableLanguage: ['en', 'pl'],
1135
+ }"
1136
+ :same-as="[
1137
+ 'https://github.com/example',
1138
+ 'https://www.linkedin.com/company/example',
1139
+ ]"
1140
+ :data="{
1141
+ foundingDate: '2020-01-01',
1142
+ knowsLanguage: ['en', 'pl'],
1143
+ }"
1144
+ />
1145
+ ```
1146
+
1147
+ `SeoOrgInfo` has direct props for identity, names, URLs, logo/images, contacts, addresses, founders, employees, organization relationships, brands, service areas, ratings/reviews, offers, merchant policies, shipping services, awards, expertise, and common company identifiers (`taxID`, `vatID`, `duns`, `globalLocationNumber`, `iso6523Code`, `leiCode`, and `naics`).
1148
+
1149
+ `SeoBrandInfo` supports the complete Brand-specific set plus the commonly useful inherited Thing properties:
1150
+
1151
+ ```vue
1152
+ <SeoBrandInfo
1153
+ id="https://example.com/#brand"
1154
+ name="Example"
1155
+ alternate-name="Example Labs"
1156
+ url="https://example.com/"
1157
+ logo="https://example.com/brand.svg"
1158
+ slogan="Build clearly"
1159
+ :same-as="['https://www.wikidata.org/wiki/Q123']"
1160
+ :owner="{ '@id': 'https://example.com/#organization' }"
1161
+ :aggregate-rating="{
1162
+ '@type': 'AggregateRating',
1163
+ ratingValue: 4.9,
1164
+ reviewCount: 86,
1165
+ }"
1166
+ />
1167
+ ```
1168
+
1169
+ Common control props:
1170
+
1171
+ | Prop | Default | Purpose |
1172
+ | ----------- | ---------------------- | ----------------------------------------------------------------------------------- |
1173
+ | `enabled` | `true` | Reactively add or remove this JSON-LD script |
1174
+ | `context` | `https://schema.org` | JSON-LD `@context` |
1175
+ | `type` | Component type | JSON-LD `@type`; accepts a string or multiple types |
1176
+ | `id` | — | Stable entity/page `@id`, normally an absolute URL with an optional fragment |
1177
+ | `data` | `{}` | Any additional or future Schema.org properties for the root record |
1178
+ | `graph` | — | Wrap the generated record and additional nodes in one root `@graph` script |
1179
+ | `scriptKey` | Generated per instance | Override Nuxt head deduplication key when the record needs a stable application key |
1180
+ | `scriptId` | — | Optional HTML id on the generated script element |
1181
+
1182
+ `data` is merged first; explicit component props override properties with the same name. `@context` and `@type` always come from their corresponding props. For `SeoProfileInfo`, `entity` is merged the same way for the generated `mainEntity`, while `mainEntity` supplies a fully custom entity and takes precedence over all shorthand entity fields. Set `entityOnly` when the root record itself must be the generated `Person` or `Organization`; in this mode the page-level `id`, `name`, `url`, `description`, and `image` props also act as fallbacks for their entity equivalents. This pass-through design supports all Schema.org properties, nested nodes, arrays, `@id` references, multi-typed records, and future vocabulary additions without waiting for a component update.
1183
+
1184
+ Always use absolute crawlable URLs, include only information represented by the visible page, and omit unknown data instead of inventing it. Validate deployed output with Google's Rich Results Test and the Schema.org validator; valid markup improves machine understanding but does not guarantee a rich result.
1185
+
1186
+ ## Cookie consent and third-party scripts
1187
+
1188
+ The application shell automatically renders the banner and settings modal. Preferences are stored for one year in `privacy-preferences`; necessary cookies are always enabled. A full decline is remembered for 30 days before the banner is shown again.
1189
+
1190
+ ### Configure presets
1191
+
1192
+ ```ts
1193
+ import {
1194
+ cookieScriptPresets,
1195
+ mergeCookieScriptConfigs,
1196
+ } from "#myelophone/app/constants/predefinedCookieScripts";
1197
+
1198
+ const cookieScripts = mergeCookieScriptConfigs(
1199
+ cookieScriptPresets.googleTagManager({ containerId: "GTM-XXXX" }),
1200
+ cookieScriptPresets.googleAnalytics4({ measurementId: "G-XXXX" }),
1201
+ cookieScriptPresets.microsoftClarity({ projectId: "xxxx" }),
1202
+ );
1203
+
1204
+ export default defineNuxtConfig({
1205
+ myelophone: {
1206
+ cookieScripts,
1207
+ },
1208
+ });
1209
+ ```
1210
+
1211
+ Available presets and required identifiers:
1212
+
1213
+ | Group | Presets |
1214
+ | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1215
+ | Google | `googleTagManager(containerId)`, `googleAnalytics4(measurementId)`, `googleAds(conversionId)`, `recaptcha(siteKey)` |
1216
+ | Advertising | `metaPixel(pixelId)`, `vkPixel(pixelId)`, `myTargetTopMailRu(counterId)`, `linkedInInsight(partnerId)`, `tiktokPixel(pixelId)`, `pinterestTag(tagId)`, `xPixel(pixelId)` |
1217
+ | Analytics | `yandexMetrica(counterId)`, `baiduTongji(siteId)`, `matomo(trackerUrl, siteId)`, `plausible(domain)`, `umami(websiteId, src)`, `hotjar(siteId)`, `microsoftClarity(projectId)`, `adobeAnalytics(src)` |
1218
+ | CRM/marketing | `hubspot(portalId)`, `bitrix24(widgetId)`, `amoCrm(widgetHash)`, `salesforcePardot(src)`, `mailchimp(src)` |
1219
+ | Chat | `intercom(appId)`, `zendeskChat(key)`, `crispChat(websiteId)`, `tawkTo(propertyId, widgetId)`, `jivoSite(widgetId)`, `liveChat(license)`, `tidio(publicKey)`, `chatwoot(websiteToken)`, `chatra(chatraId)` |
1220
+
1221
+ Every preset also accepts common overrides such as `id`, localized `name`/`description`, translation keys, `legalBasis`, `loadKey`, and Nuxt Scripts `options`. `mergeCookieScriptConfigs` combines categories and registers translation keys for build-time safelisting.
1222
+
1223
+ Umami supports `legalBasis: 'cookieless'`; in that mode it is listed as a legal notice rather than a consent-gated analytics script.
1224
+
1225
+ ### Define a custom integration
1226
+
1227
+ ```ts
1228
+ export default defineNuxtConfig({
1229
+ myelophone: {
1230
+ cookieScripts: {
1231
+ analytics: [
1232
+ {
1233
+ id: "acme-analytics",
1234
+ name: { en: "Acme Analytics", pl: "Analityka Acme" },
1235
+ nameKey: "cookies.acmeAnalytics.name",
1236
+ description: "Anonymous traffic measurement",
1237
+ descriptionKey: "cookies.acmeAnalytics.description",
1238
+ provider: "Acme",
1239
+ legalBasis: "consent",
1240
+ loadKey: "acme-analytics",
1241
+ src: "https://cdn.example.com/analytics.js",
1242
+ beforeLoad: "window.acmeQueue = window.acmeQueue || [];",
1243
+ onConsentChange: "window.acmeConsent = context.categories;",
1244
+ options: { scriptAttributes: { defer: true } },
1245
+ },
1246
+ ],
1247
+ },
1248
+ },
1249
+ });
1250
+ ```
1251
+
1252
+ Add the referenced `nameKey` and `descriptionKey` to each locale's `cookies.json`. The loader deduplicates scripts by `loadKey`, runs one-time initializers, applies category-specific hooks, and exposes consent state to hook code as `context.categories` and `context.isAllowed(category)`.
1253
+
1254
+ ### Gate embedded content
1255
+
1256
+ ```vue
1257
+ <CookieConsentWrapper category="functional" service-name="Support chat">
1258
+ <SupportChat />
1259
+ </CookieConsentWrapper>
1260
+
1261
+ <ConsentYoutube video-id="dQw4w9WgXcQ" />
1262
+ <ConsentGoogleMap address="Warsaw, Poland" language="pl" region="pl" />
1263
+
1264
+ <CookiePrivacyPolicy title="Cookie policy" />
1265
+ ```
1266
+
1267
+ Programmatic controls:
1268
+
1269
+ ```ts
1270
+ const {
1271
+ cookiePreferences,
1272
+ isBannerVisible,
1273
+ acceptAll,
1274
+ declineAll,
1275
+ savePreferences,
1276
+ checkConsent,
1277
+ } = useCookieControl();
1278
+
1279
+ savePreferences({ analytics: true, marketing: false, functional: true });
1280
+ ```
1281
+
1282
+ This is a technical consent mechanism, not legal advice. Validate categories, wording, retention, transfers, and lawful bases with counsel for each deployment.
1283
+
1284
+ ## API access
1285
+
1286
+ `useApi` returns typed `get`, `post`, `put`, and `delete` helpers. It retries requests three times, forwards `auth_token` as a Bearer token, and converts response failures into Nuxt errors.
1287
+
1288
+ ```ts
1289
+ interface Product {
1290
+ id: number;
1291
+ name: string;
1292
+ }
1293
+
1294
+ const api = useApi();
1295
+
1296
+ const products = await api.get<Product[], { category?: string }>("/products", {
1297
+ category: "audio",
1298
+ });
1299
+
1300
+ const created = await api.post<Product, { name: string }>("/products", {
1301
+ name: "Myelophone",
1302
+ });
1303
+ ```
1304
+
1305
+ Configure both `apiBaseServer` and `public.apiBase` for real deployments. The current client-side fallback is `http://localhost:3000`, so relying on the default outside local development is unsafe.
1306
+
1307
+ ## User store
1308
+
1309
+ Enable it with `myelophone.stores.user: true`. The store provides normalized profiles, roles/permissions, auth status, expiry checks, session extension, consent-aware persistence, and cross-tab synchronization.
1310
+
1311
+ ```ts
1312
+ const user = useUserStore();
1313
+
1314
+ user.configureUser({
1315
+ storageKey: "user",
1316
+ cookieMaxAge: 60 * 60 * 24 * 14,
1317
+ persistSession: true,
1318
+ sessionExtensionSeconds: 3600,
1319
+ });
1320
+
1321
+ user.setAuthenticated({
1322
+ profile: {
1323
+ id: 42,
1324
+ email: "ada@example.com",
1325
+ name: "Ada",
1326
+ roles: ["admin"],
1327
+ permissions: ["orders.read"],
1328
+ },
1329
+ session: {
1330
+ expiresAt: new Date(Date.now() + 3600_000).toISOString(),
1331
+ provider: "api",
1332
+ },
1333
+ });
1334
+
1335
+ if (user.hasRole("admin") && user.hasPermission("orders.read")) {
1336
+ // show authorized UI
1337
+ }
1338
+ ```
1339
+
1340
+ Connect renewal to a backend:
1341
+
1342
+ ```ts
1343
+ user.setSessionExtender(async ({ session }) => {
1344
+ return await $fetch("/api/session/extend", {
1345
+ method: "POST",
1346
+ body: { expiresAt: session?.expiresAt },
1347
+ });
1348
+ });
1349
+ ```
1350
+
1351
+ Main actions: `initUser`, `configureUser`, `applyState`, `setAuthenticating`, `setRefreshing`, `setAuthenticated`, `setProfile`, `patchProfile`, `setSession`, `setSessionExtender`, `extendSession`, `hasRole`, `hasAnyRole`, `hasPermission`, `hasAnyPermission`, `setError`, `clearError`, `logout`, and `resetStorage`.
1352
+
1353
+ The persisted profile/session is client-readable and must never be treated as authorization proof. Keep access/refresh tokens in secure HttpOnly cookies and enforce all authorization on the server.
1354
+
1355
+ ## Cart, totals, coupons, and currencies
1356
+
1357
+ Enable it with `myelophone.stores.cart: true`. The cart supports mixed item currencies, quantity management, coupons, custom metadata, exchange rates, calculation strategies/hooks, consent-aware persistence, and cross-tab synchronization.
1358
+
1359
+ ```ts
1360
+ const cart = useCartStore();
1361
+
1362
+ cart.configureCart({
1363
+ baseCurrency: "EUR",
1364
+ autoFetchRates: true,
1365
+ rateEndpoint: "/api/exchange-rates",
1366
+ taxPercentMetaKey: "vatPercent",
1367
+ shippingAmountMetaKey: "shippingAmount",
1368
+ shippingCurrencyMetaKey: "shippingCurrency",
1369
+ });
1370
+
1371
+ cart.addItem({ id: "sku-1", name: "Headphones", price: 99, currency: "EUR" });
1372
+ cart.incrementItem("sku-1");
1373
+ await cart.patchMeta({
1374
+ vatPercent: 23,
1375
+ shippingAmount: 8,
1376
+ shippingCurrency: "EUR",
1377
+ });
1378
+ cart.applyCoupon("WELCOME10", 10);
1379
+ cart.setCurrency("PLN");
1380
+
1381
+ console.log(cart.totalItems, cart.convertedTotals, cart.grandTotal);
1382
+ ```
1383
+
1384
+ Add asynchronous business rules without replacing the store:
1385
+
1386
+ ```ts
1387
+ const removeFreeShipping = cart.addStrategy("free-shipping", ({ totals }) => {
1388
+ if (totals.subtotal - totals.discount >= 100) return { shipping: 0 };
1389
+ });
1390
+
1391
+ const stopAudit = cart.onAfterCalculate(async ({ state, totals }) => {
1392
+ console.debug("calculated", state.items.length, totals.total);
1393
+ });
1394
+
1395
+ // later
1396
+ cart.removeStrategy("free-shipping");
1397
+ stopAudit();
1398
+ ```
1399
+
1400
+ Main actions: `initCart`, `configureCart`, `addItem`, `removeItem`, `updateQuantity`, `incrementItem`, `clearCart`, `applyCoupon`, `removeCoupon`, `setMeta`, `patchMeta`, `getMeta`, `setCurrency`, `convertAmount`, `setExchangeRate(s)`, `fetchExchangeRates`, `addStrategy`, `removeStrategy`, `onBeforeCalculate`, `onAfterCalculate`, `applyState`, and `resetStorage`.
1401
+
1402
+ `GET /api/exchange-rates?base=EUR&currencies=USD,PLN` validates three-letter currency codes and proxies Frankfurter with a 10-second timeout. Treat third-party rates as informational unless your business requirements explicitly accept that source and update cadence.
1403
+
1404
+ ## State, storage, device, and browser composables
1405
+
1406
+ ### Generic persistence
1407
+
1408
+ ```ts
1409
+ const preferences = useStorage(
1410
+ "preferences",
1411
+ { density: "comfortable", dismissed: [] as string[] },
1412
+ {
1413
+ storage: "cookie",
1414
+ fallbackStorage: "local",
1415
+ expires: 90,
1416
+ deep: true,
1417
+ syncTabs: true,
1418
+ canUseCookie: () => useCookieControl().checkConsent("functional"),
1419
+ shouldPersist: (value) => value.dismissed.length > 0,
1420
+ },
1421
+ );
1422
+
1423
+ preferences.value.value.density = "compact";
1424
+ preferences.refresh();
1425
+ preferences.remove();
1426
+ ```
1427
+
1428
+ `useStorage` supports `local`, `session`, and `cookie` backends, custom serializers, expiry/path, default writes, fallback storage, persistence predicates, storage events, and BroadcastChannel synchronization.
1429
+
1430
+ ### Cross-tab synchronization
1431
+
1432
+ Open tabs from the same origin stay consistent without polling or a page reload. Synchronization starts on the client after Nuxt is ready and uses `BroadcastChannel`; local-storage-backed values also react to the browser `storage` event.
1433
+
1434
+ The application shell synchronizes these fields automatically:
1435
+
1436
+ | State | Synchronized fields | Availability |
1437
+ | ------------------ | -------------------------------------------------------------- | --------------------------------------- |
1438
+ | Theme | `settings.theme` and the applied `data-theme`/theme class | Always enabled |
1439
+ | Cookie preferences | `settings.cookiePreferences`, `settings.isCookieBannerVisible` | Always enabled |
1440
+ | Cart | `items`, `coupon`, `meta`, `currency` | When `myelophone.stores.cart` is `true` |
1441
+ | User | `profile`, `session`, `status`, `error`, `lastAuthenticatedAt` | When `myelophone.stores.user` is `true` |
1442
+
1443
+ For example, adding an item or applying a coupon in one tab updates the other open tabs. Changing the theme updates both the Pinia state and the rendered document theme. Logging in, refreshing a session, updating a profile, or logging out is reflected across tabs when the user store is enabled.
1444
+
1445
+ Cart exchange rates, calculation strategies, hooks, and store configuration are not broadcast. User-store configuration and the session-extender callback are also local to each tab. Only the fields listed above are synchronized.
1446
+
1447
+ #### Synchronize selected fields from another Pinia store
1448
+
1449
+ Cross-tab synchronization is built in and can be added to any Pinia store with one `useStoreBroadcast` call. Pass the store and list the state fields that should be shared; no custom `BroadcastChannel`, message protocol, watchers, or loop prevention is required.
1450
+
1451
+ `useStoreBroadcast` accepts a store, an explicit key allowlist, and an optional channel name:
1452
+
1453
+ ```ts
1454
+ const filters = useCatalogFiltersStore();
1455
+
1456
+ const stopSync = useStoreBroadcast(filters, {
1457
+ keys: ["query", "sort", "selectedCategories"],
1458
+ channel: "catalog-filters",
1459
+ });
1460
+
1461
+ // Synchronization is now active for the component's lifetime.
1462
+ // Cleanup is automatic; call stopSync() only to stop it earlier.
1463
+ ```
1464
+
1465
+ To synchronize another field later, add its state key to the same allowlist:
1466
+
1467
+ ```ts
1468
+ useStoreBroadcast(filters, {
1469
+ keys: ["query", "sort", "selectedCategories", "viewMode"],
1470
+ });
1471
+ ```
1472
+
1473
+ Without `channel`, the channel name is `pinia:<store-id>`. Nested changes are watched deeply. Incoming values are applied with `$patch`, and source identifiers prevent a received update from being broadcast back in a loop.
1474
+
1475
+ Broadcast values must be serializable state composed of strings, numbers, booleans, `null`, arrays, and plain objects. Dates are transmitted as ISO strings. Functions, symbols, unsupported values, and circular references are converted to `null` and should not be included in the key list.
1476
+
1477
+ #### Synchronize standalone persisted state
1478
+
1479
+ `useStorage` enables tab synchronization by default. Set `syncTabs: false` for state that must remain isolated:
1480
+
1481
+ ```ts
1482
+ const sharedFilters = useStorage(
1483
+ "catalog-filters",
1484
+ { query: "", categories: [] as string[] },
1485
+ {
1486
+ storage: "local",
1487
+ syncTabs: true,
1488
+ deep: true,
1489
+ },
1490
+ );
1491
+
1492
+ sharedFilters.value.value.query = "headphones";
1493
+ ```
1494
+
1495
+ Updates and `remove()` calls are propagated through a key-specific `BroadcastChannel`. For local storage, the native `storage` event provides an additional synchronization path.
1496
+
1497
+ #### Theme synchronization
1498
+
1499
+ Theme changes can be broadcast directly when a custom theme control does not use the settings store:
1500
+
1501
+ ```ts
1502
+ const { applyTheme } = useThemeSync();
1503
+
1504
+ applyTheme("dark");
1505
+ ```
1506
+
1507
+ `applyTheme()` updates `data-theme`, the `light`/`dark` class, and persistent theme storage, then broadcasts the change through the `nuxt-theme-sync` channel. The settings store's `setTheme()` and `toggleTheme()` actions already use this mechanism.
1508
+
1509
+ Cross-tab synchronization is browser-local and same-origin. It does not synchronize different browsers, devices, users, server workers, or replicas; use backend persistence and realtime infrastructure when those scopes are required. Browsers without `BroadcastChannel` retain local state but cannot use `useStoreBroadcast` for live tab updates.
1510
+
1511
+ ### Other composables
1512
+
1513
+ | Composable | Returned capability | Example use |
1514
+ | ---------------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
1515
+ | `useDevice()` | `isBot`, `isOutdated`, `isMobile`, `isStaticMode`, low-battery and reduced-motion state | Disable expensive visual effects. |
1516
+ | `useDeviceStore()` | Device type, OS, browser, touch, viewport, DPR, orientation, network, language | Call `initDeviceDetection()` inside setup. |
1517
+ | `useNetwork()` | `effectiveType`, `isSlowConnection`, `isOnline` | Serve lightweight media on Save-Data/2G/3G. |
1518
+ | `useGeoFetch()` | Cached country/city lookup and `resolve()` | Drive `UiGeoDependent`; 30-minute session cache and 1-second provider timeout. |
1519
+ | `useThemeSync()` | `applyTheme(theme)` | Apply and broadcast a theme manually. |
1520
+ | `useStoreBroadcast(store, { keys, channel? })` | Selected Pinia state synchronization | Returns a stop function. |
1521
+ | `usePiniaStore(useStore)` | Store instance bound to Nuxt's Pinia | Useful in layer utilities. |
1522
+ | `usePopupControl(id, days)` | `isOpen`, `open(force?)`, `close(persist?)`, `reset` | Frequency-capped promotions. |
1523
+ | `useModalState()` | Modal stack and body-scroll lock | Used by `UiModal`. |
1524
+ | `useCookieModal()` | Open/close/toggle the global settings modal | Link a footer privacy button. |
1525
+ | `useCommand(command)` | Register/unregister a command for component lifetime | Add actions to the command palette. |
1526
+ | `useBreadcrumbs()` | Reactive breadcrumb list/home metadata | Used by `ViewBreadcrumbs`. |
1527
+ | `useLoadScript()` | Promise-based raw script loader | Simple client-only scripts; prefer consent loader for tracking. |
1528
+ | `useScriptLoader()` | Initialize consent-aware runtime scripts | Already initialized by the base app. |
1529
+ | `useSnapScroll()` | Register snap resets and scroll to top | Integrates snap sections with the global button. |
1530
+ | `useStatic()` | Reactive static-build flag | Select plain images/static effects. |
1531
+ | `useNuxtVersion()` | Injected Nuxt version string | Display diagnostics. |
1532
+ | `useSafeList(source)` | Write dynamically referenced i18n keys | Server/build-time utility only. |
1533
+
1534
+ ### Command palette
1535
+
1536
+ The global palette opens with `Ctrl+K` or `Cmd+K`. Prefix a query with `/` to search static routes and `#` to search registered commands.
1537
+
1538
+ ```ts
1539
+ useCommand({
1540
+ id: "refresh-data",
1541
+ name: "Refresh data",
1542
+ hint: "Reload the dashboard",
1543
+ keywords: ["sync", "reload"],
1544
+ action: () => refreshNuxtData(),
1545
+ });
1546
+ ```
1547
+
1548
+ ## Layout system
1549
+
1550
+ The components in `components/grid` form a mobile-first, 12-column layout system. They are auto-imported with the `Grid` prefix, so no local imports are required.
1551
+
1552
+ Use them in this order for most pages:
1553
+
1554
+ ```text
1555
+ GridStack optional full-width section or background layer
1556
+ └─ GridContainer content width and horizontal page padding
1557
+ └─ GridRow wrapping flex row, alignment, and gaps
1558
+ └─ GridCol responsive 12-column content blocks
1559
+ ```
1560
+
1561
+ ### Quick start
1562
+
1563
+ ```vue
1564
+ <template>
1565
+ <GridContainer size="boxed" class="py-12 md:py-20">
1566
+ <GridRow class="gap-y-8 md:-mx-4" align="center">
1567
+ <GridCol :span="7" class="md:px-4">
1568
+ <UiHeading :level="1">Product title</UiHeading>
1569
+ <p class="mt-4 text-lg text-gray-600">
1570
+ A production-ready page section built with the 12-column grid.
1571
+ </p>
1572
+ <UiButton to="/contact" class="mt-6">Get started</UiButton>
1573
+ </GridCol>
1574
+
1575
+ <GridCol :span="5" sm-order="first" class="md:px-4">
1576
+ <SafeNuxtImg
1577
+ src="/images/product.webp"
1578
+ alt="Product interface"
1579
+ class="w-full rounded-2xl"
1580
+ />
1581
+ </GridCol>
1582
+ </GridRow>
1583
+ </GridContainer>
1584
+ </template>
1585
+ ```
1586
+
1587
+ `GridRow` wraps automatically. Columns without `sm-span` occupy the full width below `768px`; `span` controls their width from `768px` upward. In the example, the image appears first on mobile because of `sm-order="first"`, while the source order is restored on desktop.
1588
+
1589
+ ### Components and props
1590
+
1591
+ | Component | Responsibility | Main props |
1592
+ | --------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
1593
+ | `GridContainer` | Centers content, limits its maximum width, and adds default horizontal page padding. | `size`, `py`, `my`, `mdPy`, `mdMy`, visibility and background props |
1594
+ | `GridRow` | Creates a wrapping flex row and controls cross-axis and horizontal alignment. | `size`, `align`, `justify`, `equalHeight`, spacing, visibility, and background props |
1595
+ | `GridCol` | Places content on the responsive 12-column grid. | `span`, `smSpan`, `mdSpan`, `smOrder`, visibility, and background props |
1596
+ | `GridStack` | Creates a full-width vertical wrapper for sections and layered backgrounds. | `as`, visibility and background props |
1597
+
1598
+ All components accept `id` and `class`. Use `class` for gaps, horizontal gutters, typography, borders, and any layout detail not represented by a dedicated prop.
1599
+
1600
+ #### `GridStack`
1601
+
1602
+ `GridStack` is an optional outer wrapper for a full-width page section. It arranges its children vertically and provides the common background image, gradient, local video, and YouTube background API.
1603
+
1604
+ The optional `as` prop changes the root HTML tag and defaults to `div`. Use a semantic tag such as `section`, `article`, `header`, `main`, or `footer` when the wrapper represents a meaningful part of the page.
1605
+
1606
+ ```vue
1607
+ <GridStack
1608
+ as="section"
1609
+ id="features"
1610
+ class="h-[640px] text-white"
1611
+ bg-image="/images/features-desktop.webp"
1612
+ bg-image-sm="/images/features-mobile.webp"
1613
+ bg-gradient="linear-gradient(90deg, rgba(2,6,23,.9), rgba(2,6,23,.25))"
1614
+ >
1615
+ <GridContainer size="content" class="h-full justify-center">
1616
+ <UiHeading :level="2">Everything needed to ship</UiHeading>
1617
+ <p class="mt-4 max-w-xl text-white/80">
1618
+ The section tag, responsive background, and content width are controlled independently.
1619
+ </p>
1620
+ </GridContainer>
1621
+ </GridStack>
1622
+ ```
1623
+
1624
+ Main props:
1625
+
1626
+ | Prop | Values/default | Purpose |
1627
+ | ---------------- | ------------------------------------------------------------------------- | ------------------------ |
1628
+ | `as` | Any HTML tag, default `div` | Root element |
1629
+ | `hideOnMobile` | `boolean` | Hide below `768px` |
1630
+ | `hideOnDesktop` | `boolean` | Hide from `768px` upward |
1631
+ | Background props | See [Backgrounds, gradients, and media](#backgrounds-gradients-and-media) | Full wrapper background |
1632
+
1633
+ `GridStack` fills the available parent height by default. Give it an explicit Tailwind `h-*` class when the section needs a fixed, viewport-based, or responsive height.
1634
+
1635
+ #### `GridContainer`
1636
+
1637
+ `GridContainer` centers page content, applies a maximum width selected with `size`, and adds safe horizontal padding for every size except `fullwidth`. It is a vertical flex container, so utilities such as `justify-center` and `items-center` can position its children.
1638
+
1639
+ ```vue
1640
+ <GridContainer size="text" id="article-introduction" class="py-12 md:py-20">
1641
+ <UiHeading :level="1">Article title</UiHeading>
1642
+ <p class="mt-5 text-lg leading-8 text-gray-600">
1643
+ A readable text column centered within the page.
1644
+ </p>
1645
+ </GridContainer>
1646
+ ```
1647
+
1648
+ Main props:
1649
+
1650
+ | Prop | Values/default | Purpose |
1651
+ | ------------------------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------- |
1652
+ | `size` | `fullwidth`, `full`, `boxed`, `laptop`, `content`, `medium`, `text`, `tablet`, `mobile`; default `content` | Maximum content width |
1653
+ | `py`, `my` | Tailwind spacing suffix | Vertical padding or margin |
1654
+ | `mdPy`, `mdMy` | Tailwind spacing suffix | Vertical padding or margin from `768px` |
1655
+ | `hideOnMobile`, `hideOnDesktop` | `boolean` | Responsive visibility |
1656
+ | Background props | See [Backgrounds, gradients, and media](#backgrounds-gradients-and-media) | Container background |
1657
+
1658
+ `GridContainer` always renders a `div`; choose `GridStack as="..."` when a semantic outer element is required.
1659
+
1660
+ #### `GridRow`
1661
+
1662
+ `GridRow` is a full-width wrapping flex row. It groups `GridCol` components, controls their alignment, and can optionally constrain and center itself with `size`.
1663
+
1664
+ ```vue
1665
+ <GridContainer size="content">
1666
+ <GridRow
1667
+ align="center"
1668
+ justify="between"
1669
+ class="gap-y-8 md:-mx-4"
1670
+ >
1671
+ <GridCol :span="8" class="md:px-4">
1672
+ <UiHeading :level="2">Primary content</UiHeading>
1673
+ </GridCol>
1674
+
1675
+ <GridCol :span="4" class="md:px-4">
1676
+ <UiButton to="/details" block>View details</UiButton>
1677
+ </GridCol>
1678
+ </GridRow>
1679
+ </GridContainer>
1680
+ ```
1681
+
1682
+ Main props:
1683
+
1684
+ | Prop | Values/default | Purpose |
1685
+ | ------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------- |
1686
+ | `size` | Any container size | Optional centered maximum width for the row |
1687
+ | `align` | `start`, `center`, `end`, `stretch`; default `start` | Cross-axis column alignment |
1688
+ | `justify` | `start`, `center`, `end`, `between`, `around`, `evenly`; default `start` | Horizontal distribution |
1689
+ | `equalHeight` | `boolean`, default `false` | Stretch sibling columns equally on desktop |
1690
+ | `py`, `my`, `mdPy`, `mdMy` | Tailwind spacing suffix | Responsive vertical spacing |
1691
+ | Visibility and background props | Shared grid API | Responsive visibility and row background |
1692
+
1693
+ #### `GridCol`
1694
+
1695
+ `GridCol` is a vertical flex column placed on the 12-column grid. It is full width on mobile unless `smSpan` is provided, while `span` controls its desktop width.
1696
+
1697
+ ```vue
1698
+ <GridRow class="gap-y-6 md:-mx-3">
1699
+ <GridCol :span="8" :sm-span="12" class="md:px-3">
1700
+ <article>Main content</article>
1701
+ </GridCol>
1702
+
1703
+ <GridCol
1704
+ :span="4"
1705
+ :sm-span="12"
1706
+ sm-order="first"
1707
+ class="md:px-3"
1708
+ >
1709
+ <aside>Shown above the article on mobile and on the right on desktop.</aside>
1710
+ </GridCol>
1711
+ </GridRow>
1712
+ ```
1713
+
1714
+ Main props:
1715
+
1716
+ | Prop | Values/default | Purpose |
1717
+ | ------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------ |
1718
+ | `span` | `1`–`12`, default `12` | Desktop width in twelfths |
1719
+ | `smSpan` | `1`–`12` | Mobile width in twelfths; defaults to full width |
1720
+ | `mdSpan` | `1`–`12` | Explicit desktop width override |
1721
+ | `smOrder` | `1`–`12`, `first`, `last`, `none` | Mobile order, reset on desktop |
1722
+ | `hideOnMobile`, `hideOnDesktop` | `boolean` | Responsive visibility |
1723
+ | Background props | See [Backgrounds, gradients, and media](#backgrounds-gradients-and-media) | Column background |
1724
+
1725
+ #### Container sizes
1726
+
1727
+ `GridContainer` defaults to `content`. `GridRow` can also receive `size` when a row needs its own centered maximum width.
1728
+
1729
+ | `size` value | Maximum width | Typical use |
1730
+ | ------------ | -------------------------------------------- | ------------------------------------------- |
1731
+ | `fullwidth` | `100%`, without automatic horizontal padding | Edge-to-edge sections |
1732
+ | `full` | `100%` | Full-width content with page padding |
1733
+ | `boxed` | The smaller of `90%` and `1500px` | Wide landing pages and dashboards |
1734
+ | `laptop` | `1367px` | Wide application layouts |
1735
+ | `content` | `1080px` | Default pages and marketing sections |
1736
+ | `medium` | `920px` | Forms, feature content, and compact pages |
1737
+ | `text` | `728px` | Articles and long-form copy |
1738
+ | `tablet` | `600px` | Narrow forms and dialogs presented as pages |
1739
+ | `mobile` | `480px` | Authentication and single-column flows |
1740
+
1741
+ Every size except `fullwidth` includes `px-4 md:px-0`. Use `full` when content must stay full width but still needs safe mobile page padding.
1742
+
1743
+ #### Responsive columns
1744
+
1745
+ | Prop | Values | Behaviour |
1746
+ | --------- | --------------------------------- | --------------------------------------------------------------- |
1747
+ | `span` | `1`–`12`, default `12` | Column width from the `md` breakpoint (`768px`) upward |
1748
+ | `smSpan` | `1`–`12` | Column width below `768px`; without it the column is full width |
1749
+ | `mdSpan` | `1`–`12` | Explicit desktop width override |
1750
+ | `smOrder` | `1`–`12`, `first`, `last`, `none` | Mobile order; normal source order is restored from `768px` |
1751
+
1752
+ Think in twelfths: `6 + 6` gives two equal columns, `8 + 4` gives content plus a sidebar, and `3 + 3 + 3 + 3` gives four desktop cards. A row wraps when its columns exceed 12.
1753
+
1754
+ To add gutters, put a negative horizontal margin on the row and matching padding on each column. Vertical gaps can be applied directly to the wrapping row:
1755
+
1756
+ ```vue
1757
+ <GridRow class="gap-y-6 md:-mx-3">
1758
+ <GridCol :span="4" :sm-span="6" class="md:px-3">...</GridCol>
1759
+ <GridCol :span="4" :sm-span="6" class="md:px-3">...</GridCol>
1760
+ <GridCol :span="4" :sm-span="12" class="md:px-3">...</GridCol>
1761
+ </GridRow>
1762
+ ```
1763
+
1764
+ This layout is one column on small phones, two plus one full-width column when `smSpan` is used, and three equal columns on desktop.
1765
+
1766
+ #### Row alignment
1767
+
1768
+ `align` accepts `start`, `center`, `end`, or `stretch`. `justify` accepts `start`, `center`, `end`, `between`, `around`, or `evenly`.
1769
+
1770
+ Set `equal-height` when cards in the same desktop row must stretch to the height of the tallest column:
1771
+
1772
+ ```vue
1773
+ <GridContainer size="content" class="py-16">
1774
+ <GridRow :equal-height="true" class="gap-y-6 md:-mx-3">
1775
+ <GridCol
1776
+ v-for="plan in plans"
1777
+ :key="plan.name"
1778
+ :span="4"
1779
+ class="md:px-3"
1780
+ >
1781
+ <UiCard class="h-full">
1782
+ <template #header>
1783
+ <UiHeading :level="2">{{ plan.name }}</UiHeading>
1784
+ </template>
1785
+
1786
+ <p>{{ plan.description }}</p>
1787
+
1788
+ <template #footer>
1789
+ <UiButton :to="plan.to" block>Choose plan</UiButton>
1790
+ </template>
1791
+ </UiCard>
1792
+ </GridCol>
1793
+ </GridRow>
1794
+ </GridContainer>
1795
+ ```
1796
+
1797
+ `equalHeight` applies from the desktop breakpoint. Add `h-full` to the card or inner wrapper when that element must fill the stretched column.
1798
+
1799
+ #### Spacing and visibility
1800
+
1801
+ `GridContainer` and `GridRow` provide these optional spacing shortcuts:
1802
+
1803
+ | Prop | Generated utility |
1804
+ | ------------ | ----------------- |
1805
+ | `py="12"` | `py-12` |
1806
+ | `my="8"` | `my-8` |
1807
+ | `md-py="20"` | `md:py-20` |
1808
+ | `md-my="12"` | `md:my-12` |
1809
+
1810
+ The values are Tailwind spacing scale suffixes. Regular Tailwind classes remain available for asymmetric or breakpoint-specific spacing.
1811
+
1812
+ All four grid components support `hide-on-mobile` and `hide-on-desktop`:
1813
+
1814
+ ```vue
1815
+ <GridRow>
1816
+ <GridCol :span="8">
1817
+ <!-- Shared main content -->
1818
+ </GridCol>
1819
+
1820
+ <GridCol :span="4" hide-on-mobile>
1821
+ <!-- Desktop sidebar -->
1822
+ </GridCol>
1823
+
1824
+ <GridCol hide-on-desktop>
1825
+ <!-- Compact mobile actions -->
1826
+ </GridCol>
1827
+ </GridRow>
1828
+ ```
1829
+
1830
+ `hideOnMobile` hides below `768px`; `hideOnDesktop` hides from `768px` upward.
1831
+
1832
+ ### Ready-to-use layouts
1833
+
1834
+ #### Full-width hero with constrained content
1835
+
1836
+ Place the background on `GridStack` and the readable content inside `GridContainer`. This keeps the media edge to edge without losing consistent content alignment.
1837
+
1838
+ ```vue
1839
+ <GridStack
1840
+ class="h-[min(760px,100dvh)] text-white"
1841
+ bg-color="#111827"
1842
+ bg-image="/images/hero-desktop.webp"
1843
+ bg-image-sm="/images/hero-mobile.webp"
1844
+ bg-gradient="linear-gradient(90deg, rgba(0,0,0,.82), rgba(0,0,0,.15))"
1845
+ bg-gradient-sm="linear-gradient(180deg, rgba(0,0,0,.25), rgba(0,0,0,.9))"
1846
+ >
1847
+ <GridContainer size="boxed" class="h-full justify-center">
1848
+ <GridRow align="center">
1849
+ <GridCol :span="7">
1850
+ <p class="mb-3 text-sm font-semibold uppercase tracking-widest">
1851
+ New release
1852
+ </p>
1853
+ <UiHeading :level="1" class="text-4xl md:text-7xl">
1854
+ Build the next product faster
1855
+ </UiHeading>
1856
+ <p class="mt-6 max-w-2xl text-lg text-white/80">
1857
+ A concise value proposition that remains readable over the image.
1858
+ </p>
1859
+ <div class="mt-8 flex flex-wrap gap-3">
1860
+ <UiButton to="/start">Start now</UiButton>
1861
+ <UiButton to="/docs" variant="outline">Documentation</UiButton>
1862
+ </div>
1863
+ </GridCol>
1864
+ </GridRow>
1865
+ </GridContainer>
1866
+ </GridStack>
1867
+ ```
1868
+
1869
+ #### Content with a sticky sidebar
1870
+
1871
+ ```vue
1872
+ <GridContainer size="content" class="py-12 md:py-20">
1873
+ <GridRow align="start" class="gap-y-10 md:-mx-5">
1874
+ <GridCol :span="8" class="md:px-5">
1875
+ <article class="prose max-w-none">
1876
+ <h1>Guide title</h1>
1877
+ <!-- Article content -->
1878
+ </article>
1879
+ </GridCol>
1880
+
1881
+ <GridCol :span="4" class="md:px-5">
1882
+ <aside class="md:sticky md:top-24">
1883
+ <UiCard>
1884
+ <strong>On this page</strong>
1885
+ <!-- Navigation -->
1886
+ </UiCard>
1887
+ </aside>
1888
+ </GridCol>
1889
+ </GridRow>
1890
+ </GridContainer>
1891
+ ```
1892
+
1893
+ #### Alternating feature rows
1894
+
1895
+ Keep the semantic source order consistent and change only the mobile presentation with `smOrder`:
1896
+
1897
+ ```vue
1898
+ <GridContainer size="content" class="py-16">
1899
+ <GridRow align="center" class="gap-y-8 md:-mx-6">
1900
+ <GridCol :span="6" class="md:px-6">
1901
+ <UiHeading :level="2">Analytics without extra setup</UiHeading>
1902
+ <p class="mt-4 text-gray-600">
1903
+ Explain the feature, its outcome, and the next action.
1904
+ </p>
1905
+ </GridCol>
1906
+
1907
+ <GridCol :span="6" sm-order="first" class="md:px-6">
1908
+ <SafeNuxtImg
1909
+ src="/images/analytics.webp"
1910
+ alt="Analytics dashboard"
1911
+ class="w-full rounded-2xl"
1912
+ />
1913
+ </GridCol>
1914
+ </GridRow>
1915
+ </GridContainer>
1916
+ ```
1917
+
1918
+ #### Responsive application shell
1919
+
1920
+ ```vue
1921
+ <GridContainer size="boxed" class="py-6">
1922
+ <GridRow align="start" class="gap-y-6 md:-mx-3">
1923
+ <GridCol :span="3" hide-on-mobile class="md:px-3">
1924
+ <nav class="sticky top-6 rounded-xl border p-4">
1925
+ <!-- Desktop navigation -->
1926
+ </nav>
1927
+ </GridCol>
1928
+
1929
+ <GridCol :span="9" class="md:px-3">
1930
+ <main class="min-w-0">
1931
+ <!-- Dashboard or application page -->
1932
+ </main>
1933
+ </GridCol>
1934
+ </GridRow>
1935
+ </GridContainer>
1936
+ ```
1937
+
1938
+ ### Backgrounds, gradients, and media
1939
+
1940
+ Background props are available on `GridStack`, `GridContainer`, `GridRow`, and `GridCol`.
1941
+
1942
+ | Desktop/default prop | Mobile override | Purpose |
1943
+ | -------------------- | ----------------- | -------------------------------------------- |
1944
+ | `bgColor` | — | Any CSS background color |
1945
+ | `bgImage` | `bgImageSm` | Image URL |
1946
+ | `bgGradient` | `bgGradientSm` | CSS gradient placed over the image or video |
1947
+ | `bgSize` | `bgSizeSm` | CSS background size; defaults to `cover` |
1948
+ | `bgVideo` | `bgVideoSm` | Muted, looping local background video |
1949
+ | `bgVideoPoster` | `bgVideoPosterSm` | Poster shown while a local video loads |
1950
+ | `bgYoutube` | `bgYoutubeSm` | YouTube video ID or URL used as a background |
1951
+
1952
+ Mobile overrides apply below `768px`. If an image, gradient, size, or poster override is omitted, its desktop value is reused. Set `bg-image-sm="none"` to remove a desktop background image on mobile.
1953
+
1954
+ When several media props are supplied for the same breakpoint, YouTube is rendered before local video. For the static background/poster layer, `bgImage` takes precedence over the generated YouTube poster, which takes precedence over `bgVideoPoster`.
1955
+
1956
+ #### Local background video with responsive sources
1957
+
1958
+ Always provide posters so the section has a useful first frame while video loads or when playback is unavailable.
1959
+
1960
+ ```vue
1961
+ <GridStack
1962
+ class="h-[680px] text-white"
1963
+ bg-video="/video/launch-desktop.mp4"
1964
+ bg-video-sm="/video/launch-mobile.mp4"
1965
+ bg-video-poster="/images/launch-desktop.webp"
1966
+ bg-video-poster-sm="/images/launch-mobile.webp"
1967
+ bg-gradient="linear-gradient(90deg, rgba(2,6,23,.85), rgba(2,6,23,.2))"
1968
+ >
1969
+ <GridContainer size="content" class="h-full justify-end py-12">
1970
+ <GridRow>
1971
+ <GridCol :span="7">
1972
+ <UiHeading :level="1">Launch campaign</UiHeading>
1973
+ <p class="mt-4 max-w-xl text-white/80">
1974
+ Foreground content stays above the video and gradient automatically.
1975
+ </p>
1976
+ </GridCol>
1977
+ </GridRow>
1978
+ </GridContainer>
1979
+ </GridStack>
1980
+ ```
1981
+
1982
+ #### Background image on a single column
1983
+
1984
+ The same API can be used for a card, banner, or one side of a split layout:
1985
+
1986
+ ```vue
1987
+ <GridContainer size="content" class="py-16">
1988
+ <GridRow :equal-height="true" class="overflow-hidden rounded-2xl bg-gray-50">
1989
+ <GridCol
1990
+ :span="5"
1991
+ class="min-h-80"
1992
+ bg-color="#0f172a"
1993
+ bg-image="/images/team-desktop.webp"
1994
+ bg-image-sm="/images/team-mobile.webp"
1995
+ bg-gradient="linear-gradient(180deg, transparent 35%, rgba(2,6,23,.9))"
1996
+ >
1997
+ <div class="mt-auto p-6 text-white">
1998
+ <p class="text-sm uppercase tracking-wider text-white/70">Our team</p>
1999
+ <UiHeading :level="2">Built by product engineers</UiHeading>
2000
+ </div>
2001
+ </GridCol>
2002
+
2003
+ <GridCol :span="7">
2004
+ <div class="flex h-full flex-col justify-center p-8 md:p-12">
2005
+ <UiHeading :level="2">A complete split section</UiHeading>
2006
+ <p class="mt-4 text-gray-600">
2007
+ The image column becomes a full-width visual above the copy on mobile.
2008
+ </p>
2009
+ </div>
2010
+ </GridCol>
2011
+ </GridRow>
2012
+ </GridContainer>
2013
+ ```
2014
+
2015
+ #### YouTube background
2016
+
2017
+ ```vue
2018
+ <GridStack
2019
+ class="h-[640px] text-white"
2020
+ bg-youtube="https://www.youtube.com/watch?v=VIDEO_ID"
2021
+ bg-youtube-sm="MOBILE_VIDEO_ID"
2022
+ bg-gradient="linear-gradient(rgba(0,0,0,.45), rgba(0,0,0,.72))"
2023
+ >
2024
+ <GridContainer size="text" class="h-full items-center justify-center text-center">
2025
+ <UiHeading :level="1">Campaign headline</UiHeading>
2026
+ <p class="mt-4 text-lg text-white/80">Supporting campaign message.</p>
2027
+ </GridContainer>
2028
+ </GridStack>
2029
+ ```
2030
+
2031
+ Background videos are decorative, muted, looping, non-interactive media. Use a normal video player or `ConsentYoutube` when users need playback controls, captions, or consent-gated YouTube content.
2032
+
2033
+ ### Practical rules
2034
+
2035
+ - Start sections with `GridContainer size="content"`; switch to `boxed` for wide layouts and wrap everything in `GridStack` for edge-to-edge backgrounds.
2036
+ - Keep each desktop row at 12 columns unless intentional wrapping is part of the design.
2037
+ - Use `smSpan` only when a multi-column mobile layout is genuinely readable; the full-width mobile default is safer for forms and text.
2038
+ - Keep meaningful DOM order for accessibility. Use `smOrder` for visual presentation, not to repair an illogical source order.
2039
+ - Put row gutters in `class` (`md:-mx-*` on the row and `md:px-*` on its columns) so nested content remains aligned.
2040
+ - Give background sections an explicit height or enough vertical padding, descriptive foreground content, and sufficient gradient contrast.
2041
+ - Prefer local background video for decorative motion. Use `ConsentYoutube` for user-controlled YouTube embeds.
2042
+
2043
+ ## Component reference
2044
+
2045
+ All components below are auto-imported. Standard `$attrs` and the documented slots can be used to customize them.
2046
+
2047
+ ### UI primitives
2048
+
2049
+ | Component | Purpose and principal props/events |
2050
+ | -------------------- | ----------------------------------------------------------------------------------------------------------------------- |
2051
+ | `UiAccordion` | `items[{ label, content, defaultOpen }]`, `multiple`, `faq`, `ui`; item/header/content scoped slots. |
2052
+ | `UiAlert` | Info/success/warning/error alert with title, description, icon/avatar, size, close button, and action slot. |
2053
+ | `UiAvatar` | Image/icon/initials avatar; `size`, optional status `chipColor` and position. |
2054
+ | `UiBadge` | Solid/soft/outline badge; colors, sizes, pill/rounded, icons, avatar, slots. |
2055
+ | `UiBanner` | Closable full-width banner with color/variant, icon/avatar, title/default/actions slots. |
2056
+ | `UiButton` | Button, anchor, or NuxtLink; color/variant/size, loading, disabled, block, rounded, square, leading/trailing icons. |
2057
+ | `UiCard` | Div/section/article or link card; bordered, rounded, padded, header/default/footer slots. |
2058
+ | `UiCheckbox` | Checkbox group with `v-model`, options, legend, disabled state, and dividers. |
2059
+ | `UiChip` | Positioned or standalone indicator; text, color, size, inset, visibility, and ping animation. |
2060
+ | `UiCommandPalette` | Global route/command search opened with `Ctrl/Cmd+K`. |
2061
+ | `UiConsole` | Responsive animated console with delayed/removable lines, highlighted segments, looping, slots, and controls. |
2062
+ | `UiCursorCreative` | Optional animated desktop cursor; disables itself for coarse pointers and reduced motion. |
2063
+ | `UiFeatureAccordion` | Feature list synchronized with an image; side/mobile ordering and optional links. |
2064
+ | `UiFileUpload` | Native/FormData-compatible file picker and dropzone; single/multiple files, validation, previews, removal, slots. |
2065
+ | `UiGeoDependent` | Show slot using country whitelist/blacklist and optional pre-resolved `geoData`. |
2066
+ | `UiHeading` | Semantic `h1`–`h6` with independent `xs`–`display` visual size and custom class. |
2067
+ | `UiIcon` | Server component fetching Iconify SVG by `prefix:name`; accepts `size` and class. |
2068
+ | `UiIcon8` | Server component fetching Icons8 PNG by icon, type, size, and color. |
2069
+ | `UiInput` | Floating-label `v-model` input with type, icon position, placeholder, and disabled state. |
2070
+ | `UiLangLink` | NuxtLink that adds/removes the current locale prefix. |
2071
+ | `UiLanguageSelect` | Responsive locale dropdown with full/short labels and class overrides. |
2072
+ | `UiLangVisible` | Render by `only` or `except` locale lists. |
2073
+ | `UiLightBox` | Async image gallery/lightbox; string/object images, item/image style/class, pass-through lightbox props. |
2074
+ | `UiLocalTime` | Live time for an IANA timezone; optional label/date/seconds and 12/24-hour mode. |
2075
+ | `UiModal` | Teleported stacked modal with `v-model`, title, width, close prevention, body class; emits `close`. |
2076
+ | `UiPopup` | Frequency-capped popup with id, copy/image, five visual variants, delay, and action slot. |
2077
+ | `UiProtectedEmail` | Builds an email address only after mount; split user/domain/TLD, subject, placeholder. |
2078
+ | `UiRelativeTime` | Accessible live relative date with locale override, short/long style, and dynamic tag. |
2079
+ | `UiScheduledContent` | Date-window content with timezone, preview, interval, and daily/yearly repeat; active/upcoming slots. |
2080
+ | `UiScrollToTop` | Global button shown after 600 px; also resets registered snap containers. |
2081
+ | `UiSegmentedControl` | Single/multiple `v-model`, horizontal/vertical/full layout, icons, sizes, colors, outline/soft style. |
2082
+ | `UiSiteSearch` | Lazy worker-backed Fuse search over auto-discovered Nuxt routes and sitemaps, with locale and URL presets. |
2083
+ | `UiSkeleton` | Theme-aware loading placeholder; `as`, `animated`, `rounded`, `width`, `height`, and optional accessible label. |
2084
+ | `UiSmartContrast` | Full background image/gradient wrapper with fixed/auto contrast modes and configurable height. |
2085
+ | `UiSnapContainer` | Vertical/horizontal controlled full-page snapping for `UiSnapSection` children. |
2086
+ | `UiSnapSection` | Snap panel with image/video/YouTube mobile variants, poster, overlay, lazy loading, and content class. |
2087
+ | `UiSplitSection` | Image/text split with side, container width, edge image, mobile reversal, and named text slot. |
2088
+ | `UiSteps` | Responsive progress line, icon/number steps, or step cards with navigation, alignment, placement, and `v-model`. |
2089
+ | `UiStickyWrapper` | Sticky slot with pixel offset; prepares its parent positioning. |
2090
+ | `UiTable` | Sortable table with nested keys and automatic virtualization above 100 rows; emits `sort`. |
2091
+ | `UiTabs` | Accessible vertical/horizontal tabs with configurable side, width, alignment, slots, and animated panels. |
2092
+ | `UiTextarea` | `v-model` textarea with floating label, error state, clear/save controls and `save`/`clear` events. |
2093
+ | `UiTextColumn` | Typography/spacing wrapper for prose slots. |
2094
+ | `UiTooltip` | Hover/click tooltip with arbitrary trigger/content slots, mobile tap support, viewport-aware flip/shift, and `v-model`. |
2095
+ | `UiToggler` | Accessible boolean `v-model` switch with label and disabled state. |
2096
+ | `UiTruncateText` | Responsive character limits with localized expand/collapse controls. Text-only slot. |
2097
+ | `UiViewportSpacer` | Responsive `vh/dvh` or `vw/dvw` spacer. |
2098
+
2099
+ #### UiSiteSearch
2100
+
2101
+ Place `UiSiteSearch` once in a layout. It automatically collects static routes from the Nuxt router and URLs from `robots.txt`, `sitemap.xml`, and `sitemap_index.xml`; pages do not need to be listed in component props. Page HTML is fetched and parsed in a dedicated Web Worker. The worker, Fuse.js, crawling, and IndexedDB cache are initialized only when the palette is first opened.
2102
+
2103
+ Search defaults to the active site locale. Locale is determined from the first URL segment; a URL without a supported locale prefix belongs to `defaultLocale`. Set `searchAllLocales` to search the complete multilingual index. Current-locale results are placed first, and translated variants with the same path after removing the locale prefix are collapsed into one result. The default shortcut is `Ctrl/Cmd+Shift+K`; `shortcutKey` and `shortcutShift` can change it. The trigger slot receives `open()`, `close()`, and `isOpen`; the component also exposes `open(query?, immediate?)`, `search(query?)`, `close()`, and `clearCache()`.
2104
+
2105
+ URL presets work anywhere the component is mounted:
2106
+
2107
+ - `?search=responsive&searchMode=search` opens the palette and searches immediately.
2108
+ - `?search=responsive&searchMode=prefill` opens it with a filled field and waits for Enter or editing.
2109
+ - `?search=responsive+worker&searchOperator=and` requires every search word; use `searchOperator=or` to match any word.
2110
+
2111
+ The AND/OR selector is also available inside the palette. Its default is controlled by `operator` or `v-model:operator`. Rename URL parameters with `queryParam`, `modeParam`, and `operatorParam`. When the mode parameter is absent, `urlMode` chooses the behavior. `consumeUrl` removes the parameters after they have been handled.
2112
+
2113
+ ```vue
2114
+ <UiSiteSearch search-all-locales>
2115
+ <template #trigger="{ open }">
2116
+ <UiButton @click="open()">Search</UiButton>
2117
+ </template>
2118
+ </UiSiteSearch>
2119
+ ```
2120
+
2121
+ Search strategies:
2122
+
2123
+ - `client` is the default strategy and uses automatic page discovery plus the lazy Fuse worker described above.
2124
+ - `remote` sends the query to `endpoint`. If `strategy` is omitted, is not `remote`, or `endpoint` is empty, the component falls back to `client`.
2125
+
2126
+ ```vue
2127
+ <UiSiteSearch strategy="remote" endpoint="/api/site-search" />
2128
+ ```
2129
+
2130
+ Remote search sends a GET request. Existing query parameters in `endpoint` are preserved:
2131
+
2132
+ ```text
2133
+ GET /api/site-search?q=responsive%20worker&locale=ru&operator=and&limit=10&allLocales=false
2134
+ Accept: application/json
2135
+ ```
2136
+
2137
+ The endpoint must return an object with a `results` array. Only `path` and `title` are required. `score` uses Fuse semantics where a lower value is a better match. Highlight ranges are optional, zero-based, and inclusive; if omitted, the component highlights exact query words itself.
2138
+
2139
+ The playground remote endpoint uses [JSONPlaceholder](https://jsonplaceholder.typicode.com) as its data source, performs AND/OR filtering server-side, and adapts the posts response to this contract.
2140
+
2141
+ ```json
2142
+ {
2143
+ "total": 2,
2144
+ "results": [
2145
+ {
2146
+ "id": "product-42-ru",
2147
+ "path": "/ru/products/product-42",
2148
+ "url": "https://example.com/ru/products/product-42",
2149
+ "title": "Responsive Worker",
2150
+ "description": "Короткое описание страницы",
2151
+ "snippet": "Фрагмент текста, содержащий responsive worker",
2152
+ "locale": "ru",
2153
+ "score": 0.08,
2154
+ "highlights": {
2155
+ "title": [
2156
+ [0, 9],
2157
+ [11, 16]
2158
+ ],
2159
+ "snippet": [
2160
+ [28, 37],
2161
+ [39, 44]
2162
+ ],
2163
+ "path": []
2164
+ }
2165
+ }
2166
+ ]
2167
+ }
2168
+ ```
2169
+
2170
+ #### UiSteps
2171
+
2172
+ `UiSteps` accepts `items` with `title`, optional `value`, `description`, `icon`, `content`, and `disabled`. Choose `variant="progress"`, `variant="steps"`, or `variant="cards"`; place navigation above or below the content with `placement`; and align it using `align="start | center | end"`. Number/icon steps become vertical and cards become full-width below 640 px when `stackOnMobile` is enabled.
2173
+
2174
+ The default scoped slot receives `item`, `index`, `value`, `goTo()`, `next()`, `previous()`, `complete()`, `uncomplete()`, `completeAndNext()`, `isCompleted()`, `canGoTo()`, `isFirst`, and `isLast`. Header clicks are enabled by default, while `linear` prevents skipping unvisited steps through the header.
2175
+
2176
+ Use `v-model:completed` as the source of successfully completed steps. Set `requirePrevious` when every step must wait for the previous enabled step, or add `requires: 'step-value'` / `requires: ['first-value', 'second-value']` to an individual item. Both conditions are combined when both are present. Call `complete()` only after form validation or an API operation succeeds; `completeAndNext()` marks the current step successful and opens the next available step.
2177
+
2178
+ Theme styling follows `--ui-bg` and `--ui-text`; override `--ui-steps-accent`, `--ui-steps-muted`, `--ui-steps-border`, or `--ui-steps-surface` when needed.
2179
+
2180
+ ```vue
2181
+ <UiSteps
2182
+ v-model="step"
2183
+ v-model:completed="completedSteps"
2184
+ :items="steps"
2185
+ variant="cards"
2186
+ require-previous
2187
+ >
2188
+ <template #default="{ item, goTo, canGoTo, completeAndNext }">
2189
+ <h2>{{ item.title }}</h2>
2190
+ <UiButton :disabled="!canGoTo('review')" @click="goTo('review')">Go to review</UiButton>
2191
+ <UiButton @click="completeAndNext">Save and continue</UiButton>
2192
+ </template>
2193
+ </UiSteps>
2194
+ ```
2195
+
2196
+ #### UiTooltip
2197
+
2198
+ The default `trigger="hover"` opens from mouse hover or keyboard focus and falls back to tap-to-toggle on touch screens. Use `trigger="click"` for click/tap toggling on every device. The tooltip is teleported to `body`, flips to a side with enough room, shifts inside `viewportPadding`, and is remeasured on scroll, resize, and content-size changes.
2199
+
2200
+ The default slot accepts any trigger; the `content` slot accepts arbitrary markup and receives a `close()` function. For actionable content, set `role="dialog"`. Use `as="div"` around block/layout triggers, and set `trigger-tabindex="-1"` when the slotted trigger already contains its own focusable control. `v-model`, `open`/`close` events, and exposed `open()`, `close()`, `toggle()`, and `updatePosition()` methods are available for external control.
2201
+
2202
+ Placements are `auto`, `top`, `right`, `bottom`, `left`, and their `-start`/`-end` variants. Styling can be changed with `triggerClass`, `contentClass`, or the `--tooltip-background`, `--tooltip-color`, `--tooltip-border`, `--tooltip-radius`, and `--tooltip-shadow` CSS variables.
2203
+
2204
+ #### UiConsole
2205
+
2206
+ `UiConsole` renders plain strings or structured lines. All timing values are milliseconds. A line's `delay` is the wait after the preceding line appears; when omitted, `lineDelay` is used (the first line appears immediately). `hideAfter` removes that line after the specified time, which can be used for temporary status messages or line replacement.
2207
+
2208
+ ```vue
2209
+ <script setup lang="ts">
2210
+ const output = [
2211
+ {
2212
+ prefix: "$",
2213
+ segments: [{ text: "npx telemetry " }, { text: "init", tone: "primary" }],
2214
+ },
2215
+ {
2216
+ prefix: "→",
2217
+ text: "Scanning service topology...",
2218
+ tone: "muted",
2219
+ delay: 500,
2220
+ hideAfter: 900,
2221
+ },
2222
+ {
2223
+ prefix: "→",
2224
+ delay: 1000,
2225
+ segments: [
2226
+ { text: "Found ", tone: "muted" },
2227
+ { text: "23 services", highlight: true },
2228
+ ],
2229
+ },
2230
+ { prefix: "✓", text: "Deployment complete", tone: "success" },
2231
+ ];
2232
+ </script>
2233
+
2234
+ <template>
2235
+ <UiConsole
2236
+ :lines="output"
2237
+ :line-delay="350"
2238
+ :start-delay="200"
2239
+ title="deploy"
2240
+ cursor
2241
+ @complete="onComplete"
2242
+ />
2243
+ </template>
2244
+ ```
2245
+
2246
+ Line and segment tones are `default`, `muted`, `primary`, `info`, `success`, `warning`, and `error`. Set `highlight: true` for a subtle background highlight. Content is rendered as text rather than HTML; for custom markup use the scoped `line` slot, which receives `{ line, index }`. The `prefix` slot receives the same scope.
2247
+
2248
+ Principal props:
2249
+
2250
+ | Prop | Type | Default | Purpose |
2251
+ | ------------------------- | ------------------------------ | ----------------- | ------------------------------------------------------------------------ |
2252
+ | `lines` | `Array<string \| ConsoleLine>` | Required | Console output in display order |
2253
+ | `autoplay` | `boolean` | `true` | Animate the sequence after mount; when false, show all lines immediately |
2254
+ | `lineDelay` | `number` | `400` | Default wait before each line after the first |
2255
+ | `startDelay` | `number` | `0` | Wait before starting the sequence |
2256
+ | `loop` / `loopDelay` | `boolean` / `number` | `false` / `1200` | Replay the sequence and wait between runs |
2257
+ | `chrome` | `boolean` | `true` | Show the console header and window controls |
2258
+ | `title` | `string` | Empty | Optional centered header title |
2259
+ | `cursor` | `boolean` | `false` | Show a blinking cursor while the sequence is running |
2260
+ | `autoScroll` | `boolean` | `true` | Keep the newest output visible when the body overflows |
2261
+ | `minHeight` / `maxHeight` | CSS length | `12rem` / `32rem` | Responsive body height limits |
2262
+ | `ariaLabel` | `string` | `Console output` | Accessible name for the live log |
2263
+ | `ui` | `object` | `{}` | Classes for `root`, `header`, `body`, `line`, and `cursor` |
2264
+
2265
+ Events are `line-show(line, index)`, `line-hide(line, index)`, and `complete`. Exposed methods are `play()`, `reset()`, `showAll()`, and `scrollToLatest()`.
2266
+
2267
+ SSR always renders the complete final output. Crawlers, no-JS clients, and the original HTML therefore receive every console line. When the existing early head script changes `html.nojs` to `html.js`, CSS hides every pending line except the first without removing anything from the document. After mount, the component starts its client-side timeline and controls visibility reactively. This avoids a hydration mismatch while keeping the complete semantic HTML available for SEO.
2268
+
2269
+ When `cursor` is enabled, it occupies its own new line and remains visible after the sequence completes, ready for the next simulated command.
2270
+
2271
+ The default palette follows GitHub's light and dark code colors and switches using the project theme. Individual colors can be overridden with `--ui-console-bg`, `--ui-console-header-bg`, `--ui-console-text`, `--ui-console-muted`, `--ui-console-border`, `--ui-console-primary`, `--ui-console-info`, `--ui-console-success`, `--ui-console-warning`, and `--ui-console-error`. Visual transitions and cursor blinking are disabled by `prefers-reduced-motion`.
2272
+
2273
+ `UiSkeleton` can be sized with Tailwind classes like `USkeleton`, or with its `width` and `height` props:
2274
+
2275
+ ```vue
2276
+ <div class="space-y-3">
2277
+ <UiSkeleton class="h-5 w-2/3" />
2278
+ <UiSkeleton class="h-4 w-full" rounded="sm" />
2279
+ <UiSkeleton :width="48" :height="48" rounded="full" />
2280
+ </div>
2281
+ ```
2282
+
2283
+ Numeric dimensions are pixels; strings accept any CSS length. The default skeleton is decorative and hidden from assistive technology. Set `aria-label="Loading profile"` when it should be announced as a status. Set `:animated="false"` for a static placeholder. Its color is derived from `--ui-bg` and `--ui-text`, and pulse animation is automatically disabled by `prefers-reduced-motion`.
2284
+
2285
+ #### UiTabs
2286
+
2287
+ `UiTabs` is vertical by default. `side="left"` places the tab list on the left and its content on the right; `side="right"` reverses them. Bind the selected item with `v-model` and pass a stable `value` for each item:
2288
+
2289
+ ```vue
2290
+ <script setup lang="ts">
2291
+ const activeTab = ref("overview");
2292
+ const tabs = [
2293
+ { label: "Overview", value: "overview", content: "General information" },
2294
+ { label: "Delivery", value: "delivery", content: "Delivery conditions" },
2295
+ { label: "Archive", value: "archive", disabled: true },
2296
+ ];
2297
+ </script>
2298
+
2299
+ <template>
2300
+ <UiTabs
2301
+ v-model="activeTab"
2302
+ :items="tabs"
2303
+ side="left"
2304
+ tab-width="auto"
2305
+ align="start"
2306
+ tabs-width="14rem"
2307
+ >
2308
+ <template #panel="{ item }">
2309
+ <article>{{ item.content }}</article>
2310
+ </template>
2311
+ </UiTabs>
2312
+ </template>
2313
+ ```
2314
+
2315
+ For tabs above or below the content, use horizontal orientation:
2316
+
2317
+ ```vue
2318
+ <UiTabs
2319
+ :items="tabs"
2320
+ orientation="horizontal"
2321
+ side="top"
2322
+ tab-width="auto"
2323
+ align="center"
2324
+ />
2325
+ ```
2326
+
2327
+ Props:
2328
+
2329
+ | Prop | Type | Default | Purpose |
2330
+ | --------------- | -------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------- |
2331
+ | `items` | `TabItem[]` | Required | Tabs containing `label`, optional `value`, fallback `content`, `disabled`, and any custom slot data |
2332
+ | `modelValue` | `string \| number` | — | Active item value; normally bound with `v-model` |
2333
+ | `defaultValue` | `string \| number` | First enabled item | Initial value for uncontrolled use |
2334
+ | `orientation` | `vertical \| horizontal` | `vertical` | Put the list beside the content or above/below it |
2335
+ | `side` | `left \| right \| top \| bottom` | `left` or `top` | List position; vertical accepts left/right and horizontal accepts top/bottom |
2336
+ | `tabWidth` | `full \| auto` | `full` | Fill the list width (or share a horizontal row) or use each heading's natural width |
2337
+ | `align` | `start \| center \| end` | `start` | Align tab headings within the tab-list area |
2338
+ | `tabsWidth` | CSS length | `16rem` | Width reserved for the list in vertical mode |
2339
+ | `loop` | `boolean` | `true` | Wrap keyboard navigation from the last enabled tab to the first and back |
2340
+ | `stackOnMobile` | `boolean` | `true` | Below 768 px, move a vertical list above the panel and make it horizontally scrollable |
2341
+ | `ariaLabel` | `string` | `Tabs` | Accessible name of the tab list |
2342
+ | `ui` | `object` | `{}` | Classes for `root`, `list`, `tab`, `activeTab`, and `panel` |
2343
+
2344
+ The `tab` slot receives `{ item, index, active, disabled }`; the `panel` slot receives `{ item, index, value }`. Without slots, labels and `content` are rendered as text. `change` emits `(value, item, index)`, alongside the standard `update:modelValue` event.
2345
+
2346
+ Arrow keys follow the selected orientation, while `Home` and `End` select the first or last enabled tab. The panel uses a short fade-and-slide transition; `prefers-reduced-motion` disables it. Colors are derived from the theme's `--ui-bg` and `--ui-text` variables.
2347
+
2348
+ #### UiFileUpload
2349
+
2350
+ `UiFileUpload` uses the same `v-model` pattern as the other form controls. A single field returns `File | null`; `multiple` returns `File[]`. Its native input retains `name`, `required`, `accept`, `capture`, and `disabled`, and is synchronized so `new FormData(form)` includes the current files.
2351
+
2352
+ Complete multiple-file form example:
2353
+
2354
+ ```vue
2355
+ <script setup lang="ts">
2356
+ const attachments = ref<File[]>([]);
2357
+ const uploadError = ref("");
2358
+
2359
+ const rejectFiles = (items: Array<{ file: File; reasons: string[] }>) => {
2360
+ uploadError.value = items
2361
+ .map(({ file, reasons }) => `${file.name}: ${reasons.join(", ")}`)
2362
+ .join("; ");
2363
+ };
2364
+
2365
+ const submit = async (event: Event) => {
2366
+ const form = event.currentTarget as HTMLFormElement;
2367
+ await useApi().post("/contact", new FormData(form));
2368
+ };
2369
+ </script>
2370
+
2371
+ <template>
2372
+ <form @submit.prevent="submit">
2373
+ <UiFileUpload
2374
+ v-model="attachments"
2375
+ name="attachments[]"
2376
+ label="Attachments"
2377
+ description="PDF or images, up to 5 MB each"
2378
+ accept="application/pdf,image/*"
2379
+ :max-size="5 * 1024 * 1024"
2380
+ :max-files="4"
2381
+ :error="uploadError"
2382
+ multiple
2383
+ required
2384
+ @reject="rejectFiles"
2385
+ />
2386
+ <UiButton type="submit">Send</UiButton>
2387
+ </form>
2388
+ </template>
2389
+ ```
2390
+
2391
+ For a single file, use `const avatar = ref<File | null>(null)` and omit `multiple`.
2392
+
2393
+ Props:
2394
+
2395
+ | Prop | Type | Default | Purpose |
2396
+ | ------------- | -------------------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------- |
2397
+ | `modelValue` | `File \| File[] \| null` | `null` | Selected file for a single field or files for `multiple`; normally bound with `v-model` |
2398
+ | `id` | `string` | Generated | Native input id and base for accessible description/error ids |
2399
+ | `name` | `string` | — | Native form field name; use a server-compatible name such as `attachments[]` for multiple files |
2400
+ | `accept` | `string` | — | Comma-separated MIME types, wildcards, or extensions, for example `image/*,.pdf` |
2401
+ | `multiple` | `boolean` | `false` | Switch the model and native input to multiple-file mode |
2402
+ | `required` | `boolean` | `false` | Apply native required-field validation |
2403
+ | `disabled` | `boolean` | `false` | Disable selection, dropping, removal, and keyboard interaction |
2404
+ | `capture` | `boolean \| user \| environment` | — | Pass the native mobile capture hint to the file input |
2405
+ | `dropzone` | `boolean` | `true` | Enable drag-and-drop; `false` also renders the picker in its compact form |
2406
+ | `interactive` | `boolean` | `true` | Make the whole picker clickable and keyboard-operable; the actions button remains available when `false` |
2407
+ | `preview` | `boolean` | `true` | Render selected files and image object-URL thumbnails |
2408
+ | `append` | `boolean` | `true` | Append new selections in multiple mode; set `false` to replace the array |
2409
+ | `maxSize` | `number` | — | Maximum size of each file in bytes |
2410
+ | `maxFiles` | `number` | — | Maximum accepted file count in multiple mode |
2411
+ | `label` | `string` | `Upload files` | Main picker label |
2412
+ | `description` | `string` | Empty | Supporting instructions; linked to the native input with `aria-describedby` |
2413
+ | `selectText` | `string` | `Select files` | Default action text |
2414
+ | `emptyText` | `string` | `or drag and drop them here` | Default dropzone hint when no custom description is provided |
2415
+ | `layout` | `list \| grid` | `list` | Selected-file presentation; `grid` is useful for image previews |
2416
+ | `error` | `boolean \| string` | `false` | Invalid styling; a string is also rendered as an accessible error message |
2417
+
2418
+ Events:
2419
+
2420
+ | Event | Payload | When it fires |
2421
+ | ------------------- | ------------------------------------------ | ------------------------------------------------------------------------- |
2422
+ | `update:modelValue` | `File \| File[] \| null` | Selection, removal, clear, or native form reset updates the model |
2423
+ | `change` | `File \| File[] \| null` | The committed value changes, including clear and form reset |
2424
+ | `reject` | `Array<{ file: File; reasons: Reason[] }>` | One or more incoming files fail validation |
2425
+ | `remove` | `file: File, index: number` | A specific selected file is removed |
2426
+ | `clear` | None | `clear()` or `removeFile()` without an index clears a non-empty selection |
2427
+
2428
+ `Reason` is one of `type`, `size`, `count`, or `duplicate`. Valid files in a mixed selection are still accepted; rejected files are reported separately.
2429
+
2430
+ Scoped slots:
2431
+
2432
+ | Slot | Scope | Purpose |
2433
+ | -------------- | ---------------------------------------------- | ---------------------------------------------------------------------------- |
2434
+ | `default` | `{ files, open, removeFile, clear, dragging }` | Replace the entire visible picker/dropzone content |
2435
+ | `leading` | — | Replace the default upload icon |
2436
+ | `label` | — | Replace the label markup |
2437
+ | `description` | — | Replace instructions or file constraints |
2438
+ | `actions` | `{ files, open, removeFile }` | Replace the select-file action |
2439
+ | `files-top` | `{ files, removeFile, clear }` | Insert content above the selected-file collection |
2440
+ | `files` | `{ files, removeFile, clear }` | Replace the complete selected-file collection |
2441
+ | `file` | `{ file, index, removeFile, previewUrl }` | Replace one selected-file row/card; `previewUrl` exists for supported images |
2442
+ | `files-bottom` | `{ files, removeFile, clear }` | Insert controls or status below the selected-file collection |
2443
+
2444
+ Exposed methods and refs:
2445
+
2446
+ | Member | Signature/value | Purpose |
2447
+ | ------------ | -------------------------- | ----------------------------------------------------------------------------- |
2448
+ | `open` | `() => void` | Open the native file chooser |
2449
+ | `removeFile` | `(index?: number) => void` | Remove a file by index, or clear all when omitted |
2450
+ | `clear` | `() => void` | Clear all selected files |
2451
+ | `input` | Native input template ref | Access the underlying `<input type="file">` when integration code requires it |
2452
+
2453
+ The picker listens to its parent form's native `reset` event and clears the model automatically. Image preview object URLs are revoked when files are removed or the component unmounts. Colors come from `--ui-bg` and `--ui-text`, while validation uses `--ui-error` with a fallback. Motion transitions are disabled under `prefers-reduced-motion`.
2454
+
2455
+ `useApi().post()` and `useApi().put()` accept the resulting `FormData`. The request helper removes its JSON content type for `FormData`, allowing the browser to generate the required multipart boundary. Do not set `Content-Type` manually for these requests.
2456
+
2457
+ Example form and virtual table:
2458
+
2459
+ ```vue
2460
+ <script setup lang="ts">
2461
+ const name = ref("");
2462
+ const filters = ref<string[]>([]);
2463
+ const rows = ref([{ id: 1, profile: { name: "Ada" } }]);
2464
+ </script>
2465
+
2466
+ <template>
2467
+ <UiInput v-model="name" label="Name" icon="heroicons:user" />
2468
+ <UiCheckbox
2469
+ v-model="filters"
2470
+ legend="Features"
2471
+ :options="[
2472
+ { label: 'SSR', value: 'ssr' },
2473
+ { label: 'i18n', value: 'i18n' },
2474
+ ]"
2475
+ />
2476
+ <UiTable
2477
+ height="480px"
2478
+ :items="rows"
2479
+ :columns="[
2480
+ { key: 'id', label: 'ID', sortable: true },
2481
+ { key: 'profile.name', label: 'Name', sortable: true },
2482
+ ]"
2483
+ />
2484
+ </template>
2485
+ ```
2486
+
2487
+ Additional component examples:
2488
+
2489
+ ```vue
2490
+ <UiAlert
2491
+ variant="success"
2492
+ title="Saved"
2493
+ description="Your changes are live."
2494
+ closable
2495
+ />
2496
+ <UiBadge label="New" color="blue" variant="soft" />
2497
+ <UiBanner
2498
+ title="Scheduled maintenance"
2499
+ description="Sunday at 02:00 UTC"
2500
+ closable
2501
+ />
2502
+ <UiChip text="3" color="red"><UiAvatar name="Ada Lovelace" /></UiChip>
2503
+ <UiCommandPalette />
2504
+ <UiCursorCreative />
2505
+ <UiFeatureAccordion :items="features" default-image="/images/feature.webp" />
2506
+ <UiIcon8 icon="github" type="color" :size="48" />
2507
+ <UiLightBox :images="['/gallery/one.webp', '/gallery/two.webp']" />
2508
+ <UiLocalTime timezone="Europe/Warsaw" label="Warsaw" show-date />
2509
+ <UiPopup id="welcome-offer" title="Welcome" trigger-delay="1500" />
2510
+ <UiRelativeTime :date="publishedAt" />
2511
+ <UiScheduledContent
2512
+ from="2026-12-01T00:00:00Z"
2513
+ to="2026-12-31T23:59:59Z"
2514
+ >Holiday offer</UiScheduledContent>
2515
+ <UiScrollToTop />
2516
+ <UiSegmentedControl v-model="period" :options="periodOptions" />
2517
+ <UiSmartContrast
2518
+ src="/images/cover.webp"
2519
+ height="70vh"
2520
+ ><UiHeading :level="1">Readable cover</UiHeading></UiSmartContrast>
2521
+ <UiSnapContainer
2522
+ direction="vertical"
2523
+ ><UiSnapSection id="intro" bg-image="/images/intro.webp">Intro</UiSnapSection></UiSnapContainer>
2524
+ <UiSplitSection
2525
+ image-src="/images/team.webp"
2526
+ image-alt="Our team"
2527
+ ><template #text>Team story</template></UiSplitSection>
2528
+ <UiStickyWrapper :offset="96">Sticky navigation</UiStickyWrapper>
2529
+ <UiTooltip placement="top" trigger="hover">
2530
+ <span class="underline decoration-dotted">Point at this phrase</span>
2531
+ <template #content>Short explanatory text</template>
2532
+ </UiTooltip>
2533
+ <UiTooltip trigger="click" placement="bottom-start" role="dialog">
2534
+ <img src="/images/help.webp" alt="Help" class="size-10 rounded-full" />
2535
+ <template #content="{ close }">
2536
+ <div class="space-y-3">
2537
+ <strong>Any Vue content can go here</strong>
2538
+ <UiButton size="sm" @click="close">Got it</UiButton>
2539
+ </div>
2540
+ </template>
2541
+ </UiTooltip>
2542
+ <UiTextarea
2543
+ v-model="message"
2544
+ id="message"
2545
+ label="Message"
2546
+ buttons-position="outside"
2547
+ @save="submit"
2548
+ />
2549
+ <UiTextColumn
2550
+ text-size-class="text-lg"
2551
+ >Long-form readable content</UiTextColumn>
2552
+ <UiToggler v-model="enabled" label="Enable notifications" />
2553
+ <UiTruncateText
2554
+ :mobile-limit="120"
2555
+ :desktop-limit="300"
2556
+ >Long text to truncate…</UiTruncateText>
2557
+ <UiViewportSpacer size="20" mobile-size="12" />
2558
+ ```
2559
+
2560
+ ### Content and view components
2561
+
2562
+ | Component | Purpose and principal props/events |
2563
+ | -------------------------- | ------------------------------------------------------------------------------------------------------- |
2564
+ | `ViewAnimatedCounter` | Intersection-driven counter: from/to, duration, decimals, prefix/suffix, separator, once. |
2565
+ | `ViewAnnouncementBar` | Sticky/closable colored announcement with title/default/actions slots. |
2566
+ | `ViewAvatarGroup` | Overlapping `UiAvatar` children with size, max count, and remainder badge. |
2567
+ | `ViewBreadcrumbs` | Localized generated breadcrumbs with truncation and separator. |
2568
+ | `ViewContentWithToc` | Builds an anchor navigation from slotted h2/h3/h4/heading components. |
2569
+ | `ViewCopyright` | Owner, optional start year, and localized “all rights reserved”. |
2570
+ | `ViewCountdownTimer` | Timezone-aware target countdown; exposes values to a slot and emits `finish`. |
2571
+ | `ViewCreativeCTA` | Image/gradient CTA with alignment, two links, and title/description/actions slots. |
2572
+ | `ViewCurrencySelect` | Cart-backed currency picker with class overrides and trigger slot. |
2573
+ | `ViewFeatureBoxGrid` | Linked feature cards with icons, 2–4 columns, layout direction, and description alignment. |
2574
+ | `ViewHorizontalMenu` | Horizontally scrollable route menu with edge fade masks. |
2575
+ | `ViewImageCompare` | Mouse/touch before-after image slider with optional labels. |
2576
+ | `ViewImgSlider` | Image/video carousel with autoplay interval, manual controls, and vertical mode. |
2577
+ | `ViewInfiniteMarquee` | Seamless repeated string items with duration, delay, and container class. |
2578
+ | `ViewInfoBar` | Compact information items and actions with icons/callbacks. |
2579
+ | `ViewLogoGrid` | Linked logos with color/grayscale variants and mobile item limit. |
2580
+ | `ViewLogoSlider` | Infinite logo strip with speed, grayscale, and container class. |
2581
+ | `ViewPartialBackground` | Full-width themed background with independent content width and left/right edge heights. |
2582
+ | `ViewQuoteBig` | Large quotation with author/avatar, alignment, and typography controls. |
2583
+ | `ViewResponsiveMenu` | Measures available width and moves overflowed route items into a dropdown. |
2584
+ | `ViewReviewsSlider` | Responsive draggable review carousel with autoplay and links. |
2585
+ | `ViewSocialBar` | Iconify social links with five variants, size, gap, target, and per-item color. |
2586
+ | `ViewStickyHeadingContent` | Two-column section with a configurable sticky `UiHeading` beside scrolling content. |
2587
+ | `ViewTallyForm` | Tally standard embed, popup, or fullscreen mode; hidden fields, theme colors/options; exposed `open()`. |
2588
+ | `ViewWarningOutdated` | Full-screen unsupported-browser warning; mounted globally when core APIs are missing. |
2589
+
2590
+ ```vue
2591
+ <script setup lang="ts">
2592
+ const tally = ref<{ open: () => void }>();
2593
+ </script>
2594
+
2595
+ <template>
2596
+ <ViewAnimatedCounter :to="250000" suffix="+" :duration="1800" />
2597
+
2598
+ <ViewTallyForm
2599
+ ref="tally"
2600
+ form-id="abc123"
2601
+ mode="popup"
2602
+ :hidden-fields="{ plan: 'pro' }"
2603
+ :options="{ hideTitle: true }"
2604
+ >
2605
+ <UiButton label="Contact us" @click="tally?.open()" />
2606
+ </ViewTallyForm>
2607
+ </template>
2608
+ ```
2609
+
2610
+ Additional view examples:
2611
+
2612
+ ```vue
2613
+ <ViewAnnouncementBar title="Version 2 is available" sticky closable />
2614
+ <ViewAvatarGroup
2615
+ :max="4"
2616
+ ><UiAvatar v-for="person in team" :key="person.id" :src="person.avatar" /></ViewAvatarGroup>
2617
+ <ViewContentWithToc><article><h2>Installation</h2><p>...</p></article></ViewContentWithToc>
2618
+ <ViewCopyright owner="MyelophOne" :start-year="2024" />
2619
+ <ViewCountdownTimer target-date="2027-01-01T00:00:00Z" timezone="UTC" />
2620
+ <ViewCreativeCTA
2621
+ title="Start building"
2622
+ primary-text="Get started"
2623
+ primary-link="/start"
2624
+ />
2625
+ <ViewCurrencySelect />
2626
+ <ViewFeatureBoxGrid :items="features" :columns="3" />
2627
+ <ViewHorizontalMenu
2628
+ :items="[
2629
+ { label: 'Overview', path: '/' },
2630
+ { label: 'Docs', path: '/docs' },
2631
+ ]"
2632
+ />
2633
+ <ViewImageCompare
2634
+ before-image="/before.webp"
2635
+ after-image="/after.webp"
2636
+ before-label="Before"
2637
+ after-label="After"
2638
+ />
2639
+ <ViewImgSlider
2640
+ :media="slides"
2641
+ :auto-scroll="true"
2642
+ :auto-scroll-interval="5000"
2643
+ />
2644
+ <ViewInfiniteMarquee :items="['Nuxt', 'Vue', 'Tailwind']" :duration="24" />
2645
+ <ViewInfoBar :items="contactItems" :actions="contactActions" />
2646
+ <ViewLogoGrid title="Trusted by teams" :items="logos" />
2647
+ <ViewLogoSlider :logos="logos" :speed="30" grayscale />
2648
+ <ViewPartialBackground
2649
+ container-size="boxed"
2650
+ background="url('/images/hero.webp') center / cover no-repeat"
2651
+ background-dark="linear-gradient(rgb(0 0 0 / 35%), rgb(0 0 0 / 35%)), url('/images/hero.webp') center / cover no-repeat"
2652
+ background-height-left="58dvh"
2653
+ background-height-right="66dvh"
2654
+ min-height="80dvh"
2655
+ content-align="center"
2656
+ content-class="py-16"
2657
+ >
2658
+ <GridRow>
2659
+ <GridCol :span="8">Content over the partial background</GridCol>
2660
+ </GridRow>
2661
+ </ViewPartialBackground>
2662
+
2663
+ <ViewPartialBackground
2664
+ background-after-content
2665
+ background-after-content-height="16dvh"
2666
+ background-height-left="100%"
2667
+ background-height-right="72%"
2668
+ background-color="#f59e0b"
2669
+ background-color-dark="#7c3aed"
2670
+ >
2671
+ <GridRow>
2672
+ <GridCol>Content keeps its natural height; the image starts below it.</GridCol>
2673
+ </GridRow>
2674
+ </ViewPartialBackground>
2675
+
2676
+ <ViewPartialBackground
2677
+ background-before-content
2678
+ background-before-content-height="14dvh"
2679
+ background-height-left="70%"
2680
+ background-height-right="100%"
2681
+ background-color="#38bdf8"
2682
+ background-color-dark="#075985"
2683
+ >
2684
+ <GridRow>
2685
+ <GridCol>The angled background starts before this content.</GridCol>
2686
+ </GridRow>
2687
+ </ViewPartialBackground>
2688
+ <ViewQuoteBig text="A product-defining result." author-name="Ada Lovelace" />
2689
+ <ViewResponsiveMenu :items="navigation" />
2690
+ <ViewReviewsSlider :items="reviews" />
2691
+ <ViewSocialBar :items="socialLinks" variant="rounded" size="lg" />
2692
+ <ViewWarningOutdated />
2693
+ ```
2694
+
2695
+ #### `ViewPartialBackground`
2696
+
2697
+ `ViewPartialBackground` is a full-width section built with `GridStack` and `GridContainer`. Without a minimum height, its content area has natural content-driven height. The background edge and the content width are controlled independently.
2698
+
2699
+ | Prop | Values/default | Purpose |
2700
+ | ---------------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------ |
2701
+ | `containerSize` | Any `GridContainer` size; default `content` | Width of the inner content |
2702
+ | `minHeight` | Any CSS size; unset by default | Optional minimum content height |
2703
+ | `contentAlign` | `top`, `center`, `bottom`; default `top` | Vertical content alignment when `minHeight` creates free space |
2704
+ | `backgroundHeightLeft`, `backgroundHeightRight` | Any CSS size; default `60dvh` | Visible background height at each side; different values create an angled edge |
2705
+ | `backgroundBeforeContent` | `boolean` | Add an angled background extension before the content |
2706
+ | `backgroundBeforeContentHeight` | Any CSS size; default `clamp(5rem, 12dvh, 10rem)` | Total height reserved for the extension before content |
2707
+ | `backgroundAfterContent` | `boolean` | Add an angled background extension after the content |
2708
+ | `backgroundAfterContentHeight` | Any CSS size; default `clamp(5rem, 12dvh, 10rem)` | Total height reserved for the extension after content |
2709
+ | `backgroundColor`, `backgroundColorDark` | Any CSS color | Shared light/dark color for the content area and before/after extensions |
2710
+ | `background`, `backgroundDark` | Any CSS `background` value | Light/dark color, gradient, or image on the decorative layer |
2711
+ | `backgroundSize`, `backgroundPosition`, `backgroundRepeat` | CSS background values | Decorative background rendering |
2712
+ | `mask`, `maskDark` | Any CSS `mask` value | Light/dark mask on the decorative layer |
2713
+ | `maskSize`, `maskPosition`, `maskRepeat` | CSS mask values | Decorative mask rendering |
2714
+ | `contentClass`, `layerClass` | CSS/Tailwind class string | Inner container and decorative layer customization |
2715
+
2716
+ Both before/after heights accept `px`, `rem`, `vh`, `dvh`, `clamp()`, `min()`, `max()`, CSS variables, and other valid CSS lengths. Prefer `clamp()` for a responsive height with safe limits:
2717
+
2718
+ ```vue
2719
+ <ViewPartialBackground
2720
+ background-after-content
2721
+ background-after-content-height="clamp(4rem, 10dvh, 8rem)"
2722
+ background-height-left="100%"
2723
+ background-height-right="70%"
2724
+ background-color="#f59e0b"
2725
+ background-color-dark="#7c3aed"
2726
+ >
2727
+ Content
2728
+ </ViewPartialBackground>
2729
+ ```
2730
+
2731
+ The values are reactive. Bind a ref or computed CSS value to change the height at runtime:
2732
+
2733
+ ```vue
2734
+ <script setup lang="ts">
2735
+ const lowerBackgroundHeight = ref("8rem");
2736
+ </script>
2737
+
2738
+ <template>
2739
+ <ViewPartialBackground
2740
+ background-after-content
2741
+ :background-after-content-height="lowerBackgroundHeight"
2742
+ >
2743
+ Content
2744
+ </ViewPartialBackground>
2745
+ </template>
2746
+ ```
2747
+
2748
+ When `backgroundBeforeContent` and `backgroundAfterContent` are both enabled, both extensions are rendered and their heights remain independent. The `background-before` and `background-after` slots can customize them separately; the shared `background` slot is used as fallback for either side.
2749
+
2750
+ #### `ViewStickyHeadingContent`
2751
+
2752
+ `ViewStickyHeadingContent` uses `GridContainer`, `GridRow`, and `GridCol` to place a sticky `UiHeading` beside a content column. The heading remains sticky only inside the height of the adjacent content. On mobile, the heading stays first in document order even when it is configured on the right for desktop.
2753
+
2754
+ ```vue
2755
+ <ViewStickyHeadingContent
2756
+ title="Forming lasting partnerships"
2757
+ heading-side="left"
2758
+ :heading-level="2"
2759
+ heading-size="xl"
2760
+ :heading-span="5"
2761
+ :content-span="7"
2762
+ sticky-from="md"
2763
+ sticky-offset="5rem"
2764
+ container-size="boxed"
2765
+ container-class="py-20"
2766
+ content-class="space-y-8 text-xl leading-relaxed"
2767
+ >
2768
+ <p>First long content block…</p>
2769
+ <p>More content that scrolls beside the sticky heading…</p>
2770
+ </ViewStickyHeadingContent>
2771
+ ```
2772
+
2773
+ Main props:
2774
+
2775
+ | Prop | Values/default | Purpose |
2776
+ | ------------------------------------------ | ----------------------------------------------------- | ---------------------------------------------------------------- |
2777
+ | `title` | `string` | Default heading text; replace with the `heading` slot for markup |
2778
+ | `headingSide` | `left`, `right`; default `left` | Desktop heading side; the heading remains first on mobile |
2779
+ | `headingLevel` | `1`–`6`; default `2` | Semantic heading element passed to `UiHeading` |
2780
+ | `headingSize` | `xs`, `sm`, `md`, `lg`, `xl`, `display`; default `lg` | Visual size passed independently to `UiHeading` |
2781
+ | `headingSpan` | `1`–`12`; default `5` | Desktop heading-column width |
2782
+ | `contentSpan` | `1`–`12`; default `7` | Desktop content-column width |
2783
+ | `containerSize` | Any `GridContainer` size; default `content` | Overall content width |
2784
+ | `sticky` | `boolean`; default `true` | Enable or disable sticky positioning |
2785
+ | `stickyFrom` | `always`, `sm`, `md`, `lg`; default `lg` | Breakpoint from which sticky positioning applies |
2786
+ | `stickyOffset` | Any CSS length; default `6rem` | Distance from the top of the viewport while sticky |
2787
+ | `containerClass`, `rowClass` | Class strings | Grid container and row customization |
2788
+ | `headingColumnClass`, `contentColumnClass` | Class strings | Individual grid-column customization |
2789
+ | `headingClass`, `contentClass` | Class strings | Heading and content typography/customization |
2790
+
2791
+ Slots:
2792
+
2793
+ | Slot | Purpose |
2794
+ | --------------- | -------------------------------------------------- |
2795
+ | `heading` | Heading contents rendered inside `UiHeading` |
2796
+ | `heading-after` | Optional content directly below the sticky heading |
2797
+ | `default` | Main scrolling content column |
2798
+
2799
+ Set `heading-side="right"` to swap the desktop columns. Set `:sticky="false"` when the same layout is needed without sticky behaviour.
2800
+
2801
+ ### Safe media, consent, SEO, and shell components
2802
+
2803
+ | Component | Purpose |
2804
+ | -------------------------------------- | ---------------------------------------------------------------------------------------------- |
2805
+ | `SafeNuxtImg` | Uses a plain `<img>` in static mode and `NuxtImg` otherwise. |
2806
+ | `SafeNuxtPicture` | NuxtPicture wrapper that avoids adding an error handler unless provided. |
2807
+ | `SafeImgWithLoader` | Intersection-lazy image with AVIF/WebP optimization, loader, and `/img/image404.png` fallback. |
2808
+ | `SafeEmail` / `UiProtectedEmail` | Client-assembled email address to reduce simple scraping. |
2809
+ | `ConsentYoutube` | Marketing-consent-gated YouTube embed. |
2810
+ | `ConsentGoogleMap` | Functional-consent-gated address/place-id Google Map. |
2811
+ | `CookieConsentWrapper` | Generic category-gated slot and preference prompt. |
2812
+ | `CookiePrivacyPolicy` | Generated provider/category/legal-source disclosure. |
2813
+ | `CookieBanner` / `CookieSettingsModal` | Global consent UI; already mounted by the base app. |
2814
+ | `SeoProfileInfo` | SSR ProfilePage JSON-LD with a Person or Organization main entity. |
2815
+ | `SeoOrgInfo` | SSR Organization/subtype JSON-LD with common props and unrestricted data pass-through. |
2816
+ | `SeoBrandInfo` | SSR Brand JSON-LD with ratings, identity fields, and unrestricted data pass-through. |
2817
+ | `SeoNoIndex` / `SeoContentNoIndex` | Page and content indexing controls. |
2818
+ | `PagePreloader` | Route-loading overlay driven by the settings store. |
2819
+ | `PageFullscreenPreloader` | Full-viewport themed overlay with centered arbitrary slot content. |
2820
+ | `MyelophoneWelcome` | Default framework playground landing page. |
2821
+ | `MyelophoneCopyright` | Server-rendered copyright island used by the framework welcome screen. |
2822
+
2823
+ ```vue
2824
+ <SafeEmail user="hello" domain="example" tld="com" subject="Website enquiry" />
2825
+ <SafeImgWithLoader
2826
+ src="/images/product.webp"
2827
+ alt="Product"
2828
+ width="1200"
2829
+ height="800"
2830
+ />
2831
+ <SafeNuxtPicture src="/images/hero.webp" alt="Hero" sizes="100vw md:1080px" />
2832
+ <CookieBanner />
2833
+ <CookieSettingsModal />
2834
+ <PagePreloader />
2835
+ <PageFullscreenPreloader>
2836
+ <BrandLoader />
2837
+ </PageFullscreenPreloader>
2838
+ <MyelophoneWelcome />
2839
+ <NuxtIsland name="MyelophoneCopyright" />
2840
+ ```
2841
+
2842
+ > **_NOTE:_** The `myelophone.noindex` option (when true) disables site search functionality.
2843
+
2844
+ ### Full-screen page preloader
2845
+
2846
+ `PageFullscreenPreloader` is independent from the existing `PagePreloader`. The existing component keeps its original store-driven purpose. The new component covers the viewport and centers arbitrary slot content such as an SVG logo, icon, text, GIF, animation, or a custom loader component.
2847
+
2848
+ By default, no global full-screen preloader is enabled. To use one for route transitions across the whole site, configure an auto-imported component by name in the optional `myelophone.ts` file:
2849
+
2850
+ ```ts
2851
+ export default defineNuxtConfig({
2852
+ myelophone: {
2853
+ pageFullscreenPreloader: {
2854
+ component: "SitePreloaderLogo",
2855
+ props: {
2856
+ label: "Acme Studio",
2857
+ },
2858
+ background: "#f7f7f7",
2859
+ backgroundDark: "#121212",
2860
+ minimumDuration: 250,
2861
+ ariaLabel: "Loading Acme Studio",
2862
+ },
2863
+ },
2864
+ });
2865
+ ```
2866
+
2867
+ Create the configured component inside the application's `components` directory. The selected component is resolved statically during the Nuxt build, so unrelated auto-imported components are not added to the client bundle.
2868
+
2869
+ `PageFullscreenPreloaderConfiguredContent` is an internal generated wrapper used by the base app for that static import. Do not create or call it yourself; `pageFullscreenPreloader.component` is the only place where the site's own loader-content component is selected.
2870
+
2871
+ `playground/myelophone.dev.ts` is only a configuration template and is never loaded by Nuxt directly. Copy it to `playground/myelophone.ts` when testing global configuration in this repository. If `myelophone.ts` does not exist, the default configuration keeps the global full-screen overlay disabled. The local `/demo` example does not require that file.
2872
+
2873
+ Configuration fields:
2874
+
2875
+ | Field | Values/default | Purpose |
2876
+ | ------------------------------ | --------------------------------------- | ----------------------------------------------------------------------------- |
2877
+ | `component` | Auto-imported component name | Centered global preloader content |
2878
+ | `props` | Serializable object | Props passed to the configured content component |
2879
+ | `transparent` | `boolean`; default `false` | Use a transparent overlay instead of its background |
2880
+ | `background`, `backgroundDark` | CSS backgrounds; default `var(--ui-bg)` | Light and dark overlay backgrounds |
2881
+ | `zIndex` | Number or CSS value; default `9999` | Overlay stacking level |
2882
+ | `ariaLabel` | `string`; default `Page loading` | Accessible status label |
2883
+ | `class`, `contentClass` | Class strings | Overlay and centered-content customization |
2884
+ | `minimumDuration` | Milliseconds; default `250` | Minimum time the overlay remains visible after hydration or navigation starts |
2885
+
2886
+ For a single page, place the component directly in that page. It automatically follows Nuxt's page-loading lifecycle, so it appears while that particular page is loading and disappears on `page:finish`:
2887
+
2888
+ ```vue
2889
+ <template>
2890
+ <PageFullscreenPreloader background="#f7f7f7" background-dark="#121212">
2891
+ <SitePreloaderLogo />
2892
+ </PageFullscreenPreloader>
2893
+ </template>
2894
+ ```
2895
+
2896
+ This declaration is local to that page; no manual `ref`, click handler, or global configuration is needed. On the first request the overlay is included next to the main SSR content, but CSS keeps it hidden for `html.nojs`. The existing inline head script changes that class to `js` before Nuxt loads, making the overlay visible without removing or covering the SSR content for no-JavaScript clients and crawlers. On a hard load it stays until `window.load` and `minimumDuration`; on internal navigation it stays until `page:finish` and `minimumDuration`.
2897
+
2898
+ Set `transparent` for a transparent full-screen interaction layer. The overlay uses `data-theme="light|dark"`, falls back to `prefers-color-scheme`, disables its fade transition under reduced-motion preferences, and accepts arbitrary slot content.
2899
+
2900
+ ## Utility functions
2901
+
2902
+ ```ts
2903
+ sortBy(items, "price", "asc");
2904
+ transliterate("Пример заголовка"); // suitable for generated anchor IDs
2905
+ getYoutubeEmbed("https://youtu.be/dQw4w9WgXcQ");
2906
+ getYoutubePoster("dQw4w9WgXcQ");
2907
+
2908
+ const refreshing = useAsyncRefresh(refresh);
2909
+ await refreshing();
2910
+ ```
2911
+
2912
+ `sortBy` supports comparable nested item keys at the type level; YouTube helpers accept IDs and common watch/embed/short URLs.
2913
+
2914
+ To validate forms @myelophonenuxt uses Valibot.
2915
+
2916
+ ## Built-in production behavior
2917
+
2918
+ - Nitro Node cluster by default, with an explicit static mode.
2919
+ - Brotli/Gzip compression for production HTML and precompressed public assets.
2920
+ - Immutable caching for hashed Nuxt assets.
2921
+ - CSS code splitting control, minification, empty/unresolved CSS cleanup, `!important` removal, and modern viewport-unit fallbacks.
2922
+ - Build banner and third-party license file at `/_nuxt/licenses.md`.
2923
+ - Lazy route/component/media behavior and selective route preloading.
2924
+ - Per-locale translation chunks and build-time unused-key removal.
2925
+ - Strict CSP with nonces, HSTS, content-type/frame/referrer/download/DNS/cross-origin/permissions headers.
2926
+ - 2 MB general request and 10 MB upload limits.
2927
+ - In-process rate limit of 150 tokens per five minutes.
2928
+ - Console log/error/debug/trace removal from production bundles.
2929
+ - Source maps on the server and development-only client source maps.
2930
+ - Restore-state, view transitions, component islands, typed pages, early hints, an inline initial SSR payload, and extracted payloads for client navigation.
2931
+ - Optional server-rendered Markdown posts with nested routes, safe HTML minification, SEO frontmatter, prerendering, and sitemap integration.
2932
+ - Accessibility shell: skip link, route announcer, reduced-motion handling, modal keyboard behavior, and semantic time output.
2933
+
2934
+ Review the security and performance defaults against each application's threat model. In particular, CORS currently permits all origins, CSRF and SRI are disabled, inline styles are allowed by CSP, and the rate-limit store is process-local.
2935
+
2936
+ ## Project structure
2937
+
2938
+ ```text
2939
+ app/
2940
+ ├── assets/css/ Global Tailwind, theme, animation, media and print CSS
2941
+ ├── components/ UI, grid, view, ready-page, consent, cookie, safe and SEO components
2942
+ ├── composables/ API, i18n, ready-page, storage, device, consent and UI state helpers
2943
+ ├── constants/ Layout maps and third-party cookie-script presets
2944
+ ├── content/ Default Markdown posts supplied by the layer
2945
+ ├── locales/ Built-in en/pl/ru namespaces
2946
+ ├── middleware/ Localized and canonical URL behavior
2947
+ ├── modules/ Blog, sitemap, multi18n, i18n tree shaking and static .htaccess
2948
+ ├── plugins/ Theme/locale/URL/reveal/version/restore integrations
2949
+ ├── server/ Nitro routes, middleware and render plugins
2950
+ ├── stores/ Settings, commands, device, optional user and cart stores
2951
+ ├── types/ Runtime config and cookie contracts
2952
+ └── utils/ Blog rendering, sorting, transliteration, async refresh and YouTube helpers
2953
+ content/ Application Markdown posts; overrides matching layer posts
2954
+ playground/ Nuxt-recommended test app for framework development
2955
+ public/ Logos, favicon and browser assets
2956
+ tests/ Playwright smoke tests
2957
+ vite/plugins/ CSS cleanup and viewport compatibility transforms
2958
+ ```
2959
+
2960
+ ## Maintainers
2961
+
2962
+ ### Development workflow
2963
+
2964
+ 1. Use the pinned Yarn release and run `yarn install --immutable`.
2965
+ 2. Make layer changes in `app/`, build plugins in `vite/plugins/`, and development test cases in `playground/`.
2966
+ 3. When a public API changes, update this README, the playground test application, runtime config types, and tests in the same change.
2967
+ 4. Run `yarn test:types`, `yarn biome:check`, `yarn build`, and `yarn playwright` before merging.
2968
+ 5. Run `yarn audit` and assess direct and transitive advisories in both runtime and release tooling.
2969
+ 6. Test both `yarn build` and `yarn generate` when changing rendering, images, payloads, routes, CSS, or server hooks.
2970
+
2971
+ ### Commit and release policy
2972
+
2973
+ Commits follow Conventional Commits and are checked with Commitlint. The `main` branch is released automatically through semantic-release and GitHub Actions:
2974
+
2975
+ - `feat:` → minor release;
2976
+ - `fix:` and `perf:` → patch release;
2977
+ - `BREAKING CHANGE:` → major release;
2978
+ - documentation, tests, refactors, build, CI, style, and chores do not release by default.
2979
+
2980
+ The release workflow updates `CHANGELOG.md` and `package.json`, publishes to GitHub Packages, creates a GitHub release, and commits release metadata back to the repository.
2981
+
2982
+ ### Compatibility checklist
2983
+
2984
+ - Keep `multi18n` route generation compatible with the unprefixed default locale.
2985
+ - Add static translation keys directly in `t()`/`tn()` calls; safelist config-driven keys.
2986
+ - Do not load analytics/marketing/functional scripts outside consent-aware APIs.
2987
+ - Treat user/cart browser persistence as convenience state, never trusted server state.
2988
+ - Keep server-only utilities (`node:fs`, build safelists, modules) out of client imports.
2989
+ - Verify CSP domains whenever adding external APIs, media, frames, fonts, or scripts.
2990
+ - Confirm proxy-aware origin/HTTPS behavior in the target hosting platform.
2991
+ - For multiple workers or replicas, move rate limits and any new shared mutable state to distributed infrastructure.
2992
+ - Preserve the public component/composable names or document breaking changes.
2993
+ - Check the latest Nuxt compatibility date before updating it.
2994
+
2995
+ ### Ownership
2996
+
2997
+ Maintained by [MyelophOne](https://github.com/MyelophOne).<br>
2998
+ Author: [Aliaksandr Ivanou](https://github.com/aleksivanou).
2999
+
3000
+ ## License
3001
+
3002
+ Copyright © 2026 Aliaksandr Ivanou. All rights reserved.
3003
+
3004
+ This project is licensed under the **PolyForm Noncommercial License 1.0.0**. Commercial use is not granted by that license. Read [LICENSE](LICENSE) before using, redistributing, or building on the project.