@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
@@ -1,21 +1,26 @@
1
1
  <script setup lang="ts">
2
2
  import type { HTMLAttributes } from 'vue';
3
+ import { HugeiconsIcon } from '@hugeicons/vue';
3
4
  import { cn } from '../../../lib/utils';
4
- import type { HeadingLevel } from '../../../lib/common-types';
5
+ import type { HeadingLevel, HugeiconsIconDefinition } from '../../../lib/common-types';
5
6
 
6
7
  // Props
7
8
  const props = withDefaults(defineProps<{
8
9
  class?: HTMLAttributes['class'];
9
10
  level?: HeadingLevel;
11
+ icon?: HugeiconsIconDefinition | null;
10
12
  }>(), {
11
- level: 3
13
+ level: 3,
14
+ icon: null
12
15
  });
13
16
  </script>
14
17
 
15
18
  <template>
16
19
  <component :is="`h${props.level}`"
17
20
  data-slot="card-title"
18
- :class="cn('leading-none font-semibold text-sm sm:text-base', props.class)">
21
+ :class="cn('leading-none font-semibold text-sm sm:text-base', props.icon && 'flex items-center gap-2', props.class)">
22
+ <!-- Leading icon -->
23
+ <HugeiconsIcon v-if="props.icon" :icon="props.icon" class="size-4 shrink-0" />
19
24
  <slot />
20
25
  </component>
21
26
  </template>
@@ -5,4 +5,5 @@ export { default as CardText } from './CardText.vue';
5
5
  export { default as CardDescription } from './CardDescription.vue';
6
6
  export { default as CardFooter } from './CardFooter.vue';
7
7
  export { default as CardHeader } from './CardHeader.vue';
8
+ export { default as CardMedia } from './CardMedia.vue';
8
9
  export { default as CardTitle } from './CardTitle.vue';
@@ -23,7 +23,7 @@ const fieldControlId = useFieldControlId(() => props.id);
23
23
 
24
24
  <template>
25
25
  <CheckboxRoot v-slot="slotProps" data-slot="checkbox" v-bind="forwarded" :id="fieldControlId" :class="cn(
26
- 'group/checkbox peer relative inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-sm border border-input bg-secondary text-primary-foreground shadow-elevated-sm outline-none',
26
+ 'group/checkbox peer relative inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded border border-input bg-secondary text-primary-foreground shadow-elevated-sm outline-none',
27
27
  '[transition:scale_320ms_var(--ease-spring),background-color_150ms_var(--ease-snappy),border-color_150ms_var(--ease-snappy),box-shadow_150ms_var(--ease-snappy)]',
28
28
  'hover:border-border-strong active:scale-[0.86] focus-visible:ring-[3px] focus-visible:ring-ring/45',
29
29
  'data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary',
@@ -134,10 +134,10 @@ const handleKeydown = (event: KeyboardEvent) => {
134
134
  <div class="flex min-w-0 flex-1 flex-wrap items-center gap-1.5">
135
135
  <!-- Selected tags (multiple mode) -->
136
136
  <TransitionGroup v-if="props.multiple" leave-active-class="transition-[opacity,scale] duration-150 ease-snappy" leave-to-class="scale-75 opacity-0">
137
- <span v-for="option in selectedOptions" :key="option.value" data-slot="combobox-select-tag" class="inline-flex h-7 max-w-full animate-uv-pop items-center gap-1 rounded-sm border border-border bg-surface pr-1 pl-2 text-[11px] font-semibold text-foreground sm:text-xs">
137
+ <span v-for="option in selectedOptions" :key="option.value" data-slot="combobox-select-tag" class="inline-flex h-7 max-w-full animate-uv-pop items-center gap-1 rounded border border-border bg-surface pr-1 pl-2 text-[11px] font-semibold text-foreground sm:text-xs">
138
138
  <HugeiconsIcon v-if="option.icon" :icon="option.icon" class="size-3" />
139
139
  <span class="truncate">{{ option.label }}</span>
140
- <button type="button" :disabled="props.disabled" :aria-label="`${t('uiVintage.buttons.remove')} ${option.label}`" class="flex size-5 cursor-pointer items-center justify-center rounded-sm text-muted-foreground outline-none transition-[color,background-color,scale] duration-150 ease-spring hover:bg-accent hover:text-destructive focus-visible:ring-[3px] focus-visible:ring-ring/45 active:scale-90 disabled:pointer-events-none" @click.stop="removeValue(option.value)">
140
+ <button type="button" :disabled="props.disabled" :aria-label="`${t('uiVintage.buttons.remove')} ${option.label}`" class="flex size-5 cursor-pointer items-center justify-center rounded text-muted-foreground outline-none transition-[color,background-color,scale] duration-150 ease-spring hover:bg-accent hover:text-destructive focus-visible:ring-[3px] focus-visible:ring-ring/45 active:scale-90 disabled:pointer-events-none" @click.stop="removeValue(option.value)">
141
141
  <HugeiconsIcon :icon="Cancel01Icon" class="size-3" />
142
142
  </button>
143
143
  </span>
@@ -16,7 +16,7 @@ const forwardedProps = useForwardProps(delegatedProps);
16
16
 
17
17
  <template>
18
18
  <ComboboxTrigger data-slot="combobox-trigger" v-bind="forwardedProps" :class="cn(
19
- 'group/combobox-trigger -mr-1 flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground outline-none transition-colors duration-150 hover:bg-accent hover:text-foreground disabled:cursor-not-allowed disabled:opacity-60',
19
+ 'group/combobox-trigger -mr-1 flex size-7 shrink-0 cursor-pointer items-center justify-center rounded text-muted-foreground outline-none transition-colors duration-150 hover:bg-accent hover:text-foreground disabled:cursor-not-allowed disabled:opacity-60',
20
20
  props.class
21
21
  )
22
22
  ">
@@ -157,7 +157,7 @@ const handleRowKeydown = (row: T, event: KeyboardEvent) => {
157
157
  <!-- Column headers -->
158
158
  <th v-for="column in props.columns" :key="column.key" :style="column.width ? { width: column.width } : undefined" :aria-sort="props.sort?.key === column.key ? (props.sort.direction === 'asc' ? 'ascending' : 'descending') : undefined" :class="cn('h-11 px-4 text-xs font-semibold whitespace-nowrap text-muted-foreground', alignClasses[column.align ?? 'left'], column.headerClass)">
159
159
  <slot :name="`header-${column.key}`" :column="column">
160
- <button v-if="column.sortable" type="button" :class="cn('group/sort -mx-1.5 inline-flex cursor-pointer items-center gap-1.5 rounded-sm px-1.5 py-1 outline-none transition-colors duration-150 hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/45', props.sort?.key === column.key && 'text-foreground', justifyClasses[column.align ?? 'left'])" @click="toggleSort(column)">
160
+ <button v-if="column.sortable" type="button" :class="cn('group/sort -mx-1.5 inline-flex cursor-pointer items-center gap-1.5 rounded px-1.5 py-1 outline-none transition-colors duration-150 hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/45', props.sort?.key === column.key && 'text-foreground', justifyClasses[column.align ?? 'left'])" @click="toggleSort(column)">
161
161
  {{ column.label }}
162
162
  <HugeiconsIcon :icon="ArrowUp01Icon" :class="cn('size-3.5 transition-[rotate,opacity,color] duration-300 ease-spring', props.sort?.key === column.key ? 'text-primary opacity-100' : 'opacity-0 group-hover/sort:opacity-50', props.sort?.key === column.key && props.sort.direction === 'desc' && 'rotate-180')" />
163
163
  </button>
@@ -7,6 +7,7 @@ import Cancel01Icon from '@hugeicons/core-free-icons/Cancel01Icon';
7
7
  import { HugeiconsIcon } from '@hugeicons/vue';
8
8
  import { useI18n } from 'vue-i18n';
9
9
  import { cn } from '../../../lib/utils';
10
+ import { dialogSizeClasses, type DialogSize } from '.';
10
11
  import { Button } from '../button';
11
12
  import DialogOverlay from './DialogOverlay.vue';
12
13
 
@@ -18,8 +19,10 @@ defineOptions({
18
19
  const props = withDefaults(defineProps<DialogContentProps & {
19
20
  class?: HTMLAttributes['class'];
20
21
  showCloseButton?: boolean;
22
+ size?: DialogSize;
21
23
  }>(), {
22
- showCloseButton: false
24
+ showCloseButton: false,
25
+ size: 'sm'
23
26
  });
24
27
 
25
28
  const { t } = useI18n();
@@ -27,7 +30,7 @@ const { t } = useI18n();
27
30
  // Emits
28
31
  const emits = defineEmits<DialogContentEmits>();
29
32
 
30
- const delegatedProps = reactiveOmit(props, 'class');
33
+ const delegatedProps = reactiveOmit(props, 'class', 'size');
31
34
 
32
35
  const forwarded = useForwardPropsEmits(delegatedProps, emits);
33
36
  </script>
@@ -36,7 +39,8 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
36
39
  <DialogPortal>
37
40
  <DialogOverlay />
38
41
  <DialogContent data-slot="dialog-content" v-bind="{ ...$attrs, ...forwarded }" :class="cn(
39
- 'uv-modal-motion bg-card text-card-foreground fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded border border-border p-6 shadow-elevated-xl outline-none sm:max-w-106.25',
42
+ 'uv-modal-motion bg-card text-card-foreground fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded border border-border p-6 shadow-elevated-xl outline-none',
43
+ dialogSizeClasses[props.size],
40
44
  props.class
41
45
  )
42
46
  ">
@@ -7,6 +7,7 @@ import Cancel01Icon from '@hugeicons/core-free-icons/Cancel01Icon';
7
7
  import { HugeiconsIcon } from '@hugeicons/vue';
8
8
  import { useI18n } from 'vue-i18n';
9
9
  import { cn } from '../../../lib/utils';
10
+ import { dialogSizeClasses, type DialogSize } from '.';
10
11
  import { Button } from '../button';
11
12
 
12
13
  defineOptions({
@@ -17,8 +18,10 @@ defineOptions({
17
18
  const props = withDefaults(defineProps<DialogContentProps & {
18
19
  class?: HTMLAttributes['class'];
19
20
  showCloseButton?: boolean;
21
+ size?: DialogSize;
20
22
  }>(), {
21
- showCloseButton: false
23
+ showCloseButton: false,
24
+ size: 'sm'
22
25
  });
23
26
 
24
27
  const { t } = useI18n();
@@ -26,7 +29,7 @@ const { t } = useI18n();
26
29
  // Emits
27
30
  const emits = defineEmits<DialogContentEmits>();
28
31
 
29
- const delegatedProps = reactiveOmit(props, 'class');
32
+ const delegatedProps = reactiveOmit(props, 'class', 'size');
30
33
 
31
34
  const forwarded = useForwardPropsEmits(delegatedProps, emits);
32
35
  </script>
@@ -35,7 +38,8 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
35
38
  <DialogPortal>
36
39
  <DialogOverlay class="uv-overlay-motion fixed inset-0 isolate z-50 grid place-items-center overflow-y-auto bg-black/40 backdrop-blur-[3px] dark:bg-black/60">
37
40
  <DialogContent :class="cn(
38
- 'uv-modal-motion relative z-50 my-6 grid w-full max-w-[calc(100%-2rem)] gap-4 rounded border border-border bg-card p-6 text-card-foreground shadow-elevated-xl outline-none sm:max-w-106.25',
41
+ 'uv-modal-motion relative z-50 my-6 grid w-full max-w-[calc(100%-2rem)] gap-4 rounded border border-border bg-card p-6 text-card-foreground shadow-elevated-xl outline-none',
42
+ dialogSizeClasses[props.size],
39
43
  props.class
40
44
  )
41
45
  " v-bind="{ ...$attrs, ...forwarded }" @pointer-down-outside="
@@ -7,4 +7,14 @@ export { default as DialogHeader } from './DialogHeader.vue';
7
7
  export { default as DialogOverlay } from './DialogOverlay.vue';
8
8
  export { default as DialogScrollContent } from './DialogScrollContent.vue';
9
9
  export { default as DialogTitle } from './DialogTitle.vue';
10
- export { default as DialogTrigger } from './DialogTrigger.vue';
10
+ export { default as DialogTrigger } from './DialogTrigger.vue';
11
+
12
+ export type DialogSize = 'sm' | 'md' | 'lg' | 'xl';
13
+
14
+ // Max width per dialog size (sm is the compact default)
15
+ export const dialogSizeClasses: Record<DialogSize, string> = {
16
+ sm: 'sm:max-w-106.25',
17
+ md: 'sm:max-w-md',
18
+ lg: 'sm:max-w-xl',
19
+ xl: 'sm:max-w-2xl'
20
+ };
@@ -221,7 +221,7 @@ onBeforeUnmount(() => clearTimeout(rejectTimer));
221
221
  <div class="pb-2">
222
222
  <!-- File row -->
223
223
  <div data-slot="file-dropzone-item" class="flex items-center gap-3 rounded border border-border bg-card py-2 pr-2 pl-3 shadow-elevated-sm">
224
- <span class="flex size-9 shrink-0 items-center justify-center rounded-sm bg-surface text-muted-foreground">
224
+ <span class="flex size-9 shrink-0 items-center justify-center rounded bg-surface text-muted-foreground">
225
225
  <HugeiconsIcon :icon="getFileIcon(file)" class="size-4.5" />
226
226
  </span>
227
227
  <div class="flex min-w-0 flex-1 flex-col">
@@ -1,13 +1,18 @@
1
1
  <script setup lang="ts">
2
- import type { HTMLAttributes } from 'vue';
3
- import { useVModel } from '@vueuse/core';
2
+ import { computed, ref, useSlots, type HTMLAttributes } from 'vue';
3
+ import { useElementSize, useVModel } from '@vueuse/core';
4
4
  import { cn } from '../../../lib/utils';
5
5
  import { useFieldControlId } from '../field/field-context';
6
6
 
7
+ defineOptions({
8
+ inheritAttrs: false
9
+ });
10
+
7
11
  // Props
8
12
  const props = defineProps<{
9
13
  defaultValue?: string | number;
10
14
  modelValue?: string | number;
15
+ size?: 'default' | 'sm';
11
16
  class?: HTMLAttributes['class'];
12
17
  }>();
13
18
 
@@ -23,14 +28,42 @@ const modelValue = useVModel(props, 'modelValue', emits, {
23
28
 
24
29
  // Let a surrounding Field label point to this control
25
30
  const fieldControlId = useFieldControlId();
31
+
32
+ // Field look, shared by the plain input and the one with a suffix
33
+ const inputClasses = computed(() => cn(
34
+ 'uv-field w-full min-w-0 rounded border border-input bg-secondary px-4 py-3 text-xs font-semibold text-foreground shadow-elevated-sm outline-none sm:text-sm',
35
+ props.size === 'sm' ? 'h-10 py-2' : 'h-13',
36
+ 'selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground placeholder:opacity-60 placeholder:transition-opacity focus-visible:placeholder:opacity-40',
37
+ 'file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-semibold file:text-foreground',
38
+ 'disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-60 aria-invalid:animate-uv-shake',
39
+ props.class
40
+ ));
41
+
42
+ // Optional prefix (like a search icon) and suffix (like a unit) inside the field: the text never runs under them
43
+ const slots = useSlots();
44
+ const prefixRef = ref<HTMLElement | null>(null);
45
+ const suffixRef = ref<HTMLElement | null>(null);
46
+ const { width: prefixWidth } = useElementSize(prefixRef);
47
+ const { width: suffixWidth } = useElementSize(suffixRef);
48
+ const affixPadding = computed(() => ({
49
+ ...(slots.prefix && prefixWidth.value ? { paddingLeft: `calc(${prefixWidth.value}px + 1.5rem)` } : {}),
50
+ ...(slots.suffix && suffixWidth.value ? { paddingRight: `calc(${suffixWidth.value}px + 1.75rem)` } : {})
51
+ }));
26
52
  </script>
27
53
 
28
54
  <template>
29
- <input :id="fieldControlId" v-model="modelValue" data-slot="input" :class="cn(
30
- 'uv-field h-13 w-full min-w-0 rounded border border-input bg-secondary px-4 py-3 text-xs font-semibold text-foreground shadow-elevated-sm outline-none sm:text-sm',
31
- 'selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground placeholder:opacity-60 placeholder:transition-opacity focus-visible:placeholder:opacity-40',
32
- 'file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-semibold file:text-foreground',
33
- 'disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-60 aria-invalid:animate-uv-shake',
34
- props.class
35
- )" />
55
+ <div v-if="$slots.prefix || $slots.suffix" data-slot="input-group" class="relative w-full min-w-0">
56
+ <!-- Prefix -->
57
+ <span v-if="$slots.prefix" ref="prefixRef" data-slot="input-prefix" class="pointer-events-none absolute inset-y-0 left-3.5 flex items-center text-muted-foreground [&_svg:not([class*='size-'])]:size-4">
58
+ <slot name="prefix" />
59
+ </span>
60
+
61
+ <input :id="fieldControlId" v-model="modelValue" data-slot="input" v-bind="$attrs" :style="affixPadding" :class="inputClasses" />
62
+
63
+ <!-- Suffix -->
64
+ <span v-if="$slots.suffix" ref="suffixRef" data-slot="input-suffix" class="pointer-events-none absolute inset-y-0 right-4 flex items-center text-xs font-semibold text-muted-foreground sm:text-sm">
65
+ <slot name="suffix" />
66
+ </span>
67
+ </div>
68
+ <input v-else :id="fieldControlId" v-model="modelValue" data-slot="input" v-bind="$attrs" :class="inputClasses" />
36
69
  </template>
@@ -2,17 +2,21 @@
2
2
  import type { HTMLAttributes } from 'vue';
3
3
  import { cn } from '../../../lib/utils';
4
4
 
5
- // Props
6
- const props = defineProps<{
5
+ // Props (lg is a big standalone key, for example to show a shortcut in a hint)
6
+ const props = withDefaults(defineProps<{
7
7
  class?: HTMLAttributes['class'];
8
8
  pressed?: boolean;
9
- }>();
9
+ size?: 'default' | 'lg';
10
+ }>(), {
11
+ size: 'default'
12
+ });
10
13
  </script>
11
14
 
12
15
  <template>
13
16
  <kbd data-slot="kbd" :data-pressed="props.pressed ? '' : undefined" :class="cn(
14
- 'inline-flex h-5 min-w-5 select-none items-center justify-center gap-1 rounded-sm border border-b-2 border-border bg-surface px-1.5 font-mono text-[11px] leading-none font-semibold text-muted-foreground shadow-elevated-sm transition-[translate,border-width,box-shadow] duration-100 ease-snappy [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-3',
17
+ 'inline-flex h-5 min-w-5 select-none items-center justify-center gap-1 rounded border border-b-2 border-border bg-surface px-1.5 font-mono text-[11px] leading-none font-semibold text-muted-foreground shadow-elevated-sm transition-[translate,border-width,box-shadow] duration-100 ease-snappy [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-3',
15
18
  'data-pressed:translate-y-px data-pressed:border-b data-pressed:shadow-none',
19
+ props.size === 'lg' && 'h-10 min-w-10 px-3 text-sm',
16
20
  props.class
17
21
  )
18
22
  ">
@@ -92,7 +92,7 @@ const fieldControlId = useFieldControlId(() => props.id);
92
92
  )" @update:model-value="animateChange">
93
93
  <!-- Decrement button -->
94
94
  <NumberFieldDecrement data-slot="number-field-decrement" :aria-label="resolvedDecrementLabel" :class="cn(
95
- 'inline-flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground outline-none',
95
+ 'inline-flex size-10 shrink-0 cursor-pointer items-center justify-center rounded text-muted-foreground outline-none',
96
96
  '[transition:scale_320ms_var(--ease-spring),background-color_150ms_var(--ease-snappy),color_150ms_var(--ease-snappy),opacity_150ms_var(--ease-snappy)]',
97
97
  'hover:bg-accent hover:text-foreground active:scale-[0.84] data-pressed:scale-[0.84] focus-visible:ring-[3px] focus-visible:ring-ring/45',
98
98
  'disabled:pointer-events-none disabled:opacity-35 data-disabled:pointer-events-none data-disabled:opacity-35'
@@ -119,7 +119,7 @@ const fieldControlId = useFieldControlId(() => props.id);
119
119
 
120
120
  <!-- Increment button -->
121
121
  <NumberFieldIncrement data-slot="number-field-increment" :aria-label="resolvedIncrementLabel" :class="cn(
122
- 'inline-flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground outline-none',
122
+ 'inline-flex size-10 shrink-0 cursor-pointer items-center justify-center rounded text-muted-foreground outline-none',
123
123
  '[transition:scale_320ms_var(--ease-spring),background-color_150ms_var(--ease-snappy),color_150ms_var(--ease-snappy),opacity_150ms_var(--ease-snappy)]',
124
124
  'hover:bg-accent hover:text-foreground active:scale-[0.84] data-pressed:scale-[0.84] focus-visible:ring-[3px] focus-visible:ring-ring/45',
125
125
  'disabled:pointer-events-none disabled:opacity-35 data-disabled:pointer-events-none data-disabled:opacity-35'
@@ -6,16 +6,28 @@ import { reactiveOmit } from '@vueuse/core';
6
6
  import { ProgressIndicator, ProgressRoot } from 'reka-ui';
7
7
  import { cn } from '../../../lib/utils';
8
8
 
9
+ type ProgressColor = 'primary' | 'green' | 'red' | 'yellow';
10
+
9
11
  // Props
10
12
  const props = withDefaults(defineProps<ProgressRootProps & {
11
13
  indeterminate?: boolean;
14
+ color?: ProgressColor;
12
15
  class?: HTMLAttributes['class'];
13
16
  }>(), {
14
17
  modelValue: 0,
15
- indeterminate: false
18
+ indeterminate: false,
19
+ color: 'primary'
16
20
  });
17
21
 
18
- const delegatedProps = reactiveOmit(props, 'class', 'indeterminate');
22
+ const delegatedProps = reactiveOmit(props, 'class', 'indeterminate', 'color');
23
+
24
+ // Fill color of the bar
25
+ const colorClasses: Record<ProgressColor, string> = {
26
+ primary: 'bg-primary',
27
+ green: 'bg-success',
28
+ red: 'bg-destructive',
29
+ yellow: 'bg-warning'
30
+ };
19
31
 
20
32
  // Resolve the fill percentage against the configured max
21
33
  const percentage = computed(() => {
@@ -27,13 +39,13 @@ const percentage = computed(() => {
27
39
 
28
40
  <template>
29
41
  <ProgressRoot data-slot="progress" v-bind="delegatedProps" :model-value="props.indeterminate ? null : props.modelValue" :class="cn(
30
- 'relative h-2 w-full overflow-hidden rounded-sm bg-muted',
42
+ 'relative h-2 w-full overflow-hidden rounded bg-muted',
31
43
  props.class
32
44
  )">
33
45
  <!-- Indeterminate bar -->
34
- <div v-if="props.indeterminate" data-slot="progress-indicator" class="absolute inset-y-0 left-0 w-full origin-left animate-uv-indeterminate rounded-sm bg-primary" />
46
+ <div v-if="props.indeterminate" data-slot="progress-indicator" :class="cn('absolute inset-y-0 left-0 w-full origin-left animate-uv-indeterminate rounded', colorClasses[props.color])" />
35
47
 
36
48
  <!-- Determinate bar -->
37
- <ProgressIndicator v-else data-slot="progress-indicator" class="relative h-full w-full flex-1 overflow-hidden rounded-sm bg-primary transition-transform duration-700 ease-out-expo" :style="`transform: translateX(-${100 - percentage}%);`" />
49
+ <ProgressIndicator v-else data-slot="progress-indicator" :class="cn('relative h-full w-full flex-1 overflow-hidden rounded transition-transform duration-700 ease-out-expo', colorClasses[props.color])" :style="`transform: translateX(-${100 - percentage}%);`" />
38
50
  </ProgressRoot>
39
51
  </template>
@@ -16,7 +16,7 @@ const forwardedProps = useForwardProps(delegatedProps);
16
16
 
17
17
  <template>
18
18
  <SelectItem data-slot="select-item" v-bind="forwardedProps" :class="cn(
19
- 'focus:bg-accent focus:text-accent-foreground data-[state=checked]:text-foreground transition-colors duration-150 [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex w-full cursor-pointer items-center gap-2 rounded-sm py-2.5 pr-8 pl-2 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2',
19
+ 'focus:bg-accent focus:text-accent-foreground data-[state=checked]:text-foreground transition-colors duration-150 [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex w-full cursor-pointer items-center gap-2 rounded py-2.5 pr-8 pl-2 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2',
20
20
  props.class
21
21
  )
22
22
  ">
@@ -18,9 +18,11 @@ const props = defineProps<{
18
18
 
19
19
  <template>
20
20
  <div data-slot="select-item-content" :class="cn('flex min-w-0 flex-1 items-center gap-2', props.class)">
21
- <!-- Icon -->
22
- <div v-if="props.icon" data-slot="select-item-content-icon" :class="cn('shrink-0 self-center', props.iconClass)">
23
- <HugeiconsIcon :icon="props.icon" class="block size-3 sm:size-4" />
21
+ <!-- Leading visual: the default slot (for example a flag), or the icon -->
22
+ <div v-if="$slots.default || props.icon" data-slot="select-item-content-icon" :class="cn('shrink-0 self-center', props.iconClass)">
23
+ <slot>
24
+ <HugeiconsIcon :icon="props.icon" class="block size-3 sm:size-4" />
25
+ </slot>
24
26
  </div>
25
27
 
26
28
  <!-- Text and description -->
@@ -3,14 +3,18 @@ import type { HTMLAttributes } from 'vue';
3
3
  import { cn } from '../../../lib/utils';
4
4
 
5
5
  // Props
6
- const props = defineProps<{
6
+ const props = withDefaults(defineProps<{
7
+ circle?: boolean;
7
8
  class?: HTMLAttributes['class'];
8
- }>();
9
+ }>(), {
10
+ circle: false
11
+ });
9
12
  </script>
10
13
 
11
14
  <template>
12
15
  <div data-slot="skeleton" :class="cn(
13
- 'relative overflow-hidden rounded border border-border bg-surface',
16
+ 'relative overflow-hidden border border-border bg-surface',
17
+ props.circle ? 'rounded-full' : 'rounded',
14
18
  'after:absolute after:inset-0 after:animate-uv-shimmer after:bg-[linear-gradient(90deg,transparent,color-mix(in_oklab,var(--foreground)_6%,transparent),transparent)]',
15
19
  props.class
16
20
  )" />
@@ -29,7 +29,7 @@ const fieldControlId = useFieldControlId(() => props.id);
29
29
  props.class
30
30
  )">
31
31
  <SwitchThumb data-slot="switch-thumb" :class="cn(
32
- 'pointer-events-none absolute left-1 top-1/2 flex h-4 w-4 -translate-y-1/2 items-center justify-center rounded-[0.2rem] bg-muted-foreground ring-0',
32
+ 'pointer-events-none absolute left-1 top-1/2 flex h-4 w-4 -translate-y-1/2 items-center justify-center rounded bg-muted-foreground ring-0',
33
33
  '[transition:translate_380ms_var(--ease-spring),width_220ms_var(--ease-spring),background-color_200ms]',
34
34
  'group-active/switch:w-5 data-[state=checked]:translate-x-6 data-[state=checked]:bg-primary group-active/switch:data-[state=checked]:translate-x-5'
35
35
  )">
@@ -9,7 +9,7 @@ const props = defineProps<{
9
9
  </script>
10
10
 
11
11
  <template>
12
- <thead data-slot="table-header" :class="cn('[&_tr]:border-b [&_tr]:border-border', props.class)">
12
+ <thead data-slot="table-header" :class="cn('[&_tr]:border-b [&_tr]:border-border [&_tr]:hover:bg-transparent', props.class)">
13
13
  <slot />
14
14
  </thead>
15
15
  </template>
@@ -2,15 +2,22 @@
2
2
  import type { HTMLAttributes } from 'vue';
3
3
  import { cn } from '../../../lib/utils';
4
4
 
5
- // Props
6
- const props = defineProps<{
5
+ // Props (static rows don't highlight on hover, interactive rows show a pointer for clickable rows; data-state="selected" highlights a row with a soft primary tint)
6
+ const props = withDefaults(defineProps<{
7
+ static?: boolean;
8
+ interactive?: boolean;
7
9
  class?: HTMLAttributes['class'];
8
- }>();
10
+ }>(), {
11
+ static: false,
12
+ interactive: false
13
+ });
9
14
  </script>
10
15
 
11
16
  <template>
12
17
  <tr data-slot="table-row" :class="cn(
13
- 'hover:bg-muted/50 data-[state=selected]:bg-muted border-b border-border transition-colors',
18
+ 'data-[state=selected]:bg-primary/5 data-[state=selected]:hover:bg-primary/10 border-b border-border transition-colors',
19
+ !props.static && 'hover:bg-muted/50',
20
+ props.interactive && 'cursor-pointer',
14
21
  props.class
15
22
  )
16
23
  ">
@@ -18,7 +18,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
18
18
  </script>
19
19
 
20
20
  <template>
21
- <TabsRoot v-slot="slotProps" data-slot="tabs" v-bind="forwarded" :class="cn('flex flex-col gap-2', props.class)">
21
+ <TabsRoot v-slot="slotProps" data-slot="tabs" v-bind="forwarded" :class="cn('flex flex-col gap-2 data-[orientation=vertical]:gap-4 sm:data-[orientation=vertical]:flex-row', props.class)">
22
22
  <slot v-bind="slotProps" />
23
23
  </TabsRoot>
24
24
  </template>
@@ -5,16 +5,19 @@ import { reactiveOmit } from '@vueuse/core';
5
5
  import { TabsContent } from 'reka-ui';
6
6
  import { cn } from '../../../lib/utils';
7
7
 
8
- // Props
9
- const props = defineProps<TabsContentProps & {
8
+ // Props (boxed frames the panel in a bordered surface)
9
+ const props = withDefaults(defineProps<TabsContentProps & {
10
+ variant?: 'plain' | 'boxed';
10
11
  class?: HTMLAttributes['class'];
11
- }>();
12
+ }>(), {
13
+ variant: 'plain'
14
+ });
12
15
 
13
- const delegatedProps = reactiveOmit(props, 'class');
16
+ const delegatedProps = reactiveOmit(props, 'class', 'variant');
14
17
  </script>
15
18
 
16
19
  <template>
17
- <TabsContent data-slot="tabs-content" :class="cn('flex-1 outline-none data-[state=active]:animate-uv-fade-up', props.class)" v-bind="delegatedProps">
20
+ <TabsContent data-slot="tabs-content" :class="cn('flex-1 outline-none data-[state=active]:animate-uv-fade-up', props.variant === 'boxed' && 'rounded border border-border bg-surface/50 p-4', props.class)" v-bind="delegatedProps">
18
21
  <slot />
19
22
  </TabsContent>
20
23
  </template>
@@ -18,7 +18,7 @@ const forwardedProps = useForwardProps(delegatedProps);
18
18
  <TabsTrigger data-slot="tabs-trigger" :class="cn(
19
19
  'relative inline-flex min-h-9 flex-1 cursor-pointer items-center justify-center gap-1.5 rounded border border-transparent bg-transparent px-4 py-2 text-sm font-semibold whitespace-nowrap text-muted-foreground outline-none select-none',
20
20
  '[transition:color_150ms,background-color_150ms,scale_300ms_var(--ease-spring)] hover:text-foreground active:scale-[0.97] focus-visible:ring-[3px] focus-visible:ring-ring/45 disabled:pointer-events-none disabled:opacity-50',
21
- 'data-[state=active]:text-primary data-[state=inactive]:hover:bg-accent/60',
21
+ 'data-[state=active]:text-primary data-[state=inactive]:hover:bg-accent/60 group-aria-[orientation=vertical]/tabs-list:justify-start',
22
22
  '[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
23
23
  props.class
24
24
  )" v-bind="forwardedProps">
@@ -18,7 +18,7 @@ const forwardedProps = useForwardProps(delegatedProps);
18
18
 
19
19
  <template>
20
20
  <TagsInputItemDelete data-slot="tags-input-item-delete" v-bind="forwardedProps" :class="cn(
21
- 'inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground outline-none',
21
+ 'inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded text-muted-foreground outline-none',
22
22
  '[transition:rotate_320ms_var(--ease-spring),scale_320ms_var(--ease-spring),background-color_150ms_var(--ease-snappy),color_150ms_var(--ease-snappy)]',
23
23
  'hover:rotate-90 hover:bg-accent hover:text-foreground active:scale-[0.8] focus-visible:ring-[3px] focus-visible:ring-ring/45',
24
24
  'group-data-[state=active]/tag:text-primary data-disabled:pointer-events-none',
@@ -27,7 +27,7 @@ const fieldControlId = useFieldControlId();
27
27
 
28
28
  <template>
29
29
  <textarea :id="fieldControlId" v-model="modelValue" data-slot="textarea" :class="cn(
30
- 'uv-field flex field-sizing-content min-h-30 w-full min-w-0 rounded border border-input bg-secondary px-4 py-3 text-xs font-semibold text-foreground shadow-elevated-sm outline-none sm:text-sm',
30
+ 'uv-field flex field-sizing-content min-h-30 w-full min-w-0 resize-none rounded border border-input bg-secondary px-4 py-3 text-xs font-semibold text-foreground shadow-elevated-sm outline-none sm:text-sm',
31
31
  'selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground placeholder:opacity-60',
32
32
  'disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-60 aria-invalid:animate-uv-shake',
33
33
  props.class
@@ -34,7 +34,7 @@ const isSingle = computed(() => {
34
34
  });
35
35
 
36
36
  // Share size and indicator mode with the items
37
- provide(toggleGroupContextKey, { size: toRef(props, 'size'), indicator: isSingle });
37
+ provide(toggleGroupContextKey, { size: toRef(props, 'size'), indicator: isSingle, highlight: toRef(props, 'highlight') });
38
38
 
39
39
  // Sliding indicator geometry
40
40
  const rootRef = ref();
@@ -83,7 +83,7 @@ const indicatorStyle = computed<CSSProperties>(() => ({
83
83
  <ToggleGroupRoot ref="rootRef" v-slot="slotProps" data-slot="toggle-group" :data-size="props.size" v-bind="forwarded" :class="cn('relative inline-flex w-fit items-center gap-1 rounded border border-border bg-card p-1 shadow-elevated-sm data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch', props.class)">
84
84
  <!-- Sliding indicator behind the active item -->
85
85
  <span v-if="isSingle" data-slot="toggle-group-indicator" aria-hidden="true" :style="indicatorStyle" :class="cn(
86
- 'pointer-events-none absolute top-0 left-0 z-0 rounded-sm border',
86
+ 'pointer-events-none absolute top-0 left-0 z-0 rounded border', // Same radius as the tab triggers, so every switch looks alike
87
87
  animated && '[transition:transform_380ms_var(--ease-spring),width_380ms_var(--ease-spring),height_380ms_var(--ease-spring),opacity_180ms_var(--ease-snappy)]',
88
88
  props.highlight ? 'border-primary/40 bg-primary/10' : 'border-border-strong bg-surface shadow-elevated-sm'
89
89
  )" />
@@ -17,16 +17,18 @@ const props = defineProps<ToggleGroupItemProps & {
17
17
  const delegatedProps = reactiveOmit(props, 'class', 'size');
18
18
  const forwardedProps = useForwardProps(delegatedProps);
19
19
 
20
- // Size and indicator mode come from the group
20
+ // Size, indicator mode and highlight come from the group
21
21
  const context = inject(toggleGroupContextKey, null);
22
22
  const size = computed(() => props.size ?? context?.size.value ?? 'default');
23
23
  const hasIndicator = computed(() => context?.indicator.value ?? false);
24
+ const isHighlighted = computed(() => context?.highlight.value ?? false);
24
25
  </script>
25
26
 
26
27
  <template>
27
28
  <ToggleGroupItem v-slot="slotProps" data-slot="toggle-group-item" :data-size="size" v-bind="forwardedProps" :class="cn(
28
29
  toggleGroupItemVariants({ size }),
29
30
  !hasIndicator && 'hover:bg-accent data-[state=on]:bg-accent',
31
+ isHighlighted && 'data-[state=on]:text-primary', // The highlighted group tints the active item like the active tab
30
32
  props.class
31
33
  )">
32
34
  <slot v-bind="slotProps" />
@@ -45,7 +45,7 @@ export const toggleVariants: (props?: ToggleVariants & ClassProp) => string = cv
45
45
  // Segmented control item (sits above the sliding indicator)
46
46
  export const toggleGroupItemVariants: (props?: { size?: ToggleGroupSize | null } & ClassProp) => string = cva(
47
47
  [
48
- "relative z-10 inline-flex shrink-0 cursor-pointer select-none items-center justify-center gap-1.5 whitespace-nowrap rounded-sm text-xs font-semibold text-muted-foreground outline-none sm:text-sm",
48
+ "relative z-10 inline-flex shrink-0 cursor-pointer select-none items-center justify-center gap-1.5 whitespace-nowrap rounded text-xs font-semibold text-muted-foreground outline-none sm:text-sm",
49
49
  "[transition:color_150ms,background-color_150ms,box-shadow_220ms,opacity_150ms,transform_380ms_var(--ease-spring)]",
50
50
  "hover:text-foreground data-[state=on]:text-foreground active:scale-[0.96] active:duration-75 disabled:pointer-events-none disabled:opacity-50",
51
51
  "focus-visible:ring-[3px] focus-visible:ring-ring/45",
@@ -66,4 +66,4 @@ export const toggleGroupItemVariants: (props?: { size?: ToggleGroupSize | null }
66
66
  );
67
67
 
68
68
  // Context shared by a toggle group with its items
69
- export const toggleGroupContextKey: InjectionKey<{ size: Ref<ToggleGroupSize>; indicator: Ref<boolean> }> = Symbol('uv-toggle-group');
69
+ export const toggleGroupContextKey: InjectionKey<{ size: Ref<ToggleGroupSize>; indicator: Ref<boolean>; highlight: Ref<boolean> }> = Symbol('uv-toggle-group');
package/src/i18n/de.json CHANGED
@@ -79,6 +79,7 @@
79
79
  "readMore": "Weiterlesen",
80
80
  "goToPost": "Zum Beitrag",
81
81
  "new": "Neu",
82
+ "inShort": "Kurz gesagt",
82
83
  "tableOfContents": "Inhaltsverzeichnis",
83
84
  "tableOfContentsDescription": "Zu einem Abschnitt dieses Artikels springen",
84
85
  "tableOfContentsSections": "{count} Abschnitt | {count} Abschnitte",
package/src/i18n/en.json CHANGED
@@ -79,6 +79,7 @@
79
79
  "readMore": "Read more",
80
80
  "goToPost": "Go to post",
81
81
  "new": "New",
82
+ "inShort": "In short",
82
83
  "tableOfContents": "Table of contents",
83
84
  "tableOfContentsDescription": "Jump to a section of this article",
84
85
  "tableOfContentsSections": "{count} section | {count} sections",
package/src/i18n/es.json CHANGED
@@ -79,6 +79,7 @@
79
79
  "readMore": "Leer mas",
80
80
  "goToPost": "Ir al articulo",
81
81
  "new": "Nuevo",
82
+ "inShort": "En resumen",
82
83
  "tableOfContents": "Tabla de contenidos",
83
84
  "tableOfContentsDescription": "Ir a una seccion de este articulo",
84
85
  "tableOfContentsSections": "{count} seccion | {count} secciones",
package/src/i18n/fr.json CHANGED
@@ -79,6 +79,7 @@
79
79
  "readMore": "Lire la suite",
80
80
  "goToPost": "Aller a l'article",
81
81
  "new": "Nouveau",
82
+ "inShort": "En bref",
82
83
  "tableOfContents": "Table des matieres",
83
84
  "tableOfContentsDescription": "Aller a une section de cet article",
84
85
  "tableOfContentsSections": "{count} section | {count} sections",