@aircall/blocks 0.18.0 → 0.18.1
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 = {
|
|
@@ -2523,6 +2523,380 @@ const ScoreBadge = React.forwardRef((componentProps, forwardRef) => {
|
|
|
2523
2523
|
});
|
|
2524
2524
|
ScoreBadge.displayName = "ScoreBadge";
|
|
2525
2525
|
|
|
2526
|
+
//#endregion
|
|
2527
|
+
//#region src/helpers/format-kpi.ts
|
|
2528
|
+
/** Empty KPI placeholder matching Figma empty tiles and ScoreBadge. */
|
|
2529
|
+
const KPI_EMPTY = "--";
|
|
2530
|
+
/** Default secondary unit for clock durations. */
|
|
2531
|
+
function clockDurationUnit(scale) {
|
|
2532
|
+
return scale === "hours" ? "hr/min" : "min/sec";
|
|
2533
|
+
}
|
|
2534
|
+
/**
|
|
2535
|
+
* Locale decimal separator (e.g. `.` for `en-US`, `,` for `fr-FR`).
|
|
2536
|
+
*/
|
|
2537
|
+
function getDecimalSeparator(locale) {
|
|
2538
|
+
return new Intl.NumberFormat(locale).formatToParts(1.1).find((p) => p.type === "decimal")?.value ?? ".";
|
|
2539
|
+
}
|
|
2540
|
+
/**
|
|
2541
|
+
* Split a locale-formatted number on its decimal separator so the integer group
|
|
2542
|
+
* can use primary typography and the fraction (+ unit) secondary typography.
|
|
2543
|
+
*/
|
|
2544
|
+
function splitFormattedNumber(formatted, locale) {
|
|
2545
|
+
const dec = getDecimalSeparator(locale);
|
|
2546
|
+
const idx = formatted.indexOf(dec);
|
|
2547
|
+
if (idx === -1) return {
|
|
2548
|
+
primary: formatted,
|
|
2549
|
+
secondary: ""
|
|
2550
|
+
};
|
|
2551
|
+
return {
|
|
2552
|
+
primary: formatted.slice(0, idx),
|
|
2553
|
+
secondary: formatted.slice(idx)
|
|
2554
|
+
};
|
|
2555
|
+
}
|
|
2556
|
+
/**
|
|
2557
|
+
* Split a duration string on the leading digit run (`"1h 2min 3s"` → `"1"` + `"h 2min 3s"`).
|
|
2558
|
+
*/
|
|
2559
|
+
function splitLeadingDigits(text) {
|
|
2560
|
+
const match = /^(\d+)([\s\S]*)$/.exec(text);
|
|
2561
|
+
if (!match) return null;
|
|
2562
|
+
return {
|
|
2563
|
+
primary: match[1],
|
|
2564
|
+
secondary: match[2]
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
/**
|
|
2568
|
+
* Format seconds into a readable duration (e.g. `1h 2min 3s`).
|
|
2569
|
+
* Implemented without `date-fns`.
|
|
2570
|
+
*
|
|
2571
|
+
* @example
|
|
2572
|
+
* formatSecondsToDuration(0) // => "0s"
|
|
2573
|
+
* formatSecondsToDuration(60) // => "1min 0s"
|
|
2574
|
+
* formatSecondsToDuration(3600) // => "1h 0min 0s"
|
|
2575
|
+
*/
|
|
2576
|
+
function formatSecondsToDuration(totalSeconds) {
|
|
2577
|
+
const s = Math.max(0, Math.floor(totalSeconds));
|
|
2578
|
+
const days = Math.floor(s / 86400);
|
|
2579
|
+
const hours = Math.floor(s % 86400 / 3600);
|
|
2580
|
+
const minutes = Math.floor(s % 3600 / 60);
|
|
2581
|
+
const seconds = s % 60;
|
|
2582
|
+
if (days) return `${days}d ${hours}h ${minutes}min ${seconds}s`;
|
|
2583
|
+
if (hours) return `${hours}h ${minutes}min ${seconds}s`;
|
|
2584
|
+
if (minutes) return `${minutes}min ${seconds}s`;
|
|
2585
|
+
return `${seconds}s`;
|
|
2586
|
+
}
|
|
2587
|
+
/**
|
|
2588
|
+
* Format seconds as a zero-padded clock duration (`HH:MM` / `MM:SS`).
|
|
2589
|
+
* The full clock string is primary typography; the unit is appended separately.
|
|
2590
|
+
*
|
|
2591
|
+
* @example
|
|
2592
|
+
* formatSecondsToClock(75180, 'hours') // => { primary: "20:53", secondary: "" }
|
|
2593
|
+
* formatSecondsToClock(304, 'minutes') // => { primary: "05:04", secondary: "" }
|
|
2594
|
+
*/
|
|
2595
|
+
function formatSecondsToClock(totalSeconds, scale) {
|
|
2596
|
+
const s = Math.max(0, Math.floor(totalSeconds));
|
|
2597
|
+
if (scale === "hours") {
|
|
2598
|
+
const hours = Math.floor(s / 3600);
|
|
2599
|
+
const minutes$1 = Math.floor(s % 3600 / 60);
|
|
2600
|
+
return {
|
|
2601
|
+
primary: `${String(hours).padStart(2, "0")}:${String(minutes$1).padStart(2, "0")}`,
|
|
2602
|
+
secondary: ""
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2605
|
+
const minutes = Math.floor(s / 60);
|
|
2606
|
+
const seconds = s % 60;
|
|
2607
|
+
return {
|
|
2608
|
+
primary: `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`,
|
|
2609
|
+
secondary: ""
|
|
2610
|
+
};
|
|
2611
|
+
}
|
|
2612
|
+
/**
|
|
2613
|
+
* Format a KPI `value` for split typography display.
|
|
2614
|
+
* Returns `null` for empty / non-finite values (caller renders {@link KPI_EMPTY}).
|
|
2615
|
+
*
|
|
2616
|
+
* Contract:
|
|
2617
|
+
* - `string` is always opaque text (`kind` is ignored). Coerce at the call site
|
|
2618
|
+
* if the API sent a numeric string and you need `number` / `percent` / `duration`.
|
|
2619
|
+
* - `number` / `percent` / `duration` require a finite `number`.
|
|
2620
|
+
*/
|
|
2621
|
+
function formatKpiValue(value, kind, locale, { formatOptions, durationStyle = "long", durationScale = "minutes" } = {}) {
|
|
2622
|
+
if (kind === "text" || typeof value === "string") {
|
|
2623
|
+
const text = String(value);
|
|
2624
|
+
if (text === "") return null;
|
|
2625
|
+
return {
|
|
2626
|
+
primary: text,
|
|
2627
|
+
secondary: ""
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
2630
|
+
if (!Number.isFinite(value)) return null;
|
|
2631
|
+
if (kind === "duration") {
|
|
2632
|
+
if (durationStyle === "clock") return formatSecondsToClock(value, durationScale);
|
|
2633
|
+
const formatted = formatSecondsToDuration(value);
|
|
2634
|
+
return splitLeadingDigits(formatted) ?? {
|
|
2635
|
+
primary: formatted,
|
|
2636
|
+
secondary: ""
|
|
2637
|
+
};
|
|
2638
|
+
}
|
|
2639
|
+
const options = kind === "percent" ? {
|
|
2640
|
+
style: "percent",
|
|
2641
|
+
maximumFractionDigits: 1,
|
|
2642
|
+
...formatOptions
|
|
2643
|
+
} : {
|
|
2644
|
+
maximumFractionDigits: 1,
|
|
2645
|
+
minimumFractionDigits: 0,
|
|
2646
|
+
...formatOptions
|
|
2647
|
+
};
|
|
2648
|
+
return splitFormattedNumber(new Intl.NumberFormat(locale, options).format(kind === "percent" ? value / 100 : value), locale);
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
//#endregion
|
|
2652
|
+
//#region src/components/kpi.tsx
|
|
2653
|
+
const TREND_ICON = {
|
|
2654
|
+
up: ArrowUpRight,
|
|
2655
|
+
down: ArrowDownRight,
|
|
2656
|
+
neutral: Minus
|
|
2657
|
+
};
|
|
2658
|
+
const TONE_CLASS = {
|
|
2659
|
+
positive: "text-chart-1",
|
|
2660
|
+
negative: "text-destructive",
|
|
2661
|
+
neutral: "text-muted-foreground"
|
|
2662
|
+
};
|
|
2663
|
+
const KpiCardContext = React.createContext(null);
|
|
2664
|
+
function useKpiCardDisabled() {
|
|
2665
|
+
return React.useContext(KpiCardContext)?.disabled ?? false;
|
|
2666
|
+
}
|
|
2667
|
+
function resolveKind(value, kind) {
|
|
2668
|
+
if (typeof value === "string") return "text";
|
|
2669
|
+
if (kind) return kind;
|
|
2670
|
+
return "number";
|
|
2671
|
+
}
|
|
2672
|
+
function TrendGlyph({ trend, tone, className }) {
|
|
2673
|
+
const Icon = TREND_ICON[trend];
|
|
2674
|
+
return /* @__PURE__ */ jsx(Icon, {
|
|
2675
|
+
"aria-hidden": "true",
|
|
2676
|
+
className: cn("shrink-0", TONE_CLASS[tone], className)
|
|
2677
|
+
});
|
|
2678
|
+
}
|
|
2679
|
+
/**
|
|
2680
|
+
* KPI tile container. Thin wrapper around DS `Card` (`size="sm"` by default)
|
|
2681
|
+
* that provides a `disabled` context for nested {@link KpiValue}s.
|
|
2682
|
+
* When `disabled`, applies `opacity-50` and `cursor-not-allowed`, and blocks
|
|
2683
|
+
* activation of nested interactive values.
|
|
2684
|
+
* Compose with DS `CardHeader` / `CardTitle` / `CardContent` / `CardAction`,
|
|
2685
|
+
* {@link KpiValue}, and {@link KpiDescription}.
|
|
2686
|
+
*
|
|
2687
|
+
* @example
|
|
2688
|
+
* ```tsx
|
|
2689
|
+
* import { CardHeader, CardTitle, CardContent } from '@aircall/ds';
|
|
2690
|
+
* import { KpiCard, KpiValue, KpiDescription } from '@aircall/blocks';
|
|
2691
|
+
*
|
|
2692
|
+
* <KpiCard>
|
|
2693
|
+
* <CardHeader>
|
|
2694
|
+
* <CardTitle>Answered</CardTitle>
|
|
2695
|
+
* </CardHeader>
|
|
2696
|
+
* <CardContent className="flex flex-col gap-1">
|
|
2697
|
+
* <KpiValue value={123.1} kind="percent" trend="up" tone="positive" />
|
|
2698
|
+
* <KpiDescription trend="up" tone="positive">+2.48% vs previous period</KpiDescription>
|
|
2699
|
+
* </CardContent>
|
|
2700
|
+
* </KpiCard>
|
|
2701
|
+
* ```
|
|
2702
|
+
*/
|
|
2703
|
+
const KpiCard = React.forwardRef((componentProps, forwardRef) => {
|
|
2704
|
+
const { className, size = "sm", disabled = false, children, ...props } = componentProps;
|
|
2705
|
+
return /* @__PURE__ */ jsx(KpiCardContext.Provider, {
|
|
2706
|
+
value: { disabled },
|
|
2707
|
+
children: /* @__PURE__ */ jsx(Card, {
|
|
2708
|
+
ref: forwardRef,
|
|
2709
|
+
"data-slot": "kpi-card",
|
|
2710
|
+
"data-disabled": disabled || void 0,
|
|
2711
|
+
"aria-disabled": disabled || void 0,
|
|
2712
|
+
size,
|
|
2713
|
+
className: cn("gap-2 data-[size=sm]:gap-2", disabled && "cursor-not-allowed opacity-50", className),
|
|
2714
|
+
...props,
|
|
2715
|
+
children
|
|
2716
|
+
})
|
|
2717
|
+
});
|
|
2718
|
+
});
|
|
2719
|
+
KpiCard.displayName = "KpiCard";
|
|
2720
|
+
/**
|
|
2721
|
+
* KPI metric value. Formats via `useDsLocale` and renders primary (and optional
|
|
2722
|
+
* secondary) typography, with trend variants. Passing `onClick` makes the value
|
|
2723
|
+
* interactive (button + dotted underline). Prefer {@link KpiCard}`s `disabled`
|
|
2724
|
+
* for tile-level muting; a local `disabled` still works for standalone use.
|
|
2725
|
+
* Compose inside DS `CardContent` with {@link KpiDescription}.
|
|
2726
|
+
*
|
|
2727
|
+
* Prop naming: `kind` + `formatOptions` for formatting; `trend` is direction
|
|
2728
|
+
* (icon); `tone` is meaning (color) — "up" is not always good.
|
|
2729
|
+
*
|
|
2730
|
+
* @example
|
|
2731
|
+
* ```tsx
|
|
2732
|
+
* <KpiValue value={123.1} kind="percent" trend="up" tone="positive" />
|
|
2733
|
+
* ```
|
|
2734
|
+
*/
|
|
2735
|
+
const KpiValue = React.forwardRef((componentProps, forwardRef) => {
|
|
2736
|
+
const { className, value, kind: kindProp, formatOptions, durationStyle = "long", durationScale = "minutes", unit, trend, tone = "neutral", active = false, disabled: disabledProp = false, children, onClick, render, ...props } = componentProps;
|
|
2737
|
+
const locale = useDsLocale();
|
|
2738
|
+
const cardDisabled = useKpiCardDisabled();
|
|
2739
|
+
const isDisabled = Boolean(disabledProp || cardDisabled);
|
|
2740
|
+
const isInteractive = typeof onClick === "function";
|
|
2741
|
+
const kind = resolveKind(value, kindProp);
|
|
2742
|
+
let content;
|
|
2743
|
+
const valueUnderlineClassName = cn(isInteractive && "border-b border-current", isInteractive && !active && "border-dotted", isInteractive && active && "border-solid");
|
|
2744
|
+
const trendGlyph = trend ? /* @__PURE__ */ jsx(TrendGlyph, {
|
|
2745
|
+
trend,
|
|
2746
|
+
tone: disabledProp && !cardDisabled ? "neutral" : tone,
|
|
2747
|
+
className: "size-6"
|
|
2748
|
+
}) : null;
|
|
2749
|
+
if (children != null) content = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
|
|
2750
|
+
className: cn("inline-flex items-baseline gap-0.5", valueUnderlineClassName),
|
|
2751
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
2752
|
+
className: "text-3xl font-bold",
|
|
2753
|
+
children
|
|
2754
|
+
}), unit != null && unit !== "" && /* @__PURE__ */ jsx("span", {
|
|
2755
|
+
className: "pt-2 text-xl leading-none font-semibold",
|
|
2756
|
+
children: unit
|
|
2757
|
+
})]
|
|
2758
|
+
}), trendGlyph] });
|
|
2759
|
+
else {
|
|
2760
|
+
const formatted = value == null || typeof value === "number" && !Number.isFinite(value) ? null : formatKpiValue(value, kind, locale, {
|
|
2761
|
+
formatOptions,
|
|
2762
|
+
durationStyle,
|
|
2763
|
+
durationScale
|
|
2764
|
+
});
|
|
2765
|
+
const resolvedUnit = unit !== void 0 ? unit : kind === "duration" && durationStyle === "clock" ? clockDurationUnit(durationScale) : void 0;
|
|
2766
|
+
const unitNode = resolvedUnit != null && resolvedUnit !== "" ? resolvedUnit : null;
|
|
2767
|
+
if (!formatted) content = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
2768
|
+
className: cn("text-3xl font-bold", valueUnderlineClassName),
|
|
2769
|
+
children: KPI_EMPTY
|
|
2770
|
+
}), trendGlyph] });
|
|
2771
|
+
else {
|
|
2772
|
+
const hasSecondary = formatted.secondary !== "" || unitNode != null;
|
|
2773
|
+
content = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
|
|
2774
|
+
className: cn("inline-flex items-baseline gap-0.5", valueUnderlineClassName),
|
|
2775
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
2776
|
+
className: "text-3xl font-bold",
|
|
2777
|
+
children: formatted.primary
|
|
2778
|
+
}), hasSecondary && /* @__PURE__ */ jsxs("span", {
|
|
2779
|
+
className: "pt-2 text-xl leading-none font-semibold",
|
|
2780
|
+
children: [
|
|
2781
|
+
formatted.secondary,
|
|
2782
|
+
formatted.secondary !== "" && unitNode != null ? " " : null,
|
|
2783
|
+
unitNode
|
|
2784
|
+
]
|
|
2785
|
+
})]
|
|
2786
|
+
}), trendGlyph] });
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
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);
|
|
2790
|
+
return useRender({
|
|
2791
|
+
defaultTagName: isInteractive ? "button" : "div",
|
|
2792
|
+
props: mergeProps({
|
|
2793
|
+
ref: forwardRef,
|
|
2794
|
+
className: rootClassName,
|
|
2795
|
+
children: content,
|
|
2796
|
+
...isInteractive ? {
|
|
2797
|
+
type: "button",
|
|
2798
|
+
onClick,
|
|
2799
|
+
disabled: isDisabled,
|
|
2800
|
+
"aria-pressed": active
|
|
2801
|
+
} : isDisabled ? { "aria-disabled": true } : {}
|
|
2802
|
+
}, props),
|
|
2803
|
+
render,
|
|
2804
|
+
state: {
|
|
2805
|
+
slot: "kpi-value",
|
|
2806
|
+
active,
|
|
2807
|
+
disabled: isDisabled,
|
|
2808
|
+
interactive: isInteractive,
|
|
2809
|
+
trend,
|
|
2810
|
+
tone
|
|
2811
|
+
}
|
|
2812
|
+
});
|
|
2813
|
+
});
|
|
2814
|
+
KpiValue.displayName = "KpiValue";
|
|
2815
|
+
/**
|
|
2816
|
+
* Supporting comparison / context line under a {@link KpiValue}. Compose as a
|
|
2817
|
+
* sibling inside DS `CardContent` — not the metric title (`CardTitle` owns that).
|
|
2818
|
+
*
|
|
2819
|
+
* Text stays foreground; `trend` / `tone` only color the trailing icon so an
|
|
2820
|
+
* upward change that is bad uses `trend="up" tone="negative"`.
|
|
2821
|
+
*
|
|
2822
|
+
* @example
|
|
2823
|
+
* ```tsx
|
|
2824
|
+
* <KpiDescription trend="up" tone="positive">+2.48% vs previous period</KpiDescription>
|
|
2825
|
+
* <KpiDescription trend="up" tone="negative">+12% abandoned</KpiDescription>
|
|
2826
|
+
* ```
|
|
2827
|
+
*/
|
|
2828
|
+
const KpiDescription = React.forwardRef(function KpiDescription(componentProps, forwardRef) {
|
|
2829
|
+
const { className, trend, tone = "neutral", children, ...props } = componentProps;
|
|
2830
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2831
|
+
ref: forwardRef,
|
|
2832
|
+
"data-slot": "kpi-description",
|
|
2833
|
+
"data-trend": trend,
|
|
2834
|
+
"data-tone": tone,
|
|
2835
|
+
className: cn("inline-flex items-center gap-0.5 font-sans text-xs font-medium text-foreground", className),
|
|
2836
|
+
...props,
|
|
2837
|
+
children: [children, trend ? /* @__PURE__ */ jsx(TrendGlyph, {
|
|
2838
|
+
trend,
|
|
2839
|
+
tone,
|
|
2840
|
+
className: "size-3"
|
|
2841
|
+
}) : null]
|
|
2842
|
+
});
|
|
2843
|
+
});
|
|
2844
|
+
KpiDescription.displayName = "KpiDescription";
|
|
2845
|
+
/**
|
|
2846
|
+
* Loading placeholder sized for a {@link KpiValue} row. Thin wrapper around
|
|
2847
|
+
* DS `Skeleton` (`h-4 w-[45px]` with vertical margin to match the value row).
|
|
2848
|
+
* Swap in place of {@link KpiValue} while data loads.
|
|
2849
|
+
*
|
|
2850
|
+
* @example
|
|
2851
|
+
* ```tsx
|
|
2852
|
+
* <CardContent>
|
|
2853
|
+
* {loading ? <KpiValueSkeleton /> : <KpiValue value={123} />}
|
|
2854
|
+
* </CardContent>
|
|
2855
|
+
* ```
|
|
2856
|
+
*/
|
|
2857
|
+
const KpiValueSkeleton = React.forwardRef((componentProps, forwardRef) => {
|
|
2858
|
+
const { className, ...props } = componentProps;
|
|
2859
|
+
return /* @__PURE__ */ jsx(Skeleton, {
|
|
2860
|
+
ref: forwardRef,
|
|
2861
|
+
"data-slot": "kpi-value-skeleton",
|
|
2862
|
+
className: cn("my-[9px] h-4 w-[45px]", className),
|
|
2863
|
+
...props
|
|
2864
|
+
});
|
|
2865
|
+
});
|
|
2866
|
+
KpiValueSkeleton.displayName = "KpiValueSkeleton";
|
|
2867
|
+
/**
|
|
2868
|
+
* Loading placeholder sized for a {@link KpiDescription} row. Thin wrapper
|
|
2869
|
+
* around DS `Skeleton` (`12×144`, matching `text-xs` comparison copy).
|
|
2870
|
+
* Swap in place of {@link KpiDescription} while data loads.
|
|
2871
|
+
*
|
|
2872
|
+
* @example
|
|
2873
|
+
* ```tsx
|
|
2874
|
+
* <CardContent className="flex flex-col gap-1">
|
|
2875
|
+
* {loading ? (
|
|
2876
|
+
* <>
|
|
2877
|
+
* <KpiValueSkeleton />
|
|
2878
|
+
* <KpiDescriptionSkeleton />
|
|
2879
|
+
* </>
|
|
2880
|
+
* ) : (
|
|
2881
|
+
* <>
|
|
2882
|
+
* <KpiValue value={123} />
|
|
2883
|
+
* <KpiDescription>+2.48% vs previous period</KpiDescription>
|
|
2884
|
+
* </>
|
|
2885
|
+
* )}
|
|
2886
|
+
* </CardContent>
|
|
2887
|
+
* ```
|
|
2888
|
+
*/
|
|
2889
|
+
const KpiDescriptionSkeleton = React.forwardRef((componentProps, forwardRef) => {
|
|
2890
|
+
const { className, ...props } = componentProps;
|
|
2891
|
+
return /* @__PURE__ */ jsx(Skeleton, {
|
|
2892
|
+
ref: forwardRef,
|
|
2893
|
+
"data-slot": "kpi-description-skeleton",
|
|
2894
|
+
className: cn("h-3 w-36", className),
|
|
2895
|
+
...props
|
|
2896
|
+
});
|
|
2897
|
+
});
|
|
2898
|
+
KpiDescriptionSkeleton.displayName = "KpiDescriptionSkeleton";
|
|
2899
|
+
|
|
2526
2900
|
//#endregion
|
|
2527
2901
|
//#region src/components/sentiment-badge.tsx
|
|
2528
2902
|
const SENTIMENT_BADGE = {
|
|
@@ -4051,4 +4425,4 @@ function DashboardSidebarNav({ groups, renderLink }) {
|
|
|
4051
4425
|
}
|
|
4052
4426
|
|
|
4053
4427
|
//#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 };
|
|
4428
|
+
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.
|
|
3
|
+
"version": "0.18.1",
|
|
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 |
|