@pantoia/ui 0.12.1 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pantoia/ui",
3
- "version": "0.12.1",
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);