@arc-ui/community-components 3.15.0 → 3.17.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/.turbo/turbo-build.log +14 -14
- package/CHANGELOG.md +27 -0
- package/lib/ArticleCarousel/ArticleCarousel.cjs +1 -1
- package/lib/ArticleCarousel/ArticleCarousel.mjs +1 -1
- package/lib/ComparisonTable/ComparisonTable.cjs +317 -0
- package/lib/ComparisonTable/ComparisonTable.mjs +310 -0
- package/lib/ComparisonTable/styles.css +1 -0
- package/lib/CopyLead/CopyLead.cjs +1 -1
- package/lib/CopyLead/CopyLead.mjs +1 -1
- package/lib/FeaturePost/FeaturePost.cjs +13 -12
- package/lib/FeaturePost/FeaturePost.mjs +13 -12
- package/lib/FeaturePost/styles.css +1 -1
- package/lib/Highlights/Highlights.cjs +7 -7
- package/lib/Highlights/Highlights.mjs +7 -7
- package/lib/ProductCard/ProductCard.cjs +13 -35
- package/lib/ProductCard/ProductCard.mjs +12 -34
- package/lib/ProductNavigation/ProductNavigation.cjs +1 -1
- package/lib/ProductNavigation/ProductNavigation.mjs +1 -1
- package/lib/PromoListing/PromoListing.cjs +1 -1
- package/lib/PromoListing/PromoListing.mjs +1 -1
- package/lib/Summary/Summary.cjs +1 -1
- package/lib/Summary/Summary.mjs +1 -1
- package/lib/_shared/cjs/ProductCardPriceIncrease-BP39clav.cjs +38 -0
- package/lib/_shared/cjs/{index.es-C-y6aRIY.cjs → index.es-oEhoU2Bd.cjs} +1 -1
- package/lib/_shared/esm/ProductCardPriceIncrease-CJyG2wv-.mjs +35 -0
- package/lib/_shared/esm/{index.es-YxZ1vAot.mjs → index.es-DujqqZXe.mjs} +1 -1
- package/lib/index.cjs +508 -200
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +213 -22
- package/lib/index.d.mts +213 -22
- package/lib/index.mjs +503 -201
- package/lib/index.mjs.map +1 -1
- package/lib/styles.css +1 -1
- package/package.json +16 -16
- package/src/components/ComparisonTable/ComparisonTable.module.css +503 -0
- package/src/components/ComparisonTable/ComparisonTable.tsx +104 -0
- package/src/components/ComparisonTable/components/Cell/Cell.tsx +44 -0
- package/src/components/ComparisonTable/components/ControlCell/ControlCell.tsx +21 -0
- package/src/components/ComparisonTable/components/DesktopRow/DesktopRow.tsx +59 -0
- package/src/components/ComparisonTable/components/DesktopTable/DesktopTable.tsx +150 -0
- package/src/components/ComparisonTable/components/FeatureCell/FeatureCell.module.css +56 -0
- package/src/components/ComparisonTable/components/FeatureCell/FeatureCell.tsx +96 -0
- package/src/components/ComparisonTable/components/HeaderCell/HeaderCell.tsx +59 -0
- package/src/components/ComparisonTable/components/MobileRow/MobileRow.tsx +77 -0
- package/src/components/ComparisonTable/components/MobileTable/MobileTable.tsx +103 -0
- package/src/components/ComparisonTable/components/Section/Section.tsx +75 -0
- package/src/components/ComparisonTable/components/SectionToggle/SectionToggle.tsx +63 -0
- package/src/components/ComparisonTable/components/SubHeaderCell/SubHeaderCell.tsx +62 -0
- package/src/components/ComparisonTable/index.ts +22 -0
- package/src/components/ComparisonTable/types/comparison-column.ts +22 -0
- package/src/components/ComparisonTable/types/comparison-row.ts +23 -0
- package/src/components/ComparisonTable/types/comparison-section.ts +39 -0
- package/src/components/ComparisonTable/types/comparison-sub-row.ts +8 -0
- package/src/components/ComparisonTable/utils/group-rows.ts +69 -0
- package/src/components/ComparisonTable/utils/use-section-state.ts +45 -0
- package/src/components/FeaturePost/FeaturePost.module.css +24 -2
- package/src/components/FeaturePost/FeaturePost.tsx +26 -25
- package/src/components/FeaturePost/types/feature-post-cta-footer.ts +4 -3
- package/src/components/Highlights/Highlights.tsx +5 -32
- package/src/components/ProductCard/ProductCard.tsx +14 -14
- package/src/components/ProductCard/ProductCardPrice/ProductCardPrice.tsx +11 -3
- package/src/components/ProductCard/ProductCardPriceIncrease/ProductCardPriceIncrease.module.css +1 -2
- package/src/components/ProductCard/ProductCardPriceIncrease/ProductCardPriceIncrease.tsx +14 -3
- package/src/components/ProductCardGrid/ProductCardGrid.tsx +8 -8
- package/src/components/ProductCardGrid/components/CardGrid/CardGrid.tsx +6 -6
- package/src/components/index.ts +1 -0
- package/versions.json +1 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
|
|
4
|
+
import { Text } from "@arc-ui/components/Text";
|
|
5
|
+
|
|
6
|
+
import { FeatureCell } from "../FeatureCell/FeatureCell";
|
|
7
|
+
import styles from "../../ComparisonTable.module.css";
|
|
8
|
+
import { type ComparisonRow } from "../../types/comparison-row";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The same plans are highlighted in both layouts: plan index 0, 2, 4, ... (the
|
|
12
|
+
* 1st, 3rd, 5th plan). On desktop the leading feature column shifts these into
|
|
13
|
+
* the 2nd, 4th, ... visual grid columns, but it's the same plan as on mobile.
|
|
14
|
+
*/
|
|
15
|
+
const isPlanHighlighted = (planIndex: number) => planIndex % 2 === 0;
|
|
16
|
+
|
|
17
|
+
export const DesktopRow: React.FC<DesktopRowProps> = ({
|
|
18
|
+
row,
|
|
19
|
+
isLastRow = false,
|
|
20
|
+
}) => (
|
|
21
|
+
<div
|
|
22
|
+
role="row"
|
|
23
|
+
className={classNames(styles.desktopRow, {
|
|
24
|
+
[styles.isLastRow]: isLastRow,
|
|
25
|
+
})}
|
|
26
|
+
>
|
|
27
|
+
<div role="rowheader" className={styles.desktopFeatureCell}>
|
|
28
|
+
<FeatureCell
|
|
29
|
+
icon={row.feature.icon}
|
|
30
|
+
title={row.feature.title}
|
|
31
|
+
description={row.feature.description}
|
|
32
|
+
popoverContent={row.feature.popoverContent}
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
{row.cells.map((cell, cellIndex) => (
|
|
36
|
+
<div
|
|
37
|
+
key={`desktop-cell-${cellIndex}`}
|
|
38
|
+
role="cell"
|
|
39
|
+
className={classNames(styles.cell, {
|
|
40
|
+
[styles.highlighted]: isPlanHighlighted(cellIndex),
|
|
41
|
+
[styles.highlightedBottom]: isPlanHighlighted(cellIndex) && isLastRow,
|
|
42
|
+
})}
|
|
43
|
+
>
|
|
44
|
+
<Text>{cell}</Text>
|
|
45
|
+
</div>
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
export interface DesktopRowProps {
|
|
51
|
+
/**
|
|
52
|
+
* The row to render as a single desktop row: feature in the left-hand column, a value per plan column.
|
|
53
|
+
*/
|
|
54
|
+
row: ComparisonRow;
|
|
55
|
+
/**
|
|
56
|
+
* Whether this is the last row of the whole table, so its highlighted cells round their bottom corners.
|
|
57
|
+
*/
|
|
58
|
+
isLastRow?: boolean;
|
|
59
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
|
|
4
|
+
import { Heading } from "@arc-ui/components/Heading";
|
|
5
|
+
import { Surface } from "@arc-ui/components/Surface";
|
|
6
|
+
import { Text } from "@arc-ui/components/Text";
|
|
7
|
+
import { VisuallyHidden } from "@arc-ui/components/VisuallyHidden";
|
|
8
|
+
|
|
9
|
+
import { DesktopRow } from "../DesktopRow/DesktopRow";
|
|
10
|
+
import { Section } from "../Section/Section";
|
|
11
|
+
import { SubHeaderCell } from "../SubHeaderCell/SubHeaderCell";
|
|
12
|
+
import styles from "../../ComparisonTable.module.css";
|
|
13
|
+
import { type ComparisonColumn } from "../../types/comparison-column";
|
|
14
|
+
import { type ComparisonRow } from "../../types/comparison-row";
|
|
15
|
+
import { type ResolvedSection } from "../../types/comparison-section";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The same plans are highlighted in both layouts: plan index 0, 2, 4, ... (the
|
|
19
|
+
* 1st, 3rd, 5th plan). On desktop the leading feature column shifts these into
|
|
20
|
+
* the 2nd, 4th, ... visual grid columns, but it's the same plan as on mobile.
|
|
21
|
+
*/
|
|
22
|
+
const isPlanHighlighted = (planIndex: number) => planIndex % 2 === 0;
|
|
23
|
+
|
|
24
|
+
export const DesktopTable: React.FC<DesktopTableProps> = ({
|
|
25
|
+
columns,
|
|
26
|
+
rows,
|
|
27
|
+
sections,
|
|
28
|
+
controls,
|
|
29
|
+
hasStickyHeader,
|
|
30
|
+
className,
|
|
31
|
+
}) => {
|
|
32
|
+
const hasSubheader = columns.some((column) => column.subheader);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
role="table"
|
|
37
|
+
className={classNames(styles.desktopTable, className, {
|
|
38
|
+
[styles.desktopTableSticky]: hasStickyHeader,
|
|
39
|
+
})}
|
|
40
|
+
style={{ "--comparison-columns": columns.length } as React.CSSProperties}
|
|
41
|
+
>
|
|
42
|
+
{/* Header row: feature corner + plan headers */}
|
|
43
|
+
<div
|
|
44
|
+
role="row"
|
|
45
|
+
className={classNames(styles.desktopHeaderRow, {
|
|
46
|
+
[styles.stickyHeader]: hasStickyHeader,
|
|
47
|
+
})}
|
|
48
|
+
>
|
|
49
|
+
<div role="columnheader" className={styles.desktopFeatureHeader}>
|
|
50
|
+
<VisuallyHidden>Features</VisuallyHidden>
|
|
51
|
+
</div>
|
|
52
|
+
{columns.map((column, index) => (
|
|
53
|
+
<div
|
|
54
|
+
key={column.id}
|
|
55
|
+
role="columnheader"
|
|
56
|
+
className={classNames(styles.desktopHeaderCell, {
|
|
57
|
+
[styles.highlighted]: isPlanHighlighted(index),
|
|
58
|
+
[styles.highlightedTop]: isPlanHighlighted(index),
|
|
59
|
+
})}
|
|
60
|
+
>
|
|
61
|
+
{column.promoted && (
|
|
62
|
+
<div className={styles.headerCellPromoted}>
|
|
63
|
+
<Surface
|
|
64
|
+
background={"dark-brand-01"}
|
|
65
|
+
paddingVertical="16"
|
|
66
|
+
paddingHorizontal="24"
|
|
67
|
+
>
|
|
68
|
+
<VisuallyHidden>Promotional offer. </VisuallyHidden>
|
|
69
|
+
<Text size="s">{column.promoted}</Text>
|
|
70
|
+
</Surface>
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
<Heading size="m">{column.label}</Heading>
|
|
74
|
+
</div>
|
|
75
|
+
))}
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{/* Controls (in the feature column) + sub-header cells, on one row */}
|
|
79
|
+
{(controls || hasSubheader) && (
|
|
80
|
+
<div role="row" className={styles.desktopSubHeaderRow}>
|
|
81
|
+
<div
|
|
82
|
+
className={styles.desktopControlsCell}
|
|
83
|
+
aria-label="comparison table controls"
|
|
84
|
+
>
|
|
85
|
+
{controls}
|
|
86
|
+
</div>
|
|
87
|
+
{columns.map((column, index) => (
|
|
88
|
+
<SubHeaderCell
|
|
89
|
+
key={column.id}
|
|
90
|
+
columnIndex={isPlanHighlighted(index) ? 0 : 1}
|
|
91
|
+
{...(column.subheader ?? { price: { value: "" } })}
|
|
92
|
+
/>
|
|
93
|
+
))}
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
{/* Feature rows: feature label column + value cells */}
|
|
98
|
+
{rows.map((row, rowIndex) => (
|
|
99
|
+
<DesktopRow
|
|
100
|
+
key={`desktop-row-${rowIndex}`}
|
|
101
|
+
row={row}
|
|
102
|
+
isLastRow={sections.length === 0 && rowIndex === rows.length - 1}
|
|
103
|
+
/>
|
|
104
|
+
))}
|
|
105
|
+
|
|
106
|
+
{/* Collapsible sections */}
|
|
107
|
+
{sections.map((section, index) => (
|
|
108
|
+
<Section
|
|
109
|
+
key={section.id}
|
|
110
|
+
section={section}
|
|
111
|
+
isLastSection={index === sections.length - 1}
|
|
112
|
+
renderRow={(row, rowIndex, isLast) => (
|
|
113
|
+
<DesktopRow
|
|
114
|
+
key={`desktop-section-row-${rowIndex}`}
|
|
115
|
+
row={row}
|
|
116
|
+
isLastRow={isLast}
|
|
117
|
+
/>
|
|
118
|
+
)}
|
|
119
|
+
/>
|
|
120
|
+
))}
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export interface DesktopTableProps {
|
|
126
|
+
/**
|
|
127
|
+
* Column definitions driving the plan header cells and the number of value cells per row.
|
|
128
|
+
*/
|
|
129
|
+
columns: ComparisonColumn[];
|
|
130
|
+
/**
|
|
131
|
+
* Unsectioned rows, rendered before sections; each renders as a single row with the feature in the left-hand column and a value per plan column.
|
|
132
|
+
*/
|
|
133
|
+
rows: ComparisonRow[];
|
|
134
|
+
/**
|
|
135
|
+
* Resolved collapsible sections (empty ones already removed), rendered after the unsectioned rows.
|
|
136
|
+
*/
|
|
137
|
+
sections: ResolvedSection[];
|
|
138
|
+
/**
|
|
139
|
+
* Optional controls rendered in the left-hand feature column, alongside the sub-header row.
|
|
140
|
+
*/
|
|
141
|
+
controls?: React.ReactNode;
|
|
142
|
+
/**
|
|
143
|
+
* When `true`, the header row sticks to the top of the scroll container as the body scrolls.
|
|
144
|
+
*/
|
|
145
|
+
hasStickyHeader?: boolean;
|
|
146
|
+
/**
|
|
147
|
+
* Additional class applied to the root, used by the parent to toggle visibility per breakpoint.
|
|
148
|
+
*/
|
|
149
|
+
className?: string;
|
|
150
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
@import url("@arc-ui/tokens-arc/dist/custom-media.css");
|
|
2
|
+
|
|
3
|
+
.packageCell {
|
|
4
|
+
position: relative;
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: row;
|
|
7
|
+
max-width: 300px;
|
|
8
|
+
padding: 16px 24px 16px 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.packageCellPopoverDesktop {
|
|
12
|
+
display: none;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.packageCellTitle {
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-direction: column;
|
|
18
|
+
column-gap: var(--sem-space-gap-40);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.packageCellHeading {
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: row;
|
|
24
|
+
gap: 8px;
|
|
25
|
+
align-items: center;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@media (--arc-custom-media-min-width-l-px) {
|
|
29
|
+
.packageCellTitle {
|
|
30
|
+
flex-direction: row;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.packageCellHeading {
|
|
34
|
+
padding-right: 24px;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@media (--arc-custom-media-min-width-s-px) {
|
|
39
|
+
.packageCellPopoverDesktop {
|
|
40
|
+
display: block;
|
|
41
|
+
position: absolute;
|
|
42
|
+
top: 20px;
|
|
43
|
+
right: 24px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.packageCellPopoverMobile {
|
|
47
|
+
display: none;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.packageCellPopover {
|
|
52
|
+
display: flex;
|
|
53
|
+
height: 100%;
|
|
54
|
+
justify-content: flex-start;
|
|
55
|
+
padding-top: var(--sem-space-gap-40);
|
|
56
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { Icon } from "@arc-ui/components/Icon";
|
|
4
|
+
import { Heading } from "@arc-ui/components/Heading";
|
|
5
|
+
import { Text } from "@arc-ui/components/Text";
|
|
6
|
+
import { BtIconInfo } from "@arc-ui/icons/BtIconInfo";
|
|
7
|
+
import { Popover } from "@arc-ui/components/Popover";
|
|
8
|
+
|
|
9
|
+
import styles from "./FeatureCell.module.css";
|
|
10
|
+
|
|
11
|
+
export const FeatureCell: React.FC<FeatureCellProps> = ({
|
|
12
|
+
icon,
|
|
13
|
+
title,
|
|
14
|
+
description,
|
|
15
|
+
popoverContent,
|
|
16
|
+
}) => (
|
|
17
|
+
<div className={styles.packageCell}>
|
|
18
|
+
<div className={styles.packageCellTitle}>
|
|
19
|
+
<div style={{ width: 32 }}>
|
|
20
|
+
<Icon icon={icon} size={32} color="brand" />
|
|
21
|
+
</div>
|
|
22
|
+
<div className={styles.packageCellText}>
|
|
23
|
+
<div className={styles.packageCellHeading}>
|
|
24
|
+
<Heading size="xs" isWordWrap>
|
|
25
|
+
{title}
|
|
26
|
+
</Heading>
|
|
27
|
+
{popoverContent && (
|
|
28
|
+
<div className={styles.packageCellPopoverMobile}>
|
|
29
|
+
<Popover
|
|
30
|
+
arrow
|
|
31
|
+
background="white"
|
|
32
|
+
constrainToViewport
|
|
33
|
+
content={<Text>{popoverContent}</Text>}
|
|
34
|
+
maxWidth={300}
|
|
35
|
+
side="top"
|
|
36
|
+
>
|
|
37
|
+
<Icon
|
|
38
|
+
icon={BtIconInfo}
|
|
39
|
+
size={16}
|
|
40
|
+
color="brand"
|
|
41
|
+
label={`More information about ${title}`}
|
|
42
|
+
/>
|
|
43
|
+
</Popover>
|
|
44
|
+
</div>
|
|
45
|
+
)}{" "}
|
|
46
|
+
</div>
|
|
47
|
+
{description && (
|
|
48
|
+
<Text size="xs" tone="muted">
|
|
49
|
+
{description}
|
|
50
|
+
</Text>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
{popoverContent && (
|
|
55
|
+
<div className={styles.packageCellPopoverDesktop}>
|
|
56
|
+
<Popover
|
|
57
|
+
arrow
|
|
58
|
+
background="white"
|
|
59
|
+
constrainToViewport
|
|
60
|
+
content={<Text>{popoverContent}</Text>}
|
|
61
|
+
side="bottom"
|
|
62
|
+
align="start"
|
|
63
|
+
>
|
|
64
|
+
<Icon
|
|
65
|
+
icon={BtIconInfo}
|
|
66
|
+
size={16}
|
|
67
|
+
color="brand"
|
|
68
|
+
label={`More information about ${title}`}
|
|
69
|
+
/>
|
|
70
|
+
</Popover>
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
export interface FeatureCellProps {
|
|
77
|
+
/**
|
|
78
|
+
* Icon shown alongside the package name.
|
|
79
|
+
*/
|
|
80
|
+
icon: string;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Package / plan title.
|
|
84
|
+
*/
|
|
85
|
+
title: string;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Optional supporting copy shown beneath the title.
|
|
89
|
+
*/
|
|
90
|
+
description?: string;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Optional popover content shown when the user interacts with the info icon.
|
|
94
|
+
*/
|
|
95
|
+
popoverContent?: string | React.ReactNode;
|
|
96
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
|
|
4
|
+
import { filterAttrs } from "@arc-ui/community-utils/filter-attrs";
|
|
5
|
+
import { Heading } from "@arc-ui/components/Heading";
|
|
6
|
+
import { Surface } from "@arc-ui/components/Surface";
|
|
7
|
+
import { Text } from "@arc-ui/components/Text";
|
|
8
|
+
import { VisuallyHidden } from "@arc-ui/components/VisuallyHidden";
|
|
9
|
+
|
|
10
|
+
import styles from "../../ComparisonTable.module.css";
|
|
11
|
+
|
|
12
|
+
export const HeaderCell: React.FC<HeaderCellProps> = ({
|
|
13
|
+
children,
|
|
14
|
+
columnIndex = 0,
|
|
15
|
+
promoted,
|
|
16
|
+
...props
|
|
17
|
+
}) => (
|
|
18
|
+
<div
|
|
19
|
+
role="columnheader"
|
|
20
|
+
className={classNames(styles.headerCell, {
|
|
21
|
+
[styles.highlighted]: columnIndex % 2 === 0,
|
|
22
|
+
[styles.highlightedTop]: columnIndex % 2 === 0,
|
|
23
|
+
})}
|
|
24
|
+
{...filterAttrs(props)}
|
|
25
|
+
>
|
|
26
|
+
{promoted && (
|
|
27
|
+
<div className={styles.headerCellPromoted}>
|
|
28
|
+
<Surface
|
|
29
|
+
background={"dark-brand-01"}
|
|
30
|
+
paddingVertical="16"
|
|
31
|
+
paddingHorizontal="24"
|
|
32
|
+
>
|
|
33
|
+
<VisuallyHidden>Promotional offer. </VisuallyHidden>
|
|
34
|
+
<Text size="s">{promoted}</Text>
|
|
35
|
+
</Surface>
|
|
36
|
+
</div>
|
|
37
|
+
)}
|
|
38
|
+
{children != null && (
|
|
39
|
+
<Heading size="m" isWordWrap>
|
|
40
|
+
{children}
|
|
41
|
+
</Heading>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
export interface HeaderCellProps {
|
|
47
|
+
/**
|
|
48
|
+
* Content rendered inside the column header cell (typically a column label).
|
|
49
|
+
*/
|
|
50
|
+
children?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Zero-based index of the column this header cell belongs to. Used to highlight every other column, starting with the first.
|
|
53
|
+
*/
|
|
54
|
+
columnIndex?: number;
|
|
55
|
+
/**
|
|
56
|
+
* Optional promotional text displayed in a branded banner above the column label.
|
|
57
|
+
*/
|
|
58
|
+
promoted?: string;
|
|
59
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
|
|
4
|
+
import { filterAttrs } from "@arc-ui/community-utils/filter-attrs";
|
|
5
|
+
|
|
6
|
+
import { Cell } from "../Cell/Cell";
|
|
7
|
+
import { FeatureCell } from "../FeatureCell/FeatureCell";
|
|
8
|
+
import styles from "../../ComparisonTable.module.css";
|
|
9
|
+
import { type ComparisonRow } from "../../types/comparison-row";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Renders a single content row of cells for the mobile layout.
|
|
13
|
+
*/
|
|
14
|
+
const renderRow = (
|
|
15
|
+
cells: React.ReactNode[],
|
|
16
|
+
key: string,
|
|
17
|
+
isLastRow: boolean,
|
|
18
|
+
) => (
|
|
19
|
+
<div key={key} className={styles.row}>
|
|
20
|
+
{cells.map((cell, index) => (
|
|
21
|
+
<Cell key={`cell-${index}`} columnIndex={index} isLastRow={isLastRow}>
|
|
22
|
+
{cell}
|
|
23
|
+
</Cell>
|
|
24
|
+
))}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export const MobileRow: React.FC<MobileRowProps> = ({
|
|
29
|
+
row,
|
|
30
|
+
isLastGroup = false,
|
|
31
|
+
...props
|
|
32
|
+
}) => {
|
|
33
|
+
const subRows = row.subRows ?? [];
|
|
34
|
+
const hasSubRows = subRows.length > 0;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
role="row"
|
|
39
|
+
className={classNames(styles.rowGroup, {
|
|
40
|
+
[styles.isLastRow]: isLastGroup,
|
|
41
|
+
})}
|
|
42
|
+
{...filterAttrs(props)}
|
|
43
|
+
>
|
|
44
|
+
<div className={styles.featureRow}>
|
|
45
|
+
<div role="rowheader" className={styles.featureRowHeader}>
|
|
46
|
+
<div className={styles.featureRowHeaderContent}>
|
|
47
|
+
<FeatureCell
|
|
48
|
+
icon={row.feature.icon}
|
|
49
|
+
title={row.feature.title}
|
|
50
|
+
description={row.feature.description}
|
|
51
|
+
popoverContent={row.feature.popoverContent}
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
{renderRow(row.cells, "primary-row", isLastGroup && !hasSubRows)}
|
|
57
|
+
{subRows.map((subRow, index) =>
|
|
58
|
+
renderRow(
|
|
59
|
+
subRow.cells,
|
|
60
|
+
`row-${index}`,
|
|
61
|
+
isLastGroup && index === subRows.length - 1,
|
|
62
|
+
),
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export interface MobileRowProps {
|
|
69
|
+
/**
|
|
70
|
+
* The row to render: a full-width feature header followed by its content row(s).
|
|
71
|
+
*/
|
|
72
|
+
row: ComparisonRow;
|
|
73
|
+
/**
|
|
74
|
+
* Whether this is the last row in the table. Used so the final content row can round its highlighted cells.
|
|
75
|
+
*/
|
|
76
|
+
isLastGroup?: boolean;
|
|
77
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
|
|
4
|
+
import { HeaderCell } from "../HeaderCell/HeaderCell";
|
|
5
|
+
import { ControlCell } from "../ControlCell/ControlCell";
|
|
6
|
+
import { MobileRow } from "../MobileRow/MobileRow";
|
|
7
|
+
import { Section } from "../Section/Section";
|
|
8
|
+
import { SubHeaderCell } from "../SubHeaderCell/SubHeaderCell";
|
|
9
|
+
import styles from "../../ComparisonTable.module.css";
|
|
10
|
+
import { type ComparisonColumn } from "../../types/comparison-column";
|
|
11
|
+
import { type ComparisonRow } from "../../types/comparison-row";
|
|
12
|
+
import { type ResolvedSection } from "../../types/comparison-section";
|
|
13
|
+
|
|
14
|
+
export const MobileTable: React.FC<MobileTableProps> = ({
|
|
15
|
+
columns,
|
|
16
|
+
rows,
|
|
17
|
+
sections,
|
|
18
|
+
controls,
|
|
19
|
+
className,
|
|
20
|
+
}) => (
|
|
21
|
+
<div
|
|
22
|
+
role="table"
|
|
23
|
+
className={classNames(styles.table, className)}
|
|
24
|
+
style={{ "--comparison-columns": columns.length } as React.CSSProperties}
|
|
25
|
+
>
|
|
26
|
+
<div role="row" className={styles.headerRow}>
|
|
27
|
+
{columns.map((column, index) => (
|
|
28
|
+
<HeaderCell
|
|
29
|
+
key={column.id}
|
|
30
|
+
columnIndex={index}
|
|
31
|
+
promoted={column.promoted}
|
|
32
|
+
>
|
|
33
|
+
{column.label}
|
|
34
|
+
</HeaderCell>
|
|
35
|
+
))}
|
|
36
|
+
</div>
|
|
37
|
+
{controls && (
|
|
38
|
+
<div
|
|
39
|
+
role="row"
|
|
40
|
+
className={styles.controlRow}
|
|
41
|
+
aria-label="comparison table controls"
|
|
42
|
+
>
|
|
43
|
+
<ControlCell>{controls}</ControlCell>
|
|
44
|
+
</div>
|
|
45
|
+
)}
|
|
46
|
+
{columns.some((column) => column.subheader) && (
|
|
47
|
+
<div role="row" className={styles.subHeaderRow}>
|
|
48
|
+
{columns.map((column, index) => (
|
|
49
|
+
<SubHeaderCell
|
|
50
|
+
key={column.id}
|
|
51
|
+
columnIndex={index}
|
|
52
|
+
{...(column.subheader ?? { price: { value: "" } })}
|
|
53
|
+
/>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
|
|
58
|
+
{rows.map((row, index) => (
|
|
59
|
+
<MobileRow
|
|
60
|
+
key={`row-group-${index}`}
|
|
61
|
+
row={row}
|
|
62
|
+
isLastGroup={sections.length === 0 && index === rows.length - 1}
|
|
63
|
+
/>
|
|
64
|
+
))}
|
|
65
|
+
{sections.map((section, index) => (
|
|
66
|
+
<Section
|
|
67
|
+
key={section.id}
|
|
68
|
+
section={section}
|
|
69
|
+
isLastSection={index === sections.length - 1}
|
|
70
|
+
renderRow={(row, rowIndex, isLast) => (
|
|
71
|
+
<MobileRow
|
|
72
|
+
key={`section-group-${rowIndex}`}
|
|
73
|
+
row={row}
|
|
74
|
+
isLastGroup={isLast}
|
|
75
|
+
/>
|
|
76
|
+
)}
|
|
77
|
+
/>
|
|
78
|
+
))}
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
export interface MobileTableProps {
|
|
83
|
+
/**
|
|
84
|
+
* Column definitions driving the header cells and the number of content cells per row.
|
|
85
|
+
*/
|
|
86
|
+
columns: ComparisonColumn[];
|
|
87
|
+
/**
|
|
88
|
+
* Unsectioned rows, rendered before sections; each row has a full-width feature header followed by its content row(s).
|
|
89
|
+
*/
|
|
90
|
+
rows: ComparisonRow[];
|
|
91
|
+
/**
|
|
92
|
+
* Resolved collapsible sections (empty ones already removed), rendered after the unsectioned rows.
|
|
93
|
+
*/
|
|
94
|
+
sections: ResolvedSection[];
|
|
95
|
+
/**
|
|
96
|
+
* Optional controls rendered in a single full-width cell beneath the header, spanning all columns.
|
|
97
|
+
*/
|
|
98
|
+
controls?: React.ReactNode;
|
|
99
|
+
/**
|
|
100
|
+
* Additional class applied to the root, used by the parent to toggle visibility per breakpoint.
|
|
101
|
+
*/
|
|
102
|
+
className?: string;
|
|
103
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React, { useId } from "react";
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
|
|
4
|
+
import { SectionToggle } from "../SectionToggle/SectionToggle";
|
|
5
|
+
import styles from "../../ComparisonTable.module.css";
|
|
6
|
+
import { type ResolvedSection } from "../../types/comparison-section";
|
|
7
|
+
import { type ComparisonRow } from "../../types/comparison-row";
|
|
8
|
+
import { useSectionState } from "../../utils/use-section-state";
|
|
9
|
+
|
|
10
|
+
export const Section: React.FC<SectionProps> = ({
|
|
11
|
+
section,
|
|
12
|
+
isLastSection,
|
|
13
|
+
renderRow,
|
|
14
|
+
}) => {
|
|
15
|
+
const regionId = useId();
|
|
16
|
+
const { isOpen, toggle } = useSectionState({
|
|
17
|
+
defaultOpen: section.defaultOpen,
|
|
18
|
+
open: section.open,
|
|
19
|
+
onOpenChange: section.onOpenChange,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const lastRowIndex = section.rows.length - 1;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div role="rowgroup">
|
|
26
|
+
<div
|
|
27
|
+
role="row"
|
|
28
|
+
className={classNames(styles.sectionToggleRow, {
|
|
29
|
+
[styles.isLastRow]: isLastSection === true && !isOpen,
|
|
30
|
+
})}
|
|
31
|
+
>
|
|
32
|
+
<SectionToggle
|
|
33
|
+
heading={section.heading}
|
|
34
|
+
headingLevel={section.headingLevel}
|
|
35
|
+
isOpen={isOpen}
|
|
36
|
+
onToggle={toggle}
|
|
37
|
+
controlsId={regionId}
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
{isOpen && (
|
|
41
|
+
<div id={regionId} className={styles.sectionContent}>
|
|
42
|
+
{section.rows.map((row, index) =>
|
|
43
|
+
renderRow(
|
|
44
|
+
row,
|
|
45
|
+
index,
|
|
46
|
+
isLastSection === true && index === lastRowIndex,
|
|
47
|
+
),
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export interface SectionProps {
|
|
56
|
+
/**
|
|
57
|
+
* The section to render: a toggle row and its collapsible rows.
|
|
58
|
+
*/
|
|
59
|
+
section: ResolvedSection;
|
|
60
|
+
/**
|
|
61
|
+
* Whether this is the last section in the table. When open, its final row
|
|
62
|
+
* rounds its highlighted cells; when closed, its toggle row is the table's
|
|
63
|
+
* last row and drops its bottom divider.
|
|
64
|
+
*/
|
|
65
|
+
isLastSection?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Render callback for each row in the section. The caller decides the
|
|
68
|
+
* layout-specific row component (e.g. `MobileRow` or `DesktopRow`).
|
|
69
|
+
*/
|
|
70
|
+
renderRow: (
|
|
71
|
+
row: ComparisonRow,
|
|
72
|
+
index: number,
|
|
73
|
+
isLast: boolean,
|
|
74
|
+
) => React.ReactNode;
|
|
75
|
+
}
|