@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,64 @@
1
+ import { ReactElement } from 'react'
2
+ import { Button, ComboboxButtonProps } from './button'
3
+ import { ComboboxProps, Combobox as ComboboxRoot } from './combobox'
4
+ import { useCombobox } from './combobox-context'
5
+ import { ComboboxEmptyProps, Empty } from './empty'
6
+ import { ComboboxGroupProps, Group } from './group'
7
+ import { ComboboxGroupLabelProps, GroupLabel } from './group-label'
8
+ import { ComboboxIconProps, Icon } from './icon'
9
+ import { ComboboxInputProps, Input } from './input'
10
+ import { ComboboxItemProps, Item } from './item'
11
+ import { ComboboxItemIndicatorProps, ItemIndicator } from './item-indicator'
12
+ import { ComboboxListProps, List } from './list'
13
+ import { ComboboxPopoverProps, Popover } from './popover'
14
+ import { ComboboxTriggerProps, Trigger } from './trigger'
15
+ import { ComboboxValueProps, Value } from './value'
16
+
17
+ type ComboboxCompoundType = {
18
+ <T>(props: ComboboxProps<T>): ReactElement
19
+ Empty: typeof Empty
20
+ Group: typeof Group
21
+ GroupLabel: typeof GroupLabel
22
+ Input: typeof Input
23
+ Item: typeof Item
24
+ ItemIndicator: typeof ItemIndicator
25
+ List: typeof List
26
+ Popover: typeof Popover
27
+ Trigger: typeof Trigger
28
+ Button: typeof Button
29
+ Value: typeof Value
30
+ Icon: typeof Icon
31
+ useCombobox: typeof useCombobox
32
+ }
33
+
34
+ export const Combobox: ComboboxCompoundType = Object.assign(ComboboxRoot, {
35
+ Empty,
36
+ Group,
37
+ GroupLabel,
38
+ Input,
39
+ Item,
40
+ ItemIndicator,
41
+ List,
42
+ Popover,
43
+ Trigger,
44
+ Button,
45
+ Value,
46
+ Icon,
47
+ useCombobox,
48
+ })
49
+
50
+ export type {
51
+ ComboboxProps,
52
+ ComboboxEmptyProps,
53
+ ComboboxGroupProps,
54
+ ComboboxGroupLabelProps,
55
+ ComboboxInputProps,
56
+ ComboboxItemProps,
57
+ ComboboxItemIndicatorProps,
58
+ ComboboxListProps,
59
+ ComboboxPopoverProps,
60
+ ComboboxTriggerProps,
61
+ ComboboxValueProps,
62
+ ComboboxIconProps,
63
+ ComboboxButtonProps,
64
+ }
@@ -0,0 +1,77 @@
1
+ import React, { useCallback, useRef } from 'react'
2
+ import {
3
+ Combobox as BaseCombobox,
4
+ ComboboxInputProps as BaseComboboxInputProps,
5
+ } from '@base-ui/react'
6
+ import { cn } from '../helpers'
7
+ import { useCombobox } from './combobox-context'
8
+
9
+ export type ComboboxInputProps = BaseComboboxInputProps
10
+
11
+ export const Input = ({ className, ...props }: ComboboxInputProps) => {
12
+ const { size } = useCombobox()
13
+ const observerRef = useRef<MutationObserver | null>(null)
14
+ const previouslyFocusedRef = useRef<HTMLElement | null>(null)
15
+
16
+ const inputRefCallback = useCallback((node: HTMLInputElement | null) => {
17
+ // Clean up previous observer
18
+ if (observerRef.current) {
19
+ observerRef.current.disconnect()
20
+ observerRef.current = null
21
+ }
22
+
23
+ if (!node) {
24
+ return
25
+ }
26
+
27
+ // Focus immediately if already open
28
+ if (node.hasAttribute('data-popup-open')) {
29
+ previouslyFocusedRef.current = document.activeElement as HTMLElement
30
+ node.focus()
31
+ }
32
+
33
+ // Watch for attribute changes to detect when popup opens/closes
34
+ observerRef.current = new MutationObserver((mutations) => {
35
+ for (const mutation of mutations) {
36
+ if (mutation.type === 'attributes' && mutation.attributeName === 'data-popup-open') {
37
+ if (node.hasAttribute('data-popup-open')) {
38
+ // Popup opened - store current focus and focus input
39
+ previouslyFocusedRef.current = document.activeElement as HTMLElement
40
+ node.focus()
41
+ } else {
42
+ // Popup closed - restore focus to previous element
43
+ previouslyFocusedRef.current?.focus()
44
+ previouslyFocusedRef.current = null
45
+ }
46
+ }
47
+ }
48
+ })
49
+
50
+ observerRef.current.observe(node, { attributes: true })
51
+ }, [])
52
+
53
+ return (
54
+ <BaseCombobox.Input
55
+ ref={inputRefCallback}
56
+ data-input
57
+ className={cn(
58
+ 'w-full rounded border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 font-body font-medium text-gray-400 dark:text-zinc-100 outline-none',
59
+ 'placeholder:text-gray-200 dark:placeholder:text-zinc-300 px-1.5',
60
+ 'focus-visible:border-info focus-visible:ring-2 focus-visible:ring-info/60',
61
+ 'hidden data-[popup-open]:block',
62
+ 'data-[popup-open]:ring-0 data-[popup-open]:border-gray-200 dark:data-[popup-open]:border-zinc-500',
63
+ 'data-[popup-open]:data-[popup-side="top"]:rounded-t-none',
64
+ 'data-[popup-open]:data-[popup-side="bottom"]:rounded-b-none',
65
+ 'disabled:bg-gray-50 disabled:text-gray-200 dark:disabled:bg-zinc-950 dark:disabled:text-zinc-400',
66
+ {
67
+ 'h-5 text-2xs': size === 'sm',
68
+ 'h-6 text-xs': size === 'md',
69
+ 'h-7 text-xs': size === 'lg',
70
+ 'h-9 text-base': size === 'xl',
71
+ },
72
+ className,
73
+ )}
74
+ {...props}
75
+ />
76
+ )
77
+ }
@@ -0,0 +1,16 @@
1
+ import React, { HTMLAttributes } from 'react'
2
+ import {
3
+ Combobox as BaseCombobox,
4
+ ComboboxItemIndicatorProps as BaseComboboxItemIndicatorProps,
5
+ } from '@base-ui/react'
6
+
7
+ export type ComboboxItemIndicatorProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children'> &
8
+ BaseComboboxItemIndicatorProps
9
+
10
+ export const ItemIndicator = ({ children, ...props }: ComboboxItemIndicatorProps) => {
11
+ return (
12
+ <BaseCombobox.ItemIndicator {...(props as BaseComboboxItemIndicatorProps)}>
13
+ {children}
14
+ </BaseCombobox.ItemIndicator>
15
+ )
16
+ }
@@ -0,0 +1,58 @@
1
+ import React, { useEffect, useRef, useState } from 'react'
2
+ import {
3
+ Combobox as BaseCombobox,
4
+ ComboboxItemProps as BaseComboboxItemProps,
5
+ } from '@base-ui/react'
6
+ import { useCombobox } from './combobox-context'
7
+ import { cn } from '../helpers'
8
+ import { ArrowBendUpLeftIcon } from '@phosphor-icons/react'
9
+ import { Tooltip } from '../tooltip/tooltip'
10
+
11
+ export type ComboboxItemProps = BaseComboboxItemProps
12
+
13
+ export const Item = ({ className, children, value, ...props }: ComboboxItemProps) => {
14
+ const { itemToStringLabel } = useCombobox()
15
+ const ref = useRef<HTMLDivElement>(null)
16
+ const [isTruncated, setIsTruncated] = useState(false)
17
+
18
+ useEffect(() => {
19
+ const el = ref.current
20
+ if (!el) {
21
+ return
22
+ }
23
+ const check = () => {
24
+ const nodes = [el, ...el.querySelectorAll<HTMLElement>('*')]
25
+ setIsTruncated(nodes.some((node) => node.scrollWidth > node.clientWidth))
26
+ }
27
+ check()
28
+ const observer = new ResizeObserver(check)
29
+ observer.observe(el)
30
+ return () => observer.disconnect()
31
+ }, [children])
32
+
33
+ const label = typeof value === 'string' ? value : (itemToStringLabel?.(value) ?? null)
34
+ const tip = isTruncated ? label : null
35
+
36
+ return (
37
+ <BaseCombobox.Item
38
+ className={cn(
39
+ 'group flex w-full items-center px-2 py-1 text-left gap-1 text-xs',
40
+ 'font-body font-medium text-gray-400 dark:text-zinc-300 outline-none',
41
+ 'hover:bg-gray-50 hover:text-gray-500 dark:hover:bg-zinc-800 dark:hover:text-zinc-100',
42
+ 'data-[highlighted]:bg-gray-50 data-[highlighted]:text-gray-500 dark:data-[highlighted]:bg-zinc-900 dark:data-[highlighted]:text-zinc-100',
43
+ 'data-[selected]:text-gray-500 dark:data-[selected]:text-zinc-500 dark:data-[selected]:text-white cursor-pointer',
44
+ 'data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed',
45
+ className,
46
+ )}
47
+ value={value}
48
+ {...props}
49
+ >
50
+ <div ref={ref} className="flex-1 truncate">
51
+ <Tooltip tip={tip} side="right" sideOffset={12} delay={500}>
52
+ <>{children}</>
53
+ </Tooltip>
54
+ </div>
55
+ <ArrowBendUpLeftIcon className="size-4 text-gray-300 dark:text-zinc-400 hidden group-data-[highlighted]:block group-hover:hidden group-data-[highlighted]:group-hover:hidden group-data-[selected]:hidden" />
56
+ </BaseCombobox.Item>
57
+ )
58
+ }
@@ -0,0 +1,19 @@
1
+ import React from 'react'
2
+ import {
3
+ Combobox as BaseCombobox,
4
+ ComboboxListProps as BaseComboboxListProps,
5
+ } from '@base-ui/react'
6
+ import { cn } from '../helpers'
7
+
8
+ export type ComboboxListProps = BaseComboboxListProps
9
+
10
+ export const List = ({ className, children, ...props }: ComboboxListProps) => {
11
+ return (
12
+ <BaseCombobox.List
13
+ className={cn('hide-scrollbar max-h-32 overflow-auto outline-none', className)}
14
+ {...props}
15
+ >
16
+ {children}
17
+ </BaseCombobox.List>
18
+ )
19
+ }
@@ -0,0 +1,62 @@
1
+ import React from 'react'
2
+ import {
3
+ Combobox as BaseCombobox,
4
+ ComboboxPopupProps,
5
+ ComboboxPositionerProps,
6
+ } from '@base-ui/react'
7
+ import { cn } from '../helpers'
8
+
9
+ export type ComboboxPopoverProps = ComboboxPopupProps & Omit<ComboboxPositionerProps, 'children'>
10
+
11
+ export const Popover = ({
12
+ className,
13
+ anchor,
14
+ positionMethod,
15
+ side,
16
+ sideOffset,
17
+ align,
18
+ alignOffset,
19
+ collisionBoundary,
20
+ collisionPadding,
21
+ sticky,
22
+ arrowPadding,
23
+ disableAnchorTracking,
24
+ collisionAvoidance,
25
+ children,
26
+ ...props
27
+ }: ComboboxPopoverProps) => {
28
+ const positionerProps = {
29
+ className: 'outline-none z-50',
30
+ side: side || 'bottom',
31
+ align: align || 'start',
32
+ sideOffset: sideOffset ?? -1,
33
+ anchor,
34
+ positionMethod,
35
+ alignOffset,
36
+ collisionBoundary,
37
+ collisionPadding,
38
+ sticky,
39
+ arrowPadding,
40
+ disableAnchorTracking,
41
+ collisionAvoidance,
42
+ } as const
43
+
44
+ const popupClassName = cn(
45
+ 'w-[var(--anchor-width)] font-body divide-gray-100 dark:divide-zinc-800 rounded-b border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 py-1',
46
+ 'transition duration-300 ease-in-out',
47
+ 'focus:outline-none',
48
+ 'data-[side=bottom]:rounded-t-none data-[side=bottom]:rounded-b',
49
+ 'data-[side=top]:rounded-b-none data-[side=top]:rounded-t',
50
+ className,
51
+ )
52
+
53
+ return (
54
+ <BaseCombobox.Portal>
55
+ <BaseCombobox.Positioner {...positionerProps}>
56
+ <BaseCombobox.Popup className={popupClassName} {...props}>
57
+ {children}
58
+ </BaseCombobox.Popup>
59
+ </BaseCombobox.Positioner>
60
+ </BaseCombobox.Portal>
61
+ )
62
+ }
@@ -0,0 +1,33 @@
1
+ import React, { HTMLAttributes, RefAttributes } from 'react'
2
+ import {
3
+ Combobox as BaseCombobox,
4
+ ComboboxTriggerProps as BaseComboboxTriggerProps,
5
+ } from '@base-ui/react'
6
+ import { cn } from '../helpers'
7
+
8
+ export type ComboboxTriggerProps = BaseComboboxTriggerProps & RefAttributes<HTMLElement>
9
+
10
+ export const Trigger = ({
11
+ className,
12
+ children,
13
+ nativeButton = false,
14
+ render,
15
+ ...props
16
+ }: ComboboxTriggerProps) => {
17
+ const defaultRender = (renderProps: HTMLAttributes<HTMLElement>) => (
18
+ <div {...renderProps} tabIndex={-1} className={cn('block outline-none w-full', className)}>
19
+ {renderProps.children}
20
+ </div>
21
+ )
22
+
23
+ return (
24
+ <BaseCombobox.Trigger
25
+ nativeButton={nativeButton}
26
+ render={!nativeButton ? defaultRender : render}
27
+ className={!nativeButton ? '' : className}
28
+ {...(props as BaseComboboxTriggerProps)}
29
+ >
30
+ {children}
31
+ </BaseCombobox.Trigger>
32
+ )
33
+ }
@@ -0,0 +1,12 @@
1
+ import React, { HTMLAttributes } from 'react'
2
+ import {
3
+ Combobox as BaseCombobox,
4
+ ComboboxValueProps as BaseComboboxValueProps,
5
+ } from '@base-ui/react'
6
+
7
+ export type ComboboxValueProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> &
8
+ BaseComboboxValueProps
9
+
10
+ export const Value = ({ children, ...props }: ComboboxValueProps) => {
11
+ return <BaseCombobox.Value {...(props as BaseComboboxValueProps)}>{children}</BaseCombobox.Value>
12
+ }
@@ -0,0 +1 @@
1
+ export { default as Svg } from './svg'
@@ -0,0 +1,17 @@
1
+ import React, { ReactNode, SVGProps } from 'react'
2
+ import { cn } from '../helpers'
3
+
4
+ interface Props extends SVGProps<SVGSVGElement> {
5
+ className?: string
6
+ children: ReactNode
7
+ }
8
+
9
+ function Svg({ children, className = '', ...props }: Props) {
10
+ return (
11
+ <svg className={cn('pointer-events-none fill-current', className)} {...props}>
12
+ {children}
13
+ </svg>
14
+ )
15
+ }
16
+
17
+ export default Svg
@@ -0,0 +1,17 @@
1
+ import React, { HTMLAttributes, ReactNode } from 'react'
2
+ import { cn } from '../helpers'
3
+
4
+ export interface DialogActionsProps extends HTMLAttributes<HTMLDivElement> {
5
+ children: ReactNode
6
+ }
7
+
8
+ export const Actions = ({ children, className, ...props }: DialogActionsProps) => {
9
+ return (
10
+ <div
11
+ className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end p-2 pt-4', className)}
12
+ {...props}
13
+ >
14
+ {children}
15
+ </div>
16
+ )
17
+ }
@@ -0,0 +1,25 @@
1
+ import React, { ReactNode } from 'react'
2
+ import { Button, ButtonProps } from '../button'
3
+ import { cn } from '../helpers'
4
+ import { useDialog } from './dialog-context'
5
+
6
+ export interface DialogConfirmProps extends Omit<ButtonProps, 'onClick'> {
7
+ children: ReactNode
8
+ }
9
+
10
+ export const Confirm = ({ children, className, variant, ...props }: DialogConfirmProps) => {
11
+ const { onConfirm, variant: contextVariant } = useDialog()
12
+
13
+ return (
14
+ <Button
15
+ variant={variant ?? contextVariant}
16
+ size="md"
17
+ className={cn('w-full flex-1 sm:w-auto sm:flex-none font-medium', className)}
18
+ onClick={onConfirm}
19
+ data-testid="confirm-dialog-confirm-button"
20
+ {...props}
21
+ >
22
+ {children}
23
+ </Button>
24
+ )
25
+ }
@@ -0,0 +1,21 @@
1
+ import { createContext, useContext } from 'react'
2
+
3
+ export type DialogVariant = 'danger' | 'success'
4
+
5
+ export interface DialogContextValue {
6
+ onConfirm: () => void
7
+ onDismiss: () => void
8
+ variant: DialogVariant
9
+ }
10
+
11
+ export const DialogContext = createContext<DialogContextValue | null>(null)
12
+
13
+ export const useDialog = (): DialogContextValue => {
14
+ const context = useContext(DialogContext)
15
+
16
+ if (!context) {
17
+ throw new Error('Dialog compound components must be used within a Dialog.Provider')
18
+ }
19
+
20
+ return context
21
+ }
@@ -0,0 +1,37 @@
1
+ import React, { HTMLAttributes, ReactNode, useContext } from 'react'
2
+ import { cn } from '../helpers'
3
+ import { DialogContext, DialogContextValue, DialogVariant } from './dialog-context'
4
+
5
+ export interface DialogRootProps extends HTMLAttributes<HTMLDivElement> {
6
+ children: ReactNode
7
+ variant: DialogVariant
8
+ }
9
+
10
+ export const Dialog = ({ children, variant, className, ...props }: DialogRootProps) => {
11
+ const parentContext = useContext(DialogContext)
12
+
13
+ const contextValue: DialogContextValue = {
14
+ onConfirm: parentContext?.onConfirm ?? (() => {}),
15
+ onDismiss: parentContext?.onDismiss ?? (() => {}),
16
+ variant,
17
+ }
18
+
19
+ return (
20
+ <DialogContext.Provider value={contextValue}>
21
+ <div
22
+ className={cn(
23
+ 'fixed left-1/2 top-[15vh] z-[1001] -translate-x-1/2',
24
+ 'flex w-[calc(100%-2rem)] max-w-sm flex-col rounded-xl bg-white shadow-xl outline-none',
25
+ 'dark:bg-zinc-900 dark:text-zinc-100 p-1.5',
26
+ 'transition-all duration-100',
27
+ 'data-[starting-style]:scale-50 data-[starting-style]:opacity-0',
28
+ 'data-[ending-style]:scale-50 data-[ending-style]:opacity-0',
29
+ className,
30
+ )}
31
+ {...props}
32
+ >
33
+ {children}
34
+ </div>
35
+ </DialogContext.Provider>
36
+ )
37
+ }
@@ -0,0 +1,29 @@
1
+ import React, { ReactNode } from 'react'
2
+ import { Button, ButtonProps } from '../button'
3
+ import { cn } from '../helpers'
4
+ import { useDialog } from './dialog-context'
5
+
6
+ export interface DialogDismissProps extends Omit<ButtonProps, 'onClick'> {
7
+ children: ReactNode
8
+ }
9
+
10
+ export const Dismiss = ({
11
+ children,
12
+ className,
13
+ variant = 'secondary',
14
+ ...props
15
+ }: DialogDismissProps) => {
16
+ const { onDismiss } = useDialog()
17
+
18
+ return (
19
+ <Button
20
+ variant={variant}
21
+ size="md"
22
+ className={cn('w-full flex-1 sm:w-auto sm:flex-none font-medium', className)}
23
+ onClick={onDismiss}
24
+ {...props}
25
+ >
26
+ {children}
27
+ </Button>
28
+ )
29
+ }
@@ -0,0 +1,37 @@
1
+ import { Actions, DialogActionsProps } from './actions'
2
+ import { Confirm, DialogConfirmProps } from './confirm-button'
3
+ import { Dismiss, DialogDismissProps } from './dismiss-button'
4
+ import { Message, DialogMessageProps } from './message'
5
+ import { Provider, DialogProviderProps, useDialog } from './provider'
6
+ import { DialogRootProps, Dialog as DialogRoot } from './dialog'
7
+ import { DialogTitleProps, Title } from './title'
8
+
9
+ type DialogCompoundType = typeof DialogRoot & {
10
+ Provider: typeof Provider
11
+ Title: typeof Title
12
+ Message: typeof Message
13
+ Actions: typeof Actions
14
+ Confirm: typeof Confirm
15
+ Dismiss: typeof Dismiss
16
+ useDialog: typeof useDialog
17
+ }
18
+
19
+ export const Dialog: DialogCompoundType = Object.assign(DialogRoot, {
20
+ Provider,
21
+ Title,
22
+ Message,
23
+ Actions,
24
+ Confirm,
25
+ Dismiss,
26
+ useDialog,
27
+ })
28
+
29
+ export type {
30
+ DialogRootProps,
31
+ DialogProviderProps,
32
+ DialogTitleProps,
33
+ DialogMessageProps,
34
+ DialogActionsProps,
35
+ DialogConfirmProps,
36
+ DialogDismissProps,
37
+ }
@@ -0,0 +1,20 @@
1
+ import React, { HTMLAttributes, ReactNode } from 'react'
2
+ import { cn } from '../helpers'
3
+
4
+ export interface DialogMessageProps extends HTMLAttributes<HTMLDivElement> {
5
+ children: ReactNode
6
+ }
7
+
8
+ export const Message = ({ children, className, ...props }: DialogMessageProps) => {
9
+ return (
10
+ <div
11
+ className={cn(
12
+ 'p-2 pt-4 font-body text-sm font-medium text-gray-400 dark:text-zinc-200',
13
+ className,
14
+ )}
15
+ {...props}
16
+ >
17
+ {children}
18
+ </div>
19
+ )
20
+ }
@@ -0,0 +1,102 @@
1
+ import React, {
2
+ ReactElement,
3
+ ReactNode,
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useRef,
8
+ useState,
9
+ } from 'react'
10
+ import { AlertDialog } from '@base-ui/react/alert-dialog'
11
+ import { cn } from '../helpers'
12
+ import { DialogContext, DialogContextValue } from './dialog-context'
13
+
14
+ interface ProviderContextValue {
15
+ confirm: (dialog: ReactElement) => Promise<boolean>
16
+ }
17
+
18
+ const ProviderContext = createContext<ProviderContextValue | null>(null)
19
+
20
+ export interface DialogProviderProps {
21
+ children: ReactNode
22
+ }
23
+
24
+ export const Provider = ({ children }: DialogProviderProps) => {
25
+ const [dialogElement, setDialogElement] = useState<ReactElement | null>(null)
26
+ const [open, setOpen] = useState(false)
27
+ const resolveRef = useRef<((confirmed: boolean) => void) | null>(null)
28
+ const resolvedRef = useRef(false)
29
+
30
+ const confirm = useCallback((element: ReactElement): Promise<boolean> => {
31
+ return new Promise((resolve) => {
32
+ resolvedRef.current = false
33
+ resolveRef.current = resolve
34
+ setDialogElement(element)
35
+ setOpen(true)
36
+ })
37
+ }, [])
38
+
39
+ const handleConfirm = useCallback(() => {
40
+ if (!resolvedRef.current) {
41
+ resolvedRef.current = true
42
+ resolveRef.current?.(true)
43
+ }
44
+ setOpen(false)
45
+ }, [])
46
+
47
+ const handleDismiss = useCallback(() => {
48
+ if (!resolvedRef.current) {
49
+ resolvedRef.current = true
50
+ resolveRef.current?.(false)
51
+ }
52
+ setOpen(false)
53
+ }, [])
54
+
55
+ const handleOpenChange = useCallback(
56
+ (isOpen: boolean) => {
57
+ if (!isOpen) {
58
+ handleDismiss()
59
+ }
60
+ },
61
+ [handleDismiss],
62
+ )
63
+
64
+ const dialogContextValue: DialogContextValue = {
65
+ onConfirm: handleConfirm,
66
+ onDismiss: handleDismiss,
67
+ variant: 'danger',
68
+ }
69
+
70
+ return (
71
+ <ProviderContext.Provider value={{ confirm }}>
72
+ {children}
73
+ <AlertDialog.Root open={open} onOpenChange={handleOpenChange}>
74
+ <AlertDialog.Portal>
75
+ <AlertDialog.Backdrop
76
+ className={cn(
77
+ 'fixed inset-0 z-[1000] bg-gray-900/70 dark:bg-black/50 backdrop-blur',
78
+ 'transition-opacity duration-200 ease-in-out',
79
+ 'data-[starting-style]:opacity-0',
80
+ 'data-[ending-style]:opacity-0',
81
+ )}
82
+ />
83
+ <AlertDialog.Popup>
84
+ <DialogContext.Provider value={dialogContextValue}>
85
+ {dialogElement}
86
+ </DialogContext.Provider>
87
+ </AlertDialog.Popup>
88
+ </AlertDialog.Portal>
89
+ </AlertDialog.Root>
90
+ </ProviderContext.Provider>
91
+ )
92
+ }
93
+
94
+ export const useDialog = (): ProviderContextValue => {
95
+ const context = useContext(ProviderContext)
96
+
97
+ if (!context) {
98
+ throw new Error('useDialog must be used within a Dialog.Provider')
99
+ }
100
+
101
+ return context
102
+ }
@@ -0,0 +1,20 @@
1
+ import React, { HTMLAttributes, ReactNode } from 'react'
2
+ import { cn } from '../helpers'
3
+
4
+ export interface DialogTitleProps extends HTMLAttributes<HTMLDivElement> {
5
+ children: ReactNode
6
+ }
7
+
8
+ export const Title = ({ children, className, ...props }: DialogTitleProps) => {
9
+ return (
10
+ <div
11
+ className={cn(
12
+ 'border-b border-gray-100 dark:border-zinc-800 px-3 py-2 text-base font-bold text-gray dark:text-zinc-100',
13
+ className,
14
+ )}
15
+ {...props}
16
+ >
17
+ {children}
18
+ </div>
19
+ )
20
+ }