@kitconcept/volto-light-theme 8.0.0-alpha.2 → 8.0.0-alpha.3

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,17 @@
1
- ## 8.0.0-alpha.2 (2025-10-29)
1
+ ## 8.0.0-alpha.3 (2025-11-04)
2
2
 
3
3
  ### Feature
4
4
 
5
- - Refactor `volto-button-block`, transfer all the customizations to the block. @sneridagh [#697](https://github.com/kitconcept/volto-light-theme/pull/697)
6
- - Refactor `volto-separator-block`, transfer all the customizations to the block. @sneridagh [#698](https://github.com/kitconcept/volto-light-theme/pull/698)
5
+ - Recoverable Block Error Boundaries. @sneridagh [#708](https://github.com/kitconcept/volto-light-theme/pull/708)
6
+ - Update `Buttons` widget to the one proposed for the Volto PR: #7555 @sneridagh
7
+
8
+ ### Bugfix
9
+
10
+ - [#705](https://github.com/kitconcept/volto-light-theme/pull/705)
11
+ - Fix Teaser Blocks for Person type email spacing in 4 columns gridBlock @Tishasoumya-02
12
+
13
+ ### Internal
14
+
15
+ - Add css for NotFound Page @Tishasoumya-02
7
16
 
8
17
 
package/CHANGELOG.md CHANGED
@@ -8,6 +8,22 @@
8
8
 
9
9
  <!-- towncrier release notes start -->
10
10
 
11
+ ## 8.0.0-alpha.3 (2025-11-04)
12
+
13
+ ### Feature
14
+
15
+ - Recoverable Block Error Boundaries. @sneridagh [#708](https://github.com/kitconcept/volto-light-theme/pull/708)
16
+ - Update `Buttons` widget to the one proposed for the Volto PR: #7555 @sneridagh
17
+
18
+ ### Bugfix
19
+
20
+ - [#705](https://github.com/kitconcept/volto-light-theme/pull/705)
21
+ - Fix Teaser Blocks for Person type email spacing in 4 columns gridBlock @Tishasoumya-02
22
+
23
+ ### Internal
24
+
25
+ - Add css for NotFound Page @Tishasoumya-02
26
+
11
27
  ## 8.0.0-alpha.2 (2025-10-29)
12
28
 
13
29
  ### Feature
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitconcept/volto-light-theme",
3
- "version": "8.0.0-alpha.2",
3
+ "version": "8.0.0-alpha.3",
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.7"
48
+ "@plone/types": "2.0.0-alpha.8"
49
49
  },
50
50
  "dependencies": {
51
51
  "@dnd-kit/core": "6.0.8",
@@ -54,7 +54,7 @@
54
54
  "embla-carousel-autoplay": "^8.0.0",
55
55
  "embla-carousel-react": "^8.0.0",
56
56
  "react-animate-height": "^3.2.3",
57
- "react-aria-components": "^1.7.0",
57
+ "react-aria-components": "^1.12.1",
58
58
  "react-colorful": "^5.6.1",
59
59
  "uuid": "^11.0.0",
60
60
  "@plone/components": "^4.0.0-alpha.1"
@@ -35,6 +35,7 @@ import {
35
35
  buildStyleClassNamesExtenders,
36
36
  } from '@plone/volto/helpers/Blocks/Blocks';
37
37
  import MaybeWrap from '@plone/volto/components/manage/MaybeWrap/MaybeWrap';
38
+ import ErrorBoundary from './ErrorBoundary';
38
39
 
39
40
  const messages = defineMessages({
40
41
  unknownBlock: {
@@ -288,12 +289,19 @@ export class Edit extends Component {
288
289
  as={'div'}
289
290
  className="block-inner-container"
290
291
  >
291
- <Block
292
- {...this.props}
293
- blockNode={this.blockNode}
294
- data={this.props.data}
295
- className={cx({ contained: parentIsContainer })}
296
- />
292
+ <ErrorBoundary
293
+ name={`blockId-${this.props.id}-type-${type}`}
294
+ block={this.props.block}
295
+ type={type}
296
+ isEdit
297
+ >
298
+ <Block
299
+ {...this.props}
300
+ blockNode={this.blockNode}
301
+ data={this.props.data}
302
+ className={cx({ contained: parentIsContainer })}
303
+ />
304
+ </ErrorBoundary>
297
305
  </MaybeWrap>
298
306
 
299
307
  {blocksConfig?.[type]?.blockModel === 3 && (
@@ -0,0 +1,55 @@
1
+ import React, { type FC } from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { afterAll, afterEach, describe, expect, it, vi } from 'vitest';
4
+ import { Provider } from 'react-intl-redux';
5
+ import { ErrorBoundary } from './ErrorBoundary';
6
+ import configureStore from 'redux-mock-store';
7
+
8
+ describe('Error boundary', () => {
9
+ const consoleErrorSpy = vi
10
+ .spyOn(console, 'error')
11
+ .mockImplementation(() => undefined);
12
+
13
+ afterEach(() => {
14
+ consoleErrorSpy.mockClear();
15
+ });
16
+
17
+ afterAll(() => {
18
+ consoleErrorSpy.mockRestore();
19
+ });
20
+
21
+ const mockStore = configureStore();
22
+
23
+ const store = mockStore({
24
+ intl: {
25
+ locale: 'en',
26
+ messages: {},
27
+ },
28
+ });
29
+
30
+ it('renders fallback UI when a child throws', () => {
31
+ const ThrowError: FC = () => {
32
+ throw new Error('Test');
33
+ };
34
+
35
+ const { container } = render(
36
+ <Provider store={store}>
37
+ <ErrorBoundary
38
+ name="test"
39
+ block="123"
40
+ type="slate"
41
+ blocks={null}
42
+ blocksLayout={null}
43
+ title={null}
44
+ >
45
+ <ThrowError />
46
+ </ErrorBoundary>
47
+ </Provider>,
48
+ );
49
+
50
+ expect(screen.getByText('Block error:')).toBeInTheDocument();
51
+ expect(
52
+ container.querySelector('.block-error-boundary .title'),
53
+ ).toBeInTheDocument();
54
+ });
55
+ });
@@ -0,0 +1,92 @@
1
+ import React from 'react';
2
+ import type { ErrorInfo, ReactNode } from 'react';
3
+ import { connect } from 'react-redux';
4
+ import type { BlocksData } from '@plone/types';
5
+ import ErrorBoundaryMessage from './ErrorBoundaryMessage';
6
+
7
+ type OwnProps = {
8
+ name?: string;
9
+ block?: string;
10
+ type?: string;
11
+ isEdit?: boolean;
12
+ children?: ReactNode;
13
+ };
14
+
15
+ type StateProps = {
16
+ blocks: BlocksData['blocks'];
17
+ blocksLayout: BlocksData['blocks_layout'];
18
+ title: string | null;
19
+ };
20
+
21
+ type ErrorBoundaryState = {
22
+ hasError: boolean;
23
+ };
24
+
25
+ type ErrorBoundaryProps = OwnProps & StateProps;
26
+
27
+ export class ErrorBoundary extends React.Component<
28
+ ErrorBoundaryProps,
29
+ ErrorBoundaryState
30
+ > {
31
+ state: ErrorBoundaryState = { hasError: false };
32
+
33
+ static getDerivedStateFromError(_error: Error): ErrorBoundaryState {
34
+ // Update state so the next render will show the fallback UI.
35
+ return { hasError: true };
36
+ }
37
+
38
+ componentDidUpdate(prevProps: ErrorBoundaryProps) {
39
+ const titleChanged = prevProps.title !== this.props.title;
40
+ const blocksChanged = prevProps.blocks !== this.props.blocks;
41
+ const blocksLayoutChanged =
42
+ prevProps.blocksLayout !== this.props.blocksLayout;
43
+
44
+ if (
45
+ (blocksChanged || blocksLayoutChanged || titleChanged) &&
46
+ this.state.hasError
47
+ ) {
48
+ this.setState({ hasError: false });
49
+ }
50
+ }
51
+
52
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
53
+ // eslint-disable-next-line
54
+ console.error(error, errorInfo);
55
+ }
56
+
57
+ render(): ReactNode {
58
+ if (this.state.hasError) {
59
+ return (
60
+ <ErrorBoundaryMessage
61
+ name={this.props.name}
62
+ block={this.props.block}
63
+ type={this.props.type}
64
+ isEdit={this.props.isEdit}
65
+ />
66
+ );
67
+ }
68
+
69
+ return this.props.children;
70
+ }
71
+ }
72
+
73
+ type ReduxState = {
74
+ form?: {
75
+ global?: {
76
+ blocks?: BlocksData['blocks'];
77
+ blocks_layout?: BlocksData['blocks_layout'];
78
+ title: string | null;
79
+ };
80
+ };
81
+ };
82
+
83
+ const mapStateToProps = (state: ReduxState): StateProps => ({
84
+ blocks: state.form?.global?.blocks ?? null,
85
+ blocksLayout: state.form?.global?.blocks_layout ?? null,
86
+ // Title is used for demonstration purposes
87
+ // If we want to use it in metadata sources, we should connect it to the full state
88
+ // which I am reluctant to do because nowadays the form state can be quite large
89
+ title: state.form?.global?.title ?? null,
90
+ });
91
+
92
+ export default connect(mapStateToProps)(ErrorBoundary);
@@ -0,0 +1,66 @@
1
+ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
2
+
3
+ type ErrorBoundaryMessageProps = {
4
+ name?: string;
5
+ block?: string;
6
+ type?: string;
7
+ isEdit?: boolean;
8
+ };
9
+
10
+ const messages = defineMessages({
11
+ title: {
12
+ id: 'blockErrorBoundaryTitle',
13
+ defaultMessage: 'Block error:',
14
+ },
15
+ description: {
16
+ id: 'blockErrorBoundaryDescription',
17
+ defaultMessage:
18
+ 'The {type} block with the id {block} has encountered an error.{lineBreak}You can try to undo your changes (via the undo toolbar or pressing {shortcut}), or try to delete the block and recreate it again.',
19
+ },
20
+ viewDescription: {
21
+ id: 'blockErrorBoundaryViewDescription',
22
+ defaultMessage:
23
+ 'The {type} block with the id {block} errored and cannot be displayed.{lineBreak}Please contact the site administrator for further assistance.',
24
+ },
25
+ });
26
+
27
+ const ErrorBoundaryMessage = (props: ErrorBoundaryMessageProps) => {
28
+ const intl = useIntl();
29
+
30
+ if (props.isEdit) {
31
+ return (
32
+ <div className="block-error-boundary">
33
+ <div className="title">{intl.formatMessage(messages.title)}</div>
34
+ <p>
35
+ <FormattedMessage
36
+ {...messages.description}
37
+ values={{
38
+ type: <code>{props.type}</code>,
39
+ block: <code>{props.block}</code>,
40
+ lineBreak: <br />,
41
+ shortcut: <code>ctrl/cmd + Z</code>,
42
+ }}
43
+ />
44
+ </p>
45
+ </div>
46
+ );
47
+ } else {
48
+ return (
49
+ <div className="block-error-boundary">
50
+ <div className="title">{intl.formatMessage(messages.title)}</div>
51
+ <p>
52
+ <FormattedMessage
53
+ {...messages.viewDescription}
54
+ values={{
55
+ type: <code>{props.type}</code>,
56
+ block: <code>{props.block}</code>,
57
+ lineBreak: <br />,
58
+ }}
59
+ />
60
+ </p>
61
+ </div>
62
+ );
63
+ }
64
+ };
65
+
66
+ export default ErrorBoundaryMessage;
@@ -12,6 +12,7 @@ import config from '@plone/volto/registry';
12
12
  import ViewDefaultBlock from '@plone/volto/components/manage/Blocks/Block/DefaultView';
13
13
  import MaybeWrap from '@plone/volto/components/manage/MaybeWrap/MaybeWrap';
14
14
  import RenderEmptyBlock from '@plone/volto/components/theme/View/RenderEmptyBlock';
15
+ import ErrorBoundary from '../Blocks/Block/ErrorBoundary';
15
16
 
16
17
  import StyleWrapperV3 from './StyleWrapperV3';
17
18
  import RenderBlocksV2 from './RenderBlocksV2';
@@ -69,48 +70,55 @@ const RenderBlocks = (props) => {
69
70
 
70
71
  if (Block) {
71
72
  return (
72
- <MaybeWrap
73
- key={block}
74
- condition={blockWrapperTag}
75
- as={blockWrapperTag}
73
+ <ErrorBoundary
74
+ key={`error-boundary-block-${block}`}
75
+ name={`blockId-${block}-type-${content[blocksFieldname]?.[block]?.['@type']}`}
76
+ block={block}
77
+ type={content[blocksFieldname]?.[block]?.['@type']}
76
78
  >
77
- {currentBlockModel === 3 ? (
78
- <StyleWrapperV3
79
- block={block}
80
- content={content}
81
- data={blockData}
82
- blocksConfig={blocksConfig}
83
- isContainer={isContainer}
84
- >
85
- <Block
86
- id={block}
87
- metadata={metadata}
88
- properties={content}
79
+ <MaybeWrap
80
+ key={block}
81
+ condition={blockWrapperTag}
82
+ as={blockWrapperTag}
83
+ >
84
+ {currentBlockModel === 3 ? (
85
+ <StyleWrapperV3
86
+ block={block}
87
+ content={content}
89
88
  data={blockData}
90
- path={getBaseUrl(location?.pathname || '')}
91
89
  blocksConfig={blocksConfig}
92
- />
93
- </StyleWrapperV3>
94
- ) : (
95
- <StyleWrapper
96
- key={block}
97
- {...props}
98
- id={block}
99
- block={block}
100
- data={blockData}
101
- isContainer={isContainer}
102
- >
103
- <Block
90
+ isContainer={isContainer}
91
+ >
92
+ <Block
93
+ id={block}
94
+ metadata={metadata}
95
+ properties={content}
96
+ data={blockData}
97
+ path={getBaseUrl(location?.pathname || '')}
98
+ blocksConfig={blocksConfig}
99
+ />
100
+ </StyleWrapperV3>
101
+ ) : (
102
+ <StyleWrapper
103
+ key={block}
104
+ {...props}
104
105
  id={block}
105
- metadata={metadata}
106
- properties={content}
106
+ block={block}
107
107
  data={blockData}
108
- path={getBaseUrl(location?.pathname || '')}
109
- blocksConfig={blocksConfig}
110
- />
111
- </StyleWrapper>
112
- )}
113
- </MaybeWrap>
108
+ isContainer={isContainer}
109
+ >
110
+ <Block
111
+ id={block}
112
+ metadata={metadata}
113
+ properties={content}
114
+ data={blockData}
115
+ path={getBaseUrl(location?.pathname || '')}
116
+ blocksConfig={blocksConfig}
117
+ />
118
+ </StyleWrapper>
119
+ )}
120
+ </MaybeWrap>
121
+ </ErrorBoundary>
114
122
  );
115
123
  }
116
124
 
@@ -15,6 +15,7 @@ import MaybeWrap from '@plone/volto/components/manage/MaybeWrap/MaybeWrap';
15
15
  import RenderEmptyBlock from '@plone/volto/components/theme/View/RenderEmptyBlock';
16
16
  import cx from 'classnames';
17
17
  import { groupByBGColor } from '../../helpers/grouping';
18
+ import ErrorBoundary from '../Blocks/Block/ErrorBoundary';
18
19
 
19
20
  const messages = defineMessages({
20
21
  unknownBlock: {
@@ -92,29 +93,36 @@ const RenderBlocks = (props) => {
92
93
 
93
94
  if (Block) {
94
95
  return (
95
- <MaybeWrap
96
- key={block}
97
- condition={blockWrapperTag}
98
- as={blockWrapperTag}
96
+ <ErrorBoundary
97
+ key={`error-boundary-block-${block}`}
98
+ name={`blockId-${block}-type-${content[blocksFieldname]?.[block]?.['@type']}`}
99
+ block={block}
100
+ type={content[blocksFieldname]?.[block]?.['@type']}
99
101
  >
100
- <StyleWrapper
102
+ <MaybeWrap
101
103
  key={block}
102
- {...props}
103
- id={block}
104
- block={block}
105
- data={blockData}
106
- isContainer={isContainer}
104
+ condition={blockWrapperTag}
105
+ as={blockWrapperTag}
107
106
  >
108
- <Block
107
+ <StyleWrapper
108
+ key={block}
109
+ {...props}
109
110
  id={block}
110
- metadata={metadata}
111
- properties={content}
111
+ block={block}
112
112
  data={blockData}
113
- path={getBaseUrl(location?.pathname || '')}
114
- blocksConfig={blocksConfig}
115
- />
116
- </StyleWrapper>
117
- </MaybeWrap>
113
+ isContainer={isContainer}
114
+ >
115
+ <Block
116
+ id={block}
117
+ metadata={metadata}
118
+ properties={content}
119
+ data={blockData}
120
+ path={getBaseUrl(location?.pathname || '')}
121
+ blocksConfig={blocksConfig}
122
+ />
123
+ </StyleWrapper>
124
+ </MaybeWrap>
125
+ </ErrorBoundary>
118
126
  );
119
127
  }
120
128
 
@@ -1,7 +1,8 @@
1
1
  import React from 'react';
2
2
  import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper';
3
3
  import Icon from '@plone/volto/components/theme/Icon/Icon';
4
- import { Button } from '@plone/components';
4
+ import { RadioGroup } from '@plone/components';
5
+ import { Radio } from 'react-aria-components';
5
6
  import isEqual from 'lodash/isEqual';
6
7
  import type { StyleDefinition } from '@plone/types';
7
8
 
@@ -90,6 +91,25 @@ const ButtonsWidget = (props: ButtonsWidgetProps) => {
90
91
  [actions],
91
92
  );
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
+
93
113
  React.useEffect(() => {
94
114
  if (!value && defaultValue) {
95
115
  const nextValue =
@@ -104,7 +124,14 @@ const ButtonsWidget = (props: ButtonsWidgetProps) => {
104
124
 
105
125
  return (
106
126
  <FormFieldWrapper {...props} className="widget">
107
- <div className="buttons buttons-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
+ >
108
135
  {normalizedActions.map((action) => {
109
136
  const actionInfo = actionsInfoMap?.[action.name];
110
137
  const [iconOrText, ariaLabel] = actionInfo ?? [
@@ -113,16 +140,11 @@ const ButtonsWidget = (props: ButtonsWidgetProps) => {
113
140
  ];
114
141
 
115
142
  return (
116
- <Button
143
+ <Radio
117
144
  key={action.name}
118
145
  aria-label={ariaLabel}
119
- onPress={() => onChange(id, action.value)}
120
- className={
121
- isEqual(value, action.value)
122
- ? 'react-aria-Button active'
123
- : 'react-aria-Button'
124
- }
125
- isDisabled={disabled || isDisabled}
146
+ value={action.name}
147
+ className="buttons-widget-option"
126
148
  >
127
149
  {typeof iconOrText === 'string' ? (
128
150
  <div className="image-sizes-text">{iconOrText}</div>
@@ -131,12 +153,13 @@ const ButtonsWidget = (props: ButtonsWidgetProps) => {
131
153
  name={iconOrText}
132
154
  title={ariaLabel || action.name}
133
155
  size="24px"
156
+ ariaHidden={true}
134
157
  />
135
158
  )}
136
- </Button>
159
+ </Radio>
137
160
  );
138
161
  })}
139
- </div>
162
+ </RadioGroup>
140
163
  </FormFieldWrapper>
141
164
  );
142
165
  };
@@ -6,21 +6,22 @@
6
6
  #sidebar-properties,
7
7
  #sidebar-metadata {
8
8
  .field.widget {
9
- .buttons {
9
+ .buttons-widget {
10
10
  display: flex;
11
11
  align-items: center;
12
+ gap: 4px;
12
13
 
13
- button {
14
+ .buttons-widget-option {
14
15
  padding: 5px;
15
16
  border-radius: 3px;
16
- margin-right: 3px;
17
17
  aspect-ratio: 1/1;
18
18
  font-size: 1em;
19
19
  line-height: initial;
20
+
20
21
  &[data-hovered='true'] {
21
22
  box-shadow: inset 0 0 0 1px var(--border-color-pressed, #2996da);
22
23
  }
23
- &.active {
24
+ &[data-selected='true'] {
24
25
  box-shadow: inset 0 0 0 1px var(--border-color-pressed, #2996da);
25
26
  }
26
27
 
@@ -134,12 +135,10 @@ span.color-contrast-label {
134
135
  background-color: var(--theme-color);
135
136
  }
136
137
 
137
- .buttons-widget {
138
- button {
139
- &[data-disabled] {
140
- border-color: var(--border-color-disabled);
141
- color: var(--text-color-disabled);
142
- }
138
+ .buttons-widget-option {
139
+ &[data-disabled] {
140
+ border-color: var(--border-color-disabled);
141
+ color: var(--text-color-disabled);
143
142
  }
144
143
  }
145
144
 
@@ -471,3 +471,8 @@
471
471
  }
472
472
  }
473
473
  }
474
+
475
+ .listing-item .headline span:not(:last-child)::after {
476
+ margin: 0 10px;
477
+ content: '|';
478
+ }
@@ -0,0 +1,11 @@
1
+ .block-error-boundary {
2
+ padding: 10px;
3
+ border: 2px dashed red;
4
+ background-color: #ffe6e6;
5
+ }
6
+
7
+ .block-error-boundary .title {
8
+ margin-bottom: 5px;
9
+ color: red;
10
+ font-weight: bold;
11
+ }
@@ -36,10 +36,12 @@
36
36
  @import 'blocks/eventMetadata';
37
37
  @import 'blocks/rss';
38
38
  @import 'blocks/eventSearch';
39
+ @import 'blocks/error-boundary';
39
40
  @import 'sticky-menu';
40
41
  @import 'card';
41
42
  @import 'insets';
42
43
  @import 'person';
44
+ @import 'notfound';
43
45
 
44
46
  @import 'temp';
45
47
 
@@ -0,0 +1,25 @@
1
+ .page-not-found {
2
+ .content-area {
3
+ @include default-container-width();
4
+ @include adjustMarginsToContainer($default-container-width);
5
+ .view-wrapper {
6
+ padding-top: $spacing-xlarge;
7
+ padding-bottom: $spacing-xlarge;
8
+ }
9
+ h1 {
10
+ margin-bottom: $spacing-large;
11
+ font-size: 48px;
12
+ font-weight: 700;
13
+ line-height: 56px;
14
+ }
15
+ p,
16
+ a {
17
+ font-size: 24px;
18
+ font-weight: 300;
19
+ line-height: 33px;
20
+ }
21
+ a {
22
+ color: $black;
23
+ }
24
+ }
25
+ }
@@ -325,24 +325,34 @@ body.person-squared-images,
325
325
  }
326
326
  }
327
327
  }
328
- // person with Four columns
328
+
329
+ // person with Four columns (edit mode and view mode)
330
+ .block.gridBlock .four.grid-items .person-teaser .card-summary,
331
+ .ui.stackable.stretched.four.column.grid .card-summary {
332
+ .summary-extra-info {
333
+ &.email {
334
+ min-width: 0;
335
+
336
+ a {
337
+ display: block;
338
+ overflow: hidden;
339
+ text-overflow: ellipsis;
340
+ white-space: nowrap;
341
+ }
342
+ }
343
+ }
344
+ }
345
+
346
+ // Additional styles for view mode only
329
347
  .ui.stackable.stretched.four.column.grid {
330
348
  .card-summary {
331
349
  display: grid;
350
+
332
351
  .summary-extra-info {
333
352
  gap: 5px;
334
- &.email {
335
- min-width: 0;
336
-
337
- a {
338
- display: block;
339
- overflow: hidden;
340
- text-overflow: ellipsis;
341
- white-space: nowrap;
342
- }
343
- }
344
353
  }
345
354
  }
355
+
346
356
  .block.listing,
347
357
  .block.search {
348
358
  .listing-item.person-listing .card-summary {