Unibox
ChannelSegments
The Unibox channel filter: a rail of icons where one shared pill glides to the selected channel, and only that segment spells out its name.
Guidelines#
Use it for switching the conversation list between All, Mail, LinkedIn, WhatsApp and Meetings. It fits any width, down to the 224px tablet rail.
What you provide
options:ChannelSegmentOption[], each{ value, label, icon, needsAttention?, hint? }.valueis one of"all" | "email" | "linkedin" | "whatsapp" | "meeting".needsAttentionmeans an account stopped syncing.hintis the tooltip's second line, such as "2 accounts".
value,onChangeandclassName.
In the app, only channels the workspace actually has get a segment.
Anatomy
- The rail is
rounded-full border border-border/60 bg-muted/50 p-1 backdrop-blur-smwithgap-1. - Segments are
h-8 rounded-full px-2.5 text-xs font-mediumwithsize-3.5icons,text-muted-foregroundwhen inactive. - The selected segment takes the leftover width (
flex-1) on abg-background shadow-sm ring-1 ring-border/60pill. needsAttentionadds asize-1.5 bg-amber-500dot.
Motion: the pill (layoutId) and the expanding label move on SPRING_GLIDE, and the label exits in 0.12s. Reduced motion sets durations to 0.
Keyboard: the ARIA tabs pattern. Arrow keys, Home and End select, and the rail scrolls itself rather than the page.
Don't put counts inside the segments, and don't mount two rails at once: they share the pill's layoutId.
Facts#
| Group | Unibox |
|---|---|
| Product | Max |
| Global | window.DigitalCrew.ChannelSegments |
| Source | max-agent: src/features/unibox/ui/components/channel-segments.tsx |
| Import in the app | import { ChannelSegments } from "@/features/unibox/ui/components/channel-segments"; |
API#
TypeScript declarations emitted from src/features/unibox/ui/components/channel-segments.tsx.
export interface ChannelSegmentOption {
value: ChannelFilter;
label: string;
icon: LucideIcon;
/**
* An account behind this channel stopped syncing. The dot turns "this
* channel looks empty" into "this channel has gone quiet, and here is why"
* — the reconnect link itself lives in the alerts banner above the list.
*/
needsAttention?: boolean;
/** Second line of the tooltip, e.g. "2 accounts". */
hint?: string;
}
interface ChannelSegmentsProps {
options: ChannelSegmentOption[];
value: ChannelFilter;
onChange: (value: ChannelFilter) => void;
className?: string;
}
export declare function ChannelSegments({ options, value, onChange, className, }: ChannelSegmentsProps): JSX.Element;Example#
The code of the preview above: plain React.createElement against window.DigitalCrew, with realistic data.
<div id="root" class="p-6"></div>
<script>
(function () {
var h = React.createElement, DC = window.DigitalCrew, I = DC.Icons;
var OPTIONS = [
{ value: "all", label: "All", icon: I.Inbox },
{ value: "email", label: "Mail", icon: I.Mail, hint: "2 accounts" },
{ value: "linkedin", label: "LinkedIn", icon: I.Linkedin, hint: "1 account" },
{ value: "whatsapp", label: "WhatsApp", icon: I.MessageCircleMore, hint: "1 account, 1 needs reconnecting", needsAttention: true }
];
var COUNTS = { all: 128, email: 74, linkedin: 41, whatsapp: 13 };
function App() {
var st = React.useState("all"), value = st[0], setValue = st[1];
var touched = React.useRef(false);
React.useEffect(function () {
// Glide once on load so the pill and the expanding label are visible.
var t = setTimeout(function () { if (!touched.current) setValue("linkedin"); }, 700);
return function () { clearTimeout(t); };
}, []);
var current = OPTIONS.filter(function (o) { return o.value === value; })[0];
return h("div", { className: "mx-auto flex w-[340px] flex-col gap-2" },
h(DC.ChannelSegments, { options: OPTIONS, value: value, onChange: function (v) { touched.current = true; setValue(v); } }),
h("div", { className: "flex items-center justify-between px-1 text-[11px] text-muted-foreground" },
h("span", { className: "tabular-nums" }, COUNTS[value] + " conversations"),
h("span", null, current.hint || "3 channels, 4 accounts")));
}
ReactDOM.createRoot(document.getElementById("root")).render(h(DC.TooltipProvider, { delayDuration: 80 }, h(App)));
})();
</script>