Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 0 additions & 74 deletions apps/web/src/components/app-root-context.tsx

This file was deleted.

6 changes: 2 additions & 4 deletions apps/web/src/components/avatar/avatar-management-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
} from "react";
import { createPortal } from "react-dom";

import { useAppRootContainer } from "@/components/app-root-context.js";
import { AvatarCustomizationPanel } from "@/components/avatar/avatar-customization-panel.js";
import { ChatAvatar } from "@/components/avatar/chat-avatar.js";
import { uploadAvatarImage } from "@/domains/avatar/api.js";
Expand Down Expand Up @@ -40,7 +39,6 @@ export function AvatarManagementModal({
onSaveCharacter,
onUploadImage,
}: AvatarManagementModalProps) {
const portalTarget = useAppRootContainer();
const titleId = useId();
const overlayRef = useRef<HTMLDivElement>(null);
const closeButtonRef = useRef<HTMLButtonElement>(null);
Expand Down Expand Up @@ -134,7 +132,7 @@ export function AvatarManagementModal({
[onSaveCharacter, handleClose],
);

if (!open || !portalTarget) {
if (!open) {
return null;
}

Expand Down Expand Up @@ -287,6 +285,6 @@ export function AvatarManagementModal({
onChange={handleFileSelect}
/>
</div>,
portalTarget,
document.body,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Future dark/velvet theme support requires data-theme on , not a scoped element

The CSS in tokens.css defines dark and velvet themes via [data-theme="dark"] and [data-theme="velvet"] selectors. Currently no data-theme attribute is set anywhere in the app (index.html has a bare <html lang="en">). When theme switching is implemented, the data-theme attribute must be placed on <html> (or at minimum an ancestor of <body>) for portaled content on document.body to inherit the correct theme tokens. If it were placed on an inner wrapper element (as .app-root would have been), portals to document.body would fall back to light-mode :root values. This is not a current bug but a constraint worth documenting.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a pre-existing architectural detail, not introduced by this PR.

The constraint the bot identifies is already satisfied by the current setup: applyThemePreference() applies theme classes to document.documentElement (the <html> element), and document.body is a child of <html>, so portals to document.body correctly inherit all theme tokens via CSS inheritance regardless of whether theming uses data-theme attributes or CSS classes.

Worth noting: there's a pre-existing gap between tokens.css (which uses [data-theme="dark"] selectors) and applyThemePreference() (which toggles CSS classes like .dark), but that's unrelated to this PR's portal target change.

);
}
8 changes: 1 addition & 7 deletions apps/web/src/components/command-palette/command-palette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
} from "react";
import { createPortal } from "react-dom";

import { useAppRootContainer } from "@/components/app-root-context.js";
import { Button } from "@vellum/design-library";
import { Typography } from "@vellum/design-library";
import { useIsMobile } from "@/hooks/use-is-mobile.js";
Expand Down Expand Up @@ -82,7 +81,6 @@ export const CommandPalette: FC<CommandPaletteProps> = ({
onKeyDown,
}) => {
const isMobile = useIsMobile();
const portalTarget = useAppRootContainer();
const overlayRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const listRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -239,10 +237,6 @@ export const CommandPalette: FC<CommandPaletteProps> = ({
);
}

if (!portalTarget) {
return null;
}

return createPortal(
<div
ref={overlayRef}
Expand All @@ -258,6 +252,6 @@ export const CommandPalette: FC<CommandPaletteProps> = ({
{resultsList}
</div>
</div>,
portalTarget,
document.body,
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ const OAUTH_STYLES = `
--oauth-shadow: 0 1px 3px rgba(0,0,0,0.2), 0 4px 12px rgba(0,0,0,0.3);
}
}
:root.dark {
:root[data-theme="dark"] {
--oauth-surface: #1A1A18;
--oauth-surface-card: #2A2A28;
--oauth-card-border: #3A3A37;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ const OAUTH_STYLES = `
--oauth-shadow: 0 1px 3px rgba(0,0,0,0.2), 0 4px 12px rgba(0,0,0,0.3);
}
}
:root.dark {
:root[data-theme="dark"] {
--oauth-surface: #1A1A18;
--oauth-surface-card: #2A2A28;
--oauth-card-border: #3A3A37;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type { FC, KeyboardEvent, MouseEvent } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";

import { useAppRootContainer } from "@/components/app-root-context.js";
import { Button } from "@vellum/design-library";
import { Typography } from "@vellum/design-library";
import { getActiveOrganizationIdForRequests } from "@/stores/organization-store.js";
Expand Down Expand Up @@ -63,7 +62,6 @@ export const AttachmentPreviewModal: FC<AttachmentPreviewModalProps> = ({
attachment,
assistantId,
}) => {
const portalTarget = useAppRootContainer();
const overlayRef = useRef<HTMLDivElement>(null);
const closeButtonRef = useRef<HTMLButtonElement>(null);
const [objectUrl, setObjectUrl] = useState<string | null>(null);
Expand Down Expand Up @@ -166,7 +164,7 @@ export const AttachmentPreviewModal: FC<AttachmentPreviewModalProps> = ({
await saveFile(effectiveUrl, attachment.filename);
}, [effectiveUrl, attachment.filename]);

if (!open || !portalTarget) {
if (!open) {
return null;
}

Expand Down Expand Up @@ -336,6 +334,6 @@ export const AttachmentPreviewModal: FC<AttachmentPreviewModalProps> = ({
/>
</div>
</div>,
portalTarget,
document.body,
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";

import { useAppRootContainer } from "@/components/app-root-context.js";

export interface ContextWindowUsage {
tokens: number;
maxTokens: number | null;
Expand Down Expand Up @@ -39,7 +37,6 @@ function formatTokens(count: number): string {
}

export function ContextWindowIndicator({ usage }: ContextWindowIndicatorProps) {
const appRootContainer = useAppRootContainer();
const [isHovered, setIsHovered] = useState(false);
const [tooltipPosition, setTooltipPosition] = useState<{ top: number; left: number } | null>(
null,
Expand Down Expand Up @@ -141,11 +138,8 @@ export function ContextWindowIndicator({ usage }: ContextWindowIndicatorProps) {
style={{ transition: "stroke-dashoffset 250ms ease-out, stroke 250ms ease-out" }}
/>
</svg>
{isHovered && appRootContainer &&
{isHovered &&
createPortal(
// Portal into the `.app-root` element so appTheme.css tokens like
// `--surface-lift` resolve. `document.body` would render outside the
// theme scope and the background would paint transparent.
<div
ref={tooltipRef}
role="tooltip"
Expand Down Expand Up @@ -176,7 +170,7 @@ export function ContextWindowIndicator({ usage }: ContextWindowIndicatorProps) {
compacts its context.
</div>
</div>,
appRootContainer,
document.body,
)}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@ export function DocumentViewerContainer({
const [wordCount, setWordCount] = useState(() => countWords(content));
const [isExportingPDF, setIsExportingPDF] = useState(false);

const isDark = typeof document !== "undefined" && document.documentElement.classList.contains("dark");
const theme = typeof document !== "undefined" ? document.documentElement.dataset.theme : undefined;
const isDark = theme === "dark" || theme === "velvet";

const srcdoc = useMemo(
() => generateEditorHTML(documentName, content, isDark),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ export function DocumentViewerContainer({
const [wordCount, setWordCount] = useState(() => countWords(content));
const [isExportingPDF, setIsExportingPDF] = useState(false);

const isDark = typeof document !== "undefined" && document.documentElement.classList.contains("dark");
const theme = typeof document !== "undefined" ? document.documentElement.dataset.theme : undefined;
const isDark = theme === "dark" || theme === "velvet";

const srcdoc = useMemo(
() => generateEditorHTML(documentName, content, isDark),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
} from "react";
import { createPortal } from "react-dom";

import { useAppRootContainer } from "@/components/app-root-context.js";
import { Dropdown } from "@vellum/design-library/components/dropdown";
import { Input } from "@vellum/design-library/components/input";
import { Notice } from "@vellum/design-library/components/notice";
Expand Down Expand Up @@ -44,7 +43,6 @@ export function TrustRuleFormModal({
onClose,
onSaved,
}: TrustRuleFormModalProps) {
const portalTarget = useAppRootContainer();
const dialogRef = useRef<HTMLDivElement>(null);
const [tool, setTool] = useState(existingRule.tool);
const [pattern, setPattern] = useState(existingRule.pattern);
Expand Down Expand Up @@ -95,8 +93,6 @@ export function TrustRuleFormModal({
[onClose],
);

if (!portalTarget) return null;

return createPortal(
<div
ref={dialogRef}
Expand Down Expand Up @@ -213,6 +209,6 @@ export function TrustRuleFormModal({
</form>
</div>
</div>,
portalTarget,
document.body,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Pencil, ShieldCheck, Trash2 } from "lucide-react";
import { type KeyboardEvent, type MouseEvent, useCallback, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";

import { useAppRootContainer } from "@/components/app-root-context.js";
import { Button } from "@vellum/design-library/components/button";
import { ConfirmDialog } from "@vellum/design-library/components/confirm-dialog";
import { Toggle } from "@vellum/design-library/components/toggle";
Expand Down Expand Up @@ -62,7 +61,6 @@ export interface TrustRulesModalProps {
}

export function TrustRulesModal({ assistantId, onClose }: TrustRulesModalProps) {
const portalTarget = useAppRootContainer();
const dialogRef = useRef<HTMLDivElement>(null);
const [rules, setRules] = useState<TrustRuleItem[]>([]);
const [isLoading, setIsLoading] = useState(true);
Expand Down Expand Up @@ -153,8 +151,6 @@ export function TrustRulesModal({ assistantId, onClose }: TrustRulesModalProps)
[onClose],
);

if (!portalTarget) return null;

return createPortal(
<>
<div
Expand Down Expand Up @@ -291,6 +287,6 @@ export function TrustRulesModal({ assistantId, onClose }: TrustRulesModalProps)
onCancel={() => setRuleToDelete(null)}
/>
</>,
portalTarget,
document.body,
);
}
6 changes: 4 additions & 2 deletions apps/web/src/domains/settings/utils/theme-preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ export function applyThemePreference(theme: ThemePreference): void {
isVelvet || theme === "dark" || (theme === "system" && prefersDark);

const root = document.documentElement;
root.classList.toggle("dark", shouldBeDark);
root.classList.toggle("velvet", isVelvet);
root.setAttribute(
"data-theme",
isVelvet ? "velvet" : shouldBeDark ? "dark" : "light",
);
}

export function getEffectiveThemePreference(
Expand Down