@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
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { CheckmarkCircle02Icon, Copy01Icon, CodeIcon } from '@hugeicons/core-free-icons';
|
|
3
3
|
import { HugeiconsIcon } from '@hugeicons/vue';
|
|
4
|
-
import { codeToHtml } from 'shiki';
|
|
5
4
|
import { computed, ref, watch } from 'vue';
|
|
6
5
|
import { useI18n } from 'vue-i18n';
|
|
7
6
|
import { Button } from '../ui/button';
|
|
8
|
-
import { Card } from '../ui/card';
|
|
9
7
|
|
|
10
8
|
type CodeLanguage = string;
|
|
11
9
|
|
|
@@ -27,15 +25,31 @@ const copied = ref(false);
|
|
|
27
25
|
const highlightedCode = ref('');
|
|
28
26
|
let highlightRequest = 0;
|
|
29
27
|
const resolvedTitle = computed(() => props.title || t('uiVintage.code.title'));
|
|
28
|
+
const languageLabels: Record<string, string> = {
|
|
29
|
+
js: 'JavaScript',
|
|
30
|
+
javascript: 'JavaScript',
|
|
31
|
+
ts: 'TypeScript',
|
|
32
|
+
typescript: 'TypeScript',
|
|
33
|
+
vue: 'Vue',
|
|
34
|
+
html: 'HTML',
|
|
35
|
+
css: 'CSS',
|
|
36
|
+
json: 'JSON',
|
|
37
|
+
md: 'Markdown',
|
|
38
|
+
markdown: 'Markdown',
|
|
39
|
+
yaml: 'YAML',
|
|
40
|
+
yml: 'YAML',
|
|
41
|
+
};
|
|
42
|
+
const languageLabel = computed(() => props.language === 'text' ? t('uiVintage.code.plainText') : languageLabels[props.language] || props.language);
|
|
30
43
|
const displayedCode = computed(() => {
|
|
31
|
-
if (props.language !== 'json') return props.code;
|
|
44
|
+
if (props.language !== 'json') return props.code.replace(/\n$/, '');
|
|
32
45
|
|
|
33
46
|
try {
|
|
34
47
|
return JSON.stringify(JSON.parse(props.code), null, 2);
|
|
35
48
|
} catch {
|
|
36
|
-
return props.code;
|
|
49
|
+
return props.code.replace(/\n$/, '');
|
|
37
50
|
}
|
|
38
51
|
});
|
|
52
|
+
const codeLines = computed(() => displayedCode.value.split(/\r?\n/));
|
|
39
53
|
|
|
40
54
|
// Highlight code in the library so all consumers share the same rendering behavior.
|
|
41
55
|
const highlightCode = async () => {
|
|
@@ -45,9 +59,12 @@ const highlightCode = async () => {
|
|
|
45
59
|
if (!displayedCode.value || props.language === 'text') return;
|
|
46
60
|
|
|
47
61
|
try {
|
|
62
|
+
// Load the highlighter only when a code block is rendered, so pages without code never download it
|
|
63
|
+
const { codeToHtml } = await import('shiki');
|
|
48
64
|
const html = await codeToHtml(displayedCode.value, {
|
|
49
65
|
lang: props.language,
|
|
50
|
-
|
|
66
|
+
themes: { light: 'github-light', dark: 'github-dark' },
|
|
67
|
+
defaultColor: false
|
|
51
68
|
});
|
|
52
69
|
|
|
53
70
|
if (requestId === highlightRequest) {
|
|
@@ -76,31 +93,59 @@ const copyCode = async () => {
|
|
|
76
93
|
|
|
77
94
|
<template>
|
|
78
95
|
<div class="not-prose my-6 sm:my-8">
|
|
79
|
-
<
|
|
80
|
-
<!--
|
|
81
|
-
<div class="flex items-center
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
<span class="
|
|
85
|
-
|
|
96
|
+
<div data-aos="blur-up" data-code-window class="overflow-hidden rounded border border-border bg-card text-foreground shadow-elevated-sm">
|
|
97
|
+
<!-- Window title bar -->
|
|
98
|
+
<div class="relative flex h-8 items-center border-b border-border bg-surface px-3">
|
|
99
|
+
<!-- Traffic lights -->
|
|
100
|
+
<span aria-hidden="true" class="flex items-center gap-2">
|
|
101
|
+
<span class="size-3 rounded-full bg-destructive/85" />
|
|
102
|
+
<span class="size-3 rounded-full bg-warning/85" />
|
|
103
|
+
<span class="size-3 rounded-full bg-success/85" />
|
|
104
|
+
</span>
|
|
105
|
+
|
|
106
|
+
<!-- Window title -->
|
|
107
|
+
<div class="pointer-events-none absolute inset-x-24 truncate text-center text-xs text-muted-foreground">{{ resolvedTitle }}</div>
|
|
86
108
|
|
|
87
109
|
<!-- Copy code button -->
|
|
88
110
|
<Button v-if="props.copyable"
|
|
89
111
|
variant="ghost"
|
|
90
112
|
size="icon"
|
|
91
|
-
class="
|
|
92
|
-
:aria-label="t('uiVintage.code.copy')"
|
|
93
|
-
:title="t('uiVintage.code.copy')"
|
|
113
|
+
class="ml-auto size-6 text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
114
|
+
:aria-label="copied ? t('uiVintage.code.copied') : t('uiVintage.code.copy')"
|
|
115
|
+
:title="copied ? t('uiVintage.code.copied') : t('uiVintage.code.copy')"
|
|
94
116
|
:disabled="!displayedCode"
|
|
95
117
|
@click="copyCode">
|
|
96
|
-
<HugeiconsIcon :icon="copied ? CheckmarkCircle02Icon : Copy01Icon" class="size-
|
|
118
|
+
<HugeiconsIcon :icon="copied ? CheckmarkCircle02Icon : Copy01Icon" class="size-3.5" />
|
|
97
119
|
</Button>
|
|
120
|
+
|
|
121
|
+
<!-- Announce the copy to screen readers -->
|
|
122
|
+
<span v-if="props.copyable" class="sr-only" aria-live="polite">{{ copied ? t('uiVintage.code.copied') : '' }}</span>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<!-- Editor tabs -->
|
|
126
|
+
<div class="flex h-9 items-stretch border-b border-border bg-surface">
|
|
127
|
+
<!-- Active tab -->
|
|
128
|
+
<div class="flex min-w-0 items-center gap-2 -mb-px border-r border-border border-t-2 border-t-primary bg-card px-3 text-foreground">
|
|
129
|
+
<HugeiconsIcon :icon="CodeIcon" class="size-3.5 shrink-0 text-primary" />
|
|
130
|
+
<span class="truncate text-xs">{{ resolvedTitle }}</span>
|
|
131
|
+
</div>
|
|
98
132
|
</div>
|
|
99
133
|
|
|
100
134
|
<!-- Code content -->
|
|
101
|
-
<div v-if="highlightedCode" class="code-block-content overflow-x-auto
|
|
102
|
-
<
|
|
103
|
-
|
|
135
|
+
<div v-if="highlightedCode" class="code-block-content uv-code-block-content overflow-x-auto" v-html="highlightedCode" />
|
|
136
|
+
<div v-else class="code-block-content overflow-x-auto">
|
|
137
|
+
<pre><code><template v-for="(line, index) in codeLines" :key="index"><span class="line">{{ line }}</span>{{ index < codeLines.length - 1 ? '\n' : '' }}</template></code></pre>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<!-- Status bar -->
|
|
141
|
+
<div class="flex h-6 items-center justify-between gap-3 border-t border-border bg-surface px-3 text-[11px] text-muted-foreground">
|
|
142
|
+
<span class="truncate">{{ t('uiVintage.code.position', { line: codeLines.length, column: 1 }) }}</span>
|
|
143
|
+
<span class="flex shrink-0 items-center gap-3">
|
|
144
|
+
<span>UTF-8</span>
|
|
145
|
+
<span>{{ languageLabel }}</span>
|
|
146
|
+
</span>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
104
149
|
</div>
|
|
105
150
|
</template>
|
|
106
151
|
|
|
@@ -108,15 +153,44 @@ const copyCode = async () => {
|
|
|
108
153
|
.code-block-content :deep(pre) {
|
|
109
154
|
margin: 0;
|
|
110
155
|
min-width: max-content;
|
|
111
|
-
padding: 0.75rem 1rem;
|
|
156
|
+
padding: 0.75rem 1rem 0.75rem 0;
|
|
157
|
+
background-color: transparent !important;
|
|
158
|
+
color: var(--foreground);
|
|
112
159
|
font-size: 0.75rem;
|
|
113
160
|
line-height: 1.5rem;
|
|
161
|
+
counter-reset: line;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* Line numbers in the editor gutter */
|
|
165
|
+
.code-block-content :deep(.line)::before {
|
|
166
|
+
counter-increment: line;
|
|
167
|
+
content: counter(line);
|
|
168
|
+
display: inline-block;
|
|
169
|
+
width: 2.75rem;
|
|
170
|
+
margin-right: 1rem;
|
|
171
|
+
padding-right: 0.25rem;
|
|
172
|
+
text-align: right;
|
|
173
|
+
color: color-mix(in oklab, var(--muted-foreground) 65%, transparent);
|
|
174
|
+
user-select: none;
|
|
114
175
|
}
|
|
115
176
|
|
|
116
177
|
@media (min-width: 640px) {
|
|
117
178
|
.code-block-content :deep(pre) {
|
|
118
|
-
padding: 1rem 1.25rem;
|
|
179
|
+
padding: 1rem 1.25rem 1rem 0;
|
|
119
180
|
font-size: 0.875rem;
|
|
120
181
|
}
|
|
121
182
|
}
|
|
122
|
-
</style>
|
|
183
|
+
</style>
|
|
184
|
+
|
|
185
|
+
<style>
|
|
186
|
+
/* Shiki emits both themes as CSS variables; pick the one matching the library's `.dark` class and drop Nuxt Content's per-token backgrounds */
|
|
187
|
+
.uv-code-block-content .shiki span {
|
|
188
|
+
color: var(--shiki-light);
|
|
189
|
+
background-color: transparent;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.dark .uv-code-block-content .shiki span {
|
|
193
|
+
color: var(--shiki-dark);
|
|
194
|
+
background-color: transparent;
|
|
195
|
+
}
|
|
196
|
+
</style>
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ArrowDown01Icon, ArrowRight01Icon } from '@hugeicons/core-free-icons';
|
|
3
|
-
import { HugeiconsIcon } from '@hugeicons/vue';
|
|
4
2
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
|
5
3
|
|
|
6
4
|
type FlowItem = string | {
|
|
@@ -38,11 +36,16 @@ const getItemDescription = (item: FlowItem) => {
|
|
|
38
36
|
const isVertical = () => {
|
|
39
37
|
return props.orientation === 'vertical';
|
|
40
38
|
};
|
|
39
|
+
|
|
40
|
+
// Format the step number with a leading zero
|
|
41
|
+
const formatNumber = (index: number) => {
|
|
42
|
+
return String(index + 1).padStart(2, '0');
|
|
43
|
+
};
|
|
41
44
|
</script>
|
|
42
45
|
|
|
43
46
|
<template>
|
|
44
47
|
<div class="not-prose my-6 sm:my-8">
|
|
45
|
-
<Card>
|
|
48
|
+
<Card data-aos="blur-up">
|
|
46
49
|
<!-- Card header -->
|
|
47
50
|
<CardHeader v-if="props.title || props.description">
|
|
48
51
|
<!-- Card title -->
|
|
@@ -58,42 +61,33 @@ const isVertical = () => {
|
|
|
58
61
|
|
|
59
62
|
<!-- Card content -->
|
|
60
63
|
<CardContent>
|
|
61
|
-
<
|
|
62
|
-
<!-- Flow
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
</span>
|
|
73
|
-
|
|
74
|
-
<!-- Flow item label -->
|
|
75
|
-
<span>{{ getItemLabel(item) }}</span>
|
|
76
|
-
</div>
|
|
77
|
-
|
|
78
|
-
<!-- Flow item description -->
|
|
79
|
-
<p v-if="getItemDescription(item)" class="text-xs leading-6 text-muted-foreground md:leading-5">
|
|
80
|
-
{{ getItemDescription(item) }}
|
|
81
|
-
</p>
|
|
82
|
-
</CardContent>
|
|
83
|
-
</Card>
|
|
64
|
+
<ol :class="['flex flex-col', !isVertical() && 'md:flex-row md:gap-4']">
|
|
65
|
+
<!-- Flow step -->
|
|
66
|
+
<li v-for="(item, index) in props.items"
|
|
67
|
+
:key="`${getItemLabel(item)}-${index}`"
|
|
68
|
+
data-aos="blur-up"
|
|
69
|
+
:data-aos-delay="100 + index * 90"
|
|
70
|
+
:class="['relative flex gap-4 pb-6 last:pb-0', !isVertical() && 'md:flex-1 md:flex-col md:gap-3 md:pb-0']">
|
|
71
|
+
<!-- Step number -->
|
|
72
|
+
<span class="relative z-10 flex size-7 shrink-0 items-center justify-center rounded border border-primary/30 bg-card text-[11px] font-semibold tabular-nums text-primary">
|
|
73
|
+
{{ formatNumber(index) }}
|
|
74
|
+
</span>
|
|
84
75
|
|
|
85
|
-
<!--
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
76
|
+
<!-- Connector to the next step -->
|
|
77
|
+
<span v-if="index < props.items.length - 1"
|
|
78
|
+
aria-hidden="true"
|
|
79
|
+
:class="['absolute top-8 bottom-1 left-3.5 w-px bg-border', !isVertical() && 'md:top-3.5 md:bottom-auto md:left-10 md:-right-2 md:h-px md:w-auto md:bg-linear-to-r md:from-primary/40 md:to-border']" />
|
|
89
80
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
<
|
|
81
|
+
<!-- Step text -->
|
|
82
|
+
<div :class="['flex min-w-0 flex-col gap-1 pt-1', !isVertical() && 'md:pt-0']">
|
|
83
|
+
<span class="text-xs font-semibold leading-5 text-foreground md:text-sm">{{ getItemLabel(item) }}</span>
|
|
84
|
+
<p v-if="getItemDescription(item)" class="text-xs leading-5 text-muted-foreground">
|
|
85
|
+
{{ getItemDescription(item) }}
|
|
86
|
+
</p>
|
|
93
87
|
</div>
|
|
94
|
-
</
|
|
95
|
-
</
|
|
88
|
+
</li>
|
|
89
|
+
</ol>
|
|
96
90
|
</CardContent>
|
|
97
91
|
</Card>
|
|
98
92
|
</div>
|
|
99
|
-
</template>
|
|
93
|
+
</template>
|
|
@@ -12,18 +12,18 @@ const props = withDefaults(defineProps<{
|
|
|
12
12
|
</script>
|
|
13
13
|
|
|
14
14
|
<template>
|
|
15
|
-
<div aria-hidden="true" class="not-prose my-
|
|
15
|
+
<div aria-hidden="true" data-aos="blur-up" class="not-prose my-8 sm:my-14">
|
|
16
16
|
<div class="flex items-center gap-3 sm:gap-4">
|
|
17
17
|
<!-- Left line -->
|
|
18
|
-
<div class="h-px flex-1 bg-linear-to-r from-transparent via-
|
|
18
|
+
<div class="h-px flex-1 bg-linear-to-r from-transparent via-border to-primary/40" />
|
|
19
19
|
|
|
20
20
|
<!-- Icon in the middle -->
|
|
21
|
-
<div class="flex
|
|
22
|
-
<HugeiconsIcon :icon="props.icon" class="size-
|
|
21
|
+
<div class="flex size-7 shrink-0 items-center justify-center rounded border border-primary/30 bg-card text-primary">
|
|
22
|
+
<HugeiconsIcon :icon="props.icon" class="size-3.5" />
|
|
23
23
|
</div>
|
|
24
24
|
|
|
25
25
|
<!-- Right line -->
|
|
26
|
-
<div class="h-px flex-1 bg-linear-to-l from-transparent via-
|
|
26
|
+
<div class="h-px flex-1 bg-linear-to-l from-transparent via-border to-primary/40" />
|
|
27
27
|
</div>
|
|
28
28
|
</div>
|
|
29
|
-
</template>
|
|
29
|
+
</template>
|
|
@@ -3,13 +3,18 @@ import { computed } from 'vue';
|
|
|
3
3
|
import CodeBlock from './CodeBlock.vue';
|
|
4
4
|
import Terminal from './Terminal.vue';
|
|
5
5
|
|
|
6
|
+
// Nuxt Content passes `shiki` classes; keep them off Terminal and CodeBlock, whose utilities the unlayered `.shiki span` styles override
|
|
7
|
+
defineOptions({ inheritAttrs: false });
|
|
8
|
+
|
|
6
9
|
// Props
|
|
7
10
|
const props = withDefaults(defineProps<{
|
|
8
11
|
code?: string;
|
|
9
12
|
language?: string;
|
|
13
|
+
filename?: string;
|
|
10
14
|
}>(), {
|
|
11
15
|
code: '',
|
|
12
|
-
language: ''
|
|
16
|
+
language: '',
|
|
17
|
+
filename: ''
|
|
13
18
|
});
|
|
14
19
|
|
|
15
20
|
const normalizedLanguage = computed(() => props.language.toLowerCase());
|
|
@@ -19,11 +24,11 @@ const commands = computed(() => props.code.replace(/\n$/, '').split(/\r?\n/));
|
|
|
19
24
|
|
|
20
25
|
<template>
|
|
21
26
|
<!-- Terminal value -->
|
|
22
|
-
<Terminal v-if="isShellBlock" :commands="commands" />
|
|
27
|
+
<Terminal v-if="isShellBlock" :commands="commands" :title="props.filename" />
|
|
23
28
|
|
|
24
29
|
<!-- Code block -->
|
|
25
|
-
<CodeBlock v-else-if="normalizedLanguage" :code="props.code" :language="normalizedLanguage" />
|
|
30
|
+
<CodeBlock v-else-if="normalizedLanguage" :code="props.code" :language="normalizedLanguage" :title="props.filename" />
|
|
26
31
|
|
|
27
32
|
<!-- Fallback pre block -->
|
|
28
|
-
<pre v-else><slot /></pre>
|
|
33
|
+
<pre v-else v-bind="$attrs"><slot /></pre>
|
|
29
34
|
</template>
|
|
@@ -4,7 +4,6 @@ import { HugeiconsIcon } from '@hugeicons/vue';
|
|
|
4
4
|
import { computed, ref } from 'vue';
|
|
5
5
|
import { useI18n } from 'vue-i18n';
|
|
6
6
|
import { Button } from '../ui/button';
|
|
7
|
-
import { Card } from '../ui/card';
|
|
8
7
|
|
|
9
8
|
// Props
|
|
10
9
|
const props = withDefaults(defineProps<{
|
|
@@ -42,29 +41,58 @@ const copyCommands = async () => {
|
|
|
42
41
|
|
|
43
42
|
<template>
|
|
44
43
|
<div class="not-prose my-6 sm:my-8">
|
|
45
|
-
<
|
|
46
|
-
<!--
|
|
47
|
-
<div class="flex items-center
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
<span class="
|
|
44
|
+
<div data-aos="blur-up" data-terminal class="overflow-hidden rounded border border-border bg-card text-foreground shadow-elevated-sm">
|
|
45
|
+
<!-- Window title bar -->
|
|
46
|
+
<div class="relative flex h-8 items-center border-b border-border bg-surface px-3">
|
|
47
|
+
<!-- Traffic lights -->
|
|
48
|
+
<span aria-hidden="true" class="flex items-center gap-2">
|
|
49
|
+
<span class="size-3 rounded-full bg-destructive/85" />
|
|
50
|
+
<span class="size-3 rounded-full bg-warning/85" />
|
|
51
|
+
<span class="size-3 rounded-full bg-success/85" />
|
|
52
|
+
</span>
|
|
53
|
+
|
|
54
|
+
<!-- Window title -->
|
|
55
|
+
<div class="pointer-events-none absolute inset-x-24 flex items-center justify-center gap-1.5 text-muted-foreground">
|
|
56
|
+
<HugeiconsIcon :icon="TerminalIcon" class="size-3.5 shrink-0" />
|
|
57
|
+
<span class="truncate text-xs font-medium">{{ resolvedTitle }}</span>
|
|
51
58
|
</div>
|
|
52
59
|
|
|
53
60
|
<!-- Copy command button -->
|
|
54
61
|
<Button v-if="props.copyable"
|
|
55
62
|
variant="ghost"
|
|
56
63
|
size="icon"
|
|
57
|
-
class="
|
|
58
|
-
:aria-label="t('uiVintage.terminal.copy')"
|
|
59
|
-
:title="t('uiVintage.terminal.copy')"
|
|
64
|
+
class="ml-auto size-6 text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
65
|
+
:aria-label="copied ? t('uiVintage.terminal.copied') : t('uiVintage.terminal.copy')"
|
|
66
|
+
:title="copied ? t('uiVintage.terminal.copied') : t('uiVintage.terminal.copy')"
|
|
60
67
|
:disabled="!commandText"
|
|
61
68
|
@click="copyCommands">
|
|
62
|
-
<HugeiconsIcon :icon="copied ? CheckmarkCircle02Icon : Copy01Icon" class="size-
|
|
69
|
+
<HugeiconsIcon :icon="copied ? CheckmarkCircle02Icon : Copy01Icon" class="size-3.5" />
|
|
63
70
|
</Button>
|
|
71
|
+
|
|
72
|
+
<!-- Announce the copy to screen readers -->
|
|
73
|
+
<span v-if="props.copyable" class="sr-only" aria-live="polite">{{ copied ? t('uiVintage.terminal.copied') : '' }}</span>
|
|
64
74
|
</div>
|
|
65
75
|
|
|
66
76
|
<!-- Terminal commands -->
|
|
67
|
-
<pre class="overflow-x-auto
|
|
68
|
-
</
|
|
77
|
+
<pre class="overflow-x-auto px-4 py-3 text-xs leading-6 sm:px-5 sm:py-4 sm:text-sm"><code><span v-for="(command, index) in props.commands" :key="`${command}-${index}`" class="flex min-w-max"><span class="mr-2 select-none text-primary" aria-hidden="true">{{ props.prompt }}</span><span>{{ command }}</span><span v-if="index === props.commands.length - 1" class="terminal-cursor ml-1 inline-block w-[0.6em] bg-primary/70" aria-hidden="true" /></span></code></pre>
|
|
78
|
+
</div>
|
|
69
79
|
</div>
|
|
70
|
-
</template>
|
|
80
|
+
</template>
|
|
81
|
+
|
|
82
|
+
<style scoped>
|
|
83
|
+
.terminal-cursor {
|
|
84
|
+
animation: terminal-blink 1.1s steps(1) infinite;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@keyframes terminal-blink {
|
|
88
|
+
50% {
|
|
89
|
+
opacity: 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@media (prefers-reduced-motion: reduce) {
|
|
94
|
+
.terminal-cursor {
|
|
95
|
+
animation: none;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
</style>
|
|
@@ -70,7 +70,7 @@ onMounted(() => {
|
|
|
70
70
|
<Card class="gap-3! p-4 shadow-elevated-sm">
|
|
71
71
|
<p class="text-sm text-foreground">
|
|
72
72
|
{{ resolvedMessage }}
|
|
73
|
-
<NuxtLinkLocale v-if="props.policyTo" :to="props.policyTo" class="font-
|
|
73
|
+
<NuxtLinkLocale v-if="props.policyTo" :to="props.policyTo" class="font-semibold text-primary underline underline-offset-2">{{ resolvedPolicyLabel }}</NuxtLinkLocale>
|
|
74
74
|
</p>
|
|
75
75
|
<div class="flex flex-wrap justify-end gap-2">
|
|
76
76
|
<Button variant="secondary" size="sm" @click="handleDecline">{{ resolvedDeclineLabel }}</Button>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { NuxtImg } from '#components';
|
|
3
|
+
import { computed } from 'vue';
|
|
4
|
+
import { useI18n } from 'vue-i18n';
|
|
3
5
|
import type { Component, HTMLAttributes } from 'vue';
|
|
4
6
|
import { HugeiconsIcon } from '@hugeicons/vue';
|
|
5
7
|
import BackgroundGrid from '../background-grid/BackgroundGrid.vue';
|
|
@@ -13,6 +15,7 @@ import type { HugeiconsIconDefinition } from '../../lib/common-types';
|
|
|
13
15
|
const props = withDefaults(defineProps<{
|
|
14
16
|
appName?: string;
|
|
15
17
|
appLogo?: string;
|
|
18
|
+
appLogoDark?: string;
|
|
16
19
|
appLinkTo?: string;
|
|
17
20
|
title?: string;
|
|
18
21
|
description?: string;
|
|
@@ -40,6 +43,7 @@ const props = withDefaults(defineProps<{
|
|
|
40
43
|
}>(), {
|
|
41
44
|
appName: '',
|
|
42
45
|
appLogo: '',
|
|
46
|
+
appLogoDark: '',
|
|
43
47
|
appLinkTo: '/',
|
|
44
48
|
title: '',
|
|
45
49
|
description: '',
|
|
@@ -68,6 +72,16 @@ const getSidebarItemLinkProps = (item: NonNullable<typeof props.sidebarSections>
|
|
|
68
72
|
...sharedProps
|
|
69
73
|
};
|
|
70
74
|
};
|
|
75
|
+
|
|
76
|
+
const { t } = useI18n();
|
|
77
|
+
|
|
78
|
+
// Logo sources for each theme (either one falls back to the other)
|
|
79
|
+
const lightLogo = computed(() => props.appLogo || props.appLogoDark);
|
|
80
|
+
const darkLogo = computed(() => props.appLogoDark || props.appLogo);
|
|
81
|
+
const hasDarkLogo = computed(() => darkLogo.value !== lightLogo.value);
|
|
82
|
+
|
|
83
|
+
// Logo alt text (also names the home link when the app name is not shown)
|
|
84
|
+
const logoAlt = computed(() => props.appName ? t('uiVintage.common.logo', { name: props.appName }) : t('uiVintage.common.home'));
|
|
71
85
|
</script>
|
|
72
86
|
|
|
73
87
|
<template>
|
|
@@ -75,7 +89,7 @@ const getSidebarItemLinkProps = (item: NonNullable<typeof props.sidebarSections>
|
|
|
75
89
|
<!-- Sidebar -->
|
|
76
90
|
<Sidebar :collapsible="props.collapsible" :compact="props.compact">
|
|
77
91
|
<!-- Sidebar header -->
|
|
78
|
-
<SidebarHeader v-if="props.appName || $slots['sidebar-header']" class="h-16 justify-center border-b border-sidebar-border">
|
|
92
|
+
<SidebarHeader v-if="props.appName || lightLogo || $slots['sidebar-header']" class="h-16 justify-center border-b border-sidebar-border">
|
|
79
93
|
<slot name="sidebar-header">
|
|
80
94
|
<SidebarMenu>
|
|
81
95
|
<SidebarMenuItem>
|
|
@@ -83,10 +97,13 @@ const getSidebarItemLinkProps = (item: NonNullable<typeof props.sidebarSections>
|
|
|
83
97
|
<a :href="props.appLinkTo" class="inline-flex items-center gap-2">
|
|
84
98
|
<div class="inline-flex items-center gap-2">
|
|
85
99
|
<!-- App logo -->
|
|
86
|
-
<
|
|
100
|
+
<template v-if="lightLogo">
|
|
101
|
+
<NuxtImg :src="lightLogo" :alt="logoAlt" width="32" height="32" :sizes="{ 320: '32px', 1280: '32px' }" loading="eager" :class="['size-8 shrink-0 object-contain', hasDarkLogo && 'dark:hidden']" />
|
|
102
|
+
<NuxtImg v-if="hasDarkLogo" :src="darkLogo" :alt="logoAlt" width="32" height="32" :sizes="{ 320: '32px', 1280: '32px' }" loading="eager" class="hidden size-8 shrink-0 object-contain dark:block" />
|
|
103
|
+
</template>
|
|
87
104
|
|
|
88
105
|
<!-- App name -->
|
|
89
|
-
<span v-if="props.appName" class="text-xl font-
|
|
106
|
+
<span v-if="props.appName" class="text-xl font-semibold tracking-tight text-foreground">{{ props.appName }}</span>
|
|
90
107
|
</div>
|
|
91
108
|
</a>
|
|
92
109
|
</SidebarMenuButton>
|
|
@@ -105,12 +122,12 @@ const getSidebarItemLinkProps = (item: NonNullable<typeof props.sidebarSections>
|
|
|
105
122
|
|
|
106
123
|
<!-- Section content -->
|
|
107
124
|
<SidebarGroupContent>
|
|
108
|
-
<SidebarMenu :class="cn('gap-
|
|
125
|
+
<SidebarMenu :class="cn('gap-1', props.compact && 'gap-0.5')">
|
|
109
126
|
<SidebarMenuItem v-for="item in section.items" :key="item.id">
|
|
110
|
-
<SidebarMenuButton as-child :is-active="item.active" :class="cn('group/nav relative h-auto min-h-
|
|
111
|
-
<component :is="props.sidebarLinkComponent" v-bind="getSidebarItemLinkProps(item)">
|
|
127
|
+
<SidebarMenuButton as-child :is-active="item.active" :class="cn('group/nav relative h-auto min-h-11 overflow-hidden rounded px-3 py-2.5 text-muted-foreground transition-colors duration-150 hover:bg-accent hover:text-foreground data-[active=true]:bg-primary/10 data-[active=true]:text-foreground sm:px-3.5', 'before:absolute before:inset-y-2 before:left-0 before:w-0.5 before:origin-center before:scale-y-0 before:bg-primary before:transition-transform before:duration-300 before:ease-spring data-[active=true]:before:scale-y-100', item.description ? 'items-start' : 'items-center')">
|
|
128
|
+
<component :is="props.sidebarLinkComponent" v-bind="getSidebarItemLinkProps(item)" :aria-current="item.active ? 'page' : undefined">
|
|
112
129
|
<!-- Icon -->
|
|
113
|
-
<HugeiconsIcon v-if="item.icon" :icon="item.icon" :stroke-width="1.8" :class="cn('shrink-0 opacity-90 transition-[opacity,
|
|
130
|
+
<HugeiconsIcon v-if="item.icon" :icon="item.icon" :stroke-width="1.8" :class="cn('shrink-0 opacity-90 transition-[opacity,color] duration-150 group-hover/nav:opacity-100 group-data-[active=true]/nav:text-primary', item.description ? 'mt-0.5 size-5 sm:h-5 sm:w-5' : 'size-5 self-center sm:h-6 sm:w-6')" />
|
|
114
131
|
|
|
115
132
|
<!-- Label and optional description -->
|
|
116
133
|
<div :class="cn('min-w-0 flex-1 text-left leading-tight', item.description ? 'grid' : 'flex items-center')">
|
|
@@ -43,14 +43,14 @@ const resolvedDescription = computed(() => props.description
|
|
|
43
43
|
<Card class="flex flex-col items-center justify-center px-4 py-8 text-center md:py-12">
|
|
44
44
|
<CardContent class="flex flex-col items-center justify-center p-0! text-center">
|
|
45
45
|
<!-- Icon -->
|
|
46
|
-
<div class="flex items-center justify-center text-4xl text-muted-foreground opacity-40">
|
|
46
|
+
<div aria-hidden="true" class="flex items-center justify-center text-4xl text-muted-foreground opacity-40">
|
|
47
47
|
<HugeiconsIcon :icon="props.icon" class="size-10 animate-uv-float" />
|
|
48
48
|
</div>
|
|
49
49
|
|
|
50
50
|
<!-- Title -->
|
|
51
51
|
<component :is="`h${props.level}`"
|
|
52
52
|
v-if="resolvedTitle"
|
|
53
|
-
class="text-sm font-
|
|
53
|
+
class="text-sm font-semibold text-foreground md:text-base">
|
|
54
54
|
{{ resolvedTitle }}
|
|
55
55
|
</component>
|
|
56
56
|
|
|
@@ -64,7 +64,7 @@ const handleAction = () => {
|
|
|
64
64
|
</script>
|
|
65
65
|
|
|
66
66
|
<template>
|
|
67
|
-
<div :class="cn('relative min-h-screen bg-
|
|
67
|
+
<div :class="cn('relative min-h-screen bg-background text-foreground', props.class)">
|
|
68
68
|
<!-- Decorative background -->
|
|
69
69
|
<BackgroundGrid v-if="props.showBackground" />
|
|
70
70
|
|
|
@@ -85,12 +85,12 @@ const handleAction = () => {
|
|
|
85
85
|
<!-- Error copy -->
|
|
86
86
|
<div>
|
|
87
87
|
<!-- Status code -->
|
|
88
|
-
<div class="text-
|
|
88
|
+
<div class="text-xs font-semibold uppercase tracking-[0.2em] text-primary">
|
|
89
89
|
{{ resolvedLabel }} {{ resolvedStatusCode }}
|
|
90
90
|
</div>
|
|
91
91
|
|
|
92
92
|
<!-- Title -->
|
|
93
|
-
<h1 class="text-2xl font-
|
|
93
|
+
<h1 class="mt-3 text-2xl font-semibold tracking-tight">
|
|
94
94
|
{{ resolvedTitle }}
|
|
95
95
|
</h1>
|
|
96
96
|
|
|
@@ -20,15 +20,15 @@ const props = withDefaults(defineProps<{
|
|
|
20
20
|
<Card :class="['flex flex-col items-center p-4! sm:flex-row', props.class]">
|
|
21
21
|
<CardContent class="flex w-full flex-col items-center gap-0 p-0! sm:flex-row">
|
|
22
22
|
<!-- Icon -->
|
|
23
|
-
<
|
|
24
|
-
<HugeiconsIcon :icon="props.icon" class="size-5" />
|
|
25
|
-
</
|
|
23
|
+
<span aria-hidden="true" class="mb-3 flex size-10 shrink-0 items-center justify-center rounded border border-primary/30 text-primary sm:mb-0 sm:mr-4">
|
|
24
|
+
<HugeiconsIcon :icon="props.icon" class="size-4.5" />
|
|
25
|
+
</span>
|
|
26
26
|
|
|
27
27
|
<!-- Content -->
|
|
28
28
|
<div class="flex w-full flex-1 items-center justify-between text-center sm:w-auto sm:text-left">
|
|
29
29
|
<div class="flex flex-col w-full gap-2 sm:gap-1">
|
|
30
30
|
<!-- Title -->
|
|
31
|
-
<span class="text-
|
|
31
|
+
<span class="text-sm font-semibold text-foreground md:text-base">
|
|
32
32
|
{{ props.title }}
|
|
33
33
|
</span>
|
|
34
34
|
|