@pantoia/ui 0.11.2 → 0.12.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/accordion/index.module.css +2 -1
- package/alert-dialog/index.tsx +55 -8
- package/bottom-sheet/index.module.css +4 -1
- package/bottom-sheet/index.tsx +48 -2
- package/collapsible/index.module.css +11 -0
- package/collapsible/index.tsx +30 -0
- package/combobox/index.tsx +28 -7
- package/dialog/index.tsx +48 -11
- package/drawer/index.module.css +5 -2
- package/drawer/index.tsx +48 -2
- package/fieldset/index.tsx +24 -0
- package/input/index.tsx +3 -1
- package/modal/index.tsx +141 -0
- package/number-field/index.tsx +14 -3
- package/package.json +1 -1
- package/progress/index.tsx +66 -0
- package/registry.json +125 -0
- package/segmented-control/index.tsx +63 -0
- package/select/index.tsx +18 -3
- package/separator/index.tsx +25 -0
- package/skills/pantoia-design/SKILL.md +25 -6
- package/textarea/index.tsx +4 -1
- package/tokens.css +3 -0
- package/utilities.css +0 -4
package/alert-dialog/index.tsx
CHANGED
|
@@ -19,25 +19,36 @@ export function AlertDialogTrigger(props: AlertDialogTriggerProps) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const alertDialogContent = tv({
|
|
22
|
-
base: '
|
|
23
|
-
+ ' w-full max-w-112 -translate-x-1/2 -translate-y-1/2'
|
|
22
|
+
base: 'float text-fg z-overlay fixed inset-x-4 top-1/2 mx-auto max-h-11/12 -translate-y-1/2'
|
|
24
23
|
+ ' rounded-r20 p-6 flex flex-col gap-stack overflow-y-auto'
|
|
25
24
|
+ ' open:animate-fade-in closed:animate-fade-out'
|
|
25
|
+
+ ' transition-state data-nested-dialog-open:scale-95 motion-reduce:data-nested-dialog-open:scale-100'
|
|
26
26
|
+ ' focus-visible:outline-none',
|
|
27
|
+
|
|
28
|
+
variants: {
|
|
29
|
+
size: {
|
|
30
|
+
sm: 'max-w-112',
|
|
31
|
+
md: 'max-w-120',
|
|
32
|
+
lg: 'max-w-160',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
defaultVariants: { size: 'sm' },
|
|
27
37
|
})
|
|
28
38
|
|
|
29
39
|
export type AlertDialogContentProps = Omit<ComponentProps<typeof AlertDialogPrimitive.Popup>, 'className'> & {
|
|
40
|
+
size?: 'sm' | 'md' | 'lg'
|
|
30
41
|
className?: string
|
|
31
42
|
}
|
|
32
43
|
|
|
33
|
-
export function AlertDialogContent({ className, children, ...rest }: AlertDialogContentProps) {
|
|
44
|
+
export function AlertDialogContent({ size, className, children, ...rest }: AlertDialogContentProps) {
|
|
34
45
|
return (
|
|
35
46
|
<AlertDialogPrimitive.Portal>
|
|
36
|
-
<AlertDialogPrimitive.Backdrop className="bg-bg-overlay z-overlay fixed inset-0 open:animate-fade-in closed:animate-fade-out" />
|
|
47
|
+
<AlertDialogPrimitive.Backdrop forceRender className="bg-bg-overlay z-overlay fixed inset-0 open:animate-fade-in closed:animate-fade-out" />
|
|
37
48
|
|
|
38
49
|
<AlertDialogPrimitive.Popup
|
|
39
50
|
{...rest}
|
|
40
|
-
className={alertDialogContent({ className })}
|
|
51
|
+
className={alertDialogContent({ size, className })}
|
|
41
52
|
>
|
|
42
53
|
{children}
|
|
43
54
|
</AlertDialogPrimitive.Popup>
|
|
@@ -98,11 +109,47 @@ export function AlertDialogHeader({ lead, trail, className, children }: AlertDia
|
|
|
98
109
|
)
|
|
99
110
|
}
|
|
100
111
|
|
|
101
|
-
const
|
|
112
|
+
const alertDialogBody = tv({ base: '-m-1 min-h-0 overflow-y-auto p-1' })
|
|
113
|
+
|
|
114
|
+
export function AlertDialogBody({ className, children }: { className?: string, children?: ReactNode }) {
|
|
115
|
+
return (
|
|
116
|
+
<div className={alertDialogBody({ className })}>
|
|
117
|
+
{children}
|
|
118
|
+
</div>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
type FooterLayout = 'end' | 'between' | 'fill' | 'grow-last'
|
|
123
|
+
|
|
124
|
+
const alertDialogFooter = tv({
|
|
125
|
+
base: 'mt-auto flex justify-end gap-2',
|
|
126
|
+
variants: {
|
|
127
|
+
narrow: {
|
|
128
|
+
'end': '',
|
|
129
|
+
'between': 'max-sm:*:first:mr-auto',
|
|
130
|
+
'fill': 'max-sm:*:flex-1',
|
|
131
|
+
'grow-last': 'max-sm:*:last:grow',
|
|
132
|
+
},
|
|
133
|
+
wide: {
|
|
134
|
+
'end': '',
|
|
135
|
+
'between': 'sm:*:first:mr-auto',
|
|
136
|
+
'fill': 'sm:*:flex-1',
|
|
137
|
+
'grow-last': 'sm:*:last:grow',
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
export interface AlertDialogFooterProps {
|
|
143
|
+
layout?: FooterLayout | { narrow: FooterLayout, wide: FooterLayout }
|
|
144
|
+
className?: string
|
|
145
|
+
children?: ReactNode
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function AlertDialogFooter({ layout = 'end', className, children }: AlertDialogFooterProps) {
|
|
149
|
+
const { narrow, wide } = typeof layout === 'string' ? { narrow: layout, wide: layout } : layout
|
|
102
150
|
|
|
103
|
-
export function AlertDialogFooter({ className, children }: { className?: string, children?: ReactNode }) {
|
|
104
151
|
return (
|
|
105
|
-
<div className={alertDialogFooter({ className })}>
|
|
152
|
+
<div className={alertDialogFooter({ narrow, wide, className })}>
|
|
106
153
|
{children}
|
|
107
154
|
</div>
|
|
108
155
|
)
|
|
@@ -4,13 +4,16 @@
|
|
|
4
4
|
.panel {
|
|
5
5
|
--bottom-sheet-slide: 100%;
|
|
6
6
|
transform: translateY(var(--drawer-swipe-movement-y, 0px));
|
|
7
|
-
|
|
7
|
+
transform-origin: center bottom;
|
|
8
|
+
transition: transform var(--dur-enter) var(--ease-enter), scale var(--dur-enter) var(--ease-enter);
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
.panel[data-open] { animation: slide-in var(--dur-enter) var(--ease-enter); }
|
|
11
12
|
.panel[data-closed] { animation: slide-out var(--dur-exit) var(--ease-exit) forwards; }
|
|
12
13
|
.panel[data-swiping] { transition-duration: 0ms; }
|
|
14
|
+
.panel[data-nested-drawer-open] { scale: 0.95; }
|
|
13
15
|
|
|
14
16
|
@media (prefers-reduced-motion: reduce) {
|
|
15
17
|
.panel { --bottom-sheet-slide: 0%; }
|
|
18
|
+
.panel[data-nested-drawer-open] { scale: 1; }
|
|
16
19
|
}
|
package/bottom-sheet/index.tsx
CHANGED
|
@@ -26,7 +26,7 @@ export function BottomSheetClose(props: BottomSheetCloseProps) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export const bottomSheetContent = tv({
|
|
29
|
-
base: '
|
|
29
|
+
base: 'float text-fg z-overlay fixed inset-x-0 bottom-0 mx-auto rounded-t-r20'
|
|
30
30
|
+ ' flex max-h-11/12 w-full flex-col gap-stack overflow-y-auto px-4 pb-4 sm:max-w-120'
|
|
31
31
|
+ ' focus-visible:outline-none',
|
|
32
32
|
})
|
|
@@ -38,7 +38,7 @@ export type BottomSheetContentProps = Omit<ComponentProps<typeof DrawerPrimitive
|
|
|
38
38
|
export function BottomSheetContent({ className, children, ...rest }: BottomSheetContentProps) {
|
|
39
39
|
return (
|
|
40
40
|
<DrawerPrimitive.Portal>
|
|
41
|
-
<DrawerPrimitive.Backdrop className="bg-bg-overlay z-overlay fixed inset-0 open:animate-fade-in closed:animate-fade-out" />
|
|
41
|
+
<DrawerPrimitive.Backdrop forceRender className="bg-bg-overlay z-overlay fixed inset-0 open:animate-fade-in closed:animate-fade-out" />
|
|
42
42
|
|
|
43
43
|
<DrawerPrimitive.Viewport>
|
|
44
44
|
<DrawerPrimitive.Popup {...rest} className={`${s.panel} ${bottomSheetContent({ className })}`}>
|
|
@@ -99,3 +99,49 @@ export function BottomSheetDescription({ className, ...rest }: BottomSheetDescri
|
|
|
99
99
|
<DrawerPrimitive.Description {...rest} className={bottomSheetDescription({ className })} />
|
|
100
100
|
)
|
|
101
101
|
}
|
|
102
|
+
|
|
103
|
+
const bottomSheetBody = tv({ base: '-m-1 min-h-0 overflow-y-auto p-1' })
|
|
104
|
+
|
|
105
|
+
export function BottomSheetBody({ className, children }: { className?: string, children?: ReactNode }) {
|
|
106
|
+
return (
|
|
107
|
+
<div className={bottomSheetBody({ className })}>
|
|
108
|
+
{children}
|
|
109
|
+
</div>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
type FooterLayout = 'end' | 'between' | 'fill' | 'grow-last'
|
|
114
|
+
|
|
115
|
+
const bottomSheetFooter = tv({
|
|
116
|
+
base: 'mt-auto flex justify-end gap-2',
|
|
117
|
+
variants: {
|
|
118
|
+
narrow: {
|
|
119
|
+
'end': '',
|
|
120
|
+
'between': 'max-sm:*:first:mr-auto',
|
|
121
|
+
'fill': 'max-sm:*:flex-1',
|
|
122
|
+
'grow-last': 'max-sm:*:last:grow',
|
|
123
|
+
},
|
|
124
|
+
wide: {
|
|
125
|
+
'end': '',
|
|
126
|
+
'between': 'sm:*:first:mr-auto',
|
|
127
|
+
'fill': 'sm:*:flex-1',
|
|
128
|
+
'grow-last': 'sm:*:last:grow',
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
export interface BottomSheetFooterProps {
|
|
134
|
+
layout?: FooterLayout | { narrow: FooterLayout, wide: FooterLayout }
|
|
135
|
+
className?: string
|
|
136
|
+
children?: ReactNode
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function BottomSheetFooter({ layout = 'end', className, children }: BottomSheetFooterProps) {
|
|
140
|
+
const { narrow, wide } = typeof layout === 'string' ? { narrow: layout, wide: layout } : layout
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div className={bottomSheetFooter({ narrow, wide, className })}>
|
|
144
|
+
{children}
|
|
145
|
+
</div>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
@keyframes col-open { from { height: 0; } to { height: var(--collapsible-panel-height); } }
|
|
2
|
+
@keyframes col-close { from { height: var(--collapsible-panel-height); } to { height: 0; } }
|
|
3
|
+
|
|
4
|
+
.panel { overflow: hidden; }
|
|
5
|
+
.panel[data-open] { animation: col-open var(--dur-enter) var(--ease-enter); }
|
|
6
|
+
.panel[data-closed] { animation: col-close var(--dur-exit) var(--ease-exit) forwards; }
|
|
7
|
+
|
|
8
|
+
@media (prefers-reduced-motion: reduce) {
|
|
9
|
+
.panel[data-open],
|
|
10
|
+
.panel[data-closed] { animation: none; }
|
|
11
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Collapsible as CollapsiblePrimitive } from '@base-ui/react/collapsible'
|
|
4
|
+
import type { ComponentProps } from 'react'
|
|
5
|
+
|
|
6
|
+
import s from './index.module.css'
|
|
7
|
+
|
|
8
|
+
export type CollapsibleProps = ComponentProps<typeof CollapsiblePrimitive.Root>
|
|
9
|
+
|
|
10
|
+
export function Collapsible(props: CollapsibleProps) {
|
|
11
|
+
return <CollapsiblePrimitive.Root {...props} />
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type CollapsibleTriggerProps = ComponentProps<typeof CollapsiblePrimitive.Trigger>
|
|
15
|
+
|
|
16
|
+
export function CollapsibleTrigger(props: CollapsibleTriggerProps) {
|
|
17
|
+
return <CollapsiblePrimitive.Trigger {...props} />
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type CollapsibleContentProps = Omit<ComponentProps<typeof CollapsiblePrimitive.Panel>, 'className'> & {
|
|
21
|
+
className?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function CollapsibleContent({ className, children, ...rest }: CollapsibleContentProps) {
|
|
25
|
+
return (
|
|
26
|
+
<CollapsiblePrimitive.Panel {...rest} className={s.panel}>
|
|
27
|
+
<div className={className}>{children}</div>
|
|
28
|
+
</CollapsiblePrimitive.Panel>
|
|
29
|
+
)
|
|
30
|
+
}
|
package/combobox/index.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { Combobox as ComboboxPrimitive, type ComboboxFilter, type ComboboxFilterOptions } from '@base-ui/react/combobox'
|
|
4
|
-
import { isValidElement, type ComponentProps, type ReactNode } from 'react'
|
|
4
|
+
import { createContext, isValidElement, use, type ComponentProps, type ReactNode } from 'react'
|
|
5
5
|
|
|
6
6
|
import { tv } from '../tv'
|
|
7
7
|
|
|
@@ -23,7 +23,7 @@ export function Combobox<Value = string, Item = Value>({
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export const comboboxAnchor = tv({
|
|
26
|
-
base: 'flex items-center gap-tight rounded-r8 px-3'
|
|
26
|
+
base: 'group/combobox flex items-center gap-tight rounded-r8 px-3'
|
|
27
27
|
+ ' transition-state'
|
|
28
28
|
+ ' invalid-within:ring-2 invalid-within:ring-line-danger'
|
|
29
29
|
+ ' has-disabled:bg-bg-neutral-disabled has-disabled:cursor-not-allowed'
|
|
@@ -34,6 +34,8 @@ export const comboboxAnchor = tv({
|
|
|
34
34
|
surface: {
|
|
35
35
|
bare: 'ring-1 ring-inset ring-line-strong',
|
|
36
36
|
well: 'bg-bg-well',
|
|
37
|
+
ghost: 'bg-transparent not-has-disabled:not-data-disabled:not-data-readonly:not-focus-within:hover:bg-bg-neutral-hover'
|
|
38
|
+
+ ' focus-within:bg-bg-neutral-selected data-readonly:focus-within:bg-transparent has-disabled:bg-transparent',
|
|
37
39
|
},
|
|
38
40
|
size: { sm: 'h-8', md: 'h-9', lg: 'h-10 [&_input]:text-t16' },
|
|
39
41
|
},
|
|
@@ -42,15 +44,34 @@ export const comboboxAnchor = tv({
|
|
|
42
44
|
})
|
|
43
45
|
|
|
44
46
|
export type ComboboxAnchorProps = Omit<ComponentProps<typeof ComboboxPrimitive.InputGroup>, 'className'> & {
|
|
45
|
-
surface?: 'bare' | 'well'
|
|
47
|
+
surface?: 'bare' | 'well' | 'ghost'
|
|
46
48
|
size?: 'sm' | 'md' | 'lg'
|
|
47
49
|
className?: string
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
const AnchorSurface = createContext<'bare' | 'well' | 'ghost'>('bare')
|
|
53
|
+
|
|
54
|
+
export function ComboboxAnchor({ surface = 'bare', size, className, ...rest }: ComboboxAnchorProps) {
|
|
55
|
+
return (
|
|
56
|
+
<AnchorSurface value={surface}>
|
|
57
|
+
<ComboboxPrimitive.InputGroup {...rest} className={comboboxAnchor({ surface, size, className })} />
|
|
58
|
+
</AnchorSurface>
|
|
59
|
+
)
|
|
52
60
|
}
|
|
53
61
|
|
|
62
|
+
const anchorTool = tv({
|
|
63
|
+
base: '',
|
|
64
|
+
variants: {
|
|
65
|
+
surface: {
|
|
66
|
+
bare: '',
|
|
67
|
+
well: '',
|
|
68
|
+
ghost: 'opacity-40 group-data-popup-open/combobox:opacity-100'
|
|
69
|
+
+ ' group-not-has-disabled/combobox:group-not-data-readonly/combobox:group-hover/combobox:opacity-100'
|
|
70
|
+
+ ' group-not-has-disabled/combobox:group-not-data-readonly/combobox:group-focus-within/combobox:opacity-100',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
|
|
54
75
|
export type ComboboxInputProps = Omit<ComponentProps<typeof ComboboxPrimitive.Input>, 'className'> & {
|
|
55
76
|
className?: string
|
|
56
77
|
}
|
|
@@ -79,7 +100,7 @@ export type ComboboxTriggerProps = Omit<ComponentProps<typeof ComboboxPrimitive.
|
|
|
79
100
|
|
|
80
101
|
export function ComboboxTrigger({ label = '목록 열기', className, children, ...rest }: ComboboxTriggerProps) {
|
|
81
102
|
return (
|
|
82
|
-
<ComboboxPrimitive.Trigger aria-label={label} {...rest} className={comboboxTrigger({ className })}>
|
|
103
|
+
<ComboboxPrimitive.Trigger aria-label={label} {...rest} className={comboboxTrigger({ className: [anchorTool({ surface: use(AnchorSurface) }), className] })}>
|
|
83
104
|
{children ?? (
|
|
84
105
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-4 transition-transform in-data-popup-open:rotate-180 motion-reduce:transition-none">
|
|
85
106
|
<path d="m6 9 6 6 6-6" />
|
|
@@ -103,7 +124,7 @@ export type ComboboxCancelProps = Omit<ComponentProps<typeof ComboboxPrimitive.C
|
|
|
103
124
|
|
|
104
125
|
export function ComboboxCancel({ label = '지우기', keepMounted = true, className, children, ...rest }: ComboboxCancelProps) {
|
|
105
126
|
return (
|
|
106
|
-
<ComboboxPrimitive.Clear aria-label={label} keepMounted={keepMounted} {...rest} className={comboboxCancel({ className })}>
|
|
127
|
+
<ComboboxPrimitive.Clear aria-label={label} keepMounted={keepMounted} {...rest} className={comboboxCancel({ className: [anchorTool({ surface: use(AnchorSurface) }), className] })}>
|
|
107
128
|
{children ?? (
|
|
108
129
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-4">
|
|
109
130
|
<path d="M18 6 6 18" />
|
package/dialog/index.tsx
CHANGED
|
@@ -6,16 +6,17 @@ import type { ComponentProps, ReactNode } from 'react'
|
|
|
6
6
|
import { tv } from '../tv'
|
|
7
7
|
|
|
8
8
|
export const dialog = tv({
|
|
9
|
-
base: '
|
|
10
|
-
+ ' open:animate-fade-in closed:animate-fade-out focus-visible:outline-none'
|
|
9
|
+
base: 'float text-fg z-overlay fixed flex flex-col gap-stack overflow-y-auto'
|
|
10
|
+
+ ' open:animate-fade-in closed:animate-fade-out focus-visible:outline-none'
|
|
11
|
+
+ ' transition-state data-nested-dialog-open:scale-95 motion-reduce:data-nested-dialog-open:scale-100',
|
|
11
12
|
|
|
12
13
|
variants: {
|
|
13
14
|
size: {
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
sm: 'inset-x-4 top-1/2 mx-auto max-h-11/12 max-w-112 -translate-y-1/2 rounded-r20 p-6',
|
|
16
|
+
md: 'inset-x-4 top-1/2 mx-auto max-h-11/12 max-w-120 -translate-y-1/2 rounded-r20 p-6',
|
|
17
|
+
lg: 'inset-x-4 top-1/2 mx-auto max-h-11/12 max-w-160 -translate-y-1/2 rounded-r20 p-6',
|
|
16
18
|
full: 'inset-0 p-6',
|
|
17
|
-
top: '
|
|
18
|
-
+ ' rounded-r20 gap-0 overflow-hidden',
|
|
19
|
+
top: 'inset-x-4 top-24 mx-auto max-w-144 rounded-r20 gap-0 overflow-hidden',
|
|
19
20
|
},
|
|
20
21
|
},
|
|
21
22
|
|
|
@@ -41,14 +42,14 @@ export function DialogClose(props: DialogCloseProps) {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export type DialogContentProps = Omit<ComponentProps<typeof DialogPrimitive.Popup>, 'className'> & {
|
|
44
|
-
size?: 'md' | 'full' | 'top'
|
|
45
|
+
size?: 'sm' | 'md' | 'lg' | 'full' | 'top'
|
|
45
46
|
className?: string
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
export function DialogContent({ size, className, children, ...rest }: DialogContentProps) {
|
|
49
50
|
return (
|
|
50
51
|
<DialogPrimitive.Portal>
|
|
51
|
-
<DialogPrimitive.Backdrop className="bg-bg-overlay z-overlay fixed inset-0 open:animate-fade-in closed:animate-fade-out" />
|
|
52
|
+
<DialogPrimitive.Backdrop forceRender className="bg-bg-overlay z-overlay fixed inset-0 open:animate-fade-in closed:animate-fade-out" />
|
|
52
53
|
|
|
53
54
|
<DialogPrimitive.Popup {...rest} className={dialog({ size, className })}>
|
|
54
55
|
{children}
|
|
@@ -107,11 +108,47 @@ export function DialogHeader({ lead, trail, className, children }: DialogHeaderP
|
|
|
107
108
|
)
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
const
|
|
111
|
+
const dialogBody = tv({ base: '-m-1 min-h-0 overflow-y-auto p-1' })
|
|
111
112
|
|
|
112
|
-
export function
|
|
113
|
+
export function DialogBody({ className, children }: { className?: string, children?: ReactNode }) {
|
|
113
114
|
return (
|
|
114
|
-
<div className={
|
|
115
|
+
<div className={dialogBody({ className })}>
|
|
116
|
+
{children}
|
|
117
|
+
</div>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
type FooterLayout = 'end' | 'between' | 'fill' | 'grow-last'
|
|
122
|
+
|
|
123
|
+
const dialogFooter = tv({
|
|
124
|
+
base: 'mt-auto flex justify-end gap-2',
|
|
125
|
+
variants: {
|
|
126
|
+
narrow: {
|
|
127
|
+
'end': '',
|
|
128
|
+
'between': 'max-sm:*:first:mr-auto',
|
|
129
|
+
'fill': 'max-sm:*:flex-1',
|
|
130
|
+
'grow-last': 'max-sm:*:last:grow',
|
|
131
|
+
},
|
|
132
|
+
wide: {
|
|
133
|
+
'end': '',
|
|
134
|
+
'between': 'sm:*:first:mr-auto',
|
|
135
|
+
'fill': 'sm:*:flex-1',
|
|
136
|
+
'grow-last': 'sm:*:last:grow',
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
export interface DialogFooterProps {
|
|
142
|
+
layout?: FooterLayout | { narrow: FooterLayout, wide: FooterLayout }
|
|
143
|
+
className?: string
|
|
144
|
+
children?: ReactNode
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function DialogFooter({ layout = 'end', className, children }: DialogFooterProps) {
|
|
148
|
+
const { narrow, wide } = typeof layout === 'string' ? { narrow: layout, wide: layout } : layout
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<div className={dialogFooter({ narrow, wide, className })}>
|
|
115
152
|
{children}
|
|
116
153
|
</div>
|
|
117
154
|
)
|
package/drawer/index.module.css
CHANGED
|
@@ -4,14 +4,17 @@
|
|
|
4
4
|
.panel {
|
|
5
5
|
--drawer-slide: -100%;
|
|
6
6
|
transform: translateX(var(--drawer-swipe-movement-x, 0px));
|
|
7
|
-
|
|
7
|
+
transform-origin: left center;
|
|
8
|
+
transition: transform var(--dur-enter) var(--ease-enter), scale var(--dur-enter) var(--ease-enter);
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
.panel[data-open] { animation: slide-in var(--dur-enter) var(--ease-enter); }
|
|
11
12
|
.panel[data-closed] { animation: slide-out var(--dur-exit) var(--ease-exit) forwards; }
|
|
12
13
|
.panel[data-swiping] { transition-duration: 0ms; }
|
|
13
|
-
.panel-
|
|
14
|
+
.panel[data-nested-drawer-open] { scale: 0.95; }
|
|
15
|
+
.panel-right { --drawer-slide: 100%; transform-origin: right center; }
|
|
14
16
|
|
|
15
17
|
@media (prefers-reduced-motion: reduce) {
|
|
16
18
|
.panel { --drawer-slide: 0%; }
|
|
19
|
+
.panel[data-nested-drawer-open] { scale: 1; }
|
|
17
20
|
}
|
package/drawer/index.tsx
CHANGED
|
@@ -36,7 +36,7 @@ export function DrawerClose(props: DrawerCloseProps) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export const drawerContent = tv({
|
|
39
|
-
base: '
|
|
39
|
+
base: 'float text-fg z-overlay fixed inset-y-0'
|
|
40
40
|
+ ' flex w-full flex-col gap-stack overflow-y-auto p-4 sm:w-120'
|
|
41
41
|
+ ' focus-visible:outline-none',
|
|
42
42
|
variants: {
|
|
@@ -53,7 +53,7 @@ export function DrawerContent({ className, style, ...rest }: DrawerContentProps)
|
|
|
53
53
|
|
|
54
54
|
return (
|
|
55
55
|
<DrawerPrimitive.Portal>
|
|
56
|
-
<DrawerPrimitive.Backdrop className="bg-bg-overlay z-overlay fixed inset-0 open:animate-fade-in closed:animate-fade-out" />
|
|
56
|
+
<DrawerPrimitive.Backdrop forceRender className="bg-bg-overlay z-overlay fixed inset-0 open:animate-fade-in closed:animate-fade-out" />
|
|
57
57
|
|
|
58
58
|
<DrawerPrimitive.Viewport>
|
|
59
59
|
<DrawerPrimitive.Popup
|
|
@@ -122,3 +122,49 @@ export function DrawerDescription({ className, ...rest }: DrawerDescriptionProps
|
|
|
122
122
|
<DrawerPrimitive.Description {...rest} className={drawerDescription({ className })} />
|
|
123
123
|
)
|
|
124
124
|
}
|
|
125
|
+
|
|
126
|
+
const drawerBody = tv({ base: '-m-1 min-h-0 overflow-y-auto p-1' })
|
|
127
|
+
|
|
128
|
+
export function DrawerBody({ className, children }: { className?: string, children?: ReactNode }) {
|
|
129
|
+
return (
|
|
130
|
+
<div className={drawerBody({ className })}>
|
|
131
|
+
{children}
|
|
132
|
+
</div>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
type FooterLayout = 'end' | 'between' | 'fill' | 'grow-last'
|
|
137
|
+
|
|
138
|
+
const drawerFooter = tv({
|
|
139
|
+
base: 'mt-auto flex justify-end gap-2',
|
|
140
|
+
variants: {
|
|
141
|
+
narrow: {
|
|
142
|
+
'end': '',
|
|
143
|
+
'between': 'max-sm:*:first:mr-auto',
|
|
144
|
+
'fill': 'max-sm:*:flex-1',
|
|
145
|
+
'grow-last': 'max-sm:*:last:grow',
|
|
146
|
+
},
|
|
147
|
+
wide: {
|
|
148
|
+
'end': '',
|
|
149
|
+
'between': 'sm:*:first:mr-auto',
|
|
150
|
+
'fill': 'sm:*:flex-1',
|
|
151
|
+
'grow-last': 'sm:*:last:grow',
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
export interface DrawerFooterProps {
|
|
157
|
+
layout?: FooterLayout | { narrow: FooterLayout, wide: FooterLayout }
|
|
158
|
+
className?: string
|
|
159
|
+
children?: ReactNode
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function DrawerFooter({ layout = 'end', className, children }: DrawerFooterProps) {
|
|
163
|
+
const { narrow, wide } = typeof layout === 'string' ? { narrow: layout, wide: layout } : layout
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<div className={drawerFooter({ narrow, wide, className })}>
|
|
167
|
+
{children}
|
|
168
|
+
</div>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Fieldset as FieldsetPrimitive } from '@base-ui/react/fieldset'
|
|
2
|
+
import type { ComponentProps } from 'react'
|
|
3
|
+
|
|
4
|
+
import { tv } from '../tv'
|
|
5
|
+
|
|
6
|
+
const fieldset = tv({ base: 'flex min-w-0 flex-col gap-stack' })
|
|
7
|
+
|
|
8
|
+
export type FieldsetProps = Omit<ComponentProps<typeof FieldsetPrimitive.Root>, 'className'> & {
|
|
9
|
+
className?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Fieldset({ className, ...rest }: FieldsetProps) {
|
|
13
|
+
return <FieldsetPrimitive.Root {...rest} className={fieldset({ className })} />
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const fieldsetLegend = tv({ base: 'text-label data-disabled:text-fg-disabled' })
|
|
17
|
+
|
|
18
|
+
export type FieldsetLegendProps = Omit<ComponentProps<typeof FieldsetPrimitive.Legend>, 'className'> & {
|
|
19
|
+
className?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function FieldsetLegend({ className, ...rest }: FieldsetLegendProps) {
|
|
23
|
+
return <FieldsetPrimitive.Legend {...rest} className={fieldsetLegend({ className })} />
|
|
24
|
+
}
|
package/input/index.tsx
CHANGED
|
@@ -18,6 +18,8 @@ export const input = tv({
|
|
|
18
18
|
surface: {
|
|
19
19
|
bare: 'bg-transparent ring-1 ring-inset ring-line-strong read-only:bg-bg-well',
|
|
20
20
|
well: 'bg-bg-well',
|
|
21
|
+
ghost: 'bg-transparent enabled:not-read-only:not-focus:hover:bg-bg-neutral-hover'
|
|
22
|
+
+ ' focus:bg-bg-neutral-selected read-only:focus:bg-transparent disabled:bg-transparent',
|
|
21
23
|
},
|
|
22
24
|
size: {
|
|
23
25
|
sm: 'h-8 px-3',
|
|
@@ -30,7 +32,7 @@ export const input = tv({
|
|
|
30
32
|
})
|
|
31
33
|
|
|
32
34
|
export type InputProps = Omit<ComponentProps<typeof InputPrimitive>, 'className' | 'size'> & {
|
|
33
|
-
surface?: 'bare' | 'well'
|
|
35
|
+
surface?: 'bare' | 'well' | 'ghost'
|
|
34
36
|
size?: 'sm' | 'md' | 'lg'
|
|
35
37
|
className?: string
|
|
36
38
|
}
|
package/modal/index.tsx
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { Dialog as DialogPrimitive } from '@base-ui/react/dialog'
|
|
4
|
+
import type { Drawer as DrawerPrimitive } from '@base-ui/react/drawer'
|
|
5
|
+
import { createContext, use, useCallback, useState, useSyncExternalStore, type ReactElement, type ReactNode } from 'react'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
BottomSheet,
|
|
9
|
+
BottomSheetClose,
|
|
10
|
+
BottomSheetContent,
|
|
11
|
+
BottomSheetDescription,
|
|
12
|
+
BottomSheetTitle,
|
|
13
|
+
BottomSheetTrigger,
|
|
14
|
+
} from '../bottom-sheet'
|
|
15
|
+
import {
|
|
16
|
+
Dialog,
|
|
17
|
+
DialogBody,
|
|
18
|
+
DialogClose,
|
|
19
|
+
DialogContent,
|
|
20
|
+
DialogDescription,
|
|
21
|
+
DialogFooter,
|
|
22
|
+
DialogHeader,
|
|
23
|
+
DialogTitle,
|
|
24
|
+
DialogTrigger,
|
|
25
|
+
type DialogContentProps,
|
|
26
|
+
type DialogFooterProps,
|
|
27
|
+
type DialogHeaderProps,
|
|
28
|
+
type DialogTriggerProps,
|
|
29
|
+
} from '../dialog'
|
|
30
|
+
|
|
31
|
+
type Shape = 'dialog' | 'sheet'
|
|
32
|
+
|
|
33
|
+
export type ModalBreakpoint = 'xs' | 'sm' | 'md'
|
|
34
|
+
|
|
35
|
+
export const modalQuery: Record<ModalBreakpoint, string> = {
|
|
36
|
+
xs: '(min-width: 30rem)',
|
|
37
|
+
sm: '(min-width: 40rem)',
|
|
38
|
+
md: '(min-width: 48rem)',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ModalShape = createContext<Shape>('sheet')
|
|
42
|
+
|
|
43
|
+
const media = (query: string) => (typeof window.matchMedia === 'function' ? window.matchMedia(query) : null)
|
|
44
|
+
|
|
45
|
+
function useWide(query: string) {
|
|
46
|
+
const subscribe = useCallback((notify: () => void) => {
|
|
47
|
+
const list = media(query)
|
|
48
|
+
list?.addEventListener('change', notify)
|
|
49
|
+
return () => list?.removeEventListener('change', notify)
|
|
50
|
+
}, [query])
|
|
51
|
+
|
|
52
|
+
return useSyncExternalStore(subscribe, () => media(query)?.matches ?? false, () => false)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ModalProps {
|
|
56
|
+
breakpoint?: ModalBreakpoint
|
|
57
|
+
as?: 'auto' | Shape
|
|
58
|
+
open?: boolean
|
|
59
|
+
defaultOpen?: boolean
|
|
60
|
+
onOpenChange?: (open: boolean, eventDetails: DialogPrimitive.Root.ChangeEventDetails | DrawerPrimitive.Root.ChangeEventDetails) => void
|
|
61
|
+
children?: ReactNode
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function Modal({ breakpoint = 'xs', as = 'auto', open, defaultOpen = false, onOpenChange, children }: ModalProps) {
|
|
65
|
+
const wide = useWide(modalQuery[breakpoint])
|
|
66
|
+
const [inner, setInner] = useState(defaultOpen)
|
|
67
|
+
const shape: Shape = as === 'auto' ? (wide ? 'dialog' : 'sheet') : as
|
|
68
|
+
const current = open ?? inner
|
|
69
|
+
|
|
70
|
+
const change = (next: boolean, eventDetails: DialogPrimitive.Root.ChangeEventDetails | DrawerPrimitive.Root.ChangeEventDetails) => {
|
|
71
|
+
onOpenChange?.(next, eventDetails)
|
|
72
|
+
if (open === undefined && !eventDetails.isCanceled) setInner(next)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<ModalShape value={shape}>
|
|
77
|
+
{shape === 'dialog'
|
|
78
|
+
? <Dialog open={current} onOpenChange={change}>{children}</Dialog>
|
|
79
|
+
: <BottomSheet open={current} onOpenChange={change}>{children}</BottomSheet>}
|
|
80
|
+
</ModalShape>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type ModalTriggerProps = Omit<DialogTriggerProps, 'className' | 'render' | 'style' | 'handle' | 'payload'> & {
|
|
85
|
+
render?: ReactElement
|
|
86
|
+
className?: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function ModalTrigger(props: ModalTriggerProps) {
|
|
90
|
+
return use(ModalShape) === 'dialog' ? <DialogTrigger {...props} /> : <BottomSheetTrigger {...props} />
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type ModalCloseProps = ModalTriggerProps
|
|
94
|
+
|
|
95
|
+
export function ModalClose(props: ModalCloseProps) {
|
|
96
|
+
return use(ModalShape) === 'dialog' ? <DialogClose {...props} /> : <BottomSheetClose {...props} />
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type ModalContentProps = Pick<DialogContentProps, 'initialFocus' | 'finalFocus'> & {
|
|
100
|
+
size?: 'sm' | 'md' | 'lg'
|
|
101
|
+
className?: string
|
|
102
|
+
children?: ReactNode
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function ModalContent({ size, ...rest }: ModalContentProps) {
|
|
106
|
+
return use(ModalShape) === 'dialog' ? <DialogContent {...rest} size={size} /> : <BottomSheetContent {...rest} />
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type ModalHeaderProps = DialogHeaderProps
|
|
110
|
+
|
|
111
|
+
export function ModalHeader(props: ModalHeaderProps) {
|
|
112
|
+
return <DialogHeader {...props} />
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface ModalTitleProps {
|
|
116
|
+
className?: string
|
|
117
|
+
children?: ReactNode
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function ModalTitle(props: ModalTitleProps) {
|
|
121
|
+
return use(ModalShape) === 'dialog' ? <DialogTitle {...props} /> : <BottomSheetTitle {...props} />
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export type ModalDescriptionProps = ModalTitleProps
|
|
125
|
+
|
|
126
|
+
export function ModalDescription(props: ModalDescriptionProps) {
|
|
127
|
+
return use(ModalShape) === 'dialog' ? <DialogDescription {...props} /> : <BottomSheetDescription {...props} />
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function ModalBody(props: { className?: string, children?: ReactNode }) {
|
|
131
|
+
return <DialogBody {...props} />
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export type ModalFooterProps = DialogFooterProps
|
|
135
|
+
|
|
136
|
+
export function ModalFooter({ layout, ...rest }: ModalFooterProps) {
|
|
137
|
+
const shape = use(ModalShape)
|
|
138
|
+
const resolved = typeof layout === 'object' ? layout[shape === 'dialog' ? 'wide' : 'narrow'] : layout
|
|
139
|
+
|
|
140
|
+
return <DialogFooter {...rest} layout={resolved ?? (shape === 'dialog' ? 'end' : 'fill')} />
|
|
141
|
+
}
|
package/number-field/index.tsx
CHANGED
|
@@ -7,7 +7,7 @@ import { tv } from '../tv'
|
|
|
7
7
|
|
|
8
8
|
export const numberField = tv({
|
|
9
9
|
slots: {
|
|
10
|
-
root: 'flex items-center rounded-r8'
|
|
10
|
+
root: 'group/number flex items-center rounded-r8'
|
|
11
11
|
+ ' transition-state'
|
|
12
12
|
+ ' invalid-within:ring-2 invalid-within:ring-line-danger'
|
|
13
13
|
+ ' has-disabled:bg-bg-neutral-disabled has-disabled:cursor-not-allowed'
|
|
@@ -25,6 +25,11 @@ export const numberField = tv({
|
|
|
25
25
|
surface: {
|
|
26
26
|
bare: { root: 'bg-transparent ring-1 ring-inset ring-line-strong' },
|
|
27
27
|
well: { root: 'bg-bg-well' },
|
|
28
|
+
ghost: {
|
|
29
|
+
root: 'bg-transparent focus-within:bg-bg-neutral-selected has-disabled:bg-transparent',
|
|
30
|
+
step: 'opacity-40 group-not-has-disabled/number:group-hover/number:opacity-100'
|
|
31
|
+
+ ' group-not-has-disabled/number:group-focus-within/number:opacity-100',
|
|
32
|
+
},
|
|
28
33
|
},
|
|
29
34
|
size: {
|
|
30
35
|
sm: { root: 'h-8', step: 'size-8' },
|
|
@@ -32,10 +37,16 @@ export const numberField = tv({
|
|
|
32
37
|
lg: { root: 'h-10', step: 'size-10', input: 'text-t16' },
|
|
33
38
|
},
|
|
34
39
|
readonly: {
|
|
35
|
-
true: { root: 'bg-bg-well', input: 'text-fg-muted' },
|
|
40
|
+
true: { root: 'bg-bg-well has-disabled:bg-bg-well has-disabled:cursor-default', step: 'disabled:cursor-default', input: 'text-fg-muted' },
|
|
41
|
+
false: {},
|
|
36
42
|
},
|
|
37
43
|
},
|
|
38
44
|
|
|
45
|
+
compoundVariants: [
|
|
46
|
+
{ surface: 'ghost', readonly: false, class: { root: 'not-has-disabled:not-focus-within:hover:bg-bg-neutral-hover' } },
|
|
47
|
+
{ surface: 'ghost', readonly: true, class: { root: 'bg-transparent has-disabled:bg-transparent focus-within:bg-transparent' } },
|
|
48
|
+
],
|
|
49
|
+
|
|
39
50
|
defaultVariants: { surface: 'bare', size: 'md' },
|
|
40
51
|
})
|
|
41
52
|
|
|
@@ -68,7 +79,7 @@ export type NumberFieldProps = Omit<NumberFieldPrimitive.Root.Props, 'className'
|
|
|
68
79
|
& {
|
|
69
80
|
decrementLabel: string
|
|
70
81
|
incrementLabel: string
|
|
71
|
-
surface?: 'bare' | 'well'
|
|
82
|
+
surface?: 'bare' | 'well' | 'ghost'
|
|
72
83
|
size?: 'sm' | 'md' | 'lg'
|
|
73
84
|
className?: string
|
|
74
85
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Progress as ProgressPrimitive } from '@base-ui/react/progress'
|
|
4
|
+
import type { ComponentProps } from 'react'
|
|
5
|
+
|
|
6
|
+
import { tv } from '../tv'
|
|
7
|
+
|
|
8
|
+
export const progress = tv({
|
|
9
|
+
slots: {
|
|
10
|
+
root: 'flex flex-wrap items-center justify-between gap-x-2 gap-y-tight',
|
|
11
|
+
track: 'bg-bg-neutral-selected h-1.5 w-full overflow-hidden rounded-full',
|
|
12
|
+
indicator: 'h-full rounded-full transition-all motion-reduce:transition-none',
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
variants: {
|
|
16
|
+
color: {
|
|
17
|
+
neutral: { indicator: 'bg-bg-neutral-solid' },
|
|
18
|
+
brand: { indicator: 'bg-bg-brand-solid' },
|
|
19
|
+
info: { indicator: 'bg-bg-info-solid' },
|
|
20
|
+
success: { indicator: 'bg-bg-success-solid' },
|
|
21
|
+
warning: { indicator: 'bg-bg-warning-solid' },
|
|
22
|
+
danger: { indicator: 'bg-bg-danger-solid' },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
defaultVariants: { color: 'brand' },
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
export type ProgressProps = Omit<ComponentProps<typeof ProgressPrimitive.Root>, 'className' | 'value'> & {
|
|
30
|
+
value: number
|
|
31
|
+
color?: 'neutral' | 'brand' | 'info' | 'success' | 'warning' | 'danger'
|
|
32
|
+
className?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function Progress({ color, className, children, ...rest }: ProgressProps) {
|
|
36
|
+
const ui = progress({ color })
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<ProgressPrimitive.Root {...rest} className={ui.root({ className })}>
|
|
40
|
+
{children}
|
|
41
|
+
<ProgressPrimitive.Track className={ui.track()}>
|
|
42
|
+
<ProgressPrimitive.Indicator className={ui.indicator()} />
|
|
43
|
+
</ProgressPrimitive.Track>
|
|
44
|
+
</ProgressPrimitive.Root>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const progressLabel = tv({ base: 'text-label' })
|
|
49
|
+
|
|
50
|
+
export type ProgressLabelProps = Omit<ComponentProps<typeof ProgressPrimitive.Label>, 'className'> & {
|
|
51
|
+
className?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function ProgressLabel({ className, ...rest }: ProgressLabelProps) {
|
|
55
|
+
return <ProgressPrimitive.Label {...rest} className={progressLabel({ className })} />
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const progressValue = tv({ base: 'text-caption text-fg-muted tabular-nums' })
|
|
59
|
+
|
|
60
|
+
export type ProgressValueProps = Omit<ComponentProps<typeof ProgressPrimitive.Value>, 'className'> & {
|
|
61
|
+
className?: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function ProgressValue({ className, ...rest }: ProgressValueProps) {
|
|
65
|
+
return <ProgressPrimitive.Value {...rest} className={progressValue({ className })} />
|
|
66
|
+
}
|
package/registry.json
CHANGED
|
@@ -194,6 +194,27 @@
|
|
|
194
194
|
"layer": "ui"
|
|
195
195
|
}
|
|
196
196
|
},
|
|
197
|
+
{
|
|
198
|
+
"name": "collapsible",
|
|
199
|
+
"type": "registry:ui",
|
|
200
|
+
"title": "Collapsible",
|
|
201
|
+
"files": [
|
|
202
|
+
{
|
|
203
|
+
"path": "collapsible/index.tsx",
|
|
204
|
+
"type": "registry:ui"
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"path": "collapsible/index.module.css",
|
|
208
|
+
"type": "registry:ui"
|
|
209
|
+
}
|
|
210
|
+
],
|
|
211
|
+
"dependencies": [
|
|
212
|
+
"@base-ui/react"
|
|
213
|
+
],
|
|
214
|
+
"meta": {
|
|
215
|
+
"layer": "ui"
|
|
216
|
+
}
|
|
217
|
+
},
|
|
197
218
|
{
|
|
198
219
|
"name": "combobox",
|
|
199
220
|
"type": "registry:ui",
|
|
@@ -353,6 +374,26 @@
|
|
|
353
374
|
"layer": "ui"
|
|
354
375
|
}
|
|
355
376
|
},
|
|
377
|
+
{
|
|
378
|
+
"name": "fieldset",
|
|
379
|
+
"type": "registry:ui",
|
|
380
|
+
"title": "Fieldset",
|
|
381
|
+
"files": [
|
|
382
|
+
{
|
|
383
|
+
"path": "fieldset/index.tsx",
|
|
384
|
+
"type": "registry:ui"
|
|
385
|
+
}
|
|
386
|
+
],
|
|
387
|
+
"dependencies": [
|
|
388
|
+
"@base-ui/react"
|
|
389
|
+
],
|
|
390
|
+
"registryDependencies": [
|
|
391
|
+
"tv"
|
|
392
|
+
],
|
|
393
|
+
"meta": {
|
|
394
|
+
"layer": "ui"
|
|
395
|
+
}
|
|
396
|
+
},
|
|
356
397
|
{
|
|
357
398
|
"name": "input",
|
|
358
399
|
"type": "registry:ui",
|
|
@@ -407,6 +448,27 @@
|
|
|
407
448
|
"layer": "ui"
|
|
408
449
|
}
|
|
409
450
|
},
|
|
451
|
+
{
|
|
452
|
+
"name": "modal",
|
|
453
|
+
"type": "registry:ui",
|
|
454
|
+
"title": "Modal",
|
|
455
|
+
"files": [
|
|
456
|
+
{
|
|
457
|
+
"path": "modal/index.tsx",
|
|
458
|
+
"type": "registry:ui"
|
|
459
|
+
}
|
|
460
|
+
],
|
|
461
|
+
"dependencies": [
|
|
462
|
+
"@base-ui/react"
|
|
463
|
+
],
|
|
464
|
+
"registryDependencies": [
|
|
465
|
+
"dialog",
|
|
466
|
+
"bottom-sheet"
|
|
467
|
+
],
|
|
468
|
+
"meta": {
|
|
469
|
+
"layer": "derived"
|
|
470
|
+
}
|
|
471
|
+
},
|
|
410
472
|
{
|
|
411
473
|
"name": "number-field",
|
|
412
474
|
"type": "registry:ui",
|
|
@@ -448,6 +510,26 @@
|
|
|
448
510
|
"radius": "rounded-r16"
|
|
449
511
|
}
|
|
450
512
|
},
|
|
513
|
+
{
|
|
514
|
+
"name": "progress",
|
|
515
|
+
"type": "registry:ui",
|
|
516
|
+
"title": "Progress",
|
|
517
|
+
"files": [
|
|
518
|
+
{
|
|
519
|
+
"path": "progress/index.tsx",
|
|
520
|
+
"type": "registry:ui"
|
|
521
|
+
}
|
|
522
|
+
],
|
|
523
|
+
"dependencies": [
|
|
524
|
+
"@base-ui/react"
|
|
525
|
+
],
|
|
526
|
+
"registryDependencies": [
|
|
527
|
+
"tv"
|
|
528
|
+
],
|
|
529
|
+
"meta": {
|
|
530
|
+
"layer": "ui"
|
|
531
|
+
}
|
|
532
|
+
},
|
|
451
533
|
{
|
|
452
534
|
"name": "radio-group",
|
|
453
535
|
"type": "registry:ui",
|
|
@@ -469,6 +551,29 @@
|
|
|
469
551
|
"layer": "ui"
|
|
470
552
|
}
|
|
471
553
|
},
|
|
554
|
+
{
|
|
555
|
+
"name": "segmented-control",
|
|
556
|
+
"type": "registry:ui",
|
|
557
|
+
"title": "SegmentedControl",
|
|
558
|
+
"files": [
|
|
559
|
+
{
|
|
560
|
+
"path": "segmented-control/index.tsx",
|
|
561
|
+
"type": "registry:ui"
|
|
562
|
+
}
|
|
563
|
+
],
|
|
564
|
+
"dependencies": [
|
|
565
|
+
"@base-ui/react"
|
|
566
|
+
],
|
|
567
|
+
"registryDependencies": [
|
|
568
|
+
"tv"
|
|
569
|
+
],
|
|
570
|
+
"meta": {
|
|
571
|
+
"layer": "ui",
|
|
572
|
+
"sizes": [
|
|
573
|
+
"SegmentedControlItem"
|
|
574
|
+
]
|
|
575
|
+
}
|
|
576
|
+
},
|
|
472
577
|
{
|
|
473
578
|
"name": "select",
|
|
474
579
|
"type": "registry:ui",
|
|
@@ -490,6 +595,26 @@
|
|
|
490
595
|
"radius": "rounded-r12"
|
|
491
596
|
}
|
|
492
597
|
},
|
|
598
|
+
{
|
|
599
|
+
"name": "separator",
|
|
600
|
+
"type": "registry:ui",
|
|
601
|
+
"title": "Separator",
|
|
602
|
+
"files": [
|
|
603
|
+
{
|
|
604
|
+
"path": "separator/index.tsx",
|
|
605
|
+
"type": "registry:ui"
|
|
606
|
+
}
|
|
607
|
+
],
|
|
608
|
+
"dependencies": [
|
|
609
|
+
"@base-ui/react"
|
|
610
|
+
],
|
|
611
|
+
"registryDependencies": [
|
|
612
|
+
"tv"
|
|
613
|
+
],
|
|
614
|
+
"meta": {
|
|
615
|
+
"layer": "ui"
|
|
616
|
+
}
|
|
617
|
+
},
|
|
493
618
|
{
|
|
494
619
|
"name": "switch",
|
|
495
620
|
"type": "registry:ui",
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Radio } from '@base-ui/react/radio'
|
|
4
|
+
import { RadioGroup } from '@base-ui/react/radio-group'
|
|
5
|
+
import { createContext, use, type ComponentProps } from 'react'
|
|
6
|
+
|
|
7
|
+
import { tv } from '../tv'
|
|
8
|
+
|
|
9
|
+
type Size = 'sm' | 'md' | 'lg'
|
|
10
|
+
|
|
11
|
+
const Context = createContext<{ size: Size, fill: boolean }>({ size: 'md', fill: false })
|
|
12
|
+
|
|
13
|
+
export const segmentedControl = tv({
|
|
14
|
+
slots: {
|
|
15
|
+
root: 'bg-bg-neutral-selected rounded-r10 inline-flex shrink-0 gap-0.5 p-1',
|
|
16
|
+
item: 'text-label text-fg-muted rounded-r6 inline-flex h-full items-center justify-center gap-tight whitespace-nowrap'
|
|
17
|
+
+ ' transition-state press-shrink cursor-pointer'
|
|
18
|
+
+ ' not-data-disabled:hover:text-fg'
|
|
19
|
+
+ ' checked:bg-bg-neutral-thumb checked:text-fg checked:shadow-s1'
|
|
20
|
+
+ ' focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-line-focus-ring'
|
|
21
|
+
+ ' data-disabled:text-fg-disabled data-disabled:cursor-not-allowed',
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
variants: {
|
|
25
|
+
size: {
|
|
26
|
+
sm: { root: 'h-8', item: 'text-t13 font-medium px-2.5 [&_svg]:size-4' },
|
|
27
|
+
md: { root: 'h-9', item: 'px-2.5 [&_svg]:size-4' },
|
|
28
|
+
lg: { root: 'h-10', item: 'text-t16 font-medium px-3 [&_svg]:size-5' },
|
|
29
|
+
},
|
|
30
|
+
fill: {
|
|
31
|
+
true: { root: 'flex w-full', item: 'flex-1' },
|
|
32
|
+
false: '',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
defaultVariants: { size: 'md', fill: false },
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
export type SegmentedControlProps<Value> = Omit<ComponentProps<typeof RadioGroup<Value>>, 'className'> & {
|
|
40
|
+
size?: Size
|
|
41
|
+
fill?: boolean
|
|
42
|
+
className?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function SegmentedControl<Value>({ size = 'md', fill = false, className, ...rest }: SegmentedControlProps<Value>) {
|
|
46
|
+
const ui = segmentedControl({ size, fill })
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Context value={{ size, fill }}>
|
|
50
|
+
<RadioGroup {...rest} className={ui.root({ className })} />
|
|
51
|
+
</Context>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type SegmentedControlItemProps<Value> = Omit<Radio.Root.Props<Value>, 'className'> & {
|
|
56
|
+
className?: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function SegmentedControlItem<Value>({ className, ...rest }: SegmentedControlItemProps<Value>) {
|
|
60
|
+
const { size, fill } = use(Context)
|
|
61
|
+
|
|
62
|
+
return <Radio.Root {...rest} className={segmentedControl({ size, fill }).item({ className })} />
|
|
63
|
+
}
|
package/select/index.tsx
CHANGED
|
@@ -6,8 +6,21 @@ import type { ComponentProps, ReactNode } from 'react'
|
|
|
6
6
|
|
|
7
7
|
import { tv } from '../tv'
|
|
8
8
|
|
|
9
|
+
const selectIcon = tv({
|
|
10
|
+
base: 'text-fg-subtle transition-state',
|
|
11
|
+
variants: {
|
|
12
|
+
surface: {
|
|
13
|
+
bare: '',
|
|
14
|
+
well: '',
|
|
15
|
+
ghost: 'opacity-40 group-data-popup-open/select:opacity-100'
|
|
16
|
+
+ ' group-enabled/select:group-not-data-readonly/select:group-hover/select:opacity-100'
|
|
17
|
+
+ ' group-enabled/select:group-not-data-readonly/select:group-focus-visible/select:opacity-100',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
|
|
9
22
|
export const selectTrigger = tv({
|
|
10
|
-
base: 'flex w-full items-center justify-between gap-tight rounded-r8'
|
|
23
|
+
base: 'group/select flex w-full items-center justify-between gap-tight rounded-r8'
|
|
11
24
|
+ ' text-value text-fg'
|
|
12
25
|
+ ' transition-state'
|
|
13
26
|
+ ' invalid:ring-2 invalid:ring-line-danger'
|
|
@@ -19,6 +32,8 @@ export const selectTrigger = tv({
|
|
|
19
32
|
surface: {
|
|
20
33
|
bare: 'bg-transparent ring-1 ring-inset ring-line-strong',
|
|
21
34
|
well: 'bg-bg-well',
|
|
35
|
+
ghost: 'bg-transparent enabled:not-data-readonly:not-data-popup-open:hover:bg-bg-neutral-hover'
|
|
36
|
+
+ ' data-popup-open:bg-bg-neutral-selected disabled:bg-transparent',
|
|
22
37
|
},
|
|
23
38
|
size: {
|
|
24
39
|
sm: 'h-8 px-3',
|
|
@@ -75,7 +90,7 @@ export type SelectProps = Omit<ComponentProps<typeof SelectPrimitive.Trigger>, '
|
|
|
75
90
|
defaultValue?: string | null
|
|
76
91
|
onValueChange?: (value: string | null, eventDetails: SelectPrimitive.Root.ChangeEventDetails) => void
|
|
77
92
|
placeholder?: string
|
|
78
|
-
surface?: 'bare' | 'well'
|
|
93
|
+
surface?: 'bare' | 'well' | 'ghost'
|
|
79
94
|
size?: 'sm' | 'md' | 'lg'
|
|
80
95
|
arrow?: boolean
|
|
81
96
|
className?: string
|
|
@@ -140,7 +155,7 @@ export function Select({
|
|
|
140
155
|
<span className="min-w-0 truncate">
|
|
141
156
|
<SelectPrimitive.Value placeholder={placeholder} />
|
|
142
157
|
</span>
|
|
143
|
-
<SelectPrimitive.Icon className=
|
|
158
|
+
<SelectPrimitive.Icon className={selectIcon({ surface })}>
|
|
144
159
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="size-4 transition-transform in-data-popup-open:rotate-180 motion-reduce:transition-none">
|
|
145
160
|
<path d="m6 9 6 6 6-6" />
|
|
146
161
|
</svg>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Separator as SeparatorPrimitive } from '@base-ui/react/separator'
|
|
2
|
+
import type { ComponentProps } from 'react'
|
|
3
|
+
|
|
4
|
+
import { tv } from '../tv'
|
|
5
|
+
|
|
6
|
+
export const separator = tv({
|
|
7
|
+
base: 'bg-line shrink-0',
|
|
8
|
+
|
|
9
|
+
variants: {
|
|
10
|
+
orientation: {
|
|
11
|
+
horizontal: 'h-px w-full',
|
|
12
|
+
vertical: 'w-px self-stretch',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
defaultVariants: { orientation: 'horizontal' },
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export type SeparatorProps = Omit<ComponentProps<typeof SeparatorPrimitive>, 'className'> & {
|
|
20
|
+
className?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function Separator({ orientation = 'horizontal', className, ...rest }: SeparatorProps) {
|
|
24
|
+
return <SeparatorPrimitive {...rest} orientation={orientation} className={separator({ orientation, className })} />
|
|
25
|
+
}
|
|
@@ -78,8 +78,7 @@ Select와 RadioGroup의 state는 `useState<string | null>`로 둔다.
|
|
|
78
78
|
```
|
|
79
79
|
frame 배경과 선. 페이지 안의 영역
|
|
80
80
|
card 배경과 그림자. 목록의 항목
|
|
81
|
-
float 배경과 선과 그림자.
|
|
82
|
-
sheet 배경만. 화면을 덮는 것
|
|
81
|
+
float 배경과 선과 그림자. 페이지나 overlay 위에 뜨는 것
|
|
83
82
|
well bg-bg-well. 파인 면
|
|
84
83
|
```
|
|
85
84
|
|
|
@@ -147,27 +146,33 @@ className 그 한 곳만 바뀐다
|
|
|
147
146
|
| 컴포넌트 | add | 하위 컴포넌트 | variant |
|
|
148
147
|
|---|---|---|---|
|
|
149
148
|
| Accordion | accordion | Accordion, AccordionItem, AccordionTrigger, AccordionContent | - |
|
|
150
|
-
| AlertDialog | alert-dialog | AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogTitle, AlertDialogDescription, AlertDialogHeader, AlertDialogFooter, AlertDialogAction, AlertDialogCancel |
|
|
149
|
+
| AlertDialog | alert-dialog | AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogTitle, AlertDialogDescription, AlertDialogHeader, AlertDialogBody, AlertDialogFooter, AlertDialogAction, AlertDialogCancel | size |
|
|
151
150
|
| Avatar | avatar | Avatar | color, size |
|
|
152
151
|
| Badge | badge | Badge | color, emphasis, shape |
|
|
153
|
-
| BottomSheet | bottom-sheet | BottomSheet, BottomSheetTrigger, BottomSheetClose, BottomSheetContent, BottomSheetHeader, BottomSheetTitle, BottomSheetDescription | - |
|
|
152
|
+
| BottomSheet | bottom-sheet | BottomSheet, BottomSheetTrigger, BottomSheetClose, BottomSheetContent, BottomSheetHeader, BottomSheetTitle, BottomSheetDescription, BottomSheetBody, BottomSheetFooter | - |
|
|
154
153
|
| Button | button | Button | color, emphasis, size, shape |
|
|
155
154
|
| Callout | callout | Callout | color |
|
|
156
155
|
| Checkbox | checkbox | CheckboxGroup, Checkbox | color |
|
|
156
|
+
| Collapsible | collapsible | Collapsible, CollapsibleTrigger, CollapsibleContent | - |
|
|
157
157
|
| Combobox | combobox | Combobox, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxCancel, ComboboxContent, ComboboxViewport, ComboboxCollection, ComboboxItem, ComboboxItemText, ComboboxItemDescription, ComboboxGroup, ComboboxEmpty, ComboboxSeparator | surface, size |
|
|
158
158
|
| CommandPalette | command-palette | CommandPalette | - |
|
|
159
159
|
| ContextMenu | context-menu | ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuCheckboxItem, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuSub, ContextMenuSubTrigger, ContextMenuSubContent | color |
|
|
160
|
-
| Dialog | dialog | Dialog, DialogTrigger, DialogClose, DialogContent, DialogTitle, DialogDescription, DialogHeader, DialogFooter | size |
|
|
161
|
-
| Drawer | drawer | Drawer, DrawerTrigger, DrawerClose, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription | side |
|
|
160
|
+
| Dialog | dialog | Dialog, DialogTrigger, DialogClose, DialogContent, DialogTitle, DialogDescription, DialogHeader, DialogBody, DialogFooter | size |
|
|
161
|
+
| Drawer | drawer | Drawer, DrawerTrigger, DrawerClose, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription, DrawerBody, DrawerFooter | side |
|
|
162
162
|
| DropdownMenu | dropdown-menu | DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent | color |
|
|
163
163
|
| Field | field | Field, FieldLabel, FieldHelp, FieldError, FieldPending, FieldSuccess | layout |
|
|
164
|
+
| Fieldset | fieldset | Fieldset, FieldsetLegend | - |
|
|
164
165
|
| Input | input | Input | surface, size |
|
|
165
166
|
| Kbd | kbd | Kbd | - |
|
|
166
167
|
| Label | label | Label | - |
|
|
168
|
+
| Modal | modal | Modal, ModalTrigger, ModalClose, ModalContent, ModalHeader, ModalTitle, ModalDescription, ModalBody, ModalFooter | - |
|
|
167
169
|
| NumberField | number-field | NumberField | surface, size, readonly |
|
|
168
170
|
| Popover | popover | Popover, PopoverTrigger, PopoverAnchor, PopoverClose, PopoverContent | - |
|
|
171
|
+
| Progress | progress | Progress, ProgressLabel, ProgressValue | color |
|
|
169
172
|
| RadioGroup | radio-group | RadioGroup, RadioGroupItem, RadioGroupItemLabel, RadioGroupItemDescription | color |
|
|
173
|
+
| SegmentedControl | segmented-control | SegmentedControl, SegmentedControlItem | size, fill |
|
|
170
174
|
| Select | select | Select, SelectGroup, SelectItem, SelectItemText, SelectItemDescription, SelectSeparator | surface, size |
|
|
175
|
+
| Separator | separator | Separator | orientation |
|
|
171
176
|
| Switch | switch | Switch | color |
|
|
172
177
|
| Table | table | Table, TableHeader, TableHeadCell, TableBody, TableRow, TableCell | fixed, mono |
|
|
173
178
|
| Tabs | tabs | Tabs, TabsList, TabsTrigger, TabsContent | - |
|
|
@@ -181,6 +186,7 @@ className 그 한 곳만 바뀐다
|
|
|
181
186
|
```
|
|
182
187
|
Dialog와 AlertDialog 내용을 보여 주면 Dialog, 되돌릴 수 없는 일을 한 번 더 물으면 AlertDialog
|
|
183
188
|
Dialog와 Drawer 화면 가운데면 Dialog, 가장자리에서 열리면 Drawer
|
|
189
|
+
Modal과 Dialog 화면 폭에 따라 BottomSheet로 바뀌어야 하면 Modal, 늘 가운데면 Dialog
|
|
184
190
|
Drawer와 BottomSheet 왼쪽이나 오른쪽에서 열리면 Drawer, 아래에서 올라오면 BottomSheet
|
|
185
191
|
DropdownMenu와 ContextMenu 트리거를 누르면 DropdownMenu, 영역을 오른쪽 클릭하거나 길게 누르면 ContextMenu
|
|
186
192
|
Select와 Combobox 목록에서 고르면 Select, 입력해서 찾으면 Combobox
|
|
@@ -188,11 +194,24 @@ Checkbox와 Switch 여러 개를 고르면 Checkbox, 설정을 바로
|
|
|
188
194
|
Toggle과 Switch 도구 모음에서 상태를 누르면 Toggle, 설정이면 Switch
|
|
189
195
|
Badge와 Button 누를 수 없으면 Badge, 누르면 Button
|
|
190
196
|
Tabs와 Accordion 한 번에 하나만 보이면 Tabs, 여러 개를 펴 두면 Accordion
|
|
197
|
+
SegmentedControl과 Tabs 같은 내용의 보기 방식을 바꾸면 SegmentedControl, 다른 내용으로 바꾸면 Tabs
|
|
198
|
+
SegmentedControl과 Toggle 여럿 중 하나를 고르면 SegmentedControl, 하나를 켜고 끄면 Toggle
|
|
199
|
+
Collapsible과 Accordion 한 영역을 접으면 Collapsible, 여러 영역을 함께 다루면 Accordion
|
|
191
200
|
Tooltip과 Popover 짧은 설명이면 Tooltip, 안에서 조작하면 Popover
|
|
192
201
|
```
|
|
193
202
|
|
|
194
203
|
Avatar의 배경색은 color나 seed 중 하나로 정한다. seed에는 사용자 id처럼 바뀌지 않는 값을 넘기고, 같은 seed는 어느 앱에서나 같은 색이다.
|
|
195
204
|
|
|
205
|
+
Dialog, AlertDialog, Drawer, BottomSheet의 Footer는 layout으로 버튼을 배치한다. end, between, fill, grow-last 중에 고르고, 화면 폭마다 다르면 { narrow, wide }를 넘긴다. 기준은 sm이고, ModalFooter는 Dialog 모양이면 wide, BottomSheet 모양이면 narrow를 쓴다.
|
|
206
|
+
|
|
207
|
+
오버레이 안에서 다른 오버레이를 열 때는 Root를 부모 Content 안에 둔다. 뒤의 창은 어두워진다. Dialog는 안에서 무엇을 열어도 조금 작아지고, Drawer와 BottomSheet는 Drawer나 BottomSheet를 열 때만 작아진다.
|
|
208
|
+
|
|
209
|
+
Modal은 기준 폭(기본 xs, 480px)보다 넓으면 Dialog, 좁으면 BottomSheet로 연다.
|
|
210
|
+
|
|
211
|
+
Input, Textarea, NumberField, Select, ComboboxAnchor의 surface는 폼에서는 bare나 well을 쓰고, 표 셀, 헤더, 문장 안에서 값을 바로 고칠 때는 ghost를 쓴다.
|
|
212
|
+
|
|
213
|
+
Fieldset 안의 컨트롤은 Field로 감싼다. Fieldset의 disabled는 Field를 거쳐 컨트롤에 전달된다.
|
|
214
|
+
|
|
196
215
|
Field는 라벨, 도움말, 오류 메시지를 입력 컨트롤에 묶는다. Input, Textarea, NumberField, Select, Combobox와 함께 쓴다.
|
|
197
216
|
|
|
198
217
|
Select는 고른 항목의 `SelectItemText`나 글자를 트리거에 보인다. `SelectItem`을 다른 컴포넌트로 감싸면 `items`로 라벨을 넘긴다.
|
package/textarea/index.tsx
CHANGED
|
@@ -19,6 +19,9 @@ export const textarea = tv({
|
|
|
19
19
|
surface: {
|
|
20
20
|
bare: 'bg-transparent ring-1 ring-inset ring-line-strong read-only:bg-bg-well',
|
|
21
21
|
well: 'bg-bg-well',
|
|
22
|
+
ghost: 'bg-transparent enabled:not-read-only:not-focus:hover:bg-bg-neutral-hover'
|
|
23
|
+
+ ' focus:bg-bg-neutral-selected read-only:focus:bg-transparent disabled:bg-transparent'
|
|
24
|
+
+ ' resize-none enabled:not-read-only:hover:resize-y enabled:not-read-only:focus:resize-y',
|
|
22
25
|
},
|
|
23
26
|
size: {
|
|
24
27
|
sm: 'p-2',
|
|
@@ -33,7 +36,7 @@ export const textarea = tv({
|
|
|
33
36
|
export type TextareaProps = Omit<ComponentProps<'textarea'>, 'className' | 'value' | 'defaultValue'>
|
|
34
37
|
& Pick<FieldPrimitive.Control.Props, 'value' | 'defaultValue' | 'onValueChange'>
|
|
35
38
|
& {
|
|
36
|
-
surface?: 'bare' | 'well'
|
|
39
|
+
surface?: 'bare' | 'well' | 'ghost'
|
|
37
40
|
size?: 'sm' | 'md' | 'lg'
|
|
38
41
|
className?: string
|
|
39
42
|
}
|
package/tokens.css
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
--pds-bg-neutral-hover: rgba(0,23,51,0.02);
|
|
17
17
|
--pds-bg-neutral-knob: #ffffff;
|
|
18
18
|
--pds-bg-neutral-knob-off: #ffffff;
|
|
19
|
+
--pds-bg-neutral-thumb: #ffffff;
|
|
19
20
|
--pds-bg-neutral-track: var(--pds-gray-300);
|
|
20
21
|
--pds-bg-neutral-selected: rgba(2,32,71,0.05);
|
|
21
22
|
--pds-bg-neutral-selected-hover: rgba(0,27,55,0.1);
|
|
@@ -81,6 +82,7 @@
|
|
|
81
82
|
--pds-bg-neutral-hover: rgba(209,209,253,0.05);
|
|
82
83
|
--pds-bg-neutral-knob: #2c2c35;
|
|
83
84
|
--pds-bg-neutral-knob-off: var(--pds-gray-800);
|
|
85
|
+
--pds-bg-neutral-thumb: #3c3c47;
|
|
84
86
|
--pds-bg-neutral-track: var(--pds-gray-300);
|
|
85
87
|
--pds-bg-neutral-selected: rgba(217,217,255,0.11);
|
|
86
88
|
--pds-bg-neutral-selected-hover: rgba(222,222,255,0.19);
|
|
@@ -145,6 +147,7 @@
|
|
|
145
147
|
--color-bg-neutral-hover: var(--pds-bg-neutral-hover);
|
|
146
148
|
--color-bg-neutral-knob: var(--pds-bg-neutral-knob);
|
|
147
149
|
--color-bg-neutral-knob-off: var(--pds-bg-neutral-knob-off);
|
|
150
|
+
--color-bg-neutral-thumb: var(--pds-bg-neutral-thumb);
|
|
148
151
|
--color-bg-neutral-track: var(--pds-bg-neutral-track);
|
|
149
152
|
--color-bg-neutral-selected: var(--pds-bg-neutral-selected);
|
|
150
153
|
--color-bg-neutral-selected-hover: var(--pds-bg-neutral-selected-hover);
|