@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
package/src/i18n/ja.json CHANGED
@@ -3,6 +3,7 @@
3
3
  "common": {
4
4
  "breadcrumb": "パンくずリスト",
5
5
  "more": "もっと見る",
6
+ "home": "ホーム",
6
7
  "empty": {
7
8
  "title": "まだ何もありません",
8
9
  "description": "現在表示できるコンテンツはありません。"
@@ -10,7 +11,8 @@
10
11
  "loading": {
11
12
  "title": "読み込み中...",
12
13
  "description": "コンテンツを読み込んでいます。しばらくお待ちください。"
13
- }
14
+ },
15
+ "logo": "{name} のロゴ"
14
16
  },
15
17
  "buttons": {
16
18
  "remove": "取り除く",
@@ -50,7 +52,10 @@
50
52
  "description": "実行するコマンドを検索..."
51
53
  },
52
54
  "datePicker": {
53
- "placeholder": "日付を選択"
55
+ "placeholder": "日付を選択",
56
+ "month": "月",
57
+ "year": "年",
58
+ "time": "時刻"
54
59
  },
55
60
  "terminal": {
56
61
  "title": "ターミナル",
@@ -60,7 +65,9 @@
60
65
  "code": {
61
66
  "title": "コード",
62
67
  "copy": "コードをコピー",
63
- "copied": "コードをコピーしました"
68
+ "copied": "コードをコピーしました",
69
+ "position": "{line} 行、{column} 列",
70
+ "plainText": "プレーンテキスト"
64
71
  },
65
72
  "footer": {
66
73
  "madeWith": "愛を込めて",
@@ -71,14 +78,20 @@
71
78
  "readingProgress": "読書の進捗",
72
79
  "readMore": "続きを読む",
73
80
  "goToPost": "記事を開く",
81
+ "new": "新着",
74
82
  "tableOfContents": "目次",
75
83
  "tableOfContentsDescription": "この記事のセクションに移動",
84
+ "tableOfContentsSections": "{count} セクション",
76
85
  "openTableOfContents": "目次を開く",
77
86
  "closeTableOfContents": "目次を閉じる",
78
87
  "loadMoreWithNumbers": "さらに読み込む ({current}/{total})",
79
88
  "author": "著者",
80
89
  "publishedDate": "公開日",
81
90
  "lastUpdatedDate": "更新日",
91
+ "readingTime": "読了時間",
92
+ "readingTimeValue": "{minutes}分",
93
+ "category": "カテゴリー",
94
+ "breadcrumbBlog": "ブログ",
82
95
  "carouselNavigation": "カルーセルナビゲーション",
83
96
  "goToSlide": "スライド {index} へ移動",
84
97
  "previousSlide": "前のスライド",
@@ -97,9 +110,6 @@
97
110
  "copied": "リンクをコピーしました"
98
111
  }
99
112
  },
100
- "select": {
101
- "defaultPlaceholder": "オプションを選択してください"
102
- },
103
113
  "errorPage": {
104
114
  "notFound": {
105
115
  "title": "ページが見つかりません",
package/src/i18n/pt.json CHANGED
@@ -3,6 +3,7 @@
3
3
  "common": {
4
4
  "breadcrumb": "Navegação estrutural",
5
5
  "more": "Mais",
6
+ "home": "Início",
6
7
  "empty": {
7
8
  "title": "Ainda nao ha nada aqui",
8
9
  "description": "Nao ha conteudo para mostrar neste momento."
@@ -10,7 +11,8 @@
10
11
  "loading": {
11
12
  "title": "Carregando...",
12
13
  "description": "Aguarde um momento enquanto carregamos o conteudo."
13
- }
14
+ },
15
+ "logo": "Logótipo de {name}"
14
16
  },
15
17
  "buttons": {
16
18
  "remove": "Remover",
@@ -50,7 +52,10 @@
50
52
  "description": "Pesquise um comando para executar..."
51
53
  },
52
54
  "datePicker": {
53
- "placeholder": "Selecione uma data"
55
+ "placeholder": "Selecione uma data",
56
+ "month": "Mês",
57
+ "year": "Ano",
58
+ "time": "Hora"
54
59
  },
55
60
  "terminal": {
56
61
  "title": "Terminal",
@@ -60,7 +65,9 @@
60
65
  "code": {
61
66
  "title": "Codigo",
62
67
  "copy": "Copiar codigo",
63
- "copied": "Codigo copiado"
68
+ "copied": "Codigo copiado",
69
+ "position": "Ln {line}, Col {column}",
70
+ "plainText": "Texto simples"
64
71
  },
65
72
  "footer": {
66
73
  "madeWith": "Feito com",
@@ -71,14 +78,20 @@
71
78
  "readingProgress": "Progresso da leitura",
72
79
  "readMore": "Ler mais",
73
80
  "goToPost": "Ir para o artigo",
81
+ "new": "Novo",
74
82
  "tableOfContents": "Indice",
75
83
  "tableOfContentsDescription": "Ir para uma secao deste artigo",
84
+ "tableOfContentsSections": "{count} secao | {count} secoes",
76
85
  "openTableOfContents": "Abrir indice",
77
86
  "closeTableOfContents": "Fechar indice",
78
87
  "loadMoreWithNumbers": "Carregar mais ({current}/{total})",
79
88
  "author": "Autor",
80
89
  "publishedDate": "Publicado",
81
90
  "lastUpdatedDate": "Atualizado",
91
+ "readingTime": "Tempo de leitura",
92
+ "readingTimeValue": "{minutes} min",
93
+ "category": "Categoria",
94
+ "breadcrumbBlog": "Blog",
82
95
  "carouselNavigation": "Navegacao do carrossel",
83
96
  "goToSlide": "Ir para o slide {index}",
84
97
  "previousSlide": "Slide anterior",
@@ -97,9 +110,6 @@
97
110
  "copied": "Link copiado"
98
111
  }
99
112
  },
100
- "select": {
101
- "defaultPlaceholder": "Selecione uma opcao"
102
- },
103
113
  "errorPage": {
104
114
  "notFound": {
105
115
  "title": "Página não encontrada",
package/src/i18n/ru.json CHANGED
@@ -3,6 +3,7 @@
3
3
  "common": {
4
4
  "breadcrumb": "Навигация по хлебным крошкам",
5
5
  "more": "Ещё",
6
+ "home": "Главная",
6
7
  "empty": {
7
8
  "title": "Здесь пока ничего нет",
8
9
  "description": "Сейчас нет контента для отображения."
@@ -10,7 +11,8 @@
10
11
  "loading": {
11
12
  "title": "Загрузка...",
12
13
  "description": "Подождите немного, пока мы загружаем содержимое."
13
- }
14
+ },
15
+ "logo": "Логотип {name}"
14
16
  },
15
17
  "buttons": {
16
18
  "remove": "Убрать",
@@ -50,7 +52,10 @@
50
52
  "description": "Найти команду для выполнения..."
51
53
  },
52
54
  "datePicker": {
53
- "placeholder": "Выберите дату"
55
+ "placeholder": "Выберите дату",
56
+ "month": "Месяц",
57
+ "year": "Год",
58
+ "time": "Время"
54
59
  },
55
60
  "terminal": {
56
61
  "title": "Терминал",
@@ -60,7 +65,9 @@
60
65
  "code": {
61
66
  "title": "Код",
62
67
  "copy": "Копировать код",
63
- "copied": "Код скопирован"
68
+ "copied": "Код скопирован",
69
+ "position": "Стр. {line}, стлб. {column}",
70
+ "plainText": "Обычный текст"
64
71
  },
65
72
  "footer": {
66
73
  "madeWith": "Сделано с",
@@ -71,14 +78,20 @@
71
78
  "readingProgress": "Прогресс чтения",
72
79
  "readMore": "Читать далее",
73
80
  "goToPost": "Перейти к статье",
81
+ "new": "Новое",
74
82
  "tableOfContents": "Содержание",
75
83
  "tableOfContentsDescription": "Перейти к разделу статьи",
84
+ "tableOfContentsSections": "Разделов: {count}",
76
85
  "openTableOfContents": "Открыть содержание",
77
86
  "closeTableOfContents": "Закрыть содержание",
78
87
  "loadMoreWithNumbers": "Загрузить еще ({current}/{total})",
79
88
  "author": "Автор",
80
89
  "publishedDate": "Опубликовано",
81
90
  "lastUpdatedDate": "Обновлено",
91
+ "readingTime": "Время чтения",
92
+ "readingTimeValue": "{minutes} мин",
93
+ "category": "Категория",
94
+ "breadcrumbBlog": "Блог",
82
95
  "carouselNavigation": "Навигация карусели",
83
96
  "goToSlide": "Перейти к слайду {index}",
84
97
  "previousSlide": "Предыдущий слайд",
@@ -97,9 +110,6 @@
97
110
  "copied": "Ссылка скопирована"
98
111
  }
99
112
  },
100
- "select": {
101
- "defaultPlaceholder": "Выберите вариант"
102
- },
103
113
  "errorPage": {
104
114
  "notFound": {
105
115
  "title": "Страница не найдена",
package/src/i18n/zh.json CHANGED
@@ -3,6 +3,7 @@
3
3
  "common": {
4
4
  "breadcrumb": "面包屑导航",
5
5
  "more": "更多",
6
+ "home": "首页",
6
7
  "empty": {
7
8
  "title": "这里还没有内容",
8
9
  "description": "当前没有可显示的内容。"
@@ -10,7 +11,8 @@
10
11
  "loading": {
11
12
  "title": "加载中...",
12
13
  "description": "请稍候,正在加载内容。"
13
- }
14
+ },
15
+ "logo": "{name} 标志"
14
16
  },
15
17
  "buttons": {
16
18
  "remove": "移除",
@@ -50,7 +52,10 @@
50
52
  "description": "搜索要运行的命令..."
51
53
  },
52
54
  "datePicker": {
53
- "placeholder": "选择日期"
55
+ "placeholder": "选择日期",
56
+ "month": "月份",
57
+ "year": "年份",
58
+ "time": "时间"
54
59
  },
55
60
  "terminal": {
56
61
  "title": "终端",
@@ -60,7 +65,9 @@
60
65
  "code": {
61
66
  "title": "代码",
62
67
  "copy": "复制代码",
63
- "copied": "代码已复制"
68
+ "copied": "代码已复制",
69
+ "position": "第 {line} 行,第 {column} 列",
70
+ "plainText": "纯文本"
64
71
  },
65
72
  "footer": {
66
73
  "madeWith": "用",
@@ -71,14 +78,20 @@
71
78
  "readingProgress": "阅读进度",
72
79
  "readMore": "阅读更多",
73
80
  "goToPost": "前往文章",
81
+ "new": "最新",
74
82
  "tableOfContents": "目录",
75
83
  "tableOfContentsDescription": "跳转到本文的一个部分",
84
+ "tableOfContentsSections": "{count} 个章节",
76
85
  "openTableOfContents": "打开目录",
77
86
  "closeTableOfContents": "关闭目录",
78
87
  "loadMoreWithNumbers": "加载更多 ({current}/{total})",
79
88
  "author": "作者",
80
89
  "publishedDate": "发布时间",
81
90
  "lastUpdatedDate": "更新时间",
91
+ "readingTime": "阅读时间",
92
+ "readingTimeValue": "{minutes} 分钟",
93
+ "category": "分类",
94
+ "breadcrumbBlog": "博客",
82
95
  "carouselNavigation": "轮播导航",
83
96
  "goToSlide": "转到第 {index} 张幻灯片",
84
97
  "previousSlide": "上一张幻灯片",
@@ -97,9 +110,6 @@
97
110
  "copied": "链接已复制"
98
111
  }
99
112
  },
100
- "select": {
101
- "defaultPlaceholder": "请选择一个选项"
102
- },
103
113
  "errorPage": {
104
114
  "notFound": {
105
115
  "title": "找不到页面",
@@ -32,7 +32,7 @@ export const getInteractiveSurfaceToneClasses = (color: ToneColor) => {
32
32
  // Return the list of classes for button variants
33
33
  export const buttonVariantClasses: Record<ButtonVariantName, string> = {
34
34
  // Custom button variants
35
- primary: "bg-primary text-primary-foreground shadow-[inset_0_1px_0_rgb(255_255_255/0.28),0_1px_2px_rgb(0_0_0/0.08)] hover:bg-primary-hover hover:shadow-glow",
35
+ primary: "bg-primary text-primary-foreground shadow-elevated-sm hover:bg-primary-hover before:pointer-events-none before:absolute before:inset-0 before:-z-10 before:-translate-x-full before:bg-[linear-gradient(105deg,transparent_35%,rgb(255_255_255/0.38)_50%,transparent_65%)] before:transition-[translate] before:duration-700 before:ease-out-expo hover:before:translate-x-full motion-reduce:before:hidden",
36
36
  secondary: "border border-border bg-secondary text-foreground shadow-elevated-sm hover:border-border-strong hover:bg-accent",
37
37
  ghost: "text-muted-foreground hover:bg-accent hover:text-foreground",
38
38
  link: "text-primary underline-offset-4 decoration-2 hover:underline enabled:active:scale-100",
package/src/lib/i18n.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { useI18n } from 'vue-i18n';
1
2
  import en from '../i18n/en.json';
2
3
  import it from '../i18n/it.json';
3
4
  import fr from '../i18n/fr.json';
@@ -24,12 +25,23 @@ export const uiVintageMessages = {
24
25
  // The type of the available locales based on the keys of the messages object
25
26
  type UiVintageLocale = keyof typeof uiVintageMessages;
26
27
 
27
- // Resolve the current runtime locale from the document or navigator
28
+ // Browser-only source of the app locale (set by the Nuxt plugin from the vue-i18n instance)
29
+ let runtimeLocaleSource: (() => string | undefined) | null = null;
30
+
31
+ // Let the app tell the imperatively mounted components which locale it uses
32
+ export const setUiVintageRuntimeLocaleSource = (source: (() => string | undefined) | null) => {
33
+ runtimeLocaleSource = source;
34
+ };
35
+
36
+ // Resolve the current runtime locale from the app, the document or the navigator
28
37
  const getUiVintageRuntimeLocale = (): UiVintageLocale => {
29
38
  let browserLocale = 'en';
30
39
 
31
- // Try to detect the user's locale from the document or navigator
32
- if (typeof document !== 'undefined' && document.documentElement?.lang) {
40
+ // Prefer the app locale, then try to detect the user's locale from the document or navigator
41
+ const appLocale = runtimeLocaleSource?.();
42
+ if (appLocale) {
43
+ browserLocale = appLocale.toLowerCase();
44
+ } else if (typeof document !== 'undefined' && document.documentElement?.lang) {
33
45
  browserLocale = document.documentElement.lang?.toLowerCase();
34
46
  } else if (typeof navigator !== 'undefined' && navigator.language) {
35
47
  browserLocale = navigator.language?.toLowerCase();
@@ -77,4 +89,20 @@ export const getUiVintageRuntimeMessage = (path: string, fallback: string) => {
77
89
 
78
90
  // If the final value is a string, return it; otherwise, return the fallback
79
91
  return typeof currentValue === 'string' ? currentValue : fallback;
80
- };
92
+ };
93
+
94
+ // Translate library messages inside components: use the app's vue-i18n when available (same locale on server and client),
95
+ // and fall back to the bundled messages in trees mounted outside the app (toasts, dialogs, busy indicator)
96
+ export const useUiVintageMessage = () => {
97
+ let translate: ((key: string) => string) | null = null;
98
+ try {
99
+ translate = useI18n({ useScope: 'global' }).t;
100
+ } catch {
101
+ translate = null;
102
+ }
103
+
104
+ return (path: string, fallback: string) => {
105
+ const message = translate?.(path);
106
+ return message && message !== path ? message : getUiVintageRuntimeMessage(path, fallback);
107
+ };
108
+ };
package/src/lib/utils.ts CHANGED
@@ -26,4 +26,24 @@ export function formatDateLongItalyTimezone(date: string | number | Date | null
26
26
  day: 'numeric',
27
27
  timeZone: 'Europe/Rome'
28
28
  }).format(parsedDate);
29
- };
29
+ };
30
+ // Estimate the reading time in minutes from any text or content tree (about 220 words per minute)
31
+ export function getReadingMinutes(content: unknown, wordsPerMinute = 220) {
32
+ // Follow only the branches that hold readable text: tag names, props and the table of contents are skipped
33
+ const collectText = (node: unknown): string => {
34
+ if (typeof node === 'string') return node;
35
+ if (Array.isArray(node)) {
36
+ // Minimark elements are [tag, props, ...children]
37
+ const isElement = typeof node[0] === 'string' && !!node[1] && typeof node[1] === 'object' && !Array.isArray(node[1]);
38
+ return (isElement ? node.slice(2) : node).map(collectText).join(' ');
39
+ }
40
+ if (node && typeof node === 'object') {
41
+ // Documents keep the text in body, minimark bodies in value, AST nodes in children (or value for text nodes)
42
+ const record = node as Record<string, unknown>;
43
+ return ['body', 'value', 'children'].map(key => collectText(record[key])).join(' ');
44
+ }
45
+ return '';
46
+ };
47
+ const words = collectText(content).split(/\s+/).filter(Boolean).length;
48
+ return Math.max(1, Math.round(words / wordsPerMinute));
49
+ };
@@ -1,9 +1,11 @@
1
1
  import { defineNuxtPlugin } from 'nuxt/app';
2
2
  import type { NuxtApp } from 'nuxt/app';
3
- import { uiVintageMessages } from '../lib/i18n';
3
+ import { unref } from 'vue';
4
+ import { setUiVintageRuntimeLocaleSource, uiVintageMessages } from '../lib/i18n';
4
5
 
5
6
  type MessageComposer = {
6
7
  mergeLocaleMessage: (locale: string, messages: unknown) => void;
8
+ locale?: unknown;
7
9
  };
8
10
 
9
11
  type I18nInstance = {
@@ -56,6 +58,14 @@ const applyUiVintageMessages = (nuxtApp: NuxtApp): void => {
56
58
  for (const [locale, messages] of Object.entries(uiVintageMessages)) {
57
59
  composer.mergeLocaleMessage(locale, messages);
58
60
  }
61
+
62
+ // In the browser, the toasts and dialogs mounted outside the app follow the app locale (never on the server, where state is shared across requests)
63
+ if (import.meta.client) {
64
+ setUiVintageRuntimeLocaleSource(() => {
65
+ const locale = unref(composer.locale);
66
+ return typeof locale === 'string' ? locale : undefined;
67
+ });
68
+ }
59
69
  };
60
70
 
61
71
  // Define the Nuxt plugin
package/src/styles.css CHANGED
@@ -11,6 +11,8 @@ html {
11
11
  }
12
12
 
13
13
  :root {
14
+ color-scheme: light;
15
+
14
16
  /*********************** Core custom colors ***********************/
15
17
 
16
18
  --bg-main-light: #fafafa;
@@ -43,7 +45,6 @@ html {
43
45
 
44
46
  /*********************** Motion tokens ***********************/
45
47
 
46
- --uv-duration-instant: 90ms;
47
48
  --uv-duration-fast: 150ms;
48
49
  --uv-duration-base: 220ms;
49
50
  --uv-duration-slow: 420ms;
@@ -166,10 +167,10 @@ html {
166
167
  --animate-uv-fade-up: uv-fade-up 0.5s var(--ease-out-expo) both;
167
168
  --animate-uv-ping-soft: uv-ping-soft 1.8s var(--ease-out-expo) infinite;
168
169
  --animate-uv-indeterminate: uv-indeterminate 1.4s var(--ease-snappy) infinite;
169
- --animate-uv-gradient: uv-gradient 6s ease infinite;
170
170
  --animate-uv-word-in: uv-word-in 0.9s var(--ease-spring) both;
171
171
  --animate-uv-grow-x: uv-grow-x 0.9s var(--ease-out-expo) both;
172
172
  --animate-uv-ken-burns: uv-ken-burns 9s ease-out both;
173
+ --animate-uv-draw: uv-draw 1s var(--ease-out-expo) both;
173
174
 
174
175
  @keyframes uv-word-in {
175
176
  0% { opacity: 0; transform: translateY(0.45em); filter: blur(8px); }
@@ -181,6 +182,12 @@ html {
181
182
  100% { transform: scaleX(1); }
182
183
  }
183
184
 
185
+ /* Draws an SVG stroke that uses pathLength="1" and stroke-dasharray: 1 */
186
+ @keyframes uv-draw {
187
+ 0% { stroke-dashoffset: 1; }
188
+ 100% { stroke-dashoffset: 0; }
189
+ }
190
+
184
191
  @keyframes uv-ken-burns {
185
192
  0% { transform: scale(1.12); }
186
193
  100% { transform: scale(1); }
@@ -224,11 +231,6 @@ html {
224
231
  50% { transform: translateX(40%) scaleX(0.6); }
225
232
  100% { transform: translateX(120%) scaleX(0.4); }
226
233
  }
227
-
228
- @keyframes uv-gradient {
229
- 0%, 100% { background-position: 0% 50%; }
230
- 50% { background-position: 100% 50%; }
231
- }
232
234
  }
233
235
 
234
236
  /* Floating surfaces (popover, select, dropdown, tooltip, hover card) scale from their anchor */
@@ -335,15 +337,6 @@ html {
335
337
  }
336
338
  }
337
339
 
338
- /* Shared focus ring used by interactive primitives */
339
- @utility uv-focus-ring {
340
- outline: none;
341
-
342
- &:focus-visible {
343
- box-shadow: 0 0 0 1px var(--background), 0 0 0 3px color-mix(in oklab, var(--ring) 55%, transparent);
344
- }
345
- }
346
-
347
340
  /* Shared interactive states for inputs, textareas, and field-like triggers (base colors stay in utility classes) */
348
341
  @utility uv-field {
349
342
  transition-property: border-color, box-shadow, background-color, color, opacity;
@@ -369,6 +362,9 @@ html {
369
362
  }
370
363
 
371
364
  .dark {
365
+ /* Native controls (date/time pickers, scrollbars, form widgets) follow the theme */
366
+ color-scheme: dark;
367
+
372
368
  --background: var(--bg-main-dark);
373
369
  --foreground: var(--text-primary-dark);
374
370
  --card: var(--bg-card-dark);
@@ -1,42 +0,0 @@
1
- ---
2
- name: ui-vintage
3
- description: Build Nuxt UI with the @brumaombra/ui-vintage component library. Use whenever creating or changing pages, layouts, forms, tables, dialogs, toasts, or any other UI in an app that depends on @brumaombra/ui-vintage, before writing custom components or styles.
4
- ---
5
-
6
- # UI Vintage
7
-
8
- This app uses `@brumaombra/ui-vintage`, a Nuxt component library with its own design tokens, motion system, and runtime flows. Reuse it instead of building UI from scratch.
9
-
10
- ## 1. Read the catalog first
11
-
12
- The complete, version-matched API reference is installed with the package:
13
-
14
- `node_modules/@brumaombra/ui-vintage/COMPONENTS.md`
15
-
16
- Search it (for example for `### \`@brumaombra/ui-vintage/`, a component name, or a prop) instead of reading it top to bottom. It lists every entry point with its import line, components, props and defaults, events, slots, helpers, and usage examples. Never guess prop names: check them there.
17
-
18
- ## 2. Choose the component
19
-
20
- - Before writing a custom component, check the catalog's Contents list. Common mappings:
21
- - Searchable select → `combobox` (`ComboboxSelect`); plain select → `select`.
22
- - Table with sorting or selection → `data-table`; KPI tiles → `single-value-card`.
23
- - Confirmation → `showConfirmDialog` (`confirm-dialog`); notification → `showMessageToast` (`message-toast`); blocking load → `setBusy` (`busy-indicator`). These mount themselves, so do not add them to the template.
24
- - App layout with sidebar → `dashboard-shell`; marketing or blog layout → `landing`.
25
- - Empty and loading placeholders → `empty-state-card`, `loading-state-card`, `skeleton`.
26
- - Only build something new when no entry point fits, and compose it from library primitives (`card`, `button`, `field`, ...).
27
-
28
- ## 3. Follow the library conventions
29
-
30
- - Import from explicit subpaths: `import { Button } from '@brumaombra/ui-vintage/button'`. Never import from `@brumaombra/ui-vintage/src/...`.
31
- - Style with the semantic tokens (`bg-card`, `bg-surface`, `bg-accent`, `bg-primary`, `text-muted-foreground`, `border-border`, `border-border-strong`, ...). They adapt to dark mode, so do not write `dark:` color variants or hard-coded colors for surfaces and text.
32
- - Customize components through their props and the `class` prop (merged with tailwind-merge) rather than wrapping them in extra styled elements.
33
- - For motion, reuse the library utilities (`ease-spring`, `ease-out-expo`, `animate-uv-fade-up`, `animate-uv-pop`, `uv-floating-motion`, `shadow-elevated-*`). In custom transitions, name `scale`, `rotate`, and `translate` explicitly; Tailwind v4 does not animate them through `transform`.
34
- - Icons are Hugeicons definitions: import from `@hugeicons/core-free-icons` and pass them to `:icon` props, or render them with `HugeiconsIcon` from `@hugeicons/vue`.
35
- - Form controls use `v-model`. Mark invalid fields with `aria-invalid` and show messages with `FieldError`.
36
- - Library strings are translated through vue-i18n (`uiVintage.*` keys). App copy belongs in the app's own locale files.
37
-
38
- ## 4. Check your work
39
-
40
- - Every import path you used appears as an entry point in `COMPONENTS.md`.
41
- - Every prop and event you used appears in that component's table.
42
- - No new CSS reimplements something a component or token already provides.