7onic 0.1.2 → 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 +1665 -151
- package/llms.txt +5 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1251,7 +1251,13 @@ var CSS_V4_IMPORTS = `@import '@7onic-ui/tokens/css/variables.css';
|
|
|
1251
1251
|
`;
|
|
1252
1252
|
async function init(args) {
|
|
1253
1253
|
const flagYes = args.includes("--yes") || args.includes("-y");
|
|
1254
|
+
const tailwindFlagIdx = args.indexOf("--tailwind");
|
|
1255
|
+
const tailwindFlagRaw = tailwindFlagIdx !== -1 ? args[tailwindFlagIdx + 1] : null;
|
|
1256
|
+
const forcedTailwindVersion = tailwindFlagRaw === "v3" || tailwindFlagRaw === "3" ? 3 : tailwindFlagRaw === "v4" || tailwindFlagRaw === "4" ? 4 : null;
|
|
1254
1257
|
mt(import_picocolors2.default.bold("7onic init"));
|
|
1258
|
+
if (tailwindFlagIdx !== -1 && forcedTailwindVersion === null) {
|
|
1259
|
+
O2.warn(`Invalid --tailwind value: "${tailwindFlagRaw ?? "(empty)"}". Use v3 or v4.`);
|
|
1260
|
+
}
|
|
1255
1261
|
const cwd = process.cwd();
|
|
1256
1262
|
const projectRoot = findProjectRoot(cwd);
|
|
1257
1263
|
if (!projectRoot) {
|
|
@@ -1298,13 +1304,14 @@ async function init(args) {
|
|
|
1298
1304
|
const cssDefault = CSS_CANDIDATES.find((c2) => import_node_fs4.default.existsSync(import_node_path4.default.join(projectRoot, c2))) || "src/app/globals.css";
|
|
1299
1305
|
let config;
|
|
1300
1306
|
if (flagYes) {
|
|
1307
|
+
const resolvedVersion = forcedTailwindVersion ?? tw.version;
|
|
1301
1308
|
config = {
|
|
1302
1309
|
componentsAlias: "@/components/ui",
|
|
1303
1310
|
utilsAlias: "@/lib/utils",
|
|
1304
|
-
tailwindVersion:
|
|
1311
|
+
tailwindVersion: resolvedVersion,
|
|
1305
1312
|
cssPath: cssDefault
|
|
1306
1313
|
};
|
|
1307
|
-
O2.info(`Using defaults: Tailwind v${
|
|
1314
|
+
O2.info(`Using defaults: Tailwind v${resolvedVersion}, CSS: ${cssDefault}`);
|
|
1308
1315
|
} else {
|
|
1309
1316
|
config = await dt(
|
|
1310
1317
|
{
|
|
@@ -1324,7 +1331,7 @@ async function init(args) {
|
|
|
1324
1331
|
{ value: 3, label: "Tailwind v3" },
|
|
1325
1332
|
{ value: 4, label: "Tailwind v4" }
|
|
1326
1333
|
],
|
|
1327
|
-
initialValue: tw.version
|
|
1334
|
+
initialValue: forcedTailwindVersion ?? tw.version
|
|
1328
1335
|
}),
|
|
1329
1336
|
cssPath: () => Ot({
|
|
1330
1337
|
message: "CSS file path",
|
|
@@ -1823,9 +1830,9 @@ const alertVariants = cva(
|
|
|
1823
1830
|
{
|
|
1824
1831
|
variants: {
|
|
1825
1832
|
size: {
|
|
1826
|
-
sm: 'gap-2 p-
|
|
1827
|
-
default: 'gap-2.5 p-
|
|
1828
|
-
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',
|
|
1829
1836
|
},
|
|
1830
1837
|
radius: {
|
|
1831
1838
|
none: 'rounded-none',
|
|
@@ -1853,16 +1860,23 @@ const ICON_SIZE_MAP = {
|
|
|
1853
1860
|
lg: 'icon-md', // 20px
|
|
1854
1861
|
} as const
|
|
1855
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
|
+
|
|
1856
1870
|
const TITLE_SIZE_MAP = {
|
|
1857
|
-
sm: 'text-sm font-semibold
|
|
1858
|
-
default: 'font-semibold
|
|
1859
|
-
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',
|
|
1860
1874
|
} as const
|
|
1861
1875
|
|
|
1862
1876
|
const DESC_SIZE_MAP = {
|
|
1863
1877
|
sm: 'text-xs mt-0.5',
|
|
1864
|
-
default: 'text-sm mt-
|
|
1865
|
-
lg: 'text-md mt-
|
|
1878
|
+
default: 'text-sm mt-0.5',
|
|
1879
|
+
lg: 'text-md mt-0.5',
|
|
1866
1880
|
} as const
|
|
1867
1881
|
|
|
1868
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
|
|
@@ -1947,7 +1961,7 @@ const AlertRoot = React.forwardRef<HTMLDivElement, AlertRootProps>(
|
|
|
1947
1961
|
>
|
|
1948
1962
|
{/* Icon */}
|
|
1949
1963
|
{!hideIcon && (
|
|
1950
|
-
<span className=
|
|
1964
|
+
<span className={cn('shrink-0 flex items-center min-h-[1lh]', ICON_LINE_MAP[size])}>
|
|
1951
1965
|
{icon || <StatusIcon className={ICON_SIZE_MAP[size]} />}
|
|
1952
1966
|
</span>
|
|
1953
1967
|
)}
|
|
@@ -3384,7 +3398,7 @@ const CardTitle = React.forwardRef<HTMLHeadingElement, CardTitleProps>(
|
|
|
3384
3398
|
<h3
|
|
3385
3399
|
ref={ref}
|
|
3386
3400
|
className={cn(
|
|
3387
|
-
'font-semibold text-foreground
|
|
3401
|
+
'text-base font-semibold text-foreground tracking-tight',
|
|
3388
3402
|
icon && 'flex items-center gap-2',
|
|
3389
3403
|
className
|
|
3390
3404
|
)}
|
|
@@ -4340,67 +4354,1097 @@ function ChartPie({
|
|
|
4340
4354
|
const x = cx + radius * Math.cos(-midAngle * RADIAN)
|
|
4341
4355
|
const y = cy + radius * Math.sin(-midAngle * RADIAN)
|
|
4342
4356
|
|
|
4343
|
-
return (
|
|
4344
|
-
<text
|
|
4345
|
-
x={x}
|
|
4346
|
-
y={y}
|
|
4347
|
-
textAnchor="middle"
|
|
4348
|
-
dominantBaseline="central"
|
|
4349
|
-
style={{ fontSize: 'var(--font-size-xs)', fill: 'white', fontWeight: 600 }}
|
|
4350
|
-
>
|
|
4351
|
-
{getDisplayText(entry)}
|
|
4352
|
-
</text>
|
|
4353
|
-
)
|
|
4354
|
-
}
|
|
4355
|
-
: false
|
|
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
|
+
// ============================================================================
|
|
4356
5367
|
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
)
|
|
4370
|
-
|
|
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])
|
|
4371
5395
|
|
|
4372
|
-
|
|
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'
|
|
4373
5416
|
|
|
4374
|
-
//
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
Legend: ChartLegend,
|
|
4383
|
-
LegendContent: ChartLegendContent,
|
|
4384
|
-
XAxis: ChartXAxis,
|
|
4385
|
-
YAxis: ChartYAxis,
|
|
4386
|
-
Style: ChartStyle,
|
|
5417
|
+
// ============================================================================
|
|
5418
|
+
// Namespace export (compound pattern)
|
|
5419
|
+
// ============================================================================
|
|
5420
|
+
|
|
5421
|
+
const ChatMessage = Object.assign(ChatMessageRoot, {
|
|
5422
|
+
Avatar: ChatMessageAvatar,
|
|
5423
|
+
Content: ChatMessageContent,
|
|
5424
|
+
Footer: ChatMessageFooter,
|
|
4387
5425
|
})
|
|
4388
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
|
+
|
|
4389
5442
|
export {
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
ChartPie,
|
|
4396
|
-
ChartTooltip,
|
|
4397
|
-
ChartTooltipContent,
|
|
4398
|
-
ChartLegend,
|
|
4399
|
-
ChartLegendContent,
|
|
4400
|
-
ChartXAxis,
|
|
4401
|
-
ChartYAxis,
|
|
4402
|
-
ChartStyle,
|
|
4403
|
-
useChart,
|
|
5443
|
+
ChatMessage,
|
|
5444
|
+
ChatMessageRoot,
|
|
5445
|
+
ChatMessageAvatar,
|
|
5446
|
+
ChatMessageContent,
|
|
5447
|
+
ChatMessageFooter,
|
|
4404
5448
|
}
|
|
4405
5449
|
`,
|
|
4406
5450
|
type: "ui"
|
|
@@ -8454,96 +9498,350 @@ const Progress = React.forwardRef<
|
|
|
8454
9498
|
const offset = circumference * (1 - percentage / 100)
|
|
8455
9499
|
|
|
8456
9500
|
return (
|
|
8457
|
-
<div
|
|
8458
|
-
className={cn('inline-flex items-center gap-2', className)}
|
|
8459
|
-
role="progressbar"
|
|
8460
|
-
aria-valuenow={clampedValue}
|
|
8461
|
-
aria-valuemin={0}
|
|
8462
|
-
aria-valuemax={max}
|
|
8463
|
-
aria-label={label}
|
|
8464
|
-
>
|
|
8465
|
-
<div className="relative" style={{ width: diameter, height: diameter }}>
|
|
8466
|
-
<svg
|
|
8467
|
-
viewBox={\`0 0 \${CIRCULAR_VIEWBOX} \${CIRCULAR_VIEWBOX}\`}
|
|
8468
|
-
className="transform -rotate-90"
|
|
8469
|
-
width={diameter}
|
|
8470
|
-
height={diameter}
|
|
8471
|
-
>
|
|
8472
|
-
{/* Track */}
|
|
8473
|
-
<circle
|
|
8474
|
-
cx={CIRCULAR_CENTER}
|
|
8475
|
-
cy={CIRCULAR_CENTER}
|
|
8476
|
-
r={r}
|
|
8477
|
-
fill="none"
|
|
8478
|
-
className="stroke-border"
|
|
8479
|
-
strokeWidth={strokeWidth}
|
|
8480
|
-
/>
|
|
8481
|
-
{/* Indicator */}
|
|
8482
|
-
<circle
|
|
8483
|
-
cx={CIRCULAR_CENTER}
|
|
8484
|
-
cy={CIRCULAR_CENTER}
|
|
8485
|
-
r={r}
|
|
8486
|
-
fill="none"
|
|
8487
|
-
className={cn(indicatorClassName || circularColorMap[color], 'transition-[stroke-dashoffset] duration-normal ease-out')}
|
|
8488
|
-
strokeWidth={strokeWidth}
|
|
8489
|
-
strokeDasharray={circumference}
|
|
8490
|
-
strokeDashoffset={offset}
|
|
8491
|
-
strokeLinecap="round"
|
|
8492
|
-
/>
|
|
8493
|
-
</svg>
|
|
8494
|
-
{/* Center value */}
|
|
8495
|
-
{showValue && resolvedSize !== 'sm' && (
|
|
8496
|
-
<div className={cn(
|
|
8497
|
-
'absolute inset-0 flex items-center justify-center font-semibold font-mono tabular-nums text-foreground',
|
|
8498
|
-
CIRCULAR_FONT_SIZES[resolvedSize]
|
|
8499
|
-
)}>
|
|
8500
|
-
{valueLabel}
|
|
8501
|
-
</div>
|
|
8502
|
-
)}
|
|
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}
|
|
8503
9780
|
</div>
|
|
8504
|
-
</
|
|
9781
|
+
</QuickReplyContext.Provider>
|
|
8505
9782
|
)
|
|
8506
9783
|
}
|
|
9784
|
+
)
|
|
9785
|
+
QuickReplyRoot.displayName = 'QuickReply'
|
|
8507
9786
|
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
: ''
|
|
9787
|
+
// ============================================================================
|
|
9788
|
+
// QuickReply Item \u2014 individual chip button
|
|
9789
|
+
// ============================================================================
|
|
8512
9790
|
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
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
|
|
8516
9800
|
ref={ref}
|
|
8517
|
-
|
|
8518
|
-
value={clampedValue}
|
|
8519
|
-
max={max}
|
|
8520
|
-
aria-label={label}
|
|
9801
|
+
type={!asChild ? 'button' : undefined}
|
|
8521
9802
|
{...props}
|
|
9803
|
+
className={cn(
|
|
9804
|
+
quickReplyItemVariants({ size, radius }),
|
|
9805
|
+
colorClasses,
|
|
9806
|
+
className
|
|
9807
|
+
)}
|
|
8522
9808
|
>
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
'
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
</
|
|
8533
|
-
|
|
8534
|
-
|
|
8535
|
-
|
|
8536
|
-
|
|
8537
|
-
|
|
8538
|
-
|
|
8539
|
-
|
|
8540
|
-
|
|
8541
|
-
|
|
8542
|
-
|
|
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,
|
|
8543
9830
|
})
|
|
8544
|
-
Progress.displayName = 'Progress'
|
|
8545
9831
|
|
|
8546
|
-
|
|
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
|
+
}
|
|
8547
9845
|
`,
|
|
8548
9846
|
type: "ui"
|
|
8549
9847
|
}]
|
|
@@ -12578,6 +13876,208 @@ export {
|
|
|
12578
13876
|
TooltipProvider,
|
|
12579
13877
|
tooltipContentVariants,
|
|
12580
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 }
|
|
12581
14081
|
`,
|
|
12582
14082
|
type: "ui"
|
|
12583
14083
|
}]
|
|
@@ -12604,6 +14104,8 @@ var INSTALLABLE_COMPONENTS = [
|
|
|
12604
14104
|
"button",
|
|
12605
14105
|
"card",
|
|
12606
14106
|
"chart",
|
|
14107
|
+
"chat-input",
|
|
14108
|
+
"chat-message",
|
|
12607
14109
|
"checkbox",
|
|
12608
14110
|
"divider",
|
|
12609
14111
|
"drawer",
|
|
@@ -12616,6 +14118,7 @@ var INSTALLABLE_COMPONENTS = [
|
|
|
12616
14118
|
"pagination",
|
|
12617
14119
|
"popover",
|
|
12618
14120
|
"progress",
|
|
14121
|
+
"quick-reply",
|
|
12619
14122
|
"radio-group",
|
|
12620
14123
|
"segmented",
|
|
12621
14124
|
"select",
|
|
@@ -12629,7 +14132,8 @@ var INSTALLABLE_COMPONENTS = [
|
|
|
12629
14132
|
"toast",
|
|
12630
14133
|
"toggle-group",
|
|
12631
14134
|
"toggle",
|
|
12632
|
-
"tooltip"
|
|
14135
|
+
"tooltip",
|
|
14136
|
+
"typing-indicator"
|
|
12633
14137
|
];
|
|
12634
14138
|
|
|
12635
14139
|
// cli/src/commands/add.ts
|
|
@@ -12864,7 +14368,7 @@ function showSetupHints(names) {
|
|
|
12864
14368
|
|
|
12865
14369
|
// cli/src/index.ts
|
|
12866
14370
|
var import_picocolors4 = __toESM(require_picocolors());
|
|
12867
|
-
var VERSION = "0.1.
|
|
14371
|
+
var VERSION = "0.1.4";
|
|
12868
14372
|
var HELP = `
|
|
12869
14373
|
${import_picocolors4.default.bold("7onic")} \u2014 Add 7onic design system components to your project
|
|
12870
14374
|
|
|
@@ -12875,12 +14379,22 @@ ${import_picocolors4.default.bold("Commands:")}
|
|
|
12875
14379
|
init Initialize 7onic in your project
|
|
12876
14380
|
add <component...> Add components to your project
|
|
12877
14381
|
|
|
12878
|
-
${import_picocolors4.default.bold("
|
|
14382
|
+
${import_picocolors4.default.bold("Init options:")}
|
|
14383
|
+
--tailwind v3|v4 Set Tailwind version (default: auto-detect)
|
|
14384
|
+
--yes, -y Skip prompts, use defaults
|
|
14385
|
+
|
|
14386
|
+
${import_picocolors4.default.bold("Add options:")}
|
|
14387
|
+
--all Add all components
|
|
14388
|
+
--overwrite Overwrite existing files
|
|
14389
|
+
--yes, -y Skip prompts
|
|
14390
|
+
|
|
14391
|
+
${import_picocolors4.default.bold("Global options:")}
|
|
12879
14392
|
--version, -v Show version
|
|
12880
14393
|
--help, -h Show this help message
|
|
12881
14394
|
|
|
12882
14395
|
${import_picocolors4.default.bold("Examples:")}
|
|
12883
14396
|
npx 7onic init
|
|
14397
|
+
npx 7onic init --tailwind v3 --yes
|
|
12884
14398
|
npx 7onic add button card input
|
|
12885
14399
|
npx 7onic add --all
|
|
12886
14400
|
`;
|