@kitconcept/volto-light-theme 6.0.0 → 7.0.0-alpha.0

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,3 +1,12 @@
1
- ## 6.0.0 (2025-05-14)
1
+ ## 7.0.0-alpha.0 (2025-05-16)
2
+
3
+ ### Breaking
4
+
5
+ - The new card primitive has been applied to all Teasers. @sneridagh
6
+ See upgrade guide for more information. [#537](https://github.com/kitconcept/volto-light-theme/pull/537)
7
+
8
+ ### Feature
9
+
10
+ - Added new card primitive. @sneridagh [#537](https://github.com/kitconcept/volto-light-theme/pull/537)
2
11
 
3
12
 
package/CHANGELOG.md CHANGED
@@ -8,6 +8,23 @@
8
8
 
9
9
  <!-- towncrier release notes start -->
10
10
 
11
+ ## 7.0.0-alpha.0 (2025-05-16)
12
+
13
+ ### Breaking
14
+
15
+ - The new card primitive has been applied to all Teasers. @sneridagh
16
+ See upgrade guide for more information. [#537](https://github.com/kitconcept/volto-light-theme/pull/537)
17
+
18
+ ### Feature
19
+
20
+ - Added new card primitive. @sneridagh [#537](https://github.com/kitconcept/volto-light-theme/pull/537)
21
+
22
+ ## 6.0.1 (2025-05-15)
23
+
24
+ ### Bugfix
25
+
26
+ - Removed background color specification in the whole footer. We only want it in some areas of it, and depending if some sections are present or not. @sneridagh
27
+
11
28
  ## 6.0.0 (2025-05-14)
12
29
 
13
30
  ## 6.0.0-alpha.25 (2025-05-14)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitconcept/volto-light-theme",
3
- "version": "6.0.0",
3
+ "version": "7.0.0-alpha.0",
4
4
  "description": "Volto Light Theme by kitconcept",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -28,6 +28,7 @@
28
28
  },
29
29
  "devDependencies": {
30
30
  "@plone/scripts": "^3.6.2",
31
+ "@storybook/react": "^8.6.12",
31
32
  "@types/jest": "^29.5.8",
32
33
  "@types/lodash": "^4.14.201",
33
34
  "@types/react": "^18.3.12",
@@ -4,11 +4,11 @@ import { Message } from 'semantic-ui-react';
4
4
  import { defineMessages, useIntl } from 'react-intl';
5
5
  import imageBlockSVG from '@plone/volto/components/manage/Blocks/Image/block-image.svg';
6
6
  import { isInternalURL } from '@plone/volto/helpers/Url/Url';
7
- import MaybeWrap from '@plone/volto/components/manage/MaybeWrap/MaybeWrap';
8
- import UniversalLink from '@plone/volto/components/manage/UniversalLink/UniversalLink';
9
7
  import cx from 'classnames';
10
8
  import config from '@plone/volto/registry';
11
9
  import DefaultSummary from '@kitconcept/volto-light-theme/components/Summary/DefaultSummary';
10
+ import Card from '../../Card/Card';
11
+ import isEmpty from 'lodash/isEmpty';
12
12
 
13
13
  const messages = defineMessages({
14
14
  PleaseChooseContent: {
@@ -32,11 +32,17 @@ const TeaserDefaultTemplate = (props) => {
32
32
  dependencies: [href['@type']],
33
33
  }).component || DefaultSummary;
34
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;
35
41
 
36
42
  return (
37
43
  <div className={cx('block teaser', className)} style={style}>
38
44
  <>
39
- {!href && isEditMode && (
45
+ {isEmpty(href) && isEditMode && (
40
46
  <Message>
41
47
  <div className="teaser-item placeholder">
42
48
  <img src={imageBlockSVG} alt="" />
@@ -44,42 +50,23 @@ const TeaserDefaultTemplate = (props) => {
44
50
  </div>
45
51
  </Message>
46
52
  )}
47
- {href && (
48
- <MaybeWrap
49
- condition={!isEditMode}
50
- as={UniversalLink}
51
- href={href['@id']}
52
- target={
53
- data.openLinkInNewTab ||
54
- (openExternalLinkInNewTab && !isInternalURL(href['@id']))
55
- ? '_blank'
56
- : null
57
- }
58
- >
59
- <div className="teaser-item default">
60
- {url && !image?.image_field ? (
61
- <div className="image-wrapper">
62
- <Image src={url} alt="" loading="lazy" responsive={true} />
63
- </div>
64
- ) : (
65
- (href.hasPreviewImage || href.image_field || image) && (
66
- <div className="image-wrapper">
67
- <Image
68
- item={image || href}
69
- imageField={image ? image.image_field : href.image_field}
70
- alt=""
71
- loading="lazy"
72
- responsive={true}
73
- />
74
- </div>
75
- )
76
- )}
77
- <div className="content">
78
- <Summary item={{ ...href, ...data }} HeadingTag="h2" />
79
- </div>
80
- </div>
81
- </MaybeWrap>
82
- )}
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>
83
70
  </>
84
71
  </div>
85
72
  );
@@ -0,0 +1,251 @@
1
+ import React from 'react';
2
+ import Card from './Card';
3
+
4
+ import type { Meta, StoryObj } from '@storybook/react';
5
+ import { ObjectBrowserItem } from '../../stories/mocks';
6
+ import Wrapper from '@plone/volto/storybook';
7
+ import { Button } from '@plone/components';
8
+ import DefaultSummary from '../Summary/DefaultSummary';
9
+
10
+ const meta = {
11
+ title: 'Primitives/Card',
12
+ component: Card,
13
+ parameters: {
14
+ layout: 'centered',
15
+ // backgrounds: { disable: true },
16
+ },
17
+ decorators: [
18
+ (Story) => (
19
+ <Wrapper>
20
+ <Story />
21
+ </Wrapper>
22
+ ),
23
+ ],
24
+ tags: ['autodocs'],
25
+ } satisfies Meta<typeof Card>;
26
+
27
+ export default meta;
28
+ type Story = StoryObj<typeof meta>;
29
+
30
+ const item = {
31
+ title: 'Card Title',
32
+ description:
33
+ 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea.',
34
+ head_title: 'Card Kicker',
35
+ };
36
+ const imageSRC = 'black-starry-night.jpg';
37
+ const imageSRC2 = 'image-light.jpg';
38
+
39
+ export const Simple: Story = {
40
+ render: (args) => (
41
+ <Card href={args.href}>
42
+ <Card.Image src={imageSRC} />
43
+ <Card.Summary>
44
+ <DefaultSummary item={item} HeadingTag="h2" />
45
+ </Card.Summary>
46
+ </Card>
47
+ ),
48
+ args: {
49
+ href: '/folder/page',
50
+ },
51
+ decorators: [
52
+ (Story) => (
53
+ <div style={{ width: '300px' }}>
54
+ <Story />
55
+ </div>
56
+ ),
57
+ ],
58
+ };
59
+
60
+ export const SimpleWithoutLink: Story = {
61
+ render: (args) => (
62
+ <Card>
63
+ <Card.Image src={imageSRC} />
64
+ <Card.Summary>
65
+ <DefaultSummary item={item} HeadingTag="h2" />
66
+ </Card.Summary>
67
+ </Card>
68
+ ),
69
+ args: {
70
+ href: '/folder/page',
71
+ },
72
+ decorators: [
73
+ (Story) => (
74
+ <div style={{ width: '300px' }}>
75
+ <Story />
76
+ </div>
77
+ ),
78
+ ],
79
+ };
80
+
81
+ export const AlignedLeft: Story = {
82
+ render: (args) => (
83
+ <div
84
+ className="has--align--left"
85
+ style={{ width: 'var(--default-container-width)' }}
86
+ >
87
+ <Card href={args.href}>
88
+ <Card.Image src={imageSRC} />
89
+ <Card.Summary>
90
+ <DefaultSummary item={item} HeadingTag="h2" />
91
+ </Card.Summary>
92
+ </Card>
93
+ </div>
94
+ ),
95
+ args: {
96
+ href: '/folder/page',
97
+ },
98
+ };
99
+
100
+ export const AlignedRight: Story = {
101
+ render: (args) => (
102
+ <div
103
+ className="has--align--right"
104
+ style={{ width: 'var(--default-container-width)' }}
105
+ >
106
+ <Card href={args.href}>
107
+ <Card.Image src={imageSRC} />
108
+ <Card.Summary>
109
+ <DefaultSummary item={item} HeadingTag="h2" />
110
+ </Card.Summary>
111
+ </Card>
112
+ </div>
113
+ ),
114
+ args: {
115
+ href: '/folder/page',
116
+ },
117
+ };
118
+
119
+ export const SimpleContained: Story = {
120
+ render: (args) => (
121
+ <div
122
+ className="contained"
123
+ style={{ ['--theme-high-contrast-color' as string]: '#ecebeb' }}
124
+ >
125
+ <Card href={args.href}>
126
+ <Card.Image src={imageSRC} />
127
+ <Card.Summary>
128
+ <DefaultSummary item={item} HeadingTag="h2" />
129
+ </Card.Summary>
130
+ </Card>
131
+ </div>
132
+ ),
133
+ args: {
134
+ href: '/folder/page',
135
+ },
136
+ decorators: [
137
+ (Story) => (
138
+ <div style={{ width: '300px' }}>
139
+ <Story />
140
+ </div>
141
+ ),
142
+ ],
143
+ };
144
+
145
+ export const SimpleListing: Story = {
146
+ render: (args) => (
147
+ <div
148
+ className="card-listing"
149
+ style={{ width: 'var(--default-container-width)' }}
150
+ >
151
+ <Card href={args.href}>
152
+ <Card.Image src={imageSRC} />
153
+ <Card.Summary>
154
+ <DefaultSummary item={item} HeadingTag="h2" />
155
+ </Card.Summary>
156
+ </Card>
157
+ </div>
158
+ ),
159
+ args: {
160
+ href: '/folder/page',
161
+ },
162
+ };
163
+
164
+ export const CustomImage: Story = {
165
+ render: (args) => (
166
+ <div
167
+ className="card-listing"
168
+ style={{ width: 'var(--default-container-width)' }}
169
+ >
170
+ <Card href={args.href}>
171
+ <Card.Image>
172
+ <div className="date-inset">
173
+ <div className="day">10</div>
174
+ <div className="month">May 2025</div>
175
+ </div>
176
+ </Card.Image>
177
+ <Card.Summary>
178
+ <DefaultSummary item={item} HeadingTag="h2" />
179
+ </Card.Summary>
180
+ </Card>
181
+ </div>
182
+ ),
183
+ args: {
184
+ href: '/folder/page',
185
+ },
186
+ };
187
+
188
+ export const CustomAction: Story = {
189
+ render: (args) => (
190
+ <div style={{ width: 'var(--default-container-width)' }}>
191
+ <Card href={args.href}>
192
+ <Card.Image src={imageSRC} />
193
+ <Card.Summary>
194
+ <DefaultSummary item={item} HeadingTag="h2" />
195
+ </Card.Summary>
196
+ <Card.Actions>
197
+ <div style={{ marginTop: '20px' }}>
198
+ <a href="https://www.plone.org" rel="noreferrer" target="_blank">
199
+ <Button>Read More</Button>
200
+ </a>
201
+ </div>
202
+ </Card.Actions>
203
+ </Card>
204
+ </div>
205
+ ),
206
+ args: {
207
+ href: '/folder/page',
208
+ },
209
+ };
210
+
211
+ export const WithItem: Story = {
212
+ render: (args) => (
213
+ <Card href={args.href}>
214
+ <Card.Image src={imageSRC} />
215
+ <Card.Summary>
216
+ <DefaultSummary item={ObjectBrowserItem(imageSRC)} HeadingTag="h2" />
217
+ </Card.Summary>
218
+ </Card>
219
+ ),
220
+ args: {
221
+ href: ObjectBrowserItem(imageSRC)['@id'],
222
+ },
223
+ decorators: [
224
+ (Story) => (
225
+ <div style={{ width: '300px' }}>
226
+ <Story />
227
+ </div>
228
+ ),
229
+ ],
230
+ };
231
+
232
+ export const WithCustomImage: Story = {
233
+ render: (args) => (
234
+ <Card href={args.href}>
235
+ <Card.Image image={ObjectBrowserItem(imageSRC2)} />
236
+ <Card.Summary>
237
+ <DefaultSummary item={ObjectBrowserItem(imageSRC)} HeadingTag="h2" />
238
+ </Card.Summary>
239
+ </Card>
240
+ ),
241
+ args: {
242
+ href: ObjectBrowserItem(imageSRC)['@id'],
243
+ },
244
+ decorators: [
245
+ (Story) => (
246
+ <div style={{ width: '300px' }}>
247
+ <Story />
248
+ </div>
249
+ ),
250
+ ],
251
+ };
@@ -0,0 +1,143 @@
1
+ import * as React from 'react';
2
+ import ConditionalLink from '@plone/volto/components/manage/ConditionalLink/ConditionalLink';
3
+ import cx from 'classnames';
4
+ import type { ObjectBrowserItem } from '@plone/types';
5
+
6
+ type CardProps = {
7
+ /** Optional additional CSS class names to apply to the card. */
8
+ className?: string;
9
+ /** Optional URL to make the card clickable as a link. */
10
+ href?: string;
11
+ /** If true and `href` is provided, opens the link in a new browser tab. */
12
+ openLinkInNewTab?: boolean;
13
+ children?: React.ReactNode;
14
+ };
15
+
16
+ const DefaultImage = (props: any) => {
17
+ const { src, item, imageField, alt, loading, responsive } = props;
18
+ return (
19
+ <img
20
+ src={src || item?.image_scales?.[imageField]?.[0].download}
21
+ alt={alt}
22
+ loading={loading}
23
+ className={responsive ? 'responsive' : ''}
24
+ />
25
+ );
26
+ };
27
+
28
+ const childrenWithProps = (children, extraProps) => {
29
+ return React.Children.map(children, (child) => {
30
+ if (React.isValidElement(child)) {
31
+ return React.cloneElement(child, extraProps);
32
+ }
33
+ return child;
34
+ });
35
+ };
36
+
37
+ const Card = (props: CardProps) => {
38
+ const { className, href, openLinkInNewTab } = props;
39
+
40
+ const a11yLabelId = React.useId();
41
+ const linkRef = React.useRef<HTMLAnchorElement>(null);
42
+
43
+ const triggerNavigation = () => {
44
+ // Only navigate if there is *no* text selection
45
+ const hasSelection = !!window.getSelection()?.toString();
46
+ if (!hasSelection) {
47
+ linkRef.current?.click();
48
+ }
49
+ };
50
+
51
+ const onClick: React.MouseEventHandler<HTMLDivElement> = () => {
52
+ if (href) triggerNavigation();
53
+ };
54
+
55
+ const onKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (e) => {
56
+ if (!href) return;
57
+ if (e.key === 'Enter' || e.key === ' ') {
58
+ e.preventDefault();
59
+ triggerNavigation();
60
+ }
61
+ };
62
+
63
+ return (
64
+ <div
65
+ className={cx('card', className)}
66
+ onClick={onClick}
67
+ onKeyDown={onKeyDown}
68
+ role={href ? 'link' : undefined}
69
+ tabIndex={href ? 0 : undefined}
70
+ >
71
+ {/* @ts-expect-error since this has no children, should fail */}
72
+ <ConditionalLink
73
+ aria-labelledby={a11yLabelId}
74
+ condition={!!href}
75
+ href={href}
76
+ openLinkInNewTab={openLinkInNewTab}
77
+ ref={linkRef}
78
+ />
79
+ <div className="card-inner">
80
+ {childrenWithProps(props.children, { a11yLabelId })}
81
+ </div>
82
+ </div>
83
+ );
84
+ };
85
+
86
+ type CardImageProps = {
87
+ /** The source URL of the image to display. */
88
+ src?: string;
89
+ /** An optional item object, used to provide image data from the current item. */
90
+ item?: Partial<ObjectBrowserItem>;
91
+ /** An optional image object, used as an alternative source of image data for the item. */
92
+ image?: Partial<ObjectBrowserItem>;
93
+ /** A custom React component to render the image. */
94
+ imageComponent?: React.ComponentType<any>;
95
+ children?: React.ReactNode;
96
+ };
97
+
98
+ const CardImage = (props: CardImageProps) => {
99
+ const { src, item, image, imageComponent } = props;
100
+ const Image = imageComponent || DefaultImage;
101
+
102
+ return (
103
+ <div className="image-wrapper">
104
+ {src ? (
105
+ <Image src={src} alt="" loading="lazy" responsive={true} />
106
+ ) : item || image ? (
107
+ (item?.hasPreviewImage || item?.image_field || image) && (
108
+ <Image
109
+ item={image || item}
110
+ imageField={image ? image.image_field : item?.image_field}
111
+ alt=""
112
+ loading="lazy"
113
+ responsive={true}
114
+ />
115
+ )
116
+ ) : (
117
+ props.children
118
+ )}
119
+ </div>
120
+ );
121
+ };
122
+
123
+ type CardSummaryProps = {
124
+ /** The ID of the element that labels the card. */
125
+ a11yLabelId?: string;
126
+ children?: React.ReactNode;
127
+ };
128
+
129
+ const CardSummary = (props: CardSummaryProps) => (
130
+ <div className="card-summary">
131
+ {childrenWithProps(props.children, { a11yLabelId: props.a11yLabelId })}
132
+ </div>
133
+ );
134
+
135
+ const CardActions = (props: any) => (
136
+ <div className="actions-wrapper">{props.children}</div>
137
+ );
138
+
139
+ Card.Image = CardImage;
140
+ Card.Summary = CardSummary;
141
+ Card.Actions = CardActions;
142
+
143
+ export default Card;
@@ -0,0 +1,24 @@
1
+ import * as React from 'react';
2
+ import type { ObjectBrowserItem } from '@plone/types';
3
+
4
+ export type DefaultSummaryProps = {
5
+ item: Partial<ObjectBrowserItem>;
6
+ HeadingTag?: React.ElementType;
7
+ a11yLabelId?: string;
8
+ hide_description?: boolean;
9
+ };
10
+
11
+ const DefaultSummary = (props: DefaultSummaryProps) => {
12
+ const { item, HeadingTag = 'h3', a11yLabelId, hide_description } = props;
13
+ return (
14
+ <>
15
+ {item?.head_title && <div className="headline">{item.head_title}</div>}
16
+ <HeadingTag className="title" id={a11yLabelId}>
17
+ {item.title ? item.title : item.id}
18
+ </HeadingTag>
19
+ {!hide_description && <p className="description">{item.description}</p>}
20
+ </>
21
+ );
22
+ };
23
+
24
+ export default DefaultSummary;
@@ -3,9 +3,10 @@ import {
3
3
  formatDateRange,
4
4
  } from '@kitconcept/volto-light-theme/helpers/dates';
5
5
  import FormattedDate from '@plone/volto/components/theme/FormattedDate/FormattedDate';
6
+ import type { DefaultSummaryProps } from './DefaultSummary';
6
7
 
7
- const EventSummary = (props) => {
8
- const { item, HeadingTag = 'h3' } = props;
8
+ const EventSummary = (props: DefaultSummaryProps) => {
9
+ const { item, HeadingTag = 'h3', hide_description } = props;
9
10
  const start = parseDateFromCatalog(item.start);
10
11
  const end = parseDateFromCatalog(item.end);
11
12
  const headline = [
@@ -14,6 +15,7 @@ const EventSummary = (props) => {
14
15
  {formatDateRange({ start, end, locale: item.Language })}
15
16
  </span>
16
17
  ) : start ? (
18
+ // @ts-expect-error
17
19
  <FormattedDate key="day" date={start} />
18
20
  ) : null,
19
21
  item.head_title,
@@ -28,9 +30,7 @@ const EventSummary = (props) => {
28
30
  <HeadingTag className="title">
29
31
  {item.title ? item.title : item.id}
30
32
  </HeadingTag>
31
- {!item.hide_description && (
32
- <p className="description">{item.description}</p>
33
- )}
33
+ {!hide_description && <p className="description">{item.description}</p>}
34
34
  </>
35
35
  );
36
36
  };
@@ -1,7 +1,8 @@
1
1
  import FileType from '@kitconcept/volto-light-theme/helpers/Filetype';
2
+ import type { DefaultSummaryProps } from './DefaultSummary';
2
3
 
3
- const FileSummary = (props) => {
4
- const { item, HeadingTag = 'h3' } = props;
4
+ const FileSummary = (props: DefaultSummaryProps) => {
5
+ const { item, HeadingTag = 'h3', hide_description } = props;
5
6
 
6
7
  const headline = [item.getObjSize, FileType(item.mime_type), item.head_title]
7
8
  .filter((x) => x)
@@ -14,9 +15,7 @@ const FileSummary = (props) => {
14
15
  <HeadingTag className="title">
15
16
  {item.title ? item.title : item.id}
16
17
  </HeadingTag>
17
- {!item.hide_description && (
18
- <p className="description">{item.description}</p>
19
- )}
18
+ {!hide_description && <p className="description">{item.description}</p>}
20
19
  </>
21
20
  );
22
21
  };
@@ -1,12 +1,14 @@
1
1
  import { parseDateFromCatalog } from '@kitconcept/volto-light-theme/helpers/dates';
2
2
  import FormattedDate from '@plone/volto/components/theme/FormattedDate/FormattedDate';
3
+ import type { DefaultSummaryProps } from './DefaultSummary';
3
4
 
4
- const NewsItemSummary = (props) => {
5
- const { item, HeadingTag = 'h3' } = props;
5
+ const NewsItemSummary = (props: DefaultSummaryProps) => {
6
+ const { item, HeadingTag = 'h3', hide_description } = props;
6
7
 
7
8
  const effective = parseDateFromCatalog(item.effective);
8
9
  const headline = [
9
10
  effective ? (
11
+ // @ts-expect-error
10
12
  <FormattedDate
11
13
  key="day"
12
14
  date={effective}
@@ -30,9 +32,7 @@ const NewsItemSummary = (props) => {
30
32
  <HeadingTag className="title">
31
33
  {item.title ? item.title : item.id}
32
34
  </HeadingTag>
33
- {!item.hide_description && (
34
- <p className="description">{item.description}</p>
35
- )}
35
+ {!hide_description && <p className="description">{item.description}</p>}
36
36
  </>
37
37
  );
38
38
  };