@pantoia/ui 0.12.0 → 0.13.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.
@@ -109,11 +109,11 @@ export function AlertDialogHeader({ lead, trail, className, children }: AlertDia
109
109
  )
110
110
  }
111
111
 
112
- const alertDialogBody = tv({ base: '-m-1 min-h-0 overflow-y-auto p-1' })
112
+ const alertDialogBody = tv({ base: '-m-1 min-h-0 overflow-y-auto p-1 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-line-focus-ring' })
113
113
 
114
114
  export function AlertDialogBody({ className, children }: { className?: string, children?: ReactNode }) {
115
115
  return (
116
- <div className={alertDialogBody({ className })}>
116
+ <div tabIndex={0} className={alertDialogBody({ className })}>
117
117
  {children}
118
118
  </div>
119
119
  )
@@ -100,11 +100,11 @@ export function BottomSheetDescription({ className, ...rest }: BottomSheetDescri
100
100
  )
101
101
  }
102
102
 
103
- const bottomSheetBody = tv({ base: '-m-1 min-h-0 overflow-y-auto p-1' })
103
+ const bottomSheetBody = tv({ base: '-m-1 min-h-0 overflow-y-auto p-1 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-line-focus-ring' })
104
104
 
105
105
  export function BottomSheetBody({ className, children }: { className?: string, children?: ReactNode }) {
106
106
  return (
107
- <div className={bottomSheetBody({ className })}>
107
+ <div tabIndex={0} className={bottomSheetBody({ className })}>
108
108
  {children}
109
109
  </div>
110
110
  )
package/dialog/index.tsx CHANGED
@@ -108,11 +108,11 @@ export function DialogHeader({ lead, trail, className, children }: DialogHeaderP
108
108
  )
109
109
  }
110
110
 
111
- const dialogBody = tv({ base: '-m-1 min-h-0 overflow-y-auto p-1' })
111
+ const dialogBody = tv({ base: '-m-1 min-h-0 overflow-y-auto p-1 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-line-focus-ring' })
112
112
 
113
113
  export function DialogBody({ className, children }: { className?: string, children?: ReactNode }) {
114
114
  return (
115
- <div className={dialogBody({ className })}>
115
+ <div tabIndex={0} className={dialogBody({ className })}>
116
116
  {children}
117
117
  </div>
118
118
  )
package/drawer/index.tsx CHANGED
@@ -123,11 +123,11 @@ export function DrawerDescription({ className, ...rest }: DrawerDescriptionProps
123
123
  )
124
124
  }
125
125
 
126
- const drawerBody = tv({ base: '-m-1 min-h-0 overflow-y-auto p-1' })
126
+ const drawerBody = tv({ base: '-m-1 min-h-0 overflow-y-auto p-1 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-line-focus-ring' })
127
127
 
128
128
  export function DrawerBody({ className, children }: { className?: string, children?: ReactNode }) {
129
129
  return (
130
- <div className={drawerBody({ className })}>
130
+ <div tabIndex={0} className={drawerBody({ className })}>
131
131
  {children}
132
132
  </div>
133
133
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pantoia/ui",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Pantoia Design System",
5
5
  "homepage": "https://design.pantoia.com",
6
6
  "repository": {
package/registry.json CHANGED
@@ -717,12 +717,10 @@
717
717
  "@base-ui/react"
718
718
  ],
719
719
  "registryDependencies": [
720
- "button",
721
720
  "tv"
722
721
  ],
723
722
  "meta": {
724
- "layer": "ui",
725
- "radius": "rounded-r8"
723
+ "layer": "ui"
726
724
  }
727
725
  },
728
726
  {
@@ -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/tokens.css CHANGED
@@ -17,6 +17,7 @@
17
17
  --pds-bg-neutral-knob: #ffffff;
18
18
  --pds-bg-neutral-knob-off: #ffffff;
19
19
  --pds-bg-neutral-thumb: #ffffff;
20
+ --pds-bg-neutral-toast: rgba(25,31,40,0.94);
20
21
  --pds-bg-neutral-track: var(--pds-gray-300);
21
22
  --pds-bg-neutral-selected: rgba(2,32,71,0.05);
22
23
  --pds-bg-neutral-selected-hover: rgba(0,27,55,0.1);
@@ -30,6 +31,7 @@
30
31
 
31
32
  --pds-fg-brand-contrast: var(--pds-brand-700);
32
33
  --pds-fg-brand-solid-contrast: #ffffff;
34
+ --pds-fg-neutral-toast-contrast: var(--pds-white);
33
35
  --pds-fg-danger: var(--pds-red-500);
34
36
  --pds-fg-danger-contrast: var(--pds-red-700);
35
37
  --pds-fg-info: var(--pds-blue-500);
@@ -48,6 +50,7 @@
48
50
  --pds-line-info: var(--pds-blue-500);
49
51
  --pds-line-info-weak: var(--pds-blue-200);
50
52
  --pds-line-neutral-solid: var(--pds-gray-900);
53
+ --pds-line-neutral-toast: transparent;
51
54
  --pds-line-strong: var(--pds-gray-300);
52
55
  --pds-line-success: var(--pds-green-500);
53
56
  --pds-line-success-weak: var(--pds-green-200);
@@ -83,6 +86,7 @@
83
86
  --pds-bg-neutral-knob: #2c2c35;
84
87
  --pds-bg-neutral-knob-off: var(--pds-gray-800);
85
88
  --pds-bg-neutral-thumb: #3c3c47;
89
+ --pds-bg-neutral-toast: rgba(0,0,0,0.92);
86
90
  --pds-bg-neutral-track: var(--pds-gray-300);
87
91
  --pds-bg-neutral-selected: rgba(217,217,255,0.11);
88
92
  --pds-bg-neutral-selected-hover: rgba(222,222,255,0.19);
@@ -96,6 +100,7 @@
96
100
 
97
101
  --pds-fg-brand-contrast: var(--pds-brand-700);
98
102
  --pds-fg-brand-solid-contrast: #ffffff;
103
+ --pds-fg-neutral-toast-contrast: var(--pds-white);
99
104
  --pds-fg-danger: var(--pds-red-500);
100
105
  --pds-fg-danger-contrast: var(--pds-red-700);
101
106
  --pds-fg-info: var(--pds-blue-500);
@@ -114,6 +119,7 @@
114
119
  --pds-line-info: var(--pds-blue-500);
115
120
  --pds-line-info-weak: var(--pds-blue-200);
116
121
  --pds-line-neutral-solid: var(--pds-gray-900);
122
+ --pds-line-neutral-toast: rgba(255,255,255,0.1);
117
123
  --pds-line-strong: var(--pds-gray-300);
118
124
  --pds-line-success: var(--pds-green-500);
119
125
  --pds-line-success-weak: var(--pds-green-200);
@@ -148,6 +154,7 @@
148
154
  --color-bg-neutral-knob: var(--pds-bg-neutral-knob);
149
155
  --color-bg-neutral-knob-off: var(--pds-bg-neutral-knob-off);
150
156
  --color-bg-neutral-thumb: var(--pds-bg-neutral-thumb);
157
+ --color-bg-neutral-toast: var(--pds-bg-neutral-toast);
151
158
  --color-bg-neutral-track: var(--pds-bg-neutral-track);
152
159
  --color-bg-neutral-selected: var(--pds-bg-neutral-selected);
153
160
  --color-bg-neutral-selected-hover: var(--pds-bg-neutral-selected-hover);
@@ -160,6 +167,7 @@
160
167
  --color-bg-warning-weak: var(--pds-bg-warning-weak);
161
168
  --color-fg-brand-contrast: var(--pds-fg-brand-contrast);
162
169
  --color-fg-brand-solid-contrast: var(--pds-fg-brand-solid-contrast);
170
+ --color-fg-neutral-toast-contrast: var(--pds-fg-neutral-toast-contrast);
163
171
  --color-fg-danger: var(--pds-fg-danger);
164
172
  --color-fg-danger-contrast: var(--pds-fg-danger-contrast);
165
173
  --color-fg-info: var(--pds-fg-info);
@@ -177,6 +185,7 @@
177
185
  --color-line-info: var(--pds-line-info);
178
186
  --color-line-info-weak: var(--pds-line-info-weak);
179
187
  --color-line-neutral-solid: var(--pds-line-neutral-solid);
188
+ --color-line-neutral-toast: var(--pds-line-neutral-toast);
180
189
  --color-line-strong: var(--pds-line-strong);
181
190
  --color-line-success: var(--pds-line-success);
182
191
  --color-line-success-weak: var(--pds-line-success-weak);