fix: large org routing insights UI issues (#17687)
* fix overflow colour in darkmode * truncate/show overflow scroll indicator when overflowing on response values * remove total created count * overflow Y on failed response container * fix darkmode styles on barlist * radix hovercards to properly allow overflow in container * column visible state * remove constrained width on dropdowns to w-fit * export type
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
RoutingFormResponsesTable,
|
||||
RoutingKPICards,
|
||||
FailedBookingsByField,
|
||||
type RoutingFormTableType,
|
||||
} from "@calcom/features/insights/components";
|
||||
import { FiltersProvider } from "@calcom/features/insights/context/FiltersProvider";
|
||||
import { Filters } from "@calcom/features/insights/filters";
|
||||
@@ -13,16 +16,17 @@ import InsightsLayout from "./layout";
|
||||
|
||||
export default function InsightsPage() {
|
||||
const { t } = useLocale();
|
||||
const [routingTable, setRoutingTable] = useState<RoutingFormTableType | null>(null);
|
||||
|
||||
return (
|
||||
<InsightsLayout>
|
||||
<FiltersProvider>
|
||||
<Filters showRoutingFilters />
|
||||
<Filters showRoutingFilters routingTable={routingTable} />
|
||||
|
||||
<div className="mb-4 space-y-4">
|
||||
<RoutingKPICards />
|
||||
|
||||
<RoutingFormResponsesTable />
|
||||
<RoutingFormResponsesTable onTableReady={setRoutingTable} />
|
||||
|
||||
<FailedBookingsByField />
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { BarList } from "@tremor/react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
@@ -6,6 +5,7 @@ import { trpc } from "@calcom/trpc";
|
||||
import { ToggleGroup } from "@calcom/ui";
|
||||
|
||||
import { useFilterContext } from "../context/provider";
|
||||
import { BarList } from "./tremor/BarList";
|
||||
|
||||
interface FormCardProps {
|
||||
formName: string;
|
||||
@@ -27,7 +27,7 @@ function FormCard({ formName, fields }: FormCardProps) {
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="border-subtle h-[400px] w-full rounded-md border">
|
||||
<div className="border-subtle w-full rounded-md border">
|
||||
<div className="flex flex-col">
|
||||
<div className="bg-subtle border-subtle flex h-12 items-center border-b">
|
||||
<h3 className="text-default px-2 text-left align-middle font-medium">{formName}</h3>
|
||||
@@ -40,7 +40,7 @@ function FormCard({ formName, fields }: FormCardProps) {
|
||||
onValueChange={(value) => value && setSelectedField(value)}
|
||||
/>
|
||||
{selectedFieldData && (
|
||||
<div className="scrollbar-thin mt-4 h-full overflow-y-auto">
|
||||
<div className="scrollbar-thin mt-4 h-[400px] overflow-y-auto">
|
||||
<BarList
|
||||
data={selectedFieldData}
|
||||
valueFormatter={(value: number) => value.toString()}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
createColumnHelper,
|
||||
} from "@tanstack/react-table";
|
||||
import Link from "next/link";
|
||||
import { useRef, useMemo, useId } from "react";
|
||||
import { useRef, useMemo, useId, useEffect } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
@@ -17,7 +17,16 @@ import { useCopy } from "@calcom/lib/hooks/useCopy";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { trpc, type RouterOutputs } from "@calcom/trpc";
|
||||
import { DataTable, useFetchMoreOnBottomReached, Badge, Avatar, Icon } from "@calcom/ui";
|
||||
import {
|
||||
DataTable,
|
||||
useFetchMoreOnBottomReached,
|
||||
Badge,
|
||||
Avatar,
|
||||
Icon,
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@calcom/ui";
|
||||
import type { BadgeProps } from "@calcom/ui/components/badge/Badge";
|
||||
|
||||
import { useFilterContext } from "../context/provider";
|
||||
@@ -36,7 +45,7 @@ function CellWithOverflowX({ children, className }: { children: React.ReactNode;
|
||||
return (
|
||||
<div className={classNames("group relative max-w-[200px]", className)}>
|
||||
<div
|
||||
className="no-scrollbar overflow-x-auto whitespace-nowrap"
|
||||
className="no-scrollbar flex gap-1 overflow-x-auto whitespace-nowrap"
|
||||
ref={(el) => {
|
||||
// Only add shadow if the scroll width is greater than 200px
|
||||
if (!el) return;
|
||||
@@ -51,7 +60,7 @@ function CellWithOverflowX({ children, className }: { children: React.ReactNode;
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
<div className="absolute right-0 top-0 hidden h-full w-8 bg-gradient-to-l from-white to-transparent group-hover:from-gray-100" />
|
||||
<div className="from-default absolute right-0 top-0 hidden h-full w-8 bg-gradient-to-l to-transparent " />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -92,7 +101,7 @@ function ResponseValueCell({ value, rowId }: { value: string[]; rowId: number })
|
||||
if (value.length === 0) return <div className="h-6 w-[200px]" />;
|
||||
|
||||
return (
|
||||
<div className="flex w-[200px] flex-wrap gap-1">
|
||||
<CellWithOverflowX className="flex w-[200px] gap-1">
|
||||
{value.length > 2 ? (
|
||||
<>
|
||||
{value.slice(0, 2).map((v: string, i: number) => (
|
||||
@@ -100,18 +109,20 @@ function ResponseValueCell({ value, rowId }: { value: string[]; rowId: number })
|
||||
{v}
|
||||
</Badge>
|
||||
))}
|
||||
<div className="group/badge relative">
|
||||
<Badge variant="gray">+{value.length - 2}</Badge>
|
||||
<div className="bg-default invisible absolute left-0 top-full z-20 translate-y-[-8px] rounded-md p-2 opacity-0 shadow-md transition-all duration-200 group-hover/badge:visible group-hover/badge:translate-y-0 group-hover/badge:opacity-100">
|
||||
<HoverCard>
|
||||
<HoverCardTrigger>
|
||||
<Badge variant="gray">+{value.length - 2}</Badge>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent side="bottom" align="start" className="w-fit">
|
||||
<div className="flex flex-col gap-1">
|
||||
{value.slice(2).map((v: string, i: number) => (
|
||||
<span key={`${cellId}-overflow-${i}-${rowId}`} className="text-sm text-gray-600">
|
||||
<span key={`${cellId}-overflow-${i}-${rowId}`} className="text-default text-sm">
|
||||
{v}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</>
|
||||
) : (
|
||||
value.map((v: string, i: number) => (
|
||||
@@ -120,7 +131,7 @@ function ResponseValueCell({ value, rowId }: { value: string[]; rowId: number })
|
||||
</Badge>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</CellWithOverflowX>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -158,12 +169,16 @@ function BookingStatusCell({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group/booking_status relative flex items-center gap-2" key={`${cellId}-booking-${rowId}`}>
|
||||
<Avatar size="xs" imageSrc={booking.user.avatarUrl ?? ""} alt={booking.user.name ?? ""} />
|
||||
<Link href={`/booking/${booking.uid}`}>
|
||||
<Badge variant={badgeVariant}>{dayjs(booking.createdAt).format("MMM D, YYYY HH:mm")}</Badge>
|
||||
</Link>
|
||||
<div className="bg-default invisible absolute left-0 top-full z-20 translate-y-[-8px] rounded-md p-2 opacity-0 shadow-md transition-all duration-200 group-hover/booking_status:visible group-hover/booking_status:translate-y-0 group-hover/booking_status:opacity-100 ">
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<div className="flex items-center gap-2" key={`${cellId}-booking-${rowId}`}>
|
||||
<Avatar size="xs" imageSrc={booking.user.avatarUrl ?? ""} alt={booking.user.name ?? ""} />
|
||||
<Link href={`/booking/${booking.uid}`}>
|
||||
<Badge variant={badgeVariant}>{dayjs(booking.createdAt).format("MMM D, YYYY HH:mm")}</Badge>
|
||||
</Link>
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar size="sm" imageSrc={booking.user.avatarUrl ?? ""} alt={booking.user.name ?? ""} />
|
||||
@@ -188,12 +203,18 @@ function BookingStatusCell({
|
||||
<Badge variant={badgeVariant}>{bookingStatusToText(booking.status)}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
);
|
||||
}
|
||||
|
||||
export function RoutingFormResponsesTable() {
|
||||
export type RoutingFormTableType = ReturnType<typeof useReactTable<RoutingFormTableRow>>;
|
||||
|
||||
export function RoutingFormResponsesTable({
|
||||
onTableReady,
|
||||
}: {
|
||||
onTableReady?: (table: RoutingFormTableType) => void;
|
||||
}) {
|
||||
const { t } = useLocale();
|
||||
const { filter } = useFilterContext();
|
||||
const tableContainerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -345,6 +366,10 @@ export function RoutingFormResponsesTable() {
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
onTableReady?.(table);
|
||||
}, [table, onTableReady]);
|
||||
|
||||
const fetchMoreOnBottomReached = useFetchMoreOnBottomReached(
|
||||
tableContainerRef,
|
||||
fetchNextPage,
|
||||
|
||||
@@ -51,17 +51,8 @@ export const RoutingKPICards = () => {
|
||||
|
||||
const categories: {
|
||||
title: string;
|
||||
index:
|
||||
| "created"
|
||||
| "active"
|
||||
| "total_responses"
|
||||
| "total_responses_without_booking"
|
||||
| "total_responses_with_booking";
|
||||
index: "active" | "total_responses" | "total_responses_without_booking" | "total_responses_with_booking";
|
||||
}[] = [
|
||||
{
|
||||
title: t("routing_forms_created"),
|
||||
index: "created",
|
||||
},
|
||||
{
|
||||
title: t("routing_forms_total_responses"),
|
||||
index: "total_responses",
|
||||
@@ -84,7 +75,7 @@ export const RoutingKPICards = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid numColsSm={2} numColsLg={4} className="mt-4 gap-x-4 gap-y-4">
|
||||
<Grid numColsSm={1} numColsLg={3} className="mt-4 gap-x-4 gap-y-4">
|
||||
{categories.map((item) => (
|
||||
<CardInsights key={item.title}>
|
||||
<Text className="text-default">{item.title}</Text>
|
||||
|
||||
@@ -9,5 +9,5 @@ export { RecentFeedbackTable } from "./RecentFeedbackTable";
|
||||
export { HighestNoShowHostTable } from "./HighestNoShowHostTable";
|
||||
export { HighestRatedMembersTable } from "./HighestRatedMembersTable";
|
||||
export { LowestRatedMembersTable } from "./LowestRatedMembersTable";
|
||||
export { RoutingFormResponsesTable } from "./RoutingFormResponsesTable";
|
||||
export { RoutingFormResponsesTable, type RoutingFormTableType } from "./RoutingFormResponsesTable";
|
||||
export { FailedBookingsByField } from "./FailedBookingsByField";
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
// Tremor BarList [v0.1.1]
|
||||
import React from "react";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
|
||||
type Bar<T> = T & {
|
||||
key?: string;
|
||||
href?: string;
|
||||
value: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
const focusRing = [
|
||||
// base
|
||||
"outline outline-offset-2 outline-0 focus-visible:outline-2",
|
||||
// outline color
|
||||
"outline-brand",
|
||||
];
|
||||
|
||||
interface BarListProps<T = unknown> extends React.HTMLAttributes<HTMLDivElement> {
|
||||
data: Bar<T>[];
|
||||
valueFormatter?: (value: number) => string;
|
||||
showAnimation?: boolean;
|
||||
onValueChange?: (payload: Bar<T>) => void;
|
||||
sortOrder?: "ascending" | "descending" | "none";
|
||||
}
|
||||
|
||||
function BarListInner<T>(
|
||||
{
|
||||
data = [],
|
||||
valueFormatter = (value) => value.toString(),
|
||||
showAnimation = false,
|
||||
onValueChange,
|
||||
sortOrder = "descending",
|
||||
className,
|
||||
...props
|
||||
}: BarListProps<T>,
|
||||
forwardedRef: React.ForwardedRef<HTMLDivElement>
|
||||
) {
|
||||
const Component = onValueChange ? "button" : "div";
|
||||
const sortedData = React.useMemo(() => {
|
||||
if (sortOrder === "none") {
|
||||
return data;
|
||||
}
|
||||
return [...data].sort((a, b) => {
|
||||
return sortOrder === "ascending" ? a.value - b.value : b.value - a.value;
|
||||
});
|
||||
}, [data, sortOrder]);
|
||||
|
||||
const widths = React.useMemo(() => {
|
||||
const maxValue = Math.max(...sortedData.map((item) => item.value), 0);
|
||||
return sortedData.map((item) => (item.value === 0 ? 0 : Math.max((item.value / maxValue) * 100, 2)));
|
||||
}, [sortedData]);
|
||||
|
||||
const rowHeight = "h-8";
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={forwardedRef}
|
||||
className={classNames("flex justify-between space-x-6", className)}
|
||||
aria-sort={sortOrder}
|
||||
tremor-id="tremor-raw"
|
||||
{...props}>
|
||||
<div className="relative w-full space-y-1.5">
|
||||
{sortedData.map((item, index) => (
|
||||
<Component
|
||||
key={item.key ?? item.name}
|
||||
onClick={() => {
|
||||
onValueChange?.(item);
|
||||
}}
|
||||
className={classNames(
|
||||
// base
|
||||
"group w-full rounded",
|
||||
// focus
|
||||
focusRing,
|
||||
onValueChange
|
||||
? [
|
||||
"!-m-0 cursor-pointer",
|
||||
// hover
|
||||
"hover:bg-gray-50 hover:dark:bg-gray-900",
|
||||
]
|
||||
: ""
|
||||
)}>
|
||||
<div
|
||||
className={classNames(
|
||||
// base
|
||||
"flex items-center rounded transition-all",
|
||||
rowHeight,
|
||||
// background color
|
||||
"bg-blue-200 dark:bg-blue-900",
|
||||
onValueChange ? "group-hover:bg-blue-300 group-hover:dark:bg-blue-800" : "",
|
||||
// margin and duration
|
||||
{
|
||||
"mb-0": index === sortedData.length - 1,
|
||||
"duration-800": showAnimation,
|
||||
}
|
||||
)}
|
||||
style={{ width: `${widths[index]}%` }}>
|
||||
<div className={classNames("absolute left-2 flex max-w-full pr-2")}>
|
||||
{item.href ? (
|
||||
<a
|
||||
href={item.href}
|
||||
className={classNames(
|
||||
// base
|
||||
"truncate whitespace-nowrap rounded text-sm",
|
||||
// text color
|
||||
"text-default",
|
||||
// hover
|
||||
"hover:underline hover:underline-offset-2",
|
||||
// focus
|
||||
focusRing
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={(event) => event.stopPropagation()}>
|
||||
{item.name}
|
||||
</a>
|
||||
) : (
|
||||
<p
|
||||
className={classNames(
|
||||
// base
|
||||
"truncate whitespace-nowrap text-sm",
|
||||
// text color
|
||||
"text-default"
|
||||
)}>
|
||||
{item.name}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Component>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
{sortedData.map((item, index) => (
|
||||
<div
|
||||
key={item.key ?? item.name}
|
||||
className={classNames(
|
||||
"flex items-center justify-end",
|
||||
rowHeight,
|
||||
index === sortedData.length - 1 ? "mb-0" : "mb-1.5"
|
||||
)}>
|
||||
<p
|
||||
className={classNames(
|
||||
// base
|
||||
"truncate whitespace-nowrap text-sm leading-none",
|
||||
// text color
|
||||
"text-default"
|
||||
)}>
|
||||
{valueFormatter(item.value)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
BarListInner.displayName = "BarList";
|
||||
|
||||
const BarList = React.forwardRef(BarListInner) as <T>(
|
||||
p: BarListProps<T> & { ref?: React.ForwardedRef<HTMLDivElement> }
|
||||
) => ReturnType<typeof BarListInner>;
|
||||
|
||||
export { BarList, type BarListProps };
|
||||
@@ -108,9 +108,9 @@ export const FilterType = ({ showRoutingFilters = false }: { showRoutingFilters?
|
||||
</Tooltip>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="z-50 w-44">
|
||||
<DropdownMenuContent className="z-50">
|
||||
{filterOptions?.map((option) => (
|
||||
<DropdownMenuItem key={option.label} className="w-44">
|
||||
<DropdownMenuItem key={option.label} className="w-full">
|
||||
<DropdownItem
|
||||
type="button"
|
||||
StartIcon={option.StartIcon}
|
||||
@@ -124,9 +124,11 @@ export const FilterType = ({ showRoutingFilters = false }: { showRoutingFilters?
|
||||
});
|
||||
}}
|
||||
childrenClassName="w-full">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
{t(option.label)}
|
||||
{selectedFilter?.includes(option.value) && <Icon name="check" className="h-4 w-4" />}
|
||||
<div className="flex w-full items-center justify-between whitespace-normal">
|
||||
<span className="mr-2">{t(option.label)}</span>
|
||||
{selectedFilter?.includes(option.value) && (
|
||||
<Icon name="check" className="h-4 w-4 flex-shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { RoutingFormTableType } from "@calcom/features/insights/components/RoutingFormResponsesTable";
|
||||
import { useFilterContext } from "@calcom/features/insights/context/provider";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Icon, Tooltip } from "@calcom/ui";
|
||||
import { Button, Icon, Tooltip, DataTableFilters } from "@calcom/ui";
|
||||
|
||||
import { BookingStatusFilter } from "./BookingStatusFilter";
|
||||
import { DateSelect } from "./DateSelect";
|
||||
@@ -37,7 +38,13 @@ const ClearFilters = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Filters = ({ showRoutingFilters = false }: { showRoutingFilters?: boolean }) => {
|
||||
export const Filters = ({
|
||||
showRoutingFilters = false,
|
||||
routingTable = null,
|
||||
}: {
|
||||
showRoutingFilters?: boolean;
|
||||
routingTable?: RoutingFormTableType | null;
|
||||
}) => {
|
||||
const { filter } = useFilterContext();
|
||||
const { selectedFilter } = filter;
|
||||
|
||||
@@ -98,6 +105,8 @@ export const Filters = ({ showRoutingFilters = false }: { showRoutingFilters?: b
|
||||
<div className="flex flex-col-reverse sm:flex-row sm:flex-nowrap sm:justify-between">
|
||||
{showRoutingFilters ? <RoutingDownload /> : <Download />}
|
||||
<DateSelect />
|
||||
|
||||
{routingTable && <DataTableFilters.ColumnVisibilityButton table={routingTable} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -41,6 +41,9 @@ function ColumnVisibilityButtonComponent<TData>(
|
||||
const { t } = useLocale();
|
||||
const allColumns = table.getAllLeafColumns();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [visibleColumns, setVisibleColumns] = useState(
|
||||
() => new Set(allColumns.filter((col) => col.getIsVisible()).map((col) => col.id))
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
@@ -59,15 +62,28 @@ function ColumnVisibilityButtonComponent<TData>(
|
||||
const canHide = column.getCanHide();
|
||||
if (!column.columnDef.header || typeof column.columnDef.header !== "string" || !canHide)
|
||||
return null;
|
||||
const isVisible = column.getIsVisible();
|
||||
const isVisible = visibleColumns.has(column.id);
|
||||
return (
|
||||
<CommandItem key={column.id} onSelect={() => column.toggleVisibility(!isVisible)}>
|
||||
<CommandItem
|
||||
key={column.id}
|
||||
onSelect={() => {
|
||||
column.toggleVisibility(!isVisible);
|
||||
setVisibleColumns((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (isVisible) {
|
||||
next.delete(column.id);
|
||||
} else {
|
||||
next.add(column.id);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}}>
|
||||
<div
|
||||
className={classNames(
|
||||
"border-subtle mr-2 flex h-4 w-4 items-center justify-center rounded-sm border",
|
||||
isVisible ? "text-emphasis" : "opacity-50 [&_svg]:invisible"
|
||||
)}>
|
||||
<Icon name="check" className={classNames("h-4 w-4")} />
|
||||
<Icon name="check" className={classNames("h-4 w-4", !isVisible && "invisible")} />
|
||||
</div>
|
||||
{column.columnDef.header}
|
||||
</CommandItem>
|
||||
@@ -78,7 +94,10 @@ function ColumnVisibilityButtonComponent<TData>(
|
||||
<CommandSeparator />
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
onSelect={() => allColumns.forEach((column) => column.toggleVisibility(true))}
|
||||
onSelect={() => {
|
||||
allColumns.forEach((column) => column.toggleVisibility(true));
|
||||
setVisibleColumns(new Set(allColumns.map((col) => col.id)));
|
||||
}}
|
||||
className={classNames(
|
||||
"w-full justify-center text-center",
|
||||
buttonClasses({ color: "secondary" })
|
||||
|
||||
@@ -39,7 +39,7 @@ export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuConten
|
||||
{...props}
|
||||
sideOffset={sideOffset}
|
||||
className={classNames(
|
||||
"shadow-dropdown w-50 bg-default border-subtle relative z-10 ml-1.5 origin-top-right rounded-md border text-sm",
|
||||
"shadow-dropdown bg-default border-subtle relative z-10 ml-1.5 origin-top-right rounded-md border text-sm",
|
||||
"[&>*:first-child]:mt-1 [&>*:last-child]:mb-1",
|
||||
props.className
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
|
||||
import * as React from "react";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
|
||||
const HoverCard = HoverCardPrimitive.Root;
|
||||
|
||||
const HoverCardTrigger = HoverCardPrimitive.Trigger;
|
||||
|
||||
const HoverCardContent = React.forwardRef<
|
||||
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<HoverCardPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={classNames(
|
||||
"bg-default text-default data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-subtle z-50 w-64 rounded-md border p-2 shadow-sm outline-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
|
||||
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
||||
@@ -170,6 +170,8 @@ export {
|
||||
CommandShortcut,
|
||||
} from "./components/command";
|
||||
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent } from "./components/hover-card";
|
||||
|
||||
export { OrgBanner } from "./components/organization-banner";
|
||||
export type { OrgBannerProps } from "./components/organization-banner";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user