@workerdeck/ui 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +74 -0
- package/build/index.d.mts +927 -0
- package/build/index.mjs +5236 -0
- package/build/index.mjs.map +1 -0
- package/package.json +73 -0
- package/src/components/agent/Composer.tsx +139 -0
- package/src/components/agent/Conversation.tsx +59 -0
- package/src/components/agent/FileCard.tsx +50 -0
- package/src/components/agent/Loader.tsx +21 -0
- package/src/components/agent/Message.tsx +44 -0
- package/src/components/agent/ModelSelect.tsx +87 -0
- package/src/components/agent/PermissionModeSelect.tsx +94 -0
- package/src/components/agent/PermissionPrompt.tsx +52 -0
- package/src/components/agent/QuestionPrompt.tsx +193 -0
- package/src/components/agent/Reasoning.tsx +58 -0
- package/src/components/agent/Response.tsx +31 -0
- package/src/components/agent/SessionList.tsx +93 -0
- package/src/components/agent/SessionPanel.tsx +149 -0
- package/src/components/agent/StatusBar.tsx +140 -0
- package/src/components/agent/ToolCallCard.tsx +94 -0
- package/src/components/agent/Transcript.tsx +114 -0
- package/src/components/agent/status.ts +16 -0
- package/src/components/prompt-area/animated-placeholder.tsx +42 -0
- package/src/components/prompt-area/clipboard-helpers.ts +206 -0
- package/src/components/prompt-area/cursor-helpers.ts +244 -0
- package/src/components/prompt-area/dom-helpers.ts +721 -0
- package/src/components/prompt-area/file-strip.tsx +250 -0
- package/src/components/prompt-area/html-to-markdown.ts +278 -0
- package/src/components/prompt-area/image-strip.tsx +49 -0
- package/src/components/prompt-area/index.ts +23 -0
- package/src/components/prompt-area/prompt-area-engine.ts +705 -0
- package/src/components/prompt-area/prompt-area-list-ops.ts +499 -0
- package/src/components/prompt-area/prompt-area.tsx +375 -0
- package/src/components/prompt-area/remove-button.tsx +37 -0
- package/src/components/prompt-area/segment-helpers.ts +62 -0
- package/src/components/prompt-area/trigger-popover.tsx +139 -0
- package/src/components/prompt-area/trigger-presets.ts +143 -0
- package/src/components/prompt-area/types.ts +360 -0
- package/src/components/prompt-area/use-markdown-mode.ts +113 -0
- package/src/components/prompt-area/use-prompt-area-events.ts +470 -0
- package/src/components/prompt-area/use-prompt-area-state.ts +131 -0
- package/src/components/prompt-area/use-prompt-area.ts +1507 -0
- package/src/components/prompt-area/use-trigger-search.ts +115 -0
- package/src/components/ui/AlertDialog.tsx +56 -0
- package/src/components/ui/Badge.tsx +42 -0
- package/src/components/ui/Button.tsx +47 -0
- package/src/components/ui/Card.tsx +29 -0
- package/src/components/ui/CodeBlock.tsx +31 -0
- package/src/components/ui/CopyButton.tsx +28 -0
- package/src/components/ui/Input.tsx +20 -0
- package/src/components/ui/ProgressRing.tsx +49 -0
- package/src/components/ui/Select.tsx +80 -0
- package/src/components/ui/Sonner.tsx +22 -0
- package/src/components/ui/Spinner.tsx +6 -0
- package/src/components/ui/Textarea.tsx +21 -0
- package/src/components/ui/Tooltip.tsx +34 -0
- package/src/index.ts +99 -0
- package/src/lib/format.ts +67 -0
- package/src/lib/utils.ts +33 -0
- package/src/styles/theme.css +413 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
4
|
+
import type { TriggerConfig, TriggerSuggestion } from './types.ts'
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Types
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
type UseTriggerSearchReturn = {
|
|
11
|
+
suggestions: TriggerSuggestion[]
|
|
12
|
+
suggestionsLoading: boolean
|
|
13
|
+
suggestionsError: string | null
|
|
14
|
+
/** Run a search for the given query using the trigger's onSearch config. */
|
|
15
|
+
search: (query: string, config: TriggerConfig) => void
|
|
16
|
+
/** Cancel any in-flight search and reset state. */
|
|
17
|
+
reset: () => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Hook
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Manages async trigger search lifecycle: debouncing, AbortController
|
|
26
|
+
* cancellation, race-condition prevention, loading/error state.
|
|
27
|
+
*
|
|
28
|
+
* Extracted from `usePromptArea` so the main hook stays focused on
|
|
29
|
+
* editing concerns while this hook owns the data-fetching side.
|
|
30
|
+
*/
|
|
31
|
+
export function useTriggerSearch(): UseTriggerSearchReturn {
|
|
32
|
+
const [suggestions, setSuggestions] = useState<TriggerSuggestion[]>([])
|
|
33
|
+
const [suggestionsLoading, setSuggestionsLoading] = useState(false)
|
|
34
|
+
const [suggestionsError, setSuggestionsError] = useState<string | null>(null)
|
|
35
|
+
|
|
36
|
+
// Version counter – belt-and-suspenders alongside AbortController
|
|
37
|
+
const searchVersion = useRef(0)
|
|
38
|
+
// AbortController for cancelling in-flight async searches
|
|
39
|
+
const abortController = useRef<AbortController | null>(null)
|
|
40
|
+
// Debounce timer for search queries
|
|
41
|
+
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
42
|
+
|
|
43
|
+
const reset = useCallback(() => {
|
|
44
|
+
abortController.current?.abort()
|
|
45
|
+
if (debounceTimer.current) clearTimeout(debounceTimer.current)
|
|
46
|
+
setSuggestions([])
|
|
47
|
+
setSuggestionsLoading(false)
|
|
48
|
+
setSuggestionsError(null)
|
|
49
|
+
}, [])
|
|
50
|
+
|
|
51
|
+
const search = useCallback((query: string, config: TriggerConfig) => {
|
|
52
|
+
if (!config.onSearch) return
|
|
53
|
+
|
|
54
|
+
// Cancel any previous in-flight request and pending debounce
|
|
55
|
+
abortController.current?.abort()
|
|
56
|
+
if (debounceTimer.current) clearTimeout(debounceTimer.current)
|
|
57
|
+
|
|
58
|
+
setSuggestionsLoading(true)
|
|
59
|
+
setSuggestionsError(null)
|
|
60
|
+
searchVersion.current++
|
|
61
|
+
const version = searchVersion.current
|
|
62
|
+
|
|
63
|
+
const controller = new AbortController()
|
|
64
|
+
abortController.current = controller
|
|
65
|
+
const { onSearch, onSearchError, searchDebounceMs } = config
|
|
66
|
+
|
|
67
|
+
const executeSearch = () => {
|
|
68
|
+
const result = onSearch(query, { signal: controller.signal })
|
|
69
|
+
|
|
70
|
+
if (result instanceof Promise) {
|
|
71
|
+
void result.then(
|
|
72
|
+
(items) => {
|
|
73
|
+
if (controller.signal.aborted || searchVersion.current !== version) return
|
|
74
|
+
setSuggestions(items)
|
|
75
|
+
setSuggestionsLoading(false)
|
|
76
|
+
},
|
|
77
|
+
(error: unknown) => {
|
|
78
|
+
if (controller.signal.aborted || searchVersion.current !== version) return
|
|
79
|
+
// Silently ignore AbortError (expected when superseded)
|
|
80
|
+
if (error instanceof DOMException && error.name === 'AbortError') return
|
|
81
|
+
setSuggestionsError(error instanceof Error ? error.message : 'Search failed')
|
|
82
|
+
setSuggestionsLoading(false)
|
|
83
|
+
onSearchError?.(error)
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
} else {
|
|
87
|
+
setSuggestions(result)
|
|
88
|
+
setSuggestionsLoading(false)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Debounce subsequent searches but fire immediately for the initial empty query
|
|
93
|
+
if (searchDebounceMs && searchDebounceMs > 0 && query.length > 0) {
|
|
94
|
+
debounceTimer.current = setTimeout(executeSearch, searchDebounceMs)
|
|
95
|
+
} else {
|
|
96
|
+
executeSearch()
|
|
97
|
+
}
|
|
98
|
+
}, [])
|
|
99
|
+
|
|
100
|
+
// Clean up on unmount
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
return () => {
|
|
103
|
+
abortController.current?.abort()
|
|
104
|
+
if (debounceTimer.current) clearTimeout(debounceTimer.current)
|
|
105
|
+
}
|
|
106
|
+
}, [])
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
suggestions,
|
|
110
|
+
suggestionsLoading,
|
|
111
|
+
suggestionsError,
|
|
112
|
+
search,
|
|
113
|
+
reset,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type FunctionComponent } from 'react'
|
|
2
|
+
import { AlertDialog as AlertDialogPrimitive } from '@base-ui/react/alert-dialog'
|
|
3
|
+
import { cn } from '../../lib/utils.ts'
|
|
4
|
+
|
|
5
|
+
export const AlertDialog = AlertDialogPrimitive.Root
|
|
6
|
+
export const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
|
7
|
+
export const AlertDialogClose = AlertDialogPrimitive.Close
|
|
8
|
+
|
|
9
|
+
export const AlertDialogContent: FunctionComponent<AlertDialogPrimitive.Popup.Props> = ({
|
|
10
|
+
className,
|
|
11
|
+
children,
|
|
12
|
+
...props
|
|
13
|
+
}) => (
|
|
14
|
+
<AlertDialogPrimitive.Portal>
|
|
15
|
+
<AlertDialogPrimitive.Backdrop
|
|
16
|
+
className={cn(
|
|
17
|
+
'fixed inset-0 z-70 bg-black/40 backdrop-blur-[1px]',
|
|
18
|
+
'transition-opacity duration-(--motion-base)',
|
|
19
|
+
'data-starting-style:opacity-0 data-ending-style:opacity-0',
|
|
20
|
+
)}
|
|
21
|
+
/>
|
|
22
|
+
<AlertDialogPrimitive.Popup
|
|
23
|
+
data-slot='alert-dialog-content'
|
|
24
|
+
className={cn(
|
|
25
|
+
'fixed top-1/2 left-1/2 z-70 w-[min(28rem,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2',
|
|
26
|
+
'rounded-lg border border-border bg-surface p-5 shadow-(--shadow-lg) outline-none',
|
|
27
|
+
'transition-[opacity,transform] duration-(--motion-base)',
|
|
28
|
+
'data-starting-style:scale-95 data-starting-style:opacity-0',
|
|
29
|
+
'data-ending-style:scale-95 data-ending-style:opacity-0',
|
|
30
|
+
className,
|
|
31
|
+
)}
|
|
32
|
+
{...props}>
|
|
33
|
+
{children}
|
|
34
|
+
</AlertDialogPrimitive.Popup>
|
|
35
|
+
</AlertDialogPrimitive.Portal>
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
export const AlertDialogTitle: FunctionComponent<AlertDialogPrimitive.Title.Props> = ({
|
|
39
|
+
className,
|
|
40
|
+
...props
|
|
41
|
+
}) => (
|
|
42
|
+
<AlertDialogPrimitive.Title
|
|
43
|
+
className={cn('text-heading-3 font-semibold text-text', className)}
|
|
44
|
+
{...props}
|
|
45
|
+
/>
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
export const AlertDialogDescription: FunctionComponent<AlertDialogPrimitive.Description.Props> = ({
|
|
49
|
+
className,
|
|
50
|
+
...props
|
|
51
|
+
}) => (
|
|
52
|
+
<AlertDialogPrimitive.Description
|
|
53
|
+
className={cn('mt-1.5 text-body-sm text-muted-foreground', className)}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type HTMLAttributes } from 'react'
|
|
2
|
+
import { type VariantProps, cva } from 'class-variance-authority'
|
|
3
|
+
import { cn } from '../../lib/utils.ts'
|
|
4
|
+
|
|
5
|
+
const badgeVariants = cva(
|
|
6
|
+
'inline-flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-label font-medium whitespace-nowrap',
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
neutral: 'border-border bg-surface text-fg-2',
|
|
11
|
+
accent: 'border-transparent bg-accent-bg text-fg-1',
|
|
12
|
+
success: 'border-transparent bg-success-bg text-success',
|
|
13
|
+
warning: 'border-transparent bg-warning-bg text-warning',
|
|
14
|
+
danger: 'border-transparent bg-danger-bg text-danger',
|
|
15
|
+
info: 'border-transparent bg-info-bg text-info',
|
|
16
|
+
},
|
|
17
|
+
mono: {
|
|
18
|
+
true: 'font-mono text-code',
|
|
19
|
+
false: '',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: { variant: 'neutral', mono: false },
|
|
23
|
+
},
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
export interface BadgeProps
|
|
27
|
+
extends HTMLAttributes<HTMLSpanElement>,
|
|
28
|
+
VariantProps<typeof badgeVariants> {
|
|
29
|
+
/** Render a leading status dot in the variant's color. */
|
|
30
|
+
dot?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function Badge({ className, variant = 'neutral', mono, dot, children, ...props }: BadgeProps) {
|
|
34
|
+
return (
|
|
35
|
+
<span data-slot='badge' className={cn(badgeVariants({ variant, mono, className }))} {...props}>
|
|
36
|
+
{dot ? <span aria-hidden className='size-1.5 rounded-full bg-current' /> : null}
|
|
37
|
+
{children}
|
|
38
|
+
</span>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { badgeVariants }
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type ButtonHTMLAttributes, forwardRef } from 'react'
|
|
2
|
+
import { type VariantProps, cva } from 'class-variance-authority'
|
|
3
|
+
import { cn } from '../../lib/utils.ts'
|
|
4
|
+
|
|
5
|
+
const buttonVariants = cva(
|
|
6
|
+
"group/button inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-colors outline-none select-none focus-visible:ring-2 focus-visible:ring-ring/60 focus-visible:ring-offset-1 focus-visible:ring-offset-bg disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: 'bg-primary text-primary-foreground hover:bg-accent-hover',
|
|
11
|
+
outline: 'border-border bg-surface hover:border-border-strong hover:bg-surface-hover',
|
|
12
|
+
secondary: 'bg-secondary text-secondary-foreground hover:bg-surface-hover',
|
|
13
|
+
ghost: 'text-fg-2 hover:bg-surface-hover hover:text-foreground',
|
|
14
|
+
destructive: 'text-danger hover:bg-danger-bg',
|
|
15
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
16
|
+
},
|
|
17
|
+
size: {
|
|
18
|
+
'default': 'h-8 gap-1.5 px-3',
|
|
19
|
+
'xs': "h-6 gap-1 px-2 text-xs [&_svg:not([class*='size-'])]:size-3",
|
|
20
|
+
'sm': 'h-7 gap-1 px-2.5 text-xs',
|
|
21
|
+
'lg': 'h-9 gap-1.5 px-3.5',
|
|
22
|
+
'icon': 'size-8',
|
|
23
|
+
'icon-sm': 'size-7',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
defaultVariants: { variant: 'default', size: 'default' },
|
|
27
|
+
},
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
export interface ButtonProps
|
|
31
|
+
extends ButtonHTMLAttributes<HTMLButtonElement>,
|
|
32
|
+
VariantProps<typeof buttonVariants> {}
|
|
33
|
+
|
|
34
|
+
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
35
|
+
({ className, variant = 'default', size = 'default', type = 'button', ...props }, ref) => (
|
|
36
|
+
<button
|
|
37
|
+
ref={ref}
|
|
38
|
+
type={type}
|
|
39
|
+
data-slot='button'
|
|
40
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
Button.displayName = 'Button'
|
|
46
|
+
|
|
47
|
+
export { buttonVariants }
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type HTMLAttributes } from 'react'
|
|
2
|
+
import { cn } from '../../lib/utils.ts'
|
|
3
|
+
|
|
4
|
+
export function Card({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
5
|
+
return (
|
|
6
|
+
<div
|
|
7
|
+
data-slot='card'
|
|
8
|
+
className={cn('rounded-lg border border-border bg-surface', className)}
|
|
9
|
+
{...props}
|
|
10
|
+
/>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function CardHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
className={cn('flex items-start justify-between gap-3 px-4 pt-3.5 pb-3', className)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function CardTitle({ className, ...props }: HTMLAttributes<HTMLHeadingElement>) {
|
|
24
|
+
return <h3 className={cn('text-heading-3 font-semibold text-text', className)} {...props} />
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function CardContent({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
28
|
+
return <div className={cn('px-4 pb-4', className)} {...props} />
|
|
29
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { CopyButton } from './CopyButton.tsx'
|
|
3
|
+
import { cn } from '../../lib/utils.ts'
|
|
4
|
+
|
|
5
|
+
export interface CodeBlockProps {
|
|
6
|
+
code: string
|
|
7
|
+
/** Header label, e.g. a language or "Parameters". */
|
|
8
|
+
label?: ReactNode
|
|
9
|
+
copyable?: boolean
|
|
10
|
+
className?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Plain (unhighlighted) code panel for structured data like tool inputs. Markdown code
|
|
14
|
+
* inside assistant responses is highlighted by <Response> instead. */
|
|
15
|
+
export function CodeBlock({ code, label, copyable = true, className }: CodeBlockProps) {
|
|
16
|
+
return (
|
|
17
|
+
<div
|
|
18
|
+
data-slot='code-block'
|
|
19
|
+
className={cn('overflow-hidden rounded-md border border-border bg-code-bg', className)}>
|
|
20
|
+
{label !== undefined || copyable ? (
|
|
21
|
+
<div className='flex h-8 items-center justify-between border-b border-border px-2.5'>
|
|
22
|
+
<span className='font-mono text-label text-fg-3'>{label}</span>
|
|
23
|
+
{copyable ? <CopyButton value={code} /> : null}
|
|
24
|
+
</div>
|
|
25
|
+
) : null}
|
|
26
|
+
<pre className='max-h-64 overflow-auto px-3 py-2 font-mono text-label whitespace-pre-wrap text-fg-2'>
|
|
27
|
+
{code}
|
|
28
|
+
</pre>
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { Check, Copy } from 'lucide-react'
|
|
3
|
+
import { Button, type ButtonProps } from './Button.tsx'
|
|
4
|
+
import { cn } from '../../lib/utils.ts'
|
|
5
|
+
|
|
6
|
+
export interface CopyButtonProps extends Omit<ButtonProps, 'onClick' | 'children'> {
|
|
7
|
+
value: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function CopyButton({ value, className, variant = 'ghost', size = 'icon-sm', ...props }: CopyButtonProps) {
|
|
11
|
+
const [copied, setCopied] = useState(false)
|
|
12
|
+
return (
|
|
13
|
+
<Button
|
|
14
|
+
variant={variant}
|
|
15
|
+
size={size}
|
|
16
|
+
aria-label='Copy'
|
|
17
|
+
className={cn('text-fg-3', className)}
|
|
18
|
+
onClick={() => {
|
|
19
|
+
void navigator.clipboard.writeText(value).then(() => {
|
|
20
|
+
setCopied(true)
|
|
21
|
+
setTimeout(() => setCopied(false), 1500)
|
|
22
|
+
})
|
|
23
|
+
}}
|
|
24
|
+
{...props}>
|
|
25
|
+
{copied ? <Check className='size-3.5 text-success' /> : <Copy className='size-3.5' />}
|
|
26
|
+
</Button>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type InputHTMLAttributes, forwardRef } from 'react'
|
|
2
|
+
import { cn } from '../../lib/utils.ts'
|
|
3
|
+
|
|
4
|
+
export const Input = forwardRef<HTMLInputElement, InputHTMLAttributes<HTMLInputElement>>(
|
|
5
|
+
({ className, ...props }, ref) => (
|
|
6
|
+
<input
|
|
7
|
+
ref={ref}
|
|
8
|
+
data-slot='input'
|
|
9
|
+
className={cn(
|
|
10
|
+
'h-8 w-full rounded-md border border-border bg-bg px-2.5 text-body-sm text-text',
|
|
11
|
+
'placeholder:text-fg-4 transition-colors outline-none',
|
|
12
|
+
'hover:border-border-strong focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40',
|
|
13
|
+
'disabled:pointer-events-none disabled:opacity-50',
|
|
14
|
+
className,
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
),
|
|
19
|
+
)
|
|
20
|
+
Input.displayName = 'Input'
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { cn } from '../../lib/utils.ts'
|
|
2
|
+
|
|
3
|
+
export interface ProgressRingProps {
|
|
4
|
+
/** Filled share, 0–100 (clamped). */
|
|
5
|
+
value: number
|
|
6
|
+
/** Outer diameter in px. */
|
|
7
|
+
size?: number
|
|
8
|
+
strokeWidth?: number
|
|
9
|
+
className?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Tiny SVG progress circle; stroke color comes from `currentColor` so callers set it
|
|
13
|
+
* via text color classes (e.g. warning/danger past thresholds). */
|
|
14
|
+
export function ProgressRing({ value, size = 13, strokeWidth = 2, className }: ProgressRingProps) {
|
|
15
|
+
const clamped = Math.min(100, Math.max(0, value))
|
|
16
|
+
const radius = (size - strokeWidth) / 2
|
|
17
|
+
const circumference = 2 * Math.PI * radius
|
|
18
|
+
return (
|
|
19
|
+
<svg
|
|
20
|
+
data-slot='progress-ring'
|
|
21
|
+
width={size}
|
|
22
|
+
height={size}
|
|
23
|
+
viewBox={`0 0 ${size} ${size}`}
|
|
24
|
+
role='img'
|
|
25
|
+
aria-label={`${Math.round(clamped)}%`}
|
|
26
|
+
className={cn('shrink-0 -rotate-90', className)}>
|
|
27
|
+
<circle
|
|
28
|
+
cx={size / 2}
|
|
29
|
+
cy={size / 2}
|
|
30
|
+
r={radius}
|
|
31
|
+
fill='none'
|
|
32
|
+
stroke='currentColor'
|
|
33
|
+
strokeOpacity={0.2}
|
|
34
|
+
strokeWidth={strokeWidth}
|
|
35
|
+
/>
|
|
36
|
+
<circle
|
|
37
|
+
cx={size / 2}
|
|
38
|
+
cy={size / 2}
|
|
39
|
+
r={radius}
|
|
40
|
+
fill='none'
|
|
41
|
+
stroke='currentColor'
|
|
42
|
+
strokeWidth={strokeWidth}
|
|
43
|
+
strokeLinecap='round'
|
|
44
|
+
strokeDasharray={circumference}
|
|
45
|
+
strokeDashoffset={circumference * (1 - clamped / 100)}
|
|
46
|
+
/>
|
|
47
|
+
</svg>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type FunctionComponent } from 'react'
|
|
2
|
+
import { Select as SelectPrimitive } from '@base-ui/react/select'
|
|
3
|
+
import { Check, ChevronsUpDown } from 'lucide-react'
|
|
4
|
+
import { cn } from '../../lib/utils.ts'
|
|
5
|
+
|
|
6
|
+
export const Select = SelectPrimitive.Root
|
|
7
|
+
export const SelectValue = SelectPrimitive.Value
|
|
8
|
+
export const SelectItemText = SelectPrimitive.ItemText
|
|
9
|
+
|
|
10
|
+
export const SelectTrigger: FunctionComponent<SelectPrimitive.Trigger.Props> = ({
|
|
11
|
+
className,
|
|
12
|
+
children,
|
|
13
|
+
...props
|
|
14
|
+
}) => (
|
|
15
|
+
<SelectPrimitive.Trigger
|
|
16
|
+
data-slot='select-trigger'
|
|
17
|
+
className={cn(
|
|
18
|
+
'inline-flex h-7 items-center justify-between gap-1.5 rounded-md border border-border bg-bg px-2 text-body-sm text-text',
|
|
19
|
+
'transition-colors outline-none hover:border-border-strong focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40',
|
|
20
|
+
'data-popup-open:border-border-strong disabled:pointer-events-none disabled:opacity-50',
|
|
21
|
+
className,
|
|
22
|
+
)}
|
|
23
|
+
{...props}>
|
|
24
|
+
{children}
|
|
25
|
+
<SelectPrimitive.Icon className='text-fg-4'>
|
|
26
|
+
<ChevronsUpDown className='size-3.5' />
|
|
27
|
+
</SelectPrimitive.Icon>
|
|
28
|
+
</SelectPrimitive.Trigger>
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
export const SelectContent: FunctionComponent<
|
|
32
|
+
SelectPrimitive.Popup.Props &
|
|
33
|
+
Pick<SelectPrimitive.Positioner.Props, 'align' | 'alignItemWithTrigger' | 'side' | 'sideOffset'>
|
|
34
|
+
> = ({
|
|
35
|
+
className,
|
|
36
|
+
align = 'start',
|
|
37
|
+
alignItemWithTrigger = false,
|
|
38
|
+
side = 'bottom',
|
|
39
|
+
sideOffset = 6,
|
|
40
|
+
...props
|
|
41
|
+
}) => (
|
|
42
|
+
<SelectPrimitive.Portal>
|
|
43
|
+
<SelectPrimitive.Positioner
|
|
44
|
+
align={align}
|
|
45
|
+
alignItemWithTrigger={alignItemWithTrigger}
|
|
46
|
+
side={side}
|
|
47
|
+
sideOffset={sideOffset}
|
|
48
|
+
className='isolate z-60 outline-none'>
|
|
49
|
+
<SelectPrimitive.Popup
|
|
50
|
+
data-slot='select-content'
|
|
51
|
+
className={cn(
|
|
52
|
+
'max-h-[min(24rem,var(--available-height))] min-w-[var(--anchor-width)] overflow-y-auto',
|
|
53
|
+
'rounded-md border border-border bg-surface p-1 text-fg-1 shadow-(--shadow-lg) outline-none',
|
|
54
|
+
className,
|
|
55
|
+
)}
|
|
56
|
+
{...props}
|
|
57
|
+
/>
|
|
58
|
+
</SelectPrimitive.Positioner>
|
|
59
|
+
</SelectPrimitive.Portal>
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
export const SelectItem: FunctionComponent<SelectPrimitive.Item.Props> = ({
|
|
63
|
+
className,
|
|
64
|
+
children,
|
|
65
|
+
...props
|
|
66
|
+
}) => (
|
|
67
|
+
<SelectPrimitive.Item
|
|
68
|
+
data-slot='select-item'
|
|
69
|
+
className={cn(
|
|
70
|
+
'flex cursor-pointer items-start gap-2 rounded-sm px-2 py-1.5 text-body-sm text-text outline-none select-none',
|
|
71
|
+
'data-highlighted:bg-surface-hover',
|
|
72
|
+
className,
|
|
73
|
+
)}
|
|
74
|
+
{...props}>
|
|
75
|
+
<span className='flex min-w-0 flex-1 flex-col gap-0.5'>{children}</span>
|
|
76
|
+
<SelectPrimitive.ItemIndicator className='mt-0.5 text-fg-1'>
|
|
77
|
+
<Check className='size-3.5' />
|
|
78
|
+
</SelectPrimitive.ItemIndicator>
|
|
79
|
+
</SelectPrimitive.Item>
|
|
80
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Toaster as SonnerToaster, toast } from 'sonner'
|
|
2
|
+
|
|
3
|
+
export { toast }
|
|
4
|
+
|
|
5
|
+
/** Token-themed toaster; relies on the [data-theme] swap, so no `theme` prop needed. */
|
|
6
|
+
export function Toaster() {
|
|
7
|
+
return (
|
|
8
|
+
<SonnerToaster
|
|
9
|
+
position='bottom-right'
|
|
10
|
+
toastOptions={{
|
|
11
|
+
style: {
|
|
12
|
+
background: 'var(--surface)',
|
|
13
|
+
color: 'var(--text)',
|
|
14
|
+
border: '1px solid var(--border)',
|
|
15
|
+
boxShadow: 'var(--shadow-md)',
|
|
16
|
+
fontFamily: 'var(--cw-font-sans)',
|
|
17
|
+
fontSize: 'var(--text-body-sm)',
|
|
18
|
+
},
|
|
19
|
+
}}
|
|
20
|
+
/>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type TextareaHTMLAttributes, forwardRef } from 'react'
|
|
2
|
+
import { cn } from '../../lib/utils.ts'
|
|
3
|
+
|
|
4
|
+
export const Textarea = forwardRef<
|
|
5
|
+
HTMLTextAreaElement,
|
|
6
|
+
TextareaHTMLAttributes<HTMLTextAreaElement>
|
|
7
|
+
>(({ className, ...props }, ref) => (
|
|
8
|
+
<textarea
|
|
9
|
+
ref={ref}
|
|
10
|
+
data-slot='textarea'
|
|
11
|
+
className={cn(
|
|
12
|
+
'w-full resize-none rounded-md border border-border bg-bg px-2.5 py-2 text-body-sm text-text',
|
|
13
|
+
'placeholder:text-fg-4 transition-colors outline-none',
|
|
14
|
+
'hover:border-border-strong focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40',
|
|
15
|
+
'disabled:pointer-events-none disabled:opacity-50',
|
|
16
|
+
className,
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
))
|
|
21
|
+
Textarea.displayName = 'Textarea'
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type FunctionComponent, type ReactNode } from 'react'
|
|
2
|
+
import { Tooltip as TooltipPrimitive } from '@base-ui/react/tooltip'
|
|
3
|
+
import { cn } from '../../lib/utils.ts'
|
|
4
|
+
|
|
5
|
+
export const TooltipProvider = TooltipPrimitive.Provider
|
|
6
|
+
|
|
7
|
+
export const TooltipContent: FunctionComponent<
|
|
8
|
+
TooltipPrimitive.Popup.Props & Pick<TooltipPrimitive.Positioner.Props, 'side' | 'sideOffset'>
|
|
9
|
+
> = ({ className, side = 'top', sideOffset = 6, ...props }) => (
|
|
10
|
+
<TooltipPrimitive.Portal>
|
|
11
|
+
<TooltipPrimitive.Positioner side={side} sideOffset={sideOffset} className='isolate z-60'>
|
|
12
|
+
<TooltipPrimitive.Popup
|
|
13
|
+
data-slot='tooltip-content'
|
|
14
|
+
className={cn(
|
|
15
|
+
'rounded-md border border-border bg-surface px-2 py-1 text-label text-fg-2 shadow-(--shadow-md) outline-none',
|
|
16
|
+
className,
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
</TooltipPrimitive.Positioner>
|
|
21
|
+
</TooltipPrimitive.Portal>
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
/** Convenience wrapper: <Tip content="..."><Button/></Tip> */
|
|
25
|
+
export function Tip({ content, children }: { content: ReactNode; children: ReactNode }) {
|
|
26
|
+
return (
|
|
27
|
+
<TooltipPrimitive.Root>
|
|
28
|
+
<TooltipPrimitive.Trigger render={<span className='inline-flex' />}>
|
|
29
|
+
{children}
|
|
30
|
+
</TooltipPrimitive.Trigger>
|
|
31
|
+
<TooltipContent>{content}</TooltipContent>
|
|
32
|
+
</TooltipPrimitive.Root>
|
|
33
|
+
)
|
|
34
|
+
}
|