@alggtta/editor
React/TypeScript Notion-style editor built on Tiptap 3, Tailwind CSS v4, and DaisyUI 5.
@alggtta/editor (core-first since v3) keeps the default package focused on the core editor. Heavy features such as database blocks, Mermaid, AI, and export helpers are available through opt-in subpath imports.
Install
npm install @alggtta/editor --save-exact
Install peer dependencies in your app:
npm install react react-dom @tiptap/[email protected] @tiptap/[email protected] @tiptap/[email protected] @tiptap/[email protected] @tiptap/[email protected] [email protected] [email protected] --save-exact
Keep all Tiptap singleton peers on exactly 3.31.3. Tiptap packages require
matching patch versions. This patch fixes
GHSA-cp6q-959q-f8rh;
3.28.x–3.29.x also have a known React NodeView caret regression.
@tiptap/y-tiptap uses its
independent 3.0.10 release line. [email protected] and [email protected] are also
required singleton peers because the main entry eagerly links the collaboration
extension graph.
Yarn Classic 1.x consumers must use the same exact peers with --ignore-optional:
yarn add --ignore-optional --exact @alggtta/editor react react-dom @tiptap/[email protected] @tiptap/[email protected] @tiptap/[email protected] @tiptap/[email protected] @tiptap/[email protected] [email protected] [email protected]
That flag skips every optional dependency in this Yarn install. Its important
effect in this graph is preventing the redundant newer menu copies resolved
from @tiptap/react's optional ranges; the two exact root menu packages above
keep menu functionality available. Explicitly install any other optional
package your app needs. This workaround is specific to Yarn Classic 1.x, not a
general Yarn Modern requirement.
TypeScript
Minimum TypeScript: >=5.3
typescript is declared as an optional peer dependency — JavaScript consumers need
nothing. TypeScript consumers need 5.3 or newer because the CommonJS declarations served
to exports["*"].require.types (dist/**/*.d.cts) annotate their type-only edges to
ESM-only packages (yjs, @tanstack/react-table) with
with { "resolution-mode": "import" }. TypeScript only parses that attribute from 5.3
onward; on 5.2 and older it is a syntax error (TS1005) that skipLibCheck: true
does not suppress, so the CJS type surface fails to load entirely.
Measured: 5.2.2 → TS1005 (tsc exit 2, with or without skipLibCheck); 5.3.2 → clean.
The ESM declarations (dist/**/*.d.ts) carry no such attribute and work on older
compilers — the floor applies only to the require condition.
Production usage requires an Alggtta token. Localhost development still works with a token-shaped value such as tok_dev.
Basic Usage
import { AlggttaEditor } from '@alggtta/editor'
import '@alggtta/editor/styles.css'
export function EditorPage() {
return (
<AlggttaEditor
token={process.env.NEXT_PUBLIC_ALGGTTA_TOKEN ?? ''}
content="<h1>Hello</h1><p>Type / for commands.</p>"
onUpdate={({ editor, json, html, text }) => {
console.log({ json, html, text })
}}
/>
)
}
Next.js App Router
Use the editor from a client component.
'use client'
import { AlggttaEditor } from '@alggtta/editor'
import '@alggtta/editor/styles.css'
export default function EditorClient() {
return (
<AlggttaEditor
token={process.env.NEXT_PUBLIC_ALGGTTA_TOKEN ?? ''}
variant="document"
onUpdate={({ json }) => {
void fetch('/api/document', {
method: 'POST',
body: JSON.stringify(json),
})
}}
/>
)
}
Vite / CRA / React
import { AlggttaEditor } from '@alggtta/editor'
import '@alggtta/editor/styles.css'
export function App() {
return (
<AlggttaEditor
token={import.meta.env.VITE_ALGGTTA_TOKEN}
placeholder="Write something..."
/>
)
}
Visual Variants
Use variant to fit the editor into different layouts:
<AlggttaEditor token={token} variant="document" />
<AlggttaEditor token={token} variant="embedded" />
<AlggttaEditor token={token} variant="bare" />
document: default document canvas with comfortable padding.embedded: compact shell for dashboards and admin panels.bare: minimal wrapper when your app owns the layout.
License UI
token is required. verifyEndpoint defaults to https://cdn.alggtta.com.
<AlggttaEditor
token={token}
licenseFallback={({ error }) => (
<div role="alert">Editor license check failed: {error}</div>
)}
/>
Subpath Imports
import { useAlggttaEditor, useAutosave } from '@alggtta/editor/headless'
import { definePlugin, resolvePlugins, PluginSlot } from '@alggtta/editor/plugin'
import { createSchedulePlugin, ScheduleEditor } from '@alggtta/editor/schedule'
import { createGuideFlowPlugin } from '@alggtta/editor/guideflow'
import { createMathPlugin } from '@alggtta/editor/math'
import { createDatabaseExtensions, databaseSlashCommands } from '@alggtta/editor/database'
import { createMermaidExtensions, mermaidSlashCommands } from '@alggtta/editor/mermaid'
import { createAIExtensions, getDocumentContext } from '@alggtta/editor/ai'
import { markdownToHtml, htmlToMarkdown } from '@alggtta/editor/export'
import { LicensedGate, useLicenseVerification } from '@alggtta/editor/license'
import { useComments, CommentSidebar } from '@alggtta/editor/comments'
import { createCollaborationExtensions, usePresence } from '@alggtta/editor/collab'
import { UniversalEditor } from '@alggtta/editor/legacy'
The main entry includes the core blocks: paragraph, headings 1-3, lists, task list, quote, divider, link, highlight, image, table, code block, slash menu, bubble menu, and drag handle. Pass preset="full" to also mount the advanced blocks (callout, toggle, columns, embed, file attachment, mention, wiki link, comments, find/replace, synced block) and their slash commands. Math (KaTeX) is excluded from full to keep the bundle lean — it's a heavy optional peer; add createMathPlugin() from @alggtta/editor/math via extensionConfig.plugins (or MathExtension/InlineMathExtension from @alggtta/editor/legacy via extensionConfig.customExtensions) if you need it.
For math on Node 20, use npm install --save-exact [email protected]. KaTeX 0.18.5 adds a CLI dependency requiring Node 22.12+, so an engine-strict Node 20 install of that version fails. The editor retains the compatible ^0.18.4 peer range; workspace/demo testing uses 0.18.5 on Node 22.
The main entry also exports the framework-agnostic mount(el, props) factory, the <alggtta-editor> Web Component (defineAlggttaEditorElement()), and the getWikiLinks(doc) link-enumeration API. To keep the core lean, the heavier page-chrome components (VersionHistory, PageMeta, TableOfContents) live in @alggtta/editor/legacy, and the version/migration utilities (createPageVersionFromAITransaction, MigrationRegistry) in @alggtta/editor/headless.
Embed anywhere (no React app)
The mount(el, props) factory and <alggtta-editor> Web Component work from any
stack. The bundle served from cdn.alggtta.com keeps React external, so load the
react / react-dom UMD globals first, then the editor. Every CDN asset is
token-gated, so pass ?token= on both the script and the stylesheet — a
<link> / <script> tag can't send an Authorization header (no token → 401;
invalid, expired, or wrong-domain token → 403):
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<link rel="stylesheet" href="https://cdn.alggtta.com/editor.css?token=YOUR_TOKEN" />
<script src="https://cdn.alggtta.com/editor.umd.js?token=YOUR_TOKEN"></script>
<alggtta-editor token="YOUR_TOKEN" preset="full"></alggtta-editor>
<!-- or imperatively: window.AlggttaEditor.mount('#el', { token, preset: 'full' }) -->
From npm you can call
mount/defineAlggttaEditorElementdirectly (React resolved as a peer) — no UMD globals needed.
Building a wiki? The editor ships the primitives (
[[/@triggers,getWikiLinks, autosave). Durable storage — backlinks, full-text search, and versions — is handled by the private companion package@alggtta/wiki-store, provided to licensees on request (contact https://alggtta.com).
Optional entries may require their matching peer dependencies, for example @tanstack/react-table@^9.1.2 for database blocks and mermaid for Mermaid diagrams. The database subpath uses TanStack Table v9's feature-explicit API and is not compatible with v8. Export/import UI loads html2pdf.js and mammoth only when the PDF or DOCX actions are used.
Optional Feature Composition
import { AlggttaEditor } from '@alggtta/editor'
import { createDatabaseExtensions, databaseSlashCommands } from '@alggtta/editor/database'
<AlggttaEditor
token={token}
extensionConfig={{
customExtensions: createDatabaseExtensions(),
customSlashCommands: databaseSlashCommands,
}}
/>
Export UI is also opt-in. Import the dropdown from /export and inject it into the editor footer:
import { AlggttaEditor } from '@alggtta/editor'
import { ExportDropdown, exportSlashCommands } from '@alggtta/editor/export'
<AlggttaEditor
token={token}
extensionConfig={{
customSlashCommands: exportSlashCommands,
}}
renderFooterControls={({ editor, allowedFeatures }) =>
allowedFeatures.includes('*') || allowedFeatures.includes('export')
? <ExportDropdown editor={editor} />
: null
}
/>
Only expose optional commands for features you actually register or render.
Plugin Development
Use the lightweight /plugin entry to bundle custom Tiptap extensions and
slash commands into a mount-time plugin. Plugin command ids and extension names
must be unique; command ids also cannot collide with ids declared by the host's
customSlashCommands.
import { AlggttaEditor } from '@alggtta/editor'
import { ClockIcon } from '@heroicons/react/24/outline'
import { Extension, definePlugin } from '@alggtta/editor/plugin'
import '@alggtta/editor/styles.css'
const timestampPlugin = definePlugin({
id: 'com.example.timestamp',
version: '1.0.0',
extensions: [Extension.create({ name: 'exampleTimestampBehavior' })],
slashCommands: [
{
id: 'com.example.timestamp.insert',
title: 'Insert timestamp',
description: 'Insert the current time',
icon: ClockIcon,
command: (editor) => {
editor.chain().focus().insertContent(new Date().toISOString()).run()
},
},
],
})
export function PluginEditor() {
return (
<AlggttaEditor
token={import.meta.env.VITE_ALGGTTA_TOKEN}
extensionConfig={{ plugins: [timestampPlugin] }}
/>
)
}
The plugin set is fixed when the editor is created; remount to change the
schema. External plugin packages must keep React and all Tiptap/ProseMirror
packages as peers/externals, must not import @tiptap/starter-kit at runtime,
and must publish their CSS separately. Install trusted plugins only: they run
with the host page's full JavaScript permissions. Declarative Web Component
attributes and the hosted CDN/UMD build do not load arbitrary plugin packages.
See the public Plugin Development guide for a minimal plugin definition and registration example, browser-side static rendering, document compatibility, packaging, and security guidance.
Embed Frame Security
With preset="full", the embed block shows YouTube, Figma, CodePen and X/Twitter content
in sandboxed iframes. Frame URLs are rebuilt from recognised provider links on fixed frame
hosts, but that covers only the URL a frame starts on, and YouTube and Figma frames get
allow-same-origin next to allow-scripts. Before you let embeds load, the host
application must:
- serve the editor page with
Cross-Origin-Opener-Policy: same-origin; - send
Content-Security-Policy: frame-ancestors 'none'(orX-Frame-Options: DENY) on every response that can carry user content; - never serve user-supplied HTML or SVG inline from the editor page's origin;
- list the frame hosts it wants in CSP
frame-src(https://www.youtube.com,https://www.figma.com,https://codepen.io,https://twitframe.com);default-src 'self'alone blocks every embed.
YouTube frames carry referrerpolicy="strict-origin-when-cross-origin", because YouTube
refuses to play without a Referer; this overrides a page Referrer-Policy: no-referrer
for those frames, so www.youtube.com learns the page origin (not its path or query).
Because of allow-same-origin, the frames' allow list (accelerometer, autoplay,
clipboard-write, encrypted-media, gyroscope, picture-in-picture) now takes effect for
YouTube and Figma; cap it with your own Permissions-Policy header if needed (a feature
whose allowlist omits a provider's origin, such as gyroscope=(), is not delegated to it).
A framed document can navigate itself, or open a popup that inherits its sandbox, to any
URL; the first three requirements keep such a page on your origin from reaching the editor
page.
Tailwind and DaisyUI
The package ships compiled CSS at @alggtta/editor/styles.css. DaisyUI is internally prefixed with ue- to reduce conflicts with consuming apps. You can use the editor in apps with or without Tailwind/DaisyUI.
Migration from v2
onUpdate
// v2
<UniversalEditor onUpdate={(editor) => save(editor.getJSON())} />
// v3
<AlggttaEditor onUpdate={({ json }) => save(json)} />
UniversalEditor
AlggttaEditor is the recommended component (since v3). The previous UniversalEditor remains available from the legacy entry during migration:
import { UniversalEditor } from '@alggtta/editor/legacy'
Optional Features
Database, Mermaid, AI, export helpers, GuideFlow blocks, and schedule blocks are no longer part of the default core entry. Import the matching subpath and pass its extensions/commands explicitly.
Optional citation document schema
Import createCitationPlugin from @alggtta/editor/citations and add it to
extensionConfig.plugins before loading documents with citation nodes. It is
not installed by core or full presets. insertCitation({ references, displayText, styleId?, locale? }), updateCitation(instanceId, content) and
deleteCitation(instanceId) use normal editor transactions and undo/redo.
Each ordered reference carries libraryId, itemId and optional locator,
label, prefix, suffix, suppressAuthor. A node-local schemaVersion: 1
and generated instanceId survive JSON/HTML reopen; clipboard paste creates new
occurrence IDs. Raw duplicate-ID imports/transactions are rejected.
parseCitationData validates imported payloads. Validate stored content before
initial mounting and use setContent(json, { errorOnInvalidContent: true }) for
JSON replacement so an invalid import cannot clear an existing document.
Citation node commands store literal display text; they do not fetch a library or
format citations automatically. The same optional /citations entry exports
renderCslDocument for offline CSL citation and bibliography rendering from
host-supplied styles, locales and items. It does not connect Word/Docs. Invalid
citation HTML falls back to visible plain text.
Hosts must preserve the original document and block downgrade saves when the
citation plugin or its node schema is unsupported. Complete that compatibility
check before consuming a published exact editor version in Atlas. Document schema
v3 includes citations with node-local payload v1; a registered v2-to-v3 migration
preserves existing content. The root editor shows a read-only text preview and
blocks autosave, host updates and localStorage writes when citation JSON cannot
be safely loaded. Standalone Tiptap hosts need equivalent guards. Backend must
advertise the paired document schema; the existing Atlas npm pin is unchanged.
License
Proprietary — Copyright © 2026 Alggtta. All rights reserved. Use is governed by the Alggtta Commercial License Agreement (EULA) and a valid, domain-bound license token issued by Alggtta. See LICENSE.
Versions up to and including 5.1.1 were published under the MIT License and those already-distributed copies remain MIT; this proprietary license applies to 6.0.0 and later.
Independent feature plugins
Install only the features you need through extensionConfig.plugins:
createSchedulePlugin from @alggtta/editor/schedule, createGuideFlowPlugin from
@alggtta/editor/guideflow, createMathPlugin from @alggtta/editor/math, and the
createDatabasePlugin, createAIPlugin, createMermaidPlugin, createCommentsPlugin
factories in their existing subpaths. Use the core preset when composing all of them.
Schedule retains the existing guideflow license entitlement. createGuideFlowPlugin()
installs the map, hotel, contact, divider and staff blocks, so together with
createSchedulePlugin() it registers exactly the nodes of the legacy guideFlowExtensions
bundle: moving from customExtensions: guideFlowExtensions needs no schema or storage
migration. Install both when opening existing GuideFlow documents. Legacy exports are unchanged.
Plugins can supply React ui.toolbar, ui.sidebar, ui.footer and ui.overlay
components, mounted and licensed with their extensions. Each contribution renders inside
its own error boundary: a component that throws, or a React.lazy chunk that fails to
load, is hidden and logged without unmounting the editor. Overlays render inside the
themed editor root, and without UI contributions AlggttaEditor adds no slot wrapper or
layout element around the editor surface and body.
Headless hosts can use PluginSlot and useAlggttaEditor().plugins. See
Plugin Development for migration and UI examples.
Offline CSL core (optional /citations entry)
mapResearchItemToCsl(id, metadata) returns { item, source, losses }; the original own JSON metadata remains available when a field or creator role has no faithful CSL representation. The offline compatibility catalog covers 37 research item types.
renderCslDocument({ style, locales, locale, items, citations, format }) creates a fresh document-scoped citeproc engine and returns every contextual citation plus bibliography entries. Supply independent CSL XML and offline locale XML (requested locale and en-US); there is no network lookup or fallback formatter. Citation clusters have stable id, items, and explicit positive ordered noteIndex for note styles; item options include locator, label, prefix/suffix and suppressAuthor. format is text (default) or restricted safe html.
The core is capped at 200 items/clusters, 100 references per cluster, 256000 characters per XML file, bounded XML nesting/macros and bounded own JSON input. Invalid/unsupported inputs raise CslError with a machine-readable code. XML checking enforces structural/security constraints and is not full CSL Relax NG schema validation. Dependent styles must first be resolved by the host to their independent parent. The synchronous processor has structural work bounds; hosts needing a hard wall-clock deadline should run it in their own worker.
This module does not change persisted citation nodes, embed fallback snapshots, install styles, or connect library APIs. Those are separate integration tasks. Consumers must preserve third-party notices and expose CSL_COMPONENT_NOTICE attribution in their citation UI. Style/locale fixture authors and provenance remain in the source fixtures.
refreshEditorCitations(editor, { style, locales, locale, items, noteIndices }) resolves
CSL output for every existing occurrence in document order and replaces its display
text in one document transaction. Supply authorized, current metadata as
{ libraryId, itemId, metadata } rows. The refresh retains occurrence identities,
reference order, locators, prefixes, suffixes and author suppression. Missing
references, invalid metadata/style or oversized display text abort the whole
refresh. Actual footnote numbers are required in noteIndices for note styles.
The refresh has its own undo event, separate from adjacent typing. It returns
bibliography entries and mapping losses for host review; it neither inserts a
bibliography node nor persists source metadata. No persisted schema changes or
Atlas sibling-package links are introduced.
Optional research source references
Import createResearchNotesPlugin from @alggtta/editor/research-notes and add it to extensionConfig.plugins before loading researchReference nodes. This optional entry stores note or annotation source identities separately from citations, with an independent quote snapshot, visible label and optional human location. It performs no lookup, navigation, rendering of source URLs, or automatic refresh from later source edits. researchReferenceDocumentId projects only a note's explicit existing Document identity for the host's wiki flow; it never guesses a document from an item or annotation.
Insert/update/delete commands retain occurrence identity on updates and use normal undo/redo. Clipboard paste creates fresh occurrence identities. Strict node-local v1 parsing rejects unsupported fields/versions; HTML renders the label as escaped text. Hosts resolve library/source identities through their approved scoped APIs. The quote is a copy, not a live annotation editor. Visible label extraction agrees with the Backend document text renderer.
Global document schema v4 includes this optional vocabulary and preserves the reviewed v3 snapshot. The v3-to-v4 migration changes only the root version stamp. Unsupported, malformed or duplicate reference documents open as display-only previews, with host updates, autosave and localStorage writes blocked; the original JSON must be retained. Commands reject read-only/destroyed editors. Coordinate the Backend v4 contract and exact published npm consumer before enabling persistence. Notes APIs, readers, live annotation navigation and Atlas adoption are separate integrations.
Static bibliography and quick-copy output
renderCslQuickCopy is available from the existing optional @alggtta/editor/citations entry. Supply offline style/locales, mapped CSL items, 1–100 explicit selected item IDs in selection, and duplicates: 'first' | 'reject'. First-position deduplication reports duplicateIds; duplicate source item IDs always fail. Supply mode: 'bibliography' with bibliographyOrder: 'style' | 'selection', or mode: 'citation'. Selection ordering preserves grouped CSL entries, placing a group at its first selected member. Numeric labels are assigned by selected citation order; choosing style order does not renumber those labels.
The result contains paired plain text and restricted html, resolved selectedIds, optional bibliography entry identities, and static: true. Only selected items enter a standalone output. Styles without a bibliography fail rather than receive invented formatting. For a contextual citation, pass context: { citations, citationId } containing the whole ordered document, including the target cluster; its ordered item IDs must equal the deduplicated selection. Whole-context rendering preserves disambiguation, locator options and actual note indices. Context affects the chosen citation, but unselected bibliography output is never returned.
This is a snapshot API: it does not bind or edit a document, use the clipboard, write a file, fetch metadata/styles, or perform host navigation. Hosts explicitly regenerate after edits, choose clipboard/download behavior, and sanitize HTML at their established rendering boundary. It inherits the offline core's 200-item/document-cluster, XML and processor-work limits and absence of hard wall-clock isolation; assembled text plus HTML is limited to 2 MiB. RTF, linked bibliography editing, uncited/excluded document overrides and export-format Quick Copy remain separate host features. No new package entry or persisted node schema is introduced.
Offline citation style management
The optional @alggtta/editor/citations entry exports CslStyleManager and immutable style-library helpers. Start with emptyCslStyleLibrary(), install local .csl styles with installCslStyle, and install locale XML with installCslLocale. serializeCslStyleLibrary and readCslStyleLibrary round-trip the bounded profile without changing the original XML. Unknown/corrupt profiles must be retained and shown read-only; never replace them with an empty library automatically.
CslStyleManager is controlled by value and onChange(next, expectedRevision). The host must persist with an atomic revision comparison and keep the component keyed to its account/storage scope. Use disabled when editing permission is withdrawn. onApply receives the resolved independent style and offline locales; the host applies them through its existing document revision/permission checks and refreshEditorCitations. Installed parents cannot be removed while a dependent style uses them. A dependent style's default locale applies unless the user explicitly chooses another installed locale. en-US and the requested locale must be supplied explicitly.
Limits: 50 styles, 20 locales, 256000 characters per XML file, 1 MiB file input (fatal UTF-8 or BOM-marked UTF-16 decoding), and 8 MiB serialized library. XML safety/structure checks are not full CSL Relax NG conformance. This component searches installed styles and reads local files; online acquisition, cross-device storage, and Atlas integration through a reviewed exact npm version remain host work. It does not publish a package, fetch style/parent/locale URLs, or directly save document content.
Static RTF citation scan (CITE-07)
@alggtta/editor/citations exports scanRtfCitations, formatRtfCitations, and the optional offline RtfScanPanel. Supply up to 200 authorized CSL items and an installed style/locale snapshot. Scan results are process-bound immutable snapshots; every occurrence requires explicit ordered item IDs before formatting. Candidate matching supports {Author, YYYY}, and/&, et al., grouped references separated by semicolons, optional p./pp. locators, and n.d.. Ambiguous and unmatched references require a user choice; the formatter never silently chooses a candidate.
The input is a bounded 2 MiB RTF 1 byte-string, with Windows-1252 literals/hex escapes and signed Unicode escapes (including surrogate pairs); it is not a UTF-8 decoded file. Unsupported code pages, binary payloads, existing fields/objects/footnotes, malformed groups and oversized text are rejected. Header/footer, pictures, hidden text and ignorable destinations are preserved without scanning. Source character ranges are replaced without removing intervening formatting groups; other input bytes remain unchanged. The real CSL engine supplies restricted RTF formatting and static footnotes for note styles. {Bibliography} chooses the bibliography position; otherwise it is appended. Output is bounded to 4 MiB.
RtfScanPanel reads files locally and downloads a newly named Blob; it does not upload, overwrite the input, publish, or create live citation fields. Hosts must pass a new item/style snapshot when account/library authorization changes and set disabled on permission loss. Target/permission changes invalidate pending reads and revoke completed output URLs. Hosts implementing native file writes must separately enforce distinct input/output paths. Full RTF dialect support, live Word/LibreOffice fields, and actual Word/LibreOffice rendering are not claimed by these helpers.
Optional document DOI notice warnings
@alggtta/editor/citations exports createCitationWarningGate, CitationWarningPanel and inspectExistingCitationWarnings. The host supplies already-authorized current projections from Backend deea9b9fdbb641b4bfa22edf7ba044b7cb6ed5d0, with matching library/item/source revisions and a logical document ID/binding. This module never fetches providers, credentials or files. Bind a separate gate to each document and replace its state whenever the document session, source or notice binding changes; unknown/future/malformed projections block visibly. Do not reuse a gate across document identities. On authorization withdrawal, replace state with null to fail closed and invalidate approvals, or unmount the document.
Configure CitationExtension.configure({warningGate: gate, onWarning: decision => ...}) or createCitationPlugin({warningGate: gate, onWarning}). Known retraction/concern evidence, including stale/offline evidence, blocks silent insertCitation and updateCitation. Host UI explicitly calls gate.resolve(decision, 'proceed' | 'skip', 'each-time' | 'document'); then a separate user action may retry the current command. The UI does not automatically replay commands. One-shot authorization requires the same immutable document object and exact parsed proposal; dry-run .can() neither consumes approval nor emits decisions. A document-wide repeat choice takes effect only on a matching actual command and is bound to the same cloned evidence snapshot. Replacing state invalidates all decisions.
On reopening, explicitly call inspectExistingCitationWarnings(editor, gate) and display its bounded results, even for previously acknowledged warnings. Absence of notices is incomplete information, never a healthy verdict. CitationWarningPanel is a controlled, presentational component; the host owns acknowledgement state and current authorized document/source lifecycle. Generic JSON/HTML imports, clipboard paste, CSL batch refresh and external Office/Docs adapters are not protected by the insert/update command gate; hosts must inspect and enforce their own admission boundaries. Existing citation schema v1, occurrence identity, HTML and undo behavior are unchanged. No warning data or acknowledgement is persisted into citation nodes. Exact npm publication/adoption, Atlas host mounting and full original CITE-05 acceptance remain separate.
PDF annotation domain proposals (READ-02 foundation)
The optional @alggtta/editor/citations entry exports parsePdfAnnotationRecord,
preparePdfAnnotationCreate, preparePdfAnnotationUpdate, and
pdfAnnotationToResearchReference. These pure helpers accept own-data records;
there are no PDF, filesystem, network, clipboard or editor write side effects.
This profile supports highlight, underline and position-note proposals with a
page from 1 to 500 and at most 256 positive normalized rectangles. Coordinates
use the top-left origin on an unrotated page; the reader host owns transforms.
Text is preserved verbatim (including Unicode order), bounded to 10,000 UTF-16
units for each quote/comment; tags are bounded to 50 and color is a six-digit hex
value. Inputs reject unsupported future profiles, accessors, aliases, sparse
arrays and invalid Unicode, with a 256KiB own-data string/key budget.
import {
preparePdfAnnotationCreate, preparePdfAnnotationUpdate,
pdfAnnotationToResearchReference,
} from '@alggtta/editor/citations'
const source = {
libraryId, attachmentId, attachmentRevision, attachmentSha256,
itemId: null, itemRevision: null, // or both confirmed parent values
}
const proposal = preparePdfAnnotationCreate(annotationId, source, {
type: 'highlight', page: 1,
rects: [{ x: 0.1, y: 0.2, width: 0.5, height: 0.03 }],
quoteText: 'Selected text', comment: '', color: '#ffff00', tags: [],
})
// Explicitly authorize and save proposal in the host; only use its confirmed record.
const update = preparePdfAnnotationUpdate(confirmedRecord, source, confirmedRecord.revision, {
comment: 'Reviewed comment',
})
const node = pdfAnnotationToResearchReference(confirmedRecord, source, occurrenceId, 'Author · p. 1')
// Insert node via the existing research-notes plugin after the host's explicit action.
Create uses expected revision 0; update requires the captured annotation revision and cannot change source identities, parent, attachment revision or SHA256. These are proposals, not saved records, authorization or durable deduplication. The host must verify the real private attachment/hash and current permissions, perform atomic revision CAS, and return a confirmed record. An old source binding fails rather than relocating a quote automatically.
Copy returns the existing researchReference node shape, retaining a separate
quote snapshot and a caller-supplied occurrence ID. It adds no document schema.
Geometry, attachment hash/revision and comment remain in the host's annotation
record; the existing document source stores annotation ID/revision and optional
parent item ID. Host navigation must resolve and verify that source record.
An unsupported record must remain intact in host storage for repair, not be
silently migrated. This source change requires an exact published npm version
before Atlas consumption; a sibling source link is not a release boundary.
Reader persistence, reopen/edit/permission tests, embedded PDF import/export,
EPUB annotations, image/text/ink annotations, native clients and Reading Mode
cache/position linkage are separate pending work.