diff --git a/CHANGELOG.md b/CHANGELOG.md index bf02f79..c1b05c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,12 @@ PDFs and dictation can be set up the way you like them, the Mac window is cleane ### Added - **The look of the PDF is yours to set** (Settings, "PDF look"): paper (Auto picks Letter or A4 by where the computer is), text size, typeface (Latin Modern, Palatino, Times or Helvetica), margins, an accent colour for the title and headings, numbered or plain headings, and the date under the title. The defaults are the look PDFs always had. **Edit template…** opens `lucid.sty`, the file that sets the house style, so everything else can be changed too; if it has a mistake the PDF is made with the built-in one and says so. +- **The page has its own light and dark**, apart from the app's (a small button in the top bar that goes through like the app, light and dark, and the same three choices in Settings): a dark app can show a light page, as paper, and a light app a dark one. The diagrams follow the page. - **Dictation timing** (Settings, "Dictation"): how long a paste waits before it is sent by itself: 0.5 s as before, 1 s, 2 s, or never (the words wait in the box until Enter). ### Changed +- **Three note formats, nothing too specialized.** The inference engineering and benchmark formats are gone: Lucid comes with a concept note, a system design note and a comparison, and **New page** has a small **Blank page** button instead of a big card. A page that an earlier version made in one of the two opens as a plain page, with everything in it; formats of your own are still one entry of data. - **A clean top of the window on a Mac.** The grey title bar with the name in it is gone: the page's top row takes its place, with the three round buttons at its left. Windows and Linux keep their own window frame. - **A short README**, with the details in `docs/` (installing, what Lucid does, the note formats, dictation), and, in the README, why I made Lucid: so that notes can be taken by voice, or with one hand. The website says the same. - Tables in the PDF are set ragged right with a little more room between rows, and the bars of a chart use the same greys as on screen. diff --git a/README.md b/README.md index 9c6fd30..9bb1b53 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ VoiceNative, and I may integrate the two without merging them. These are ideas, - **By voice or by keyboard.** Notes, commands (*"cross out Memcached"*, *"add a CDN in front of the web server"*, *"undo"*), diagrams and tables can all be spoken. See [dictation](docs/dictation.md) for the apps and for using Lucid with one hand. - **Made for system design, open to other fields.** The note formats are plain data: a concept note, a system design, a - comparison, an inference-maths note, a benchmark, and one more entry for anything else. [See them](docs/formats.md). + comparison, a blank page, and one more entry for anything else. [See them](docs/formats.md). - **You stay in charge.** Every change by the model is one undo step, any earlier change can be reverted on its own, and your words are never lost. - **Local and private.** No account, no cloud service. The model runs in [LM Studio](https://lmstudio.ai) on your computer. @@ -82,7 +82,7 @@ not signed, so macOS and Windows ask once before opening them; the steps, the ch ## Quick start -1. Click **New page**, or **New from a format…** to start from one of five note formats. +1. Click **New page**, or **New from a format…** to start from one of three note formats, or from a blank page. 2. Type or dictate notes in the box at the bottom: *"A cache hit costs microseconds, a miss costs a database round trip. Options are Redis, Memcached and a local LRU."* They appear on the page, structured, a few seconds later. 3. Switch to **Command** (⌘2) and talk about the page: *"Cross out Memcached, we are choosing the local LRU."* @@ -98,7 +98,7 @@ To dictate, use any app that pastes text: [Wispr Flow](https://wisprflow.ai), |---|---| | [User guide](docs/guide.html) | the whole app on one page (also in the app: Help → Lucid Guide) | | [What Lucid does](docs/features.md) | typed blocks, three modes, diagrams and tables by voice, history, export and the PDF look | -| [Note formats](docs/formats.md) | the five formats, with the PDF of a sample of each | +| [Note formats](docs/formats.md) | the three formats, with the PDF of a sample of each | | [Dictation](docs/dictation.md) | which apps to speak with, using Lucid by voice or with one hand, and a ten-minute check-list | | [For AI agents](docs/mcp.md) | the MCP tools and how to connect Claude Code | | [Installing](docs/install.md) | steps for each system, checksums, troubleshooting | diff --git a/apps/desktop/e2e/formats.spec.ts b/apps/desktop/e2e/formats.spec.ts index 05073db..80c8ef0 100644 --- a/apps/desktop/e2e/formats.spec.ts +++ b/apps/desktop/e2e/formats.spec.ts @@ -132,12 +132,12 @@ test("what the person chose is not overwritten by a late answer, and the page ca await dialog(page).getByRole("button", { name: "Suggest a format" }).click(); await expect(formatOption(page, "System design note")).toBeChecked(); // The person picks something else before the model has answered. - await formatOption(page, "Benchmark note").check({ force: true }); + await formatOption(page, "Comparison").check({ force: true }); await dialog(page).getByLabel("Title").fill("My own title"); await expect(dialog(page).getByRole("status")).toContainText("Suggested: System design note", { timeout: 30_000, }); - await expect(formatOption(page, "Benchmark note")).toBeChecked(); + await expect(formatOption(page, "Comparison")).toBeChecked(); await expect(dialog(page).getByLabel("Title")).toHaveValue("My own title"); await formatOption(page, "Blank page").check({ force: true }); @@ -159,14 +159,14 @@ test("when the model is not there the keyword pick still works, and Esc closes t await page.getByRole("button", { name: "New from a format…" }).click(); await dialog(page) .getByLabel("What will you write about?") - .fill("benchmarking qwen vs gemma tok/s on my mac"); + .fill("Kafka vs SQS for order events"); await dialog(page).getByRole("button", { name: "Suggest a format" }).click(); - await expect(formatOption(page, "Benchmark note")).toBeChecked(); - await expect(dialog(page).getByRole("status")).toContainText("Looks like a benchmark note."); + await expect(formatOption(page, "Comparison")).toBeChecked(); + await expect(dialog(page).getByRole("status")).toContainText("Looks like a comparison."); await expect(dialog(page).getByRole("status")).not.toContainText("Asking the model", { timeout: 30_000, }); - await expect(formatOption(page, "Benchmark note")).toBeChecked(); + await expect(formatOption(page, "Comparison")).toBeChecked(); await page.keyboard.press("Escape"); await expect(dialog(page)).toBeHidden(); } finally { @@ -215,40 +215,40 @@ test("dictation on a format page goes under the section it belongs to, and the m test("the format chip changes the format and the fields shown under the title, and the PDF prints only what is filled in", async () => { test.setTimeout(180_000); const { app, page, home } = await launch([ - reply("@@ section Goal and hypothesis\nDoes batching raise throughput?"), + reply("@@ section Requirements\n- Create a short link"), ]); try { await page.getByRole("button", { name: "New from a format…" }).click(); - await formatOption(page, "Benchmark note").check({ force: true }); - await dialog(page).getByLabel("Title").fill("Batching"); + await formatOption(page, "System design note").check({ force: true }); + await dialog(page).getByLabel("Title").fill("URL shortener"); await dialog(page).getByRole("button", { name: "Create page" }).click(); - await expect(headings(page)).toHaveCount(6); + await expect(headings(page)).toHaveCount(8); await page.keyboard.press("Escape"); - await page.getByRole("button", { name: /^Benchmark note/ }).click(); + await page.getByRole("button", { name: /^System design note/ }).click(); const pop = page.getByRole("dialog", { name: "Note format" }); - await pop.getByLabel("Hardware").fill("M4 Max, 48 GB"); + await pop.getByLabel("Source").fill("Study group"); await pop.getByLabel("Date").fill("2026-10-11"); await page.keyboard.press("Escape"); await expect(pop).toBeHidden(); const bar = page.getByLabel("Dictate or type your notes"); - await bar.fill("Does batching raise throughput?"); + await bar.fill("It must create a short link."); await bar.press("Enter"); - await expect(page.locator(".page-editor")).toContainText("Does batching raise throughput?", { + await expect(page.locator(".page-editor")).toContainText("Create a short link", { timeout: 30_000, }); await expect(page.getByText("Saved")).toBeVisible({ timeout: 15_000 }); - expect(pageFile(home).meta.info).toEqual({ hardware: "M4 Max, 48 GB", date: "2026-10-11" }); + expect(pageFile(home).meta.info).toEqual({ source: "Study group", date: "2026-10-11" }); const path = await autoSaveDialog(app); const text = await downloadPdf(page, path); if (text !== undefined) { - expect(text).toContain("Hardware: M4 Max, 48 GB"); + expect(text).toContain("Source: Study group"); expect(text).toContain("Date: 2026-10-11"); - expect(text).toContain("Goal and hypothesis"); - expect(text).not.toContain("Next experiments"); - expect(text).not.toContain("Observations"); + expect(text).toContain("Requirements"); + expect(text).not.toContain("Open questions"); + expect(text).not.toContain("Deep dives"); } } finally { await app.close(); @@ -288,3 +288,63 @@ test("a blank page can take a format later, and the missing sections are added i await app.close(); } }); + +test("the new page dialog offers three formats and a small Blank page button, nothing more specialized", async () => { + test.setTimeout(60_000); + const { app, page } = await launch(); + try { + await page.getByRole("button", { name: "New from a format…" }).click(); + const radios = dialog(page).getByRole("radiogroup", { name: "Format" }).getByRole("radio"); + await expect(radios).toHaveCount(4); + await expect( + dialog(page).locator(".np-formats").getByRole("radio"), + "the three formats are the cards", + ).toHaveCount(3); + await expect(formatOption(page, "Concept note")).toBeAttached(); + await expect(formatOption(page, "System design note")).toBeAttached(); + await expect(formatOption(page, "Comparison")).toBeAttached(); + await expect(dialog(page)).not.toContainText("Benchmark"); + await expect(dialog(page)).not.toContainText("Inference"); + // Blank is a small button under the cards, not a card, and it is what is chosen to start with. + await expect(formatOption(page, "Blank page")).toBeChecked(); + const blank = await dialog(page).locator(".np-blank").boundingBox(); + const card = await dialog(page).locator(".np-format").first().boundingBox(); + expect(blank?.height ?? 99).toBeLessThan(40); + expect(blank?.y ?? 0).toBeGreaterThan((card?.y ?? 0) + (card?.height ?? 0)); + } finally { + await app.close(); + } +}); + +test("a page that an earlier version made in a format that is gone opens as a plain page, with its words", async () => { + test.setTimeout(60_000); + const { app, page } = await launch(); + try { + await page.evaluate(async () => { + const file = await window.lucid.pages.create("Old benchmark"); + await window.lucid.pages.save({ + ...file, + doc: { + blocks: [ + { + id: "p1", + type: "paragraph", + props: {}, + content: [{ type: "text", text: "Tokens per second at batch four.", styles: {} }], + }, + ], + }, + meta: { ...file.meta, format: "benchmark", info: { hardware: "M4 Max" } }, + }); + }); + await page.reload(); + await page + .getByRole("navigation", { name: "Pages" }) + .getByRole("button", { name: /Old benchmark/ }) + .click(); + await expect(page.locator(".page-editor")).toContainText("Tokens per second at batch four."); + await expect(page.getByRole("button", { name: /^Blank page/ })).toBeVisible(); + } finally { + await app.close(); + } +}); diff --git a/apps/desktop/e2e/mcp.spec.ts b/apps/desktop/e2e/mcp.spec.ts index 851e109..29e448c 100644 --- a/apps/desktop/e2e/mcp.spec.ts +++ b/apps/desktop/e2e/mcp.spec.ts @@ -185,41 +185,42 @@ test("an agent can see the formats, create a page of one, and put notes under it }), ); const formats = text(await client.callTool({ name: "list_formats", arguments: {} })); - expect(formats).toContain("benchmark: Benchmark note."); - expect(formats).toContain(" - Setup:"); + expect(formats).toContain("concept-note: Concept note."); + expect(formats).toContain(" - Intuition:"); + expect(formats).not.toContain("benchmark"); const made = text( await client.callTool({ name: "create_document", - arguments: { title: "Agent benchmark", format: "benchmark" }, + arguments: { title: "Agent concept", format: "concept-note" }, }), ); - expect(made).toContain("format benchmark"); + expect(made).toContain("format concept-note"); // The page appears in the list at once; opening it shows the format. - const row = page.getByRole("navigation", { name: "Pages" }).getByText("Agent benchmark"); + const row = page.getByRole("navigation", { name: "Pages" }).getByText("Agent concept"); await expect(row).toBeVisible({ timeout: 15_000 }); await row.click(); await expect(page.locator(".page-editor h2")).toHaveCount(6); - await expect(page.getByRole("button", { name: /^Benchmark note/ })).toBeVisible(); + await expect(page.getByRole("button", { name: /^Concept note/ })).toBeVisible(); const outline = text(await client.callTool({ name: "get_outline", arguments: {} })); - expect(outline).toContain("open in Lucid, format benchmark"); - expect(outline).toContain("SECTIONS of this benchmark note"); + expect(outline).toContain("open in Lucid, format concept-note"); + expect(outline).toContain("SECTIONS of this concept note"); const added = await client.callTool({ name: "insert_notes", - arguments: { lmd: "- M4 Max, 48 GB\n- LM Studio", position: "section:Setup" }, + arguments: { lmd: "- Hash the key\n- Set the bits", position: "section:Intuition" }, }); expect(added.isError).toBe(false); const order = await page .locator(".page-editor .bn-block-content") .evaluateAll((els) => els.map((e) => e.textContent.trim()).filter((t) => t.length > 0)); expect(order.slice(0, 5)).toEqual([ - "Goal and hypothesis", - "Setup", - "M4 Max, 48 GB", - "LM Studio", - "Method", + "Definition", + "Intuition", + "Hash the key", + "Set the bits", + "How it works", ]); const bad = await client.callTool({ diff --git a/apps/desktop/e2e/page-look.spec.ts b/apps/desktop/e2e/page-look.spec.ts new file mode 100644 index 0000000..5467952 --- /dev/null +++ b/apps/desktop/e2e/page-look.spec.ts @@ -0,0 +1,110 @@ +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { Block } from "@lucid/core"; +import { registry } from "@lucid/kit"; +import { _electron as electron, expect, test, type Page } from "@playwright/test"; +import { maximizeWindow } from "./helpers"; + +/** + * The page (the document with its diagrams and tables) has a light and dark of its own, apart from the app's: a dark app can + * show a light page, as paper, and a light app a dark page. The choice is kept, and "like the app" follows the app again. + */ +const luminance = (value: string): number => { + const v = value.trim(); + const hex = /^#([0-9a-f]{6})$/i.exec(v); + const [r = 0, g = 0, b = 0] = hex + ? [0, 2, 4].map((i) => Number.parseInt((hex[1] ?? "").slice(i, i + 2), 16)) + : (v.match(/\d+(\.\d+)?/g) ?? []).map(Number); + return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255; +}; +const colour = (page: Page, selector: string, property: string) => + page + .locator(selector) + .first() + .evaluate((el, p) => getComputedStyle(el).getPropertyValue(p), property); + +test("the page has its own light and dark, apart from the app's, and it is kept", async () => { + test.setTimeout(90_000); + const app = await electron.launch({ + args: [join(__dirname, "../out/main/index.js")], + env: { ...process.env, LUCID_HOME: mkdtempSync(join(tmpdir(), "lucid-home-")) }, + }); + try { + const page = await app.firstWindow(); + await maximizeWindow(app, page); // this also makes the app light + const diagram = registry.get("diagram").fixtures[0] as Block; + await page.evaluate( + async (blocks: Block[]) => { + const file = await window.lucid.pages.create("Look"); + await window.lucid.pages.save({ ...file, doc: { blocks } }); + }, + [diagram], + ); + await page.reload(); + await page.emulateMedia({ colorScheme: "light" }); + await page + .getByRole("navigation", { name: "Pages" }) + .getByRole("button", { name: /Look/ }) + .click(); + await expect(page.locator(".page-editor svg g.node").first()).toBeVisible({ timeout: 20_000 }); + + const toggle = page.getByRole("button", { name: /^Look of the page:/ }); + const nodeFill = () => + colour(page, ".page-editor svg g.node rect, .page-editor svg g.node path", "fill"); + const oneLine = async () => { + const bar = await page.locator(".topbar").boundingBox(); + expect(bar?.height ?? 999, "the top bar stays on one line").toBeLessThan(75); + }; + + // To begin with the page is like the app: light in a light app, and nothing is marked. + await expect(toggle).toHaveAccessibleName(/like the app\. Click for light/); + expect(luminance(await colour(page, ".editor-scroll", "--bg"))).toBeGreaterThan(0.9); + const lightFill = luminance(await nodeFill()); + await expect(page.locator(".editor-scroll[data-page-look]")).toHaveCount(0); + await oneLine(); + + // One click: a light page, whatever the app is. Another: a dark page in the light app, its diagram dark too, and the + // sidebar and the top bar stay light. + await toggle.click(); + await expect(toggle).toHaveAccessibleName(/: light\. Click for dark/); + await toggle.click(); + await expect(toggle).toHaveAccessibleName(/: dark\. Click for like the app/); + await expect(page.locator(".editor-scroll[data-page-look='dark']")).toHaveCount(1); + expect(luminance(await colour(page, ".editor-scroll", "--bg"))).toBeLessThan(0.2); + expect(luminance(await colour(page, ".sidebar", "background-color"))).toBeGreaterThan(0.8); + expect(luminance(await colour(page, ".topbar", "background-color"))).toBeGreaterThan(0.9); + await expect.poll(async () => luminance(await nodeFill())).toBeLessThan(lightFill); + await expect(page.locator("html")).toHaveAttribute("data-theme", "light"); + await oneLine(); + + // It is kept. + await page.reload(); + await page + .getByRole("navigation", { name: "Pages" }) + .getByRole("button", { name: /Look/ }) + .click(); + await expect(page.locator(".editor-scroll[data-page-look='dark']")).toHaveCount(1); + + // A light page in a dark app (paper), chosen in Settings: the app follows the system, here made dark. + await page.emulateMedia({ colorScheme: "dark" }); + await expect(page.locator("html")).toHaveAttribute("data-theme", "dark"); + await page.keyboard.press("Meta+,"); + const row = page + .getByRole("dialog", { name: "Settings" }) + .getByRole("radiogroup", { name: "Page", exact: true }); + await row.getByRole("radio", { name: "Light" }).click(); + await page.keyboard.press("Escape"); + await expect(page.locator(".editor-scroll[data-page-look='light']")).toHaveCount(1); + expect(luminance(await colour(page, ".editor-scroll", "--bg"))).toBeGreaterThan(0.9); + expect(luminance(await colour(page, ".sidebar", "background-color"))).toBeLessThan(0.3); + + // Like the app again. + await page.keyboard.press("Meta+,"); + await row.getByRole("radio", { name: "Like the app" }).click(); + await page.keyboard.press("Escape"); + await expect(page.locator(".editor-scroll[data-page-look]")).toHaveCount(0); + } finally { + await app.close(); + } +}); diff --git a/apps/desktop/electron/appBackend.test.ts b/apps/desktop/electron/appBackend.test.ts index 391ef5f..b6eaa83 100644 --- a/apps/desktop/electron/appBackend.test.ts +++ b/apps/desktop/electron/appBackend.test.ts @@ -204,14 +204,14 @@ describe("AppBackend", () => { const { backend, files, a, compiled } = setup(); files.set(a.id, { ...a, - meta: { vocabulary: [], format: "benchmark", info: { model: "Q" } }, + meta: { vocabulary: [], format: "comparison", info: { source: "Q" } }, }); await backend.exportPdf(a.id); expect(compiled[0]).toMatchObject({ pageId: `agent-${a.id}`, title: "Closed page", - format: { id: "benchmark" }, - info: { model: "Q" }, + format: { id: "comparison" }, + info: { source: "Q" }, }); }); diff --git a/apps/desktop/electron/contrast.test.ts b/apps/desktop/electron/contrast.test.ts index b45fbdd..b14f85a 100644 --- a/apps/desktop/electron/contrast.test.ts +++ b/apps/desktop/electron/contrast.test.ts @@ -11,8 +11,9 @@ import { colourTokens, contrastRatio as ratio } from "./wcag"; const css = readFileSync(join(__dirname, "../src/styles.css"), "utf8"); describe.each([ - ["light", ":root"], - ["dark", ':root[data-theme="dark"]'], + // The colour tokens are shared by the app and by a page that has a look of its own (data-page-look). + ["light", '[data-page-look="light"]'], + ["dark", '[data-page-look="dark"]'], ])("the %s theme", (_name, selector) => { const t = colourTokens(css, selector); const get = (k: string): string => { diff --git a/apps/desktop/electron/pdf.test.ts b/apps/desktop/electron/pdf.test.ts index 0d9b062..c8483e0 100644 --- a/apps/desktop/electron/pdf.test.ts +++ b/apps/desktop/electron/pdf.test.ts @@ -400,25 +400,25 @@ shellScripts("PdfCompiler with stand-in tools", () => { title: "Q", doc: { blocks: [ - heading("a", "Goal and hypothesis"), + heading("a", "Context"), { id: "p", type: "paragraph", props: {}, - content: [{ type: "text", text: "Does it?", styles: {} }], + content: [{ type: "text", text: "Which queue?", styles: {} }], }, - heading("b", "Setup"), + heading("b", "Options"), ], }, figures: {}, - format: getFormat("benchmark"), - info: { hardware: "M4 Max", source: "ignored" }, + format: getFormat("comparison"), + info: { date: "2026-10-12", hardware: "ignored" }, }); const tex = await readFile(join(workRoot, "page-7", "main.tex"), "utf8"); - expect(tex).toContain("\\lucidmeta{Hardware}{M4 Max}"); + expect(tex).toContain("\\lucidmeta{Date}{2026-10-12}"); expect(tex).not.toContain("ignored"); - expect(tex).toContain("{Goal and hypothesis}"); - expect(tex).not.toContain("{Setup}"); + expect(tex).toContain("{Context}"); + expect(tex).not.toContain("{Options}"); }); it("writes main.tex, the style and one work folder per page", async () => { diff --git a/apps/desktop/src/editor/PageEditor.tsx b/apps/desktop/src/editor/PageEditor.tsx index 233e921..063fa68 100644 --- a/apps/desktop/src/editor/PageEditor.tsx +++ b/apps/desktop/src/editor/PageEditor.tsx @@ -1,5 +1,5 @@ import { filterSuggestionItems, insertOrUpdateBlockForSlashMenu } from "@blocknote/core"; -import { useResolvedTheme } from "../theme/appearance"; +import { useResolvedPageLook } from "../theme/appearance"; import "@blocknote/core/fonts/inter.css"; import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/mantine/style.css"; @@ -108,7 +108,7 @@ export function PageEditor({ initial, onChange, onReady, editable = true }: Prop const [live, setLive] = useState(initial); const rootRef = useRef(null); const lastRevert = useRef(undefined); - const look = useResolvedTheme(); + const look = useResolvedPageLook(); const first = useMemo(() => toBn(initial), [initial]); const editor = useCreateBlockNote( { diff --git a/apps/desktop/src/editor/schema.tsx b/apps/desktop/src/editor/schema.tsx index 0592a8b..e5f027e 100644 --- a/apps/desktop/src/editor/schema.tsx +++ b/apps/desktop/src/editor/schema.tsx @@ -19,7 +19,7 @@ import { useEffect, useRef, useState, type CSSProperties } from "react"; import { minWidthFor, type Layout } from "../preview/fit"; import { renderFitted } from "../preview/mermaid"; import { useWidth } from "../preview/useWidth"; -import { useResolvedTheme } from "../theme/appearance"; +import { useResolvedPageLook } from "../theme/appearance"; export const CALLOUT_KINDS = [ "note", @@ -117,7 +117,7 @@ function Figure({ }) { const [layout, setLayout] = useState(); const [failed, setFailed] = useState(false); - const look = useResolvedTheme(); + const look = useResolvedPageLook(); const figure = useRef(null); const width = useWidth(figure); useEffect(() => { diff --git a/apps/desktop/src/guided/DiagramWorkbench.tsx b/apps/desktop/src/guided/DiagramWorkbench.tsx index bc9c56e..248b1d2 100644 --- a/apps/desktop/src/guided/DiagramWorkbench.tsx +++ b/apps/desktop/src/guided/DiagramWorkbench.tsx @@ -3,7 +3,7 @@ import { useEffect, useMemo, useRef, useState, type CSSProperties } from "react" import { minWidthFor, type Layout } from "../preview/fit"; import { renderFitted } from "../preview/mermaid"; import { useWidth } from "../preview/useWidth"; -import { useResolvedTheme } from "../theme/appearance"; +import { useResolvedPageLook } from "../theme/appearance"; import { isDiagramDraft, type BuilderState } from "./useBuilder"; import { WorkbenchFrame } from "./WorkbenchFrame"; @@ -44,7 +44,7 @@ const PHASES: { phase: Phase; label: string; hint: string }[] = [ function Canvas({ draft }: { draft: DiagramDraft }) { const [layout, setLayout] = useState(); const [failed, setFailed] = useState(false); - const look = useResolvedTheme(); + const look = useResolvedPageLook(); const fit = useRef(null); const width = useWidth(fit); const source = useMemo( diff --git a/apps/desktop/src/pages/NewPageDialog.tsx b/apps/desktop/src/pages/NewPageDialog.tsx index ad0b48e..487aab4 100644 --- a/apps/desktop/src/pages/NewPageDialog.tsx +++ b/apps/desktop/src/pages/NewPageDialog.tsx @@ -95,7 +95,7 @@ export function NewPageDialog({ onCreate, onClose }: Props) { ref={field} rows={2} value={about} - placeholder="notes on how paged attention saves KV memory…" + placeholder="notes on how consistent hashing works…" onChange={(e) => { setAbout(e.target.value); }} @@ -124,33 +124,46 @@ export function NewPageDialog({ onCreate, onClose }: Props) { -
- {[ - { id: BLANK, name: "Blank page", description: "Start empty and write what you like." }, - ...FORMATS, - ].map((f) => ( -