diff --git a/.claude/CLAUDE-KNOWLEDGE.md b/.claude/CLAUDE-KNOWLEDGE.md
index f01d96f85d..fa68fc8d04 100644
--- a/.claude/CLAUDE-KNOWLEDGE.md
+++ b/.claude/CLAUDE-KNOWLEDGE.md
@@ -563,5 +563,10 @@ A: Project config overrides only support the hosted `sourceOfTruth` shape. Legac
## Q: How should managed email onboarding e2e tests wait for mock verification?
A: Do not rely on a fixed `wait(1500)` after setup. The mock onboarding path flips the domain to `verified` asynchronously through `runAsynchronously`, so tests should poll the managed-onboarding check endpoint until the expected status appears.
+## Q: How can we redesign or customize account settings pages for the dashboard only, without modifying any shared packages (like `@stackframe/stack` or `packages/template`)?
+A: Intercept the catch-all dynamic route in `apps/dashboard/src/app/(main)/handler/[...stack]/page.tsx` on the server by awaiting `params` and checking if `stack.join("/") === "account-settings"`. If matched, completely bypass `StackHandler` and return a local, custom-built `DashboardAccountSettingsPage` Client Component. To implement the custom views cleanly, copy the necessary forms and layouts from `packages/template` into `apps/dashboard/src/components/dashboard-account-settings/`, replace the relative and package UI imports with local dashboard UI components (such as `@/components/ui/button`, `@/components/ui/input`, and local `Table` / `Badge` / `Switch` / `Avatar` / `Skeleton`), and custom style them with neutral, zinc/white border and card layers to match the dashboard's design system seamlessly.
+
+## Q: What can make dashboard/docs Vercel builds fail after adding dashboard-only account settings code?
+A: Dashboard-only imports still need direct dependency declarations in `apps/dashboard/package.json`; transitive workspace deps are not enough for CI/Vercel resolution. For example, imports of `@oslojs/otp`, `@stackframe/stack-ui`, `qrcode`, and `react-easy-crop` must be declared on the dashboard app itself. Also, placeholder comments like `NEXT_PUBLIC_STACK_PROJECT_ID=# ...` in `docs/.env` parse as empty values during `next build`; use real internal development defaults instead of comment-only assignments.
## Q: How should Microsoft OAuth callback token exchange include scopes?
A: Microsoft Entra ID's v2 token endpoint can reject authorization-code exchanges with `AADSTS70011` if the token request omits `scope`. Keep scope emission opt-in at the provider layer (`includeScopeInCallbackTokenExchange`) and pass the same merged base/provider scopes to `openid-client` via the callback `extras.exchangeBody.scope` parameter. The callback route must forward stored `providerScope` from the outer OAuth info so custom Microsoft provider scopes are included in the token exchange.
diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json
index 39bdbb84ff..6085e965b4 100644
--- a/apps/dashboard/package.json
+++ b/apps/dashboard/package.json
@@ -29,6 +29,7 @@
"@dnd-kit/utilities": "^3.2.2",
"@hookform/resolvers": "^3.3.4",
"@monaco-editor/react": "4.7.0",
+ "@oslojs/otp": "^1.1.0",
"@phosphor-icons/react": "^2.1.10",
"@radix-ui/react-accordion": "^1.2.1",
"@radix-ui/react-alert-dialog": "^1.1.2",
@@ -64,6 +65,7 @@
"@stackframe/dashboard-ui-components": "workspace:*",
"@stackframe/stack": "workspace:*",
"@stackframe/stack-shared": "workspace:*",
+ "@stackframe/stack-ui": "workspace:*",
"@stripe/connect-js": "^3.3.27",
"@stripe/react-connect-js": "^3.3.24",
"@stripe/react-stripe-js": "^3.8.1",
@@ -89,9 +91,11 @@
"next": "16.1.7",
"next-themes": "^0.2.1",
"posthog-js": "^1.336.1",
+ "qrcode": "^1.5.4",
"react": "19.2.3",
"react-day-picker": "^9.6.7",
"react-dom": "19.2.3",
+ "react-easy-crop": "^5.5.6",
"react-globe.gl": "^2.28.2",
"react-hook-form": "^7.53.1",
"react-icons": "^5.0.1",
@@ -114,6 +118,7 @@
"@types/canvas-confetti": "^1.6.4",
"@types/lodash": "^4.17.5",
"@types/node": "20.17.6",
+ "@types/qrcode": "^1.5.5",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/react-syntax-highlighter": "^15.5.13",
diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/queries/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/queries/page-client.tsx
index 0e40542852..b82052353e 100644
--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/queries/page-client.tsx
+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/queries/page-client.tsx
@@ -612,7 +612,7 @@ function QueriesContent() {
value={sqlQuery}
onChange={(e) => setSqlQuery(e.target.value)}
placeholder="SELECT * FROM default.events ORDER BY event_at DESC LIMIT 100"
- className="font-mono text-sm min-h-[80px] resize-y bg-background/60"
+ className="min-h-[80px] resize-y border-black/[0.08] bg-white/95 font-mono text-sm shadow-sm ring-1 ring-black/[0.06] dark:border-border/40 dark:bg-background/60 dark:ring-white/[0.06]"
onKeyDown={(e) => {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey) && !loading) {
e.preventDefault();
diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/ai-query-bar.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/ai-query-bar.tsx
index 70ea162234..1efa2ff6c3 100644
--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/ai-query-bar.tsx
+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/ai-query-bar.tsx
@@ -60,10 +60,11 @@ export function AiQueryBar({
-
+
Current query
@@ -417,7 +417,7 @@ export function AiQueryDialog({
}
}}
placeholder="Ask the AI a question to generate a query."
- className="min-h-16 max-h-32 resize-y overflow-auto border-border/40 bg-background/70 font-mono text-[11px] text-foreground/90 shadow-none placeholder:italic placeholder:text-muted-foreground"
+ className="min-h-16 max-h-32 resize-y overflow-auto border-black/[0.08] bg-white/95 font-mono text-[11px] text-foreground/90 shadow-sm ring-1 ring-black/[0.06] placeholder:italic placeholder:text-muted-foreground dark:border-border/40 dark:bg-background/60 dark:ring-white/[0.06]"
/>
diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/page-client.tsx
index 18d70181bd..f238c53e1d 100644
--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/page-client.tsx
+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/page-client.tsx
@@ -231,7 +231,7 @@ export default function PageClient() {
-
+
{/* Left sidebar — hidden on mobile */}
- {/* Right content */}
-
+ {/* Right content — flush to card edge; companion gap is on
in sidebar-layout */}
+ div:first-child>div]:pt-3",
+ "[&_[role=grid]_.sticky>div:first-child>div]:pb-2.5",
+ "[&_[role=grid]_.sticky>div:first-child>div]:pr-0",
+ "[&_[role=grid]_.sticky>div:first-child>div]:pl-2.5",
+ )}
+ >
{selectedTable ? (
) : (
diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/query-data-grid.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/query-data-grid.tsx
index 3d7c30a657..94d841c412 100644
--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/query-data-grid.tsx
+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/query-data-grid.tsx
@@ -637,7 +637,7 @@ export const QueryDataGrid = forwardRef
)}
{!showEmptyError && (
-
+
columns={columns}
rows={gridData.rows}
diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page-client.tsx
index f8535d189b..e9c51d3ed3 100644
--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page-client.tsx
+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page-client.tsx
@@ -162,7 +162,7 @@ function DisabledProvidersDialog({ open, onOpenChange }: { open?: boolean, onOpe
placeholder="Search for a provider..."
value={providerSearch}
onChange={(e) => setProviderSearch(e.target.value)}
- leadingIcon={ }
+ leadingIcon={ }
/>
{filteredProviders
@@ -448,6 +448,7 @@ function useEmailVerificationToggle() {
open={!!pendingChange}
onClose={() => setPendingChange(null)}
title="Enable email verification requirement"
+ size="2xl"
danger
okButton={{
label: "Apply Change",
@@ -468,35 +469,50 @@ function useEmailVerificationToggle() {
Affected users
-
-
-
+
+
+
+
+
+
+
+
User
Email
- Reason
+ Reason
-
- {pendingChange.affectedUsers.map((user) => (
-
-
- {user.displayName || No name }
-
-
- {user.primaryEmail || No email }
-
-
-
-
-
- ))}
-
+
+
+
+
+
+
+
+
+ {pendingChange.affectedUsers.map((user) => (
+
+
+ {user.displayName || No name }
+
+
+ {user.primaryEmail || No email }
+
+
+
+
+
+ ))}
+
+
+
{pendingChange.totalAffectedCount > pendingChange.affectedUsers.length && (
diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx
index 47e9cbdfe7..e2a5e4dca5 100644
--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx
+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx
@@ -80,9 +80,6 @@ export const providerFormSchema = yupObject({
export type ProviderFormValues = yup.InferType
-/** Modal chrome — "Floating soft" (variant G). */
-const PROVIDER_DIALOG_CHROME_CLASS = "border-0 rounded-3xl bg-background shadow-2xl shadow-black/30 dark:shadow-black/60";
-
function ProviderHeader({ providerId }: { providerId: string }) {
return (
@@ -329,7 +326,6 @@ export function ProviderSettingDialog(props: Props & { open: boolean, onClose: (
title={`${toTitle(props.id)} OAuth provider`}
cancelButton
okButton={{ label: 'Save' }}
- contentClassName={PROVIDER_DIALOG_CHROME_CLASS}
render={(form) => (
("normal");
- const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
useEffect(() => {
@@ -600,36 +603,72 @@ function NewConversationDialog(props: {
const canSubmit = subject.trim() !== "" && initialMessage.trim() !== "";
return (
-
-
-
- Create conversation
-
- Start a support conversation for a user and keep replies, notes, and context in one place.
-
-
-
-
-
-
- User
-
- {selectedUserLabel != null ? (
-
- {selectedUserLabel}
- {
- setSelectedUserId(null);
- setSelectedUserLabel(null);
- }}
- >
-
-
-
- ) : (
+
+
+
+ Cancel
+
+
+ {
+ if (!canSubmit) {
+ return;
+ }
+ const nextUserId = selectedUserId ?? throwErr("A support conversation must be attached to a selected user.");
+
+ setErrorMessage(null);
+ const result = await createConversation(props.currentUser, {
+ projectId: props.projectId,
+ userId: nextUserId,
+ subject: subject.trim(),
+ initialMessage: initialMessage.trim(),
+ priority,
+ });
+ props.onCreated(result.conversationId, nextUserId);
+ props.onOpenChange(false);
+ }}
+ >
+
+ Create Conversation
+
+
+ )}
+ >
+
+
+
+ User
+
+ {selectedUserLabel != null ? (
+
+ {selectedUserLabel}
+ {
+ setSelectedUserId(null);
+ setSelectedUserLabel(null);
+ }}
+ >
+
+
+
+ ) : (
+
(
)}
/>
- )}
-
-
-
-
-
- Subject
-
- setSubject(event.target.value)}
- placeholder="Password reset loop on mobile"
- />
-
-
-
- Priority
-
- setPriority(value)}>
-
-
-
-
- {PRIORITY_OPTIONS.map((option) => (
- {option.label}
- ))}
-
-
-
+ )}
+
+
- Initial message
+ Subject
-
-
- {errorMessage != null && (
-
- )}
-
-
-
props.onOpenChange(false)}>
- Cancel
-
-
{
- if (!canSubmit) {
- return;
- }
- const nextUserId = selectedUserId ?? throwErr("A support conversation must be attached to a selected user.");
-
- setIsSubmitting(true);
- setErrorMessage(null);
- try {
- const result = await createConversation(props.currentUser, {
- projectId: props.projectId,
- userId: nextUserId,
- subject: subject.trim(),
- initialMessage: initialMessage.trim(),
- priority,
- });
- props.onCreated(result.conversationId, nextUserId);
- props.onOpenChange(false);
- } finally {
- setIsSubmitting(false);
- }
- }}
- >
- {isSubmitting ? : }
- Create Conversation
-
+
+
+ Priority
+
+ setPriority(value)}>
+
+
+
+
+ {PRIORITY_OPTIONS.map((option) => (
+ {option.label}
+ ))}
+
+
-
-
+
+
+
+ Initial message
+
+
+
+ {errorMessage != null && (
+
+ )}
+
+
);
}
@@ -1111,28 +1116,22 @@ export default function PageClient() {
} : { fillWidth: true as const })}
>
-
{!isConversationSelected && (
updateSelection({ userId: null, conversationId: null })}>
+ Clear Filter
+
+ ) : undefined}
>
-
-
-
- Unified inbox
-
- Search conversations by subject or user and jump straight into the full history.
-
-
- {selectedUserId != null && (
-
updateSelection({ userId: null, conversationId: null })}>
- Clear Filter
-
- )}
-
-
+
setSearchInput(event.target.value)}
@@ -1146,11 +1145,12 @@ export default function PageClient() {
onSelect={(id) => id === "closed" ? setStatusFilter("closed") : id === "pending" ? setStatusFilter("pending") : id === "open" ? setStatusFilter("open") : setStatusFilter("all")}
showBadge={false}
size="sm"
- gradient="blue"
+ gradient="default"
+ glassmorphic={false}
/>
-
+
{conversationsLoading && (
@@ -1201,8 +1201,8 @@ export default function PageClient() {
"w-full rounded-2xl border border-transparent px-2 py-2 text-left",
"transition-colors duration-150 hover:transition-none",
isActive
- ? "border-blue-400/25 bg-blue-500/10 shadow-[0_8px_24px_-20px_rgba(30,80,255,0.9)]"
- : "hover:bg-foreground/[0.03]",
+ ? "border-black/[0.08] bg-zinc-50 ring-1 ring-black/[0.06] dark:border-white/[0.08] dark:bg-white/[0.06] dark:ring-white/[0.06]"
+ : "hover:bg-zinc-50 dark:hover:bg-foreground/[0.03]",
)}
onClick={() => updateSelection({ conversationId: conversation.conversationId, userId: conversation.userId })}
>
@@ -1254,7 +1254,7 @@ export default function PageClient() {
-
+
Last message
@@ -1289,7 +1289,7 @@ export default function PageClient() {
{conversationLoading && (
-
+
)}
@@ -1301,7 +1301,7 @@ export default function PageClient() {
{!conversationLoading && conversationDetail != null && (
-
+
{isLgViewport && (
@@ -1462,7 +1463,7 @@ export default function PageClient() {
)}
{!conversationLoading && conversationDetail == null && selectedUserId != null && selectedUser != null && (
-
+
@@ -1492,15 +1493,15 @@ export default function PageClient() {
-
+
Primary Email
{selectedUser.primaryEmail ?? "Not set"}
-
+
Signed Up
{fromNow(selectedUser.signedUpAt)}
-
+
Last Active
{fromNow(selectedUser.lastActiveAt)}
@@ -1510,22 +1511,17 @@ export default function PageClient() {
)}
{!conversationLoading && conversationDetail == null && selectedUserId == null && (
-
-
-
-
-
-
- Pick a conversation or open a user
-
- This inbox is the single place to investigate a user, add internal context, and reply in the same conversation the user sees.
-
-
-
setNewConversationOpen(true)}>
-
- Create First Conversation
-
-
+
+ setNewConversationOpen(true)}>
+
+ Create First Conversation
+
)}
diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx
index 4b5561c45a..96aba917e1 100644
--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx
+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx
@@ -2,6 +2,7 @@
import { FormDialog } from "@/components/form-dialog";
import { InputField, SwitchField } from "@/components/form-fields";
import { InlineSaveDiscard } from "@/components/inline-save-discard";
+import { DesignAlert } from "@/components/design-components";
import { SettingCard, SettingSwitch } from "@/components/settings";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, ActionCell, ActionDialog, Alert, Button, Typography } from "@/components/ui";
import { useUpdateConfig } from "@/lib/config-update";
@@ -180,18 +181,18 @@ function EditDialog(props: {
}}
render={(form) => (
<>
-
-
+
+
Please ensure you own or have control over this domain. Also note that each subdomain (e.g. blog.example.com, app.example.com) is treated as a distinct domain.
-
Wildcard domains: You can use wildcards to match multiple domains:
-
-
+
0 ? (
) : (
-
- No domains added yet.
-
+
)}
diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-sent/domain-reputation-card.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-sent/domain-reputation-card.tsx
index a845873083..047f2aec93 100644
--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-sent/domain-reputation-card.tsx
+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-sent/domain-reputation-card.tsx
@@ -270,7 +270,7 @@ export function DomainReputationCard() {
diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-sent/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-sent/page-client.tsx
index 96bc4276f9..9319fa6cf9 100644
--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-sent/page-client.tsx
+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-sent/page-client.tsx
@@ -73,14 +73,16 @@ const emailTableColumns: DataGridColumnDef
[] = [
{
id: "recipient",
header: "Recipient",
- width: 200,
+ width: 160,
+ minWidth: 96,
type: "string",
accessor: (row) => getRecipientDisplay(row),
},
{
id: "subject",
header: "Subject",
- width: 220,
+ width: 180,
+ minWidth: 120,
flex: 1,
type: "string",
accessor: (row) => getSubjectDisplay(row),
@@ -88,7 +90,8 @@ const emailTableColumns: DataGridColumnDef[] = [
{
id: "scheduledAt",
header: "Time",
- width: 180,
+ width: 140,
+ minWidth: 100,
type: "dateTime",
accessor: (row) => getTimeValue(row),
},
@@ -96,6 +99,7 @@ const emailTableColumns: DataGridColumnDef[] = [
id: "status",
header: "Status",
width: 120,
+ minWidth: 108,
renderCell: ({ row }) => {
const status = row.status;
return (
@@ -158,26 +162,28 @@ function EmailSendDataTable() {
}
return (
- {
- router.push(`email-viewer/${row.id}`);
- }}
- />
+
+ {
+ router.push(`email-viewer/${row.id}`);
+ }}
+ />
+
);
}
@@ -190,15 +196,16 @@ export default function PageClient() {
title="Sent"
description="View email logs and domain reputation"
>
-
+
{/* Left side: Email Log with toggle inside card */}
-
+
-
+
@@ -220,7 +227,7 @@ export default function PageClient() {
{/* Right side: Domain Reputation */}
-
diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/launch-checklist/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/launch-checklist/page-client.tsx
index c22eea820c..03881ff1fd 100644
--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/launch-checklist/page-client.tsx
+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/launch-checklist/page-client.tsx
@@ -142,20 +142,16 @@ type LaunchTask = {
const STATUS_META: Record<
LaunchTaskStatus,
{
- cardClass: string,
inactiveIcon: string,
}
> = {
done: {
- cardClass: "border-primary/30 bg-background transition-all duration-300 hover:shadow-lg dark:border-primary/40 dark:shadow-primary/5",
inactiveIcon: "text-emerald-500 dark:text-emerald-400",
},
action: {
- cardClass: "border-primary/30 bg-background transition-all duration-300 hover:shadow-lg dark:border-primary/40 dark:shadow-primary/5",
inactiveIcon: "text-muted-foreground",
},
blocked: {
- cardClass: "border-primary/30 bg-background transition-all duration-300 hover:shadow-lg dark:border-primary/40 dark:shadow-primary/5",
inactiveIcon: "text-muted-foreground",
},
};
@@ -191,17 +187,16 @@ function TaskCard(props: {
isExpanded: boolean,
onToggle: () => void,
}) {
- const meta = STATUS_META[props.task.status];
const allItemsDone = props.task.items.every((item) => item.done);
return (
{props.isExpanded ? (
@@ -616,8 +611,9 @@ export default function PageClient() {
selectedCategory={selectedProviderTab}
onSelect={setSelectedProviderTab}
showBadge={false}
+ size="sm"
gradient="default"
- className="!border-0 !bg-transparent !p-0"
+ glassmorphic
/>
{selectedProviderGuide && (
@@ -744,13 +740,10 @@ export default function PageClient() {
>
- {/* Subtle blue glow on bottom border */}
-
-
-
+
{/* Header */}
diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/page-layout.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/page-layout.tsx
index d7bc2618f3..76a872ce62 100644
--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/page-layout.tsx
+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/page-layout.tsx
@@ -37,6 +37,7 @@ export function PageLayout(props: {
diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/list-section.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/list-section.tsx
index 5e1de69ef8..559b252645 100644
--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/list-section.tsx
+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/list-section.tsx
@@ -1,6 +1,7 @@
"use client";
-import { Button, Input, SimpleTooltip } from "@/components/ui";
+import { Button, SimpleTooltip } from "@/components/ui";
+import { DesignInput } from "@/components/design-components";
import { cn } from "@/lib/utils";
import { MagnifyingGlassIcon, PlusIcon } from "@phosphor-icons/react";
import React, { ReactNode, useState } from "react";
@@ -53,22 +54,16 @@ export function ListSection({
"relative flex items-center transition-all duration-150 hover:transition-none",
isSearchFocused ? "w-[160px]" : "w-[140px]"
)}>
- }
+ leadingIcon={ }
placeholder={searchPlaceholder}
value={searchValue || ''}
onChange={(e) => onSearchChange(e.target.value)}
onFocus={() => setIsSearchFocused(true)}
onBlur={() => setIsSearchFocused(false)}
- className={cn(
- "w-full rounded-lg",
- "bg-background dark:bg-foreground/[0.04] border border-border/50 dark:border-foreground/[0.08]",
- "focus:bg-background dark:focus:bg-foreground/[0.06] focus:outline-none focus:ring-1 focus:ring-foreground/[0.1] focus:border-border dark:focus:border-foreground/[0.12]",
- "placeholder:text-muted-foreground/50",
- "transition-all duration-150 hover:transition-none"
- )}
+ className="h-8 w-full rounded-lg"
/>
diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx
index 1a944589e4..acf68b6c1d 100644
--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx
+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-list-view.tsx
@@ -257,7 +257,7 @@ function ConnectionLine({ fromRef, toRef, containerRef, quantity }: ConnectionLi
cx={midpoint.x}
cy={midpoint.y}
r="14"
- className="fill-background"
+ className="fill-white dark:fill-background"
stroke="hsl(200, 91%, 70%)"
strokeWidth="1"
strokeOpacity="0.3"
diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/session-replays/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/session-replays/page-client.tsx
index 3c240fca9b..4145e77c79 100644
--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/session-replays/page-client.tsx
+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/session-replays/page-client.tsx
@@ -39,6 +39,16 @@ import {
type StreamInfo,
} from "./session-replay-machine";
+const replaysPanelShellClass =
+ "flex-1 min-h-[520px] overflow-hidden rounded-xl bg-white/90 ring-1 ring-black/[0.06] dark:bg-background/60 dark:ring-white/[0.06]";
+const replaysListChromeClass =
+ "shrink-0 space-y-2 border-b border-black/[0.06] px-3 py-2.5 dark:border-border/30";
+const replaysDetailChromeClass =
+ "flex h-10 shrink-0 items-center justify-between gap-3 border-b border-black/[0.06] px-3 py-2 dark:border-border/30";
+const replaysViewerSurfaceClass = "bg-zinc-100 dark:bg-background";
+const replaysTransportBarClass =
+ "flex items-center gap-3 border-t border-black/[0.06] bg-white/95 px-3 dark:border-border/30 dark:bg-background/80";
+
const PAGE_SIZE = 50;
const INITIAL_CHUNK_BATCH = 20;
const BACKGROUND_CHUNK_BATCH = 50;
@@ -289,7 +299,7 @@ function Timeline({
const hoveredMarker = hoveredMarkerIndex !== null ? markers?.[hoveredMarkerIndex] ?? null : null;
return (
-
+
onSpeedChange(Number(e.target.value))}
>
@@ -1463,642 +1473,647 @@ export default function PageClient({ initialReplayId, lockedUserId }: PageClient
) : undefined}
fillWidth
- noPadding={isEmbedded}
+ noPadding
>
-
-
- {!isStandaloneReplayPage && (
- <>
-
-
-
-
-
- Sessions{!loadingInitial && recordings.length > 0 ? ` (${recordings.length}${nextCursor ? "+" : ""})` : ""}
-
-
-
-
+
+
+ {!isStandaloneReplayPage && (
+ <>
+
+
+
+
+
+ Sessions{!loadingInitial && recordings.length > 0 ? ` (${recordings.length}${nextCursor ? "+" : ""})` : ""}
+
+
+
+
+
+ Filters
+ {activeFilterCount > 0 && (
+
+ {activeFilterCount}
+
+ )}
+
+
+ e.preventDefault()}
>
-
- Filters
- {activeFilterCount > 0 && (
-
- {activeFilterCount}
-
+ {!isEmbedded && (
+ { requestAnimationFrame(() => openFilterDialog("user")); }}>
+ User
+
)}
-
-
- e.preventDefault()}
- >
- {!isEmbedded && (
- { requestAnimationFrame(() => openFilterDialog("user")); }}>
- User
+ { requestAnimationFrame(() => openFilterDialog("team")); }}>
+ Team
+
+ { requestAnimationFrame(() => openFilterDialog("duration")); }}>
+ Duration
+
+ { requestAnimationFrame(() => openFilterDialog("lastActive")); }}>
+ Last active
+
+ { requestAnimationFrame(() => openFilterDialog("clicks")); }}>
+ Click count
+
+
+
+
+ {activeFilterCount > 0 && (
+
+ {appliedFilters.userId && !isEmbedded && (
+
+ user:{appliedFilters.userLabel || "selected"}
+
+ )}
+ {appliedFilters.teamId && (
+
+ team:{appliedFilters.teamLabel || "selected"}
+
+ )}
+ {(appliedFilters.durationMinSeconds || appliedFilters.durationMaxSeconds) && (
+
+ duration
+
)}
- { requestAnimationFrame(() => openFilterDialog("team")); }}>
- Team
-
- { requestAnimationFrame(() => openFilterDialog("duration")); }}>
- Duration
-
- { requestAnimationFrame(() => openFilterDialog("lastActive")); }}>
- Last active
-
- { requestAnimationFrame(() => openFilterDialog("clicks")); }}>
- Click count
-
-
-
+ {appliedFilters.lastActivePreset && (
+
+ last active: {appliedFilters.lastActivePreset}
+
+ )}
+ {appliedFilters.clickCountMin && (
+
+ clicks
+
+ )}
+ setAppliedFilters(baseFilters)}
+ >
+
+ clear
+
+
+ )}
- {activeFilterCount > 0 && (
-
- {appliedFilters.userId && !isEmbedded && (
-
- user:{appliedFilters.userLabel || "selected"}
-
- )}
- {appliedFilters.teamId && (
-
- team:{appliedFilters.teamLabel || "selected"}
-
- )}
- {(appliedFilters.durationMinSeconds || appliedFilters.durationMaxSeconds) && (
-
- duration
-
- )}
- {appliedFilters.lastActivePreset && (
-
- last active: {appliedFilters.lastActivePreset}
-
- )}
- {appliedFilters.clickCountMin && (
-
- clicks
-
- )}
- setAppliedFilters(baseFilters)}
- >
-
- clear
-
-
- )}
-
-
- setActiveFilterDialog(open ? "user" : null)}>
-
-
- User Filter
-
-
-
(
- {
+ setActiveFilterDialog(open ? "user" : null)}>
+
+
+ User Filter
+
+
+
(
+ {
setAppliedFilters((prev) => ({
...prev,
userId: user.id,
userLabel: user.displayName ?? user.primaryEmail ?? user.id,
}));
setActiveFilterDialog(null);
+ }}
+ >
+ Select
+
+ )}
+ />
+
+
{
+ setAppliedFilters((prev) => ({ ...prev, userId: "", userLabel: "" }));
+ setActiveFilterDialog(null);
}}
>
- Select
+ Clear
- )}
- />
-
- {
- setAppliedFilters((prev) => ({ ...prev, userId: "", userLabel: "" }));
- setActiveFilterDialog(null);
- }}
- >
- Clear
-
+
-
-
-
-
- setActiveFilterDialog(open ? "team" : null)}>
-
-
- Team Filter
-
-
-
(
- {
+
+
+
+ setActiveFilterDialog(open ? "team" : null)}>
+
+
+ Team Filter
+
+
+
(
+ {
setAppliedFilters((prev) => ({
...prev,
teamId: team.id,
teamLabel: team.displayName,
}));
setActiveFilterDialog(null);
+ }}
+ >
+ Select
+
+ )}
+ />
+
+
{
+ setAppliedFilters((prev) => ({ ...prev, teamId: "", teamLabel: "" }));
+ setActiveFilterDialog(null);
}}
>
- Select
+ Clear
- )}
- />
-
+
+
+
+
+
+ setActiveFilterDialog(open ? "duration" : null)}>
+
+
+ Duration Filter
+
+
+
+
+
+ setActiveFilterDialog(open ? "lastActive" : null)}>
+
+
+ Last Active Filter
+
+
+ {([["24h", "Last 24 hours"], ["7d", "Last 7 days"], ["30d", "Last 30 days"]] as const).map(([value, label]) => (
+ {
+ setAppliedFilters((prev) => ({ ...prev, lastActivePreset: value }));
+ setActiveFilterDialog(null);
+ }}
+ >
+ {label}
+
+ ))}
+
+
{
- setAppliedFilters((prev) => ({ ...prev, teamId: "", teamLabel: "" }));
- setActiveFilterDialog(null);
+ setAppliedFilters((prev) => ({ ...prev, lastActivePreset: "" }));
+ setActiveFilterDialog(null);
}}
>
Clear
-
-
-
-
- setActiveFilterDialog(open ? "duration" : null)}>
-
-
- Duration Filter
-
-
+
+
+ setActiveFilterDialog(open ? "clicks" : null)}>
+
+
+ Click Count Filter
+
+