@aircall/blocks 0.18.0 → 0.19.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.
package/dist/index.js CHANGED
@@ -1,15 +1,15 @@
1
- import { Anchor, Badge, Button, Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle, CounterBadge, DropdownMenu, DropdownMenuAddon, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, Field, FieldContent, FieldDescription, FieldError, FieldLabel, FieldLabelAside, FieldLabelInfo, FieldLabelRow, HoverCard, HoverCardContent, HoverCardTrigger, InputGroup, InputGroupButton, InputGroupTextarea, Item, ItemActions, ItemContent, ItemGroup, ItemMedia, ItemTitle, Popover, PopoverContent, PopoverTrigger, Separator, Sidebar, SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuAction, SidebarMenuButton, SidebarMenuItem, SidebarProvider, Spinner, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, TabsList, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipTrigger, cn, registerI18nNamespace, useDsLocale, useDsTranslation, useSidebar } from "@aircall/ds";
1
+ import { Anchor, Badge, Button, Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle, CounterBadge, DropdownMenu, DropdownMenuAddon, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, Field, FieldContent, FieldDescription, FieldError, FieldLabel, FieldLabelAside, FieldLabelInfo, FieldLabelRow, HoverCard, HoverCardContent, HoverCardTrigger, InputGroup, InputGroupButton, InputGroupTextarea, Item, ItemActions, ItemContent, ItemGroup, ItemMedia, ItemTitle, Popover, PopoverContent, PopoverTrigger, Separator, Sidebar, SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuAction, SidebarMenuButton, SidebarMenuItem, SidebarProvider, Skeleton, Spinner, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, TabsList, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipTrigger, cn, registerI18nNamespace, useDsLocale, useDsTranslation, useSidebar } from "@aircall/ds";
2
2
  import * as React from "react";
3
3
  import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
4
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
- import { AircallLoader, AircallLogo, AircallMark, ArrowUp, Check, ChevronDown, ChevronLeft, ChevronRight, Copy, Ellipsis, ExternalLink, FileText, Frown, History, LockKeyhole, Meh, Menu, MessageSquare, Minus, PanelLeft, PanelLeftClose, PanelLeftOpen, Plus, Search, Settings, Smile, Sprout, Square, SquareArrowOutUpRight, ThumbsDown, ThumbsDownFilled, ThumbsUp, ThumbsUpFilled, TriangleAlert } from "@aircall/react-icons";
5
+ import { AircallLoader, AircallLogo, AircallMark, ArrowDownRight, ArrowUp, ArrowUpRight, Check, ChevronDown, ChevronLeft, ChevronRight, Copy, Ellipsis, ExternalLink, FileText, Frown, History, LockKeyhole, Meh, Menu, MessageSquare, Minus, PanelLeft, PanelLeftClose, PanelLeftOpen, Plus, Search, Settings, Smile, Sprout, Square, SquareArrowOutUpRight, ThumbsDown, ThumbsDownFilled, ThumbsUp, ThumbsUpFilled, TriangleAlert } from "@aircall/react-icons";
6
6
  import ReactMarkdown from "react-markdown";
7
7
  import remarkGfm from "remark-gfm";
8
8
  import { useCallbackRef, useControllableState, useIsMounted, useUnmount } from "@aircall/hooks";
9
9
  import { cva } from "class-variance-authority";
10
- import { createFormHook, createFormHookContexts } from "@tanstack/react-form";
11
10
  import { mergeProps } from "@base-ui/react/merge-props";
12
11
  import { useRender } from "@base-ui/react/use-render";
12
+ import { createFormHook, createFormHookContexts } from "@tanstack/react-form";
13
13
 
14
14
  //#region src/i18n/locales/de.ts
15
15
  const de = {
@@ -1078,7 +1078,7 @@ CopyButtonLabel.displayName = "CopyButtonLabel";
1078
1078
 
1079
1079
  //#endregion
1080
1080
  //#region src/components/chatbot-response-block.tsx
1081
- function SourcesBadge({ sources }) {
1081
+ function SourcesBadge({ sources, onSourceClick }) {
1082
1082
  const t = useDsTranslation("blocks");
1083
1083
  return /* @__PURE__ */ jsxs(HoverCard, { children: [/* @__PURE__ */ jsx(HoverCardTrigger, {
1084
1084
  render: /* @__PURE__ */ jsx(Button, {
@@ -1098,7 +1098,8 @@ function SourcesBadge({ sources }) {
1098
1098
  href: source.url,
1099
1099
  target: "_blank",
1100
1100
  rel: "noreferrer noopener",
1101
- "aria-label": t("chatbotResponse.openSource", { title: source.title })
1101
+ "aria-label": t("chatbotResponse.openSource", { title: source.title }),
1102
+ onClick: () => onSourceClick?.(source)
1102
1103
  }),
1103
1104
  children: [
1104
1105
  /* @__PURE__ */ jsx(ItemMedia, {
@@ -1187,7 +1188,7 @@ const MARKDOWN_COMPONENTS = {
1187
1188
  * />
1188
1189
  * ```
1189
1190
  */
1190
- function ChatbotResponseBlock({ messageId, markdown, streaming = false, sources, onFeedback, className }) {
1191
+ function ChatbotResponseBlock({ messageId, markdown, streaming = false, sources, onFeedback, onSourceClick, onCopy, className }) {
1191
1192
  const t = useDsTranslation("blocks");
1192
1193
  const [feedback, setFeedback] = useState(null);
1193
1194
  const onFeedbackRef = useCallbackRef(onFeedback);
@@ -1233,7 +1234,10 @@ function ChatbotResponseBlock({ messageId, markdown, streaming = false, sources,
1233
1234
  className: "flex items-center gap-1",
1234
1235
  children: [sources && sources.length > 0 && /* @__PURE__ */ jsx("div", {
1235
1236
  className: "flex flex-1",
1236
- children: /* @__PURE__ */ jsx(SourcesBadge, { sources })
1237
+ children: /* @__PURE__ */ jsx(SourcesBadge, {
1238
+ sources,
1239
+ onSourceClick
1240
+ })
1237
1241
  }), /* @__PURE__ */ jsxs("div", {
1238
1242
  className: "ml-auto flex items-center gap-1",
1239
1243
  children: [
@@ -1242,6 +1246,7 @@ function ChatbotResponseBlock({ messageId, markdown, streaming = false, sources,
1242
1246
  variant: "ghost",
1243
1247
  size: "icon-sm",
1244
1248
  "aria-label": t("chatbotResponse.copy"),
1249
+ onCopied: onCopy,
1245
1250
  children: /* @__PURE__ */ jsx(CopyButtonIcon, {})
1246
1251
  }),
1247
1252
  /* @__PURE__ */ jsx(Button, {
@@ -2523,6 +2528,380 @@ const ScoreBadge = React.forwardRef((componentProps, forwardRef) => {
2523
2528
  });
2524
2529
  ScoreBadge.displayName = "ScoreBadge";
2525
2530
 
2531
+ //#endregion
2532
+ //#region src/helpers/format-kpi.ts
2533
+ /** Empty KPI placeholder matching Figma empty tiles and ScoreBadge. */
2534
+ const KPI_EMPTY = "--";
2535
+ /** Default secondary unit for clock durations. */
2536
+ function clockDurationUnit(scale) {
2537
+ return scale === "hours" ? "hr/min" : "min/sec";
2538
+ }
2539
+ /**
2540
+ * Locale decimal separator (e.g. `.` for `en-US`, `,` for `fr-FR`).
2541
+ */
2542
+ function getDecimalSeparator(locale) {
2543
+ return new Intl.NumberFormat(locale).formatToParts(1.1).find((p) => p.type === "decimal")?.value ?? ".";
2544
+ }
2545
+ /**
2546
+ * Split a locale-formatted number on its decimal separator so the integer group
2547
+ * can use primary typography and the fraction (+ unit) secondary typography.
2548
+ */
2549
+ function splitFormattedNumber(formatted, locale) {
2550
+ const dec = getDecimalSeparator(locale);
2551
+ const idx = formatted.indexOf(dec);
2552
+ if (idx === -1) return {
2553
+ primary: formatted,
2554
+ secondary: ""
2555
+ };
2556
+ return {
2557
+ primary: formatted.slice(0, idx),
2558
+ secondary: formatted.slice(idx)
2559
+ };
2560
+ }
2561
+ /**
2562
+ * Split a duration string on the leading digit run (`"1h 2min 3s"` → `"1"` + `"h 2min 3s"`).
2563
+ */
2564
+ function splitLeadingDigits(text) {
2565
+ const match = /^(\d+)([\s\S]*)$/.exec(text);
2566
+ if (!match) return null;
2567
+ return {
2568
+ primary: match[1],
2569
+ secondary: match[2]
2570
+ };
2571
+ }
2572
+ /**
2573
+ * Format seconds into a readable duration (e.g. `1h 2min 3s`).
2574
+ * Implemented without `date-fns`.
2575
+ *
2576
+ * @example
2577
+ * formatSecondsToDuration(0) // => "0s"
2578
+ * formatSecondsToDuration(60) // => "1min 0s"
2579
+ * formatSecondsToDuration(3600) // => "1h 0min 0s"
2580
+ */
2581
+ function formatSecondsToDuration(totalSeconds) {
2582
+ const s = Math.max(0, Math.floor(totalSeconds));
2583
+ const days = Math.floor(s / 86400);
2584
+ const hours = Math.floor(s % 86400 / 3600);
2585
+ const minutes = Math.floor(s % 3600 / 60);
2586
+ const seconds = s % 60;
2587
+ if (days) return `${days}d ${hours}h ${minutes}min ${seconds}s`;
2588
+ if (hours) return `${hours}h ${minutes}min ${seconds}s`;
2589
+ if (minutes) return `${minutes}min ${seconds}s`;
2590
+ return `${seconds}s`;
2591
+ }
2592
+ /**
2593
+ * Format seconds as a zero-padded clock duration (`HH:MM` / `MM:SS`).
2594
+ * The full clock string is primary typography; the unit is appended separately.
2595
+ *
2596
+ * @example
2597
+ * formatSecondsToClock(75180, 'hours') // => { primary: "20:53", secondary: "" }
2598
+ * formatSecondsToClock(304, 'minutes') // => { primary: "05:04", secondary: "" }
2599
+ */
2600
+ function formatSecondsToClock(totalSeconds, scale) {
2601
+ const s = Math.max(0, Math.floor(totalSeconds));
2602
+ if (scale === "hours") {
2603
+ const hours = Math.floor(s / 3600);
2604
+ const minutes$1 = Math.floor(s % 3600 / 60);
2605
+ return {
2606
+ primary: `${String(hours).padStart(2, "0")}:${String(minutes$1).padStart(2, "0")}`,
2607
+ secondary: ""
2608
+ };
2609
+ }
2610
+ const minutes = Math.floor(s / 60);
2611
+ const seconds = s % 60;
2612
+ return {
2613
+ primary: `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`,
2614
+ secondary: ""
2615
+ };
2616
+ }
2617
+ /**
2618
+ * Format a KPI `value` for split typography display.
2619
+ * Returns `null` for empty / non-finite values (caller renders {@link KPI_EMPTY}).
2620
+ *
2621
+ * Contract:
2622
+ * - `string` is always opaque text (`kind` is ignored). Coerce at the call site
2623
+ * if the API sent a numeric string and you need `number` / `percent` / `duration`.
2624
+ * - `number` / `percent` / `duration` require a finite `number`.
2625
+ */
2626
+ function formatKpiValue(value, kind, locale, { formatOptions, durationStyle = "long", durationScale = "minutes" } = {}) {
2627
+ if (kind === "text" || typeof value === "string") {
2628
+ const text = String(value);
2629
+ if (text === "") return null;
2630
+ return {
2631
+ primary: text,
2632
+ secondary: ""
2633
+ };
2634
+ }
2635
+ if (!Number.isFinite(value)) return null;
2636
+ if (kind === "duration") {
2637
+ if (durationStyle === "clock") return formatSecondsToClock(value, durationScale);
2638
+ const formatted = formatSecondsToDuration(value);
2639
+ return splitLeadingDigits(formatted) ?? {
2640
+ primary: formatted,
2641
+ secondary: ""
2642
+ };
2643
+ }
2644
+ const options = kind === "percent" ? {
2645
+ style: "percent",
2646
+ maximumFractionDigits: 1,
2647
+ ...formatOptions
2648
+ } : {
2649
+ maximumFractionDigits: 1,
2650
+ minimumFractionDigits: 0,
2651
+ ...formatOptions
2652
+ };
2653
+ return splitFormattedNumber(new Intl.NumberFormat(locale, options).format(kind === "percent" ? value / 100 : value), locale);
2654
+ }
2655
+
2656
+ //#endregion
2657
+ //#region src/components/kpi.tsx
2658
+ const TREND_ICON = {
2659
+ up: ArrowUpRight,
2660
+ down: ArrowDownRight,
2661
+ neutral: Minus
2662
+ };
2663
+ const TONE_CLASS = {
2664
+ positive: "text-chart-1",
2665
+ negative: "text-destructive",
2666
+ neutral: "text-muted-foreground"
2667
+ };
2668
+ const KpiCardContext = React.createContext(null);
2669
+ function useKpiCardDisabled() {
2670
+ return React.useContext(KpiCardContext)?.disabled ?? false;
2671
+ }
2672
+ function resolveKind(value, kind) {
2673
+ if (typeof value === "string") return "text";
2674
+ if (kind) return kind;
2675
+ return "number";
2676
+ }
2677
+ function TrendGlyph({ trend, tone, className }) {
2678
+ const Icon = TREND_ICON[trend];
2679
+ return /* @__PURE__ */ jsx(Icon, {
2680
+ "aria-hidden": "true",
2681
+ className: cn("shrink-0", TONE_CLASS[tone], className)
2682
+ });
2683
+ }
2684
+ /**
2685
+ * KPI tile container. Thin wrapper around DS `Card` (`size="sm"` by default)
2686
+ * that provides a `disabled` context for nested {@link KpiValue}s.
2687
+ * When `disabled`, applies `opacity-50` and `cursor-not-allowed`, and blocks
2688
+ * activation of nested interactive values.
2689
+ * Compose with DS `CardHeader` / `CardTitle` / `CardContent` / `CardAction`,
2690
+ * {@link KpiValue}, and {@link KpiDescription}.
2691
+ *
2692
+ * @example
2693
+ * ```tsx
2694
+ * import { CardHeader, CardTitle, CardContent } from '@aircall/ds';
2695
+ * import { KpiCard, KpiValue, KpiDescription } from '@aircall/blocks';
2696
+ *
2697
+ * <KpiCard>
2698
+ * <CardHeader>
2699
+ * <CardTitle>Answered</CardTitle>
2700
+ * </CardHeader>
2701
+ * <CardContent className="flex flex-col gap-1">
2702
+ * <KpiValue value={123.1} kind="percent" trend="up" tone="positive" />
2703
+ * <KpiDescription trend="up" tone="positive">+2.48% vs previous period</KpiDescription>
2704
+ * </CardContent>
2705
+ * </KpiCard>
2706
+ * ```
2707
+ */
2708
+ const KpiCard = React.forwardRef((componentProps, forwardRef) => {
2709
+ const { className, size = "sm", disabled = false, children, ...props } = componentProps;
2710
+ return /* @__PURE__ */ jsx(KpiCardContext.Provider, {
2711
+ value: { disabled },
2712
+ children: /* @__PURE__ */ jsx(Card, {
2713
+ ref: forwardRef,
2714
+ "data-slot": "kpi-card",
2715
+ "data-disabled": disabled || void 0,
2716
+ "aria-disabled": disabled || void 0,
2717
+ size,
2718
+ className: cn("gap-2 data-[size=sm]:gap-2", disabled && "cursor-not-allowed opacity-50", className),
2719
+ ...props,
2720
+ children
2721
+ })
2722
+ });
2723
+ });
2724
+ KpiCard.displayName = "KpiCard";
2725
+ /**
2726
+ * KPI metric value. Formats via `useDsLocale` and renders primary (and optional
2727
+ * secondary) typography, with trend variants. Passing `onClick` makes the value
2728
+ * interactive (button + dotted underline). Prefer {@link KpiCard}`s `disabled`
2729
+ * for tile-level muting; a local `disabled` still works for standalone use.
2730
+ * Compose inside DS `CardContent` with {@link KpiDescription}.
2731
+ *
2732
+ * Prop naming: `kind` + `formatOptions` for formatting; `trend` is direction
2733
+ * (icon); `tone` is meaning (color) — "up" is not always good.
2734
+ *
2735
+ * @example
2736
+ * ```tsx
2737
+ * <KpiValue value={123.1} kind="percent" trend="up" tone="positive" />
2738
+ * ```
2739
+ */
2740
+ const KpiValue = React.forwardRef((componentProps, forwardRef) => {
2741
+ const { className, value, kind: kindProp, formatOptions, durationStyle = "long", durationScale = "minutes", unit, trend, tone = "neutral", active = false, disabled: disabledProp = false, children, onClick, render, ...props } = componentProps;
2742
+ const locale = useDsLocale();
2743
+ const cardDisabled = useKpiCardDisabled();
2744
+ const isDisabled = Boolean(disabledProp || cardDisabled);
2745
+ const isInteractive = typeof onClick === "function";
2746
+ const kind = resolveKind(value, kindProp);
2747
+ let content;
2748
+ const valueUnderlineClassName = cn(isInteractive && "border-b border-current", isInteractive && !active && "border-dotted", isInteractive && active && "border-solid");
2749
+ const trendGlyph = trend ? /* @__PURE__ */ jsx(TrendGlyph, {
2750
+ trend,
2751
+ tone: disabledProp && !cardDisabled ? "neutral" : tone,
2752
+ className: "size-6"
2753
+ }) : null;
2754
+ if (children != null) content = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
2755
+ className: cn("inline-flex items-baseline gap-0.5", valueUnderlineClassName),
2756
+ children: [/* @__PURE__ */ jsx("span", {
2757
+ className: "text-3xl font-bold",
2758
+ children
2759
+ }), unit != null && unit !== "" && /* @__PURE__ */ jsx("span", {
2760
+ className: "pt-2 text-xl leading-none font-semibold",
2761
+ children: unit
2762
+ })]
2763
+ }), trendGlyph] });
2764
+ else {
2765
+ const formatted = value == null || typeof value === "number" && !Number.isFinite(value) ? null : formatKpiValue(value, kind, locale, {
2766
+ formatOptions,
2767
+ durationStyle,
2768
+ durationScale
2769
+ });
2770
+ const resolvedUnit = unit !== void 0 ? unit : kind === "duration" && durationStyle === "clock" ? clockDurationUnit(durationScale) : void 0;
2771
+ const unitNode = resolvedUnit != null && resolvedUnit !== "" ? resolvedUnit : null;
2772
+ if (!formatted) content = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
2773
+ className: cn("text-3xl font-bold", valueUnderlineClassName),
2774
+ children: KPI_EMPTY
2775
+ }), trendGlyph] });
2776
+ else {
2777
+ const hasSecondary = formatted.secondary !== "" || unitNode != null;
2778
+ content = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
2779
+ className: cn("inline-flex items-baseline gap-0.5", valueUnderlineClassName),
2780
+ children: [/* @__PURE__ */ jsx("span", {
2781
+ className: "text-3xl font-bold",
2782
+ children: formatted.primary
2783
+ }), hasSecondary && /* @__PURE__ */ jsxs("span", {
2784
+ className: "pt-2 text-xl leading-none font-semibold",
2785
+ children: [
2786
+ formatted.secondary,
2787
+ formatted.secondary !== "" && unitNode != null ? " " : null,
2788
+ unitNode
2789
+ ]
2790
+ })]
2791
+ }), trendGlyph] });
2792
+ }
2793
+ }
2794
+ const rootClassName = cn("inline-flex w-fit items-center gap-0.5 font-sans", disabledProp && !cardDisabled ? "text-muted-foreground" : active && isInteractive && !isDisabled ? "text-primary" : "text-foreground", isInteractive && !isDisabled && cn("cursor-pointer border-0 bg-transparent p-0 transition-colors active:opacity-60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50", active ? "hover:text-primary/80" : "hover:text-muted-foreground"), isInteractive && isDisabled && "cursor-not-allowed border-0 bg-transparent p-0", className);
2795
+ return useRender({
2796
+ defaultTagName: isInteractive ? "button" : "div",
2797
+ props: mergeProps({
2798
+ ref: forwardRef,
2799
+ className: rootClassName,
2800
+ children: content,
2801
+ ...isInteractive ? {
2802
+ type: "button",
2803
+ onClick,
2804
+ disabled: isDisabled,
2805
+ "aria-pressed": active
2806
+ } : isDisabled ? { "aria-disabled": true } : {}
2807
+ }, props),
2808
+ render,
2809
+ state: {
2810
+ slot: "kpi-value",
2811
+ active,
2812
+ disabled: isDisabled,
2813
+ interactive: isInteractive,
2814
+ trend,
2815
+ tone
2816
+ }
2817
+ });
2818
+ });
2819
+ KpiValue.displayName = "KpiValue";
2820
+ /**
2821
+ * Supporting comparison / context line under a {@link KpiValue}. Compose as a
2822
+ * sibling inside DS `CardContent` — not the metric title (`CardTitle` owns that).
2823
+ *
2824
+ * Text stays foreground; `trend` / `tone` only color the trailing icon so an
2825
+ * upward change that is bad uses `trend="up" tone="negative"`.
2826
+ *
2827
+ * @example
2828
+ * ```tsx
2829
+ * <KpiDescription trend="up" tone="positive">+2.48% vs previous period</KpiDescription>
2830
+ * <KpiDescription trend="up" tone="negative">+12% abandoned</KpiDescription>
2831
+ * ```
2832
+ */
2833
+ const KpiDescription = React.forwardRef(function KpiDescription(componentProps, forwardRef) {
2834
+ const { className, trend, tone = "neutral", children, ...props } = componentProps;
2835
+ return /* @__PURE__ */ jsxs("div", {
2836
+ ref: forwardRef,
2837
+ "data-slot": "kpi-description",
2838
+ "data-trend": trend,
2839
+ "data-tone": tone,
2840
+ className: cn("inline-flex items-center gap-0.5 font-sans text-xs font-medium text-foreground", className),
2841
+ ...props,
2842
+ children: [children, trend ? /* @__PURE__ */ jsx(TrendGlyph, {
2843
+ trend,
2844
+ tone,
2845
+ className: "size-3"
2846
+ }) : null]
2847
+ });
2848
+ });
2849
+ KpiDescription.displayName = "KpiDescription";
2850
+ /**
2851
+ * Loading placeholder sized for a {@link KpiValue} row. Thin wrapper around
2852
+ * DS `Skeleton` (`h-4 w-[45px]` with vertical margin to match the value row).
2853
+ * Swap in place of {@link KpiValue} while data loads.
2854
+ *
2855
+ * @example
2856
+ * ```tsx
2857
+ * <CardContent>
2858
+ * {loading ? <KpiValueSkeleton /> : <KpiValue value={123} />}
2859
+ * </CardContent>
2860
+ * ```
2861
+ */
2862
+ const KpiValueSkeleton = React.forwardRef((componentProps, forwardRef) => {
2863
+ const { className, ...props } = componentProps;
2864
+ return /* @__PURE__ */ jsx(Skeleton, {
2865
+ ref: forwardRef,
2866
+ "data-slot": "kpi-value-skeleton",
2867
+ className: cn("my-[9px] h-4 w-[45px]", className),
2868
+ ...props
2869
+ });
2870
+ });
2871
+ KpiValueSkeleton.displayName = "KpiValueSkeleton";
2872
+ /**
2873
+ * Loading placeholder sized for a {@link KpiDescription} row. Thin wrapper
2874
+ * around DS `Skeleton` (`12×144`, matching `text-xs` comparison copy).
2875
+ * Swap in place of {@link KpiDescription} while data loads.
2876
+ *
2877
+ * @example
2878
+ * ```tsx
2879
+ * <CardContent className="flex flex-col gap-1">
2880
+ * {loading ? (
2881
+ * <>
2882
+ * <KpiValueSkeleton />
2883
+ * <KpiDescriptionSkeleton />
2884
+ * </>
2885
+ * ) : (
2886
+ * <>
2887
+ * <KpiValue value={123} />
2888
+ * <KpiDescription>+2.48% vs previous period</KpiDescription>
2889
+ * </>
2890
+ * )}
2891
+ * </CardContent>
2892
+ * ```
2893
+ */
2894
+ const KpiDescriptionSkeleton = React.forwardRef((componentProps, forwardRef) => {
2895
+ const { className, ...props } = componentProps;
2896
+ return /* @__PURE__ */ jsx(Skeleton, {
2897
+ ref: forwardRef,
2898
+ "data-slot": "kpi-description-skeleton",
2899
+ className: cn("h-3 w-36", className),
2900
+ ...props
2901
+ });
2902
+ });
2903
+ KpiDescriptionSkeleton.displayName = "KpiDescriptionSkeleton";
2904
+
2526
2905
  //#endregion
2527
2906
  //#region src/components/sentiment-badge.tsx
2528
2907
  const SENTIMENT_BADGE = {
@@ -4051,4 +4430,4 @@ function DashboardSidebarNav({ groups, renderLink }) {
4051
4430
  }
4052
4431
 
4053
4432
  //#endregion
4054
- export { BetaBadge, CardSaveBar, ChatbotExpandSuggestion, ChatbotInput, ChatbotPage, ChatbotPageBody, ChatbotPageContent, ChatbotPageConversation, ChatbotPageFooter, ChatbotPageHeading, ChatbotPageInputBar, ChatbotPanel, ChatbotPanelHeader, ChatbotPanelSuggestion, ChatbotPanelTrigger, ChatbotResponseBlock, ChatbotResponseLoading, ChatbotSidebar, ChatbotUserMessage, ComingSoonDescription, ComingSoonEmptyState, ComingSoonMedia, ComingSoonTitle, CommonForm, CopyButton, CopyButtonIcon, CopyButtonLabel, DashboardPage, DashboardPageBanner, DashboardPageContent, DashboardPageHeader, DashboardPageHeaderAction, DashboardPageHeaderActions, DashboardPageHeaderDescription, DashboardPageHeaderNav, DashboardPageHeaderNavBack, DashboardPageHeaderPrefix, DashboardPageHeaderSubtitle, DashboardPageHeaderTitle, DashboardPageHeaderTitleGroup, DashboardPageMain, DashboardPageTabs, DashboardSidebar, DashboardSidebarContent, DashboardSidebarFooter, DashboardSidebarGroup, DashboardSidebarGroupLabel, DashboardSidebarHeader, DashboardSidebarMenu, DashboardSidebarMenuBadge, DashboardSidebarMenuButton, DashboardSidebarMenuItem, DashboardSidebarNav, DashboardSidebarProvider, DashboardSidebarSubmenu, DashboardSidebarSubmenuItem, DashboardSidebarSubmenuSeparator, DashboardSidebarTrigger, DashboardStandalonePage, DashboardStandalonePageAction, DashboardStandalonePageActions, DashboardStandalonePageContent, DashboardStandalonePageDescription, DashboardStandalonePageHeader, DashboardStandalonePageMain, DashboardStandalonePageTitle, DeprecatedBadge, EmptyState, EmptyStateActions, EmptyStateButton, EmptyStateDescription, EmptyStateExternalLink, EmptyStateHeader, EmptyStateMedia, EmptyStateTitle, FormArrayField, FormComboboxField, FormFieldBase, FormFieldShell, FormInputField, FormMultiComboboxField, FormNumericField, FormOTPField, FormRadioGroupField, FormSelectField, FormSliderField, FormSwitchField, FormTextareaField, FormToggleGroupField, NewBadge, NoDataDescription, NoDataEmptyState, NoDataMedia, NoDataTitle, NoSupportDescription, NoSupportEmptyState, NoSupportMedia, NoSupportTitle, NotFoundDescription, NotFoundEmptyState, NotFoundMedia, NotFoundTitle, PERCENTAGE_THRESHOLDS, ProfessionalBadge, RestrictedAccessDescription, RestrictedAccessEmptyState, RestrictedAccessMedia, RestrictedAccessTitle, RoleBadge, SaveBar, ScoreBadge, SentimentBadge, SettingCard, SettingCardAction, SettingCardContent, SettingCardDescription, SettingCardHeader, SettingCardTitle, SubmitButton, TrialBadge, UnknownErrorDescription, UnknownErrorEmptyState, UnknownErrorMedia, UnknownErrorTitle, fieldContext, formContext, toFieldErrors, useCopyToClipboard, useSidebar as useDashboardSidebar, useFieldContext, useForm, useFormContext, withFieldGroup, withForm };
4433
+ export { BetaBadge, CardSaveBar, ChatbotExpandSuggestion, ChatbotInput, ChatbotPage, ChatbotPageBody, ChatbotPageContent, ChatbotPageConversation, ChatbotPageFooter, ChatbotPageHeading, ChatbotPageInputBar, ChatbotPanel, ChatbotPanelHeader, ChatbotPanelSuggestion, ChatbotPanelTrigger, ChatbotResponseBlock, ChatbotResponseLoading, ChatbotSidebar, ChatbotUserMessage, ComingSoonDescription, ComingSoonEmptyState, ComingSoonMedia, ComingSoonTitle, CommonForm, CopyButton, CopyButtonIcon, CopyButtonLabel, DashboardPage, DashboardPageBanner, DashboardPageContent, DashboardPageHeader, DashboardPageHeaderAction, DashboardPageHeaderActions, DashboardPageHeaderDescription, DashboardPageHeaderNav, DashboardPageHeaderNavBack, DashboardPageHeaderPrefix, DashboardPageHeaderSubtitle, DashboardPageHeaderTitle, DashboardPageHeaderTitleGroup, DashboardPageMain, DashboardPageTabs, DashboardSidebar, DashboardSidebarContent, DashboardSidebarFooter, DashboardSidebarGroup, DashboardSidebarGroupLabel, DashboardSidebarHeader, DashboardSidebarMenu, DashboardSidebarMenuBadge, DashboardSidebarMenuButton, DashboardSidebarMenuItem, DashboardSidebarNav, DashboardSidebarProvider, DashboardSidebarSubmenu, DashboardSidebarSubmenuItem, DashboardSidebarSubmenuSeparator, DashboardSidebarTrigger, DashboardStandalonePage, DashboardStandalonePageAction, DashboardStandalonePageActions, DashboardStandalonePageContent, DashboardStandalonePageDescription, DashboardStandalonePageHeader, DashboardStandalonePageMain, DashboardStandalonePageTitle, DeprecatedBadge, EmptyState, EmptyStateActions, EmptyStateButton, EmptyStateDescription, EmptyStateExternalLink, EmptyStateHeader, EmptyStateMedia, EmptyStateTitle, FormArrayField, FormComboboxField, FormFieldBase, FormFieldShell, FormInputField, FormMultiComboboxField, FormNumericField, FormOTPField, FormRadioGroupField, FormSelectField, FormSliderField, FormSwitchField, FormTextareaField, FormToggleGroupField, KpiCard, KpiDescription, KpiDescriptionSkeleton, KpiValue, KpiValueSkeleton, NewBadge, NoDataDescription, NoDataEmptyState, NoDataMedia, NoDataTitle, NoSupportDescription, NoSupportEmptyState, NoSupportMedia, NoSupportTitle, NotFoundDescription, NotFoundEmptyState, NotFoundMedia, NotFoundTitle, PERCENTAGE_THRESHOLDS, ProfessionalBadge, RestrictedAccessDescription, RestrictedAccessEmptyState, RestrictedAccessMedia, RestrictedAccessTitle, RoleBadge, SaveBar, ScoreBadge, SentimentBadge, SettingCard, SettingCardAction, SettingCardContent, SettingCardDescription, SettingCardHeader, SettingCardTitle, SubmitButton, TrialBadge, UnknownErrorDescription, UnknownErrorEmptyState, UnknownErrorMedia, UnknownErrorTitle, fieldContext, formContext, toFieldErrors, useCopyToClipboard, useSidebar as useDashboardSidebar, useFieldContext, useForm, useFormContext, withFieldGroup, withForm };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aircall/blocks",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "sideEffects": [
@@ -82,7 +82,7 @@
82
82
  "vite": "7.3.1",
83
83
  "vitest": "4.0.17",
84
84
  "zod": "4.4.3",
85
- "@aircall/ds": "0.28.0",
85
+ "@aircall/ds": "0.28.1",
86
86
  "@aircall/hooks": "0.6.0"
87
87
  },
88
88
  "keywords": [
@@ -106,7 +106,7 @@ Components whose migration decision is still **TBD**, **TODO**, or **Ready for d
106
106
  | `FormWizard` + `useFormWizard` + `FormField` | `useForm`, `CommonForm`, `FormInputField`/`FormSelectField`/`FormComboboxField`/etc., `CardSaveBar`/`SaveBar` (@aircall/blocks) | load @aircall/blocks#aircall-blocks/migrate-dashboard/form-wizard | available |
107
107
  | `Loading` | `Spinner` (and `Skeleton` for content placeholders) (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/loading | available |
108
108
  | `List` + `ListItem` | `ItemGroup`, `Item`, `ItemMedia`, `ItemContent`, `ItemActions` (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/list | available |
109
- | `Tile` + `TileHeader` + `TileValue` | `Card` (+ parts) as the tile container (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/tile | available |
109
+ | `Tile` + `TileHeader` + `TileValue` | `KpiCard` + `KpiValue` + `KpiDescription` (+ `KpiValueSkeleton` / `KpiDescriptionSkeleton`) with DS `CardHeader` / `CardTitle` / `CardContent` / `CardAction` (@aircall/blocks + @aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/tile | available |
110
110
  | `GridLayout` + `GridItem` + `Gap` | native `<div>` + Tailwind grid/flex/gap utilities (NO component import needed — these are layout primitives) (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/layout | available |
111
111
  | `AudioPlayer` | `Audio`, `AudioPlayerRow`, `AudioPlayerButton`, `AudioPlayerBar`, `AudioPlayerTime`, `AudioPlayerSpeed`, `AudioPlayerSkip`, `AudioPlayerManager`, `useAudioPlayer` (@aircall/ds) | load @aircall/ds#aircall-ds/migrate-audio-player | available |
112
112
  | `InfoPopup` + `InfoPopupTrigger` + `InfoPopupContent` | `HoverCard`, `HoverCardTrigger`, `HoverCardContent` (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/info-popup | available |