@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,202 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
ReactNode,
|
|
3
|
+
SyntheticEvent,
|
|
4
|
+
createContext,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react'
|
|
9
|
+
import { Popover, type PopoverRootChangeEventDetails } from '@base-ui/react/popover'
|
|
10
|
+
import { cn } from '../helpers'
|
|
11
|
+
import { XIcon } from '@phosphor-icons/react'
|
|
12
|
+
import { useDismissedCallouts } from './use-dismissed-callouts'
|
|
13
|
+
|
|
14
|
+
type CalloutContextValue = {
|
|
15
|
+
open: boolean
|
|
16
|
+
show: boolean
|
|
17
|
+
dismissed: boolean
|
|
18
|
+
side: 'top' | 'bottom' | 'left' | 'right'
|
|
19
|
+
sideOffset: number
|
|
20
|
+
align: 'start' | 'center' | 'end'
|
|
21
|
+
highlight: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const CalloutContext = createContext<CalloutContextValue | null>(null)
|
|
25
|
+
|
|
26
|
+
const useCalloutContext = () => {
|
|
27
|
+
const ctx = useContext(CalloutContext)
|
|
28
|
+
if (!ctx) {
|
|
29
|
+
throw new Error('Callout sub-components must be used inside <Callout>')
|
|
30
|
+
}
|
|
31
|
+
return ctx
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type CalloutProps = {
|
|
35
|
+
id: string
|
|
36
|
+
children: ReactNode
|
|
37
|
+
show?: boolean
|
|
38
|
+
side?: 'top' | 'bottom' | 'left' | 'right'
|
|
39
|
+
sideOffset?: number
|
|
40
|
+
align?: 'start' | 'center' | 'end'
|
|
41
|
+
highlight?: boolean
|
|
42
|
+
/** When false, dismiss state is local only and not persisted to localStorage. Defaults to true. */
|
|
43
|
+
rememberDismissed?: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const Callout = ({
|
|
47
|
+
id,
|
|
48
|
+
children,
|
|
49
|
+
show = true,
|
|
50
|
+
highlight = false,
|
|
51
|
+
side = 'bottom',
|
|
52
|
+
sideOffset = 8,
|
|
53
|
+
align = 'center',
|
|
54
|
+
rememberDismissed = true,
|
|
55
|
+
}: CalloutProps) => {
|
|
56
|
+
const { isDismissed, dismiss } = useDismissedCallouts()
|
|
57
|
+
const persistedDismissed = rememberDismissed && isDismissed(id)
|
|
58
|
+
const [open, setOpen] = useState(!persistedDismissed)
|
|
59
|
+
|
|
60
|
+
const dismissed = rememberDismissed ? persistedDismissed : !open
|
|
61
|
+
|
|
62
|
+
const handleOpenChange = useCallback(
|
|
63
|
+
(nextOpen: boolean, event: PopoverRootChangeEventDetails) => {
|
|
64
|
+
if (nextOpen) {
|
|
65
|
+
setOpen(true)
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
const intentional =
|
|
69
|
+
event.reason === 'close-press' ||
|
|
70
|
+
event.reason === 'escape-key' ||
|
|
71
|
+
event.reason === 'trigger-press'
|
|
72
|
+
if (!intentional) {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
if (rememberDismissed) {
|
|
76
|
+
dismiss(id)
|
|
77
|
+
}
|
|
78
|
+
setOpen(false)
|
|
79
|
+
},
|
|
80
|
+
[dismiss, id, rememberDismissed],
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<CalloutContext.Provider value={{ open, show, dismissed, side, sideOffset, align, highlight }}>
|
|
85
|
+
<Popover.Root open={show && open && !dismissed} onOpenChange={handleOpenChange}>
|
|
86
|
+
{children}
|
|
87
|
+
</Popover.Root>
|
|
88
|
+
</CalloutContext.Provider>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
type CalloutTriggerProps = {
|
|
93
|
+
children: ReactNode
|
|
94
|
+
className?: string
|
|
95
|
+
/** Set to -1 to remove the trigger from the tab order and suppress focus styling. */
|
|
96
|
+
tabIndex?: number
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const CalloutTrigger = ({ children, className, tabIndex }: CalloutTriggerProps) => {
|
|
100
|
+
const { highlight, show, open, dismissed } = useCalloutContext()
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<Popover.Trigger
|
|
104
|
+
render={<span />}
|
|
105
|
+
nativeButton={false}
|
|
106
|
+
tabIndex={tabIndex}
|
|
107
|
+
className={cn(
|
|
108
|
+
'rounded outline-none focus-visible:ring-2 focus-visible:ring-info/75',
|
|
109
|
+
highlight &&
|
|
110
|
+
show &&
|
|
111
|
+
open &&
|
|
112
|
+
!dismissed &&
|
|
113
|
+
'bg-info-lighten/50 dark:bg-info/5 rounded-sm shadow-[0_0_2px_2px_theme(colors.info/0.3)] dark: ring-1 dark:ring-info/40',
|
|
114
|
+
className,
|
|
115
|
+
)}
|
|
116
|
+
>
|
|
117
|
+
{children}
|
|
118
|
+
</Popover.Trigger>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
type CalloutContentProps = {
|
|
123
|
+
children: ReactNode
|
|
124
|
+
className?: string
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const stopCalloutPointerBubbling = (e: SyntheticEvent) => {
|
|
128
|
+
e.stopPropagation()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const CalloutContent = ({ children, className }: CalloutContentProps) => {
|
|
132
|
+
const { side, sideOffset, align } = useCalloutContext()
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<Popover.Portal>
|
|
136
|
+
<Popover.Positioner
|
|
137
|
+
className={cn(
|
|
138
|
+
'z-50 outline-none',
|
|
139
|
+
// Base UI + Floating UI `hide` middleware sets `data-anchor-hidden` when the
|
|
140
|
+
// trigger is clipped or scrolled out of view; hide the portaled popup to match.
|
|
141
|
+
'data-[anchor-hidden]:invisible data-[anchor-hidden]:pointer-events-none',
|
|
142
|
+
)}
|
|
143
|
+
side={side}
|
|
144
|
+
sideOffset={sideOffset}
|
|
145
|
+
align={align}
|
|
146
|
+
>
|
|
147
|
+
<Popover.Popup
|
|
148
|
+
initialFocus={false}
|
|
149
|
+
onClick={stopCalloutPointerBubbling}
|
|
150
|
+
onPointerDown={stopCalloutPointerBubbling}
|
|
151
|
+
className={cn(
|
|
152
|
+
'bg-white dark:bg-zinc-900 ring-1 ring-gray-200 dark:ring-zinc-800 rounded-lg shadow-lg',
|
|
153
|
+
'font-normal font-body text-xs tracking-normal text-gray-700 dark:text-zinc-100',
|
|
154
|
+
'px-3 py-2.5 pr-8 max-w-72',
|
|
155
|
+
'transition duration-200 ease-in-out',
|
|
156
|
+
'data-[side=top]:origin-bottom',
|
|
157
|
+
'data-[side=bottom]:origin-top',
|
|
158
|
+
'data-[side=left]:origin-right',
|
|
159
|
+
'data-[side=right]:origin-left',
|
|
160
|
+
className,
|
|
161
|
+
)}
|
|
162
|
+
>
|
|
163
|
+
<Popover.Arrow className="flex data-[side=top]:bottom-[-8px] data-[side=top]:rotate-180 data-[side=bottom]:top-[-8px] data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90">
|
|
164
|
+
<ArrowSvg />
|
|
165
|
+
</Popover.Arrow>
|
|
166
|
+
<Popover.Close
|
|
167
|
+
className={cn(
|
|
168
|
+
'absolute top-1.5 right-1.5 p-0.5 rounded',
|
|
169
|
+
'text-gray-400 dark:text-zinc-400',
|
|
170
|
+
'hover:text-gray-600 dark:hover:text-zinc-100',
|
|
171
|
+
'hover:bg-gray-100 dark:hover:bg-zinc-800',
|
|
172
|
+
'transition-colors cursor-pointer',
|
|
173
|
+
)}
|
|
174
|
+
>
|
|
175
|
+
<XIcon className="size-3.5" />
|
|
176
|
+
</Popover.Close>
|
|
177
|
+
{children}
|
|
178
|
+
</Popover.Popup>
|
|
179
|
+
</Popover.Positioner>
|
|
180
|
+
</Popover.Portal>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
Callout.Trigger = CalloutTrigger
|
|
185
|
+
Callout.Content = CalloutContent
|
|
186
|
+
|
|
187
|
+
const ArrowSvg = (props: React.ComponentProps<'svg'>) => (
|
|
188
|
+
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
|
|
189
|
+
<path
|
|
190
|
+
d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
|
|
191
|
+
className="fill-white dark:fill-zinc-900"
|
|
192
|
+
/>
|
|
193
|
+
<path
|
|
194
|
+
d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
|
|
195
|
+
className="fill-gray-200 dark:fill-zinc-800"
|
|
196
|
+
/>
|
|
197
|
+
<path
|
|
198
|
+
d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
|
|
199
|
+
className="fill-none"
|
|
200
|
+
/>
|
|
201
|
+
</svg>
|
|
202
|
+
)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useCallback, useSyncExternalStore } from 'react'
|
|
2
|
+
|
|
3
|
+
const STORAGE_KEY = 'dismissed-callouts'
|
|
4
|
+
|
|
5
|
+
const getSnapshot = (): Array<string> => {
|
|
6
|
+
if (typeof window === 'undefined') {
|
|
7
|
+
return []
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
const raw = localStorage.getItem(STORAGE_KEY)
|
|
11
|
+
return raw ? JSON.parse(raw) : []
|
|
12
|
+
} catch {
|
|
13
|
+
return []
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const getServerSnapshot = (): Array<string> => []
|
|
18
|
+
|
|
19
|
+
const listeners = new Set<() => void>()
|
|
20
|
+
|
|
21
|
+
const subscribe = (callback: () => void) => {
|
|
22
|
+
listeners.add(callback)
|
|
23
|
+
return () => {
|
|
24
|
+
listeners.delete(callback)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const notifyListeners = () => {
|
|
29
|
+
for (const listener of listeners) {
|
|
30
|
+
listener()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let cachedSnapshot: Array<string> | null = null
|
|
35
|
+
|
|
36
|
+
const getStableSnapshot = (): Array<string> => {
|
|
37
|
+
const next = getSnapshot()
|
|
38
|
+
if (
|
|
39
|
+
cachedSnapshot !== null &&
|
|
40
|
+
cachedSnapshot.length === next.length &&
|
|
41
|
+
cachedSnapshot.every((id, i) => id === next[i])
|
|
42
|
+
) {
|
|
43
|
+
return cachedSnapshot
|
|
44
|
+
}
|
|
45
|
+
cachedSnapshot = next
|
|
46
|
+
return next
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const useDismissedCallouts = () => {
|
|
50
|
+
const dismissed = useSyncExternalStore(subscribe, getStableSnapshot, getServerSnapshot)
|
|
51
|
+
|
|
52
|
+
const isDismissed = useCallback((id: string) => dismissed.includes(id), [dismissed])
|
|
53
|
+
|
|
54
|
+
const dismiss = useCallback((id: string) => {
|
|
55
|
+
const current = getSnapshot()
|
|
56
|
+
if (current.includes(id)) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
const next = [...current, id]
|
|
60
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(next))
|
|
61
|
+
cachedSnapshot = null
|
|
62
|
+
notifyListeners()
|
|
63
|
+
}, [])
|
|
64
|
+
|
|
65
|
+
const reset = useCallback((id?: string) => {
|
|
66
|
+
if (id) {
|
|
67
|
+
const current = getSnapshot()
|
|
68
|
+
const next = current.filter((d) => d !== id)
|
|
69
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(next))
|
|
70
|
+
} else {
|
|
71
|
+
localStorage.removeItem(STORAGE_KEY)
|
|
72
|
+
}
|
|
73
|
+
cachedSnapshot = null
|
|
74
|
+
notifyListeners()
|
|
75
|
+
}, [])
|
|
76
|
+
|
|
77
|
+
return { isDismissed, dismiss, reset }
|
|
78
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { cn } from '../helpers'
|
|
3
|
+
|
|
4
|
+
export interface CardProps {
|
|
5
|
+
children: ReactNode
|
|
6
|
+
className?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** A standard Toolpath application surface. */
|
|
10
|
+
export const Card = ({ children, className }: CardProps) => (
|
|
11
|
+
<div
|
|
12
|
+
className={cn(
|
|
13
|
+
'rounded-xl border border-gray-100 bg-white dark:border-zinc-800 dark:bg-zinc-900',
|
|
14
|
+
className,
|
|
15
|
+
)}
|
|
16
|
+
>
|
|
17
|
+
{children}
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React, { useRef } from 'react'
|
|
2
|
+
import { Checkbox as BaseCheckbox, CheckboxRootProps } from '@base-ui/react'
|
|
3
|
+
import { cn } from '../helpers'
|
|
4
|
+
import { CheckIcon, MinusIcon } from '@phosphor-icons/react'
|
|
5
|
+
|
|
6
|
+
export interface CheckboxChangeEvent {
|
|
7
|
+
shiftKey: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type CheckboxProps = CheckboxRootProps & {
|
|
11
|
+
checked: boolean
|
|
12
|
+
onChange: (checked: boolean, event: CheckboxChangeEvent) => void
|
|
13
|
+
name: string
|
|
14
|
+
className?: string
|
|
15
|
+
disabled?: boolean
|
|
16
|
+
indeterminate?: boolean
|
|
17
|
+
tabIndex?: number
|
|
18
|
+
variant?: 'default' | 'muted'
|
|
19
|
+
size?: 'md' | 'sm'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const Checkbox = ({
|
|
23
|
+
checked,
|
|
24
|
+
onChange,
|
|
25
|
+
name,
|
|
26
|
+
className,
|
|
27
|
+
disabled = false,
|
|
28
|
+
indeterminate = false,
|
|
29
|
+
variant = 'default',
|
|
30
|
+
size = 'md',
|
|
31
|
+
...props
|
|
32
|
+
}: CheckboxProps) => {
|
|
33
|
+
if (checked && indeterminate) {
|
|
34
|
+
throw new Error('Checkbox cannot be both checked & indeterminate.')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const isActive = checked || indeterminate
|
|
38
|
+
const shiftKeyRef = useRef(false)
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<BaseCheckbox.Root
|
|
42
|
+
name={name}
|
|
43
|
+
checked={checked}
|
|
44
|
+
indeterminate={indeterminate}
|
|
45
|
+
onClick={(e) => {
|
|
46
|
+
shiftKeyRef.current = e.shiftKey
|
|
47
|
+
}}
|
|
48
|
+
onCheckedChange={(newChecked) => {
|
|
49
|
+
onChange(newChecked, { shiftKey: shiftKeyRef.current })
|
|
50
|
+
}}
|
|
51
|
+
disabled={disabled}
|
|
52
|
+
className={cn(
|
|
53
|
+
'flex cursor-pointer items-center justify-center bg-white outline-none focus-visible:ring-2 ring-info/75 ring-offset-1 dark:bg-zinc-900 dark:ring-offset-zinc-950',
|
|
54
|
+
'border dark:border-zinc-500',
|
|
55
|
+
{
|
|
56
|
+
'size-4 rounded': size === 'md',
|
|
57
|
+
'size-3 rounded-sm': size === 'sm',
|
|
58
|
+
'bg-gray-300 dark:bg-zinc-800': variant === 'muted' && isActive,
|
|
59
|
+
'bg-info': variant === 'default' && isActive,
|
|
60
|
+
'bg-transparent': !isActive,
|
|
61
|
+
'border-none text-white': isActive,
|
|
62
|
+
'cursor-not-allowed saturate-50': disabled,
|
|
63
|
+
'bg-gray-50 dark:bg-zinc-950': !isActive && disabled,
|
|
64
|
+
'text-white/65': isActive && disabled,
|
|
65
|
+
},
|
|
66
|
+
className,
|
|
67
|
+
)}
|
|
68
|
+
{...props}
|
|
69
|
+
>
|
|
70
|
+
<BaseCheckbox.Indicator className="flex items-center justify-center text-white" keepMounted>
|
|
71
|
+
{checked && <CheckIcon className="w-full h-full" />}
|
|
72
|
+
{indeterminate && <MinusIcon className="w-full h-full" />}
|
|
73
|
+
</BaseCheckbox.Indicator>
|
|
74
|
+
</BaseCheckbox.Root>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './checkbox'
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React, { FC, ReactNode, SVGProps } from 'react'
|
|
2
|
+
import { cn } from '../helpers'
|
|
3
|
+
import { ArrowsOutLineVerticalIcon } from '@phosphor-icons/react'
|
|
4
|
+
import { useCombobox } from './combobox-context'
|
|
5
|
+
import { Icon } from './icon'
|
|
6
|
+
import { Trigger } from './trigger'
|
|
7
|
+
import { Value } from './value'
|
|
8
|
+
|
|
9
|
+
export type ComboboxButtonProps = {
|
|
10
|
+
placeholder?: string
|
|
11
|
+
icon?: FC<SVGProps<SVGSVGElement>>
|
|
12
|
+
children?: ReactNode
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const Button = ({
|
|
16
|
+
placeholder = 'Select Option',
|
|
17
|
+
icon: IconComponent,
|
|
18
|
+
children,
|
|
19
|
+
}: ComboboxButtonProps) => {
|
|
20
|
+
const { size, variant, disabled, invalid } = useCombobox()
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Trigger
|
|
24
|
+
nativeButton={true}
|
|
25
|
+
render={undefined}
|
|
26
|
+
tabIndex={0}
|
|
27
|
+
className={cn(
|
|
28
|
+
'group [&:has(~[data-input])]:data-[popup-open]:hidden',
|
|
29
|
+
'w-full rounded flex gap-1 items-center text-left',
|
|
30
|
+
'outline-none font-medium',
|
|
31
|
+
'data-[popup-open]:data-[popup-side="top"]:rounded-t-none',
|
|
32
|
+
'data-[popup-open]:data-[popup-side="bottom"]:rounded-b-none',
|
|
33
|
+
{
|
|
34
|
+
'px-1': variant !== 'ghost',
|
|
35
|
+
'focus-visible:ring-2 ring-info/75': !invalid,
|
|
36
|
+
'border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 focus-visible:ring-1 focus-visible:border-info':
|
|
37
|
+
variant === 'secondary' && !invalid,
|
|
38
|
+
'bg-gray-50 dark:bg-zinc-800':
|
|
39
|
+
(variant === 'default' || (disabled && variant === 'secondary')) && !invalid,
|
|
40
|
+
'bg-gray-100 dark:bg-zinc-800': variant === 'muted' && !invalid,
|
|
41
|
+
'border border-[#FFD4E0] bg-[#FFD4E0]/25 focus-visible:border-[#FFD4E0] focus-visible:ring-2 ring-[#FFD4E0] dark:border-danger/50 dark:bg-danger/25 dark:ring-danger/50 dark:focus-visible:border-danger/50':
|
|
42
|
+
invalid && !disabled,
|
|
43
|
+
'h-9': size === 'xl' && variant !== 'ghost',
|
|
44
|
+
'h-7': size === 'lg' && variant !== 'ghost',
|
|
45
|
+
'h-6': size === 'md' && variant !== 'ghost',
|
|
46
|
+
'h-5': size === 'sm' && variant !== 'ghost',
|
|
47
|
+
'text-lg': size === 'xl',
|
|
48
|
+
'text-sm': size === 'lg',
|
|
49
|
+
'text-xs': size === 'md',
|
|
50
|
+
'text-2xs': size === 'sm',
|
|
51
|
+
'cursor-not-allowed': disabled,
|
|
52
|
+
},
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
{IconComponent && (
|
|
56
|
+
<IconComponent
|
|
57
|
+
data-icon
|
|
58
|
+
className={cn('text-gray-300 dark:text-zinc-400 shrink-0', {
|
|
59
|
+
'group-hover:text-gray-400 dark:group-hover:text-zinc-100': !disabled,
|
|
60
|
+
'text-gray-200 dark:text-zinc-500': disabled,
|
|
61
|
+
'size-2.5': size === 'sm',
|
|
62
|
+
'size-3.5': size === 'md' || size === 'lg',
|
|
63
|
+
'size-4': size === 'xl',
|
|
64
|
+
})}
|
|
65
|
+
/>
|
|
66
|
+
)}
|
|
67
|
+
<div
|
|
68
|
+
className={cn('truncate dark:text-zinc-100', {
|
|
69
|
+
'text-gray-400': variant === 'secondary',
|
|
70
|
+
'text-gray-500': variant !== 'secondary' && variant !== 'ghost',
|
|
71
|
+
'text-inherit': variant === 'ghost',
|
|
72
|
+
'group-hover:text-gray-500 dark:group-hover:text-zinc-50': !disabled,
|
|
73
|
+
'text-gray-200 dark:text-zinc-400': disabled,
|
|
74
|
+
'pl-1': !IconComponent && variant !== 'ghost',
|
|
75
|
+
})}
|
|
76
|
+
>
|
|
77
|
+
<Value
|
|
78
|
+
placeholder={placeholder}
|
|
79
|
+
className={cn('truncate dark:text-zinc-100', {
|
|
80
|
+
'text-gray-400': variant === 'secondary',
|
|
81
|
+
'text-gray-500': variant !== 'secondary' && variant !== 'ghost',
|
|
82
|
+
'text-inherit': variant === 'ghost',
|
|
83
|
+
'group-hover:text-gray-500 dark:group-hover:text-zinc-50': !disabled,
|
|
84
|
+
'text-gray-200 dark:text-zinc-400': disabled,
|
|
85
|
+
})}
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
{Boolean(children) && children}
|
|
89
|
+
<Icon
|
|
90
|
+
className={cn('ml-auto', {
|
|
91
|
+
'opacity-0 group-hover:opacity-100 group-data-[popup-open]:opacity-100':
|
|
92
|
+
variant === 'ghost',
|
|
93
|
+
})}
|
|
94
|
+
>
|
|
95
|
+
<ArrowsOutLineVerticalIcon className="size-3.5 text-gray-200 dark:text-zinc-400" />
|
|
96
|
+
</Icon>
|
|
97
|
+
</Trigger>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React, { FC, ReactNode, createContext, useContext, useMemo } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface ComboboxContextInterface {
|
|
4
|
+
size: 'xl' | 'lg' | 'md' | 'sm'
|
|
5
|
+
variant: 'default' | 'secondary' | 'muted' | 'ghost'
|
|
6
|
+
disabled: boolean
|
|
7
|
+
invalid: boolean
|
|
8
|
+
itemToStringLabel?: (item: any) => string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const ComboboxContext = createContext<Partial<ComboboxContextInterface>>(
|
|
12
|
+
{} as ComboboxContextInterface,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
interface ProviderProps {
|
|
16
|
+
size: 'xl' | 'lg' | 'md' | 'sm'
|
|
17
|
+
variant: 'default' | 'secondary' | 'muted' | 'ghost'
|
|
18
|
+
disabled: boolean
|
|
19
|
+
invalid: boolean
|
|
20
|
+
itemToStringLabel?: (item: any) => string
|
|
21
|
+
children: ReactNode
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const ComboboxProvider: FC<ProviderProps> = ({
|
|
25
|
+
size = 'md',
|
|
26
|
+
variant = 'default',
|
|
27
|
+
disabled = false,
|
|
28
|
+
invalid = false,
|
|
29
|
+
itemToStringLabel,
|
|
30
|
+
children,
|
|
31
|
+
}) => {
|
|
32
|
+
const value = useMemo(
|
|
33
|
+
() => ({ size, variant, disabled, invalid, itemToStringLabel }),
|
|
34
|
+
[size, variant, disabled, invalid, itemToStringLabel],
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return <ComboboxContext.Provider value={value}>{children}</ComboboxContext.Provider>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const useCombobox = () => useContext(ComboboxContext)
|
|
41
|
+
const ComboboxConsumer = ComboboxContext.Consumer
|
|
42
|
+
export { useCombobox, ComboboxProvider, ComboboxContext, ComboboxConsumer }
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React, { memo, useCallback, useState } from 'react'
|
|
2
|
+
import { Combobox as BaseCombobox, ComboboxRootProps } from '@base-ui/react'
|
|
3
|
+
import { ComboboxProvider } from './combobox-context'
|
|
4
|
+
|
|
5
|
+
export type ComboboxProps<T> = ComboboxRootProps<T> & {
|
|
6
|
+
size?: 'xl' | 'lg' | 'md' | 'sm'
|
|
7
|
+
variant?: 'default' | 'secondary' | 'muted' | 'ghost'
|
|
8
|
+
invalid?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Use a type assertion for the generic memo
|
|
12
|
+
export const Combobox = memo(
|
|
13
|
+
<T,>({
|
|
14
|
+
children,
|
|
15
|
+
size = 'md',
|
|
16
|
+
variant = 'default',
|
|
17
|
+
invalid = false,
|
|
18
|
+
onOpenChange,
|
|
19
|
+
...props
|
|
20
|
+
}: ComboboxProps<T>) => {
|
|
21
|
+
if (!props.items) {
|
|
22
|
+
throw new Error('Combobox requires an `items` prop.')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const [inputValue, setInputValue] = useState('')
|
|
26
|
+
|
|
27
|
+
const handleOpenChange = useCallback<NonNullable<typeof onOpenChange>>(
|
|
28
|
+
(open, eventDetails) => {
|
|
29
|
+
if (open) {
|
|
30
|
+
// Clear input value when combobox opens
|
|
31
|
+
setInputValue('')
|
|
32
|
+
}
|
|
33
|
+
onOpenChange?.(open, eventDetails)
|
|
34
|
+
},
|
|
35
|
+
[onOpenChange],
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<ComboboxProvider
|
|
40
|
+
disabled={Boolean(props.disabled)}
|
|
41
|
+
size={size}
|
|
42
|
+
variant={variant}
|
|
43
|
+
invalid={invalid}
|
|
44
|
+
itemToStringLabel={props.itemToStringLabel}
|
|
45
|
+
>
|
|
46
|
+
<BaseCombobox.Root
|
|
47
|
+
data-variant={variant}
|
|
48
|
+
data-size={size}
|
|
49
|
+
data-invalid={invalid || undefined}
|
|
50
|
+
inputValue={inputValue}
|
|
51
|
+
onInputValueChange={setInputValue}
|
|
52
|
+
onOpenChange={handleOpenChange}
|
|
53
|
+
{...props}
|
|
54
|
+
>
|
|
55
|
+
{children}
|
|
56
|
+
</BaseCombobox.Root>
|
|
57
|
+
</ComboboxProvider>
|
|
58
|
+
)
|
|
59
|
+
},
|
|
60
|
+
) as <T>(props: ComboboxProps<T>) => React.ReactElement
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React, { HTMLAttributes, ReactNode } from 'react'
|
|
2
|
+
import { Combobox as BaseCombobox } from '@base-ui/react'
|
|
3
|
+
import { cn } from '../helpers'
|
|
4
|
+
|
|
5
|
+
export type ComboboxEmptyProps = HTMLAttributes<HTMLDivElement> & {
|
|
6
|
+
children?: ReactNode
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const Empty = ({ className, children = 'No matches', ...props }: ComboboxEmptyProps) => {
|
|
10
|
+
return (
|
|
11
|
+
<BaseCombobox.Empty
|
|
12
|
+
className={cn(
|
|
13
|
+
'w-full [[data-empty]_&]:py-4 text-center text-gray-300 text-xs dark:text-zinc-400',
|
|
14
|
+
className,
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
>
|
|
18
|
+
{children}
|
|
19
|
+
</BaseCombobox.Empty>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Combobox as BaseCombobox,
|
|
4
|
+
ComboboxGroupLabelProps as BaseComboboxGroupLabelProps,
|
|
5
|
+
} from '@base-ui/react'
|
|
6
|
+
import { cn } from '../helpers'
|
|
7
|
+
|
|
8
|
+
export type ComboboxGroupLabelProps = BaseComboboxGroupLabelProps
|
|
9
|
+
|
|
10
|
+
export const GroupLabel = ({ className, children, ...props }: ComboboxGroupLabelProps) => (
|
|
11
|
+
<BaseCombobox.GroupLabel
|
|
12
|
+
className={cn(
|
|
13
|
+
'px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-gray-300 font-body dark:text-zinc-500',
|
|
14
|
+
className,
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
>
|
|
18
|
+
{children}
|
|
19
|
+
</BaseCombobox.GroupLabel>
|
|
20
|
+
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Combobox as BaseCombobox,
|
|
4
|
+
ComboboxGroupProps as BaseComboboxGroupProps,
|
|
5
|
+
} from '@base-ui/react'
|
|
6
|
+
|
|
7
|
+
export type ComboboxGroupProps = BaseComboboxGroupProps
|
|
8
|
+
|
|
9
|
+
export const Group = ({ children, ...props }: ComboboxGroupProps) => (
|
|
10
|
+
<BaseCombobox.Group {...props}>{children}</BaseCombobox.Group>
|
|
11
|
+
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React, { RefAttributes } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Combobox as BaseCombobox,
|
|
4
|
+
ComboboxIconProps as BaseComboboxIconProps,
|
|
5
|
+
} from '@base-ui/react'
|
|
6
|
+
|
|
7
|
+
export type ComboboxIconProps = BaseComboboxIconProps & RefAttributes<HTMLDivElement>
|
|
8
|
+
|
|
9
|
+
export const Icon = ({ children, ...props }: ComboboxIconProps) => {
|
|
10
|
+
return <BaseCombobox.Icon {...props}>{children}</BaseCombobox.Icon>
|
|
11
|
+
}
|