7onic 0.1.3 → 0.1.4
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/index.js +1651 -154
- package/llms.txt +3 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1830,9 +1830,9 @@ const alertVariants = cva(
|
|
|
1830
1830
|
{
|
|
1831
1831
|
variants: {
|
|
1832
1832
|
size: {
|
|
1833
|
-
sm: 'gap-2 p-
|
|
1834
|
-
default: 'gap-2.5 p-
|
|
1835
|
-
lg: 'gap-3 p-
|
|
1833
|
+
sm: 'gap-2 p-2.5 text-sm',
|
|
1834
|
+
default: 'gap-2.5 p-3.5 text-md',
|
|
1835
|
+
lg: 'gap-3 p-4 text-md',
|
|
1836
1836
|
},
|
|
1837
1837
|
radius: {
|
|
1838
1838
|
none: 'rounded-none',
|
|
@@ -1860,16 +1860,23 @@ const ICON_SIZE_MAP = {
|
|
|
1860
1860
|
lg: 'icon-md', // 20px
|
|
1861
1861
|
} as const
|
|
1862
1862
|
|
|
1863
|
+
/** Icon wrapper text class \u2014 matches title line-height for vertical alignment */
|
|
1864
|
+
const ICON_LINE_MAP = {
|
|
1865
|
+
sm: 'text-sm',
|
|
1866
|
+
default: 'text-md',
|
|
1867
|
+
lg: 'text-base',
|
|
1868
|
+
} as const
|
|
1869
|
+
|
|
1863
1870
|
const TITLE_SIZE_MAP = {
|
|
1864
|
-
sm: 'text-sm font-semibold
|
|
1865
|
-
default: 'font-semibold
|
|
1866
|
-
lg: 'text-base font-semibold
|
|
1871
|
+
sm: 'text-sm font-semibold tracking-tight',
|
|
1872
|
+
default: 'text-md font-semibold tracking-tight',
|
|
1873
|
+
lg: 'text-base font-semibold tracking-tight',
|
|
1867
1874
|
} as const
|
|
1868
1875
|
|
|
1869
1876
|
const DESC_SIZE_MAP = {
|
|
1870
1877
|
sm: 'text-xs mt-0.5',
|
|
1871
|
-
default: 'text-sm mt-
|
|
1872
|
-
lg: 'text-md mt-
|
|
1878
|
+
default: 'text-sm mt-0.5',
|
|
1879
|
+
lg: 'text-md mt-0.5',
|
|
1873
1880
|
} as const
|
|
1874
1881
|
|
|
1875
1882
|
// \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
@@ -1954,7 +1961,7 @@ const AlertRoot = React.forwardRef<HTMLDivElement, AlertRootProps>(
|
|
|
1954
1961
|
>
|
|
1955
1962
|
{/* Icon */}
|
|
1956
1963
|
{!hideIcon && (
|
|
1957
|
-
<span className=
|
|
1964
|
+
<span className={cn('shrink-0 flex items-center min-h-[1lh]', ICON_LINE_MAP[size])}>
|
|
1958
1965
|
{icon || <StatusIcon className={ICON_SIZE_MAP[size]} />}
|
|
1959
1966
|
</span>
|
|
1960
1967
|
)}
|
|
@@ -3391,7 +3398,7 @@ const CardTitle = React.forwardRef<HTMLHeadingElement, CardTitleProps>(
|
|
|
3391
3398
|
<h3
|
|
3392
3399
|
ref={ref}
|
|
3393
3400
|
className={cn(
|
|
3394
|
-
'font-semibold text-foreground
|
|
3401
|
+
'text-base font-semibold text-foreground tracking-tight',
|
|
3395
3402
|
icon && 'flex items-center gap-2',
|
|
3396
3403
|
className
|
|
3397
3404
|
)}
|
|
@@ -4339,75 +4346,1105 @@ function ChartPie({
|
|
|
4339
4346
|
const angle = Math.abs(entry.endAngle - entry.startAngle)
|
|
4340
4347
|
if (angle < CHART_PIE_SKIP_ANGLE) return null
|
|
4341
4348
|
|
|
4342
|
-
const RADIAN = Math.PI / 180
|
|
4343
|
-
const { cx, cy, innerRadius: ir, outerRadius: or, midAngle } = entry
|
|
4344
|
-
// Pie: push toward outer edge (0.65). Donut ring: use midpoint (0.5)
|
|
4345
|
-
const ratio = ir > 0 ? 0.5 : 0.65
|
|
4346
|
-
const radius = ir + (or - ir) * ratio
|
|
4347
|
-
const x = cx + radius * Math.cos(-midAngle * RADIAN)
|
|
4348
|
-
const y = cy + radius * Math.sin(-midAngle * RADIAN)
|
|
4349
|
+
const RADIAN = Math.PI / 180
|
|
4350
|
+
const { cx, cy, innerRadius: ir, outerRadius: or, midAngle } = entry
|
|
4351
|
+
// Pie: push toward outer edge (0.65). Donut ring: use midpoint (0.5)
|
|
4352
|
+
const ratio = ir > 0 ? 0.5 : 0.65
|
|
4353
|
+
const radius = ir + (or - ir) * ratio
|
|
4354
|
+
const x = cx + radius * Math.cos(-midAngle * RADIAN)
|
|
4355
|
+
const y = cy + radius * Math.sin(-midAngle * RADIAN)
|
|
4356
|
+
|
|
4357
|
+
return (
|
|
4358
|
+
<text
|
|
4359
|
+
x={x}
|
|
4360
|
+
y={y}
|
|
4361
|
+
textAnchor="middle"
|
|
4362
|
+
dominantBaseline="central"
|
|
4363
|
+
style={{ fontSize: 'var(--font-size-xs)', fill: 'white', fontWeight: 600 }}
|
|
4364
|
+
>
|
|
4365
|
+
{getDisplayText(entry)}
|
|
4366
|
+
</text>
|
|
4367
|
+
)
|
|
4368
|
+
}
|
|
4369
|
+
: false
|
|
4370
|
+
|
|
4371
|
+
return (
|
|
4372
|
+
<RechartsPrimitive.Pie
|
|
4373
|
+
innerRadius={resolvedInnerRadius}
|
|
4374
|
+
paddingAngle={paddingAngle}
|
|
4375
|
+
cornerRadius={cornerRadius}
|
|
4376
|
+
startAngle={startAngle}
|
|
4377
|
+
endAngle={endAngle}
|
|
4378
|
+
label={labelConfig as any}
|
|
4379
|
+
labelLine={false}
|
|
4380
|
+
activeShape={activeShapeConfig as any}
|
|
4381
|
+
{...props}
|
|
4382
|
+
/>
|
|
4383
|
+
)
|
|
4384
|
+
}
|
|
4385
|
+
|
|
4386
|
+
// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4387
|
+
|
|
4388
|
+
// \u2500\u2500\u2500 Namespace \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4389
|
+
const Chart = Object.assign(ChartContainer, {
|
|
4390
|
+
Bar: ChartBar,
|
|
4391
|
+
Line: ChartLine,
|
|
4392
|
+
Area: ChartArea,
|
|
4393
|
+
Pie: ChartPie,
|
|
4394
|
+
Tooltip: ChartTooltip,
|
|
4395
|
+
TooltipContent: ChartTooltipContent,
|
|
4396
|
+
Legend: ChartLegend,
|
|
4397
|
+
LegendContent: ChartLegendContent,
|
|
4398
|
+
XAxis: ChartXAxis,
|
|
4399
|
+
YAxis: ChartYAxis,
|
|
4400
|
+
Style: ChartStyle,
|
|
4401
|
+
})
|
|
4402
|
+
|
|
4403
|
+
export {
|
|
4404
|
+
Chart,
|
|
4405
|
+
ChartContainer,
|
|
4406
|
+
ChartBar,
|
|
4407
|
+
ChartLine,
|
|
4408
|
+
ChartArea,
|
|
4409
|
+
ChartPie,
|
|
4410
|
+
ChartTooltip,
|
|
4411
|
+
ChartTooltipContent,
|
|
4412
|
+
ChartLegend,
|
|
4413
|
+
ChartLegendContent,
|
|
4414
|
+
ChartXAxis,
|
|
4415
|
+
ChartYAxis,
|
|
4416
|
+
ChartStyle,
|
|
4417
|
+
useChart,
|
|
4418
|
+
}
|
|
4419
|
+
`,
|
|
4420
|
+
type: "ui"
|
|
4421
|
+
}]
|
|
4422
|
+
},
|
|
4423
|
+
"chat-input": {
|
|
4424
|
+
name: "chat-input",
|
|
4425
|
+
dependencies: [],
|
|
4426
|
+
registryDependencies: [],
|
|
4427
|
+
reverseDependencies: [],
|
|
4428
|
+
namespace: true,
|
|
4429
|
+
description: "",
|
|
4430
|
+
files: [{
|
|
4431
|
+
path: "chat-input.tsx",
|
|
4432
|
+
content: `'use client'
|
|
4433
|
+
|
|
4434
|
+
import * as React from 'react'
|
|
4435
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
4436
|
+
import { cn } from '@/lib/utils'
|
|
4437
|
+
|
|
4438
|
+
// ============================================================================
|
|
4439
|
+
// Size maps
|
|
4440
|
+
// ============================================================================
|
|
4441
|
+
|
|
4442
|
+
/** Textarea padding per size */
|
|
4443
|
+
const fieldPaddingMap = {
|
|
4444
|
+
sm: 'pt-2.5 px-3 text-md',
|
|
4445
|
+
default: 'pt-3 px-4 text-md',
|
|
4446
|
+
lg: 'pt-4 px-5 text-base',
|
|
4447
|
+
} as const
|
|
4448
|
+
|
|
4449
|
+
/** Submit row padding per size */
|
|
4450
|
+
const submitWrapperMap = {
|
|
4451
|
+
sm: 'px-2.5 pb-2.5',
|
|
4452
|
+
default: 'px-3 pb-3',
|
|
4453
|
+
lg: 'px-4 pb-4',
|
|
4454
|
+
} as const
|
|
4455
|
+
|
|
4456
|
+
/** Submit button dimensions per size */
|
|
4457
|
+
const submitSizeMap = {
|
|
4458
|
+
sm: 'h-7 w-7',
|
|
4459
|
+
default: 'h-8 w-8',
|
|
4460
|
+
lg: 'h-9 w-9',
|
|
4461
|
+
} as const
|
|
4462
|
+
|
|
4463
|
+
/** Submit button radius \u2014 auto-calculated from container radius (min rounded-md) */
|
|
4464
|
+
const submitRadiusMap = {
|
|
4465
|
+
sm: 'rounded-md',
|
|
4466
|
+
md: 'rounded-md',
|
|
4467
|
+
lg: 'rounded-md',
|
|
4468
|
+
xl: 'rounded-lg',
|
|
4469
|
+
'2xl': 'rounded-xl',
|
|
4470
|
+
full: 'rounded-full',
|
|
4471
|
+
} as const
|
|
4472
|
+
|
|
4473
|
+
/** Submit button radius \u2014 direct class map for explicit buttonRadius prop */
|
|
4474
|
+
const buttonRadiusClassMap: Record<ChatInputRadius, string> = {
|
|
4475
|
+
sm: 'rounded-sm',
|
|
4476
|
+
md: 'rounded-md',
|
|
4477
|
+
lg: 'rounded-lg',
|
|
4478
|
+
xl: 'rounded-xl',
|
|
4479
|
+
'2xl': 'rounded-2xl',
|
|
4480
|
+
full: 'rounded-full',
|
|
4481
|
+
}
|
|
4482
|
+
|
|
4483
|
+
/** Submit icon size class per size */
|
|
4484
|
+
const submitIconSizeMap = {
|
|
4485
|
+
sm: 'icon-xs',
|
|
4486
|
+
default: 'icon-xs',
|
|
4487
|
+
lg: 'icon-sm',
|
|
4488
|
+
} as const
|
|
4489
|
+
|
|
4490
|
+
/** Character count text size per size */
|
|
4491
|
+
const countStyleMap = {
|
|
4492
|
+
sm: 'text-xs',
|
|
4493
|
+
default: 'text-xs',
|
|
4494
|
+
lg: 'text-sm',
|
|
4495
|
+
} as const
|
|
4496
|
+
|
|
4497
|
+
/** Approximate line-height (px) per size for auto-resize */
|
|
4498
|
+
const lineHeightMap = {
|
|
4499
|
+
sm: 20,
|
|
4500
|
+
default: 22,
|
|
4501
|
+
lg: 24,
|
|
4502
|
+
} as const
|
|
4503
|
+
|
|
4504
|
+
/** Field padding in inline layout \u2014 horizontal only (vertical centering via flex) */
|
|
4505
|
+
const inlineFieldPaddingMap = {
|
|
4506
|
+
sm: 'px-3 text-md',
|
|
4507
|
+
default: 'px-4 text-md',
|
|
4508
|
+
lg: 'px-5 text-base',
|
|
4509
|
+
} as const
|
|
4510
|
+
|
|
4511
|
+
/** Submit wrapper padding in inline layout \u2014 symmetric vertical */
|
|
4512
|
+
const submitWrapperInlineMap = {
|
|
4513
|
+
sm: 'px-2.5 py-2.5',
|
|
4514
|
+
default: 'px-3 py-3',
|
|
4515
|
+
lg: 'px-4 py-4',
|
|
4516
|
+
} as const
|
|
4517
|
+
|
|
4518
|
+
// ============================================================================
|
|
4519
|
+
// CVA \u2014 root container variants
|
|
4520
|
+
// ============================================================================
|
|
4521
|
+
|
|
4522
|
+
const chatInputVariants = cva(
|
|
4523
|
+
[
|
|
4524
|
+
'flex flex-col w-full overflow-hidden cursor-text',
|
|
4525
|
+
'transition-colors duration-micro',
|
|
4526
|
+
].join(' '),
|
|
4527
|
+
{
|
|
4528
|
+
variants: {
|
|
4529
|
+
variant: {
|
|
4530
|
+
outline: [
|
|
4531
|
+
'border border-border bg-background',
|
|
4532
|
+
'hover:border-border-strong',
|
|
4533
|
+
'focus-within:border-border-strong focus-within:hover:border-border-strong',
|
|
4534
|
+
].join(' '),
|
|
4535
|
+
filled: [
|
|
4536
|
+
'border border-transparent bg-background-muted',
|
|
4537
|
+
'focus-within:border-border',
|
|
4538
|
+
].join(' '),
|
|
4539
|
+
},
|
|
4540
|
+
radius: {
|
|
4541
|
+
sm: 'rounded-sm',
|
|
4542
|
+
md: 'rounded-md',
|
|
4543
|
+
lg: 'rounded-lg',
|
|
4544
|
+
xl: 'rounded-xl',
|
|
4545
|
+
'2xl': 'rounded-2xl',
|
|
4546
|
+
full: 'rounded-full',
|
|
4547
|
+
},
|
|
4548
|
+
},
|
|
4549
|
+
defaultVariants: {
|
|
4550
|
+
variant: 'outline',
|
|
4551
|
+
radius: 'xl',
|
|
4552
|
+
},
|
|
4553
|
+
}
|
|
4554
|
+
)
|
|
4555
|
+
|
|
4556
|
+
// ============================================================================
|
|
4557
|
+
// Types
|
|
4558
|
+
// ============================================================================
|
|
4559
|
+
|
|
4560
|
+
export type ChatInputSize = 'sm' | 'default' | 'lg'
|
|
4561
|
+
export type ChatInputVariant = 'outline' | 'filled'
|
|
4562
|
+
export type ChatInputRadius = 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full'
|
|
4563
|
+
export type ChatInputColor = 'default' | 'primary'
|
|
4564
|
+
export type ChatInputLayout = 'default' | 'inline'
|
|
4565
|
+
|
|
4566
|
+
// ============================================================================
|
|
4567
|
+
// Context
|
|
4568
|
+
// ============================================================================
|
|
4569
|
+
|
|
4570
|
+
interface ChatInputContextValue {
|
|
4571
|
+
size: ChatInputSize
|
|
4572
|
+
color: ChatInputColor
|
|
4573
|
+
layout: ChatInputLayout
|
|
4574
|
+
radius: ChatInputRadius
|
|
4575
|
+
disabled: boolean
|
|
4576
|
+
isEmpty: boolean
|
|
4577
|
+
charCount: number
|
|
4578
|
+
showCount: boolean
|
|
4579
|
+
maxLength: number | undefined
|
|
4580
|
+
_setIsEmpty: React.Dispatch<React.SetStateAction<boolean>>
|
|
4581
|
+
_setCharCount: React.Dispatch<React.SetStateAction<number>>
|
|
4582
|
+
_setShowCount: React.Dispatch<React.SetStateAction<boolean>>
|
|
4583
|
+
_setMaxLength: React.Dispatch<React.SetStateAction<number | undefined>>
|
|
4584
|
+
_fieldRef: React.MutableRefObject<HTMLTextAreaElement | null>
|
|
4585
|
+
_handleSubmit: () => void
|
|
4586
|
+
}
|
|
4587
|
+
|
|
4588
|
+
const ChatInputContext = React.createContext<ChatInputContextValue>({
|
|
4589
|
+
size: 'default',
|
|
4590
|
+
color: 'default',
|
|
4591
|
+
layout: 'default',
|
|
4592
|
+
radius: 'xl',
|
|
4593
|
+
disabled: false,
|
|
4594
|
+
isEmpty: true,
|
|
4595
|
+
charCount: 0,
|
|
4596
|
+
showCount: false,
|
|
4597
|
+
maxLength: undefined,
|
|
4598
|
+
_setIsEmpty: () => {},
|
|
4599
|
+
_setCharCount: () => {},
|
|
4600
|
+
_setShowCount: () => {},
|
|
4601
|
+
_setMaxLength: () => {},
|
|
4602
|
+
_fieldRef: { current: null },
|
|
4603
|
+
_handleSubmit: () => {},
|
|
4604
|
+
})
|
|
4605
|
+
|
|
4606
|
+
// ============================================================================
|
|
4607
|
+
// Props interfaces
|
|
4608
|
+
// ============================================================================
|
|
4609
|
+
|
|
4610
|
+
export interface ChatInputRootProps
|
|
4611
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onSubmit'>,
|
|
4612
|
+
VariantProps<typeof chatInputVariants> {
|
|
4613
|
+
/** Visual style of the container */
|
|
4614
|
+
variant?: ChatInputVariant
|
|
4615
|
+
/** Corner radius */
|
|
4616
|
+
radius?: ChatInputRadius
|
|
4617
|
+
/** Size scale */
|
|
4618
|
+
size?: ChatInputSize
|
|
4619
|
+
/** Submit button color \u2014 default (dark) or primary (brand) */
|
|
4620
|
+
color?: ChatInputColor
|
|
4621
|
+
/** Layout direction \u2014 default (stacked) or inline (field + button side by side) */
|
|
4622
|
+
layout?: ChatInputLayout
|
|
4623
|
+
/** Disable all interaction */
|
|
4624
|
+
disabled?: boolean
|
|
4625
|
+
/** Called when the user submits (Enter key or Submit button) \u2014 receives current field value */
|
|
4626
|
+
onSubmit?: (value: string) => void
|
|
4627
|
+
}
|
|
4628
|
+
|
|
4629
|
+
export interface ChatInputFieldProps
|
|
4630
|
+
extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'children'> {
|
|
4631
|
+
/** Maximum visible rows before the field scrolls */
|
|
4632
|
+
maxRows?: number
|
|
4633
|
+
/** Show character counter \u2014 requires maxLength to be set */
|
|
4634
|
+
showCount?: boolean
|
|
4635
|
+
}
|
|
4636
|
+
|
|
4637
|
+
export interface ChatInputSubmitProps
|
|
4638
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
4639
|
+
/** Show loading/stop state \u2014 keeps button enabled so user can stop generation */
|
|
4640
|
+
loading?: boolean
|
|
4641
|
+
/** Called when clicked during loading state (e.g., stop generation) */
|
|
4642
|
+
onStop?: () => void
|
|
4643
|
+
/** Override the auto-calculated button radius */
|
|
4644
|
+
buttonRadius?: ChatInputRadius
|
|
4645
|
+
}
|
|
4646
|
+
|
|
4647
|
+
// ============================================================================
|
|
4648
|
+
// ChatInput \u2014 Root container
|
|
4649
|
+
// ============================================================================
|
|
4650
|
+
|
|
4651
|
+
const ChatInputRoot = React.forwardRef<HTMLDivElement, ChatInputRootProps>(
|
|
4652
|
+
(
|
|
4653
|
+
{
|
|
4654
|
+
className,
|
|
4655
|
+
variant = 'outline',
|
|
4656
|
+
radius = 'xl',
|
|
4657
|
+
size = 'default',
|
|
4658
|
+
color = 'default',
|
|
4659
|
+
layout = 'default',
|
|
4660
|
+
disabled = false,
|
|
4661
|
+
onSubmit,
|
|
4662
|
+
children,
|
|
4663
|
+
...props
|
|
4664
|
+
},
|
|
4665
|
+
ref
|
|
4666
|
+
) => {
|
|
4667
|
+
const [isEmpty, setIsEmpty] = React.useState(true)
|
|
4668
|
+
const [charCount, setCharCount] = React.useState(0)
|
|
4669
|
+
const [showCount, setShowCount] = React.useState(false)
|
|
4670
|
+
const [maxLength, setMaxLength] = React.useState<number | undefined>(undefined)
|
|
4671
|
+
const fieldRef = React.useRef<HTMLTextAreaElement | null>(null)
|
|
4672
|
+
|
|
4673
|
+
const handleSubmit = React.useCallback(() => {
|
|
4674
|
+
const el = fieldRef.current
|
|
4675
|
+
if (!el || disabled) return
|
|
4676
|
+
const value = el.value.trim()
|
|
4677
|
+
if (!value) return
|
|
4678
|
+
onSubmit?.(value)
|
|
4679
|
+
// Auto-clear in uncontrolled mode
|
|
4680
|
+
// Controlled-mode users should clear value inside their onSubmit callback
|
|
4681
|
+
el.value = ''
|
|
4682
|
+
el.style.height = 'auto'
|
|
4683
|
+
setIsEmpty(true)
|
|
4684
|
+
setCharCount(0)
|
|
4685
|
+
}, [disabled, onSubmit])
|
|
4686
|
+
|
|
4687
|
+
const contextValue = React.useMemo<ChatInputContextValue>(
|
|
4688
|
+
() => ({
|
|
4689
|
+
size,
|
|
4690
|
+
color,
|
|
4691
|
+
layout,
|
|
4692
|
+
radius,
|
|
4693
|
+
disabled,
|
|
4694
|
+
isEmpty,
|
|
4695
|
+
charCount,
|
|
4696
|
+
showCount,
|
|
4697
|
+
maxLength,
|
|
4698
|
+
_setIsEmpty: setIsEmpty,
|
|
4699
|
+
_setCharCount: setCharCount,
|
|
4700
|
+
_setShowCount: setShowCount,
|
|
4701
|
+
_setMaxLength: setMaxLength,
|
|
4702
|
+
_fieldRef: fieldRef,
|
|
4703
|
+
_handleSubmit: handleSubmit,
|
|
4704
|
+
}),
|
|
4705
|
+
[size, color, layout, radius, disabled, isEmpty, charCount, showCount, maxLength, handleSubmit]
|
|
4706
|
+
)
|
|
4707
|
+
|
|
4708
|
+
const handleContainerClick = React.useCallback(
|
|
4709
|
+
(e: React.MouseEvent<HTMLDivElement>) => {
|
|
4710
|
+
// Focus the textarea when clicking anywhere in the container (not on a button)
|
|
4711
|
+
if (!disabled && !(e.target as HTMLElement).closest('button')) {
|
|
4712
|
+
fieldRef.current?.focus()
|
|
4713
|
+
}
|
|
4714
|
+
},
|
|
4715
|
+
[disabled]
|
|
4716
|
+
)
|
|
4717
|
+
|
|
4718
|
+
return (
|
|
4719
|
+
<ChatInputContext.Provider value={contextValue}>
|
|
4720
|
+
<div
|
|
4721
|
+
ref={ref}
|
|
4722
|
+
data-disabled={disabled || undefined}
|
|
4723
|
+
onClick={handleContainerClick}
|
|
4724
|
+
className={cn(
|
|
4725
|
+
chatInputVariants({ variant, radius }),
|
|
4726
|
+
layout === 'inline' && 'flex-row',
|
|
4727
|
+
disabled && 'opacity-50 pointer-events-none',
|
|
4728
|
+
className
|
|
4729
|
+
)}
|
|
4730
|
+
{...props}
|
|
4731
|
+
>
|
|
4732
|
+
{children}
|
|
4733
|
+
</div>
|
|
4734
|
+
</ChatInputContext.Provider>
|
|
4735
|
+
)
|
|
4736
|
+
}
|
|
4737
|
+
)
|
|
4738
|
+
ChatInputRoot.displayName = 'ChatInput'
|
|
4739
|
+
|
|
4740
|
+
// ============================================================================
|
|
4741
|
+
// ChatInput.Field \u2014 Auto-resizing textarea
|
|
4742
|
+
// ============================================================================
|
|
4743
|
+
|
|
4744
|
+
const ChatInputField = React.forwardRef<HTMLTextAreaElement, ChatInputFieldProps>(
|
|
4745
|
+
(
|
|
4746
|
+
{
|
|
4747
|
+
className,
|
|
4748
|
+
maxRows = 8,
|
|
4749
|
+
showCount = false,
|
|
4750
|
+
maxLength,
|
|
4751
|
+
placeholder,
|
|
4752
|
+
onChange,
|
|
4753
|
+
onKeyDown,
|
|
4754
|
+
disabled,
|
|
4755
|
+
...props
|
|
4756
|
+
},
|
|
4757
|
+
forwardedRef
|
|
4758
|
+
) => {
|
|
4759
|
+
const ctx = React.useContext(ChatInputContext)
|
|
4760
|
+
const internalRef = React.useRef<HTMLTextAreaElement | null>(null)
|
|
4761
|
+
const isDisabled = disabled ?? ctx.disabled
|
|
4762
|
+
|
|
4763
|
+
// Sync showCount + maxLength into root context on mount/update
|
|
4764
|
+
React.useEffect(() => {
|
|
4765
|
+
ctx._setShowCount(showCount)
|
|
4766
|
+
ctx._setMaxLength(maxLength)
|
|
4767
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
4768
|
+
}, [showCount, maxLength])
|
|
4769
|
+
|
|
4770
|
+
// Merge forwarded ref + internal ref + context field ref
|
|
4771
|
+
const setRef = React.useCallback(
|
|
4772
|
+
(el: HTMLTextAreaElement | null) => {
|
|
4773
|
+
internalRef.current = el
|
|
4774
|
+
ctx._fieldRef.current = el
|
|
4775
|
+
if (typeof forwardedRef === 'function') {
|
|
4776
|
+
forwardedRef(el)
|
|
4777
|
+
} else if (forwardedRef) {
|
|
4778
|
+
(forwardedRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = el
|
|
4779
|
+
}
|
|
4780
|
+
},
|
|
4781
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
4782
|
+
[forwardedRef]
|
|
4783
|
+
)
|
|
4784
|
+
|
|
4785
|
+
// Adjust textarea height to content, bounded by maxRows
|
|
4786
|
+
const autoResize = React.useCallback(() => {
|
|
4787
|
+
const el = internalRef.current
|
|
4788
|
+
if (!el) return
|
|
4789
|
+
el.style.height = 'auto'
|
|
4790
|
+
const maxHeight = maxRows * lineHeightMap[ctx.size]
|
|
4791
|
+
const newHeight = Math.min(el.scrollHeight, maxHeight)
|
|
4792
|
+
el.style.height = \`\${newHeight}px\`
|
|
4793
|
+
el.style.overflowY = el.scrollHeight > maxHeight ? 'auto' : 'hidden'
|
|
4794
|
+
}, [ctx.size, maxRows])
|
|
4795
|
+
|
|
4796
|
+
const handleChange = React.useCallback(
|
|
4797
|
+
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
4798
|
+
const val = e.target.value
|
|
4799
|
+
ctx._setIsEmpty(val.trim() === '')
|
|
4800
|
+
ctx._setCharCount(val.length)
|
|
4801
|
+
autoResize()
|
|
4802
|
+
onChange?.(e)
|
|
4803
|
+
},
|
|
4804
|
+
[ctx, autoResize, onChange]
|
|
4805
|
+
)
|
|
4806
|
+
|
|
4807
|
+
const handleKeyDown = React.useCallback(
|
|
4808
|
+
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
4809
|
+
// Enter = submit | Shift+Enter = new line
|
|
4810
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
4811
|
+
e.preventDefault()
|
|
4812
|
+
ctx._handleSubmit()
|
|
4813
|
+
}
|
|
4814
|
+
onKeyDown?.(e)
|
|
4815
|
+
},
|
|
4816
|
+
[ctx, onKeyDown]
|
|
4817
|
+
)
|
|
4818
|
+
|
|
4819
|
+
return (
|
|
4820
|
+
<div className={cn('flex-1 min-w-0', ctx.layout === 'inline' && 'flex items-center self-stretch')}>
|
|
4821
|
+
<textarea
|
|
4822
|
+
ref={setRef}
|
|
4823
|
+
rows={1}
|
|
4824
|
+
placeholder={placeholder}
|
|
4825
|
+
maxLength={maxLength}
|
|
4826
|
+
disabled={isDisabled}
|
|
4827
|
+
className={cn(
|
|
4828
|
+
'w-full bg-transparent text-foreground placeholder:text-foreground/30',
|
|
4829
|
+
'resize-none outline-none focus:outline-none',
|
|
4830
|
+
'disabled:cursor-not-allowed',
|
|
4831
|
+
'overflow-hidden',
|
|
4832
|
+
ctx.layout === 'inline' ? inlineFieldPaddingMap[ctx.size] : fieldPaddingMap[ctx.size],
|
|
4833
|
+
className
|
|
4834
|
+
)}
|
|
4835
|
+
onChange={handleChange}
|
|
4836
|
+
onKeyDown={handleKeyDown}
|
|
4837
|
+
{...props}
|
|
4838
|
+
/>
|
|
4839
|
+
</div>
|
|
4840
|
+
)
|
|
4841
|
+
}
|
|
4842
|
+
)
|
|
4843
|
+
ChatInputField.displayName = 'ChatInput.Field'
|
|
4844
|
+
|
|
4845
|
+
// ============================================================================
|
|
4846
|
+
// ChatInput.Submit \u2014 Send / stop button
|
|
4847
|
+
// ============================================================================
|
|
4848
|
+
|
|
4849
|
+
const ChatInputSubmit = React.forwardRef<HTMLButtonElement, ChatInputSubmitProps>(
|
|
4850
|
+
(
|
|
4851
|
+
{
|
|
4852
|
+
className,
|
|
4853
|
+
loading = false,
|
|
4854
|
+
onStop,
|
|
4855
|
+
children,
|
|
4856
|
+
disabled,
|
|
4857
|
+
onClick,
|
|
4858
|
+
buttonRadius,
|
|
4859
|
+
...props
|
|
4860
|
+
},
|
|
4861
|
+
ref
|
|
4862
|
+
) => {
|
|
4863
|
+
const ctx = React.useContext(ChatInputContext)
|
|
4864
|
+
|
|
4865
|
+
// Disabled: context disabled | user prop | empty field (only when not loading)
|
|
4866
|
+
const isDisabled = (disabled ?? ctx.disabled) || (!loading && ctx.isEmpty)
|
|
4867
|
+
|
|
4868
|
+
const handleClick = React.useCallback(
|
|
4869
|
+
(e: React.MouseEvent<HTMLButtonElement>) => {
|
|
4870
|
+
if (loading) {
|
|
4871
|
+
onStop?.()
|
|
4872
|
+
} else if (!isDisabled) {
|
|
4873
|
+
ctx._handleSubmit()
|
|
4874
|
+
}
|
|
4875
|
+
onClick?.(e)
|
|
4876
|
+
},
|
|
4877
|
+
[ctx, isDisabled, loading, onStop, onClick]
|
|
4878
|
+
)
|
|
4879
|
+
|
|
4880
|
+
return (
|
|
4881
|
+
<div className={cn('flex items-center justify-end gap-2 shrink-0 cursor-default', ctx.layout === 'inline' ? submitWrapperInlineMap[ctx.size] : submitWrapperMap[ctx.size])}>
|
|
4882
|
+
{ctx.showCount && ctx.maxLength != null && (
|
|
4883
|
+
<span className={cn('text-text-subtle select-none', countStyleMap[ctx.size])}>
|
|
4884
|
+
{ctx.charCount} / {ctx.maxLength}
|
|
4885
|
+
</span>
|
|
4886
|
+
)}
|
|
4887
|
+
<button
|
|
4888
|
+
ref={ref}
|
|
4889
|
+
type="button"
|
|
4890
|
+
disabled={isDisabled && !loading}
|
|
4891
|
+
aria-label={loading ? 'Stop generating' : 'Send message'}
|
|
4892
|
+
className={cn(
|
|
4893
|
+
'inline-flex items-center justify-center shrink-0',
|
|
4894
|
+
buttonRadius ? buttonRadiusClassMap[buttonRadius] : submitRadiusMap[ctx.radius],
|
|
4895
|
+
'transition-all duration-fast',
|
|
4896
|
+
'focus-visible:outline-none focus-visible:focus-ring',
|
|
4897
|
+
submitSizeMap[ctx.size],
|
|
4898
|
+
// Loading state \u2014 enabled, pulsing
|
|
4899
|
+
loading && ctx.color === 'default' && 'bg-foreground text-background animate-pulse cursor-pointer',
|
|
4900
|
+
loading && ctx.color === 'primary' && 'bg-primary text-primary-foreground animate-pulse cursor-pointer',
|
|
4901
|
+
// Enabled state
|
|
4902
|
+
!loading && !isDisabled && ctx.color === 'default' && [
|
|
4903
|
+
'bg-foreground text-background',
|
|
4904
|
+
'hover:bg-foreground/90 active:bg-foreground/80 active:scale-pressed cursor-pointer',
|
|
4905
|
+
],
|
|
4906
|
+
!loading && !isDisabled && ctx.color === 'primary' && [
|
|
4907
|
+
'bg-primary text-primary-foreground',
|
|
4908
|
+
'hover:bg-primary-hover active:scale-pressed cursor-pointer',
|
|
4909
|
+
],
|
|
4910
|
+
// Disabled state
|
|
4911
|
+
!loading && isDisabled && 'bg-background-muted text-text-subtle pointer-events-none',
|
|
4912
|
+
className
|
|
4913
|
+
)}
|
|
4914
|
+
onClick={handleClick}
|
|
4915
|
+
{...props}
|
|
4916
|
+
>
|
|
4917
|
+
<span
|
|
4918
|
+
aria-hidden="true"
|
|
4919
|
+
className={cn('flex items-center justify-center', submitIconSizeMap[ctx.size])}
|
|
4920
|
+
>
|
|
4921
|
+
{children ?? (loading ? <StopIcon /> : <SendIcon />)}
|
|
4922
|
+
</span>
|
|
4923
|
+
</button>
|
|
4924
|
+
</div>
|
|
4925
|
+
)
|
|
4926
|
+
}
|
|
4927
|
+
)
|
|
4928
|
+
ChatInputSubmit.displayName = 'ChatInput.Submit'
|
|
4929
|
+
|
|
4930
|
+
// ============================================================================
|
|
4931
|
+
// Built-in SVG icons \u2014 no external icon dependency
|
|
4932
|
+
// ============================================================================
|
|
4933
|
+
|
|
4934
|
+
function SendIcon() {
|
|
4935
|
+
return (
|
|
4936
|
+
<svg
|
|
4937
|
+
viewBox="0 0 24 24"
|
|
4938
|
+
fill="none"
|
|
4939
|
+
stroke="currentColor"
|
|
4940
|
+
strokeWidth={2.5}
|
|
4941
|
+
strokeLinecap="round"
|
|
4942
|
+
strokeLinejoin="round"
|
|
4943
|
+
className="w-full h-full"
|
|
4944
|
+
>
|
|
4945
|
+
<path d="M22 2 11 13" />
|
|
4946
|
+
<path d="M22 2 15 22 11 13 2 9l20-7Z" />
|
|
4947
|
+
</svg>
|
|
4948
|
+
)
|
|
4949
|
+
}
|
|
4950
|
+
|
|
4951
|
+
function StopIcon() {
|
|
4952
|
+
return (
|
|
4953
|
+
<svg viewBox="0 0 24 24" fill="currentColor" className="w-full h-full">
|
|
4954
|
+
<rect x="5" y="5" width="14" height="14" rx="2" />
|
|
4955
|
+
</svg>
|
|
4956
|
+
)
|
|
4957
|
+
}
|
|
4958
|
+
|
|
4959
|
+
// ============================================================================
|
|
4960
|
+
// Namespace export (compound pattern)
|
|
4961
|
+
// ============================================================================
|
|
4962
|
+
|
|
4963
|
+
const ChatInput = Object.assign(ChatInputRoot, {
|
|
4964
|
+
Field: ChatInputField,
|
|
4965
|
+
Submit: ChatInputSubmit,
|
|
4966
|
+
})
|
|
4967
|
+
|
|
4968
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
4969
|
+
namespace ChatInput {
|
|
4970
|
+
export type RootProps = ChatInputRootProps
|
|
4971
|
+
export type FieldProps = ChatInputFieldProps
|
|
4972
|
+
export type SubmitProps = ChatInputSubmitProps
|
|
4973
|
+
export type Layout = ChatInputLayout
|
|
4974
|
+
}
|
|
4975
|
+
|
|
4976
|
+
export {
|
|
4977
|
+
ChatInput,
|
|
4978
|
+
ChatInputRoot,
|
|
4979
|
+
ChatInputField,
|
|
4980
|
+
ChatInputSubmit,
|
|
4981
|
+
chatInputVariants,
|
|
4982
|
+
}
|
|
4983
|
+
`,
|
|
4984
|
+
type: "ui"
|
|
4985
|
+
}]
|
|
4986
|
+
},
|
|
4987
|
+
"chat-message": {
|
|
4988
|
+
name: "chat-message",
|
|
4989
|
+
dependencies: [],
|
|
4990
|
+
registryDependencies: ["avatar", "typing-indicator"],
|
|
4991
|
+
reverseDependencies: [],
|
|
4992
|
+
namespace: true,
|
|
4993
|
+
description: "",
|
|
4994
|
+
files: [{
|
|
4995
|
+
path: "chat-message.tsx",
|
|
4996
|
+
content: `'use client'
|
|
4997
|
+
|
|
4998
|
+
import * as React from 'react'
|
|
4999
|
+
import { cn } from '@/lib/utils'
|
|
5000
|
+
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
|
5001
|
+
import { TypingIndicator, type TypingIndicatorProps } from '@/components/ui/typing-indicator'
|
|
5002
|
+
|
|
5003
|
+
// ============================================================================
|
|
5004
|
+
// Types
|
|
5005
|
+
// ============================================================================
|
|
5006
|
+
|
|
5007
|
+
export type ChatMessageRole = 'user' | 'assistant'
|
|
5008
|
+
export type ChatMessageVariant = 'bubble' | 'flat'
|
|
5009
|
+
export type ChatMessageColor = 'default' | 'muted' | 'primary' | 'dark'
|
|
5010
|
+
export type ChatMessageSize = 'sm' | 'default' | 'lg'
|
|
5011
|
+
export type ChatMessageRadius = 'md' | 'lg' | 'xl' | '2xl'
|
|
5012
|
+
export type ChatMessageAvatarSize = 'sm' | 'md' | 'lg'
|
|
5013
|
+
|
|
5014
|
+
/** Built-in status presets (pass as string to Footer.status for auto-rendering) */
|
|
5015
|
+
export type ChatMessageStatus = 'sending' | 'sent' | 'read' | 'error'
|
|
5016
|
+
|
|
5017
|
+
// ============================================================================
|
|
5018
|
+
// Context \u2014 propagates root props to sub-components
|
|
5019
|
+
// ============================================================================
|
|
5020
|
+
|
|
5021
|
+
interface ChatMessageContextValue {
|
|
5022
|
+
role: ChatMessageRole
|
|
5023
|
+
variant: ChatMessageVariant
|
|
5024
|
+
color: ChatMessageColor
|
|
5025
|
+
size: ChatMessageSize
|
|
5026
|
+
radius: ChatMessageRadius
|
|
5027
|
+
tail: boolean
|
|
5028
|
+
typing: boolean | Omit<TypingIndicatorProps, 'className'>
|
|
5029
|
+
actions?: React.ReactNode
|
|
5030
|
+
}
|
|
5031
|
+
|
|
5032
|
+
const ChatMessageContext = React.createContext<ChatMessageContextValue>({
|
|
5033
|
+
role: 'assistant',
|
|
5034
|
+
variant: 'bubble',
|
|
5035
|
+
color: 'default',
|
|
5036
|
+
size: 'default',
|
|
5037
|
+
radius: '2xl',
|
|
5038
|
+
tail: true,
|
|
5039
|
+
typing: false,
|
|
5040
|
+
actions: undefined,
|
|
5041
|
+
})
|
|
5042
|
+
|
|
5043
|
+
// ============================================================================
|
|
5044
|
+
// Size maps
|
|
5045
|
+
// ============================================================================
|
|
5046
|
+
|
|
5047
|
+
/** Bubble padding per size */
|
|
5048
|
+
const bubblePaddingMap: Record<ChatMessageSize, string> = {
|
|
5049
|
+
sm: 'px-3 py-1.5',
|
|
5050
|
+
default: 'px-3.5 py-2',
|
|
5051
|
+
lg: 'px-4 py-2.5',
|
|
5052
|
+
}
|
|
5053
|
+
|
|
5054
|
+
/** Bubble text class per size */
|
|
5055
|
+
const bubbleTextMap: Record<ChatMessageSize, string> = {
|
|
5056
|
+
sm: 'text-sm',
|
|
5057
|
+
default: 'text-md',
|
|
5058
|
+
lg: 'text-base',
|
|
5059
|
+
}
|
|
5060
|
+
|
|
5061
|
+
/** Footer text size per size */
|
|
5062
|
+
const footerTextMap: Record<ChatMessageSize, string> = {
|
|
5063
|
+
sm: 'text-2xs',
|
|
5064
|
+
default: 'text-2xs',
|
|
5065
|
+
lg: 'text-xs',
|
|
5066
|
+
}
|
|
5067
|
+
|
|
5068
|
+
/** Gap between avatar and content per message size */
|
|
5069
|
+
const rowGapMap: Record<ChatMessageSize, string> = {
|
|
5070
|
+
sm: 'gap-2',
|
|
5071
|
+
default: 'gap-2.5',
|
|
5072
|
+
lg: 'gap-3',
|
|
5073
|
+
}
|
|
5074
|
+
|
|
5075
|
+
/** Gap override when avatarSize is explicitly set */
|
|
5076
|
+
const avatarGapMap: Record<ChatMessageAvatarSize, string> = {
|
|
5077
|
+
sm: 'gap-1.5',
|
|
5078
|
+
md: 'gap-2.5',
|
|
5079
|
+
lg: 'gap-3',
|
|
5080
|
+
}
|
|
5081
|
+
|
|
5082
|
+
// ============================================================================
|
|
5083
|
+
// Chat avatar sizes (independent from message size)
|
|
5084
|
+
// ============================================================================
|
|
5085
|
+
|
|
5086
|
+
const chatAvatarSizeMap: Record<ChatMessageAvatarSize, string> = {
|
|
5087
|
+
sm: 'w-6 h-6', // 24px
|
|
5088
|
+
md: 'w-7 h-7', // 28px
|
|
5089
|
+
lg: 'w-8 h-8', // 32px
|
|
5090
|
+
}
|
|
5091
|
+
|
|
5092
|
+
const chatAvatarFontMap: Record<ChatMessageAvatarSize, string> = {
|
|
5093
|
+
sm: 'text-2xs',
|
|
5094
|
+
md: 'text-2xs',
|
|
5095
|
+
lg: 'text-xs',
|
|
5096
|
+
}
|
|
5097
|
+
|
|
5098
|
+
// ============================================================================
|
|
5099
|
+
// Bubble radius \u2014 static maps for Tailwind JIT scanner compatibility
|
|
5100
|
+
// ============================================================================
|
|
5101
|
+
|
|
5102
|
+
/**
|
|
5103
|
+
* Asymmetric with tail: tail corner = rounded-none (0px).
|
|
5104
|
+
* assistant \u2192 tail at bottom-left | user \u2192 tail at bottom-right
|
|
5105
|
+
*/
|
|
5106
|
+
const bubbleRadiusTailMap: Record<ChatMessageRadius, Record<ChatMessageRole, string>> = {
|
|
5107
|
+
md: { assistant: 'rounded-tl-md rounded-tr-md rounded-br-md rounded-bl-none', user: 'rounded-tl-md rounded-tr-md rounded-br-none rounded-bl-md' },
|
|
5108
|
+
lg: { assistant: 'rounded-tl-lg rounded-tr-lg rounded-br-lg rounded-bl-none', user: 'rounded-tl-lg rounded-tr-lg rounded-br-none rounded-bl-lg' },
|
|
5109
|
+
xl: { assistant: 'rounded-tl-xl rounded-tr-xl rounded-br-xl rounded-bl-none', user: 'rounded-tl-xl rounded-tr-xl rounded-br-none rounded-bl-xl' },
|
|
5110
|
+
'2xl': { assistant: 'rounded-tl-2xl rounded-tr-2xl rounded-br-2xl rounded-bl-none', user: 'rounded-tl-2xl rounded-tr-2xl rounded-br-none rounded-bl-2xl' },
|
|
5111
|
+
}
|
|
5112
|
+
|
|
5113
|
+
/** Symmetric: all corners same radius */
|
|
5114
|
+
const bubbleRadiusSymMap: Record<ChatMessageRadius, string> = {
|
|
5115
|
+
md: 'rounded-md',
|
|
5116
|
+
lg: 'rounded-lg',
|
|
5117
|
+
xl: 'rounded-xl',
|
|
5118
|
+
'2xl': 'rounded-2xl',
|
|
5119
|
+
}
|
|
5120
|
+
|
|
5121
|
+
// ============================================================================
|
|
5122
|
+
// Bubble color helper \u2014 applies to both roles uniformly
|
|
5123
|
+
// ============================================================================
|
|
5124
|
+
|
|
5125
|
+
function getBubbleColors(color: ChatMessageColor, variant: ChatMessageVariant): string {
|
|
5126
|
+
if (variant === 'flat') {
|
|
5127
|
+
return color === 'muted' ? 'bg-background-muted' : ''
|
|
5128
|
+
}
|
|
5129
|
+
switch (color) {
|
|
5130
|
+
case 'default': return 'bg-background-paper border border-border text-foreground'
|
|
5131
|
+
case 'muted': return 'bg-background-muted text-foreground'
|
|
5132
|
+
case 'primary': return 'bg-primary text-primary-foreground'
|
|
5133
|
+
case 'dark': return 'bg-foreground text-background'
|
|
5134
|
+
}
|
|
5135
|
+
}
|
|
5136
|
+
|
|
5137
|
+
// ============================================================================
|
|
5138
|
+
// Built-in status labels
|
|
5139
|
+
// ============================================================================
|
|
5140
|
+
|
|
5141
|
+
const builtInStatusLabels: Record<string, { label: string; className: string }> = {
|
|
5142
|
+
sending: { label: 'Sending...', className: 'text-text-subtle' },
|
|
5143
|
+
sent: { label: 'Sent', className: 'text-text-subtle' },
|
|
5144
|
+
read: { label: 'Read', className: 'text-text-subtle' },
|
|
5145
|
+
error: { label: 'Failed', className: 'text-error' },
|
|
5146
|
+
}
|
|
5147
|
+
|
|
5148
|
+
// ============================================================================
|
|
5149
|
+
// TypingIndicator size mapping (chat sizes \u2192 indicator sizes)
|
|
5150
|
+
// ============================================================================
|
|
5151
|
+
|
|
5152
|
+
const typingSizeMap: Record<ChatMessageSize, 'sm' | 'default' | 'lg'> = {
|
|
5153
|
+
sm: 'sm',
|
|
5154
|
+
default: 'sm',
|
|
5155
|
+
lg: 'default',
|
|
5156
|
+
}
|
|
5157
|
+
|
|
5158
|
+
/** Minimum bubble width when typing \u2014 accounts for dot size + padding per chat size */
|
|
5159
|
+
const typingMinWidthMap: Record<ChatMessageSize, string> = {
|
|
5160
|
+
sm: 'min-w-12',
|
|
5161
|
+
default: 'min-w-12',
|
|
5162
|
+
lg: 'min-w-16',
|
|
5163
|
+
}
|
|
5164
|
+
|
|
5165
|
+
// ============================================================================
|
|
5166
|
+
// Props interfaces
|
|
5167
|
+
// ============================================================================
|
|
5168
|
+
|
|
5169
|
+
export interface ChatMessageRootProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
5170
|
+
/** Sender role \u2014 controls alignment (left=assistant, right=user) */
|
|
5171
|
+
role?: ChatMessageRole
|
|
5172
|
+
/** Bubble visual style */
|
|
5173
|
+
variant?: ChatMessageVariant
|
|
5174
|
+
/** Bubble background color */
|
|
5175
|
+
color?: ChatMessageColor
|
|
5176
|
+
/** Size scale \u2014 affects text and padding */
|
|
5177
|
+
size?: ChatMessageSize
|
|
5178
|
+
/** Bubble corner radius */
|
|
5179
|
+
radius?: ChatMessageRadius
|
|
5180
|
+
/** Asymmetric tail corner (radius=0 on the avatar side) */
|
|
5181
|
+
tail?: boolean
|
|
5182
|
+
/** Show typing indicator inside Content. Pass true for defaults, or TypingIndicatorProps for custom. */
|
|
5183
|
+
typing?: boolean | Omit<TypingIndicatorProps, 'className'>
|
|
5184
|
+
/** Avatar size \u2014 overrides default gap between avatar and bubble */
|
|
5185
|
+
avatarSize?: ChatMessageAvatarSize
|
|
5186
|
+
/** Action buttons revealed on hover */
|
|
5187
|
+
actions?: React.ReactNode
|
|
5188
|
+
}
|
|
5189
|
+
|
|
5190
|
+
export interface ChatMessageAvatarProps {
|
|
5191
|
+
/** Avatar size: sm(24px), md(28px), lg(32px) */
|
|
5192
|
+
size?: ChatMessageAvatarSize
|
|
5193
|
+
/** Image URL */
|
|
5194
|
+
src?: string
|
|
5195
|
+
/** Alt text for avatar image */
|
|
5196
|
+
alt?: string
|
|
5197
|
+
/** Initials to display when no image (1\u20132 characters) */
|
|
5198
|
+
initials?: string
|
|
5199
|
+
/** Custom icon or ReactNode to render inside the avatar */
|
|
5200
|
+
icon?: React.ReactNode
|
|
5201
|
+
/** Additional CSS classes */
|
|
5202
|
+
className?: string
|
|
5203
|
+
}
|
|
5204
|
+
|
|
5205
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
5206
|
+
export interface ChatMessageContentProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
5207
|
+
|
|
5208
|
+
export interface ChatMessageFooterProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
5209
|
+
/** Pre-formatted timestamp string (e.g. "12:34 PM") */
|
|
5210
|
+
timestamp?: string
|
|
5211
|
+
/** Status: built-in preset ('sending' | 'sent' | 'read' | 'error') or custom ReactNode */
|
|
5212
|
+
status?: React.ReactNode
|
|
5213
|
+
/** Size override (defaults to context value from ChatMessage root) */
|
|
5214
|
+
size?: ChatMessageSize
|
|
5215
|
+
}
|
|
5216
|
+
|
|
5217
|
+
// ============================================================================
|
|
5218
|
+
// ChatMessage Root
|
|
5219
|
+
// ============================================================================
|
|
5220
|
+
|
|
5221
|
+
const ChatMessageRoot = React.forwardRef<HTMLDivElement, ChatMessageRootProps>(
|
|
5222
|
+
(
|
|
5223
|
+
{
|
|
5224
|
+
className,
|
|
5225
|
+
role = 'assistant',
|
|
5226
|
+
variant = 'bubble',
|
|
5227
|
+
color = 'default',
|
|
5228
|
+
size = 'default',
|
|
5229
|
+
radius = '2xl',
|
|
5230
|
+
tail = true,
|
|
5231
|
+
typing = false,
|
|
5232
|
+
avatarSize,
|
|
5233
|
+
actions,
|
|
5234
|
+
children,
|
|
5235
|
+
...props
|
|
5236
|
+
},
|
|
5237
|
+
ref
|
|
5238
|
+
) => {
|
|
5239
|
+
const ctx = React.useMemo<ChatMessageContextValue>(
|
|
5240
|
+
() => ({ role, variant, color, size, radius, tail, typing, actions }),
|
|
5241
|
+
[role, variant, color, size, radius, tail, typing, actions]
|
|
5242
|
+
)
|
|
5243
|
+
|
|
5244
|
+
const gapClass = avatarSize ? avatarGapMap[avatarSize] : rowGapMap[size]
|
|
5245
|
+
|
|
5246
|
+
return (
|
|
5247
|
+
<ChatMessageContext.Provider value={ctx}>
|
|
5248
|
+
<div
|
|
5249
|
+
ref={ref}
|
|
5250
|
+
className={cn(
|
|
5251
|
+
'group/message flex items-end',
|
|
5252
|
+
gapClass,
|
|
5253
|
+
role === 'user' && variant !== 'flat' ? 'flex-row-reverse' : 'flex-row',
|
|
5254
|
+
className
|
|
5255
|
+
)}
|
|
5256
|
+
{...props}
|
|
5257
|
+
>
|
|
5258
|
+
{children}
|
|
5259
|
+
</div>
|
|
5260
|
+
</ChatMessageContext.Provider>
|
|
5261
|
+
)
|
|
5262
|
+
}
|
|
5263
|
+
)
|
|
5264
|
+
ChatMessageRoot.displayName = 'ChatMessage'
|
|
5265
|
+
|
|
5266
|
+
// ============================================================================
|
|
5267
|
+
// Default AI sparkle icon
|
|
5268
|
+
// ============================================================================
|
|
5269
|
+
|
|
5270
|
+
function DefaultAiIcon() {
|
|
5271
|
+
return (
|
|
5272
|
+
<svg viewBox="0 0 24 24" fill="currentColor" className="w-3/5 h-3/5" aria-hidden="true">
|
|
5273
|
+
<path d="M12 3l1.8 5.4 5.4 1.8-5.4 1.8L12 17.4l-1.8-5.4L4.8 10.2l5.4-1.8z" />
|
|
5274
|
+
<path d="M19.5 3.5l.8 2.4 2.4.8-2.4.8L19.5 10l-.8-2.5-2.4-.8 2.4-.8z" opacity=".6" />
|
|
5275
|
+
<path d="M4.5 14.5l.7 2 2 .7-2 .7L4.5 20l-.7-2-2-.7 2-.7z" opacity=".4" />
|
|
5276
|
+
</svg>
|
|
5277
|
+
)
|
|
5278
|
+
}
|
|
5279
|
+
|
|
5280
|
+
// ============================================================================
|
|
5281
|
+
// ChatMessage.Avatar \u2014 wraps Avatar component with chat-specific sizes
|
|
5282
|
+
// ============================================================================
|
|
5283
|
+
|
|
5284
|
+
const ChatMessageAvatar = React.forwardRef<HTMLSpanElement, ChatMessageAvatarProps>(
|
|
5285
|
+
({ className, size: avatarSize = 'md', src, alt = 'Avatar', initials, icon }, ref) => (
|
|
5286
|
+
<Avatar
|
|
5287
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5288
|
+
ref={ref as any}
|
|
5289
|
+
aria-hidden="true"
|
|
5290
|
+
className={cn(chatAvatarSizeMap[avatarSize], 'shrink-0', className)}
|
|
5291
|
+
>
|
|
5292
|
+
{src && <AvatarImage src={src} alt={alt} />}
|
|
5293
|
+
<AvatarFallback className={cn('font-semibold', chatAvatarFontMap[avatarSize])}>
|
|
5294
|
+
{icon ?? (initials || <DefaultAiIcon />)}
|
|
5295
|
+
</AvatarFallback>
|
|
5296
|
+
</Avatar>
|
|
5297
|
+
)
|
|
5298
|
+
)
|
|
5299
|
+
ChatMessageAvatar.displayName = 'ChatMessage.Avatar'
|
|
5300
|
+
|
|
5301
|
+
// ============================================================================
|
|
5302
|
+
// ChatMessage.Content
|
|
5303
|
+
// ============================================================================
|
|
5304
|
+
|
|
5305
|
+
const ChatMessageContent = React.forwardRef<HTMLDivElement, ChatMessageContentProps>(
|
|
5306
|
+
({ className, children, ...props }, ref) => {
|
|
5307
|
+
const { role, variant, color, size, radius, tail, typing, actions } = React.useContext(ChatMessageContext)
|
|
5308
|
+
const isTyping = !!typing
|
|
5309
|
+
const typingProps = typeof typing === 'object' ? typing : {}
|
|
5310
|
+
|
|
5311
|
+
// Dark bubble backgrounds need inverted typing dot colors
|
|
5312
|
+
const isDarkBg = color === 'primary' || color === 'dark'
|
|
5313
|
+
const invertDots = variant === 'bubble' && isDarkBg
|
|
5314
|
+
|
|
5315
|
+
// flat variant never has a tail (asymmetric corner)
|
|
5316
|
+
const effectiveTail = variant === 'flat' ? false : tail
|
|
5317
|
+
const radiusClasses = effectiveTail
|
|
5318
|
+
? bubbleRadiusTailMap[radius][role]
|
|
5319
|
+
: bubbleRadiusSymMap[radius]
|
|
5320
|
+
|
|
5321
|
+
const bubble = (
|
|
5322
|
+
<div
|
|
5323
|
+
ref={ref}
|
|
5324
|
+
className={cn(
|
|
5325
|
+
bubbleTextMap[size],
|
|
5326
|
+
variant === 'bubble' && 'max-w-[75%]',
|
|
5327
|
+
isTyping && [typingMinWidthMap[size], 'text-center'],
|
|
5328
|
+
getBubbleColors(color, variant),
|
|
5329
|
+
(variant === 'bubble' || variant === 'flat') && bubblePaddingMap[size],
|
|
5330
|
+
(variant === 'bubble' || (variant === 'flat' && color === 'muted')) && radiusClasses,
|
|
5331
|
+
className
|
|
5332
|
+
)}
|
|
5333
|
+
{...props}
|
|
5334
|
+
>
|
|
5335
|
+
{isTyping ? (
|
|
5336
|
+
<TypingIndicator
|
|
5337
|
+
size={typingSizeMap[size]}
|
|
5338
|
+
color="muted"
|
|
5339
|
+
{...typingProps}
|
|
5340
|
+
className={cn('align-middle', invertDots && '[&>div>div]:bg-current')}
|
|
5341
|
+
/>
|
|
5342
|
+
) : children}
|
|
5343
|
+
</div>
|
|
5344
|
+
)
|
|
5345
|
+
|
|
5346
|
+
if (!actions) return bubble
|
|
5347
|
+
|
|
5348
|
+
return (
|
|
5349
|
+
<div className={cn('flex items-end gap-1.5', role === 'user' && variant !== 'flat' ? 'flex-row-reverse' : 'flex-row')}>
|
|
5350
|
+
{bubble}
|
|
5351
|
+
<div className={cn(
|
|
5352
|
+
'shrink-0 flex items-center gap-1 pb-0.5',
|
|
5353
|
+
'opacity-0 group-hover/message:opacity-100',
|
|
5354
|
+
'transition-opacity duration-normal',
|
|
5355
|
+
)}>
|
|
5356
|
+
{actions}
|
|
5357
|
+
</div>
|
|
5358
|
+
</div>
|
|
5359
|
+
)
|
|
5360
|
+
}
|
|
5361
|
+
)
|
|
5362
|
+
ChatMessageContent.displayName = 'ChatMessage.Content'
|
|
5363
|
+
|
|
5364
|
+
// ============================================================================
|
|
5365
|
+
// ChatMessage.Footer
|
|
5366
|
+
// ============================================================================
|
|
4349
5367
|
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
5368
|
+
const ChatMessageFooter = React.forwardRef<HTMLDivElement, ChatMessageFooterProps>(
|
|
5369
|
+
(
|
|
5370
|
+
{
|
|
5371
|
+
className,
|
|
5372
|
+
timestamp,
|
|
5373
|
+
status,
|
|
5374
|
+
role: roleProp,
|
|
5375
|
+
size: sizeProp,
|
|
5376
|
+
children,
|
|
5377
|
+
...props
|
|
5378
|
+
},
|
|
5379
|
+
ref
|
|
5380
|
+
) => {
|
|
5381
|
+
const ctx = React.useContext(ChatMessageContext)
|
|
5382
|
+
const role = roleProp ?? ctx.role
|
|
5383
|
+
const size = sizeProp ?? ctx.size
|
|
5384
|
+
const variant = ctx.variant
|
|
5385
|
+
|
|
5386
|
+
// Resolve status: built-in preset string \u2192 label, or pass-through ReactNode
|
|
5387
|
+
const statusContent = React.useMemo(() => {
|
|
5388
|
+
if (status == null) return null
|
|
5389
|
+
if (typeof status === 'string' && status in builtInStatusLabels) {
|
|
5390
|
+
const { label, className: cls } = builtInStatusLabels[status]
|
|
5391
|
+
return <span className={cls}>{label}</span>
|
|
5392
|
+
}
|
|
5393
|
+
return status
|
|
5394
|
+
}, [status])
|
|
4363
5395
|
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
}
|
|
5396
|
+
return (
|
|
5397
|
+
<div
|
|
5398
|
+
ref={ref}
|
|
5399
|
+
className={cn(
|
|
5400
|
+
'flex items-center gap-1 select-none',
|
|
5401
|
+
footerTextMap[size],
|
|
5402
|
+
'text-text-subtle',
|
|
5403
|
+
role === 'user' && variant !== 'flat' ? 'justify-end' : 'justify-start',
|
|
5404
|
+
className
|
|
5405
|
+
)}
|
|
5406
|
+
{...props}
|
|
5407
|
+
>
|
|
5408
|
+
{children}
|
|
5409
|
+
{statusContent}
|
|
5410
|
+
{timestamp && <span>{timestamp}</span>}
|
|
5411
|
+
</div>
|
|
5412
|
+
)
|
|
5413
|
+
}
|
|
5414
|
+
)
|
|
5415
|
+
ChatMessageFooter.displayName = 'ChatMessage.Footer'
|
|
4378
5416
|
|
|
4379
|
-
//
|
|
5417
|
+
// ============================================================================
|
|
5418
|
+
// Namespace export (compound pattern)
|
|
5419
|
+
// ============================================================================
|
|
4380
5420
|
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
Area: ChartArea,
|
|
4386
|
-
Pie: ChartPie,
|
|
4387
|
-
Tooltip: ChartTooltip,
|
|
4388
|
-
TooltipContent: ChartTooltipContent,
|
|
4389
|
-
Legend: ChartLegend,
|
|
4390
|
-
LegendContent: ChartLegendContent,
|
|
4391
|
-
XAxis: ChartXAxis,
|
|
4392
|
-
YAxis: ChartYAxis,
|
|
4393
|
-
Style: ChartStyle,
|
|
5421
|
+
const ChatMessage = Object.assign(ChatMessageRoot, {
|
|
5422
|
+
Avatar: ChatMessageAvatar,
|
|
5423
|
+
Content: ChatMessageContent,
|
|
5424
|
+
Footer: ChatMessageFooter,
|
|
4394
5425
|
})
|
|
4395
5426
|
|
|
5427
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
5428
|
+
namespace ChatMessage {
|
|
5429
|
+
export type RootProps = ChatMessageRootProps
|
|
5430
|
+
export type AvatarProps = ChatMessageAvatarProps
|
|
5431
|
+
export type ContentProps = ChatMessageContentProps
|
|
5432
|
+
export type FooterProps = ChatMessageFooterProps
|
|
5433
|
+
export type Role = ChatMessageRole
|
|
5434
|
+
export type Variant = ChatMessageVariant
|
|
5435
|
+
export type Color = ChatMessageColor
|
|
5436
|
+
export type Size = ChatMessageSize
|
|
5437
|
+
export type Radius = ChatMessageRadius
|
|
5438
|
+
export type Status = ChatMessageStatus
|
|
5439
|
+
export type AvatarSize = ChatMessageAvatarSize
|
|
5440
|
+
}
|
|
5441
|
+
|
|
4396
5442
|
export {
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
ChartPie,
|
|
4403
|
-
ChartTooltip,
|
|
4404
|
-
ChartTooltipContent,
|
|
4405
|
-
ChartLegend,
|
|
4406
|
-
ChartLegendContent,
|
|
4407
|
-
ChartXAxis,
|
|
4408
|
-
ChartYAxis,
|
|
4409
|
-
ChartStyle,
|
|
4410
|
-
useChart,
|
|
5443
|
+
ChatMessage,
|
|
5444
|
+
ChatMessageRoot,
|
|
5445
|
+
ChatMessageAvatar,
|
|
5446
|
+
ChatMessageContent,
|
|
5447
|
+
ChatMessageFooter,
|
|
4411
5448
|
}
|
|
4412
5449
|
`,
|
|
4413
5450
|
type: "ui"
|
|
@@ -8461,96 +9498,350 @@ const Progress = React.forwardRef<
|
|
|
8461
9498
|
const offset = circumference * (1 - percentage / 100)
|
|
8462
9499
|
|
|
8463
9500
|
return (
|
|
8464
|
-
<div
|
|
8465
|
-
className={cn('inline-flex items-center gap-2', className)}
|
|
8466
|
-
role="progressbar"
|
|
8467
|
-
aria-valuenow={clampedValue}
|
|
8468
|
-
aria-valuemin={0}
|
|
8469
|
-
aria-valuemax={max}
|
|
8470
|
-
aria-label={label}
|
|
8471
|
-
>
|
|
8472
|
-
<div className="relative" style={{ width: diameter, height: diameter }}>
|
|
8473
|
-
<svg
|
|
8474
|
-
viewBox={\`0 0 \${CIRCULAR_VIEWBOX} \${CIRCULAR_VIEWBOX}\`}
|
|
8475
|
-
className="transform -rotate-90"
|
|
8476
|
-
width={diameter}
|
|
8477
|
-
height={diameter}
|
|
8478
|
-
>
|
|
8479
|
-
{/* Track */}
|
|
8480
|
-
<circle
|
|
8481
|
-
cx={CIRCULAR_CENTER}
|
|
8482
|
-
cy={CIRCULAR_CENTER}
|
|
8483
|
-
r={r}
|
|
8484
|
-
fill="none"
|
|
8485
|
-
className="stroke-border"
|
|
8486
|
-
strokeWidth={strokeWidth}
|
|
8487
|
-
/>
|
|
8488
|
-
{/* Indicator */}
|
|
8489
|
-
<circle
|
|
8490
|
-
cx={CIRCULAR_CENTER}
|
|
8491
|
-
cy={CIRCULAR_CENTER}
|
|
8492
|
-
r={r}
|
|
8493
|
-
fill="none"
|
|
8494
|
-
className={cn(indicatorClassName || circularColorMap[color], 'transition-[stroke-dashoffset] duration-normal ease-out')}
|
|
8495
|
-
strokeWidth={strokeWidth}
|
|
8496
|
-
strokeDasharray={circumference}
|
|
8497
|
-
strokeDashoffset={offset}
|
|
8498
|
-
strokeLinecap="round"
|
|
8499
|
-
/>
|
|
8500
|
-
</svg>
|
|
8501
|
-
{/* Center value */}
|
|
8502
|
-
{showValue && resolvedSize !== 'sm' && (
|
|
8503
|
-
<div className={cn(
|
|
8504
|
-
'absolute inset-0 flex items-center justify-center font-semibold font-mono tabular-nums text-foreground',
|
|
8505
|
-
CIRCULAR_FONT_SIZES[resolvedSize]
|
|
8506
|
-
)}>
|
|
8507
|
-
{valueLabel}
|
|
8508
|
-
</div>
|
|
8509
|
-
)}
|
|
9501
|
+
<div
|
|
9502
|
+
className={cn('inline-flex items-center gap-2', className)}
|
|
9503
|
+
role="progressbar"
|
|
9504
|
+
aria-valuenow={clampedValue}
|
|
9505
|
+
aria-valuemin={0}
|
|
9506
|
+
aria-valuemax={max}
|
|
9507
|
+
aria-label={label}
|
|
9508
|
+
>
|
|
9509
|
+
<div className="relative" style={{ width: diameter, height: diameter }}>
|
|
9510
|
+
<svg
|
|
9511
|
+
viewBox={\`0 0 \${CIRCULAR_VIEWBOX} \${CIRCULAR_VIEWBOX}\`}
|
|
9512
|
+
className="transform -rotate-90"
|
|
9513
|
+
width={diameter}
|
|
9514
|
+
height={diameter}
|
|
9515
|
+
>
|
|
9516
|
+
{/* Track */}
|
|
9517
|
+
<circle
|
|
9518
|
+
cx={CIRCULAR_CENTER}
|
|
9519
|
+
cy={CIRCULAR_CENTER}
|
|
9520
|
+
r={r}
|
|
9521
|
+
fill="none"
|
|
9522
|
+
className="stroke-border"
|
|
9523
|
+
strokeWidth={strokeWidth}
|
|
9524
|
+
/>
|
|
9525
|
+
{/* Indicator */}
|
|
9526
|
+
<circle
|
|
9527
|
+
cx={CIRCULAR_CENTER}
|
|
9528
|
+
cy={CIRCULAR_CENTER}
|
|
9529
|
+
r={r}
|
|
9530
|
+
fill="none"
|
|
9531
|
+
className={cn(indicatorClassName || circularColorMap[color], 'transition-[stroke-dashoffset] duration-normal ease-out')}
|
|
9532
|
+
strokeWidth={strokeWidth}
|
|
9533
|
+
strokeDasharray={circumference}
|
|
9534
|
+
strokeDashoffset={offset}
|
|
9535
|
+
strokeLinecap="round"
|
|
9536
|
+
/>
|
|
9537
|
+
</svg>
|
|
9538
|
+
{/* Center value */}
|
|
9539
|
+
{showValue && resolvedSize !== 'sm' && (
|
|
9540
|
+
<div className={cn(
|
|
9541
|
+
'absolute inset-0 flex items-center justify-center font-semibold font-mono tabular-nums text-foreground',
|
|
9542
|
+
CIRCULAR_FONT_SIZES[resolvedSize]
|
|
9543
|
+
)}>
|
|
9544
|
+
{valueLabel}
|
|
9545
|
+
</div>
|
|
9546
|
+
)}
|
|
9547
|
+
</div>
|
|
9548
|
+
</div>
|
|
9549
|
+
)
|
|
9550
|
+
}
|
|
9551
|
+
|
|
9552
|
+
// \u2500\u2500 Linear \u2500\u2500
|
|
9553
|
+
const stripedBg = variant === 'striped'
|
|
9554
|
+
? 'bg-[length:1rem_1rem] bg-[linear-gradient(45deg,rgba(255,255,255,0.15)_25%,transparent_25%,transparent_50%,rgba(255,255,255,0.15)_50%,rgba(255,255,255,0.15)_75%,transparent_75%,transparent)]'
|
|
9555
|
+
: ''
|
|
9556
|
+
|
|
9557
|
+
return (
|
|
9558
|
+
<div className={cn('flex items-center gap-3', showValue && 'w-full', className)}>
|
|
9559
|
+
<ProgressPrimitive.Root
|
|
9560
|
+
ref={ref}
|
|
9561
|
+
className={cn(linearTrackVariants({ size }))}
|
|
9562
|
+
value={clampedValue}
|
|
9563
|
+
max={max}
|
|
9564
|
+
aria-label={label}
|
|
9565
|
+
{...props}
|
|
9566
|
+
>
|
|
9567
|
+
<ProgressPrimitive.Indicator
|
|
9568
|
+
className={cn(
|
|
9569
|
+
'h-full transition-[width] duration-normal ease-out rounded-full',
|
|
9570
|
+
indicatorClassName || colorMap[color],
|
|
9571
|
+
stripedBg,
|
|
9572
|
+
animated && variant === 'striped' && 'animate-progress-stripe',
|
|
9573
|
+
)}
|
|
9574
|
+
style={{ width: \`\${percentage}%\` }}
|
|
9575
|
+
/>
|
|
9576
|
+
</ProgressPrimitive.Root>
|
|
9577
|
+
{showValue && (
|
|
9578
|
+
<span className={cn(
|
|
9579
|
+
'shrink-0 font-semibold font-mono tabular-nums text-foreground',
|
|
9580
|
+
valueFontSizes[resolvedSize]
|
|
9581
|
+
)}>
|
|
9582
|
+
{valueLabel}
|
|
9583
|
+
</span>
|
|
9584
|
+
)}
|
|
9585
|
+
</div>
|
|
9586
|
+
)
|
|
9587
|
+
})
|
|
9588
|
+
Progress.displayName = 'Progress'
|
|
9589
|
+
|
|
9590
|
+
export { Progress, linearTrackVariants }
|
|
9591
|
+
`,
|
|
9592
|
+
type: "ui"
|
|
9593
|
+
}]
|
|
9594
|
+
},
|
|
9595
|
+
"quick-reply": {
|
|
9596
|
+
name: "quick-reply",
|
|
9597
|
+
dependencies: ["@radix-ui/react-slot"],
|
|
9598
|
+
registryDependencies: [],
|
|
9599
|
+
reverseDependencies: [],
|
|
9600
|
+
namespace: true,
|
|
9601
|
+
description: "",
|
|
9602
|
+
files: [{
|
|
9603
|
+
path: "quick-reply.tsx",
|
|
9604
|
+
content: `'use client'
|
|
9605
|
+
|
|
9606
|
+
import * as React from 'react'
|
|
9607
|
+
import { Slot } from '@radix-ui/react-slot'
|
|
9608
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
9609
|
+
import { cn } from '@/lib/utils'
|
|
9610
|
+
|
|
9611
|
+
// ============================================================================
|
|
9612
|
+
// Color mapping per variant \xD7 color
|
|
9613
|
+
// ============================================================================
|
|
9614
|
+
|
|
9615
|
+
const colorMap = {
|
|
9616
|
+
default: {
|
|
9617
|
+
outline: 'border border-border text-foreground bg-background hover:bg-background-muted',
|
|
9618
|
+
filled: 'bg-background-muted text-foreground hover:bg-background-elevated',
|
|
9619
|
+
ghost: 'text-foreground hover:bg-background-muted',
|
|
9620
|
+
},
|
|
9621
|
+
primary: {
|
|
9622
|
+
outline: 'border border-primary text-text-primary bg-background hover:bg-primary-tint',
|
|
9623
|
+
filled: 'bg-primary-tint text-text-primary hover:bg-primary/20',
|
|
9624
|
+
ghost: 'text-text-primary hover:bg-primary-tint',
|
|
9625
|
+
},
|
|
9626
|
+
} as const
|
|
9627
|
+
|
|
9628
|
+
// ============================================================================
|
|
9629
|
+
// Context \u2014 propagates variant/color/size/radius from Root to Items
|
|
9630
|
+
// ============================================================================
|
|
9631
|
+
|
|
9632
|
+
type QuickReplyContextValue = {
|
|
9633
|
+
size: QuickReplySize
|
|
9634
|
+
variant: QuickReplyVariant
|
|
9635
|
+
color: QuickReplyColor
|
|
9636
|
+
radius: QuickReplyRadius
|
|
9637
|
+
}
|
|
9638
|
+
|
|
9639
|
+
const QuickReplyContext = React.createContext<QuickReplyContextValue>({
|
|
9640
|
+
size: 'default',
|
|
9641
|
+
variant: 'outline',
|
|
9642
|
+
color: 'default',
|
|
9643
|
+
radius: 'full',
|
|
9644
|
+
})
|
|
9645
|
+
|
|
9646
|
+
// ============================================================================
|
|
9647
|
+
// Variants (CVA)
|
|
9648
|
+
// ============================================================================
|
|
9649
|
+
|
|
9650
|
+
const quickReplyRootVariants = cva(
|
|
9651
|
+
'flex items-center',
|
|
9652
|
+
{
|
|
9653
|
+
variants: {
|
|
9654
|
+
layout: {
|
|
9655
|
+
scroll: 'overflow-x-auto',
|
|
9656
|
+
wrap: 'flex-wrap',
|
|
9657
|
+
},
|
|
9658
|
+
gap: {
|
|
9659
|
+
sm: 'gap-1.5',
|
|
9660
|
+
default: 'gap-2',
|
|
9661
|
+
lg: 'gap-2.5',
|
|
9662
|
+
},
|
|
9663
|
+
},
|
|
9664
|
+
defaultVariants: {
|
|
9665
|
+
layout: 'scroll',
|
|
9666
|
+
gap: 'default',
|
|
9667
|
+
},
|
|
9668
|
+
}
|
|
9669
|
+
)
|
|
9670
|
+
|
|
9671
|
+
const quickReplyItemVariants = cva(
|
|
9672
|
+
[
|
|
9673
|
+
'inline-flex items-center justify-center',
|
|
9674
|
+
'whitespace-nowrap font-semibold shrink-0',
|
|
9675
|
+
'transition-colors duration-fast cursor-pointer',
|
|
9676
|
+
'focus-visible:outline-none focus-visible:focus-ring',
|
|
9677
|
+
'disabled:pointer-events-none disabled:opacity-50',
|
|
9678
|
+
'select-none',
|
|
9679
|
+
].join(' '),
|
|
9680
|
+
{
|
|
9681
|
+
variants: {
|
|
9682
|
+
size: {
|
|
9683
|
+
sm: 'h-7 px-2.5 text-xs gap-1.5', // 28px
|
|
9684
|
+
default: 'h-8 px-3 text-sm gap-1.5', // 32px
|
|
9685
|
+
lg: 'h-9 px-3.5 text-md gap-1.5', // 36px
|
|
9686
|
+
},
|
|
9687
|
+
radius: {
|
|
9688
|
+
md: 'rounded-md', // 6px
|
|
9689
|
+
lg: 'rounded-lg', // 8px
|
|
9690
|
+
full: 'rounded-full', // 9999px
|
|
9691
|
+
},
|
|
9692
|
+
},
|
|
9693
|
+
defaultVariants: {
|
|
9694
|
+
size: 'default',
|
|
9695
|
+
radius: 'full',
|
|
9696
|
+
},
|
|
9697
|
+
}
|
|
9698
|
+
)
|
|
9699
|
+
|
|
9700
|
+
// ============================================================================
|
|
9701
|
+
// Icon sizes per item size
|
|
9702
|
+
// ============================================================================
|
|
9703
|
+
|
|
9704
|
+
const iconSizeMap = {
|
|
9705
|
+
sm: 'icon-xs', // 14px
|
|
9706
|
+
default: 'icon-xs', // 14px
|
|
9707
|
+
lg: 'icon-sm', // 16px
|
|
9708
|
+
} as const
|
|
9709
|
+
|
|
9710
|
+
// ============================================================================
|
|
9711
|
+
// Types
|
|
9712
|
+
// ============================================================================
|
|
9713
|
+
|
|
9714
|
+
export type QuickReplyLayout = 'scroll' | 'wrap'
|
|
9715
|
+
export type QuickReplyVariant = 'outline' | 'filled' | 'ghost'
|
|
9716
|
+
export type QuickReplyColor = 'default' | 'primary'
|
|
9717
|
+
export type QuickReplySize = 'sm' | 'default' | 'lg'
|
|
9718
|
+
export type QuickReplyRadius = 'md' | 'lg' | 'full'
|
|
9719
|
+
export type QuickReplyGap = 'sm' | 'default' | 'lg'
|
|
9720
|
+
|
|
9721
|
+
export interface QuickReplyRootProps
|
|
9722
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
9723
|
+
VariantProps<typeof quickReplyRootVariants> {
|
|
9724
|
+
/** Layout mode: horizontal scroll or wrap to multiple rows */
|
|
9725
|
+
layout?: QuickReplyLayout
|
|
9726
|
+
/** Visual style applied to all child Items */
|
|
9727
|
+
variant?: QuickReplyVariant
|
|
9728
|
+
/** Color theme applied to all child Items */
|
|
9729
|
+
color?: QuickReplyColor
|
|
9730
|
+
/** Chip size applied to all child Items */
|
|
9731
|
+
size?: QuickReplySize
|
|
9732
|
+
/** Border radius applied to all child Items */
|
|
9733
|
+
radius?: QuickReplyRadius
|
|
9734
|
+
/** Gap between Items */
|
|
9735
|
+
gap?: QuickReplyGap
|
|
9736
|
+
}
|
|
9737
|
+
|
|
9738
|
+
export interface QuickReplyItemProps
|
|
9739
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
9740
|
+
/** Leading icon (SVG or ReactNode) */
|
|
9741
|
+
icon?: React.ReactNode
|
|
9742
|
+
/** Render as child element (Slot pattern) */
|
|
9743
|
+
asChild?: boolean
|
|
9744
|
+
}
|
|
9745
|
+
|
|
9746
|
+
// ============================================================================
|
|
9747
|
+
// QuickReply Root \u2014 group container that propagates style via Context
|
|
9748
|
+
// ============================================================================
|
|
9749
|
+
|
|
9750
|
+
const QuickReplyRoot = React.forwardRef<HTMLDivElement, QuickReplyRootProps>(
|
|
9751
|
+
(
|
|
9752
|
+
{
|
|
9753
|
+
className,
|
|
9754
|
+
layout = 'scroll',
|
|
9755
|
+
gap = 'default',
|
|
9756
|
+
variant = 'outline',
|
|
9757
|
+
color = 'default',
|
|
9758
|
+
size = 'default',
|
|
9759
|
+
radius = 'full',
|
|
9760
|
+
role = 'group',
|
|
9761
|
+
children,
|
|
9762
|
+
...props
|
|
9763
|
+
},
|
|
9764
|
+
ref
|
|
9765
|
+
) => {
|
|
9766
|
+
const contextValue = React.useMemo<QuickReplyContextValue>(
|
|
9767
|
+
() => ({ size, variant, color, radius }),
|
|
9768
|
+
[size, variant, color, radius]
|
|
9769
|
+
)
|
|
9770
|
+
|
|
9771
|
+
return (
|
|
9772
|
+
<QuickReplyContext.Provider value={contextValue}>
|
|
9773
|
+
<div
|
|
9774
|
+
ref={ref}
|
|
9775
|
+
role={role}
|
|
9776
|
+
className={cn(quickReplyRootVariants({ layout, gap }), className)}
|
|
9777
|
+
{...props}
|
|
9778
|
+
>
|
|
9779
|
+
{children}
|
|
8510
9780
|
</div>
|
|
8511
|
-
</
|
|
9781
|
+
</QuickReplyContext.Provider>
|
|
8512
9782
|
)
|
|
8513
9783
|
}
|
|
9784
|
+
)
|
|
9785
|
+
QuickReplyRoot.displayName = 'QuickReply'
|
|
8514
9786
|
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
: ''
|
|
9787
|
+
// ============================================================================
|
|
9788
|
+
// QuickReply Item \u2014 individual chip button
|
|
9789
|
+
// ============================================================================
|
|
8519
9790
|
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
9791
|
+
const QuickReplyItem = React.forwardRef<HTMLButtonElement, QuickReplyItemProps>(
|
|
9792
|
+
({ className, icon, asChild = false, children, ...props }, ref) => {
|
|
9793
|
+
const { size, variant, color, radius } = React.useContext(QuickReplyContext)
|
|
9794
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9795
|
+
const Comp = asChild ? (Slot as any) : 'button'
|
|
9796
|
+
const colorClasses = colorMap[color][variant]
|
|
9797
|
+
|
|
9798
|
+
return (
|
|
9799
|
+
<Comp
|
|
8523
9800
|
ref={ref}
|
|
8524
|
-
|
|
8525
|
-
value={clampedValue}
|
|
8526
|
-
max={max}
|
|
8527
|
-
aria-label={label}
|
|
9801
|
+
type={!asChild ? 'button' : undefined}
|
|
8528
9802
|
{...props}
|
|
9803
|
+
className={cn(
|
|
9804
|
+
quickReplyItemVariants({ size, radius }),
|
|
9805
|
+
colorClasses,
|
|
9806
|
+
className
|
|
9807
|
+
)}
|
|
8529
9808
|
>
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
'
|
|
8533
|
-
|
|
8534
|
-
|
|
8535
|
-
|
|
8536
|
-
|
|
8537
|
-
|
|
8538
|
-
|
|
8539
|
-
</
|
|
8540
|
-
|
|
8541
|
-
|
|
8542
|
-
|
|
8543
|
-
|
|
8544
|
-
|
|
8545
|
-
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
9809
|
+
{icon && (
|
|
9810
|
+
<span
|
|
9811
|
+
className={cn('shrink-0 [&>svg]:w-full [&>svg]:h-full', iconSizeMap[size])}
|
|
9812
|
+
aria-hidden="true"
|
|
9813
|
+
>
|
|
9814
|
+
{icon}
|
|
9815
|
+
</span>
|
|
9816
|
+
)}
|
|
9817
|
+
{children}
|
|
9818
|
+
</Comp>
|
|
9819
|
+
)
|
|
9820
|
+
}
|
|
9821
|
+
)
|
|
9822
|
+
QuickReplyItem.displayName = 'QuickReply.Item'
|
|
9823
|
+
|
|
9824
|
+
// ============================================================================
|
|
9825
|
+
// Namespace export (compound pattern)
|
|
9826
|
+
// ============================================================================
|
|
9827
|
+
|
|
9828
|
+
const QuickReply = Object.assign(QuickReplyRoot, {
|
|
9829
|
+
Item: QuickReplyItem,
|
|
8550
9830
|
})
|
|
8551
|
-
Progress.displayName = 'Progress'
|
|
8552
9831
|
|
|
8553
|
-
|
|
9832
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
9833
|
+
namespace QuickReply {
|
|
9834
|
+
export type RootProps = QuickReplyRootProps
|
|
9835
|
+
export type ItemProps = QuickReplyItemProps
|
|
9836
|
+
}
|
|
9837
|
+
|
|
9838
|
+
export {
|
|
9839
|
+
QuickReply,
|
|
9840
|
+
QuickReplyRoot,
|
|
9841
|
+
QuickReplyItem,
|
|
9842
|
+
quickReplyRootVariants,
|
|
9843
|
+
quickReplyItemVariants,
|
|
9844
|
+
}
|
|
8554
9845
|
`,
|
|
8555
9846
|
type: "ui"
|
|
8556
9847
|
}]
|
|
@@ -12585,6 +13876,208 @@ export {
|
|
|
12585
13876
|
TooltipProvider,
|
|
12586
13877
|
tooltipContentVariants,
|
|
12587
13878
|
}
|
|
13879
|
+
`,
|
|
13880
|
+
type: "ui"
|
|
13881
|
+
}]
|
|
13882
|
+
},
|
|
13883
|
+
"typing-indicator": {
|
|
13884
|
+
name: "typing-indicator",
|
|
13885
|
+
dependencies: [],
|
|
13886
|
+
registryDependencies: [],
|
|
13887
|
+
reverseDependencies: [],
|
|
13888
|
+
namespace: false,
|
|
13889
|
+
description: "",
|
|
13890
|
+
files: [{
|
|
13891
|
+
path: "typing-indicator.tsx",
|
|
13892
|
+
content: `'use client'
|
|
13893
|
+
|
|
13894
|
+
import * as React from 'react'
|
|
13895
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
13896
|
+
import { cn } from '@/lib/utils'
|
|
13897
|
+
|
|
13898
|
+
// ============================================================================
|
|
13899
|
+
// Constants
|
|
13900
|
+
// ============================================================================
|
|
13901
|
+
|
|
13902
|
+
/** Dot diameter per size (px) */
|
|
13903
|
+
const DOT_SIZES = { sm: 4, default: 6, lg: 8 } as const
|
|
13904
|
+
|
|
13905
|
+
/** Gap between dots per size (px) */
|
|
13906
|
+
const DOT_GAP = { sm: 3, default: 4, lg: 5 } as const
|
|
13907
|
+
|
|
13908
|
+
/** Animation speed in ms */
|
|
13909
|
+
const SPEED_MS = { slow: 1600, default: 1100, fast: 700 } as const
|
|
13910
|
+
|
|
13911
|
+
/** Number of dots */
|
|
13912
|
+
const DOT_COUNT = 3
|
|
13913
|
+
|
|
13914
|
+
// ============================================================================
|
|
13915
|
+
// Color mapping
|
|
13916
|
+
// ============================================================================
|
|
13917
|
+
|
|
13918
|
+
const bgColorMap = {
|
|
13919
|
+
default: 'bg-foreground',
|
|
13920
|
+
primary: 'bg-primary',
|
|
13921
|
+
muted: 'bg-text-subtle',
|
|
13922
|
+
} as const
|
|
13923
|
+
|
|
13924
|
+
// ============================================================================
|
|
13925
|
+
// Variants (CVA)
|
|
13926
|
+
// ============================================================================
|
|
13927
|
+
|
|
13928
|
+
const typingIndicatorVariants = cva(
|
|
13929
|
+
'inline-flex items-center',
|
|
13930
|
+
{
|
|
13931
|
+
variants: {
|
|
13932
|
+
size: {
|
|
13933
|
+
sm: 'gap-1.5 text-xs',
|
|
13934
|
+
default: 'gap-2 text-sm',
|
|
13935
|
+
lg: 'gap-2.5 text-base',
|
|
13936
|
+
},
|
|
13937
|
+
},
|
|
13938
|
+
defaultVariants: {
|
|
13939
|
+
size: 'default',
|
|
13940
|
+
},
|
|
13941
|
+
}
|
|
13942
|
+
)
|
|
13943
|
+
|
|
13944
|
+
// ============================================================================
|
|
13945
|
+
// Types
|
|
13946
|
+
// ============================================================================
|
|
13947
|
+
|
|
13948
|
+
export interface TypingIndicatorProps
|
|
13949
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
13950
|
+
VariantProps<typeof typingIndicatorVariants> {
|
|
13951
|
+
/** Animation style */
|
|
13952
|
+
variant?: 'dots' | 'cursor'
|
|
13953
|
+
/** Indicator color */
|
|
13954
|
+
color?: 'default' | 'primary' | 'muted'
|
|
13955
|
+
/** Animation speed */
|
|
13956
|
+
speed?: 'slow' | 'default' | 'fast'
|
|
13957
|
+
/** Accessible label (also shown as visual text when showLabel is true) */
|
|
13958
|
+
label?: string
|
|
13959
|
+
/** Render label text alongside the indicator */
|
|
13960
|
+
showLabel?: boolean
|
|
13961
|
+
}
|
|
13962
|
+
|
|
13963
|
+
// ============================================================================
|
|
13964
|
+
// Component
|
|
13965
|
+
// ============================================================================
|
|
13966
|
+
|
|
13967
|
+
const TypingIndicator = React.forwardRef<HTMLDivElement, TypingIndicatorProps>(
|
|
13968
|
+
(
|
|
13969
|
+
{
|
|
13970
|
+
className,
|
|
13971
|
+
variant = 'dots',
|
|
13972
|
+
size,
|
|
13973
|
+
color = 'muted',
|
|
13974
|
+
speed = 'default',
|
|
13975
|
+
label = 'Typing',
|
|
13976
|
+
showLabel = false,
|
|
13977
|
+
...props
|
|
13978
|
+
},
|
|
13979
|
+
ref
|
|
13980
|
+
) => {
|
|
13981
|
+
const resolvedSize = size ?? 'default'
|
|
13982
|
+
|
|
13983
|
+
return (
|
|
13984
|
+
<div
|
|
13985
|
+
ref={ref}
|
|
13986
|
+
role="status"
|
|
13987
|
+
aria-live="polite"
|
|
13988
|
+
aria-label={label}
|
|
13989
|
+
className={cn(typingIndicatorVariants({ size }), className)}
|
|
13990
|
+
{...props}
|
|
13991
|
+
>
|
|
13992
|
+
{variant === 'dots' && (
|
|
13993
|
+
<DotsIndicator size={resolvedSize} color={color} speed={speed} />
|
|
13994
|
+
)}
|
|
13995
|
+
{variant === 'cursor' && (
|
|
13996
|
+
<CursorIndicator size={resolvedSize} color={color} />
|
|
13997
|
+
)}
|
|
13998
|
+
{showLabel && (
|
|
13999
|
+
<span className="text-text-muted select-none">{label}</span>
|
|
14000
|
+
)}
|
|
14001
|
+
</div>
|
|
14002
|
+
)
|
|
14003
|
+
}
|
|
14004
|
+
)
|
|
14005
|
+
TypingIndicator.displayName = 'TypingIndicator'
|
|
14006
|
+
|
|
14007
|
+
// ============================================================================
|
|
14008
|
+
// Dots Indicator \u2014 3 staggered bouncing dots (reuses animate-spinner-dot)
|
|
14009
|
+
// ============================================================================
|
|
14010
|
+
|
|
14011
|
+
function DotsIndicator({
|
|
14012
|
+
size,
|
|
14013
|
+
color,
|
|
14014
|
+
speed,
|
|
14015
|
+
}: {
|
|
14016
|
+
size: 'sm' | 'default' | 'lg'
|
|
14017
|
+
color: 'default' | 'primary' | 'muted'
|
|
14018
|
+
speed: 'slow' | 'default' | 'fast'
|
|
14019
|
+
}) {
|
|
14020
|
+
const dotSize = DOT_SIZES[size]
|
|
14021
|
+
const gap = DOT_GAP[size]
|
|
14022
|
+
const duration = SPEED_MS[speed]
|
|
14023
|
+
|
|
14024
|
+
return (
|
|
14025
|
+
<div className="inline-flex items-center" style={{ gap }}>
|
|
14026
|
+
{Array.from({ length: DOT_COUNT }, (_, i) => (
|
|
14027
|
+
<div
|
|
14028
|
+
key={i}
|
|
14029
|
+
className={cn(
|
|
14030
|
+
'rounded-full animate-spinner-dot motion-reduce:animate-none motion-reduce:opacity-60',
|
|
14031
|
+
bgColorMap[color]
|
|
14032
|
+
)}
|
|
14033
|
+
style={{
|
|
14034
|
+
width: dotSize,
|
|
14035
|
+
height: dotSize,
|
|
14036
|
+
animationDuration: \`\${duration}ms\`,
|
|
14037
|
+
animationDelay: \`\${i * (duration / DOT_COUNT / 1.5)}ms\`,
|
|
14038
|
+
}}
|
|
14039
|
+
/>
|
|
14040
|
+
))}
|
|
14041
|
+
</div>
|
|
14042
|
+
)
|
|
14043
|
+
}
|
|
14044
|
+
|
|
14045
|
+
// ============================================================================
|
|
14046
|
+
// Cursor sizes per component size
|
|
14047
|
+
// ============================================================================
|
|
14048
|
+
|
|
14049
|
+
const CURSOR_SIZE = {
|
|
14050
|
+
sm: { width: 2, height: 14 },
|
|
14051
|
+
default: { width: 2.5, height: 20 },
|
|
14052
|
+
lg: { width: 3, height: 26 },
|
|
14053
|
+
} as const
|
|
14054
|
+
|
|
14055
|
+
// ============================================================================
|
|
14056
|
+
// Cursor Indicator \u2014 blinking cursor (text-cursor style)
|
|
14057
|
+
// ============================================================================
|
|
14058
|
+
|
|
14059
|
+
function CursorIndicator({
|
|
14060
|
+
size,
|
|
14061
|
+
color,
|
|
14062
|
+
}: {
|
|
14063
|
+
size: 'sm' | 'default' | 'lg'
|
|
14064
|
+
color: 'default' | 'primary' | 'muted'
|
|
14065
|
+
}) {
|
|
14066
|
+
const { width, height } = CURSOR_SIZE[size]
|
|
14067
|
+
|
|
14068
|
+
return (
|
|
14069
|
+
<div
|
|
14070
|
+
aria-hidden="true"
|
|
14071
|
+
className={cn(
|
|
14072
|
+
'rounded-full animate-typing-cursor motion-reduce:animate-none motion-reduce:opacity-60',
|
|
14073
|
+
bgColorMap[color]
|
|
14074
|
+
)}
|
|
14075
|
+
style={{ width, height, animationDuration: '1200ms' }}
|
|
14076
|
+
/>
|
|
14077
|
+
)
|
|
14078
|
+
}
|
|
14079
|
+
|
|
14080
|
+
export { TypingIndicator, typingIndicatorVariants }
|
|
12588
14081
|
`,
|
|
12589
14082
|
type: "ui"
|
|
12590
14083
|
}]
|
|
@@ -12611,6 +14104,8 @@ var INSTALLABLE_COMPONENTS = [
|
|
|
12611
14104
|
"button",
|
|
12612
14105
|
"card",
|
|
12613
14106
|
"chart",
|
|
14107
|
+
"chat-input",
|
|
14108
|
+
"chat-message",
|
|
12614
14109
|
"checkbox",
|
|
12615
14110
|
"divider",
|
|
12616
14111
|
"drawer",
|
|
@@ -12623,6 +14118,7 @@ var INSTALLABLE_COMPONENTS = [
|
|
|
12623
14118
|
"pagination",
|
|
12624
14119
|
"popover",
|
|
12625
14120
|
"progress",
|
|
14121
|
+
"quick-reply",
|
|
12626
14122
|
"radio-group",
|
|
12627
14123
|
"segmented",
|
|
12628
14124
|
"select",
|
|
@@ -12636,7 +14132,8 @@ var INSTALLABLE_COMPONENTS = [
|
|
|
12636
14132
|
"toast",
|
|
12637
14133
|
"toggle-group",
|
|
12638
14134
|
"toggle",
|
|
12639
|
-
"tooltip"
|
|
14135
|
+
"tooltip",
|
|
14136
|
+
"typing-indicator"
|
|
12640
14137
|
];
|
|
12641
14138
|
|
|
12642
14139
|
// cli/src/commands/add.ts
|
|
@@ -12871,7 +14368,7 @@ function showSetupHints(names) {
|
|
|
12871
14368
|
|
|
12872
14369
|
// cli/src/index.ts
|
|
12873
14370
|
var import_picocolors4 = __toESM(require_picocolors());
|
|
12874
|
-
var VERSION = "0.1.
|
|
14371
|
+
var VERSION = "0.1.4";
|
|
12875
14372
|
var HELP = `
|
|
12876
14373
|
${import_picocolors4.default.bold("7onic")} \u2014 Add 7onic design system components to your project
|
|
12877
14374
|
|