feat: add custom CSS save system with dual-editor interface
All checks were successful
Deploy / Build and Deploy to Production (push) Successful in 16s

Implement complete custom CSS management system:
- Separate base CSS (readonly) and custom CSS (editable)
- Save custom CSS to Kirby backend per narrative
- Visual save button with state indicators (dirty/saving/success/error)
- CSRF-protected API endpoint for CSS operations
- Dual-editor StylesheetViewer (base + custom with edit mode toggle)
- Auto-format custom CSS with Prettier on edit mode exit

Backend changes:
- Add web2print Kirby plugin with POST/GET routes
- Add customCss field to narrative blueprint
- Add CSRF token meta tag in header
- Include customCss and modified timestamps in JSON template
- Install code-editor plugin for Kirby panel

Frontend changes:
- Refactor stylesheet store with baseCss/customCss refs
- Make content a computed property (baseCss + customCss)
- Add helper methods: replaceBlock, replaceInCustomCss, setCustomCss
- Update all components to use new store API
- Create SaveButton component with FAB design
- Redesign StylesheetViewer with collapsable sections
- Initialize store from narrative data on app mount

File changes:
- Rename stylesheet.css → stylesheet.print.css
- Update all references to new filename

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
isUnknown 2026-01-09 13:39:25 +01:00
parent 4d1183d1af
commit 0f46618066
32 changed files with 1207 additions and 89 deletions

View file

@ -354,10 +354,7 @@ const updateMargins = () => {
`$1${marginValue}`
);
stylesheetStore.content = stylesheetStore.content.replace(
currentBlock,
updatedBlock
);
stylesheetStore.replaceBlock(currentBlock, updatedBlock);
};
// Watch margin values (number inputs) with debounce
@ -398,19 +395,13 @@ const updateBackground = () => {
/(background:\s*)[^;]+/,
`$1${background.value.value}`
);
stylesheetStore.content = stylesheetStore.content.replace(
currentBlock,
updatedBlock
);
stylesheetStore.replaceBlock(currentBlock, updatedBlock);
} else {
const updatedBlock = currentBlock.replace(
/(\s*})$/,
` background: ${background.value.value};\n$1`
);
stylesheetStore.content = stylesheetStore.content.replace(
currentBlock,
updatedBlock
);
stylesheetStore.replaceBlock(currentBlock, updatedBlock);
}
};
@ -457,7 +448,7 @@ watch(runningTitle, (enabled) => {
});
const updatePageFooters = () => {
let currentCss = stylesheetStore.content;
let currentCss = stylesheetStore.customCss;
// Remove existing @page:left and @page:right rules
currentCss = currentCss.replace(/@page:left\s*\{[^}]*\}/g, '');
@ -539,7 +530,7 @@ const updatePageFooters = () => {
currentCss.slice(insertPosition);
}
stylesheetStore.content = currentCss;
stylesheetStore.setCustomCss(currentCss);
};
const syncFromStore = () => {