@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/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,8 @@
1
+ .swatch {
2
+ background-color: var(--swatch-picker-color);
3
+ }
4
+
5
+ .check {
6
+ color: white;
7
+ color: oklch(from var(--swatch-picker-color) clamp(0, (0.7 - l) * 1000, 1) 0 0);
8
+ }
@@ -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
+ }
@@ -23,7 +23,7 @@
23
23
  --toast-inset: 16px;
24
24
  position: fixed;
25
25
  box-sizing: border-box;
26
- width: 356px;
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: 100%;
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 { MouseEvent, ReactNode } from 'react'
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: 'float rounded-r8 flex w-full items-center gap-stack p-4',
56
- variants: {
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-label text-fg' })
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 toastDescription = tv({ base: 'text-caption text-fg-muted' })
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="20" width="20" aria-hidden="true" className="size-5">
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="20" width="20" aria-hidden="true" className="size-5">
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="20" width="20" aria-hidden="true" className="size-5">
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="20" width="20" aria-hidden="true" className="size-5">
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 rounded-full inline-flex h-8 items-center gap-tight px-3'
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
- defaultVariants: { surface: 'bare' },
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
- surface?: 'bare' | 'well' | 'panel'
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
  }