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({
-
+
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 -
- - +
+
+ + + + + + - + - - {pendingChange.affectedUsers.map((user) => ( - - - - - - ))} -
User EmailReasonReason
- {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} - -
- ) : ( + + + + 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} + +
+ ) : ( +
(
- -
-
- - Subject - - setSubject(event.target.value)} - placeholder="Password reset loop on mobile" - /> -
-
- - Priority - -
-
+ )} +
+
- Initial message + Subject -