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
25 changes: 17 additions & 8 deletions apps/desktop/e2e/appearance.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import {
} from "@playwright/test";
import { autoSaveDialog, downloadPdf, maximizeWindow } from "./helpers";

/** Neutral light and dark surfaces with semantic accents, set in monospace: values are read from the page's own styles. */
const LIGHT = { bg: "rgb(255, 255, 255)", ink: "rgb(44, 44, 43)" };
/** Crisp light and dark surfaces with semantic signal colours, set in monospace. */
const LIGHT = { bg: "rgb(251, 252, 255)", ink: "rgb(17, 19, 24)" };
const DARK = {
bg: "rgb(25, 25, 25)",
ink: "rgb(240, 239, 237)",
structure: "rgb(140, 180, 255)",
structureInk: "rgb(25, 25, 25)",
bg: "rgb(17, 19, 26)",
ink: "rgb(247, 248, 255)",
structure: "rgb(125, 162, 255)",
structureInk: "rgb(12, 16, 32)",
};

async function launch(): Promise<{ app: ElectronApplication; page: Page }> {
Expand Down Expand Up @@ -57,6 +57,7 @@ test("it is light by default, the sidebar's moon switches to dark, and the choic
expect(await style(page, "body", "background-color")).toBe(LIGHT.bg);
expect(await style(page, "body", "color")).toBe(LIGHT.ink);
expect(await style(page, "body", "font-family")).toContain("iA Writer Mono");
await expect(page.getByRole("complementary", { name: "History" })).toHaveCount(0);
await page.screenshot({ path: join(__dirname, "../test-results/appearance-light.png") });

await page.getByRole("button", { name: "Switch to dark mode" }).click();
Expand Down Expand Up @@ -84,11 +85,11 @@ test("the page's text and its diagram follow the theme at once, and the PDF does
try {
// The page body is in the app's type, not BlockNote's own.
expect(await style(page, ".page-editor .bn-editor", "font-family")).toContain("iA Writer Mono");
expect(await labelFill(page)).toBe(LIGHT.bg);
expect(await labelFill(page)).toBe("rgb(255, 255, 255)");

await page.getByRole("button", { name: "Switch to dark mode" }).click();
// The drawing is made again in the dark look (no reload): the label behind the words is the page's colour.
await expect.poll(() => labelFill(page)).toBe(DARK.bg);
await expect.poll(() => labelFill(page)).toBe("rgb(25, 25, 25)");
expect(await style(page, ".page-editor .bn-editor", "color")).toBe(DARK.ink);

// A PDF made in the dark theme is the same white page with the house style.
Expand All @@ -108,6 +109,11 @@ test("Settings: Theme follows the system when asked to, and Type changes the fac
const dialog = page.getByRole("dialog", { name: "Settings" });
const theme_ = dialog.getByRole("radiogroup", { name: "Theme" });
const type = dialog.getByRole("radiogroup", { name: "Type" });
const colour = dialog.getByRole("radiogroup", { name: "Colour" });
await expect(colour.getByRole("radio", { name: /Lucid/ })).toHaveAttribute(
"aria-checked",
"true",
);
await expect(theme_.getByRole("radio", { name: "System" })).toHaveAttribute(
"aria-checked",
"true",
Expand All @@ -133,6 +139,9 @@ test("Settings: Theme follows the system when asked to, and Type changes the fac
expect(await style(page, ".page-editor .bn-editor", "font-family")).toContain("ui-serif");
await type.getByRole("radio", { name: "Mono" }).click();
expect(await style(page, ".page-editor .bn-editor", "font-family")).toContain("iA Writer Mono");
await colour.getByRole("radio", { name: /Solar/ }).click();
expect(await page.evaluate(() => document.documentElement.dataset["palette"])).toBe("solar");
await colour.getByRole("radio", { name: /Lucid/ }).click();
await page.screenshot({ path: join(__dirname, "../test-results/appearance-settings.png") });
} finally {
await app.close();
Expand Down
5 changes: 2 additions & 3 deletions apps/desktop/e2e/editing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,14 @@ test("one view that scales: the page, its text and the input box grow with the w
};
});

// The maximized window itself (what a person gets): everything fits, and a wide one shows the history by itself.
// The maximized window itself (what a person gets): everything fits, and the page stays focused until History is asked for.
const maximized = await measure();
expect(maximized.overflow, "maximized: no horizontal scrolling").toBe(false);
expect(maximized.barBottom, "maximized: the input box is on screen").toBeLessThanOrEqual(
maximized.innerHeight,
);
expect(maximized.barRight).toBeLessThanOrEqual(maximized.innerWidth);
if (maximized.innerWidth >= 1500)
await expect(page.getByRole("complementary", { name: "History" })).toBeVisible();
await expect(page.getByRole("complementary", { name: "History" })).toHaveCount(0);
await page.screenshot({ path: join(__dirname, "../test-results/app-maximized.png") });

const sizes = [
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/e2e/formats.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ test("a new page from a format: the model's choice and title, and the page start
// Blank is the default.
await expect(formatOption(page, "Blank page")).toBeChecked();

// It is laid out as a dialog (a white card, the field and the six formats readable), not squeezed into a row.
// It is laid out as a paper card (the field and the formats readable), not squeezed into a row.
const look = await dialog(page).evaluate((el) => {
const box = el.getBoundingClientRect();
const about = el.querySelector("textarea")?.getBoundingClientRect();
Expand All @@ -81,7 +81,7 @@ test("a new page from a format: the model's choice and title, and the page start
sameRow: first && second ? Math.abs(first.top - second.top) < 4 : false,
};
});
expect(look.background).toBe("rgb(255, 255, 255)");
expect(look.background).toBe("rgb(251, 252, 255)");
expect(look.width).toBeGreaterThan(500);
expect(look.aboutWidth).toBeGreaterThan(400);
expect(look.formatWidth).toBeGreaterThan(180);
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/e2e/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export async function maximizeWindow(
return page.evaluate(() => ({ width: window.innerWidth, height: window.innerHeight }));
}

/** Opens the History panel unless it is open already: a wide (maximized) window opens it by itself. */
/** Opens the History panel unless it is open already. */
export async function showHistory(page: Page): Promise<void> {
const button = page.getByRole("button", { name: /^History/ });
if ((await button.getAttribute("aria-pressed")) !== "true") await button.click();
Expand Down
7 changes: 7 additions & 0 deletions apps/desktop/e2e/onboarding.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ test("a first run leads into a real first-success task", async () => {
await expect(
window.getByRole("heading", { name: "Speak or type the messy version." }),
).toBeVisible();
const onboarding = window.getByRole("dialog");
const frame = await onboarding.boundingBox();
expect(frame?.width ?? 0).toBeGreaterThan((frame?.height ?? 1) * 1.5);
expect(
await onboarding.evaluate((element) => element.scrollWidth <= element.clientWidth),
"the wide onboarding frame does not clip sideways",
).toBe(true);
await window.getByRole("button", { name: "Set up Lucid" }).click();

await expect(window.getByRole("heading", { name: "Connect an AI provider" })).toBeVisible();
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/dictation/DictationBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ export function DictationBar({
{status.kind === "done" && (
<>
<span className="tick" aria-hidden="true">
✓
<Icon name="check" size={14} />
</span>
<span>
{status.summary}.{status.notice ? ` ${status.notice}` : ""}
Expand Down
44 changes: 28 additions & 16 deletions apps/desktop/src/onboarding/FirstRun.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
import { ProviderSettings } from "../settings/ProviderSettings";
import { saveSettings } from "../settings/settingsStore";
import { BrandMark } from "../ui/BrandMark";
import { Icon } from "../ui/Icon";
import { decideProvider, loadActivationProgress, rememberSetupStep } from "./activation";

interface Props {
Expand Down Expand Up @@ -117,6 +118,7 @@ export function FirstRun({ settings, onSettingsChange, onFinish }: Props) {
role="dialog"
aria-modal="true"
aria-labelledby="onboarding-title"
data-step={step}
>
<aside className="onboarding-rail" aria-label="Setup progress">
<div className="onboarding-brand">
Expand All @@ -126,7 +128,7 @@ export function FirstRun({ settings, onSettingsChange, onFinish }: Props) {
<ol>
{STEP_NAMES.map((name, index) => (
<li key={name} data-current={step === index} data-done={step > index}>
<span>{step > index ? "✓" : index + 1}</span>
<span>{step > index ? <Icon name="check" size={15} /> : index + 1}</span>
{name}
</li>
))}
Expand All @@ -145,29 +147,39 @@ export function FirstRun({ settings, onSettingsChange, onFinish }: Props) {
<div className="onboarding-content">
{step === 0 && (
<div className="onboarding-welcome">
<div className="onboarding-welcome-copy">
<p className="onboarding-kicker">A quieter way to think</p>
<h1 id="onboarding-title" ref={heading} tabIndex={-1}>
Speak or type the messy version.
</h1>
<p>
Lucid turns it into a clear page, then helps you revise it, build diagrams and
tables, and export the result.
</p>
<div className="onboarding-promise">
<span>
<Icon name="check" size={18} /> Your pages stay on this computer.
</span>
<span>
<Icon name="check" size={18} /> AI is optional and always chosen by you.
</span>
</div>
</div>
<div className="thoughts-to-page" aria-hidden="true">
<span className="thought thought-one">rough idea</span>
<span className="thought thought-two">and then…</span>
<span className="thought thought-three">connect these</span>
<div className="thought-cloud">
<span className="thought thought-one">rough idea</span>
<span className="thought thought-two">and then…</span>
<span className="thought thought-three">connect these</span>
</div>
<div className="thought-line" />
<div className="ordered-page">
<span>Structured by Lucid</span>
<i />
<i />
<i />
<b />
</div>
</div>
<p className="onboarding-kicker">A quieter way to think</p>
<h1 id="onboarding-title" ref={heading} tabIndex={-1}>
Speak or type the messy version.
</h1>
<p>
Lucid turns it into a clear page, then helps you revise it, build diagrams and
tables, and export the result.
</p>
<div className="onboarding-promise">
<span>Your pages stay on this computer.</span>
<span>AI is optional and always chosen by you.</span>
<strong>Messy thought → clear page</strong>
</div>
</div>
)}
Expand Down
5 changes: 4 additions & 1 deletion apps/desktop/src/onboarding/FirstSuccessGuide.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type ActivationMilestone,
type ActivationProgress,
} from "./activation";
import { Icon } from "../ui/Icon";

interface Props {
progress: ActivationProgress;
Expand Down Expand Up @@ -91,7 +92,9 @@ export function FirstSuccessGuide({ progress, onCreatePage, onPause, onFinish }:
<ul className="first-success-checklist">
{checks.map((item) => (
<li key={item.milestone} data-done={done(progress, item.milestone)}>
<span aria-hidden="true">{done(progress, item.milestone) ? "✓" : "○"}</span>
<span aria-hidden="true" data-done={done(progress, item.milestone)}>
{done(progress, item.milestone) && <Icon name="check" size={14} />}
</span>
{item.label}
</li>
))}
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/pages/Workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function sideOpenAtStart(): boolean {
} catch {
// Remembering is a convenience only.
}
return window.innerWidth >= 1500;
return false;
}

/** The text of a heading block, lower-cased, to tell whether a section is already there. */
Expand Down
40 changes: 37 additions & 3 deletions apps/desktop/src/settings/SettingsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import {
} from "../../electron/ipc/types";
import {
setFont,
setPalette,
setPageLook,
setTheme,
useAppearance,
type PaletteChoice,
type ThemeChoice,
type Typeface,
} from "../theme/appearance";
Expand All @@ -31,6 +33,11 @@ const FONT_OPTIONS: { value: Typeface; label: string }[] = [
{ value: "sans", label: "Sans" },
{ value: "serif", label: "Serif" },
];
const PALETTE_OPTIONS: { value: PaletteChoice; label: string; detail: string }[] = [
{ value: "lucid", label: "Lucid", detail: "Cobalt · violet · green" },
{ value: "solar", label: "Solar", detail: "Blue · coral · amber" },
{ value: "neutral", label: "Neutral", detail: "Quiet monochrome" },
];

/** A row of one-of-several choices in the look of the Appearance rows. */
function Choice<T extends string | number | boolean>(props: {
Expand Down Expand Up @@ -199,6 +206,33 @@ export function SettingsDialog({ settings, onClose, onOpenOnboarding }: Props) {

<fieldset className="appearance">
<legend>Appearance</legend>
<div className="setting-row palette-setting">
<span id="palette-label">Colour</span>
<div className="palette-options" role="radiogroup" aria-labelledby="palette-label">
{PALETTE_OPTIONS.map((option) => (
<button
key={option.value}
type="button"
className="palette-option"
role="radio"
aria-checked={appearance.palette === option.value}
data-palette-preview={option.value}
onClick={() => {
setPalette(option.value);
}}
>
<span className="palette-swatches" aria-hidden="true">
<i />
<i />
<i />
<i />
</span>
<strong>{option.label}</strong>
<small>{option.detail}</small>
</button>
))}
</div>
</div>
<div className="setting-row">
<span id="theme-label">Theme</span>
<div className="segmented" role="radiogroup" aria-labelledby="theme-label">
Expand Down Expand Up @@ -281,9 +315,9 @@ export function SettingsDialog({ settings, onClose, onOpenOnboarding }: Props) {
</div>
</div>
<small>
Applied at once. Mono is the default. The page can be light or dark whatever the app
is; the look of PDFs is set below. Block numbers (B1, B2…) sit in the page margin; say
“in block 3, …” to talk about one.
Applied at once. Lucid colour and Mono are the defaults. The page can be light or dark
whatever the app is; the look of PDFs is set below. Block numbers (B1, B2…) sit in the
page margin; say “in block 3, …” to talk about one.
</small>
</fieldset>

Expand Down
Loading
Loading