diff --git a/app/models/pageflow/customized_theme.rb b/app/models/pageflow/customized_theme.rb
index afc6ddf427..f8c900fda3 100644
--- a/app/models/pageflow/customized_theme.rb
+++ b/app/models/pageflow/customized_theme.rb
@@ -24,7 +24,7 @@ def self.build(entry:, theme:, theme_customization:)
config = Pageflow.config_for(entry)
new(theme,
- config.themes.apply_default_options(theme.options),
+ config.themes.apply_default_options(theme.options, entry:),
config.transform_theme_customization_overrides.call(
theme_customization.overrides,
entry:,
diff --git a/app/models/pageflow/entry_at_revision.rb b/app/models/pageflow/entry_at_revision.rb
index 03c425cd8c..6083fbfbdc 100644
--- a/app/models/pageflow/entry_at_revision.rb
+++ b/app/models/pageflow/entry_at_revision.rb
@@ -26,7 +26,8 @@ def initialize(entry, revision, theme: nil)
:type_name,
to: :entry)
- delegate(:title, :summary, :credits,
+ delegate(:layout_version,
+ :title, :summary, :credits,
:widgets,
:storylines, :main_storyline_chapters, :chapters, :pages,
:share_url, :share_image_id, :share_image_x, :share_image_y,
diff --git a/db/migrate/20260303000000_add_layout_version_to_revisions.rb b/db/migrate/20260303000000_add_layout_version_to_revisions.rb
new file mode 100644
index 0000000000..6d61f02171
--- /dev/null
+++ b/db/migrate/20260303000000_add_layout_version_to_revisions.rb
@@ -0,0 +1,6 @@
+class AddLayoutVersionToRevisions < ActiveRecord::Migration[6.0]
+ def change
+ add_column :pageflow_revisions, :layout_version, :integer
+ change_column_default :pageflow_revisions, :layout_version, from: nil, to: 1
+ end
+end
diff --git a/entry_types/scrolled/config/locales/de.yml b/entry_types/scrolled/config/locales/de.yml
index aa9269c4fe..47197a4af5 100644
--- a/entry_types/scrolled/config/locales/de.yml
+++ b/entry_types/scrolled/config/locales/de.yml
@@ -123,14 +123,10 @@ de:
name: 360° Bilder
decoration_effects:
feature_name: Dekoration-Effekte
- content_element_margins:
- feature_name: "Element Margins"
icon_scroll_indicator:
feature_name: Icon Scroll-Indikator
iconScrollIndicator:
widget_type_name: Icon unten am Viewport-Rand
- section_paddings:
- feature_name: "Abschnittsinnenabstände"
pageflow_scrolled:
editor:
backdrop_effects:
@@ -1241,12 +1237,8 @@ de:
label: Hintergrund-Video (Hochkant)
dynamicShadowOpacity:
inline_help: Intensität der Abblendung, die - gesteuert durch die Scrollposition - hinter dem Text eingeblendet wird, sobald sich Motivbereich und Inhalt des Abschnitts zu überschneiden beginnen, um einen ausreichnenden Kontrast zu gewährleisten. Beachte, dass diese Abblendung nur sichtbar ist, wenn Motivbereich und Inhalt nicht nebeneinander dargestellt werden können. Wechsle am besten in die Phone-Vorschau, wenn du den eingestellten Wert veränderst.
- inline_help_disabled: Aktiviere "Motivbereich freilegen" weiter oben, um diese Einstellung zu verwenden.
+ inline_help_disabled: Setze den oberen Innenabstand in den Abstandseinstellungen des Abschnitts auf "Automatisch anpassen", um diese Einstellung zu verwenden.
label: Dynamische Abblendung
- exposeMotifArea:
- inline_help: Sicherstellen, dass der markierte Motivbereich des Hintergrunds beim Erreichen des Abschnitts sichtbar ist und nicht von anderen Elementen überlagert wird. Wenn Motivbereich und Inhalt des Abschnitts nicht nebeneiner dargestellt werden können, wird zusätzlicher Freiraum am Anfang des Abschnitts eingefügt und der Inhalt nach unten verschoben.
- inline_help_disabled: Wähle "Motivbereich markieren" im Drei-Punkte-Menü des Hintergrund-Bilds/Videos weiter oben, um diese Funktion zu verwenden. du kannst damit sicherzustellen, dass wichtige Teile des Hintegrunds nicht von anderen Elementen überdeckt werden.
- label: Motivbereich freilegen
fullHeight:
inline_help: Aktiviere diese Einstellung, wenn der Hintergrund des Abschnitts (z.B. Bild, Farbfläche oder Hintergrundvideo) so groß angezeigt werden soll, dass das gesamte Browserfenster (der sogenannte Viewport) gefüllt ist. Wenn du mit kurzen Abschnitten arbeiten möchtest, musst du diese Option deaktivieren. Dann nimmt der Abschnitt und sein Hintergrund vertikal nur so viel Platz ein, wie für den Inhalt des Abschnitts unbedingt nötig ist. Es können dann mehrere Abschnitte mit verschiedenen Hintergründen gleichzeitig auf dem Bildschirm des Nutzers sichtbar sein. Um einen Überblendeffekt zwischen den Hintergründen zweier Abschnitte verwenden zu können, muss diese Option für beide Abschnitte aktiviert sein.
label: Volle Viewport-Höhe
@@ -1646,10 +1638,11 @@ de:
rechts neben dem Text dargestellt werden kann, so wird
kein zusätzlicher Abstand oberhalb des Texts eingefügt.
- Deaktiviere die Option **Motivbereich freilegen**, falls
- der Bildauschnitt zwar passend zum Motivbereich gewählt
- werden soll, der Inhalt des Abschnitts das Motiv aber
- überlagern darf.
+ Wähle in den Abstandseinstellungen des Abschnitts beim
+ oberen Innenabstand die Option **Manuell festlegen**,
+ falls der Bildauschnitt zwar passend zum Motivbereich
+ gewählt werden soll, der Inhalt des Abschnitts das Motiv
+ aber überlagern darf.
Der Motivbereich kann über den Menüpunkt **Bildmotiv
markieren** der **Hintergrund-Bild**- und **Hintergrund
diff --git a/entry_types/scrolled/config/locales/en.yml b/entry_types/scrolled/config/locales/en.yml
index 6018ab4095..c5b14f9b6d 100644
--- a/entry_types/scrolled/config/locales/en.yml
+++ b/entry_types/scrolled/config/locales/en.yml
@@ -123,14 +123,10 @@ en:
name: 360° images
decoration_effects:
feature_name: Decoration Effects
- content_element_margins:
- feature_name: "Element Margins"
icon_scroll_indicator:
feature_name: Icon Scroll Indicator
iconScrollIndicator:
widget_type_name: Icon at bottom of viewport
- section_paddings:
- feature_name: "Section paddings"
pageflow_scrolled:
editor:
backdrop_effects:
@@ -1225,12 +1221,8 @@ en:
label: Background-Video (Portrait)
dynamicShadowOpacity:
inline_help: Intensity of the shadow which is faded in behind the text to ensure contrast when content is scrolled to intersect with the motif area. Note that this shadow is only visible if motif area and section content do not fit side by side. It's best to switch to phone preview when adjusting this value.
- inline_help_disabled: Activate "Expose motif area" above to use this setting.
+ inline_help_disabled: Set top padding to "Adjust automatically" in the section's padding settings to use this option.
label: Dynamic shadow
- exposeMotifArea:
- inline_help: Ensure the selected motif area of the background asset is visible and not covered by other elements when first reaching the section. If motif area and section content do not fit side by side, extra space will be inserted at the top of the section to move content down.
- inline_help_disabled: Click "Select motif area" in the "three dots" menu of the background image/video field above to use this feature. It let's you make sure important parts of the backdrop asset won't be hidden by other elements.
- label: Expose motif area
fullHeight:
inline_help: Activate this option if the background of this section (e.g. image, color or video) is supposed to be enlarged such that the whole browser window (the so called viewport) is covered. Deactivate this option to create a short section. The height of the section and its background is then determined only by the content of the section. Multiple sections with different backgrounds can then be visible at once on the user's screen. This option needs to be activated for two adjacent sections to be able to use a fade transition between them.
label: Use Full Viewport-Height
diff --git a/entry_types/scrolled/lib/pageflow_scrolled/plugin.rb b/entry_types/scrolled/lib/pageflow_scrolled/plugin.rb
index 35dbf2e109..5f10ce05f0 100644
--- a/entry_types/scrolled/lib/pageflow_scrolled/plugin.rb
+++ b/entry_types/scrolled/lib/pageflow_scrolled/plugin.rb
@@ -30,21 +30,21 @@ def configure(config)
'xxl' => '16em'
}
- c.themes.register_default_options(
+ c.themes.register_options_transform(
ThemeOptionsDefaultScale.new(
prefix: 'section_padding_top',
values: padding_scale
)
)
- c.themes.register_default_options(
+ c.themes.register_options_transform(
ThemeOptionsDefaultScale.new(
prefix: 'section_padding_bottom',
values: padding_scale
)
)
- c.themes.register_default_options(
+ c.themes.register_options_transform(
ThemeOptionsDefaultScale.new(
prefix: 'content_element_margin',
values: margin_scale
@@ -54,16 +54,37 @@ def configure(config)
c.themes.register_default_options(
properties: {
root: {
- 'section_default_padding_top' => '1.375em',
- 'section_default_padding_bottom' => '4.375em',
+ 'section_default_padding_top' => 'max(10em, 20svh)',
+ 'section_default_padding_bottom' => 'max(10em, 20svh)',
'content_element_margin_style_default' => '2em'
},
+ cards_appearance_section: {
+ 'section_default_padding_top' => 'max(10em, 20svh)',
+ 'section_default_padding_bottom' => 'max(10em, 20svh)'
+ }
+ }
+ )
+
+ legacy_paddings = {
+ properties: {
+ root: {
+ 'section_default_padding_top' => '1.375em',
+ 'section_default_padding_bottom' => '4.375em'
+ },
cards_appearance_section: {
'section_default_padding_top' => '3em',
'section_default_padding_bottom' => '6em'
}
}
- )
+ }
+
+ c.themes.register_options_transform(lambda { |options, entry:, **|
+ if entry.layout_version
+ options
+ else
+ options.deep_merge(legacy_paddings)
+ end
+ })
c.themes.register_default_options(
typography: {
@@ -174,9 +195,7 @@ def configure(config)
c.features.register('backdrop_content_elements')
c.features.register('custom_palette_colors')
c.features.register('decoration_effects')
- c.features.register('content_element_margins')
c.features.register('backdrop_size')
- c.features.register('section_paddings')
c.features.register('faq_page_structured_data') do |feature_config|
feature_config.entry_structured_data_types.register(
diff --git a/entry_types/scrolled/lib/pageflow_scrolled/theme_options_default_scale.rb b/entry_types/scrolled/lib/pageflow_scrolled/theme_options_default_scale.rb
index 0b6de884d7..fb5e6c8474 100644
--- a/entry_types/scrolled/lib/pageflow_scrolled/theme_options_default_scale.rb
+++ b/entry_types/scrolled/lib/pageflow_scrolled/theme_options_default_scale.rb
@@ -10,7 +10,7 @@ def initialize(prefix:, values:)
@values = values
end
- def call(options)
+ def call(options, **)
return options if scale_defined?(options)
options.deep_merge(
diff --git a/entry_types/scrolled/package/spec/editor/views/EditDefaultsView-spec.js b/entry_types/scrolled/package/spec/editor/views/EditDefaultsView-spec.js
index 155feb055c..2bc6c555bb 100644
--- a/entry_types/scrolled/package/spec/editor/views/EditDefaultsView-spec.js
+++ b/entry_types/scrolled/package/spec/editor/views/EditDefaultsView-spec.js
@@ -2,7 +2,6 @@ import '@testing-library/jest-dom/extend-expect';
import {fireEvent} from '@testing-library/react';
import {EditDefaultsView} from 'editor/views/EditDefaultsView';
-import {features} from 'pageflow/frontend';
import {CheckBoxInputView} from 'pageflow/ui';
import {ConfigurationEditor, useFakeTranslations, renderBackboneView} from 'pageflow/testHelpers';
@@ -99,50 +98,46 @@ describe('EditDefaultsView', () => {
expect(getByText('Changes to these settings have no effect on existing elements.')).toBeInTheDocument();
});
- describe('with section_paddings feature flag', () => {
- beforeEach(() => features.enable('frontend', ['section_paddings']));
-
- it('contains defaultSectionPaddingTop slider input', () => {
- const entry = createEntry({});
-
- const view = new EditDefaultsView({
- model: entry.metadata,
- entry
- });
-
- view.render();
- const configurationEditor = ConfigurationEditor.find(view);
+ it('contains defaultSectionPaddingTop slider input', () => {
+ const entry = createEntry({});
- expect(configurationEditor.inputPropertyNames()).toContain('defaultSectionPaddingTop');
+ const view = new EditDefaultsView({
+ model: entry.metadata,
+ entry
});
- it('contains defaultSectionPaddingBottom slider input', () => {
- const entry = createEntry({});
+ view.render();
+ const configurationEditor = ConfigurationEditor.find(view);
- const view = new EditDefaultsView({
- model: entry.metadata,
- entry
- });
+ expect(configurationEditor.inputPropertyNames()).toContain('defaultSectionPaddingTop');
+ });
- view.render();
- const configurationEditor = ConfigurationEditor.find(view);
+ it('contains defaultSectionPaddingBottom slider input', () => {
+ const entry = createEntry({});
- expect(configurationEditor.inputPropertyNames()).toContain('defaultSectionPaddingBottom');
+ const view = new EditDefaultsView({
+ model: entry.metadata,
+ entry
});
- it('shows padding visualizations', () => {
- const entry = createEntry({});
+ view.render();
+ const configurationEditor = ConfigurationEditor.find(view);
- const view = new EditDefaultsView({
- model: entry.metadata,
- entry
- });
+ expect(configurationEditor.inputPropertyNames()).toContain('defaultSectionPaddingBottom');
+ });
- const {getByRole} = renderBackboneView(view);
+ it('shows padding visualizations', () => {
+ const entry = createEntry({});
- expect(getByRole('img', {name: 'TopPadding'})).toBeInTheDocument();
- expect(getByRole('img', {name: 'Bottom'})).toBeInTheDocument();
+ const view = new EditDefaultsView({
+ model: entry.metadata,
+ entry
});
+
+ const {getByRole} = renderBackboneView(view);
+
+ expect(getByRole('img', {name: 'TopPadding'})).toBeInTheDocument();
+ expect(getByRole('img', {name: 'Bottom'})).toBeInTheDocument();
});
describe('with content element type defaultsInputs', () => {
diff --git a/entry_types/scrolled/package/spec/frontend/features/marginIndicator-spec.js b/entry_types/scrolled/package/spec/frontend/features/marginIndicator-spec.js
index a760027ab6..a945cb35ce 100644
--- a/entry_types/scrolled/package/spec/frontend/features/marginIndicator-spec.js
+++ b/entry_types/scrolled/package/spec/frontend/features/marginIndicator-spec.js
@@ -1,6 +1,5 @@
import {useInlineEditingPageObjects, renderEntry} from 'support/pageObjects';
import {fakeParentWindow} from 'support';
-import {features} from 'pageflow/frontend';
import '@testing-library/jest-dom/extend-expect';
describe('MarginIndicator', () => {
@@ -8,7 +7,6 @@ describe('MarginIndicator', () => {
beforeEach(() => {
fakeParentWindow();
- features.enable('frontend', ['content_element_margins']);
});
it('displays scale translation for top margin when element is selected', () => {
diff --git a/entry_types/scrolled/package/spec/frontend/features/paddingIndicator-spec.js b/entry_types/scrolled/package/spec/frontend/features/paddingIndicator-spec.js
index e45a97a802..e9cec751ca 100644
--- a/entry_types/scrolled/package/spec/frontend/features/paddingIndicator-spec.js
+++ b/entry_types/scrolled/package/spec/frontend/features/paddingIndicator-spec.js
@@ -1,7 +1,6 @@
import {useInlineEditingPageObjects, renderEntry} from 'support/pageObjects';
import {fakeParentWindow} from 'support';
import {useMotifAreaState} from 'frontend/v1/useMotifAreaState';
-import {features} from 'pageflow/frontend';
import '@testing-library/jest-dom/extend-expect';
jest.mock('frontend/v1/useMotifAreaState');
@@ -11,7 +10,6 @@ describe('PaddingIndicator', () => {
beforeEach(() => {
fakeParentWindow();
- features.enable('frontend', ['section_paddings']);
});
it('displays scale translation for top padding when section is selected', () => {
diff --git a/entry_types/scrolled/package/spec/frontend/features/sectionPadding-spec.js b/entry_types/scrolled/package/spec/frontend/features/sectionPadding-spec.js
index 8acf9c8102..27db28b1f2 100644
--- a/entry_types/scrolled/package/spec/frontend/features/sectionPadding-spec.js
+++ b/entry_types/scrolled/package/spec/frontend/features/sectionPadding-spec.js
@@ -3,17 +3,12 @@ import {act} from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
-import {features} from 'pageflow/frontend';
import {usePortraitOrientation} from 'frontend/usePortraitOrientation';
import {useMotifAreaState} from 'frontend/v1/useMotifAreaState';
jest.mock('frontend/usePortraitOrientation');
jest.mock('frontend/v1/useMotifAreaState');
describe('section padding', () => {
- beforeEach(() => {
- features.enable('frontend', ['section_paddings']);
- });
-
useInlineEditingPageObjects();
it('does not suppress top padding by default', () => {
diff --git a/entry_types/scrolled/package/spec/frontend/features/selectedMessage-spec.js b/entry_types/scrolled/package/spec/frontend/features/selectedMessage-spec.js
index 243454c963..b3510fc89d 100644
--- a/entry_types/scrolled/package/spec/frontend/features/selectedMessage-spec.js
+++ b/entry_types/scrolled/package/spec/frontend/features/selectedMessage-spec.js
@@ -3,7 +3,6 @@ import {frontend, WidgetSelectionRect} from 'frontend';
import {useInlineEditingPageObjects, renderEntry} from 'support/pageObjects';
import {fakeParentWindow} from 'support';
-import {features} from 'pageflow/frontend';
import {fireEvent} from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect'
@@ -124,46 +123,40 @@ describe('SELECTED message', () => {
}, expect.anything());
});
- describe('padding indicators', () => {
- beforeEach(() => {
- features.enable('frontend', ['section_paddings']);
+ it('is posted when top padding indicator is clicked', () => {
+ const {getSectionByPermaId} = renderEntry({
+ seed: {
+ sections: [{id: 1, permaId: 10}],
+ contentElements: [{sectionId: 1}]
+ }
});
- it('is posted when top padding indicator is clicked', () => {
- const {getSectionByPermaId} = renderEntry({
- seed: {
- sections: [{id: 1, permaId: 10}],
- contentElements: [{sectionId: 1}]
- }
- });
-
- const section = getSectionByPermaId(10);
- section.select();
- fireEvent.click(section.getPaddingIndicator('top'));
-
- expect(window.parent.postMessage).toHaveBeenCalledWith({
- type: 'SELECTED',
- payload: {id: 1, type: 'sectionPaddings', position: 'top'}
- }, expect.anything());
- });
+ const section = getSectionByPermaId(10);
+ section.select();
+ fireEvent.click(section.getPaddingIndicator('top'));
+
+ expect(window.parent.postMessage).toHaveBeenCalledWith({
+ type: 'SELECTED',
+ payload: {id: 1, type: 'sectionPaddings', position: 'top'}
+ }, expect.anything());
+ });
- it('is posted when bottom padding indicator is clicked', () => {
- const {getSectionByPermaId} = renderEntry({
- seed: {
- sections: [{id: 1, permaId: 10}],
- contentElements: [{sectionId: 1}]
- }
- });
-
- const section = getSectionByPermaId(10);
- section.select();
- fireEvent.click(section.getPaddingIndicator('bottom'));
-
- expect(window.parent.postMessage).toHaveBeenCalledWith({
- type: 'SELECTED',
- payload: {id: 1, type: 'sectionPaddings', position: 'bottom'}
- }, expect.anything());
+ it('is posted when bottom padding indicator is clicked', () => {
+ const {getSectionByPermaId} = renderEntry({
+ seed: {
+ sections: [{id: 1, permaId: 10}],
+ contentElements: [{sectionId: 1}]
+ }
});
+
+ const section = getSectionByPermaId(10);
+ section.select();
+ fireEvent.click(section.getPaddingIndicator('bottom'));
+
+ expect(window.parent.postMessage).toHaveBeenCalledWith({
+ type: 'SELECTED',
+ payload: {id: 1, type: 'sectionPaddings', position: 'bottom'}
+ }, expect.anything());
});
it('is posted when widget selection rect is clicked', () => {
diff --git a/entry_types/scrolled/package/src/editor/views/EditDefaultsView.js b/entry_types/scrolled/package/src/editor/views/EditDefaultsView.js
index 1a4ff82a73..d42183f9f7 100644
--- a/entry_types/scrolled/package/src/editor/views/EditDefaultsView.js
+++ b/entry_types/scrolled/package/src/editor/views/EditDefaultsView.js
@@ -1,8 +1,6 @@
import I18n from 'i18n-js';
import {EditConfigurationView, InfoBoxView} from 'pageflow/editor';
import {CheckBoxInputView, SelectInputView, SliderInputView} from 'pageflow/ui';
-import {features} from 'pageflow/frontend';
-
import {editor} from '../api';
import {ContentElementTypeSeparatorView} from './ContentElementTypeSeparatorView';
import {SectionPaddingVisualizationView} from './inputs/SectionPaddingVisualizationView';
@@ -32,32 +30,30 @@ export const EditDefaultsView = EditConfigurationView.extend({
values: ['shadow', 'cards', 'transparent']
});
- if (features.isEnabled('section_paddings')) {
- const paddingTopScale = entry.getScale('sectionPaddingTop');
- const paddingBottomScale = entry.getScale('sectionPaddingBottom');
+ const paddingTopScale = entry.getScale('sectionPaddingTop');
+ const paddingBottomScale = entry.getScale('sectionPaddingBottom');
- this.input('topPaddingVisualization', SectionPaddingVisualizationView, {
- variant: 'topPadding'
- });
- this.input('defaultSectionPaddingTop', SliderInputView, {
- hideLabel: true,
- icon: paddingTopIcon,
- values: paddingTopScale.values,
- texts: paddingTopScale.texts,
- defaultValue: paddingTopScale.defaultValue
- });
+ this.input('topPaddingVisualization', SectionPaddingVisualizationView, {
+ variant: 'topPadding'
+ });
+ this.input('defaultSectionPaddingTop', SliderInputView, {
+ hideLabel: true,
+ icon: paddingTopIcon,
+ values: paddingTopScale.values,
+ texts: paddingTopScale.texts,
+ defaultValue: paddingTopScale.defaultValue
+ });
- this.input('bottomPaddingVisualization', SectionPaddingVisualizationView, {
- variant: 'bottomPadding'
- });
- this.input('defaultSectionPaddingBottom', SliderInputView, {
- hideLabel: true,
- icon: paddingBottomIcon,
- values: paddingBottomScale.values,
- texts: paddingBottomScale.texts,
- defaultValue: paddingBottomScale.defaultValue
- });
- }
+ this.input('bottomPaddingVisualization', SectionPaddingVisualizationView, {
+ variant: 'bottomPadding'
+ });
+ this.input('defaultSectionPaddingBottom', SliderInputView, {
+ hideLabel: true,
+ icon: paddingBottomIcon,
+ values: paddingBottomScale.values,
+ texts: paddingBottomScale.texts,
+ defaultValue: paddingBottomScale.defaultValue
+ });
});
configurationEditor.tab('content_elements', function() {
diff --git a/entry_types/scrolled/package/src/editor/views/EditSectionView.js b/entry_types/scrolled/package/src/editor/views/EditSectionView.js
index 5a72540d00..2e6ddd83a0 100644
--- a/entry_types/scrolled/package/src/editor/views/EditSectionView.js
+++ b/entry_types/scrolled/package/src/editor/views/EditSectionView.js
@@ -159,11 +159,9 @@ export const EditSectionView = EditConfigurationView.extend({
values: ['left', 'right', 'center', 'centerRagged']
});
- if (features.isEnabled('section_paddings')) {
- this.input('sectionPaddings', SectionPaddingsInputView, {
- entry
- });
- }
+ this.input('sectionPaddings', SectionPaddingsInputView, {
+ entry
+ });
if (entry.supportsSectionWidths()) {
this.input('width', SelectInputView, {
@@ -175,18 +173,6 @@ export const EditSectionView = EditConfigurationView.extend({
});
this.input('invert', CheckBoxInputView);
- if (!features.isEnabled('section_paddings')) {
- this.input('exposeMotifArea', CheckBoxInputView, {
- displayUncheckedIfDisabled: true,
- visibleBinding: ['backdropType'],
- visible: ([backdropType]) => {
- return backdropType !== 'color' && backdropType !== 'contentElement';
- },
- disabledBinding: motifAreaDisabledBinding,
- disabled: motifAreaDisabled,
- });
- }
-
this.input('staticShadowOpacity', SliderInputView, {
defaultValue: 70,
visibleBinding: 'appearance',
diff --git a/entry_types/scrolled/package/src/editor/views/configurationEditors/groups/CommonContentElementAttributes.js b/entry_types/scrolled/package/src/editor/views/configurationEditors/groups/CommonContentElementAttributes.js
index c2aacacccf..5f9c6b2df4 100644
--- a/entry_types/scrolled/package/src/editor/views/configurationEditors/groups/CommonContentElementAttributes.js
+++ b/entry_types/scrolled/package/src/editor/views/configurationEditors/groups/CommonContentElementAttributes.js
@@ -79,13 +79,11 @@ ConfigurationEditorTabView.groups.define('ContentElementPosition', function({ent
});
}
- if (features.isEnabled('content_element_margins')) {
- this.input('styles', ContentElementStyleListInputView, {
- entry,
- contentElement,
- attributeTranslationKeyPrefixes: ['pageflow_scrolled.editor.common_content_element_attributes']
- });
- }
+ this.input('styles', ContentElementStyleListInputView, {
+ entry,
+ contentElement,
+ attributeTranslationKeyPrefixes: ['pageflow_scrolled.editor.common_content_element_attributes']
+ });
});
ConfigurationEditorTabView.groups.define(
diff --git a/entry_types/scrolled/package/src/frontend/inlineEditing/ContentElementDecorator.js b/entry_types/scrolled/package/src/frontend/inlineEditing/ContentElementDecorator.js
index 4e4c4e83c3..fcd4857e8c 100644
--- a/entry_types/scrolled/package/src/frontend/inlineEditing/ContentElementDecorator.js
+++ b/entry_types/scrolled/package/src/frontend/inlineEditing/ContentElementDecorator.js
@@ -1,8 +1,6 @@
import React from 'react';
import {useDrag} from 'react-dnd';
-import {features} from 'pageflow/frontend';
-
import {useContentElementEditorState} from '../useContentElementEditorState';
import {useI18n} from '../i18n';
import {api} from '../api';
@@ -81,10 +79,6 @@ function DefaultSelectionRect(props) {
}
function renderMarginIndicators(props) {
- if (!features.isEnabled('content_element_margins')) {
- return null;
- }
-
return (
<>
diff --git a/entry_types/scrolled/package/src/frontend/inlineEditing/ForegroundDecorator.js b/entry_types/scrolled/package/src/frontend/inlineEditing/ForegroundDecorator.js
index a466ac26de..a017344a81 100644
--- a/entry_types/scrolled/package/src/frontend/inlineEditing/ForegroundDecorator.js
+++ b/entry_types/scrolled/package/src/frontend/inlineEditing/ForegroundDecorator.js
@@ -1,14 +1,8 @@
import React from 'react';
-import {features} from 'pageflow/frontend';
-
import {PaddingIndicator} from './PaddingIndicator';
export function ForegroundDecorator({section, motifAreaState, sectionPadding, suppressedPaddings, children}) {
- if (!features.isEnabled('section_paddings')) {
- return children;
- }
-
return (
<>
'0', 'sm' => '1em'}
)
- result = transform.call(colors: {accent: '#f00'})
+ result = transform.call({colors: {accent: '#f00'}})
expect(result).to eq(
colors: {accent: '#f00'},
@@ -46,11 +46,11 @@ module PageflowScrolled
)
result = transform.call(
- properties: {
+ {properties: {
root: {
'section_padding_top-lg' => '5em'
}
- }
+ }}
)
expect(result).to eq(
@@ -69,11 +69,11 @@ module PageflowScrolled
)
result = transform.call(
- properties: {
+ {properties: {
root: {
'section_padding_bottom-lg' => '5em'
}
- }
+ }}
)
expect(result).to eq(
diff --git a/lib/pageflow/themes.rb b/lib/pageflow/themes.rb
index b42182370c..1ac1ea20b3 100644
--- a/lib/pageflow/themes.rb
+++ b/lib/pageflow/themes.rb
@@ -4,37 +4,46 @@ class Themes # rubocop:todo Style/Documentation
def initialize
@themes = HashWithIndifferentAccess.new
+ @default_options = {}
@options_transforms = []
end
# Register default options that apply to all themes. Can be called
# multiple times to accumulate defaults from different sources
- # (gem, plugins, host app).
+ # (gem, plugins, host app). Later calls override earlier ones for
+ # the same keys. Theme options always take precedence over defaults.
#
- # @overload register_default_options(options)
- # @param options [Hash]
- # Default options to deep merge into theme options.
+ # @param options [Hash]
+ # Default options to deep merge into accumulated defaults.
#
- # @overload register_default_options(callable)
- # @param callable [#call]
- # Receives options hash, returns transformed options.
- # Use for conditional defaults based on what theme defines.
+ # @since edge
+ def register_default_options(options)
+ @default_options = @default_options.deep_merge(options)
+ end
+
+ # Register a transform that can conditionally modify theme options.
+ # Transforms run after defaults are merged with theme options, so
+ # they can inspect what the theme defines and add conditional defaults.
+ #
+ # @param callable [#call]
+ # Receives merged options hash, returns transformed options.
+ # Use for conditional defaults based on what theme defines.
#
# @since edge
- def register_default_options(options_or_callable)
- @options_transforms << if options_or_callable.respond_to?(:call)
- options_or_callable
- else
- ->(options) { options_or_callable.deep_merge(options) }
- end
+ def register_options_transform(callable)
+ @options_transforms << callable
end
# Apply all registered defaults to theme options.
#
# @api private
- def apply_default_options(options)
- @options_transforms.reduce(options.deep_dup) do |opts, transform|
- transform.call(opts)
+ def apply_default_options(options, entry:)
+ # First merge hash defaults under theme options (theme wins)
+ result = @default_options.deep_merge(options)
+
+ # Then apply callable transforms (they can see theme options)
+ @options_transforms.reduce(result) do |opts, transform|
+ transform.call(opts, entry:)
end
end
diff --git a/spec/pageflow/theme_customizations_spec.rb b/spec/pageflow/theme_customizations_spec.rb
index c4ccd60d1e..017fa2b108 100644
--- a/spec/pageflow/theme_customizations_spec.rb
+++ b/spec/pageflow/theme_customizations_spec.rb
@@ -546,12 +546,12 @@ module Pageflow
)
end
- it 'supports callable for conditional defaults' do
+ it 'supports transform for conditional defaults' do
pageflow_configure do |config|
rainbow_entry_type = TestEntryType.register(config, name: 'rainbow')
config.for_entry_type(rainbow_entry_type) do |c|
- c.themes.register_default_options(->(options) {
+ c.themes.register_options_transform(->(options, **) {
if options[:colors].blank?
options.deep_merge(colors: {accent: '#default'})
else
@@ -568,6 +568,33 @@ module Pageflow
expect(entry.theme.options).to match(colors: {accent: '#f00'})
end
+ it 'lets transform skip defaults based on other theme option' do
+ pageflow_configure do |config|
+ rainbow_entry_type = TestEntryType.register(config, name: 'rainbow')
+
+ config.for_entry_type(rainbow_entry_type) do |c|
+ c.themes.register_options_transform(lambda { |options, **|
+ if options[:custom_scale].blank?
+ options.deep_merge(scale: {small: '1em', large: '2em'})
+ else
+ options
+ end
+ })
+ c.themes.register('default')
+ c.themes.register('custom', custom_scale: true, scale: {small: '10px'})
+ end
+ end
+ default_entry = create(:published_entry,
+ type_name: 'rainbow',
+ revision_attributes: {theme_name: 'default'})
+ custom_entry = create(:published_entry,
+ type_name: 'rainbow',
+ revision_attributes: {theme_name: 'custom'})
+
+ expect(default_entry.theme.options).to match(scale: {small: '1em', large: '2em'})
+ expect(custom_entry.theme.options).to match(custom_scale: true, scale: {small: '10px'})
+ end
+
it 'scopes default options by entry type' do
pageflow_configure do |config|
rainbow_entry_type = TestEntryType.register(config, name: 'rainbow')
@@ -588,5 +615,96 @@ module Pageflow
expect(entry.theme.options).to match(colors: {accent: '#f00'})
end
+
+ it 'lets feature blocks override default options' do
+ pageflow_configure do |config|
+ rainbow_entry_type = TestEntryType.register(config, name: 'rainbow')
+
+ config.for_entry_type(rainbow_entry_type) do |c|
+ c.features.register('legacy_colors') do |feature_config|
+ feature_config.themes.register_default_options(colors: {accent: '#legacy'})
+ end
+
+ c.themes.register_default_options(colors: {accent: '#new'})
+ c.themes.register('dark')
+ end
+ end
+ entry = create(:published_entry,
+ type_name: 'rainbow',
+ with_feature: 'legacy_colors',
+ revision_attributes: {theme_name: 'dark'})
+
+ expect(entry.theme.options).to match(colors: {accent: '#legacy'})
+ end
+
+ it 'uses default options when feature is not enabled' do
+ pageflow_configure do |config|
+ rainbow_entry_type = TestEntryType.register(config, name: 'rainbow')
+
+ config.for_entry_type(rainbow_entry_type) do |c|
+
+ c.features.register('legacy_colors') do |feature_config|
+ feature_config.themes.register_default_options(colors: {accent: '#legacy'})
+ end
+
+ c.themes.register_default_options(colors: {accent: '#new'})
+ c.themes.register('dark')
+ end
+ end
+ entry = create(:published_entry,
+ type_name: 'rainbow',
+ revision_attributes: {theme_name: 'dark'})
+
+ expect(entry.theme.options).to match(colors: {accent: '#new'})
+ end
+
+ it 'passes entry to options transforms' do
+ transform = double('transform', call: {})
+ pageflow_configure do |config|
+ rainbow_entry_type = TestEntryType.register(config, name: 'rainbow')
+
+ config.for_entry_type(rainbow_entry_type) do |c|
+ c.themes.register_options_transform(transform)
+ c.themes.register('dark')
+ end
+ end
+ entry = create(:published_entry,
+ type_name: 'rainbow',
+ revision_attributes: {theme_name: 'dark'})
+
+ entry.theme
+
+ expect(transform)
+ .to have_received(:call).with(anything, entry:)
+ end
+
+ it 'lets transform use entry to conditionally set defaults' do
+ pageflow_configure do |config|
+ rainbow_entry_type = TestEntryType.register(config, name: 'rainbow')
+
+ config.for_entry_type(rainbow_entry_type) do |c|
+ c.themes.register_default_options(colors: {accent: '#new'})
+ c.themes.register_options_transform(lambda { |options, entry:, **|
+ if entry.layout_version
+ options
+ else
+ options.deep_merge(colors: {accent: '#legacy'})
+ end
+ })
+ c.themes.register('dark')
+ end
+ end
+ legacy_entry = create(:published_entry,
+ type_name: 'rainbow',
+ revision_attributes: {theme_name: 'dark',
+ layout_version: nil})
+ new_entry = create(:published_entry,
+ type_name: 'rainbow',
+ revision_attributes: {theme_name: 'dark',
+ layout_version: 1})
+
+ expect(legacy_entry.theme.options).to match(colors: {accent: '#legacy'})
+ expect(new_entry.theme.options).to match(colors: {accent: '#new'})
+ end
end
end