7onic 0.1.2 → 0.1.4

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