@pantoia/ui 0.12.1 → 0.14.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/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 +73 -3
- 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/toast/index.module.css +11 -8
- package/toast/index.tsx +64 -37
- package/toggle/index.tsx +49 -11
- package/toggle-group/index.tsx +76 -0
- package/tokens.css +9 -0
- package/tooltip/index.tsx +9 -2
- package/utilities.css +1 -1
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",
|
|
@@ -717,12 +768,10 @@
|
|
|
717
768
|
"@base-ui/react"
|
|
718
769
|
],
|
|
719
770
|
"registryDependencies": [
|
|
720
|
-
"button",
|
|
721
771
|
"tv"
|
|
722
772
|
],
|
|
723
773
|
"meta": {
|
|
724
|
-
"layer": "ui"
|
|
725
|
-
"radius": "rounded-r8"
|
|
774
|
+
"layer": "ui"
|
|
726
775
|
}
|
|
727
776
|
},
|
|
728
777
|
{
|
|
@@ -748,6 +797,27 @@
|
|
|
748
797
|
]
|
|
749
798
|
}
|
|
750
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
|
+
}
|
|
820
|
+
},
|
|
751
821
|
{
|
|
752
822
|
"name": "tooltip",
|
|
753
823
|
"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, 떨어진 알약 여러 개 중에 고르면 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/toast/index.module.css
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
--toast-inset: 16px;
|
|
24
24
|
position: fixed;
|
|
25
25
|
box-sizing: border-box;
|
|
26
|
-
width:
|
|
26
|
+
width: 352px;
|
|
27
27
|
margin: 0;
|
|
28
28
|
padding: 0;
|
|
29
29
|
outline: none;
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
touch-action: none;
|
|
49
49
|
opacity: 1;
|
|
50
50
|
transform: var(--toast-y);
|
|
51
|
+
backdrop-filter: blur(12px);
|
|
51
52
|
transition: transform 400ms, opacity 400ms, height 400ms, box-shadow 200ms;
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -62,6 +63,7 @@
|
|
|
62
63
|
.viewport[data-position^='top'] .toast { --toast-lift: 1; top: 0; bottom: auto; }
|
|
63
64
|
.viewport[data-position$='right'] .toast { right: 0; }
|
|
64
65
|
.viewport[data-position$='left'] .toast { left: 0; }
|
|
66
|
+
.viewport[data-position$='center'] .toast { left: 0; right: 0; margin-inline: auto; }
|
|
65
67
|
|
|
66
68
|
.toast:not([data-expanded]) {
|
|
67
69
|
--toast-y: translateY(calc(var(--toast-lift) * var(--toast-gap) * var(--toast-index))) scale(calc(1 - var(--toast-index) * 0.05));
|
|
@@ -69,6 +71,13 @@
|
|
|
69
71
|
|
|
70
72
|
.toast:not([data-expanded]):not([data-front]) {
|
|
71
73
|
height: var(--toast-frontmost-height);
|
|
74
|
+
width: var(--toast-frontmost-width, auto);
|
|
75
|
+
border-radius: 20px;
|
|
76
|
+
overflow: hidden;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.viewport:not([data-expand]) .toast:not([data-expanded]):not([data-front]) > * {
|
|
80
|
+
opacity: 0;
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
.toast[data-expanded],
|
|
@@ -162,13 +171,7 @@
|
|
|
162
171
|
.viewport {
|
|
163
172
|
right: var(--toast-inset);
|
|
164
173
|
left: var(--toast-inset);
|
|
165
|
-
width:
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
.viewport .toast {
|
|
169
|
-
right: 0;
|
|
170
|
-
left: 0;
|
|
171
|
-
width: calc(100% - var(--toast-inset) * 2);
|
|
174
|
+
width: auto;
|
|
172
175
|
}
|
|
173
176
|
|
|
174
177
|
.viewport[data-position$='center'] {
|
package/toast/index.tsx
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { Toast as ToastPrimitive, type ToastManager, type ToastManagerAddOptions } from '@base-ui/react/toast'
|
|
4
|
-
import type
|
|
4
|
+
import { type MouseEvent, type ReactNode, useLayoutEffect, useRef, useState } from 'react'
|
|
5
5
|
|
|
6
|
-
import { button } from '../button'
|
|
7
6
|
import { tv } from '../tv'
|
|
8
7
|
|
|
9
8
|
import s from './index.module.css'
|
|
@@ -52,41 +51,41 @@ export const toast = createToast()
|
|
|
52
51
|
const toastViewport = tv({ base: 'z-toast' })
|
|
53
52
|
|
|
54
53
|
const toastRoot = tv({
|
|
55
|
-
base: '
|
|
56
|
-
|
|
57
|
-
type: {
|
|
58
|
-
success: 'text-fg-success',
|
|
59
|
-
error: 'text-fg-danger',
|
|
60
|
-
warning: 'text-fg-warning',
|
|
61
|
-
info: 'text-fg-info',
|
|
62
|
-
},
|
|
63
|
-
},
|
|
54
|
+
base: 'bg-bg-neutral-toast text-fg-neutral-toast-contrast inset-ring inset-ring-line-neutral-toast flex min-h-10 w-fit max-w-full items-center gap-2.5 rounded-full px-4.5 py-2.5'
|
|
55
|
+
+ ' data-multiline:rounded-r20 data-multiline:w-full data-multiline:items-start data-multiline:px-4 data-multiline:py-3',
|
|
64
56
|
})
|
|
65
57
|
|
|
58
|
+
const toastIcon = tv({ base: 'shrink-0 in-data-multiline:pt-px' })
|
|
59
|
+
|
|
66
60
|
const toastContent = tv({ base: 'min-w-0 flex-1' })
|
|
67
61
|
|
|
68
|
-
const toastTitle = tv({ base: 'text-
|
|
62
|
+
const toastTitle = tv({ base: 'text-t14 in-data-multiline:font-medium' })
|
|
63
|
+
|
|
64
|
+
const toastDescription = tv({ base: 'text-t13 text-fg-neutral-toast-contrast/72 line-clamp-2' })
|
|
69
65
|
|
|
70
|
-
const
|
|
66
|
+
const toastAction = tv({
|
|
67
|
+
base: 'text-t14 rounded-r4 shrink-0 cursor-pointer font-bold underline underline-offset-3 in-data-multiline:self-center'
|
|
68
|
+
+ ' focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-line-focus-ring',
|
|
69
|
+
})
|
|
71
70
|
|
|
72
71
|
const icons: Record<Kind, ReactNode> = {
|
|
73
72
|
success: (
|
|
74
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" height="
|
|
73
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" height="18" width="18" aria-hidden="true" className="size-4.5">
|
|
75
74
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clipRule="evenodd" />
|
|
76
75
|
</svg>
|
|
77
76
|
),
|
|
78
77
|
warning: (
|
|
79
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" height="
|
|
78
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" height="18" width="18" aria-hidden="true" className="size-4.5">
|
|
80
79
|
<path fillRule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" clipRule="evenodd" />
|
|
81
80
|
</svg>
|
|
82
81
|
),
|
|
83
82
|
info: (
|
|
84
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" height="
|
|
83
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" height="18" width="18" aria-hidden="true" className="size-4.5">
|
|
85
84
|
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clipRule="evenodd" />
|
|
86
85
|
</svg>
|
|
87
86
|
),
|
|
88
87
|
error: (
|
|
89
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" height="
|
|
88
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" height="18" width="18" aria-hidden="true" className="size-4.5">
|
|
90
89
|
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z" clipRule="evenodd" />
|
|
91
90
|
</svg>
|
|
92
91
|
),
|
|
@@ -101,30 +100,58 @@ function swipeOf(position: ToastPosition): Swipe[] {
|
|
|
101
100
|
return out
|
|
102
101
|
}
|
|
103
102
|
|
|
103
|
+
const ONE_LINE = 28
|
|
104
|
+
|
|
105
|
+
type Item = ReturnType<typeof ToastPrimitive.useToastManager>['toasts'][number]
|
|
106
|
+
|
|
107
|
+
function ToastItem({ item, front, swipe }: { item: Item, front: boolean, swipe: Swipe[] }) {
|
|
108
|
+
const title = useRef<HTMLHeadingElement>(null)
|
|
109
|
+
const root = useRef<HTMLDivElement>(null)
|
|
110
|
+
const [wrapped, setWrapped] = useState(false)
|
|
111
|
+
const icon = item.type ? icons[item.type as Kind] : undefined
|
|
112
|
+
const multiline = wrapped || Boolean(item.description)
|
|
113
|
+
|
|
114
|
+
useLayoutEffect(() => {
|
|
115
|
+
if (multiline || !title.current) return
|
|
116
|
+
if (title.current.offsetHeight > ONE_LINE) setWrapped(true)
|
|
117
|
+
}, [multiline])
|
|
118
|
+
|
|
119
|
+
useLayoutEffect(() => {
|
|
120
|
+
const el = root.current
|
|
121
|
+
const viewport = el?.parentElement
|
|
122
|
+
if (!front || !el || !viewport) return
|
|
123
|
+
const sync = () => viewport.style.setProperty('--toast-frontmost-width', `${el.offsetWidth}px`)
|
|
124
|
+
sync()
|
|
125
|
+
if (typeof ResizeObserver === 'undefined') return
|
|
126
|
+
const observer = new ResizeObserver(sync)
|
|
127
|
+
observer.observe(el)
|
|
128
|
+
return () => observer.disconnect()
|
|
129
|
+
}, [front])
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<ToastPrimitive.Root
|
|
133
|
+
ref={root}
|
|
134
|
+
toast={item}
|
|
135
|
+
swipeDirection={swipe}
|
|
136
|
+
data-front={front ? '' : undefined}
|
|
137
|
+
data-multiline={multiline ? '' : undefined}
|
|
138
|
+
className={toastRoot({ className: s.toast })}
|
|
139
|
+
>
|
|
140
|
+
{icon && <div className={toastIcon()}>{icon}</div>}
|
|
141
|
+
<ToastPrimitive.Content className={toastContent()}>
|
|
142
|
+
<ToastPrimitive.Title ref={title} className={toastTitle()} />
|
|
143
|
+
<ToastPrimitive.Description className={toastDescription()} />
|
|
144
|
+
</ToastPrimitive.Content>
|
|
145
|
+
<ToastPrimitive.Action className={toastAction()} />
|
|
146
|
+
</ToastPrimitive.Root>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
104
150
|
function ToastList({ position }: { position: ToastPosition }) {
|
|
105
151
|
const { toasts } = ToastPrimitive.useToastManager()
|
|
106
152
|
const swipe = swipeOf(position)
|
|
107
153
|
|
|
108
|
-
return toasts.map((item, index) => {
|
|
109
|
-
const icon = item.type ? icons[item.type as Kind] : undefined
|
|
110
|
-
|
|
111
|
-
return (
|
|
112
|
-
<ToastPrimitive.Root
|
|
113
|
-
key={item.id}
|
|
114
|
-
toast={item}
|
|
115
|
-
swipeDirection={swipe}
|
|
116
|
-
data-front={index === 0 ? '' : undefined}
|
|
117
|
-
className={toastRoot({ type: item.type as Kind | undefined, className: s.toast })}
|
|
118
|
-
>
|
|
119
|
-
{icon && <div className="shrink-0">{icon}</div>}
|
|
120
|
-
<ToastPrimitive.Content className={toastContent()}>
|
|
121
|
-
<ToastPrimitive.Title className={toastTitle()} />
|
|
122
|
-
<ToastPrimitive.Description className={toastDescription()} />
|
|
123
|
-
</ToastPrimitive.Content>
|
|
124
|
-
<ToastPrimitive.Action className={button({ color: 'neutral', emphasis: 'weak', size: 'sm' })} />
|
|
125
|
-
</ToastPrimitive.Root>
|
|
126
|
-
)
|
|
127
|
-
})
|
|
154
|
+
return toasts.map((item, index) => <ToastItem key={item.id} item={item} front={index === 0} swipe={swipe} />)
|
|
128
155
|
}
|
|
129
156
|
|
|
130
157
|
export type ToastProps = Omit<ToastPrimitive.Provider.Props, 'children'> & {
|
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
|
}
|