@kitconcept/volto-light-theme 8.0.0-alpha.4 → 8.0.0-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.changelog.draft +8 -3
- package/CHANGELOG.md +15 -0
- package/package.json +2 -2
- package/src/components/Header/Header.tsx +1 -1
- package/src/components/Widgets/ColorSwatch.stories.tsx +147 -0
- package/src/components/Widgets/ColorSwatch.test.tsx +188 -0
- package/src/components/Widgets/ColorSwatch.tsx +77 -39
- package/src/components/Widgets/ThemeColorSwatch.tsx +5 -9
- package/src/config/blocks.tsx +11 -28
- package/src/config/widgets.ts +1 -9
- package/src/helpers/styleDefinitions.test.tsx +30 -0
- package/src/helpers/styleDefinitions.ts +49 -0
- package/src/theme/_widgets.scss +6 -17
- package/src/theme/sticky-menu.scss +6 -4
- package/src/components/Widgets/AlignWidget.tsx +0 -84
- package/src/components/Widgets/BlockAlignment.tsx +0 -88
- package/src/components/Widgets/BlockWidth.tsx +0 -101
- package/src/components/Widgets/Buttons.tsx +0 -167
- package/src/components/Widgets/Size.tsx +0 -78
package/.changelog.draft
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
## 8.0.0-alpha.
|
|
1
|
+
## 8.0.0-alpha.5 (2025-11-11)
|
|
2
|
+
|
|
3
|
+
### Feature
|
|
4
|
+
|
|
5
|
+
- Registry color definitions support for ColorSwatch widget. @sneridagh [#723](https://github.com/kitconcept/volto-light-theme/pull/723)
|
|
2
6
|
|
|
3
7
|
### Bugfix
|
|
4
8
|
|
|
5
|
-
-
|
|
9
|
+
- Fixes text in the sticky menu if the text was long enough to wrap. @jnptk [#691](https://github.com/kitconcept/volto-light-theme/pull/691)
|
|
10
|
+
- Identify intranet header with a className in `header header-intranet` div. @sneridagh
|
|
6
11
|
|
|
7
12
|
### Internal
|
|
8
13
|
|
|
9
|
-
-
|
|
14
|
+
- Normalize all remaining add-ons to be "alpha" versions exclusively for vlt8. @sneridagh
|
|
10
15
|
|
|
11
16
|
|
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,21 @@
|
|
|
8
8
|
|
|
9
9
|
<!-- towncrier release notes start -->
|
|
10
10
|
|
|
11
|
+
## 8.0.0-alpha.5 (2025-11-11)
|
|
12
|
+
|
|
13
|
+
### Feature
|
|
14
|
+
|
|
15
|
+
- Registry color definitions support for ColorSwatch widget. @sneridagh [#723](https://github.com/kitconcept/volto-light-theme/pull/723)
|
|
16
|
+
|
|
17
|
+
### Bugfix
|
|
18
|
+
|
|
19
|
+
- Fixes text in the sticky menu if the text was long enough to wrap. @jnptk [#691](https://github.com/kitconcept/volto-light-theme/pull/691)
|
|
20
|
+
- Identify intranet header with a className in `header header-intranet` div. @sneridagh
|
|
21
|
+
|
|
22
|
+
### Internal
|
|
23
|
+
|
|
24
|
+
- Normalize all remaining add-ons to be "alpha" versions exclusively for vlt8. @sneridagh
|
|
25
|
+
|
|
11
26
|
## 8.0.0-alpha.4 (2025-11-05)
|
|
12
27
|
|
|
13
28
|
### Bugfix
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kitconcept/volto-light-theme",
|
|
3
|
-
"version": "8.0.0-alpha.
|
|
3
|
+
"version": "8.0.0-alpha.5",
|
|
4
4
|
"description": "Volto Light Theme by kitconcept",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"release-it": "^19.0.3",
|
|
46
46
|
"typescript": "^5.7.3",
|
|
47
47
|
"vitest": "^3.1.2",
|
|
48
|
-
"@plone/types": "2.0.0-alpha.
|
|
48
|
+
"@plone/types": "2.0.0-alpha.10"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@dnd-kit/core": "6.0.8",
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { fn } from '@storybook/test';
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
4
|
+
import Wrapper from '@plone/volto/storybook';
|
|
5
|
+
import type { StyleDefinition } from '@plone/types';
|
|
6
|
+
|
|
7
|
+
import ColorSwatch, { type ColorSwatchProps } from './ColorSwatch';
|
|
8
|
+
|
|
9
|
+
const palettes: StyleDefinition[] = [
|
|
10
|
+
{
|
|
11
|
+
name: 'default',
|
|
12
|
+
label: 'Default',
|
|
13
|
+
style: {
|
|
14
|
+
'--theme-color': '#ffffff',
|
|
15
|
+
'--theme-foreground-color': '#1b1c1d',
|
|
16
|
+
'--theme-low-contrast-foreground-color': '#585858',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'warm',
|
|
21
|
+
label: 'Warm',
|
|
22
|
+
style: {
|
|
23
|
+
'--theme-color': '#fff5ed',
|
|
24
|
+
'--theme-foreground-color': '#6b2c1f',
|
|
25
|
+
'--theme-low-contrast-foreground-color': '#b06a54',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'unstyled',
|
|
30
|
+
label: 'No Inline Style',
|
|
31
|
+
style: undefined,
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const themePalettes: StyleDefinition[] = [
|
|
36
|
+
{
|
|
37
|
+
name: 'ocean',
|
|
38
|
+
label: 'Ocean',
|
|
39
|
+
style: {
|
|
40
|
+
'--theme-color': '#dff5ff',
|
|
41
|
+
'--theme-foreground-color': '#003d5b',
|
|
42
|
+
'--theme-low-contrast-foreground-color': '#5c6b73',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'forest',
|
|
47
|
+
label: 'Forest',
|
|
48
|
+
style: {
|
|
49
|
+
'--theme-color': '#e6f4ea',
|
|
50
|
+
'--theme-foreground-color': '#1e4d2b',
|
|
51
|
+
'--theme-low-contrast-foreground-color': '#4b7754',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const meta = {
|
|
57
|
+
title: 'Widgets/ColorSwatch',
|
|
58
|
+
component: ColorSwatch,
|
|
59
|
+
parameters: {
|
|
60
|
+
layout: 'centered',
|
|
61
|
+
},
|
|
62
|
+
decorators: [
|
|
63
|
+
(Story) => (
|
|
64
|
+
<Wrapper>
|
|
65
|
+
<div style={{ width: 320 }}>
|
|
66
|
+
<Story />
|
|
67
|
+
</div>
|
|
68
|
+
</Wrapper>
|
|
69
|
+
),
|
|
70
|
+
],
|
|
71
|
+
tags: ['autodocs'],
|
|
72
|
+
} satisfies Meta<typeof ColorSwatch>;
|
|
73
|
+
|
|
74
|
+
export default meta;
|
|
75
|
+
type Story = StoryObj<typeof meta>;
|
|
76
|
+
|
|
77
|
+
const InteractiveColorSwatch = (args: ColorSwatchProps) => {
|
|
78
|
+
const [value, setValue] = React.useState(args.value || args.default);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<>
|
|
82
|
+
<ColorSwatch
|
|
83
|
+
{...args}
|
|
84
|
+
onChange={(id, selectedValue) => {
|
|
85
|
+
setValue(selectedValue);
|
|
86
|
+
args.onChange?.(id, selectedValue);
|
|
87
|
+
}}
|
|
88
|
+
/>
|
|
89
|
+
<div style={{ marginTop: '10px' }}>
|
|
90
|
+
The selected value is: <strong>{value}</strong>
|
|
91
|
+
</div>
|
|
92
|
+
</>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const DefaultPalette: Story = {
|
|
97
|
+
render: (args) => <InteractiveColorSwatch {...args} />,
|
|
98
|
+
args: {
|
|
99
|
+
id: 'color',
|
|
100
|
+
title: 'Color palette',
|
|
101
|
+
value: 'default',
|
|
102
|
+
colors: palettes,
|
|
103
|
+
onChange: fn(),
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const ThemePalette: Story = {
|
|
108
|
+
render: (args) => <InteractiveColorSwatch {...args} />,
|
|
109
|
+
args: {
|
|
110
|
+
id: 'theme',
|
|
111
|
+
title: 'Theme palette',
|
|
112
|
+
value: 'ocean',
|
|
113
|
+
themes: themePalettes,
|
|
114
|
+
onChange: fn(),
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export const ColorPaletteWithDefault: Story = {
|
|
119
|
+
render: (args) => <InteractiveColorSwatch {...args} />,
|
|
120
|
+
args: {
|
|
121
|
+
id: 'color',
|
|
122
|
+
title: 'Color palette',
|
|
123
|
+
colors: palettes,
|
|
124
|
+
default: 'warm',
|
|
125
|
+
onChange: fn(),
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const ColorPaletteNoValueNoDefault: Story = {
|
|
130
|
+
render: (args) => <InteractiveColorSwatch {...args} />,
|
|
131
|
+
args: {
|
|
132
|
+
id: 'color',
|
|
133
|
+
title: 'Color palette',
|
|
134
|
+
colors: palettes,
|
|
135
|
+
onChange: fn(),
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export const ColorPaletteNoValueNoDefaultWithFallbackToFirst: Story = {
|
|
140
|
+
render: (args) => <InteractiveColorSwatch {...args} />,
|
|
141
|
+
args: {
|
|
142
|
+
id: 'color',
|
|
143
|
+
title: 'Color palette',
|
|
144
|
+
themes: themePalettes,
|
|
145
|
+
onChange: fn(),
|
|
146
|
+
},
|
|
147
|
+
};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import React, { createContext, useContext } from 'react';
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import { render, fireEvent } from '@testing-library/react';
|
|
4
|
+
import ColorSwatch from './ColorSwatch';
|
|
5
|
+
import type { StyleDefinition } from '@plone/types';
|
|
6
|
+
|
|
7
|
+
vi.mock('@plone/volto/components/manage/Widgets/FormFieldWrapper', () => ({
|
|
8
|
+
__esModule: true,
|
|
9
|
+
default: ({ children }: { children: React.ReactNode }) => (
|
|
10
|
+
<div data-testid="form-field-wrapper">{children}</div>
|
|
11
|
+
),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const MockRadioGroupContext = createContext<
|
|
15
|
+
((value: string) => void) | undefined
|
|
16
|
+
>(undefined);
|
|
17
|
+
|
|
18
|
+
vi.mock('@plone/components', () => ({
|
|
19
|
+
RadioGroup: ({
|
|
20
|
+
children,
|
|
21
|
+
onChange,
|
|
22
|
+
...rest
|
|
23
|
+
}: {
|
|
24
|
+
children: React.ReactNode;
|
|
25
|
+
onChange?: (value: string) => void;
|
|
26
|
+
}) => {
|
|
27
|
+
const { isDisabled: _isDisabled, ...passThrough } = rest as Record<
|
|
28
|
+
string,
|
|
29
|
+
unknown
|
|
30
|
+
>;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<MockRadioGroupContext.Provider value={onChange}>
|
|
34
|
+
<div role="radiogroup" {...passThrough}>
|
|
35
|
+
{React.Children.map(children, (child) =>
|
|
36
|
+
React.isValidElement(child)
|
|
37
|
+
? React.cloneElement(child, {
|
|
38
|
+
onSelect: onChange,
|
|
39
|
+
})
|
|
40
|
+
: child,
|
|
41
|
+
)}
|
|
42
|
+
</div>
|
|
43
|
+
</MockRadioGroupContext.Provider>
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
Radio: ({
|
|
47
|
+
value,
|
|
48
|
+
onSelect,
|
|
49
|
+
className,
|
|
50
|
+
children,
|
|
51
|
+
...rest
|
|
52
|
+
}: {
|
|
53
|
+
value: string;
|
|
54
|
+
onSelect?: (value: string) => void;
|
|
55
|
+
className?: string;
|
|
56
|
+
children?: React.ReactNode;
|
|
57
|
+
}) => {
|
|
58
|
+
const groupOnChange = useContext(MockRadioGroupContext);
|
|
59
|
+
return (
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
className={className}
|
|
63
|
+
data-value={value}
|
|
64
|
+
onClick={() => {
|
|
65
|
+
groupOnChange?.(value);
|
|
66
|
+
onSelect?.(value);
|
|
67
|
+
}}
|
|
68
|
+
{...rest}
|
|
69
|
+
>
|
|
70
|
+
{children}
|
|
71
|
+
</button>
|
|
72
|
+
);
|
|
73
|
+
},
|
|
74
|
+
Tooltip: ({ children }: { children: React.ReactNode }) => (
|
|
75
|
+
<span data-testid="tooltip">{children}</span>
|
|
76
|
+
),
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
vi.mock('react-aria-components', () => ({
|
|
80
|
+
Focusable: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
81
|
+
TooltipTrigger: ({ children }: { children: React.ReactNode }) => (
|
|
82
|
+
<>{children}</>
|
|
83
|
+
),
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
const palette: StyleDefinition[] = [
|
|
87
|
+
{
|
|
88
|
+
name: 'default',
|
|
89
|
+
label: 'Default',
|
|
90
|
+
style: { '--theme-color': '#fff' },
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'ocean',
|
|
94
|
+
label: 'Ocean',
|
|
95
|
+
style: { '--theme-color': '#00a0e6' },
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'sunset',
|
|
99
|
+
label: 'Sunset',
|
|
100
|
+
style: { '--theme-color': '#ff6b00' },
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const renderColorSwatch = (
|
|
105
|
+
props: Partial<React.ComponentProps<typeof ColorSwatch>> = {},
|
|
106
|
+
) => {
|
|
107
|
+
const { colors = palette, themes, onChange = vi.fn(), ...rest } = props;
|
|
108
|
+
|
|
109
|
+
const componentProps: React.ComponentProps<typeof ColorSwatch> = {
|
|
110
|
+
id: 'theme',
|
|
111
|
+
label: 'Theme',
|
|
112
|
+
title: 'Theme',
|
|
113
|
+
colors,
|
|
114
|
+
onChange,
|
|
115
|
+
...rest,
|
|
116
|
+
} as React.ComponentProps<typeof ColorSwatch>;
|
|
117
|
+
|
|
118
|
+
if (themes !== undefined) {
|
|
119
|
+
componentProps.themes = themes;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return render(<ColorSwatch {...componentProps} />);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const getActiveClassName = (container: HTMLElement | Document) =>
|
|
126
|
+
container.querySelector('.color-swatch-option-handler.active')?.className ??
|
|
127
|
+
'';
|
|
128
|
+
|
|
129
|
+
describe('ColorSwatch', () => {
|
|
130
|
+
it('renders nothing when no palettes are provided', () => {
|
|
131
|
+
const { container } = renderColorSwatch({ themes: [] });
|
|
132
|
+
expect(container.firstChild).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('highlights the provided value', () => {
|
|
136
|
+
const { container } = renderColorSwatch({ value: 'ocean' });
|
|
137
|
+
expect(getActiveClassName(container)).toContain('ocean');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('highlights the default value when no current value exists', () => {
|
|
141
|
+
const { container } = renderColorSwatch({
|
|
142
|
+
value: undefined,
|
|
143
|
+
default: 'sunset',
|
|
144
|
+
});
|
|
145
|
+
expect(getActiveClassName(container)).toContain('sunset');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('falls back to the "default" palette name when no value or default is provided', () => {
|
|
149
|
+
const { container } = renderColorSwatch({
|
|
150
|
+
value: undefined,
|
|
151
|
+
default: undefined,
|
|
152
|
+
});
|
|
153
|
+
expect(getActiveClassName(container)).toContain('default');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('falls back to the first color when no "default" palette is present', () => {
|
|
157
|
+
const colorsWithoutDefault: StyleDefinition[] = [
|
|
158
|
+
{
|
|
159
|
+
name: 'primary',
|
|
160
|
+
label: 'Primary',
|
|
161
|
+
style: { '--theme-color': '#333' },
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'secondary',
|
|
165
|
+
label: 'Secondary',
|
|
166
|
+
style: { '--theme-color': '#666' },
|
|
167
|
+
},
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
const { container } = renderColorSwatch({
|
|
171
|
+
colors: colorsWithoutDefault,
|
|
172
|
+
value: undefined,
|
|
173
|
+
default: undefined,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
expect(getActiveClassName(container)).toContain('primary');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('notifies when a different swatch is chosen', () => {
|
|
180
|
+
const handleChange = vi.fn();
|
|
181
|
+
const { container } = renderColorSwatch({ onChange: handleChange });
|
|
182
|
+
const buttons = container.querySelectorAll('.color-swatch-option-wrapper');
|
|
183
|
+
|
|
184
|
+
fireEvent.click(buttons[2]);
|
|
185
|
+
|
|
186
|
+
expect(handleChange).toHaveBeenCalledWith('theme', 'sunset');
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -1,58 +1,96 @@
|
|
|
1
1
|
import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper';
|
|
2
|
-
import {
|
|
2
|
+
import { Radio, RadioGroup, Tooltip } from '@plone/components';
|
|
3
3
|
import cx from 'classnames';
|
|
4
|
+
import { Focusable, TooltipTrigger } from 'react-aria-components';
|
|
5
|
+
import type { StyleDefinition } from '@plone/types';
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
| {
|
|
7
|
-
name: string;
|
|
8
|
-
label: string;
|
|
9
|
-
style: Record<`--${string}`, string>;
|
|
10
|
-
}
|
|
11
|
-
| {
|
|
12
|
-
name: string;
|
|
13
|
-
label: string;
|
|
14
|
-
style: undefined;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export type ColorSwatchProps = {
|
|
7
|
+
type BaseColorSwatchProps = {
|
|
18
8
|
id: string;
|
|
19
9
|
title: string;
|
|
10
|
+
label: string;
|
|
20
11
|
value?: string;
|
|
21
|
-
default?: string
|
|
12
|
+
default?: string;
|
|
22
13
|
required?: boolean;
|
|
23
|
-
missing_value?: unknown;
|
|
24
14
|
className?: string;
|
|
25
15
|
onChange: (id: string, value: any) => void;
|
|
26
|
-
|
|
27
|
-
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
isDisabled?: boolean;
|
|
28
18
|
};
|
|
29
19
|
|
|
20
|
+
type ColorsOnly = { colors: StyleDefinition[]; themes?: undefined };
|
|
21
|
+
type ThemesOnly = { themes: StyleDefinition[]; colors?: undefined };
|
|
22
|
+
|
|
23
|
+
export type ColorSwatchProps = BaseColorSwatchProps & (ColorsOnly | ThemesOnly);
|
|
24
|
+
|
|
30
25
|
const ColorSwatch = (props: ColorSwatchProps) => {
|
|
31
|
-
const {
|
|
26
|
+
const {
|
|
27
|
+
id,
|
|
28
|
+
label,
|
|
29
|
+
title,
|
|
30
|
+
value,
|
|
31
|
+
onChange,
|
|
32
|
+
disabled,
|
|
33
|
+
isDisabled,
|
|
34
|
+
default: defaultValue,
|
|
35
|
+
} = props;
|
|
32
36
|
const colors = props.themes || props.colors || [];
|
|
33
37
|
|
|
38
|
+
const selectedColorName = colors.find(({ name }) => name === value)?.name;
|
|
39
|
+
const defaultSelectedColorName =
|
|
40
|
+
!selectedColorName && typeof defaultValue === 'string'
|
|
41
|
+
? colors.find(({ name }) => name === defaultValue)?.name
|
|
42
|
+
: undefined;
|
|
43
|
+
|
|
44
|
+
const fallbackColorName =
|
|
45
|
+
!selectedColorName && !defaultSelectedColorName
|
|
46
|
+
? colors.find(({ name }) => name === 'default')?.name || colors[0]?.name
|
|
47
|
+
: undefined;
|
|
48
|
+
|
|
49
|
+
const radioGroupValueProps: {
|
|
50
|
+
value?: string;
|
|
51
|
+
defaultValue?: string;
|
|
52
|
+
} = selectedColorName
|
|
53
|
+
? { value: selectedColorName }
|
|
54
|
+
: defaultSelectedColorName
|
|
55
|
+
? { defaultValue: defaultSelectedColorName }
|
|
56
|
+
: fallbackColorName
|
|
57
|
+
? { defaultValue: fallbackColorName }
|
|
58
|
+
: {};
|
|
59
|
+
|
|
60
|
+
const currentColorName =
|
|
61
|
+
selectedColorName ?? defaultSelectedColorName ?? fallbackColorName;
|
|
62
|
+
|
|
34
63
|
return colors.length > 0 ? (
|
|
35
64
|
<FormFieldWrapper {...props} className="color-swatch-widget">
|
|
36
|
-
<
|
|
37
|
-
{
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
<RadioGroup
|
|
66
|
+
aria-label={title || label || id}
|
|
67
|
+
orientation="horizontal"
|
|
68
|
+
{...radioGroupValueProps}
|
|
69
|
+
onChange={(value) => onChange(id, value)}
|
|
70
|
+
isDisabled={disabled || isDisabled}
|
|
71
|
+
>
|
|
72
|
+
{colors.map((color) => (
|
|
73
|
+
<Radio
|
|
74
|
+
aria-label={color.label}
|
|
75
|
+
value={color.name}
|
|
76
|
+
className="color-swatch-option-wrapper"
|
|
77
|
+
key={color.name}
|
|
78
|
+
>
|
|
79
|
+
<TooltipTrigger delay={120} closeDelay={80} trigger="hover">
|
|
80
|
+
<Focusable>
|
|
81
|
+
<div
|
|
82
|
+
role="img"
|
|
83
|
+
className={cx('color-swatch-option-handler', color.name, {
|
|
84
|
+
active: currentColorName === color.name,
|
|
85
|
+
})}
|
|
86
|
+
style={color.style}
|
|
87
|
+
></div>
|
|
88
|
+
</Focusable>
|
|
89
|
+
<Tooltip>{color.label}</Tooltip>
|
|
90
|
+
</TooltipTrigger>
|
|
91
|
+
</Radio>
|
|
92
|
+
))}
|
|
93
|
+
</RadioGroup>
|
|
56
94
|
</FormFieldWrapper>
|
|
57
95
|
) : null;
|
|
58
96
|
};
|
|
@@ -2,16 +2,12 @@ import ColorSwatchWidget from './ColorSwatch';
|
|
|
2
2
|
import config from '@plone/volto/registry';
|
|
3
3
|
import type { ColorSwatchProps } from './ColorSwatch';
|
|
4
4
|
|
|
5
|
-
const ThemeColorSwatch = (
|
|
6
|
-
|
|
5
|
+
const ThemeColorSwatch = (
|
|
6
|
+
props: Omit<ColorSwatchProps, 'themes' | 'colors'>,
|
|
7
|
+
) => {
|
|
8
|
+
const themes: ColorSwatchProps['themes'] = config.blocks.themes;
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
(color) => color.name === config.settings.defaultBackgroundColor,
|
|
10
|
-
)?.style;
|
|
11
|
-
|
|
12
|
-
return (
|
|
13
|
-
<ColorSwatchWidget {...props} default={defaultValue} colors={colors} />
|
|
14
|
-
);
|
|
10
|
+
return <ColorSwatchWidget {...props} themes={themes} />;
|
|
15
11
|
};
|
|
16
12
|
|
|
17
13
|
export default ThemeColorSwatch;
|
package/src/config/blocks.tsx
CHANGED
|
@@ -4,8 +4,12 @@ import type { StyleDefinition } from '@plone/types';
|
|
|
4
4
|
import cloneDeep from 'lodash/cloneDeep';
|
|
5
5
|
|
|
6
6
|
import { composeSchema } from '@plone/volto/helpers/Extensions';
|
|
7
|
-
import { findStyleByName } from '@plone/volto/helpers/Blocks/Blocks';
|
|
8
7
|
import { defaultStylingSchema } from '../components/Blocks/schema';
|
|
8
|
+
import {
|
|
9
|
+
blockThemesEnhancer,
|
|
10
|
+
styleDefinitionsEnhancer,
|
|
11
|
+
} from '../helpers/styleDefinitions';
|
|
12
|
+
|
|
9
13
|
import {
|
|
10
14
|
gridTeaserDisableStylingSchema,
|
|
11
15
|
teaserSchemaEnhancer,
|
|
@@ -40,7 +44,6 @@ import { tocBlockSchemaEnhancer } from '../components/Blocks/Toc/schema';
|
|
|
40
44
|
import { mapsBlockSchemaEnhancer } from '../components/Blocks/Maps/schema';
|
|
41
45
|
import { sliderBlockSchemaEnhancer } from '../components/Blocks/Slider/schema';
|
|
42
46
|
import EventMetadataView from '../components/Blocks/EventMetadata/View';
|
|
43
|
-
import isEmpty from 'lodash/isEmpty';
|
|
44
47
|
|
|
45
48
|
import SearchBlockViewEvent from '../components/Blocks/EventCalendar/Search/SearchBlockView';
|
|
46
49
|
import SearchBlockEditEvent from '../components/Blocks/EventCalendar/Search/SearchBlockEdit';
|
|
@@ -142,38 +145,18 @@ export default function install(config: ConfigType) {
|
|
|
142
145
|
},
|
|
143
146
|
];
|
|
144
147
|
|
|
145
|
-
function blockThemesEnhancer({ data, container }) {
|
|
146
|
-
if (!data['@type']) return {};
|
|
147
|
-
const blockConfig = config.blocks.blocksConfig[data['@type']];
|
|
148
|
-
if (!blockConfig) return {};
|
|
149
|
-
const blockStyleDefinitions =
|
|
150
|
-
// We look up for the blockThemes in the block's config, then in the global config
|
|
151
|
-
// We keep `colors` for BBB, but `themes` should be used
|
|
152
|
-
blockConfig.themes || blockConfig.colors || config.blocks.themes || [];
|
|
153
|
-
|
|
154
|
-
if (
|
|
155
|
-
!isEmpty(container) &&
|
|
156
|
-
container.theme &&
|
|
157
|
-
(!data.theme || data.theme === 'default')
|
|
158
|
-
) {
|
|
159
|
-
return findStyleByName(blockStyleDefinitions, container.theme);
|
|
160
|
-
}
|
|
161
|
-
if (data.theme) {
|
|
162
|
-
return data.theme
|
|
163
|
-
? findStyleByName(blockStyleDefinitions, data.theme)
|
|
164
|
-
: {};
|
|
165
|
-
} else {
|
|
166
|
-
// No theme, return default color
|
|
167
|
-
return findStyleByName(config.blocks.themes, 'default');
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
148
|
config.registerUtility({
|
|
172
149
|
name: 'blockThemesEnhancer',
|
|
173
150
|
type: 'styleWrapperStyleObjectEnhancer',
|
|
174
151
|
method: blockThemesEnhancer,
|
|
175
152
|
});
|
|
176
153
|
|
|
154
|
+
config.registerUtility({
|
|
155
|
+
name: 'styleDefinitionsEnhancer',
|
|
156
|
+
type: 'styleWrapperStyleObjectEnhancer',
|
|
157
|
+
method: styleDefinitionsEnhancer,
|
|
158
|
+
});
|
|
159
|
+
|
|
177
160
|
// No required blocks except eventMetadata
|
|
178
161
|
config.blocks.requiredBlocks = [
|
|
179
162
|
...config.blocks.requiredBlocks,
|
package/src/config/widgets.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import type { ConfigType } from '@plone/registry';
|
|
2
|
-
import BlockWidth from '../components/Widgets/BlockWidth';
|
|
3
|
-
import BlockAlignment from '../components/Widgets/BlockAlignment';
|
|
4
2
|
import ColorSwatch from '../components/Widgets/ColorSwatch';
|
|
5
|
-
import Size from '../components/Widgets/Size';
|
|
6
3
|
import ColorPicker from '../components/Widgets/ColorPicker';
|
|
7
4
|
import ThemeColorSwatch from '../components/Widgets/ThemeColorSwatch';
|
|
8
5
|
// import BlocksObject from '../components/Widgets/BlocksObject';
|
|
@@ -12,7 +9,6 @@ import { footerLogosSchema } from '../components/Widgets/schema/footerLogosSchem
|
|
|
12
9
|
import { footerLinksSchema } from '../components/Widgets/schema/footerLinksSchema';
|
|
13
10
|
import { iconLinkListSchema } from '../components/Widgets/schema/iconLinkListSchema';
|
|
14
11
|
import ModalJSONEditor from '../components/Widgets/ModalJSONEditor';
|
|
15
|
-
import AlignWidget from '../components/Widgets/AlignWidget';
|
|
16
12
|
|
|
17
13
|
export default function install(config: ConfigType) {
|
|
18
14
|
// Color picker widget override - use our own non-semanticUI widget
|
|
@@ -20,20 +16,16 @@ export default function install(config: ConfigType) {
|
|
|
20
16
|
// `color_picker` is a terrible name for this widget, it should be `colorSwatch`
|
|
21
17
|
// ToDo: Rename it in Volto 19
|
|
22
18
|
config.widgets.widget.color_picker = ColorSwatch;
|
|
19
|
+
config.widgets.widget.colorSwatch = ColorSwatch;
|
|
23
20
|
|
|
24
21
|
// ObjectList widget override - use our own non-semanticUI widget
|
|
25
22
|
// it uses also dnd-kit for drag and drop
|
|
26
23
|
config.widgets.widget.object_list = ObjectList;
|
|
27
24
|
|
|
28
|
-
config.widgets.widget.align = AlignWidget;
|
|
29
|
-
|
|
30
25
|
// Force Preview image link widget to the image widget
|
|
31
26
|
config.widgets.id.preview_image_link = config.widgets.widget.image;
|
|
32
27
|
|
|
33
|
-
config.widgets.widget.blockWidth = BlockWidth;
|
|
34
|
-
config.widgets.widget.blockAlignment = BlockAlignment;
|
|
35
28
|
config.widgets.widget.colorPicker = ColorPicker;
|
|
36
|
-
config.widgets.widget.size = Size;
|
|
37
29
|
config.widgets.widget.themeColorSwatch = ThemeColorSwatch;
|
|
38
30
|
|
|
39
31
|
config.widgets.widget.modalJSONEditor = ModalJSONEditor;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { styleDefinitionsEnhancer } from './styleDefinitions';
|
|
2
|
+
import config from '@plone/volto/registry';
|
|
3
|
+
|
|
4
|
+
describe('styleDefinitionsEnhancer', () => {
|
|
5
|
+
it('should enhance style definitions correctly', () => {
|
|
6
|
+
const data = {
|
|
7
|
+
styles: {
|
|
8
|
+
backgroundColor: 'red',
|
|
9
|
+
textColor: 'blue',
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
config.registerUtility({
|
|
14
|
+
type: 'styleFieldDefinition',
|
|
15
|
+
name: 'backgroundColor',
|
|
16
|
+
method: () => {
|
|
17
|
+
return [
|
|
18
|
+
{ name: 'red', label: 'Red', style: { '--bg-color': 'red' } },
|
|
19
|
+
{ name: 'green', label: 'Green', style: { '--bg-color': 'green' } },
|
|
20
|
+
];
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const container = {};
|
|
25
|
+
|
|
26
|
+
const result = styleDefinitionsEnhancer({ data, container });
|
|
27
|
+
|
|
28
|
+
expect(result).toEqual({ '--bg-color': 'red' });
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import config from '@plone/volto/registry';
|
|
2
|
+
import { findStyleByName } from '@plone/volto/helpers/Blocks/Blocks';
|
|
3
|
+
import isEmpty from 'lodash/isEmpty';
|
|
4
|
+
|
|
5
|
+
export function blockThemesEnhancer({ data, container }) {
|
|
6
|
+
if (!data['@type']) return {};
|
|
7
|
+
const blockConfig = config.blocks.blocksConfig[data['@type']];
|
|
8
|
+
if (!blockConfig) return {};
|
|
9
|
+
const blockStyleDefinitions =
|
|
10
|
+
// We look up for the blockThemes in the block's config, then in the global config
|
|
11
|
+
// We keep `colors` for BBB, but `themes` should be used
|
|
12
|
+
blockConfig.themes || blockConfig.colors || config.blocks.themes || [];
|
|
13
|
+
|
|
14
|
+
if (
|
|
15
|
+
!isEmpty(container) &&
|
|
16
|
+
container.theme &&
|
|
17
|
+
(!data.theme || data.theme === 'default')
|
|
18
|
+
) {
|
|
19
|
+
return findStyleByName(blockStyleDefinitions, container.theme);
|
|
20
|
+
}
|
|
21
|
+
if (data.theme) {
|
|
22
|
+
return data.theme ? findStyleByName(blockStyleDefinitions, data.theme) : {};
|
|
23
|
+
} else {
|
|
24
|
+
// No theme, return default color
|
|
25
|
+
return findStyleByName(config.blocks.themes, 'default');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function styleDefinitionsEnhancer({ data, container }) {
|
|
30
|
+
let resultantStyles = {};
|
|
31
|
+
Object.keys(data.styles || {}).forEach((fieldName) => {
|
|
32
|
+
const styleFieldEnhancer = config.getUtility({
|
|
33
|
+
type: 'styleFieldDefinition',
|
|
34
|
+
name: fieldName,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (styleFieldEnhancer.method) {
|
|
38
|
+
resultantStyles = {
|
|
39
|
+
...resultantStyles,
|
|
40
|
+
...findStyleByName(
|
|
41
|
+
styleFieldEnhancer.method({ data, container }),
|
|
42
|
+
data.styles[fieldName],
|
|
43
|
+
),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return resultantStyles;
|
|
49
|
+
}
|
package/src/theme/_widgets.scss
CHANGED
|
@@ -89,20 +89,18 @@ span.color-contrast-label {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
.color-swatch-widget {
|
|
92
|
-
.
|
|
93
|
-
|
|
94
|
-
margin: 1rem 0 0.5rem;
|
|
95
|
-
text-align: center;
|
|
92
|
+
.react-aria-RadioGroup {
|
|
93
|
+
gap: 5px;
|
|
96
94
|
}
|
|
97
95
|
|
|
98
|
-
|
|
96
|
+
.color-swatch-option-handler {
|
|
99
97
|
position: relative;
|
|
100
98
|
width: 32px;
|
|
101
99
|
height: 32px;
|
|
102
100
|
padding: 1rem;
|
|
103
101
|
border: 2px solid #ccc;
|
|
104
102
|
border-radius: 100%;
|
|
105
|
-
|
|
103
|
+
cursor: pointer;
|
|
106
104
|
|
|
107
105
|
&.active {
|
|
108
106
|
border: 2px solid red;
|
|
@@ -121,18 +119,9 @@ span.color-contrast-label {
|
|
|
121
119
|
}
|
|
122
120
|
}
|
|
123
121
|
|
|
124
|
-
.color-swatch-widget
|
|
125
|
-
background-color: var(--theme-color);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
.color-swatch-widget .button.grey,
|
|
129
|
-
.color-swatch-widget .button.grey.active,
|
|
130
|
-
.color-swatch-widget .ui.grey.button:hover {
|
|
131
|
-
background-color: var(--theme-color);
|
|
132
|
-
}
|
|
133
|
-
.color-swatch-widget .button.active,
|
|
134
|
-
.color-swatch-widget .button.active:hover {
|
|
122
|
+
.color-swatch-widget .color-swatch-option-handler {
|
|
135
123
|
background-color: var(--theme-color);
|
|
124
|
+
color: var(--theme-foreground-color);
|
|
136
125
|
}
|
|
137
126
|
|
|
138
127
|
.buttons-widget-option {
|
|
@@ -21,7 +21,9 @@ body.cms-ui .sticky-menu {
|
|
|
21
21
|
flex-direction: column;
|
|
22
22
|
padding: 0;
|
|
23
23
|
margin: 0;
|
|
24
|
+
gap: 1px;
|
|
24
25
|
list-style: none;
|
|
26
|
+
text-align: center;
|
|
25
27
|
|
|
26
28
|
li a {
|
|
27
29
|
display: flex;
|
|
@@ -34,24 +36,24 @@ body.cms-ui .sticky-menu {
|
|
|
34
36
|
font-size: 10px;
|
|
35
37
|
font-style: normal;
|
|
36
38
|
font-weight: 700;
|
|
37
|
-
line-height:
|
|
39
|
+
line-height: 12px;
|
|
38
40
|
text-transform: capitalize;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
li {
|
|
42
44
|
display: flex;
|
|
43
|
-
height:
|
|
45
|
+
min-height: 97px;
|
|
44
46
|
flex-direction: column;
|
|
45
47
|
align-items: center;
|
|
46
48
|
justify-content: center;
|
|
47
|
-
|
|
49
|
+
padding: 5px 10px;
|
|
48
50
|
background: var(--sticky-menu-color, #555);
|
|
49
51
|
color: var(--sticky-menu-foreground-color, #fff);
|
|
50
52
|
font-family: Inter;
|
|
51
53
|
font-size: 10px;
|
|
52
54
|
font-style: normal;
|
|
53
55
|
font-weight: 700;
|
|
54
|
-
line-height:
|
|
56
|
+
line-height: 12px;
|
|
55
57
|
text-transform: capitalize;
|
|
56
58
|
}
|
|
57
59
|
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { defineMessages, useIntl } from 'react-intl';
|
|
3
|
-
import ButtonsWidget, {
|
|
4
|
-
type ActionInfo,
|
|
5
|
-
type ButtonsWidgetProps,
|
|
6
|
-
} from './Buttons';
|
|
7
|
-
import imageLeftSVG from '@plone/volto/icons/image-left.svg';
|
|
8
|
-
import imageRightSVG from '@plone/volto/icons/image-right.svg';
|
|
9
|
-
import imageFitSVG from '@plone/volto/icons/image-fit.svg';
|
|
10
|
-
import imageNarrowSVG from '@plone/volto/icons/image-narrow.svg';
|
|
11
|
-
import imageWideSVG from '@plone/volto/icons/image-wide.svg';
|
|
12
|
-
import imageFullSVG from '@plone/volto/icons/image-full.svg';
|
|
13
|
-
import type { IntlShape } from 'react-intl';
|
|
14
|
-
|
|
15
|
-
const messages = defineMessages({
|
|
16
|
-
left: {
|
|
17
|
-
id: 'Left',
|
|
18
|
-
defaultMessage: 'Left',
|
|
19
|
-
},
|
|
20
|
-
right: {
|
|
21
|
-
id: 'Right',
|
|
22
|
-
defaultMessage: 'Right',
|
|
23
|
-
},
|
|
24
|
-
center: {
|
|
25
|
-
id: 'Center',
|
|
26
|
-
defaultMessage: 'Center',
|
|
27
|
-
},
|
|
28
|
-
narrow: {
|
|
29
|
-
id: 'Narrow',
|
|
30
|
-
defaultMessage: 'Narrow',
|
|
31
|
-
},
|
|
32
|
-
wide: {
|
|
33
|
-
id: 'Wide',
|
|
34
|
-
defaultMessage: 'Wide',
|
|
35
|
-
},
|
|
36
|
-
full: {
|
|
37
|
-
id: 'Full',
|
|
38
|
-
defaultMessage: 'Full',
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
export const defaultActionsInfo = ({
|
|
43
|
-
intl,
|
|
44
|
-
}: {
|
|
45
|
-
intl: IntlShape;
|
|
46
|
-
}): Record<string, ActionInfo> => ({
|
|
47
|
-
left: [imageLeftSVG, intl.formatMessage(messages.left)],
|
|
48
|
-
center: [imageFitSVG, intl.formatMessage(messages.center)],
|
|
49
|
-
right: [imageRightSVG, intl.formatMessage(messages.right)],
|
|
50
|
-
narrow: [imageNarrowSVG, intl.formatMessage(messages.narrow)],
|
|
51
|
-
wide: [imageWideSVG, intl.formatMessage(messages.wide)],
|
|
52
|
-
full: [imageFullSVG, intl.formatMessage(messages.full)],
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
type AlignWidgetProps = ButtonsWidgetProps & {
|
|
56
|
-
defaultAction?: string;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const AlignWidget = (props: AlignWidgetProps) => {
|
|
60
|
-
const intl = useIntl();
|
|
61
|
-
|
|
62
|
-
const {
|
|
63
|
-
actions = ['left', 'center', 'right', 'full'],
|
|
64
|
-
actionsInfoMap,
|
|
65
|
-
default: defaultValue,
|
|
66
|
-
defaultAction,
|
|
67
|
-
} = props;
|
|
68
|
-
|
|
69
|
-
const actionsInfo = {
|
|
70
|
-
...defaultActionsInfo({ intl }),
|
|
71
|
-
...actionsInfoMap,
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
return (
|
|
75
|
-
<ButtonsWidget
|
|
76
|
-
{...props}
|
|
77
|
-
actions={actions}
|
|
78
|
-
actionsInfoMap={actionsInfo}
|
|
79
|
-
default={defaultValue ?? defaultAction ?? 'center'}
|
|
80
|
-
/>
|
|
81
|
-
);
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
export default AlignWidget;
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { defineMessages, useIntl } from 'react-intl';
|
|
3
|
-
import ButtonsWidget, {
|
|
4
|
-
type ActionInfo,
|
|
5
|
-
type ButtonsWidgetProps,
|
|
6
|
-
} from './Buttons';
|
|
7
|
-
import imageFitSVG from '@plone/volto/icons/image-fit.svg';
|
|
8
|
-
import imageLeftSVG from '@plone/volto/icons/image-left.svg';
|
|
9
|
-
import imageRightSVG from '@plone/volto/icons/image-right.svg';
|
|
10
|
-
import type { IntlShape } from 'react-intl';
|
|
11
|
-
import type { StyleDefinition } from '@plone/types';
|
|
12
|
-
|
|
13
|
-
const messages = defineMessages({
|
|
14
|
-
left: {
|
|
15
|
-
id: 'Left',
|
|
16
|
-
defaultMessage: 'Left',
|
|
17
|
-
},
|
|
18
|
-
right: {
|
|
19
|
-
id: 'Right',
|
|
20
|
-
defaultMessage: 'Right',
|
|
21
|
-
},
|
|
22
|
-
center: {
|
|
23
|
-
id: 'Center',
|
|
24
|
-
defaultMessage: 'Center',
|
|
25
|
-
},
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
export const defaultActionsInfo = ({
|
|
29
|
-
intl,
|
|
30
|
-
}: {
|
|
31
|
-
intl: IntlShape;
|
|
32
|
-
}): Record<string, ActionInfo> => ({
|
|
33
|
-
left: [imageLeftSVG, intl.formatMessage(messages.left)],
|
|
34
|
-
right: [imageRightSVG, intl.formatMessage(messages.right)],
|
|
35
|
-
center: [imageFitSVG, intl.formatMessage(messages.center)],
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
const DEFAULT_ACTIONS: StyleDefinition[] = [
|
|
39
|
-
{
|
|
40
|
-
style: {
|
|
41
|
-
'--block-alignment': 'var(--align-left)',
|
|
42
|
-
},
|
|
43
|
-
name: 'left',
|
|
44
|
-
label: 'Left',
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
style: {
|
|
48
|
-
'--block-alignment': 'var(--align-center)',
|
|
49
|
-
},
|
|
50
|
-
name: 'center',
|
|
51
|
-
label: 'Center',
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
style: {
|
|
55
|
-
'--block-alignment': 'var(--align-right)',
|
|
56
|
-
},
|
|
57
|
-
name: 'right',
|
|
58
|
-
label: 'Right',
|
|
59
|
-
},
|
|
60
|
-
];
|
|
61
|
-
|
|
62
|
-
const BlockAlignmentWidget = (props: ButtonsWidgetProps) => {
|
|
63
|
-
const intl = useIntl();
|
|
64
|
-
|
|
65
|
-
const { actions = DEFAULT_ACTIONS, actionsInfoMap, filterActions } = props;
|
|
66
|
-
const filteredActions =
|
|
67
|
-
filterActions && filterActions.length > 0
|
|
68
|
-
? actions.filter((action) => {
|
|
69
|
-
const actionName = typeof action === 'string' ? action : action.name;
|
|
70
|
-
return filterActions.includes(actionName);
|
|
71
|
-
})
|
|
72
|
-
: actions;
|
|
73
|
-
|
|
74
|
-
const actionsInfo = {
|
|
75
|
-
...defaultActionsInfo({ intl }),
|
|
76
|
-
...actionsInfoMap,
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
return (
|
|
80
|
-
<ButtonsWidget
|
|
81
|
-
{...props}
|
|
82
|
-
actions={filteredActions}
|
|
83
|
-
actionsInfoMap={actionsInfo}
|
|
84
|
-
/>
|
|
85
|
-
);
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
export default BlockAlignmentWidget;
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { defineMessages, useIntl } from 'react-intl';
|
|
3
|
-
import type { IntlShape } from 'react-intl';
|
|
4
|
-
import ButtonsWidget, {
|
|
5
|
-
type ActionInfo,
|
|
6
|
-
type ButtonsWidgetProps,
|
|
7
|
-
} from './Buttons';
|
|
8
|
-
import imageFitSVG from '@plone/volto/icons/image-fit.svg';
|
|
9
|
-
import imageNarrowSVG from '@plone/volto/icons/image-narrow.svg';
|
|
10
|
-
import imageWideSVG from '@plone/volto/icons/image-wide.svg';
|
|
11
|
-
import imageFullSVG from '@plone/volto/icons/image-full.svg';
|
|
12
|
-
import type { StyleDefinition } from '@plone/types';
|
|
13
|
-
|
|
14
|
-
const messages = defineMessages({
|
|
15
|
-
narrow: {
|
|
16
|
-
id: 'Narrow',
|
|
17
|
-
defaultMessage: 'Narrow',
|
|
18
|
-
},
|
|
19
|
-
default: {
|
|
20
|
-
id: 'Default',
|
|
21
|
-
defaultMessage: 'Default',
|
|
22
|
-
},
|
|
23
|
-
layout: {
|
|
24
|
-
id: 'Layout',
|
|
25
|
-
defaultMessage: 'Layout',
|
|
26
|
-
},
|
|
27
|
-
full: {
|
|
28
|
-
id: 'Full',
|
|
29
|
-
defaultMessage: 'Full',
|
|
30
|
-
},
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
export const defaultActionsInfo = ({
|
|
34
|
-
intl,
|
|
35
|
-
}: {
|
|
36
|
-
intl: IntlShape;
|
|
37
|
-
}): Record<string, ActionInfo> => ({
|
|
38
|
-
narrow: [imageNarrowSVG, intl.formatMessage(messages.narrow)],
|
|
39
|
-
default: [imageFitSVG, intl.formatMessage(messages.default)],
|
|
40
|
-
layout: [imageWideSVG, intl.formatMessage(messages.layout)],
|
|
41
|
-
full: [imageFullSVG, intl.formatMessage(messages.full)],
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
const DEFAULT_ACTIONS: StyleDefinition[] = [
|
|
45
|
-
{
|
|
46
|
-
style: {
|
|
47
|
-
'--block-width': 'var(--narrow-container-width)',
|
|
48
|
-
},
|
|
49
|
-
name: 'narrow',
|
|
50
|
-
label: 'Narrow',
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
style: {
|
|
54
|
-
'--block-width': 'var(--default-container-width)',
|
|
55
|
-
},
|
|
56
|
-
name: 'default',
|
|
57
|
-
label: 'Default',
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
style: {
|
|
61
|
-
'--block-width': 'var(--layout-container-width)',
|
|
62
|
-
},
|
|
63
|
-
name: 'layout',
|
|
64
|
-
label: 'Layout',
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
style: {
|
|
68
|
-
'--block-width': 'unset',
|
|
69
|
-
},
|
|
70
|
-
name: 'full',
|
|
71
|
-
label: 'Full',
|
|
72
|
-
},
|
|
73
|
-
];
|
|
74
|
-
|
|
75
|
-
const BlockWidthWidget = (props: ButtonsWidgetProps) => {
|
|
76
|
-
const intl = useIntl();
|
|
77
|
-
|
|
78
|
-
const { actions = DEFAULT_ACTIONS, actionsInfoMap, filterActions } = props;
|
|
79
|
-
const filteredActions =
|
|
80
|
-
filterActions && filterActions.length > 0
|
|
81
|
-
? actions.filter((action) => {
|
|
82
|
-
const actionName = typeof action === 'string' ? action : action.name;
|
|
83
|
-
return filterActions.includes(actionName);
|
|
84
|
-
})
|
|
85
|
-
: actions;
|
|
86
|
-
|
|
87
|
-
const actionsInfo = {
|
|
88
|
-
...defaultActionsInfo({ intl }),
|
|
89
|
-
...actionsInfoMap,
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
return (
|
|
93
|
-
<ButtonsWidget
|
|
94
|
-
{...props}
|
|
95
|
-
actions={filteredActions}
|
|
96
|
-
actionsInfoMap={actionsInfo}
|
|
97
|
-
/>
|
|
98
|
-
);
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
export default BlockWidthWidget;
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper';
|
|
3
|
-
import Icon from '@plone/volto/components/theme/Icon/Icon';
|
|
4
|
-
import { RadioGroup } from '@plone/components';
|
|
5
|
-
import { Radio } from 'react-aria-components';
|
|
6
|
-
import isEqual from 'lodash/isEqual';
|
|
7
|
-
import type { StyleDefinition } from '@plone/types';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* A tuple that has an icon in the first element and a i18n string in the second.
|
|
11
|
-
*/
|
|
12
|
-
export type ActionInfo = [React.ReactElement<any>, string] | [string, string];
|
|
13
|
-
|
|
14
|
-
type ActionValue = string | Record<`--${string}`, string>;
|
|
15
|
-
|
|
16
|
-
export type ButtonsWidgetProps = {
|
|
17
|
-
/**
|
|
18
|
-
* Unique identifier for the widget.
|
|
19
|
-
*/
|
|
20
|
-
id: string;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Callback function to handle changes.
|
|
24
|
-
*/
|
|
25
|
-
onChange: (id: string, value: ActionValue) => void;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* List of actions available for the widget.
|
|
29
|
-
*/
|
|
30
|
-
actions?: Array<StyleDefinition | string>;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Map containing additional the information (icon and i18n string) for each action.
|
|
34
|
-
*/
|
|
35
|
-
actionsInfoMap?: Record<string, ActionInfo>;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* List of actions to be filtered out. In case that we don't want the default ones
|
|
39
|
-
* we can filter them out.
|
|
40
|
-
*/
|
|
41
|
-
filterActions?: string[];
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Current value of the widget.
|
|
45
|
-
*/
|
|
46
|
-
value?: ActionValue;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Default value of the widget.
|
|
50
|
-
*/
|
|
51
|
-
default?: ActionValue;
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Indicates if the widget is disabled.
|
|
55
|
-
*/
|
|
56
|
-
disabled?: boolean;
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Indicates if the widget is disabled (alternative flag for compatibility reasons).
|
|
60
|
-
*/
|
|
61
|
-
isDisabled?: boolean;
|
|
62
|
-
[key: string]: any;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
type NormalizedAction = {
|
|
66
|
-
name: string;
|
|
67
|
-
value: ActionValue;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const ButtonsWidget = (props: ButtonsWidgetProps) => {
|
|
71
|
-
const {
|
|
72
|
-
disabled,
|
|
73
|
-
id,
|
|
74
|
-
onChange,
|
|
75
|
-
actions = [],
|
|
76
|
-
actionsInfoMap,
|
|
77
|
-
value,
|
|
78
|
-
isDisabled,
|
|
79
|
-
default: defaultValue,
|
|
80
|
-
} = props;
|
|
81
|
-
const normalizedActions = React.useMemo<NormalizedAction[]>(
|
|
82
|
-
() =>
|
|
83
|
-
actions.map((action) =>
|
|
84
|
-
typeof action === 'string'
|
|
85
|
-
? { name: action, value: action }
|
|
86
|
-
: {
|
|
87
|
-
name: action.name,
|
|
88
|
-
value: action.style ?? action.name,
|
|
89
|
-
},
|
|
90
|
-
),
|
|
91
|
-
[actions],
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
const selectedActionName = React.useMemo(
|
|
95
|
-
() =>
|
|
96
|
-
normalizedActions.find((action) => isEqual(value, action.value))?.name,
|
|
97
|
-
[normalizedActions, value],
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
const handleChange = React.useCallback(
|
|
101
|
-
(selectedName: string) => {
|
|
102
|
-
const selectedAction = normalizedActions.find(
|
|
103
|
-
({ name }) => name === selectedName,
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
if (selectedAction) {
|
|
107
|
-
onChange(id, selectedAction.value);
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
[id, normalizedActions, onChange],
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
React.useEffect(() => {
|
|
114
|
-
if (!value && defaultValue) {
|
|
115
|
-
const nextValue =
|
|
116
|
-
typeof defaultValue === 'string'
|
|
117
|
-
? normalizedActions.find(({ name }) => name === defaultValue)
|
|
118
|
-
?.value ?? defaultValue
|
|
119
|
-
: defaultValue;
|
|
120
|
-
|
|
121
|
-
onChange(id, nextValue);
|
|
122
|
-
}
|
|
123
|
-
}, [defaultValue, id, normalizedActions, onChange, value]);
|
|
124
|
-
|
|
125
|
-
return (
|
|
126
|
-
<FormFieldWrapper {...props} className="widget">
|
|
127
|
-
<RadioGroup
|
|
128
|
-
aria-label={props.title || props.label || id}
|
|
129
|
-
orientation="horizontal"
|
|
130
|
-
value={selectedActionName}
|
|
131
|
-
onChange={handleChange}
|
|
132
|
-
isDisabled={disabled || isDisabled}
|
|
133
|
-
className="buttons buttons-widget"
|
|
134
|
-
>
|
|
135
|
-
{normalizedActions.map((action) => {
|
|
136
|
-
const actionInfo = actionsInfoMap?.[action.name];
|
|
137
|
-
const [iconOrText, ariaLabel] = actionInfo ?? [
|
|
138
|
-
action.name,
|
|
139
|
-
action.name,
|
|
140
|
-
];
|
|
141
|
-
|
|
142
|
-
return (
|
|
143
|
-
<Radio
|
|
144
|
-
key={action.name}
|
|
145
|
-
aria-label={ariaLabel}
|
|
146
|
-
value={action.name}
|
|
147
|
-
className="buttons-widget-option"
|
|
148
|
-
>
|
|
149
|
-
{typeof iconOrText === 'string' ? (
|
|
150
|
-
<div className="image-sizes-text">{iconOrText}</div>
|
|
151
|
-
) : (
|
|
152
|
-
<Icon
|
|
153
|
-
name={iconOrText}
|
|
154
|
-
title={ariaLabel || action.name}
|
|
155
|
-
size="24px"
|
|
156
|
-
ariaHidden={true}
|
|
157
|
-
/>
|
|
158
|
-
)}
|
|
159
|
-
</Radio>
|
|
160
|
-
);
|
|
161
|
-
})}
|
|
162
|
-
</RadioGroup>
|
|
163
|
-
</FormFieldWrapper>
|
|
164
|
-
);
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
export default ButtonsWidget;
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { defineMessages, useIntl } from 'react-intl';
|
|
2
|
-
import ButtonsWidget, {
|
|
3
|
-
type ActionInfo,
|
|
4
|
-
type ButtonsWidgetProps,
|
|
5
|
-
} from './Buttons';
|
|
6
|
-
import type { IntlShape } from 'react-intl';
|
|
7
|
-
import type { StyleDefinition } from '@plone/types';
|
|
8
|
-
|
|
9
|
-
const messages = defineMessages({
|
|
10
|
-
s: {
|
|
11
|
-
id: 'Small',
|
|
12
|
-
defaultMessage: 'Small',
|
|
13
|
-
},
|
|
14
|
-
m: {
|
|
15
|
-
id: 'Medium',
|
|
16
|
-
defaultMessage: 'Medium',
|
|
17
|
-
},
|
|
18
|
-
l: {
|
|
19
|
-
id: 'Large',
|
|
20
|
-
defaultMessage: 'Large',
|
|
21
|
-
},
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
export const defaultActionsInfo = ({
|
|
25
|
-
intl,
|
|
26
|
-
}: {
|
|
27
|
-
intl: IntlShape;
|
|
28
|
-
}): Record<string, ActionInfo> => ({
|
|
29
|
-
s: ['S', intl.formatMessage(messages.s)],
|
|
30
|
-
m: ['M', intl.formatMessage(messages.m)],
|
|
31
|
-
l: ['L', intl.formatMessage(messages.l)],
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const DEFAULT_ACTIONS: StyleDefinition[] = [
|
|
35
|
-
{
|
|
36
|
-
name: 's',
|
|
37
|
-
label: 'Small',
|
|
38
|
-
style: undefined,
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
name: 'm',
|
|
42
|
-
label: 'Medium',
|
|
43
|
-
style: undefined,
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
name: 'l',
|
|
47
|
-
label: 'Large',
|
|
48
|
-
style: undefined,
|
|
49
|
-
},
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
const SizeWidget = (props: ButtonsWidgetProps) => {
|
|
53
|
-
const intl = useIntl();
|
|
54
|
-
|
|
55
|
-
const { actions = DEFAULT_ACTIONS, actionsInfoMap, filterActions } = props;
|
|
56
|
-
const filteredActions =
|
|
57
|
-
filterActions && filterActions.length > 0
|
|
58
|
-
? actions.filter((action) => {
|
|
59
|
-
const actionName = typeof action === 'string' ? action : action.name;
|
|
60
|
-
return filterActions.includes(actionName);
|
|
61
|
-
})
|
|
62
|
-
: actions;
|
|
63
|
-
|
|
64
|
-
const actionsInfo = {
|
|
65
|
-
...defaultActionsInfo({ intl }),
|
|
66
|
-
...actionsInfoMap,
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
return (
|
|
70
|
-
<ButtonsWidget
|
|
71
|
-
{...props}
|
|
72
|
-
actions={filteredActions}
|
|
73
|
-
actionsInfoMap={actionsInfo}
|
|
74
|
-
/>
|
|
75
|
-
);
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
export default SizeWidget;
|