From c19037ca186da1450d65bf4d9ae44f065ed289bc Mon Sep 17 00:00:00 2001 From: 1aurend Date: Wed, 1 Oct 2025 11:40:18 -0700 Subject: [PATCH 01/12] [F] Split theme settings into content + theme --- .../backend/containers/route-containers.js | 1 + .../backend/containers/settings/Content.js | 132 ++++++++++++++++++ .../src/backend/containers/settings/Theme.js | 83 +---------- .../src/backend/containers/settings/index.js | 4 +- client/src/backend/routes.js | 7 + .../locale/en-US/json/backend/settings.json | 28 ++-- .../locale/en-US/json/shared/page-titles.json | 1 + client/src/helpers/router/navigation.js | 10 ++ 8 files changed, 172 insertions(+), 94 deletions(-) create mode 100644 client/src/backend/containers/settings/Content.js diff --git a/client/src/backend/containers/route-containers.js b/client/src/backend/containers/route-containers.js index fe4d589f68..154a46fc94 100644 --- a/client/src/backend/containers/route-containers.js +++ b/client/src/backend/containers/route-containers.js @@ -123,6 +123,7 @@ export default { ExportTargetsEdit: ExportTargets.Edit, SettingsWrapper: Settings.Wrapper, SettingsTheme: Settings.Theme, + SettingsContent: Settings.Content, SettingsIntegrations: Settings.Integrations, SettingsSubjectsList: Settings.Subjects.List, SettingsSubjectsNew: Settings.Subjects.New, diff --git a/client/src/backend/containers/settings/Content.js b/client/src/backend/containers/settings/Content.js new file mode 100644 index 0000000000..1230b86a4b --- /dev/null +++ b/client/src/backend/containers/settings/Content.js @@ -0,0 +1,132 @@ +import React, { PureComponent } from "react"; +import PropTypes from "prop-types"; +import { withTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import Layout from "backend/components/layout"; +import Form from "global/components/form"; +import FormContainer from "global/containers/form"; +import { settingsAPI, requests } from "api"; +import { select } from "utils/entityUtils"; +import PageHeader from "backend/components/layout/PageHeader"; + +export class SettingsContentContainer extends PureComponent { + static mapStateToProps = state => { + return { + settings: select(requests.settings, state.entityStore) + }; + }; + + static displayName = "Settings.Content"; + + static propTypes = { + settings: PropTypes.object + }; + + render() { + if (!this.props.settings) return null; + const t = this.props.t; + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ ); + } +} + +export default withTranslation()( + connect(SettingsContentContainer.mapStateToProps)(SettingsContentContainer) +); diff --git a/client/src/backend/containers/settings/Theme.js b/client/src/backend/containers/settings/Theme.js index 3de8e80312..e8d56cde43 100644 --- a/client/src/backend/containers/settings/Theme.js +++ b/client/src/backend/containers/settings/Theme.js @@ -88,6 +88,8 @@ export class SettingsThemeContainer extends PureComponent { placeholder="0" instructions={t("settings.theme.offset_instructions")} /> + + - - - - - - - - - - - - - - - - - diff --git a/client/src/backend/containers/settings/index.js b/client/src/backend/containers/settings/index.js index 1f5f7a6ff9..134c6c2852 100644 --- a/client/src/backend/containers/settings/index.js +++ b/client/src/backend/containers/settings/index.js @@ -6,6 +6,7 @@ import Integrations from "./Integrations"; import Email from "./Email"; import Ingestion from "./Ingestion"; import Subjects from "./subjects"; +import Content from "./Content"; export default { Properties, @@ -15,5 +16,6 @@ export default { Integrations, Email, Ingestion, - Subjects + Subjects, + Content }; diff --git a/client/src/backend/routes.js b/client/src/backend/routes.js index 33333135e5..75cdb59d95 100644 --- a/client/src/backend/routes.js +++ b/client/src/backend/routes.js @@ -1133,6 +1133,13 @@ const routes = { path: "/backend/settings/theme", helper: () => "/backend/settings/theme" }, + { + name: "backendSettingsContent", + exact: true, + component: "SettingsContent", + path: "/backend/settings/content", + helper: () => "/backend/settings/content" + }, { name: "backendSettingsIntegrations", exact: true, diff --git a/client/src/config/app/locale/en-US/json/backend/settings.json b/client/src/config/app/locale/en-US/json/backend/settings.json index ae362c748b..2232c05ac3 100644 --- a/client/src/config/app/locale/en-US/json/backend/settings.json +++ b/client/src/config/app/locale/en-US/json/backend/settings.json @@ -82,6 +82,21 @@ "typography_header": "Typography", "typekit_label": "Typekit ID", "typekit_placeholder": "Enter Typekit ID", + "colors_header": "Colors" + }, + "content": { + "header": "Content Settings", + "content_signup_header": "Signup", + "content_data_use_header": "Data Use", + "string_signup_terms_header": "Terms & Conditions Header", + "string_signup_terms_one": "Terms & Conditions First Paragraph", + "string_signup_terms_two": "Terms & Conditions Second Paragraph", + "string_data_use_header": "Page Header", + "string_data_use_copy": "Page Copy", + "data_use_copy_instructions": "This field accepts markdown", + "string_cookies_banner_header": "Cookies Banner Header", + "string_cookies_banner_copy": "Cookies Banner Body", + "cookies_banner_copy_instructions": "This field accepts markdown", "top_bar_header": "Top Bar", "text_label": "Text", "text_placeholder": "Enter Top Bar Text", @@ -95,18 +110,7 @@ "disabled": "Disabled", "always": "Always Visible", "standalone": "Only Visible in Standalone Mode" - }, - "content_signup_header": "Content: Signup", - "content_data_use_header": "Content: Data Use", - "string_signup_terms_header": "Terms & Conditions Header", - "string_signup_terms_one": "Terms & Conditions First Paragraph", - "string_signup_terms_two": "Terms & Conditions Second Paragraph", - "string_data_use_header": "Page Header", - "string_data_use_copy": "Page Copy", - "data_use_copy_instructions": "This field accepts markdown", - "string_cookies_banner_header": "Cookies Banner Header", - "string_cookies_banner_copy": "Cookies Banner Body", - "cookies_banner_copy_instructions": "This field accepts markdown" + } }, "integrations": { "header": "External Integrations Settings", diff --git a/client/src/config/app/locale/en-US/json/shared/page-titles.json b/client/src/config/app/locale/en-US/json/shared/page-titles.json index 8c8a2c9706..5c1543a2c6 100644 --- a/client/src/config/app/locale/en-US/json/shared/page-titles.json +++ b/client/src/config/app/locale/en-US/json/shared/page-titles.json @@ -8,6 +8,7 @@ "assets": "Assets", "collaborators": "People", "comments": "Comments", + "content": "Content", "contents": "Table of Contents", "dashboard": "Dashboard", "details": "Details", diff --git a/client/src/helpers/router/navigation.js b/client/src/helpers/router/navigation.js index d419ab0645..43d90184ce 100644 --- a/client/src/helpers/router/navigation.js +++ b/client/src/helpers/router/navigation.js @@ -153,6 +153,10 @@ class Navigation { label: "titles.theme", route: "backendSettingsTheme" }, + { + label: "titles.content", + route: "backendSettingsContent" + }, { label: "titles.ingestion", route: "backendSettingsIngestion" @@ -530,6 +534,12 @@ class Navigation { entity: "settings", ability: "update" }, + { + label: "titles.content", + route: "backendSettingsContent", + entity: "settings", + ability: "update" + }, { label: "titles.integrations", route: "backendSettingsIntegrations", From 4d4b2044232d5887e24567a49340c8ebcab48f72 Mon Sep 17 00:00:00 2001 From: 1aurend Date: Wed, 1 Oct 2025 15:24:22 -0700 Subject: [PATCH 02/12] [F] Add simple color input --- .../src/backend/containers/settings/Theme.js | 20 +++--- .../global/components/form/BaseInput/index.js | 69 +++++++++++-------- .../components/form/ColorInput/index.js | 21 ++++++ .../components/form/ColorInput/styles.js | 21 ++++++ client/src/global/components/form/index.js | 4 +- 5 files changed, 94 insertions(+), 41 deletions(-) create mode 100644 client/src/global/components/form/ColorInput/index.js create mode 100644 client/src/global/components/form/ColorInput/styles.js diff --git a/client/src/backend/containers/settings/Theme.js b/client/src/backend/containers/settings/Theme.js index e8d56cde43..80becc6c2d 100644 --- a/client/src/backend/containers/settings/Theme.js +++ b/client/src/backend/containers/settings/Theme.js @@ -90,35 +90,31 @@ export class SettingsThemeContainer extends PureComponent { /> - - - - diff --git a/client/src/global/components/form/BaseInput/index.js b/client/src/global/components/form/BaseInput/index.js index 5a3bb61ac4..bcfbc0a7bd 100644 --- a/client/src/global/components/form/BaseInput/index.js +++ b/client/src/global/components/form/BaseInput/index.js @@ -107,6 +107,38 @@ export class FormBaseInput extends PureComponent { return props.renderValue(props.value); } + renderInputComponent() { + const { id, idForError, idForInstructions, ariaRequired } = this.props; + + const InputComponent = + this.context?.styleType === "secondary" + ? Styled.SecondaryInput + : Styled.PrimaryInput; + + return ( + { + this.inputElement = input; + }} + id={id} + name={this.props.name} + disabled={this.props.isDisabled} + type={this.props.inputType ?? this.props.type} + placeholder={this.props.placeholder} + onChange={this.props.onChange} + onKeyDown={e => { + if (this.props.onKeyDown) this.props.onKeyDown(e, this.inputElement); + }} + value={this.renderValue(this.props)} + aria-describedby={`${idForError || ""} ${idForInstructions || ""}`} + autoComplete={this.props.autoComplete} + defaultValue={this.props.defaultValue} + required={this.props.required} + aria-required={ariaRequired} + /> + ); + } + render() { const { id, @@ -115,8 +147,7 @@ export class FormBaseInput extends PureComponent { buttons, instructions, wide, - className, - ariaRequired + className } = this.props; const fieldClasses = classnames(className, { @@ -124,11 +155,6 @@ export class FormBaseInput extends PureComponent { }); const Wrapper = buttons ? Styled.WrapperWithActions : Errorable; - const InputComponent = - this.context?.styleType === "secondary" - ? Styled.SecondaryInput - : Styled.PrimaryInput; - return ( - { - this.inputElement = input; - }} - id={id} - name={this.props.name} - disabled={this.props.isDisabled} - type={this.props.inputType ?? this.props.type} - placeholder={this.props.placeholder} - onChange={this.props.onChange} - onKeyDown={e => { - if (this.props.onKeyDown) - this.props.onKeyDown(e, this.inputElement); - }} - value={this.renderValue(this.props)} - aria-describedby={`${idForError || ""} ${idForInstructions || ""}`} - autoComplete={this.props.autoComplete} - defaultValue={this.props.defaultValue} - required={this.props.required} - aria-required={ariaRequired} - /> + {this.props.inputType === "color" ? ( + + {this.renderInputComponent()} + {this.renderValue(this.props)} + + ) : ( + this.renderInputComponent() + )} {buttons && this.renderButtons(buttons)} {this.props.instructions && ( value ?? defaultValue; + return ( + + ); +} + +ColorInput.displayName = "Form.ColorInput"; diff --git a/client/src/global/components/form/ColorInput/styles.js b/client/src/global/components/form/ColorInput/styles.js new file mode 100644 index 0000000000..1a928df06f --- /dev/null +++ b/client/src/global/components/form/ColorInput/styles.js @@ -0,0 +1,21 @@ +import styled from "@emotion/styled"; +import BaseInput from "../BaseInput"; + +export const ColorInput = styled(BaseInput)` + input[type="color"] { + width: 24px; + height: 24px; + display: inline-block; + cursor: pointer; + border: none; + } + + span.ColorInput-wrapper { + display: flex; + gap: 8px; + align-items: center; + font-family: var(--font-family-sans); + padding-block-end: 8px; + border-bottom: 1px solid var(--input-border-color); + } +`; diff --git a/client/src/global/components/form/index.js b/client/src/global/components/form/index.js index 4f867b87d9..9fe8a4a053 100644 --- a/client/src/global/components/form/index.js +++ b/client/src/global/components/form/index.js @@ -31,6 +31,7 @@ import Label from "./BaseLabel"; import FieldWrapper from "./FieldWrapper"; import { InputGroupPrimary, InputGroupSecondary } from "./InputGroup/styles"; import DrawerButtons from "./DrawerButtons"; +import ColorInput from "./ColorInput"; export default { CoverUploadPlaceholder, @@ -65,7 +66,8 @@ export default { FieldWrapper, InputGroupPrimary, InputGroupSecondary, - DrawerButtons + DrawerButtons, + ColorInput }; export const Unwrapped = { From e09f55f8515a49f91f0b328df293e01327d495cf Mon Sep 17 00:00:00 2001 From: 1aurend Date: Thu, 2 Oct 2025 11:31:30 -0700 Subject: [PATCH 03/12] [F] Add header preview component --- .../components/theme/HeaderPreview/index.js | 95 ++++++++ .../src/backend/containers/settings/Theme.js | 203 ++++++++++-------- .../frontend/layout/header/libraryHeader.js | 13 ++ 3 files changed, 222 insertions(+), 89 deletions(-) create mode 100644 client/src/backend/components/theme/HeaderPreview/index.js diff --git a/client/src/backend/components/theme/HeaderPreview/index.js b/client/src/backend/components/theme/HeaderPreview/index.js new file mode 100644 index 0000000000..3c9614d478 --- /dev/null +++ b/client/src/backend/components/theme/HeaderPreview/index.js @@ -0,0 +1,95 @@ +import { useFromStore } from "hooks"; +import { useTranslation } from "react-i18next"; +import SetCSSProperty from "global/components/utility/SetCSSProperty"; +import PressLogo from "global/components/PressLogo"; +import HeaderLogo from "global/components/atomic/HeaderLogo"; +import IconComposer from "global/components/utility/IconComposer"; + +export default function HeaderPreview({ + accentColor, + foregroundColor, + backgroundColor, + activeColor +}) { + const { t } = useTranslation(); + const settings = useFromStore({ requestKey: "settings", action: "select" }); + const offset = settings.attributes.theme.headerOffset; + const navStyle = offset + ? { position: "relative", top: parseInt(offset, 10) } + : {}; + + return ( +
+ +
+ + <> + + {t("navigation.return_home")} + + + + + + + +
+
+
+ ); +} diff --git a/client/src/backend/containers/settings/Theme.js b/client/src/backend/containers/settings/Theme.js index 80becc6c2d..fd4cb7981f 100644 --- a/client/src/backend/containers/settings/Theme.js +++ b/client/src/backend/containers/settings/Theme.js @@ -8,6 +8,7 @@ import FormContainer from "global/containers/form"; import { settingsAPI, requests } from "api"; import { select } from "utils/entityUtils"; import PageHeader from "backend/components/layout/PageHeader"; +import HeaderPreview from "backend/components/theme/HeaderPreview"; export class SettingsThemeContainer extends PureComponent { static mapStateToProps = state => { @@ -25,6 +26,7 @@ export class SettingsThemeContainer extends PureComponent { render() { if (!this.props.settings) return null; const t = this.props.t; + return (
@@ -36,95 +38,118 @@ export class SettingsThemeContainer extends PureComponent { create={settingsAPI.update} className="form-secondary" > - - - - - - - - - - - - - - - - - - - + {getValue => ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + )}
diff --git a/client/src/theme/styles/components/frontend/layout/header/libraryHeader.js b/client/src/theme/styles/components/frontend/layout/header/libraryHeader.js index faa8e00a61..e7fba95221 100644 --- a/client/src/theme/styles/components/frontend/layout/header/libraryHeader.js +++ b/client/src/theme/styles/components/frontend/layout/header/libraryHeader.js @@ -32,6 +32,19 @@ export default ` } } + &__inner-preview { + ${headerContainerPrimary} + display: grid; + grid-template: "logo breadcrumbs hamburger" / 1fr auto 1fr; + width: 100%; + height: 100%; + + ${respond( + `grid-template: 'logo site-nav . user-nav' / max-content max-content 1fr max-content;`, + 82 + )} + } + .header-logo, .breadcrumb-list, .mobile-nav-toggle { From d4f34ba6241c226ace9f9a500a9fdb9d9200c9bb Mon Sep 17 00:00:00 2001 From: 1aurend Date: Tue, 18 Nov 2025 13:17:01 -0800 Subject: [PATCH 04/12] [E] Add inert to header preview --- client/src/backend/components/theme/HeaderPreview/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/backend/components/theme/HeaderPreview/index.js b/client/src/backend/components/theme/HeaderPreview/index.js index 3c9614d478..496b310f2d 100644 --- a/client/src/backend/components/theme/HeaderPreview/index.js +++ b/client/src/backend/components/theme/HeaderPreview/index.js @@ -28,6 +28,7 @@ export default function HeaderPreview({ "--color-header-foreground": foregroundColor, "--color-header-foreground-active": activeColor }} + inert="" > Date: Tue, 18 Nov 2025 18:23:50 -0800 Subject: [PATCH 05/12] [E] Fix preview markup; add default colors --- .../components/theme/HeaderPreview/index.js | 51 +++++++++++-------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/client/src/backend/components/theme/HeaderPreview/index.js b/client/src/backend/components/theme/HeaderPreview/index.js index 496b310f2d..73fea59911 100644 --- a/client/src/backend/components/theme/HeaderPreview/index.js +++ b/client/src/backend/components/theme/HeaderPreview/index.js @@ -5,6 +5,13 @@ import PressLogo from "global/components/PressLogo"; import HeaderLogo from "global/components/atomic/HeaderLogo"; import IconComposer from "global/components/utility/IconComposer"; +const DEFAULTS = { + accentColor: "#52e3ac", + foregroundColor: "#696969", + backgroundColor: "#ffffff", + activeColor: "#363636" +}; + export default function HeaderPreview({ accentColor, foregroundColor, @@ -22,11 +29,13 @@ export default function HeaderPreview({
@@ -54,41 +63,39 @@ export default function HeaderPreview({ > - + - +
From 83791001b6c7e78b88ea02c42306493c560bddc1 Mon Sep 17 00:00:00 2001 From: 1aurend Date: Tue, 18 Nov 2025 18:24:12 -0800 Subject: [PATCH 06/12] [B] Fix swapped defaults --- client/src/backend/containers/settings/Theme.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/backend/containers/settings/Theme.js b/client/src/backend/containers/settings/Theme.js index fd4cb7981f..4c1655485b 100644 --- a/client/src/backend/containers/settings/Theme.js +++ b/client/src/backend/containers/settings/Theme.js @@ -1,4 +1,4 @@ -import React, { PureComponent } from "react"; +import { PureComponent } from "react"; import PropTypes from "prop-types"; import { withTranslation } from "react-i18next"; import { connect } from "react-redux"; @@ -103,7 +103,7 @@ export class SettingsThemeContainer extends PureComponent { Date: Tue, 18 Nov 2025 18:36:00 -0800 Subject: [PATCH 07/12] [F] Implement Coloris color picker --- client/package.json | 1 + .../global/components/form/BaseInput/index.js | 1 + .../components/form/ColorInput/ColorInput.js | 65 ++ .../components/form/ColorInput/index.js | 24 +- .../components/form/ColorInput/styles.js | 6 +- client/src/theme/styles/vendor/coloris.js | 614 ++++++++++++++++++ client/src/theme/styles/vendor/index.js | 2 + client/yarn.lock | 8 + 8 files changed, 700 insertions(+), 21 deletions(-) create mode 100644 client/src/global/components/form/ColorInput/ColorInput.js create mode 100644 client/src/theme/styles/vendor/coloris.js diff --git a/client/package.json b/client/package.json index 386e251974..243b855f22 100644 --- a/client/package.json +++ b/client/package.json @@ -58,6 +58,7 @@ "@emotion/server": "^11.11.0", "@emotion/styled": "^11.14.1", "@loadable/component": "^5.16.7", + "@melloware/coloris": "^0.25.0", "@vidstack/react": "^1.12.13", "ace-builds": "^1.x", "actioncable": "~5.0.7", diff --git a/client/src/global/components/form/BaseInput/index.js b/client/src/global/components/form/BaseInput/index.js index bcfbc0a7bd..4e15da7f08 100644 --- a/client/src/global/components/form/BaseInput/index.js +++ b/client/src/global/components/form/BaseInput/index.js @@ -119,6 +119,7 @@ export class FormBaseInput extends PureComponent { { this.inputElement = input; + if (this.props.colorRef) this.props.colorRef.current = input; }} id={id} name={this.props.name} diff --git a/client/src/global/components/form/ColorInput/ColorInput.js b/client/src/global/components/form/ColorInput/ColorInput.js new file mode 100644 index 0000000000..175381f24e --- /dev/null +++ b/client/src/global/components/form/ColorInput/ColorInput.js @@ -0,0 +1,65 @@ +import { useId, useEffect, useRef } from "react"; +import Coloris from "@melloware/coloris"; +import setter from "../setter"; +import * as Styled from "./styles"; + +function ColorInput({ defaultValue, ...props }) { + const id = useId(); + const inputRef = useRef(); + const colorRef = useRef(props.value || defaultValue); + + const inputId = `color-input-${id}`; + + useEffect(() => { + Coloris.init(); + + const setColor = event => { + const isTarget = event.detail.currentEl.id === inputId; + if (isTarget) colorRef.current = event.detail.color; + }; + + document.addEventListener("coloris:pick", setColor); + + return () => document.removeEventListener("coloris:pick", setColor); + }, [inputId, props]); + + useEffect(() => { + const onClose = () => { + props.onChange({ target: { value: colorRef.current } }); + }; + + const inputEl = inputRef.current; + + if (inputEl) { + Coloris({ + el: inputEl, + format: "mixed", + formatToggle: true, + themeMode: "dark", + clearButton: true, + defaultColor: defaultValue, + margin: 5 + }); + + inputEl.addEventListener("close", onClose); + + return () => inputEl.removeEventListener("close", onClose); + } + }, [inputRef, defaultValue, colorRef, props]); + + return ( + + ); +} + +export default setter(ColorInput); + +ColorInput.displayName = "Form.ColorInput"; diff --git a/client/src/global/components/form/ColorInput/index.js b/client/src/global/components/form/ColorInput/index.js index 53b8504a2d..d808257eaf 100644 --- a/client/src/global/components/form/ColorInput/index.js +++ b/client/src/global/components/form/ColorInput/index.js @@ -1,21 +1,7 @@ -import { useId } from "react"; -import PropTypes from "prop-types"; -import * as Styled from "./styles"; +import loadable from "@loadable/component"; -export default function ColorInput({ defaultValue, ...props }) { - const id = useId(); +const ColorInput = loadable(() => + import(/* webpackChunkName: "coloris" */ "./ColorInput") +); - const renderValue = value => value ?? defaultValue; - return ( - - ); -} - -ColorInput.displayName = "Form.ColorInput"; +export default ColorInput; diff --git a/client/src/global/components/form/ColorInput/styles.js b/client/src/global/components/form/ColorInput/styles.js index 1a928df06f..684c093ed6 100644 --- a/client/src/global/components/form/ColorInput/styles.js +++ b/client/src/global/components/form/ColorInput/styles.js @@ -1,7 +1,9 @@ import styled from "@emotion/styled"; -import BaseInput from "../BaseInput"; +import { FormBaseInput } from "../BaseInput"; + +export const ColorInput = styled(FormBaseInput)` + --ColorInput-default-color: ${({ $defaultColor }) => $defaultColor}; -export const ColorInput = styled(BaseInput)` input[type="color"] { width: 24px; height: 24px; diff --git a/client/src/theme/styles/vendor/coloris.js b/client/src/theme/styles/vendor/coloris.js new file mode 100644 index 0000000000..9d530e60c2 --- /dev/null +++ b/client/src/theme/styles/vendor/coloris.js @@ -0,0 +1,614 @@ +export default ` + .clr-picker { + display: none; + flex-wrap: wrap; + position: absolute; + width: 200px; + z-index: 1000; + border-radius: 10px; + background-color: #fff; + justify-content: flex-end; + direction: ltr; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.05), 0 5px 20px rgba(0, 0, 0, 0.1); + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; + font-family: var(--font-family-sans); + } + + .clr-picker.clr-open, + .clr-picker[data-inline="true"] { + display: flex; + } + + .clr-picker[data-inline="true"] { + position: relative; + } + + .clr-gradient { + position: relative; + width: 100%; + height: 100px; + margin-bottom: 15px; + border-radius: 3px 3px 0 0; + background-image: linear-gradient(rgba(0, 0, 0, 0), #000), + linear-gradient(90deg, #fff, currentColor); + cursor: pointer; + } + + .clr-marker { + position: absolute; + width: 12px; + height: 12px; + margin: -6px 0 0 -6px; + border: 1px solid #fff; + border-radius: 50%; + background-color: currentColor; + cursor: pointer; + } + + .clr-picker input[type="range"]::-webkit-slider-runnable-track { + width: 100%; + height: 16px; + } + + .clr-picker input[type="range"]::-webkit-slider-thumb { + width: 16px; + height: 16px; + -webkit-appearance: none; + } + + .clr-picker input[type="range"]::-moz-range-track { + width: 100%; + height: 16px; + border: 0; + } + + .clr-picker input[type="range"]::-moz-range-thumb { + width: 16px; + height: 16px; + border: 0; + } + + .clr-hue { + background-image: linear-gradient( + to right, + #f00 0%, + #ff0 16.66%, + #0f0 33.33%, + #0ff 50%, + #00f 66.66%, + #f0f 83.33%, + #f00 100% + ); + } + + .clr-hue, + .clr-alpha { + position: relative; + width: calc(100% - 40px); + height: 8px; + margin: 5px 20px; + border-radius: 4px; + } + + .clr-alpha span { + display: block; + height: 100%; + width: 100%; + border-radius: inherit; + background-image: linear-gradient(90deg, rgba(0, 0, 0, 0), currentColor); + } + + .clr-hue input[type="range"], + .clr-alpha input[type="range"] { + position: absolute; + width: calc(100% + 32px); + height: 16px; + left: -16px; + top: -4px; + margin: 0; + background-color: transparent; + opacity: 0; + cursor: pointer; + appearance: none; + -webkit-appearance: none; + } + + .clr-hue div, + .clr-alpha div { + position: absolute; + width: 16px; + height: 16px; + left: 0; + top: 50%; + margin-left: -8px; + transform: translateY(-50%); + border: 2px solid #fff; + border-radius: 50%; + background-color: currentColor; + box-shadow: 0 0 1px #888; + pointer-events: none; + } + + .clr-alpha div:before { + content: ""; + position: absolute; + height: 100%; + width: 100%; + left: 0; + top: 0; + border-radius: 50%; + background-color: currentColor; + } + + .clr-format { + display: none; + order: 1; + width: calc(100% - 40px); + margin: 0 20px 20px; + } + + .clr-segmented { + display: flex; + position: relative; + width: 100%; + margin: 0; + padding: 0; + border: 1px solid #ddd; + border-radius: 15px; + box-sizing: border-box; + color: #999; + font-size: 12px; + } + + .clr-segmented input, + .clr-segmented legend { + position: absolute; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + border: 0; + left: 0; + top: 0; + opacity: 0; + pointer-events: none; + } + + .clr-segmented label { + flex-grow: 1; + margin: 0; + padding: 4px 0; + font-size: inherit; + font-weight: normal; + line-height: initial; + text-align: center; + cursor: pointer; + } + + .clr-segmented label:first-of-type { + border-radius: 10px 0 0 10px; + } + + .clr-segmented label:last-of-type { + border-radius: 0 10px 10px 0; + } + + .clr-segmented input:checked + label { + color: #fff; + background-color: #666; + } + + .clr-swatches { + order: 2; + width: calc(100% - 32px); + margin: 0 16px; + } + + .clr-swatches div { + display: flex; + flex-wrap: wrap; + padding-bottom: 12px; + justify-content: center; + } + + .clr-swatches button { + position: relative; + width: 20px; + height: 20px; + margin: 0 4px 6px 4px; + padding: 0; + border: 0; + border-radius: 50%; + color: inherit; + text-indent: -1000px; + white-space: nowrap; + overflow: hidden; + cursor: pointer; + } + + .clr-swatches button:after { + content: ""; + display: block; + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; + border-radius: inherit; + background-color: currentColor; + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1); + } + + input.clr-color { + order: 1; + width: calc(100% - 80px); + height: 32px; + margin: 15px 20px 20px auto; + padding: 0 10px; + border: 1px solid #ddd; + border-radius: 16px; + color: #444; + background-color: #fff; + font-family: sans-serif; + font-size: 14px; + text-align: center; + box-shadow: none; + } + + input.clr-color:focus { + outline: none; + border: 1px solid #1e90ff; + } + + .clr-close, + .clr-clear { + display: block; + order: 2; + margin: 0 20px 20px; + padding: 5px 20px 8px; + border: 0; + border-radius: 12px; + color: #fff; + background-color: #666; + font-family: inherit; + font-size: 12px; + font-weight: 400; + line-height: 1; + cursor: pointer; + } + + .clr-close { + margin: 0 20px 20px auto; + } + + .clr-preview { + position: relative; + width: 32px; + height: 32px; + margin: 15px 0 20px 20px; + border-radius: 50%; + overflow: hidden; + } + + .clr-preview:before, + .clr-preview:after { + content: ""; + position: absolute; + height: 100%; + width: 100%; + left: 0; + top: 0; + border: 1px solid #fff; + border-radius: 50%; + } + + .clr-preview:after { + border: 0; + background-color: currentColor; + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1); + } + + .clr-preview button { + position: absolute; + width: 100%; + height: 100%; + z-index: 1; + margin: 0; + padding: 0; + border: 0; + border-radius: 50%; + outline-offset: -2px; + background-color: transparent; + text-indent: -9999px; + cursor: pointer; + overflow: hidden; + } + + .clr-marker, + .clr-hue div, + .clr-alpha div, + .clr-color { + box-sizing: border-box; + } + + .clr-field { + display: inline-block; + position: relative; + color: var(--ColorInput-default-color, transparent); + } + + .clr-field input { + margin: 0; + padding-inline-start: 30px !important; + direction: ltr; + color: var(--input-color); + } + + .clr-field.clr-rtl input { + text-align: right; + } + + .clr-field button { + position: absolute; + width: 20px; + height: 20px; + left: 0; + top: 50%; + transform: translateY(-50%); + margin: 0; + padding: 0; + border: 1px solid var(--input-color); + color: inherit; + text-indent: -1000px; + white-space: nowrap; + overflow: hidden; + pointer-events: none; + } + + .clr-field.clr-rtl button { + right: auto; + left: 0; + } + + .clr-field button:after { + content: ""; + display: block; + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; + border-radius: inherit; + background-color: currentColor; + box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5); + } + + .clr-alpha, + .clr-alpha div, + .clr-swatches button, + .clr-preview:before, + .clr-field button { + background-image: repeating-linear-gradient( + 45deg, + #aaa 25%, + transparent 25%, + transparent 75%, + #aaa 75%, + #aaa + ), + repeating-linear-gradient( + 45deg, + #aaa 25%, + #fff 25%, + #fff 75%, + #aaa 75%, + #aaa + ); + background-position: 0 0, 4px 4px; + background-size: 8px 8px; + } + + .clr-marker:focus { + outline: none; + } + + .clr-keyboard-nav .clr-marker:focus, + .clr-keyboard-nav .clr-hue input:focus + div, + .clr-keyboard-nav .clr-alpha input:focus + div, + .clr-keyboard-nav .clr-segmented input:focus + label { + outline: none; + box-shadow: 0 0 0 2px #1e90ff, 0 0 2px 2px #fff; + } + + .clr-picker[data-alpha="false"] .clr-alpha { + display: none; + } + + .clr-picker[data-minimal="true"] { + padding-top: 16px; + } + + .clr-picker[data-minimal="true"] .clr-gradient, + .clr-picker[data-minimal="true"] .clr-hue, + .clr-picker[data-minimal="true"] .clr-alpha, + .clr-picker[data-minimal="true"] .clr-color, + .clr-picker[data-minimal="true"] .clr-preview { + display: none; + } + + .clr-dark { + background-color: #444; + } + + .clr-dark .clr-segmented { + border-color: #777; + } + + .clr-dark .clr-swatches button:after { + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.3); + } + + .clr-dark input.clr-color { + color: #fff; + border-color: #777; + background-color: #555; + } + + .clr-dark input.clr-color:focus { + border-color: #1e90ff; + } + + .clr-dark .clr-preview:after { + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.5); + } + + .clr-dark .clr-alpha, + .clr-dark .clr-alpha div, + .clr-dark .clr-swatches button, + .clr-dark .clr-preview:before { + background-image: repeating-linear-gradient( + 45deg, + #666 25%, + transparent 25%, + transparent 75%, + #888 75%, + #888 + ), + repeating-linear-gradient( + 45deg, + #888 25%, + #444 25%, + #444 75%, + #888 75%, + #888 + ); + } + + .clr-picker.clr-polaroid { + border-radius: 6px; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.1), 0 5px 30px rgba(0, 0, 0, 0.2); + } + + .clr-picker.clr-polaroid:before { + content: ""; + display: block; + position: absolute; + width: 16px; + height: 10px; + left: 20px; + top: -10px; + border: solid transparent; + border-width: 0 8px 10px 8px; + border-bottom-color: currentColor; + box-sizing: border-box; + color: #fff; + filter: drop-shadow(0 -4px 3px rgba(0, 0, 0, 0.1)); + pointer-events: none; + } + + .clr-picker.clr-polaroid.clr-dark:before { + color: #444; + } + + .clr-picker.clr-polaroid.clr-left:before { + left: auto; + right: 20px; + } + + .clr-picker.clr-polaroid.clr-top:before { + top: auto; + bottom: -10px; + transform: rotateZ(180deg); + } + + .clr-polaroid .clr-gradient { + width: calc(100% - 20px); + height: 120px; + margin: 10px; + border-radius: 3px; + } + + .clr-polaroid .clr-hue, + .clr-polaroid .clr-alpha { + width: calc(100% - 30px); + height: 10px; + margin: 6px 15px; + border-radius: 5px; + } + + .clr-polaroid .clr-hue div, + .clr-polaroid .clr-alpha div { + box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); + } + + .clr-polaroid .clr-format { + width: calc(100% - 20px); + margin: 0 10px 15px; + } + + .clr-polaroid .clr-swatches { + width: calc(100% - 12px); + margin: 0 6px; + } + + .clr-polaroid .clr-swatches div { + padding-bottom: 10px; + } + + .clr-polaroid .clr-swatches button { + width: 22px; + height: 22px; + } + + .clr-polaroid input.clr-color { + width: calc(100% - 60px); + margin: 10px 10px 15px auto; + } + + .clr-polaroid .clr-clear { + margin: 0 10px 15px 10px; + } + + .clr-polaroid .clr-close { + margin: 0 10px 15px auto; + } + + .clr-polaroid .clr-preview { + margin: 10px 0 15px 10px; + } + + .clr-picker.clr-large { + width: 275px; + } + + .clr-large .clr-gradient { + height: 150px; + } + + .clr-large .clr-swatches button { + width: 22px; + height: 22px; + } + + .clr-picker.clr-pill { + width: 380px; + padding-left: 180px; + box-sizing: border-box; + } + + .clr-pill .clr-gradient { + position: absolute; + width: 180px; + height: 100%; + left: 0; + top: 0; + margin-bottom: 0; + border-radius: 3px 0 0 3px; + } + + .clr-pill .clr-hue { + margin-top: 20px; + } +`; diff --git a/client/src/theme/styles/vendor/index.js b/client/src/theme/styles/vendor/index.js index 780e17d29b..9fc80df951 100644 --- a/client/src/theme/styles/vendor/index.js +++ b/client/src/theme/styles/vendor/index.js @@ -1,5 +1,7 @@ import reactDatePicker from "./react-datepicker"; +import coloris from "./coloris"; export default ` ${reactDatePicker} + ${coloris} `; diff --git a/client/yarn.lock b/client/yarn.lock index 46b45d90b7..75b046953b 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2450,6 +2450,13 @@ __metadata: languageName: node linkType: hard +"@melloware/coloris@npm:^0.25.0": + version: 0.25.0 + resolution: "@melloware/coloris@npm:0.25.0" + checksum: 10c0/15727dc98891ae8cca394c4300e4c7ba5015bdc5e1fb7bc411d6f726207ebb14c60de843ed58ef19dfe5bc001655ed38e45dd1ba7680f0c256ca035a417945da + languageName: node + linkType: hard + "@npmcli/agent@npm:^3.0.0": version: 3.0.0 resolution: "@npmcli/agent@npm:3.0.0" @@ -9763,6 +9770,7 @@ __metadata: "@emotion/server": "npm:^11.11.0" "@emotion/styled": "npm:^11.14.1" "@loadable/component": "npm:^5.16.7" + "@melloware/coloris": "npm:^0.25.0" "@pmmmwh/react-refresh-webpack-plugin": "npm:^0.6.1" "@types/react": "npm:^18.x" "@types/react-dom": "npm:^18.x" From 9dbb02368d420a21d789f06edd7fe5b73f84bb09 Mon Sep 17 00:00:00 2001 From: 1aurend Date: Wed, 19 Nov 2025 09:40:31 -0800 Subject: [PATCH 08/12] [B] Ensure color input can render in drawer --- .../hero/Builder/forms/JournalDescription.js | 5 +++-- .../components/form/ColorInput/ColorInput.js | 20 ++++++++++++++----- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/client/src/backend/components/hero/Builder/forms/JournalDescription.js b/client/src/backend/components/hero/Builder/forms/JournalDescription.js index b983708daf..c4c8470fa1 100644 --- a/client/src/backend/components/hero/Builder/forms/JournalDescription.js +++ b/client/src/backend/components/hero/Builder/forms/JournalDescription.js @@ -124,11 +124,12 @@ function JournalDescription({ altTextName="attributes[logoAltText]" altTextLabel={t("journals.forms.logo_alt_label")} /> - { - Coloris.init(); + Coloris.init({ parent: container }); const setColor = event => { const isTarget = event.detail.currentEl.id === inputId; @@ -21,7 +22,7 @@ function ColorInput({ defaultValue, ...props }) { document.addEventListener("coloris:pick", setColor); return () => document.removeEventListener("coloris:pick", setColor); - }, [inputId, props]); + }, [inputId, container, props]); useEffect(() => { const onClose = () => { @@ -38,14 +39,15 @@ function ColorInput({ defaultValue, ...props }) { themeMode: "dark", clearButton: true, defaultColor: defaultValue, - margin: 5 + margin: 5, + parent: container }); inputEl.addEventListener("close", onClose); return () => inputEl.removeEventListener("close", onClose); } - }, [inputRef, defaultValue, colorRef, props]); + }, [inputRef, defaultValue, colorRef, container, props]); return ( . + // Pass a selector if rendering in a focus trap. + container: PropTypes.string + // See BaseInput for remaining propTypes +}; + export default setter(ColorInput); ColorInput.displayName = "Form.ColorInput"; From b6e07de93e8576819f4770d3d9ebfa3c7da4ec4f Mon Sep 17 00:00:00 2001 From: 1aurend Date: Wed, 19 Nov 2025 11:40:08 -0800 Subject: [PATCH 09/12] [B] Ensure preview attrs are shown for feature --- .../src/backend/containers/features/Detail.js | 15 ++++++------ .../backend/containers/features/Properties.js | 12 +++++----- .../locale/en-US/json/backend/records.json | 2 +- .../components/layout/Splash/index.js | 23 +++++++++++++++---- 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/client/src/backend/containers/features/Detail.js b/client/src/backend/containers/features/Detail.js index 6aad66687e..0f3ad1e9a1 100644 --- a/client/src/backend/containers/features/Detail.js +++ b/client/src/backend/containers/features/Detail.js @@ -113,13 +113,8 @@ class FeatureDetailContainer extends PureComponent { previewableFeature(props) { const { session } = props; if (!session) return null; - const { source, dirty } = session; - const previewAttributes = { - ...source.attributes, - ...dirty.attributes - }; - const preview = { ...source, attributes: previewAttributes }; - return preview; + const { dirty } = session; + return dirty.attributes; } isNew(props) { @@ -214,7 +209,11 @@ class FeatureDetailContainer extends PureComponent { label={t("records.features.preview.section_title")} instructions={t("records.features.preview.instructions")} > - +
) : null} {this.renderRoutes()} diff --git a/client/src/backend/containers/features/Properties.js b/client/src/backend/containers/features/Properties.js index 5a50876e1a..713d8a9d88 100644 --- a/client/src/backend/containers/features/Properties.js +++ b/client/src/backend/containers/features/Properties.js @@ -88,20 +88,20 @@ class FeaturesPropertiesContainer extends PureComponent { } ]} /> - - - { + if (preview && previewAttrs?.header) return previewAttrs.header; + return headerFormatted || header; + }; + + const renderedSubheader = () => { + if (preview && previewAttrs?.subheader) return previewAttrs.subheader; + return subheaderFormatted || subheader; + }; + + const renderedBody = () => { + if (preview && previewAttrs?.body) return previewAttrs.body; + return bodyFormatted || body; + }; + return ( {(subheaderFormatted || subheader) && ( )} {linkText && linkUrl ? ( From 3ea19c68c666068874317b4b84ec6c7599548071 Mon Sep 17 00:00:00 2001 From: 1aurend Date: Fri, 21 Nov 2025 12:40:33 -0800 Subject: [PATCH 10/12] [E] Update color input instruction copy --- .../config/app/locale/en-US/json/backend/settings.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/config/app/locale/en-US/json/backend/settings.json b/client/src/config/app/locale/en-US/json/backend/settings.json index 2232c05ac3..08c4ea87a8 100644 --- a/client/src/config/app/locale/en-US/json/backend/settings.json +++ b/client/src/config/app/locale/en-US/json/backend/settings.json @@ -72,13 +72,13 @@ "offset_label": "Header Navigation Offset", "offset_instructions": "Use this field to adjust the vertical position of header navigation. For example, enter \"5\" to move the header down 5 pixels. Enter \"-5\" to move it up 5 pixels.", "accent_color_label": "Accent Color", - "accent_color_instructions": "Enter a color in one of the following formats: CSS color keyword, hexadecimal, rgb, rgba, hsl, hsla, or hwb. Leave blank to restore default accent color.", + "accent_color_instructions": "Enter a color in an accepted CSS color format. Leave blank to restore default color.", "foreground_color_label": "Header Foreground Color", - "foreground_color_instructions": "Override the header foreground color in one of the allowed formats (see above).", + "foreground_color_instructions": "Override the header foreground color. Leave blank to restore default color.", "active_foreground_color_label": "Header Foreground Active Color", - "active_foreground_color_instructions": "Override the header foreground active state color in one of the allowed formats (see above). A link in the primary navigation is \"active\" when the user is currently on that page.", + "active_foreground_color_instructions": "Override the header foreground active route color. Leave blank to restore default color.", "background_color_label": "Header Background Color", - "background_color_instructions": "Override the header background color in one of the allowed formats (see above).", + "background_color_instructions": "Override the header background color. Leave blank to restore default color.", "typography_header": "Typography", "typekit_label": "Typekit ID", "typekit_placeholder": "Enter Typekit ID", From 1ae21a9765f82b73c5d1d34ff954cf2b927aae20 Mon Sep 17 00:00:00 2001 From: 1aurend Date: Fri, 21 Nov 2025 12:44:50 -0800 Subject: [PATCH 11/12] [B] Update color picker focus color --- client/src/theme/styles/vendor/coloris.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/src/theme/styles/vendor/coloris.js b/client/src/theme/styles/vendor/coloris.js index 9d530e60c2..a9c31b607c 100644 --- a/client/src/theme/styles/vendor/coloris.js +++ b/client/src/theme/styles/vendor/coloris.js @@ -1,5 +1,7 @@ export default ` .clr-picker { + --clr-picker-focus-color: var(--hover-color); + display: none; flex-wrap: wrap; position: absolute; @@ -259,7 +261,7 @@ export default ` input.clr-color:focus { outline: none; - border: 1px solid #1e90ff; + border: 1px solid var(--clr-picker-focus-color); } .clr-close, @@ -419,7 +421,7 @@ export default ` .clr-keyboard-nav .clr-alpha input:focus + div, .clr-keyboard-nav .clr-segmented input:focus + label { outline: none; - box-shadow: 0 0 0 2px #1e90ff, 0 0 2px 2px #fff; + box-shadow: 0 0 0 2px var(--clr-picker-focus-color), 0 0 2px 2px #fff; } .clr-picker[data-alpha="false"] .clr-alpha { @@ -457,7 +459,7 @@ export default ` } .clr-dark input.clr-color:focus { - border-color: #1e90ff; + border-color: var(--clr-picker-focus-color); } .clr-dark .clr-preview:after { From acc7440b5017c3a978573a109422c165c68ac5ff Mon Sep 17 00:00:00 2001 From: 1aurend Date: Fri, 21 Nov 2025 12:50:21 -0800 Subject: [PATCH 12/12] [E] Prevent Enter in picker from submitting form --- .../global/components/form/ColorInput/ColorInput.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/client/src/global/components/form/ColorInput/ColorInput.js b/client/src/global/components/form/ColorInput/ColorInput.js index 4a98a279f3..7fb6c5a7b1 100644 --- a/client/src/global/components/form/ColorInput/ColorInput.js +++ b/client/src/global/components/form/ColorInput/ColorInput.js @@ -21,7 +21,18 @@ function ColorInput({ defaultValue, container, ...props }) { document.addEventListener("coloris:pick", setColor); - return () => document.removeEventListener("coloris:pick", setColor); + const handleEnter = e => { + if (e.key !== "Enter") return; + e.preventDefault(); + }; + + const picker = document.querySelector("#clr-picker"); + picker.addEventListener("keydown", handleEnter); + + return () => { + document.removeEventListener("coloris:pick", setColor); + picker.removeEventListener("keydown", handleEnter); + }; }, [inputId, container, props]); useEffect(() => {