From 3e5377e7d93d6b64d9f0a3128425cc638c7650e1 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Wed, 4 Feb 2026 12:19:09 -0300 Subject: [PATCH 01/18] feat(super-editor): add w:lock support for StructuredContent nodes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement ECMA-376 §17.5.2.23 w:lock support for StructuredContent and StructuredContentBlock nodes. This enables template variables to enforce read-only behavior based on lock modes. Lock modes: - unlocked: no restrictions (default) - sdtLocked: SDT wrapper cannot be deleted, content editable - contentLocked: content read-only, SDT can be deleted - sdtContentLocked: fully locked (wrapper and content) Changes: - Add lockMode attribute to StructuredContent/Block extensions - Parse w:lock element on DOCX import - Export w:lock element on DOCX save - Add lock enforcement plugin (prevents deletion of locked SDTs) - Add NodeView methods for content editability - Add visual styling matching Word's appearance (presentation mode) - Add TypeScript types for lock modes - Add unit tests for import, export, and lock behavior --- packages/layout-engine/contracts/src/index.ts | 3 + .../painters/dom/src/renderer.ts | 2 + .../layout-engine/painters/dom/src/styles.ts | 37 +++ .../painters/dom/src/utils/sdt-helpers.ts | 17 +- .../layout-engine/style-engine/src/index.ts | 25 ++ .../helpers/handle-structured-content-node.js | 7 + .../handle-structured-content-node.test.js | 98 +++++++ .../helpers/translate-structured-content.js | 8 +- .../translate-structured-content.test.js | 104 +++++++ .../StructuredContentBlockView.js | 4 + .../StructuredContentInlineView.js | 4 + .../StructuredContentViewBase.js | 28 ++ .../structured-content-block.js | 9 + .../structured-content-lock-plugin.js | 36 +++ .../structured-content-lock-plugin.test.js | 271 ++++++++++++++++++ .../structured-content/structured-content.js | 14 + .../src/extensions/types/node-attributes.ts | 4 + 17 files changed, 666 insertions(+), 5 deletions(-) create mode 100644 packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js create mode 100644 packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.test.js diff --git a/packages/layout-engine/contracts/src/index.ts b/packages/layout-engine/contracts/src/index.ts index eaebd6444c..701695b3aa 100644 --- a/packages/layout-engine/contracts/src/index.ts +++ b/packages/layout-engine/contracts/src/index.ts @@ -56,12 +56,15 @@ export type FieldAnnotationMetadata = { marks?: Record; }; +export type StructuredContentLockMode = 'unlocked' | 'sdtLocked' | 'contentLocked' | 'sdtContentLocked'; + export type StructuredContentMetadata = { type: 'structuredContent'; scope: 'inline' | 'block'; id?: string | null; tag?: string | null; alias?: string | null; + lockMode?: StructuredContentLockMode; sdtPr?: unknown; }; diff --git a/packages/layout-engine/painters/dom/src/renderer.ts b/packages/layout-engine/painters/dom/src/renderer.ts index 5b69abb9be..97e89fa6a2 100644 --- a/packages/layout-engine/painters/dom/src/renderer.ts +++ b/packages/layout-engine/painters/dom/src/renderer.ts @@ -5113,6 +5113,7 @@ export class DomPainter { 'sdtScope', 'sdtTag', 'sdtAlias', + 'lockMode', 'sdtSectionTitle', 'sdtSectionType', 'sdtSectionLocked', @@ -5169,6 +5170,7 @@ export class DomPainter { this.setDatasetString(el, 'sdtScope', metadata.scope); this.setDatasetString(el, 'sdtTag', metadata.tag); this.setDatasetString(el, 'sdtAlias', metadata.alias); + this.setDatasetString(el, 'lockMode', metadata.lockMode || 'unlocked'); } else if (metadata.type === 'documentSection') { this.setDatasetString(el, 'sdtSectionTitle', metadata.title); this.setDatasetString(el, 'sdtSectionType', metadata.sectionType); diff --git a/packages/layout-engine/painters/dom/src/styles.ts b/packages/layout-engine/painters/dom/src/styles.ts index 0ea2ce6e88..c14e16f5b0 100644 --- a/packages/layout-engine/painters/dom/src/styles.ts +++ b/packages/layout-engine/painters/dom/src/styles.ts @@ -453,6 +453,43 @@ const SDT_CONTAINER_STYLES = ` display: block; } +/* Lock mode styles for structured content - matches Word appearance exactly */ +/* Default: background color only, no border. Border appears on hover/focus */ + +/* unlocked: light mint green - fully editable and deletable */ +.superdoc-structured-content-block[data-lock-mode="unlocked"], +.superdoc-structured-content-inline[data-lock-mode="unlocked"] { + background-color: #e6f4ea; + border: 1px solid transparent; +} + +/* sdtLocked: golden yellow - SDT cannot be deleted but content can be edited */ +.superdoc-structured-content-block[data-lock-mode="sdtLocked"], +.superdoc-structured-content-inline[data-lock-mode="sdtLocked"] { + background-color: #fff3cd; + border: 1px solid transparent; +} + +/* contentLocked: light blue/lavender - content is read-only but SDT can be deleted */ +.superdoc-structured-content-block[data-lock-mode="contentLocked"], +.superdoc-structured-content-inline[data-lock-mode="contentLocked"] { + background-color: #e8f0f8; + border: 1px solid transparent; +} + +/* sdtContentLocked: light peach/salmon - fully locked */ +.superdoc-structured-content-block[data-lock-mode="sdtContentLocked"], +.superdoc-structured-content-inline[data-lock-mode="sdtContentLocked"] { + background-color: #ffe8e0; + border: 1px solid transparent; +} + +/* Show blue border on hover for all lock modes */ +.superdoc-structured-content-block[data-lock-mode]:hover, +.superdoc-structured-content-inline[data-lock-mode]:hover { + border-color: #629be7; +} + /* Viewing mode: remove structured content affordances */ .presentation-editor--viewing .superdoc-structured-content-block, .presentation-editor--viewing .superdoc-structured-content-inline { diff --git a/packages/layout-engine/painters/dom/src/utils/sdt-helpers.ts b/packages/layout-engine/painters/dom/src/utils/sdt-helpers.ts index 807cfd42e6..867b065ffe 100644 --- a/packages/layout-engine/painters/dom/src/utils/sdt-helpers.ts +++ b/packages/layout-engine/painters/dom/src/utils/sdt-helpers.ts @@ -6,7 +6,7 @@ * duplication across rendering logic. */ -import type { SdtMetadata } from '@superdoc/contracts'; +import type { SdtMetadata, StructuredContentLockMode } from '@superdoc/contracts'; /** * Type guard for StructuredContentMetadata with specific properties. @@ -24,9 +24,12 @@ import type { SdtMetadata } from '@superdoc/contracts'; * } * ``` */ -export function isStructuredContentMetadata( - sdt: SdtMetadata | null | undefined, -): sdt is { type: 'structuredContent'; scope: 'inline' | 'block'; alias?: string | null } { +export function isStructuredContentMetadata(sdt: SdtMetadata | null | undefined): sdt is { + type: 'structuredContent'; + scope: 'inline' | 'block'; + alias?: string | null; + lockMode?: StructuredContentLockMode; +} { return ( sdt !== null && sdt !== undefined && typeof sdt === 'object' && 'type' in sdt && sdt.type === 'structuredContent' ); @@ -257,6 +260,12 @@ export function applySdtContainerStyling( container.dataset.sdtContainerEnd = String(isEnd); container.style.overflow = 'visible'; // Allow label to show above + if (isStructuredContentMetadata(sdt)) { + container.dataset.lockMode = sdt.lockMode || 'unlocked'; + } else if (isStructuredContentMetadata(containerSdt)) { + container.dataset.lockMode = containerSdt.lockMode || 'unlocked'; + } + if (boundaryOptions?.widthOverride != null) { container.style.width = `${boundaryOptions.widthOverride}px`; } diff --git a/packages/layout-engine/style-engine/src/index.ts b/packages/layout-engine/style-engine/src/index.ts index 7a801ff319..d7a73d07ec 100644 --- a/packages/layout-engine/style-engine/src/index.ts +++ b/packages/layout-engine/style-engine/src/index.ts @@ -247,10 +247,35 @@ function normalizeStructuredContentMetadata( id: toNullableString(attrs.id), tag: toOptionalString(attrs.tag), alias: toOptionalString(attrs.alias), + lockMode: normalizeLockMode(attrs.lockMode), sdtPr: attrs.sdtPr, }; } +function normalizeLockMode(value: unknown): StructuredContentMetadata['lockMode'] { + if (typeof value !== 'string') return undefined; + const normalized = value.toLowerCase(); + if ( + normalized === 'unlocked' || + normalized === 'sdtlocked' || + normalized === 'contentlocked' || + normalized === 'sdtcontentlocked' + ) { + // Normalize to proper camelCase format + switch (normalized) { + case 'sdtlocked': + return 'sdtLocked'; + case 'contentlocked': + return 'contentLocked'; + case 'sdtcontentlocked': + return 'sdtContentLocked'; + default: + return 'unlocked'; + } + } + return undefined; +} + function normalizeDocumentSectionMetadata(attrs: Record): DocumentSectionMetadata { return { type: 'documentSection', diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/sdt/helpers/handle-structured-content-node.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/sdt/helpers/handle-structured-content-node.js index 79dac4cc1b..1e233dbd64 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/w/sdt/helpers/handle-structured-content-node.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/sdt/helpers/handle-structured-content-node.js @@ -19,6 +19,12 @@ export function handleStructuredContentNode(params) { const tag = sdtPr?.elements?.find((el) => el.name === 'w:tag'); const alias = sdtPr?.elements?.find((el) => el.name === 'w:alias'); + // Get the lock tag and value + const lockTag = sdtPr?.elements?.find((el) => el.name === 'w:lock'); + const lockValue = lockTag?.attributes?.['w:val']; + const validModes = ['unlocked', 'sdtLocked', 'contentLocked', 'sdtContentLocked']; + const lockMode = validModes.includes(lockValue) ? lockValue : 'unlocked'; + if (!sdtContent) { return null; } @@ -43,6 +49,7 @@ export function handleStructuredContentNode(params) { id: id?.attributes?.['w:val'] || null, tag: tag?.attributes?.['w:val'] || null, alias: alias?.attributes?.['w:val'] || null, + lockMode, sdtPr, }, }; diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/sdt/helpers/handle-structured-content-node.test.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/sdt/helpers/handle-structured-content-node.test.js index 855eaba735..ebae9ef5a5 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/w/sdt/helpers/handle-structured-content-node.test.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/sdt/helpers/handle-structured-content-node.test.js @@ -126,4 +126,102 @@ describe('handleStructuredContentNode', () => { expect(result.attrs.sdtPr).toEqual(sdtPr); }); + + describe('w:lock parsing', () => { + it('parses sdtLocked lock mode', () => { + const sdtPrElements = [{ name: 'w:lock', attributes: { 'w:val': 'sdtLocked' } }]; + const node = createNode(sdtPrElements, [{ name: 'w:r', text: 'content' }]); + + const params = { + nodes: [node], + nodeListHandler: mockNodeListHandler, + }; + + parseAnnotationMarks.mockReturnValue({ marks: [] }); + + const result = handleStructuredContentNode(params); + + expect(result.attrs.lockMode).toBe('sdtLocked'); + }); + + it('parses contentLocked lock mode', () => { + const sdtPrElements = [{ name: 'w:lock', attributes: { 'w:val': 'contentLocked' } }]; + const node = createNode(sdtPrElements, [{ name: 'w:r', text: 'content' }]); + + const params = { + nodes: [node], + nodeListHandler: mockNodeListHandler, + }; + + parseAnnotationMarks.mockReturnValue({ marks: [] }); + + const result = handleStructuredContentNode(params); + + expect(result.attrs.lockMode).toBe('contentLocked'); + }); + + it('parses sdtContentLocked lock mode', () => { + const sdtPrElements = [{ name: 'w:lock', attributes: { 'w:val': 'sdtContentLocked' } }]; + const node = createNode(sdtPrElements, [{ name: 'w:r', text: 'content' }]); + + const params = { + nodes: [node], + nodeListHandler: mockNodeListHandler, + }; + + parseAnnotationMarks.mockReturnValue({ marks: [] }); + + const result = handleStructuredContentNode(params); + + expect(result.attrs.lockMode).toBe('sdtContentLocked'); + }); + + it('defaults to unlocked when w:lock element is missing', () => { + const sdtPrElements = [{ name: 'w:tag', attributes: { 'w:val': 'test' } }]; + const node = createNode(sdtPrElements, [{ name: 'w:r', text: 'content' }]); + + const params = { + nodes: [node], + nodeListHandler: mockNodeListHandler, + }; + + parseAnnotationMarks.mockReturnValue({ marks: [] }); + + const result = handleStructuredContentNode(params); + + expect(result.attrs.lockMode).toBe('unlocked'); + }); + + it('defaults to unlocked for invalid lock mode values', () => { + const sdtPrElements = [{ name: 'w:lock', attributes: { 'w:val': 'invalidMode' } }]; + const node = createNode(sdtPrElements, [{ name: 'w:r', text: 'content' }]); + + const params = { + nodes: [node], + nodeListHandler: mockNodeListHandler, + }; + + parseAnnotationMarks.mockReturnValue({ marks: [] }); + + const result = handleStructuredContentNode(params); + + expect(result.attrs.lockMode).toBe('unlocked'); + }); + + it('parses unlocked lock mode explicitly', () => { + const sdtPrElements = [{ name: 'w:lock', attributes: { 'w:val': 'unlocked' } }]; + const node = createNode(sdtPrElements, [{ name: 'w:r', text: 'content' }]); + + const params = { + nodes: [node], + nodeListHandler: mockNodeListHandler, + }; + + parseAnnotationMarks.mockReturnValue({ marks: [] }); + + const result = handleStructuredContentNode(params); + + expect(result.attrs.lockMode).toBe('unlocked'); + }); + }); }); diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/sdt/helpers/translate-structured-content.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/sdt/helpers/translate-structured-content.js index 4740426c75..0363b91054 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/w/sdt/helpers/translate-structured-content.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/sdt/helpers/translate-structured-content.js @@ -56,15 +56,21 @@ function generateSdtPrTagForStructuredContent({ node }) { type: 'element', attributes: { 'w:val': attrs.tag }, }; + const lock = { + name: 'w:lock', + type: 'element', + attributes: { 'w:val': attrs.lockMode }, + }; const resultElements = []; if (attrs.id) resultElements.push(id); if (attrs.alias) resultElements.push(alias); if (attrs.tag) resultElements.push(tag); + if (attrs.lockMode && attrs.lockMode !== 'unlocked') resultElements.push(lock); if (attrs.sdtPr) { const elements = attrs.sdtPr.elements || []; - const elementsToExclude = ['w:id', 'w:alias', 'w:tag']; + const elementsToExclude = ['w:id', 'w:alias', 'w:tag', 'w:lock']; const restElements = elements.filter((el) => !elementsToExclude.includes(el.name)); const result = { name: 'w:sdtPr', diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/sdt/helpers/translate-structured-content.test.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/sdt/helpers/translate-structured-content.test.js index f2bb30ca9f..5edc18de1a 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/w/sdt/helpers/translate-structured-content.test.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/sdt/helpers/translate-structured-content.test.js @@ -95,4 +95,108 @@ describe('translateStructuredContent', () => { expect(translateChildNodes).toHaveBeenCalledWith({ ...params, node }); expect(result).toEqual(childElements[0]); }); + + describe('w:lock export', () => { + it('exports w:lock element for sdtLocked mode', () => { + const node = { + content: [{ type: 'text', text: 'Test' }], + attrs: { id: '123', lockMode: 'sdtLocked' }, + }; + const params = { node }; + + const result = translateStructuredContent(params); + + const sdtPr = result.elements.find((el) => el.name === 'w:sdtPr'); + const lockElement = sdtPr.elements.find((el) => el.name === 'w:lock'); + + expect(lockElement).toBeDefined(); + expect(lockElement.attributes['w:val']).toBe('sdtLocked'); + }); + + it('exports w:lock element for contentLocked mode', () => { + const node = { + content: [{ type: 'text', text: 'Test' }], + attrs: { id: '123', lockMode: 'contentLocked' }, + }; + const params = { node }; + + const result = translateStructuredContent(params); + + const sdtPr = result.elements.find((el) => el.name === 'w:sdtPr'); + const lockElement = sdtPr.elements.find((el) => el.name === 'w:lock'); + + expect(lockElement).toBeDefined(); + expect(lockElement.attributes['w:val']).toBe('contentLocked'); + }); + + it('exports w:lock element for sdtContentLocked mode', () => { + const node = { + content: [{ type: 'text', text: 'Test' }], + attrs: { id: '123', lockMode: 'sdtContentLocked' }, + }; + const params = { node }; + + const result = translateStructuredContent(params); + + const sdtPr = result.elements.find((el) => el.name === 'w:sdtPr'); + const lockElement = sdtPr.elements.find((el) => el.name === 'w:lock'); + + expect(lockElement).toBeDefined(); + expect(lockElement.attributes['w:val']).toBe('sdtContentLocked'); + }); + + it('does not export w:lock element for unlocked mode', () => { + const node = { + content: [{ type: 'text', text: 'Test' }], + attrs: { id: '123', lockMode: 'unlocked' }, + }; + const params = { node }; + + const result = translateStructuredContent(params); + + const sdtPr = result.elements.find((el) => el.name === 'w:sdtPr'); + const lockElement = sdtPr.elements.find((el) => el.name === 'w:lock'); + + expect(lockElement).toBeUndefined(); + }); + + it('does not export w:lock element when lockMode is not set', () => { + const node = { + content: [{ type: 'text', text: 'Test' }], + attrs: { id: '123' }, + }; + const params = { node }; + + const result = translateStructuredContent(params); + + const sdtPr = result.elements.find((el) => el.name === 'w:sdtPr'); + const lockElement = sdtPr.elements.find((el) => el.name === 'w:lock'); + + expect(lockElement).toBeUndefined(); + }); + + it('excludes w:lock from passthrough sdtPr elements to avoid duplication', () => { + const originalSdtPr = { + name: 'w:sdtPr', + elements: [ + { name: 'w:lock', attributes: { 'w:val': 'contentLocked' } }, + { name: 'w:placeholder', elements: [] }, + ], + }; + const node = { + content: [{ type: 'text', text: 'Test' }], + attrs: { id: '123', lockMode: 'sdtContentLocked', sdtPr: originalSdtPr }, + }; + const params = { node }; + + const result = translateStructuredContent(params); + + const sdtPr = result.elements.find((el) => el.name === 'w:sdtPr'); + const lockElements = sdtPr.elements.filter((el) => el.name === 'w:lock'); + + // Should only have one w:lock element with the new value + expect(lockElements.length).toBe(1); + expect(lockElements[0].attributes['w:val']).toBe('sdtContentLocked'); + }); + }); }); diff --git a/packages/super-editor/src/extensions/structured-content/StructuredContentBlockView.js b/packages/super-editor/src/extensions/structured-content/StructuredContentBlockView.js index 0af6e3f3ca..88fe2a2c3a 100644 --- a/packages/super-editor/src/extensions/structured-content/StructuredContentBlockView.js +++ b/packages/super-editor/src/extensions/structured-content/StructuredContentBlockView.js @@ -39,11 +39,15 @@ export class StructuredContentBlockView extends StructuredContentViewBase { element.prepend(dragHandle); element.addEventListener('dragstart', (e) => this.onDragStart(e)); this.root = element; + this.updateContentEditability(); + this.updateLockStateClasses(); } updateView() { const domAttrs = Attribute.mergeAttributes(this.htmlAttributes); updateDOMAttributes(this.dom, { ...domAttrs }); + this.updateContentEditability(); + this.updateLockStateClasses(); } update(node, decorations, innerDecorations) { diff --git a/packages/super-editor/src/extensions/structured-content/StructuredContentInlineView.js b/packages/super-editor/src/extensions/structured-content/StructuredContentInlineView.js index 1f647d5565..e9cadb85fc 100644 --- a/packages/super-editor/src/extensions/structured-content/StructuredContentInlineView.js +++ b/packages/super-editor/src/extensions/structured-content/StructuredContentInlineView.js @@ -39,11 +39,15 @@ export class StructuredContentInlineView extends StructuredContentViewBase { element.prepend(dragHandle); element.addEventListener('dragstart', (e) => this.onDragStart(e)); this.root = element; + this.updateContentEditability(); + this.updateLockStateClasses(); } updateView() { const domAttrs = Attribute.mergeAttributes(this.htmlAttributes); updateDOMAttributes(this.dom, { ...domAttrs }); + this.updateContentEditability(); + this.updateLockStateClasses(); } update(node, decorations, innerDecorations) { diff --git a/packages/super-editor/src/extensions/structured-content/StructuredContentViewBase.js b/packages/super-editor/src/extensions/structured-content/StructuredContentViewBase.js index cd7a0f1b6d..385da03e7c 100644 --- a/packages/super-editor/src/extensions/structured-content/StructuredContentViewBase.js +++ b/packages/super-editor/src/extensions/structured-content/StructuredContentViewBase.js @@ -189,6 +189,34 @@ export class StructuredContentViewBase { return dragHandle; } + isContentLocked() { + const lockMode = this.node.attrs.lockMode; + return lockMode === 'contentLocked' || lockMode === 'sdtContentLocked'; + } + + isSdtLocked() { + const lockMode = this.node.attrs.lockMode; + return lockMode === 'sdtLocked' || lockMode === 'sdtContentLocked'; + } + + updateContentEditability() { + if (this.contentDOM) { + this.contentDOM.setAttribute('contenteditable', this.isContentLocked() ? 'false' : 'true'); + } + } + + updateLockStateClasses() { + const lockMode = this.node.attrs.lockMode || 'unlocked'; + this.dom.classList.toggle( + 'sd-structured-content--content-locked', + lockMode === 'contentLocked' || lockMode === 'sdtContentLocked', + ); + this.dom.classList.toggle( + 'sd-structured-content--sdt-locked', + lockMode === 'sdtLocked' || lockMode === 'sdtContentLocked', + ); + } + onDragStart(event) { const { view } = this.editor; const target = event.target; diff --git a/packages/super-editor/src/extensions/structured-content/structured-content-block.js b/packages/super-editor/src/extensions/structured-content/structured-content-block.js index 18ef2755dc..42ee58cbfd 100644 --- a/packages/super-editor/src/extensions/structured-content/structured-content-block.js +++ b/packages/super-editor/src/extensions/structured-content/structured-content-block.js @@ -77,6 +77,15 @@ export const StructuredContentBlock = Node.create({ }, }, + lockMode: { + default: 'unlocked', + parseDOM: (elem) => elem.getAttribute('data-lock-mode') || 'unlocked', + renderDOM: (attrs) => { + if (!attrs.lockMode || attrs.lockMode === 'unlocked') return {}; + return { 'data-lock-mode': attrs.lockMode }; + }, + }, + sdtPr: { rendered: false, }, diff --git a/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js b/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js new file mode 100644 index 0000000000..6092440ee2 --- /dev/null +++ b/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js @@ -0,0 +1,36 @@ +import { Plugin, PluginKey } from 'prosemirror-state'; + +export const STRUCTURED_CONTENT_LOCK_KEY = new PluginKey('structuredContentLock'); + +export function createStructuredContentLockPlugin() { + return new Plugin({ + key: STRUCTURED_CONTENT_LOCK_KEY, + + filterTransaction(tr, state) { + if (!tr.docChanged) return true; + + // Find all SDT-locked nodes in old state + const lockedPositions = []; + state.doc.descendants((node, pos) => { + if ( + (node.type.name === 'structuredContent' || node.type.name === 'structuredContentBlock') && + (node.attrs.lockMode === 'sdtLocked' || node.attrs.lockMode === 'sdtContentLocked') + ) { + lockedPositions.push({ pos, end: pos + node.nodeSize }); + } + }); + + if (lockedPositions.length === 0) return true; + + // Check if any locked node would be deleted + for (const { pos, end } of lockedPositions) { + const mappedPos = tr.mapping.mapResult(pos); + const mappedEnd = tr.mapping.mapResult(end); + if (mappedPos.deleted || mappedEnd.deleted) { + return false; // Block transaction + } + } + return true; + }, + }); +} diff --git a/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.test.js b/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.test.js new file mode 100644 index 0000000000..44549e382a --- /dev/null +++ b/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.test.js @@ -0,0 +1,271 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { EditorState } from 'prosemirror-state'; +import { initTestEditor } from '@tests/helpers/helpers.js'; + +describe('StructuredContentLockPlugin', () => { + let editor; + let schema; + + beforeEach(() => { + ({ editor } = initTestEditor()); + ({ schema } = editor); + }); + + afterEach(() => { + editor?.destroy(); + editor = null; + schema = null; + }); + + const createDocWithStructuredContent = (lockMode, type = 'structuredContent') => { + const text = schema.text('Locked content'); + let node; + let doc; + + if (type === 'structuredContent') { + node = schema.nodes.structuredContent.create({ id: '123', lockMode }, text); + const paragraph = schema.nodes.paragraph.create(null, [node]); + doc = schema.nodes.doc.create(null, [paragraph]); + } else { + const innerParagraph = schema.nodes.paragraph.create(null, text); + node = schema.nodes.structuredContentBlock.create({ id: '123', lockMode }, [innerParagraph]); + doc = schema.nodes.doc.create(null, [node]); + } + + const nextState = EditorState.create({ schema, doc, plugins: editor.state.plugins }); + editor.setState(nextState); + return node; + }; + + describe('sdtLocked mode', () => { + it('prevents deletion of sdtLocked inline structured content', () => { + createDocWithStructuredContent('sdtLocked', 'structuredContent'); + + // Find the structured content node position + let nodePos = null; + let nodeSize = null; + editor.state.doc.descendants((node, pos) => { + if (node.type.name === 'structuredContent') { + nodePos = pos; + nodeSize = node.nodeSize; + return false; + } + }); + + expect(nodePos).not.toBeNull(); + + // Try to delete the node + const tr = editor.state.tr.delete(nodePos, nodePos + nodeSize); + const newState = editor.state.apply(tr); + + // The document should remain unchanged (deletion blocked) + expect(newState.doc.textContent).toBe('Locked content'); + }); + + it('prevents deletion of sdtLocked block structured content', () => { + createDocWithStructuredContent('sdtLocked', 'structuredContentBlock'); + + // Find the structured content block position + let nodePos = null; + let nodeSize = null; + editor.state.doc.descendants((node, pos) => { + if (node.type.name === 'structuredContentBlock') { + nodePos = pos; + nodeSize = node.nodeSize; + return false; + } + }); + + expect(nodePos).not.toBeNull(); + + // Try to delete the node + const tr = editor.state.tr.delete(nodePos, nodePos + nodeSize); + const newState = editor.state.apply(tr); + + // The document should remain unchanged (deletion blocked) + expect(newState.doc.textContent).toBe('Locked content'); + }); + }); + + describe('sdtContentLocked mode', () => { + it('prevents deletion of sdtContentLocked inline structured content', () => { + createDocWithStructuredContent('sdtContentLocked', 'structuredContent'); + + // Find the structured content node position + let nodePos = null; + let nodeSize = null; + editor.state.doc.descendants((node, pos) => { + if (node.type.name === 'structuredContent') { + nodePos = pos; + nodeSize = node.nodeSize; + return false; + } + }); + + expect(nodePos).not.toBeNull(); + + // Try to delete the node + const tr = editor.state.tr.delete(nodePos, nodePos + nodeSize); + const newState = editor.state.apply(tr); + + // The document should remain unchanged (deletion blocked) + expect(newState.doc.textContent).toBe('Locked content'); + }); + + it('prevents deletion of sdtContentLocked block structured content', () => { + createDocWithStructuredContent('sdtContentLocked', 'structuredContentBlock'); + + // Find the structured content block position + let nodePos = null; + let nodeSize = null; + editor.state.doc.descendants((node, pos) => { + if (node.type.name === 'structuredContentBlock') { + nodePos = pos; + nodeSize = node.nodeSize; + return false; + } + }); + + expect(nodePos).not.toBeNull(); + + // Try to delete the node + const tr = editor.state.tr.delete(nodePos, nodePos + nodeSize); + const newState = editor.state.apply(tr); + + // The document should remain unchanged (deletion blocked) + expect(newState.doc.textContent).toBe('Locked content'); + }); + }); + + describe('contentLocked mode', () => { + it('allows deletion of contentLocked inline structured content', () => { + createDocWithStructuredContent('contentLocked', 'structuredContent'); + + // Find the structured content node position + let nodePos = null; + let nodeSize = null; + editor.state.doc.descendants((node, pos) => { + if (node.type.name === 'structuredContent') { + nodePos = pos; + nodeSize = node.nodeSize; + return false; + } + }); + + expect(nodePos).not.toBeNull(); + + // Try to delete the node + const tr = editor.state.tr.delete(nodePos, nodePos + nodeSize); + const newState = editor.state.apply(tr); + + // The node should be deleted + let foundNode = false; + newState.doc.descendants((node) => { + if (node.type.name === 'structuredContent') { + foundNode = true; + return false; + } + }); + + expect(foundNode).toBe(false); + }); + + it('allows deletion of contentLocked block structured content', () => { + createDocWithStructuredContent('contentLocked', 'structuredContentBlock'); + + // Find the structured content block position + let nodePos = null; + let nodeSize = null; + editor.state.doc.descendants((node, pos) => { + if (node.type.name === 'structuredContentBlock') { + nodePos = pos; + nodeSize = node.nodeSize; + return false; + } + }); + + expect(nodePos).not.toBeNull(); + + // Try to delete the node + const tr = editor.state.tr.delete(nodePos, nodePos + nodeSize); + const newState = editor.state.apply(tr); + + // The node should be deleted + let foundNode = false; + newState.doc.descendants((node) => { + if (node.type.name === 'structuredContentBlock') { + foundNode = true; + return false; + } + }); + + expect(foundNode).toBe(false); + }); + }); + + describe('unlocked mode', () => { + it('allows deletion of unlocked inline structured content', () => { + createDocWithStructuredContent('unlocked', 'structuredContent'); + + // Find the structured content node position + let nodePos = null; + let nodeSize = null; + editor.state.doc.descendants((node, pos) => { + if (node.type.name === 'structuredContent') { + nodePos = pos; + nodeSize = node.nodeSize; + return false; + } + }); + + expect(nodePos).not.toBeNull(); + + // Try to delete the node + const tr = editor.state.tr.delete(nodePos, nodePos + nodeSize); + const newState = editor.state.apply(tr); + + // The node should be deleted + let foundNode = false; + newState.doc.descendants((node) => { + if (node.type.name === 'structuredContent') { + foundNode = true; + return false; + } + }); + + expect(foundNode).toBe(false); + }); + + it('allows deletion of unlocked block structured content', () => { + createDocWithStructuredContent('unlocked', 'structuredContentBlock'); + + // Find the structured content block position + let nodePos = null; + let nodeSize = null; + editor.state.doc.descendants((node, pos) => { + if (node.type.name === 'structuredContentBlock') { + nodePos = pos; + nodeSize = node.nodeSize; + return false; + } + }); + + expect(nodePos).not.toBeNull(); + + // Try to delete the node + const tr = editor.state.tr.delete(nodePos, nodePos + nodeSize); + const newState = editor.state.apply(tr); + + // The node should be deleted + let foundNode = false; + newState.doc.descendants((node) => { + if (node.type.name === 'structuredContentBlock') { + foundNode = true; + return false; + } + }); + + expect(foundNode).toBe(false); + }); + }); +}); diff --git a/packages/super-editor/src/extensions/structured-content/structured-content.js b/packages/super-editor/src/extensions/structured-content/structured-content.js index 355663a570..8a33cd3422 100644 --- a/packages/super-editor/src/extensions/structured-content/structured-content.js +++ b/packages/super-editor/src/extensions/structured-content/structured-content.js @@ -1,5 +1,6 @@ import { Node, Attribute } from '@core/index'; import { StructuredContentInlineView } from './StructuredContentInlineView'; +import { createStructuredContentLockPlugin } from './structured-content-lock-plugin'; export const structuredContentClass = 'sd-structured-content'; export const structuredContentInnerClass = 'sd-structured-content__content'; @@ -84,6 +85,15 @@ export const StructuredContent = Node.create({ }, }, + lockMode: { + default: 'unlocked', + parseDOM: (elem) => elem.getAttribute('data-lock-mode') || 'unlocked', + renderDOM: (attrs) => { + if (!attrs.lockMode || attrs.lockMode === 'unlocked') return {}; + return { 'data-lock-mode': attrs.lockMode }; + }, + }, + sdtPr: { rendered: false, }, @@ -104,6 +114,10 @@ export const StructuredContent = Node.create({ ]; }, + addPmPlugins() { + return [createStructuredContentLockPlugin()]; + }, + addNodeView() { return (props) => { return new StructuredContentInlineView({ ...props }); diff --git a/packages/super-editor/src/extensions/types/node-attributes.ts b/packages/super-editor/src/extensions/types/node-attributes.ts index dbb95aa8fc..eaa77e292a 100644 --- a/packages/super-editor/src/extensions/types/node-attributes.ts +++ b/packages/super-editor/src/extensions/types/node-attributes.ts @@ -593,6 +593,8 @@ export interface HardBreakAttrs extends InlineNodeAttributes { // STRUCTURED CONTENT // ============================================ +export type StructuredContentLockMode = 'unlocked' | 'sdtLocked' | 'contentLocked' | 'sdtContentLocked'; + /** Structured content node attributes */ export interface StructuredContentAttrs extends BlockNodeAttributes { /** Unique identifier */ @@ -607,6 +609,8 @@ export interface StructuredContentAttrs extends BlockNodeAttributes { description?: string; /** Whether the content is locked */ isLocked?: boolean; + /** Lock mode */ + lockMode?: StructuredContentLockMode; } // ============================================ From d04f6c6ae1458ab97b2f56a36da9a81846ecab0a Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Wed, 4 Feb 2026 13:51:22 -0300 Subject: [PATCH 02/18] perf(super-editor): optimize lock plugin to check only changed ranges Replace state.doc.descendants() with nodesBetween() to avoid iterating the entire document on every transaction. Now only checks nodes within the affected ranges. Also simplify normalizeLockMode in style-engine since lockMode values are already validated at import time. --- .../layout-engine/style-engine/src/index.ts | 26 +------ .../structured-content-lock-plugin.js | 72 +++++++++++++------ 2 files changed, 53 insertions(+), 45 deletions(-) diff --git a/packages/layout-engine/style-engine/src/index.ts b/packages/layout-engine/style-engine/src/index.ts index d7a73d07ec..419d2424ba 100644 --- a/packages/layout-engine/style-engine/src/index.ts +++ b/packages/layout-engine/style-engine/src/index.ts @@ -247,35 +247,11 @@ function normalizeStructuredContentMetadata( id: toNullableString(attrs.id), tag: toOptionalString(attrs.tag), alias: toOptionalString(attrs.alias), - lockMode: normalizeLockMode(attrs.lockMode), + lockMode: attrs.lockMode as StructuredContentMetadata['lockMode'], sdtPr: attrs.sdtPr, }; } -function normalizeLockMode(value: unknown): StructuredContentMetadata['lockMode'] { - if (typeof value !== 'string') return undefined; - const normalized = value.toLowerCase(); - if ( - normalized === 'unlocked' || - normalized === 'sdtlocked' || - normalized === 'contentlocked' || - normalized === 'sdtcontentlocked' - ) { - // Normalize to proper camelCase format - switch (normalized) { - case 'sdtlocked': - return 'sdtLocked'; - case 'contentlocked': - return 'contentLocked'; - case 'sdtcontentlocked': - return 'sdtContentLocked'; - default: - return 'unlocked'; - } - } - return undefined; -} - function normalizeDocumentSectionMetadata(attrs: Record): DocumentSectionMetadata { return { type: 'documentSection', diff --git a/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js b/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js index 6092440ee2..d69ac9e30a 100644 --- a/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js +++ b/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js @@ -2,6 +2,37 @@ import { Plugin, PluginKey } from 'prosemirror-state'; export const STRUCTURED_CONTENT_LOCK_KEY = new PluginKey('structuredContentLock'); +/** + * Collects the ranges affected by a transaction, based on the document BEFORE the change. + * @param {import('prosemirror-state').Transaction} tr + * @returns {Array<{ from: number, to: number }>} + */ +const collectChangedRanges = (tr) => { + const ranges = []; + tr.mapping.maps.forEach((map) => { + map.forEach((oldStart, oldEnd) => { + const from = Math.min(oldStart, oldEnd); + const to = Math.max(oldStart, oldEnd); + if (from !== to) { + ranges.push({ from, to }); + } + }); + }); + return ranges; +}; + +/** + * Checks if a node is a locked SDT (sdtLocked or sdtContentLocked). + * @param {import('prosemirror-model').Node} node + * @returns {boolean} + */ +const isLockedSdt = (node) => { + return ( + (node.type.name === 'structuredContent' || node.type.name === 'structuredContentBlock') && + (node.attrs.lockMode === 'sdtLocked' || node.attrs.lockMode === 'sdtContentLocked') + ); +}; + export function createStructuredContentLockPlugin() { return new Plugin({ key: STRUCTURED_CONTENT_LOCK_KEY, @@ -9,27 +40,28 @@ export function createStructuredContentLockPlugin() { filterTransaction(tr, state) { if (!tr.docChanged) return true; - // Find all SDT-locked nodes in old state - const lockedPositions = []; - state.doc.descendants((node, pos) => { - if ( - (node.type.name === 'structuredContent' || node.type.name === 'structuredContentBlock') && - (node.attrs.lockMode === 'sdtLocked' || node.attrs.lockMode === 'sdtContentLocked') - ) { - lockedPositions.push({ pos, end: pos + node.nodeSize }); - } - }); - - if (lockedPositions.length === 0) return true; - - // Check if any locked node would be deleted - for (const { pos, end } of lockedPositions) { - const mappedPos = tr.mapping.mapResult(pos); - const mappedEnd = tr.mapping.mapResult(end); - if (mappedPos.deleted || mappedEnd.deleted) { - return false; // Block transaction - } + // Get only the ranges affected by this transaction + const changedRanges = collectChangedRanges(tr); + if (changedRanges.length === 0) return true; + + // Check only nodes within the changed ranges for locked SDTs + for (const { from, to } of changedRanges) { + // Use nodesBetween to only traverse affected range + let hasLockedNode = false; + state.doc.nodesBetween(from, to, (node, pos) => { + if (isLockedSdt(node)) { + // Check if this locked node would be deleted + const mappedPos = tr.mapping.mapResult(pos); + const mappedEnd = tr.mapping.mapResult(pos + node.nodeSize); + if (mappedPos.deleted || mappedEnd.deleted) { + hasLockedNode = true; + return false; // Stop traversal + } + } + }); + if (hasLockedNode) return false; // Block transaction } + return true; }, }); From 786e5b779a6b7b40f063c1823d7924fa4de02662 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Wed, 4 Feb 2026 13:54:55 -0300 Subject: [PATCH 03/18] fix(super-editor): clamp nodesBetween range to valid document bounds --- .../structured-content/structured-content-lock-plugin.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js b/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js index d69ac9e30a..1c91b14ccb 100644 --- a/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js +++ b/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js @@ -44,11 +44,18 @@ export function createStructuredContentLockPlugin() { const changedRanges = collectChangedRanges(tr); if (changedRanges.length === 0) return true; + const docSize = state.doc.content.size; + // Check only nodes within the changed ranges for locked SDTs for (const { from, to } of changedRanges) { + // Clamp range to valid document bounds + const safeFrom = Math.max(0, Math.min(from, docSize)); + const safeTo = Math.max(0, Math.min(to, docSize)); + if (safeFrom >= safeTo) continue; + // Use nodesBetween to only traverse affected range let hasLockedNode = false; - state.doc.nodesBetween(from, to, (node, pos) => { + state.doc.nodesBetween(safeFrom, safeTo, (node, pos) => { if (isLockedSdt(node)) { // Check if this locked node would be deleted const mappedPos = tr.mapping.mapResult(pos); From 9137b9c5d305925e3b53355c5779e4b744a62e03 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Wed, 4 Feb 2026 14:15:06 -0300 Subject: [PATCH 04/18] refactor(super-editor): remove unused lock state methods and CSS class toggling Remove isSdtLocked() method that was never called - SDT deletion prevention is handled by the lock plugin instead. Remove updateLockStateClasses() and its calls - the CSS classes it toggled had no corresponding CSS rules. Presentation mode uses data-lock-mode attributes with CSS in styles.ts instead. --- .../StructuredContentBlockView.js | 2 -- .../StructuredContentInlineView.js | 2 -- .../StructuredContentViewBase.js | 17 ----------------- 3 files changed, 21 deletions(-) diff --git a/packages/super-editor/src/extensions/structured-content/StructuredContentBlockView.js b/packages/super-editor/src/extensions/structured-content/StructuredContentBlockView.js index 88fe2a2c3a..39adb2f18a 100644 --- a/packages/super-editor/src/extensions/structured-content/StructuredContentBlockView.js +++ b/packages/super-editor/src/extensions/structured-content/StructuredContentBlockView.js @@ -40,14 +40,12 @@ export class StructuredContentBlockView extends StructuredContentViewBase { element.addEventListener('dragstart', (e) => this.onDragStart(e)); this.root = element; this.updateContentEditability(); - this.updateLockStateClasses(); } updateView() { const domAttrs = Attribute.mergeAttributes(this.htmlAttributes); updateDOMAttributes(this.dom, { ...domAttrs }); this.updateContentEditability(); - this.updateLockStateClasses(); } update(node, decorations, innerDecorations) { diff --git a/packages/super-editor/src/extensions/structured-content/StructuredContentInlineView.js b/packages/super-editor/src/extensions/structured-content/StructuredContentInlineView.js index e9cadb85fc..d58e4a7b3b 100644 --- a/packages/super-editor/src/extensions/structured-content/StructuredContentInlineView.js +++ b/packages/super-editor/src/extensions/structured-content/StructuredContentInlineView.js @@ -40,14 +40,12 @@ export class StructuredContentInlineView extends StructuredContentViewBase { element.addEventListener('dragstart', (e) => this.onDragStart(e)); this.root = element; this.updateContentEditability(); - this.updateLockStateClasses(); } updateView() { const domAttrs = Attribute.mergeAttributes(this.htmlAttributes); updateDOMAttributes(this.dom, { ...domAttrs }); this.updateContentEditability(); - this.updateLockStateClasses(); } update(node, decorations, innerDecorations) { diff --git a/packages/super-editor/src/extensions/structured-content/StructuredContentViewBase.js b/packages/super-editor/src/extensions/structured-content/StructuredContentViewBase.js index 385da03e7c..b67a517764 100644 --- a/packages/super-editor/src/extensions/structured-content/StructuredContentViewBase.js +++ b/packages/super-editor/src/extensions/structured-content/StructuredContentViewBase.js @@ -194,29 +194,12 @@ export class StructuredContentViewBase { return lockMode === 'contentLocked' || lockMode === 'sdtContentLocked'; } - isSdtLocked() { - const lockMode = this.node.attrs.lockMode; - return lockMode === 'sdtLocked' || lockMode === 'sdtContentLocked'; - } - updateContentEditability() { if (this.contentDOM) { this.contentDOM.setAttribute('contenteditable', this.isContentLocked() ? 'false' : 'true'); } } - updateLockStateClasses() { - const lockMode = this.node.attrs.lockMode || 'unlocked'; - this.dom.classList.toggle( - 'sd-structured-content--content-locked', - lockMode === 'contentLocked' || lockMode === 'sdtContentLocked', - ); - this.dom.classList.toggle( - 'sd-structured-content--sdt-locked', - lockMode === 'sdtLocked' || lockMode === 'sdtContentLocked', - ); - } - onDragStart(event) { const { view } = this.editor; const target = event.target; From fb4a38c0cd5de15a79148e02d556e0dcda0dba2d Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Wed, 4 Feb 2026 15:18:47 -0300 Subject: [PATCH 05/18] fix(super-editor): allow cursor movement in locked SDT content Change lock enforcement strategy to use plugin-only defense instead of contentEditable='false'. This allows users to: - Move cursor within locked content nodes - Select text for copying - Navigate smoothly through the document The lock plugin now handles all edit blocking through: - handleKeyDown: Block Delete/Backspace/Cut before transaction - handleTextInput: Block typing in content-locked nodes - filterTransaction: Safety net for paste, drag-drop, programmatic changes NodeView now only adds CSS classes for visual feedback without disabling cursor interaction. Also adds comprehensive test suite with 35 tests covering all lock modes and adds research documentation in .tupizz/docs/. --- .../StructuredContentViewBase.js | 15 +- .../structured-content-lock-plugin.js | 200 +++++-- .../structured-content-lock-plugin.test.js | 547 +++++++++++------- 3 files changed, 506 insertions(+), 256 deletions(-) diff --git a/packages/super-editor/src/extensions/structured-content/StructuredContentViewBase.js b/packages/super-editor/src/extensions/structured-content/StructuredContentViewBase.js index b67a517764..708b933daf 100644 --- a/packages/super-editor/src/extensions/structured-content/StructuredContentViewBase.js +++ b/packages/super-editor/src/extensions/structured-content/StructuredContentViewBase.js @@ -194,9 +194,20 @@ export class StructuredContentViewBase { return lockMode === 'contentLocked' || lockMode === 'sdtContentLocked'; } + isSdtLocked() { + const lockMode = this.node.attrs.lockMode; + return lockMode === 'sdtLocked' || lockMode === 'sdtContentLocked'; + } + updateContentEditability() { - if (this.contentDOM) { - this.contentDOM.setAttribute('contenteditable', this.isContentLocked() ? 'false' : 'true'); + // Note: We intentionally do NOT set contentEditable='false' for locked content. + // This allows cursor movement and selection within locked nodes. + // The lock plugin (structured-content-lock-plugin.js) handles blocking actual edits + // via handleKeyDown, handleTextInput, and filterTransaction. + // We only add CSS classes for visual feedback. + if (this.dom) { + this.dom.classList.toggle('sd-structured-content--content-locked', this.isContentLocked()); + this.dom.classList.toggle('sd-structured-content--sdt-locked', this.isSdtLocked()); } } diff --git a/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js b/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js index 1c91b14ccb..e3c900307f 100644 --- a/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js +++ b/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js @@ -3,70 +3,164 @@ import { Plugin, PluginKey } from 'prosemirror-state'; export const STRUCTURED_CONTENT_LOCK_KEY = new PluginKey('structuredContentLock'); /** - * Collects the ranges affected by a transaction, based on the document BEFORE the change. - * @param {import('prosemirror-state').Transaction} tr - * @returns {Array<{ from: number, to: number }>} + * Lock enforcement plugin for StructuredContent nodes. + * + * Lock modes (ECMA-376 w:lock): + * - unlocked: No restrictions + * - sdtLocked: Cannot delete the SDT wrapper (content editable) + * - contentLocked: Cannot edit content (can delete wrapper) + * - sdtContentLocked: Cannot delete wrapper OR edit content + * + * Strategy: + * 1. handleKeyDown - Intercept keys BEFORE transaction to prevent browser selection issues + * 2. filterTransaction - Safety net to catch programmatic changes */ -const collectChangedRanges = (tr) => { - const ranges = []; - tr.mapping.maps.forEach((map) => { - map.forEach((oldStart, oldEnd) => { - const from = Math.min(oldStart, oldEnd); - const to = Math.max(oldStart, oldEnd); - if (from !== to) { - ranges.push({ from, to }); - } - }); + +/** + * Collect all SDT nodes from the document + */ +function collectSDTNodes(doc) { + const sdtNodes = []; + doc.descendants((node, pos) => { + if (node.type.name === 'structuredContent' || node.type.name === 'structuredContentBlock') { + sdtNodes.push({ + type: node.type.name, + lockMode: node.attrs.lockMode, + pos, + end: pos + node.nodeSize, + }); + } }); - return ranges; -}; + return sdtNodes; +} /** - * Checks if a node is a locked SDT (sdtLocked or sdtContentLocked). - * @param {import('prosemirror-model').Node} node - * @returns {boolean} + * Check if a range [from, to] would violate any lock rules + * Returns { blocked: boolean, reason?: string } */ -const isLockedSdt = (node) => { - return ( - (node.type.name === 'structuredContent' || node.type.name === 'structuredContentBlock') && - (node.attrs.lockMode === 'sdtLocked' || node.attrs.lockMode === 'sdtContentLocked') - ); -}; +function checkLockViolation(sdtNodes, from, to) { + for (const sdt of sdtNodes) { + const overlaps = from < sdt.end && to > sdt.pos; + if (!overlaps) continue; + + // Calculate relationship + const containsSDT = from <= sdt.pos && to >= sdt.end; + const insideSDT = from >= sdt.pos && to <= sdt.end; + const crossesStart = from < sdt.pos && to > sdt.pos && to < sdt.end; + const crossesEnd = from > sdt.pos && from < sdt.end && to > sdt.end; + + const wouldDamageWrapper = containsSDT || crossesStart || crossesEnd; + // Content modification: inside SDT but NOT deleting the entire wrapper + const wouldModifyContent = insideSDT && !containsSDT; + + const isSdtLocked = sdt.lockMode === 'sdtLocked' || sdt.lockMode === 'sdtContentLocked'; + const isContentLocked = sdt.lockMode === 'contentLocked' || sdt.lockMode === 'sdtContentLocked'; + + if (isSdtLocked && wouldDamageWrapper) { + return { blocked: true, reason: `Cannot delete SDT wrapper (${sdt.lockMode})` }; + } + + if (isContentLocked && wouldModifyContent) { + return { blocked: true, reason: `Cannot modify content (${sdt.lockMode})` }; + } + } + return { blocked: false }; +} export function createStructuredContentLockPlugin() { return new Plugin({ key: STRUCTURED_CONTENT_LOCK_KEY, - filterTransaction(tr, state) { - if (!tr.docChanged) return true; - - // Get only the ranges affected by this transaction - const changedRanges = collectChangedRanges(tr); - if (changedRanges.length === 0) return true; - - const docSize = state.doc.content.size; - - // Check only nodes within the changed ranges for locked SDTs - for (const { from, to } of changedRanges) { - // Clamp range to valid document bounds - const safeFrom = Math.max(0, Math.min(from, docSize)); - const safeTo = Math.max(0, Math.min(to, docSize)); - if (safeFrom >= safeTo) continue; - - // Use nodesBetween to only traverse affected range - let hasLockedNode = false; - state.doc.nodesBetween(safeFrom, safeTo, (node, pos) => { - if (isLockedSdt(node)) { - // Check if this locked node would be deleted - const mappedPos = tr.mapping.mapResult(pos); - const mappedEnd = tr.mapping.mapResult(pos + node.nodeSize); - if (mappedPos.deleted || mappedEnd.deleted) { - hasLockedNode = true; - return false; // Stop traversal - } + props: { + /** + * Intercept key events BEFORE any transaction is created. + * This prevents the browser selection from getting out of sync. + */ + handleKeyDown(view, event) { + const { state } = view; + const { selection } = state; + const { from, to } = selection; + + // Only intercept destructive keys + const isDelete = event.key === 'Delete'; + const isBackspace = event.key === 'Backspace'; + const isCut = (event.metaKey || event.ctrlKey) && event.key === 'x'; + + if (!isDelete && !isBackspace && !isCut) { + return false; // Let other handlers process + } + + const sdtNodes = collectSDTNodes(state.doc); + if (sdtNodes.length === 0) { + return false; + } + + // Calculate the range that would be affected + let affectedFrom = from; + let affectedTo = to; + + // If selection is collapsed, backspace/delete affects adjacent position + if (from === to) { + if (isBackspace && from > 0) { + affectedFrom = from - 1; + } else if (isDelete && to < state.doc.content.size) { + affectedTo = to + 1; } - }); - if (hasLockedNode) return false; // Block transaction + } + + const result = checkLockViolation(sdtNodes, affectedFrom, affectedTo); + + if (result.blocked) { + event.preventDefault(); + return true; // Stop event propagation + } + + return false; + }, + + /** + * Handle text input (typing) for content-locked nodes + */ + handleTextInput(view, from, to, _text) { + const sdtNodes = collectSDTNodes(view.state.doc); + if (sdtNodes.length === 0) { + return false; + } + + const result = checkLockViolation(sdtNodes, from, to); + + if (result.blocked) { + return true; // Prevent the input + } + + return false; + }, + }, + + /** + * Safety net: filter transactions that slip through + * (e.g., programmatic changes, paste, drag-drop) + */ + filterTransaction(tr, state) { + if (!tr.docChanged) { + return true; + } + + const sdtNodes = collectSDTNodes(state.doc); + if (sdtNodes.length === 0) { + return true; + } + + for (const step of tr.steps) { + if (step.from === undefined || step.to === undefined) { + continue; + } + + const result = checkLockViolation(sdtNodes, step.from, step.to); + + if (result.blocked) { + return false; + } } return true; diff --git a/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.test.js b/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.test.js index 44549e382a..a6752efc33 100644 --- a/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.test.js +++ b/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.test.js @@ -1,7 +1,34 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { EditorState } from 'prosemirror-state'; +import { EditorState, TextSelection } from 'prosemirror-state'; import { initTestEditor } from '@tests/helpers/helpers.js'; +/** + * Test suite for StructuredContentLockPlugin + * + * Tests ECMA-376 w:lock behavior for StructuredContent nodes: + * - unlocked: No restrictions (can delete wrapper, can edit content) + * - sdtLocked: Cannot delete wrapper, CAN edit content + * - contentLocked: CAN delete wrapper, cannot edit content + * - sdtContentLocked: Cannot delete wrapper, cannot edit content + */ + +// Helper to find SDT node position in document +function findSDTNode(doc, nodeType = 'structuredContent') { + let result = null; + doc.descendants((node, pos) => { + if (node.type.name === nodeType) { + result = { node, pos, end: pos + node.nodeSize }; + return false; + } + }); + return result; +} + +// Helper to check if SDT node exists in document +function sdtNodeExists(doc, nodeType = 'structuredContent') { + return findSDTNode(doc, nodeType) !== null; +} + describe('StructuredContentLockPlugin', () => { let editor; let schema; @@ -17,255 +44,373 @@ describe('StructuredContentLockPlugin', () => { schema = null; }); - const createDocWithStructuredContent = (lockMode, type = 'structuredContent') => { - const text = schema.text('Locked content'); - let node; - let doc; - - if (type === 'structuredContent') { - node = schema.nodes.structuredContent.create({ id: '123', lockMode }, text); - const paragraph = schema.nodes.paragraph.create(null, [node]); - doc = schema.nodes.doc.create(null, [paragraph]); - } else { - const innerParagraph = schema.nodes.paragraph.create(null, text); - node = schema.nodes.structuredContentBlock.create({ id: '123', lockMode }, [innerParagraph]); - doc = schema.nodes.doc.create(null, [node]); - } - - const nextState = EditorState.create({ schema, doc, plugins: editor.state.plugins }); - editor.setState(nextState); - return node; - }; - - describe('sdtLocked mode', () => { - it('prevents deletion of sdtLocked inline structured content', () => { - createDocWithStructuredContent('sdtLocked', 'structuredContent'); - - // Find the structured content node position - let nodePos = null; - let nodeSize = null; - editor.state.doc.descendants((node, pos) => { - if (node.type.name === 'structuredContent') { - nodePos = pos; - nodeSize = node.nodeSize; - return false; - } - }); + // Factory to create document with SDT node + function createDocWithSDT(lockMode, nodeType = 'structuredContent') { + const text = schema.text('Test content'); - expect(nodePos).not.toBeNull(); + if (nodeType === 'structuredContent') { + const sdt = schema.nodes.structuredContent.create({ id: 'test-123', lockMode }, text); + const paragraph = schema.nodes.paragraph.create(null, [sdt]); + return schema.nodes.doc.create(null, [paragraph]); + } - // Try to delete the node - const tr = editor.state.tr.delete(nodePos, nodePos + nodeSize); - const newState = editor.state.apply(tr); + const innerParagraph = schema.nodes.paragraph.create(null, text); + const sdt = schema.nodes.structuredContentBlock.create({ id: 'test-123', lockMode }, [innerParagraph]); + return schema.nodes.doc.create(null, [sdt]); + } + + // Factory to create doc with text before and after SDT (for boundary tests) + function createDocWithSDTAndSurroundingText(lockMode, nodeType = 'structuredContent') { + const beforeText = schema.text('Before '); + const sdtText = schema.text('SDT content'); + const afterText = schema.text(' After'); + + if (nodeType === 'structuredContent') { + const sdt = schema.nodes.structuredContent.create({ id: 'test-123', lockMode }, sdtText); + const paragraph = schema.nodes.paragraph.create(null, [beforeText, sdt, afterText]); + return schema.nodes.doc.create(null, [paragraph]); + } - // The document should remain unchanged (deletion blocked) - expect(newState.doc.textContent).toBe('Locked content'); + const beforePara = schema.nodes.paragraph.create(null, beforeText); + const innerPara = schema.nodes.paragraph.create(null, sdtText); + const sdt = schema.nodes.structuredContentBlock.create({ id: 'test-123', lockMode }, [innerPara]); + const afterPara = schema.nodes.paragraph.create(null, afterText); + return schema.nodes.doc.create(null, [beforePara, sdt, afterPara]); + } + + // Apply document to editor and return state + function applyDocToEditor(doc) { + const state = EditorState.create({ schema, doc, plugins: editor.state.plugins }); + editor.setState(state); + return state; + } + + describe('wrapper deletion (sdtLocked behavior)', () => { + const wrapperDeletionCases = [ + // [lockMode, nodeType, shouldBlock, description] + ['unlocked', 'structuredContent', false, 'allows deletion of unlocked inline SDT'], + ['unlocked', 'structuredContentBlock', false, 'allows deletion of unlocked block SDT'], + ['sdtLocked', 'structuredContent', true, 'blocks deletion of sdtLocked inline SDT'], + ['sdtLocked', 'structuredContentBlock', true, 'blocks deletion of sdtLocked block SDT'], + ['contentLocked', 'structuredContent', false, 'allows deletion of contentLocked inline SDT'], + ['contentLocked', 'structuredContentBlock', false, 'allows deletion of contentLocked block SDT'], + ['sdtContentLocked', 'structuredContent', true, 'blocks deletion of sdtContentLocked inline SDT'], + ['sdtContentLocked', 'structuredContentBlock', true, 'blocks deletion of sdtContentLocked block SDT'], + ]; + + it.each(wrapperDeletionCases)('%s %s: %s', (lockMode, nodeType, shouldBlock) => { + // Arrange + const doc = createDocWithSDT(lockMode, nodeType); + const state = applyDocToEditor(doc); + const sdtInfo = findSDTNode(state.doc, nodeType); + expect(sdtInfo).not.toBeNull(); + + // Act: attempt to delete the entire SDT node + const tr = state.tr.delete(sdtInfo.pos, sdtInfo.end); + const newState = state.apply(tr); + + // Assert + const sdtStillExists = sdtNodeExists(newState.doc, nodeType); + expect(sdtStillExists).toBe(shouldBlock); }); + }); - it('prevents deletion of sdtLocked block structured content', () => { - createDocWithStructuredContent('sdtLocked', 'structuredContentBlock'); - - // Find the structured content block position - let nodePos = null; - let nodeSize = null; - editor.state.doc.descendants((node, pos) => { - if (node.type.name === 'structuredContentBlock') { - nodePos = pos; - nodeSize = node.nodeSize; - return false; - } - }); - - expect(nodePos).not.toBeNull(); - - // Try to delete the node - const tr = editor.state.tr.delete(nodePos, nodePos + nodeSize); - const newState = editor.state.apply(tr); + describe('content modification (contentLocked behavior)', () => { + const contentModificationCases = [ + // [lockMode, nodeType, shouldBlock, description] + ['unlocked', 'structuredContent', false, 'allows content modification in unlocked inline SDT'], + ['unlocked', 'structuredContentBlock', false, 'allows content modification in unlocked block SDT'], + ['sdtLocked', 'structuredContent', false, 'allows content modification in sdtLocked inline SDT'], + ['sdtLocked', 'structuredContentBlock', false, 'allows content modification in sdtLocked block SDT'], + ['contentLocked', 'structuredContent', true, 'blocks content modification in contentLocked inline SDT'], + ['contentLocked', 'structuredContentBlock', true, 'blocks content modification in contentLocked block SDT'], + ['sdtContentLocked', 'structuredContent', true, 'blocks content modification in sdtContentLocked inline SDT'], + ['sdtContentLocked', 'structuredContentBlock', true, 'blocks content modification in sdtContentLocked block SDT'], + ]; + + it.each(contentModificationCases)('%s %s: %s', (lockMode, nodeType, shouldBlock) => { + // Arrange + const doc = createDocWithSDT(lockMode, nodeType); + const state = applyDocToEditor(doc); + const sdtInfo = findSDTNode(state.doc, nodeType); + expect(sdtInfo).not.toBeNull(); + + // Calculate position inside the SDT content + const contentStart = sdtInfo.pos + 1; // +1 to enter the node + const contentEnd = sdtInfo.end - 1; // -1 to stay inside + + // Act: attempt to delete content inside SDT + const tr = state.tr.delete(contentStart, contentEnd); + const newState = state.apply(tr); + + // Assert: check if content was modified + const originalContent = state.doc.textContent; + const newContent = newState.doc.textContent; + const contentWasModified = originalContent !== newContent; + + expect(contentWasModified).toBe(!shouldBlock); + }); + }); - // The document should remain unchanged (deletion blocked) - expect(newState.doc.textContent).toBe('Locked content'); + describe('boundary crossing (protects SDT structure)', () => { + const boundaryCrossingCases = [ + // [lockMode, crossType, shouldBlock, description] + ['sdtLocked', 'crossesStart', true, 'blocks deletion that crosses into sdtLocked SDT from before'], + ['sdtLocked', 'crossesEnd', true, 'blocks deletion that crosses out of sdtLocked SDT'], + ['sdtContentLocked', 'crossesStart', true, 'blocks deletion that crosses into sdtContentLocked SDT from before'], + ['sdtContentLocked', 'crossesEnd', true, 'blocks deletion that crosses out of sdtContentLocked SDT'], + [ + 'contentLocked', + 'crossesStart', + false, + 'allows deletion that crosses into contentLocked SDT (wrapper deletable)', + ], + [ + 'contentLocked', + 'crossesEnd', + false, + 'allows deletion that crosses out of contentLocked SDT (wrapper deletable)', + ], + ['unlocked', 'crossesStart', false, 'allows deletion that crosses into unlocked SDT'], + ['unlocked', 'crossesEnd', false, 'allows deletion that crosses out of unlocked SDT'], + ]; + + it.each(boundaryCrossingCases)('%s %s: %s', (lockMode, crossType, shouldBlock) => { + // Arrange + const doc = createDocWithSDTAndSurroundingText(lockMode, 'structuredContent'); + const state = applyDocToEditor(doc); + const sdtInfo = findSDTNode(state.doc, 'structuredContent'); + expect(sdtInfo).not.toBeNull(); + + // Act: create deletion that crosses SDT boundary + let deleteFrom, deleteTo; + if (crossType === 'crossesStart') { + // Delete from before SDT into SDT content + deleteFrom = Math.max(0, sdtInfo.pos - 3); + deleteTo = sdtInfo.pos + 3; + } else { + // Delete from inside SDT to after SDT + deleteFrom = sdtInfo.end - 3; + deleteTo = Math.min(state.doc.content.size, sdtInfo.end + 3); + } + + const tr = state.tr.delete(deleteFrom, deleteTo); + const newState = state.apply(tr); + + // Assert: check if SDT still exists (boundary crossing damages wrapper) + const sdtStillIntact = sdtNodeExists(newState.doc, 'structuredContent'); + const contentUnchanged = state.doc.textContent === newState.doc.textContent; + + if (shouldBlock) { + // Transaction should be blocked - document unchanged + expect(contentUnchanged).toBe(true); + } else { + // Transaction should proceed - something changed + expect(contentUnchanged).toBe(false); + } }); }); - describe('sdtContentLocked mode', () => { - it('prevents deletion of sdtContentLocked inline structured content', () => { - createDocWithStructuredContent('sdtContentLocked', 'structuredContent'); + describe('insertion operations', () => { + it('allows text insertion in unlocked SDT', () => { + // Arrange + const doc = createDocWithSDT('unlocked', 'structuredContent'); + const state = applyDocToEditor(doc); + const sdtInfo = findSDTNode(state.doc, 'structuredContent'); + const insertPos = sdtInfo.pos + 2; - // Find the structured content node position - let nodePos = null; - let nodeSize = null; - editor.state.doc.descendants((node, pos) => { - if (node.type.name === 'structuredContent') { - nodePos = pos; - nodeSize = node.nodeSize; - return false; - } - }); + // Act + const tr = state.tr.insertText('NEW', insertPos); + const newState = state.apply(tr); - expect(nodePos).not.toBeNull(); + // Assert + expect(newState.doc.textContent).toContain('NEW'); + }); + + it('allows text insertion in sdtLocked SDT (content is editable)', () => { + // Arrange + const doc = createDocWithSDT('sdtLocked', 'structuredContent'); + const state = applyDocToEditor(doc); + const sdtInfo = findSDTNode(state.doc, 'structuredContent'); + const insertPos = sdtInfo.pos + 2; - // Try to delete the node - const tr = editor.state.tr.delete(nodePos, nodePos + nodeSize); - const newState = editor.state.apply(tr); + // Act + const tr = state.tr.insertText('NEW', insertPos); + const newState = state.apply(tr); - // The document should remain unchanged (deletion blocked) - expect(newState.doc.textContent).toBe('Locked content'); + // Assert + expect(newState.doc.textContent).toContain('NEW'); }); - it('prevents deletion of sdtContentLocked block structured content', () => { - createDocWithStructuredContent('sdtContentLocked', 'structuredContentBlock'); + it('blocks text insertion in contentLocked SDT', () => { + // Arrange + const doc = createDocWithSDT('contentLocked', 'structuredContent'); + const state = applyDocToEditor(doc); + const sdtInfo = findSDTNode(state.doc, 'structuredContent'); + const insertPos = sdtInfo.pos + 2; + const originalContent = state.doc.textContent; - // Find the structured content block position - let nodePos = null; - let nodeSize = null; - editor.state.doc.descendants((node, pos) => { - if (node.type.name === 'structuredContentBlock') { - nodePos = pos; - nodeSize = node.nodeSize; - return false; - } - }); + // Act + const tr = state.tr.insertText('NEW', insertPos); + const newState = state.apply(tr); + + // Assert: content should be unchanged + expect(newState.doc.textContent).toBe(originalContent); + }); - expect(nodePos).not.toBeNull(); + it('blocks text insertion in sdtContentLocked SDT', () => { + // Arrange + const doc = createDocWithSDT('sdtContentLocked', 'structuredContent'); + const state = applyDocToEditor(doc); + const sdtInfo = findSDTNode(state.doc, 'structuredContent'); + const insertPos = sdtInfo.pos + 2; + const originalContent = state.doc.textContent; - // Try to delete the node - const tr = editor.state.tr.delete(nodePos, nodePos + nodeSize); - const newState = editor.state.apply(tr); + // Act + const tr = state.tr.insertText('NEW', insertPos); + const newState = state.apply(tr); - // The document should remain unchanged (deletion blocked) - expect(newState.doc.textContent).toBe('Locked content'); + // Assert: content should be unchanged + expect(newState.doc.textContent).toBe(originalContent); }); }); - describe('contentLocked mode', () => { - it('allows deletion of contentLocked inline structured content', () => { - createDocWithStructuredContent('contentLocked', 'structuredContent'); - - // Find the structured content node position - let nodePos = null; - let nodeSize = null; - editor.state.doc.descendants((node, pos) => { - if (node.type.name === 'structuredContent') { - nodePos = pos; - nodeSize = node.nodeSize; - return false; - } - }); - - expect(nodePos).not.toBeNull(); + describe('multiple SDT nodes', () => { + function createDocWithMultipleSDTs() { + const text1 = schema.text('Unlocked text'); + const text2 = schema.text('Locked text'); + const sdt1 = schema.nodes.structuredContent.create({ id: 'sdt-1', lockMode: 'unlocked' }, text1); + const sdt2 = schema.nodes.structuredContent.create({ id: 'sdt-2', lockMode: 'sdtLocked' }, text2); + const space = schema.text(' '); + const paragraph = schema.nodes.paragraph.create(null, [sdt1, space, sdt2]); + return schema.nodes.doc.create(null, [paragraph]); + } - // Try to delete the node - const tr = editor.state.tr.delete(nodePos, nodePos + nodeSize); - const newState = editor.state.apply(tr); + it('allows deletion of unlocked SDT while preserving locked SDT in same document', () => { + // Arrange + const doc = createDocWithMultipleSDTs(); + const state = applyDocToEditor(doc); - // The node should be deleted - let foundNode = false; - newState.doc.descendants((node) => { - if (node.type.name === 'structuredContent') { - foundNode = true; + // Find the unlocked SDT (first one) + let unlockedSDT = null; + state.doc.descendants((node, pos) => { + if (node.type.name === 'structuredContent' && node.attrs.lockMode === 'unlocked') { + unlockedSDT = { pos, end: pos + node.nodeSize }; return false; } }); + expect(unlockedSDT).not.toBeNull(); - expect(foundNode).toBe(false); + // Act: delete the unlocked SDT + const tr = state.tr.delete(unlockedSDT.pos, unlockedSDT.end); + const newState = state.apply(tr); + + // Assert: unlocked SDT deleted, locked SDT preserved + expect(newState.doc.textContent).not.toContain('Unlocked text'); + expect(newState.doc.textContent).toContain('Locked text'); }); - it('allows deletion of contentLocked block structured content', () => { - createDocWithStructuredContent('contentLocked', 'structuredContentBlock'); + it('blocks deletion that would affect locked SDT even when unlocked SDT is also selected', () => { + // Arrange + const doc = createDocWithMultipleSDTs(); + const state = applyDocToEditor(doc); - // Find the structured content block position - let nodePos = null; - let nodeSize = null; - editor.state.doc.descendants((node, pos) => { - if (node.type.name === 'structuredContentBlock') { - nodePos = pos; - nodeSize = node.nodeSize; - return false; + // Find both SDTs + const sdts = []; + state.doc.descendants((node, pos) => { + if (node.type.name === 'structuredContent') { + sdts.push({ pos, end: pos + node.nodeSize, lockMode: node.attrs.lockMode }); } }); + expect(sdts.length).toBe(2); - expect(nodePos).not.toBeNull(); - - // Try to delete the node - const tr = editor.state.tr.delete(nodePos, nodePos + nodeSize); - const newState = editor.state.apply(tr); - - // The node should be deleted - let foundNode = false; - newState.doc.descendants((node) => { - if (node.type.name === 'structuredContentBlock') { - foundNode = true; - return false; - } - }); + // Act: try to delete everything (both SDTs) + const deleteFrom = sdts[0].pos; + const deleteTo = sdts[1].end; + const tr = state.tr.delete(deleteFrom, deleteTo); + const newState = state.apply(tr); - expect(foundNode).toBe(false); + // Assert: locked SDT should still exist + expect(newState.doc.textContent).toContain('Locked text'); }); }); - describe('unlocked mode', () => { - it('allows deletion of unlocked inline structured content', () => { - createDocWithStructuredContent('unlocked', 'structuredContent'); + describe('edge cases', () => { + it('allows transaction when document has no SDT nodes', () => { + // Arrange: create doc without SDT + const text = schema.text('Regular paragraph'); + const paragraph = schema.nodes.paragraph.create(null, [text]); + const doc = schema.nodes.doc.create(null, [paragraph]); + const state = applyDocToEditor(doc); - // Find the structured content node position - let nodePos = null; - let nodeSize = null; - editor.state.doc.descendants((node, pos) => { - if (node.type.name === 'structuredContent') { - nodePos = pos; - nodeSize = node.nodeSize; - return false; - } - }); + // Act + const tr = state.tr.delete(2, 5); + const newState = state.apply(tr); - expect(nodePos).not.toBeNull(); + // Assert: deletion should proceed + expect(newState.doc.textContent).not.toBe(state.doc.textContent); + }); - // Try to delete the node - const tr = editor.state.tr.delete(nodePos, nodePos + nodeSize); - const newState = editor.state.apply(tr); + it('allows non-document-changing transactions', () => { + // Arrange + const doc = createDocWithSDT('sdtContentLocked', 'structuredContent'); + const state = applyDocToEditor(doc); - // The node should be deleted - let foundNode = false; - newState.doc.descendants((node) => { - if (node.type.name === 'structuredContent') { - foundNode = true; - return false; - } - }); + // Act: create selection-only transaction + const tr = state.tr.setSelection(TextSelection.create(state.doc, 1)); + const newState = state.apply(tr); - expect(foundNode).toBe(false); + // Assert: should not throw, selection should change + expect(newState.selection.from).toBe(1); }); - it('allows deletion of unlocked block structured content', () => { - createDocWithStructuredContent('unlocked', 'structuredContentBlock'); + it('handles deletion at document boundaries gracefully', () => { + // Arrange + const doc = createDocWithSDT('unlocked', 'structuredContent'); + const state = applyDocToEditor(doc); - // Find the structured content block position - let nodePos = null; - let nodeSize = null; - editor.state.doc.descendants((node, pos) => { - if (node.type.name === 'structuredContentBlock') { - nodePos = pos; - nodeSize = node.nodeSize; - return false; - } - }); - - expect(nodePos).not.toBeNull(); + // Act: delete from start of document + const tr = state.tr.delete(0, 2); + const newState = state.apply(tr); - // Try to delete the node - const tr = editor.state.tr.delete(nodePos, nodePos + nodeSize); - const newState = editor.state.apply(tr); + // Assert: should handle gracefully (exact behavior depends on schema) + expect(newState).toBeDefined(); + }); + }); - // The node should be deleted - let foundNode = false; - newState.doc.descendants((node) => { - if (node.type.name === 'structuredContentBlock') { - foundNode = true; - return false; - } - }); + describe('lock mode attribute validation', () => { + it('treats missing lockMode as unlocked', () => { + // Arrange: create SDT without explicit lockMode (defaults to unlocked) + const text = schema.text('Default lock'); + const sdt = schema.nodes.structuredContent.create({ id: 'test-123' }, text); + const paragraph = schema.nodes.paragraph.create(null, [sdt]); + const doc = schema.nodes.doc.create(null, [paragraph]); + const state = applyDocToEditor(doc); + const sdtInfo = findSDTNode(state.doc, 'structuredContent'); + + // Act: attempt to delete + const tr = state.tr.delete(sdtInfo.pos, sdtInfo.end); + const newState = state.apply(tr); + + // Assert: should be deletable (unlocked behavior) + expect(sdtNodeExists(newState.doc, 'structuredContent')).toBe(false); + }); - expect(foundNode).toBe(false); + it('treats invalid lockMode as unlocked', () => { + // Arrange: create SDT with invalid lockMode + const text = schema.text('Invalid lock'); + const sdt = schema.nodes.structuredContent.create({ id: 'test-123', lockMode: 'invalidMode' }, text); + const paragraph = schema.nodes.paragraph.create(null, [sdt]); + const doc = schema.nodes.doc.create(null, [paragraph]); + const state = applyDocToEditor(doc); + const sdtInfo = findSDTNode(state.doc, 'structuredContent'); + + // Act: attempt to delete + const tr = state.tr.delete(sdtInfo.pos, sdtInfo.end); + const newState = state.apply(tr); + + // Assert: should be deletable (treated as unlocked) + expect(sdtNodeExists(newState.doc, 'structuredContent')).toBe(false); }); }); }); From 0646a2f4514e01d3080d2602a0a2f20a8d6feee3 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Sat, 7 Feb 2026 10:10:24 -0300 Subject: [PATCH 06/18] feat(super-editor): enhance SDT boundary handling and hover functionality - Introduced SdtGroupedHover class to manage hover states for multi-fragment SDT blocks, allowing simultaneous highlighting of all fragments. - Updated shouldRebuildForSdtBoundary function to improve checks for SDT boundary changes, ensuring stale attributes are removed and boundaries are correctly validated. - Adjusted DOM rendering logic to incorporate new hover functionality and boundary checks. - Modified styles for SDT container labels to improve visibility and interaction during hover states. --- .../painters/dom/src/renderer.ts | 38 +++++---- .../layout-engine/painters/dom/src/styles.ts | 54 +++++-------- .../painters/dom/src/utils/sdt-helpers.ts | 22 ++++++ .../painters/dom/src/utils/sdt-hover.ts | 79 +++++++++++++++++++ 4 files changed, 145 insertions(+), 48 deletions(-) create mode 100644 packages/layout-engine/painters/dom/src/utils/sdt-hover.ts diff --git a/packages/layout-engine/painters/dom/src/renderer.ts b/packages/layout-engine/painters/dom/src/renderer.ts index 97e89fa6a2..3abbb218aa 100644 --- a/packages/layout-engine/painters/dom/src/renderer.ts +++ b/packages/layout-engine/painters/dom/src/renderer.ts @@ -68,7 +68,13 @@ import { DOM_CLASS_NAMES } from './constants.js'; import { sanitizeHref, encodeTooltip } from '@superdoc/url-validation'; import { renderTableFragment as renderTableFragmentElement } from './table/renderTableFragment.js'; import { assertPmPositions, assertFragmentPmPositions } from './pm-position-validation.js'; -import { applySdtContainerStyling, getSdtContainerKey, type SdtBoundaryOptions } from './utils/sdt-helpers.js'; +import { + applySdtContainerStyling, + getSdtContainerKey, + shouldRebuildForSdtBoundary, + type SdtBoundaryOptions, +} from './utils/sdt-helpers.js'; +import { SdtGroupedHover } from './utils/sdt-hover.js'; import { generateRulerDefinitionFromPx, createRulerElement, @@ -822,6 +828,7 @@ export class DomPainter { private onScrollHandler: ((e: Event) => void) | null = null; private onWindowScrollHandler: ((e: Event) => void) | null = null; private onResizeHandler: ((e: Event) => void) | null = null; + private sdtHover = new SdtGroupedHover(); /** The currently active/selected comment ID for highlighting */ private activeCommentId: string | null = null; @@ -1181,6 +1188,8 @@ export class DomPainter { }; win.addEventListener('resize', this.onResizeHandler); } + + this.sdtHover.bind(mount); } private computeVirtualMetrics(): void { @@ -1361,6 +1370,8 @@ export class DomPainter { // Clear changed blocks now that current visible pages are patched this.changedBlocks.clear(); this.processedLayoutVersion = this.layoutVersion; + + this.sdtHover.reapply(); } private updateSpacers(start: number, end: number): void { @@ -1722,6 +1733,7 @@ export class DomPainter { this.onScrollHandler = null; this.onWindowScrollHandler = null; this.onResizeHandler = null; + this.sdtHover.destroy(); this.layoutVersion = 0; this.processedLayoutVersion = -1; } @@ -1796,10 +1808,20 @@ export class DomPainter { if (current) { existing.delete(key); const sdtBoundaryMismatch = shouldRebuildForSdtBoundary(current.element, sdtBoundary); + // Verify the position mapping is reliable: if mapping the old pmStart doesn't produce + // the expected new pmStart, the mapping is degenerate (e.g. full-document paste) and + // we must rebuild to get correct span position attributes. + const newPmStart = (fragment as { pmStart?: number }).pmStart; + const mappingUnreliable = + this.currentMapping != null && + newPmStart != null && + current.element.dataset.pmStart != null && + this.currentMapping.map(Number(current.element.dataset.pmStart)) !== newPmStart; const needsRebuild = this.changedBlocks.has(fragment.blockId) || current.signature !== fragmentSignature(fragment, this.blockLookup) || - sdtBoundaryMismatch; + sdtBoundaryMismatch || + mappingUnreliable; if (needsRebuild) { const replacement = this.renderFragment(fragment, contextBase, sdtBoundary); @@ -5281,18 +5303,6 @@ const computeSdtBoundaries = ( return boundaries; }; -const shouldRebuildForSdtBoundary = (element: HTMLElement, boundary: SdtBoundaryOptions | undefined): boolean => { - if (!boundary) return false; - const startAttr = element.dataset.sdtContainerStart; - const endAttr = element.dataset.sdtContainerEnd; - const expectedStart = String(boundary.isStart ?? true); - const expectedEnd = String(boundary.isEnd ?? true); - if (startAttr === undefined || endAttr === undefined) { - return true; - } - return startAttr !== expectedStart || endAttr !== expectedEnd; -}; - const fragmentKey = (fragment: Fragment): string => { if (fragment.kind === 'para') { return `para:${fragment.blockId}:${fragment.fromLine}:${fragment.toLine}`; diff --git a/packages/layout-engine/painters/dom/src/styles.ts b/packages/layout-engine/painters/dom/src/styles.ts index c14e16f5b0..98e16f71f7 100644 --- a/packages/layout-engine/painters/dom/src/styles.ts +++ b/packages/layout-engine/painters/dom/src/styles.ts @@ -358,21 +358,21 @@ const SDT_CONTAINER_STYLES = ` /* Structured content drag handle/label - positioned above */ .superdoc-structured-content__label { - font-size: 10px; + font-size: 11px; align-items: center; justify-content: center; position: absolute; left: 2px; top: -19px; width: calc(100% - 4px); - max-width: 110px; + max-width: 130px; min-width: 0; height: 18px; padding: 0 4px; border: 1px solid #629be7; border-bottom: none; border-radius: 6px 6px 0 0; - background-color: #629be7dd; + background-color: #629be7ee; box-sizing: border-box; z-index: 10; display: none; @@ -386,7 +386,7 @@ const SDT_CONTAINER_STYLES = ` text-overflow: ellipsis; } -.superdoc-structured-content-block:hover .superdoc-structured-content__label { +.superdoc-structured-content-block.sdt-hover .superdoc-structured-content__label { display: inline-flex; } @@ -438,9 +438,9 @@ const SDT_CONTAINER_STYLES = ` bottom: calc(100% + 2px); left: 50%; transform: translateX(-50%); - font-size: 10px; - padding: 2px 6px; - background-color: #629be7dd; + font-size: 11px; + padding: 0 4px; + background-color: #629be7ee; color: white; border-radius: 4px; white-space: nowrap; @@ -456,38 +456,24 @@ const SDT_CONTAINER_STYLES = ` /* Lock mode styles for structured content - matches Word appearance exactly */ /* Default: background color only, no border. Border appears on hover/focus */ -/* unlocked: light mint green - fully editable and deletable */ -.superdoc-structured-content-block[data-lock-mode="unlocked"], -.superdoc-structured-content-inline[data-lock-mode="unlocked"] { - background-color: #e6f4ea; - border: 1px solid transparent; +/* Lock mode: hide border by default, show on hover. + * Use border-color (not border shorthand) to preserve continuation rules + * that remove border-top/border-bottom on multi-fragment SDT containers. */ +.superdoc-structured-content-block[data-lock-mode], +.superdoc-structured-content-inline[data-lock-mode] { + border-color: transparent; } -/* sdtLocked: golden yellow - SDT cannot be deleted but content can be edited */ -.superdoc-structured-content-block[data-lock-mode="sdtLocked"], -.superdoc-structured-content-inline[data-lock-mode="sdtLocked"] { - background-color: #fff3cd; - border: 1px solid transparent; -} - -/* contentLocked: light blue/lavender - content is read-only but SDT can be deleted */ -.superdoc-structured-content-block[data-lock-mode="contentLocked"], -.superdoc-structured-content-inline[data-lock-mode="contentLocked"] { - background-color: #e8f0f8; - border: 1px solid transparent; -} - -/* sdtContentLocked: light peach/salmon - fully locked */ -.superdoc-structured-content-block[data-lock-mode="sdtContentLocked"], -.superdoc-structured-content-inline[data-lock-mode="sdtContentLocked"] { - background-color: #ffe8e0; - border: 1px solid transparent; +/* Show blue border on hover for all lock modes */ +.superdoc-structured-content-block[data-lock-mode].sdt-hover { + border-color: #629be7; + background-color: rgba(98, 155, 231, 0.05); + z-index: 99; } -/* Show blue border on hover for all lock modes */ -.superdoc-structured-content-block[data-lock-mode]:hover, .superdoc-structured-content-inline[data-lock-mode]:hover { border-color: #629be7; + z-index: 99; } /* Viewing mode: remove structured content affordances */ @@ -536,7 +522,7 @@ const FIELD_ANNOTATION_STYLES = ` .superdoc-layout .annotation *::selection { background: transparent; } - + .superdoc-layout .annotation::-moz-selection, .superdoc-layout .annotation *::-moz-selection { background: transparent; diff --git a/packages/layout-engine/painters/dom/src/utils/sdt-helpers.ts b/packages/layout-engine/painters/dom/src/utils/sdt-helpers.ts index 867b065ffe..507acbe225 100644 --- a/packages/layout-engine/painters/dom/src/utils/sdt-helpers.ts +++ b/packages/layout-engine/painters/dom/src/utils/sdt-helpers.ts @@ -280,3 +280,25 @@ export function applySdtContainerStyling( container.appendChild(labelEl); } } + +/** + * Checks whether a fragment element needs rebuilding due to SDT boundary changes. + * + * Handles two cases: + * 1. Element was in an SDT but no longer is (stale attributes need removal) + * 2. Element's start/end boundary flags don't match expected values + */ +export function shouldRebuildForSdtBoundary(element: HTMLElement, boundary: SdtBoundaryOptions | undefined): boolean { + if (!boundary) { + // Rebuild if element has stale SDT container attributes that should be removed + return element.dataset.sdtContainerStart !== undefined; + } + const startAttr = element.dataset.sdtContainerStart; + const endAttr = element.dataset.sdtContainerEnd; + const expectedStart = String(boundary.isStart ?? true); + const expectedEnd = String(boundary.isEnd ?? true); + if (startAttr === undefined || endAttr === undefined) { + return true; + } + return startAttr !== expectedStart || endAttr !== expectedEnd; +} diff --git a/packages/layout-engine/painters/dom/src/utils/sdt-hover.ts b/packages/layout-engine/painters/dom/src/utils/sdt-hover.ts new file mode 100644 index 0000000000..e19bd82e43 --- /dev/null +++ b/packages/layout-engine/painters/dom/src/utils/sdt-hover.ts @@ -0,0 +1,79 @@ +/** + * Grouped hover for multi-fragment SDT blocks. + * + * When a block SDT spans multiple paragraphs, each renders as a separate DOM element. + * This class uses event delegation to highlight ALL fragments of the same SDT + * simultaneously via the `.sdt-hover` CSS class. + */ + +const SDT_BLOCK_SELECTOR = '.superdoc-structured-content-block[data-sdt-id]'; +const HOVER_CLASS = 'sdt-hover'; + +function sdtElementsById(root: HTMLElement, sdtId: string): NodeListOf { + return root.querySelectorAll(`.superdoc-structured-content-block[data-sdt-id="${sdtId}"]`); +} + +export class SdtGroupedHover { + private hoveredSdtId: string | null = null; + private mount: HTMLElement | null = null; + private onMouseOver: ((e: Event) => void) | null = null; + private onMouseLeave: (() => void) | null = null; + + /** Attach hover listeners to the mount element. Safe to call again on remount. */ + bind(mount: HTMLElement): void { + this.destroy(); + this.mount = mount; + + this.onMouseOver = (e: Event) => { + const target = (e.target as HTMLElement).closest?.(SDT_BLOCK_SELECTOR) as HTMLElement | null; + const sdtId = target?.dataset.sdtId ?? null; + + if (this.hoveredSdtId && this.hoveredSdtId !== sdtId) { + sdtElementsById(mount, this.hoveredSdtId).forEach((el) => el.classList.remove(HOVER_CLASS)); + } + + this.hoveredSdtId = sdtId; + + if (sdtId) { + sdtElementsById(mount, sdtId).forEach((el) => el.classList.add(HOVER_CLASS)); + } + }; + + this.onMouseLeave = () => { + if (this.hoveredSdtId) { + sdtElementsById(mount, this.hoveredSdtId).forEach((el) => el.classList.remove(HOVER_CLASS)); + this.hoveredSdtId = null; + } + }; + + mount.addEventListener('mouseover', this.onMouseOver); + mount.addEventListener('mouseleave', this.onMouseLeave); + } + + /** Re-apply hover class after render. New/rebuilt elements lose the class. */ + reapply(): void { + if (this.hoveredSdtId && this.mount) { + sdtElementsById(this.mount, this.hoveredSdtId).forEach((el) => el.classList.add(HOVER_CLASS)); + } + } + + /** Remove listeners and reset state. */ + destroy(): void { + if (this.mount) { + if (this.onMouseOver) { + try { + this.mount.removeEventListener('mouseover', this.onMouseOver); + } catch {} + } + if (this.onMouseLeave) { + try { + this.mount.removeEventListener('mouseleave', this.onMouseLeave); + } catch {} + } + } + this.mount = null; + this.onMouseOver = null; + this.onMouseLeave = null; + this.hoveredSdtId = null; + } +} From e377ab95a60cea1637e801b681e4c74338fa255f Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Tue, 10 Feb 2026 19:28:36 -0300 Subject: [PATCH 07/18] feat(super-editor): add inline SDT wrapping for geometry rendering path and visual test story - Route tab, image, field annotation, and text elements through inline SDT wrapper in the geometry-based rendering path (previously only the run-based path had SDT wrapping) - Add `sdt` metadata to TabRun contract type and pm-adapter tab converter - Unify hover styles for block and inline SDTs (consistent colors, !important to override lock-mode transparent borders and continuation rules) - Remove unnecessary try/catch around removeEventListener in SdtGroupedHover - Add performance optimization TODO to collectSDTNodes in lock plugin - Re-export StructuredContentLockMode from contracts instead of duplicating - Add visual testing story (sdt-lock-modes) demonstrating all lock modes, SDT creation/update commands, and keyboard interactions --- .../structured-content/sdt-lock-modes.ts | 280 ++++++++++++++++++ packages/layout-engine/contracts/src/index.ts | 2 + .../painters/dom/src/renderer.ts | 96 +++++- .../layout-engine/painters/dom/src/styles.ts | 27 +- .../painters/dom/src/utils/sdt-hover.ts | 8 +- .../src/converters/inline-converters/tab.ts | 5 + .../structured-content-lock-plugin.js | 6 +- .../src/extensions/types/node-attributes.ts | 3 +- 8 files changed, 403 insertions(+), 24 deletions(-) create mode 100644 devtools/visual-testing/tests/interactions/stories/structured-content/sdt-lock-modes.ts diff --git a/devtools/visual-testing/tests/interactions/stories/structured-content/sdt-lock-modes.ts b/devtools/visual-testing/tests/interactions/stories/structured-content/sdt-lock-modes.ts new file mode 100644 index 0000000000..4160b731a9 --- /dev/null +++ b/devtools/visual-testing/tests/interactions/stories/structured-content/sdt-lock-modes.ts @@ -0,0 +1,280 @@ +import { defineStory } from '@superdoc-testing/helpers'; +import type { Page } from '@playwright/test'; + +const WAIT_MS = 400; + +/** + * Find an SDT node position by its id attribute. + * Returns { pos, size } for the first matching structuredContent or structuredContentBlock node. + */ +async function findSdtPosition(page: Page, id: string): Promise<{ pos: number; size: number } | null> { + return page.evaluate((sdtId) => { + const editor = (window as unknown as { editor?: { state?: { doc?: { descendants?: Function } } } }).editor; + if (!editor?.state?.doc?.descendants) return null; + + let result: { pos: number; size: number } | null = null; + editor.state.doc.descendants( + (node: { type: { name: string }; attrs: Record; nodeSize: number }, pos: number) => { + if (result) return false; + if ( + (node.type.name === 'structuredContent' || node.type.name === 'structuredContentBlock') && + String(node.attrs.id) === sdtId + ) { + result = { pos, size: node.nodeSize }; + return false; + } + return true; + }, + ); + return result; + }, id); +} + +/** + * Set the cursor position in the editor. + */ +async function setCursorPosition(page: Page, pos: number): Promise { + await page.evaluate((p) => { + const editor = ( + window as unknown as { + editor?: { commands?: { setTextSelection?: (sel: { from: number; to: number }) => void; focus?: () => void } }; + } + ).editor; + editor?.commands?.setTextSelection?.({ from: p, to: p }); + editor?.commands?.focus?.(); + }, pos); +} + +/** + * Insert an inline structured content node via the editor command. + */ +async function insertInlineSdt( + page: Page, + attrs: { id: string; alias: string; lockMode: string }, + text: string, +): Promise { + await page.evaluate( + ({ attrs, text }) => { + const editor = ( + window as unknown as { + editor?: { + commands?: { + insertStructuredContentInline?: (opts: { attrs: typeof attrs; text: string }) => boolean; + }; + }; + } + ).editor; + if (!editor?.commands?.insertStructuredContentInline) { + throw new Error('insertStructuredContentInline command not available'); + } + editor.commands.insertStructuredContentInline({ attrs, text }); + }, + { attrs, text }, + ); +} + +/** + * Demonstrates SDT (Structured Document Tag) lock modes via programmatic + * commands and keyboard interactions. + * + * Lock modes: + * - unlocked: wrapper deletable, content editable + * - sdtLocked: wrapper NOT deletable, content editable + * - contentLocked: wrapper deletable, content NOT editable + * - sdtContentLocked: wrapper NOT deletable, content NOT editable + * + * This story exercises insertStructuredContentInline, insertStructuredContentBlock, + * updateStructuredContentById, cursor placement inside SDTs, and demonstrates + * lock enforcement by attempting keyboard interactions in locked SDTs. + */ +export default defineStory({ + name: 'sdt-lock-modes', + description: 'Create SDTs with various lock modes, interact with keyboard, demonstrate lock enforcement', + startDocument: null, + hideCaret: false, + + async run(page, helpers): Promise { + const { step, focus, type, press, waitForStable, milestone } = helpers; + + // ----------------------------------------------------------------- + // Step 1 – Insert inline SDTs with different lock modes + // ----------------------------------------------------------------- + await step('Insert inline SDTs', async () => { + await focus(); + + // Line 1: unlocked inline SDT + await type('Unlocked inline: '); + await waitForStable(WAIT_MS); + await insertInlineSdt(page, { id: '100', alias: 'Unlocked Field', lockMode: 'unlocked' }, 'editable value'); + await waitForStable(WAIT_MS); + + // Line 2: sdtLocked inline SDT + await press('End'); + await press('Enter'); + await type('SDT-locked inline: '); + await waitForStable(WAIT_MS); + await insertInlineSdt(page, { id: '200', alias: 'SDT Locked', lockMode: 'sdtLocked' }, 'cannot delete wrapper'); + await waitForStable(WAIT_MS); + + // Line 3: contentLocked inline SDT + await press('End'); + await press('Enter'); + await type('Content-locked inline: '); + await waitForStable(WAIT_MS); + await insertInlineSdt( + page, + { id: '300', alias: 'Content Locked', lockMode: 'contentLocked' }, + 'read-only content', + ); + await waitForStable(WAIT_MS); + + await milestone('inline-sdts-created', 'Three inline SDTs: unlocked, sdtLocked, contentLocked'); + }); + + // ----------------------------------------------------------------- + // Step 2 – Insert a block SDT with sdtContentLocked + // ----------------------------------------------------------------- + await step('Insert block SDT (sdtContentLocked)', async () => { + await press('End'); + await press('Enter'); + await press('Enter'); + await waitForStable(WAIT_MS); + + await page.evaluate(() => { + const editor = ( + window as unknown as { + editor?: { + commands?: { + insertStructuredContentBlock?: (opts: { + attrs: { id: string; alias: string; lockMode: string }; + html: string; + }) => boolean; + }; + }; + } + ).editor; + if (!editor?.commands?.insertStructuredContentBlock) { + throw new Error('insertStructuredContentBlock command not available'); + } + editor.commands.insertStructuredContentBlock({ + attrs: { id: '400', alias: 'Fully Locked Block', lockMode: 'sdtContentLocked' }, + html: '

This block is fully locked (sdtContentLocked).

', + }); + }); + await waitForStable(WAIT_MS); + + await milestone('block-sdt-created', 'Block SDT with sdtContentLocked created'); + }); + + // ----------------------------------------------------------------- + // Step 3 – Place cursor inside sdtLocked inline and type + // (content is editable — sdtLocked only protects the wrapper) + // ----------------------------------------------------------------- + await step('Type inside sdtLocked inline (content editable)', async () => { + const sdt = await findSdtPosition(page, '200'); + if (!sdt) throw new Error('sdtLocked SDT (id=200) not found'); + + // Place cursor inside the SDT text + await setCursorPosition(page, sdt.pos + 2); + await waitForStable(WAIT_MS); + + await type(' ADDED'); + await waitForStable(WAIT_MS); + + await milestone('sdt-locked-typed', 'Typed " ADDED" inside sdtLocked inline — content is editable'); + }); + + // ----------------------------------------------------------------- + // Step 4 – Place cursor inside contentLocked inline and try typing + // (content is NOT editable) + // ----------------------------------------------------------------- + await step('Try typing inside contentLocked inline', async () => { + const sdt = await findSdtPosition(page, '300'); + if (!sdt) throw new Error('contentLocked SDT (id=300) not found'); + + await setCursorPosition(page, sdt.pos + 2); + await waitForStable(WAIT_MS); + + // Attempt to type — should be blocked by contentLocked + await type('BLOCKED'); + await waitForStable(WAIT_MS); + + await milestone('content-locked-typing-blocked', 'Typing inside contentLocked SDT — should be blocked'); + }); + + // ----------------------------------------------------------------- + // Step 5 – Place cursor inside contentLocked and try Backspace + // (content deletion should also be blocked) + // ----------------------------------------------------------------- + await step('Try Backspace inside contentLocked inline', async () => { + const sdt = await findSdtPosition(page, '300'); + if (!sdt) throw new Error('contentLocked SDT (id=300) not found'); + + // Place cursor at end of SDT content + await setCursorPosition(page, sdt.pos + sdt.size - 2); + await waitForStable(WAIT_MS); + + await press('Backspace'); + await press('Backspace'); + await press('Backspace'); + await waitForStable(WAIT_MS); + + await milestone('content-locked-backspace-blocked', 'Backspace inside contentLocked SDT — should be blocked'); + }); + + // ----------------------------------------------------------------- + // Step 6 – Update lock mode via updateStructuredContentById + // Change the unlocked inline (id=100) to contentLocked + // ----------------------------------------------------------------- + await step('Update lock mode: unlocked → contentLocked', async () => { + await page.evaluate(() => { + const editor = ( + window as unknown as { + editor?: { + commands?: { + updateStructuredContentById?: (id: string, opts: { attrs: { lockMode: string } }) => boolean; + }; + }; + } + ).editor; + if (!editor?.commands?.updateStructuredContentById) { + throw new Error('updateStructuredContentById command not available'); + } + editor.commands.updateStructuredContentById('100', { + attrs: { lockMode: 'contentLocked' }, + }); + }); + await waitForStable(WAIT_MS); + + await milestone('lock-mode-updated', 'Updated id=100 from unlocked → contentLocked'); + }); + + // ----------------------------------------------------------------- + // Step 7 – Verify updated lock — try typing inside formerly unlocked SDT + // ----------------------------------------------------------------- + await step('Try typing in updated contentLocked SDT', async () => { + const sdt = await findSdtPosition(page, '100'); + if (!sdt) throw new Error('Updated SDT (id=100) not found'); + + await setCursorPosition(page, sdt.pos + 2); + await waitForStable(WAIT_MS); + + // Attempt to type — should now be blocked + await type('SHOULD FAIL'); + await waitForStable(WAIT_MS); + + await milestone('updated-lock-enforced', 'Typing in updated contentLocked SDT — should be blocked'); + }); + + // ----------------------------------------------------------------- + // Step 8 – Final state + // ----------------------------------------------------------------- + await step('Final state', async () => { + await focus(); + await setCursorPosition(page, 1); + await waitForStable(WAIT_MS); + + await milestone('final-state', 'Final document state with all SDT lock modes'); + }); + }, +}); diff --git a/packages/layout-engine/contracts/src/index.ts b/packages/layout-engine/contracts/src/index.ts index e524f78a8a..77bd061260 100644 --- a/packages/layout-engine/contracts/src/index.ts +++ b/packages/layout-engine/contracts/src/index.ts @@ -219,6 +219,8 @@ export type TabRun = RunMarks & { indent?: ParagraphIndent; pmStart?: number; pmEnd?: number; + /** SDT metadata if tab is inside a structured document tag. */ + sdt?: SdtMetadata; }; export type LineBreakRun = { diff --git a/packages/layout-engine/painters/dom/src/renderer.ts b/packages/layout-engine/painters/dom/src/renderer.ts index 6f5b10a0b8..6453da286c 100644 --- a/packages/layout-engine/painters/dom/src/renderer.ts +++ b/packages/layout-engine/painters/dom/src/renderer.ts @@ -4650,6 +4650,80 @@ export class DomPainter { return undefined; }; + // Inline SDT wrapping for geometry path (absolute-positioned elements). + // Same concept as the run-based path's SDT wrapper, but here elements use + // position:absolute so the wrapper itself must be absolutely positioned to + // span from the leftmost to rightmost child element. + let geoSdtWrapper: HTMLElement | null = null; + let geoSdtId: string | null = null; + let geoSdtWrapperLeft = 0; + let geoSdtMaxRight = 0; + + const closeGeoSdtWrapper = () => { + if (geoSdtWrapper) { + geoSdtWrapper.style.width = `${geoSdtMaxRight - geoSdtWrapperLeft}px`; + el.appendChild(geoSdtWrapper); + geoSdtWrapper = null; + geoSdtId = null; + } + }; + + /** + * Append an element to the line, routing through an inline SDT wrapper + * when the run has inline structuredContent metadata. + */ + const appendToLineGeo = (elem: HTMLElement, runForSdt: Run, elemLeftPx: number, elemWidthPx: number) => { + const runSdt = (runForSdt as TextRun).sdt; + const isInlineSdt = runSdt?.type === 'structuredContent' && runSdt?.scope === 'inline'; + const thisRunSdtId = isInlineSdt && runSdt?.id ? String(runSdt.id) : null; + + if (thisRunSdtId !== geoSdtId) { + closeGeoSdtWrapper(); + } + + if (isInlineSdt && thisRunSdtId && this.doc) { + if (!geoSdtWrapper) { + geoSdtWrapper = this.doc.createElement('span'); + geoSdtWrapper.className = DOM_CLASS_NAMES.INLINE_SDT_WRAPPER; + geoSdtWrapper.dataset.layoutEpoch = String(this.layoutEpoch); + geoSdtId = thisRunSdtId; + this.applySdtDataset(geoSdtWrapper, runSdt!); + geoSdtWrapperLeft = elemLeftPx; + geoSdtMaxRight = elemLeftPx; + geoSdtWrapper.style.position = 'absolute'; + geoSdtWrapper.style.left = `${elemLeftPx}px`; + geoSdtWrapper.style.top = '0px'; + geoSdtWrapper.style.height = `${line.lineHeight}px`; + const alias = (runSdt as { alias?: string })?.alias || 'Inline content'; + const labelEl = this.doc.createElement('span'); + labelEl.className = `${DOM_CLASS_NAMES.INLINE_SDT_WRAPPER}__label`; + labelEl.textContent = alias; + geoSdtWrapper.appendChild(labelEl); + } + // Adjust element left to be relative to wrapper + elem.style.left = `${elemLeftPx - geoSdtWrapperLeft}px`; + geoSdtMaxRight = Math.max(geoSdtMaxRight, elemLeftPx + elemWidthPx); + // Track PM positions on wrapper to span all contained runs + const pmStart = (runForSdt as TextRun).pmStart; + const pmEnd = (runForSdt as TextRun).pmEnd; + if (pmStart != null) { + const curStart = geoSdtWrapper.dataset.pmStart; + if (!curStart || pmStart < parseInt(curStart, 10)) { + geoSdtWrapper.dataset.pmStart = String(pmStart); + } + } + if (pmEnd != null) { + const curEnd = geoSdtWrapper.dataset.pmEnd; + if (!curEnd || pmEnd > parseInt(curEnd, 10)) { + geoSdtWrapper.dataset.pmEnd = String(pmEnd); + } + } + geoSdtWrapper.appendChild(elem); + } else { + el.appendChild(elem); + } + }; + for (let runIndex = line.fromRun; runIndex <= line.toRun; runIndex += 1) { const baseRun = block.runs[runIndex]; if (!baseRun) continue; @@ -4703,7 +4777,7 @@ export class DomPainter { if (baseRun.pmStart != null) tabEl.dataset.pmStart = String(baseRun.pmStart); if (baseRun.pmEnd != null) tabEl.dataset.pmEnd = String(baseRun.pmEnd); tabEl.dataset.layoutEpoch = String(this.layoutEpoch); - el.appendChild(tabEl); + appendToLineGeo(tabEl, baseRun, tabStartX + indentOffset, actualTabWidth); // Update cumulativeX to where the next content begins // This ensures proper positioning for subsequent elements @@ -4727,7 +4801,7 @@ export class DomPainter { (runSegments && runSegments[0]?.width !== undefined ? runSegments[0].width : elem.offsetWidth) ?? 0; elem.style.position = 'absolute'; elem.style.left = `${segX}px`; - el.appendChild(elem); + appendToLineGeo(elem, baseRun, segX, segWidth); cumulativeX = baseSegX + segWidth; } continue; @@ -4758,7 +4832,7 @@ export class DomPainter { const segWidth = (runSegments && runSegments[0]?.width !== undefined ? runSegments[0].width : 0) ?? 0; elem.style.position = 'absolute'; elem.style.left = `${segX}px`; - el.appendChild(elem); + appendToLineGeo(elem, baseRun, segX, segWidth); cumulativeX = baseSegX + segWidth; } continue; @@ -4805,7 +4879,7 @@ export class DomPainter { elem.style.position = 'absolute'; elem.style.left = `${xPos}px`; - el.appendChild(elem); + appendToLineGeo(elem, segmentRun, xPos, segment.width ?? 0); // Update cumulative X for next segment by measuring this element's width // This applies to ALL segments (both with and without explicit X) @@ -4822,9 +4896,15 @@ export class DomPainter { this.doc.body.removeChild(measureEl); } cumulativeX = baseX + width; + // Update SDT wrapper width if actual measured width differs from initial estimate + if (geoSdtWrapper) { + geoSdtMaxRight = Math.max(geoSdtMaxRight, xPos + width); + } } }); } + // Close any remaining SDT wrapper at end of geometry rendering + closeGeoSdtWrapper(); } else { // Use run-based rendering for normal text flow // Track current inline SDT wrapper to group adjacent runs with the same SDT id @@ -4851,6 +4931,7 @@ export class DomPainter { } // Special handling for TabRuns (e.g., signature lines with underlines) + let elem: HTMLElement | null = null; if (run.kind === 'tab') { const tabEl = this.doc!.createElement('span'); tabEl.classList.add('superdoc-tab'); @@ -4889,11 +4970,10 @@ export class DomPainter { if (run.pmEnd != null) tabEl.dataset.pmEnd = String(run.pmEnd); tabEl.dataset.layoutEpoch = String(this.layoutEpoch); - el.appendChild(tabEl); - return; + elem = tabEl; + } else { + elem = this.renderRun(run, context, trackedConfig); } - - const elem = this.renderRun(run, context, trackedConfig); if (elem) { if (styleId) { elem.setAttribute('styleid', styleId); diff --git a/packages/layout-engine/painters/dom/src/styles.ts b/packages/layout-engine/painters/dom/src/styles.ts index 316da3f302..8b3c8bb4f5 100644 --- a/packages/layout-engine/painters/dom/src/styles.ts +++ b/packages/layout-engine/painters/dom/src/styles.ts @@ -394,6 +394,13 @@ const SDT_CONTAINER_STYLES = ` text-overflow: ellipsis; } +/* Hover effect for block structured content (via event delegation class) */ +.superdoc-structured-content-block.sdt-hover { + border-color: #629be7 !important; + background-color: rgba(98, 155, 231, 0.08); + z-index: 9999999; +} + .superdoc-structured-content-block.sdt-hover .superdoc-structured-content__label { display: inline-flex; } @@ -436,8 +443,9 @@ const SDT_CONTAINER_STYLES = ` /* Hover effect for inline structured content */ .superdoc-structured-content-inline:hover { - background-color: rgba(98, 155, 231, 0.15); - border-color: #4a8ad9; + border-color: #629be7 !important; + background-color: rgba(98, 155, 231, 0.08); + z-index: 9999999; } /* Inline structured content label - shown on hover */ @@ -472,16 +480,19 @@ const SDT_CONTAINER_STYLES = ` border-color: transparent; } -/* Show blue border on hover for all lock modes */ +/* Show blue border on hover for all lock modes. + * Use !important on border-color to override the transparent default above + * and any continuation rules that remove border-top/border-bottom. */ .superdoc-structured-content-block[data-lock-mode].sdt-hover { - border-color: #629be7; - background-color: rgba(98, 155, 231, 0.05); - z-index: 99; + border-color: #629be7 !important; + background-color: rgba(98, 155, 231, 0.08); + z-index: 9999999; } .superdoc-structured-content-inline[data-lock-mode]:hover { - border-color: #629be7; - z-index: 99; + border-color: #629be7 !important; + background-color: rgba(98, 155, 231, 0.08); + z-index: 9999999; } /* Viewing mode: remove structured content affordances */ diff --git a/packages/layout-engine/painters/dom/src/utils/sdt-hover.ts b/packages/layout-engine/painters/dom/src/utils/sdt-hover.ts index e19bd82e43..05c685bf5f 100644 --- a/packages/layout-engine/painters/dom/src/utils/sdt-hover.ts +++ b/packages/layout-engine/painters/dom/src/utils/sdt-hover.ts @@ -61,14 +61,10 @@ export class SdtGroupedHover { destroy(): void { if (this.mount) { if (this.onMouseOver) { - try { - this.mount.removeEventListener('mouseover', this.onMouseOver); - } catch {} + this.mount.removeEventListener('mouseover', this.onMouseOver); } if (this.onMouseLeave) { - try { - this.mount.removeEventListener('mouseleave', this.onMouseLeave); - } catch {} + this.mount.removeEventListener('mouseleave', this.onMouseLeave); } } this.mount = null; diff --git a/packages/layout-engine/pm-adapter/src/converters/inline-converters/tab.ts b/packages/layout-engine/pm-adapter/src/converters/inline-converters/tab.ts index 357aa4788f..dfde920094 100644 --- a/packages/layout-engine/pm-adapter/src/converters/inline-converters/tab.ts +++ b/packages/layout-engine/pm-adapter/src/converters/inline-converters/tab.ts @@ -18,6 +18,7 @@ export function tabNodeToRun({ tabOrdinal, paragraphAttrs, inheritedMarks, + sdtMetadata, }: InlineConverterParams): Run | null { const pos = positions.get(node); if (!pos) return null; @@ -34,6 +35,10 @@ export function tabNodeToRun({ leader: (node.attrs?.leader as TabRun['leader']) ?? null, }; + if (sdtMetadata) { + run.sdt = sdtMetadata; + } + // Apply marks (e.g., underline) to the tab run const marks = [...(node.marks ?? []), ...(inheritedMarks ?? [])]; if (marks.length > 0) { diff --git a/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js b/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js index e3c900307f..7f192f4272 100644 --- a/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js +++ b/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js @@ -17,7 +17,11 @@ export const STRUCTURED_CONTENT_LOCK_KEY = new PluginKey('structuredContentLock' */ /** - * Collect all SDT nodes from the document + * Collect all SDT nodes from the document. + * + * TODO: For large documents, consider caching SDT nodes in plugin state + * (rebuild on docChanged only), early-exit on unlocked nodes, or limiting + * the search to nodes near the current selection for key/input handlers. */ function collectSDTNodes(doc) { const sdtNodes = []; diff --git a/packages/super-editor/src/extensions/types/node-attributes.ts b/packages/super-editor/src/extensions/types/node-attributes.ts index eaa77e292a..1261f081fc 100644 --- a/packages/super-editor/src/extensions/types/node-attributes.ts +++ b/packages/super-editor/src/extensions/types/node-attributes.ts @@ -14,6 +14,7 @@ import type { InlineNodeAttributes, ShapeNodeAttributes, } from '../../core/types/NodeCategories.js'; +import type { StructuredContentLockMode } from '@superdoc/contracts'; // ============================================ // SHARED TYPES @@ -593,7 +594,7 @@ export interface HardBreakAttrs extends InlineNodeAttributes { // STRUCTURED CONTENT // ============================================ -export type StructuredContentLockMode = 'unlocked' | 'sdtLocked' | 'contentLocked' | 'sdtContentLocked'; +export type { StructuredContentLockMode }; /** Structured content node attributes */ export interface StructuredContentAttrs extends BlockNodeAttributes { From 0fb2bd753cf56c14427818a96bf94e454151d32c Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Wed, 11 Feb 2026 09:45:57 -0300 Subject: [PATCH 08/18] chore: revert unrelated formatting changes in demos/cdn From e632da1a739b038cf975c32ee6c43123f64e0828 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Wed, 11 Feb 2026 09:48:00 -0300 Subject: [PATCH 09/18] chore: revert unrelated formatting changes in demos/cdn --- demos/cdn/demo-config.json | 9 ++++++--- demos/cdn/file-upload.css | 18 +++++++++--------- demos/cdn/file-upload.js | 8 ++++---- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/demos/cdn/demo-config.json b/demos/cdn/demo-config.json index 2dceccd1fd..e4220552a4 100644 --- a/demos/cdn/demo-config.json +++ b/demos/cdn/demo-config.json @@ -1,4 +1,7 @@ { - "tags": ["editing", "viewing"], - "title": "CDN" -} + "tags": [ + "editing", + "viewing" + ], + "title": "CDN" +} \ No newline at end of file diff --git a/demos/cdn/file-upload.css b/demos/cdn/file-upload.css index 61ba20361d..8e5aa64cc5 100644 --- a/demos/cdn/file-upload.css +++ b/demos/cdn/file-upload.css @@ -1,10 +1,10 @@ .file-upload-button { - cursor: pointer; - padding: 8px 12px; - border-radius: 8px; - margin: 10px; - outline: none; - border: none; - background-color: #1355ff; - color: white; -} + cursor: pointer; + padding: 8px 12px; + border-radius: 8px; + margin: 10px; + outline: none; + border: none; + background-color: #1355ff; + color: white; +} \ No newline at end of file diff --git a/demos/cdn/file-upload.js b/demos/cdn/file-upload.js index c0e5879ad6..519d52dacb 100644 --- a/demos/cdn/file-upload.js +++ b/demos/cdn/file-upload.js @@ -3,7 +3,7 @@ const fileInput = document.querySelector('.file-upload-input'); uploadBtn.addEventListener('click', () => fileInput.click()); fileInput.addEventListener('change', (event) => { - const file = event.target.files?.[0]; - const uploadEvent = new CustomEvent('file-upload', { detail: file }); - if (file) window.dispatchEvent(uploadEvent); -}); + const file = event.target.files?.[0]; + const uploadEvent = new CustomEvent("file-upload", { detail: file }); + if (file) window.dispatchEvent(uploadEvent); +}); \ No newline at end of file From d600ffaccd4298d2ce60ee3684ac056180a2801e Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Wed, 11 Feb 2026 09:49:57 -0300 Subject: [PATCH 10/18] chore: revert unrelated formatting changes in demos/ --- .../chrome-extension/background.js | 86 +- .../chrome-extension/content.js | 210 +- .../chrome-extension/docx-validator.js | 272 +- .../chrome-extension/lib/style.css | 5346 ++++++++--------- 4 files changed, 2695 insertions(+), 3219 deletions(-) diff --git a/demos/chrome-extension/chrome-extension/background.js b/demos/chrome-extension/chrome-extension/background.js index cc54872e6f..ad128903ab 100644 --- a/demos/chrome-extension/chrome-extension/background.js +++ b/demos/chrome-extension/chrome-extension/background.js @@ -8,12 +8,12 @@ importScripts('dist/docx-validator.bundle.js'); function updateIcon(enabled) { const suffix = enabled ? '' : '-disabled'; const iconPath = { - 16: `icons/icon-16x16${suffix}.png`, - 19: `icons/icon-19x19${suffix}.png`, - 48: `icons/icon-48x48${suffix}.png`, - 128: `icons/icon-128x128${suffix}.png`, + "16": `icons/icon-16x16${suffix}.png`, + "19": `icons/icon-19x19${suffix}.png`, + "48": `icons/icon-48x48${suffix}.png`, + "128": `icons/icon-128x128${suffix}.png` }; - + chrome.action.setIcon({ path: iconPath }); } @@ -26,9 +26,9 @@ chrome.storage.sync.get(['extensionEnabled'], (result) => { // Create context menu on installation for selecting text chrome.runtime.onInstalled.addListener(() => { chrome.contextMenus.create({ - id: 'openSelectedInSuperdoc', - title: 'Open selected content in SuperDoc', - contexts: ['selection'], + id: "openSelectedInSuperdoc", + title: "Open selected content in SuperDoc", + contexts: ["selection"] }); }); @@ -55,13 +55,13 @@ async function handleDownloadFile(request, _sender, sendResponse) { const downloadId = await chrome.downloads.download({ url: request.url, filename: request.filename, - saveAs: true, + saveAs: true }); // Track this download to ignore it if downloaded from viewer viewerDownloadIds.add(downloadId); console.log('File download initiated:', request.filename, 'ID:', downloadId); - + sendResponse({ success: true, downloadId: downloadId }); } catch (error) { console.error('Error downloading file:', error); @@ -72,8 +72,8 @@ async function handleDownloadFile(request, _sender, sendResponse) { // Action to handler mapping const messageHandlers = { - toggleExtension: handleToggleExtension, - downloadFile: handleDownloadFile, + 'toggleExtension': handleToggleExtension, + 'downloadFile': handleDownloadFile, }; // Listen for messages @@ -87,13 +87,13 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { // Handle context menu clicks chrome.contextMenus.onClicked.addListener(async (info, tab) => { - if (!(info.menuItemId === 'openSelectedInSuperdoc' && info.selectionText)) return; + if (!(info.menuItemId === "openSelectedInSuperdoc" && info.selectionText)) return; // Send message to content script to capture HTML and open in SuperDoc try { await chrome.tabs.sendMessage(tab.id, { action: 'captureSelectedHTML', - selectedText: info.selectionText, + selectedText: info.selectionText }); } catch (error) { console.error('Error sending message to content script:', error); @@ -109,14 +109,14 @@ chrome.downloads.onChanged.addListener(async (downloadDelta) => { console.log('Extension disabled, ignoring download'); return; } - + // Check if this is a download from viewer - if so, ignore it, otherwise we get endless loop of opening modals if (viewerDownloadIds.has(downloadDelta.id)) { viewerDownloadIds.delete(downloadDelta.id); console.log('Ignoring viewer download completion:', downloadDelta.id); return; } - + try { await processDownload(downloadDelta.id); } catch (error) { @@ -131,7 +131,7 @@ async function sendMessageToActiveTab(action, payload) { await chrome.tabs.sendMessage(tabs[0].id, { action, - ...payload, + ...payload }); } catch (error) { console.error('Error sending message to content script:', error); @@ -144,19 +144,19 @@ async function processDownload(downloadId) { const download = downloads[0]; const filename = download.filename.toLowerCase(); - + // File type handlers // We will handle markdown like html, since they are interoperable (to a point) const fileHandlers = { '.docx': processDocxFile, '.md': processMarkdownFile, - '.markdown': processMarkdownFile, + '.markdown': processMarkdownFile }; const extension = filename.substring(filename.lastIndexOf('.')); const handler = fileHandlers[extension]; if (!handler) throw new Error(`No handler for file type: ${extension}`); - + await handler(download); } @@ -165,7 +165,7 @@ async function processDocxFile(download) { // fetch and stringify (actual blob was getting dropped on message to viewer.js) const response = await fetch(`file://${download.filename}`); // background scripts let us do cool stuff like this const blob = await response.blob(); - + // Validate and correct the DOCX file // Some DOCX files are generate with little to no style info or a poor schema, // here we try to fill in the blanks. @@ -178,7 +178,7 @@ async function processDocxFile(download) { console.error('Error validating DOCX:', error); // Continue with original blob if validation fails } - + // convert to b64, actual blobs were getting dropped on message to content script const base64Data = await new Promise((resolve, reject) => { const reader = new FileReader(); @@ -186,15 +186,15 @@ async function processDocxFile(download) { reader.onerror = reject; reader.readAsDataURL(correctedBlob); }); - + // Send message to content script to display modal await sendMessageToActiveTab('displayDOCX', { data: { filename: download.filename, mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', fileSize: correctedBlob.size, - base64Data, - }, + base64Data + } }); } @@ -202,62 +202,62 @@ async function processMarkdownFile(download) { // Fetch the markdown file content const response = await fetch(`file://${download.filename}`); const markdownText = await response.text(); - + // Convert markdown to HTML const htmlContent = markdownToHtml(markdownText); - + // Send message to content script with HTML content await sendMessageToActiveTab('displayMarkdown', { data: { filename: download.filename, htmlContent: htmlContent, - originalMarkdown: markdownText, - }, + originalMarkdown: markdownText + } }); } // Simple markdown to HTML converter function markdownToHtml(markdown) { let html = markdown; - + // Headers html = html.replace(/^### (.*$)/gim, '

$1

'); html = html.replace(/^## (.*$)/gim, '

$1

'); html = html.replace(/^# (.*$)/gim, '

$1

'); - + // Bold html = html.replace(/\*\*(.*?)\*\*/gim, '$1'); html = html.replace(/__(.*?)__/gim, '$1'); - + // Italic html = html.replace(/\*(.*?)\*/gim, '$1'); html = html.replace(/_(.+?)_/gim, '$1'); - + // Code blocks html = html.replace(/```([\s\S]*?)```/gim, '
$1
'); - + // Inline code html = html.replace(/`(.*?)`/gim, '$1'); - + // Links html = html.replace(/\[([^\]]*)\]\(([^\)]*)\)/gim, '$1'); - + // Images html = html.replace(/!\[([^\]]*)\]\(([^\)]*)\)/gim, '$1'); - + // Lists html = html.replace(/^\* (.*$)/gim, '
  • $1
  • '); html = html.replace(/^\- (.*$)/gim, '
  • $1
  • '); html = html.replace(/^\+ (.*$)/gim, '
  • $1
  • '); - + // Wrap consecutive list items in ul tags html = html.replace(/(
  • .*<\/li>)/gims, '
      $1
    '); html = html.replace(/<\/ul>\s*
      /gim, ''); - + // Line breaks html = html.replace(/\n\n/gim, '

      '); html = html.replace(/\n/gim, '
      '); - + // Clean up empty paragraphs html = html.replace(/

      <\/p>/gim, ''); html = html.replace(/

      ()/gim, '$1'); @@ -266,9 +266,9 @@ function markdownToHtml(markdown) { html = html.replace(/(<\/ul>)<\/p>/gim, '$1'); html = html.replace(/

      (

      )/gim, '$1');
         html = html.replace(/(<\/pre>)<\/p>/gim, '$1');
      -
      +  
         // Wrap in paragraphs
         html = '

      ' + html + '

      '; - + return html; -} +} \ No newline at end of file diff --git a/demos/chrome-extension/chrome-extension/content.js b/demos/chrome-extension/chrome-extension/content.js index d583ac7a80..8203ca33c4 100644 --- a/demos/chrome-extension/chrome-extension/content.js +++ b/demos/chrome-extension/chrome-extension/content.js @@ -21,7 +21,7 @@ async function loadModalHTML() { // Inject CSS for modal function injectModalCSS() { if (document.getElementById(`${ID_PREFIX}modal-css`)) return; - + const style = document.createElement('style'); style.id = `${ID_PREFIX}modal-css`; style.textContent = ` @@ -47,7 +47,7 @@ async function loadSuperDoc() { cssLink.rel = 'stylesheet'; cssLink.href = chrome.runtime.getURL('lib/style.css'); document.head.appendChild(cssLink); - + // Check if SuperDoc library is available if (!window.SuperDocLibrary) { throw new Error('SuperDocLibrary not found - should be loaded via content script'); @@ -57,63 +57,63 @@ async function loadSuperDoc() { // Create modal async function createModal() { if (modalContainer) return modalContainer; - + injectModalCSS(); - + // Load external modal CSS const modalCssLink = document.createElement('link'); modalCssLink.rel = 'stylesheet'; modalCssLink.href = chrome.runtime.getURL('modal.css'); document.head.appendChild(modalCssLink); - + // Load modal HTML from file const modalHTML = await loadModalHTML(); if (!modalHTML) { console.error('Failed to load modal HTML'); return null; } - + const div = document.createElement('div'); div.innerHTML = modalHTML; modalContainer = div.firstElementChild; - + // Set the logo source after loading the HTML const logoImg = modalContainer.querySelector(`#${ID_PREFIX}logo`); if (logoImg) { // Try to get the page's favicon from gstatic first const currentDomain = window.location.hostname; const faviconUrl = `https://www.google.com/s2/favicons?domain=${currentDomain}&sz=32`; - + logoImg.src = faviconUrl; - + // Fallback to extension logo if favicon fails to load logoImg.onerror = () => { logoImg.src = chrome.runtime.getURL('icons/logo.webp'); }; } - + // Set the document title const titleElement = modalContainer.querySelector(`#${ID_PREFIX}document-title`); if (titleElement && currentFileData) { const filename = currentFileData.filename.split('/').pop(); // Get just the filename - const title = filename.replace(/\.[^/.]+$/, ''); // Remove file extension - titleElement.textContent = title || 'Untitled Document'; + const title = filename.replace(/\.[^/.]+$/, ""); // Remove file extension + titleElement.textContent = title || "Untitled Document"; } - + document.body.appendChild(modalContainer); - + // Setup event listeners const closeBtn = modalContainer.querySelector(`#${ID_PREFIX}close-btn`); const downloadBtn = modalContainer.querySelector(`#${ID_PREFIX}download-btn`); const downloadDropdown = modalContainer.querySelector(`#${ID_PREFIX}download-dropdown`); const downloadMarkdownBtn = modalContainer.querySelector(`#${ID_PREFIX}download-markdown`); const downloadHtmlBtn = modalContainer.querySelector(`#${ID_PREFIX}download-html`); - + closeBtn.addEventListener('click', closeModal); downloadBtn.addEventListener('click', handleDownloadClick); downloadMarkdownBtn.addEventListener('click', () => exportMarkdown()); downloadHtmlBtn.addEventListener('click', () => exportHTML()); - + // Close dropdown when clicking outside document.addEventListener('click', (e) => { if (!e.target.closest(`#${ID_PREFIX}download-wrapper`)) { @@ -127,21 +127,21 @@ async function createModal() { closeModal(); } }); - + // Close on Escape key document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && modalContainer.style.display !== 'none') { closeModal(); } }); - + return modalContainer; } // Close modal function closeModal() { if (!modalContainer) return; - + superdoc = null; // Remove modal from DOM completely @@ -153,25 +153,26 @@ function closeModal() { // Show modal function showModal() { if (!modalContainer) return; - + modalContainer.style.display = 'flex'; } + // Initialize SuperDoc in modal async function initSuperdocWithDOCX(data) { console.log('Initializing SuperDoc in modal'); - + try { if (!window.SuperDocLibrary?.SuperDoc) { console.error('SuperDocLibrary not available'); showFallback(data); return; } - + const file = new File([data.blob], data.filename, { type: data.mimeType }); const fileUrl = URL.createObjectURL(file); const superdocFile = await SuperDocLibrary.getFileObject(fileUrl, data.filename, data.mimeType); - + const config = { selector: `#${ID_PREFIX}docx-viewer`, toolbar: `#${ID_PREFIX}toolbar`, @@ -180,9 +181,9 @@ async function initSuperdocWithDOCX(data) { rulers: true, document: superdocFile, onReady: () => console.log('SuperDoc ready in modal'), - onEditorCreate: () => console.log('Editor created in modal'), + onEditorCreate: () => console.log('Editor created in modal') }; - + superdoc = new SuperDocLibrary.SuperDoc(config); // unhide selector const viewerElement = modalContainer.querySelector(`#${ID_PREFIX}docx-viewer`); @@ -190,6 +191,7 @@ async function initSuperdocWithDOCX(data) { viewerElement.style.display = 'flex'; } console.log('SuperDoc initialized in modal'); + } catch (error) { console.error('Error:', error.message); showFallback(data); @@ -201,7 +203,7 @@ async function handleDownloadClick() { const markdownViewer = document.getElementById(`${ID_PREFIX}markdown-viewer`); const docxViewer = document.getElementById(`${ID_PREFIX}docx-viewer`); const downloadDropdown = document.getElementById(`${ID_PREFIX}download-dropdown`); - + // Check if this is a markdown file if (markdownViewer && markdownViewer.style.display !== 'none') { // Show dropdown for markdown files @@ -228,7 +230,7 @@ async function downloadCurrentFile() { await exportMarkdown(); return; } - + // Export the current document from SuperDoc editor (DOCX files) const blobToDownload = await superdoc.activeEditor.exportDocx(); @@ -245,12 +247,12 @@ async function downloadCurrentFile() { if (fileName.includes('/') || fileName.includes('\\')) { fileName = fileName.split('/').pop().split('\\').pop(); } - + // Send download request to background script const response = await chrome.runtime.sendMessage({ action: 'downloadFile', url: dataUrl, - filename: fileName, + filename: fileName }); if (!response || !response.success) { @@ -270,8 +272,8 @@ function showFallback(data) { const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); - const formattedSize = bytes === 0 ? '0 B' : Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]; - + const formattedSize = bytes === 0 ? '0 B' : Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; + container.innerHTML = `

      File: ${data.filename}

      @@ -284,14 +286,14 @@ function showFallback(data) { // Initialize SuperDoc with HTML content for markdown files async function initSuperdocWithHTML(data) { console.log('Initializing SuperDoc with HTML content'); - + try { if (!window.SuperDocLibrary?.SuperDoc) { console.error('SuperDocLibrary not available'); showMarkdownFallback(data); return; } - + // Create a simple HTML document structure const htmlContent = ` @@ -312,7 +314,8 @@ async function initSuperdocWithHTML(data) { `; - + + const config = { selector: `#${ID_PREFIX}markdown-viewer`, documentMode: 'editing', @@ -322,9 +325,9 @@ async function initSuperdocWithHTML(data) { content: htmlContent, onReady: () => console.log('SuperDoc ready with HTML content'), onEditorCreate: () => console.log('Editor created with HTML content'), - converter: SuperDocLibrary.SuperConverter, + converter: SuperDocLibrary.SuperConverter }; - + superdoc = new SuperDocLibrary.Editor(config); superdoc.converter = new SuperDocLibrary.SuperConverter(); // unhide selector @@ -334,12 +337,8 @@ async function initSuperdocWithHTML(data) { } console.log('SuperDoc initialized with HTML content'); - const toolbar = new SuperDocLibrary.SuperToolbar({ - element: `${ID_PREFIX}toolbar`, - editor: superdoc, - isDev: true, - pagination: true, - }); + const toolbar = new SuperDocLibrary.SuperToolbar({ element: `${ID_PREFIX}toolbar`, editor: superdoc, isDev: true, pagination: true, }); + } catch (error) { console.error('Error initializing SuperDoc with HTML:', error.message); showMarkdownFallback(data); @@ -349,7 +348,7 @@ async function initSuperdocWithHTML(data) { // Show fallback content for markdown files function showMarkdownFallback(data) { const container = modalContainer.querySelector(`#${ID_PREFIX}viewer`); - + container.innerHTML = `

      Markdown File: ${data.filename}

      @@ -364,7 +363,7 @@ function showMarkdownFallback(data) { // Handle selected HTML capture async function handleCaptureSelectedHTML(request, sendResponse) { console.log('Capturing selected HTML for SuperDoc'); - + // Get the current selection const selection = window.getSelection(); if (!selection?.rangeCount) { @@ -377,18 +376,18 @@ async function handleCaptureSelectedHTML(request, sendResponse) { try { // Get the range of the selection const range = selection.getRangeAt(0); - + // Extract the HTML content of the selection const tempDiv = document.createElement('div'); tempDiv.appendChild(range.cloneContents()); const htmlContent = tempDiv.innerHTML; - + // Create data object similar to markdown processing const currentDomain = window.location.hostname; const data = { filename: `Selected content from ${currentDomain}.html`, htmlContent: htmlContent, - originalSource: 'webpage_selection', + originalSource: 'webpage_selection' }; // Store as current file data @@ -396,11 +395,11 @@ async function handleCaptureSelectedHTML(request, sendResponse) { // Load SuperDoc library await loadSuperDoc(); - + // Create and show modal await createModal(); showModal(); - + // Initialize SuperDoc with HTML content await initSuperdocWithHTML(data); @@ -410,7 +409,7 @@ async function handleCaptureSelectedHTML(request, sendResponse) { console.error('Error capturing HTML:', error); sendResponse({ success: false, error: error.message }); } - + return true; } @@ -427,9 +426,9 @@ async function handleCaptureSelectedHTML(request, sendResponse) { */ async function handledisplayDOCX(request, sendResponse) { if (!request.data.base64Data) return false; - + console.log('Received DOCX file data from background, displaying in modal'); - + const bytes = atob(request.data.base64Data); const array = new Uint8Array(bytes.length); for (let i = 0; i < bytes.length; i++) { @@ -438,17 +437,17 @@ async function handledisplayDOCX(request, sendResponse) { const blob = new Blob([array], { type: request.data.mimeType }); const data = { ...request.data, blob }; currentFileData = data; - + // Load SuperDoc library await loadSuperDoc(); // Create and show modal await createModal(); showModal(); - + // Initialize SuperDoc await initSuperdocWithDOCX(data); - + sendResponse({ success: true }); return true; } @@ -465,30 +464,30 @@ async function handledisplayDOCX(request, sendResponse) { */ async function handleDisplayMarkdown(request, sendResponse) { if (!request.data.htmlContent) return false; - + console.log('Received markdown file data from background, displaying in modal'); - + currentFileData = request.data; - + // Load SuperDoc library await loadSuperDoc(); // Create and show modal await createModal(); showModal(); - + // Initialize SuperDoc with HTML content await initSuperdocWithHTML(request.data); - + sendResponse({ success: true }); return true; } // Action to handler mapping const messageHandlers = { - captureSelectedHTML: handleCaptureSelectedHTML, - displayDOCX: handledisplayDOCX, - displayMarkdown: handleDisplayMarkdown, + 'captureSelectedHTML': handleCaptureSelectedHTML, + 'displayDOCX': handledisplayDOCX, + 'displayMarkdown': handleDisplayMarkdown }; // Export markdown from SuperDoc editor @@ -498,26 +497,26 @@ async function exportMarkdown() { console.error('Markdown viewer element not found'); return; } - + const htmlContent = viewerElement.innerHTML; if (!htmlContent) { console.error('No HTML content found in markdown viewer'); return; } - + // Convert HTML to markdown const markdownContent = htmlToMarkdown(htmlContent); - + // Create and download markdown file const blob = new Blob([markdownContent], { type: 'text/markdown' }); const url = URL.createObjectURL(blob); - + // Generate filename const filename = currentFileData?.filename || 'document'; const baseName = filename.replace(/\.[^/.]+$/, ''); // Remove extension const cleanBaseName = baseName.split('/').pop().split('\\').pop(); // Remove path const markdownFilename = `${cleanBaseName}.md`; - + // Download using Chrome downloads API try { const dataUrl = await new Promise((resolve) => { @@ -525,11 +524,11 @@ async function exportMarkdown() { reader.onload = () => resolve(reader.result); reader.readAsDataURL(blob); }); - + const response = await chrome.runtime.sendMessage({ action: 'downloadFile', url: dataUrl, - filename: markdownFilename, + filename: markdownFilename }); if (response?.success) { @@ -552,7 +551,7 @@ function htmlToMarkdown(html) { // Remove script and style tags html = html.replace(/]*>[\s\S]*?<\/script>/gi, ''); html = html.replace(/]*>[\s\S]*?<\/style>/gi, ''); - + // Convert headings html = html.replace(/]*>(.*?)<\/h1>/gi, '# $1\n\n'); html = html.replace(/]*>(.*?)<\/h2>/gi, '## $1\n\n'); @@ -560,65 +559,52 @@ function htmlToMarkdown(html) { html = html.replace(/]*>(.*?)<\/h4>/gi, '#### $1\n\n'); html = html.replace(/]*>(.*?)<\/h5>/gi, '##### $1\n\n'); html = html.replace(/]*>(.*?)<\/h6>/gi, '###### $1\n\n'); - + // Convert paragraphs html = html.replace(/]*>(.*?)<\/p>/gi, '$1\n\n'); - + // Convert line breaks html = html.replace(//gi, '\n'); - + // Convert bold and italic html = html.replace(/]*>(.*?)<\/strong>/gi, '**$1**'); html = html.replace(/]*>(.*?)<\/b>/gi, '**$1**'); html = html.replace(/]*>(.*?)<\/em>/gi, '*$1*'); html = html.replace(/]*>(.*?)<\/i>/gi, '*$1*'); - + // Convert code html = html.replace(/]*>(.*?)<\/code>/gi, '`$1`'); html = html.replace(/]*>(.*?)<\/pre>/gi, '```\n$1\n```\n\n'); - + // Convert blockquotes html = html.replace(/]*>(.*?)<\/blockquote>/gi, (_, content) => { - return ( - content - .split('\n') - .map((line) => `> ${line}`) - .join('\n') + '\n\n' - ); + return content.split('\n').map(line => `> ${line}`).join('\n') + '\n\n'; }); - + // Convert lists html = html.replace(/]*>(.*?)<\/ul>/gi, (_, content) => { const items = content.match(/]*>(.*?)<\/li>/gi) || []; - return ( - items - .map((item) => { - const text = item.replace(/<\/?li[^>]*>/gi, '').trim(); - return `- ${text}`; - }) - .join('\n') + '\n\n' - ); + return items.map(item => { + const text = item.replace(/<\/?li[^>]*>/gi, '').trim(); + return `- ${text}`; + }).join('\n') + '\n\n'; }); - + html = html.replace(/]*>(.*?)<\/ol>/gi, (_, content) => { const items = content.match(/]*>(.*?)<\/li>/gi) || []; - return ( - items - .map((item, index) => { - const text = item.replace(/<\/?li[^>]*>/gi, '').trim(); - return `${index + 1}. ${text}`; - }) - .join('\n') + '\n\n' - ); + return items.map((item, index) => { + const text = item.replace(/<\/?li[^>]*>/gi, '').trim(); + return `${index + 1}. ${text}`; + }).join('\n') + '\n\n'; }); - + // Remove remaining HTML tags html = html.replace(/<[^>]*>/g, ''); - + // Clean up whitespace html = html.replace(/\n\s*\n\s*\n/g, '\n\n'); // Multiple newlines to double html = html.replace(/^\s+|\s+$/g, ''); // Trim - + return html; } @@ -629,13 +615,13 @@ async function exportHTML() { console.error('Markdown viewer element not found'); return; } - + let htmlContent = viewerElement.innerHTML; if (!htmlContent) { console.error('No HTML content found in markdown viewer'); return; } - + // Create a complete HTML document const completeHTML = ` @@ -687,16 +673,16 @@ async function exportHTML() { ${htmlContent} `; - + // Create and download HTML file const blob = new Blob([completeHTML], { type: 'text/html' }); - + // Generate filename const filename = currentFileData?.filename || 'document'; const baseName = filename.replace(/\.[^/.]+$/, ''); // Remove extension const cleanBaseName = baseName.split('/').pop().split('\\').pop(); // Remove path const htmlFilename = `${cleanBaseName}.html`; - + // Download using Chrome downloads API try { const dataUrl = await new Promise((resolve) => { @@ -704,13 +690,13 @@ async function exportHTML() { reader.onload = () => resolve(reader.result); reader.readAsDataURL(blob); }); - + const response = await chrome.runtime.sendMessage({ action: 'downloadFile', url: dataUrl, - filename: htmlFilename, + filename: htmlFilename }); - + if (response?.success) { console.log('HTML file downloaded:', htmlFilename); // Hide dropdown after successful download @@ -730,4 +716,4 @@ chrome.runtime.onMessage.addListener(async (request, _, sendResponse) => { if (handler) { return await handler(request, sendResponse); } -}); +}); \ No newline at end of file diff --git a/demos/chrome-extension/chrome-extension/docx-validator.js b/demos/chrome-extension/chrome-extension/docx-validator.js index 969810a9b0..453556d6cf 100644 --- a/demos/chrome-extension/chrome-extension/docx-validator.js +++ b/demos/chrome-extension/chrome-extension/docx-validator.js @@ -22,17 +22,17 @@ function validateAndCorrectDocx(docxBlob) { try { const zip = new JSZip(); const docxContent = await zip.loadAsync(docxBlob); - - const documentXml = await docxContent.file('word/document.xml').async('text'); - const stylesXml = (await docxContent.file('word/styles.xml')?.async('text')) || ''; - + + const documentXml = await docxContent.file("word/document.xml").async("text"); + const stylesXml = await docxContent.file("word/styles.xml")?.async("text") || ""; + const correctedDocumentXml = validateAndCorrectStructure(documentXml); const correctedStyles = validateAndCorrectStyles(stylesXml); - - docxContent.file('word/document.xml', correctedDocumentXml); - docxContent.file('word/styles.xml', correctedStyles); - - const correctedBlob = await docxContent.generateAsync({ type: 'blob' }); + + docxContent.file("word/document.xml", correctedDocumentXml); + docxContent.file("word/styles.xml", correctedStyles); + + const correctedBlob = await docxContent.generateAsync({ type: "blob" }); resolve(correctedBlob); } catch (error) { reject(error); @@ -43,121 +43,115 @@ function validateAndCorrectDocx(docxBlob) { function validateAndCorrectStructure(documentXml) { const parser = new XMLParser({ ignoreAttributes: false, - attributeNamePrefix: '@_', + attributeNamePrefix: "@_", parseAttributeValue: false, parseTrueNumberOnly: false, preserveOrder: false, - trimValues: false, + trimValues: false }); - + const builder = new XMLBuilder({ ignoreAttributes: false, - attributeNamePrefix: '@_', + attributeNamePrefix: "@_", format: true, preserveOrder: false, - suppressEmptyNode: false, + suppressEmptyNode: false }); - + try { const doc = parser.parse(documentXml); - - if (!doc['w:document'] || !doc['w:document']['w:body']) { + + if (!doc["w:document"] || !doc["w:document"]["w:body"]) { return documentXml; } - - const body = doc['w:document']['w:body']; - + + const body = doc["w:document"]["w:body"]; + // Only add missing elements, don't modify existing structure - if (!body['w:p'] && !body['w:tbl'] && !body['w:sectPr']) { + if (!body["w:p"] && !body["w:tbl"] && !body["w:sectPr"]) { // Only if completely empty, add a paragraph with proper structure - body['w:p'] = [ - { - 'w:pPr': {}, - 'w:r': [{ 'w:t': '' }], - }, - ]; + body["w:p"] = [{ + "w:pPr": {}, + "w:r": [{ "w:t": "" }] + }]; } - + // Ensure paragraphs have minimum required structure only if they're broken - if (body['w:p']) { - if (!Array.isArray(body['w:p'])) { - body['w:p'] = [body['w:p']]; + if (body["w:p"]) { + if (!Array.isArray(body["w:p"])) { + body["w:p"] = [body["w:p"]]; } - - body['w:p'].forEach((paragraph) => { + + body["w:p"].forEach(paragraph => { // Ensure paragraph properties exist for proper formatting if (paragraph && typeof paragraph === 'object') { - if (!paragraph['w:pPr']) { - paragraph['w:pPr'] = {}; + if (!paragraph["w:pPr"]) { + paragraph["w:pPr"] = {}; } - + // Only fix completely broken paragraphs (no content at all) - if ( - !paragraph['w:r'] && - !paragraph['w:hyperlink'] && - !paragraph['w:fldSimple'] && - !paragraph['w:bookmarkStart'] && - !paragraph['w:bookmarkEnd'] - ) { - paragraph['w:r'] = [{ 'w:t': '' }]; + if (!paragraph["w:r"] && !paragraph["w:hyperlink"] && !paragraph["w:fldSimple"] && + !paragraph["w:bookmarkStart"] && !paragraph["w:bookmarkEnd"]) { + paragraph["w:r"] = [{ "w:t": "" }]; } } }); } - + // Ensure page margins and size - if (!body['w:sectPr']) { - body['w:sectPr'] = { - 'w:pgSz': { - '@_w:w': '12240', // 8.5 inches = 12240 twips - '@_w:h': '15840', // 11 inches = 15840 twips (US Letter) - }, - 'w:pgMar': { - '@_w:top': '1440', - '@_w:right': '1440', - '@_w:bottom': '1440', - '@_w:left': '1440', - '@_w:header': '720', - '@_w:footer': '720', - '@_w:gutter': '0', + if (!body["w:sectPr"]) { + body["w:sectPr"] = { + "w:pgSz": { + "@_w:w": "12240", // 8.5 inches = 12240 twips + "@_w:h": "15840" // 11 inches = 15840 twips (US Letter) }, + "w:pgMar": { + "@_w:top": "1440", + "@_w:right": "1440", + "@_w:bottom": "1440", + "@_w:left": "1440", + "@_w:header": "720", + "@_w:footer": "720", + "@_w:gutter": "0" + } }; } else { - const sectPr = body['w:sectPr']; - + const sectPr = body["w:sectPr"]; + // Ensure page size - if (!sectPr['w:pgSz']) { - sectPr['w:pgSz'] = { - '@_w:w': '12240', // 8.5 inches = 12240 twips - '@_w:h': '15840', // 11 inches = 15840 twips (US Letter) + if (!sectPr["w:pgSz"]) { + sectPr["w:pgSz"] = { + "@_w:w": "12240", // 8.5 inches = 12240 twips + "@_w:h": "15840" // 11 inches = 15840 twips (US Letter) }; } else { - const pgSz = sectPr['w:pgSz']; - if (!pgSz['@_w:w']) pgSz['@_w:w'] = '12240'; - if (!pgSz['@_w:h']) pgSz['@_w:h'] = '15840'; + const pgSz = sectPr["w:pgSz"]; + if (!pgSz["@_w:w"]) pgSz["@_w:w"] = "12240"; + if (!pgSz["@_w:h"]) pgSz["@_w:h"] = "15840"; } - + // Ensure page margins - if (!sectPr['w:pgMar']) { - sectPr['w:pgMar'] = { - '@_w:top': '1440', - '@_w:right': '1440', - '@_w:bottom': '1440', - '@_w:left': '1440', - '@_w:header': '720', - '@_w:footer': '720', - '@_w:gutter': '0', + if (!sectPr["w:pgMar"]) { + sectPr["w:pgMar"] = { + "@_w:top": "1440", + "@_w:right": "1440", + "@_w:bottom": "1440", + "@_w:left": "1440", + "@_w:header": "720", + "@_w:footer": "720", + "@_w:gutter": "0" }; } else { - const pgMar = sectPr['w:pgMar']; - if (!pgMar['@_w:top']) pgMar['@_w:top'] = '1440'; - if (!pgMar['@_w:right']) pgMar['@_w:right'] = '1440'; - if (!pgMar['@_w:bottom']) pgMar['@_w:bottom'] = '1440'; - if (!pgMar['@_w:left']) pgMar['@_w:left'] = '1440'; + const pgMar = sectPr["w:pgMar"]; + if (!pgMar["@_w:top"]) pgMar["@_w:top"] = "1440"; + if (!pgMar["@_w:right"]) pgMar["@_w:right"] = "1440"; + if (!pgMar["@_w:bottom"]) pgMar["@_w:bottom"] = "1440"; + if (!pgMar["@_w:left"]) pgMar["@_w:left"] = "1440"; } } - + return builder.build(doc); + } catch (error) { console.error('Error parsing document XML:', error); return documentXml; @@ -171,87 +165,89 @@ function validateAndCorrectStyles(stylesXml) { const parser = new XMLParser({ ignoreAttributes: false, - attributeNamePrefix: '@_', + attributeNamePrefix: "@_", parseAttributeValue: false, - parseTrueNumberOnly: false, + parseTrueNumberOnly: false }); - + const builder = new XMLBuilder({ ignoreAttributes: false, - attributeNamePrefix: '@_', - format: true, + attributeNamePrefix: "@_", + format: true }); - + try { const stylesDoc = parser.parse(stylesXml); - - if (!stylesDoc['w:styles']) { + + if (!stylesDoc["w:styles"]) { return DEFAULT_STYLES; } - - const styles = stylesDoc['w:styles']; - + + const styles = stylesDoc["w:styles"]; + // Ensure styles array exists - if (!styles['w:style']) { - styles['w:style'] = []; - } else if (!Array.isArray(styles['w:style'])) { - styles['w:style'] = [styles['w:style']]; + if (!styles["w:style"]) { + styles["w:style"] = []; + } else if (!Array.isArray(styles["w:style"])) { + styles["w:style"] = [styles["w:style"]]; } - + // Find Normal style - const normalStyle = styles['w:style'].find((style) => style['@_w:styleId'] === 'Normal'); - + const normalStyle = styles["w:style"].find(style => + style["@_w:styleId"] === "Normal" + ); + if (!normalStyle) { // Add Normal style - styles['w:style'].push({ - '@_w:type': 'paragraph', - '@_w:default': '1', - '@_w:styleId': 'Normal', - 'w:name': { '@_w:val': 'Normal' }, - 'w:qFormat': {}, - 'w:pPr': { - 'w:spacing': { - '@_w:after': '0', - '@_w:line': '276', - '@_w:lineRule': 'auto', - }, + styles["w:style"].push({ + "@_w:type": "paragraph", + "@_w:default": "1", + "@_w:styleId": "Normal", + "w:name": { "@_w:val": "Normal" }, + "w:qFormat": {}, + "w:pPr": { + "w:spacing": { + "@_w:after": "0", + "@_w:line": "276", + "@_w:lineRule": "auto" + } }, - 'w:rPr': { - 'w:rFonts': { - '@_w:ascii': 'Times New Roman', - '@_w:eastAsia': 'Times New Roman', - '@_w:hAnsi': 'Times New Roman', - '@_w:cs': 'Times New Roman', + "w:rPr": { + "w:rFonts": { + "@_w:ascii": "Times New Roman", + "@_w:eastAsia": "Times New Roman", + "@_w:hAnsi": "Times New Roman", + "@_w:cs": "Times New Roman" }, - 'w:sz': { '@_w:val': '24' }, - 'w:szCs': { '@_w:val': '24' }, - }, + "w:sz": { "@_w:val": "24" }, + "w:szCs": { "@_w:val": "24" } + } }); } else { // Ensure Normal style has required properties - if (!normalStyle['w:rPr']) { - normalStyle['w:rPr'] = {}; + if (!normalStyle["w:rPr"]) { + normalStyle["w:rPr"] = {}; } - - if (!normalStyle['w:rPr']['w:rFonts']) { - normalStyle['w:rPr']['w:rFonts'] = { - '@_w:ascii': 'Times New Roman', - '@_w:eastAsia': 'Times New Roman', - '@_w:hAnsi': 'Times New Roman', - '@_w:cs': 'Times New Roman', + + if (!normalStyle["w:rPr"]["w:rFonts"]) { + normalStyle["w:rPr"]["w:rFonts"] = { + "@_w:ascii": "Times New Roman", + "@_w:eastAsia": "Times New Roman", + "@_w:hAnsi": "Times New Roman", + "@_w:cs": "Times New Roman" }; } - - if (!normalStyle['w:rPr']['w:sz']) { - normalStyle['w:rPr']['w:sz'] = { '@_w:val': '24' }; + + if (!normalStyle["w:rPr"]["w:sz"]) { + normalStyle["w:rPr"]["w:sz"] = { "@_w:val": "24" }; } } - - return builder.build(stylesDoc); + + return builder.build(stylesDoc); } catch (error) { console.error('Error parsing styles XML:', error); return DEFAULT_STYLES; } } -export { validateAndCorrectDocx }; +export { validateAndCorrectDocx }; \ No newline at end of file diff --git a/demos/chrome-extension/chrome-extension/lib/style.css b/demos/chrome-extension/chrome-extension/lib/style.css index 6857f8580a..7faac95b44 100644 --- a/demos/chrome-extension/chrome-extension/lib/style.css +++ b/demos/chrome-extension/chrome-extension/lib/style.css @@ -118,9 +118,9 @@ height: 16px; } .toolbar-icon__icon--highlight[data-v-6d7523ab] { - width: 16px; - margin-left: 3px; - margin-bottom: 1px; + width: 16px; + margin-left: 3px; + margin-bottom: 1px; } .toolbar-icon__icon[data-v-6d7523ab] svg { width: auto; /* needed for safari */ @@ -182,11 +182,11 @@ background-color: #dbdbdb; } .toolbar-button:hover .toolbar-icon.high-contrast[data-v-2518a3d1] { - color: #fff; + color: #fff; } .toolbar-button:hover.high-contrast[data-v-2518a3d1] { - background-color: #000; - color: #fff; + background-color: #000; + color: #fff; } .toolbar-button[data-v-2518a3d1]:active, .active[data-v-2518a3d1] { @@ -202,7 +202,7 @@ font-size: 15px; margin: 5px; } -.toolbar-icon + .dropdown-caret[data-v-2518a3d1] { +.toolbar-icon+.dropdown-caret[data-v-2518a3d1] { margin-left: 4px; } .left[data-v-2518a3d1], @@ -247,7 +247,7 @@ box-sizing: border-box; } .button-text-input.high-contrast[data-v-2518a3d1] { - background-color: #fff; + background-color: #fff; } .button-text-input[data-v-2518a3d1]::placeholder { color: #47484a; @@ -262,18 +262,18 @@ height: 10px; } @media (max-width: 1280px) { - .toolbar-item--doc-mode .button-label[data-v-2518a3d1] { +.toolbar-item--doc-mode .button-label[data-v-2518a3d1] { display: none; - } - .toolbar-item--doc-mode .toolbar-icon[data-v-2518a3d1] { +} +.toolbar-item--doc-mode .toolbar-icon[data-v-2518a3d1] { margin-right: 5px; - } - .toolbar-item--linked-styles[data-v-2518a3d1] { +} +.toolbar-item--linked-styles[data-v-2518a3d1] { width: auto !important; - } - .toolbar-item--linked-styles .button-label[data-v-2518a3d1] { +} +.toolbar-item--linked-styles .button-label[data-v-2518a3d1] { display: none; - } +} } .toolbar-separator[data-v-b240fc3a] { @@ -305,16 +305,16 @@ position: relative; } .overflow-menu_items[data-v-53852ffa] { - position: absolute; - width: 200px; - top: calc(100% + 3px); - right: 0; - padding: 4px 8px; - background-color: #fff; - border-radius: 8px; - z-index: 100; - box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.25); - box-sizing: border-box; + position: absolute; + width: 200px; + top: calc(100% + 3px); + right: 0; + padding: 4px 8px; + background-color: #fff; + border-radius: 8px; + z-index: 100; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.25); + box-sizing: border-box; } .superdoc-toolbar-overflow[data-v-53852ffa] { min-width: auto !important; @@ -322,11 +322,11 @@ flex-wrap: wrap; } @media (max-width: 300px) { - .overflow-menu_items[data-v-53852ffa] { +.overflow-menu_items[data-v-53852ffa] { right: auto; left: 0; transform: translateX(-50%); - } +} } .sd-editor-toolbar-dropdown { @@ -335,15 +335,15 @@ cursor: pointer; } .sd-editor-toolbar-dropdown.high-contrast .n-dropdown-option-body:hover::before, -.sd-editor-toolbar-dropdown.high-contrast .n-dropdown-option-body:hover::after { - background-color: #000 !important; + .sd-editor-toolbar-dropdown.high-contrast .n-dropdown-option-body:hover::after { + background-color: #000 !important; } .sd-editor-toolbar-dropdown.high-contrast .n-dropdown-option-body__label:hover { - color: #fff !important; + color: #fff !important; } .sd-editor-toolbar-dropdown .n-dropdown-option-body:hover::before, -.sd-editor-toolbar-dropdown .n-dropdown-option-body:hover::after { - background-color: #d8dee5 !important; + .sd-editor-toolbar-dropdown .n-dropdown-option-body:hover::after { + background-color: #d8dee5 !important; } .sd-editor-toolbar-tooltip, .sd-editor-toolbar-tooltip.n-popover { @@ -364,15 +364,15 @@ box-sizing: border-box; } @media (max-width: 1280px) { - .superdoc-toolbar-group-side[data-v-9d38c912] { +.superdoc-toolbar-group-side[data-v-9d38c912] { min-width: auto !important; - } +} } @media (max-width: 768px) { - .superdoc-toolbar[data-v-9d38c912] { +.superdoc-toolbar[data-v-9d38c912] { padding: 4px 10px; justify-content: inherit; - } +} } /* Add isolation styles */ @@ -464,12 +464,12 @@ opacity: 0.8; } @keyframes spin-4cf86e1f { - from { +from { transform: rotate(0deg); - } - to { +} +to { transform: rotate(360deg); - } +} } .ai-loader[data-v-4cf86e1f] { display: flex; @@ -505,7 +505,7 @@ white-space: break-spaces; -webkit-font-variant-ligatures: none; font-variant-ligatures: none; - font-feature-settings: 'liga' 0; /* the above doesn't seem to work in Edge */ + font-feature-settings: "liga" 0; /* the above doesn't seem to work in Edge */ } .ProseMirror pre { white-space: pre-wrap; @@ -513,37 +513,24 @@ .ProseMirror li { position: relative; } -.ProseMirror.header-footer-edit > p, -.ProseMirror.header-footer-edit li, -.ProseMirror.header-footer-edit span { - color: #7e7e7e !important; -} -.ProseMirror.header-footer-edit > p:before, -.ProseMirror.header-footer-edit > p:after, -.ProseMirror.header-footer-edit li:before, -.ProseMirror.header-footer-edit li:after, -.ProseMirror.header-footer-edit span:before, -.ProseMirror.header-footer-edit span:after { - color: #7e7e7e !important; -} -.ProseMirror.header-footer-edit > p img, -.ProseMirror.header-footer-edit > p a, -.ProseMirror.header-footer-edit li img, -.ProseMirror.header-footer-edit li a, -.ProseMirror.header-footer-edit span img, -.ProseMirror.header-footer-edit span a { - opacity: 0.5; -} +.ProseMirror.header-footer-edit > p, .ProseMirror.header-footer-edit li, .ProseMirror.header-footer-edit span { + color: #7e7e7e !important; + } +.ProseMirror.header-footer-edit > p:before, .ProseMirror.header-footer-edit > p:after, .ProseMirror.header-footer-edit li:before, .ProseMirror.header-footer-edit li:after, .ProseMirror.header-footer-edit span:before, .ProseMirror.header-footer-edit span:after { + color: #7e7e7e !important; + } +.ProseMirror.header-footer-edit > p img, .ProseMirror.header-footer-edit > p a, .ProseMirror.header-footer-edit li img, .ProseMirror.header-footer-edit li a, .ProseMirror.header-footer-edit span img, .ProseMirror.header-footer-edit span a { + opacity: .5; + } .ProseMirror.header-footer-edit .pagination-break-wrapper { - color: initial !important; -} + color: initial !important; + } .ProseMirror.header-footer-edit .pagination-break-wrapper span { - color: initial !important; -} -.ProseMirror.header-footer-edit .pagination-break-wrapper img, -.ProseMirror.header-footer-edit .pagination-break-wrapper a { - opacity: 1; -} + color: initial !important; + } +.ProseMirror.header-footer-edit .pagination-break-wrapper img, .ProseMirror.header-footer-edit .pagination-break-wrapper a { + opacity: 1; + } /** * Hide marker for indented lists. * If a list-item contains a list but doesn't contain a "p" tag with text. @@ -569,7 +556,7 @@ align-items: start; } .ProseMirror li[data-marker-type]::marker { - content: ''; + content: ""; } .ProseMirror li[data-marker-type]::before { content: attr(data-marker-type); @@ -592,8 +579,8 @@ caret-color: transparent; } /* See https://github.com/ProseMirror/prosemirror/issues/1421#issuecomment-1759320191 */ -.ProseMirror [draggable][contenteditable='false'] { - user-select: text; +.ProseMirror [draggable][contenteditable=false] { + user-select: text } .ProseMirror-selectednode { outline: 2px solid #8cf; @@ -603,12 +590,10 @@ li.ProseMirror-selectednode { outline: none; } li.ProseMirror-selectednode:after { - content: ''; + content: ""; position: absolute; left: -32px; - right: -2px; - top: -2px; - bottom: -2px; + right: -2px; top: -2px; bottom: -2px; border: 2px solid #8cf; pointer-events: none; } @@ -638,7 +623,7 @@ https://github.com/ProseMirror/prosemirror-tables/blob/master/demo/index.html */ .ProseMirror.resize-cursor { cursor: ew-resize; - cursor: col-resize; + cursor: col-resize } .ProseMirror .tableWrapper { --table-border-width: 1px; @@ -673,7 +658,7 @@ https://github.com/ProseMirror/prosemirror-tables/blob/master/demo/index.html } .ProseMirror th { font-weight: bold; - text-align: left; + text-align: left } .ProseMirror table .column-resize-handle { position: absolute; @@ -708,14 +693,14 @@ https://github.com/ProseMirror/prosemirror-tables/blob/master/demo/index.html display: none; } .ProseMirror .track-insert-dec.highlighted { - border-top: 1px dashed #00853d; - border-bottom: 1px dashed #00853d; - background-color: #399c7222; + border-top: 1px dashed #00853D; + border-bottom: 1px dashed #00853D; + background-color: #399C7222; } .ProseMirror .track-delete-dec.highlighted { - border-top: 1px dashed #cb0e47; - border-bottom: 1px dashed #cb0e47; - background-color: #cb0e4722; + border-top: 1px dashed #CB0E47; + border-bottom: 1px dashed #CB0E47; + background-color: #CB0E4722; text-decoration: line-through; text-decoration-thickness: 2px; } @@ -764,7 +749,7 @@ https://github.com/ProseMirror/prosemirror-tables/blob/master/demo/index.html color: #ccc; } .ProseMirror placeholder:after { - content: '☁'; + content: "☁"; font-size: 200%; line-height: 0.1; font-weight: bold; @@ -777,7 +762,7 @@ https://github.com/ProseMirror/prosemirror-tables/blob/master/demo/index.html margin: 0; } .ProseMirror-gapcursor:after { - content: ''; + content: ""; display: block; position: absolute; top: -2px; @@ -793,10 +778,10 @@ https://github.com/ProseMirror/prosemirror-tables/blob/master/demo/index.html .ProseMirror-focused .ProseMirror-gapcursor { display: block; } -.ProseMirror div[data-type='contentBlock'] { - position: absolute; - outline: none; - user-select: none; +.ProseMirror div[data-type="contentBlock"] { + position: absolute; + outline: none; + user-select: none; z-index: -1; } .sd-editor-dropcap { @@ -818,8 +803,8 @@ https://github.com/ProseMirror/prosemirror-tables/blob/master/demo/index.html fill: currentColor; } .superdoc-toolbar svg path { - stroke: currentColor; -} + stroke: currentColor; + } .sd-editor-toolbar-dropdown .n-dropdown-option .dropdown-select-icon { display: flex; width: 12px; @@ -835,7 +820,7 @@ https://github.com/ProseMirror/prosemirror-tables/blob/master/demo/index.html fill: transparent; } .toolbar-icon__icon--ai::before { - content: ''; + content: ""; position: absolute; top: 0; left: 0; @@ -849,10 +834,8 @@ https://github.com/ProseMirror/prosemirror-tables/blob/master/demo/index.html rgba(77, 82, 217, 1) 60%, rgb(255, 219, 102) 150% ); - -webkit-mask: url("data:image/svg+xml;charset=utf-8,") - center / contain no-repeat; - mask: url("data:image/svg+xml;charset=utf-8,") - center / contain no-repeat; + -webkit-mask: url("data:image/svg+xml;charset=utf-8,") center / contain no-repeat; + mask: url("data:image/svg+xml;charset=utf-8,") center / contain no-repeat; filter: brightness(1.2); transition: filter 0.2s ease; } @@ -873,7 +856,7 @@ https://github.com/ProseMirror/prosemirror-tables/blob/master/demo/index.html .sd-ai-text-appear { display: inline; opacity: 0; - animation: aiTextAppear 0.7s ease-out forwards; + animation: aiTextAppear .7s ease-out forwards; animation-fill-mode: both; will-change: opacity, transform; /* Ensure each mark is treated as a separate animation context */ @@ -904,7 +887,7 @@ https://github.com/ProseMirror/prosemirror-tables/blob/master/demo/index.html .sd-editor-auto-page-number, .sd-editor-auto-total-pages { transition: all 250ms ease; - border-bottom: 1px solid #9a9a9a; + border-bottom: 1px solid #9A9A9A; cursor: not-allowed; } .sd-editor-auto-page-numbe:hover, @@ -943,8 +926,8 @@ https://github.com/ProseMirror/prosemirror-tables/blob/master/demo/index.html min-height: var(--sd-editor-separator-height); min-width: 100%; width: 100%; - border-top: 1px solid #dbdbdb; - border-bottom: 1px solid #dbdbdb; + border-top: 1px solid #DBDBDB; + border-bottom: 1px solid #DBDBDB; cursor: default; } .pagination-separator--table { @@ -953,8 +936,8 @@ https://github.com/ProseMirror/prosemirror-tables/blob/master/demo/index.html .pagination-separator-floating { position: absolute; height: var(--sd-editor-separator-height); - border-top: 1px solid #dbdbdb; - border-bottom: 1px solid #dbdbdb; + border-top: 1px solid #DBDBDB; + border-bottom: 1px solid #DBDBDB; pointer-events: none; } .pagination-inner { @@ -969,15 +952,18 @@ https://github.com/ProseMirror/prosemirror-tables/blob/master/demo/index.html Workaround to display pagination in footer on the right if it is inside shape textbox. */ -.pagination-section-footer .sd-editor-shape-container:has([data-id='auto-page-number'], [data-id='auto-total-pages']) { +.pagination-section-footer .sd-editor-shape-container:has( + [data-id="auto-page-number"], + [data-id="auto-total-pages"] +) { margin-left: auto; } -.pagination-section-header img[contenteditable='false'], -.pagination-section-footer img[contenteditable='false'] { +.pagination-section-header img[contenteditable="false"], +.pagination-section-footer img[contenteditable="false"] { pointer-events: none; } -.pagination-section-header div[contenteditable='false']:not([documentmode='viewing']), -.pagination-section-footer div[contenteditable='false']:not([documentmode='viewing']) { +.pagination-section-header div[contenteditable="false"]:not([documentmode="viewing"]), +.pagination-section-footer div[contenteditable="false"]:not([documentmode="viewing"]) { opacity: 0.5; } .sd-editor-popover { @@ -1032,7 +1018,7 @@ on the right if it is inside shape textbox. transition: background-color 250ms ease; } .sd-editor-comment-highlight:hover { - background-color: #1354ff55; + background-color: #1354FF55; } /* Resize handles container */ .sd-editor-resize-container { @@ -1119,29 +1105,29 @@ on the right if it is inside shape textbox. box-sizing: border-box; } .alignment-buttons .button-icon[data-v-3f28435a] { - cursor: pointer; - padding: 5px; - font-size: 16px; - width: 25px; - height: 25px; - border-radius: 4px; - display: flex; - justify-content: center; - align-items: center; - box-sizing: border-box; + cursor: pointer; + padding: 5px; + font-size: 16px; + width: 25px; + height: 25px; + border-radius: 4px; + display: flex; + justify-content: center; + align-items: center; + box-sizing: border-box; } .alignment-buttons .button-icon[data-v-3f28435a]:hover { - background-color: #d8dee5; + background-color: #d8dee5; } .alignment-buttons .button-icon[data-v-3f28435a] svg { - width: 100%; - height: 100%; - display: block; - fill: currentColor; + width: 100%; + height: 100%; + display: block; + fill: currentColor; } .alignment-buttons.high-contrast .button-icon[data-v-3f28435a]:hover { - background-color: #000; - color: #fff; + background-color: #000; + color: #fff; } .link-input-ctn[data-v-64aa14bc] { @@ -1154,32 +1140,32 @@ on the right if it is inside shape textbox. box-sizing: border-box; } .link-input-ctn[data-v-64aa14bc] svg { - width: 100%; - height: 100%; - display: block; - fill: currentColor; + width: 100%; + height: 100%; + display: block; + fill: currentColor; } .link-input-ctn .input-row[data-v-64aa14bc] { - align-content: baseline; - display: flex; - align-items: center; - font-size: 16px; + align-content: baseline; + display: flex; + align-items: center; + font-size: 16px; } .link-input-ctn .input-row input[data-v-64aa14bc] { - font-size: 13px; - flex-grow: 1; - padding: 10px; - border-radius: 8px; - padding-left: 32px; - box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.15); - color: #666; - border: 1px solid #ddd; - box-sizing: border-box; + font-size: 13px; + flex-grow: 1; + padding: 10px; + border-radius: 8px; + padding-left: 32px; + box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.15); + color: #666; + border: 1px solid #ddd; + box-sizing: border-box; } .link-input-ctn .input-row input[data-v-64aa14bc]:active, -.link-input-ctn .input-row input[data-v-64aa14bc]:focus { - outline: none; - border: 1px solid #1355ff; + .link-input-ctn .input-row input[data-v-64aa14bc]:focus { + outline: none; + border: 1px solid #1355ff; } .link-input-ctn .input-icon[data-v-64aa14bc] { position: absolute; @@ -1191,11 +1177,11 @@ on the right if it is inside shape textbox. pointer-events: none; } .link-input-ctn.high-contrast .input-icon[data-v-64aa14bc] { - color: #000; + color: #000; } .link-input-ctn.high-contrast .input-row input[data-v-64aa14bc] { - color: #000; - border-color: #000; + color: #000; + border-color: #000; } .open-link-icon[data-v-64aa14bc] { margin-left: 10px; @@ -1299,7 +1285,7 @@ on the right if it is inside shape textbox. background-color: black; } */ .submit-btn[data-v-64aa14bc]:hover { - background-color: #0d47c1; + background-color: #0d47c1; } .error[data-v-64aa14bc] { border-color: red !important; @@ -1309,14 +1295,14 @@ on the right if it is inside shape textbox. cursor: pointer; } -.document-mode[data-v-7febe4d9] { +.document-mode[data-v-7febe4d9] { display: flex; flex-direction: column; padding: 10px; box-sizing: border-box; } .document-mode[data-v-7febe4d9] svg { - width: 100%; + width: 100%; height: 100%; display: block; fill: currentColor; @@ -1331,18 +1317,18 @@ on the right if it is inside shape textbox. box-sizing: border-box; } .document-mode .option-item[data-v-7febe4d9]:hover { - background-color: #c8d0d8; + background-color: #c8d0d8; } .document-mode.high-contrast .option-item[data-v-7febe4d9]:hover { - background-color: #000; - color: #fff; + background-color: #000; + color: #fff; } .document-mode.high-contrast .option-item:hover .icon-column__icon[data-v-7febe4d9] { - color: #fff; + color: #fff; } -.document-mode.high-contrast .option-item:hover .text-column > .document-mode-type[data-v-7febe4d9], -.document-mode.high-contrast .option-item:hover .text-column > .document-mode-description[data-v-7febe4d9] { - color: #fff; +.document-mode.high-contrast .option-item:hover .text-column >.document-mode-type[data-v-7febe4d9], + .document-mode.high-contrast .option-item:hover .text-column >.document-mode-description[data-v-7febe4d9] { + color: #fff; } .disabled[data-v-7febe4d9] { opacity: 0.5; @@ -1368,12 +1354,12 @@ on the right if it is inside shape textbox. box-sizing: border-box; } .icon-column__icon[data-v-7febe4d9] { - display: inline-flex; - justify-content: center; - align-items: center; - flex-shrink: 0; - height: 18px; - color: #47484a; + display: inline-flex; + justify-content: center; + align-items: center; + flex-shrink: 0; + height: 18px; + color: #47484a; } .icon-column__icon[data-v-7febe4d9] svg { width: auto; @@ -1464,7 +1450,7 @@ on the right if it is inside shape textbox. padding: 4px; } .none-option[data-v-a00a9a3e]:hover { - opacity: 0.65; + opacity: 0.65; } .none-icon[data-v-a00a9a3e] { width: 16px; @@ -1477,9 +1463,9 @@ on the right if it is inside shape textbox. box-sizing: border-box; } .option-grid-ctn__subtitle[data-v-a00a9a3e] { - padding: 3px; - font-size: 12px; - font-weight: 600; + padding: 3px; + font-size: 12px; + font-weight: 600; } .option-grid-ctn[data-v-a00a9a3e] svg { width: 100%; @@ -1489,32 +1475,32 @@ on the right if it is inside shape textbox. } .toolbar-table-grid-wrapper .toolbar-table-grid[data-v-3e1154b8] { - display: grid; - grid-template-columns: repeat(5, 1fr); - gap: 2px; - padding: 8px; - box-sizing: border-box; + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 2px; + padding: 8px; + box-sizing: border-box; } .toolbar-table-grid-wrapper .toolbar-table-grid__item[data-v-3e1154b8] { - width: 20px; - height: 20px; - border: 1px solid #d3d3d3; - cursor: pointer; - transition: all 0.15s; + width: 20px; + height: 20px; + border: 1px solid #d3d3d3; + cursor: pointer; + transition: all .15s; } .toolbar-table-grid-wrapper .toolbar-table-grid__item.selected[data-v-3e1154b8] { - background-color: #dbdbdb; + background-color: #dbdbdb; } .toolbar-table-grid-wrapper.high-contrast .toolbar-table-grid__item[data-v-3e1154b8] { - border-color: #000; + border-color: #000; } .toolbar-table-grid-wrapper.high-contrast .toolbar-table-grid__item.selected[data-v-3e1154b8] { - background: #000; + background: #000; } .toolbar-table-grid-wrapper .toolbar-table-grid-value[data-v-3e1154b8] { - font-size: 13px; - line-height: 1.1; - padding: 0px 8px 2px; + font-size: 13px; + line-height: 1.1; + padding: 0px 8px 2px; } .toolbar-table-actions[data-v-f82e859a] { @@ -1586,123 +1572,49 @@ on the right if it is inside shape textbox. border-radius: 5px; } .search-input-ctn .search-input[data-v-af870fbd] { - min-width: 200px; - font-size: 13px; - flex-grow: 1; - padding: 10px; - border-radius: 8px; - color: #666; - border: 1px solid #ddd; - box-sizing: border-box; - box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.15); + min-width: 200px; + font-size: 13px; + flex-grow: 1; + padding: 10px; + border-radius: 8px; + color: #666; + border: 1px solid #ddd; + box-sizing: border-box; + box-shadow: 0 4px 12px 0 rgba(0, 0, 0, .15); } -.search-input-ctn .search-input[data-v-af870fbd]:active, -.search-input-ctn .search-input[data-v-af870fbd]:focus { - outline: none; - border: 1px solid #1355ff; +.search-input-ctn .search-input[data-v-af870fbd]:active, .search-input-ctn .search-input[data-v-af870fbd]:focus { + outline: none; + border: 1px solid #1355ff; } .search-input-ctn .row[data-v-af870fbd] { - display: flex; + display: flex; } .search-input-ctn .row.submit[data-v-af870fbd] { - margin-top: 10px; - flex-direction: row-reverse; + margin-top: 10px; + flex-direction: row-reverse; } .search-input-ctn .submit-btn[data-v-af870fbd] { - display: flex; - justify-content: center; - align-items: center; - padding: 10px 16px; - border-radius: 8px; - outline: none; - border: none; - background-color: #1355ff; - color: white; - font-weight: 400; - font-size: 13px; - cursor: pointer; - transition: all 0.2s ease; - box-sizing: border-box; -} -.tippy-box[data-animation='fade'][data-state='hidden'] { - opacity: 0; -} -[data-tippy-root] { - max-width: calc(100vw - 10px); -} -.tippy-box { - position: relative; - background-color: #333; - color: #fff; - border-radius: 4px; - font-size: 14px; - line-height: 1.4; - white-space: normal; - outline: 0; - transition-property: transform, visibility, opacity; -} -.tippy-box[data-placement^='top'] > .tippy-arrow { - bottom: 0; -} -.tippy-box[data-placement^='top'] > .tippy-arrow:before { - bottom: -7px; - left: 0; - border-width: 8px 8px 0; - border-top-color: initial; - transform-origin: center top; -} -.tippy-box[data-placement^='bottom'] > .tippy-arrow { - top: 0; -} -.tippy-box[data-placement^='bottom'] > .tippy-arrow:before { - top: -7px; - left: 0; - border-width: 0 8px 8px; - border-bottom-color: initial; - transform-origin: center bottom; -} -.tippy-box[data-placement^='left'] > .tippy-arrow { - right: 0; -} -.tippy-box[data-placement^='left'] > .tippy-arrow:before { - border-width: 8px 0 8px 8px; - border-left-color: initial; - right: -7px; - transform-origin: center left; -} -.tippy-box[data-placement^='right'] > .tippy-arrow { - left: 0; -} -.tippy-box[data-placement^='right'] > .tippy-arrow:before { - left: -7px; - border-width: 8px 8px 8px 0; - border-right-color: initial; - transform-origin: center right; -} -.tippy-box[data-inertia][data-state='visible'] { - transition-timing-function: cubic-bezier(0.54, 1.5, 0.38, 1.11); -} -.tippy-arrow { - width: 16px; - height: 16px; - color: #333; -} -.tippy-arrow:before { - content: ''; - position: absolute; - border-color: transparent; - border-style: solid; -} -.tippy-content { - position: relative; - padding: 5px 9px; - z-index: 1; -} + display: flex; + justify-content: center; + align-items: center; + padding: 10px 16px; + border-radius: 8px; + outline: none; + border: none; + background-color: #1355ff; + color: white; + font-weight: 400; + font-size: 13px; + cursor: pointer; + transition: all 0.2s ease; + box-sizing: border-box; +} +.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{position:relative;background-color:#333;color:#fff;border-radius:4px;font-size:14px;line-height:1.4;white-space:normal;outline:0;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-7px;left:0;border-width:8px 8px 0;border-top-color:initial;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-7px;left:0;border-width:0 8px 8px;border-bottom-color:initial;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:initial;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-7px;border-width:8px 8px 8px 0;border-right-color:initial;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{width:16px;height:16px;color:#333}.tippy-arrow:before{content:"";position:absolute;border-color:transparent;border-style:solid}.tippy-content{position:relative;padding:5px 9px;z-index:1} .vertical-indicator[data-v-f63566b2] { position: absolute; height: 0px; min-width: 1px; - background-color: #aaa; + background-color: #AAA; top: 20px; z-index: 100; } @@ -1953,7 +1865,7 @@ span[data-v-53e13009] { transition: all 250ms ease; } .overflow-menu__icon[data-v-7e86617a]:hover { - background-color: #dbdbdb; + background-color: #DBDBDB; } .overflow-menu__icon[data-v-7e86617a] svg { width: 100%; @@ -1970,6 +1882,7 @@ span[data-v-53e13009] { height: 16px; } + .comment-entry[data-v-dd84b4db] { box-sizing: border-box; border-radius: 8px; @@ -2117,2680 +2030,2262 @@ span[data-v-53e13009] { * See the License for the specific language governing permissions and * limitations under the License. */ -.superdoc-pdf-viewer .dialog { - --dialog-bg-color: white; - --dialog-border-color: white; - --dialog-shadow: 0 2px 14px 0 rgb(58 57 68 / 0.2); - --text-primary-color: #15141a; - --text-secondary-color: #5b5b66; - --hover-filter: brightness(0.9); - --focus-ring-color: #0060df; - --focus-ring-outline: 2px solid var(--focus-ring-color); +.superdoc-pdf-viewer .dialog{ + --dialog-bg-color:white; + --dialog-border-color:white; + --dialog-shadow:0 2px 14px 0 rgb(58 57 68 / 0.2); + --text-primary-color:#15141a; + --text-secondary-color:#5b5b66; + --hover-filter:brightness(0.9); + --focus-ring-color:#0060df; + --focus-ring-outline:2px solid var(--focus-ring-color); - --textarea-border-color: #8f8f9d; - --textarea-bg-color: white; - --textarea-fg-color: var(--text-secondary-color); + --textarea-border-color:#8f8f9d; + --textarea-bg-color:white; + --textarea-fg-color:var(--text-secondary-color); - --radio-bg-color: #f0f0f4; - --radio-checked-bg-color: #fbfbfe; - --radio-border-color: #8f8f9d; - --radio-checked-border-color: #0060df; + --radio-bg-color:#f0f0f4; + --radio-checked-bg-color:#fbfbfe; + --radio-border-color:#8f8f9d; + --radio-checked-border-color:#0060df; - --button-secondary-bg-color: #f0f0f4; - --button-secondary-fg-color: var(--text-primary-color); - --button-secondary-border-color: var(--button-secondary-bg-color); - --button-secondary-hover-bg-color: var(--button-secondary-bg-color); - --button-secondary-hover-fg-color: var(--button-secondary-fg-color); - --button-secondary-hover-border-color: var(--button-secondary-hover-bg-color); + --button-secondary-bg-color:#f0f0f4; + --button-secondary-fg-color:var(--text-primary-color); + --button-secondary-border-color:var(--button-secondary-bg-color); + --button-secondary-hover-bg-color:var(--button-secondary-bg-color); + --button-secondary-hover-fg-color:var(--button-secondary-fg-color); + --button-secondary-hover-border-color:var(--button-secondary-hover-bg-color); - --button-primary-bg-color: #0060df; - --button-primary-fg-color: #fbfbfe; - --button-primary-hover-bg-color: var(--button-primary-bg-color); - --button-primary-hover-fg-color: var(--button-primary-fg-color); - --button-primary-hover-border-color: var(--button-primary-hover-bg-color); + --button-primary-bg-color:#0060df; + --button-primary-fg-color:#fbfbfe; + --button-primary-hover-bg-color:var(--button-primary-bg-color); + --button-primary-hover-fg-color:var(--button-primary-fg-color); + --button-primary-hover-border-color:var(--button-primary-hover-bg-color); - font: message-box; - font-size: 13px; - font-weight: 400; - line-height: 150%; - border-radius: 4px; - padding: 12px 16px; - border: 1px solid var(--dialog-border-color); - background: var(--dialog-bg-color); - color: var(--text-primary-color); - box-shadow: var(--dialog-shadow); -} -@media (prefers-color-scheme: dark) { - .superdoc-pdf-viewer .dialog { - --dialog-bg-color: #1c1b22; - --dialog-border-color: #1c1b22; - --dialog-shadow: 0 2px 14px 0 #15141a; - --text-primary-color: #fbfbfe; - --text-secondary-color: #cfcfd8; - --focus-ring-color: #0df; - --hover-filter: brightness(1.4); + font:message-box; + font-size:13px; + font-weight:400; + line-height:150%; + border-radius:4px; + padding:12px 16px; + border:1px solid var(--dialog-border-color); + background:var(--dialog-bg-color); + color:var(--text-primary-color); + box-shadow:var(--dialog-shadow); +} +@media (prefers-color-scheme: dark){ +.superdoc-pdf-viewer .dialog{ + --dialog-bg-color:#1c1b22; + --dialog-border-color:#1c1b22; + --dialog-shadow:0 2px 14px 0 #15141a; + --text-primary-color:#fbfbfe; + --text-secondary-color:#cfcfd8; + --focus-ring-color:#0df; + --hover-filter:brightness(1.4); - --textarea-bg-color: #42414d; + --textarea-bg-color:#42414d; - --radio-bg-color: #2b2a33; - --radio-checked-bg-color: #15141a; - --radio-checked-border-color: #0df; + --radio-bg-color:#2b2a33; + --radio-checked-bg-color:#15141a; + --radio-checked-border-color:#0df; - --button-secondary-bg-color: #2b2a33; - --button-primary-bg-color: #0df; - --button-primary-fg-color: #15141a; - } -} -@media screen and (forced-colors: active) { - .superdoc-pdf-viewer .dialog { - --dialog-bg-color: Canvas; - --dialog-border-color: CanvasText; - --dialog-shadow: none; - --text-primary-color: CanvasText; - --text-secondary-color: CanvasText; - --hover-filter: none; - --focus-ring-color: ButtonBorder; + --button-secondary-bg-color:#2b2a33; + --button-primary-bg-color:#0df; + --button-primary-fg-color:#15141a; +} +} +@media screen and (forced-colors: active){ +.superdoc-pdf-viewer .dialog{ + --dialog-bg-color:Canvas; + --dialog-border-color:CanvasText; + --dialog-shadow:none; + --text-primary-color:CanvasText; + --text-secondary-color:CanvasText; + --hover-filter:none; + --focus-ring-color:ButtonBorder; - --textarea-border-color: ButtonBorder; - --textarea-bg-color: Field; - --textarea-fg-color: ButtonText; + --textarea-border-color:ButtonBorder; + --textarea-bg-color:Field; + --textarea-fg-color:ButtonText; - --radio-bg-color: ButtonFace; - --radio-checked-bg-color: ButtonFace; - --radio-border-color: ButtonText; - --radio-checked-border-color: ButtonText; + --radio-bg-color:ButtonFace; + --radio-checked-bg-color:ButtonFace; + --radio-border-color:ButtonText; + --radio-checked-border-color:ButtonText; - --button-secondary-bg-color: ButtonFace; - --button-secondary-fg-color: ButtonText; - --button-secondary-border-color: ButtonText; - --button-secondary-hover-bg-color: AccentColor; - --button-secondary-hover-fg-color: AccentColorText; + --button-secondary-bg-color:ButtonFace; + --button-secondary-fg-color:ButtonText; + --button-secondary-border-color:ButtonText; + --button-secondary-hover-bg-color:AccentColor; + --button-secondary-hover-fg-color:AccentColorText; - --button-primary-bg-color: ButtonText; - --button-primary-fg-color: ButtonFace; - --button-primary-hover-bg-color: AccentColor; - --button-primary-hover-fg-color: AccentColorText; - } + --button-primary-bg-color:ButtonText; + --button-primary-fg-color:ButtonFace; + --button-primary-hover-bg-color:AccentColor; + --button-primary-hover-fg-color:AccentColorText; +} +} +.superdoc-pdf-viewer .dialog .mainContainer *:focus-visible{ + outline:var(--focus-ring-outline); + outline-offset:2px; +} +.superdoc-pdf-viewer .dialog .mainContainer .radio{ + display:flex; + flex-direction:column; + align-items:flex-start; + gap:4px; +} +.superdoc-pdf-viewer .dialog .mainContainer .radio > .radioButton{ + display:flex; + gap:8px; + align-self:stretch; + align-items:center; +} +.superdoc-pdf-viewer .dialog .mainContainer .radio > .radioButton input{ + -webkit-appearance:none; + -moz-appearance:none; + appearance:none; + box-sizing:border-box; + width:16px; + height:16px; + border-radius:50%; + background-color:var(--radio-bg-color); + border:1px solid var(--radio-border-color); +} +.superdoc-pdf-viewer .dialog .mainContainer .radio > .radioButton input:hover{ + filter:var(--hover-filter); +} +.superdoc-pdf-viewer .dialog .mainContainer .radio > .radioButton input:checked{ + background-color:var(--radio-checked-bg-color); + border:4px solid var(--radio-checked-border-color); +} +.superdoc-pdf-viewer .dialog .mainContainer .radio > .radioLabel{ + display:flex; + padding-inline-start:24px; + align-items:flex-start; + gap:10px; + align-self:stretch; +} +.superdoc-pdf-viewer .dialog .mainContainer .radio > .radioLabel > span{ + flex:1 0 0; + font-size:11px; + color:var(--text-secondary-color); +} +.superdoc-pdf-viewer .dialog .mainContainer button{ + border-radius:4px; + border:1px solid; + font:menu; + font-weight:600; + padding:4px 16px; + width:auto; + height:32px; +} +.superdoc-pdf-viewer .dialog .mainContainer button:hover{ + cursor:pointer; + filter:var(--hover-filter); +} +.superdoc-pdf-viewer .dialog .mainContainer button.secondaryButton{ + color:var(--button-secondary-fg-color); + background-color:var(--button-secondary-bg-color); + border-color:var(--button-secondary-border-color); +} +.superdoc-pdf-viewer .dialog .mainContainer button.secondaryButton:hover{ + color:var(--button-secondary-hover-fg-color); + background-color:var(--button-secondary-hover-bg-color); + border-color:var(--button-secondary-hover-border-color); +} +.superdoc-pdf-viewer .dialog .mainContainer button.primaryButton{ + color:var(--button-primary-hover-fg-color); + background-color:var(--button-primary-hover-bg-color); + border-color:var(--button-primary-hover-border-color); + opacity:1; +} +.superdoc-pdf-viewer .dialog .mainContainer button.primaryButton:hover{ + color:var(--button-primary-hover-fg-color); + background-color:var(--button-primary-hover-bg-color); + border-color:var(--button-primary-hover-border-color); +} +.superdoc-pdf-viewer .dialog .mainContainer textarea{ + font:inherit; + padding:8px; + resize:none; + margin:0; + box-sizing:border-box; + border-radius:4px; + border:1px solid var(--textarea-border-color); + background:var(--textarea-bg-color); + color:var(--textarea-fg-color); +} +.superdoc-pdf-viewer .dialog .mainContainer textarea:focus{ + outline-offset:0; + border-color:transparent; +} +.superdoc-pdf-viewer .dialog .mainContainer textarea:disabled{ + pointer-events:none; + opacity:0.4; +} +.superdoc-pdf-viewer .textLayer{ + position:absolute; + text-align:initial; + inset:0; + overflow:clip; + opacity:1; + line-height:1; + -webkit-text-size-adjust:none; + -moz-text-size-adjust:none; + text-size-adjust:none; + forced-color-adjust:none; + transform-origin:0 0; + caret-color:CanvasText; + z-index:0; +} +.superdoc-pdf-viewer .textLayer.highlighting{ + touch-action:none; +} +.superdoc-pdf-viewer .textLayer :is(span, br){ + color:transparent; + position:absolute; + white-space:pre; + cursor:text; + transform-origin:0% 0%; } -.superdoc-pdf-viewer .dialog .mainContainer *:focus-visible { - outline: var(--focus-ring-outline); - outline-offset: 2px; +.superdoc-pdf-viewer .textLayer > :not(.markedContent), + .superdoc-pdf-viewer .textLayer .markedContent span:not(.markedContent){ + z-index:1; +} +.superdoc-pdf-viewer .textLayer span.markedContent{ + top:0; + height:0; +} +.superdoc-pdf-viewer .textLayer .highlight{ + --highlight-bg-color:rgb(180 0 170 / 0.25); + --highlight-selected-bg-color:rgb(0 100 0 / 0.25); + --highlight-backdrop-filter:none; + --highlight-selected-backdrop-filter:none; + + margin:-1px; + padding:1px; + background-color:var(--highlight-bg-color); + -webkit-backdrop-filter:var(--highlight-backdrop-filter); + backdrop-filter:var(--highlight-backdrop-filter); + border-radius:4px; +} +@media screen and (forced-colors: active){ +.superdoc-pdf-viewer .textLayer .highlight{ + --highlight-bg-color:transparent; + --highlight-selected-bg-color:transparent; + --highlight-backdrop-filter:var(--hcm-highlight-filter); + --highlight-selected-backdrop-filter:var( + --hcm-highlight-selected-filter + ); +} +} +.superdoc-pdf-viewer .textLayer .highlight.appended{ + position:initial; +} +.superdoc-pdf-viewer .textLayer .highlight.begin{ + border-radius:4px 0 0 4px; +} +.superdoc-pdf-viewer .textLayer .highlight.end{ + border-radius:0 4px 4px 0; +} +.superdoc-pdf-viewer .textLayer .highlight.middle{ + border-radius:0; +} +.superdoc-pdf-viewer .textLayer .highlight.selected{ + background-color:var(--highlight-selected-bg-color); + -webkit-backdrop-filter:var(--highlight-selected-backdrop-filter); + backdrop-filter:var(--highlight-selected-backdrop-filter); +} +.superdoc-pdf-viewer .textLayer ::-moz-selection{ + background:rgba(0 0 255 / 0.25); + background:color-mix(in srgb, AccentColor, transparent 75%); +} +.superdoc-pdf-viewer .textLayer ::selection{ + background:rgba(0 0 255 / 0.25); + background:color-mix(in srgb, AccentColor, transparent 75%); +} +.superdoc-pdf-viewer .textLayer br::-moz-selection{ + background:transparent; +} +.superdoc-pdf-viewer .textLayer br::selection{ + background:transparent; +} +.superdoc-pdf-viewer .textLayer .endOfContent{ + display:block; + position:absolute; + inset:100% 0 0; + z-index:0; + cursor:default; + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; +} +.superdoc-pdf-viewer .textLayer .endOfContent.active{ + top:0; +} +.superdoc-pdf-viewer .annotationLayer{ + --annotation-unfocused-field-background:url("data:image/svg+xml;charset=UTF-8,"); + --input-focus-border-color:Highlight; + --input-focus-outline:1px solid Canvas; + --input-unfocused-border-color:transparent; + --input-disabled-border-color:transparent; + --input-hover-border-color:black; + --link-outline:none; + + position:absolute; + top:0; + left:0; + pointer-events:none; + transform-origin:0 0; +} +@media screen and (forced-colors: active){ +.superdoc-pdf-viewer .annotationLayer{ + --input-focus-border-color:CanvasText; + --input-unfocused-border-color:ActiveText; + --input-disabled-border-color:GrayText; + --input-hover-border-color:Highlight; + --link-outline:1.5px solid LinkText; +} +.superdoc-pdf-viewer .annotationLayer .textWidgetAnnotation :is(input, textarea):required, .superdoc-pdf-viewer .annotationLayer .choiceWidgetAnnotation select:required, .superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input:required{ + outline:1.5px solid selectedItem; +} +.superdoc-pdf-viewer .annotationLayer .linkAnnotation{ + outline:var(--link-outline); +} +.superdoc-pdf-viewer .annotationLayer .linkAnnotation:hover{ + -webkit-backdrop-filter:var(--hcm-highlight-filter); + backdrop-filter:var(--hcm-highlight-filter); +} +.superdoc-pdf-viewer .annotationLayer .linkAnnotation > a:hover{ + opacity:0 !important; + background:none !important; + box-shadow:none; +} +.superdoc-pdf-viewer .annotationLayer .popupAnnotation .popup{ + outline:calc(1.5px * var(--scale-factor)) solid CanvasText !important; + background-color:ButtonFace !important; + color:ButtonText !important; +} +.superdoc-pdf-viewer .annotationLayer .highlightArea:hover::after{ + position:absolute; + top:0; + left:0; + width:100%; + height:100%; + -webkit-backdrop-filter:var(--hcm-highlight-filter); + backdrop-filter:var(--hcm-highlight-filter); + content:""; + pointer-events:none; +} +.superdoc-pdf-viewer .annotationLayer .popupAnnotation.focused .popup{ + outline:calc(3px * var(--scale-factor)) solid Highlight !important; +} +} +.superdoc-pdf-viewer .annotationLayer[data-main-rotation="90"] .norotate{ + transform:rotate(270deg) translateX(-100%); +} +.superdoc-pdf-viewer .annotationLayer[data-main-rotation="180"] .norotate{ + transform:rotate(180deg) translate(-100%, -100%); +} +.superdoc-pdf-viewer .annotationLayer[data-main-rotation="270"] .norotate{ + transform:rotate(90deg) translateY(-100%); } -.superdoc-pdf-viewer .dialog .mainContainer .radio { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 4px; +.superdoc-pdf-viewer .annotationLayer.disabled section, + .superdoc-pdf-viewer .annotationLayer.disabled .popup{ + pointer-events:none; +} +.superdoc-pdf-viewer .annotationLayer .annotationContent{ + position:absolute; + width:100%; + height:100%; + pointer-events:none; +} +.superdoc-pdf-viewer .annotationLayer .annotationContent.freetext{ + background:transparent; + border:none; + inset:0; + overflow:visible; + white-space:nowrap; + font:10px sans-serif; + line-height:1.35; + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; +} +.superdoc-pdf-viewer .annotationLayer section{ + position:absolute; + text-align:initial; + pointer-events:auto; + box-sizing:border-box; + transform-origin:0 0; +} +.superdoc-pdf-viewer .annotationLayer section:has(div.annotationContent) canvas.annotationContent{ + display:none; +} +.superdoc-pdf-viewer .annotationLayer :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton) > a{ + position:absolute; + font-size:1em; + top:0; + left:0; + width:100%; + height:100%; +} +.superdoc-pdf-viewer .annotationLayer :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton):not(.hasBorder) + > a:hover{ + opacity:0.2; + background-color:rgb(255 255 0); + box-shadow:0 2px 10px rgb(255 255 0); +} +.superdoc-pdf-viewer .annotationLayer .linkAnnotation.hasBorder:hover{ + background-color:rgb(255 255 0 / 0.2); +} +.superdoc-pdf-viewer .annotationLayer .hasBorder{ + background-size:100% 100%; +} +.superdoc-pdf-viewer .annotationLayer .textAnnotation img{ + position:absolute; + cursor:pointer; + width:100%; + height:100%; + top:0; + left:0; +} +.superdoc-pdf-viewer .annotationLayer .textWidgetAnnotation :is(input, textarea), .superdoc-pdf-viewer .annotationLayer .choiceWidgetAnnotation select, .superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input{ + background-image:var(--annotation-unfocused-field-background); + border:2px solid var(--input-unfocused-border-color); + box-sizing:border-box; + font:calc(9px * var(--scale-factor)) sans-serif; + height:100%; + margin:0; + vertical-align:top; + width:100%; +} +.superdoc-pdf-viewer .annotationLayer .textWidgetAnnotation :is(input, textarea):required, .superdoc-pdf-viewer .annotationLayer .choiceWidgetAnnotation select:required, .superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input:required{ + outline:1.5px solid red; +} +.superdoc-pdf-viewer .annotationLayer .choiceWidgetAnnotation select option{ + padding:0; +} +.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation.radioButton input{ + border-radius:50%; +} +.superdoc-pdf-viewer .annotationLayer .textWidgetAnnotation textarea{ + resize:none; +} +.superdoc-pdf-viewer .annotationLayer .textWidgetAnnotation [disabled]:is(input, textarea), .superdoc-pdf-viewer .annotationLayer .choiceWidgetAnnotation select[disabled], .superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input[disabled]{ + background:none; + border:2px solid var(--input-disabled-border-color); + cursor:not-allowed; +} +.superdoc-pdf-viewer .annotationLayer .textWidgetAnnotation :is(input, textarea):hover, .superdoc-pdf-viewer .annotationLayer .choiceWidgetAnnotation select:hover, .superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input:hover{ + border:2px solid var(--input-hover-border-color); +} +.superdoc-pdf-viewer .annotationLayer .textWidgetAnnotation :is(input, textarea):hover, .superdoc-pdf-viewer .annotationLayer .choiceWidgetAnnotation select:hover, .superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation.checkBox input:hover{ + border-radius:2px; +} +.superdoc-pdf-viewer .annotationLayer .textWidgetAnnotation :is(input, textarea):focus, .superdoc-pdf-viewer .annotationLayer .choiceWidgetAnnotation select:focus{ + background:none; + border:2px solid var(--input-focus-border-color); + border-radius:2px; + outline:var(--input-focus-outline); +} +.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) :focus{ + background-image:none; + background-color:transparent; +} +.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation.checkBox :focus{ + border:2px solid var(--input-focus-border-color); + border-radius:2px; + outline:var(--input-focus-outline); +} +.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation.radioButton :focus{ + border:2px solid var(--input-focus-border-color); + outline:var(--input-focus-outline); } -.superdoc-pdf-viewer .dialog .mainContainer .radio > .radioButton { - display: flex; - gap: 8px; - align-self: stretch; - align-items: center; +.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation.checkBox input:checked::before, + .superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation.checkBox input:checked::after, + .superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation.radioButton input:checked::before{ + background-color:CanvasText; + content:""; + display:block; + position:absolute; } -.superdoc-pdf-viewer .dialog .mainContainer .radio > .radioButton input { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - box-sizing: border-box; - width: 16px; - height: 16px; - border-radius: 50%; - background-color: var(--radio-bg-color); - border: 1px solid var(--radio-border-color); +.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation.checkBox input:checked::before, + .superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation.checkBox input:checked::after{ + height:80%; + left:45%; + width:1px; +} +.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation.checkBox input:checked::before{ + transform:rotate(45deg); +} +.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation.checkBox input:checked::after{ + transform:rotate(-45deg); +} +.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation.radioButton input:checked::before{ + border-radius:50%; + height:50%; + left:25%; + top:25%; + width:50%; +} +.superdoc-pdf-viewer .annotationLayer .textWidgetAnnotation input.comb{ + font-family:monospace; + padding-left:2px; + padding-right:0; +} +.superdoc-pdf-viewer .annotationLayer .textWidgetAnnotation input.comb:focus{ + width:103%; +} +.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input{ + -webkit-appearance:none; + -moz-appearance:none; + appearance:none; +} +.superdoc-pdf-viewer .annotationLayer .fileAttachmentAnnotation .popupTriggerArea{ + height:100%; + width:100%; +} +.superdoc-pdf-viewer .annotationLayer .popupAnnotation{ + position:absolute; + font-size:calc(9px * var(--scale-factor)); + pointer-events:none; + width:-moz-max-content; + width:max-content; + max-width:45%; + height:auto; +} +.superdoc-pdf-viewer .annotationLayer .popup{ + background-color:rgb(255 255 153); + box-shadow:0 calc(2px * var(--scale-factor)) calc(5px * var(--scale-factor)) rgb(136 136 136); + border-radius:calc(2px * var(--scale-factor)); + outline:1.5px solid rgb(255 255 74); + padding:calc(6px * var(--scale-factor)); + cursor:pointer; + font:message-box; + white-space:normal; + word-wrap:break-word; + pointer-events:auto; +} +.superdoc-pdf-viewer .annotationLayer .popupAnnotation.focused .popup{ + outline-width:3px; +} +.superdoc-pdf-viewer .annotationLayer .popup *{ + font-size:calc(9px * var(--scale-factor)); +} +.superdoc-pdf-viewer .annotationLayer .popup > .header{ + display:inline-block; +} +.superdoc-pdf-viewer .annotationLayer .popup > .header h1{ + display:inline; +} +.superdoc-pdf-viewer .annotationLayer .popup > .header .popupDate{ + display:inline-block; + margin-left:calc(5px * var(--scale-factor)); + width:-moz-fit-content; + width:fit-content; +} +.superdoc-pdf-viewer .annotationLayer .popupContent{ + border-top:1px solid rgb(51 51 51); + margin-top:calc(2px * var(--scale-factor)); + padding-top:calc(2px * var(--scale-factor)); +} +.superdoc-pdf-viewer .annotationLayer .richText > *{ + white-space:pre-wrap; + font-size:calc(9px * var(--scale-factor)); +} +.superdoc-pdf-viewer .annotationLayer .popupTriggerArea{ + cursor:pointer; +} +.superdoc-pdf-viewer .annotationLayer section svg{ + position:absolute; + width:100%; + height:100%; + top:0; + left:0; +} +.superdoc-pdf-viewer .annotationLayer .annotationTextContent{ + position:absolute; + width:100%; + height:100%; + opacity:0; + color:transparent; + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; + pointer-events:none; +} +.superdoc-pdf-viewer .annotationLayer .annotationTextContent span{ + width:100%; + display:inline-block; +} +.superdoc-pdf-viewer .annotationLayer svg.quadrilateralsContainer{ + contain:strict; + width:0; + height:0; + position:absolute; + top:0; + left:0; + z-index:-1; +} +.superdoc-pdf-viewer :root{ + --xfa-unfocused-field-background:url("data:image/svg+xml;charset=UTF-8,"); + --xfa-focus-outline:auto; +} +@media screen and (forced-colors: active){ +.superdoc-pdf-viewer :root{ + --xfa-focus-outline:2px solid CanvasText; +} +.superdoc-pdf-viewer .xfaLayer *:required{ + outline:1.5px solid selectedItem; +} +} +.superdoc-pdf-viewer .xfaLayer{ + background-color:transparent; +} +.superdoc-pdf-viewer .xfaLayer .highlight{ + margin:-1px; + padding:1px; + background-color:rgb(239 203 237); + border-radius:4px; +} +.superdoc-pdf-viewer .xfaLayer .highlight.appended{ + position:initial; +} +.superdoc-pdf-viewer .xfaLayer .highlight.begin{ + border-radius:4px 0 0 4px; +} +.superdoc-pdf-viewer .xfaLayer .highlight.end{ + border-radius:0 4px 4px 0; +} +.superdoc-pdf-viewer .xfaLayer .highlight.middle{ + border-radius:0; +} +.superdoc-pdf-viewer .xfaLayer .highlight.selected{ + background-color:rgb(203 223 203); +} +.superdoc-pdf-viewer .xfaPage{ + overflow:hidden; + position:relative; +} +.superdoc-pdf-viewer .xfaContentarea{ + position:absolute; +} +.superdoc-pdf-viewer .xfaPrintOnly{ + display:none; +} +.superdoc-pdf-viewer .xfaLayer{ + position:absolute; + text-align:initial; + top:0; + left:0; + transform-origin:0 0; + line-height:1.2; +} +.superdoc-pdf-viewer .xfaLayer *{ + color:inherit; + font:inherit; + font-style:inherit; + font-weight:inherit; + font-kerning:inherit; + letter-spacing:-0.01px; + text-align:inherit; + text-decoration:inherit; + box-sizing:border-box; + background-color:transparent; + padding:0; + margin:0; + pointer-events:auto; + line-height:inherit; +} +.superdoc-pdf-viewer .xfaLayer *:required{ + outline:1.5px solid red; } -.superdoc-pdf-viewer .dialog .mainContainer .radio > .radioButton input:hover { - filter: var(--hover-filter); +.superdoc-pdf-viewer .xfaLayer div, +.superdoc-pdf-viewer .xfaLayer svg, +.superdoc-pdf-viewer .xfaLayer svg *{ + pointer-events:none; +} +.superdoc-pdf-viewer .xfaLayer a{ + color:blue; +} +.superdoc-pdf-viewer .xfaRich li{ + margin-left:3em; +} +.superdoc-pdf-viewer .xfaFont{ + color:black; + font-weight:normal; + font-kerning:none; + font-size:10px; + font-style:normal; + letter-spacing:0; + text-decoration:none; + vertical-align:0; +} +.superdoc-pdf-viewer .xfaCaption{ + overflow:hidden; + flex:0 0 auto; +} +.superdoc-pdf-viewer .xfaCaptionForCheckButton{ + overflow:hidden; + flex:1 1 auto; +} +.superdoc-pdf-viewer .xfaLabel{ + height:100%; + width:100%; +} +.superdoc-pdf-viewer .xfaLeft{ + display:flex; + flex-direction:row; + align-items:center; +} +.superdoc-pdf-viewer .xfaRight{ + display:flex; + flex-direction:row-reverse; + align-items:center; +} +.superdoc-pdf-viewer :is(.xfaLeft, .xfaRight) > :is(.xfaCaption, .xfaCaptionForCheckButton){ + max-height:100%; +} +.superdoc-pdf-viewer .xfaTop{ + display:flex; + flex-direction:column; + align-items:flex-start; +} +.superdoc-pdf-viewer .xfaBottom{ + display:flex; + flex-direction:column-reverse; + align-items:flex-start; +} +.superdoc-pdf-viewer :is(.xfaTop, .xfaBottom) > :is(.xfaCaption, .xfaCaptionForCheckButton){ + width:100%; +} +.superdoc-pdf-viewer .xfaBorder{ + background-color:transparent; + position:absolute; + pointer-events:none; +} +.superdoc-pdf-viewer .xfaWrapped{ + width:100%; + height:100%; +} +.superdoc-pdf-viewer :is(.xfaTextfield, .xfaSelect):focus{ + background-image:none; + background-color:transparent; + outline:var(--xfa-focus-outline); + outline-offset:-1px; +} +.superdoc-pdf-viewer :is(.xfaCheckbox, .xfaRadio):focus{ + outline:var(--xfa-focus-outline); } -.superdoc-pdf-viewer .dialog .mainContainer .radio > .radioButton input:checked { - background-color: var(--radio-checked-bg-color); - border: 4px solid var(--radio-checked-border-color); +.superdoc-pdf-viewer .xfaTextfield, +.superdoc-pdf-viewer .xfaSelect{ + height:100%; + width:100%; + flex:1 1 auto; + border:none; + resize:none; + background-image:var(--xfa-unfocused-field-background); +} +.superdoc-pdf-viewer .xfaSelect{ + padding-inline:2px; +} +.superdoc-pdf-viewer :is(.xfaTop, .xfaBottom) > :is(.xfaTextfield, .xfaSelect){ + flex:0 1 auto; +} +.superdoc-pdf-viewer .xfaButton{ + cursor:pointer; + width:100%; + height:100%; + border:none; + text-align:center; +} +.superdoc-pdf-viewer .xfaLink{ + width:100%; + height:100%; + position:absolute; + top:0; + left:0; } -.superdoc-pdf-viewer .dialog .mainContainer .radio > .radioLabel { - display: flex; - padding-inline-start: 24px; - align-items: flex-start; - gap: 10px; - align-self: stretch; +.superdoc-pdf-viewer .xfaCheckbox, +.superdoc-pdf-viewer .xfaRadio{ + width:100%; + height:100%; + flex:0 0 auto; + border:none; +} +.superdoc-pdf-viewer .xfaRich{ + white-space:pre-wrap; + width:100%; + height:100%; +} +.superdoc-pdf-viewer .xfaImage{ + -o-object-position:left top; + object-position:left top; + -o-object-fit:contain; + object-fit:contain; + width:100%; + height:100%; } -.superdoc-pdf-viewer .dialog .mainContainer .radio > .radioLabel > span { - flex: 1 0 0; - font-size: 11px; - color: var(--text-secondary-color); +.superdoc-pdf-viewer .xfaLrTb, +.superdoc-pdf-viewer .xfaRlTb, +.superdoc-pdf-viewer .xfaTb{ + display:flex; + flex-direction:column; + align-items:stretch; } -.superdoc-pdf-viewer .dialog .mainContainer button { - border-radius: 4px; - border: 1px solid; - font: menu; - font-weight: 600; - padding: 4px 16px; - width: auto; - height: 32px; +.superdoc-pdf-viewer .xfaLr{ + display:flex; + flex-direction:row; + align-items:stretch; } -.superdoc-pdf-viewer .dialog .mainContainer button:hover { - cursor: pointer; - filter: var(--hover-filter); -} -.superdoc-pdf-viewer .dialog .mainContainer button.secondaryButton { - color: var(--button-secondary-fg-color); - background-color: var(--button-secondary-bg-color); - border-color: var(--button-secondary-border-color); -} -.superdoc-pdf-viewer .dialog .mainContainer button.secondaryButton:hover { - color: var(--button-secondary-hover-fg-color); - background-color: var(--button-secondary-hover-bg-color); - border-color: var(--button-secondary-hover-border-color); -} -.superdoc-pdf-viewer .dialog .mainContainer button.primaryButton { - color: var(--button-primary-hover-fg-color); - background-color: var(--button-primary-hover-bg-color); - border-color: var(--button-primary-hover-border-color); - opacity: 1; +.superdoc-pdf-viewer .xfaRl{ + display:flex; + flex-direction:row-reverse; + align-items:stretch; } -.superdoc-pdf-viewer .dialog .mainContainer button.primaryButton:hover { - color: var(--button-primary-hover-fg-color); - background-color: var(--button-primary-hover-bg-color); - border-color: var(--button-primary-hover-border-color); +.superdoc-pdf-viewer .xfaTb > div{ + justify-content:left; } -.superdoc-pdf-viewer .dialog .mainContainer textarea { - font: inherit; - padding: 8px; - resize: none; - margin: 0; - box-sizing: border-box; - border-radius: 4px; - border: 1px solid var(--textarea-border-color); - background: var(--textarea-bg-color); - color: var(--textarea-fg-color); +.superdoc-pdf-viewer .xfaPosition{ + position:relative; } -.superdoc-pdf-viewer .dialog .mainContainer textarea:focus { - outline-offset: 0; - border-color: transparent; +.superdoc-pdf-viewer .xfaArea{ + position:relative; } -.superdoc-pdf-viewer .dialog .mainContainer textarea:disabled { - pointer-events: none; - opacity: 0.4; +.superdoc-pdf-viewer .xfaValignMiddle{ + display:flex; + align-items:center; } -.superdoc-pdf-viewer .textLayer { - position: absolute; - text-align: initial; - inset: 0; - overflow: clip; - opacity: 1; - line-height: 1; - -webkit-text-size-adjust: none; - -moz-text-size-adjust: none; - text-size-adjust: none; - forced-color-adjust: none; - transform-origin: 0 0; - caret-color: CanvasText; - z-index: 0; -} -.superdoc-pdf-viewer .textLayer.highlighting { - touch-action: none; -} -.superdoc-pdf-viewer .textLayer :is(span, br) { - color: transparent; - position: absolute; - white-space: pre; - cursor: text; - transform-origin: 0% 0%; +.superdoc-pdf-viewer .xfaTable{ + display:flex; + flex-direction:column; + align-items:stretch; } -.superdoc-pdf-viewer .textLayer > :not(.markedContent), -.superdoc-pdf-viewer .textLayer .markedContent span:not(.markedContent) { - z-index: 1; +.superdoc-pdf-viewer .xfaTable .xfaRow{ + display:flex; + flex-direction:row; + align-items:stretch; } -.superdoc-pdf-viewer .textLayer span.markedContent { - top: 0; - height: 0; +.superdoc-pdf-viewer .xfaTable .xfaRlRow{ + display:flex; + flex-direction:row-reverse; + align-items:stretch; + flex:1; } -.superdoc-pdf-viewer .textLayer .highlight { - --highlight-bg-color: rgb(180 0 170 / 0.25); - --highlight-selected-bg-color: rgb(0 100 0 / 0.25); - --highlight-backdrop-filter: none; - --highlight-selected-backdrop-filter: none; - - margin: -1px; - padding: 1px; - background-color: var(--highlight-bg-color); - -webkit-backdrop-filter: var(--highlight-backdrop-filter); - backdrop-filter: var(--highlight-backdrop-filter); - border-radius: 4px; +.superdoc-pdf-viewer .xfaTable .xfaRlRow > div{ + flex:1; } -@media screen and (forced-colors: active) { - .superdoc-pdf-viewer .textLayer .highlight { - --highlight-bg-color: transparent; - --highlight-selected-bg-color: transparent; - --highlight-backdrop-filter: var(--hcm-highlight-filter); - --highlight-selected-backdrop-filter: var(--hcm-highlight-selected-filter); - } +.superdoc-pdf-viewer :is(.xfaNonInteractive, .xfaDisabled, .xfaReadOnly) :is(input, textarea){ + background:initial; } -.superdoc-pdf-viewer .textLayer .highlight.appended { - position: initial; +@media print{ +.superdoc-pdf-viewer .xfaTextfield, + .superdoc-pdf-viewer .xfaSelect{ + background:transparent; } -.superdoc-pdf-viewer .textLayer .highlight.begin { - border-radius: 4px 0 0 4px; +.superdoc-pdf-viewer .xfaSelect{ + -webkit-appearance:none; + -moz-appearance:none; + appearance:none; + text-indent:1px; + text-overflow:""; } -.superdoc-pdf-viewer .textLayer .highlight.end { - border-radius: 0 4px 4px 0; } -.superdoc-pdf-viewer .textLayer .highlight.middle { - border-radius: 0; +.superdoc-pdf-viewer .canvasWrapper svg{ + transform:none; } -.superdoc-pdf-viewer .textLayer .highlight.selected { - background-color: var(--highlight-selected-bg-color); - -webkit-backdrop-filter: var(--highlight-selected-backdrop-filter); - backdrop-filter: var(--highlight-selected-backdrop-filter); +.superdoc-pdf-viewer .canvasWrapper svg[data-main-rotation="90"] mask, + .superdoc-pdf-viewer .canvasWrapper svg[data-main-rotation="90"] use:not(.clip, .mask){ + transform:matrix(0, 1, -1, 0, 1, 0); } -.superdoc-pdf-viewer .textLayer ::-moz-selection { - background: rgba(0 0 255 / 0.25); - background: color-mix(in srgb, AccentColor, transparent 75%); +.superdoc-pdf-viewer .canvasWrapper svg[data-main-rotation="180"] mask, + .superdoc-pdf-viewer .canvasWrapper svg[data-main-rotation="180"] use:not(.clip, .mask){ + transform:matrix(-1, 0, 0, -1, 1, 1); } -.superdoc-pdf-viewer .textLayer ::selection { - background: rgba(0 0 255 / 0.25); - background: color-mix(in srgb, AccentColor, transparent 75%); +.superdoc-pdf-viewer .canvasWrapper svg[data-main-rotation="270"] mask, + .superdoc-pdf-viewer .canvasWrapper svg[data-main-rotation="270"] use:not(.clip, .mask){ + transform:matrix(0, -1, 1, 0, 0, 1); } -.superdoc-pdf-viewer .textLayer br::-moz-selection { - background: transparent; +.superdoc-pdf-viewer .canvasWrapper svg.highlight{ + --blend-mode:multiply; + + position:absolute; + mix-blend-mode:var(--blend-mode); +} +@media screen and (forced-colors: active){ +.superdoc-pdf-viewer .canvasWrapper svg.highlight{ + --blend-mode:difference; +} +} +.superdoc-pdf-viewer .canvasWrapper svg.highlight:not(.free){ + fill-rule:evenodd; +} +.superdoc-pdf-viewer .canvasWrapper svg.highlightOutline{ + position:absolute; + mix-blend-mode:normal; + fill-rule:evenodd; + fill:none; +} +.superdoc-pdf-viewer .canvasWrapper svg.highlightOutline.hovered:not(.free):not(.selected){ + stroke:var(--hover-outline-color); + stroke-width:var(--outline-width); +} +.superdoc-pdf-viewer .canvasWrapper svg.highlightOutline.selected:not(.free) .mainOutline{ + stroke:var(--outline-around-color); + stroke-width:calc( + var(--outline-width) + 2 * var(--outline-around-width) + ); +} +.superdoc-pdf-viewer .canvasWrapper svg.highlightOutline.selected:not(.free) .secondaryOutline{ + stroke:var(--outline-color); + stroke-width:var(--outline-width); +} +.superdoc-pdf-viewer .canvasWrapper svg.highlightOutline.free.hovered:not(.selected){ + stroke:var(--hover-outline-color); + stroke-width:calc(2 * var(--outline-width)); +} +.superdoc-pdf-viewer .canvasWrapper svg.highlightOutline.free.selected .mainOutline{ + stroke:var(--outline-around-color); + stroke-width:calc( + 2 * (var(--outline-width) + var(--outline-around-width)) + ); +} +.superdoc-pdf-viewer .canvasWrapper svg.highlightOutline.free.selected .secondaryOutline{ + stroke:var(--outline-color); + stroke-width:calc(2 * var(--outline-width)); +} +.superdoc-pdf-viewer .toggle-button{ + --button-background-color:#f0f0f4; + --button-background-color-hover:#e0e0e6; + --button-background-color-active:#cfcfd8; + --color-accent-primary:#0060df; + --color-accent-primary-hover:#0250bb; + --color-accent-primary-active:#054096; + --border-interactive-color:#8f8f9d; + --border-radius-circle:9999px; + --border-width:1px; + --size-item-small:16px; + --size-item-large:32px; + --color-canvas:white; + + --toggle-background-color:var(--button-background-color); + --toggle-background-color-hover:var(--button-background-color-hover); + --toggle-background-color-active:var(--button-background-color-active); + --toggle-background-color-pressed:var(--color-accent-primary); + --toggle-background-color-pressed-hover:var(--color-accent-primary-hover); + --toggle-background-color-pressed-active:var(--color-accent-primary-active); + --toggle-border-color:var(--border-interactive-color); + --toggle-border-color-hover:var(--toggle-border-color); + --toggle-border-color-active:var(--toggle-border-color); + --toggle-border-radius:var(--border-radius-circle); + --toggle-border-width:var(--border-width); + --toggle-height:var(--size-item-small); + --toggle-width:var(--size-item-large); + --toggle-dot-background-color:var(--toggle-border-color); + --toggle-dot-background-color-hover:var(--toggle-dot-background-color); + --toggle-dot-background-color-active:var(--toggle-dot-background-color); + --toggle-dot-background-color-on-pressed:var(--color-canvas); + --toggle-dot-margin:1px; + --toggle-dot-height:calc( + var(--toggle-height) - 2 * var(--toggle-dot-margin) - 2 * + var(--toggle-border-width) + ); + --toggle-dot-width:var(--toggle-dot-height); + --toggle-dot-transform-x:calc( + var(--toggle-width) - 4 * var(--toggle-dot-margin) - var(--toggle-dot-width) + ); + + -webkit-appearance:none; + + -moz-appearance:none; + + appearance:none; + padding:0; + margin:0; + border:var(--toggle-border-width) solid var(--toggle-border-color); + height:var(--toggle-height); + width:var(--toggle-width); + border-radius:var(--toggle-border-radius); + background:var(--toggle-background-color); + box-sizing:border-box; + flex-shrink:0; +} +@media (prefers-color-scheme: dark){ +.superdoc-pdf-viewer .toggle-button{ + --button-background-color:color-mix(in srgb, currentColor 7%, transparent); + --button-background-color-hover:color-mix( + in srgb, + currentColor 14%, + transparent + ); + --button-background-color-active:color-mix( + in srgb, + currentColor 21%, + transparent + ); + --color-accent-primary:#0df; + --color-accent-primary-hover:#80ebff; + --color-accent-primary-active:#aaf2ff; + --border-interactive-color:#bfbfc9; + --color-canvas:#1c1b22; +} +} +@media (forced-colors: active){ +.superdoc-pdf-viewer .toggle-button{ + --color-accent-primary:ButtonText; + --color-accent-primary-hover:SelectedItem; + --color-accent-primary-active:SelectedItem; + --border-interactive-color:ButtonText; + --button-background-color:ButtonFace; + --border-interactive-color-hover:SelectedItem; + --border-interactive-color-active:SelectedItem; + --border-interactive-color-disabled:GrayText; + --color-canvas:ButtonText; +} +} +.superdoc-pdf-viewer .toggle-button:focus-visible{ + outline:var(--focus-outline); + outline-offset:var(--focus-outline-offset); +} +.superdoc-pdf-viewer .toggle-button:enabled:hover{ + background:var(--toggle-background-color-hover); + border-color:var(--toggle-border-color); +} +.superdoc-pdf-viewer .toggle-button:enabled:active{ + background:var(--toggle-background-color-active); + border-color:var(--toggle-border-color); +} +.superdoc-pdf-viewer .toggle-button[aria-pressed="true"]{ + background:var(--toggle-background-color-pressed); + border-color:transparent; +} +.superdoc-pdf-viewer .toggle-button[aria-pressed="true"]:enabled:hover{ + background:var(--toggle-background-color-pressed-hover); + border-color:transparent; +} +.superdoc-pdf-viewer .toggle-button[aria-pressed="true"]:enabled:active{ + background:var(--toggle-background-color-pressed-active); + border-color:transparent; +} +.superdoc-pdf-viewer .toggle-button::before{ + display:block; + content:""; + background-color:var(--toggle-dot-background-color); + height:var(--toggle-dot-height); + width:var(--toggle-dot-width); + margin:var(--toggle-dot-margin); + border-radius:var(--toggle-border-radius); + translate:0; +} +.superdoc-pdf-viewer .toggle-button[aria-pressed="true"]::before{ + translate:var(--toggle-dot-transform-x); + background-color:var(--toggle-dot-background-color-on-pressed); +} +.superdoc-pdf-viewer .toggle-button[aria-pressed="true"]:enabled:hover::before, + .superdoc-pdf-viewer .toggle-button[aria-pressed="true"]:enabled:active::before{ + background-color:var(--toggle-dot-background-color-on-pressed); +} +.superdoc-pdf-viewer [dir="rtl"] .toggle-button[aria-pressed="true"]::before{ + translate:calc(-1 * var(--toggle-dot-transform-x)); +} +@media (prefers-reduced-motion: no-preference){ +.superdoc-pdf-viewer .toggle-button::before{ + transition:translate 100ms; +} +} +@media (prefers-contrast){ +.superdoc-pdf-viewer .toggle-button:enabled:hover{ + border-color:var(--toggle-border-color-hover); +} +.superdoc-pdf-viewer .toggle-button:enabled:active{ + border-color:var(--toggle-border-color-active); +} +.superdoc-pdf-viewer .toggle-button[aria-pressed="true"]:enabled{ + border-color:var(--toggle-border-color); + position:relative; +} +.superdoc-pdf-viewer .toggle-button[aria-pressed="true"]:enabled:hover, + .superdoc-pdf-viewer .toggle-button[aria-pressed="true"]:enabled:hover:active{ + border-color:var(--toggle-border-color-hover); +} +.superdoc-pdf-viewer .toggle-button[aria-pressed="true"]:enabled:active{ + background-color:var(--toggle-dot-background-color-active); + border-color:var(--toggle-dot-background-color-hover); +} +.superdoc-pdf-viewer .toggle-button:hover::before, + .superdoc-pdf-viewer .toggle-button:active::before{ + background-color:var(--toggle-dot-background-color-hover); +} +} +@media (forced-colors){ +.superdoc-pdf-viewer .toggle-button{ + --toggle-dot-background-color:var(--color-accent-primary); + --toggle-dot-background-color-hover:var(--color-accent-primary-hover); + --toggle-dot-background-color-active:var(--color-accent-primary-active); + --toggle-dot-background-color-on-pressed:var(--button-background-color); + --toggle-background-color-disabled:var(--button-background-color-disabled); + --toggle-border-color-hover:var(--border-interactive-color-hover); + --toggle-border-color-active:var(--border-interactive-color-active); + --toggle-border-color-disabled:var(--border-interactive-color-disabled); +} +.superdoc-pdf-viewer .toggle-button[aria-pressed="true"]:enabled::after{ + border:1px solid var(--button-background-color); + content:""; + position:absolute; + height:var(--toggle-height); + width:var(--toggle-width); + display:block; + border-radius:var(--toggle-border-radius); + inset:-2px; +} +.superdoc-pdf-viewer .toggle-button[aria-pressed="true"]:enabled:active::after{ + border-color:var(--toggle-border-color-active); +} +} +.superdoc-pdf-viewer :root{ + --outline-width:2px; + --outline-color:#0060df; + --outline-around-width:1px; + --outline-around-color:#f0f0f4; + --hover-outline-around-color:var(--outline-around-color); + --focus-outline:solid var(--outline-width) var(--outline-color); + --unfocus-outline:solid var(--outline-width) transparent; + --focus-outline-around:solid var(--outline-around-width) var(--outline-around-color); + --hover-outline-color:#8f8f9d; + --hover-outline:solid var(--outline-width) var(--hover-outline-color); + --hover-outline-around:solid var(--outline-around-width) var(--hover-outline-around-color); + --freetext-line-height:1.35; + --freetext-padding:2px; + --resizer-bg-color:var(--outline-color); + --resizer-size:6px; + --resizer-shift:calc( + 0px - (var(--outline-width) + var(--resizer-size)) / 2 - + var(--outline-around-width) + ); + --editorFreeText-editing-cursor:text; + --editorInk-editing-cursor:url(images/cursor-editorInk.svg) 0 16, pointer; + --editorHighlight-editing-cursor:url(images/cursor-editorTextHighlight.svg) 24 24, text; + --editorFreeHighlight-editing-cursor:url(images/cursor-editorFreeHighlight.svg) 1 18, pointer; +} +.superdoc-pdf-viewer .visuallyHidden{ + position:absolute; + top:0; + left:0; + border:0; + margin:0; + padding:0; + width:0; + height:0; + overflow:hidden; + white-space:nowrap; + font-size:0; +} +.superdoc-pdf-viewer .textLayer.highlighting{ + cursor:var(--editorFreeHighlight-editing-cursor); +} +.superdoc-pdf-viewer .textLayer.highlighting:not(.free) span{ + cursor:var(--editorHighlight-editing-cursor); +} +.superdoc-pdf-viewer .textLayer.highlighting.free span{ + cursor:var(--editorFreeHighlight-editing-cursor); +} +@media (min-resolution: 1.1dppx){ +.superdoc-pdf-viewer :root{ + --editorFreeText-editing-cursor:url(images/cursor-editorFreeText.svg) 0 16, text; +} +} +@media screen and (forced-colors: active){ +.superdoc-pdf-viewer :root{ + --outline-color:CanvasText; + --outline-around-color:ButtonFace; + --resizer-bg-color:ButtonText; + --hover-outline-color:Highlight; + --hover-outline-around-color:SelectedItemText; +} +} +.superdoc-pdf-viewer [data-editor-rotation="90"]{ + transform:rotate(90deg); +} +.superdoc-pdf-viewer [data-editor-rotation="180"]{ + transform:rotate(180deg); +} +.superdoc-pdf-viewer [data-editor-rotation="270"]{ + transform:rotate(270deg); +} +.superdoc-pdf-viewer .annotationEditorLayer{ + background:transparent; + position:absolute; + inset:0; + font-size:calc(100px * var(--scale-factor)); + transform-origin:0 0; + cursor:auto; +} +.superdoc-pdf-viewer .annotationEditorLayer.waiting{ + content:""; + cursor:wait; + position:absolute; + inset:0; + width:100%; + height:100%; +} +.superdoc-pdf-viewer .annotationEditorLayer.disabled{ + pointer-events:none; +} +.superdoc-pdf-viewer .annotationEditorLayer.freetextEditing{ + cursor:var(--editorFreeText-editing-cursor); +} +.superdoc-pdf-viewer .annotationEditorLayer.inkEditing{ + cursor:var(--editorInk-editing-cursor); +} +.superdoc-pdf-viewer .annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor){ + position:absolute; + background:transparent; + z-index:1; + transform-origin:0 0; + cursor:auto; + max-width:100%; + max-height:100%; + border:var(--unfocus-outline); +} +.superdoc-pdf-viewer .annotationEditorLayer .draggable.selectedEditor:is(.freeTextEditor, .inkEditor, .stampEditor){ + cursor:move; +} +.superdoc-pdf-viewer .annotationEditorLayer .moving:is(.freeTextEditor, .inkEditor, .stampEditor){ + touch-action:none; +} +.superdoc-pdf-viewer .annotationEditorLayer .selectedEditor:is(.freeTextEditor, .inkEditor, .stampEditor){ + border:var(--focus-outline); + outline:var(--focus-outline-around); +} +.superdoc-pdf-viewer .annotationEditorLayer .selectedEditor:is(.freeTextEditor, .inkEditor, .stampEditor)::before{ + content:""; + position:absolute; + inset:0; + border:var(--focus-outline-around); + pointer-events:none; +} +.superdoc-pdf-viewer .annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor):hover:not(.selectedEditor){ + border:var(--hover-outline); + outline:var(--hover-outline-around); +} +.superdoc-pdf-viewer .annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor):hover:not(.selectedEditor)::before{ + content:""; + position:absolute; + inset:0; + border:var(--focus-outline-around); +} +.superdoc-pdf-viewer :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar{ + --editor-toolbar-delete-image:url(images/editor-toolbar-delete.svg); + --editor-toolbar-bg-color:#f0f0f4; + --editor-toolbar-highlight-image:url(images/toolbarButton-editorHighlight.svg); + --editor-toolbar-fg-color:#2e2e56; + --editor-toolbar-border-color:#8f8f9d; + --editor-toolbar-hover-border-color:var(--editor-toolbar-border-color); + --editor-toolbar-hover-bg-color:#e0e0e6; + --editor-toolbar-hover-fg-color:var(--editor-toolbar-fg-color); + --editor-toolbar-hover-outline:none; + --editor-toolbar-focus-outline-color:#0060df; + --editor-toolbar-shadow:0 2px 6px 0 rgb(58 57 68 / 0.2); + --editor-toolbar-vert-offset:6px; + --editor-toolbar-height:28px; + --editor-toolbar-padding:2px; + + display:flex; + width:-moz-fit-content; + width:fit-content; + height:var(--editor-toolbar-height); + flex-direction:column; + justify-content:center; + align-items:center; + cursor:default; + pointer-events:auto; + box-sizing:content-box; + padding:var(--editor-toolbar-padding); + + position:absolute; + inset-inline-end:0; + inset-block-start:calc(100% + var(--editor-toolbar-vert-offset)); + + border-radius:6px; + background-color:var(--editor-toolbar-bg-color); + border:1px solid var(--editor-toolbar-border-color); + box-shadow:var(--editor-toolbar-shadow); +} +@media (prefers-color-scheme: dark){ +.superdoc-pdf-viewer :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar{ + --editor-toolbar-bg-color:#2b2a33; + --editor-toolbar-fg-color:#fbfbfe; + --editor-toolbar-hover-bg-color:#52525e; + --editor-toolbar-focus-outline-color:#0df; +} +} +@media screen and (forced-colors: active){ +.superdoc-pdf-viewer :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar{ + --editor-toolbar-bg-color:ButtonFace; + --editor-toolbar-fg-color:ButtonText; + --editor-toolbar-border-color:ButtonText; + --editor-toolbar-hover-border-color:AccentColor; + --editor-toolbar-hover-bg-color:ButtonFace; + --editor-toolbar-hover-fg-color:AccentColor; + --editor-toolbar-hover-outline:2px solid var(--editor-toolbar-hover-border-color); + --editor-toolbar-focus-outline-color:ButtonBorder; + --editor-toolbar-shadow:none; +} +} +.superdoc-pdf-viewer :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar.hidden{ + display:none; +} +.superdoc-pdf-viewer :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar:has(:focus-visible){ + border-color:transparent; +} +.superdoc-pdf-viewer [dir="ltr"] :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar{ + transform-origin:100% 0; +} +.superdoc-pdf-viewer [dir="rtl"] :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar{ + transform-origin:0 0; +} +.superdoc-pdf-viewer :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar .buttons{ + display:flex; + justify-content:center; + align-items:center; + gap:0; + height:100%; +} +.superdoc-pdf-viewer :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar .buttons .divider{ + width:1px; + height:calc( + 2 * var(--editor-toolbar-padding) + var(--editor-toolbar-height) + ); + background-color:var(--editor-toolbar-border-color); + display:inline-block; + margin-inline:2px; +} +.superdoc-pdf-viewer :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar .buttons .highlightButton{ + width:var(--editor-toolbar-height); +} +.superdoc-pdf-viewer :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar .buttons .highlightButton::before{ + content:""; + -webkit-mask-image:var(--editor-toolbar-highlight-image); + mask-image:var(--editor-toolbar-highlight-image); + -webkit-mask-repeat:no-repeat; + mask-repeat:no-repeat; + -webkit-mask-position:center; + mask-position:center; + display:inline-block; + background-color:var(--editor-toolbar-fg-color); + width:100%; + height:100%; +} +.superdoc-pdf-viewer :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar .buttons .highlightButton:hover::before{ + background-color:var(--editor-toolbar-hover-fg-color); +} +.superdoc-pdf-viewer :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar .buttons .delete{ + width:var(--editor-toolbar-height); +} +.superdoc-pdf-viewer :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar .buttons .delete::before{ + content:""; + -webkit-mask-image:var(--editor-toolbar-delete-image); + mask-image:var(--editor-toolbar-delete-image); + -webkit-mask-repeat:no-repeat; + mask-repeat:no-repeat; + -webkit-mask-position:center; + mask-position:center; + display:inline-block; + background-color:var(--editor-toolbar-fg-color); + width:100%; + height:100%; +} +.superdoc-pdf-viewer :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar .buttons .delete:hover::before{ + background-color:var(--editor-toolbar-hover-fg-color); +} +.superdoc-pdf-viewer :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar .buttons > *{ + height:var(--editor-toolbar-height); +} +.superdoc-pdf-viewer :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar .buttons > :not(.divider){ + border:none; + background-color:transparent; + cursor:pointer; +} +.superdoc-pdf-viewer :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar .buttons > :not(.divider):hover{ + border-radius:2px; + background-color:var(--editor-toolbar-hover-bg-color); + color:var(--editor-toolbar-hover-fg-color); + outline:var(--editor-toolbar-hover-outline); + outline-offset:1px; +} +.superdoc-pdf-viewer :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar .buttons > :not(.divider):hover:active{ + outline:none; +} +.superdoc-pdf-viewer :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar .buttons > :not(.divider):focus-visible{ + border-radius:2px; + outline:2px solid var(--editor-toolbar-focus-outline-color); +} +.superdoc-pdf-viewer :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar .buttons .altText{ + --alt-text-add-image:url(images/altText_add.svg); + --alt-text-done-image:url(images/altText_done.svg); + + display:flex; + align-items:center; + justify-content:center; + width:-moz-max-content; + width:max-content; + padding-inline:8px; + pointer-events:all; + font:menu; + font-weight:590; + font-size:12px; + color:var(--editor-toolbar-fg-color); +} +.superdoc-pdf-viewer :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar .buttons .altText:disabled{ + pointer-events:none; +} +.superdoc-pdf-viewer :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar .buttons .altText::before{ + content:""; + -webkit-mask-image:var(--alt-text-add-image); + mask-image:var(--alt-text-add-image); + -webkit-mask-repeat:no-repeat; + mask-repeat:no-repeat; + -webkit-mask-position:center; + mask-position:center; + display:inline-block; + width:12px; + height:13px; + background-color:var(--editor-toolbar-fg-color); + margin-inline-end:4px; +} +.superdoc-pdf-viewer :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar .buttons .altText:hover::before{ + background-color:var(--editor-toolbar-hover-fg-color); +} +.superdoc-pdf-viewer :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar .buttons .altText.done::before{ + -webkit-mask-image:var(--alt-text-done-image); + mask-image:var(--alt-text-done-image); +} +.superdoc-pdf-viewer :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar .buttons .altText .tooltip{ + display:none; +} +.superdoc-pdf-viewer :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar .buttons .altText .tooltip.show{ + --alt-text-tooltip-bg:#f0f0f4; + --alt-text-tooltip-fg:#15141a; + --alt-text-tooltip-border:#8f8f9d; + --alt-text-tooltip-shadow:0px 2px 6px 0px rgb(58 57 68 / 0.2); + + display:inline-flex; + flex-direction:column; + align-items:center; + justify-content:center; + position:absolute; + top:calc(100% + 2px); + inset-inline-start:0; + padding-block:2px 3px; + padding-inline:3px; + max-width:300px; + width:-moz-max-content; + width:max-content; + height:auto; + font-size:12px; + + border:0.5px solid var(--alt-text-tooltip-border); + background:var(--alt-text-tooltip-bg); + box-shadow:var(--alt-text-tooltip-shadow); + color:var(--alt-text-tooltip-fg); + + pointer-events:none; +} +@media (prefers-color-scheme: dark){ +.superdoc-pdf-viewer :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar .buttons .altText .tooltip.show{ + --alt-text-tooltip-bg:#1c1b22; + --alt-text-tooltip-fg:#fbfbfe; + --alt-text-tooltip-shadow:0px 2px 6px 0px #15141a; +} +} +@media screen and (forced-colors: active){ +.superdoc-pdf-viewer :is(.annotationEditorLayer + :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor),.textLayer) .editToolbar .buttons .altText .tooltip.show{ + --alt-text-tooltip-bg:Canvas; + --alt-text-tooltip-fg:CanvasText; + --alt-text-tooltip-border:CanvasText; + --alt-text-tooltip-shadow:none; +} +} +.superdoc-pdf-viewer .annotationEditorLayer .freeTextEditor{ + padding:calc(var(--freetext-padding) * var(--scale-factor)); + width:auto; + height:auto; + touch-action:none; +} +.superdoc-pdf-viewer .annotationEditorLayer .freeTextEditor .internal{ + background:transparent; + border:none; + inset:0; + overflow:visible; + white-space:nowrap; + font:10px sans-serif; + line-height:var(--freetext-line-height); + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; +} +.superdoc-pdf-viewer .annotationEditorLayer .freeTextEditor .overlay{ + position:absolute; + display:none; + background:transparent; + inset:0; + width:100%; + height:100%; +} +.superdoc-pdf-viewer .annotationEditorLayer freeTextEditor .overlay.enabled{ + display:block; +} +.superdoc-pdf-viewer .annotationEditorLayer .freeTextEditor .internal:empty::before{ + content:attr(default-content); + color:gray; +} +.superdoc-pdf-viewer .annotationEditorLayer .freeTextEditor .internal:focus{ + outline:none; + -webkit-user-select:auto; + -moz-user-select:auto; + user-select:auto; +} +.superdoc-pdf-viewer .annotationEditorLayer .inkEditor{ + width:100%; + height:100%; +} +.superdoc-pdf-viewer .annotationEditorLayer .inkEditor.editing{ + cursor:inherit; +} +.superdoc-pdf-viewer .annotationEditorLayer .inkEditor .inkEditorCanvas{ + position:absolute; + inset:0; + width:100%; + height:100%; + touch-action:none; +} +.superdoc-pdf-viewer .annotationEditorLayer .stampEditor{ + width:auto; + height:auto; +} +.superdoc-pdf-viewer .annotationEditorLayer .stampEditor canvas{ + position:absolute; + width:100%; + height:100%; + margin:0; +} +.superdoc-pdf-viewer .annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) > .resizers{ + position:absolute; + inset:0; +} +.superdoc-pdf-viewer .annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) > .resizers.hidden{ + display:none; +} +.superdoc-pdf-viewer .annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) > .resizers > .resizer{ + width:var(--resizer-size); + height:var(--resizer-size); + background:content-box var(--resizer-bg-color); + border:var(--focus-outline-around); + border-radius:2px; + position:absolute; +} +.superdoc-pdf-viewer .annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) > .resizers > .resizer.topLeft{ + top:var(--resizer-shift); + left:var(--resizer-shift); +} +.superdoc-pdf-viewer .annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) > .resizers > .resizer.topMiddle{ + top:var(--resizer-shift); + left:calc(50% + var(--resizer-shift)); +} +.superdoc-pdf-viewer .annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) > .resizers > .resizer.topRight{ + top:var(--resizer-shift); + right:var(--resizer-shift); +} +.superdoc-pdf-viewer .annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) > .resizers > .resizer.middleRight{ + top:calc(50% + var(--resizer-shift)); + right:var(--resizer-shift); +} +.superdoc-pdf-viewer .annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) > .resizers > .resizer.bottomRight{ + bottom:var(--resizer-shift); + right:var(--resizer-shift); +} +.superdoc-pdf-viewer .annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) > .resizers > .resizer.bottomMiddle{ + bottom:var(--resizer-shift); + left:calc(50% + var(--resizer-shift)); +} +.superdoc-pdf-viewer .annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) > .resizers > .resizer.bottomLeft{ + bottom:var(--resizer-shift); + left:var(--resizer-shift); +} +.superdoc-pdf-viewer .annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) > .resizers > .resizer.middleLeft{ + top:calc(50% + var(--resizer-shift)); + left:var(--resizer-shift); +} +.superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="0"] + :is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.topLeft, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="90"] + :is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.topLeft, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="180"] + :is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.topLeft, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="270"] + :is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.topLeft, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="0"] + :is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.bottomRight, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="90"] + :is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.bottomRight, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="180"] + :is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.bottomRight, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="270"] + :is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.bottomRight{ + cursor:nwse-resize; +} +.superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="0"] + :is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.topMiddle, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="90"] + :is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.topMiddle, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="180"] + :is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.topMiddle, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="270"] + :is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.topMiddle, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="0"] + :is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.bottomMiddle, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="90"] + :is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.bottomMiddle, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="180"] + :is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.bottomMiddle, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="270"] + :is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.bottomMiddle{ + cursor:ns-resize; +} +.superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="0"] + :is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.topRight, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="90"] + :is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.topRight, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="180"] + :is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.topRight, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="270"] + :is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.topRight, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="0"] + :is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.bottomLeft, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="90"] + :is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.bottomLeft, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="180"] + :is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.bottomLeft, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="270"] + :is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.bottomLeft{ + cursor:nesw-resize; +} +.superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="0"] + :is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.middleRight, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="90"] + :is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.middleRight, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="180"] + :is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.middleRight, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="270"] + :is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.middleRight, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="0"] + :is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.middleLeft, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="90"] + :is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.middleLeft, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="180"] + :is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.middleLeft, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="270"] + :is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.middleLeft{ + cursor:ew-resize; +} +.superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="0"] + :is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.topLeft, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="90"] + :is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.topLeft, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="180"] + :is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.topLeft, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="270"] + :is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.topLeft, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="0"] + :is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.bottomRight, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="90"] + :is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.bottomRight, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="180"] + :is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.bottomRight, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="270"] + :is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.bottomRight{ + cursor:nesw-resize; +} +.superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="0"] + :is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.topMiddle, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="90"] + :is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.topMiddle, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="180"] + :is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.topMiddle, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="270"] + :is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.topMiddle, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="0"] + :is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.bottomMiddle, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="90"] + :is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.bottomMiddle, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="180"] + :is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.bottomMiddle, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="270"] + :is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.bottomMiddle{ + cursor:ew-resize; +} +.superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="0"] + :is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.topRight, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="90"] + :is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.topRight, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="180"] + :is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.topRight, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="270"] + :is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.topRight, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="0"] + :is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.bottomLeft, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="90"] + :is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.bottomLeft, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="180"] + :is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.bottomLeft, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="270"] + :is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.bottomLeft{ + cursor:nwse-resize; +} +.superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="0"] + :is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.middleRight, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="90"] + :is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.middleRight, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="180"] + :is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.middleRight, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="270"] + :is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.middleRight, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="0"] + :is([data-editor-rotation="90"], [data-editor-rotation="270"]) > .resizers > .resizer.middleLeft, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="90"] + :is([data-editor-rotation="0"], [data-editor-rotation="180"]) > .resizers > .resizer.middleLeft, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="180"] + :is([data-editor-rotation="270"], [data-editor-rotation="90"]) > .resizers > .resizer.middleLeft, .superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="270"] + :is([data-editor-rotation="180"], [data-editor-rotation="0"]) > .resizers > .resizer.middleLeft{ + cursor:ns-resize; +} +.superdoc-pdf-viewer .annotationEditorLayer + :is( + [data-main-rotation="0"] [data-editor-rotation="90"], + [data-main-rotation="90"] [data-editor-rotation="0"], + [data-main-rotation="180"] [data-editor-rotation="270"], + [data-main-rotation="270"] [data-editor-rotation="180"] + ) .editToolbar{ + rotate:270deg; +} +.superdoc-pdf-viewer [dir="ltr"] .annotationEditorLayer + :is( + [data-main-rotation="0"] [data-editor-rotation="90"], + [data-main-rotation="90"] [data-editor-rotation="0"], + [data-main-rotation="180"] [data-editor-rotation="270"], + [data-main-rotation="270"] [data-editor-rotation="180"] + ) .editToolbar{ + inset-inline-end:calc(0px - var(--editor-toolbar-vert-offset)); + inset-block-start:0; +} +.superdoc-pdf-viewer [dir="rtl"] .annotationEditorLayer + :is( + [data-main-rotation="0"] [data-editor-rotation="90"], + [data-main-rotation="90"] [data-editor-rotation="0"], + [data-main-rotation="180"] [data-editor-rotation="270"], + [data-main-rotation="270"] [data-editor-rotation="180"] + ) .editToolbar{ + inset-inline-end:calc(100% + var(--editor-toolbar-vert-offset)); + inset-block-start:0; +} +.superdoc-pdf-viewer .annotationEditorLayer + :is( + [data-main-rotation="0"] [data-editor-rotation="180"], + [data-main-rotation="90"] [data-editor-rotation="90"], + [data-main-rotation="180"] [data-editor-rotation="0"], + [data-main-rotation="270"] [data-editor-rotation="270"] + ) .editToolbar{ + rotate:180deg; + inset-inline-end:100%; + inset-block-start:calc(0pc - var(--editor-toolbar-vert-offset)); +} +.superdoc-pdf-viewer .annotationEditorLayer + :is( + [data-main-rotation="0"] [data-editor-rotation="270"], + [data-main-rotation="90"] [data-editor-rotation="180"], + [data-main-rotation="180"] [data-editor-rotation="90"], + [data-main-rotation="270"] [data-editor-rotation="0"] + ) .editToolbar{ + rotate:90deg; +} +.superdoc-pdf-viewer [dir="ltr"] .annotationEditorLayer + :is( + [data-main-rotation="0"] [data-editor-rotation="270"], + [data-main-rotation="90"] [data-editor-rotation="180"], + [data-main-rotation="180"] [data-editor-rotation="90"], + [data-main-rotation="270"] [data-editor-rotation="0"] + ) .editToolbar{ + inset-inline-end:calc(100% + var(--editor-toolbar-vert-offset)); + inset-block-start:100%; +} +.superdoc-pdf-viewer [dir="rtl"] .annotationEditorLayer + :is( + [data-main-rotation="0"] [data-editor-rotation="270"], + [data-main-rotation="90"] [data-editor-rotation="180"], + [data-main-rotation="180"] [data-editor-rotation="90"], + [data-main-rotation="270"] [data-editor-rotation="0"] + ) .editToolbar{ + inset-inline-start:calc(0px - var(--editor-toolbar-vert-offset)); + inset-block-start:0; +} +.superdoc-pdf-viewer .dialog.altText::backdrop{ + -webkit-mask:url(#alttext-manager-mask); + mask:url(#alttext-manager-mask); +} +.superdoc-pdf-viewer .dialog.altText.positioned{ + margin:0; +} +.superdoc-pdf-viewer .dialog.altText #altTextContainer{ + width:300px; + height:-moz-fit-content; + height:fit-content; + display:inline-flex; + flex-direction:column; + align-items:flex-start; + gap:16px; +} +.superdoc-pdf-viewer .dialog.altText #altTextContainer #overallDescription{ + display:flex; + flex-direction:column; + align-items:flex-start; + gap:4px; + align-self:stretch; +} +.superdoc-pdf-viewer .dialog.altText #altTextContainer #overallDescription span{ + align-self:stretch; +} +.superdoc-pdf-viewer .dialog.altText #altTextContainer #overallDescription .title{ + font-size:13px; + font-style:normal; + font-weight:590; +} +.superdoc-pdf-viewer .dialog.altText #altTextContainer #addDescription{ + display:flex; + flex-direction:column; + align-items:stretch; + gap:8px; +} +.superdoc-pdf-viewer .dialog.altText #altTextContainer #addDescription .descriptionArea{ + flex:1; + padding-inline:24px 10px; +} +.superdoc-pdf-viewer .dialog.altText #altTextContainer #addDescription .descriptionArea textarea{ + width:100%; + min-height:75px; +} +.superdoc-pdf-viewer .dialog.altText #altTextContainer #buttons{ + display:flex; + justify-content:flex-end; + align-items:flex-start; + gap:8px; + align-self:stretch; +} +.superdoc-pdf-viewer .colorPicker{ + --hover-outline-color:#0250bb; + --selected-outline-color:#0060df; + --swatch-border-color:#cfcfd8; +} +@media (prefers-color-scheme: dark){ +.superdoc-pdf-viewer .colorPicker{ + --hover-outline-color:#80ebff; + --selected-outline-color:#aaf2ff; + --swatch-border-color:#52525e; +} +} +@media screen and (forced-colors: active){ +.superdoc-pdf-viewer .colorPicker{ + --hover-outline-color:Highlight; + --selected-outline-color:var(--hover-outline-color); + --swatch-border-color:ButtonText; +} +} +.superdoc-pdf-viewer .colorPicker .swatch{ + width:16px; + height:16px; + border:1px solid var(--swatch-border-color); + border-radius:100%; + outline-offset:2px; + box-sizing:border-box; + forced-color-adjust:none; +} +.superdoc-pdf-viewer .colorPicker button:is(:hover, .selected) > .swatch{ + border:none; +} +.superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="0"] .highlightEditor:not(.free) > .editToolbar{ + rotate:0deg; +} +.superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="90"] .highlightEditor:not(.free) > .editToolbar{ + rotate:270deg; +} +.superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="180"] .highlightEditor:not(.free) > .editToolbar{ + rotate:180deg; +} +.superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation="270"] .highlightEditor:not(.free) > .editToolbar{ + rotate:90deg; +} +.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor{ + position:absolute; + background:transparent; + z-index:1; + cursor:auto; + max-width:100%; + max-height:100%; + border:none; + outline:none; + pointer-events:none; + transform-origin:0 0; +} +.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor:not(.free){ + transform:none; +} +.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor .internal{ + position:absolute; + top:0; + left:0; + width:100%; + height:100%; + pointer-events:auto; +} +.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor.disabled .internal{ + pointer-events:none; +} +.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor.selectedEditor .internal{ + cursor:pointer; +} +.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor .editToolbar{ + --editor-toolbar-colorpicker-arrow-image:url(images/toolbarButton-menuArrow.svg); + + transform-origin:center !important; +} +.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor .editToolbar .buttons .colorPicker{ + position:relative; + width:auto; + display:flex; + justify-content:center; + align-items:center; + gap:4px; + padding:4px; +} +.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor .editToolbar .buttons .colorPicker::after{ + content:""; + -webkit-mask-image:var(--editor-toolbar-colorpicker-arrow-image); + mask-image:var(--editor-toolbar-colorpicker-arrow-image); + -webkit-mask-repeat:no-repeat; + mask-repeat:no-repeat; + -webkit-mask-position:center; + mask-position:center; + display:inline-block; + background-color:var(--editor-toolbar-fg-color); + width:12px; + height:12px; +} +.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor .editToolbar .buttons .colorPicker:hover::after{ + background-color:var(--editor-toolbar-hover-fg-color); +} +.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor .editToolbar .buttons .colorPicker:has(.dropdown:not(.hidden)){ + background-color:var(--editor-toolbar-hover-bg-color); +} +.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor .editToolbar .buttons .colorPicker:has(.dropdown:not(.hidden))::after{ + scale:-1; +} +.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor .editToolbar .buttons .colorPicker .dropdown{ + position:absolute; + display:flex; + justify-content:center; + align-items:center; + flex-direction:column; + gap:11px; + padding-block:8px; + border-radius:6px; + background-color:var(--editor-toolbar-bg-color); + border:1px solid var(--editor-toolbar-border-color); + box-shadow:var(--editor-toolbar-shadow); + inset-block-start:calc(100% + 4px); + width:calc(100% + 2 * var(--editor-toolbar-padding)); +} +.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor .editToolbar .buttons .colorPicker .dropdown button{ + width:100%; + height:auto; + border:none; + cursor:pointer; + display:flex; + justify-content:center; + align-items:center; + background:none; +} +.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor .editToolbar .buttons .colorPicker .dropdown button:is(:active, :focus-visible){ + outline:none; +} +.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor .editToolbar .buttons .colorPicker .dropdown button > .swatch{ + outline-offset:2px; +} +.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor .editToolbar .buttons .colorPicker .dropdown button[aria-selected="true"] > .swatch{ + outline:2px solid var(--selected-outline-color); +} +.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor .editToolbar .buttons .colorPicker .dropdown button:is(:hover, :active, :focus-visible) > .swatch{ + outline:2px solid var(--hover-outline-color); +} +.superdoc-pdf-viewer .editorParamsToolbar:has(#highlightParamsToolbarContainer){ + padding:unset; +} +.superdoc-pdf-viewer #highlightParamsToolbarContainer{ + height:auto; + padding-inline:10px; + padding-block:10px 16px; + gap:16px; + display:flex; + flex-direction:column; + box-sizing:border-box; +} +.superdoc-pdf-viewer #highlightParamsToolbarContainer .editorParamsLabel{ + width:-moz-fit-content; + width:fit-content; + inset-inline-start:0; +} +.superdoc-pdf-viewer #highlightParamsToolbarContainer .colorPicker{ + display:flex; + flex-direction:column; + gap:8px; +} +.superdoc-pdf-viewer #highlightParamsToolbarContainer .colorPicker .dropdown{ + display:flex; + justify-content:space-between; + align-items:center; + flex-direction:row; + height:auto; +} +.superdoc-pdf-viewer #highlightParamsToolbarContainer .colorPicker .dropdown button{ + width:auto; + height:auto; + border:none; + cursor:pointer; + display:flex; + justify-content:center; + align-items:center; + background:none; + flex:0 0 auto; +} +.superdoc-pdf-viewer #highlightParamsToolbarContainer .colorPicker .dropdown button .swatch{ + width:24px; + height:24px; +} +.superdoc-pdf-viewer #highlightParamsToolbarContainer .colorPicker .dropdown button:is(:active, :focus-visible){ + outline:none; +} +.superdoc-pdf-viewer #highlightParamsToolbarContainer .colorPicker .dropdown button[aria-selected="true"] > .swatch{ + outline:2px solid var(--selected-outline-color); +} +.superdoc-pdf-viewer #highlightParamsToolbarContainer .colorPicker .dropdown button:is(:hover, :active, :focus-visible) > .swatch{ + outline:2px solid var(--hover-outline-color); +} +.superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightThickness{ + display:flex; + flex-direction:column; + align-items:center; + gap:4px; + align-self:stretch; +} +.superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightThickness .editorParamsLabel{ + width:100%; + height:auto; + align-self:stretch; +} +.superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightThickness .thicknessPicker{ + display:flex; + justify-content:space-between; + align-items:center; + align-self:stretch; + + --example-color:#bfbfc9; } -.superdoc-pdf-viewer .textLayer br::selection { - background: transparent; +@media (prefers-color-scheme: dark){ +.superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightThickness .thicknessPicker{ + --example-color:#80808e; } -.superdoc-pdf-viewer .textLayer .endOfContent { - display: block; - position: absolute; - inset: 100% 0 0; - z-index: 0; - cursor: default; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; } -.superdoc-pdf-viewer .textLayer .endOfContent.active { - top: 0; +@media screen and (forced-colors: active){ +.superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightThickness .thicknessPicker{ + --example-color:CanvasText; +} } -.superdoc-pdf-viewer .annotationLayer { - --annotation-unfocused-field-background: url("data:image/svg+xml;charset=UTF-8,"); - --input-focus-border-color: Highlight; - --input-focus-outline: 1px solid Canvas; - --input-unfocused-border-color: transparent; - --input-disabled-border-color: transparent; - --input-hover-border-color: black; - --link-outline: none; +.superdoc-pdf-viewer :is(#highlightParamsToolbarContainer #editorHighlightThickness .thicknessPicker > .editorParamsSlider[disabled]){ + opacity:0.4; +} +.superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightThickness .thicknessPicker::before, + .superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightThickness .thicknessPicker::after{ + content:""; + width:8px; + aspect-ratio:1; + display:block; + border-radius:100%; + background-color:var(--example-color); +} +.superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightThickness .thicknessPicker::after{ + width:24px; +} +.superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightThickness .thicknessPicker .editorParamsSlider{ + width:unset; + height:14px; +} +.superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightVisibility{ + display:flex; + flex-direction:column; + align-items:flex-start; + gap:8px; + align-self:stretch; +} +.superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightVisibility .divider{ + --divider-color:#d7d7db; - position: absolute; - top: 0; - left: 0; - pointer-events: none; - transform-origin: 0 0; -} -@media screen and (forced-colors: active) { - .superdoc-pdf-viewer .annotationLayer { - --input-focus-border-color: CanvasText; - --input-unfocused-border-color: ActiveText; - --input-disabled-border-color: GrayText; - --input-hover-border-color: Highlight; - --link-outline: 1.5px solid LinkText; - } - .superdoc-pdf-viewer .annotationLayer .textWidgetAnnotation :is(input, textarea):required, - .superdoc-pdf-viewer .annotationLayer .choiceWidgetAnnotation select:required, - .superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input:required { - outline: 1.5px solid selectedItem; - } - .superdoc-pdf-viewer .annotationLayer .linkAnnotation { - outline: var(--link-outline); - } - .superdoc-pdf-viewer .annotationLayer .linkAnnotation:hover { - -webkit-backdrop-filter: var(--hcm-highlight-filter); - backdrop-filter: var(--hcm-highlight-filter); - } - .superdoc-pdf-viewer .annotationLayer .linkAnnotation > a:hover { - opacity: 0 !important; - background: none !important; - box-shadow: none; - } - .superdoc-pdf-viewer .annotationLayer .popupAnnotation .popup { - outline: calc(1.5px * var(--scale-factor)) solid CanvasText !important; - background-color: ButtonFace !important; - color: ButtonText !important; - } - .superdoc-pdf-viewer .annotationLayer .highlightArea:hover::after { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - -webkit-backdrop-filter: var(--hcm-highlight-filter); - backdrop-filter: var(--hcm-highlight-filter); - content: ''; - pointer-events: none; - } - .superdoc-pdf-viewer .annotationLayer .popupAnnotation.focused .popup { - outline: calc(3px * var(--scale-factor)) solid Highlight !important; - } -} -.superdoc-pdf-viewer .annotationLayer[data-main-rotation='90'] .norotate { - transform: rotate(270deg) translateX(-100%); -} -.superdoc-pdf-viewer .annotationLayer[data-main-rotation='180'] .norotate { - transform: rotate(180deg) translate(-100%, -100%); -} -.superdoc-pdf-viewer .annotationLayer[data-main-rotation='270'] .norotate { - transform: rotate(90deg) translateY(-100%); -} -.superdoc-pdf-viewer .annotationLayer.disabled section, -.superdoc-pdf-viewer .annotationLayer.disabled .popup { - pointer-events: none; -} -.superdoc-pdf-viewer .annotationLayer .annotationContent { - position: absolute; - width: 100%; - height: 100%; - pointer-events: none; -} -.superdoc-pdf-viewer .annotationLayer .annotationContent.freetext { - background: transparent; - border: none; - inset: 0; - overflow: visible; - white-space: nowrap; - font: 10px sans-serif; - line-height: 1.35; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; -} -.superdoc-pdf-viewer .annotationLayer section { - position: absolute; - text-align: initial; - pointer-events: auto; - box-sizing: border-box; - transform-origin: 0 0; -} -.superdoc-pdf-viewer .annotationLayer section:has(div.annotationContent) canvas.annotationContent { - display: none; -} -.superdoc-pdf-viewer .annotationLayer :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton) > a { - position: absolute; - font-size: 1em; - top: 0; - left: 0; - width: 100%; - height: 100%; -} -.superdoc-pdf-viewer - .annotationLayer - :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton):not(.hasBorder) - > a:hover { - opacity: 0.2; - background-color: rgb(255 255 0); - box-shadow: 0 2px 10px rgb(255 255 0); -} -.superdoc-pdf-viewer .annotationLayer .linkAnnotation.hasBorder:hover { - background-color: rgb(255 255 0 / 0.2); -} -.superdoc-pdf-viewer .annotationLayer .hasBorder { - background-size: 100% 100%; -} -.superdoc-pdf-viewer .annotationLayer .textAnnotation img { - position: absolute; - cursor: pointer; - width: 100%; - height: 100%; - top: 0; - left: 0; -} -.superdoc-pdf-viewer .annotationLayer .textWidgetAnnotation :is(input, textarea), -.superdoc-pdf-viewer .annotationLayer .choiceWidgetAnnotation select, -.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input { - background-image: var(--annotation-unfocused-field-background); - border: 2px solid var(--input-unfocused-border-color); - box-sizing: border-box; - font: calc(9px * var(--scale-factor)) sans-serif; - height: 100%; - margin: 0; - vertical-align: top; - width: 100%; -} -.superdoc-pdf-viewer .annotationLayer .textWidgetAnnotation :is(input, textarea):required, -.superdoc-pdf-viewer .annotationLayer .choiceWidgetAnnotation select:required, -.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input:required { - outline: 1.5px solid red; -} -.superdoc-pdf-viewer .annotationLayer .choiceWidgetAnnotation select option { - padding: 0; -} -.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation.radioButton input { - border-radius: 50%; -} -.superdoc-pdf-viewer .annotationLayer .textWidgetAnnotation textarea { - resize: none; -} -.superdoc-pdf-viewer .annotationLayer .textWidgetAnnotation [disabled]:is(input, textarea), -.superdoc-pdf-viewer .annotationLayer .choiceWidgetAnnotation select[disabled], -.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input[disabled] { - background: none; - border: 2px solid var(--input-disabled-border-color); - cursor: not-allowed; -} -.superdoc-pdf-viewer .annotationLayer .textWidgetAnnotation :is(input, textarea):hover, -.superdoc-pdf-viewer .annotationLayer .choiceWidgetAnnotation select:hover, -.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input:hover { - border: 2px solid var(--input-hover-border-color); -} -.superdoc-pdf-viewer .annotationLayer .textWidgetAnnotation :is(input, textarea):hover, -.superdoc-pdf-viewer .annotationLayer .choiceWidgetAnnotation select:hover, -.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation.checkBox input:hover { - border-radius: 2px; -} -.superdoc-pdf-viewer .annotationLayer .textWidgetAnnotation :is(input, textarea):focus, -.superdoc-pdf-viewer .annotationLayer .choiceWidgetAnnotation select:focus { - background: none; - border: 2px solid var(--input-focus-border-color); - border-radius: 2px; - outline: var(--input-focus-outline); -} -.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) :focus { - background-image: none; - background-color: transparent; -} -.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation.checkBox :focus { - border: 2px solid var(--input-focus-border-color); - border-radius: 2px; - outline: var(--input-focus-outline); -} -.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation.radioButton :focus { - border: 2px solid var(--input-focus-border-color); - outline: var(--input-focus-outline); -} -.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation.checkBox input:checked::before, -.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation.checkBox input:checked::after, -.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation.radioButton input:checked::before { - background-color: CanvasText; - content: ''; - display: block; - position: absolute; -} -.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation.checkBox input:checked::before, -.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation.checkBox input:checked::after { - height: 80%; - left: 45%; - width: 1px; -} -.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation.checkBox input:checked::before { - transform: rotate(45deg); -} -.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation.checkBox input:checked::after { - transform: rotate(-45deg); -} -.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation.radioButton input:checked::before { - border-radius: 50%; - height: 50%; - left: 25%; - top: 25%; - width: 50%; -} -.superdoc-pdf-viewer .annotationLayer .textWidgetAnnotation input.comb { - font-family: monospace; - padding-left: 2px; - padding-right: 0; -} -.superdoc-pdf-viewer .annotationLayer .textWidgetAnnotation input.comb:focus { - width: 103%; -} -.superdoc-pdf-viewer .annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; -} -.superdoc-pdf-viewer .annotationLayer .fileAttachmentAnnotation .popupTriggerArea { - height: 100%; - width: 100%; -} -.superdoc-pdf-viewer .annotationLayer .popupAnnotation { - position: absolute; - font-size: calc(9px * var(--scale-factor)); - pointer-events: none; - width: -moz-max-content; - width: max-content; - max-width: 45%; - height: auto; -} -.superdoc-pdf-viewer .annotationLayer .popup { - background-color: rgb(255 255 153); - box-shadow: 0 calc(2px * var(--scale-factor)) calc(5px * var(--scale-factor)) rgb(136 136 136); - border-radius: calc(2px * var(--scale-factor)); - outline: 1.5px solid rgb(255 255 74); - padding: calc(6px * var(--scale-factor)); - cursor: pointer; - font: message-box; - white-space: normal; - word-wrap: break-word; - pointer-events: auto; -} -.superdoc-pdf-viewer .annotationLayer .popupAnnotation.focused .popup { - outline-width: 3px; -} -.superdoc-pdf-viewer .annotationLayer .popup * { - font-size: calc(9px * var(--scale-factor)); -} -.superdoc-pdf-viewer .annotationLayer .popup > .header { - display: inline-block; -} -.superdoc-pdf-viewer .annotationLayer .popup > .header h1 { - display: inline; -} -.superdoc-pdf-viewer .annotationLayer .popup > .header .popupDate { - display: inline-block; - margin-left: calc(5px * var(--scale-factor)); - width: -moz-fit-content; - width: fit-content; -} -.superdoc-pdf-viewer .annotationLayer .popupContent { - border-top: 1px solid rgb(51 51 51); - margin-top: calc(2px * var(--scale-factor)); - padding-top: calc(2px * var(--scale-factor)); -} -.superdoc-pdf-viewer .annotationLayer .richText > * { - white-space: pre-wrap; - font-size: calc(9px * var(--scale-factor)); -} -.superdoc-pdf-viewer .annotationLayer .popupTriggerArea { - cursor: pointer; -} -.superdoc-pdf-viewer .annotationLayer section svg { - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; -} -.superdoc-pdf-viewer .annotationLayer .annotationTextContent { - position: absolute; - width: 100%; - height: 100%; - opacity: 0; - color: transparent; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - pointer-events: none; -} -.superdoc-pdf-viewer .annotationLayer .annotationTextContent span { - width: 100%; - display: inline-block; -} -.superdoc-pdf-viewer .annotationLayer svg.quadrilateralsContainer { - contain: strict; - width: 0; - height: 0; - position: absolute; - top: 0; - left: 0; - z-index: -1; -} -.superdoc-pdf-viewer :root { - --xfa-unfocused-field-background: url("data:image/svg+xml;charset=UTF-8,"); - --xfa-focus-outline: auto; -} -@media screen and (forced-colors: active) { - .superdoc-pdf-viewer :root { - --xfa-focus-outline: 2px solid CanvasText; - } - .superdoc-pdf-viewer .xfaLayer *:required { - outline: 1.5px solid selectedItem; - } -} -.superdoc-pdf-viewer .xfaLayer { - background-color: transparent; -} -.superdoc-pdf-viewer .xfaLayer .highlight { - margin: -1px; - padding: 1px; - background-color: rgb(239 203 237); - border-radius: 4px; -} -.superdoc-pdf-viewer .xfaLayer .highlight.appended { - position: initial; -} -.superdoc-pdf-viewer .xfaLayer .highlight.begin { - border-radius: 4px 0 0 4px; -} -.superdoc-pdf-viewer .xfaLayer .highlight.end { - border-radius: 0 4px 4px 0; -} -.superdoc-pdf-viewer .xfaLayer .highlight.middle { - border-radius: 0; -} -.superdoc-pdf-viewer .xfaLayer .highlight.selected { - background-color: rgb(203 223 203); -} -.superdoc-pdf-viewer .xfaPage { - overflow: hidden; - position: relative; -} -.superdoc-pdf-viewer .xfaContentarea { - position: absolute; -} -.superdoc-pdf-viewer .xfaPrintOnly { - display: none; -} -.superdoc-pdf-viewer .xfaLayer { - position: absolute; - text-align: initial; - top: 0; - left: 0; - transform-origin: 0 0; - line-height: 1.2; -} -.superdoc-pdf-viewer .xfaLayer * { - color: inherit; - font: inherit; - font-style: inherit; - font-weight: inherit; - font-kerning: inherit; - letter-spacing: -0.01px; - text-align: inherit; - text-decoration: inherit; - box-sizing: border-box; - background-color: transparent; - padding: 0; - margin: 0; - pointer-events: auto; - line-height: inherit; -} -.superdoc-pdf-viewer .xfaLayer *:required { - outline: 1.5px solid red; -} -.superdoc-pdf-viewer .xfaLayer div, -.superdoc-pdf-viewer .xfaLayer svg, -.superdoc-pdf-viewer .xfaLayer svg * { - pointer-events: none; -} -.superdoc-pdf-viewer .xfaLayer a { - color: blue; -} -.superdoc-pdf-viewer .xfaRich li { - margin-left: 3em; -} -.superdoc-pdf-viewer .xfaFont { - color: black; - font-weight: normal; - font-kerning: none; - font-size: 10px; - font-style: normal; - letter-spacing: 0; - text-decoration: none; - vertical-align: 0; -} -.superdoc-pdf-viewer .xfaCaption { - overflow: hidden; - flex: 0 0 auto; -} -.superdoc-pdf-viewer .xfaCaptionForCheckButton { - overflow: hidden; - flex: 1 1 auto; -} -.superdoc-pdf-viewer .xfaLabel { - height: 100%; - width: 100%; -} -.superdoc-pdf-viewer .xfaLeft { - display: flex; - flex-direction: row; - align-items: center; -} -.superdoc-pdf-viewer .xfaRight { - display: flex; - flex-direction: row-reverse; - align-items: center; -} -.superdoc-pdf-viewer :is(.xfaLeft, .xfaRight) > :is(.xfaCaption, .xfaCaptionForCheckButton) { - max-height: 100%; -} -.superdoc-pdf-viewer .xfaTop { - display: flex; - flex-direction: column; - align-items: flex-start; -} -.superdoc-pdf-viewer .xfaBottom { - display: flex; - flex-direction: column-reverse; - align-items: flex-start; -} -.superdoc-pdf-viewer :is(.xfaTop, .xfaBottom) > :is(.xfaCaption, .xfaCaptionForCheckButton) { - width: 100%; -} -.superdoc-pdf-viewer .xfaBorder { - background-color: transparent; - position: absolute; - pointer-events: none; -} -.superdoc-pdf-viewer .xfaWrapped { - width: 100%; - height: 100%; -} -.superdoc-pdf-viewer :is(.xfaTextfield, .xfaSelect):focus { - background-image: none; - background-color: transparent; - outline: var(--xfa-focus-outline); - outline-offset: -1px; -} -.superdoc-pdf-viewer :is(.xfaCheckbox, .xfaRadio):focus { - outline: var(--xfa-focus-outline); -} -.superdoc-pdf-viewer .xfaTextfield, -.superdoc-pdf-viewer .xfaSelect { - height: 100%; - width: 100%; - flex: 1 1 auto; - border: none; - resize: none; - background-image: var(--xfa-unfocused-field-background); -} -.superdoc-pdf-viewer .xfaSelect { - padding-inline: 2px; -} -.superdoc-pdf-viewer :is(.xfaTop, .xfaBottom) > :is(.xfaTextfield, .xfaSelect) { - flex: 0 1 auto; -} -.superdoc-pdf-viewer .xfaButton { - cursor: pointer; - width: 100%; - height: 100%; - border: none; - text-align: center; -} -.superdoc-pdf-viewer .xfaLink { - width: 100%; - height: 100%; - position: absolute; - top: 0; - left: 0; -} -.superdoc-pdf-viewer .xfaCheckbox, -.superdoc-pdf-viewer .xfaRadio { - width: 100%; - height: 100%; - flex: 0 0 auto; - border: none; -} -.superdoc-pdf-viewer .xfaRich { - white-space: pre-wrap; - width: 100%; - height: 100%; -} -.superdoc-pdf-viewer .xfaImage { - -o-object-position: left top; - object-position: left top; - -o-object-fit: contain; - object-fit: contain; - width: 100%; - height: 100%; -} -.superdoc-pdf-viewer .xfaLrTb, -.superdoc-pdf-viewer .xfaRlTb, -.superdoc-pdf-viewer .xfaTb { - display: flex; - flex-direction: column; - align-items: stretch; -} -.superdoc-pdf-viewer .xfaLr { - display: flex; - flex-direction: row; - align-items: stretch; -} -.superdoc-pdf-viewer .xfaRl { - display: flex; - flex-direction: row-reverse; - align-items: stretch; -} -.superdoc-pdf-viewer .xfaTb > div { - justify-content: left; -} -.superdoc-pdf-viewer .xfaPosition { - position: relative; -} -.superdoc-pdf-viewer .xfaArea { - position: relative; -} -.superdoc-pdf-viewer .xfaValignMiddle { - display: flex; - align-items: center; -} -.superdoc-pdf-viewer .xfaTable { - display: flex; - flex-direction: column; - align-items: stretch; -} -.superdoc-pdf-viewer .xfaTable .xfaRow { - display: flex; - flex-direction: row; - align-items: stretch; -} -.superdoc-pdf-viewer .xfaTable .xfaRlRow { - display: flex; - flex-direction: row-reverse; - align-items: stretch; - flex: 1; -} -.superdoc-pdf-viewer .xfaTable .xfaRlRow > div { - flex: 1; -} -.superdoc-pdf-viewer :is(.xfaNonInteractive, .xfaDisabled, .xfaReadOnly) :is(input, textarea) { - background: initial; -} -@media print { - .superdoc-pdf-viewer .xfaTextfield, - .superdoc-pdf-viewer .xfaSelect { - background: transparent; - } - .superdoc-pdf-viewer .xfaSelect { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - text-indent: 1px; - text-overflow: ''; - } -} -.superdoc-pdf-viewer .canvasWrapper svg { - transform: none; -} -.superdoc-pdf-viewer .canvasWrapper svg[data-main-rotation='90'] mask, -.superdoc-pdf-viewer .canvasWrapper svg[data-main-rotation='90'] use:not(.clip, .mask) { - transform: matrix(0, 1, -1, 0, 1, 0); -} -.superdoc-pdf-viewer .canvasWrapper svg[data-main-rotation='180'] mask, -.superdoc-pdf-viewer .canvasWrapper svg[data-main-rotation='180'] use:not(.clip, .mask) { - transform: matrix(-1, 0, 0, -1, 1, 1); -} -.superdoc-pdf-viewer .canvasWrapper svg[data-main-rotation='270'] mask, -.superdoc-pdf-viewer .canvasWrapper svg[data-main-rotation='270'] use:not(.clip, .mask) { - transform: matrix(0, -1, 1, 0, 0, 1); -} -.superdoc-pdf-viewer .canvasWrapper svg.highlight { - --blend-mode: multiply; - - position: absolute; - mix-blend-mode: var(--blend-mode); -} -@media screen and (forced-colors: active) { - .superdoc-pdf-viewer .canvasWrapper svg.highlight { - --blend-mode: difference; - } -} -.superdoc-pdf-viewer .canvasWrapper svg.highlight:not(.free) { - fill-rule: evenodd; -} -.superdoc-pdf-viewer .canvasWrapper svg.highlightOutline { - position: absolute; - mix-blend-mode: normal; - fill-rule: evenodd; - fill: none; -} -.superdoc-pdf-viewer .canvasWrapper svg.highlightOutline.hovered:not(.free):not(.selected) { - stroke: var(--hover-outline-color); - stroke-width: var(--outline-width); -} -.superdoc-pdf-viewer .canvasWrapper svg.highlightOutline.selected:not(.free) .mainOutline { - stroke: var(--outline-around-color); - stroke-width: calc(var(--outline-width) + 2 * var(--outline-around-width)); -} -.superdoc-pdf-viewer .canvasWrapper svg.highlightOutline.selected:not(.free) .secondaryOutline { - stroke: var(--outline-color); - stroke-width: var(--outline-width); -} -.superdoc-pdf-viewer .canvasWrapper svg.highlightOutline.free.hovered:not(.selected) { - stroke: var(--hover-outline-color); - stroke-width: calc(2 * var(--outline-width)); -} -.superdoc-pdf-viewer .canvasWrapper svg.highlightOutline.free.selected .mainOutline { - stroke: var(--outline-around-color); - stroke-width: calc(2 * (var(--outline-width) + var(--outline-around-width))); -} -.superdoc-pdf-viewer .canvasWrapper svg.highlightOutline.free.selected .secondaryOutline { - stroke: var(--outline-color); - stroke-width: calc(2 * var(--outline-width)); -} -.superdoc-pdf-viewer .toggle-button { - --button-background-color: #f0f0f4; - --button-background-color-hover: #e0e0e6; - --button-background-color-active: #cfcfd8; - --color-accent-primary: #0060df; - --color-accent-primary-hover: #0250bb; - --color-accent-primary-active: #054096; - --border-interactive-color: #8f8f9d; - --border-radius-circle: 9999px; - --border-width: 1px; - --size-item-small: 16px; - --size-item-large: 32px; - --color-canvas: white; - - --toggle-background-color: var(--button-background-color); - --toggle-background-color-hover: var(--button-background-color-hover); - --toggle-background-color-active: var(--button-background-color-active); - --toggle-background-color-pressed: var(--color-accent-primary); - --toggle-background-color-pressed-hover: var(--color-accent-primary-hover); - --toggle-background-color-pressed-active: var(--color-accent-primary-active); - --toggle-border-color: var(--border-interactive-color); - --toggle-border-color-hover: var(--toggle-border-color); - --toggle-border-color-active: var(--toggle-border-color); - --toggle-border-radius: var(--border-radius-circle); - --toggle-border-width: var(--border-width); - --toggle-height: var(--size-item-small); - --toggle-width: var(--size-item-large); - --toggle-dot-background-color: var(--toggle-border-color); - --toggle-dot-background-color-hover: var(--toggle-dot-background-color); - --toggle-dot-background-color-active: var(--toggle-dot-background-color); - --toggle-dot-background-color-on-pressed: var(--color-canvas); - --toggle-dot-margin: 1px; - --toggle-dot-height: calc(var(--toggle-height) - 2 * var(--toggle-dot-margin) - 2 * var(--toggle-border-width)); - --toggle-dot-width: var(--toggle-dot-height); - --toggle-dot-transform-x: calc(var(--toggle-width) - 4 * var(--toggle-dot-margin) - var(--toggle-dot-width)); - - -webkit-appearance: none; - - -moz-appearance: none; - - appearance: none; - padding: 0; - margin: 0; - border: var(--toggle-border-width) solid var(--toggle-border-color); - height: var(--toggle-height); - width: var(--toggle-width); - border-radius: var(--toggle-border-radius); - background: var(--toggle-background-color); - box-sizing: border-box; - flex-shrink: 0; -} -@media (prefers-color-scheme: dark) { - .superdoc-pdf-viewer .toggle-button { - --button-background-color: color-mix(in srgb, currentColor 7%, transparent); - --button-background-color-hover: color-mix(in srgb, currentColor 14%, transparent); - --button-background-color-active: color-mix(in srgb, currentColor 21%, transparent); - --color-accent-primary: #0df; - --color-accent-primary-hover: #80ebff; - --color-accent-primary-active: #aaf2ff; - --border-interactive-color: #bfbfc9; - --color-canvas: #1c1b22; - } -} -@media (forced-colors: active) { - .superdoc-pdf-viewer .toggle-button { - --color-accent-primary: ButtonText; - --color-accent-primary-hover: SelectedItem; - --color-accent-primary-active: SelectedItem; - --border-interactive-color: ButtonText; - --button-background-color: ButtonFace; - --border-interactive-color-hover: SelectedItem; - --border-interactive-color-active: SelectedItem; - --border-interactive-color-disabled: GrayText; - --color-canvas: ButtonText; - } -} -.superdoc-pdf-viewer .toggle-button:focus-visible { - outline: var(--focus-outline); - outline-offset: var(--focus-outline-offset); -} -.superdoc-pdf-viewer .toggle-button:enabled:hover { - background: var(--toggle-background-color-hover); - border-color: var(--toggle-border-color); -} -.superdoc-pdf-viewer .toggle-button:enabled:active { - background: var(--toggle-background-color-active); - border-color: var(--toggle-border-color); -} -.superdoc-pdf-viewer .toggle-button[aria-pressed='true'] { - background: var(--toggle-background-color-pressed); - border-color: transparent; -} -.superdoc-pdf-viewer .toggle-button[aria-pressed='true']:enabled:hover { - background: var(--toggle-background-color-pressed-hover); - border-color: transparent; -} -.superdoc-pdf-viewer .toggle-button[aria-pressed='true']:enabled:active { - background: var(--toggle-background-color-pressed-active); - border-color: transparent; -} -.superdoc-pdf-viewer .toggle-button::before { - display: block; - content: ''; - background-color: var(--toggle-dot-background-color); - height: var(--toggle-dot-height); - width: var(--toggle-dot-width); - margin: var(--toggle-dot-margin); - border-radius: var(--toggle-border-radius); - translate: 0; -} -.superdoc-pdf-viewer .toggle-button[aria-pressed='true']::before { - translate: var(--toggle-dot-transform-x); - background-color: var(--toggle-dot-background-color-on-pressed); -} -.superdoc-pdf-viewer .toggle-button[aria-pressed='true']:enabled:hover::before, -.superdoc-pdf-viewer .toggle-button[aria-pressed='true']:enabled:active::before { - background-color: var(--toggle-dot-background-color-on-pressed); -} -.superdoc-pdf-viewer [dir='rtl'] .toggle-button[aria-pressed='true']::before { - translate: calc(-1 * var(--toggle-dot-transform-x)); -} -@media (prefers-reduced-motion: no-preference) { - .superdoc-pdf-viewer .toggle-button::before { - transition: translate 100ms; - } -} -@media (prefers-contrast) { - .superdoc-pdf-viewer .toggle-button:enabled:hover { - border-color: var(--toggle-border-color-hover); - } - .superdoc-pdf-viewer .toggle-button:enabled:active { - border-color: var(--toggle-border-color-active); - } - .superdoc-pdf-viewer .toggle-button[aria-pressed='true']:enabled { - border-color: var(--toggle-border-color); - position: relative; - } - .superdoc-pdf-viewer .toggle-button[aria-pressed='true']:enabled:hover, - .superdoc-pdf-viewer .toggle-button[aria-pressed='true']:enabled:hover:active { - border-color: var(--toggle-border-color-hover); - } - .superdoc-pdf-viewer .toggle-button[aria-pressed='true']:enabled:active { - background-color: var(--toggle-dot-background-color-active); - border-color: var(--toggle-dot-background-color-hover); - } - .superdoc-pdf-viewer .toggle-button:hover::before, - .superdoc-pdf-viewer .toggle-button:active::before { - background-color: var(--toggle-dot-background-color-hover); - } -} -@media (forced-colors) { - .superdoc-pdf-viewer .toggle-button { - --toggle-dot-background-color: var(--color-accent-primary); - --toggle-dot-background-color-hover: var(--color-accent-primary-hover); - --toggle-dot-background-color-active: var(--color-accent-primary-active); - --toggle-dot-background-color-on-pressed: var(--button-background-color); - --toggle-background-color-disabled: var(--button-background-color-disabled); - --toggle-border-color-hover: var(--border-interactive-color-hover); - --toggle-border-color-active: var(--border-interactive-color-active); - --toggle-border-color-disabled: var(--border-interactive-color-disabled); - } - .superdoc-pdf-viewer .toggle-button[aria-pressed='true']:enabled::after { - border: 1px solid var(--button-background-color); - content: ''; - position: absolute; - height: var(--toggle-height); - width: var(--toggle-width); - display: block; - border-radius: var(--toggle-border-radius); - inset: -2px; - } - .superdoc-pdf-viewer .toggle-button[aria-pressed='true']:enabled:active::after { - border-color: var(--toggle-border-color-active); - } -} -.superdoc-pdf-viewer :root { - --outline-width: 2px; - --outline-color: #0060df; - --outline-around-width: 1px; - --outline-around-color: #f0f0f4; - --hover-outline-around-color: var(--outline-around-color); - --focus-outline: solid var(--outline-width) var(--outline-color); - --unfocus-outline: solid var(--outline-width) transparent; - --focus-outline-around: solid var(--outline-around-width) var(--outline-around-color); - --hover-outline-color: #8f8f9d; - --hover-outline: solid var(--outline-width) var(--hover-outline-color); - --hover-outline-around: solid var(--outline-around-width) var(--hover-outline-around-color); - --freetext-line-height: 1.35; - --freetext-padding: 2px; - --resizer-bg-color: var(--outline-color); - --resizer-size: 6px; - --resizer-shift: calc(0px - (var(--outline-width) + var(--resizer-size)) / 2 - var(--outline-around-width)); - --editorFreeText-editing-cursor: text; - --editorInk-editing-cursor: url(images/cursor-editorInk.svg) 0 16, pointer; - --editorHighlight-editing-cursor: url(images/cursor-editorTextHighlight.svg) 24 24, text; - --editorFreeHighlight-editing-cursor: url(images/cursor-editorFreeHighlight.svg) 1 18, pointer; -} -.superdoc-pdf-viewer .visuallyHidden { - position: absolute; - top: 0; - left: 0; - border: 0; - margin: 0; - padding: 0; - width: 0; - height: 0; - overflow: hidden; - white-space: nowrap; - font-size: 0; -} -.superdoc-pdf-viewer .textLayer.highlighting { - cursor: var(--editorFreeHighlight-editing-cursor); -} -.superdoc-pdf-viewer .textLayer.highlighting:not(.free) span { - cursor: var(--editorHighlight-editing-cursor); -} -.superdoc-pdf-viewer .textLayer.highlighting.free span { - cursor: var(--editorFreeHighlight-editing-cursor); -} -@media (min-resolution: 1.1dppx) { - .superdoc-pdf-viewer :root { - --editorFreeText-editing-cursor: url(images/cursor-editorFreeText.svg) 0 16, text; - } -} -@media screen and (forced-colors: active) { - .superdoc-pdf-viewer :root { - --outline-color: CanvasText; - --outline-around-color: ButtonFace; - --resizer-bg-color: ButtonText; - --hover-outline-color: Highlight; - --hover-outline-around-color: SelectedItemText; - } -} -.superdoc-pdf-viewer [data-editor-rotation='90'] { - transform: rotate(90deg); -} -.superdoc-pdf-viewer [data-editor-rotation='180'] { - transform: rotate(180deg); -} -.superdoc-pdf-viewer [data-editor-rotation='270'] { - transform: rotate(270deg); -} -.superdoc-pdf-viewer .annotationEditorLayer { - background: transparent; - position: absolute; - inset: 0; - font-size: calc(100px * var(--scale-factor)); - transform-origin: 0 0; - cursor: auto; -} -.superdoc-pdf-viewer .annotationEditorLayer.waiting { - content: ''; - cursor: wait; - position: absolute; - inset: 0; - width: 100%; - height: 100%; -} -.superdoc-pdf-viewer .annotationEditorLayer.disabled { - pointer-events: none; -} -.superdoc-pdf-viewer .annotationEditorLayer.freetextEditing { - cursor: var(--editorFreeText-editing-cursor); -} -.superdoc-pdf-viewer .annotationEditorLayer.inkEditing { - cursor: var(--editorInk-editing-cursor); -} -.superdoc-pdf-viewer .annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) { - position: absolute; - background: transparent; - z-index: 1; - transform-origin: 0 0; - cursor: auto; - max-width: 100%; - max-height: 100%; - border: var(--unfocus-outline); -} -.superdoc-pdf-viewer .annotationEditorLayer .draggable.selectedEditor:is(.freeTextEditor, .inkEditor, .stampEditor) { - cursor: move; -} -.superdoc-pdf-viewer .annotationEditorLayer .moving:is(.freeTextEditor, .inkEditor, .stampEditor) { - touch-action: none; -} -.superdoc-pdf-viewer .annotationEditorLayer .selectedEditor:is(.freeTextEditor, .inkEditor, .stampEditor) { - border: var(--focus-outline); - outline: var(--focus-outline-around); -} -.superdoc-pdf-viewer .annotationEditorLayer .selectedEditor:is(.freeTextEditor, .inkEditor, .stampEditor)::before { - content: ''; - position: absolute; - inset: 0; - border: var(--focus-outline-around); - pointer-events: none; -} -.superdoc-pdf-viewer .annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor):hover:not(.selectedEditor) { - border: var(--hover-outline); - outline: var(--hover-outline-around); -} -.superdoc-pdf-viewer - .annotationEditorLayer - :is(.freeTextEditor, .inkEditor, .stampEditor):hover:not(.selectedEditor)::before { - content: ''; - position: absolute; - inset: 0; - border: var(--focus-outline-around); -} -.superdoc-pdf-viewer - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar { - --editor-toolbar-delete-image: url(images/editor-toolbar-delete.svg); - --editor-toolbar-bg-color: #f0f0f4; - --editor-toolbar-highlight-image: url(images/toolbarButton-editorHighlight.svg); - --editor-toolbar-fg-color: #2e2e56; - --editor-toolbar-border-color: #8f8f9d; - --editor-toolbar-hover-border-color: var(--editor-toolbar-border-color); - --editor-toolbar-hover-bg-color: #e0e0e6; - --editor-toolbar-hover-fg-color: var(--editor-toolbar-fg-color); - --editor-toolbar-hover-outline: none; - --editor-toolbar-focus-outline-color: #0060df; - --editor-toolbar-shadow: 0 2px 6px 0 rgb(58 57 68 / 0.2); - --editor-toolbar-vert-offset: 6px; - --editor-toolbar-height: 28px; - --editor-toolbar-padding: 2px; - - display: flex; - width: -moz-fit-content; - width: fit-content; - height: var(--editor-toolbar-height); - flex-direction: column; - justify-content: center; - align-items: center; - cursor: default; - pointer-events: auto; - box-sizing: content-box; - padding: var(--editor-toolbar-padding); - - position: absolute; - inset-inline-end: 0; - inset-block-start: calc(100% + var(--editor-toolbar-vert-offset)); - - border-radius: 6px; - background-color: var(--editor-toolbar-bg-color); - border: 1px solid var(--editor-toolbar-border-color); - box-shadow: var(--editor-toolbar-shadow); -} -@media (prefers-color-scheme: dark) { - .superdoc-pdf-viewer - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar { - --editor-toolbar-bg-color: #2b2a33; - --editor-toolbar-fg-color: #fbfbfe; - --editor-toolbar-hover-bg-color: #52525e; - --editor-toolbar-focus-outline-color: #0df; - } -} -@media screen and (forced-colors: active) { - .superdoc-pdf-viewer - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar { - --editor-toolbar-bg-color: ButtonFace; - --editor-toolbar-fg-color: ButtonText; - --editor-toolbar-border-color: ButtonText; - --editor-toolbar-hover-border-color: AccentColor; - --editor-toolbar-hover-bg-color: ButtonFace; - --editor-toolbar-hover-fg-color: AccentColor; - --editor-toolbar-hover-outline: 2px solid var(--editor-toolbar-hover-border-color); - --editor-toolbar-focus-outline-color: ButtonBorder; - --editor-toolbar-shadow: none; - } -} -.superdoc-pdf-viewer - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar.hidden { - display: none; -} -.superdoc-pdf-viewer - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar:has(:focus-visible) { - border-color: transparent; -} -.superdoc-pdf-viewer - [dir='ltr'] - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar { - transform-origin: 100% 0; -} -.superdoc-pdf-viewer - [dir='rtl'] - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar { - transform-origin: 0 0; -} -.superdoc-pdf-viewer - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar - .buttons { - display: flex; - justify-content: center; - align-items: center; - gap: 0; - height: 100%; -} -.superdoc-pdf-viewer - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar - .buttons - .divider { - width: 1px; - height: calc(2 * var(--editor-toolbar-padding) + var(--editor-toolbar-height)); - background-color: var(--editor-toolbar-border-color); - display: inline-block; - margin-inline: 2px; -} -.superdoc-pdf-viewer - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar - .buttons - .highlightButton { - width: var(--editor-toolbar-height); -} -.superdoc-pdf-viewer - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar - .buttons - .highlightButton::before { - content: ''; - -webkit-mask-image: var(--editor-toolbar-highlight-image); - mask-image: var(--editor-toolbar-highlight-image); - -webkit-mask-repeat: no-repeat; - mask-repeat: no-repeat; - -webkit-mask-position: center; - mask-position: center; - display: inline-block; - background-color: var(--editor-toolbar-fg-color); - width: 100%; - height: 100%; -} -.superdoc-pdf-viewer - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar - .buttons - .highlightButton:hover::before { - background-color: var(--editor-toolbar-hover-fg-color); -} -.superdoc-pdf-viewer - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar - .buttons - .delete { - width: var(--editor-toolbar-height); -} -.superdoc-pdf-viewer - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar - .buttons - .delete::before { - content: ''; - -webkit-mask-image: var(--editor-toolbar-delete-image); - mask-image: var(--editor-toolbar-delete-image); - -webkit-mask-repeat: no-repeat; - mask-repeat: no-repeat; - -webkit-mask-position: center; - mask-position: center; - display: inline-block; - background-color: var(--editor-toolbar-fg-color); - width: 100%; - height: 100%; -} -.superdoc-pdf-viewer - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar - .buttons - .delete:hover::before { - background-color: var(--editor-toolbar-hover-fg-color); -} -.superdoc-pdf-viewer - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar - .buttons - > * { - height: var(--editor-toolbar-height); -} -.superdoc-pdf-viewer - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar - .buttons - > :not(.divider) { - border: none; - background-color: transparent; - cursor: pointer; -} -.superdoc-pdf-viewer - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar - .buttons - > :not(.divider):hover { - border-radius: 2px; - background-color: var(--editor-toolbar-hover-bg-color); - color: var(--editor-toolbar-hover-fg-color); - outline: var(--editor-toolbar-hover-outline); - outline-offset: 1px; -} -.superdoc-pdf-viewer - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar - .buttons - > :not(.divider):hover:active { - outline: none; -} -.superdoc-pdf-viewer - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar - .buttons - > :not(.divider):focus-visible { - border-radius: 2px; - outline: 2px solid var(--editor-toolbar-focus-outline-color); -} -.superdoc-pdf-viewer - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar - .buttons - .altText { - --alt-text-add-image: url(images/altText_add.svg); - --alt-text-done-image: url(images/altText_done.svg); - - display: flex; - align-items: center; - justify-content: center; - width: -moz-max-content; - width: max-content; - padding-inline: 8px; - pointer-events: all; - font: menu; - font-weight: 590; - font-size: 12px; - color: var(--editor-toolbar-fg-color); -} -.superdoc-pdf-viewer - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar - .buttons - .altText:disabled { - pointer-events: none; -} -.superdoc-pdf-viewer - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar - .buttons - .altText::before { - content: ''; - -webkit-mask-image: var(--alt-text-add-image); - mask-image: var(--alt-text-add-image); - -webkit-mask-repeat: no-repeat; - mask-repeat: no-repeat; - -webkit-mask-position: center; - mask-position: center; - display: inline-block; - width: 12px; - height: 13px; - background-color: var(--editor-toolbar-fg-color); - margin-inline-end: 4px; -} -.superdoc-pdf-viewer - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar - .buttons - .altText:hover::before { - background-color: var(--editor-toolbar-hover-fg-color); -} -.superdoc-pdf-viewer - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar - .buttons - .altText.done::before { - -webkit-mask-image: var(--alt-text-done-image); - mask-image: var(--alt-text-done-image); -} -.superdoc-pdf-viewer - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar - .buttons - .altText - .tooltip { - display: none; -} -.superdoc-pdf-viewer - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar - .buttons - .altText - .tooltip.show { - --alt-text-tooltip-bg: #f0f0f4; - --alt-text-tooltip-fg: #15141a; - --alt-text-tooltip-border: #8f8f9d; - --alt-text-tooltip-shadow: 0px 2px 6px 0px rgb(58 57 68 / 0.2); - - display: inline-flex; - flex-direction: column; - align-items: center; - justify-content: center; - position: absolute; - top: calc(100% + 2px); - inset-inline-start: 0; - padding-block: 2px 3px; - padding-inline: 3px; - max-width: 300px; - width: -moz-max-content; - width: max-content; - height: auto; - font-size: 12px; - - border: 0.5px solid var(--alt-text-tooltip-border); - background: var(--alt-text-tooltip-bg); - box-shadow: var(--alt-text-tooltip-shadow); - color: var(--alt-text-tooltip-fg); - - pointer-events: none; -} -@media (prefers-color-scheme: dark) { - .superdoc-pdf-viewer - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar - .buttons - .altText - .tooltip.show { - --alt-text-tooltip-bg: #1c1b22; - --alt-text-tooltip-fg: #fbfbfe; - --alt-text-tooltip-shadow: 0px 2px 6px 0px #15141a; - } -} -@media screen and (forced-colors: active) { - .superdoc-pdf-viewer - :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor), .textLayer) - .editToolbar - .buttons - .altText - .tooltip.show { - --alt-text-tooltip-bg: Canvas; - --alt-text-tooltip-fg: CanvasText; - --alt-text-tooltip-border: CanvasText; - --alt-text-tooltip-shadow: none; - } -} -.superdoc-pdf-viewer .annotationEditorLayer .freeTextEditor { - padding: calc(var(--freetext-padding) * var(--scale-factor)); - width: auto; - height: auto; - touch-action: none; -} -.superdoc-pdf-viewer .annotationEditorLayer .freeTextEditor .internal { - background: transparent; - border: none; - inset: 0; - overflow: visible; - white-space: nowrap; - font: 10px sans-serif; - line-height: var(--freetext-line-height); - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; -} -.superdoc-pdf-viewer .annotationEditorLayer .freeTextEditor .overlay { - position: absolute; - display: none; - background: transparent; - inset: 0; - width: 100%; - height: 100%; -} -.superdoc-pdf-viewer .annotationEditorLayer freeTextEditor .overlay.enabled { - display: block; -} -.superdoc-pdf-viewer .annotationEditorLayer .freeTextEditor .internal:empty::before { - content: attr(default-content); - color: gray; -} -.superdoc-pdf-viewer .annotationEditorLayer .freeTextEditor .internal:focus { - outline: none; - -webkit-user-select: auto; - -moz-user-select: auto; - user-select: auto; -} -.superdoc-pdf-viewer .annotationEditorLayer .inkEditor { - width: 100%; - height: 100%; -} -.superdoc-pdf-viewer .annotationEditorLayer .inkEditor.editing { - cursor: inherit; -} -.superdoc-pdf-viewer .annotationEditorLayer .inkEditor .inkEditorCanvas { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - touch-action: none; -} -.superdoc-pdf-viewer .annotationEditorLayer .stampEditor { - width: auto; - height: auto; -} -.superdoc-pdf-viewer .annotationEditorLayer .stampEditor canvas { - position: absolute; - width: 100%; - height: 100%; - margin: 0; -} -.superdoc-pdf-viewer .annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) > .resizers { - position: absolute; - inset: 0; -} -.superdoc-pdf-viewer .annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) > .resizers.hidden { - display: none; -} -.superdoc-pdf-viewer .annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) > .resizers > .resizer { - width: var(--resizer-size); - height: var(--resizer-size); - background: content-box var(--resizer-bg-color); - border: var(--focus-outline-around); - border-radius: 2px; - position: absolute; -} -.superdoc-pdf-viewer - .annotationEditorLayer - :is(.freeTextEditor, .inkEditor, .stampEditor) - > .resizers - > .resizer.topLeft { - top: var(--resizer-shift); - left: var(--resizer-shift); -} -.superdoc-pdf-viewer - .annotationEditorLayer - :is(.freeTextEditor, .inkEditor, .stampEditor) - > .resizers - > .resizer.topMiddle { - top: var(--resizer-shift); - left: calc(50% + var(--resizer-shift)); -} -.superdoc-pdf-viewer - .annotationEditorLayer - :is(.freeTextEditor, .inkEditor, .stampEditor) - > .resizers - > .resizer.topRight { - top: var(--resizer-shift); - right: var(--resizer-shift); -} -.superdoc-pdf-viewer - .annotationEditorLayer - :is(.freeTextEditor, .inkEditor, .stampEditor) - > .resizers - > .resizer.middleRight { - top: calc(50% + var(--resizer-shift)); - right: var(--resizer-shift); -} -.superdoc-pdf-viewer - .annotationEditorLayer - :is(.freeTextEditor, .inkEditor, .stampEditor) - > .resizers - > .resizer.bottomRight { - bottom: var(--resizer-shift); - right: var(--resizer-shift); -} -.superdoc-pdf-viewer - .annotationEditorLayer - :is(.freeTextEditor, .inkEditor, .stampEditor) - > .resizers - > .resizer.bottomMiddle { - bottom: var(--resizer-shift); - left: calc(50% + var(--resizer-shift)); -} -.superdoc-pdf-viewer - .annotationEditorLayer - :is(.freeTextEditor, .inkEditor, .stampEditor) - > .resizers - > .resizer.bottomLeft { - bottom: var(--resizer-shift); - left: var(--resizer-shift); -} -.superdoc-pdf-viewer - .annotationEditorLayer - :is(.freeTextEditor, .inkEditor, .stampEditor) - > .resizers - > .resizer.middleLeft { - top: calc(50% + var(--resizer-shift)); - left: var(--resizer-shift); -} -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='0'] - :is([data-editor-rotation='0'], [data-editor-rotation='180']) - > .resizers - > .resizer.topLeft, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='90'] - :is([data-editor-rotation='270'], [data-editor-rotation='90']) - > .resizers - > .resizer.topLeft, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='180'] - :is([data-editor-rotation='180'], [data-editor-rotation='0']) - > .resizers - > .resizer.topLeft, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='270'] - :is([data-editor-rotation='90'], [data-editor-rotation='270']) - > .resizers - > .resizer.topLeft, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='0'] - :is([data-editor-rotation='0'], [data-editor-rotation='180']) - > .resizers - > .resizer.bottomRight, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='90'] - :is([data-editor-rotation='270'], [data-editor-rotation='90']) - > .resizers - > .resizer.bottomRight, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='180'] - :is([data-editor-rotation='180'], [data-editor-rotation='0']) - > .resizers - > .resizer.bottomRight, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='270'] - :is([data-editor-rotation='90'], [data-editor-rotation='270']) - > .resizers - > .resizer.bottomRight { - cursor: nwse-resize; -} -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='0'] - :is([data-editor-rotation='0'], [data-editor-rotation='180']) - > .resizers - > .resizer.topMiddle, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='90'] - :is([data-editor-rotation='270'], [data-editor-rotation='90']) - > .resizers - > .resizer.topMiddle, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='180'] - :is([data-editor-rotation='180'], [data-editor-rotation='0']) - > .resizers - > .resizer.topMiddle, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='270'] - :is([data-editor-rotation='90'], [data-editor-rotation='270']) - > .resizers - > .resizer.topMiddle, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='0'] - :is([data-editor-rotation='0'], [data-editor-rotation='180']) - > .resizers - > .resizer.bottomMiddle, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='90'] - :is([data-editor-rotation='270'], [data-editor-rotation='90']) - > .resizers - > .resizer.bottomMiddle, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='180'] - :is([data-editor-rotation='180'], [data-editor-rotation='0']) - > .resizers - > .resizer.bottomMiddle, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='270'] - :is([data-editor-rotation='90'], [data-editor-rotation='270']) - > .resizers - > .resizer.bottomMiddle { - cursor: ns-resize; -} -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='0'] - :is([data-editor-rotation='0'], [data-editor-rotation='180']) - > .resizers - > .resizer.topRight, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='90'] - :is([data-editor-rotation='270'], [data-editor-rotation='90']) - > .resizers - > .resizer.topRight, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='180'] - :is([data-editor-rotation='180'], [data-editor-rotation='0']) - > .resizers - > .resizer.topRight, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='270'] - :is([data-editor-rotation='90'], [data-editor-rotation='270']) - > .resizers - > .resizer.topRight, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='0'] - :is([data-editor-rotation='0'], [data-editor-rotation='180']) - > .resizers - > .resizer.bottomLeft, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='90'] - :is([data-editor-rotation='270'], [data-editor-rotation='90']) - > .resizers - > .resizer.bottomLeft, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='180'] - :is([data-editor-rotation='180'], [data-editor-rotation='0']) - > .resizers - > .resizer.bottomLeft, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='270'] - :is([data-editor-rotation='90'], [data-editor-rotation='270']) - > .resizers - > .resizer.bottomLeft { - cursor: nesw-resize; -} -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='0'] - :is([data-editor-rotation='0'], [data-editor-rotation='180']) - > .resizers - > .resizer.middleRight, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='90'] - :is([data-editor-rotation='270'], [data-editor-rotation='90']) - > .resizers - > .resizer.middleRight, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='180'] - :is([data-editor-rotation='180'], [data-editor-rotation='0']) - > .resizers - > .resizer.middleRight, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='270'] - :is([data-editor-rotation='90'], [data-editor-rotation='270']) - > .resizers - > .resizer.middleRight, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='0'] - :is([data-editor-rotation='0'], [data-editor-rotation='180']) - > .resizers - > .resizer.middleLeft, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='90'] - :is([data-editor-rotation='270'], [data-editor-rotation='90']) - > .resizers - > .resizer.middleLeft, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='180'] - :is([data-editor-rotation='180'], [data-editor-rotation='0']) - > .resizers - > .resizer.middleLeft, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='270'] - :is([data-editor-rotation='90'], [data-editor-rotation='270']) - > .resizers - > .resizer.middleLeft { - cursor: ew-resize; -} -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='0'] - :is([data-editor-rotation='90'], [data-editor-rotation='270']) - > .resizers - > .resizer.topLeft, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='90'] - :is([data-editor-rotation='0'], [data-editor-rotation='180']) - > .resizers - > .resizer.topLeft, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='180'] - :is([data-editor-rotation='270'], [data-editor-rotation='90']) - > .resizers - > .resizer.topLeft, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='270'] - :is([data-editor-rotation='180'], [data-editor-rotation='0']) - > .resizers - > .resizer.topLeft, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='0'] - :is([data-editor-rotation='90'], [data-editor-rotation='270']) - > .resizers - > .resizer.bottomRight, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='90'] - :is([data-editor-rotation='0'], [data-editor-rotation='180']) - > .resizers - > .resizer.bottomRight, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='180'] - :is([data-editor-rotation='270'], [data-editor-rotation='90']) - > .resizers - > .resizer.bottomRight, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='270'] - :is([data-editor-rotation='180'], [data-editor-rotation='0']) - > .resizers - > .resizer.bottomRight { - cursor: nesw-resize; -} -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='0'] - :is([data-editor-rotation='90'], [data-editor-rotation='270']) - > .resizers - > .resizer.topMiddle, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='90'] - :is([data-editor-rotation='0'], [data-editor-rotation='180']) - > .resizers - > .resizer.topMiddle, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='180'] - :is([data-editor-rotation='270'], [data-editor-rotation='90']) - > .resizers - > .resizer.topMiddle, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='270'] - :is([data-editor-rotation='180'], [data-editor-rotation='0']) - > .resizers - > .resizer.topMiddle, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='0'] - :is([data-editor-rotation='90'], [data-editor-rotation='270']) - > .resizers - > .resizer.bottomMiddle, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='90'] - :is([data-editor-rotation='0'], [data-editor-rotation='180']) - > .resizers - > .resizer.bottomMiddle, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='180'] - :is([data-editor-rotation='270'], [data-editor-rotation='90']) - > .resizers - > .resizer.bottomMiddle, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='270'] - :is([data-editor-rotation='180'], [data-editor-rotation='0']) - > .resizers - > .resizer.bottomMiddle { - cursor: ew-resize; -} -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='0'] - :is([data-editor-rotation='90'], [data-editor-rotation='270']) - > .resizers - > .resizer.topRight, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='90'] - :is([data-editor-rotation='0'], [data-editor-rotation='180']) - > .resizers - > .resizer.topRight, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='180'] - :is([data-editor-rotation='270'], [data-editor-rotation='90']) - > .resizers - > .resizer.topRight, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='270'] - :is([data-editor-rotation='180'], [data-editor-rotation='0']) - > .resizers - > .resizer.topRight, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='0'] - :is([data-editor-rotation='90'], [data-editor-rotation='270']) - > .resizers - > .resizer.bottomLeft, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='90'] - :is([data-editor-rotation='0'], [data-editor-rotation='180']) - > .resizers - > .resizer.bottomLeft, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='180'] - :is([data-editor-rotation='270'], [data-editor-rotation='90']) - > .resizers - > .resizer.bottomLeft, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='270'] - :is([data-editor-rotation='180'], [data-editor-rotation='0']) - > .resizers - > .resizer.bottomLeft { - cursor: nwse-resize; -} -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='0'] - :is([data-editor-rotation='90'], [data-editor-rotation='270']) - > .resizers - > .resizer.middleRight, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='90'] - :is([data-editor-rotation='0'], [data-editor-rotation='180']) - > .resizers - > .resizer.middleRight, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='180'] - :is([data-editor-rotation='270'], [data-editor-rotation='90']) - > .resizers - > .resizer.middleRight, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='270'] - :is([data-editor-rotation='180'], [data-editor-rotation='0']) - > .resizers - > .resizer.middleRight, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='0'] - :is([data-editor-rotation='90'], [data-editor-rotation='270']) - > .resizers - > .resizer.middleLeft, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='90'] - :is([data-editor-rotation='0'], [data-editor-rotation='180']) - > .resizers - > .resizer.middleLeft, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='180'] - :is([data-editor-rotation='270'], [data-editor-rotation='90']) - > .resizers - > .resizer.middleLeft, -.superdoc-pdf-viewer - .annotationEditorLayer[data-main-rotation='270'] - :is([data-editor-rotation='180'], [data-editor-rotation='0']) - > .resizers - > .resizer.middleLeft { - cursor: ns-resize; -} -.superdoc-pdf-viewer - .annotationEditorLayer - :is( - [data-main-rotation='0'] [data-editor-rotation='90'], - [data-main-rotation='90'] [data-editor-rotation='0'], - [data-main-rotation='180'] [data-editor-rotation='270'], - [data-main-rotation='270'] [data-editor-rotation='180'] - ) - .editToolbar { - rotate: 270deg; -} -.superdoc-pdf-viewer - [dir='ltr'] - .annotationEditorLayer - :is( - [data-main-rotation='0'] [data-editor-rotation='90'], - [data-main-rotation='90'] [data-editor-rotation='0'], - [data-main-rotation='180'] [data-editor-rotation='270'], - [data-main-rotation='270'] [data-editor-rotation='180'] - ) - .editToolbar { - inset-inline-end: calc(0px - var(--editor-toolbar-vert-offset)); - inset-block-start: 0; -} -.superdoc-pdf-viewer - [dir='rtl'] - .annotationEditorLayer - :is( - [data-main-rotation='0'] [data-editor-rotation='90'], - [data-main-rotation='90'] [data-editor-rotation='0'], - [data-main-rotation='180'] [data-editor-rotation='270'], - [data-main-rotation='270'] [data-editor-rotation='180'] - ) - .editToolbar { - inset-inline-end: calc(100% + var(--editor-toolbar-vert-offset)); - inset-block-start: 0; -} -.superdoc-pdf-viewer - .annotationEditorLayer - :is( - [data-main-rotation='0'] [data-editor-rotation='180'], - [data-main-rotation='90'] [data-editor-rotation='90'], - [data-main-rotation='180'] [data-editor-rotation='0'], - [data-main-rotation='270'] [data-editor-rotation='270'] - ) - .editToolbar { - rotate: 180deg; - inset-inline-end: 100%; - inset-block-start: calc(0pc - var(--editor-toolbar-vert-offset)); -} -.superdoc-pdf-viewer - .annotationEditorLayer - :is( - [data-main-rotation='0'] [data-editor-rotation='270'], - [data-main-rotation='90'] [data-editor-rotation='180'], - [data-main-rotation='180'] [data-editor-rotation='90'], - [data-main-rotation='270'] [data-editor-rotation='0'] - ) - .editToolbar { - rotate: 90deg; -} -.superdoc-pdf-viewer - [dir='ltr'] - .annotationEditorLayer - :is( - [data-main-rotation='0'] [data-editor-rotation='270'], - [data-main-rotation='90'] [data-editor-rotation='180'], - [data-main-rotation='180'] [data-editor-rotation='90'], - [data-main-rotation='270'] [data-editor-rotation='0'] - ) - .editToolbar { - inset-inline-end: calc(100% + var(--editor-toolbar-vert-offset)); - inset-block-start: 100%; -} -.superdoc-pdf-viewer - [dir='rtl'] - .annotationEditorLayer - :is( - [data-main-rotation='0'] [data-editor-rotation='270'], - [data-main-rotation='90'] [data-editor-rotation='180'], - [data-main-rotation='180'] [data-editor-rotation='90'], - [data-main-rotation='270'] [data-editor-rotation='0'] - ) - .editToolbar { - inset-inline-start: calc(0px - var(--editor-toolbar-vert-offset)); - inset-block-start: 0; -} -.superdoc-pdf-viewer .dialog.altText::backdrop { - -webkit-mask: url(#alttext-manager-mask); - mask: url(#alttext-manager-mask); -} -.superdoc-pdf-viewer .dialog.altText.positioned { - margin: 0; -} -.superdoc-pdf-viewer .dialog.altText #altTextContainer { - width: 300px; - height: -moz-fit-content; - height: fit-content; - display: inline-flex; - flex-direction: column; - align-items: flex-start; - gap: 16px; -} -.superdoc-pdf-viewer .dialog.altText #altTextContainer #overallDescription { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 4px; - align-self: stretch; -} -.superdoc-pdf-viewer .dialog.altText #altTextContainer #overallDescription span { - align-self: stretch; -} -.superdoc-pdf-viewer .dialog.altText #altTextContainer #overallDescription .title { - font-size: 13px; - font-style: normal; - font-weight: 590; -} -.superdoc-pdf-viewer .dialog.altText #altTextContainer #addDescription { - display: flex; - flex-direction: column; - align-items: stretch; - gap: 8px; -} -.superdoc-pdf-viewer .dialog.altText #altTextContainer #addDescription .descriptionArea { - flex: 1; - padding-inline: 24px 10px; -} -.superdoc-pdf-viewer .dialog.altText #altTextContainer #addDescription .descriptionArea textarea { - width: 100%; - min-height: 75px; -} -.superdoc-pdf-viewer .dialog.altText #altTextContainer #buttons { - display: flex; - justify-content: flex-end; - align-items: flex-start; - gap: 8px; - align-self: stretch; -} -.superdoc-pdf-viewer .colorPicker { - --hover-outline-color: #0250bb; - --selected-outline-color: #0060df; - --swatch-border-color: #cfcfd8; -} -@media (prefers-color-scheme: dark) { - .superdoc-pdf-viewer .colorPicker { - --hover-outline-color: #80ebff; - --selected-outline-color: #aaf2ff; - --swatch-border-color: #52525e; - } -} -@media screen and (forced-colors: active) { - .superdoc-pdf-viewer .colorPicker { - --hover-outline-color: Highlight; - --selected-outline-color: var(--hover-outline-color); - --swatch-border-color: ButtonText; - } -} -.superdoc-pdf-viewer .colorPicker .swatch { - width: 16px; - height: 16px; - border: 1px solid var(--swatch-border-color); - border-radius: 100%; - outline-offset: 2px; - box-sizing: border-box; - forced-color-adjust: none; -} -.superdoc-pdf-viewer .colorPicker button:is(:hover, .selected) > .swatch { - border: none; -} -.superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation='0'] .highlightEditor:not(.free) > .editToolbar { - rotate: 0deg; -} -.superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation='90'] .highlightEditor:not(.free) > .editToolbar { - rotate: 270deg; -} -.superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation='180'] .highlightEditor:not(.free) > .editToolbar { - rotate: 180deg; -} -.superdoc-pdf-viewer .annotationEditorLayer[data-main-rotation='270'] .highlightEditor:not(.free) > .editToolbar { - rotate: 90deg; -} -.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor { - position: absolute; - background: transparent; - z-index: 1; - cursor: auto; - max-width: 100%; - max-height: 100%; - border: none; - outline: none; - pointer-events: none; - transform-origin: 0 0; -} -.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor:not(.free) { - transform: none; -} -.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor .internal { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - pointer-events: auto; -} -.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor.disabled .internal { - pointer-events: none; -} -.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor.selectedEditor .internal { - cursor: pointer; -} -.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor .editToolbar { - --editor-toolbar-colorpicker-arrow-image: url(images/toolbarButton-menuArrow.svg); - - transform-origin: center !important; -} -.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor .editToolbar .buttons .colorPicker { - position: relative; - width: auto; - display: flex; - justify-content: center; - align-items: center; - gap: 4px; - padding: 4px; -} -.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor .editToolbar .buttons .colorPicker::after { - content: ''; - -webkit-mask-image: var(--editor-toolbar-colorpicker-arrow-image); - mask-image: var(--editor-toolbar-colorpicker-arrow-image); - -webkit-mask-repeat: no-repeat; - mask-repeat: no-repeat; - -webkit-mask-position: center; - mask-position: center; - display: inline-block; - background-color: var(--editor-toolbar-fg-color); - width: 12px; - height: 12px; -} -.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor .editToolbar .buttons .colorPicker:hover::after { - background-color: var(--editor-toolbar-hover-fg-color); -} -.superdoc-pdf-viewer - .annotationEditorLayer - .highlightEditor - .editToolbar - .buttons - .colorPicker:has(.dropdown:not(.hidden)) { - background-color: var(--editor-toolbar-hover-bg-color); -} -.superdoc-pdf-viewer - .annotationEditorLayer - .highlightEditor - .editToolbar - .buttons - .colorPicker:has(.dropdown:not(.hidden))::after { - scale: -1; -} -.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor .editToolbar .buttons .colorPicker .dropdown { - position: absolute; - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; - gap: 11px; - padding-block: 8px; - border-radius: 6px; - background-color: var(--editor-toolbar-bg-color); - border: 1px solid var(--editor-toolbar-border-color); - box-shadow: var(--editor-toolbar-shadow); - inset-block-start: calc(100% + 4px); - width: calc(100% + 2 * var(--editor-toolbar-padding)); -} -.superdoc-pdf-viewer .annotationEditorLayer .highlightEditor .editToolbar .buttons .colorPicker .dropdown button { - width: 100%; - height: auto; - border: none; - cursor: pointer; - display: flex; - justify-content: center; - align-items: center; - background: none; -} -.superdoc-pdf-viewer - .annotationEditorLayer - .highlightEditor - .editToolbar - .buttons - .colorPicker - .dropdown - button:is(:active, :focus-visible) { - outline: none; -} -.superdoc-pdf-viewer - .annotationEditorLayer - .highlightEditor - .editToolbar - .buttons - .colorPicker - .dropdown - button - > .swatch { - outline-offset: 2px; -} -.superdoc-pdf-viewer - .annotationEditorLayer - .highlightEditor - .editToolbar - .buttons - .colorPicker - .dropdown - button[aria-selected='true'] - > .swatch { - outline: 2px solid var(--selected-outline-color); -} -.superdoc-pdf-viewer - .annotationEditorLayer - .highlightEditor - .editToolbar - .buttons - .colorPicker - .dropdown - button:is(:hover, :active, :focus-visible) - > .swatch { - outline: 2px solid var(--hover-outline-color); -} -.superdoc-pdf-viewer .editorParamsToolbar:has(#highlightParamsToolbarContainer) { - padding: unset; -} -.superdoc-pdf-viewer #highlightParamsToolbarContainer { - height: auto; - padding-inline: 10px; - padding-block: 10px 16px; - gap: 16px; - display: flex; - flex-direction: column; - box-sizing: border-box; -} -.superdoc-pdf-viewer #highlightParamsToolbarContainer .editorParamsLabel { - width: -moz-fit-content; - width: fit-content; - inset-inline-start: 0; -} -.superdoc-pdf-viewer #highlightParamsToolbarContainer .colorPicker { - display: flex; - flex-direction: column; - gap: 8px; -} -.superdoc-pdf-viewer #highlightParamsToolbarContainer .colorPicker .dropdown { - display: flex; - justify-content: space-between; - align-items: center; - flex-direction: row; - height: auto; -} -.superdoc-pdf-viewer #highlightParamsToolbarContainer .colorPicker .dropdown button { - width: auto; - height: auto; - border: none; - cursor: pointer; - display: flex; - justify-content: center; - align-items: center; - background: none; - flex: 0 0 auto; -} -.superdoc-pdf-viewer #highlightParamsToolbarContainer .colorPicker .dropdown button .swatch { - width: 24px; - height: 24px; -} -.superdoc-pdf-viewer #highlightParamsToolbarContainer .colorPicker .dropdown button:is(:active, :focus-visible) { - outline: none; -} -.superdoc-pdf-viewer #highlightParamsToolbarContainer .colorPicker .dropdown button[aria-selected='true'] > .swatch { - outline: 2px solid var(--selected-outline-color); -} -.superdoc-pdf-viewer - #highlightParamsToolbarContainer - .colorPicker - .dropdown - button:is(:hover, :active, :focus-visible) - > .swatch { - outline: 2px solid var(--hover-outline-color); -} -.superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightThickness { - display: flex; - flex-direction: column; - align-items: center; - gap: 4px; - align-self: stretch; -} -.superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightThickness .editorParamsLabel { - width: 100%; - height: auto; - align-self: stretch; -} -.superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightThickness .thicknessPicker { - display: flex; - justify-content: space-between; - align-items: center; - align-self: stretch; - - --example-color: #bfbfc9; -} -@media (prefers-color-scheme: dark) { - .superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightThickness .thicknessPicker { - --example-color: #80808e; - } -} -@media screen and (forced-colors: active) { - .superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightThickness .thicknessPicker { - --example-color: CanvasText; - } -} -.superdoc-pdf-viewer - :is(#highlightParamsToolbarContainer #editorHighlightThickness .thicknessPicker > .editorParamsSlider[disabled]) { - opacity: 0.4; + margin-block:4px; + width:100%; + height:1px; + background-color:var(--divider-color); } -.superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightThickness .thicknessPicker::before, -.superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightThickness .thicknessPicker::after { - content: ''; - width: 8px; - aspect-ratio: 1; - display: block; - border-radius: 100%; - background-color: var(--example-color); +@media (prefers-color-scheme: dark){ +.superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightVisibility .divider{ + --divider-color:#8f8f9d; } -.superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightThickness .thicknessPicker::after { - width: 24px; } -.superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightThickness .thicknessPicker .editorParamsSlider { - width: unset; - height: 14px; +@media screen and (forced-colors: active){ +.superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightVisibility .divider{ + --divider-color:CanvasText; } -.superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightVisibility { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 8px; - align-self: stretch; } -.superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightVisibility .divider { - --divider-color: #d7d7db; - - margin-block: 4px; - width: 100%; - height: 1px; - background-color: var(--divider-color); +.superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightVisibility .toggler{ + display:flex; + justify-content:space-between; + align-items:center; + align-self:stretch; } -@media (prefers-color-scheme: dark) { - .superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightVisibility .divider { - --divider-color: #8f8f9d; - } +.superdoc-pdf-viewer :root{ + --viewer-container-height:0; + --pdfViewer-padding-bottom:0; + --page-margin:1px auto -8px; + --page-border:9px solid transparent; + --spreadHorizontalWrapped-margin-LR:-3.5px; + --loading-icon-delay:400ms; } -@media screen and (forced-colors: active) { - .superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightVisibility .divider { - --divider-color: CanvasText; - } +@media screen and (forced-colors: active){ +.superdoc-pdf-viewer :root{ + --pdfViewer-padding-bottom:9px; + --page-margin:8px auto -1px; + --page-border:1px solid CanvasText; + --spreadHorizontalWrapped-margin-LR:3.5px; } -.superdoc-pdf-viewer #highlightParamsToolbarContainer #editorHighlightVisibility .toggler { - display: flex; - justify-content: space-between; - align-items: center; - align-self: stretch; -} -.superdoc-pdf-viewer :root { - --viewer-container-height: 0; - --pdfViewer-padding-bottom: 0; - --page-margin: 1px auto -8px; - --page-border: 9px solid transparent; - --spreadHorizontalWrapped-margin-LR: -3.5px; - --loading-icon-delay: 400ms; -} -@media screen and (forced-colors: active) { - .superdoc-pdf-viewer :root { - --pdfViewer-padding-bottom: 9px; - --page-margin: 8px auto -1px; - --page-border: 1px solid CanvasText; - --spreadHorizontalWrapped-margin-LR: 3.5px; - } } -.superdoc-pdf-viewer [data-main-rotation='90'] { - transform: rotate(90deg) translateY(-100%); +.superdoc-pdf-viewer [data-main-rotation="90"]{ + transform:rotate(90deg) translateY(-100%); } -.superdoc-pdf-viewer [data-main-rotation='180'] { - transform: rotate(180deg) translate(-100%, -100%); +.superdoc-pdf-viewer [data-main-rotation="180"]{ + transform:rotate(180deg) translate(-100%, -100%); } -.superdoc-pdf-viewer [data-main-rotation='270'] { - transform: rotate(270deg) translateX(-100%); +.superdoc-pdf-viewer [data-main-rotation="270"]{ + transform:rotate(270deg) translateX(-100%); } .superdoc-pdf-viewer #hiddenCopyElement, -.superdoc-pdf-viewer .hiddenCanvasElement { - position: absolute; - top: 0; - left: 0; - width: 0; - height: 0; - display: none; -} -.superdoc-pdf-viewer .pdfViewer { - --scale-factor: 1; +.superdoc-pdf-viewer .hiddenCanvasElement{ + position:absolute; + top:0; + left:0; + width:0; + height:0; + display:none; +} +.superdoc-pdf-viewer .pdfViewer{ + --scale-factor:1; - padding-bottom: var(--pdfViewer-padding-bottom); + padding-bottom:var(--pdfViewer-padding-bottom); - --hcm-highlight-filter: none; - --hcm-highlight-selected-filter: none; + --hcm-highlight-filter:none; + --hcm-highlight-selected-filter:none; } -@media screen and (forced-colors: active) { - .superdoc-pdf-viewer .pdfViewer { - --hcm-highlight-filter: invert(100%); - } +@media screen and (forced-colors: active){ +.superdoc-pdf-viewer .pdfViewer{ + --hcm-highlight-filter:invert(100%); } -.superdoc-pdf-viewer .pdfViewer .canvasWrapper { - overflow: hidden; - width: 100%; - height: 100%; } -.superdoc-pdf-viewer .pdfViewer .canvasWrapper canvas { - margin: 0; - display: block; +.superdoc-pdf-viewer .pdfViewer .canvasWrapper{ + overflow:hidden; + width:100%; + height:100%; } -.superdoc-pdf-viewer .pdfViewer .canvasWrapper canvas[hidden] { - display: none; +.superdoc-pdf-viewer .pdfViewer .canvasWrapper canvas{ + margin:0; + display:block; } -.superdoc-pdf-viewer .pdfViewer .canvasWrapper canvas[zooming] { - width: 100%; - height: 100%; +.superdoc-pdf-viewer .pdfViewer .canvasWrapper canvas[hidden]{ + display:none; } -.superdoc-pdf-viewer .pdfViewer .canvasWrapper canvas .structTree { - contain: strict; +.superdoc-pdf-viewer .pdfViewer .canvasWrapper canvas[zooming]{ + width:100%; + height:100%; } -.superdoc-pdf-viewer .pdfViewer .page { - direction: ltr; - width: 816px; - height: 1056px; - margin: var(--page-margin); - position: relative; - overflow: visible; - border: var(--page-border); - background-clip: content-box; - background-color: rgb(255 255 255); +.superdoc-pdf-viewer .pdfViewer .canvasWrapper canvas .structTree{ + contain:strict; } -.superdoc-pdf-viewer .pdfViewer .dummyPage { - position: relative; - width: 0; - height: var(--viewer-container-height); +.superdoc-pdf-viewer .pdfViewer .page{ + direction:ltr; + width:816px; + height:1056px; + margin:var(--page-margin); + position:relative; + overflow:visible; + border:var(--page-border); + background-clip:content-box; + background-color:rgb(255 255 255); } -.superdoc-pdf-viewer .pdfViewer.noUserSelect { - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; +.superdoc-pdf-viewer .pdfViewer .dummyPage{ + position:relative; + width:0; + height:var(--viewer-container-height); } -.superdoc-pdf-viewer .pdfViewer.removePageBorders .page { - margin: 0 auto 10px; - border: none; +.superdoc-pdf-viewer .pdfViewer.noUserSelect{ + -webkit-user-select:none; + -moz-user-select:none; + user-select:none; } -.superdoc-pdf-viewer .pdfViewer.singlePageView { - display: inline-block; +.superdoc-pdf-viewer .pdfViewer.removePageBorders .page{ + margin:0 auto 10px; + border:none; } -.superdoc-pdf-viewer .pdfViewer.singlePageView .page { - margin: 0; - border: none; +.superdoc-pdf-viewer .pdfViewer.singlePageView{ + display:inline-block; } -.superdoc-pdf-viewer .pdfViewer:is(.scrollHorizontal, .scrollWrapped), -.superdoc-pdf-viewer .spread { - margin-inline: 3.5px; - text-align: center; +.superdoc-pdf-viewer .pdfViewer.singlePageView .page{ + margin:0; + border:none; +} +.superdoc-pdf-viewer .pdfViewer:is(.scrollHorizontal, .scrollWrapped), .superdoc-pdf-viewer .spread{ + margin-inline:3.5px; + text-align:center; } .superdoc-pdf-viewer .pdfViewer.scrollHorizontal, -.superdoc-pdf-viewer .spread { - white-space: nowrap; +.superdoc-pdf-viewer .spread{ + white-space:nowrap; } .superdoc-pdf-viewer .pdfViewer.removePageBorders, -.superdoc-pdf-viewer .pdfViewer:is(.scrollHorizontal, .scrollWrapped) .spread { - margin-inline: 0; +.superdoc-pdf-viewer .pdfViewer:is(.scrollHorizontal, .scrollWrapped) .spread{ + margin-inline:0; } -.superdoc-pdf-viewer .spread :is(.page, .dummyPage), -.superdoc-pdf-viewer .pdfViewer:is(.scrollHorizontal, .scrollWrapped) :is(.page, .spread) { - display: inline-block; - vertical-align: middle; +.superdoc-pdf-viewer .spread :is(.page, .dummyPage), .superdoc-pdf-viewer .pdfViewer:is(.scrollHorizontal, .scrollWrapped) :is(.page, .spread){ + display:inline-block; + vertical-align:middle; } .superdoc-pdf-viewer .spread .page, -.superdoc-pdf-viewer .pdfViewer:is(.scrollHorizontal, .scrollWrapped) .page { - margin-inline: var(--spreadHorizontalWrapped-margin-LR); +.superdoc-pdf-viewer .pdfViewer:is(.scrollHorizontal, .scrollWrapped) .page{ + margin-inline:var(--spreadHorizontalWrapped-margin-LR); } .superdoc-pdf-viewer .pdfViewer.removePageBorders .spread .page, -.superdoc-pdf-viewer .pdfViewer.removePageBorders:is(.scrollHorizontal, .scrollWrapped) .page { - margin-inline: 5px; -} -.superdoc-pdf-viewer .pdfViewer .page.loadingIcon::after { - position: absolute; - top: 0; - left: 0; - content: ''; - width: 100%; - height: 100%; - background: url('images/loading-icon.gif') center no-repeat; - display: none; - transition-property: display; - transition-delay: var(--loading-icon-delay); - z-index: 5; - contain: strict; -} -.superdoc-pdf-viewer .pdfViewer .page.loading::after { - display: block; -} -.superdoc-pdf-viewer .pdfViewer .page:not(.loading)::after { - transition-property: none; - display: none; -} -.superdoc-pdf-viewer .pdfPresentationMode .pdfViewer { - padding-bottom: 0; -} -.superdoc-pdf-viewer .pdfPresentationMode .spread { - margin: 0; -} -.superdoc-pdf-viewer .pdfPresentationMode .pdfViewer .page { - margin: 0 auto; - border: 2px solid transparent; +.superdoc-pdf-viewer .pdfViewer.removePageBorders:is(.scrollHorizontal, .scrollWrapped) .page{ + margin-inline:5px; +} +.superdoc-pdf-viewer .pdfViewer .page.loadingIcon::after{ + position:absolute; + top:0; + left:0; + content:""; + width:100%; + height:100%; + background:url("images/loading-icon.gif") center no-repeat; + display:none; + transition-property:display; + transition-delay:var(--loading-icon-delay); + z-index:5; + contain:strict; +} +.superdoc-pdf-viewer .pdfViewer .page.loading::after{ + display:block; +} +.superdoc-pdf-viewer .pdfViewer .page:not(.loading)::after{ + transition-property:none; + display:none; +} +.superdoc-pdf-viewer .pdfPresentationMode .pdfViewer{ + padding-bottom:0; +} +.superdoc-pdf-viewer .pdfPresentationMode .spread{ + margin:0; +} +.superdoc-pdf-viewer .pdfPresentationMode .pdfViewer .page{ + margin:0 auto; + border:2px solid transparent; } .superdoc-pdf-viewer-container[data-v-6d611c0c] { @@ -4803,39 +4298,39 @@ span[data-v-53e13009] { position: relative; } .superdoc-pdf-viewer__loader[data-v-6d611c0c] { - display: flex; - justify-content: center; - align-items: center; - width: 100%; - min-width: 150px; - min-height: 150px; + display: flex; + justify-content: center; + align-items: center; + width: 100%; + min-width: 150px; + min-height: 150px; } .superdoc-pdf-viewer__loader[data-v-6d611c0c] .n-spin { - --n-color: #1354ff !important; - --n-text-color: #1354ff !important; + --n-color: #1354ff !important; + --n-text-color: #1354ff !important; } .superdoc-pdf-viewer[data-v-6d611c0c] .pdf-page { - border-top: 1px solid #dfdfdf; - border-bottom: 1px solid #dfdfdf; - margin: 0 0 20px 0; - position: relative; - overflow: hidden; + border-top: 1px solid #dfdfdf; + border-bottom: 1px solid #dfdfdf; + margin: 0 0 20px 0; + position: relative; + overflow: hidden; } .superdoc-pdf-viewer[data-v-6d611c0c] .pdf-page:first-child { - border-radius: 16px 16px 0 0; - border-top: none; + border-radius: 16px 16px 0 0; + border-top: none; } .superdoc-pdf-viewer[data-v-6d611c0c] .pdf-page:last-child { - border-radius: 0 0 16px 16px; - border-bottom: none; + border-radius: 0 0 16px 16px; + border-bottom: none; } .superdoc-pdf-viewer[data-v-6d611c0c] .textLayer { - z-index: 2; - position: absolute; + z-index: 2; + position: absolute; } .superdoc-pdf-viewer[data-v-6d611c0c] .textLayer::selection { - background-color: #1355ff66; - mix-blend-mode: difference; + background-color: #1355ff66; + mix-blend-mode: difference; } .comment-doc[data-v-0dbcad34] { @@ -4968,19 +4463,20 @@ img[data-v-7dd69850] { display: none; } + .superdoc.high-contrast { - border-color: #000; + border-color: #000; } .superdoc.high-contrast .super-editor { - border-color: #000; + border-color: #000; } .superdoc.high-contrast .super-editor:focus-within { - border-color: blue; + border-color: blue; } .superdoc .super-editor { - border-radius: 8px; - border: 1px solid #d3d3d3; - box-shadow: 0 0 5px hsla(0, 0%, 0%, 0.05); + border-radius: 8px; + border: 1px solid #d3d3d3; + box-shadow: 0 0 5px hsla(0, 0%, 0%, .05); } .superdoc[data-v-fc218ba5] { @@ -5074,19 +4570,19 @@ img[data-v-7dd69850] { /* 834px is iPad screen size in portrait orientation */ @media (max-width: 834px) { - .superdoc .superdoc__layers[data-v-fc218ba5] { +.superdoc .superdoc__layers[data-v-fc218ba5] { margin: 0; border: 0 !important; box-shadow: none; - } - .superdoc__sub-document[data-v-fc218ba5] { +} +.superdoc__sub-document[data-v-fc218ba5] { max-width: 100%; - } - .superdoc__right-sidebar[data-v-fc218ba5] { +} +.superdoc__right-sidebar[data-v-fc218ba5] { padding: 10px; width: 55px; position: relative; - } +} } /* AI Writer styles */ @@ -5145,7 +4641,7 @@ img[data-v-7dd69850] { fill: transparent; } .ai-tool[data-v-fc218ba5]::before { - content: ''; + content: ""; position: absolute; width: 20px; height: 20px; @@ -5158,14 +4654,12 @@ img[data-v-7dd69850] { rgba(77, 82, 217, 1) 60%, rgb(255, 219, 102) 150% ); - -webkit-mask: url("data:image/svg+xml;charset=utf-8,") - center / contain no-repeat; - mask: url("data:image/svg+xml;charset=utf-8,") - center / contain no-repeat; + -webkit-mask: url("data:image/svg+xml;charset=utf-8,") center / contain no-repeat; + mask: url("data:image/svg+xml;charset=utf-8,") center / contain no-repeat; filter: brightness(1.2); transition: filter 0.2s ease; } .ai-tool[data-v-fc218ba5]:hover::before { filter: brightness(1.3); -} +} /* Tools styles - end */ From 97f28e1b69d57611204b6f1cde877ffb6c455b4c Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Wed, 11 Feb 2026 10:00:12 -0300 Subject: [PATCH 11/18] perf(super-editor): cache SDT nodes in lock plugin state Cache collectSDTNodes() result in ProseMirror plugin state instead of calling it in every handler. Rebuilds only on docChanged, reducing document traversals from 2-3 per keystroke to at most 1 per document-changing transaction. --- .../structured-content-lock-plugin.js | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js b/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js index 7f192f4272..4a2a01f81a 100644 --- a/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js +++ b/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js @@ -18,10 +18,6 @@ export const STRUCTURED_CONTENT_LOCK_KEY = new PluginKey('structuredContentLock' /** * Collect all SDT nodes from the document. - * - * TODO: For large documents, consider caching SDT nodes in plugin state - * (rebuild on docChanged only), early-exit on unlocked nodes, or limiting - * the search to nodes near the current selection for key/input handlers. */ function collectSDTNodes(doc) { const sdtNodes = []; @@ -75,6 +71,16 @@ export function createStructuredContentLockPlugin() { return new Plugin({ key: STRUCTURED_CONTENT_LOCK_KEY, + state: { + init(_, editorState) { + return collectSDTNodes(editorState.doc); + }, + apply(tr, cachedSDTNodes, _oldState, newState) { + if (!tr.docChanged) return cachedSDTNodes; + return collectSDTNodes(newState.doc); + }, + }, + props: { /** * Intercept key events BEFORE any transaction is created. @@ -94,7 +100,7 @@ export function createStructuredContentLockPlugin() { return false; // Let other handlers process } - const sdtNodes = collectSDTNodes(state.doc); + const sdtNodes = STRUCTURED_CONTENT_LOCK_KEY.getState(state); if (sdtNodes.length === 0) { return false; } @@ -126,7 +132,7 @@ export function createStructuredContentLockPlugin() { * Handle text input (typing) for content-locked nodes */ handleTextInput(view, from, to, _text) { - const sdtNodes = collectSDTNodes(view.state.doc); + const sdtNodes = STRUCTURED_CONTENT_LOCK_KEY.getState(view.state); if (sdtNodes.length === 0) { return false; } @@ -150,7 +156,7 @@ export function createStructuredContentLockPlugin() { return true; } - const sdtNodes = collectSDTNodes(state.doc); + const sdtNodes = STRUCTURED_CONTENT_LOCK_KEY.getState(state); if (sdtNodes.length === 0) { return true; } From 1d15b4b497313bbc92d6a93b7159763f55cbbd15 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Wed, 11 Feb 2026 10:03:48 -0300 Subject: [PATCH 12/18] docs(super-editor): add clarifying comments in lock plugin Add comment explaining why AttrStep/node-mark steps are safely skipped in filterTransaction, and note the backspace range approximation with its filterTransaction safety net. --- .../structured-content/structured-content-lock-plugin.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js b/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js index 4a2a01f81a..578cd6bc85 100644 --- a/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js +++ b/packages/super-editor/src/extensions/structured-content/structured-content-lock-plugin.js @@ -109,7 +109,10 @@ export function createStructuredContentLockPlugin() { let affectedFrom = from; let affectedTo = to; - // If selection is collapsed, backspace/delete affects adjacent position + // If selection is collapsed, backspace/delete affects adjacent position. + // Note: this is a single-character approximation. joinBackward at paragraph + // boundaries can span wider ranges, but filterTransaction catches the real + // step range as a safety net (with a possible brief cursor jump). if (from === to) { if (isBackspace && from > 0) { affectedFrom = from - 1; @@ -162,6 +165,8 @@ export function createStructuredContentLockPlugin() { } for (const step of tr.steps) { + // Skip steps without from/to (AttrStep, AddNodeMarkStep, RemoveNodeMarkStep) — + // these change metadata, not content, so they can't violate lock rules. if (step.from === undefined || step.to === undefined) { continue; } From 49aaaab5d92af38ddd950c536f6473eba694ac29 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Wed, 11 Feb 2026 10:09:29 -0300 Subject: [PATCH 13/18] fix(layout-engine): remove dead CSS hover rules and clarify SDT border intent Remove base hover rules for block/inline SDTs that were shadowed by the more specific [data-lock-mode] selectors (dead code since data-lock-mode is set on all SDTs). Consolidate block/inline lock-mode hover rules into a single grouped selector. Add comments explaining the intentional border-hidden-by-default behavior that matches Word. --- .../painters/dom/src/renderer.ts | 2 ++ .../layout-engine/painters/dom/src/styles.ts | 36 +++++-------------- 2 files changed, 11 insertions(+), 27 deletions(-) diff --git a/packages/layout-engine/painters/dom/src/renderer.ts b/packages/layout-engine/painters/dom/src/renderer.ts index 6453da286c..5f37fa1ed0 100644 --- a/packages/layout-engine/painters/dom/src/renderer.ts +++ b/packages/layout-engine/painters/dom/src/renderer.ts @@ -5270,6 +5270,8 @@ export class DomPainter { this.setDatasetString(el, 'sdtScope', metadata.scope); this.setDatasetString(el, 'sdtTag', metadata.tag); this.setDatasetString(el, 'sdtAlias', metadata.alias); + // Always set lockMode (defaulting to 'unlocked') so the CSS selector [data-lock-mode] + // applies to all SDTs — this hides borders by default, matching Word behavior. this.setDatasetString(el, 'lockMode', metadata.lockMode || 'unlocked'); } else if (metadata.type === 'documentSection') { this.setDatasetString(el, 'sdtSectionTitle', metadata.title); diff --git a/packages/layout-engine/painters/dom/src/styles.ts b/packages/layout-engine/painters/dom/src/styles.ts index 8b3c8bb4f5..f13eb398b3 100644 --- a/packages/layout-engine/painters/dom/src/styles.ts +++ b/packages/layout-engine/painters/dom/src/styles.ts @@ -394,13 +394,8 @@ const SDT_CONTAINER_STYLES = ` text-overflow: ellipsis; } -/* Hover effect for block structured content (via event delegation class) */ -.superdoc-structured-content-block.sdt-hover { - border-color: #629be7 !important; - background-color: rgba(98, 155, 231, 0.08); - z-index: 9999999; -} - +/* Hover effect for block structured content (via event delegation class). + * Shows label on hover — border reveal is handled by the lock-mode hover rule below. */ .superdoc-structured-content-block.sdt-hover .superdoc-structured-content__label { display: inline-flex; } @@ -441,13 +436,6 @@ const SDT_CONTAINER_STYLES = ` z-index: 10; } -/* Hover effect for inline structured content */ -.superdoc-structured-content-inline:hover { - border-color: #629be7 !important; - background-color: rgba(98, 155, 231, 0.08); - z-index: 9999999; -} - /* Inline structured content label - shown on hover */ .superdoc-structured-content-inline__label { position: absolute; @@ -469,10 +457,8 @@ const SDT_CONTAINER_STYLES = ` display: block; } -/* Lock mode styles for structured content - matches Word appearance exactly */ -/* Default: background color only, no border. Border appears on hover/focus */ - -/* Lock mode: hide border by default, show on hover. +/* SDT border visibility — matches Word behavior: borders hidden by default, shown on hover. + * data-lock-mode is set on ALL SDTs (including unlocked) so this applies universally. * Use border-color (not border shorthand) to preserve continuation rules * that remove border-top/border-bottom on multi-fragment SDT containers. */ .superdoc-structured-content-block[data-lock-mode], @@ -480,15 +466,11 @@ const SDT_CONTAINER_STYLES = ` border-color: transparent; } -/* Show blue border on hover for all lock modes. - * Use !important on border-color to override the transparent default above - * and any continuation rules that remove border-top/border-bottom. */ -.superdoc-structured-content-block[data-lock-mode].sdt-hover { - border-color: #629be7 !important; - background-color: rgba(98, 155, 231, 0.08); - z-index: 9999999; -} - +/* Reveal blue border + highlight on hover. + * Block SDTs use .sdt-hover class (event delegation for multi-fragment coordination). + * Inline SDTs use :hover (single element, no coordination needed). + * !important overrides transparent default and continuation border rules. */ +.superdoc-structured-content-block[data-lock-mode].sdt-hover, .superdoc-structured-content-inline[data-lock-mode]:hover { border-color: #629be7 !important; background-color: rgba(98, 155, 231, 0.08); From 0369553c7b6c630b5f16881a4b5213a32e4f6c1d Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Wed, 11 Feb 2026 10:12:43 -0300 Subject: [PATCH 14/18] perf(layout-engine): skip redundant querySelectorAll on same SDT hover Early return when sdtId === hoveredSdtId to avoid unnecessary querySelectorAll + classList.add on every mouseover pixel within the same SDT. --- packages/layout-engine/painters/dom/src/utils/sdt-hover.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/layout-engine/painters/dom/src/utils/sdt-hover.ts b/packages/layout-engine/painters/dom/src/utils/sdt-hover.ts index 05c685bf5f..a85ac063bf 100644 --- a/packages/layout-engine/painters/dom/src/utils/sdt-hover.ts +++ b/packages/layout-engine/painters/dom/src/utils/sdt-hover.ts @@ -28,7 +28,9 @@ export class SdtGroupedHover { const target = (e.target as HTMLElement).closest?.(SDT_BLOCK_SELECTOR) as HTMLElement | null; const sdtId = target?.dataset.sdtId ?? null; - if (this.hoveredSdtId && this.hoveredSdtId !== sdtId) { + if (sdtId === this.hoveredSdtId) return; + + if (this.hoveredSdtId) { sdtElementsById(mount, this.hoveredSdtId).forEach((el) => el.classList.remove(HOVER_CLASS)); } From a70f784a13e0f6cc3d01e26dc568873aa5fca5de Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Wed, 11 Feb 2026 10:17:56 -0300 Subject: [PATCH 15/18] refactor(layout-engine): extract shared inline SDT wrapper helpers and add SDT_HOVER to constants Extract resolveRunSdtId, createInlineSdtWrapper, and expandSdtWrapperPmRange as shared private methods on DomPainter, deduplicating ~30 lines between the geometry and run-based inline SDT rendering paths. Add SDT_HOVER to DOM_CLASS_NAMES and use constants in sdt-hover.ts instead of hardcoded strings. --- .../painters/dom/src/constants.ts | 6 + .../painters/dom/src/renderer.ts | 113 +++++++++--------- .../painters/dom/src/utils/sdt-hover.ts | 8 +- 3 files changed, 67 insertions(+), 60 deletions(-) diff --git a/packages/layout-engine/painters/dom/src/constants.ts b/packages/layout-engine/painters/dom/src/constants.ts index b5adb992ec..67b17b8219 100644 --- a/packages/layout-engine/painters/dom/src/constants.ts +++ b/packages/layout-engine/painters/dom/src/constants.ts @@ -55,6 +55,12 @@ export const DOM_CLASS_NAMES = { * Class name for document section containers. */ DOCUMENT_SECTION: 'superdoc-document-section', + + /** + * Class name added to block SDT fragments on hover via event delegation. + * Applied/removed by SdtGroupedHover to highlight all fragments of the same SDT. + */ + SDT_HOVER: 'sdt-hover', } as const; /** diff --git a/packages/layout-engine/painters/dom/src/renderer.ts b/packages/layout-engine/painters/dom/src/renderer.ts index 5f37fa1ed0..088b34b878 100644 --- a/packages/layout-engine/painters/dom/src/renderer.ts +++ b/packages/layout-engine/painters/dom/src/renderer.ts @@ -4673,51 +4673,28 @@ export class DomPainter { * when the run has inline structuredContent metadata. */ const appendToLineGeo = (elem: HTMLElement, runForSdt: Run, elemLeftPx: number, elemWidthPx: number) => { - const runSdt = (runForSdt as TextRun).sdt; - const isInlineSdt = runSdt?.type === 'structuredContent' && runSdt?.scope === 'inline'; - const thisRunSdtId = isInlineSdt && runSdt?.id ? String(runSdt.id) : null; + const resolved = this.resolveRunSdtId(runForSdt); + const thisRunSdtId = resolved?.sdtId ?? null; if (thisRunSdtId !== geoSdtId) { closeGeoSdtWrapper(); } - if (isInlineSdt && thisRunSdtId && this.doc) { + if (resolved && this.doc) { if (!geoSdtWrapper) { - geoSdtWrapper = this.doc.createElement('span'); - geoSdtWrapper.className = DOM_CLASS_NAMES.INLINE_SDT_WRAPPER; - geoSdtWrapper.dataset.layoutEpoch = String(this.layoutEpoch); + geoSdtWrapper = this.createInlineSdtWrapper(resolved.sdt); geoSdtId = thisRunSdtId; - this.applySdtDataset(geoSdtWrapper, runSdt!); geoSdtWrapperLeft = elemLeftPx; geoSdtMaxRight = elemLeftPx; geoSdtWrapper.style.position = 'absolute'; geoSdtWrapper.style.left = `${elemLeftPx}px`; geoSdtWrapper.style.top = '0px'; geoSdtWrapper.style.height = `${line.lineHeight}px`; - const alias = (runSdt as { alias?: string })?.alias || 'Inline content'; - const labelEl = this.doc.createElement('span'); - labelEl.className = `${DOM_CLASS_NAMES.INLINE_SDT_WRAPPER}__label`; - labelEl.textContent = alias; - geoSdtWrapper.appendChild(labelEl); } // Adjust element left to be relative to wrapper elem.style.left = `${elemLeftPx - geoSdtWrapperLeft}px`; geoSdtMaxRight = Math.max(geoSdtMaxRight, elemLeftPx + elemWidthPx); - // Track PM positions on wrapper to span all contained runs - const pmStart = (runForSdt as TextRun).pmStart; - const pmEnd = (runForSdt as TextRun).pmEnd; - if (pmStart != null) { - const curStart = geoSdtWrapper.dataset.pmStart; - if (!curStart || pmStart < parseInt(curStart, 10)) { - geoSdtWrapper.dataset.pmStart = String(pmStart); - } - } - if (pmEnd != null) { - const curEnd = geoSdtWrapper.dataset.pmEnd; - if (!curEnd || pmEnd > parseInt(curEnd, 10)) { - geoSdtWrapper.dataset.pmEnd = String(pmEnd); - } - } + this.expandSdtWrapperPmRange(geoSdtWrapper, (runForSdt as TextRun).pmStart, (runForSdt as TextRun).pmEnd); geoSdtWrapper.appendChild(elem); } else { el.appendChild(elem); @@ -4921,9 +4898,8 @@ export class DomPainter { runsForLine.forEach((run) => { // Check if this run has inline structuredContent SDT - const runSdt = (run as TextRun).sdt; - const isInlineSdt = runSdt?.type === 'structuredContent' && runSdt?.scope === 'inline'; - const runSdtId = isInlineSdt && runSdt?.id ? String(runSdt.id) : null; + const resolved = this.resolveRunSdtId(run); + const runSdtId = resolved?.sdtId ?? null; // If SDT context changed, close the current wrapper if (runSdtId !== currentInlineSdtId) { @@ -4980,35 +4956,12 @@ export class DomPainter { } // If this run has inline SDT, add to or create wrapper - if (isInlineSdt && runSdtId && this.doc) { + if (resolved && this.doc) { if (!currentInlineSdtWrapper) { - // Create new wrapper for this SDT group - currentInlineSdtWrapper = this.doc.createElement('span'); - currentInlineSdtWrapper.className = DOM_CLASS_NAMES.INLINE_SDT_WRAPPER; - currentInlineSdtWrapper.dataset.layoutEpoch = String(this.layoutEpoch); + currentInlineSdtWrapper = this.createInlineSdtWrapper(resolved.sdt); currentInlineSdtId = runSdtId; - // Apply SDT metadata to wrapper - this.applySdtDataset(currentInlineSdtWrapper, runSdt); - // Add label element for hover display - const alias = (runSdt as { alias?: string })?.alias || 'Inline content'; - const labelEl = this.doc.createElement('span'); - labelEl.className = `${DOM_CLASS_NAMES.INLINE_SDT_WRAPPER}__label`; - labelEl.textContent = alias; - currentInlineSdtWrapper.appendChild(labelEl); - } - // Update PM positions on wrapper to span all contained runs - const wrapperPmStart = currentInlineSdtWrapper.dataset.pmStart; - const wrapperPmEnd = currentInlineSdtWrapper.dataset.pmEnd; - if (run.pmStart != null) { - if (!wrapperPmStart || run.pmStart < parseInt(wrapperPmStart, 10)) { - currentInlineSdtWrapper.dataset.pmStart = String(run.pmStart); - } - } - if (run.pmEnd != null) { - if (!wrapperPmEnd || run.pmEnd > parseInt(wrapperPmEnd, 10)) { - currentInlineSdtWrapper.dataset.pmEnd = String(run.pmEnd); - } } + this.expandSdtWrapperPmRange(currentInlineSdtWrapper, run.pmStart, run.pmEnd); currentInlineSdtWrapper.appendChild(elem); } else { el.appendChild(elem); @@ -5240,6 +5193,52 @@ export class DomPainter { } } + /** + * Resolve the inline SDT id from a run, or null if the run is not inside an inline SDT. + */ + private resolveRunSdtId(run: Run): { sdtId: string; sdt: SdtMetadata } | null { + const sdt = (run as TextRun).sdt; + if (sdt?.type === 'structuredContent' && sdt?.scope === 'inline' && sdt?.id) { + return { sdtId: String(sdt.id), sdt }; + } + return null; + } + + /** + * Create an inline SDT wrapper `` with className, layoutEpoch, dataset, and label. + * Shared by both the geometry and run-based rendering paths. + */ + private createInlineSdtWrapper(sdt: SdtMetadata): HTMLElement { + const wrapper = this.doc!.createElement('span'); + wrapper.className = DOM_CLASS_NAMES.INLINE_SDT_WRAPPER; + wrapper.dataset.layoutEpoch = String(this.layoutEpoch); + this.applySdtDataset(wrapper, sdt); + const alias = (sdt as { alias?: string })?.alias || 'Inline content'; + const labelEl = this.doc!.createElement('span'); + labelEl.className = `${DOM_CLASS_NAMES.INLINE_SDT_WRAPPER}__label`; + labelEl.textContent = alias; + wrapper.appendChild(labelEl); + return wrapper; + } + + /** + * Expand the PM position range tracked on an SDT wrapper to include a new run's range. + */ + private expandSdtWrapperPmRange(wrapper: HTMLElement, pmStart?: number | null, pmEnd?: number | null): void { + if (pmStart != null) { + const cur = wrapper.dataset.pmStart; + if (!cur || pmStart < parseInt(cur, 10)) { + wrapper.dataset.pmStart = String(pmStart); + } + } + if (pmEnd != null) { + const cur = wrapper.dataset.pmEnd; + if (!cur || pmEnd > parseInt(cur, 10)) { + wrapper.dataset.pmEnd = String(pmEnd); + } + } + } + /** * Applies SDT (Structured Document Tag) metadata to an element's dataset as data-sdt-* attributes. * Supports field annotations, structured content, document sections, and doc parts. diff --git a/packages/layout-engine/painters/dom/src/utils/sdt-hover.ts b/packages/layout-engine/painters/dom/src/utils/sdt-hover.ts index a85ac063bf..ce758ca047 100644 --- a/packages/layout-engine/painters/dom/src/utils/sdt-hover.ts +++ b/packages/layout-engine/painters/dom/src/utils/sdt-hover.ts @@ -6,11 +6,13 @@ * simultaneously via the `.sdt-hover` CSS class. */ -const SDT_BLOCK_SELECTOR = '.superdoc-structured-content-block[data-sdt-id]'; -const HOVER_CLASS = 'sdt-hover'; +import { DOM_CLASS_NAMES } from '../constants.js'; + +const SDT_BLOCK_SELECTOR = `.${DOM_CLASS_NAMES.BLOCK_SDT}[data-sdt-id]`; +const HOVER_CLASS = DOM_CLASS_NAMES.SDT_HOVER; function sdtElementsById(root: HTMLElement, sdtId: string): NodeListOf { - return root.querySelectorAll(`.superdoc-structured-content-block[data-sdt-id="${sdtId}"]`); + return root.querySelectorAll(`.${DOM_CLASS_NAMES.BLOCK_SDT}[data-sdt-id="${sdtId}"]`); } export class SdtGroupedHover { From cc6a79d64da893b94b565f54d8af846763cd3b8b Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Wed, 11 Feb 2026 10:57:44 -0300 Subject: [PATCH 16/18] feat(layout-engine): show SDT borders by default instead of only on hover Remove the transparent border-color override so SDT containers always display their blue border, making them visible across the document without needing to hover. Hover still adds background highlight. --- .../layout-engine/painters/dom/src/renderer.ts | 3 +-- .../layout-engine/painters/dom/src/styles.ts | 17 ++++------------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/packages/layout-engine/painters/dom/src/renderer.ts b/packages/layout-engine/painters/dom/src/renderer.ts index 088b34b878..c06538fbd7 100644 --- a/packages/layout-engine/painters/dom/src/renderer.ts +++ b/packages/layout-engine/painters/dom/src/renderer.ts @@ -5269,8 +5269,7 @@ export class DomPainter { this.setDatasetString(el, 'sdtScope', metadata.scope); this.setDatasetString(el, 'sdtTag', metadata.tag); this.setDatasetString(el, 'sdtAlias', metadata.alias); - // Always set lockMode (defaulting to 'unlocked') so the CSS selector [data-lock-mode] - // applies to all SDTs — this hides borders by default, matching Word behavior. + // Always set lockMode (defaulting to 'unlocked') so CSS can target all SDTs uniformly. this.setDatasetString(el, 'lockMode', metadata.lockMode || 'unlocked'); } else if (metadata.type === 'documentSection') { this.setDatasetString(el, 'sdtSectionTitle', metadata.title); diff --git a/packages/layout-engine/painters/dom/src/styles.ts b/packages/layout-engine/painters/dom/src/styles.ts index f13eb398b3..6b1e2debf6 100644 --- a/packages/layout-engine/painters/dom/src/styles.ts +++ b/packages/layout-engine/painters/dom/src/styles.ts @@ -457,22 +457,13 @@ const SDT_CONTAINER_STYLES = ` display: block; } -/* SDT border visibility — matches Word behavior: borders hidden by default, shown on hover. - * data-lock-mode is set on ALL SDTs (including unlocked) so this applies universally. - * Use border-color (not border shorthand) to preserve continuation rules - * that remove border-top/border-bottom on multi-fragment SDT containers. */ -.superdoc-structured-content-block[data-lock-mode], -.superdoc-structured-content-inline[data-lock-mode] { - border-color: transparent; -} - -/* Reveal blue border + highlight on hover. +/* Hover highlight for SDT containers. + * Blue border is always visible (set in base rules above). + * Hover adds background highlight and z-index boost. * Block SDTs use .sdt-hover class (event delegation for multi-fragment coordination). - * Inline SDTs use :hover (single element, no coordination needed). - * !important overrides transparent default and continuation border rules. */ + * Inline SDTs use :hover (single element, no coordination needed). */ .superdoc-structured-content-block[data-lock-mode].sdt-hover, .superdoc-structured-content-inline[data-lock-mode]:hover { - border-color: #629be7 !important; background-color: rgba(98, 155, 231, 0.08); z-index: 9999999; } From fc44ca36926b78a83f8715e5ba777e1b63f578c4 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Wed, 11 Feb 2026 10:59:53 -0300 Subject: [PATCH 17/18] fix(layout-engine): collapse double borders between adjacent SDT blocks Remove top border and top corner radius on SDT blocks that immediately follow another SDT block, preventing the 2px double-border effect when multiple SDT containers are stacked vertically. --- packages/layout-engine/painters/dom/src/styles.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/layout-engine/painters/dom/src/styles.ts b/packages/layout-engine/painters/dom/src/styles.ts index 6b1e2debf6..10dad860be 100644 --- a/packages/layout-engine/painters/dom/src/styles.ts +++ b/packages/layout-engine/painters/dom/src/styles.ts @@ -425,6 +425,13 @@ const SDT_CONTAINER_STYLES = ` border-bottom: none; } +/* Collapse double borders between adjacent SDT blocks */ +.superdoc-structured-content-block + .superdoc-structured-content-block { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + /* Structured Content Inline - Inline wrapper with blue border */ .superdoc-structured-content-inline { padding: 1px; From 5b91684fb598207991e2fa3cad5a0947d4b080df Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Wed, 11 Feb 2026 11:05:30 -0300 Subject: [PATCH 18/18] feat(super-editor): select all inline SDT content on first click When clicking into an inline structuredContent node from outside, the entire content is selected so the user can immediately type to replace it. Clicking again while already inside allows normal cursor placement. Uses appendTransaction to work in both editing and presentation mode. --- .../structured-content-select-plugin.js | 58 +++++++++++++++++++ .../structured-content/structured-content.js | 3 +- 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 packages/super-editor/src/extensions/structured-content/structured-content-select-plugin.js diff --git a/packages/super-editor/src/extensions/structured-content/structured-content-select-plugin.js b/packages/super-editor/src/extensions/structured-content/structured-content-select-plugin.js new file mode 100644 index 0000000000..fa6fa1eeec --- /dev/null +++ b/packages/super-editor/src/extensions/structured-content/structured-content-select-plugin.js @@ -0,0 +1,58 @@ +import { Plugin, TextSelection } from 'prosemirror-state'; + +/** + * Select-all-on-click plugin for inline StructuredContent nodes. + * + * When a click places a collapsed cursor inside an inline SDT and the previous + * selection was outside that SDT, the entire SDT content is selected. This + * matches Word's content control behavior: first click selects all for easy + * replacement, second click (cursor already inside) allows normal positioning. + * + * Uses appendTransaction so it works in both editing mode (PM DOM clicks) and + * presentation mode (PresentationEditor dispatched selections). + */ +export function createStructuredContentSelectPlugin() { + return new Plugin({ + appendTransaction(transactions, oldState, newState) { + const { selection } = newState; + + // Only for collapsed selections (cursor placement, not range selections) + if (!selection.empty) return null; + + // Only when selection actually changed + if (oldState.selection.eq(newState.selection)) return null; + + // Only for selection-only transactions (no doc changes — filters out + // typing, paste, etc. that also move the cursor) + if (transactions.some((tr) => tr.docChanged)) return null; + + const $pos = selection.$from; + + // Walk up to find an enclosing inline structuredContent node + for (let d = $pos.depth; d > 0; d--) { + const node = $pos.node(d); + if (node.type.name === 'structuredContent') { + const sdtStart = $pos.before(d); + const contentFrom = $pos.start(d); + const contentTo = $pos.end(d); + + // Don't select empty content + if (contentFrom === contentTo) return null; + + // If old selection was already inside this same SDT, allow normal + // cursor placement (second click / arrow navigation within SDT) + const old$pos = oldState.selection.$from; + for (let od = old$pos.depth; od > 0; od--) { + if (old$pos.node(od).type.name === 'structuredContent' && old$pos.before(od) === sdtStart) { + return null; + } + } + + return newState.tr.setSelection(TextSelection.create(newState.doc, contentFrom, contentTo)); + } + } + + return null; + }, + }); +} diff --git a/packages/super-editor/src/extensions/structured-content/structured-content.js b/packages/super-editor/src/extensions/structured-content/structured-content.js index 8a33cd3422..eaa495d6eb 100644 --- a/packages/super-editor/src/extensions/structured-content/structured-content.js +++ b/packages/super-editor/src/extensions/structured-content/structured-content.js @@ -1,6 +1,7 @@ import { Node, Attribute } from '@core/index'; import { StructuredContentInlineView } from './StructuredContentInlineView'; import { createStructuredContentLockPlugin } from './structured-content-lock-plugin'; +import { createStructuredContentSelectPlugin } from './structured-content-select-plugin'; export const structuredContentClass = 'sd-structured-content'; export const structuredContentInnerClass = 'sd-structured-content__content'; @@ -115,7 +116,7 @@ export const StructuredContent = Node.create({ }, addPmPlugins() { - return [createStructuredContentLockPlugin()]; + return [createStructuredContentLockPlugin(), createStructuredContentSelectPlugin()]; }, addNodeView() {