mirror of
https://github.com/Xevion/banner.git
synced 2026-02-01 02:23:45 -06:00
refactor(web): consolidate tooltip implementations with shared components
This commit is contained in:
@@ -13,7 +13,7 @@ import {
|
||||
rmpUrl,
|
||||
} from "$lib/course";
|
||||
import { themeStore } from "$lib/stores/theme.svelte";
|
||||
import { cn, formatNumber, tooltipContentClass } from "$lib/utils";
|
||||
import { formatNumber } from "$lib/utils";
|
||||
import {
|
||||
Calendar,
|
||||
Check,
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
Star,
|
||||
Triangle,
|
||||
} from "@lucide/svelte";
|
||||
import { Tooltip } from "bits-ui";
|
||||
import RichTooltip from "./RichTooltip.svelte";
|
||||
import SimpleTooltip from "./SimpleTooltip.svelte";
|
||||
|
||||
let { course }: { course: CourseResponse } = $props();
|
||||
@@ -40,8 +40,8 @@ const clipboard = useClipboard();
|
||||
{#if course.instructors.length > 0}
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
{#each course.instructors as instructor}
|
||||
<Tooltip.Root delayDuration={200}>
|
||||
<Tooltip.Trigger>
|
||||
<RichTooltip delay={200} contentClass="px-3 py-2">
|
||||
{#snippet children()}
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 text-sm font-medium bg-card border border-border rounded-md px-2.5 py-1 text-foreground hover:border-foreground/20 hover:bg-card/80 transition-colors"
|
||||
>
|
||||
@@ -71,11 +71,8 @@ const clipboard = useClipboard();
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content
|
||||
sideOffset={6}
|
||||
class={cn(tooltipContentClass, "px-3 py-2")}
|
||||
>
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
<div class="space-y-1.5">
|
||||
<div class="font-medium">
|
||||
{instructor.displayName}
|
||||
@@ -126,8 +123,8 @@ const clipboard = useClipboard();
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/snippet}
|
||||
</RichTooltip>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
@@ -272,8 +269,8 @@ const clipboard = useClipboard();
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
</h4>
|
||||
<Tooltip.Root delayDuration={150} disableHoverableContent>
|
||||
<Tooltip.Trigger>
|
||||
<RichTooltip passthrough>
|
||||
{#snippet children()}
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 text-foreground font-mono"
|
||||
>
|
||||
@@ -288,8 +285,8 @@ const clipboard = useClipboard();
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content sideOffset={6} class={tooltipContentClass}>
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
Group <span class="font-mono font-medium"
|
||||
>{course.crossList}</span
|
||||
>
|
||||
@@ -297,8 +294,8 @@ const clipboard = useClipboard();
|
||||
— {formatNumber(course.crossListCount)} enrolled across {formatNumber(course.crossListCapacity)}
|
||||
shared seats
|
||||
{/if}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/snippet}
|
||||
</RichTooltip>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
seatsDotColor,
|
||||
} from "$lib/course";
|
||||
import { themeStore } from "$lib/stores/theme.svelte";
|
||||
import { cn, formatNumber, tooltipContentClass } from "$lib/utils";
|
||||
import { formatNumber } from "$lib/utils";
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
@@ -43,11 +43,12 @@ import {
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
} from "@tanstack/table-core";
|
||||
import { ContextMenu, DropdownMenu, Tooltip } from "bits-ui";
|
||||
import { ContextMenu, DropdownMenu } from "bits-ui";
|
||||
import { flip } from "svelte/animate";
|
||||
import { cubicOut } from "svelte/easing";
|
||||
import { fade, slide } from "svelte/transition";
|
||||
import CourseDetail from "./CourseDetail.svelte";
|
||||
import RichTooltip from "./RichTooltip.svelte";
|
||||
import SimpleTooltip from "./SimpleTooltip.svelte";
|
||||
|
||||
let {
|
||||
@@ -532,10 +533,12 @@ const table = createSvelteTable({
|
||||
{@const lowConfidence =
|
||||
ratingData.count <
|
||||
RMP_CONFIDENCE_THRESHOLD}
|
||||
<Tooltip.Root
|
||||
delayDuration={150}
|
||||
<RichTooltip
|
||||
side="bottom"
|
||||
sideOffset={6}
|
||||
contentClass="px-2.5 py-1.5"
|
||||
>
|
||||
<Tooltip.Trigger>
|
||||
{#snippet children()}
|
||||
<span
|
||||
class="ml-1 text-xs font-medium inline-flex items-center gap-0.5"
|
||||
style={ratingStyle(
|
||||
@@ -556,15 +559,8 @@ const table = createSvelteTable({
|
||||
/>
|
||||
{/if}
|
||||
</span>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content
|
||||
side="bottom"
|
||||
sideOffset={6}
|
||||
class={cn(
|
||||
tooltipContentClass,
|
||||
"px-2.5 py-1.5",
|
||||
)}
|
||||
>
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 text-xs"
|
||||
>
|
||||
@@ -592,8 +588,8 @@ const table = createSvelteTable({
|
||||
</a>
|
||||
{/if}
|
||||
</span>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/snippet}
|
||||
</RichTooltip>
|
||||
{/if}
|
||||
</td>
|
||||
{:else if colId === "time"}
|
||||
|
||||
@@ -1,105 +1,116 @@
|
||||
<script lang="ts">
|
||||
import { formatNumber } from "$lib/utils";
|
||||
import { ChevronDown, ChevronUp } from "@lucide/svelte";
|
||||
import { Select } from "bits-ui";
|
||||
import type { Action } from "svelte/action";
|
||||
import { formatNumber } from "$lib/utils";
|
||||
import { ChevronDown, ChevronUp } from "@lucide/svelte";
|
||||
import { Select } from "bits-ui";
|
||||
import type { Action } from "svelte/action";
|
||||
|
||||
const slideIn: Action<HTMLElement, number> = (node, direction) => {
|
||||
if (direction !== 0) {
|
||||
node.animate(
|
||||
[
|
||||
{ transform: `translateX(${direction * 20}px)`, opacity: 0 },
|
||||
{ transform: "translateX(0)", opacity: 1 },
|
||||
],
|
||||
{ duration: 200, easing: "ease-out" }
|
||||
const slideIn: Action<HTMLElement, number> = (node, direction) => {
|
||||
if (direction !== 0) {
|
||||
node.animate(
|
||||
[
|
||||
{
|
||||
transform: `translateX(${direction * 20}px)`,
|
||||
opacity: 0,
|
||||
},
|
||||
{ transform: "translateX(0)", opacity: 1 },
|
||||
],
|
||||
{ duration: 200, easing: "ease-out" },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let {
|
||||
totalCount,
|
||||
offset,
|
||||
limit,
|
||||
loading = false,
|
||||
onPageChange,
|
||||
}: {
|
||||
totalCount: number;
|
||||
offset: number;
|
||||
limit: number;
|
||||
loading?: boolean;
|
||||
onPageChange: (newOffset: number) => void;
|
||||
} = $props();
|
||||
|
||||
const currentPage = $derived(Math.floor(offset / limit) + 1);
|
||||
const totalPages = $derived(Math.ceil(totalCount / limit));
|
||||
const start = $derived(offset + 1);
|
||||
const end = $derived(Math.min(offset + limit, totalCount));
|
||||
|
||||
// Track direction for slide animation
|
||||
let direction = $state(0);
|
||||
|
||||
// 5 page slots: current-2, current-1, current, current+1, current+2
|
||||
const pageSlots = $derived(
|
||||
[-2, -1, 0, 1, 2].map((delta) => currentPage + delta),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let {
|
||||
totalCount,
|
||||
offset,
|
||||
limit,
|
||||
loading = false,
|
||||
onPageChange,
|
||||
}: {
|
||||
totalCount: number;
|
||||
offset: number;
|
||||
limit: number;
|
||||
loading?: boolean;
|
||||
onPageChange: (newOffset: number) => void;
|
||||
} = $props();
|
||||
function isSlotVisible(page: number): boolean {
|
||||
return page >= 1 && page <= totalPages;
|
||||
}
|
||||
|
||||
const currentPage = $derived(Math.floor(offset / limit) + 1);
|
||||
const totalPages = $derived(Math.ceil(totalCount / limit));
|
||||
const start = $derived(offset + 1);
|
||||
const end = $derived(Math.min(offset + limit, totalCount));
|
||||
function goToPage(page: number) {
|
||||
direction = page > currentPage ? 1 : -1;
|
||||
onPageChange((page - 1) * limit);
|
||||
}
|
||||
|
||||
// Track direction for slide animation
|
||||
let direction = $state(0);
|
||||
// Build items array for the Select dropdown
|
||||
const pageItems = $derived(
|
||||
Array.from({ length: totalPages }, (_, i) => ({
|
||||
value: String(i + 1),
|
||||
label: String(i + 1),
|
||||
})),
|
||||
);
|
||||
|
||||
// 5 page slots: current-2, current-1, current, current+1, current+2
|
||||
const pageSlots = $derived([-2, -1, 0, 1, 2].map((delta) => currentPage + delta));
|
||||
|
||||
function isSlotVisible(page: number): boolean {
|
||||
return page >= 1 && page <= totalPages;
|
||||
}
|
||||
|
||||
function goToPage(page: number) {
|
||||
direction = page > currentPage ? 1 : -1;
|
||||
onPageChange((page - 1) * limit);
|
||||
}
|
||||
|
||||
// Build items array for the Select dropdown
|
||||
const pageItems = $derived(
|
||||
Array.from({ length: totalPages }, (_, i) => ({
|
||||
value: String(i + 1),
|
||||
label: String(i + 1),
|
||||
}))
|
||||
);
|
||||
|
||||
const selectValue = $derived(String(currentPage));
|
||||
const selectValue = $derived(String(currentPage));
|
||||
</script>
|
||||
|
||||
{#if totalCount > 0 && totalPages > 1}
|
||||
<div class="flex items-start text-xs -mt-3 pl-2">
|
||||
<!-- Left zone: result count -->
|
||||
<div class="flex-1">
|
||||
<span class="text-muted-foreground">
|
||||
Showing {formatNumber(start)}–{formatNumber(end)} of {formatNumber(totalCount)} courses
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-start text-xs mt-2 pl-2">
|
||||
<!-- Left zone: result count -->
|
||||
<div class="flex-1">
|
||||
<span class="text-muted-foreground">
|
||||
Showing {formatNumber(start)}–{formatNumber(end)} of {formatNumber(
|
||||
totalCount,
|
||||
)} courses
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Center zone: page buttons -->
|
||||
<div class="flex items-center gap-1">
|
||||
{#key currentPage}
|
||||
{#each pageSlots as page, i (i)}
|
||||
{#if i === 2}
|
||||
<!-- Center slot: current page with dropdown trigger -->
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={selectValue}
|
||||
onValueChange={(v) => {
|
||||
if (v) goToPage(Number(v));
|
||||
}}
|
||||
items={pageItems}
|
||||
>
|
||||
<Select.Trigger
|
||||
class="inline-flex items-center justify-center gap-1 w-auto min-w-9 h-9 px-2.5
|
||||
<!-- Center zone: page buttons -->
|
||||
<div class="flex items-center gap-1">
|
||||
{#key currentPage}
|
||||
{#each pageSlots as page, i (i)}
|
||||
{#if i === 2}
|
||||
<!-- Center slot: current page with dropdown trigger -->
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={selectValue}
|
||||
onValueChange={(v) => {
|
||||
if (v) goToPage(Number(v));
|
||||
}}
|
||||
items={pageItems}
|
||||
>
|
||||
<Select.Trigger
|
||||
class="inline-flex items-center justify-center gap-1 w-auto min-w-9 h-9 px-2.5
|
||||
rounded-md text-sm font-medium tabular-nums
|
||||
border border-border bg-card text-foreground
|
||||
hover:bg-muted/50 active:bg-muted transition-colors
|
||||
cursor-pointer select-none outline-none
|
||||
focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background
|
||||
{loading ? 'animate-pulse' : ''}"
|
||||
aria-label="Page {currentPage} of {totalPages}, click to select page"
|
||||
>
|
||||
<span use:slideIn={direction}>{currentPage}</span>
|
||||
<ChevronUp class="size-3 text-muted-foreground" />
|
||||
</Select.Trigger>
|
||||
<Select.Portal>
|
||||
<Select.Content
|
||||
class="border border-border bg-card shadow-md outline-hidden z-50
|
||||
aria-label="Page {currentPage} of {totalPages}, click to select page"
|
||||
>
|
||||
<span use:slideIn={direction}
|
||||
>{currentPage}</span
|
||||
>
|
||||
<ChevronUp
|
||||
class="size-3 text-muted-foreground"
|
||||
/>
|
||||
</Select.Trigger>
|
||||
<Select.Portal>
|
||||
<Select.Content
|
||||
class="border border-border bg-card shadow-md outline-hidden z-50
|
||||
max-h-72 min-w-16 w-auto
|
||||
select-none rounded-md p-1
|
||||
data-[state=open]:animate-in data-[state=closed]:animate-out
|
||||
@@ -107,65 +118,81 @@ const selectValue = $derived(String(currentPage));
|
||||
data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95
|
||||
data-[side=top]:slide-in-from-bottom-2
|
||||
data-[side=bottom]:slide-in-from-top-2"
|
||||
side="top"
|
||||
sideOffset={6}
|
||||
>
|
||||
<Select.ScrollUpButton class="flex w-full items-center justify-center py-0.5">
|
||||
<ChevronUp class="size-3.5 text-muted-foreground" />
|
||||
</Select.ScrollUpButton>
|
||||
<Select.Viewport class="p-0.5">
|
||||
{#each pageItems as item (item.value)}
|
||||
<Select.Item
|
||||
class="rounded-sm outline-hidden flex h-8 w-full select-none items-center
|
||||
side="top"
|
||||
sideOffset={6}
|
||||
>
|
||||
<Select.ScrollUpButton
|
||||
class="flex w-full items-center justify-center py-0.5"
|
||||
>
|
||||
<ChevronUp
|
||||
class="size-3.5 text-muted-foreground"
|
||||
/>
|
||||
</Select.ScrollUpButton>
|
||||
<Select.Viewport class="p-0.5">
|
||||
{#each pageItems as item (item.value)}
|
||||
<Select.Item
|
||||
class="rounded-sm outline-hidden flex h-8 w-full select-none items-center
|
||||
justify-center px-3 text-sm tabular-nums
|
||||
data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground
|
||||
data-[selected]:font-semibold"
|
||||
value={item.value}
|
||||
label={item.label}
|
||||
>
|
||||
{item.label}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Viewport>
|
||||
<Select.ScrollDownButton class="flex w-full items-center justify-center py-0.5">
|
||||
<ChevronDown class="size-3.5 text-muted-foreground" />
|
||||
</Select.ScrollDownButton>
|
||||
</Select.Content>
|
||||
</Select.Portal>
|
||||
</Select.Root>
|
||||
{:else}
|
||||
<!-- Side slot: navigable page button or invisible placeholder -->
|
||||
<button
|
||||
class="inline-flex items-center justify-center w-9 h-9
|
||||
value={item.value}
|
||||
label={item.label}
|
||||
>
|
||||
{item.label}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Viewport>
|
||||
<Select.ScrollDownButton
|
||||
class="flex w-full items-center justify-center py-0.5"
|
||||
>
|
||||
<ChevronDown
|
||||
class="size-3.5 text-muted-foreground"
|
||||
/>
|
||||
</Select.ScrollDownButton>
|
||||
</Select.Content>
|
||||
</Select.Portal>
|
||||
</Select.Root>
|
||||
{:else}
|
||||
<!-- Side slot: navigable page button or invisible placeholder -->
|
||||
<button
|
||||
class="inline-flex items-center justify-center w-9 h-9
|
||||
rounded-md text-sm tabular-nums
|
||||
text-muted-foreground
|
||||
hover:bg-muted/50 hover:text-foreground active:bg-muted transition-colors
|
||||
cursor-pointer select-none
|
||||
focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background
|
||||
{!isSlotVisible(page) ? 'invisible' : loading ? 'opacity-40' : ''}
|
||||
{!isSlotVisible(page) || loading ? 'pointer-events-none' : ''}"
|
||||
onclick={() => goToPage(page)}
|
||||
aria-label="Go to page {page}"
|
||||
aria-hidden={!isSlotVisible(page)}
|
||||
tabindex={isSlotVisible(page) ? 0 : -1}
|
||||
disabled={!isSlotVisible(page) || loading}
|
||||
use:slideIn={direction}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
{/key}
|
||||
</div>
|
||||
{!isSlotVisible(page)
|
||||
? 'invisible'
|
||||
: loading
|
||||
? 'opacity-40'
|
||||
: ''}
|
||||
{!isSlotVisible(page) || loading
|
||||
? 'pointer-events-none'
|
||||
: ''}"
|
||||
onclick={() => goToPage(page)}
|
||||
aria-label="Go to page {page}"
|
||||
aria-hidden={!isSlotVisible(page)}
|
||||
tabindex={isSlotVisible(page) ? 0 : -1}
|
||||
disabled={!isSlotVisible(page) || loading}
|
||||
use:slideIn={direction}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
<!-- Right zone: spacer for centering -->
|
||||
<div class="flex-1"></div>
|
||||
</div>
|
||||
<!-- Right zone: spacer for centering -->
|
||||
<div class="flex-1"></div>
|
||||
</div>
|
||||
{:else if totalCount > 0}
|
||||
<!-- Single page: just show the count, no pagination controls -->
|
||||
<div class="flex items-start text-xs -mt-3 pl-2">
|
||||
<span class="text-muted-foreground">
|
||||
Showing {formatNumber(start)}–{formatNumber(end)} of {formatNumber(totalCount)} courses
|
||||
</span>
|
||||
</div>
|
||||
<!-- Single page: just show the count, no pagination controls -->
|
||||
<div class="flex items-start text-xs mt-2 pl-2">
|
||||
<span class="text-muted-foreground">
|
||||
Showing {formatNumber(start)}–{formatNumber(end)} of {formatNumber(
|
||||
totalCount,
|
||||
)} courses
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<script lang="ts">
|
||||
import { cn, tooltipContentClass } from "$lib/utils";
|
||||
import { Tooltip } from "bits-ui";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
let {
|
||||
delay,
|
||||
side = "top",
|
||||
sideOffset = 6,
|
||||
passthrough = false,
|
||||
triggerClass = "",
|
||||
contentClass = "",
|
||||
portal = true,
|
||||
avoidCollisions = true,
|
||||
collisionPadding = 8,
|
||||
children,
|
||||
content,
|
||||
}: {
|
||||
delay?: number;
|
||||
side?: "top" | "bottom" | "left" | "right";
|
||||
sideOffset?: number;
|
||||
passthrough?: boolean;
|
||||
triggerClass?: string;
|
||||
contentClass?: string;
|
||||
portal?: boolean;
|
||||
avoidCollisions?: boolean;
|
||||
collisionPadding?: number;
|
||||
children: Snippet;
|
||||
content: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Root delayDuration={delay} disableHoverableContent={passthrough}>
|
||||
<Tooltip.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<span class={triggerClass} {...props}>
|
||||
{@render children()}
|
||||
</span>
|
||||
{/snippet}
|
||||
</Tooltip.Trigger>
|
||||
{#if portal}
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
{side}
|
||||
{sideOffset}
|
||||
{avoidCollisions}
|
||||
{collisionPadding}
|
||||
class={cn(tooltipContentClass, contentClass)}
|
||||
>
|
||||
{@render content()}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
{:else}
|
||||
<Tooltip.Content
|
||||
{side}
|
||||
{sideOffset}
|
||||
{avoidCollisions}
|
||||
{collisionPadding}
|
||||
class={cn(tooltipContentClass, contentClass)}
|
||||
>
|
||||
{@render content()}
|
||||
</Tooltip.Content>
|
||||
{/if}
|
||||
</Tooltip.Root>
|
||||
@@ -11,6 +11,9 @@ let {
|
||||
triggerClass = "",
|
||||
contentClass = "",
|
||||
sideOffset = 6,
|
||||
portal = true,
|
||||
avoidCollisions = true,
|
||||
collisionPadding = 8,
|
||||
children,
|
||||
}: {
|
||||
text: string;
|
||||
@@ -20,6 +23,9 @@ let {
|
||||
triggerClass?: string;
|
||||
contentClass?: string;
|
||||
sideOffset?: number;
|
||||
portal?: boolean;
|
||||
avoidCollisions?: boolean;
|
||||
collisionPadding?: number;
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
@@ -32,11 +38,27 @@ let {
|
||||
</span>
|
||||
{/snippet}
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content
|
||||
{side}
|
||||
{sideOffset}
|
||||
class={cn("z-50 bg-card text-card-foreground text-xs border border-border rounded-md px-2.5 py-1.5 shadow-sm whitespace-pre-line max-w-max text-left", contentClass)}
|
||||
>
|
||||
{text}
|
||||
</Tooltip.Content>
|
||||
{#if portal}
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
{side}
|
||||
{sideOffset}
|
||||
{avoidCollisions}
|
||||
{collisionPadding}
|
||||
class={cn("z-50 bg-card text-card-foreground text-xs border border-border rounded-md px-2.5 py-1.5 shadow-sm whitespace-pre-line max-w-max text-left", contentClass)}
|
||||
>
|
||||
{text}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
{:else}
|
||||
<Tooltip.Content
|
||||
{side}
|
||||
{sideOffset}
|
||||
{avoidCollisions}
|
||||
{collisionPadding}
|
||||
class={cn("z-50 bg-card text-card-foreground text-xs border border-border rounded-md px-2.5 py-1.5 shadow-sm whitespace-pre-line max-w-max text-left", contentClass)}
|
||||
>
|
||||
{text}
|
||||
</Tooltip.Content>
|
||||
{/if}
|
||||
</Tooltip.Root>
|
||||
|
||||
@@ -39,7 +39,7 @@ onMount(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Provider delayDuration={150} skipDelayDuration={50}>
|
||||
<div class="relative flex min-h-screen flex-col">
|
||||
<!-- pointer-events-none so the navbar doesn't block canvas interactions;
|
||||
NavBar re-enables pointer-events on its own container. -->
|
||||
|
||||
Reference in New Issue
Block a user