@spree/dashboard-ui 0.1.0
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/LICENSE +21 -0
- package/README.md +26 -0
- package/package.json +86 -0
- package/src/hooks/use-copy-to-clipboard.ts +44 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/use-scrolled.ts +30 -0
- package/src/index.ts +99 -0
- package/src/lib/date-locale.ts +29 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/validation-messages.ts +19 -0
- package/src/spree/address-block.tsx +74 -0
- package/src/spree/back-button.tsx +48 -0
- package/src/spree/bulk-dialog.tsx +71 -0
- package/src/spree/bulk-price-table.tsx +254 -0
- package/src/spree/calculator-summary.tsx +120 -0
- package/src/spree/color-picker.tsx +202 -0
- package/src/spree/confirm-dialog.tsx +82 -0
- package/src/spree/copy-to-clipboard-button.tsx +90 -0
- package/src/spree/country-flag.tsx +30 -0
- package/src/spree/data-grid/cells.tsx +415 -0
- package/src/spree/data-grid/context.ts +59 -0
- package/src/spree/data-grid/data-grid.tsx +214 -0
- package/src/spree/data-grid/fill-handle.tsx +153 -0
- package/src/spree/data-grid/helpers.ts +18 -0
- package/src/spree/data-grid/index.ts +5 -0
- package/src/spree/data-grid/types.ts +36 -0
- package/src/spree/data-grid/use-data-grid-keyboard.ts +254 -0
- package/src/spree/data-grid/use-stable-cell-registration.ts +45 -0
- package/src/spree/drag-handle.tsx +51 -0
- package/src/spree/form-actions.tsx +140 -0
- package/src/spree/json-preview-drawer.tsx +254 -0
- package/src/spree/json-value-view.tsx +149 -0
- package/src/spree/language-menu-items.tsx +54 -0
- package/src/spree/metadata/metadata-card.tsx +62 -0
- package/src/spree/relative-time.tsx +49 -0
- package/src/spree/resource-combobox.tsx +108 -0
- package/src/spree/resource-layout.tsx +31 -0
- package/src/spree/resource-multi-autocomplete.tsx +180 -0
- package/src/spree/resource-name-cell.tsx +44 -0
- package/src/spree/route-error-boundary.tsx +102 -0
- package/src/spree/row-actions.tsx +85 -0
- package/src/spree/row-click-bridge.tsx +17 -0
- package/src/spree/secret-input.tsx +137 -0
- package/src/spree/storefront-visible-switch.tsx +58 -0
- package/src/spree/tag-list.tsx +26 -0
- package/src/spree/theme-provider.tsx +83 -0
- package/src/spree/theme-toggle.tsx +35 -0
- package/src/styles.css +338 -0
- package/src/ui/attachment.tsx +187 -0
- package/src/ui/avatar.tsx +94 -0
- package/src/ui/badge.tsx +161 -0
- package/src/ui/breadcrumb.tsx +109 -0
- package/src/ui/button.tsx +60 -0
- package/src/ui/calendar.tsx +66 -0
- package/src/ui/card.tsx +72 -0
- package/src/ui/chart.tsx +94 -0
- package/src/ui/checkbox.tsx +50 -0
- package/src/ui/collapsible.tsx +18 -0
- package/src/ui/combobox.tsx +270 -0
- package/src/ui/command.tsx +172 -0
- package/src/ui/data-table.tsx +60 -0
- package/src/ui/date-picker.tsx +253 -0
- package/src/ui/date-range-picker.tsx +150 -0
- package/src/ui/dialog.tsx +214 -0
- package/src/ui/dropdown-menu.tsx +345 -0
- package/src/ui/empty.tsx +94 -0
- package/src/ui/field.tsx +221 -0
- package/src/ui/input-group.tsx +141 -0
- package/src/ui/input.tsx +19 -0
- package/src/ui/label.tsx +29 -0
- package/src/ui/pagination.tsx +123 -0
- package/src/ui/popover.tsx +76 -0
- package/src/ui/progress.tsx +73 -0
- package/src/ui/radio-group.tsx +42 -0
- package/src/ui/rich-text-editor.tsx +263 -0
- package/src/ui/select.tsx +213 -0
- package/src/ui/separator.tsx +31 -0
- package/src/ui/sheet.tsx +208 -0
- package/src/ui/sidebar.tsx +679 -0
- package/src/ui/skeleton.tsx +13 -0
- package/src/ui/slot.tsx +35 -0
- package/src/ui/sonner.tsx +47 -0
- package/src/ui/switch.tsx +39 -0
- package/src/ui/textarea.tsx +18 -0
- package/src/ui/tooltip.tsx +100 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import { cn } from '../lib/utils'
|
|
4
|
+
import { Button } from './button'
|
|
5
|
+
import { Input } from './input'
|
|
6
|
+
import { Textarea } from './textarea'
|
|
7
|
+
|
|
8
|
+
function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
data-slot="input-group"
|
|
12
|
+
role="group"
|
|
13
|
+
className={cn(
|
|
14
|
+
'group/input-group relative flex w-full min-w-0 items-center rounded-lg border border-border bg-card text-foreground shadow-xs transition-all duration-100 ease-in-out outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:shadow-none has-disabled:bg-muted has-disabled:border-border has-disabled:opacity-50 has-disabled:shadow-none has-[[data-slot=input-group-control]:focus-visible]:border-blue-500 has-[[data-slot=input-group-control]:focus-visible]:shadow-[0_0_0_3px_rgba(59,130,246,0.15)] has-[[data-slot][aria-invalid=true]]:border-destructive has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5',
|
|
15
|
+
className,
|
|
16
|
+
)}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const inputGroupAddonVariants = cva(
|
|
23
|
+
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
|
|
24
|
+
{
|
|
25
|
+
variants: {
|
|
26
|
+
align: {
|
|
27
|
+
'inline-start': 'order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]',
|
|
28
|
+
'inline-end': 'order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]',
|
|
29
|
+
'block-start':
|
|
30
|
+
'order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2',
|
|
31
|
+
'block-end':
|
|
32
|
+
'order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
defaultVariants: {
|
|
36
|
+
align: 'inline-start',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
function InputGroupAddon({
|
|
42
|
+
className,
|
|
43
|
+
align = 'inline-start',
|
|
44
|
+
...props
|
|
45
|
+
}: React.ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>) {
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
role="group"
|
|
49
|
+
data-slot="input-group-addon"
|
|
50
|
+
data-align={align}
|
|
51
|
+
className={cn(inputGroupAddonVariants({ align }), className)}
|
|
52
|
+
onClick={(e) => {
|
|
53
|
+
if ((e.target as HTMLElement).closest('button')) {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
e.currentTarget.parentElement?.querySelector('input')?.focus()
|
|
57
|
+
}}
|
|
58
|
+
{...props}
|
|
59
|
+
/>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const inputGroupButtonVariants = cva('flex items-center gap-2 text-sm shadow-none', {
|
|
64
|
+
variants: {
|
|
65
|
+
size: {
|
|
66
|
+
xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
|
|
67
|
+
sm: '',
|
|
68
|
+
'icon-xs': 'size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0',
|
|
69
|
+
'icon-sm': 'size-8 p-0 has-[>svg]:p-0',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
defaultVariants: {
|
|
73
|
+
size: 'xs',
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
function InputGroupButton({
|
|
78
|
+
className,
|
|
79
|
+
type = 'button',
|
|
80
|
+
variant = 'ghost',
|
|
81
|
+
size = 'xs',
|
|
82
|
+
...props
|
|
83
|
+
}: Omit<React.ComponentProps<typeof Button>, 'size'> &
|
|
84
|
+
VariantProps<typeof inputGroupButtonVariants>) {
|
|
85
|
+
return (
|
|
86
|
+
<Button
|
|
87
|
+
type={type}
|
|
88
|
+
data-size={size}
|
|
89
|
+
variant={variant}
|
|
90
|
+
className={cn(inputGroupButtonVariants({ size }), className)}
|
|
91
|
+
{...props}
|
|
92
|
+
/>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
|
|
97
|
+
return (
|
|
98
|
+
<span
|
|
99
|
+
className={cn(
|
|
100
|
+
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
|
101
|
+
className,
|
|
102
|
+
)}
|
|
103
|
+
{...props}
|
|
104
|
+
/>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function InputGroupInput({ className, ...props }: React.ComponentProps<'input'>) {
|
|
109
|
+
return (
|
|
110
|
+
<Input
|
|
111
|
+
data-slot="input-group-control"
|
|
112
|
+
className={cn(
|
|
113
|
+
'flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus:border-0 focus:shadow-none focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0',
|
|
114
|
+
className,
|
|
115
|
+
)}
|
|
116
|
+
{...props}
|
|
117
|
+
/>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function InputGroupTextarea({ className, ...props }: React.ComponentProps<'textarea'>) {
|
|
122
|
+
return (
|
|
123
|
+
<Textarea
|
|
124
|
+
data-slot="input-group-control"
|
|
125
|
+
className={cn(
|
|
126
|
+
'flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent',
|
|
127
|
+
className,
|
|
128
|
+
)}
|
|
129
|
+
{...props}
|
|
130
|
+
/>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export {
|
|
135
|
+
InputGroup,
|
|
136
|
+
InputGroupAddon,
|
|
137
|
+
InputGroupButton,
|
|
138
|
+
InputGroupInput,
|
|
139
|
+
InputGroupText,
|
|
140
|
+
InputGroupTextarea,
|
|
141
|
+
}
|
package/src/ui/input.tsx
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type * as React from 'react'
|
|
2
|
+
|
|
3
|
+
import { cn } from '../lib/utils'
|
|
4
|
+
|
|
5
|
+
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
|
6
|
+
return (
|
|
7
|
+
<input
|
|
8
|
+
type={type}
|
|
9
|
+
data-slot="input"
|
|
10
|
+
className={cn(
|
|
11
|
+
'block w-full min-w-0 min-h-8 rounded-lg border border-border bg-card py-1.5 px-2.5 text-base font-normal leading-normal text-foreground shadow-xs transition-all duration-100 ease-in-out outline-none placeholder:text-muted-foreground focus:border-blue-500 focus:shadow-[0_0_0_3px_rgba(59,130,246,0.15)] disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-muted disabled:border-border disabled:text-muted-foreground disabled:shadow-none aria-invalid:border-destructive file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground',
|
|
12
|
+
className,
|
|
13
|
+
)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { Input }
|
package/src/ui/label.tsx
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Field } from '@base-ui/react/field'
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
|
|
4
|
+
import { cn } from '../lib/utils'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Label component backed by Base UI's Field.Label.
|
|
8
|
+
*
|
|
9
|
+
* Wraps each label in a Field.Root so it can be used standalone
|
|
10
|
+
* without requiring consumers to manually add Field.Root parents.
|
|
11
|
+
*/
|
|
12
|
+
function Label({ className, children, ...props }: React.ComponentProps<'label'>) {
|
|
13
|
+
return (
|
|
14
|
+
<Field.Root>
|
|
15
|
+
<Field.Label
|
|
16
|
+
data-slot="label"
|
|
17
|
+
className={cn(
|
|
18
|
+
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
|
19
|
+
className,
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
>
|
|
23
|
+
{children}
|
|
24
|
+
</Field.Label>
|
|
25
|
+
</Field.Root>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { Label }
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChevronLeftIcon,
|
|
3
|
+
ChevronRightIcon,
|
|
4
|
+
ChevronsLeftIcon,
|
|
5
|
+
ChevronsRightIcon,
|
|
6
|
+
} from 'lucide-react'
|
|
7
|
+
import { useTranslation } from 'react-i18next'
|
|
8
|
+
import { Button } from './button'
|
|
9
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select'
|
|
10
|
+
|
|
11
|
+
interface PaginationMeta {
|
|
12
|
+
page: number
|
|
13
|
+
limit: number
|
|
14
|
+
count: number
|
|
15
|
+
pages: number
|
|
16
|
+
from: number
|
|
17
|
+
to: number
|
|
18
|
+
previous: number | null
|
|
19
|
+
next: number | null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface PaginationProps {
|
|
23
|
+
meta: PaginationMeta
|
|
24
|
+
onPageChange: (page: number) => void
|
|
25
|
+
onPageSizeChange?: (size: number) => void
|
|
26
|
+
pageSizeOptions?: number[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function Pagination({
|
|
30
|
+
meta,
|
|
31
|
+
onPageChange,
|
|
32
|
+
onPageSizeChange,
|
|
33
|
+
pageSizeOptions = [10, 20, 25, 30, 50],
|
|
34
|
+
}: PaginationProps) {
|
|
35
|
+
const { t } = useTranslation()
|
|
36
|
+
|
|
37
|
+
if (meta.pages <= 1 && !onPageSizeChange) return null
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="flex items-center justify-between border-t border-border px-4 py-3">
|
|
41
|
+
<div className="text-sm text-muted-foreground">
|
|
42
|
+
{t('admin.components.pagination.range', {
|
|
43
|
+
from: meta.from,
|
|
44
|
+
to: meta.to,
|
|
45
|
+
count: meta.count,
|
|
46
|
+
})}
|
|
47
|
+
</div>
|
|
48
|
+
<div className="flex items-center gap-6">
|
|
49
|
+
{onPageSizeChange && (
|
|
50
|
+
<div className="flex items-center gap-2">
|
|
51
|
+
<span className="text-sm text-muted-foreground">
|
|
52
|
+
{t('admin.components.pagination.rows_per_page')}
|
|
53
|
+
</span>
|
|
54
|
+
<Select
|
|
55
|
+
value={`${meta.limit}`}
|
|
56
|
+
onValueChange={(value) => onPageSizeChange(Number(value))}
|
|
57
|
+
>
|
|
58
|
+
<SelectTrigger className="h-8 w-[70px]">
|
|
59
|
+
<SelectValue />
|
|
60
|
+
</SelectTrigger>
|
|
61
|
+
<SelectContent side="top">
|
|
62
|
+
{pageSizeOptions.map((size) => (
|
|
63
|
+
<SelectItem key={size} value={`${size}`}>
|
|
64
|
+
{size}
|
|
65
|
+
</SelectItem>
|
|
66
|
+
))}
|
|
67
|
+
</SelectContent>
|
|
68
|
+
</Select>
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
{meta.pages > 1 && (
|
|
72
|
+
<>
|
|
73
|
+
<span className="text-sm text-muted-foreground">
|
|
74
|
+
{t('admin.common.page_of', { page: meta.page, total: meta.pages })}
|
|
75
|
+
</span>
|
|
76
|
+
<div className="flex items-center gap-1">
|
|
77
|
+
<Button
|
|
78
|
+
size="icon-sm"
|
|
79
|
+
variant="outline"
|
|
80
|
+
className="hidden lg:flex"
|
|
81
|
+
disabled={!meta.previous}
|
|
82
|
+
onClick={() => onPageChange(1)}
|
|
83
|
+
>
|
|
84
|
+
<span className="sr-only">{t('admin.components.pagination.first_page')}</span>
|
|
85
|
+
<ChevronsLeftIcon />
|
|
86
|
+
</Button>
|
|
87
|
+
<Button
|
|
88
|
+
size="icon-sm"
|
|
89
|
+
variant="outline"
|
|
90
|
+
disabled={!meta.previous}
|
|
91
|
+
onClick={() => meta.previous && onPageChange(meta.previous)}
|
|
92
|
+
>
|
|
93
|
+
<span className="sr-only">{t('admin.components.pagination.previous_page')}</span>
|
|
94
|
+
<ChevronLeftIcon />
|
|
95
|
+
</Button>
|
|
96
|
+
<Button
|
|
97
|
+
size="icon-sm"
|
|
98
|
+
variant="outline"
|
|
99
|
+
disabled={!meta.next}
|
|
100
|
+
onClick={() => meta.next && onPageChange(meta.next)}
|
|
101
|
+
>
|
|
102
|
+
<span className="sr-only">{t('admin.components.pagination.next_page')}</span>
|
|
103
|
+
<ChevronRightIcon />
|
|
104
|
+
</Button>
|
|
105
|
+
<Button
|
|
106
|
+
size="icon-sm"
|
|
107
|
+
className="hidden lg:flex"
|
|
108
|
+
variant="outline"
|
|
109
|
+
disabled={!meta.next}
|
|
110
|
+
onClick={() => onPageChange(meta.pages)}
|
|
111
|
+
>
|
|
112
|
+
<span className="sr-only">{t('admin.components.pagination.last_page')}</span>
|
|
113
|
+
<ChevronsRightIcon />
|
|
114
|
+
</Button>
|
|
115
|
+
</div>
|
|
116
|
+
</>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type { PaginationMeta }
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Popover as PopoverPrimitive } from '@base-ui/react/popover'
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import { cn } from '../lib/utils'
|
|
4
|
+
|
|
5
|
+
function Popover({
|
|
6
|
+
children,
|
|
7
|
+
...props
|
|
8
|
+
}: {
|
|
9
|
+
children?: React.ReactNode
|
|
10
|
+
open?: boolean
|
|
11
|
+
defaultOpen?: boolean
|
|
12
|
+
onOpenChange?: (open: boolean, eventDetails: PopoverPrimitive.Root.ChangeEventDetails) => void
|
|
13
|
+
modal?: boolean
|
|
14
|
+
}) {
|
|
15
|
+
return (
|
|
16
|
+
<PopoverPrimitive.Root data-slot="popover" {...props}>
|
|
17
|
+
{children}
|
|
18
|
+
</PopoverPrimitive.Root>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function PopoverTrigger({
|
|
23
|
+
asChild,
|
|
24
|
+
children,
|
|
25
|
+
...props
|
|
26
|
+
}: React.ComponentProps<typeof PopoverPrimitive.Trigger> & {
|
|
27
|
+
asChild?: boolean
|
|
28
|
+
}) {
|
|
29
|
+
if (asChild && React.isValidElement(children)) {
|
|
30
|
+
return <PopoverPrimitive.Trigger data-slot="popover-trigger" render={children} {...props} />
|
|
31
|
+
}
|
|
32
|
+
return (
|
|
33
|
+
<PopoverPrimitive.Trigger data-slot="popover-trigger" {...props}>
|
|
34
|
+
{children}
|
|
35
|
+
</PopoverPrimitive.Trigger>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function PopoverContent({
|
|
40
|
+
className,
|
|
41
|
+
align = 'center',
|
|
42
|
+
sideOffset = 4,
|
|
43
|
+
side = 'bottom',
|
|
44
|
+
children,
|
|
45
|
+
...props
|
|
46
|
+
}: {
|
|
47
|
+
className?: string
|
|
48
|
+
align?: 'start' | 'center' | 'end'
|
|
49
|
+
sideOffset?: number
|
|
50
|
+
side?: 'top' | 'bottom' | 'left' | 'right'
|
|
51
|
+
children?: React.ReactNode
|
|
52
|
+
} & Omit<React.ComponentProps<typeof PopoverPrimitive.Popup>, 'className'>) {
|
|
53
|
+
return (
|
|
54
|
+
<PopoverPrimitive.Portal>
|
|
55
|
+
<PopoverPrimitive.Positioner side={side} sideOffset={sideOffset} align={align}>
|
|
56
|
+
<PopoverPrimitive.Popup
|
|
57
|
+
data-slot="popover-content"
|
|
58
|
+
className={cn(
|
|
59
|
+
'z-50 w-auto rounded-lg border border-border bg-popover p-3 text-popover-foreground shadow-md',
|
|
60
|
+
'duration-100 data-[starting-style]:opacity-0 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[ending-style]:scale-95 transition-[opacity,transform]',
|
|
61
|
+
className,
|
|
62
|
+
)}
|
|
63
|
+
{...props}
|
|
64
|
+
>
|
|
65
|
+
{children}
|
|
66
|
+
</PopoverPrimitive.Popup>
|
|
67
|
+
</PopoverPrimitive.Positioner>
|
|
68
|
+
</PopoverPrimitive.Portal>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function PopoverAnchor({ ...props }: React.ComponentProps<'div'>) {
|
|
73
|
+
return <div data-slot="popover-anchor" {...props} />
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger }
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Progress as ProgressPrimitive } from '@base-ui/react/progress'
|
|
4
|
+
|
|
5
|
+
import { cn } from '../lib/utils'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Base UI progress bar. `value={null}` renders the indeterminate state
|
|
9
|
+
* (Base UI stamps `data-indeterminate` on every part). Compose
|
|
10
|
+
* `ProgressLabel`/`ProgressValue` as children when a caption is needed —
|
|
11
|
+
* the track always renders last.
|
|
12
|
+
*/
|
|
13
|
+
function Progress({ className, children, value, ...props }: ProgressPrimitive.Root.Props) {
|
|
14
|
+
return (
|
|
15
|
+
<ProgressPrimitive.Root
|
|
16
|
+
value={value}
|
|
17
|
+
data-slot="progress"
|
|
18
|
+
className={cn('flex w-full flex-wrap items-center gap-2', className)}
|
|
19
|
+
{...props}
|
|
20
|
+
>
|
|
21
|
+
{children}
|
|
22
|
+
<ProgressTrack>
|
|
23
|
+
<ProgressIndicator />
|
|
24
|
+
</ProgressTrack>
|
|
25
|
+
</ProgressPrimitive.Root>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function ProgressTrack({ className, ...props }: ProgressPrimitive.Track.Props) {
|
|
30
|
+
return (
|
|
31
|
+
<ProgressPrimitive.Track
|
|
32
|
+
data-slot="progress-track"
|
|
33
|
+
className={cn('relative h-2 w-full overflow-hidden rounded-full bg-primary/20', className)}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function ProgressIndicator({ className, ...props }: ProgressPrimitive.Indicator.Props) {
|
|
40
|
+
return (
|
|
41
|
+
<ProgressPrimitive.Indicator
|
|
42
|
+
data-slot="progress-indicator"
|
|
43
|
+
className={cn(
|
|
44
|
+
'h-full rounded-full bg-primary transition-all',
|
|
45
|
+
'data-[indeterminate]:w-1/3 data-[indeterminate]:animate-pulse',
|
|
46
|
+
className,
|
|
47
|
+
)}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ProgressLabel({ className, ...props }: ProgressPrimitive.Label.Props) {
|
|
54
|
+
return (
|
|
55
|
+
<ProgressPrimitive.Label
|
|
56
|
+
data-slot="progress-label"
|
|
57
|
+
className={cn('font-medium text-sm', className)}
|
|
58
|
+
{...props}
|
|
59
|
+
/>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function ProgressValue({ className, ...props }: ProgressPrimitive.Value.Props) {
|
|
64
|
+
return (
|
|
65
|
+
<ProgressPrimitive.Value
|
|
66
|
+
data-slot="progress-value"
|
|
67
|
+
className={cn('ml-auto text-muted-foreground text-sm', className)}
|
|
68
|
+
{...props}
|
|
69
|
+
/>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export { Progress, ProgressIndicator, ProgressLabel, ProgressTrack, ProgressValue }
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Radio as RadioPrimitive } from '@base-ui/react/radio'
|
|
2
|
+
import { RadioGroup as RadioGroupPrimitive } from '@base-ui/react/radio-group'
|
|
3
|
+
import type * as React from 'react'
|
|
4
|
+
import { cn } from '../lib/utils'
|
|
5
|
+
|
|
6
|
+
function RadioGroup({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive>) {
|
|
7
|
+
return (
|
|
8
|
+
<RadioGroupPrimitive
|
|
9
|
+
data-slot="radio-group"
|
|
10
|
+
className={cn('grid w-full gap-2', className)}
|
|
11
|
+
{...props}
|
|
12
|
+
/>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function RadioGroupItem({ className, ...props }: React.ComponentProps<typeof RadioPrimitive.Root>) {
|
|
17
|
+
return (
|
|
18
|
+
<RadioPrimitive.Root
|
|
19
|
+
data-slot="radio-group-item"
|
|
20
|
+
className={cn(
|
|
21
|
+
'peer relative inline-block size-4 shrink-0 rounded-full border border-input align-[-3px] shadow-xs outline-none transition-shadow',
|
|
22
|
+
'focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50',
|
|
23
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
24
|
+
'aria-invalid:border-destructive aria-invalid:ring-destructive/20',
|
|
25
|
+
'data-[checked]:border-blue-500 data-[checked]:bg-blue-500 data-[checked]:text-white',
|
|
26
|
+
'dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:data-[checked]:bg-blue-500',
|
|
27
|
+
className,
|
|
28
|
+
)}
|
|
29
|
+
{...props}
|
|
30
|
+
>
|
|
31
|
+
<RadioPrimitive.Indicator
|
|
32
|
+
keepMounted
|
|
33
|
+
data-slot="radio-group-indicator"
|
|
34
|
+
className="absolute inset-0 flex items-center justify-center text-current transition-none data-[unchecked]:hidden"
|
|
35
|
+
>
|
|
36
|
+
<span className="size-1.5 rounded-full bg-current" />
|
|
37
|
+
</RadioPrimitive.Indicator>
|
|
38
|
+
</RadioPrimitive.Root>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { RadioGroup, RadioGroupItem }
|