@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,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
|
+
}
|