@kitconcept/volto-light-theme 7.0.0-alpha.3 → 7.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 CHANGED
@@ -1,8 +1,13 @@
1
- ## 7.0.0-alpha.3 (2025-05-20)
1
+ ## 7.0.0-alpha.5 (2025-06-04)
2
+
3
+ ### Breaking
4
+
5
+ - The `Body` Teaser component has been shadowed, including the `placeholder` in case that the Teaser is empty. @sneridagh
6
+ See [the upgrade guide](https://volto-light-theme.readthedocs.io/how-to-guides/upgrade-guide.html) for more information. [#555](https://github.com/kitconcept/volto-light-theme/pull/555)
2
7
 
3
8
  ### Feature
4
9
 
5
- - Improve Footer color handling. @danalvrz [#551](https://github.com/kitconcept/volto-light-theme/pull/551)
6
- - Added support for a customizable colophon footer. @sneridagh
10
+ - Add support for Person content type in teasers, teasers in grids and listings. @sneridagh [#555](https://github.com/kitconcept/volto-light-theme/pull/555)
11
+ - Added string interpolation {searchTerm} in the custom searchURL in intranet header search widget. @sneridagh
7
12
 
8
13
 
package/CHANGELOG.md CHANGED
@@ -8,6 +8,24 @@
8
8
 
9
9
  <!-- towncrier release notes start -->
10
10
 
11
+ ## 7.0.0-alpha.5 (2025-06-04)
12
+
13
+ ### Breaking
14
+
15
+ - The `Body` Teaser component has been shadowed, including the `placeholder` in case that the Teaser is empty. @sneridagh
16
+ See [the upgrade guide](https://volto-light-theme.readthedocs.io/how-to-guides/upgrade-guide.html) for more information. [#555](https://github.com/kitconcept/volto-light-theme/pull/555)
17
+
18
+ ### Feature
19
+
20
+ - Add support for Person content type in teasers, teasers in grids and listings. @sneridagh [#555](https://github.com/kitconcept/volto-light-theme/pull/555)
21
+ - Added string interpolation {searchTerm} in the custom searchURL in intranet header search widget. @sneridagh
22
+
23
+ ## 7.0.0-alpha.4 (2025-05-22)
24
+
25
+ ### Internal
26
+
27
+ - Added some of the Volto's Cypress tests. @sneridagh [#540](https://github.com/kitconcept/volto-light-theme/pull/540)
28
+
11
29
  ## 7.0.0-alpha.3 (2025-05-20)
12
30
 
13
31
  ### Feature
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitconcept/volto-light-theme",
3
- "version": "7.0.0-alpha.3",
3
+ "version": "7.0.0-alpha.5",
4
4
  "description": "Volto Light Theme by kitconcept",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -26,6 +26,9 @@
26
26
  "publishConfig": {
27
27
  "access": "public"
28
28
  },
29
+ "addons": [
30
+ "@kitconcept/volto-bm3-compat"
31
+ ],
29
32
  "devDependencies": {
30
33
  "@plone/scripts": "^3.6.2",
31
34
  "@storybook/react": "^8.6.12",
@@ -47,6 +50,7 @@
47
50
  "@plone/components": "^3.0.2"
48
51
  },
49
52
  "peerDependencies": {
53
+ "@kitconcept/volto-bm3-compat": "^1.0.0-alpha.1",
50
54
  "@eeacms/volto-accordion-block": "^10.4.6",
51
55
  "@kitconcept/volto-banner-block": "^1.0.1",
52
56
  "@kitconcept/volto-button-block": "^4.0.0-alpha.0",
@@ -5,6 +5,7 @@ import UniversalLink from '@plone/volto/components/manage/UniversalLink/Universa
5
5
  import { flattenToAppURL, isInternalURL } from '@plone/volto/helpers/Url/Url';
6
6
  import config from '@plone/volto/registry';
7
7
  import DefaultSummary from '@kitconcept/volto-light-theme/components/Summary/DefaultSummary';
8
+ import cx from 'classnames';
8
9
 
9
10
  const DefaultTemplate = ({ items, linkTitle, linkHref, isEditMode }) => {
10
11
  let link = null;
@@ -30,7 +31,12 @@ const DefaultTemplate = ({ items, linkTitle, linkHref, isEditMode }) => {
30
31
  dependencies: [item['@type']],
31
32
  }).component || DefaultSummary;
32
33
  return (
33
- <div className="listing-item" key={item['@id']}>
34
+ <div
35
+ className={cx('listing-item', {
36
+ [`${item['@type']?.toLowerCase()}-listing`]: item['@type'],
37
+ })}
38
+ key={item['@id']}
39
+ >
34
40
  <ConditionalLink item={item} condition={!isEditMode}>
35
41
  <div className="listing-body">
36
42
  <Summary item={item} HeadingTag="h2" />
@@ -35,7 +35,7 @@ const Headline = ({
35
35
  );
36
36
  };
37
37
 
38
- const ListingBody = withQuerystringResults((props) => {
38
+ export const ListingBody = (props) => {
39
39
  const {
40
40
  data = {},
41
41
  isEditMode,
@@ -68,11 +68,9 @@ const ListingBody = withQuerystringResults((props) => {
68
68
  ListingBodyTemplate =
69
69
  variation?.template ?? defaultVariation?.template ?? null;
70
70
  }
71
- let galleryRef;
72
71
 
73
- if (data.variation === 'imageGallery') {
74
- galleryRef = useRef();
75
- }
72
+ const galleryRef = useRef();
73
+
76
74
  const listingRef = createRef();
77
75
  const NoResults = variation?.noResultsComponent
78
76
  ? variation.noResultsComponent
@@ -95,7 +93,7 @@ const ListingBody = withQuerystringResults((props) => {
95
93
  <ListingBodyTemplate
96
94
  items={listingItems}
97
95
  isEditMode={isEditMode}
98
- ref={galleryRef}
96
+ ref={data.variation === 'imageGallery' ? galleryRef : undefined}
99
97
  {...data}
100
98
  {...variation}
101
99
  />
@@ -192,6 +190,6 @@ const ListingBody = withQuerystringResults((props) => {
192
190
  )}
193
191
  </>
194
192
  );
195
- });
193
+ };
196
194
 
197
- export default injectIntl(ListingBody);
195
+ export default injectIntl(withQuerystringResults(ListingBody));
@@ -5,6 +5,7 @@ import Component from '@plone/volto/components/theme/Component/Component';
5
5
  import { flattenToAppURL, isInternalURL } from '@plone/volto/helpers/Url/Url';
6
6
  import config from '@plone/volto/registry';
7
7
  import DefaultSummary from '@kitconcept/volto-light-theme/components/Summary/DefaultSummary';
8
+ import cx from 'classnames';
8
9
 
9
10
  const SummaryTemplate = ({ items, linkTitle, linkHref, isEditMode }) => {
10
11
  let link = null;
@@ -46,7 +47,12 @@ const SummaryTemplate = ({ items, linkTitle, linkHref, isEditMode }) => {
46
47
  </>
47
48
  );
48
49
  return (
49
- <div className="listing-item" key={item['@id']}>
50
+ <div
51
+ className={cx('listing-item', {
52
+ [`${item['@type']?.toLowerCase()}-listing`]: item['@type'],
53
+ })}
54
+ key={item['@id']}
55
+ >
50
56
  <ConditionalLink item={item} condition={!isEditMode}>
51
57
  <ItemBodyTemplate item={item} />
52
58
  </ConditionalLink>
@@ -0,0 +1,53 @@
1
+ import DefaultBody from './DefaultBody';
2
+ import { BlockWrapper } from '@kitconcept/volto-bm3-compat';
3
+ import config from '@plone/volto/registry';
4
+ import cx from 'classnames';
5
+ import imageBlockSVG from '@plone/volto/components/manage/Blocks/Image/block-image.svg';
6
+ import isEmpty from 'lodash/isEmpty';
7
+ import { defineMessages, useIntl } from 'react-intl';
8
+
9
+ const messages = defineMessages({
10
+ PleaseChooseContent: {
11
+ id: 'Please choose an existing content as source for this element',
12
+ defaultMessage:
13
+ 'Please choose an existing content as source for this element',
14
+ },
15
+ });
16
+
17
+ const TeaserBody = (props) => {
18
+ const { variation, data, className, isEditMode } = props;
19
+ const intl = useIntl();
20
+
21
+ const hasType = data.href?.[0]?.['@type'];
22
+
23
+ const BodyComponent =
24
+ (config?.getComponent &&
25
+ hasType &&
26
+ config.getComponent({ name: 'Teaser', dependencies: [hasType] })
27
+ .component) ||
28
+ variation?.template ||
29
+ DefaultBody;
30
+
31
+ return (
32
+ <BlockWrapper
33
+ {...props}
34
+ className={cx(
35
+ { [`${hasType?.toLowerCase()}-teaser`]: hasType },
36
+ className,
37
+ )}
38
+ >
39
+ {isEmpty(data.href) && isEditMode ? (
40
+ <div className="ui message">
41
+ <div className="teaser-item placeholder">
42
+ <img src={imageBlockSVG} alt="" />
43
+ <p>{intl.formatMessage(messages.PleaseChooseContent)}</p>
44
+ </div>
45
+ </div>
46
+ ) : (
47
+ <BodyComponent {...props} />
48
+ )}
49
+ </BlockWrapper>
50
+ );
51
+ };
52
+
53
+ export default TeaserBody;
@@ -0,0 +1,45 @@
1
+ import { isInternalURL } from '@plone/volto/helpers/Url/Url';
2
+ import DefaultSummary from '@kitconcept/volto-light-theme/components/Summary/DefaultSummary';
3
+ import Card from '../../../primitives/Card/Card';
4
+ import config from '@plone/volto/registry';
5
+
6
+ const TeaserDefaultTemplate = (props) => {
7
+ const { data, isEditMode } = props;
8
+ const href = data.href?.[0] || {};
9
+ const image = data.preview_image?.[0];
10
+ const url = data.preview_image?.[0]?.['@id'];
11
+
12
+ const Image = config.getComponent('Image').component;
13
+ const Summary =
14
+ config.getComponent({
15
+ name: 'Summary',
16
+ dependencies: [href['@type']],
17
+ }).component || DefaultSummary;
18
+ const { openExternalLinkInNewTab } = config.settings;
19
+ const openLinkInNewTab =
20
+ data.openLinkInNewTab ||
21
+ (openExternalLinkInNewTab && !isInternalURL(href['@id']));
22
+ const { '@id': id, ...filteredData } = data;
23
+
24
+ return (
25
+ <Card
26
+ href={!isEditMode ? href['@id'] : null}
27
+ openLinkInNewTab={openLinkInNewTab}
28
+ >
29
+ <Card.Image
30
+ src={url && !image?.image_field ? url : undefined}
31
+ item={!data.overwrite ? href : { ...href, ...filteredData }}
32
+ image={data.overwrite ? image : undefined}
33
+ imageComponent={Image}
34
+ />
35
+ <Card.Summary>
36
+ <Summary
37
+ item={!data.overwrite ? href : { ...href, ...filteredData }}
38
+ HeadingTag="h2"
39
+ />
40
+ </Card.Summary>
41
+ </Card>
42
+ );
43
+ };
44
+
45
+ export default TeaserDefaultTemplate;
@@ -87,11 +87,11 @@ class IntranetSearchWidget extends Component {
87
87
  : '';
88
88
 
89
89
  if (searchURL) {
90
- window.open(
91
- `${searchURL}?SearchableText=${encodeURIComponent(this.state.text)}${path}`,
92
- '_blank',
93
- 'noopener,noreferrer',
94
- );
90
+ // If searchURL contains {searchTerm}, replace it with the encoded search text
91
+ const externalUrl =
92
+ searchURL.replace('{searchTerm}', encodeURIComponent(this.state.text)) +
93
+ path;
94
+ window.open(externalUrl, '_blank', 'noopener,noreferrer');
95
95
  } else {
96
96
  this.props.history.push(
97
97
  `/search?SearchableText=${encodeURIComponent(this.state.text)}${path}`,
@@ -0,0 +1,73 @@
1
+ import * as React from 'react';
2
+ import Icon from '@plone/volto/components/theme/Icon/Icon';
3
+ import mailSVG from '@plone/volto/icons/email.svg';
4
+ import locationSVG from '@plone/volto/icons/map.svg';
5
+ import phoneSVG from '@plone/volto/icons/mobile.svg';
6
+ import type { DefaultSummaryProps } from './DefaultSummary';
7
+ import { defineMessages, useIntl } from 'react-intl';
8
+
9
+ const messages = defineMessages({
10
+ phone: {
11
+ id: 'Phone',
12
+ defaultMessage: 'Phone',
13
+ },
14
+ email: {
15
+ id: 'eMail',
16
+ defaultMessage: 'eMail',
17
+ },
18
+ room: {
19
+ id: 'Room',
20
+ defaultMessage: 'Room',
21
+ },
22
+ });
23
+
24
+ const PersonSummary = (props: DefaultSummaryProps) => {
25
+ const { item, HeadingTag = 'h3', a11yLabelId, hide_description } = props;
26
+ const intl = useIntl();
27
+
28
+ return (
29
+ <>
30
+ {item?.head_title && <div className="headline">{item.head_title}</div>}
31
+ <HeadingTag className="title" id={a11yLabelId}>
32
+ {item.title ? item.title : item.id}
33
+ </HeadingTag>
34
+ {!hide_description && <p className="description">{item.description}</p>}
35
+
36
+ {item.contact_email && (
37
+ <div className="summary-extra-info">
38
+ <Icon
39
+ title={intl.formatMessage(messages.email)}
40
+ name={mailSVG}
41
+ size="24px"
42
+ />
43
+ <a href={`mailto:${item.contact_email}`}>{item.contact_email}</a>
44
+ </div>
45
+ )}
46
+
47
+ <div className="summary-room-phone">
48
+ {item.contact_room && (
49
+ <div className="summary-extra-info">
50
+ <Icon
51
+ title={intl.formatMessage(messages.room)}
52
+ name={locationSVG}
53
+ size="24px"
54
+ />
55
+ {item.contact_room}
56
+ </div>
57
+ )}
58
+ {item.contact_phone && (
59
+ <div className="summary-extra-info">
60
+ <Icon
61
+ title={intl.formatMessage(messages.phone)}
62
+ name={phoneSVG}
63
+ size="24px"
64
+ />
65
+ {item.contact_phone}
66
+ </div>
67
+ )}
68
+ </div>
69
+ </>
70
+ );
71
+ };
72
+
73
+ export default PersonSummary;
@@ -2,6 +2,7 @@ import type { ConfigType } from '@plone/registry';
2
2
  import NewsItemSummary from '../components/Summary/NewsItemSummary';
3
3
  import EventSummary from '../components/Summary/EventSummary';
4
4
  import FileSummary from '../components/Summary/FileSummary';
5
+ import PersonSummary from '../components/Summary/PersonSummary';
5
6
 
6
7
  export default function install(config: ConfigType) {
7
8
  config.registerComponent({
@@ -19,6 +20,11 @@ export default function install(config: ConfigType) {
19
20
  component: FileSummary,
20
21
  dependencies: ['File'],
21
22
  });
23
+ config.registerComponent({
24
+ name: 'Summary',
25
+ component: PersonSummary,
26
+ dependencies: ['Person'],
27
+ });
22
28
 
23
29
  return config;
24
30
  }
@@ -13,6 +13,10 @@ import { footerLinksSchema } from '../components/Widgets/schema/footerLinksSchem
13
13
  import { iconLinkListSchema } from '../components/Widgets/schema/iconLinkListSchema';
14
14
 
15
15
  declare module '@plone/types' {
16
+ export interface WidgetsConfigById {
17
+ preview_image_link: React.ComponentType;
18
+ }
19
+
16
20
  export interface WidgetsConfigByWidget {
17
21
  themeColorSwatch: typeof ColorSwatch;
18
22
  blockWidth: typeof BlockWidth;
@@ -20,6 +24,7 @@ declare module '@plone/types' {
20
24
  size: typeof Size;
21
25
  colorPicker: typeof ColorPicker;
22
26
  blocksObject: typeof BlocksObject;
27
+ image: React.ComponentType;
23
28
  }
24
29
  }
25
30
 
@@ -0,0 +1,8 @@
1
+ /**
2
+ * OVERRIDE Body.jsx
3
+ * REASON: Better wrap for Teasers.
4
+ */
5
+
6
+ import TeaserBody from '../../../../../../components/Blocks/Teaser/Body';
7
+
8
+ export default TeaserBody;
package/src/index.ts CHANGED
@@ -61,6 +61,14 @@ declare module '@plone/types' {
61
61
  'plonegovbr.socialmedia.settings': CustomInheritBehavior<PloneGobrSocialMediaSettings>;
62
62
  };
63
63
  }
64
+
65
+ export interface ObjectBrowserItem {
66
+ contact_email: string;
67
+ contact_phone: string;
68
+ contact_room: string;
69
+ contact_website: string;
70
+ contact_building: string;
71
+ }
64
72
  }
65
73
 
66
74
  const applyConfig = (config: ConfigType) => {
@@ -5,6 +5,11 @@ import {
5
5
  gridBlockTwo,
6
6
  gridBlockThree,
7
7
  gridBlockFour,
8
+ gridBlockTwoDocumentPerson,
9
+ gridBlockOnePerson,
10
+ gridBlockTwoPerson,
11
+ gridBlockThreePerson,
12
+ gridBlockFourPerson,
8
13
  } from './mocks';
9
14
  import Wrapper from '@plone/volto/storybook';
10
15
  import BlockWrapper from './BlockWrapper';
@@ -154,3 +159,78 @@ export const FourInversed: Story = {
154
159
  data: { ...gridBlockFour, theme: 'grey' },
155
160
  },
156
161
  };
162
+
163
+ export const OnePerson: Story = {
164
+ render: (args) => (
165
+ <Wrapper>
166
+ <div style={{ width: 'var(--default-container-width)' }}>
167
+ <BlockWrapper {...args}>
168
+ <GridBlockView {...args} />
169
+ </BlockWrapper>
170
+ </div>
171
+ </Wrapper>
172
+ ),
173
+ args: {
174
+ data: gridBlockOnePerson,
175
+ },
176
+ };
177
+
178
+ export const TwoDocumentPerson: Story = {
179
+ render: (args) => (
180
+ <Wrapper>
181
+ <div style={{ width: 'var(--default-container-width)' }}>
182
+ <BlockWrapper {...args}>
183
+ <GridBlockView {...args} />
184
+ </BlockWrapper>
185
+ </div>
186
+ </Wrapper>
187
+ ),
188
+ args: {
189
+ data: gridBlockTwoDocumentPerson,
190
+ },
191
+ };
192
+
193
+ export const TwoPerson: Story = {
194
+ render: (args) => (
195
+ <Wrapper>
196
+ <div style={{ width: 'var(--default-container-width)' }}>
197
+ <BlockWrapper {...args}>
198
+ <GridBlockView {...args} />
199
+ </BlockWrapper>
200
+ </div>
201
+ </Wrapper>
202
+ ),
203
+ args: {
204
+ data: gridBlockTwoPerson,
205
+ },
206
+ };
207
+
208
+ export const ThreePerson: Story = {
209
+ render: (args) => (
210
+ <Wrapper>
211
+ <div style={{ width: 'var(--default-container-width)' }}>
212
+ <BlockWrapper {...args}>
213
+ <GridBlockView {...args} />
214
+ </BlockWrapper>
215
+ </div>
216
+ </Wrapper>
217
+ ),
218
+ args: {
219
+ data: gridBlockThreePerson,
220
+ },
221
+ };
222
+
223
+ export const FourPerson: Story = {
224
+ render: (args) => (
225
+ <Wrapper>
226
+ <div style={{ width: 'var(--default-container-width)' }}>
227
+ <BlockWrapper {...args}>
228
+ <GridBlockView {...args} />
229
+ </BlockWrapper>
230
+ </div>
231
+ </Wrapper>
232
+ ),
233
+ args: {
234
+ data: gridBlockFourPerson,
235
+ },
236
+ };
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import { ListingBody } from '../components/Blocks/Listing/ListingBody';
3
+ import SummaryTemplate from '../components/Blocks/Listing/SummaryTemplate';
4
+ import { listingBlockPerson } from './mocks';
5
+ import Wrapper from '@plone/volto/storybook';
6
+ import BlockWrapper from './BlockWrapper';
7
+
8
+ import type { Meta, StoryObj } from '@storybook/react';
9
+
10
+ const meta = {
11
+ title: 'Blocks/Listing',
12
+ component: ListingBody,
13
+ parameters: {
14
+ layout: 'centered',
15
+ },
16
+ tags: ['autodocs'],
17
+ } satisfies Meta<typeof ListingBody>;
18
+
19
+ export default meta;
20
+ type Story = StoryObj<typeof meta>;
21
+
22
+ export const PersonListing: Story = {
23
+ render: (args) => (
24
+ <Wrapper>
25
+ <div style={{ width: 'var(--default-container-width)' }}>
26
+ <BlockWrapper {...args}>
27
+ <div className="block listing">
28
+ <ListingBody {...args} />
29
+ </div>
30
+ </BlockWrapper>
31
+ </div>
32
+ </Wrapper>
33
+ ),
34
+ args: {
35
+ data: listingBlockPerson,
36
+ listingItems: listingBlockPerson.items,
37
+ },
38
+ };
39
+
40
+ export const PersonListingSummary: Story = {
41
+ render: (args) => (
42
+ <Wrapper>
43
+ <div style={{ width: 'var(--default-container-width)' }}>
44
+ <BlockWrapper {...args}>
45
+ <div className="block listing summary">
46
+ <ListingBody {...args} />
47
+ </div>
48
+ </BlockWrapper>
49
+ </div>
50
+ </Wrapper>
51
+ ),
52
+ args: {
53
+ data: {
54
+ ...listingBlockPerson,
55
+ },
56
+ listingItems: listingBlockPerson.items,
57
+ variation: {
58
+ id: 'summary',
59
+ template: SummaryTemplate,
60
+ title: 'List with images',
61
+ },
62
+ },
63
+ };
@@ -1,4 +1,5 @@
1
1
  const demoImage = 'black-starry-night.jpg';
2
+ const personImage = 'person.png';
2
3
 
3
4
  export function ObjectBrowserItem(demoImage) {
4
5
  return {
@@ -169,6 +170,98 @@ export const teaserBlock = {
169
170
  title: 'Headline H2',
170
171
  };
171
172
 
173
+ export const personBlock = {
174
+ '@type': 'teaser',
175
+ href: [
176
+ {
177
+ '@id': '.',
178
+ '@type': 'Person',
179
+ title: 'Kathryn Janeway',
180
+ Title: 'Kathryn Janeway',
181
+ description:
182
+ 'USS Voyager Captain from the 24th century. She is known for her strong leadership skills and her ability to make tough decisions in difficult situations.',
183
+ Description:
184
+ 'USS Voyager Captain from the 24th century. She is known for her strong leadership skills and her ability to make tough decisions in difficult situations.',
185
+ contact_email: 'janeway@ufp.earth.com',
186
+ contact_room: "Captain's Ready Room",
187
+ contact_phone: '123456789',
188
+ contact_building: 'Sapphire Building',
189
+ contact_website: null,
190
+ getRemoteUrl: null,
191
+ hasPreviewImage: true,
192
+ image_field: 'preview_image',
193
+ image_scales: {
194
+ preview_image: [
195
+ {
196
+ 'content-type': 'image/jpeg',
197
+ download: personImage,
198
+ filename: 'black-starry-night.jpg',
199
+ height: 1708,
200
+ scales: {
201
+ great: {
202
+ download: personImage,
203
+ height: 854,
204
+ width: 1200,
205
+ },
206
+ huge: {
207
+ download: personImage,
208
+ height: 1138,
209
+ width: 1600,
210
+ },
211
+ icon: {
212
+ download: personImage,
213
+ height: 22,
214
+ width: 32,
215
+ },
216
+ large: {
217
+ download: personImage,
218
+ height: 569,
219
+ width: 800,
220
+ },
221
+ larger: {
222
+ download: personImage,
223
+ height: 711,
224
+ width: 1000,
225
+ },
226
+ mini: {
227
+ download: personImage,
228
+ height: 142,
229
+ width: 200,
230
+ },
231
+ preview: {
232
+ download: personImage,
233
+ height: 284,
234
+ width: 400,
235
+ },
236
+ teaser: {
237
+ download: personImage,
238
+ height: 427,
239
+ width: 600,
240
+ },
241
+ thumb: {
242
+ download: personImage,
243
+ height: 91,
244
+ width: 128,
245
+ },
246
+ tile: {
247
+ download: personImage,
248
+ height: 45,
249
+ width: 64,
250
+ },
251
+ },
252
+ size: 693013,
253
+ width: 2400,
254
+ },
255
+ ],
256
+ },
257
+ },
258
+ ],
259
+ styles: {
260
+ align: 'left',
261
+ },
262
+ title: 'Headline H2',
263
+ };
264
+
172
265
  export const gridBlockOne = {
173
266
  '@type': 'gridBlock',
174
267
  blocks: {
@@ -502,3 +595,194 @@ export const gridBlock = {
502
595
  backgroundColor: 'grey',
503
596
  },
504
597
  };
598
+
599
+ export const gridBlockTwoDocumentPerson = {
600
+ '@type': 'gridBlock',
601
+ blocks: {
602
+ '281a22fb-c353-4b91-b2bc-88e6a52ed65a': {
603
+ ...teaserBlock,
604
+ },
605
+ 'f34fa7dd-ce59-4f7f-b795-7d7f1e388df1': {
606
+ ...personBlock,
607
+ },
608
+ },
609
+ blocks_layout: {
610
+ items: [
611
+ '281a22fb-c353-4b91-b2bc-88e6a52ed65a',
612
+ 'f34fa7dd-ce59-4f7f-b795-7d7f1e388df1',
613
+ ],
614
+ },
615
+ };
616
+
617
+ export const gridBlockOnePerson = {
618
+ '@type': 'gridBlock',
619
+ blocks: {
620
+ 'f34fa7dd-ce59-4f7f-b795-7d7f1e388df1': {
621
+ ...personBlock,
622
+ },
623
+ },
624
+ blocks_layout: {
625
+ items: ['f34fa7dd-ce59-4f7f-b795-7d7f1e388df1'],
626
+ },
627
+ };
628
+
629
+ export const gridBlockTwoPerson = {
630
+ '@type': 'gridBlock',
631
+ blocks: {
632
+ '281a22fb-c353-4b91-b2bc-88e6a52ed65a': {
633
+ ...personBlock,
634
+ },
635
+ 'f34fa7dd-ce59-4f7f-b795-7d7f1e388df1': {
636
+ ...personBlock,
637
+ },
638
+ },
639
+ blocks_layout: {
640
+ items: [
641
+ '281a22fb-c353-4b91-b2bc-88e6a52ed65a',
642
+ 'f34fa7dd-ce59-4f7f-b795-7d7f1e388df1',
643
+ ],
644
+ },
645
+ };
646
+
647
+ export const gridBlockThreePerson = {
648
+ '@type': 'gridBlock',
649
+ blocks: {
650
+ '281a22fb-c353-4b91-b2bc-88e6a52ed65a': {
651
+ ...personBlock,
652
+ },
653
+ 'f34fa7dd-ce59-4f7f-b795-7d7f1e388df1': {
654
+ ...personBlock,
655
+ },
656
+ 'f34fa7dd-ce59-4f7f-b795-7d7f1e388df2': {
657
+ ...personBlock,
658
+ },
659
+ },
660
+ blocks_layout: {
661
+ items: [
662
+ '281a22fb-c353-4b91-b2bc-88e6a52ed65a',
663
+ 'f34fa7dd-ce59-4f7f-b795-7d7f1e388df1',
664
+ 'f34fa7dd-ce59-4f7f-b795-7d7f1e388df2',
665
+ ],
666
+ },
667
+ };
668
+
669
+ export const gridBlockFourPerson = {
670
+ '@type': 'gridBlock',
671
+ blocks: {
672
+ '281a22fb-c353-4b91-b2bc-88e6a52ed65a': {
673
+ ...personBlock,
674
+ },
675
+ 'f34fa7dd-ce59-4f7f-b795-7d7f1e388df1': {
676
+ ...personBlock,
677
+ },
678
+ 'f34fa7dd-ce59-4f7f-b795-7d7f1e388df2': {
679
+ ...personBlock,
680
+ },
681
+ 'f34fa7dd-ce59-4f7f-b795-7d7f1e388df3': {
682
+ ...personBlock,
683
+ },
684
+ },
685
+ blocks_layout: {
686
+ items: [
687
+ '281a22fb-c353-4b91-b2bc-88e6a52ed65a',
688
+ 'f34fa7dd-ce59-4f7f-b795-7d7f1e388df1',
689
+ 'f34fa7dd-ce59-4f7f-b795-7d7f1e388df2',
690
+ 'f34fa7dd-ce59-4f7f-b795-7d7f1e388df3',
691
+ ],
692
+ },
693
+ };
694
+
695
+ export const listingBlockPerson = {
696
+ '@type': 'listing',
697
+ items: [
698
+ {
699
+ '@id': 'http://localhost:3000/asdasd-asdasdasd',
700
+ '@type': 'Person',
701
+ CreationDate: '2025-05-19T17:23:06+02:00',
702
+ Creator: 'admin',
703
+ Date: '2025-05-22T09:10:50+02:00',
704
+ Description:
705
+ 'USS Voyager Captain from the 24th century. She is known for her strong leadership skills and her ability to make tough decisions in difficult situations.',
706
+ EffectiveDate: 'None',
707
+ ExpirationDate: 'None',
708
+ ModificationDate: '2025-05-22T09:10:50+02:00',
709
+ Subject: [],
710
+ Title: 'Kathryn Janeway',
711
+ Type: 'Person',
712
+ UID: '2b730627efc24a089e00a315735bfe5b',
713
+ cmf_uid: 1,
714
+ contact_building: null,
715
+ contact_email: 'janeway@ufp.earth.com',
716
+ contact_phone: '124312312313',
717
+ contact_room: 'Ready room',
718
+ country: null,
719
+ created: '2025-05-19T15:23:06+00:00',
720
+ description:
721
+ 'USS Voyager Captain from the 24th century. She is known for her strong leadership skills and her ability to make tough decisions in difficult situations.',
722
+ effective: '1969-12-30T22:00:00+00:00',
723
+ end: null,
724
+ exclude_from_nav: false,
725
+ expires: '2499-12-30T22:00:00+00:00',
726
+ getIcon: null,
727
+ getId: 'asdasd-asdasdasd',
728
+ getObjSize: '0 KB',
729
+ getPath: '/Plone/asdasd-asdasdasd',
730
+ getRemoteUrl: null,
731
+ getURL: 'http://localhost:3000/asdasd-asdasdasd',
732
+ hasPreviewImage: null,
733
+ head_title: null,
734
+ id: 'asdasd-asdasdasd',
735
+ image_field: 'preview_image_link',
736
+ image_scales: {
737
+ preview_image_link: [
738
+ {
739
+ base_path: './',
740
+ 'content-type': 'image/png',
741
+ download: personImage,
742
+ filename: 'person.png',
743
+ height: 286,
744
+ scales: {
745
+ icon: {
746
+ download: personImage,
747
+ height: 32,
748
+ width: 24,
749
+ },
750
+ mini: {
751
+ download: personImage,
752
+ height: 260,
753
+ width: 200,
754
+ },
755
+ thumb: {
756
+ download: personImage,
757
+ height: 128,
758
+ width: 98,
759
+ },
760
+ tile: {
761
+ download: personImage,
762
+ height: 64,
763
+ width: 49,
764
+ },
765
+ },
766
+ size: 37757,
767
+ width: 220,
768
+ },
769
+ ],
770
+ },
771
+ is_folderish: true,
772
+ listCreators: ['admin'],
773
+ location: null,
774
+ mime_type: 'text/plain',
775
+ modified: '2025-05-22T07:10:50+00:00',
776
+ nav_title: null,
777
+ portal_type: 'Person',
778
+ review_state: 'private',
779
+ roles: ['member'],
780
+ start: null,
781
+ sync_uid: null,
782
+ title: 'Kathryn Janeway',
783
+ type_title: 'Person',
784
+ username: null,
785
+ },
786
+ ],
787
+ items_total: 1,
788
+ };
Binary file
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
- import TeaserDefaultBody from '@plone/volto/components/manage/Blocks/Teaser/DefaultBody';
3
- import { teaserBlock } from './mocks';
2
+ import TeaserBody from '@plone/volto/components/manage/Blocks/Teaser/Body';
3
+ import { teaserBlock, personBlock } from './mocks';
4
4
  import Wrapper from '@plone/volto/storybook';
5
5
  import BlockWrapper from './BlockWrapper';
6
6
 
@@ -8,12 +8,12 @@ import type { Meta, StoryObj } from '@storybook/react';
8
8
 
9
9
  const meta = {
10
10
  title: 'Blocks/Teaser',
11
- component: TeaserDefaultBody,
11
+ component: TeaserBody,
12
12
  parameters: {
13
13
  layout: 'centered',
14
14
  },
15
15
  tags: ['autodocs'],
16
- } satisfies Meta<typeof TeaserDefaultBody>;
16
+ } satisfies Meta<typeof TeaserBody>;
17
17
 
18
18
  export default meta;
19
19
  type Story = StoryObj<typeof meta>;
@@ -23,7 +23,7 @@ export const Left: Story = {
23
23
  <Wrapper>
24
24
  <div style={{ width: 'var(--default-container-width)' }}>
25
25
  <BlockWrapper {...args}>
26
- <TeaserDefaultBody {...args} />
26
+ <TeaserBody {...args} />
27
27
  </BlockWrapper>
28
28
  </div>
29
29
  </Wrapper>
@@ -38,7 +38,7 @@ export const Right: Story = {
38
38
  <Wrapper>
39
39
  <div style={{ width: 'var(--default-container-width)' }}>
40
40
  <BlockWrapper {...args}>
41
- <TeaserDefaultBody {...args} />
41
+ <TeaserBody {...args} />
42
42
  </BlockWrapper>
43
43
  </div>
44
44
  </Wrapper>
@@ -58,7 +58,7 @@ export const Center: Story = {
58
58
  <Wrapper>
59
59
  <div style={{ width: 'var(--default-container-width)' }}>
60
60
  <BlockWrapper {...args}>
61
- <TeaserDefaultBody {...args} />
61
+ <TeaserBody {...args} />
62
62
  </BlockWrapper>
63
63
  </div>
64
64
  </Wrapper>
@@ -72,3 +72,18 @@ export const Center: Story = {
72
72
  },
73
73
  },
74
74
  };
75
+
76
+ export const PersonTeaser: Story = {
77
+ render: (args) => (
78
+ <Wrapper>
79
+ <div style={{ width: 'var(--default-container-width)' }}>
80
+ <BlockWrapper {...args}>
81
+ <TeaserBody {...args} />
82
+ </BlockWrapper>
83
+ </div>
84
+ </Wrapper>
85
+ ),
86
+ args: {
87
+ data: personBlock,
88
+ },
89
+ };
@@ -316,7 +316,6 @@
316
316
  .colophon {
317
317
  @include add(size, xs);
318
318
  @include add(height, s);
319
- padding-bottom: $spacing-small;
320
319
 
321
320
  a {
322
321
  @include add(size, xs);
@@ -358,6 +358,7 @@ External link removal for all the blocks.
358
358
  .block.teaser.has--align--center,
359
359
  .block.eventMetadata .details-container,
360
360
  .block-editor-teaser .teaser-item.default,
361
+ .block-editor-teaser .card-inner, // deprecate when category is in place
361
362
  .block-editor-slateTable .block.table,
362
363
  .block-editor-highlight .teaser-description-title,
363
364
  .block-editor-toc .table-of-contents {
@@ -460,6 +461,7 @@ body.has-toolbar.has-sidebar .block .ui.basic.button.delete-button {
460
461
  #page-edit,
461
462
  #page-document {
462
463
  img {
464
+ object-fit: cover;
463
465
  object-position: center;
464
466
  }
465
467
  }
@@ -67,7 +67,7 @@
67
67
  }
68
68
  }
69
69
 
70
- // // Left-aligned
70
+ // // Side-aligned
71
71
  .card {
72
72
  .has--align--left:not(.contained) &,
73
73
  .has--align--right:not(.contained) & {
@@ -149,7 +149,6 @@
149
149
  display: flex;
150
150
 
151
151
  .image-wrapper {
152
- // width: 50%;
153
152
  flex: 0 1 var(--card-listing-image-size, 220px);
154
153
  margin-right: 20px;
155
154
 
@@ -160,7 +159,6 @@
160
159
  }
161
160
 
162
161
  .card-summary {
163
- // width: 50%;
164
162
  flex: 1 1 50%;
165
163
  padding: 0;
166
164
  }
@@ -37,6 +37,7 @@
37
37
  @import 'sticky-menu';
38
38
  @import 'card';
39
39
  @import 'insets';
40
+ @import 'person';
40
41
 
41
42
  @import 'temp';
42
43
 
@@ -0,0 +1,123 @@
1
+ .card {
2
+ .person-teaser.has--align--left:not(.contained) &,
3
+ .has--align--left:not(.contained) .person-teaser &,
4
+ .person-teaser.has--align--right:not(.contained) & {
5
+ .card-inner {
6
+ .image-wrapper {
7
+ flex: 0 1 var(--card-listing-image-size, 220px);
8
+ img {
9
+ aspect-ratio: initial !important;
10
+ }
11
+ }
12
+ }
13
+
14
+ .card-summary {
15
+ align-self: center;
16
+
17
+ .description {
18
+ margin-bottom: $spacing-small;
19
+ }
20
+ }
21
+ }
22
+
23
+ .person-teaser & {
24
+ .card-summary {
25
+ .description {
26
+ margin-bottom: $spacing-small;
27
+ }
28
+ }
29
+ }
30
+
31
+ .contained.person-teaser & {
32
+ .image-wrapper {
33
+ display: flex;
34
+ height: 255px;
35
+ justify-content: center;
36
+ aspect-ratio: var(--image-aspect-ratio, 1.7777777778);
37
+
38
+ img {
39
+ max-width: 198px; // 255x198px Roughly 3:4 (enough to cover the 16:9 portrait height)
40
+ height: 100%;
41
+ flex: 0 1 auto;
42
+ aspect-ratio: initial !important;
43
+ object-fit: cover;
44
+ object-position: top center;
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ // One column exception for contained person-teasers
51
+ .one .contained.person-teaser {
52
+ width: calc(var(--default-container-width) / 2);
53
+
54
+ .column & {
55
+ align-items: center;
56
+ }
57
+ }
58
+
59
+ .one .column:has(.contained.person-teaser) {
60
+ align-items: center;
61
+ }
62
+
63
+ // Three column exception for contained person-teasers
64
+ .three .contained.person-teaser,
65
+ .four .contained.person-teaser {
66
+ .summary-room-phone {
67
+ flex-direction: column;
68
+
69
+ .summary-extra-info {
70
+ margin-bottom: 0;
71
+ }
72
+ }
73
+ }
74
+
75
+ // contained person-teasers do not have background color, so we need to set it to --theme-color
76
+ body .block.gridBlock .block.teaser.contained.person-teaser .card-inner {
77
+ background: var(--theme-color);
78
+ }
79
+
80
+ // contained person-teasers do not have background color, so we need to remove the padding
81
+ // to snap to the default container width
82
+ .contained.person-teaser {
83
+ .card-summary {
84
+ padding-right: 0px;
85
+ padding-left: 0px;
86
+ text-align: center;
87
+ }
88
+
89
+ .summary-extra-info,
90
+ .summary-room-phone {
91
+ justify-content: center;
92
+ }
93
+ }
94
+
95
+ .summary-extra-info {
96
+ display: flex;
97
+ align-items: center;
98
+ margin-bottom: $spacing-small;
99
+ }
100
+
101
+ .summary-room-phone {
102
+ display: flex;
103
+ gap: $spacing-small;
104
+ }
105
+
106
+ // Listings (no card)
107
+ .block.listing {
108
+ .listing-item.person-listing {
109
+ &,
110
+ & a {
111
+ align-items: center;
112
+ }
113
+
114
+ h2,
115
+ h3 {
116
+ margin-bottom: $spacing-small !important;
117
+ }
118
+
119
+ img {
120
+ aspect-ratio: initial !important;
121
+ }
122
+ }
123
+ }
@@ -1,81 +0,0 @@
1
- import React from 'react';
2
- import PropTypes from 'prop-types';
3
- import { Message } from 'semantic-ui-react';
4
- import { defineMessages, useIntl } from 'react-intl';
5
- import imageBlockSVG from '@plone/volto/components/manage/Blocks/Image/block-image.svg';
6
- import { isInternalURL } from '@plone/volto/helpers/Url/Url';
7
- import cx from 'classnames';
8
- import config from '@plone/volto/registry';
9
- import DefaultSummary from '@kitconcept/volto-light-theme/components/Summary/DefaultSummary';
10
- import Card from '../../../primitives/Card/Card';
11
- import isEmpty from 'lodash/isEmpty';
12
-
13
- const messages = defineMessages({
14
- PleaseChooseContent: {
15
- id: 'Please choose an existing content as source for this element',
16
- defaultMessage:
17
- 'Please choose an existing content as source for this element',
18
- },
19
- });
20
-
21
- const TeaserDefaultTemplate = (props) => {
22
- const { className, data, isEditMode, style } = props;
23
- const intl = useIntl();
24
- const href = data.href?.[0] || {};
25
- const image = data.preview_image?.[0];
26
- const url = data.preview_image?.[0]?.['@id'];
27
-
28
- const Image = config.getComponent('Image').component;
29
- const Summary =
30
- config.getComponent({
31
- name: 'Summary',
32
- dependencies: [href['@type']],
33
- }).component || DefaultSummary;
34
- const { openExternalLinkInNewTab } = config.settings;
35
- const openLinkInNewTab =
36
- data.openLinkInNewTab ||
37
- (openExternalLinkInNewTab && !isInternalURL(href['@id']))
38
- ? '_blank'
39
- : null;
40
- const { '@id': id, ...filteredData } = data;
41
-
42
- return (
43
- <div className={cx('block teaser', className)} style={style}>
44
- <>
45
- {isEmpty(href) && isEditMode ? (
46
- <Message>
47
- <div className="teaser-item placeholder">
48
- <img src={imageBlockSVG} alt="" />
49
- <p>{intl.formatMessage(messages.PleaseChooseContent)}</p>
50
- </div>
51
- </Message>
52
- ) : (
53
- <Card
54
- href={!isEditMode ? href['@id'] : null}
55
- openLinkInNewTab={openLinkInNewTab}
56
- >
57
- <Card.Image
58
- src={url && !image?.image_field ? url : undefined}
59
- item={!data.overwrite ? href : { ...href, ...filteredData }}
60
- image={data.overwrite ? image : undefined}
61
- imageComponent={Image}
62
- />
63
- <Card.Summary>
64
- <Summary
65
- item={!data.overwrite ? href : { ...href, ...filteredData }}
66
- HeadingTag="h2"
67
- />
68
- </Card.Summary>
69
- </Card>
70
- )}
71
- </>
72
- </div>
73
- );
74
- };
75
-
76
- TeaserDefaultTemplate.propTypes = {
77
- data: PropTypes.objectOf(PropTypes.any).isRequired,
78
- isEditMode: PropTypes.bool,
79
- };
80
-
81
- export default TeaserDefaultTemplate;