sparkdesign 0.4.8 → 0.4.9
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/cli/registry/AGENTS.md +1 -1
- package/cli/registry/agent-manifest.json +3996 -67
- package/cli/registry/basic/accordion.tsx +79 -0
- package/cli/registry/basic/badge.tsx +49 -0
- package/cli/registry/basic/button.tsx +19 -14
- package/cli/registry/basic/collapsible-card.tsx +10 -1
- package/cli/registry/basic/ellipsis-text.tsx +151 -0
- package/cli/registry/basic/form.tsx +186 -0
- package/cli/registry/basic/icon-button.tsx +12 -4
- package/cli/registry/basic/sidebar.tsx +665 -0
- package/cli/registry/basic/sonner.tsx +10 -10
- package/cli/registry/chat/chat-input/compound.tsx +1 -0
- package/cli/registry/chat/user-question/compound.tsx +2 -0
- package/cli/registry/meta.json +153 -0
- package/dist/registry/basic/accordion.d.ts +15 -0
- package/dist/registry/basic/badge.d.ts +23 -0
- package/dist/registry/basic/ellipsis-text.d.ts +45 -0
- package/dist/registry/basic/form.d.ts +23 -0
- package/dist/registry/basic/icon-button.d.ts +15 -2
- package/dist/registry/basic/item.d.ts +1 -1
- package/dist/registry/basic/sidebar.d.ts +72 -0
- package/dist/registry/basic/tag.d.ts +1 -1
- package/dist/spark-design.cjs.js +24 -27
- package/dist/spark-design.es.js +5353 -4470
- package/dist/sparkdesign.css +1 -1
- package/dist/src/components/basic/Accordion/index.d.ts +13 -0
- package/dist/src/components/basic/Badge/index.d.ts +13 -0
- package/dist/src/components/basic/EllipsisText/index.d.ts +4 -36
- package/dist/src/components/basic/Form/index.d.ts +12 -0
- package/dist/src/components/basic/Sidebar/index.d.ts +13 -0
- package/dist/src/components/index.d.ts +6 -2
- package/docs/agent/component-selection.md +106 -4
- package/package.json +8 -3
- package/registry/agent-manifest.json +3996 -67
- package/cli/registry/chat/user-question/UserQuestionCard.tsx +0 -198
- package/cli/registry/chat/user-question/UserQuestionFooter.tsx +0 -66
- package/cli/registry/chat/user-question/UserQuestionHeader.tsx +0 -64
- package/cli/registry/chat/user-question/useUserQuestionState.ts +0 -165
- package/dist/registry/chat/user-question/UserQuestionCard.d.ts +0 -36
- package/dist/registry/chat/user-question/UserQuestionFooter.d.ts +0 -24
- package/dist/registry/chat/user-question/UserQuestionHeader.d.ts +0 -26
- package/dist/registry/chat/user-question/useUserQuestionState.d.ts +0 -26
- package/dist/src/components/basic/CollapsibleSection/index.d.ts +0 -43
- package/dist/src/components/chat/Response/StreamingMarkdownBlock.d.ts +0 -12
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
import { ArrowDownSLine } from "./icons-inline"
|
|
8
|
+
|
|
9
|
+
const Accordion = AccordionPrimitive.Root
|
|
10
|
+
|
|
11
|
+
const AccordionItem = React.forwardRef<
|
|
12
|
+
React.ElementRef<typeof AccordionPrimitive.Item>,
|
|
13
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
|
14
|
+
>(({ className, ...props }, ref) => (
|
|
15
|
+
<AccordionPrimitive.Item
|
|
16
|
+
ref={ref}
|
|
17
|
+
data-slot="accordion-item"
|
|
18
|
+
className={cn("border-b border-border-tertiary last:border-b-0", className)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
))
|
|
22
|
+
AccordionItem.displayName = "AccordionItem"
|
|
23
|
+
|
|
24
|
+
const AccordionTrigger = React.forwardRef<
|
|
25
|
+
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
|
26
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> & {
|
|
27
|
+
chevronIcon?: React.ReactNode
|
|
28
|
+
}
|
|
29
|
+
>(({ className, children, chevronIcon, ...props }, ref) => (
|
|
30
|
+
<AccordionPrimitive.Header className="flex">
|
|
31
|
+
<AccordionPrimitive.Trigger
|
|
32
|
+
ref={ref}
|
|
33
|
+
data-slot="accordion-trigger"
|
|
34
|
+
className={cn(
|
|
35
|
+
"group flex flex-1 items-center justify-between gap-2 py-4 text-left text-sm font-medium text-text transition-all outline-none hover:text-text-secondary",
|
|
36
|
+
"focus-visible:ring-2 focus-visible:ring-primary-border focus-visible:ring-offset-2 focus-visible:ring-offset-bg-base",
|
|
37
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
38
|
+
className
|
|
39
|
+
)}
|
|
40
|
+
{...props}
|
|
41
|
+
>
|
|
42
|
+
{children}
|
|
43
|
+
<span
|
|
44
|
+
className="inline-flex shrink-0 transition-transform duration-200 group-data-[state=open]:rotate-180"
|
|
45
|
+
aria-hidden
|
|
46
|
+
>
|
|
47
|
+
{chevronIcon ?? <ArrowDownSLine className="size-4 text-text-secondary" />}
|
|
48
|
+
</span>
|
|
49
|
+
</AccordionPrimitive.Trigger>
|
|
50
|
+
</AccordionPrimitive.Header>
|
|
51
|
+
))
|
|
52
|
+
AccordionTrigger.displayName = "AccordionTrigger"
|
|
53
|
+
|
|
54
|
+
const AccordionContent = React.forwardRef<
|
|
55
|
+
React.ElementRef<typeof AccordionPrimitive.Content>,
|
|
56
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
|
57
|
+
>(({ className, children, ...props }, ref) => (
|
|
58
|
+
<AccordionPrimitive.Content
|
|
59
|
+
ref={ref}
|
|
60
|
+
data-slot="accordion-content"
|
|
61
|
+
className={cn(
|
|
62
|
+
"overflow-hidden text-sm text-text-secondary data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
|
|
63
|
+
className
|
|
64
|
+
)}
|
|
65
|
+
{...props}
|
|
66
|
+
>
|
|
67
|
+
<div className="pb-4 pt-0">{children}</div>
|
|
68
|
+
</AccordionPrimitive.Content>
|
|
69
|
+
))
|
|
70
|
+
AccordionContent.displayName = "AccordionContent"
|
|
71
|
+
|
|
72
|
+
export type AccordionProps = React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Root>
|
|
73
|
+
export type AccordionItemProps = React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
|
74
|
+
export type AccordionTriggerProps = React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> & {
|
|
75
|
+
chevronIcon?: React.ReactNode
|
|
76
|
+
}
|
|
77
|
+
export type AccordionContentProps = React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
|
78
|
+
|
|
79
|
+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const badgeVariants = cva(
|
|
8
|
+
"inline-flex items-center justify-center gap-1 rounded-md border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-border focus-visible:ring-offset-2 focus-visible:ring-offset-bg-base [&>svg]:size-3 [&>svg]:pointer-events-none",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "border-transparent bg-primary text-text-on-primary hover:bg-primary-hover",
|
|
13
|
+
secondary: "border-transparent bg-fill-secondary text-text hover:bg-fill",
|
|
14
|
+
destructive: "border-transparent bg-error text-text-on-primary hover:bg-error-hover",
|
|
15
|
+
outline: "border-border-tertiary bg-transparent text-text",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
defaultVariants: {
|
|
19
|
+
variant: "default",
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
function Badge({
|
|
25
|
+
className,
|
|
26
|
+
variant,
|
|
27
|
+
asChild = false,
|
|
28
|
+
...props
|
|
29
|
+
}: React.ComponentProps<"span"> &
|
|
30
|
+
VariantProps<typeof badgeVariants> & {
|
|
31
|
+
asChild?: boolean
|
|
32
|
+
}) {
|
|
33
|
+
const Comp = asChild ? Slot : "span"
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Comp
|
|
37
|
+
data-slot="badge"
|
|
38
|
+
className={cn(badgeVariants({ variant }), className)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type BadgeProps = React.ComponentProps<"span"> &
|
|
45
|
+
VariantProps<typeof badgeVariants> & {
|
|
46
|
+
asChild?: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { Badge, badgeVariants }
|
|
@@ -2,11 +2,10 @@ import { forwardRef } from 'react'
|
|
|
2
2
|
import type { ButtonHTMLAttributes, ReactNode } from 'react'
|
|
3
3
|
import { Slot } from 'radix-ui'
|
|
4
4
|
import { cva, type VariantProps } from 'class-variance-authority'
|
|
5
|
-
import { cn } from '@/lib/utils'
|
|
6
5
|
import { Spinner } from './spinner'
|
|
7
6
|
|
|
8
7
|
const buttonVariants = cva(
|
|
9
|
-
'inline-flex items-center justify-center gap-1.5 font-medium transition-[
|
|
8
|
+
'inline-flex items-center justify-center gap-1.5 font-medium transition-[color,background-color,border-color,box-shadow,transform] duration-200 active:scale-[0.98] focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer',
|
|
10
9
|
{
|
|
11
10
|
variants: {
|
|
12
11
|
variant: {
|
|
@@ -91,18 +90,24 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
91
90
|
aria-busy={loading || undefined}
|
|
92
91
|
{...props}
|
|
93
92
|
>
|
|
94
|
-
{
|
|
95
|
-
|
|
96
|
-
) :
|
|
97
|
-
|
|
98
|
-
{
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
93
|
+
{asChild ? (
|
|
94
|
+
children
|
|
95
|
+
) : (
|
|
96
|
+
<>
|
|
97
|
+
{loading ? (
|
|
98
|
+
<Spinner className="shrink-0" />
|
|
99
|
+
) : prefixIcon ? (
|
|
100
|
+
<span className="inline-flex shrink-0 items-center justify-center [&>*]:block [&>*]:leading-none">
|
|
101
|
+
{prefixIcon}
|
|
102
|
+
</span>
|
|
103
|
+
) : null}
|
|
104
|
+
{children}
|
|
105
|
+
{suffixIcon && (
|
|
106
|
+
<span className="inline-flex shrink-0 items-center justify-center [&>*]:block [&>*]:leading-none">
|
|
107
|
+
{suffixIcon}
|
|
108
|
+
</span>
|
|
109
|
+
)}
|
|
110
|
+
</>
|
|
106
111
|
)}
|
|
107
112
|
</Comp>
|
|
108
113
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useRef, useLayoutEffect, type ReactNode } from 'react'
|
|
1
|
+
import { useState, useRef, useLayoutEffect, type KeyboardEvent, type ReactNode } from 'react'
|
|
2
2
|
import { motion, AnimatePresence } from 'framer-motion'
|
|
3
3
|
import { cn } from '@/lib/utils'
|
|
4
4
|
import { MOTION_EXPAND } from '@/lib/motion'
|
|
@@ -89,6 +89,13 @@ export function CollapsibleCard({
|
|
|
89
89
|
setTimeout(() => setIsAnimating(false), 300)
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
const handleToggleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
93
|
+
if (!collapsible) return
|
|
94
|
+
if (event.key !== 'Enter' && event.key !== ' ') return
|
|
95
|
+
event.preventDefault()
|
|
96
|
+
handleToggle()
|
|
97
|
+
}
|
|
98
|
+
|
|
92
99
|
const showArrow = collapsible && (isHovered || headerIcon == null)
|
|
93
100
|
const headerIconNode = showArrow
|
|
94
101
|
? <ArrowDownSLine className={cn('w-[var(--font-size-sm)] h-[var(--font-size-sm)] shrink-0 transition-transform duration-200 text-text-secondary', isExpanded ? 'rotate-0' : '-rotate-90')} />
|
|
@@ -123,7 +130,9 @@ export function CollapsibleCard({
|
|
|
123
130
|
<div
|
|
124
131
|
className={cn('flex flex-row items-center gap-2 flex-1 min-w-0', collapsible && 'cursor-pointer')}
|
|
125
132
|
onClick={handleToggle}
|
|
133
|
+
onKeyDown={handleToggleKeyDown}
|
|
126
134
|
role={collapsible ? 'button' : undefined}
|
|
135
|
+
tabIndex={collapsible ? 0 : undefined}
|
|
127
136
|
aria-expanded={collapsible ? isExpanded : undefined}
|
|
128
137
|
>
|
|
129
138
|
<div className="w-[var(--font-size-sm)] h-[var(--font-size-sm)] flex items-center justify-center shrink-0 text-text-secondary [&>svg]:w-[var(--font-size-sm)] [&>svg]:h-[var(--font-size-sm)]">
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
forwardRef,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
type CSSProperties,
|
|
9
|
+
} from 'react'
|
|
10
|
+
import { clsx } from 'clsx'
|
|
11
|
+
import { Tooltip } from './tooltip'
|
|
12
|
+
|
|
13
|
+
export interface EllipsisTextProps {
|
|
14
|
+
/** Text or inline content to display. */
|
|
15
|
+
children: ReactNode
|
|
16
|
+
/**
|
|
17
|
+
* Maximum visible lines before truncation.
|
|
18
|
+
* @default 1
|
|
19
|
+
*/
|
|
20
|
+
lines?: number
|
|
21
|
+
/** Extra className for the text node. */
|
|
22
|
+
className?: string
|
|
23
|
+
/**
|
|
24
|
+
* Tooltip placement when overflow is detected.
|
|
25
|
+
* @default 'top'
|
|
26
|
+
*/
|
|
27
|
+
placement?: 'top' | 'bottom' | 'left' | 'right'
|
|
28
|
+
/** Extra className for Tooltip content. */
|
|
29
|
+
tooltipClassName?: string
|
|
30
|
+
/** Custom tooltip content. Defaults to children. */
|
|
31
|
+
tooltipContent?: ReactNode
|
|
32
|
+
/**
|
|
33
|
+
* Disable tooltip even when content overflows.
|
|
34
|
+
* @default false
|
|
35
|
+
*/
|
|
36
|
+
disabled?: boolean
|
|
37
|
+
/** Inline style for the text node. */
|
|
38
|
+
style?: CSSProperties
|
|
39
|
+
/**
|
|
40
|
+
* Rendered text element.
|
|
41
|
+
* @default 'span'
|
|
42
|
+
*/
|
|
43
|
+
as?: 'span' | 'div' | 'p'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function setRef<T>(ref: React.Ref<T> | undefined, value: T | null) {
|
|
47
|
+
if (!ref) return
|
|
48
|
+
if (typeof ref === 'function') ref(value)
|
|
49
|
+
else (ref as React.MutableRefObject<T | null>).current = value
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const EllipsisText = forwardRef<HTMLElement, EllipsisTextProps>(
|
|
53
|
+
(
|
|
54
|
+
{
|
|
55
|
+
children,
|
|
56
|
+
lines = 1,
|
|
57
|
+
className = '',
|
|
58
|
+
placement = 'top',
|
|
59
|
+
tooltipClassName = '',
|
|
60
|
+
tooltipContent,
|
|
61
|
+
disabled = false,
|
|
62
|
+
style,
|
|
63
|
+
as: Component = 'span',
|
|
64
|
+
},
|
|
65
|
+
ref
|
|
66
|
+
) => {
|
|
67
|
+
const textRef = useRef<HTMLElement>(null)
|
|
68
|
+
const [isOverflowing, setIsOverflowing] = useState(false)
|
|
69
|
+
|
|
70
|
+
const checkOverflow = useCallback(() => {
|
|
71
|
+
const el = textRef.current
|
|
72
|
+
if (!el) return
|
|
73
|
+
const overflowing =
|
|
74
|
+
lines === 1
|
|
75
|
+
? el.scrollWidth > el.clientWidth
|
|
76
|
+
: el.scrollHeight > el.clientHeight
|
|
77
|
+
setIsOverflowing(overflowing)
|
|
78
|
+
}, [lines])
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const raf = requestAnimationFrame(() => checkOverflow())
|
|
82
|
+
const el = textRef.current
|
|
83
|
+
if (!el) return () => cancelAnimationFrame(raf)
|
|
84
|
+
const ro = new ResizeObserver(() => checkOverflow())
|
|
85
|
+
ro.observe(el)
|
|
86
|
+
window.addEventListener('resize', checkOverflow)
|
|
87
|
+
return () => {
|
|
88
|
+
cancelAnimationFrame(raf)
|
|
89
|
+
ro.disconnect()
|
|
90
|
+
window.removeEventListener('resize', checkOverflow)
|
|
91
|
+
}
|
|
92
|
+
}, [checkOverflow, children])
|
|
93
|
+
|
|
94
|
+
const ellipsisStyle: CSSProperties =
|
|
95
|
+
lines === 1
|
|
96
|
+
? {
|
|
97
|
+
display: 'block',
|
|
98
|
+
overflow: 'hidden',
|
|
99
|
+
textOverflow: 'ellipsis',
|
|
100
|
+
whiteSpace: 'nowrap',
|
|
101
|
+
...style,
|
|
102
|
+
}
|
|
103
|
+
: {
|
|
104
|
+
overflow: 'hidden',
|
|
105
|
+
display: '-webkit-box',
|
|
106
|
+
WebkitLineClamp: lines,
|
|
107
|
+
WebkitBoxOrient: 'vertical',
|
|
108
|
+
...style,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const refCallback = useCallback(
|
|
112
|
+
(el: HTMLElement | null) => {
|
|
113
|
+
setRef(textRef, el)
|
|
114
|
+
setRef(ref, el)
|
|
115
|
+
},
|
|
116
|
+
[ref]
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
const triggerNode = (
|
|
120
|
+
<Component
|
|
121
|
+
ref={refCallback}
|
|
122
|
+
className={className}
|
|
123
|
+
style={ellipsisStyle}
|
|
124
|
+
>
|
|
125
|
+
{children}
|
|
126
|
+
</Component>
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
const useTooltip = isOverflowing && !disabled
|
|
130
|
+
if (useTooltip) {
|
|
131
|
+
return (
|
|
132
|
+
<Tooltip
|
|
133
|
+
content={tooltipContent ?? children}
|
|
134
|
+
side={placement}
|
|
135
|
+
contentClassName={clsx(tooltipClassName)}
|
|
136
|
+
>
|
|
137
|
+
<span
|
|
138
|
+
ref={refCallback}
|
|
139
|
+
className={className}
|
|
140
|
+
style={ellipsisStyle}
|
|
141
|
+
>
|
|
142
|
+
{children}
|
|
143
|
+
</span>
|
|
144
|
+
</Tooltip>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
return triggerNode
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
EllipsisText.displayName = 'EllipsisText'
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as LabelPrimitive from "@radix-ui/react-label"
|
|
5
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
6
|
+
import {
|
|
7
|
+
Controller,
|
|
8
|
+
FormProvider,
|
|
9
|
+
useFormContext,
|
|
10
|
+
type ControllerProps,
|
|
11
|
+
type FieldPath,
|
|
12
|
+
type FieldValues,
|
|
13
|
+
} from "react-hook-form"
|
|
14
|
+
|
|
15
|
+
import { cn } from "@/lib/utils"
|
|
16
|
+
import { Label } from "./label"
|
|
17
|
+
|
|
18
|
+
const Form = FormProvider
|
|
19
|
+
|
|
20
|
+
type FormFieldContextValue<
|
|
21
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
22
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
23
|
+
> = {
|
|
24
|
+
name: TName
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
|
28
|
+
{} as FormFieldContextValue
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
function FormField<
|
|
32
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
33
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
34
|
+
>({ ...props }: ControllerProps<TFieldValues, TName>) {
|
|
35
|
+
return (
|
|
36
|
+
<FormFieldContext.Provider value={{ name: props.name }}>
|
|
37
|
+
<Controller {...props} />
|
|
38
|
+
</FormFieldContext.Provider>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type FormItemContextValue = {
|
|
43
|
+
id: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const FormItemContext = React.createContext<FormItemContextValue>(
|
|
47
|
+
{} as FormItemContextValue
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
function useFormField() {
|
|
51
|
+
const fieldContext = React.useContext(FormFieldContext)
|
|
52
|
+
const itemContext = React.useContext(FormItemContext)
|
|
53
|
+
const { getFieldState, formState } = useFormContext()
|
|
54
|
+
|
|
55
|
+
if (!fieldContext.name) {
|
|
56
|
+
throw new Error("useFormField should be used within <FormField>")
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const fieldState = getFieldState(fieldContext.name, formState)
|
|
60
|
+
|
|
61
|
+
const { id } = itemContext
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
id,
|
|
65
|
+
name: fieldContext.name,
|
|
66
|
+
formItemId: `${id}-form-item`,
|
|
67
|
+
formDescriptionId: `${id}-form-item-description`,
|
|
68
|
+
formMessageId: `${id}-form-item-message`,
|
|
69
|
+
...fieldState,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const FormItem = React.forwardRef<
|
|
74
|
+
HTMLDivElement,
|
|
75
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
76
|
+
>(({ className, ...props }, ref) => {
|
|
77
|
+
const id = React.useId()
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<FormItemContext.Provider value={{ id }}>
|
|
81
|
+
<div
|
|
82
|
+
ref={ref}
|
|
83
|
+
data-slot="form-item"
|
|
84
|
+
className={cn("space-y-2", className)}
|
|
85
|
+
{...props}
|
|
86
|
+
/>
|
|
87
|
+
</FormItemContext.Provider>
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
FormItem.displayName = "FormItem"
|
|
91
|
+
|
|
92
|
+
const FormLabel = React.forwardRef<
|
|
93
|
+
React.ElementRef<typeof LabelPrimitive.Root>,
|
|
94
|
+
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
|
95
|
+
>(({ className, ...props }, ref) => {
|
|
96
|
+
const { error, formItemId } = useFormField()
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<Label
|
|
100
|
+
ref={ref}
|
|
101
|
+
data-slot="form-label"
|
|
102
|
+
data-error={!!error}
|
|
103
|
+
className={cn("data-[error=true]:text-error", className)}
|
|
104
|
+
htmlFor={formItemId}
|
|
105
|
+
{...props}
|
|
106
|
+
/>
|
|
107
|
+
)
|
|
108
|
+
})
|
|
109
|
+
FormLabel.displayName = "FormLabel"
|
|
110
|
+
|
|
111
|
+
const FormControl = React.forwardRef<
|
|
112
|
+
React.ElementRef<typeof Slot>,
|
|
113
|
+
React.ComponentPropsWithoutRef<typeof Slot>
|
|
114
|
+
>(({ ...props }, ref) => {
|
|
115
|
+
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<Slot
|
|
119
|
+
ref={ref}
|
|
120
|
+
data-slot="form-control"
|
|
121
|
+
id={formItemId}
|
|
122
|
+
aria-describedby={
|
|
123
|
+
!error
|
|
124
|
+
? `${formDescriptionId}`
|
|
125
|
+
: `${formDescriptionId} ${formMessageId}`
|
|
126
|
+
}
|
|
127
|
+
aria-invalid={!!error}
|
|
128
|
+
{...props}
|
|
129
|
+
/>
|
|
130
|
+
)
|
|
131
|
+
})
|
|
132
|
+
FormControl.displayName = "FormControl"
|
|
133
|
+
|
|
134
|
+
const FormDescription = React.forwardRef<
|
|
135
|
+
HTMLParagraphElement,
|
|
136
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
137
|
+
>(({ className, ...props }, ref) => {
|
|
138
|
+
const { formDescriptionId } = useFormField()
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<p
|
|
142
|
+
ref={ref}
|
|
143
|
+
id={formDescriptionId}
|
|
144
|
+
data-slot="form-description"
|
|
145
|
+
className={cn("text-sm text-text-tertiary", className)}
|
|
146
|
+
{...props}
|
|
147
|
+
/>
|
|
148
|
+
)
|
|
149
|
+
})
|
|
150
|
+
FormDescription.displayName = "FormDescription"
|
|
151
|
+
|
|
152
|
+
const FormMessage = React.forwardRef<
|
|
153
|
+
HTMLParagraphElement,
|
|
154
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
155
|
+
>(({ className, children, ...props }, ref) => {
|
|
156
|
+
const { error, formMessageId } = useFormField()
|
|
157
|
+
const body = error ? String(error?.message ?? "") : children
|
|
158
|
+
|
|
159
|
+
if (!body) {
|
|
160
|
+
return null
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<p
|
|
165
|
+
ref={ref}
|
|
166
|
+
id={formMessageId}
|
|
167
|
+
data-slot="form-message"
|
|
168
|
+
className={cn("text-sm font-medium text-error", className)}
|
|
169
|
+
{...props}
|
|
170
|
+
>
|
|
171
|
+
{body}
|
|
172
|
+
</p>
|
|
173
|
+
)
|
|
174
|
+
})
|
|
175
|
+
FormMessage.displayName = "FormMessage"
|
|
176
|
+
|
|
177
|
+
export {
|
|
178
|
+
useFormField,
|
|
179
|
+
Form,
|
|
180
|
+
FormItem,
|
|
181
|
+
FormLabel,
|
|
182
|
+
FormControl,
|
|
183
|
+
FormDescription,
|
|
184
|
+
FormMessage,
|
|
185
|
+
FormField,
|
|
186
|
+
}
|
|
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from 'class-variance-authority'
|
|
|
5
5
|
import { cn } from '@/lib/utils'
|
|
6
6
|
|
|
7
7
|
const iconButtonVariants = cva(
|
|
8
|
-
'flex-none shrink-0 inline-flex items-center justify-center box-border transition-[
|
|
8
|
+
'flex-none shrink-0 inline-flex items-center justify-center box-border transition-[color,background-color,border-color,box-shadow,transform] duration-200 active:scale-[0.98] focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer [&>*]:flex [&>*]:items-center [&>*]:justify-center [&>*]:size-full [&>*]:[&>svg]:block [&>*]:[&>svg]:leading-none',
|
|
9
9
|
{
|
|
10
10
|
variants: {
|
|
11
11
|
variant: {
|
|
@@ -34,9 +34,17 @@ const iconButtonVariants = cva(
|
|
|
34
34
|
}
|
|
35
35
|
)
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
type IconButtonAccessibleName =
|
|
38
|
+
| { 'aria-label': string; 'aria-labelledby'?: string; title?: string }
|
|
39
|
+
| { 'aria-label'?: string; 'aria-labelledby': string; title?: string }
|
|
40
|
+
| { 'aria-label'?: string; 'aria-labelledby'?: string; title: string }
|
|
41
|
+
|
|
42
|
+
export type IconButtonProps = Omit<
|
|
43
|
+
ButtonHTMLAttributes<HTMLButtonElement>,
|
|
44
|
+
'aria-label' | 'aria-labelledby' | 'title'
|
|
45
|
+
> &
|
|
46
|
+
VariantProps<typeof iconButtonVariants> &
|
|
47
|
+
IconButtonAccessibleName & {
|
|
40
48
|
icon: ReactNode
|
|
41
49
|
asChild?: boolean
|
|
42
50
|
}
|