freemd 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md ADDED
@@ -0,0 +1,37 @@
1
+ # AGENTS.md — freemd
2
+
3
+ Markdown notes editor: Node/Express CLI (`bin/openmd.js`, intended use `npx freemd`) serving a Svelte 5 + Vite frontend from `dist/`. Notes live in `~/freemd`, created on boot (override: `--dir` / `FREEMD_DIR`); a legacy `~/openmd` folder is auto-renamed to `~/freemd` on first boot when the latter doesn't exist.
4
+
5
+ ## Commands
6
+
7
+ - **Use pnpm, never npm** (project constraint).
8
+ - `pnpm build` — Vite build → `dist/`. Required before `pnpm start` and before publishing: the server only serves static files from `dist/` and returns an error page without it.
9
+ - `pnpm dev:server` — API server on `:5813` with `--no-open`. Pair with `pnpm dev` (Vite on `:5174`, proxies `/api` and `/media` to `:5813`).
10
+ - No test suite or linter. Verify changes with `pnpm build` plus manual curl calls against a scratch root: `node bin/openmd.js --dir <tmp> --port 5899 --no-open`. To eyeball exported PDF/docx on macOS: `qlmanage -t -s 900 -o <dir> <file>`.
11
+ - pnpm v11 blocks esbuild's postinstall; `pnpm-workspace.yaml` whitelists it (`onlyBuiltDependencies`). If a fresh install fails with `ERR_PNPM_IGNORED_BUILDS`, run `pnpm approve-builds --all`.
12
+
13
+ ## Data model (core invariant)
14
+
15
+ - A **note** is a directory ending in `-md` containing `index.md` plus its resources (images, attachments). Directories without `-md` are plain folders.
16
+ - `/api/tree` presents notes as files: `name: "nota.md"`, `path: "nota-md/index.md"`, and `dir: "nota-md"` (the folder). Delete/rename/upload-targeting must use `dir` for notes, `path` for everything else. `index.md` is never shown to the user.
17
+ - `slug()` keeps Unicode letters — folder names can contain accents (`bitácora-md`).
18
+
19
+ ## Server invariants (`server/`)
20
+
21
+ - Only `bin/`, `server/`, `dist/` publish (`files` in package.json). Server code ships as plain ESM, unbundled — run `pnpm build` before `pnpm publish`.
22
+ - `PUT /api/file` must NOT `mkdir` parents: saving to a renamed/deleted path must 404, otherwise an in-flight autosave resurrects the old note folder.
23
+ - `GET /api/file` self-heals a missing `index.md` inside a `*-md` folder by creating it with the note title.
24
+ - `POST /api/folder` with a name ending in `-md` must also create that note's `index.md`.
25
+ - Visible names/crumbs come from `fileLabels()`; export titles follow the same rule so `recetas-md/index.md` exports as `recetas.pdf` / `recetas.docx` (never `index.pdf`).
26
+ - Every endpoint taking a user path goes through `safeJoin()` (traversal guard). Resources are served via `/media/`.
27
+
28
+ ## Frontend (`src/`)
29
+
30
+ - Svelte 5 **runes** (`$state`, `$derived`, `$props`, `$bindable`, `$effect`) — no Svelte 4 `export let` or stores.
31
+ - `src/lib/markdown.js` resolves relative image srcs against the note dir (`env.base`) into `/media/...` URLs; the resources panel inserts URL-encoded filenames (`mi%20foto.png`).
32
+ - App theme is One Dark (CSS variables in `src/app.css`); exports are deliberately light-themed (atom-one-light palette in `server/export/highlight.js`).
33
+
34
+ ## pdfkit gotchas (`server/export/to-pdf.js`)
35
+
36
+ - `.text()` ignores a `color` option: call `doc.fillColor()` before every run, or text inherits the last fill state (this caused invisible "ghost" headings after code blocks).
37
+ - `.text(str, x, y)` mutates `doc.x`; reset it to the margin after code blocks, or the next block starts at the last code segment's x.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 openmd contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.es.md ADDED
@@ -0,0 +1,95 @@
1
+ # freemd
2
+
3
+ [English](./README.md) · **Español**
4
+
5
+ Una app de notas en markdown *local-first* que vive en tu máquina. Un comando y estás editando notas en tu navegador — interfaz dark limpia, vista previa en vivo, imágenes por nota y exportación a PDF o Word en un click.
6
+
7
+ ```bash
8
+ npx freemd
9
+ ```
10
+
11
+ Eso es todo: crea `~/freemd` si no existe, levanta un servidor local en [http://localhost:5813](http://localhost:5813) y abre tu navegador.
12
+
13
+ ## Por qué freemd
14
+
15
+ - **Tus notas son archivos simples.** Todo es markdown en tu propio disco: sin cuentas, sin nube, sin ataduras. Puedes respaldarlas, sincronizarlas o buscarlas con grep.
16
+ - **Las notas llevan sus propias imágenes.** Cada nota es una carpeta, así que capturas y adjuntos viajan con la nota y siguen funcionando si la mueves.
17
+ - **Pensada para escribir.** Editor sin distracciones, vista renderizada, o ambas en paralelo.
18
+ - **Exportación con buena pinta.** PDF y `.docx` con tema claro, código resaltado e imágenes incrustadas.
19
+
20
+ ## Requisitos
21
+
22
+ Node.js 18 o superior. Nada más.
23
+
24
+ ## Uso
25
+
26
+ ```bash
27
+ npx freemd [opciones]
28
+ ```
29
+
30
+ | Opción | Descripción |
31
+ | --- | --- |
32
+ | `-p, --port <n>` | Puerto del servidor (por defecto `5813`) |
33
+ | `-d, --dir <ruta>` | Carpeta de notas (por defecto `~/freemd`) |
34
+ | `-H, --host <addr>` | Escuchar en otra interfaz; sin valor usa `0.0.0.0` (toda la red local) |
35
+ | `--no-open` | No abrir el navegador automáticamente |
36
+ | `-v, --version` | Mostrar versión |
37
+ | `-h, --help` | Mostrar ayuda |
38
+
39
+ También funcionan las variables de entorno `FREEMD_PORT`, `FREEMD_DIR` y `FREEMD_HOST`.
40
+
41
+ ### Acceder desde otros dispositivos
42
+
43
+ ```bash
44
+ npx freemd --host
45
+ ```
46
+
47
+ Expone la app en tu red local e imprime la URL de cada interfaz de red, p. ej. `http://192.168.1.20:5813` — ábrela desde tu teléfono, tablet u otro ordenador.
48
+
49
+ > ⚠️ No hay autenticación. Cualquier equipo de tu red puede acceder a la app, así que usa `--host` solo en redes de confianza.
50
+
51
+ ## Cómo se guardan tus notas
52
+
53
+ Todo vive en una carpeta (por defecto `~/freemd`):
54
+
55
+ ```
56
+ ~/freemd
57
+ ├── viaje-a-japon-md
58
+ │ ├── index.md ← la nota en sí
59
+ │ ├── dia-1.png ← sus imágenes y adjuntos
60
+ │ └── recibo.pdf
61
+ ├── recetas-md
62
+ │ └── index.md
63
+ ├── trabajo ← una carpeta normal (organiza como quieras)
64
+ │ └── notas-md
65
+ │ └── index.md
66
+ └── leeme.md ← los .md sueltos también funcionan
67
+ ```
68
+
69
+ - Una **nota** es una carpeta terminada en `-md` cuyo contenido vive en `index.md`. La carpeta `-md` es lo que mantiene las imágenes de la nota junto a su texto — en la app nunca verás la carpeta, solo `viaje-a-japon.md`.
70
+ - Referencia las imágenes con rutas relativas normales: `![mapa](./dia-1.png)`.
71
+
72
+ ## La app
73
+
74
+ - **Sidebar** — tus notas y carpetas. Al pasar el cursor sobre un elemento aparecen renombrar (✏️) y eliminar (🗑); los iconos de la cabecera crean notas, carpetas y suben adjuntos a la nota actual.
75
+ - **Editor con tres modos** — *Edición* (markdown crudo), *Split* (editor y vista renderizada lado a lado, con scroll sincronizado), *Vista* (solo preview). Los bloques de código llevan resaltado de sintaxis.
76
+ - **Panel de recursos** — panel plegable a la derecha con las imágenes de la nota actual en miniaturas. Haz click en una para insertar su markdown en el cursor; al pasar el cursor puedes eliminarla.
77
+ - **Pegar imágenes** — copia una captura o imagen y pulsa `Cmd/Ctrl + V` sobre la nota: se guarda en la carpeta de la nota y se inserta su markdown en el cursor.
78
+ - **Autoguardado** — los cambios se guardan solos 1.2 s después de dejar de escribir (o al instante con `Cmd/Ctrl + S`).
79
+ - **Exportar** — un click para descargar la nota como **PDF** o **Word (.docx)**: tema claro, código resaltado, imágenes incrustadas y con el nombre de la nota (`recetas.pdf`, nunca `index.pdf`). Si hay cambios sin guardar, se guardan primero.
80
+
81
+ ## Desarrollo
82
+
83
+ ```bash
84
+ pnpm install
85
+ pnpm dev:server # servidor API en :5813 (sin navegador)
86
+ pnpm dev # servidor Vite en :5174 con HMR
87
+ pnpm build # build de producción → dist/
88
+ pnpm start # servir el build de producción
89
+ ```
90
+
91
+ Construido con **Svelte 5** y **Vite** en el front, **Node + Express** puro en el back. El markdown se renderiza con **markdown-it** y **highlight.js**; las exportaciones con **pdfkit** y **docx**. Las notas de arquitectura están en [`AGENTS.md`](./AGENTS.md).
92
+
93
+ ## Licencia
94
+
95
+ [MIT](./LICENSE)
package/README.md CHANGED
@@ -1,3 +1,95 @@
1
- # Temporary Holding Version
1
+ # freemd
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **English** · [Español](./README.es.md)
4
+
5
+ A local-first markdown notes app that lives on your machine. One command and you're editing notes in your browser — clean dark UI, live preview, per-note image attachments, and one-click export to PDF or Word.
6
+
7
+ ```bash
8
+ npx freemd
9
+ ```
10
+
11
+ That's it: it creates `~/freemd` if it doesn't exist, starts a local server at [http://localhost:5813](http://localhost:5813) and opens your browser.
12
+
13
+ ## Why freemd
14
+
15
+ - **Your notes are plain files.** Everything is markdown on your own disk, no accounts, no cloud, no lock-in. Back it up, sync it, grep it, own it.
16
+ - **Notes can carry their own images.** Each note is a folder, so screenshots and attachments travel with the note and keep working if you move it.
17
+ - **Built for writing.** Distraction-free editor, rendered preview, or both side by side.
18
+ - **Export that looks right.** Clean, light-themed PDF and `.docx` with syntax-highlighted code and embedded images.
19
+
20
+ ## Requirements
21
+
22
+ Node.js 18 or newer. That's all.
23
+
24
+ ## Usage
25
+
26
+ ```bash
27
+ npx freemd [options]
28
+ ```
29
+
30
+ | Option | Description |
31
+ | --- | --- |
32
+ | `-p, --port <n>` | Server port (default `5813`) |
33
+ | `-d, --dir <path>` | Notes folder (default `~/freemd`) |
34
+ | `-H, --host <addr>` | Listen on another interface; with no value binds `0.0.0.0` (whole local network) |
35
+ | `--no-open` | Don't open the browser automatically |
36
+ | `-v, --version` | Print version |
37
+ | `-h, --help` | Show help |
38
+
39
+ `FREEMD_PORT`, `FREEMD_DIR` and `FREEMD_HOST` environment variables work too.
40
+
41
+ ### Access from other devices
42
+
43
+ ```bash
44
+ npx freemd --host
45
+ ```
46
+
47
+ This exposes the app on your local network and prints the URL for each network interface, e.g. `http://192.168.1.20:5813` — open it from your phone, tablet or another computer.
48
+
49
+ > ⚠️ There is no authentication. Anything on your network can reach the app, so only use `--host` on networks you trust.
50
+
51
+ ## How your notes are stored
52
+
53
+ Everything lives in one folder (by default `~/freemd`):
54
+
55
+ ```
56
+ ~/freemd
57
+ ├── trip-to-japan-md
58
+ │ ├── index.md ← the note itself
59
+ │ ├── day-1.png ← its images and attachments
60
+ │ └── receipt.pdf
61
+ ├── recipes-md
62
+ │ └── index.md
63
+ ├── work ← a plain folder (organize as you like)
64
+ │ └── notes-md
65
+ │ └── index.md
66
+ └── readme.md ← loose .md files work too
67
+ ```
68
+
69
+ - A **note** is a folder ending in `-md` whose content lives in `index.md`. The `-md` folder is what keeps the note's images next to its text — you'll never see the folder in the app; you just see `trip-to-japan.md`.
70
+ - Reference images with normal relative paths: `![map](./day-1.png)`.
71
+
72
+ ## The app
73
+
74
+ - **Sidebar** — your notes and folders. Hover any item to rename (✏️) or delete (🗑); the toolbar icons create notes, folders and upload attachments to the current note.
75
+ - **Editor with three view modes** — *Edición* (raw markdown), *Split* (editor and rendered preview side by side, scroll-synced), *Vista* (rendered preview). Code blocks get syntax highlighting.
76
+ - **Resources panel** — a collapsible panel on the right showing the current note's images as thumbnails. Click one to insert its markdown at the cursor; hover to delete it.
77
+ - **Paste images** — copy a screenshot or image and press `Cmd/Ctrl + V` anywhere in the note: it's saved into the note's folder and its markdown inserted at the cursor.
78
+ - **Autosave** — changes save automatically 1.2 s after you stop typing (or instantly with `Cmd/Ctrl + S`).
79
+ - **Export** — one click to download the current note as **PDF** or **Word (.docx)**: light-themed, syntax-highlighted code, embedded images, named after the note (`recipes.pdf`, never `index.pdf`). Unsaved changes are saved first.
80
+
81
+ ## Development
82
+
83
+ ```bash
84
+ pnpm install
85
+ pnpm dev:server # API server on :5813 (no browser)
86
+ pnpm dev # Vite dev server on :5174 with HMR
87
+ pnpm build # production build → dist/
88
+ pnpm start # serve the production build
89
+ ```
90
+
91
+ Built with **Svelte 5** and **Vite** on the front, plain **Node + Express** on the back. Markdown rendering by **markdown-it** with **highlight.js**; exports by **pdfkit** and **docx**. See [`AGENTS.md`](./AGENTS.md) for architecture notes.
92
+
93
+ ## License
94
+
95
+ [MIT](./LICENSE)
package/bin/openmd.js ADDED
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import { start } from '../server/index.js';
5
+
6
+ const args = process.argv.slice(2);
7
+ const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
8
+
9
+ const hasFlag = (...flags) => args.some((arg) => flags.includes(arg));
10
+ const valueOf = (...flags) => {
11
+ for (const flag of flags) {
12
+ const index = args.indexOf(flag);
13
+ if (index !== -1) return args[index + 1];
14
+ }
15
+ return undefined;
16
+ };
17
+
18
+ if (hasFlag('-h', '--help')) {
19
+ console.log(`
20
+ freemd — editor de notas markdown en el navegador
21
+
22
+ Uso:
23
+ npx freemd [opciones]
24
+
25
+ Opciones:
26
+ -p, --port <n> Puerto del servidor (por defecto 5813)
27
+ -d, --dir <ruta> Carpeta de notas (por defecto ~/freemd)
28
+ -H, --host <addr> Escuchar en otra interfaz; sin valor usa 0.0.0.0 (toda la red local)
29
+ --no-open No abrir el navegador automáticamente
30
+ -v, --version Mostrar versión
31
+ -h, --help Mostrar esta ayuda
32
+ `);
33
+ process.exit(0);
34
+ }
35
+
36
+ if (hasFlag('-v', '--version')) {
37
+ console.log(pkg.version);
38
+ process.exit(0);
39
+ }
40
+
41
+ const portArg = valueOf('--port', '-p');
42
+ let port;
43
+ if (portArg === undefined) {
44
+ port = Number(process.env.FREEMD_PORT) || 5813;
45
+ } else {
46
+ port = Number(portArg);
47
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
48
+ console.error(`Puerto inválido: ${portArg}`);
49
+ process.exit(1);
50
+ }
51
+ }
52
+
53
+ const dir = valueOf('--dir', '-d') ?? process.env.FREEMD_DIR;
54
+ const open = !hasFlag('--no-open');
55
+
56
+ let hostArg = valueOf('--host', '-H');
57
+ if (hasFlag('--host', '-H') && (hostArg === undefined || hostArg.startsWith('-'))) {
58
+ hostArg = '0.0.0.0';
59
+ }
60
+ const host = hostArg ?? process.env.FREEMD_HOST;
61
+
62
+ try {
63
+ const { url, root, migrated } = await start({ port, dir, host, open });
64
+ console.log('');
65
+ console.log(` freemd ${pkg.version}`);
66
+ console.log(` notas ${root}`);
67
+ if (migrated) console.log(` migrada ${migrated} → ${root}`);
68
+ console.log(` listo ${url}`);
69
+ if (host === '0.0.0.0' || host === '::') {
70
+ for (const address of lanAddresses(port)) {
71
+ console.log(` red ${address}`);
72
+ }
73
+ }
74
+ console.log('');
75
+ } catch (error) {
76
+ if (error.code === 'EADDRINUSE') {
77
+ console.error(`El puerto ${port} ya está en uso. Prueba con --port <otro puerto>`);
78
+ } else {
79
+ console.error(error.message ?? error);
80
+ }
81
+ process.exit(1);
82
+ }
83
+
84
+ function lanAddresses(port) {
85
+ const urls = [];
86
+ for (const addresses of Object.values(os.networkInterfaces())) {
87
+ for (const entry of addresses ?? []) {
88
+ if (entry.family !== 'IPv4' || entry.internal) continue;
89
+ urls.push(`http://${entry.address}:${port}`);
90
+ }
91
+ }
92
+ return urls;
93
+ }
@@ -0,0 +1 @@
1
+ pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#abb2bf;background:#282c34}.hljs-comment,.hljs-quote{color:#5c6370;font-style:italic}.hljs-doctag,.hljs-keyword,.hljs-formula{color:#c678dd}.hljs-section,.hljs-name,.hljs-selector-tag,.hljs-deletion,.hljs-subst{color:#e06c75}.hljs-literal{color:#56b6c2}.hljs-string,.hljs-regexp,.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string{color:#98c379}.hljs-attr,.hljs-variable,.hljs-template-variable,.hljs-type,.hljs-selector-class,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-number{color:#d19a66}.hljs-symbol,.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-title{color:#61aeee}.hljs-built_in,.hljs-title.class_,.hljs-class .hljs-title{color:#e6c07b}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}:root{--bg: #282c34;--bg-darker: #21252b;--bg-darkest: #1b1e24;--bg-panel: #2c313a;--bg-hover: #333842;--border: #3a3f4b;--border-dark: #181a1f;--fg: #abb2bf;--fg-bright: #d7dae0;--fg-dim: #5c6370;--red: #e06c75;--orange: #d19a66;--yellow: #e5c07b;--green: #98c379;--cyan: #56b6c2;--blue: #61afef;--purple: #c678dd;--mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, "Liberation Mono", monospace}*{box-sizing:border-box}html,body,#app{height:100%;margin:0}body{background:var(--bg);color:var(--fg);font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:14px;-webkit-font-smoothing:antialiased}::selection{background:#61afef47}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border);border-radius:5px;border:2px solid transparent;background-clip:content-box}::-webkit-scrollbar-thumb:hover{background:var(--fg-dim);border:2px solid transparent;background-clip:content-box}button{font-family:inherit}.app{display:flex;height:100%;overflow:hidden}.main{flex:1;display:flex;flex-direction:column;min-width:0}.sidebar{width:280px;min-width:280px;display:flex;flex-direction:column;background:var(--bg-darker);border-right:1px solid var(--border-dark)}.side-head{display:flex;align-items:center;justify-content:space-between;height:48px;padding:0 10px 0 16px;border-bottom:1px solid var(--border-dark);flex-shrink:0}.brand{font-size:15px;font-weight:600;letter-spacing:.4px;color:var(--fg-bright)}.brand-md{color:var(--green)}.head-actions{display:flex;gap:2px}.icon-btn{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;padding:0;border:none;border-radius:6px;background:none;color:var(--fg-dim);cursor:pointer}.icon-btn:hover:not(:disabled){color:var(--fg-bright);background:var(--bg-panel)}.icon-btn:disabled{opacity:.35;cursor:default}.new-item{padding:10px;border-bottom:1px solid var(--border-dark)}.new-item input{width:100%;padding:7px 10px;font-size:13px;color:var(--fg-bright);background:var(--bg-darkest);border:1px solid var(--blue);border-radius:6px;outline:none}.new-item input::placeholder{color:var(--fg-dim)}.tree{flex:1;overflow-y:auto;padding:8px 6px}.tree-loading,.tree-empty{padding:16px 12px;font-size:12.5px;line-height:1.6;color:var(--fg-dim)}.item{display:flex;align-items:center;border-radius:6px}.item .row{flex:1;display:flex;align-items:center;gap:7px;min-width:0;padding:5px 8px;border:none;border-radius:6px;background:none;color:var(--fg);font-size:13px;text-align:left;cursor:pointer}.item .row:hover{background:var(--bg-panel)}.item.active .row{background:var(--bg-panel);color:var(--fg-bright)}.item .label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.chev{display:inline-flex;align-items:center;justify-content:center;width:11px;flex-shrink:0;color:var(--fg-dim)}.chev svg{transition:transform .15s ease}.chev.open svg{transform:rotate(90deg)}.icon{display:inline-flex;flex-shrink:0}.t-note{color:var(--purple)}.t-folder{color:var(--blue)}.t-md{color:var(--cyan)}.t-image{color:var(--orange)}.t-file{color:var(--fg-dim)}.item .del{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;margin-right:4px;padding:0;border:none;border-radius:6px;background:none;color:var(--fg-dim);cursor:pointer;visibility:hidden;flex-shrink:0}.rename-item{padding:2px 6px 2px 0}.rename-item input{width:100%;padding:4px 8px;font-size:13px;color:var(--fg-bright);background:var(--bg-darkest);border:1px solid var(--blue);border-radius:6px;outline:none}.item:hover .del{visibility:visible}.item .del:hover{color:var(--red);background:var(--bg-panel)}.side-foot{display:flex;flex-direction:column;gap:8px;padding:12px;border-top:1px solid var(--border-dark);flex-shrink:0}.upload-btn{display:flex;align-items:center;justify-content:center;gap:8px;padding:8px;font-size:12.5px;color:var(--fg);background:var(--bg-panel);border:1px solid var(--border);border-radius:8px;cursor:pointer}.upload-btn:hover:not(:disabled){color:var(--blue);border-color:var(--blue)}.upload-btn:disabled{opacity:.4;cursor:default}.root-hint{font-size:11px;font-family:var(--mono);color:var(--fg-dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.toolbar{display:flex;align-items:center;gap:12px;height:48px;padding:0 16px;background:var(--bg-darker);border-bottom:1px solid var(--border-dark);flex-shrink:0}.meta{display:flex;align-items:center;gap:9px;min-width:0}.meta .name{font-size:13px;font-weight:500;color:var(--fg-bright);white-space:nowrap}.meta .crumb{font-size:12px;color:var(--fg-dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.unsaved{width:7px;height:7px;border-radius:50%;background:var(--yellow);flex-shrink:0}.spacer{flex:1}.count{font-size:12px;color:var(--fg-dim);white-space:nowrap}.seg{display:flex;padding:2px;background:var(--bg-darkest);border:1px solid var(--border);border-radius:8px}.seg button{padding:5px 12px;font-size:12px;border:none;border-radius:6px;background:transparent;color:var(--fg-dim);cursor:pointer}.seg button.active{background:var(--bg-panel);color:var(--fg-bright)}.save-btn{display:flex;align-items:center;gap:7px;padding:6px 12px;font-size:12.5px;color:var(--blue);background:var(--bg-panel);border:1px solid var(--border);border-radius:8px;cursor:pointer}.save-btn:hover:not(:disabled){border-color:var(--blue)}.save-btn:disabled{color:var(--fg-dim);opacity:.6;cursor:default}.export-wrap{position:relative}.export-chev{display:inline-flex;color:var(--fg-dim)}.export-chev svg{transition:transform .15s ease}.export-chev.open svg{transform:rotate(180deg)}.menu-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:44;padding:0;border:none;background:transparent;cursor:default}.export-menu{position:absolute;top:calc(100% + 6px);right:0;z-index:45;display:flex;flex-direction:column;min-width:150px;padding:4px;background:var(--bg-darker);border:1px solid var(--border);border-radius:8px;box-shadow:0 10px 32px #00000073}.export-menu button{display:flex;align-items:center;gap:8px;padding:8px 10px;border:none;border-radius:6px;background:none;color:var(--fg);font-size:12.5px;text-align:left;cursor:pointer}.export-menu button:hover{background:var(--bg-panel);color:var(--fg-bright)}.editor-body{flex:1;display:flex;min-height:0}.autosaved{font-size:11.5px;color:var(--green);white-space:nowrap}.panel-btn.active{color:var(--blue);background:var(--bg-panel)}.resources{width:240px;flex-shrink:0;display:flex;flex-direction:column;background:var(--bg-darker);border-left:1px solid var(--border-dark);overflow:hidden}.res-head{display:flex;align-items:center;justify-content:space-between;height:40px;padding:0 6px 0 14px;font-size:11px;font-weight:600;letter-spacing:.8px;text-transform:uppercase;color:var(--fg-dim);border-bottom:1px solid var(--border-dark);flex-shrink:0}.res-actions{display:flex;gap:2px}.res-empty{padding:14px;font-size:12px;line-height:1.6;color:var(--fg-dim)}.res-scroll{flex:1;overflow-y:auto}.res-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;padding:10px}.res-item{position:relative;aspect-ratio:1;padding:0;border:1px solid var(--border);border-radius:6px;background:var(--bg-panel);overflow:hidden;cursor:pointer}.res-item:hover{border-color:var(--blue)}.res-remove{position:absolute;top:4px;right:4px;display:flex;align-items:center;justify-content:center;width:18px;height:18px;padding:0;border:none;border-radius:4px;background:#181a1fd9;color:var(--red);cursor:pointer;opacity:0}.res-item:hover .res-remove,.res-remove:focus-visible{opacity:1}.res-remove:hover{background:var(--bg-darkest)}.res-item img{display:block;width:100%;height:100%;object-fit:cover}.res-file{display:flex;align-items:center;justify-content:center;height:100%;color:var(--fg-dim)}.res-name{position:absolute;left:4px;right:4px;bottom:4px;padding:2px 6px;font-size:10px;text-align:left;color:#fff;background:#181a1fcc;border-radius:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}textarea.code{flex:1;width:100%;padding:24px 32px;border:none;outline:none;resize:none;background:var(--bg);color:var(--fg);font-family:var(--mono);font-size:14px;line-height:1.75;caret-color:var(--blue)}textarea.code.half{flex:1 1 50%;width:auto;min-width:0;padding:20px 24px;border-right:1px solid var(--border-dark)}.preview-wrap.half{flex:1 1 50%;min-width:0}.preview-wrap.half .preview{padding:24px 24px 60px}.preview-wrap{flex:1;overflow-y:auto}.preview{max-width:760px;margin:0 auto;padding:36px 48px 80px;font-size:15px;line-height:1.75}.preview h1,.preview h2,.preview h3,.preview h4{color:var(--fg-bright);line-height:1.3;margin:1.6em 0 .6em}.preview h1,.preview h2{padding-bottom:.3em;border-bottom:1px solid var(--border)}.preview h1{margin-top:.4em}.preview p{margin:.8em 0}.preview a{color:var(--blue);text-decoration:none}.preview a:hover{text-decoration:underline}.preview code{padding:2px 6px;border-radius:4px;background:#3a3f4b;color:var(--yellow);font-family:var(--mono);font-size:.88em}.preview pre{padding:16px;overflow-x:auto;background:var(--bg-darker);border:1px solid var(--border);border-radius:8px;line-height:1.6}.preview pre code{padding:0;background:none;color:inherit;font-size:13px}.preview blockquote{margin:1em 0;padding:2px 16px;color:var(--fg-dim);border-left:3px solid var(--fg-dim)}.preview ul,.preview ol{padding-left:1.6em}.preview li{margin:.3em 0}.preview img{max-width:100%;border-radius:6px}.preview hr{margin:1.6em 0;border:none;border-top:1px solid var(--border)}.preview table{border-collapse:collapse;margin:1em 0;width:100%}.preview th,.preview td{padding:7px 12px;border:1px solid var(--border)}.preview th{background:var(--bg-panel);color:var(--fg-bright)}.media-view{flex:1;display:flex;align-items:center;justify-content:center;overflow:auto;padding:32px}.media-view img{max-width:100%;max-height:100%;border-radius:8px;box-shadow:0 8px 40px #0006}.empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;color:var(--fg-dim)}.primary-btn{display:flex;align-items:center;gap:8px;margin-top:8px;padding:9px 18px;font-size:13px;font-weight:600;color:var(--bg-darkest);background:var(--green);border:none;border-radius:8px;cursor:pointer}.primary-btn:hover{filter:brightness(1.1)}.logo{margin:0;font-size:30px;font-weight:600;letter-spacing:1px;color:#3e4451}.toast{position:fixed;bottom:24px;left:50%;transform:translate(-50%);padding:10px 16px;font-size:13px;color:var(--fg-bright);background:var(--bg-darkest);border:1px solid var(--border);border-radius:8px;box-shadow:0 8px 30px #00000059;z-index:100;animation:rise .2s ease}.toast.error{color:var(--red);border-color:var(--red)}@keyframes rise{0%{opacity:0;transform:translate(-50%) translateY(8px)}to{opacity:1;transform:translate(-50%) translateY(0)}}