@salesforcedevs/dx-components 0.22.3 → 0.23.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.
Files changed (23) hide show
  1. package/lwc.config.json +2 -0
  2. package/package.json +2 -2
  3. package/src/modules/dx/cardExpanded/__tests__/cardExpanded.test.ts +193 -0
  4. package/src/modules/dx/cardExpanded/__tests__/mockProps.ts +21 -0
  5. package/src/modules/dx/cardExpanded/cardExpanded.css +49 -0
  6. package/src/modules/dx/cardExpanded/cardExpanded.html +37 -0
  7. package/src/modules/dx/cardExpanded/cardExpanded.stories.ts +59 -0
  8. package/src/modules/dx/cardExpanded/cardExpanded.ts +45 -0
  9. package/src/modules/dx/{minimalCard/__tests__/minimalCard.test.ts → cardMinimal/__tests__/cardMinimal.test.ts} +3 -3
  10. package/src/modules/dx/{minimalCard → cardMinimal}/__tests__/mockProps.ts +0 -0
  11. package/src/modules/dx/{minimalCard/minimalCard.css → cardMinimal/cardMinimal.css} +3 -4
  12. package/src/modules/dx/{minimalCard/minimalCard.html → cardMinimal/cardMinimal.html} +1 -1
  13. package/src/modules/dx/{minimalCard/minimalCard.stories.ts → cardMinimal/cardMinimal.stories.ts} +6 -6
  14. package/src/modules/dx/cardMinimal/cardMinimal.ts +3 -0
  15. package/src/modules/dx/imageAndLabel/imageAndLabel.html +14 -2
  16. package/src/modules/dx/topics/__tests__/mockProps.ts +3 -0
  17. package/src/modules/dx/topics/__tests__/topics.test.ts +31 -0
  18. package/src/modules/dx/topics/topics.css +25 -0
  19. package/src/modules/dx/topics/topics.html +12 -0
  20. package/src/modules/dx/topics/topics.ts +25 -0
  21. package/src/modules/helpers/text/text.css +7 -0
  22. package/src/modules/{dx/minimalCard/minimalCard.ts → utils/archiveCard/archiveCard.ts} +9 -9
  23. package/src/modules/utils/normalizers/normalizers.ts +2 -2
package/lwc.config.json CHANGED
@@ -11,6 +11,8 @@
11
11
  "dx/cardDemo",
12
12
  "dx/cardDocs",
13
13
  "dx/cardEvent",
14
+ "dx/cardExpanded",
15
+ "dx/cardMinimal",
14
16
  "dx/cardNews",
15
17
  "dx/cardPodcastEpisode",
16
18
  "dx/cardPodcastEpisodeProvider",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforcedevs/dx-components",
3
- "version": "0.22.3",
3
+ "version": "0.23.0",
4
4
  "description": "DX Lightning web components",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -29,5 +29,5 @@
29
29
  "@types/debounce": "^1.2.0",
30
30
  "@types/lodash.get": "^4.4.6"
31
31
  },
32
- "gitHead": "c45075053423bcb7a50d00953243aa0e93bf2a59"
32
+ "gitHead": "d1652c07c826539ae47b435125a43243496bd5c9"
33
33
  }
@@ -0,0 +1,193 @@
1
+ import CardTitle from "dx/cardTitle";
2
+ import CardExpanded from "dx/cardExpanded";
3
+ import FormattedDateTime from "dx/formattedDateTime";
4
+ import ImageAndLabel from "dx/imageAndLabel";
5
+ import Topics from "dx/topics";
6
+ import TypeBadge from "dx/typeBadge";
7
+ import { createMediaMock } from "utils/jest";
8
+ import { createRenderComponent } from "utils/tests";
9
+ import mockProps, { podcastMock } from "./mockProps";
10
+
11
+ const MOBILE_QUERY = "(max-width: 640px)";
12
+ const TAG = "dx-card-expanded";
13
+ const render = createRenderComponent(TAG, CardExpanded);
14
+
15
+ describe(TAG, () => {
16
+ describe("render", () => {
17
+ it("renders blog content type", () => {
18
+ const component = render(mockProps);
19
+
20
+ const container: HTMLElement =
21
+ component.shadowRoot.querySelector(".card-container");
22
+ expect(container).not.toBeNull();
23
+ expect(container.classList).toHaveLength(1);
24
+
25
+ const typeBadge: TypeBadge =
26
+ component.shadowRoot.querySelector("dx-type-badge");
27
+ expect(typeBadge).not.toBeNull();
28
+ expect(typeBadge.color).toBe("teal");
29
+ expect(typeBadge.value).toBe("Blog");
30
+ expect(typeBadge.size).toBe("default");
31
+
32
+ const title: CardTitle =
33
+ component.shadowRoot.querySelector("dx-card-title");
34
+ expect(title).not.toBeNull();
35
+ expect(title.href).toBe(mockProps.href);
36
+ expect(title.target).toBe("_self");
37
+
38
+ const imageAndLabel: ImageAndLabel =
39
+ component.shadowRoot.querySelector("dx-image-and-label");
40
+ expect(imageAndLabel).not.toBeNull();
41
+ expect(imageAndLabel.href).toBe(mockProps.authorHref);
42
+ expect(imageAndLabel.imgSrc).toBe(mockProps.authorImgSrc);
43
+ expect(imageAndLabel.label).toBe(mockProps.authorLabel);
44
+
45
+ expect(
46
+ component.shadowRoot.querySelector(".card-dot-separator")
47
+ ).not.toBeNull();
48
+
49
+ const formatedDate: FormattedDateTime =
50
+ component.shadowRoot.querySelector("dx-formatted-date-time");
51
+ expect(formatedDate).not.toBeNull();
52
+ expect(formatedDate.value).toBe("February 20, 2021");
53
+
54
+ const body: HTMLSpanElement = component.shadowRoot.querySelector(
55
+ ".card-container > span"
56
+ );
57
+ expect(body).not.toBeNull();
58
+ expect(body.textContent).toBe(mockProps.body);
59
+
60
+ const topics: Topics =
61
+ component.shadowRoot.querySelector("dx-topics");
62
+ expect(topics).not.toBeNull();
63
+ expect(topics.topics).toEqual(mockProps.topics);
64
+
65
+ expect(window.matchMedia).toBeCalledTimes(1);
66
+ expect(window.matchMedia).toBeCalledWith(MOBILE_QUERY);
67
+
68
+ return expect(component).toBeAccessible();
69
+ });
70
+
71
+ it("renders with no props", () => {
72
+ const component = render();
73
+
74
+ const container: HTMLElement =
75
+ component.shadowRoot.querySelector(".card-container");
76
+ expect(container).not.toBeNull();
77
+ expect(container.classList).toHaveLength(1);
78
+
79
+ const typeBadge: TypeBadge =
80
+ component.shadowRoot.querySelector("dx-type-badge");
81
+ expect(typeBadge).not.toBeNull();
82
+ expect(typeBadge.color).toBe("teal");
83
+ expect(typeBadge.value).toBe("Blog");
84
+ expect(typeBadge.size).toBe("default");
85
+
86
+ expect(
87
+ component.shadowRoot.querySelector("dx-card-title")
88
+ ).toBeNull();
89
+ expect(
90
+ component.shadowRoot.querySelector("dx-image-and-label")
91
+ ).toBeNull();
92
+ expect(
93
+ component.shadowRoot.querySelector(".card-dot-separator")
94
+ ).toBeNull();
95
+ expect(
96
+ component.shadowRoot.querySelector("dx-formatted-date-time")
97
+ ).toBeNull();
98
+ expect(
99
+ component.shadowRoot.querySelector("span:last-of-type")
100
+ ).toBeNull();
101
+ expect(component.shadowRoot.querySelector("dx-topics")).toBeNull();
102
+
103
+ expect(window.matchMedia).toBeCalledTimes(1);
104
+ expect(window.matchMedia).toBeCalledWith(MOBILE_QUERY);
105
+ });
106
+
107
+ it("renders Podcast content type", () => {
108
+ const component = render(podcastMock);
109
+
110
+ const container: HTMLElement =
111
+ component.shadowRoot.querySelector(".card-container");
112
+ expect(container).not.toBeNull();
113
+ expect(container.classList).toHaveLength(1);
114
+
115
+ const typeBadge: TypeBadge =
116
+ component.shadowRoot.querySelector("dx-type-badge");
117
+ expect(typeBadge).not.toBeNull();
118
+ expect(typeBadge.color).toBe("blue");
119
+ expect(typeBadge.value).toBe("Podcast");
120
+ expect(typeBadge.size).toBe("default");
121
+
122
+ const title: CardTitle =
123
+ component.shadowRoot.querySelector("dx-card-title");
124
+ expect(title).not.toBeNull();
125
+ expect(title.href).toBe(podcastMock.href);
126
+ expect(title.target).toBe("_self");
127
+
128
+ expect(
129
+ component.shadowRoot.querySelector("dx-image-and-label")
130
+ ).toBeNull();
131
+
132
+ const lengthSpan: HTMLSpanElement =
133
+ component.shadowRoot.querySelector(".card-extra-info > span");
134
+ expect(lengthSpan).not.toBeNull();
135
+ expect(lengthSpan.textContent).toBe(podcastMock.length);
136
+
137
+ expect(
138
+ component.shadowRoot.querySelector(".card-dot-separator")
139
+ ).not.toBeNull();
140
+
141
+ const formatedDate: FormattedDateTime =
142
+ component.shadowRoot.querySelector("dx-formatted-date-time");
143
+ expect(formatedDate).not.toBeNull();
144
+ expect(formatedDate.value).toBe("February 20, 2021");
145
+
146
+ const body: HTMLSpanElement = component.shadowRoot.querySelector(
147
+ ".card-container > span"
148
+ );
149
+ expect(body).not.toBeNull();
150
+ expect(body.textContent).toBe(podcastMock.body);
151
+
152
+ const topics: Topics =
153
+ component.shadowRoot.querySelector("dx-topics");
154
+ expect(topics).not.toBeNull();
155
+ expect(topics.topics).toEqual(podcastMock.topics);
156
+
157
+ expect(window.matchMedia).toBeCalledTimes(1);
158
+ expect(window.matchMedia).toBeCalledWith(MOBILE_QUERY);
159
+
160
+ return expect(component).toBeAccessible();
161
+ });
162
+ });
163
+
164
+ describe("mobile render", () => {
165
+ beforeAll(() => {
166
+ window.matchMedia = createMediaMock(true);
167
+ });
168
+
169
+ it("renders blog content type as mobile", () => {
170
+ const component = render(mockProps);
171
+
172
+ const container: HTMLElement =
173
+ component.shadowRoot.querySelector(".card-container");
174
+ expect(container).not.toBeNull();
175
+ expect(container.classList).toHaveLength(2);
176
+ expect(container.classList).toContain("card-mobile");
177
+
178
+ const typeBadge: TypeBadge =
179
+ component.shadowRoot.querySelector("dx-type-badge");
180
+ expect(typeBadge).not.toBeNull();
181
+ expect(typeBadge.size).toBe("small");
182
+
183
+ const imageAndLabel: ImageAndLabel =
184
+ component.shadowRoot.querySelector("dx-image-and-label");
185
+ expect(imageAndLabel).not.toBeNull();
186
+ expect(imageAndLabel.href).toBe(mockProps.authorHref);
187
+ expect(imageAndLabel.imgSrc).toBe("");
188
+ expect(imageAndLabel.label).toBe(mockProps.authorLabel);
189
+
190
+ return expect(component).toBeAccessible();
191
+ });
192
+ });
193
+ });
@@ -0,0 +1,21 @@
1
+ export default {
2
+ authorHref: "/",
3
+ authorImgSrc: "https://placekitten.com/300/300",
4
+ authorLabel: "Author Name",
5
+ contentType: "Blog",
6
+ href: "/awesome-blog-post",
7
+ date: "02/20/2021",
8
+ title: "Lightning Web Components for your Inbox",
9
+ body: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptate nulla inventore, quis enim dignissimos culpa assumenda non sed corporis similique praesentium ex quasi, cumque quia sequi rerum ab asperiores fugiat!",
10
+ topics: ["mobile", "performance"]
11
+ };
12
+
13
+ export const podcastMock = {
14
+ body: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptate nulla inventore, quis enim dignissimos culpa assumenda non sed corporis similique praesentium ex quasi, cumque quia sequi rerum ab asperiores fugiat!",
15
+ contentType: "Podcast",
16
+ date: "02/20/2021",
17
+ href: "/awesome-blog-post",
18
+ length: "40 min",
19
+ title: "Lightning Web Components for your Inbox",
20
+ topics: ["mobile", "performance"]
21
+ };
@@ -0,0 +1,49 @@
1
+ @import "helpers/text";
2
+
3
+ :host {
4
+ display: block;
5
+ }
6
+
7
+ dx-card-title:hover {
8
+ --dx-c-card-title-color: var(--dx-g-blue-vibrant-40);
9
+ }
10
+
11
+ dx-formatted-date-time {
12
+ /* Push down a little to align with dx-image-and-label */
13
+ margin-top: 3px;
14
+ }
15
+
16
+ .card-body {
17
+ --dx-c-max-lines: var(--dx-c-body-max-lines, 3);
18
+ }
19
+
20
+ .card-extra-info {
21
+ gap: var(--dx-g-spacing-sm);
22
+ align-items: center;
23
+ }
24
+
25
+ .card-container {
26
+ border-bottom: 1px solid var(--sds-g-gray-4);
27
+ padding-bottom: var(--dx-g-spacing-xl);
28
+ flex-direction: column;
29
+ gap: var(--dx-g-spacing-smd);
30
+ }
31
+
32
+ .card-container,
33
+ .card-extra-info {
34
+ display: flex;
35
+ }
36
+
37
+ .card-dot-separator {
38
+ --size: 5px;
39
+
40
+ height: var(--size);
41
+ width: var(--size);
42
+ background-color: var(--dx-g-text-label-color);
43
+ border-radius: 100%;
44
+ margin-bottom: 3px;
45
+ }
46
+
47
+ .card-mobile .card-body {
48
+ --dx-c-max-lines: var(--dx-c-body-max-lines, 2);
49
+ }
@@ -0,0 +1,37 @@
1
+ <template>
2
+ <div class={containerClass}>
3
+ <dx-type-badge
4
+ color={badgeColor}
5
+ type="content-type"
6
+ value={contentType}
7
+ size={badgeSize}
8
+ ></dx-type-badge>
9
+ <dx-card-title
10
+ if:true={title}
11
+ href={href}
12
+ target={target}
13
+ title={title}
14
+ ></dx-card-title>
15
+ <div class="card-extra-info dx-text-label-1-dark">
16
+ <dx-image-and-label
17
+ if:true={renderAuthor}
18
+ href={authorHref}
19
+ img-src={authorImage}
20
+ label={authorLabel}
21
+ ></dx-image-and-label>
22
+ <span if:true={renderLength}>{length}</span>
23
+ <span if:true={renderDot} class="card-dot-separator"></span>
24
+ <dx-formatted-date-time
25
+ if:true={date}
26
+ day="2-digit"
27
+ month="long"
28
+ year="numeric"
29
+ value={date}
30
+ ></dx-formatted-date-time>
31
+ </div>
32
+ <span if:true={body} class="card-body dx-text-body-2 dx-text-truncate">
33
+ {body}
34
+ </span>
35
+ <dx-topics if:true={hasTopics} topics={topics}></dx-topics>
36
+ </div>
37
+ </template>
@@ -0,0 +1,59 @@
1
+ import { html } from "lit-html";
2
+ import mockProps, { podcastMock } from "./__tests__/mockProps";
3
+
4
+ export default {
5
+ title: "dx/dx-card-expanded",
6
+ component: "dx-card-expanded",
7
+ argTypes: {
8
+ contentType: {
9
+ control: {
10
+ options: ["Blog", "Podcast"],
11
+ type: "select"
12
+ }
13
+ }
14
+ }
15
+ };
16
+
17
+ const style = html`
18
+ <style>
19
+ dx-card-expanded {
20
+ width: clamp(400px, 80%, 800px);
21
+ }
22
+ </style>
23
+ `;
24
+
25
+ type CardExpandedProps = {
26
+ authorHref?: string;
27
+ authorImgSrc?: string;
28
+ authorLabel?: string;
29
+ body: string;
30
+ contentType: "Blog" | "Podcast";
31
+ date: string;
32
+ href: string;
33
+ title: string;
34
+ length?: string;
35
+ topics: Array<string>;
36
+ };
37
+
38
+ const Template = (args: CardExpandedProps) => html`
39
+ ${style}
40
+ <dx-card-expanded
41
+ author-href="${args.authorHref || ""}"
42
+ author-img-src="${args.authorImgSrc || ""}"
43
+ author-label="${args.authorLabel || ""}"
44
+ body="${args.body}"
45
+ content-type=${args.contentType || "Blog"}
46
+ date="${args.date}"
47
+ href="${args.href}"
48
+ length="${args.length || ""}"
49
+ title="${args.title}"
50
+ topics=${JSON.stringify(args.topics)}
51
+ ></dx-card>-expanded
52
+ `;
53
+
54
+ export const BlogPost = (args: CardExpandedProps) => Template(args);
55
+
56
+ BlogPost.args = mockProps;
57
+
58
+ export const PodcastPost = (args: CardExpandedProps) => Template(args);
59
+ PodcastPost.args = podcastMock;
@@ -0,0 +1,45 @@
1
+ import { api } from "lwc";
2
+ import { ArchiveCard } from "utils/archiveCard";
3
+ import cx from "classnames";
4
+
5
+ export default class ExpandedCard extends ArchiveCard {
6
+ @api body!: string;
7
+ @api date: string | null = null;
8
+ @api topics: Array<string> = [];
9
+ @api authorLabel?: string | null = null;
10
+ @api authorHref?: string | null = null;
11
+ @api authorImgSrc?: string | null = null;
12
+ @api length?: string;
13
+
14
+ private get isBlog() {
15
+ return this.contentType === "Blog";
16
+ }
17
+
18
+ private get hasAuthor(): boolean {
19
+ return !!(this.authorImgSrc && this.authorLabel);
20
+ }
21
+
22
+ private get renderAuthor(): boolean {
23
+ return this.isBlog && this.hasAuthor;
24
+ }
25
+
26
+ private get renderLength(): boolean {
27
+ return !this.isBlog && !!this.length;
28
+ }
29
+
30
+ private get renderDot(): boolean {
31
+ return !!((this.renderAuthor || this.renderLength) && this.date);
32
+ }
33
+
34
+ private get hasTopics(): boolean {
35
+ return !!this.topics?.length;
36
+ }
37
+
38
+ private get authorImage(): string {
39
+ return !this.mobile && this.authorImgSrc ? this.authorImgSrc : "";
40
+ }
41
+
42
+ private get containerClass(): string {
43
+ return cx("card-container", this.mobile && "card-mobile");
44
+ }
45
+ }
@@ -1,12 +1,12 @@
1
- import MinimalCard from "dx/minimalCard";
1
+ import CardMinimal from "dx/cardMinimal";
2
2
  import TypeBadge from "dx/typeBadge";
3
3
  import { createRenderComponent } from "utils/tests";
4
4
  import { createMediaMock } from "utils/jest";
5
5
  import { blogPostMock, podcastEpisodeMock } from "./mockProps";
6
6
 
7
7
  const MOBILE_QUERY = "(max-width: 640px)";
8
- const TAG = "dx-minimal-card";
9
- const render = createRenderComponent(TAG, MinimalCard);
8
+ const TAG = "dx-card-minimal";
9
+ const render = createRenderComponent(TAG, CardMinimal);
10
10
 
11
11
  describe(TAG, () => {
12
12
  describe("default render", () => {
@@ -1,19 +1,18 @@
1
1
  @import "helpers/reset";
2
+ @import "helpers/text";
2
3
 
3
4
  :host {
4
5
  display: block;
5
6
  }
6
7
 
7
8
  a {
9
+ --dx-c-max-lines: var(--dx-c-title-max-lines, 3);
10
+
8
11
  color: var(--dx-g-blue-vibrant-50);
9
- display: -webkit-box;
10
12
  flex-grow: 1;
11
13
  font-family: var(--dx-g-font-sans);
12
14
  font-size: var(--dx-g-text-sm);
13
15
  font-weight: var(--dx-g-font-demi);
14
- overflow: hidden;
15
- -webkit-line-clamp: var(--dx-c-title-max-lines, 3);
16
- -webkit-box-orient: vertical;
17
16
  }
18
17
 
19
18
  a:hover {
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div class="card-container">
3
- <a href={href} target={target}>
3
+ <a class="dx-text-truncate" href={href} target={target}>
4
4
  <h4>{title}</h4>
5
5
  </a>
6
6
  <dx-type-badge
@@ -2,15 +2,15 @@ import { html } from "lit-html";
2
2
  import { blogPostMock, podcastEpisodeMock } from "./__tests__/mockProps";
3
3
 
4
4
  export default {
5
- title: "dx/dx-minimal-card",
6
- component: "dx-minimal-card"
5
+ title: "dx/dx-card-minimal",
6
+ component: "dx-card-minimal"
7
7
  };
8
8
 
9
9
  export const BlogPost = (args: { title: string; href: string }) => html`
10
- <dx-minimal-card
10
+ <dx-card-minimal
11
11
  title="${args.title}"
12
12
  href="${args.href}"
13
- ></dx-minimal-card>
13
+ ></dx-card-minimal>
14
14
  `;
15
15
 
16
16
  BlogPost.args = {
@@ -19,11 +19,11 @@ BlogPost.args = {
19
19
  };
20
20
 
21
21
  export const PodcastEpisode = (args: { title: string; href: string }) => html`
22
- <dx-minimal-card
22
+ <dx-card-minimal
23
23
  title="${args.title}"
24
24
  href="${args.href}"
25
25
  content-type="${podcastEpisodeMock.contentType}"
26
- ></dx-minimal-card>
26
+ ></dx-card-minimal>
27
27
  `;
28
28
 
29
29
  PodcastEpisode.args = {
@@ -0,0 +1,3 @@
1
+ import { ArchiveCard } from "utils/archiveCard";
2
+
3
+ export default class MinimalCard extends ArchiveCard {}
@@ -1,10 +1,22 @@
1
1
  <template>
2
2
  <div if:false={href} class="container">
3
- <img class={imgClass} src={imgSrc} alt={imgAlt} part="image" />
3
+ <img
4
+ if:true={imgSrc}
5
+ class={imgClass}
6
+ src={imgSrc}
7
+ alt={imgAlt}
8
+ part="image"
9
+ />
4
10
  <span class="dx-text-label-1-dark" part="label">{label}</span>
5
11
  </div>
6
12
  <a if:true={href} href={href} class="container">
7
- <img class={imgClass} src={imgSrc} alt={imgAlt} part="image" />
13
+ <img
14
+ if:true={imgSrc}
15
+ class={imgClass}
16
+ src={imgSrc}
17
+ alt={imgAlt}
18
+ part="image"
19
+ />
8
20
  <span class="dx-text-label-1-dark" part="label">{label}</span>
9
21
  </a>
10
22
  </template>
@@ -0,0 +1,3 @@
1
+ export default {
2
+ topics: ["mobile", "performance"]
3
+ };
@@ -0,0 +1,31 @@
1
+ import Topics from "dx/topics";
2
+ import { createRenderComponent } from "utils/tests";
3
+ import mockProps from "./mockProps";
4
+
5
+ const TAG = "dx-topics";
6
+ const render = createRenderComponent(TAG, Topics);
7
+ describe(TAG, () => {
8
+ it("renders", () => {
9
+ const component = render(mockProps);
10
+ expect(component.topics).toEqual(mockProps.topics);
11
+
12
+ const spans: Array<HTMLElement> =
13
+ component.shadowRoot.querySelectorAll("span");
14
+ expect(spans).toHaveLength(3);
15
+
16
+ const [expectedFirst, expectedSecond] = mockProps.topics;
17
+ const [first, dot, second] = spans;
18
+ expect(first.textContent).toBe(expectedFirst);
19
+ expect(second.textContent).toBe(expectedSecond);
20
+ expect(dot.classList).toContain("dot-separator");
21
+
22
+ return expect(component).toBeAccessible();
23
+ });
24
+
25
+ it("doesn't render any span if list is empty", () => {
26
+ const component = render();
27
+ const spans: Array<HTMLElement> =
28
+ component.shadowRoot.querySelectorAll("span");
29
+ expect(spans).toHaveLength(0);
30
+ });
31
+ });
@@ -0,0 +1,25 @@
1
+ :host {
2
+ display: block;
3
+ }
4
+
5
+ .container {
6
+ align-items: center;
7
+ color: var(--dx-g-blue-vibrant-20);
8
+ font-size: var(--dx-g-text-xs);
9
+ font-family: var(--dx-g-font-display);
10
+ font-weight: var(--dx-g-font-demi);
11
+ display: flex;
12
+ gap: var(--dx-g-spacing-sm);
13
+ letter-spacing: 1px;
14
+ text-transform: uppercase;
15
+ }
16
+
17
+ .dot-separator {
18
+ --size: 4px;
19
+
20
+ height: var(--size);
21
+ width: var(--size);
22
+ background-color: var(--dx-g-blue-vibrant-20);
23
+ border-radius: 100%;
24
+ margin-bottom: 3px;
25
+ }
@@ -0,0 +1,12 @@
1
+ <template>
2
+ <div class="container dx-text-label-1-dark">
3
+ <template for:each={normalizedTopics} for:item="topic">
4
+ <span key={topic.id}>{topic.label}</span>
5
+ <span
6
+ key={topic.id}
7
+ if:false={topic.isLast}
8
+ class="dot-separator"
9
+ ></span>
10
+ </template>
11
+ </div>
12
+ </template>
@@ -0,0 +1,25 @@
1
+ import { LightningElement, api } from "lwc";
2
+ import { normalizeToArray } from "utils/normalizers";
3
+
4
+ export default class Topics extends LightningElement {
5
+ private _topics: Array<string> = [];
6
+
7
+ @api
8
+ get topics() {
9
+ return this._topics;
10
+ }
11
+
12
+ set topics(value) {
13
+ this._topics = normalizeToArray(value);
14
+ }
15
+
16
+ get normalizedTopics() {
17
+ const { length } = this._topics;
18
+
19
+ return this._topics?.map((topic, index) => ({
20
+ id: topic,
21
+ label: topic,
22
+ isLast: index === length - 1
23
+ }));
24
+ }
25
+ }
@@ -154,3 +154,10 @@
154
154
  font-size: var(--dx-g-text-xs);
155
155
  line-height: 14px;
156
156
  }
157
+
158
+ .dx-text-truncate {
159
+ display: -webkit-box;
160
+ -webkit-line-clamp: var(--dx-c-max-lines, 6);
161
+ -webkit-box-orient: vertical;
162
+ overflow: hidden;
163
+ }
@@ -1,25 +1,21 @@
1
1
  import { LightningElement, api } from "lwc";
2
- import { Color, LinkTarget } from "typings/custom";
2
+ import { LinkTarget, Color } from "typings/custom";
3
3
 
4
4
  const MOBILE_QUERY = "(max-width: 640px)";
5
5
 
6
- export default class MinimalCard extends LightningElement {
6
+ export abstract class ArchiveCard extends LightningElement {
7
7
  @api contentType: "Blog" | "Podcast" = "Blog";
8
8
  @api href!: string;
9
- @api target: LinkTarget = "_self";
10
9
  @api title!: string;
10
+ @api target: LinkTarget = "_self";
11
11
 
12
12
  private matchMedia!: MediaQueryList;
13
- private mobile = false;
13
+ protected mobile = false;
14
14
 
15
- get badgeColor(): Color {
15
+ protected get badgeColor(): Color {
16
16
  return this.contentType === "Blog" ? "teal" : "blue";
17
17
  }
18
18
 
19
- get badgeSize(): string {
20
- return (this.mobile && "small") || "default";
21
- }
22
-
23
19
  connectedCallback(): void {
24
20
  this.matchMedia = window.matchMedia(MOBILE_QUERY);
25
21
  this.onMediaChange(this.matchMedia);
@@ -32,4 +28,8 @@ export default class MinimalCard extends LightningElement {
32
28
 
33
29
  private onMediaChange = (e: MediaQueryListEvent | MediaQueryList) =>
34
30
  (this.mobile = e.matches);
31
+
32
+ protected get badgeSize(): string {
33
+ return (this.mobile && "small") || "default";
34
+ }
35
35
  }
@@ -6,7 +6,7 @@ export const toJson = (value: string | object | null) =>
6
6
  export const normalizeToArray = (
7
7
  value: string[] | string | null,
8
8
  splitKey: string = ","
9
- ) => {
9
+ ): Array<string> => {
10
10
  if (!value) {
11
11
  return [];
12
12
  }
@@ -19,7 +19,7 @@ export const normalizeToArray = (
19
19
  }
20
20
  }
21
21
 
22
- return Array.isArray(value) ? value : [value];
22
+ return (Array.isArray(value) ? value : [value]) as Array<string>;
23
23
  };
24
24
 
25
25
  type WordpressPodcastEpisode = {