@aircall/blocks 0.6.0 → 0.7.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,10 +1,12 @@
1
- import { Badge, Button, CounterBadge, DropdownMenu, DropdownMenuAddon, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, Field, FieldDescription, FieldError, FieldLabel, InputGroup, InputGroupButton, InputGroupTextarea, Sidebar, SidebarContent, SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuAction, SidebarMenuButton, SidebarMenuItem, SidebarProvider, TabsList, Tooltip, TooltipContent, TooltipTrigger, cn, registerI18nNamespace, useDsTranslation, useSidebar } from "@aircall/ds";
1
+ import { Badge, Button, CounterBadge, DropdownMenu, DropdownMenuAddon, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, Field, FieldDescription, FieldError, FieldLabel, InputGroup, InputGroupButton, InputGroupTextarea, Popover, PopoverContent, PopoverTrigger, Sidebar, SidebarContent, SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuAction, SidebarMenuButton, SidebarMenuItem, SidebarProvider, TabsList, Tooltip, TooltipContent, TooltipTrigger, cn, registerI18nNamespace, useDsTranslation, useSidebar } from "@aircall/ds";
2
2
  import * as React from "react";
3
3
  import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useRef, useState } from "react";
4
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
- import { AircallLogo, ArrowUp, Check, ChevronLeft, ChevronRight, Copy, Ellipsis, LockKeyhole, PanelLeftClose, PanelLeftOpen, Plus, Search, Settings, Sprout, Square, TriangleAlert } from "@aircall/react-icons";
6
- import { cva } from "class-variance-authority";
5
+ import { AircallLogo, ArrowUp, Check, ChevronLeft, ChevronRight, Copy, Ellipsis, ExternalLink, FileText, LockKeyhole, MessageSquare, Minus, PanelLeft, PanelLeftClose, PanelLeftOpen, Plus, Search, Settings, Sprout, Square, ThumbsDown, ThumbsDownFilled, ThumbsUp, ThumbsUpFilled, TriangleAlert, X } from "@aircall/react-icons";
6
+ import ReactMarkdown from "react-markdown";
7
+ import remarkGfm from "remark-gfm";
7
8
  import { useCallbackRef, useControllableState, useIsMounted, useUnmount } from "@aircall/hooks";
9
+ import { cva } from "class-variance-authority";
8
10
  import { createFormHook, createFormHookContexts } from "@tanstack/react-form";
9
11
  import { mergeProps } from "@base-ui/react/merge-props";
10
12
  import { useRender } from "@base-ui/react/use-render";
@@ -371,6 +373,290 @@ const UnknownErrorMedia = UnknownError.Media;
371
373
  const UnknownErrorTitle = UnknownError.Title;
372
374
  const UnknownErrorDescription = UnknownError.Description;
373
375
 
376
+ //#endregion
377
+ //#region src/hooks/use-copy-to-clipboard.ts
378
+ function useCopyToClipboard(value, { timeout = 5e3, onCopied } = {}) {
379
+ const [copied, setCopied] = useState(false);
380
+ const timerRef = useRef(void 0);
381
+ const getIsMounted = useIsMounted();
382
+ const onCopiedRef = useCallbackRef(onCopied);
383
+ useUnmount(() => clearTimeout(timerRef.current));
384
+ useEffect(() => {
385
+ clearTimeout(timerRef.current);
386
+ setCopied(false);
387
+ }, [value]);
388
+ return {
389
+ copy: useCallback(async () => {
390
+ try {
391
+ await navigator.clipboard.writeText(value);
392
+ if (!getIsMounted()) return;
393
+ setCopied(true);
394
+ onCopiedRef();
395
+ clearTimeout(timerRef.current);
396
+ timerRef.current = setTimeout(() => {
397
+ if (getIsMounted()) setCopied(false);
398
+ }, timeout);
399
+ } catch {}
400
+ }, [
401
+ value,
402
+ timeout,
403
+ getIsMounted,
404
+ onCopiedRef
405
+ ]),
406
+ copied
407
+ };
408
+ }
409
+
410
+ //#endregion
411
+ //#region src/components/copy-button.tsx
412
+ const CopyButtonContext = createContext(null);
413
+ function useCopyButtonContext() {
414
+ const ctx = useContext(CopyButtonContext);
415
+ if (ctx === null) throw new Error("CopyButtonIcon and CopyButtonLabel must be rendered inside a CopyButton.");
416
+ return ctx;
417
+ }
418
+ const CopyButton = React.forwardRef((componentProps, forwardRef) => {
419
+ const { value, timeout, onCopied, variant = "outline", children, ...buttonProps } = componentProps;
420
+ const { copy, copied } = useCopyToClipboard(value, {
421
+ timeout,
422
+ onCopied
423
+ });
424
+ return /* @__PURE__ */ jsx(CopyButtonContext.Provider, {
425
+ value: { copied },
426
+ children: /* @__PURE__ */ jsx(Button, {
427
+ ref: forwardRef,
428
+ onClick: () => {
429
+ copy();
430
+ },
431
+ variant,
432
+ ...buttonProps,
433
+ children
434
+ })
435
+ });
436
+ });
437
+ CopyButton.displayName = "CopyButton";
438
+ const CopyButtonIcon = React.forwardRef((componentProps, forwardRef) => {
439
+ const { className } = componentProps;
440
+ const { copied } = useCopyButtonContext();
441
+ return /* @__PURE__ */ jsx(copied ? Check : Copy, {
442
+ ref: forwardRef,
443
+ className
444
+ });
445
+ });
446
+ CopyButtonIcon.displayName = "CopyButtonIcon";
447
+ const CopyButtonLabel = React.forwardRef((componentProps, forwardRef) => {
448
+ const { label = "Copy", copiedLabel = "Copied", className } = componentProps;
449
+ const { copied } = useCopyButtonContext();
450
+ return /* @__PURE__ */ jsx("span", {
451
+ ref: forwardRef,
452
+ className: cn(className),
453
+ children: copied ? copiedLabel : label
454
+ });
455
+ });
456
+ CopyButtonLabel.displayName = "CopyButtonLabel";
457
+
458
+ //#endregion
459
+ //#region src/components/chatbot-response-block.tsx
460
+ function SourcesBadge({ sources }) {
461
+ const [open, setOpen] = useState(false);
462
+ const closeTimerRef = useRef();
463
+ const openedByHoverRef = useRef(false);
464
+ useUnmount(() => clearTimeout(closeTimerRef.current));
465
+ function handlePointerEnter(e) {
466
+ if (e.pointerType !== "mouse") return;
467
+ openedByHoverRef.current = true;
468
+ clearTimeout(closeTimerRef.current);
469
+ setOpen(true);
470
+ }
471
+ function handlePointerLeave(e) {
472
+ if (e.pointerType !== "mouse") return;
473
+ clearTimeout(closeTimerRef.current);
474
+ closeTimerRef.current = setTimeout(() => {
475
+ openedByHoverRef.current = false;
476
+ setOpen(false);
477
+ }, 150);
478
+ }
479
+ return /* @__PURE__ */ jsxs(Popover, {
480
+ open,
481
+ onOpenChange: (nextOpen, eventDetails) => {
482
+ if (!nextOpen) {
483
+ if (eventDetails.reason === "trigger-press" && openedByHoverRef.current) return;
484
+ openedByHoverRef.current = false;
485
+ clearTimeout(closeTimerRef.current);
486
+ }
487
+ setOpen(nextOpen);
488
+ },
489
+ children: [/* @__PURE__ */ jsx(PopoverTrigger, {
490
+ className: "cursor-default rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
491
+ onPointerEnter: handlePointerEnter,
492
+ onPointerLeave: handlePointerLeave,
493
+ children: /* @__PURE__ */ jsx(Badge, {
494
+ className: "!rounded-full !bg-charcoal-400/20",
495
+ children: `${sources.length} ${sources.length === 1 ? "Source" : "Sources"}`
496
+ })
497
+ }), /* @__PURE__ */ jsx(PopoverContent, {
498
+ side: "top",
499
+ align: "start",
500
+ sideOffset: 8,
501
+ className: "w-80",
502
+ onPointerEnter: handlePointerEnter,
503
+ onPointerLeave: handlePointerLeave,
504
+ children: /* @__PURE__ */ jsx("div", {
505
+ role: "list",
506
+ className: "flex flex-col",
507
+ children: sources.map((source) => /* @__PURE__ */ jsxs("div", {
508
+ role: "listitem",
509
+ className: "flex items-center gap-2 px-2 py-2",
510
+ children: [
511
+ /* @__PURE__ */ jsx(FileText, { className: "size-3.5 shrink-0 text-foreground" }),
512
+ /* @__PURE__ */ jsx("span", {
513
+ className: "min-w-0 flex-1 line-clamp-2 text-sm",
514
+ children: source.title
515
+ }),
516
+ /* @__PURE__ */ jsx("a", {
517
+ href: source.url,
518
+ target: "_blank",
519
+ rel: "noreferrer",
520
+ "aria-label": `Open ${source.title}`,
521
+ className: "shrink-0 text-foreground hover:text-primary",
522
+ children: /* @__PURE__ */ jsx(ExternalLink, { className: "size-3.5" })
523
+ })
524
+ ]
525
+ }, source.url))
526
+ })
527
+ })]
528
+ });
529
+ }
530
+ function ChatbotResponseBlock({ messageId, markdown, streaming = false, sources, onFeedback, className }) {
531
+ const [feedback, setFeedback] = useState(null);
532
+ const onFeedbackRef = useCallbackRef(onFeedback);
533
+ const isMounted = useRef(false);
534
+ const feedbackRef = useRef(feedback);
535
+ feedbackRef.current = feedback;
536
+ useEffect(() => {
537
+ if (!isMounted.current) {
538
+ isMounted.current = true;
539
+ return;
540
+ }
541
+ if (feedbackRef.current === null) return;
542
+ setFeedback(null);
543
+ onFeedbackRef(null);
544
+ }, [
545
+ messageId,
546
+ markdown,
547
+ onFeedbackRef
548
+ ]);
549
+ function handleFeedback(value) {
550
+ const next = feedback === value ? null : value;
551
+ setFeedback(next);
552
+ onFeedback?.(next);
553
+ }
554
+ return /* @__PURE__ */ jsxs("div", {
555
+ "data-slot": "chatbot-response-block",
556
+ className: cn("flex flex-col gap-2", className),
557
+ children: [/* @__PURE__ */ jsx(ReactMarkdown, {
558
+ remarkPlugins: [remarkGfm],
559
+ components: {
560
+ h1: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("h1", {
561
+ className: "mt-10 text-4xl font-semibold first:mt-0",
562
+ ...props
563
+ }),
564
+ h2: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("h2", {
565
+ className: "mt-11 border-b border-border pb-2 text-3xl font-semibold leading-9 first:mt-0",
566
+ ...props
567
+ }),
568
+ h3: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("h3", {
569
+ className: "mt-10 text-2xl font-semibold leading-8 first:mt-0",
570
+ ...props
571
+ }),
572
+ p: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("p", {
573
+ className: "mt-4 text-base leading-6 text-foreground first:mt-0",
574
+ ...props
575
+ }),
576
+ blockquote: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("blockquote", {
577
+ className: "mt-4 border-l-2 border-border pl-6 first:mt-0",
578
+ ...props
579
+ }),
580
+ ul: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("ul", {
581
+ className: "mt-4 list-disc pl-6 text-base leading-6 text-foreground first:mt-0",
582
+ ...props
583
+ }),
584
+ ol: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("ol", {
585
+ className: "mt-4 list-decimal pl-6 text-base leading-6 text-foreground first:mt-0",
586
+ ...props
587
+ }),
588
+ li: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("li", {
589
+ className: "mb-2 last:mb-0",
590
+ ...props
591
+ }),
592
+ a: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("a", {
593
+ className: "text-green-700 underline underline-offset-2 hover:text-green-700/80",
594
+ ...props
595
+ }),
596
+ table: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("div", {
597
+ className: "mt-4 overflow-x-auto rounded border border-border first:mt-0",
598
+ children: /* @__PURE__ */ jsx("table", {
599
+ className: "w-full text-base leading-6",
600
+ ...props
601
+ })
602
+ }),
603
+ th: ({ node: _n, style, ...props }) => /* @__PURE__ */ jsx("th", {
604
+ className: "border-t border-border px-4 py-2 text-left font-bold text-foreground",
605
+ style,
606
+ ...props
607
+ }),
608
+ td: ({ node: _n, style, ...props }) => /* @__PURE__ */ jsx("td", {
609
+ className: "border-t border-border px-4 py-2 text-foreground",
610
+ style,
611
+ ...props
612
+ }),
613
+ tr: ({ node: _n, ...props }) => /* @__PURE__ */ jsx("tr", {
614
+ className: "even:bg-muted",
615
+ ...props
616
+ })
617
+ },
618
+ children: markdown
619
+ }), !streaming && /* @__PURE__ */ jsxs("div", {
620
+ className: "flex items-center gap-1",
621
+ children: [sources && sources.length > 0 && /* @__PURE__ */ jsx("div", {
622
+ className: "flex flex-1",
623
+ children: /* @__PURE__ */ jsx(SourcesBadge, { sources })
624
+ }), /* @__PURE__ */ jsxs("div", {
625
+ className: "ml-auto flex items-center gap-1",
626
+ children: [
627
+ /* @__PURE__ */ jsx(CopyButton, {
628
+ value: markdown,
629
+ variant: "ghost",
630
+ size: "icon-sm",
631
+ "aria-label": "Copy",
632
+ children: /* @__PURE__ */ jsx(CopyButtonIcon, {})
633
+ }),
634
+ /* @__PURE__ */ jsx(Button, {
635
+ type: "button",
636
+ variant: "ghost",
637
+ size: "icon-sm",
638
+ "aria-pressed": feedback === "up",
639
+ "aria-label": "Helpful",
640
+ onClick: () => handleFeedback("up"),
641
+ className: cn(feedback === "up" && "text-primary"),
642
+ children: feedback === "up" ? /* @__PURE__ */ jsx(ThumbsUpFilled, {}) : /* @__PURE__ */ jsx(ThumbsUp, {})
643
+ }),
644
+ /* @__PURE__ */ jsx(Button, {
645
+ type: "button",
646
+ variant: "ghost",
647
+ size: "icon-sm",
648
+ "aria-pressed": feedback === "down",
649
+ "aria-label": "Not helpful",
650
+ onClick: () => handleFeedback("down"),
651
+ className: cn(feedback === "down" && "text-primary"),
652
+ children: feedback === "down" ? /* @__PURE__ */ jsx(ThumbsDownFilled, {}) : /* @__PURE__ */ jsx(ThumbsDown, {})
653
+ })
654
+ ]
655
+ })]
656
+ })]
657
+ });
658
+ }
659
+
374
660
  //#endregion
375
661
  //#region src/components/chatbot-page.tsx
376
662
  const ChatbotPage = React.forwardRef((componentProps, forwardRef) => {
@@ -384,6 +670,55 @@ const ChatbotPage = React.forwardRef((componentProps, forwardRef) => {
384
670
  });
385
671
  ChatbotPage.displayName = "ChatbotPage";
386
672
 
673
+ //#endregion
674
+ //#region src/components/chatbot-panel-header.tsx
675
+ /**
676
+ * Header bar for the chatbot panel.
677
+ *
678
+ * Always visible — the panel body collapses/expands below it. Controlled by
679
+ * the parent via `open` + `onOpenChange`; pass `onClose` to show the dismiss (×) button.
680
+ */
681
+ function ChatbotPanelHeader({ title = "Ask Aircall", open, onOpenChange, onFeedback, onClose, className, ...props }) {
682
+ return /* @__PURE__ */ jsxs("div", {
683
+ "data-slot": "chatbot-panel-header",
684
+ "data-state": open ? "open" : "closed",
685
+ className: cn("flex w-full items-center gap-2 border-b border-border bg-background p-2", className),
686
+ ...props,
687
+ children: [
688
+ /* @__PURE__ */ jsx("span", {
689
+ "aria-hidden": "true",
690
+ className: "flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground",
691
+ children: /* @__PURE__ */ jsx(PanelLeft, { className: "size-4" })
692
+ }),
693
+ /* @__PURE__ */ jsx("span", {
694
+ className: "min-w-0 flex-1 truncate text-base font-bold leading-none text-foreground",
695
+ children: title
696
+ }),
697
+ onFeedback != null && /* @__PURE__ */ jsx(Button, {
698
+ variant: "ghost",
699
+ size: "icon",
700
+ "aria-label": "Send feedback",
701
+ onClick: onFeedback,
702
+ children: /* @__PURE__ */ jsx(MessageSquare, { className: "size-4" })
703
+ }),
704
+ /* @__PURE__ */ jsx(Button, {
705
+ variant: "ghost",
706
+ size: "icon",
707
+ "aria-label": open ? "Collapse panel" : "Expand panel",
708
+ onClick: () => onOpenChange(!open),
709
+ children: open ? /* @__PURE__ */ jsx(Minus, { className: "size-4" }) : /* @__PURE__ */ jsx(Plus, { className: "size-4" })
710
+ }),
711
+ onClose != null && /* @__PURE__ */ jsx(Button, {
712
+ variant: "ghost",
713
+ size: "icon",
714
+ "aria-label": "Close panel",
715
+ onClick: onClose,
716
+ children: /* @__PURE__ */ jsx(X, { className: "size-4" })
717
+ })
718
+ ]
719
+ });
720
+ }
721
+
387
722
  //#endregion
388
723
  //#region src/components/chatbot-sidebar.tsx
389
724
  const ChatbotSidebar = React.forwardRef((componentProps, forwardRef) => {
@@ -505,88 +840,6 @@ function ChatbotInput({ className, value, onValueChange, onSubmit, onStop, place
505
840
  });
506
841
  }
507
842
 
508
- //#endregion
509
- //#region src/hooks/use-copy-to-clipboard.ts
510
- function useCopyToClipboard(value, { timeout = 5e3, onCopied } = {}) {
511
- const [copied, setCopied] = useState(false);
512
- const timerRef = useRef(void 0);
513
- const getIsMounted = useIsMounted();
514
- const onCopiedRef = useCallbackRef(onCopied);
515
- useUnmount(() => clearTimeout(timerRef.current));
516
- useEffect(() => {
517
- clearTimeout(timerRef.current);
518
- setCopied(false);
519
- }, [value]);
520
- return {
521
- copy: useCallback(async () => {
522
- try {
523
- await navigator.clipboard.writeText(value);
524
- if (!getIsMounted()) return;
525
- setCopied(true);
526
- onCopiedRef();
527
- clearTimeout(timerRef.current);
528
- timerRef.current = setTimeout(() => {
529
- if (getIsMounted()) setCopied(false);
530
- }, timeout);
531
- } catch {}
532
- }, [
533
- value,
534
- timeout,
535
- getIsMounted,
536
- onCopiedRef
537
- ]),
538
- copied
539
- };
540
- }
541
-
542
- //#endregion
543
- //#region src/components/copy-button.tsx
544
- const CopyButtonContext = createContext(null);
545
- function useCopyButtonContext() {
546
- const ctx = useContext(CopyButtonContext);
547
- if (ctx === null) throw new Error("CopyButtonIcon and CopyButtonLabel must be rendered inside a CopyButton.");
548
- return ctx;
549
- }
550
- const CopyButton = React.forwardRef((componentProps, forwardRef) => {
551
- const { value, timeout, onCopied, variant = "outline", children, ...buttonProps } = componentProps;
552
- const { copy, copied } = useCopyToClipboard(value, {
553
- timeout,
554
- onCopied
555
- });
556
- return /* @__PURE__ */ jsx(CopyButtonContext.Provider, {
557
- value: { copied },
558
- children: /* @__PURE__ */ jsx(Button, {
559
- ref: forwardRef,
560
- onClick: () => {
561
- copy();
562
- },
563
- variant,
564
- ...buttonProps,
565
- children
566
- })
567
- });
568
- });
569
- CopyButton.displayName = "CopyButton";
570
- const CopyButtonIcon = React.forwardRef((componentProps, forwardRef) => {
571
- const { className } = componentProps;
572
- const { copied } = useCopyButtonContext();
573
- return /* @__PURE__ */ jsx(copied ? Check : Copy, {
574
- ref: forwardRef,
575
- className
576
- });
577
- });
578
- CopyButtonIcon.displayName = "CopyButtonIcon";
579
- const CopyButtonLabel = React.forwardRef((componentProps, forwardRef) => {
580
- const { label = "Copy", copiedLabel = "Copied", className } = componentProps;
581
- const { copied } = useCopyButtonContext();
582
- return /* @__PURE__ */ jsx("span", {
583
- ref: forwardRef,
584
- className: cn(className),
585
- children: copied ? copiedLabel : label
586
- });
587
- });
588
- CopyButtonLabel.displayName = "CopyButtonLabel";
589
-
590
843
  //#endregion
591
844
  //#region src/components/chatbot-panel-suggestion.tsx
592
845
  function ChatbotPanelSuggestion({ className, ...props }) {
@@ -1777,4 +2030,4 @@ function DashboardSidebarNav({ groups, renderLink }) {
1777
2030
  }
1778
2031
 
1779
2032
  //#endregion
1780
- export { BetaBadge, CardSaveBar, ChatbotInput, ChatbotPage, ChatbotPanelSuggestion, ChatbotPanelTrigger, 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, DashboardStandalonePageHeader, DashboardStandalonePageTitle, DeprecatedBadge, EmptyState, EmptyStateActions, EmptyStateButton, EmptyStateDescription, EmptyStateExternalLink, EmptyStateHeader, EmptyStateMedia, EmptyStateTitle, FormComboboxField, FormFieldBase, FormInputField, FormMultiComboboxField, FormNumberField, FormOTPField, FormRadioGroupField, FormSelectField, FormSliderField, FormSwitchField, FormTextareaField, FormToggleGroupField, NewBadge, NoDataDescription, NoDataEmptyState, NoDataMedia, NoDataTitle, NoSupportDescription, NoSupportEmptyState, NoSupportMedia, NoSupportTitle, NotFoundDescription, NotFoundEmptyState, NotFoundMedia, NotFoundTitle, ProfessionalBadge, RestrictedAccessDescription, RestrictedAccessEmptyState, RestrictedAccessMedia, RestrictedAccessTitle, RoleBadge, SaveBar, SubmitButton, TrialBadge, UnknownErrorDescription, UnknownErrorEmptyState, UnknownErrorMedia, UnknownErrorTitle, fieldContext, formContext, toFieldErrors, useCopyToClipboard, useSidebar as useDashboardSidebar, useFieldContext, useForm, useFormContext, withForm };
2033
+ export { BetaBadge, CardSaveBar, ChatbotInput, ChatbotPage, ChatbotPanelHeader, ChatbotPanelSuggestion, ChatbotPanelTrigger, ChatbotResponseBlock, 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, DashboardStandalonePageHeader, DashboardStandalonePageTitle, DeprecatedBadge, EmptyState, EmptyStateActions, EmptyStateButton, EmptyStateDescription, EmptyStateExternalLink, EmptyStateHeader, EmptyStateMedia, EmptyStateTitle, FormComboboxField, FormFieldBase, FormInputField, FormMultiComboboxField, FormNumberField, FormOTPField, FormRadioGroupField, FormSelectField, FormSliderField, FormSwitchField, FormTextareaField, FormToggleGroupField, NewBadge, NoDataDescription, NoDataEmptyState, NoDataMedia, NoDataTitle, NoSupportDescription, NoSupportEmptyState, NoSupportMedia, NoSupportTitle, NotFoundDescription, NotFoundEmptyState, NotFoundMedia, NotFoundTitle, ProfessionalBadge, RestrictedAccessDescription, RestrictedAccessEmptyState, RestrictedAccessMedia, RestrictedAccessTitle, RoleBadge, SaveBar, SubmitButton, TrialBadge, UnknownErrorDescription, UnknownErrorEmptyState, UnknownErrorMedia, UnknownErrorTitle, fieldContext, formContext, toFieldErrors, useCopyToClipboard, useSidebar as useDashboardSidebar, useFieldContext, useForm, useFormContext, withForm };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aircall/blocks",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "sideEffects": [
@@ -16,7 +16,8 @@
16
16
  "files": [
17
17
  "dist",
18
18
  "README.md",
19
- "package.json"
19
+ "package.json",
20
+ "skills"
20
21
  ],
21
22
  "exports": {
22
23
  ".": {
@@ -40,6 +41,10 @@
40
41
  "optional": true
41
42
  }
42
43
  },
44
+ "dependencies": {
45
+ "react-markdown": "9.0.3",
46
+ "remark-gfm": "4.0.1"
47
+ },
43
48
  "devDependencies": {
44
49
  "@aircall/react-icons": "*",
45
50
  "@aircall/tsconfig": "1.4.3",
@@ -55,6 +60,7 @@
55
60
  "@storybook/react-vite": "10.3.3",
56
61
  "@tailwindcss/cli": "4.1.18",
57
62
  "@tailwindcss/postcss": "4.1.18",
63
+ "@tanstack/intent": "0.1.1",
58
64
  "@tanstack/react-form": "^1.33.0",
59
65
  "@types/node": "24",
60
66
  "@types/react": "19",
@@ -75,9 +81,17 @@
75
81
  "vite": "7.3.1",
76
82
  "vitest": "4.0.17",
77
83
  "zod": "4.4.3",
78
- "@aircall/ds": "0.14.0",
84
+ "@aircall/ds": "0.15.0",
79
85
  "@aircall/hooks": "0.5.1"
80
86
  },
87
+ "keywords": [
88
+ "tanstack-intent"
89
+ ],
90
+ "repository": {
91
+ "type": "git",
92
+ "url": "git+https://gitlab.com/aircall/shared/hydra.git",
93
+ "directory": "packages/blocks"
94
+ },
81
95
  "scripts": {
82
96
  "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
83
97
  "build:js": "tsdown --config ./tsdown.config.ts",
@@ -0,0 +1,127 @@
1
+ ---
2
+ name: aircall-blocks/migrate-dashboard
3
+ description: >
4
+ Migrate a file from @dashboard/library to @aircall/blocks / @aircall/ds. Load FIRST
5
+ to get the full component-level routing table before picking a recipe. Identifies what
6
+ has a blocks/ds equivalent, what is out of scope, and which sub-skill to load next.
7
+ type: core
8
+ library: aircall-blocks
9
+ library_version: "0.5.1"
10
+ requires:
11
+ - aircall-blocks/setup
12
+ sources:
13
+ - "aircall/hydra:docs/migration-guides/dashboard-lib-to-blocks/AGENTS.md"
14
+ ---
15
+
16
+ # Migrate @dashboard/library → @aircall/blocks / @aircall/ds
17
+
18
+ This is the overview router for the `@dashboard/library` migration. Load it first to
19
+ identify the right sub-skill for each component you are migrating, and to determine what
20
+ falls outside the scope of blocks and ds entirely.
21
+
22
+ ## How to run this migration (end-to-end)
23
+
24
+ Migrate incrementally — one screen/file at a time, shipping each green:
25
+
26
+ 0. **Set up once** — load `@aircall/blocks#aircall-blocks/setup` (it builds on
27
+ `@aircall/ds#aircall-ds/setup`): install `@aircall/blocks` + `@aircall/ds`, import both
28
+ `globals.css` bundles, and mount the DS root providers — including `DsI18nProvider`
29
+ (under your react-i18next provider, fed the user's language; importing `@aircall/blocks`
30
+ registers a `blocks` i18n namespace, so it needs DS's i18n engine) and
31
+ `NotificationQueueProvider` if you use banners. Add the jsdom test shims from setup.
32
+ (Loaded automatically via `requires`, but do the wiring first.)
33
+ 1. **Inventory** — list the file's `@dashboard/library` imports. The routing table below
34
+ says which have a blocks/ds equivalent and which are out of scope (hooks/utils/constants
35
+ don't migrate — leave them on `@dashboard/library`).
36
+ 2. **Migrate** — load the per-area recipe from the routing table for each UI component. Any
37
+ Tractor primitives in the same file migrate via `@aircall/ds#aircall-ds/migrate-tractor`;
38
+ icons via `@aircall/ds#aircall-ds/migrate-icons`.
39
+ 3. **Verify green** — `tsc --noEmit`, tests (DS popups/Switch need the setup jsdom shims),
40
+ biome/lint.
41
+ 4. **Repeat** until no in-scope `@dashboard/library` UI imports remain.
42
+
43
+ ## Cross-cutting note
44
+
45
+ `@dashboard/library` mixes UI components with non-UI utilities. Only the UI components
46
+ migrate. When writing replacement code:
47
+
48
+ - Import block-level compositions (page layout, empty states, form layer) from
49
+ `'@aircall/blocks'`.
50
+ - Import DS primitives (Card, Spinner, DataTable, Combobox, ItemGroup, etc.) from
51
+ `'@aircall/ds'`.
52
+ - Never mix the two import paths for the same logical component — pick the package that
53
+ owns it per the table below.
54
+
55
+ ## Forms — never local state
56
+
57
+ Any form that collects and submits data migrates to **`@aircall/blocks` `useForm` + the
58
+ `Form*Field` wrappers** (the Storybook-proven `CommonForm` pattern) — NOT React
59
+ `useState` per field, and NOT bare ds `Field`/`Input` primitives wired by hand. The form
60
+ owns field state, validation, dirty/`canSubmit`/`isSubmitting`, and error display; those
61
+ are what drive `SubmitButton`/`CardSaveBar`. Hand-rolled `useState` bypasses all of it and
62
+ must be rewritten, not preserved, during migration. (`useState` for non-field UI — open,
63
+ active tab — is fine.) Converting a large legacy `useState` form is a deliberate refactor,
64
+ not a 1:1 swap. See `…/migrate-dashboard/form-wizard` and `@aircall/ds#aircall-ds/migrate-tractor/form-and-field`.
65
+
66
+ ## Out of scope
67
+
68
+ The following `@dashboard/library` exports do NOT have an equivalent in `@aircall/blocks`
69
+ or `@aircall/ds`. Do not try to map them to a DS component. They need a shared
70
+ utils/data home or must stay in the consuming app.
71
+
72
+ **Hooks**: `useGraphQuery`, `useGraphMutation`, `useToast`, `useToggle`,
73
+ `useBroadcastChannel`
74
+
75
+ **Helpers / utils**: `generateRandomUUID`, `isTruthy`, `toFixedNumber`,
76
+ `capitalizeFirstLetter`, `getInitials`
77
+
78
+ **Constants, types, and contexts**: `ROLE_NAME`, `RESOURCE`,
79
+ `NavigationBlockerProvider`, `ClientError`
80
+
81
+ **UI with no blocks/ds equivalent yet**: some `@dashboard/library` UI components have
82
+ no target in `@aircall/blocks` or `@aircall/ds` yet (e.g. `PieChart` and other charts,
83
+ `AudioPlayer`, `TileLegend`). If a UI component is not in the routing table below and
84
+ not listed above, leave it imported from `@dashboard/library` for now — do not force a
85
+ migration or invent a target.
86
+
87
+ ## Component routing table
88
+
89
+ | @dashboard/library | Target (pkg) | Recipe to load | Status |
90
+ |---|---|---|---|
91
+ | `MessageScreen` family + `UnknownError` | `ComingSoonEmptyState`, `RestrictedAccessEmptyState`, `NotFoundEmptyState`, `NoDataEmptyState`, `NoSupportEmptyState`, `UnknownErrorEmptyState` and the `EmptyState*` parts (@aircall/blocks) | load @aircall/blocks#aircall-blocks/migrate-dashboard/empty-states | available |
92
+ | `PageHeader` | `DashboardPageHeader` (+ `DashboardPageHeaderTitle` / `DashboardPageHeaderActions` / `DashboardPageHeaderAction` / `DashboardPageHeaderNav` / `DashboardPageHeaderDescription`) (@aircall/blocks) | load @aircall/blocks#aircall-blocks/migrate-dashboard/page-header | available |
93
+ | Page / screen shell — incl. full-page flows (Campaign Creation, Add Contacts) | `DashboardPage` (standard: sidebar + header + tabs + content) / `DashboardStandalonePage` (full-page, no sidebar) (@aircall/blocks) | load @aircall/blocks#aircall-blocks/migrate-dashboard/dashboard-page | available |
94
+ | `Paper` | `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`, `CardAction` (and @aircall/blocks `CardSaveBar` for a save bar) (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/card | available |
95
+ | `LoadMoreTable` | `DataTable` (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/data-table | available |
96
+ | `MultiSearchSelect` + `MultiInlineSearchSelect` + `MultiSelectOption` | `Combobox` with `multiple` (multi-select) (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/combobox | available |
97
+ | `SingleSearchSelect` / `SearchSelect` / single-value `MultiSelect` | `Combobox` WITHOUT `multiple` (single-select) (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/combobox | available |
98
+ | `FormWizard` + `useFormWizard` + `FormField` | `useForm`, `CommonForm`, `FormInputField`/`FormSelectField`/`FormComboboxField`/etc., `CardSaveBar`/`SaveBar` (@aircall/blocks) | load @aircall/blocks#aircall-blocks/migrate-dashboard/form-wizard | available |
99
+ | `Loading` | `Spinner` (and `Skeleton` for content placeholders) (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/loading | available |
100
+ | `List` + `ListItem` | `ItemGroup`, `Item`, `ItemMedia`, `ItemContent`, `ItemActions` (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/list | available |
101
+ | `Tile` + `TileHeader` + `TileValue` | `Card` (+ parts) as the tile container (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/tile | available |
102
+ | `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 |
103
+ | `InfoPopup` + `InfoPopupTrigger` + `InfoPopupContent` | `HoverCard`, `HoverCardTrigger`, `HoverCardContent` (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/info-popup | available |
104
+ | `CopyToClipboardButton` + `CopyToClipboardText` | `CopyButton`, `CopyButtonIcon`, `CopyButtonLabel` (@aircall/blocks) | load @aircall/blocks#aircall-blocks/migrate-dashboard/copy-button | available |
105
+ | `ToggleRow` | `Field` (orientation="horizontal") + `Switch` + `FieldLabel` + optional `FieldDescription` (@aircall/ds) | load @aircall/blocks#aircall-blocks/migrate-dashboard/toggle-row | available |
106
+ | `AddButton` + `SaveButton` + `LoadingButton` | `Button` (@aircall/ds) — `AddButton` → `Button` with leading icon; `LoadingButton` loading state → `Button disabled` + `Spinner`; `SaveButton` saved state → `Button variant="outline"` + check icon | load @aircall/ds#aircall-ds/migrate-tractor/button | available |
107
+ | `ConditionalTooltip` | Conditional JSX: wrap children in `<Tooltip>` / `<TooltipTrigger>` / `<TooltipContent>` when condition is true, render children bare when false — no wrapper component needed (@aircall/ds) | inline — no sub-skill | available |
108
+ | `RadioBoxGroup` + `RadioBox` | `RadioGroup` + `RadioGroupItem` wrapped in `Field` + `FieldLabel` + `FieldContent` (@aircall/ds) | inline — see @aircall/ds#aircall-ds/migrate-tractor/form-and-field for Field patterns | available |
109
+ | `TagBeautified` | `Badge` with `legacyColor` prop for stored hex colors; `Badge` with `color` + `tone` props for new tags (@aircall/ds) | inline — `<Badge legacyColor="#0761b5">Sales</Badge>` | available |
110
+ | `RolesTags` | `RoleBadge` with `role` prop (`"owner"` \| `"admin"` \| `"supervisor"` \| `"agent"`) (@aircall/blocks) | inline — `<RoleBadge role="admin" />` | available |
111
+ | `Count` | `CounterBadge` — pass the capped value as children: `<CounterBadge>{n > 99 ? '99+' : n}</CounterBadge>` (@aircall/ds) | inline — no size or max props | available |
112
+ | `Avatar` | `Avatar`, `AvatarImage`, `AvatarFallback` compound (@aircall/ds) | load @aircall/ds#aircall-ds/migrate-tractor/avatar | available |
113
+ | `ProgressBar` + `OptimisticProgressBar` | `Progress` with `value` prop (0–100) (@aircall/ds) | inline — `<Progress value={60} />` | available |
114
+ | `ShadowScrollContainer` | `ScrollArea` with `scrollFade` prop — use a fixed `h-[…]` not `max-h-[…]` on the root (@aircall/ds) | inline — `<ScrollArea scrollFade className="h-[400px] rounded-md border">` | available |
115
+ | `AccordionSection` | `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionContent` (@aircall/ds) | load @aircall/ds#aircall-ds/migrate-tractor/accordion | available |
116
+ | `ConfirmationModal` | `AlertDialog` and its parts (@aircall/ds) | load @aircall/ds#aircall-ds/migrate-tractor/dialog | available |
117
+ | `Tab` | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` (@aircall/ds) | load @aircall/ds#aircall-ds/migrate-tractor/tabs | available |
118
+ | `Skeleton` + `SkeletonText` | `Skeleton` (@aircall/ds) | load @aircall/ds#aircall-ds/migrate-tractor/skeleton | available |
119
+
120
+ ## How to use this router
121
+
122
+ 1. Identify the `@dashboard/library` export(s) in the file you are migrating.
123
+ 2. Check the **Out of scope** section first — if the export is listed there, skip it.
124
+ 3. For each UI component, find the matching row in the table above and load the
125
+ indicated recipe sub-skill.
126
+ 4. Each sub-skill is self-contained: it carries the full prop mapping, common mistakes,
127
+ and import paths for its component family.