@carbonid1/design-system 5.1.0 → 5.2.1
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/dist/Badge/Badge.d.ts +6 -0
- package/dist/Badge/Badge.js +23 -0
- package/{src/Badge/Badge.types.ts → dist/Badge/Badge.types.d.ts} +8 -11
- package/dist/Badge/Badge.types.js +1 -0
- package/dist/Button/Button.d.ts +7 -0
- package/dist/Button/Button.js +34 -0
- package/dist/Button/Button.types.d.ts +8 -0
- package/dist/Button/Button.types.js +1 -0
- package/dist/ContextMenu/ContextMenu.d.ts +56 -0
- package/dist/ContextMenu/ContextMenu.js +40 -0
- package/dist/Kbd/Kbd.d.ts +6 -0
- package/dist/Kbd/Kbd.js +38 -0
- package/dist/Kbd/Kbd.types.d.ts +8 -0
- package/dist/Kbd/Kbd.types.js +1 -0
- package/dist/ProgressRing/ProgressRing.consts.d.ts +4 -0
- package/dist/ProgressRing/ProgressRing.consts.js +4 -0
- package/dist/ProgressRing/ProgressRing.d.ts +10 -0
- package/dist/ProgressRing/ProgressRing.js +12 -0
- package/dist/Select/Select.d.ts +2 -0
- package/dist/Select/Select.js +85 -0
- package/dist/Select/Select.types.d.ts +26 -0
- package/dist/Select/Select.types.js +1 -0
- package/dist/Slider/Slider.d.ts +13 -0
- package/dist/Slider/Slider.js +5 -0
- package/dist/ThemeCycler/ThemeCycler.d.ts +1 -0
- package/dist/ThemeCycler/ThemeCycler.js +30 -0
- package/dist/ThemeProvider/ThemeProvider.d.ts +5 -0
- package/dist/ThemeProvider/ThemeProvider.js +4 -0
- package/dist/Toaster/Toaster.d.ts +2 -0
- package/dist/Toaster/Toaster.js +4 -0
- package/dist/Tooltip/Tooltip.d.ts +15 -0
- package/dist/Tooltip/Tooltip.js +59 -0
- package/dist/helpers/cn/cn.d.ts +2 -0
- package/dist/helpers/cn/cn.js +5 -0
- package/dist/helpers/getModKey/getModKey.d.ts +1 -0
- package/dist/helpers/getModKey/getModKey.js +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +16 -0
- package/package.json +17 -35
- package/themes/dashboard.css +4 -2
- package/themes/reader.css +4 -2
- package/src/Badge/Badge.tsx +0 -53
- package/src/Button/Button.tsx +0 -67
- package/src/Button/Button.types.ts +0 -11
- package/src/ContextMenu/ContextMenu.tsx +0 -159
- package/src/Kbd/Kbd.tsx +0 -56
- package/src/Kbd/Kbd.types.ts +0 -10
- package/src/ProgressRing/ProgressRing.consts.ts +0 -4
- package/src/ProgressRing/ProgressRing.tsx +0 -68
- package/src/Select/Select.test.tsx +0 -129
- package/src/Select/Select.tsx +0 -156
- package/src/Select/Select.types.ts +0 -30
- package/src/Slider/Slider.test.tsx +0 -29
- package/src/Slider/Slider.tsx +0 -53
- package/src/ThemeCycler/ThemeCycler.tsx +0 -37
- package/src/ThemeProvider/ThemeProvider.tsx +0 -18
- package/src/Toaster/Toaster.tsx +0 -7
- package/src/Tooltip/Tooltip.tsx +0 -107
- package/src/helpers/cn/cn.ts +0 -6
- package/src/helpers/getModKey/getModKey.test.ts +0 -23
- package/src/helpers/getModKey/getModKey.ts +0 -2
- package/src/index.ts +0 -36
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { fireEvent, render, screen } from '@testing-library/react'
|
|
2
|
-
import { StrictMode } from 'react'
|
|
3
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
4
|
-
import { Select } from './Select'
|
|
5
|
-
import type { SelectGroup } from './Select.types'
|
|
6
|
-
|
|
7
|
-
const options = [
|
|
8
|
-
{ value: 'a', label: 'Alpha' },
|
|
9
|
-
{ value: 'b', label: 'Bravo' },
|
|
10
|
-
{ value: 'c', label: 'Charlie' },
|
|
11
|
-
]
|
|
12
|
-
|
|
13
|
-
const groups: SelectGroup[] = [
|
|
14
|
-
{
|
|
15
|
-
label: 'Letters',
|
|
16
|
-
options: [
|
|
17
|
-
{ value: 'a', label: 'Alpha' },
|
|
18
|
-
{ value: 'b', label: 'Bravo' },
|
|
19
|
-
],
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
label: 'Numbers',
|
|
23
|
-
options: [
|
|
24
|
-
{ value: '1', label: 'One' },
|
|
25
|
-
{ value: '2', label: 'Two' },
|
|
26
|
-
],
|
|
27
|
-
},
|
|
28
|
-
]
|
|
29
|
-
|
|
30
|
-
const renderWith = (ui: React.ReactElement) => render(<StrictMode>{ui}</StrictMode>)
|
|
31
|
-
|
|
32
|
-
describe('Select', () => {
|
|
33
|
-
it('renders selected label and chevron in button', () => {
|
|
34
|
-
renderWith(<Select options={options} value="b" onChange={vi.fn()} />)
|
|
35
|
-
|
|
36
|
-
const button = screen.getByRole('button', { name: /bravo/i })
|
|
37
|
-
expect(button).toBeInTheDocument()
|
|
38
|
-
expect(button.querySelector('svg')).toBeInTheDocument()
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('opens menu on click showing all options', () => {
|
|
42
|
-
renderWith(<Select options={options} value="a" onChange={vi.fn()} />)
|
|
43
|
-
|
|
44
|
-
fireEvent.click(screen.getByRole('button', { name: /alpha/i }))
|
|
45
|
-
|
|
46
|
-
expect(screen.getByRole('listbox')).toBeInTheDocument()
|
|
47
|
-
expect(screen.getByRole('option', { name: /alpha/i })).toBeInTheDocument()
|
|
48
|
-
expect(screen.getByRole('option', { name: /bravo/i })).toBeInTheDocument()
|
|
49
|
-
expect(screen.getByRole('option', { name: /charlie/i })).toBeInTheDocument()
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('calls onChange and closes menu when an option is selected', () => {
|
|
53
|
-
const onChange = vi.fn()
|
|
54
|
-
renderWith(<Select options={options} value="a" onChange={onChange} />)
|
|
55
|
-
|
|
56
|
-
fireEvent.click(screen.getByRole('button', { name: /alpha/i }))
|
|
57
|
-
fireEvent.click(screen.getByRole('option', { name: /charlie/i }))
|
|
58
|
-
|
|
59
|
-
expect(onChange).toHaveBeenCalledWith('c')
|
|
60
|
-
expect(screen.queryByRole('listbox')).not.toBeInTheDocument()
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
it('closes on Escape key', () => {
|
|
64
|
-
renderWith(<Select options={options} value="a" onChange={vi.fn()} />)
|
|
65
|
-
|
|
66
|
-
const button = screen.getByRole('button', { name: /alpha/i })
|
|
67
|
-
fireEvent.click(button)
|
|
68
|
-
expect(screen.getByRole('listbox')).toBeInTheDocument()
|
|
69
|
-
|
|
70
|
-
fireEvent.keyDown(button, { key: 'Escape' })
|
|
71
|
-
expect(screen.queryByRole('listbox')).not.toBeInTheDocument()
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('closes on click outside', () => {
|
|
75
|
-
renderWith(<Select options={options} value="a" onChange={vi.fn()} />)
|
|
76
|
-
|
|
77
|
-
fireEvent.click(screen.getByRole('button', { name: /alpha/i }))
|
|
78
|
-
expect(screen.getByRole('listbox')).toBeInTheDocument()
|
|
79
|
-
|
|
80
|
-
fireEvent.mouseDown(document.body)
|
|
81
|
-
expect(screen.queryByRole('listbox')).not.toBeInTheDocument()
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
it('navigates with arrow keys and selects with Enter', () => {
|
|
85
|
-
const onChange = vi.fn()
|
|
86
|
-
renderWith(<Select options={options} value="a" onChange={onChange} />)
|
|
87
|
-
|
|
88
|
-
const button = screen.getByRole('button', { name: /alpha/i })
|
|
89
|
-
fireEvent.click(button)
|
|
90
|
-
|
|
91
|
-
fireEvent.keyDown(button, { key: 'ArrowDown' })
|
|
92
|
-
fireEvent.keyDown(button, { key: 'ArrowDown' })
|
|
93
|
-
fireEvent.keyDown(button, { key: 'Enter' })
|
|
94
|
-
|
|
95
|
-
expect(onChange).toHaveBeenCalledWith('b')
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('renders groups with headers', () => {
|
|
99
|
-
renderWith(<Select groups={groups} value="a" onChange={vi.fn()} />)
|
|
100
|
-
|
|
101
|
-
fireEvent.click(screen.getByRole('button', { name: /alpha/i }))
|
|
102
|
-
|
|
103
|
-
expect(screen.getByText('Letters')).toBeInTheDocument()
|
|
104
|
-
expect(screen.getByText('Numbers')).toBeInTheDocument()
|
|
105
|
-
expect(screen.getByRole('option', { name: /one/i })).toBeInTheDocument()
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('uses renderOption for custom item rendering', () => {
|
|
109
|
-
renderWith(
|
|
110
|
-
<Select
|
|
111
|
-
options={options}
|
|
112
|
-
value="a"
|
|
113
|
-
onChange={vi.fn()}
|
|
114
|
-
renderOption={option => <span data-testid="custom">{option.label.toUpperCase()}</span>}
|
|
115
|
-
/>,
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
fireEvent.click(screen.getByRole('button', { name: /alpha/i }))
|
|
119
|
-
|
|
120
|
-
expect(screen.getAllByTestId('custom')).toHaveLength(3)
|
|
121
|
-
expect(screen.getByText('BRAVO')).toBeInTheDocument()
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
it('shows placeholder when value does not match any option', () => {
|
|
125
|
-
renderWith(<Select options={options} value="x" onChange={vi.fn()} placeholder="Pick one" />)
|
|
126
|
-
|
|
127
|
-
expect(screen.getByRole('button', { name: /pick one/i })).toBeInTheDocument()
|
|
128
|
-
})
|
|
129
|
-
})
|
package/src/Select/Select.tsx
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { ChevronDown } from 'lucide-react'
|
|
4
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
5
|
-
import type { SelectOption, SelectProps } from './Select.types'
|
|
6
|
-
|
|
7
|
-
export const Select = ({
|
|
8
|
-
value,
|
|
9
|
-
onChange,
|
|
10
|
-
options,
|
|
11
|
-
groups,
|
|
12
|
-
placeholder,
|
|
13
|
-
id,
|
|
14
|
-
className,
|
|
15
|
-
menuClassName,
|
|
16
|
-
renderOption,
|
|
17
|
-
onOpenChange,
|
|
18
|
-
'aria-label': ariaLabel,
|
|
19
|
-
}: SelectProps) => {
|
|
20
|
-
const [open, setOpen] = useState(false)
|
|
21
|
-
const [highlightedIndex, setHighlightedIndex] = useState(-1)
|
|
22
|
-
const containerRef = useRef<HTMLDivElement>(null)
|
|
23
|
-
|
|
24
|
-
const changeOpen = useCallback(
|
|
25
|
-
(next: boolean) => {
|
|
26
|
-
setOpen(next)
|
|
27
|
-
onOpenChange?.(next)
|
|
28
|
-
},
|
|
29
|
-
[onOpenChange],
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
// Build flat items list with group metadata
|
|
33
|
-
const { items, groupStartIndices } = useMemo(() => {
|
|
34
|
-
const flat: SelectOption[] = [...(options ?? [])]
|
|
35
|
-
const starts: { index: number; label: string }[] = []
|
|
36
|
-
|
|
37
|
-
for (const group of groups ?? []) {
|
|
38
|
-
starts.push({ index: flat.length, label: group.label })
|
|
39
|
-
flat.push(...group.options)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return { items: flat, groupStartIndices: starts }
|
|
43
|
-
}, [options, groups])
|
|
44
|
-
|
|
45
|
-
const selectedOption = items.find(o => o.value === value)
|
|
46
|
-
const displayText = selectedOption?.label ?? placeholder ?? value
|
|
47
|
-
|
|
48
|
-
// Click outside to close
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
const handleClickOutside = (e: MouseEvent) => {
|
|
51
|
-
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
52
|
-
changeOpen(false)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
document.addEventListener('mousedown', handleClickOutside)
|
|
56
|
-
return () => document.removeEventListener('mousedown', handleClickOutside)
|
|
57
|
-
}, [changeOpen])
|
|
58
|
-
|
|
59
|
-
const selectItem = (itemValue: string) => {
|
|
60
|
-
onChange(itemValue)
|
|
61
|
-
changeOpen(false)
|
|
62
|
-
setHighlightedIndex(-1)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
66
|
-
if (e.key === 'Escape') {
|
|
67
|
-
changeOpen(false)
|
|
68
|
-
setHighlightedIndex(-1)
|
|
69
|
-
return
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (!open) {
|
|
73
|
-
if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Enter' || e.key === ' ') {
|
|
74
|
-
e.preventDefault()
|
|
75
|
-
changeOpen(true)
|
|
76
|
-
setHighlightedIndex(0)
|
|
77
|
-
}
|
|
78
|
-
return
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (e.key === 'ArrowDown') {
|
|
82
|
-
e.preventDefault()
|
|
83
|
-
setHighlightedIndex(prev => (prev + 1) % items.length)
|
|
84
|
-
} else if (e.key === 'ArrowUp') {
|
|
85
|
-
e.preventDefault()
|
|
86
|
-
setHighlightedIndex(prev => (prev - 1 + items.length) % items.length)
|
|
87
|
-
} else if (e.key === 'Enter') {
|
|
88
|
-
e.preventDefault()
|
|
89
|
-
const highlighted = items[highlightedIndex]
|
|
90
|
-
if (highlighted) {
|
|
91
|
-
selectItem(highlighted.value)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const itemClassName = (index: number, itemValue: string) => {
|
|
97
|
-
const base = 'px-3 py-2.5 cursor-pointer'
|
|
98
|
-
const highlight = index === highlightedIndex ? 'bg-primary-muted' : 'hover:bg-primary-muted'
|
|
99
|
-
const selected = itemValue === value ? 'font-medium' : ''
|
|
100
|
-
return `${base} ${highlight} ${selected}`
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return (
|
|
104
|
-
<div className="relative" ref={containerRef} onKeyDown={handleKeyDown}>
|
|
105
|
-
<button
|
|
106
|
-
type="button"
|
|
107
|
-
id={id}
|
|
108
|
-
className={`flex items-center justify-between gap-2 ${className ?? ''}`}
|
|
109
|
-
aria-expanded={open}
|
|
110
|
-
aria-haspopup="listbox"
|
|
111
|
-
aria-label={ariaLabel}
|
|
112
|
-
onClick={() => changeOpen(!open)}
|
|
113
|
-
>
|
|
114
|
-
<span className="truncate">{displayText}</span>
|
|
115
|
-
<ChevronDown
|
|
116
|
-
className={`size-4 shrink-0 transition-transform ${open ? 'rotate-180' : ''}`}
|
|
117
|
-
/>
|
|
118
|
-
</button>
|
|
119
|
-
|
|
120
|
-
{open && (
|
|
121
|
-
<ul
|
|
122
|
-
role="listbox"
|
|
123
|
-
className={`border-border bg-background absolute z-50 mt-1 max-h-60 w-max min-w-full overflow-y-auto rounded-lg border shadow-lg ${menuClassName ?? ''}`}
|
|
124
|
-
>
|
|
125
|
-
{items.map((item, index) => {
|
|
126
|
-
const groupHeader = groupStartIndices.find(g => g.index === index)
|
|
127
|
-
|
|
128
|
-
return (
|
|
129
|
-
<li key={item.value} role="presentation">
|
|
130
|
-
{groupHeader && (
|
|
131
|
-
<div className="text-muted-foreground px-3 pt-2 pb-1 text-xs font-medium uppercase">
|
|
132
|
-
{groupHeader.label}
|
|
133
|
-
</div>
|
|
134
|
-
)}
|
|
135
|
-
<div
|
|
136
|
-
role="option"
|
|
137
|
-
aria-selected={value === item.value}
|
|
138
|
-
className={itemClassName(index, item.value)}
|
|
139
|
-
onClick={() => selectItem(item.value)}
|
|
140
|
-
onMouseEnter={() => setHighlightedIndex(index)}
|
|
141
|
-
>
|
|
142
|
-
{renderOption
|
|
143
|
-
? renderOption(item, {
|
|
144
|
-
highlighted: index === highlightedIndex,
|
|
145
|
-
selected: value === item.value,
|
|
146
|
-
})
|
|
147
|
-
: item.label}
|
|
148
|
-
</div>
|
|
149
|
-
</li>
|
|
150
|
-
)
|
|
151
|
-
})}
|
|
152
|
-
</ul>
|
|
153
|
-
)}
|
|
154
|
-
</div>
|
|
155
|
-
)
|
|
156
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from 'react'
|
|
2
|
-
|
|
3
|
-
export type SelectOption = {
|
|
4
|
-
value: string
|
|
5
|
-
label: string
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export type SelectGroup = {
|
|
9
|
-
label: string
|
|
10
|
-
options: SelectOption[]
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export type SelectOptionState = {
|
|
14
|
-
highlighted: boolean
|
|
15
|
-
selected: boolean
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export type SelectProps = {
|
|
19
|
-
value: string
|
|
20
|
-
onChange: (value: string) => void
|
|
21
|
-
options?: SelectOption[]
|
|
22
|
-
groups?: SelectGroup[]
|
|
23
|
-
placeholder?: string
|
|
24
|
-
id?: string
|
|
25
|
-
className?: string
|
|
26
|
-
menuClassName?: string
|
|
27
|
-
renderOption?: (option: SelectOption, state: SelectOptionState) => ReactNode
|
|
28
|
-
onOpenChange?: (open: boolean) => void
|
|
29
|
-
'aria-label'?: string
|
|
30
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { cleanup, render, screen } from '@testing-library/react'
|
|
2
|
-
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
-
import { Slider } from './Slider'
|
|
4
|
-
|
|
5
|
-
afterEach(cleanup)
|
|
6
|
-
|
|
7
|
-
describe('Slider', () => {
|
|
8
|
-
it('renders a slider with the correct value', () => {
|
|
9
|
-
render(<Slider value={50} onChange={() => {}} min={0} max={100} aria-label="Volume" />)
|
|
10
|
-
const slider = screen.getByRole('slider')
|
|
11
|
-
expect(slider).toHaveAttribute('aria-valuenow', '50')
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
it('calls onCommit when interaction ends', () => {
|
|
15
|
-
const onCommit = vi.fn()
|
|
16
|
-
render(
|
|
17
|
-
<Slider
|
|
18
|
-
value={50}
|
|
19
|
-
onChange={() => {}}
|
|
20
|
-
onCommit={onCommit}
|
|
21
|
-
min={0}
|
|
22
|
-
max={100}
|
|
23
|
-
aria-label="Volume"
|
|
24
|
-
/>,
|
|
25
|
-
)
|
|
26
|
-
// Just verify it renders without error — slider drag testing is unreliable in jsdom
|
|
27
|
-
expect(screen.getByRole('slider')).toBeInTheDocument()
|
|
28
|
-
})
|
|
29
|
-
})
|
package/src/Slider/Slider.tsx
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { cn } from '../helpers/cn/cn'
|
|
4
|
-
import { Slider as SliderPrimitive } from '@base-ui/react/slider'
|
|
5
|
-
|
|
6
|
-
type SliderProps = {
|
|
7
|
-
value: number
|
|
8
|
-
onChange: (value: number) => void
|
|
9
|
-
onCommit?: (value: number) => void
|
|
10
|
-
min: number
|
|
11
|
-
max: number
|
|
12
|
-
step?: number
|
|
13
|
-
disabled?: boolean
|
|
14
|
-
'aria-label'?: string
|
|
15
|
-
className?: string
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const Slider = ({
|
|
19
|
-
value,
|
|
20
|
-
onChange,
|
|
21
|
-
onCommit,
|
|
22
|
-
min,
|
|
23
|
-
max,
|
|
24
|
-
step = 1,
|
|
25
|
-
disabled,
|
|
26
|
-
'aria-label': ariaLabel,
|
|
27
|
-
className,
|
|
28
|
-
}: SliderProps) => (
|
|
29
|
-
<SliderPrimitive.Root
|
|
30
|
-
value={value}
|
|
31
|
-
onValueChange={v => onChange(v as number)}
|
|
32
|
-
onValueCommitted={v => onCommit?.(v as number)}
|
|
33
|
-
min={min}
|
|
34
|
-
max={max}
|
|
35
|
-
step={step}
|
|
36
|
-
disabled={disabled}
|
|
37
|
-
aria-label={ariaLabel}
|
|
38
|
-
className={cn('relative flex w-full touch-none items-center py-2', className)}
|
|
39
|
-
>
|
|
40
|
-
<SliderPrimitive.Control className="relative flex h-1.5 w-full items-center">
|
|
41
|
-
<SliderPrimitive.Track className="bg-input h-full w-full overflow-hidden rounded-full">
|
|
42
|
-
<SliderPrimitive.Indicator className="bg-primary h-full rounded-full" />
|
|
43
|
-
</SliderPrimitive.Track>
|
|
44
|
-
<SliderPrimitive.Thumb
|
|
45
|
-
className={cn(
|
|
46
|
-
'bg-primary border-background block size-4 rounded-full border-2 shadow-sm',
|
|
47
|
-
'focus-visible:ring-ring/50 focus-visible:ring-3 focus-visible:outline-hidden',
|
|
48
|
-
disabled && 'pointer-events-none opacity-50',
|
|
49
|
-
)}
|
|
50
|
-
/>
|
|
51
|
-
</SliderPrimitive.Control>
|
|
52
|
-
</SliderPrimitive.Root>
|
|
53
|
-
)
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useTheme } from 'next-themes'
|
|
4
|
-
import { useCallback, useSyncExternalStore } from 'react'
|
|
5
|
-
import { useHotkeys } from 'react-hotkeys-hook'
|
|
6
|
-
import { toast } from 'sonner'
|
|
7
|
-
|
|
8
|
-
const THEME_CYCLE = ['system', 'light', 'dark'] as const
|
|
9
|
-
|
|
10
|
-
const THEME_LABELS: Record<string, string> = {
|
|
11
|
-
system: 'System',
|
|
12
|
-
light: 'Light',
|
|
13
|
-
dark: 'Dark',
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const emptySubscribe = () => () => {}
|
|
17
|
-
const getSnapshot = () => true
|
|
18
|
-
const getServerSnapshot = () => false
|
|
19
|
-
|
|
20
|
-
export const ThemeCycler = () => {
|
|
21
|
-
const { theme, setTheme } = useTheme()
|
|
22
|
-
const mounted = useSyncExternalStore(emptySubscribe, getSnapshot, getServerSnapshot)
|
|
23
|
-
|
|
24
|
-
const cycleTheme = useCallback(() => {
|
|
25
|
-
if (!mounted) return
|
|
26
|
-
const current = theme ?? 'system'
|
|
27
|
-
const currentIndex = THEME_CYCLE.indexOf(current as (typeof THEME_CYCLE)[number])
|
|
28
|
-
const nextIndex = (currentIndex + 1) % THEME_CYCLE.length
|
|
29
|
-
const next = THEME_CYCLE[nextIndex] ?? 'system'
|
|
30
|
-
setTheme(next)
|
|
31
|
-
toast(`Theme: ${THEME_LABELS[next]}`, { duration: 2000 })
|
|
32
|
-
}, [theme, setTheme, mounted])
|
|
33
|
-
|
|
34
|
-
useHotkeys('shift+t', cycleTheme, { preventDefault: true })
|
|
35
|
-
|
|
36
|
-
return null
|
|
37
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
|
4
|
-
|
|
5
|
-
type Props = {
|
|
6
|
-
children: React.ReactNode
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export const ThemeProvider = ({ children }: Props) => (
|
|
10
|
-
<NextThemesProvider
|
|
11
|
-
attribute="class"
|
|
12
|
-
defaultTheme="system"
|
|
13
|
-
enableSystem
|
|
14
|
-
disableTransitionOnChange
|
|
15
|
-
>
|
|
16
|
-
{children}
|
|
17
|
-
</NextThemesProvider>
|
|
18
|
-
)
|
package/src/Toaster/Toaster.tsx
DELETED
package/src/Tooltip/Tooltip.tsx
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { Kbd } from '../Kbd/Kbd'
|
|
4
|
-
import { type ReactElement, cloneElement, useCallback, useEffect, useRef, useState } from 'react'
|
|
5
|
-
|
|
6
|
-
type TooltipProps = {
|
|
7
|
-
label: string
|
|
8
|
-
shortcut?: string | string[]
|
|
9
|
-
position?: 'top' | 'bottom'
|
|
10
|
-
delay?: number
|
|
11
|
-
maxWidth?: number
|
|
12
|
-
disabled?: boolean
|
|
13
|
-
className?: string
|
|
14
|
-
children: ReactElement<{ 'aria-label'?: string }>
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const DEFAULT_DELAY = 200
|
|
18
|
-
|
|
19
|
-
export const Tooltip = ({
|
|
20
|
-
label,
|
|
21
|
-
shortcut,
|
|
22
|
-
position = 'top',
|
|
23
|
-
delay = DEFAULT_DELAY,
|
|
24
|
-
maxWidth,
|
|
25
|
-
disabled,
|
|
26
|
-
className,
|
|
27
|
-
children,
|
|
28
|
-
}: TooltipProps) => {
|
|
29
|
-
const [visible, setVisible] = useState(false)
|
|
30
|
-
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
31
|
-
const wrapperRef = useRef<HTMLDivElement>(null)
|
|
32
|
-
|
|
33
|
-
const show = useCallback(() => {
|
|
34
|
-
if (disabled) return
|
|
35
|
-
timeoutRef.current = setTimeout(() => setVisible(true), delay)
|
|
36
|
-
}, [disabled, delay])
|
|
37
|
-
|
|
38
|
-
const hide = useCallback(() => {
|
|
39
|
-
if (timeoutRef.current) {
|
|
40
|
-
clearTimeout(timeoutRef.current)
|
|
41
|
-
timeoutRef.current = null
|
|
42
|
-
}
|
|
43
|
-
setVisible(false)
|
|
44
|
-
}, [])
|
|
45
|
-
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
if (disabled && timeoutRef.current) {
|
|
48
|
-
clearTimeout(timeoutRef.current)
|
|
49
|
-
timeoutRef.current = null
|
|
50
|
-
}
|
|
51
|
-
}, [disabled])
|
|
52
|
-
|
|
53
|
-
useEffect(() => {
|
|
54
|
-
return () => {
|
|
55
|
-
if (timeoutRef.current) clearTimeout(timeoutRef.current)
|
|
56
|
-
}
|
|
57
|
-
}, [])
|
|
58
|
-
|
|
59
|
-
useEffect(() => {
|
|
60
|
-
if (!visible) return
|
|
61
|
-
let lastCheck = 0
|
|
62
|
-
const handlePointerMove = (e: PointerEvent) => {
|
|
63
|
-
if (e.timeStamp - lastCheck < 100) return
|
|
64
|
-
lastCheck = e.timeStamp
|
|
65
|
-
const rect = wrapperRef.current?.getBoundingClientRect()
|
|
66
|
-
if (!rect) return
|
|
67
|
-
const inside =
|
|
68
|
-
e.clientX >= rect.left &&
|
|
69
|
-
e.clientX <= rect.right &&
|
|
70
|
-
e.clientY >= rect.top &&
|
|
71
|
-
e.clientY <= rect.bottom
|
|
72
|
-
if (!inside) hide()
|
|
73
|
-
}
|
|
74
|
-
document.addEventListener('pointermove', handlePointerMove, { passive: true })
|
|
75
|
-
return () => document.removeEventListener('pointermove', handlePointerMove)
|
|
76
|
-
}, [visible, hide])
|
|
77
|
-
|
|
78
|
-
const positionClasses = position === 'top' ? 'bottom-full mb-2' : 'top-full mt-2'
|
|
79
|
-
|
|
80
|
-
return (
|
|
81
|
-
<div
|
|
82
|
-
ref={wrapperRef}
|
|
83
|
-
className={`relative inline-flex${className ? ` ${className}` : ''}`}
|
|
84
|
-
onMouseEnter={show}
|
|
85
|
-
onMouseLeave={hide}
|
|
86
|
-
onPointerDown={hide}
|
|
87
|
-
onFocus={show}
|
|
88
|
-
onBlur={hide}
|
|
89
|
-
>
|
|
90
|
-
{cloneElement(children, {
|
|
91
|
-
'aria-label': children.props['aria-label'] ?? label,
|
|
92
|
-
})}
|
|
93
|
-
{visible && !disabled && (
|
|
94
|
-
<div
|
|
95
|
-
role="tooltip"
|
|
96
|
-
style={maxWidth ? { maxWidth } : undefined}
|
|
97
|
-
className={`absolute left-1/2 z-50 -translate-x-1/2 ${maxWidth ? 'w-max whitespace-normal' : 'whitespace-nowrap'} bg-foreground text-background pointer-events-none flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs shadow-lg ${positionClasses}`}
|
|
98
|
-
>
|
|
99
|
-
{label}
|
|
100
|
-
{shortcut && (
|
|
101
|
-
<Kbd keys={shortcut} size="sm" className="bg-background/15 border-transparent" />
|
|
102
|
-
)}
|
|
103
|
-
</div>
|
|
104
|
-
)}
|
|
105
|
-
</div>
|
|
106
|
-
)
|
|
107
|
-
}
|
package/src/helpers/cn/cn.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
-
import { getModKey } from './getModKey'
|
|
3
|
-
|
|
4
|
-
describe('getModKey', () => {
|
|
5
|
-
afterEach(() => {
|
|
6
|
-
vi.unstubAllGlobals()
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
it('returns "Cmd" on Mac', () => {
|
|
10
|
-
vi.stubGlobal('navigator', { userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)' })
|
|
11
|
-
expect(getModKey()).toBe('Cmd')
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
it('returns "Ctrl" on Windows', () => {
|
|
15
|
-
vi.stubGlobal('navigator', { userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' })
|
|
16
|
-
expect(getModKey()).toBe('Ctrl')
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it('returns "Ctrl" when navigator is undefined', () => {
|
|
20
|
-
vi.stubGlobal('navigator', undefined)
|
|
21
|
-
expect(getModKey()).toBe('Ctrl')
|
|
22
|
-
})
|
|
23
|
-
})
|
package/src/index.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
export { Badge, badgeVariants } from './Badge/Badge'
|
|
2
|
-
export type { BadgeProps } from './Badge/Badge.types'
|
|
3
|
-
|
|
4
|
-
export { Button, buttonVariants } from './Button/Button'
|
|
5
|
-
export type { ButtonProps } from './Button/Button.types'
|
|
6
|
-
|
|
7
|
-
export { Kbd, kbdVariants } from './Kbd/Kbd'
|
|
8
|
-
export type { KbdProps } from './Kbd/Kbd.types'
|
|
9
|
-
|
|
10
|
-
export { ProgressRing } from './ProgressRing/ProgressRing'
|
|
11
|
-
|
|
12
|
-
export { Slider } from './Slider/Slider'
|
|
13
|
-
|
|
14
|
-
export { ContextMenu } from './ContextMenu/ContextMenu'
|
|
15
|
-
|
|
16
|
-
export { Select } from './Select/Select'
|
|
17
|
-
export type {
|
|
18
|
-
SelectOption,
|
|
19
|
-
SelectGroup,
|
|
20
|
-
SelectOptionState,
|
|
21
|
-
SelectProps,
|
|
22
|
-
} from './Select/Select.types'
|
|
23
|
-
|
|
24
|
-
export { Tooltip } from './Tooltip/Tooltip'
|
|
25
|
-
|
|
26
|
-
export { ThemeProvider } from './ThemeProvider/ThemeProvider'
|
|
27
|
-
|
|
28
|
-
export { ThemeCycler } from './ThemeCycler/ThemeCycler'
|
|
29
|
-
|
|
30
|
-
export { Toaster } from './Toaster/Toaster'
|
|
31
|
-
export { toast } from 'sonner'
|
|
32
|
-
|
|
33
|
-
export { useTheme } from 'next-themes'
|
|
34
|
-
|
|
35
|
-
export { cn } from './helpers/cn/cn'
|
|
36
|
-
export { getModKey } from './helpers/getModKey/getModKey'
|