@kitconcept/volto-light-theme 7.1.0 → 7.2.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,18 +1,14 @@
1
- ## 7.1.0 (2025-09-29)
1
+ ## 7.2.0 (2025-10-01)
2
2
 
3
3
  ### Feature
4
4
 
5
- - Add Basque translation @erral [#675](https://github.com/kitconcept/volto-light-theme/pull/675)
5
+ - Added smartTextRenderer helper for rendering markdown formatted links in plain text. @sneridagh [#679.1](https://github.com/kitconcept/volto-light-theme/pull/679.1)
6
+ - Link support in descriptions in summaries via a custom mardown parser (smartText). @sneridagh [#679.2](https://github.com/kitconcept/volto-light-theme/pull/679.2)
6
7
 
7
8
  ### Bugfix
8
9
 
9
- - Fix use Sass interpolation for CSS variable fallbacks @iRohitSingh [#673](https://github.com/kitconcept/volto-light-theme/pull/673)
10
- - Fix vertical spacing for first block in case that there is a change of color from default. @sneridagh [#676](https://github.com/kitconcept/volto-light-theme/pull/676)
11
- - Don't show parent tags when adding the child. @iFlameing
12
-
13
- ### Internal
14
-
15
- - Fix cypress test for calendar block. @iFlameing [#calendar-cypress](https://github.com/kitconcept/volto-light-theme/pull/calendar-cypress)
16
- - Misc bugfixes. Upgrade to Volto 18.27.2. @sneridagh
10
+ - Fixed icons and spacing in calendar event range widget. @sneridagh [#680](https://github.com/kitconcept/volto-light-theme/pull/680)
11
+ - Added card img 100% width for account for small images. @sneridagh [#681](https://github.com/kitconcept/volto-light-theme/pull/681)
12
+ - Fixed regression for contained teasers applying a margin only meant for contained ones. @sneridagh [#683](https://github.com/kitconcept/volto-light-theme/pull/683)
17
13
 
18
14
 
package/CHANGELOG.md CHANGED
@@ -8,6 +8,19 @@
8
8
 
9
9
  <!-- towncrier release notes start -->
10
10
 
11
+ ## 7.2.0 (2025-10-01)
12
+
13
+ ### Feature
14
+
15
+ - Added smartTextRenderer helper for rendering markdown formatted links in plain text. @sneridagh [#679.1](https://github.com/kitconcept/volto-light-theme/pull/679.1)
16
+ - Link support in descriptions in summaries via a custom mardown parser (smartText). @sneridagh [#679.2](https://github.com/kitconcept/volto-light-theme/pull/679.2)
17
+
18
+ ### Bugfix
19
+
20
+ - Fixed icons and spacing in calendar event range widget. @sneridagh [#680](https://github.com/kitconcept/volto-light-theme/pull/680)
21
+ - Added card img 100% width for account for small images. @sneridagh [#681](https://github.com/kitconcept/volto-light-theme/pull/681)
22
+ - Fixed regression for contained teasers applying a margin only meant for contained ones. @sneridagh [#683](https://github.com/kitconcept/volto-light-theme/pull/683)
23
+
11
24
  ## 7.1.0 (2025-09-29)
12
25
 
13
26
  ### Feature
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitconcept/volto-light-theme",
3
- "version": "7.1.0",
3
+ "version": "7.2.0",
4
4
  "description": "Volto Light Theme by kitconcept",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -33,14 +33,17 @@
33
33
  "@plone/scripts": "^3.6.2",
34
34
  "@storybook/react": "^8.6.12",
35
35
  "@testing-library/cypress": "10.0.3",
36
+ "@testing-library/jest-dom": "^6.8.0",
36
37
  "@testing-library/react": "^16.2.0",
37
38
  "@types/jest": "^29.5.8",
38
39
  "@types/lodash": "^4.14.201",
39
40
  "@types/node": "^22",
40
41
  "@types/react": "^18.3.12",
41
42
  "@types/react-dom": "^18.3.1",
42
- "typescript": "^5.7.3",
43
+ "react-intl-redux": "2.3.0",
44
+ "redux-mock-store": "1.5.4",
43
45
  "release-it": "^19.0.3",
46
+ "typescript": "^5.7.3",
44
47
  "vitest": "^3.1.2",
45
48
  "@plone/types": "1.4.5"
46
49
  },
@@ -48,12 +51,12 @@
48
51
  "@dnd-kit/core": "6.0.8",
49
52
  "@dnd-kit/sortable": "7.0.2",
50
53
  "@dnd-kit/utilities": "3.2.2",
54
+ "embla-carousel-autoplay": "^8.0.0",
55
+ "embla-carousel-react": "^8.0.0",
51
56
  "react-animate-height": "^3.2.3",
52
57
  "react-aria-components": "^1.7.0",
53
58
  "react-colorful": "^5.6.1",
54
59
  "uuid": "^11.0.0",
55
- "embla-carousel-autoplay": "^8.0.0",
56
- "embla-carousel-react": "^8.0.0",
57
60
  "@plone/components": "^3.0.2"
58
61
  },
59
62
  "peerDependencies": {
@@ -22,6 +22,8 @@ import cx from 'classnames';
22
22
  import Icon from '@plone/volto/components/theme/Icon/Icon';
23
23
  import CalendarSVG from '@plone/volto/icons/calendar.svg';
24
24
  import ClearSVG from '@plone/volto/icons/clear.svg';
25
+ import LeftArrowSVG from '@plone/volto/icons/left-key.svg';
26
+ import RightArrowSVG from '@plone/volto/icons/right-key.svg';
25
27
 
26
28
  export interface DateRangePickerProps<T extends DateValue>
27
29
  extends RACDateRangePickerProps<T> {
@@ -64,13 +66,17 @@ export function DateRangePicker<T extends DateValue>({
64
66
 
65
67
  {description && <Text slot="description">{description}</Text>}
66
68
  <FieldError>{errorMessage}</FieldError>
67
- <Popover>
69
+ <Popover offset={0}>
68
70
  <Dialog>
69
71
  <RangeCalendar>
70
72
  <header>
71
- <Button slot="previous">◀</Button>
73
+ <Button slot="previous">
74
+ <Icon name={LeftArrowSVG} />
75
+ </Button>
72
76
  <Heading />
73
- <Button slot="next">▶</Button>
77
+ <Button slot="next">
78
+ <Icon name={RightArrowSVG} />
79
+ </Button>
74
80
  </header>
75
81
  <CalendarGrid>
76
82
  {(date) => <CalendarCell date={date} />}
@@ -0,0 +1,50 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import configureStore from 'redux-mock-store';
4
+ import { Provider } from 'react-intl-redux';
5
+ import { MemoryRouter } from 'react-router-dom';
6
+ import TeaserDefaultTemplate from './DefaultBody';
7
+
8
+ const mockStore = configureStore();
9
+
10
+ describe('TeaserDefaultTemplate', () => {
11
+ it('renders markdown links inside the description', () => {
12
+ const store = mockStore({
13
+ intl: {
14
+ locale: 'en',
15
+ messages: {},
16
+ },
17
+ userSession: {
18
+ token: null,
19
+ },
20
+ });
21
+
22
+ render(
23
+ <Provider store={store}>
24
+ <MemoryRouter>
25
+ <TeaserDefaultTemplate
26
+ className=""
27
+ data={{
28
+ href: [
29
+ {
30
+ '@id': '/news',
31
+ },
32
+ ],
33
+ preview_image: [],
34
+ title: 'News',
35
+ description: 'Read the [Portal](/portal) updates',
36
+ overwrite: true,
37
+ }}
38
+ isEditMode={false}
39
+ />
40
+ </MemoryRouter>
41
+ </Provider>,
42
+ );
43
+
44
+ const link = screen.getByRole('link', { name: 'Portal' });
45
+ expect(link.getAttribute('href')).toBe('/portal');
46
+
47
+ const mainLink = screen.getByRole('link', { name: 'News' });
48
+ expect(mainLink.getAttribute('href')).toBe('/news');
49
+ });
50
+ });
@@ -1,5 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import type { ObjectBrowserItem } from '@plone/types';
3
+ import { smartTextRenderer } from '../../helpers/smartText';
3
4
 
4
5
  export type DefaultSummaryProps = {
5
6
  item: Partial<ObjectBrowserItem>;
@@ -20,7 +21,9 @@ const DefaultSummary = (props: DefaultSummaryProps) => {
20
21
  <HeadingTag className="title" id={a11yLabelId}>
21
22
  {item.title ? item.title : item.id}
22
23
  </HeadingTag>
23
- {!hide_description && <p className="description">{item.description}</p>}
24
+ {!hide_description && (
25
+ <p className="description">{smartTextRenderer(item.description)}</p>
26
+ )}
24
27
  </>
25
28
  );
26
29
  };
@@ -4,6 +4,7 @@ import {
4
4
  } from '@kitconcept/volto-light-theme/helpers/dates';
5
5
  import FormattedDate from '@plone/volto/components/theme/FormattedDate/FormattedDate';
6
6
  import type { DefaultSummaryProps } from './DefaultSummary';
7
+ import { smartTextRenderer } from '../../helpers/smartText';
7
8
 
8
9
  const EventSummary = (props: DefaultSummaryProps) => {
9
10
  const { item, HeadingTag = 'h3', a11yLabelId, hide_description } = props;
@@ -30,7 +31,9 @@ const EventSummary = (props: DefaultSummaryProps) => {
30
31
  <HeadingTag className="title" id={a11yLabelId}>
31
32
  {item.title ? item.title : item.id}
32
33
  </HeadingTag>
33
- {!hide_description && <p className="description">{item.description}</p>}
34
+ {!hide_description && (
35
+ <p className="description">{smartTextRenderer(item.description)}</p>
36
+ )}
34
37
  </>
35
38
  );
36
39
  };
@@ -1,5 +1,6 @@
1
1
  import FileType from '@kitconcept/volto-light-theme/helpers/Filetype';
2
2
  import type { DefaultSummaryProps } from './DefaultSummary';
3
+ import { smartTextRenderer } from '../../helpers/smartText';
3
4
 
4
5
  const FileSummary = (props: DefaultSummaryProps) => {
5
6
  const { item, HeadingTag = 'h3', a11yLabelId, hide_description } = props;
@@ -15,7 +16,9 @@ const FileSummary = (props: DefaultSummaryProps) => {
15
16
  <HeadingTag className="title" id={a11yLabelId}>
16
17
  {item.title ? item.title : item.id}
17
18
  </HeadingTag>
18
- {!hide_description && <p className="description">{item.description}</p>}
19
+ {!hide_description && (
20
+ <p className="description">{smartTextRenderer(item.description)}</p>
21
+ )}
19
22
  </>
20
23
  );
21
24
  };
@@ -1,6 +1,7 @@
1
1
  import { parseDateFromCatalog } from '@kitconcept/volto-light-theme/helpers/dates';
2
2
  import FormattedDate from '@plone/volto/components/theme/FormattedDate/FormattedDate';
3
3
  import type { DefaultSummaryProps } from './DefaultSummary';
4
+ import { smartTextRenderer } from '../../helpers/smartText';
4
5
 
5
6
  const NewsItemSummary = (props: DefaultSummaryProps) => {
6
7
  const { item, HeadingTag = 'h3', a11yLabelId, hide_description } = props;
@@ -32,7 +33,9 @@ const NewsItemSummary = (props: DefaultSummaryProps) => {
32
33
  <HeadingTag className="title" id={a11yLabelId}>
33
34
  {item.title ? item.title : item.id}
34
35
  </HeadingTag>
35
- {!hide_description && <p className="description">{item.description}</p>}
36
+ {!hide_description && (
37
+ <p className="description">{smartTextRenderer(item.description)}</p>
38
+ )}
36
39
  </>
37
40
  );
38
41
  };
@@ -1,10 +1,10 @@
1
- import * as React from 'react';
2
1
  import Icon from '@plone/volto/components/theme/Icon/Icon';
3
2
  import mailSVG from '@plone/volto/icons/email.svg';
4
3
  import locationSVG from '@plone/volto/icons/map.svg';
5
4
  import phoneSVG from '@plone/volto/icons/mobile.svg';
6
5
  import type { DefaultSummaryProps } from './DefaultSummary';
7
6
  import { defineMessages, useIntl } from 'react-intl';
7
+ import { smartTextRenderer } from '../../helpers/smartText';
8
8
 
9
9
  const messages = defineMessages({
10
10
  phone: {
@@ -31,7 +31,9 @@ const PersonSummary = (props: DefaultSummaryProps) => {
31
31
  <HeadingTag className="title" id={a11yLabelId}>
32
32
  {item.title ? item.title : item.id}
33
33
  </HeadingTag>
34
- {!hide_description && <p className="description">{item.description}</p>}
34
+ {!hide_description && (
35
+ <p className="description">{smartTextRenderer(item.description)}</p>
36
+ )}
35
37
 
36
38
  {item.contact_email && (
37
39
  <div className="summary-extra-info email">
@@ -38,6 +38,24 @@ export const Summary: Story = {
38
38
  },
39
39
  };
40
40
 
41
+ export const SummaryWithLink: Story = {
42
+ render: (args) => (
43
+ <div style={{ width: '300px' }}>
44
+ <Wrapper>
45
+ <DefaultSummary {...args} />
46
+ </Wrapper>
47
+ </div>
48
+ ),
49
+ args: {
50
+ item: {
51
+ title: 'Simple Card with strings',
52
+ description:
53
+ '[Lorem ipsum](https://example.com) 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.',
54
+ head_title: 'Simple Card',
55
+ },
56
+ },
57
+ };
58
+
41
59
  export const SummaryHideDescription: Story = {
42
60
  render: (args) => (
43
61
  <div style={{ width: '300px' }}>
@@ -0,0 +1,95 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import * as React from 'react';
4
+ import { smartTextRenderer } from './smartText';
5
+
6
+ vi.mock('@plone/volto/components/manage/UniversalLink/UniversalLink', () => ({
7
+ default: ({
8
+ href,
9
+ children,
10
+ }: {
11
+ href: string;
12
+ children: React.ReactNode;
13
+ }) => (
14
+ <a href={href} data-testid="universal-link">
15
+ {children}
16
+ </a>
17
+ ),
18
+ }));
19
+
20
+ const renderWithWrapper = (description: string | null | undefined) =>
21
+ render(
22
+ <div data-testid="description-wrapper">
23
+ {smartTextRenderer(description)}
24
+ </div>,
25
+ );
26
+
27
+ describe('smartTextRenderer', () => {
28
+ it('returns null for empty description', () => {
29
+ expect(smartTextRenderer('')).toBeNull();
30
+ expect(smartTextRenderer(null)).toBeNull();
31
+ expect(smartTextRenderer(undefined)).toBeNull();
32
+ });
33
+
34
+ it('renders plain text without creating links', () => {
35
+ const description = 'This is a plain description.';
36
+ const { container, queryByTestId } = renderWithWrapper(description);
37
+
38
+ expect(container).toHaveTextContent(description);
39
+ expect(queryByTestId('universal-link')).toBeNull();
40
+ });
41
+
42
+ it('renders a single markdown link and preserves surrounding text', () => {
43
+ const description = 'Visit [Google](https://google.com) for more info.';
44
+ const { container } = renderWithWrapper(description);
45
+ const link = screen.getByRole('link', { name: 'Google' });
46
+
47
+ expect(container).toHaveTextContent('Visit Google for more info.');
48
+ expect(link).toHaveAttribute('href', 'https://google.com');
49
+ });
50
+
51
+ it('renders multiple markdown links with text segments between them', () => {
52
+ const description = 'Check [One](/one) and [Two](/two) for details.';
53
+ renderWithWrapper(description);
54
+ const links = screen.getAllByRole('link');
55
+
56
+ expect(links).toHaveLength(2);
57
+ expect(links[0]).toHaveAttribute('href', '/one');
58
+ expect(links[1]).toHaveAttribute('href', '/two');
59
+ });
60
+
61
+ it('trims whitespace around link text and href values', () => {
62
+ const description = 'Go to [ Space ]( /space )!';
63
+ const { container } = renderWithWrapper(description);
64
+ const link = screen.getByRole('link', { name: 'Space' });
65
+
66
+ expect(container).toHaveTextContent('Go to Space!');
67
+ expect(link).toHaveAttribute('href', '/space');
68
+ });
69
+
70
+ it('ignores bracketed text that is not a markdown link', () => {
71
+ const description = 'Keep [this] but not a link.';
72
+ const { container } = renderWithWrapper(description);
73
+
74
+ expect(container).toHaveTextContent('Keep [this] but not a link.');
75
+ });
76
+
77
+ it('resets the markdown matcher between invocations', () => {
78
+ const { rerender } = renderWithWrapper('First [Link](/first) call.');
79
+ expect(screen.getByRole('link', { name: 'Link' })).toHaveAttribute(
80
+ 'href',
81
+ '/first',
82
+ );
83
+
84
+ rerender(
85
+ <div data-testid="description-wrapper">
86
+ {smartTextRenderer('Second [Link](/second) call.')}
87
+ </div>,
88
+ );
89
+
90
+ expect(screen.getByRole('link', { name: 'Link' })).toHaveAttribute(
91
+ 'href',
92
+ '/second',
93
+ );
94
+ });
95
+ });
@@ -0,0 +1,46 @@
1
+ import * as React from 'react';
2
+ import UniversalLink from '@plone/volto/components/manage/UniversalLink/UniversalLink';
3
+
4
+ const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
5
+
6
+ export const smartTextRenderer = (smartText) => {
7
+ if (!smartText) return null;
8
+
9
+ linkPattern.lastIndex = 0;
10
+
11
+ const parts = [];
12
+ let lastIndex = 0;
13
+ let match;
14
+
15
+ while ((match = linkPattern.exec(smartText)) !== null) {
16
+ if (match.index > lastIndex) {
17
+ parts.push(smartText.slice(lastIndex, match.index));
18
+ }
19
+
20
+ const [, text, href] = match;
21
+
22
+ parts.push({
23
+ type: 'link',
24
+ text: text.trim(),
25
+ href: href.trim(),
26
+ });
27
+
28
+ lastIndex = match.index + match[0].length;
29
+ }
30
+
31
+ if (lastIndex < smartText.length) {
32
+ parts.push(smartText.slice(lastIndex));
33
+ }
34
+
35
+ return parts.map((part, index) => {
36
+ if (typeof part === 'string') {
37
+ return <React.Fragment key={`text-${index}`}>{part}</React.Fragment>;
38
+ }
39
+
40
+ return (
41
+ <UniversalLink href={part.href} key={`link-${index}`}>
42
+ {part.text}
43
+ </UniversalLink>
44
+ );
45
+ });
46
+ };
@@ -3,17 +3,8 @@
3
3
  color: var(--theme-foreground-color);
4
4
  @include color-block-change-vertical-spacing();
5
5
 
6
- // // TODO: investigate why on SSR the whitespace is removed and in client not
7
- // // This is a workaround to avoid the two use cases...
8
- // &[style*='--theme-color:#fff']:first-child,
9
- // &[style*='--theme-color: #fff']:first-child {
10
- // padding-top: 0;
11
- // }
12
-
13
- // &:first-child:has(> .previous--has--same--backgroundColor) {
14
- // padding-top: 0;
15
- // }
16
-
6
+ // First block of the page has no top padding, only if previous block has same background color
7
+ // eg: when it's different than default.
17
8
  &:first-child:has(> :first-child.previous--has--same--backgroundColor) {
18
9
  padding-top: 0;
19
10
  }
@@ -368,7 +368,7 @@ External link removal for all the blocks.
368
368
  .block.teaser.has--align--center,
369
369
  .block.eventMetadata .details-container,
370
370
  .block-editor-teaser .teaser-item.default,
371
- .block-editor-teaser .card-inner, // deprecate when category is in place
371
+ .block-editor-teaser:not(.contained) .card-inner, // deprecate when category is in place
372
372
  .block-editor-slateTable .block.table,
373
373
  .block-editor-highlight .teaser-description-title,
374
374
  .block-editor-toc .table-of-contents,
@@ -417,12 +417,20 @@ This is css code of popup of calendar */
417
417
  border-radius: unset;
418
418
  }
419
419
  .react-aria-RangeCalendar {
420
+ font-size: 24px;
421
+
422
+ table .react-aria-CalendarCell {
423
+ width: 2.75rem;
424
+ }
425
+
420
426
  header {
421
427
  display: flex;
428
+ margin: 10px 0 20px 0;
422
429
  }
423
430
 
424
431
  //ask victor about this. Font family causing unwanted problems in rendering of icon.
425
432
  .react-aria-Button {
433
+ border: none;
426
434
  border-radius: unset;
427
435
  font-family: unset;
428
436
  }
@@ -432,3 +440,17 @@ This is css code of popup of calendar */
432
440
  text-align: center;
433
441
  }
434
442
  }
443
+
444
+ // body:has(.react-aria-Popover[data-trigger='DateRangePicker'])
445
+ // .react-aria-Group {
446
+ // border-bottom: none;
447
+ // }
448
+
449
+ .react-aria-Popover[data-trigger='DateRangePicker'] {
450
+ min-width: var(--trigger-width);
451
+ border-top: none;
452
+ }
453
+
454
+ .react-aria-Dialog:has(.react-aria-RangeCalendar) {
455
+ padding: 10px;
456
+ }
@@ -24,6 +24,9 @@
24
24
 
25
25
  img {
26
26
  display: block;
27
+ // Beware of this, it can cause issues with some images, added back on 2025-09-30 @sneridagh
28
+ // Small images will be pixelated though
29
+ width: 100%;
27
30
  aspect-ratio: var(--image-aspect-ratio, $aspect-ratio) !important;
28
31
  }
29
32
  }
package/tsconfig.json CHANGED
@@ -12,6 +12,7 @@
12
12
  "noEmit": true,
13
13
  "lib": ["es2022", "dom", "dom.iterable"],
14
14
  "jsx": "react-jsx",
15
+ "types": ["vitest", "@testing-library/jest-dom"],
15
16
  "paths": {
16
17
  "@plone/volto/*": [
17
18
  "../../core/packages/volto/src/*",
package/vitest.config.mjs CHANGED
@@ -4,11 +4,18 @@ import path from 'path';
4
4
 
5
5
  export default defineConfig({
6
6
  ...voltoVitestConfig,
7
+ resolve: {
8
+ alias: {
9
+ ...voltoVitestConfig.resolve.alias,
10
+ // Alias for absolute imports
11
+ '@kitconcept/volto-light-theme': path.resolve(__dirname, './src'),
12
+ '@kitconcept/volto-light-theme/': path.resolve(__dirname, './src/'),
13
+ },
14
+ },
7
15
  server: {
8
16
  fs: {
9
17
  allow: [
10
- // Allow vite/vitest to access these folders
11
- '..', // allow going up from frontend/
18
+ '..',
12
19
  path.resolve(__dirname, '../../../../../core/packages/volto'),
13
20
  ],
14
21
  },