@corraya/cms-editor

Blokowy edytor wizualny treści dla aplikacji Vue 3 — kanwa, konspekt, panel właściwości i renderer HTML.
820
Install
npm install @corraya/cms-editor
Latest Version:0.10.5
Vue:3
License:PolyForm-Noncommercial-1.0.0
Last Updated:Oct 21, 2026
Links: Bitbucket  ·  npm
Maintainer: chryssalid

@corraya/cms-editor

npm

Blokowy edytor wizualny treści dla aplikacji Vue 3: kanwa z podglądem, konspekt dokumentu, panel właściwości i renderer HTML.

Moduł nie zna hosta. Nie wykonuje żadnego zapytania sieciowego, nie zna żadnego adresu ani schematu bazy — wszystko, co zależne od aplikacji, wchodzi propsami.

Instalacja

npm install @corraya/cms-editor

Zależności równorzędne

vue oraz pakiety @tiptap/* są peerDependencies i host musi je dostarczyć. Dla TipTapa to nie jest oszczędność rozmiaru, tylko poprawność: ProseMirror trzyma stan w modułowych singletonach, a dwie jego kopie w jednym dokumencie dają błędy, których praktycznie nie da się zdiagnozować.

Peery TipTapa są oznaczone jako opcjonalne, bo potrzebuje ich wyłącznie edytor. Host sięgający tylko po /render nie ściąga ich wcale — bez tego npm doklejałby mu kilkanaście pakietów po to, żeby wygenerować HTML.

Style

import '@corraya/cms-editor/style.css';

Interfejs edytora jest napisany klasami Tailwinda, a te generuje Tailwind hosta — więc host musi skanować zbudowany pakiet:

@import 'tailwindcss';
@source "../../node_modules/@corraya/cms-editor/dist";

Bez tej linii edytor wyrenderuje się bez stylów.

Kanwa pisze tekst czernią, jak strona bez własnych stylów, a nie kolorem panelu. Host, którego front ma inny kolor treści, ustawia --cms-canvas-text — inaczej podgląd i publikacja różnią się odcieniem. Czcionkę frontu podaje --cms-canvas-font (np. 'Lato', sans-serif); host musi ją też załadować w panelu. Bez tej zmiennej kanwa pisze czcionką panelu.

Kontener, w który host wstawia wyrenderowany dokument, nie powinien mieć własnych reguł dla elementów (.content h2 { margin }, .content ul { margin } itp.) — bloki niosą swoje odstępy i rozmiary, a takie reguły nakładają się na nie i strona wygląda inaczej niż kanwa. Kontener potrzebuje tylko koloru tekstu, wyglądu linków i overflow-wrap: anywhere, żeby długi adres nie rozpychał kolumny na telefonie.

Miniatury

Wgrane obrazy idą zawsze przez miniaturownik hosta, nigdy wprost z pliku: /thumb/cms_<proporcja>_<szerokość>/<ścieżka>. Host musi znać te presety:

Preset Szerokości Uwagi
cms_orig_*, cms_1x1_*, cms_16x9_* 400, 800, 1200, 1600 proporcja pliku / kadr
cms_9x16_* 225, 450, 675, 900 kadr
cms_4x5_* 320, 640, 960, 1280 kadr
cms_natural_1600 do 1600 bez powiększania — obraz w „oryginalnym rozmiarze"

Szerokości to deskryptory w w srcset, więc muszą się zgadzać co do piksela. cms_natural_1600 nie może powiększać: mniejszy plik ma wrócić w swoim rozmiarze, bo właśnie ten rozmiar pokazuje strona.

Użycie

<script setup>
import { CmsEditor } from '@corraya/cms-editor';
import '@corraya/cms-editor/style.css';
</script>

<template>
    <CmsEditor
        :initial-blocks="blocks"
        :page-meta="meta"
        :on-save="save"
        :on-image-upload="upload"
        @close="close"
    />
</template>

Warstwa adaptera

Props on* to cała powierzchnia styku z aplikacją:

Prop Odpowiedzialność hosta
onSave(payload) zapis bloków i wygenerowanego HTML-a; może zwrócić { pageMeta } — pola po zapisie tak, jak przyjął je serwer (np. adres wyliczony z tytułu), a edytor wpisze je do panelu
onImageUpload(file) przyjęcie pliku, zwrot { path }
onImagePick() własna biblioteka mediów, zwrot { src, alt?, caption? }
onLoadVersions() / onLoadVersion(id) historia wersji; lista jest pobierana po KAŻDYM udanym zapisie, także autozapisie, więc ma być tania
previewUrl adres strony na froncie (string albo funkcja pól strony z ostatniego zapisu ręcznego) — ikona „Otwórz na stronie"
documentTitle nazwa dokumentu w pasku zamiast „Edytor wizualny"
legacyHtml treść ze starego edytora; póki dokument jest pusty, edytor proponuje przeniesienie jej na bloki (konwersja bez AI, jako kopia robocza do „Zapisz")
slot topbar element hosta w pasku, np. przełącznik wersji językowych

Autozapis a zapis ręczny

payload.type rozróżnia dwa rodzaje zapisu i edytor zakłada dla nich różne znaczenie:

  • autosave — kopia robocza. Host dopisuje ją do historii wersji, ale nie zmienia treści widocznej publicznie. Inaczej niedokończone zmiany lądowałyby na stronie w trakcie edycji.
  • manual — „Zapisz": treść trafia na stronę.

Przy otwieraniu host sprawdza, czy najnowsza wersja to autozapis o treści innej niż publiczna. Jeśli tak, podaje jej bloki w initialBlocks razem z initialUnpublished: true, a edytor od startu pokazuje „Zmiany tylko w autozapisie". Zamknięcie niczego nie wyrzuca. Edytor pyta o utratę tylko wtedy, gdy coś naprawdę przepadnie, czyli przy zmianach pól strony (pageMeta nie jedzie z autozapisem).

Kopiowanie i wklejanie

Przez systemowy schowek, więc działa między dokumentami i między środowiskami — np. artykuł przygotowany na dev wklejony na produkcji.

  • Element: „Kopiuj" na pasku bloku albo Ctrl/Cmd+C przy zaznaczonym bloku (poza edycją tekstu). Wklejenie: „Wklej ze schowka" w menu dodawania — wszędzie tam, gdzie da się dodać blok — albo Ctrl/Cmd+V (za zaznaczonym blokiem).
  • Cały dokument: przyciski w pasku. Kopia niesie też domyślne odstępy i pola z bocznego panelu; przy wklejeniu edytor pyta, czy je przejąć. Wkleja się tylko do pól, które ma bieżący panel.
  • Obrazy jadą w kopii jako data URL (pliki z jednego środowiska nie istnieją w drugim) i przy wklejeniu są wgrywane ponownie przez onImageUpload. W tym samym środowisku nic nie jest wgrywane drugi raz.
  • Wklejana treść przechodzi normalizeDocument jak import: nowe identyfikatory, odrzucenie nieznanych bloków i właściwości.

Rozszerzanie

plugins przyjmuje obiekty EditorPlugin wnoszące własne bloki (blocks) i typy pól sidebara (fieldTypes). sidebarConfig opisuje zakładki i pola deklaratywnie — tędy wchodzą pola swoiste dla typu treści, których biblioteka nie może znać: artykuł ma inne niż strona swobodna.

Wbudowane typy pól: text, textarea, number, datetime, select (z options), checkbox, group, slot oraz image — wgranie pliku przez onImageUpload z podglądem; w polu zostaje ścieżka. Podgląd ma proporcje 16:9, a aspect (np. '4 / 3') ustawia te, w których host faktycznie pokazuje obraz — autor widzi wtedy kadr, zanim zobaczy go na stronie.

Renderowanie poza edytorem

Front publiczny nie potrzebuje edytora, tylko zamiany dokumentu na HTML — i ma na to osobne wejście, z którego nie prowadzi żadna ścieżka do Vue ani TipTapa:

import { renderDocument } from '@corraya/cms-editor/render';

const html = renderDocument(blocks, { pageMeta, configDefaults, documentDefaults });
Wejście Rozmiar (gzip) Wciąga Vue?
@corraya/cms-editor ~35 kB + Vue i TipTap hosta tak
@corraya/cms-editor/render ~8,7 kB nie

Wynik zawiera własny <style> z regułami wyliczonymi dla tych bloków, więc nie trzeba dołączać arkusza z pakietu. Znaczniki niosą natomiast klasy Tailwinda (text-2xl, leading-relaxed, rounded-md…), które musi wygenerować host — u siebie albo @source na zapisanym HTML-u.

Nieznany typ bloku nie wywraca renderowania: zostaje po nim komentarz HTML. Dokument zapisany nowszą wersją edytora wyświetli się w tej części, którą host rozumie, zamiast zniknąć w całości.

Bloki własne

Blok wniesiony przez hosta ma dwie warstwy w osobnych plikach: definition.ts (metadane i renderHtml, bez Vue) oraz ui.ts (te same dane plus komponenty edycyjne). Renderer dostaje pierwszą przez customBlocks, edytor drugą przez plugins. Zlanie ich w jeden plik cofa cały podział — import renderera zacznie wtedy ściągać Vue.

Import treści przez model językowy

Biblioteka nie wywołuje żadnego modelu — dostarcza to, czego host do tego potrzebuje, i przyjmuje wynik z powrotem.

import { buildAiSchema, coreRenderBlocks, normalizeDocument } from '@corraya/cms-editor/render';

buildAiSchema() składa opis struktury dokumentu z definicji bloków: typy, właściwości, reguły zagnieżdżania i wartości dziedziczone. Build zrzuca go też do dist/ai-schema.json, dostępnego jako @corraya/cms-editor/ai-schema.json — bo host składający prompt często nie jest napisany w JavaScripcie.

Opisy dla modelu żyją przy definicjach bloków (BlockRenderDef.ai), nie w osobnym promptcie. Prompt pisany ręcznie rozjeżdża się przy pierwszym nowym bloku i nikt tego nie zauważa, bo model dalej zwraca coś sensownego, tylko gorszego.

Okno importu jest w bibliotece i włącza się samo, gdy host poda prop onImportContent. Bez niego znika z interfejsu — biblioteka nigdy nie woła modelu sama. Dwa punkty wejścia: przycisk w pasku górnym zastępuje cały dokument, pozycja w menu dodawania bloku wstawia serię bloków w wybranym miejscu.

normalizeDocument() naprawia odpowiedź modelu: wycina nieznane typy bloków i właściwości, pilnuje reguł zagnieżdżania, nadaje identyfikatory. Zwraca to, co udało się uratować, plus listę ostrzeżeń — import wywracający się na jednym bloku z trzydziestu jest bezużyteczny. Host musi użyć tej samej funkcji, inaczej użytkownik zobaczy podgląd inny niż to, co zostanie zapisane.

Sanityzacja HTML-a w treści bloków zostaje po stronie hosta — biblioteka nie zakłada, jakiej używasz.

Zgodność wsteczna

Przyjęta zasada: drobne zmiany formatu dokumentu pozostają zgodne, a zmiana głównej wersji zgodności nie gwarantuje — edytor ma wtedy odmówić otwarcia dokumentu z komunikatem, zamiast próbować go migrować.

Nie jest to jeszcze zaimplementowane: dokument nie niesie dziś numeru wersji formatu. Do dodania przed pierwszym użyciem przez drugiego hosta.

Rozwój

npm install
npm run typecheck
npm run build

Wydanie

npm version patch      # albo minor / major
npm publish            # scope jest publiczny, więc bez --access
git push --follow-tags

dist/ jest wersjonowane w repozytorium, więc commit po każdej zmianie w src/ musi zawierać świeży build.

Tryb ciemny

Interfejs edytora przełącza się razem z hostem, warunkiem prefers-color-scheme: dark z wyjątkiem :root.light. Działa to przez przedefiniowanie palety Tailwinda w obrębie edytora — .bg-zinc-900 kompiluje się do var(--color-zinc-900), więc podmiana zmiennej przestawia wszystkie klasy naraz, bez wariantów dark: w komponentach.

Kanwa też ciemnieje, razem z paskami przewijania (color-scheme: dark). Tekst treści ma wtedy --cms-canvas-text-dark (domyślnie #fafafa) — host ustawia go na kolor tekstu swojego frontu w trybie ciemnym. Kolory, które blok wnosi do wyniku (obramowania i nagłówek tabeli, znak cytatu), są liczone od koloru tekstu, więc działają w obu trybach także na stronie hosta.

Host, który przełącza motyw inaczej (klasą, atrybutem), nadpisuje te same zmienne --color-* na .cms-editor-overlay i .cms-editor-modal.

Licencja

PolyForm Noncommercial 1.0.0 — wolno używać do dowolnych celów niekomercyjnych. Zastosowanie komercyjne wymaga odrębnych ustaleń z CORRAYA.

Related Packages

@revenexx/editor

revenexx hard fork of blökkli — interactive page building experience for Nuxt. I...

3,495
@tessera-editor/vue

Tessera Vue 3 binding with the full default UI

235 0
@bentricode/editor

Um editor de texto estilo Notion baseado em blocos para Vue 3.

2,098 0
guodu-tiptap

A WYSIWYG rich-text editor using tiptap

842 18
tiptap-vuetify

Vuetify WYSIWYG editor based on Tiptap.

175,989 813

Version History

Version Released Vue License
0.10.5 ^3.5.0 PolyForm-Noncommercial-1.0.0
0.10.4 ^3.5.0 PolyForm-Noncommercial-1.0.0
0.10.3 ^3.5.0 PolyForm-Noncommercial-1.0.0
0.10.2 ^3.5.0 PolyForm-Noncommercial-1.0.0
0.10.1 ^3.5.0 PolyForm-Noncommercial-1.0.0