@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, {
|
|
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.
|
|
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.
|
|
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` | `
|
|
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 |
|