@brumaombra/ui-vintage 0.9.6 → 0.9.7

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 (75) hide show
  1. package/AGENTS.md +3 -1
  2. package/COMPONENTS.md +128 -14
  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 -3
  7. package/src/components/blog/BlogPostPage.vue +4 -0
  8. package/src/components/blog/BlogSectionTitle.vue +7 -21
  9. package/src/components/blog/BlogSummary.vue +31 -0
  10. package/src/components/blog/blogPageUtils.ts +1 -0
  11. package/src/components/blog/index.ts +1 -0
  12. package/src/components/chip/Chip.vue +1 -1
  13. package/src/components/dashboard-shell/DashboardShell.vue +3 -3
  14. package/src/components/data-list/DataListText.vue +7 -4
  15. package/src/components/language-flag/flags/ChineseFlag.vue +1 -1
  16. package/src/components/language-flag/flags/EnglishFlag.vue +1 -1
  17. package/src/components/language-flag/flags/FrenchFlag.vue +1 -1
  18. package/src/components/language-flag/flags/GermanFlag.vue +1 -1
  19. package/src/components/language-flag/flags/ItalianFlag.vue +1 -1
  20. package/src/components/language-flag/flags/JapaneseFlag.vue +1 -1
  21. package/src/components/language-flag/flags/PortugueseFlag.vue +1 -1
  22. package/src/components/language-flag/flags/RussianFlag.vue +1 -1
  23. package/src/components/language-flag/flags/SpanishFlag.vue +1 -1
  24. package/src/components/language-selector/LanguageSelector.vue +2 -2
  25. package/src/components/section-title/SectionTitle.vue +47 -0
  26. package/src/components/section-title/index.ts +2 -0
  27. package/src/components/single-value-card/SingleValueCard.vue +1 -1
  28. package/src/components/ui/alert-dialog/AlertDialogAction.vue +6 -6
  29. package/src/components/ui/avatar/Avatar.vue +4 -2
  30. package/src/components/ui/avatar/AvatarFallback.vue +25 -5
  31. package/src/components/ui/badge/Badge.vue +6 -2
  32. package/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue +1 -1
  33. package/src/components/ui/breadcrumb/BreadcrumbLink.vue +1 -1
  34. package/src/components/ui/button/Button.vue +18 -2
  35. package/src/components/ui/button/index.ts +4 -0
  36. package/src/components/ui/card/Card.vue +25 -1
  37. package/src/components/ui/card/CardMedia.vue +15 -0
  38. package/src/components/ui/card/CardTitle.vue +8 -3
  39. package/src/components/ui/card/index.ts +1 -0
  40. package/src/components/ui/checkbox/Checkbox.vue +1 -1
  41. package/src/components/ui/combobox/ComboboxSelect.vue +2 -2
  42. package/src/components/ui/combobox/ComboboxTrigger.vue +1 -1
  43. package/src/components/ui/data-table/DataTable.vue +1 -1
  44. package/src/components/ui/dialog/DialogContent.vue +7 -3
  45. package/src/components/ui/dialog/DialogScrollContent.vue +7 -3
  46. package/src/components/ui/dialog/index.ts +11 -1
  47. package/src/components/ui/file-dropzone/FileDropzone.vue +1 -1
  48. package/src/components/ui/input/Input.vue +42 -9
  49. package/src/components/ui/kbd/Kbd.vue +8 -4
  50. package/src/components/ui/number-field/NumberField.vue +2 -2
  51. package/src/components/ui/progress/Progress.vue +17 -5
  52. package/src/components/ui/select/SelectItem.vue +1 -1
  53. package/src/components/ui/skeleton/Skeleton.vue +7 -3
  54. package/src/components/ui/switch/Switch.vue +1 -1
  55. package/src/components/ui/table/TableHeader.vue +1 -1
  56. package/src/components/ui/table/TableRow.vue +11 -4
  57. package/src/components/ui/tabs/Tabs.vue +1 -1
  58. package/src/components/ui/tabs/TabsContent.vue +8 -5
  59. package/src/components/ui/tabs/TabsTrigger.vue +1 -1
  60. package/src/components/ui/tags-input/TagsInputItemDelete.vue +1 -1
  61. package/src/components/ui/textarea/Textarea.vue +1 -1
  62. package/src/components/ui/toggle-group/ToggleGroup.vue +2 -2
  63. package/src/components/ui/toggle-group/ToggleGroupItem.vue +3 -1
  64. package/src/components/ui/toggle-group/index.ts +2 -2
  65. package/src/i18n/de.json +1 -0
  66. package/src/i18n/en.json +1 -0
  67. package/src/i18n/es.json +1 -0
  68. package/src/i18n/fr.json +1 -0
  69. package/src/i18n/it.json +1 -0
  70. package/src/i18n/ja.json +1 -0
  71. package/src/i18n/pt.json +1 -0
  72. package/src/i18n/ru.json +1 -0
  73. package/src/i18n/zh.json +1 -0
  74. package/src/lib/color-tokens.ts +6 -6
  75. package/src/styles.css +11 -6
@@ -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
  ">
@@ -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",
package/src/i18n/it.json CHANGED
@@ -79,6 +79,7 @@
79
79
  "readMore": "Leggi di piu",
80
80
  "goToPost": "Vai all'articolo",
81
81
  "new": "Novità",
82
+ "inShort": "In breve",
82
83
  "tableOfContents": "Indice dei contenuti",
83
84
  "tableOfContentsDescription": "Vai a una sezione dell'articolo",
84
85
  "tableOfContentsSections": "{count} sezione | {count} sezioni",
package/src/i18n/ja.json CHANGED
@@ -79,6 +79,7 @@
79
79
  "readMore": "続きを読む",
80
80
  "goToPost": "記事を開く",
81
81
  "new": "新着",
82
+ "inShort": "要点",
82
83
  "tableOfContents": "目次",
83
84
  "tableOfContentsDescription": "この記事のセクションに移動",
84
85
  "tableOfContentsSections": "{count} セクション",
package/src/i18n/pt.json CHANGED
@@ -79,6 +79,7 @@
79
79
  "readMore": "Ler mais",
80
80
  "goToPost": "Ir para o artigo",
81
81
  "new": "Novo",
82
+ "inShort": "Em resumo",
82
83
  "tableOfContents": "Indice",
83
84
  "tableOfContentsDescription": "Ir para uma secao deste artigo",
84
85
  "tableOfContentsSections": "{count} secao | {count} secoes",
package/src/i18n/ru.json CHANGED
@@ -79,6 +79,7 @@
79
79
  "readMore": "Читать далее",
80
80
  "goToPost": "Перейти к статье",
81
81
  "new": "Новое",
82
+ "inShort": "Коротко",
82
83
  "tableOfContents": "Содержание",
83
84
  "tableOfContentsDescription": "Перейти к разделу статьи",
84
85
  "tableOfContentsSections": "Разделов: {count}",
package/src/i18n/zh.json CHANGED
@@ -79,6 +79,7 @@
79
79
  "readMore": "阅读更多",
80
80
  "goToPost": "前往文章",
81
81
  "new": "最新",
82
+ "inShort": "简而言之",
82
83
  "tableOfContents": "目录",
83
84
  "tableOfContentsDescription": "跳转到本文的一个部分",
84
85
  "tableOfContentsSections": "{count} 个章节",
@@ -37,12 +37,12 @@ export const buttonVariantClasses: Record<ButtonVariantName, string> = {
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",
39
39
 
40
- // Tone-based button variants
41
- gray: getInteractiveSurfaceToneClasses("gray"),
42
- green: getInteractiveSurfaceToneClasses("green"),
43
- red: getInteractiveSurfaceToneClasses("red"),
44
- blue: getInteractiveSurfaceToneClasses("blue"),
45
- yellow: getInteractiveSurfaceToneClasses("yellow")
40
+ // Tone-based button variants (the secondary shadow, so they read as buttons next to it, and a stronger tint in dark mode)
41
+ gray: `${getInteractiveSurfaceToneClasses("gray")} shadow-elevated-sm`,
42
+ green: `${getInteractiveSurfaceToneClasses("green")} shadow-elevated-sm dark:bg-green-900/25 dark:hover:bg-green-900/40`,
43
+ red: `${getInteractiveSurfaceToneClasses("red")} shadow-elevated-sm dark:bg-red-900/25 dark:hover:bg-red-900/40`,
44
+ blue: `${getInteractiveSurfaceToneClasses("blue")} shadow-elevated-sm dark:bg-blue-900/25 dark:hover:bg-blue-900/40`,
45
+ yellow: `${getInteractiveSurfaceToneClasses("yellow")} shadow-elevated-sm dark:bg-yellow-900/25 dark:hover:bg-yellow-900/40`
46
46
  }
47
47
 
48
48
  // Get the appropriate classes for a given button variant
package/src/styles.css CHANGED
@@ -57,9 +57,11 @@ html {
57
57
  --uv-shadow-xl: 0 4px 8px rgb(16 24 40 / 0.04), 0 24px 64px -12px rgb(16 24 40 / 0.28);
58
58
  --uv-shadow-glow: 0 0 0 1px color-mix(in oklab, var(--primary) 40%, transparent), 0 8px 24px -6px color-mix(in oklab, var(--primary) 45%, transparent);
59
59
 
60
+ /* The one corner radius of every component (cards, buttons, fields, badges...): change it here to restyle them all */
61
+ --uv-radius: 0.25rem;
62
+
60
63
  /*********************** Shadcn UI variables ***********************/
61
64
 
62
- --radius: 0.625rem;
63
65
  --background: var(--bg-main-light);
64
66
  --foreground: var(--text-primary-light);
65
67
  --card: var(--bg-card-light);
@@ -77,7 +79,8 @@ html {
77
79
  --accent: var(--bg-selected-light);
78
80
  --accent-foreground: var(--text-primary-light);
79
81
  --destructive: oklch(0.577 0.245 27.325);
80
- --success: oklch(0.627 0.17 149.2);
82
+ /* 5.8:1 on white and 5:1 on its own tint, so small success text meets WCAG AA (green-600 is only 3.3:1) */
83
+ --success: oklch(0.49 0.15 150.07);
81
84
  --warning: oklch(0.705 0.17 62.5);
82
85
  --info: oklch(0.588 0.19 256.8);
83
86
  --border: var(--border-light);
@@ -100,10 +103,12 @@ html {
100
103
  }
101
104
 
102
105
  @theme inline {
103
- --radius-sm: calc(var(--radius) - 4px);
104
- --radius-md: calc(var(--radius) - 2px);
105
- --radius-lg: var(--radius);
106
- --radius-xl: calc(var(--radius) + 4px);
106
+ /* Every radius step resolves to the shared corner radius, so rounded, rounded-sm... rounded-xl all match (rounded-full stays a circle) */
107
+ --radius: var(--uv-radius);
108
+ --radius-sm: var(--uv-radius);
109
+ --radius-md: var(--uv-radius);
110
+ --radius-lg: var(--uv-radius);
111
+ --radius-xl: var(--uv-radius);
107
112
  --color-background: var(--background);
108
113
  --color-foreground: var(--foreground);
109
114
  --color-card: var(--card);