7onic 0.1.3 → 0.1.5
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 +1697 -150
- package/llms.txt +3 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1198,7 +1198,7 @@ function installDeps(deps, options) {
|
|
|
1198
1198
|
const commands = {
|
|
1199
1199
|
npm: `npm install${devFlag} ${deps.join(" ")}`,
|
|
1200
1200
|
pnpm: `pnpm add${devFlag} ${deps.join(" ")}`,
|
|
1201
|
-
yarn: `yarn add${devFlag} ${deps.join(" ")}`,
|
|
1201
|
+
yarn: `yarn add --ignore-engines${devFlag} ${deps.join(" ")}`,
|
|
1202
1202
|
bun: `bun add${devFlag} ${deps.join(" ")}`
|
|
1203
1203
|
};
|
|
1204
1204
|
(0, import_node_child_process.execSync)(commands[pm], { cwd, stdio: "pipe" });
|
|
@@ -1246,7 +1246,11 @@ var CSS_V3_IMPORTS = `@import '@7onic-ui/tokens/css/variables.css';
|
|
|
1246
1246
|
@import '@7onic-ui/tokens/css/themes/light.css';
|
|
1247
1247
|
@import '@7onic-ui/tokens/css/themes/dark.css';
|
|
1248
1248
|
`;
|
|
1249
|
-
var CSS_V4_IMPORTS = `@import
|
|
1249
|
+
var CSS_V4_IMPORTS = `@import "tailwindcss";
|
|
1250
|
+
@import '@7onic-ui/tokens/css/variables.css';
|
|
1251
|
+
@import '@7onic-ui/tokens/tailwind/v4.css';
|
|
1252
|
+
`;
|
|
1253
|
+
var CSS_V4_TOKEN_ONLY = `@import '@7onic-ui/tokens/css/variables.css';
|
|
1250
1254
|
@import '@7onic-ui/tokens/tailwind/v4.css';
|
|
1251
1255
|
`;
|
|
1252
1256
|
async function init(args) {
|
|
@@ -1281,14 +1285,20 @@ async function init(args) {
|
|
|
1281
1285
|
const tw = detectTailwindVersion(projectRoot);
|
|
1282
1286
|
const pm = detectPackageManager(projectRoot);
|
|
1283
1287
|
let hasPathAlias = false;
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
+
const TSCONFIG_CANDIDATES = ["tsconfig.json", "tsconfig.app.json"];
|
|
1289
|
+
for (const tc of TSCONFIG_CANDIDATES) {
|
|
1290
|
+
try {
|
|
1291
|
+
const raw = import_node_fs4.default.readFileSync(import_node_path4.default.join(projectRoot, tc), "utf-8");
|
|
1292
|
+
if (raw.includes('"@/')) {
|
|
1293
|
+
hasPathAlias = true;
|
|
1294
|
+
break;
|
|
1295
|
+
}
|
|
1296
|
+
} catch {
|
|
1297
|
+
}
|
|
1288
1298
|
}
|
|
1289
1299
|
if (!hasPathAlias) {
|
|
1290
1300
|
O2.warn(
|
|
1291
|
-
|
|
1301
|
+
'No @/ path alias detected in tsconfig.\n Components use @/lib/utils \u2014 make sure your project has path aliases configured.\n Next.js: automatic (no action needed)\n Vite: add to both files:\n\n // tsconfig.app.json \u2192 "compilerOptions.paths"\n { "@/*": ["./src/*"] }\n\n // vite.config.ts \u2192 "resolve.alias"\n { "@": path.resolve(__dirname, "./src") }'
|
|
1292
1302
|
);
|
|
1293
1303
|
}
|
|
1294
1304
|
try {
|
|
@@ -1360,18 +1370,30 @@ async function init(args) {
|
|
|
1360
1370
|
process.exit(1);
|
|
1361
1371
|
}
|
|
1362
1372
|
const cssFullPath = import_node_path4.default.join(projectRoot, config.cssPath);
|
|
1363
|
-
const cssImports = tailwindVersion === 3 ? CSS_V3_IMPORTS : CSS_V4_IMPORTS;
|
|
1364
1373
|
if (!import_node_fs4.default.existsSync(cssFullPath)) {
|
|
1365
1374
|
const cssDir = import_node_path4.default.dirname(cssFullPath);
|
|
1366
1375
|
if (!import_node_fs4.default.existsSync(cssDir)) {
|
|
1367
1376
|
import_node_fs4.default.mkdirSync(cssDir, { recursive: true });
|
|
1368
1377
|
}
|
|
1378
|
+
const cssImports = tailwindVersion === 3 ? CSS_V3_IMPORTS : CSS_V4_IMPORTS;
|
|
1369
1379
|
import_node_fs4.default.writeFileSync(cssFullPath, cssImports, "utf-8");
|
|
1370
1380
|
O2.success(`Created ${config.cssPath} with token imports`);
|
|
1371
1381
|
} else {
|
|
1372
1382
|
const cssContent = import_node_fs4.default.readFileSync(cssFullPath, "utf-8");
|
|
1373
1383
|
if (!cssContent.includes("@7onic-ui/tokens")) {
|
|
1374
|
-
|
|
1384
|
+
const tokenImports = tailwindVersion === 3 ? CSS_V3_IMPORTS : CSS_V4_TOKEN_ONLY;
|
|
1385
|
+
if (tailwindVersion === 4) {
|
|
1386
|
+
const twMatch = cssContent.match(/@import\s+["']tailwindcss["'].*\n/);
|
|
1387
|
+
if (twMatch) {
|
|
1388
|
+
const insertAt = twMatch.index + twMatch[0].length;
|
|
1389
|
+
const updated = cssContent.slice(0, insertAt) + tokenImports + cssContent.slice(insertAt);
|
|
1390
|
+
import_node_fs4.default.writeFileSync(cssFullPath, updated, "utf-8");
|
|
1391
|
+
} else {
|
|
1392
|
+
import_node_fs4.default.writeFileSync(cssFullPath, CSS_V4_IMPORTS + "\n" + cssContent, "utf-8");
|
|
1393
|
+
}
|
|
1394
|
+
} else {
|
|
1395
|
+
import_node_fs4.default.writeFileSync(cssFullPath, tokenImports + "\n" + cssContent, "utf-8");
|
|
1396
|
+
}
|
|
1375
1397
|
O2.success(`Added token imports to ${config.cssPath}`);
|
|
1376
1398
|
} else {
|
|
1377
1399
|
O2.info(`Token imports already present in ${config.cssPath}`);
|
|
@@ -1830,9 +1852,9 @@ const alertVariants = cva(
|
|
|
1830
1852
|
{
|
|
1831
1853
|
variants: {
|
|
1832
1854
|
size: {
|
|
1833
|
-
sm: 'gap-2 p-
|
|
1834
|
-
default: 'gap-2.5 p-
|
|
1835
|
-
lg: 'gap-3 p-
|
|
1855
|
+
sm: 'gap-2 p-2.5 text-sm',
|
|
1856
|
+
default: 'gap-2.5 p-3.5 text-md',
|
|
1857
|
+
lg: 'gap-3 p-4 text-md',
|
|
1836
1858
|
},
|
|
1837
1859
|
radius: {
|
|
1838
1860
|
none: 'rounded-none',
|
|
@@ -1860,16 +1882,23 @@ const ICON_SIZE_MAP = {
|
|
|
1860
1882
|
lg: 'icon-md', // 20px
|
|
1861
1883
|
} as const
|
|
1862
1884
|
|
|
1885
|
+
/** Icon wrapper text class \u2014 matches title line-height for vertical alignment */
|
|
1886
|
+
const ICON_LINE_MAP = {
|
|
1887
|
+
sm: 'text-sm',
|
|
1888
|
+
default: 'text-md',
|
|
1889
|
+
lg: 'text-base',
|
|
1890
|
+
} as const
|
|
1891
|
+
|
|
1863
1892
|
const TITLE_SIZE_MAP = {
|
|
1864
|
-
sm: 'text-sm font-semibold
|
|
1865
|
-
default: 'font-semibold
|
|
1866
|
-
lg: 'text-base font-semibold
|
|
1893
|
+
sm: 'text-sm font-semibold tracking-tight',
|
|
1894
|
+
default: 'text-md font-semibold tracking-tight',
|
|
1895
|
+
lg: 'text-base font-semibold tracking-tight',
|
|
1867
1896
|
} as const
|
|
1868
1897
|
|
|
1869
1898
|
const DESC_SIZE_MAP = {
|
|
1870
1899
|
sm: 'text-xs mt-0.5',
|
|
1871
|
-
default: 'text-sm mt-
|
|
1872
|
-
lg: 'text-md mt-
|
|
1900
|
+
default: 'text-sm mt-0.5',
|
|
1901
|
+
lg: 'text-md mt-0.5',
|
|
1873
1902
|
} as const
|
|
1874
1903
|
|
|
1875
1904
|
// \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 +1983,7 @@ const AlertRoot = React.forwardRef<HTMLDivElement, AlertRootProps>(
|
|
|
1954
1983
|
>
|
|
1955
1984
|
{/* Icon */}
|
|
1956
1985
|
{!hideIcon && (
|
|
1957
|
-
<span className=
|
|
1986
|
+
<span className={cn('shrink-0 flex items-center min-h-[1lh]', ICON_LINE_MAP[size])}>
|
|
1958
1987
|
{icon || <StatusIcon className={ICON_SIZE_MAP[size]} />}
|
|
1959
1988
|
</span>
|
|
1960
1989
|
)}
|
|
@@ -3216,12 +3245,6 @@ const CardRoot = React.forwardRef<HTMLDivElement, CardProps>(
|
|
|
3216
3245
|
CardRoot.displayName = 'Card'
|
|
3217
3246
|
|
|
3218
3247
|
// \u2500\u2500 Size-based padding map (responsive: mobile \u2192 desktop) \u2500\u2500
|
|
3219
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
3220
|
-
const sizePaddingMap = {
|
|
3221
|
-
sm: 'p-4', // 16px
|
|
3222
|
-
default: 'p-4 sm:p-6', // 16px \u2192 24px
|
|
3223
|
-
lg: 'p-6 sm:p-8', // 24px \u2192 32px
|
|
3224
|
-
} as const
|
|
3225
3248
|
|
|
3226
3249
|
const sizePaddingXMap = {
|
|
3227
3250
|
sm: 'px-4', // 16px
|
|
@@ -3391,7 +3414,7 @@ const CardTitle = React.forwardRef<HTMLHeadingElement, CardTitleProps>(
|
|
|
3391
3414
|
<h3
|
|
3392
3415
|
ref={ref}
|
|
3393
3416
|
className={cn(
|
|
3394
|
-
'font-semibold text-foreground
|
|
3417
|
+
'text-base font-semibold text-foreground tracking-tight',
|
|
3395
3418
|
icon && 'flex items-center gap-2',
|
|
3396
3419
|
className
|
|
3397
3420
|
)}
|
|
@@ -4376,38 +4399,1068 @@ function ChartPie({
|
|
|
4376
4399
|
)
|
|
4377
4400
|
}
|
|
4378
4401
|
|
|
4379
|
-
// \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
|
|
4402
|
+
// \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
|
|
4403
|
+
|
|
4404
|
+
// \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
|
|
4405
|
+
const Chart = Object.assign(ChartContainer, {
|
|
4406
|
+
Bar: ChartBar,
|
|
4407
|
+
Line: ChartLine,
|
|
4408
|
+
Area: ChartArea,
|
|
4409
|
+
Pie: ChartPie,
|
|
4410
|
+
Tooltip: ChartTooltip,
|
|
4411
|
+
TooltipContent: ChartTooltipContent,
|
|
4412
|
+
Legend: ChartLegend,
|
|
4413
|
+
LegendContent: ChartLegendContent,
|
|
4414
|
+
XAxis: ChartXAxis,
|
|
4415
|
+
YAxis: ChartYAxis,
|
|
4416
|
+
Style: ChartStyle,
|
|
4417
|
+
})
|
|
4418
|
+
|
|
4419
|
+
export {
|
|
4420
|
+
Chart,
|
|
4421
|
+
ChartContainer,
|
|
4422
|
+
ChartBar,
|
|
4423
|
+
ChartLine,
|
|
4424
|
+
ChartArea,
|
|
4425
|
+
ChartPie,
|
|
4426
|
+
ChartTooltip,
|
|
4427
|
+
ChartTooltipContent,
|
|
4428
|
+
ChartLegend,
|
|
4429
|
+
ChartLegendContent,
|
|
4430
|
+
ChartXAxis,
|
|
4431
|
+
ChartYAxis,
|
|
4432
|
+
ChartStyle,
|
|
4433
|
+
useChart,
|
|
4434
|
+
}
|
|
4435
|
+
`,
|
|
4436
|
+
type: "ui"
|
|
4437
|
+
}]
|
|
4438
|
+
},
|
|
4439
|
+
"chat-input": {
|
|
4440
|
+
name: "chat-input",
|
|
4441
|
+
dependencies: [],
|
|
4442
|
+
registryDependencies: [],
|
|
4443
|
+
reverseDependencies: [],
|
|
4444
|
+
namespace: true,
|
|
4445
|
+
description: "",
|
|
4446
|
+
files: [{
|
|
4447
|
+
path: "chat-input.tsx",
|
|
4448
|
+
content: `'use client'
|
|
4449
|
+
|
|
4450
|
+
import * as React from 'react'
|
|
4451
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
4452
|
+
import { cn } from '@/lib/utils'
|
|
4453
|
+
|
|
4454
|
+
// ============================================================================
|
|
4455
|
+
// Size maps
|
|
4456
|
+
// ============================================================================
|
|
4457
|
+
|
|
4458
|
+
/** Textarea padding per size */
|
|
4459
|
+
const fieldPaddingMap = {
|
|
4460
|
+
sm: 'pt-2.5 px-3 text-md',
|
|
4461
|
+
default: 'pt-3 px-4 text-md',
|
|
4462
|
+
lg: 'pt-4 px-5 text-base',
|
|
4463
|
+
} as const
|
|
4464
|
+
|
|
4465
|
+
/** Submit row padding per size */
|
|
4466
|
+
const submitWrapperMap = {
|
|
4467
|
+
sm: 'px-2.5 pb-2.5',
|
|
4468
|
+
default: 'px-3 pb-3',
|
|
4469
|
+
lg: 'px-4 pb-4',
|
|
4470
|
+
} as const
|
|
4471
|
+
|
|
4472
|
+
/** Submit button dimensions per size */
|
|
4473
|
+
const submitSizeMap = {
|
|
4474
|
+
sm: 'h-7 w-7',
|
|
4475
|
+
default: 'h-8 w-8',
|
|
4476
|
+
lg: 'h-9 w-9',
|
|
4477
|
+
} as const
|
|
4478
|
+
|
|
4479
|
+
/** Submit button radius \u2014 auto-calculated from container radius (min rounded-md) */
|
|
4480
|
+
const submitRadiusMap = {
|
|
4481
|
+
sm: 'rounded-md',
|
|
4482
|
+
md: 'rounded-md',
|
|
4483
|
+
lg: 'rounded-md',
|
|
4484
|
+
xl: 'rounded-lg',
|
|
4485
|
+
'2xl': 'rounded-xl',
|
|
4486
|
+
full: 'rounded-full',
|
|
4487
|
+
} as const
|
|
4488
|
+
|
|
4489
|
+
/** Submit button radius \u2014 direct class map for explicit buttonRadius prop */
|
|
4490
|
+
const buttonRadiusClassMap: Record<ChatInputRadius, string> = {
|
|
4491
|
+
sm: 'rounded-sm',
|
|
4492
|
+
md: 'rounded-md',
|
|
4493
|
+
lg: 'rounded-lg',
|
|
4494
|
+
xl: 'rounded-xl',
|
|
4495
|
+
'2xl': 'rounded-2xl',
|
|
4496
|
+
full: 'rounded-full',
|
|
4497
|
+
}
|
|
4498
|
+
|
|
4499
|
+
/** Submit icon size class per size */
|
|
4500
|
+
const submitIconSizeMap = {
|
|
4501
|
+
sm: 'icon-xs',
|
|
4502
|
+
default: 'icon-xs',
|
|
4503
|
+
lg: 'icon-sm',
|
|
4504
|
+
} as const
|
|
4505
|
+
|
|
4506
|
+
/** Character count text size per size */
|
|
4507
|
+
const countStyleMap = {
|
|
4508
|
+
sm: 'text-xs',
|
|
4509
|
+
default: 'text-xs',
|
|
4510
|
+
lg: 'text-sm',
|
|
4511
|
+
} as const
|
|
4512
|
+
|
|
4513
|
+
/** Approximate line-height (px) per size for auto-resize */
|
|
4514
|
+
const lineHeightMap = {
|
|
4515
|
+
sm: 20,
|
|
4516
|
+
default: 22,
|
|
4517
|
+
lg: 24,
|
|
4518
|
+
} as const
|
|
4519
|
+
|
|
4520
|
+
/** Field padding in inline layout \u2014 horizontal only (vertical centering via flex) */
|
|
4521
|
+
const inlineFieldPaddingMap = {
|
|
4522
|
+
sm: 'px-3 text-md',
|
|
4523
|
+
default: 'px-4 text-md',
|
|
4524
|
+
lg: 'px-5 text-base',
|
|
4525
|
+
} as const
|
|
4526
|
+
|
|
4527
|
+
/** Submit wrapper padding in inline layout \u2014 symmetric vertical */
|
|
4528
|
+
const submitWrapperInlineMap = {
|
|
4529
|
+
sm: 'px-2.5 py-2.5',
|
|
4530
|
+
default: 'px-3 py-3',
|
|
4531
|
+
lg: 'px-4 py-4',
|
|
4532
|
+
} as const
|
|
4533
|
+
|
|
4534
|
+
// ============================================================================
|
|
4535
|
+
// CVA \u2014 root container variants
|
|
4536
|
+
// ============================================================================
|
|
4537
|
+
|
|
4538
|
+
const chatInputVariants = cva(
|
|
4539
|
+
[
|
|
4540
|
+
'flex flex-col w-full overflow-hidden cursor-text',
|
|
4541
|
+
'transition-colors duration-micro',
|
|
4542
|
+
].join(' '),
|
|
4543
|
+
{
|
|
4544
|
+
variants: {
|
|
4545
|
+
variant: {
|
|
4546
|
+
outline: [
|
|
4547
|
+
'border border-border bg-background',
|
|
4548
|
+
'hover:border-border-strong',
|
|
4549
|
+
'focus-within:border-border-strong focus-within:hover:border-border-strong',
|
|
4550
|
+
].join(' '),
|
|
4551
|
+
filled: [
|
|
4552
|
+
'border border-transparent bg-background-muted',
|
|
4553
|
+
'focus-within:border-border',
|
|
4554
|
+
].join(' '),
|
|
4555
|
+
},
|
|
4556
|
+
radius: {
|
|
4557
|
+
sm: 'rounded-sm',
|
|
4558
|
+
md: 'rounded-md',
|
|
4559
|
+
lg: 'rounded-lg',
|
|
4560
|
+
xl: 'rounded-xl',
|
|
4561
|
+
'2xl': 'rounded-2xl',
|
|
4562
|
+
full: 'rounded-full',
|
|
4563
|
+
},
|
|
4564
|
+
},
|
|
4565
|
+
defaultVariants: {
|
|
4566
|
+
variant: 'outline',
|
|
4567
|
+
radius: 'xl',
|
|
4568
|
+
},
|
|
4569
|
+
}
|
|
4570
|
+
)
|
|
4571
|
+
|
|
4572
|
+
// ============================================================================
|
|
4573
|
+
// Types
|
|
4574
|
+
// ============================================================================
|
|
4575
|
+
|
|
4576
|
+
export type ChatInputSize = 'sm' | 'default' | 'lg'
|
|
4577
|
+
export type ChatInputVariant = 'outline' | 'filled'
|
|
4578
|
+
export type ChatInputRadius = 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full'
|
|
4579
|
+
export type ChatInputColor = 'default' | 'primary'
|
|
4580
|
+
export type ChatInputLayout = 'default' | 'inline'
|
|
4581
|
+
|
|
4582
|
+
// ============================================================================
|
|
4583
|
+
// Context
|
|
4584
|
+
// ============================================================================
|
|
4585
|
+
|
|
4586
|
+
interface ChatInputContextValue {
|
|
4587
|
+
size: ChatInputSize
|
|
4588
|
+
color: ChatInputColor
|
|
4589
|
+
layout: ChatInputLayout
|
|
4590
|
+
radius: ChatInputRadius
|
|
4591
|
+
disabled: boolean
|
|
4592
|
+
isEmpty: boolean
|
|
4593
|
+
charCount: number
|
|
4594
|
+
showCount: boolean
|
|
4595
|
+
maxLength: number | undefined
|
|
4596
|
+
_setIsEmpty: React.Dispatch<React.SetStateAction<boolean>>
|
|
4597
|
+
_setCharCount: React.Dispatch<React.SetStateAction<number>>
|
|
4598
|
+
_setShowCount: React.Dispatch<React.SetStateAction<boolean>>
|
|
4599
|
+
_setMaxLength: React.Dispatch<React.SetStateAction<number | undefined>>
|
|
4600
|
+
_fieldRef: React.MutableRefObject<HTMLTextAreaElement | null>
|
|
4601
|
+
_handleSubmit: () => void
|
|
4602
|
+
}
|
|
4603
|
+
|
|
4604
|
+
const ChatInputContext = React.createContext<ChatInputContextValue>({
|
|
4605
|
+
size: 'default',
|
|
4606
|
+
color: 'default',
|
|
4607
|
+
layout: 'default',
|
|
4608
|
+
radius: 'xl',
|
|
4609
|
+
disabled: false,
|
|
4610
|
+
isEmpty: true,
|
|
4611
|
+
charCount: 0,
|
|
4612
|
+
showCount: false,
|
|
4613
|
+
maxLength: undefined,
|
|
4614
|
+
_setIsEmpty: () => {},
|
|
4615
|
+
_setCharCount: () => {},
|
|
4616
|
+
_setShowCount: () => {},
|
|
4617
|
+
_setMaxLength: () => {},
|
|
4618
|
+
_fieldRef: { current: null },
|
|
4619
|
+
_handleSubmit: () => {},
|
|
4620
|
+
})
|
|
4621
|
+
|
|
4622
|
+
// ============================================================================
|
|
4623
|
+
// Props interfaces
|
|
4624
|
+
// ============================================================================
|
|
4625
|
+
|
|
4626
|
+
export interface ChatInputRootProps
|
|
4627
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onSubmit'>,
|
|
4628
|
+
VariantProps<typeof chatInputVariants> {
|
|
4629
|
+
/** Visual style of the container */
|
|
4630
|
+
variant?: ChatInputVariant
|
|
4631
|
+
/** Corner radius */
|
|
4632
|
+
radius?: ChatInputRadius
|
|
4633
|
+
/** Size scale */
|
|
4634
|
+
size?: ChatInputSize
|
|
4635
|
+
/** Submit button color \u2014 default (dark) or primary (brand) */
|
|
4636
|
+
color?: ChatInputColor
|
|
4637
|
+
/** Layout direction \u2014 default (stacked) or inline (field + button side by side) */
|
|
4638
|
+
layout?: ChatInputLayout
|
|
4639
|
+
/** Disable all interaction */
|
|
4640
|
+
disabled?: boolean
|
|
4641
|
+
/** Called when the user submits (Enter key or Submit button) \u2014 receives current field value */
|
|
4642
|
+
onSubmit?: (value: string) => void
|
|
4643
|
+
}
|
|
4644
|
+
|
|
4645
|
+
export interface ChatInputFieldProps
|
|
4646
|
+
extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'children'> {
|
|
4647
|
+
/** Maximum visible rows before the field scrolls */
|
|
4648
|
+
maxRows?: number
|
|
4649
|
+
/** Show character counter \u2014 requires maxLength to be set */
|
|
4650
|
+
showCount?: boolean
|
|
4651
|
+
}
|
|
4652
|
+
|
|
4653
|
+
export interface ChatInputSubmitProps
|
|
4654
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
4655
|
+
/** Show loading/stop state \u2014 keeps button enabled so user can stop generation */
|
|
4656
|
+
loading?: boolean
|
|
4657
|
+
/** Called when clicked during loading state (e.g., stop generation) */
|
|
4658
|
+
onStop?: () => void
|
|
4659
|
+
/** Override the auto-calculated button radius */
|
|
4660
|
+
buttonRadius?: ChatInputRadius
|
|
4661
|
+
}
|
|
4662
|
+
|
|
4663
|
+
// ============================================================================
|
|
4664
|
+
// ChatInput \u2014 Root container
|
|
4665
|
+
// ============================================================================
|
|
4666
|
+
|
|
4667
|
+
const ChatInputRoot = React.forwardRef<HTMLDivElement, ChatInputRootProps>(
|
|
4668
|
+
(
|
|
4669
|
+
{
|
|
4670
|
+
className,
|
|
4671
|
+
variant = 'outline',
|
|
4672
|
+
radius = 'xl',
|
|
4673
|
+
size = 'default',
|
|
4674
|
+
color = 'default',
|
|
4675
|
+
layout = 'default',
|
|
4676
|
+
disabled = false,
|
|
4677
|
+
onSubmit,
|
|
4678
|
+
children,
|
|
4679
|
+
...props
|
|
4680
|
+
},
|
|
4681
|
+
ref
|
|
4682
|
+
) => {
|
|
4683
|
+
const [isEmpty, setIsEmpty] = React.useState(true)
|
|
4684
|
+
const [charCount, setCharCount] = React.useState(0)
|
|
4685
|
+
const [showCount, setShowCount] = React.useState(false)
|
|
4686
|
+
const [maxLength, setMaxLength] = React.useState<number | undefined>(undefined)
|
|
4687
|
+
const fieldRef = React.useRef<HTMLTextAreaElement | null>(null)
|
|
4688
|
+
|
|
4689
|
+
const handleSubmit = React.useCallback(() => {
|
|
4690
|
+
const el = fieldRef.current
|
|
4691
|
+
if (!el || disabled) return
|
|
4692
|
+
const value = el.value.trim()
|
|
4693
|
+
if (!value) return
|
|
4694
|
+
onSubmit?.(value)
|
|
4695
|
+
// Auto-clear in uncontrolled mode
|
|
4696
|
+
// Controlled-mode users should clear value inside their onSubmit callback
|
|
4697
|
+
el.value = ''
|
|
4698
|
+
el.style.height = 'auto'
|
|
4699
|
+
setIsEmpty(true)
|
|
4700
|
+
setCharCount(0)
|
|
4701
|
+
}, [disabled, onSubmit])
|
|
4702
|
+
|
|
4703
|
+
const contextValue = React.useMemo<ChatInputContextValue>(
|
|
4704
|
+
() => ({
|
|
4705
|
+
size,
|
|
4706
|
+
color,
|
|
4707
|
+
layout,
|
|
4708
|
+
radius,
|
|
4709
|
+
disabled,
|
|
4710
|
+
isEmpty,
|
|
4711
|
+
charCount,
|
|
4712
|
+
showCount,
|
|
4713
|
+
maxLength,
|
|
4714
|
+
_setIsEmpty: setIsEmpty,
|
|
4715
|
+
_setCharCount: setCharCount,
|
|
4716
|
+
_setShowCount: setShowCount,
|
|
4717
|
+
_setMaxLength: setMaxLength,
|
|
4718
|
+
_fieldRef: fieldRef,
|
|
4719
|
+
_handleSubmit: handleSubmit,
|
|
4720
|
+
}),
|
|
4721
|
+
[size, color, layout, radius, disabled, isEmpty, charCount, showCount, maxLength, handleSubmit]
|
|
4722
|
+
)
|
|
4723
|
+
|
|
4724
|
+
const handleContainerClick = React.useCallback(
|
|
4725
|
+
(e: React.MouseEvent<HTMLDivElement>) => {
|
|
4726
|
+
// Focus the textarea when clicking anywhere in the container (not on a button)
|
|
4727
|
+
if (!disabled && !(e.target as HTMLElement).closest('button')) {
|
|
4728
|
+
fieldRef.current?.focus()
|
|
4729
|
+
}
|
|
4730
|
+
},
|
|
4731
|
+
[disabled]
|
|
4732
|
+
)
|
|
4733
|
+
|
|
4734
|
+
return (
|
|
4735
|
+
<ChatInputContext.Provider value={contextValue}>
|
|
4736
|
+
<div
|
|
4737
|
+
ref={ref}
|
|
4738
|
+
data-disabled={disabled || undefined}
|
|
4739
|
+
onClick={handleContainerClick}
|
|
4740
|
+
className={cn(
|
|
4741
|
+
chatInputVariants({ variant, radius }),
|
|
4742
|
+
layout === 'inline' && 'flex-row',
|
|
4743
|
+
disabled && 'opacity-50 pointer-events-none',
|
|
4744
|
+
className
|
|
4745
|
+
)}
|
|
4746
|
+
{...props}
|
|
4747
|
+
>
|
|
4748
|
+
{children}
|
|
4749
|
+
</div>
|
|
4750
|
+
</ChatInputContext.Provider>
|
|
4751
|
+
)
|
|
4752
|
+
}
|
|
4753
|
+
)
|
|
4754
|
+
ChatInputRoot.displayName = 'ChatInput'
|
|
4755
|
+
|
|
4756
|
+
// ============================================================================
|
|
4757
|
+
// ChatInput.Field \u2014 Auto-resizing textarea
|
|
4758
|
+
// ============================================================================
|
|
4759
|
+
|
|
4760
|
+
const ChatInputField = React.forwardRef<HTMLTextAreaElement, ChatInputFieldProps>(
|
|
4761
|
+
(
|
|
4762
|
+
{
|
|
4763
|
+
className,
|
|
4764
|
+
maxRows = 8,
|
|
4765
|
+
showCount = false,
|
|
4766
|
+
maxLength,
|
|
4767
|
+
placeholder,
|
|
4768
|
+
onChange,
|
|
4769
|
+
onKeyDown,
|
|
4770
|
+
disabled,
|
|
4771
|
+
...props
|
|
4772
|
+
},
|
|
4773
|
+
forwardedRef
|
|
4774
|
+
) => {
|
|
4775
|
+
const ctx = React.useContext(ChatInputContext)
|
|
4776
|
+
const internalRef = React.useRef<HTMLTextAreaElement | null>(null)
|
|
4777
|
+
const isDisabled = disabled ?? ctx.disabled
|
|
4778
|
+
|
|
4779
|
+
// Sync showCount + maxLength into root context on mount/update
|
|
4780
|
+
React.useEffect(() => {
|
|
4781
|
+
ctx._setShowCount(showCount)
|
|
4782
|
+
ctx._setMaxLength(maxLength)
|
|
4783
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
4784
|
+
}, [showCount, maxLength])
|
|
4785
|
+
|
|
4786
|
+
// Merge forwarded ref + internal ref + context field ref
|
|
4787
|
+
const setRef = React.useCallback(
|
|
4788
|
+
(el: HTMLTextAreaElement | null) => {
|
|
4789
|
+
internalRef.current = el
|
|
4790
|
+
ctx._fieldRef.current = el
|
|
4791
|
+
if (typeof forwardedRef === 'function') {
|
|
4792
|
+
forwardedRef(el)
|
|
4793
|
+
} else if (forwardedRef) {
|
|
4794
|
+
(forwardedRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = el
|
|
4795
|
+
}
|
|
4796
|
+
},
|
|
4797
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
4798
|
+
[forwardedRef]
|
|
4799
|
+
)
|
|
4800
|
+
|
|
4801
|
+
// Adjust textarea height to content, bounded by maxRows
|
|
4802
|
+
const autoResize = React.useCallback(() => {
|
|
4803
|
+
const el = internalRef.current
|
|
4804
|
+
if (!el) return
|
|
4805
|
+
el.style.height = 'auto'
|
|
4806
|
+
const maxHeight = maxRows * lineHeightMap[ctx.size]
|
|
4807
|
+
const newHeight = Math.min(el.scrollHeight, maxHeight)
|
|
4808
|
+
el.style.height = \`\${newHeight}px\`
|
|
4809
|
+
el.style.overflowY = el.scrollHeight > maxHeight ? 'auto' : 'hidden'
|
|
4810
|
+
}, [ctx.size, maxRows])
|
|
4811
|
+
|
|
4812
|
+
const handleChange = React.useCallback(
|
|
4813
|
+
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
4814
|
+
const val = e.target.value
|
|
4815
|
+
ctx._setIsEmpty(val.trim() === '')
|
|
4816
|
+
ctx._setCharCount(val.length)
|
|
4817
|
+
autoResize()
|
|
4818
|
+
onChange?.(e)
|
|
4819
|
+
},
|
|
4820
|
+
[ctx, autoResize, onChange]
|
|
4821
|
+
)
|
|
4822
|
+
|
|
4823
|
+
const handleKeyDown = React.useCallback(
|
|
4824
|
+
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
4825
|
+
// Enter = submit | Shift+Enter = new line
|
|
4826
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
4827
|
+
e.preventDefault()
|
|
4828
|
+
ctx._handleSubmit()
|
|
4829
|
+
}
|
|
4830
|
+
onKeyDown?.(e)
|
|
4831
|
+
},
|
|
4832
|
+
[ctx, onKeyDown]
|
|
4833
|
+
)
|
|
4834
|
+
|
|
4835
|
+
return (
|
|
4836
|
+
<div className={cn('flex-1 min-w-0', ctx.layout === 'inline' && 'flex items-center self-stretch')}>
|
|
4837
|
+
<textarea
|
|
4838
|
+
ref={setRef}
|
|
4839
|
+
rows={1}
|
|
4840
|
+
placeholder={placeholder}
|
|
4841
|
+
maxLength={maxLength}
|
|
4842
|
+
disabled={isDisabled}
|
|
4843
|
+
className={cn(
|
|
4844
|
+
'w-full bg-transparent text-foreground placeholder:text-foreground/30',
|
|
4845
|
+
'resize-none outline-none focus:outline-none',
|
|
4846
|
+
'disabled:cursor-not-allowed',
|
|
4847
|
+
'overflow-hidden',
|
|
4848
|
+
ctx.layout === 'inline' ? inlineFieldPaddingMap[ctx.size] : fieldPaddingMap[ctx.size],
|
|
4849
|
+
className
|
|
4850
|
+
)}
|
|
4851
|
+
onChange={handleChange}
|
|
4852
|
+
onKeyDown={handleKeyDown}
|
|
4853
|
+
{...props}
|
|
4854
|
+
/>
|
|
4855
|
+
</div>
|
|
4856
|
+
)
|
|
4857
|
+
}
|
|
4858
|
+
)
|
|
4859
|
+
ChatInputField.displayName = 'ChatInput.Field'
|
|
4860
|
+
|
|
4861
|
+
// ============================================================================
|
|
4862
|
+
// ChatInput.Submit \u2014 Send / stop button
|
|
4863
|
+
// ============================================================================
|
|
4864
|
+
|
|
4865
|
+
const ChatInputSubmit = React.forwardRef<HTMLButtonElement, ChatInputSubmitProps>(
|
|
4866
|
+
(
|
|
4867
|
+
{
|
|
4868
|
+
className,
|
|
4869
|
+
loading = false,
|
|
4870
|
+
onStop,
|
|
4871
|
+
children,
|
|
4872
|
+
disabled,
|
|
4873
|
+
onClick,
|
|
4874
|
+
buttonRadius,
|
|
4875
|
+
...props
|
|
4876
|
+
},
|
|
4877
|
+
ref
|
|
4878
|
+
) => {
|
|
4879
|
+
const ctx = React.useContext(ChatInputContext)
|
|
4880
|
+
|
|
4881
|
+
// Disabled: context disabled | user prop | empty field (only when not loading)
|
|
4882
|
+
const isDisabled = (disabled ?? ctx.disabled) || (!loading && ctx.isEmpty)
|
|
4883
|
+
|
|
4884
|
+
const handleClick = React.useCallback(
|
|
4885
|
+
(e: React.MouseEvent<HTMLButtonElement>) => {
|
|
4886
|
+
if (loading) {
|
|
4887
|
+
onStop?.()
|
|
4888
|
+
} else if (!isDisabled) {
|
|
4889
|
+
ctx._handleSubmit()
|
|
4890
|
+
}
|
|
4891
|
+
onClick?.(e)
|
|
4892
|
+
},
|
|
4893
|
+
[ctx, isDisabled, loading, onStop, onClick]
|
|
4894
|
+
)
|
|
4895
|
+
|
|
4896
|
+
return (
|
|
4897
|
+
<div className={cn('flex items-center justify-end gap-2 shrink-0 cursor-default', ctx.layout === 'inline' ? submitWrapperInlineMap[ctx.size] : submitWrapperMap[ctx.size])}>
|
|
4898
|
+
{ctx.showCount && ctx.maxLength != null && (
|
|
4899
|
+
<span className={cn('text-text-subtle select-none', countStyleMap[ctx.size])}>
|
|
4900
|
+
{ctx.charCount} / {ctx.maxLength}
|
|
4901
|
+
</span>
|
|
4902
|
+
)}
|
|
4903
|
+
<button
|
|
4904
|
+
ref={ref}
|
|
4905
|
+
type="button"
|
|
4906
|
+
disabled={isDisabled && !loading}
|
|
4907
|
+
aria-label={loading ? 'Stop generating' : 'Send message'}
|
|
4908
|
+
className={cn(
|
|
4909
|
+
'inline-flex items-center justify-center shrink-0',
|
|
4910
|
+
buttonRadius ? buttonRadiusClassMap[buttonRadius] : submitRadiusMap[ctx.radius],
|
|
4911
|
+
'transition-all duration-fast',
|
|
4912
|
+
'focus-visible:outline-none focus-visible:focus-ring',
|
|
4913
|
+
submitSizeMap[ctx.size],
|
|
4914
|
+
// Loading state \u2014 enabled, pulsing
|
|
4915
|
+
loading && ctx.color === 'default' && 'bg-foreground text-background animate-pulse cursor-pointer',
|
|
4916
|
+
loading && ctx.color === 'primary' && 'bg-primary text-primary-foreground animate-pulse cursor-pointer',
|
|
4917
|
+
// Enabled state
|
|
4918
|
+
!loading && !isDisabled && ctx.color === 'default' && [
|
|
4919
|
+
'bg-foreground text-background',
|
|
4920
|
+
'hover:bg-foreground/90 active:bg-foreground/80 active:scale-pressed cursor-pointer',
|
|
4921
|
+
],
|
|
4922
|
+
!loading && !isDisabled && ctx.color === 'primary' && [
|
|
4923
|
+
'bg-primary text-primary-foreground',
|
|
4924
|
+
'hover:bg-primary-hover active:scale-pressed cursor-pointer',
|
|
4925
|
+
],
|
|
4926
|
+
// Disabled state
|
|
4927
|
+
!loading && isDisabled && 'bg-background-muted text-text-subtle pointer-events-none',
|
|
4928
|
+
className
|
|
4929
|
+
)}
|
|
4930
|
+
onClick={handleClick}
|
|
4931
|
+
{...props}
|
|
4932
|
+
>
|
|
4933
|
+
<span
|
|
4934
|
+
aria-hidden="true"
|
|
4935
|
+
className={cn('flex items-center justify-center', submitIconSizeMap[ctx.size])}
|
|
4936
|
+
>
|
|
4937
|
+
{children ?? (loading ? <StopIcon /> : <SendIcon />)}
|
|
4938
|
+
</span>
|
|
4939
|
+
</button>
|
|
4940
|
+
</div>
|
|
4941
|
+
)
|
|
4942
|
+
}
|
|
4943
|
+
)
|
|
4944
|
+
ChatInputSubmit.displayName = 'ChatInput.Submit'
|
|
4945
|
+
|
|
4946
|
+
// ============================================================================
|
|
4947
|
+
// Built-in SVG icons \u2014 no external icon dependency
|
|
4948
|
+
// ============================================================================
|
|
4949
|
+
|
|
4950
|
+
function SendIcon() {
|
|
4951
|
+
return (
|
|
4952
|
+
<svg
|
|
4953
|
+
viewBox="0 0 24 24"
|
|
4954
|
+
fill="none"
|
|
4955
|
+
stroke="currentColor"
|
|
4956
|
+
strokeWidth={2.5}
|
|
4957
|
+
strokeLinecap="round"
|
|
4958
|
+
strokeLinejoin="round"
|
|
4959
|
+
className="w-full h-full"
|
|
4960
|
+
>
|
|
4961
|
+
<path d="M22 2 11 13" />
|
|
4962
|
+
<path d="M22 2 15 22 11 13 2 9l20-7Z" />
|
|
4963
|
+
</svg>
|
|
4964
|
+
)
|
|
4965
|
+
}
|
|
4966
|
+
|
|
4967
|
+
function StopIcon() {
|
|
4968
|
+
return (
|
|
4969
|
+
<svg viewBox="0 0 24 24" fill="currentColor" className="w-full h-full">
|
|
4970
|
+
<rect x="5" y="5" width="14" height="14" rx="2" />
|
|
4971
|
+
</svg>
|
|
4972
|
+
)
|
|
4973
|
+
}
|
|
4974
|
+
|
|
4975
|
+
// ============================================================================
|
|
4976
|
+
// Namespace export (compound pattern)
|
|
4977
|
+
// ============================================================================
|
|
4978
|
+
|
|
4979
|
+
const ChatInput = Object.assign(ChatInputRoot, {
|
|
4980
|
+
Field: ChatInputField,
|
|
4981
|
+
Submit: ChatInputSubmit,
|
|
4982
|
+
})
|
|
4983
|
+
|
|
4984
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
4985
|
+
namespace ChatInput {
|
|
4986
|
+
export type RootProps = ChatInputRootProps
|
|
4987
|
+
export type FieldProps = ChatInputFieldProps
|
|
4988
|
+
export type SubmitProps = ChatInputSubmitProps
|
|
4989
|
+
export type Layout = ChatInputLayout
|
|
4990
|
+
}
|
|
4991
|
+
|
|
4992
|
+
export {
|
|
4993
|
+
ChatInput,
|
|
4994
|
+
ChatInputRoot,
|
|
4995
|
+
ChatInputField,
|
|
4996
|
+
ChatInputSubmit,
|
|
4997
|
+
chatInputVariants,
|
|
4998
|
+
}
|
|
4999
|
+
`,
|
|
5000
|
+
type: "ui"
|
|
5001
|
+
}]
|
|
5002
|
+
},
|
|
5003
|
+
"chat-message": {
|
|
5004
|
+
name: "chat-message",
|
|
5005
|
+
dependencies: [],
|
|
5006
|
+
registryDependencies: ["avatar", "typing-indicator"],
|
|
5007
|
+
reverseDependencies: [],
|
|
5008
|
+
namespace: true,
|
|
5009
|
+
description: "",
|
|
5010
|
+
files: [{
|
|
5011
|
+
path: "chat-message.tsx",
|
|
5012
|
+
content: `'use client'
|
|
5013
|
+
|
|
5014
|
+
import * as React from 'react'
|
|
5015
|
+
import { cn } from '@/lib/utils'
|
|
5016
|
+
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
|
5017
|
+
import { TypingIndicator, type TypingIndicatorProps } from '@/components/ui/typing-indicator'
|
|
5018
|
+
|
|
5019
|
+
// ============================================================================
|
|
5020
|
+
// Types
|
|
5021
|
+
// ============================================================================
|
|
5022
|
+
|
|
5023
|
+
export type ChatMessageRole = 'user' | 'assistant'
|
|
5024
|
+
export type ChatMessageVariant = 'bubble' | 'flat'
|
|
5025
|
+
export type ChatMessageColor = 'default' | 'muted' | 'primary' | 'dark'
|
|
5026
|
+
export type ChatMessageSize = 'sm' | 'default' | 'lg'
|
|
5027
|
+
export type ChatMessageRadius = 'md' | 'lg' | 'xl' | '2xl'
|
|
5028
|
+
export type ChatMessageAvatarSize = 'sm' | 'md' | 'lg'
|
|
5029
|
+
|
|
5030
|
+
/** Built-in status presets (pass as string to Footer.status for auto-rendering) */
|
|
5031
|
+
export type ChatMessageStatus = 'sending' | 'sent' | 'read' | 'error'
|
|
5032
|
+
|
|
5033
|
+
// ============================================================================
|
|
5034
|
+
// Context \u2014 propagates root props to sub-components
|
|
5035
|
+
// ============================================================================
|
|
5036
|
+
|
|
5037
|
+
interface ChatMessageContextValue {
|
|
5038
|
+
role: ChatMessageRole
|
|
5039
|
+
variant: ChatMessageVariant
|
|
5040
|
+
color: ChatMessageColor
|
|
5041
|
+
size: ChatMessageSize
|
|
5042
|
+
radius: ChatMessageRadius
|
|
5043
|
+
tail: boolean
|
|
5044
|
+
typing: boolean | Omit<TypingIndicatorProps, 'className'>
|
|
5045
|
+
actions?: React.ReactNode
|
|
5046
|
+
}
|
|
5047
|
+
|
|
5048
|
+
const ChatMessageContext = React.createContext<ChatMessageContextValue>({
|
|
5049
|
+
role: 'assistant',
|
|
5050
|
+
variant: 'bubble',
|
|
5051
|
+
color: 'default',
|
|
5052
|
+
size: 'default',
|
|
5053
|
+
radius: '2xl',
|
|
5054
|
+
tail: true,
|
|
5055
|
+
typing: false,
|
|
5056
|
+
actions: undefined,
|
|
5057
|
+
})
|
|
5058
|
+
|
|
5059
|
+
// ============================================================================
|
|
5060
|
+
// Size maps
|
|
5061
|
+
// ============================================================================
|
|
5062
|
+
|
|
5063
|
+
/** Bubble padding per size */
|
|
5064
|
+
const bubblePaddingMap: Record<ChatMessageSize, string> = {
|
|
5065
|
+
sm: 'px-3 py-1.5',
|
|
5066
|
+
default: 'px-3.5 py-2',
|
|
5067
|
+
lg: 'px-4 py-2.5',
|
|
5068
|
+
}
|
|
5069
|
+
|
|
5070
|
+
/** Bubble text class per size */
|
|
5071
|
+
const bubbleTextMap: Record<ChatMessageSize, string> = {
|
|
5072
|
+
sm: 'text-sm',
|
|
5073
|
+
default: 'text-md',
|
|
5074
|
+
lg: 'text-base',
|
|
5075
|
+
}
|
|
5076
|
+
|
|
5077
|
+
/** Footer text size per size */
|
|
5078
|
+
const footerTextMap: Record<ChatMessageSize, string> = {
|
|
5079
|
+
sm: 'text-2xs',
|
|
5080
|
+
default: 'text-2xs',
|
|
5081
|
+
lg: 'text-xs',
|
|
5082
|
+
}
|
|
5083
|
+
|
|
5084
|
+
/** Gap between avatar and content per message size */
|
|
5085
|
+
const rowGapMap: Record<ChatMessageSize, string> = {
|
|
5086
|
+
sm: 'gap-2',
|
|
5087
|
+
default: 'gap-2.5',
|
|
5088
|
+
lg: 'gap-3',
|
|
5089
|
+
}
|
|
5090
|
+
|
|
5091
|
+
/** Gap override when avatarSize is explicitly set */
|
|
5092
|
+
const avatarGapMap: Record<ChatMessageAvatarSize, string> = {
|
|
5093
|
+
sm: 'gap-1.5',
|
|
5094
|
+
md: 'gap-2.5',
|
|
5095
|
+
lg: 'gap-3',
|
|
5096
|
+
}
|
|
5097
|
+
|
|
5098
|
+
// ============================================================================
|
|
5099
|
+
// Chat avatar sizes (independent from message size)
|
|
5100
|
+
// ============================================================================
|
|
5101
|
+
|
|
5102
|
+
const chatAvatarSizeMap: Record<ChatMessageAvatarSize, string> = {
|
|
5103
|
+
sm: 'w-6 h-6', // 24px
|
|
5104
|
+
md: 'w-7 h-7', // 28px
|
|
5105
|
+
lg: 'w-8 h-8', // 32px
|
|
5106
|
+
}
|
|
5107
|
+
|
|
5108
|
+
const chatAvatarFontMap: Record<ChatMessageAvatarSize, string> = {
|
|
5109
|
+
sm: 'text-2xs',
|
|
5110
|
+
md: 'text-2xs',
|
|
5111
|
+
lg: 'text-xs',
|
|
5112
|
+
}
|
|
5113
|
+
|
|
5114
|
+
// ============================================================================
|
|
5115
|
+
// Bubble radius \u2014 static maps for Tailwind JIT scanner compatibility
|
|
5116
|
+
// ============================================================================
|
|
5117
|
+
|
|
5118
|
+
/**
|
|
5119
|
+
* Asymmetric with tail: tail corner = rounded-none (0px).
|
|
5120
|
+
* assistant \u2192 tail at bottom-left | user \u2192 tail at bottom-right
|
|
5121
|
+
*/
|
|
5122
|
+
const bubbleRadiusTailMap: Record<ChatMessageRadius, Record<ChatMessageRole, string>> = {
|
|
5123
|
+
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' },
|
|
5124
|
+
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' },
|
|
5125
|
+
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' },
|
|
5126
|
+
'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' },
|
|
5127
|
+
}
|
|
5128
|
+
|
|
5129
|
+
/** Symmetric: all corners same radius */
|
|
5130
|
+
const bubbleRadiusSymMap: Record<ChatMessageRadius, string> = {
|
|
5131
|
+
md: 'rounded-md',
|
|
5132
|
+
lg: 'rounded-lg',
|
|
5133
|
+
xl: 'rounded-xl',
|
|
5134
|
+
'2xl': 'rounded-2xl',
|
|
5135
|
+
}
|
|
5136
|
+
|
|
5137
|
+
// ============================================================================
|
|
5138
|
+
// Bubble color helper \u2014 applies to both roles uniformly
|
|
5139
|
+
// ============================================================================
|
|
5140
|
+
|
|
5141
|
+
function getBubbleColors(color: ChatMessageColor, variant: ChatMessageVariant): string {
|
|
5142
|
+
if (variant === 'flat') {
|
|
5143
|
+
return color === 'muted' ? 'bg-background-muted' : ''
|
|
5144
|
+
}
|
|
5145
|
+
switch (color) {
|
|
5146
|
+
case 'default': return 'bg-background-paper border border-border text-foreground'
|
|
5147
|
+
case 'muted': return 'bg-background-muted text-foreground'
|
|
5148
|
+
case 'primary': return 'bg-primary text-primary-foreground'
|
|
5149
|
+
case 'dark': return 'bg-foreground text-background'
|
|
5150
|
+
}
|
|
5151
|
+
}
|
|
5152
|
+
|
|
5153
|
+
// ============================================================================
|
|
5154
|
+
// Built-in status labels
|
|
5155
|
+
// ============================================================================
|
|
5156
|
+
|
|
5157
|
+
const builtInStatusLabels: Record<string, { label: string; className: string }> = {
|
|
5158
|
+
sending: { label: 'Sending...', className: 'text-text-subtle' },
|
|
5159
|
+
sent: { label: 'Sent', className: 'text-text-subtle' },
|
|
5160
|
+
read: { label: 'Read', className: 'text-text-subtle' },
|
|
5161
|
+
error: { label: 'Failed', className: 'text-error' },
|
|
5162
|
+
}
|
|
5163
|
+
|
|
5164
|
+
// ============================================================================
|
|
5165
|
+
// TypingIndicator size mapping (chat sizes \u2192 indicator sizes)
|
|
5166
|
+
// ============================================================================
|
|
5167
|
+
|
|
5168
|
+
const typingSizeMap: Record<ChatMessageSize, 'sm' | 'default' | 'lg'> = {
|
|
5169
|
+
sm: 'sm',
|
|
5170
|
+
default: 'sm',
|
|
5171
|
+
lg: 'default',
|
|
5172
|
+
}
|
|
5173
|
+
|
|
5174
|
+
/** Minimum bubble width when typing \u2014 accounts for dot size + padding per chat size */
|
|
5175
|
+
const typingMinWidthMap: Record<ChatMessageSize, string> = {
|
|
5176
|
+
sm: 'min-w-12',
|
|
5177
|
+
default: 'min-w-12',
|
|
5178
|
+
lg: 'min-w-16',
|
|
5179
|
+
}
|
|
5180
|
+
|
|
5181
|
+
// ============================================================================
|
|
5182
|
+
// Props interfaces
|
|
5183
|
+
// ============================================================================
|
|
5184
|
+
|
|
5185
|
+
export interface ChatMessageRootProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
5186
|
+
/** Sender role \u2014 controls alignment (left=assistant, right=user) */
|
|
5187
|
+
role?: ChatMessageRole
|
|
5188
|
+
/** Bubble visual style */
|
|
5189
|
+
variant?: ChatMessageVariant
|
|
5190
|
+
/** Bubble background color */
|
|
5191
|
+
color?: ChatMessageColor
|
|
5192
|
+
/** Size scale \u2014 affects text and padding */
|
|
5193
|
+
size?: ChatMessageSize
|
|
5194
|
+
/** Bubble corner radius */
|
|
5195
|
+
radius?: ChatMessageRadius
|
|
5196
|
+
/** Asymmetric tail corner (radius=0 on the avatar side) */
|
|
5197
|
+
tail?: boolean
|
|
5198
|
+
/** Show typing indicator inside Content. Pass true for defaults, or TypingIndicatorProps for custom. */
|
|
5199
|
+
typing?: boolean | Omit<TypingIndicatorProps, 'className'>
|
|
5200
|
+
/** Avatar size \u2014 overrides default gap between avatar and bubble */
|
|
5201
|
+
avatarSize?: ChatMessageAvatarSize
|
|
5202
|
+
/** Action buttons revealed on hover */
|
|
5203
|
+
actions?: React.ReactNode
|
|
5204
|
+
}
|
|
5205
|
+
|
|
5206
|
+
export interface ChatMessageAvatarProps {
|
|
5207
|
+
/** Avatar size: sm(24px), md(28px), lg(32px) */
|
|
5208
|
+
size?: ChatMessageAvatarSize
|
|
5209
|
+
/** Image URL */
|
|
5210
|
+
src?: string
|
|
5211
|
+
/** Alt text for avatar image */
|
|
5212
|
+
alt?: string
|
|
5213
|
+
/** Initials to display when no image (1\u20132 characters) */
|
|
5214
|
+
initials?: string
|
|
5215
|
+
/** Custom icon or ReactNode to render inside the avatar */
|
|
5216
|
+
icon?: React.ReactNode
|
|
5217
|
+
/** Additional CSS classes */
|
|
5218
|
+
className?: string
|
|
5219
|
+
}
|
|
5220
|
+
|
|
5221
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
5222
|
+
export interface ChatMessageContentProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
5223
|
+
|
|
5224
|
+
export interface ChatMessageFooterProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
5225
|
+
/** Pre-formatted timestamp string (e.g. "12:34 PM") */
|
|
5226
|
+
timestamp?: string
|
|
5227
|
+
/** Status: built-in preset ('sending' | 'sent' | 'read' | 'error') or custom ReactNode */
|
|
5228
|
+
status?: React.ReactNode
|
|
5229
|
+
/** Size override (defaults to context value from ChatMessage root) */
|
|
5230
|
+
size?: ChatMessageSize
|
|
5231
|
+
}
|
|
5232
|
+
|
|
5233
|
+
// ============================================================================
|
|
5234
|
+
// ChatMessage Root
|
|
5235
|
+
// ============================================================================
|
|
5236
|
+
|
|
5237
|
+
const ChatMessageRoot = React.forwardRef<HTMLDivElement, ChatMessageRootProps>(
|
|
5238
|
+
(
|
|
5239
|
+
{
|
|
5240
|
+
className,
|
|
5241
|
+
role = 'assistant',
|
|
5242
|
+
variant = 'bubble',
|
|
5243
|
+
color = 'default',
|
|
5244
|
+
size = 'default',
|
|
5245
|
+
radius = '2xl',
|
|
5246
|
+
tail = true,
|
|
5247
|
+
typing = false,
|
|
5248
|
+
avatarSize,
|
|
5249
|
+
actions,
|
|
5250
|
+
children,
|
|
5251
|
+
...props
|
|
5252
|
+
},
|
|
5253
|
+
ref
|
|
5254
|
+
) => {
|
|
5255
|
+
const ctx = React.useMemo<ChatMessageContextValue>(
|
|
5256
|
+
() => ({ role, variant, color, size, radius, tail, typing, actions }),
|
|
5257
|
+
[role, variant, color, size, radius, tail, typing, actions]
|
|
5258
|
+
)
|
|
5259
|
+
|
|
5260
|
+
const gapClass = avatarSize ? avatarGapMap[avatarSize] : rowGapMap[size]
|
|
5261
|
+
|
|
5262
|
+
return (
|
|
5263
|
+
<ChatMessageContext.Provider value={ctx}>
|
|
5264
|
+
<div
|
|
5265
|
+
ref={ref}
|
|
5266
|
+
className={cn(
|
|
5267
|
+
'group/message flex items-end',
|
|
5268
|
+
gapClass,
|
|
5269
|
+
role === 'user' && variant !== 'flat' ? 'flex-row-reverse' : 'flex-row',
|
|
5270
|
+
className
|
|
5271
|
+
)}
|
|
5272
|
+
{...props}
|
|
5273
|
+
>
|
|
5274
|
+
{children}
|
|
5275
|
+
</div>
|
|
5276
|
+
</ChatMessageContext.Provider>
|
|
5277
|
+
)
|
|
5278
|
+
}
|
|
5279
|
+
)
|
|
5280
|
+
ChatMessageRoot.displayName = 'ChatMessage'
|
|
5281
|
+
|
|
5282
|
+
// ============================================================================
|
|
5283
|
+
// Default AI sparkle icon
|
|
5284
|
+
// ============================================================================
|
|
5285
|
+
|
|
5286
|
+
function DefaultAiIcon() {
|
|
5287
|
+
return (
|
|
5288
|
+
<svg viewBox="0 0 24 24" fill="currentColor" className="w-3/5 h-3/5" aria-hidden="true">
|
|
5289
|
+
<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" />
|
|
5290
|
+
<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" />
|
|
5291
|
+
<path d="M4.5 14.5l.7 2 2 .7-2 .7L4.5 20l-.7-2-2-.7 2-.7z" opacity=".4" />
|
|
5292
|
+
</svg>
|
|
5293
|
+
)
|
|
5294
|
+
}
|
|
5295
|
+
|
|
5296
|
+
// ============================================================================
|
|
5297
|
+
// ChatMessage.Avatar \u2014 wraps Avatar component with chat-specific sizes
|
|
5298
|
+
// ============================================================================
|
|
5299
|
+
|
|
5300
|
+
const ChatMessageAvatar = React.forwardRef<HTMLSpanElement, ChatMessageAvatarProps>(
|
|
5301
|
+
({ className, size: avatarSize = 'md', src, alt = 'Avatar', initials, icon }, ref) => (
|
|
5302
|
+
<Avatar
|
|
5303
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5304
|
+
ref={ref as any}
|
|
5305
|
+
aria-hidden="true"
|
|
5306
|
+
className={cn(chatAvatarSizeMap[avatarSize], 'shrink-0', className)}
|
|
5307
|
+
>
|
|
5308
|
+
{src && <AvatarImage src={src} alt={alt} />}
|
|
5309
|
+
<AvatarFallback className={cn('font-semibold', chatAvatarFontMap[avatarSize])}>
|
|
5310
|
+
{icon ?? (initials || <DefaultAiIcon />)}
|
|
5311
|
+
</AvatarFallback>
|
|
5312
|
+
</Avatar>
|
|
5313
|
+
)
|
|
5314
|
+
)
|
|
5315
|
+
ChatMessageAvatar.displayName = 'ChatMessage.Avatar'
|
|
5316
|
+
|
|
5317
|
+
// ============================================================================
|
|
5318
|
+
// ChatMessage.Content
|
|
5319
|
+
// ============================================================================
|
|
5320
|
+
|
|
5321
|
+
const ChatMessageContent = React.forwardRef<HTMLDivElement, ChatMessageContentProps>(
|
|
5322
|
+
({ className, children, ...props }, ref) => {
|
|
5323
|
+
const { role, variant, color, size, radius, tail, typing, actions } = React.useContext(ChatMessageContext)
|
|
5324
|
+
const isTyping = !!typing
|
|
5325
|
+
const typingProps = typeof typing === 'object' ? typing : {}
|
|
5326
|
+
|
|
5327
|
+
// Dark bubble backgrounds need inverted typing dot colors
|
|
5328
|
+
const isDarkBg = color === 'primary' || color === 'dark'
|
|
5329
|
+
const invertDots = variant === 'bubble' && isDarkBg
|
|
5330
|
+
|
|
5331
|
+
// flat variant never has a tail (asymmetric corner)
|
|
5332
|
+
const effectiveTail = variant === 'flat' ? false : tail
|
|
5333
|
+
const radiusClasses = effectiveTail
|
|
5334
|
+
? bubbleRadiusTailMap[radius][role]
|
|
5335
|
+
: bubbleRadiusSymMap[radius]
|
|
5336
|
+
|
|
5337
|
+
const bubble = (
|
|
5338
|
+
<div
|
|
5339
|
+
ref={ref}
|
|
5340
|
+
className={cn(
|
|
5341
|
+
bubbleTextMap[size],
|
|
5342
|
+
variant === 'bubble' && 'max-w-[75%]',
|
|
5343
|
+
isTyping && [typingMinWidthMap[size], 'text-center'],
|
|
5344
|
+
getBubbleColors(color, variant),
|
|
5345
|
+
(variant === 'bubble' || variant === 'flat') && bubblePaddingMap[size],
|
|
5346
|
+
(variant === 'bubble' || (variant === 'flat' && color === 'muted')) && radiusClasses,
|
|
5347
|
+
className
|
|
5348
|
+
)}
|
|
5349
|
+
{...props}
|
|
5350
|
+
>
|
|
5351
|
+
{isTyping ? (
|
|
5352
|
+
<TypingIndicator
|
|
5353
|
+
size={typingSizeMap[size]}
|
|
5354
|
+
color="muted"
|
|
5355
|
+
{...typingProps}
|
|
5356
|
+
className={cn('align-middle', invertDots && '[&>div>div]:bg-current')}
|
|
5357
|
+
/>
|
|
5358
|
+
) : children}
|
|
5359
|
+
</div>
|
|
5360
|
+
)
|
|
5361
|
+
|
|
5362
|
+
if (!actions) return bubble
|
|
5363
|
+
|
|
5364
|
+
return (
|
|
5365
|
+
<div className={cn('flex items-end gap-1.5', role === 'user' && variant !== 'flat' ? 'flex-row-reverse' : 'flex-row')}>
|
|
5366
|
+
{bubble}
|
|
5367
|
+
<div className={cn(
|
|
5368
|
+
'shrink-0 flex items-center gap-1 pb-0.5',
|
|
5369
|
+
'opacity-0 group-hover/message:opacity-100',
|
|
5370
|
+
'transition-opacity duration-normal',
|
|
5371
|
+
)}>
|
|
5372
|
+
{actions}
|
|
5373
|
+
</div>
|
|
5374
|
+
</div>
|
|
5375
|
+
)
|
|
5376
|
+
}
|
|
5377
|
+
)
|
|
5378
|
+
ChatMessageContent.displayName = 'ChatMessage.Content'
|
|
5379
|
+
|
|
5380
|
+
// ============================================================================
|
|
5381
|
+
// ChatMessage.Footer
|
|
5382
|
+
// ============================================================================
|
|
4380
5383
|
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
5384
|
+
const ChatMessageFooter = React.forwardRef<HTMLDivElement, ChatMessageFooterProps>(
|
|
5385
|
+
(
|
|
5386
|
+
{
|
|
5387
|
+
className,
|
|
5388
|
+
timestamp,
|
|
5389
|
+
status,
|
|
5390
|
+
role: roleProp,
|
|
5391
|
+
size: sizeProp,
|
|
5392
|
+
children,
|
|
5393
|
+
...props
|
|
5394
|
+
},
|
|
5395
|
+
ref
|
|
5396
|
+
) => {
|
|
5397
|
+
const ctx = React.useContext(ChatMessageContext)
|
|
5398
|
+
const role = roleProp ?? ctx.role
|
|
5399
|
+
const size = sizeProp ?? ctx.size
|
|
5400
|
+
const variant = ctx.variant
|
|
5401
|
+
|
|
5402
|
+
// Resolve status: built-in preset string \u2192 label, or pass-through ReactNode
|
|
5403
|
+
const statusContent = React.useMemo(() => {
|
|
5404
|
+
if (status == null) return null
|
|
5405
|
+
if (typeof status === 'string' && status in builtInStatusLabels) {
|
|
5406
|
+
const { label, className: cls } = builtInStatusLabels[status]
|
|
5407
|
+
return <span className={cls}>{label}</span>
|
|
5408
|
+
}
|
|
5409
|
+
return status
|
|
5410
|
+
}, [status])
|
|
5411
|
+
|
|
5412
|
+
return (
|
|
5413
|
+
<div
|
|
5414
|
+
ref={ref}
|
|
5415
|
+
className={cn(
|
|
5416
|
+
'flex items-center gap-1 select-none',
|
|
5417
|
+
footerTextMap[size],
|
|
5418
|
+
'text-text-subtle',
|
|
5419
|
+
role === 'user' && variant !== 'flat' ? 'justify-end' : 'justify-start',
|
|
5420
|
+
className
|
|
5421
|
+
)}
|
|
5422
|
+
{...props}
|
|
5423
|
+
>
|
|
5424
|
+
{children}
|
|
5425
|
+
{statusContent}
|
|
5426
|
+
{timestamp && <span>{timestamp}</span>}
|
|
5427
|
+
</div>
|
|
5428
|
+
)
|
|
5429
|
+
}
|
|
5430
|
+
)
|
|
5431
|
+
ChatMessageFooter.displayName = 'ChatMessage.Footer'
|
|
5432
|
+
|
|
5433
|
+
// ============================================================================
|
|
5434
|
+
// Namespace export (compound pattern)
|
|
5435
|
+
// ============================================================================
|
|
5436
|
+
|
|
5437
|
+
const ChatMessage = Object.assign(ChatMessageRoot, {
|
|
5438
|
+
Avatar: ChatMessageAvatar,
|
|
5439
|
+
Content: ChatMessageContent,
|
|
5440
|
+
Footer: ChatMessageFooter,
|
|
4394
5441
|
})
|
|
4395
5442
|
|
|
5443
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
5444
|
+
namespace ChatMessage {
|
|
5445
|
+
export type RootProps = ChatMessageRootProps
|
|
5446
|
+
export type AvatarProps = ChatMessageAvatarProps
|
|
5447
|
+
export type ContentProps = ChatMessageContentProps
|
|
5448
|
+
export type FooterProps = ChatMessageFooterProps
|
|
5449
|
+
export type Role = ChatMessageRole
|
|
5450
|
+
export type Variant = ChatMessageVariant
|
|
5451
|
+
export type Color = ChatMessageColor
|
|
5452
|
+
export type Size = ChatMessageSize
|
|
5453
|
+
export type Radius = ChatMessageRadius
|
|
5454
|
+
export type Status = ChatMessageStatus
|
|
5455
|
+
export type AvatarSize = ChatMessageAvatarSize
|
|
5456
|
+
}
|
|
5457
|
+
|
|
4396
5458
|
export {
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
ChartPie,
|
|
4403
|
-
ChartTooltip,
|
|
4404
|
-
ChartTooltipContent,
|
|
4405
|
-
ChartLegend,
|
|
4406
|
-
ChartLegendContent,
|
|
4407
|
-
ChartXAxis,
|
|
4408
|
-
ChartYAxis,
|
|
4409
|
-
ChartStyle,
|
|
4410
|
-
useChart,
|
|
5459
|
+
ChatMessage,
|
|
5460
|
+
ChatMessageRoot,
|
|
5461
|
+
ChatMessageAvatar,
|
|
5462
|
+
ChatMessageContent,
|
|
5463
|
+
ChatMessageFooter,
|
|
4411
5464
|
}
|
|
4412
5465
|
`,
|
|
4413
5466
|
type: "ui"
|
|
@@ -7743,8 +8796,9 @@ const PaginationRoot = React.forwardRef<HTMLElement, PaginationProps>(
|
|
|
7743
8796
|
color = 'default',
|
|
7744
8797
|
radius = 'md',
|
|
7745
8798
|
disabled = false,
|
|
7746
|
-
|
|
7747
|
-
|
|
8799
|
+
// Pre-declared API; conditional auto-rendering is tracked in COMPONENT-IMPROVEMENTS.md.
|
|
8800
|
+
withControls: _withControls = true,
|
|
8801
|
+
withEdges: _withEdges = false,
|
|
7748
8802
|
loop = false,
|
|
7749
8803
|
children,
|
|
7750
8804
|
...props
|
|
@@ -8452,105 +9506,359 @@ const Progress = React.forwardRef<
|
|
|
8452
9506
|
? formatLabel(clampedValue, max)
|
|
8453
9507
|
: \`\${Math.round(percentage)}%\`
|
|
8454
9508
|
|
|
8455
|
-
// \u2500\u2500 Circular \u2500\u2500
|
|
8456
|
-
if (type === 'circular') {
|
|
8457
|
-
const diameter = CIRCULAR_SIZES[resolvedSize]
|
|
8458
|
-
const strokeWidth = thickness ?? CIRCULAR_THICKNESS[resolvedSize]
|
|
8459
|
-
const r = (CIRCULAR_VIEWBOX - strokeWidth) / 2
|
|
8460
|
-
const circumference = 2 * Math.PI * r
|
|
8461
|
-
const offset = circumference * (1 - percentage / 100)
|
|
9509
|
+
// \u2500\u2500 Circular \u2500\u2500
|
|
9510
|
+
if (type === 'circular') {
|
|
9511
|
+
const diameter = CIRCULAR_SIZES[resolvedSize]
|
|
9512
|
+
const strokeWidth = thickness ?? CIRCULAR_THICKNESS[resolvedSize]
|
|
9513
|
+
const r = (CIRCULAR_VIEWBOX - strokeWidth) / 2
|
|
9514
|
+
const circumference = 2 * Math.PI * r
|
|
9515
|
+
const offset = circumference * (1 - percentage / 100)
|
|
9516
|
+
|
|
9517
|
+
return (
|
|
9518
|
+
<div
|
|
9519
|
+
className={cn('inline-flex items-center gap-2', className)}
|
|
9520
|
+
role="progressbar"
|
|
9521
|
+
aria-valuenow={clampedValue}
|
|
9522
|
+
aria-valuemin={0}
|
|
9523
|
+
aria-valuemax={max}
|
|
9524
|
+
aria-label={label}
|
|
9525
|
+
>
|
|
9526
|
+
<div className="relative" style={{ width: diameter, height: diameter }}>
|
|
9527
|
+
<svg
|
|
9528
|
+
viewBox={\`0 0 \${CIRCULAR_VIEWBOX} \${CIRCULAR_VIEWBOX}\`}
|
|
9529
|
+
className="transform -rotate-90"
|
|
9530
|
+
width={diameter}
|
|
9531
|
+
height={diameter}
|
|
9532
|
+
>
|
|
9533
|
+
{/* Track */}
|
|
9534
|
+
<circle
|
|
9535
|
+
cx={CIRCULAR_CENTER}
|
|
9536
|
+
cy={CIRCULAR_CENTER}
|
|
9537
|
+
r={r}
|
|
9538
|
+
fill="none"
|
|
9539
|
+
className="stroke-border"
|
|
9540
|
+
strokeWidth={strokeWidth}
|
|
9541
|
+
/>
|
|
9542
|
+
{/* Indicator */}
|
|
9543
|
+
<circle
|
|
9544
|
+
cx={CIRCULAR_CENTER}
|
|
9545
|
+
cy={CIRCULAR_CENTER}
|
|
9546
|
+
r={r}
|
|
9547
|
+
fill="none"
|
|
9548
|
+
className={cn(indicatorClassName || circularColorMap[color], 'transition-[stroke-dashoffset] duration-normal ease-out')}
|
|
9549
|
+
strokeWidth={strokeWidth}
|
|
9550
|
+
strokeDasharray={circumference}
|
|
9551
|
+
strokeDashoffset={offset}
|
|
9552
|
+
strokeLinecap="round"
|
|
9553
|
+
/>
|
|
9554
|
+
</svg>
|
|
9555
|
+
{/* Center value */}
|
|
9556
|
+
{showValue && resolvedSize !== 'sm' && (
|
|
9557
|
+
<div className={cn(
|
|
9558
|
+
'absolute inset-0 flex items-center justify-center font-semibold font-mono tabular-nums text-foreground',
|
|
9559
|
+
CIRCULAR_FONT_SIZES[resolvedSize]
|
|
9560
|
+
)}>
|
|
9561
|
+
{valueLabel}
|
|
9562
|
+
</div>
|
|
9563
|
+
)}
|
|
9564
|
+
</div>
|
|
9565
|
+
</div>
|
|
9566
|
+
)
|
|
9567
|
+
}
|
|
9568
|
+
|
|
9569
|
+
// \u2500\u2500 Linear \u2500\u2500
|
|
9570
|
+
const stripedBg = variant === 'striped'
|
|
9571
|
+
? '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)]'
|
|
9572
|
+
: ''
|
|
9573
|
+
|
|
9574
|
+
return (
|
|
9575
|
+
<div className={cn('flex items-center gap-3', showValue && 'w-full', className)}>
|
|
9576
|
+
<ProgressPrimitive.Root
|
|
9577
|
+
ref={ref}
|
|
9578
|
+
className={cn(linearTrackVariants({ size }))}
|
|
9579
|
+
value={clampedValue}
|
|
9580
|
+
max={max}
|
|
9581
|
+
aria-label={label}
|
|
9582
|
+
{...props}
|
|
9583
|
+
>
|
|
9584
|
+
<ProgressPrimitive.Indicator
|
|
9585
|
+
className={cn(
|
|
9586
|
+
'h-full transition-[width] duration-normal ease-out rounded-full',
|
|
9587
|
+
indicatorClassName || colorMap[color],
|
|
9588
|
+
stripedBg,
|
|
9589
|
+
animated && variant === 'striped' && 'animate-progress-stripe',
|
|
9590
|
+
)}
|
|
9591
|
+
style={{ width: \`\${percentage}%\` }}
|
|
9592
|
+
/>
|
|
9593
|
+
</ProgressPrimitive.Root>
|
|
9594
|
+
{showValue && (
|
|
9595
|
+
<span className={cn(
|
|
9596
|
+
'shrink-0 font-semibold font-mono tabular-nums text-foreground',
|
|
9597
|
+
valueFontSizes[resolvedSize]
|
|
9598
|
+
)}>
|
|
9599
|
+
{valueLabel}
|
|
9600
|
+
</span>
|
|
9601
|
+
)}
|
|
9602
|
+
</div>
|
|
9603
|
+
)
|
|
9604
|
+
})
|
|
9605
|
+
Progress.displayName = 'Progress'
|
|
9606
|
+
|
|
9607
|
+
export { Progress, linearTrackVariants }
|
|
9608
|
+
`,
|
|
9609
|
+
type: "ui"
|
|
9610
|
+
}]
|
|
9611
|
+
},
|
|
9612
|
+
"quick-reply": {
|
|
9613
|
+
name: "quick-reply",
|
|
9614
|
+
dependencies: ["@radix-ui/react-slot"],
|
|
9615
|
+
registryDependencies: [],
|
|
9616
|
+
reverseDependencies: [],
|
|
9617
|
+
namespace: true,
|
|
9618
|
+
description: "",
|
|
9619
|
+
files: [{
|
|
9620
|
+
path: "quick-reply.tsx",
|
|
9621
|
+
content: `'use client'
|
|
9622
|
+
|
|
9623
|
+
import * as React from 'react'
|
|
9624
|
+
import { Slot } from '@radix-ui/react-slot'
|
|
9625
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
9626
|
+
import { cn } from '@/lib/utils'
|
|
9627
|
+
|
|
9628
|
+
// ============================================================================
|
|
9629
|
+
// Color mapping per variant \xD7 color
|
|
9630
|
+
// ============================================================================
|
|
9631
|
+
|
|
9632
|
+
const colorMap = {
|
|
9633
|
+
default: {
|
|
9634
|
+
outline: 'border border-border text-foreground bg-background hover:bg-background-muted',
|
|
9635
|
+
filled: 'bg-background-muted text-foreground hover:bg-background-elevated',
|
|
9636
|
+
ghost: 'text-foreground hover:bg-background-muted',
|
|
9637
|
+
},
|
|
9638
|
+
primary: {
|
|
9639
|
+
outline: 'border border-primary text-text-primary bg-background hover:bg-primary-tint',
|
|
9640
|
+
filled: 'bg-primary-tint text-text-primary hover:bg-primary/20',
|
|
9641
|
+
ghost: 'text-text-primary hover:bg-primary-tint',
|
|
9642
|
+
},
|
|
9643
|
+
} as const
|
|
9644
|
+
|
|
9645
|
+
// ============================================================================
|
|
9646
|
+
// Context \u2014 propagates variant/color/size/radius from Root to Items
|
|
9647
|
+
// ============================================================================
|
|
9648
|
+
|
|
9649
|
+
type QuickReplyContextValue = {
|
|
9650
|
+
size: QuickReplySize
|
|
9651
|
+
variant: QuickReplyVariant
|
|
9652
|
+
color: QuickReplyColor
|
|
9653
|
+
radius: QuickReplyRadius
|
|
9654
|
+
}
|
|
9655
|
+
|
|
9656
|
+
const QuickReplyContext = React.createContext<QuickReplyContextValue>({
|
|
9657
|
+
size: 'default',
|
|
9658
|
+
variant: 'outline',
|
|
9659
|
+
color: 'default',
|
|
9660
|
+
radius: 'full',
|
|
9661
|
+
})
|
|
9662
|
+
|
|
9663
|
+
// ============================================================================
|
|
9664
|
+
// Variants (CVA)
|
|
9665
|
+
// ============================================================================
|
|
9666
|
+
|
|
9667
|
+
const quickReplyRootVariants = cva(
|
|
9668
|
+
'flex items-center',
|
|
9669
|
+
{
|
|
9670
|
+
variants: {
|
|
9671
|
+
layout: {
|
|
9672
|
+
scroll: 'overflow-x-auto',
|
|
9673
|
+
wrap: 'flex-wrap',
|
|
9674
|
+
},
|
|
9675
|
+
gap: {
|
|
9676
|
+
sm: 'gap-1.5',
|
|
9677
|
+
default: 'gap-2',
|
|
9678
|
+
lg: 'gap-2.5',
|
|
9679
|
+
},
|
|
9680
|
+
},
|
|
9681
|
+
defaultVariants: {
|
|
9682
|
+
layout: 'scroll',
|
|
9683
|
+
gap: 'default',
|
|
9684
|
+
},
|
|
9685
|
+
}
|
|
9686
|
+
)
|
|
9687
|
+
|
|
9688
|
+
const quickReplyItemVariants = cva(
|
|
9689
|
+
[
|
|
9690
|
+
'inline-flex items-center justify-center',
|
|
9691
|
+
'whitespace-nowrap font-semibold shrink-0',
|
|
9692
|
+
'transition-colors duration-fast cursor-pointer',
|
|
9693
|
+
'focus-visible:outline-none focus-visible:focus-ring',
|
|
9694
|
+
'disabled:pointer-events-none disabled:opacity-50',
|
|
9695
|
+
'select-none',
|
|
9696
|
+
].join(' '),
|
|
9697
|
+
{
|
|
9698
|
+
variants: {
|
|
9699
|
+
size: {
|
|
9700
|
+
sm: 'h-7 px-2.5 text-xs gap-1.5', // 28px
|
|
9701
|
+
default: 'h-8 px-3 text-sm gap-1.5', // 32px
|
|
9702
|
+
lg: 'h-9 px-3.5 text-md gap-1.5', // 36px
|
|
9703
|
+
},
|
|
9704
|
+
radius: {
|
|
9705
|
+
md: 'rounded-md', // 6px
|
|
9706
|
+
lg: 'rounded-lg', // 8px
|
|
9707
|
+
full: 'rounded-full', // 9999px
|
|
9708
|
+
},
|
|
9709
|
+
},
|
|
9710
|
+
defaultVariants: {
|
|
9711
|
+
size: 'default',
|
|
9712
|
+
radius: 'full',
|
|
9713
|
+
},
|
|
9714
|
+
}
|
|
9715
|
+
)
|
|
9716
|
+
|
|
9717
|
+
// ============================================================================
|
|
9718
|
+
// Icon sizes per item size
|
|
9719
|
+
// ============================================================================
|
|
9720
|
+
|
|
9721
|
+
const iconSizeMap = {
|
|
9722
|
+
sm: 'icon-xs', // 14px
|
|
9723
|
+
default: 'icon-xs', // 14px
|
|
9724
|
+
lg: 'icon-sm', // 16px
|
|
9725
|
+
} as const
|
|
9726
|
+
|
|
9727
|
+
// ============================================================================
|
|
9728
|
+
// Types
|
|
9729
|
+
// ============================================================================
|
|
9730
|
+
|
|
9731
|
+
export type QuickReplyLayout = 'scroll' | 'wrap'
|
|
9732
|
+
export type QuickReplyVariant = 'outline' | 'filled' | 'ghost'
|
|
9733
|
+
export type QuickReplyColor = 'default' | 'primary'
|
|
9734
|
+
export type QuickReplySize = 'sm' | 'default' | 'lg'
|
|
9735
|
+
export type QuickReplyRadius = 'md' | 'lg' | 'full'
|
|
9736
|
+
export type QuickReplyGap = 'sm' | 'default' | 'lg'
|
|
9737
|
+
|
|
9738
|
+
export interface QuickReplyRootProps
|
|
9739
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
9740
|
+
VariantProps<typeof quickReplyRootVariants> {
|
|
9741
|
+
/** Layout mode: horizontal scroll or wrap to multiple rows */
|
|
9742
|
+
layout?: QuickReplyLayout
|
|
9743
|
+
/** Visual style applied to all child Items */
|
|
9744
|
+
variant?: QuickReplyVariant
|
|
9745
|
+
/** Color theme applied to all child Items */
|
|
9746
|
+
color?: QuickReplyColor
|
|
9747
|
+
/** Chip size applied to all child Items */
|
|
9748
|
+
size?: QuickReplySize
|
|
9749
|
+
/** Border radius applied to all child Items */
|
|
9750
|
+
radius?: QuickReplyRadius
|
|
9751
|
+
/** Gap between Items */
|
|
9752
|
+
gap?: QuickReplyGap
|
|
9753
|
+
}
|
|
9754
|
+
|
|
9755
|
+
export interface QuickReplyItemProps
|
|
9756
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
9757
|
+
/** Leading icon (SVG or ReactNode) */
|
|
9758
|
+
icon?: React.ReactNode
|
|
9759
|
+
/** Render as child element (Slot pattern) */
|
|
9760
|
+
asChild?: boolean
|
|
9761
|
+
}
|
|
9762
|
+
|
|
9763
|
+
// ============================================================================
|
|
9764
|
+
// QuickReply Root \u2014 group container that propagates style via Context
|
|
9765
|
+
// ============================================================================
|
|
9766
|
+
|
|
9767
|
+
const QuickReplyRoot = React.forwardRef<HTMLDivElement, QuickReplyRootProps>(
|
|
9768
|
+
(
|
|
9769
|
+
{
|
|
9770
|
+
className,
|
|
9771
|
+
layout = 'scroll',
|
|
9772
|
+
gap = 'default',
|
|
9773
|
+
variant = 'outline',
|
|
9774
|
+
color = 'default',
|
|
9775
|
+
size = 'default',
|
|
9776
|
+
radius = 'full',
|
|
9777
|
+
role = 'group',
|
|
9778
|
+
children,
|
|
9779
|
+
...props
|
|
9780
|
+
},
|
|
9781
|
+
ref
|
|
9782
|
+
) => {
|
|
9783
|
+
const contextValue = React.useMemo<QuickReplyContextValue>(
|
|
9784
|
+
() => ({ size, variant, color, radius }),
|
|
9785
|
+
[size, variant, color, radius]
|
|
9786
|
+
)
|
|
9787
|
+
|
|
9788
|
+
return (
|
|
9789
|
+
<QuickReplyContext.Provider value={contextValue}>
|
|
9790
|
+
<div
|
|
9791
|
+
ref={ref}
|
|
9792
|
+
role={role}
|
|
9793
|
+
className={cn(quickReplyRootVariants({ layout, gap }), className)}
|
|
9794
|
+
{...props}
|
|
9795
|
+
>
|
|
9796
|
+
{children}
|
|
9797
|
+
</div>
|
|
9798
|
+
</QuickReplyContext.Provider>
|
|
9799
|
+
)
|
|
9800
|
+
}
|
|
9801
|
+
)
|
|
9802
|
+
QuickReplyRoot.displayName = 'QuickReply'
|
|
9803
|
+
|
|
9804
|
+
// ============================================================================
|
|
9805
|
+
// QuickReply Item \u2014 individual chip button
|
|
9806
|
+
// ============================================================================
|
|
9807
|
+
|
|
9808
|
+
const QuickReplyItem = React.forwardRef<HTMLButtonElement, QuickReplyItemProps>(
|
|
9809
|
+
({ className, icon, asChild = false, children, ...props }, ref) => {
|
|
9810
|
+
const { size, variant, color, radius } = React.useContext(QuickReplyContext)
|
|
9811
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9812
|
+
const Comp = asChild ? (Slot as any) : 'button'
|
|
9813
|
+
const colorClasses = colorMap[color][variant]
|
|
8462
9814
|
|
|
8463
9815
|
return (
|
|
8464
|
-
<
|
|
8465
|
-
|
|
8466
|
-
|
|
8467
|
-
|
|
8468
|
-
|
|
8469
|
-
|
|
8470
|
-
|
|
9816
|
+
<Comp
|
|
9817
|
+
ref={ref}
|
|
9818
|
+
type={!asChild ? 'button' : undefined}
|
|
9819
|
+
{...props}
|
|
9820
|
+
className={cn(
|
|
9821
|
+
quickReplyItemVariants({ size, radius }),
|
|
9822
|
+
colorClasses,
|
|
9823
|
+
className
|
|
9824
|
+
)}
|
|
8471
9825
|
>
|
|
8472
|
-
|
|
8473
|
-
<
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
width={diameter}
|
|
8477
|
-
height={diameter}
|
|
9826
|
+
{icon && (
|
|
9827
|
+
<span
|
|
9828
|
+
className={cn('shrink-0 [&>svg]:w-full [&>svg]:h-full', iconSizeMap[size])}
|
|
9829
|
+
aria-hidden="true"
|
|
8478
9830
|
>
|
|
8479
|
-
{
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
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
|
-
)}
|
|
8510
|
-
</div>
|
|
8511
|
-
</div>
|
|
9831
|
+
{icon}
|
|
9832
|
+
</span>
|
|
9833
|
+
)}
|
|
9834
|
+
{children}
|
|
9835
|
+
</Comp>
|
|
8512
9836
|
)
|
|
8513
9837
|
}
|
|
9838
|
+
)
|
|
9839
|
+
QuickReplyItem.displayName = 'QuickReply.Item'
|
|
8514
9840
|
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
: ''
|
|
9841
|
+
// ============================================================================
|
|
9842
|
+
// Namespace export (compound pattern)
|
|
9843
|
+
// ============================================================================
|
|
8519
9844
|
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
<ProgressPrimitive.Root
|
|
8523
|
-
ref={ref}
|
|
8524
|
-
className={cn(linearTrackVariants({ size }))}
|
|
8525
|
-
value={clampedValue}
|
|
8526
|
-
max={max}
|
|
8527
|
-
aria-label={label}
|
|
8528
|
-
{...props}
|
|
8529
|
-
>
|
|
8530
|
-
<ProgressPrimitive.Indicator
|
|
8531
|
-
className={cn(
|
|
8532
|
-
'h-full transition-[width] duration-normal ease-out rounded-full',
|
|
8533
|
-
indicatorClassName || colorMap[color],
|
|
8534
|
-
stripedBg,
|
|
8535
|
-
animated && variant === 'striped' && 'animate-progress-stripe',
|
|
8536
|
-
)}
|
|
8537
|
-
style={{ width: \`\${percentage}%\` }}
|
|
8538
|
-
/>
|
|
8539
|
-
</ProgressPrimitive.Root>
|
|
8540
|
-
{showValue && (
|
|
8541
|
-
<span className={cn(
|
|
8542
|
-
'shrink-0 font-semibold font-mono tabular-nums text-foreground',
|
|
8543
|
-
valueFontSizes[resolvedSize]
|
|
8544
|
-
)}>
|
|
8545
|
-
{valueLabel}
|
|
8546
|
-
</span>
|
|
8547
|
-
)}
|
|
8548
|
-
</div>
|
|
8549
|
-
)
|
|
9845
|
+
const QuickReply = Object.assign(QuickReplyRoot, {
|
|
9846
|
+
Item: QuickReplyItem,
|
|
8550
9847
|
})
|
|
8551
|
-
Progress.displayName = 'Progress'
|
|
8552
9848
|
|
|
8553
|
-
|
|
9849
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
9850
|
+
namespace QuickReply {
|
|
9851
|
+
export type RootProps = QuickReplyRootProps
|
|
9852
|
+
export type ItemProps = QuickReplyItemProps
|
|
9853
|
+
}
|
|
9854
|
+
|
|
9855
|
+
export {
|
|
9856
|
+
QuickReply,
|
|
9857
|
+
QuickReplyRoot,
|
|
9858
|
+
QuickReplyItem,
|
|
9859
|
+
quickReplyRootVariants,
|
|
9860
|
+
quickReplyItemVariants,
|
|
9861
|
+
}
|
|
8554
9862
|
`,
|
|
8555
9863
|
type: "ui"
|
|
8556
9864
|
}]
|
|
@@ -11737,7 +13045,6 @@ const ToastItem = React.memo(function ToastItem({
|
|
|
11737
13045
|
}: ToastItemProps) {
|
|
11738
13046
|
const [isExiting, setIsExiting] = React.useState(false)
|
|
11739
13047
|
const [isEntered, setIsEntered] = React.useState(false)
|
|
11740
|
-
const [isPaused, setIsPaused] = React.useState(false) // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
11741
13048
|
const timerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
11742
13049
|
const remainingRef = React.useRef<number>(0)
|
|
11743
13050
|
const startTimeRef = React.useRef<number>(0)
|
|
@@ -11804,10 +13111,9 @@ const ToastItem = React.memo(function ToastItem({
|
|
|
11804
13111
|
}
|
|
11805
13112
|
}, [duration, handleAutoClose])
|
|
11806
13113
|
|
|
11807
|
-
// Pause/resume on hover
|
|
13114
|
+
// Pause/resume on hover (ref-only \u2014 no state update needed, avoids unnecessary re-renders)
|
|
11808
13115
|
const handleMouseEnter = React.useCallback(() => {
|
|
11809
13116
|
if (duration <= 0) return
|
|
11810
|
-
setIsPaused(true)
|
|
11811
13117
|
if (timerRef.current) {
|
|
11812
13118
|
clearTimeout(timerRef.current)
|
|
11813
13119
|
remainingRef.current -= Date.now() - startTimeRef.current
|
|
@@ -11816,7 +13122,6 @@ const ToastItem = React.memo(function ToastItem({
|
|
|
11816
13122
|
|
|
11817
13123
|
const handleMouseLeave = React.useCallback(() => {
|
|
11818
13124
|
if (duration <= 0) return
|
|
11819
|
-
setIsPaused(false)
|
|
11820
13125
|
startTimeRef.current = Date.now()
|
|
11821
13126
|
timerRef.current = setTimeout(handleAutoClose, Math.max(remainingRef.current, TOAST_MIN_RESUME_MS))
|
|
11822
13127
|
}, [duration, handleAutoClose])
|
|
@@ -12585,6 +13890,208 @@ export {
|
|
|
12585
13890
|
TooltipProvider,
|
|
12586
13891
|
tooltipContentVariants,
|
|
12587
13892
|
}
|
|
13893
|
+
`,
|
|
13894
|
+
type: "ui"
|
|
13895
|
+
}]
|
|
13896
|
+
},
|
|
13897
|
+
"typing-indicator": {
|
|
13898
|
+
name: "typing-indicator",
|
|
13899
|
+
dependencies: [],
|
|
13900
|
+
registryDependencies: [],
|
|
13901
|
+
reverseDependencies: [],
|
|
13902
|
+
namespace: false,
|
|
13903
|
+
description: "",
|
|
13904
|
+
files: [{
|
|
13905
|
+
path: "typing-indicator.tsx",
|
|
13906
|
+
content: `'use client'
|
|
13907
|
+
|
|
13908
|
+
import * as React from 'react'
|
|
13909
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
13910
|
+
import { cn } from '@/lib/utils'
|
|
13911
|
+
|
|
13912
|
+
// ============================================================================
|
|
13913
|
+
// Constants
|
|
13914
|
+
// ============================================================================
|
|
13915
|
+
|
|
13916
|
+
/** Dot diameter per size (px) */
|
|
13917
|
+
const DOT_SIZES = { sm: 4, default: 6, lg: 8 } as const
|
|
13918
|
+
|
|
13919
|
+
/** Gap between dots per size (px) */
|
|
13920
|
+
const DOT_GAP = { sm: 3, default: 4, lg: 5 } as const
|
|
13921
|
+
|
|
13922
|
+
/** Animation speed in ms */
|
|
13923
|
+
const SPEED_MS = { slow: 1600, default: 1100, fast: 700 } as const
|
|
13924
|
+
|
|
13925
|
+
/** Number of dots */
|
|
13926
|
+
const DOT_COUNT = 3
|
|
13927
|
+
|
|
13928
|
+
// ============================================================================
|
|
13929
|
+
// Color mapping
|
|
13930
|
+
// ============================================================================
|
|
13931
|
+
|
|
13932
|
+
const bgColorMap = {
|
|
13933
|
+
default: 'bg-foreground',
|
|
13934
|
+
primary: 'bg-primary',
|
|
13935
|
+
muted: 'bg-text-subtle',
|
|
13936
|
+
} as const
|
|
13937
|
+
|
|
13938
|
+
// ============================================================================
|
|
13939
|
+
// Variants (CVA)
|
|
13940
|
+
// ============================================================================
|
|
13941
|
+
|
|
13942
|
+
const typingIndicatorVariants = cva(
|
|
13943
|
+
'inline-flex items-center',
|
|
13944
|
+
{
|
|
13945
|
+
variants: {
|
|
13946
|
+
size: {
|
|
13947
|
+
sm: 'gap-1.5 text-xs',
|
|
13948
|
+
default: 'gap-2 text-sm',
|
|
13949
|
+
lg: 'gap-2.5 text-base',
|
|
13950
|
+
},
|
|
13951
|
+
},
|
|
13952
|
+
defaultVariants: {
|
|
13953
|
+
size: 'default',
|
|
13954
|
+
},
|
|
13955
|
+
}
|
|
13956
|
+
)
|
|
13957
|
+
|
|
13958
|
+
// ============================================================================
|
|
13959
|
+
// Types
|
|
13960
|
+
// ============================================================================
|
|
13961
|
+
|
|
13962
|
+
export interface TypingIndicatorProps
|
|
13963
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
13964
|
+
VariantProps<typeof typingIndicatorVariants> {
|
|
13965
|
+
/** Animation style */
|
|
13966
|
+
variant?: 'dots' | 'cursor'
|
|
13967
|
+
/** Indicator color */
|
|
13968
|
+
color?: 'default' | 'primary' | 'muted'
|
|
13969
|
+
/** Animation speed */
|
|
13970
|
+
speed?: 'slow' | 'default' | 'fast'
|
|
13971
|
+
/** Accessible label (also shown as visual text when showLabel is true) */
|
|
13972
|
+
label?: string
|
|
13973
|
+
/** Render label text alongside the indicator */
|
|
13974
|
+
showLabel?: boolean
|
|
13975
|
+
}
|
|
13976
|
+
|
|
13977
|
+
// ============================================================================
|
|
13978
|
+
// Component
|
|
13979
|
+
// ============================================================================
|
|
13980
|
+
|
|
13981
|
+
const TypingIndicator = React.forwardRef<HTMLDivElement, TypingIndicatorProps>(
|
|
13982
|
+
(
|
|
13983
|
+
{
|
|
13984
|
+
className,
|
|
13985
|
+
variant = 'dots',
|
|
13986
|
+
size,
|
|
13987
|
+
color = 'muted',
|
|
13988
|
+
speed = 'default',
|
|
13989
|
+
label = 'Typing',
|
|
13990
|
+
showLabel = false,
|
|
13991
|
+
...props
|
|
13992
|
+
},
|
|
13993
|
+
ref
|
|
13994
|
+
) => {
|
|
13995
|
+
const resolvedSize = size ?? 'default'
|
|
13996
|
+
|
|
13997
|
+
return (
|
|
13998
|
+
<div
|
|
13999
|
+
ref={ref}
|
|
14000
|
+
role="status"
|
|
14001
|
+
aria-live="polite"
|
|
14002
|
+
aria-label={label}
|
|
14003
|
+
className={cn(typingIndicatorVariants({ size }), className)}
|
|
14004
|
+
{...props}
|
|
14005
|
+
>
|
|
14006
|
+
{variant === 'dots' && (
|
|
14007
|
+
<DotsIndicator size={resolvedSize} color={color} speed={speed} />
|
|
14008
|
+
)}
|
|
14009
|
+
{variant === 'cursor' && (
|
|
14010
|
+
<CursorIndicator size={resolvedSize} color={color} />
|
|
14011
|
+
)}
|
|
14012
|
+
{showLabel && (
|
|
14013
|
+
<span className="text-text-muted select-none">{label}</span>
|
|
14014
|
+
)}
|
|
14015
|
+
</div>
|
|
14016
|
+
)
|
|
14017
|
+
}
|
|
14018
|
+
)
|
|
14019
|
+
TypingIndicator.displayName = 'TypingIndicator'
|
|
14020
|
+
|
|
14021
|
+
// ============================================================================
|
|
14022
|
+
// Dots Indicator \u2014 3 staggered bouncing dots (reuses animate-spinner-dot)
|
|
14023
|
+
// ============================================================================
|
|
14024
|
+
|
|
14025
|
+
function DotsIndicator({
|
|
14026
|
+
size,
|
|
14027
|
+
color,
|
|
14028
|
+
speed,
|
|
14029
|
+
}: {
|
|
14030
|
+
size: 'sm' | 'default' | 'lg'
|
|
14031
|
+
color: 'default' | 'primary' | 'muted'
|
|
14032
|
+
speed: 'slow' | 'default' | 'fast'
|
|
14033
|
+
}) {
|
|
14034
|
+
const dotSize = DOT_SIZES[size]
|
|
14035
|
+
const gap = DOT_GAP[size]
|
|
14036
|
+
const duration = SPEED_MS[speed]
|
|
14037
|
+
|
|
14038
|
+
return (
|
|
14039
|
+
<div className="inline-flex items-center" style={{ gap }}>
|
|
14040
|
+
{Array.from({ length: DOT_COUNT }, (_, i) => (
|
|
14041
|
+
<div
|
|
14042
|
+
key={i}
|
|
14043
|
+
className={cn(
|
|
14044
|
+
'rounded-full animate-spinner-dot motion-reduce:animate-none motion-reduce:opacity-60',
|
|
14045
|
+
bgColorMap[color]
|
|
14046
|
+
)}
|
|
14047
|
+
style={{
|
|
14048
|
+
width: dotSize,
|
|
14049
|
+
height: dotSize,
|
|
14050
|
+
animationDuration: \`\${duration}ms\`,
|
|
14051
|
+
animationDelay: \`\${i * (duration / DOT_COUNT / 1.5)}ms\`,
|
|
14052
|
+
}}
|
|
14053
|
+
/>
|
|
14054
|
+
))}
|
|
14055
|
+
</div>
|
|
14056
|
+
)
|
|
14057
|
+
}
|
|
14058
|
+
|
|
14059
|
+
// ============================================================================
|
|
14060
|
+
// Cursor sizes per component size
|
|
14061
|
+
// ============================================================================
|
|
14062
|
+
|
|
14063
|
+
const CURSOR_SIZE = {
|
|
14064
|
+
sm: { width: 2, height: 14 },
|
|
14065
|
+
default: { width: 2.5, height: 20 },
|
|
14066
|
+
lg: { width: 3, height: 26 },
|
|
14067
|
+
} as const
|
|
14068
|
+
|
|
14069
|
+
// ============================================================================
|
|
14070
|
+
// Cursor Indicator \u2014 blinking cursor (text-cursor style)
|
|
14071
|
+
// ============================================================================
|
|
14072
|
+
|
|
14073
|
+
function CursorIndicator({
|
|
14074
|
+
size,
|
|
14075
|
+
color,
|
|
14076
|
+
}: {
|
|
14077
|
+
size: 'sm' | 'default' | 'lg'
|
|
14078
|
+
color: 'default' | 'primary' | 'muted'
|
|
14079
|
+
}) {
|
|
14080
|
+
const { width, height } = CURSOR_SIZE[size]
|
|
14081
|
+
|
|
14082
|
+
return (
|
|
14083
|
+
<div
|
|
14084
|
+
aria-hidden="true"
|
|
14085
|
+
className={cn(
|
|
14086
|
+
'rounded-full animate-typing-cursor motion-reduce:animate-none motion-reduce:opacity-60',
|
|
14087
|
+
bgColorMap[color]
|
|
14088
|
+
)}
|
|
14089
|
+
style={{ width, height, animationDuration: '1200ms' }}
|
|
14090
|
+
/>
|
|
14091
|
+
)
|
|
14092
|
+
}
|
|
14093
|
+
|
|
14094
|
+
export { TypingIndicator, typingIndicatorVariants }
|
|
12588
14095
|
`,
|
|
12589
14096
|
type: "ui"
|
|
12590
14097
|
}]
|
|
@@ -12611,6 +14118,8 @@ var INSTALLABLE_COMPONENTS = [
|
|
|
12611
14118
|
"button",
|
|
12612
14119
|
"card",
|
|
12613
14120
|
"chart",
|
|
14121
|
+
"chat-input",
|
|
14122
|
+
"chat-message",
|
|
12614
14123
|
"checkbox",
|
|
12615
14124
|
"divider",
|
|
12616
14125
|
"drawer",
|
|
@@ -12623,6 +14132,7 @@ var INSTALLABLE_COMPONENTS = [
|
|
|
12623
14132
|
"pagination",
|
|
12624
14133
|
"popover",
|
|
12625
14134
|
"progress",
|
|
14135
|
+
"quick-reply",
|
|
12626
14136
|
"radio-group",
|
|
12627
14137
|
"segmented",
|
|
12628
14138
|
"select",
|
|
@@ -12636,7 +14146,8 @@ var INSTALLABLE_COMPONENTS = [
|
|
|
12636
14146
|
"toast",
|
|
12637
14147
|
"toggle-group",
|
|
12638
14148
|
"toggle",
|
|
12639
|
-
"tooltip"
|
|
14149
|
+
"tooltip",
|
|
14150
|
+
"typing-indicator"
|
|
12640
14151
|
];
|
|
12641
14152
|
|
|
12642
14153
|
// cli/src/commands/add.ts
|
|
@@ -12758,6 +14269,42 @@ async function add(args) {
|
|
|
12758
14269
|
addedCount++;
|
|
12759
14270
|
logger.success(`${entry.exists ? "Overwrote" : "Added"} ${entry.fileName}`);
|
|
12760
14271
|
}
|
|
14272
|
+
if (config.tailwind.version === 4 && addedCount > 0) {
|
|
14273
|
+
try {
|
|
14274
|
+
const cssFullPath = import_node_path5.default.join(projectRoot, config.tailwind.css);
|
|
14275
|
+
if (import_node_fs5.default.existsSync(cssFullPath)) {
|
|
14276
|
+
const cssContent = import_node_fs5.default.readFileSync(cssFullPath, "utf-8");
|
|
14277
|
+
const cssDir = import_node_path5.default.dirname(cssFullPath);
|
|
14278
|
+
const relRaw = import_node_path5.default.relative(cssDir, componentsDir).replace(/\\/g, "/");
|
|
14279
|
+
const relSource = relRaw.startsWith(".") ? relRaw : `./${relRaw}`;
|
|
14280
|
+
const sourcePattern = new RegExp(`@source\\s+["']${relSource.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']`);
|
|
14281
|
+
if (!sourcePattern.test(cssContent)) {
|
|
14282
|
+
const marker = "@7onic-ui/tokens";
|
|
14283
|
+
const lastIdx = cssContent.lastIndexOf(marker);
|
|
14284
|
+
if (lastIdx !== -1) {
|
|
14285
|
+
const lineEnd = cssContent.indexOf("\n", lastIdx);
|
|
14286
|
+
const insertAt = lineEnd !== -1 ? lineEnd + 1 : cssContent.length;
|
|
14287
|
+
const updated = cssContent.slice(0, insertAt) + `@source "${relSource}";
|
|
14288
|
+
` + cssContent.slice(insertAt);
|
|
14289
|
+
import_node_fs5.default.writeFileSync(cssFullPath, updated, "utf-8");
|
|
14290
|
+
O2.success(`Added @source "${relSource}" to ${config.tailwind.css}`);
|
|
14291
|
+
} else {
|
|
14292
|
+
O2.warn(
|
|
14293
|
+
`Could not find @7onic-ui/tokens import in ${config.tailwind.css}.
|
|
14294
|
+
Run ${import_picocolors3.default.cyan("npx 7onic init")} first, or manually add to your CSS:
|
|
14295
|
+
|
|
14296
|
+
@source "${relSource}";`
|
|
14297
|
+
);
|
|
14298
|
+
}
|
|
14299
|
+
}
|
|
14300
|
+
}
|
|
14301
|
+
} catch (err) {
|
|
14302
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
14303
|
+
} else {
|
|
14304
|
+
O2.warn(`Failed to inject @source into ${config.tailwind.css}: ${err}`);
|
|
14305
|
+
}
|
|
14306
|
+
}
|
|
14307
|
+
}
|
|
12761
14308
|
if (depsToInstall.length > 0) {
|
|
12762
14309
|
const pm = detectPackageManager(projectRoot);
|
|
12763
14310
|
const s = fe();
|
|
@@ -12871,7 +14418,7 @@ function showSetupHints(names) {
|
|
|
12871
14418
|
|
|
12872
14419
|
// cli/src/index.ts
|
|
12873
14420
|
var import_picocolors4 = __toESM(require_picocolors());
|
|
12874
|
-
var VERSION = "0.1.
|
|
14421
|
+
var VERSION = "0.1.5";
|
|
12875
14422
|
var HELP = `
|
|
12876
14423
|
${import_picocolors4.default.bold("7onic")} \u2014 Add 7onic design system components to your project
|
|
12877
14424
|
|