diff --git a/apps/dashboard/app/(app)/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx b/apps/dashboard/app/(app)/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx index e994388b66..869b10ff64 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/_components/create-key/components/key-created-success-dialog.tsx @@ -1,11 +1,10 @@ "use client"; -import { RatelimitOverviewTooltip } from "@/app/(app)/ratelimits/[namespaceId]/_overview/components/table/components/ratelimit-overview-tooltip"; import { ConfirmPopover } from "@/components/confirmation-popover"; import { Dialog, DialogContent } from "@/components/ui/dialog"; import { toast } from "@/components/ui/toaster"; import { ArrowRight, Check, CircleInfo, Eye, EyeSlash, Key2, Plus } from "@unkey/icons"; -import { Button, CopyButton } from "@unkey/ui"; +import { Button, CopyButton, InfoTooltip } from "@unkey/ui"; import { useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import { UNNAMED_KEY } from "../create-key.constants"; @@ -179,16 +178,17 @@ export const KeyCreatedSuccessDialog = ({
{keyData.id}
-
{keyData.name ?? UNNAMED_KEY}
-
+
- + ); }; diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/components/override-indicator.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/components/override-indicator.tsx index 1f5835e189..fbe222a6f0 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/components/override-indicator.tsx +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/components/override-indicator.tsx @@ -2,11 +2,10 @@ import { formatNumber } from "@/lib/fmt"; import { cn } from "@/lib/utils"; import type { RatelimitOverviewLog } from "@unkey/clickhouse/src/ratelimits"; import { ArrowDotAntiClockwise, Focus, TriangleWarning2 } from "@unkey/icons"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@unkey/ui"; +import { InfoTooltip } from "@unkey/ui"; import ms from "ms"; import { calculateBlockedPercentage } from "../utils/calculate-blocked-percentage"; import { getStatusStyle } from "../utils/get-row-class"; -import { RatelimitOverviewTooltip } from "./ratelimit-overview-tooltip"; type IdentifierColumnProps = { log: RatelimitOverviewLog; @@ -25,9 +24,10 @@ export const IdentifierColumn = ({ log }: IdentifierColumnProps) => { return (
- +
{isFullyBlocked ? ( "All requests have been blocked in this timeframe" ) : ( @@ -37,13 +37,13 @@ export const IdentifierColumn = ({ log }: IdentifierColumnProps) => { blocked in this timeframe )} -

+
} >
-
+
( - - - -
-
-
+ +
+
- - -
-
- +
+
+ Custom override in effect +
-
-
- Custom override in effect -
+ {log.override && ( +
+ Limit set to {formatNumber(log.override.limit)} + requests per {ms(log.override.duration)}
- {log.override && ( -
- Limit set to{" "} - {formatNumber(log.override.limit)} - requests per {ms(log.override.duration)} -
- )} -
+ )}
- - - +
+ } + asChild + > +
+
+
+
+ ); diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/components/ratelimit-overview-tooltip.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/components/ratelimit-overview-tooltip.tsx deleted file mode 100644 index 4793da1f36..0000000000 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/_overview/components/table/components/ratelimit-overview-tooltip.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@unkey/ui"; -import type { PropsWithChildren } from "react"; - -type TooltipPosition = { - side?: "top" | "right" | "bottom" | "left"; - align?: "start" | "center" | "end"; - sideOffset?: number; -}; - -export const RatelimitOverviewTooltip = ({ - content, - children, - position, - disabled = false, - asChild = false, -}: PropsWithChildren<{ - content: React.ReactNode; - position?: TooltipPosition; - disabled?: boolean; - asChild?: boolean; -}>) => { - return ( - - - {children} - - {content} - - - - ); -}; diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/namespace-navbar.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/namespace-navbar.tsx index 9ed531abe2..3fd6205163 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/namespace-navbar.tsx +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/namespace-navbar.tsx @@ -85,7 +85,9 @@ export const NamespaceNavbar = ({ - setOpen(true)}>Override Identifier + setOpen(true)}> + Override Identifier + diff --git a/apps/dashboard/app/(app)/ratelimits/_components/create-namespace-button.tsx b/apps/dashboard/app/(app)/ratelimits/_components/create-namespace-button.tsx index 456eb0d26d..7c3aa3aac2 100644 --- a/apps/dashboard/app/(app)/ratelimits/_components/create-namespace-button.tsx +++ b/apps/dashboard/app/(app)/ratelimits/_components/create-namespace-button.tsx @@ -66,7 +66,12 @@ export const CreateNamespaceButton = ({ return ( <> - setIsOpen(true)}> + setIsOpen(true)} + > Create new namespace diff --git a/apps/dashboard/components/dashboard/root-key-table/index.tsx b/apps/dashboard/components/dashboard/root-key-table/index.tsx index e6a8c6db18..b96dfefef8 100644 --- a/apps/dashboard/components/dashboard/root-key-table/index.tsx +++ b/apps/dashboard/components/dashboard/root-key-table/index.tsx @@ -1,6 +1,5 @@ "use client"; -import { RatelimitOverviewTooltip } from "@/app/(app)/ratelimits/[namespaceId]/_overview/components/table/components/ratelimit-overview-tooltip"; import { Alert } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; import { @@ -21,7 +20,7 @@ import { import { toast } from "@/components/ui/toaster"; import { trpc } from "@/lib/trpc/client"; import type { ColumnDef } from "@tanstack/react-table"; -import { Button, Checkbox } from "@unkey/ui"; +import { Button, Checkbox, InfoTooltip } from "@unkey/ui"; import { ArrowUpDown, Minus, MoreHorizontal, MoreVertical, Trash } from "lucide-react"; import ms from "ms"; import Link from "next/link"; @@ -88,17 +87,19 @@ export const RootKeyTable: React.FC = ({ data }) => { accessorKey: "start", header: "Key", cell: ({ row }) => ( - + This is the first part of the key to visually match it. We don't store the full key + for security reasons. +

+ } > {row.getValue("start")}... -
+ ), }, { diff --git a/apps/dashboard/components/logs/details/request-response-details.tsx b/apps/dashboard/components/logs/details/request-response-details.tsx index 961c2c45b8..5f1f93015d 100644 --- a/apps/dashboard/components/logs/details/request-response-details.tsx +++ b/apps/dashboard/components/logs/details/request-response-details.tsx @@ -1,6 +1,6 @@ -import { RatelimitOverviewTooltip } from "@/app/(app)/ratelimits/[namespaceId]/_overview/components/table/components/ratelimit-overview-tooltip"; import { toast } from "@/components/ui/toaster"; import { cn } from "@/lib/utils"; +import { InfoTooltip } from "@unkey/ui"; import type { ReactNode } from "react"; type Field = { @@ -71,7 +71,7 @@ export const RequestResponseDetails = ({ fields, className )} onClick={field.skipTooltip ? undefined : () => handleClick(field)} > - {field.label} + {field.label} {field.description(field.content as NonNullable)} @@ -83,17 +83,16 @@ export const RequestResponseDetails = ({ fields, className } return ( - {baseContent} - + ); }; diff --git a/apps/dashboard/components/logs/llm-search/components/search-example-tooltip.tsx b/apps/dashboard/components/logs/llm-search/components/search-example-tooltip.tsx index c94b7a07b4..56e23caf33 100644 --- a/apps/dashboard/components/logs/llm-search/components/search-example-tooltip.tsx +++ b/apps/dashboard/components/logs/llm-search/components/search-example-tooltip.tsx @@ -1,5 +1,5 @@ import { CaretRightOutline, CircleInfoSparkle } from "@unkey/icons"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@unkey/ui"; +import { InfoTooltip } from "@unkey/ui"; type SearchExampleTooltipProps = { onSelectExample: (query: string) => void; @@ -17,40 +17,35 @@ export const SearchExampleTooltip: React.FC = ({ ]; return ( - - - -
- + +
+ Try queries like: + (click to use)
- - -
-
- Try queries like: - (click to use) -
-
    - {examples.map((example) => ( -
  • - - -
  • - ))} -
-
-
- - +
    + {examples.map((example) => ( +
  • + + +
  • + ))} +
+
+ } + delayDuration={150} + > +
+ +
+ ); }; diff --git a/apps/dashboard/components/logs/queries/list-group.tsx b/apps/dashboard/components/logs/queries/list-group.tsx index 0d1298249b..bcf26ddd52 100644 --- a/apps/dashboard/components/logs/queries/list-group.tsx +++ b/apps/dashboard/components/logs/queries/list-group.tsx @@ -1,7 +1,7 @@ import { toast } from "@/components/ui/toaster"; import { cn } from "@/lib/utils"; import { Bookmark, CircleCheck, Layers2 } from "@unkey/icons"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@unkey/ui"; +import { InfoTooltip } from "@unkey/ui"; import { useEffect, useState } from "react"; import { useQueries } from "./queries-context"; import { QueriesItemRow } from "./queries-item-row"; @@ -51,7 +51,6 @@ export function ListGroup({ selectedIndex, changeBookmark, }: ListGroupProps) { - const [toolTipOpen, setToolTipOpen] = useState(false); const [toastMessage, setToastMessage] = useState(); const [tooltipMessage, setTooltipMessage] = useState( filterList.bookmarked ? tooltipMessageOptions.saved : tooltipMessageOptions.save, @@ -81,16 +80,11 @@ export function ListGroup({ } const handleMouseEnter = () => { - setToolTipOpen(true); setTooltipMessage( filterList.bookmarked ? tooltipMessageOptions.remove : tooltipMessageOptions.save, ); }; - const handleMouseLeave = () => { - setToolTipOpen(false); - }; - const handleSelection = () => { applyFilterGroup(filterList.id); }; @@ -147,33 +141,24 @@ export function ListGroup({
- - - - - + +
{ } return ( - - - - - + {list?.map((item) => { return ( @@ -28,7 +23,9 @@ export const QueriesOverflow = ({ list }: QueriesOverflowProps) => { ); })} - - + } + > + + ); }; diff --git a/apps/dashboard/components/logs/refresh-button/index.tsx b/apps/dashboard/components/logs/refresh-button/index.tsx index b340258f86..af3d9465fd 100644 --- a/apps/dashboard/components/logs/refresh-button/index.tsx +++ b/apps/dashboard/components/logs/refresh-button/index.tsx @@ -1,7 +1,7 @@ -import { RatelimitOverviewTooltip } from "@/app/(app)/ratelimits/[namespaceId]/_overview/components/table/components/ratelimit-overview-tooltip"; import { KeyboardButton } from "@/components/keyboard-button"; import { useKeyboardShortcut } from "@/hooks/use-keyboard-shortcut"; import { Refresh3 } from "@unkey/icons"; +import { InfoTooltip } from "@unkey/ui"; import { Button } from "@unkey/ui"; import { useState } from "react"; @@ -47,8 +47,9 @@ export const RefreshButton = ({ onRefresh, isEnabled, isLive, toggleLive }: Refr }); return ( -
- + ); }; diff --git a/apps/dashboard/components/navigation/sidebar/team-switcher.tsx b/apps/dashboard/components/navigation/sidebar/team-switcher.tsx index c7fc4ec512..8eba0b8208 100644 --- a/apps/dashboard/components/navigation/sidebar/team-switcher.tsx +++ b/apps/dashboard/components/navigation/sidebar/team-switcher.tsx @@ -1,5 +1,4 @@ "use client"; -import { RatelimitOverviewTooltip } from "@/app/(app)/ratelimits/[namespaceId]/_overview/components/table/components/ratelimit-overview-tooltip"; import { Loading } from "@/components/dashboard/loading"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { @@ -18,6 +17,7 @@ import { setSessionCookie } from "@/lib/auth/cookies"; import { trpc } from "@/lib/trpc/client"; import { cn } from "@/lib/utils"; import { ChevronExpandY } from "@unkey/icons"; +import { InfoTooltip } from "@unkey/ui"; import { Check, Plus, UserPlus } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; @@ -110,13 +110,15 @@ export const WorkspaceSwitcher: React.FC = (props): JSX.Element => { {isUserMembershipsLoading ? ( ) : isCollapsed ? null : ( - {props.workspace.name}} + {props.workspace.name}} + className="text-xs font-medium py-2" + triggerClassName="overflow-hidden text-sm font-medium text-ellipsis" > - - {props.workspace.name} - - + {props.workspace.name} + )}
{!isCollapsed && ( diff --git a/apps/dashboard/components/stats-card/components/chart/stats-chart.tsx b/apps/dashboard/components/stats-card/components/chart/stats-chart.tsx index b32be9f4a5..f088f3f0e7 100644 --- a/apps/dashboard/components/stats-card/components/chart/stats-chart.tsx +++ b/apps/dashboard/components/stats-card/components/chart/stats-chart.tsx @@ -76,7 +76,7 @@ export function StatsTimeseriesBarChart({ label={label} active={active} bottomExplainer={ -
+
@@ -96,7 +96,7 @@ export function StatsTimeseriesBarChart({ {tooltipExtraContent?.(payload)}
} - className="rounded-lg shadow-lg border border-gray-4" + className="rounded-lg shadow-lg border border-grayA-4" labelFormatter={(_, payload) => createTimeIntervalFormatter(data)(payload)} /> ); diff --git a/apps/dashboard/components/stats-card/index.tsx b/apps/dashboard/components/stats-card/index.tsx index 24d0d5fbcc..857a39cad9 100644 --- a/apps/dashboard/components/stats-card/index.tsx +++ b/apps/dashboard/components/stats-card/index.tsx @@ -1,6 +1,6 @@ "use client"; import { ProgressBar } from "@unkey/icons"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@unkey/ui"; +import { InfoTooltip } from "@unkey/ui"; import Link from "next/link"; import type { ReactNode } from "react"; @@ -32,28 +32,18 @@ export const StatsCard = ({
{icon} - - -
- {name} -
-
- + +
{name} - - +
+
{secondaryId && ( - - -
- {secondaryId} -
-
- + +
{secondaryId} - - +
+
)}
{rightContent &&
{rightContent}
} diff --git a/apps/engineering/content/design/components/info-tooltip.example.tsx b/apps/engineering/content/design/components/info-tooltip.example.tsx new file mode 100644 index 0000000000..c9e70bd943 --- /dev/null +++ b/apps/engineering/content/design/components/info-tooltip.example.tsx @@ -0,0 +1,42 @@ +import { RenderComponentWithSnippet } from "@/app/components/render"; +import { Row } from "@/app/components/row"; +import { InfoTooltip } from "@unkey/ui"; + +export function InfoTooltipExample() { + return ( +
+

InfoTooltip Position Side

+ +
+ + Right Tooltip + + Left Tooltip + + + Top Tooltip + + + + + Bottom Tooltip + + + Custom Alignment + + + Disabled Tooltip + + +
+
+
+ ); +} diff --git a/apps/engineering/content/design/components/info-tooltip.mdx b/apps/engineering/content/design/components/info-tooltip.mdx new file mode 100644 index 0000000000..d101a87001 --- /dev/null +++ b/apps/engineering/content/design/components/info-tooltip.mdx @@ -0,0 +1,39 @@ +--- +title: InfoTooltip +description: The Info Tooltip is a specialized tooltip component that provides contextual information about elements in a consistent and accessible way. It's built on top of the base Tooltip component with additional styling and positioning options. +--- +import { InfoTooltipExample } from "./info-tooltip.example" + +## Features + +- Customizable positioning (top, right, bottom, left) +- Alignment control (start, center, end) +- Side offset adjustment +- Disabled state support +- Child element support via `asChild` prop + +## Usage + + + +## Props + +| Prop | Type | Default | Description | +|-----------|-------------------------|-----------|--------------------------------------------------| +| content | React.ReactNode | - | The content to display in the tooltip | +| position | TooltipPosition | - | Configuration for tooltip positioning | +| disabled | boolean | false | Whether the tooltip is disabled | +| asChild | boolean | false | Whether to render the trigger as a child element | +| delayDuration | number | - | Delay before showing the tooltip in milliseconds | +| variant | string | - | Styling variant for the tooltip | +| className | string | - | Additional CSS classes for the tooltip | + +## Position Configuration + +The `position` prop accepts an object with the following properties: + +| Property | Type | Default | Description | +|-------------|-------------------------|-----------|--------------------------------------------------| +| side | "top" \| "right" \| "bottom" \| "left" | "right" | The side where the tooltip appears | +| align | "start" \| "center" \| "end" | "center" | The alignment of the tooltip | +| sideOffset | number | - | The offset from the side in pixels | \ No newline at end of file diff --git a/internal/ui/src/components/date-time/components/time-split.tsx b/internal/ui/src/components/date-time/components/time-split.tsx index f66e847a6f..95af92dc0e 100644 --- a/internal/ui/src/components/date-time/components/time-split.tsx +++ b/internal/ui/src/components/date-time/components/time-split.tsx @@ -127,7 +127,7 @@ const TimeSplitInput: React.FC = ({ type }) => {
diff --git a/internal/ui/src/components/id.tsx b/internal/ui/src/components/id.tsx index b0d733858a..fccbb62fce 100644 --- a/internal/ui/src/components/id.tsx +++ b/internal/ui/src/components/id.tsx @@ -2,7 +2,7 @@ import { TaskChecked, TaskUnchecked } from "@unkey/icons"; import * as React from "react"; import { cn } from "../lib/utils"; -import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip"; +import { InfoTooltip } from "./info-tooltip"; type IdProps = { /** @@ -55,20 +55,15 @@ export const Id: React.FC = ({ className, value, truncate, ...props }) {...props} > {truncateValue} - -
- -
- {isCopied ? ( - - ) : ( - - )} -
-
- Copy ID + +
+ {isCopied ? ( + + ) : ( + + )}
- +
); }; diff --git a/internal/ui/src/components/info-tooltip.tsx b/internal/ui/src/components/info-tooltip.tsx new file mode 100644 index 0000000000..af2714d31d --- /dev/null +++ b/internal/ui/src/components/info-tooltip.tsx @@ -0,0 +1,61 @@ +// biome-ignore lint: React in this context is used throughout, so biome will change to types because no APIs are used even though React is needed. +import React, { type PropsWithChildren } from "react"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip"; +import { cn } from "../lib/utils"; +const baseVariant = + "px-3 py-2 text-xs font-medium shadow-md rounded-lg focus:border focus:border-accent-12 focus:ring-2 focus:ring-grayA-4 focus-visible:outline-none focus:ring-offset-0"; +const variants = { + primary: ["border border-grayA-4 bg-white dark:bg-black"], + inverted: ["bg-black dark:bg-white text-gray-1 border border-grayA-4"], + secondary: ["border dark:border-gray-12 text-gray-12 text-sm"], + muted: ["border border-grayA-4 text-gray-12 text-sm"], +} as const; + +type TooltipVariant = keyof typeof variants; + +type TooltipPosition = { + side?: "top" | "right" | "bottom" | "left"; + align?: "start" | "center" | "end"; + sideOffset?: number; +}; + +const InfoTooltip = ({ + delayDuration, + content, + children, + position, + disabled = false, + asChild = false, + className, + variant = "primary", + triggerClassName, +}: PropsWithChildren<{ + variant?: TooltipVariant; + delayDuration?: number; + content: React.ReactNode; + position?: TooltipPosition; + disabled?: boolean; + asChild?: boolean; + className?: string; + triggerClassName?: string; +}>) => { + return ( + + + + {children} + + + {content} + + + + ); +}; + +export { InfoTooltip }; diff --git a/internal/ui/src/components/settings-card.tsx b/internal/ui/src/components/settings-card.tsx index 1141708592..29a453706e 100644 --- a/internal/ui/src/components/settings-card.tsx +++ b/internal/ui/src/components/settings-card.tsx @@ -27,7 +27,7 @@ export function SettingCard({ }; const borderClass = { - "border border-gray-4": border !== "none", + "border border-grayA-4": border !== "none", "border-t-0": border === "bottom", "border-b-0": border === "top", }; diff --git a/internal/ui/src/components/timestamp-info.tsx b/internal/ui/src/components/timestamp-info.tsx index c63a8d574a..84ffaef4db 100644 --- a/internal/ui/src/components/timestamp-info.tsx +++ b/internal/ui/src/components/timestamp-info.tsx @@ -142,7 +142,7 @@ export const TimestampInfo: React.FC<{
diff --git a/internal/ui/src/components/tooltip.tsx b/internal/ui/src/components/tooltip.tsx index 2b3ef06303..0dd94ab052 100644 --- a/internal/ui/src/components/tooltip.tsx +++ b/internal/ui/src/components/tooltip.tsx @@ -19,7 +19,7 @@ const TooltipContent = React.forwardRef< ref={ref} sideOffset={sideOffset} className={cn( - "z-50 overflow-hidden font-sans font-medium shadow-md rounded-lg leading-6 bg-gray-12 text-gray-1 px-2 py-1 gap-2 animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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", + "z-50 bg-gray-1 text-gray-12 overflow-hidden rounded-md px-3 py-1.5 text-sm shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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", className, )} {...props} diff --git a/internal/ui/src/index.ts b/internal/ui/src/index.ts index 545c23f2b9..518ddec306 100644 --- a/internal/ui/src/index.ts +++ b/internal/ui/src/index.ts @@ -6,6 +6,7 @@ export * from "./components/form"; export * from "./components/id"; export * from "./components/inline-link"; export * from "./components/input"; +export * from "./components/info-tooltip"; export * from "./components/select"; export * from "./components/settings-card"; export * from "./components/textarea";