refactor: rename 'recit' to 'narrative' for English code naming

- Rename store: recit.js → narrative.js (useRecitStore → useNarrativeStore)
- Rename templates: recit.php/json.php → narrative.php/json.php
- Rename blueprint: recit.yml → narrative.yml
- Update all imports and references in Vue/JS files
- Update PHP template references and data attributes
- Update CLAUDE.md documentation
- Create comprehensive README.md with English-French dictionary

The dictionary section maps English code terms to French content terms
for easier navigation between codebase and CMS content.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
isUnknown 2026-01-09 10:34:10 +01:00
parent ea0994ed45
commit af788ad1e0
12 changed files with 267 additions and 66 deletions

206
README.md
View file

@ -1,5 +1,205 @@
# Vue 3 + Vite
# GeoProject - Web-to-Print Interface
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
A web-to-print application for creating printable narratives with real-time layout editing.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
## Overview
GeoProject is a sophisticated web-to-print platform that combines:
- **Kirby CMS** for content management
- **Vue 3** for interactive editing interface
- **PagedJS** for print-ready rendering
The application allows users to create and edit multi-page narratives with dynamic layouts, supporting various content blocks (text, images, videos, maps) and custom page templates.
## Tech Stack
- **Frontend**: Vue 3 (Composition API) + Vite
- **Print Engine**: PagedJS (CSS Paged Media)
- **CMS**: Kirby 5 (headless, flat-file)
- **Backend**: PHP 8.1+
- **State Management**: Pinia
- **Styling**: CSS with CSS Variables
## Project Structure
```
/src # Vue 3 SPA
├── main.js # Vue bootstrap
├── App.vue # Root component + PagedJS init
├── components/
│ ├── blocks/ # Content block components (HeadingBlock, TextBlock, etc.)
│ ├── editor/ # Editor panels (PageSettings, TextSettings, etc.)
│ ├── ui/ # Reusable UI components (InputWithUnit, MarginEditor, etc.)
│ └── *.vue # Core components (PagedJsWrapper, ElementPopup, etc.)
├── composables/ # Vue composables (useCssSync, useCssUpdater, etc.)
├── stores/ # Pinia stores (narrative.js, stylesheet.js)
└── utils/ # JavaScript utilities
/public # Kirby CMS + static assets
├── site/
│ ├── blueprints/ # Content schemas
│ ├── templates/ # PHP templates
│ ├── snippets/ # PHP snippets
│ └── plugins/ # Kirby plugins
├── content/ # Markdown content files
└── assets/ # Static assets (CSS, fonts, SVG)
/.forgejo/workflows # CI/CD pipeline
```
## Key Features
### Content Types
- **Narratives**: Main story containers with cover, author, introduction
- **Geoformats**: Structured content sections with chapters
- **Chapters**: Individual chapters with rich content blocks
- **Maps**: Special map-based content pages
### Content Blocks
- Text blocks with rich formatting
- Headings with customizable levels
- Images with captions and positioning
- Lists (ordered and unordered)
- Blockquotes with citations
- Video embeds
- Interactive maps
### Print Features
- Real-time preview with PagedJS rendering
- Custom @page rules for different templates
- Interactive element and page editing
- CSS variable-based theming
- Print-optimized output
## Getting Started
### Development
```bash
# Install dependencies
npm install
# Start Vite dev server
npm run dev
# Start PHP server for Kirby (separate terminal)
php -S localhost:8000 -t public
```
The Vue app will be served at `http://localhost:5173` and Kirby at `http://localhost:8000`.
### Production Build
```bash
# Build for production
npm run build
```
Builds are output to `/public/assets/dist/`.
## Data Flow
1. **Kirby CMS** stores and manages content as flat files
2. **PHP Templates** render the HTML structure and inject Vue
3. **Vue App** provides the interactive editing interface
4. **PagedJS** transforms content into print-ready pages
## API
### Narrative JSON Endpoint
```
GET /projet/{narrative-slug}.json
```
Returns the complete narrative data structure including all child pages, blocks, and metadata.
## Naming Conventions
- **Vue Components**: PascalCase (e.g., `PagedJsWrapper.vue`)
- **Composables**: Prefixed with `use` (e.g., `useCssSync`)
- **Stores**: camelCase files, PascalCase store names (e.g., `useNarrativeStore`)
- **Code Language**: English preferred for all code, comments, and identifiers
## English-French Dictionary
The codebase uses English naming conventions, but some French terms remain in content and templates for compatibility. Here's a reference guide:
### Core Concepts
| English | French | Context |
|---------|--------|---------|
| narrative | récit | Main content container type |
| chapter | chapitre | Chapter/section within a geoformat |
| map | carte | Map-based content page |
| cover | couverture | Cover page/image |
| author | auteur | Narrative author(s) |
| introduction | introduction | Introductory text |
| print | impression | Print/output functionality |
### Template Types
| English | French | File/Template Name |
|---------|--------|--------------------|
| narrative | recit | `narrative.php`, `narrative.json.php` |
| chapter | chapitre | `chapitre.php` |
| map | carte | `carte.php` |
| geoformat | geoformat | `geoformat.php` |
### UI Elements
| English | French | Notes |
|---------|--------|-------|
| settings | paramètres | Editor panel settings |
| page | page | Page template/type |
| block | bloc | Content block |
| edit | éditer | Edit action |
| preview | aperçu | Preview mode |
### Technical Terms
| English | French | Notes |
|---------|--------|-------|
| store | magasin | Pinia store (use English 'store') |
| template | template/modèle | Page template |
| blueprint | schéma | Kirby content schema |
| field | champ | Form/content field |
### Code Examples
**Store naming:**
```javascript
// Correct
import { useNarrativeStore } from './stores/narrative';
// Old (deprecated)
import { useRecitStore } from './stores/recit';
```
**Template references:**
```javascript
// Check for narrative template
if (item.template === 'narrative') { /* ... */ }
// Check for chapter template
if (item.template === 'chapitre') { /* ... */ }
```
**CSS classes:**
```css
/* Narrative cover page */
.narrative-cover { /* ... */ }
/* Chapter content */
.chapitre { /* ... */ }
```
## CI/CD
The project uses Forgejo Actions for continuous deployment:
1. Code is pushed to Forgejo repository
2. Workflow builds the Vue app
3. Files are deployed via FTP to production server
See `.forgejo/workflows/deploy.yml` for details.
## Contributing
For detailed development guidelines, see `CLAUDE.md`.

View file

@ -2,7 +2,7 @@
## Vue d'ensemble
Application web-to-print permettant la mise en page de récits imprimables. L'édition de contenu se fait via Kirby CMS (headless), la mise en page est rendue par PagedJS, et l'interface d'édition réactive utilise Vue 3.
Application web-to-print permettant la mise en page de récits (narratives) imprimables. L'édition de contenu se fait via Kirby CMS (headless), la mise en page est rendue par PagedJS, et l'interface d'édition réactive utilise Vue 3.
## Stack technique
@ -30,7 +30,7 @@ Application web-to-print permettant la mise en page de récits imprimables. L'é
│ ├── PreviewLoader.vue # Loader de preview
│ └── StylesheetViewer.vue # Viewer de feuilles de style
├── composables/ # COMPOSABLES VUE (useCssSync, useCssUpdater, etc.)
├── stores/ # STORES PINIA (recit.js, stylesheet.js)
├── stores/ # STORES PINIA (narrative.js, stylesheet.js)
└── utils/ # UTILITAIRES JS (css-parsing.js, etc.)
/api/cache # Cache des données API (donorbox_data.json, etc.)
@ -78,7 +78,7 @@ Application web-to-print permettant la mise en page de récits imprimables. L'é
### State Management
- **Pinia** utilisé pour la gestion d'état
- `stores/recit.js` : État du récit (contenu, navigation)
- `stores/narrative.js` : État du récit/narrative (contenu, navigation)
- `stores/stylesheet.js` : État des feuilles de style CSS
### Conventions de placement (IMPORTANT : respecter cette organisation)
@ -149,5 +149,6 @@ site/sessions/ # Sessions actives
- Composants Vue : PascalCase
- CSS : Variables pour theming, scoped styles
- Print CSS : W3C Paged Media spec
- Stores Pinia : camelCase pour les fichiers, PascalCase pour les noms (ex: `useRecitStore`)
- Stores Pinia : camelCase pour les fichiers, PascalCase pour les noms (ex: `useNarrativeStore`)
- Composables : Préfixe `use` (ex: `useCssSync`)
- Code naming : English preferred (ex: `narrative` instead of `recit`)

View file

@ -1,4 +1,4 @@
title: Récit
title: Narrative
columns:
main:

View file

@ -17,9 +17,9 @@ columns:
multiple: false
width: 1/2
pages:
label: Récits
label: Narratives
type: pages
template: recit
template: narrative
sidebar:
width: 1/3
sections:

View file

@ -2,8 +2,8 @@
/**
* Virtual Print Page Plugin
*
* Crée une page virtuelle /print pour chaque récit
* Permet d'accéder à l'éditeur d'impression via /projet/recit/print
* Creates a virtual /print page for each narrative
* Allows access to print editor via /projet/narrative/print
*/
use Kirby\Cms\Page;
@ -14,20 +14,20 @@ Kirby::plugin('geoproject/virtual-print-page', [
[
'pattern' => '(:all)/print',
'action' => function ($parentPath) {
// Trouver la page parente (le récit)
// Find parent page (the narrative)
$parent = page($parentPath);
if (!$parent || $parent->intendedTemplate()->name() !== 'recit') {
if (!$parent || $parent->intendedTemplate()->name() !== 'narrative') {
return $this->next();
}
// Créer la page virtuelle avec Page::factory()
// Create virtual page with Page::factory()
return Page::factory([
'slug' => 'print',
'template' => 'print',
'parent' => $parent,
'content' => [
'title' => 'Impression - ' . $parent->title()->value(),
'title' => 'Print - ' . $parent->title()->value(),
'uuid' => Uuid::generate()
]
]);

View file

@ -26,5 +26,5 @@
<?php endif ?>
</head>
<body data-template="<?= $page->template() ?>"<?php if (isset($recitJsonUrl)): ?> data-recit-url="<?= $recitJsonUrl ?>"<?php endif ?>>
<body data-template="<?= $page->template() ?>"<?php if (isset($narrativeJsonUrl)): ?> data-narrative-url="<?= $narrativeJsonUrl ?>"<?php endif ?>>
<div id="app">

View file

@ -1,7 +1,7 @@
<?php
/**
* Template JSON pour exposer les données d'un récit
* Accessible via /projet/recit.json ou /projet/recit?format=json
* JSON template to expose narrative data
* Accessible via /projet/narrative.json or /projet/narrative?format=json
*/
header('Content-Type: application/json; charset=utf-8');
@ -175,11 +175,11 @@ function parseGeoformat($geoformat) {
];
}
// Construction de la réponse JSON
// Build JSON response
$data = [
'id' => $page->id(),
'uuid' => $page->uuid()->toString(),
'template' => 'recit',
'template' => 'narrative',
'title' => $page->title()->value(),
'slug' => $page->slug(),
'author' => $page->author()->value() ?? '',
@ -188,7 +188,7 @@ $data = [
'children' => []
];
// Parser les enfants (geoformats et cartes)
// Parse children (geoformats and maps)
foreach ($page->children()->listed() as $child) {
$template = $child->intendedTemplate()->name();

View file

@ -1,12 +1,12 @@
<?php
/**
* Template pour afficher un récit
* Ce template est requis pour que recit.json.php fonctionne
* Template to display a narrative
* This template is required for narrative.json.php to work
*/
?>
<?php snippet('header') ?>
<article class="recit">
<article class="narrative">
<h1><?= $page->title() ?></h1>
<?php if ($page->author()->isNotEmpty()): ?>
@ -27,7 +27,7 @@
</div>
<?php endif ?>
<p><a href="<?= $page->url() ?>/print">Ouvrir l'éditeur d'impression</a></p>
<p><a href="<?= $page->url() ?>/print">Open print editor</a></p>
</article>
<?php snippet('footer') ?>

View file

@ -1,17 +1,17 @@
<?php
/**
* Template pour l'éditeur d'impression Vue.js
* Route: /projet/recit/print
* Template for Vue.js print editor
* Route: /projet/narrative/print
*
* Ce template charge l'app Vue et lui passe l'URL JSON du récit parent
* This template loads the Vue app and passes the parent narrative JSON URL
*/
// Récupérer le récit parent
$recit = $page->parent();
// Get parent narrative
$narrative = $page->parent();
// Construire l'URL JSON du récit
$recitJsonUrl = $recit->url() . '.json';
// Build narrative JSON URL
$narrativeJsonUrl = $narrative->url() . '.json';
?>
<?php snippet('header', ['recitJsonUrl' => $recitJsonUrl]) ?>
<?php snippet('header', ['narrativeJsonUrl' => $narrativeJsonUrl]) ?>
<?php snippet('footer') ?>

View file

@ -6,14 +6,14 @@ import PagePopup from './components/PagePopup.vue';
import PreviewLoader from './components/PreviewLoader.vue';
import { onMounted, ref, watch, computed, provide } from 'vue';
import { useStylesheetStore } from './stores/stylesheet';
import { useRecitStore } from './stores/recit';
import { useNarrativeStore } from './stores/narrative';
import Coloris from '@melloware/coloris';
const stylesheetStore = useStylesheetStore();
const recitStore = useRecitStore();
const narrativeStore = useNarrativeStore();
// Get recit URL from body data attribute (set by print.php template)
const recitUrl = document.body.dataset.recitUrl || null;
// Get narrative URL from body data attribute (set by print.php template)
const narrativeUrl = document.body.dataset.narrativeUrl || null;
const previewFrame1 = ref(null);
const previewFrame2 = ref(null);
const elementPopup = ref(null);
@ -488,11 +488,11 @@ watch(
}
);
// Re-render when recit data changes
// Re-render when narrative data changes
watch(
() => recitStore.data,
() => narrativeStore.data,
() => {
if (recitStore.data) {
if (narrativeStore.data) {
renderPreview();
}
}
@ -558,9 +558,9 @@ const printPreview = async () => {
};
onMounted(async () => {
// Load recit data if URL is provided (print mode)
if (recitUrl) {
await recitStore.loadRecit(recitUrl);
// Load narrative data if URL is provided (print mode)
if (narrativeUrl) {
await narrativeStore.loadNarrative(narrativeUrl);
}
// Render preview after data is loaded

View file

@ -1,6 +1,6 @@
<template>
<!-- Fallback static content when no recit data -->
<template v-if="!hasRecitData">
<!-- Fallback static content when no narrative data -->
<template v-if="!hasNarrativeData">
<section class="chapter">
<p>
Accumsan arcu tristique purus eros pellentesque rutrum hendrerit
@ -10,13 +10,13 @@
</section>
</template>
<!-- Dynamic content from recit -->
<!-- Dynamic content from narrative -->
<template v-else>
<template v-for="item in flattenedContent" :key="item.id">
<!-- Récit (cover page) -->
<!-- Narrative (cover page) -->
<section
v-if="item.template === 'recit'"
class="recit-cover"
v-if="item.template === 'narrative'"
class="narrative-cover"
:data-page-type="item.template"
>
<img v-if="item.cover" :src="item.cover" class="cover-image" alt="" />
@ -75,7 +75,7 @@
<script setup>
import { computed } from 'vue';
import { useRecitStore } from '../stores/recit';
import { useNarrativeStore } from '../stores/narrative';
import {
TextBlock,
HeadingBlock,
@ -87,10 +87,10 @@ import {
blockComponents
} from './blocks';
const recitStore = useRecitStore();
const narrativeStore = useNarrativeStore();
const hasRecitData = computed(() => recitStore.data !== null);
const flattenedContent = computed(() => recitStore.flattenedContent);
const hasNarrativeData = computed(() => narrativeStore.data !== null);
const flattenedContent = computed(() => narrativeStore.flattenedContent);
// Filter out hidden blocks
const visibleBlocks = (blocks) => {
@ -114,24 +114,24 @@ const getBlockComponent = (type) => {
<style>
/* Base print styles for content sections */
.recit-cover,
.narrative-cover,
.geoformat,
.chapitre,
.carte {
break-before: page;
}
.recit-cover .cover-image,
.narrative-cover .cover-image,
.geoformat .cover-image {
max-width: 100%;
height: auto;
}
.recit-cover h1 {
.narrative-cover h1 {
margin-top: 1rem;
}
.recit-cover .author {
.narrative-cover .author {
font-style: italic;
color: #666;
}

View file

@ -1,7 +1,7 @@
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useRecitStore = defineStore('recit', () => {
export const useNarrativeStore = defineStore('narrative', () => {
const data = ref(null);
const loading = ref(false);
const error = ref(null);
@ -19,10 +19,10 @@ export const useRecitStore = defineStore('recit', () => {
const items = [];
// Add recit intro as first section
// Add narrative intro as first section
items.push({
id: data.value.id,
template: 'recit',
template: 'narrative',
title: data.value.title,
author: data.value.author,
cover: data.value.cover,
@ -72,10 +72,10 @@ export const useRecitStore = defineStore('recit', () => {
return items;
});
// Load recit data from URL
const loadRecit = async (url) => {
// Load narrative data from URL
const loadNarrative = async (url) => {
if (!url) {
error.value = 'No recit URL provided';
error.value = 'No narrative URL provided';
return;
}
@ -91,7 +91,7 @@ export const useRecitStore = defineStore('recit', () => {
data.value = await response.json();
} catch (e) {
console.error('Error loading recit:', e);
console.error('Error loading narrative:', e);
error.value = e.message;
data.value = null;
} finally {
@ -121,7 +121,7 @@ export const useRecitStore = defineStore('recit', () => {
flattenedContent,
// Actions
loadRecit,
loadNarrative,
reset
};
});