@meith/ui 0.35.1 → 0.36.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.
- package/package.json +3 -1
- package/src/card.tsx +6 -8
- package/src/disclosure.tsx +2 -2
- package/src/empty.tsx +2 -2
- package/src/field.tsx +16 -15
- package/src/index.ts +23 -2
- package/src/nav-tabs-enhancer.tsx +64 -0
- package/src/nav-tabs.tsx +60 -0
- package/src/navigation-drawer.tsx +92 -0
- package/src/page-header.tsx +56 -0
- package/src/password-input.tsx +61 -0
- package/src/variants.ts +60 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meith/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.1",
|
|
4
4
|
"description": "Meith's shared UI primitives: buttons, menus and the class utilities themes build with.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
"./dialog": "./src/dialog.tsx",
|
|
18
18
|
"./input-otp": "./src/input-otp.tsx",
|
|
19
19
|
"./menu": "./src/menu.tsx",
|
|
20
|
+
"./nav-tabs-enhancer": "./src/nav-tabs-enhancer.tsx",
|
|
21
|
+
"./password-input": "./src/password-input.tsx",
|
|
20
22
|
"./popover": "./src/popover.tsx",
|
|
21
23
|
"./tabs": "./src/tabs.tsx",
|
|
22
24
|
"./toast": "./src/toast.tsx",
|
package/src/card.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cn } from './utils'
|
|
2
|
+
import { surfaceVariants } from './variants'
|
|
2
3
|
|
|
3
4
|
type CardElement = 'section' | 'article' | 'div'
|
|
4
5
|
|
|
@@ -12,10 +13,7 @@ function Card({ as = 'section', className, ...props }: CardProps) {
|
|
|
12
13
|
return (
|
|
13
14
|
<Component
|
|
14
15
|
data-slot="card"
|
|
15
|
-
className={cn(
|
|
16
|
-
'overflow-hidden rounded-lg border border-border bg-card text-card-foreground shadow-elevation',
|
|
17
|
-
className,
|
|
18
|
-
)}
|
|
16
|
+
className={cn(surfaceVariants(), '@container/card overflow-hidden', className)}
|
|
19
17
|
{...props}
|
|
20
18
|
/>
|
|
21
19
|
)
|
|
@@ -26,7 +24,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
|
|
26
24
|
<div
|
|
27
25
|
data-slot="card-header"
|
|
28
26
|
className={cn(
|
|
29
|
-
'flex flex-wrap items-center justify-between gap-x-4 gap-y-1 border-b border-border bg-surface px-
|
|
27
|
+
'flex flex-wrap items-center justify-between gap-x-4 gap-y-1 border-b border-border bg-surface/60 px-5 py-3',
|
|
30
28
|
className,
|
|
31
29
|
)}
|
|
32
30
|
{...props}
|
|
@@ -48,7 +46,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
|
|
48
46
|
return (
|
|
49
47
|
<p
|
|
50
48
|
data-slot="card-description"
|
|
51
|
-
className={cn('text-sm text-muted-foreground', className)}
|
|
49
|
+
className={cn('text-sm leading-relaxed text-muted-foreground', className)}
|
|
52
50
|
{...props}
|
|
53
51
|
/>
|
|
54
52
|
)
|
|
@@ -65,7 +63,7 @@ function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
|
|
|
65
63
|
}
|
|
66
64
|
|
|
67
65
|
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
|
|
68
|
-
return <div data-slot="card-content" className={cn('px-
|
|
66
|
+
return <div data-slot="card-content" className={cn('px-5 py-4', className)} {...props} />
|
|
69
67
|
}
|
|
70
68
|
|
|
71
69
|
function CardRows({ className, ...props }: React.ComponentProps<'ul'>) {
|
|
@@ -77,7 +75,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
|
|
77
75
|
<div
|
|
78
76
|
data-slot="card-footer"
|
|
79
77
|
className={cn(
|
|
80
|
-
'flex flex-wrap items-center gap-3 border-t border-border px-
|
|
78
|
+
'flex flex-wrap items-center gap-3 border-t border-border px-5 py-3 text-xs text-muted-foreground',
|
|
81
79
|
className,
|
|
82
80
|
)}
|
|
83
81
|
{...props}
|
package/src/disclosure.tsx
CHANGED
|
@@ -26,7 +26,7 @@ function Disclosure({
|
|
|
26
26
|
>
|
|
27
27
|
<summary
|
|
28
28
|
className={cn(
|
|
29
|
-
'flex cursor-
|
|
29
|
+
'flex cursor-pointer list-none items-center gap-2 min-h-11 px-4 py-3 text-sm font-medium select-none',
|
|
30
30
|
'transition-colors hover:bg-muted',
|
|
31
31
|
'[&::-webkit-details-marker]:hidden [&::marker]:content-none',
|
|
32
32
|
)}
|
|
@@ -44,7 +44,7 @@ function Disclosure({
|
|
|
44
44
|
<path d="m4.5 3 3 3-3 3" />
|
|
45
45
|
</svg>
|
|
46
46
|
|
|
47
|
-
<span className="min-w-0 flex-1
|
|
47
|
+
<span className="min-w-0 flex-1 [overflow-wrap:anywhere]">{summary}</span>
|
|
48
48
|
|
|
49
49
|
{aside !== undefined && (
|
|
50
50
|
<span className="shrink-0 text-xs font-normal text-muted-foreground">{aside}</span>
|
package/src/empty.tsx
CHANGED
|
@@ -17,7 +17,7 @@ function EmptyTitle({ className, ...props }: React.ComponentProps<'p'>) {
|
|
|
17
17
|
return (
|
|
18
18
|
<p
|
|
19
19
|
data-slot="empty-title"
|
|
20
|
-
className={cn('text-
|
|
20
|
+
className={cn('text-base font-semibold tracking-tight text-foreground', className)}
|
|
21
21
|
{...props}
|
|
22
22
|
/>
|
|
23
23
|
)
|
|
@@ -27,7 +27,7 @@ function EmptyDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
|
|
27
27
|
return (
|
|
28
28
|
<p
|
|
29
29
|
data-slot="empty-description"
|
|
30
|
-
className={cn('max-w-
|
|
30
|
+
className={cn('max-w-md text-sm leading-relaxed text-muted-foreground', className)}
|
|
31
31
|
{...props}
|
|
32
32
|
/>
|
|
33
33
|
)
|
package/src/field.tsx
CHANGED
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
import { cn } from './utils'
|
|
2
|
-
|
|
3
|
-
const CONTROL = [
|
|
4
|
-
'w-full rounded-md border border-input bg-card px-3 text-sm text-foreground',
|
|
5
|
-
'transition-[border-color,box-shadow] duration-100',
|
|
6
|
-
'placeholder:text-muted-foreground',
|
|
7
|
-
'disabled:cursor-not-allowed disabled:opacity-60',
|
|
8
|
-
'aria-invalid:border-destructive',
|
|
9
|
-
].join(' ')
|
|
2
|
+
import { controlVariants } from './variants'
|
|
10
3
|
|
|
11
4
|
function Input({ className, type = 'text', ...props }: React.ComponentProps<'input'>) {
|
|
12
5
|
return (
|
|
13
6
|
<input
|
|
14
7
|
data-slot="input"
|
|
15
8
|
type={type}
|
|
16
|
-
className={cn(
|
|
9
|
+
className={cn(controlVariants(), 'py-2', className)}
|
|
17
10
|
{...props}
|
|
18
11
|
/>
|
|
19
12
|
)
|
|
@@ -23,15 +16,23 @@ function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
|
|
|
23
16
|
return (
|
|
24
17
|
<textarea
|
|
25
18
|
data-slot="textarea"
|
|
26
|
-
className={cn(
|
|
19
|
+
className={cn(controlVariants(), 'h-auto min-h-36 resize-y py-3 leading-relaxed', className)}
|
|
27
20
|
{...props}
|
|
28
21
|
/>
|
|
29
22
|
)
|
|
30
23
|
}
|
|
31
24
|
|
|
32
|
-
function NativeSelect({
|
|
25
|
+
function NativeSelect({
|
|
26
|
+
className,
|
|
27
|
+
controlSize = 'default',
|
|
28
|
+
...props
|
|
29
|
+
}: React.ComponentProps<'select'> & { controlSize?: 'sm' | 'default' }) {
|
|
33
30
|
return (
|
|
34
|
-
<select
|
|
31
|
+
<select
|
|
32
|
+
data-slot="native-select"
|
|
33
|
+
className={cn(controlVariants({ size: controlSize }), 'pr-8', className)}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
35
36
|
)
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -40,7 +41,7 @@ function Label({ className, ...props }: React.ComponentProps<'label'>) {
|
|
|
40
41
|
// biome-ignore lint/a11y/noLabelWithoutControl: the Label primitive itself — its caller supplies the control it wraps
|
|
41
42
|
<label
|
|
42
43
|
data-slot="label"
|
|
43
|
-
className={cn('text-sm font-medium leading-
|
|
44
|
+
className={cn('text-sm font-medium leading-5 text-foreground select-none', className)}
|
|
44
45
|
{...props}
|
|
45
46
|
/>
|
|
46
47
|
)
|
|
@@ -77,7 +78,7 @@ function Field({
|
|
|
77
78
|
const describedBy = [errorId, descriptionId].filter((value) => value !== null).join(' ')
|
|
78
79
|
|
|
79
80
|
return (
|
|
80
|
-
<div data-slot="field" className={cn('flex flex-col gap-
|
|
81
|
+
<div data-slot="field" className={cn('flex min-w-0 flex-col gap-2', className)} {...props}>
|
|
81
82
|
<Label htmlFor={id}>{label}</Label>
|
|
82
83
|
|
|
83
84
|
{children({
|
|
@@ -94,7 +95,7 @@ function Field({
|
|
|
94
95
|
)}
|
|
95
96
|
|
|
96
97
|
{descriptionId !== null && (
|
|
97
|
-
<p id={descriptionId} className="text-xs text-muted-foreground">
|
|
98
|
+
<p id={descriptionId} className="text-xs leading-relaxed text-muted-foreground">
|
|
98
99
|
{description}
|
|
99
100
|
</p>
|
|
100
101
|
)}
|
package/src/index.ts
CHANGED
|
@@ -19,10 +19,31 @@ export { Disclosure } from './disclosure'
|
|
|
19
19
|
export { Empty, EmptyAction, EmptyDescription, EmptyTitle } from './empty'
|
|
20
20
|
export type { FieldProps } from './field'
|
|
21
21
|
export { Field, Input, Label, NativeSelect, Textarea } from './field'
|
|
22
|
+
export { type NavTab, NavTabs } from './nav-tabs'
|
|
23
|
+
export {
|
|
24
|
+
NavigationDrawer,
|
|
25
|
+
type NavigationDrawerProps,
|
|
26
|
+
NavigationDrawerTrigger,
|
|
27
|
+
} from './navigation-drawer'
|
|
28
|
+
export {
|
|
29
|
+
PageDescription,
|
|
30
|
+
PageHeader,
|
|
31
|
+
PageHeaderActions,
|
|
32
|
+
PageHeaderContent,
|
|
33
|
+
PageTitle,
|
|
34
|
+
} from './page-header'
|
|
22
35
|
export type { SeparatorProps } from './separator'
|
|
23
36
|
export { Separator } from './separator'
|
|
24
37
|
export type { TextLinkProps } from './text-link'
|
|
25
38
|
export { TextLink } from './text-link'
|
|
26
39
|
export { cn } from './utils'
|
|
27
|
-
export type { BadgeVariants, ButtonVariants, TextLinkVariants } from './variants'
|
|
28
|
-
export {
|
|
40
|
+
export type { BadgeVariants, ButtonVariants, ControlVariants, TextLinkVariants } from './variants'
|
|
41
|
+
export {
|
|
42
|
+
badgeVariants,
|
|
43
|
+
buttonVariants,
|
|
44
|
+
controlVariants,
|
|
45
|
+
navTabListVariants,
|
|
46
|
+
navTabVariants,
|
|
47
|
+
surfaceVariants,
|
|
48
|
+
textLinkVariants,
|
|
49
|
+
} from './variants'
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react'
|
|
4
|
+
|
|
5
|
+
const SELECTOR = '[data-nav-tabs]'
|
|
6
|
+
const CURRENT = '[aria-current]:not([aria-current="false"])'
|
|
7
|
+
|
|
8
|
+
function reveal(list: HTMLElement, active: HTMLElement | null) {
|
|
9
|
+
if (active === null || list.clientWidth === 0) return
|
|
10
|
+
const bounds = list.getBoundingClientRect()
|
|
11
|
+
const tab = active.getBoundingClientRect()
|
|
12
|
+
if (tab.left >= bounds.left + 4 && tab.right <= bounds.right - 4) return
|
|
13
|
+
list.scrollBy({ left: tab.left - bounds.left - (list.clientWidth - tab.width) / 2 })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function NavTabsEnhancer() {
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const lists = new Map<HTMLElement, HTMLElement | null>()
|
|
19
|
+
const resize = new ResizeObserver((entries) => {
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
const list = entry.target.closest<HTMLElement>(SELECTOR)
|
|
22
|
+
if (list === null) continue
|
|
23
|
+
reveal(list, lists.get(list) ?? null)
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
function refresh() {
|
|
28
|
+
for (const list of lists.keys()) {
|
|
29
|
+
if (list.isConnected) continue
|
|
30
|
+
resize.unobserve(list)
|
|
31
|
+
const active = lists.get(list)
|
|
32
|
+
if (active) resize.unobserve(active)
|
|
33
|
+
lists.delete(list)
|
|
34
|
+
}
|
|
35
|
+
for (const list of document.querySelectorAll<HTMLElement>(SELECTOR)) {
|
|
36
|
+
const active = list.querySelector<HTMLElement>(CURRENT)
|
|
37
|
+
if (!lists.has(list)) resize.observe(list)
|
|
38
|
+
if (lists.get(list) !== active) {
|
|
39
|
+
const previous = lists.get(list)
|
|
40
|
+
if (previous) resize.unobserve(previous)
|
|
41
|
+
if (active) resize.observe(active)
|
|
42
|
+
reveal(list, active)
|
|
43
|
+
}
|
|
44
|
+
lists.set(list, active)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const changes = new MutationObserver(refresh)
|
|
49
|
+
changes.observe(document.body, {
|
|
50
|
+
childList: true,
|
|
51
|
+
subtree: true,
|
|
52
|
+
attributes: true,
|
|
53
|
+
attributeFilter: ['aria-current'],
|
|
54
|
+
})
|
|
55
|
+
refresh()
|
|
56
|
+
|
|
57
|
+
return () => {
|
|
58
|
+
changes.disconnect()
|
|
59
|
+
resize.disconnect()
|
|
60
|
+
}
|
|
61
|
+
}, [])
|
|
62
|
+
|
|
63
|
+
return null
|
|
64
|
+
}
|
package/src/nav-tabs.tsx
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { cn } from './utils'
|
|
2
|
+
import { navTabListVariants, navTabVariants } from './variants'
|
|
3
|
+
|
|
4
|
+
export interface NavTab {
|
|
5
|
+
readonly href: string
|
|
6
|
+
readonly label: string
|
|
7
|
+
readonly isCurrent: boolean
|
|
8
|
+
readonly count?: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function NavTabs({
|
|
12
|
+
label,
|
|
13
|
+
tabs,
|
|
14
|
+
aside,
|
|
15
|
+
className,
|
|
16
|
+
}: {
|
|
17
|
+
label: string
|
|
18
|
+
tabs: readonly NavTab[]
|
|
19
|
+
aside?: React.ReactNode
|
|
20
|
+
className?: string
|
|
21
|
+
}) {
|
|
22
|
+
if (tabs.length === 0) return null
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<nav
|
|
26
|
+
aria-label={label}
|
|
27
|
+
className={cn('flex min-w-0 max-w-full flex-wrap items-center gap-x-3 gap-y-2', className)}
|
|
28
|
+
>
|
|
29
|
+
<ul data-nav-tabs className={navTabListVariants()}>
|
|
30
|
+
{tabs.map((tab) => (
|
|
31
|
+
<li key={tab.href} className="shrink-0">
|
|
32
|
+
<a
|
|
33
|
+
href={tab.href}
|
|
34
|
+
{...(tab.isCurrent ? { 'aria-current': 'page' as const } : {})}
|
|
35
|
+
className={navTabVariants({ active: tab.isCurrent })}
|
|
36
|
+
>
|
|
37
|
+
{tab.label}
|
|
38
|
+
{tab.count !== undefined && tab.count > 0 && (
|
|
39
|
+
<span
|
|
40
|
+
className={cn(
|
|
41
|
+
'rounded px-1.5 text-xs font-semibold tabular-nums',
|
|
42
|
+
tab.isCurrent
|
|
43
|
+
? 'bg-secondary text-secondary-foreground'
|
|
44
|
+
: 'bg-muted text-muted-foreground',
|
|
45
|
+
)}
|
|
46
|
+
>
|
|
47
|
+
{tab.count}
|
|
48
|
+
</span>
|
|
49
|
+
)}
|
|
50
|
+
</a>
|
|
51
|
+
</li>
|
|
52
|
+
))}
|
|
53
|
+
</ul>
|
|
54
|
+
|
|
55
|
+
{aside !== undefined && (
|
|
56
|
+
<span className="shrink-0 text-xs text-muted-foreground">{aside}</span>
|
|
57
|
+
)}
|
|
58
|
+
</nav>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { cn } from './utils'
|
|
2
|
+
import { buttonVariants } from './variants'
|
|
3
|
+
|
|
4
|
+
export interface NavigationDrawerProps {
|
|
5
|
+
readonly id: string
|
|
6
|
+
readonly title: string
|
|
7
|
+
readonly openLabel: string
|
|
8
|
+
readonly closeLabel: string
|
|
9
|
+
readonly showTrigger?: boolean
|
|
10
|
+
readonly children: React.ReactNode
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function NavigationDrawerTrigger({
|
|
14
|
+
target,
|
|
15
|
+
label,
|
|
16
|
+
className,
|
|
17
|
+
...props
|
|
18
|
+
}: Omit<React.ComponentProps<'button'>, 'children'> & {
|
|
19
|
+
readonly target: string
|
|
20
|
+
readonly label: string
|
|
21
|
+
}) {
|
|
22
|
+
return (
|
|
23
|
+
<button
|
|
24
|
+
{...props}
|
|
25
|
+
type="button"
|
|
26
|
+
popoverTarget={target}
|
|
27
|
+
aria-label={label}
|
|
28
|
+
className={cn(buttonVariants({ variant: 'outline', size: 'icon' }), 'size-11', className)}
|
|
29
|
+
>
|
|
30
|
+
<svg
|
|
31
|
+
aria-hidden="true"
|
|
32
|
+
viewBox="0 0 24 24"
|
|
33
|
+
className="size-5"
|
|
34
|
+
fill="none"
|
|
35
|
+
stroke="currentColor"
|
|
36
|
+
strokeWidth="1.5"
|
|
37
|
+
strokeLinecap="round"
|
|
38
|
+
>
|
|
39
|
+
<path d="M4 6h16M4 12h16M4 18h16" />
|
|
40
|
+
</svg>
|
|
41
|
+
</button>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function NavigationDrawer({
|
|
46
|
+
id,
|
|
47
|
+
title,
|
|
48
|
+
openLabel,
|
|
49
|
+
closeLabel,
|
|
50
|
+
showTrigger = true,
|
|
51
|
+
children,
|
|
52
|
+
}: NavigationDrawerProps) {
|
|
53
|
+
return (
|
|
54
|
+
<>
|
|
55
|
+
{showTrigger && <NavigationDrawerTrigger target={id} label={openLabel} />}
|
|
56
|
+
<div
|
|
57
|
+
id={id}
|
|
58
|
+
popover="auto"
|
|
59
|
+
role="dialog"
|
|
60
|
+
data-slot="navigation-drawer"
|
|
61
|
+
aria-labelledby={`${id}-title`}
|
|
62
|
+
className="fixed inset-y-0 end-0 start-auto m-0 h-dvh max-h-dvh w-80 max-w-[calc(100vw-3rem)] flex-col border-0 border-s border-border bg-card p-0 text-card-foreground shadow-xl backdrop:bg-black/40 [&:popover-open]:flex"
|
|
63
|
+
>
|
|
64
|
+
<div className="flex shrink-0 items-center justify-between gap-3 border-b border-border px-4 py-3">
|
|
65
|
+
<h2 id={`${id}-title`} className="font-heading text-base font-semibold">
|
|
66
|
+
{title}
|
|
67
|
+
</h2>
|
|
68
|
+
<button
|
|
69
|
+
type="button"
|
|
70
|
+
popoverTarget={id}
|
|
71
|
+
popoverTargetAction="hide"
|
|
72
|
+
aria-label={closeLabel}
|
|
73
|
+
className={cn(buttonVariants({ variant: 'ghost', size: 'icon' }), 'size-11')}
|
|
74
|
+
>
|
|
75
|
+
<svg
|
|
76
|
+
aria-hidden="true"
|
|
77
|
+
viewBox="0 0 24 24"
|
|
78
|
+
className="size-5"
|
|
79
|
+
fill="none"
|
|
80
|
+
stroke="currentColor"
|
|
81
|
+
strokeWidth="1.5"
|
|
82
|
+
strokeLinecap="round"
|
|
83
|
+
>
|
|
84
|
+
<path d="m6 6 12 12M18 6 6 18" />
|
|
85
|
+
</svg>
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain p-3">{children}</div>
|
|
89
|
+
</div>
|
|
90
|
+
</>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { cn } from './utils'
|
|
2
|
+
|
|
3
|
+
function PageHeader({ className, ...props }: React.ComponentProps<'header'>) {
|
|
4
|
+
return (
|
|
5
|
+
<header
|
|
6
|
+
data-slot="page-header"
|
|
7
|
+
className={cn('flex flex-wrap items-end justify-between gap-x-6 gap-y-4', className)}
|
|
8
|
+
{...props}
|
|
9
|
+
/>
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function PageHeaderContent({ className, ...props }: React.ComponentProps<'div'>) {
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
data-slot="page-header-content"
|
|
17
|
+
className={cn('flex min-w-0 flex-1 basis-64 flex-col gap-2', className)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function PageTitle({ className, ...props }: React.ComponentProps<'h1'>) {
|
|
24
|
+
return (
|
|
25
|
+
<h1
|
|
26
|
+
data-slot="page-title"
|
|
27
|
+
className={cn(
|
|
28
|
+
'font-heading text-2xl font-semibold tracking-tight text-balance text-foreground [overflow-wrap:anywhere] sm:text-3xl',
|
|
29
|
+
className,
|
|
30
|
+
)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function PageDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
|
37
|
+
return (
|
|
38
|
+
<p
|
|
39
|
+
data-slot="page-description"
|
|
40
|
+
className={cn('max-w-prose text-sm leading-relaxed text-muted-foreground', className)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function PageHeaderActions({ className, ...props }: React.ComponentProps<'div'>) {
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
data-slot="page-header-actions"
|
|
50
|
+
className={cn('flex max-w-full flex-wrap items-center gap-2', className)}
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export { PageDescription, PageHeader, PageHeaderActions, PageHeaderContent, PageTitle }
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import { Input } from './field'
|
|
6
|
+
import { cn } from './utils'
|
|
7
|
+
|
|
8
|
+
export interface PasswordInputProps extends Omit<React.ComponentProps<'input'>, 'type'> {
|
|
9
|
+
readonly showLabel: string
|
|
10
|
+
readonly hideLabel: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function PasswordInput({
|
|
14
|
+
showLabel,
|
|
15
|
+
hideLabel,
|
|
16
|
+
className,
|
|
17
|
+
disabled,
|
|
18
|
+
...props
|
|
19
|
+
}: PasswordInputProps) {
|
|
20
|
+
const [visible, setVisible] = useState(false)
|
|
21
|
+
const [enhanced, setEnhanced] = useState(false)
|
|
22
|
+
|
|
23
|
+
useEffect(() => setEnhanced(true), [])
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<span data-slot="password-input" className="relative block min-w-0">
|
|
27
|
+
<Input
|
|
28
|
+
{...props}
|
|
29
|
+
disabled={disabled}
|
|
30
|
+
type={visible ? 'text' : 'password'}
|
|
31
|
+
className={cn(className, 'pr-12')}
|
|
32
|
+
/>
|
|
33
|
+
{enhanced && (
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
disabled={disabled}
|
|
37
|
+
aria-label={visible ? hideLabel : showLabel}
|
|
38
|
+
aria-controls={props.id}
|
|
39
|
+
title={visible ? hideLabel : showLabel}
|
|
40
|
+
onClick={() => setVisible((current) => !current)}
|
|
41
|
+
className="absolute inset-y-0 right-0 flex w-11 items-center justify-center rounded-r-md text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50"
|
|
42
|
+
>
|
|
43
|
+
<svg
|
|
44
|
+
aria-hidden="true"
|
|
45
|
+
viewBox="0 0 24 24"
|
|
46
|
+
className="size-4"
|
|
47
|
+
fill="none"
|
|
48
|
+
stroke="currentColor"
|
|
49
|
+
strokeWidth="1.5"
|
|
50
|
+
strokeLinecap="round"
|
|
51
|
+
strokeLinejoin="round"
|
|
52
|
+
>
|
|
53
|
+
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12Z" />
|
|
54
|
+
<circle cx="12" cy="12" r="3" />
|
|
55
|
+
{visible && <path d="m3 3 18 18" />}
|
|
56
|
+
</svg>
|
|
57
|
+
</button>
|
|
58
|
+
)}
|
|
59
|
+
</span>
|
|
60
|
+
)
|
|
61
|
+
}
|
package/src/variants.ts
CHANGED
|
@@ -4,6 +4,7 @@ export const buttonVariants = cva(
|
|
|
4
4
|
[
|
|
5
5
|
'inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap',
|
|
6
6
|
'rounded-md border text-sm font-medium leading-none',
|
|
7
|
+
'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring',
|
|
7
8
|
'transition-[color,background-color,border-color,opacity] duration-100 ease-out',
|
|
8
9
|
'disabled:pointer-events-none disabled:opacity-50',
|
|
9
10
|
'aria-disabled:pointer-events-none aria-disabled:opacity-50',
|
|
@@ -12,7 +13,8 @@ export const buttonVariants = cva(
|
|
|
12
13
|
{
|
|
13
14
|
variants: {
|
|
14
15
|
variant: {
|
|
15
|
-
primary:
|
|
16
|
+
primary:
|
|
17
|
+
'border-transparent bg-primary text-primary-foreground shadow-sm hover:bg-primary-hover',
|
|
16
18
|
secondary:
|
|
17
19
|
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/70',
|
|
18
20
|
outline: 'border-border bg-card text-foreground hover:bg-muted',
|
|
@@ -23,10 +25,10 @@ export const buttonVariants = cva(
|
|
|
23
25
|
link: 'h-auto border-transparent p-0 text-foreground underline underline-offset-4 decoration-border hover:decoration-current',
|
|
24
26
|
},
|
|
25
27
|
size: {
|
|
26
|
-
sm: 'h-8 gap-1.5 px-2.5 text-xs',
|
|
27
|
-
default: 'h-
|
|
28
|
-
lg: 'h-
|
|
29
|
-
icon: 'size-
|
|
28
|
+
sm: 'h-8 gap-1.5 px-2.5 text-xs pointer-coarse:min-h-11',
|
|
29
|
+
default: 'h-10 px-4 pointer-coarse:min-h-11',
|
|
30
|
+
lg: 'h-11 px-5',
|
|
31
|
+
icon: 'size-10 p-0 pointer-coarse:size-11',
|
|
30
32
|
},
|
|
31
33
|
},
|
|
32
34
|
defaultVariants: { variant: 'secondary', size: 'default' },
|
|
@@ -93,3 +95,56 @@ export const textLinkVariants = cva(
|
|
|
93
95
|
)
|
|
94
96
|
|
|
95
97
|
export type TextLinkVariants = VariantProps<typeof textLinkVariants>
|
|
98
|
+
|
|
99
|
+
export const controlVariants = cva(
|
|
100
|
+
[
|
|
101
|
+
'w-full min-w-0 rounded-md border border-input bg-card text-foreground',
|
|
102
|
+
'transition-[border-color,box-shadow]',
|
|
103
|
+
'duration-150',
|
|
104
|
+
'placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-60',
|
|
105
|
+
'focus-visible:border-ring focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring',
|
|
106
|
+
'aria-invalid:border-destructive aria-invalid:focus-visible:outline-destructive',
|
|
107
|
+
'pointer-coarse:min-h-11 pointer-coarse:text-base',
|
|
108
|
+
],
|
|
109
|
+
{
|
|
110
|
+
variants: {
|
|
111
|
+
size: {
|
|
112
|
+
sm: 'min-h-8 px-2.5 py-1 text-xs',
|
|
113
|
+
default: 'min-h-10 px-3 py-2 text-sm',
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
defaultVariants: { size: 'default' },
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
export type ControlVariants = VariantProps<typeof controlVariants>
|
|
121
|
+
|
|
122
|
+
export const surfaceVariants = cva(
|
|
123
|
+
'min-w-0 rounded-xl border border-border bg-card text-card-foreground shadow-elevation',
|
|
124
|
+
{
|
|
125
|
+
variants: {
|
|
126
|
+
padded: { true: 'flex flex-col gap-4 p-5', false: '' },
|
|
127
|
+
},
|
|
128
|
+
defaultVariants: { padded: false },
|
|
129
|
+
},
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
export const navTabListVariants = cva(
|
|
133
|
+
'inline-flex max-w-full items-center gap-1 overflow-x-auto rounded-lg border border-border bg-surface p-1',
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
export const navTabVariants = cva(
|
|
137
|
+
[
|
|
138
|
+
'inline-flex h-9 items-center gap-1.5 rounded-md px-3 text-sm whitespace-nowrap transition-colors pointer-coarse:min-h-11',
|
|
139
|
+
'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring',
|
|
140
|
+
],
|
|
141
|
+
{
|
|
142
|
+
variants: {
|
|
143
|
+
active: {
|
|
144
|
+
true: 'bg-card font-semibold text-primary shadow-sm',
|
|
145
|
+
false: 'font-medium text-muted-foreground hover:bg-card/60 hover:text-foreground',
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
defaultVariants: { active: false },
|
|
149
|
+
},
|
|
150
|
+
)
|