freemd 0.1.1 → 0.1.2
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/README.es.md +5 -5
- package/README.md +3 -3
- package/bin/openmd.js +16 -16
- package/dist/assets/index-C0VJG4kI.css +1 -0
- package/dist/assets/index-D77myrtE.js +29 -0
- package/dist/index.html +3 -3
- package/package.json +1 -1
- package/server/export/to-docx.js +1 -1
- package/server/export/to-pdf.js +1 -1
- package/server/index.js +24 -24
- package/dist/assets/index-C0w_6LCS.css +0 -1
- package/dist/assets/index-CiAQinMf.js +0 -29
package/dist/index.html
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
|
-
<html lang="
|
|
2
|
+
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
href="data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20100%20100'%3E%3Crect%20width='100'%20height='100'%20rx='22'%20fill='%23282c34'/%3E%3Ctext%20x='50'%20y='70'%20font-size='54'%20font-family='monospace'%20font-weight='700'%20text-anchor='middle'%20fill='%2398c379'%3EM%3C/text%3E%3C/svg%3E"
|
|
9
9
|
/>
|
|
10
10
|
<title>freemd</title>
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-D77myrtE.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C0VJG4kI.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="app"></div>
|
package/package.json
CHANGED
package/server/export/to-docx.js
CHANGED
|
@@ -119,7 +119,7 @@ function pushImages(out, images, state) {
|
|
|
119
119
|
new Paragraph({
|
|
120
120
|
spacing: { before: 120, after: 160 },
|
|
121
121
|
children: [
|
|
122
|
-
new TextRun({ text: `[
|
|
122
|
+
new TextRun({ text: `[unsupported image: ${alt}]`, italics: true, color: '808080' })
|
|
123
123
|
]
|
|
124
124
|
})
|
|
125
125
|
);
|
package/server/export/to-pdf.js
CHANGED
|
@@ -365,7 +365,7 @@ class PdfRenderer {
|
|
|
365
365
|
.font('Helvetica-Oblique')
|
|
366
366
|
.fontSize(10)
|
|
367
367
|
.fillColor(COLORS.muted)
|
|
368
|
-
.text(`[
|
|
368
|
+
.text(`[unsupported image: ${alt}]`, { align: 'center' });
|
|
369
369
|
doc.moveDown(0.5);
|
|
370
370
|
return;
|
|
371
371
|
}
|
package/server/index.js
CHANGED
|
@@ -27,7 +27,7 @@ const isNoteName = (name) => name.endsWith('-md');
|
|
|
27
27
|
function safeJoin(root, rel = '') {
|
|
28
28
|
const abs = path.resolve(root, rel);
|
|
29
29
|
if (abs !== root && !abs.startsWith(root + path.sep)) {
|
|
30
|
-
const error = new Error('
|
|
30
|
+
const error = new Error('Path not allowed');
|
|
31
31
|
error.status = 400;
|
|
32
32
|
throw error;
|
|
33
33
|
}
|
|
@@ -138,10 +138,10 @@ export function createApp(root) {
|
|
|
138
138
|
}
|
|
139
139
|
} catch {}
|
|
140
140
|
}
|
|
141
|
-
if (!stat) return res.status(404).json({ error: '
|
|
141
|
+
if (!stat) return res.status(404).json({ error: 'File not found' });
|
|
142
142
|
}
|
|
143
143
|
if (!stat.isFile()) {
|
|
144
|
-
return res.status(400).json({ error: '
|
|
144
|
+
return res.status(400).json({ error: 'Not a file' });
|
|
145
145
|
}
|
|
146
146
|
const ext = path.extname(abs).toLowerCase();
|
|
147
147
|
const { name, dir } = fileLabels(rel);
|
|
@@ -169,7 +169,7 @@ export function createApp(root) {
|
|
|
169
169
|
await fsp.writeFile(abs, String(req.body?.content ?? ''), 'utf8');
|
|
170
170
|
} catch (error) {
|
|
171
171
|
if (error.code === 'ENOENT') {
|
|
172
|
-
return res.status(404).json({ error: '
|
|
172
|
+
return res.status(404).json({ error: 'The file no longer exists' });
|
|
173
173
|
}
|
|
174
174
|
throw error;
|
|
175
175
|
}
|
|
@@ -178,12 +178,12 @@ export function createApp(root) {
|
|
|
178
178
|
|
|
179
179
|
app.post('/api/file', json, wrap(async (req, res) => {
|
|
180
180
|
const name = slug(req.body?.name);
|
|
181
|
-
if (!name) return res.status(400).json({ error: '
|
|
181
|
+
if (!name) return res.status(400).json({ error: 'Invalid name' });
|
|
182
182
|
const parentRel = String(req.body?.parent ?? '');
|
|
183
183
|
const parentAbs = safeJoin(root, parentRel);
|
|
184
184
|
const rel = parentRel ? `${parentRel}/${name}.md` : `${name}.md`;
|
|
185
185
|
const abs = path.join(parentAbs, `${name}.md`);
|
|
186
|
-
if (fs.existsSync(abs)) return res.status(409).json({ error: '
|
|
186
|
+
if (fs.existsSync(abs)) return res.status(409).json({ error: 'A file with that name already exists' });
|
|
187
187
|
await fsp.writeFile(abs, String(req.body?.content ?? ''), 'utf8');
|
|
188
188
|
res.status(201).json({ ok: true, path: rel });
|
|
189
189
|
}));
|
|
@@ -191,12 +191,12 @@ export function createApp(root) {
|
|
|
191
191
|
app.post('/api/note', json, wrap(async (req, res) => {
|
|
192
192
|
const raw = String(req.body?.name ?? '').trim();
|
|
193
193
|
let base = slug(raw);
|
|
194
|
-
if (!base) return res.status(400).json({ error: '
|
|
194
|
+
if (!base) return res.status(400).json({ error: 'Invalid name' });
|
|
195
195
|
base = base.replace(/-md$/, '');
|
|
196
|
-
if (!base) return res.status(400).json({ error: '
|
|
196
|
+
if (!base) return res.status(400).json({ error: 'Invalid name' });
|
|
197
197
|
const folder = `${base}-md`;
|
|
198
198
|
const abs = path.join(root, folder);
|
|
199
|
-
if (fs.existsSync(abs)) return res.status(409).json({ error: '
|
|
199
|
+
if (fs.existsSync(abs)) return res.status(409).json({ error: 'A note with that name already exists' });
|
|
200
200
|
await fsp.mkdir(abs, { recursive: true });
|
|
201
201
|
const title = raw.replace(/-md$/i, '').trim() || base;
|
|
202
202
|
await fsp.writeFile(path.join(abs, 'index.md'), `# ${title}\n\n`, 'utf8');
|
|
@@ -205,11 +205,11 @@ export function createApp(root) {
|
|
|
205
205
|
|
|
206
206
|
app.post('/api/folder', json, wrap(async (req, res) => {
|
|
207
207
|
const name = slug(req.body?.name);
|
|
208
|
-
if (!name) return res.status(400).json({ error: '
|
|
208
|
+
if (!name) return res.status(400).json({ error: 'Invalid name' });
|
|
209
209
|
const parentRel = String(req.body?.parent ?? '');
|
|
210
210
|
const parentAbs = safeJoin(root, parentRel);
|
|
211
211
|
const abs = path.join(parentAbs, name);
|
|
212
|
-
if (fs.existsSync(abs)) return res.status(409).json({ error: '
|
|
212
|
+
if (fs.existsSync(abs)) return res.status(409).json({ error: 'A folder with that name already exists' });
|
|
213
213
|
await fsp.mkdir(abs);
|
|
214
214
|
// Una carpeta terminada en -md es una nota: le creamos su index.md
|
|
215
215
|
if (name.endsWith('-md')) {
|
|
@@ -224,7 +224,7 @@ export function createApp(root) {
|
|
|
224
224
|
const dirRel = String(req.query.dir ?? '');
|
|
225
225
|
const dirAbs = safeJoin(root, dirRel);
|
|
226
226
|
const stat = await fsp.stat(dirAbs);
|
|
227
|
-
if (!stat.isDirectory()) return res.status(400).json({ error: '
|
|
227
|
+
if (!stat.isDirectory()) return res.status(400).json({ error: 'Not a folder' });
|
|
228
228
|
const entries = await fsp.readdir(dirAbs, { withFileTypes: true });
|
|
229
229
|
const files = [];
|
|
230
230
|
for (const entry of entries) {
|
|
@@ -249,7 +249,7 @@ export function createApp(root) {
|
|
|
249
249
|
const dirAbs = safeJoin(root, dirRel);
|
|
250
250
|
const rawName = String(req.query.name ?? req.headers['x-filename'] ?? 'file');
|
|
251
251
|
const name = path.basename(rawName).replace(/[^\p{L}\p{N}._ -]/gu, '_');
|
|
252
|
-
if (!req.body?.length) return res.status(400).json({ error: '
|
|
252
|
+
if (!req.body?.length) return res.status(400).json({ error: 'No content' });
|
|
253
253
|
await fsp.mkdir(dirAbs, { recursive: true });
|
|
254
254
|
await fsp.writeFile(path.join(dirAbs, name), req.body);
|
|
255
255
|
const rel = dirRel ? `${dirRel}/${name}` : name;
|
|
@@ -258,9 +258,9 @@ export function createApp(root) {
|
|
|
258
258
|
|
|
259
259
|
app.delete('/api/entry', wrap(async (req, res) => {
|
|
260
260
|
const rel = String(req.query.path ?? '');
|
|
261
|
-
if (!rel) return res.status(400).json({ error: '
|
|
261
|
+
if (!rel) return res.status(400).json({ error: 'Missing path' });
|
|
262
262
|
const abs = safeJoin(root, rel);
|
|
263
|
-
if (abs === root) return res.status(400).json({ error: '
|
|
263
|
+
if (abs === root) return res.status(400).json({ error: 'Cannot delete the root folder' });
|
|
264
264
|
await fsp.rm(abs, { recursive: true });
|
|
265
265
|
res.json({ ok: true });
|
|
266
266
|
}));
|
|
@@ -268,10 +268,10 @@ export function createApp(root) {
|
|
|
268
268
|
app.patch('/api/entry', json, wrap(async (req, res) => {
|
|
269
269
|
const rel = String(req.body?.path ?? '').split('\\').join('/');
|
|
270
270
|
const rawName = String(req.body?.name ?? '');
|
|
271
|
-
if (!rel) return res.status(400).json({ error: '
|
|
271
|
+
if (!rel) return res.status(400).json({ error: 'Missing path' });
|
|
272
272
|
const abs = safeJoin(root, rel);
|
|
273
273
|
const stat = await fsp.stat(abs).catch(() => null);
|
|
274
|
-
if (!stat) return res.status(404).json({ error: '
|
|
274
|
+
if (!stat) return res.status(404).json({ error: 'Item not found' });
|
|
275
275
|
const parentRel = path.posix.dirname(rel);
|
|
276
276
|
const parentAbs = safeJoin(root, parentRel === '.' ? '' : parentRel);
|
|
277
277
|
const currentName = path.basename(abs);
|
|
@@ -280,13 +280,13 @@ export function createApp(root) {
|
|
|
280
280
|
if (stat.isDirectory()) {
|
|
281
281
|
// notas (carpetas *-md): se admite que el usuario escriba el nombre con .md o -md
|
|
282
282
|
let base = slug(rawName.replace(/\.md$/i, '').replace(/-md$/i, ''));
|
|
283
|
-
if (!base) return res.status(400).json({ error: '
|
|
283
|
+
if (!base) return res.status(400).json({ error: 'Invalid name' });
|
|
284
284
|
if (currentName.endsWith('-md')) base = `${base}-md`;
|
|
285
285
|
newName = base;
|
|
286
286
|
} else {
|
|
287
287
|
const ext = path.extname(rel);
|
|
288
288
|
const base = slug(rawName.replace(/\.[^.]+$/, ''));
|
|
289
|
-
if (!base) return res.status(400).json({ error: '
|
|
289
|
+
if (!base) return res.status(400).json({ error: 'Invalid name' });
|
|
290
290
|
newName = `${base}${ext}`;
|
|
291
291
|
}
|
|
292
292
|
|
|
@@ -295,7 +295,7 @@ export function createApp(root) {
|
|
|
295
295
|
}
|
|
296
296
|
const target = path.join(parentAbs, newName);
|
|
297
297
|
if (fs.existsSync(target)) {
|
|
298
|
-
return res.status(409).json({ error: '
|
|
298
|
+
return res.status(409).json({ error: 'An item with that name already exists' });
|
|
299
299
|
}
|
|
300
300
|
await fsp.rename(abs, target);
|
|
301
301
|
const newRel = parentRel === '.' ? newName : `${parentRel}/${newName}`;
|
|
@@ -306,16 +306,16 @@ export function createApp(root) {
|
|
|
306
306
|
const rel = String(req.body?.path ?? '').split('\\').join('/');
|
|
307
307
|
const format = String(req.body?.format ?? 'pdf');
|
|
308
308
|
if (format !== 'pdf' && format !== 'docx') {
|
|
309
|
-
return res.status(400).json({ error: '
|
|
309
|
+
return res.status(400).json({ error: 'Unsupported format' });
|
|
310
310
|
}
|
|
311
311
|
const abs = safeJoin(root, rel);
|
|
312
312
|
let stat;
|
|
313
313
|
try {
|
|
314
314
|
stat = await fsp.stat(abs);
|
|
315
315
|
} catch {
|
|
316
|
-
return res.status(404).json({ error: '
|
|
316
|
+
return res.status(404).json({ error: 'File not found' });
|
|
317
317
|
}
|
|
318
|
-
if (!stat.isFile()) return res.status(400).json({ error: '
|
|
318
|
+
if (!stat.isFile()) return res.status(400).json({ error: 'Not a file' });
|
|
319
319
|
const source = await fsp.readFile(abs, 'utf8');
|
|
320
320
|
const dirName = path.posix.dirname(rel);
|
|
321
321
|
const baseRel = dirName === '.' ? '' : dirName;
|
|
@@ -343,7 +343,7 @@ export function createApp(root) {
|
|
|
343
343
|
if (req.method !== 'GET' || req.path.startsWith('/api')) return next();
|
|
344
344
|
const index = path.join(DIST_DIR, 'index.html');
|
|
345
345
|
if (!fs.existsSync(index)) {
|
|
346
|
-
return res.status(500).send('
|
|
346
|
+
return res.status(500).send('Frontend build missing. Run: pnpm build');
|
|
347
347
|
}
|
|
348
348
|
res.sendFile(index);
|
|
349
349
|
});
|
|
@@ -1 +0,0 @@
|
|
|
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)}}
|