@brumaombra/ui-vintage 0.8.2 → 0.9.1
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 +89 -18
- package/README.md +296 -262
- package/module.mjs +5 -11
- package/package.json +30 -12
- package/src/components/blog/BlogContentRenderer.vue +1 -1
- package/src/components/blog/BlogFAQSection.vue +1 -1
- package/src/components/blog/BlogHeaderSection.vue +93 -19
- package/src/components/blog/BlogInfoSection.vue +42 -37
- package/src/components/blog/BlogPostHeader.vue +181 -0
- package/src/components/blog/BlogProgressBar.vue +3 -5
- package/src/components/blog/BlogSectionTitle.vue +8 -6
- package/src/components/blog/BlogTagsSection.vue +16 -7
- package/src/components/blog/CategoryCard.vue +19 -23
- package/src/components/blog/HeaderCarousel.vue +17 -7
- package/src/components/blog/PostCard.vue +14 -21
- package/src/components/blog/SocialShareSidebar.vue +32 -15
- package/src/components/blog/TableOfContents.vue +64 -29
- package/src/components/blog/index.ts +1 -0
- package/src/components/card-grid/CardGrid.vue +3 -3
- package/src/components/chip/Chip.vue +1 -1
- package/src/components/confirm-dialog/ConfirmDialog.vue +6 -1
- package/src/components/confirm-dialog/confirm-dialog-state.ts +22 -22
- package/src/components/content/BlogList.vue +29 -52
- package/src/components/content/BlogTable.vue +26 -30
- package/src/components/content/CodeBlock.vue +99 -23
- package/src/components/content/Flow.vue +29 -35
- package/src/components/content/ProseHr.vue +6 -6
- package/src/components/content/ProsePre.vue +9 -4
- package/src/components/content/Terminal.vue +45 -21
- 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 +3 -3
- package/src/components/error-page/ErrorPage.vue +4 -4
- package/src/components/info-card/InfoCard.vue +4 -4
- package/src/components/landing/landing-footer/LandingFooter.vue +17 -12
- package/src/components/landing/landing-navbar/LandingNavbar.vue +22 -6
- package/src/components/landing/landing-shell/LandingShell.vue +2 -7
- package/src/components/language-flag/index.ts +1 -0
- package/src/components/language-selector/LanguageSelector.vue +19 -11
- package/src/components/load-more-button/LoadMoreButton.vue +9 -13
- package/src/components/loading-state-card/LoadingStateCard.vue +3 -3
- package/src/components/message-dialog/MessageDialog.vue +14 -12
- package/src/components/message-dialog/message-dialog-state.ts +1 -4
- package/src/components/message-toast/MessageToast.vue +28 -10
- 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 +39 -40
- 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 +6 -3
- package/src/components/theme-selector/theme-transition.ts +15 -3
- package/src/components/ui/accordion/Accordion.vue +19 -19
- 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 +3 -3
- 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 +2 -2
- package/src/components/ui/calendar/CalendarPrevButton.vue +2 -2
- 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/combobox/ComboboxItem.vue +1 -1
- package/src/components/ui/combobox/ComboboxSelect.vue +2 -1
- package/src/components/ui/combobox/ComboboxTrigger.vue +1 -1
- package/src/components/ui/command/CommandGroup.vue +17 -3
- package/src/components/ui/command/CommandInput.vue +2 -2
- package/src/components/ui/command/CommandList.vue +5 -1
- package/src/components/ui/data-table/DataTable.vue +13 -4
- package/src/components/ui/date-picker/DatePicker.vue +8 -2
- package/src/components/ui/date-time-picker/DateTimePicker.vue +5 -1
- package/src/components/ui/dialog/DialogContent.vue +1 -1
- package/src/components/ui/dialog/DialogScrollContent.vue +1 -1
- package/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +1 -1
- package/src/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
- package/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +2 -2
- 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 +14 -5
- package/src/components/ui/input/Input.vue +5 -1
- package/src/components/ui/native-select/NativeSelect.vue +7 -3
- package/src/components/ui/number-field/NumberField.vue +7 -2
- package/src/components/ui/pagination/PaginationEllipsis.vue +1 -1
- package/src/components/ui/pagination/PaginationFirst.vue +1 -1
- package/src/components/ui/pagination/PaginationItem.vue +1 -1
- package/src/components/ui/pagination/PaginationLast.vue +1 -1
- package/src/components/ui/pagination/PaginationNext.vue +1 -1
- package/src/components/ui/pagination/PaginationPrevious.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 +2 -2
- package/src/components/ui/select/SelectItemContent.vue +1 -1
- package/src/components/ui/select/SelectScrollDownButton.vue +1 -1
- package/src/components/ui/select/SelectScrollUpButton.vue +1 -1
- package/src/components/ui/select/SelectTrigger.vue +6 -2
- 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/SidebarTrigger.vue +1 -1
- 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/SimpleStepper.vue +1 -1
- package/src/components/ui/stepper/StepperDescription.vue +5 -3
- package/src/components/ui/stepper/StepperIndicator.vue +3 -3
- 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/tags-input/TagsInputItemDelete.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 +16 -6
- package/src/i18n/en.json +16 -6
- package/src/i18n/es.json +16 -6
- package/src/i18n/fr.json +16 -6
- package/src/i18n/it.json +16 -6
- package/src/i18n/ja.json +16 -6
- package/src/i18n/pt.json +16 -6
- package/src/i18n/ru.json +16 -6
- package/src/i18n/zh.json +16 -6
- 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 +12 -16
- package/skills/ui-vintage/SKILL.md +0 -42
|
@@ -120,10 +120,10 @@ const clearCurrentConfirmDialog = () => {
|
|
|
120
120
|
openNextConfirmDialog();
|
|
121
121
|
};
|
|
122
122
|
|
|
123
|
-
// Close the current dialog after the leave transition
|
|
124
|
-
const scheduleCloseCurrentConfirmDialog = () => {
|
|
125
|
-
// Guard against missing or already closed dialogs
|
|
126
|
-
if (
|
|
123
|
+
// Close the current dialog after the leave transition (only if it is still the given request)
|
|
124
|
+
const scheduleCloseCurrentConfirmDialog = (request: ConfirmDialogRequest) => {
|
|
125
|
+
// Guard against missing, replaced, or already closed dialogs
|
|
126
|
+
if (confirmDialogState.current !== request || !confirmDialogState.isOpen) return;
|
|
127
127
|
confirmDialogState.isOpen = false;
|
|
128
128
|
|
|
129
129
|
// Clear any existing close timer to avoid racing conditions
|
|
@@ -138,40 +138,40 @@ const scheduleCloseCurrentConfirmDialog = () => {
|
|
|
138
138
|
}, DIALOG_CLOSE_DURATION_MS);
|
|
139
139
|
};
|
|
140
140
|
|
|
141
|
-
// Resolve the dialog as cancelled
|
|
141
|
+
// Resolve the dialog as cancelled (ignored while a confirm or cancel handler is still running)
|
|
142
142
|
export const closeConfirmDialog = () => {
|
|
143
143
|
const current = confirmDialogState.current;
|
|
144
|
-
if (!current || !confirmDialogState.isOpen) return;
|
|
144
|
+
if (!current || !confirmDialogState.isOpen || confirmDialogState.busy) return;
|
|
145
145
|
current.resolve(false);
|
|
146
|
-
scheduleCloseCurrentConfirmDialog();
|
|
146
|
+
scheduleCloseCurrentConfirmDialog(current);
|
|
147
147
|
};
|
|
148
148
|
|
|
149
|
-
// Run
|
|
150
|
-
|
|
149
|
+
// Run a dialog handler, then resolve and close the dialog it belongs to
|
|
150
|
+
const runConfirmDialogHandler = async (handler: (() => void | Promise<void>) | null, result: boolean) => {
|
|
151
151
|
const current = confirmDialogState.current;
|
|
152
|
-
if (!current || confirmDialogState.busy) return;
|
|
152
|
+
if (!current || !confirmDialogState.isOpen || confirmDialogState.busy) return;
|
|
153
153
|
confirmDialogState.busy = true;
|
|
154
154
|
|
|
155
155
|
try {
|
|
156
|
-
await
|
|
156
|
+
await handler?.();
|
|
157
|
+
current.resolve(result);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
// Settle the caller's promise even when the handler throws, then surface the error
|
|
157
160
|
current.resolve(false);
|
|
161
|
+
throw error;
|
|
158
162
|
} finally {
|
|
159
|
-
scheduleCloseCurrentConfirmDialog();
|
|
163
|
+
scheduleCloseCurrentConfirmDialog(current);
|
|
160
164
|
}
|
|
161
165
|
};
|
|
162
166
|
|
|
167
|
+
// Run the cancel handler for the active dialog
|
|
168
|
+
export const cancelActiveConfirmDialog = async () => {
|
|
169
|
+
await runConfirmDialogHandler(confirmDialogState.current?.options.onCancel ?? null, false);
|
|
170
|
+
};
|
|
171
|
+
|
|
163
172
|
// Run the confirm handler for the active dialog
|
|
164
173
|
export const confirmActiveDialog = async () => {
|
|
165
|
-
|
|
166
|
-
if (!current || confirmDialogState.busy) return;
|
|
167
|
-
confirmDialogState.busy = true;
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
await current.options.onConfirm?.();
|
|
171
|
-
current.resolve(true);
|
|
172
|
-
} finally {
|
|
173
|
-
scheduleCloseCurrentConfirmDialog();
|
|
174
|
-
}
|
|
174
|
+
await runConfirmDialogHandler(confirmDialogState.current?.options.onConfirm ?? null, true);
|
|
175
175
|
};
|
|
176
176
|
|
|
177
177
|
// Queue and show a confirm dialog
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import
|
|
2
|
+
import Cancel01Icon from '@hugeicons/core-free-icons/Cancel01Icon';
|
|
3
|
+
import Tick02Icon from '@hugeicons/core-free-icons/Tick02Icon';
|
|
3
4
|
import { HugeiconsIcon } from '@hugeicons/vue';
|
|
4
|
-
import { Card
|
|
5
|
+
import { Card } from '../ui/card';
|
|
5
6
|
|
|
6
7
|
// Props
|
|
7
8
|
const props = withDefaults(defineProps<{
|
|
@@ -12,64 +13,40 @@ const props = withDefaults(defineProps<{
|
|
|
12
13
|
variant: 'checkmark'
|
|
13
14
|
});
|
|
14
15
|
|
|
15
|
-
//
|
|
16
|
-
const
|
|
16
|
+
// Marker classes
|
|
17
|
+
const getMarkerClasses = () => {
|
|
17
18
|
const toneClasses = {
|
|
18
|
-
checkmark: '
|
|
19
|
-
cross: '
|
|
20
|
-
numbered: '
|
|
21
|
-
circle: '
|
|
19
|
+
checkmark: 'rounded bg-success/10 text-success',
|
|
20
|
+
cross: 'rounded bg-destructive/10 text-destructive',
|
|
21
|
+
numbered: 'text-[11px] font-semibold tabular-nums text-primary',
|
|
22
|
+
circle: ''
|
|
22
23
|
};
|
|
23
|
-
return ['
|
|
24
|
+
return ['mt-px flex size-5 shrink-0 items-center justify-center', toneClasses[props.variant]];
|
|
24
25
|
};
|
|
25
26
|
|
|
26
|
-
//
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
checkmark: 'text-green-600 dark:text-green-400',
|
|
30
|
-
cross: 'text-red-600 dark:text-red-400',
|
|
31
|
-
numbered: 'text-primary',
|
|
32
|
-
circle: 'text-primary'
|
|
33
|
-
};
|
|
34
|
-
return ['text-xs size-4', toneClasses[props.variant]];
|
|
27
|
+
// Format the item number with a leading zero
|
|
28
|
+
const formatNumber = (index: number) => {
|
|
29
|
+
return String(index + 1).padStart(2, '0');
|
|
35
30
|
};
|
|
36
31
|
</script>
|
|
37
32
|
|
|
38
33
|
<template>
|
|
39
34
|
<div class="not-prose my-6 sm:my-8">
|
|
40
|
-
<Card data-aos="blur-up">
|
|
41
|
-
<
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
</
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
<div v-else-if="props.variant === 'numbered'" :class="getIconContainerClasses()">
|
|
56
|
-
<span :class="[getIconClasses(), 'font-semibold']">{{ index + 1 }}</span>
|
|
57
|
-
</div>
|
|
58
|
-
|
|
59
|
-
<!-- Cross -->
|
|
60
|
-
<div v-else-if="props.variant === 'cross'" :class="getIconContainerClasses()">
|
|
61
|
-
<HugeiconsIcon :icon="Cancel01Icon" :class="getIconClasses()" />
|
|
62
|
-
</div>
|
|
63
|
-
|
|
64
|
-
<!-- Content -->
|
|
65
|
-
<div class="flex-1">
|
|
66
|
-
<div class="text-foreground leading-relaxed">
|
|
67
|
-
{{ item }}
|
|
68
|
-
</div>
|
|
69
|
-
</div>
|
|
70
|
-
</li>
|
|
71
|
-
</ul>
|
|
72
|
-
</CardContent>
|
|
35
|
+
<Card data-aos="blur-up" class="gap-0! py-0! overflow-hidden">
|
|
36
|
+
<ul class="divide-y divide-border">
|
|
37
|
+
<li v-for="(item, index) in props.items" :key="index" data-aos="blur-up" :data-aos-delay="100 + index * 80" class="flex items-start gap-3 px-4 py-3.5 text-xs leading-relaxed text-foreground sm:px-5 md:text-sm">
|
|
38
|
+
<!-- Marker -->
|
|
39
|
+
<span aria-hidden="true" :class="getMarkerClasses()">
|
|
40
|
+
<HugeiconsIcon v-if="props.variant === 'checkmark'" :icon="Tick02Icon" :stroke-width="2.5" class="size-3" />
|
|
41
|
+
<HugeiconsIcon v-else-if="props.variant === 'cross'" :icon="Cancel01Icon" :stroke-width="2.5" class="size-3" />
|
|
42
|
+
<template v-else-if="props.variant === 'numbered'">{{ formatNumber(index) }}</template>
|
|
43
|
+
<span v-else class="size-1.5 rounded-full bg-primary" />
|
|
44
|
+
</span>
|
|
45
|
+
|
|
46
|
+
<!-- Content -->
|
|
47
|
+
<span class="flex-1">{{ item }}</span>
|
|
48
|
+
</li>
|
|
49
|
+
</ul>
|
|
73
50
|
</Card>
|
|
74
51
|
</div>
|
|
75
|
-
</template>
|
|
52
|
+
</template>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { Card
|
|
2
|
+
import { Card } from '../ui/card';
|
|
3
3
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
|
|
4
4
|
|
|
5
5
|
// Props
|
|
@@ -15,46 +15,42 @@ const props = withDefaults(defineProps<{
|
|
|
15
15
|
|
|
16
16
|
// Header cell classes
|
|
17
17
|
const getHeaderClasses = (colIndex: number) => {
|
|
18
|
-
const baseClasses = 'text-left font-semibold text-foreground';
|
|
19
|
-
const highlightClass = props.highlightCol === colIndex ? '
|
|
18
|
+
const baseClasses = 'h-10 px-4 text-left text-[11px] font-semibold uppercase tracking-wider text-muted-foreground sm:px-5';
|
|
19
|
+
const highlightClass = props.highlightCol === colIndex ? 'text-primary shadow-[inset_0_-2px_0_var(--primary)]' : '';
|
|
20
20
|
return [baseClasses, highlightClass];
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
// Body cell classes
|
|
24
24
|
const getCellClasses = (colIndex: number) => {
|
|
25
|
-
const baseClasses = 'align-top
|
|
26
|
-
const firstColClass = colIndex === 0 ? 'font-
|
|
27
|
-
const highlightClass = props.highlightCol === colIndex ? 'bg-primary/5
|
|
25
|
+
const baseClasses = 'min-w-32 px-4 py-3 align-top leading-relaxed whitespace-normal sm:px-5';
|
|
26
|
+
const firstColClass = colIndex === 0 ? 'font-medium text-foreground' : 'text-muted-foreground';
|
|
27
|
+
const highlightClass = props.highlightCol === colIndex ? 'bg-primary/5 text-foreground' : '';
|
|
28
28
|
return [baseClasses, firstColClass, highlightClass];
|
|
29
29
|
};
|
|
30
30
|
</script>
|
|
31
31
|
|
|
32
32
|
<template>
|
|
33
33
|
<div class="not-prose my-6 sm:my-8">
|
|
34
|
-
<Card data-aos="blur-up">
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
</TableRow>
|
|
45
|
-
</TableHeader>
|
|
34
|
+
<Card data-aos="blur-up" class="gap-0! py-0! overflow-hidden">
|
|
35
|
+
<Table class="w-full text-left text-sm">
|
|
36
|
+
<!-- Table header -->
|
|
37
|
+
<TableHeader>
|
|
38
|
+
<TableRow class="bg-surface hover:bg-surface">
|
|
39
|
+
<TableHead v-for="(header, colIndex) in props.headers" :key="colIndex" :class="getHeaderClasses(colIndex)">
|
|
40
|
+
{{ header }}
|
|
41
|
+
</TableHead>
|
|
42
|
+
</TableRow>
|
|
43
|
+
</TableHeader>
|
|
46
44
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
</div>
|
|
57
|
-
</CardContent>
|
|
45
|
+
<!-- Table body -->
|
|
46
|
+
<TableBody>
|
|
47
|
+
<TableRow v-for="(row, rowIndex) in props.rows" :key="rowIndex" class="last:border-b-0 hover:bg-surface/60">
|
|
48
|
+
<TableCell v-for="(cell, colIndex) in row" :key="colIndex" :class="getCellClasses(colIndex)">
|
|
49
|
+
{{ cell }}
|
|
50
|
+
</TableCell>
|
|
51
|
+
</TableRow>
|
|
52
|
+
</TableBody>
|
|
53
|
+
</Table>
|
|
58
54
|
</Card>
|
|
59
55
|
</div>
|
|
60
|
-
</template>
|
|
56
|
+
</template>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import
|
|
2
|
+
import CheckmarkCircle02Icon from '@hugeicons/core-free-icons/CheckmarkCircle02Icon';
|
|
3
|
+
import CodeIcon from '@hugeicons/core-free-icons/CodeIcon';
|
|
4
|
+
import Copy01Icon from '@hugeicons/core-free-icons/Copy01Icon';
|
|
3
5
|
import { HugeiconsIcon } from '@hugeicons/vue';
|
|
4
|
-
import { codeToHtml } from 'shiki';
|
|
5
6
|
import { computed, ref, watch } from 'vue';
|
|
6
7
|
import { useI18n } from 'vue-i18n';
|
|
7
8
|
import { Button } from '../ui/button';
|
|
8
|
-
import { Card } from '../ui/card';
|
|
9
9
|
|
|
10
10
|
type CodeLanguage = string;
|
|
11
11
|
|
|
@@ -27,15 +27,31 @@ const copied = ref(false);
|
|
|
27
27
|
const highlightedCode = ref('');
|
|
28
28
|
let highlightRequest = 0;
|
|
29
29
|
const resolvedTitle = computed(() => props.title || t('uiVintage.code.title'));
|
|
30
|
+
const languageLabels: Record<string, string> = {
|
|
31
|
+
js: 'JavaScript',
|
|
32
|
+
javascript: 'JavaScript',
|
|
33
|
+
ts: 'TypeScript',
|
|
34
|
+
typescript: 'TypeScript',
|
|
35
|
+
vue: 'Vue',
|
|
36
|
+
html: 'HTML',
|
|
37
|
+
css: 'CSS',
|
|
38
|
+
json: 'JSON',
|
|
39
|
+
md: 'Markdown',
|
|
40
|
+
markdown: 'Markdown',
|
|
41
|
+
yaml: 'YAML',
|
|
42
|
+
yml: 'YAML',
|
|
43
|
+
};
|
|
44
|
+
const languageLabel = computed(() => props.language === 'text' ? t('uiVintage.code.plainText') : languageLabels[props.language] || props.language);
|
|
30
45
|
const displayedCode = computed(() => {
|
|
31
|
-
if (props.language !== 'json') return props.code;
|
|
46
|
+
if (props.language !== 'json') return props.code.replace(/\n$/, '');
|
|
32
47
|
|
|
33
48
|
try {
|
|
34
49
|
return JSON.stringify(JSON.parse(props.code), null, 2);
|
|
35
50
|
} catch {
|
|
36
|
-
return props.code;
|
|
51
|
+
return props.code.replace(/\n$/, '');
|
|
37
52
|
}
|
|
38
53
|
});
|
|
54
|
+
const codeLines = computed(() => displayedCode.value.split(/\r?\n/));
|
|
39
55
|
|
|
40
56
|
// Highlight code in the library so all consumers share the same rendering behavior.
|
|
41
57
|
const highlightCode = async () => {
|
|
@@ -45,9 +61,12 @@ const highlightCode = async () => {
|
|
|
45
61
|
if (!displayedCode.value || props.language === 'text') return;
|
|
46
62
|
|
|
47
63
|
try {
|
|
64
|
+
// Load the highlighter only when a code block is rendered, so pages without code never download it
|
|
65
|
+
const { codeToHtml } = await import('shiki');
|
|
48
66
|
const html = await codeToHtml(displayedCode.value, {
|
|
49
67
|
lang: props.language,
|
|
50
|
-
|
|
68
|
+
themes: { light: 'github-light', dark: 'github-dark' },
|
|
69
|
+
defaultColor: false
|
|
51
70
|
});
|
|
52
71
|
|
|
53
72
|
if (requestId === highlightRequest) {
|
|
@@ -76,31 +95,59 @@ const copyCode = async () => {
|
|
|
76
95
|
|
|
77
96
|
<template>
|
|
78
97
|
<div class="not-prose my-6 sm:my-8">
|
|
79
|
-
<
|
|
80
|
-
<!--
|
|
81
|
-
<div class="flex items-center
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
<span class="
|
|
85
|
-
|
|
98
|
+
<div data-aos="blur-up" data-code-window class="overflow-hidden rounded border border-border bg-card text-foreground shadow-elevated-sm">
|
|
99
|
+
<!-- Window title bar -->
|
|
100
|
+
<div class="relative flex h-8 items-center border-b border-border bg-surface px-3">
|
|
101
|
+
<!-- Traffic lights -->
|
|
102
|
+
<span aria-hidden="true" class="flex items-center gap-2">
|
|
103
|
+
<span class="size-3 rounded-full bg-destructive/85" />
|
|
104
|
+
<span class="size-3 rounded-full bg-warning/85" />
|
|
105
|
+
<span class="size-3 rounded-full bg-success/85" />
|
|
106
|
+
</span>
|
|
107
|
+
|
|
108
|
+
<!-- Window title -->
|
|
109
|
+
<div class="pointer-events-none absolute inset-x-24 truncate text-center text-xs text-muted-foreground">{{ resolvedTitle }}</div>
|
|
86
110
|
|
|
87
111
|
<!-- Copy code button -->
|
|
88
112
|
<Button v-if="props.copyable"
|
|
89
113
|
variant="ghost"
|
|
90
114
|
size="icon"
|
|
91
|
-
class="size-
|
|
92
|
-
:aria-label="t('uiVintage.code.copy')"
|
|
93
|
-
:title="t('uiVintage.code.copy')"
|
|
115
|
+
class="ml-auto size-6 text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
116
|
+
:aria-label="copied ? t('uiVintage.code.copied') : t('uiVintage.code.copy')"
|
|
117
|
+
:title="copied ? t('uiVintage.code.copied') : t('uiVintage.code.copy')"
|
|
94
118
|
:disabled="!displayedCode"
|
|
95
119
|
@click="copyCode">
|
|
96
|
-
<HugeiconsIcon :icon="copied ? CheckmarkCircle02Icon : Copy01Icon" class="size-
|
|
120
|
+
<HugeiconsIcon :icon="copied ? CheckmarkCircle02Icon : Copy01Icon" class="size-3.5" />
|
|
97
121
|
</Button>
|
|
122
|
+
|
|
123
|
+
<!-- Announce the copy to screen readers -->
|
|
124
|
+
<span v-if="props.copyable" class="sr-only" aria-live="polite">{{ copied ? t('uiVintage.code.copied') : '' }}</span>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<!-- Editor tabs -->
|
|
128
|
+
<div class="flex h-9 items-stretch border-b border-border bg-surface">
|
|
129
|
+
<!-- Active tab -->
|
|
130
|
+
<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">
|
|
131
|
+
<HugeiconsIcon :icon="CodeIcon" class="size-3.5 shrink-0 text-primary" />
|
|
132
|
+
<span class="truncate text-xs">{{ resolvedTitle }}</span>
|
|
133
|
+
</div>
|
|
98
134
|
</div>
|
|
99
135
|
|
|
100
136
|
<!-- Code content -->
|
|
101
|
-
<div v-if="highlightedCode" class="code-block-content overflow-x-auto
|
|
102
|
-
<
|
|
103
|
-
|
|
137
|
+
<div v-if="highlightedCode" class="code-block-content uv-code-block-content overflow-x-auto" v-html="highlightedCode" />
|
|
138
|
+
<div v-else class="code-block-content overflow-x-auto">
|
|
139
|
+
<pre><code><template v-for="(line, index) in codeLines" :key="index"><span class="line">{{ line }}</span>{{ index < codeLines.length - 1 ? '\n' : '' }}</template></code></pre>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<!-- Status bar -->
|
|
143
|
+
<div class="flex h-6 items-center justify-between gap-3 border-t border-border bg-surface px-3 text-[11px] text-muted-foreground">
|
|
144
|
+
<span class="truncate">{{ t('uiVintage.code.position', { line: codeLines.length, column: 1 }) }}</span>
|
|
145
|
+
<span class="flex shrink-0 items-center gap-3">
|
|
146
|
+
<span>UTF-8</span>
|
|
147
|
+
<span>{{ languageLabel }}</span>
|
|
148
|
+
</span>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
104
151
|
</div>
|
|
105
152
|
</template>
|
|
106
153
|
|
|
@@ -108,15 +155,44 @@ const copyCode = async () => {
|
|
|
108
155
|
.code-block-content :deep(pre) {
|
|
109
156
|
margin: 0;
|
|
110
157
|
min-width: max-content;
|
|
111
|
-
padding: 0.75rem 1rem;
|
|
158
|
+
padding: 0.75rem 1rem 0.75rem 0;
|
|
159
|
+
background-color: transparent !important;
|
|
160
|
+
color: var(--foreground);
|
|
112
161
|
font-size: 0.75rem;
|
|
113
162
|
line-height: 1.5rem;
|
|
163
|
+
counter-reset: line;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* Line numbers in the editor gutter */
|
|
167
|
+
.code-block-content :deep(.line)::before {
|
|
168
|
+
counter-increment: line;
|
|
169
|
+
content: counter(line);
|
|
170
|
+
display: inline-block;
|
|
171
|
+
width: 2.75rem;
|
|
172
|
+
margin-right: 1rem;
|
|
173
|
+
padding-right: 0.25rem;
|
|
174
|
+
text-align: right;
|
|
175
|
+
color: color-mix(in oklab, var(--muted-foreground) 65%, transparent);
|
|
176
|
+
user-select: none;
|
|
114
177
|
}
|
|
115
178
|
|
|
116
179
|
@media (min-width: 640px) {
|
|
117
180
|
.code-block-content :deep(pre) {
|
|
118
|
-
padding: 1rem 1.25rem;
|
|
181
|
+
padding: 1rem 1.25rem 1rem 0;
|
|
119
182
|
font-size: 0.875rem;
|
|
120
183
|
}
|
|
121
184
|
}
|
|
122
|
-
</style>
|
|
185
|
+
</style>
|
|
186
|
+
|
|
187
|
+
<style>
|
|
188
|
+
/* Shiki emits both themes as CSS variables; pick the one matching the library's `.dark` class and drop Nuxt Content's per-token backgrounds */
|
|
189
|
+
.uv-code-block-content .shiki span {
|
|
190
|
+
color: var(--shiki-light);
|
|
191
|
+
background-color: transparent;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.dark .uv-code-block-content .shiki span {
|
|
195
|
+
color: var(--shiki-dark);
|
|
196
|
+
background-color: transparent;
|
|
197
|
+
}
|
|
198
|
+
</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,6 +36,11 @@ 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>
|
|
@@ -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>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import
|
|
2
|
+
import ChartLineData02Icon from '@hugeicons/core-free-icons/ChartLineData02Icon';
|
|
3
3
|
import { HugeiconsIcon } from '@hugeicons/vue';
|
|
4
4
|
import type { HugeiconsIconDefinition } from '../../lib/common-types';
|
|
5
5
|
|
|
@@ -15,15 +15,15 @@ const props = withDefaults(defineProps<{
|
|
|
15
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-border to-primary/
|
|
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 size-
|
|
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-border to-primary/
|
|
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>
|