story-theme-mcp 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -16
- package/package.json +1 -1
- package/src/build/assemblage.js +209 -0
- package/src/build/compose.js +207 -20
- package/src/build/fiche.js +229 -0
- package/src/build/moments.js +656 -0
- package/src/build/nettoyage.js +262 -0
- package/src/build/palette.js +14 -5
- package/src/build/quick.js +668 -329
- package/src/build/systeme.js +123 -0
- package/src/knowledge/brancher.js +35 -0
- package/src/knowledge/demos.js +1 -1
- package/src/knowledge/descriptions.js +264 -113
- package/src/knowledge/directions.js +11 -1
- package/src/knowledge/fonts.js +2 -1
- package/src/knowledge/guides.js +37 -15
- package/src/knowledge/metiers.js +321 -0
- package/src/migrate/extract.js +14 -4
- package/src/migrate/source.js +6 -10
- package/src/migrate/v3.js +70 -12
- package/src/project/review.js +43 -7
- package/src/project/store.js +7 -0
- package/src/server.js +220 -97
- package/src/shops/app-tools.js +165 -0
- package/src/shops/client.js +2 -0
- package/src/shops/direct.js +247 -105
- package/src/shops/format.js +23 -1
- package/src/shops/tools.js +20 -5
- package/src/shops/update.js +390 -0
- package/src/validate/design.js +80 -7
- package/src/version.js +1 -1
package/README.md
CHANGED
|
@@ -6,9 +6,11 @@ Réservé aux titulaires d'une licence Story Thème. Le jeton se génère dans l
|
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
Vous : « Crée la boutique Maison Lumen : bougies parfumées coulées à la main à Lyon. »
|
|
9
|
-
IA :
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
IA : une seule salve de questions (photos, collection, conditions… ou « vas-y »), puis
|
|
10
|
+
une V1 pensée pour une bougie artisanale : manifeste, atelier, étapes de fabrication,
|
|
11
|
+
fiche produit à accordéons et badges, pages Notre histoire / Comment utiliser /
|
|
12
|
+
Contact / FAQ / Suivi, tous les textes écrits pour la marque, aucun texte d'exemple
|
|
13
|
+
du thème, zip prêt à importer — puis la liste de ce qui a été inventé.
|
|
12
14
|
```
|
|
13
15
|
|
|
14
16
|
## Installation
|
|
@@ -52,15 +54,16 @@ Le MCP ne contient pas le thème : au démarrage, il demande à l'espace membre
|
|
|
52
54
|
| Étape | Outils |
|
|
53
55
|
|---|---|
|
|
54
56
|
| Connaître le thème | `get_started`, `theme_source`, `search_theme`, `list_sections`, `get_section`, `list_blocks`, `get_block`, `get_theme_settings`, `get_theme_template`, `read_guide` |
|
|
55
|
-
| S'inspirer | `inspiration` : les
|
|
57
|
+
| S'inspirer | `inspiration` : les 10 boutiques démo composées par le designer — l'ossature de leurs pages, à regarder avant de construire ; `read_guide metiers` : la composition qui convient à chaque métier |
|
|
56
58
|
| Concevoir | `recipes` (15 compositions de pages), `generate_color_schemes`, `check_contrast`, `find_fonts` |
|
|
57
|
-
| Construire vite | `build_store` : la V1 complète d'une boutique en un appel
|
|
58
|
-
| Composer librement | `compose_page` : n'importe quelle page avec toutes les sections du thème, en donnant seulement le contenu (titre, texte, boutons, étapes, cartes, questions, chiffres…) |
|
|
59
|
+
| Construire vite | `build_store` : la V1 complète d'une boutique en un appel — structure choisie selon ce qui est vendu, ou selon le `plan` de pages de l'assistant ; direction artistique, pages de marque composées pour le métier (routine beauté, prise en main tech, entretien mode…), pages 404, mot de passe et blog dans la langue de la boutique, traduction, zip. Tout ou rien : un échec ramène le projet à son état d'avant |
|
|
60
|
+
| Composer librement | `compose_page` : n'importe quelle page avec toutes les sections du thème, en donnant seulement le contenu (titre, texte, boutons, étapes, cartes, questions, chiffres, colonnes d'un comparatif de modèles…) et la collection montrée |
|
|
59
61
|
| Construire | `create_project`, `update_brief`, `apply_recipe`, `localize_template`, `list_texts`, `edit_template`, `write_file`, `update_theme_settings`, `read_file`, `exclude_template` |
|
|
60
62
|
| Contrôler | `validate_json`, `review_project`, `undo`, `revert_file` |
|
|
61
63
|
| Livrer | `export_theme` (zip à importer + check-list), `import_theme` (auditer un thème téléchargé d'une boutique) |
|
|
62
64
|
| Passer en 4.x | `migrate_v3` : une boutique en Story Thème 3.7 ou avant, reconstruite en 4.x avec son contenu (textes, photos, avis, FAQ, produits, couleurs, polices, logo, menus), section par section — ce que la mise à jour de l'espace membre ne sait pas faire |
|
|
63
|
-
| Vos boutiques | `list_shops
|
|
65
|
+
| Vos boutiques (Theme Access) | `list_shops` (et la marche à suivre pour brancher une boutique), `pull_from_shop` (relire le thème de la boutique), `push_to_shop` (écrire le projet dans un thème non publié, sans zip), `update_theme` (mettre à jour le Story Thème dans un thème neuf non publié) — la boutique se branche une fois depuis l'espace membre avec un mot de passe Theme Access, qui n'ouvre que ses thèmes |
|
|
66
|
+
| L'appli privée Story Thème | `app_status`, `app_reviews` (import CSV, modération, réponses), `app_ab_tests`, `app_offers` (la remise des offres du thème), `app_discount_codes`, `shops_overview`, `shop_graphql` — sur les boutiques où l'équipe Story Thème a installé l'appli |
|
|
64
67
|
|
|
65
68
|
Prompts prêts à l'emploi : `creer-boutique`, `refaire-une-page`, `auditer-un-theme`. Les guides sont aussi exposés en ressources (`storytheme://guides/<sujet>`).
|
|
66
69
|
|
|
@@ -68,9 +71,10 @@ Prompts prêts à l'emploi : `creer-boutique`, `refaire-une-page`, `auditer-un-t
|
|
|
68
71
|
|
|
69
72
|
- **Aucun fichier invalide n'est enregistré.** Chaque modèle, groupe de sections et `settings_data.json` est validé contre les vrais schémas du thème : types de sections et de blocs, blocs privés, blocs statiques et leurs identifiants, réglages (identifiant, type, options, bornes et pas des curseurs), texte riche, liens, images, palettes, polices de la bibliothèque Shopify, limites Shopify (25 sections, 50 blocs, 8 niveaux). Les erreurs proposent la bonne orthographe (« vouliez-vous dire banner ? »).
|
|
70
73
|
- **Plus strict que Shopify Theme Check** : sur un même fichier piégé, Theme Check ne voit que le bloc interdit ; le MCP refuse aussi le réglage inconnu et la valeur hors limites.
|
|
71
|
-
- **
|
|
74
|
+
- **Aucun texte d'exemple livré** : après composition, chaque page est relue avec les règles de `list_texts`, dans les quatorze langues du thème ; un texte d'exemple restant part avec son bloc, ou la section part, et c'est dit. Sans annonce de la marque, la barre d'annonce est masquée plutôt que d'afficher les siennes ; le HTML glissé dans un texte est nettoyé.
|
|
75
|
+
- **Revue design** : textes d'exemple ou en anglais laissés sur une boutique française, avis d'exemple à remplacer, faux avis et faux chiffres des presets, bouton sans lien, plusieurs `<h1>`, contrastes insuffisants (WCAG AA, boutons compris), polices dépréciées par Shopify (Futura → Jost…), compte à rebours qui redémarre et stock simulé (pratiques commerciales trompeuses), pop-ups multiples, rythme des palettes.
|
|
72
76
|
- **Jamais le code du thème** : seuls `templates/*.json`, `sections/<groupe>.json` et `config/settings_data.json` sont modifiables, dans un dossier de projet. Le Liquid, le CSS et le JS du thème ne sont jamais touchés, ni en local ni à distance.
|
|
73
|
-
- **Écrire dans une boutique reste encadré** : `push_to_shop` n'envoie que les fichiers du projet, refuse une boutique qui ne tourne pas sur un Story Thème de la même version majeure,
|
|
77
|
+
- **Écrire dans une boutique reste encadré** : `push_to_shop` n'envoie que les fichiers du projet, refuse une boutique qui ne tourne pas sur un Story Thème de la même version majeure, n'écrit dans le thème publié que des fichiers nommés (`files`, ex. un modèle B de test A/B) avec confirmation explicite, jamais le projet entier, et ne supprime jamais rien à distance. Les thèmes s'ouvrent par Theme Access (mot de passe chiffré dans l'espace membre, révocable par le marchand) ; l'appli privée, seulement sur les boutiques dont le compte a prouvé la propriété. Chaque accès est journalisé.
|
|
74
78
|
- **Historique** : chaque écriture est précédée d'un instantané ; `undo` revient en arrière autant de fois que nécessaire.
|
|
75
79
|
- **Export bloqué** tant qu'une erreur bloquante reste ; signalé « pas prêt à publier » tant qu'un point 🔴 reste.
|
|
76
80
|
|
|
@@ -87,12 +91,14 @@ Prompts prêts à l'emploi : `creer-boutique`, `refaire-une-page`, `auditer-un-t
|
|
|
87
91
|
|
|
88
92
|
## Déroulé type
|
|
89
93
|
|
|
90
|
-
1. **Premier message** (« Crée la boutique X, qui vend Y ») : l'IA
|
|
91
|
-
- **
|
|
92
|
-
- **
|
|
93
|
-
- **
|
|
94
|
-
- **
|
|
95
|
-
- **
|
|
94
|
+
1. **Premier message** (« Crée la boutique X, qui vend Y ») : l'IA pose UNE seule salve de questions (ce qui est vendu et à qui, le style, couleur, logo et photos, les vraies conditions), terminée par « ou dites-moi "vas-y" ». Puis `build_store`, en un appel : couleurs et polices, accueil, fiche produit, collection, pages à propos / utilisation / contact / FAQ / suivi, reste du thème traduit, démos retirées, zip exporté.
|
|
95
|
+
- **Structure selon le métier** (`business`, déduit de l'activité) : la beauté prouve un résultat (avant / après, actifs, routine), la tech se compare (caractéristiques, comparatif, produit expliqué), la mode se montre portée (lookbook, acheter le look, guide des tailles et stock réel sur la fiche), l'épicerie raconte l'origine et se rachète (étapes, fiche, paliers), l'artisanat montre le geste, la maison fait entrer dans les rayons. Compositions relevées sur les démos du designer.
|
|
96
|
+
- **Ou le plan de l'assistant** (`plan`) : page par page, les sections au format de `compose_page` ; le MCP assemble, relie le catalogue, traduit et valide.
|
|
97
|
+
- **Textes** : tout ce qui s'affiche vient de `copy` (promesse, manifeste, bénéfices, histoire, étapes, FAQ, détails produit, caractéristiques, comparatif, paliers d'offre, guide des tailles…). Une section que la marque ne peut pas remplir est écartée, et l'assistant en reçoit la liste : **aucun texte d'exemple du thème** ne part dans une boutique.
|
|
98
|
+
- **Photos** : avec un connecteur Shopify, l'IA copie d'abord les photos de la boutique dans Contenu > Fichiers (`fileCreate`) et les passe à `build_store` (`images`) : héro, histoire, ambiance, avant / après, scène à points cliquables, photos clientes. Seules les photos de 1600 px de large ou plus (`images.large`) vont en plein écran. Sans le média, la section qui en dépend n'est pas posée : jamais de carré « image manquante ».
|
|
99
|
+
- **Preuves** : `proofs` ne reçoit que du réel (note, avis, chiffres). Les avis d'exemple d'une maquette vont dans `copy.example_reviews` : placés, déclarés au marchand, et signalés par `review_project` jusqu'à leur remplacement par de vrais avis.
|
|
100
|
+
- **Offres par paliers** : sur ce qui se rachète (consommables, soin, épicerie) ou à la demande, jamais d'office sur une robe ou une coque.
|
|
101
|
+
- **Direction artistique** (`direction`) : editorial, minimal, organique, artisan, douceur, pop, energie, tech. Chacune règle arrondis, boutons, survols, transitions de page, formes de séparation animées et polices, et ses moments signature (manifeste qui s'illumine, mots qui tournent, cartes empilées, étapes, grand visuel au défilement).
|
|
96
102
|
2. Elle résume la V1, donne le zip, puis pose 3 à 5 questions (collection principale, avis réels, conditions de livraison, couleur et logo, photos).
|
|
97
103
|
3. Chaque réponse s'applique au **projet en cours**, sans le re-préciser.
|
|
98
104
|
4. `review_project`, puis `export_theme` pour un nouveau zip.
|
|
@@ -113,7 +119,7 @@ npm run bundle # fabrique build/story-theme-mcp-<version>.mcpb (extension Cla
|
|
|
113
119
|
|
|
114
120
|
Après `npm run bundle`, déposer le `.mcpb` dans le dossier `mcp/` de l'espace membre : la page MCP & Appli sert automatiquement la version la plus récente qu'elle y trouve.
|
|
115
121
|
|
|
116
|
-
Avec `STORY_THEME_DIR` sur le dépôt du thème, le MCP lit le thème en cours d'écriture et n'appelle pas l'espace membre. Les connaissances rédigées à la main vivent dans `src/knowledge/` ; tout le reste vient du thème.
|
|
122
|
+
Avec `STORY_THEME_DIR` sur le dépôt du thème, le MCP lit le thème en cours d'écriture et n'appelle pas l'espace membre. Les connaissances rédigées à la main vivent dans `src/knowledge/` (descriptions de chaque section et bloc, compositions par métier, directions artistiques, guides) ; tout le reste vient du thème. `npm run check` signale une section ou un bloc public sans description.
|
|
117
123
|
|
|
118
124
|
Publication : `npm version <x.y.z>` (mettre à jour `src/version.js` à l'identique), `npm publish` — `prepublishOnly` vérifie l'ensemble.
|
|
119
125
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "story-theme-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Serveur MCP du Story Thème : votre assistant IA connaît le thème Shopify Story par cœur et construit des boutiques propres et professionnelles.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// =====================================================================
|
|
2
|
+
// Une page à partir de spécifications de sections
|
|
3
|
+
// =====================================================================
|
|
4
|
+
// Le même chemin pour tout ce que build_store écrit : le `plan` de l'assistant comme les moments
|
|
5
|
+
// choisis selon le métier. Les sections sont composées (compose.js), reliées au catalogue (collections,
|
|
6
|
+
// produit phare, lot), débarrassées des médias absents et des textes d'exemple, mises au rythme des
|
|
7
|
+
// palettes et à la direction artistique, puis validées.
|
|
8
|
+
|
|
9
|
+
import { composeSections } from './compose.js';
|
|
10
|
+
import { composerFiche } from './fiche.js';
|
|
11
|
+
import { collectIds } from './scaffold.js';
|
|
12
|
+
import { elaguerExemples, retirerMediasVides, sansDoublons } from './nettoyage.js';
|
|
13
|
+
import { validateFile } from '../validate/template.js';
|
|
14
|
+
|
|
15
|
+
const clone = (value) => JSON.parse(JSON.stringify(value));
|
|
16
|
+
const HERO_TYPES = ['banner', 'slideshow', 'video'];
|
|
17
|
+
// Clés internes d'une spécification, qui ne regardent pas compose.js.
|
|
18
|
+
const INTERNES = ['moment', 'role', 'catalogue', 'paliers', 'exemples', 'collection', 'collections', 'product', 'blocks', 'horaires'];
|
|
19
|
+
|
|
20
|
+
function* parcourir(noeud) {
|
|
21
|
+
const ids = [...Object.keys(noeud.blocks || {}).filter((id) => noeud.blocks[id].static), ...(noeud.block_order || [])];
|
|
22
|
+
for (const id of ids) {
|
|
23
|
+
const bloc = noeud.blocks?.[id];
|
|
24
|
+
if (!bloc) continue;
|
|
25
|
+
yield { id, bloc, parent: noeud };
|
|
26
|
+
yield* parcourir(bloc);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---- rythme et direction
|
|
31
|
+
|
|
32
|
+
// Le rythme des fonds, comme sur les démos du designer (Havn, Ma Petite Ruche : un fond sur deux
|
|
33
|
+
// change). Deux voisines sur le même fond coloré se lisent comme un seul bloc : la seconde revient
|
|
34
|
+
// au fond principal. Trois sections d'affilée sur le fond principal font un long mur sans relief,
|
|
35
|
+
// surtout sans photo : la troisième passe sur le fond doux. Les héros couverts par leur photo ne
|
|
36
|
+
// comptent pas ; un héro typographique, si. Les palettes choisies par l'assistant (explicites)
|
|
37
|
+
// restent les siennes, sauf deux voisines identiques.
|
|
38
|
+
export function fixPaletteRhythm(catalog, data, { explicites = new Set() } = {}) {
|
|
39
|
+
let precedente = null;
|
|
40
|
+
let serie = 0;
|
|
41
|
+
for (const id of data.order || []) {
|
|
42
|
+
const section = data.sections[id];
|
|
43
|
+
if (!section) continue;
|
|
44
|
+
const schema = catalog.section(section.type)?.schema?.settings || [];
|
|
45
|
+
const setting = (HERO_TYPES.includes(section.type) ? ['frame_palette', 'color_palette'] : ['color_palette', 'frame_palette']).find((key) => schema.some((item) => item.id === key));
|
|
46
|
+
const couverte = HERO_TYPES.includes(section.type) && (section.settings?.image || section.settings?.video || section.settings?.video_url || section.type === 'slideshow' || section.settings?.media_type !== 'none');
|
|
47
|
+
if (!setting || couverte) {
|
|
48
|
+
precedente = null;
|
|
49
|
+
serie = 0;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
section.settings = section.settings || {};
|
|
53
|
+
let valeur = section.settings[setting] ?? schema.find((item) => item.id === setting).default;
|
|
54
|
+
const libre = !explicites.has(id) && !/^main-/.test(section.type);
|
|
55
|
+
// Un manifeste révélé au défilement est un moment fort : sur fond sombre plutôt que fondu dans
|
|
56
|
+
// le fond principal (Sylys, Ma Petite Ruche le posent en inverse).
|
|
57
|
+
const manifeste = section.type === 'rich-text' && [...parcourir(section)].some(({ bloc }) => bloc.type === 'text-reveal');
|
|
58
|
+
if (precedente && valeur === precedente && valeur !== 'background-1' && !/^main-/.test(section.type)) valeur = manifeste && precedente !== 'inverse' ? 'inverse' : 'background-1';
|
|
59
|
+
else if (libre && valeur === 'background-1' && serie >= 2) valeur = 'background-2';
|
|
60
|
+
if (valeur !== (section.settings[setting] ?? schema.find((item) => item.id === setting).default)) section.settings[setting] = valeur;
|
|
61
|
+
serie = valeur === 'background-1' ? serie + 1 : 0;
|
|
62
|
+
precedente = valeur;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// A hero without a photo becomes a typographic one on the brand colour, text and background
|
|
67
|
+
// from the same scheme (an image hero keeps white text for the overlay, unreadable without it).
|
|
68
|
+
export function typographicHero(data) {
|
|
69
|
+
const hero = (data.order || []).map((id) => data.sections[id]).find((section) => section?.type === 'banner');
|
|
70
|
+
if (!hero || hero.settings?.image) return false;
|
|
71
|
+
hero.settings = hero.settings || {};
|
|
72
|
+
Object.assign(hero.settings, { media_type: 'none', frame_palette: 'scheme', color_palette: 'scheme', overlay: 'none', height: 'medium', height_mobile: 'medium', media_effect: 'none' });
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Coloured bands get the direction's shape at their edges; the hero gets it at the bottom.
|
|
77
|
+
export function decorate(catalog, data, direction, { garder = new Set() } = {}) {
|
|
78
|
+
if (!direction?.separator) return 0;
|
|
79
|
+
let count = 0;
|
|
80
|
+
const has = (section, id) => (catalog.section(section.type)?.schema?.settings || []).some((setting) => setting.id === id);
|
|
81
|
+
(data.order || []).forEach((id, position) => {
|
|
82
|
+
const section = data.sections[id];
|
|
83
|
+
// Les formes choisies par l'assistant (spec.separator) restent les siennes.
|
|
84
|
+
if (!section || !has(section, 'separator_bottom') || garder.has(id)) return;
|
|
85
|
+
section.settings = section.settings || {};
|
|
86
|
+
const palette = section.settings.frame_palette ?? section.settings.color_palette ?? 'background-1';
|
|
87
|
+
const hero = position === 0 && ['banner', 'slideshow'].includes(section.type);
|
|
88
|
+
if (!hero && palette === 'background-1') return;
|
|
89
|
+
if (!hero) section.settings.separator_top = direction.separator.shape;
|
|
90
|
+
section.settings.separator_bottom = direction.separator.shape;
|
|
91
|
+
if (direction.separator.animation && has(section, 'separator_animation')) section.settings.separator_animation = direction.separator.animation;
|
|
92
|
+
count += 1;
|
|
93
|
+
});
|
|
94
|
+
return count;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---- catalogue
|
|
98
|
+
|
|
99
|
+
/** Relie une section à la boutique : collection d'une liste, collections montrées, produit phare, lot. */
|
|
100
|
+
function relier(catalog, section, spec, { collections = [] }) {
|
|
101
|
+
const index = spec.catalogue?.collectionIndex ?? 0;
|
|
102
|
+
const collection = spec.collection || collections[index] || null;
|
|
103
|
+
if (section.type === 'featured-product' && (spec.product || spec.settings?.product)) section.settings.product = spec.product || spec.settings.product;
|
|
104
|
+
for (const { bloc } of parcourir(section)) {
|
|
105
|
+
bloc.settings = bloc.settings || {};
|
|
106
|
+
if (bloc.type === 'collection-list' && (bloc.settings.source ?? 'collections') === 'collections') {
|
|
107
|
+
const liste = spec.collections || collections;
|
|
108
|
+
if (liste.length) bloc.settings.collections = liste.slice(0, bloc.settings.limit || 8);
|
|
109
|
+
else bloc.settings.source = 'all';
|
|
110
|
+
}
|
|
111
|
+
if (bloc.type === 'product-list' && (bloc.settings.source ?? 'collection') === 'collection' && collection) bloc.settings.collection = collection;
|
|
112
|
+
if (bloc.type === 'bundle-builder' && collection) {
|
|
113
|
+
bloc.settings.source = 'collection';
|
|
114
|
+
bloc.settings.collection = collection;
|
|
115
|
+
}
|
|
116
|
+
// Horaires d'ouverture : ceux de la marque, sinon aucun (des horaires d'exemple seraient faux).
|
|
117
|
+
if (bloc.type === 'contact-info') {
|
|
118
|
+
for (const cle of (bloc.block_order || []).filter((item) => bloc.blocks[item]?.type === '_contact-hours')) {
|
|
119
|
+
if (spec.horaires) bloc.blocks[cle].settings = { ...bloc.blocks[cle].settings, text: String(spec.horaires).replace(/[<>]/g, ''), hours_weekdays: '', hours_saturday: '', hours_sunday: '', show_status: false };
|
|
120
|
+
else {
|
|
121
|
+
delete bloc.blocks[cle];
|
|
122
|
+
bloc.block_order = bloc.block_order.filter((item) => item !== cle);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (bloc.type === '_bundle-progress' && spec.paliers?.length) {
|
|
127
|
+
const cles = (bloc.block_order || []).filter((cle) => bloc.blocks[cle]?.type === '_bundle-tier');
|
|
128
|
+
cles.forEach((cle, rang) => {
|
|
129
|
+
const palier = spec.paliers[rang];
|
|
130
|
+
if (!palier) {
|
|
131
|
+
delete bloc.blocks[cle];
|
|
132
|
+
bloc.block_order = bloc.block_order.filter((item) => item !== cle);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
bloc.blocks[cle].settings = {
|
|
136
|
+
count: Math.max(1, Math.round(Number(palier.count) || rang + 2)),
|
|
137
|
+
discount: Math.max(0, Math.min(100, Math.round(Number(palier.discount) || 0))),
|
|
138
|
+
...(palier.label ? { label: String(palier.label).replace(/[<>]/g, '') } : {}),
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Relie au catalogue les sections fraîchement composées (compose_page comme build_store) : la
|
|
147
|
+
* collection d'une liste de produits, les collections montrées, le produit en vedette.
|
|
148
|
+
* @param {string[]} ids identifiants des sections, dans l'ordre des spécifications
|
|
149
|
+
*/
|
|
150
|
+
export function relierAuCatalogue(catalog, data, ids, specs, { collections = [] } = {}) {
|
|
151
|
+
ids.forEach((id, index) => {
|
|
152
|
+
if (data.sections[id]) relier(catalog, data.sections[id], specs[index] || {}, { collections });
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Assemble une page complète.
|
|
158
|
+
* @param {object} catalog
|
|
159
|
+
* @param {object} options
|
|
160
|
+
* @param {string} options.fichier modèle écrit (templates/index.json…)
|
|
161
|
+
* @param {object[]} options.specs sections, au format de compose_page (plus collection, product…)
|
|
162
|
+
* @param {object} [options.base] modèle de départ (la page collection du thème garde sa grille)
|
|
163
|
+
* @param {string[]} [options.fiche] blocs de la colonne d'achat : la page commence par la fiche produit
|
|
164
|
+
* @returns {{ data, notes: string[], retraits: string[], ok: boolean, erreur?: string, poses: string[] }}
|
|
165
|
+
*/
|
|
166
|
+
export function assemblerPage(catalog, { fichier, specs = [], base = null, lang = 'fr', collections = [], proofs = {}, direction = null, fiche = null, copy = {}, offres = false, inventions = [], marque = new Set(), plan = false, garderOrdre = false }) {
|
|
167
|
+
let data = base ? clone(base) : { sections: {}, order: [] };
|
|
168
|
+
const notes = [];
|
|
169
|
+
let poses = [];
|
|
170
|
+
const pageProduit = fichier.startsWith('templates/product');
|
|
171
|
+
if (fiche) {
|
|
172
|
+
const { id, section, poses: blocsPoses } = composerFiche(catalog, fiche, { copy, lang, offres, inventions, remarques: notes, taken: collectIds(data) });
|
|
173
|
+
data.sections[id] = section;
|
|
174
|
+
data.order.unshift(id);
|
|
175
|
+
poses = blocsPoses;
|
|
176
|
+
}
|
|
177
|
+
const avant = new Set(data.order);
|
|
178
|
+
const propres = specs.map((spec) => Object.fromEntries(Object.entries(spec).filter(([cle]) => !INTERNES.includes(cle))));
|
|
179
|
+
const compose = composeSections(catalog, propres, { language: lang, data, h1First: !pageProduit && !fichier.startsWith('templates/collection') });
|
|
180
|
+
data = compose.data;
|
|
181
|
+
notes.push(...compose.notes);
|
|
182
|
+
const nouvelles = data.order.filter((id) => !avant.has(id));
|
|
183
|
+
nouvelles.forEach((id, index) => relier(catalog, data.sections[id], specs[index] || {}, { collections }));
|
|
184
|
+
const separateursChoisis = new Set(nouvelles.filter((id, index) => specs[index]?.separator));
|
|
185
|
+
// Les palettes écrites dans le plan de l'assistant sont sa décision.
|
|
186
|
+
const palettesChoisies = new Set(plan ? nouvelles.filter((id, index) => specs[index]?.palette) : []);
|
|
187
|
+
|
|
188
|
+
const retraits = [...retirerMediasVides(data, { note: proofs.rating })];
|
|
189
|
+
const nettoye = elaguerExemples(catalog, fichier, data, { language: lang, marque });
|
|
190
|
+
data = nettoye.data;
|
|
191
|
+
// Un plan est la décision de l'assistant : seuls les doublons exacts partent, rien n'est déplacé.
|
|
192
|
+
retraits.push(...nettoye.retraits, ...sansDoublons(data, { plan: plan || garderOrdre }));
|
|
193
|
+
|
|
194
|
+
if (!pageProduit && !fichier.startsWith('templates/collection')) typographicHero(data);
|
|
195
|
+
fixPaletteRhythm(catalog, data, { explicites: palettesChoisies });
|
|
196
|
+
decorate(catalog, data, direction, { garder: separateursChoisis });
|
|
197
|
+
if (plan) {
|
|
198
|
+
data.order.forEach((id, index) => {
|
|
199
|
+
// Deux sections de même type sur des fonds différents (les fiches d'ingrédients de Ma Petite
|
|
200
|
+
// Ruche) se lisent bien ; sur le même fond, elles se confondent.
|
|
201
|
+
const precedente = data.sections[data.order[index - 1]];
|
|
202
|
+
const fond = (section) => section?.settings?.color_palette ?? section?.settings?.frame_palette ?? 'background-1';
|
|
203
|
+
if (index && data.sections[id]?.type === precedente?.type && fond(data.sections[id]) === fond(precedente)) notes.push(`${id} : même type de section et même fond que la précédente (${data.sections[id].type}), les deux se liront comme une seule`);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const verdict = validateFile(catalog, fichier, data);
|
|
208
|
+
return { data, notes, retraits, poses, ok: verdict.ok, erreur: verdict.ok ? null : `${verdict.errors[0].path} : ${verdict.errors[0].message}` };
|
|
209
|
+
}
|