@brumaombra/ui-vintage 0.8.0 → 0.8.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.
- package/COMPONENTS.md +46 -2
- package/README.md +1 -1
- package/package.json +3 -2
- 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 +27 -9
- package/src/components/blog/BlogInfoSection.vue +37 -36
- package/src/components/blog/BlogProgressBar.vue +5 -2
- package/src/components/blog/BlogSectionTitle.vue +24 -5
- package/src/components/blog/BlogTagsSection.vue +8 -5
- package/src/components/blog/CategoriesList.vue +2 -2
- package/src/components/blog/CategoryCard.vue +25 -16
- package/src/components/blog/HeaderCarousel.vue +109 -146
- package/src/components/blog/PostCard.vue +34 -35
- package/src/components/blog/PostsList.vue +3 -2
- package/src/components/blog/SocialShareSidebar.vue +51 -41
- package/src/components/blog/TableOfContents.vue +14 -10
- package/src/components/content/BlogList.vue +16 -4
- package/src/components/content/BlogTable.vue +4 -4
- package/src/components/content/CodeBlock.vue +5 -5
- package/src/components/content/Flow.vue +4 -4
- package/src/components/content/ProseHr.vue +5 -5
- package/src/components/content/Terminal.vue +10 -4
- package/src/components/cookie-consent/CookieConsent.vue +82 -0
- package/src/components/cookie-consent/index.ts +3 -0
- package/src/components/cookie-consent/useCookieConsent.ts +55 -0
- package/src/components/landing/landing-footer/LandingFooter.vue +9 -4
- package/src/components/landing/landing-navbar/LandingNavbar.vue +47 -51
- package/src/components/landing/landing-shell/LandingShell.vue +7 -2
- package/src/components/ui/accordion/Accordion.vue +6 -6
- package/src/components/ui/animated-number/AnimatedNumber.vue +19 -5
- package/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue +1 -1
- 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/card/Card.vue +1 -1
- package/src/components/ui/command/CommandItem.vue +2 -2
- package/src/i18n/de.json +10 -1
- package/src/i18n/en.json +10 -1
- package/src/i18n/es.json +10 -1
- package/src/i18n/fr.json +10 -1
- package/src/i18n/it.json +10 -1
- package/src/i18n/ja.json +10 -1
- package/src/i18n/pt.json +10 -1
- package/src/i18n/ru.json +10 -1
- package/src/i18n/zh.json +10 -1
- package/src/styles.css +40 -0
package/COMPONENTS.md
CHANGED
|
@@ -20,7 +20,7 @@ This file lists every public entry point of the package with its components, pro
|
|
|
20
20
|
- **Data display:** [animated-number](#brumaombraui-vintageanimated-number), [avatar](#brumaombraui-vintageavatar), [badge](#brumaombraui-vintagebadge), [card](#brumaombraui-vintagecard), [card-grid](#brumaombraui-vintagecard-grid), [chip](#brumaombraui-vintagechip), [data-list](#brumaombraui-vintagedata-list), [data-table](#brumaombraui-vintagedata-table), [info-card](#brumaombraui-vintageinfo-card), [progress](#brumaombraui-vintageprogress), [progress-component](#brumaombraui-vintageprogress-component), [scroll-area](#brumaombraui-vintagescroll-area), [separator](#brumaombraui-vintageseparator), [single-value-card](#brumaombraui-vintagesingle-value-card), [skeleton](#brumaombraui-vintageskeleton), [table](#brumaombraui-vintagetable)
|
|
21
21
|
- **Navigation:** [accordion](#brumaombraui-vintageaccordion), [breadcrumb](#brumaombraui-vintagebreadcrumb), [collapsible](#brumaombraui-vintagecollapsible), [pagination](#brumaombraui-vintagepagination), [sidebar](#brumaombraui-vintagesidebar), [stepper](#brumaombraui-vintagestepper), [tabs](#brumaombraui-vintagetabs)
|
|
22
22
|
- **Overlays:** [alert-dialog](#brumaombraui-vintagealert-dialog), [command](#brumaombraui-vintagecommand), [dialog](#brumaombraui-vintagedialog), [hover-card](#brumaombraui-vintagehover-card), [popover](#brumaombraui-vintagepopover), [sheet](#brumaombraui-vintagesheet), [tooltip](#brumaombraui-vintagetooltip)
|
|
23
|
-
- **Feedback:** [alert](#brumaombraui-vintagealert), [busy](#brumaombraui-vintagebusy), [busy-indicator](#brumaombraui-vintagebusy-indicator), [confirm-dialog](#brumaombraui-vintageconfirm-dialog), [empty-state-card](#brumaombraui-vintageempty-state-card), [loading-state-card](#brumaombraui-vintageloading-state-card), [message-dialog](#brumaombraui-vintagemessage-dialog), [message-toast](#brumaombraui-vintagemessage-toast), [spinner](#brumaombraui-vintagespinner)
|
|
23
|
+
- **Feedback:** [alert](#brumaombraui-vintagealert), [busy](#brumaombraui-vintagebusy), [busy-indicator](#brumaombraui-vintagebusy-indicator), [confirm-dialog](#brumaombraui-vintageconfirm-dialog), [cookie-consent](#brumaombraui-vintagecookie-consent), [empty-state-card](#brumaombraui-vintageempty-state-card), [loading-state-card](#brumaombraui-vintageloading-state-card), [message-dialog](#brumaombraui-vintagemessage-dialog), [message-toast](#brumaombraui-vintagemessage-toast), [spinner](#brumaombraui-vintagespinner)
|
|
24
24
|
- **Layouts and app shell:** [background-grid](#brumaombraui-vintagebackground-grid), [dashboard-shell](#brumaombraui-vintagedashboard-shell), [error-page](#brumaombraui-vintageerror-page), [landing](#brumaombraui-vintagelanding), [landing-content](#brumaombraui-vintagelanding-content), [landing-footer](#brumaombraui-vintagelanding-footer), [landing-navbar](#brumaombraui-vintagelanding-navbar), [landing-shell](#brumaombraui-vintagelanding-shell), [language-flag](#brumaombraui-vintagelanguage-flag), [language-selector](#brumaombraui-vintagelanguage-selector), [page-header](#brumaombraui-vintagepage-header), [theme-selector](#brumaombraui-vintagetheme-selector)
|
|
25
25
|
- **Content and blog:** [blog](#brumaombraui-vintageblog), [content](#brumaombraui-vintagecontent)
|
|
26
26
|
- **Utilities and types:** [common-types](#brumaombraui-vintagecommon-types), [utils](#brumaombraui-vintageutils)
|
|
@@ -2814,6 +2814,47 @@ interface ShowConfirmDialogOptions {
|
|
|
2814
2814
|
|
|
2815
2815
|
---
|
|
2816
2816
|
|
|
2817
|
+
### `@brumaombra/ui-vintage/cookie-consent`
|
|
2818
|
+
|
|
2819
|
+
Cookie consent banner fixed to the bottom of the page, shown in the browser until the visitor accepts or declines. The choice is saved in `localStorage` under `storage-key`; `useCookieConsent(storageKey)` shares it, so optional scripts (analytics, ads) can wait for `consent.value === "granted"` and a settings page can call `reset()` to ask again. Texts have translated defaults, `policy-to` adds a link to the cookie policy.
|
|
2820
|
+
|
|
2821
|
+
```ts
|
|
2822
|
+
import { CookieConsent, useCookieConsent } from '@brumaombra/ui-vintage/cookie-consent';
|
|
2823
|
+
import type { CookieConsentValue } from '@brumaombra/ui-vintage/cookie-consent';
|
|
2824
|
+
```
|
|
2825
|
+
|
|
2826
|
+
```vue
|
|
2827
|
+
<CookieConsent storage-key="analyticsConsent" policy-to="/cookie-policy" @accept="loadAnalytics" />
|
|
2828
|
+
|
|
2829
|
+
<script setup>
|
|
2830
|
+
const { consent, reset } = useCookieConsent('analyticsConsent');
|
|
2831
|
+
watch(consent, value => value === 'granted' && loadAnalytics(), { immediate: true });
|
|
2832
|
+
</script>
|
|
2833
|
+
```
|
|
2834
|
+
|
|
2835
|
+
#### `CookieConsent`
|
|
2836
|
+
|
|
2837
|
+
| Prop | Type | Default |
|
|
2838
|
+
| --- | --- | --- |
|
|
2839
|
+
| `storageKey` | `string` | `'cookieConsent'` |
|
|
2840
|
+
| `message` | `string` | `undefined` |
|
|
2841
|
+
| `policyTo` | `string` | `undefined` |
|
|
2842
|
+
| `policyLabel` | `string` | `undefined` |
|
|
2843
|
+
| `acceptLabel` | `string` | `undefined` |
|
|
2844
|
+
| `declineLabel` | `string` | `undefined` |
|
|
2845
|
+
| `class` | `HTMLAttributes['class']` | `undefined` |
|
|
2846
|
+
|
|
2847
|
+
- **Emits:** `accept`, `decline`.
|
|
2848
|
+
|
|
2849
|
+
#### Functions, constants, and types
|
|
2850
|
+
|
|
2851
|
+
```ts
|
|
2852
|
+
function useCookieConsent(storageKey = 'cookieConsent')
|
|
2853
|
+
type CookieConsentValue = 'granted' | 'denied' | null
|
|
2854
|
+
```
|
|
2855
|
+
|
|
2856
|
+
---
|
|
2857
|
+
|
|
2817
2858
|
### `@brumaombra/ui-vintage/empty-state-card`
|
|
2818
2859
|
|
|
2819
2860
|
Empty state with an icon, title, description, and an `action` slot. Falls back to localized default copy.
|
|
@@ -3383,7 +3424,7 @@ Components for Nuxt Content pages and a complete blog.
|
|
|
3383
3424
|
|
|
3384
3425
|
### `@brumaombra/ui-vintage/blog`
|
|
3385
3426
|
|
|
3386
|
-
Blog building blocks: header carousel, post
|
|
3427
|
+
Blog building blocks: an animated hero header, a full-bleed featured carousel, post and category cards, tags, table of contents, reading progress, FAQ, share sidebar, and the content renderer. Scroll entrances use AOS: install `aos` in the app and initialize it (the components carry `data-aos="blur-up"`, and the library stylesheet defines that animation). Without AOS the content simply renders without entrances.
|
|
3387
3428
|
|
|
3388
3429
|
```ts
|
|
3389
3430
|
import { AllPostsSection, BlogContentRenderer, BlogFAQSection, BlogInfoSection, BlogHeaderSection, BlogSectionTitle, BlogTagsSection, CategoriesList, CategoriesSection, CategoryCard, HeaderCarousel, PostCard, PostsList, SocialShareSidebar, TableOfContents } from '@brumaombra/ui-vintage/blog';
|
|
@@ -3437,6 +3478,9 @@ import { AllPostsSection, BlogContentRenderer, BlogFAQSection, BlogInfoSection,
|
|
|
3437
3478
|
| Prop | Type | Default |
|
|
3438
3479
|
| --- | --- | --- |
|
|
3439
3480
|
| `title` *(required)* | `string` | |
|
|
3481
|
+
| `description` | `string` | `''` |
|
|
3482
|
+
|
|
3483
|
+
- **Slots:** `action`.
|
|
3440
3484
|
|
|
3441
3485
|
#### `BlogTagsSection`
|
|
3442
3486
|
|
package/README.md
CHANGED
|
@@ -358,7 +358,7 @@ There is no required library build step before publishing. The normal verificati
|
|
|
358
358
|
npm run typecheck
|
|
359
359
|
```
|
|
360
360
|
|
|
361
|
-
For release
|
|
361
|
+
For the release process, see the project skill in [`.claude/skills/publish-npm/SKILL.md`](.claude/skills/publish-npm/SKILL.md). The package is published under the public npm scope `@brumaombra/ui-vintage`.
|
|
362
362
|
|
|
363
363
|
<a id="requirements"></a>
|
|
364
364
|
## 🧰 Requirements
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brumaombra/ui-vintage",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
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",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"./command": "./src/components/ui/command/index.ts",
|
|
60
60
|
"./common-types": "./src/lib/common-types.ts",
|
|
61
61
|
"./confirm-dialog": "./src/components/confirm-dialog/index.ts",
|
|
62
|
+
"./cookie-consent": "./src/components/cookie-consent/index.ts",
|
|
62
63
|
"./content": "./src/components/content/index.ts",
|
|
63
64
|
"./dashboard-shell": "./src/components/dashboard-shell/index.ts",
|
|
64
65
|
"./data-list": "./src/components/data-list/index.ts",
|
|
@@ -132,7 +133,7 @@
|
|
|
132
133
|
"@nuxt/image": "^2.0.0",
|
|
133
134
|
"nuxt": "^4.0.0",
|
|
134
135
|
"vue": "^3.5.0",
|
|
135
|
-
"vue-i18n": "^9.0.0 || ^10.0.0"
|
|
136
|
+
"vue-i18n": "^9.0.0 || ^10.0.0 || ^11.0.0"
|
|
136
137
|
},
|
|
137
138
|
"dependencies": {
|
|
138
139
|
"@hugeicons/core-free-icons": "4.1.1",
|
|
@@ -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-md);
|
|
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,4 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
2
3
|
import type { ToneColor } from '../../lib/color-tokens';
|
|
3
4
|
import { Badge } from '../ui/badge';
|
|
4
5
|
|
|
@@ -13,26 +14,43 @@ const props = withDefaults(defineProps<{
|
|
|
13
14
|
}>(), {
|
|
14
15
|
badges: () => []
|
|
15
16
|
});
|
|
17
|
+
|
|
18
|
+
// Split the title so each word can rise into place on its own
|
|
19
|
+
const titleWords = computed(() => props.title.split(/\s+/).filter(Boolean));
|
|
20
|
+
|
|
21
|
+
// The description follows the last word of the title
|
|
22
|
+
const descriptionDelay = computed(() => `${Math.min(titleWords.value.length, 12) * 55 + 200}ms`);
|
|
16
23
|
</script>
|
|
17
24
|
|
|
18
25
|
<template>
|
|
19
|
-
<
|
|
26
|
+
<header class="relative isolate flex flex-col gap-6 pt-4 pb-2 md:gap-8 md:pt-10">
|
|
27
|
+
<!-- Ambient glow -->
|
|
28
|
+
<div aria-hidden="true" class="pointer-events-none absolute -top-24 -left-24 -z-10 h-80 w-[36rem] max-w-[120%] rounded-full bg-primary/12 blur-[100px]" />
|
|
29
|
+
|
|
20
30
|
<!-- Badges -->
|
|
21
|
-
<div v-if="props.badges.length > 0" class="flex flex-wrap gap-3
|
|
22
|
-
<Badge v-for="badge in props.badges"
|
|
31
|
+
<div v-if="props.badges.length > 0" class="flex flex-wrap gap-3">
|
|
32
|
+
<Badge v-for="(badge, index) in props.badges"
|
|
23
33
|
:key="`${badge.color}-${badge.text}`"
|
|
24
34
|
:color="badge.color"
|
|
25
|
-
:text="badge.text"
|
|
35
|
+
:text="badge.text"
|
|
36
|
+
:pulse="index === 0"
|
|
37
|
+
class="animate-uv-fade-up"
|
|
38
|
+
:style="{ animationDelay: `${index * 70}ms` }" />
|
|
26
39
|
</div>
|
|
27
40
|
|
|
28
|
-
<!-- Title -->
|
|
29
|
-
<h1 class="text-3xl
|
|
30
|
-
|
|
41
|
+
<!-- Title (each word rises in with a spring) -->
|
|
42
|
+
<h1 class="text-3xl leading-[1.05] font-bold tracking-tight text-foreground md:text-6xl">
|
|
43
|
+
<template v-for="(word, index) in titleWords" :key="`${word}-${index}`">
|
|
44
|
+
<span class="inline-block animate-uv-word-in" :style="{ animationDelay: `${120 + Math.min(index, 12) * 55}ms` }">{{ word }}</span>{{ index < titleWords.length - 1 ? ' ' : '' }}
|
|
45
|
+
</template>
|
|
31
46
|
</h1>
|
|
32
47
|
|
|
48
|
+
<!-- Accent rule -->
|
|
49
|
+
<div aria-hidden="true" class="h-1 w-20 origin-left animate-uv-grow-x rounded-full bg-primary shadow-glow" :style="{ animationDelay: descriptionDelay }" />
|
|
50
|
+
|
|
33
51
|
<!-- Description -->
|
|
34
|
-
<p class="text-sm
|
|
52
|
+
<p class="max-w-3xl animate-uv-fade-up text-sm leading-relaxed text-muted-foreground md:text-xl" :style="{ animationDelay: descriptionDelay }">
|
|
35
53
|
{{ props.description }}
|
|
36
54
|
</p>
|
|
37
|
-
</
|
|
55
|
+
</header>
|
|
38
56
|
</template>
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { NuxtImg } from '#components';
|
|
3
3
|
import { computed } from 'vue';
|
|
4
4
|
import { useI18n } from 'vue-i18n';
|
|
5
|
+
import { ArrowUpRight01Icon, Calendar03Icon, RefreshIcon } from '@hugeicons/core-free-icons';
|
|
6
|
+
import { HugeiconsIcon } from '@hugeicons/vue';
|
|
5
7
|
import { Card, CardContent } from '../ui/card';
|
|
6
8
|
import { formatDateLongItalyTimezone } from '../../lib/utils';
|
|
7
9
|
|
|
@@ -21,50 +23,49 @@ const { t, locale } = useI18n();
|
|
|
21
23
|
// Formatted dates
|
|
22
24
|
const formattedPublishedDate = computed(() => formatDateLongItalyTimezone(props.datePublished, locale.value));
|
|
23
25
|
const formattedModifiedDate = computed(() => formatDateLongItalyTimezone(props.dateModified, locale.value));
|
|
26
|
+
|
|
27
|
+
// Only show the update date when it differs from the publication date
|
|
28
|
+
const showModifiedDate = computed(() => Boolean(props.dateModified) && formattedModifiedDate.value !== formattedPublishedDate.value);
|
|
24
29
|
</script>
|
|
25
30
|
|
|
26
31
|
<template>
|
|
27
32
|
<!-- Author and date information -->
|
|
28
|
-
<Card data-aos="
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
<Card data-aos="blur-up" class="group/author relative overflow-hidden">
|
|
34
|
+
<!-- Accent glow -->
|
|
35
|
+
<div aria-hidden="true" class="pointer-events-none absolute -top-16 -left-16 size-48 rounded-full bg-primary/10 blur-3xl" />
|
|
36
|
+
|
|
37
|
+
<CardContent class="relative flex flex-col gap-5 sm:flex-row sm:items-center sm:justify-between">
|
|
38
|
+
<!-- Author -->
|
|
39
|
+
<div class="flex items-center gap-4">
|
|
40
|
+
<!-- Author image -->
|
|
41
|
+
<NuxtLink :to="props.authorUrl" target="_blank" rel="noopener noreferrer" class="relative shrink-0 rounded outline-none focus-visible:ring-[3px] focus-visible:ring-ring/45">
|
|
42
|
+
<NuxtImg :src="props.authorImageUrl" :alt="props.author" height="56" width="56" format="avif" quality="40" :sizes="{ 480: '56px', 1280: '56px' }" loading="lazy" decoding="async" class="size-14 rounded border border-border object-cover shadow-elevated-sm transition-[scale] duration-300 ease-spring hover:scale-105" />
|
|
43
|
+
</NuxtLink>
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
</p>
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
45
|
+
<!-- Author name -->
|
|
46
|
+
<div class="min-w-0">
|
|
47
|
+
<p class="text-[11px] font-semibold tracking-[0.16em] text-muted-foreground uppercase">{{ t('uiVintage.blog.author') }}</p>
|
|
48
|
+
<NuxtLink :to="props.authorUrl" target="_blank" rel="noopener noreferrer" class="group/link mt-0.5 inline-flex items-center gap-1 text-base font-bold text-foreground outline-none hover:text-primary focus-visible:underline">
|
|
49
|
+
{{ props.author }}
|
|
50
|
+
<HugeiconsIcon :icon="ArrowUpRight01Icon" class="size-4 transition-[translate] duration-300 ease-spring group-hover/link:translate-x-0.5 group-hover/link:-translate-y-0.5" />
|
|
51
|
+
</NuxtLink>
|
|
49
52
|
</div>
|
|
53
|
+
</div>
|
|
50
54
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
</div>
|
|
55
|
+
<!-- Date information -->
|
|
56
|
+
<div class="flex flex-wrap gap-2 sm:justify-end">
|
|
57
|
+
<!-- Published date -->
|
|
58
|
+
<div v-if="props.datePublished" class="flex items-center gap-2 rounded border border-border bg-surface px-3 py-2 text-xs text-muted-foreground">
|
|
59
|
+
<HugeiconsIcon :icon="Calendar03Icon" class="size-4 shrink-0 text-primary" />
|
|
60
|
+
<span class="font-semibold">{{ t('uiVintage.blog.publishedDate') }}</span>
|
|
61
|
+
<time :datetime="props.datePublished" class="text-foreground">{{ formattedPublishedDate }}</time>
|
|
62
|
+
</div>
|
|
60
63
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
</time>
|
|
67
|
-
</div>
|
|
64
|
+
<!-- Last updated date -->
|
|
65
|
+
<div v-if="showModifiedDate" class="flex items-center gap-2 rounded border border-border bg-surface px-3 py-2 text-xs text-muted-foreground">
|
|
66
|
+
<HugeiconsIcon :icon="RefreshIcon" class="size-4 shrink-0 text-primary" />
|
|
67
|
+
<span class="font-semibold">{{ t('uiVintage.blog.lastUpdatedDate') }}</span>
|
|
68
|
+
<time :datetime="props.dateModified ?? undefined" class="text-foreground">{{ formattedModifiedDate }}</time>
|
|
68
69
|
</div>
|
|
69
70
|
</div>
|
|
70
71
|
</CardContent>
|
|
@@ -43,13 +43,16 @@ onUnmounted(() => {
|
|
|
43
43
|
</script>
|
|
44
44
|
|
|
45
45
|
<template>
|
|
46
|
-
<div :class="cn('relative h-
|
|
46
|
+
<div :class="cn('relative h-[3px] w-full bg-border/40 landing-navbar-progress', props.class)"
|
|
47
47
|
role="progressbar"
|
|
48
48
|
:aria-label="resolvedAriaLabel"
|
|
49
49
|
aria-valuemin="0"
|
|
50
50
|
aria-valuemax="100"
|
|
51
51
|
:aria-valuenow="Math.round(progress)">
|
|
52
|
-
|
|
52
|
+
<!-- Fill with a glowing leading edge (the dot stays inside the fill, so it is never clipped and never widens the page) -->
|
|
53
|
+
<div class="relative h-full bg-linear-to-r from-primary/40 via-primary to-primary transition-[width] duration-150 ease-out" :style="{ width: `${progress}%` }">
|
|
54
|
+
<span v-if="progress > 0" aria-hidden="true" class="absolute top-1/2 right-0 size-2 -translate-y-1/2 rounded-full bg-primary shadow-[0_0_10px_2px_color-mix(in_oklab,var(--primary)_70%,transparent)]" />
|
|
55
|
+
</div>
|
|
53
56
|
</div>
|
|
54
57
|
</template>
|
|
55
58
|
|
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// Props
|
|
3
|
-
const props = defineProps<{
|
|
3
|
+
const props = withDefaults(defineProps<{
|
|
4
4
|
title: string;
|
|
5
|
-
|
|
5
|
+
description?: string;
|
|
6
|
+
}>(), {
|
|
7
|
+
description: ''
|
|
8
|
+
});
|
|
6
9
|
</script>
|
|
7
10
|
|
|
8
11
|
<template>
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
<div data-aos="blur-up" class="mb-6 flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between md:mb-8">
|
|
13
|
+
<div class="min-w-0">
|
|
14
|
+
<!-- Title with accent bar -->
|
|
15
|
+
<h2 class="flex items-center gap-3 text-xl font-bold tracking-tight text-foreground md:text-2xl">
|
|
16
|
+
<span aria-hidden="true" class="h-6 w-1 shrink-0 rounded-full bg-primary shadow-glow md:h-7" />
|
|
17
|
+
{{ props.title }}
|
|
18
|
+
</h2>
|
|
19
|
+
|
|
20
|
+
<!-- Description -->
|
|
21
|
+
<p v-if="props.description" class="mt-2 max-w-2xl text-xs leading-6 text-muted-foreground md:text-sm">
|
|
22
|
+
{{ props.description }}
|
|
23
|
+
</p>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<!-- Optional trailing action (for example a "View all" link) -->
|
|
27
|
+
<div v-if="$slots.action" class="shrink-0">
|
|
28
|
+
<slot name="action" />
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
12
31
|
</template>
|