diff options
| -rw-r--r-- | .demo/EDITOR-README.md | 109 | ||||
| -rw-r--r-- | .demo/example-editor.html | 58 | ||||
| -rw-r--r-- | esbuild.config.js | 24 | ||||
| -rw-r--r-- | package.json | 2 | ||||
| -rw-r--r-- | src/css/style.css | 8 | ||||
| -rw-r--r-- | src/index.html | 2 | ||||
| -rw-r--r-- | src/ts/editor-standalone.ts | 74 | ||||
| -rw-r--r-- | src/ts/editor.ts | 108 | ||||
| -rw-r--r-- | src/ts/script.ts | 113 |
9 files changed, 328 insertions, 170 deletions
diff --git a/.demo/EDITOR-README.md b/.demo/EDITOR-README.md new file mode 100644 index 0000000..9195fe2 --- /dev/null +++ b/.demo/EDITOR-README.md @@ -0,0 +1,109 @@ +# Adelie Editor - Standalone Usage + +The Adelie Editor is a vim-enabled CodeMirror editor with a retro aesthetic, syntax highlighting, and theme support. + +## Quick Start + +### 1. Include the files + +```html +<!-- CSS (required for styling) --> +<link rel="stylesheet" href="https://beta.adelie.liz.coffee/bundle.css" /> + +<!-- Editor JS --> +<script src="https://beta.adelie.liz.coffee/adelie-editor.js"></script> +``` + +### 2. Add a container element + +```html +<div id="my-editor"></div> +``` + +### 3. Initialize the editor + +```html +<script> + adelieEditor.init('#my-editor', { + language: 'javascript', + initialCode: '// Your code here' + }); +</script> +``` + +## API Reference + +### `adelieEditor.init(element, options)` + +Initialize an editor in the specified element. + +**Parameters:** +- `element` (string | HTMLElement) - Container element or CSS selector +- `options` (optional): + - `language` ('javascript' | 'css' | 'html') - Programming language (default: 'javascript') + - `initialCode` (string) - Initial code content (default: '') + - `theme` ('light' | 'dark') - Color theme (default: auto-detected from `data-theme` attribute) + +**Returns:** EditorView instance + +**Example:** +```javascript +const editor = adelieEditor.init('#my-editor', { + language: 'css', + initialCode: '.my-class { color: red; }' +}); +``` + +### Theme Switching + +The editor automatically detects theme changes on the `<html>` element: + +```javascript +// Switch to dark mode +document.documentElement.setAttribute('data-theme', 'dark'); + +// Switch to light mode +document.documentElement.removeAttribute('data-theme'); +``` + +The editor will automatically update its theme when the `data-theme` attribute changes. + +## Features + +- **Vim Mode**: Full vim keybindings (enabled by default) +- **Relative Line Numbers**: Vim-style relative numbering +- **Auto-closing Brackets**: Automatically closes `()`, `{}`, `[]`, `""`, `''` +- **Whitespace Indicators**: Shows dots for spaces/tabs when text is selected +- **Syntax Highlighting**: Matches Prism.js colors for consistency +- **Theme Support**: Light and dark themes with retro styling + +## Complete Example + +See `example-editor.html` for a working example: + +```html +<!DOCTYPE html> +<html lang="en"> +<head> + <link rel="stylesheet" href="dist/bundle.css" /> +</head> +<body> + <div id="my-editor"></div> + + <script src="dist/adelie-editor.js"></script> + <script> + adelieEditor.init('#my-editor', { + language: 'javascript', + initialCode: 'console.log("Hello World!");' + }); + </script> +</body> +</html> +``` + +## File Size + +- **bundle.css**: ~23 KB (minified) +- **adelie-editor.js**: ~596 KB (minified, includes CodeMirror + vim mode) + +The editor bundle is self-contained and includes all dependencies. diff --git a/.demo/example-editor.html b/.demo/example-editor.html new file mode 100644 index 0000000..f6505bc --- /dev/null +++ b/.demo/example-editor.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Adelie Editor - Standalone Example</title> + <link rel="stylesheet" href="dist/bundle.css" /> +</head> +<body> + <header> + <h1>Adelie Editor Standalone Example</h1> + <input type="checkbox" id="theme-toggle" class="toggle" aria-label="Toggle dark mode" /> + </header> + + <main style="max-width: 1000px; margin: 0 auto; padding: 2rem;"> + <article> + <h2>Simple Usage</h2> + <div id="my-editor"></div> + </article> + + <article style="margin-top: 3rem;"> + <h2>With Options</h2> + <div id="css-editor"></div> + </article> + </main> + + <!-- Load the standalone editor bundle --> + <script src="dist/adelie-editor.js"></script> + + <script> + // Simple initialization + const editor1 = adelieEditor.init('#my-editor', { + language: 'javascript', + initialCode: `// Type your code here +function hello() { + console.log("Hello from Adelie Editor!"); +}` + }); + + // With custom options + const editor2 = adelieEditor.init('#css-editor', { + language: 'css', + initialCode: `/* Adelie Editor with CSS */ +.my-class { + color: var(--primary); + background: var(--surface); +}` + }); + + // Theme toggle + const toggle = document.getElementById('theme-toggle'); + toggle.addEventListener('change', () => { + const theme = toggle.checked ? 'dark' : 'light'; + document.documentElement.setAttribute('data-theme', theme); + }); + </script> +</body> +</html> diff --git a/esbuild.config.js b/esbuild.config.js index 9d1c3df..ce5a36b 100644 --- a/esbuild.config.js +++ b/esbuild.config.js @@ -7,9 +7,12 @@ const paths = { distDir: 'dist', assetsDir: 'src/assets', jsEntry: 'src/ts/script.ts', + editorEntry: 'src/ts/editor-standalone.ts', cssEntry: 'src/css/style.css', componentsHtml: 'src/index.html', demoHtml: '.demo/index.html', + outJs: 'dist/bundle.js', + outEditor: 'dist/adelie-editor.js', outCss: 'dist/bundle.css', outComponentsHtml: 'dist/index.html', outDemoHtml: 'dist/demo.html', @@ -24,13 +27,24 @@ function buildJavaScript() { return esbuild.build({ entryPoints: [paths.jsEntry], bundle: true, - splitting: true, - format: 'esm', minify: isProduction, sourcemap: true, target: 'es2020', - outdir: paths.distDir, - chunkNames: 'chunks/[name]-[hash]', + outfile: paths.outJs, + logLevel: 'info', + }); +} + +function buildEditor() { + return esbuild.build({ + entryPoints: [paths.editorEntry], + bundle: true, + minify: isProduction, + sourcemap: true, + target: 'es2020', + format: 'iife', + globalName: 'adelieEditor', + outfile: paths.outEditor, logLevel: 'info', }); } @@ -69,7 +83,7 @@ async function copyHtml() { async function build() { await cleanDist(); - await Promise.all([buildJavaScript(), buildCss()]); + await Promise.all([buildJavaScript(), buildEditor(), buildCss()]); await copyAssets(); await copyHtml(); } diff --git a/package.json b/package.json index 097cc1e..577d78d 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "build": "NODE_ENV=production node esbuild.config.js", "dev": "node esbuild.config.js", "clean": "rm -rf dist", - "serve": "npx http-server dist -p 8080 -o", + "serve": "npx http-server dist -p 9000 -o", "dev:serve": "npm run dev && npm run serve", "lint": "biome lint src", "format": "biome format --write src", diff --git a/src/css/style.css b/src/css/style.css index 48d964e..4c83cb0 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -635,8 +635,8 @@ nav a:active { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; - box-shadow: inset 1px 1px 0 var(--border-light), inset -1px -1px 0 var(--border-dark), 2px 2px 0 - rgba(0, 0, 0, 0.12); + box-shadow: inset 1px 1px 0 var(--border-light), inset -1px -1px 0 var(--border-dark), 2px + 2px 0 rgba(0, 0, 0, 0.12); } th, @@ -646,8 +646,8 @@ nav a:active { pre { padding: var(--space-sm); - box-shadow: inset 1px 1px 0 var(--border-light), inset -1px -1px 0 var(--border-dark), 2px 2px 0 - rgba(0, 0, 0, 0.12); + box-shadow: inset 1px 1px 0 var(--border-light), inset -1px -1px 0 var(--border-dark), 2px + 2px 0 rgba(0, 0, 0, 0.12); } } diff --git a/src/index.html b/src/index.html index 511e113..36590ff 100644 --- a/src/index.html +++ b/src/index.html @@ -537,7 +537,7 @@ retro(); // "That's totally rad!"</code></pre> </main> <footer> - <p>© 2025 Liz CSS Framework. Made with coffee and retro vibes.</p> + <p>hai</p> </footer> <script type="module" src="/script.js"></script> diff --git a/src/ts/editor-standalone.ts b/src/ts/editor-standalone.ts new file mode 100644 index 0000000..2c8ec81 --- /dev/null +++ b/src/ts/editor-standalone.ts @@ -0,0 +1,74 @@ +import { createEditor, setEditorTheme } from './editor'; + +// Global API +const adelieEditor = { + /** + * Initialize an editor in the given element + * @param element - The container element or selector + * @param options - Editor configuration + */ + init( + element: HTMLElement | string, + options?: { + language?: 'javascript' | 'css' | 'html'; + initialCode?: string; + theme?: 'light' | 'dark'; + } + ) { + const container = + typeof element === 'string' ? document.querySelector<HTMLElement>(element) : element; + + if (!container) { + throw new Error('Editor container not found'); + } + + // Auto-detect theme if not specified + const theme = + options?.theme || + (document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light'); + + const view = createEditor({ + parent: container, + language: options?.language || 'javascript', + initialCode: options?.initialCode || '', + theme, + }); + + // Listen for theme changes + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.attributeName === 'data-theme') { + const newTheme = + document.documentElement.getAttribute('data-theme') === 'dark' + ? 'dark' + : 'light'; + setEditorTheme(view, newTheme); + } + }); + }); + + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['data-theme'], + }); + + return view; + }, + + /** + * Create an editor with full control + */ + create: createEditor, + + /** + * Change editor theme + */ + setTheme: setEditorTheme, +}; + +// Export to window +if (typeof window !== 'undefined') { + (window as any).adelieEditor = adelieEditor; +} + +export default adelieEditor; diff --git a/src/ts/editor.ts b/src/ts/editor.ts index 2d2a3da..ebc6b9b 100644 --- a/src/ts/editor.ts +++ b/src/ts/editor.ts @@ -61,7 +61,10 @@ function highlightWhitespace() { // Relative line numbers that update on cursor movement class RelativeLineNumberMarker extends GutterMarker { - constructor(private lineNo: number, private currentLine: number) { + constructor( + private lineNo: number, + private currentLine: number + ) { super(); } @@ -184,50 +187,53 @@ const lightTheme = EditorView.theme({ }, }); -const darkTheme = EditorView.theme({ - '&': { - backgroundColor: 'var(--surface)', - color: 'var(--text)', - }, - '.cm-content': { - caretColor: 'var(--primary)', - fontFamily: 'var(--font-mono)', - fontSize: '0.875rem', - paddingLeft: '0', - }, - '.cm-gutters': { - backgroundColor: 'var(--bg)', - color: 'var(--muted)', - border: 'none', - borderRight: '1px solid var(--border)', - }, - '.cm-activeLineGutter': { - backgroundColor: 'rgba(229, 106, 166, 0.2)', - color: 'var(--text)', - }, - '.cm-activeLine': { - backgroundColor: 'rgba(240, 106, 166, 0.08)', - }, - '.cm-selectionBackground': { - backgroundColor: 'rgba(240, 106, 166, 0.45) !important', - }, - '&.cm-focused .cm-selectionBackground': { - backgroundColor: 'rgba(240, 106, 166, 0.45) !important', - }, - '&.cm-focused .cm-selectionMatch': { - backgroundColor: 'rgba(182, 156, 255, 0.35) !important', - }, - '.cm-cursor': { - borderLeftColor: 'var(--primary)', - }, - '.cm-whitespace::before': { - content: '"·"', - position: 'absolute', - opacity: '0.25', - color: 'var(--muted)', - pointerEvents: 'none', +const darkTheme = EditorView.theme( + { + '&': { + backgroundColor: 'var(--surface)', + color: 'var(--text)', + }, + '.cm-content': { + caretColor: 'var(--primary)', + fontFamily: 'var(--font-mono)', + fontSize: '0.875rem', + paddingLeft: '0', + }, + '.cm-gutters': { + backgroundColor: 'var(--bg)', + color: 'var(--muted)', + border: 'none', + borderRight: '1px solid var(--border)', + }, + '.cm-activeLineGutter': { + backgroundColor: 'rgba(229, 106, 166, 0.2)', + color: 'var(--text)', + }, + '.cm-activeLine': { + backgroundColor: 'rgba(240, 106, 166, 0.08)', + }, + '.cm-selectionBackground': { + backgroundColor: 'rgba(240, 106, 166, 0.45) !important', + }, + '&.cm-focused .cm-selectionBackground': { + backgroundColor: 'rgba(240, 106, 166, 0.45) !important', + }, + '&.cm-focused .cm-selectionMatch': { + backgroundColor: 'rgba(182, 156, 255, 0.35) !important', + }, + '.cm-cursor': { + borderLeftColor: 'var(--primary)', + }, + '.cm-whitespace::before': { + content: '"·"', + position: 'absolute', + opacity: '0.25', + color: 'var(--muted)', + pointerEvents: 'none', + }, }, -}, { dark: true }); + { dark: true } +); interface EditorOptions { parent: HTMLElement; @@ -254,9 +260,10 @@ export function createEditor(options: EditorOptions): EditorView { highlightWhitespace(), vim(), languageExtension(), - themeConfig.of(theme === 'dark' - ? [darkTheme, syntaxHighlighting(darkHighlighting)] - : [lightTheme, syntaxHighlighting(lightHighlighting)] + themeConfig.of( + theme === 'dark' + ? [darkTheme, syntaxHighlighting(darkHighlighting)] + : [lightTheme, syntaxHighlighting(lightHighlighting)] ), ], }); @@ -271,9 +278,10 @@ export function createEditor(options: EditorOptions): EditorView { export function setEditorTheme(view: EditorView, theme: 'light' | 'dark'): void { view.dispatch({ - effects: themeConfig.reconfigure(theme === 'dark' - ? [darkTheme, syntaxHighlighting(darkHighlighting)] - : [lightTheme, syntaxHighlighting(lightHighlighting)] + effects: themeConfig.reconfigure( + theme === 'dark' + ? [darkTheme, syntaxHighlighting(darkHighlighting)] + : [lightTheme, syntaxHighlighting(lightHighlighting)] ), }); } diff --git a/src/ts/script.ts b/src/ts/script.ts index e8502c8..8aff107 100644 --- a/src/ts/script.ts +++ b/src/ts/script.ts @@ -3,15 +3,15 @@ import 'prismjs/components/prism-css'; import 'prismjs/components/prism-javascript'; import 'prismjs/components/prism-markup'; import { initOneko } from './oneko'; +import { createEditor, setEditorTheme } from './editor'; +import type { EditorView } from 'codemirror'; type Theme = 'light' | 'dark'; -type EditorView = any; const THEME_STORAGE_KEY = 'theme'; const DARK_THEME_ATTRIBUTE = 'data-theme'; let editorInstance: EditorView | null = null; -let editorModule: typeof import('./editor') | null = null; function detectAssetBase(): string { const currentScript = document.currentScript as HTMLScriptElement | null; @@ -63,8 +63,8 @@ function initThemeToggle(): void { applyTheme(nextTheme); sessionStorage.setItem(THEME_STORAGE_KEY, nextTheme); - if (editorInstance && editorModule) { - editorModule.setEditorTheme(editorInstance, nextTheme); + if (editorInstance) { + setEditorTheme(editorInstance, nextTheme); } }); } @@ -150,116 +150,11 @@ function initFileInputs(): void { }); } -async function loadEditor() { - if (!editorModule) { - editorModule = await import('./editor'); - } - return editorModule; -} - -function initCodeEditor(): void { - const editorContainer = document.getElementById('code-editor'); - const languageSelect = document.getElementById('language-select'); - - if (!editorContainer || !(languageSelect instanceof HTMLSelectElement)) return; - - const initialCode = { - javascript: `// Welcome to the live code editor! -function greet(name) { - return \`Hello, \${name}! 👋\`; -} - -console.log(greet('World'));`, - css: `/* Try editing this CSS! */ -.retro-box { - background: var(--primary); - color: var(--text); - padding: var(--space-md); - border-radius: var(--border-radius); - box-shadow: var(--shadow-box); -}`, - html: `<!-- Try editing this HTML! --> -<!DOCTYPE html> -<html lang="en"> -<head> - <meta charset="UTF-8"> - <title>Retro Page</title> -</head> -<body> - <h1>Hello, World!</h1> - <p>Welcome to the retro web.</p> -</body> -</html>`, - }; - - let editorLoaded = false; - - const currentTheme = getStoredTheme() ?? getSystemTheme(); - - async function createEditorInstance() { - if (editorLoaded) return; - editorLoaded = true; - - const { createEditor } = await loadEditor(); - const language = languageSelect.value as 'javascript' | 'css' | 'html'; - - editorInstance = createEditor({ - parent: editorContainer, - language, - initialCode: initialCode[language], - theme: currentTheme, - }); - } - - // Lazy load on intersection (when editor scrolls into view) - const observer = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - createEditorInstance(); - observer.disconnect(); - } - }); - }, - { rootMargin: '100px' } - ); - - observer.observe(editorContainer); - - // Also load on language change - languageSelect.addEventListener('change', async () => { - const language = languageSelect.value as 'javascript' | 'css' | 'html'; - - if (!editorModule) { - await createEditorInstance(); - return; - } - - if (editorInstance && editorContainer) { - editorContainer.innerHTML = ''; - - const { createEditor } = editorModule; - editorInstance = createEditor({ - parent: editorContainer, - language, - initialCode: initialCode[language], - theme: currentTheme, - }); - } - }); -} - function init(): void { setAssetBase(); initThemeToggle(); initFairyDust(); initFileInputs(); - - // Only initialize code editor if the container exists - if (document.getElementById('code-editor')) { - initCodeEditor(); - } - Prism.highlightAll(); initOneko(); } |
