@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
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@toolpath/ui",
3
+ "version": "0.1.0",
4
+ "description": "Toolpath React and Tailwind UI primitives",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/toolpath/toolpath.git",
9
+ "directory": "packages/ui"
10
+ },
11
+ "homepage": "https://developers.toolpath.com",
12
+ "bugs": {
13
+ "url": "https://github.com/toolpath/toolpath/issues"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public",
17
+ "registry": "https://registry.npmjs.org"
18
+ },
19
+ "type": "module",
20
+ "main": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js"
26
+ },
27
+ "./theme.css": "./theme.css",
28
+ "./package.json": "./package.json"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "src",
33
+ "theme.css",
34
+ "LICENSE",
35
+ "README.md"
36
+ ],
37
+ "sideEffects": false,
38
+ "peerDependencies": {
39
+ "react": "^19.2.0",
40
+ "react-dom": "^19.2.0",
41
+ "tailwindcss": "^4.0.0"
42
+ },
43
+ "dependencies": {
44
+ "@base-ui/react": "1.2.0",
45
+ "@phosphor-icons/react": "2.1.10",
46
+ "@table-library/react-table-library": "^4.1.15",
47
+ "clsx": "^2.1.1",
48
+ "lodash-es": "^4.18.1",
49
+ "react-resizable-panels": "4.9.0",
50
+ "tailwind-merge": "^3.4.0"
51
+ },
52
+ "devDependencies": {
53
+ "@testing-library/jest-dom": "^7.0.1",
54
+ "@testing-library/react": "^16.3.2",
55
+ "@types/node": "^24.10.1",
56
+ "@types/react": "^19.0.0",
57
+ "@types/react-dom": "^19.0.0",
58
+ "jsdom": "^30.0.1",
59
+ "tsup": "^8.5.1",
60
+ "typescript": "5.9.3",
61
+ "vitest": "^4.1.10"
62
+ },
63
+ "scripts": {
64
+ "build": "tsup src/index.ts --format esm --dts --clean --external react --external react-dom",
65
+ "check-types": "tsc --noEmit",
66
+ "test": "pnpm build && vitest run"
67
+ }
68
+ }
@@ -0,0 +1,13 @@
1
+ import React, { HTMLAttributes, ReactNode } from 'react'
2
+ import { cn } from '../helpers'
3
+ import { capitalize } from 'lodash-es'
4
+
5
+ export type AttributionActionProps = HTMLAttributes<HTMLSpanElement> & {
6
+ children?: ReactNode
7
+ }
8
+
9
+ export const Action = ({ className, children = 'created', ...props }: AttributionActionProps) => (
10
+ <span className={cn(className)} {...props}>
11
+ {typeof children === 'string' ? capitalize(children) : children}
12
+ </span>
13
+ )
@@ -0,0 +1,28 @@
1
+ import React, { HTMLAttributes, useMemo } from 'react'
2
+ import { cn } from '../helpers'
3
+ import { Tooltip } from '../tooltip/tooltip'
4
+ import { formatFullDate, formatRelativeTime } from './format-time'
5
+
6
+ export type AttributionAtProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children'> & {
7
+ date: string | Date
8
+ hideTime?: boolean
9
+ }
10
+
11
+ export const At = ({ date, hideTime = false, className, ...props }: AttributionAtProps) => {
12
+ const parsed = useMemo(() => new Date(date), [date])
13
+
14
+ const formattedTime = useMemo(() => formatRelativeTime(parsed, hideTime), [parsed, hideTime])
15
+
16
+ const fullDate = useMemo(() => formatFullDate(parsed), [parsed])
17
+
18
+ return (
19
+ <>
20
+ {' '}
21
+ <Tooltip tip={fullDate}>
22
+ <span className={cn('inline-block', className)} {...props}>
23
+ {formattedTime}
24
+ </span>
25
+ </Tooltip>
26
+ </>
27
+ )
28
+ }
@@ -0,0 +1,16 @@
1
+ import React, { HTMLAttributes } from 'react'
2
+ import { cn } from '../helpers'
3
+
4
+ export type AttributionProps = HTMLAttributes<HTMLDivElement>
5
+
6
+ export const Attribution = ({ className, children, ...props }: AttributionProps) => (
7
+ <div
8
+ className={cn(
9
+ 'truncate break-words text-xs leading-4 font-semibold tracking-normal text-gray-300 dark:text-zinc-400 md:line-clamp-2 md:whitespace-normal',
10
+ className,
11
+ )}
12
+ {...props}
13
+ >
14
+ {children}
15
+ </div>
16
+ )
@@ -0,0 +1,21 @@
1
+ import React, { HTMLAttributes, ReactNode } from 'react'
2
+ import { cn } from '../helpers'
3
+
4
+ export type AttributionByProps = HTMLAttributes<HTMLSpanElement> & {
5
+ children?: ReactNode
6
+ }
7
+
8
+ export const By = ({ className, children, ...props }: AttributionByProps) => {
9
+ if (children == null) {
10
+ return null
11
+ }
12
+
13
+ return (
14
+ <>
15
+ {' by '}
16
+ <span className={cn(className)} {...props}>
17
+ {children}
18
+ </span>
19
+ </>
20
+ )
21
+ }
@@ -0,0 +1,48 @@
1
+ const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
2
+
3
+ const formatHour = (date: Date): string => {
4
+ const hours = date.getHours()
5
+ const minutes = date.getMinutes()
6
+ const ampm = hours >= 12 ? 'PM' : 'AM'
7
+ const h = hours % 12 || 12
8
+ const mm = minutes.toString().padStart(2, '0')
9
+ return `${h}:${mm} ${ampm}`
10
+ }
11
+
12
+ export const formatFullDate = (date: Date): string =>
13
+ `${MONTHS[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()} at ${formatHour(date)}`
14
+
15
+ export const formatRelativeTime = (
16
+ date: Date,
17
+ hideTime: boolean,
18
+ now: Date = new Date(),
19
+ ): string => {
20
+ const diffMs = now.getTime() - date.getTime()
21
+ const diffMinutes = Math.floor(diffMs / 60_000)
22
+ const diffHours = Math.floor(diffMs / 3_600_000)
23
+ const diffDays = Math.floor(diffMs / 86_400_000)
24
+
25
+ if (diffMinutes < 1) {
26
+ return 'Just now'
27
+ }
28
+
29
+ if (diffHours < 1) {
30
+ return `${diffMinutes}m ago`
31
+ }
32
+
33
+ if (diffDays < 1) {
34
+ return `${diffHours}h ago`
35
+ }
36
+
37
+ if (diffDays < 2) {
38
+ return `Yesterday${!hideTime ? ` at ${formatHour(date)}` : ''}`
39
+ }
40
+
41
+ const isWithinYear = Math.floor(diffMs / (365.25 * 86_400_000)) < 1
42
+
43
+ if (isWithinYear) {
44
+ return `${MONTHS[date.getMonth()]} ${date.getDate()}${!hideTime ? ` at ${formatHour(date)}` : ''}`
45
+ }
46
+
47
+ return `${MONTHS[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}`
48
+ }
@@ -0,0 +1,20 @@
1
+ import { ReactElement } from 'react'
2
+ import { Action, AttributionActionProps } from './action'
3
+ import { At, AttributionAtProps } from './at'
4
+ import { AttributionProps, Attribution as AttributionRoot } from './attribution'
5
+ import { AttributionByProps, By } from './by'
6
+
7
+ type AttributionCompoundType = {
8
+ (props: AttributionProps): ReactElement
9
+ Action: typeof Action
10
+ At: typeof At
11
+ By: typeof By
12
+ }
13
+
14
+ export const Attribution: AttributionCompoundType = Object.assign(AttributionRoot, {
15
+ Action,
16
+ At,
17
+ By,
18
+ })
19
+
20
+ export type { AttributionProps, AttributionActionProps, AttributionAtProps, AttributionByProps }
@@ -0,0 +1,38 @@
1
+ import React, { ReactNode } from 'react'
2
+ import { cn } from '../helpers'
3
+
4
+ export type BadgeProps = {
5
+ children: ReactNode
6
+ size?: 'sm' | 'md' | 'lg'
7
+ variant?: 'default' | 'secondary' | 'success' | 'primary' | 'info' | 'danger'
8
+ className?: string
9
+ }
10
+
11
+ export const Badge = ({ children, size = 'md', variant = 'default', className }: BadgeProps) => {
12
+ return (
13
+ <span
14
+ className={cn(
15
+ 'inline-flex items-center justify-center whitespace-nowrap border font-semibold tracking-wide',
16
+ {
17
+ 'h-3 px-1 text-3xs rounded-sm': size === 'sm',
18
+ 'h-4 px-1.5 text-2xs rounded': size === 'md',
19
+ 'h-6 px-2 text-xs rounded': size === 'lg',
20
+ },
21
+ {
22
+ 'border-gray bg-gray/25 text-gray dark:border-zinc-800 dark:bg-zinc-800/50 dark:text-zinc-100':
23
+ variant === 'default',
24
+ 'border-gray-300 bg-gray-300/25 text-gray-400 dark:border-zinc-700 dark:bg-zinc-700/25 dark:text-zinc-200':
25
+ variant === 'secondary',
26
+ 'border-success bg-success/25 text-success dark:text-success': variant === 'success',
27
+ 'border-primary bg-primary/25 text-primary dark:text-primary': variant === 'primary',
28
+ 'border-info bg-info/25 text-info dark:text-info': variant === 'info',
29
+ 'border-danger bg-danger/25 text-danger dark:text-danger': variant === 'danger',
30
+ },
31
+ 'data-[content]:leading-none',
32
+ className,
33
+ )}
34
+ >
35
+ <span data-content>{children}</span>
36
+ </span>
37
+ )
38
+ }
@@ -0,0 +1 @@
1
+ export * from './badge'
@@ -0,0 +1,26 @@
1
+ import React, { createContext, ReactNode, useContext } from 'react'
2
+
3
+ type Breakpoint = 'sm' | 'md' | 'lg' | 'xl'
4
+
5
+ export type BreadcrumbsContextValue = {
6
+ breakpoint: Breakpoint
7
+ }
8
+
9
+ const BreadcrumbsContext = createContext<BreadcrumbsContextValue>({
10
+ breakpoint: 'md',
11
+ })
12
+
13
+ export const useBreadcrumbs = () => {
14
+ return useContext(BreadcrumbsContext)
15
+ }
16
+
17
+ export type BreadcrumbsProviderProps = {
18
+ children: ReactNode
19
+ breakpoint?: Breakpoint
20
+ }
21
+
22
+ export const BreadcrumbsProvider = ({ children, breakpoint = 'md' }: BreadcrumbsProviderProps) => {
23
+ return (
24
+ <BreadcrumbsContext.Provider value={{ breakpoint }}>{children}</BreadcrumbsContext.Provider>
25
+ )
26
+ }
@@ -0,0 +1,24 @@
1
+ import React, { ReactNode } from 'react'
2
+ import { cn } from '../helpers'
3
+ import { BreadcrumbsProvider } from './breadcrumbs-context'
4
+
5
+ type Breakpoint = 'sm' | 'md' | 'lg' | 'xl'
6
+
7
+ export type BreadcrumbsProps = {
8
+ children: ReactNode
9
+ breakpoint?: Breakpoint
10
+ className?: string
11
+ }
12
+
13
+ export const BreadcrumbsRoot = ({ children, breakpoint = 'md', className }: BreadcrumbsProps) => (
14
+ <BreadcrumbsProvider breakpoint={breakpoint}>
15
+ <ul
16
+ className={cn(
17
+ 'flex whitespace-nowrap text-sm font-medium text-gray-300 dark:text-zinc-500',
18
+ className,
19
+ )}
20
+ >
21
+ {children}
22
+ </ul>
23
+ </BreadcrumbsProvider>
24
+ )
@@ -0,0 +1,12 @@
1
+ import { ReactElement } from 'react'
2
+ import { BreadcrumbsRoot, BreadcrumbsProps } from './breadcrumbs'
3
+ import { Item, BreadcrumbsItemProps } from './item'
4
+
5
+ type BreadcrumbsCompoundType = {
6
+ (props: BreadcrumbsProps): ReactElement
7
+ Item: typeof Item
8
+ }
9
+
10
+ export const Breadcrumbs: BreadcrumbsCompoundType = Object.assign(BreadcrumbsRoot, { Item })
11
+
12
+ export type { BreadcrumbsProps, BreadcrumbsItemProps }
@@ -0,0 +1,85 @@
1
+ import React, { ReactNode } from 'react'
2
+ import { cn } from '../helpers'
3
+ import { CaretRightIcon } from '@phosphor-icons/react'
4
+ import { Link } from '../link'
5
+ import { useBreadcrumbs } from './breadcrumbs-context'
6
+
7
+ export type BreadcrumbsItemProps = {
8
+ to: string
9
+ children: ReactNode
10
+ current?: boolean
11
+ previous?: boolean
12
+ className?: string
13
+ }
14
+
15
+ export const Item = ({
16
+ to,
17
+ children,
18
+ current = false,
19
+ previous = false,
20
+ className,
21
+ }: BreadcrumbsItemProps) => {
22
+ const { breakpoint } = useBreadcrumbs()
23
+
24
+ return (
25
+ <li
26
+ className={cn(
27
+ 'hidden items-center',
28
+ {
29
+ 'sm:flex': breakpoint === 'sm',
30
+ 'md:flex': breakpoint === 'md',
31
+ 'lg:flex': breakpoint === 'lg',
32
+ 'xl:flex': breakpoint === 'xl',
33
+ flex: previous,
34
+ hidden: !previous,
35
+ },
36
+ className,
37
+ )}
38
+ >
39
+ {current ? (
40
+ <span data-content className="flex truncate pl-2 text-gray-300 dark:text-zinc-500">
41
+ {children}
42
+ </span>
43
+ ) : (
44
+ <>
45
+ <Link
46
+ href={to}
47
+ data-content
48
+ className={cn(
49
+ 'no-underline hover:no-underline',
50
+ 'flex items-center rounded px-2 text-gray-300 dark:text-zinc-500',
51
+ 'focus:outline-none focus-visible:ring-2 focus-visible:ring-info/75',
52
+ )}
53
+ >
54
+ <>
55
+ {previous && (
56
+ <CaretRightIcon
57
+ data-previous
58
+ className={cn(
59
+ 'flex size-5 rotate-180 truncate text-gray-300 dark:text-zinc-500',
60
+ {
61
+ 'sm:hidden': breakpoint === 'sm',
62
+ 'md:hidden': breakpoint === 'md',
63
+ 'lg:hidden': breakpoint === 'lg',
64
+ 'xl:hidden': breakpoint === 'xl',
65
+ },
66
+ )}
67
+ />
68
+ )}
69
+ {children}
70
+ </>
71
+ </Link>
72
+ <CaretRightIcon
73
+ data-separator
74
+ className={cn('hidden size-5 text-gray-300 dark:text-zinc-500', {
75
+ 'sm:flex': breakpoint === 'sm',
76
+ 'md:flex': breakpoint === 'md',
77
+ 'lg:flex': breakpoint === 'lg',
78
+ 'xl:flex': breakpoint === 'xl',
79
+ })}
80
+ />
81
+ </>
82
+ )}
83
+ </li>
84
+ )
85
+ }
@@ -0,0 +1,44 @@
1
+ import React from 'react'
2
+ import { Svg } from '../common'
3
+ import { cn } from '../helpers'
4
+
5
+ export type AnimatedEllipsisProps = {
6
+ className?: string
7
+ }
8
+
9
+ export const AnimatedEllipsis = ({ className }: AnimatedEllipsisProps) => (
10
+ <div className="mx-auto inline-block">
11
+ <div className="flex items-center">
12
+ <Svg
13
+ viewBox="0 0 24 24"
14
+ className={cn(
15
+ 'opacity-85 mx-1 h-3 w-3',
16
+ className,
17
+ 'animate-[pulse_1.5s_ease-in-out_infinite]',
18
+ )}
19
+ >
20
+ <circle cx="12" cy="12" r="12" />
21
+ </Svg>
22
+ <Svg
23
+ viewBox="0 0 24 24"
24
+ className={cn(
25
+ 'opacity-85 mx-1 h-3 w-3',
26
+ className,
27
+ 'animate-[pulse_1.5s_ease-in-out_0.5s_infinite]',
28
+ )}
29
+ >
30
+ <circle cx="12" cy="12" r="12" />
31
+ </Svg>
32
+ <Svg
33
+ viewBox="0 0 24 24"
34
+ className={cn(
35
+ 'opacity-85 mx-1 h-3 w-3',
36
+ className,
37
+ 'animate-[pulse_1.5s_ease-in-out_1.5s_infinite]',
38
+ )}
39
+ >
40
+ <circle cx="12" cy="12" r="12" />
41
+ </Svg>
42
+ </div>
43
+ </div>
44
+ )
@@ -0,0 +1,168 @@
1
+ import React, { Dispatch, HTMLAttributes, MouseEvent, ReactNode, Ref } from 'react'
2
+ import { cn } from '../helpers'
3
+ import { Link } from '../link'
4
+ import { AnimatedEllipsis } from './animated-ellipsis'
5
+
6
+ export type ButtonProps = HTMLAttributes<HTMLButtonElement | HTMLAnchorElement> & {
7
+ children: ReactNode
8
+ variant?: 'default' | 'primary' | 'secondary' | 'info' | 'muted' | 'danger' | 'success'
9
+ size?: 'sm' | 'md' | 'lg'
10
+ shape?: 'default' | 'pill'
11
+ disabled?: boolean
12
+ isLoading?: boolean
13
+ onClick?: Dispatch<MouseEvent>
14
+ type?: 'button' | 'submit' | 'reset'
15
+ className?: string
16
+ asLink?: boolean
17
+ href?: string
18
+ form?: string
19
+ target?: string
20
+ full?: boolean
21
+ ref?: Ref<HTMLButtonElement | HTMLAnchorElement>
22
+ }
23
+
24
+ export const Button = ({
25
+ variant = 'default',
26
+ size = 'md',
27
+ shape = 'default',
28
+ type = 'button',
29
+ className,
30
+ isLoading = false,
31
+ children,
32
+ onClick,
33
+ asLink = false,
34
+ href,
35
+ form,
36
+ full = false,
37
+ disabled,
38
+ ref,
39
+ ...props
40
+ }: ButtonProps) => {
41
+ const isMuted = variant === 'muted' && !disabled
42
+ const isPrimary = variant === 'primary' && !disabled
43
+ const isSuccess = variant === 'success' && !disabled
44
+ const isSecondary = variant === 'secondary' && !disabled
45
+ const isInfo = variant === 'info' && !disabled
46
+ const isDanger = variant === 'danger' && !disabled
47
+ const isColoredVariant =
48
+ variant === 'primary' || variant === 'success' || variant === 'info' || variant === 'danger'
49
+
50
+ const isPill = shape === 'pill'
51
+
52
+ const buttonClasses = cn(
53
+ 'flex flex-col items-center group rounded outline-none font-semibold',
54
+ 'cursor-pointer select-none transition duration-200 ease-in-out',
55
+ 'focus-visible:ring-2 focus-visible:ring-info/75',
56
+ 'text-gray dark:text-zinc-50 focus-visible:bg-gray-100 dark:focus-visible:bg-zinc-800',
57
+ {
58
+ 'text-white focus-visible:bg-success': isSuccess,
59
+ 'text-white focus-visible:bg-primary': isPrimary,
60
+ 'text-white focus-visible:bg-info': isInfo,
61
+ 'focus-visible:bg-gray-50': isMuted,
62
+ 'text-white focus-visible:bg-danger': isDanger,
63
+ 'text-gray-400 dark:text-zinc-200 focus-visible:bg-white dark:focus-visible:bg-zinc-800 focus-visible:rounded-md':
64
+ isSecondary,
65
+ 'text-gray-300 dark:text-zinc-400 cursor-not-allowed': disabled && !isColoredVariant,
66
+ 'text-white/90 dark:text-zinc-100 cursor-not-allowed': disabled && isColoredVariant,
67
+ 'text-gray-200 dark:text-zinc-500': variant === 'muted' && disabled,
68
+ 'block text-center': asLink,
69
+ 'rounded-lg': size === 'lg' && !isPill,
70
+ 'rounded-full focus-visible:rounded-full': isPill,
71
+ 'w-full': full,
72
+ },
73
+ )
74
+
75
+ const clickHandler = (event: React.MouseEvent): void => {
76
+ if (typeof onClick === 'function') {
77
+ if (!asLink) {
78
+ event.preventDefault()
79
+ }
80
+
81
+ onClick(event)
82
+ }
83
+ }
84
+
85
+ const ContentWrapper = ({ children }: { children: ReactNode }) => (
86
+ <div
87
+ className={cn(
88
+ 'whitespace-nowrap rounded px-3 py-1 font-body text-xs font-semibold tracking-normal',
89
+ 'bg-gray-100 group-hover:bg-gray-200/75 dark:bg-zinc-800 dark:group-hover:bg-zinc-900',
90
+ 'transition duration-200 ease-in-out',
91
+ {
92
+ 'bg-primary group-hover:bg-primary-darken group-active:bg-primary-darken dark:bg-primary dark:group-hover:bg-primary-darken dark:group-active:bg-primary-darken':
93
+ isPrimary,
94
+ 'bg-success group-hover:bg-success-darken group-active:bg-success-darken dark:bg-success dark:group-hover:bg-success-darken dark:group-active:bg-success-darken':
95
+ isSuccess,
96
+ 'bg-info group-hover:bg-info group-active:bg-info dark:bg-info dark:group-hover:bg-info dark:group-active:bg-info':
97
+ isInfo,
98
+ 'bg-gray-50 group-hover:bg-gray-100 group-active:bg-gray-100 dark:bg-zinc-800 dark:group-hover:bg-zinc-900 dark:group-active:bg-zinc-900':
99
+ isMuted,
100
+ 'bg-danger group-hover:bg-danger-darken group-active:bg-danger-darken dark:bg-danger dark:group-hover:bg-danger-darken dark:group-active:bg-danger-darken':
101
+ isDanger,
102
+ 'border border-gray-100 bg-white dark:bg-zinc-900 dark:border-zinc-800 group-hover:bg-gray-50 dark:group-hover:bg-zinc-800 group-active:bg-gray-50 dark:group-active:bg-zinc-800 dark:group-hover:border-zinc-800':
103
+ isSecondary,
104
+ 'bg-primary/50 dark:bg-primary/45': disabled && variant === 'primary',
105
+ 'bg-success/50 dark:bg-success/45': disabled && variant === 'success',
106
+ 'bg-info/50 dark:bg-info/45': disabled && variant === 'info',
107
+ 'bg-danger/50 dark:bg-danger/45': disabled && variant === 'danger',
108
+ 'dark:bg-zinc-900 group-hover:bg-gray-100 dark:group-hover:bg-zinc-900 dark:group-active:bg-zinc-900':
109
+ disabled && !isColoredVariant,
110
+ 'group-active:scale-95': !disabled,
111
+ 'px-1.5 py-px text-2xs': size === 'sm',
112
+ 'px-4 py-2 text-base': size === 'lg',
113
+ 'rounded-lg': size === 'lg' && !isPill,
114
+ 'rounded-full': isPill,
115
+ 'px-8': isPill && size === 'lg',
116
+ 'w-full': full,
117
+ },
118
+ className,
119
+ )}
120
+ >
121
+ {children}
122
+ </div>
123
+ )
124
+
125
+ const content = !isLoading ? (
126
+ <ContentWrapper>{children}</ContentWrapper>
127
+ ) : (
128
+ <ContentWrapper>
129
+ <AnimatedEllipsis
130
+ className={cn({
131
+ 'h-2 w-1': size === 'sm',
132
+ 'h-2 w-1.5': size === 'md',
133
+ 'h-3 w-2': size === 'lg',
134
+ })}
135
+ />
136
+ </ContentWrapper>
137
+ )
138
+
139
+ if (asLink) {
140
+ return (
141
+ <Link
142
+ ref={ref as Ref<HTMLAnchorElement>}
143
+ href={disabled ? '#' : href}
144
+ aria-disabled={disabled}
145
+ onClick={clickHandler}
146
+ className={cn('text-inherit no-underline hover:no-underline', buttonClasses)}
147
+ {...props}
148
+ >
149
+ {content}
150
+ </Link>
151
+ )
152
+ }
153
+
154
+ return (
155
+ <button
156
+ ref={ref as Ref<HTMLButtonElement>}
157
+ aria-label={isLoading ? 'Loading...' : ''}
158
+ type={type}
159
+ form={type === 'submit' ? form : undefined}
160
+ onClick={clickHandler}
161
+ className={buttonClasses}
162
+ {...props}
163
+ disabled={disabled}
164
+ >
165
+ {content}
166
+ </button>
167
+ )
168
+ }
@@ -0,0 +1 @@
1
+ export * from './button'