@myelophone/nuxt 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/.devcontainer/Dockerfile +7 -0
  2. package/.devcontainer/devcontainer.json +21 -0
  3. package/.editorconfig +43 -0
  4. package/.github/workflows/package.yml +73 -0
  5. package/.nuxtignore +6 -0
  6. package/.releaserc.js +132 -0
  7. package/.yarn/releases/yarn-4.18.0.cjs +1000 -0
  8. package/.yarnrc.yml +8 -0
  9. package/CHANGELOG.md +141 -0
  10. package/LICENSE +77 -0
  11. package/README.md +3004 -0
  12. package/app/app.vue +455 -0
  13. package/app/assets/css/animation.css +81 -0
  14. package/app/assets/css/backgrounds.css +57 -0
  15. package/app/assets/css/base.css +182 -0
  16. package/app/assets/css/index.css +9 -0
  17. package/app/assets/css/links.css +12 -0
  18. package/app/assets/css/multimedia.css +102 -0
  19. package/app/assets/css/preloader.css +68 -0
  20. package/app/assets/css/print.css +40 -0
  21. package/app/assets/css/tailwind.css +20 -0
  22. package/app/assets/css/theme.css +29 -0
  23. package/app/components/consent/GoogleMap.vue +59 -0
  24. package/app/components/consent/Youtube.vue +27 -0
  25. package/app/components/cookie/Banner.vue +70 -0
  26. package/app/components/cookie/ConsentWrapper.vue +71 -0
  27. package/app/components/cookie/PrivacyPolicy.vue +179 -0
  28. package/app/components/cookie/SettingsModal.vue +387 -0
  29. package/app/components/grid/Col.vue +262 -0
  30. package/app/components/grid/Container.vue +236 -0
  31. package/app/components/grid/Row.vue +248 -0
  32. package/app/components/grid/Stack.vue +233 -0
  33. package/app/components/myelophone/Copyright.vue +5 -0
  34. package/app/components/myelophone/Welcome.vue +107 -0
  35. package/app/components/page/FullscreenPreloader.vue +177 -0
  36. package/app/components/page/Preloader.vue +18 -0
  37. package/app/components/page/ReadyLocalized.vue +19 -0
  38. package/app/components/ready/.gitkeep +0 -0
  39. package/app/components/safe/Email.vue +52 -0
  40. package/app/components/safe/ImgWithLoader.vue +180 -0
  41. package/app/components/safe/NuxtImg.vue +25 -0
  42. package/app/components/safe/NuxtPicture.vue +29 -0
  43. package/app/components/section/.gitkeep +0 -0
  44. package/app/components/seo/BrandInfo.vue +76 -0
  45. package/app/components/seo/ContentNoIndex.vue +35 -0
  46. package/app/components/seo/NoIndex.vue +22 -0
  47. package/app/components/seo/OrgInfo.vue +130 -0
  48. package/app/components/seo/ProfileInfo.vue +137 -0
  49. package/app/components/ui/Accordion.vue +163 -0
  50. package/app/components/ui/Alert.vue +179 -0
  51. package/app/components/ui/Avatar.vue +88 -0
  52. package/app/components/ui/Badge.vue +150 -0
  53. package/app/components/ui/Banner.vue +133 -0
  54. package/app/components/ui/Button.vue +175 -0
  55. package/app/components/ui/Card.vue +66 -0
  56. package/app/components/ui/Checkbox.vue +134 -0
  57. package/app/components/ui/Chip.vue +136 -0
  58. package/app/components/ui/CommandPalette.vue +434 -0
  59. package/app/components/ui/Console.vue +583 -0
  60. package/app/components/ui/CursorCreative.vue +148 -0
  61. package/app/components/ui/FeatureAccordion.vue +165 -0
  62. package/app/components/ui/FileUpload.vue +705 -0
  63. package/app/components/ui/GeoDependent.vue +72 -0
  64. package/app/components/ui/Heading.vue +51 -0
  65. package/app/components/ui/Icon.vue +42 -0
  66. package/app/components/ui/Icon8.vue +39 -0
  67. package/app/components/ui/Input.vue +88 -0
  68. package/app/components/ui/LangLink.vue +34 -0
  69. package/app/components/ui/LangVisible.vue +33 -0
  70. package/app/components/ui/LanguageSelect.vue +202 -0
  71. package/app/components/ui/LightBox.vue +155 -0
  72. package/app/components/ui/LocalTime.vue +170 -0
  73. package/app/components/ui/Modal.vue +161 -0
  74. package/app/components/ui/Popup.vue +200 -0
  75. package/app/components/ui/PostContent.server.vue +23 -0
  76. package/app/components/ui/ProtectedEmail.vue +52 -0
  77. package/app/components/ui/RelativeTime.vue +141 -0
  78. package/app/components/ui/ScheduledContent.vue +150 -0
  79. package/app/components/ui/ScrollToTop.client.vue +48 -0
  80. package/app/components/ui/SegmentedControl.vue +148 -0
  81. package/app/components/ui/SiteSearch.vue +1191 -0
  82. package/app/components/ui/Skeleton.vue +91 -0
  83. package/app/components/ui/SmartContrast.vue +74 -0
  84. package/app/components/ui/SnapContainer.vue +222 -0
  85. package/app/components/ui/SnapSection.vue +294 -0
  86. package/app/components/ui/SonnerStyle.vue +7 -0
  87. package/app/components/ui/SplitSection.vue +99 -0
  88. package/app/components/ui/Steps.vue +858 -0
  89. package/app/components/ui/StickyWrapper.vue +46 -0
  90. package/app/components/ui/Table.vue +251 -0
  91. package/app/components/ui/Tabs.vue +451 -0
  92. package/app/components/ui/TextColumn.vue +36 -0
  93. package/app/components/ui/Textarea.vue +251 -0
  94. package/app/components/ui/Toggler.vue +40 -0
  95. package/app/components/ui/Tooltip.vue +524 -0
  96. package/app/components/ui/TruncateText.vue +105 -0
  97. package/app/components/ui/ViewportSpacer.vue +72 -0
  98. package/app/components/view/AnimatedCounter.vue +121 -0
  99. package/app/components/view/AnnouncementBar.vue +104 -0
  100. package/app/components/view/AvatarGroup.vue +104 -0
  101. package/app/components/view/Breadcrumbs.vue +97 -0
  102. package/app/components/view/ContentWithToc.vue +152 -0
  103. package/app/components/view/Copyright.vue +68 -0
  104. package/app/components/view/CountdownTimer.vue +184 -0
  105. package/app/components/view/CreativeCTA.vue +143 -0
  106. package/app/components/view/CurrencySelect.vue +254 -0
  107. package/app/components/view/FeatureBoxGrid.vue +142 -0
  108. package/app/components/view/HorizontalMenu.vue +97 -0
  109. package/app/components/view/ImageCompare.vue +141 -0
  110. package/app/components/view/ImgSlider.vue +333 -0
  111. package/app/components/view/InfiniteMarquee.vue +147 -0
  112. package/app/components/view/InfoBar.vue +86 -0
  113. package/app/components/view/LogoGrid.vue +106 -0
  114. package/app/components/view/LogoSlider.vue +83 -0
  115. package/app/components/view/NotFound.vue +81 -0
  116. package/app/components/view/PartialBackground.vue +313 -0
  117. package/app/components/view/QuoteBig.vue +111 -0
  118. package/app/components/view/ResponsiveMenu.vue +219 -0
  119. package/app/components/view/ReviewsSlider.vue +230 -0
  120. package/app/components/view/SocialBar.vue +78 -0
  121. package/app/components/view/StickyHeadingContent.vue +158 -0
  122. package/app/components/view/TallyForm.vue +295 -0
  123. package/app/components/view/WarningOutdated.vue +113 -0
  124. package/app/composables/useApi.ts +88 -0
  125. package/app/composables/useAppSeo.ts +78 -0
  126. package/app/composables/useBreadcrumbs.ts +106 -0
  127. package/app/composables/useChangeUrl.ts +73 -0
  128. package/app/composables/useCommand.ts +28 -0
  129. package/app/composables/useCookieControl.ts +111 -0
  130. package/app/composables/useCookieModal.ts +9 -0
  131. package/app/composables/useDevice.ts +109 -0
  132. package/app/composables/useGeoFetch.ts +139 -0
  133. package/app/composables/useLoadScript.ts +36 -0
  134. package/app/composables/useModalState.ts +37 -0
  135. package/app/composables/useMultiLang.ts +278 -0
  136. package/app/composables/useNetwork.ts +49 -0
  137. package/app/composables/useNuxtVersion.ts +5 -0
  138. package/app/composables/usePiniaStore.ts +5 -0
  139. package/app/composables/usePopupControl.ts +57 -0
  140. package/app/composables/useQuery.ts +307 -0
  141. package/app/composables/useReadyPage.ts +182 -0
  142. package/app/composables/useSafeList.ts +90 -0
  143. package/app/composables/useScriptLoader.ts +221 -0
  144. package/app/composables/useSiteSearch.ts +32 -0
  145. package/app/composables/useSnapScroll.ts +24 -0
  146. package/app/composables/useStatic.ts +5 -0
  147. package/app/composables/useStorage.ts +252 -0
  148. package/app/composables/useStoreBroadcast.ts +146 -0
  149. package/app/composables/useThemeSync.ts +81 -0
  150. package/app/constants/layout.ts +109 -0
  151. package/app/constants/predefinedCookieScripts.ts +1222 -0
  152. package/app/constants/ui-events.ts +1 -0
  153. package/app/error.vue +169 -0
  154. package/app/layouts/default-blog.vue +5 -0
  155. package/app/layouts/default.vue +3 -0
  156. package/app/locales/en/commands.json +5 -0
  157. package/app/locales/en/cookies.json +182 -0
  158. package/app/locales/en/error.json +4 -0
  159. package/app/locales/en/interface.json +33 -0
  160. package/app/locales/en/myelophone.json +8 -0
  161. package/app/locales/en/outdated.json +5 -0
  162. package/app/locales/en/times.json +18 -0
  163. package/app/locales/pl/commands.json +5 -0
  164. package/app/locales/pl/cookies.json +182 -0
  165. package/app/locales/pl/error.json +4 -0
  166. package/app/locales/pl/interface.json +33 -0
  167. package/app/locales/pl/myelophone.json +8 -0
  168. package/app/locales/pl/outdated.json +5 -0
  169. package/app/locales/pl/times.json +18 -0
  170. package/app/locales/ru/commands.json +5 -0
  171. package/app/locales/ru/cookies.json +182 -0
  172. package/app/locales/ru/error.json +4 -0
  173. package/app/locales/ru/interface.json +33 -0
  174. package/app/locales/ru/myelophone.json +8 -0
  175. package/app/locales/ru/outdated.json +5 -0
  176. package/app/locales/ru/times.json +18 -0
  177. package/app/middleware/blog-layout.ts +28 -0
  178. package/app/middleware/i18n.global.ts +17 -0
  179. package/app/middleware/not-found-status.ts +11 -0
  180. package/app/modules/default-blog/index.ts +39 -0
  181. package/app/modules/default-blog/posts.ts +80 -0
  182. package/app/modules/default-sitemap/index.ts +116 -0
  183. package/app/modules/default-sitemap/runtime/sitemap.get.ts +42 -0
  184. package/app/modules/fullscreen-preloader/module.ts +43 -0
  185. package/app/modules/htaccess/.htaccess +38 -0
  186. package/app/modules/htaccess/index.ts +23 -0
  187. package/app/modules/i18n-shaker.mjs +221 -0
  188. package/app/modules/multi18n/module.ts +71 -0
  189. package/app/modules/myelophone-config/module.ts +241 -0
  190. package/app/pages/[...all].vue +9 -0
  191. package/app/pages/index.vue +3 -0
  192. package/app/pages/post/[...slug].vue +30 -0
  193. package/app/plugins/bfcache-refresh.client.ts +9 -0
  194. package/app/plugins/canonical.ts +30 -0
  195. package/app/plugins/csp-nonce.server.ts +41 -0
  196. package/app/plugins/multi18n.client.ts +21 -0
  197. package/app/plugins/multi18n.server.ts +15 -0
  198. package/app/plugins/nuxt-version.client.ts +4 -0
  199. package/app/plugins/restore.client.ts +21 -0
  200. package/app/plugins/reveal.ts +89 -0
  201. package/app/plugins/url-helper.client.ts +66 -0
  202. package/app/server/api/exchange-rates.get.ts +216 -0
  203. package/app/server/api/healthz.ts +1 -0
  204. package/app/server/api/logout.post.ts +6 -0
  205. package/app/server/middleware/normalize-url.ts +14 -0
  206. package/app/server/middleware/security-headers.ts +147 -0
  207. package/app/server/middleware/url-helper.ts +43 -0
  208. package/app/server/plugins/clean-html-spaces.ts +15 -0
  209. package/app/server/plugins/clean-styles.ts +94 -0
  210. package/app/server/plugins/compression.ts +122 -0
  211. package/app/server/plugins/css-preload.ts +26 -0
  212. package/app/server/plugins/early-hints.ts +31 -0
  213. package/app/server/plugins/force-noindex.ts +43 -0
  214. package/app/server/plugins/html-one-line.ts +17 -0
  215. package/app/server/plugins/inline-style.ts +7 -0
  216. package/app/server/routes/healthz.get.ts +1 -0
  217. package/app/server/tsconfig.json +3 -0
  218. package/app/stores/cart.ts +945 -0
  219. package/app/stores/commands.ts +43 -0
  220. package/app/stores/device.ts +156 -0
  221. package/app/stores/querySync.ts +5 -0
  222. package/app/stores/settings.ts +145 -0
  223. package/app/stores/user.ts +607 -0
  224. package/app/types/cookie.ts +89 -0
  225. package/app/types/myelophone.d.ts +87 -0
  226. package/app/types/site-search.ts +110 -0
  227. package/app/utils/asyncRefresh.ts +4 -0
  228. package/app/utils/blog.server.ts +142 -0
  229. package/app/utils/jsonLd.ts +56 -0
  230. package/app/utils/myelophoneConfig.ts +4 -0
  231. package/app/utils/sortBy.ts +18 -0
  232. package/app/utils/transliterate.ts +75 -0
  233. package/app/utils/ytVideo.ts +37 -0
  234. package/app/workers/site-search.worker.ts +661 -0
  235. package/biome.json +94 -0
  236. package/commitlint.config.js +3 -0
  237. package/nuxt.config.ts +539 -0
  238. package/package.json +101 -0
  239. package/playground/content/.gitkeep +0 -0
  240. package/playground/locales/.gitkeep +0 -0
  241. package/playground/myelophone.dev.ts +45 -0
  242. package/playground/nuxt.config.ts +105 -0
  243. package/playground/package.json +5 -0
  244. package/playground/pages/.gitkeep +0 -0
  245. package/playground/tsconfig.json +3 -0
  246. package/playwright.config.ts +23 -0
  247. package/public/browsers/chrome.svg +77 -0
  248. package/public/browsers/edge.svg +98 -0
  249. package/public/browsers/firefox.svg +300 -0
  250. package/public/favicon.ico +0 -0
  251. package/public/myelophone_eng.png +0 -0
  252. package/public/myelophone_eng_white.png +0 -0
  253. package/spa-loading-template.html +72 -0
  254. package/tests/home.spec.ts +9 -0
  255. package/tests/utils.ts +23 -0
  256. package/tsconfig.json +16 -0
  257. package/vite/plugins/clean-css.ts +190 -0
  258. package/vite/plugins/postcss-add-viewportunits.ts +33 -0
  259. package/vite/plugins/postcss-viewport-fallback.ts +49 -0
@@ -0,0 +1,150 @@
1
+ <script setup lang="ts">
2
+ const props = withDefaults(
3
+ defineProps<{
4
+ label?: string;
5
+
6
+ color?: "primary" | "gray" | "info" | "success" | "warning" | "error";
7
+ variant?: "solid" | "soft" | "outline";
8
+ size?: "xs" | "sm" | "md";
9
+
10
+ rounded?: boolean;
11
+ pill?: boolean;
12
+
13
+ icon?: string;
14
+ leadingIcon?: string;
15
+ trailingIcon?: string;
16
+ showLeading?: boolean;
17
+ showTrailing?: boolean;
18
+
19
+ avatar?: {
20
+ src?: string;
21
+ icon?: string;
22
+ alt?: string;
23
+ };
24
+ }>(),
25
+ {
26
+ color: "gray",
27
+ variant: "soft",
28
+ size: "sm",
29
+ rounded: true,
30
+ pill: false,
31
+ showLeading: true,
32
+ showTrailing: true,
33
+ },
34
+ );
35
+
36
+ const sizes = {
37
+ xs: {
38
+ container: "text-xs px-2 py-0.5 gap-1",
39
+ icon: "h-3 w-3",
40
+ avatar: "h-4 w-4",
41
+ },
42
+ sm: {
43
+ container: "text-xs px-3 py-0.5 gap-1.5",
44
+ icon: "h-3.5 w-3.5",
45
+ avatar: "h-5 w-5",
46
+ },
47
+ md: {
48
+ container: "text-sm px-4 py-1 gap-2",
49
+ icon: "h-4 w-4",
50
+ avatar: "h-6 w-6",
51
+ },
52
+ } as const;
53
+
54
+ const colorVariants: Record<
55
+ NonNullable<typeof props.color>,
56
+ Record<NonNullable<typeof props.variant>, string>
57
+ > = {
58
+ gray: {
59
+ solid: "bg-gray-800 text-white",
60
+ soft: "bg-gray-100 text-gray-800",
61
+ outline: "border border-gray-300 text-gray-800",
62
+ },
63
+ primary: {
64
+ solid: "bg-primary-600 text-white",
65
+ soft: "bg-primary-100 text-primary-800",
66
+ outline: "border border-primary-300 text-primary-700",
67
+ },
68
+ info: {
69
+ solid: "bg-blue-600 text-white",
70
+ soft: "bg-blue-100 text-blue-800",
71
+ outline: "border border-blue-300 text-blue-700",
72
+ },
73
+ success: {
74
+ solid: "bg-emerald-600 text-white",
75
+ soft: "bg-emerald-100 text-emerald-800",
76
+ outline: "border border-emerald-300 text-emerald-700",
77
+ },
78
+ warning: {
79
+ solid: "bg-amber-500 text-white",
80
+ soft: "bg-amber-100 text-amber-800",
81
+ outline: "border border-amber-300 text-amber-700",
82
+ },
83
+ error: {
84
+ solid: "bg-red-600 text-white",
85
+ soft: "bg-red-100 text-red-800",
86
+ outline: "border border-red-300 text-red-700",
87
+ },
88
+ };
89
+
90
+ const leadingIconName = computed<string | null>(() => {
91
+ return props.leadingIcon ?? props.icon ?? null;
92
+ });
93
+
94
+ const showLeadingIcon = computed(
95
+ () => props.showLeading && !!leadingIconName.value && !props.avatar,
96
+ );
97
+
98
+ const showTrailingIcon = computed(
99
+ () => props.showTrailing && !!props.trailingIcon,
100
+ );
101
+
102
+ const classes = computed(() => [
103
+ "inline-flex items-center font-medium whitespace-nowrap w-fit",
104
+ sizes[props.size].container,
105
+ colorVariants[props.color][props.variant],
106
+ props.pill ? "rounded-full" : props.rounded ? "rounded-md" : "rounded-none",
107
+ ]);
108
+
109
+ const trailingIconName = computed<string | null>(() => {
110
+ return props.showTrailing && props.trailingIcon ? props.trailingIcon : null;
111
+ });
112
+ </script>
113
+
114
+ <template>
115
+ <span :class="classes">
116
+ <span
117
+ v-if="avatar"
118
+ class="flex items-center justify-center shrink-0"
119
+ :class="sizes[size].avatar"
120
+ >
121
+ <SafeImgWithLoader
122
+ v-if="avatar.src"
123
+ :src="avatar.src"
124
+ :alt="avatar.alt || ''"
125
+ class="h-full w-full rounded-full object-contain"
126
+ />
127
+ <UiIcon
128
+ v-else-if="avatar.icon"
129
+ :name="avatar.icon"
130
+ class="h-full w-full"
131
+ />
132
+ </span>
133
+
134
+ <UiIcon
135
+ v-if="showLeadingIcon && leadingIconName"
136
+ :name="leadingIconName"
137
+ :class="sizes[size].icon"
138
+ />
139
+
140
+ <span>
141
+ <slot>{{ label }}</slot>
142
+ </span>
143
+
144
+ <UiIcon
145
+ v-if="trailingIconName"
146
+ :name="trailingIconName"
147
+ :class="sizes[size].icon"
148
+ />
149
+ </span>
150
+ </template>
@@ -0,0 +1,133 @@
1
+ <script setup lang="ts">
2
+ const props = withDefaults(
3
+ defineProps<{
4
+ title?: string;
5
+ description?: string;
6
+
7
+ icon?: string;
8
+ avatar?: {
9
+ src?: string;
10
+ icon?: string;
11
+ alt?: string;
12
+ };
13
+
14
+ color?: "primary" | "gray" | "info" | "success" | "warning" | "error";
15
+ variant?: "solid" | "soft" | "outline";
16
+
17
+ closable?: boolean;
18
+ rounded?: boolean;
19
+ bordered?: boolean;
20
+ }>(),
21
+ {
22
+ color: "gray",
23
+ variant: "soft",
24
+ closable: false,
25
+ rounded: true,
26
+ bordered: false,
27
+ },
28
+ );
29
+
30
+ const visible = ref(true);
31
+
32
+ const close = () => {
33
+ visible.value = false;
34
+ };
35
+
36
+ const colorVariants: Record<
37
+ NonNullable<typeof props.color>,
38
+ Record<NonNullable<typeof props.variant>, string>
39
+ > = {
40
+ gray: {
41
+ solid: "bg-gray-800 text-white border border-gray-800/30",
42
+ soft: "bg-gray-100 text-gray-900 border border-gray-300",
43
+ outline: "bg-transparent text-gray-900 border border-gray-400",
44
+ },
45
+ primary: {
46
+ solid: "bg-primary-600 text-white border border-primary-600/30",
47
+ soft: "bg-primary-100 text-primary-900 border border-primary-300",
48
+ outline: "bg-transparent text-primary-900 border border-primary-400",
49
+ },
50
+ info: {
51
+ solid: "bg-blue-600 text-white border border-blue-600/30",
52
+ soft: "bg-blue-100 text-blue-900 border border-blue-300",
53
+ outline: "bg-transparent text-blue-900 border border-blue-400",
54
+ },
55
+ success: {
56
+ solid: "bg-emerald-600 text-white border border-emerald-600/30",
57
+ soft: "bg-emerald-100 text-emerald-900 border border-emerald-300",
58
+ outline: "bg-transparent text-emerald-900 border border-emerald-400",
59
+ },
60
+ warning: {
61
+ solid: "bg-amber-500 text-white border border-amber-500/30",
62
+ soft: "bg-amber-100 text-amber-900 border border-amber-300",
63
+ outline: "bg-transparent text-amber-900 border border-amber-400",
64
+ },
65
+ error: {
66
+ solid: "bg-red-600 text-white border border-red-600/30",
67
+ soft: "bg-red-100 text-red-900 border border-red-300",
68
+ outline: "bg-transparent text-red-900 border border-red-400",
69
+ },
70
+ };
71
+
72
+ const classes = computed(() => [
73
+ "w-full flex items-center gap-4 px-4 py-3",
74
+ colorVariants[props.color][props.variant],
75
+ props.rounded ? "rounded-md" : "rounded-none",
76
+ props.bordered || props.variant === "outline"
77
+ ? "border border-current/20"
78
+ : null,
79
+ ]);
80
+ </script>
81
+
82
+ <template>
83
+ <div v-if="visible" role="region" :class="classes">
84
+ <div
85
+ v-if="avatar || icon"
86
+ class="flex items-center justify-center shrink-0"
87
+ >
88
+ <div class="h-10 w-10 flex items-center justify-center">
89
+ <SafeImgWithLoader
90
+ v-if="avatar?.src"
91
+ :src="avatar.src"
92
+ :alt="avatar.alt || ''"
93
+ class="h-full w-full object-contain rounded-full"
94
+ />
95
+ <UiIcon
96
+ v-else-if="avatar?.icon"
97
+ :name="avatar.icon"
98
+ class="h-full w-full"
99
+ />
100
+ <UiIcon v-else-if="icon" :name="icon" class="h-6 w-6" />
101
+ </div>
102
+ </div>
103
+
104
+ <div class="flex-1 min-w-0">
105
+ <div
106
+ v-if="$slots.title || title"
107
+ class="font-semibold leading-tight"
108
+ >
109
+ <slot name="title">{{ title }}</slot>
110
+ </div>
111
+
112
+ <div
113
+ v-if="$slots.default || description"
114
+ class="text-sm opacity-90 leading-relaxed"
115
+ >
116
+ <slot>{{ description }}</slot>
117
+ </div>
118
+ </div>
119
+
120
+ <div v-if="$slots.actions" class="flex items-center gap-2 shrink-0">
121
+ <slot name="actions" />
122
+ </div>
123
+
124
+ <button
125
+ v-if="closable"
126
+ @click="close"
127
+ class="ml-2 opacity-60 hover:opacity-100 transition shrink-0"
128
+ aria-label="Close banner"
129
+ >
130
+ <UiIcon name="heroicons:x-mark" class="h-5 w-5" />
131
+ </button>
132
+ </div>
133
+ </template>
@@ -0,0 +1,175 @@
1
+ <script setup lang="ts">
2
+ import { NuxtLink } from "#components";
3
+
4
+ const props = withDefaults(
5
+ defineProps<{
6
+ as?: "button" | "a" | "nuxt-link" | any;
7
+
8
+ label?: string;
9
+
10
+ to?: string | object;
11
+ href?: string;
12
+ target?: string;
13
+
14
+ color?:
15
+ | "primary"
16
+ | "gray"
17
+ | "info"
18
+ | "success"
19
+ | "warning"
20
+ | "error"
21
+ | "base";
22
+ variant?: "solid" | "soft" | "outline" | "ghost";
23
+ size?: "xs" | "sm" | "md" | "lg";
24
+
25
+ icon?: string;
26
+ leadingIcon?: string;
27
+ trailingIcon?: string;
28
+
29
+ loading?: boolean;
30
+ disabled?: boolean;
31
+
32
+ block?: boolean;
33
+ rounded?: boolean;
34
+ square?: boolean;
35
+ }>(),
36
+ {
37
+ as: "button",
38
+ color: "base",
39
+ variant: "solid",
40
+ size: "md",
41
+ loading: false,
42
+ disabled: false,
43
+ block: false,
44
+ rounded: true,
45
+ square: false,
46
+ },
47
+ );
48
+
49
+ const resolvedAs = computed(() => {
50
+ if (props.as === "nuxt-link") return NuxtLink;
51
+ return props.as;
52
+ });
53
+
54
+ const isDisabled = computed(() => props.disabled || props.loading);
55
+
56
+ const leadingIconName = computed<string | null>(() => {
57
+ return props.leadingIcon ?? props.icon ?? null;
58
+ });
59
+
60
+ const trailingIconName = computed<string | null>(() => {
61
+ return props.trailingIcon ?? null;
62
+ });
63
+
64
+ const sizes = {
65
+ xs: "text-xs px-2 py-1 gap-1",
66
+ sm: "text-sm px-2.5 py-1.5 gap-1.5",
67
+ md: "text-sm px-4 py-2 gap-2",
68
+ lg: "text-base px-5 py-2.5 gap-2.5",
69
+ } as const;
70
+
71
+ const iconSizes = {
72
+ xs: "h-3 w-3",
73
+ sm: "h-4 w-4",
74
+ md: "h-5 w-5",
75
+ lg: "h-6 w-6",
76
+ } as const;
77
+
78
+ const baseVariants = {
79
+ solid: "bg-[var(--ui-bg)] text-[var(--ui-text)] border border-[var(--ui-border,var(--ui-text))]",
80
+ soft: "bg-[color-mix(in srgb, var(--ui-bg) 85%, transparent)] text-[var(--ui-text)] border border-[var(--ui-border,var(--ui-text))]",
81
+ outline:
82
+ "bg-transparent text-[var(--ui-text)] border border-[var(--ui-border,var(--ui-text))]",
83
+ ghost: "bg-transparent text-[var(--ui-text)]",
84
+ };
85
+
86
+ const colorVariants: Record<
87
+ NonNullable<typeof props.color>,
88
+ Record<NonNullable<typeof props.variant>, string>
89
+ > = {
90
+ base: baseVariants,
91
+
92
+ gray: {
93
+ solid: "bg-gray-800 text-white border border-gray-800/30",
94
+ soft: "bg-gray-100 text-gray-900 border border-gray-300",
95
+ outline: "bg-transparent text-gray-900 border border-gray-400",
96
+ ghost: "bg-transparent text-gray-900",
97
+ },
98
+ primary: {
99
+ solid: "bg-primary-600 text-white border border-primary-600/30",
100
+ soft: "bg-primary-100 text-primary-900 border border-primary-300",
101
+ outline: "bg-transparent text-primary-900 border border-primary-400",
102
+ ghost: "bg-transparent text-primary-900",
103
+ },
104
+ info: {
105
+ solid: "bg-blue-600 text-white border border-blue-600/30",
106
+ soft: "bg-blue-100 text-blue-900 border border-blue-300",
107
+ outline: "bg-transparent text-blue-900 border border-blue-400",
108
+ ghost: "bg-transparent text-blue-900",
109
+ },
110
+ success: {
111
+ solid: "bg-emerald-600 text-white border border-emerald-600/30",
112
+ soft: "bg-emerald-100 text-emerald-900 border border-emerald-300",
113
+ outline: "bg-transparent text-emerald-900 border border-emerald-400",
114
+ ghost: "bg-transparent text-emerald-900",
115
+ },
116
+ warning: {
117
+ solid: "bg-amber-500 text-white border border-amber-500/30",
118
+ soft: "bg-amber-100 text-amber-900 border border-amber-300",
119
+ outline: "bg-transparent text-amber-900 border border-amber-400",
120
+ ghost: "bg-transparent text-amber-900",
121
+ },
122
+ error: {
123
+ solid: "bg-red-600 text-white border border-red-600/30",
124
+ soft: "bg-red-100 text-red-900 border border-red-300",
125
+ outline: "bg-transparent text-red-900 border border-red-400",
126
+ ghost: "bg-transparent text-red-900",
127
+ },
128
+ };
129
+
130
+ const classes = computed(() => [
131
+ "inline-flex items-center justify-center font-medium whitespace-nowrap transition",
132
+ sizes[props.size],
133
+ colorVariants[props.color][props.variant],
134
+ props.block ? "w-full" : "w-fit",
135
+ props.square ? "aspect-square p-0" : null,
136
+ props.rounded ? "rounded-md" : "rounded-none",
137
+ isDisabled.value ? "opacity-50 pointer-events-none" : "hover:brightness-95",
138
+ ]);
139
+ </script>
140
+
141
+ <template>
142
+ <component
143
+ :is="resolvedAs"
144
+ :to="to"
145
+ :href="href"
146
+ :target="target"
147
+ :disabled="resolvedAs === 'button' ? isDisabled : undefined"
148
+ :aria-disabled="isDisabled || undefined"
149
+ :class="classes"
150
+ >
151
+ <UiIcon
152
+ v-if="loading"
153
+ name="heroicons:arrow-path"
154
+ class="animate-spin"
155
+ :style="{ color: 'var(--loader-fill-color, currentColor)' }"
156
+ :class="iconSizes[size]"
157
+ />
158
+
159
+ <UiIcon
160
+ v-else-if="leadingIconName"
161
+ :name="leadingIconName"
162
+ :class="iconSizes[size]"
163
+ />
164
+
165
+ <span v-if="$slots.default || label">
166
+ <slot>{{ label }}</slot>
167
+ </span>
168
+
169
+ <UiIcon
170
+ v-if="!loading && trailingIconName"
171
+ :name="trailingIconName"
172
+ :class="iconSizes[size]"
173
+ />
174
+ </component>
175
+ </template>
@@ -0,0 +1,66 @@
1
+ <script setup lang="ts">
2
+ const props = withDefaults(
3
+ defineProps<{
4
+ as?: "div" | "section" | "article";
5
+
6
+ to?: string | object;
7
+ href?: string;
8
+ target?: string;
9
+
10
+ bordered?: boolean;
11
+ rounded?: boolean;
12
+ padded?: boolean;
13
+ }>(),
14
+ {
15
+ as: "div",
16
+ bordered: true,
17
+ rounded: true,
18
+ padded: true,
19
+ },
20
+ );
21
+
22
+ const isLink = computed(() => !!props.to || !!props.href);
23
+
24
+ const tag = computed(() => {
25
+ if (props.to) return "NuxtLink";
26
+ if (props.href) return "a";
27
+ return props.as;
28
+ });
29
+
30
+ const classes = computed(() => [
31
+ "bg-[var(--ui-bg)] text-[var(--ui-text)] transition",
32
+ props.bordered ? "border border-[var(--ui-border,var(--ui-text))]" : "",
33
+ props.rounded ? "rounded-lg" : "rounded-none",
34
+ isLink.value
35
+ ? "cursor-pointer hover:bg-[color-mix(in srgb, var(--ui-bg) 92%, var(--ui-text))] focus:outline-none focus:ring-2 focus:ring-[var(--ui-border,var(--ui-text))] focus:ring-offset-2"
36
+ : "",
37
+ ]);
38
+ </script>
39
+
40
+ <template>
41
+ <component
42
+ :is="tag"
43
+ :to="to"
44
+ :href="href"
45
+ :target="target"
46
+ :class="classes"
47
+ >
48
+ <div
49
+ v-if="$slots.header"
50
+ class="px-4 py-3 border-b border-(--ui-border,var(--ui-text))"
51
+ >
52
+ <slot name="header" />
53
+ </div>
54
+
55
+ <div :class="padded ? 'p-4' : ''">
56
+ <slot />
57
+ </div>
58
+
59
+ <div
60
+ v-if="$slots.footer"
61
+ class="px-4 py-3 border-t border-(--ui-border,var(--ui-text))"
62
+ >
63
+ <slot name="footer" />
64
+ </div>
65
+ </component>
66
+ </template>
@@ -0,0 +1,134 @@
1
+ <template>
2
+ <fieldset>
3
+ <legend class="sr-only">{{ legend || "Checkboxes" }}</legend>
4
+
5
+ <div :class="containerClasses">
6
+ <div :class="wrapperClasses">
7
+ <label
8
+ v-for="option in options"
9
+ :key="option.value"
10
+ :for="getInputId(option.value)"
11
+ class="flex w-full cursor-pointer items-start gap-3 py-3 select-none"
12
+ >
13
+ <input
14
+ :id="getInputId(option.value)"
15
+ type="checkbox"
16
+ :value="option.value"
17
+ :checked="model.includes(option.value)"
18
+ :disabled="option.disabled || disabled"
19
+ @change="toggle(option.value)"
20
+ class="sr-only peer"
21
+ />
22
+
23
+ <div
24
+ class="relative flex size-5 shrink-0 items-center justify-center rounded border-2 transition-colors duration-200"
25
+ :class="[
26
+ 'peer-checked:bg-blue-600 peer-checked:border-blue-600',
27
+ 'peer-disabled:opacity-60 peer-disabled:cursor-not-allowed',
28
+ 'border-gray-300 dark:border-gray-600',
29
+ 'bg-(--ui-bg) mt-0.5',
30
+ ]"
31
+ >
32
+ <svg
33
+ class="size-3.5 text-white transition-transform duration-200 origin-center"
34
+ :class="
35
+ model.includes(option.value)
36
+ ? 'scale-100'
37
+ : 'scale-0'
38
+ "
39
+ viewBox="0 0 16 16"
40
+ fill="currentColor"
41
+ >
42
+ <path
43
+ d="M13.78 3.22a.75.75 0 0 1 0 1.06l-7.5 7.5a.75.75 0 0 1-1.06 0l-3-3a.75.75 0 1 1 1.06-1.06L6 10.44l7-7a.75.75 0 0 1 1.06 0z"
44
+ />
45
+ </svg>
46
+ </div>
47
+
48
+ <div class="flex-1">
49
+ <span
50
+ class="font-medium"
51
+ :style="{ color: 'var(--ui-text)' }"
52
+ >
53
+ {{ option.label }}
54
+ </span>
55
+ <p
56
+ v-if="option.description"
57
+ class="mt-0.5 text-sm opacity-70"
58
+ :style="{ color: 'var(--ui-text)' }"
59
+ >
60
+ {{ option.description }}
61
+ </p>
62
+ </div>
63
+ </label>
64
+ </div>
65
+ </div>
66
+ </fieldset>
67
+ </template>
68
+
69
+ <script setup lang="ts">
70
+ interface CheckboxOption {
71
+ label: string;
72
+ value: string | number;
73
+ description?: string;
74
+ disabled?: boolean;
75
+ }
76
+
77
+ const props = defineProps({
78
+ modelValue: {
79
+ type: Array as () => (string | number)[],
80
+ default: () => [],
81
+ },
82
+ options: {
83
+ type: Array as () => CheckboxOption[],
84
+ required: true,
85
+ },
86
+ legend: String,
87
+ disabled: Boolean,
88
+ divider: { type: Boolean, default: false },
89
+ });
90
+
91
+ const emit = defineEmits<{
92
+ (e: "update:modelValue", value: (string | number)[]): void;
93
+ }>();
94
+
95
+ const idMap = ref<Map<string | number, string>>(new Map());
96
+ let counter = 0;
97
+
98
+ onMounted(() => {
99
+ props.options.forEach((opt) => {
100
+ if (!idMap.value.has(opt.value)) {
101
+ idMap.value.set(opt.value, `cb-${++counter}`);
102
+ }
103
+ });
104
+ });
105
+
106
+ const getInputId = (value: string | number): string => {
107
+ return idMap.value.get(value) || `cb-${value}`;
108
+ };
109
+
110
+ const model = computed<(string | number)[]>({
111
+ get: () => props.modelValue,
112
+ set: (val) => emit("update:modelValue", val),
113
+ });
114
+
115
+ const toggle = (value: string | number) => {
116
+ const current = [...model.value];
117
+ const index = current.indexOf(value);
118
+ if (index > -1) {
119
+ current.splice(index, 1);
120
+ } else {
121
+ current.push(value);
122
+ }
123
+ emit("update:modelValue", current);
124
+ };
125
+
126
+ const containerClasses = computed(() =>
127
+ props.divider ? "flow-root" : "space-y-3",
128
+ );
129
+
130
+ const wrapperClasses = computed(() => {
131
+ if (!props.divider) return "";
132
+ return "w-full divide-y divide-gray-300 dark:divide-gray-700";
133
+ });
134
+ </script>