@brumaombra/ui-vintage 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +41 -0
- package/COMPONENTS.md +91 -17
- package/README.md +296 -262
- package/module.mjs +5 -11
- package/package.json +31 -13
- package/src/components/blog/AllPostsSection.vue +1 -2
- package/src/components/blog/BlogContentRenderer.vue +144 -49
- package/src/components/blog/BlogFAQSection.vue +13 -15
- package/src/components/blog/BlogHeaderSection.vue +103 -11
- package/src/components/blog/BlogInfoSection.vue +46 -42
- package/src/components/blog/BlogPostHeader.vue +181 -0
- package/src/components/blog/BlogProgressBar.vue +3 -2
- package/src/components/blog/BlogSectionTitle.vue +27 -6
- package/src/components/blog/BlogTagsSection.vue +20 -8
- package/src/components/blog/CategoriesList.vue +2 -2
- package/src/components/blog/CategoryCard.vue +23 -19
- package/src/components/blog/HeaderCarousel.vue +117 -145
- package/src/components/blog/PostCard.vue +25 -33
- package/src/components/blog/PostsList.vue +3 -2
- package/src/components/blog/SocialShareSidebar.vue +62 -42
- package/src/components/blog/TableOfContents.vue +71 -34
- package/src/components/blog/index.ts +1 -0
- package/src/components/card-grid/CardGrid.vue +2 -2
- package/src/components/confirm-dialog/ConfirmDialog.vue +5 -0
- package/src/components/confirm-dialog/confirm-dialog-state.ts +22 -22
- package/src/components/content/BlogList.vue +30 -42
- package/src/components/content/BlogTable.vue +26 -30
- package/src/components/content/CodeBlock.vue +96 -22
- package/src/components/content/Flow.vue +30 -36
- package/src/components/content/ProseHr.vue +6 -6
- package/src/components/content/ProsePre.vue +9 -4
- package/src/components/content/Terminal.vue +42 -14
- package/src/components/cookie-consent/CookieConsent.vue +1 -1
- package/src/components/dashboard-shell/DashboardShell.vue +24 -7
- package/src/components/empty-state-card/EmptyStateCard.vue +2 -2
- package/src/components/error-page/ErrorPage.vue +3 -3
- package/src/components/info-card/InfoCard.vue +4 -4
- package/src/components/landing/landing-footer/LandingFooter.vue +16 -6
- package/src/components/landing/landing-navbar/LandingNavbar.vue +63 -51
- package/src/components/language-flag/index.ts +1 -0
- package/src/components/language-selector/LanguageSelector.vue +18 -10
- package/src/components/load-more-button/LoadMoreButton.vue +7 -12
- package/src/components/loading-state-card/LoadingStateCard.vue +2 -2
- package/src/components/message-dialog/MessageDialog.vue +11 -11
- package/src/components/message-dialog/message-dialog-state.ts +1 -4
- package/src/components/message-toast/MessageToast.vue +24 -9
- package/src/components/message-toast/message-toast-state.ts +20 -0
- package/src/components/page-header/PageHeader.vue +1 -1
- package/src/components/progress-component/ProgressComponent.vue +7 -3
- package/src/components/single-value-card/SingleValueCard.vue +37 -39
- package/src/components/slider-form-component/SliderFormComponent.vue +1 -1
- package/src/components/stat-strip/StatStrip.vue +36 -0
- package/src/components/stat-strip/index.ts +2 -0
- package/src/components/switch-form-component/SwitchFormComponent.vue +5 -5
- package/src/components/text-link/TextLink.vue +1 -1
- package/src/components/theme-selector/ThemeSelector.vue +2 -2
- package/src/components/theme-selector/theme-transition.ts +15 -3
- package/src/components/ui/accordion/Accordion.vue +18 -18
- package/src/components/ui/alert/AlertTitle.vue +1 -1
- package/src/components/ui/alert/index.ts +1 -1
- package/src/components/ui/avatar/AvatarFallback.vue +2 -1
- package/src/components/ui/avatar/AvatarGroup.vue +13 -3
- package/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue +2 -2
- package/src/components/ui/breadcrumb/BreadcrumbLink.vue +1 -1
- package/src/components/ui/breadcrumb/BreadcrumbList.vue +1 -1
- package/src/components/ui/breadcrumb/BreadcrumbPage.vue +1 -1
- package/src/components/ui/breadcrumb/BreadcrumbSeparator.vue +2 -6
- package/src/components/ui/busy/Busy.vue +5 -7
- package/src/components/ui/button/Button.vue +4 -2
- package/src/components/ui/button/index.ts +1 -4
- package/src/components/ui/calendar/Calendar.vue +12 -7
- package/src/components/ui/calendar/CalendarCell.vue +1 -1
- package/src/components/ui/calendar/CalendarCellTrigger.vue +1 -1
- package/src/components/ui/calendar/CalendarHeadCell.vue +1 -1
- package/src/components/ui/calendar/CalendarNextButton.vue +1 -1
- package/src/components/ui/calendar/CalendarPrevButton.vue +1 -1
- package/src/components/ui/card/Card.vue +5 -15
- package/src/components/ui/checkbox/Checkbox.vue +5 -1
- package/src/components/ui/combobox/ComboboxInput.vue +5 -1
- package/src/components/ui/command/CommandGroup.vue +17 -3
- package/src/components/ui/command/CommandInput.vue +1 -1
- package/src/components/ui/command/CommandItem.vue +2 -2
- package/src/components/ui/command/CommandList.vue +5 -1
- package/src/components/ui/data-table/DataTable.vue +11 -3
- package/src/components/ui/date-picker/DatePicker.vue +7 -1
- package/src/components/ui/date-time-picker/DateTimePicker.vue +5 -1
- package/src/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
- package/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +1 -1
- package/src/components/ui/dropdown-menu/classes.ts +2 -0
- package/src/components/ui/field/Field.vue +4 -0
- package/src/components/ui/field/FieldLabel.vue +6 -2
- package/src/components/ui/field/field-context.ts +62 -0
- package/src/components/ui/field/index.ts +1 -0
- package/src/components/ui/file-dropzone/FileDropzone.vue +4 -4
- package/src/components/ui/input/Input.vue +5 -1
- package/src/components/ui/native-select/NativeSelect.vue +6 -2
- package/src/components/ui/number-field/NumberField.vue +5 -1
- package/src/components/ui/pagination/PaginationItem.vue +1 -1
- package/src/components/ui/pin-input/PinInputInput.vue +2 -2
- package/src/components/ui/progress/Progress.vue +2 -2
- package/src/components/ui/radio-group/RadioGroupCard.vue +1 -1
- package/src/components/ui/select/SelectItem.vue +1 -1
- package/src/components/ui/select/SelectItemContent.vue +1 -1
- package/src/components/ui/select/SelectTrigger.vue +5 -1
- package/src/components/ui/sidebar/Sidebar.vue +1 -1
- package/src/components/ui/sidebar/SidebarGroupAction.vue +1 -1
- package/src/components/ui/sidebar/SidebarGroupLabel.vue +1 -1
- package/src/components/ui/sidebar/SidebarInset.vue +1 -1
- package/src/components/ui/sidebar/SidebarMenuAction.vue +1 -1
- package/src/components/ui/sidebar/SidebarMenuBadge.vue +1 -1
- package/src/components/ui/sidebar/SidebarMenuSkeleton.vue +2 -2
- package/src/components/ui/sidebar/SidebarMenuSubButton.vue +1 -1
- package/src/components/ui/sidebar/SidebarProvider.vue +17 -6
- package/src/components/ui/sidebar/index.ts +2 -2
- package/src/components/ui/sidebar/utils.ts +1 -1
- package/src/components/ui/slider/Slider.vue +5 -4
- package/src/components/ui/spinner/Spinner.vue +3 -2
- package/src/components/ui/stepper/StepperDescription.vue +5 -3
- package/src/components/ui/stepper/StepperIndicator.vue +2 -2
- package/src/components/ui/stepper/StepperTitle.vue +5 -3
- package/src/components/ui/switch/Switch.vue +6 -2
- package/src/components/ui/tabs/TabsList.vue +1 -1
- package/src/components/ui/tabs/TabsTrigger.vue +1 -1
- package/src/components/ui/textarea/Textarea.vue +5 -1
- package/src/components/ui/time-picker/TimePicker.vue +2 -2
- package/src/components/ui/toggle-group/ToggleGroup.vue +1 -1
- package/src/components/ui/tooltip/TooltipContent.vue +1 -1
- package/src/i18n/de.json +19 -7
- package/src/i18n/en.json +19 -7
- package/src/i18n/es.json +19 -7
- package/src/i18n/fr.json +19 -7
- package/src/i18n/it.json +19 -7
- package/src/i18n/ja.json +19 -7
- package/src/i18n/pt.json +19 -7
- package/src/i18n/ru.json +19 -7
- package/src/i18n/zh.json +19 -7
- package/src/lib/color-tokens.ts +1 -1
- package/src/lib/i18n.ts +32 -4
- package/src/lib/utils.ts +21 -1
- package/src/runtime/ui-vintage-i18n-plugin.ts +11 -1
- package/src/styles.css +52 -16
- package/skills/ui-vintage/SKILL.md +0 -42
package/module.mjs
CHANGED
|
@@ -28,24 +28,18 @@ const loadNuxtKit = async () => {
|
|
|
28
28
|
throw new Error('Unable to resolve @nuxt/kit for @brumaombra/ui-vintage. Install the package dependencies or add Nuxt to the consuming app.');
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
const { addPlugin, createResolver, defineNuxtModule, installModule } = await loadNuxtKit();
|
|
31
|
+
const { addPlugin, createResolver, defineNuxtModule, hasNuxtModule, installModule } = await loadNuxtKit();
|
|
32
|
+
const { version } = moduleRequire('./package.json');
|
|
32
33
|
const resolver = createResolver(import.meta.url);
|
|
33
34
|
const sourceRoot = resolver.resolve('src');
|
|
34
35
|
const stylesPath = resolver.resolve('src/styles.css');
|
|
35
36
|
|
|
36
|
-
// Check whether the consuming app already has a specific module installed
|
|
37
|
-
const hasModuleInstalled = (modules, moduleName) => {
|
|
38
|
-
return modules.some((entry) => {
|
|
39
|
-
if (typeof entry === 'string') return entry === moduleName;
|
|
40
|
-
if (Array.isArray(entry)) return entry[0] === moduleName;
|
|
41
|
-
return false;
|
|
42
|
-
});
|
|
43
|
-
};
|
|
44
|
-
|
|
45
37
|
export default defineNuxtModule({
|
|
46
38
|
// Module meta information
|
|
47
39
|
meta: {
|
|
48
40
|
name: '@brumaombra/ui-vintage',
|
|
41
|
+
configKey: 'uiVintage',
|
|
42
|
+
version,
|
|
49
43
|
compatibility: {
|
|
50
44
|
nuxt: '>=4.0.0'
|
|
51
45
|
}
|
|
@@ -63,7 +57,7 @@ export default defineNuxtModule({
|
|
|
63
57
|
nuxt.options.build.transpile.push(new RegExp(`^${cleanSourceRoot}`));
|
|
64
58
|
|
|
65
59
|
// Ensure Nuxt Image is available for components that render NuxtImg
|
|
66
|
-
if (!
|
|
60
|
+
if (!hasNuxtModule('@nuxt/image', nuxt)) {
|
|
67
61
|
await installModule('@nuxt/image');
|
|
68
62
|
}
|
|
69
63
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brumaombra/ui-vintage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Source-published Nuxt UI runtime with shared theme tokens and built-in Nuxt integrations.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Mauro Brambilla",
|
|
@@ -30,12 +30,14 @@
|
|
|
30
30
|
"src",
|
|
31
31
|
"module.mjs",
|
|
32
32
|
"COMPONENTS.md",
|
|
33
|
-
"
|
|
33
|
+
"AGENTS.md"
|
|
34
34
|
],
|
|
35
35
|
"exports": {
|
|
36
36
|
".": {
|
|
37
|
-
"import": "./module.mjs"
|
|
37
|
+
"import": "./module.mjs",
|
|
38
|
+
"default": "./module.mjs"
|
|
38
39
|
},
|
|
40
|
+
"./package.json": "./package.json",
|
|
39
41
|
"./style.css": "./src/styles.css",
|
|
40
42
|
"./accordion": "./src/components/ui/accordion/index.ts",
|
|
41
43
|
"./alert": "./src/components/ui/alert/index.ts",
|
|
@@ -107,6 +109,7 @@
|
|
|
107
109
|
"./slider": "./src/components/ui/slider/index.ts",
|
|
108
110
|
"./slider-form-component": "./src/components/slider-form-component/index.ts",
|
|
109
111
|
"./spinner": "./src/components/ui/spinner/index.ts",
|
|
112
|
+
"./stat-strip": "./src/components/stat-strip/index.ts",
|
|
110
113
|
"./stepper": "./src/components/ui/stepper/index.ts",
|
|
111
114
|
"./switch": "./src/components/ui/switch/index.ts",
|
|
112
115
|
"./switch-form-component": "./src/components/switch-form-component/index.ts",
|
|
@@ -127,32 +130,47 @@
|
|
|
127
130
|
"scripts": {
|
|
128
131
|
"typecheck": "vue-tsc --noEmit",
|
|
129
132
|
"docs:components": "node scripts/generate-components-doc.mjs",
|
|
130
|
-
"docs:components:check": "node scripts/generate-components-doc.mjs --check"
|
|
133
|
+
"docs:components:check": "node scripts/generate-components-doc.mjs --check",
|
|
134
|
+
"test": "npm --prefix demo-app run test"
|
|
131
135
|
},
|
|
132
136
|
"peerDependencies": {
|
|
137
|
+
"@hugeicons/core-free-icons": "^4.1.0",
|
|
138
|
+
"@hugeicons/vue": "^1.0.0",
|
|
139
|
+
"@nuxt/content": "^3.0.0",
|
|
133
140
|
"@nuxt/image": "^2.0.0",
|
|
134
141
|
"nuxt": "^4.0.0",
|
|
142
|
+
"tailwindcss": "^4.0.0",
|
|
135
143
|
"vue": "^3.5.0",
|
|
136
|
-
"vue-i18n": "^9.0.0 || ^10.0.0"
|
|
144
|
+
"vue-i18n": "^9.0.0 || ^10.0.0 || ^11.0.0"
|
|
145
|
+
},
|
|
146
|
+
"peerDependenciesMeta": {
|
|
147
|
+
"@nuxt/content": {
|
|
148
|
+
"optional": true
|
|
149
|
+
}
|
|
137
150
|
},
|
|
138
151
|
"dependencies": {
|
|
139
|
-
"@hugeicons/core-free-icons": "4.1.1",
|
|
140
|
-
"@hugeicons/vue": "1.0.5",
|
|
141
152
|
"@internationalized/date": "3.10.0",
|
|
142
|
-
"@nuxt/kit": "4.
|
|
153
|
+
"@nuxt/kit": "4.5.2",
|
|
143
154
|
"@vueuse/core": "14.2.1",
|
|
144
155
|
"class-variance-authority": "0.7.1",
|
|
145
156
|
"clsx": "2.1.1",
|
|
146
157
|
"reka-ui": "2.9.2",
|
|
147
158
|
"shiki": "4.4.3",
|
|
148
|
-
"tailwind-merge": "3.5.0"
|
|
149
|
-
"tailwindcss": "4.2.4"
|
|
159
|
+
"tailwind-merge": "3.5.0"
|
|
150
160
|
},
|
|
151
161
|
"devDependencies": {
|
|
152
|
-
"@
|
|
153
|
-
"
|
|
162
|
+
"@hugeicons/core-free-icons": "4.1.1",
|
|
163
|
+
"@hugeicons/vue": "1.0.5",
|
|
164
|
+
"nuxt": "4.5.2",
|
|
165
|
+
"tailwindcss": "4.2.4",
|
|
154
166
|
"typescript": "5.9.3",
|
|
155
|
-
"vue": "3.5.
|
|
167
|
+
"vue": "3.5.42",
|
|
168
|
+
"vue-i18n": "11.4.13",
|
|
156
169
|
"vue-tsc": "2.2.10"
|
|
170
|
+
},
|
|
171
|
+
"overrides": {
|
|
172
|
+
"archiver-utils": {
|
|
173
|
+
"glob": "13.0.6"
|
|
174
|
+
}
|
|
157
175
|
}
|
|
158
176
|
}
|
|
@@ -56,7 +56,6 @@ const visiblePostsCount = computed(() => Math.min(props.posts.length, props.tota
|
|
|
56
56
|
:busy="props.isLoading"
|
|
57
57
|
:text="t('uiVintage.blog.loadMoreWithNumbers', { current: visiblePostsCount, total: props.totalPosts })"
|
|
58
58
|
@load-more="handleLoadMorePosts"
|
|
59
|
-
class="mt-8"
|
|
60
|
-
data-aos="fade-up" />
|
|
59
|
+
class="mt-8" />
|
|
61
60
|
</div>
|
|
62
61
|
</template>
|
|
@@ -13,24 +13,15 @@ const props = defineProps<{
|
|
|
13
13
|
|
|
14
14
|
<style scoped>
|
|
15
15
|
.blog-content {
|
|
16
|
-
|
|
17
|
-
--blog-content-code-bg: var(--bg-selected-light);
|
|
18
|
-
--blog-content-inline-code-bg: #f9fafb;
|
|
19
|
-
--blog-content-inline-code-border: var(--border-light);
|
|
20
|
-
color: var(--blog-content-text);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
.dark .blog-content {
|
|
24
|
-
--blog-content-text: var(--text-primary-dark);
|
|
25
|
-
--blog-content-code-bg: var(--bg-selected-dark);
|
|
26
|
-
--blog-content-inline-code-bg: var(--bg-card-dark);
|
|
27
|
-
--blog-content-inline-code-border: var(--border-dark);
|
|
16
|
+
color: var(--foreground);
|
|
28
17
|
}
|
|
29
18
|
|
|
19
|
+
/* Headings */
|
|
30
20
|
.blog-content :deep(h1:not(.not-prose):not(.not-prose *)),
|
|
31
21
|
.blog-content :deep(h2:not(.not-prose):not(.not-prose *)),
|
|
32
22
|
.blog-content :deep(h3:not(.not-prose):not(.not-prose *)) {
|
|
33
|
-
color: var(--
|
|
23
|
+
color: var(--foreground);
|
|
24
|
+
letter-spacing: -0.01em;
|
|
34
25
|
scroll-margin-top: 6rem;
|
|
35
26
|
}
|
|
36
27
|
|
|
@@ -41,91 +32,195 @@ const props = defineProps<{
|
|
|
41
32
|
}
|
|
42
33
|
|
|
43
34
|
.blog-content :deep(h2:not(.not-prose):not(.not-prose *)) {
|
|
44
|
-
margin-top:
|
|
45
|
-
margin-bottom: 1.
|
|
35
|
+
margin-top: 3rem;
|
|
36
|
+
margin-bottom: 1.25rem;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.blog-content :deep(h3:not(.not-prose):not(.not-prose *)) {
|
|
40
|
+
margin-top: 2.25rem;
|
|
41
|
+
margin-bottom: 1rem;
|
|
46
42
|
}
|
|
47
43
|
|
|
48
44
|
.blog-content :deep(h2:not(.not-prose):not(.not-prose *) a:not(.not-prose):not(.not-prose *)) {
|
|
49
|
-
font-size: 1.
|
|
45
|
+
font-size: 1.5rem;
|
|
50
46
|
font-weight: 700;
|
|
51
47
|
line-height: 2rem;
|
|
52
48
|
}
|
|
53
49
|
|
|
54
|
-
.blog-content :deep(h3:not(.not-prose):not(.not-prose *)) {
|
|
55
|
-
margin-top: 2rem;
|
|
56
|
-
margin-bottom: 1rem;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
50
|
.blog-content :deep(h3:not(.not-prose):not(.not-prose *) a:not(.not-prose):not(.not-prose *)) {
|
|
60
|
-
font-size: 1.
|
|
51
|
+
font-size: 1.15rem;
|
|
61
52
|
font-weight: 700;
|
|
62
53
|
line-height: 1.75rem;
|
|
63
54
|
}
|
|
64
55
|
|
|
56
|
+
/* Heading anchors: a "#" fades in on hover */
|
|
57
|
+
.blog-content :deep(:is(h2, h3):not(.not-prose):not(.not-prose *) > a) {
|
|
58
|
+
position: relative;
|
|
59
|
+
color: inherit;
|
|
60
|
+
text-decoration: none;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.blog-content :deep(:is(h2, h3):not(.not-prose):not(.not-prose *) > a::before) {
|
|
64
|
+
content: '#';
|
|
65
|
+
position: absolute;
|
|
66
|
+
left: -1.1em;
|
|
67
|
+
color: var(--primary);
|
|
68
|
+
opacity: 0;
|
|
69
|
+
translate: 4px 0;
|
|
70
|
+
transition: opacity 0.2s var(--ease-out-expo), translate 0.3s var(--ease-spring);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.blog-content :deep(:is(h2, h3):not(.not-prose):not(.not-prose *):hover > a::before) {
|
|
74
|
+
opacity: 1;
|
|
75
|
+
translate: 0 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* Body text */
|
|
65
79
|
.blog-content :deep(p:not(.not-prose):not(.not-prose *)),
|
|
66
80
|
.blog-content :deep(li:not(.not-prose):not(.not-prose *)),
|
|
67
81
|
.blog-content :deep(blockquote:not(.not-prose):not(.not-prose *)),
|
|
68
82
|
.blog-content :deep(td:not(.not-prose):not(.not-prose *)),
|
|
69
83
|
.blog-content :deep(th:not(.not-prose):not(.not-prose *)) {
|
|
70
|
-
color: var(--
|
|
84
|
+
color: var(--foreground);
|
|
71
85
|
}
|
|
72
86
|
|
|
73
87
|
.blog-content :deep(p:not(.not-prose):not(.not-prose *)) {
|
|
74
88
|
font-size: 15px;
|
|
75
89
|
margin-top: 1rem;
|
|
76
90
|
margin-bottom: 1rem;
|
|
77
|
-
line-height: 1.
|
|
91
|
+
line-height: 1.85;
|
|
78
92
|
}
|
|
79
93
|
|
|
80
|
-
|
|
81
|
-
.blog-content :deep(
|
|
82
|
-
font-
|
|
94
|
+
/* Lede: the opening paragraph reads a little larger */
|
|
95
|
+
.blog-content :deep(p:not(.not-prose):not(.not-prose *):first-of-type) {
|
|
96
|
+
font-size: 17px;
|
|
97
|
+
line-height: 1.8;
|
|
98
|
+
color: var(--muted-foreground);
|
|
83
99
|
}
|
|
84
100
|
|
|
85
|
-
.blog-content :deep(
|
|
86
|
-
.blog-content :deep(li:not(.not-prose):not(.not-prose *) a:not(.not-prose):not(.not-prose *)),
|
|
87
|
-
.blog-content :deep(blockquote:not(.not-prose):not(.not-prose *) a:not(.not-prose):not(.not-prose *)) {
|
|
88
|
-
color: var(--blog-content-text);
|
|
89
|
-
text-decoration: none;
|
|
101
|
+
.blog-content :deep(:is(p, blockquote, li):not(.not-prose):not(.not-prose *) strong) {
|
|
90
102
|
font-weight: 700;
|
|
103
|
+
color: var(--foreground);
|
|
91
104
|
}
|
|
92
105
|
|
|
93
|
-
|
|
94
|
-
.blog-content :deep(li:not(.not-prose):not(.not-prose *) a:not(.not-prose):not(.not-prose *)
|
|
95
|
-
|
|
106
|
+
/* Links: a primary underline that thickens on hover */
|
|
107
|
+
.blog-content :deep(:is(p, li, blockquote):not(.not-prose):not(.not-prose *) a:not(.not-prose):not(.not-prose *)) {
|
|
108
|
+
color: var(--foreground);
|
|
109
|
+
font-weight: 700;
|
|
96
110
|
text-decoration: underline;
|
|
111
|
+
text-decoration-color: color-mix(in oklab, var(--primary) 45%, transparent);
|
|
112
|
+
text-decoration-thickness: 2px;
|
|
113
|
+
text-underline-offset: 4px;
|
|
114
|
+
transition: color 0.15s, text-decoration-color 0.15s, text-decoration-thickness 0.15s;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.blog-content :deep(:is(p, li, blockquote):not(.not-prose):not(.not-prose *) a:not(.not-prose):not(.not-prose *):hover) {
|
|
118
|
+
color: var(--primary);
|
|
119
|
+
text-decoration-color: var(--primary);
|
|
120
|
+
text-decoration-thickness: 3px;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* Lists */
|
|
124
|
+
.blog-content :deep(:is(ul, ol):not(.not-prose):not(.not-prose *)) {
|
|
125
|
+
margin: 1.25rem 0;
|
|
126
|
+
padding-left: 1.4rem;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.blog-content :deep(ul:not(.not-prose):not(.not-prose *)) {
|
|
130
|
+
list-style: square;
|
|
97
131
|
}
|
|
98
132
|
|
|
99
|
-
.blog-content :deep(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
.blog-content :deep(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
133
|
+
.blog-content :deep(ol:not(.not-prose):not(.not-prose *)) {
|
|
134
|
+
list-style: decimal;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.blog-content :deep(li:not(.not-prose):not(.not-prose *)) {
|
|
138
|
+
font-size: 15px;
|
|
139
|
+
line-height: 1.8;
|
|
140
|
+
margin: 0.4rem 0;
|
|
141
|
+
padding-left: 0.25rem;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.blog-content :deep(li:not(.not-prose):not(.not-prose *)::marker) {
|
|
145
|
+
color: var(--primary);
|
|
146
|
+
font-weight: 700;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* Blockquotes */
|
|
150
|
+
.blog-content :deep(blockquote:not(.not-prose):not(.not-prose *)) {
|
|
151
|
+
position: relative;
|
|
152
|
+
margin: 2rem 0;
|
|
153
|
+
padding: 1rem 1.25rem 1rem 1.5rem;
|
|
154
|
+
border: 1px solid var(--border);
|
|
155
|
+
border-left: 3px solid var(--primary);
|
|
156
|
+
border-radius: var(--radius-sm);
|
|
157
|
+
background-color: var(--surface);
|
|
158
|
+
font-size: 15px;
|
|
159
|
+
font-style: italic;
|
|
160
|
+
line-height: 1.8;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.blog-content :deep(blockquote:not(.not-prose):not(.not-prose *) p) {
|
|
164
|
+
margin: 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* Inline code */
|
|
168
|
+
.blog-content :deep(:is(p, li, blockquote, td, th):not(.not-prose):not(.not-prose *) code:not(.not-prose):not(.not-prose *)) {
|
|
169
|
+
color: var(--foreground);
|
|
170
|
+
background-color: var(--surface);
|
|
171
|
+
border: 1px solid var(--border);
|
|
107
172
|
border-radius: 0.25rem;
|
|
108
173
|
display: inline-block;
|
|
109
174
|
font-size: 0.75rem;
|
|
110
175
|
font-weight: 600;
|
|
111
176
|
line-height: 1rem;
|
|
112
177
|
padding: 0.125rem 0.5rem;
|
|
113
|
-
transition: color 0.3s ease-in-out, background-color 0.3s ease-in-out, border-color 0.3s ease-in-out;
|
|
114
178
|
white-space: nowrap;
|
|
115
179
|
}
|
|
116
180
|
|
|
117
181
|
.blog-content :deep(pre:not(.not-prose):not(.not-prose *) code:not(.not-prose):not(.not-prose *)) {
|
|
118
|
-
color: var(--
|
|
119
|
-
background-color: var(--
|
|
182
|
+
color: var(--foreground);
|
|
183
|
+
background-color: var(--accent);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* Images */
|
|
187
|
+
.blog-content :deep(img:not(.not-prose):not(.not-prose *)) {
|
|
188
|
+
border: 1px solid var(--border);
|
|
189
|
+
border-radius: var(--radius-sm);
|
|
190
|
+
box-shadow: var(--uv-shadow-sm);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* Plain markdown tables */
|
|
194
|
+
.blog-content :deep(table:not(.not-prose):not(.not-prose *)) {
|
|
195
|
+
width: 100%;
|
|
196
|
+
margin: 2rem 0;
|
|
197
|
+
border-collapse: collapse;
|
|
198
|
+
font-size: 14px;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.blog-content :deep(:is(th, td):not(.not-prose):not(.not-prose *)) {
|
|
202
|
+
border-bottom: 1px solid var(--border);
|
|
203
|
+
padding: 0.65rem 0.75rem;
|
|
204
|
+
text-align: left;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.blog-content :deep(th:not(.not-prose):not(.not-prose *)) {
|
|
208
|
+
background-color: var(--surface);
|
|
209
|
+
font-weight: 700;
|
|
120
210
|
}
|
|
121
211
|
|
|
122
212
|
@media (max-width: 640px) {
|
|
123
|
-
.blog-content :deep(p:not(.not-prose):not(.not-prose *)) {
|
|
124
|
-
font-size:
|
|
125
|
-
line-height: 1.
|
|
213
|
+
.blog-content :deep(:is(p, li):not(.not-prose):not(.not-prose *)) {
|
|
214
|
+
font-size: 14px;
|
|
215
|
+
line-height: 1.8;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.blog-content :deep(p:not(.not-prose):not(.not-prose *):first-of-type) {
|
|
219
|
+
font-size: 15px;
|
|
126
220
|
}
|
|
127
221
|
|
|
128
222
|
.blog-content :deep(h2:not(.not-prose):not(.not-prose *)) {
|
|
223
|
+
margin-top: 2.25rem;
|
|
129
224
|
margin-bottom: 1rem;
|
|
130
225
|
}
|
|
131
226
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from 'vue';
|
|
3
3
|
import { useI18n } from 'vue-i18n';
|
|
4
|
-
import {
|
|
4
|
+
import { HelpCircleIcon } from '@hugeicons/core-free-icons';
|
|
5
|
+
import { Accordion } from '../ui/accordion';
|
|
5
6
|
import BlogSectionTitle from './BlogSectionTitle.vue';
|
|
6
7
|
|
|
7
8
|
// Props
|
|
@@ -20,21 +21,18 @@ const hasValidFAQs = computed(() => props.faqs && props.faqs.length > 0);
|
|
|
20
21
|
|
|
21
22
|
<template>
|
|
22
23
|
<div v-if="hasValidFAQs" class="mt-12 lg:mt-16">
|
|
23
|
-
<!-- Title -->
|
|
24
|
-
<BlogSectionTitle :title="t('uiVintage.blog.faq.title')" />
|
|
24
|
+
<!-- Title and description -->
|
|
25
|
+
<BlogSectionTitle :title="t('uiVintage.blog.faq.title')" :description="t('uiVintage.blog.faq.description')" />
|
|
25
26
|
|
|
26
|
-
<!--
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
{{ faq.answer }}
|
|
36
|
-
</p>
|
|
37
|
-
</Accordion>
|
|
27
|
+
<!-- FAQ items -->
|
|
28
|
+
<div class="space-y-3">
|
|
29
|
+
<div v-for="(faq, index) in props.faqs" :key="index" data-aos="blur-up" :data-aos-delay="index * 70">
|
|
30
|
+
<Accordion :title="faq.question" :icon="HelpCircleIcon" :initially-expanded="index === 0">
|
|
31
|
+
<p class="text-xs leading-relaxed text-muted-foreground md:text-sm">
|
|
32
|
+
{{ faq.answer }}
|
|
33
|
+
</p>
|
|
34
|
+
</Accordion>
|
|
35
|
+
</div>
|
|
38
36
|
</div>
|
|
39
37
|
</div>
|
|
40
38
|
</template>
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import { useI18n } from 'vue-i18n';
|
|
4
|
+
import { ArrowRight01Icon } from '@hugeicons/core-free-icons';
|
|
5
|
+
import { HugeiconsIcon } from '@hugeicons/vue';
|
|
6
|
+
import type { HugeiconsIconDefinition } from '../../lib/common-types';
|
|
2
7
|
import type { ToneColor } from '../../lib/color-tokens';
|
|
8
|
+
import { cn } from '../../lib/utils';
|
|
3
9
|
import { Badge } from '../ui/badge';
|
|
10
|
+
import { StatStrip } from '../stat-strip';
|
|
4
11
|
|
|
5
12
|
// Props
|
|
6
13
|
const props = withDefaults(defineProps<{
|
|
@@ -8,31 +15,116 @@ const props = withDefaults(defineProps<{
|
|
|
8
15
|
color: ToneColor;
|
|
9
16
|
text: string;
|
|
10
17
|
}>;
|
|
18
|
+
announcement?: {
|
|
19
|
+
text: string;
|
|
20
|
+
to: string;
|
|
21
|
+
label?: string;
|
|
22
|
+
} | null;
|
|
11
23
|
title: string;
|
|
24
|
+
highlight?: string;
|
|
12
25
|
description: string;
|
|
26
|
+
stats?: Array<{
|
|
27
|
+
label: string;
|
|
28
|
+
value: string | number;
|
|
29
|
+
icon?: HugeiconsIconDefinition;
|
|
30
|
+
}>;
|
|
31
|
+
align?: 'left' | 'center' | 'right';
|
|
13
32
|
}>(), {
|
|
14
|
-
badges: () => []
|
|
33
|
+
badges: () => [],
|
|
34
|
+
announcement: null,
|
|
35
|
+
highlight: '',
|
|
36
|
+
stats: () => [],
|
|
37
|
+
align: 'center'
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const { t } = useI18n();
|
|
41
|
+
|
|
42
|
+
// Alignment of the whole header: the column, its text, and the rows of badges and stats
|
|
43
|
+
const alignClasses = {
|
|
44
|
+
left: { header: 'items-start text-left', row: 'justify-start' },
|
|
45
|
+
center: { header: 'items-center text-center', row: 'justify-center' },
|
|
46
|
+
right: { header: 'items-end text-right', row: 'justify-end' }
|
|
47
|
+
} as const;
|
|
48
|
+
const alignClass = computed(() => alignClasses[props.align] ?? alignClasses.center);
|
|
49
|
+
|
|
50
|
+
// Split the title around the highlighted part (when it appears in the title)
|
|
51
|
+
const titleParts = computed(() => {
|
|
52
|
+
const index = props.highlight ? props.title.indexOf(props.highlight) : -1;
|
|
53
|
+
if (index === -1) return { before: props.title, highlight: '', after: '' };
|
|
54
|
+
return {
|
|
55
|
+
before: props.title.slice(0, index),
|
|
56
|
+
highlight: props.highlight,
|
|
57
|
+
after: props.title.slice(index + props.highlight.length)
|
|
58
|
+
};
|
|
15
59
|
});
|
|
60
|
+
|
|
61
|
+
// Words that rise in one by one (the highlight rises in as a single block between them)
|
|
62
|
+
const splitWords = (text: string) => text.split(/\s+/).filter(Boolean);
|
|
63
|
+
const beforeWords = computed(() => splitWords(titleParts.value.before));
|
|
64
|
+
const afterWords = computed(() => splitWords(titleParts.value.after));
|
|
65
|
+
// Space after a word before the highlight: always between words, and before the highlight only when the title has one there
|
|
66
|
+
const wordGap = (index: number) => {
|
|
67
|
+
if (index < beforeWords.value.length - 1) return ' ';
|
|
68
|
+
return titleParts.value.highlight && /\s$/.test(titleParts.value.before) ? ' ' : '';
|
|
69
|
+
};
|
|
70
|
+
const wordDelay = (index: number) => `${120 + Math.min(index, 12) * 55}ms`;
|
|
71
|
+
const highlightDelay = computed(() => wordDelay(beforeWords.value.length));
|
|
72
|
+
const afterDelay = (index: number) => wordDelay(beforeWords.value.length + 1 + index);
|
|
73
|
+
|
|
74
|
+
// The rest of the header follows the last word of the title
|
|
75
|
+
const titleLength = computed(() => beforeWords.value.length + afterWords.value.length + (titleParts.value.highlight ? 1 : 0));
|
|
76
|
+
const descriptionDelay = computed(() => `${Math.min(titleLength.value, 12) * 55 + 200}ms`);
|
|
77
|
+
const statsDelay = computed(() => `${Math.min(titleLength.value, 12) * 55 + 300}ms`);
|
|
78
|
+
const underlineDelay = computed(() => `${beforeWords.value.length * 55 + 600}ms`);
|
|
16
79
|
</script>
|
|
17
80
|
|
|
18
81
|
<template>
|
|
19
|
-
<
|
|
82
|
+
<header :class="cn('relative flex flex-col gap-6 pt-6 md:pt-12', alignClass.header)">
|
|
83
|
+
<!-- Announcement (links to something new, such as the latest post) -->
|
|
84
|
+
<NuxtLink v-if="props.announcement" :to="props.announcement.to" class="group/announce max-w-full animate-uv-fade-up rounded outline-none focus-visible:ring-[3px] focus-visible:ring-ring/45">
|
|
85
|
+
<span class="inline-flex max-w-full items-center gap-2 rounded border border-warning/35 bg-warning/10 px-3 py-1 text-xs text-foreground transition-colors duration-150 group-hover/announce:border-warning/70">
|
|
86
|
+
<span aria-hidden="true" class="relative flex size-1.5 shrink-0 text-warning">
|
|
87
|
+
<span class="absolute inline-flex size-full animate-uv-ping-soft rounded-full bg-current opacity-60" />
|
|
88
|
+
<span class="relative inline-flex size-1.5 rounded-full bg-current" />
|
|
89
|
+
</span>
|
|
90
|
+
<span class="shrink-0 font-semibold">{{ props.announcement.label || t('uiVintage.blog.new') }}</span>
|
|
91
|
+
<span class="truncate opacity-80">{{ props.announcement.text }}</span>
|
|
92
|
+
<HugeiconsIcon :icon="ArrowRight01Icon" class="size-3.5 shrink-0 transition-transform duration-300 ease-spring group-hover/announce:translate-x-0.5" />
|
|
93
|
+
</span>
|
|
94
|
+
</NuxtLink>
|
|
95
|
+
|
|
20
96
|
<!-- Badges -->
|
|
21
|
-
<div v-if="props.badges.length > 0" class="flex flex-wrap gap-
|
|
22
|
-
<Badge v-for="badge in props.badges"
|
|
97
|
+
<div v-if="props.badges.length > 0" :class="cn('flex flex-wrap gap-2', alignClass.row)">
|
|
98
|
+
<Badge v-for="(badge, index) in props.badges"
|
|
23
99
|
:key="`${badge.color}-${badge.text}`"
|
|
24
100
|
:color="badge.color"
|
|
25
|
-
:text="badge.text"
|
|
101
|
+
:text="badge.text"
|
|
102
|
+
:pulse="index === 0 && !props.announcement"
|
|
103
|
+
class="animate-uv-fade-up"
|
|
104
|
+
:style="{ animationDelay: `${index * 70}ms` }" />
|
|
26
105
|
</div>
|
|
27
106
|
|
|
28
|
-
<!-- Title -->
|
|
29
|
-
<h1 class="
|
|
30
|
-
|
|
107
|
+
<!-- Title (each word rises in with a spring, the highlight gets a hand-drawn underline) -->
|
|
108
|
+
<h1 class="max-w-4xl text-4xl leading-[1.05] font-semibold tracking-tight text-foreground sm:text-5xl md:text-6xl">
|
|
109
|
+
<template v-for="(word, index) in beforeWords" :key="`before-${word}-${index}`">
|
|
110
|
+
<span class="inline-block animate-uv-word-in" :style="{ animationDelay: wordDelay(index) }">{{ word }}</span>{{ wordGap(index) }}
|
|
111
|
+
</template>
|
|
112
|
+
<!-- The text and the underline sit on one line so no stray space ends up inside the highlight -->
|
|
113
|
+
<span v-if="titleParts.highlight" class="relative inline-block animate-uv-word-in text-primary" :style="{ animationDelay: highlightDelay }">{{ titleParts.highlight }}<svg class="absolute -bottom-2 left-0 h-3 w-full text-primary/60" viewBox="0 0 200 12" fill="none" preserveAspectRatio="none" aria-hidden="true">
|
|
114
|
+
<path d="M2 9C40 3 80 2 198 7" stroke="currentColor" stroke-width="4" stroke-linecap="round" pathLength="1" class="animate-uv-draw [stroke-dasharray:1]" :style="{ animationDelay: underlineDelay }" />
|
|
115
|
+
</svg>
|
|
116
|
+
</span>
|
|
117
|
+
<template v-for="(word, index) in afterWords" :key="`after-${word}-${index}`">
|
|
118
|
+
{{ index === 0 && !/^\s/.test(titleParts.after) ? '' : ' ' }}<span class="inline-block animate-uv-word-in" :style="{ animationDelay: afterDelay(index) }">{{ word }}</span>
|
|
119
|
+
</template>
|
|
31
120
|
</h1>
|
|
32
121
|
|
|
33
122
|
<!-- Description -->
|
|
34
|
-
<p class="text-sm
|
|
123
|
+
<p class="max-w-2xl animate-uv-fade-up text-sm leading-7 text-muted-foreground sm:text-base" :style="{ animationDelay: descriptionDelay }">
|
|
35
124
|
{{ props.description }}
|
|
36
125
|
</p>
|
|
37
|
-
|
|
38
|
-
|
|
126
|
+
|
|
127
|
+
<!-- Stats strip -->
|
|
128
|
+
<StatStrip :items="props.stats" :class="cn('animate-uv-fade-up', alignClass.row)" :style="{ animationDelay: statsDelay }" />
|
|
129
|
+
</header>
|
|
130
|
+
</template>
|