@aircall/blocks 0.11.0 → 0.13.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,8 +1,8 @@
1
- import { Anchor, Badge, Button, CounterBadge, DropdownMenu, DropdownMenuAddon, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, Field, FieldDescription, FieldError, FieldLabel, 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, useDsTranslation, useSidebar } from "@aircall/ds";
1
+ import { Anchor, Badge, Button, CounterBadge, DropdownMenu, DropdownMenuAddon, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, Field, 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, 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, LockKeyhole, Menu, MessageSquare, Minus, PanelLeft, PanelLeftClose, PanelLeftOpen, Plus, Search, Settings, Sprout, Square, SquareArrowOutUpRight, ThumbsDown, ThumbsDownFilled, ThumbsUp, ThumbsUpFilled, TriangleAlert } from "@aircall/react-icons";
5
+ import { AircallLoader, AircallLogo, AircallMark, ArrowUp, Check, ChevronDown, ChevronLeft, ChevronRight, Copy, Ellipsis, ExternalLink, FileText, History, LockKeyhole, Menu, MessageSquare, Minus, PanelLeft, PanelLeftClose, PanelLeftOpen, Plus, Search, Settings, 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";
@@ -29,6 +29,9 @@ const de = {
29
29
  "chatbotInput.messageLabel": "Nachricht",
30
30
  "chatbotInput.stopGenerating": "Generierung stoppen",
31
31
  "chatbotInput.sendMessage": "Nachricht senden",
32
+ "chatbotInput.conversationHistory": "Gesprächsverlauf",
33
+ "chatbotInput.noConversations": "Keine vergangenen Gespräche",
34
+ "chatbotInput.recentChatHistory": "Letzter Chatverlauf",
32
35
  "productBadge.beta": "Beta",
33
36
  "productBadge.new": "Neu",
34
37
  "productBadge.deprecated": "Veraltet",
@@ -85,6 +88,9 @@ const en = {
85
88
  "chatbotInput.messageLabel": "Message",
86
89
  "chatbotInput.stopGenerating": "Stop generating",
87
90
  "chatbotInput.sendMessage": "Send message",
91
+ "chatbotInput.conversationHistory": "Conversation history",
92
+ "chatbotInput.noConversations": "No past conversations",
93
+ "chatbotInput.recentChatHistory": "Recent chat history",
88
94
  "productBadge.beta": "Beta",
89
95
  "productBadge.new": "New",
90
96
  "productBadge.deprecated": "Deprecated",
@@ -141,6 +147,9 @@ const es = {
141
147
  "chatbotInput.messageLabel": "Mensaje",
142
148
  "chatbotInput.stopGenerating": "Detener la generación",
143
149
  "chatbotInput.sendMessage": "Enviar mensaje",
150
+ "chatbotInput.conversationHistory": "Historial de conversaciones",
151
+ "chatbotInput.noConversations": "No hay conversaciones pasadas",
152
+ "chatbotInput.recentChatHistory": "Historial de chats recientes",
144
153
  "productBadge.beta": "Beta",
145
154
  "productBadge.new": "Nuevo",
146
155
  "productBadge.deprecated": "Obsoleto",
@@ -197,6 +206,9 @@ const fr = {
197
206
  "chatbotInput.messageLabel": "Message",
198
207
  "chatbotInput.stopGenerating": "Arrêter la génération",
199
208
  "chatbotInput.sendMessage": "Envoyer un message",
209
+ "chatbotInput.conversationHistory": "Historique des conversations",
210
+ "chatbotInput.noConversations": "Aucune conversation passée",
211
+ "chatbotInput.recentChatHistory": "Historique récent des chats",
200
212
  "productBadge.beta": "Bêta",
201
213
  "productBadge.new": "Nouveau",
202
214
  "productBadge.deprecated": "Obsolète",
@@ -253,6 +265,9 @@ const it = {
253
265
  "chatbotInput.messageLabel": "Messaggio",
254
266
  "chatbotInput.stopGenerating": "Interrompi la generazione",
255
267
  "chatbotInput.sendMessage": "Invia messaggio",
268
+ "chatbotInput.conversationHistory": "Cronologia conversazioni",
269
+ "chatbotInput.noConversations": "Nessuna conversazione passata",
270
+ "chatbotInput.recentChatHistory": "Cronologia recente delle chat",
256
271
  "productBadge.beta": "Beta",
257
272
  "productBadge.new": "Nuovo",
258
273
  "productBadge.deprecated": "Obsoleto",
@@ -1445,7 +1460,7 @@ function ChatbotPanelHeader({ title = "Ask Aircall", onMenu, menuContent, menuOp
1445
1460
  const SIDEBAR_MIN_WIDTH = 140;
1446
1461
  const SIDEBAR_MAX_WIDTH = 480;
1447
1462
  const ChatbotSidebar = React.forwardRef((componentProps, forwardRef) => {
1448
- const { conversations, activeConversationId, onNewChat, onSelectConversation, renderConversationMenuContent, recentLabel = "Recent", popup = false, className } = componentProps;
1463
+ const { conversations, activeConversationId, onNewChat, onSelectConversation, renderConversationMenuContent, recentLabel = "Recent", showNewChat = true, popup = false, className } = componentProps;
1449
1464
  const t = useDsTranslation("blocks");
1450
1465
  const isNewChatActive = !activeConversationId || !conversations.some((c) => c.id === activeConversationId);
1451
1466
  const sidebarInternalRef = React.useRef(null);
@@ -1478,7 +1493,7 @@ const ChatbotSidebar = React.forwardRef((componentProps, forwardRef) => {
1478
1493
  const scrollableContent = /* @__PURE__ */ jsxs("div", {
1479
1494
  "data-slot": "sidebar-content",
1480
1495
  className: cn("flex min-h-0 flex-1 flex-col gap-2 overflow-auto", "[scrollbar-width:thin] [scrollbar-color:transparent_transparent] hover:[scrollbar-color:rgba(0,0,0,0.15)_transparent]", "[&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-thumb]:rounded-full", "[&::-webkit-scrollbar-thumb]:bg-transparent [&:hover::-webkit-scrollbar-thumb]:bg-black/20"),
1481
- children: [/* @__PURE__ */ jsx(SidebarGroup, { children: /* @__PURE__ */ jsx(SidebarMenu, { children: /* @__PURE__ */ jsx(SidebarMenuItem, { children: /* @__PURE__ */ jsxs(SidebarMenuButton, {
1496
+ children: [showNewChat && /* @__PURE__ */ jsx(SidebarGroup, { children: /* @__PURE__ */ jsx(SidebarMenu, { children: /* @__PURE__ */ jsx(SidebarMenuItem, { children: /* @__PURE__ */ jsxs(SidebarMenuButton, {
1482
1497
  isActive: isNewChatActive,
1483
1498
  onClick: onNewChat,
1484
1499
  children: [/* @__PURE__ */ jsx(Plus, {}), /* @__PURE__ */ jsx("span", { children: t("chatbotSidebar.newChat") })]
@@ -1533,6 +1548,17 @@ const ChatbotSidebar = React.forwardRef((componentProps, forwardRef) => {
1533
1548
  });
1534
1549
  ChatbotSidebar.displayName = "ChatbotSidebar";
1535
1550
 
1551
+ //#endregion
1552
+ //#region src/components/chatbot-panel-suggestion.tsx
1553
+ function ChatbotPanelSuggestion({ className, ...props }) {
1554
+ return /* @__PURE__ */ jsx(Button, {
1555
+ "data-slot": "chatbot-panel-suggestion",
1556
+ variant: "ghost",
1557
+ className: cn("h-auto max-w-[280px] text-left rounded-[18px] border border-primary px-3 py-1.5", "bg-background text-primary whitespace-normal", "hover:bg-[color-mix(in_srgb,#5BF1B2_20%,var(--background))] hover:text-primary", className),
1558
+ ...props
1559
+ });
1560
+ }
1561
+
1536
1562
  //#endregion
1537
1563
  //#region src/components/chatbot-input.tsx
1538
1564
  const textareaVariants = cva([
@@ -1556,13 +1582,14 @@ const textareaVariants = cva([
1556
1582
  expanded: false
1557
1583
  }
1558
1584
  });
1559
- function ChatbotInput({ className, value, onValueChange, onSubmit, onStop, placeholder, loading = false, size = "default", expanded = false, disabled, ...textareaProps }) {
1585
+ function ChatbotInput({ className, value, onValueChange, onSubmit, onStop, placeholder, loading = false, size = "default", expanded = false, disabled, suggestions, onSuggestionClick, conversations, activeConversationId, onSelectConversation, ...textareaProps }) {
1560
1586
  const t = useDsTranslation("blocks");
1561
1587
  const [currentValue = "", setCurrentValue] = useControllableState({
1562
1588
  prop: value,
1563
1589
  defaultProp: "",
1564
1590
  onChange: onValueChange
1565
1591
  });
1592
+ const [historyOpen, setHistoryOpen] = React.useState(false);
1566
1593
  const hasValue = currentValue.trim().length > 0;
1567
1594
  const handleChange = (event) => {
1568
1595
  setCurrentValue(event.target.value);
@@ -1579,10 +1606,63 @@ function ChatbotInput({ className, value, onValueChange, onSubmit, onStop, place
1579
1606
  }
1580
1607
  textareaProps.onKeyDown?.(event);
1581
1608
  };
1609
+ const hasSuggestions = suggestions && suggestions.length > 0;
1610
+ const hasBottomRow = hasSuggestions || conversations !== void 0;
1611
+ const sendStopButton = loading ? /* @__PURE__ */ jsx(InputGroupButton, {
1612
+ variant: "secondary",
1613
+ size: "icon",
1614
+ className: "rounded-full",
1615
+ "aria-label": t("chatbotInput.stopGenerating"),
1616
+ disabled,
1617
+ onClick: onStop,
1618
+ children: /* @__PURE__ */ jsx(Square, {})
1619
+ }) : /* @__PURE__ */ jsx(InputGroupButton, {
1620
+ variant: hasValue ? "default" : "ghost",
1621
+ size: "icon",
1622
+ className: "rounded-full",
1623
+ "aria-label": t("chatbotInput.sendMessage"),
1624
+ disabled: disabled || !hasValue,
1625
+ onClick: submit,
1626
+ children: /* @__PURE__ */ jsx(ArrowUp, {})
1627
+ });
1628
+ const historyButton = conversations !== void 0 ? /* @__PURE__ */ jsxs(Popover, {
1629
+ open: historyOpen,
1630
+ onOpenChange: setHistoryOpen,
1631
+ children: [/* @__PURE__ */ jsx(PopoverTrigger, { render: /* @__PURE__ */ jsx(Button, {
1632
+ variant: "ghost",
1633
+ size: "icon",
1634
+ disabled,
1635
+ "aria-label": t("chatbotInput.conversationHistory"),
1636
+ children: /* @__PURE__ */ jsx(History, {})
1637
+ }) }), /* @__PURE__ */ jsx(PopoverContent, {
1638
+ align: "end",
1639
+ sideOffset: 4,
1640
+ className: "w-80 gap-0 p-0",
1641
+ children: conversations.length === 0 ? /* @__PURE__ */ jsx("p", {
1642
+ className: "px-4 py-6 text-center text-sm text-muted-foreground",
1643
+ children: t("chatbotInput.noConversations")
1644
+ }) : /* @__PURE__ */ jsx(SidebarProvider, {
1645
+ className: "!min-h-0",
1646
+ style: { "--sidebar-width": "20rem" },
1647
+ children: /* @__PURE__ */ jsx(ChatbotSidebar, {
1648
+ popup: true,
1649
+ showNewChat: false,
1650
+ recentLabel: t("chatbotInput.recentChatHistory"),
1651
+ className: "bg-sidebar [&_[data-slot=sidebar-content]]:gap-0 [&_[data-slot=sidebar-content]]:max-h-[280px]",
1652
+ conversations,
1653
+ activeConversationId,
1654
+ onSelectConversation: (id) => {
1655
+ onSelectConversation?.(id);
1656
+ setHistoryOpen(false);
1657
+ }
1658
+ })
1659
+ })
1660
+ })]
1661
+ }) : null;
1582
1662
  return /* @__PURE__ */ jsxs(InputGroup, {
1583
1663
  "data-slot": "chatbot-input",
1584
1664
  "data-loading": loading || void 0,
1585
- className: cn("h-auto items-end gap-2 overflow-clip rounded-3xl bg-background py-2 pr-2 pl-6", "shadow-[0px_0px_8px_0px_var(--input)]", "hover:shadow-[0px_0px_8px_0px_var(--ring)]", "has-[[data-slot=input-group-control]:focus-visible]:border-green-400", "has-[[data-slot=input-group-control]:focus-visible]:ring-0", "has-[[data-slot=input-group-control]:focus-visible]:shadow-[0px_0px_8px_0px_var(--ring)]", className),
1665
+ className: cn("h-auto gap-2 overflow-clip rounded-3xl bg-background py-2 pr-2 pl-6", hasBottomRow ? "w-fit flex-col items-stretch" : "items-end", "shadow-[0px_0px_8px_0px_var(--input)]", "hover:shadow-[0px_0px_8px_0px_var(--ring)]", "has-[[data-slot=input-group-control]:focus-visible]:border-green-400", "has-[[data-slot=input-group-control]:focus-visible]:ring-0", "has-[[data-slot=input-group-control]:focus-visible]:shadow-[0px_0px_8px_0px_var(--ring)]", className),
1586
1666
  "aria-disabled": disabled || void 0,
1587
1667
  children: [/* @__PURE__ */ jsx(InputGroupTextarea, {
1588
1668
  rows: 1,
@@ -1597,40 +1677,27 @@ function ChatbotInput({ className, value, onValueChange, onSubmit, onStop, place
1597
1677
  size,
1598
1678
  expanded
1599
1679
  }))
1600
- }), /* @__PURE__ */ jsx("div", {
1680
+ }), hasBottomRow ? /* @__PURE__ */ jsxs("div", {
1681
+ className: "flex items-center justify-between gap-2",
1682
+ children: [/* @__PURE__ */ jsx("div", {
1683
+ className: "flex gap-2",
1684
+ children: hasSuggestions && suggestions.map((s) => /* @__PURE__ */ jsx(ChatbotPanelSuggestion, {
1685
+ className: "max-w-none shrink-0 whitespace-nowrap",
1686
+ disabled,
1687
+ onClick: () => onSuggestionClick?.(s),
1688
+ children: s.label
1689
+ }, s.id))
1690
+ }), /* @__PURE__ */ jsxs("div", {
1691
+ className: "flex items-center gap-2",
1692
+ children: [historyButton, sendStopButton]
1693
+ })]
1694
+ }) : /* @__PURE__ */ jsx("div", {
1601
1695
  className: "flex shrink-0 items-center gap-2",
1602
- children: loading ? /* @__PURE__ */ jsx(InputGroupButton, {
1603
- variant: "secondary",
1604
- size: "icon",
1605
- className: "rounded-full",
1606
- "aria-label": t("chatbotInput.stopGenerating"),
1607
- disabled,
1608
- onClick: onStop,
1609
- children: /* @__PURE__ */ jsx(Square, {})
1610
- }) : /* @__PURE__ */ jsx(InputGroupButton, {
1611
- variant: hasValue ? "default" : "ghost",
1612
- size: "icon",
1613
- className: "rounded-full",
1614
- "aria-label": t("chatbotInput.sendMessage"),
1615
- disabled: disabled || !hasValue,
1616
- onClick: submit,
1617
- children: /* @__PURE__ */ jsx(ArrowUp, {})
1618
- })
1696
+ children: sendStopButton
1619
1697
  })]
1620
1698
  });
1621
1699
  }
1622
1700
 
1623
- //#endregion
1624
- //#region src/components/chatbot-panel-suggestion.tsx
1625
- function ChatbotPanelSuggestion({ className, ...props }) {
1626
- return /* @__PURE__ */ jsx(Button, {
1627
- "data-slot": "chatbot-panel-suggestion",
1628
- variant: "ghost",
1629
- className: cn("h-auto max-w-[280px] text-left rounded-[18px] border border-primary px-3 py-1.5", "bg-background text-primary whitespace-normal", "hover:bg-[color-mix(in_srgb,#5BF1B2_20%,var(--background))] hover:text-primary", className),
1630
- ...props
1631
- });
1632
- }
1633
-
1634
1701
  //#endregion
1635
1702
  //#region src/components/chatbot-user-message.tsx
1636
1703
  function ChatbotUserMessage({ className, children, ...props }) {
@@ -2366,17 +2433,35 @@ function toFieldErrors(errors) {
2366
2433
 
2367
2434
  //#endregion
2368
2435
  //#region src/components/form-field.tsx
2369
- function FormFieldShell({ label, description, necessityIndicator, children }) {
2436
+ function FormFieldShell({ label, description, necessityIndicator, id, children }) {
2370
2437
  const errors = useFieldContext().state.meta.errors;
2438
+ const labelContent = typeof label === "string" ? label : label?.content;
2439
+ const labelInfo = typeof label === "object" ? label.info : void 0;
2440
+ const labelAside = typeof label === "object" ? label.aside : void 0;
2441
+ const hasAddons = labelInfo !== void 0 || labelAside !== void 0;
2442
+ const descriptionContent = typeof description === "string" ? description : description?.content;
2443
+ const descriptionVariant = (typeof description === "object" ? description.variant : void 0) ?? "instructional";
2444
+ const labelNode = labelContent !== void 0 && /* @__PURE__ */ jsx(FieldLabel, {
2445
+ necessityIndicator,
2446
+ htmlFor: id,
2447
+ children: labelContent
2448
+ });
2449
+ const labelRow = labelContent !== void 0 && (hasAddons ? /* @__PURE__ */ jsxs(FieldLabelRow, { children: [
2450
+ labelNode,
2451
+ labelInfo !== void 0 && /* @__PURE__ */ jsx(FieldLabelInfo, { children: labelInfo }),
2452
+ labelAside !== void 0 && /* @__PURE__ */ jsx(FieldLabelAside, { children: labelAside })
2453
+ ] }) : labelNode);
2454
+ const descriptionNode = descriptionContent !== void 0 && /* @__PURE__ */ jsx(FieldDescription, {
2455
+ variant: descriptionVariant,
2456
+ children: descriptionContent
2457
+ });
2371
2458
  return /* @__PURE__ */ jsxs(Field, {
2372
2459
  "data-invalid": errors.length > 0 ? true : void 0,
2373
2460
  children: [
2374
- label && /* @__PURE__ */ jsx(FieldLabel, {
2375
- necessityIndicator,
2376
- children: label
2377
- }),
2378
- description && /* @__PURE__ */ jsx(FieldDescription, { children: description }),
2461
+ labelRow,
2462
+ descriptionVariant === "instructional" && descriptionNode,
2379
2463
  children,
2464
+ descriptionVariant === "contextual" && descriptionNode,
2380
2465
  /* @__PURE__ */ jsx(FieldError, { errors: toFieldErrors(errors) })
2381
2466
  ]
2382
2467
  });
@@ -2388,6 +2473,7 @@ function FormFieldShell({ label, description, necessityIndicator, children }) {
2388
2473
  */
2389
2474
  function FormFieldBase(props) {
2390
2475
  const { form, name, validators, listeners, defaultValue, asyncDebounceMs, asyncAlways, label, description, necessityIndicator, buildControl, children } = props;
2476
+ const id = `${React.useId()}-${name}`;
2391
2477
  const AppField = form.AppField;
2392
2478
  return /* @__PURE__ */ jsx(AppField, {
2393
2479
  name,
@@ -2400,20 +2486,22 @@ function FormFieldBase(props) {
2400
2486
  label,
2401
2487
  description,
2402
2488
  necessityIndicator,
2403
- children: children(field, buildControl(field))
2489
+ id,
2490
+ children: children(field, buildControl(field, id))
2404
2491
  })
2405
2492
  });
2406
2493
  }
2407
2494
 
2408
2495
  //#endregion
2409
2496
  //#region src/components/form-select-field.tsx
2410
- function selectControl(field) {
2497
+ function selectControl(field, id) {
2411
2498
  return {
2412
2499
  selectProps: {
2413
2500
  value: field.state.value || null,
2414
2501
  onValueChange: (value) => field.handleChange(value ?? "")
2415
2502
  },
2416
2503
  selectTriggerProps: {
2504
+ id,
2417
2505
  onBlur: field.handleBlur,
2418
2506
  "aria-invalid": field.state.meta.errors.length > 0 ? true : void 0
2419
2507
  }
@@ -2446,8 +2534,9 @@ function FormSelectField(props) {
2446
2534
 
2447
2535
  //#endregion
2448
2536
  //#region src/components/form-input-field.tsx
2449
- function inputControl(field) {
2537
+ function inputControl(field, id) {
2450
2538
  return { inputProps: {
2539
+ id,
2451
2540
  value: field.state.value ?? "",
2452
2541
  onChange: (event) => field.handleChange(event.target.value),
2453
2542
  onBlur: field.handleBlur,
@@ -2473,13 +2562,14 @@ function FormInputField(props) {
2473
2562
 
2474
2563
  //#endregion
2475
2564
  //#region src/components/form-combobox-field.tsx
2476
- function comboboxControl(field) {
2565
+ function comboboxControl(field, id) {
2477
2566
  return {
2478
2567
  comboboxProps: {
2479
2568
  value: field.state.value || null,
2480
2569
  onValueChange: (value) => field.handleChange(value ?? "")
2481
2570
  },
2482
2571
  comboboxInputProps: {
2572
+ id,
2483
2573
  onBlur: field.handleBlur,
2484
2574
  "aria-invalid": field.state.meta.errors.length > 0 ? true : void 0
2485
2575
  }
@@ -2513,13 +2603,14 @@ function FormComboboxField(props) {
2513
2603
 
2514
2604
  //#endregion
2515
2605
  //#region src/components/form-multi-combobox-field.tsx
2516
- function multiComboboxControl(field) {
2606
+ function multiComboboxControl(field, id) {
2517
2607
  return {
2518
2608
  comboboxProps: {
2519
2609
  value: field.state.value ?? [],
2520
2610
  onValueChange: (values) => field.handleChange(values)
2521
2611
  },
2522
2612
  comboboxInputProps: {
2613
+ id,
2523
2614
  onBlur: field.handleBlur,
2524
2615
  "aria-invalid": field.state.meta.errors.length > 0 ? true : void 0
2525
2616
  }
@@ -2559,7 +2650,7 @@ function FormMultiComboboxField(props) {
2559
2650
  * Each row is composed from the existing single-control wrappers with an indexed `name`.
2560
2651
  *
2561
2652
  * @example
2562
- * <FormArrayField form={form} name="webhooks" label="Webhooks" description="Endpoints to notify">
2653
+ * <FormArrayField form={form} name="webhooks" label="Webhooks" description={{ content: 'Endpoints to notify', variant: 'contextual' }}>
2563
2654
  * {field => (
2564
2655
  * <>
2565
2656
  * {field.state.value.map((_row, i) => (
@@ -2577,6 +2668,7 @@ function FormMultiComboboxField(props) {
2577
2668
  */
2578
2669
  function FormArrayField(props) {
2579
2670
  const { form, name, validators, listeners, defaultValue, asyncDebounceMs, asyncAlways, label, description, necessityIndicator, children } = props;
2671
+ const id = `${React.useId()}-${name}`;
2580
2672
  const AppField = form.AppField;
2581
2673
  return /* @__PURE__ */ jsx(AppField, {
2582
2674
  name,
@@ -2590,6 +2682,7 @@ function FormArrayField(props) {
2590
2682
  label,
2591
2683
  description,
2592
2684
  necessityIndicator,
2685
+ id,
2593
2686
  children: children(field)
2594
2687
  })
2595
2688
  });
@@ -2597,8 +2690,9 @@ function FormArrayField(props) {
2597
2690
 
2598
2691
  //#endregion
2599
2692
  //#region src/components/form-textarea-field.tsx
2600
- function textareaControl(field) {
2693
+ function textareaControl(field, id) {
2601
2694
  return { textareaProps: {
2695
+ id,
2602
2696
  value: field.state.value ?? "",
2603
2697
  onChange: (event) => field.handleChange(event.target.value),
2604
2698
  onBlur: field.handleBlur,
@@ -2624,7 +2718,7 @@ function FormTextareaField(props) {
2624
2718
 
2625
2719
  //#endregion
2626
2720
  //#region src/components/form-radio-group-field.tsx
2627
- function radioGroupControl(field) {
2721
+ function radioGroupControl(field, _id) {
2628
2722
  return { radioGroupProps: {
2629
2723
  value: field.state.value || null,
2630
2724
  onValueChange: (value) => field.handleChange(value ?? ""),
@@ -2655,8 +2749,9 @@ function FormRadioGroupField(props) {
2655
2749
 
2656
2750
  //#endregion
2657
2751
  //#region src/components/form-otp-field.tsx
2658
- function otpControl(field) {
2752
+ function otpControl(field, id) {
2659
2753
  return { otpProps: {
2754
+ id,
2660
2755
  value: field.state.value ?? "",
2661
2756
  onChange: (value) => field.handleChange(value),
2662
2757
  onBlur: field.handleBlur,
@@ -2682,8 +2777,9 @@ function FormOTPField(props) {
2682
2777
 
2683
2778
  //#endregion
2684
2779
  //#region src/components/form-switch-field.tsx
2685
- function switchControl(field) {
2780
+ function switchControl(field, id) {
2686
2781
  return { switchProps: {
2782
+ id,
2687
2783
  checked: field.state.value,
2688
2784
  onCheckedChange: (checked) => field.handleChange(checked),
2689
2785
  onBlur: field.handleBlur,
@@ -2709,8 +2805,9 @@ function FormSwitchField(props) {
2709
2805
 
2710
2806
  //#endregion
2711
2807
  //#region src/components/form-numeric-field.tsx
2712
- function numericControl(field) {
2808
+ function numericControl(field, id) {
2713
2809
  return { numericProps: {
2810
+ id,
2714
2811
  value: field.state.value,
2715
2812
  onValueChange: (value) => field.handleChange(value ?? 0),
2716
2813
  onBlur: field.handleBlur,
@@ -2736,10 +2833,11 @@ function FormNumericField(props) {
2736
2833
 
2737
2834
  //#endregion
2738
2835
  //#region src/components/form-slider-field.tsx
2739
- function sliderControl(field) {
2836
+ function sliderControl(field, id) {
2740
2837
  return { sliderProps: {
2838
+ id,
2741
2839
  value: [field.state.value],
2742
- onValueChange: (values) => field.handleChange(values[0] ?? 0),
2840
+ onValueChange: (value) => field.handleChange(Array.isArray(value) ? value[0] ?? 0 : value),
2743
2841
  onBlur: field.handleBlur
2744
2842
  } };
2745
2843
  }
@@ -2762,7 +2860,7 @@ function FormSliderField(props) {
2762
2860
 
2763
2861
  //#endregion
2764
2862
  //#region src/components/form-toggle-group-field.tsx
2765
- function toggleGroupControl(field) {
2863
+ function toggleGroupControl(field, _id) {
2766
2864
  return { toggleGroupProps: {
2767
2865
  value: field.state.value ? [field.state.value] : [],
2768
2866
  onValueChange: (values) => field.handleChange(values[values.length - 1] ?? null),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aircall/blocks",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "sideEffects": [
@@ -81,7 +81,7 @@
81
81
  "vite": "7.3.1",
82
82
  "vitest": "4.0.17",
83
83
  "zod": "4.4.3",
84
- "@aircall/ds": "0.17.1",
84
+ "@aircall/ds": "0.19.0",
85
85
  "@aircall/hooks": "0.5.1"
86
86
  },
87
87
  "keywords": [
@@ -97,7 +97,7 @@
97
97
  "build:js": "tsdown --config ./tsdown.config.ts",
98
98
  "build:css": "tailwindcss -i ./src/styles/globals.css -o ./dist/globals.css --minify",
99
99
  "build:package": "pnpm run build:js && pnpm run build:css",
100
- "typecheck": "tsc --noEmit",
100
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.strict.json",
101
101
  "lint": "eslint . --max-warnings 0",
102
102
  "sb:dev": "storybook dev -p 6009",
103
103
  "sb:build": "storybook build",
@@ -169,15 +169,34 @@ cross-step validation.
169
169
 
170
170
  | `FormFieldProps` | After |
171
171
  |---|---|
172
- | `name` / `label` | `name` / `label` on the `Form*Field` |
172
+ | `name` | `name` on the `Form*Field` |
173
+ | `label` | `label` — a string, or `{ content, aside?, info? }` (see below) |
174
+ | helper/description text | `description` — a string, or `{ content, variant? }` (see below) |
173
175
  | `validate` | `validators.onChange` / `onSubmit` on the `Form*Field` |
174
176
  | `defaultValue` | `defaultValues[name]` in `useForm` |
175
177
  | `getErrorMessage(e)` | return the translated string from the validator |
176
178
 
179
+ **`label`** — a string is the label text. The object form adds label-row affordances:
180
+ - `label={{ content, info }}` — `info` is an `(i)` icon opening a popover of purely additive context
181
+ (hide it when ~90% of users don't need it).
182
+ - `label={{ content, aside }}` — `aside` is free-form content pinned to the **right** of the row (a
183
+ "Where do I find this?" link, a popover, plain text, …). You pass the node — an `Anchor`, a
184
+ `Popover`, whatever — and blocks wraps it in `FieldLabelAside` for alignment + typography, forcing
185
+ no styling of its own.
186
+
187
+ **`description`** — a string renders as the **`instructional`** variant (under the label, always
188
+ visible — the default). Pass `{ content, variant: 'contextual' }` to move it **below the control**,
189
+ where it is **hidden when the field is invalid** (the error takes its slot). Never mix both kinds on
190
+ one field.
191
+
192
+ **`necessityIndicator="required" | "optional"`** — mark only the exceptions (the few optional fields
193
+ in a mostly-required form, or vice versa); never mix both markers in one form.
194
+
177
195
  ## Fields reference
178
196
 
179
197
  `@aircall/blocks` ships typed wrappers for all common DS controls; each takes `form`,
180
- `name`, `label`, optional `validators`, and a render-prop `(field, control) => …` —
198
+ `name`, `label`, optional `validators`, optional `description` / `necessityIndicator`
199
+ (see Prop mapping above), and a render-prop `(field, control) => …` —
181
200
  spread the control bundle onto the matching DS primitive.
182
201
 
183
202
  | Input | Block | Input | Block |
@@ -5,7 +5,8 @@ description: >
5
5
  Load when wiring blocks compositions (DashboardPage, DashboardPageHeader,
6
6
  DashboardSidebar, empty states, form fields) into a project: installing the
7
7
  package, importing its precompiled globals.css after the DS bundle, and the DS
8
- providers it relies on.
8
+ providers it relies on. Covers both standalone apps and module-federation
9
+ consumers where the host already loads DS globals.
9
10
  type: core
10
11
  library: aircall-blocks
11
12
  library_version: "0.5.1"
@@ -29,8 +30,27 @@ Install blocks alongside DS:
29
30
  pnpm add @aircall/blocks @aircall/ds @aircall/react-icons
30
31
  ```
31
32
 
32
- Import the blocks bundle **after** the DS bundle from your JS/TS entry. It carries
33
- the page-layout grid and block-only tokens on top of DS:
33
+ How you import the CSS depends on whether your app is standalone or a
34
+ **module-federation consumer** whose host already loads DS globals.
35
+
36
+ ### Standalone app (owns its own Preflight)
37
+
38
+ `@aircall/blocks/globals.css` is a **delta build** — it contains only blocks-specific tokens
39
+ and utility classes; it does NOT bundle DS. Import DS globals first (Preflight + DS tokens +
40
+ DS utilities), then blocks globals (blocks-specific delta). Always import in this order.
41
+
42
+ ```css
43
+ /* style.css */
44
+ @layer theme, base, components, utilities;
45
+ @import 'tailwindcss/theme.css' layer(theme);
46
+ @import 'tailwindcss/utilities.css' layer(utilities);
47
+ @import '@aircall/ds/globals.css';
48
+ @import '@aircall/blocks/globals.css';
49
+
50
+ @source './src/**/*.{ts,tsx}';
51
+ ```
52
+
53
+ Or from a JS/TS entry (no custom Tailwind classes of your own):
34
54
 
35
55
  ```tsx
36
56
  // main.tsx
@@ -38,6 +58,28 @@ import '@aircall/ds/globals.css';
38
58
  import '@aircall/blocks/globals.css';
39
59
  ```
40
60
 
61
+ ### Module-federation consumer (host already owns the Preflight)
62
+
63
+ When your app is a remote loaded inside a host that already imports DS globals
64
+ (e.g. `dashboard-v4`), **do not re-import DS or blocks globals** in your own
65
+ CSS. The host's single Preflight applies to the whole document — re-importing
66
+ it duplicates the base reset and causes cascade conflicts (see Common Mistakes
67
+ below).
68
+
69
+ Import only the Tailwind layers you need for your own authored utility classes:
70
+
71
+ ```css
72
+ /* style.css */
73
+ @layer theme, base, components, utilities;
74
+ @import 'tailwindcss/theme.css' layer(theme);
75
+ @import 'tailwindcss/utilities.css' layer(utilities);
76
+
77
+ @source './**/*.{ts,tsx}';
78
+ ```
79
+
80
+ > **Note on the `.css` extension**: `@import "tailwindcss/theme"` (no `.css`)
81
+ > silently fails in webpack/Rsbuild PostCSS pipelines — the extension is required.
82
+
41
83
  Blocks render under the same DS providers — there is no blocks-specific provider; mount the
42
84
  DS root providers from `aircall-ds/setup` as needed (`ThemeProvider` / `TooltipProvider` /
43
85
  `Toaster`). Two DS providers matter specifically once you use blocks:
@@ -78,26 +120,82 @@ function CampaignsHeader() {
78
120
 
79
121
  ## Common Mistakes
80
122
 
123
+ ### HIGH — Re-importing DS/blocks globals in a module-federation consumer
124
+
125
+ Wrong — in a consumer whose host already loads DS globals:
126
+
127
+ ```css
128
+ @import 'tailwindcss';
129
+ @import '@aircall/ds/globals.css';
130
+ @import '@aircall/blocks/globals.css';
131
+ ```
132
+
133
+ Correct — consumer owns only its own utilities, no Preflight:
134
+
135
+ ```css
136
+ @layer theme, base, components, utilities;
137
+ @import 'tailwindcss/theme.css' layer(theme);
138
+ @import 'tailwindcss/utilities.css' layer(utilities);
139
+ @source './**/*.{ts,tsx}';
140
+ ```
141
+
142
+ In a module-federation setup, CSS from each remote lands in the **same document**
143
+ as the host. DS globals ships a Preflight (`@layer base { * { border-color: var(--color-border); } }`)
144
+ that sets the token-based border color globally. If the consumer also imports DS globals
145
+ (or the full `@import 'tailwindcss'`), it emits a second `@layer base` reset. Because
146
+ webpack/Rsbuild emits deep dependencies first and the consumer's own CSS last, the
147
+ consumer's Preflight (`border-color: currentColor`) ends up later in the output — and
148
+ later wins. DS component borders (Card, etc.) lose their token color and become invisible.
149
+
81
150
  ### HIGH — Importing blocks globals before DS globals (or omitting DS globals)
82
151
 
83
- Wrong:
152
+ Wrong — wrong order, or omitting DS globals:
84
153
 
85
- ```tsx
86
- import '@aircall/blocks/globals.css';
87
- import '@aircall/ds/globals.css';
154
+ ```css
155
+ @import '@aircall/blocks/globals.css';
156
+ @import '@aircall/ds/globals.css';
88
157
  ```
89
158
 
90
- Correct:
159
+ ```css
160
+ /* missing DS globals entirely */
161
+ @import '@aircall/blocks/globals.css';
162
+ ```
91
163
 
92
- ```tsx
93
- import '@aircall/ds/globals.css';
94
- import '@aircall/blocks/globals.css';
164
+ Correct — DS globals first, then blocks globals:
165
+
166
+ ```css
167
+ @import '@aircall/ds/globals.css';
168
+ @import '@aircall/blocks/globals.css';
95
169
  ```
96
170
 
97
- The blocks bundle layers page-layout tokens on top of the DS reset/tokens; loading it first (or without DS) leaves block compositions with missing base tokens and broken layout.
171
+ `@aircall/blocks/globals.css` is a delta build it contains only blocks-specific tokens
172
+ and utility classes. It depends on DS globals being loaded first to provide Preflight and
173
+ DS token definitions. Loading blocks before DS means DS's base reset (`border-color:
174
+ var(--color-border)`) lands after blocks and within the same `@layer base`, and component
175
+ tokens like `--background` and `--border` may not be defined when blocks tries to use them.
98
176
 
99
177
  Source: aircall/hydra:docs/migration-guides/tractor-to-ds/00-setup.md (§3)
100
178
 
179
+ ### MEDIUM — Using bare specifiers without `.css` in Rsbuild/webpack
180
+
181
+ Wrong:
182
+
183
+ ```css
184
+ @import 'tailwindcss/theme';
185
+ @import 'tailwindcss/utilities';
186
+ ```
187
+
188
+ Correct:
189
+
190
+ ```css
191
+ @import 'tailwindcss/theme.css';
192
+ @import 'tailwindcss/utilities.css';
193
+ ```
194
+
195
+ The bare form (no `.css`) silently produces no output in webpack/Rsbuild PostCSS
196
+ pipelines — classes like `flex`, `gap-4`, `text-sm` are never generated. Always
197
+ use the `.css` extension when splitting Tailwind imports.
198
+
101
199
  ### MEDIUM — Installing @aircall/blocks without @aircall/ds
102
200
 
103
201
  Wrong: