@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.
Files changed (146) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +44 -0
  3. package/dist/index.d.ts +1174 -0
  4. package/dist/index.js +5934 -0
  5. package/package.json +68 -0
  6. package/src/attribution/action.tsx +13 -0
  7. package/src/attribution/at.tsx +28 -0
  8. package/src/attribution/attribution.tsx +16 -0
  9. package/src/attribution/by.tsx +21 -0
  10. package/src/attribution/format-time.ts +48 -0
  11. package/src/attribution/index.ts +20 -0
  12. package/src/badge/badge.tsx +38 -0
  13. package/src/badge/index.ts +1 -0
  14. package/src/breadcrumbs/breadcrumbs-context.tsx +26 -0
  15. package/src/breadcrumbs/breadcrumbs.tsx +24 -0
  16. package/src/breadcrumbs/index.ts +12 -0
  17. package/src/breadcrumbs/item.tsx +85 -0
  18. package/src/button/animated-ellipsis.tsx +44 -0
  19. package/src/button/button.tsx +168 -0
  20. package/src/button/index.ts +1 -0
  21. package/src/callout/callout.tsx +202 -0
  22. package/src/callout/index.ts +2 -0
  23. package/src/callout/use-dismissed-callouts.ts +78 -0
  24. package/src/card/card.tsx +19 -0
  25. package/src/card/index.ts +2 -0
  26. package/src/checkbox/checkbox.tsx +76 -0
  27. package/src/checkbox/index.ts +1 -0
  28. package/src/combobox/button.tsx +99 -0
  29. package/src/combobox/combobox-context.tsx +42 -0
  30. package/src/combobox/combobox.tsx +60 -0
  31. package/src/combobox/empty.tsx +21 -0
  32. package/src/combobox/group-label.tsx +20 -0
  33. package/src/combobox/group.tsx +11 -0
  34. package/src/combobox/icon.tsx +11 -0
  35. package/src/combobox/index.tsx +64 -0
  36. package/src/combobox/input.tsx +77 -0
  37. package/src/combobox/item-indicator.tsx +16 -0
  38. package/src/combobox/item.tsx +58 -0
  39. package/src/combobox/list.tsx +19 -0
  40. package/src/combobox/popover.tsx +62 -0
  41. package/src/combobox/trigger.tsx +33 -0
  42. package/src/combobox/value.tsx +12 -0
  43. package/src/common/index.ts +1 -0
  44. package/src/common/svg.tsx +17 -0
  45. package/src/dialog/actions.tsx +17 -0
  46. package/src/dialog/confirm-button.tsx +25 -0
  47. package/src/dialog/dialog-context.tsx +21 -0
  48. package/src/dialog/dialog.tsx +37 -0
  49. package/src/dialog/dismiss-button.tsx +29 -0
  50. package/src/dialog/index.ts +37 -0
  51. package/src/dialog/message.tsx +20 -0
  52. package/src/dialog/provider.tsx +102 -0
  53. package/src/dialog/title.tsx +20 -0
  54. package/src/editable-cell/editable-cell.tsx +307 -0
  55. package/src/editable-cell/index.ts +7 -0
  56. package/src/editable-cell/use-editable-grid.tsx +296 -0
  57. package/src/field/field.tsx +22 -0
  58. package/src/field/index.ts +2 -0
  59. package/src/helpers/cn.ts +6 -0
  60. package/src/helpers/index.ts +1 -0
  61. package/src/icon-button/icon-button.tsx +126 -0
  62. package/src/icon-button/index.ts +2 -0
  63. package/src/index.ts +32 -0
  64. package/src/input/index.ts +1 -0
  65. package/src/input/input.tsx +276 -0
  66. package/src/link/index.ts +4 -0
  67. package/src/link/link-context.tsx +33 -0
  68. package/src/link/link.tsx +24 -0
  69. package/src/menu/divider.tsx +17 -0
  70. package/src/menu/index.ts +40 -0
  71. package/src/menu/item.tsx +149 -0
  72. package/src/menu/menu-context.tsx +24 -0
  73. package/src/menu/menu.tsx +14 -0
  74. package/src/menu/popover.tsx +69 -0
  75. package/src/menu/submenu-trigger.tsx +48 -0
  76. package/src/menu/submenu.tsx +20 -0
  77. package/src/menu/trigger.tsx +43 -0
  78. package/src/notification/index.ts +16 -0
  79. package/src/notification/list.tsx +123 -0
  80. package/src/notification/provider.tsx +75 -0
  81. package/src/page-header/index.ts +2 -0
  82. package/src/page-header/page-header.tsx +14 -0
  83. package/src/pagination/index.ts +1 -0
  84. package/src/pagination/pagination.tsx +136 -0
  85. package/src/panels/collapsed.tsx +32 -0
  86. package/src/panels/group.tsx +27 -0
  87. package/src/panels/index.ts +57 -0
  88. package/src/panels/layout-context.ts +5 -0
  89. package/src/panels/orientation-context.tsx +5 -0
  90. package/src/panels/panel.tsx +95 -0
  91. package/src/panels/separator.tsx +44 -0
  92. package/src/pin/index.ts +1 -0
  93. package/src/pin/pin.tsx +99 -0
  94. package/src/progress-bar/index.ts +1 -0
  95. package/src/progress-bar/progress-bar.tsx +48 -0
  96. package/src/quantity-input/index.ts +1 -0
  97. package/src/quantity-input/quantity-input.tsx +158 -0
  98. package/src/scroll-area/index.ts +1 -0
  99. package/src/scroll-area/scroll-area.tsx +82 -0
  100. package/src/slider/index.ts +1 -0
  101. package/src/slider/slider.tsx +75 -0
  102. package/src/star/index.ts +1 -0
  103. package/src/star/star.tsx +99 -0
  104. package/src/switch/index.ts +1 -0
  105. package/src/switch/switch.tsx +33 -0
  106. package/src/table/body.tsx +15 -0
  107. package/src/table/cell-context.tsx +48 -0
  108. package/src/table/cell.tsx +83 -0
  109. package/src/table/group-empty-state.tsx +28 -0
  110. package/src/table/group-header.tsx +62 -0
  111. package/src/table/header-cell.tsx +158 -0
  112. package/src/table/header-row.tsx +28 -0
  113. package/src/table/index.ts +56 -0
  114. package/src/table/is-group-empty-state.ts +17 -0
  115. package/src/table/is-group-header.ts +5 -0
  116. package/src/table/row-context.tsx +61 -0
  117. package/src/table/row-disabled-context.tsx +7 -0
  118. package/src/table/row.tsx +98 -0
  119. package/src/table/table-context.tsx +127 -0
  120. package/src/table/table-empty.tsx +57 -0
  121. package/src/table/table.tsx +547 -0
  122. package/src/table/tree-icon.tsx +147 -0
  123. package/src/table/use-checkbox.ts +200 -0
  124. package/src/table/use-column-layout.ts +117 -0
  125. package/src/table/use-groups.ts +134 -0
  126. package/src/table/use-keyboard-navigation.ts +274 -0
  127. package/src/table/use-sort-functions.ts +90 -0
  128. package/src/tabs/index.ts +20 -0
  129. package/src/tabs/list.tsx +29 -0
  130. package/src/tabs/tab.tsx +141 -0
  131. package/src/tabs/tabs-context.tsx +38 -0
  132. package/src/tabs/tabs.tsx +27 -0
  133. package/src/textarea/index.ts +1 -0
  134. package/src/textarea/textarea.tsx +88 -0
  135. package/src/toast/index.ts +19 -0
  136. package/src/toast/list.tsx +133 -0
  137. package/src/toast/provider.tsx +36 -0
  138. package/src/toggle/index.ts +17 -0
  139. package/src/toggle/indicator.tsx +27 -0
  140. package/src/toggle/item.tsx +54 -0
  141. package/src/toggle/toggle-context.tsx +68 -0
  142. package/src/toggle/toggle.tsx +124 -0
  143. package/src/toggle/use-indicator.tsx +69 -0
  144. package/src/tooltip/index.ts +1 -0
  145. package/src/tooltip/tooltip.tsx +122 -0
  146. 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,2 @@
1
+ export { PageHeader } from './page-header'
2
+ export type { PageHeaderProps } from './page-header'
@@ -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'