@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
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// @thewhatmatters/wmds@0.4.
|
|
1
|
+
// @thewhatmatters/wmds@0.4.9 · 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
|
|
|
5
|
-
import { useEffect, useId, useState, type ReactNode } from "react";
|
|
5
|
+
import { useEffect, useId, useState, type ReactElement, type ReactNode } from "react";
|
|
6
6
|
import { Copy, FileText } from "lucide-react";
|
|
7
7
|
import {
|
|
8
8
|
Badge,
|
|
@@ -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,142 @@ 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
|
+
/**
|
|
111
|
+
* The router's link for the breadcrumb and the category tags, in place of a plain anchor —
|
|
112
|
+
* (link) => <Link href={link.href} />.
|
|
113
|
+
*/
|
|
114
|
+
renderLink?: (link: { label: string; href: string }) => ReactElement;
|
|
115
|
+
/** Page-level layers inside the page grid — for example a GridOverlay. */
|
|
116
|
+
overlay?: ReactNode;
|
|
101
117
|
}
|
|
102
118
|
|
|
103
119
|
/** A blog post: the title, a metadata panel, and the article, which is the rendered markdown. */
|
|
104
|
-
export function PostPage({
|
|
120
|
+
export function PostPage({
|
|
121
|
+
post,
|
|
122
|
+
children,
|
|
123
|
+
metadataSide = "start",
|
|
124
|
+
metadataColumns = 4,
|
|
125
|
+
metadataStack = "after",
|
|
126
|
+
renderLink,
|
|
127
|
+
overlay,
|
|
128
|
+
}: PostPageProps) {
|
|
105
129
|
const metadataHeadingId = useId();
|
|
106
130
|
const encodedUrl = encodeURIComponent(post.url);
|
|
107
131
|
const encodedTitle = encodeURIComponent(post.title);
|
|
132
|
+
const stackedAfter = metadataStack === "after";
|
|
133
|
+
// From lg the panel's side wins; the order utility moves it only when its side and its place
|
|
134
|
+
// in the markup disagree.
|
|
135
|
+
const panelOrder =
|
|
136
|
+
metadataSide === "end" && !stackedAfter ? " lg:order-last" : metadataSide === "start" && stackedAfter ? " lg:order-1" : "";
|
|
137
|
+
const articleOrder = metadataSide === "start" && stackedAfter ? " lg:order-2" : "";
|
|
138
|
+
|
|
139
|
+
const panel = (
|
|
140
|
+
<aside
|
|
141
|
+
aria-labelledby={metadataHeadingId}
|
|
142
|
+
className={
|
|
143
|
+
"col-span-full lg:sticky lg:top-[calc(var(--site-nav-height)+1rem)] lg:mt-0 " +
|
|
144
|
+
(stackedAfter ? "mt-10 " : "") +
|
|
145
|
+
metadataColumnClasses[metadataColumns] +
|
|
146
|
+
panelOrder
|
|
147
|
+
}
|
|
148
|
+
>
|
|
149
|
+
<SectionCaption id={metadataHeadingId}>Metadata</SectionCaption>
|
|
150
|
+
<DescriptionList variant="mono" rule="dotted">
|
|
151
|
+
<DescriptionList.Item name="Date">
|
|
152
|
+
<time dateTime={post.date}>{post.dateLabel}</time>
|
|
153
|
+
</DescriptionList.Item>
|
|
154
|
+
<DescriptionList.Item name="Author">
|
|
155
|
+
<Badge emphasis="outline" mono>
|
|
156
|
+
{post.author}
|
|
157
|
+
</Badge>
|
|
158
|
+
</DescriptionList.Item>
|
|
159
|
+
<DescriptionList.Item name="Reading time">{post.readingTime}</DescriptionList.Item>
|
|
160
|
+
<DescriptionList.Item name="Categories">
|
|
161
|
+
{post.categories.map((category) => (
|
|
162
|
+
<Badge
|
|
163
|
+
key={category.href}
|
|
164
|
+
emphasis="outline"
|
|
165
|
+
mono
|
|
166
|
+
render={renderLink != null ? renderLink(category) : <a href={category.href} />}
|
|
167
|
+
>
|
|
168
|
+
{category.label}
|
|
169
|
+
</Badge>
|
|
170
|
+
))}
|
|
171
|
+
</DescriptionList.Item>
|
|
172
|
+
<DescriptionList.Item name="Agents" layout="stacked">
|
|
173
|
+
<CopyButton text={post.markdown} label="Copy for LLM" />
|
|
174
|
+
<Button role="secondary" size="sm" icon={<FileText />} render={<a href={post.markdownHref} />}>
|
|
175
|
+
View as Markdown
|
|
176
|
+
</Button>
|
|
177
|
+
</DescriptionList.Item>
|
|
178
|
+
<DescriptionList.Item name="Share" layout="stacked">
|
|
179
|
+
<Button
|
|
180
|
+
role="secondary"
|
|
181
|
+
size="sm"
|
|
182
|
+
external
|
|
183
|
+
render={<a href={"https://x.com/intent/post?url=" + encodedUrl + "&text=" + encodedTitle} />}
|
|
184
|
+
>
|
|
185
|
+
Twitter/X
|
|
186
|
+
</Button>
|
|
187
|
+
<Button
|
|
188
|
+
role="secondary"
|
|
189
|
+
size="sm"
|
|
190
|
+
external
|
|
191
|
+
render={<a href={"https://www.linkedin.com/sharing/share-offsite/?url=" + encodedUrl} />}
|
|
192
|
+
>
|
|
193
|
+
LinkedIn
|
|
194
|
+
</Button>
|
|
195
|
+
</DescriptionList.Item>
|
|
196
|
+
</DescriptionList>
|
|
197
|
+
</aside>
|
|
198
|
+
);
|
|
108
199
|
|
|
109
200
|
return (
|
|
110
201
|
<main className="grid-page bg-body">
|
|
202
|
+
{overlay}
|
|
111
203
|
<article className="band py-6 sm:py-10 lg:py-14">
|
|
112
204
|
<header className="col-span-full mb-4 flex flex-col gap-3 sm:mb-8 lg:col-span-10">
|
|
113
|
-
<Breadcrumb
|
|
205
|
+
<Breadcrumb
|
|
206
|
+
items={post.breadcrumb}
|
|
207
|
+
variant="mono"
|
|
208
|
+
separator="slash"
|
|
209
|
+
renderLink={renderLink}
|
|
210
|
+
className="mb-3"
|
|
211
|
+
/>
|
|
114
212
|
<h1 className="type-display-2 text-balance text-fg">{post.title}</h1>
|
|
115
213
|
<p className="type-reading max-w-[40rem] text-muted">{post.description}</p>
|
|
214
|
+
{stackedAfter ? (
|
|
215
|
+
<p className="type-eyebrow text-muted lg:hidden">
|
|
216
|
+
<time dateTime={post.date}>{post.dateLabel}</time>
|
|
217
|
+
<span aria-hidden="true"> / </span>
|
|
218
|
+
<span className="sr-only">, </span>
|
|
219
|
+
{post.readingTime} read
|
|
220
|
+
</p>
|
|
221
|
+
) : null}
|
|
116
222
|
</header>
|
|
117
223
|
|
|
118
|
-
|
|
119
|
-
|
|
224
|
+
{stackedAfter ? null : panel}
|
|
225
|
+
|
|
226
|
+
<div
|
|
120
227
|
className={
|
|
121
|
-
"col-span-full lg:
|
|
122
|
-
metadataColumnClasses[metadataColumns] +
|
|
123
|
-
(metadataSide === "end" ? " lg:order-last" : "")
|
|
228
|
+
"col-span-full lg:mt-0 " + (stackedAfter ? "" : "mt-6 ") + articleColumnClasses[metadataColumns] + articleOrder
|
|
124
229
|
}
|
|
125
230
|
>
|
|
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
231
|
<SectionCaption as="p">Article</SectionCaption>
|
|
173
232
|
<Prose className="mt-6">{children}</Prose>
|
|
174
233
|
</div>
|
|
234
|
+
|
|
235
|
+
{stackedAfter ? panel : null}
|
|
175
236
|
</article>
|
|
176
237
|
</main>
|
|
177
238
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @thewhatmatters/wmds@0.4.
|
|
1
|
+
// @thewhatmatters/wmds@0.4.9 · 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,270 @@
|
|
|
1
|
+
// @thewhatmatters/wmds@0.4.9 · 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 ReactElement, 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
|
+
type BreadcrumbLinkItem,
|
|
16
|
+
} from "@thewhatmatters/wmds";
|
|
17
|
+
|
|
18
|
+
export interface ResourceImage {
|
|
19
|
+
src: string;
|
|
20
|
+
alt: string;
|
|
21
|
+
/** The image's own size, in px. */
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ResourceVideo {
|
|
27
|
+
src: string;
|
|
28
|
+
/** The frame shown before the video plays. */
|
|
29
|
+
poster: string;
|
|
30
|
+
/** A WebVTT captions file, when the video has speech. */
|
|
31
|
+
captions?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ResourceDetailItem {
|
|
35
|
+
id: string;
|
|
36
|
+
title: string;
|
|
37
|
+
/** The preview. Shown unless there is a video. */
|
|
38
|
+
image: ResourceImage;
|
|
39
|
+
/** One video, with controls. It does not play by itself. */
|
|
40
|
+
video?: ResourceVideo;
|
|
41
|
+
/** The eyebrow over the title and the Category row, for example "Tools". */
|
|
42
|
+
category: string;
|
|
43
|
+
tags?: string[];
|
|
44
|
+
/** Where the resource lives, for example the domain — the Source row and the Visit button. */
|
|
45
|
+
meta?: string;
|
|
46
|
+
/** The curator's note: one or two sentences. */
|
|
47
|
+
note: string;
|
|
48
|
+
/** ISO date the resource was added, for the time element. */
|
|
49
|
+
added: string;
|
|
50
|
+
/** The date as readers see it, for example "Sep 14, 2026". */
|
|
51
|
+
addedLabel: string;
|
|
52
|
+
/** The resource itself, on its own site. */
|
|
53
|
+
visitHref: string;
|
|
54
|
+
/** More metadata rows, after Source. */
|
|
55
|
+
rows?: { name: string; value: string }[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The media well: one image or one video, whole, on the page floor. */
|
|
59
|
+
const mediaClasses =
|
|
60
|
+
"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";
|
|
61
|
+
|
|
62
|
+
/** Over the grid, from md: the well takes the height the window allows and the media fits inside it. */
|
|
63
|
+
const mediaFillClasses =
|
|
64
|
+
"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";
|
|
65
|
+
|
|
66
|
+
function ResourceMedia({
|
|
67
|
+
item,
|
|
68
|
+
fill = false,
|
|
69
|
+
renderImage,
|
|
70
|
+
}: {
|
|
71
|
+
item: ResourceDetailItem;
|
|
72
|
+
fill?: boolean;
|
|
73
|
+
renderImage?: (image: ResourceImage) => ReactNode;
|
|
74
|
+
}) {
|
|
75
|
+
return (
|
|
76
|
+
<div className={fill ? mediaClasses + " " + mediaFillClasses : mediaClasses}>
|
|
77
|
+
{item.video != null ? (
|
|
78
|
+
<video key={item.video.src} controls playsInline preload="metadata" poster={item.video.poster}>
|
|
79
|
+
<source src={item.video.src} />
|
|
80
|
+
{item.video.captions != null ? (
|
|
81
|
+
<track kind="captions" src={item.video.captions} srcLang="en" label="English" default />
|
|
82
|
+
) : null}
|
|
83
|
+
</video>
|
|
84
|
+
) : renderImage != null ? (
|
|
85
|
+
renderImage(item.image)
|
|
86
|
+
) : (
|
|
87
|
+
<img src={item.image.src} alt={item.image.alt} width={item.image.width} height={item.image.height} />
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function ResourceInfo({
|
|
94
|
+
item,
|
|
95
|
+
headingId,
|
|
96
|
+
titleAs: Title,
|
|
97
|
+
}: {
|
|
98
|
+
item: ResourceDetailItem;
|
|
99
|
+
headingId: string;
|
|
100
|
+
titleAs: "h1" | "h2";
|
|
101
|
+
}) {
|
|
102
|
+
return (
|
|
103
|
+
<div className="flex flex-col gap-6">
|
|
104
|
+
<div className="flex flex-col gap-2">
|
|
105
|
+
<p className="type-eyebrow text-muted">{item.category}</p>
|
|
106
|
+
<Title id={headingId} className="type-heading-1 text-balance text-fg">
|
|
107
|
+
{item.title}
|
|
108
|
+
</Title>
|
|
109
|
+
<p className="type-reading text-fg">{item.note}</p>
|
|
110
|
+
<p className="type-supporting text-muted">
|
|
111
|
+
Added <time dateTime={item.added}>{item.addedLabel}</time>
|
|
112
|
+
</p>
|
|
113
|
+
</div>
|
|
114
|
+
<DescriptionList variant="mono" rule="dotted" aria-label="Details">
|
|
115
|
+
<DescriptionList.Item name="Category">{item.category}</DescriptionList.Item>
|
|
116
|
+
{item.tags != null && item.tags.length > 0 ? (
|
|
117
|
+
<DescriptionList.Item name="Tags">
|
|
118
|
+
{item.tags.map((tag) => (
|
|
119
|
+
<Badge key={tag} emphasis="outline" mono>
|
|
120
|
+
{tag}
|
|
121
|
+
</Badge>
|
|
122
|
+
))}
|
|
123
|
+
</DescriptionList.Item>
|
|
124
|
+
) : null}
|
|
125
|
+
{item.meta != null ? <DescriptionList.Item name="Source">{item.meta}</DescriptionList.Item> : null}
|
|
126
|
+
{item.rows?.map((row) => (
|
|
127
|
+
<DescriptionList.Item key={row.name} name={row.name}>
|
|
128
|
+
{row.value}
|
|
129
|
+
</DescriptionList.Item>
|
|
130
|
+
))}
|
|
131
|
+
</DescriptionList>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function VisitButton({ item }: { item: ResourceDetailItem }) {
|
|
137
|
+
return (
|
|
138
|
+
<Button role="primary" external render={<a href={item.visitHref} />}>
|
|
139
|
+
Visit {item.meta ?? "resource"}
|
|
140
|
+
</Button>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface ResourceDetailDialogProps {
|
|
145
|
+
item: ResourceDetailItem;
|
|
146
|
+
open: boolean;
|
|
147
|
+
/** Close, the scrim, and Escape. Go back to the grid's route here. */
|
|
148
|
+
onClose: () => void;
|
|
149
|
+
/** Go to the resource before this one. Leave it out on the first: the control is off. */
|
|
150
|
+
onPrevious?: () => void;
|
|
151
|
+
/** Go to the resource after this one. Leave it out on the last. */
|
|
152
|
+
onNext?: () => void;
|
|
153
|
+
/** Where this resource is in the list, for example "3 of 9". */
|
|
154
|
+
position?: string;
|
|
155
|
+
/** The app's image component, in place of img — (image) => <Image {...image} sizes="…" />. */
|
|
156
|
+
renderImage?: (image: ResourceImage) => ReactNode;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The detail over the grid: the page behind is dimmed and does not scroll. */
|
|
160
|
+
export function ResourceDetailDialog({
|
|
161
|
+
item,
|
|
162
|
+
open,
|
|
163
|
+
onClose,
|
|
164
|
+
onPrevious,
|
|
165
|
+
onNext,
|
|
166
|
+
position,
|
|
167
|
+
renderImage,
|
|
168
|
+
}: ResourceDetailDialogProps) {
|
|
169
|
+
const headingId = useId();
|
|
170
|
+
|
|
171
|
+
// Left and Right move between resources — except in the video, where they seek.
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (!open) return;
|
|
174
|
+
function onKeyDown(event: KeyboardEvent) {
|
|
175
|
+
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
|
|
176
|
+
if (event.target instanceof HTMLVideoElement) return;
|
|
177
|
+
if (event.key === "ArrowLeft" && onPrevious != null) {
|
|
178
|
+
event.preventDefault();
|
|
179
|
+
onPrevious();
|
|
180
|
+
} else if (event.key === "ArrowRight" && onNext != null) {
|
|
181
|
+
event.preventDefault();
|
|
182
|
+
onNext();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
document.addEventListener("keydown", onKeyDown);
|
|
186
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
187
|
+
}, [open, onPrevious, onNext]);
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<Dialog
|
|
191
|
+
open={open}
|
|
192
|
+
onOpenChange={(next) => {
|
|
193
|
+
if (!next) onClose();
|
|
194
|
+
}}
|
|
195
|
+
>
|
|
196
|
+
<Dialog.Content
|
|
197
|
+
size="full"
|
|
198
|
+
aria-labelledby={headingId}
|
|
199
|
+
headerStart={position != null ? <span className="type-supporting tabular-nums">{position}</span> : undefined}
|
|
200
|
+
headerEnd={
|
|
201
|
+
<>
|
|
202
|
+
<IconButton
|
|
203
|
+
size="sm"
|
|
204
|
+
role="ghost"
|
|
205
|
+
icon={<ChevronLeft />}
|
|
206
|
+
aria-label="Previous resource"
|
|
207
|
+
disabled={onPrevious == null}
|
|
208
|
+
onClick={onPrevious}
|
|
209
|
+
/>
|
|
210
|
+
<IconButton
|
|
211
|
+
size="sm"
|
|
212
|
+
role="ghost"
|
|
213
|
+
icon={<ChevronRight />}
|
|
214
|
+
aria-label="Next resource"
|
|
215
|
+
disabled={onNext == null}
|
|
216
|
+
onClick={onNext}
|
|
217
|
+
/>
|
|
218
|
+
</>
|
|
219
|
+
}
|
|
220
|
+
footer={<VisitButton item={item} />}
|
|
221
|
+
>
|
|
222
|
+
<div className="grid gap-6 md:h-full md:grid-cols-[minmax(0,1fr)_20rem] lg:grid-cols-[minmax(0,1fr)_24rem]">
|
|
223
|
+
<ResourceMedia item={item} fill renderImage={renderImage} />
|
|
224
|
+
<div className="md:min-h-0 md:overflow-y-auto">
|
|
225
|
+
<ResourceInfo item={item} headingId={headingId} titleAs="h2" />
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
</Dialog.Content>
|
|
229
|
+
</Dialog>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export interface ResourceDetailPageProps {
|
|
234
|
+
item: ResourceDetailItem;
|
|
235
|
+
/** The path to this resource, ending with it — Home, Resources, this resource. */
|
|
236
|
+
breadcrumb: BreadcrumbItemDef[];
|
|
237
|
+
/** The router's link for the breadcrumb, in place of a plain anchor — (item) => <Link href={item.href} />. */
|
|
238
|
+
renderLink?: (item: BreadcrumbLinkItem) => ReactElement;
|
|
239
|
+
/** The app's image component, in place of img — (image) => <Image {...image} sizes="…" />. */
|
|
240
|
+
renderImage?: (image: ResourceImage) => ReactNode;
|
|
241
|
+
/** Page-level layers inside the page grid — for example a GridOverlay. */
|
|
242
|
+
overlay?: ReactNode;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** The detail as its own page: opened directly, or from a shared link. */
|
|
246
|
+
export function ResourceDetailPage({ item, breadcrumb, renderLink, renderImage, overlay }: ResourceDetailPageProps) {
|
|
247
|
+
const headingId = useId();
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<main className="grid-page bg-body">
|
|
251
|
+
{overlay}
|
|
252
|
+
<article aria-labelledby={headingId} className="band py-6 sm:py-10 lg:py-14">
|
|
253
|
+
<Breadcrumb
|
|
254
|
+
items={breadcrumb}
|
|
255
|
+
variant="mono"
|
|
256
|
+
separator="slash"
|
|
257
|
+
renderLink={renderLink}
|
|
258
|
+
className="col-span-full mb-6"
|
|
259
|
+
/>
|
|
260
|
+
<div className="col-span-full lg:col-span-8">
|
|
261
|
+
<ResourceMedia item={item} renderImage={renderImage} />
|
|
262
|
+
</div>
|
|
263
|
+
<div className="col-span-full mt-6 flex flex-col items-start gap-6 lg:col-span-4 lg:mt-0">
|
|
264
|
+
<ResourceInfo item={item} headingId={headingId} titleAs="h1" />
|
|
265
|
+
<VisitButton item={item} />
|
|
266
|
+
</div>
|
|
267
|
+
</article>
|
|
268
|
+
</main>
|
|
269
|
+
);
|
|
270
|
+
}
|
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.9",
|
|
4
4
|
"patterns": [
|
|
5
5
|
{
|
|
6
6
|
"id": "components-accordion--flush",
|
|
@@ -442,6 +442,14 @@
|
|
|
442
442
|
"storybook": "?path=/story/components-field--multi-control",
|
|
443
443
|
"source": "src/components/molecules/Field/Field.stories.tsx"
|
|
444
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
|
+
},
|
|
445
453
|
{
|
|
446
454
|
"id": "components-footerreveal--marketing-hero-pattern",
|
|
447
455
|
"title": "Components/FooterReveal",
|
|
@@ -594,6 +602,22 @@
|
|
|
594
602
|
"storybook": "?path=/story/components-kbd--shortcut",
|
|
595
603
|
"source": "src/components/atoms/Kbd/Kbd.stories.tsx"
|
|
596
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
|
+
},
|
|
597
621
|
{
|
|
598
622
|
"id": "components-moremenu--action-menu",
|
|
599
623
|
"title": "Components/MoreMenu",
|
|
@@ -922,6 +946,14 @@
|
|
|
922
946
|
"storybook": "?path=/story/components-textsequence--shapes",
|
|
923
947
|
"source": "src/components/molecules/TextSequence/TextSequence.stories.tsx"
|
|
924
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
|
+
},
|
|
925
957
|
{
|
|
926
958
|
"id": "components-toast--stacked-notifications",
|
|
927
959
|
"title": "Components/Toast",
|
|
@@ -994,6 +1026,14 @@
|
|
|
994
1026
|
"storybook": "?path=/story/guides-chart-explorations--comparison-gallery",
|
|
995
1027
|
"source": "src/guides/ChartExplorations/ChartExplorations.stories.tsx"
|
|
996
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
|
+
},
|
|
997
1037
|
{
|
|
998
1038
|
"id": "guides-filter-panel--filtered-index",
|
|
999
1039
|
"title": "Guides/Filter panel",
|
|
@@ -1025,6 +1065,14 @@
|
|
|
1025
1065
|
"file": "patterns/guides-profile-typography--editorial-profile.tsx",
|
|
1026
1066
|
"storybook": "?path=/story/guides-profile-typography--editorial-profile",
|
|
1027
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"
|
|
1028
1076
|
}
|
|
1029
1077
|
]
|
|
1030
1078
|
}
|
package/package.json
CHANGED