@thewhatmatters/wmds 0.4.6 → 0.4.8
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 +52 -0
- package/CONSUMING.md +4 -0
- package/README.md +2 -2
- 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 +115 -0
- package/dist/components/molecules/LinkTile/linkTileStyles.d.ts +31 -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/Carousel/Carousel.d.ts +58 -0
- package/dist/components/organisms/Carousel/Carousel.d.ts.map +1 -0
- package/dist/components/organisms/Carousel/Carousel.js +310 -0
- package/dist/components/organisms/Carousel/carouselMath.d.ts +39 -0
- package/dist/components/organisms/Carousel/carouselMath.d.ts.map +1 -0
- package/dist/components/organisms/Carousel/carouselMath.js +47 -0
- package/dist/components/organisms/Carousel/carouselStyles.d.ts +49 -0
- package/dist/components/organisms/Carousel/carouselStyles.d.ts.map +1 -0
- package/dist/components/organisms/Carousel/carouselStyles.js +25 -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 +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +73 -65
- package/dist/lib/compositionAudit.d.ts.map +1 -1
- package/dist/styles.css +1 -1
- package/docs/components.md +8 -4
- package/docs/exports.json +56 -3
- 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 +30 -0
- 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 +64 -153
- package/docs/patterns/guides-overlay-flows--notification-preferences.tsx +1 -1
- package/docs/patterns/guides-post-page--post-page.tsx +98 -54
- package/docs/patterns/guides-profile-typography--editorial-profile.tsx +1 -1
- package/docs/patterns/guides-resource-detail--detail.tsx +261 -0
- package/docs/patterns/index.json +57 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @thewhatmatters/wmds@0.4.
|
|
1
|
+
// @thewhatmatters/wmds@0.4.8 · Pattern — post page
|
|
2
2
|
// Storybook: Guides/Post page → Pattern — post page (?path=/story/guides-post-page--post-page)
|
|
3
3
|
// Show code — copy verbatim and keep this header; upgrades find pasted patterns by it.
|
|
4
4
|
|
|
@@ -77,6 +77,9 @@ function CopyButton({ text, label }: { text: string; label: string }) {
|
|
|
77
77
|
/** Where the metadata panel sits from lg: before the article (start) or after it (end). */
|
|
78
78
|
export type PostMetadataSide = "start" | "end";
|
|
79
79
|
|
|
80
|
+
/** Where the panel sits below lg, where it stacks: before the article or after it (default). */
|
|
81
|
+
export type PostMetadataStack = "before" | "after";
|
|
82
|
+
|
|
80
83
|
/** How many of the page's 12 columns the panel takes from lg; the article takes the rest. */
|
|
81
84
|
export type PostMetadataColumns = 3 | 4;
|
|
82
85
|
|
|
@@ -94,84 +97,125 @@ export interface PostPageProps {
|
|
|
94
97
|
post: PostPageData;
|
|
95
98
|
/** The post's rendered markdown. */
|
|
96
99
|
children: ReactNode;
|
|
97
|
-
/** Where the panel sits from lg
|
|
100
|
+
/** Where the panel sits from lg, beside the article. Default: "start". */
|
|
98
101
|
metadataSide?: PostMetadataSide;
|
|
99
102
|
/** The panel's width from lg, in columns. Default: 4. */
|
|
100
103
|
metadataColumns?: PostMetadataColumns;
|
|
104
|
+
/**
|
|
105
|
+
* Where the panel sits below lg, where it stacks. "after" (default) puts the article first, with
|
|
106
|
+
* the date and reading time in a line under the title. It is also the panel's place in reading
|
|
107
|
+
* and tab order at every width, so pair "after" with metadataSide="end" and "before" with "start".
|
|
108
|
+
*/
|
|
109
|
+
metadataStack?: PostMetadataStack;
|
|
110
|
+
/** Page-level layers inside the page grid — for example a GridOverlay. */
|
|
111
|
+
overlay?: ReactNode;
|
|
101
112
|
}
|
|
102
113
|
|
|
103
114
|
/** A blog post: the title, a metadata panel, and the article, which is the rendered markdown. */
|
|
104
|
-
export function PostPage({
|
|
115
|
+
export function PostPage({
|
|
116
|
+
post,
|
|
117
|
+
children,
|
|
118
|
+
metadataSide = "start",
|
|
119
|
+
metadataColumns = 4,
|
|
120
|
+
metadataStack = "after",
|
|
121
|
+
overlay,
|
|
122
|
+
}: PostPageProps) {
|
|
105
123
|
const metadataHeadingId = useId();
|
|
106
124
|
const encodedUrl = encodeURIComponent(post.url);
|
|
107
125
|
const encodedTitle = encodeURIComponent(post.title);
|
|
126
|
+
const stackedAfter = metadataStack === "after";
|
|
127
|
+
// From lg the panel's side wins; the order utility moves it only when its side and its place
|
|
128
|
+
// in the markup disagree.
|
|
129
|
+
const panelOrder =
|
|
130
|
+
metadataSide === "end" && !stackedAfter ? " lg:order-last" : metadataSide === "start" && stackedAfter ? " lg:order-1" : "";
|
|
131
|
+
const articleOrder = metadataSide === "start" && stackedAfter ? " lg:order-2" : "";
|
|
132
|
+
|
|
133
|
+
const panel = (
|
|
134
|
+
<aside
|
|
135
|
+
aria-labelledby={metadataHeadingId}
|
|
136
|
+
className={
|
|
137
|
+
"col-span-full lg:sticky lg:top-[calc(var(--site-nav-height)+1rem)] lg:mt-0 " +
|
|
138
|
+
(stackedAfter ? "mt-10 " : "") +
|
|
139
|
+
metadataColumnClasses[metadataColumns] +
|
|
140
|
+
panelOrder
|
|
141
|
+
}
|
|
142
|
+
>
|
|
143
|
+
<SectionCaption id={metadataHeadingId}>Metadata</SectionCaption>
|
|
144
|
+
<DescriptionList variant="mono" rule="dotted">
|
|
145
|
+
<DescriptionList.Item name="Date">
|
|
146
|
+
<time dateTime={post.date}>{post.dateLabel}</time>
|
|
147
|
+
</DescriptionList.Item>
|
|
148
|
+
<DescriptionList.Item name="Author">
|
|
149
|
+
<Badge emphasis="outline" mono>
|
|
150
|
+
{post.author}
|
|
151
|
+
</Badge>
|
|
152
|
+
</DescriptionList.Item>
|
|
153
|
+
<DescriptionList.Item name="Reading time">{post.readingTime}</DescriptionList.Item>
|
|
154
|
+
<DescriptionList.Item name="Categories">
|
|
155
|
+
{post.categories.map((category) => (
|
|
156
|
+
<Badge key={category.href} emphasis="outline" mono render={<a href={category.href} />}>
|
|
157
|
+
{category.label}
|
|
158
|
+
</Badge>
|
|
159
|
+
))}
|
|
160
|
+
</DescriptionList.Item>
|
|
161
|
+
<DescriptionList.Item name="Agents" layout="stacked">
|
|
162
|
+
<CopyButton text={post.markdown} label="Copy for LLM" />
|
|
163
|
+
<Button role="secondary" size="sm" icon={<FileText />} render={<a href={post.markdownHref} />}>
|
|
164
|
+
View as Markdown
|
|
165
|
+
</Button>
|
|
166
|
+
</DescriptionList.Item>
|
|
167
|
+
<DescriptionList.Item name="Share" layout="stacked">
|
|
168
|
+
<Button
|
|
169
|
+
role="secondary"
|
|
170
|
+
size="sm"
|
|
171
|
+
external
|
|
172
|
+
render={<a href={"https://x.com/intent/post?url=" + encodedUrl + "&text=" + encodedTitle} />}
|
|
173
|
+
>
|
|
174
|
+
Twitter/X
|
|
175
|
+
</Button>
|
|
176
|
+
<Button
|
|
177
|
+
role="secondary"
|
|
178
|
+
size="sm"
|
|
179
|
+
external
|
|
180
|
+
render={<a href={"https://www.linkedin.com/sharing/share-offsite/?url=" + encodedUrl} />}
|
|
181
|
+
>
|
|
182
|
+
LinkedIn
|
|
183
|
+
</Button>
|
|
184
|
+
</DescriptionList.Item>
|
|
185
|
+
</DescriptionList>
|
|
186
|
+
</aside>
|
|
187
|
+
);
|
|
108
188
|
|
|
109
189
|
return (
|
|
110
190
|
<main className="grid-page bg-body">
|
|
191
|
+
{overlay}
|
|
111
192
|
<article className="band py-6 sm:py-10 lg:py-14">
|
|
112
193
|
<header className="col-span-full mb-4 flex flex-col gap-3 sm:mb-8 lg:col-span-10">
|
|
113
194
|
<Breadcrumb items={post.breadcrumb} variant="mono" separator="slash" className="mb-3" />
|
|
114
195
|
<h1 className="type-display-2 text-balance text-fg">{post.title}</h1>
|
|
115
196
|
<p className="type-reading max-w-[40rem] text-muted">{post.description}</p>
|
|
197
|
+
{stackedAfter ? (
|
|
198
|
+
<p className="type-eyebrow text-muted lg:hidden">
|
|
199
|
+
<time dateTime={post.date}>{post.dateLabel}</time>
|
|
200
|
+
<span aria-hidden="true"> / </span>
|
|
201
|
+
<span className="sr-only">, </span>
|
|
202
|
+
{post.readingTime} read
|
|
203
|
+
</p>
|
|
204
|
+
) : null}
|
|
116
205
|
</header>
|
|
117
206
|
|
|
118
|
-
|
|
119
|
-
|
|
207
|
+
{stackedAfter ? null : panel}
|
|
208
|
+
|
|
209
|
+
<div
|
|
120
210
|
className={
|
|
121
|
-
"col-span-full lg:
|
|
122
|
-
metadataColumnClasses[metadataColumns] +
|
|
123
|
-
(metadataSide === "end" ? " lg:order-last" : "")
|
|
211
|
+
"col-span-full lg:mt-0 " + (stackedAfter ? "" : "mt-6 ") + articleColumnClasses[metadataColumns] + articleOrder
|
|
124
212
|
}
|
|
125
213
|
>
|
|
126
|
-
<SectionCaption id={metadataHeadingId}>Metadata</SectionCaption>
|
|
127
|
-
<DescriptionList variant="mono" rule="dotted">
|
|
128
|
-
<DescriptionList.Item name="Date">
|
|
129
|
-
<time dateTime={post.date}>{post.dateLabel}</time>
|
|
130
|
-
</DescriptionList.Item>
|
|
131
|
-
<DescriptionList.Item name="Author">
|
|
132
|
-
<Badge emphasis="outline" mono>
|
|
133
|
-
{post.author}
|
|
134
|
-
</Badge>
|
|
135
|
-
</DescriptionList.Item>
|
|
136
|
-
<DescriptionList.Item name="Reading time">{post.readingTime}</DescriptionList.Item>
|
|
137
|
-
<DescriptionList.Item name="Categories">
|
|
138
|
-
{post.categories.map((category) => (
|
|
139
|
-
<Badge key={category.href} emphasis="outline" mono render={<a href={category.href} />}>
|
|
140
|
-
{category.label}
|
|
141
|
-
</Badge>
|
|
142
|
-
))}
|
|
143
|
-
</DescriptionList.Item>
|
|
144
|
-
<DescriptionList.Item name="Agents" layout="stacked">
|
|
145
|
-
<CopyButton text={post.markdown} label="Copy for LLM" />
|
|
146
|
-
<Button role="secondary" size="sm" icon={<FileText />} render={<a href={post.markdownHref} />}>
|
|
147
|
-
View as Markdown
|
|
148
|
-
</Button>
|
|
149
|
-
</DescriptionList.Item>
|
|
150
|
-
<DescriptionList.Item name="Share" layout="stacked">
|
|
151
|
-
<Button
|
|
152
|
-
role="secondary"
|
|
153
|
-
size="sm"
|
|
154
|
-
external
|
|
155
|
-
render={<a href={"https://x.com/intent/post?url=" + encodedUrl + "&text=" + encodedTitle} />}
|
|
156
|
-
>
|
|
157
|
-
Twitter/X
|
|
158
|
-
</Button>
|
|
159
|
-
<Button
|
|
160
|
-
role="secondary"
|
|
161
|
-
size="sm"
|
|
162
|
-
external
|
|
163
|
-
render={<a href={"https://www.linkedin.com/sharing/share-offsite/?url=" + encodedUrl} />}
|
|
164
|
-
>
|
|
165
|
-
LinkedIn
|
|
166
|
-
</Button>
|
|
167
|
-
</DescriptionList.Item>
|
|
168
|
-
</DescriptionList>
|
|
169
|
-
</aside>
|
|
170
|
-
|
|
171
|
-
<div className={"col-span-full mt-6 lg:mt-0 " + articleColumnClasses[metadataColumns]}>
|
|
172
214
|
<SectionCaption as="p">Article</SectionCaption>
|
|
173
215
|
<Prose className="mt-6">{children}</Prose>
|
|
174
216
|
</div>
|
|
217
|
+
|
|
218
|
+
{stackedAfter ? panel : null}
|
|
175
219
|
</article>
|
|
176
220
|
</main>
|
|
177
221
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @thewhatmatters/wmds@0.4.
|
|
1
|
+
// @thewhatmatters/wmds@0.4.8 · Pattern — editorial profile
|
|
2
2
|
// Storybook: Guides/Profile typography → Pattern — editorial profile (?path=/story/guides-profile-typography--editorial-profile)
|
|
3
3
|
// Show code — copy verbatim and keep this header; upgrades find pasted patterns by it.
|
|
4
4
|
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// @thewhatmatters/wmds@0.4.8 · Pattern — resource detail
|
|
2
|
+
// Storybook: Guides/Resource detail → Pattern — resource detail (?path=/story/guides-resource-detail--detail)
|
|
3
|
+
// Show code — copy verbatim and keep this header; upgrades find pasted patterns by it.
|
|
4
|
+
|
|
5
|
+
import { useEffect, useId, type ReactNode } from "react";
|
|
6
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
7
|
+
import {
|
|
8
|
+
Badge,
|
|
9
|
+
Breadcrumb,
|
|
10
|
+
Button,
|
|
11
|
+
DescriptionList,
|
|
12
|
+
Dialog,
|
|
13
|
+
IconButton,
|
|
14
|
+
type BreadcrumbItemDef,
|
|
15
|
+
} from "@thewhatmatters/wmds";
|
|
16
|
+
|
|
17
|
+
export interface ResourceImage {
|
|
18
|
+
src: string;
|
|
19
|
+
alt: string;
|
|
20
|
+
/** The image's own size, in px. */
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ResourceVideo {
|
|
26
|
+
src: string;
|
|
27
|
+
/** The frame shown before the video plays. */
|
|
28
|
+
poster: string;
|
|
29
|
+
/** A WebVTT captions file, when the video has speech. */
|
|
30
|
+
captions?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ResourceDetailItem {
|
|
34
|
+
id: string;
|
|
35
|
+
title: string;
|
|
36
|
+
/** The preview. Shown unless there is a video. */
|
|
37
|
+
image: ResourceImage;
|
|
38
|
+
/** One video, with controls. It does not play by itself. */
|
|
39
|
+
video?: ResourceVideo;
|
|
40
|
+
/** The eyebrow over the title and the Category row, for example "Tools". */
|
|
41
|
+
category: string;
|
|
42
|
+
tags?: string[];
|
|
43
|
+
/** Where the resource lives, for example the domain — the Source row and the Visit button. */
|
|
44
|
+
meta?: string;
|
|
45
|
+
/** The curator's note: one or two sentences. */
|
|
46
|
+
note: string;
|
|
47
|
+
/** ISO date the resource was added, for the time element. */
|
|
48
|
+
added: string;
|
|
49
|
+
/** The date as readers see it, for example "Sep 14, 2026". */
|
|
50
|
+
addedLabel: string;
|
|
51
|
+
/** The resource itself, on its own site. */
|
|
52
|
+
visitHref: string;
|
|
53
|
+
/** More metadata rows, after Source. */
|
|
54
|
+
rows?: { name: string; value: string }[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The media well: one image or one video, whole, on the page floor. */
|
|
58
|
+
const mediaClasses =
|
|
59
|
+
"flex items-center justify-center overflow-hidden rounded-[var(--radius-card-body)] border border-border bg-muted-surface [&>img]:block [&>img]:h-auto [&>img]:w-full [&>video]:block [&>video]:h-auto [&>video]:w-full";
|
|
60
|
+
|
|
61
|
+
/** Over the grid, from md: the well takes the height the window allows and the media fits inside it. */
|
|
62
|
+
const mediaFillClasses =
|
|
63
|
+
"md:h-full md:min-h-0 md:[&>img]:max-h-full md:[&>img]:w-auto md:[&>img]:max-w-full md:[&>img]:object-contain md:[&>video]:max-h-full md:[&>video]:w-auto md:[&>video]:max-w-full";
|
|
64
|
+
|
|
65
|
+
function ResourceMedia({
|
|
66
|
+
item,
|
|
67
|
+
fill = false,
|
|
68
|
+
renderImage,
|
|
69
|
+
}: {
|
|
70
|
+
item: ResourceDetailItem;
|
|
71
|
+
fill?: boolean;
|
|
72
|
+
renderImage?: (image: ResourceImage) => ReactNode;
|
|
73
|
+
}) {
|
|
74
|
+
return (
|
|
75
|
+
<div className={fill ? mediaClasses + " " + mediaFillClasses : mediaClasses}>
|
|
76
|
+
{item.video != null ? (
|
|
77
|
+
<video key={item.video.src} controls playsInline preload="metadata" poster={item.video.poster}>
|
|
78
|
+
<source src={item.video.src} />
|
|
79
|
+
{item.video.captions != null ? (
|
|
80
|
+
<track kind="captions" src={item.video.captions} srcLang="en" label="English" default />
|
|
81
|
+
) : null}
|
|
82
|
+
</video>
|
|
83
|
+
) : renderImage != null ? (
|
|
84
|
+
renderImage(item.image)
|
|
85
|
+
) : (
|
|
86
|
+
<img src={item.image.src} alt={item.image.alt} width={item.image.width} height={item.image.height} />
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function ResourceInfo({
|
|
93
|
+
item,
|
|
94
|
+
headingId,
|
|
95
|
+
titleAs: Title,
|
|
96
|
+
}: {
|
|
97
|
+
item: ResourceDetailItem;
|
|
98
|
+
headingId: string;
|
|
99
|
+
titleAs: "h1" | "h2";
|
|
100
|
+
}) {
|
|
101
|
+
return (
|
|
102
|
+
<div className="flex flex-col gap-6">
|
|
103
|
+
<div className="flex flex-col gap-2">
|
|
104
|
+
<p className="type-eyebrow text-muted">{item.category}</p>
|
|
105
|
+
<Title id={headingId} className="type-heading-1 text-balance text-fg">
|
|
106
|
+
{item.title}
|
|
107
|
+
</Title>
|
|
108
|
+
<p className="type-reading text-fg">{item.note}</p>
|
|
109
|
+
<p className="type-supporting text-muted">
|
|
110
|
+
Added <time dateTime={item.added}>{item.addedLabel}</time>
|
|
111
|
+
</p>
|
|
112
|
+
</div>
|
|
113
|
+
<DescriptionList variant="mono" rule="dotted" aria-label="Details">
|
|
114
|
+
<DescriptionList.Item name="Category">{item.category}</DescriptionList.Item>
|
|
115
|
+
{item.tags != null && item.tags.length > 0 ? (
|
|
116
|
+
<DescriptionList.Item name="Tags">
|
|
117
|
+
{item.tags.map((tag) => (
|
|
118
|
+
<Badge key={tag} emphasis="outline" mono>
|
|
119
|
+
{tag}
|
|
120
|
+
</Badge>
|
|
121
|
+
))}
|
|
122
|
+
</DescriptionList.Item>
|
|
123
|
+
) : null}
|
|
124
|
+
{item.meta != null ? <DescriptionList.Item name="Source">{item.meta}</DescriptionList.Item> : null}
|
|
125
|
+
{item.rows?.map((row) => (
|
|
126
|
+
<DescriptionList.Item key={row.name} name={row.name}>
|
|
127
|
+
{row.value}
|
|
128
|
+
</DescriptionList.Item>
|
|
129
|
+
))}
|
|
130
|
+
</DescriptionList>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function VisitButton({ item }: { item: ResourceDetailItem }) {
|
|
136
|
+
return (
|
|
137
|
+
<Button role="primary" external render={<a href={item.visitHref} />}>
|
|
138
|
+
Visit {item.meta ?? "resource"}
|
|
139
|
+
</Button>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface ResourceDetailDialogProps {
|
|
144
|
+
item: ResourceDetailItem;
|
|
145
|
+
open: boolean;
|
|
146
|
+
/** Close, the scrim, and Escape. Go back to the grid's route here. */
|
|
147
|
+
onClose: () => void;
|
|
148
|
+
/** Go to the resource before this one. Leave it out on the first: the control is off. */
|
|
149
|
+
onPrevious?: () => void;
|
|
150
|
+
/** Go to the resource after this one. Leave it out on the last. */
|
|
151
|
+
onNext?: () => void;
|
|
152
|
+
/** Where this resource is in the list, for example "3 of 9". */
|
|
153
|
+
position?: string;
|
|
154
|
+
/** The app's image component, in place of img — (image) => <Image {...image} sizes="…" />. */
|
|
155
|
+
renderImage?: (image: ResourceImage) => ReactNode;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The detail over the grid: the page behind is dimmed and does not scroll. */
|
|
159
|
+
export function ResourceDetailDialog({
|
|
160
|
+
item,
|
|
161
|
+
open,
|
|
162
|
+
onClose,
|
|
163
|
+
onPrevious,
|
|
164
|
+
onNext,
|
|
165
|
+
position,
|
|
166
|
+
renderImage,
|
|
167
|
+
}: ResourceDetailDialogProps) {
|
|
168
|
+
const headingId = useId();
|
|
169
|
+
|
|
170
|
+
// Left and Right move between resources — except in the video, where they seek.
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (!open) return;
|
|
173
|
+
function onKeyDown(event: KeyboardEvent) {
|
|
174
|
+
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
|
|
175
|
+
if (event.target instanceof HTMLVideoElement) return;
|
|
176
|
+
if (event.key === "ArrowLeft" && onPrevious != null) {
|
|
177
|
+
event.preventDefault();
|
|
178
|
+
onPrevious();
|
|
179
|
+
} else if (event.key === "ArrowRight" && onNext != null) {
|
|
180
|
+
event.preventDefault();
|
|
181
|
+
onNext();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
document.addEventListener("keydown", onKeyDown);
|
|
185
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
186
|
+
}, [open, onPrevious, onNext]);
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<Dialog
|
|
190
|
+
open={open}
|
|
191
|
+
onOpenChange={(next) => {
|
|
192
|
+
if (!next) onClose();
|
|
193
|
+
}}
|
|
194
|
+
>
|
|
195
|
+
<Dialog.Content
|
|
196
|
+
size="full"
|
|
197
|
+
aria-labelledby={headingId}
|
|
198
|
+
headerStart={position != null ? <span className="type-supporting tabular-nums">{position}</span> : undefined}
|
|
199
|
+
headerEnd={
|
|
200
|
+
<>
|
|
201
|
+
<IconButton
|
|
202
|
+
size="sm"
|
|
203
|
+
role="ghost"
|
|
204
|
+
icon={<ChevronLeft />}
|
|
205
|
+
aria-label="Previous resource"
|
|
206
|
+
disabled={onPrevious == null}
|
|
207
|
+
onClick={onPrevious}
|
|
208
|
+
/>
|
|
209
|
+
<IconButton
|
|
210
|
+
size="sm"
|
|
211
|
+
role="ghost"
|
|
212
|
+
icon={<ChevronRight />}
|
|
213
|
+
aria-label="Next resource"
|
|
214
|
+
disabled={onNext == null}
|
|
215
|
+
onClick={onNext}
|
|
216
|
+
/>
|
|
217
|
+
</>
|
|
218
|
+
}
|
|
219
|
+
footer={<VisitButton item={item} />}
|
|
220
|
+
>
|
|
221
|
+
<div className="grid gap-6 md:h-full md:grid-cols-[minmax(0,1fr)_20rem] lg:grid-cols-[minmax(0,1fr)_24rem]">
|
|
222
|
+
<ResourceMedia item={item} fill renderImage={renderImage} />
|
|
223
|
+
<div className="md:min-h-0 md:overflow-y-auto">
|
|
224
|
+
<ResourceInfo item={item} headingId={headingId} titleAs="h2" />
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
</Dialog.Content>
|
|
228
|
+
</Dialog>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface ResourceDetailPageProps {
|
|
233
|
+
item: ResourceDetailItem;
|
|
234
|
+
/** The path to this resource, ending with it — Home, Resources, this resource. */
|
|
235
|
+
breadcrumb: BreadcrumbItemDef[];
|
|
236
|
+
/** The app's image component, in place of img — (image) => <Image {...image} sizes="…" />. */
|
|
237
|
+
renderImage?: (image: ResourceImage) => ReactNode;
|
|
238
|
+
/** Page-level layers inside the page grid — for example a GridOverlay. */
|
|
239
|
+
overlay?: ReactNode;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** The detail as its own page: opened directly, or from a shared link. */
|
|
243
|
+
export function ResourceDetailPage({ item, breadcrumb, renderImage, overlay }: ResourceDetailPageProps) {
|
|
244
|
+
const headingId = useId();
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<main className="grid-page bg-body">
|
|
248
|
+
{overlay}
|
|
249
|
+
<article aria-labelledby={headingId} className="band py-6 sm:py-10 lg:py-14">
|
|
250
|
+
<Breadcrumb items={breadcrumb} variant="mono" separator="slash" className="col-span-full mb-6" />
|
|
251
|
+
<div className="col-span-full lg:col-span-8">
|
|
252
|
+
<ResourceMedia item={item} renderImage={renderImage} />
|
|
253
|
+
</div>
|
|
254
|
+
<div className="col-span-full mt-6 flex flex-col items-start gap-6 lg:col-span-4 lg:mt-0">
|
|
255
|
+
<ResourceInfo item={item} headingId={headingId} titleAs="h1" />
|
|
256
|
+
<VisitButton item={item} />
|
|
257
|
+
</div>
|
|
258
|
+
</article>
|
|
259
|
+
</main>
|
|
260
|
+
);
|
|
261
|
+
}
|
package/docs/patterns/index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": "@thewhatmatters/wmds",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
4
4
|
"patterns": [
|
|
5
5
|
{
|
|
6
6
|
"id": "components-accordion--flush",
|
|
@@ -242,6 +242,14 @@
|
|
|
242
242
|
"storybook": "?path=/story/components-card-selectablecard--selectable-cards-pattern",
|
|
243
243
|
"source": "src/components/molecules/SelectableCard/SelectableCard.stories.tsx"
|
|
244
244
|
},
|
|
245
|
+
{
|
|
246
|
+
"id": "components-carousel--image-carousel",
|
|
247
|
+
"title": "Components/Carousel",
|
|
248
|
+
"name": "Pattern — image carousel",
|
|
249
|
+
"file": "patterns/components-carousel--image-carousel.tsx",
|
|
250
|
+
"storybook": "?path=/story/components-carousel--image-carousel",
|
|
251
|
+
"source": "src/components/organisms/Carousel/Carousel.stories.tsx"
|
|
252
|
+
},
|
|
245
253
|
{
|
|
246
254
|
"id": "components-chart--audience-activity-heatmap",
|
|
247
255
|
"title": "Components/Chart",
|
|
@@ -434,6 +442,14 @@
|
|
|
434
442
|
"storybook": "?path=/story/components-field--multi-control",
|
|
435
443
|
"source": "src/components/molecules/Field/Field.stories.tsx"
|
|
436
444
|
},
|
|
445
|
+
{
|
|
446
|
+
"id": "components-filterpanel--panel",
|
|
447
|
+
"title": "Components/FilterPanel",
|
|
448
|
+
"name": "Pattern — filter panel",
|
|
449
|
+
"file": "patterns/components-filterpanel--panel.tsx",
|
|
450
|
+
"storybook": "?path=/story/components-filterpanel--panel",
|
|
451
|
+
"source": "src/components/organisms/FilterPanel/FilterPanel.stories.tsx"
|
|
452
|
+
},
|
|
437
453
|
{
|
|
438
454
|
"id": "components-footerreveal--marketing-hero-pattern",
|
|
439
455
|
"title": "Components/FooterReveal",
|
|
@@ -586,6 +602,22 @@
|
|
|
586
602
|
"storybook": "?path=/story/components-kbd--shortcut",
|
|
587
603
|
"source": "src/components/atoms/Kbd/Kbd.stories.tsx"
|
|
588
604
|
},
|
|
605
|
+
{
|
|
606
|
+
"id": "components-linktile--resource-tile",
|
|
607
|
+
"title": "Components/LinkTile",
|
|
608
|
+
"name": "Pattern — resource tile",
|
|
609
|
+
"file": "patterns/components-linktile--resource-tile.tsx",
|
|
610
|
+
"storybook": "?path=/story/components-linktile--resource-tile",
|
|
611
|
+
"source": "src/components/molecules/LinkTile/LinkTile.stories.tsx"
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
"id": "components-linktile--router-link",
|
|
615
|
+
"title": "Components/LinkTile",
|
|
616
|
+
"name": "Pattern — router link",
|
|
617
|
+
"file": "patterns/components-linktile--router-link.tsx",
|
|
618
|
+
"storybook": "?path=/story/components-linktile--router-link",
|
|
619
|
+
"source": "src/components/molecules/LinkTile/LinkTile.stories.tsx"
|
|
620
|
+
},
|
|
589
621
|
{
|
|
590
622
|
"id": "components-moremenu--action-menu",
|
|
591
623
|
"title": "Components/MoreMenu",
|
|
@@ -914,6 +946,14 @@
|
|
|
914
946
|
"storybook": "?path=/story/components-textsequence--shapes",
|
|
915
947
|
"source": "src/components/molecules/TextSequence/TextSequence.stories.tsx"
|
|
916
948
|
},
|
|
949
|
+
{
|
|
950
|
+
"id": "components-tilegrid--masonry",
|
|
951
|
+
"title": "Components/TileGrid",
|
|
952
|
+
"name": "Pattern — tile grid",
|
|
953
|
+
"file": "patterns/components-tilegrid--masonry.tsx",
|
|
954
|
+
"storybook": "?path=/story/components-tilegrid--masonry",
|
|
955
|
+
"source": "src/components/molecules/TileGrid/TileGrid.stories.tsx"
|
|
956
|
+
},
|
|
917
957
|
{
|
|
918
958
|
"id": "components-toast--stacked-notifications",
|
|
919
959
|
"title": "Components/Toast",
|
|
@@ -986,6 +1026,14 @@
|
|
|
986
1026
|
"storybook": "?path=/story/guides-chart-explorations--comparison-gallery",
|
|
987
1027
|
"source": "src/guides/ChartExplorations/ChartExplorations.stories.tsx"
|
|
988
1028
|
},
|
|
1029
|
+
{
|
|
1030
|
+
"id": "guides-filter-panel--filtered-grid",
|
|
1031
|
+
"title": "Guides/Filter panel",
|
|
1032
|
+
"name": "Pattern — filtered grid",
|
|
1033
|
+
"file": "patterns/guides-filter-panel--filtered-grid.tsx",
|
|
1034
|
+
"storybook": "?path=/story/guides-filter-panel--filtered-grid",
|
|
1035
|
+
"source": "src/guides/FilterPanel/FilterPanel.stories.tsx"
|
|
1036
|
+
},
|
|
989
1037
|
{
|
|
990
1038
|
"id": "guides-filter-panel--filtered-index",
|
|
991
1039
|
"title": "Guides/Filter panel",
|
|
@@ -1017,6 +1065,14 @@
|
|
|
1017
1065
|
"file": "patterns/guides-profile-typography--editorial-profile.tsx",
|
|
1018
1066
|
"storybook": "?path=/story/guides-profile-typography--editorial-profile",
|
|
1019
1067
|
"source": "src/guides/ProfileTypography/ProfileTypography.stories.tsx"
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
"id": "guides-resource-detail--detail",
|
|
1071
|
+
"title": "Guides/Resource detail",
|
|
1072
|
+
"name": "Pattern — resource detail",
|
|
1073
|
+
"file": "patterns/guides-resource-detail--detail.tsx",
|
|
1074
|
+
"storybook": "?path=/story/guides-resource-detail--detail",
|
|
1075
|
+
"source": "src/guides/ResourceDetail/ResourceDetail.stories.tsx"
|
|
1020
1076
|
}
|
|
1021
1077
|
]
|
|
1022
1078
|
}
|
package/package.json
CHANGED