From 74419f7ea8436aad9565b099ee4c787dc470a371 Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Sat, 28 Feb 2026 17:33:35 -0600 Subject: [PATCH] refactor: clean up theming --- crates/store/src/file_utils.rs | 7 + crates/store/src/lib.rs | 31 ++-- docs/nlp.md | 2 +- docs/pdf.md | 13 +- docs/spec.md | 38 ----- src/editor/themes/oxocarbon-dark.ts | 179 ++--------------------- src/editor/themes/oxocarbon-light.ts | 178 ++--------------------- src/editor/themes/shared.ts | 206 +++++++++++++++++++++++++++ 8 files changed, 266 insertions(+), 388 deletions(-) delete mode 100644 docs/spec.md create mode 100644 src/editor/themes/shared.ts diff --git a/crates/store/src/file_utils.rs b/crates/store/src/file_utils.rs index 64d6e67..ae0af21 100644 --- a/crates/store/src/file_utils.rs +++ b/crates/store/src/file_utils.rs @@ -41,3 +41,10 @@ pub fn collect_file_paths_recursive(dir: &Path, files: &mut Vec) -> Res Ok(()) } + +pub fn fallback_title_from_path(rel_path: &Path) -> Option { + rel_path + .file_stem() + .and_then(|value| value.to_str()) + .map(|value| value.to_string()) +} diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index 90af1ec..3b4ead1 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -17,9 +17,8 @@ mod file_utils; mod settings; mod text_utils; -pub use settings::StyleCheckSettings; -pub use settings::UiLayoutSettings; -pub use settings::{CaptureDocRef, CaptureMode, FocusDimmingMode, GlobalCaptureSettings, SessionState, SessionTab}; +pub use settings::{CaptureDocRef, CaptureMode, FocusDimmingMode, SessionState, SessionTab}; +pub use settings::{GlobalCaptureSettings, StyleCheckSettings, UiLayoutSettings}; const UI_LAYOUT_SETTINGS_KEY: &str = "ui_layout"; const STYLE_CHECK_SETTINGS_KEY: &str = "style_check"; @@ -40,6 +39,15 @@ pub struct StyleCheckPattern { } /// Manages the SQLite database for the application +/// +/// TODO: Break this impl up into smaller "Repositories" +/// 1. Location +/// 2. Document +/// 3. Directory +/// 4. File Operations +/// 5. Settings +/// 6. Search/FTS +/// 7. Session pub struct Store { conn: Arc>, } @@ -55,24 +63,19 @@ impl Store { Self::default_app_dir().map(|app_dir| app_dir.join("app.db")) } - fn fallback_title_from_path(rel_path: &Path) -> Option { - rel_path - .file_stem() - .and_then(|value| value.to_str()) - .map(|value| value.to_string()) - } - fn derive_text_metadata(text: &str, rel_path: &Path) -> (Option, usize) { let engine = MarkdownEngine::new(); match engine.metadata(text, MarkdownProfile::Extended) { Ok(metadata) => ( - metadata.title.or_else(|| Self::fallback_title_from_path(rel_path)), + metadata + .title + .or_else(|| file_utils::fallback_title_from_path(rel_path)), metadata.word_count, ), Err(error) => { log::warn!("Failed to derive markdown metadata for {:?}: {}", rel_path, error); ( - Self::fallback_title_from_path(rel_path), + file_utils::fallback_title_from_path(rel_path), text.split_whitespace().filter(|segment| !segment.is_empty()).count(), ) } @@ -999,7 +1002,7 @@ impl Store { let (derived_title, derived_word_count) = Self::derive_text_metadata(content, &rel_path); (derived_title, Some(derived_word_count)) } - None => (Self::fallback_title_from_path(&rel_path), None), + None => (file_utils::fallback_title_from_path(&rel_path), None), }; let content_hash = text_content.as_ref().map(|content| text_utils::hash_text(content)); @@ -1656,7 +1659,7 @@ impl Store { let title = meta .title .clone() - .or_else(|| Self::fallback_title_from_path(&doc_id.rel_path)) + .or_else(|| file_utils::fallback_title_from_path(&doc_id.rel_path)) .unwrap_or_else(|| "Untitled".to_string()); self.upsert_fts_entry(doc_id, &title, text) } diff --git a/docs/nlp.md b/docs/nlp.md index 7ffb829..f465406 100644 --- a/docs/nlp.md +++ b/docs/nlp.md @@ -1,6 +1,6 @@ --- title: "Writing and NLP" -last_updated: 2026-02-26 +last_updated: 2026-02-28 --- ## Current Features diff --git a/docs/pdf.md b/docs/pdf.md index ddd7819..7ca3afe 100644 --- a/docs/pdf.md +++ b/docs/pdf.md @@ -1,6 +1,6 @@ --- title: "PDF Exporting" -last_updated: 2026-02-26 +last_updated: 2026-02-28 --- ## Pipeline @@ -12,9 +12,10 @@ PDF export is split into two stages: ## Frontend Flow -- UI entry: toolbar export action + `PdfExportDialog`. +- UI entry: toolbar export action + `ExportDialog` (unified multi-format dialog supporting PDF, DOCX, and plaintext). - Runtime export hook: `src/hooks/usePdfExport.tsx`. -- Document renderer: PDF components under `src/components/pdf/`. +- Document renderer: PDF components under `src/components/export/`. +- PDF preview: `src/components/export/preview/PdfPreview.tsx` (uses `pdfjs-dist` with zoom, fit mode, and page navigation). ## Backend Flow @@ -23,12 +24,12 @@ PDF export is split into two stages: ## Export Options -The dialog supports page/layout options (size, orientation, font/margins/header/footer). +The dialog supports page/layout options (size, orientation, font size, line height, margins, header/footer). -PDF export state is tracked in Zustand (`isExportingPdf`, `pdfExportError`). +PDF export state is tracked in a dedicated Zustand store slice (`isExportingPdf`, `pdfExportError`). ## Failure Handling - User cancel exits cleanly without writes. - Export failures are surfaced through export error state. -- Font fallback handling is implemented in `src/pdf/` to improve cross-platform reliability. +- Font fallback handling is implemented in `src/pdf/fonts.ts` — custom font fetch failures automatically fall back to builtin fonts. diff --git a/docs/spec.md b/docs/spec.md deleted file mode 100644 index 19f9c17..0000000 --- a/docs/spec.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: "Writer Product Spec" -last_updated: 2026-02-26 ---- - -## Product Intent - -Writer is a desktop markdown writing environment optimized for local-first authoring with robust folder-based workspaces. - -## Core Principles - -- Local-first files: user-selected folders are authoritative. -- Fast feedback: live preview and lightweight writing assistance. -- Defensive behavior: graceful handling for missing paths, command failures, and partial IO issues. -- Predictable controls: explicit layout/view state through centralized store selectors. - -## Current Capabilities - -- Location library (add/remove/select folders) -- Document listing/open/save flows -- Document tabs + active tab model -- Editor-only, preview-only, and split layouts -- Focus mode panel -- Search command integration -- Global quick capture route/window -- PDF export with configurable output options - -## Non-Goals (Current) - -- Cloud-synced proprietary storage backend -- AI-generated rewriting built into save path -- Plugin marketplace/runtime - -## Quality Bar - -- Keep frontend/backend contracts explicit through `src/ports/`. -- Keep data-loss risk minimized via backend save/indexing behavior. -- Keep UI tests deterministic and close to user behavior. diff --git a/src/editor/themes/oxocarbon-dark.ts b/src/editor/themes/oxocarbon-dark.ts index b1e62b6..51394be 100644 --- a/src/editor/themes/oxocarbon-dark.ts +++ b/src/editor/themes/oxocarbon-dark.ts @@ -7,11 +7,9 @@ * @see https://carbondesignsystem.com/guidelines/color/overview/ */ -import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; -import { EditorView } from "@codemirror/view"; -import { tags } from "@lezer/highlight"; +import { type Colors, createTheme, type ThemeOptions } from "./shared"; -const colors = { +const colors: Colors = { background: "#161616", surface: "#262626", text: "#f2f4f8", @@ -31,165 +29,16 @@ const colors = { black: "#000000", }; -export const oxocarbonDarkTheme = EditorView.theme({ - "&": { - backgroundColor: colors.background, - color: colors.text, - fontSize: "var(--editor-font-size, 14px)", - fontFamily: "var(--editor-font-family, \"IBM Plex Mono\", \"SF Mono\", Monaco, monospace)", - }, - ".cm-scroller": { fontFamily: "var(--editor-font-family, \"IBM Plex Mono\", \"SF Mono\", Monaco, monospace)" }, - ".cm-content": { - fontFamily: "var(--editor-font-family, \"IBM Plex Mono\", \"SF Mono\", Monaco, monospace)", - caretColor: colors.cyan, - padding: "16px 0", - }, - ".cm-cursor": { borderLeftColor: colors.cyan, borderLeftWidth: "2px" }, - "&.cm-focused .cm-cursor": { borderLeftColor: colors.cyan }, - "&.cm-focused .cm-selectionBackground": { backgroundColor: "rgba(51, 177, 255, 0.3)" }, - ".cm-selectionBackground": { backgroundColor: "rgba(51, 177, 255, 0.2)" }, - ".cm-activeLine": { backgroundColor: "rgba(57, 57, 57, 0.5)" }, - ".cm-activeLineGutter": { backgroundColor: colors.surface }, - ".cm-gutters": { - backgroundColor: colors.background, - color: colors.textMuted, - borderRight: `1px solid ${colors.border}`, - paddingRight: "8px", - }, - ".cm-lineNumbers .cm-gutterElement": { padding: "0 8px", minWidth: "32px" }, - ".cm-foldPlaceholder": { - backgroundColor: colors.surface, - border: `1px solid ${colors.border}`, - borderRadius: "3px", - color: colors.textMuted, - padding: "0 4px", - }, - ".cm-foldGutter .cm-gutterElement": { color: colors.textMuted, cursor: "pointer" }, - ".cm-tooltip": { - backgroundColor: colors.surface, - border: `1px solid ${colors.border}`, - borderRadius: "4px", - boxShadow: "0 4px 12px rgba(0, 0, 0, 0.5)", - }, - ".cm-tooltip-autocomplete": { backgroundColor: colors.surface }, - ".cm-tooltip-autocomplete ul li[aria-selected]": { backgroundColor: colors.blue, color: colors.white }, - ".cm-completionLabel": { color: colors.text }, - ".cm-completionDetail": { color: colors.textMuted }, - ".cm-completionIcon": { color: colors.cyan }, - ".cm-searchMatch": { backgroundColor: "rgba(255, 111, 0, 0.3)", outline: `1px solid ${colors.yellow}` }, - ".cm-searchMatch.cm-searchMatch-selected": { backgroundColor: "rgba(255, 111, 0, 0.5)" }, - ".cm-panel": { backgroundColor: colors.surface, borderTop: `1px solid ${colors.border}` }, - ".cm-panel button": { - backgroundColor: colors.blue, - color: colors.white, - border: "none", - borderRadius: "3px", - padding: "4px 12px", - cursor: "pointer", - }, - ".cm-panel input": { - backgroundColor: colors.background, - color: colors.text, - border: `1px solid ${colors.border}`, - borderRadius: "3px", - padding: "4px 8px", - }, - ".cm-placeholder": { color: colors.textMuted }, - ".cm-highlight": { backgroundColor: "rgba(255, 111, 0, 0.3)" }, - ".cm-specialChar": { color: colors.red }, - "& .cm-scroller::-webkit-scrollbar": { width: "10px", height: "10px" }, - "& .cm-scroller::-webkit-scrollbar-track": { background: colors.background }, - "& .cm-scroller::-webkit-scrollbar-thumb": { background: colors.border, borderRadius: "5px" }, - "& .cm-scroller::-webkit-scrollbar-thumb:hover": { background: colors.textDisabled }, - "& .cm-scroller": { scrollbarColor: `${colors.border} ${colors.background}`, scrollbarWidth: "thin" }, -}, { dark: true }); - -export const oxocarbonDarkHighlight = HighlightStyle.define([ - { tag: tags.keyword, color: colors.magenta, fontWeight: "bold" }, - { tag: tags.controlKeyword, color: colors.magenta }, - { tag: tags.definitionKeyword, color: colors.magenta }, - { tag: tags.modifier, color: colors.magenta }, - { tag: tags.operatorKeyword, color: colors.magenta }, - { tag: tags.function(tags.variableName), color: colors.blue }, - { tag: tags.function(tags.definition(tags.variableName)), color: colors.blue }, - { tag: tags.labelName, color: colors.cyan }, - { tag: tags.namespace, color: colors.cyan }, - { tag: tags.macroName, color: colors.cyan }, - { tag: tags.literal, color: colors.teal }, - { tag: tags.string, color: colors.green }, - { tag: tags.docString, color: colors.green }, - { tag: tags.character, color: colors.green }, - { tag: tags.attributeValue, color: colors.green }, - { tag: tags.number, color: colors.teal }, - { tag: tags.integer, color: colors.teal }, - { tag: tags.float, color: colors.teal }, - { tag: tags.bool, color: colors.teal }, - { tag: tags.regexp, color: colors.orange }, - { tag: tags.escape, color: colors.orange }, - { tag: tags.color, color: colors.orange }, - { tag: tags.url, color: colors.cyan, textDecoration: "underline" }, - { tag: tags.keyword, color: colors.magenta }, - { tag: tags.self, color: colors.purple }, - { tag: tags.null, color: colors.purple }, - { tag: tags.atom, color: colors.purple }, - { tag: tags.unit, color: colors.purple }, - { tag: tags.modifier, color: colors.magenta }, - { tag: tags.operator, color: colors.cyan }, - { tag: tags.derefOperator, color: colors.cyan }, - { tag: tags.arithmeticOperator, color: colors.cyan }, - { tag: tags.logicOperator, color: colors.cyan }, - { tag: tags.bitwiseOperator, color: colors.cyan }, - { tag: tags.compareOperator, color: colors.cyan }, - { tag: tags.updateOperator, color: colors.cyan }, - { tag: tags.definitionOperator, color: colors.cyan }, - { tag: tags.typeOperator, color: colors.magenta }, - { tag: tags.controlOperator, color: colors.cyan }, - { tag: tags.punctuation, color: colors.text }, - { tag: tags.separator, color: colors.text }, - { tag: tags.bracket, color: colors.text }, - { tag: tags.angleBracket, color: colors.text }, - { tag: tags.squareBracket, color: colors.text }, - { tag: tags.paren, color: colors.text }, - { tag: tags.brace, color: colors.text }, - { tag: tags.contentSeparator, color: colors.text }, - { tag: tags.comment, color: colors.textMuted, fontStyle: "italic" }, - { tag: tags.lineComment, color: colors.textMuted, fontStyle: "italic" }, - { tag: tags.blockComment, color: colors.textMuted, fontStyle: "italic" }, - { tag: tags.docComment, color: colors.textMuted, fontStyle: "italic" }, - { tag: tags.name, color: colors.text }, - { tag: tags.variableName, color: colors.text }, - { tag: tags.typeName, color: colors.yellow }, - { tag: tags.tagName, color: colors.blue }, - { tag: tags.attributeName, color: colors.cyan }, - { tag: tags.className, color: colors.yellow }, - { tag: tags.labelName, color: colors.cyan }, - { tag: tags.namespace, color: colors.cyan }, - { tag: tags.macroName, color: colors.cyan }, - { tag: tags.propertyName, color: colors.cyan }, - { tag: tags.special(tags.propertyName), color: colors.cyan }, - { tag: tags.heading1, color: colors.blue, fontWeight: "bold", fontSize: "1.5em" }, - { tag: tags.heading2, color: colors.blue, fontWeight: "bold", fontSize: "1.3em" }, - { tag: tags.heading3, color: colors.blue, fontWeight: "bold", fontSize: "1.1em" }, - { tag: tags.heading4, color: colors.blue, fontWeight: "bold" }, - { tag: tags.heading5, color: colors.blue, fontWeight: "bold" }, - { tag: tags.heading6, color: colors.blue, fontWeight: "bold" }, - { tag: tags.heading, color: colors.blue, fontWeight: "bold" }, - { tag: tags.quote, color: colors.green, fontStyle: "italic" }, - { tag: tags.list, color: colors.cyan }, - { tag: tags.emphasis, color: colors.text, fontStyle: "italic" }, - { tag: tags.strong, color: colors.text, fontWeight: "bold" }, - { tag: tags.strikethrough, color: colors.textMuted, textDecoration: "line-through" }, - { tag: tags.link, color: colors.cyan, textDecoration: "underline" }, - { tag: tags.monospace, color: colors.orange, fontFamily: "monospace" }, - { tag: tags.processingInstruction, color: colors.magenta }, - { tag: tags.annotation, color: colors.yellow }, - { tag: tags.invalid, color: colors.red, borderBottom: `1px dotted ${colors.red}` }, - { tag: tags.meta, color: colors.textMuted }, - { tag: tags.documentMeta, color: colors.textMuted }, - - { tag: tags.changed, color: colors.yellow }, - { tag: tags.deleted, color: colors.red }, - { tag: tags.inserted, color: colors.green }, -]); +const options: ThemeOptions = { + dark: true, + selectionBg: "rgba(51, 177, 255, 0.2)", + selectionBgFocused: "rgba(51, 177, 255, 0.3)", + activeLineBg: "rgba(57, 57, 57, 0.5)", + tooltipShadowOpacity: 0.5, + searchMatchOpacity: 0.3, + searchMatchSelectedOpacity: 0.5, + highlightOpacity: 0.3, + placeholderColor: "textMuted", +}; -export const oxocarbonDark = [oxocarbonDarkTheme, syntaxHighlighting(oxocarbonDarkHighlight)]; +export const oxocarbonDark = createTheme(colors, options); diff --git a/src/editor/themes/oxocarbon-light.ts b/src/editor/themes/oxocarbon-light.ts index a758b16..86457e9 100644 --- a/src/editor/themes/oxocarbon-light.ts +++ b/src/editor/themes/oxocarbon-light.ts @@ -7,11 +7,9 @@ * @see https://carbondesignsystem.com/guidelines/color/overview/ */ -import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; -import { EditorView } from "@codemirror/view"; -import { tags } from "@lezer/highlight"; +import { type Colors, createTheme, type ThemeOptions } from "./shared"; -const colors = { +const colors: Colors = { background: "#f2f4f8", surface: "#dde1e6", text: "#272d35", @@ -31,164 +29,16 @@ const colors = { black: "#000000", }; -export const oxocarbonLightTheme = EditorView.theme({ - "&": { - backgroundColor: colors.background, - color: colors.text, - fontSize: "var(--editor-font-size, 14px)", - fontFamily: "var(--editor-font-family, \"IBM Plex Mono\", \"SF Mono\", Monaco, monospace)", - }, - ".cm-scroller": { fontFamily: "var(--editor-font-family, \"IBM Plex Mono\", \"SF Mono\", Monaco, monospace)" }, - ".cm-content": { - fontFamily: "var(--editor-font-family, \"IBM Plex Mono\", \"SF Mono\", Monaco, monospace)", - caretColor: colors.cyan, - padding: "16px 0", - }, - ".cm-cursor": { borderLeftColor: colors.cyan, borderLeftWidth: "2px" }, - "&.cm-focused .cm-cursor": { borderLeftColor: colors.cyan }, - "&.cm-focused .cm-selectionBackground": { backgroundColor: "rgba(15, 98, 254, 0.2)" }, - ".cm-selectionBackground": { backgroundColor: "rgba(15, 98, 254, 0.15)" }, - ".cm-activeLine": { backgroundColor: "rgba(190, 198, 207, 0.45)" }, - ".cm-activeLineGutter": { backgroundColor: colors.surface }, - ".cm-gutters": { - backgroundColor: colors.background, - color: colors.textMuted, - borderRight: `1px solid ${colors.border}`, - paddingRight: "8px", - }, - ".cm-lineNumbers .cm-gutterElement": { padding: "0 8px", minWidth: "32px" }, - ".cm-foldPlaceholder": { - backgroundColor: colors.surface, - border: `1px solid ${colors.border}`, - borderRadius: "3px", - color: colors.textMuted, - padding: "0 4px", - }, - ".cm-foldGutter .cm-gutterElement": { color: colors.textMuted, cursor: "pointer" }, - ".cm-tooltip": { - backgroundColor: colors.surface, - border: `1px solid ${colors.border}`, - borderRadius: "4px", - boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)", - }, - ".cm-tooltip-autocomplete": { backgroundColor: colors.surface }, - ".cm-tooltip-autocomplete ul li[aria-selected]": { backgroundColor: colors.blue, color: colors.white }, - ".cm-completionLabel": { color: colors.text }, - ".cm-completionDetail": { color: colors.textMuted }, - ".cm-completionIcon": { color: colors.cyan }, - ".cm-searchMatch": { backgroundColor: "rgba(255, 111, 0, 0.35)", outline: `1px solid ${colors.yellow}` }, - ".cm-searchMatch.cm-searchMatch-selected": { backgroundColor: "rgba(255, 111, 0, 0.55)" }, - ".cm-panel": { backgroundColor: colors.surface, borderTop: `1px solid ${colors.border}` }, - ".cm-panel button": { - backgroundColor: colors.blue, - color: colors.white, - border: "none", - borderRadius: "3px", - padding: "4px 12px", - cursor: "pointer", - }, - ".cm-panel input": { - backgroundColor: colors.background, - color: colors.text, - border: `1px solid ${colors.border}`, - borderRadius: "3px", - padding: "4px 8px", - }, - ".cm-placeholder": { color: colors.textDisabled }, - ".cm-highlight": { backgroundColor: "rgba(255, 111, 0, 0.35)" }, - ".cm-specialChar": { color: colors.red }, - "& .cm-scroller::-webkit-scrollbar": { width: "10px", height: "10px" }, - "& .cm-scroller::-webkit-scrollbar-track": { background: colors.background }, - "& .cm-scroller::-webkit-scrollbar-thumb": { background: colors.border, borderRadius: "5px" }, - "& .cm-scroller::-webkit-scrollbar-thumb:hover": { background: colors.textDisabled }, - "& .cm-scroller": { scrollbarColor: `${colors.border} ${colors.background}`, scrollbarWidth: "thin" }, -}, { dark: false }); - -export const oxocarbonLightHighlight = HighlightStyle.define([ - { tag: tags.keyword, color: colors.magenta, fontWeight: "bold" }, - { tag: tags.controlKeyword, color: colors.magenta }, - { tag: tags.definitionKeyword, color: colors.magenta }, - { tag: tags.modifier, color: colors.magenta }, - { tag: tags.operatorKeyword, color: colors.magenta }, - { tag: tags.function(tags.variableName), color: colors.blue }, - { tag: tags.function(tags.definition(tags.variableName)), color: colors.blue }, - { tag: tags.labelName, color: colors.cyan }, - { tag: tags.namespace, color: colors.cyan }, - { tag: tags.macroName, color: colors.cyan }, - { tag: tags.literal, color: colors.teal }, - { tag: tags.string, color: colors.green }, - { tag: tags.docString, color: colors.green }, - { tag: tags.character, color: colors.green }, - { tag: tags.attributeValue, color: colors.green }, - { tag: tags.number, color: colors.teal }, - { tag: tags.integer, color: colors.teal }, - { tag: tags.float, color: colors.teal }, - { tag: tags.bool, color: colors.teal }, - { tag: tags.regexp, color: colors.orange }, - { tag: tags.escape, color: colors.orange }, - { tag: tags.color, color: colors.orange }, - { tag: tags.url, color: colors.cyan, textDecoration: "underline" }, - { tag: tags.keyword, color: colors.magenta }, - { tag: tags.self, color: colors.purple }, - { tag: tags.null, color: colors.purple }, - { tag: tags.atom, color: colors.purple }, - { tag: tags.unit, color: colors.purple }, - { tag: tags.modifier, color: colors.magenta }, - { tag: tags.operator, color: colors.cyan }, - { tag: tags.derefOperator, color: colors.cyan }, - { tag: tags.arithmeticOperator, color: colors.cyan }, - { tag: tags.logicOperator, color: colors.cyan }, - { tag: tags.bitwiseOperator, color: colors.cyan }, - { tag: tags.compareOperator, color: colors.cyan }, - { tag: tags.updateOperator, color: colors.cyan }, - { tag: tags.definitionOperator, color: colors.cyan }, - { tag: tags.typeOperator, color: colors.magenta }, - { tag: tags.controlOperator, color: colors.cyan }, - { tag: tags.punctuation, color: colors.text }, - { tag: tags.separator, color: colors.text }, - { tag: tags.bracket, color: colors.text }, - { tag: tags.angleBracket, color: colors.text }, - { tag: tags.squareBracket, color: colors.text }, - { tag: tags.paren, color: colors.text }, - { tag: tags.brace, color: colors.text }, - { tag: tags.contentSeparator, color: colors.text }, - { tag: tags.comment, color: colors.textMuted, fontStyle: "italic" }, - { tag: tags.lineComment, color: colors.textMuted, fontStyle: "italic" }, - { tag: tags.blockComment, color: colors.textMuted, fontStyle: "italic" }, - { tag: tags.docComment, color: colors.textMuted, fontStyle: "italic" }, - { tag: tags.name, color: colors.text }, - { tag: tags.variableName, color: colors.text }, - { tag: tags.typeName, color: colors.yellow }, - { tag: tags.tagName, color: colors.blue }, - { tag: tags.attributeName, color: colors.cyan }, - { tag: tags.className, color: colors.yellow }, - { tag: tags.labelName, color: colors.cyan }, - { tag: tags.namespace, color: colors.cyan }, - { tag: tags.macroName, color: colors.cyan }, - { tag: tags.propertyName, color: colors.cyan }, - { tag: tags.special(tags.propertyName), color: colors.cyan }, - { tag: tags.heading1, color: colors.blue, fontWeight: "bold", fontSize: "1.5em" }, - { tag: tags.heading2, color: colors.blue, fontWeight: "bold", fontSize: "1.3em" }, - { tag: tags.heading3, color: colors.blue, fontWeight: "bold", fontSize: "1.1em" }, - { tag: tags.heading4, color: colors.blue, fontWeight: "bold" }, - { tag: tags.heading5, color: colors.blue, fontWeight: "bold" }, - { tag: tags.heading6, color: colors.blue, fontWeight: "bold" }, - { tag: tags.heading, color: colors.blue, fontWeight: "bold" }, - { tag: tags.quote, color: colors.green, fontStyle: "italic" }, - { tag: tags.list, color: colors.cyan }, - { tag: tags.emphasis, color: colors.text, fontStyle: "italic" }, - { tag: tags.strong, color: colors.text, fontWeight: "bold" }, - { tag: tags.strikethrough, color: colors.textMuted, textDecoration: "line-through" }, - { tag: tags.link, color: colors.cyan, textDecoration: "underline" }, - { tag: tags.monospace, color: colors.orange, fontFamily: "monospace" }, - { tag: tags.processingInstruction, color: colors.magenta }, - { tag: tags.annotation, color: colors.yellow }, - { tag: tags.invalid, color: colors.red, borderBottom: `1px dotted ${colors.red}` }, - { tag: tags.meta, color: colors.textMuted }, - { tag: tags.documentMeta, color: colors.textMuted }, - { tag: tags.changed, color: colors.yellow }, - { tag: tags.deleted, color: colors.red }, - { tag: tags.inserted, color: colors.green }, -]); +const options: ThemeOptions = { + dark: false, + selectionBg: "rgba(15, 98, 254, 0.15)", + selectionBgFocused: "rgba(15, 98, 254, 0.2)", + activeLineBg: "rgba(190, 198, 207, 0.45)", + tooltipShadowOpacity: 0.15, + searchMatchOpacity: 0.35, + searchMatchSelectedOpacity: 0.55, + highlightOpacity: 0.35, + placeholderColor: "textDisabled", +}; -export const oxocarbonLight = [oxocarbonLightTheme, syntaxHighlighting(oxocarbonLightHighlight)]; +export const oxocarbonLight = createTheme(colors, options); diff --git a/src/editor/themes/shared.ts b/src/editor/themes/shared.ts new file mode 100644 index 0000000..75be774 --- /dev/null +++ b/src/editor/themes/shared.ts @@ -0,0 +1,206 @@ +import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; +import { EditorView } from "@codemirror/view"; +import { tags } from "@lezer/highlight"; + +type Color = + | "background" + | "surface" + | "text" + | "textMuted" + | "textDisabled" + | "border" + | "blue" + | "cyan" + | "green" + | "magenta" + | "orange" + | "purple" + | "red" + | "teal" + | "yellow" + | "white" + | "black"; + +export type ThemeOptions = { + dark: boolean; + selectionBg: string; + selectionBgFocused: string; + activeLineBg: string; + tooltipShadowOpacity: number; + searchMatchOpacity: number; + searchMatchSelectedOpacity: number; + highlightOpacity: number; + placeholderColor: "textMuted" | "textDisabled"; +}; + +export type Colors = Record; + +export function createTheme(palette: Colors, options: ThemeOptions) { + const theme = EditorView.theme({ + "&": { + backgroundColor: palette.background, + color: palette.text, + fontSize: "var(--editor-font-size, 14px)", + fontFamily: "var(--editor-font-family, \"IBM Plex Mono\", \"SF Mono\", Monaco, monospace)", + }, + ".cm-scroller": { fontFamily: "var(--editor-font-family, \"IBM Plex Mono\", \"SF Mono\", Monaco, monospace)" }, + ".cm-content": { + fontFamily: "var(--editor-font-family, \"IBM Plex Mono\", \"SF Mono\", Monaco, monospace)", + caretColor: palette.cyan, + padding: "16px 0", + }, + ".cm-cursor": { borderLeftColor: palette.cyan, borderLeftWidth: "2px" }, + "&.cm-focused .cm-cursor": { borderLeftColor: palette.cyan }, + "&.cm-focused .cm-selectionBackground": { backgroundColor: options.selectionBgFocused }, + ".cm-selectionBackground": { backgroundColor: options.selectionBg }, + ".cm-activeLine": { backgroundColor: options.activeLineBg }, + ".cm-activeLineGutter": { backgroundColor: palette.surface }, + ".cm-gutters": { + backgroundColor: palette.background, + color: palette.textMuted, + borderRight: `1px solid ${palette.border}`, + paddingRight: "8px", + }, + ".cm-lineNumbers": { fontFamily: "\"IBM Plex Mono\", \"SF Mono\", Monaco, monospace" }, + ".cm-lineNumbers .cm-gutterElement": { padding: "0 8px", minWidth: "32px" }, + ".cm-foldPlaceholder": { + backgroundColor: palette.surface, + border: `1px solid ${palette.border}`, + borderRadius: "3px", + color: palette.textMuted, + padding: "0 4px", + }, + ".cm-foldGutter .cm-gutterElement": { color: palette.textMuted, cursor: "pointer" }, + ".cm-tooltip": { + backgroundColor: palette.surface, + border: `1px solid ${palette.border}`, + borderRadius: "4px", + boxShadow: `0 4px 12px rgba(0, 0, 0, ${options.tooltipShadowOpacity})`, + }, + ".cm-tooltip-autocomplete": { backgroundColor: palette.surface }, + ".cm-tooltip-autocomplete ul li[aria-selected]": { backgroundColor: palette.blue, color: palette.white }, + ".cm-completionLabel": { color: palette.text }, + ".cm-completionDetail": { color: palette.textMuted }, + ".cm-completionIcon": { color: palette.cyan }, + ".cm-searchMatch": { + backgroundColor: `rgba(255, 111, 0, ${options.searchMatchOpacity})`, + outline: `1px solid ${palette.yellow}`, + }, + ".cm-searchMatch.cm-searchMatch-selected": { + backgroundColor: `rgba(255, 111, 0, ${options.searchMatchSelectedOpacity})`, + }, + ".cm-panel": { backgroundColor: palette.surface, borderTop: `1px solid ${palette.border}` }, + ".cm-panel button": { + backgroundColor: palette.blue, + color: palette.white, + border: "none", + borderRadius: "3px", + padding: "4px 12px", + cursor: "pointer", + }, + ".cm-panel input": { + backgroundColor: palette.background, + color: palette.text, + border: `1px solid ${palette.border}`, + borderRadius: "3px", + padding: "4px 8px", + }, + ".cm-placeholder": { color: palette[options.placeholderColor] }, + ".cm-highlight": { backgroundColor: `rgba(255, 111, 0, ${options.highlightOpacity})` }, + ".cm-specialChar": { color: palette.red }, + "& .cm-scroller::-webkit-scrollbar": { width: "10px", height: "10px" }, + "& .cm-scroller::-webkit-scrollbar-track": { background: palette.background }, + "& .cm-scroller::-webkit-scrollbar-thumb": { background: palette.border, borderRadius: "5px" }, + "& .cm-scroller::-webkit-scrollbar-thumb:hover": { background: palette.textDisabled }, + "& .cm-scroller": { scrollbarColor: `${palette.border} ${palette.background}`, scrollbarWidth: "thin" }, + }, { dark: options.dark }); + + const highlight = HighlightStyle.define([ + { tag: tags.keyword, color: palette.magenta, fontWeight: "bold" }, + { tag: tags.controlKeyword, color: palette.magenta }, + { tag: tags.definitionKeyword, color: palette.magenta }, + { tag: tags.modifier, color: palette.magenta }, + { tag: tags.operatorKeyword, color: palette.magenta }, + { tag: tags.function(tags.variableName), color: palette.blue }, + { tag: tags.function(tags.definition(tags.variableName)), color: palette.blue }, + { tag: tags.labelName, color: palette.cyan }, + { tag: tags.namespace, color: palette.cyan }, + { tag: tags.macroName, color: palette.cyan }, + { tag: tags.literal, color: palette.teal }, + { tag: tags.string, color: palette.green }, + { tag: tags.docString, color: palette.green }, + { tag: tags.character, color: palette.green }, + { tag: tags.attributeValue, color: palette.green }, + { tag: tags.number, color: palette.teal }, + { tag: tags.integer, color: palette.teal }, + { tag: tags.float, color: palette.teal }, + { tag: tags.bool, color: palette.teal }, + { tag: tags.regexp, color: palette.orange }, + { tag: tags.escape, color: palette.orange }, + { tag: tags.color, color: palette.orange }, + { tag: tags.url, color: palette.cyan, textDecoration: "underline" }, + { tag: tags.keyword, color: palette.magenta }, + { tag: tags.self, color: palette.purple }, + { tag: tags.null, color: palette.purple }, + { tag: tags.atom, color: palette.purple }, + { tag: tags.unit, color: palette.purple }, + { tag: tags.modifier, color: palette.magenta }, + { tag: tags.operator, color: palette.cyan }, + { tag: tags.derefOperator, color: palette.cyan }, + { tag: tags.arithmeticOperator, color: palette.cyan }, + { tag: tags.logicOperator, color: palette.cyan }, + { tag: tags.bitwiseOperator, color: palette.cyan }, + { tag: tags.compareOperator, color: palette.cyan }, + { tag: tags.updateOperator, color: palette.cyan }, + { tag: tags.definitionOperator, color: palette.cyan }, + { tag: tags.typeOperator, color: palette.magenta }, + { tag: tags.controlOperator, color: palette.cyan }, + { tag: tags.punctuation, color: palette.text }, + { tag: tags.separator, color: palette.text }, + { tag: tags.bracket, color: palette.text }, + { tag: tags.angleBracket, color: palette.text }, + { tag: tags.squareBracket, color: palette.text }, + { tag: tags.paren, color: palette.text }, + { tag: tags.brace, color: palette.text }, + { tag: tags.contentSeparator, color: palette.text }, + { tag: tags.comment, color: palette.textMuted, fontStyle: "italic" }, + { tag: tags.lineComment, color: palette.textMuted, fontStyle: "italic" }, + { tag: tags.blockComment, color: palette.textMuted, fontStyle: "italic" }, + { tag: tags.docComment, color: palette.textMuted, fontStyle: "italic" }, + { tag: tags.name, color: palette.text }, + { tag: tags.variableName, color: palette.text }, + { tag: tags.typeName, color: palette.yellow }, + { tag: tags.tagName, color: palette.blue }, + { tag: tags.attributeName, color: palette.cyan }, + { tag: tags.className, color: palette.yellow }, + { tag: tags.labelName, color: palette.cyan }, + { tag: tags.namespace, color: palette.cyan }, + { tag: tags.macroName, color: palette.cyan }, + { tag: tags.propertyName, color: palette.cyan }, + { tag: tags.special(tags.propertyName), color: palette.cyan }, + { tag: tags.heading1, color: palette.blue, fontWeight: "bold", fontSize: "1.5em" }, + { tag: tags.heading2, color: palette.blue, fontWeight: "bold", fontSize: "1.3em" }, + { tag: tags.heading3, color: palette.blue, fontWeight: "bold", fontSize: "1.1em" }, + { tag: tags.heading4, color: palette.blue, fontWeight: "bold" }, + { tag: tags.heading5, color: palette.blue, fontWeight: "bold" }, + { tag: tags.heading6, color: palette.blue, fontWeight: "bold" }, + { tag: tags.heading, color: palette.blue, fontWeight: "bold" }, + { tag: tags.quote, color: palette.green, fontStyle: "italic" }, + { tag: tags.list, color: palette.cyan }, + { tag: tags.emphasis, color: palette.text, fontStyle: "italic" }, + { tag: tags.strong, color: palette.text, fontWeight: "bold" }, + { tag: tags.strikethrough, color: palette.textMuted, textDecoration: "line-through" }, + { tag: tags.link, color: palette.cyan, textDecoration: "underline" }, + { tag: tags.monospace, color: palette.orange, fontFamily: "monospace" }, + { tag: tags.processingInstruction, color: palette.magenta }, + { tag: tags.annotation, color: palette.yellow }, + { tag: tags.invalid, color: palette.red, borderBottom: `1px dotted ${palette.red}` }, + { tag: tags.meta, color: palette.textMuted }, + { tag: tags.documentMeta, color: palette.textMuted }, + { tag: tags.changed, color: palette.yellow }, + { tag: tags.deleted, color: palette.red }, + { tag: tags.inserted, color: palette.green }, + ]); + + return [theme, syntaxHighlighting(highlight)]; +} -- 2.51.2