@sciflow/editor-start 0.0.3 → 0.1.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.
@@ -22,6 +22,27 @@
22
22
  *
23
23
  * The rest of this file is heavily documented to map those steps to concrete
24
24
  * code. Treat it as a cookbook for your own integration.
25
+ *
26
+ * Light-DOM editable surface
27
+ * --------------------------
28
+ * The ProseMirror `contenteditable` (`view.dom`) is mounted on a **light-DOM
29
+ * child** of the host element (class `sf-editable-surface`), projected
30
+ * visually into the shadow root via a `<slot>`. All chrome (formatbar,
31
+ * selection editor, node-view popovers) continues to live in the shadow root
32
+ * and is fully encapsulated.
33
+ *
34
+ * Why: external proofreading / AI writing extensions (Grammarly, LanguageTool
35
+ * browser add-on) walk the page's flat document tree for `[contenteditable]`
36
+ * and never enter shadow roots. Mounting the editable in the light DOM makes
37
+ * it discoverable by those tools without sacrificing shadow encapsulation for
38
+ * the surrounding chrome.
39
+ *
40
+ * Consequence for theming: `.ProseMirror` content styles and inline node-view
41
+ * styles (figures, citations, footnotes, marks) are injected into a `<style>`
42
+ * element appended to `document.head` (scoped to `.sf-editable-surface`) so
43
+ * they reach PM's nested DOM. Shadow-scoped `::part` / `:host` rules that
44
+ * applied to the editable content must be migrated to the
45
+ * `.sf-editable-surface .ProseMirror { … }` selector instead.
25
46
  */
26
47
  import { __decorate } from "tslib";
27
48
  import { css, html, unsafeCSS, LitElement } from 'lit';
@@ -31,6 +52,200 @@ import { manuscript as manuscriptSchema } from '@sciflow/schema-prosemirror';
31
52
  import editorStyles from './editor-element.css?inline';
32
53
  import { ghostCursorFeature } from './ghost-cursor.js';
33
54
  import { applyThemeStylesToRoot, subscribeToSciFlowTheme } from './theme.js';
55
+ // ---------------------------------------------------------------------------
56
+ // Light-DOM content styles
57
+ //
58
+ // ProseMirror's `.contenteditable` (view.dom) is mounted on `.sf-editable-surface`,
59
+ // a light-DOM child of the host element. Because `::slotted()` cannot reach
60
+ // PM's nested content, content styles are injected into `document.head` under
61
+ // a data attribute guard.
62
+ //
63
+ // Ref-counting: the shared `<style>` is injected when the first instance
64
+ // connects and removed when the last instance disconnects. A per-element
65
+ // width `<style>` is also managed in `document.head`, keyed by a unique id
66
+ // written as a `data-sf-width-<id>` attribute so it can be replaced or removed
67
+ // without touching the shared content styles.
68
+ //
69
+ // These styles are scoped to `.sf-editable-surface` so they don't leak into
70
+ // unrelated page content. Chrome styles (:host, .editor border, theme vars)
71
+ // remain shadow-scoped in `editor-element.css`.
72
+ // ---------------------------------------------------------------------------
73
+ const LIGHT_DOM_CONTENT_STYLES_ATTR = 'data-sf-content-styles';
74
+ /** Module-level ref-count: number of currently connected `<sciflow-editor>` instances. */
75
+ let _contentStylesRefCount = 0;
76
+ const LIGHT_DOM_CONTENT_STYLES = `
77
+ /* === SciFlow editor content styles (light-DOM surface) === */
78
+ /* Scoped to .sf-editable-surface to avoid leaking into the host page. */
79
+ .sf-editable-surface .ProseMirror {
80
+ position: relative;
81
+ word-wrap: break-word;
82
+ white-space: pre-wrap;
83
+ white-space: break-spaces;
84
+ -webkit-font-variant-ligatures: none;
85
+ font-variant-ligatures: none;
86
+ font-feature-settings: "liga" 0;
87
+ font-weight: lighter;
88
+ outline: none;
89
+ min-height: inherit;
90
+ padding: calc(0.75rem + 1.5em) 4rem 0.5rem calc(2.5rem + 1rem);
91
+ color: inherit;
92
+ font-size: 1rem;
93
+ counter-reset: sciflow-footnote;
94
+ line-height: 1.6;
95
+ }
96
+ .sf-editable-surface .ProseMirror p { margin: 0 0 0.75rem; }
97
+ .sf-editable-surface .ProseMirror p:only-child:last-child,
98
+ .sf-editable-surface .ProseMirror table p { margin: 0; }
99
+ .sf-editable-surface .ProseMirror h1,
100
+ .sf-editable-surface .ProseMirror h2,
101
+ .sf-editable-surface .ProseMirror h3,
102
+ .sf-editable-surface .ProseMirror h4,
103
+ .sf-editable-surface .ProseMirror h5,
104
+ .sf-editable-surface .ProseMirror h6 {
105
+ margin: 1.25rem 0 0.5rem; line-height: 1.2; position: relative;
106
+ }
107
+ .sf-editable-surface .ProseMirror > h1:first-child,
108
+ .sf-editable-surface .ProseMirror > h2:first-child,
109
+ .sf-editable-surface .ProseMirror > h3:first-child,
110
+ .sf-editable-surface .ProseMirror > h4:first-child,
111
+ .sf-editable-surface .ProseMirror > h5:first-child,
112
+ .sf-editable-surface .ProseMirror > h6:first-child { margin-top: 0; }
113
+ .sf-editable-surface .ProseMirror h1 { font-size: 1.33em; padding-top: 0.35em; font-weight: normal; }
114
+ .sf-editable-surface .ProseMirror h2 { font-size: 1.22em; font-weight: normal; }
115
+ .sf-editable-surface .ProseMirror h3 { font-size: 1.17em; font-weight: normal; }
116
+ .sf-editable-surface .ProseMirror h4 { font-size: 1.11em; font-weight: normal; }
117
+ .sf-editable-surface .ProseMirror h5 { font-size: 1.06em; font-weight: normal; }
118
+ .sf-editable-surface .ProseMirror h6 { font-size: 1em; font-weight: bold; }
119
+ .sf-editable-surface .ProseMirror h1::before {
120
+ content: "#"; opacity: 0.5; font-size: 0.8rem; position: absolute;
121
+ left: -2.5rem; top: 60%; transform: translateY(-50%); padding-right: .5rem;
122
+ }
123
+ .sf-editable-surface .ProseMirror h2::before {
124
+ content: "##"; opacity: 0.5; font-size: 0.8rem; padding-right: .5rem;
125
+ position: absolute; left: -2.5rem; top: 50%; transform: translateY(-50%);
126
+ }
127
+ .sf-editable-surface .ProseMirror h3::before {
128
+ content: "###"; opacity: 0.5; font-size: 0.8rem; padding-right: .5rem;
129
+ position: absolute; left: -2.5rem; top: 50%; transform: translateY(-50%);
130
+ }
131
+ .sf-editable-surface .ProseMirror li { position: relative; }
132
+ .sf-editable-surface .ProseMirror img { max-width: 40vw; height: auto; }
133
+ .sf-editable-surface .ProseMirror pre { white-space: pre-wrap; }
134
+ .sf-editable-surface .ProseMirror cite {
135
+ background: var(--sciflow-editor-citation-bg, rgba(15,23,42,0.05));
136
+ color: var(--sciflow-editor-citation-color, inherit);
137
+ padding: 0.1rem 0.35rem; border-radius: 4px;
138
+ }
139
+ .sf-editable-surface .ProseMirror a[data-type='xref'] {
140
+ background: var(--sciflow-editor-citation-bg, rgba(15,23,42,0.05));
141
+ color: var(--sciflow-editor-citation-color, inherit);
142
+ padding: 0.1rem 0.35rem; border-radius: 4px; text-decoration: none;
143
+ }
144
+ .sf-editable-surface .ProseMirror a[data-type='xref']:hover { text-decoration: underline; }
145
+ .sf-editable-surface .ProseMirror table {
146
+ width: 100%; border-collapse: collapse; border-spacing: 0; margin: 1rem 0;
147
+ }
148
+ .sf-editable-surface .ProseMirror th,
149
+ .sf-editable-surface .ProseMirror td {
150
+ padding: 0.5rem 0.75rem;
151
+ border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
152
+ vertical-align: top; box-sizing: border-box; position: relative;
153
+ }
154
+ .sf-editable-surface .ProseMirror td:not([data-colwidth]):not(.column-resize-dragging),
155
+ .sf-editable-surface .ProseMirror th:not([data-colwidth]):not(.column-resize-dragging) {
156
+ min-width: var(--default-cell-min-width, 80px);
157
+ }
158
+ .sf-editable-surface .ProseMirror .tableWrapper { overflow-x: auto; }
159
+ .sf-editable-surface .ProseMirror .column-resize-handle {
160
+ position: absolute; right: -2px; top: 0; bottom: 0; width: 4px;
161
+ z-index: 20; background-color: #adf; pointer-events: none;
162
+ }
163
+ .sf-editable-surface .ProseMirror.resize-cursor { cursor: ew-resize; cursor: col-resize; }
164
+ .sf-editable-surface .ProseMirror .selectedCell::after {
165
+ z-index: 2; position: absolute; content: '';
166
+ left: 0; right: 0; top: 0; bottom: 0;
167
+ background: rgba(200,200,255,0.4); pointer-events: none;
168
+ }
169
+ .sf-editable-surface .ProseMirror section { margin-top: 2.5rem; }
170
+ .sf-editable-surface .ProseMirror section:first-child { margin-top: 0.5rem; }
171
+ .sf-editable-surface .ProseMirror-hideselection *::selection { background: transparent; }
172
+ .sf-editable-surface .ProseMirror-hideselection *::-moz-selection { background: transparent; }
173
+ .sf-editable-surface .ProseMirror-hideselection { caret-color: transparent; }
174
+ .sf-editable-surface .ProseMirror [draggable][contenteditable='false'] { user-select: text; }
175
+ .sf-editable-surface .ProseMirror-selectednode {
176
+ outline: 2px solid var(--sciflow-editor-selectednode, var(--sciflow-editor-border-active, #2563eb));
177
+ outline-offset: 1px;
178
+ }
179
+ .sf-editable-surface .ProseMirror cite.ProseMirror-selectednode {
180
+ outline: none;
181
+ box-shadow: 0 0 0 2px var(--sciflow-editor-border-active, #2563eb);
182
+ }
183
+ .sf-editable-surface .ProseMirror span[data-type='footnote'].ProseMirror-selectednode {
184
+ outline: none; box-shadow: none;
185
+ }
186
+ .sf-editable-surface .ProseMirror span[data-type='footnote'].ProseMirror-selectednode .sciflow-footnote-body {
187
+ border-color: var(--sciflow-editor-border-active, #2563eb);
188
+ }
189
+ .sf-editable-surface .ProseMirror a[data-type='xref'].ProseMirror-selectednode {
190
+ outline: none;
191
+ box-shadow: 0 0 0 2px var(--sciflow-editor-selectednode, var(--sciflow-editor-border-active, #2563eb));
192
+ }
193
+ .sf-editable-surface .ProseMirror li.ProseMirror-selectednode { outline: none; }
194
+ .sf-editable-surface .ProseMirror li.ProseMirror-selectednode:after {
195
+ content: ''; position: absolute; left: -32px; right: -2px; top: -2px; bottom: -2px;
196
+ border: 2px solid var(--sciflow-editor-selectednode, var(--sciflow-editor-border-active, #2563eb));
197
+ pointer-events: none;
198
+ }
199
+ .sf-editable-surface .ProseMirror img.ProseMirror-separator {
200
+ display: inline !important; border: none !important; margin: 0 !important;
201
+ }
202
+ .sf-editable-surface .prosemirror-ghost-cursor {
203
+ display: inline-block; width: 3px; height: 1.25em;
204
+ background: #2563eb; border-radius: 2px; vertical-align: text-bottom;
205
+ margin: 0 -1px; pointer-events: none;
206
+ box-shadow: 0 0 0 1.5px color-mix(in srgb, #2563eb 30%, transparent);
207
+ animation: sf-ghost-cursor-pulse 1.8s ease-in-out infinite;
208
+ }
209
+ @keyframes sf-ghost-cursor-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
210
+ .sf-editable-surface .prosemirror-ghost-selection {
211
+ background: color-mix(in srgb, #2563eb 25%, transparent);
212
+ border-radius: 2px;
213
+ outline: 1px solid color-mix(in srgb, #2563eb 40%, transparent);
214
+ }
215
+ @media (prefers-color-scheme: dark) {
216
+ .sf-editable-surface .ProseMirror-selectednode { outline-color: var(--sciflow-editor-selectednode, rgba(148,163,184,0.6)); }
217
+ .sf-editable-surface .ProseMirror cite.ProseMirror-selectednode { box-shadow: 0 0 0 2px var(--sciflow-editor-border-active, #3b82f6); }
218
+ .sf-editable-surface .ProseMirror a[data-type='xref'].ProseMirror-selectednode { box-shadow: 0 0 0 2px var(--sciflow-editor-selectednode, rgba(148,163,184,0.6)); }
219
+ .sf-editable-surface .ProseMirror li.ProseMirror-selectednode:after { border-color: var(--sciflow-editor-selectednode, rgba(148,163,184,0.6)); }
220
+ }
221
+ `;
222
+ /**
223
+ * Increment the ref-count and inject the light-DOM content styles into
224
+ * document.head if this is the first active instance. SSR-safe.
225
+ */
226
+ function acquireLightDomContentStyles() {
227
+ if (typeof document === 'undefined')
228
+ return;
229
+ _contentStylesRefCount++;
230
+ if (_contentStylesRefCount === 1) {
231
+ const style = document.createElement('style');
232
+ style.setAttribute(LIGHT_DOM_CONTENT_STYLES_ATTR, '');
233
+ style.textContent = LIGHT_DOM_CONTENT_STYLES;
234
+ document.head.appendChild(style);
235
+ }
236
+ }
237
+ /**
238
+ * Decrement the ref-count and remove the shared content styles from
239
+ * document.head once the last active instance disconnects. SSR-safe.
240
+ */
241
+ function releaseLightDomContentStyles() {
242
+ if (typeof document === 'undefined')
243
+ return;
244
+ _contentStylesRefCount = Math.max(0, _contentStylesRefCount - 1);
245
+ if (_contentStylesRefCount === 0) {
246
+ document.querySelector(`[${LIGHT_DOM_CONTENT_STYLES_ATTR}]`)?.remove();
247
+ }
248
+ }
34
249
  /**
35
250
  * Custom element that owns a SciFlow editor instance.
36
251
  *
@@ -93,6 +308,72 @@ let SciFlowEditorElement = class SciFlowEditorElement extends LitElement {
93
308
  * ```
94
309
  */
95
310
  this.locale = null;
311
+ // -------------------------------------------------------------------------
312
+ // Binding: which document, which part, which client, through which strategy.
313
+ //
314
+ // All four are mount-time identity. A running editor is bound to exactly one
315
+ // of each, and none can be swapped underneath it, so changing any of them
316
+ // after the editor is up tears the editor down and re-creates it against the
317
+ // new binding — the same path a replaced `features` list takes.
318
+ //
319
+ // The element opens no connections of its own: it only forwards what the
320
+ // host gives it to `Editor.create()`.
321
+ // -------------------------------------------------------------------------
322
+ /**
323
+ * Sync strategy that owns loading and persisting this document.
324
+ *
325
+ * Unset (the default) means the element uses a built-in in-memory strategy:
326
+ * the document stays in the page and nothing is persisted. Set it to hand
327
+ * loading, persistence and — with `clientID` — collaborative concurrency to
328
+ * your own implementation.
329
+ *
330
+ * When a strategy is supplied and neither `doc` nor `initialContent` is set,
331
+ * the editor starts from the strategy's `load(docId)`.
332
+ *
333
+ * Note that tearing the editor down (element disconnect, or a change to any
334
+ * binding property) disposes the strategy via its `dispose()`. Supply a
335
+ * fresh strategy whenever you change the binding.
336
+ *
337
+ * @example
338
+ * ```js
339
+ * editor.docId = 'doc-123';
340
+ * editor.partId = 'chapter-2';
341
+ * editor.clientID = 'tab-7';
342
+ * editor.sync = myStrategy;
343
+ * ```
344
+ */
345
+ this.sync = null;
346
+ /**
347
+ * Stable identifier of the document this editor is bound to. Handed to the
348
+ * sync strategy's `load()` and `bind()`.
349
+ *
350
+ * Defaults to a fresh random id per mount, which is only meaningful for the
351
+ * built-in in-memory strategy — set it whenever you supply your own `sync`.
352
+ */
353
+ this.docId = null;
354
+ /**
355
+ * Stable client identifier for collaborative editing. Setting it activates
356
+ * step-based concurrency (`prosemirror-collab`) in the editor runtime, so
357
+ * remote changes rebase local edits instead of replacing the document.
358
+ *
359
+ * Should stay stable across reloads of the same client (for example one id
360
+ * per browser tab) so the server and other clients can recognize this
361
+ * client's own confirmed changes. Only useful together with a `sync`
362
+ * strategy that speaks a step protocol.
363
+ *
364
+ * Set as an attribute (`client-id`) the value is always a string; set as a
365
+ * property it may also be a number.
366
+ */
367
+ this.clientID = null;
368
+ /**
369
+ * Part of the document this editor is bound to, for backends whose
370
+ * concurrency unit is narrower than a document — one document holding
371
+ * several parts, each with its own version counter and change log.
372
+ *
373
+ * The editor never interprets it; it is forwarded to the sync strategy's
374
+ * `bind()` so the strategy can address the right part.
375
+ */
376
+ this.partId = null;
96
377
  this.view = null;
97
378
  this.pendingOps = [];
98
379
  this.pendingOpsFromTransform = false;
@@ -101,6 +382,20 @@ let SciFlowEditorElement = class SciFlowEditorElement extends LitElement {
101
382
  this.shadowStyleElements = [];
102
383
  this.themeStyleElements = [];
103
384
  this._hostDragoverBound = null;
385
+ /**
386
+ * The light-DOM container that ProseMirror mounts into.
387
+ * A direct child of the host element (not the shadow root), projected
388
+ * visually via the shadow root's `<slot>`.
389
+ */
390
+ this.lightDomMount = null;
391
+ /**
392
+ * Unique identifier for this element instance. Used to key the per-element
393
+ * width `<style>` in `document.head` so that each instance can update or
394
+ * remove its own rule without affecting other instances.
395
+ */
396
+ this._instanceId = `sf-${Math.random().toString(36).slice(2)}`;
397
+ /** The `<style>` element in `document.head` that holds this instance's width rules. */
398
+ this._widthStyleEl = null;
104
399
  }
105
400
  get shadowStyles() {
106
401
  return this._shadowStyles;
@@ -152,9 +447,14 @@ let SciFlowEditorElement = class SciFlowEditorElement extends LitElement {
152
447
  dispatchEvent: (event) => this.editor?.dispatchEvent(event) ?? false,
153
448
  };
154
449
  }
450
+ // Shadow root retains chrome styles. The `.editor` wrapper provides the
451
+ // border/focus-ring and projects the light-DOM editable via `<slot>`.
155
452
  static { this.styles = css `${unsafeCSS(editorStyles)}`; }
156
453
  render() {
157
- return html `<div class="editor" part="editor"></div>`;
454
+ // The <slot> projects the light-DOM .sf-editable-surface child visually
455
+ // into the shadow layout. All other chrome (formatbar slot, popovers)
456
+ // remains shadow-scoped via the editor-element.css `.editor` rules.
457
+ return html `<div class="editor" part="editor"><slot></slot></div>`;
158
458
  }
159
459
  /** Lit lifecycle hook: start the editor once the DOM container exists. */
160
460
  async firstUpdated() {
@@ -170,6 +470,29 @@ let SciFlowEditorElement = class SciFlowEditorElement extends LitElement {
170
470
  if (this.locale) {
171
471
  setLocale(this.locale);
172
472
  }
473
+ // Inject light-DOM content styles (ref-counted; injected on first connect,
474
+ // removed on last disconnect).
475
+ acquireLightDomContentStyles();
476
+ // Create the light-DOM mount point as a direct child of the host.
477
+ // ProseMirror will mount its editable (view.dom) inside this div.
478
+ // Because it is a child of the host element — not the shadow root — its
479
+ // getRootNode() === document, making it discoverable by external tools.
480
+ if (!this.lightDomMount) {
481
+ const div = document.createElement('div');
482
+ div.className = 'sf-editable-surface';
483
+ div.setAttribute('data-sf-lightdom-mount', '');
484
+ this.appendChild(div);
485
+ this.lightDomMount = div;
486
+ }
487
+ // Re-apply the per-element width style whenever the element (re-)connects.
488
+ // disconnectedCallback removes _widthStyleEl from document.head; without
489
+ // this call the width constraint is silently lost after a DOM re-parent.
490
+ // Guard with hasUpdated so we don't race with firstUpdated on the initial
491
+ // render cycle (firstUpdated → applyShadowStyles → applyWidthStyles already
492
+ // covers that path).
493
+ if (this.hasUpdated) {
494
+ this.applyWidthStyles();
495
+ }
173
496
  this._hostDragoverBound = (e) => {
174
497
  if (!e.dataTransfer?.types || !this.contains(e.target))
175
498
  return;
@@ -193,11 +516,22 @@ let SciFlowEditorElement = class SciFlowEditorElement extends LitElement {
193
516
  */
194
517
  updated(changedProperties) {
195
518
  super.updated(changedProperties);
196
- if (this.deferInitializationForDoc && this.doc !== null) {
519
+ if (this.deferInitializationForDoc && !this.shouldDeferInitialization()) {
197
520
  this.deferInitializationForDoc = false;
198
521
  void this.initializeView();
199
522
  }
200
- if (!this.suppressFeatureUpdate && changedProperties.has('features') && this.editor) {
523
+ // A running editor cannot be re-bound in place: its document identity, its
524
+ // part, its client identity and its strategy are all fixed at creation.
525
+ // Re-create it instead, discarding the previous binding's content — the
526
+ // new instance sources its document the same way a fresh mount does.
527
+ const bindingChanged = changedProperties.has('sync') ||
528
+ changedProperties.has('docId') ||
529
+ changedProperties.has('clientID') ||
530
+ changedProperties.has('partId');
531
+ if (bindingChanged && this.editor) {
532
+ void this.reinitializeEditor({ preserveContent: false });
533
+ }
534
+ else if (!this.suppressFeatureUpdate && changedProperties.has('features') && this.editor) {
201
535
  void this.reinitializeEditor();
202
536
  }
203
537
  // Only apply shadowStyles if changed via property (not via setShadowStyles method)
@@ -232,6 +566,18 @@ let SciFlowEditorElement = class SciFlowEditorElement extends LitElement {
232
566
  this.themeUnsub = undefined;
233
567
  this.destroyEditor();
234
568
  this.view = null;
569
+ // Remove the light-DOM mount point.
570
+ if (this.lightDomMount && this.lightDomMount.parentNode === this) {
571
+ this.removeChild(this.lightDomMount);
572
+ this.lightDomMount = null;
573
+ }
574
+ // Remove the per-element width style from document.head.
575
+ if (this._widthStyleEl) {
576
+ this._widthStyleEl.remove();
577
+ this._widthStyleEl = null;
578
+ }
579
+ // Release the shared content styles (removes from head when last instance disconnects).
580
+ releaseLightDomContentStyles();
235
581
  }
236
582
  /**
237
583
  * Public helper to update the feature list and reconfigure the editor.
@@ -274,17 +620,20 @@ let SciFlowEditorElement = class SciFlowEditorElement extends LitElement {
274
620
  return this.features ?? this.getDefaultFeatures();
275
621
  }
276
622
  shouldDeferInitialization() {
277
- return this.doc === null && this.initialContent === null;
623
+ // A host-supplied strategy is itself a source for the document (its
624
+ // `load()`), so there is nothing left to wait for.
625
+ return this.doc === null && this.initialContent === null && this.sync === null;
278
626
  }
279
627
  async initializeView() {
280
628
  if (this.view) {
281
629
  return;
282
630
  }
283
- const mountPoint = this.renderRoot.querySelector('.editor');
631
+ // Mount ProseMirror on the light-DOM child — the visual slot wrapper
632
+ // (.editor) is in the shadow root and projects this via <slot>.
633
+ const mountPoint = this.lightDomMount;
284
634
  if (!mountPoint) {
285
635
  return;
286
636
  }
287
- const initialDoc = this.resolveInitialDoc();
288
637
  const docInput = this.doc;
289
638
  const initialFiles = this.isExternalDocUpdate(docInput) && Array.isArray(docInput.files) ? docInput.files : [];
290
639
  const initialReferences = this.isExternalDocUpdate(docInput) && Array.isArray(docInput.references) ? docInput.references : [];
@@ -292,11 +641,28 @@ let SciFlowEditorElement = class SciFlowEditorElement extends LitElement {
292
641
  const externalSelection = this.isExternalDocUpdate(docInput) ? docInput.selection : undefined;
293
642
  const initialVersion = this.isExternalDocUpdate(docInput) && typeof docInput.version === 'number' ? docInput.version : undefined;
294
643
  const initialSelection = this.initialSelection ?? externalSelection;
644
+ // Who supplies the starting document: the host, or the strategy? A
645
+ // host-supplied strategy with no host-supplied content owns the load, and
646
+ // `Editor.create()` only calls `sync.load(docId)` when it receives no
647
+ // `initialDoc` — so that option has to be left out entirely, not passed
648
+ // as a placeholder.
649
+ const hostSync = this.sync;
650
+ let syncStrategy;
651
+ let initialDoc = null;
652
+ if (hostSync && docInput === null && this.initialContent === null) {
653
+ syncStrategy = hostSync;
654
+ }
655
+ else {
656
+ initialDoc = this.resolveInitialDoc();
657
+ syncStrategy = hostSync ?? this.createLocalSync(initialDoc);
658
+ }
295
659
  try {
296
660
  const editor = await Editor.create({
297
- docId: `lit-${Math.random().toString(36).slice(2)}`,
298
- sync: this.createLocalSync(initialDoc),
299
- initialDoc: initialDoc.toJSON(),
661
+ docId: this.docId ?? `lit-${Math.random().toString(36).slice(2)}`,
662
+ sync: syncStrategy,
663
+ clientID: this.clientID ?? undefined,
664
+ partId: this.partId ?? undefined,
665
+ ...(initialDoc ? { initialDoc: initialDoc.toJSON() } : {}),
300
666
  initialFiles,
301
667
  initialReferences,
302
668
  features: activeFeatures,
@@ -324,7 +690,7 @@ let SciFlowEditorElement = class SciFlowEditorElement extends LitElement {
324
690
  this.dispatchSelectionChange(selection);
325
691
  });
326
692
  this.validationUnsub = editor.onValidation(() => undefined);
327
- this.dispatchEditorChange(initialDoc.toJSON(), [], editor.getFiles());
693
+ this.dispatchEditorChange(initialDoc ? initialDoc.toJSON() : editor.getDoc(), [], editor.getFiles());
328
694
  this.dispatchEditorReady();
329
695
  this.initialSelection = undefined;
330
696
  }
@@ -360,7 +726,7 @@ let SciFlowEditorElement = class SciFlowEditorElement extends LitElement {
360
726
  return {
361
727
  doc: docJSON,
362
728
  version: 0,
363
- selection: { anchor: 0, head: 0 },
729
+ selection: { anchor: 1, head: 1 },
364
730
  files: [],
365
731
  references: [],
366
732
  };
@@ -601,16 +967,27 @@ let SciFlowEditorElement = class SciFlowEditorElement extends LitElement {
601
967
  * Tear down subscriptions and dispose the editor instance.
602
968
  * Called automatically when the element disconnects.
603
969
  */
604
- async reinitializeEditor() {
970
+ async reinitializeEditor(options = {}) {
605
971
  if (!this.isConnected) {
606
972
  return;
607
973
  }
608
- const currentDoc = this.editor?.getDoc() ?? null;
609
- const currentSelection = this.editor?.getSelection() ?? null;
610
- const currentFiles = this.editor?.getFiles() ?? [];
611
- const currentVersion = this.editor?.getVersion();
974
+ // Reconfiguring the editor (a new feature list) keeps the live document:
975
+ // it is the same document, edited by the same client, through the same
976
+ // strategy. Re-binding it (a new document, part, client or strategy) does
977
+ // not — that content belongs to the previous binding, and the new one
978
+ // sources its own from `doc`/`initialContent`/`sync.load()`.
979
+ const preserveContent = options.preserveContent ?? true;
980
+ const currentDoc = preserveContent ? this.editor?.getDoc() ?? null : null;
981
+ const currentSelection = preserveContent ? this.editor?.getSelection() ?? null : null;
982
+ const currentFiles = preserveContent ? this.editor?.getFiles() ?? [] : [];
983
+ const currentVersion = preserveContent ? this.editor?.getVersion() : undefined;
612
984
  this.destroyEditor();
613
985
  this.view = null;
986
+ // Clear the light-DOM mount so ProseMirror gets a fresh container on
987
+ // re-init (avoids stale node-view DOM from the previous instance).
988
+ if (this.lightDomMount) {
989
+ this.lightDomMount.innerHTML = '';
990
+ }
614
991
  if (currentDoc) {
615
992
  const nextDoc = {
616
993
  doc: currentDoc,
@@ -624,7 +1001,9 @@ let SciFlowEditorElement = class SciFlowEditorElement extends LitElement {
624
1001
  }
625
1002
  this.doc = nextDoc;
626
1003
  }
627
- this.initialSelection = currentSelection ?? this.initialSelection;
1004
+ this.initialSelection = preserveContent
1005
+ ? currentSelection ?? this.initialSelection
1006
+ : undefined;
628
1007
  await this.initializeView();
629
1008
  }
630
1009
  /**
@@ -684,30 +1063,49 @@ let SciFlowEditorElement = class SciFlowEditorElement extends LitElement {
684
1063
  if (!root || typeof root.appendChild !== 'function') {
685
1064
  return;
686
1065
  }
1066
+ // Remove previously injected consumer shadow styles.
687
1067
  for (const styleEl of this.shadowStyleElements) {
688
1068
  if (styleEl.parentNode === root) {
689
1069
  root.removeChild(styleEl);
690
1070
  }
691
1071
  }
692
1072
  this.shadowStyleElements = [];
1073
+ // Consumer-supplied shadow styles go into the shadow root (unchanged API).
693
1074
  const cssTexts = this.normalizeShadowStyles(this.shadowStyles);
694
- const widthCss = this.getEditorWidthStyles();
695
- if (widthCss) {
696
- cssTexts.push(widthCss);
697
- }
698
- if (cssTexts.length === 0) {
699
- return;
700
- }
701
1075
  for (const cssText of cssTexts) {
702
1076
  const styleEl = document.createElement('style');
703
1077
  styleEl.textContent = cssText;
704
1078
  root.appendChild(styleEl);
705
1079
  this.shadowStyleElements.push(styleEl);
706
1080
  }
1081
+ // Width styles target the light-DOM editable (.sf-editable-surface .ProseMirror).
1082
+ // Shadow CSS cannot pierce into slotted descendants, so these must live in
1083
+ // document.head where they reach the light-DOM tree directly.
1084
+ this.applyWidthStyles();
1085
+ }
1086
+ /**
1087
+ * Inject (or replace) the per-element width style in `document.head`.
1088
+ * Keyed by `data-sf-width-<instanceId>` so each instance manages its own rule.
1089
+ * SSR-safe.
1090
+ */
1091
+ applyWidthStyles() {
1092
+ if (typeof document === 'undefined')
1093
+ return;
1094
+ const css = this.getEditorWidthStyles();
1095
+ if (!this._widthStyleEl) {
1096
+ const el = document.createElement('style');
1097
+ el.setAttribute(`data-sf-width-${this._instanceId}`, '');
1098
+ document.head.appendChild(el);
1099
+ this._widthStyleEl = el;
1100
+ }
1101
+ this._widthStyleEl.textContent = css;
707
1102
  }
708
1103
  getEditorWidthStyles() {
709
1104
  const isNarrow = this.editorWidth === 'narrow';
710
- return `.ProseMirror {
1105
+ // Scoped to .sf-editable-surface so the rule reaches the light-DOM editable.
1106
+ // Injected into document.head (not the shadow root) so shadow-CSS slot
1107
+ // restrictions do not apply.
1108
+ return `.sf-editable-surface[data-sf-lightdom-mount] .ProseMirror {
711
1109
  font-size: 11pt;
712
1110
  max-width: ${isNarrow ? '52ch' : '82ch'};
713
1111
  margin-left: auto;
@@ -776,6 +1174,18 @@ __decorate([
776
1174
  __decorate([
777
1175
  property({ type: String, reflect: true })
778
1176
  ], SciFlowEditorElement.prototype, "locale", void 0);
1177
+ __decorate([
1178
+ property({ attribute: false })
1179
+ ], SciFlowEditorElement.prototype, "sync", void 0);
1180
+ __decorate([
1181
+ property({ type: String, attribute: 'doc-id' })
1182
+ ], SciFlowEditorElement.prototype, "docId", void 0);
1183
+ __decorate([
1184
+ property({ attribute: 'client-id' })
1185
+ ], SciFlowEditorElement.prototype, "clientID", void 0);
1186
+ __decorate([
1187
+ property({ type: String, attribute: 'part-id' })
1188
+ ], SciFlowEditorElement.prototype, "partId", void 0);
779
1189
  SciFlowEditorElement = __decorate([
780
1190
  customElement('sciflow-editor')
781
1191
  ], SciFlowEditorElement);
@@ -35,6 +35,16 @@ export declare class SciFlowFormatBarElement extends LitElement {
35
35
  editor: SciFlowEditorElement | null;
36
36
  /** ID reference to resolve an editor instance declaratively. */
37
37
  for?: string;
38
+ /**
39
+ * Space- or comma-separated list of command-metadata `group` names to omit
40
+ * from rendering (e.g. `"align"`). Suppresses the toolbar buttons for that
41
+ * group without touching command registration — the commands stay
42
+ * schema-legal and remain reachable via `editor.commands`/`runner.available()`.
43
+ * Use this for structured-manuscript contexts (e.g. a JATS-based export)
44
+ * where a UI-exposed capability isn't representable in the export content
45
+ * model. See docs/pages/user-guide/web-component-api.md.
46
+ */
47
+ hiddenGroups: string;
38
48
  private selectionVersion;
39
49
  private styleMenuOpen;
40
50
  private insertMenuOpen;
@@ -109,6 +119,18 @@ export declare class SciFlowFormatBarElement extends LitElement {
109
119
  * Render a single command button, wiring up availability checks and active state handling.
110
120
  */
111
121
  private renderCommandButton;
122
+ /**
123
+ * Parse the `hidden-groups` attribute into a set of group names to suppress.
124
+ * Accepts whitespace- and/or comma-separated values (e.g. `"align table"`, `"align,table"`).
125
+ */
126
+ private get hiddenGroupSet();
127
+ /**
128
+ * Drop metadata entries whose `group` is listed in `hidden-groups` before any
129
+ * rendering derivation runs. This is a rendering-only filter: it does not touch
130
+ * command registration, so hidden commands stay schema-legal and remain reachable
131
+ * via `editor.commands`/`runner.available()` — only the toolbar buttons disappear.
132
+ */
133
+ private filterHiddenGroups;
112
134
  /**
113
135
  * Determine which groups should be displayed and in which order based on command metadata.
114
136
  */
@@ -1 +1 @@
1
- {"version":3,"file":"format-bar.d.ts","sourceRoot":"","sources":["../../src/lib/format-bar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAa,UAAU,EAAsB,MAAM,KAAK,CAAC;AAOhE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAoChE;;;;;;;;;;;GAWG;AACH,qBACa,uBAAwB,SAAQ,UAAU;IACrD,oFAAoF;IAEpF,MAAM,EAAE,oBAAoB,GAAG,IAAI,CAAQ;IAE3C,gEAAgE;IAEhE,GAAG,CAAC,EAAE,MAAM,CAAC;IAGb,OAAO,CAAC,gBAAgB,CAAK;IAG7B,OAAO,CAAC,aAAa,CAAS;IAG9B,OAAO,CAAC,cAAc,CAAS;IAE/B,OAAO,CAAC,cAAc,CAAqC;IAC3D,OAAO,CAAC,iBAAiB,CAAC,CAAgB;IAC1C,OAAO,CAAC,aAAa,CAAC,CAAgB;IACtC,OAAO,CAAC,cAAc,CAAC,CAAgB;IACvC,OAAO,CAAC,UAAU,CAAC,CAAa;IAChC,OAAO,CAAC,kBAAkB,CAA0B;IACpD,OAAO,CAAC,mBAAmB,CAAC,CAA0B;IAEtD,OAAgB,MAAM,0BAAmC;IAEzD,uEAAuE;IAC9D,iBAAiB,IAAI,IAAI;IAoBlC,qDAAqD;IAC5C,oBAAoB,IAAI,IAAI;IAYrC;;;OAGG;cACgB,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAMjF;;;OAGG;cACgB,MAAM;IAwCzB,OAAO,CAAC,mBAAmB;IAyD3B,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,WAAW;IAoBnB,OAAO,CAAC,kBAAkB;IAc1B,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,oBAAoB;IAuD5B,OAAO,CAAC,gBAAgB;IAWxB,OAAO,CAAC,oBAAoB;IAmB5B,OAAO,CAAC,0BAA0B;IAkBlC,OAAO,CAAC,yBAAyB;IAWjC;;OAEG;IACH,OAAO,CAAC,wBAAwB;IA2ChC;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IA+B9B,OAAO,CAAC,aAAa;IAkBrB,+DAA+D;IAC/D,OAAO,CAAC,qBAAqB;IAc7B,2DAA2D;IAC3D,OAAO,CAAC,gBAAgB;IAQxB,8EAA8E;IAC9E,OAAO,CAAC,gBAAgB;IA6BxB,mDAAmD;IACnD,OAAO,CAAC,kBAAkB;IA6B1B,yEAAyE;IACzE,OAAO,CAAC,mBAAmB;IA6C3B,gFAAgF;IAChF,OAAO,CAAC,iBAAiB;IAmBzB,wEAAwE;IACxE,OAAO,CAAC,kBAAkB;IAmB1B;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAkB5B;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAoB1B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAuC3B;;OAEG;IACH,OAAO,CAAC,gBAAgB;CA2BzB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,mBAAmB,EAAE,uBAAuB,CAAC;KAC9C;CACF"}
1
+ {"version":3,"file":"format-bar.d.ts","sourceRoot":"","sources":["../../src/lib/format-bar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAa,UAAU,EAAsB,MAAM,KAAK,CAAC;AAOhE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAoChE;;;;;;;;;;;GAWG;AACH,qBACa,uBAAwB,SAAQ,UAAU;IACrD,oFAAoF;IAEpF,MAAM,EAAE,oBAAoB,GAAG,IAAI,CAAQ;IAE3C,gEAAgE;IAEhE,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;;;;;;OAQG;IAEH,YAAY,SAAM;IAGlB,OAAO,CAAC,gBAAgB,CAAK;IAG7B,OAAO,CAAC,aAAa,CAAS;IAG9B,OAAO,CAAC,cAAc,CAAS;IAE/B,OAAO,CAAC,cAAc,CAAqC;IAC3D,OAAO,CAAC,iBAAiB,CAAC,CAAgB;IAC1C,OAAO,CAAC,aAAa,CAAC,CAAgB;IACtC,OAAO,CAAC,cAAc,CAAC,CAAgB;IACvC,OAAO,CAAC,UAAU,CAAC,CAAa;IAChC,OAAO,CAAC,kBAAkB,CAA0B;IACpD,OAAO,CAAC,mBAAmB,CAAC,CAA0B;IAEtD,OAAgB,MAAM,0BAAmC;IAEzD,uEAAuE;IAC9D,iBAAiB,IAAI,IAAI;IAoBlC,qDAAqD;IAC5C,oBAAoB,IAAI,IAAI;IAYrC;;;OAGG;cACgB,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAMjF;;;OAGG;cACgB,MAAM;IAwCzB,OAAO,CAAC,mBAAmB;IAyD3B,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,WAAW;IAoBnB,OAAO,CAAC,kBAAkB;IAc1B,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,oBAAoB;IAuD5B,OAAO,CAAC,gBAAgB;IAWxB,OAAO,CAAC,oBAAoB;IAmB5B,OAAO,CAAC,0BAA0B;IAkBlC,OAAO,CAAC,yBAAyB;IAWjC;;OAEG;IACH,OAAO,CAAC,wBAAwB;IA2ChC;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IA+B9B,OAAO,CAAC,aAAa;IAkBrB,+DAA+D;IAC/D,OAAO,CAAC,qBAAqB;IAc7B,2DAA2D;IAC3D,OAAO,CAAC,gBAAgB;IAQxB,8EAA8E;IAC9E,OAAO,CAAC,gBAAgB;IA6BxB,mDAAmD;IACnD,OAAO,CAAC,kBAAkB;IA6B1B,yEAAyE;IACzE,OAAO,CAAC,mBAAmB;IA6C3B,gFAAgF;IAChF,OAAO,CAAC,iBAAiB;IAmBzB,wEAAwE;IACxE,OAAO,CAAC,kBAAkB;IAmB1B;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAkB5B;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAoB1B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAuC3B;;;OAGG;IACH,OAAO,KAAK,cAAc,GAOzB;IAED;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IAY1B;;OAEG;IACH,OAAO,CAAC,gBAAgB;CA2BzB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,mBAAmB,EAAE,uBAAuB,CAAC;KAC9C;CACF"}