@thewhatmatters/wmds 0.4.7 → 0.4.9
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.md +61 -0
- package/CONSUMING.md +4 -1
- package/bin/wmds-check.mjs +8 -4
- package/dist/components/atoms/TextLink/TextLink.d.ts +9 -3
- package/dist/components/atoms/TextLink/TextLink.d.ts.map +1 -1
- package/dist/components/atoms/TextLink/TextLink.js +22 -17
- package/dist/components/molecules/IndexList/IndexList.d.ts +6 -3
- package/dist/components/molecules/IndexList/IndexList.d.ts.map +1 -1
- package/dist/components/molecules/IndexList/IndexList.js +22 -21
- package/dist/components/molecules/LinkTile/LinkTile.d.ts +45 -0
- package/dist/components/molecules/LinkTile/LinkTile.d.ts.map +1 -0
- package/dist/components/molecules/LinkTile/LinkTile.js +121 -0
- package/dist/components/molecules/LinkTile/linkTileStyles.d.ts +37 -0
- package/dist/components/molecules/LinkTile/linkTileStyles.d.ts.map +1 -0
- package/dist/components/molecules/LinkTile/linkTileStyles.js +8 -0
- package/dist/components/molecules/TileGrid/TileGrid.d.ts +42 -0
- package/dist/components/molecules/TileGrid/TileGrid.d.ts.map +1 -0
- package/dist/components/molecules/TileGrid/TileGrid.js +94 -0
- package/dist/components/molecules/TileGrid/tileGridMath.d.ts +8 -0
- package/dist/components/molecules/TileGrid/tileGridMath.d.ts.map +1 -0
- package/dist/components/molecules/TileGrid/tileGridMath.js +20 -0
- package/dist/components/molecules/TileGrid/tileGridStyles.d.ts +33 -0
- package/dist/components/molecules/TileGrid/tileGridStyles.d.ts.map +1 -0
- package/dist/components/molecules/TileGrid/tileGridStyles.js +16 -0
- package/dist/components/organisms/Dialog/Dialog.d.ts +12 -2
- package/dist/components/organisms/Dialog/Dialog.d.ts.map +1 -1
- package/dist/components/organisms/Dialog/Dialog.js +46 -44
- package/dist/components/organisms/Dialog/OverlayPanelHeader.d.ts +3 -1
- package/dist/components/organisms/Dialog/OverlayPanelHeader.d.ts.map +1 -1
- package/dist/components/organisms/Dialog/OverlayPanelHeader.js +20 -17
- package/dist/components/organisms/Dialog/dialogStyles.d.ts +5 -1
- package/dist/components/organisms/Dialog/dialogStyles.d.ts.map +1 -1
- package/dist/components/organisms/Dialog/dialogStyles.js +13 -11
- package/dist/components/organisms/FilterPanel/FilterPanel.d.ts +54 -0
- package/dist/components/organisms/FilterPanel/FilterPanel.d.ts.map +1 -0
- package/dist/components/organisms/FilterPanel/FilterPanel.js +137 -0
- package/dist/components/organisms/FilterPanel/filterSelection.d.ts +44 -0
- package/dist/components/organisms/FilterPanel/filterSelection.d.ts.map +1 -0
- package/dist/components/organisms/FilterPanel/filterSelection.js +35 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +73 -67
- package/dist/lib/compositionAudit.d.ts.map +1 -1
- package/dist/lib/typography.d.ts +2 -2
- package/dist/lib/typography.d.ts.map +1 -1
- package/dist/lib/typography.js +11 -0
- package/dist/styles.css +1 -1
- package/dist/typography.css +16 -0
- package/docs/components.md +10 -7
- package/docs/exports.json +43 -2
- package/docs/patterns/components-accordion--flush.tsx +1 -1
- package/docs/patterns/components-avatar--dropdown-row.tsx +1 -1
- package/docs/patterns/components-badge--outline-tags.tsx +1 -1
- package/docs/patterns/components-badge--with-avatar.tsx +1 -1
- package/docs/patterns/components-breadcrumb--editorial.tsx +1 -1
- package/docs/patterns/components-breadcrumb--long-path.tsx +1 -1
- package/docs/patterns/components-breadcrumb--path.tsx +1 -1
- package/docs/patterns/components-breadcrumb--router-links.tsx +1 -1
- package/docs/patterns/components-button-button--copy-button.tsx +1 -1
- package/docs/patterns/components-button-button--destructive-action.tsx +1 -1
- package/docs/patterns/components-button-button--external-link.tsx +1 -1
- package/docs/patterns/components-button-button--ghost-action.tsx +1 -1
- package/docs/patterns/components-button-button--inverse-action.tsx +1 -1
- package/docs/patterns/components-button-button--link-render.tsx +1 -1
- package/docs/patterns/components-button-button--multi-state-badge.tsx +1 -1
- package/docs/patterns/components-button-button--outline-mono.tsx +1 -1
- package/docs/patterns/components-button-button--primary-action.tsx +1 -1
- package/docs/patterns/components-button-button--row-hug.tsx +1 -1
- package/docs/patterns/components-button-button--row-layout.tsx +1 -1
- package/docs/patterns/components-button-button--secondary-action.tsx +1 -1
- package/docs/patterns/components-button-button--suggestion-pills.tsx +1 -1
- package/docs/patterns/components-button-button--with-count.tsx +1 -1
- package/docs/patterns/components-button-button--with-icon.tsx +1 -1
- package/docs/patterns/components-button-floatingactionbutton--vertical-actions.tsx +1 -1
- package/docs/patterns/components-calembed--pattern-skip-in-footer.tsx +1 -1
- package/docs/patterns/components-card-card--body-slot-occupancy-history.tsx +1 -1
- package/docs/patterns/components-card-card--headerless-media.tsx +1 -1
- package/docs/patterns/components-card-card--outlined-layout.tsx +1 -1
- package/docs/patterns/components-card-card--pinned-height.tsx +1 -1
- package/docs/patterns/components-card-selectablecard--selectable-cards-pattern.tsx +1 -1
- package/docs/patterns/components-carousel--image-carousel.tsx +1 -1
- package/docs/patterns/components-chart--audience-activity-heatmap.tsx +1 -1
- package/docs/patterns/components-chart--cartesian-no-data-gaps.tsx +1 -1
- package/docs/patterns/components-chart--occupancy-history-in-card.tsx +1 -1
- package/docs/patterns/components-chart--occupancy-in-card.tsx +1 -1
- package/docs/patterns/components-chart--post-reach-distribution.tsx +1 -1
- package/docs/patterns/components-chart--ranked-audience-breakdown.tsx +1 -1
- package/docs/patterns/components-chart--unit-grid-composition.tsx +1 -1
- package/docs/patterns/components-chatdock--pattern-chat-dock.tsx +1 -1
- package/docs/patterns/components-chatdock--pattern-start-project-gate.tsx +1 -1
- package/docs/patterns/components-chatqa--pattern-qa-with-reply.tsx +1 -1
- package/docs/patterns/components-checkbox-checkbox--default.tsx +1 -1
- package/docs/patterns/components-checkbox-checkbox--with-count.tsx +1 -1
- package/docs/patterns/components-checkbox-checkbox--with-description.tsx +1 -1
- package/docs/patterns/components-checkbox-checkboxgroup--default.tsx +1 -1
- package/docs/patterns/components-checkbox-checkboxgroup--option-counts.tsx +1 -1
- package/docs/patterns/components-descriptionlist--details.tsx +1 -1
- package/docs/patterns/components-descriptionlist--post-metadata.tsx +1 -1
- package/docs/patterns/components-dialog--alert-dialog-destructive.tsx +1 -1
- package/docs/patterns/components-dialog--default-dialog.tsx +1 -1
- package/docs/patterns/components-dialog--form-dialog.tsx +1 -1
- package/docs/patterns/components-displaycontrols--page-utilities.tsx +1 -1
- package/docs/patterns/components-field--horizontal-input.tsx +1 -1
- package/docs/patterns/components-field--horizontal-text-area.tsx +1 -1
- package/docs/patterns/components-field--multi-control.tsx +1 -1
- package/docs/patterns/components-filterpanel--panel.tsx +55 -0
- package/docs/patterns/components-footerreveal--marketing-hero-pattern.tsx +1 -1
- package/docs/patterns/components-footerreveal--marketing-hero-ruled-pattern.tsx +1 -1
- package/docs/patterns/components-footerreveal--marketing-hero-with-gallery-intro.tsx +1 -1
- package/docs/patterns/components-footerreveal--marketing-page-pattern.tsx +1 -1
- package/docs/patterns/components-footerreveal--ruled-grid-footer-pattern.tsx +1 -1
- package/docs/patterns/components-footerreveal--sequenced-marketing-hero.tsx +1 -1
- package/docs/patterns/components-herointro--two-line-intro.tsx +1 -1
- package/docs/patterns/components-herotilestack--marketing-hero-pattern.tsx +1 -1
- package/docs/patterns/components-herotilestack--marketing-hero-text-sequence.tsx +1 -1
- package/docs/patterns/components-herotilestack--marketing-hero-with-gallery-intro.tsx +1 -1
- package/docs/patterns/components-indexlist--post-index.tsx +1 -1
- package/docs/patterns/components-intakeconfirmation--booked-pattern.tsx +1 -1
- package/docs/patterns/components-intakeconfirmation--emailed-pattern.tsx +1 -1
- package/docs/patterns/components-intakeform--about-you-pattern.tsx +1 -1
- package/docs/patterns/components-intakeform--short-form-pattern.tsx +1 -1
- package/docs/patterns/components-intakemodal--intake-modal-pattern.tsx +1 -1
- package/docs/patterns/components-kbd--command-row.tsx +1 -1
- package/docs/patterns/components-kbd--numbered-choices.tsx +1 -1
- package/docs/patterns/components-kbd--shortcut.tsx +1 -1
- package/docs/patterns/components-linktile--resource-tile.tsx +33 -0
- package/docs/patterns/components-linktile--router-link.tsx +25 -0
- package/docs/patterns/components-moremenu--action-menu.tsx +1 -1
- package/docs/patterns/components-moremenu--in-card-header.tsx +1 -1
- package/docs/patterns/components-navlist--side-nav-settings.tsx +1 -1
- package/docs/patterns/components-pageheader--app-header-pattern.tsx +1 -1
- package/docs/patterns/components-pageheader--page-section-header.tsx +1 -1
- package/docs/patterns/components-pageheader--toolbar-header.tsx +1 -1
- package/docs/patterns/components-panel--end-detail-rail.tsx +1 -1
- package/docs/patterns/components-pillgroup--pill-group-pattern.tsx +1 -1
- package/docs/patterns/components-promptbar--pattern-prompt-bar.tsx +1 -1
- package/docs/patterns/components-prose--article-body.tsx +1 -1
- package/docs/patterns/components-radio-radiogroup--default.tsx +1 -1
- package/docs/patterns/components-scrollhorizontal--project-gallery-pattern.tsx +1 -1
- package/docs/patterns/components-scrollhorizontal--with-intro.tsx +1 -1
- package/docs/patterns/components-sectioncaption--column-caption.tsx +1 -1
- package/docs/patterns/components-sectioncaption--with-action.tsx +1 -1
- package/docs/patterns/components-sheet--bottom-sheet.tsx +1 -1
- package/docs/patterns/components-sheet--end-sheet.tsx +1 -1
- package/docs/patterns/components-sitenav--brand-and-cta.tsx +1 -1
- package/docs/patterns/components-sitenav--centered-links.tsx +1 -1
- package/docs/patterns/components-sitenav--compact.tsx +1 -1
- package/docs/patterns/components-sitenav--marketing-header.tsx +1 -1
- package/docs/patterns/components-sitenav--mega-menu.tsx +1 -1
- package/docs/patterns/components-stat--insights-row.tsx +1 -1
- package/docs/patterns/components-stepprogress--step-progress-pattern.tsx +1 -1
- package/docs/patterns/components-switch--default.tsx +1 -1
- package/docs/patterns/components-switch--settings-row.tsx +1 -1
- package/docs/patterns/components-tab--settings-pages.tsx +1 -1
- package/docs/patterns/components-taskrows--action-details.tsx +1 -1
- package/docs/patterns/components-taskrows--capsules.tsx +1 -1
- package/docs/patterns/components-taskrows--card-with-status-rows.tsx +1 -1
- package/docs/patterns/components-taskrows--controlled-expand.tsx +1 -1
- package/docs/patterns/components-taskrows--custom-slots.tsx +1 -1
- package/docs/patterns/components-taskrows--status-list.tsx +1 -1
- package/docs/patterns/components-taskrows--tag-chips.tsx +1 -1
- package/docs/patterns/components-textlink--external-destination.tsx +1 -1
- package/docs/patterns/components-textlink--inline-prose.tsx +1 -1
- package/docs/patterns/components-textlink--quiet-title.tsx +1 -1
- package/docs/patterns/components-textlink--supporting-copy.tsx +1 -1
- package/docs/patterns/components-textsequence--default.tsx +1 -1
- package/docs/patterns/components-textsequence--reduced-motion.tsx +1 -1
- package/docs/patterns/components-textsequence--shapes.tsx +1 -1
- package/docs/patterns/components-tilegrid--masonry.tsx +44 -0
- package/docs/patterns/components-toast--stacked-notifications.tsx +1 -1
- package/docs/patterns/components-tooltip--controlled.tsx +1 -1
- package/docs/patterns/components-tooltip--disabled-control.tsx +1 -1
- package/docs/patterns/components-tooltip--icon-action.tsx +1 -1
- package/docs/patterns/components-tooltip--keyboard-shortcut.tsx +1 -1
- package/docs/patterns/components-tooltip--placement.tsx +1 -1
- package/docs/patterns/foundations-scroll-fade--horizontal-rail.tsx +1 -1
- package/docs/patterns/foundations-scroll-fade--overflowing-list.tsx +1 -1
- package/docs/patterns/guides-chart-explorations--comparison-gallery.tsx +1 -1
- package/docs/patterns/guides-filter-panel--filtered-grid.tsx +182 -0
- package/docs/patterns/guides-filter-panel--filtered-index.tsx +68 -153
- package/docs/patterns/guides-overlay-flows--notification-preferences.tsx +1 -1
- package/docs/patterns/guides-post-page--post-page.tsx +117 -56
- package/docs/patterns/guides-profile-typography--editorial-profile.tsx +1 -1
- package/docs/patterns/guides-resource-detail--detail.tsx +270 -0
- package/docs/patterns/index.json +49 -1
- package/package.json +1 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// @thewhatmatters/wmds@0.4.9 · Pattern — tile grid
|
|
2
|
+
// Storybook: Components/TileGrid → Pattern — tile grid (?path=/story/components-tilegrid--masonry)
|
|
3
|
+
// Show code — copy verbatim and keep this header; upgrades find pasted patterns by it.
|
|
4
|
+
|
|
5
|
+
import { LinkTile, TileGrid } from "@thewhatmatters/wmds";
|
|
6
|
+
|
|
7
|
+
export interface ResourceTileItem {
|
|
8
|
+
id: string;
|
|
9
|
+
title: string;
|
|
10
|
+
href: string;
|
|
11
|
+
/** Another site: the tile opens it in a new tab. */
|
|
12
|
+
external?: boolean;
|
|
13
|
+
/** One line under the title, for example the domain. */
|
|
14
|
+
meta?: string;
|
|
15
|
+
/** The preview. Its width and height hold the tile's space before the image loads. */
|
|
16
|
+
image: { src: string; alt: string; width: number; height: number };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ResourceGrid({ label, items }: { label: string; items: ResourceTileItem[] }) {
|
|
20
|
+
return (
|
|
21
|
+
<TileGrid aria-label={label} empty={<p className="type-body text-muted">Nothing here yet.</p>}>
|
|
22
|
+
{items.map((item) => (
|
|
23
|
+
<TileGrid.Item key={item.id}>
|
|
24
|
+
<LinkTile
|
|
25
|
+
title={item.title}
|
|
26
|
+
href={item.href}
|
|
27
|
+
external={item.external}
|
|
28
|
+
meta={item.meta}
|
|
29
|
+
media={
|
|
30
|
+
<img
|
|
31
|
+
src={item.image.src}
|
|
32
|
+
alt={item.image.alt}
|
|
33
|
+
width={item.image.width}
|
|
34
|
+
height={item.image.height}
|
|
35
|
+
loading="lazy"
|
|
36
|
+
decoding="async"
|
|
37
|
+
/>
|
|
38
|
+
}
|
|
39
|
+
/>
|
|
40
|
+
</TileGrid.Item>
|
|
41
|
+
))}
|
|
42
|
+
</TileGrid>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @thewhatmatters/wmds@0.4.
|
|
1
|
+
// @thewhatmatters/wmds@0.4.9 · Pattern — stacked notifications
|
|
2
2
|
// Storybook: Components/Toast → Pattern — stacked notifications (?path=/story/components-toast--stacked-notifications)
|
|
3
3
|
// Show code — copy verbatim and keep this header; upgrades find pasted patterns by it.
|
|
4
4
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @thewhatmatters/wmds@0.4.
|
|
1
|
+
// @thewhatmatters/wmds@0.4.9 · Pattern — controlled state
|
|
2
2
|
// Storybook: Components/Tooltip → Pattern — controlled state (?path=/story/components-tooltip--controlled)
|
|
3
3
|
// Show code — copy verbatim and keep this header; upgrades find pasted patterns by it.
|
|
4
4
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @thewhatmatters/wmds@0.4.
|
|
1
|
+
// @thewhatmatters/wmds@0.4.9 · Pattern — disabled control
|
|
2
2
|
// Storybook: Components/Tooltip → Pattern — disabled control (?path=/story/components-tooltip--disabled-control)
|
|
3
3
|
// Show code — copy verbatim and keep this header; upgrades find pasted patterns by it.
|
|
4
4
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @thewhatmatters/wmds@0.4.
|
|
1
|
+
// @thewhatmatters/wmds@0.4.9 · Pattern — icon-only action
|
|
2
2
|
// Storybook: Components/Tooltip → Pattern — icon-only action (?path=/story/components-tooltip--icon-action)
|
|
3
3
|
// Show code — copy verbatim and keep this header; upgrades find pasted patterns by it.
|
|
4
4
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @thewhatmatters/wmds@0.4.
|
|
1
|
+
// @thewhatmatters/wmds@0.4.9 · Pattern — keyboard shortcut
|
|
2
2
|
// Storybook: Components/Tooltip → Pattern — keyboard shortcut (?path=/story/components-tooltip--keyboard-shortcut)
|
|
3
3
|
// Show code — copy verbatim and keep this header; upgrades find pasted patterns by it.
|
|
4
4
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @thewhatmatters/wmds@0.4.
|
|
1
|
+
// @thewhatmatters/wmds@0.4.9 · Pattern — placement
|
|
2
2
|
// Storybook: Components/Tooltip → Pattern — placement (?path=/story/components-tooltip--placement)
|
|
3
3
|
// Show code — copy verbatim and keep this header; upgrades find pasted patterns by it.
|
|
4
4
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @thewhatmatters/wmds@0.4.
|
|
1
|
+
// @thewhatmatters/wmds@0.4.9 · Pattern — horizontal rail
|
|
2
2
|
// Storybook: Foundations/Scroll fade → Pattern — horizontal rail (?path=/story/foundations-scroll-fade--horizontal-rail)
|
|
3
3
|
// Show code — copy verbatim and keep this header; upgrades find pasted patterns by it.
|
|
4
4
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @thewhatmatters/wmds@0.4.
|
|
1
|
+
// @thewhatmatters/wmds@0.4.9 · Pattern — overflowing list
|
|
2
2
|
// Storybook: Foundations/Scroll fade → Pattern — overflowing list (?path=/story/foundations-scroll-fade--overflowing-list)
|
|
3
3
|
// Show code — copy verbatim and keep this header; upgrades find pasted patterns by it.
|
|
4
4
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @thewhatmatters/wmds@0.4.
|
|
1
|
+
// @thewhatmatters/wmds@0.4.9 · Pattern — chart comparison gallery
|
|
2
2
|
// Storybook: Guides/Chart explorations → Pattern — chart comparison gallery (?path=/story/guides-chart-explorations--comparison-gallery)
|
|
3
3
|
// Show code — copy verbatim and keep this header; upgrades find pasted patterns by it.
|
|
4
4
|
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// @thewhatmatters/wmds@0.4.9 · Pattern — filtered grid
|
|
2
|
+
// Storybook: Guides/Filter panel → Pattern — filtered grid (?path=/story/guides-filter-panel--filtered-grid)
|
|
3
|
+
// Show code — copy verbatim and keep this header; upgrades find pasted patterns by it.
|
|
4
|
+
|
|
5
|
+
import { useId, type ReactElement, type ReactNode } from "react";
|
|
6
|
+
import {
|
|
7
|
+
Badge,
|
|
8
|
+
FilterPanel,
|
|
9
|
+
LinkTile,
|
|
10
|
+
SectionCaption,
|
|
11
|
+
TileGrid,
|
|
12
|
+
countFilterOptions,
|
|
13
|
+
matchesFilterSelection,
|
|
14
|
+
useFilterSelection,
|
|
15
|
+
type FilterFacets,
|
|
16
|
+
type FilterPanelGroup,
|
|
17
|
+
type FilterSelection,
|
|
18
|
+
type TileGridColumns,
|
|
19
|
+
type TileGridLayout,
|
|
20
|
+
} from "@thewhatmatters/wmds";
|
|
21
|
+
|
|
22
|
+
export interface ResourceImage {
|
|
23
|
+
src: string;
|
|
24
|
+
alt: string;
|
|
25
|
+
/** The image's own size, in px. They hold the tile's space before the image loads. */
|
|
26
|
+
width: number;
|
|
27
|
+
height: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface FilteredResource {
|
|
31
|
+
id: string;
|
|
32
|
+
title: string;
|
|
33
|
+
/** Where the tile goes — the resource's page in the app, for example /resources/type-scale. */
|
|
34
|
+
href: string;
|
|
35
|
+
/** Only for a tile that links straight to another site: it opens a new tab. */
|
|
36
|
+
external?: boolean;
|
|
37
|
+
/** The preview. */
|
|
38
|
+
image: ResourceImage;
|
|
39
|
+
/** One line under the title, for example the domain. */
|
|
40
|
+
meta?: string;
|
|
41
|
+
/** A short label over the image, for example "Free". */
|
|
42
|
+
tag?: string;
|
|
43
|
+
/** The resource's options in each filter group, by group id — one or several, for example { type: ["tools", "fonts"] }. */
|
|
44
|
+
facets: FilterFacets;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resourcesLabel(count: number) {
|
|
48
|
+
return count === 1 ? "1 resource" : `${count} resources`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The image's shape in a uniform grid, and of the placeholders while loading, as width / height. */
|
|
52
|
+
const uniformRatio = 4 / 3;
|
|
53
|
+
|
|
54
|
+
/** One placeholder tile each, while loading. */
|
|
55
|
+
const loadingTiles = ["one", "two", "three", "four", "five", "six"];
|
|
56
|
+
|
|
57
|
+
export interface FilteredGridProps {
|
|
58
|
+
title: string;
|
|
59
|
+
/** The caption over the grid, in sentence case. It names the list. */
|
|
60
|
+
caption: string;
|
|
61
|
+
items: FilteredResource[];
|
|
62
|
+
groups: FilterPanelGroup[];
|
|
63
|
+
/** masonry (default): tiles keep their image's height and pack. uniform: every image is 4:3. */
|
|
64
|
+
layout?: TileGridLayout;
|
|
65
|
+
/** Columns per breakpoint. Default: 1 on phones, 2 from sm, 3 from lg. */
|
|
66
|
+
columns?: TileGridColumns;
|
|
67
|
+
/** Placeholder tiles while the items are on their way. */
|
|
68
|
+
loading?: boolean;
|
|
69
|
+
/** The filters on when the page opens — for example { type: ["tools"] } from /resources?type=tools. */
|
|
70
|
+
defaultSelection?: FilterSelection;
|
|
71
|
+
/** The filters on, when the app holds them — for example in the URL. Pass with onSelectionChange. */
|
|
72
|
+
selection?: FilterSelection;
|
|
73
|
+
/** Every change: an option on or off, or Clear all. */
|
|
74
|
+
onSelectionChange?: (selection: FilterSelection) => void;
|
|
75
|
+
/** The router's link for a tile, in place of a plain anchor — (item) => <Link href={item.href} />. Not used for external items. */
|
|
76
|
+
renderLink?: (item: FilteredResource) => ReactElement;
|
|
77
|
+
/** The app's image component, in place of img — (image) => <Image {...image} sizes="…" />. */
|
|
78
|
+
renderImage?: (image: ResourceImage) => ReactNode;
|
|
79
|
+
/** Page-level layers inside the page grid — for example a GridOverlay. */
|
|
80
|
+
overlay?: ReactNode;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function FilteredGrid({
|
|
84
|
+
title,
|
|
85
|
+
caption,
|
|
86
|
+
items,
|
|
87
|
+
groups,
|
|
88
|
+
layout = "masonry",
|
|
89
|
+
columns,
|
|
90
|
+
loading = false,
|
|
91
|
+
defaultSelection,
|
|
92
|
+
selection: selectionProp,
|
|
93
|
+
onSelectionChange,
|
|
94
|
+
renderLink,
|
|
95
|
+
renderImage,
|
|
96
|
+
overlay,
|
|
97
|
+
}: FilteredGridProps) {
|
|
98
|
+
const gridHeadingId = useId();
|
|
99
|
+
const [selection, setSelection] = useFilterSelection({
|
|
100
|
+
defaultSelection,
|
|
101
|
+
selection: selectionProp,
|
|
102
|
+
onSelectionChange,
|
|
103
|
+
});
|
|
104
|
+
const shown = items.filter((item) => matchesFilterSelection(item.facets, selection));
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<FilterPanel
|
|
108
|
+
groups={countFilterOptions(groups, items, resourcesLabel)}
|
|
109
|
+
selection={selection}
|
|
110
|
+
onSelectionChange={setSelection}
|
|
111
|
+
resultsLabel={resourcesLabel(shown.length)}
|
|
112
|
+
>
|
|
113
|
+
<main className="grid-page bg-body">
|
|
114
|
+
{overlay}
|
|
115
|
+
<div className="band py-6 sm:py-10 lg:py-14">
|
|
116
|
+
<header className="col-span-full mb-4 flex items-end justify-between gap-4 sm:mb-8">
|
|
117
|
+
<h1 className="type-display-2 text-fg">
|
|
118
|
+
{title} {loading ? null : <span className="tabular-nums text-muted">{shown.length}</span>}
|
|
119
|
+
</h1>
|
|
120
|
+
<FilterPanel.Trigger className="md:hidden" />
|
|
121
|
+
</header>
|
|
122
|
+
<p role="status" className="sr-only">
|
|
123
|
+
{loading ? "Loading" : resourcesLabel(shown.length)}
|
|
124
|
+
</p>
|
|
125
|
+
|
|
126
|
+
<FilterPanel.Rail className="hidden md:col-span-3 md:block" />
|
|
127
|
+
|
|
128
|
+
<div className="col-span-full md:col-span-5 lg:col-span-9">
|
|
129
|
+
<SectionCaption id={gridHeadingId}>{caption}</SectionCaption>
|
|
130
|
+
<TileGrid
|
|
131
|
+
aria-labelledby={gridHeadingId}
|
|
132
|
+
layout={layout}
|
|
133
|
+
columns={columns}
|
|
134
|
+
busy={loading}
|
|
135
|
+
empty={<p className="type-body text-muted">No resources match these filters.</p>}
|
|
136
|
+
className="mt-6"
|
|
137
|
+
>
|
|
138
|
+
{loading
|
|
139
|
+
? loadingTiles.map((key) => (
|
|
140
|
+
<TileGrid.Item key={key}>
|
|
141
|
+
<LinkTile loading ratio={uniformRatio} />
|
|
142
|
+
</TileGrid.Item>
|
|
143
|
+
))
|
|
144
|
+
: shown.map((item) => {
|
|
145
|
+
const routed = renderLink != null && !item.external;
|
|
146
|
+
return (
|
|
147
|
+
<TileGrid.Item key={item.id}>
|
|
148
|
+
<LinkTile
|
|
149
|
+
title={item.title}
|
|
150
|
+
href={routed ? undefined : item.href}
|
|
151
|
+
render={routed ? renderLink(item) : undefined}
|
|
152
|
+
external={item.external}
|
|
153
|
+
meta={item.meta}
|
|
154
|
+
ratio={layout === "uniform" ? uniformRatio : undefined}
|
|
155
|
+
tag={item.tag != null ? <Badge>{item.tag}</Badge> : undefined}
|
|
156
|
+
media={
|
|
157
|
+
renderImage != null ? (
|
|
158
|
+
renderImage(item.image)
|
|
159
|
+
) : (
|
|
160
|
+
<img
|
|
161
|
+
src={item.image.src}
|
|
162
|
+
alt={item.image.alt}
|
|
163
|
+
width={item.image.width}
|
|
164
|
+
height={item.image.height}
|
|
165
|
+
loading="lazy"
|
|
166
|
+
decoding="async"
|
|
167
|
+
/>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
/>
|
|
171
|
+
</TileGrid.Item>
|
|
172
|
+
);
|
|
173
|
+
})}
|
|
174
|
+
</TileGrid>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<FilterPanel.Sheet />
|
|
179
|
+
</main>
|
|
180
|
+
</FilterPanel>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
// @thewhatmatters/wmds@0.4.
|
|
1
|
+
// @thewhatmatters/wmds@0.4.9 · Pattern — filtered index
|
|
2
2
|
// Storybook: Guides/Filter panel → Pattern — filtered index (?path=/story/guides-filter-panel--filtered-index)
|
|
3
3
|
// Show code — copy verbatim and keep this header; upgrades find pasted patterns by it.
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
import { type ReactElement, type ReactNode } from "react";
|
|
6
|
+
import {
|
|
7
|
+
FilterPanel,
|
|
8
|
+
IndexList,
|
|
9
|
+
countFilterOptions,
|
|
10
|
+
matchesFilterSelection,
|
|
11
|
+
useFilterSelection,
|
|
12
|
+
type FilterFacets,
|
|
13
|
+
type FilterPanelGroup,
|
|
14
|
+
type FilterSelection,
|
|
15
|
+
} from "@thewhatmatters/wmds";
|
|
14
16
|
|
|
15
17
|
export interface FilteredPost {
|
|
16
18
|
slug: string;
|
|
@@ -22,80 +24,27 @@ export interface FilteredPost {
|
|
|
22
24
|
dateLabel: string;
|
|
23
25
|
description: string;
|
|
24
26
|
/** The post's options in each filter group, by group id — one or several, for example { topic: ["guides", "brand"] }. */
|
|
25
|
-
facets:
|
|
27
|
+
facets: FilterFacets;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
/** The options on in each group, by group id — for example { topic: ["guides"] }. */
|
|
29
|
-
export type FilterSelection = Record<string, string[]>;
|
|
30
|
-
|
|
31
30
|
function postsLabel(count: number) {
|
|
32
31
|
return count === 1 ? "1 post" : `${count} posts`;
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
function facetValues(post: FilteredPost, groupId: string): string[] {
|
|
36
|
-
const value = post.facets[groupId];
|
|
37
|
-
return value == null ? [] : Array.isArray(value) ? value : [value];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** A post shows when, in every group with options on, it has at least one of them. */
|
|
41
|
-
function matches(post: FilteredPost, groups: FilterGroupDef[], selection: FilterSelection) {
|
|
42
|
-
return groups.every((group) => {
|
|
43
|
-
const values = selection[group.id] ?? [];
|
|
44
|
-
return values.length === 0 || facetValues(post, group.id).some((value) => values.includes(value));
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function FilterGroups({
|
|
49
|
-
groups,
|
|
50
|
-
posts,
|
|
51
|
-
selection,
|
|
52
|
-
onGroupChange,
|
|
53
|
-
}: {
|
|
54
|
-
groups: FilterGroupDef[];
|
|
55
|
-
posts: FilteredPost[];
|
|
56
|
-
selection: FilterSelection;
|
|
57
|
-
onGroupChange: (groupId: string, values: string[]) => void;
|
|
58
|
-
}) {
|
|
59
|
-
return (
|
|
60
|
-
<Accordion variant="plain" flush>
|
|
61
|
-
{groups.map((group) => (
|
|
62
|
-
<Accordion.Item key={group.id} label={group.label} defaultOpen>
|
|
63
|
-
<CheckboxGroup
|
|
64
|
-
label={group.label}
|
|
65
|
-
labelHidden
|
|
66
|
-
size="sm"
|
|
67
|
-
values={selection[group.id] ?? []}
|
|
68
|
-
onValuesChange={(values) => onGroupChange(group.id, values)}
|
|
69
|
-
>
|
|
70
|
-
{group.options.map((option) => {
|
|
71
|
-
const count = posts.filter((post) => facetValues(post, group.id).includes(option.value)).length;
|
|
72
|
-
return (
|
|
73
|
-
<CheckboxGroup.Item
|
|
74
|
-
key={option.value}
|
|
75
|
-
value={option.value}
|
|
76
|
-
label={option.label}
|
|
77
|
-
count={count}
|
|
78
|
-
countLabel={postsLabel(count)}
|
|
79
|
-
/>
|
|
80
|
-
);
|
|
81
|
-
})}
|
|
82
|
-
</CheckboxGroup>
|
|
83
|
-
</Accordion.Item>
|
|
84
|
-
))}
|
|
85
|
-
</Accordion>
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
34
|
export interface FilteredIndexProps {
|
|
90
35
|
title: string;
|
|
91
36
|
posts: FilteredPost[];
|
|
92
|
-
groups:
|
|
37
|
+
groups: FilterPanelGroup[];
|
|
93
38
|
/** The filters on when the page opens — for example { topic: ["guides"] } from /blog?topic=guides. */
|
|
94
39
|
defaultSelection?: FilterSelection;
|
|
95
40
|
/** The filters on, when the app holds them — for example in the URL. Pass with onSelectionChange. */
|
|
96
41
|
selection?: FilterSelection;
|
|
97
42
|
/** Every change: an option on or off, or Clear all. */
|
|
98
43
|
onSelectionChange?: (selection: FilterSelection) => void;
|
|
44
|
+
/** The router's link for a post's title, in place of a plain anchor — (post) => <Link href={post.href} />. */
|
|
45
|
+
renderLink?: (post: FilteredPost) => ReactElement;
|
|
46
|
+
/** Page-level layers inside the page grid — for example a GridOverlay. */
|
|
47
|
+
overlay?: ReactNode;
|
|
99
48
|
}
|
|
100
49
|
|
|
101
50
|
export function FilteredIndex({
|
|
@@ -105,94 +54,60 @@ export function FilteredIndex({
|
|
|
105
54
|
defaultSelection,
|
|
106
55
|
selection: selectionProp,
|
|
107
56
|
onSelectionChange,
|
|
57
|
+
renderLink,
|
|
58
|
+
overlay,
|
|
108
59
|
}: FilteredIndexProps) {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const shown = posts.filter((post) =>
|
|
115
|
-
|
|
116
|
-
function changeSelection(next: FilterSelection) {
|
|
117
|
-
if (selectionProp == null) setOwnSelection(next);
|
|
118
|
-
onSelectionChange?.(next);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function setGroup(groupId: string, values: string[]) {
|
|
122
|
-
changeSelection({ ...selection, [groupId]: values });
|
|
123
|
-
}
|
|
60
|
+
const [selection, setSelection] = useFilterSelection({
|
|
61
|
+
defaultSelection,
|
|
62
|
+
selection: selectionProp,
|
|
63
|
+
onSelectionChange,
|
|
64
|
+
});
|
|
65
|
+
const shown = posts.filter((post) => matchesFilterSelection(post.facets, selection));
|
|
124
66
|
|
|
125
67
|
return (
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
className="
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
</
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
>
|
|
166
|
-
{shown.map((post) => (
|
|
167
|
-
<IndexList.Item
|
|
168
|
-
key={post.slug}
|
|
169
|
-
title={post.title}
|
|
170
|
-
href={post.href}
|
|
171
|
-
meta={<time dateTime={post.date}>{post.dateLabel}</time>}
|
|
172
|
-
preview={<p>{post.description}</p>}
|
|
173
|
-
/>
|
|
174
|
-
))}
|
|
175
|
-
</IndexList>
|
|
68
|
+
<FilterPanel
|
|
69
|
+
groups={countFilterOptions(groups, posts, postsLabel)}
|
|
70
|
+
selection={selection}
|
|
71
|
+
onSelectionChange={setSelection}
|
|
72
|
+
resultsLabel={postsLabel(shown.length)}
|
|
73
|
+
>
|
|
74
|
+
<main className="grid-page bg-body">
|
|
75
|
+
{overlay}
|
|
76
|
+
<div className="band py-6 sm:py-10 lg:py-14">
|
|
77
|
+
<header className="col-span-full mb-4 flex items-end justify-between gap-4 sm:mb-8">
|
|
78
|
+
<h1 className="type-display-2 text-fg">
|
|
79
|
+
{title} <span className="tabular-nums text-muted">{shown.length}</span>
|
|
80
|
+
</h1>
|
|
81
|
+
<FilterPanel.Trigger className="md:hidden" />
|
|
82
|
+
</header>
|
|
83
|
+
<p role="status" className="sr-only">
|
|
84
|
+
{postsLabel(shown.length)}
|
|
85
|
+
</p>
|
|
86
|
+
|
|
87
|
+
<FilterPanel.Rail className="hidden md:col-span-3 md:block" />
|
|
88
|
+
|
|
89
|
+
<div className="col-span-full md:col-span-5 lg:col-span-9">
|
|
90
|
+
<IndexList
|
|
91
|
+
aria-label="Posts"
|
|
92
|
+
captions={{ meta: "Date", title: "Name" }}
|
|
93
|
+
empty={<p className="type-body text-muted">No posts match these filters.</p>}
|
|
94
|
+
>
|
|
95
|
+
{shown.map((post) => (
|
|
96
|
+
<IndexList.Item
|
|
97
|
+
key={post.slug}
|
|
98
|
+
title={post.title}
|
|
99
|
+
href={renderLink != null ? undefined : post.href}
|
|
100
|
+
render={renderLink?.(post)}
|
|
101
|
+
meta={<time dateTime={post.date}>{post.dateLabel}</time>}
|
|
102
|
+
preview={<p>{post.description}</p>}
|
|
103
|
+
/>
|
|
104
|
+
))}
|
|
105
|
+
</IndexList>
|
|
106
|
+
</div>
|
|
176
107
|
</div>
|
|
177
|
-
</div>
|
|
178
108
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
footer={
|
|
183
|
-
<div className="flex items-center justify-end gap-2">
|
|
184
|
-
<Button role="secondary" size="sm" disabled={activeCount === 0} onClick={() => changeSelection({})}>
|
|
185
|
-
Clear all
|
|
186
|
-
</Button>
|
|
187
|
-
<Button role="primary" size="sm" onClick={() => setSheetOpen(false)}>
|
|
188
|
-
Show {postsLabel(shown.length)}
|
|
189
|
-
</Button>
|
|
190
|
-
</div>
|
|
191
|
-
}
|
|
192
|
-
>
|
|
193
|
-
<FilterGroups groups={groups} posts={posts} selection={selection} onGroupChange={setGroup} />
|
|
194
|
-
</Sheet.Content>
|
|
195
|
-
</Sheet>
|
|
196
|
-
</main>
|
|
109
|
+
<FilterPanel.Sheet />
|
|
110
|
+
</main>
|
|
111
|
+
</FilterPanel>
|
|
197
112
|
);
|
|
198
113
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @thewhatmatters/wmds@0.4.
|
|
1
|
+
// @thewhatmatters/wmds@0.4.9 · Pattern — notification preferences
|
|
2
2
|
// Storybook: Guides/Overlay flows → Pattern — notification preferences (?path=/story/guides-overlay-flows--notification-preferences)
|
|
3
3
|
// Show code — copy verbatim and keep this header; upgrades find pasted patterns by it.
|
|
4
4
|
|