@brumaombra/ui-vintage 0.9.6 → 0.9.8

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 (83) hide show
  1. package/AGENTS.md +3 -1
  2. package/COMPONENTS.md +132 -15
  3. package/package.json +4 -2
  4. package/src/components/action-bar/ActionBar.vue +38 -0
  5. package/src/components/action-bar/index.ts +2 -0
  6. package/src/components/blog/BlogContentRenderer.vue +3 -14
  7. package/src/components/blog/BlogFAQSection.vue +1 -1
  8. package/src/components/blog/BlogIndexPage.vue +1 -1
  9. package/src/components/blog/BlogInfoSection.vue +4 -4
  10. package/src/components/blog/BlogPostHeader.vue +4 -5
  11. package/src/components/blog/BlogPostPage.vue +6 -2
  12. package/src/components/blog/BlogSectionTitle.vue +7 -21
  13. package/src/components/blog/BlogSummary.vue +31 -0
  14. package/src/components/blog/BlogTagsSection.vue +1 -1
  15. package/src/components/blog/CategoryCard.vue +1 -1
  16. package/src/components/blog/PostCard.vue +1 -1
  17. package/src/components/blog/blogPageUtils.ts +1 -0
  18. package/src/components/blog/index.ts +1 -0
  19. package/src/components/chip/Chip.vue +1 -1
  20. package/src/components/dashboard-shell/DashboardShell.vue +3 -3
  21. package/src/components/data-list/DataListText.vue +7 -4
  22. package/src/components/language-flag/flags/ChineseFlag.vue +1 -1
  23. package/src/components/language-flag/flags/EnglishFlag.vue +1 -1
  24. package/src/components/language-flag/flags/FrenchFlag.vue +1 -1
  25. package/src/components/language-flag/flags/GermanFlag.vue +1 -1
  26. package/src/components/language-flag/flags/ItalianFlag.vue +1 -1
  27. package/src/components/language-flag/flags/JapaneseFlag.vue +1 -1
  28. package/src/components/language-flag/flags/PortugueseFlag.vue +1 -1
  29. package/src/components/language-flag/flags/RussianFlag.vue +1 -1
  30. package/src/components/language-flag/flags/SpanishFlag.vue +1 -1
  31. package/src/components/language-selector/LanguageSelector.vue +2 -2
  32. package/src/components/section-title/SectionTitle.vue +47 -0
  33. package/src/components/section-title/index.ts +2 -0
  34. package/src/components/single-value-card/SingleValueCard.vue +1 -1
  35. package/src/components/ui/alert-dialog/AlertDialogAction.vue +6 -6
  36. package/src/components/ui/avatar/Avatar.vue +4 -2
  37. package/src/components/ui/avatar/AvatarFallback.vue +25 -5
  38. package/src/components/ui/badge/Badge.vue +21 -4
  39. package/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue +1 -1
  40. package/src/components/ui/breadcrumb/BreadcrumbLink.vue +1 -1
  41. package/src/components/ui/button/Button.vue +18 -2
  42. package/src/components/ui/button/index.ts +4 -0
  43. package/src/components/ui/card/Card.vue +25 -1
  44. package/src/components/ui/card/CardMedia.vue +15 -0
  45. package/src/components/ui/card/CardTitle.vue +8 -3
  46. package/src/components/ui/card/index.ts +1 -0
  47. package/src/components/ui/checkbox/Checkbox.vue +1 -1
  48. package/src/components/ui/combobox/ComboboxSelect.vue +2 -2
  49. package/src/components/ui/combobox/ComboboxTrigger.vue +1 -1
  50. package/src/components/ui/data-table/DataTable.vue +1 -1
  51. package/src/components/ui/dialog/DialogContent.vue +7 -3
  52. package/src/components/ui/dialog/DialogScrollContent.vue +7 -3
  53. package/src/components/ui/dialog/index.ts +11 -1
  54. package/src/components/ui/file-dropzone/FileDropzone.vue +1 -1
  55. package/src/components/ui/input/Input.vue +42 -9
  56. package/src/components/ui/kbd/Kbd.vue +8 -4
  57. package/src/components/ui/number-field/NumberField.vue +2 -2
  58. package/src/components/ui/progress/Progress.vue +17 -5
  59. package/src/components/ui/select/SelectItem.vue +1 -1
  60. package/src/components/ui/select/SelectItemContent.vue +5 -3
  61. package/src/components/ui/skeleton/Skeleton.vue +7 -3
  62. package/src/components/ui/switch/Switch.vue +1 -1
  63. package/src/components/ui/table/TableHeader.vue +1 -1
  64. package/src/components/ui/table/TableRow.vue +11 -4
  65. package/src/components/ui/tabs/Tabs.vue +1 -1
  66. package/src/components/ui/tabs/TabsContent.vue +8 -5
  67. package/src/components/ui/tabs/TabsTrigger.vue +1 -1
  68. package/src/components/ui/tags-input/TagsInputItemDelete.vue +1 -1
  69. package/src/components/ui/textarea/Textarea.vue +1 -1
  70. package/src/components/ui/toggle-group/ToggleGroup.vue +2 -2
  71. package/src/components/ui/toggle-group/ToggleGroupItem.vue +3 -1
  72. package/src/components/ui/toggle-group/index.ts +2 -2
  73. package/src/i18n/de.json +1 -0
  74. package/src/i18n/en.json +1 -0
  75. package/src/i18n/es.json +1 -0
  76. package/src/i18n/fr.json +1 -0
  77. package/src/i18n/it.json +1 -0
  78. package/src/i18n/ja.json +1 -0
  79. package/src/i18n/pt.json +1 -0
  80. package/src/i18n/ru.json +1 -0
  81. package/src/i18n/zh.json +1 -0
  82. package/src/lib/color-tokens.ts +6 -6
  83. package/src/styles.css +11 -6
package/AGENTS.md CHANGED
@@ -28,7 +28,9 @@ Only build something new when no entry point fits, and compose it from library p
28
28
  - Import from explicit subpaths: `import { Button } from '@brumaombra/ui-vintage/button'`. Never import from `@brumaombra/ui-vintage/src/...`.
29
29
  - Style with the semantic tokens (`bg-background`, `bg-card`, `bg-surface`, `bg-accent`, `bg-primary`, `text-foreground`, `text-muted-foreground`, `border-border`, `border-border-strong`, ...). They adapt to dark mode, so don't write `dark:` color variants or hard-coded colors for surfaces and text.
30
30
  - Customize components through their props and the `class` prop (merged with tailwind-merge) rather than wrapping them in extra styled elements.
31
- - Keep corners at `rounded` (the library's square look) and use `shadow-elevated-sm|md|lg|xl` for elevation, without glows or gradients.
31
+ - Keep corners at `rounded` (the library's square look) and use `shadow-elevated-sm|md|lg|xl` for elevation, without glows or gradients. Every radius step resolves to the shared `--uv-radius` token, so change the token instead of passing other radii.
32
+ - Never restyle a library component from an app with `class` (padding, gaps, colors, borders, radius, shadows, typography, `!` modifiers, `[data-slot]` selectors): use its props and variants, and add a prop or variant to the library when one is missing. Layout classes (margins, width, flex/grid placement, visibility, entrance animations) are fine.
33
+ - `SectionTitle` (and `BlogSectionTitle`) owns the space under it (`mb-6 md:mb-8`): don't add margins, padding or `space-y` to the content that follows it.
32
34
  - For motion, reuse the library utilities (`ease-spring`, `ease-out-expo`, `animate-uv-fade-up`, `animate-uv-pop`, `uv-floating-motion`). In custom transitions, name `scale`, `rotate`, and `translate` explicitly; Tailwind v4 doesn't animate them through `transform`.
33
35
  - Icons are Hugeicons definitions: import each one from its own module (`import Rocket01Icon from '@hugeicons/core-free-icons/Rocket01Icon'`, never the `@hugeicons/core-free-icons` barrel, which makes the build parse the whole icon set) and pass them to `:icon` props, or render them with `HugeiconsIcon` from `@hugeicons/vue`. Both packages are peer dependencies, so the app must list them in its own `package.json`.
34
36
  - Form controls use `v-model`. Mark invalid fields with `aria-invalid` and show messages with `FieldError`.
package/COMPONENTS.md CHANGED
@@ -15,13 +15,13 @@ This file lists every public entry point of the package with its components, pro
15
15
 
16
16
  ## Contents
17
17
 
18
- - **Actions:** [button](#brumaombraui-vintagebutton), [dropdown-menu](#brumaombraui-vintagedropdown-menu), [kbd](#brumaombraui-vintagekbd), [load-more-button](#brumaombraui-vintageload-more-button), [text-link](#brumaombraui-vintagetext-link), [toggle-group](#brumaombraui-vintagetoggle-group)
18
+ - **Actions:** [action-bar](#brumaombraui-vintageaction-bar), [button](#brumaombraui-vintagebutton), [dropdown-menu](#brumaombraui-vintagedropdown-menu), [kbd](#brumaombraui-vintagekbd), [load-more-button](#brumaombraui-vintageload-more-button), [text-link](#brumaombraui-vintagetext-link), [toggle-group](#brumaombraui-vintagetoggle-group)
19
19
  - **Forms:** [calendar](#brumaombraui-vintagecalendar), [checkbox](#brumaombraui-vintagecheckbox), [combobox](#brumaombraui-vintagecombobox), [date-picker](#brumaombraui-vintagedate-picker), [date-time-picker](#brumaombraui-vintagedate-time-picker), [field](#brumaombraui-vintagefield), [file-dropzone](#brumaombraui-vintagefile-dropzone), [input](#brumaombraui-vintageinput), [label](#brumaombraui-vintagelabel), [native-select](#brumaombraui-vintagenative-select), [number-field](#brumaombraui-vintagenumber-field), [pin-input](#brumaombraui-vintagepin-input), [radio-group](#brumaombraui-vintageradio-group), [select](#brumaombraui-vintageselect), [slider](#brumaombraui-vintageslider), [slider-form-component](#brumaombraui-vintageslider-form-component), [switch](#brumaombraui-vintageswitch), [switch-form-component](#brumaombraui-vintageswitch-form-component), [tags-input](#brumaombraui-vintagetags-input), [textarea](#brumaombraui-vintagetextarea), [time-picker](#brumaombraui-vintagetime-picker)
20
20
  - **Data display:** [animated-number](#brumaombraui-vintageanimated-number), [avatar](#brumaombraui-vintageavatar), [badge](#brumaombraui-vintagebadge), [card](#brumaombraui-vintagecard), [card-grid](#brumaombraui-vintagecard-grid), [chip](#brumaombraui-vintagechip), [data-list](#brumaombraui-vintagedata-list), [data-table](#brumaombraui-vintagedata-table), [info-card](#brumaombraui-vintageinfo-card), [progress](#brumaombraui-vintageprogress), [progress-component](#brumaombraui-vintageprogress-component), [scroll-area](#brumaombraui-vintagescroll-area), [separator](#brumaombraui-vintageseparator), [single-value-card](#brumaombraui-vintagesingle-value-card), [skeleton](#brumaombraui-vintageskeleton), [stat-strip](#brumaombraui-vintagestat-strip), [table](#brumaombraui-vintagetable)
21
21
  - **Navigation:** [accordion](#brumaombraui-vintageaccordion), [breadcrumb](#brumaombraui-vintagebreadcrumb), [collapsible](#brumaombraui-vintagecollapsible), [pagination](#brumaombraui-vintagepagination), [sidebar](#brumaombraui-vintagesidebar), [stepper](#brumaombraui-vintagestepper), [tabs](#brumaombraui-vintagetabs)
22
22
  - **Overlays:** [alert-dialog](#brumaombraui-vintagealert-dialog), [command](#brumaombraui-vintagecommand), [dialog](#brumaombraui-vintagedialog), [hover-card](#brumaombraui-vintagehover-card), [popover](#brumaombraui-vintagepopover), [sheet](#brumaombraui-vintagesheet), [tooltip](#brumaombraui-vintagetooltip)
23
23
  - **Feedback:** [alert](#brumaombraui-vintagealert), [busy](#brumaombraui-vintagebusy), [busy-indicator](#brumaombraui-vintagebusy-indicator), [confirm-dialog](#brumaombraui-vintageconfirm-dialog), [cookie-consent](#brumaombraui-vintagecookie-consent), [empty-state-card](#brumaombraui-vintageempty-state-card), [loading-state-card](#brumaombraui-vintageloading-state-card), [message-dialog](#brumaombraui-vintagemessage-dialog), [message-toast](#brumaombraui-vintagemessage-toast), [spinner](#brumaombraui-vintagespinner)
24
- - **Layouts and app shell:** [background-grid](#brumaombraui-vintagebackground-grid), [dashboard-shell](#brumaombraui-vintagedashboard-shell), [error-page](#brumaombraui-vintageerror-page), [landing](#brumaombraui-vintagelanding), [landing-content](#brumaombraui-vintagelanding-content), [landing-footer](#brumaombraui-vintagelanding-footer), [landing-navbar](#brumaombraui-vintagelanding-navbar), [landing-shell](#brumaombraui-vintagelanding-shell), [language-flag](#brumaombraui-vintagelanguage-flag), [language-selector](#brumaombraui-vintagelanguage-selector), [page-header](#brumaombraui-vintagepage-header), [theme-selector](#brumaombraui-vintagetheme-selector)
24
+ - **Layouts and app shell:** [background-grid](#brumaombraui-vintagebackground-grid), [dashboard-shell](#brumaombraui-vintagedashboard-shell), [error-page](#brumaombraui-vintageerror-page), [landing](#brumaombraui-vintagelanding), [landing-content](#brumaombraui-vintagelanding-content), [landing-footer](#brumaombraui-vintagelanding-footer), [landing-navbar](#brumaombraui-vintagelanding-navbar), [landing-shell](#brumaombraui-vintagelanding-shell), [language-flag](#brumaombraui-vintagelanguage-flag), [language-selector](#brumaombraui-vintagelanguage-selector), [page-header](#brumaombraui-vintagepage-header), [section-title](#brumaombraui-vintagesection-title), [theme-selector](#brumaombraui-vintagetheme-selector)
25
25
  - **Content and blog:** [blog](#brumaombraui-vintageblog), [content](#brumaombraui-vintagecontent)
26
26
  - **Utilities and types:** [common-types](#brumaombraui-vintagecommon-types), [utils](#brumaombraui-vintageutils)
27
27
 
@@ -29,9 +29,37 @@ This file lists every public entry point of the package with its components, pro
29
29
 
30
30
  Buttons, segmented controls, menus, and keyboard hints.
31
31
 
32
+ ### `@brumaombra/ui-vintage/action-bar`
33
+
34
+ Row of action buttons that turns into a full-width bar of equal tiles (icon above the label) on mobile, like an app toolbar.
35
+
36
+ ```ts
37
+ import { ActionBar } from '@brumaombra/ui-vintage/action-bar';
38
+ ```
39
+
40
+ ```vue
41
+ <ActionBar>
42
+ <Button variant="secondary">
43
+ <HugeiconsIcon :icon="Edit01Icon" />
44
+ Edit
45
+ </Button>
46
+ <Button variant="secondary">
47
+ <HugeiconsIcon :icon="Share08Icon" />
48
+ Share
49
+ </Button>
50
+ </ActionBar>
51
+ ```
52
+
53
+ #### `ActionBar`
54
+
55
+ - **Props:** `class`.
56
+ - **Slots:** `default`.
57
+
58
+ ---
59
+
32
60
  ### `@brumaombra/ui-vintage/button`
33
61
 
34
- Button with primary, secondary, ghost, link, and tone variants. Spring press feedback and a `loading` state that keeps the width stable. Give it a `to` to make it a navigation button (a localized link, external URLs work too).
62
+ Button with primary, secondary, ghost, link, and tone variants. Spring press feedback and a `loading` state that keeps the width stable. Give it a `to` to make it a navigation button (a localized link, external URLs work too), and a `count` to show a notification badge on its corner.
35
63
 
36
64
  ```ts
37
65
  import { Button, buttonVariants } from '@brumaombra/ui-vintage/button';
@@ -47,6 +75,10 @@ import type { ButtonVariants } from '@brumaombra/ui-vintage/button';
47
75
  <Button to="/pricing" variant="secondary">
48
76
  Pricing
49
77
  </Button>
78
+
79
+ <Button variant="secondary" size="icon" :count="unread" aria-label="Notifications">
80
+ <HugeiconsIcon :icon="Notification01Icon" />
81
+ </Button>
50
82
  ```
51
83
 
52
84
  #### `Button`
@@ -58,6 +90,8 @@ import type { ButtonVariants } from '@brumaombra/ui-vintage/button';
58
90
  | `loading` | `boolean` | `false` |
59
91
  | `disabled` | `boolean` | `false` |
60
92
  | `to` | `string` | |
93
+ | `count` | `number \| null` | `null` |
94
+ | `countMax` | `number` | `99` |
61
95
  | `class` | `HTMLAttributes['class']` | |
62
96
 
63
97
  - **Also accepts** all props of Reka `PrimitiveProps`.
@@ -219,6 +253,7 @@ import { Kbd, KbdGroup } from '@brumaombra/ui-vintage/kbd';
219
253
  | --- | --- | --- |
220
254
  | `class` | `HTMLAttributes['class']` | |
221
255
  | `pressed` | `boolean` | |
256
+ | `size` | `'default' \| 'lg'` | `'default'` |
222
257
 
223
258
  - **Slots:** `default`.
224
259
 
@@ -330,7 +365,7 @@ type ToggleVariants = { variant?: 'default' | 'outline' | null | undefined; size
330
365
  type ToggleGroupSize = 'default' | 'sm' | 'lg'
331
366
  const toggleVariants: (props?: ToggleVariants & ClassProp) => string
332
367
  const toggleGroupItemVariants: (props?: { size?: ToggleGroupSize | null } & ClassProp) => string
333
- const toggleGroupContextKey: InjectionKey<{ size: Ref<ToggleGroupSize>; indicator: Ref<boolean> }>
368
+ const toggleGroupContextKey: InjectionKey<{ size: Ref<ToggleGroupSize>; indicator: Ref<boolean>; highlight: Ref<boolean> }>
334
369
  ```
335
370
 
336
371
  ## Forms
@@ -766,9 +801,11 @@ import { Input } from '@brumaombra/ui-vintage/input';
766
801
  | --- | --- | --- |
767
802
  | `defaultValue` | `string \| number` | |
768
803
  | `modelValue` | `string \| number` | |
804
+ | `size` | `'default' \| 'sm'` | |
769
805
  | `class` | `HTMLAttributes['class']` | |
770
806
 
771
807
  - **Emits:** `update:modelValue` (payload: string \| number).
808
+ - **Slots:** `prefix`, `suffix`.
772
809
 
773
810
  ---
774
811
 
@@ -927,7 +964,7 @@ import { RadioGroup, RadioGroupItem, RadioGroupCard } from '@brumaombra/ui-vinta
927
964
 
928
965
  ### `@brumaombra/ui-vintage/select`
929
966
 
930
- Styled Reka select. `SelectValueContent` and `SelectItemContent` render an icon, label, and description.
967
+ Styled Reka select. `SelectValueContent` and `SelectItemContent` render an icon, label, and description; the default slot of `SelectItemContent` replaces the icon with any leading visual, such as a `LanguageFlag`.
931
968
 
932
969
  ```ts
933
970
  import { Select, SelectContent, SelectGroup, SelectItem, SelectItemContent, SelectItemText, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, SelectValueContent } from '@brumaombra/ui-vintage/select';
@@ -982,6 +1019,8 @@ import { Select, SelectContent, SelectGroup, SelectItem, SelectItemContent, Sele
982
1019
  | `textClass` | `HTMLAttributes['class']` | |
983
1020
  | `descriptionClass` | `HTMLAttributes['class']` | |
984
1021
 
1022
+ - **Slots:** `default`.
1023
+
985
1024
  #### `SelectItemText`
986
1025
 
987
1026
  - **Also accepts** all props of Reka `SelectItemTextProps`.
@@ -1268,6 +1307,7 @@ import type { AvatarSize, AvatarShape, AvatarStatus } from '@brumaombra/ui-vinta
1268
1307
  | `size` | `AvatarSize` | `'md'` |
1269
1308
  | `shape` | `AvatarShape` | `'circle'` |
1270
1309
  | `status` | `AvatarStatus` | |
1310
+ | `ring` | `boolean` | `false` |
1271
1311
  | `class` | `HTMLAttributes['class']` | |
1272
1312
 
1273
1313
  - **Also accepts** all props of Reka `AvatarRootProps`.
@@ -1278,6 +1318,7 @@ import type { AvatarSize, AvatarShape, AvatarStatus } from '@brumaombra/ui-vinta
1278
1318
  | Prop | Type | Default |
1279
1319
  | --- | --- | --- |
1280
1320
  | `name` | `string` | |
1321
+ | `color` | `'neutral' \| 'auto' \| 'primary'` | `'neutral'` |
1281
1322
  | `class` | `HTMLAttributes['class']` | |
1282
1323
 
1283
1324
  - **Also accepts** all props of Reka `AvatarFallbackProps`.
@@ -1331,9 +1372,10 @@ import { Badge } from '@brumaombra/ui-vintage/badge';
1331
1372
  | --- | --- | --- |
1332
1373
  | `text` *(required)* | `string` | |
1333
1374
  | `icon` | `HugeiconsIconDefinition \| null` | `null` |
1334
- | `color` | `ToneColor` | `'gray'` |
1375
+ | `color` | `ToneColor \| 'primary'` | `'gray'` |
1335
1376
  | `dot` | `boolean` | `false` |
1336
1377
  | `pulse` | `boolean` | `false` |
1378
+ | `interactive` | `boolean` | `false` |
1337
1379
  | `class` | `HTMLAttributes['class']` | |
1338
1380
 
1339
1381
  ---
@@ -1343,7 +1385,7 @@ import { Badge } from '@brumaombra/ui-vintage/badge';
1343
1385
  Surface container with header, title, description, content, footer, and action slots. `interactive` adds a gentle hover lift with a stronger shadow; `color` switches to a tone surface.
1344
1386
 
1345
1387
  ```ts
1346
- import { Card, CardAction, CardContent, CardText, CardDescription, CardFooter, CardHeader, CardTitle } from '@brumaombra/ui-vintage/card';
1388
+ import { Card, CardAction, CardContent, CardText, CardDescription, CardFooter, CardHeader, CardMedia, CardTitle } from '@brumaombra/ui-vintage/card';
1347
1389
  ```
1348
1390
 
1349
1391
  ```vue
@@ -1361,6 +1403,8 @@ import { Card, CardAction, CardContent, CardText, CardDescription, CardFooter, C
1361
1403
  | Prop | Type | Default |
1362
1404
  | --- | --- | --- |
1363
1405
  | `color` | `CardColor` | |
1406
+ | `padding` | `CardPadding` | `'default'` |
1407
+ | `elevation` | `CardElevation` | |
1364
1408
  | `interactive` | `boolean` | `false` |
1365
1409
  | `class` | `HTMLAttributes['class']` | |
1366
1410
 
@@ -1396,12 +1440,18 @@ import { Card, CardAction, CardContent, CardText, CardDescription, CardFooter, C
1396
1440
  - **Props:** `class`.
1397
1441
  - **Slots:** `default`.
1398
1442
 
1443
+ #### `CardMedia`
1444
+
1445
+ - **Props:** `class`.
1446
+ - **Slots:** `default`.
1447
+
1399
1448
  #### `CardTitle`
1400
1449
 
1401
1450
  | Prop | Type | Default |
1402
1451
  | --- | --- | --- |
1403
1452
  | `class` | `HTMLAttributes['class']` | |
1404
1453
  | `level` | `HeadingLevel` | `3` |
1454
+ | `icon` | `HugeiconsIconDefinition \| null` | `null` |
1405
1455
 
1406
1456
  - **Slots:** `default`.
1407
1457
 
@@ -1477,7 +1527,11 @@ import { DataList, DataListItem, DataListLabel, DataListText, DataListValue } fr
1477
1527
 
1478
1528
  #### `DataListText`
1479
1529
 
1480
- - **Props:** `class`.
1530
+ | Prop | Type | Default |
1531
+ | --- | --- | --- |
1532
+ | `wrap` | `boolean` | `false` |
1533
+ | `class` | `HTMLAttributes['class']` | |
1534
+
1481
1535
  - **Slots:** `default`.
1482
1536
 
1483
1537
  #### `DataListValue`
@@ -1582,6 +1636,7 @@ import { Progress } from '@brumaombra/ui-vintage/progress';
1582
1636
  | Prop | Type | Default |
1583
1637
  | --- | --- | --- |
1584
1638
  | `indeterminate` | `boolean` | `false` |
1639
+ | `color` | `ProgressColor` | `'primary'` |
1585
1640
  | `class` | `HTMLAttributes['class']` | |
1586
1641
 
1587
1642
  - **Also accepts** all props of Reka `ProgressRootProps`.
@@ -1684,7 +1739,10 @@ import { Skeleton } from '@brumaombra/ui-vintage/skeleton';
1684
1739
 
1685
1740
  #### `Skeleton`
1686
1741
 
1687
- - **Props:** `class`.
1742
+ | Prop | Type | Default |
1743
+ | --- | --- | --- |
1744
+ | `circle` | `boolean` | `false` |
1745
+ | `class` | `HTMLAttributes['class']` | |
1688
1746
 
1689
1747
  ---
1690
1748
 
@@ -1768,7 +1826,12 @@ import { Table, TableBody, TableCaption, TableCell, TableEmpty, TableFooter, Tab
1768
1826
 
1769
1827
  #### `TableRow`
1770
1828
 
1771
- - **Props:** `class`.
1829
+ | Prop | Type | Default |
1830
+ | --- | --- | --- |
1831
+ | `static` | `boolean` | `false` |
1832
+ | `interactive` | `boolean` | `false` |
1833
+ | `class` | `HTMLAttributes['class']` | |
1834
+
1772
1835
  - **Slots:** `default`.
1773
1836
 
1774
1837
  ## Navigation
@@ -2252,7 +2315,11 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@brumaombra/ui-vintage
2252
2315
 
2253
2316
  #### `TabsContent`
2254
2317
 
2255
- - **Props:** `class`.
2318
+ | Prop | Type | Default |
2319
+ | --- | --- | --- |
2320
+ | `variant` | `'plain' \| 'boxed'` | `'plain'` |
2321
+ | `class` | `HTMLAttributes['class']` | |
2322
+
2256
2323
  - **Also accepts** all props of Reka `TabsContentProps`.
2257
2324
  - **Slots:** `default`.
2258
2325
 
@@ -2292,7 +2359,11 @@ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
2292
2359
 
2293
2360
  #### `AlertDialogAction`
2294
2361
 
2295
- - **Props:** `class`.
2362
+ | Prop | Type | Default |
2363
+ | --- | --- | --- |
2364
+ | `variant` | `ButtonVariants['variant']` | |
2365
+ | `class` | `HTMLAttributes['class']` | |
2366
+
2296
2367
  - **Also accepts** all props of Reka `AlertDialogActionProps`.
2297
2368
  - **Slots:** `default`.
2298
2369
 
@@ -2437,7 +2508,8 @@ value [useCommandGroup, provideCommandGroupContext]
2437
2508
  Modal dialog. `DialogContent` accepts `show-close-button`; use `DialogScrollContent` for long content.
2438
2509
 
2439
2510
  ```ts
2440
- import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogScrollContent, DialogTitle, DialogTrigger } from '@brumaombra/ui-vintage/dialog';
2511
+ import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogScrollContent, DialogTitle, DialogTrigger, dialogSizeClasses } from '@brumaombra/ui-vintage/dialog';
2512
+ import type { DialogSize } from '@brumaombra/ui-vintage/dialog';
2441
2513
  ```
2442
2514
 
2443
2515
  ```vue
@@ -2475,6 +2547,7 @@ import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, Di
2475
2547
  | --- | --- | --- |
2476
2548
  | `class` | `HTMLAttributes['class']` | |
2477
2549
  | `showCloseButton` | `boolean` | `false` |
2550
+ | `size` | `DialogSize` | `'sm'` |
2478
2551
 
2479
2552
  - **Also accepts** all props of Reka `DialogContentProps`.
2480
2553
  - **Emits:** all events of Reka `DialogContentEmits`.
@@ -2508,6 +2581,7 @@ import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, Di
2508
2581
  | --- | --- | --- |
2509
2582
  | `class` | `HTMLAttributes['class']` | |
2510
2583
  | `showCloseButton` | `boolean` | `false` |
2584
+ | `size` | `DialogSize` | `'sm'` |
2511
2585
 
2512
2586
  - **Also accepts** all props of Reka `DialogContentProps`.
2513
2587
  - **Emits:** all events of Reka `DialogContentEmits`.
@@ -2524,6 +2598,13 @@ import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, Di
2524
2598
  - **Also accepts** all props of Reka `DialogTriggerProps`.
2525
2599
  - **Slots:** `default`.
2526
2600
 
2601
+ #### Functions, constants, and types
2602
+
2603
+ ```ts
2604
+ type DialogSize = 'sm' | 'md' | 'lg' | 'xl'
2605
+ const dialogSizeClasses: Record<DialogSize, string>
2606
+ ```
2607
+
2527
2608
  ---
2528
2609
 
2529
2610
  ### `@brumaombra/ui-vintage/hover-card`
@@ -3424,6 +3505,35 @@ import { PageHeader } from '@brumaombra/ui-vintage/page-header';
3424
3505
 
3425
3506
  ---
3426
3507
 
3508
+ ### `@brumaombra/ui-vintage/section-title`
3509
+
3510
+ Section heading over a rule that starts with a primary segment, with an optional icon tile, description, and trailing action (the blog sections use it too).
3511
+
3512
+ ```ts
3513
+ import { SectionTitle } from '@brumaombra/ui-vintage/section-title';
3514
+ ```
3515
+
3516
+ ```vue
3517
+ <SectionTitle title="Market FAQ" :icon="HelpCircleIcon" description="Everything about how this market works">
3518
+ <template #action>
3519
+ <Button variant="ghost" size="sm">View all</Button>
3520
+ </template>
3521
+ </SectionTitle>
3522
+ ```
3523
+
3524
+ #### `SectionTitle`
3525
+
3526
+ | Prop | Type | Default |
3527
+ | --- | --- | --- |
3528
+ | `title` *(required)* | `string` | |
3529
+ | `description` | `string` | `''` |
3530
+ | `icon` | `HugeiconsIconDefinition \| null` | `null` |
3531
+ | `headingLevel` | `HeadingLevel` | `2` |
3532
+
3533
+ - **Slots:** `action`.
3534
+
3535
+ ---
3536
+
3427
3537
  ### `@brumaombra/ui-vintage/theme-selector`
3428
3538
 
3429
3539
  Light, dark, and auto theme menu with a circular reveal (View Transitions API). `applyThemeWithTransition` switches themes from your own controls.
@@ -3461,10 +3571,10 @@ Components for Nuxt Content pages and a complete blog.
3461
3571
 
3462
3572
  ### `@brumaombra/ui-vintage/blog`
3463
3573
 
3464
- Complete blog pages and their building blocks. The page components (`BlogIndexPage`, `BlogPostPage`, `BlogCategoriesPage`, `BlogTagsPage`, `BlogPostsPage` for the posts of a category or a tag) render the whole design: the app keeps one thin file per route that queries Nuxt Content, sets the SEO and passes the data and its own copy (titles and descriptions), with the helpers (`slugify`, `normalizeContentPath`, `buildCategoriesFromPosts`, `buildTagsFromPosts`, `sortByStableSeed`) shaping the data. The post page renders the app's `ProseHr` content component as divider and has an `after-article` slot for a call to action. The building blocks: an animated hero header, a post header (breadcrumbs, category and tags, title, author line, and featured image), a full-bleed featured carousel, post and category cards, tags, table of contents, reading progress, FAQ, share sidebar, and the content renderer. Scroll entrances use AOS: install `aos` in the app and initialize it (the components carry `data-aos="blur-up"`, and the library stylesheet defines that animation). Without AOS the content simply renders without entrances.
3574
+ Complete blog pages and their building blocks. The page components (`BlogIndexPage`, `BlogPostPage`, `BlogCategoriesPage`, `BlogTagsPage`, `BlogPostsPage` for the posts of a category or a tag) render the whole design: the app keeps one thin file per route that queries Nuxt Content, sets the SEO and passes the data and its own copy (titles and descriptions), with the helpers (`slugify`, `normalizeContentPath`, `buildCategoriesFromPosts`, `buildTagsFromPosts`, `sortByStableSeed`) shaping the data. The post page renders the app's `ProseHr` content component as divider and has an `after-article` slot for a call to action. The building blocks: an animated hero header, a post header (breadcrumbs, category and tags, title, author line, and featured image), an "In short" box with the `summary` field of the frontmatter (the post page shows it before the table of contents when the post has one), a full-bleed featured carousel, post and category cards, tags, table of contents, reading progress, FAQ, share sidebar, and the content renderer. Scroll entrances use AOS: install `aos` in the app and initialize it (the components carry `data-aos="blur-up"`, and the library stylesheet defines that animation). Without AOS the content simply renders without entrances.
3465
3575
 
3466
3576
  ```ts
3467
- import { AllPostsSection, BlogContentRenderer, BlogFAQSection, BlogInfoSection, BlogPostHeader, BlogHeaderSection, BlogSectionTitle, BlogTagsSection, CategoriesList, CategoriesSection, CategoryCard, HeaderCarousel, PostCard, PostsList, SocialShareSidebar, TableOfContents, BlogIndexPage, BlogPostPage, BlogCategoriesPage, BlogTagsPage, BlogPostsPage, BlogViewAllLink, slugify, normalizeContentPath, buildCategoriesFromPosts, buildTagsFromPosts, sortByStableSeed } from '@brumaombra/ui-vintage/blog';
3577
+ import { AllPostsSection, BlogContentRenderer, BlogFAQSection, BlogInfoSection, BlogPostHeader, BlogHeaderSection, BlogSectionTitle, BlogSummary, BlogTagsSection, CategoriesList, CategoriesSection, CategoryCard, HeaderCarousel, PostCard, PostsList, SocialShareSidebar, TableOfContents, BlogIndexPage, BlogPostPage, BlogCategoriesPage, BlogTagsPage, BlogPostsPage, BlogViewAllLink, slugify, normalizeContentPath, buildCategoriesFromPosts, buildTagsFromPosts, sortByStableSeed } from '@brumaombra/ui-vintage/blog';
3468
3578
  import type { BlogPostData, BlogCategoryData, BlogTagData } from '@brumaombra/ui-vintage/blog';
3469
3579
  ```
3470
3580
 
@@ -3546,6 +3656,12 @@ import type { BlogPostData, BlogCategoryData, BlogTagData } from '@brumaombra/ui
3546
3656
 
3547
3657
  - **Slots:** `action`.
3548
3658
 
3659
+ #### `BlogSummary`
3660
+
3661
+ | Prop | Type | Default |
3662
+ | --- | --- | --- |
3663
+ | `summary` *(required)* | `string` | |
3664
+
3549
3665
  #### `BlogTagsSection`
3550
3666
 
3551
3667
  | Prop | Type | Default |
@@ -3686,6 +3802,7 @@ interface BlogPostData {
3686
3802
  path?: string;
3687
3803
  title?: string;
3688
3804
  description?: string;
3805
+ summary?: string;
3689
3806
  image?: string;
3690
3807
  categoryText?: string;
3691
3808
  categorySlug?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brumaombra/ui-vintage",
3
- "version": "0.9.6",
3
+ "version": "0.9.8",
4
4
  "description": "Source-published Nuxt UI runtime with shared theme tokens and built-in Nuxt integrations.",
5
5
  "private": false,
6
6
  "author": "Mauro Brambilla",
@@ -40,6 +40,7 @@
40
40
  "./package.json": "./package.json",
41
41
  "./style.css": "./src/styles.css",
42
42
  "./accordion": "./src/components/ui/accordion/index.ts",
43
+ "./action-bar": "./src/components/action-bar/index.ts",
43
44
  "./alert": "./src/components/ui/alert/index.ts",
44
45
  "./alert-dialog": "./src/components/ui/alert-dialog/index.ts",
45
46
  "./animated-number": "./src/components/ui/animated-number/index.ts",
@@ -100,6 +101,7 @@
100
101
  "./progress-component": "./src/components/progress-component/index.ts",
101
102
  "./radio-group": "./src/components/ui/radio-group/index.ts",
102
103
  "./scroll-area": "./src/components/ui/scroll-area/index.ts",
104
+ "./section-title": "./src/components/section-title/index.ts",
103
105
  "./select": "./src/components/ui/select/index.ts",
104
106
  "./separator": "./src/components/ui/separator/index.ts",
105
107
  "./sheet": "./src/components/ui/sheet/index.ts",
@@ -173,4 +175,4 @@
173
175
  "glob": "13.0.6"
174
176
  }
175
177
  }
176
- }
178
+ }
@@ -0,0 +1,38 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue';
3
+ import { cn } from '../../lib/utils';
4
+
5
+ // Props (a row of buttons on desktop; on mobile a full-width bar of equal tiles with the icon above the label, like an app)
6
+ const props = defineProps<{
7
+ class?: HTMLAttributes['class'];
8
+ }>();
9
+ </script>
10
+
11
+ <template>
12
+ <div data-slot="action-bar" :class="cn('action-bar grid w-full auto-cols-fr grid-flow-col gap-2 md:flex md:w-auto md:flex-wrap md:items-center md:gap-3', props.class)">
13
+ <slot />
14
+ </div>
15
+ </template>
16
+
17
+ <style scoped>
18
+ /* Mobile tiles: icon above a short label, filling the bar */
19
+ @media (width < 48rem) {
20
+ .action-bar > :slotted([data-slot="button"]) {
21
+ height: auto;
22
+ width: auto;
23
+ flex-direction: column;
24
+ gap: 0.375rem;
25
+ padding: 0.75rem 0.25rem;
26
+ font-size: 0.6875rem;
27
+ line-height: 1.2;
28
+ white-space: normal;
29
+ text-align: center;
30
+ }
31
+
32
+ /* Same icon size on every tile */
33
+ .action-bar > :slotted([data-slot="button"]) svg {
34
+ width: 1.125rem;
35
+ height: 1.125rem;
36
+ }
37
+ }
38
+ </style>
@@ -0,0 +1,2 @@
1
+ export { default } from './ActionBar.vue';
2
+ export { default as ActionBar } from './ActionBar.vue';
@@ -91,13 +91,6 @@ const props = defineProps<{
91
91
  line-height: 1.85;
92
92
  }
93
93
 
94
- /* Lede: the opening paragraph reads a little larger */
95
- .blog-content :deep(p:not(.not-prose):not(.not-prose *):first-of-type) {
96
- font-size: 17px;
97
- line-height: 1.8;
98
- color: var(--muted-foreground);
99
- }
100
-
101
94
  .blog-content :deep(:is(p, blockquote, li):not(.not-prose):not(.not-prose *) strong) {
102
95
  font-weight: 700;
103
96
  color: var(--foreground);
@@ -153,7 +146,7 @@ const props = defineProps<{
153
146
  padding: 1rem 1.25rem 1rem 1.5rem;
154
147
  border: 1px solid var(--border);
155
148
  border-left: 3px solid var(--primary);
156
- border-radius: var(--radius-sm);
149
+ border-radius: var(--uv-radius);
157
150
  background-color: var(--surface);
158
151
  font-size: 15px;
159
152
  font-style: italic;
@@ -169,7 +162,7 @@ const props = defineProps<{
169
162
  color: var(--foreground);
170
163
  background-color: var(--surface);
171
164
  border: 1px solid var(--border);
172
- border-radius: 0.25rem;
165
+ border-radius: var(--uv-radius);
173
166
  display: inline-block;
174
167
  font-size: 0.75rem;
175
168
  font-weight: 600;
@@ -186,7 +179,7 @@ const props = defineProps<{
186
179
  /* Images */
187
180
  .blog-content :deep(img:not(.not-prose):not(.not-prose *)) {
188
181
  border: 1px solid var(--border);
189
- border-radius: var(--radius-sm);
182
+ border-radius: var(--uv-radius);
190
183
  box-shadow: var(--uv-shadow-sm);
191
184
  }
192
185
 
@@ -215,10 +208,6 @@ const props = defineProps<{
215
208
  line-height: 1.8;
216
209
  }
217
210
 
218
- .blog-content :deep(p:not(.not-prose):not(.not-prose *):first-of-type) {
219
- font-size: 15px;
220
- }
221
-
222
211
  .blog-content :deep(h2:not(.not-prose):not(.not-prose *)) {
223
212
  margin-top: 2.25rem;
224
213
  margin-bottom: 1rem;
@@ -20,7 +20,7 @@ const hasValidFAQs = computed(() => props.faqs && props.faqs.length > 0);
20
20
  </script>
21
21
 
22
22
  <template>
23
- <div v-if="hasValidFAQs" class="mt-12 lg:mt-16">
23
+ <div v-if="hasValidFAQs" class="mt-10 lg:mt-12">
24
24
  <!-- Title and description -->
25
25
  <BlogSectionTitle :title="t('uiVintage.blog.faq.title')" :description="t('uiVintage.blog.faq.description')" />
26
26
 
@@ -75,7 +75,7 @@ const headerStats = computed(() => [
75
75
  </script>
76
76
 
77
77
  <template>
78
- <div class="space-y-14 md:space-y-18">
78
+ <div class="space-y-12 md:space-y-14">
79
79
  <!-- Header section -->
80
80
  <BlogHeaderSection :announcement="headerAnnouncement"
81
81
  :title="props.title"
@@ -34,12 +34,12 @@ const showModifiedDate = computed(() => Boolean(props.dateModified) && formatted
34
34
 
35
35
  <template>
36
36
  <!-- Author and date information -->
37
- <Card data-aos="blur-up" class="gap-0! overflow-hidden p-0!">
37
+ <Card data-aos="blur-up" padding="none">
38
38
  <!-- Author -->
39
- <div :class="['flex gap-3 px-5 py-4', props.authorBio ? 'items-start' : 'items-center']">
40
- <!-- Author image -->
39
+ <div :class="['group/author flex gap-3 px-5 py-4', props.authorBio ? 'items-start' : 'items-center']">
40
+ <!-- Author image (grows softly while the author row is hovered) -->
41
41
  <NuxtLinkLocale :to="props.authorUrl" target="_blank" rel="noopener noreferrer" class="shrink-0 rounded outline-none focus-visible:ring-[3px] focus-visible:ring-ring/45">
42
- <NuxtImg :src="props.authorImageUrl" :alt="props.author" height="44" width="44" format="avif" quality="40" :sizes="{ 480: '44px', 1280: '44px' }" loading="lazy" decoding="async" class="size-11 rounded border border-border object-cover" />
42
+ <NuxtImg :src="props.authorImageUrl" :alt="props.author" height="44" width="44" format="avif" quality="40" :sizes="{ 480: '44px', 1280: '44px' }" loading="lazy" decoding="async" class="size-11 rounded border border-border object-cover transition-[scale] duration-500 ease-spring group-hover/author:scale-110" />
43
43
  </NuxtLinkLocale>
44
44
 
45
45
  <!-- Author name and bio -->
@@ -67,7 +67,7 @@ const hasCategory = computed(() => Boolean(props.categoryText && props.categoryS
67
67
  // Whether the details row has any column to show
68
68
  const hasDetails = computed(() => Boolean(props.author || formattedDate.value || resolvedReadingMinutes.value || hasCategory.value));
69
69
 
70
- const badgeLinkClasses = 'rounded outline-none transition-opacity duration-150 hover:opacity-80 focus-visible:ring-[3px] focus-visible:ring-ring/45';
70
+ const badgeLinkClasses = 'rounded outline-none focus-visible:ring-[3px] focus-visible:ring-ring/45';
71
71
 
72
72
  // Shared classes for the details cells (exposed to the meta slot)
73
73
  const detailCellClasses = 'min-w-0 border-t border-l border-border py-4 pl-4 pr-4';
@@ -153,7 +153,7 @@ const detailLabelClasses = 'text-[11px] font-semibold tracking-wider text-muted-
153
153
  <dt :class="detailLabelClasses">{{ t('uiVintage.blog.category') }}</dt>
154
154
  <dd class="mt-1.5">
155
155
  <NuxtLinkLocale :to="categoryPath" :class="['inline-flex', badgeLinkClasses]">
156
- <Badge color="yellow" :text="props.categoryText" pulse />
156
+ <Badge color="yellow" :text="props.categoryText" pulse interactive />
157
157
  </NuxtLinkLocale>
158
158
  </dd>
159
159
  </div>
@@ -166,15 +166,14 @@ const detailLabelClasses = 'text-[11px] font-semibold tracking-wider text-muted-
166
166
  <!-- Tags -->
167
167
  <div v-if="props.tags.length > 0" :class="['mt-4 flex flex-wrap items-center gap-2', enter('animate-uv-fade-up [animation-delay:600ms]')]">
168
168
  <NuxtLinkLocale v-for="tag in props.tags" :key="tag.slug" :to="tagPath(tag.slug)" :class="badgeLinkClasses">
169
- <Badge :text="`#${tag.name}`" />
169
+ <Badge :text="`#${tag.name}`" interactive />
170
170
  </NuxtLinkLocale>
171
171
  </div>
172
172
 
173
173
  <!-- Featured image (replaceable through the media slot) -->
174
174
  <slot name="media">
175
- <div v-if="props.image" :class="['relative mt-8 aspect-video w-full overflow-hidden rounded border border-border shadow-elevated-xl', enter('animate-uv-fade-up [animation-delay:650ms]')]">
175
+ <div v-if="props.image" :class="['relative mt-8 aspect-video w-full overflow-hidden rounded border border-border shadow-elevated-lg', enter('animate-uv-fade-up [animation-delay:650ms]')]">
176
176
  <NuxtImg :src="props.image" :alt="props.title" format="avif" quality="50" :sizes="{ 480: '480px', 1536: '896px' }" loading="eager" fetchpriority="high" preload :class="['size-full object-cover', enter('animate-uv-ken-burns')]" />
177
- <div aria-hidden="true" class="pointer-events-none absolute inset-0 bg-linear-to-t from-black/20 to-transparent" />
178
177
  </div>
179
178
  </slot>
180
179
  </header>
@@ -8,6 +8,7 @@ import BlogFAQSection from './BlogFAQSection.vue';
8
8
  import BlogInfoSection from './BlogInfoSection.vue';
9
9
  import BlogPostHeader from './BlogPostHeader.vue';
10
10
  import BlogSectionTitle from './BlogSectionTitle.vue';
11
+ import BlogSummary from './BlogSummary.vue';
11
12
  import BlogViewAllLink from './BlogViewAllLink.vue';
12
13
  import CategoryCard from './CategoryCard.vue';
13
14
  import PostCard from './PostCard.vue';
@@ -54,6 +55,9 @@ const postTags = computed(() => ((props.post.tags as string[] | undefined) || []
54
55
  :tags="postTags"
55
56
  :body="props.post.body" />
56
57
 
58
+ <!-- Short answer of the post (the summary field of the frontmatter) -->
59
+ <BlogSummary v-if="props.post.summary" :summary="props.post.summary" />
60
+
57
61
  <!-- Table of contents -->
58
62
  <TableOfContents :content="props.post" />
59
63
 
@@ -84,7 +88,7 @@ const postTags = computed(() => ((props.post.tags as string[] | undefined) || []
84
88
  </article>
85
89
 
86
90
  <!-- Related posts -->
87
- <section v-if="props.relatedPosts.length" class="mt-16 md:mt-24">
91
+ <section v-if="props.relatedPosts.length" class="mt-12 md:mt-16">
88
92
  <BlogSectionTitle :title="t('uiVintage.blog.readAlso')" />
89
93
  <div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
90
94
  <NuxtLinkLocale v-for="(relatedPost, index) in props.relatedPosts" :key="relatedPost.path" :to="relatedPost.path" data-aos="blur-up" :data-aos-delay="(index % 2) * 90" class="block h-full rounded outline-none focus-visible:ring-[3px] focus-visible:ring-ring/45">
@@ -98,7 +102,7 @@ const postTags = computed(() => ((props.post.tags as string[] | undefined) || []
98
102
  </section>
99
103
 
100
104
  <!-- Related categories -->
101
- <section v-if="props.relatedCategories.length" class="mt-16 md:mt-24">
105
+ <section v-if="props.relatedCategories.length" class="mt-12 md:mt-16">
102
106
  <BlogSectionTitle :title="t('uiVintage.blog.exploreCategories')">
103
107
  <template #action>
104
108
  <BlogViewAllLink to="/blog/categories" :label="t('uiVintage.blog.exploreAllCategories')" />
@@ -1,5 +1,7 @@
1
1
  <script setup lang="ts">
2
- // Props
2
+ import { SectionTitle } from '../section-title';
3
+
4
+ // Props (the blog sections use the shared section title, revealed on scroll)
3
5
  const props = withDefaults(defineProps<{
4
6
  title: string;
5
7
  description?: string;
@@ -9,25 +11,9 @@ const props = withDefaults(defineProps<{
9
11
  </script>
10
12
 
11
13
  <template>
12
- <div data-aos="blur-up" class="relative mb-6 flex flex-col gap-3 border-b border-border pb-4 sm:flex-row sm:items-end sm:justify-between md:mb-8">
13
- <!-- Primary segment at the start of the rule -->
14
- <span aria-hidden="true" class="absolute -bottom-px left-0 h-0.5 w-12 bg-primary" />
15
-
16
- <div class="min-w-0">
17
- <!-- Title -->
18
- <h2 class="text-xl font-semibold tracking-tight text-foreground md:text-2xl">
19
- {{ props.title }}
20
- </h2>
21
-
22
- <!-- Description -->
23
- <p v-if="props.description" class="mt-1 max-w-2xl text-xs leading-6 text-muted-foreground md:text-sm">
24
- {{ props.description }}
25
- </p>
26
- </div>
27
-
28
- <!-- Optional trailing action (for example a "View all" link) -->
29
- <div v-if="$slots.action" class="shrink-0">
14
+ <SectionTitle data-aos="blur-up" :title="props.title" :description="props.description">
15
+ <template v-if="$slots.action" #action>
30
16
  <slot name="action" />
31
- </div>
32
- </div>
17
+ </template>
18
+ </SectionTitle>
33
19
  </template>