@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.
Files changed (152) hide show
  1. package/AGENTS.md +41 -0
  2. package/COMPONENTS.md +89 -18
  3. package/README.md +296 -262
  4. package/module.mjs +5 -11
  5. package/package.json +30 -12
  6. package/src/components/blog/BlogContentRenderer.vue +1 -1
  7. package/src/components/blog/BlogFAQSection.vue +1 -1
  8. package/src/components/blog/BlogHeaderSection.vue +93 -19
  9. package/src/components/blog/BlogInfoSection.vue +42 -37
  10. package/src/components/blog/BlogPostHeader.vue +181 -0
  11. package/src/components/blog/BlogProgressBar.vue +3 -5
  12. package/src/components/blog/BlogSectionTitle.vue +8 -6
  13. package/src/components/blog/BlogTagsSection.vue +16 -7
  14. package/src/components/blog/CategoryCard.vue +19 -23
  15. package/src/components/blog/HeaderCarousel.vue +17 -7
  16. package/src/components/blog/PostCard.vue +14 -21
  17. package/src/components/blog/SocialShareSidebar.vue +32 -15
  18. package/src/components/blog/TableOfContents.vue +64 -29
  19. package/src/components/blog/index.ts +1 -0
  20. package/src/components/card-grid/CardGrid.vue +3 -3
  21. package/src/components/chip/Chip.vue +1 -1
  22. package/src/components/confirm-dialog/ConfirmDialog.vue +6 -1
  23. package/src/components/confirm-dialog/confirm-dialog-state.ts +22 -22
  24. package/src/components/content/BlogList.vue +29 -52
  25. package/src/components/content/BlogTable.vue +26 -30
  26. package/src/components/content/CodeBlock.vue +99 -23
  27. package/src/components/content/Flow.vue +29 -35
  28. package/src/components/content/ProseHr.vue +6 -6
  29. package/src/components/content/ProsePre.vue +9 -4
  30. package/src/components/content/Terminal.vue +45 -21
  31. package/src/components/cookie-consent/CookieConsent.vue +1 -1
  32. package/src/components/dashboard-shell/DashboardShell.vue +24 -7
  33. package/src/components/empty-state-card/EmptyStateCard.vue +3 -3
  34. package/src/components/error-page/ErrorPage.vue +4 -4
  35. package/src/components/info-card/InfoCard.vue +4 -4
  36. package/src/components/landing/landing-footer/LandingFooter.vue +17 -12
  37. package/src/components/landing/landing-navbar/LandingNavbar.vue +22 -6
  38. package/src/components/landing/landing-shell/LandingShell.vue +2 -7
  39. package/src/components/language-flag/index.ts +1 -0
  40. package/src/components/language-selector/LanguageSelector.vue +19 -11
  41. package/src/components/load-more-button/LoadMoreButton.vue +9 -13
  42. package/src/components/loading-state-card/LoadingStateCard.vue +3 -3
  43. package/src/components/message-dialog/MessageDialog.vue +14 -12
  44. package/src/components/message-dialog/message-dialog-state.ts +1 -4
  45. package/src/components/message-toast/MessageToast.vue +28 -10
  46. package/src/components/message-toast/message-toast-state.ts +20 -0
  47. package/src/components/page-header/PageHeader.vue +1 -1
  48. package/src/components/progress-component/ProgressComponent.vue +7 -3
  49. package/src/components/single-value-card/SingleValueCard.vue +39 -40
  50. package/src/components/slider-form-component/SliderFormComponent.vue +1 -1
  51. package/src/components/stat-strip/StatStrip.vue +36 -0
  52. package/src/components/stat-strip/index.ts +2 -0
  53. package/src/components/switch-form-component/SwitchFormComponent.vue +5 -5
  54. package/src/components/text-link/TextLink.vue +1 -1
  55. package/src/components/theme-selector/ThemeSelector.vue +6 -3
  56. package/src/components/theme-selector/theme-transition.ts +15 -3
  57. package/src/components/ui/accordion/Accordion.vue +19 -19
  58. package/src/components/ui/alert/AlertTitle.vue +1 -1
  59. package/src/components/ui/alert/index.ts +1 -1
  60. package/src/components/ui/avatar/AvatarFallback.vue +2 -1
  61. package/src/components/ui/avatar/AvatarGroup.vue +13 -3
  62. package/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue +3 -3
  63. package/src/components/ui/busy/Busy.vue +5 -7
  64. package/src/components/ui/button/Button.vue +4 -2
  65. package/src/components/ui/button/index.ts +1 -4
  66. package/src/components/ui/calendar/Calendar.vue +12 -7
  67. package/src/components/ui/calendar/CalendarCell.vue +1 -1
  68. package/src/components/ui/calendar/CalendarCellTrigger.vue +1 -1
  69. package/src/components/ui/calendar/CalendarHeadCell.vue +1 -1
  70. package/src/components/ui/calendar/CalendarNextButton.vue +2 -2
  71. package/src/components/ui/calendar/CalendarPrevButton.vue +2 -2
  72. package/src/components/ui/card/Card.vue +5 -15
  73. package/src/components/ui/checkbox/Checkbox.vue +5 -1
  74. package/src/components/ui/combobox/ComboboxInput.vue +5 -1
  75. package/src/components/ui/combobox/ComboboxItem.vue +1 -1
  76. package/src/components/ui/combobox/ComboboxSelect.vue +2 -1
  77. package/src/components/ui/combobox/ComboboxTrigger.vue +1 -1
  78. package/src/components/ui/command/CommandGroup.vue +17 -3
  79. package/src/components/ui/command/CommandInput.vue +2 -2
  80. package/src/components/ui/command/CommandList.vue +5 -1
  81. package/src/components/ui/data-table/DataTable.vue +13 -4
  82. package/src/components/ui/date-picker/DatePicker.vue +8 -2
  83. package/src/components/ui/date-time-picker/DateTimePicker.vue +5 -1
  84. package/src/components/ui/dialog/DialogContent.vue +1 -1
  85. package/src/components/ui/dialog/DialogScrollContent.vue +1 -1
  86. package/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +1 -1
  87. package/src/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
  88. package/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +2 -2
  89. package/src/components/ui/dropdown-menu/classes.ts +2 -0
  90. package/src/components/ui/field/Field.vue +4 -0
  91. package/src/components/ui/field/FieldLabel.vue +6 -2
  92. package/src/components/ui/field/field-context.ts +62 -0
  93. package/src/components/ui/field/index.ts +1 -0
  94. package/src/components/ui/file-dropzone/FileDropzone.vue +14 -5
  95. package/src/components/ui/input/Input.vue +5 -1
  96. package/src/components/ui/native-select/NativeSelect.vue +7 -3
  97. package/src/components/ui/number-field/NumberField.vue +7 -2
  98. package/src/components/ui/pagination/PaginationEllipsis.vue +1 -1
  99. package/src/components/ui/pagination/PaginationFirst.vue +1 -1
  100. package/src/components/ui/pagination/PaginationItem.vue +1 -1
  101. package/src/components/ui/pagination/PaginationLast.vue +1 -1
  102. package/src/components/ui/pagination/PaginationNext.vue +1 -1
  103. package/src/components/ui/pagination/PaginationPrevious.vue +1 -1
  104. package/src/components/ui/pin-input/PinInputInput.vue +2 -2
  105. package/src/components/ui/progress/Progress.vue +2 -2
  106. package/src/components/ui/radio-group/RadioGroupCard.vue +1 -1
  107. package/src/components/ui/select/SelectItem.vue +2 -2
  108. package/src/components/ui/select/SelectItemContent.vue +1 -1
  109. package/src/components/ui/select/SelectScrollDownButton.vue +1 -1
  110. package/src/components/ui/select/SelectScrollUpButton.vue +1 -1
  111. package/src/components/ui/select/SelectTrigger.vue +6 -2
  112. package/src/components/ui/sidebar/Sidebar.vue +1 -1
  113. package/src/components/ui/sidebar/SidebarGroupAction.vue +1 -1
  114. package/src/components/ui/sidebar/SidebarGroupLabel.vue +1 -1
  115. package/src/components/ui/sidebar/SidebarInset.vue +1 -1
  116. package/src/components/ui/sidebar/SidebarMenuAction.vue +1 -1
  117. package/src/components/ui/sidebar/SidebarMenuBadge.vue +1 -1
  118. package/src/components/ui/sidebar/SidebarMenuSkeleton.vue +2 -2
  119. package/src/components/ui/sidebar/SidebarMenuSubButton.vue +1 -1
  120. package/src/components/ui/sidebar/SidebarProvider.vue +17 -6
  121. package/src/components/ui/sidebar/SidebarTrigger.vue +1 -1
  122. package/src/components/ui/sidebar/index.ts +2 -2
  123. package/src/components/ui/sidebar/utils.ts +1 -1
  124. package/src/components/ui/slider/Slider.vue +5 -4
  125. package/src/components/ui/spinner/Spinner.vue +3 -2
  126. package/src/components/ui/stepper/SimpleStepper.vue +1 -1
  127. package/src/components/ui/stepper/StepperDescription.vue +5 -3
  128. package/src/components/ui/stepper/StepperIndicator.vue +3 -3
  129. package/src/components/ui/stepper/StepperTitle.vue +5 -3
  130. package/src/components/ui/switch/Switch.vue +6 -2
  131. package/src/components/ui/tabs/TabsList.vue +1 -1
  132. package/src/components/ui/tabs/TabsTrigger.vue +1 -1
  133. package/src/components/ui/tags-input/TagsInputItemDelete.vue +1 -1
  134. package/src/components/ui/textarea/Textarea.vue +5 -1
  135. package/src/components/ui/time-picker/TimePicker.vue +2 -2
  136. package/src/components/ui/toggle-group/ToggleGroup.vue +1 -1
  137. package/src/components/ui/tooltip/TooltipContent.vue +1 -1
  138. package/src/i18n/de.json +16 -6
  139. package/src/i18n/en.json +16 -6
  140. package/src/i18n/es.json +16 -6
  141. package/src/i18n/fr.json +16 -6
  142. package/src/i18n/it.json +16 -6
  143. package/src/i18n/ja.json +16 -6
  144. package/src/i18n/pt.json +16 -6
  145. package/src/i18n/ru.json +16 -6
  146. package/src/i18n/zh.json +16 -6
  147. package/src/lib/color-tokens.ts +1 -1
  148. package/src/lib/i18n.ts +32 -4
  149. package/src/lib/utils.ts +21 -1
  150. package/src/runtime/ui-vintage-i18n-plugin.ts +11 -1
  151. package/src/styles.css +12 -16
  152. 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 (!confirmDialogState.current || !confirmDialogState.isOpen) return;
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 the cancel handler for the active dialog
150
- export const cancelActiveConfirmDialog = async () => {
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 current.options.onCancel?.();
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
- const current = confirmDialogState.current;
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 { Cancel01Icon, CheckmarkCircle02Icon, CircleSmallIcon } from '@hugeicons/core-free-icons';
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, CardContent } from '../ui/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
- // Icon container classes
16
- const getIconContainerClasses = () => {
16
+ // Marker classes
17
+ const getMarkerClasses = () => {
17
18
  const toneClasses = {
18
- checkmark: 'border-green-200 bg-green-50 dark:border-green-900/50 dark:bg-green-950/30',
19
- cross: 'border-red-200 bg-red-50 dark:border-red-900/50 dark:bg-red-950/30',
20
- numbered: 'border-primary/30 bg-primary/10',
21
- circle: 'border-primary/30 bg-primary/10'
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 ['flex items-center justify-center size-8 border rounded mr-4 shrink-0', toneClasses[props.variant]];
24
+ return ['mt-px flex size-5 shrink-0 items-center justify-center', toneClasses[props.variant]];
24
25
  };
25
26
 
26
- // Icon color classes
27
- const getIconClasses = () => {
28
- const toneClasses = {
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
- <CardContent class="gap-0">
42
- <ul class="text-xs md:text-sm p-0! space-y-6!">
43
- <li v-for="(item, index) in props.items" :key="index" data-aos="blur-up" :data-aos-delay="100 + index * 80" class="flex items-center">
44
- <!-- Circle -->
45
- <div v-if="props.variant === 'circle'" :class="getIconContainerClasses()">
46
- <HugeiconsIcon :icon="CircleSmallIcon" :class="getIconClasses()" />
47
- </div>
48
-
49
- <!-- Checkmark -->
50
- <div v-else-if="props.variant === 'checkmark'" :class="getIconContainerClasses()">
51
- <HugeiconsIcon :icon="CheckmarkCircle02Icon" :class="getIconClasses()" />
52
- </div>
53
-
54
- <!-- Numbered -->
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, CardContent } from '../ui/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 ? 'bg-primary/10 text-primary' : '';
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 text-foreground leading-relaxed whitespace-normal';
26
- const firstColClass = colIndex === 0 ? 'font-semibold text-muted-foreground' : '';
27
- const highlightClass = props.highlightCol === colIndex ? 'bg-primary/5 font-semibold' : '';
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
- <CardContent>
36
- <div class="w-full overflow-x-auto">
37
- <Table class="w-full text-sm text-left">
38
- <!-- Table header -->
39
- <TableHeader>
40
- <TableRow class="bg-transparent text-sm text-muted-foreground hover:bg-transparent">
41
- <TableHead v-for="(header, colIndex) in props.headers" :key="colIndex" :class="getHeaderClasses(colIndex)">
42
- {{ header }}
43
- </TableHead>
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
- <!-- Table body -->
48
- <TableBody>
49
- <TableRow v-for="(row, rowIndex) in props.rows" :key="rowIndex" class="text-muted-foreground transition-colors duration-150 hover:bg-surface/70">
50
- <TableCell v-for="(cell, colIndex) in row" :key="colIndex" :class="getCellClasses(colIndex)">
51
- {{ cell }}
52
- </TableCell>
53
- </TableRow>
54
- </TableBody>
55
- </Table>
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 { CheckmarkCircle02Icon, Copy01Icon, CodeIcon } from '@hugeicons/core-free-icons';
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
- theme: 'github-dark'
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
- <Card data-aos="blur-up" class="gap-0! sm:gap-0! overflow-hidden p-0! text-foreground">
80
- <!-- Code header -->
81
- <div class="flex items-center justify-between gap-3 border-b border-border bg-card px-3 py-2 sm:px-4">
82
- <div class="flex min-w-0 items-center gap-2">
83
- <HugeiconsIcon :icon="CodeIcon" class="size-4 shrink-0 text-primary" />
84
- <span class="truncate text-xs font-semibold text-muted-foreground">{{ resolvedTitle }}</span>
85
- </div>
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-7 text-muted-foreground hover:bg-accent hover:text-foreground"
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-4" />
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 bg-surface" v-html="highlightedCode" />
102
- <pre v-else class="code-block-content overflow-x-auto bg-surface px-4 py-3 text-xs leading-6 sm:px-5 sm:py-4 sm:text-sm"><code>{{ displayedCode }}</code></pre>
103
- </Card>
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
- <div :class="[isVertical() ? 'flex flex-col gap-3' : 'flex flex-col gap-3 md:flex-row md:items-stretch']">
62
- <!-- Flow items -->
63
- <template v-for="(item, index) in props.items" :key="`${getItemLabel(item)}-${index}`">
64
- <!-- Flow item -->
65
- <Card data-aos="blur-up" :data-aos-delay="100 + index * 90" class="flex-1 overflow-hidden transition-[border-color,translate] duration-300 ease-spring hover:-translate-y-0.5 hover:border-primary/40 md:flex md:items-center">
66
- <CardContent class="w-full gap-3 md:justify-center">
67
- <!-- Flow item header -->
68
- <div class="inline-flex items-center gap-3 text-xs font-semibold text-foreground">
69
- <!-- Flow item number -->
70
- <span class="inline-flex size-8 shrink-0 items-center justify-center rounded border border-primary/30 bg-primary/10 text-xs font-bold text-primary">
71
- {{ index + 1 }}
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
- <!-- Flow item separator -->
86
- <div v-if="index < props.items.length - 1" data-aos="blur-up" :data-aos-delay="140 + index * 90" class="flex items-center justify-center text-primary">
87
- <!-- Flow item separator for vertical orientation -->
88
- <HugeiconsIcon v-if="isVertical()" :icon="ArrowDown01Icon" class="size-5 shrink-0" />
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
- <!-- Flow item separator for horizontal orientation -->
91
- <HugeiconsIcon v-else :icon="ArrowRight01Icon" class="hidden size-5 shrink-0 md:block" />
92
- <HugeiconsIcon v-if="!isVertical()" :icon="ArrowDown01Icon" class="size-5 shrink-0 md:hidden" />
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
- </template>
95
- </div>
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 { ChartLineData02Icon } from '@hugeicons/core-free-icons';
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/50" />
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-10 rotate-45 items-center justify-center rounded border border-primary/30 bg-card shadow-glow">
22
- <HugeiconsIcon :icon="props.icon" class="size-4 -rotate-45 text-primary" />
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/50" />
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>