From 3c0e0dc96b376ae620916dd13af9d00b04bd483d Mon Sep 17 00:00:00 2001 From: Hatton Date: Tue, 30 Dec 2025 16:21:02 -0700 Subject: [PATCH 1/4] BL-15642 Intro Page Settings --- .../bookEdit/css/origamiEditing.less | 25 +- src/BloomBrowserUI/bookEdit/editViewFrame.ts | 7 + src/BloomBrowserUI/bookEdit/js/origami.ts | 28 +- .../pageSettings/PageSettingsDialog.tsx | 392 ++++++++++++++++++ src/BloomExe/Book/HtmlDom.cs | 8 + src/BloomExe/Edit/EditingView.cs | 17 + .../web/controllers/EditingViewApi.cs | 11 + .../efl-zeromargin1/customBookStyles.css | 2 +- .../appearance-theme-default.css | 2 + .../appearance-theme-rounded-border-ebook.css | 3 +- .../appearance-theme-zero-margin-ebook.css | 3 +- src/content/bookLayout/pageNumbers.less | 10 + yarn.lock | 4 + 13 files changed, 503 insertions(+), 9 deletions(-) create mode 100644 src/BloomBrowserUI/bookEdit/pageSettings/PageSettingsDialog.tsx create mode 100644 yarn.lock diff --git a/src/BloomBrowserUI/bookEdit/css/origamiEditing.less b/src/BloomBrowserUI/bookEdit/css/origamiEditing.less index f7f4c1d26736..a300627654b6 100644 --- a/src/BloomBrowserUI/bookEdit/css/origamiEditing.less +++ b/src/BloomBrowserUI/bookEdit/css/origamiEditing.less @@ -145,9 +145,10 @@ top: @ToggleVerticalOffset; width: 100%; display: flex; - justify-content: end; + justify-content: space-between; box-sizing: border-box; } + .origami-toggle { cursor: pointer; margin-right: 19px; @@ -160,6 +161,28 @@ display: inline; } } +.page-settings-button { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + padding: 0; + margin-right: 8px; + border: none; + background-color: transparent; + cursor: pointer; + color: @bloom-purple; + + &:hover { + opacity: 0.8; + } + + svg { + width: 20px; + height: 20px; + } +} // here follows the inner workings of the toggle .onoffswitch { diff --git a/src/BloomBrowserUI/bookEdit/editViewFrame.ts b/src/BloomBrowserUI/bookEdit/editViewFrame.ts index 0c39afffb399..92fb8de70f03 100644 --- a/src/BloomBrowserUI/bookEdit/editViewFrame.ts +++ b/src/BloomBrowserUI/bookEdit/editViewFrame.ts @@ -56,6 +56,7 @@ export { showPageChooserDialog }; import "../lib/errorHandler"; import { showBookSettingsDialog } from "./bookSettings/BookSettingsDialog"; export { showBookSettingsDialog }; +import { showPageSettingsDialog } from "./pageSettings/PageSettingsDialog"; import { showRegistrationDialogForEditTab } from "../react_components/registration/registrationDialog"; export { showRegistrationDialogForEditTab as showRegistrationDialog }; import { showAboutDialog } from "../react_components/aboutDialog"; @@ -261,6 +262,10 @@ export function showEditViewBookSettingsDialog( showBookSettingsDialog(initiallySelectedGroupIndex); } +export function showEditViewPageSettingsDialog() { + showPageSettingsDialog(); +} + export function showAboutDialogInEditTab() { showAboutDialog(); } @@ -319,6 +324,7 @@ interface EditTabBundleApi { showCopyrightAndLicenseDialog: typeof showCopyrightAndLicenseDialog; showEditViewTopicChooserDialog: typeof showEditViewTopicChooserDialog; showEditViewBookSettingsDialog: typeof showEditViewBookSettingsDialog; + showEditViewPageSettingsDialog: typeof showEditViewPageSettingsDialog; showAboutDialogInEditTab: typeof showAboutDialogInEditTab; showRequiresSubscriptionDialog: typeof showRequiresSubscriptionDialog; showRegistrationDialogInEditTab: typeof showRegistrationDialogInEditTab; @@ -356,6 +362,7 @@ window.editTabBundle = { showCopyrightAndLicenseDialog, showEditViewTopicChooserDialog, showEditViewBookSettingsDialog, + showEditViewPageSettingsDialog, showAboutDialogInEditTab, showRequiresSubscriptionDialog, showRegistrationDialogInEditTab, diff --git a/src/BloomBrowserUI/bookEdit/js/origami.ts b/src/BloomBrowserUI/bookEdit/js/origami.ts index e5bb91a50156..2055492a6372 100644 --- a/src/BloomBrowserUI/bookEdit/js/origami.ts +++ b/src/BloomBrowserUI/bookEdit/js/origami.ts @@ -1,5 +1,3 @@ -// not yet: neither bloomEditing nor this is yet a module import {SetupImage} from './bloomEditing'; -/// import { SetupImage } from "./bloomImages"; import { kBloomCanvasClass } from "../toolbox/canvas/canvasElementUtils"; import "../../lib/split-pane/split-pane.js"; @@ -52,6 +50,7 @@ export function setupOrigami() { // the two results, but none of the controls shows up if we leave it all // outside the bloomApi functions. $(".origami-toggle .onoffswitch").change(layoutToggleClickHandler); + $(".page-settings-button").click(pageSettingsButtonClickHandler); if ($(".customPage .marginBox.origami-layout-mode").length) { setupLayoutMode(); @@ -354,11 +353,16 @@ function getAbovePageControlContainer(): JQuery { .getElementsByClassName("bloom-page")[0] ?.getAttribute("data-tool-id") === "game" ) { - return $("
"); + return $( + `
\ +${getPageSettingsButtonHtml()}\ +
`, + ); } return $( - "\ + `\
\ +${getPageSettingsButtonHtml()}\
\
Change Layout
\
\ @@ -369,10 +373,24 @@ function getAbovePageControlContainer(): JQuery { \
\
\ -
", +`, ); } +function getPageSettingsButtonHtml(): string { + // SVG path matches MUI Settings icon + return ``; +} + +function pageSettingsButtonClickHandler(e: Event) { + e.preventDefault(); + post("editView/showPageSettingsDialog"); +} + function getButtons() { const buttons = $( "
", diff --git a/src/BloomBrowserUI/bookEdit/pageSettings/PageSettingsDialog.tsx b/src/BloomBrowserUI/bookEdit/pageSettings/PageSettingsDialog.tsx new file mode 100644 index 000000000000..ce4984eced25 --- /dev/null +++ b/src/BloomBrowserUI/bookEdit/pageSettings/PageSettingsDialog.tsx @@ -0,0 +1,392 @@ +import { css } from "@emotion/react"; +import * as React from "react"; +import { + ConfigrCustomStringInput, + ConfigrGroup, + ConfigrPane, + ConfigrSubgroup, +} from "@sillsdev/config-r"; +import { kBloomBlue } from "../../bloomMaterialUITheme"; +import { + BloomDialog, + DialogBottomButtons, + DialogMiddle, + DialogTitle, +} from "../../react_components/BloomDialog/BloomDialog"; +import { useSetupBloomDialog } from "../../react_components/BloomDialog/BloomDialogPlumbing"; +import { + DialogCancelButton, + DialogOkButton, +} from "../../react_components/BloomDialog/commonDialogComponents"; +import { useL10n } from "../../react_components/l10nHooks"; +import { + ColorDisplayButton, + DialogResult, +} from "../../react_components/color-picking/colorPickerDialog"; +import { BloomPalette } from "../../react_components/color-picking/bloomPalette"; +import { getPageIframeBody } from "../../utils/shared"; +import { ShowEditViewDialog } from "../editViewFrame"; +import tinycolor from "tinycolor2"; + +let isOpenAlready = false; + +type IPageSettings = { + page: { + backgroundColor: string; + pageNumberColor: string; + pageNumberBackgroundColor: string; + }; +}; + +const getCurrentPageElement = (): HTMLElement => { + const page = getPageIframeBody()?.querySelector( + ".bloom-page", + ) as HTMLElement | null; + if (!page) { + throw new Error( + "PageSettingsDialog could not find .bloom-page in the page iframe", + ); + } + return page; +}; + +const normalizeToHexOrEmpty = (color: string): string => { + const trimmed = color.trim(); + if (!trimmed) { + return ""; + } + + const parsed = tinycolor(trimmed); + if (!parsed.isValid()) { + return trimmed; + } + + // Treat fully transparent as "not set". + if (parsed.getAlpha() === 0) { + return ""; + } + + return parsed.toHexString().toUpperCase(); +}; + +const getComputedStyleForPage = (page: HTMLElement): CSSStyleDeclaration => { + const view = page.ownerDocument.defaultView; + if (view) { + return view.getComputedStyle(page); + } + return getComputedStyle(page); +}; + +const getCurrentPageBackgroundColor = (): string => { + const page = getCurrentPageElement(); + + const inline = normalizeToHexOrEmpty( + page.style.getPropertyValue("--page-background-color"), + ); + if (inline) return inline; + + const computedVariable = normalizeToHexOrEmpty( + getComputedStyleForPage(page).getPropertyValue( + "--page-background-color", + ), + ); + if (computedVariable) return computedVariable; + + const computedMarginBoxVariable = normalizeToHexOrEmpty( + getComputedStyleForPage(page).getPropertyValue( + "--marginBox-background-color", + ), + ); + if (computedMarginBoxVariable) return computedMarginBoxVariable; + + const computedBackground = normalizeToHexOrEmpty( + getComputedStyleForPage(page).backgroundColor, + ); + return computedBackground || "#FFFFFF"; +}; + +const setCurrentPageBackgroundColor = (color: string): void => { + const page = getCurrentPageElement(); + page.style.setProperty("--page-background-color", color); + page.style.setProperty("--marginBox-background-color", color); +}; + +const getPageNumberColor = (): string => { + const page = getCurrentPageElement(); + + const inline = normalizeToHexOrEmpty( + page.style.getPropertyValue("--pageNumber-color"), + ); + if (inline) return inline; + + const computed = normalizeToHexOrEmpty( + getComputedStyleForPage(page).getPropertyValue("--pageNumber-color"), + ); + return computed || "#000000"; +}; + +const setPageNumberColor = (color: string): void => { + const page = getCurrentPageElement(); + page.style.setProperty("--pageNumber-color", color); +}; + +const getPageNumberBackgroundColor = (): string => { + const page = getCurrentPageElement(); + + const inline = normalizeToHexOrEmpty( + page.style.getPropertyValue("--pageNumber-background-color"), + ); + if (inline) return inline; + + const computed = normalizeToHexOrEmpty( + getComputedStyleForPage(page).getPropertyValue( + "--pageNumber-background-color", + ), + ); + return computed || ""; +}; + +const setPageNumberBackgroundColor = (color: string): void => { + const page = getCurrentPageElement(); + page.style.setProperty("--pageNumber-background-color", color); +}; + +const PageBackgroundColorPickerForConfigr: React.FunctionComponent<{ + value: string; + disabled: boolean; + onChange: (value: string) => void; +}> = (props) => { + const backgroundColorLabel = useL10n( + "Background Color", + "Common.BackgroundColor", + ); + + return ( + { + if (dialogResult === DialogResult.OK) props.onChange(newColor); + }} + /> + ); +}; + +const PageNumberColorPickerForConfigr: React.FunctionComponent<{ + value: string; + disabled: boolean; + onChange: (value: string) => void; +}> = (props) => { + const pageNumberColorLabel = useL10n( + "Page Number Color", + "PageSettings.PageNumberColor", + ); + + return ( + { + if (dialogResult === DialogResult.OK) props.onChange(newColor); + }} + /> + ); +}; + +const PageNumberBackgroundColorPickerForConfigr: React.FunctionComponent<{ + value: string; + disabled: boolean; + onChange: (value: string) => void; +}> = (props) => { + const pageNumberBackgroundColorLabel = useL10n( + "Page Number Background Color", + "PageSettings.PageNumberBackgroundColor", + ); + + return ( + { + if (dialogResult === DialogResult.OK) props.onChange(newColor); + }} + /> + ); +}; + +export const PageSettingsDialog: React.FunctionComponent = () => { + const { closeDialog, propsForBloomDialog } = useSetupBloomDialog({ + initiallyOpen: true, + dialogFrameProvidedExternally: false, + }); + + const pageSettingsTitle = useL10n("Page Settings", "PageSettings.Title"); + const backgroundColorLabel = useL10n( + "Background Color", + "Common.BackgroundColor", + ); + const pageNumberColorLabel = useL10n( + "Page Number Color", + "PageSettings.PageNumberColor", + ); + const pageNumberBackgroundColorLabel = useL10n( + "Page Number Background Color", + "PageSettings.PageNumberBackgroundColor", + ); + + const [initialValues, setInitialValues] = React.useState< + IPageSettings | undefined + >(undefined); + + const [settingsToReturnLater, setSettingsToReturnLater] = React.useState< + IPageSettings | string | undefined + >(undefined); + + // Read after mount so we get the current page's color even if opening this dialog + // is preceded by a save/refresh that updates the page iframe. + React.useEffect(() => { + setInitialValues({ + page: { + backgroundColor: getCurrentPageBackgroundColor(), + pageNumberColor: getPageNumberColor(), + pageNumberBackgroundColor: getPageNumberBackgroundColor(), + }, + }); + }, []); + + const onOk = (): void => { + const rawSettings = settingsToReturnLater ?? initialValues; + if (!rawSettings) { + throw new Error( + "PageSettingsDialog: expected settings to be loaded before OK", + ); + } + + const settings = + typeof rawSettings === "string" + ? (JSON.parse(rawSettings) as IPageSettings) + : rawSettings; + + setCurrentPageBackgroundColor(settings.page.backgroundColor); + setPageNumberColor(settings.page.pageNumberColor); + setPageNumberBackgroundColor(settings.page.pageNumberBackgroundColor); + isOpenAlready = false; + closeDialog(); + }; + + const onCancel = (): void => { + isOpenAlready = false; + closeDialog(); + }; + + return ( + + + + {initialValues && ( +
+ { + if (typeof s === "string") { + setSettingsToReturnLater(s); + return; + } + + if (typeof s === "object" && s) { + setSettingsToReturnLater( + s as IPageSettings, + ); + return; + } + + throw new Error( + "PageSettingsDialog: unexpected value from config-r onChange", + ); + }} + > + + + + + + + + +
+ )} +
+ + + + +
+ ); +}; + +export const showPageSettingsDialog = () => { + if (!isOpenAlready) { + isOpenAlready = true; + ShowEditViewDialog(); + } +}; diff --git a/src/BloomExe/Book/HtmlDom.cs b/src/BloomExe/Book/HtmlDom.cs index 8386e6787a8e..a53a180cbbd7 100644 --- a/src/BloomExe/Book/HtmlDom.cs +++ b/src/BloomExe/Book/HtmlDom.cs @@ -1915,6 +1915,14 @@ SafeXmlElement edittedPageDiv //html file in a browser. destinationPageDiv.SetAttribute("lang", edittedPageDiv.GetAttribute("lang")); + // Allow saving per-page CSS custom properties (e.g. --page-background-color) stored on the page div. + // If missing, remove any previously-saved style. + var style = edittedPageDiv.GetAttribute("style"); + if (string.IsNullOrEmpty(style)) + destinationPageDiv.RemoveAttribute("style"); + else + destinationPageDiv.SetAttribute("style", style); + // Copy the two background audio attributes which can be set using the music toolbox. // Ensuring that volume is missing unless the main attribute is non-empty is // currently redundant, everything should work if we just copied all attributes. diff --git a/src/BloomExe/Edit/EditingView.cs b/src/BloomExe/Edit/EditingView.cs index b315fe1a48e5..c7932243c420 100644 --- a/src/BloomExe/Edit/EditingView.cs +++ b/src/BloomExe/Edit/EditingView.cs @@ -1913,6 +1913,23 @@ public void SaveAndOpenBookSettingsDialog() ); } + public void SaveAndOpenPageSettingsDialog() + { + _model.SaveThen( + () => + { + RunJavascriptAsync("editTabBundle.showEditViewPageSettingsDialog();"); + return _model.CurrentPage.Id; + }, + () => { } // wrong state, do nothing + ); + } + + private void _pageSettingsButton_Click(object sender, EventArgs e) + { + SaveAndOpenPageSettingsDialog(); + } + // This is temporary code we added in 6.0 when trying to determine why we are sometimes losing // user data upon save. See BL-13120. private void _topBarPanel_Click(object sender, EventArgs e) diff --git a/src/BloomExe/web/controllers/EditingViewApi.cs b/src/BloomExe/web/controllers/EditingViewApi.cs index 114a27348e6b..5bd7dcfa6e89 100644 --- a/src/BloomExe/web/controllers/EditingViewApi.cs +++ b/src/BloomExe/web/controllers/EditingViewApi.cs @@ -120,6 +120,11 @@ public void RegisterWithApiHandler(BloomApiHandler apiHandler) HandleShowBookSettingsDialog, true ); + apiHandler.RegisterEndpointHandler( + "editView/showPageSettingsDialog", + HandleShowPageSettingsDialog, + true + ); } private void HandleJumpToPage(ApiRequest request) @@ -135,6 +140,12 @@ private void HandleShowBookSettingsDialog(ApiRequest request) View.SaveAndOpenBookSettingsDialog(); } + private void HandleShowPageSettingsDialog(ApiRequest request) + { + request.PostSucceeded(); + View.SaveAndOpenPageSettingsDialog(); + } + /// /// This one is for the snapping function on dragging origami splitters. /// diff --git a/src/content/appearanceMigrations/efl-zeromargin1/customBookStyles.css b/src/content/appearanceMigrations/efl-zeromargin1/customBookStyles.css index c70b168314f5..6a7f94f5558a 100644 --- a/src/content/appearanceMigrations/efl-zeromargin1/customBookStyles.css +++ b/src/content/appearanceMigrations/efl-zeromargin1/customBookStyles.css @@ -28,7 +28,7 @@ --pageNumber-color: black; --pageNumber-background-width: 17px; --pageNumber-border-radius: 50%; - --pageNumber-background-color: #ffffff; + --pageNumber-background-color: transparent; font-family: "ABeeZee"; z-index: 1000; diff --git a/src/content/appearanceThemes/appearance-theme-default.css b/src/content/appearanceThemes/appearance-theme-default.css index 532dbed270cb..f7d7274e5a36 100644 --- a/src/content/appearanceThemes/appearance-theme-default.css +++ b/src/content/appearanceThemes/appearance-theme-default.css @@ -39,6 +39,8 @@ --pageNumber-background-width: unset; /* for when we need to have a colored background, e.g. a circle */ /* background-color: value in .numberedPage:after to display the page number */ --pageNumber-background-color: transparent; + /* color: value in .numberedPage:after to display the page number */ + --pageNumber-color: black; /* border-radius: value in .numberedPage:after to display the page number */ --pageNumber-border-radius: 0px; /* left: value in .numberedPage.side-left:after to display the page number */ diff --git a/src/content/appearanceThemes/appearance-theme-rounded-border-ebook.css b/src/content/appearanceThemes/appearance-theme-rounded-border-ebook.css index 36d7d8f3cf4c..8cf4c0d7626f 100644 --- a/src/content/appearanceThemes/appearance-theme-rounded-border-ebook.css +++ b/src/content/appearanceThemes/appearance-theme-rounded-border-ebook.css @@ -29,13 +29,14 @@ [class*="Device"].numberedPage:not(.bloom-interactive-page) { --pageNumber-extra-height: 0mm !important; /* we put the page number on top of the image so we don't need a margin boost */ + --pageNumber-background-color: #ffffff; /* I'm not clear why this is white, but all I did in this change is to move it so that it can be overridden by page settings */ } [class*="Device"].numberedPage:not(.bloom-interactive-page)::after { --pageNumber-bottom: var(--page-margin-bottom); --pageNumber-top: unset; --pageNumber-font-size: 11pt; --pageNumber-border-radius: 50%; - --pageNumber-background-color: #ffffff; + --pageNumber-background-width: 33px; --pageNumber-always-left-margin: var(--page-margin-left); --pageNumber-right-margin: deliberately-invalid; /* prevents right being set at all. unset does not work. Prevent centering for this layout */ diff --git a/src/content/appearanceThemes/appearance-theme-zero-margin-ebook.css b/src/content/appearanceThemes/appearance-theme-zero-margin-ebook.css index 520bf437b01c..c6f28dfb7701 100644 --- a/src/content/appearanceThemes/appearance-theme-zero-margin-ebook.css +++ b/src/content/appearanceThemes/appearance-theme-zero-margin-ebook.css @@ -25,6 +25,7 @@ Note that hiding the page numbers is done by a setting in appearance.json, not h /* this rule will apply more generally than we'd prefer, but the common situation we're improving here is picture on top, text on the bottom, we need to make room for the page number */ --pageNumber-extra-height: 12mm !important; + --pageNumber-background-color: #ffffff; /* I'm not clear why this is white, but all I did in this change is to move it so that it can be overridden by page settings */ } .Device16x9Landscape.numberedPage::after { --pageNumber-bottom: 0mm; @@ -32,7 +33,7 @@ Note that hiding the page numbers is done by a setting in appearance.json, not h --pageNumber-font-size: 11pt; border-radius: 50%; - --pageNumber-background-color: #ffffff; + --pageNumber-background-width: 33px; --pageNumber-always-left-margin: var(--page-margin-left); --pageNumber-right-margin: deliberately-invalid; /* prevents right being set at all. unset does not work. Prevent centering for this layout */ diff --git a/src/content/bookLayout/pageNumbers.less b/src/content/bookLayout/pageNumbers.less index 441e0ab35281..1a1c9f71c978 100644 --- a/src/content/bookLayout/pageNumbers.less +++ b/src/content/bookLayout/pageNumbers.less @@ -7,6 +7,14 @@ // themes can override this as needed. If you have reasonable margins, you don't need to add anything to fit in a pageNumber --pageNumber-extra-height: 0mm; // must have units } + +// If the page has explicitly set a background color (e.g. via Page Settings), +// make the page number background match it, unless it has its own explicit setting. +// .bloom-page.numberedPage[style*="--page-background-color"]:not( +// [style*="--pageNumber-background-color"] +// ) { +// --pageNumber-background-color: var(--page-background-color); +// } .numberedPage { &:after { content: attr(data-page-number); @@ -22,6 +30,8 @@ bottom: var(--pageNumber-bottom); top: var(--pageNumber-top); background-color: var(--pageNumber-background-color); + color: var(--pageNumber-color); + border-radius: var(--pageNumber-border-radius); z-index: 1000; // These are needed to get the number centered in a circle. They have diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 000000000000..fb57ccd13afb --- /dev/null +++ b/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + From a424e7490ca377a3ceac8f950fb5f44cedca4815 Mon Sep 17 00:00:00 2001 From: Hatton Date: Tue, 30 Dec 2025 16:44:01 -0700 Subject: [PATCH 2/4] See color results on page as you make changes. --- .../pageSettings/PageSettingsDialog.tsx | 25 ++++++++--- .../color-picking/colorPickerDialog.tsx | 45 ++++++++++++++++--- 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/src/BloomBrowserUI/bookEdit/pageSettings/PageSettingsDialog.tsx b/src/BloomBrowserUI/bookEdit/pageSettings/PageSettingsDialog.tsx index ce4984eced25..b5c6d92e6d87 100644 --- a/src/BloomBrowserUI/bookEdit/pageSettings/PageSettingsDialog.tsx +++ b/src/BloomBrowserUI/bookEdit/pageSettings/PageSettingsDialog.tsx @@ -151,6 +151,12 @@ const setPageNumberBackgroundColor = (color: string): void => { page.style.setProperty("--pageNumber-background-color", color); }; +const applyPageSettings = (settings: IPageSettings): void => { + setCurrentPageBackgroundColor(settings.page.backgroundColor); + setPageNumberColor(settings.page.pageNumberColor); + setPageNumberBackgroundColor(settings.page.pageNumberBackgroundColor); +}; + const PageBackgroundColorPickerForConfigr: React.FunctionComponent<{ value: string; disabled: boolean; @@ -172,6 +178,7 @@ const PageBackgroundColorPickerForConfigr: React.FunctionComponent<{ onClose={(dialogResult: DialogResult, newColor: string) => { if (dialogResult === DialogResult.OK) props.onChange(newColor); }} + onChange={(newColor) => props.onChange(newColor)} /> ); }; @@ -197,6 +204,7 @@ const PageNumberColorPickerForConfigr: React.FunctionComponent<{ onClose={(dialogResult: DialogResult, newColor: string) => { if (dialogResult === DialogResult.OK) props.onChange(newColor); }} + onChange={(newColor) => props.onChange(newColor)} /> ); }; @@ -222,6 +230,7 @@ const PageNumberBackgroundColorPickerForConfigr: React.FunctionComponent<{ onClose={(dialogResult: DialogResult, newColor: string) => { if (dialogResult === DialogResult.OK) props.onChange(newColor); }} + onChange={(newColor) => props.onChange(newColor)} /> ); }; @@ -279,14 +288,15 @@ export const PageSettingsDialog: React.FunctionComponent = () => { ? (JSON.parse(rawSettings) as IPageSettings) : rawSettings; - setCurrentPageBackgroundColor(settings.page.backgroundColor); - setPageNumberColor(settings.page.pageNumberColor); - setPageNumberBackgroundColor(settings.page.pageNumberBackgroundColor); + applyPageSettings(settings); isOpenAlready = false; closeDialog(); }; const onCancel = (): void => { + if (initialValues) { + applyPageSettings(initialValues); + } isOpenAlready = false; closeDialog(); }; @@ -329,13 +339,16 @@ export const PageSettingsDialog: React.FunctionComponent = () => { onChange={(s: unknown) => { if (typeof s === "string") { setSettingsToReturnLater(s); + applyPageSettings( + JSON.parse(s) as IPageSettings, + ); return; } if (typeof s === "object" && s) { - setSettingsToReturnLater( - s as IPageSettings, - ); + const settings = s as IPageSettings; + setSettingsToReturnLater(settings); + applyPageSettings(settings); return; } diff --git a/src/BloomBrowserUI/react_components/color-picking/colorPickerDialog.tsx b/src/BloomBrowserUI/react_components/color-picking/colorPickerDialog.tsx index 6688187f6c7b..0cd7d2c30124 100644 --- a/src/BloomBrowserUI/react_components/color-picking/colorPickerDialog.tsx +++ b/src/BloomBrowserUI/react_components/color-picking/colorPickerDialog.tsx @@ -1,4 +1,4 @@ -import { css } from "@emotion/react"; +import { css, Global } from "@emotion/react"; import * as React from "react"; import * as ReactDOM from "react-dom"; import { useEffect, useRef, useState } from "react"; @@ -283,9 +283,30 @@ const ColorPickerDialog: React.FC = (props) => { props.onChange(color); }; + const dialogOpen = props.open === undefined ? open : props.open; + + // The MUI backdrop is rendered outside the dialog tree, so we use a body class + // to suppress it while the color picker is open. + useEffect(() => { + if (!dialogOpen) { + return; + } + document.body.classList.add("bloom-hide-color-picker-backdrop"); + return () => { + document.body.classList.remove("bloom-hide-color-picker-backdrop"); + }; + }, [dialogOpen]); + return ( + = (props) => { padding: 10px 14px 10px 10px; // maintain same spacing all around dialog content and between header/footer } `} - open={props.open === undefined ? open : props.open} + hideBackdrop={true} + BackdropProps={{ + invisible: true, + }} + slotProps={{ + backdrop: { + invisible: true, + }, + }} + open={dialogOpen} ref={dlgRef} onClose={( _event, @@ -429,6 +459,7 @@ export interface IColorDisplayButtonProps { width?: number; disabled?: boolean; onClose: (result: DialogResult, newColor: string) => void; + onChange?: (newColor: string) => void; palette: BloomPalette; } @@ -494,9 +525,13 @@ export const ColorDisplayButton: React.FC = ( props.initialColor, )} onInputFocus={() => {}} - onChange={(color: IColorInfo) => - setCurrentButtonColor(color.colors[0]) - } + onChange={(color: IColorInfo) => { + const newColor = color.colors[0]; + setCurrentButtonColor(newColor); + if (props.onChange) { + props.onChange(newColor); + } + }} /> ); From faf424496ed631030292780a3c827618b1ce4301 Mon Sep 17 00:00:00 2001 From: Hatton Date: Tue, 30 Dec 2025 16:48:38 -0700 Subject: [PATCH 3/4] Add palette for page backgrounds --- .../pageSettings/PageSettingsDialog.tsx | 6 ++--- .../color-picking/bloomPalette.ts | 26 +++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/BloomBrowserUI/bookEdit/pageSettings/PageSettingsDialog.tsx b/src/BloomBrowserUI/bookEdit/pageSettings/PageSettingsDialog.tsx index b5c6d92e6d87..47ef932a86dd 100644 --- a/src/BloomBrowserUI/bookEdit/pageSettings/PageSettingsDialog.tsx +++ b/src/BloomBrowserUI/bookEdit/pageSettings/PageSettingsDialog.tsx @@ -173,7 +173,7 @@ const PageBackgroundColorPickerForConfigr: React.FunctionComponent<{ initialColor={props.value} localizedTitle={backgroundColorLabel} transparency={false} - palette={BloomPalette.CoverBackground} + palette={BloomPalette.PageColors} width={75} onClose={(dialogResult: DialogResult, newColor: string) => { if (dialogResult === DialogResult.OK) props.onChange(newColor); @@ -199,7 +199,7 @@ const PageNumberColorPickerForConfigr: React.FunctionComponent<{ initialColor={props.value} localizedTitle={pageNumberColorLabel} transparency={false} - palette={BloomPalette.CoverBackground} + palette={BloomPalette.Text} width={75} onClose={(dialogResult: DialogResult, newColor: string) => { if (dialogResult === DialogResult.OK) props.onChange(newColor); @@ -225,7 +225,7 @@ const PageNumberBackgroundColorPickerForConfigr: React.FunctionComponent<{ initialColor={props.value || "transparent"} localizedTitle={pageNumberBackgroundColorLabel} transparency={true} - palette={BloomPalette.CoverBackground} + palette={BloomPalette.PageColors} width={75} onClose={(dialogResult: DialogResult, newColor: string) => { if (dialogResult === DialogResult.OK) props.onChange(newColor); diff --git a/src/BloomBrowserUI/react_components/color-picking/bloomPalette.ts b/src/BloomBrowserUI/react_components/color-picking/bloomPalette.ts index 0b8e3278b01d..04b9d1e09ab4 100644 --- a/src/BloomBrowserUI/react_components/color-picking/bloomPalette.ts +++ b/src/BloomBrowserUI/react_components/color-picking/bloomPalette.ts @@ -8,6 +8,7 @@ export enum BloomPalette { BloomReaderBookshelf = "bloom-reader-bookshelf", TextBackground = "overlay-background", HighlightBackground = "highlight-background", + PageColors = "page-colors", } // This array provides a useful default palette for the color picker dialog. @@ -64,6 +65,25 @@ export const HighlightBackgroundPalette: string[] = [ "#C5F0FF", ]; +// Light background colors suitable for page backgrounds. +// (Users can still pick any color, but these are the suggested defaults.) +export const PageColorsPalette: string[] = [ + "#FFFFFF", // white + "#F7F7F7", // very light gray + "#FFF7E6", // warm cream + "#FFF1F2", // very light pink + "#FCE7F3", // pale rose + "#F3E8FF", // pale lavender + "#EDE9FE", // pale purple + "#E0F2FE", // pale sky + "#E0F7FA", // pale cyan + "#E6FFFA", // pale teal + "#ECFDF3", // pale green + "#F7FEE7", // pale lime + "#FFFBEB", // pale amber + "#FEF3C7", // light beige +]; + const specialColors: IColorInfo[] = [ // #DFB28B is the color Comical has been using as the default for captions. // It's fairly close to the "Calico" color defined at https://www.htmlcsscolor.com/hex/D5B185 (#D5B185) @@ -110,6 +130,9 @@ export async function getHexColorsForPalette( case BloomPalette.CoverBackground: factoryColors = CoverBackgroundPalette; break; + case BloomPalette.PageColors: + factoryColors = PageColorsPalette; + break; case BloomPalette.Text: factoryColors = TextColorPalette; break; @@ -156,6 +179,9 @@ export function getDefaultColorsFromPalette( case BloomPalette.CoverBackground: palette = CoverBackgroundPalette; break; + case BloomPalette.PageColors: + palette = PageColorsPalette; + break; case BloomPalette.Text: palette = TextColorPalette; break; From ae661db5a2ca49baf51c3a7e41f18aa1f4459ed5 Mon Sep 17 00:00:00 2001 From: Hatton Date: Tue, 30 Dec 2025 17:01:17 -0700 Subject: [PATCH 4/4] review fixes --- .../pageSettings/PageSettingsDialog.tsx | 57 +++++++++++++++---- .../appearance-theme-rounded-border-ebook.css | 1 - .../appearance-theme-zero-margin-ebook.css | 5 +- src/content/bookLayout/pageNumbers.less | 7 --- 4 files changed, 49 insertions(+), 21 deletions(-) diff --git a/src/BloomBrowserUI/bookEdit/pageSettings/PageSettingsDialog.tsx b/src/BloomBrowserUI/bookEdit/pageSettings/PageSettingsDialog.tsx index 47ef932a86dd..4ddce0a35e03 100644 --- a/src/BloomBrowserUI/bookEdit/pageSettings/PageSettingsDialog.tsx +++ b/src/BloomBrowserUI/bookEdit/pageSettings/PageSettingsDialog.tsx @@ -105,10 +105,27 @@ const getCurrentPageBackgroundColor = (): string => { return computedBackground || "#FFFFFF"; }; +const setOrRemoveCustomProperty = ( + style: CSSStyleDeclaration, + propertyName: string, + value: string, +): void => { + const normalized = normalizeToHexOrEmpty(value); + if (normalized) { + style.setProperty(propertyName, normalized); + } else { + style.removeProperty(propertyName); + } +}; + const setCurrentPageBackgroundColor = (color: string): void => { const page = getCurrentPageElement(); - page.style.setProperty("--page-background-color", color); - page.style.setProperty("--marginBox-background-color", color); + setOrRemoveCustomProperty(page.style, "--page-background-color", color); + setOrRemoveCustomProperty( + page.style, + "--marginBox-background-color", + color, + ); }; const getPageNumberColor = (): string => { @@ -127,7 +144,7 @@ const getPageNumberColor = (): string => { const setPageNumberColor = (color: string): void => { const page = getCurrentPageElement(); - page.style.setProperty("--pageNumber-color", color); + setOrRemoveCustomProperty(page.style, "--pageNumber-color", color); }; const getPageNumberBackgroundColor = (): string => { @@ -148,7 +165,11 @@ const getPageNumberBackgroundColor = (): string => { const setPageNumberBackgroundColor = (color: string): void => { const page = getCurrentPageElement(); - page.style.setProperty("--pageNumber-background-color", color); + setOrRemoveCustomProperty( + page.style, + "--pageNumber-background-color", + color, + ); }; const applyPageSettings = (settings: IPageSettings): void => { @@ -241,6 +262,11 @@ export const PageSettingsDialog: React.FunctionComponent = () => { dialogFrameProvidedExternally: false, }); + const closeDialogAndClearOpenFlag = React.useCallback(() => { + isOpenAlready = false; + closeDialog(); + }, [closeDialog]); + const pageSettingsTitle = useL10n("Page Settings", "PageSettings.Title"); const backgroundColorLabel = useL10n( "Background Color", @@ -289,22 +315,33 @@ export const PageSettingsDialog: React.FunctionComponent = () => { : rawSettings; applyPageSettings(settings); - isOpenAlready = false; - closeDialog(); + closeDialogAndClearOpenFlag(); }; - const onCancel = (): void => { + const onCancel = ( + _reason?: + | "escapeKeyDown" + | "backdropClick" + | "titleCloseClick" + | "cancelClicked", + ): void => { if (initialValues) { applyPageSettings(initialValues); } - isOpenAlready = false; - closeDialog(); + closeDialogAndClearOpenFlag(); + }; + + const onClose = ( + _evt?: object, + _reason?: "escapeKeyDown" | "backdropClick", + ): void => { + onCancel(_reason); }; return (