Le studio est un espace navigateur, pas un cloud, une collaboration ou une sauvegarde durable. Son branchement au package réel relève de l’intégration des outils ; les API de persistance ci-dessous sont celles du store livré.
Importer et exporter le modèle, pas une session complète
toJSON(): string sérialise le document versionné en JSON compact. Nœuds, ports, liens, groupes, dimensions, configuration et viewport sont conservés. La sélection, l’historique, les abonnements et l’état d’un éventuel moteur externe ne font pas partie de cet export.
loadJSON(json) parse et valide avant de remplacer tout le document. replaceDocument(document) fait la même validation à partir d’un objet typé. Le succès est une action annulable et vide la sélection. Un import invalide n’est pas appliqué partiellement : document, sélection, révision et historique restent inchangés. Affichez l’erreur à l’utilisateur au lieu de supprimer son travail.
import { Editor, createDemoDocument, parseDocument } from '@newvexa/flowora';
const editor = new Editor(createDemoDocument());
try {
const json = editor.toJSON(); // JSON compact, schemaVersion: 1.
const validated = parseDocument(json); // Validation sans remplacer le store.
console.log(validated.name);
editor.loadJSON(json); // Remplacement complet, atomique et annulable.
const before = editor.getSnapshot();
try {
editor.loadJSON('{"schemaVersion":99}');
} catch (error: unknown) {
console.error(error instanceof Error ? error.message : 'Import refusé');
console.log(editor.getSnapshot() === before); // true : état inchangé.
}
} finally {
editor.dispose();
} Pour examiner un fichier sans modifier l’éditeur, la racine exporte parseDocument(json, options?) et validateDocument(value, options?). Leur option allowCycles doit correspondre à celle du store qui le chargera. La validation vérifie version, références, ports, cardinalité, hiérarchie, données JSON sûres, nombres finis et taille. Elle ne vérifie pas la logique de votre backend.
Des documents nommés, sans synchronisation
Le studio ajoute au même éditeur une bibliothèque locale : création vide ou depuis un exemple fictif, nommage, liste, ouverture, renommage, sauvegarde, import/export et suppression confirmée. La suppression d’un document local ne supprime aucun fichier JSON déjà exporté sur votre appareil. Le playground sert à explorer les exemples et les commandes ; ce n’est pas un service de stockage distant.
Le stockage du navigateur n’est pas une sauvegarde
Les données de localStorage dépendent de l’origine du site, du navigateur et du profil. Un autre domaine, protocole, port, profil ou appareil ne retrouve pas automatiquement les mêmes documents. Effacer les données du site, perdre le profil ou utiliser une navigation privée peut les supprimer ou empêcher leur conservation. Une Preview et une future origine publique ont donc des stockages distincts.
Le studio sur https://flowora.dev ne peut pas lire la bibliothèque enregistrée sur l’ancien domaine Manus ou dans la Preview. Pour déplacer vos documents, exportez volontairement chaque JSON depuis l’ancienne origine, puis importez et enregistrez cette copie dans le nouveau studio. Aucun transfert automatique, aucun accès à votre ancien stockage et aucune synchronisation cloud ne sont effectués.
Le quota est propre au navigateur et à son contexte : la borne de 32 MiB du modèle n’est pas une promesse d’espace disponible dans localStorage. Ses écritures sont synchrones, peuvent bloquer l’interface et peuvent échouer. Regroupez-les ; n’écrivez pas tout le graphe à chaque mouvement de souris.
Échec de quota : garder la mémoire et permettre l’export
Un refus de stockage ne rend pas l’éditeur inutilisable. Conservez l’instance et le document en mémoire, affichez « non enregistré », puis proposez immédiatement l’export JSON. Ne réinitialisez pas le graphe, ne supprimez pas l’ancien export et ne prétendez pas que l’écriture a réussi. Fermer ou recharger l’onglet avant cet export peut perdre les changements : la mémoire n’est pas persistante.
// local-document.ts · à appeler dans le navigateur, pas pendant le SSR.
import { MAX_DOCUMENT_BYTES } from '@newvexa/flowora';
import type { Editor } from '@newvexa/flowora';
// Namespace d'exemple pour UN document, indépendant du stockage du studio.
const storageKey = 'flowora:docs-example:v1:document';
export interface PersistenceResult {
readonly saved: boolean;
readonly message: string;
}
export function saveLocal(editor: Editor): PersistenceResult {
try {
window.localStorage.setItem(storageKey, editor.toJSON());
return { saved: true, message: 'Enregistré dans ce navigateur uniquement.' };
} catch (error: unknown) {
const detail = error instanceof Error ? error.message : 'Stockage indisponible';
// Le document reste dans editor : ne le réinitialisez pas.
return {
saved: false,
message: 'Non enregistré. Exportez avant de fermer cet onglet. ' + detail,
};
}
}
export function restoreLocal(editor: Editor): string {
try {
const json = window.localStorage.getItem(storageKey);
if (json === null) return 'Aucun document enregistré.';
editor.loadJSON(json);
return 'Document local chargé.';
} catch (error: unknown) {
// Ni effacement de la valeur stockée, ni remplacement par un document vide.
return error instanceof Error ? error.message : 'Lecture ou import refusé';
}
}
export async function importFile(editor: Editor, file: File): Promise<void> {
if (file.size > MAX_DOCUMENT_BYTES) {
throw new Error('Fichier trop volumineux : maximum 32 MiB.');
}
const before = editor.getSnapshot().document;
const json = await file.text();
if (editor.getSnapshot().document !== before) {
throw new Error('Le document a changé pendant la lecture. Relancez l’import.');
}
editor.loadJSON(json); // Toute erreur laisse le document courant intact.
}
export function downloadJSON(editor: Editor): void {
const blob = new Blob([editor.toJSON()], { type: 'application/json;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
try {
link.href = url;
link.download = 'flowora-document.json';
document.body.append(link);
link.click();
} finally {
link.remove();
// Laisse au navigateur le temps de prendre en charge le téléchargement.
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
}
} Ce module est un exemple complet pour un seul document, pas la bibliothèque multi-document du studio. Appelez saveLocal(editor) depuis votre action de sauvegarde et affichez son résultat ; gardez downloadJSON(editor) disponible même si saved est faux. Appelez restoreLocal(editor) seulement après confirmation d’un remplacement éventuel. Un échec de lecture ou de validation conserve la valeur stockée pour récupération manuelle.
Reliez importFile(editor, file) à votre champ de fichier, après confirmation si le document courant a des changements non exportés, puis interceptez sa Promise pour afficher l’erreur. Le contrôle de taille évite de lire un fichier manifestement trop gros ; loadJSON() reste l’autorité de validation. Ne lancez pas plusieurs imports concurrents.
Origine locale, pas compte ou cloud
Les fonctions de documents du studio sont locales, sans backend, compte ou synchronisation. Elles ne transmettent pas le contenu des graphes au réseau. Les fichiers statiques et bundles du site sont chargés normalement : ce chargement n’est pas un envoi de vos documents. Si votre application hôte ajoute un backend, elle doit définir clairement ses règles de transfert et de conservation.
Ne placez jamais de mot de passe, token, clé API ou données confidentielles dans le graphe. Le stockage local n’est pas un coffre-fort ; un script autorisé sur la même origine ou une compromission de l’application peut accéder aux données. Gardez les exports réguliers dans un emplacement que vous maîtrisez.
Lire les limites et règles de sécurité