@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,126 @@
1
+ import React, { ButtonHTMLAttributes, createContext, ReactNode, Ref, useContext } from 'react'
2
+ import { cn } from '../helpers'
3
+
4
+ type IconButtonSize = 'sm' | 'md' | 'lg' | 'xl'
5
+
6
+ const IconButtonContext = createContext<{ size: IconButtonSize }>({
7
+ size: 'md',
8
+ })
9
+
10
+ export interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
11
+ variant?: 'default' | 'muted' | 'secondary'
12
+ size?: IconButtonSize
13
+ children: ReactNode
14
+ toggled?: boolean
15
+ invalid?: boolean
16
+ deemphasize?: boolean
17
+ ref?: Ref<HTMLButtonElement>
18
+ }
19
+
20
+ export interface IconButtonIndicatorProps {
21
+ variant?: 'info' | 'success' | 'primary' | 'danger' | 'warning'
22
+ children?: ReactNode
23
+ }
24
+
25
+ const IconButtonIndicator = ({ variant = 'info', children }: IconButtonIndicatorProps) => {
26
+ const { size } = useContext(IconButtonContext)
27
+ const hasText = children !== undefined && children !== null && size !== 'sm'
28
+
29
+ return (
30
+ <span
31
+ aria-hidden="true"
32
+ className={cn(
33
+ 'absolute flex items-center justify-center rounded-full font-bold text-white select-none',
34
+ {
35
+ 'bg-info': variant === 'info',
36
+ 'bg-success': variant === 'success',
37
+ 'bg-primary': variant === 'primary',
38
+ 'bg-danger': variant === 'danger',
39
+ 'bg-warning': variant === 'warning',
40
+ },
41
+ hasText
42
+ ? {
43
+ '-right-2 -top-2 h-4 min-w-4 px-1 text-2xs': size === 'xl',
44
+ '-right-1.5 -top-1.5 h-3.5 min-w-3.5 px-0.5 text-3xs': size === 'lg',
45
+ '-right-1 -top-1 h-3 min-w-3 px-0.5 text-3xs': size === 'md',
46
+ }
47
+ : {
48
+ '-right-0.5 -top-0.5 size-2.5': size === 'xl',
49
+ '-right-0.5 -top-0.5 size-2': size === 'lg',
50
+ '-right-0.5 -top-0.5 size-1.5': size === 'md',
51
+ '-right-px -top-px size-1': size === 'sm',
52
+ },
53
+ )}
54
+ >
55
+ {hasText && children}
56
+ </span>
57
+ )
58
+ }
59
+
60
+ const IconButtonRoot = ({
61
+ variant = 'default',
62
+ size = 'md',
63
+ disabled = false,
64
+ children,
65
+ className,
66
+ toggled = false,
67
+ invalid = false,
68
+ deemphasize = false,
69
+ ref,
70
+ ...props
71
+ }: IconButtonProps) => {
72
+ return (
73
+ <IconButtonContext.Provider value={{ size }}>
74
+ <button
75
+ ref={ref}
76
+ type="button"
77
+ disabled={disabled}
78
+ data-disabled={disabled}
79
+ data-toggled={toggled || undefined}
80
+ data-invalid={invalid || undefined}
81
+ {...props}
82
+ className={cn(
83
+ 'flex justify-center items-center rounded-sm shrink-0',
84
+ 'transition-transform duraiton-150 ease-in-out',
85
+ 'outline-none focus-visible:ring-2 ring-info/75',
86
+ {
87
+ 'bg-[#FFD4E0]/50 border border-[#FFD4E0]': invalid && !disabled,
88
+ '!bg-info-lighten text-gray-500 dark:!bg-info/20 dark:ring-1 dark:ring-info/40 dark:text-zinc-50':
89
+ toggled && !invalid,
90
+ 'hover:bg-gray-100 dark:hover:bg-zinc-900 active:bg-gray-100 dark:active:bg-zinc-900 active:scale-95':
91
+ !toggled && !disabled && !invalid && variant !== 'secondary',
92
+ 'border border-gray-100 bg-white dark:border-zinc-800 dark:bg-zinc-900':
93
+ variant === 'secondary' && !invalid,
94
+ 'text-gray-400 dark:text-zinc-200 hover:bg-gray-50 dark:hover:bg-zinc-800 active:bg-gray-50 dark:active:bg-zinc-800 active:scale-95':
95
+ variant === 'secondary' && !disabled && !toggled,
96
+ 'hover:!bg-info-lighten/80 dark:hover:!bg-info/25 active:!bg-info-lighten/90 dark:active:!bg-info/30 active:scale-95':
97
+ toggled && !disabled && !invalid,
98
+ 'text-gray-500 active:text-gray-700 dark:text-zinc-400 dark:hover:text-zinc-100 dark:active:text-zinc-100':
99
+ variant === 'default' && !deemphasize && !disabled && !toggled,
100
+ 'text-gray-300 hover:text-gray-400 active:text-gray-500 dark:text-zinc-400 dark:hover:text-zinc-100 dark:active:text-zinc-100':
101
+ variant === 'default' && deemphasize && !disabled && !toggled,
102
+ 'text-gray-300 hover:text-gray-400 active:text-gray-400 dark:text-zinc-400 dark:hover:text-zinc-100 dark:active:text-zinc-100':
103
+ variant === 'muted' && !deemphasize && !disabled && !toggled,
104
+ 'text-gray-200 hover:text-gray-300 active:text-gray-400 dark:text-zinc-400 dark:hover:text-zinc-100 dark:active:text-zinc-100':
105
+ variant === 'muted' && deemphasize && !disabled && !toggled,
106
+ 'text-gray-200 hover:bg-transparent active:bg-transparent active:scale-100 active:text-gray-200 cursor-not-allowed dark:text-zinc-400 dark:hover:bg-transparent dark:active:bg-transparent dark:active:scale-100 dark:active:text-zinc-400':
107
+ disabled,
108
+ 'hover:bg-white active:bg-white dark:hover:bg-zinc-900 dark:active:bg-zinc-900':
109
+ variant === 'secondary' && disabled,
110
+ 'size-8 [&_svg]:size-6 rounded': size === 'xl',
111
+ 'size-6 [&_svg]:size-4': size === 'lg',
112
+ 'size-5 [&_svg]:size-3.5': size === 'md',
113
+ 'size-4 [&_svg]:size-2.5': size === 'sm',
114
+ },
115
+ className,
116
+ )}
117
+ >
118
+ {children}
119
+ </button>
120
+ </IconButtonContext.Provider>
121
+ )
122
+ }
123
+
124
+ export const IconButton = Object.assign(IconButtonRoot, {
125
+ Indicator: IconButtonIndicator,
126
+ })
@@ -0,0 +1,2 @@
1
+ export { IconButton } from './icon-button'
2
+ export type { IconButtonProps, IconButtonIndicatorProps } from './icon-button'
package/src/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ export * from './attribution'
2
+ export * from './button'
3
+ export * from './callout'
4
+ export * from './checkbox'
5
+ export * from './combobox'
6
+ export * from './dialog'
7
+ export * from './editable-cell'
8
+ export * from './badge'
9
+ export * from './breadcrumbs'
10
+ export * from './card'
11
+ export * from './field'
12
+ export * from './icon-button'
13
+ export * from './input'
14
+ export * from './link'
15
+ export * from './menu'
16
+ export * from './notification'
17
+ export * from './quantity-input'
18
+ export * from './slider'
19
+ export * from './pin'
20
+ export * from './progress-bar'
21
+ export * from './star'
22
+ export * from './switch'
23
+ export * from './table'
24
+ export * from './textarea'
25
+ export * from './tabs'
26
+ export * from './toggle'
27
+ export * from './toast'
28
+ export * from './tooltip'
29
+ export * from './panels'
30
+ export * from './scroll-area'
31
+ export * from './pagination'
32
+ export * from './page-header'
@@ -0,0 +1 @@
1
+ export * from './input'
@@ -0,0 +1,276 @@
1
+ import React, {
2
+ ChangeEvent,
3
+ ComponentType,
4
+ FocusEvent,
5
+ InputHTMLAttributes,
6
+ ReactNode,
7
+ Ref,
8
+ SVGProps,
9
+ useEffect,
10
+ useRef,
11
+ useState,
12
+ } from 'react'
13
+ import { cn } from '../helpers'
14
+ import { IconButton } from '../icon-button'
15
+ import { CheckIcon, CopyIcon, MagnifyingGlassIcon } from '@phosphor-icons/react'
16
+ import { Tooltip } from '../tooltip'
17
+
18
+ export type InputProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'prefix'> & {
19
+ name: string
20
+ id: string
21
+ size?: 'md' | 'lg' | 'xl'
22
+ variant?: 'default' | 'secondary' | 'muted' | 'ghost'
23
+ icon?: ComponentType<SVGProps<SVGSVGElement>>
24
+ iconPosition?: 'left' | 'right'
25
+ prefix?: ReactNode
26
+ suffix?: ReactNode
27
+ textEnd?: boolean
28
+ invalid?: boolean
29
+ showSpinner?: boolean
30
+ copyable?: boolean
31
+ onCopyError?: () => void
32
+ onValueChange?: (value: string | null) => void
33
+ ref?: Ref<HTMLInputElement>
34
+ }
35
+
36
+ export const Input = ({
37
+ type = 'text',
38
+ name,
39
+ id,
40
+ value,
41
+ className,
42
+ disabled = false,
43
+ variant = 'default',
44
+ icon: IconComponent,
45
+ iconPosition = 'left',
46
+ suffix,
47
+ prefix,
48
+ size = 'lg',
49
+ textEnd = false,
50
+ invalid = false,
51
+ showSpinner = false,
52
+ copyable = false,
53
+ onCopyError,
54
+ onValueChange,
55
+ ref,
56
+ ...props
57
+ }: InputProps) => {
58
+ const [copied, setCopied] = useState(false)
59
+ const copyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
60
+
61
+ useEffect(() => {
62
+ return () => {
63
+ if (copyTimeoutRef.current) {
64
+ clearTimeout(copyTimeoutRef.current)
65
+ }
66
+ }
67
+ }, [])
68
+
69
+ const copyToClipboard = () =>
70
+ navigator.clipboard.writeText(typeof value === 'string' ? value : String(value ?? ''))
71
+
72
+ const scheduleCopiedReset = () => {
73
+ if (copyTimeoutRef.current) {
74
+ clearTimeout(copyTimeoutRef.current)
75
+ }
76
+ copyTimeoutRef.current = setTimeout(() => setCopied(false), 1500)
77
+ }
78
+
79
+ const handleCopy = async () => {
80
+ try {
81
+ await copyToClipboard()
82
+ setCopied(true)
83
+ scheduleCopiedReset()
84
+ } catch {
85
+ onCopyError?.()
86
+ }
87
+ }
88
+
89
+ const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
90
+ const value = event.target.value
91
+ if (type === 'number') {
92
+ if (value === '') {
93
+ onValueChange?.(null)
94
+ return
95
+ }
96
+
97
+ if (isNaN(Number(value))) {
98
+ return
99
+ }
100
+ }
101
+
102
+ onValueChange?.(value)
103
+ props?.onChange?.(event)
104
+ }
105
+
106
+ const handleBlur = (event: FocusEvent<HTMLInputElement>) => {
107
+ const { min, max } = props
108
+ const value = event.target.value
109
+ if (min && Number(value) < Number(min)) {
110
+ onValueChange?.(String(min))
111
+ }
112
+
113
+ if (max && Number(value) > Number(max)) {
114
+ onValueChange?.(String(max))
115
+ }
116
+
117
+ props?.onBlur?.(event)
118
+ }
119
+
120
+ const iconClassName = cn(
121
+ {
122
+ 'text-gray-400 dark:text-zinc-400':
123
+ (variant === 'muted' || variant === 'default') && !disabled,
124
+ 'text-gray-200 dark:text-zinc-400': variant === 'secondary' || disabled,
125
+ },
126
+ iconPosition === 'left'
127
+ ? {
128
+ 'ml-1 w-3': size === 'md',
129
+ '-mr-1 ml-1.5 w-4': size === 'lg',
130
+ '-mr-1 ml-2 w-5': size === 'xl',
131
+ }
132
+ : {
133
+ 'mr-1 w-3': size === 'md',
134
+ '-ml-1 mr-1.5 w-4': size === 'lg',
135
+ '-ml-1 mr-2 w-5': size === 'xl',
136
+ },
137
+ )
138
+
139
+ const iconElement =
140
+ type === 'search' && !IconComponent ? (
141
+ <MagnifyingGlassIcon data-icon className={iconClassName} />
142
+ ) : IconComponent ? (
143
+ <IconComponent data-icon className={iconClassName} />
144
+ ) : null
145
+
146
+ let placeholder = props.placeholder
147
+ if (!props.placeholder && type === 'search') {
148
+ placeholder = 'Search'
149
+ }
150
+
151
+ const numberInputProps: InputHTMLAttributes<HTMLInputElement> & {
152
+ 'data-lpignore': boolean
153
+ } = {
154
+ inputMode: 'numeric' as const,
155
+ step: 'any',
156
+ autoComplete: 'off',
157
+ autoCorrect: 'off',
158
+ spellCheck: false,
159
+ 'data-lpignore': true,
160
+ 'aria-roledescription': 'Number field',
161
+ }
162
+
163
+ return (
164
+ <div
165
+ className={cn(
166
+ 'relative flex w-full flex-nowrap items-stretch font-medium',
167
+ { 'group/input': copyable },
168
+ {
169
+ 'focus-within:ring-2 ring-info/50 dark:ring-info/75': !invalid,
170
+ 'text-gray-400 dark:text-zinc-100 border border-gray-200 bg-white focus-within:border-info dark:border-zinc-800 dark:bg-zinc-900 dark:focus-within:border-info':
171
+ variant === 'secondary',
172
+ 'bg-gray-50 dark:bg-zinc-800': variant === 'default',
173
+ 'bg-gray-100 dark:bg-zinc-800': variant === 'muted',
174
+ 'bg-transparent': variant === 'ghost',
175
+ 'text-gray-500 dark:text-zinc-100':
176
+ (variant === 'default' || variant === 'muted') && !disabled,
177
+ 'text-inherit': variant === 'ghost' && !disabled,
178
+ 'text-gray-200 dark:text-zinc-400': disabled,
179
+ 'h-6 rounded text-xs': size === 'md',
180
+ 'h-7 rounded-md text-sm': size === 'lg',
181
+ 'h-9 rounded-md text-lg': size === 'xl',
182
+ 'border border-[#FFD4E0] dark:border-danger/50 bg-[#FFD4E0]/25 dark:bg-danger/25 focus-within:border-[#FFD4E0] dark:focus-within:border-danger focus-within:ring-2 ring-[#FFD4E0] dark:ring-danger/50':
183
+ invalid && !disabled,
184
+ },
185
+ className,
186
+ )}
187
+ data-disabled={disabled || undefined}
188
+ data-invalid={invalid || undefined}
189
+ >
190
+ {prefix && (
191
+ <div
192
+ data-prefix
193
+ className={cn('flex items-center text-white font-medium', {
194
+ 'bg-gray-200': variant === 'secondary' || variant === 'muted',
195
+ 'dark:bg-zinc-800': variant === 'secondary',
196
+ 'dark:bg-zinc-700/50': variant === 'muted',
197
+ 'bg-gray-100 dark:bg-zinc-800/50': variant === 'default',
198
+ 'bg-[#FFD4E0]': invalid && !disabled,
199
+ 'rounded-l-sm px-1.5 text-xs': size === 'md',
200
+ 'rounded-l px-1.5 text-sm': size === 'lg',
201
+ 'rounded-l px-2': size === 'xl',
202
+ })}
203
+ >
204
+ {prefix}
205
+ </div>
206
+ )}
207
+ {iconPosition === 'left' && iconElement}
208
+ <input
209
+ ref={ref}
210
+ type={type}
211
+ data-testid={id}
212
+ data-input
213
+ data-1p-ignore
214
+ id={id}
215
+ placeholder={placeholder}
216
+ name={name}
217
+ value={value}
218
+ autoComplete="off"
219
+ className={cn('w-px flex-1 grow bg-transparent leading-normal', 'outline-none', {
220
+ 'search-close-icon': type === 'search',
221
+ 'cursor-not-allowed select-none': disabled,
222
+ 'text-end': textEnd,
223
+ 'placeholder-gray-200 dark:placeholder-zinc-400': variant === 'secondary',
224
+ 'placeholder-gray-300 dark:placeholder-zinc-400':
225
+ variant === 'default' || variant === 'muted' || variant === 'ghost',
226
+ 'px-1': size === 'md' && variant !== 'ghost',
227
+ 'pl-2 pr-1.5': size === 'lg' && variant !== 'ghost',
228
+ 'px-2': size === 'xl' && variant !== 'ghost',
229
+ 'px-0': variant === 'ghost',
230
+ 'appearance-none': type === 'number' && !showSpinner,
231
+ })}
232
+ onChange={handleChange}
233
+ onBlur={handleBlur}
234
+ disabled={disabled}
235
+ {...(type === 'number' ? numberInputProps : {})}
236
+ {...props}
237
+ />
238
+ {iconPosition === 'right' && iconElement}
239
+ {copyable && (
240
+ <div className="flex items-center">
241
+ <Tooltip tip="Copy to clipboard" disabled={!value}>
242
+ <IconButton
243
+ size="sm"
244
+ variant="muted"
245
+ aria-label="Copy to clipboard"
246
+ onClick={handleCopy}
247
+ className={cn(
248
+ 'transition-opacity group-hover/input:opacity-100 focus:opacity-100',
249
+ value ? 'opacity-0' : 'invisible',
250
+ )}
251
+ >
252
+ {copied ? <CheckIcon /> : <CopyIcon />}
253
+ </IconButton>
254
+ </Tooltip>
255
+ </div>
256
+ )}
257
+ {suffix && (
258
+ <div
259
+ data-suffix
260
+ className={cn('flex items-center text-white font-medium', {
261
+ 'bg-gray-200': variant === 'secondary' || variant === 'muted',
262
+ 'dark:bg-zinc-800': variant === 'secondary',
263
+ 'dark:bg-zinc-700/50': variant === 'muted',
264
+ 'bg-gray-100 dark:bg-zinc-800/50': variant === 'default',
265
+ 'bg-[#FFD4E0]': invalid && !disabled,
266
+ 'rounded-r-sm px-1.5 text-xs': size === 'md',
267
+ 'rounded-r px-1.5': size === 'lg',
268
+ 'rounded-r px-2': size === 'xl',
269
+ })}
270
+ >
271
+ {suffix}
272
+ </div>
273
+ )}
274
+ </div>
275
+ )
276
+ }
@@ -0,0 +1,4 @@
1
+ export { Link } from './link'
2
+ export type { LinkProps } from './link'
3
+ export { LinkProvider, useLinkContext } from './link-context'
4
+ export type { LinkComponentProps, LinkProviderProps } from './link-context'
@@ -0,0 +1,33 @@
1
+ import React, {
2
+ AnchorHTMLAttributes,
3
+ ComponentType,
4
+ createContext,
5
+ ReactNode,
6
+ Ref,
7
+ useContext,
8
+ } from 'react'
9
+
10
+ export type LinkComponentProps = AnchorHTMLAttributes<HTMLAnchorElement> & {
11
+ ref?: Ref<HTMLAnchorElement>
12
+ }
13
+
14
+ export type LinkContextValue = {
15
+ LinkComponent: ComponentType<LinkComponentProps>
16
+ }
17
+
18
+ const LinkContext = createContext<LinkContextValue | null>(null)
19
+
20
+ export const useLinkContext = () => {
21
+ return useContext(LinkContext)
22
+ }
23
+
24
+ export type LinkProviderProps = {
25
+ children: ReactNode
26
+ component: ComponentType<LinkComponentProps>
27
+ }
28
+
29
+ export const LinkProvider = ({ children, component }: LinkProviderProps) => {
30
+ return (
31
+ <LinkContext.Provider value={{ LinkComponent: component }}>{children}</LinkContext.Provider>
32
+ )
33
+ }
@@ -0,0 +1,24 @@
1
+ import React, { ReactNode, Ref } from 'react'
2
+ import { cn } from '../helpers'
3
+ import { LinkComponentProps, useLinkContext } from './link-context'
4
+
5
+ export type LinkProps = LinkComponentProps & {
6
+ children: ReactNode
7
+ className?: string
8
+ ref?: Ref<HTMLAnchorElement>
9
+ }
10
+
11
+ export const Link = ({ children, className, ref, ...props }: LinkProps) => {
12
+ const context = useLinkContext()
13
+ const Component = context?.LinkComponent ?? 'a'
14
+
15
+ return (
16
+ <Component
17
+ ref={ref}
18
+ className={cn('cursor-pointer text-info underline-offset-2 hover:underline', className)}
19
+ {...props}
20
+ >
21
+ {children}
22
+ </Component>
23
+ )
24
+ }
@@ -0,0 +1,17 @@
1
+ import React from 'react'
2
+ import { Menu as BaseMenu, ContextMenu, SeparatorProps } from '@base-ui/react'
3
+ import { cn } from '../helpers'
4
+ import { useMenu } from './menu-context'
5
+
6
+ export type MenuDividerProps = SeparatorProps
7
+
8
+ export const Divider = ({ className, ...props }: MenuDividerProps) => {
9
+ const { context } = useMenu()
10
+ const dividerClass = cn('mx-1 border-t border-gray-100 dark:border-zinc-800', className)
11
+
12
+ if (context) {
13
+ return <ContextMenu.Separator className={dividerClass} {...props} />
14
+ }
15
+
16
+ return <BaseMenu.Separator className={dividerClass} {...props} />
17
+ }
@@ -0,0 +1,40 @@
1
+ import { ReactElement } from 'react'
2
+ import { Divider, MenuDividerProps } from './divider'
3
+ import { Item, MenuItemProps } from './item'
4
+ import { MenuProps, Menu as MenuRoot } from './menu'
5
+ import { useMenu } from './menu-context'
6
+ import { MenuPopoverProps, Popover } from './popover'
7
+ import { MenuSubmenuRootProps, Submenu } from './submenu'
8
+ import { MenuSubmenuTriggerProps, SubmenuTrigger } from './submenu-trigger'
9
+ import { MenuTriggerProps, Trigger } from './trigger'
10
+
11
+ type MenuCompoundType = {
12
+ (props: MenuProps): ReactElement
13
+ Divider: typeof Divider
14
+ Item: typeof Item
15
+ Popover: typeof Popover
16
+ SubmenuTrigger: typeof SubmenuTrigger
17
+ Submenu: typeof Submenu
18
+ Trigger: typeof Trigger
19
+ useMenu: typeof useMenu
20
+ }
21
+
22
+ export const Menu: MenuCompoundType = Object.assign(MenuRoot, {
23
+ Divider,
24
+ Item,
25
+ Popover,
26
+ SubmenuTrigger,
27
+ Submenu,
28
+ Trigger,
29
+ useMenu,
30
+ })
31
+
32
+ export type {
33
+ MenuProps,
34
+ MenuDividerProps,
35
+ MenuItemProps,
36
+ MenuPopoverProps,
37
+ MenuSubmenuTriggerProps,
38
+ MenuSubmenuRootProps,
39
+ MenuTriggerProps,
40
+ }