Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/assets/stylesheets/pageflow/ui/color_picker.scss
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@
display: none;
}

.color_picker-swatch_divider {
flex-basis: 100%;
height: 0;
margin: size(1) 0;
border-top: 1px solid var(--ui-on-surface-color-lightest);
}

button {
position: relative;
width: size(5);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,3 +377,44 @@ en:

Users will now be able to select "Brand Blue" as a color for quotes,
counters and text paragraphs.

## Background Color Presets

Themes can supply a set of background color presets that editors can
pick from when choosing the background color of a section, the surface
color of cards or the overlay color of split sections. They appear as
swatches in the color picker, separated by a divider from the colors
already used elsewhere in the story.

Unlike palette colors, presets are **not** CSS custom properties and are
**not** referenced by name. When an editor picks a preset, its color
value is copied into the section. Editing or removing a preset later
therefore does not change sections that already use that color – think
of a preset as a suggested starting point rather than a managed color
that stays in sync. This makes presets a good fit for background colors,
which are usually chosen for a specific section while looking at its
content and contrast, and rarely meant to change story-wide.

Presets are configured via the `presets` theme option as a list of
objects with a `value` and an optional `name`:

``` ruby
entry_type_config.themes.register(:my_custom_theme,
# ...
presets: {
background_colors: [
{value: '#c9e9fb', name: 'Extra Light Blue'},
{value: '#b9d3c6', name: 'BG Green'},
{value: '#f6e8d7', name: 'BG Sand'}
]
})
```

The `name` is shown as the swatch's tooltip. Since presets are data, not
managed colors, the name is stored inline with the value instead of in a
translation file. This keeps presets editable from a host application
(for example via theme customization overrides) without having to deploy
translations.

If a theme defines no `presets`, the color picker only offers the colors
already used in other sections, as before.
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {ScrolledEntry} from 'editor/models/ScrolledEntry';
import {factories} from 'pageflow/testHelpers';
import {normalizeSeed} from 'support';

describe('ScrolledEntry', () => {
describe('#getBackgroundColorPresets', () => {
it('returns empty array by default', () => {
const entry = factories.entry(
ScrolledEntry,
{},
{entryTypeSeed: normalizeSeed()}
);

expect(entry.getBackgroundColorPresets()).toEqual([]);
});

it('returns value and text for each preset', () => {
const entry = factories.entry(
ScrolledEntry,
{},
{
entryTypeSeed: normalizeSeed({
themeOptions: {
presets: {
backgroundColors: [
{value: '#c9e9fb', name: 'Extra Light Blue'},
{value: '#fbe6b8', name: 'BG Yellow'}
]
}
}
})
}
);

expect(entry.getBackgroundColorPresets()).toEqual([
{value: '#c9e9fb', text: 'Extra Light Blue'},
{value: '#fbe6b8', text: 'BG Yellow'}
]);
});

it('falls back to value as text when name is missing', () => {
const entry = factories.entry(
ScrolledEntry,
{},
{
entryTypeSeed: normalizeSeed({
themeOptions: {
presets: {
backgroundColors: [{value: '#c9e9fb'}]
}
}
})
}
);

expect(entry.getBackgroundColorPresets()).toEqual([
{value: '#c9e9fb', text: '#c9e9fb'}
]);
});
});
});
100 changes: 100 additions & 0 deletions entry_types/scrolled/package/spec/editor/views/EditSectionView-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import {EditSectionView} from 'editor/views/EditSectionView';
import {ConfigurationEditor, DropDownButton, useFakeTranslations} from 'pageflow/testHelpers';
import {useEditorGlobals} from 'support';

function backdropColorSwatches(view) {
return view.$el.find('.color_input').filter((_, el) =>
el.querySelector('.color_picker-swatches button[title="Extra Light Blue"]')
).first();
}

describe('EditSectionView', () => {
const {createEntry} = useEditorGlobals();

Expand Down Expand Up @@ -271,6 +277,100 @@ describe('EditSectionView', () => {
.not.toContain('backdropEffectsMobile');
});

describe('background color presets', () => {
it('offers theme background color presets as swatches', () => {
const entry = createEntry({
themeOptions: {
presets: {
backgroundColors: [{value: '#c9e9fb', name: 'Extra Light Blue'}]
}
},
sections: [{id: 1, configuration: {backdropType: 'color'}}]
});

const view = new EditSectionView({
model: entry.sections.get(1),
entry
});

view.render();

const swatch =
view.$el.find('.color_picker-swatches button[title="Extra Light Blue"]')[0];

expect(swatch).toBeDefined();
expect(swatch.textContent).toBe('#c9e9fb');
});

it('offers colors already used in other sections as swatches', () => {
const entry = createEntry({
sections: [
{id: 1, configuration: {backdropType: 'color'}},
{id: 2, configuration: {backdropType: 'color', backdropColor: '#040404'}}
]
});

const view = new EditSectionView({
model: entry.sections.get(1),
entry
});

view.render();

const swatch =
view.$el.find('.color_picker-swatches button[title="#040404"]')[0];

expect(swatch).toBeDefined();
});

describe('swatch group divider', () => {
it('separates theme presets from used colors with a divider', () => {
const entry = createEntry({
themeOptions: {
presets: {
backgroundColors: [{value: '#c9e9fb', name: 'Extra Light Blue'}]
}
},
sections: [
{id: 1, configuration: {backdropType: 'color'}},
{id: 2, configuration: {backdropType: 'color', backdropColor: '#040404'}}
]
});

const view = new EditSectionView({
model: entry.sections.get(1),
entry
});

view.render();

expect(backdropColorSwatches(view).find('.color_picker-swatch_divider'))
.toHaveLength(1);
});

it('shows no divider when no colors are used yet', () => {
const entry = createEntry({
themeOptions: {
presets: {
backgroundColors: [{value: '#c9e9fb', name: 'Extra Light Blue'}]
}
},
sections: [{id: 1, configuration: {backdropType: 'color'}}]
});

const view = new EditSectionView({
model: entry.sections.get(1),
entry
});

view.render();

expect(backdropColorSwatches(view).find('.color_picker-swatch_divider'))
.toHaveLength(0);
});
});
});

describe('actions dropdown', () => {
useFakeTranslations({
'pageflow_scrolled.editor.section_menu_items.duplicate': 'Duplicate',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,15 @@ export const ScrolledEntry = Entry.extend({
return sortColors([...colors].filter(Boolean));
},

getBackgroundColorPresets() {
const presets = this.scrolledSeed.config.theme.options.presets || {};

return (presets.backgroundColors || []).map(({value, name}) => ({
value,
text: name || value
}));
},

getThemeProperties() {
return this.scrolledSeed.config.theme.options.properties || {};
},
Expand Down
11 changes: 8 additions & 3 deletions entry_types/scrolled/package/src/editor/views/EditSectionView.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export const EditSectionView = EditConfigurationView.extend({
const entry = this.options.entry;
const editor = this.options.editor;

const backgroundColorSwatches = [
...entry.getBackgroundColorPresets().map(swatch => ({...swatch, group: 'presets'})),
...entry.getUsedSectionBackgroundColors().map(value => ({value, group: 'used'}))
];

const editMotifAreaMenuItem = {
name: 'editMotifArea',
label: I18n.t('pageflow_scrolled.editor.edit_motif_area_menu_item'),
Expand Down Expand Up @@ -149,7 +154,7 @@ export const EditSectionView = EditConfigurationView.extend({
this.input('backdropColor', ColorInputView, {
visibleBinding: 'backdropType',
visibleBindingValue: 'color',
swatches: entry.getUsedSectionBackgroundColors()
swatches: backgroundColorSwatches
});
if (hasDecorationEffects(entry)) {
this.input('backdropEffects', EffectListInputView, {
Expand Down Expand Up @@ -212,7 +217,7 @@ export const EditSectionView = EditConfigurationView.extend({
placeholderColorBinding: 'invert',
placeholderColor: invert => invert ? '#101010' : '#ffffff',
placeholderColorDescription: I18n.t('pageflow_scrolled.editor.edit_section.attributes.cardSurfaceColor.auto_color'),
swatches: entry.getUsedSectionBackgroundColors()
swatches: backgroundColorSwatches
});

this.input('splitOverlayColor', ColorInputView, {
Expand All @@ -223,7 +228,7 @@ export const EditSectionView = EditConfigurationView.extend({
placeholderColorBinding: 'invert',
placeholderColor: invert => invert ? '#ffffffb3' : '#000000b3',
placeholderColorDescription: I18n.t('pageflow_scrolled.editor.edit_section.attributes.splitOverlayColor.auto_color'),
swatches: entry.getUsedSectionBackgroundColors()
swatches: backgroundColorSwatches
});

this.input('overlayBackdropBlur', SliderInputView, {
Expand Down
85 changes: 85 additions & 0 deletions package/spec/ui/views/ColorPicker-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,91 @@ describe('ColorPicker', () => {
});
});

describe('labelled swatches', () => {
it('uses object value as swatch color and text as title', () => {
createColorPicker({swatches: [{value: '#aabbcc', text: 'Sky'}]});

var button = picker().querySelector('.color_picker-swatches button');

expect(button.title).toBe('Sky');
expect(button).toHaveStyle('color: #aabbcc');
});

it('falls back to value as title when text is missing', () => {
createColorPicker({swatches: [{value: '#aabbcc'}]});

var button = picker().querySelector('.color_picker-swatches button');

expect(button.title).toBe('#aabbcc');
});

it('applies object swatch value when clicked', () => {
createColorPicker({swatches: [{value: '#aabbcc', text: 'Sky'}]});
open();

picker().querySelector('.color_picker-swatches button')
.dispatchEvent(new Event('click', {bubbles: true}));

expect(input.value).toBe('#aabbcc');
});

it('filters translucent object swatches when alpha is not enabled', () => {
createColorPicker({
swatches: [{value: '#aabbcc', text: 'A'}, {value: '#ff000080', text: 'B'}]
});

expect(picker().querySelectorAll('.color_picker-swatches button')).toHaveLength(1);
});
});

describe('swatch groups', () => {
function dividerCount() {
return picker().querySelectorAll('.color_picker-swatch_divider').length;
}

it('renders a divider between groups of swatches', () => {
createColorPicker({
swatches: [
{value: '#aabbcc', group: 'presets'},
{value: '#112233', group: 'used'}
]
});

expect(dividerCount()).toBe(1);
expect(picker().querySelectorAll('.color_picker-swatches button')).toHaveLength(2);
});

it('keeps swatches that share a group together', () => {
createColorPicker({
swatches: [
{value: '#aabbcc', group: 'presets'},
{value: '#445566', group: 'presets'},
{value: '#112233', group: 'used'}
]
});

expect(dividerCount()).toBe(1);
expect(picker().querySelectorAll('.color_picker-swatches button')).toHaveLength(3);
});

it('renders no divider for plain swatches', () => {
createColorPicker({swatches: ['#aabbcc', '#112233']});

expect(dividerCount()).toBe(0);
expect(picker().querySelectorAll('.color_picker-swatches button')).toHaveLength(2);
});

it('still applies the value of a grouped swatch when clicked', () => {
createColorPicker({swatches: [{value: '#aabbcc', group: 'presets'}]});
open();

picker().querySelector('.color_picker-swatches button')
.dispatchEvent(new Event('click', {bubbles: true}));

expect(input.value).toBe('#aabbcc');
});
});

describe('open and close', () => {
it('opens picker on input click', () => {
createColorPicker();
Expand Down
Loading
Loading