Compare commits

..

3 commits

Author SHA1 Message Date
isUnknown
cb5d056b51 fix: restore TextSettings functionality after store refactoring
All checks were successful
Deploy / Build and Deploy to Production (push) Successful in 16s
Fixed TextSettings fields not updating the stylesheet and preview after
the store refactoring that made content a computed property.

- Add missing font watcher in TextSettings.vue
- Update useCssUpdater.js to use store.replaceBlock() instead of
  writing to readonly store.content
- Update createRule() to append to store.customCss instead of store.content

All TextSettings fields (font, size, margins, padding, alignment) now
correctly update the stylesheet and preview.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-09 16:42:34 +01:00
isUnknown
e42eeab437 feat: add scrollable CSS editor and complete stylesheet export
Add two improvements to StylesheetViewer:

1. Scrollable CSS sections
   - Add max-height (500px) and overflow-y to custom CSS editor
   - Applies to both read-only and editable modes
   - Improves UX when content exceeds viewport

2. Complete stylesheet export button
   - Exports merged base CSS + custom CSS to single file
   - Filename format: <narrative-slug>-style.print.css
   - Includes informative comments:
     * Header with narrative title and download date
     * Section markers for base CSS and custom CSS
   - Full-width button below custom CSS section
   - Download via blob + automatic cleanup

Export file structure:
- Header comment (narrative info, date)
- Base CSS section with comment
- Custom CSS section with comment

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-09 16:30:18 +01:00
isUnknown
f8ac1ec8fc untrack content 2026-01-09 16:22:56 +01:00
5 changed files with 130 additions and 5 deletions

5
public/.gitignore vendored
View file

@ -48,3 +48,8 @@ Icon
# ---------------
/site/config/.license
# Content
# ---------------
content
/content/*

View file

@ -22,6 +22,20 @@ Customcss:
background: rgba(255, 255, 255, 1);
}
p {
font-family: DM Sans;
font-style: normal;
font-weight: 400;
font-size: 16px;
text-align: start;
color: rgb(0, 0, 0);
background: rgba(113, 54, 255, 0.1);
margin: 0px;
padding: 0px;
background: blue;
}
----
Uuid: xi60pjkz5bp1nlwp

View file

@ -50,12 +50,21 @@
></textarea>
</div>
</div>
<!-- Export Button -->
<button class="export-button" @click="handleExport" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M9 16v-6H5l7-7 7 7h-4v6H9zm-4 4h14v-2H5v2z"/>
</svg>
<span>Exporter la feuille de style complète</span>
</button>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
import { useStylesheetStore } from '../stores/stylesheet';
import { useNarrativeStore } from '../stores/narrative';
import CssFileImport from './ui/CssFileImport.vue';
import hljs from 'highlight.js/lib/core';
import css from 'highlight.js/lib/languages/css';
@ -64,6 +73,7 @@ import 'highlight.js/styles/atom-one-dark.css';
hljs.registerLanguage('css', css);
const stylesheetStore = useStylesheetStore();
const narrativeStore = useNarrativeStore();
const isBaseCssExpanded = ref(false);
const isCustomCssEditable = ref(false);
let debounceTimer = null;
@ -99,6 +109,65 @@ const handleCssImport = (cssContent) => {
stylesheetStore.customCss = cssContent;
};
const handleExport = () => {
const now = new Date();
const dateStr = now.toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
const narrativeTitle = narrativeStore.data?.title || 'Sans titre';
const narrativeSlug = narrativeStore.data?.slug || 'narrative';
// Build complete CSS with comments
let completeCSS = `/*
* Feuille de style pour l'impression
* Récit : ${narrativeTitle}
* Téléchargé le : ${dateStr}
*
* Ce fichier contient le CSS de base et le CSS personnalisé
* fusionnés pour une utilisation hors ligne.
*/
`;
// Add base CSS
if (stylesheetStore.baseCss) {
completeCSS += `/* ========================================
* CSS DE BASE
* Styles par défaut de l'application
* ======================================== */
${stylesheetStore.baseCss}
`;
}
// Add custom CSS
if (stylesheetStore.customCss) {
completeCSS += `/* ========================================
* CSS PERSONNALISÉ
* Styles spécifiques à ce récit
* ======================================== */
${stylesheetStore.customCss}`;
}
// Create blob and download
const blob = new Blob([completeCSS], { type: 'text/css' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${narrativeSlug}-style.print.css`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
// Watch editing mode and format when exiting
watch(isCustomCssEditable, async (newValue, oldValue) => {
stylesheetStore.isEditing = newValue;
@ -234,6 +303,7 @@ h3 {
font-family: 'Courier New', Courier, monospace;
font-size: 0.875rem;
line-height: 1.5;
max-height: 500px;
}
.readonly code {
@ -244,6 +314,8 @@ textarea {
width: 100%;
flex: 1;
min-height: 300px;
max-height: 500px;
overflow-y: auto;
background: #1e1e1e;
color: #abb2bf;
border: none;
@ -259,4 +331,33 @@ textarea::placeholder {
color: #5c6370;
font-style: italic;
}
.export-button {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
width: 100%;
padding: 0.75rem 1rem;
margin-top: 1rem;
background: #2c313c;
color: #abb2bf;
border: 1px solid #3e4451;
border-radius: 0.25rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.export-button:hover {
background: #3e4451;
border-color: #61afef;
color: #61afef;
}
.export-button svg {
width: 1.25rem;
height: 1.25rem;
}
</style>

View file

@ -452,6 +452,11 @@ const updateMarginInnerUnit = (unit) => {
};
// Watchers for body styles
watch(font, (val) => {
if (isUpdatingFromStore) return;
updateStyle('body', 'font-family', `"${val}"`);
});
watch(italic, (val) => {
if (isUpdatingFromStore) return;
updateStyle('body', 'font-style', val ? 'italic' : 'normal');

View file

@ -14,13 +14,13 @@ export function useCssUpdater() {
new RegExp(`(${property}:\\s*)[^;]+`, 'i'),
`$1${value}`
);
store.content = store.content.replace(currentBlock, updatedBlock);
store.replaceBlock(currentBlock, updatedBlock);
} else {
const updatedBlock = currentBlock.replace(
/(\s*})$/,
` ${property}: ${value};\n$1`
);
store.content = store.content.replace(currentBlock, updatedBlock);
store.replaceBlock(currentBlock, updatedBlock);
}
};
@ -37,7 +37,7 @@ export function useCssUpdater() {
);
if (updatedBlock !== currentBlock) {
store.content = store.content.replace(currentBlock, updatedBlock);
store.replaceBlock(currentBlock, updatedBlock);
}
};
@ -57,7 +57,7 @@ export function useCssUpdater() {
}
if (updatedBlock !== currentBlock) {
store.content = store.content.replace(currentBlock, updatedBlock);
store.replaceBlock(currentBlock, updatedBlock);
}
};
@ -65,7 +65,7 @@ export function useCssUpdater() {
* Create a new CSS rule for a selector
*/
const createRule = (selector) => {
store.content += `\n\n${selector} {\n}\n`;
store.customCss += `\n\n${selector} {\n}\n`;
return `${selector} {\n}`;
};