@pantoia/ui 0.2.0 → 0.4.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.
Files changed (66) hide show
  1. package/accordion/index.module.css +4 -4
  2. package/accordion/index.tsx +30 -58
  3. package/alert-dialog/index.tsx +38 -44
  4. package/badge/index.tsx +2 -2
  5. package/button/index.tsx +31 -30
  6. package/callout/index.tsx +29 -8
  7. package/checkbox/index.tsx +24 -88
  8. package/combobox/index.tsx +133 -323
  9. package/command-palette/index.tsx +55 -34
  10. package/dialog/index.tsx +30 -26
  11. package/dist/index.js +155 -27
  12. package/drawer/index.module.css +2 -2
  13. package/drawer/index.tsx +54 -132
  14. package/dropdown-menu/index.tsx +95 -100
  15. package/field/index.tsx +43 -94
  16. package/input/index.tsx +13 -9
  17. package/kbd/index.tsx +2 -2
  18. package/label/index.tsx +7 -9
  19. package/number-field/index.tsx +129 -176
  20. package/package.json +12 -15
  21. package/palette.css +114 -0
  22. package/popover/index.tsx +62 -33
  23. package/radio-group/index.tsx +36 -39
  24. package/registry.json +65 -169
  25. package/select/index.tsx +154 -76
  26. package/switch/index.tsx +13 -6
  27. package/table/index.tsx +5 -6
  28. package/tabs/index.module.css +5 -3
  29. package/tabs/index.tsx +26 -77
  30. package/tailwind.css +3 -0
  31. package/textarea/index.tsx +25 -13
  32. package/toast/index.module.css +187 -0
  33. package/toast/index.tsx +141 -46
  34. package/toggle/index.tsx +8 -7
  35. package/{styles.css → tokens.css} +82 -303
  36. package/tooltip/index.tsx +62 -31
  37. package/tv.ts +0 -1
  38. package/utilities.css +124 -0
  39. package/accordion/AGENTS.md +0 -60
  40. package/alert-dialog/AGENTS.md +0 -78
  41. package/badge/AGENTS.md +0 -35
  42. package/button/AGENTS.md +0 -39
  43. package/callout/AGENTS.md +0 -41
  44. package/checkbox/AGENTS.md +0 -54
  45. package/combobox/AGENTS.md +0 -150
  46. package/command-palette/AGENTS.md +0 -45
  47. package/dialog/AGENTS.md +0 -81
  48. package/drawer/AGENTS.md +0 -66
  49. package/dropdown-menu/AGENTS.md +0 -133
  50. package/field/AGENTS.md +0 -96
  51. package/input/AGENTS.md +0 -35
  52. package/kbd/AGENTS.md +0 -17
  53. package/label/AGENTS.md +0 -21
  54. package/number-field/AGENTS.md +0 -53
  55. package/popover/AGENTS.md +0 -64
  56. package/press.css +0 -37
  57. package/radio-group/AGENTS.md +0 -81
  58. package/select/AGENTS.md +0 -90
  59. package/switch/AGENTS.md +0 -37
  60. package/table/AGENTS.md +0 -82
  61. package/tabs/AGENTS.md +0 -60
  62. package/test-utils.ts +0 -10
  63. package/textarea/AGENTS.md +0 -35
  64. package/toast/AGENTS.md +0 -34
  65. package/toggle/AGENTS.md +0 -37
  66. package/tooltip/AGENTS.md +0 -50
@@ -0,0 +1,187 @@
1
+ @keyframes swipe-out-left {
2
+ from { transform: var(--toast-y) translateX(var(--toast-swipe-movement-x)); opacity: 1; }
3
+ to { transform: var(--toast-y) translateX(calc(var(--toast-swipe-movement-x) - 100%)); opacity: 0; }
4
+ }
5
+
6
+ @keyframes swipe-out-right {
7
+ from { transform: var(--toast-y) translateX(var(--toast-swipe-movement-x)); opacity: 1; }
8
+ to { transform: var(--toast-y) translateX(calc(var(--toast-swipe-movement-x) + 100%)); opacity: 0; }
9
+ }
10
+
11
+ @keyframes swipe-out-up {
12
+ from { transform: var(--toast-y) translateY(var(--toast-swipe-movement-y)); opacity: 1; }
13
+ to { transform: var(--toast-y) translateY(calc(var(--toast-swipe-movement-y) - 100%)); opacity: 0; }
14
+ }
15
+
16
+ @keyframes swipe-out-down {
17
+ from { transform: var(--toast-y) translateY(var(--toast-swipe-movement-y)); opacity: 1; }
18
+ to { transform: var(--toast-y) translateY(calc(var(--toast-swipe-movement-y) + 100%)); opacity: 0; }
19
+ }
20
+
21
+ .viewport {
22
+ --toast-gap: 8px;
23
+ --toast-inset: 16px;
24
+ position: fixed;
25
+ box-sizing: border-box;
26
+ width: 356px;
27
+ margin: 0;
28
+ padding: 0;
29
+ outline: none;
30
+ transition: transform 400ms ease;
31
+ }
32
+
33
+ .viewport[data-position$='right'] { right: var(--toast-inset); }
34
+ .viewport[data-position$='left'] { left: var(--toast-inset); }
35
+ .viewport[data-position$='center'] { left: 50%; transform: translateX(-50%); }
36
+ .viewport[data-position^='top'] { top: var(--toast-inset); }
37
+ .viewport[data-position^='bottom'] { bottom: var(--toast-inset); }
38
+
39
+ .toast {
40
+ --toast-lift: -1;
41
+ --toast-y: translateY(0);
42
+ position: absolute;
43
+ bottom: 0;
44
+ z-index: calc(1000 - var(--toast-index));
45
+ box-sizing: border-box;
46
+ outline: none;
47
+ overflow-wrap: anywhere;
48
+ touch-action: none;
49
+ opacity: 1;
50
+ transform: var(--toast-y);
51
+ transition: transform 400ms, opacity 400ms, height 400ms, box-shadow 200ms;
52
+ }
53
+
54
+ .toast:focus-visible {
55
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1), 0 0 0 2px rgba(0, 0, 0, 0.2);
56
+ }
57
+
58
+ .toast > * {
59
+ transition: opacity 400ms;
60
+ }
61
+
62
+ .viewport[data-position^='top'] .toast { --toast-lift: 1; top: 0; bottom: auto; }
63
+ .viewport[data-position$='right'] .toast { right: 0; }
64
+ .viewport[data-position$='left'] .toast { left: 0; }
65
+
66
+ .toast:not([data-expanded]) {
67
+ --toast-y: translateY(calc(var(--toast-lift) * var(--toast-gap) * var(--toast-index))) scale(calc(1 - var(--toast-index) * 0.05));
68
+ }
69
+
70
+ .toast:not([data-expanded]):not([data-front]) {
71
+ height: var(--toast-frontmost-height);
72
+ }
73
+
74
+ .toast[data-expanded],
75
+ .viewport[data-expand] .toast {
76
+ --toast-y: translateY(calc(var(--toast-lift) * (var(--toast-offset-y) + var(--toast-index) * var(--toast-gap))));
77
+ height: var(--toast-height);
78
+ }
79
+
80
+ .viewport[data-expand] .toast {
81
+ height: auto;
82
+ }
83
+
84
+ .toast[data-expanded]::after,
85
+ .viewport[data-expand] .toast::after {
86
+ content: '';
87
+ position: absolute;
88
+ bottom: 100%;
89
+ left: 0;
90
+ width: 100%;
91
+ height: calc(var(--toast-gap) + 1px);
92
+ }
93
+
94
+ .viewport .toast[data-starting-style] {
95
+ --toast-y: translateY(calc(var(--toast-lift) * -100%));
96
+ opacity: 0;
97
+ }
98
+
99
+ .toast[data-limited] {
100
+ opacity: 0;
101
+ pointer-events: none;
102
+ }
103
+
104
+ .viewport .toast[data-ending-style]:not([data-swipe-direction]) {
105
+ opacity: 0;
106
+ }
107
+
108
+ .viewport .toast[data-ending-style][data-front]:not([data-swipe-direction]) {
109
+ --toast-y: translateY(calc(var(--toast-lift) * -100%));
110
+ }
111
+
112
+ .viewport .toast[data-ending-style][data-expanded]:not([data-front]):not([data-swipe-direction]),
113
+ .viewport[data-expand] .toast[data-ending-style]:not([data-front]):not([data-swipe-direction]) {
114
+ --toast-y: translateY(calc(var(--toast-lift) * (var(--toast-offset-y) + var(--toast-index) * var(--toast-gap)) + var(--toast-lift) * -100%));
115
+ }
116
+
117
+ .viewport:not([data-expand]) .toast[data-ending-style]:not([data-expanded]):not([data-front]):not([data-swipe-direction]) {
118
+ --toast-y: translateY(40%);
119
+ transition: transform 500ms, opacity 200ms;
120
+ }
121
+
122
+ .toast[data-swiping] {
123
+ transition: none;
124
+ }
125
+
126
+ .toast[data-swiping]::before {
127
+ content: '';
128
+ position: absolute;
129
+ left: -100%;
130
+ right: -100%;
131
+ z-index: -1;
132
+ height: 100%;
133
+ top: 50%;
134
+ transform: scaleY(3) translateY(-50%);
135
+ }
136
+
137
+ .viewport[data-position^='top'] .toast[data-swiping]::before {
138
+ top: auto;
139
+ bottom: 50%;
140
+ transform: scaleY(3) translateY(50%);
141
+ }
142
+
143
+ .toast[data-ending-style]:not([data-swiping])::before {
144
+ content: '';
145
+ position: absolute;
146
+ inset: 0;
147
+ transform: scaleY(2);
148
+ }
149
+
150
+ .toast[data-ending-style][data-swipe-direction] {
151
+ animation-duration: 200ms;
152
+ animation-timing-function: ease-out;
153
+ animation-fill-mode: forwards;
154
+ }
155
+
156
+ .toast[data-ending-style][data-swipe-direction='left'] { animation-name: swipe-out-left; }
157
+ .toast[data-ending-style][data-swipe-direction='right'] { animation-name: swipe-out-right; }
158
+ .toast[data-ending-style][data-swipe-direction='up'] { animation-name: swipe-out-up; }
159
+ .toast[data-ending-style][data-swipe-direction='down'] { animation-name: swipe-out-down; }
160
+
161
+ @media (max-width: 600px) {
162
+ .viewport {
163
+ right: var(--toast-inset);
164
+ 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);
172
+ }
173
+
174
+ .viewport[data-position$='center'] {
175
+ left: var(--toast-inset);
176
+ right: var(--toast-inset);
177
+ transform: none;
178
+ }
179
+ }
180
+
181
+ @media (prefers-reduced-motion) {
182
+ .toast,
183
+ .toast > * {
184
+ transition: none !important;
185
+ animation: none !important;
186
+ }
187
+ }
package/toast/index.tsx CHANGED
@@ -1,65 +1,160 @@
1
1
  'use client'
2
2
 
3
- import type { ComponentProps } from 'react'
4
- import { Toaster as SonnerToaster } from 'sonner'
3
+ import { Toast as ToastPrimitive, type ToastManager, type ToastManagerAddOptions } from '@base-ui/react/toast'
4
+ import type { MouseEvent, ReactNode } from 'react'
5
5
 
6
+ import { button } from '../button'
6
7
  import { tv } from '../tv'
7
8
 
8
- const toaster = tv({ base: 'z-toast' })
9
+ import s from './index.module.css'
9
10
 
10
- export { toast } from 'sonner'
11
+ export type ToastPosition = 'top-left' | 'top-center' | 'top-right'
12
+ | 'bottom-left' | 'bottom-center' | 'bottom-right'
11
13
 
12
- export interface ToastProps
13
- extends Omit<ComponentProps<typeof SonnerToaster>, 'id' | 'position' | 'duration' | 'expand'> {
14
- toasterId?: string
15
- position?: 'top-left' | 'top-center' | 'top-right'
16
- | 'bottom-left' | 'bottom-center' | 'bottom-right'
17
- duration?: number
14
+ export type ToastOptions = Omit<ToastManagerAddOptions<object>, 'title' | 'type' | 'actionProps' | 'transitionStatus'> & {
15
+ action?: {
16
+ label: ReactNode
17
+ onClick: (event: MouseEvent<HTMLButtonElement>) => void
18
+ }
19
+ }
20
+
21
+ type Kind = 'success' | 'error' | 'warning' | 'info'
22
+
23
+ export function createToast(manager: ToastManager = ToastPrimitive.createToastManager()) {
24
+ const show = (type?: Kind) => (message: ReactNode, { action, ...options }: ToastOptions = {}) => {
25
+ const id: string = manager.add({
26
+ ...options,
27
+ type,
28
+ title: message,
29
+ actionProps: action && {
30
+ children: action.label,
31
+ onClick: event => {
32
+ action.onClick(event)
33
+ if (!event.defaultPrevented) manager.close(id)
34
+ },
35
+ },
36
+ })
37
+ return id
38
+ }
39
+
40
+ return Object.assign(show(), {
41
+ success: show('success'),
42
+ error: show('error'),
43
+ warning: show('warning'),
44
+ info: show('info'),
45
+ dismiss: (id?: string) => manager.close(id),
46
+ manager,
47
+ })
48
+ }
49
+
50
+ export const toast = createToast()
51
+
52
+ const toastViewport = tv({ base: 'z-toast' })
53
+
54
+ 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
+ },
64
+ })
65
+
66
+ const toastContent = tv({ base: 'min-w-0 flex-1' })
67
+
68
+ const toastTitle = tv({ base: 'text-label text-fg' })
69
+
70
+ const toastDescription = tv({ base: 'text-caption text-fg-muted' })
71
+
72
+ const icons: Record<Kind, ReactNode> = {
73
+ 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">
75
+ <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
+ </svg>
77
+ ),
78
+ 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">
80
+ <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
+ </svg>
82
+ ),
83
+ 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">
85
+ <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
+ </svg>
87
+ ),
88
+ 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">
90
+ <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
+ </svg>
92
+ ),
93
+ }
94
+
95
+ type Swipe = 'up' | 'down' | 'left' | 'right'
96
+
97
+ function swipeOf(position: ToastPosition): Swipe[] {
98
+ const [y, x] = position.split('-')
99
+ const out: Swipe[] = [y === 'top' ? 'up' : 'down']
100
+ if (x === 'left' || x === 'right') out.push(x)
101
+ return out
102
+ }
103
+
104
+ function ToastList({ position }: { position: ToastPosition }) {
105
+ const { toasts } = ToastPrimitive.useToastManager()
106
+ const swipe = swipeOf(position)
107
+
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
+ })
128
+ }
129
+
130
+ export type ToastProps = Omit<ToastPrimitive.Provider.Props, 'children'> & {
131
+ position?: ToastPosition
18
132
  expand?: boolean
133
+ className?: string
134
+ 'aria-label'?: string
19
135
  }
20
136
 
21
137
  export function Toast({
22
- toasterId,
138
+ toastManager = toast.manager,
139
+ timeout = 4000,
23
140
  position = 'bottom-center',
24
- duration = 4000,
25
141
  expand = false,
26
- offset = 16,
27
- gap = 8,
28
142
  className,
29
- toastOptions,
143
+ 'aria-label': label,
30
144
  ...rest
31
145
  }: ToastProps) {
32
146
  return (
33
- <SonnerToaster
34
- {...rest}
35
- id={toasterId}
36
- position={position}
37
- duration={duration}
38
- expand={expand}
39
- offset={offset}
40
- gap={gap}
41
- className={toaster({ className })}
42
- toastOptions={{
43
- unstyled: true,
44
- ...toastOptions,
45
- classNames: {
46
- toast: 'float rounded-r8'
47
- + ' flex w-full items-center gap-stack p-4',
48
- content: 'min-w-0 flex-1',
49
- title: 'text-label text-foreground',
50
- description: 'text-caption text-foreground-muted',
51
- icon: 'shrink-0',
52
- actionButton: 'text-label text-foreground shrink-0 rounded-r8 px-3 h-8'
53
- + ' bg-background-well transition-state press press-shrink enabled:hover:bg-bg-neutral-solid/10'
54
- + ' focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-line-focus-ring'
55
- + ' disabled:text-foreground-disabled disabled:cursor-not-allowed',
56
- success: 'text-fg-success',
57
- error: 'text-fg-danger',
58
- warning: 'text-fg-warning',
59
- info: 'text-fg-info',
60
- ...toastOptions?.classNames,
61
- },
62
- }}
63
- />
147
+ <ToastPrimitive.Provider {...rest} toastManager={toastManager} timeout={timeout}>
148
+ <ToastPrimitive.Portal>
149
+ <ToastPrimitive.Viewport
150
+ {...(label && { 'aria-label': label })}
151
+ data-position={position}
152
+ data-expand={expand ? '' : undefined}
153
+ className={toastViewport({ className: [s.viewport, className] })}
154
+ >
155
+ <ToastList position={position} />
156
+ </ToastPrimitive.Viewport>
157
+ </ToastPrimitive.Portal>
158
+ </ToastPrimitive.Provider>
64
159
  )
65
160
  }
package/toggle/index.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { Toggle as TogglePrimitive } from 'radix-ui'
3
+ import { Toggle as TogglePrimitive } from '@base-ui/react/toggle'
4
4
  import type { ComponentProps } from 'react'
5
5
 
6
6
  import { tv } from '../tv'
@@ -9,29 +9,30 @@ export const toggle = tv({
9
9
  base: 'text-label rounded-full inline-flex h-8 items-center gap-tight px-3'
10
10
  + ' [&_svg]:size-4'
11
11
  + ' ring-1 ring-inset ring-line-strong transition-state press press-shrink'
12
- + ' text-foreground-muted'
12
+ + ' text-fg-muted'
13
13
  + ' enabled:hover:bg-bg-neutral-hover'
14
14
  + ' on:bg-bg-neutral-solid on:ring-line-neutral-solid on:enabled:text-fg-neutral-solid-contrast'
15
15
  + ' on:enabled:hover:bg-bg-neutral-solid-hover'
16
16
  + ' focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-line-focus-ring'
17
- + ' disabled:text-foreground-disabled disabled:ring-line disabled:cursor-not-allowed'
17
+ + ' disabled:text-fg-disabled disabled:ring-line disabled:cursor-not-allowed'
18
18
  + ' on:disabled:bg-bg-neutral-solid-checked-disabled on:disabled:ring-transparent on:disabled:text-fg-neutral-solid-contrast',
19
19
 
20
20
  variants: {
21
21
  surface: {
22
22
  bare: '',
23
- well: 'bg-background-well',
24
- panel: 'bg-background-panel',
23
+ well: 'bg-bg-well',
24
+ panel: 'bg-bg-panel',
25
25
  },
26
26
  },
27
27
 
28
28
  defaultVariants: { surface: 'bare' },
29
29
  })
30
30
 
31
- export interface ToggleProps extends ComponentProps<typeof TogglePrimitive.Root> {
31
+ export type ToggleProps = Omit<ComponentProps<typeof TogglePrimitive>, 'className'> & {
32
32
  surface?: 'bare' | 'well' | 'panel'
33
+ className?: string
33
34
  }
34
35
 
35
36
  export function Toggle({ surface, className, ...rest }: ToggleProps) {
36
- return <TogglePrimitive.Root {...rest} className={toggle({ surface, className })} />
37
+ return <TogglePrimitive {...rest} className={toggle({ surface, className })} />
37
38
  }