@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.
- package/AGENTS.md +3 -1
- package/COMPONENTS.md +132 -15
- package/package.json +4 -2
- package/src/components/action-bar/ActionBar.vue +38 -0
- package/src/components/action-bar/index.ts +2 -0
- package/src/components/blog/BlogContentRenderer.vue +3 -14
- package/src/components/blog/BlogFAQSection.vue +1 -1
- package/src/components/blog/BlogIndexPage.vue +1 -1
- package/src/components/blog/BlogInfoSection.vue +4 -4
- package/src/components/blog/BlogPostHeader.vue +4 -5
- package/src/components/blog/BlogPostPage.vue +6 -2
- package/src/components/blog/BlogSectionTitle.vue +7 -21
- package/src/components/blog/BlogSummary.vue +31 -0
- package/src/components/blog/BlogTagsSection.vue +1 -1
- package/src/components/blog/CategoryCard.vue +1 -1
- package/src/components/blog/PostCard.vue +1 -1
- package/src/components/blog/blogPageUtils.ts +1 -0
- package/src/components/blog/index.ts +1 -0
- package/src/components/chip/Chip.vue +1 -1
- package/src/components/dashboard-shell/DashboardShell.vue +3 -3
- package/src/components/data-list/DataListText.vue +7 -4
- package/src/components/language-flag/flags/ChineseFlag.vue +1 -1
- package/src/components/language-flag/flags/EnglishFlag.vue +1 -1
- package/src/components/language-flag/flags/FrenchFlag.vue +1 -1
- package/src/components/language-flag/flags/GermanFlag.vue +1 -1
- package/src/components/language-flag/flags/ItalianFlag.vue +1 -1
- package/src/components/language-flag/flags/JapaneseFlag.vue +1 -1
- package/src/components/language-flag/flags/PortugueseFlag.vue +1 -1
- package/src/components/language-flag/flags/RussianFlag.vue +1 -1
- package/src/components/language-flag/flags/SpanishFlag.vue +1 -1
- package/src/components/language-selector/LanguageSelector.vue +2 -2
- package/src/components/section-title/SectionTitle.vue +47 -0
- package/src/components/section-title/index.ts +2 -0
- package/src/components/single-value-card/SingleValueCard.vue +1 -1
- package/src/components/ui/alert-dialog/AlertDialogAction.vue +6 -6
- package/src/components/ui/avatar/Avatar.vue +4 -2
- package/src/components/ui/avatar/AvatarFallback.vue +25 -5
- package/src/components/ui/badge/Badge.vue +21 -4
- package/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue +1 -1
- package/src/components/ui/breadcrumb/BreadcrumbLink.vue +1 -1
- package/src/components/ui/button/Button.vue +18 -2
- package/src/components/ui/button/index.ts +4 -0
- package/src/components/ui/card/Card.vue +25 -1
- package/src/components/ui/card/CardMedia.vue +15 -0
- package/src/components/ui/card/CardTitle.vue +8 -3
- package/src/components/ui/card/index.ts +1 -0
- package/src/components/ui/checkbox/Checkbox.vue +1 -1
- package/src/components/ui/combobox/ComboboxSelect.vue +2 -2
- package/src/components/ui/combobox/ComboboxTrigger.vue +1 -1
- package/src/components/ui/data-table/DataTable.vue +1 -1
- package/src/components/ui/dialog/DialogContent.vue +7 -3
- package/src/components/ui/dialog/DialogScrollContent.vue +7 -3
- package/src/components/ui/dialog/index.ts +11 -1
- package/src/components/ui/file-dropzone/FileDropzone.vue +1 -1
- package/src/components/ui/input/Input.vue +42 -9
- package/src/components/ui/kbd/Kbd.vue +8 -4
- package/src/components/ui/number-field/NumberField.vue +2 -2
- package/src/components/ui/progress/Progress.vue +17 -5
- package/src/components/ui/select/SelectItem.vue +1 -1
- package/src/components/ui/select/SelectItemContent.vue +5 -3
- package/src/components/ui/skeleton/Skeleton.vue +7 -3
- package/src/components/ui/switch/Switch.vue +1 -1
- package/src/components/ui/table/TableHeader.vue +1 -1
- package/src/components/ui/table/TableRow.vue +11 -4
- package/src/components/ui/tabs/Tabs.vue +1 -1
- package/src/components/ui/tabs/TabsContent.vue +8 -5
- 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 +1 -1
- package/src/components/ui/toggle-group/ToggleGroup.vue +2 -2
- package/src/components/ui/toggle-group/ToggleGroupItem.vue +3 -1
- package/src/components/ui/toggle-group/index.ts +2 -2
- package/src/i18n/de.json +1 -0
- package/src/i18n/en.json +1 -0
- package/src/i18n/es.json +1 -0
- package/src/i18n/fr.json +1 -0
- package/src/i18n/it.json +1 -0
- package/src/i18n/ja.json +1 -0
- package/src/i18n/pt.json +1 -0
- package/src/i18n/ru.json +1 -0
- package/src/i18n/zh.json +1 -0
- package/src/lib/color-tokens.ts +6 -6
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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>
|
|
@@ -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
|
|
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:
|
|
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
|
|
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-
|
|
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
|
|
|
@@ -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"
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
<
|
|
13
|
-
|
|
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
|
-
</
|
|
32
|
-
</
|
|
17
|
+
</template>
|
|
18
|
+
</SectionTitle>
|
|
33
19
|
</template>
|