@ekanos/sdk 0.15.1 → 0.17.0

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.
@@ -27,7 +27,7 @@ export function BaseMarketplaceTile(props) {
27
27
  if (target.nextSibling) {
28
28
  target.nextSibling.style.display = 'flex';
29
29
  }
30
- } }), _jsx("div", { className: "bg-border ring-border/50 text-headline hidden h-full w-full items-center justify-center rounded-lg ring-1", children: displayBrandName.charAt(0).toUpperCase() })] })) : (_jsx("div", { className: "bg-border ring-border/50 text-headline flex h-16 w-16 items-center justify-center rounded-lg border-2 ring-1", children: displayBrandName.charAt(0).toUpperCase() })) }), _jsx(CardTitle, { className: "text-subhead-lg line-clamp-2 leading-tight", children: displayTitle }), _jsx(If, { condition: categories.length > 0, children: _jsxs("div", { className: "mt-2 flex flex-wrap justify-center gap-1", children: [categories.slice(0, 2).map((cat) => (_jsx(CategoryChip, { productColor: productColor, children: cat.name }, cat.id))), _jsx(If, { condition: categories.length > 2, children: _jsxs(CategoryChip, { productColor: productColor, children: ["+", categories.length - 2] }) })] }) })] }), _jsx(CardContent, { className: "flex-1 pb-8 text-center", children: _jsx(If, { condition: displaySummary, children: _jsx(CardDescription, { className: "text-muted-foreground text-body line-clamp-2 leading-relaxed", children: displaySummary }) }) }), _jsx(If, { condition: displayBrandName !== displayTitle, children: _jsx("p", { className: "text-muted-foreground/70 text-meta absolute right-3 bottom-3", children: displayBrandName }) }), _jsx(If, { condition: !!ctaLink, children: _jsx("div", { className: "text-muted-foreground/60 group-hover:text-foreground absolute bottom-3 left-3 transition-colors", "aria-hidden": "true", children: _jsx(Icon, { name: "fa-light fa-arrow-up-right-from-square", className: "h-4 w-4" }) }) })] }));
30
+ } }), _jsx("div", { className: "bg-border ring-border/50 heading-headline hidden h-full w-full items-center justify-center rounded-lg ring-1", children: displayBrandName.charAt(0).toUpperCase() })] })) : (_jsx("div", { className: "bg-border ring-border/50 heading-headline flex h-16 w-16 items-center justify-center rounded-lg border-2 ring-1", children: displayBrandName.charAt(0).toUpperCase() })) }), _jsx(CardTitle, { className: "heading-subhead-lg line-clamp-2", children: displayTitle }), _jsx(If, { condition: categories.length > 0, children: _jsxs("div", { className: "mt-2 flex flex-wrap justify-center gap-1", children: [categories.slice(0, 2).map((cat) => (_jsx(CategoryChip, { productColor: productColor, children: cat.name }, cat.id))), _jsx(If, { condition: categories.length > 2, children: _jsxs(CategoryChip, { productColor: productColor, children: ["+", categories.length - 2] }) })] }) })] }), _jsx(CardContent, { className: "flex-1 pb-8 text-center", children: _jsx(If, { condition: displaySummary, children: _jsx(CardDescription, { className: "text-muted-foreground text-body line-clamp-2 leading-relaxed", children: displaySummary }) }) }), _jsx(If, { condition: displayBrandName !== displayTitle, children: _jsx("p", { className: "text-muted-foreground/70 text-meta absolute right-3 bottom-3", children: displayBrandName }) }), _jsx(If, { condition: !!ctaLink, children: _jsx("div", { className: "text-muted-foreground/60 group-hover:text-foreground absolute bottom-3 left-3 transition-colors", "aria-hidden": "true", children: _jsx(Icon, { name: "fa-light fa-arrow-up-right-from-square", className: "h-4 w-4" }) }) })] }));
31
31
  // External link: opens in new tab
32
32
  if (ctaLink) {
33
33
  return (_jsx("a", { href: ctaLink, target: "_blank", rel: "noopener noreferrer", className: "block h-full", "aria-label": `${displayTitle} (opens in new tab)`, children: cardContent }));
@@ -46,9 +46,7 @@ export function BaseMarketplaceTile(props) {
46
46
  * its colour). The tint is that colour at 40% alpha.
47
47
  */
48
48
  function CategoryChip({ productColor, children, }) {
49
- return (_jsx("span", { className: cn(
50
- // oxlint-disable-next-line design/no-raw-text-size -- category chip at 12px/500 (text-xs font-medium); text-fine is 12px but bakes weight 400, a weight change
51
- 'text-secondary-foreground inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full px-2 py-0.5 text-xs font-medium whitespace-nowrap', productColor ? 'bg-(--product-color)/40' : 'bg-secondary'), style: productColor
49
+ return (_jsx("span", { className: cn('text-secondary-foreground text-fine inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full px-2 py-0.5 font-medium whitespace-nowrap', productColor ? 'bg-(--product-color)/40' : 'bg-secondary'), style: productColor
52
50
  ? { '--product-color': productColor }
53
51
  : undefined, children: children }));
54
52
  }
@@ -1 +1 @@
1
- {"version":3,"file":"base-marketplace-tile.js","sourceRoot":"","sources":["../../src/components/base-marketplace-tile.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport type { CSSProperties, ReactNode } from 'react';\n\n// `next/link.js`, not `next/link`. `next` ships no `exports` map, so the\n// extensionless subpath only resolves under a bundler — Node's own ESM loader\n// throws ERR_MODULE_NOT_FOUND, and any partner running vitest or plain Node\n// against the published tarball loses this whole module. The `.js` form is not\n// a bypass of Next's resolution: `create-compiler-aliases.js` keys its aliases\n// on the RESOLVED FILE PATH (`<next>/link.js`, with the comment \"Handle fully\n// specified imports like `next/image.js`\"), so an App Router build still gets\n// `next/dist/client/app-dir/link`, exactly as `next/link` would. Next's dist\n// also ends with `Object.assign(exports.default, exports); module.exports =\n// exports.default`, so Node's CJS-default interop and a bundler's `__esModule`\n// interop land on the same component object. Do not \"tidy\" the extension away.\nimport Link from 'next/link.js';\n\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@ekanos/ui/card';\nimport { Icon } from '@ekanos/ui/icon';\nimport { If } from '@ekanos/ui/if';\nimport { cn } from '@ekanos/ui/utils';\n\nimport type { MarketplaceTileProps } from '../types/integration';\n\n// Type for product category\nexport interface ProductCategory {\n id: string;\n name: string;\n slug: string;\n description?: string;\n}\n\nexport type BaseMarketplaceTileProps = MarketplaceTileProps & {\n /** The display name of the product */\n name: string;\n /** Full description of the product (used for detail pages) */\n description: string;\n /** URL-friendly identifier for the product */\n slug: string;\n /** Short summary text displayed on the tile (1-2 sentences) */\n summary?: string;\n /** Name of the company/brand that provides this product */\n brandName?: string;\n /** Path to the product logo image */\n logoPath?: string;\n /** Text for the call-to-action button */\n ctaText?: string;\n /** External URL for the CTA (opens in new tab when set) */\n ctaLink?: string;\n\n /** Array of categories this product belongs to */\n categories?: ProductCategory[];\n\n /** Hex color from product schema for tile theming */\n productColor?: string;\n /** Additional CSS classes to apply to the tile */\n className?: string;\n\n /** Internal navigation URL for the integration detail page */\n href?: string;\n};\n\nexport function BaseMarketplaceTile(props: BaseMarketplaceTileProps) {\n const {\n name,\n summary,\n brandName,\n logoPath,\n categories = [],\n productColor,\n className,\n href,\n ctaLink,\n } = props;\n\n const displayTitle = name || 'Unknown Product';\n const displaySummary = summary;\n const displayBrandName = brandName || displayTitle;\n\n const cardContent = (\n <Card\n className={cn(\n 'group bg-card relative flex h-full w-full cursor-pointer flex-col overflow-hidden border-0 pt-3 pb-2 shadow-sm transition-all duration-300 hover:shadow-md',\n className,\n )}\n >\n {/* Header: Centered Logo, Title, Categories */}\n <CardHeader className=\"flex flex-col items-center pt-6 pb-3 text-center\">\n {/* Logo */}\n <div className=\"mb-3\">\n {logoPath ? (\n <div className=\"flex h-16 w-16 items-center justify-center\">\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img\n src={logoPath}\n alt={`${displayBrandName} logo`}\n width={64}\n height={64}\n className=\"max-h-full max-w-full object-contain\"\n onError={(e) => {\n const target = e.target as HTMLImageElement;\n target.style.display = 'none';\n if (target.nextSibling) {\n (target.nextSibling as HTMLElement).style.display = 'flex';\n }\n }}\n />\n <div className=\"bg-border ring-border/50 text-headline hidden h-full w-full items-center justify-center rounded-lg ring-1\">\n {displayBrandName.charAt(0).toUpperCase()}\n </div>\n </div>\n ) : (\n <div className=\"bg-border ring-border/50 text-headline flex h-16 w-16 items-center justify-center rounded-lg border-2 ring-1\">\n {displayBrandName.charAt(0).toUpperCase()}\n </div>\n )}\n </div>\n\n {/* Title */}\n <CardTitle className=\"text-subhead-lg line-clamp-2 leading-tight\">\n {displayTitle}\n </CardTitle>\n\n {/* Category Badges - Under Title */}\n <If condition={categories.length > 0}>\n <div className=\"mt-2 flex flex-wrap justify-center gap-1\">\n {categories.slice(0, 2).map((cat) => (\n <CategoryChip key={cat.id} productColor={productColor}>\n {cat.name}\n </CategoryChip>\n ))}\n <If condition={categories.length > 2}>\n <CategoryChip productColor={productColor}>\n +{categories.length - 2}\n </CategoryChip>\n </If>\n </div>\n </If>\n </CardHeader>\n\n {/* Content: Summary */}\n <CardContent className=\"flex-1 pb-8 text-center\">\n <If condition={displaySummary}>\n <CardDescription className=\"text-muted-foreground text-body line-clamp-2 leading-relaxed\">\n {displaySummary}\n </CardDescription>\n </If>\n </CardContent>\n\n {/* Brand Name - Bottom Right */}\n <If condition={displayBrandName !== displayTitle}>\n <p className=\"text-muted-foreground/70 text-meta absolute right-3 bottom-3\">\n {displayBrandName}\n </p>\n </If>\n\n {/* External Link Indicator */}\n <If condition={!!ctaLink}>\n <div\n className=\"text-muted-foreground/60 group-hover:text-foreground absolute bottom-3 left-3 transition-colors\"\n aria-hidden=\"true\"\n >\n <Icon\n name=\"fa-light fa-arrow-up-right-from-square\"\n className=\"h-4 w-4\"\n />\n </div>\n </If>\n </Card>\n );\n\n // External link: opens in new tab\n if (ctaLink) {\n return (\n <a\n href={ctaLink}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"block h-full\"\n aria-label={`${displayTitle} (opens in new tab)`}\n >\n {cardContent}\n </a>\n );\n }\n\n // Internal link: uses Next.js Link\n if (href) {\n return (\n <Link href={href} className=\"block h-full\">\n {cardContent}\n </Link>\n );\n }\n\n // Fallback: render without Link (for backwards compatibility)\n return cardContent;\n}\n\n/**\n * A category chip, tinted with the product's own colour when it has one. The\n * colour is a partner's product colour from the product schema, not a theme\n * token — a data colour, so it is a chip rather than a `Badge` (a Badge owns\n * its colour). The tint is that colour at 40% alpha.\n */\nfunction CategoryChip({\n productColor,\n children,\n}: {\n productColor?: string;\n children: ReactNode;\n}) {\n return (\n <span\n className={cn(\n // oxlint-disable-next-line design/no-raw-text-size -- category chip at 12px/500 (text-xs font-medium); text-fine is 12px but bakes weight 400, a weight change\n 'text-secondary-foreground inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full px-2 py-0.5 text-xs font-medium whitespace-nowrap',\n productColor ? 'bg-(--product-color)/40' : 'bg-secondary',\n )}\n style={\n productColor\n ? ({ '--product-color': productColor } as CSSProperties)\n : undefined\n }\n >\n {children}\n </span>\n );\n}\n"]}
1
+ {"version":3,"file":"base-marketplace-tile.js","sourceRoot":"","sources":["../../src/components/base-marketplace-tile.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport type { CSSProperties, ReactNode } from 'react';\n\n// `next/link.js`, not `next/link`. `next` ships no `exports` map, so the\n// extensionless subpath only resolves under a bundler — Node's own ESM loader\n// throws ERR_MODULE_NOT_FOUND, and any partner running vitest or plain Node\n// against the published tarball loses this whole module. The `.js` form is not\n// a bypass of Next's resolution: `create-compiler-aliases.js` keys its aliases\n// on the RESOLVED FILE PATH (`<next>/link.js`, with the comment \"Handle fully\n// specified imports like `next/image.js`\"), so an App Router build still gets\n// `next/dist/client/app-dir/link`, exactly as `next/link` would. Next's dist\n// also ends with `Object.assign(exports.default, exports); module.exports =\n// exports.default`, so Node's CJS-default interop and a bundler's `__esModule`\n// interop land on the same component object. Do not \"tidy\" the extension away.\nimport Link from 'next/link.js';\n\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@ekanos/ui/card';\nimport { Icon } from '@ekanos/ui/icon';\nimport { If } from '@ekanos/ui/if';\nimport { cn } from '@ekanos/ui/utils';\n\nimport type { MarketplaceTileProps } from '../types/integration';\n\n// Type for product category\nexport interface ProductCategory {\n id: string;\n name: string;\n slug: string;\n description?: string;\n}\n\nexport type BaseMarketplaceTileProps = MarketplaceTileProps & {\n /** The display name of the product */\n name: string;\n /** Full description of the product (used for detail pages) */\n description: string;\n /** URL-friendly identifier for the product */\n slug: string;\n /** Short summary text displayed on the tile (1-2 sentences) */\n summary?: string;\n /** Name of the company/brand that provides this product */\n brandName?: string;\n /** Path to the product logo image */\n logoPath?: string;\n /** Text for the call-to-action button */\n ctaText?: string;\n /** External URL for the CTA (opens in new tab when set) */\n ctaLink?: string;\n\n /** Array of categories this product belongs to */\n categories?: ProductCategory[];\n\n /** Hex color from product schema for tile theming */\n productColor?: string;\n /** Additional CSS classes to apply to the tile */\n className?: string;\n\n /** Internal navigation URL for the integration detail page */\n href?: string;\n};\n\nexport function BaseMarketplaceTile(props: BaseMarketplaceTileProps) {\n const {\n name,\n summary,\n brandName,\n logoPath,\n categories = [],\n productColor,\n className,\n href,\n ctaLink,\n } = props;\n\n const displayTitle = name || 'Unknown Product';\n const displaySummary = summary;\n const displayBrandName = brandName || displayTitle;\n\n const cardContent = (\n <Card\n className={cn(\n 'group bg-card relative flex h-full w-full cursor-pointer flex-col overflow-hidden border-0 pt-3 pb-2 shadow-sm transition-all duration-300 hover:shadow-md',\n className,\n )}\n >\n {/* Header: Centered Logo, Title, Categories */}\n <CardHeader className=\"flex flex-col items-center pt-6 pb-3 text-center\">\n {/* Logo */}\n <div className=\"mb-3\">\n {logoPath ? (\n <div className=\"flex h-16 w-16 items-center justify-center\">\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img\n src={logoPath}\n alt={`${displayBrandName} logo`}\n width={64}\n height={64}\n className=\"max-h-full max-w-full object-contain\"\n onError={(e) => {\n const target = e.target as HTMLImageElement;\n target.style.display = 'none';\n if (target.nextSibling) {\n (target.nextSibling as HTMLElement).style.display = 'flex';\n }\n }}\n />\n <div className=\"bg-border ring-border/50 heading-headline hidden h-full w-full items-center justify-center rounded-lg ring-1\">\n {displayBrandName.charAt(0).toUpperCase()}\n </div>\n </div>\n ) : (\n <div className=\"bg-border ring-border/50 heading-headline flex h-16 w-16 items-center justify-center rounded-lg border-2 ring-1\">\n {displayBrandName.charAt(0).toUpperCase()}\n </div>\n )}\n </div>\n\n {/* Title */}\n <CardTitle className=\"heading-subhead-lg line-clamp-2\">\n {displayTitle}\n </CardTitle>\n\n {/* Category Badges - Under Title */}\n <If condition={categories.length > 0}>\n <div className=\"mt-2 flex flex-wrap justify-center gap-1\">\n {categories.slice(0, 2).map((cat) => (\n <CategoryChip key={cat.id} productColor={productColor}>\n {cat.name}\n </CategoryChip>\n ))}\n <If condition={categories.length > 2}>\n <CategoryChip productColor={productColor}>\n +{categories.length - 2}\n </CategoryChip>\n </If>\n </div>\n </If>\n </CardHeader>\n\n {/* Content: Summary */}\n <CardContent className=\"flex-1 pb-8 text-center\">\n <If condition={displaySummary}>\n <CardDescription className=\"text-muted-foreground text-body line-clamp-2 leading-relaxed\">\n {displaySummary}\n </CardDescription>\n </If>\n </CardContent>\n\n {/* Brand Name - Bottom Right */}\n <If condition={displayBrandName !== displayTitle}>\n <p className=\"text-muted-foreground/70 text-meta absolute right-3 bottom-3\">\n {displayBrandName}\n </p>\n </If>\n\n {/* External Link Indicator */}\n <If condition={!!ctaLink}>\n <div\n className=\"text-muted-foreground/60 group-hover:text-foreground absolute bottom-3 left-3 transition-colors\"\n aria-hidden=\"true\"\n >\n <Icon\n name=\"fa-light fa-arrow-up-right-from-square\"\n className=\"h-4 w-4\"\n />\n </div>\n </If>\n </Card>\n );\n\n // External link: opens in new tab\n if (ctaLink) {\n return (\n <a\n href={ctaLink}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"block h-full\"\n aria-label={`${displayTitle} (opens in new tab)`}\n >\n {cardContent}\n </a>\n );\n }\n\n // Internal link: uses Next.js Link\n if (href) {\n return (\n <Link href={href} className=\"block h-full\">\n {cardContent}\n </Link>\n );\n }\n\n // Fallback: render without Link (for backwards compatibility)\n return cardContent;\n}\n\n/**\n * A category chip, tinted with the product's own colour when it has one. The\n * colour is a partner's product colour from the product schema, not a theme\n * token — a data colour, so it is a chip rather than a `Badge` (a Badge owns\n * its colour). The tint is that colour at 40% alpha.\n */\nfunction CategoryChip({\n productColor,\n children,\n}: {\n productColor?: string;\n children: ReactNode;\n}) {\n return (\n <span\n className={cn(\n 'text-secondary-foreground text-fine inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full px-2 py-0.5 font-medium whitespace-nowrap',\n productColor ? 'bg-(--product-color)/40' : 'bg-secondary',\n )}\n style={\n productColor\n ? ({ '--product-color': productColor } as CSSProperties)\n : undefined\n }\n >\n {children}\n </span>\n );\n}\n"]}
@@ -27,14 +27,12 @@ export function AiPromptChip({ label, prompt, context, sourceWidget, title, onAf
27
27
  onAfterAsk === null || onAfterAsk === void 0 ? void 0 : onAfterAsk();
28
28
  };
29
29
  if (variant === 'header') {
30
- return (_jsx(Button, { variant: "ghost", size: "sm", className: cn('h-8 w-8 p-0', className), onClick: handleClick, "aria-label": `Ask AI: ${label}`, children: _jsx(Icon, { name: "fa-light fa-sparkles", className: "h-4 w-4" }) }));
30
+ return (_jsx(Button, { variant: "ghost", size: "sm", className: cn('h-8 w-8 p-0', className), onClick: handleClick, "aria-label": `Ask AI: ${label}`, children: _jsx(Icon, { name: "fa-light fa-wand-magic-sparkles", className: "h-4 w-4" }) }));
31
31
  }
32
32
  if (variant === 'footer') {
33
- return (_jsxs("button", { type: "button", onClick: handleClick, className: cn('flex w-full items-center gap-2 rounded-lg border border-dashed px-4 py-3', 'border-primary/20 bg-primary/5 text-primary-strong text-label', 'hover:bg-primary/10 hover:border-primary/40 transition-colors', 'focus-visible:ring-ring/60 focus-visible:ring-2 focus-visible:outline-none', className), "aria-label": label, children: [_jsx(Icon, { name: "fa-light fa-sparkles", className: "h-4 w-4 shrink-0" }), _jsx("span", { className: "text-left", children: label })] }));
33
+ return (_jsxs(Button, { type: "button", variant: "outline", onClick: handleClick, className: cn('h-auto w-full justify-start rounded-lg px-4 py-3 whitespace-normal', className), "aria-label": label, children: [_jsx(Icon, { name: "fa-light fa-wand-magic-sparkles", className: "text-primary-strong h-4 w-4 shrink-0" }), _jsx("span", { className: "text-left", children: label })] }));
34
34
  }
35
35
  // inline (Type B)
36
- return (_jsxs("button", { type: "button", onClick: handleClick, className: cn('inline-flex items-center gap-1.5 rounded-full px-3 py-1.5',
37
- // oxlint-disable-next-line design/no-raw-text-size -- AI suggestion chip at 12px/500 (text-xs font-medium); text-fine is 12px but bakes weight 400, a weight change. Repeated in widget-ask-bar.tsx's suggestion chip.
38
- 'border-primary/20 bg-primary/5 text-primary-strong border text-xs font-medium', 'hover:bg-primary/10 hover:border-primary/40 transition-colors', 'focus-visible:ring-ring/60 focus-visible:ring-2 focus-visible:outline-none', className), "aria-label": label, children: [_jsx(Icon, { name: "fa-light fa-sparkles", className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { children: label })] }));
36
+ return (_jsxs(Button, { type: "button", variant: "outline", size: "xs", onClick: handleClick, className: cn('rounded-full px-3', className), "aria-label": label, children: [_jsx(Icon, { name: "fa-light fa-wand-magic-sparkles", className: "text-primary-strong h-3.5 w-3.5 shrink-0" }), _jsx("span", { children: label })] }));
39
37
  }
40
38
  //# sourceMappingURL=ai-prompt-chip.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ai-prompt-chip.js","sourceRoot":"","sources":["../../../src/components/widgets/ai-prompt-chip.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvC,OAAO,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAEtC,OAAO,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAqBpE;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAAC,EAC3B,KAAK,EACL,MAAM,EACN,OAAO,EACP,YAAY,EACZ,KAAK,EACL,UAAU,EACV,OAAO,GAAG,QAAQ,EAClB,SAAS,GACS;IAClB,MAAM,YAAY,GAAG,eAAe,EAAE,CAAC;IAEvC,MAAM,WAAW,GAAG,GAAG,EAAE;QACvB,YAAY,CAAC;YACX,QAAQ,EAAE,YAAY;YACtB,KAAK,EAAE,KAAK,aAAL,KAAK,cAAL,KAAK,GAAI,KAAK;YACrB,QAAQ,EAAE,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,EAAE;YACvB,eAAe,EAAE,MAAM;SACxB,CAAC,CAAC;QACH,UAAU,aAAV,UAAU,uBAAV,UAAU,EAAI,CAAC;IACjB,CAAC,CAAC;IAEF,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;QACzB,OAAO,CACL,KAAC,MAAM,IACL,OAAO,EAAC,OAAO,EACf,IAAI,EAAC,IAAI,EACT,SAAS,EAAE,EAAE,CAAC,aAAa,EAAE,SAAS,CAAC,EACvC,OAAO,EAAE,WAAW,gBACR,WAAW,KAAK,EAAE,YAE9B,KAAC,IAAI,IAAC,IAAI,EAAC,sBAAsB,EAAC,SAAS,EAAC,SAAS,GAAG,GACjD,CACV,CAAC;IACJ,CAAC;IAED,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;QACzB,OAAO,CACL,kBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,EAAE,CACX,0EAA0E,EAC1E,+DAA+D,EAC/D,+DAA+D,EAC/D,4EAA4E,EAC5E,SAAS,CACV,gBACW,KAAK,aAEjB,KAAC,IAAI,IAAC,IAAI,EAAC,sBAAsB,EAAC,SAAS,EAAC,kBAAkB,GAAG,EACjE,eAAM,SAAS,EAAC,WAAW,YAAE,KAAK,GAAQ,IACnC,CACV,CAAC;IACJ,CAAC;IAED,kBAAkB;IAClB,OAAO,CACL,kBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,EAAE,CACX,2DAA2D;QAC3D,uNAAuN;QACvN,+EAA+E,EAC/E,+DAA+D,EAC/D,4EAA4E,EAC5E,SAAS,CACV,gBACW,KAAK,aAEjB,KAAC,IAAI,IAAC,IAAI,EAAC,sBAAsB,EAAC,SAAS,EAAC,sBAAsB,GAAG,EACrE,yBAAO,KAAK,GAAQ,IACb,CACV,CAAC;AACJ,CAAC","sourcesContent":["'use client';\n\nimport { Button } from '@ekanos/ui/button';\nimport { Icon } from '@ekanos/ui/icon';\nimport { cn } from '@ekanos/ui/utils';\n\nimport { useAskAssistant } from '../../hooks/ask-assistant-context';\n\ntype AiPromptChipProps = {\n label: string;\n prompt: string;\n context?: Record<string, unknown>;\n sourceWidget: string;\n /**\n * Human-readable label for the dock's ArrivalChip (\"Asking about …\").\n * Falls back to the chip's display label when omitted.\n */\n title?: string;\n /**\n * Fires after the assistant has been invoked. Drawers pass their close\n * handler here so the dock takes over the screen on click.\n */\n onAfterAsk?: () => void;\n variant?: 'header' | 'inline' | 'footer';\n className?: string;\n};\n\n/**\n * AI handoff chip that opens the assistant dock pre-seeded with this\n * widget/drawer's snapshot. Routes through `useAskAssistant` so the dock\n * receives the standard `{widgetId, title, snapshot, suggestedPrompt}`\n * shape used by all AI handoffs in the app.\n *\n * Variants:\n * - header (Type A): Compact icon button for widget headers\n * - inline (Type B): Chip with sparkle icon + text, below data sections\n * - footer (Type C): Full-width chip in drawer footers\n */\nexport function AiPromptChip({\n label,\n prompt,\n context,\n sourceWidget,\n title,\n onAfterAsk,\n variant = 'inline',\n className,\n}: AiPromptChipProps) {\n const askAssistant = useAskAssistant();\n\n const handleClick = () => {\n askAssistant({\n widgetId: sourceWidget,\n title: title ?? label,\n snapshot: context ?? {},\n suggestedPrompt: prompt,\n });\n onAfterAsk?.();\n };\n\n if (variant === 'header') {\n return (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className={cn('h-8 w-8 p-0', className)}\n onClick={handleClick}\n aria-label={`Ask AI: ${label}`}\n >\n <Icon name=\"fa-light fa-sparkles\" className=\"h-4 w-4\" />\n </Button>\n );\n }\n\n if (variant === 'footer') {\n return (\n <button\n type=\"button\"\n onClick={handleClick}\n className={cn(\n 'flex w-full items-center gap-2 rounded-lg border border-dashed px-4 py-3',\n 'border-primary/20 bg-primary/5 text-primary-strong text-label',\n 'hover:bg-primary/10 hover:border-primary/40 transition-colors',\n 'focus-visible:ring-ring/60 focus-visible:ring-2 focus-visible:outline-none',\n className,\n )}\n aria-label={label}\n >\n <Icon name=\"fa-light fa-sparkles\" className=\"h-4 w-4 shrink-0\" />\n <span className=\"text-left\">{label}</span>\n </button>\n );\n }\n\n // inline (Type B)\n return (\n <button\n type=\"button\"\n onClick={handleClick}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-3 py-1.5',\n // oxlint-disable-next-line design/no-raw-text-size -- AI suggestion chip at 12px/500 (text-xs font-medium); text-fine is 12px but bakes weight 400, a weight change. Repeated in widget-ask-bar.tsx's suggestion chip.\n 'border-primary/20 bg-primary/5 text-primary-strong border text-xs font-medium',\n 'hover:bg-primary/10 hover:border-primary/40 transition-colors',\n 'focus-visible:ring-ring/60 focus-visible:ring-2 focus-visible:outline-none',\n className,\n )}\n aria-label={label}\n >\n <Icon name=\"fa-light fa-sparkles\" className=\"h-3.5 w-3.5 shrink-0\" />\n <span>{label}</span>\n </button>\n );\n}\n"]}
1
+ {"version":3,"file":"ai-prompt-chip.js","sourceRoot":"","sources":["../../../src/components/widgets/ai-prompt-chip.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvC,OAAO,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAEtC,OAAO,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAqBpE;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAAC,EAC3B,KAAK,EACL,MAAM,EACN,OAAO,EACP,YAAY,EACZ,KAAK,EACL,UAAU,EACV,OAAO,GAAG,QAAQ,EAClB,SAAS,GACS;IAClB,MAAM,YAAY,GAAG,eAAe,EAAE,CAAC;IAEvC,MAAM,WAAW,GAAG,GAAG,EAAE;QACvB,YAAY,CAAC;YACX,QAAQ,EAAE,YAAY;YACtB,KAAK,EAAE,KAAK,aAAL,KAAK,cAAL,KAAK,GAAI,KAAK;YACrB,QAAQ,EAAE,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,EAAE;YACvB,eAAe,EAAE,MAAM;SACxB,CAAC,CAAC;QACH,UAAU,aAAV,UAAU,uBAAV,UAAU,EAAI,CAAC;IACjB,CAAC,CAAC;IAEF,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;QACzB,OAAO,CACL,KAAC,MAAM,IACL,OAAO,EAAC,OAAO,EACf,IAAI,EAAC,IAAI,EACT,SAAS,EAAE,EAAE,CAAC,aAAa,EAAE,SAAS,CAAC,EACvC,OAAO,EAAE,WAAW,gBACR,WAAW,KAAK,EAAE,YAE9B,KAAC,IAAI,IAAC,IAAI,EAAC,iCAAiC,EAAC,SAAS,EAAC,SAAS,GAAG,GAC5D,CACV,CAAC;IACJ,CAAC;IAED,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;QACzB,OAAO,CACL,MAAC,MAAM,IACL,IAAI,EAAC,QAAQ,EACb,OAAO,EAAC,SAAS,EACjB,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,EAAE,CACX,oEAAoE,EACpE,SAAS,CACV,gBACW,KAAK,aAEjB,KAAC,IAAI,IACH,IAAI,EAAC,iCAAiC,EACtC,SAAS,EAAC,sCAAsC,GAChD,EACF,eAAM,SAAS,EAAC,WAAW,YAAE,KAAK,GAAQ,IACnC,CACV,CAAC;IACJ,CAAC;IAED,kBAAkB;IAClB,OAAO,CACL,MAAC,MAAM,IACL,IAAI,EAAC,QAAQ,EACb,OAAO,EAAC,SAAS,EACjB,IAAI,EAAC,IAAI,EACT,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,EAAE,CAAC,mBAAmB,EAAE,SAAS,CAAC,gBACjC,KAAK,aAEjB,KAAC,IAAI,IACH,IAAI,EAAC,iCAAiC,EACtC,SAAS,EAAC,0CAA0C,GACpD,EACF,yBAAO,KAAK,GAAQ,IACb,CACV,CAAC;AACJ,CAAC","sourcesContent":["'use client';\n\nimport { Button } from '@ekanos/ui/button';\nimport { Icon } from '@ekanos/ui/icon';\nimport { cn } from '@ekanos/ui/utils';\n\nimport { useAskAssistant } from '../../hooks/ask-assistant-context';\n\ntype AiPromptChipProps = {\n label: string;\n prompt: string;\n context?: Record<string, unknown>;\n sourceWidget: string;\n /**\n * Human-readable label for the dock's ArrivalChip (\"Asking about …\").\n * Falls back to the chip's display label when omitted.\n */\n title?: string;\n /**\n * Fires after the assistant has been invoked. Drawers pass their close\n * handler here so the dock takes over the screen on click.\n */\n onAfterAsk?: () => void;\n variant?: 'header' | 'inline' | 'footer';\n className?: string;\n};\n\n/**\n * AI handoff chip that opens the assistant dock pre-seeded with this\n * widget/drawer's snapshot. Routes through `useAskAssistant` so the dock\n * receives the standard `{widgetId, title, snapshot, suggestedPrompt}`\n * shape used by all AI handoffs in the app.\n *\n * Variants:\n * - header (Type A): Compact icon button for widget headers\n * - inline (Type B): Chip with sparkle icon + text, below data sections\n * - footer (Type C): Full-width chip in drawer footers\n */\nexport function AiPromptChip({\n label,\n prompt,\n context,\n sourceWidget,\n title,\n onAfterAsk,\n variant = 'inline',\n className,\n}: AiPromptChipProps) {\n const askAssistant = useAskAssistant();\n\n const handleClick = () => {\n askAssistant({\n widgetId: sourceWidget,\n title: title ?? label,\n snapshot: context ?? {},\n suggestedPrompt: prompt,\n });\n onAfterAsk?.();\n };\n\n if (variant === 'header') {\n return (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className={cn('h-8 w-8 p-0', className)}\n onClick={handleClick}\n aria-label={`Ask AI: ${label}`}\n >\n <Icon name=\"fa-light fa-wand-magic-sparkles\" className=\"h-4 w-4\" />\n </Button>\n );\n }\n\n if (variant === 'footer') {\n return (\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={handleClick}\n className={cn(\n 'h-auto w-full justify-start rounded-lg px-4 py-3 whitespace-normal',\n className,\n )}\n aria-label={label}\n >\n <Icon\n name=\"fa-light fa-wand-magic-sparkles\"\n className=\"text-primary-strong h-4 w-4 shrink-0\"\n />\n <span className=\"text-left\">{label}</span>\n </Button>\n );\n }\n\n // inline (Type B)\n return (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"xs\"\n onClick={handleClick}\n className={cn('rounded-full px-3', className)}\n aria-label={label}\n >\n <Icon\n name=\"fa-light fa-wand-magic-sparkles\"\n className=\"text-primary-strong h-3.5 w-3.5 shrink-0\"\n />\n <span>{label}</span>\n </Button>\n );\n}\n"]}
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useCallback, useEffect, useRef, useState } from 'react';
4
4
  import { PromptInput, PromptInputBody, PromptInputFooter, PromptInputSubmit, PromptInputTextarea, PromptInputTools, } from '@ekanos/ui/ai-prompt-input';
5
+ import { Button } from '@ekanos/ui/button';
5
6
  import { Icon } from '@ekanos/ui/icon';
6
7
  import { cn } from '@ekanos/ui/utils';
7
8
  import { useAskAssistant } from '../../hooks/ask-assistant-context.js';
@@ -50,11 +51,9 @@ export function WidgetAskBar({ context }) {
50
51
  document.addEventListener('pointerdown', onPointerDown);
51
52
  return () => document.removeEventListener('pointerdown', onPointerDown);
52
53
  }, [dismiss, isActive]);
53
- return (_jsxs("div", { ref: containerRef, className: "border-border/60 shrink-0 border-t", "data-test": "widget-ask-bar", children: [_jsxs("button", { type: "button", onClick: () => (isActive ? dismiss() : setIsActive(true)), "aria-expanded": isActive, "aria-label": isActive
54
+ return (_jsxs("div", { ref: containerRef, className: "border-border/60 shrink-0 border-t", "data-test": "widget-ask-bar", children: [_jsxs(Button, { type: "button", variant: "ghost", onClick: () => (isActive ? dismiss() : setIsActive(true)), "aria-expanded": isActive, "aria-label": isActive
54
55
  ? `Close ask input for ${context.title}`
55
- : `Ask about ${context.title}`, "data-test": "widget-ask-bar-toggle", className: cn('group/tag hover:bg-accent/40 flex w-full items-center gap-2.5 px-4 py-2.5 text-left transition-colors sm:px-5', isActive && 'bg-accent/30'), children: [_jsx(AskIcon, { className: cn('text-primary h-5 w-5 shrink-0 transition-transform duration-200', isActive && 'scale-110') }), _jsx("span", { className: "text-muted-foreground group-hover/tag:text-foreground flex-1 text-base font-medium transition-colors", children: "Ask about this" }), _jsx(Icon, { name: "fa-light fa-chevron-up", className: cn('text-muted-foreground/50 group-hover/tag:text-muted-foreground h-3.5 w-3.5 shrink-0 transition-transform duration-200', isActive && 'rotate-180') })] }), isActive && (_jsxs("div", { className: "border-border/40 space-y-3 border-t p-3 sm:px-5", "data-test": "widget-ask-bar-input", children: [suggestions.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", "data-test": "widget-ask-bar-suggestions", children: suggestions.map((s) => (_jsxs("button", { type: "button", onClick: () => handleSuggestion(s.prompt), className: cn('inline-flex items-center gap-1.5 rounded-full px-3 py-1.5',
56
- // oxlint-disable-next-line design/no-raw-text-size -- AI suggestion chip at 12px/500 (text-xs font-medium); text-fine is 12px but bakes weight 400, a weight change. Repeated in ai-prompt-chip.tsx's inline chip.
57
- 'border-primary/20 bg-primary/5 text-primary-strong border text-xs font-medium', 'hover:bg-primary/10 hover:border-primary/40 transition-colors', 'focus-visible:ring-ring/60 focus-visible:ring-2 focus-visible:outline-none'), "aria-label": s.label, children: [_jsx(Icon, { name: "fa-light fa-sparkles", className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "text-left", children: s.label })] }, s.label))) })), _jsxs(PromptInput, { onSubmit: handleSubmit, children: [_jsx(PromptInputBody, { children: _jsx(PromptInputTextarea, { autoFocus: true, placeholder: `Ask about ${context.title.toLowerCase()}…`, "aria-label": `Ask about ${context.title}`, onKeyDown: (e) => {
56
+ : `Ask about ${context.title}`, "data-test": "widget-ask-bar-toggle", className: "group/tag h-auto w-full justify-start gap-2.5 rounded-none px-4 py-2.5 text-left sm:px-5", children: [_jsx(AskIcon, { className: cn('text-primary h-5 w-5 shrink-0 transition-transform duration-200', isActive && 'scale-110') }), _jsx("span", { className: "text-muted-foreground group-hover/tag:text-foreground text-body flex-1 font-medium transition-colors", children: "Ask about this" }), _jsx(Icon, { name: "fa-light fa-chevron-up", className: cn('text-muted-foreground/50 group-hover/tag:text-muted-foreground h-3.5 w-3.5 shrink-0 transition-transform duration-200', isActive && 'rotate-180') })] }), isActive && (_jsxs("div", { className: "border-border/40 space-y-3 border-t p-3 sm:px-5", "data-test": "widget-ask-bar-input", children: [suggestions.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", "data-test": "widget-ask-bar-suggestions", children: suggestions.map((s) => (_jsxs(Button, { type: "button", variant: "outline", size: "xs", onClick: () => handleSuggestion(s.prompt), className: "rounded-full px-3", "aria-label": s.label, children: [_jsx(Icon, { name: "fa-light fa-wand-magic-sparkles", className: "text-primary-strong h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "text-left", children: s.label })] }, s.label))) })), _jsxs(PromptInput, { onSubmit: handleSubmit, children: [_jsx(PromptInputBody, { children: _jsx(PromptInputTextarea, { autoFocus: true, placeholder: `Ask about ${context.title.toLowerCase()}…`, "aria-label": `Ask about ${context.title}`, onKeyDown: (e) => {
58
57
  if (e.key === 'Escape') {
59
58
  e.preventDefault();
60
59
  dismiss();
@@ -1 +1 @@
1
- {"version":3,"file":"widget-ask-bar.js","sourceRoot":"","sources":["../../../src/components/widgets/widget-ask-bar.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport {\n PromptInput,\n PromptInputBody,\n PromptInputFooter,\n PromptInputSubmit,\n PromptInputTextarea,\n PromptInputTools,\n} from '@ekanos/ui/ai-prompt-input';\nimport { Icon } from '@ekanos/ui/icon';\nimport { cn } from '@ekanos/ui/utils';\n\nimport { useAskAssistant } from '../../hooks/ask-assistant-context';\nimport type { WidgetAskContext } from '../../types/widget-ask-context';\nimport { AskIcon } from './ask-icon';\n\ninterface Props {\n context: WidgetAskContext;\n}\n\n/**\n * Widget footer tag + expanding prompt input.\n *\n * - closed: a quiet, always-visible tag row at the bottom of the widget\n * (\"Aperture icon · Ask about this · chevron up\")\n * - open: clicking the tag reveals an inline `PromptInput` (the canonical\n * AI input component) below it. The tag itself doubles as the\n * close affordance (chevron rotates).\n *\n * Dismisses on click-outside or Escape. Submitting sends the user's typed\n * question (or the widget's suggestedPrompt fallback) to the assistant dock.\n */\nexport function WidgetAskBar({ context }: Props) {\n const [isActive, setIsActive] = useState(false);\n const containerRef = useRef<HTMLDivElement>(null);\n const askAssistant = useAskAssistant();\n\n const dismiss = useCallback(() => {\n setIsActive(false);\n }, []);\n\n const handleSubmit = useCallback(\n (message: { text: string }) => {\n const prompt = message.text.trim() || context.suggestedPrompt;\n askAssistant({ ...context, suggestedPrompt: prompt });\n dismiss();\n },\n [askAssistant, context, dismiss],\n );\n\n const handleSuggestion = useCallback(\n (prompt: string) => {\n askAssistant({ ...context, suggestedPrompt: prompt });\n dismiss();\n },\n [askAssistant, context, dismiss],\n );\n\n const suggestions = context.suggestedPrompts ?? [];\n\n useEffect(() => {\n if (!isActive) return;\n const onPointerDown = (e: PointerEvent) => {\n const el = containerRef.current;\n if (!el) return;\n if (el.contains(e.target as Node)) return;\n dismiss();\n };\n document.addEventListener('pointerdown', onPointerDown);\n return () => document.removeEventListener('pointerdown', onPointerDown);\n }, [dismiss, isActive]);\n\n return (\n <div\n ref={containerRef}\n className=\"border-border/60 shrink-0 border-t\"\n data-test=\"widget-ask-bar\"\n >\n <button\n type=\"button\"\n onClick={() => (isActive ? dismiss() : setIsActive(true))}\n aria-expanded={isActive}\n aria-label={\n isActive\n ? `Close ask input for ${context.title}`\n : `Ask about ${context.title}`\n }\n data-test=\"widget-ask-bar-toggle\"\n className={cn(\n 'group/tag hover:bg-accent/40 flex w-full items-center gap-2.5 px-4 py-2.5 text-left transition-colors sm:px-5',\n isActive && 'bg-accent/30',\n )}\n >\n <AskIcon\n className={cn(\n 'text-primary h-5 w-5 shrink-0 transition-transform duration-200',\n isActive && 'scale-110',\n )}\n />\n {/* oxlint-disable-next-line design/no-raw-text-size -- \"Ask about this\" label at 16px/500 (text-base font-medium); no tier pairs 500 at 16px (text-title bakes 600, text-body bakes 400) */}\n <span className=\"text-muted-foreground group-hover/tag:text-foreground flex-1 text-base font-medium transition-colors\">\n Ask about this\n </span>\n <Icon\n name=\"fa-light fa-chevron-up\"\n className={cn(\n 'text-muted-foreground/50 group-hover/tag:text-muted-foreground h-3.5 w-3.5 shrink-0 transition-transform duration-200',\n isActive && 'rotate-180',\n )}\n />\n </button>\n\n {isActive && (\n <div\n className=\"border-border/40 space-y-3 border-t p-3 sm:px-5\"\n data-test=\"widget-ask-bar-input\"\n >\n {suggestions.length > 0 && (\n <div\n className=\"flex flex-wrap gap-2\"\n data-test=\"widget-ask-bar-suggestions\"\n >\n {suggestions.map((s) => (\n <button\n key={s.label}\n type=\"button\"\n onClick={() => handleSuggestion(s.prompt)}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-3 py-1.5',\n // oxlint-disable-next-line design/no-raw-text-size -- AI suggestion chip at 12px/500 (text-xs font-medium); text-fine is 12px but bakes weight 400, a weight change. Repeated in ai-prompt-chip.tsx's inline chip.\n 'border-primary/20 bg-primary/5 text-primary-strong border text-xs font-medium',\n 'hover:bg-primary/10 hover:border-primary/40 transition-colors',\n 'focus-visible:ring-ring/60 focus-visible:ring-2 focus-visible:outline-none',\n )}\n aria-label={s.label}\n >\n <Icon\n name=\"fa-light fa-sparkles\"\n className=\"h-3.5 w-3.5 shrink-0\"\n />\n <span className=\"text-left\">{s.label}</span>\n </button>\n ))}\n </div>\n )}\n <PromptInput onSubmit={handleSubmit}>\n <PromptInputBody>\n <PromptInputTextarea\n autoFocus\n placeholder={`Ask about ${context.title.toLowerCase()}…`}\n aria-label={`Ask about ${context.title}`}\n onKeyDown={(e) => {\n if (e.key === 'Escape') {\n e.preventDefault();\n dismiss();\n }\n }}\n />\n </PromptInputBody>\n <PromptInputFooter>\n <PromptInputTools />\n <PromptInputSubmit data-test=\"widget-ask-bar-send\" />\n </PromptInputFooter>\n </PromptInput>\n </div>\n )}\n </div>\n );\n}\n"]}
1
+ {"version":3,"file":"widget-ask-bar.js","sourceRoot":"","sources":["../../../src/components/widgets/widget-ask-bar.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport {\n PromptInput,\n PromptInputBody,\n PromptInputFooter,\n PromptInputSubmit,\n PromptInputTextarea,\n PromptInputTools,\n} from '@ekanos/ui/ai-prompt-input';\nimport { Button } from '@ekanos/ui/button';\nimport { Icon } from '@ekanos/ui/icon';\nimport { cn } from '@ekanos/ui/utils';\n\nimport { useAskAssistant } from '../../hooks/ask-assistant-context';\nimport type { WidgetAskContext } from '../../types/widget-ask-context';\nimport { AskIcon } from './ask-icon';\n\ninterface Props {\n context: WidgetAskContext;\n}\n\n/**\n * Widget footer tag + expanding prompt input.\n *\n * - closed: a quiet, always-visible tag row at the bottom of the widget\n * (\"Aperture icon · Ask about this · chevron up\")\n * - open: clicking the tag reveals an inline `PromptInput` (the canonical\n * AI input component) below it. The tag itself doubles as the\n * close affordance (chevron rotates).\n *\n * Dismisses on click-outside or Escape. Submitting sends the user's typed\n * question (or the widget's suggestedPrompt fallback) to the assistant dock.\n */\nexport function WidgetAskBar({ context }: Props) {\n const [isActive, setIsActive] = useState(false);\n const containerRef = useRef<HTMLDivElement>(null);\n const askAssistant = useAskAssistant();\n\n const dismiss = useCallback(() => {\n setIsActive(false);\n }, []);\n\n const handleSubmit = useCallback(\n (message: { text: string }) => {\n const prompt = message.text.trim() || context.suggestedPrompt;\n askAssistant({ ...context, suggestedPrompt: prompt });\n dismiss();\n },\n [askAssistant, context, dismiss],\n );\n\n const handleSuggestion = useCallback(\n (prompt: string) => {\n askAssistant({ ...context, suggestedPrompt: prompt });\n dismiss();\n },\n [askAssistant, context, dismiss],\n );\n\n const suggestions = context.suggestedPrompts ?? [];\n\n useEffect(() => {\n if (!isActive) return;\n const onPointerDown = (e: PointerEvent) => {\n const el = containerRef.current;\n if (!el) return;\n if (el.contains(e.target as Node)) return;\n dismiss();\n };\n document.addEventListener('pointerdown', onPointerDown);\n return () => document.removeEventListener('pointerdown', onPointerDown);\n }, [dismiss, isActive]);\n\n return (\n <div\n ref={containerRef}\n className=\"border-border/60 shrink-0 border-t\"\n data-test=\"widget-ask-bar\"\n >\n <Button\n type=\"button\"\n variant=\"ghost\"\n onClick={() => (isActive ? dismiss() : setIsActive(true))}\n aria-expanded={isActive}\n aria-label={\n isActive\n ? `Close ask input for ${context.title}`\n : `Ask about ${context.title}`\n }\n data-test=\"widget-ask-bar-toggle\"\n className=\"group/tag h-auto w-full justify-start gap-2.5 rounded-none px-4 py-2.5 text-left sm:px-5\"\n >\n <AskIcon\n className={cn(\n 'text-primary h-5 w-5 shrink-0 transition-transform duration-200',\n isActive && 'scale-110',\n )}\n />\n <span className=\"text-muted-foreground group-hover/tag:text-foreground text-body flex-1 font-medium transition-colors\">\n Ask about this\n </span>\n <Icon\n name=\"fa-light fa-chevron-up\"\n className={cn(\n 'text-muted-foreground/50 group-hover/tag:text-muted-foreground h-3.5 w-3.5 shrink-0 transition-transform duration-200',\n isActive && 'rotate-180',\n )}\n />\n </Button>\n\n {isActive && (\n <div\n className=\"border-border/40 space-y-3 border-t p-3 sm:px-5\"\n data-test=\"widget-ask-bar-input\"\n >\n {suggestions.length > 0 && (\n <div\n className=\"flex flex-wrap gap-2\"\n data-test=\"widget-ask-bar-suggestions\"\n >\n {suggestions.map((s) => (\n <Button\n key={s.label}\n type=\"button\"\n variant=\"outline\"\n size=\"xs\"\n onClick={() => handleSuggestion(s.prompt)}\n className=\"rounded-full px-3\"\n aria-label={s.label}\n >\n <Icon\n name=\"fa-light fa-wand-magic-sparkles\"\n className=\"text-primary-strong h-3.5 w-3.5 shrink-0\"\n />\n <span className=\"text-left\">{s.label}</span>\n </Button>\n ))}\n </div>\n )}\n <PromptInput onSubmit={handleSubmit}>\n <PromptInputBody>\n <PromptInputTextarea\n autoFocus\n placeholder={`Ask about ${context.title.toLowerCase()}…`}\n aria-label={`Ask about ${context.title}`}\n onKeyDown={(e) => {\n if (e.key === 'Escape') {\n e.preventDefault();\n dismiss();\n }\n }}\n />\n </PromptInputBody>\n <PromptInputFooter>\n <PromptInputTools />\n <PromptInputSubmit data-test=\"widget-ask-bar-send\" />\n </PromptInputFooter>\n </PromptInput>\n </div>\n )}\n </div>\n );\n}\n"]}
@@ -17,7 +17,7 @@ function StateAction({ action, variant = 'default', }) {
17
17
  * and at most one action, left-aligned in the body's padding.
18
18
  */
19
19
  function StateNote({ icon, title, description, action, actionVariant, role, testId, }) {
20
- return (_jsxs("div", { role: role, className: "flex flex-1 flex-col items-start gap-3 px-5 pt-2 pb-5", "data-test": testId, children: [_jsx(Icon, { name: icon, className: "text-muted-foreground h-6 w-6", "aria-hidden": true }), _jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-foreground text-base font-medium", children: title }), description ? (_jsx("p", { className: "text-muted-foreground text-meta max-w-prose", children: description })) : null] }), action ? _jsx(StateAction, { action: action, variant: actionVariant }) : null] }));
20
+ return (_jsxs("div", { role: role, className: "flex flex-1 flex-col items-start gap-3 px-5 pt-2 pb-5", "data-test": testId, children: [_jsx(Icon, { name: icon, className: "text-muted-foreground h-6 w-6", "aria-hidden": true }), _jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-foreground text-body font-medium", children: title }), description ? (_jsx("p", { className: "text-muted-foreground text-meta max-w-prose", children: description })) : null] }), action ? _jsx(StateAction, { action: action, variant: actionVariant }) : null] }));
21
21
  }
22
22
  /**
23
23
  * The empty state for a widget with nothing to show. Positive by default:
@@ -1 +1 @@
1
- {"version":3,"file":"widget-state-components.js","sourceRoot":"","sources":["../../../src/components/widgets/widget-state-components.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { ReactNode } from 'react';\n\nimport Link from 'next/link.js';\n\nimport { Button } from '@ekanos/ui/button';\nimport { Icon } from '@ekanos/ui/icon';\nimport { Skeleton } from '@ekanos/ui/skeleton';\nimport { Trans } from '@ekanos/ui/trans';\nimport { cn } from '@ekanos/ui/utils';\n\n/**\n * The one action a state view offers: \"Connect QuickBooks\", \"Ask an admin\",\n * \"Create a deal\". A link with `href`, otherwise a button running `onClick`.\n */\nexport interface WidgetStateAction {\n label: ReactNode;\n href?: string;\n /** Open `href` in a new tab. */\n external?: boolean;\n onClick?: () => void;\n}\n\n/** The body shape a loading skeleton stands in for. */\nexport type WidgetSkeletonVariant = 'rows' | 'stat' | 'chart' | 'schedule';\n\n/** Why an active integration still can't show this widget's data. */\nexport type WidgetInactiveReason =\n | 'not-connected'\n | 'no-access'\n | 'setup-needed';\n\nexport interface WidgetEmptyStateProps {\n /**\n * `positive` (the default) says nothing needs the owner: \"You're all caught\n * up\". `neutral` is for an empty list that isn't good news, like a search\n * with no results.\n */\n tone?: 'positive' | 'neutral';\n title?: ReactNode;\n description?: ReactNode;\n action?: WidgetStateAction;\n}\n\nexport interface WidgetInactiveStateProps {\n reason?: WidgetInactiveReason;\n /** The integration's name, for the default copy (\"Connect QuickBooks\"). */\n integrationName?: string;\n title?: ReactNode;\n /** One sentence on what connecting or access gives the owner. */\n description?: ReactNode;\n action?: WidgetStateAction;\n}\n\nfunction StateAction({\n action,\n variant = 'default',\n}: {\n action: WidgetStateAction;\n variant?: 'default' | 'outline';\n}) {\n if (action.href) {\n const link = action.external ? (\n <a href={action.href} target=\"_blank\" rel=\"noopener noreferrer\" />\n ) : (\n <Link href={action.href} />\n );\n\n return (\n <Button size=\"sm\" variant={variant} render={link} nativeButton={false}>\n {action.label}\n </Button>\n );\n }\n\n return (\n <Button size=\"sm\" variant={variant} type=\"button\" onClick={action.onClick}>\n {action.label}\n </Button>\n );\n}\n\n/**\n * The shared shape of every non-data state: an icon, a title, one sentence\n * and at most one action, left-aligned in the body's padding.\n */\nfunction StateNote({\n icon,\n title,\n description,\n action,\n actionVariant,\n role,\n testId,\n}: {\n icon: string;\n title: ReactNode;\n description?: ReactNode;\n action?: WidgetStateAction;\n actionVariant?: 'default' | 'outline';\n role?: 'alert' | 'status';\n testId: string;\n}) {\n return (\n <div\n role={role}\n className=\"flex flex-1 flex-col items-start gap-3 px-5 pt-2 pb-5\"\n data-test={testId}\n >\n <Icon name={icon} className=\"text-muted-foreground h-6 w-6\" aria-hidden />\n <div className=\"space-y-1\">\n {/* oxlint-disable-next-line design/no-raw-text-size -- state-note title at 16px/500 (text-base font-medium); no tier pairs 500 at 16px (text-title bakes 600, text-body bakes 400) */}\n <p className=\"text-foreground text-base font-medium\">{title}</p>\n {description ? (\n <p className=\"text-muted-foreground text-meta max-w-prose\">\n {description}\n </p>\n ) : null}\n </div>\n {action ? <StateAction action={action} variant={actionVariant} /> : null}\n </div>\n );\n}\n\n/**\n * The empty state for a widget with nothing to show. Positive by default:\n * an empty review queue is good news, so it reassures instead of apologizing.\n */\nexport function WidgetEmptyState({\n tone = 'positive',\n title,\n description,\n action,\n}: WidgetEmptyStateProps) {\n const positive = tone === 'positive';\n\n return (\n <StateNote\n icon={positive ? 'fa-light fa-circle-check' : 'fa-light fa-inbox'}\n title={\n title ??\n (positive ? (\n <Trans\n i18nKey=\"dashboard:widgetEmptyPositive\"\n defaults=\"You're all caught up\"\n />\n ) : (\n <Trans\n i18nKey=\"dashboard:widgetEmptyNeutral\"\n defaults=\"Nothing here yet\"\n />\n ))\n }\n description={description}\n action={action}\n actionVariant=\"outline\"\n role=\"status\"\n testId={`widget-empty-${tone}`}\n />\n );\n}\n\nconst INACTIVE_ICON: Record<WidgetInactiveReason, string> = {\n 'not-connected': 'fa-light fa-plug',\n 'no-access': 'fa-light fa-user-group',\n 'setup-needed': 'fa-light fa-sliders',\n};\n\n/**\n * Why the widget can't show data. `not-connected` offers one Connect button;\n * `no-access` is a calm note that points to an admin, never a lock screen;\n * `setup-needed` points to the integration's settings. Without a `reason` it\n * keeps the original one-line \"Widget inactive\".\n */\nexport function WidgetInactiveState({\n reason,\n integrationName,\n title,\n description,\n action,\n}: WidgetInactiveStateProps = {}) {\n if (!reason) {\n return (\n <div className=\"flex flex-col items-center justify-center py-8 text-center\">\n <p className=\"text-muted-foreground text-meta\">\n {title ?? (\n <Trans\n i18nKey=\"dashboard:widgetInactive\"\n defaults=\"Widget inactive\"\n />\n )}\n </p>\n </div>\n );\n }\n\n const name = integrationName ?? 'this integration';\n\n const defaults: Record<\n WidgetInactiveReason,\n { title: ReactNode; description: ReactNode }\n > = {\n 'not-connected': {\n title: (\n <Trans\n i18nKey=\"dashboard:widgetNotConnectedTitle\"\n defaults=\"Connect {{name}}\"\n values={{ name }}\n />\n ),\n description: (\n <Trans\n i18nKey=\"dashboard:widgetNotConnectedBody\"\n defaults=\"Connect {{name}} and this card fills in with your own data.\"\n values={{ name }}\n />\n ),\n },\n 'no-access': {\n title: (\n <Trans\n i18nKey=\"dashboard:widgetNoAccessTitle\"\n defaults=\"Shared with admins for now\"\n />\n ),\n description: (\n <Trans\n i18nKey=\"dashboard:widgetNoAccessBody\"\n defaults=\"Your role doesn't include this data yet. Ask an admin if you need it.\"\n />\n ),\n },\n 'setup-needed': {\n title: (\n <Trans\n i18nKey=\"dashboard:widgetSetupNeededTitle\"\n defaults=\"Finish setting up {{name}}\"\n values={{ name }}\n />\n ),\n description: (\n <Trans\n i18nKey=\"dashboard:widgetSetupNeededBody\"\n defaults=\"One more step and this card starts showing data.\"\n />\n ),\n },\n };\n\n return (\n <StateNote\n icon={INACTIVE_ICON[reason]}\n title={title ?? defaults[reason].title}\n description={description ?? defaults[reason].description}\n action={action}\n actionVariant={reason === 'no-access' ? 'outline' : 'default'}\n testId={`widget-inactive-${reason}`}\n />\n );\n}\n\n/**\n * Loading, drawn as the body it stands in for so the card doesn't jump when\n * data lands. Rows suit lists and feeds, `stat` a headline figure, `chart` a\n * chart, `schedule` a day of events.\n */\nexport function WidgetLoadingSkeleton({\n variant = 'rows',\n}: {\n variant?: WidgetSkeletonVariant;\n}) {\n return (\n <div\n role=\"status\"\n aria-busy=\"true\"\n className=\"flex flex-1 flex-col\"\n data-test={`widget-skeleton-${variant}`}\n >\n <span className=\"sr-only\">\n <Trans i18nKey=\"dashboard:widgetLoading\" defaults=\"Loading...\" />\n </span>\n {variant === 'stat' ? (\n <div className=\"space-y-2 px-5 pt-2 pb-5\">\n <Skeleton className=\"h-3 w-24\" />\n <Skeleton className=\"h-8 w-40\" />\n <Skeleton className=\"h-3 w-32\" />\n </div>\n ) : variant === 'chart' ? (\n <div className=\"space-y-3 px-5 pt-2 pb-5\">\n <Skeleton className=\"h-8 w-40\" />\n <Skeleton className=\"h-60 w-full\" />\n </div>\n ) : (\n <div className=\"divide-border/50 divide-y\">\n {[0, 1, 2].map((row) => (\n <div\n key={row}\n className={cn(\n 'px-5 py-3',\n variant === 'schedule' ? 'flex items-start gap-3' : 'space-y-2',\n )}\n >\n {variant === 'schedule' ? (\n <>\n <Skeleton className=\"h-4 w-14 shrink-0\" />\n <div className=\"flex-1 space-y-2\">\n <Skeleton className=\"h-4 w-2/3\" />\n <Skeleton className=\"h-3 w-1/3\" />\n </div>\n </>\n ) : (\n <>\n <Skeleton className=\"h-4 w-2/3\" />\n <Skeleton className=\"h-3 w-1/2\" />\n </>\n )}\n </div>\n ))}\n </div>\n )}\n </div>\n );\n}\n\nexport function WidgetErrorState({\n title,\n description,\n onRetry,\n retrying = false,\n}: {\n title?: ReactNode;\n description?: ReactNode;\n onRetry?: () => void;\n retrying?: boolean;\n}) {\n return (\n <div\n role=\"alert\"\n className=\"flex flex-col items-center justify-center gap-3 px-5 py-8 text-center\"\n >\n <div className=\"bg-destructive/10 flex h-10 w-10 items-center justify-center rounded-full\">\n <Icon\n name=\"fa-light fa-circle-exclamation\"\n className=\"text-destructive h-5 w-5\"\n aria-hidden\n />\n </div>\n <div className=\"space-y-1\">\n <p className=\"text-foreground text-label\">\n {title ?? (\n <Trans\n i18nKey=\"dashboard:widgetErrorTitle\"\n defaults=\"Couldn't load this widget\"\n />\n )}\n </p>\n <p className=\"text-muted-foreground text-meta\">\n {description ? (\n description\n ) : onRetry ? (\n <Trans\n i18nKey=\"dashboard:widgetErrorBodyRetry\"\n defaults=\"Try again, or check back in a few minutes.\"\n />\n ) : (\n <Trans\n i18nKey=\"dashboard:widgetErrorBody\"\n defaults=\"Check back in a few minutes.\"\n />\n )}\n </p>\n </div>\n {onRetry ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={onRetry}\n disabled={retrying}\n data-test=\"widget-error-retry\"\n >\n <Icon\n name=\"fa-light fa-arrows-rotate\"\n className={cn('h-4 w-4', retrying && 'animate-spin')}\n aria-hidden\n />\n <Trans i18nKey=\"dashboard:widgetRetry\" defaults=\"Try again\" />\n </Button>\n ) : null}\n </div>\n );\n}\n\nexport function WidgetDataLoading() {\n return (\n <div className=\"flex flex-1 items-center justify-center py-12\">\n <Icon\n name=\"fa-light fa-spinner-third\"\n className=\"text-muted-foreground h-8 w-8 animate-spin\"\n />\n </div>\n );\n}\n\nexport function WidgetDisabledState() {\n return (\n <div className=\"flex flex-col items-center justify-center py-8 text-center\">\n <Icon\n name=\"fa-light fa-ban\"\n className=\"text-muted-foreground mb-4 h-8 w-8\"\n />\n <p className=\"text-muted-foreground text-meta\">\n <Trans i18nKey=\"dashboard:widgetDisabled\" defaults=\"Widget Disabled\" />\n </p>\n </div>\n );\n}\n"]}
1
+ {"version":3,"file":"widget-state-components.js","sourceRoot":"","sources":["../../../src/components/widgets/widget-state-components.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { ReactNode } from 'react';\n\nimport Link from 'next/link.js';\n\nimport { Button } from '@ekanos/ui/button';\nimport { Icon } from '@ekanos/ui/icon';\nimport { Skeleton } from '@ekanos/ui/skeleton';\nimport { Trans } from '@ekanos/ui/trans';\nimport { cn } from '@ekanos/ui/utils';\n\n/**\n * The one action a state view offers: \"Connect QuickBooks\", \"Ask an admin\",\n * \"Create a deal\". A link with `href`, otherwise a button running `onClick`.\n */\nexport interface WidgetStateAction {\n label: ReactNode;\n href?: string;\n /** Open `href` in a new tab. */\n external?: boolean;\n onClick?: () => void;\n}\n\n/** The body shape a loading skeleton stands in for. */\nexport type WidgetSkeletonVariant = 'rows' | 'stat' | 'chart' | 'schedule';\n\n/** Why an active integration still can't show this widget's data. */\nexport type WidgetInactiveReason =\n | 'not-connected'\n | 'no-access'\n | 'setup-needed';\n\nexport interface WidgetEmptyStateProps {\n /**\n * `positive` (the default) says nothing needs the owner: \"You're all caught\n * up\". `neutral` is for an empty list that isn't good news, like a search\n * with no results.\n */\n tone?: 'positive' | 'neutral';\n title?: ReactNode;\n description?: ReactNode;\n action?: WidgetStateAction;\n}\n\nexport interface WidgetInactiveStateProps {\n reason?: WidgetInactiveReason;\n /** The integration's name, for the default copy (\"Connect QuickBooks\"). */\n integrationName?: string;\n title?: ReactNode;\n /** One sentence on what connecting or access gives the owner. */\n description?: ReactNode;\n action?: WidgetStateAction;\n}\n\nfunction StateAction({\n action,\n variant = 'default',\n}: {\n action: WidgetStateAction;\n variant?: 'default' | 'outline';\n}) {\n if (action.href) {\n const link = action.external ? (\n <a href={action.href} target=\"_blank\" rel=\"noopener noreferrer\" />\n ) : (\n <Link href={action.href} />\n );\n\n return (\n <Button size=\"sm\" variant={variant} render={link} nativeButton={false}>\n {action.label}\n </Button>\n );\n }\n\n return (\n <Button size=\"sm\" variant={variant} type=\"button\" onClick={action.onClick}>\n {action.label}\n </Button>\n );\n}\n\n/**\n * The shared shape of every non-data state: an icon, a title, one sentence\n * and at most one action, left-aligned in the body's padding.\n */\nfunction StateNote({\n icon,\n title,\n description,\n action,\n actionVariant,\n role,\n testId,\n}: {\n icon: string;\n title: ReactNode;\n description?: ReactNode;\n action?: WidgetStateAction;\n actionVariant?: 'default' | 'outline';\n role?: 'alert' | 'status';\n testId: string;\n}) {\n return (\n <div\n role={role}\n className=\"flex flex-1 flex-col items-start gap-3 px-5 pt-2 pb-5\"\n data-test={testId}\n >\n <Icon name={icon} className=\"text-muted-foreground h-6 w-6\" aria-hidden />\n <div className=\"space-y-1\">\n <p className=\"text-foreground text-body font-medium\">{title}</p>\n {description ? (\n <p className=\"text-muted-foreground text-meta max-w-prose\">\n {description}\n </p>\n ) : null}\n </div>\n {action ? <StateAction action={action} variant={actionVariant} /> : null}\n </div>\n );\n}\n\n/**\n * The empty state for a widget with nothing to show. Positive by default:\n * an empty review queue is good news, so it reassures instead of apologizing.\n */\nexport function WidgetEmptyState({\n tone = 'positive',\n title,\n description,\n action,\n}: WidgetEmptyStateProps) {\n const positive = tone === 'positive';\n\n return (\n <StateNote\n icon={positive ? 'fa-light fa-circle-check' : 'fa-light fa-inbox'}\n title={\n title ??\n (positive ? (\n <Trans\n i18nKey=\"dashboard:widgetEmptyPositive\"\n defaults=\"You're all caught up\"\n />\n ) : (\n <Trans\n i18nKey=\"dashboard:widgetEmptyNeutral\"\n defaults=\"Nothing here yet\"\n />\n ))\n }\n description={description}\n action={action}\n actionVariant=\"outline\"\n role=\"status\"\n testId={`widget-empty-${tone}`}\n />\n );\n}\n\nconst INACTIVE_ICON: Record<WidgetInactiveReason, string> = {\n 'not-connected': 'fa-light fa-plug',\n 'no-access': 'fa-light fa-user-group',\n 'setup-needed': 'fa-light fa-sliders',\n};\n\n/**\n * Why the widget can't show data. `not-connected` offers one Connect button;\n * `no-access` is a calm note that points to an admin, never a lock screen;\n * `setup-needed` points to the integration's settings. Without a `reason` it\n * keeps the original one-line \"Widget inactive\".\n */\nexport function WidgetInactiveState({\n reason,\n integrationName,\n title,\n description,\n action,\n}: WidgetInactiveStateProps = {}) {\n if (!reason) {\n return (\n <div className=\"flex flex-col items-center justify-center py-8 text-center\">\n <p className=\"text-muted-foreground text-meta\">\n {title ?? (\n <Trans\n i18nKey=\"dashboard:widgetInactive\"\n defaults=\"Widget inactive\"\n />\n )}\n </p>\n </div>\n );\n }\n\n const name = integrationName ?? 'this integration';\n\n const defaults: Record<\n WidgetInactiveReason,\n { title: ReactNode; description: ReactNode }\n > = {\n 'not-connected': {\n title: (\n <Trans\n i18nKey=\"dashboard:widgetNotConnectedTitle\"\n defaults=\"Connect {{name}}\"\n values={{ name }}\n />\n ),\n description: (\n <Trans\n i18nKey=\"dashboard:widgetNotConnectedBody\"\n defaults=\"Connect {{name}} and this card fills in with your own data.\"\n values={{ name }}\n />\n ),\n },\n 'no-access': {\n title: (\n <Trans\n i18nKey=\"dashboard:widgetNoAccessTitle\"\n defaults=\"Shared with admins for now\"\n />\n ),\n description: (\n <Trans\n i18nKey=\"dashboard:widgetNoAccessBody\"\n defaults=\"Your role doesn't include this data yet. Ask an admin if you need it.\"\n />\n ),\n },\n 'setup-needed': {\n title: (\n <Trans\n i18nKey=\"dashboard:widgetSetupNeededTitle\"\n defaults=\"Finish setting up {{name}}\"\n values={{ name }}\n />\n ),\n description: (\n <Trans\n i18nKey=\"dashboard:widgetSetupNeededBody\"\n defaults=\"One more step and this card starts showing data.\"\n />\n ),\n },\n };\n\n return (\n <StateNote\n icon={INACTIVE_ICON[reason]}\n title={title ?? defaults[reason].title}\n description={description ?? defaults[reason].description}\n action={action}\n actionVariant={reason === 'no-access' ? 'outline' : 'default'}\n testId={`widget-inactive-${reason}`}\n />\n );\n}\n\n/**\n * Loading, drawn as the body it stands in for so the card doesn't jump when\n * data lands. Rows suit lists and feeds, `stat` a headline figure, `chart` a\n * chart, `schedule` a day of events.\n */\nexport function WidgetLoadingSkeleton({\n variant = 'rows',\n}: {\n variant?: WidgetSkeletonVariant;\n}) {\n return (\n <div\n role=\"status\"\n aria-busy=\"true\"\n className=\"flex flex-1 flex-col\"\n data-test={`widget-skeleton-${variant}`}\n >\n <span className=\"sr-only\">\n <Trans i18nKey=\"dashboard:widgetLoading\" defaults=\"Loading...\" />\n </span>\n {variant === 'stat' ? (\n <div className=\"space-y-2 px-5 pt-2 pb-5\">\n <Skeleton className=\"h-3 w-24\" />\n <Skeleton className=\"h-8 w-40\" />\n <Skeleton className=\"h-3 w-32\" />\n </div>\n ) : variant === 'chart' ? (\n <div className=\"space-y-3 px-5 pt-2 pb-5\">\n <Skeleton className=\"h-8 w-40\" />\n <Skeleton className=\"h-60 w-full\" />\n </div>\n ) : (\n <div className=\"divide-border/50 divide-y\">\n {[0, 1, 2].map((row) => (\n <div\n key={row}\n className={cn(\n 'px-5 py-3',\n variant === 'schedule' ? 'flex items-start gap-3' : 'space-y-2',\n )}\n >\n {variant === 'schedule' ? (\n <>\n <Skeleton className=\"h-4 w-14 shrink-0\" />\n <div className=\"flex-1 space-y-2\">\n <Skeleton className=\"h-4 w-2/3\" />\n <Skeleton className=\"h-3 w-1/3\" />\n </div>\n </>\n ) : (\n <>\n <Skeleton className=\"h-4 w-2/3\" />\n <Skeleton className=\"h-3 w-1/2\" />\n </>\n )}\n </div>\n ))}\n </div>\n )}\n </div>\n );\n}\n\nexport function WidgetErrorState({\n title,\n description,\n onRetry,\n retrying = false,\n}: {\n title?: ReactNode;\n description?: ReactNode;\n onRetry?: () => void;\n retrying?: boolean;\n}) {\n return (\n <div\n role=\"alert\"\n className=\"flex flex-col items-center justify-center gap-3 px-5 py-8 text-center\"\n >\n <div className=\"bg-destructive/10 flex h-10 w-10 items-center justify-center rounded-full\">\n <Icon\n name=\"fa-light fa-circle-exclamation\"\n className=\"text-destructive h-5 w-5\"\n aria-hidden\n />\n </div>\n <div className=\"space-y-1\">\n <p className=\"text-foreground text-label\">\n {title ?? (\n <Trans\n i18nKey=\"dashboard:widgetErrorTitle\"\n defaults=\"Couldn't load this widget\"\n />\n )}\n </p>\n <p className=\"text-muted-foreground text-meta\">\n {description ? (\n description\n ) : onRetry ? (\n <Trans\n i18nKey=\"dashboard:widgetErrorBodyRetry\"\n defaults=\"Try again, or check back in a few minutes.\"\n />\n ) : (\n <Trans\n i18nKey=\"dashboard:widgetErrorBody\"\n defaults=\"Check back in a few minutes.\"\n />\n )}\n </p>\n </div>\n {onRetry ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={onRetry}\n disabled={retrying}\n data-test=\"widget-error-retry\"\n >\n <Icon\n name=\"fa-light fa-arrows-rotate\"\n className={cn('h-4 w-4', retrying && 'animate-spin')}\n aria-hidden\n />\n <Trans i18nKey=\"dashboard:widgetRetry\" defaults=\"Try again\" />\n </Button>\n ) : null}\n </div>\n );\n}\n\nexport function WidgetDataLoading() {\n return (\n <div className=\"flex flex-1 items-center justify-center py-12\">\n <Icon\n name=\"fa-light fa-spinner-third\"\n className=\"text-muted-foreground h-8 w-8 animate-spin\"\n />\n </div>\n );\n}\n\nexport function WidgetDisabledState() {\n return (\n <div className=\"flex flex-col items-center justify-center py-8 text-center\">\n <Icon\n name=\"fa-light fa-ban\"\n className=\"text-muted-foreground mb-4 h-8 w-8\"\n />\n <p className=\"text-muted-foreground text-meta\">\n <Trans i18nKey=\"dashboard:widgetDisabled\" defaults=\"Widget Disabled\" />\n </p>\n </div>\n );\n}\n"]}
@@ -22,10 +22,10 @@
22
22
  * inputs, selects, textareas and labels, each of which carries accessibility
23
23
  * wiring a bare element does not.
24
24
  *
25
- * The other nine are the host's own `design/*` rules — palette-adjacent
25
+ * The other eleven are the host's own `design/*` rules — palette-adjacent
26
26
  * white/black utilities, Badge tone variants, overlay widths, rounded row
27
27
  * hovers, faded muted text, viewport breakpoints in widgets, tinted chips,
28
- * toolbar tabs, raw type steps — imported from `./eslint.design-rules.mjs`, the single
28
+ * toolbar tabs, raw type steps, raw buttons, hand-drawn cards — imported from `./eslint.design-rules.mjs`, the single
29
29
  * implementation the host repo's oxlint plugin also loads (through the
30
30
  * two-line re-export the host's lint config keeps). There is
31
31
  * exactly one copy of each rule, so a partner is held to what the host holds
@@ -1,10 +1,10 @@
1
1
  /**
2
- * The design-system rules the shadcn linter does not cover — nine of them,
2
+ * The design-system rules the shadcn linter does not cover — eleven of them,
3
3
  * as one lint plugin object. The rules and their reasoning live in the design system's
4
4
  * rules; each message names the rule.
5
5
  *
6
6
  * ── Why this file sits in a published package ────────────────────────────
7
- * Two consumers need the same nine rules and neither may fork them:
7
+ * Two consumers need the same rules and neither may fork them:
8
8
  *
9
9
  * - the host repo's own lint, as an oxlint JS plugin. `.oxlintrc.json`
10
10
  * still loads this plugin through a
@@ -205,7 +205,8 @@ function isRoundedRowHover(value) {
205
205
  const CHIP_TONES = ['success', 'primary', 'warning', 'destructive', 'info'];
206
206
 
207
207
  // The two raw steps PLUS the three tiers that replaced them (the type scale:
208
- // `text-fine` is 12px, `text-meta` 14px/400, `text-label` 14px/500). Without
208
+ // `text-fine` and `text-eyebrow` are 12px, `text-meta` and `text-body-sm`
209
+ // 14px/400, `text-label` 14px/500). Without
209
210
  // the tiers this rule went silently blind the moment the type-scale sweep
210
211
  // landed — five weak tinted chips stopped matching while looking identical.
211
212
  // Any rule keyed on a raw step has the same failure mode; check for it.
@@ -214,7 +215,7 @@ const CHIP_TONES = ['success', 'primary', 'warning', 'destructive', 'info'];
214
215
  // this is the SMALL-text set, and a chip at 20px is not a weak chip, it is a
215
216
  // mis-sized one. Re-check this list whenever a tier is added at 12 or 14px.
216
217
  const CHIP_SMALL_TEXT =
217
- /(?:^|\s)(?:[\w-]+:)*text-(?:xs|sm|fine|meta|label)(?=$|\s)/;
218
+ /(?:^|\s)(?:[\w-]+:)*text-(?:xs|sm|fine|eyebrow|meta|body-sm|label)(?=$|\s)/;
218
219
 
219
220
  /** Base-tone small text on a tint of the same tone. */
220
221
  /** @param {string} value */
@@ -293,12 +294,57 @@ const VIEWPORT_BREAKPOINT =
293
294
  const RAW_TEXT_SIZE =
294
295
  /(?:^|[\s:])text-(?:xs|sm|base|lg|xl|[2-9]xl|\[(?:length:[^\]]*|[\d.]+(?:rem|px|em|ex|ch|pt|pc|in|cm|mm|vw|vh|vmin|vmax|%))\])(?:\/[\w.[\]-]+)?(?=$|\s)/;
295
296
 
297
+ // <type-scale> Generated from the Ekanos type-scale table by `pnpm type-scale`.
298
+ const HEADING_FACE_TIER =
299
+ /(?:^|[\s:])text-(?:display-lg|display|headline-lg|headline|subhead-lg)(?=$|\s)/;
300
+
301
+ const HEADING_FACE_TIER_MESSAGE =
302
+ 'A heading-face tier carries its face: write heading-display-lg | heading-display | heading-headline-lg | heading-headline | heading-subhead-lg instead of text-<tier> (with or without font-heading beside it). The heading-<tier> utility sets the face, size, leading, weight and tracking in one class (design-system type-scale rule).';
303
+
296
304
  const RAW_TEXT_SIZE_MESSAGE =
297
- 'A raw type step is not the scale. Use the Ekanos type-scale tier: text-display-lg (48px) | text-display (36px) | text-headline-lg (30px, 700) | text-headline (24px, 600) | text-subhead-lg (20px, 600) | text-subhead (18px, 600) | text-title (16px, 600) | text-body (16px, 400) | text-label (14px, 500) | text-meta (14px, 400) | text-fine (12px). Each tier carries its leading and weight, so the font-* class beside the old step usually goes with it (design-system type-scale rule).';
305
+ 'A raw type step is not the scale. Use the Ekanos type-scale tier: heading-display-lg (48px/52, 700) | heading-display (36px/40, 700) | heading-headline-lg (30px/36, 700) | heading-headline (24px/32, 600) | heading-subhead-lg (20px/28, 600) | text-subhead (18px/28, 600) | text-title (16px/24, 600) | text-body (16px/24, 400) | text-body-sm (14px/20, 400) | text-label (14px/20, 500) | text-meta (14px/20, 400) | text-eyebrow (12px/16, 500) | text-fine (12px/16, 400). Each tier carries its leading and weight, so the font-* class beside the old step usually goes with it (design-system type-scale rule).';
306
+ // </type-scale>
298
307
 
299
308
  const TOOLBAR_TAB_ELEMENTS = new Set(['TabsList', 'ScopePills', 'ScopeLinks']);
300
309
  const TOOLBARS = new Set(['PageToolbar', 'PageToolbarActions']);
301
310
 
311
+ /**
312
+ * Whether a JSX element is the value of a `render` prop —
313
+ * `<Item render={<button type="button" />}>`. That is the sanctioned way to
314
+ * give a kit component a native button element, so it is not a raw button.
315
+ *
316
+ * @param {JsxElement} node the opening element
317
+ */
318
+ function isRenderPropValue(node) {
319
+ const element = node.parent;
320
+ const container = element?.parent;
321
+ const attribute = container?.parent;
322
+
323
+ return (
324
+ container?.type === 'JSXExpressionContainer' &&
325
+ attribute?.type === 'JSXAttribute' &&
326
+ attribute.name?.name === 'render'
327
+ );
328
+ }
329
+
330
+ /**
331
+ * A card surface drawn by hand: a `bg-card` fill with rounded corners and an
332
+ * edge (an elevation tier or a border) on a plain element. `Card` and
333
+ * its `cardSurfaceVariants` own that combination; a call site that spells it out drifts
334
+ * from the radius, padding and elevation the component carries.
335
+ */
336
+ const CARD_FILL = /(?:^|\s)bg-card(?=$|\s)/;
337
+ const CARD_CORNER = /(?:^|\s)rounded(?:-[\w[\]]+)?(?=$|\s)/;
338
+ const CARD_EDGE =
339
+ /(?:^|\s)(?:shadow-(?:card|widget|xs|sm|md)|border(?:-border)?)(?=$|\s)/;
340
+
341
+ /** @param {string} value */
342
+ function isHandDrawnCard(value) {
343
+ return (
344
+ CARD_FILL.test(value) && CARD_CORNER.test(value) && CARD_EDGE.test(value)
345
+ );
346
+ }
347
+
302
348
  export default {
303
349
  meta: { name: 'design' },
304
350
  rules: {
@@ -485,10 +531,105 @@ export default {
485
531
  type: 'problem',
486
532
  docs: {
487
533
  description:
488
- 'Disallow a raw Tailwind type step; one scale, nine tiers, each carrying its leading and weight.',
534
+ 'Disallow a raw Tailwind type step, or a heading-face tier spelled text-<tier>; one scale of tiers, each carrying its leading, weight and face.',
489
535
  },
490
536
  },
491
- create: stringVisitors(RAW_TEXT_SIZE, RAW_TEXT_SIZE_MESSAGE),
537
+ /** @param {RuleContext} context */
538
+ create(context) {
539
+ const raw = stringVisitors(
540
+ RAW_TEXT_SIZE,
541
+ RAW_TEXT_SIZE_MESSAGE,
542
+ )(context);
543
+ const face = stringVisitors(
544
+ HEADING_FACE_TIER,
545
+ HEADING_FACE_TIER_MESSAGE,
546
+ )(context);
547
+
548
+ return {
549
+ /** @param {StringLiteral} node */
550
+ Literal: (node) => {
551
+ raw.Literal(node);
552
+ face.Literal(node);
553
+ },
554
+ /** @param {TemplateChunk} node */
555
+ TemplateElement: (node) => {
556
+ raw.TemplateElement(node);
557
+ face.TemplateElement(node);
558
+ },
559
+ };
560
+ },
561
+ },
562
+ 'no-raw-button': {
563
+ meta: {
564
+ type: 'problem',
565
+ docs: {
566
+ description:
567
+ 'Disallow a raw <button> outside the kit; use Button, or render a button through a kit component.',
568
+ },
569
+ },
570
+ /** @param {RuleContext} context */
571
+ create(context) {
572
+ return {
573
+ /** @param {JsxElement} node */
574
+ JSXOpeningElement(node) {
575
+ if (jsxName(node.name) !== 'button' || isRenderPropValue(node)) {
576
+ return;
577
+ }
578
+
579
+ context.report({
580
+ node,
581
+ message:
582
+ 'A raw <button> misses the focus ring, disabled and loading states, the size scale and the per-Source retint. Use Button (variant="ghost" or "link", size="xs" for a quiet text action), or give a kit row its button element with render={<button type="button" />} (design-system buttons rule).',
583
+ });
584
+ },
585
+ };
586
+ },
587
+ },
588
+ 'no-hand-drawn-card': {
589
+ meta: {
590
+ type: 'problem',
591
+ docs: {
592
+ description:
593
+ 'Disallow a card surface (bg-card + rounded + shadow or border) drawn by hand on a plain element.',
594
+ },
595
+ },
596
+ /** @param {RuleContext} context */
597
+ create(context) {
598
+ return {
599
+ /** @param {JsxElement} node */
600
+ JSXOpeningElement(node) {
601
+ const name = jsxName(node.name);
602
+
603
+ // Only intrinsic elements: a `Card`, `EmptyState` or other kit
604
+ // component given `bg-card` is restyling a component, which is a
605
+ // different question.
606
+ if (!name || name[0] !== name[0]?.toLowerCase()) return;
607
+
608
+ for (const attribute of node.attributes ?? []) {
609
+ if (
610
+ attribute.type !== 'JSXAttribute' ||
611
+ attribute.name?.name !== 'className'
612
+ ) {
613
+ continue;
614
+ }
615
+
616
+ const classes = classNameStrings(attribute.value)
617
+ .map((literal) =>
618
+ typeof literal.value === 'string' ? literal.value : '',
619
+ )
620
+ .join(' ');
621
+
622
+ if (isHandDrawnCard(classes)) {
623
+ context.report({
624
+ node: attribute,
625
+ message:
626
+ 'This is a card drawn by hand. Use Card (@kit/ui/card; @ekanos/ui/card for partners) with surface="card" or "widget" and layout="padded" or "bleed", and render={<section />} when it must be another element, so the corner, padding and elevation stay the component\u2019s (design-system cards rule).',
627
+ });
628
+ }
629
+ }
630
+ },
631
+ };
632
+ },
492
633
  },
493
634
  },
494
635
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekanos/sdk",
3
- "version": "0.15.1",
3
+ "version": "0.17.0",
4
4
  "private": false,
5
5
  "description": "The official SDK for building Ekanos integrations.",
6
6
  "homepage": "https://sdk.ekanos.dev/sdk/overview",
@@ -77,8 +77,8 @@
77
77
  "access": "public"
78
78
  },
79
79
  "dependencies": {
80
- "@ekanos/integration-schema": "0.15.1",
81
- "@ekanos/ui": "0.15.1",
80
+ "@ekanos/integration-schema": "0.17.0",
81
+ "@ekanos/ui": "0.17.0",
82
82
  "@supabase/supabase-js": "2.87.1",
83
83
  "server-only": "^0.0.1"
84
84
  },