diff --git a/external/stylelint/mozcentral-tokens.css b/external/stylelint/mozcentral-tokens.css new file mode 100644 index 0000000000000..4f02be1fb03be --- /dev/null +++ b/external/stylelint/mozcentral-tokens.css @@ -0,0 +1,91 @@ +/* Copyright 2026 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:root { + --background-color-box: initial; + --background-color-box-info: initial; + --background-color-canvas: initial; + --background-color-critical: initial; + --background-color-information: initial; + --background-color-success: initial; + --border-color: initial; + --border-color-deemphasized: initial; + --border-color-interactive: initial; + --border-color-interactive-hover: initial; + --border-radius-circle: initial; + --border-radius-large: initial; + --border-radius-medium: initial; + --border-radius-small: initial; + --border-radius-xsmall: initial; + --box-shadow-level-2: initial; + --box-shadow-level-3: initial; + --button-background-color: initial; + --button-background-color-active: initial; + --button-background-color-ghost-hover: initial; + --button-background-color-hover: initial; + --button-background-color-menu-active: initial; + --button-background-color-menu-hover: initial; + --button-border: initial; + --button-border-color: initial; + --button-border-color-active: initial; + --button-border-color-hover: initial; + --button-border-radius: initial; + --button-font-weight: initial; + --button-min-height: initial; + --button-opacity-disabled: initial; + --button-padding-block: initial; + --button-padding-inline: initial; + --button-text-color: initial; + --button-text-color-hover: initial; + --button-text-color-menu-active: initial; + --button-text-color-menu-hover: initial; + --button-text-color-primary: initial; + --color-accent-attention: initial; + --color-accent-primary: initial; + --color-accent-primary-active: initial; + --color-gray-15: initial; + --color-gray-85: initial; + --focus-outline: initial; + --focus-outline-inset: initial; + --focus-outline-offset: initial; + --font-size-root: initial; + --font-size-small: initial; + --font-weight: initial; + --icon-color: initial; + --icon-color-critical: initial; + --icon-color-information: initial; + --input-search-border-radius: initial; + --link-color: initial; + --link-color-hover: initial; + --panel-border-radius: initial; + --panel-menuitem-border-radius: initial; + --panel-menuitem-margin: initial; + --panel-menuitem-margin-inline: initial; + --panel-menuitem-padding: initial; + --panel-menuitem-padding-inline: initial; + --size-item-large: initial; + --size-item-medium: initial; + --space-large: initial; + --space-medium: initial; + --space-small: initial; + --space-xsmall: initial; + --space-xxsmall: initial; + --text-color: initial; + --text-color-deemphasized: initial; + --toolbar-background-color: initial; + --toolbarbutton-background-color-active: initial; + --toolbarbutton-background-color-hover: initial; + --toolbarbutton-icon-fill: initial; +} diff --git a/src/core/annotation.js b/src/core/annotation.js index 39ed7982bb2bb..5934f251952b0 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -757,7 +757,8 @@ class Annotation { annotationGlobals.structTreeRoot.addAnnotationIdToPage( params.pageRef, - structParent + structParent, + this.ref ); } diff --git a/src/core/document.js b/src/core/document.js index 46ddce9c70b07..7e1adf0a68316 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -1980,7 +1980,7 @@ class PDFDocument { for (const [name, promises] of fieldPromises) { allPromises.push( Promise.all(promises).then(fields => { - fields = fields.filter(field => !!field); + fields = fields.filter(Boolean); if (fields.length > 0) { allFields.set(name, fields); } diff --git a/src/core/editor/pdf_editor.js b/src/core/editor/pdf_editor.js index 1bcf7c5736fea..7de6ab47e60f7 100644 --- a/src/core/editor/pdf_editor.js +++ b/src/core/editor/pdf_editor.js @@ -1291,7 +1291,7 @@ class PDFEditor { } await Promise.all(promises); - newAnnotations = newAnnotations.filter(annot => !!annot); + newAnnotations = newAnnotations.filter(Boolean); pageData.annotations = newAnnotations.length > 0 ? newAnnotations : null; pageData.documentData.hasSignatureAnnotations ||= hasSignatureAnnotations; } @@ -2377,7 +2377,7 @@ class PDFEditor { const numPages = document.numPages; const labelsByPageIndex = new Map(); const oldPageIndices = new Set( - this.oldPages.filter(p => !!p).map(({ page: { pageIndex } }) => pageIndex) + this.oldPages.filter(Boolean).map(({ page: { pageIndex } }) => pageIndex) ); let currentLabel = null; let stFirstIndex = -1; diff --git a/src/core/fonts.js b/src/core/fonts.js index 58e07bceaad7f..3601ac67af88f 100644 --- a/src/core/fonts.js +++ b/src/core/fonts.js @@ -966,7 +966,7 @@ function createNameTable(name, proto) { proto[0][8] || "Unknown", // 8.Manufacturer proto[0][9] || "Unknown", // 9.Designer ]; - const stringsBytes = strings.map(s => stringToBytes(s)); + const stringsBytes = strings.map(stringToBytes); // Mac want 1-byte per character strings while Windows want // 2-bytes per character, so duplicate the names table diff --git a/src/core/struct_tree.js b/src/core/struct_tree.js index 480b5ca58aa96..d3a19abf2500f 100644 --- a/src/core/struct_tree.js +++ b/src/core/struct_tree.js @@ -101,17 +101,22 @@ class StructTreeRoot { : -1; } - #addIdToPage(pageRef, id, type) { + #addIdToPage(pageRef, id, type, objId) { if (!(pageRef instanceof Ref) || id < 0) { return; } (this.structParentIds ??= new RefMap()) .getOrPutComputed(pageRef, makeArr) - .push([id, type]); + .push([id, type, objId]); } - addAnnotationIdToPage(pageRef, id) { - this.#addIdToPage(pageRef, id, StructElementType.ANNOTATION); + addAnnotationIdToPage(pageRef, id, ref) { + this.#addIdToPage( + pageRef, + id, + StructElementType.ANNOTATION, + ref instanceof Ref ? ref.toString() : null + ); } static async canCreateStructureTree({ @@ -728,7 +733,7 @@ class StructElementNode { if (Array.isArray(headers)) { const ids = headers .filter(header => typeof header === "string") - .map(header => stringToPDFString(header)); + .map(stringToPDFString); if (ids.length > 0) { map.set("headers", ids); } @@ -924,18 +929,20 @@ class StructTreePage { if (!ids) { return; } - for (const [elemId, type] of ids) { + for (const [elemId, type, objId] of ids) { const obj = parentTree.get(elemId); - if (obj) { - const elem = this.addNode(this.xref.fetchIfRef(obj), map); - if ( - elem?.kids?.length === 1 && - elem.kids[0].type === StructElementType.OBJECT - ) { - // The node in the struct tree is wrapping an object (annotation - // or xobject), so we need to update the type of the node to match - // the type of the object. - elem.kids[0].type = type; + if (!obj) { + continue; + } + const elem = this.addNode(this.xref.fetchIfRef(obj), map); + if (!elem || !objId) { + continue; + } + // Match the annotation by object reference because its structure + // element may have other children. + for (const kid of elem.kids) { + if (kid.type === StructElementType.OBJECT && kid.refObjId === objId) { + kid.type = type; } } } diff --git a/src/core/worker.js b/src/core/worker.js index 9324ef1b374e2..ddc9d0991de40 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -515,7 +515,7 @@ class WorkerMessageHandler { } await Promise.all(pagePromises); const annotations = await Promise.all(annotationPromises); - return annotations.filter(a => !!a); + return annotations.filter(Boolean); } finally { if (task) { finishWorkerTask(task); diff --git a/src/core/xfa/config.js b/src/core/xfa/config.js index 832933896ae1f..a6006608fa862 100644 --- a/src/core/xfa/config.js +++ b/src/core/xfa/config.js @@ -426,7 +426,7 @@ class EquateRange extends XFAObject { for (let range of unicodeRange .split(",") .map(x => x.trim()) - .filter(x => !!x)) { + .filter(Boolean)) { range = range.split("-", 2).map(x => { const found = x.match(unicodeRegex); if (!found) { diff --git a/src/core/xfa/template.js b/src/core/xfa/template.js index a9c52738670a9..9964cfd848a23 100644 --- a/src/core/xfa/template.js +++ b/src/core/xfa/template.js @@ -5703,7 +5703,7 @@ class Text extends ContentObject { if (typeof this[$content] === "string") { return this[$content] .split(/[\u2029\u2028\n]/) - .filter(line => !!line) + .filter(Boolean) .join("\n"); } return this[$content][$text](); diff --git a/src/core/xfa/xhtml.js b/src/core/xfa/xhtml.js index d0f1cf34950c4..614c26aecbab5 100644 --- a/src/core/xfa/xhtml.js +++ b/src/core/xfa/xhtml.js @@ -265,7 +265,7 @@ class XhtmlObject extends XmlObject { xfaFont.letterSpacing = getMeasurement(value); break; case "margin": - const values = value.split(/ \t/).map(x => getMeasurement(x)); + const values = value.split(/ \t/).map(getMeasurement); switch (values.length) { case 1: margin.top = diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index c265c67de58d1..9d43c57e24a3f 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -654,8 +654,8 @@ class AnnotationElement { return; } - const [rectBlX, rectBlY, rectTrX, rectTrY] = this.data.rect.map(x => - Math.fround(x) + const [rectBlX, rectBlY, rectTrX, rectTrY] = this.data.rect.map( + Math.fround ); if (quadPoints.length === 8) { @@ -4212,9 +4212,14 @@ class AnnotationLayer { this.div.append(fragment); await Promise.all(promises); if (this.#accessibilityManager) { - for (const element of this.#elements) { + const annotationIds = await this.#structTreeLayer?.getAnnotationIds(); + for (const { contentElement } of this.#elements) { + if (annotationIds?.has(contentElement.id)) { + // The structure tree already positions this annotation. + continue; + } this.#accessibilityManager.addPointerInTextLayer( - element.contentElement, + contentElement, /* isRemovable = */ false ); } diff --git a/stylelint-mozcentral.json b/stylelint-mozcentral.json index 6f5e7fffc1f3c..cc96a754dd32b 100644 --- a/stylelint-mozcentral.json +++ b/stylelint-mozcentral.json @@ -1,5 +1,6 @@ { "plugins": ["./external/stylelint/no-unused-custom-properties.mjs"], + "referenceFiles": ["./external/stylelint/mozcentral-tokens.css"], "rules": { "no-unknown-custom-properties": true, "pdfjs/no-unused-custom-properties": [ diff --git a/test/integration/accessibility_spec.mjs b/test/integration/accessibility_spec.mjs index 518a418674eb5..5cafb3c0cde5c 100644 --- a/test/integration/accessibility_spec.mjs +++ b/test/integration/accessibility_spec.mjs @@ -230,16 +230,12 @@ describe("accessibility", () => { pages.map(async ([browserName, page]) => { await page.waitForSelector(".structTree"); - const isLinkedToStampAnnotation = await page.$eval( - ".structTree [role='figure']", - el => - document - .getElementById(el.getAttribute("aria-owns")) - .classList.contains("stampAnnotation") + const owners = await page.$$eval( + `[aria-owns~="pdfjs_internal_id_20R"]`, + elements => + elements.map(element => element.closest(".structTree") !== null) ); - expect(isLinkedToStampAnnotation) - .withContext(`In ${browserName}`) - .toBeTrue(); + expect(owners).withContext(`In ${browserName}`).toEqual([true]); }) ); }); diff --git a/test/integration/freetext_editor_spec.mjs b/test/integration/freetext_editor_spec.mjs index ada2868e2d29f..35aaa2e72bd9b 100644 --- a/test/integration/freetext_editor_spec.mjs +++ b/test/integration/freetext_editor_spec.mjs @@ -867,7 +867,7 @@ describe("FreeText Editor", () => { }, proprName); const rects = (await serialize("rect")).map(rect => - rect.slice(0, 2).map(x => Math.floor(x)) + rect.slice(0, 2).map(Math.floor) ); const expected = [ [-28, 695], diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 7b1e5e6fbf084..4a976b987a880 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -4899,7 +4899,10 @@ have written that much by now. So, here’s to squashing bugs.`); { role: "Link", children: [ - { type: "object", id: "432R" }, + { + type: "annotation", + id: "pdfjs_internal_id_432R", + }, { type: "content", id: "p406R_mc34" }, ], }, @@ -4922,7 +4925,10 @@ have written that much by now. So, here’s to squashing bugs.`); { role: "Link", children: [ - { type: "object", id: "433R" }, + { + type: "annotation", + id: "pdfjs_internal_id_433R", + }, { type: "content", id: "p406R_mc36" }, ], }, @@ -4948,7 +4954,7 @@ have written that much by now. So, here’s to squashing bugs.`); { role: "Link", children: [ - { type: "object", id: "434R" }, + { type: "annotation", id: "pdfjs_internal_id_434R" }, { type: "content", id: "p406R_mc10" }, ], }, diff --git a/test/unit/display_utils_spec.js b/test/unit/display_utils_spec.js index de8af9aeeb3a5..e5c2344a1fc15 100644 --- a/test/unit/display_utils_spec.js +++ b/test/unit/display_utils_spec.js @@ -407,7 +407,7 @@ describe("display_utils", function () { // Unlike other tests we cannot simply compare the HTML-strings since // Chrome and Firefox produce different results. Instead we compare sets // containing the individual parts of the HTML-strings. - const splitParts = s => new Set(s.split(/[<>/ ]+/).filter(x => x)); + const splitParts = s => new Set(s.split(/[<>/ ]+/).filter(Boolean)); it("should render plain text", function () { if (isNodeJS) { diff --git a/test/unit/name_number_tree_spec.js b/test/unit/name_number_tree_spec.js index 3432c51101a8b..48e5103dbb8ce 100644 --- a/test/unit/name_number_tree_spec.js +++ b/test/unit/name_number_tree_spec.js @@ -21,7 +21,7 @@ import { XRefMock } from "./test_utils.js"; describe("NameOrNumberTree", function () { describe("NameTree", function () { it("should return an empty map when root is null", function () { - const xref = new XRefMock([]); + const xref = new XRefMock(); const tree = new NameTree(null, xref); expect(tree.getAll().size).toEqual(0); }); @@ -30,7 +30,7 @@ describe("NameOrNumberTree", function () { const root = new Dict(); root.set("Names", ["alpha", "value_a", "beta", "value_b"]); - const xref = new XRefMock([]); + const xref = new XRefMock(); const tree = new NameTree(root, xref); const map = tree.getAll(); @@ -66,7 +66,7 @@ describe("NameOrNumberTree", function () { const root = new Dict(); root.set("Kids", [inlineLeaf]); - const xref = new XRefMock([]); + const xref = new XRefMock(); const tree = new NameTree(root, xref); // Should not throw even though the kid is an inline Dict (not a Ref). @@ -126,7 +126,7 @@ describe("NameOrNumberTree", function () { const root = new Dict(); root.set("Nums", [1, "one", 2, "two"]); - const xref = new XRefMock([]); + const xref = new XRefMock(); const tree = new NumberTree(root, xref); const map = tree.getAll(); @@ -144,7 +144,7 @@ describe("NameOrNumberTree", function () { const root = new Dict(); root.set("Kids", [inlineLeaf]); - const xref = new XRefMock([]); + const xref = new XRefMock(); const tree = new NumberTree(root, xref); const map = tree.getAll(); diff --git a/test/unit/primitives_spec.js b/test/unit/primitives_spec.js index c047dfa615856..fb31e53f08da1 100644 --- a/test/unit/primitives_spec.js +++ b/test/unit/primitives_spec.js @@ -127,7 +127,7 @@ describe("primitives", function () { const dict = new Dict(null); expect(dict.xref).toBeNull(); - const xref = new XRefMock([]); + const xref = new XRefMock(); dict.assignXref(xref); expect(dict.xref).toEqual(xref); }); diff --git a/test/unit/struct_tree_layer_builder_spec.js b/test/unit/struct_tree_layer_builder_spec.js index 52bf3e87cd41d..dc19d0fe6aa0a 100644 --- a/test/unit/struct_tree_layer_builder_spec.js +++ b/test/unit/struct_tree_layer_builder_spec.js @@ -17,10 +17,14 @@ import { isNodeJS } from "../../src/shared/util.js"; import { StructTreeLayerBuilder } from "../../web/struct_tree_layer_builder.js"; describe("StructTreeLayerBuilder", function () { - function render(structTree) { + function build(structTree) { return new StructTreeLayerBuilder({ getStructTree: async () => structTree, - }).render(); + }); + } + + function render(structTree) { + return build(structTree).render(); } beforeEach(function () { @@ -356,6 +360,67 @@ describe("StructTreeLayerBuilder", function () { expect(row.id).toEqual(""); }); + it("keeps structured annotations in the structure tree", async function () { + const builder = build({ + role: "Root", + children: [ + { + role: "P", + children: [ + { type: "content", id: "p1R_mc0" }, + { + role: "Link", + children: [ + { + role: "Figure", + children: [{ type: "content", id: "p1R_mc1" }], + }, + { type: "annotation", id: "pdfjs_internal_id_1R" }, + ], + }, + { + role: "Link", + children: [ + { type: "content", id: "p1R_mc2" }, + { type: "annotation", id: "pdfjs_internal_id_2R" }, + { type: "content", id: "p1R_mc3" }, + { type: "annotation", id: "pdfjs_internal_id_3R" }, + ], + }, + { + role: "Link", + children: [{ type: "annotation", id: "pdfjs_internal_id_4R" }], + }, + ], + }, + ], + }); + const tree = await builder.render(); + + expect(await builder.getAnnotationIds()).toEqual( + new Set([ + "pdfjs_internal_id_1R", + "pdfjs_internal_id_2R", + "pdfjs_internal_id_3R", + "pdfjs_internal_id_4R", + ]) + ); + expect( + [...tree.querySelectorAll("[aria-owns]")].map(e => + e.getAttribute("aria-owns") + ) + ).toEqual([ + "p1R_mc0", + "p1R_mc1", + "pdfjs_internal_id_1R", + "p1R_mc2", + "pdfjs_internal_id_2R", + "p1R_mc3", + "pdfjs_internal_id_3R", + "pdfjs_internal_id_4R", + ]); + }); + it("only uses the caption role inside a table or a figure", async function () { const tree = await render({ role: "Root", diff --git a/test/unit/test_utils.js b/test/unit/test_utils.js index cd1cbea0aa054..02e1e0bb92f9d 100644 --- a/test/unit/test_utils.js +++ b/test/unit/test_utils.js @@ -123,36 +123,38 @@ function getCrossOriginHostname(hostname) { } class XRefMock { - constructor(array) { - this._map = Object.create(null); - this._newTemporaryRefNum = null; - this._newPersistentRefNum = null; - this.stream = new NullStream(); - - for (const key in array) { - const obj = array[key]; - this._map[obj.ref.toString()] = obj.data; + #map = new Map(); + + #newPersistentRefNum = null; + + #newTemporaryRefNum = null; + + stream = new NullStream(); + + constructor(array = []) { + for (const { ref, data } of array) { + this.#map.set(ref.toString(), data); } } getNewPersistentRef(obj) { - if (this._newPersistentRefNum === null) { - this._newPersistentRefNum = Object.keys(this._map).length || 1; + if (this.#newPersistentRefNum === null) { + this.#newPersistentRefNum = this.#map.size || 1; } - const ref = Ref.get(this._newPersistentRefNum++, 0); - this._map[ref.toString()] = obj; + const ref = Ref.get(this.#newPersistentRefNum++, 0); + this.#map.set(ref.toString(), obj); return ref; } getNewTemporaryRef() { - if (this._newTemporaryRefNum === null) { - this._newTemporaryRefNum = Object.keys(this._map).length || 1; + if (this.#newTemporaryRefNum === null) { + this.#newTemporaryRefNum = this.#map.size || 1; } - return Ref.get(this._newTemporaryRefNum++, 0); + return Ref.get(this.#newTemporaryRefNum++, 0); } resetNewTemporaryRef() { - this._newTemporaryRefNum = null; + this.#newTemporaryRefNum = null; } countUpdatesAfter(offset) { @@ -160,7 +162,7 @@ class XRefMock { } fetch(ref) { - return this._map[ref.toString()]; + return this.#map.get(ref.toString()); } async fetchAsync(ref) { diff --git a/web/annotation_editor_layer_builder.css b/web/annotation_editor_layer_builder.css index bb011844b8e92..13f4768c4d334 100644 --- a/web/annotation_editor_layer_builder.css +++ b/web/annotation_editor_layer_builder.css @@ -267,6 +267,21 @@ --alt-text-hover-done-color: var(--alt-text-done-color); --alt-text-hover-warning-color: var(--alt-text-warning-color); + /* Match Nova's panel surface and 32px toolbar controls. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + --editor-toolbar-bg-color: var(--background-color-box); + --editor-toolbar-fg-color: var(--toolbarbutton-icon-fill); + --editor-toolbar-border-color: var(--border-color-deemphasized); + --editor-toolbar-hover-bg-color: var( + --toolbarbutton-background-color-hover + ); + --editor-toolbar-shadow: var(--box-shadow-level-2); + --editor-toolbar-height: var(--size-item-large); + --alt-text-warning-color: var(--icon-color-information); + } + /*#endif*/ + @media screen and (forced-colors: active) { --editor-toolbar-bg-color: var(--button-background-color, ButtonFace); --editor-toolbar-fg-color: var(--button-text-color, ButtonText); @@ -320,6 +335,12 @@ border: 1px solid var(--editor-toolbar-border-color); box-shadow: var(--editor-toolbar-shadow); + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-radius: var(--panel-border-radius); + } + /*#endif*/ + &.hidden { display: none; } @@ -413,6 +434,15 @@ border-radius: 2px; outline: 2px solid var(--editor-toolbar-focus-outline-color); } + + /* Match the radius of hovered and focused Nova buttons. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + &:is(:hover, :focus-visible) { + border-radius: var(--button-border-radius); + } + } + /*#endif*/ } .altText { @@ -493,6 +523,17 @@ --alt-text-tooltip-shadow: 0 2px 6px 0 light-dark(rgb(58 57 68 / 0.2), #15141a); + /* Use Nova's box surface and compact radius. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + --alt-text-tooltip-bg: var(--background-color-box); + --alt-text-tooltip-border: var(--border-color-deemphasized); + --alt-text-tooltip-shadow: var(--box-shadow-level-2); + + border-radius: var(--border-radius-xsmall); + } + /*#endif*/ + @media screen and (forced-colors: active) { --alt-text-tooltip-bg: Canvas; --alt-text-tooltip-fg: var(--text-color, CanvasText); @@ -619,6 +660,15 @@ --no-alt-text-badge-bg-color: light-dark(#cfcfd8, #fbfbfe); --no-alt-text-badge-fg-color: light-dark(#5b5b66, #15141a); + /* Use Nova's neutral badge colors. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + --no-alt-text-badge-border-color: var(--border-color-deemphasized); + --no-alt-text-badge-bg-color: var(--background-color-box); + --no-alt-text-badge-fg-color: var(--icon-color); + } + /*#endif*/ + @media screen and (forced-colors: active) { --no-alt-text-badge-border-color: ButtonText; --no-alt-text-badge-bg-color: ButtonFace; @@ -641,6 +691,12 @@ border: 1px solid var(--no-alt-text-badge-border-color); background: var(--no-alt-text-badge-bg-color); + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-radius: var(--border-radius-xsmall); + } + /*#endif*/ + &::before { content: ""; display: inline-block; @@ -906,6 +962,13 @@ --preview-image-bg-color: light-dark(#f0f0f4, #2b2a33); --preview-image-border: none; + /* Use Nova's secondary box surface for the preview. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + --preview-image-bg-color: var(--background-color-box-info); + } + /*#endif*/ + @media screen and (forced-colors: active) { --preview-image-bg-color: ButtonFace; --preview-image-border: 1px solid ButtonText; @@ -1089,6 +1152,14 @@ --selected-outline-color: light-dark(#0060df, #aaf2ff); --swatch-border-color: light-dark(#cfcfd8, #52525e); + /* Use Nova selection and border colors for the swatches. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + --selected-outline-color: var(--color-accent-primary); + --swatch-border-color: var(--border-color-deemphasized); + } + /*#endif*/ + @media screen and (forced-colors: active) { --hover-outline-color: Highlight; --selected-outline-color: var(--hover-outline-color); @@ -1113,6 +1184,15 @@ .basicColorPicker { width: 28px; + /* Keep the swatch square when the row height changes. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + width: auto; + aspect-ratio: 1; + padding: var(--space-small); + } + /*#endif*/ + &::-moz-color-swatch { border-radius: 100%; margin: 0; @@ -1219,6 +1299,14 @@ &:has(.dropdown:not(.hidden)) { background-color: var(--editor-toolbar-hover-bg-color); + /* Match the active toolbar-button state while the menu is open. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + background-color: var(--toolbarbutton-background-color-active); + border-radius: var(--button-border-radius); + } + /*#endif*/ + &::after { scale: -1; } @@ -1265,6 +1353,30 @@ outline: 2px solid var(--hover-outline-color); } } + + /* Match Firefox's horizontal visual picker. Keep this after the + button rule to override its width: 100%. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + flex-direction: row; + gap: var(--space-small); + padding: var(--space-small); + width: max-content; + inset-inline-start: 50%; + translate: calc(-50% * var(--dir-factor)); + border-radius: var(--border-radius-medium); + + button { + width: auto; + flex: 0 0 auto; + + > .swatch { + width: var(--size-item-medium); + height: var(--size-item-medium); + } + } + } + /*#endif*/ } } } diff --git a/web/app_options.js b/web/app_options.js index 23a036dce1544..0078c917e2c15 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -417,10 +417,8 @@ const defaultOptions = new Map([ }, ], [ - // Whether the viewer follows the Firefox design system (see the pref-gated - // @import of tokens-brand.css in viewer.css). Read from CSS via - // -moz-pref(), not from JS, so it is Firefox-only and has no effect - // elsewhere. + // Import Firefox design-system tokens and enable Nova viewer styles. The + // Nova overrides also require browser.nova.enabled. "enableNova", { /** @type {boolean} */ diff --git a/web/buttons.css b/web/buttons.css index cd49b8e17d252..f4d5d396e2edf 100644 --- a/web/buttons.css +++ b/web/buttons.css @@ -82,6 +82,16 @@ --hover-filter: brightness(1.4); } + /* Nova supplies state colors; preserve its border and disabled opacity. */ + /*#if MOZCENTRAL && !GECKOVIEW*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + --hover-filter: none; + --button-disabled-opacity: var(--button-opacity-disabled); + --button-secondary-hover-border-color: var(--button-border-color-hover); + --button-secondary-active-border-color: var(--button-border-color-active); + } + /*#endif*/ + @media screen and (forced-colors: active) { --button-primary-bg-color: var( --button-background-color-primary, diff --git a/web/comment_manager.css b/web/comment_manager.css index c79a7886aad18..f74acac44d8a3 100644 --- a/web/comment_manager.css +++ b/web/comment_manager.css @@ -25,6 +25,13 @@ box-sizing: border-box; border-radius: 8px; + + /* Match the Nova radius used by .dialog. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-radius: var(--border-radius-medium); + } + /*#endif*/ } #commentManagerDialog { @@ -264,6 +271,26 @@ --button-comment-hover-bg: light-dark(#e0e0e6, #52525e); --button-comment-hover-color: var(--button-comment-color); + /* Use Nova's secondary box surface and toolbar-button states. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + --comment-date-fg-color: color-mix( + in srgb, + var(--comment-fg-color) 69%, + transparent + ); + --comment-bg-color: var(--background-color-box-info); + --comment-border-color: var(--border-color-deemphasized); + --comment-count-bg-color: var(--background-color-information); + --comment-indicator-active-fg-color: var(--color-accent-primary-active); + --comment-indicator-focus-fg-color: var(--icon-color); + + --button-comment-color: var(--toolbarbutton-icon-fill); + --button-comment-active-bg: var(--toolbarbutton-background-color-active); + --button-comment-hover-bg: var(--toolbarbutton-background-color-hover); + } + /*#endif*/ + @media screen and (forced-colors: active) { --comment-date-fg-color: CanvasText; --comment-bg-color: Canvas; @@ -334,6 +361,13 @@ font-style: normal; font-weight: 400; line-height: normal; + + /* Use Nova's small radius. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-radius: var(--border-radius-small); + } + /*#endif*/ } } @@ -384,6 +418,18 @@ height: 0; overflow: hidden; } + + /* Match Nova toolbar-button radius and focus styling. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-radius: var(--button-border-radius); + + &:focus-visible { + outline: var(--focus-outline); + outline-offset: var(--focus-outline-offset); + } + } + /*#endif*/ } } @@ -413,6 +459,14 @@ border: 0.5px solid var(--comment-border-color); background-color: var(--comment-bg-color); + /* Use Nova's radius and 1px separator. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-radius: var(--border-radius-medium); + border-width: 1px; + } + /*#endif*/ + &:not(.noComments) { &:hover { @media screen and (forced-colors: active) { diff --git a/web/dialog.css b/web/dialog.css index ac98c2bbafc5f..d550f6d707bcf 100644 --- a/web/dialog.css +++ b/web/dialog.css @@ -36,6 +36,15 @@ --input-text-bg-color: light-dark(white, #42414d); --input-text-fg-color: var(--text-primary-color); + /* Match Nova popup borders, separators, and shadows. */ + /*#if MOZCENTRAL && !GECKOVIEW*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + --dialog-border-color: var(--border-color-deemphasized); + --dialog-shadow: var(--box-shadow-level-3); + --separator-color: var(--border-color-deemphasized); + } + /*#endif*/ + @media screen and (forced-colors: active) { --dialog-bg-color: var(--background-color-canvas, Canvas); /* border-only: --background-color-canvas would resolve to Canvas in FF HCM diff --git a/web/digital_signature_properties.css b/web/digital_signature_properties.css index 769dc0807334c..6b3475ea5a9be 100644 --- a/web/digital_signature_properties.css +++ b/web/digital_signature_properties.css @@ -73,6 +73,27 @@ light-dark(rgb(29 142 61), rgb(106 210 126)) ); + /* Like Nova message bars, use a neutral banner surface and keep severity in + the colored stripe. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + --sig-card-border: var(--border-color-deemphasized); + --sig-card-nested-bg: var(--background-color-box-info); + --sig-row-color: var(--text-color); + --sig-detail-color: var(--text-color-deemphasized); + --sig-divider-color: var(--border-color-deemphasized); + --sig-summary-hover-color: var(--link-color-hover); + --sig-link-hover-bg: var(--button-background-color-ghost-hover); + --sig-banner-verified-bg: var(--background-color-box-info); + --sig-banner-warn-bg: var(--background-color-box-info); + --sig-banner-error-bg: var(--background-color-box-info); + --sig-banner-verified-color: var(--text-color); + --sig-banner-warn-color: var(--text-color); + --sig-banner-error-color: var(--text-color); + --sig-icon-default: var(--text-color-deemphasized); + } + /*#endif*/ + @media screen and (forced-colors: active) { /* HCM keywords are picked by *semantic role*, not by hue — the * user's high-contrast theme resolves them to whatever palette it @@ -222,6 +243,13 @@ * user's high-contrast palette without us hard-coding any hue. */ border-inline-start: 3px solid currentcolor; + /* Match the Nova message-bar radius. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-radius: var(--border-radius-medium); + } + /*#endif*/ + &.verified { background: var(--sig-banner-verified-bg); color: var(--sig-banner-verified-color); @@ -258,6 +286,13 @@ gap: 3px; background: var(--sig-card-bg); + /* Match the banner's inset radius. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-radius: var(--border-radius-medium); + } + /*#endif*/ + .signer { font-weight: 600; font-size: 13px; @@ -361,6 +396,14 @@ border-radius: 4px; white-space: nowrap; + /* Use Nova ghost-button spacing and radius. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + padding-inline: var(--space-small); + border-radius: var(--button-border-radius); + } + /*#endif*/ + &:hover { background: var(--sig-link-hover-bg); text-decoration: underline; @@ -368,6 +411,13 @@ &:focus-visible { outline: 2px solid var(--sig-link-color); outline-offset: 1px; + + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + outline: var(--focus-outline); + outline-offset: var(--focus-outline-offset); + } + /*#endif*/ } } @@ -377,6 +427,13 @@ padding-top: 4px; font-size: 12px; + /* Use Nova's solid separator. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-top-style: solid; + } + /*#endif*/ + > summary { cursor: pointer; user-select: none; @@ -426,6 +483,13 @@ padding: 6px 8px; background: var(--sig-card-nested-bg); gap: 2px; + + /* Use the radius below the outer card's. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-radius: var(--border-radius-small); + } + /*#endif*/ } .subSignatures .signer, diff --git a/web/images/toolbarButton-menuArrowNova.svg b/web/images/toolbarButton-menuArrowNova.svg new file mode 100644 index 0000000000000..f5c80208f990a --- /dev/null +++ b/web/images/toolbarButton-menuArrowNova.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/menu.css b/web/menu.css index 3ff790dba5d0b..3d225ed5c3f65 100644 --- a/web/menu.css +++ b/web/menu.css @@ -66,6 +66,23 @@ button.hasPopupMenu { --menuitem-hover-background-blend-mode: normal; --disabled-opacity: 0.62; + /* Map menu states to Nova panel and menu-item tokens. Keep this before the + forced-colors overrides. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + --menuitem-gap: var(--space-small); + --menuitem-border-radius: var(--panel-menuitem-border-radius); + --menu-box-shadow: var(--box-shadow-level-2); + --menu-border-color: var(--border-color-deemphasized); + --menuitem-hover-bg: var(--button-background-color-menu-hover); + --menuitem-text-hover-fg: var(--button-text-color-menu-hover); + --menuitem-active-bg: var(--button-background-color-menu-active); + --menuitem-text-active-fg: var(--button-text-color-menu-active); + --menuitem-focus-border-color: transparent; + --disabled-opacity: var(--button-opacity-disabled); + } + /*#endif*/ + @media screen and (forced-colors: active) { --menu-bg: var(--background-color-box, Canvas); --menu-background-blend-mode: normal; @@ -104,6 +121,14 @@ button.hasPopupMenu { border: 1px solid var(--menu-border-color); backdrop-filter: var(--menu-backdrop-filter); + /* Rows provide their own inline inset. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + padding: var(--space-small) 0; + border-radius: var(--panel-border-radius); + } + /*#endif*/ + &.withMark { --menu-mark-icon-size: 16px; } @@ -185,6 +210,14 @@ button.hasPopupMenu { background-color: var(--menuitem-focus-bg); outline: 2px solid var(--menuitem-focus-outline-color); outline-offset: 2px; + + /* Use the inset focus ring from Firefox panel items. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + outline: var(--focus-outline); + outline-offset: var(--focus-outline-inset); + } + /*#endif*/ } } @@ -215,5 +248,36 @@ button.hasPopupMenu { font-weight: 510; line-height: normal; } + + /* Match Firefox panel-item spacing. width: auto prevents the margins from + overflowing; add checkmark padding only where its size has a unit. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + height: auto; + min-height: var(--size-item-large); + width: auto; + margin: var(--panel-menuitem-margin); + padding: var(--panel-menuitem-padding); + + &.selected::after { + inset-inline-start: var(--panel-menuitem-padding-inline); + } + + > span { + padding-inline-start: 0; + } + } + /*#endif*/ + } + + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + &.withMark > button { + padding-inline-start: calc( + var(--panel-menuitem-padding-inline) + var(--menu-mark-icon-size) + + var(--menuitem-gap) + ); + } } + /*#endif*/ } diff --git a/web/message_bar.css b/web/message_bar.css index 67b5a1fd76ad9..7f7815878c2bc 100644 --- a/web/message_bar.css +++ b/web/message_bar.css @@ -32,6 +32,13 @@ light-dark(rgb(21 20 26 / 0.07), rgb(251 251 254 / 0.07)) ); + /* Use Nova's icon-button radius. */ + /*#if MOZCENTRAL && !GECKOVIEW*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + --message-bar-close-button-border-radius: var(--button-border-radius); + } + /*#endif*/ + @media screen and (forced-colors: active) { --message-bar-close-button-color: var(--button-text-color, ButtonText); --message-bar-close-button-border: 1px solid @@ -159,6 +166,15 @@ rgb(255 255 255 / 0.08) ); + /* Match Nova message bars: use a neutral surface and information icon. */ + /*#if MOZCENTRAL && !GECKOVIEW*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + --message-bar-icon-color: var(--icon-color-information); + --message-bar-bg-color: var(--background-color-box); + --message-bar-border-color: var(--border-color-deemphasized); + } + /*#endif*/ + @media screen and (forced-colors: active) { --message-bar-icon-color: CanvasText; --message-bar-bg-color: Canvas; diff --git a/web/new_alt_text_manager.js b/web/new_alt_text_manager.js index 2b39e018b701c..edcfdd5a77515 100644 --- a/web/new_alt_text_manager.js +++ b/web/new_alt_text_manager.js @@ -498,7 +498,7 @@ class NewAltTextManager { text .toLowerCase() .split(/[^\p{L}\p{N}]+/gu) - .filter(x => !!x) + .filter(Boolean) ); } diff --git a/web/pdf_find_controller.js b/web/pdf_find_controller.js index 9315380762738..71345b91754cf 100644 --- a/web/pdf_find_controller.js +++ b/web/pdf_find_controller.js @@ -629,7 +629,7 @@ class PDFFindController { } // We don't bother caching the normalized search query in the Array-case, // since this code-path is *essentially* unused in the default viewer. - return (query || []).filter(q => !!q).map(q => normalize(q)[0]); + return (query || []).filter(Boolean).map(q => normalize(q)[0]); } #shouldDirtyMatch(state) { diff --git a/web/pdf_viewer.css b/web/pdf_viewer.css index eae6d0e4eee19..b1fd58c080029 100644 --- a/web/pdf_viewer.css +++ b/web/pdf_viewer.css @@ -60,6 +60,15 @@ --new-badge-color: light-dark(#fff, #15141a); --new-badge-border-color: light-dark(#fbfbfe / 40%, #15141a / 40%); + /* Use Nova's semantic success surface and text color. */ + /*#if MOZCENTRAL && !GECKOVIEW*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + --new-badge-bg: var(--background-color-success); + --new-badge-color: var(--text-color); + --new-badge-border-color: transparent; + } + /*#endif*/ + @media screen and (forced-colors: active) { --pdfViewer-padding-bottom: 9px; --page-margin: 8px auto -1px; @@ -81,6 +90,13 @@ border-radius: 4px; border: 1px solid var(--new-badge-border-color); padding-inline: 4px; + + /*#if MOZCENTRAL && !GECKOVIEW*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-radius: var(--border-radius-circle); + padding-inline: var(--space-small); + } + /*#endif*/ margin-inline: 4px; font: menu; font-size: 12px; diff --git a/web/sidebar.css b/web/sidebar.css index fe5e9fc618566..de435e9ca184e 100644 --- a/web/sidebar.css +++ b/web/sidebar.css @@ -35,6 +35,15 @@ light-dark(#0062fa, #00cadb) ); + /* Style the sidebar as a Nova panel. Keep this before forced-colors. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + --sidebar-border-color: var(--border-color-deemphasized); + --sidebar-box-shadow: var(--box-shadow-level-2); + --sidebar-border-radius: var(--panel-border-radius); + } + /*#endif*/ + @media screen and (forced-colors: active) { --sidebar-bg-color: var(--background-color-box, Canvas); --sidebar-border-color: CanvasText; @@ -81,6 +90,14 @@ background-color: var(--resizer-hover-bg-color); outline: none; } + + /* Match Firefox's Nova splitter: use its full width and rounded ends. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-inline-width: 0; + border-radius: var(--border-radius-large); + } + /*#endif*/ } &.resizing { diff --git a/web/signature_manager.css b/web/signature_manager.css index 592617c3276d8..4e55701a7f6e2 100644 --- a/web/signature_manager.css +++ b/web/signature_manager.css @@ -34,6 +34,14 @@ --button-signature-hover-bg: light-dark(#e0e0e6, #52525e); --button-signature-hover-color: var(--button-signature-color); + /* Use Nova button states for saved-signature rows. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + --button-signature-hover-bg: var(--button-background-color-hover); + --button-signature-active-bg: var(--button-background-color-active); + } + /*#endif*/ + @media screen and (forced-colors: active) { --signature-bg: HighlightText; --signature-hover-bg: var(--signature-bg); @@ -374,6 +382,12 @@ margin: 0; background-color: var(--thickness-bg); border-radius: 4px 4px 0 0; + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-radius: var(--border-radius-small) + var(--border-radius-small) 0 0; + } + /*#endif*/ border-inline: var(--thickness-border); border-top: var(--thickness-border); pointer-events: auto; @@ -623,6 +637,11 @@ button { border: var(--button-signature-border); border-radius: 4px; + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-radius: var(--border-radius-small); + } + /*#endif*/ background-color: var(--button-signature-bg); color: var(--button-signature-color); @@ -665,6 +684,11 @@ justify-content: flex-start; outline: none; border-radius: 4px; + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-radius: var(--border-radius-small); + } + /*#endif*/ box-sizing: border-box; font: message-box; position: relative; @@ -686,6 +710,11 @@ box-sizing: border-box; border: none; border-radius: 4px; + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-radius: var(--border-radius-small); + } + /*#endif*/ > path { stroke: var(--button-signature-color); @@ -705,6 +734,12 @@ &:is(:hover, :active) > svg { border-radius: 4px 0 0 4px; + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-radius: var(--border-radius-small) 0 0 + var(--border-radius-small); + } + /*#endif*/ background-color: var(--signature-hover-bg); } diff --git a/web/struct_tree_layer_builder.js b/web/struct_tree_layer_builder.js index 2ab554b9af591..1db99a9c1cd1b 100644 --- a/web/struct_tree_layer_builder.js +++ b/web/struct_tree_layer_builder.js @@ -193,6 +193,8 @@ class StructTreeLayerBuilder { #treePromise; + #annotationIds = new Set(); + #elementAttributes = new Map(); #structElementIdPrefix = `pdfjs_internal_struct_${getUuid()}_`; @@ -230,6 +232,7 @@ class StructTreeLayerBuilder { try { const tree = await this.#promise; this.#collectStructElements(tree); + this.#collectAnnotations(tree); this.#treeDom = this.#walk(tree); } catch (ex) { reject(ex); @@ -253,6 +256,20 @@ class StructTreeLayerBuilder { return null; } + /** + * Get the ids of annotations owned by the structure tree. + * @returns {Promise|null>} + */ + async getAnnotationIds() { + try { + await this.render(); + return this.#annotationIds; + } catch { + // See the comment in `getAriaAttributes`. + } + return null; + } + hide() { if (this.#treeDom && !this.#treeDom.hidden) { this.#treeDom.hidden = true; @@ -279,6 +296,19 @@ class StructTreeLayerBuilder { } } + #collectAnnotations(node) { + if (!node) { + return; + } + if (node.type === "annotation") { + this.#annotationIds.add(node.id); + return; + } + for (const child of node.children || []) { + this.#collectAnnotations(child); + } + } + #getStructElementId(structId) { return this.#structElementIds.getOrInsertComputed( structId, diff --git a/web/tree.css b/web/tree.css index 346b62c1d1308..323226fa12a15 100644 --- a/web/tree.css +++ b/web/tree.css @@ -28,6 +28,19 @@ --treeitem-expanded-icon: url(images/treeitem-expanded.svg); --treeitem-collapsed-icon: url(images/treeitem-collapsed.svg); + /* Style tree rows like Nova toolbar buttons and inset them from the sidebar. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + --treeitem-color: var(--text-color); + --treeitem-bg-color: var(--toolbarbutton-background-color-hover); + --treeitem-hover-color: var(--text-color); + --treeitem-selected-color: var(--text-color); + --treeitem-selected-bg-color: var(--toolbarbutton-background-color-active); + + padding-inline: var(--panel-menuitem-margin-inline); + } + /*#endif*/ + &.withNesting { .treeItemToggler { &::before { @@ -68,6 +81,13 @@ background-clip: padding-box; border-radius: 2px; color: var(--treeitem-hover-color); + + /* This selector outranks the row's own radius, so repeat it here. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-radius: var(--panel-menuitem-border-radius); + } + /*#endif*/ } } @@ -110,11 +130,28 @@ white-space: normal; cursor: default; + /* Fill the inset width and pad the label inside the rounded row. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + box-sizing: border-box; + min-width: 100%; + padding-block: var(--space-xsmall); + padding-inline: var(--space-small); + border-radius: var(--panel-menuitem-border-radius); + } + /*#endif*/ + &:hover { background-color: var(--treeitem-bg-color); background-clip: padding-box; border-radius: 2px; color: var(--treeitem-hover-color); + + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-radius: var(--panel-menuitem-border-radius); + } + /*#endif*/ } } diff --git a/web/viewer.css b/web/viewer.css index 2b8f3b2dc50ea..2bd6791295085 100644 --- a/web/viewer.css +++ b/web/viewer.css @@ -13,11 +13,8 @@ * limitations under the License. */ -/* Pref-gated, so the Firefox tokens are opt-in (and with them the Nova theme, - which tokens-brand.css gates on browser.nova.enabled): with the pref off - every var() falls back to its shipped literal. -moz-pref() works here - because chrome rules are enabled for a resource:// stylesheet, and it only - sees pdfjs.* prefs. */ +/* Import Firefox tokens only when enabled; the token sheet gates its Nova + overrides on browser.nova.enabled. */ /*#if MOZCENTRAL*/ @import url(chrome://global/skin/design-system/tokens-brand.css) -moz-pref( "pdfjs.enableNova" @@ -135,6 +132,61 @@ --secondaryToolbarButton-documentProperties-icon: url(images/secondaryToolbarButton-documentProperties.svg); --editorParams-stampAddImage-icon: url(images/toolbarButton-zoomIn.svg); --comment-edit-button-icon: url(images/comment-editButton.svg); + + /* Map viewer variables to Nova design tokens. Keep Nova blocks before later + forced-colors overrides. Comments must stay outside #if blocks because the + preprocessor strips one comment layer from their contents. The root font + size makes Firefox's rem-based spacing tokens resolve correctly. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + font-size: var(--font-size-root); + + --toolbarButton-menuArrowNova-icon: url(images/toolbarButton-menuArrowNova.svg); + + --toolbar-height: 40px; + --toolbar-horizontal-padding: 6px; + --toolbar-vertical-padding: 4px; + + --toolbar-icon-opacity: 1; + --doorhanger-icon-opacity: 1; + + --main-color: var(--text-color); + --body-bg-color: light-dark(var(--color-gray-15), var(--color-gray-85)); + --progressBar-color: var(--color-accent-primary); + --progressBar-bg-color: var(--background-color-box-info); + --progressBar-blend-color: color-mix( + in srgb, + var(--color-accent-primary) 40%, + var(--background-color-box) + ); + --toolbar-icon-bg-color: var(--toolbarbutton-icon-fill); + --toolbar-icon-hover-bg-color: var(--toolbarbutton-icon-fill); + + --sidebar-narrow-bg-color: color-mix( + in srgb, + var(--background-color-canvas) 90%, + transparent + ); + --sidebar-toolbar-bg-color: var(--toolbar-background-color); + --toolbar-bg-color: var(--toolbar-background-color); + --toolbar-border-color: var(--border-color-deemphasized); + --toggled-btn-color: var(--toolbarbutton-icon-fill); + --toggled-btn-bg-color: var(--toolbarbutton-background-color-active); + --toggled-hover-active-btn-color: var( + --toolbarbutton-background-color-active + ); + --dropdown-btn-bg-color: var(--button-background-color); + --dropdown-btn-border: 1px solid var(--button-border-color); + --separator-color: var(--border-color-deemphasized); + --field-color: var(--text-color); + --field-bg-color: var(--background-color-box); + --field-border-color: var(--border-color-interactive); + --doorhanger-bg-color: var(--background-color-box); + --doorhanger-border-color: var(--border-color-deemphasized); + --doorhanger-hover-color: var(--text-color); + --doorhanger-separator-color: var(--border-color-deemphasized); + } + /*#endif*/ } :root:dir(rtl) { @@ -190,9 +242,23 @@ html { &[data-toolbar-density="compact"] { --toolbar-height: 30px; + + /* 24px buttons, i.e. --size-item-medium. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + --toolbar-height: 32px; + } + /*#endif*/ } &[data-toolbar-density="touch"] { --toolbar-height: 44px; + + /* 40px buttons, i.e. --button-min-height-large. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + --toolbar-height: 48px; + } + /*#endif*/ } } @@ -393,6 +459,20 @@ body { &::after { border-width: var(--doorhanger-height); } + + /* Nova doorhangers use a bordered panel surface without a caret. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-radius: var(--panel-border-radius); + border: 1px solid var(--doorhanger-border-color); + box-shadow: var(--box-shadow-level-2); + + &::after, + &::before { + display: none; + } + } + /*#endif*/ } .doorHangerRight { @@ -406,6 +486,13 @@ body { border-bottom-color: var(--doorhanger-bg-color); inset-inline-end: 1px; } + + /* Without the caret, align the panel edge to its anchor. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + inset-inline-end: 0; + } + /*#endif*/ } .doorHanger { @@ -419,6 +506,12 @@ body { border-bottom-color: var(--toolbar-bg-color); inset-inline-start: 1px; } + + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + inset-inline-start: 0; + } + /*#endif*/ } .splitToolbarButtonSeparator { @@ -508,6 +601,12 @@ body { height: 9px; width: 9px; border-radius: 50%; + + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + background-color: var(--color-accent-attention); + } + /*#endif*/ } .verticalToolbarSeparator { @@ -568,6 +667,24 @@ body { &:focus { border-color: #0a84ff; } + + /* Use Nova field focus styling. Do not fix the height: Firefox tests that + #pageNumber follows its font size. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-radius: var(--border-radius-small); + padding: var(--space-xsmall) var(--space-small); + + &:focus { + border-color: var(--field-border-color); + } + + &:focus-visible { + outline: var(--focus-outline); + outline-offset: var(--focus-outline-inset); + } + } + /*#endif*/ } #pageNumber { @@ -674,6 +791,30 @@ dialog :link { color: rgb(255 255 255); } +/* Legacy dialogs do not use .dialog, so map their surface, type, and links to + Nova tokens. Keep line-height proportional to the tokenized font size. */ +/*#if MOZCENTRAL*/ +@media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + dialog:not(.dialog) { + padding: var(--space-large); + font-size: var(--font-size-small); + line-height: 150%; + border-spacing: var(--space-xsmall); + background-color: var(--background-color-canvas); + border: 1px solid var(--border-color); + box-shadow: var(--box-shadow-level-3); + + .separator { + margin-block: var(--space-small); + } + + :link { + color: var(--link-color); + } + } +} +/*#endif*/ + #passwordDialog { text-align: center; } @@ -846,6 +987,29 @@ dialog :link { height: auto; } } + + /* Use Nova's inset focus ring. The toggled-hover rule is !important, so pass + its ring through the variable. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-radius: var(--button-border-radius); + + &:focus-visible { + outline: var(--focus-outline); + outline-offset: var(--focus-outline-inset); + } + + &.toggled:hover:focus-visible { + --toggled-hover-btn-outline: var(--focus-outline); + } + + &.labeled { + border-radius: var(--panel-menuitem-border-radius); + gap: var(--space-small); + padding-inline-start: var(--space-small); + } + } + /*#endif*/ } .toolbarButtonWithContainer { @@ -862,6 +1026,14 @@ dialog :link { .menu { padding-block: 5px; + + /* Inset Nova menu rows from the panel edge. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + padding-block: var(--space-small); + padding-inline: var(--space-small); + } + /*#endif*/ } .menuContainer { @@ -915,6 +1087,13 @@ dialog :link { padding-inline: 10px; padding-block: 10px; + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + padding-inline: var(--space-medium); + padding-block: var(--space-medium); + } + /*#endif*/ + > .editorParamsSetter { min-height: 26px; display: flex; @@ -952,6 +1131,23 @@ dialog :link { &::-moz-range-thumb { background-color: white; } + + /* Use Nova slider colors. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + &::-moz-range-progress { + background-color: var(--color-accent-primary); + } + + &::-moz-range-track { + background-color: var(--border-color-interactive); + } + + &::-moz-range-thumb { + background-color: var(--color-accent-primary); + } + } + /*#endif*/ } } } @@ -1069,6 +1265,17 @@ dialog :link { flex-wrap: wrap; justify-content: flex-start; + /* Inset the bar without changing its height; PDFFindBar uses height to + detect wrapping. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + --input-horizontal-padding: var(--space-medium); + + border-radius: var(--border-radius-medium); + padding-inline: var(--space-small); + } + /*#endif*/ + > * { height: var(--toolbar-height); padding: var(--findbar-padding); @@ -1106,7 +1313,20 @@ dialog :link { &[data-status="notFound"] { background-color: rgb(255 102 102); + + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + background-color: var(--background-color-critical); + } + /*#endif*/ } + + /* Use Nova's search-field radius. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-radius: var(--input-search-border-radius); + } + /*#endif*/ } } @@ -1123,6 +1343,14 @@ dialog :link { color: rgb(82 82 82); padding-block: 4px; + /* Replace the fixed gray pair with Nova theme tokens. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + background-color: var(--background-color-box-info); + color: var(--text-color-deemphasized); + } + /*#endif*/ + &:empty { display: none; } @@ -1238,6 +1466,15 @@ dialog :link { padding-inline: 4px; margin: 2px; border-radius: 2px; + + /* Use Nova's pill shape for toolbar labels and findbar toggles. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + padding-inline: var(--space-small); + border-radius: var(--button-border-radius); + } + /*#endif*/ + color: var(--main-color); font-size: 12px; line-height: 14px; @@ -1264,6 +1501,12 @@ dialog :link { justify-content: space-between; gap: 1px; box-sizing: border-box; + + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + gap: var(--space-xxsmall); + } + /*#endif*/ } .dropdownToolbarButton { @@ -1337,11 +1580,85 @@ dialog :link { &:is(:hover, :focus-visible, :active)::after { background-color: var(--toolbar-icon-hover-bg-color); } + + /* Mirror moz-select. Keep the inner select opaque because SelectParent uses + its computed background for the option popup; paint hover as a gradient so + background-color stays stable. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + overflow: hidden; + border: var(--button-border); + border-radius: var(--button-border-radius); + background-color: var(--button-background-color); + color: var(--button-text-color); + + &:is(:hover, :has(> select:open)) { + border-color: var(--border-color-interactive-hover); + background-color: var(--button-background-color-hover); + color: var(--button-text-color-hover); + } + + &:has(> select:focus-visible) { + outline: var(--focus-outline); + outline-offset: var(--focus-outline-offset); + } + + > select { + height: auto; + min-height: var(--button-min-height); + border-radius: inherit; + padding-block: var(--button-padding-block); + padding-inline: var(--button-padding-inline) + calc( + var(--button-padding-inline) + var(--icon-size) + var(--space-small) + ); + font-size: var(--font-size-root); + font-weight: var(--button-font-weight); + color: inherit; + background-color: var(--background-color-box); + + > option { + color: var(--text-color); + font-weight: var(--font-weight); + } + + &:is(:hover, :focus-visible) { + background-color: var(--background-color-box); + color: inherit; + } + + &:focus-visible { + outline: none; + } + } + + &:is(:hover, :has(> select:open)) > select { + background-image: linear-gradient( + var(--button-background-color-hover), + var(--button-background-color-hover) + ); + } + + &::after, + &:is(:hover, :focus-visible, :active)::after { + inset-inline-end: var(--button-padding-inline); + background-color: currentColor; + mask-image: var(--toolbarButton-menuArrowNova-icon); + } + } + /*#endif*/ } #toolbarContainer { --menuitem-height: calc(var(--toolbar-height) - 6px); + /* Keep menu rows at Nova's 32px size across toolbar densities. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + --menuitem-height: var(--size-item-large); + } + /*#endif*/ + width: 100%; height: var(--toolbar-height); padding: var(--toolbar-vertical-padding) var(--toolbar-horizontal-padding); @@ -1374,6 +1691,13 @@ dialog :link { #toolbarViewerLeft { margin-inline-start: 8px; + /* --toolbar-horizontal-padding already insets the row. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + margin-inline-start: 0; + } + /*#endif*/ + #numPages.toolbarLabel { padding-inline-start: 3px; flex: none; diff --git a/web/views_manager.css b/web/views_manager.css index 4fa3e69e57377..1ad2c2bf14bc8 100644 --- a/web/views_manager.css +++ b/web/views_manager.css @@ -149,6 +149,32 @@ --multiple-dragging-indicator-bg: var(--indicator-color); --multiple-dragging-text-color: light-dark(#fbfbfe, #15141a); + /* Use opaque Nova surfaces and carry warning severity in the icon color. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + --sidebar-bg-color: var(--background-color-box); + --sidebar-backdrop-filter: none; + --header-bg: var(--background-color-box); + --status-undo-bg: color-mix( + in srgb, + var(--color-accent-primary) 8%, + transparent + ); + --status-warning-bg: var(--background-color-box-info); + --indicator-warning-color: var(--icon-color-critical); + --image-border-color: var(--border-color-deemphasized); + --image-hover-border-color: var(--border-color-interactive); + --image-page-number-bg: var(--background-color-box-info); + --image-current-page-number-fg: var(--button-text-color-primary); + --image-dragging-placeholder-bg: color-mix( + in srgb, + var(--color-accent-primary) 8%, + transparent + ); + --multiple-dragging-bg: var(--background-color-box); + } + /*#endif*/ + @media screen and (forced-colors: active) { --views-text-color: var(--text-color, CanvasText); --button-fg: var(--button-text-color, ButtonText); @@ -263,6 +289,19 @@ } } + /* Radius the opaque header instead of clipping the panel, which would hide + the protruding resizer. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + padding-block-start: 0; + + #viewsManagerHeader { + border-start-start-radius: var(--panel-border-radius); + border-start-end-radius: var(--panel-border-radius); + } + } + /*#endif*/ + #viewsManagerHeader { display: flex; flex-direction: column; @@ -683,6 +722,12 @@ min-height: 24px; padding: 4px 16px; + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-radius: var(--button-border-radius); + } + /*#endif*/ + font: menu; font-size: 13px; font-style: normal; @@ -725,6 +770,11 @@ &:not(.isDragging) > .thumbnailImageContainer::after { content: attr(page-number); border-radius: 8px; + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-radius: var(--border-radius-circle); + } + /*#endif*/ border: 1px solid var(--image-page-number-border-color); background-color: var(--image-page-number-bg); color: var(--image-page-number-fg); @@ -777,6 +827,13 @@ user-select: none; position: relative; + /* Use Nova's medium card radius. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-radius: var(--border-radius-medium); + } + /*#endif*/ + img { width: 100%; height: 100%; @@ -785,6 +842,12 @@ outline: none; user-select: none; pointer-events: none; + + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-radius: var(--border-radius-medium); + } + /*#endif*/ } &.missingThumbnailImage { @@ -912,11 +975,26 @@ box-sizing: content-box; outline: none; user-select: none; + + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-radius: var(--border-radius-medium); + } + /*#endif*/ } &::after { content: attr(data-multiple-count); border-radius: calc(8px * var(--thumbnail-dragging-scale)); + + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + border-radius: calc( + var(--border-radius-medium) * + var(--thumbnail-dragging-scale) + ); + } + /*#endif*/ background-color: var(--multiple-dragging-indicator-bg); color: var(--multiple-dragging-text-color); position: absolute; @@ -949,10 +1027,26 @@ rgb(255 255 255 / 0.9) ); + /* Match the Nova row colors used by the outline and layers views. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + --attachment-color: var(--text-color); + --attachment-hover-color: var(--text-color); + --attachment-bg-color: var(--toolbarbutton-background-color-hover); + } + /*#endif*/ + > ul { list-style-type: none; padding: 0; + /* Match the menu-item inset in tree.css. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + padding-inline: var(--panel-menuitem-margin-inline); + } + /*#endif*/ + > li > a { text-decoration: none; display: inline-block; @@ -976,6 +1070,21 @@ border-radius: 2px; color: var(--attachment-hover-color); } + + /* Repeat the radius for the more-specific :hover rule above. */ + /*#if MOZCENTRAL*/ + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled") { + box-sizing: border-box; + min-width: 100%; + padding-block: var(--space-xsmall); + padding-inline: var(--space-small); + + &, + &:hover { + border-radius: var(--panel-menuitem-border-radius); + } + } + /*#endif*/ } } }