@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,158 @@
1
+ import React, { ChangeEvent, MouseEvent, Ref } from 'react'
2
+ import { cn } from '../helpers'
3
+ import { MinusIcon, PlusIcon } from '@phosphor-icons/react'
4
+
5
+ export type QuantityInputProps = {
6
+ value: number
7
+ onValueChange: (value: number) => void
8
+ min?: number
9
+ max?: number
10
+ step?: number
11
+ size?: 'sm' | 'md' | 'lg' | 'xl'
12
+ variant?: 'default' | 'muted'
13
+ disabled?: boolean
14
+ className?: string
15
+ id?: string
16
+ ref?: Ref<HTMLInputElement>
17
+ }
18
+
19
+ export const QuantityInput = ({
20
+ value,
21
+ onValueChange,
22
+ min = 0,
23
+ max = Infinity,
24
+ step = 1,
25
+ size = 'md',
26
+ variant = 'default',
27
+ disabled = false,
28
+ className,
29
+ id,
30
+ ref,
31
+ }: QuantityInputProps) => {
32
+ const handleDecrement = (e: MouseEvent) => {
33
+ e.preventDefault()
34
+ const next = value - step
35
+ if (next >= min) {
36
+ onValueChange(next)
37
+ }
38
+ }
39
+
40
+ const handleIncrement = (e: MouseEvent) => {
41
+ e.preventDefault()
42
+ const next = value + step
43
+ if (next <= max) {
44
+ onValueChange(next)
45
+ }
46
+ }
47
+
48
+ const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
49
+ const raw = e.target.value
50
+ if (raw === '') {
51
+ return
52
+ }
53
+ const parsed = Number(raw)
54
+ if (!isNaN(parsed)) {
55
+ onValueChange(Math.min(max, Math.max(min, parsed)))
56
+ }
57
+ }
58
+
59
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
60
+ if (e.key === 'ArrowUp') {
61
+ e.preventDefault()
62
+ const next = value + step
63
+ if (next <= max) {
64
+ onValueChange(next)
65
+ }
66
+ } else if (e.key === 'ArrowDown') {
67
+ e.preventDefault()
68
+ const next = value - step
69
+ if (next >= min) {
70
+ onValueChange(next)
71
+ }
72
+ }
73
+ }
74
+
75
+ const handleBlur = () => {
76
+ onValueChange(Math.min(max, Math.max(min, value)))
77
+ }
78
+
79
+ const atMin = value <= min
80
+ const atMax = value >= max
81
+
82
+ return (
83
+ <div
84
+ className={cn(
85
+ 'inline-flex items-stretch overflow-hidden font-medium',
86
+ 'focus-within:ring-2 ring-info/50 dark:ring-info/75',
87
+ {
88
+ 'border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 focus-within:border-info dark:focus-within:border-info':
89
+ variant === 'default',
90
+ 'bg-gray-50 dark:bg-zinc-800': variant === 'muted',
91
+ 'h-5 rounded text-xs': size === 'sm',
92
+ 'h-6 rounded text-xs': size === 'md',
93
+ 'h-7 rounded text-sm': size === 'lg',
94
+ 'h-9 rounded-md text-lg': size === 'xl',
95
+ 'opacity-50 pointer-events-none': disabled,
96
+ },
97
+ className,
98
+ )}
99
+ data-disabled={disabled || undefined}
100
+ >
101
+ <button
102
+ type="button"
103
+ tabIndex={-1}
104
+ disabled={disabled || atMin}
105
+ onClick={handleDecrement}
106
+ className={cn(
107
+ 'flex shrink-0 items-center justify-center bg-gray-200 dark:bg-zinc-800 transition-colors cursor-pointer',
108
+ 'hover:bg-gray-300 dark:hover:bg-zinc-900 active:bg-gray-400 dark:active:bg-zinc-900 disabled:opacity-40 disabled:pointer-events-none',
109
+ {
110
+ 'w-5': size === 'sm',
111
+ 'w-6': size === 'md',
112
+ 'w-7': size === 'lg',
113
+ 'w-9': size === 'xl',
114
+ },
115
+ )}
116
+ >
117
+ <MinusIcon className="size-3.5 text-white dark:text-zinc-100" />
118
+ </button>
119
+ <input
120
+ ref={ref}
121
+ id={id}
122
+ data-testid={id}
123
+ type="text"
124
+ inputMode="numeric"
125
+ value={value}
126
+ onChange={handleChange}
127
+ onKeyDown={handleKeyDown}
128
+ onBlur={handleBlur}
129
+ disabled={disabled}
130
+ className={cn(
131
+ 'min-w-0 flex-1 bg-transparent text-center text-gray-500 dark:text-zinc-100 outline-none',
132
+ {
133
+ 'px-1': size === 'sm' || size === 'md',
134
+ 'px-2': size === 'lg' || size === 'xl',
135
+ },
136
+ )}
137
+ />
138
+ <button
139
+ type="button"
140
+ tabIndex={-1}
141
+ disabled={disabled || atMax}
142
+ onClick={handleIncrement}
143
+ className={cn(
144
+ 'flex shrink-0 items-center justify-center bg-gray-200 dark:bg-zinc-800 transition-colors cursor-pointer',
145
+ 'hover:bg-gray-300 dark:hover:bg-zinc-900 active:bg-gray-400 dark:active:bg-zinc-900 disabled:opacity-40 disabled:pointer-events-none',
146
+ {
147
+ 'w-5': size === 'sm',
148
+ 'w-6': size === 'md',
149
+ 'w-7': size === 'lg',
150
+ 'w-9': size === 'xl',
151
+ },
152
+ )}
153
+ >
154
+ <PlusIcon className="size-3.5 text-white dark:text-zinc-100" />
155
+ </button>
156
+ </div>
157
+ )
158
+ }
@@ -0,0 +1 @@
1
+ export * from './scroll-area'
@@ -0,0 +1,82 @@
1
+ import React, { ReactNode, useEffect, useRef, useState } from 'react'
2
+ import { ScrollArea as BaseScrollArea } from '@base-ui/react'
3
+ import { cn } from '../helpers'
4
+
5
+ export interface ScrollAreaState {
6
+ hasOverflow: boolean
7
+ }
8
+
9
+ export type ScrollAreaSize = 'sm' | 'md'
10
+
11
+ export interface ScrollAreaProps {
12
+ children: ReactNode | ((state: ScrollAreaState) => ReactNode)
13
+ className?: string
14
+ size?: ScrollAreaSize
15
+ }
16
+
17
+ export const ScrollArea = ({ children, className, size = 'md' }: ScrollAreaProps) => {
18
+ const viewportRef = useRef<HTMLDivElement>(null)
19
+ const [hasOverflow, setHasOverflow] = useState(false)
20
+
21
+ useEffect(() => {
22
+ const viewport = viewportRef.current
23
+ if (!viewport) {
24
+ return
25
+ }
26
+
27
+ const checkOverflow = () => {
28
+ setHasOverflow(viewport.scrollHeight > viewport.clientHeight)
29
+ }
30
+
31
+ checkOverflow()
32
+
33
+ const resizeObserver = new ResizeObserver(checkOverflow)
34
+ resizeObserver.observe(viewport)
35
+
36
+ const mutationObserver = new MutationObserver(checkOverflow)
37
+ mutationObserver.observe(viewport, { childList: true, subtree: true })
38
+
39
+ return () => {
40
+ resizeObserver.disconnect()
41
+ mutationObserver.disconnect()
42
+ }
43
+ }, [])
44
+
45
+ const state: ScrollAreaState = { hasOverflow }
46
+
47
+ return (
48
+ <BaseScrollArea.Root className={cn('flex-1 min-h-0 w-full', className)}>
49
+ <BaseScrollArea.Viewport
50
+ ref={viewportRef}
51
+ tabIndex={-1}
52
+ data-viewport
53
+ className="h-full w-full overscroll-contain outline-none"
54
+ >
55
+ {typeof children === 'function' ? children(state) : children}
56
+ </BaseScrollArea.Viewport>
57
+ <BaseScrollArea.Scrollbar
58
+ data-scrollbar
59
+ className={cn(
60
+ 'relative flex rounded bg-gray-100 dark:bg-zinc-700 opacity-0 pointer-events-none transition-opacity',
61
+ "before:absolute before:content-['']",
62
+ 'data-[orientation=vertical]:m-2 data-[orientation=vertical]:before:h-full data-[orientation=vertical]:before:w-5 data-[orientation=vertical]:before:left-1/2 data-[orientation=vertical]:before:-translate-x-1/2',
63
+ 'data-[orientation=horizontal]:m-2 data-[orientation=horizontal]:before:h-5 data-[orientation=horizontal]:before:w-full data-[orientation=horizontal]:before:left-0 data-[orientation=horizontal]:before:right-0 data-[orientation=horizontal]:before:-bottom-2',
64
+ // Reveal each scrollbar only when its orientation has overflow.
65
+ 'data-[orientation=vertical]:data-[has-overflow-y]:opacity-100',
66
+ 'data-[orientation=horizontal]:data-[has-overflow-x]:opacity-100',
67
+ 'data-[hovering]:pointer-events-auto data-[hovering]:opacity-100 data-[hovering]:delay-0',
68
+ 'data-[scrolling]:pointer-events-auto data-[scrolling]:opacity-100 data-[scrolling]:duration-0',
69
+ {
70
+ 'data-[orientation=vertical]:w-1 data-[orientation=horizontal]:h-1': size === 'sm',
71
+ 'data-[orientation=vertical]:w-2 data-[orientation=horizontal]:h-2': size === 'md',
72
+ },
73
+ )}
74
+ >
75
+ <BaseScrollArea.Thumb
76
+ data-thumb
77
+ className="w-full rounded bg-gray-500/50 dark:bg-zinc-400/50"
78
+ />
79
+ </BaseScrollArea.Scrollbar>
80
+ </BaseScrollArea.Root>
81
+ )
82
+ }
@@ -0,0 +1 @@
1
+ export * from './slider'
@@ -0,0 +1,75 @@
1
+ import { debounce } from 'lodash-es'
2
+ import React, { useEffect, useMemo } from 'react'
3
+ import {
4
+ Slider as BaseSlider,
5
+ SliderRoot,
6
+ SliderRootProps,
7
+ SliderThumbProps,
8
+ } from '@base-ui/react/slider'
9
+ import { cn } from '../helpers'
10
+
11
+ export type SliderProps = Omit<SliderRootProps, 'onValueChange'> & {
12
+ variant?: 'default'
13
+ onFocus?: SliderThumbProps['onFocus'] | undefined
14
+ onBlur?: SliderThumbProps['onBlur'] | undefined
15
+ onValueChange?: (value: number, eventDetails: SliderRoot.ChangeEventDetails) => void
16
+ }
17
+
18
+ export const Slider = ({
19
+ variant = 'default',
20
+ onFocus,
21
+ onBlur,
22
+ onValueChange,
23
+ className = '',
24
+ ...props
25
+ }: SliderProps) => {
26
+ const debouncedOnValueChange = useMemo(
27
+ () =>
28
+ debounce((value, eventDetails) => {
29
+ if (typeof onValueChange === 'function') {
30
+ onValueChange(Array.isArray(value) ? value[0] : value, eventDetails)
31
+ }
32
+ }, 10),
33
+ [onValueChange],
34
+ )
35
+
36
+ useEffect(() => {
37
+ return () => debouncedOnValueChange.cancel()
38
+ }, [debouncedOnValueChange])
39
+
40
+ return (
41
+ <BaseSlider.Root
42
+ thumbAlignment="edge"
43
+ className={cn('w-full data-[dragging]:cursor-ew-resize hover:cursor-ew-resize', className)}
44
+ onValueChange={debouncedOnValueChange}
45
+ {...props}
46
+ >
47
+ <BaseSlider.Control
48
+ data-control
49
+ className="flex w-full touch-none items-center select-none outline-none"
50
+ >
51
+ <BaseSlider.Track className="h-2 w-full rounded-full bg-gray-100 dark:bg-zinc-700 select-none hover:cursor-ew-resize">
52
+ <BaseSlider.Indicator
53
+ data-indicator
54
+ className={cn('rounded-full bg-gray-600 dark:bg-zinc-400 select-none', {
55
+ 'bg-gray-200 dark:bg-zinc-700': props.disabled && variant === 'default',
56
+ })}
57
+ />
58
+ <BaseSlider.Thumb
59
+ data-thumb
60
+ onBlur={onBlur}
61
+ onFocus={onFocus}
62
+ className={cn(
63
+ 'size-4 appearance-none rounded-full bg-white dark:bg-zinc-200 shadow-lg ring-1 ring-gray-200 dark:ring-zinc-800 outline-none',
64
+ 'has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-info/60 dark:has-[:focus-visible]:ring-info',
65
+ {
66
+ 'cursor-ew-resize hover:ring-offset-2 transition ease-in-out': !props.disabled,
67
+ },
68
+ { 'cursor-not-allowed bg-gray-100': props.disabled },
69
+ )}
70
+ />
71
+ </BaseSlider.Track>
72
+ </BaseSlider.Control>
73
+ </BaseSlider.Root>
74
+ )
75
+ }
@@ -0,0 +1 @@
1
+ export * from './star'
@@ -0,0 +1,99 @@
1
+ import React, { useRef } from 'react'
2
+ import { cn } from '../helpers'
3
+ import { StarIcon } from '@phosphor-icons/react'
4
+
5
+ export interface StarChangeEvent {
6
+ shiftKey: boolean
7
+ }
8
+
9
+ export type StarProps = {
10
+ checked: boolean
11
+ onChange: (checked: boolean, event: StarChangeEvent) => void
12
+ name: string
13
+ className?: string
14
+ disabled?: boolean
15
+ size?: 'sm' | 'md' | 'lg'
16
+ tabIndex?: number
17
+ }
18
+
19
+ export const Star = ({
20
+ checked,
21
+ onChange,
22
+ name,
23
+ className,
24
+ disabled = false,
25
+ size = 'md',
26
+ tabIndex = 0,
27
+ }: StarProps) => {
28
+ const shiftKeyRef = useRef(false)
29
+
30
+ const handleClick = (e: React.MouseEvent) => {
31
+ e.stopPropagation()
32
+ if (disabled) {
33
+ return
34
+ }
35
+ shiftKeyRef.current = e.shiftKey
36
+ onChange(!checked, { shiftKey: shiftKeyRef.current })
37
+ }
38
+
39
+ const handleKeyDown = (e: React.KeyboardEvent) => {
40
+ if (disabled) {
41
+ return
42
+ }
43
+ if (e.key === ' ' || e.key === 'Enter') {
44
+ e.preventDefault()
45
+ shiftKeyRef.current = e.shiftKey
46
+ onChange(!checked, { shiftKey: shiftKeyRef.current })
47
+ }
48
+ }
49
+
50
+ const sizeClasses = {
51
+ sm: 'size-3',
52
+ md: 'size-4',
53
+ lg: 'size-5',
54
+ }
55
+
56
+ return (
57
+ <button
58
+ type="button"
59
+ role="checkbox"
60
+ aria-checked={checked}
61
+ name={name}
62
+ disabled={disabled}
63
+ tabIndex={tabIndex}
64
+ onClick={handleClick}
65
+ onKeyDown={handleKeyDown}
66
+ className={cn(
67
+ 'group grid cursor-pointer outline-none',
68
+ 'focus-visible:ring-2 ring-info/75 ring-offset-1 rounded-sm',
69
+ 'transition-colors duration-150',
70
+ {
71
+ 'cursor-not-allowed opacity-50': disabled,
72
+ },
73
+ className,
74
+ )}
75
+ >
76
+ <StarIcon
77
+ weight="fill"
78
+ className={cn(
79
+ sizeClasses[size],
80
+ 'col-start-1 row-start-1 transition-opacity duration-150 pointer-event-none',
81
+ {
82
+ 'text-warning': checked,
83
+ 'text-transparent': !checked,
84
+ 'group-hover:text-warning': !disabled && !checked,
85
+ },
86
+ )}
87
+ />
88
+ <StarIcon
89
+ weight="regular"
90
+ className={cn(sizeClasses[size], 'col-start-1 row-start-1 pointer-event-none', {
91
+ 'text-[#F0D360]': checked,
92
+ 'text-gray-200 group-hover:text-gray-200 dark:text-zinc-500 group-hover:dark:text-warning-darken/75':
93
+ !checked && !disabled,
94
+ 'text-gray-200 dark:text-zinc-800': !checked && disabled,
95
+ })}
96
+ />
97
+ </button>
98
+ )
99
+ }
@@ -0,0 +1 @@
1
+ export * from './switch'
@@ -0,0 +1,33 @@
1
+ import React from 'react'
2
+ import { Switch as BaseSwitch, SwitchRootProps } from '@base-ui/react'
3
+ import { cn } from '../helpers'
4
+
5
+ export type SwitchProps = SwitchRootProps
6
+
7
+ export const Switch = ({
8
+ checked,
9
+ onCheckedChange,
10
+ disabled = false,
11
+ className = '',
12
+ ...props
13
+ }: SwitchProps) => {
14
+ return (
15
+ <BaseSwitch.Root
16
+ checked={checked}
17
+ onCheckedChange={onCheckedChange}
18
+ disabled={disabled}
19
+ className={cn(
20
+ 'relative inline-flex h-6 w-11 shrink-0 items-center rounded-full bg-gray-200 dark:bg-zinc-700 transition-colors',
21
+ 'focus:outline-none focus:ring-2 focus:ring-info/50 focus:ring-offset-1 ring-offset-white dark:ring-offset-zinc-800 dark:focus:ring-info/75',
22
+ 'data-[checked]:bg-info dark:data-[checked]:bg-info/75 data-[disabled]:cursor-not-allowed data-[disabled]:opacity-60',
23
+ className,
24
+ )}
25
+ {...props}
26
+ >
27
+ <BaseSwitch.Thumb
28
+ data-thumb
29
+ className="inline-block h-4 w-4 bg-white dark:bg-zinc-100 translate-x-1 rounded-full transition-transform data-[checked]:translate-x-6"
30
+ />
31
+ </BaseSwitch.Root>
32
+ )
33
+ }
@@ -0,0 +1,15 @@
1
+ import React, { ReactNode, RefObject } from 'react'
2
+ import { useKeyboardNavigation } from './use-keyboard-navigation'
3
+
4
+ interface Props {
5
+ indexes: Array<[number, number | string]>
6
+ children: ReactNode
7
+ containerRef?: RefObject<HTMLElement | null>
8
+ data?: Array<any>
9
+ }
10
+
11
+ export const Body = ({ indexes, children, containerRef, data }: Props) => {
12
+ useKeyboardNavigation(indexes, containerRef, data)
13
+
14
+ return <>{children}</>
15
+ }
@@ -0,0 +1,48 @@
1
+ import React, { FC, ReactElement, createContext, useCallback, useContext, useMemo } from 'react'
2
+ import { useRow } from './row-context'
3
+ import { useTable } from './table-context'
4
+
5
+ export interface CellContextInterface {
6
+ index: number
7
+ isEditing: boolean
8
+ toggleIsEditing: () => void
9
+ isHovered: boolean
10
+ }
11
+
12
+ const CellContext = createContext<CellContextInterface>({} as CellContextInterface)
13
+
14
+ interface ProviderProps {
15
+ index: number
16
+ isHovered: boolean
17
+ children: ReactElement
18
+ }
19
+
20
+ const CellProvider: FC<ProviderProps> = ({ index, isHovered, children }) => {
21
+ const { select, editingCell, setEditingCell } = useTable()
22
+ const { item, index: rowIndex } = useRow()
23
+ const isEditing =
24
+ editingCell !== null &&
25
+ editingCell[0] === rowIndex &&
26
+ editingCell[1] === item.id &&
27
+ editingCell[2] === index
28
+
29
+ const toggleIsEditing = useCallback(() => {
30
+ setEditingCell((prev) => (prev ? null : [rowIndex, item.id, index]))
31
+ }, [setEditingCell, rowIndex, item?.id, index])
32
+
33
+ const value = useMemo(
34
+ () => ({
35
+ index,
36
+ isEditing: select ? isEditing : false,
37
+ isHovered,
38
+ toggleIsEditing,
39
+ }),
40
+ [index, select, isEditing, isHovered, toggleIsEditing],
41
+ )
42
+
43
+ return <CellContext.Provider value={value}>{children}</CellContext.Provider>
44
+ }
45
+
46
+ const useCell = () => useContext(CellContext)
47
+ const CellConsumer = CellContext.Consumer
48
+ export { useCell, CellProvider, CellContext, CellConsumer }
@@ -0,0 +1,83 @@
1
+ import React, { memo, useState } from 'react'
2
+ import { Cell as BaseCell } from '@table-library/react-table-library/table'
3
+ import { CellTree } from '@table-library/react-table-library/tree'
4
+ import { CellProps as BaseCellProps } from '@table-library/react-table-library/types/table'
5
+ import { cn } from '../helpers'
6
+ import { CellConsumer, CellProvider } from './cell-context'
7
+ import { useRow } from './row-context'
8
+ import { useRowDisabled } from './row-disabled-context'
9
+ import { useTable } from './table-context'
10
+
11
+ const Divider = () => {
12
+ const { isSelected, isHovered } = useRow()
13
+ return (
14
+ <div className="absolute top-0 right-0 w-1 h-full py-1 shrink-0">
15
+ <div
16
+ className={cn('w-1 h-full border-r border-gray-50 dark:border-zinc-800', {
17
+ 'border-gray-100 dark:border-zinc-800': isSelected || isHovered,
18
+ })}
19
+ ></div>
20
+ </div>
21
+ )
22
+ }
23
+
24
+ export interface CellProps extends BaseCellProps {
25
+ divider?: boolean
26
+ tree?: boolean
27
+ }
28
+
29
+ export const Cell = memo(function Cell({
30
+ children,
31
+ className,
32
+ divider = true,
33
+ tree = false,
34
+ ...props
35
+ }: CellProps) {
36
+ const { columns, select, editingCell, density } = useTable()
37
+ const { isSelected: isRowSelected, isHovered: isRowHovered, item, index: rowIndex } = useRow()
38
+ const isRowDisabled = useRowDisabled()
39
+ const [isHovered, setIsHovered] = useState(false)
40
+ const isLastColumn = props.index === columns
41
+ const cellIndex = props.index - (select ? 1 : 0)
42
+ const nextCellIsEditing =
43
+ editingCell !== null &&
44
+ editingCell[0] === rowIndex &&
45
+ editingCell[1] === item.id &&
46
+ editingCell[2] === cellIndex + 1
47
+ const Wrapper = tree ? CellTree : BaseCell
48
+
49
+ return (
50
+ <Wrapper
51
+ item={tree ? item : undefined}
52
+ onMouseEnter={() => setIsHovered(true)}
53
+ onMouseLeave={() => setIsHovered(false)}
54
+ className="border-b border-gray-50 dark:border-zinc-800 relative"
55
+ {...props}
56
+ >
57
+ <CellProvider isHovered={isHovered} index={cellIndex}>
58
+ <CellConsumer>
59
+ {({ isEditing }) => {
60
+ return (
61
+ <>
62
+ <div
63
+ className={cn(
64
+ 'flex flex-row items-center text-xs font-medium px-1.5 flex-1',
65
+ density === 'roomy' ? 'h-11' : 'h-8',
66
+ {
67
+ 'bg-gray-50 dark:bg-zinc-800': isRowSelected || isRowHovered,
68
+ 'text-gray-300 dark:text-zinc-500': isRowDisabled,
69
+ },
70
+ className,
71
+ )}
72
+ >
73
+ {children}
74
+ </div>
75
+ {divider && !isLastColumn && !isEditing && !nextCellIsEditing && <Divider />}
76
+ </>
77
+ )
78
+ }}
79
+ </CellConsumer>
80
+ </CellProvider>
81
+ </Wrapper>
82
+ )
83
+ })
@@ -0,0 +1,28 @@
1
+ import React, { ReactNode } from 'react'
2
+ import { cn } from '../helpers'
3
+ import { getRowHeight } from './table'
4
+ import { useTable } from './table-context'
5
+
6
+ interface GroupEmptyStateProps {
7
+ children: ReactNode
8
+ className?: string
9
+ }
10
+
11
+ export const GroupEmptyState = ({ children, className }: GroupEmptyStateProps) => {
12
+ const { columns, select, density } = useTable()
13
+
14
+ return (
15
+ <div
16
+ className={cn(
17
+ 'w-full bg-white dark:bg-zinc-950 flex flex-row items-center justify-center px-2 text-xs text-gray-300 dark:text-zinc-200 font-normal border-b border-gray-100 dark:border-zinc-800',
18
+ className,
19
+ )}
20
+ style={{
21
+ gridColumn: `span ${columns + (select ? 1 : 0)}`,
22
+ height: getRowHeight(density) * 2,
23
+ }}
24
+ >
25
+ {children}
26
+ </div>
27
+ )
28
+ }