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

@ -356,7 +356,7 @@ const getOrCreateTemplateBlock = () => {
const marginValue = `${margins.value.top.value}${margins.value.top.unit} ${margins.value.right.value}${margins.value.right.unit} ${margins.value.bottom.value}${margins.value.bottom.unit} ${margins.value.left.value}${margins.value.left.unit}`;
const newBlock = `\n@page ${templateName.value} {\n margin: ${marginValue};${background.value.value ? `\n background: ${background.value.value};` : ''}\n}\n`;
stylesheetStore.content = stylesheetStore.content.replace(
stylesheetStore.replaceInCustomCss(
baseBlock,
baseBlock + newBlock
);
@ -376,7 +376,7 @@ const removeTemplateBlock = () => {
if (block) {
// Remove the block and any surrounding whitespace
stylesheetStore.content = stylesheetStore.content.replace(
stylesheetStore.replaceInCustomCss(
new RegExp(`\\n?${selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\{[^}]*\\}\\n?`),
'\n'
);
@ -399,7 +399,7 @@ const updateMargins = (force = false) => {
/(margin:\s*)[^;]+/,
`$1${marginValue}`
);
stylesheetStore.content = stylesheetStore.content.replace(
stylesheetStore.replaceInCustomCss(
currentBlock,
updatedBlock
);
@ -408,7 +408,7 @@ const updateMargins = (force = false) => {
/(\s*})$/,
` margin: ${marginValue};\n$1`
);
stylesheetStore.content = stylesheetStore.content.replace(
stylesheetStore.replaceInCustomCss(
currentBlock,
updatedBlock
);
@ -428,7 +428,7 @@ const updateBackground = (force = false) => {
/(background:\s*)[^;]+/,
`$1${background.value.value}`
);
stylesheetStore.content = stylesheetStore.content.replace(
stylesheetStore.replaceInCustomCss(
currentBlock,
updatedBlock
);
@ -437,7 +437,7 @@ const updateBackground = (force = false) => {
/(\s*})$/,
` background: ${background.value.value};\n$1`
);
stylesheetStore.content = stylesheetStore.content.replace(
stylesheetStore.replaceInCustomCss(
currentBlock,
updatedBlock
);
@ -674,7 +674,7 @@ const handleCssInput = (event) => {
// Get the actual CSS block (not the commented preview)
const oldBlock = pageCss.value;
if (oldBlock) {
stylesheetStore.content = stylesheetStore.content.replace(
stylesheetStore.replaceInCustomCss(
oldBlock,
newCss
);
@ -688,7 +688,7 @@ watch(isEditable, async (newValue, oldValue) => {
// Format when exiting editing mode
if (oldValue && !newValue) {
await stylesheetStore.formatContent();
await stylesheetStore.formatCustomCss();
}
});