mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-10-19 01:50:51 -05:00
feat(admin-sidebar): one pointer-following tooltip over locked pages; logical tooltip sides (#15926)
This commit is contained in:
1 parent
e540e6f37f
commit
c9df4da344
26 files changed
+185
-77
No files matched your search
@@ -89,14 +89,14 @@ export const TooltipSides: Story = {
|
||||
<LinkButton href="/" tooltip="Tooltip on top" tooltipSide="top">
|
||||
top
|
||||
</LinkButton>
|
||||
<LinkButton href="/" tooltip="Tooltip on right" tooltipSide="right">
|
||||
right
|
||||
<LinkButton href="/" tooltip="Tooltip at the end" tooltipSide="end">
|
||||
end
|
||||
</LinkButton>
|
||||
<LinkButton href="/" tooltip="Tooltip on bottom" tooltipSide="bottom">
|
||||
bottom
|
||||
</LinkButton>
|
||||
<LinkButton href="/" tooltip="Tooltip on left" tooltipSide="left">
|
||||
left
|
||||
<LinkButton href="/" tooltip="Tooltip at the start" tooltipSide="start">
|
||||
start
|
||||
</LinkButton>
|
||||
</div>
|
||||
),
|
||||
|
||||
@@ -96,7 +96,7 @@ function FoldedTooltip({ label, folded, children }: FoldedTooltipProps) {
|
||||
/* `suppressed`, not a controlled `open`: hover stays Radix's to track, so an
|
||||
unfolded tab keeps no hover state of its own that a later fold could act on. */
|
||||
return (
|
||||
<Tooltip tooltip={label} side="right" suppressed={!effectiveFolded}>
|
||||
<Tooltip tooltip={label} side="end" suppressed={!effectiveFolded}>
|
||||
{children}
|
||||
</Tooltip>
|
||||
);
|
||||
@@ -196,7 +196,7 @@ function SidebarTab({
|
||||
const trigger =
|
||||
overlay &&
|
||||
(tooltip ? (
|
||||
<Tooltip tooltip={tooltip} side="right">
|
||||
<Tooltip tooltip={tooltip} side="end">
|
||||
{overlay}
|
||||
</Tooltip>
|
||||
) : label !== undefined ? (
|
||||
|
||||
@@ -168,7 +168,7 @@ function InputPasswordTypeIn({
|
||||
variant={isRevealed ? "action" : undefined}
|
||||
prominence="tertiary"
|
||||
size="sm"
|
||||
tooltipSide="left"
|
||||
tooltipSide="start"
|
||||
tooltip={toggleLabel}
|
||||
aria-label={toggleLabel}
|
||||
/>
|
||||
|
||||
@@ -164,7 +164,7 @@ Forwards all [Radix Tabs.Root](https://www.radix-ui.com/docs/primitives/componen
|
||||
| `value` | `string` | **required** | Tab value |
|
||||
| `icon` | `FunctionComponent<IconProps>` | — | Icon before the label |
|
||||
| `tooltip` | `string` | — | Tooltip on hover |
|
||||
| `tooltipSide` | `"top" \| "bottom" \| "left" \| "right"` | `"top"` | Tooltip placement |
|
||||
| `tooltipSide` | `"top" \| "bottom" \| "start" \| "end"` | `"top"` | Tooltip placement; `start`/`end` follow the text direction |
|
||||
| `disabled` | `boolean` | — | Disables the tab (tooltip still shows) |
|
||||
| `isLoading` | `boolean` | — | Shows a spinner after the label |
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { spacingToRem } from "@opal/shared";
|
||||
// instead of the barrel's RTL-mirrored wrappers.
|
||||
import SvgChevronLeft from "@opal/icons/chevron-left";
|
||||
import SvgChevronRight from "@opal/icons/chevron-right";
|
||||
import { Tooltip, Button } from "@opal/components";
|
||||
import { Tooltip, Button, type TooltipSide } from "@opal/components";
|
||||
import { Content } from "@opal/layouts";
|
||||
import {
|
||||
TabsContext,
|
||||
@@ -250,7 +250,7 @@ interface TabsTriggerProps extends WithoutStyles<
|
||||
/** Tooltip shown on hover. */
|
||||
tooltip?: string;
|
||||
/** Side where the tooltip appears. @default "top" */
|
||||
tooltipSide?: "top" | "bottom" | "left" | "right";
|
||||
tooltipSide?: TooltipSide;
|
||||
/** Icon rendered before the label. */
|
||||
icon?: IconFunctionComponent;
|
||||
/** Show a loading spinner after the label. */
|
||||
|
||||
@@ -13,8 +13,8 @@ off while keeping the trigger mounted.
|
||||
| Prop | Type | Default | Description |
|
||||
| --------------- | ---------------------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| `tooltip` | `ReactNode \| RichStr` | — | Tooltip content. `string`/`RichStr` rendered via `Text`; `ReactNode` rendered as-is. `undefined` = no tooltip. |
|
||||
| `side` | `"top" \| "bottom" \| "left" \| "right"` | `"right"` | Which side the tooltip appears on |
|
||||
| `align` | `"start" \| "center" \| "end"` | `"center"` | Alignment along the tooltip's side axis |
|
||||
| `side` | `"top" \| "bottom" \| "start" \| "end"` | `"end"` | Which side the tooltip appears on. `start`/`end` are logical: in a right-to-left layout `start` is the right side |
|
||||
| `align` | `"start" \| "center" \| "end" \| "follow"` | `"center"` | Alignment along the tooltip's side axis. `follow` slides it to stay level with the pointer, for a tall or wide trigger |
|
||||
| `suppressed` | `boolean` | `false` | Shows nothing on hover, but keeps the trigger mounted |
|
||||
| `delayDuration` | `number` | — | Delay in ms before the tooltip appears on hover |
|
||||
| `sideOffset` | `number` | `4` | Distance in pixels between the trigger and the tooltip |
|
||||
|
||||
@@ -64,3 +64,22 @@ it("does not reveal a hover the pointer already left when suppression lifts", as
|
||||
expect(screen.queryByRole("tooltip")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps showing and tracking the pointer with align="follow"', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<TooltipPrimitive.Provider delayDuration={0}>
|
||||
<Tooltip tooltip="Locked" align="follow">
|
||||
<div>Block</div>
|
||||
</Tooltip>
|
||||
</TooltipPrimitive.Provider>
|
||||
);
|
||||
const block = screen.getByText("Block");
|
||||
|
||||
await user.hover(block);
|
||||
await user.pointer({ target: block, coords: { clientX: 4, clientY: 40 } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("tooltip")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import "@opal/components/tooltip/styles.css";
|
||||
import { useRef, useState } from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import type { RichStr } from "@opal/types";
|
||||
import { Text } from "@opal/components";
|
||||
@@ -10,8 +11,17 @@ import { isRichStr } from "@opal/components/text/InlineMarkdown";
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type TooltipSide = "top" | "bottom" | "left" | "right";
|
||||
type TooltipAlign = "start" | "center" | "end";
|
||||
/**
|
||||
* Which side of the trigger the tooltip appears on. `start` and `end` are
|
||||
* logical: in a right-to-left layout `start` is the right side.
|
||||
*/
|
||||
type TooltipSide = "top" | "bottom" | "start" | "end";
|
||||
/**
|
||||
* Where the tooltip sits along its side. `follow` slides it to stay level
|
||||
* with the pointer: vertically for `start`/`end` sides, horizontally for
|
||||
* `top`/`bottom`. For a tall or wide trigger, such as a block of items.
|
||||
*/
|
||||
type TooltipAlign = "start" | "center" | "end" | "follow";
|
||||
|
||||
interface TooltipProps {
|
||||
/**
|
||||
@@ -23,7 +33,7 @@ interface TooltipProps {
|
||||
*/
|
||||
tooltip?: React.ReactNode | RichStr;
|
||||
|
||||
/** Which side the tooltip appears on. @default "right" */
|
||||
/** Which side the tooltip appears on. @default "end" */
|
||||
side?: TooltipSide;
|
||||
|
||||
/** Alignment along the tooltip's side axis. @default "center" */
|
||||
@@ -55,6 +65,23 @@ interface TooltipProps {
|
||||
children: React.ReactElement;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Direction
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Whether the page reads right to left. The root layout sets `dir`. */
|
||||
function isRtl(): boolean {
|
||||
return (
|
||||
typeof document !== "undefined" && document.documentElement.dir === "rtl"
|
||||
);
|
||||
}
|
||||
|
||||
/** Radix positions by physical side, so `start` and `end` follow `dir`. */
|
||||
function physicalSide(side: TooltipSide): "top" | "bottom" | "left" | "right" {
|
||||
if (side === "top" || side === "bottom") return side;
|
||||
return (side === "start") !== isRtl() ? "left" : "right";
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tooltip
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -87,15 +114,42 @@ interface TooltipProps {
|
||||
*/
|
||||
function Tooltip({
|
||||
tooltip,
|
||||
side = "right",
|
||||
side = "end",
|
||||
align = "center",
|
||||
suppressed,
|
||||
delayDuration,
|
||||
sideOffset = 4,
|
||||
children,
|
||||
}: TooltipProps) {
|
||||
// With `align="follow"`: the pointer's distance from the trigger's start
|
||||
// along the side's axis, and the tooltip's size on that axis.
|
||||
const [pointerOffset, setPointerOffset] = useState<number | null>(null);
|
||||
const [contentSize, setContentSize] = useState<number>(0);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const alongY: boolean = side === "start" || side === "end";
|
||||
|
||||
if (tooltip == null) return children;
|
||||
|
||||
function trackPointer(event: React.PointerEvent<HTMLElement>) {
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
// Measured from the trigger's start edge, which `align="start"` uses.
|
||||
setPointerOffset(
|
||||
alongY
|
||||
? event.clientY - rect.top
|
||||
: isRtl()
|
||||
? rect.right - event.clientX
|
||||
: event.clientX - rect.left
|
||||
);
|
||||
const content = contentRef.current;
|
||||
if (content) {
|
||||
setContentSize(alongY ? content.offsetHeight : content.offsetWidth);
|
||||
}
|
||||
}
|
||||
|
||||
// Aligned to the trigger's start, then moved so the tooltip's middle
|
||||
// meets the pointer. Radix still shifts it back into view at an edge.
|
||||
const following: boolean = align === "follow" && pointerOffset !== null;
|
||||
|
||||
const content =
|
||||
typeof tooltip === "string" || isRichStr(tooltip) ? (
|
||||
<Text font="secondary-body" color="inherit" as="p">
|
||||
@@ -115,14 +169,24 @@ function Tooltip({
|
||||
appears the moment the suppression lifts. */
|
||||
disableHoverableContent={suppressed}
|
||||
>
|
||||
<TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
|
||||
<TooltipPrimitive.Trigger
|
||||
asChild
|
||||
onPointerMove={align === "follow" ? trackPointer : undefined}
|
||||
>
|
||||
{children}
|
||||
</TooltipPrimitive.Trigger>
|
||||
{!suppressed && (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
/* Dark surface in both themes, so the content resolves dark tokens. */
|
||||
ref={contentRef}
|
||||
className="dark opal-tooltip"
|
||||
side={side}
|
||||
align={align}
|
||||
side={physicalSide(side)}
|
||||
// Until the pointer moves, a follower sits centered.
|
||||
align={following ? "start" : align === "follow" ? "center" : align}
|
||||
alignOffset={
|
||||
following ? (pointerOffset ?? 0) - contentSize / 2 : undefined
|
||||
}
|
||||
sideOffset={sideOffset}
|
||||
>
|
||||
{content}
|
||||
|
||||
@@ -22,7 +22,8 @@ disabled: screen readers read the reason in the region and announce it when it c
|
||||
| `disabled` | `boolean` | `false` | Applies disabled styling when truthy |
|
||||
| `allowClick` | `boolean` | `false` | Re-enables pointer events and keyboard input while keeping disabled visuals |
|
||||
| `tooltip` | `string \| RichStr` | — | Tooltip shown on hover when disabled (implies `allowClick`). Supports `markdown()`. |
|
||||
| `tooltipSide` | `"top" \| "bottom" \| "left" \| "right"` | `"right"` | Which side the tooltip appears on |
|
||||
| `tooltipSide` | `"top" \| "bottom" \| "start" \| "end"` | `"end"` | Which side the tooltip appears on |
|
||||
| `tooltipAlign` | `TooltipAlign` | `"center"` | Where the tooltip sits along its side; `"follow"` keeps it level with the pointer. See `Tooltip`'s `align` |
|
||||
|
||||
## CSS behavior
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "@opal/core/disabled/styles.css";
|
||||
import React from "react";
|
||||
import { Tooltip, type TooltipSide } from "@opal/components";
|
||||
import { Tooltip, type TooltipAlign, type TooltipSide } from "@opal/components";
|
||||
import { toPlainString } from "@opal/components/text/InlineMarkdown";
|
||||
import type { RichStr, WithoutStyles } from "@opal/types";
|
||||
|
||||
@@ -37,6 +37,9 @@ interface DisabledProps extends WithoutStyles<
|
||||
/** Which side the tooltip appears on. @default "right" */
|
||||
tooltipSide?: TooltipSide;
|
||||
|
||||
/** Where the tooltip sits along its side. See `Tooltip`'s `align`. @default "center" */
|
||||
tooltipAlign?: TooltipAlign;
|
||||
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -69,7 +72,8 @@ function Disabled({
|
||||
disabled,
|
||||
allowClick,
|
||||
tooltip,
|
||||
tooltipSide = "right",
|
||||
tooltipSide = "end",
|
||||
tooltipAlign = "center",
|
||||
ref,
|
||||
children,
|
||||
...rest
|
||||
@@ -112,7 +116,7 @@ function Disabled({
|
||||
if (!showTooltip) return wrapper;
|
||||
|
||||
return (
|
||||
<Tooltip tooltip={tooltip} side={tooltipSide}>
|
||||
<Tooltip tooltip={tooltip} side={tooltipSide} align={tooltipAlign}>
|
||||
{wrapper}
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -190,7 +190,7 @@ function ContentLg({
|
||||
prominence="internal"
|
||||
size={config.editButtonSize}
|
||||
tooltip={strings.edit}
|
||||
tooltipSide="right"
|
||||
tooltipSide="end"
|
||||
onClick={startEditing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -317,7 +317,7 @@ function ContentMd({
|
||||
prominence="internal"
|
||||
size={config.editButtonSize}
|
||||
tooltip={strings.edit}
|
||||
tooltipSide="right"
|
||||
tooltipSide="end"
|
||||
onClick={startEditing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -239,7 +239,7 @@ function ContentXl({
|
||||
prominence="internal"
|
||||
size={config.editButtonSize}
|
||||
tooltip={strings.edit}
|
||||
tooltipSide="right"
|
||||
tooltipSide="end"
|
||||
onClick={startEditing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -33,10 +33,10 @@ Sidebar entry point. Handles three viewport sizes:
|
||||
- **Mobile** (`≤ 724 px`) — full-height fixed overlay with a tinted and
|
||||
blurred backdrop that closes on click.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| ---------- | ----------- | ------- | ------------------------------------- |
|
||||
| `foldable` | `boolean` | `false` | Enable fold/unfold on desktop |
|
||||
| `children` | `ReactNode` | — | `Header`, `Body`, `Footer`, `Section` |
|
||||
| Prop | Type | Default | Description |
|
||||
| ---------- | ----------- | ------- | ------------------------------------------------ |
|
||||
| `foldable` | `boolean` | `false` | Enable fold/unfold on desktop |
|
||||
| `children` | `ReactNode` | — | `Header`, `Body`, `Footer`, `Section`, `Divider` |
|
||||
|
||||
### `SidebarLayouts.Header`
|
||||
|
||||
@@ -75,6 +75,11 @@ optional hover-revealed action.
|
||||
| `disabled` | `boolean` | — | Dims the section header to indicate it is unavailable |
|
||||
| `children` | `ReactNode` | — | |
|
||||
|
||||
### `SidebarLayouts.Divider`
|
||||
|
||||
Horizontal rule between groups in the sidebar. Takes no props. It has
|
||||
`0.5rem` padding on all sides.
|
||||
|
||||
## Hooks
|
||||
|
||||
### `useSidebarState()`
|
||||
|
||||
@@ -11,7 +11,14 @@ import {
|
||||
useRef,
|
||||
} from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Button, ShadowDiv, Spacer, Text, Tooltip } from "@opal/components";
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
ShadowDiv,
|
||||
Spacer,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from "@opal/components";
|
||||
import { iconWrapper } from "@opal/components/buttons/icon-wrapper";
|
||||
import { Disabled, Hoverable, Interactive } from "@opal/core";
|
||||
import { SvgSidebar } from "@opal/icons";
|
||||
@@ -183,7 +190,7 @@ function SidebarHeader({
|
||||
prominence="tertiary"
|
||||
aria-label={foldLabel}
|
||||
tooltip={foldLabel}
|
||||
tooltipSide={folded ? "right" : "bottom"}
|
||||
tooltipSide={folded ? "end" : "bottom"}
|
||||
size="md"
|
||||
onClick={toggleFolded}
|
||||
/>
|
||||
@@ -199,7 +206,7 @@ function SidebarHeader({
|
||||
// sidebar can still be opened where there is no hover to reveal anything —
|
||||
// touch devices and keyboard navigation.
|
||||
const foldedLogoButton = (
|
||||
<Tooltip tooltip={foldLabel} side="right">
|
||||
<Tooltip tooltip={foldLabel} side="end">
|
||||
<Interactive.Stateless
|
||||
prominence="tertiary"
|
||||
type="button"
|
||||
@@ -370,6 +377,14 @@ function SidebarSection({
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Divider — horizontal rule between groups in the sidebar
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function SidebarDivider() {
|
||||
return <Divider paddingParallel={2} paddingPerpendicular={2} />;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Exports
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -380,5 +395,6 @@ export {
|
||||
SidebarBody as Body,
|
||||
SidebarFooter as Footer,
|
||||
SidebarSection as Section,
|
||||
SidebarDivider as Divider,
|
||||
};
|
||||
export type { SidebarRootProps };
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { FileUpload } from "@/components/admin/connectors/FileUpload";
|
||||
import * as Yup from "yup";
|
||||
import { FormBodyBuilder } from "./admin/connectors/types";
|
||||
import { InputSingleSelect } from "@opal/components";
|
||||
import { InputSingleSelect, type TooltipSide } from "@opal/components";
|
||||
import { FiInfo, FiX } from "react-icons/fi";
|
||||
import { useTranslations } from "next-intl";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
@@ -707,7 +707,7 @@ interface BooleanFormFieldProps {
|
||||
optional?: boolean;
|
||||
tooltip?: string;
|
||||
disabledTooltip?: string;
|
||||
disabledTooltipSide?: "top" | "bottom" | "left" | "right";
|
||||
disabledTooltipSide?: TooltipSide;
|
||||
onChange?: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,9 @@ export const HoverPopup = ({
|
||||
return (
|
||||
<Tooltip
|
||||
tooltip={popupContent}
|
||||
side={direction === "left-top" ? "left" : direction}
|
||||
side={
|
||||
direction === "left" || direction === "left-top" ? "start" : direction
|
||||
}
|
||||
>
|
||||
<div>{mainContent}</div>
|
||||
</Tooltip>
|
||||
|
||||
@@ -64,7 +64,7 @@ export const CustomTooltipSide: Story = {
|
||||
"Hover to see the tooltip appear on the right side instead of the default top.",
|
||||
mainUiBody: true,
|
||||
text04: true,
|
||||
side: "right",
|
||||
side: "end",
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import React, { useState, useRef, useCallback, useLayoutEffect } from "react";
|
||||
import { TextProps } from "@/refresh-components/texts/Text";
|
||||
import { Tooltip } from "@opal/components";
|
||||
import { Tooltip, type TooltipSide } from "@opal/components";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { cn } from "@opal/utils";
|
||||
|
||||
@@ -37,7 +37,7 @@ function useTruncated(children: React.ReactNode) {
|
||||
}
|
||||
|
||||
export interface TruncatedProps extends TextProps {
|
||||
side?: "top" | "right" | "bottom" | "left";
|
||||
side?: TooltipSide;
|
||||
sideOffset?: number;
|
||||
disable?: boolean;
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ export default function ChatDocumentDisplay({
|
||||
) : (
|
||||
<SourceIcon sourceType={document.source_type} iconSize={18} />
|
||||
)}
|
||||
<Truncated className="line-clamp-2" side="left">
|
||||
<Truncated className="line-clamp-2" side="start">
|
||||
{title}
|
||||
</Truncated>
|
||||
</div>
|
||||
|
||||
@@ -14,8 +14,9 @@ import { useSettings } from "@/lib/settings/hooks";
|
||||
import { SidebarLayouts, useSidebarState } from "@opal/layouts";
|
||||
import { useCustomAnalyticsEnabled } from "@/lib/hooks/useCustomAnalyticsEnabled";
|
||||
import { useUser } from "@/providers/UserProvider";
|
||||
import { Divider, InputTypeIn, SidebarTab } from "@opal/components";
|
||||
import { InputTypeIn, SidebarTab } from "@opal/components";
|
||||
import { SvgSearch, SvgX } from "@opal/icons";
|
||||
import { Disabled } from "@opal/core";
|
||||
import {
|
||||
useBillingInformation,
|
||||
useLicense,
|
||||
@@ -163,42 +164,38 @@ export default function AdminSidebar() {
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
||||
{disabledGroups.length > 0 && <SidebarLayouts.Divider />}
|
||||
{disabledGroups.length > 0 && (
|
||||
<>
|
||||
<Divider paddingPerpendicular={0} />
|
||||
{/* Empty div here just to add spacing (via the `gap` property on `SidebarLayouts.Body`) */}
|
||||
<div />
|
||||
</>
|
||||
<Disabled
|
||||
disabled
|
||||
// One tooltip over a tall block: it stays level with the pointer.
|
||||
tooltipAlign="follow"
|
||||
tooltip={markdown(
|
||||
disabled.every((item) => item.requiredTier === Tier.ENTERPRISE)
|
||||
? t("adminSidebar.enterpriseOnly.tooltip")
|
||||
: t("adminSidebar.businessOrEnterpriseOnly.tooltip")
|
||||
)}
|
||||
>
|
||||
{disabledGroups.map((group, groupIndex) => (
|
||||
<SidebarLayouts.Section
|
||||
key={`disabled-${groupIndex}`}
|
||||
title={
|
||||
group.sectionId ? sectionLabels[group.sectionId] : undefined
|
||||
}
|
||||
>
|
||||
{group.items.map(({ link, icon, nameId }) => (
|
||||
<SidebarTab key={link} icon={icon}>
|
||||
{navLabels[nameId]}
|
||||
</SidebarTab>
|
||||
))}
|
||||
</SidebarLayouts.Section>
|
||||
))}
|
||||
</Disabled>
|
||||
)}
|
||||
{disabledGroups.map((group, groupIndex) => (
|
||||
<React.Fragment key={`disabled-${groupIndex}`}>
|
||||
<SidebarLayouts.Section
|
||||
title={
|
||||
group.sectionId ? sectionLabels[group.sectionId] : undefined
|
||||
}
|
||||
disabled
|
||||
>
|
||||
{group.items.map(({ link, icon, nameId, requiredTier }) => (
|
||||
<SidebarTab
|
||||
key={link}
|
||||
disabled
|
||||
icon={icon}
|
||||
tooltip={markdown(
|
||||
requiredTier === Tier.ENTERPRISE
|
||||
? t("adminSidebar.enterpriseOnly.tooltip")
|
||||
: t("adminSidebar.businessOrEnterpriseOnly.tooltip")
|
||||
)}
|
||||
>
|
||||
{navLabels[nameId]}
|
||||
</SidebarTab>
|
||||
))}
|
||||
</SidebarLayouts.Section>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</SidebarLayouts.Body>
|
||||
|
||||
<SidebarLayouts.Footer>
|
||||
{!folded && <Divider paddingPerpendicular={2} />}
|
||||
{!folded && <SidebarLayouts.Divider />}
|
||||
<SidebarTab
|
||||
icon={SvgX}
|
||||
href={pathname?.startsWith("/admin/craft") ? "/craft/v1" : "/app"}
|
||||
|
||||
@@ -166,7 +166,7 @@ function CloudDisabled({
|
||||
: tooltipProp;
|
||||
|
||||
return (
|
||||
<Disabled disabled={isDisabled} tooltip={tooltip} tooltipSide="right">
|
||||
<Disabled disabled={isDisabled} tooltip={tooltip} tooltipSide="end">
|
||||
{children}
|
||||
</Disabled>
|
||||
);
|
||||
|
||||
@@ -298,7 +298,7 @@ export default function EditServiceAccountModal({
|
||||
rightChildren={
|
||||
<Tooltip
|
||||
tooltip={t("editModal.removeGroupButton.tooltip")}
|
||||
side="left"
|
||||
side="start"
|
||||
>
|
||||
<SvgLogOut height={16} width={16} />
|
||||
</Tooltip>
|
||||
|
||||
@@ -269,7 +269,7 @@ export default function EditUserModal({
|
||||
rightChildren={
|
||||
<Tooltip
|
||||
tooltip={t("editModal.removeGroupButton.tooltip")}
|
||||
side="left"
|
||||
side="start"
|
||||
>
|
||||
<SvgLogOut height={16} width={16} />
|
||||
</Tooltip>
|
||||
|
||||
@@ -196,7 +196,7 @@ export default function GroupsCell({
|
||||
icon={SvgEdit}
|
||||
prominence="tertiary"
|
||||
tooltip={t("groupsCell.editButton.tooltip")}
|
||||
tooltipSide="left"
|
||||
tooltipSide="start"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowModal(true);
|
||||
|
||||
@@ -43,7 +43,7 @@ function StatCell({ value, label, onFilter }: StatCellProps) {
|
||||
prominence="tertiary"
|
||||
icon={SvgFilterPlus}
|
||||
tooltip={t("summary.filterButton.tooltip")}
|
||||
tooltipSide="left"
|
||||
tooltipSide="start"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onFilter();
|
||||
|
||||
Reference in new issue
Block a user