More consistent dashboard (#1613)

<!--

Make sure you've read the CONTRIBUTING.md guidelines:
https://github.com/hexclave/hexclave/blob/dev/CONTRIBUTING.md

-->

## Summary

This PR tightens dashboard consistency across the project list, team
invite flow, hosted auth previews, and shared overlay surfaces. It also
includes smaller visual fixes for charts/tooltips, globe sizing, sticky
data-grid surfaces, and dashboard/hosted component light-mode parity.

**Base:** `dev@2c5b2d02c` -> **Head:**
`more-consistent-dashboard@b1eae206`
**Capture notes:** screenshots were captured locally at `1440x1000`
after signing in through GitHub -> `admin@example.com` on the mock OAuth
server. Each page/state was allowed to settle for 5 seconds immediately
before capture. Dark mode is used by default; light mode is included
only for the light-specific overlay/dialog surfaces changed in this PR.

## Visual Comparison

### Hosted Auth Preview - Dark Mode

| Before | After |
| --- | --- |
| ![Before hosted auth preview
dark](https://gist.githubusercontent.com/Developing-Gamer/7f29365532f54161bc5b4531f7a71b1b/raw/before-auth-methods-preview-dark.png)
| ![After hosted auth preview
dark](https://gist.githubusercontent.com/Developing-Gamer/7f29365532f54161bc5b4531f7a71b1b/raw/after-auth-methods-preview-dark.png)
|

### Team Invite Dialog - Light-Specific Surface

| Before | After |
| --- | --- |
| ![Before team invite dialog
light](https://gist.githubusercontent.com/Developing-Gamer/7f29365532f54161bc5b4531f7a71b1b/raw/before-projects-invite-dialog-light.png)
| ![After team invite dialog
light](https://gist.githubusercontent.com/Developing-Gamer/7f29365532f54161bc5b4531f7a71b1b/raw/after-projects-invite-dialog-light.png)
|

## What's Changed

- Reworks the team invite dialog with clearer loading/retry states, seat
information, capacity warnings, pending invites, revoke actions, and
refresh behavior.
- Adds a dashboard-local hosted auth preview used by Auth Methods and
onboarding-style flows, showing sign-in/sign-up methods with project
display names and disabled sign-up behavior.
- Standardizes overlay and popover surfaces across dashboard, hosted
components, and shared UI primitives for better light/dark consistency.
- Adjusts chart, tooltip, globe, sidebar drawer, data-grid, and
email-log presentation details to reduce clipping, bleed, and stacking
issues.

## Notes For Reviewers

- The visual comparison focuses on places where this PR has visible UI
differences: project cards/actions, the team invite modal, overlay menu
styling, and the hosted auth preview.
- Lower-level primitive background/ring changes are represented through
the menu/dialog screenshots rather than separate screenshots for every
consuming component.

## Test Plan

- Captured the before screenshots on `dev@2c5b2d02c` and the after
screenshots on `more-consistent-dashboard@b1eae206`.
- Waited 5 seconds on each page/state before capturing screenshots.
- Uploaded exactly 10 screenshots to a fresh gist and embedded them
above.
- Not run in this pass: code tests, because this pass only updates the
PR description/screenshots.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added a live hosted authentication preview for project setup and auth
methods, including sign-in/sign-up states and multiple login options.

* **Bug Fixes**
* Improved invite dialog behavior so seat limits, plan upgrades, loading
states, retries, and invitation refreshes stay in sync.
* Fixed chart tooltip and globe display behavior so overlays, sizing,
and labels render more reliably.

* **Style**
* Refreshed dialogs, menus, tooltips, charts, and sidebars with improved
light/dark theme styling and spacing.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: armaan <armaan@stack-auth.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Armaan Jain 2026-07-01 09:24:08 -07:00 committed by GitHub
parent 244fc3ab7b
commit db94ec2d94
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 689 additions and 331 deletions

View File

@ -55,6 +55,10 @@ vi.mock("@/components/design-components/select", () => ({
),
}));
vi.mock("@/components/hosted-auth-preview", () => ({
HostedAuthMethodPreview: () => <div>Hosted auth preview</div>,
}));
vi.mock("@/components/router", () => ({
useRouter: () => ({
push: vi.fn(),
@ -93,7 +97,6 @@ vi.mock("@/components/config-update", () => ({
vi.mock("@hexclave/next", () => ({
AdminOwnedProject: class {},
AuthPage: () => <div>Auth preview</div>,
}));
vi.mock("@hexclave/shared/dist/utils/oauth", () => ({

View File

@ -4,6 +4,7 @@ import { DesignCard, DesignPillToggle } from "@/components/design-components";
import { DesignAlert } from "@/components/design-components/alert";
import { DesignButton } from "@/components/design-components/button";
import { DesignSelectorDropdown } from "@/components/design-components/select";
import { HostedAuthMethodPreview } from "@/components/hosted-auth-preview";
import { useRouter } from "@/components/router";
import { StripeWordmark } from "@/components/stripe-wordmark";
import {
@ -30,7 +31,7 @@ import {
WarningCircleIcon,
WebhooksLogoIcon,
} from "@phosphor-icons/react";
import { AuthPage, type AdminOwnedProject } from "@hexclave/next";
import { type AdminOwnedProject } from "@hexclave/next";
import { type AppId } from "@hexclave/shared/dist/apps/apps-config";
import { type EnvironmentConfigOverrideOverride } from "@hexclave/shared/dist/config/schema";
import { previewTemplateSource } from "@hexclave/shared/dist/helpers/emails";
@ -249,7 +250,7 @@ export function ProjectOnboardingWizard(props: {
const authPreviewProject = useMemo(() => {
return {
id: project.id,
displayName: project.displayName,
config: {
signUpEnabled: true,
credentialEnabled: signInMethods.has("credential"),
@ -260,7 +261,7 @@ export function ProjectOnboardingWizard(props: {
.map((providerId) => ({ id: providerId, type: "shared" as const })),
},
};
}, [project.id, signInMethods]);
}, [project.displayName, signInMethods]);
const toggleSignInMethod = (method: SignInMethod, enabled: boolean) => {
setSignInMethods((previous) => {
@ -824,9 +825,7 @@ export function ProjectOnboardingWizard(props: {
<div className="flex min-h-[180px] items-center justify-center px-4 py-3 sm:min-h-[220px] md:min-h-[260px] md:px-5 md:py-4 lg:min-h-[300px]">
<div className="pointer-events-none relative flex w-full items-center justify-center" inert>
<div className="absolute inset-0 z-10 bg-transparent" />
<div className="auth-preview-host-theme flex w-full justify-center">
<AuthPage type="sign-in" mockProject={authPreviewProject} />
</div>
<HostedAuthMethodPreview project={authPreviewProject} />
</div>
</div>
</BrowserFrame>

View File

@ -754,7 +754,7 @@ export default function PageClient() {
)}
{showTooltip && analyticsCardHoveredIndex !== null && (
<div
className="absolute z-20 -top-2 -translate-x-1/2 -translate-y-full rounded-xl bg-background/95 border border-foreground/[0.1] shadow-lg px-3 py-2 min-w-[170px]"
className="absolute z-20 -top-2 -translate-x-1/2 -translate-y-full rounded-xl bg-white/95 border border-black/[0.08] shadow-lg ring-1 ring-black/[0.06] px-3 py-2 min-w-[170px] dark:bg-background/95 dark:border-white/[0.08] dark:ring-white/[0.08]"
style={{ left: `${tooltipLeftPercent}%` }}
>
<div className="text-[10px] font-medium text-muted-foreground">{hoveredPoint.date}</div>

View File

@ -4,10 +4,11 @@ import { Link } from "@/components/link";
import { ProjectCard } from "@/components/project-card";
import { useRouter } from "@/components/router";
import { SearchBar } from "@/components/search-bar";
import { DesignAlert, DesignBadge, DesignButton, DesignDialog, DesignInput } from "@/components/design-components";
import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, Skeleton, Typography, toast } from "@/components/ui";
import { getPublicEnvVar } from "@/lib/env";
import { hexclaveAppInternalsSymbol } from "@/lib/hexclave-app-internals";
import { FileCode, GearIcon } from "@phosphor-icons/react";
import { FileCode, GearIcon, UserPlusIcon } from "@phosphor-icons/react";
import { AdminOwnedProject, Team, useStackApp, useUser } from "@hexclave/next";
import { isPaidPlan } from "@hexclave/shared/dist/plans";
import { projectOnboardingStatusValues, strictEmailSchema, yupObject, type ProjectOnboardingStatus } from "@hexclave/shared/dist/schema-fields";
@ -17,7 +18,7 @@ import { runAsynchronously, runAsynchronouslyWithAlert, wait } from "@hexclave/s
import { useQueryState } from "@hexclave/shared/dist/utils/react";
import { stringCompare } from "@hexclave/shared/dist/utils/strings";
import { urlString } from "@hexclave/shared/dist/utils/urls";
import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import * as yup from "yup";
import { inviteUser, listInvitations, revokeInvitation } from "./actions";
import Footer from "./footer";
@ -507,9 +508,37 @@ const inviteFormSchema = yupObject({
email: strictEmailSchema("Please enter a valid email address").defined(),
});
type TeamAddUserDialogData = {
invitations: Awaited<ReturnType<typeof listInvitations>>,
userCount: number,
seatLimit: number,
hasPaidPlan: boolean,
};
async function loadTeamAddUserDialogData(team: Team): Promise<TeamAddUserDialogData> {
const [invitations, users, admins, products] = await Promise.all([
listInvitations(team.id),
team.listUsers(),
team.getItem("dashboard_admins"),
team.listProducts(),
]);
return {
invitations,
userCount: users.length,
seatLimit: admins.quantity,
hasPaidPlan: isPaidPlan(products),
};
}
function TeamAddUserDialog(props: { team: Team }) {
const [teamSettingsId, setTeamSettingsId] = useQueryState("team_settings");
const [dialogData, setDialogData] = useState<TeamAddUserDialogData | null>(null);
const [loadingData, setLoadingData] = useState(false);
const [loadError, setLoadError] = useState<string | null>(null);
const [email, setEmail] = useState("");
const [formError, setFormError] = useState<string | null>(null);
const open = teamSettingsId === props.team.id;
const setOpen = (isOpen: boolean) => {
@ -520,86 +549,58 @@ function TeamAddUserDialog(props: { team: Team }) {
}
};
return (
<>
<Button
variant="ghost"
size="icon"
aria-label={`Invite teammates to ${props.team.displayName}`}
title={`Invite teammates to ${props.team.displayName}`}
onClick={() => setOpen(true)}
>
<GearIcon className="h-4 w-4" />
</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-[480px]">
<DialogHeader>
<DialogTitle>Invite a new user to {props.team.displayName}</DialogTitle>
</DialogHeader>
<Suspense fallback={<TeamAddUserDialogContentSkeleton />}>
<TeamAddUserDialogContent
team={props.team}
onClose={() => setOpen(false)}
/>
</Suspense>
</DialogContent>
</Dialog>
</>
);
}
function TeamAddUserDialogContent(props: {
team: Team,
onClose: () => void,
}) {
const [invitations, setInvitations] = useState<Awaited<ReturnType<typeof listInvitations>>>();
const [invitationsError, setInvitationsError] = useState<string | null>(null);
const fetchInvitations = useCallback(async () => {
setInvitationsError(null);
const fetchDialogData = useCallback(async (isCanceled: () => boolean = () => false) => {
setLoadingData(true);
setLoadError(null);
try {
const invitations = await listInvitations(props.team.id);
setInvitations(invitations);
const data = await loadTeamAddUserDialogData(props.team);
if (!isCanceled()) {
setDialogData(data);
}
} catch (error) {
setInvitationsError("Failed to load invitations. Please try again.");
captureError("team-admin-invite-dialog-load", error);
if (!isCanceled()) {
setLoadError("Failed to load team admin seats. Please try again.");
}
} finally {
if (!isCanceled()) {
setLoadingData(false);
}
}
}, [props.team.id]);
}, [props.team]);
useEffect(() => {
if (!open) {
setFormError(null);
setDialogData(null);
setLoadError(null);
return;
}
setDialogData(null);
let canceled = false;
runAsynchronously(async () => {
try {
const invitations = await listInvitations(props.team.id);
if (!canceled) {
setInvitations(invitations);
}
} catch (error) {
if (!canceled) {
setInvitationsError("Failed to load invitations. Please try again.");
}
}
});
runAsynchronously(fetchDialogData(() => canceled));
return () => {
canceled = true;
};
}, [props.team.id]);
}, [fetchDialogData, open]);
const users = props.team.useUsers();
const admins = props.team.useItem("dashboard_admins");
const products = props.team.useProducts();
const hasPaidPlan = isPaidPlan(products);
const refreshInvitations = useCallback(async () => {
setLoadError(null);
try {
const data = await loadTeamAddUserDialogData(props.team);
setDialogData(data);
} catch (error) {
captureError("team-admin-invite-dialog-refresh-invitations", error);
setLoadError("Failed to refresh pending invitations. Please try again.");
}
}, [props.team]);
const [email, setEmail] = useState("");
const [formError, setFormError] = useState<string | null>(null);
const invitationsLoaded = invitations != null;
const activeSeats = users.length + (invitations?.length ?? 0);
const seatLimit = admins.quantity;
const atCapacity = invitationsLoaded && activeSeats >= seatLimit;
const activeSeats = dialogData == null ? null : dialogData.userCount + dialogData.invitations.length;
const atCapacity = dialogData != null && activeSeats != null && activeSeats >= dialogData.seatLimit;
const handleInvite = async () => {
if (!invitationsLoaded || atCapacity) {
if (dialogData == null || atCapacity) {
return;
}
@ -609,7 +610,7 @@ function TeamAddUserDialogContent(props: {
await inviteUser(props.team.id, values.email, window.location.origin);
toast({ variant: "success", title: "Team invitation sent" });
setEmail("");
await fetchInvitations();
await refreshInvitations();
} catch (error) {
if (error instanceof yup.ValidationError) {
setFormError(error.errors[0] ?? error.message);
@ -636,152 +637,184 @@ function TeamAddUserDialogContent(props: {
window.location.assign(checkoutUrl);
};
const footer = (
<div className="flex w-full flex-col-reverse gap-2 sm:flex-row sm:justify-end">
<DesignButton variant="secondary" size="sm" type="button" onClick={() => setOpen(false)}>
Close
</DesignButton>
{atCapacity ? (
dialogData.hasPaidPlan ? (
<DesignButton size="sm" type="button" onClick={handleAddSeat}>
Add seat ($29/mo)
</DesignButton>
) : (
<DesignButton size="sm" type="button" onClick={handleUpgrade}>
Upgrade plan
</DesignButton>
)
) : (
<DesignButton
size="sm"
type="button"
onClick={handleInvite}
disabled={dialogData == null || loadingData}
>
Invite
</DesignButton>
)}
</div>
);
return (
<>
<div className="space-y-4 py-2">
<div className="flex items-center justify-between rounded-md border border-border px-3 py-2">
<Typography type="label">Dashboard admin seats</Typography>
{invitationsLoaded ? (
<Typography variant="secondary">
{activeSeats}/{seatLimit}
</Typography>
) : (
<Skeleton className="h-4 w-12" />
)}
<DesignDialog
open={open}
onOpenChange={setOpen}
size="lg"
icon={UserPlusIcon}
title={`Invite a user to ${props.team.displayName}`}
description="Add a dashboard admin and keep pending invitations visible."
trigger={(
<DesignButton
variant="ghost"
size="icon"
className="h-8 w-8 rounded-lg text-muted-foreground hover:text-foreground"
aria-label={`Invite teammates to ${props.team.displayName}`}
title={`Invite teammates to ${props.team.displayName}`}
>
<GearIcon className="h-4 w-4" />
</DesignButton>
)}
footer={footer}
>
<div className="space-y-4">
<div className="rounded-2xl border border-foreground/[0.08] bg-foreground/[0.02] p-4">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1">
<Typography className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Dashboard admin seats
</Typography>
<Typography variant="secondary" className="text-xs">
{dialogData == null
? "Checking seats and pending invitations..."
: `${dialogData.userCount} active admin${dialogData.userCount === 1 ? "" : "s"} plus ${dialogData.invitations.length} pending invitation${dialogData.invitations.length === 1 ? "" : "s"}.`}
</Typography>
</div>
{activeSeats == null || dialogData == null ? (
<Skeleton className="h-5 w-14 rounded-full" />
) : (
<DesignBadge
label={`${activeSeats}/${dialogData.seatLimit}`}
color={atCapacity ? "orange" : "green"}
size="sm"
/>
)}
</div>
</div>
{atCapacity && (
<Typography variant="secondary" className="text-destructive">
{hasPaidPlan
? "You are at capacity. Add an extra seat for $29/month."
: "You are at capacity. Upgrade your plan to add more admins."}
</Typography>
{loadError != null && (
<DesignAlert
variant="error"
title="Could not load team data"
description={loadError}
className="p-3"
>
<DesignButton
variant="ghost"
size="sm"
className="mt-2 h-7 px-2"
type="button"
onClick={() => fetchDialogData()}
>
Retry
</DesignButton>
</DesignAlert>
)}
{atCapacity && (
<DesignAlert
variant="warning"
title="No admin seats available"
description={dialogData.hasPaidPlan
? "Add an extra seat for $29/month to invite another dashboard admin."
: "Upgrade your plan to invite more dashboard admins."}
className="p-3"
/>
)}
<div className="space-y-2">
<Input
<Typography className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
New admin
</Typography>
<DesignInput
value={email}
onChange={(event) => {
setEmail(event.target.value);
if (formError) {
if (formError != null) {
setFormError(null);
}
}}
placeholder="Email"
placeholder="admin@example.com"
type="email"
disabled={(!invitationsLoaded && !invitationsError) || atCapacity}
disabled={dialogData == null || loadingData || atCapacity}
autoFocus
/>
{formError && (
<Typography type="label" className="text-destructive">
{formError != null && (
<Typography type="label" className="text-xs text-destructive">
{formError}
</Typography>
)}
</div>
<div className="space-y-2">
<Typography type="label">Pending invitations</Typography>
{invitationsError ? (
<div className="flex items-center justify-between rounded-md border border-destructive/50 bg-destructive/5 px-3 py-2">
<Typography variant="secondary" className="text-destructive text-sm">
{invitationsError}
<div className="flex items-center justify-between gap-2">
<Typography className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Pending invitations
</Typography>
{dialogData != null && dialogData.invitations.length > 0 && (
<Typography variant="secondary" className="text-xs">
{dialogData.invitations.length} pending
</Typography>
)}
</div>
{dialogData == null && loadingData ? (
<Skeleton className="h-10 w-full rounded-xl" />
) : dialogData == null ? (
null
) : dialogData.invitations.length === 0 ? (
<div className="rounded-xl border border-dashed border-foreground/[0.12] bg-foreground/[0.02] px-3 py-3">
<Typography variant="secondary" className="text-sm">
No pending invitations
</Typography>
<Button
variant="ghost"
size="sm"
onClick={fetchInvitations}
>
Retry
</Button>
</div>
) : invitations?.length === 0 ? (
<Typography variant="secondary">None</Typography>
) : (
<div className="space-y-2 max-h-48 overflow-y-auto">
{invitations?.map((invitation) => (
<div className="max-h-44 space-y-2 overflow-y-auto pr-1">
{dialogData.invitations.map((invitation) => (
<div
key={invitation.id}
className="flex items-center justify-between rounded-md border border-border px-3 py-2"
className="flex items-center justify-between gap-3 rounded-xl border border-foreground/[0.08] bg-white/60 px-3 py-2 dark:bg-foreground/[0.03]"
>
<div className="flex flex-col">
<Typography>{invitation.recipientEmail ?? "Pending invitation"}</Typography>
</div>
<Button
<Typography className="min-w-0 truncate text-sm">
{invitation.recipientEmail ?? "Pending invitation"}
</Typography>
<DesignButton
variant="ghost"
size="sm"
type="button"
className="h-7 px-2"
onClick={async () => {
await revokeInvitation(props.team.id, invitation.id);
await fetchInvitations();
await refreshInvitations();
}}
>
Revoke
</Button>
</DesignButton>
</div>
))}
{!invitations && (
<Skeleton className="h-8 w-full" />
)}
</div>
)}
</div>
</div>
<DialogFooter className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-end">
<Button variant="outline" onClick={props.onClose}>
Close
</Button>
{atCapacity ? (
hasPaidPlan ? (
<Button onClick={handleAddSeat} variant="default">
Add seat ($29/mo)
</Button>
) : (
<Button onClick={handleUpgrade} variant="default">
Upgrade plan
</Button>
)
) : (
<Button onClick={handleInvite} disabled={!invitationsLoaded && !invitationsError}>
Invite
</Button>
)}
</DialogFooter>
</>
);
}
function TeamAddUserDialogContentSkeleton() {
return (
<>
<div className="space-y-4 py-2">
<div className="flex items-center justify-between rounded-md border border-border px-3 py-2">
<Typography type="label">Dashboard admin seats</Typography>
<div className="stack-scope text-md text-zinc-600 dark:text-zinc-400">
<Skeleton className="h-4 w-16" />
</div>
</div>
<div className="space-y-2">
<Input
disabled
placeholder="Email"
type="email"
autoFocus
/>
</div>
<div className="space-y-2">
<Typography type="label">Pending invitations</Typography>
<Skeleton className="h-8 w-full" />
</div>
</div>
<DialogFooter className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-end">
<Button variant="outline" disabled>
Close
</Button>
<Button disabled>
Invite
</Button>
</DialogFooter>
</>
</DesignDialog>
);
}

View File

@ -2,7 +2,6 @@ import { useWaitForIdle } from '@/hooks/use-wait-for-idle';
import { useDashboardUser } from '@/lib/dashboard-user';
import { useThemeWatcher } from '@/lib/theme';
import { cn } from '@/lib/utils';
import useResizeObserver from '@react-hook/resize-observer';
import { UserAvatar } from '@hexclave/next';
import type { MetricsRecentUser } from '@hexclave/shared/dist/interface/admin-metrics';
import { throwErr } from '@hexclave/shared/dist/utils/errors';
@ -22,6 +21,7 @@ import {
Group,
Mesh,
MeshLambertMaterial,
PerspectiveCamera,
SphereGeometry,
Vector3,
} from 'three';
@ -35,32 +35,24 @@ export const globeImages = {
const Globe = dynamic(() => import('react-globe.gl').then((mod) => mod.default), { ssr: false });
const countriesPromise = import('./country-data.geo.json');
function useSize(target: RefObject<HTMLDivElement | null>) {
const [size, setSize] = useState<DOMRectReadOnly>();
useLayoutEffect(() => {
setSize(target.current?.getBoundingClientRect());
}, [target]);
// Where the magic happens
useResizeObserver(target, (entry) => setSize(entry.contentRect));
return size;
}
function calculateGlobeVisualDiameter(globeRef: RefObject<GlobeMethods | undefined>): number {
if (!globeRef.current) return 0;
const current = globeRef.current;
const globeRadius = current.getGlobeRadius();
const camera = current.camera();
if (!(camera instanceof PerspectiveCamera)) {
throwErr("Expected react-globe.gl to use a PerspectiveCamera for globe sizing");
}
const renderer = current.renderer();
const centerWorld = new Vector3(0, 0, 0);
const cameraPosition = camera.position;
const distanceToCenter = centerWorld.distanceTo(cameraPosition);
const fov = (camera as any).fov * (Math.PI / 180);
const screenHeight = renderer.domElement.height;
const visualRadius = (globeRadius / distanceToCenter) * (screenHeight / (2 * Math.tan(fov / 2)));
return visualRadius * 1.065; // Return diameter
const fov = camera.fov * (Math.PI / 180);
const cssHeight = renderer.domElement.getBoundingClientRect().height;
if (cssHeight <= 0) return 0;
const visualRadius = (globeRadius / distanceToCenter) * (cssHeight / (2 * Math.tan(fov / 2)));
return visualRadius * 2 * 1.065;
}
// --- Country point-in-polygon (used by the orbiting satellites to detect
@ -408,8 +400,8 @@ function GlobeLoading(props: { devReason: string, className?: string }) {
});
return (
<div className={cn("w-full aspect-square flex items-center justify-center", props.className)}>
<div className="relative w-[70%] aspect-square">
<div className={cn("w-full h-full flex items-center justify-center", props.className)}>
<div className="relative aspect-square h-[70%] max-w-[70%]">
{/* Main globe circle with gradient */}
<div
className="absolute inset-0 rounded-full bg-gradient-to-br from-sky-500/[0.09] via-sky-400/[0.05] to-transparent animate-pulse dark:from-sky-400/[0.12] dark:via-sky-500/[0.06]"
@ -487,12 +479,6 @@ function GlobeSectionInner({ countryData, totalUsers, activeUsersByCountry, sate
[activeUsersByCountry, countryVizData],
);
// Only `globeContainerSize` is actually consumed (drives zoom / border math
// further down). The other refs/useSize calls were leftovers from an earlier
// layout and each spawned a live ResizeObserver subscription for no reason.
const globeContainerRef = useRef<HTMLDivElement>(null);
const globeContainerSize = useSize(globeContainerRef);
// Measure the parent element so the root can size itself to min(w, h) of
// the available space (container queries misfire here on initial layout).
const rootRef = useRef<HTMLDivElement>(null);
@ -502,7 +488,8 @@ function GlobeSectionInner({ countryData, totalUsers, activeUsersByCountry, sate
if (!parent) return;
const update = () => {
const r = parent.getBoundingClientRect();
setParentBox({ w: r.width, h: r.height });
const nextBox = { w: Math.floor(r.width), h: Math.floor(r.height) };
setParentBox((current) => current.w === nextBox.w && current.h === nextBox.h ? current : nextBox);
};
update();
const ro = new ResizeObserver(update);
@ -511,9 +498,6 @@ function GlobeSectionInner({ countryData, totalUsers, activeUsersByCountry, sate
}, []);
const squareSize = Math.min(parentBox.w, parentBox.h);
// Simplified sizing for the new layout - only use width
const globeSize = globeContainerSize?.width ?? 400;
// Calculate camera distance (zoom) based on canvas width
// Linear interpolation: zoom decreases as width increases (less aggressive slope)
// Lower zoom values = larger globe size
@ -523,10 +507,10 @@ function GlobeSectionInner({ countryData, totalUsers, activeUsersByCountry, sate
// - Canvas width >= 500: zoom stays at 309 so the globe keeps a constant
// visual fill ratio on widescreens instead of growing without bound and
// overflowing the canvas.
const canvasWidth = globeContainerSize?.width ?? 0;
const canvasWidth = squareSize;
const GLOBE_MIN_WIDTH = 350;
const shouldShowGlobe = canvasWidth >= GLOBE_MIN_WIDTH;
const shouldShowGlobe = squareSize > 0 && canvasWidth >= GLOBE_MIN_WIDTH;
// Calculate zoom based on width
// For widths >= 355, use linear formula clamped to a minimum distance.
@ -536,19 +520,6 @@ function GlobeSectionInner({ countryData, totalUsers, activeUsersByCountry, sate
? Math.max(MIN_CAMERA_DISTANCE, 436 - 0.35 * canvasWidth)
: 325; // For 350-355 range
// Calculate border size using exact same formula structure as cameraDistance
// Uses same scale factor (0.35) but inverted direction (increases as width increases)
// - Canvas width 350: Hide border
// - Canvas width 355: borderSize = BORDER_BASE_SIZE
// - Canvas width 500: borderSize = BORDER_BASE_SIZE + 0.35 * (500 - 355)
// Formula: borderSize = BORDER_BASE_SIZE + 0.35 * (width - 355) for width >= 355
const BORDER_BASE_SIZE = 180; // Only variable to change - base size at 355px
const borderSize = canvasWidth >= 355
? BORDER_BASE_SIZE + 0.35 * (canvasWidth - 355)
: canvasWidth >= GLOBE_MIN_WIDTH
? BORDER_BASE_SIZE
: 0;
const [selectedCountry, setSelectedCountry] = useState<{ code: string, name: string } | null>(null);
const [previousSelectedCountry, setPreviousSelectedCountry] = useState<{ code: string, name: string } | null>(null);
const lastSelectedCountry = selectedCountry ?? previousSelectedCountry;
@ -704,7 +675,7 @@ function GlobeSectionInner({ countryData, totalUsers, activeUsersByCountry, sate
const visualDiameter = calculateGlobeVisualDiameter(globeRef);
setBorderSizeFromGlobe(visualDiameter);
resumeRender();
}, [cameraDistance, shouldShowGlobe, globeSize, interactive]);
}, [cameraDistance, shouldShowGlobe, squareSize, interactive]);
const totalUsersInCountries = Object.values(countryData).reduce((acc, curr) => acc + curr, 0);
@ -1068,7 +1039,14 @@ function GlobeSectionInner({ countryData, totalUsers, activeUsersByCountry, sate
}, []);
return (
<div ref={rootRef} className='relative mx-auto overflow-hidden' style={{ width: squareSize || '100%', height: squareSize || '100%' }}>
<div
ref={rootRef}
className='relative mx-auto overflow-hidden'
style={{
width: squareSize > 0 ? squareSize : '100%',
height: squareSize > 0 ? squareSize : '100%',
}}
>
<div inert className='absolute inset-0 pointer-events-none'>
<GlobeLoading
devReason="not ready"
@ -1084,9 +1062,6 @@ function GlobeSectionInner({ countryData, totalUsers, activeUsersByCountry, sate
globeReady ? 'opacity-100' : 'opacity-0',
)}
>
{/* Hidden measurement div - always rendered to track size */}
<div ref={globeContainerRef} className='absolute inset-0 pointer-events-none' aria-hidden="true" />
{/* Globe Container - Premium 3D */}
{shouldShowGlobe && (
<div className='relative w-full h-full overflow-hidden flex items-center justify-center'>
@ -1157,10 +1132,10 @@ function GlobeSectionInner({ countryData, totalUsers, activeUsersByCountry, sate
backgroundColor='rgba(0,0,0,0)'
// globeImageUrl={globeImages[theme]}
globeMaterial={globeMaterial}
width={globeSize}
width={squareSize}
showGraticules={theme === 'dark'}
showAtmosphere={false}
height={globeSize}
height={squareSize}
onGlobeReady={() => {
setGlobeReady(true);
@ -1352,7 +1327,7 @@ function GlobeSectionInner({ countryData, totalUsers, activeUsersByCountry, sate
style={{ transform: 'translate3d(0px, 0px, 0) translate(-50%, -140%)' }}
>
<div className='flex flex-col items-center gap-1.5'>
<div className='flex items-center gap-1.5 px-2 py-1 rounded-full bg-background/95 ring-1 ring-foreground/10 shadow-lg backdrop-blur-md'>
<div className='flex items-center gap-1.5 px-2 py-1 rounded-full bg-white/95 ring-1 ring-black/[0.08] shadow-lg backdrop-blur-md dark:bg-background/95 dark:ring-white/[0.08]'>
{display?.user && (
<div className='w-7 h-7 rounded-full overflow-hidden ring-2 ring-sky-400/60 dark:ring-sky-300/50'>
<UserAvatar
@ -1390,7 +1365,7 @@ function GlobeSectionInner({ countryData, totalUsers, activeUsersByCountry, sate
<div
ref={tooltipRef}
className={cn(
"fixed top-0 left-0 z-[100] min-w-[180px] p-4 rounded-2xl shadow-xl bg-background/95 backdrop-blur-xl ring-1 ring-foreground/[0.08] pointer-events-none",
"fixed top-0 left-0 z-[100] min-w-[180px] p-4 rounded-2xl shadow-xl bg-white/95 backdrop-blur-xl ring-1 ring-black/[0.08] pointer-events-none dark:bg-background/95 dark:ring-white/[0.08]",
selectedCountry ? 'opacity-100' : 'opacity-0 transition-opacity duration-300 ease-out',
)}
>

View File

@ -826,7 +826,7 @@ export function ComposedAnalyticsChart({
<ComposedChart
id={id}
data={taggedDatapoints}
margin={{ top: 10, right: 4, left: 4, bottom: 0 }}
margin={{ top: 10, right: 10, left: -10, bottom: 0 }}
onMouseMove={(state) => {
updateHoveredIndexFromChartState(state, datapoints.length, setHoveredIndex);
setHoveredX(getActiveCoordinateX(state));
@ -976,7 +976,7 @@ export function ComposedAnalyticsChart({
yAxisId="visitors"
tickLine={false}
axisLine={false}
width={compact ? 28 : 36}
width={compact ? 35 : 50}
tick={{ fill: "hsl(var(--muted-foreground))", fontSize: compact ? 9 : 10 }}
ticks={visitorTicks}
domain={[0, visitorsMax]}
@ -2434,7 +2434,7 @@ export function VisitorsHoverChart({
>
<ComposedChart
data={chartData}
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
margin={{ top: 10, right: 10, left: -10, bottom: 0 }}
onMouseMove={(state) => updateHoveredIndexFromChartState(state, chartData.length, setHoveredIndex)}
onMouseLeave={() => setHoveredIndex(null)}
>
@ -2525,7 +2525,7 @@ export function VisitorsHoverChart({
tickMargin={4}
tick={{ fontSize: compact ? 9 : 11, fill: "hsl(var(--muted-foreground))" }}
allowDecimals={false}
width={28}
width={compact ? 35 : 50}
/>
</ComposedChart>
</ChartContainer>
@ -2650,7 +2650,7 @@ export function RevenueHoverChart({
>
<ComposedChart
data={chartData}
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
margin={{ top: 10, right: 10, left: -10, bottom: 0 }}
onMouseMove={(state) => updateHoveredIndexFromChartState(state, chartData.length, setHoveredIndex)}
onMouseLeave={() => setHoveredIndex(null)}
>
@ -2770,7 +2770,7 @@ export function RevenueHoverChart({
tick={{ fontSize: compact ? 9 : 11, fill: "hsl(var(--muted-foreground))" }}
ticks={ticksCents}
tickFormatter={(v: number) => formatUsdCompact(v)}
width={36}
width={compact ? 35 : 50}
/>
</ComposedChart>
</ChartContainer>

View File

@ -526,7 +526,7 @@ function FilterMenuContent({
<DropdownMenuPortal>
<DropdownMenuContent
align="end"
className="w-[min(30rem,calc(100vw-2rem))] overflow-hidden rounded-xl border border-foreground/[0.08] bg-background p-0 shadow-lg"
className="w-[min(30rem,calc(100vw-2rem))] overflow-hidden rounded-xl border border-black/[0.08] bg-white p-0 shadow-lg ring-1 ring-black/[0.06] dark:border-white/[0.08] dark:bg-background dark:ring-white/[0.08]"
>
<DropdownMenuLabel className="px-3 py-2 text-[10px] uppercase tracking-wider text-muted-foreground">
Filter analytics by

View File

@ -3,6 +3,7 @@
import { InlineSaveDiscard } from "@/components/inline-save-discard";
import { ActionDialog, BrandIcons, BrowserFrame, FormControl, FormField, FormItem, FormLabel, FormMessage, InlineCode, Label, SimpleTooltip, Switch, Typography } from "@/components/ui";
import { FormDialog } from "@/components/form-dialog";
import { HostedAuthMethodPreview } from "@/components/hosted-auth-preview";
import { useUpdateConfig } from "@/components/config-update";
import { useDashboardInternalUser } from "@/lib/dashboard-user";
import {
@ -31,7 +32,7 @@ import {
UserCircleIcon,
UserPlusIcon,
} from "@phosphor-icons/react";
import { AdminProject, AuthPage } from "@hexclave/next";
import { AdminProject } from "@hexclave/next";
import type { CompleteConfig } from "@hexclave/shared/dist/config/schema";
import type { RestrictedReason } from "@hexclave/shared/dist/schema-fields";
import { urlSchema, yupObject, yupString } from "@hexclave/shared/dist/schema-fields";
@ -751,12 +752,14 @@ function ProviderInlineRow({ provider }: { provider: AdminOAuthProviderConfig })
function LivePreviewBody({
config,
projectDisplayName,
passwordEnabled,
otpEnabled,
passkeyEnabled,
enabledProviders,
}: {
config: AdminProject['config'],
projectDisplayName: string,
passwordEnabled: boolean,
otpEnabled: boolean,
passkeyEnabled: boolean,
@ -768,15 +771,15 @@ function LivePreviewBody({
<div className="flex flex-col items-center justify-center min-h-[400px]">
<div className='w-full sm:max-w-xs m-auto scale-90 pointer-events-none' inert>
<div className="absolute inset-0 bg-transparent z-10"></div>
<AuthPage
type="sign-in"
mockProject={{
<HostedAuthMethodPreview
project={{
displayName: projectDisplayName,
config: {
...config,
signUpEnabled: config.signUpEnabled,
credentialEnabled: passwordEnabled,
magicLinkEnabled: otpEnabled,
passkeyEnabled: passkeyEnabled,
oauthProviders: enabledProviders,
passkeyEnabled,
oauthProviders: enabledProviders.map((provider) => ({ id: provider.id })),
},
}}
/>
@ -1145,6 +1148,7 @@ export default function PageClient() {
>
<LivePreviewBody
config={project.config}
projectDisplayName={project.displayName}
passwordEnabled={passwordEnabled}
otpEnabled={otpEnabled}
passkeyEnabled={passkeyEnabled}

View File

@ -172,7 +172,7 @@ export function KpiBlock({
</div>
<div
role="tooltip"
className="pointer-events-none absolute left-4 right-4 top-full z-20 mt-1.5 rounded-xl border border-foreground/10 bg-background/95 p-3 opacity-0 shadow-[0_12px_28px_rgba(15,23,42,0.18)] backdrop-blur-xl transition-opacity duration-150 group-hover:opacity-100 group-focus-within:opacity-100 dark:shadow-[0_12px_28px_rgba(0,0,0,0.55)]"
className="pointer-events-none absolute left-4 right-4 top-full z-20 mt-1.5 rounded-xl border border-black/[0.08] bg-white/95 p-3 opacity-0 shadow-[0_12px_28px_rgba(15,23,42,0.18)] ring-1 ring-black/[0.06] backdrop-blur-xl transition-opacity duration-150 group-hover:opacity-100 group-focus-within:opacity-100 dark:border-white/[0.08] dark:bg-background/95 dark:ring-white/[0.08] dark:shadow-[0_12px_28px_rgba(0,0,0,0.55)]"
>
<div className="grid grid-cols-2 gap-3">
<div className="flex flex-col gap-0.5">

View File

@ -193,7 +193,7 @@ function DemoChartTooltip({ active, payload }: TooltipProps<number, string>) {
? date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })
: data.date;
return (
<div className="rounded-xl bg-background/95 px-3.5 py-2.5 shadow-lg backdrop-blur-xl ring-1 ring-foreground/[0.08]">
<div className="rounded-xl bg-white/95 px-3.5 py-2.5 shadow-lg backdrop-blur-xl ring-1 ring-black/[0.08] dark:bg-background/95 dark:ring-white/[0.08]">
<div className="flex flex-col gap-2">
<span className="text-[11px] font-medium text-muted-foreground tracking-wide">{formattedDate}</span>
<div className="flex items-center gap-2.5">

View File

@ -258,16 +258,9 @@ export default function PageClient() {
contentClassName="p-3 min-w-0"
gradient="default"
glassmorphic
>
<div className="flex flex-wrap items-center justify-between gap-2 mb-2">
<div className="flex items-center gap-2">
<div className="p-1 rounded-md bg-foreground/[0.06] dark:bg-foreground/[0.04]">
<Envelope className="h-3 w-3 text-foreground/70 dark:text-muted-foreground" />
</div>
<span className="text-xs font-semibold text-foreground uppercase tracking-wider">
Email Log
</span>
</div>
title="Email Log"
icon={Envelope}
actions={(
<DesignPillToggle
options={[...VIEW_MODE_OPTIONS]}
selected={viewMode}
@ -275,7 +268,8 @@ export default function PageClient() {
size="sm"
gradient="default"
/>
</div>
)}
>
{viewMode === "list" ? <EmailSendDataTable /> : <GroupedEmailTable />}
</DesignCard>
</div>

View File

@ -239,8 +239,8 @@ export function ProductDialog({
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className={cn(
"max-w-2xl rounded-2xl",
"bg-background/95 backdrop-blur-xl",
"border border-border/50 dark:border-foreground/[0.1]",
"bg-white/95 backdrop-blur-xl dark:bg-background/95",
"border border-black/[0.08] dark:border-foreground/[0.1]",
"shadow-2xl"
)}>
<Stepper currentStep={currentStep} onStepChange={setCurrentStep} className="min-h-[400px]">

View File

@ -352,7 +352,7 @@ function Timeline({
className="absolute bottom-full mb-1.5 -translate-x-1/2 pointer-events-none z-50"
style={{ left: `${left}%` }}
>
<div className="rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground whitespace-nowrap max-w-52">
<div className="max-w-52 whitespace-nowrap rounded-md border border-black/[0.08] bg-white px-3 py-1.5 text-xs text-foreground shadow-md ring-1 ring-black/[0.06] dark:border-white/[0.08] dark:bg-primary dark:text-primary-foreground dark:ring-white/[0.08]">
<div className="truncate">{hoveredMarker.label}</div>
<div className="text-[10px] opacity-70">{formatTimelineMs(hoveredMarker.timeMs)}</div>
</div>

View File

@ -468,11 +468,13 @@ function SidebarContent({
onNavigate,
isCollapsed,
onToggleCollapse,
isDrawer = false,
}: {
projectId: string,
onNavigate?: () => void,
isCollapsed?: boolean,
onToggleCollapse?: () => void,
isDrawer?: boolean,
}) {
const hexclaveAdminApp = useAdminApp();
const pathname = usePathname();
@ -611,7 +613,11 @@ function SidebarContent({
<div className="flex-grow" />
</div>
<div className={cn("sticky bottom-0 border-t border-black/[0.06] dark:border-foreground/10 py-3 transition-all duration-200 dark:backdrop-blur-xl dark:rounded-b-2xl", isCollapsed ? "px-2" : "px-3")}>
<div className={cn(
"sticky bottom-0 border-t border-black/[0.06] dark:border-foreground/10 py-3 transition-all duration-200 dark:backdrop-blur-xl",
!isDrawer && "dark:rounded-b-2xl",
isCollapsed ? "px-2" : "px-3",
)}>
<div className="space-y-2">
{bottomItems.map((item) => (
<NavItem
@ -748,7 +754,7 @@ export default function SidebarLayout(props: { children?: React.ReactNode }) {
className="w-[248px] bg-white/90 dark:bg-foreground/5 border-black/[0.06] dark:border-foreground/5 p-0 backdrop-blur-sm shadow-md"
hasCloseButton={false}
>
<SidebarContent projectId={projectId} onNavigate={() => setSidebarOpen(false)} />
<SidebarContent projectId={projectId} onNavigate={() => setSidebarOpen(false)} isDrawer />
</SheetContent>
</Sheet>

View File

@ -222,10 +222,19 @@
}
.analytics-card-tooltip-escape .recharts-tooltip-wrapper {
z-index: 9999 !important;
z-index: 2147483647 !important;
overflow: visible !important;
}
.analytics-card-tooltip-escape:has(.recharts-tooltip-wrapper[style*="visibility: visible"]) {
z-index: 2147483647 !important;
}
main:has(.analytics-card-tooltip-escape .recharts-tooltip-wrapper[style*="visibility: visible"]) {
position: relative;
z-index: 60;
}
.analytics-card-tooltip-escape .recharts-tooltip-wrapper > * {
overflow: visible !important;
}

View File

@ -0,0 +1,337 @@
"use client";
import { BrandIcons, Button, Input, Label, Separator, Tabs, TabsContent, TabsList, TabsTrigger, Typography, cn } from "@/components/ui";
import { KeyIcon } from "@phosphor-icons/react";
import type { ReactElement, ReactNode } from "react";
type HostedPreviewOAuthProvider = {
id: string,
};
type HostedAuthPreviewProject = {
displayName?: string,
config: {
signUpEnabled: boolean,
credentialEnabled: boolean,
passkeyEnabled: boolean,
magicLinkEnabled: boolean,
oauthProviders: HostedPreviewOAuthProvider[],
},
};
type HostedAuthType = "sign-in" | "sign-up";
const authTabsListClassName = "mb-4 h-10 w-full rounded-lg border border-black/[0.08] bg-zinc-100/70 p-1 dark:border-white/[0.10] dark:bg-zinc-900/45";
const authTabsTriggerClassName = "h-8 flex-1 rounded-md py-0 text-sm font-medium text-muted-foreground transition-colors duration-300 hover:text-foreground/90 data-[state=active]:font-semibold data-[state=active]:text-foreground";
const authFooterClassName = "mt-6 border-t border-black/[0.06] pt-5 text-center text-sm dark:border-white/[0.10]";
const authFooterLinkClassName = "font-medium text-foreground/90 underline-offset-4 transition-colors hover:text-foreground hover:underline";
const providerButtonClassNames = new Map<string, string>([
["google", "bg-white text-black hover:bg-zinc-50 border border-border shadow-sm"],
["github", "bg-[#24292e] text-white hover:bg-[#1f2327] border border-[#1b1f23] shadow-sm"],
["facebook", "bg-[#1877F2] text-white hover:bg-[#166fe5] border border-[#1464d3] shadow-sm"],
["microsoft", "bg-[#2f2f2f] text-white hover:bg-[#252525] border border-[#202020] shadow-sm"],
["spotify", "bg-[#1ED760] text-black hover:bg-[#1db954] border border-[#1aa34a] shadow-sm"],
["discord", "bg-[#5865F2] text-white hover:bg-[#4752c4] border border-[#3c45b0] shadow-sm"],
["apple", "bg-black text-white hover:bg-zinc-900 dark:bg-white dark:text-black dark:hover:bg-zinc-100 border border-zinc-900 dark:border-zinc-200 shadow-sm"],
["x", "bg-black text-white hover:bg-zinc-900 dark:bg-white dark:text-black dark:hover:bg-zinc-100 border border-zinc-900 dark:border-zinc-200 shadow-sm"],
["gitlab", "bg-[#FC6D26] text-white hover:bg-[#e24329] border border-[#d13b1f] shadow-sm"],
["bitbucket", "bg-[#0052CC] text-white hover:bg-[#0047b3] border border-[#003d99] shadow-sm"],
["linkedin", "bg-[#0077B5] text-white hover:bg-[#006699] border border-[#005580] shadow-sm"],
["twitch", "bg-[#9146FF] text-white hover:bg-[#772ce8] border border-[#641bdf] shadow-sm"],
]);
function getProviderButtonClassName(provider: string) {
return providerButtonClassNames.get(provider) ?? "bg-primary text-primary-foreground hover:bg-primary/90 border border-transparent shadow-sm";
}
function getProviderStyle(provider: string): {
name: string,
icon: ReactElement | null,
iconClassName?: string,
} {
const iconSize = 20;
switch (provider) {
case "google": {
return { name: "Google", icon: <BrandIcons.Google iconSize={iconSize} /> };
}
case "github": {
return { name: "GitHub", icon: <BrandIcons.GitHub iconSize={iconSize} />, iconClassName: "invert-0" };
}
case "facebook": {
return { name: "Facebook", icon: <BrandIcons.Facebook iconSize={iconSize} />, iconClassName: "invert-0" };
}
case "microsoft": {
return { name: "Microsoft", icon: <BrandIcons.Microsoft iconSize={iconSize} /> };
}
case "spotify": {
return { name: "Spotify", icon: <BrandIcons.Spotify iconSize={iconSize} />, iconClassName: "invert dark:invert" };
}
case "discord": {
return { name: "Discord", icon: <BrandIcons.Discord iconSize={iconSize} />, iconClassName: "invert-0" };
}
case "gitlab": {
return { name: "GitLab", icon: <BrandIcons.Gitlab iconSize={iconSize} /> };
}
case "apple": {
return { name: "Apple", icon: <BrandIcons.Apple iconSize={iconSize} />, iconClassName: "invert-0 dark:invert" };
}
case "bitbucket": {
return { name: "Bitbucket", icon: <BrandIcons.Bitbucket iconSize={iconSize} /> };
}
case "linkedin": {
return { name: "LinkedIn", icon: <BrandIcons.LinkedIn iconSize={iconSize} />, iconClassName: "invert-0" };
}
case "x": {
return { name: "X", icon: <BrandIcons.X iconSize={iconSize} />, iconClassName: "invert-0 dark:invert" };
}
case "twitch": {
return { name: "Twitch", icon: <BrandIcons.Twitch iconSize={iconSize} />, iconClassName: "invert-0" };
}
default: {
return { name: provider, icon: null };
}
}
}
function PreviewFrame(props: {
children: ReactNode,
className?: string,
}) {
return (
<div className={cn("auth-preview-host-theme stack-scope flex w-full justify-center", props.className)}>
<div className="stack-scope relative z-10 flex w-full max-w-[400px] flex-col items-stretch text-foreground">
{props.children}
</div>
</div>
);
}
function HostedAuthHeading(props: {
title: string,
children?: ReactNode,
}) {
return (
<div className="mb-6 text-center">
<Typography type="h2" className="mb-1 text-xl font-semibold tracking-tight">{props.title}</Typography>
{props.children != null && (
<Typography className="text-sm text-muted-foreground">{props.children}</Typography>
)}
</div>
);
}
function SeparatorWithText(props: {
text: string,
}) {
return (
<div className="stack-scope my-6 flex items-center justify-center">
<div className="flex-1">
<Separator />
</div>
<div className="mx-2 text-sm text-muted-foreground">{props.text}</div>
<div className="flex-1">
<Separator />
</div>
</div>
);
}
function OAuthPreviewButton(props: {
provider: string,
type: HostedAuthType,
}) {
const style = getProviderStyle(props.provider);
return (
<Button
variant="plain"
tabIndex={-1}
className={cn("stack-scope relative h-10 w-full overflow-visible rounded-xl font-medium transition-all duration-150", getProviderButtonClassName(props.provider))}
>
<div className="flex w-full items-center gap-3">
<span className={style.iconClassName}>{style.icon}</span>
<span className="flex-1 text-sm">
{props.type === "sign-up" ? `Sign up with ${style.name}` : `Sign in with ${style.name}`}
</span>
</div>
</Button>
);
}
function OAuthPreviewButtonGroup(props: {
providers: HostedPreviewOAuthProvider[],
type: HostedAuthType,
}) {
return (
<div className="stack-scope flex flex-col items-stretch gap-3">
{props.providers.map((provider) => (
<OAuthPreviewButton key={provider.id} provider={provider.id} type={props.type} />
))}
</div>
);
}
function PasskeyPreviewButton(props: {
type: HostedAuthType,
}) {
return (
<Button
variant="plain"
tabIndex={-1}
className="stack-scope h-10 rounded-xl border border-transparent bg-primary font-medium text-primary-foreground shadow-sm transition-all duration-150 hover:bg-primary/90"
>
<div className="flex w-full items-center gap-4">
<KeyIcon className="size-5" />
<span className="flex-1">
{props.type === "sign-up" ? "Sign up with Passkey" : "Sign in with Passkey"}
</span>
</div>
</Button>
);
}
function HostedMagicLinkPreviewForm() {
return (
<form className="stack-scope flex flex-col items-stretch" noValidate>
<Label htmlFor="hosted-preview-email" className="mb-1.5 text-xs font-semibold uppercase tracking-wider text-muted-foreground">Email</Label>
<Input
id="hosted-preview-email"
type="email"
autoComplete="email"
className="h-10 rounded-xl border-border bg-background"
tabIndex={-1}
/>
<Button type="button" className="mt-6 h-10 rounded-xl font-semibold shadow-sm hover:shadow" tabIndex={-1}>
Send email
</Button>
</form>
);
}
function HostedCredentialPreviewForm(props: {
type: HostedAuthType,
}) {
return (
<form className="stack-scope flex flex-col items-stretch" noValidate>
<Label htmlFor="hosted-preview-email-password-email" className="mb-1.5 text-xs font-semibold uppercase tracking-wider text-muted-foreground">Email</Label>
<Input
id="hosted-preview-email-password-email"
type="email"
autoComplete="email"
className="h-10 rounded-xl border-border bg-background"
tabIndex={-1}
/>
<div className="mb-1.5 mt-4 flex items-center justify-between">
<Label htmlFor="hosted-preview-email-password-password" className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Password</Label>
{props.type === "sign-in" && (
<a href="#" className="text-xs text-muted-foreground hover:text-foreground" tabIndex={-1}>
Forgot password?
</a>
)}
</div>
<Input
id="hosted-preview-email-password-password"
type="password"
autoComplete={props.type === "sign-in" ? "current-password" : "new-password"}
className="h-10 rounded-xl border-border bg-background"
tabIndex={-1}
/>
<Button type="button" className="mt-6 h-10 rounded-xl font-semibold shadow-sm hover:shadow" tabIndex={-1}>
{props.type === "sign-in" ? "Sign In" : "Sign Up"}
</Button>
</form>
);
}
export function HostedAuthMethodPreview(props: {
project: HostedAuthPreviewProject,
type?: HostedAuthType,
firstTab?: "magic-link" | "password",
className?: string,
}) {
const type = props.type ?? "sign-in";
if (type === "sign-up" && !props.project.config.signUpEnabled) {
return (
<PreviewFrame className={props.className}>
<HostedAuthHeading title="Sign up disabled">
New account creation is disabled for this project.
</HostedAuthHeading>
</PreviewFrame>
);
}
const hasOAuthProviders = props.project.config.oauthProviders.length > 0;
const hasPasskey = props.project.config.passkeyEnabled === true && type === "sign-in";
const hasEmailMethods = props.project.config.credentialEnabled || props.project.config.magicLinkEnabled;
const enableSeparator = hasEmailMethods && (hasOAuthProviders || hasPasskey);
return (
<PreviewFrame className={props.className}>
<HostedAuthHeading title={type === "sign-in" ? "Sign in" : "Create account"}>
{type === "sign-in" ? (
<>
to continue to <span className="font-medium text-foreground">{props.project.displayName}</span>
</>
) : (
<>
to get started with <span className="font-medium text-foreground">{props.project.displayName}</span>
</>
)}
</HostedAuthHeading>
{(hasOAuthProviders || hasPasskey) && (
<div className="mb-2 flex flex-col items-stretch gap-3">
{hasOAuthProviders && <OAuthPreviewButtonGroup providers={props.project.config.oauthProviders} type={type} />}
{hasPasskey && <PasskeyPreviewButton type={type} />}
</div>
)}
{enableSeparator && <SeparatorWithText text="Or continue with" />}
{props.project.config.credentialEnabled && props.project.config.magicLinkEnabled ? (
<Tabs defaultValue={props.firstTab || "magic-link"} className="w-full">
<TabsList className={cn(authTabsListClassName, {
"flex-row-reverse": props.firstTab === "password",
})}>
<TabsTrigger value="magic-link" className={authTabsTriggerClassName} tabIndex={-1}>Email</TabsTrigger>
<TabsTrigger value="password" className={authTabsTriggerClassName} tabIndex={-1}>Email & Password</TabsTrigger>
</TabsList>
<TabsContent value="magic-link" className="focus-visible:outline-none focus-visible:ring-0">
<HostedMagicLinkPreviewForm />
</TabsContent>
<TabsContent value="password" className="focus-visible:outline-none focus-visible:ring-0">
<HostedCredentialPreviewForm type={type} />
</TabsContent>
</Tabs>
) : props.project.config.credentialEnabled ? (
<HostedCredentialPreviewForm type={type} />
) : props.project.config.magicLinkEnabled ? (
<HostedMagicLinkPreviewForm />
) : !(hasOAuthProviders || hasPasskey) ? (
<p className="py-4 text-center text-sm text-destructive">No authentication method enabled.</p>
) : null}
{(type === "sign-up" || props.project.config.signUpEnabled) && (
<div className={authFooterClassName}>
{type === "sign-in" ? (
<p className="text-muted-foreground">
Don&apos;t have an account?{" "}
<a href="#" className={authFooterLinkClassName} tabIndex={-1}>
Sign up
</a>
</p>
) : (
<p className="text-muted-foreground">
Already have an account?{" "}
<a href="#" className={authFooterLinkClassName} tabIndex={-1}>
Sign in
</a>
</p>
)}
</div>
)}
</PreviewFrame>
);
}

View File

@ -1,6 +1,6 @@
'use client';
import { Area, AreaChart, ResponsiveContainer, Tooltip, XAxis } from 'recharts';
import { Area, AreaChart, ResponsiveContainer, Tooltip, XAxis, type TooltipProps } from 'recharts';
import { useId } from 'react';
type DataPoint = { date: string, activity: number };
@ -9,7 +9,26 @@ const CHART_HEIGHT = 56;
const EMPTY_BASELINE_COUNT = 30;
function formatDay(iso: string): string {
return new Date(iso).toLocaleDateString(undefined, { weekday: 'short', day: 'numeric' });
// Append T00:00:00 to force local-time interpretation for bare YYYY-MM-DD strings,
// which are otherwise parsed as UTC midnight and can shift to the previous day in negative-UTC timezones
const date = iso.includes('T') ? new Date(iso) : new Date(`${iso}T00:00:00`);
return date.toLocaleDateString(undefined, { weekday: 'short', day: 'numeric' });
}
function ProjectUsersMetricTooltip({ active, label, payload }: TooltipProps<number, string>) {
if (!active || !payload?.length) {
return null;
}
const value = payload[0]?.value;
const formattedValue = typeof value === 'number' ? value.toLocaleString() : value;
return (
<div className="rounded-md border border-black/[0.08] bg-white px-3 py-1.5 text-xs text-foreground shadow-md ring-1 ring-black/[0.06] dark:border-white/[0.08] dark:bg-background dark:ring-white/[0.08]">
<div className="text-[10px] text-muted-foreground">{formatDay(label)}</div>
<div>new signups: {formattedValue}</div>
</div>
);
}
function EmptyBaseline({ count }: { count: number }) {
@ -111,18 +130,7 @@ export function ProjectUsersMetric(props: {
</defs>
<Tooltip
cursor={{ stroke: 'currentColor', strokeOpacity: 0.2, strokeDasharray: '2 3' }}
contentStyle={{
background: 'hsl(var(--background))',
border: '1px solid hsl(var(--border))',
borderRadius: 6,
fontSize: 11,
padding: '4px 8px',
boxShadow: '0 6px 20px -10px rgb(0 0 0 / 0.3)',
}}
labelStyle={{ color: 'hsl(var(--muted-foreground))', marginBottom: 1, fontSize: 10 }}
itemStyle={{ color: 'hsl(var(--foreground))', padding: 0 }}
labelFormatter={(label: string) => formatDay(label)}
formatter={(value: number) => [value.toLocaleString(), 'new signups']}
content={<ProjectUsersMetricTooltip />}
/>
<Area
type="monotone"

View File

@ -188,7 +188,7 @@ const ChartTooltipContent = React.forwardRef<
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-black/[0.08] bg-white px-2.5 py-1.5 text-xs shadow-xl ring-1 ring-black/[0.06] dark:border-white/[0.08] dark:bg-background dark:ring-white/[0.08]",
className
)}
>
@ -370,4 +370,3 @@ export {
ChartStyle, ChartTooltip,
ChartTooltipContent
};

View File

@ -16,7 +16,7 @@ const Command: React.FC<React.ComponentPropsWithoutRef<typeof CommandPrimitive>>
<CommandPrimitive
ref={ref}
className={cn(
"stack-scope flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
"stack-scope flex h-full w-full flex-col overflow-hidden rounded-md bg-white text-popover-foreground dark:bg-popover",
className
)}
{...props}
@ -156,4 +156,3 @@ export {
CommandSeparator,
CommandShortcut,
};

View File

@ -86,7 +86,7 @@ const DropdownMenuSubContent = forwardRefIfNeeded<
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"stack-scope z-50 min-w-[8rem] overflow-hidden rounded-xl border border-border/50 bg-popover p-1.5 text-popover-foreground shadow-lg 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",
"stack-scope z-50 min-w-[8rem] overflow-hidden rounded-xl border border-black/[0.08] bg-white p-1.5 text-popover-foreground shadow-lg ring-1 ring-black/[0.06] dark:border-white/[0.08] dark:bg-background/95 dark:ring-white/[0.08] 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",
className
)}
{...props}
@ -270,4 +270,3 @@ export {
DropdownMenuSubContent,
DropdownMenuSubTrigger, DropdownMenuTrigger
};

View File

@ -22,7 +22,7 @@ const PopoverContent = forwardRefIfNeeded<
align={align}
sideOffset={sideOffset}
className={cn(
"stack-scope z-50 pointer-events-auto w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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",
"stack-scope z-50 pointer-events-auto w-72 rounded-md border border-black/[0.08] bg-white p-4 text-popover-foreground shadow-md ring-1 ring-black/[0.06] outline-none dark:border-white/[0.08] dark:bg-popover dark:ring-white/[0.08] 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",
className
)}
{...props}
@ -32,4 +32,3 @@ const PopoverContent = forwardRefIfNeeded<
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };

View File

@ -32,7 +32,7 @@ const SheetOverlay = forwardRefIfNeeded<
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"stack-scope fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-100 data-[state=open]:duration-100",
"stack-scope fixed z-50 gap-4 bg-white p-6 shadow-lg transition ease-in-out dark:bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-100 data-[state=open]:duration-100",
{
variants: {
side: {
@ -137,4 +137,3 @@ export {
SheetTitle,
SheetDescription,
};

View File

@ -29,7 +29,7 @@ const TooltipContent = forwardRefIfNeeded<
ref={ref}
sideOffset={sideOffset}
className={cn(
"stack-scope z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground 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",
"stack-scope z-50 overflow-hidden rounded-md border border-black/[0.08] bg-white px-3 py-1.5 text-xs text-foreground shadow-md ring-1 ring-black/[0.06] dark:border-white/[0.08] dark:bg-primary dark:text-primary-foreground dark:ring-white/[0.08] 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}
@ -38,4 +38,3 @@ const TooltipContent = forwardRefIfNeeded<
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View File

@ -79,7 +79,7 @@ function HostedUserButtonInner() {
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="stack-scope w-64 rounded-xl border border-black/[0.08] bg-popover/95 p-1.5 shadow-lg backdrop-blur-md dark:border-white/[0.08]"
className="stack-scope w-64 rounded-xl border border-black/[0.08] bg-white/95 p-1.5 shadow-lg ring-1 ring-black/[0.06] backdrop-blur-md dark:border-white/[0.08] dark:bg-popover/95 dark:ring-white/[0.08]"
>
<DropdownMenuLabel className="px-3 py-2.5">
<div className="flex items-center gap-2.5">

View File

@ -12,7 +12,7 @@ const Command: React.FC<React.ComponentPropsWithoutRef<typeof CommandPrimitive>>
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-xl bg-popover text-popover-foreground",
"flex h-full w-full flex-col overflow-hidden rounded-xl bg-white text-popover-foreground dark:bg-popover",
className
)}
{...props}

View File

@ -80,7 +80,7 @@ const DropdownMenuSubContent = forwardRefIfNeeded<
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-xl border border-black/[0.08] dark:border-white/[0.10] bg-popover/95 p-1.5 text-popover-foreground shadow-md 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",
"z-50 min-w-[8rem] overflow-hidden rounded-xl border border-black/[0.08] bg-white/95 p-1.5 text-popover-foreground shadow-md ring-1 ring-black/[0.06] dark:border-white/[0.10] dark:bg-popover/95 dark:ring-white/[0.08] 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",
className
)}
{...props}
@ -98,7 +98,7 @@ const DropdownMenuContent = forwardRefIfNeeded<
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-xl border border-black/[0.08] dark:border-white/[0.10] bg-popover/95 p-1.5 text-popover-foreground shadow-md",
"z-50 min-w-[8rem] overflow-hidden rounded-xl border border-black/[0.08] bg-white/95 p-1.5 text-popover-foreground shadow-md ring-1 ring-black/[0.06] dark:border-white/[0.10] dark:bg-popover/95 dark:ring-white/[0.08]",
"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",
className
)}

View File

@ -20,7 +20,7 @@ const PopoverContent = forwardRefIfNeeded<
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 pointer-events-auto w-72 rounded-xl border border-black/[0.08] dark:border-white/[0.10] bg-popover/95 p-4 text-popover-foreground shadow-md outline-none 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",
"z-50 pointer-events-auto w-72 rounded-xl border border-black/[0.08] bg-white/95 p-4 text-popover-foreground shadow-md ring-1 ring-black/[0.06] outline-none dark:border-white/[0.10] dark:bg-popover/95 dark:ring-white/[0.08] 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",
className
)}
{...props}

View File

@ -79,7 +79,7 @@ const SelectContent = forwardRefIfNeeded<
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-xl border border-black/[0.08] dark:border-white/[0.10] bg-popover/95 text-popover-foreground shadow-md 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",
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-xl border border-black/[0.08] bg-white/95 text-popover-foreground shadow-md ring-1 ring-black/[0.06] dark:border-white/[0.10] dark:bg-popover/95 dark:ring-white/[0.08] 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",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className

View File

@ -29,7 +29,7 @@ const TooltipContent = forwardRefIfNeeded<
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-lg bg-primary px-3 py-1.5 text-xs text-primary-foreground 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",
"z-50 overflow-hidden rounded-lg border border-black/[0.08] bg-white px-3 py-1.5 text-xs text-foreground shadow-md ring-1 ring-black/[0.06] dark:border-white/[0.08] dark:bg-primary dark:text-primary-foreground dark:ring-white/[0.08] 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}

View File

@ -1329,7 +1329,7 @@ export function AnalyticsChart({
const days = hi - lo + 1;
return (
<div
className="pointer-events-none absolute -top-1 z-30 -translate-x-1/2 -translate-y-full rounded-lg border border-blue-500/30 bg-background/95 px-3 py-1.5 text-[11px] shadow-lg backdrop-blur-xl dark:border-blue-400/30"
className="pointer-events-none absolute -top-1 z-30 -translate-x-1/2 -translate-y-full rounded-lg border border-blue-500/30 bg-white/95 px-3 py-1.5 text-[11px] shadow-lg backdrop-blur-xl dark:border-blue-400/30 dark:bg-background/95"
style={{ left: indexToCss((lo + hi) / 2) }}
>
<div className="flex items-center gap-2">
@ -1380,7 +1380,7 @@ export function AnalyticsChart({
const draft = annotationDraft;
return (
<div className="absolute top-2 z-30" style={anchorStyle}>
<div className="flex items-center gap-1 rounded-full border border-foreground/10 bg-background/95 py-1 pl-2 pr-1 shadow-[0_10px_28px_rgba(15,23,42,0.18)] backdrop-blur-xl dark:shadow-[0_10px_28px_rgba(0,0,0,0.55)]">
<div className="flex items-center gap-1 rounded-full border border-black/[0.08] bg-white/95 py-1 pl-2 pr-1 shadow-[0_10px_28px_rgba(15,23,42,0.18)] ring-1 ring-black/[0.06] backdrop-blur-xl dark:border-white/[0.08] dark:bg-background/95 dark:ring-white/[0.08] dark:shadow-[0_10px_28px_rgba(0,0,0,0.55)]">
{draft == null ? (
<>
<span className="flex items-center gap-1.5 whitespace-nowrap px-1 font-mono text-[10px] tabular-nums text-muted-foreground">
@ -1582,4 +1582,3 @@ export function AnalyticsChart({
</div>
);
}

View File

@ -108,7 +108,7 @@ export function DefaultAnalyticsChartTooltip({ ctx }: DefaultAnalyticsChartToolt
const { point, isPinned, primary, compare, delta, formatValue: fv, formatDate: fd, strings } = ctx;
const anySegmented = (primary?.segmented ?? false) || (compare?.segmented ?? false);
return (
<div className="rounded-xl border border-foreground/10 bg-background/95 px-3 py-2.5 shadow-[0_10px_28px_rgba(15,23,42,0.18)] backdrop-blur-xl dark:shadow-[0_10px_28px_rgba(0,0,0,0.55)] min-w-[180px]">
<div className="rounded-xl border border-black/[0.08] bg-white/95 px-3 py-2.5 shadow-[0_10px_28px_rgba(15,23,42,0.18)] ring-1 ring-black/[0.06] backdrop-blur-xl dark:border-white/[0.08] dark:bg-background/95 dark:ring-white/[0.08] dark:shadow-[0_10px_28px_rgba(0,0,0,0.55)] min-w-[180px]">
<div className="flex items-center justify-between gap-3">
<span className="font-mono text-[10px] uppercase tracking-wider text-muted-foreground">
{fd(point.ts)}

View File

@ -85,7 +85,7 @@ export const DesignChartTooltipContent = React.forwardRef<
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-xl bg-background/95 px-3.5 py-2.5 text-xs shadow-lg backdrop-blur-xl ring-1 ring-foreground/[0.08]",
"grid min-w-[8rem] items-start gap-1.5 rounded-xl bg-white/95 px-3.5 py-2.5 text-xs shadow-lg ring-1 ring-black/[0.08] backdrop-blur-xl dark:bg-background/95 dark:ring-white/[0.08]",
className
)}
style={{ zIndex: 9999 }}

View File

@ -60,7 +60,7 @@ function PopoverPanel({
ref={popoverRef}
className={cn(
"absolute top-full left-0 mt-1 z-50",
"bg-popover text-popover-foreground rounded-xl shadow-lg",
"bg-white text-popover-foreground rounded-xl shadow-lg dark:bg-popover",
"ring-1 ring-black/[0.08] dark:ring-white/[0.1]",
"backdrop-blur-xl",
className,

View File

@ -1032,7 +1032,7 @@ export function DataGrid<TRow>(props: DataGridProps<TRow>) {
>
<div
ref={stickyChromeRef}
className="sticky z-30 w-full min-w-0 shrink-0 overflow-visible rounded-t-[calc(var(--radius)*2)] bg-white/90 dark:bg-background/60 backdrop-blur-xl"
className="sticky z-30 w-full min-w-0 shrink-0 overflow-visible rounded-t-[calc(var(--radius)*2)] bg-white/90 dark:bg-background backdrop-blur-xl"
style={{ top: stickyTop ?? (maxHeight != null ? 0 : "var(--data-grid-sticky-top, 0px)") }}
>
{toolbar !== false && (
@ -1219,7 +1219,7 @@ export function DataGrid<TRow>(props: DataGridProps<TRow>) {
</div>
{footer !== false && (
<div className="sticky bottom-0 z-30 shrink-0 overflow-hidden rounded-b-[calc(var(--radius)*2)] bg-white/90 dark:bg-background/60 backdrop-blur-xl">
<div className="sticky bottom-0 z-30 shrink-0 overflow-hidden rounded-b-[calc(var(--radius)*2)] bg-white/90 dark:bg-background backdrop-blur-xl">
{footer ? footer(footerCtx) : <DefaultFooter ctx={footerCtx} pagination={paginationMode} onChange={onChange} />}
{footerExtra && (typeof footerExtra === "function" ? footerExtra(footerCtx) : footerExtra)}
</div>

View File

@ -16,7 +16,7 @@ const Command: React.FC<React.ComponentPropsWithoutRef<typeof CommandPrimitive>>
<CommandPrimitive
ref={ref}
className={cn(
"stack-scope flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
"stack-scope flex h-full w-full flex-col overflow-hidden rounded-md bg-white text-popover-foreground dark:bg-popover",
className
)}
{...props}
@ -156,4 +156,3 @@ export {
CommandSeparator,
CommandShortcut,
};

View File

@ -51,7 +51,7 @@ const ContextMenuSubContent = forwardRefIfNeeded<
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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",
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-black/[0.08] bg-white p-1 text-popover-foreground shadow-lg ring-1 ring-black/[0.06] dark:border-white/[0.08] dark:bg-popover dark:ring-white/[0.08] 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",
className
)}
{...props}
@ -67,7 +67,7 @@ const ContextMenuContent = forwardRefIfNeeded<
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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",
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-black/[0.08] bg-white p-1 text-popover-foreground shadow-md ring-1 ring-black/[0.06] dark:border-white/[0.08] dark:bg-popover dark:ring-white/[0.08] 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",
className
)}
{...props}

View File

@ -42,7 +42,7 @@ const DialogContent = forwardRefIfNeeded<
<DialogPrimitive.Content
ref={ref}
className={cn(
"stack-scope fixed left-[50%] top-[50%] max-h-screen z-50 flex flex-col w-full max-w-lg translate-x-[-50%] translate-y-[-50%] border bg-background p-6 shadow-lg duration-100 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
"stack-scope fixed left-[50%] top-[50%] max-h-screen z-50 flex flex-col w-full max-w-lg translate-x-[-50%] translate-y-[-50%] border bg-white p-6 shadow-lg duration-100 dark:bg-background 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
@ -132,4 +132,3 @@ export {
DialogTitle,
DialogTrigger
};

View File

@ -82,7 +82,7 @@ const DropdownMenuSubContent = forwardRefIfNeeded<
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"stack-scope z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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",
"stack-scope z-50 min-w-[8rem] overflow-hidden rounded-md border border-black/[0.08] bg-white p-1 text-popover-foreground shadow-lg ring-1 ring-black/[0.06] dark:border-white/[0.08] dark:bg-popover dark:ring-white/[0.08] 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",
className
)}
{...props}
@ -100,7 +100,7 @@ const DropdownMenuContent = forwardRefIfNeeded<
ref={ref}
sideOffset={sideOffset}
className={cn(
"stack-scope z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"stack-scope z-50 min-w-[8rem] overflow-hidden rounded-md border border-black/[0.08] bg-white p-1 text-popover-foreground shadow-md ring-1 ring-black/[0.06] dark:border-white/[0.08] dark:bg-popover dark:ring-white/[0.08]",
"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",
className
)}

View File

@ -19,7 +19,7 @@ const HoverCardContent = forwardRefIfNeeded<
align={align}
sideOffset={sideOffset}
className={cn(
"stack-scope z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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",
"stack-scope z-50 w-64 rounded-md border border-black/[0.08] bg-white p-4 text-popover-foreground shadow-md ring-1 ring-black/[0.06] outline-none dark:border-white/[0.08] dark:bg-popover dark:ring-white/[0.08] 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",
className
)}
{...props}

View File

@ -79,7 +79,7 @@ const MenubarSubContent = React.forwardRef<
<MenubarPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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",
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-black/[0.08] bg-white p-1 text-popover-foreground shadow-lg ring-1 ring-black/[0.06] dark:border-white/[0.08] dark:bg-popover dark:ring-white/[0.08] 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",
className
)}
{...props}
@ -102,7 +102,7 @@ const MenubarContent = React.forwardRef<
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in 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",
"z-50 min-w-[12rem] overflow-hidden rounded-md border border-black/[0.08] bg-white p-1 text-popover-foreground shadow-md ring-1 ring-black/[0.06] dark:border-white/[0.08] dark:bg-popover dark:ring-white/[0.08] data-[state=open]:animate-in 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",
className
)}
{...props}

View File

@ -89,7 +89,7 @@ const NavigationMenuViewport = forwardRefIfNeeded<
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"stack-scope origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
"stack-scope origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border border-black/[0.08] bg-white text-popover-foreground shadow ring-1 ring-black/[0.06] dark:border-white/[0.08] dark:bg-popover dark:ring-white/[0.08] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
ref={ref}

View File

@ -22,7 +22,7 @@ const PopoverContent = forwardRefIfNeeded<
align={align}
sideOffset={sideOffset}
className={cn(
"stack-scope z-50 pointer-events-auto w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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",
"stack-scope z-50 pointer-events-auto w-72 rounded-md border border-black/[0.08] bg-white p-4 text-popover-foreground shadow-md ring-1 ring-black/[0.06] outline-none dark:border-white/[0.08] dark:bg-popover dark:ring-white/[0.08] 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",
className
)}
{...props}

View File

@ -86,7 +86,7 @@ const SelectContent = forwardRefIfNeeded<
<SelectPrimitive.Content
ref={ref}
className={cn(
"stack-scope relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md 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",
"stack-scope relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-black/[0.08] bg-white text-popover-foreground shadow-md ring-1 ring-black/[0.06] dark:border-white/[0.08] dark:bg-popover dark:ring-white/[0.08] 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",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
@ -159,4 +159,3 @@ SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue
};

View File

@ -32,7 +32,7 @@ const SheetOverlay = forwardRefIfNeeded<
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"stack-scope fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-100 data-[state=open]:duration-100",
"stack-scope fixed z-50 gap-4 bg-white p-6 shadow-lg transition ease-in-out dark:bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-100 data-[state=open]:duration-100",
{
variants: {
side: {

View File

@ -31,7 +31,7 @@ const TooltipContent = forwardRefIfNeeded<
ref={ref}
sideOffset={sideOffset}
className={cn(
"stack-scope z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground 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",
"stack-scope z-50 overflow-hidden rounded-md border border-black/[0.08] bg-white px-3 py-1.5 text-xs text-foreground shadow-md ring-1 ring-black/[0.06] dark:border-white/[0.08] dark:bg-primary dark:text-primary-foreground dark:ring-white/[0.08] 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}