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>
This commit is contained in:
parent
f8ac1ec8fc
commit
e42eeab437
1 changed files with 101 additions and 0 deletions
|
|
@ -50,12 +50,21 @@
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch } from 'vue';
|
import { ref, computed, watch } from 'vue';
|
||||||
import { useStylesheetStore } from '../stores/stylesheet';
|
import { useStylesheetStore } from '../stores/stylesheet';
|
||||||
|
import { useNarrativeStore } from '../stores/narrative';
|
||||||
import CssFileImport from './ui/CssFileImport.vue';
|
import CssFileImport from './ui/CssFileImport.vue';
|
||||||
import hljs from 'highlight.js/lib/core';
|
import hljs from 'highlight.js/lib/core';
|
||||||
import css from 'highlight.js/lib/languages/css';
|
import css from 'highlight.js/lib/languages/css';
|
||||||
|
|
@ -64,6 +73,7 @@ import 'highlight.js/styles/atom-one-dark.css';
|
||||||
hljs.registerLanguage('css', css);
|
hljs.registerLanguage('css', css);
|
||||||
|
|
||||||
const stylesheetStore = useStylesheetStore();
|
const stylesheetStore = useStylesheetStore();
|
||||||
|
const narrativeStore = useNarrativeStore();
|
||||||
const isBaseCssExpanded = ref(false);
|
const isBaseCssExpanded = ref(false);
|
||||||
const isCustomCssEditable = ref(false);
|
const isCustomCssEditable = ref(false);
|
||||||
let debounceTimer = null;
|
let debounceTimer = null;
|
||||||
|
|
@ -99,6 +109,65 @@ const handleCssImport = (cssContent) => {
|
||||||
stylesheetStore.customCss = 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 editing mode and format when exiting
|
||||||
watch(isCustomCssEditable, async (newValue, oldValue) => {
|
watch(isCustomCssEditable, async (newValue, oldValue) => {
|
||||||
stylesheetStore.isEditing = newValue;
|
stylesheetStore.isEditing = newValue;
|
||||||
|
|
@ -234,6 +303,7 @@ h3 {
|
||||||
font-family: 'Courier New', Courier, monospace;
|
font-family: 'Courier New', Courier, monospace;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
max-height: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.readonly code {
|
.readonly code {
|
||||||
|
|
@ -244,6 +314,8 @@ textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
background: #1e1e1e;
|
background: #1e1e1e;
|
||||||
color: #abb2bf;
|
color: #abb2bf;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
@ -259,4 +331,33 @@ textarea::placeholder {
|
||||||
color: #5c6370;
|
color: #5c6370;
|
||||||
font-style: italic;
|
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>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue