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

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/profiles/web/dist/assets/index-BwJcCbGq.css

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion src/profiles/web/dist/assets/index-DwqUySVf.css

This file was deleted.

28 changes: 14 additions & 14 deletions src/profiles/web/dist/index.html
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ProFiles</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<script type="module" crossorigin src="/assets/index-D7fUv0zM.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DwqUySVf.css">
<link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head>
<body class="antialiased selection:bg-indigo-500 selection:text-white">
<div id="root"></div>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ProFiles</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<script type="module" crossorigin src="/assets/index-7FpTlh3a.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BwJcCbGq.css">
<link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head>
<body class="antialiased selection:bg-indigo-500 selection:text-white">
<div id="root"></div>
</body>
</html>
Expand Down
2 changes: 1 addition & 1 deletion src/profiles/web/dist/sw.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

79 changes: 79 additions & 0 deletions src/profiles/web/src/components/ConfigEditorModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { act, cleanup, render, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { EditorView } from "codemirror";
import { ConfigEditorModal } from "./ConfigEditorModal";
import {
Expand Down Expand Up @@ -755,3 +757,80 @@ describe("ConfigEditorModal", () => {
expect(saveSpy).not.toHaveBeenCalled();
});
});

/**
* Editor chrome tests (issues #98 + #99): the CodeMirror surface must be
* solid and token-backed (never the frosted modal glass), the gutter must
* be opaque, and the editor must sit inside a padded bordered card that
* separates it from the modal chrome.
*/
describe("ConfigEditorModal editor chrome (issues #98/#99)", () => {
it("test_editor_theme_is_solid_and_token_backed", async () => {
renderModal();
await readyEditorView();

// CodeMirror injects the compiled theme as real <style> rules; those
// declarations are the honest thing to assert on. Only the stylesheet
// of the editor's own theme module is inspected — CodeMirror separately
// mounts built-in base-theme styles (which may use transparent) and
// they are not this modal's responsibility.
const themeModuleStyles = Array.from(document.querySelectorAll("style"))
.map((node) => node.textContent ?? "")
.filter((cssText) => cssText.includes("--app-editor-bg"))
.join("\n");

// Editor canvas: solid, sourced from the new editor token (issue #98).
expect(themeModuleStyles).toMatch(/background-color:\s*var\(--app-editor-bg\)/);
// Gutter: opaque background from its own token.
expect(themeModuleStyles).toMatch(/background-color:\s*var\(--app-editor-gutter-bg\)/);
// Static guard against regression to the old bug (the theme root
// painting `transparent` over the frosted modal glass): the shipped
// theme object must never set a transparent background. CodeMirror
// additionally mounts built-in base-theme <style> rules that may
// legitimately use transparent — that is why this is asserted on the
// source rather than the merged stylesheet.
const modalSource = (await import("./ConfigEditorModal.tsx?raw")).default;
expect(modalSource).not.toMatch(/backgroundColor:\s*"transparent"/);
});

it("test_editor_tokens_defined_for_both_themes", async () => {
// The index.css token sheet defines the editor tokens in BOTH the
// dark and light theme blocks — the same single-attribute flip as
// every other --app-* token. (A plain node:fs read: vitest serves
// source files through virtual paths, so import.meta URLs cannot be
// dereferenced here.)
const css = readFileSync(join(process.cwd(), "src", "index.css"), "utf8");
expect(css).not.toBe("");
const darkBlock = css.slice(0, css.indexOf('[data-theme="light"]'));
const lightBlock = css.slice(css.indexOf('[data-theme="light"]'));
for (const token of [
"--app-editor-bg",
"--app-editor-gutter-bg",
"--app-editor-active-line",
"--app-editor-selection",
"--app-editor-border",
]) {
expect(darkBlock).toContain(`${token}:`);
expect(lightBlock).toContain(`${token}:`);
}
});

it("test_editor_sits_inside_padded_bordered_card", async () => {
renderModal();
const dialog = await screen.findByRole("dialog", { name: /configuration editor/i });
await readyEditorView();

// The bordered card wraps the editor — padding on the body, a
// rounded border on the card (issue #99).
const card = dialog.querySelector('[data-testid="editor-card"]');
expect(card).toBeTruthy();
expect(card!.className).toContain("rounded-lg");
expect(card!.className).toContain("border");
const cardParent = card!.parentElement!;
expect(
cardParent.className.includes("p-3") || cardParent.className.includes("p-4"),
).toBe(true);
// The editor really lives inside the card.
expect(card!.querySelector(".cm-editor")).toBeTruthy();
});
});
66 changes: 43 additions & 23 deletions src/profiles/web/src/components/ConfigEditorModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,18 @@ type SaveAsPrompt = { resolve: (target: SaveAsTarget | null) => void };

/**
* Editor theme built from the app's `data-theme` CSS tokens (index.css
* `--app-*` variables). Only `var()` references are used — the editor
* follows the light/dark switch automatically, like every other surface.
* `--app-editor-*` surface tokens, issue #98). A solid VSCode-style canvas:
* opaque editor background and gutter (no transparency over the frosted
* modal), token-backed active line, selection, bracket match and panel
* colors. Only `var()` references are used — the editor follows the
* light/dark switch automatically, like every other surface.
*/
const configEditorTheme = EditorView.theme({
"&": {
height: "100%",
fontSize: "12.5px",
color: "var(--app-text-primary)",
backgroundColor: "transparent",
backgroundColor: "var(--app-editor-bg)",
},
".cm-scroller": {
fontFamily: "'JetBrains Mono', 'Fira Code', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
Expand All @@ -84,21 +87,30 @@ const configEditorTheme = EditorView.theme({
".cm-cursor, .cm-dropCursor": { borderLeftColor: "var(--color-indigo-500, #6366f1)" },
"&.cm-focused": { outline: "none" },
".cm-selectionBackground, &.cm-focused .cm-selectionBackground": {
backgroundColor: "var(--app-hover)",
backgroundColor: "var(--app-editor-selection)",
},
".cm-activeLine": { backgroundColor: "var(--app-editor-active-line)" },
".cm-matchingBracket": {
color: "inherit",
backgroundColor: "var(--app-editor-selection)",
outline: "1px solid var(--app-editor-border)",
},
".cm-nonmatchingBracket": {
color: "inherit",
outline: "1px solid var(--status-danger-border, #fb7185)",
},
".cm-activeLine": { backgroundColor: "var(--app-elevated)" },
".cm-gutters": {
backgroundColor: "transparent",
backgroundColor: "var(--app-editor-gutter-bg)",
color: "var(--app-text-faint)",
border: "none",
borderRight: "1px solid var(--app-border)",
borderRight: "1px solid var(--app-editor-border)",
},
".cm-activeLineGutter": {
backgroundColor: "var(--app-elevated)",
backgroundColor: "var(--app-editor-active-line)",
color: "var(--app-text-secondary)",
},
".cm-panels": {
backgroundColor: "var(--app-glass-panel-strong)",
backgroundColor: "var(--app-editor-bg)",
color: "var(--app-text-primary)",
},
".cm-tooltip": {
Expand Down Expand Up @@ -809,21 +821,29 @@ export function ConfigEditorModal({ open, onClose, onApplied }: ConfigEditorModa
</div>
)}

{/* Editor body */}
<div className="min-h-0 flex-1 overflow-hidden">
{load.status === "loading" ? (
<div className="flex h-full items-center justify-center">
<div className="h-8 w-8 animate-spin rounded-full border-2 border-indigo-600 border-t-transparent" />
</div>
) : load.status === "error" ? (
<div className="flex h-full items-center justify-center p-6">
<div className="max-w-md rounded-md border border-status-danger-border bg-status-danger-bg p-4 text-center text-xs text-status-danger-text">
{load.message}
{/* Editor body — padded, then the bordered card that visually
separates the solid editor canvas from the modal chrome
(issue #99). Every state renders inside the card so layout
does not jump between load, error and ready. */}
<div className="min-h-0 flex-1 p-3 sm:p-4">
<div
data-testid="editor-card"
className="h-full overflow-hidden rounded-lg border border-[var(--app-editor-border)] bg-[var(--app-editor-bg)]"
>
{load.status === "loading" ? (
<div className="flex h-full items-center justify-center">
<div className="h-8 w-8 animate-spin rounded-full border-2 border-indigo-600 border-t-transparent" />
</div>
</div>
) : (
<div ref={editorHostRef} className="h-full" />
)}
) : load.status === "error" ? (
<div className="flex h-full items-center justify-center p-6">
<div className="max-w-md rounded-md border border-status-danger-border bg-status-danger-bg p-4 text-center text-xs text-status-danger-text">
{load.message}
</div>
</div>
) : (
<div ref={editorHostRef} className="h-full" />
)}
</div>
</div>
</div>
</div>
Expand Down
14 changes: 14 additions & 0 deletions src/profiles/web/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@
--app-glass-panel-strong: rgb(2 6 23 / 0.88);
--app-glass-highlight: rgb(255 255 255 / 0.09);
--app-panel-shadow: 0 24px 60px -12px rgb(2 6 23 / 0.55);
/* Editor surface tokens (issue #98): a solid VSCode-style canvas for the
YAML editor, visibly distinct from the frosted modal glass. Dark values
track VSCode Dark Modern; selection is the app's indigo at low alpha. */
--app-editor-bg: #1e1e1e; /* vscode dark editor.background */
--app-editor-gutter-bg: #181818; /* slightly darker than the canvas */
--app-editor-active-line: #282828; /* vscode dark editor.lineHighlightBackground */
--app-editor-selection: rgb(99 102 241 / 0.3); /* indigo-500 @ 30% */
--app-editor-border: #34343e; /* hairline edge between canvas, gutter and card */
color-scheme: dark;
}

Expand Down Expand Up @@ -135,6 +143,12 @@
--app-glass-panel-strong: rgb(248 250 252 / 0.92);
--app-glass-highlight: rgb(255 255 255 / 0.9);
--app-panel-shadow: 0 24px 60px -18px rgb(15 23 42 / 0.28);
/* Editor surface tokens (issue #98) — light counterparts. */
--app-editor-bg: #ffffff;
--app-editor-gutter-bg: #f8fafc; /* slate-50 — slightly off the white canvas */
--app-editor-active-line: #f1f5f9; /* slate-100 */
--app-editor-selection: rgb(99 102 241 / 0.16); /* indigo-500 @ 16% */
--app-editor-border: #e2e8f0; /* slate-200 */
color-scheme: light;
}

Expand Down
Loading