@toolpath/ui 0.1.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/LICENSE +21 -0
- package/README.md +44 -0
- package/dist/index.d.ts +1174 -0
- package/dist/index.js +5934 -0
- package/package.json +68 -0
- package/src/attribution/action.tsx +13 -0
- package/src/attribution/at.tsx +28 -0
- package/src/attribution/attribution.tsx +16 -0
- package/src/attribution/by.tsx +21 -0
- package/src/attribution/format-time.ts +48 -0
- package/src/attribution/index.ts +20 -0
- package/src/badge/badge.tsx +38 -0
- package/src/badge/index.ts +1 -0
- package/src/breadcrumbs/breadcrumbs-context.tsx +26 -0
- package/src/breadcrumbs/breadcrumbs.tsx +24 -0
- package/src/breadcrumbs/index.ts +12 -0
- package/src/breadcrumbs/item.tsx +85 -0
- package/src/button/animated-ellipsis.tsx +44 -0
- package/src/button/button.tsx +168 -0
- package/src/button/index.ts +1 -0
- package/src/callout/callout.tsx +202 -0
- package/src/callout/index.ts +2 -0
- package/src/callout/use-dismissed-callouts.ts +78 -0
- package/src/card/card.tsx +19 -0
- package/src/card/index.ts +2 -0
- package/src/checkbox/checkbox.tsx +76 -0
- package/src/checkbox/index.ts +1 -0
- package/src/combobox/button.tsx +99 -0
- package/src/combobox/combobox-context.tsx +42 -0
- package/src/combobox/combobox.tsx +60 -0
- package/src/combobox/empty.tsx +21 -0
- package/src/combobox/group-label.tsx +20 -0
- package/src/combobox/group.tsx +11 -0
- package/src/combobox/icon.tsx +11 -0
- package/src/combobox/index.tsx +64 -0
- package/src/combobox/input.tsx +77 -0
- package/src/combobox/item-indicator.tsx +16 -0
- package/src/combobox/item.tsx +58 -0
- package/src/combobox/list.tsx +19 -0
- package/src/combobox/popover.tsx +62 -0
- package/src/combobox/trigger.tsx +33 -0
- package/src/combobox/value.tsx +12 -0
- package/src/common/index.ts +1 -0
- package/src/common/svg.tsx +17 -0
- package/src/dialog/actions.tsx +17 -0
- package/src/dialog/confirm-button.tsx +25 -0
- package/src/dialog/dialog-context.tsx +21 -0
- package/src/dialog/dialog.tsx +37 -0
- package/src/dialog/dismiss-button.tsx +29 -0
- package/src/dialog/index.ts +37 -0
- package/src/dialog/message.tsx +20 -0
- package/src/dialog/provider.tsx +102 -0
- package/src/dialog/title.tsx +20 -0
- package/src/editable-cell/editable-cell.tsx +307 -0
- package/src/editable-cell/index.ts +7 -0
- package/src/editable-cell/use-editable-grid.tsx +296 -0
- package/src/field/field.tsx +22 -0
- package/src/field/index.ts +2 -0
- package/src/helpers/cn.ts +6 -0
- package/src/helpers/index.ts +1 -0
- package/src/icon-button/icon-button.tsx +126 -0
- package/src/icon-button/index.ts +2 -0
- package/src/index.ts +32 -0
- package/src/input/index.ts +1 -0
- package/src/input/input.tsx +276 -0
- package/src/link/index.ts +4 -0
- package/src/link/link-context.tsx +33 -0
- package/src/link/link.tsx +24 -0
- package/src/menu/divider.tsx +17 -0
- package/src/menu/index.ts +40 -0
- package/src/menu/item.tsx +149 -0
- package/src/menu/menu-context.tsx +24 -0
- package/src/menu/menu.tsx +14 -0
- package/src/menu/popover.tsx +69 -0
- package/src/menu/submenu-trigger.tsx +48 -0
- package/src/menu/submenu.tsx +20 -0
- package/src/menu/trigger.tsx +43 -0
- package/src/notification/index.ts +16 -0
- package/src/notification/list.tsx +123 -0
- package/src/notification/provider.tsx +75 -0
- package/src/page-header/index.ts +2 -0
- package/src/page-header/page-header.tsx +14 -0
- package/src/pagination/index.ts +1 -0
- package/src/pagination/pagination.tsx +136 -0
- package/src/panels/collapsed.tsx +32 -0
- package/src/panels/group.tsx +27 -0
- package/src/panels/index.ts +57 -0
- package/src/panels/layout-context.ts +5 -0
- package/src/panels/orientation-context.tsx +5 -0
- package/src/panels/panel.tsx +95 -0
- package/src/panels/separator.tsx +44 -0
- package/src/pin/index.ts +1 -0
- package/src/pin/pin.tsx +99 -0
- package/src/progress-bar/index.ts +1 -0
- package/src/progress-bar/progress-bar.tsx +48 -0
- package/src/quantity-input/index.ts +1 -0
- package/src/quantity-input/quantity-input.tsx +158 -0
- package/src/scroll-area/index.ts +1 -0
- package/src/scroll-area/scroll-area.tsx +82 -0
- package/src/slider/index.ts +1 -0
- package/src/slider/slider.tsx +75 -0
- package/src/star/index.ts +1 -0
- package/src/star/star.tsx +99 -0
- package/src/switch/index.ts +1 -0
- package/src/switch/switch.tsx +33 -0
- package/src/table/body.tsx +15 -0
- package/src/table/cell-context.tsx +48 -0
- package/src/table/cell.tsx +83 -0
- package/src/table/group-empty-state.tsx +28 -0
- package/src/table/group-header.tsx +62 -0
- package/src/table/header-cell.tsx +158 -0
- package/src/table/header-row.tsx +28 -0
- package/src/table/index.ts +56 -0
- package/src/table/is-group-empty-state.ts +17 -0
- package/src/table/is-group-header.ts +5 -0
- package/src/table/row-context.tsx +61 -0
- package/src/table/row-disabled-context.tsx +7 -0
- package/src/table/row.tsx +98 -0
- package/src/table/table-context.tsx +127 -0
- package/src/table/table-empty.tsx +57 -0
- package/src/table/table.tsx +547 -0
- package/src/table/tree-icon.tsx +147 -0
- package/src/table/use-checkbox.ts +200 -0
- package/src/table/use-column-layout.ts +117 -0
- package/src/table/use-groups.ts +134 -0
- package/src/table/use-keyboard-navigation.ts +274 -0
- package/src/table/use-sort-functions.ts +90 -0
- package/src/tabs/index.ts +20 -0
- package/src/tabs/list.tsx +29 -0
- package/src/tabs/tab.tsx +141 -0
- package/src/tabs/tabs-context.tsx +38 -0
- package/src/tabs/tabs.tsx +27 -0
- package/src/textarea/index.ts +1 -0
- package/src/textarea/textarea.tsx +88 -0
- package/src/toast/index.ts +19 -0
- package/src/toast/list.tsx +133 -0
- package/src/toast/provider.tsx +36 -0
- package/src/toggle/index.ts +17 -0
- package/src/toggle/indicator.tsx +27 -0
- package/src/toggle/item.tsx +54 -0
- package/src/toggle/toggle-context.tsx +68 -0
- package/src/toggle/toggle.tsx +124 -0
- package/src/toggle/use-indicator.tsx +69 -0
- package/src/tooltip/index.ts +1 -0
- package/src/tooltip/tooltip.tsx +122 -0
- package/theme.css +70 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import React, { Ref, useCallback, useEffect, useRef } from 'react'
|
|
2
|
+
import { Menu as BaseMenu, MenuItemProps as BaseMenuItemProps, ContextMenu } from '@base-ui/react'
|
|
3
|
+
import { cn } from '../helpers'
|
|
4
|
+
import { useMenu } from './menu-context'
|
|
5
|
+
|
|
6
|
+
type BaseUIEvent<E extends React.SyntheticEvent> = E & {
|
|
7
|
+
preventBaseUIHandler: () => void
|
|
8
|
+
readonly baseUIHandlerPrevented?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type MenuItemProps = BaseMenuItemProps & {
|
|
12
|
+
variant?: 'default' | 'danger'
|
|
13
|
+
ref?: Ref<HTMLElement>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const Item = ({
|
|
17
|
+
children,
|
|
18
|
+
className,
|
|
19
|
+
disabled,
|
|
20
|
+
variant = 'default',
|
|
21
|
+
ref,
|
|
22
|
+
onClick,
|
|
23
|
+
...props
|
|
24
|
+
}: MenuItemProps) => {
|
|
25
|
+
const { context } = useMenu()
|
|
26
|
+
|
|
27
|
+
// Wrap onClick to stop propagation, preventing parent click handlers from firing
|
|
28
|
+
const handleClick = (event: BaseUIEvent<React.MouseEvent<HTMLDivElement>>) => {
|
|
29
|
+
event.stopPropagation()
|
|
30
|
+
onClick?.(event)
|
|
31
|
+
}
|
|
32
|
+
const internalRef = useRef<HTMLElement>(null)
|
|
33
|
+
// Track if we should block the next click (due to right-click mouseup)
|
|
34
|
+
const blockNextClickRef = useRef(false)
|
|
35
|
+
|
|
36
|
+
// Merge passed ref with internal ref (only needed for context menus)
|
|
37
|
+
const setRefs = useCallback(
|
|
38
|
+
(element: HTMLDivElement | null) => {
|
|
39
|
+
internalRef.current = element
|
|
40
|
+
if (typeof ref === 'function') {
|
|
41
|
+
ref(element)
|
|
42
|
+
} else if (ref && 'current' in ref) {
|
|
43
|
+
ref.current = element
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
[ref],
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
// Attach native DOM event listeners during capture phase to intercept
|
|
50
|
+
// right-click mouseup before @base-ui/react's handlers can trigger selection.
|
|
51
|
+
// The library calls element.click() directly on mouseup for right-clicks,
|
|
52
|
+
// so we need to block both the mouseup and the resulting synthetic click.
|
|
53
|
+
// Only needed for context menus since they're opened via right-click.
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!context) {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const element = internalRef.current
|
|
60
|
+
if (!element) {
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let resetTimeoutId: ReturnType<typeof setTimeout> | undefined
|
|
65
|
+
|
|
66
|
+
const handleMouseUpCapture = (event: MouseEvent) => {
|
|
67
|
+
// Block right-click (button === 2) mouseup events
|
|
68
|
+
if (event.button === 2) {
|
|
69
|
+
// Use stopImmediatePropagation to prevent ALL other listeners on this
|
|
70
|
+
// element from firing, not just listeners on parent elements
|
|
71
|
+
event.stopImmediatePropagation()
|
|
72
|
+
event.preventDefault()
|
|
73
|
+
// Mark that we should also block the next click event
|
|
74
|
+
// (which @base-ui/react triggers via element.click())
|
|
75
|
+
blockNextClickRef.current = true
|
|
76
|
+
// Reset the flag after a short delay in case click doesn't fire
|
|
77
|
+
resetTimeoutId = setTimeout(() => {
|
|
78
|
+
blockNextClickRef.current = false
|
|
79
|
+
}, 100)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const handleClickCapture = (event: MouseEvent) => {
|
|
84
|
+
// Block clicks that were triggered by right-click mouseup
|
|
85
|
+
if (blockNextClickRef.current) {
|
|
86
|
+
event.stopImmediatePropagation()
|
|
87
|
+
event.preventDefault()
|
|
88
|
+
blockNextClickRef.current = false
|
|
89
|
+
if (resetTimeoutId) {
|
|
90
|
+
clearTimeout(resetTimeoutId)
|
|
91
|
+
resetTimeoutId = undefined
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Use capture phase to intercept before @base-ui/react's handlers
|
|
97
|
+
element.addEventListener('mouseup', handleMouseUpCapture, true)
|
|
98
|
+
element.addEventListener('click', handleClickCapture, true)
|
|
99
|
+
|
|
100
|
+
return () => {
|
|
101
|
+
element.removeEventListener('mouseup', handleMouseUpCapture, true)
|
|
102
|
+
element.removeEventListener('click', handleClickCapture, true)
|
|
103
|
+
if (resetTimeoutId) {
|
|
104
|
+
clearTimeout(resetTimeoutId)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}, [context])
|
|
108
|
+
|
|
109
|
+
const itemClassName = cn(
|
|
110
|
+
'group flex w-full items-center p-2 py-1 text-left whitespace-nowrap',
|
|
111
|
+
'text-xs font-medium outline-none cursor-pointer',
|
|
112
|
+
{
|
|
113
|
+
'cursor-not-allowed border-gray-200 dark:border-zinc-800 text-gray-100 dark:text-zinc-500':
|
|
114
|
+
disabled,
|
|
115
|
+
'text-gray data-[highlighted]:bg-gray-50 text-gray-500 dark:text-zinc-100 dark:data-[highlighted]:bg-zinc-800':
|
|
116
|
+
!disabled && variant === 'default',
|
|
117
|
+
'text-danger dark:text-red-300 data-[highlighted]:bg-red-100 dark:data-[highlighted]:bg-danger-darken dark:data-[highlighted]:text-red-100':
|
|
118
|
+
!disabled && variant === 'danger',
|
|
119
|
+
},
|
|
120
|
+
'font-body',
|
|
121
|
+
className,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if (context) {
|
|
125
|
+
return (
|
|
126
|
+
<ContextMenu.Item
|
|
127
|
+
{...props}
|
|
128
|
+
onClick={handleClick}
|
|
129
|
+
disabled={disabled}
|
|
130
|
+
ref={setRefs}
|
|
131
|
+
className={itemClassName}
|
|
132
|
+
>
|
|
133
|
+
{children}
|
|
134
|
+
</ContextMenu.Item>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<BaseMenu.Item
|
|
140
|
+
{...props}
|
|
141
|
+
onClick={handleClick}
|
|
142
|
+
disabled={disabled}
|
|
143
|
+
ref={ref}
|
|
144
|
+
className={itemClassName}
|
|
145
|
+
>
|
|
146
|
+
{children}
|
|
147
|
+
</BaseMenu.Item>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React, { FC, ReactNode, createContext, useContext, useMemo } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface MenuContextInterface {
|
|
4
|
+
context: boolean
|
|
5
|
+
submenu: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const MenuContext = createContext<Partial<MenuContextInterface>>({} as MenuContextInterface)
|
|
9
|
+
|
|
10
|
+
interface ProviderProps {
|
|
11
|
+
context: boolean
|
|
12
|
+
submenu?: boolean
|
|
13
|
+
children: ReactNode
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const MenuProvider: FC<ProviderProps> = ({ context = false, submenu = false, children }) => {
|
|
17
|
+
const value = useMemo(() => ({ context, submenu }), [context, submenu])
|
|
18
|
+
|
|
19
|
+
return <MenuContext.Provider value={value}>{children}</MenuContext.Provider>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const useMenu = () => useContext(MenuContext)
|
|
23
|
+
const MenuConsumer = MenuContext.Consumer
|
|
24
|
+
export { useMenu, MenuProvider, MenuContext, MenuConsumer }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Menu as BaseMenu, ContextMenu, MenuRoot } from '@base-ui/react'
|
|
3
|
+
import { MenuProvider } from './menu-context'
|
|
4
|
+
|
|
5
|
+
export type MenuProps =
|
|
6
|
+
| (MenuRoot.Props & { context?: false })
|
|
7
|
+
| (ContextMenu.Root.Props & { context: true })
|
|
8
|
+
|
|
9
|
+
export const Menu = ({ context = false, ...props }: MenuProps) => (
|
|
10
|
+
<MenuProvider context={context}>
|
|
11
|
+
{!context && <BaseMenu.Root {...(props as MenuRoot.Props)} />}
|
|
12
|
+
{context && <ContextMenu.Root {...(props as ContextMenu.Root.Props)} />}
|
|
13
|
+
</MenuProvider>
|
|
14
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Menu as BaseMenu, ContextMenu, MenuPopupProps, MenuPositionerProps } from '@base-ui/react'
|
|
3
|
+
import { cn } from '../helpers'
|
|
4
|
+
import { useMenu } from './menu-context'
|
|
5
|
+
|
|
6
|
+
export type MenuPopoverProps = MenuPopupProps & Omit<MenuPositionerProps, 'children'>
|
|
7
|
+
|
|
8
|
+
export const Popover = ({
|
|
9
|
+
className,
|
|
10
|
+
anchor,
|
|
11
|
+
positionMethod,
|
|
12
|
+
side,
|
|
13
|
+
sideOffset,
|
|
14
|
+
align,
|
|
15
|
+
alignOffset,
|
|
16
|
+
collisionBoundary,
|
|
17
|
+
collisionPadding,
|
|
18
|
+
sticky,
|
|
19
|
+
arrowPadding,
|
|
20
|
+
disableAnchorTracking,
|
|
21
|
+
collisionAvoidance,
|
|
22
|
+
children,
|
|
23
|
+
...props
|
|
24
|
+
}: MenuPopoverProps) => {
|
|
25
|
+
const { context, submenu } = useMenu()
|
|
26
|
+
|
|
27
|
+
const positionerProps = {
|
|
28
|
+
className: 'outline-none z-50',
|
|
29
|
+
side: side || (context || submenu ? 'right' : 'bottom'),
|
|
30
|
+
align: align || (context || submenu ? 'start' : 'end'),
|
|
31
|
+
anchor,
|
|
32
|
+
positionMethod,
|
|
33
|
+
sideOffset,
|
|
34
|
+
alignOffset,
|
|
35
|
+
collisionBoundary,
|
|
36
|
+
collisionPadding,
|
|
37
|
+
sticky,
|
|
38
|
+
arrowPadding,
|
|
39
|
+
disableAnchorTracking,
|
|
40
|
+
collisionAvoidance,
|
|
41
|
+
} as const
|
|
42
|
+
|
|
43
|
+
const popupClassName = cn(
|
|
44
|
+
'outline-none border border-gray-100 dark:border-zinc-800 rounded bg-white dark:bg-zinc-900 overflow-hidden font-body',
|
|
45
|
+
className,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if (context) {
|
|
49
|
+
return (
|
|
50
|
+
<ContextMenu.Portal>
|
|
51
|
+
<ContextMenu.Positioner {...positionerProps}>
|
|
52
|
+
<ContextMenu.Popup className={popupClassName} {...props}>
|
|
53
|
+
{children}
|
|
54
|
+
</ContextMenu.Popup>
|
|
55
|
+
</ContextMenu.Positioner>
|
|
56
|
+
</ContextMenu.Portal>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<BaseMenu.Portal>
|
|
62
|
+
<BaseMenu.Positioner {...positionerProps}>
|
|
63
|
+
<BaseMenu.Popup className={popupClassName} {...props}>
|
|
64
|
+
{children}
|
|
65
|
+
</BaseMenu.Popup>
|
|
66
|
+
</BaseMenu.Positioner>
|
|
67
|
+
</BaseMenu.Portal>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React, { RefAttributes } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Menu as BaseMenu,
|
|
4
|
+
MenuSubmenuTriggerProps as BaseMenuSubmenuTriggerProps,
|
|
5
|
+
ContextMenu,
|
|
6
|
+
} from '@base-ui/react'
|
|
7
|
+
import { cn } from '../helpers'
|
|
8
|
+
import { CaretRightIcon } from '@phosphor-icons/react'
|
|
9
|
+
import { useMenu } from './menu-context'
|
|
10
|
+
|
|
11
|
+
export type MenuSubmenuTriggerProps = BaseMenuSubmenuTriggerProps & RefAttributes<HTMLElement>
|
|
12
|
+
|
|
13
|
+
export const SubmenuTrigger = ({ className, children, ...props }: MenuSubmenuTriggerProps) => {
|
|
14
|
+
const { context } = useMenu()
|
|
15
|
+
const submenuTriggerClass = cn(
|
|
16
|
+
'group flex w-full items-center p-2 py-1 text-left whitespace-nowrap',
|
|
17
|
+
'text-xs font-medium outline-none text-gray dark:text-zinc-200 data-[highlighted]:bg-gray-50 dark:data-[highlighted]:bg-zinc-800',
|
|
18
|
+
'font-body',
|
|
19
|
+
className,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if (context) {
|
|
23
|
+
return (
|
|
24
|
+
<ContextMenu.SubmenuTrigger
|
|
25
|
+
className={submenuTriggerClass}
|
|
26
|
+
{...(props as MenuSubmenuTriggerProps)}
|
|
27
|
+
>
|
|
28
|
+
<div className="flex-1">{children}</div>
|
|
29
|
+
<CaretRightIcon className="size-4" />
|
|
30
|
+
</ContextMenu.SubmenuTrigger>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<BaseMenu.SubmenuTrigger
|
|
36
|
+
nativeButton={false}
|
|
37
|
+
render={(renderProps) => (
|
|
38
|
+
<div {...renderProps} tabIndex={-1} className={submenuTriggerClass}>
|
|
39
|
+
<div className="flex-1">{renderProps.children}</div>
|
|
40
|
+
<CaretRightIcon className="size-4" />
|
|
41
|
+
</div>
|
|
42
|
+
)}
|
|
43
|
+
{...(props as MenuSubmenuTriggerProps)}
|
|
44
|
+
>
|
|
45
|
+
{children}
|
|
46
|
+
</BaseMenu.SubmenuTrigger>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Menu as BaseMenu,
|
|
4
|
+
MenuSubmenuRootProps as BaseMenuSubmenuRootProps,
|
|
5
|
+
ContextMenu,
|
|
6
|
+
} from '@base-ui/react'
|
|
7
|
+
import { MenuProvider, useMenu } from './menu-context'
|
|
8
|
+
|
|
9
|
+
export type MenuSubmenuRootProps = BaseMenuSubmenuRootProps
|
|
10
|
+
|
|
11
|
+
export const Submenu = ({ ...props }: MenuSubmenuRootProps) => {
|
|
12
|
+
const { context } = useMenu()
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<MenuProvider context={context ?? false} submenu={true}>
|
|
16
|
+
{!context && <ContextMenu.SubmenuRoot {...props} />}
|
|
17
|
+
{context && <BaseMenu.SubmenuRoot {...props} />}
|
|
18
|
+
</MenuProvider>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React, { RefAttributes } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Menu as BaseMenu,
|
|
4
|
+
MenuTriggerProps as BaseMenuTriggerProps,
|
|
5
|
+
ContextMenu,
|
|
6
|
+
ContextMenuTriggerProps,
|
|
7
|
+
} from '@base-ui/react'
|
|
8
|
+
import { cn } from '../helpers'
|
|
9
|
+
import { useMenu } from './menu-context'
|
|
10
|
+
|
|
11
|
+
export type MenuTriggerProps =
|
|
12
|
+
| (BaseMenuTriggerProps & RefAttributes<HTMLElement>)
|
|
13
|
+
| (ContextMenuTriggerProps & RefAttributes<HTMLDivElement>)
|
|
14
|
+
|
|
15
|
+
export const Trigger = ({ className, children, ...props }: MenuTriggerProps) => {
|
|
16
|
+
const { context } = useMenu()
|
|
17
|
+
|
|
18
|
+
if (context) {
|
|
19
|
+
return (
|
|
20
|
+
<ContextMenu.Trigger
|
|
21
|
+
className={cn('inline-block outline-none', className)}
|
|
22
|
+
{...(props as ContextMenuTriggerProps)}
|
|
23
|
+
>
|
|
24
|
+
{children}
|
|
25
|
+
</ContextMenu.Trigger>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<BaseMenu.Trigger
|
|
31
|
+
nativeButton={false}
|
|
32
|
+
onClick={(e) => e.stopPropagation()}
|
|
33
|
+
render={(renderProps) => (
|
|
34
|
+
<div {...renderProps} tabIndex={-1} className={cn('inline-block outline-none', className)}>
|
|
35
|
+
{renderProps.children}
|
|
36
|
+
</div>
|
|
37
|
+
)}
|
|
38
|
+
{...(props as BaseMenuTriggerProps)}
|
|
39
|
+
>
|
|
40
|
+
{children}
|
|
41
|
+
</BaseMenu.Trigger>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { List } from './list'
|
|
2
|
+
import { NotificationManager, Provider, useNotificationManager } from './provider'
|
|
3
|
+
|
|
4
|
+
type NotificationCompoundType = {
|
|
5
|
+
List: typeof List
|
|
6
|
+
Provider: typeof Provider
|
|
7
|
+
useNotificationManager: typeof useNotificationManager
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Notification: NotificationCompoundType = Object.assign({
|
|
11
|
+
List,
|
|
12
|
+
Provider,
|
|
13
|
+
useNotificationManager,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export type { NotificationManager }
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Toast, ToastObject } from '@base-ui/react'
|
|
3
|
+
import { cn } from '../helpers'
|
|
4
|
+
import { IconButton } from '../icon-button'
|
|
5
|
+
import { XIcon } from '@phosphor-icons/react'
|
|
6
|
+
import { useNotificationToastManager } from './provider'
|
|
7
|
+
|
|
8
|
+
export type NotificationVariant = 'info' | 'warning' | 'danger' | 'success'
|
|
9
|
+
|
|
10
|
+
interface NotificationData {
|
|
11
|
+
type: 'notification'
|
|
12
|
+
variant?: NotificationVariant
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const List = () => {
|
|
16
|
+
// Use the separate notification context - all toasts here are notifications
|
|
17
|
+
const { toasts: notifications } = useNotificationToastManager()
|
|
18
|
+
|
|
19
|
+
const getVariant = (toast: ToastObject<any>): NotificationVariant => {
|
|
20
|
+
const data = toast.data as NotificationData | undefined
|
|
21
|
+
return data?.variant ?? 'info'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Toast.Portal>
|
|
26
|
+
<Toast.Viewport
|
|
27
|
+
className={cn(
|
|
28
|
+
'font-body text-base font-medium tracking-normal fixed left-1/2 top-5 z-[1100] -translate-x-1/2 transform',
|
|
29
|
+
{
|
|
30
|
+
'flex w-full flex-col px-4 pb-8 sm:w-[512px] sm:px-0': notifications.length,
|
|
31
|
+
'pointer-events-none': !notifications.length,
|
|
32
|
+
},
|
|
33
|
+
)}
|
|
34
|
+
>
|
|
35
|
+
{notifications.map((toast) => {
|
|
36
|
+
const variant = getVariant(toast)
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Toast.Root
|
|
40
|
+
key={toast.id}
|
|
41
|
+
toast={toast}
|
|
42
|
+
swipeDirection={['up', 'left', 'right']}
|
|
43
|
+
tabIndex={-1}
|
|
44
|
+
className={cn(
|
|
45
|
+
// Base styles
|
|
46
|
+
'hide-scrollbar outline-none relative flex max-h-[140px] w-full max-w-lg items-start gap-2 self-center overflow-y-scroll rounded-lg p-2 pl-3 shadow-md',
|
|
47
|
+
// Animation: transition properties
|
|
48
|
+
'transition-all duration-300 ease-out',
|
|
49
|
+
// Stacking: absolute positioning to overlap
|
|
50
|
+
'absolute top-0 left-0 right-0',
|
|
51
|
+
// Stacking: z-index based on --toast-index (lower index = front, above dialog z-[1001])
|
|
52
|
+
'[z-index:calc(1100-var(--toast-index))]',
|
|
53
|
+
// Stacking: scale down and offset each toast behind the front one
|
|
54
|
+
'[transform:scale(calc(1-0.05*var(--toast-index)))_translateY(calc(var(--toast-index)*12px))_translateX(var(--toast-swipe-movement-x,0px))]',
|
|
55
|
+
// Stacking: reduce brightness for toasts behind
|
|
56
|
+
'[filter:brightness(calc(1-0.08*var(--toast-index)))]',
|
|
57
|
+
// Expanded state: spread out when hovering the viewport (with 8px gap between each)
|
|
58
|
+
'data-[expanded]:[transform:translateY(calc(var(--toast-offset-y)+var(--toast-index)*8px))_translateX(var(--toast-swipe-movement-x,0px))]',
|
|
59
|
+
'data-[expanded]:[filter:brightness(1)]',
|
|
60
|
+
// Entry animation (starting style)
|
|
61
|
+
'data-[starting-style]:[transform:scale(calc(1-0.05*var(--toast-index)))_translateY(-150%)]',
|
|
62
|
+
'data-[starting-style]:opacity-0',
|
|
63
|
+
// Exit animation (ending style)
|
|
64
|
+
'data-[ending-style]:opacity-0',
|
|
65
|
+
'data-[ending-style]:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y,0px)-150%))]',
|
|
66
|
+
'data-[ending-style]:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y,0px)+150%))]',
|
|
67
|
+
'data-[ending-style]:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x,0px)-150%))]',
|
|
68
|
+
'data-[ending-style]:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x,0px)+150%))]',
|
|
69
|
+
// Variant colors
|
|
70
|
+
{
|
|
71
|
+
'bg-gray-500 text-white dark:bg-zinc-900 dark:text-zinc-100 dark:ring-1 dark:ring-zinc-800':
|
|
72
|
+
variant === 'info',
|
|
73
|
+
'bg-warning text-gray dark:text-zinc-950': variant === 'warning',
|
|
74
|
+
'bg-danger text-white dark:text-zinc-50': variant === 'danger',
|
|
75
|
+
'bg-success text-white dark:text-zinc-50': variant === 'success',
|
|
76
|
+
},
|
|
77
|
+
)}
|
|
78
|
+
>
|
|
79
|
+
<Toast.Content className="mr-8 flex flex-1 flex-col whitespace-pre-line break-words overflow-hidden transition-opacity duration-300 data-[behind]:opacity-0 data-[expanded]:opacity-100">
|
|
80
|
+
<Toast.Description />
|
|
81
|
+
<Toast.Action
|
|
82
|
+
className={cn(
|
|
83
|
+
'mt-1 inline-block w-fit rounded-md px-0 py-0 text-sm font-semibold underline underline-offset-2 transition-all duration-200 text-left',
|
|
84
|
+
'hover:no-underline focus-visible:no-underline outline-none',
|
|
85
|
+
{
|
|
86
|
+
'text-gray hover:text-black focus-visible:text-black dark:text-zinc-950 dark:hover:text-black dark:focus-visible:text-black':
|
|
87
|
+
variant === 'warning',
|
|
88
|
+
'text-white/80 hover:text-white focus-visible:text-white dark:text-zinc-50/80 dark:hover:text-zinc-50 dark:focus-visible:text-zinc-50':
|
|
89
|
+
variant === 'danger' || variant === 'success' || variant === 'info',
|
|
90
|
+
},
|
|
91
|
+
)}
|
|
92
|
+
/>
|
|
93
|
+
</Toast.Content>
|
|
94
|
+
<Toast.Close
|
|
95
|
+
aria-label="Close"
|
|
96
|
+
nativeButton={true}
|
|
97
|
+
render={(renderProps) => {
|
|
98
|
+
return (
|
|
99
|
+
<div className="absolute right-2 top-2">
|
|
100
|
+
<IconButton
|
|
101
|
+
size="lg"
|
|
102
|
+
className="[&_svg]:size-5 rounded active:bg-black/10 hover:bg-black/10 focus-visible:bg-black/10 focus-visible:ring-0 dark:active:bg-black/20 dark:hover:bg-black/20 dark:focus-visible:bg-black/20"
|
|
103
|
+
{...renderProps}
|
|
104
|
+
>
|
|
105
|
+
<XIcon
|
|
106
|
+
className={cn({
|
|
107
|
+
'text-gray dark:text-zinc-950': variant === 'warning',
|
|
108
|
+
'text-white dark:text-zinc-50':
|
|
109
|
+
variant === 'danger' || variant === 'success' || variant === 'info',
|
|
110
|
+
})}
|
|
111
|
+
/>
|
|
112
|
+
</IconButton>
|
|
113
|
+
</div>
|
|
114
|
+
)
|
|
115
|
+
}}
|
|
116
|
+
/>
|
|
117
|
+
</Toast.Root>
|
|
118
|
+
)
|
|
119
|
+
})}
|
|
120
|
+
</Toast.Viewport>
|
|
121
|
+
</Toast.Portal>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React, { ReactNode, createContext, useContext, useMemo } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Toast as BaseToast,
|
|
4
|
+
ToastProviderProps as BaseToastProviderProps,
|
|
5
|
+
UseToastManagerReturnValue,
|
|
6
|
+
} from '@base-ui/react'
|
|
7
|
+
import { NotificationVariant } from './list'
|
|
8
|
+
|
|
9
|
+
// Create a separate context for the notification manager
|
|
10
|
+
// This allows notifications to have their own independent toast stack
|
|
11
|
+
const NotificationContext = createContext<UseToastManagerReturnValue | null>(null)
|
|
12
|
+
|
|
13
|
+
export type NotificationProviderProps = Omit<BaseToastProviderProps, 'children'> & {
|
|
14
|
+
children?: ReactNode
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const Provider = ({ children, ...props }: NotificationProviderProps) => (
|
|
18
|
+
<BaseToast.Provider {...props}>
|
|
19
|
+
<NotificationContextProvider>{children}</NotificationContextProvider>
|
|
20
|
+
</BaseToast.Provider>
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
// Internal component that captures and provides the toast manager
|
|
24
|
+
const NotificationContextProvider = ({ children }: { children?: ReactNode }) => {
|
|
25
|
+
const toastManager = BaseToast.useToastManager()
|
|
26
|
+
return (
|
|
27
|
+
<NotificationContext.Provider value={toastManager}>{children}</NotificationContext.Provider>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type NotificationManager = {
|
|
32
|
+
toasts: UseToastManagerReturnValue['toasts']
|
|
33
|
+
add: <Data extends object>(
|
|
34
|
+
options: Parameters<UseToastManagerReturnValue['add']>[0] & {
|
|
35
|
+
variant: NotificationVariant
|
|
36
|
+
} & { data?: Data },
|
|
37
|
+
) => string
|
|
38
|
+
close: UseToastManagerReturnValue['close']
|
|
39
|
+
update: UseToastManagerReturnValue['update']
|
|
40
|
+
promise: UseToastManagerReturnValue['promise']
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const useNotificationManager = (): NotificationManager => {
|
|
44
|
+
const toastManager = useContext(NotificationContext)
|
|
45
|
+
|
|
46
|
+
if (!toastManager) {
|
|
47
|
+
throw new Error('useNotificationManager must be used within a Notification.Provider')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return useMemo(
|
|
51
|
+
() => ({
|
|
52
|
+
toasts: toastManager.toasts,
|
|
53
|
+
close: toastManager.close,
|
|
54
|
+
update: toastManager.update,
|
|
55
|
+
promise: toastManager.promise,
|
|
56
|
+
add: ({ variant = 'info', ...props }) => {
|
|
57
|
+
return toastManager.add({
|
|
58
|
+
...props,
|
|
59
|
+
data: { ...props.data, variant, type: 'notification' },
|
|
60
|
+
})
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
[toastManager],
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const useNotificationToastManager = (): UseToastManagerReturnValue => {
|
|
68
|
+
const toastManager = useContext(NotificationContext)
|
|
69
|
+
|
|
70
|
+
if (!toastManager) {
|
|
71
|
+
throw new Error('useNotificationToastManager must be used within a Notification.Provider')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return toastManager
|
|
75
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface PageHeaderProps {
|
|
4
|
+
title: string
|
|
5
|
+
children?: ReactNode
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** The standard heading row for a Toolpath application page. */
|
|
9
|
+
export const PageHeader = ({ title, children }: PageHeaderProps) => (
|
|
10
|
+
<div className="mb-4 flex items-center justify-between">
|
|
11
|
+
<h1 className="text-base font-bold text-gray-900 dark:text-white">{title}</h1>
|
|
12
|
+
{children}
|
|
13
|
+
</div>
|
|
14
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './pagination'
|