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.
Files changed (3) hide show
  1. package/dist/index.js +1697 -150
  2. package/llms.txt +3 -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 '@7onic-ui/tokens/css/variables.css';
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
- try {
1285
- const tsConfigRaw = import_node_fs4.default.readFileSync(import_node_path4.default.join(projectRoot, "tsconfig.json"), "utf-8");
1286
- hasPathAlias = tsConfigRaw.includes('"@/');
1287
- } catch {
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
- "No @/ path alias detected in tsconfig.json.\n Components use @/lib/utils \u2014 make sure your project has path aliases configured.\n Next.js: automatic. Vite: add resolve.alias in vite.config.ts"
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
- import_node_fs4.default.writeFileSync(cssFullPath, cssImports + "\n" + cssContent, "utf-8");
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-3 text-sm',
1834
- default: 'gap-2.5 p-4 text-md',
1835
- lg: 'gap-3 p-5 text-md',
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 leading-4 tracking-tight',
1865
- default: 'font-semibold leading-[18px] tracking-tight',
1866
- lg: 'text-base font-semibold leading-5 tracking-tight',
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-1',
1872
- lg: 'text-md mt-1.5',
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="shrink-0">
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 leading-none tracking-tight',
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
- // \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
4382
- const Chart = Object.assign(ChartContainer, {
4383
- Bar: ChartBar,
4384
- Line: ChartLine,
4385
- Area: ChartArea,
4386
- Pie: ChartPie,
4387
- Tooltip: ChartTooltip,
4388
- TooltipContent: ChartTooltipContent,
4389
- Legend: ChartLegend,
4390
- LegendContent: ChartLegendContent,
4391
- XAxis: ChartXAxis,
4392
- YAxis: ChartYAxis,
4393
- Style: ChartStyle,
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
- Chart,
4398
- ChartContainer,
4399
- ChartBar,
4400
- ChartLine,
4401
- ChartArea,
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
- withControls = true, // eslint-disable-line @typescript-eslint/no-unused-vars
7747
- withEdges = false, // eslint-disable-line @typescript-eslint/no-unused-vars
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
- <div
8465
- className={cn('inline-flex items-center gap-2', className)}
8466
- role="progressbar"
8467
- aria-valuenow={clampedValue}
8468
- aria-valuemin={0}
8469
- aria-valuemax={max}
8470
- aria-label={label}
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
- <div className="relative" style={{ width: diameter, height: diameter }}>
8473
- <svg
8474
- viewBox={\`0 0 \${CIRCULAR_VIEWBOX} \${CIRCULAR_VIEWBOX}\`}
8475
- className="transform -rotate-90"
8476
- width={diameter}
8477
- height={diameter}
9826
+ {icon && (
9827
+ <span
9828
+ className={cn('shrink-0 [&>svg]:w-full [&>svg]:h-full', iconSizeMap[size])}
9829
+ aria-hidden="true"
8478
9830
  >
8479
- {/* Track */}
8480
- <circle
8481
- cx={CIRCULAR_CENTER}
8482
- cy={CIRCULAR_CENTER}
8483
- r={r}
8484
- fill="none"
8485
- className="stroke-border"
8486
- strokeWidth={strokeWidth}
8487
- />
8488
- {/* Indicator */}
8489
- <circle
8490
- cx={CIRCULAR_CENTER}
8491
- cy={CIRCULAR_CENTER}
8492
- r={r}
8493
- fill="none"
8494
- className={cn(indicatorClassName || circularColorMap[color], 'transition-[stroke-dashoffset] duration-normal ease-out')}
8495
- strokeWidth={strokeWidth}
8496
- strokeDasharray={circumference}
8497
- strokeDashoffset={offset}
8498
- strokeLinecap="round"
8499
- />
8500
- </svg>
8501
- {/* Center value */}
8502
- {showValue && resolvedSize !== 'sm' && (
8503
- <div className={cn(
8504
- 'absolute inset-0 flex items-center justify-center font-semibold font-mono tabular-nums text-foreground',
8505
- CIRCULAR_FONT_SIZES[resolvedSize]
8506
- )}>
8507
- {valueLabel}
8508
- </div>
8509
- )}
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
- // \u2500\u2500 Linear \u2500\u2500
8516
- const stripedBg = variant === 'striped'
8517
- ? '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)]'
8518
- : ''
9841
+ // ============================================================================
9842
+ // Namespace export (compound pattern)
9843
+ // ============================================================================
8519
9844
 
8520
- return (
8521
- <div className={cn('flex items-center gap-3', showValue && 'w-full', className)}>
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
- export { Progress, linearTrackVariants }
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.3";
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