vault backup: 2025-12-26 02:09:22
All checks were successful
Deploy Quartz site to GitHub Pages / build (push) Successful in 2m29s
All checks were successful
Deploy Quartz site to GitHub Pages / build (push) Successful in 2m29s
This commit is contained in:
3249
stroma/file/frontend/package-lock.json
generated
Normal file
3249
stroma/file/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
stroma/file/frontend/package.json
Normal file
29
stroma/file/frontend/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "stroma-file-editor",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"postcss": "^8.5.6",
|
||||
"sass": "^1.83.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"vite": "^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tiptap/core": "^2.10.3",
|
||||
"@tiptap/extension-bubble-menu": "^2.10.3",
|
||||
"@tiptap/extension-placeholder": "^2.10.3",
|
||||
"@tiptap/pm": "^2.10.3",
|
||||
"@tiptap/starter-kit": "^2.10.3",
|
||||
"remixicon": "^4.5.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"tiptap-markdown": "^0.8.10"
|
||||
}
|
||||
}
|
||||
6
stroma/file/frontend/postcss.config.js
Normal file
6
stroma/file/frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
130
stroma/file/frontend/src/editor.js
Normal file
130
stroma/file/frontend/src/editor.js
Normal file
@@ -0,0 +1,130 @@
|
||||
import { Editor } from '@tiptap/core';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import { Markdown } from 'tiptap-markdown';
|
||||
import Placeholder from '@tiptap/extension-placeholder';
|
||||
import BubbleMenu from '@tiptap/extension-bubble-menu';
|
||||
import './styles.css';
|
||||
import 'remixicon/fonts/remixicon.css';
|
||||
|
||||
// DOM Elements
|
||||
const saveBtn = document.getElementById('save-btn');
|
||||
const editorContainer = document.getElementById('editor-container');
|
||||
const bubbleMenuEl = document.getElementById('bubble-menu');
|
||||
|
||||
// Editor Instance
|
||||
let editor;
|
||||
let currentFileId = null;
|
||||
|
||||
// CSRF Helper
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
// Initialize Editor (only once)
|
||||
function initEditor() {
|
||||
// Bubble Menu Buttons
|
||||
const btnBold = document.getElementById('btn-bold');
|
||||
const btnItalic = document.getElementById('btn-italic');
|
||||
const btnStrike = document.getElementById('btn-strike');
|
||||
const btnCode = document.getElementById('btn-code');
|
||||
|
||||
editor = new Editor({
|
||||
element: document.querySelector('#editor'),
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Markdown,
|
||||
Placeholder.configure({
|
||||
placeholder: "Type '/' for commands...",
|
||||
}),
|
||||
BubbleMenu.configure({
|
||||
element: bubbleMenuEl,
|
||||
tippyOptions: { duration: 100 },
|
||||
}),
|
||||
],
|
||||
content: '',
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: 'prose prose-sm sm:prose lg:prose-lg max-w-none focus:outline-none',
|
||||
},
|
||||
},
|
||||
onSelectionUpdate({ editor }) {
|
||||
btnBold?.classList.toggle('is-active', editor.isActive('bold'));
|
||||
btnItalic?.classList.toggle('is-active', editor.isActive('italic'));
|
||||
btnStrike?.classList.toggle('is-active', editor.isActive('strike'));
|
||||
btnCode?.classList.toggle('is-active', editor.isActive('code'));
|
||||
}
|
||||
});
|
||||
|
||||
// Bubble Menu Listeners
|
||||
if (btnBold) btnBold.addEventListener('click', () => editor.chain().focus().toggleBold().run());
|
||||
if (btnItalic) btnItalic.addEventListener('click', () => editor.chain().focus().toggleItalic().run());
|
||||
if (btnStrike) btnStrike.addEventListener('click', () => editor.chain().focus().toggleStrike().run());
|
||||
if (btnCode) btnCode.addEventListener('click', () => editor.chain().focus().toggleCode().run());
|
||||
|
||||
// Container Focus
|
||||
if (editorContainer) {
|
||||
editorContainer.addEventListener('click', () => {
|
||||
if (editor && !editor.isFocused) {
|
||||
editor.chain().focus().run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Expose to global scope for SPA usage
|
||||
window.editorInstance = editor;
|
||||
window.setEditorFileId = (fileId) => { currentFileId = fileId; };
|
||||
}
|
||||
|
||||
// Save Content
|
||||
if (saveBtn) {
|
||||
saveBtn.addEventListener('click', async () => {
|
||||
if (!editor || !currentFileId) return;
|
||||
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = 'Saving...';
|
||||
|
||||
const markdownContent = editor.storage.markdown.getMarkdown();
|
||||
const saveUrl = `/file/content/${currentFileId}/save/`;
|
||||
|
||||
try {
|
||||
const response = await fetch(saveUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
},
|
||||
body: JSON.stringify({ content: markdownContent })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
saveBtn.textContent = 'Saved!';
|
||||
setTimeout(() => {
|
||||
saveBtn.textContent = 'Save Changes';
|
||||
saveBtn.disabled = false;
|
||||
}, 1000);
|
||||
} else {
|
||||
throw new Error('Save failed');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Failed to save file');
|
||||
saveBtn.textContent = 'Save Changes';
|
||||
saveBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize editor on load
|
||||
if (document.querySelector('#editor')) {
|
||||
initEditor();
|
||||
}
|
||||
2
stroma/file/frontend/src/styles.css
Normal file
2
stroma/file/frontend/src/styles.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@import "tailwindcss";
|
||||
@import 'remixicon/fonts/remixicon.css';
|
||||
13
stroma/file/frontend/tailwind.config.js
Normal file
13
stroma/file/frontend/tailwind.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
"../../templates/**/*.html",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/typography'),
|
||||
],
|
||||
}
|
||||
25
stroma/file/frontend/vite.config.js
Normal file
25
stroma/file/frontend/vite.config.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
root: 'src',
|
||||
base: '/static/file/editor_dist/',
|
||||
build: {
|
||||
outDir: '../../static/file/editor_dist',
|
||||
emptyOutDir: true,
|
||||
manifest: false, // simplified for now
|
||||
rollupOptions: {
|
||||
input: path.resolve(__dirname, 'src/editor.js'),
|
||||
output: {
|
||||
entryFileNames: `assets/[name].js`,
|
||||
chunkFileNames: `assets/[name].js`,
|
||||
assetFileNames: `assets/[name].[ext]`
|
||||
}
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user