mirror of
https://github.com/litruv/lit.ruv.wtf.git
synced 2026-07-24 02:36:02 +10:00
blog post editor
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
---
|
||||
title: Markdown Formatting Test
|
||||
date: 2026-04-20
|
||||
date: 2026-09-11
|
||||
author: Test Author
|
||||
tags: test, formatting, markdown
|
||||
---
|
||||
|
||||
# Heading 1
|
||||
# Heading 1 test 123
|
||||
|
||||
## Heading 2
|
||||
|
||||
@@ -30,11 +29,15 @@ Here's a [regular link](https://example.com) and here's a [YouTube link](https:/
|
||||
## Lists
|
||||
|
||||
Unordered list:
|
||||
- First item
|
||||
- Second item
|
||||
- Third item
|
||||
|
||||
* First item
|
||||
|
||||
* Second item
|
||||
|
||||
* Third item
|
||||
|
||||
Ordered list:
|
||||
|
||||
1. First step
|
||||
2. Second step
|
||||
3. Third step
|
||||
@@ -42,6 +45,7 @@ Ordered list:
|
||||
## Code Blocks
|
||||
|
||||
Standard JavaScript:
|
||||
|
||||
```javascript
|
||||
const hello = "world";
|
||||
console.log(hello);
|
||||
@@ -52,6 +56,7 @@ function test() {
|
||||
```
|
||||
|
||||
Python with syntax highlighting:
|
||||
|
||||
```python
|
||||
def hello_world():
|
||||
print("Hello, World!")
|
||||
@@ -59,7 +64,8 @@ def hello_world():
|
||||
```
|
||||
|
||||
Code with max height (200px):
|
||||
```javascript {maxHeight: 200}
|
||||
|
||||
```javascript
|
||||
// This is a long code block that will scroll
|
||||
const data = [1, 2, 3, 4, 5];
|
||||
|
||||
@@ -92,31 +98,36 @@ const obj = {
|
||||
|
||||
## Horizontal Rule
|
||||
|
||||
---
|
||||
***
|
||||
|
||||
## Images
|
||||
|
||||

|
||||

|
||||
|
||||
## Mixed Content
|
||||
|
||||
You can mix **bold**, *italic*, and `code` in the same paragraph. Here's a [link with **bold** text](https://example.com) too.
|
||||
You can mix **bold**, *italic*, and `code` in the same paragraph. Here's a [link with](https://example.com) **[bold](https://example.com)** [text](https://example.com) too.
|
||||
|
||||
### Nested Lists
|
||||
|
||||
- Top level item
|
||||
- Another top level
|
||||
- Nested item (if supported)
|
||||
- Another nested
|
||||
* Top level itemsss
|
||||
|
||||
* Another top level
|
||||
|
||||
* Nested item (if supported)
|
||||
|
||||
* Another nested
|
||||
|
||||
1. Numbered item
|
||||
2. Another numbered
|
||||
- Mixed with bullets
|
||||
- More bullets
|
||||
|
||||
* Mixed with bullets
|
||||
|
||||
* More bullets
|
||||
|
||||
## Special Characters
|
||||
|
||||
Testing special chars: < > & " '
|
||||
Testing special chars: < > & " '
|
||||
|
||||
## Long Paragraph
|
||||
|
||||
|
||||
BIN
website/data/blog/media/paste-1778401785819-7erqa.png
Normal file
BIN
website/data/blog/media/paste-1778401785819-7erqa.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
BIN
website/data/blog/media/test.gif
Normal file
BIN
website/data/blog/media/test.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
15
website/data/blog/testddd.md
Normal file
15
website/data/blog/testddd.md
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: test d d d d
|
||||
date: 2026-05-10
|
||||
author: it me
|
||||
---
|
||||
asdffdsaasdffdsa
|
||||
|
||||
| <br /> | <br /> | <br /> | <br /> |
|
||||
| :----- | :----- | :----- | -----: |
|
||||
| <br /> | <br /> | <br /> | <br /> |
|
||||
| <br /> | <br /> | <br /> | <br /> |
|
||||
|
||||
<br />
|
||||
|
||||
this is a test post, should't publish i guess, but who's reading lmao
|
||||
@@ -1,10 +1,9 @@
|
||||
---
|
||||
title: Welcome to My Blog
|
||||
date: 2026-04-19
|
||||
date: 2026-05-10
|
||||
author: Max Litruv Boonzaayer
|
||||
tags: welcome, meta, introduction
|
||||
---
|
||||
|
||||
# Welcome to My Blog
|
||||
|
||||
This is my first blog post! I'm excited to share my thoughts and experiences with you.
|
||||
@@ -15,15 +14,20 @@ This blog is built using a **custom node-based graph system** where each blog po
|
||||
|
||||
### Features
|
||||
|
||||
- 📝 Markdown support with YAML front matter
|
||||
- 📅 Date-based organization
|
||||
- 🏷️ Tag system for categorization
|
||||
- 🎨 Interactive node-based visualization
|
||||
- 📁 Media support in `data/blog/media/`
|
||||
* 📝 Markdown support with YAML front matter
|
||||
|
||||
* 📅 Date-based organization
|
||||
|
||||
* 🏷️ Tag system for categorization
|
||||
|
||||
* 🎨 Interactive node-based visualization
|
||||
|
||||
* 📁 Media support in `data/blog/media/`
|
||||
|
||||
## Technical Details
|
||||
|
||||
The build system automatically:
|
||||
|
||||
1. Scans the `data/blog/` directory for `.md` files
|
||||
2. Parses YAML front matter for metadata
|
||||
3. Generates a `blogs.json` file
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"formatting",
|
||||
"markdown"
|
||||
],
|
||||
"content": "\r\n# Heading 1\r\n\r\n## Heading 2\r\n\r\n### Heading 3\r\n\r\n#### Heading 4\r\n\r\n##### Heading 5\r\n\r\n###### Heading 6\r\n\r\n## Text Formatting\r\n\r\nThis is **bold text** and this is *italic text* and this is ***bold italic text***.\r\n\r\nHere's some `inline code` in a sentence.\r\n\r\n## Links\r\n\r\nHere's a [regular link](https://example.com) and here's a [YouTube link](https://youtube.com/watch?v=test).\r\n\r\n## Lists\r\n\r\nUnordered list:\r\n- First item\r\n- Second item\r\n- Third item\r\n\r\nOrdered list:\r\n1. First step\r\n2. Second step\r\n3. Third step\r\n\r\n## Code Blocks\r\n\r\nStandard JavaScript:\r\n```javascript\r\nconst hello = \"world\";\r\nconsole.log(hello);\r\n\r\nfunction test() {\r\n return true;\r\n}\r\n```\r\n\r\nPython with syntax highlighting:\r\n```python\r\ndef hello_world():\r\n print(\"Hello, World!\")\r\n return True\r\n```\r\n\r\nCode with max height (200px):\r\n```javascript {maxHeight: 200}\r\n// This is a long code block that will scroll\r\nconst data = [1, 2, 3, 4, 5];\r\n\r\nfunction processData(arr) {\r\n return arr.map(x => x * 2);\r\n}\r\n\r\nconsole.log(processData(data));\r\n\r\n// Adding more lines to demonstrate scrolling\r\nfor (let i = 0; i < 10; i++) {\r\n console.log(`Iteration ${i}`);\r\n}\r\n\r\n// Even more content\r\nconst obj = {\r\n name: \"Test\",\r\n value: 42,\r\n nested: {\r\n deep: true\r\n }\r\n};\r\n```\r\n\r\n## Blockquotes\r\n\r\n> This is a blockquote.\r\n> It can span multiple lines.\r\n> And continues here.\r\n\r\n## Horizontal Rule\r\n\r\n---\r\n\r\n## Images\r\n\r\n\r\n\r\n## Mixed Content\r\n\r\nYou can mix **bold**, *italic*, and `code` in the same paragraph. Here's a [link with **bold** text](https://example.com) too.\r\n\r\n### Nested Lists\r\n\r\n- Top level item\r\n- Another top level\r\n - Nested item (if supported)\r\n - Another nested\r\n\r\n1. Numbered item\r\n2. Another numbered\r\n - Mixed with bullets\r\n - More bullets\r\n\r\n## Special Characters\r\n\r\nTesting special chars: < > & \" ' \r\n\r\n## Long Paragraph\r\n\r\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\r\n\r\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\r\n"
|
||||
"content": "\r\n# Heading 1\r\n\r\n## Heading 2\r\n\r\n### Heading 3\r\n\r\n#### Heading 4\r\n\r\n##### Heading 5\r\n\r\n###### Heading 6\r\n\r\n## Text Formatting\r\n\r\nThis is **bold text** and this is *italic text* and this is ***bold italic text***.\r\n\r\nHere's some `inline code` in a sentence.\r\n\r\n## Links\r\n\r\nHere's a [regular link](https://example.com) and here's a [YouTube link](https://youtube.com/watch?v=test).\r\n\r\n## Lists\r\n\r\nUnordered list:\r\n- First item\r\n- Second item\r\n- Third item\r\n\r\nOrdered list:\r\n1. First step\r\n2. Second step\r\n3. Third step\r\n\r\n## Code Blocks\r\n\r\nStandard JavaScript:\r\n```javascript\r\nconst hello = \"world\";\r\nconsole.log(hello);\r\n\r\nfunction test() {\r\n return true;\r\n}\r\n```\r\n\r\nPython with syntax highlighting:\r\n```python\r\ndef hello_world():\r\n print(\"Hello, World!\")\r\n return True\r\n```\r\n\r\nCode with max height (200px):\r\n```javascript {maxHeight: 200}\r\n// This is a long code block that will scroll\r\nconst data = [1, 2, 3, 4, 5];\r\n\r\nfunction processData(arr) {\r\n return arr.map(x => x * 2);\r\n}\r\n\r\nconsole.log(processData(data));\r\n\r\n// Adding more lines to demonstrate scrolling\r\nfor (let i = 0; i < 10; i++) {\r\n console.log(`Iteration ${i}`);\r\n}\r\n\r\n// Even more content\r\nconst obj = {\r\n name: \"Test\",\r\n value: 42,\r\n nested: {\r\n deep: true\r\n }\r\n};\r\n```\r\n\r\n## Blockquotes\r\n\r\n> This is a blockquote.\r\n> It can span multiple lines.\r\n> And continues here.\r\n\r\n## Horizontal Rule\r\n\r\n---\r\n\r\n## Images\r\n\r\n\r\n\r\n## Mixed Content\r\n\r\nYou can mix **bold**, *italic*, and `code` in the same paragraph. Here's a [link with **bold** text](https://example.com) too.\r\n\r\n### Nested Lists\r\n\r\n- Top level item\r\n- Another top level\r\n - Nested item (if supported)\r\n - Another nested\r\n\r\n1. Numbered item\r\n2. Another numbered\r\n - Mixed with bullets\r\n - More bullets\r\n\r\n## Special Characters\r\n\r\nTesting special chars: < > & \" ' \r\n\r\n## Long Paragraph\r\n\r\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\r\n\r\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\r\n"
|
||||
},
|
||||
{
|
||||
"slug": "welcome",
|
||||
|
||||
@@ -29,17 +29,23 @@
|
||||
<meta name="twitter:description" content="Personal site of Lit.ruv.wtf. Graph editor edition">
|
||||
<meta name="twitter:image" content="https://lit.ruv.wtf/banner.webp">
|
||||
|
||||
<!-- Preconnect to external domains -->
|
||||
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
|
||||
<link rel="preconnect" href="https://unpkg.com" crossorigin>
|
||||
<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">
|
||||
<link rel="dns-prefetch" href="https://unpkg.com">
|
||||
|
||||
<link rel="stylesheet" href="styles/main.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-typescript.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-jsx.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-css.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-bash.min.js"></script>
|
||||
<script src="https://unpkg.com/@lottiefiles/lottie-player@latest/dist/lottie-player.js"></script>
|
||||
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
|
||||
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js"></script>
|
||||
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
|
||||
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-typescript.min.js"></script>
|
||||
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-jsx.min.js"></script>
|
||||
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-css.min.js"></script>
|
||||
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>
|
||||
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-bash.min.js"></script>
|
||||
<script defer src="https://unpkg.com/@lottiefiles/lottie-player@latest/dist/lottie-player.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="workspaceCanvas" class="workspace-canvas">
|
||||
|
||||
@@ -202,6 +202,13 @@ export class ItemDragger {
|
||||
if (!this.#enabled) return;
|
||||
const target = /** @type {HTMLElement} */ (e.target);
|
||||
if (target.closest(".pin-handle")) return;
|
||||
|
||||
// Allow text selection in blog/info node bodies
|
||||
const nodeBody = target.closest(".node-body");
|
||||
if (nodeBody && (node.type === 'info' || node.type === 'blog_post')) {
|
||||
return; // Don't drag, allow text selection
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
|
||||
const pointerWorld = this.#clientToWorkspace(e.clientX, e.clientY);
|
||||
|
||||
@@ -91,7 +91,8 @@ export class MarkdownRenderer {
|
||||
|
||||
const langClass = lang ? ` language-${lang}` : "";
|
||||
const styleAttr = maxHeight ? ` style="max-height: ${maxHeight}px; overflow-y: auto;"` : "";
|
||||
return `<pre class="md-pre"${styleAttr}><code class="${langClass}">${highlightedCode}</code></pre>`;
|
||||
const escapedCode = MarkdownRenderer.#escape(code);
|
||||
return `<div class="md-code-block"><button class="md-copy-btn" data-code="${escapedCode}" title="Copy code">📋</button><pre class="md-pre"${styleAttr}><code class="${langClass}">${highlightedCode}</code></pre></div>`;
|
||||
}
|
||||
|
||||
// Horizontal rule
|
||||
@@ -106,6 +107,16 @@ export class MarkdownRenderer {
|
||||
return `<h${level} class="md-h${level}">${MarkdownRenderer.#renderInline(headingMatch[2])}</h${level}>`;
|
||||
}
|
||||
|
||||
// Standalone image
|
||||
const imgBlockMatch = block.match(/^!\[([^\]]*)\]\(([^)]+)\)$/);
|
||||
if (imgBlockMatch) {
|
||||
const alt = MarkdownRenderer.#escape(imgBlockMatch[1]);
|
||||
const src = MarkdownRenderer.#sanitizeUrl(imgBlockMatch[2]);
|
||||
if (src) {
|
||||
return `<figure class="md-figure"><img class="md-img" src="${src}" alt="${alt}" />${alt ? `<figcaption class="md-figcaption">${alt}</figcaption>` : ""}</figure>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Blockquote
|
||||
if (/^> /.test(block)) {
|
||||
const inner = block.replace(/^> ?/gm, "");
|
||||
@@ -114,20 +125,12 @@ export class MarkdownRenderer {
|
||||
|
||||
// Unordered list
|
||||
if (/^[-*+] /.test(block)) {
|
||||
const items = block.split("\n").filter(Boolean).map(line => {
|
||||
const content = line.replace(/^[-*+]\s+/, "");
|
||||
return `<li class="md-li">${MarkdownRenderer.#renderInline(content)}</li>`;
|
||||
});
|
||||
return `<ul class="md-ul">${items.join("")}</ul>`;
|
||||
return MarkdownRenderer.#renderList(block, "ul");
|
||||
}
|
||||
|
||||
// Ordered list
|
||||
if (/^\d+\. /.test(block)) {
|
||||
const items = block.split("\n").filter(Boolean).map(line => {
|
||||
const content = line.replace(/^\d+\.\s+/, "");
|
||||
return `<li class="md-li">${MarkdownRenderer.#renderInline(content)}</li>`;
|
||||
});
|
||||
return `<ol class="md-ol">${items.join("")}</ol>`;
|
||||
return MarkdownRenderer.#renderList(block, "ol");
|
||||
}
|
||||
|
||||
// Paragraph (handle single-line line breaks within block)
|
||||
@@ -135,6 +138,57 @@ export class MarkdownRenderer {
|
||||
return `<p class="md-p">${lines.join("<br />")}</p>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders nested lists (ul or ol) with indentation support.
|
||||
* @param {string} block
|
||||
* @param {"ul" | "ol"} listType
|
||||
* @returns {string}
|
||||
*/
|
||||
static #renderList(block, listType) {
|
||||
const lines = block.split("\n").filter(Boolean);
|
||||
const items = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < lines.length) {
|
||||
const line = lines[i];
|
||||
const indent = line.match(/^(\s*)/)[1].length;
|
||||
|
||||
// Extract content
|
||||
let content;
|
||||
if (listType === "ul") {
|
||||
content = line.replace(/^\s*[-*+]\s+/, "");
|
||||
} else {
|
||||
content = line.replace(/^\s*\d+\.\s+/, "");
|
||||
}
|
||||
|
||||
// Look ahead for nested items (more indented)
|
||||
let nestedBlock = "";
|
||||
let j = i + 1;
|
||||
while (j < lines.length) {
|
||||
const nextLine = lines[j];
|
||||
const nextIndent = nextLine.match(/^(\s*)/)[1].length;
|
||||
if (nextIndent > indent) {
|
||||
nestedBlock += (nestedBlock ? "\n" : "") + nextLine.slice(indent + 2); // Remove parent indent
|
||||
j++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Render item with nested list if present
|
||||
let itemHtml = MarkdownRenderer.#renderInline(content);
|
||||
if (nestedBlock) {
|
||||
const nestedType = /^[-*+] /.test(nestedBlock.trim()) ? "ul" : "ol";
|
||||
itemHtml += MarkdownRenderer.#renderList(nestedBlock, nestedType);
|
||||
}
|
||||
items.push(`<li class="md-li">${itemHtml}</li>`);
|
||||
i = j;
|
||||
}
|
||||
|
||||
const tag = listType === "ul" ? "ul" : "ol";
|
||||
return `<${tag} class="md-${tag}">${items.join("")}</${tag}>`;
|
||||
}
|
||||
|
||||
// ─── Inline-level ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -159,6 +213,16 @@ export class MarkdownRenderer {
|
||||
// Italic
|
||||
out = out.replace(/\*(.+?)\*/g, "<em>$1</em>");
|
||||
|
||||
// Images  — must be matched before links
|
||||
out = out.replace(
|
||||
/!\[([^\]]*)\]\(([^)]+)\)/g,
|
||||
(_, alt, url) => {
|
||||
const safeUrl = MarkdownRenderer.#sanitizeUrl(url);
|
||||
if (!safeUrl) return MarkdownRenderer.#escape(alt);
|
||||
return `<img class="md-img md-img--inline" src="${safeUrl}" alt="${MarkdownRenderer.#escape(alt)}" />`;
|
||||
}
|
||||
);
|
||||
|
||||
// Links [text](url)
|
||||
out = out.replace(
|
||||
/\[([^\]]+)\]\(([^)]+)\)/g,
|
||||
@@ -203,7 +267,10 @@ export class MarkdownRenderer {
|
||||
*/
|
||||
static #sanitizeUrl(url) {
|
||||
const trimmed = url.trim();
|
||||
// Allow absolute URLs, root-relative, hash links, relative-dot paths, and plain relative paths
|
||||
if (/^(https?:\/\/|mailto:|\/|#|\.)/.test(trimmed)) return trimmed;
|
||||
// Allow relative paths (e.g. data/blog/media/image.png) — no protocol = safe relative
|
||||
if (/^[a-zA-Z0-9_\-][a-zA-Z0-9_.\-\/]*$/.test(trimmed)) return trimmed;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,6 +481,19 @@ export class WorkspaceNavigator {
|
||||
/** @param {PointerEvent} e */
|
||||
#onPointerDown(e) {
|
||||
if (e.button !== 0 && e.button !== 2) return;
|
||||
|
||||
// Allow text selection in blog/info nodes
|
||||
const nodeBody = e.target.closest('.node-body');
|
||||
if (nodeBody) {
|
||||
const blueprintNode = nodeBody.closest('.blueprint-node');
|
||||
if (blueprintNode) {
|
||||
const nodeType = blueprintNode.dataset.nodeType;
|
||||
if (nodeType === 'info' || nodeType === 'blog_post') {
|
||||
return; // Don't pan, allow text selection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.#beginPan(e);
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,27 @@ export class BlogPostNode extends NodeBase {
|
||||
const { MarkdownRenderer } = await import('../MarkdownRenderer.js');
|
||||
contentDiv.innerHTML = MarkdownRenderer.render(post.content);
|
||||
|
||||
// Add copy button handlers
|
||||
contentDiv.querySelectorAll('.md-copy-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const code = btn.getAttribute('data-code');
|
||||
if (code) {
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
const originalText = btn.textContent;
|
||||
btn.textContent = '✓';
|
||||
btn.classList.add('copied');
|
||||
setTimeout(() => {
|
||||
btn.textContent = originalText;
|
||||
btn.classList.remove('copied');
|
||||
}, 2000);
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy:', err);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
body.innerHTML = `<p style="color: #ff6b6b;">Error loading blog post: ${error.message}</p>`;
|
||||
console.error('Failed to load blog post:', error);
|
||||
|
||||
@@ -40,3 +40,5 @@ export class PrintNode extends NodeBase {
|
||||
}
|
||||
|
||||
NodeRegistry.UCLASS_Register(PrintNode);
|
||||
|
||||
|
||||
|
||||
@@ -1,281 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>Personal Site</title>
|
||||
<link rel="stylesheet" href="styles/main.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="workspaceCanvas" class="workspace-canvas">
|
||||
<svg id="connectionLayer" class="connection-layer"></svg>
|
||||
<div id="worldLayer" class="world-layer">
|
||||
<div id="nodeLayer" class="node-layer"></div>
|
||||
</div>
|
||||
|
||||
<div class="hud">
|
||||
<button id="resetViewBtn" class="hud-btn" title="Reset View">⌖</button>
|
||||
</div>
|
||||
|
||||
<div id="debugPanel" class="debug-panel collapsed">
|
||||
<div class="debug-header">
|
||||
<div class="debug-title">Debug</div>
|
||||
<button class="debug-toggle-btn" title="Toggle debug panel" aria-label="Toggle debug panel">
|
||||
<svg class="debug-eye-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path class="eye-visible" d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
|
||||
<circle class="eye-visible" cx="12" cy="12" r="3"></circle>
|
||||
<path class="eye-hidden" d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" style="display: none;"></path>
|
||||
<line class="eye-hidden" x1="1" y1="1" x2="23" y2="23" style="display: none;"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="debug-row">
|
||||
<span class="debug-label">Zoom</span>
|
||||
<span class="debug-value debug-zoom">1.000</span>
|
||||
</div>
|
||||
<div class="debug-row">
|
||||
<span class="debug-label">DPR</span>
|
||||
<span class="debug-value debug-dpr">1.0</span>
|
||||
</div>
|
||||
<div class="debug-row">
|
||||
<span class="debug-label">Viewport</span>
|
||||
<span class="debug-value debug-viewport">0 × 0</span>
|
||||
</div>
|
||||
<div class="debug-row">
|
||||
<span class="debug-label">Center</span>
|
||||
<span class="debug-value debug-center">0.0, 0.0</span>
|
||||
</div>
|
||||
<div class="debug-row">
|
||||
<span class="debug-label">Viewbox</span>
|
||||
<span class="debug-value debug-viewbox">—</span>
|
||||
</div>
|
||||
<div class="debug-row">
|
||||
<span class="debug-label">NodeAnchor</span>
|
||||
<span class="debug-value debug-anchor">—</span>
|
||||
</div>
|
||||
<div class="debug-divider"></div>
|
||||
<label class="debug-row debug-check-row">
|
||||
<span class="debug-label">Draggable</span>
|
||||
<input class="debug-draggable-cb" type="checkbox" />
|
||||
</label>
|
||||
<label class="debug-row debug-check-row">
|
||||
<span class="debug-label">WorldBoxes</span>
|
||||
<input class="debug-worldbox-cb" type="checkbox" />
|
||||
</label>
|
||||
<label class="debug-row debug-check-row">
|
||||
<span class="debug-label">Viewbox</span>
|
||||
<input class="debug-viewbox-cb" type="checkbox" />
|
||||
</label>
|
||||
<div class="debug-divider"></div>
|
||||
<button class="debug-copy-json-btn" title="Copy current graph as JSON">Copy JSON</button>
|
||||
<button class="debug-show-properties-btn" title="Open properties panel">Show Properties</button>
|
||||
<div class="debug-row debug-active-row" hidden>
|
||||
<span class="debug-label debug-active-label">—</span>
|
||||
<span class="debug-value debug-active-pos">0.0, 0.0</span>
|
||||
<button class="debug-copy-btn" title="Copy graph.json">Copy graph</button>
|
||||
</div>
|
||||
|
||||
<div class="debug-divider" id="debugNodeEditorDivider" hidden></div>
|
||||
<div id="debugNodeEditor" class="debug-node-editor" hidden>
|
||||
<div class="debug-subtitle">Node Editor</div>
|
||||
<div class="debug-row">
|
||||
<span class="debug-label">ID</span>
|
||||
<input class="debug-input debug-node-id" type="text" readonly />
|
||||
</div>
|
||||
<div class="debug-row">
|
||||
<span class="debug-label">Type</span>
|
||||
<input class="debug-input debug-node-type" type="text" readonly />
|
||||
</div>
|
||||
<div class="debug-row">
|
||||
<span class="debug-label">Title</span>
|
||||
<input class="debug-input debug-node-title" type="text" />
|
||||
</div>
|
||||
<div class="debug-row">
|
||||
<span class="debug-label">X</span>
|
||||
<input class="debug-input debug-node-x" type="number" step="1" />
|
||||
</div>
|
||||
<div class="debug-row">
|
||||
<span class="debug-label">Y</span>
|
||||
<input class="debug-input debug-node-y" type="number" step="1" />
|
||||
</div>
|
||||
<div class="debug-row">
|
||||
<span class="debug-label">Width</span>
|
||||
<input class="debug-input debug-node-width" type="number" step="1" />
|
||||
</div>
|
||||
|
||||
<div class="debug-subtitle">Inputs</div>
|
||||
<div id="debugInputsList" class="debug-pins-list"></div>
|
||||
<button class="debug-add-btn debug-add-input-btn">+ Add Input</button>
|
||||
|
||||
<div class="debug-subtitle">Outputs</div>
|
||||
<div id="debugOutputsList" class="debug-pins-list"></div>
|
||||
<button class="debug-add-btn debug-add-output-btn">+ Add Output</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Properties Editor Panel (Right Side) -->
|
||||
<div id="propertiesPanel" class="properties-panel collapsed">
|
||||
<button class="properties-panel-tab" title="Open properties panel" aria-label="Open properties panel">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="7" height="7"></rect>
|
||||
<rect x="14" y="3" width="7" height="7"></rect>
|
||||
<rect x="14" y="14" width="7" height="7"></rect>
|
||||
<rect x="3" y="14" width="7" height="7"></rect>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="properties-header">
|
||||
<h2 class="properties-title">Properties</h2>
|
||||
<button class="properties-toggle-btn" title="Close properties panel" aria-label="Close properties panel">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="15 18 9 12 15 6"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="properties-content">
|
||||
<!-- Node Properties Section -->
|
||||
<div id="nodePropertiesSection" class="properties-section" style="display: none;">
|
||||
<div class="section-header">
|
||||
<h3>Node</h3>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">ID</label>
|
||||
<input type="text" class="prop-input" id="propNodeId" readonly />
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Type</label>
|
||||
<input type="text" class="prop-input" id="propNodeType" readonly />
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Title</label>
|
||||
<input type="text" class="prop-input" id="propNodeTitle" />
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Position X</label>
|
||||
<input type="number" class="prop-input" id="propNodeX" step="20" />
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Position Y</label>
|
||||
<input type="number" class="prop-input" id="propNodeY" step="20" />
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Width</label>
|
||||
<input type="number" class="prop-input" id="propNodeWidth" placeholder="Auto" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Focus Options Section -->
|
||||
<div id="focusOptionsSection" class="properties-section" style="display: none;">
|
||||
<div class="section-header collapsible">
|
||||
<h3>Focus Options</h3>
|
||||
<button class="section-toggle">▼</button>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Duration (ms)</label>
|
||||
<input type="number" class="prop-input" id="propFocusDuration" placeholder="550" />
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Anchor X</label>
|
||||
<input type="number" class="prop-input" id="propFocusAnchorX" step="0.1" min="0" max="1" placeholder="0.5" />
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Anchor Y</label>
|
||||
<input type="number" class="prop-input" id="propFocusAnchorY" step="0.1" min="0" max="1" placeholder="0.5" />
|
||||
</div>
|
||||
|
||||
<div class="prop-divider"></div>
|
||||
<h4 class="prop-subtitle">Default WorldBox</h4>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Width</label>
|
||||
<input type="number" class="prop-input" id="propWorldBoxWidth" placeholder="Auto" />
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Height</label>
|
||||
<input type="number" class="prop-input" id="propWorldBoxHeight" placeholder="Auto" />
|
||||
</div>
|
||||
|
||||
<div class="prop-divider"></div>
|
||||
<h4 class="prop-subtitle">Responsive Breakpoints</h4>
|
||||
<div id="breakpointsList" class="breakpoints-list"></div>
|
||||
<button class="prop-btn-add" id="addBreakpointBtn">+ Add Breakpoint</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Graph Settings Section -->
|
||||
<div id="graphSettingsSection" class="properties-section">
|
||||
<div class="section-header collapsible">
|
||||
<h3>Graph Settings</h3>
|
||||
<button class="section-toggle">▼</button>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Draggable</label>
|
||||
<input type="checkbox" class="prop-checkbox" id="propGraphDraggable" />
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Intro Duration (ms)</label>
|
||||
<input type="number" class="prop-input" id="propIntroDuration" placeholder="700" />
|
||||
</div>
|
||||
|
||||
<div class="prop-divider"></div>
|
||||
<h4 class="prop-subtitle">Initial Viewport</h4>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">X</label>
|
||||
<input type="number" class="prop-input" id="propInitViewX" />
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Y</label>
|
||||
<input type="number" class="prop-input" id="propInitViewY" />
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Width</label>
|
||||
<input type="number" class="prop-input" id="propInitViewWidth" />
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Height</label>
|
||||
<input type="number" class="prop-input" id="propInitViewHeight" />
|
||||
</div>
|
||||
|
||||
<div class="prop-divider"></div>
|
||||
<button class="prop-btn-primary" id="applySettingsBtn">Apply Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No Selection Message -->
|
||||
<div id="noSelectionMessage" class="no-selection">
|
||||
<p>Select a node to edit its properties</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template id="nodeTemplate">
|
||||
<article class="blueprint-node" draggable="false">
|
||||
<header class="node-header">
|
||||
<div class="node-title"></div>
|
||||
</header>
|
||||
<section class="node-io node-inputs"></section>
|
||||
<section class="node-io node-outputs"></section>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<template id="pinTemplate">
|
||||
<div class="pin" draggable="false">
|
||||
<span class="pin-label"></span>
|
||||
<span class="pin-handle" tabindex="-1"></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- WorldBox Overlay Layer -->
|
||||
<svg id="worldBoxLayer" class="worldbox-layer" style="display: none;"></svg>
|
||||
|
||||
<!-- Viewbox Overlay Layer -->
|
||||
<svg id="viewboxLayer" class="viewbox-layer" style="display: none;"></svg>
|
||||
|
||||
<!-- JSON Editor Modal -->
|
||||
<script type="module" src="scripts/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user