@pantoia/ui 0.13.0 → 0.14.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/avatar-group/index.module.css +3 -0
- package/avatar-group/index.tsx +52 -0
- package/bottom-sheet/index.tsx +15 -1
- package/combobox/index.tsx +7 -3
- package/context-menu/index.tsx +16 -4
- package/dialog/index.tsx +15 -1
- package/drawer/index.tsx +15 -1
- package/dropdown-menu/index.tsx +106 -47
- package/modal/index.tsx +7 -2
- package/package.json +1 -1
- package/popover/index.tsx +12 -4
- package/registry.json +75 -0
- package/select/index.tsx +7 -1
- package/skills/pantoia-design/SKILL.md +7 -3
- package/swatch-picker/index.module.css +8 -0
- package/swatch-picker/index.tsx +176 -0
- package/toggle/index.tsx +49 -11
- package/toggle-group/index.tsx +76 -0
- package/tooltip/index.tsx +9 -2
- package/utilities.css +1 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Children, cloneElement, isValidElement, type ComponentProps, type ReactElement, type ReactNode } from 'react'
|
|
4
|
+
|
|
5
|
+
import { avatar, type AvatarProps } from '../avatar'
|
|
6
|
+
import { tv } from '../tv'
|
|
7
|
+
|
|
8
|
+
import s from './index.module.css'
|
|
9
|
+
|
|
10
|
+
type Size = NonNullable<AvatarProps['size']>
|
|
11
|
+
|
|
12
|
+
export const avatarGroup = tv({
|
|
13
|
+
base: 'inline-flex items-center',
|
|
14
|
+
|
|
15
|
+
variants: {
|
|
16
|
+
size: {
|
|
17
|
+
xs: '-space-x-1',
|
|
18
|
+
sm: '-space-x-1',
|
|
19
|
+
md: '-space-x-1.5',
|
|
20
|
+
lg: '-space-x-2',
|
|
21
|
+
xl: '-space-x-3',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
defaultVariants: { size: 'md' },
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export type AvatarGroupProps = Omit<ComponentProps<'div'>, 'className' | 'children'> & {
|
|
29
|
+
max?: number
|
|
30
|
+
size?: Size
|
|
31
|
+
renderOverflow?: (count: number) => ReactNode
|
|
32
|
+
className?: string
|
|
33
|
+
children?: ReactNode
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function AvatarGroup({ max, size = 'md', renderOverflow, className, children, ...rest }: AvatarGroupProps) {
|
|
37
|
+
const people = Children.toArray(children).filter(isValidElement) as ReactElement<AvatarProps>[]
|
|
38
|
+
const shown = max !== undefined && people.length > max ? people.slice(0, max) : people
|
|
39
|
+
const hidden = people.length - shown.length
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div role="group" {...rest} className={avatarGroup({ size, className: [s.group, className] })}>
|
|
43
|
+
{shown.map(person => cloneElement(person, { size }))}
|
|
44
|
+
|
|
45
|
+
{hidden > 0 && (
|
|
46
|
+
<span role="img" aria-label={`외 ${hidden}명`} className={avatar({ color: 'neutral', size })}>
|
|
47
|
+
{renderOverflow ? renderOverflow(hidden) : `+${hidden}`}
|
|
48
|
+
</span>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
package/bottom-sheet/index.tsx
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { Drawer as DrawerPrimitive } from '@base-ui/react/drawer'
|
|
4
4
|
import type { ComponentProps, ReactNode } from 'react'
|
|
5
5
|
|
|
6
|
+
import { Button } from '../button'
|
|
6
7
|
import { tv } from '../tv'
|
|
7
8
|
|
|
8
9
|
import s from './index.module.css'
|
|
@@ -62,11 +63,23 @@ export const bottomSheetHeader = tv({
|
|
|
62
63
|
export interface BottomSheetHeaderProps {
|
|
63
64
|
lead?: ReactNode
|
|
64
65
|
trail?: ReactNode
|
|
66
|
+
closable?: boolean
|
|
67
|
+
closeLabel?: string
|
|
65
68
|
className?: string
|
|
66
69
|
children?: ReactNode
|
|
67
70
|
}
|
|
68
71
|
|
|
69
|
-
|
|
72
|
+
function HeaderClose({ label }: { label: string }) {
|
|
73
|
+
return (
|
|
74
|
+
<DrawerPrimitive.Close render={<Button color="neutral" emphasis="ghost" shape="square" aria-label={label} />}>
|
|
75
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
76
|
+
<path d="M18 6 6 18M6 6l12 12" />
|
|
77
|
+
</svg>
|
|
78
|
+
</DrawerPrimitive.Close>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function BottomSheetHeader({ lead, trail, closable = false, closeLabel = '닫기', className, children }: BottomSheetHeaderProps) {
|
|
70
83
|
const ui = bottomSheetHeader()
|
|
71
84
|
|
|
72
85
|
return (
|
|
@@ -74,6 +87,7 @@ export function BottomSheetHeader({ lead, trail, className, children }: BottomSh
|
|
|
74
87
|
{lead && <div className={ui.lead()}>{lead}</div>}
|
|
75
88
|
<div className={ui.body()}>{children}</div>
|
|
76
89
|
{trail && <div className={ui.trail()}>{trail}</div>}
|
|
90
|
+
{closable && <div className={ui.trail()}><HeaderClose label={closeLabel} /></div>}
|
|
77
91
|
</div>
|
|
78
92
|
)
|
|
79
93
|
}
|
package/combobox/index.tsx
CHANGED
|
@@ -135,7 +135,9 @@ export function ComboboxCancel({ label = '지우기', keepMounted = true, classN
|
|
|
135
135
|
)
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
type ComboboxPlacement = Pick<ComponentProps<typeof ComboboxPrimitive.Positioner>, 'sideOffset' | 'alignOffset' | 'collisionAvoidance'>
|
|
139
|
+
|
|
140
|
+
export type ComboboxContentProps = Omit<ComponentProps<typeof ComboboxPrimitive.Popup>, 'className' | 'children'> & ComboboxPlacement & {
|
|
139
141
|
className?: string
|
|
140
142
|
children?: ComboboxPrimitive.List.Props['children'] | ComboboxPrimitive.List.Props['children'][]
|
|
141
143
|
}
|
|
@@ -148,7 +150,7 @@ const comboboxContent = tv({
|
|
|
148
150
|
|
|
149
151
|
const isEmpty = (child: unknown) => isValidElement(child) && child.type === ComboboxEmpty
|
|
150
152
|
|
|
151
|
-
export function ComboboxContent({ 'aria-label': label, className, children, ...rest }: ComboboxContentProps) {
|
|
153
|
+
export function ComboboxContent({ 'aria-label': label, sideOffset = 4, alignOffset, collisionAvoidance, className, children, ...rest }: ComboboxContentProps) {
|
|
152
154
|
const nodes = Array.isArray(children) ? children : [children]
|
|
153
155
|
const empty = nodes.filter(isEmpty) as ReactNode[]
|
|
154
156
|
const list = nodes.filter(node => !isEmpty(node))
|
|
@@ -157,7 +159,9 @@ export function ComboboxContent({ 'aria-label': label, className, children, ...r
|
|
|
157
159
|
<ComboboxPrimitive.Portal>
|
|
158
160
|
<ComboboxPrimitive.Positioner
|
|
159
161
|
align="start"
|
|
160
|
-
sideOffset={
|
|
162
|
+
sideOffset={sideOffset}
|
|
163
|
+
alignOffset={alignOffset}
|
|
164
|
+
collisionAvoidance={collisionAvoidance}
|
|
161
165
|
className="z-overlay min-w-max will-change-transform"
|
|
162
166
|
>
|
|
163
167
|
<ComboboxPrimitive.Popup {...rest} className={comboboxContent({ className })}>
|
package/context-menu/index.tsx
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { ContextMenu as ContextMenuPrimitive } from '@base-ui/react/context-menu'
|
|
4
|
-
import type { ComponentProps } from 'react'
|
|
4
|
+
import type { ComponentProps, ReactNode } from 'react'
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
DropdownMenuCheckboxItem,
|
|
8
8
|
DropdownMenuGroup,
|
|
9
9
|
DropdownMenuItem,
|
|
10
|
+
DropdownMenuItemDescription,
|
|
10
11
|
DropdownMenuRadioGroup,
|
|
11
12
|
DropdownMenuRadioItem,
|
|
12
13
|
DropdownMenuSeparator,
|
|
@@ -37,14 +38,21 @@ export function ContextMenuTrigger(props: ContextMenuTriggerProps) {
|
|
|
37
38
|
return <ContextMenuPrimitive.Trigger {...props} />
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
type ContextMenuPlacement = Pick<ComponentProps<typeof ContextMenuPrimitive.Positioner>, 'sideOffset' | 'alignOffset' | 'collisionAvoidance'>
|
|
42
|
+
|
|
43
|
+
export type ContextMenuContentProps = Omit<ComponentProps<typeof ContextMenuPrimitive.Popup>, 'className'> & ContextMenuPlacement & {
|
|
41
44
|
className?: string
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
export function ContextMenuContent({ className, ...rest }: ContextMenuContentProps) {
|
|
47
|
+
export function ContextMenuContent({ sideOffset, alignOffset, collisionAvoidance, className, ...rest }: ContextMenuContentProps) {
|
|
45
48
|
return (
|
|
46
49
|
<ContextMenuPrimitive.Portal>
|
|
47
|
-
<ContextMenuPrimitive.Positioner
|
|
50
|
+
<ContextMenuPrimitive.Positioner
|
|
51
|
+
sideOffset={sideOffset}
|
|
52
|
+
alignOffset={alignOffset}
|
|
53
|
+
collisionAvoidance={collisionAvoidance}
|
|
54
|
+
className="z-overlay min-w-max will-change-transform"
|
|
55
|
+
>
|
|
48
56
|
<ContextMenuPrimitive.Popup {...rest} className={menuPanel({ className })} />
|
|
49
57
|
</ContextMenuPrimitive.Positioner>
|
|
50
58
|
</ContextMenuPrimitive.Portal>
|
|
@@ -63,6 +71,10 @@ export function ContextMenuItem(props: ContextMenuItemProps) {
|
|
|
63
71
|
return <DropdownMenuItem {...props} />
|
|
64
72
|
}
|
|
65
73
|
|
|
74
|
+
export function ContextMenuItemDescription(props: { className?: string, children?: ReactNode }) {
|
|
75
|
+
return <DropdownMenuItemDescription {...props} />
|
|
76
|
+
}
|
|
77
|
+
|
|
66
78
|
export type ContextMenuCheckboxItemProps = DropdownMenuCheckboxItemProps
|
|
67
79
|
|
|
68
80
|
export function ContextMenuCheckboxItem(props: ContextMenuCheckboxItemProps) {
|
package/dialog/index.tsx
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'
|
|
4
4
|
import type { ComponentProps, ReactNode } from 'react'
|
|
5
5
|
|
|
6
|
+
import { Button } from '../button'
|
|
6
7
|
import { tv } from '../tv'
|
|
7
8
|
|
|
8
9
|
export const dialog = tv({
|
|
@@ -92,11 +93,23 @@ export const dialogHeader = tv({
|
|
|
92
93
|
export interface DialogHeaderProps {
|
|
93
94
|
lead?: ReactNode
|
|
94
95
|
trail?: ReactNode
|
|
96
|
+
closable?: boolean
|
|
97
|
+
closeLabel?: string
|
|
95
98
|
className?: string
|
|
96
99
|
children?: ReactNode
|
|
97
100
|
}
|
|
98
101
|
|
|
99
|
-
|
|
102
|
+
function HeaderClose({ label }: { label: string }) {
|
|
103
|
+
return (
|
|
104
|
+
<DialogPrimitive.Close render={<Button color="neutral" emphasis="ghost" shape="square" aria-label={label} />}>
|
|
105
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
106
|
+
<path d="M18 6 6 18M6 6l12 12" />
|
|
107
|
+
</svg>
|
|
108
|
+
</DialogPrimitive.Close>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function DialogHeader({ lead, trail, closable = false, closeLabel = '닫기', className, children }: DialogHeaderProps) {
|
|
100
113
|
const ui = dialogHeader()
|
|
101
114
|
|
|
102
115
|
return (
|
|
@@ -104,6 +117,7 @@ export function DialogHeader({ lead, trail, className, children }: DialogHeaderP
|
|
|
104
117
|
{lead && <div className={ui.lead()}>{lead}</div>}
|
|
105
118
|
<div className={ui.body()}>{children}</div>
|
|
106
119
|
{trail && <div className={ui.trail()}>{trail}</div>}
|
|
120
|
+
{closable && <div className={ui.trail()}><HeaderClose label={closeLabel} /></div>}
|
|
107
121
|
</div>
|
|
108
122
|
)
|
|
109
123
|
}
|
package/drawer/index.tsx
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { Drawer as DrawerPrimitive } from '@base-ui/react/drawer'
|
|
4
4
|
import { createContext, use, type ComponentProps, type ReactNode } from 'react'
|
|
5
5
|
|
|
6
|
+
import { Button } from '../button'
|
|
6
7
|
import { tv } from '../tv'
|
|
7
8
|
|
|
8
9
|
import s from './index.module.css'
|
|
@@ -85,11 +86,23 @@ export const drawerHeader = tv({
|
|
|
85
86
|
export interface DrawerHeaderProps {
|
|
86
87
|
lead?: ReactNode
|
|
87
88
|
trail?: ReactNode
|
|
89
|
+
closable?: boolean
|
|
90
|
+
closeLabel?: string
|
|
88
91
|
className?: string
|
|
89
92
|
children?: ReactNode
|
|
90
93
|
}
|
|
91
94
|
|
|
92
|
-
|
|
95
|
+
function HeaderClose({ label }: { label: string }) {
|
|
96
|
+
return (
|
|
97
|
+
<DrawerPrimitive.Close render={<Button color="neutral" emphasis="ghost" shape="square" aria-label={label} />}>
|
|
98
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
99
|
+
<path d="M18 6 6 18M6 6l12 12" />
|
|
100
|
+
</svg>
|
|
101
|
+
</DrawerPrimitive.Close>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function DrawerHeader({ lead, trail, closable = false, closeLabel = '닫기', className, children }: DrawerHeaderProps) {
|
|
93
106
|
const ui = drawerHeader()
|
|
94
107
|
|
|
95
108
|
return (
|
|
@@ -97,6 +110,7 @@ export function DrawerHeader({ lead, trail, className, children }: DrawerHeaderP
|
|
|
97
110
|
{lead && <div className={ui.lead()}>{lead}</div>}
|
|
98
111
|
<div className={ui.body()}>{children}</div>
|
|
99
112
|
{trail && <div className={ui.trail()}>{trail}</div>}
|
|
113
|
+
{closable && <div className={ui.trail()}><HeaderClose label={closeLabel} /></div>}
|
|
100
114
|
</div>
|
|
101
115
|
)
|
|
102
116
|
}
|
package/dropdown-menu/index.tsx
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { Menu as MenuPrimitive } from '@base-ui/react/menu'
|
|
4
|
-
import type
|
|
4
|
+
import { createContext, use, useEffect, useId, useState, type ComponentProps, type ReactNode } from 'react'
|
|
5
5
|
|
|
6
6
|
import { tv } from '../tv'
|
|
7
7
|
|
|
8
8
|
export const menuItem = tv({
|
|
9
|
-
base: 'flex h-9 cursor-default items-center gap-2 rounded-r8 px-2 menu-gutter:ps-8'
|
|
9
|
+
base: 'flex min-h-9 cursor-default items-center gap-2 rounded-r8 px-2 py-1.5 menu-gutter:ps-8'
|
|
10
10
|
+ ' [&_svg]:size-4 [&_svg]:shrink-0'
|
|
11
|
+
+ ' described:items-start described:[&>.menu-lead]:mt-0.5 described:[&>svg]:mt-0.5'
|
|
11
12
|
+ ' text-label transition-state'
|
|
12
13
|
+ ' data-highlighted:outline-none data-disabled:pointer-events-none',
|
|
13
14
|
|
|
@@ -21,6 +22,20 @@ export const menuItem = tv({
|
|
|
21
22
|
defaultVariants: { color: 'neutral' },
|
|
22
23
|
})
|
|
23
24
|
|
|
25
|
+
interface DescriptionContext {
|
|
26
|
+
id: string
|
|
27
|
+
setDescribed: (described: boolean) => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ItemDescription = createContext<DescriptionContext | null>(null)
|
|
31
|
+
|
|
32
|
+
function useDescription() {
|
|
33
|
+
const id = useId()
|
|
34
|
+
const [described, setDescribed] = useState(false)
|
|
35
|
+
|
|
36
|
+
return { context: { id, setDescribed }, describedBy: described ? id : undefined }
|
|
37
|
+
}
|
|
38
|
+
|
|
24
39
|
const PANEL = 'menu float text-fg rounded-r12 z-overlay'
|
|
25
40
|
+ ' flex min-w-44 flex-col gap-px p-1'
|
|
26
41
|
+ ' open:animate-enter closed:animate-exit'
|
|
@@ -38,7 +53,9 @@ export function DropdownMenuTrigger(props: DropdownMenuTriggerProps) {
|
|
|
38
53
|
return <MenuPrimitive.Trigger {...props} />
|
|
39
54
|
}
|
|
40
55
|
|
|
41
|
-
|
|
56
|
+
type MenuPlacement = Pick<ComponentProps<typeof MenuPrimitive.Positioner>, 'anchor' | 'sideOffset' | 'alignOffset' | 'collisionAvoidance'>
|
|
57
|
+
|
|
58
|
+
export type DropdownMenuContentProps = Omit<ComponentProps<typeof MenuPrimitive.Popup>, 'className'> & MenuPlacement & {
|
|
42
59
|
side?: 'top' | 'right' | 'bottom' | 'left'
|
|
43
60
|
align?: 'start' | 'center' | 'end'
|
|
44
61
|
arrow?: boolean
|
|
@@ -59,6 +76,10 @@ export function DropdownMenuContent({
|
|
|
59
76
|
side = 'bottom',
|
|
60
77
|
align = 'start',
|
|
61
78
|
arrow = true,
|
|
79
|
+
anchor,
|
|
80
|
+
sideOffset,
|
|
81
|
+
alignOffset,
|
|
82
|
+
collisionAvoidance,
|
|
62
83
|
className,
|
|
63
84
|
children,
|
|
64
85
|
...rest
|
|
@@ -66,9 +87,12 @@ export function DropdownMenuContent({
|
|
|
66
87
|
return (
|
|
67
88
|
<MenuPrimitive.Portal>
|
|
68
89
|
<MenuPrimitive.Positioner
|
|
90
|
+
anchor={anchor}
|
|
69
91
|
side={side}
|
|
70
92
|
align={align}
|
|
71
|
-
sideOffset={arrow ? 14 : 8}
|
|
93
|
+
sideOffset={sideOffset ?? (arrow ? 14 : 8)}
|
|
94
|
+
alignOffset={alignOffset}
|
|
95
|
+
collisionAvoidance={collisionAvoidance}
|
|
72
96
|
arrowPadding={16}
|
|
73
97
|
className="z-overlay min-w-max will-change-transform"
|
|
74
98
|
>
|
|
@@ -116,17 +140,21 @@ export type DropdownMenuItemProps = Omit<ComponentProps<typeof MenuPrimitive.Ite
|
|
|
116
140
|
}
|
|
117
141
|
|
|
118
142
|
export function DropdownMenuItem({ color, lead, trail, className, children, ...rest }: DropdownMenuItemProps) {
|
|
119
|
-
|
|
120
|
-
<MenuPrimitive.Item {...rest} className={menuItem({ color, className })}>
|
|
121
|
-
{lead && (
|
|
122
|
-
<span className="menu-lead grid size-4 shrink-0 place-items-center">
|
|
123
|
-
{lead}
|
|
124
|
-
</span>
|
|
125
|
-
)}
|
|
143
|
+
const { context, describedBy } = useDescription()
|
|
126
144
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
145
|
+
return (
|
|
146
|
+
<ItemDescription value={context}>
|
|
147
|
+
<MenuPrimitive.Item aria-describedby={describedBy} {...rest} className={menuItem({ color, className })}>
|
|
148
|
+
{lead && (
|
|
149
|
+
<span className="menu-lead grid size-4 shrink-0 place-items-center">
|
|
150
|
+
{lead}
|
|
151
|
+
</span>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
<span className="min-w-0 grow">{children}</span>
|
|
155
|
+
{trail}
|
|
156
|
+
</MenuPrimitive.Item>
|
|
157
|
+
</ItemDescription>
|
|
130
158
|
)
|
|
131
159
|
}
|
|
132
160
|
|
|
@@ -135,18 +163,22 @@ export type DropdownMenuCheckboxItemProps = Omit<ComponentProps<typeof MenuPrimi
|
|
|
135
163
|
}
|
|
136
164
|
|
|
137
165
|
export function DropdownMenuCheckboxItem({ className, children, ...rest }: DropdownMenuCheckboxItemProps) {
|
|
166
|
+
const { context, describedBy } = useDescription()
|
|
167
|
+
|
|
138
168
|
return (
|
|
139
|
-
<
|
|
140
|
-
<
|
|
141
|
-
<
|
|
142
|
-
<
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
169
|
+
<ItemDescription value={context}>
|
|
170
|
+
<MenuPrimitive.CheckboxItem aria-describedby={describedBy} {...rest} className={menuItem({ className })}>
|
|
171
|
+
<span className="menu-lead grid size-4 shrink-0 place-items-center">
|
|
172
|
+
<MenuPrimitive.CheckboxItemIndicator>
|
|
173
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-4">
|
|
174
|
+
<path d="M20 6 9 17l-5-5" />
|
|
175
|
+
</svg>
|
|
176
|
+
</MenuPrimitive.CheckboxItemIndicator>
|
|
177
|
+
</span>
|
|
178
|
+
|
|
179
|
+
<span className="min-w-0 grow">{children}</span>
|
|
180
|
+
</MenuPrimitive.CheckboxItem>
|
|
181
|
+
</ItemDescription>
|
|
150
182
|
)
|
|
151
183
|
}
|
|
152
184
|
|
|
@@ -161,16 +193,39 @@ export type DropdownMenuRadioItemProps = Omit<ComponentProps<typeof MenuPrimitiv
|
|
|
161
193
|
}
|
|
162
194
|
|
|
163
195
|
export function DropdownMenuRadioItem({ className, children, ...rest }: DropdownMenuRadioItemProps) {
|
|
196
|
+
const { context, describedBy } = useDescription()
|
|
197
|
+
|
|
164
198
|
return (
|
|
165
|
-
<
|
|
166
|
-
<
|
|
167
|
-
<
|
|
168
|
-
<
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
199
|
+
<ItemDescription value={context}>
|
|
200
|
+
<MenuPrimitive.RadioItem aria-describedby={describedBy} {...rest} className={menuItem({ className })}>
|
|
201
|
+
<span className="menu-lead grid size-4 shrink-0 place-items-center">
|
|
202
|
+
<MenuPrimitive.RadioItemIndicator>
|
|
203
|
+
<span className="bg-bg-neutral-solid in-disabled:bg-bg-neutral-solid-checked-disabled block size-2 rounded-full" />
|
|
204
|
+
</MenuPrimitive.RadioItemIndicator>
|
|
205
|
+
</span>
|
|
206
|
+
|
|
207
|
+
<span className="min-w-0 grow">{children}</span>
|
|
208
|
+
</MenuPrimitive.RadioItem>
|
|
209
|
+
</ItemDescription>
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const menuItemDescription = tv({ base: 'text-caption text-fg-subtle block' })
|
|
214
|
+
|
|
215
|
+
export function DropdownMenuItemDescription({ className, children }: { className?: string, children?: ReactNode }) {
|
|
216
|
+
const ctx = use(ItemDescription)
|
|
217
|
+
const setDescribed = ctx?.setDescribed
|
|
218
|
+
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
if (!setDescribed) return
|
|
221
|
+
setDescribed(true)
|
|
222
|
+
return () => setDescribed(false)
|
|
223
|
+
}, [setDescribed])
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<span id={ctx?.id} data-description aria-hidden="true" className={menuItemDescription({ className })}>
|
|
227
|
+
{children}
|
|
228
|
+
</span>
|
|
174
229
|
)
|
|
175
230
|
}
|
|
176
231
|
|
|
@@ -196,20 +251,24 @@ export type DropdownMenuSubTriggerProps = Omit<ComponentProps<typeof MenuPrimiti
|
|
|
196
251
|
}
|
|
197
252
|
|
|
198
253
|
export function DropdownMenuSubTrigger({ lead, className, children, ...rest }: DropdownMenuSubTriggerProps) {
|
|
199
|
-
|
|
200
|
-
<MenuPrimitive.SubmenuTrigger {...rest} className={menuItem({ className })}>
|
|
201
|
-
{lead && (
|
|
202
|
-
<span className="menu-lead grid size-4 shrink-0 place-items-center">
|
|
203
|
-
{lead}
|
|
204
|
-
</span>
|
|
205
|
-
)}
|
|
254
|
+
const { context, describedBy } = useDescription()
|
|
206
255
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
<
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
256
|
+
return (
|
|
257
|
+
<ItemDescription value={context}>
|
|
258
|
+
<MenuPrimitive.SubmenuTrigger aria-describedby={describedBy} {...rest} className={menuItem({ className })}>
|
|
259
|
+
{lead && (
|
|
260
|
+
<span className="menu-lead grid size-4 shrink-0 place-items-center">
|
|
261
|
+
{lead}
|
|
262
|
+
</span>
|
|
263
|
+
)}
|
|
264
|
+
|
|
265
|
+
<span className="min-w-0 grow">{children}</span>
|
|
266
|
+
|
|
267
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="text-fg-subtle size-4 shrink-0">
|
|
268
|
+
<path d="m9 18 6-6-6-6" />
|
|
269
|
+
</svg>
|
|
270
|
+
</MenuPrimitive.SubmenuTrigger>
|
|
271
|
+
</ItemDescription>
|
|
213
272
|
)
|
|
214
273
|
}
|
|
215
274
|
|
package/modal/index.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
BottomSheetClose,
|
|
10
10
|
BottomSheetContent,
|
|
11
11
|
BottomSheetDescription,
|
|
12
|
+
BottomSheetHeader,
|
|
12
13
|
BottomSheetTitle,
|
|
13
14
|
BottomSheetTrigger,
|
|
14
15
|
} from '../bottom-sheet'
|
|
@@ -38,7 +39,11 @@ export const modalQuery: Record<ModalBreakpoint, string> = {
|
|
|
38
39
|
md: '(min-width: 48rem)',
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
const ModalShape = createContext<Shape>(
|
|
42
|
+
const ModalShape = createContext<Shape | null>(null)
|
|
43
|
+
|
|
44
|
+
export function useModalShape(): Shape | null {
|
|
45
|
+
return use(ModalShape)
|
|
46
|
+
}
|
|
42
47
|
|
|
43
48
|
const media = (query: string) => (typeof window.matchMedia === 'function' ? window.matchMedia(query) : null)
|
|
44
49
|
|
|
@@ -109,7 +114,7 @@ export function ModalContent({ size, ...rest }: ModalContentProps) {
|
|
|
109
114
|
export type ModalHeaderProps = DialogHeaderProps
|
|
110
115
|
|
|
111
116
|
export function ModalHeader(props: ModalHeaderProps) {
|
|
112
|
-
return <DialogHeader {...props} />
|
|
117
|
+
return use(ModalShape) === 'dialog' ? <DialogHeader {...props} /> : <BottomSheetHeader {...props} />
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
export interface ModalTitleProps {
|
package/package.json
CHANGED
package/popover/index.tsx
CHANGED
|
@@ -48,7 +48,9 @@ export function PopoverClose(props: PopoverCloseProps) {
|
|
|
48
48
|
return <PopoverPrimitive.Close {...props} />
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
type PopoverPlacement = Pick<ComponentProps<typeof PopoverPrimitive.Positioner>, 'anchor' | 'sideOffset' | 'alignOffset' | 'collisionAvoidance'>
|
|
52
|
+
|
|
53
|
+
export type PopoverContentProps = Omit<ComponentProps<typeof PopoverPrimitive.Popup>, 'className'> & PopoverPlacement & {
|
|
52
54
|
side?: 'top' | 'right' | 'bottom' | 'left'
|
|
53
55
|
align?: 'start' | 'center' | 'end'
|
|
54
56
|
arrow?: boolean
|
|
@@ -73,19 +75,25 @@ export function PopoverContent({
|
|
|
73
75
|
side = 'bottom',
|
|
74
76
|
align = 'center',
|
|
75
77
|
arrow = true,
|
|
78
|
+
anchor,
|
|
79
|
+
sideOffset,
|
|
80
|
+
alignOffset,
|
|
81
|
+
collisionAvoidance,
|
|
76
82
|
className,
|
|
77
83
|
children,
|
|
78
84
|
...rest
|
|
79
85
|
}: PopoverContentProps) {
|
|
80
|
-
const [
|
|
86
|
+
const [anchorPart] = useContext(AnchorContext)
|
|
81
87
|
|
|
82
88
|
return (
|
|
83
89
|
<PopoverPrimitive.Portal>
|
|
84
90
|
<PopoverPrimitive.Positioner
|
|
85
|
-
anchor={anchor ?? undefined}
|
|
91
|
+
anchor={anchor ?? anchorPart ?? undefined}
|
|
86
92
|
side={side}
|
|
87
93
|
align={align}
|
|
88
|
-
sideOffset={arrow ? 14 : 8}
|
|
94
|
+
sideOffset={sideOffset ?? (arrow ? 14 : 8)}
|
|
95
|
+
alignOffset={alignOffset}
|
|
96
|
+
collisionAvoidance={collisionAvoidance}
|
|
89
97
|
arrowPadding={16}
|
|
90
98
|
className="z-overlay min-w-max will-change-transform"
|
|
91
99
|
>
|
package/registry.json
CHANGED
|
@@ -87,6 +87,29 @@
|
|
|
87
87
|
"layer": "ui"
|
|
88
88
|
}
|
|
89
89
|
},
|
|
90
|
+
{
|
|
91
|
+
"name": "avatar-group",
|
|
92
|
+
"type": "registry:ui",
|
|
93
|
+
"title": "AvatarGroup",
|
|
94
|
+
"files": [
|
|
95
|
+
{
|
|
96
|
+
"path": "avatar-group/index.tsx",
|
|
97
|
+
"type": "registry:ui"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"path": "avatar-group/index.module.css",
|
|
101
|
+
"type": "registry:ui"
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
"dependencies": [],
|
|
105
|
+
"registryDependencies": [
|
|
106
|
+
"avatar",
|
|
107
|
+
"tv"
|
|
108
|
+
],
|
|
109
|
+
"meta": {
|
|
110
|
+
"layer": "ui"
|
|
111
|
+
}
|
|
112
|
+
},
|
|
90
113
|
{
|
|
91
114
|
"name": "badge",
|
|
92
115
|
"type": "registry:ui",
|
|
@@ -125,6 +148,7 @@
|
|
|
125
148
|
"@base-ui/react"
|
|
126
149
|
],
|
|
127
150
|
"registryDependencies": [
|
|
151
|
+
"button",
|
|
128
152
|
"tv"
|
|
129
153
|
],
|
|
130
154
|
"meta": {
|
|
@@ -295,6 +319,7 @@
|
|
|
295
319
|
"@base-ui/react"
|
|
296
320
|
],
|
|
297
321
|
"registryDependencies": [
|
|
322
|
+
"button",
|
|
298
323
|
"tv"
|
|
299
324
|
],
|
|
300
325
|
"meta": {
|
|
@@ -320,6 +345,7 @@
|
|
|
320
345
|
"@base-ui/react"
|
|
321
346
|
],
|
|
322
347
|
"registryDependencies": [
|
|
348
|
+
"button",
|
|
323
349
|
"tv"
|
|
324
350
|
],
|
|
325
351
|
"meta": {
|
|
@@ -615,6 +641,31 @@
|
|
|
615
641
|
"layer": "ui"
|
|
616
642
|
}
|
|
617
643
|
},
|
|
644
|
+
{
|
|
645
|
+
"name": "swatch-picker",
|
|
646
|
+
"type": "registry:ui",
|
|
647
|
+
"title": "SwatchPicker",
|
|
648
|
+
"files": [
|
|
649
|
+
{
|
|
650
|
+
"path": "swatch-picker/index.tsx",
|
|
651
|
+
"type": "registry:ui"
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
"path": "swatch-picker/index.module.css",
|
|
655
|
+
"type": "registry:ui"
|
|
656
|
+
}
|
|
657
|
+
],
|
|
658
|
+
"dependencies": [
|
|
659
|
+
"@base-ui/react"
|
|
660
|
+
],
|
|
661
|
+
"registryDependencies": [
|
|
662
|
+
"tooltip",
|
|
663
|
+
"tv"
|
|
664
|
+
],
|
|
665
|
+
"meta": {
|
|
666
|
+
"layer": "ui"
|
|
667
|
+
}
|
|
668
|
+
},
|
|
618
669
|
{
|
|
619
670
|
"name": "switch",
|
|
620
671
|
"type": "registry:ui",
|
|
@@ -746,6 +797,30 @@
|
|
|
746
797
|
]
|
|
747
798
|
}
|
|
748
799
|
},
|
|
800
|
+
{
|
|
801
|
+
"name": "toggle-group",
|
|
802
|
+
"type": "registry:ui",
|
|
803
|
+
"title": "ToggleGroup",
|
|
804
|
+
"files": [
|
|
805
|
+
{
|
|
806
|
+
"path": "toggle-group/index.tsx",
|
|
807
|
+
"type": "registry:ui"
|
|
808
|
+
}
|
|
809
|
+
],
|
|
810
|
+
"dependencies": [
|
|
811
|
+
"@base-ui/react"
|
|
812
|
+
],
|
|
813
|
+
"registryDependencies": [
|
|
814
|
+
"toggle",
|
|
815
|
+
"tv"
|
|
816
|
+
],
|
|
817
|
+
"meta": {
|
|
818
|
+
"layer": "ui",
|
|
819
|
+
"sizes": [
|
|
820
|
+
"ToggleGroupItem"
|
|
821
|
+
]
|
|
822
|
+
}
|
|
823
|
+
},
|
|
749
824
|
{
|
|
750
825
|
"name": "tooltip",
|
|
751
826
|
"type": "registry:ui",
|
package/select/index.tsx
CHANGED
|
@@ -95,6 +95,7 @@ export type SelectProps = Omit<ComponentProps<typeof SelectPrimitive.Trigger>, '
|
|
|
95
95
|
arrow?: boolean
|
|
96
96
|
className?: string
|
|
97
97
|
}
|
|
98
|
+
& Pick<ComponentProps<typeof SelectPrimitive.Positioner>, 'sideOffset' | 'alignOffset' | 'collisionAvoidance'>
|
|
98
99
|
|
|
99
100
|
export function Select({
|
|
100
101
|
value,
|
|
@@ -113,6 +114,9 @@ export function Select({
|
|
|
113
114
|
surface,
|
|
114
115
|
size,
|
|
115
116
|
arrow = false,
|
|
117
|
+
sideOffset,
|
|
118
|
+
alignOffset,
|
|
119
|
+
collisionAvoidance,
|
|
116
120
|
className,
|
|
117
121
|
children,
|
|
118
122
|
...rest
|
|
@@ -166,7 +170,9 @@ export function Select({
|
|
|
166
170
|
<SelectPrimitive.Positioner
|
|
167
171
|
alignItemWithTrigger={false}
|
|
168
172
|
align="start"
|
|
169
|
-
sideOffset={arrow ? 14 : 8}
|
|
173
|
+
sideOffset={sideOffset ?? (arrow ? 14 : 8)}
|
|
174
|
+
alignOffset={alignOffset}
|
|
175
|
+
collisionAvoidance={collisionAvoidance}
|
|
170
176
|
arrowPadding={16}
|
|
171
177
|
className="z-overlay min-w-max will-change-transform"
|
|
172
178
|
>
|
|
@@ -148,6 +148,7 @@ className 그 한 곳만 바뀐다
|
|
|
148
148
|
| Accordion | accordion | Accordion, AccordionItem, AccordionTrigger, AccordionContent | - |
|
|
149
149
|
| AlertDialog | alert-dialog | AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogTitle, AlertDialogDescription, AlertDialogHeader, AlertDialogBody, AlertDialogFooter, AlertDialogAction, AlertDialogCancel | size |
|
|
150
150
|
| Avatar | avatar | Avatar | color, size |
|
|
151
|
+
| AvatarGroup | avatar-group | AvatarGroup | size |
|
|
151
152
|
| Badge | badge | Badge | color, emphasis, shape |
|
|
152
153
|
| BottomSheet | bottom-sheet | BottomSheet, BottomSheetTrigger, BottomSheetClose, BottomSheetContent, BottomSheetHeader, BottomSheetTitle, BottomSheetDescription, BottomSheetBody, BottomSheetFooter | - |
|
|
153
154
|
| Button | button | Button | color, emphasis, size, shape |
|
|
@@ -156,10 +157,10 @@ className 그 한 곳만 바뀐다
|
|
|
156
157
|
| Collapsible | collapsible | Collapsible, CollapsibleTrigger, CollapsibleContent | - |
|
|
157
158
|
| Combobox | combobox | Combobox, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxCancel, ComboboxContent, ComboboxViewport, ComboboxCollection, ComboboxItem, ComboboxItemText, ComboboxItemDescription, ComboboxGroup, ComboboxEmpty, ComboboxSeparator | surface, size |
|
|
158
159
|
| CommandPalette | command-palette | CommandPalette | - |
|
|
159
|
-
| ContextMenu | context-menu | ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuCheckboxItem, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuSub, ContextMenuSubTrigger, ContextMenuSubContent | color |
|
|
160
|
+
| ContextMenu | context-menu | ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuItemDescription, ContextMenuCheckboxItem, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuSub, ContextMenuSubTrigger, ContextMenuSubContent | color |
|
|
160
161
|
| Dialog | dialog | Dialog, DialogTrigger, DialogClose, DialogContent, DialogTitle, DialogDescription, DialogHeader, DialogBody, DialogFooter | size |
|
|
161
162
|
| Drawer | drawer | Drawer, DrawerTrigger, DrawerClose, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription, DrawerBody, DrawerFooter | side |
|
|
162
|
-
| DropdownMenu | dropdown-menu | DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent | color |
|
|
163
|
+
| DropdownMenu | dropdown-menu | DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuItemDescription, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent | color |
|
|
163
164
|
| Field | field | Field, FieldLabel, FieldHelp, FieldError, FieldPending, FieldSuccess | layout |
|
|
164
165
|
| Fieldset | fieldset | Fieldset, FieldsetLegend | - |
|
|
165
166
|
| Input | input | Input | surface, size |
|
|
@@ -173,12 +174,14 @@ className 그 한 곳만 바뀐다
|
|
|
173
174
|
| SegmentedControl | segmented-control | SegmentedControl, SegmentedControlItem | size, fill |
|
|
174
175
|
| Select | select | Select, SelectGroup, SelectItem, SelectItemText, SelectItemDescription, SelectSeparator | surface, size |
|
|
175
176
|
| Separator | separator | Separator | orientation |
|
|
177
|
+
| SwatchPicker | swatch-picker | SwatchPicker, SwatchPickerItem, SwatchPickerCustom | size |
|
|
176
178
|
| Switch | switch | Switch | color |
|
|
177
179
|
| Table | table | Table, TableHeader, TableHeadCell, TableBody, TableRow, TableCell | fixed, mono |
|
|
178
180
|
| Tabs | tabs | Tabs, TabsList, TabsTrigger, TabsContent | - |
|
|
179
181
|
| Textarea | textarea | Textarea | surface, size |
|
|
180
182
|
| Toast | toast | Toast | - |
|
|
181
|
-
| Toggle | toggle | Toggle | surface |
|
|
183
|
+
| Toggle | toggle | Toggle | size, shape, color, surface |
|
|
184
|
+
| ToggleGroup | toggle-group | ToggleGroup, ToggleGroupItem | size, shape, surface |
|
|
182
185
|
| Tooltip | tooltip | TooltipProvider, Tooltip, TooltipTrigger, TooltipContent | - |
|
|
183
186
|
|
|
184
187
|
컴포넌트를 고를 때 헷갈리는 짝은 이렇다.
|
|
@@ -196,6 +199,7 @@ Badge와 Button 누를 수 없으면 Badge, 누르면 Button
|
|
|
196
199
|
Tabs와 Accordion 한 번에 하나만 보이면 Tabs, 여러 개를 펴 두면 Accordion
|
|
197
200
|
SegmentedControl과 Tabs 같은 내용의 보기 방식을 바꾸면 SegmentedControl, 다른 내용으로 바꾸면 Tabs
|
|
198
201
|
SegmentedControl과 Toggle 여럿 중 하나를 고르면 SegmentedControl, 하나를 켜고 끄면 Toggle
|
|
202
|
+
SegmentedControl과 ToggleGroup 이어진 트랙 안에서 고르면 SegmentedControl, 떨어진 pill 여러 개 중에 고르면 ToggleGroup
|
|
199
203
|
Collapsible과 Accordion 한 영역을 접으면 Collapsible, 여러 영역을 함께 다루면 Accordion
|
|
200
204
|
Tooltip과 Popover 짧은 설명이면 Tooltip, 안에서 조작하면 Popover
|
|
201
205
|
```
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Radio } from '@base-ui/react/radio'
|
|
4
|
+
import { RadioGroup } from '@base-ui/react/radio-group'
|
|
5
|
+
import { Children, createContext, isValidElement, use, useRef, useState, type CSSProperties, type ReactNode } from 'react'
|
|
6
|
+
|
|
7
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip'
|
|
8
|
+
import { tv } from '../tv'
|
|
9
|
+
|
|
10
|
+
import s from './index.module.css'
|
|
11
|
+
|
|
12
|
+
type Size = 'sm' | 'md' | 'lg'
|
|
13
|
+
|
|
14
|
+
interface PickerContext {
|
|
15
|
+
size: Size
|
|
16
|
+
value: string | null
|
|
17
|
+
choose: (value: string) => void
|
|
18
|
+
known: ReadonlySet<string>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const Context = createContext<PickerContext>({
|
|
22
|
+
size: 'md',
|
|
23
|
+
value: null,
|
|
24
|
+
choose: () => {},
|
|
25
|
+
known: new Set(),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export const swatchPicker = tv({
|
|
29
|
+
slots: {
|
|
30
|
+
root: 'flex flex-wrap gap-2',
|
|
31
|
+
item: 'inline-flex shrink-0 items-center justify-center rounded-full cursor-pointer'
|
|
32
|
+
+ ' inset-ring inset-ring-line transition-state press press-shrink'
|
|
33
|
+
+ ' checked:ring-2 checked:ring-line-neutral-solid checked:ring-offset-2 checked:ring-offset-bg'
|
|
34
|
+
+ ' focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-line-focus-ring'
|
|
35
|
+
+ ' disabled:cursor-not-allowed disabled:opacity-40',
|
|
36
|
+
custom: 'text-fg-muted inline-flex shrink-0 items-center justify-center rounded-full cursor-pointer'
|
|
37
|
+
+ ' ring-1 ring-inset ring-line-strong transition-state press press-shrink'
|
|
38
|
+
+ ' enabled:hover:bg-bg-neutral-hover'
|
|
39
|
+
+ ' focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-line-focus-ring'
|
|
40
|
+
+ ' disabled:text-fg-disabled disabled:ring-line disabled:cursor-not-allowed',
|
|
41
|
+
icon: 'size-4 shrink-0',
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
variants: {
|
|
45
|
+
size: {
|
|
46
|
+
sm: { item: 'size-6', custom: 'size-6', icon: 'size-3.5' },
|
|
47
|
+
md: { item: 'size-8', custom: 'size-8' },
|
|
48
|
+
lg: { item: 'size-10', custom: 'size-10', icon: 'size-5' },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
defaultVariants: { size: 'md' },
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
export type SwatchPickerProps = Omit<RadioGroup.Props<string | null>, 'className' | 'onValueChange'> & {
|
|
56
|
+
onValueChange?: (value: string) => void
|
|
57
|
+
size?: Size
|
|
58
|
+
className?: string
|
|
59
|
+
children?: ReactNode
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function SwatchPicker({ value, defaultValue = null, onValueChange, size = 'md', className, children, ...rest }: SwatchPickerProps) {
|
|
63
|
+
const [inner, setInner] = useState<string | null>(defaultValue)
|
|
64
|
+
const known = new Set(
|
|
65
|
+
Children.toArray(children)
|
|
66
|
+
.filter(child => isValidElement<SwatchPickerItemProps>(child) && child.type === SwatchPickerItem)
|
|
67
|
+
.map(child => (child as { props: SwatchPickerItemProps }).props.value),
|
|
68
|
+
)
|
|
69
|
+
const current = value === undefined ? inner : value
|
|
70
|
+
|
|
71
|
+
const choose = (next: string) => {
|
|
72
|
+
onValueChange?.(next)
|
|
73
|
+
if (value === undefined) setInner(next)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<Context value={{ size, value: current, choose, known }}>
|
|
78
|
+
<RadioGroup<string | null>
|
|
79
|
+
{...rest}
|
|
80
|
+
value={current}
|
|
81
|
+
onValueChange={next => next !== null && choose(next)}
|
|
82
|
+
className={swatchPicker({ size }).root({ className })}
|
|
83
|
+
>
|
|
84
|
+
{children}
|
|
85
|
+
</RadioGroup>
|
|
86
|
+
</Context>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function Swatch({ value, label, disabled }: { value: string, label: string, disabled?: boolean }) {
|
|
91
|
+
const { size } = use(Context)
|
|
92
|
+
const ui = swatchPicker({ size })
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<Tooltip>
|
|
96
|
+
<TooltipTrigger
|
|
97
|
+
render={(
|
|
98
|
+
<Radio.Root
|
|
99
|
+
value={value}
|
|
100
|
+
disabled={disabled}
|
|
101
|
+
nativeButton
|
|
102
|
+
render={<button type="button" />}
|
|
103
|
+
aria-label={label}
|
|
104
|
+
style={{ '--swatch-picker-color': value } as CSSProperties}
|
|
105
|
+
className={ui.item({ className: s.swatch })}
|
|
106
|
+
/>
|
|
107
|
+
)}
|
|
108
|
+
>
|
|
109
|
+
<Radio.Indicator className={`flex ${s.check}`}>
|
|
110
|
+
<svg className={ui.icon()} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={3} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
111
|
+
<path d="M20 6 9 17l-5-5" />
|
|
112
|
+
</svg>
|
|
113
|
+
</Radio.Indicator>
|
|
114
|
+
</TooltipTrigger>
|
|
115
|
+
<TooltipContent>{label}</TooltipContent>
|
|
116
|
+
</Tooltip>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface SwatchPickerItemProps {
|
|
121
|
+
value: string
|
|
122
|
+
label: string
|
|
123
|
+
disabled?: boolean
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function SwatchPickerItem(props: SwatchPickerItemProps) {
|
|
127
|
+
return <Swatch {...props} />
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const HEX = /^#[0-9a-f]{6}$/i
|
|
131
|
+
|
|
132
|
+
export interface SwatchPickerCustomProps {
|
|
133
|
+
label?: string
|
|
134
|
+
disabled?: boolean
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function SwatchPickerCustom({ label = '직접 고르기', disabled }: SwatchPickerCustomProps) {
|
|
138
|
+
const { size, value, choose, known } = use(Context)
|
|
139
|
+
const ui = swatchPicker({ size })
|
|
140
|
+
const input = useRef<HTMLInputElement>(null)
|
|
141
|
+
const extra = value !== null && !known.has(value) ? value : null
|
|
142
|
+
|
|
143
|
+
const open = () => {
|
|
144
|
+
const el = input.current
|
|
145
|
+
if (!el) return
|
|
146
|
+
if (typeof el.showPicker === 'function') el.showPicker()
|
|
147
|
+
else el.click()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<>
|
|
152
|
+
{extra && <Swatch value={extra} label={extra} disabled={disabled} />}
|
|
153
|
+
|
|
154
|
+
<Tooltip>
|
|
155
|
+
<TooltipTrigger
|
|
156
|
+
render={<button type="button" disabled={disabled} aria-label={label} onClick={open} className={ui.custom()} />}
|
|
157
|
+
>
|
|
158
|
+
<svg className={ui.icon()} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
159
|
+
<path d="M12 5v14M5 12h14" />
|
|
160
|
+
</svg>
|
|
161
|
+
</TooltipTrigger>
|
|
162
|
+
<TooltipContent>{label}</TooltipContent>
|
|
163
|
+
</Tooltip>
|
|
164
|
+
|
|
165
|
+
<input
|
|
166
|
+
ref={input}
|
|
167
|
+
type="color"
|
|
168
|
+
tabIndex={-1}
|
|
169
|
+
aria-hidden="true"
|
|
170
|
+
value={value !== null && HEX.test(value) ? value : '#000000'}
|
|
171
|
+
onChange={event => choose(event.target.value)}
|
|
172
|
+
className="sr-only"
|
|
173
|
+
/>
|
|
174
|
+
</>
|
|
175
|
+
)
|
|
176
|
+
}
|
package/toggle/index.tsx
CHANGED
|
@@ -5,19 +5,45 @@ import type { ComponentProps } from 'react'
|
|
|
5
5
|
|
|
6
6
|
import { tv } from '../tv'
|
|
7
7
|
|
|
8
|
+
export type ToggleSize = 'sm' | 'md' | 'lg'
|
|
9
|
+
export type ToggleShape = 'pill' | 'circle' | 'square'
|
|
10
|
+
export type ToggleColor = 'neutral' | 'brand' | 'info' | 'success' | 'warning' | 'danger'
|
|
11
|
+
export type ToggleSurface = 'bare' | 'well' | 'panel'
|
|
12
|
+
|
|
8
13
|
export const toggle = tv({
|
|
9
|
-
base: 'text-label
|
|
10
|
-
+ ' [&_svg]:size-4'
|
|
14
|
+
base: 'text-label inline-flex shrink-0 items-center justify-center gap-tight whitespace-nowrap'
|
|
11
15
|
+ ' ring-1 ring-inset ring-line-strong transition-state press press-shrink'
|
|
12
16
|
+ ' text-fg-muted'
|
|
13
17
|
+ ' enabled:hover:bg-bg-neutral-hover'
|
|
14
|
-
+ ' on:bg-bg-neutral-solid on:ring-line-neutral-solid on:enabled:text-fg-neutral-solid-contrast'
|
|
15
|
-
+ ' on:enabled:hover:bg-bg-neutral-solid-hover'
|
|
16
18
|
+ ' focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-line-focus-ring'
|
|
17
|
-
+ ' disabled:text-fg-disabled disabled:ring-line disabled:cursor-not-allowed'
|
|
18
|
-
+ ' on:disabled:bg-bg-neutral-solid-checked-disabled on:disabled:ring-transparent on:disabled:text-fg-neutral-solid-contrast',
|
|
19
|
+
+ ' disabled:text-fg-disabled disabled:ring-line disabled:cursor-not-allowed',
|
|
19
20
|
|
|
20
21
|
variants: {
|
|
22
|
+
size: {
|
|
23
|
+
sm: 'h-8 [&_svg]:size-4',
|
|
24
|
+
md: 'h-9 [&_svg]:size-4',
|
|
25
|
+
lg: 'h-10 text-t16 font-medium [&_svg]:size-5',
|
|
26
|
+
},
|
|
27
|
+
shape: {
|
|
28
|
+
pill: 'rounded-full',
|
|
29
|
+
circle: 'rounded-full',
|
|
30
|
+
square: 'rounded-r8',
|
|
31
|
+
},
|
|
32
|
+
color: {
|
|
33
|
+
neutral: 'on:bg-bg-neutral-solid on:ring-line-neutral-solid on:enabled:text-fg-neutral-solid-contrast'
|
|
34
|
+
+ ' on:enabled:hover:bg-bg-neutral-solid-hover'
|
|
35
|
+
+ ' on:disabled:bg-bg-neutral-solid-checked-disabled on:disabled:ring-transparent on:disabled:text-fg-neutral-solid-contrast',
|
|
36
|
+
brand: 'on:bg-bg-brand-weak on:ring-line-brand-weak on:enabled:text-fg-brand-contrast'
|
|
37
|
+
+ ' on:enabled:hover:bg-bg-brand-solid/10 on:disabled:text-fg-brand-contrast/40',
|
|
38
|
+
info: 'on:bg-bg-info-weak on:ring-line-info-weak on:enabled:text-fg-info-contrast'
|
|
39
|
+
+ ' on:enabled:hover:bg-bg-info-solid/10 on:disabled:text-fg-info-contrast/40',
|
|
40
|
+
success: 'on:bg-bg-success-weak on:ring-line-success-weak on:enabled:text-fg-success-contrast'
|
|
41
|
+
+ ' on:enabled:hover:bg-bg-success-solid/10 on:disabled:text-fg-success-contrast/40',
|
|
42
|
+
warning: 'on:bg-bg-warning-weak on:ring-line-warning-weak on:enabled:text-fg-warning-contrast'
|
|
43
|
+
+ ' on:enabled:hover:bg-bg-warning-solid/10 on:disabled:text-fg-warning-contrast/40',
|
|
44
|
+
danger: 'on:bg-bg-danger-weak on:ring-line-danger-weak on:enabled:text-fg-danger-contrast'
|
|
45
|
+
+ ' on:enabled:hover:bg-bg-danger-solid/10 on:disabled:text-fg-danger-contrast/40',
|
|
46
|
+
},
|
|
21
47
|
surface: {
|
|
22
48
|
bare: '',
|
|
23
49
|
well: 'bg-bg-well',
|
|
@@ -25,14 +51,26 @@ export const toggle = tv({
|
|
|
25
51
|
},
|
|
26
52
|
},
|
|
27
53
|
|
|
28
|
-
|
|
54
|
+
compoundVariants: [
|
|
55
|
+
{ shape: 'pill', size: 'sm', class: 'px-3' },
|
|
56
|
+
{ shape: 'pill', size: 'md', class: 'px-3.5' },
|
|
57
|
+
{ shape: 'pill', size: 'lg', class: 'px-4' },
|
|
58
|
+
{ shape: ['circle', 'square'], size: 'sm', class: 'w-8' },
|
|
59
|
+
{ shape: ['circle', 'square'], size: 'md', class: 'w-9' },
|
|
60
|
+
{ shape: ['circle', 'square'], size: 'lg', class: 'w-10' },
|
|
61
|
+
],
|
|
62
|
+
|
|
63
|
+
defaultVariants: { size: 'sm', shape: 'pill', color: 'neutral', surface: 'bare' },
|
|
29
64
|
})
|
|
30
65
|
|
|
31
|
-
export type ToggleProps = Omit<ComponentProps<typeof TogglePrimitive>, 'className'> & {
|
|
32
|
-
|
|
66
|
+
export type ToggleProps = Omit<ComponentProps<typeof TogglePrimitive>, 'className' | 'color'> & {
|
|
67
|
+
size?: ToggleSize
|
|
68
|
+
shape?: ToggleShape
|
|
69
|
+
color?: ToggleColor
|
|
70
|
+
surface?: ToggleSurface
|
|
33
71
|
className?: string
|
|
34
72
|
}
|
|
35
73
|
|
|
36
|
-
export function Toggle({ surface, className, ...rest }: ToggleProps) {
|
|
37
|
-
return <TogglePrimitive {...rest} className={toggle({ surface, className })} />
|
|
74
|
+
export function Toggle({ size, shape, color, surface, className, ...rest }: ToggleProps) {
|
|
75
|
+
return <TogglePrimitive {...rest} className={toggle({ size, shape, color, surface, className })} />
|
|
38
76
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Radio } from '@base-ui/react/radio'
|
|
4
|
+
import { RadioGroup } from '@base-ui/react/radio-group'
|
|
5
|
+
import { Toggle as TogglePrimitive } from '@base-ui/react/toggle'
|
|
6
|
+
import { ToggleGroup as ToggleGroupPrimitive } from '@base-ui/react/toggle-group'
|
|
7
|
+
import { createContext, use, type ReactNode } from 'react'
|
|
8
|
+
|
|
9
|
+
import { toggle, type ToggleColor, type ToggleShape, type ToggleSize, type ToggleSurface } from '../toggle'
|
|
10
|
+
import { tv } from '../tv'
|
|
11
|
+
|
|
12
|
+
interface Shared {
|
|
13
|
+
size: ToggleSize
|
|
14
|
+
shape: ToggleShape
|
|
15
|
+
surface: ToggleSurface
|
|
16
|
+
multiple: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const Context = createContext<Shared>({ size: 'sm', shape: 'pill', surface: 'bare', multiple: false })
|
|
20
|
+
|
|
21
|
+
const toggleGroup = tv({ base: 'flex flex-wrap gap-2' })
|
|
22
|
+
|
|
23
|
+
interface ToggleGroupLook {
|
|
24
|
+
size?: ToggleSize
|
|
25
|
+
shape?: ToggleShape
|
|
26
|
+
surface?: ToggleSurface
|
|
27
|
+
className?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type ToggleGroupSingleProps = Omit<RadioGroup.Props<string | null>, 'className'> & ToggleGroupLook & {
|
|
31
|
+
multiple?: false
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type ToggleGroupMultipleProps = Omit<ToggleGroupPrimitive.Props<string>, 'className' | 'multiple'> & ToggleGroupLook & {
|
|
35
|
+
multiple: true
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ToggleGroupProps = ToggleGroupSingleProps | ToggleGroupMultipleProps
|
|
39
|
+
|
|
40
|
+
export function ToggleGroup({ size = 'sm', shape = 'pill', surface = 'bare', className, ...rest }: ToggleGroupProps) {
|
|
41
|
+
const root = toggleGroup({ className })
|
|
42
|
+
|
|
43
|
+
if (rest.multiple) {
|
|
44
|
+
return (
|
|
45
|
+
<Context value={{ size, shape, surface, multiple: true }}>
|
|
46
|
+
<ToggleGroupPrimitive<string> {...rest} className={root} />
|
|
47
|
+
</Context>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const { multiple: _multiple, ...single } = rest
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Context value={{ size, shape, surface, multiple: false }}>
|
|
55
|
+
<RadioGroup<string | null> {...single} className={root} />
|
|
56
|
+
</Context>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ToggleGroupItemProps {
|
|
61
|
+
value: string
|
|
62
|
+
color?: ToggleColor
|
|
63
|
+
disabled?: boolean
|
|
64
|
+
'aria-label'?: string
|
|
65
|
+
className?: string
|
|
66
|
+
children?: ReactNode
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function ToggleGroupItem({ color, className, ...rest }: ToggleGroupItemProps) {
|
|
70
|
+
const { size, shape, surface, multiple } = use(Context)
|
|
71
|
+
const look = toggle({ size, shape, color, surface, className })
|
|
72
|
+
|
|
73
|
+
if (multiple) return <TogglePrimitive {...rest} className={look} />
|
|
74
|
+
|
|
75
|
+
return <Radio.Root {...rest} nativeButton render={<button type="button" />} className={look} />
|
|
76
|
+
}
|
package/tooltip/index.tsx
CHANGED
|
@@ -46,7 +46,9 @@ export function TooltipTrigger(props: TooltipTriggerProps) {
|
|
|
46
46
|
return <TooltipPrimitive.Trigger delay={inProvider ? undefined : 100} {...props} aria-describedby={open ? id : undefined} />
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
type TooltipPlacement = Pick<ComponentProps<typeof TooltipPrimitive.Positioner>, 'sideOffset' | 'alignOffset' | 'collisionAvoidance'>
|
|
50
|
+
|
|
51
|
+
export type TooltipContentProps = Omit<ComponentProps<typeof TooltipPrimitive.Popup>, 'className'> & TooltipPlacement & {
|
|
50
52
|
side?: 'top' | 'right' | 'bottom' | 'left'
|
|
51
53
|
arrow?: boolean
|
|
52
54
|
className?: string
|
|
@@ -69,6 +71,9 @@ const tooltipArrow = tv({
|
|
|
69
71
|
export function TooltipContent({
|
|
70
72
|
side = 'top',
|
|
71
73
|
arrow = true,
|
|
74
|
+
sideOffset,
|
|
75
|
+
alignOffset,
|
|
76
|
+
collisionAvoidance,
|
|
72
77
|
className,
|
|
73
78
|
children,
|
|
74
79
|
...rest
|
|
@@ -79,7 +84,9 @@ export function TooltipContent({
|
|
|
79
84
|
<TooltipPrimitive.Portal>
|
|
80
85
|
<TooltipPrimitive.Positioner
|
|
81
86
|
side={side}
|
|
82
|
-
sideOffset={arrow ? 11 : 6}
|
|
87
|
+
sideOffset={sideOffset ?? (arrow ? 11 : 6)}
|
|
88
|
+
alignOffset={alignOffset}
|
|
89
|
+
collisionAvoidance={collisionAvoidance}
|
|
83
90
|
className="z-overlay min-w-max will-change-transform"
|
|
84
91
|
>
|
|
85
92
|
<TooltipPrimitive.Popup {...rest} id={id} role="tooltip" className={tooltipContent({ className })}>
|
package/utilities.css
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
@custom-variant checked (&:where([data-checked], [data-indeterminate]));
|
|
14
14
|
@custom-variant open (&[data-open]);
|
|
15
15
|
@custom-variant closed (&[data-closed]);
|
|
16
|
-
@custom-variant on (
|
|
16
|
+
@custom-variant on (&:is([aria-pressed="true"], [aria-checked="true"]));
|
|
17
17
|
@custom-variant side-top (&[data-side="top"]);
|
|
18
18
|
@custom-variant side-right (&[data-side="right"]);
|
|
19
19
|
@custom-variant side-bottom (&[data-side="bottom"]);
|