feat: comprehensive theme.json config with live hot-reload for editor styling
This commit is contained in:
@@ -5,7 +5,9 @@ import { createReactInlineContentSpec, useCreateBlockNote, SuggestionMenuControl
|
||||
import { BlockNoteView } from '@blocknote/mantine'
|
||||
import '@blocknote/mantine/style.css'
|
||||
import type { VaultEntry } from '../types'
|
||||
import { useEditorTheme } from '../hooks/useTheme'
|
||||
import './Editor.css'
|
||||
import './EditorTheme.css'
|
||||
|
||||
interface Tab {
|
||||
entry: VaultEntry
|
||||
@@ -163,6 +165,7 @@ function BlockNoteTab({ content, entries, onNavigateWikilink }: { content: strin
|
||||
const [, body] = useMemo(() => splitFrontmatter(content), [content])
|
||||
const navigateRef = useRef(onNavigateWikilink)
|
||||
navigateRef.current = onNavigateWikilink
|
||||
const { cssVars } = useEditorTheme()
|
||||
|
||||
const editor = useCreateBlockNote({ schema })
|
||||
|
||||
@@ -217,7 +220,7 @@ function BlockNoteTab({ content, entries, onNavigateWikilink }: { content: strin
|
||||
const isDark = typeof document !== 'undefined' && document.documentElement.getAttribute('data-theme') !== 'light'
|
||||
|
||||
return (
|
||||
<div className="editor__blocknote-container">
|
||||
<div className="editor__blocknote-container" style={cssVars as React.CSSProperties}>
|
||||
<BlockNoteView
|
||||
editor={editor}
|
||||
theme={isDark ? 'dark' : 'light'}
|
||||
|
||||
179
src/components/EditorTheme.css
Normal file
179
src/components/EditorTheme.css
Normal file
@@ -0,0 +1,179 @@
|
||||
/* ==============================================
|
||||
EditorTheme.css — BlockNote theme overrides
|
||||
Driven by CSS custom properties from theme.json
|
||||
============================================== */
|
||||
|
||||
/* --- Editor root --- */
|
||||
.editor__blocknote-container .bn-editor {
|
||||
font-family: var(--editor-font-family);
|
||||
font-size: var(--editor-font-size);
|
||||
line-height: var(--editor-line-height);
|
||||
max-width: var(--editor-max-width);
|
||||
padding: var(--editor-padding-vertical) var(--editor-padding-horizontal);
|
||||
color: var(--colors-text);
|
||||
caret-color: var(--colors-cursor);
|
||||
}
|
||||
|
||||
.editor__blocknote-container .bn-editor ::selection {
|
||||
background: var(--colors-selection);
|
||||
}
|
||||
|
||||
/* --- Paragraph spacing --- */
|
||||
.editor__blocknote-container .bn-block-content[data-content-type="paragraph"] {
|
||||
margin-bottom: var(--editor-paragraph-spacing);
|
||||
}
|
||||
|
||||
/* --- Headings --- */
|
||||
.editor__blocknote-container [data-content-type="heading"][data-level="1"] .bn-inline-content {
|
||||
font-size: var(--headings-h1-font-size);
|
||||
font-weight: var(--headings-h1-font-weight);
|
||||
line-height: var(--headings-h1-line-height);
|
||||
color: var(--headings-h1-color);
|
||||
letter-spacing: var(--headings-h1-letter-spacing);
|
||||
}
|
||||
.editor__blocknote-container [data-content-type="heading"][data-level="1"] {
|
||||
margin-top: var(--headings-h1-margin-top);
|
||||
margin-bottom: var(--headings-h1-margin-bottom);
|
||||
}
|
||||
|
||||
.editor__blocknote-container [data-content-type="heading"][data-level="2"] .bn-inline-content {
|
||||
font-size: var(--headings-h2-font-size);
|
||||
font-weight: var(--headings-h2-font-weight);
|
||||
line-height: var(--headings-h2-line-height);
|
||||
color: var(--headings-h2-color);
|
||||
letter-spacing: var(--headings-h2-letter-spacing);
|
||||
}
|
||||
.editor__blocknote-container [data-content-type="heading"][data-level="2"] {
|
||||
margin-top: var(--headings-h2-margin-top);
|
||||
margin-bottom: var(--headings-h2-margin-bottom);
|
||||
}
|
||||
|
||||
.editor__blocknote-container [data-content-type="heading"][data-level="3"] .bn-inline-content {
|
||||
font-size: var(--headings-h3-font-size);
|
||||
font-weight: var(--headings-h3-font-weight);
|
||||
line-height: var(--headings-h3-line-height);
|
||||
color: var(--headings-h3-color);
|
||||
letter-spacing: var(--headings-h3-letter-spacing);
|
||||
}
|
||||
.editor__blocknote-container [data-content-type="heading"][data-level="3"] {
|
||||
margin-top: var(--headings-h3-margin-top);
|
||||
margin-bottom: var(--headings-h3-margin-bottom);
|
||||
}
|
||||
|
||||
.editor__blocknote-container [data-content-type="heading"][data-level="4"] .bn-inline-content {
|
||||
font-size: var(--headings-h4-font-size);
|
||||
font-weight: var(--headings-h4-font-weight);
|
||||
line-height: var(--headings-h4-line-height);
|
||||
color: var(--headings-h4-color);
|
||||
letter-spacing: var(--headings-h4-letter-spacing);
|
||||
}
|
||||
.editor__blocknote-container [data-content-type="heading"][data-level="4"] {
|
||||
margin-top: var(--headings-h4-margin-top);
|
||||
margin-bottom: var(--headings-h4-margin-bottom);
|
||||
}
|
||||
|
||||
/* --- Bullet lists --- */
|
||||
.editor__blocknote-container [data-content-type="bulletListItem"] {
|
||||
padding-left: var(--lists-padding-left);
|
||||
margin-bottom: var(--lists-item-spacing);
|
||||
}
|
||||
|
||||
/* --- Numbered lists --- */
|
||||
.editor__blocknote-container [data-content-type="numberedListItem"] {
|
||||
padding-left: var(--lists-padding-left);
|
||||
margin-bottom: var(--lists-item-spacing);
|
||||
}
|
||||
|
||||
/* --- Checkbox / check list items --- */
|
||||
.editor__blocknote-container [data-content-type="checkListItem"] {
|
||||
margin-bottom: var(--lists-item-spacing);
|
||||
}
|
||||
|
||||
/* --- Inline: bold --- */
|
||||
.editor__blocknote-container .bn-editor strong {
|
||||
font-weight: var(--inline-styles-bold-font-weight);
|
||||
color: var(--inline-styles-bold-color);
|
||||
}
|
||||
|
||||
/* --- Inline: italic --- */
|
||||
.editor__blocknote-container .bn-editor em {
|
||||
font-style: var(--inline-styles-italic-font-style);
|
||||
color: var(--inline-styles-italic-color);
|
||||
}
|
||||
|
||||
/* --- Inline: strikethrough --- */
|
||||
.editor__blocknote-container .bn-editor s,
|
||||
.editor__blocknote-container .bn-editor del {
|
||||
color: var(--inline-styles-strikethrough-color);
|
||||
text-decoration: var(--inline-styles-strikethrough-text-decoration);
|
||||
}
|
||||
|
||||
/* --- Inline: code --- */
|
||||
.editor__blocknote-container .bn-editor code {
|
||||
font-family: var(--inline-styles-code-font-family);
|
||||
font-size: var(--inline-styles-code-font-size);
|
||||
background-color: var(--inline-styles-code-background-color);
|
||||
padding: var(--inline-styles-code-padding-vertical) var(--inline-styles-code-padding-horizontal);
|
||||
border-radius: var(--inline-styles-code-border-radius);
|
||||
color: var(--inline-styles-code-color);
|
||||
}
|
||||
|
||||
/* --- Inline: links --- */
|
||||
.editor__blocknote-container .bn-editor a {
|
||||
color: var(--inline-styles-link-color);
|
||||
text-decoration: var(--inline-styles-link-text-decoration);
|
||||
}
|
||||
|
||||
/* --- Wikilinks (override from Editor.css with theme vars) --- */
|
||||
.editor__blocknote-container .wikilink {
|
||||
color: var(--inline-styles-wikilink-color);
|
||||
text-decoration: var(--inline-styles-wikilink-text-decoration);
|
||||
border-bottom: var(--inline-styles-wikilink-border-bottom);
|
||||
cursor: var(--inline-styles-wikilink-cursor);
|
||||
}
|
||||
|
||||
/* --- Code blocks --- */
|
||||
.editor__blocknote-container [data-content-type="codeBlock"] {
|
||||
font-family: var(--code-blocks-font-family);
|
||||
font-size: var(--code-blocks-font-size);
|
||||
line-height: var(--code-blocks-line-height);
|
||||
background-color: var(--code-blocks-background-color);
|
||||
padding: var(--code-blocks-padding-vertical) var(--code-blocks-padding-horizontal);
|
||||
border-radius: var(--code-blocks-border-radius);
|
||||
margin-top: var(--code-blocks-margin-vertical);
|
||||
margin-bottom: var(--code-blocks-margin-vertical);
|
||||
}
|
||||
|
||||
/* --- Blockquote --- */
|
||||
.editor__blocknote-container [data-content-type="blockquote"],
|
||||
.editor__blocknote-container blockquote {
|
||||
border-left: var(--blockquote-border-left-width) solid var(--blockquote-border-left-color);
|
||||
padding-left: var(--blockquote-padding-left);
|
||||
margin-top: var(--blockquote-margin-vertical);
|
||||
margin-bottom: var(--blockquote-margin-vertical);
|
||||
color: var(--blockquote-color);
|
||||
font-style: var(--blockquote-font-style);
|
||||
}
|
||||
|
||||
/* --- Table --- */
|
||||
.editor__blocknote-container [data-content-type="table"] table {
|
||||
border-color: var(--table-border-color);
|
||||
font-size: var(--table-font-size);
|
||||
}
|
||||
|
||||
.editor__blocknote-container [data-content-type="table"] th {
|
||||
background: var(--table-header-background);
|
||||
padding: var(--table-cell-padding-vertical) var(--table-cell-padding-horizontal);
|
||||
}
|
||||
|
||||
.editor__blocknote-container [data-content-type="table"] td {
|
||||
padding: var(--table-cell-padding-vertical) var(--table-cell-padding-horizontal);
|
||||
border-color: var(--table-border-color);
|
||||
}
|
||||
|
||||
/* --- Horizontal rule --- */
|
||||
.editor__blocknote-container hr {
|
||||
border: none;
|
||||
border-top: var(--horizontal-rule-thickness) solid var(--horizontal-rule-color);
|
||||
margin: var(--horizontal-rule-margin-vertical) 0;
|
||||
}
|
||||
51
src/hooks/useTheme.ts
Normal file
51
src/hooks/useTheme.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useMemo } from 'react'
|
||||
import themeConfig from '../theme.json'
|
||||
|
||||
type ThemeValue = string | number | Record<string, unknown> | unknown[]
|
||||
|
||||
/** Convert a nested theme config object into a flat map of CSS custom properties */
|
||||
function flattenTheme(
|
||||
obj: Record<string, ThemeValue>,
|
||||
prefix = '--'
|
||||
): Record<string, string> {
|
||||
const result: Record<string, string> = {}
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const cssKey = `${prefix}${camelToKebab(key)}`
|
||||
|
||||
if (value === null || value === undefined) continue
|
||||
if (Array.isArray(value)) continue // skip arrays (e.g. nestedBulletSymbols)
|
||||
|
||||
if (typeof value === 'object') {
|
||||
Object.assign(result, flattenTheme(value as Record<string, ThemeValue>, `${cssKey}-`))
|
||||
} else if (typeof value === 'number') {
|
||||
// Numbers that look like px values get 'px' suffix; ratios/weights don't
|
||||
// These are unitless values; everything else gets 'px'
|
||||
const isUnitless = /weight|lineHeight|opacity/i.test(key) ||
|
||||
cssKey.includes('line-height') ||
|
||||
cssKey.includes('font-weight')
|
||||
const needsPx = !isUnitless
|
||||
result[cssKey] = needsPx ? `${value}px` : String(value)
|
||||
} else {
|
||||
result[cssKey] = String(value)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function camelToKebab(str: string): string {
|
||||
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
|
||||
}
|
||||
|
||||
export function useEditorTheme() {
|
||||
const { cssVars, styleString } = useMemo(() => {
|
||||
const vars = flattenTheme(themeConfig as Record<string, ThemeValue>)
|
||||
const str = Object.entries(vars)
|
||||
.map(([k, v]) => `${k}: ${v};`)
|
||||
.join('\n')
|
||||
return { cssVars: vars, styleString: str }
|
||||
}, [])
|
||||
|
||||
return { themeConfig, cssVars, styleString }
|
||||
}
|
||||
137
src/theme.json
Normal file
137
src/theme.json
Normal file
@@ -0,0 +1,137 @@
|
||||
{
|
||||
"editor": {
|
||||
"fontFamily": "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
|
||||
"fontSize": 16,
|
||||
"lineHeight": 1.7,
|
||||
"maxWidth": 720,
|
||||
"paddingHorizontal": 40,
|
||||
"paddingVertical": 20,
|
||||
"paragraphSpacing": 8
|
||||
},
|
||||
"headings": {
|
||||
"h1": {
|
||||
"fontSize": 32,
|
||||
"fontWeight": 700,
|
||||
"lineHeight": 1.2,
|
||||
"marginTop": 32,
|
||||
"marginBottom": 12,
|
||||
"color": "var(--text-heading)",
|
||||
"letterSpacing": -0.5
|
||||
},
|
||||
"h2": {
|
||||
"fontSize": 24,
|
||||
"fontWeight": 600,
|
||||
"lineHeight": 1.3,
|
||||
"marginTop": 28,
|
||||
"marginBottom": 10,
|
||||
"color": "var(--text-heading)",
|
||||
"letterSpacing": -0.3
|
||||
},
|
||||
"h3": {
|
||||
"fontSize": 20,
|
||||
"fontWeight": 600,
|
||||
"lineHeight": 1.4,
|
||||
"marginTop": 24,
|
||||
"marginBottom": 8,
|
||||
"color": "var(--text-heading)",
|
||||
"letterSpacing": 0
|
||||
},
|
||||
"h4": {
|
||||
"fontSize": 17,
|
||||
"fontWeight": 600,
|
||||
"lineHeight": 1.4,
|
||||
"marginTop": 20,
|
||||
"marginBottom": 6,
|
||||
"color": "var(--text-heading)",
|
||||
"letterSpacing": 0
|
||||
}
|
||||
},
|
||||
"lists": {
|
||||
"bulletSymbol": "•",
|
||||
"bulletSize": 14,
|
||||
"bulletColor": "var(--text-tertiary)",
|
||||
"indentSize": 24,
|
||||
"itemSpacing": 4,
|
||||
"paddingLeft": 8,
|
||||
"nestedBulletSymbols": ["•", "◦", "▪"]
|
||||
},
|
||||
"checkboxes": {
|
||||
"size": 16,
|
||||
"borderRadius": 3,
|
||||
"checkedColor": "var(--accent-blue)",
|
||||
"uncheckedBorderColor": "var(--text-muted)"
|
||||
},
|
||||
"inlineStyles": {
|
||||
"bold": {
|
||||
"fontWeight": 700,
|
||||
"color": "var(--text-primary)"
|
||||
},
|
||||
"italic": {
|
||||
"fontStyle": "italic",
|
||||
"color": "var(--text-primary)"
|
||||
},
|
||||
"strikethrough": {
|
||||
"color": "var(--text-tertiary)",
|
||||
"textDecoration": "line-through"
|
||||
},
|
||||
"code": {
|
||||
"fontFamily": "'SF Mono', 'Fira Code', monospace",
|
||||
"fontSize": 14,
|
||||
"backgroundColor": "var(--bg-hover-subtle)",
|
||||
"paddingHorizontal": 4,
|
||||
"paddingVertical": 2,
|
||||
"borderRadius": 3,
|
||||
"color": "var(--text-secondary)"
|
||||
},
|
||||
"link": {
|
||||
"color": "var(--accent-blue)",
|
||||
"textDecoration": "underline"
|
||||
},
|
||||
"wikilink": {
|
||||
"color": "var(--accent-blue)",
|
||||
"textDecoration": "none",
|
||||
"borderBottom": "1px dotted var(--accent-blue)",
|
||||
"cursor": "pointer"
|
||||
}
|
||||
},
|
||||
"codeBlocks": {
|
||||
"fontFamily": "'SF Mono', 'Fira Code', monospace",
|
||||
"fontSize": 13,
|
||||
"lineHeight": 1.5,
|
||||
"backgroundColor": "var(--bg-card)",
|
||||
"paddingHorizontal": 16,
|
||||
"paddingVertical": 12,
|
||||
"borderRadius": 6,
|
||||
"marginVertical": 12
|
||||
},
|
||||
"blockquote": {
|
||||
"borderLeftWidth": 3,
|
||||
"borderLeftColor": "var(--accent-blue)",
|
||||
"paddingLeft": 16,
|
||||
"marginVertical": 12,
|
||||
"color": "var(--text-secondary)",
|
||||
"fontStyle": "italic"
|
||||
},
|
||||
"table": {
|
||||
"borderColor": "var(--border-primary)",
|
||||
"headerBackground": "var(--bg-card)",
|
||||
"cellPaddingHorizontal": 12,
|
||||
"cellPaddingVertical": 8,
|
||||
"fontSize": 14
|
||||
},
|
||||
"horizontalRule": {
|
||||
"color": "var(--border-primary)",
|
||||
"marginVertical": 24,
|
||||
"thickness": 1
|
||||
},
|
||||
"colors": {
|
||||
"background": "var(--bg-primary)",
|
||||
"text": "var(--text-primary)",
|
||||
"textSecondary": "var(--text-secondary)",
|
||||
"textMuted": "var(--text-muted)",
|
||||
"heading": "var(--text-heading)",
|
||||
"accent": "var(--accent-blue)",
|
||||
"selection": "var(--bg-selected)",
|
||||
"cursor": "var(--text-primary)"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user