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