@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
@@ -2,7 +2,7 @@
2
2
  import type { DropdownMenuSubTriggerProps } from 'reka-ui';
3
3
  import type { HTMLAttributes } from 'vue';
4
4
  import { reactiveOmit } from '@vueuse/core';
5
- import { ArrowRight01Icon } from '@hugeicons/core-free-icons';
5
+ import ArrowRight01Icon from '@hugeicons/core-free-icons/ArrowRight01Icon';
6
6
  import { HugeiconsIcon } from '@hugeicons/vue';
7
7
  import { DropdownMenuSubTrigger, useForwardProps } from 'reka-ui';
8
8
  import { cn } from '../../../lib/utils';
@@ -22,7 +22,7 @@ const forwardedProps = useForwardProps(delegatedProps);
22
22
  <DropdownMenuSubTrigger data-slot="dropdown-menu-sub-trigger" :data-inset="props.inset ? '' : undefined" v-bind="forwardedProps" :class="cn(
23
23
  'group/dropdown-sub-trigger',
24
24
  dropdownMenuItemClass,
25
- 'data-[state=open]:bg-accent data-[state=open]:text-foreground',
25
+ 'data-[state=open]:bg-accent data-[state=open]:text-foreground data-[state=open]:before:scale-y-100',
26
26
  props.class
27
27
  )
28
28
  ">
@@ -19,6 +19,8 @@ export const dropdownMenuItemClass = [
19
19
  'relative flex cursor-pointer items-center gap-3 rounded px-3 py-2 text-xs font-semibold text-muted-foreground outline-hidden select-none transition-colors duration-150 sm:text-sm',
20
20
  'data-highlighted:bg-accent data-highlighted:text-foreground data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-9',
21
21
  '[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
22
+ // A primary bar springs onto the highlighted item, like in the command palette
23
+ 'before:absolute before:inset-y-1.5 before:left-0 before:w-0.5 before:scale-y-0 before:rounded-full before:bg-primary before:transition-transform before:duration-300 before:ease-spring data-highlighted:before:scale-y-100',
22
24
  // Leading icons nudge slightly when the item is highlighted
23
25
  '[&>svg]:transition-transform [&>svg]:duration-300 [&>svg]:ease-spring [&[data-highlighted]>svg]:translate-x-0.5 [&[data-highlighted]>svg]:scale-110'
24
26
  ].join(' ');
@@ -3,12 +3,16 @@ import type { HTMLAttributes } from 'vue';
3
3
  import type { FieldVariants } from '.';
4
4
  import { cn } from '../../../lib/utils';
5
5
  import { fieldVariants } from '.';
6
+ import { provideFieldContext } from './field-context';
6
7
 
7
8
  // Props
8
9
  const props = defineProps<{
9
10
  class?: HTMLAttributes['class'];
10
11
  orientation?: FieldVariants['orientation'];
11
12
  }>();
13
+
14
+ // Let FieldLabel point to the control placed in this field
15
+ provideFieldContext();
12
16
  </script>
13
17
 
14
18
  <template>
@@ -2,17 +2,21 @@
2
2
  import type { HTMLAttributes } from 'vue';
3
3
  import { cn } from '../../../lib/utils';
4
4
  import { Label } from '../label';
5
+ import { useFieldLabel } from './field-context';
5
6
 
6
7
  // Props
7
8
  const props = defineProps<{
8
9
  class?: HTMLAttributes['class'];
9
10
  }>();
11
+
12
+ // Point to the control of the surrounding Field (an explicit for attribute still wins)
13
+ const { labelId, target: labelTarget } = useFieldLabel();
10
14
  </script>
11
15
 
12
16
  <template>
13
- <Label data-slot="field-label" :class="cn(
17
+ <Label :id="labelId" data-slot="field-label" :for="labelTarget" :class="cn(
14
18
  'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
15
- 'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border *:data-[slot=field]:p-4',
19
+ 'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded has-[>[data-slot=field]]:border *:data-[slot=field]:p-4',
16
20
  'has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10',
17
21
  props.class
18
22
  )
@@ -0,0 +1,62 @@
1
+ import type { InjectionKey, Ref } from 'vue';
2
+ import { computed, inject, onBeforeUnmount, provide, ref, useAttrs, useId } from 'vue';
3
+
4
+ interface FieldContext {
5
+ // Id of the control the field label points to (undefined until a control registers)
6
+ controlId: Ref<string | undefined>;
7
+ // Id of the field label, for controls that combine it with their own text
8
+ labelId: Ref<string | undefined>;
9
+ }
10
+
11
+ const fieldContextKey: InjectionKey<FieldContext> = Symbol('uv-field');
12
+
13
+ // Share the label target with the label and the control inside a Field
14
+ export const provideFieldContext = () => {
15
+ const context: FieldContext = { controlId: ref(undefined), labelId: ref(undefined) };
16
+ provide(fieldContextKey, context);
17
+ return context;
18
+ };
19
+
20
+ // Id the FieldLabel should point to, if the field has a control, and the id the label registers for itself
21
+ export const useFieldLabel = () => {
22
+ const field = inject(fieldContextKey, null);
23
+ const labelId = `uv-field-label-${useId()}`;
24
+ if (field && !field.labelId.value) {
25
+ field.labelId.value = labelId;
26
+ onBeforeUnmount(() => {
27
+ if (field.labelId.value === labelId) field.labelId.value = undefined;
28
+ });
29
+ }
30
+ return {
31
+ labelId: field ? labelId : undefined,
32
+ target: computed(() => field?.controlId.value)
33
+ };
34
+ };
35
+
36
+ // Id of the surrounding Field label, if any
37
+ export const useFieldLabelId = () => {
38
+ const field = inject(fieldContextKey, null);
39
+ return computed(() => field?.labelId.value);
40
+ };
41
+
42
+ // Give the first control inside a Field an id its FieldLabel points to (an explicit id on the control wins)
43
+ export const useFieldControlId = (explicitId?: () => string | undefined) => {
44
+ const field = inject(fieldContextKey, null);
45
+ const attrs = useAttrs();
46
+ const generatedId = `uv-field-control-${useId()}`;
47
+
48
+ // Only the first control claims the label, so composite controls (like DateTimePicker) don't fight over it
49
+ if (!field || field.controlId.value) {
50
+ return computed(() => explicitId?.());
51
+ }
52
+
53
+ const id = (explicitId?.() ?? (attrs.id as string | undefined)) || generatedId;
54
+ field.controlId.value = id;
55
+
56
+ // Release the label when the control goes away
57
+ onBeforeUnmount(() => {
58
+ if (field.controlId.value === id) field.controlId.value = undefined;
59
+ });
60
+
61
+ return computed(() => id);
62
+ };
@@ -29,6 +29,7 @@ export const fieldVariants: (props?: FieldVariants & ClassProp) => string = cva(
29
29
  }
30
30
  );
31
31
 
32
+ export { useFieldControlId } from './field-context';
32
33
  export { default as Field } from './Field.vue';
33
34
  export { default as FieldContent } from './FieldContent.vue';
34
35
  export { default as FieldDescription } from './FieldDescription.vue';
@@ -2,7 +2,16 @@
2
2
  import type { HTMLAttributes } from 'vue';
3
3
  import type { HugeiconsIconDefinition } from '../../../lib/common-types';
4
4
  import type { FileRejection } from '.';
5
- import { Cancel01Icon, CloudUploadIcon, Doc01Icon, File01Icon, FileZipIcon, Image01Icon, MusicNote01Icon, Pdf01Icon, Video01Icon, Xls01Icon } from '@hugeicons/core-free-icons';
5
+ import Cancel01Icon from '@hugeicons/core-free-icons/Cancel01Icon';
6
+ import CloudUploadIcon from '@hugeicons/core-free-icons/CloudUploadIcon';
7
+ import Doc01Icon from '@hugeicons/core-free-icons/Doc01Icon';
8
+ import File01Icon from '@hugeicons/core-free-icons/File01Icon';
9
+ import FileZipIcon from '@hugeicons/core-free-icons/FileZipIcon';
10
+ import Image01Icon from '@hugeicons/core-free-icons/Image01Icon';
11
+ import MusicNote01Icon from '@hugeicons/core-free-icons/MusicNote01Icon';
12
+ import Pdf01Icon from '@hugeicons/core-free-icons/Pdf01Icon';
13
+ import Video01Icon from '@hugeicons/core-free-icons/Video01Icon';
14
+ import Xls01Icon from '@hugeicons/core-free-icons/Xls01Icon';
6
15
  import { HugeiconsIcon } from '@hugeicons/vue';
7
16
  import { computed, onBeforeUnmount, ref } from 'vue';
8
17
  import { useI18n } from 'vue-i18n';
@@ -175,16 +184,16 @@ onBeforeUnmount(() => clearTimeout(rejectTimer));
175
184
  'group/dropzone relative isolate flex min-h-44 cursor-pointer select-none flex-col items-center justify-center gap-3 rounded border-2 border-dashed border-border bg-surface/50 px-6 py-8 text-center outline-none',
176
185
  '[transition:border-color_150ms,background-color_220ms,box-shadow_220ms,transform_420ms_var(--ease-spring)]',
177
186
  'hover:border-border-strong hover:bg-surface focus-visible:ring-[3px] focus-visible:ring-ring/45 active:scale-[0.99] active:duration-75',
178
- 'data-dragging:scale-[1.01] data-dragging:border-transparent data-dragging:bg-primary/5 data-dragging:shadow-[0_0_0_4px_color-mix(in_oklab,var(--primary)_12%,transparent)] data-dragging:hover:border-transparent data-dragging:hover:bg-primary/5',
187
+ 'data-dragging:scale-[1.01] data-dragging:border-transparent data-dragging:bg-primary/5 data-dragging:hover:border-transparent data-dragging:hover:bg-primary/5',
179
188
  'data-disabled:cursor-not-allowed data-disabled:opacity-50 data-disabled:hover:border-border data-disabled:hover:bg-surface/50 data-disabled:active:scale-100',
180
189
  isRejected && 'animate-uv-shake border-destructive/70'
181
190
  )" @click="openPicker" @keydown.enter.prevent="openPicker" @keydown.space.prevent="openPicker" @dragenter.prevent="onDragEnter" @dragover.prevent="onDragOver" @dragleave="onDragLeave" @drop.prevent="onDrop">
182
191
  <!-- Marching dashed border while dragging -->
183
192
  <span aria-hidden="true" class="uv-dropzone-ants pointer-events-none absolute -inset-0.5 rounded opacity-0 transition-opacity duration-150 group-data-dragging/dropzone:opacity-100" />
184
193
 
185
- <!-- Upload icon (floats up and grows while dragging) -->
186
- <span aria-hidden="true" class="relative flex size-14 items-center justify-center rounded border border-border bg-card text-muted-foreground shadow-elevated-sm transition-[translate,scale,color,border-color,box-shadow] duration-420 ease-bounce group-hover/dropzone:-translate-y-0.5 group-hover/dropzone:text-foreground group-data-dragging/dropzone:-translate-y-2 group-data-dragging/dropzone:scale-115 group-data-dragging/dropzone:border-primary/50 group-data-dragging/dropzone:text-primary group-data-dragging/dropzone:shadow-glow">
187
- <HugeiconsIcon :icon="CloudUploadIcon" class="size-6 group-data-dragging/dropzone:animate-uv-float" />
194
+ <!-- Upload icon (lifts and grows while dragging) -->
195
+ <span aria-hidden="true" class="relative flex size-14 items-center justify-center rounded border border-border bg-card text-muted-foreground shadow-elevated-sm transition-[translate,scale,color,border-color,box-shadow] duration-420 ease-bounce group-hover/dropzone:-translate-y-0.5 group-hover/dropzone:text-foreground group-data-dragging/dropzone:-translate-y-2 group-data-dragging/dropzone:scale-115 group-data-dragging/dropzone:border-primary/50 group-data-dragging/dropzone:text-primary group-data-dragging/dropzone:shadow-elevated-md">
196
+ <HugeiconsIcon :icon="CloudUploadIcon" class="size-6" />
188
197
  </span>
189
198
 
190
199
  <!-- Texts -->
@@ -2,6 +2,7 @@
2
2
  import type { HTMLAttributes } from 'vue';
3
3
  import { useVModel } from '@vueuse/core';
4
4
  import { cn } from '../../../lib/utils';
5
+ import { useFieldControlId } from '../field/field-context';
5
6
 
6
7
  // Props
7
8
  const props = defineProps<{
@@ -19,10 +20,13 @@ const modelValue = useVModel(props, 'modelValue', emits, {
19
20
  passive: true,
20
21
  defaultValue: props.defaultValue
21
22
  });
23
+
24
+ // Let a surrounding Field label point to this control
25
+ const fieldControlId = useFieldControlId();
22
26
  </script>
23
27
 
24
28
  <template>
25
- <input v-model="modelValue" data-slot="input" :class="cn(
29
+ <input :id="fieldControlId" v-model="modelValue" data-slot="input" :class="cn(
26
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',
27
31
  'selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground placeholder:opacity-60 placeholder:transition-opacity focus-visible:placeholder:opacity-40',
28
32
  'file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-semibold file:text-foreground',
@@ -2,9 +2,10 @@
2
2
  import type { AcceptableValue } from 'reka-ui';
3
3
  import type { HTMLAttributes } from 'vue';
4
4
  import { reactiveOmit, useVModel } from '@vueuse/core';
5
- import { ArrowDown01Icon } from '@hugeicons/core-free-icons';
5
+ import ArrowDown01Icon from '@hugeicons/core-free-icons/ArrowDown01Icon';
6
6
  import { HugeiconsIcon } from '@hugeicons/vue';
7
7
  import { cn } from '../../../lib/utils';
8
+ import { useFieldControlId } from '../field/field-context';
8
9
 
9
10
  defineOptions({
10
11
  inheritAttrs: false
@@ -24,12 +25,15 @@ const modelValue = useVModel(props, 'modelValue', emit, {
24
25
  });
25
26
 
26
27
  const delegatedProps = reactiveOmit(props, 'class');
28
+
29
+ // Let a surrounding Field label point to this control
30
+ const fieldControlId = useFieldControlId();
27
31
  </script>
28
32
 
29
33
  <template>
30
34
  <div class="group/native-select relative w-fit has-[select:disabled]:opacity-50" data-slot="native-select-wrapper">
31
- <select v-bind="{ ...$attrs, ...delegatedProps }" v-model="modelValue" data-slot="native-select" :class="cn(
32
- 'border-input placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 dark:hover:bg-input/50 h-9 w-full min-w-0 appearance-none rounded-md border bg-transparent px-3 py-2 pr-9 text-sm shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed',
35
+ <select :id="fieldControlId" v-bind="{ ...$attrs, ...delegatedProps }" v-model="modelValue" data-slot="native-select" :class="cn(
36
+ 'border-input placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 dark:hover:bg-input/50 h-9 w-full min-w-0 appearance-none rounded border bg-transparent px-3 py-2 pr-9 text-sm shadow-elevated-sm transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed',
33
37
  'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
34
38
  'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
35
39
  props.class
@@ -3,11 +3,13 @@ import type { NumberFieldRootEmits, NumberFieldRootProps } from 'reka-ui';
3
3
  import type { HTMLAttributes } from 'vue';
4
4
  import { computed, nextTick, ref, watch } from 'vue';
5
5
  import { reactiveOmit } from '@vueuse/core';
6
- import { MinusSignIcon, PlusSignIcon } from '@hugeicons/core-free-icons';
6
+ import MinusSignIcon from '@hugeicons/core-free-icons/MinusSignIcon';
7
+ import PlusSignIcon from '@hugeicons/core-free-icons/PlusSignIcon';
7
8
  import { HugeiconsIcon } from '@hugeicons/vue';
8
9
  import { NumberFieldDecrement, NumberFieldIncrement, NumberFieldInput, NumberFieldRoot, useForwardPropsEmits } from 'reka-ui';
9
10
  import { useI18n } from 'vue-i18n';
10
11
  import { cn } from '../../../lib/utils';
12
+ import { useFieldControlId } from '../field/field-context';
11
13
 
12
14
  // Props
13
15
  const props = defineProps<NumberFieldRootProps & {
@@ -77,10 +79,13 @@ const animateChange = async (value: number) => {
77
79
  };
78
80
  }
79
81
  };
82
+
83
+ // Let a surrounding Field label point to this control
84
+ const fieldControlId = useFieldControlId(() => props.id);
80
85
  </script>
81
86
 
82
87
  <template>
83
- <NumberFieldRoot v-slot="slotProps" data-slot="number-field" v-bind="forwarded" :class="cn(
88
+ <NumberFieldRoot v-slot="slotProps" data-slot="number-field" v-bind="forwarded" :id="fieldControlId" :class="cn(
84
89
  'uv-field flex h-13 w-full items-center gap-1 rounded border border-input bg-secondary px-1.5 text-foreground shadow-elevated-sm',
85
90
  'data-disabled:cursor-not-allowed data-disabled:opacity-60 aria-invalid:animate-uv-shake',
86
91
  props.class
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import type { PaginationEllipsisProps } from 'reka-ui';
3
3
  import type { HTMLAttributes } from 'vue';
4
- import { MoreHorizontalIcon } from '@hugeicons/core-free-icons';
4
+ import MoreHorizontalIcon from '@hugeicons/core-free-icons/MoreHorizontalIcon';
5
5
  import { HugeiconsIcon } from '@hugeicons/vue';
6
6
  import { reactiveOmit } from '@vueuse/core';
7
7
  import { PaginationEllipsis } from 'reka-ui';
@@ -2,7 +2,7 @@
2
2
  import type { PaginationFirstProps } from 'reka-ui';
3
3
  import type { HTMLAttributes } from 'vue';
4
4
  import type { ButtonVariants } from '../button';
5
- import { ArrowLeftDoubleIcon } from '@hugeicons/core-free-icons';
5
+ import ArrowLeftDoubleIcon from '@hugeicons/core-free-icons/ArrowLeftDoubleIcon';
6
6
  import { HugeiconsIcon } from '@hugeicons/vue';
7
7
  import { reactiveOmit } from '@vueuse/core';
8
8
  import { PaginationFirst } from 'reka-ui';
@@ -25,7 +25,7 @@ const { t } = useI18n();
25
25
  <PaginationListItem data-slot="pagination-item" :aria-label="t('uiVintage.pagination.page', { page: props.value })" v-bind="delegatedProps" :class="cn(
26
26
  buttonVariants({ variant: 'secondary', size: props.size }),
27
27
  'tabular-nums',
28
- 'data-selected:border-primary data-selected:bg-primary data-selected:text-primary-foreground data-selected:shadow-glow data-selected:hover:border-primary data-selected:hover:bg-primary-hover',
28
+ 'data-selected:border-primary data-selected:bg-primary data-selected:text-primary-foreground data-selected:hover:border-primary data-selected:hover:bg-primary-hover',
29
29
  'data-selected:animate-[uv-pagination-pop_380ms_var(--ease-bounce)]',
30
30
  props.class
31
31
  )">
@@ -2,7 +2,7 @@
2
2
  import type { PaginationLastProps } from 'reka-ui';
3
3
  import type { HTMLAttributes } from 'vue';
4
4
  import type { ButtonVariants } from '../button';
5
- import { ArrowRightDoubleIcon } from '@hugeicons/core-free-icons';
5
+ import ArrowRightDoubleIcon from '@hugeicons/core-free-icons/ArrowRightDoubleIcon';
6
6
  import { HugeiconsIcon } from '@hugeicons/vue';
7
7
  import { reactiveOmit } from '@vueuse/core';
8
8
  import { PaginationLast } from 'reka-ui';
@@ -2,7 +2,7 @@
2
2
  import type { PaginationNextProps } from 'reka-ui';
3
3
  import type { HTMLAttributes } from 'vue';
4
4
  import type { ButtonVariants } from '../button';
5
- import { ArrowRight01Icon } from '@hugeicons/core-free-icons';
5
+ import ArrowRight01Icon from '@hugeicons/core-free-icons/ArrowRight01Icon';
6
6
  import { HugeiconsIcon } from '@hugeicons/vue';
7
7
  import { reactiveOmit } from '@vueuse/core';
8
8
  import { PaginationNext } from 'reka-ui';
@@ -2,7 +2,7 @@
2
2
  import type { PaginationPrevProps } from 'reka-ui';
3
3
  import type { HTMLAttributes } from 'vue';
4
4
  import type { ButtonVariants } from '../button';
5
- import { ArrowLeft01Icon } from '@hugeicons/core-free-icons';
5
+ import ArrowLeft01Icon from '@hugeicons/core-free-icons/ArrowLeft01Icon';
6
6
  import { HugeiconsIcon } from '@hugeicons/vue';
7
7
  import { reactiveOmit } from '@vueuse/core';
8
8
  import { PaginationPrev } from 'reka-ui';
@@ -34,7 +34,7 @@ const isInvalid = computed(() => pinInputContext?.invalid.value || undefined);
34
34
  <span data-slot="pin-input-cell" class="relative inline-flex shrink-0">
35
35
  <!-- Native input (text is transparent, the overlay below renders the character) -->
36
36
  <PinInputInput data-slot="pin-input-input" v-bind="{ ...$attrs, ...forwardedProps }" :aria-invalid="isInvalid" :data-filled="isFilled ? '' : undefined" :class="cn(
37
- 'uv-field size-12 rounded border border-input bg-secondary text-center text-lg font-bold text-transparent caret-primary shadow-elevated-sm outline-none sm:size-13',
37
+ 'uv-field size-12 rounded border border-input bg-secondary text-center text-lg font-semibold text-transparent caret-primary shadow-elevated-sm outline-none sm:size-13',
38
38
  'placeholder:text-muted-foreground placeholder:opacity-50 selection:bg-primary/25',
39
39
  'data-filled:border-primary/60 data-filled:bg-primary/5',
40
40
  'disabled:cursor-not-allowed disabled:opacity-60',
@@ -42,7 +42,7 @@ const isInvalid = computed(() => pinInputContext?.invalid.value || undefined);
42
42
  )" />
43
43
 
44
44
  <!-- Animated character (re-keyed on every value so it pops) -->
45
- <span v-if="isFilled" :key="cellValue" aria-hidden="true" class="pointer-events-none absolute inset-0 flex animate-uv-pop items-center justify-center text-lg font-bold text-foreground">
45
+ <span v-if="isFilled" :key="cellValue" aria-hidden="true" class="pointer-events-none absolute inset-0 flex animate-uv-pop items-center justify-center text-lg font-semibold text-foreground">
46
46
  {{ displayValue }}
47
47
  </span>
48
48
  </span>
@@ -26,7 +26,7 @@ const percentage = computed(() => {
26
26
  </script>
27
27
 
28
28
  <template>
29
- <ProgressRoot data-slot="progress" v-bind="delegatedProps" :class="cn(
29
+ <ProgressRoot data-slot="progress" v-bind="delegatedProps" :model-value="props.indeterminate ? null : props.modelValue" :class="cn(
30
30
  'relative h-2 w-full overflow-hidden rounded-sm bg-muted',
31
31
  props.class
32
32
  )">
@@ -34,6 +34,6 @@ const percentage = computed(() => {
34
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" />
35
35
 
36
36
  <!-- 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 after:absolute after:inset-0 after:animate-uv-shimmer after:bg-[linear-gradient(90deg,transparent,rgb(255_255_255/0.35),transparent)]" :style="`transform: translateX(-${100 - percentage}%);`" />
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}%);`" />
38
38
  </ProgressRoot>
39
39
  </template>
@@ -23,7 +23,7 @@ const props = defineProps<{
23
23
  'group/radio-card relative flex w-full cursor-pointer items-center gap-3 rounded border border-border bg-card px-4 py-3 text-left shadow-elevated-sm outline-none',
24
24
  '[transition:scale_320ms_var(--ease-spring),border-color_150ms_var(--ease-snappy),background-color_150ms_var(--ease-snappy),box-shadow_200ms_var(--ease-snappy)]',
25
25
  'hover:border-border-strong active:scale-[0.985] focus-visible:ring-[3px] focus-visible:ring-ring/45',
26
- 'data-[state=checked]:border-primary data-[state=checked]:bg-primary/5 data-[state=checked]:shadow-glow data-[state=checked]:hover:border-primary',
26
+ 'data-[state=checked]:border-primary data-[state=checked]:bg-primary/5 data-[state=checked]:hover:border-primary',
27
27
  'disabled:cursor-not-allowed disabled:opacity-60 disabled:active:scale-100',
28
28
  props.class
29
29
  )">
@@ -2,7 +2,7 @@
2
2
  import type { SelectItemProps } from 'reka-ui';
3
3
  import type { HTMLAttributes } from 'vue';
4
4
  import { reactiveOmit } from '@vueuse/core';
5
- import { Tick02Icon } from '@hugeicons/core-free-icons';
5
+ import Tick02Icon from '@hugeicons/core-free-icons/Tick02Icon';
6
6
  import { HugeiconsIcon } from '@hugeicons/vue';
7
7
  import { SelectItem, SelectItemIndicator, SelectItemText, useForwardProps } from 'reka-ui';
8
8
  import { cn } from '../../../lib/utils';
@@ -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-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',
20
20
  props.class
21
21
  )
22
22
  ">
@@ -36,7 +36,7 @@ const props = defineProps<{
36
36
 
37
37
  <!-- Description -->
38
38
  <span v-if="props.description" data-slot="select-item-content-description" :class="cn(
39
- 'font-regular text-[11px] sm:text-xs leading-4 sm:leading-5 text-muted-foreground',
39
+ 'font-normal text-[11px] sm:text-xs leading-4 sm:leading-5 text-muted-foreground',
40
40
  props.descriptionClass
41
41
  )
42
42
  ">
@@ -2,7 +2,7 @@
2
2
  import type { SelectScrollDownButtonProps } from 'reka-ui';
3
3
  import type { HTMLAttributes } from 'vue';
4
4
  import { reactiveOmit } from '@vueuse/core';
5
- import { ArrowDown01Icon } from '@hugeicons/core-free-icons';
5
+ import ArrowDown01Icon from '@hugeicons/core-free-icons/ArrowDown01Icon';
6
6
  import { HugeiconsIcon } from '@hugeicons/vue';
7
7
  import { SelectScrollDownButton, useForwardProps } from 'reka-ui';
8
8
  import { cn } from '../../../lib/utils';
@@ -2,7 +2,7 @@
2
2
  import type { SelectScrollUpButtonProps } from 'reka-ui';
3
3
  import type { HTMLAttributes } from 'vue';
4
4
  import { reactiveOmit } from '@vueuse/core';
5
- import { ArrowUp01Icon } from '@hugeicons/core-free-icons';
5
+ import ArrowUp01Icon from '@hugeicons/core-free-icons/ArrowUp01Icon';
6
6
  import { HugeiconsIcon } from '@hugeicons/vue';
7
7
  import { SelectScrollUpButton, useForwardProps } from 'reka-ui';
8
8
  import { cn } from '../../../lib/utils';
@@ -2,10 +2,11 @@
2
2
  import type { SelectTriggerProps } from 'reka-ui';
3
3
  import type { HTMLAttributes } from 'vue';
4
4
  import { reactiveOmit } from '@vueuse/core';
5
- import { ArrowDown01Icon } from '@hugeicons/core-free-icons';
5
+ import ArrowDown01Icon from '@hugeicons/core-free-icons/ArrowDown01Icon';
6
6
  import { HugeiconsIcon } from '@hugeicons/vue';
7
7
  import { SelectIcon, SelectTrigger, useForwardProps } from 'reka-ui';
8
8
  import { cn } from '../../../lib/utils';
9
+ import { useFieldControlId } from '../field/field-context';
9
10
 
10
11
  // Props
11
12
  const props = withDefaults(defineProps<SelectTriggerProps & {
@@ -15,10 +16,13 @@ const props = withDefaults(defineProps<SelectTriggerProps & {
15
16
 
16
17
  const delegatedProps = reactiveOmit(props, 'class', 'size');
17
18
  const forwardedProps = useForwardProps(delegatedProps);
19
+
20
+ // Let a surrounding Field label point to this control
21
+ const fieldControlId = useFieldControlId();
18
22
  </script>
19
23
 
20
24
  <template>
21
- <SelectTrigger data-slot="select-trigger" :data-size="size" v-bind="forwardedProps" :class="cn(
25
+ <SelectTrigger :id="fieldControlId" data-slot="select-trigger" :data-size="size" v-bind="forwardedProps" :class="cn(
22
26
  'group/select-trigger uv-field flex w-fit cursor-pointer items-center justify-between gap-2 rounded border border-input bg-secondary px-4 py-3 text-xs font-semibold whitespace-nowrap text-foreground shadow-elevated-sm outline-none data-placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-60 data-[size=default]:h-13 data-[size=sm]:h-10 sm:text-sm',
23
27
  '*:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2',
24
28
  '[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 [&_svg:not([class*=\'text-\'])]:text-muted-foreground',
@@ -78,7 +78,7 @@ function handleMobileSidebarClick(event: MouseEvent) {
78
78
  props.class
79
79
  )
80
80
  " v-bind="$attrs">
81
- <div data-sidebar="sidebar" class="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm">
81
+ <div data-sidebar="sidebar" class="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded group-data-[variant=floating]:border group-data-[variant=floating]:shadow-elevated-sm">
82
82
  <slot />
83
83
  </div>
84
84
  </div>
@@ -10,7 +10,7 @@ const props = defineProps<PrimitiveProps & { class?: HTMLAttributes['class'] }>(
10
10
 
11
11
  <template>
12
12
  <Primitive data-slot="sidebar-group-action" data-sidebar="group-action" :as="as" :as-child="asChild" :class="cn(
13
- 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
13
+ 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
14
14
  'after:absolute after:-inset-2 md:after:hidden',
15
15
  'group-data-[collapsible=icon]:hidden',
16
16
  props.class
@@ -10,7 +10,7 @@ const props = defineProps<PrimitiveProps & { class?: HTMLAttributes['class'] }>(
10
10
 
11
11
  <template>
12
12
  <Primitive data-slot="sidebar-group-label" data-sidebar="group-label" :as="as" :as-child="asChild" :class="cn(
13
- 'text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-semibold outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 group-data-[compact=true]/sidebar-wrapper:h-7 group-data-[compact=true]/sidebar-wrapper:px-1.5 [&>svg]:size-4 [&>svg]:shrink-0',
13
+ 'text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded px-2 text-xs font-semibold outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 group-data-[compact=true]/sidebar-wrapper:h-7 group-data-[compact=true]/sidebar-wrapper:px-1.5 [&>svg]:size-4 [&>svg]:shrink-0',
14
14
  'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
15
15
  props.class
16
16
  )
@@ -11,7 +11,7 @@ const props = defineProps<{
11
11
  <template>
12
12
  <main data-slot="sidebar-inset" :class="cn(
13
13
  'bg-background relative flex w-full flex-1 flex-col',
14
- 'md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2',
14
+ 'md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded md:peer-data-[variant=inset]:shadow-elevated-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2',
15
15
  props.class
16
16
  )
17
17
  ">
@@ -15,7 +15,7 @@ const props = withDefaults(defineProps<PrimitiveProps & {
15
15
 
16
16
  <template>
17
17
  <Primitive data-slot="sidebar-menu-action" data-sidebar="menu-action" :class="cn(
18
- 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
18
+ 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
19
19
  'after:absolute after:-inset-2 md:after:hidden',
20
20
  'peer-data-[size=sm]/menu-button:top-1',
21
21
  'peer-data-[size=default]/menu-button:top-1.5',
@@ -10,7 +10,7 @@ const props = defineProps<{
10
10
 
11
11
  <template>
12
12
  <div data-slot="sidebar-menu-badge" data-sidebar="menu-badge" :class="cn(
13
- 'text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-semibold tabular-nums select-none',
13
+ 'text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded px-1 text-xs font-semibold tabular-nums select-none',
14
14
  'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
15
15
  'peer-data-[size=sm]/menu-button:top-1',
16
16
  'peer-data-[size=default]/menu-button:top-1.5',
@@ -14,11 +14,11 @@ const width = '72%';
14
14
 
15
15
  <template>
16
16
  <div data-slot="sidebar-menu-skeleton" data-sidebar="menu-skeleton" :class="cn(
17
- 'flex h-8 items-center gap-2 rounded-md px-2 group-data-[compact=true]/sidebar-wrapper:h-7 group-data-[compact=true]/sidebar-wrapper:gap-1.5 group-data-[compact=true]/sidebar-wrapper:px-1.5',
17
+ 'flex h-8 items-center gap-2 rounded px-2 group-data-[compact=true]/sidebar-wrapper:h-7 group-data-[compact=true]/sidebar-wrapper:gap-1.5 group-data-[compact=true]/sidebar-wrapper:px-1.5',
18
18
  props.class
19
19
  )
20
20
  ">
21
- <Skeleton v-if="showIcon" class="size-4 rounded-md group-data-[compact=true]/sidebar-wrapper:size-3.5" data-sidebar="menu-skeleton-icon" />
21
+ <Skeleton v-if="showIcon" class="size-4 rounded group-data-[compact=true]/sidebar-wrapper:size-3.5" data-sidebar="menu-skeleton-icon" />
22
22
 
23
23
  <Skeleton class="h-4 max-w-(--skeleton-width) flex-1" data-sidebar="menu-skeleton-text" :style="{ '--skeleton-width': width }" />
24
24
  </div>
@@ -17,7 +17,7 @@ const props = withDefaults(defineProps<PrimitiveProps & {
17
17
 
18
18
  <template>
19
19
  <Primitive data-slot="sidebar-menu-sub-button" data-sidebar="menu-sub-button" :as="as" :as-child="asChild" :data-size="size" :data-active="isActive" :class="cn(
20
- 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex min-h-7 min-w-0 -translate-x-px items-center gap-2 rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 group-data-[compact=true]/sidebar-wrapper:min-h-6 group-data-[compact=true]/sidebar-wrapper:gap-1.5 group-data-[compact=true]/sidebar-wrapper:px-1.5 group-data-[compact=true]/sidebar-wrapper:[&>svg]:size-3.5 [&>svg]:size-4 [&>svg]:shrink-0',
20
+ 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex min-h-7 min-w-0 -translate-x-px items-center gap-2 rounded px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 group-data-[compact=true]/sidebar-wrapper:min-h-6 group-data-[compact=true]/sidebar-wrapper:gap-1.5 group-data-[compact=true]/sidebar-wrapper:px-1.5 group-data-[compact=true]/sidebar-wrapper:[&>svg]:size-3.5 [&>svg]:size-4 [&>svg]:shrink-0',
21
21
  'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
22
22
  size === 'sm' && 'text-xs',
23
23
  size === 'md' &&
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import type { HTMLAttributes, Ref } from 'vue';
3
- import { defaultDocument, useEventListener, useMediaQuery, useVModel } from '@vueuse/core';
3
+ import { useEventListener, useMediaQuery, useMounted, useVModel } from '@vueuse/core';
4
+ import { useCookie } from 'nuxt/app';
4
5
  import { TooltipProvider } from 'reka-ui';
5
6
  import { computed, ref } from 'vue';
6
7
  import { cn } from '../../../lib/utils';
@@ -13,7 +14,7 @@ const props = withDefaults(defineProps<{
13
14
  compact?: boolean;
14
15
  class?: HTMLAttributes['class'];
15
16
  }>(), {
16
- defaultOpen: !defaultDocument?.cookie.includes(`${SIDEBAR_COOKIE_NAME}=false`),
17
+ defaultOpen: undefined,
17
18
  open: undefined,
18
19
  compact: false
19
20
  });
@@ -23,19 +24,29 @@ const emits = defineEmits<{
23
24
  'update:open': [open: boolean];
24
25
  }>();
25
26
 
26
- const isMobile = useMediaQuery('(max-width: 768px)');
27
+ // Remembered open state, read per instance on both server and client so the first render matches
28
+ const sidebarCookie = useCookie<boolean | null>(SIDEBAR_COOKIE_NAME, {
29
+ default: () => null,
30
+ maxAge: SIDEBAR_COOKIE_MAX_AGE,
31
+ path: '/'
32
+ });
33
+
34
+ // The mobile layout only exists in the browser: render the desktop markup until hydration is done
35
+ const isMounted = useMounted();
36
+ const matchesMobile = useMediaQuery('(max-width: 768px)');
37
+ const isMobile = computed(() => isMounted.value && matchesMobile.value);
27
38
  const openMobile = ref(false);
28
39
 
29
40
  const open = useVModel(props, 'open', emits, {
30
- defaultValue: props.defaultOpen ?? false,
41
+ defaultValue: props.defaultOpen ?? sidebarCookie.value !== false,
31
42
  passive: (props.open === undefined) as false
32
43
  }) as Ref<boolean>;
33
44
 
34
45
  function setOpen(value: boolean) {
35
46
  open.value = value; // emits('update:open', value)
36
47
 
37
- // This sets the cookie to keep the sidebar state.
38
- document.cookie = `${SIDEBAR_COOKIE_NAME}=${open.value}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
48
+ // Keep the sidebar state for the next visit
49
+ sidebarCookie.value = value;
39
50
  }
40
51
 
41
52
  function setOpenMobile(value: boolean) {
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import type { HTMLAttributes } from 'vue';
3
- import { PanelLeftIcon } from '@hugeicons/core-free-icons';
3
+ import PanelLeftIcon from '@hugeicons/core-free-icons/PanelLeftIcon';
4
4
  import { HugeiconsIcon } from '@hugeicons/vue';
5
5
  import { useI18n } from 'vue-i18n';
6
6
  import { cn } from '../../../lib/utils';