@hraness/design-kit 0.35.1 → 0.35.3
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/ARTICLE_COPY.md +7 -1
- package/README.md +8 -2
- package/dist/{chunk-6bfr0g23.js → chunk-4tgt8egt.js} +17 -12
- package/dist/{chunk-c80g58j1.js → chunk-fmeg5cxf.js} +14 -6
- package/dist/index.js +1 -1
- package/dist/mockups/client.js +97 -87
- package/dist/react/index.js +4 -2
- package/dist/react/server.js +1 -1
- package/dist/stylex-manifest.json +1 -1
- package/package.json +1 -1
- package/portfolio-inventory.json +1 -1
- package/src/article-html.ts +41 -19
- package/src/mockups/client.tsx +69 -58
- package/src/mockups.css +15 -7
- package/src/react/article.tsx +32 -14
- package/src/react/design-gallery.tsx +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hraness/design-kit",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.3",
|
|
4
4
|
"description": "React components and CSS for Hraness apps and product sites: application shells, marketing sections, charts, themes, effects, and syntax highlighting. Built on @hraness/ui.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/portfolio-inventory.json
CHANGED
package/src/article-html.ts
CHANGED
|
@@ -83,6 +83,8 @@ export interface ArticleHtmlInput {
|
|
|
83
83
|
readonly id?: string;
|
|
84
84
|
readonly provenance: ArticleProvenanceRecord | null;
|
|
85
85
|
readonly published: ArticleIsoDate;
|
|
86
|
+
/** Show publication and update dates. Defaults to true; dates remain required and validated when hidden. */
|
|
87
|
+
readonly showDates?: boolean;
|
|
86
88
|
readonly toc?: readonly ArticleTocItem[];
|
|
87
89
|
readonly tocLabel?: string;
|
|
88
90
|
readonly updated?: ArticleIsoDate;
|
|
@@ -91,7 +93,7 @@ export interface ArticleHtmlInput {
|
|
|
91
93
|
/** The static equivalent of `MarketingArticle`. */
|
|
92
94
|
export function renderArticleHtml(input: ArticleHtmlInput): string {
|
|
93
95
|
const headingId = input.headingId ?? "article-title";
|
|
94
|
-
const { published, updated } = input;
|
|
96
|
+
const { published, updated, showDates = true } = input;
|
|
95
97
|
assertArticleDates(updated === undefined ? { published } : { published, updated });
|
|
96
98
|
const toc = input.toc ?? [];
|
|
97
99
|
for (const item of toc) {
|
|
@@ -99,16 +101,16 @@ export function renderArticleHtml(input: ArticleHtmlInput): string {
|
|
|
99
101
|
}
|
|
100
102
|
const tocId = `${headingId}-contents`;
|
|
101
103
|
const meta = [
|
|
102
|
-
input.author === undefined ? "" : renderArticleBylineHtml(input.author)
|
|
103
|
-
dateHtml("Published", published),
|
|
104
|
-
updated
|
|
105
|
-
].join(
|
|
104
|
+
input.author === undefined ? "" : renderArticleBylineHtml(input.author),
|
|
105
|
+
showDates ? dateHtml("Published", published) : "",
|
|
106
|
+
showDates && updated !== undefined ? dateHtml("Updated", updated) : "",
|
|
107
|
+
].filter(Boolean).join(SEPARATOR);
|
|
106
108
|
const header = [
|
|
107
109
|
'<header class="plain-publication__article-header">',
|
|
108
110
|
present(input.eyebrow) ? `<p class="plain-publication__eyebrow">${escapeArticleHtml(input.eyebrow)}</p>` : "",
|
|
109
111
|
`<h1 id="${escapeArticleHtml(headingId)}">${escapeArticleHtml(input.heading)}</h1>`,
|
|
110
112
|
present(input.dek) ? `<p class="plain-publication__article-dek">${escapeArticleHtml(input.dek)}</p>` : "",
|
|
111
|
-
`<p class="plain-publication__article-meta">${meta}</p>`,
|
|
113
|
+
meta === "" ? "" : `<p class="plain-publication__article-meta">${meta}</p>`,
|
|
112
114
|
input.provenance === null ? "" : renderArticleProvenanceHtml(input.provenance),
|
|
113
115
|
"</header>",
|
|
114
116
|
].join("");
|
|
@@ -135,21 +137,35 @@ export function renderArticleHtml(input: ArticleHtmlInput): string {
|
|
|
135
137
|
export function renderArticleSourcesHtml({
|
|
136
138
|
heading = ARTICLE_SOURCES_HEADING,
|
|
137
139
|
headingId = "article-sources",
|
|
140
|
+
showDates = true,
|
|
138
141
|
sources,
|
|
139
|
-
}: Readonly<{
|
|
142
|
+
}: Readonly<{
|
|
143
|
+
heading?: string;
|
|
144
|
+
headingId?: string;
|
|
145
|
+
/** Show check dates. Defaults to true; checkedOn remains required and validated when hidden. */
|
|
146
|
+
showDates?: boolean;
|
|
147
|
+
sources: readonly ArticleSourceItem[];
|
|
148
|
+
}>): string {
|
|
140
149
|
if (sources.length === 0) return "";
|
|
141
|
-
for (const source of sources)
|
|
150
|
+
for (const source of sources) {
|
|
151
|
+
assertArticleHref(source.href);
|
|
152
|
+
formatArticleDate(source.checkedOn);
|
|
153
|
+
}
|
|
142
154
|
return [
|
|
143
155
|
`<section aria-labelledby="${escapeArticleHtml(headingId)}" class="plain-publication__sources">`,
|
|
144
156
|
`<h2 id="${escapeArticleHtml(headingId)}">${escapeArticleHtml(heading)}</h2>`,
|
|
145
157
|
"<ol>",
|
|
146
|
-
...sources.map((source) =>
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
158
|
+
...sources.map((source) => {
|
|
159
|
+
const meta = [
|
|
160
|
+
present(source.publisher) ? escapeArticleHtml(source.publisher) : "",
|
|
161
|
+
showDates ? dateHtml("Checked", source.checkedOn) : "",
|
|
162
|
+
].filter(Boolean).join(SEPARATOR);
|
|
163
|
+
return [
|
|
164
|
+
`<li><a href="${escapeArticleHtml(source.href)}">${escapeArticleHtml(source.title)}</a>`,
|
|
165
|
+
meta === "" ? "" : `<span>${meta}</span>`,
|
|
166
|
+
"</li>",
|
|
167
|
+
].join("");
|
|
168
|
+
}),
|
|
153
169
|
"</ol></section>",
|
|
154
170
|
].join("");
|
|
155
171
|
}
|
|
@@ -228,6 +244,7 @@ export function renderArticleIndexHtml({
|
|
|
228
244
|
headingLevel = 2,
|
|
229
245
|
id,
|
|
230
246
|
items,
|
|
247
|
+
showDates = true,
|
|
231
248
|
summary,
|
|
232
249
|
}: Readonly<{
|
|
233
250
|
className?: string;
|
|
@@ -236,6 +253,8 @@ export function renderArticleIndexHtml({
|
|
|
236
253
|
headingLevel?: 1 | 2 | 3 | 4 | 5;
|
|
237
254
|
id?: string;
|
|
238
255
|
items: readonly ArticleIndexItem[];
|
|
256
|
+
/** Show publication and update dates. Defaults to true; item dates remain required and validated when hidden. */
|
|
257
|
+
showDates?: boolean;
|
|
239
258
|
summary?: string;
|
|
240
259
|
}>): string {
|
|
241
260
|
if (![1, 2, 3, 4, 5].includes(headingLevel)) throw new RangeError("Article index heading level must be 1 to 5.");
|
|
@@ -259,10 +278,13 @@ export function renderArticleIndexHtml({
|
|
|
259
278
|
present(item.eyebrow) ? `<p class="plain-publication__entry-label">${escapeArticleHtml(item.eyebrow)}</p>` : "",
|
|
260
279
|
`<${entry} class="plain-publication__entry-title"><a href="${escapeArticleHtml(item.href)}">${escapeArticleHtml(item.title)}</a></${entry}>`,
|
|
261
280
|
`<p class="plain-publication__entry-dek">${escapeArticleHtml(item.dek)}</p>`,
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
281
|
+
showDates ? [
|
|
282
|
+
'<p class="plain-publication__entry-meta">',
|
|
283
|
+
dateHtml("Published", item.published),
|
|
284
|
+
item.updated === undefined ? "" : SEPARATOR + dateHtml("Updated", item.updated),
|
|
285
|
+
"</p>",
|
|
286
|
+
].join("") : "",
|
|
287
|
+
"</article>",
|
|
266
288
|
].join("")),
|
|
267
289
|
"</div></section>",
|
|
268
290
|
].join("");
|
package/src/mockups/client.tsx
CHANGED
|
@@ -229,73 +229,84 @@ export function ModeShowcase<S extends string, M extends string, O extends strin
|
|
|
229
229
|
return (
|
|
230
230
|
<figure
|
|
231
231
|
aria-label={label?.(surface) ?? `Illustration of ${surface.label.toLowerCase()}`}
|
|
232
|
-
className={joinMockupClasses("hkm-showcase", className)}
|
|
232
|
+
className={joinMockupClasses("hkm-showcase", "hkm-modes", className)}
|
|
233
233
|
data-hkm-theme={theme}
|
|
234
234
|
data-nosnippet=""
|
|
235
235
|
>
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
))}
|
|
259
|
-
</div>
|
|
260
|
-
) : null}
|
|
261
|
-
<div className="hkm-showcase-settings">
|
|
262
|
-
<div className="hkm-showcase-row">
|
|
263
|
-
<span className="hkm-showcase-label" id={`${id}-mode`}>{modeLabel}</span>
|
|
264
|
-
<div aria-labelledby={`${id}-mode`} className="hkm-segmented" role="group">
|
|
265
|
-
{modes.map((entry) => (
|
|
266
|
-
<button aria-pressed={entry.id === mode} key={entry.id} onClick={() => chooseMode(entry.id)} type="button">
|
|
267
|
-
{entry.label}
|
|
268
|
-
</button>
|
|
269
|
-
))}
|
|
270
|
-
</div>
|
|
271
|
-
</div>
|
|
272
|
-
{options === undefined ? null : (
|
|
273
|
-
<div className="hkm-showcase-row">
|
|
274
|
-
<span className="hkm-showcase-label" id={`${id}-option`}>{optionLabel}</span>
|
|
275
|
-
<div aria-labelledby={`${id}-option`} className="hkm-segmented" role="group">
|
|
276
|
-
{options.map((entry) => (
|
|
277
|
-
<button aria-pressed={entry.id === option} disabled={optionInactive} key={entry.id} onClick={() => chooseOption(entry.id)} type="button">
|
|
278
|
-
{entry.label}
|
|
279
|
-
</button>
|
|
280
|
-
))}
|
|
281
|
-
</div>
|
|
282
|
-
</div>
|
|
283
|
-
)}
|
|
236
|
+
{surfaces.length > 1 ? (
|
|
237
|
+
<div aria-label="Surface" className="hkm-tabs hkm-folder-tabs" role="tablist">
|
|
238
|
+
{surfaces.map((entry) => (
|
|
239
|
+
<button
|
|
240
|
+
aria-controls={`${id}-panel`}
|
|
241
|
+
aria-selected={entry.id === surface.id}
|
|
242
|
+
className="hkm-tab"
|
|
243
|
+
id={`${id}-tab-${entry.id}`}
|
|
244
|
+
key={entry.id}
|
|
245
|
+
onClick={() => setSurfaceId(entry.id)}
|
|
246
|
+
onKeyDown={onTabKey}
|
|
247
|
+
ref={(element) => {
|
|
248
|
+
if (element === null) tabs.current.delete(entry.id);
|
|
249
|
+
else tabs.current.set(entry.id, element);
|
|
250
|
+
}}
|
|
251
|
+
role="tab"
|
|
252
|
+
tabIndex={entry.id === surface.id ? 0 : -1}
|
|
253
|
+
type="button"
|
|
254
|
+
>
|
|
255
|
+
{entry.label}
|
|
256
|
+
</button>
|
|
257
|
+
))}
|
|
284
258
|
</div>
|
|
285
|
-
|
|
286
|
-
</div>
|
|
259
|
+
) : null}
|
|
287
260
|
<div
|
|
288
261
|
aria-labelledby={surfaces.length > 1 ? `${id}-tab-${surface.id}` : undefined}
|
|
289
|
-
className="hkm-showcase-stage"
|
|
290
|
-
data-hkm-animated={animated ? "" : undefined}
|
|
291
|
-
data-hkm-from={animated ? previousMode : undefined}
|
|
262
|
+
className="hkm-showcase-stage hkm-mode-panel"
|
|
292
263
|
id={`${id}-panel`}
|
|
293
264
|
role={surfaces.length > 1 ? "tabpanel" : undefined}
|
|
294
265
|
style={{ "--hkm-page-height": `${height}px` } as CSSProperties}
|
|
295
266
|
>
|
|
296
|
-
<
|
|
297
|
-
|
|
298
|
-
|
|
267
|
+
<div className="hkm-showcase-controls">
|
|
268
|
+
<div className="hkm-showcase-settings">
|
|
269
|
+
<div className="hkm-showcase-row">
|
|
270
|
+
<span className="hkm-showcase-label" id={`${id}-mode`}>{modeLabel}</span>
|
|
271
|
+
<div aria-labelledby={`${id}-mode`} className="hkm-segmented" role="group">
|
|
272
|
+
{modes.map((entry) => (
|
|
273
|
+
<button aria-pressed={entry.id === mode} key={entry.id} onClick={() => chooseMode(entry.id)} type="button">
|
|
274
|
+
{entry.label}
|
|
275
|
+
</button>
|
|
276
|
+
))}
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
{options === undefined ? null : (
|
|
280
|
+
<div className="hkm-showcase-row">
|
|
281
|
+
<span className="hkm-showcase-label" id={`${id}-option`}>{optionLabel}</span>
|
|
282
|
+
<div aria-labelledby={`${id}-option`} className="hkm-segmented" role="group">
|
|
283
|
+
{options.map((entry) => (
|
|
284
|
+
<button aria-pressed={entry.id === option} disabled={optionInactive} key={entry.id} onClick={() => chooseOption(entry.id)} type="button">
|
|
285
|
+
{entry.label}
|
|
286
|
+
</button>
|
|
287
|
+
))}
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
)}
|
|
291
|
+
</div>
|
|
292
|
+
{hint === "" ? null : <p className="hkm-showcase-hint">{hint}</p>}
|
|
293
|
+
</div>
|
|
294
|
+
<div className="hkm-mode-stage">
|
|
295
|
+
{surfaces.map((entry) => (
|
|
296
|
+
<div
|
|
297
|
+
aria-hidden={entry.id !== surface.id}
|
|
298
|
+
className="hkm-mode-surface"
|
|
299
|
+
data-hkm-animated={entry.id === surface.id && animated ? "" : undefined}
|
|
300
|
+
data-hkm-from={entry.id === surface.id && animated ? previousMode : undefined}
|
|
301
|
+
inert={entry.id !== surface.id}
|
|
302
|
+
key={entry.id}
|
|
303
|
+
>
|
|
304
|
+
<FitToWidth minWidth={minWidth}>
|
|
305
|
+
{entry.render({ animated: entry.id === surface.id && animated, mode, option, previousMode, theme })}
|
|
306
|
+
</FitToWidth>
|
|
307
|
+
</div>
|
|
308
|
+
))}
|
|
309
|
+
</div>
|
|
299
310
|
</div>
|
|
300
311
|
{!hasStatus && captionText === undefined ? null : (
|
|
301
312
|
<figcaption className="hkm-showcase-caption">
|
|
@@ -367,7 +378,7 @@ export function StepThrough({
|
|
|
367
378
|
return (
|
|
368
379
|
<figure aria-label={`Illustration: ${label.toLowerCase()}`} className={joinMockupClasses("hkm-showcase", "hkm-steps", className)} data-hkm-theme={theme} data-nosnippet="">
|
|
369
380
|
<div className="hkm-showcase-controls">
|
|
370
|
-
<div aria-label={label} className="hkm-tabs hkm-step-tabs" role="tablist">
|
|
381
|
+
<div aria-label={label} className="hkm-tabs hkm-folder-tabs hkm-step-tabs" role="tablist">
|
|
371
382
|
{steps.map((entry, position) => (
|
|
372
383
|
<button
|
|
373
384
|
aria-controls={`${id}-panel-${entry.id}`}
|
package/src/mockups.css
CHANGED
|
@@ -2503,14 +2503,22 @@
|
|
|
2503
2503
|
}
|
|
2504
2504
|
}
|
|
2505
2505
|
|
|
2506
|
-
/* Folder tabs share an edge with
|
|
2507
|
-
.hkm-showcase.hkm-steps { gap: 0; }
|
|
2506
|
+
/* Folder tabs share an edge with each showcase panel. */
|
|
2507
|
+
.hkm-showcase:is(.hkm-steps, .hkm-modes) { gap: 0; }
|
|
2508
2508
|
.hkm-showcase.hkm-steps .hkm-showcase-controls { padding: 0; border: 0; border-radius: 0; background: transparent; }
|
|
2509
|
-
.hkm-showcase
|
|
2510
|
-
.hkm-showcase.hkm-
|
|
2511
|
-
.hkm-showcase.hkm-
|
|
2512
|
-
.hkm-showcase.hkm-
|
|
2509
|
+
.hkm-showcase .hkm-folder-tabs { gap: 6px; margin: 0; padding: 0; border: 0; position: relative; z-index: 1; }
|
|
2510
|
+
.hkm-showcase .hkm-folder-tabs .hkm-tab { min-block-size: 48px; padding: 12px 18px; border: 1px solid var(--hkm-sc-line); border-block-end: 0; border-radius: 12px 12px 0 0; background: var(--hkm-sc-field); color: var(--hkm-sc-muted); }
|
|
2511
|
+
.hkm-showcase .hkm-folder-tabs .hkm-tab[aria-selected="true"] { background: var(--hkm-sc-bg); color: var(--hkm-sc-fg); }
|
|
2512
|
+
.hkm-showcase .hkm-folder-tabs .hkm-tab:hover { color: var(--hkm-sc-fg); }
|
|
2513
2513
|
.hkm-showcase.hkm-steps .hkm-step-stage { display: grid; border: 1px solid var(--hkm-sc-line); border-radius: 0 12px 12px 12px; background: var(--hkm-sc-bg); margin-block-start: -1px; overflow: clip; }
|
|
2514
|
+
.hkm-showcase.hkm-modes .hkm-mode-panel { border: 1px solid var(--hkm-sc-line); border-radius: 12px; background: var(--hkm-sc-bg); overflow: clip; }
|
|
2515
|
+
.hkm-showcase.hkm-modes .hkm-folder-tabs + .hkm-mode-panel { border-start-start-radius: 0; margin-block-start: -1px; }
|
|
2516
|
+
.hkm-showcase.hkm-modes .hkm-showcase-controls { border: 0; border-block-end: 1px solid var(--hkm-sc-line); border-radius: 0; }
|
|
2517
|
+
.hkm-mode-stage { display: grid; min-inline-size: 0; }
|
|
2518
|
+
.hkm-mode-surface { grid-area: 1 / 1; min-inline-size: 0; align-self: start; }
|
|
2519
|
+
.hkm-mode-surface[aria-hidden="true"] { visibility: hidden; pointer-events: none; }
|
|
2520
|
+
.hkm-showcase.hkm-modes .hkm-mode-surface > .hkm-fit > .hkm-fit-inner > .hkm-root,
|
|
2521
|
+
.hkm-showcase.hkm-modes .hkm-mode-surface > .hkm-fit > .hkm-fit-inner > .hkm-root > .hkm-window { border: 0; border-radius: 0; box-shadow: none; }
|
|
2514
2522
|
.hkm-step-panel { grid-area: 1 / 1; min-inline-size: 0; align-self: start; }
|
|
2515
2523
|
.hkm-step-panel[aria-hidden="true"] { visibility: hidden; pointer-events: none; }
|
|
2516
2524
|
.hkm-showcase.hkm-steps .hkm-step-panel .hkm-root { border-start-start-radius: 0; border-start-end-radius: 0; box-shadow: none; }
|
|
@@ -2520,7 +2528,7 @@
|
|
|
2520
2528
|
.hkm-showcase.hkm-steps .hkm-step-button:disabled { color: var(--hkm-sc-muted); background: transparent; }
|
|
2521
2529
|
.hkm-showcase.hkm-steps .hkm-step-button[data-hkm-primary]:disabled { background: var(--hkm-sc-field); }
|
|
2522
2530
|
.hkm-showcase.hkm-steps .hkm-step-announcement { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
2523
|
-
.hkm-showcase.hkm-steps .hkm-showcase-caption { margin-block-start: 1rem; }
|
|
2531
|
+
.hkm-showcase:is(.hkm-steps, .hkm-modes) .hkm-showcase-caption { margin-block-start: 1rem; }
|
|
2524
2532
|
|
|
2525
2533
|
/* ---------------------------------------------------------------- */
|
|
2526
2534
|
/* Motion */
|
package/src/react/article.tsx
CHANGED
|
@@ -93,6 +93,8 @@ export interface MarketingArticleProps {
|
|
|
93
93
|
/** Required so every article decides its drafting note. Pass null only for writing with no AI involvement and no review record. */
|
|
94
94
|
readonly provenance: ArticleProvenanceRecord | null;
|
|
95
95
|
readonly published: ArticleIsoDate;
|
|
96
|
+
/** Show publication and update dates. Defaults to true; dates remain required and validated when hidden. */
|
|
97
|
+
readonly showDates?: boolean;
|
|
96
98
|
readonly toc?: readonly ArticleTocItem[];
|
|
97
99
|
readonly tocLabel?: string;
|
|
98
100
|
readonly updated?: ArticleIsoDate;
|
|
@@ -116,6 +118,7 @@ export function MarketingArticle({
|
|
|
116
118
|
id,
|
|
117
119
|
provenance,
|
|
118
120
|
published,
|
|
121
|
+
showDates = true,
|
|
119
122
|
toc,
|
|
120
123
|
tocLabel = ARTICLE_TOC_LABEL,
|
|
121
124
|
updated,
|
|
@@ -138,11 +141,13 @@ export function MarketingArticle({
|
|
|
138
141
|
{eyebrow === undefined || eyebrow === "" ? null : <p className="plain-publication__eyebrow">{eyebrow}</p>}
|
|
139
142
|
<h1 id={headingId}>{heading}</h1>
|
|
140
143
|
{dek === undefined || dek === "" ? null : <p className="plain-publication__article-dek">{dek}</p>}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
{author === undefined && !showDates ? null : (
|
|
145
|
+
<p className="plain-publication__article-meta">
|
|
146
|
+
{author === undefined ? null : <><ArticleByline author={author} />{showDates ? <Separator /> : null}</>}
|
|
147
|
+
{showDates ? <ArticleDate label="Published" value={published} /> : null}
|
|
148
|
+
{!showDates || updated === undefined ? null : <><Separator /><ArticleDate label="Updated" value={updated} /></>}
|
|
149
|
+
</p>
|
|
150
|
+
)}
|
|
146
151
|
{provenance === null ? null : <ArticleProvenance provenance={provenance} />}
|
|
147
152
|
</header>
|
|
148
153
|
<div className="plain-publication__article-layout">
|
|
@@ -165,14 +170,20 @@ export function MarketingArticle({
|
|
|
165
170
|
export function ArticleSources({
|
|
166
171
|
heading = ARTICLE_SOURCES_HEADING,
|
|
167
172
|
headingId = "article-sources",
|
|
173
|
+
showDates = true,
|
|
168
174
|
sources,
|
|
169
175
|
}: Readonly<{
|
|
170
176
|
heading?: string;
|
|
171
177
|
headingId?: string;
|
|
178
|
+
/** Show check dates. Defaults to true; checkedOn remains required and validated when hidden. */
|
|
179
|
+
showDates?: boolean;
|
|
172
180
|
sources: readonly ArticleSourceItem[];
|
|
173
181
|
}>) {
|
|
174
182
|
if (sources.length === 0) return null;
|
|
175
|
-
for (const source of sources)
|
|
183
|
+
for (const source of sources) {
|
|
184
|
+
assertArticleHref(source.href);
|
|
185
|
+
formatArticleDate(source.checkedOn);
|
|
186
|
+
}
|
|
176
187
|
return (
|
|
177
188
|
<section aria-labelledby={headingId} className="plain-publication__sources">
|
|
178
189
|
<h2 id={headingId}>{heading}</h2>
|
|
@@ -180,10 +191,12 @@ export function ArticleSources({
|
|
|
180
191
|
{sources.map((source) => (
|
|
181
192
|
<li key={`${source.href}-${source.title}`}>
|
|
182
193
|
<a href={source.href}>{source.title}</a>
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
194
|
+
{!showDates && (source.publisher === undefined || source.publisher === "") ? null : (
|
|
195
|
+
<span>
|
|
196
|
+
{source.publisher === undefined || source.publisher === "" ? null : <>{source.publisher}{showDates ? <Separator /> : null}</>}
|
|
197
|
+
{showDates ? <ArticleDate label="Checked" value={source.checkedOn} /> : null}
|
|
198
|
+
</span>
|
|
199
|
+
)}
|
|
187
200
|
</li>
|
|
188
201
|
))}
|
|
189
202
|
</ol>
|
|
@@ -253,6 +266,7 @@ export function ArticleIndex({
|
|
|
253
266
|
headingLevel = 2,
|
|
254
267
|
id,
|
|
255
268
|
items,
|
|
269
|
+
showDates = true,
|
|
256
270
|
summary,
|
|
257
271
|
}: Readonly<{
|
|
258
272
|
className?: string;
|
|
@@ -261,6 +275,8 @@ export function ArticleIndex({
|
|
|
261
275
|
headingLevel?: 1 | 2 | 3 | 4 | 5;
|
|
262
276
|
id?: string;
|
|
263
277
|
items: readonly ArticleIndexItem[];
|
|
278
|
+
/** Show publication and update dates. Defaults to true; item dates remain required and validated when hidden. */
|
|
279
|
+
showDates?: boolean;
|
|
264
280
|
summary?: string;
|
|
265
281
|
}>) {
|
|
266
282
|
if (![1, 2, 3, 4, 5].includes(headingLevel)) throw new RangeError("Article index heading level must be 1 to 5.");
|
|
@@ -290,10 +306,12 @@ export function ArticleIndex({
|
|
|
290
306
|
{item.eyebrow === undefined || item.eyebrow === "" ? null : <p className="plain-publication__entry-label">{item.eyebrow}</p>}
|
|
291
307
|
<EntryTag className="plain-publication__entry-title"><a href={item.href}>{item.title}</a></EntryTag>
|
|
292
308
|
<p className="plain-publication__entry-dek">{item.dek}</p>
|
|
293
|
-
|
|
294
|
-
<
|
|
295
|
-
|
|
296
|
-
|
|
309
|
+
{showDates ? (
|
|
310
|
+
<p className="plain-publication__entry-meta">
|
|
311
|
+
<ArticleDate label="Published" value={item.published} />
|
|
312
|
+
{item.updated === undefined ? null : <><Separator /><ArticleDate label="Updated" value={item.updated} /></>}
|
|
313
|
+
</p>
|
|
314
|
+
) : null}
|
|
297
315
|
</article>
|
|
298
316
|
))}
|
|
299
317
|
</div>
|
|
@@ -678,6 +678,7 @@ export function DesignSystemGallery({
|
|
|
678
678
|
after={(
|
|
679
679
|
<ArticleSources
|
|
680
680
|
headingId="gallery-article-sources"
|
|
681
|
+
showDates={false}
|
|
681
682
|
sources={[
|
|
682
683
|
{ checkedOn: "2026-09-20", href: "#articles", publisher: "Relay", title: "Relay 2.4 release notes" },
|
|
683
684
|
]}
|
|
@@ -693,6 +694,7 @@ export function DesignSystemGallery({
|
|
|
693
694
|
review: { reviewer: "an independent AI editorial review", reviewerType: "ai" },
|
|
694
695
|
}}
|
|
695
696
|
published="2026-09-10"
|
|
697
|
+
showDates={false}
|
|
696
698
|
toc={[
|
|
697
699
|
{ href: "#gallery-article-problem", label: "The problem" },
|
|
698
700
|
{ href: "#gallery-article-approach", label: "The approach" },
|