|
@@ -0,0 +1,189 @@
|
|
|
|
|
+/**
|
|
|
|
|
+ * Theme system for the editor.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Themes are CSS variable overrides scoped to the editor shell element.
|
|
|
|
|
+ * A theme can be a preset name (string) or a config object with color
|
|
|
|
|
+ * and syntax highlighting overrides.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Usage:
|
|
|
|
|
+ * <Editor theme="kanagawa" />
|
|
|
|
|
+ * <Editor :theme="{ colors: { primary: '#3b82f6' } }" />
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+// ── Types ──────────────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+export interface ThemeColors {
|
|
|
|
|
+ primary?: string
|
|
|
|
|
+ success?: string
|
|
|
|
|
+ error?: string
|
|
|
|
|
+ neutral?: string
|
|
|
|
|
+
|
|
|
|
|
+ text?: {
|
|
|
|
|
+ main?: string
|
|
|
|
|
+ muted?: string
|
|
|
|
|
+ highlight?: string
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ background?: {
|
|
|
|
|
+ elevated?: string
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ border?: string
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export interface ThemeSyntax {
|
|
|
|
|
+ keyword?: string
|
|
|
|
|
+ string?: string
|
|
|
|
|
+ string2?: string
|
|
|
|
|
+ comment?: string
|
|
|
|
|
+ number?: string
|
|
|
|
|
+ atom?: string
|
|
|
|
|
+ bool?: string
|
|
|
|
|
+ null?: string
|
|
|
|
|
+ type?: string
|
|
|
|
|
+ typeName?: string
|
|
|
|
|
+ className?: string
|
|
|
|
|
+ function?: string
|
|
|
|
|
+ definition?: string
|
|
|
|
|
+ variableName?: string
|
|
|
|
|
+ propertyName?: string
|
|
|
|
|
+ attributeName?: string
|
|
|
|
|
+ operator?: string
|
|
|
|
|
+ punctuation?: string
|
|
|
|
|
+ tag?: string
|
|
|
|
|
+ meta?: string
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export interface EditorTheme {
|
|
|
|
|
+ name?: string
|
|
|
|
|
+ colors?: ThemeColors
|
|
|
|
|
+ syntax?: ThemeSyntax
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export type ThemeInput = string | EditorTheme
|
|
|
|
|
+
|
|
|
|
|
+// ── Default theme (uses @nuxt/ui variables as-is) ──────────────────
|
|
|
|
|
+
|
|
|
|
|
+const defaultTheme: EditorTheme = {
|
|
|
|
|
+ name: "default",
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ── Built-in presets ───────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+const kanagawa: EditorTheme = {
|
|
|
|
|
+ name: "kanagawa",
|
|
|
|
|
+ colors: {
|
|
|
|
|
+ primary: "#7fb4ca",
|
|
|
|
|
+ success: "#98bb6c",
|
|
|
|
|
+ error: "#e46876",
|
|
|
|
|
+ neutral: "#727169",
|
|
|
|
|
+ text: {
|
|
|
|
|
+ main: "#dcd7ba",
|
|
|
|
|
+ muted: "#727169",
|
|
|
|
|
+ highlight: "#e6c384",
|
|
|
|
|
+ },
|
|
|
|
|
+ background: {
|
|
|
|
|
+ elevated: "#1f1f28",
|
|
|
|
|
+ },
|
|
|
|
|
+ border: "#363646",
|
|
|
|
|
+ },
|
|
|
|
|
+ syntax: {
|
|
|
|
|
+ keyword: "#e6c384",
|
|
|
|
|
+ string: "#98bb6c",
|
|
|
|
|
+ string2: "#98bb6c",
|
|
|
|
|
+ comment: "#727169",
|
|
|
|
|
+ number: "#dca561",
|
|
|
|
|
+ atom: "#e46876",
|
|
|
|
|
+ bool: "#e46876",
|
|
|
|
|
+ null: "#e46876",
|
|
|
|
|
+ type: "#7e9cd8",
|
|
|
|
|
+ typeName: "#7e9cd8",
|
|
|
|
|
+ className: "#7e9cd8",
|
|
|
|
|
+ function: "#7fb4ca",
|
|
|
|
|
+ definition: "#7fb4ca",
|
|
|
|
|
+ variableName: "#dcd7ba",
|
|
|
|
|
+ propertyName: "#dcd7ba",
|
|
|
|
|
+ attributeName: "#7fb4ca",
|
|
|
|
|
+ operator: "#dcd7ba",
|
|
|
|
|
+ punctuation: "#dcd7ba",
|
|
|
|
|
+ tag: "#7fb4ca",
|
|
|
|
|
+ meta: "#e6c384",
|
|
|
|
|
+ },
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export const themePresets: Record<string, EditorTheme | undefined> = {
|
|
|
|
|
+ default: defaultTheme,
|
|
|
|
|
+ kanagawa,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ── Resolution ─────────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+export function resolveTheme(input: ThemeInput | undefined): EditorTheme {
|
|
|
|
|
+ if (!input || input === "default") return defaultTheme
|
|
|
|
|
+
|
|
|
|
|
+ if (typeof input === "string") {
|
|
|
|
|
+ const preset = themePresets[input]
|
|
|
|
|
+ if (preset) return preset
|
|
|
|
|
+ console.warn(
|
|
|
|
|
+ `[editor] unknown theme preset "${input}", falling back to default`,
|
|
|
|
|
+ )
|
|
|
|
|
+ return defaultTheme
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return input
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ── CSS variable mapping ───────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Map a resolved theme to CSS variable overrides scoped to the editor
|
|
|
|
|
+ * shell element. Variables using `--ui-*` prefixes cascade into all
|
|
|
|
|
+ * child components (Block, InsertionZone, CodeBlockView) overriding
|
|
|
|
|
+ * the page-level @nuxt/ui values. Syntax variables override the
|
|
|
|
|
+ * `--code-*` values used by CM6 syntax highlighting.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Because these are set inline on the editor shell, they do NOT leak
|
|
|
|
|
+ * to sibling or ancestor elements outside the editor.
|
|
|
|
|
+ */
|
|
|
|
|
+export function themeToCSSVars(theme: EditorTheme): Record<string, string> {
|
|
|
|
|
+ const vars: Record<string, string> = {}
|
|
|
|
|
+ const c = theme.colors
|
|
|
|
|
+ if (!c) return vars
|
|
|
|
|
+
|
|
|
|
|
+ // Map to --ui-* variables (overrides @nuxt/ui at editor scope)
|
|
|
|
|
+ if (c.primary) vars["--ui-primary"] = c.primary
|
|
|
|
|
+ if (c.success) vars["--ui-success"] = c.success
|
|
|
|
|
+ if (c.error) vars["--ui-error"] = c.error
|
|
|
|
|
+ if (c.text?.main) vars["--ui-text-main"] = c.text.main
|
|
|
|
|
+ if (c.text?.muted) vars["--ui-text-muted"] = c.text.muted
|
|
|
|
|
+ if (c.text?.highlight) vars["--ui-text-highlight"] = c.text.highlight
|
|
|
|
|
+ if (c.background?.elevated) vars["--ui-bg-elevated"] = c.background.elevated
|
|
|
|
|
+ if (c.border) vars["--ui-border"] = c.border
|
|
|
|
|
+
|
|
|
|
|
+ // Map to --code-* variables (overrides syntax highlighting at editor scope)
|
|
|
|
|
+ const s = theme.syntax
|
|
|
|
|
+ if (s) {
|
|
|
|
|
+ if (s.keyword) vars["--code-keyword"] = s.keyword
|
|
|
|
|
+ if (s.string) vars["--code-string"] = s.string
|
|
|
|
|
+ if (s.string2) vars["--code-string2"] = s.string2
|
|
|
|
|
+ if (s.comment) vars["--code-comment"] = s.comment
|
|
|
|
|
+ if (s.number) vars["--code-number"] = s.number
|
|
|
|
|
+ if (s.atom) vars["--code-atom"] = s.atom
|
|
|
|
|
+ if (s.bool) vars["--code-bool"] = s.bool
|
|
|
|
|
+ if (s.null) vars["--code-null"] = s.null
|
|
|
|
|
+ if (s.type) vars["--code-type"] = s.type
|
|
|
|
|
+ if (s.typeName) vars["--code-typeName"] = s.typeName
|
|
|
|
|
+ if (s.className) vars["--code-className"] = s.className
|
|
|
|
|
+ if (s.function) vars["--code-function"] = s.function
|
|
|
|
|
+ if (s.definition) vars["--code-definition"] = s.definition
|
|
|
|
|
+ if (s.variableName) vars["--code-variableName"] = s.variableName
|
|
|
|
|
+ if (s.propertyName) vars["--code-propertyName"] = s.propertyName
|
|
|
|
|
+ if (s.attributeName) vars["--code-attributeName"] = s.attributeName
|
|
|
|
|
+ if (s.operator) vars["--code-operator"] = s.operator
|
|
|
|
|
+ if (s.punctuation) vars["--code-punctuation"] = s.punctuation
|
|
|
|
|
+ if (s.tag) vars["--code-tag"] = s.tag
|
|
|
|
|
+ if (s.meta) vars["--code-meta"] = s.meta
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return vars
|
|
|
|
|
+}
|