summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.demo/EDITOR-README.md109
-rw-r--r--.demo/example-editor.html58
-rw-r--r--esbuild.config.js24
-rw-r--r--package.json2
-rw-r--r--src/css/style.css8
-rw-r--r--src/index.html2
-rw-r--r--src/ts/editor-standalone.ts74
-rw-r--r--src/ts/editor.ts108
-rw-r--r--src/ts/script.ts113
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>&copy; 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();
}