@vendure-io/ui 2.0.0-beta.18 → 2.0.0-beta.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vendure-io/ui",
3
- "version": "2.0.0-beta.18",
3
+ "version": "2.0.0-beta.19",
4
4
  "description": "React component library for Vendure, built on shadcn/ui and Tailwind v4",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "homepage": "https://github.com/vendurehq/design/tree/main/packages/ui",
@@ -19,15 +19,26 @@ function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
19
19
  return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />
20
20
  }
21
21
 
22
+ // Field styling matches SelectTrigger, so a select-like combobox (the trigger
23
+ // shows the value, the search input sits in the popup) looks like the other
24
+ // form fields. ComboboxValue renders no element, so there is no value slot to
25
+ // style.
22
26
  function ComboboxTrigger({
23
27
  className,
28
+ size = "default",
24
29
  children,
25
30
  ...props
26
- }: ComboboxPrimitive.Trigger.Props) {
31
+ }: ComboboxPrimitive.Trigger.Props & {
32
+ size?: "sm" | "default"
33
+ }) {
27
34
  return (
28
35
  <ComboboxPrimitive.Trigger
29
36
  data-slot="combobox-trigger"
30
- className={cn("[&_svg:not([class*='size-'])]:size-4", className)}
37
+ data-size={size}
38
+ className={cn(
39
+ "border-input data-placeholder:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 flex w-fit items-center justify-between gap-1.5 rounded-md border bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 data-[size=default]:h-9 data-[size=sm]:h-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
40
+ className
41
+ )}
31
42
  {...props}
32
43
  >
33
44
  {children}
@@ -68,14 +79,18 @@ function ComboboxInput({
68
79
  />
69
80
  <InputGroupAddon align="inline-end">
70
81
  {showTrigger && (
82
+ // The bare primitive, not ComboboxTrigger: the icon button inside
83
+ // the input group must not take the field styling.
71
84
  <InputGroupButton
72
85
  size="icon-xs"
73
86
  variant="ghost"
74
- render={<ComboboxTrigger />}
87
+ render={<ComboboxPrimitive.Trigger />}
75
88
  data-slot="input-group-button"
76
89
  className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent"
77
90
  disabled={disabled}
78
- />
91
+ >
92
+ <ChevronDownIcon className="text-muted-foreground pointer-events-none size-4" />
93
+ </InputGroupButton>
79
94
  )}
80
95
  {showClear && <ComboboxClear disabled={disabled} />}
81
96
  </InputGroupAddon>
@@ -1,6 +1,8 @@
1
1
  "use client"
2
2
 
3
+ import { mergeProps } from "@base-ui/react/merge-props"
3
4
  import { Tabs as TabsPrimitive } from "@base-ui/react/tabs"
5
+ import { useRender } from "@base-ui/react/use-render"
4
6
  import { cva, type VariantProps } from "class-variance-authority"
5
7
 
6
8
  import { cn } from "@vendure-io/ui/lib/utils"
@@ -82,4 +84,65 @@ function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {
82
84
  )
83
85
  }
84
86
 
85
- export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
87
+ // Link tabs: section navigation where each tab is a route. They look like the
88
+ // line variant, but they are a <nav> of links with aria-current="page", not a
89
+ // tablist. Base UI Tabs would put role="tab" on a link and expect a panel for
90
+ // each tab, and the router already owns which section shows.
91
+ function TabsNav({ className, ...props }: React.ComponentProps<"nav">) {
92
+ return (
93
+ <nav
94
+ data-slot="tabs-nav"
95
+ className={cn(
96
+ // The padding keeps the focus ring and the underline inside the
97
+ // scroll box, which clips everything outside it.
98
+ "text-muted-foreground flex h-9 max-w-full items-center gap-1 overflow-x-auto p-[3px]",
99
+ className
100
+ )}
101
+ {...props}
102
+ />
103
+ )
104
+ }
105
+
106
+ // `active` sets aria-current="page". The style reads aria-current, so a router
107
+ // link that sets it on the active route (TanStack Router, React Router's
108
+ // NavLink) works through `render` without `active`.
109
+ function TabsLink({
110
+ className,
111
+ active,
112
+ render,
113
+ ...props
114
+ }: useRender.ComponentProps<"a"> & { active?: boolean }) {
115
+ return useRender({
116
+ defaultTagName: "a",
117
+ render,
118
+ props: mergeProps<"a">(
119
+ {
120
+ // Leave the attribute to the router link unless `active` is set.
121
+ ...(active !== undefined && {
122
+ "aria-current": active ? ("page" as const) : undefined,
123
+ }),
124
+ className: cn(
125
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-full shrink-0 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all focus-visible:ring-[3px] focus-visible:outline-1 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
126
+ "aria-[current=page]:text-foreground dark:aria-[current=page]:text-foreground",
127
+ // The underline sits on the bottom edge of the nav, like the line variant.
128
+ "after:bg-foreground after:absolute after:inset-x-0 after:bottom-[-4px] after:h-0.5 after:opacity-0 after:transition-opacity aria-[current=page]:after:opacity-100",
129
+ className
130
+ ),
131
+ },
132
+ props
133
+ ),
134
+ state: {
135
+ slot: "tabs-link",
136
+ },
137
+ })
138
+ }
139
+
140
+ export {
141
+ Tabs,
142
+ TabsList,
143
+ TabsTrigger,
144
+ TabsContent,
145
+ TabsNav,
146
+ TabsLink,
147
+ tabsListVariants,
148
+ }
@@ -6,12 +6,16 @@ import type * as React from 'react';
6
6
  // header and lets variance in the wild (back links, badge rows beside the title,
7
7
  // mono subtitles) live as free children rather than as an ever-growing prop set.
8
8
 
9
- /** The header row: title/description on the left, actions on the right. */
9
+ /**
10
+ * The header row: title/description on the left, actions on the right. The row
11
+ * wraps, so on a narrow screen the actions move below the title instead of
12
+ * squeezing it.
13
+ */
10
14
  function PageHeader({ className, ...props }: React.ComponentProps<'header'>) {
11
15
  return (
12
16
  <header
13
17
  data-slot="page-header"
14
- className={cn('flex items-start justify-between gap-4', className)}
18
+ className={cn('flex flex-wrap items-start justify-between gap-4', className)}
15
19
  {...props}
16
20
  />
17
21
  );
@@ -48,7 +52,7 @@ function PageHeaderDescription({ className, ...props }: React.ComponentProps<'p'
48
52
  );
49
53
  }
50
54
 
51
- /** Right-hand action slot (buttons, menus). Stays put as the title wraps. */
55
+ /** Right-hand action slot (buttons, menus). Moves below the title when the row has no room. */
52
56
  function PageHeaderActions({ className, ...props }: React.ComponentProps<'div'>) {
53
57
  return (
54
58
  <div
@@ -25,6 +25,12 @@ export interface EmptyStateProps extends Omit<React.ComponentProps<typeof Empty>
25
25
  description?: React.ReactNode;
26
26
  /** Actions/CTA — rendered in the content slot below the header. */
27
27
  children?: React.ReactNode;
28
+ /**
29
+ * Draw the dashed box. Defaults to `true`. Set `false` when the empty state
30
+ * fills a region that already has a frame, such as a table body: the table
31
+ * owns its perimeter.
32
+ */
33
+ bordered?: boolean;
28
34
  }
29
35
 
30
36
  /**
@@ -42,6 +48,7 @@ function EmptyState({
42
48
  title,
43
49
  description,
44
50
  children,
51
+ bordered = true,
45
52
  className,
46
53
  ...props
47
54
  }: EmptyStateProps) {
@@ -57,7 +64,11 @@ function EmptyState({
57
64
  ) : null;
58
65
 
59
66
  return (
60
- <Empty data-slot="empty-state" className={cn('border', className)} {...props}>
67
+ <Empty
68
+ data-slot="empty-state"
69
+ className={cn(bordered ? 'border' : 'rounded-none', className)}
70
+ {...props}
71
+ >
61
72
  <EmptyHeader>
62
73
  {resolvedIllustration ? (
63
74
  <EmptyMedia>{resolvedIllustration}</EmptyMedia>