@thewhatmatters/wmds 0.4.7 → 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.
Files changed (175) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/CONSUMING.md +3 -0
  3. package/dist/components/molecules/LinkTile/LinkTile.d.ts +45 -0
  4. package/dist/components/molecules/LinkTile/LinkTile.d.ts.map +1 -0
  5. package/dist/components/molecules/LinkTile/LinkTile.js +115 -0
  6. package/dist/components/molecules/LinkTile/linkTileStyles.d.ts +31 -0
  7. package/dist/components/molecules/LinkTile/linkTileStyles.d.ts.map +1 -0
  8. package/dist/components/molecules/LinkTile/linkTileStyles.js +8 -0
  9. package/dist/components/molecules/TileGrid/TileGrid.d.ts +42 -0
  10. package/dist/components/molecules/TileGrid/TileGrid.d.ts.map +1 -0
  11. package/dist/components/molecules/TileGrid/TileGrid.js +94 -0
  12. package/dist/components/molecules/TileGrid/tileGridMath.d.ts +8 -0
  13. package/dist/components/molecules/TileGrid/tileGridMath.d.ts.map +1 -0
  14. package/dist/components/molecules/TileGrid/tileGridMath.js +20 -0
  15. package/dist/components/molecules/TileGrid/tileGridStyles.d.ts +33 -0
  16. package/dist/components/molecules/TileGrid/tileGridStyles.d.ts.map +1 -0
  17. package/dist/components/molecules/TileGrid/tileGridStyles.js +16 -0
  18. package/dist/components/organisms/Dialog/Dialog.d.ts +12 -2
  19. package/dist/components/organisms/Dialog/Dialog.d.ts.map +1 -1
  20. package/dist/components/organisms/Dialog/Dialog.js +46 -44
  21. package/dist/components/organisms/Dialog/OverlayPanelHeader.d.ts +3 -1
  22. package/dist/components/organisms/Dialog/OverlayPanelHeader.d.ts.map +1 -1
  23. package/dist/components/organisms/Dialog/OverlayPanelHeader.js +20 -17
  24. package/dist/components/organisms/Dialog/dialogStyles.d.ts +5 -1
  25. package/dist/components/organisms/Dialog/dialogStyles.d.ts.map +1 -1
  26. package/dist/components/organisms/Dialog/dialogStyles.js +13 -11
  27. package/dist/components/organisms/FilterPanel/FilterPanel.d.ts +54 -0
  28. package/dist/components/organisms/FilterPanel/FilterPanel.d.ts.map +1 -0
  29. package/dist/components/organisms/FilterPanel/FilterPanel.js +137 -0
  30. package/dist/components/organisms/FilterPanel/filterSelection.d.ts +44 -0
  31. package/dist/components/organisms/FilterPanel/filterSelection.d.ts.map +1 -0
  32. package/dist/components/organisms/FilterPanel/filterSelection.js +35 -0
  33. package/dist/index.d.ts +3 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +73 -67
  36. package/dist/lib/compositionAudit.d.ts.map +1 -1
  37. package/dist/styles.css +1 -1
  38. package/docs/components.md +7 -4
  39. package/docs/exports.json +43 -2
  40. package/docs/patterns/components-accordion--flush.tsx +1 -1
  41. package/docs/patterns/components-avatar--dropdown-row.tsx +1 -1
  42. package/docs/patterns/components-badge--outline-tags.tsx +1 -1
  43. package/docs/patterns/components-badge--with-avatar.tsx +1 -1
  44. package/docs/patterns/components-breadcrumb--editorial.tsx +1 -1
  45. package/docs/patterns/components-breadcrumb--long-path.tsx +1 -1
  46. package/docs/patterns/components-breadcrumb--path.tsx +1 -1
  47. package/docs/patterns/components-breadcrumb--router-links.tsx +1 -1
  48. package/docs/patterns/components-button-button--copy-button.tsx +1 -1
  49. package/docs/patterns/components-button-button--destructive-action.tsx +1 -1
  50. package/docs/patterns/components-button-button--external-link.tsx +1 -1
  51. package/docs/patterns/components-button-button--ghost-action.tsx +1 -1
  52. package/docs/patterns/components-button-button--inverse-action.tsx +1 -1
  53. package/docs/patterns/components-button-button--link-render.tsx +1 -1
  54. package/docs/patterns/components-button-button--multi-state-badge.tsx +1 -1
  55. package/docs/patterns/components-button-button--outline-mono.tsx +1 -1
  56. package/docs/patterns/components-button-button--primary-action.tsx +1 -1
  57. package/docs/patterns/components-button-button--row-hug.tsx +1 -1
  58. package/docs/patterns/components-button-button--row-layout.tsx +1 -1
  59. package/docs/patterns/components-button-button--secondary-action.tsx +1 -1
  60. package/docs/patterns/components-button-button--suggestion-pills.tsx +1 -1
  61. package/docs/patterns/components-button-button--with-count.tsx +1 -1
  62. package/docs/patterns/components-button-button--with-icon.tsx +1 -1
  63. package/docs/patterns/components-button-floatingactionbutton--vertical-actions.tsx +1 -1
  64. package/docs/patterns/components-calembed--pattern-skip-in-footer.tsx +1 -1
  65. package/docs/patterns/components-card-card--body-slot-occupancy-history.tsx +1 -1
  66. package/docs/patterns/components-card-card--headerless-media.tsx +1 -1
  67. package/docs/patterns/components-card-card--outlined-layout.tsx +1 -1
  68. package/docs/patterns/components-card-card--pinned-height.tsx +1 -1
  69. package/docs/patterns/components-card-selectablecard--selectable-cards-pattern.tsx +1 -1
  70. package/docs/patterns/components-carousel--image-carousel.tsx +1 -1
  71. package/docs/patterns/components-chart--audience-activity-heatmap.tsx +1 -1
  72. package/docs/patterns/components-chart--cartesian-no-data-gaps.tsx +1 -1
  73. package/docs/patterns/components-chart--occupancy-history-in-card.tsx +1 -1
  74. package/docs/patterns/components-chart--occupancy-in-card.tsx +1 -1
  75. package/docs/patterns/components-chart--post-reach-distribution.tsx +1 -1
  76. package/docs/patterns/components-chart--ranked-audience-breakdown.tsx +1 -1
  77. package/docs/patterns/components-chart--unit-grid-composition.tsx +1 -1
  78. package/docs/patterns/components-chatdock--pattern-chat-dock.tsx +1 -1
  79. package/docs/patterns/components-chatdock--pattern-start-project-gate.tsx +1 -1
  80. package/docs/patterns/components-chatqa--pattern-qa-with-reply.tsx +1 -1
  81. package/docs/patterns/components-checkbox-checkbox--default.tsx +1 -1
  82. package/docs/patterns/components-checkbox-checkbox--with-count.tsx +1 -1
  83. package/docs/patterns/components-checkbox-checkbox--with-description.tsx +1 -1
  84. package/docs/patterns/components-checkbox-checkboxgroup--default.tsx +1 -1
  85. package/docs/patterns/components-checkbox-checkboxgroup--option-counts.tsx +1 -1
  86. package/docs/patterns/components-descriptionlist--details.tsx +1 -1
  87. package/docs/patterns/components-descriptionlist--post-metadata.tsx +1 -1
  88. package/docs/patterns/components-dialog--alert-dialog-destructive.tsx +1 -1
  89. package/docs/patterns/components-dialog--default-dialog.tsx +1 -1
  90. package/docs/patterns/components-dialog--form-dialog.tsx +1 -1
  91. package/docs/patterns/components-displaycontrols--page-utilities.tsx +1 -1
  92. package/docs/patterns/components-field--horizontal-input.tsx +1 -1
  93. package/docs/patterns/components-field--horizontal-text-area.tsx +1 -1
  94. package/docs/patterns/components-field--multi-control.tsx +1 -1
  95. package/docs/patterns/components-filterpanel--panel.tsx +55 -0
  96. package/docs/patterns/components-footerreveal--marketing-hero-pattern.tsx +1 -1
  97. package/docs/patterns/components-footerreveal--marketing-hero-ruled-pattern.tsx +1 -1
  98. package/docs/patterns/components-footerreveal--marketing-hero-with-gallery-intro.tsx +1 -1
  99. package/docs/patterns/components-footerreveal--marketing-page-pattern.tsx +1 -1
  100. package/docs/patterns/components-footerreveal--ruled-grid-footer-pattern.tsx +1 -1
  101. package/docs/patterns/components-footerreveal--sequenced-marketing-hero.tsx +1 -1
  102. package/docs/patterns/components-herointro--two-line-intro.tsx +1 -1
  103. package/docs/patterns/components-herotilestack--marketing-hero-pattern.tsx +1 -1
  104. package/docs/patterns/components-herotilestack--marketing-hero-text-sequence.tsx +1 -1
  105. package/docs/patterns/components-herotilestack--marketing-hero-with-gallery-intro.tsx +1 -1
  106. package/docs/patterns/components-indexlist--post-index.tsx +1 -1
  107. package/docs/patterns/components-intakeconfirmation--booked-pattern.tsx +1 -1
  108. package/docs/patterns/components-intakeconfirmation--emailed-pattern.tsx +1 -1
  109. package/docs/patterns/components-intakeform--about-you-pattern.tsx +1 -1
  110. package/docs/patterns/components-intakeform--short-form-pattern.tsx +1 -1
  111. package/docs/patterns/components-intakemodal--intake-modal-pattern.tsx +1 -1
  112. package/docs/patterns/components-kbd--command-row.tsx +1 -1
  113. package/docs/patterns/components-kbd--numbered-choices.tsx +1 -1
  114. package/docs/patterns/components-kbd--shortcut.tsx +1 -1
  115. package/docs/patterns/components-linktile--resource-tile.tsx +33 -0
  116. package/docs/patterns/components-linktile--router-link.tsx +25 -0
  117. package/docs/patterns/components-moremenu--action-menu.tsx +1 -1
  118. package/docs/patterns/components-moremenu--in-card-header.tsx +1 -1
  119. package/docs/patterns/components-navlist--side-nav-settings.tsx +1 -1
  120. package/docs/patterns/components-pageheader--app-header-pattern.tsx +1 -1
  121. package/docs/patterns/components-pageheader--page-section-header.tsx +1 -1
  122. package/docs/patterns/components-pageheader--toolbar-header.tsx +1 -1
  123. package/docs/patterns/components-panel--end-detail-rail.tsx +1 -1
  124. package/docs/patterns/components-pillgroup--pill-group-pattern.tsx +1 -1
  125. package/docs/patterns/components-promptbar--pattern-prompt-bar.tsx +1 -1
  126. package/docs/patterns/components-prose--article-body.tsx +1 -1
  127. package/docs/patterns/components-radio-radiogroup--default.tsx +1 -1
  128. package/docs/patterns/components-scrollhorizontal--project-gallery-pattern.tsx +1 -1
  129. package/docs/patterns/components-scrollhorizontal--with-intro.tsx +1 -1
  130. package/docs/patterns/components-sectioncaption--column-caption.tsx +1 -1
  131. package/docs/patterns/components-sectioncaption--with-action.tsx +1 -1
  132. package/docs/patterns/components-sheet--bottom-sheet.tsx +1 -1
  133. package/docs/patterns/components-sheet--end-sheet.tsx +1 -1
  134. package/docs/patterns/components-sitenav--brand-and-cta.tsx +1 -1
  135. package/docs/patterns/components-sitenav--centered-links.tsx +1 -1
  136. package/docs/patterns/components-sitenav--compact.tsx +1 -1
  137. package/docs/patterns/components-sitenav--marketing-header.tsx +1 -1
  138. package/docs/patterns/components-sitenav--mega-menu.tsx +1 -1
  139. package/docs/patterns/components-stat--insights-row.tsx +1 -1
  140. package/docs/patterns/components-stepprogress--step-progress-pattern.tsx +1 -1
  141. package/docs/patterns/components-switch--default.tsx +1 -1
  142. package/docs/patterns/components-switch--settings-row.tsx +1 -1
  143. package/docs/patterns/components-tab--settings-pages.tsx +1 -1
  144. package/docs/patterns/components-taskrows--action-details.tsx +1 -1
  145. package/docs/patterns/components-taskrows--capsules.tsx +1 -1
  146. package/docs/patterns/components-taskrows--card-with-status-rows.tsx +1 -1
  147. package/docs/patterns/components-taskrows--controlled-expand.tsx +1 -1
  148. package/docs/patterns/components-taskrows--custom-slots.tsx +1 -1
  149. package/docs/patterns/components-taskrows--status-list.tsx +1 -1
  150. package/docs/patterns/components-taskrows--tag-chips.tsx +1 -1
  151. package/docs/patterns/components-textlink--external-destination.tsx +1 -1
  152. package/docs/patterns/components-textlink--inline-prose.tsx +1 -1
  153. package/docs/patterns/components-textlink--quiet-title.tsx +1 -1
  154. package/docs/patterns/components-textlink--supporting-copy.tsx +1 -1
  155. package/docs/patterns/components-textsequence--default.tsx +1 -1
  156. package/docs/patterns/components-textsequence--reduced-motion.tsx +1 -1
  157. package/docs/patterns/components-textsequence--shapes.tsx +1 -1
  158. package/docs/patterns/components-tilegrid--masonry.tsx +44 -0
  159. package/docs/patterns/components-toast--stacked-notifications.tsx +1 -1
  160. package/docs/patterns/components-tooltip--controlled.tsx +1 -1
  161. package/docs/patterns/components-tooltip--disabled-control.tsx +1 -1
  162. package/docs/patterns/components-tooltip--icon-action.tsx +1 -1
  163. package/docs/patterns/components-tooltip--keyboard-shortcut.tsx +1 -1
  164. package/docs/patterns/components-tooltip--placement.tsx +1 -1
  165. package/docs/patterns/foundations-scroll-fade--horizontal-rail.tsx +1 -1
  166. package/docs/patterns/foundations-scroll-fade--overflowing-list.tsx +1 -1
  167. package/docs/patterns/guides-chart-explorations--comparison-gallery.tsx +1 -1
  168. package/docs/patterns/guides-filter-panel--filtered-grid.tsx +182 -0
  169. package/docs/patterns/guides-filter-panel--filtered-index.tsx +64 -153
  170. package/docs/patterns/guides-overlay-flows--notification-preferences.tsx +1 -1
  171. package/docs/patterns/guides-post-page--post-page.tsx +98 -54
  172. package/docs/patterns/guides-profile-typography--editorial-profile.tsx +1 -1
  173. package/docs/patterns/guides-resource-detail--detail.tsx +261 -0
  174. package/docs/patterns/index.json +49 -1
  175. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- // @thewhatmatters/wmds@0.4.7 · Pattern — post page
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. Below lg it stacks above the article either way. Default: "start". */
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({ post, children, metadataSide = "start", metadataColumns = 4 }: PostPageProps) {
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
- <aside
119
- aria-labelledby={metadataHeadingId}
207
+ {stackedAfter ? null : panel}
208
+
209
+ <div
120
210
  className={
121
- "col-span-full lg:sticky lg:top-[calc(var(--site-nav-height)+1rem)] " +
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.7 · Pattern — editorial profile
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
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "package": "@thewhatmatters/wmds",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@thewhatmatters/wmds",
3
3
  "description": "WhatMatters Design System — React components, Tailwind v4 theme, and Storybook patterns",
4
- "version": "0.4.7",
4
+ "version": "0.4.8",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {