@hraness/design-kit 0.36.5 → 0.38.0
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/README.md +46 -5
- package/dist/{chunk-xa28y4fg.js → chunk-13bq7myc.js} +1 -1
- package/dist/{chunk-sabcr66p.js → chunk-gpsekwkj.js} +26 -0
- package/dist/{chunk-evb02bc1.js → chunk-kz59614q.js} +48 -1
- package/dist/{chunk-thv5t11w.js → chunk-s222cmt9.js} +1 -1
- package/dist/chunk-san9drwt.js +1807 -0
- package/dist/chunk-z974c20s.js +775 -0
- package/dist/index.js +2 -2
- package/dist/mockups/client.js +6 -654
- package/dist/mockups/index.js +29 -1797
- package/dist/provider-marks.js +1 -1
- package/dist/react/index.js +156 -4
- package/dist/react/server.js +2 -2
- package/dist/stylex-manifest.json +1 -1
- package/package.json +2 -2
- package/portfolio-inventory.json +1 -1
- package/src/compiler-foundation.css +1 -0
- package/src/design-gallery.css +12 -0
- package/src/mockups/client.tsx +157 -66
- package/src/mockups/core.tsx +30 -0
- package/src/mockups.css +158 -23
- package/src/provider-marks.generated.ts +8 -0
- package/src/provider-marks.ts +2 -0
- package/src/react/design-gallery.tsx +34 -0
- package/src/styles.css +1 -0
- package/vendor/provider-marks/BOOTSTRAP-ICONS-LICENSE +21 -0
- package/vendor/provider-marks/UPSTREAM.md +14 -0
- package/vendor/provider-marks/bootstrap-icons/linkedin.svg +3 -0
- package/vendor/provider-marks/simple-icons/apple.svg +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hraness/design-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
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",
|
|
@@ -323,7 +323,7 @@
|
|
|
323
323
|
"test": "bun test --preload ./scripts/register-stylex-test-transform.ts ./src",
|
|
324
324
|
"browser:install": "bun ./node_modules/playwright-core/cli.js install chromium",
|
|
325
325
|
"test:browser": "bun run test:browser:marketing && bun run test:browser:rest",
|
|
326
|
-
"test:browser:marketing": "bun run ./scripts/product-marketing-browser.ts
|
|
326
|
+
"test:browser:marketing": "bun run ./scripts/product-marketing-browser.ts",
|
|
327
327
|
"test:browser:rest": "bun run ./scripts/site-shell-browser.ts && bun run ./scripts/palette-browser.ts && bun run ./scripts/gallery-browser.ts && bun run ./scripts/global-error-browser.ts && bun run ./scripts/security-delivery-browser.ts && bun run ./scripts/paper-theme-browser.ts && bun run ./scripts/product-marketing-preset-browser.ts && bun run ./scripts/lantern-material-browser.ts && bun run ./scripts/foil-browser.ts && bun run ./scripts/hero-light-browser.ts && bun run ./scripts/status-page-browser.ts && bun run ./scripts/platform-install-browser.ts && bun run ./scripts/mockups-browser.ts && bun run ./scripts/agent-setup-browser.ts",
|
|
328
328
|
"test:package": "bun run ./scripts/package-smoke.ts && bun run ./scripts/mockups-import.ts",
|
|
329
329
|
"kb:refresh": "bunx --bun --package https://github.com/hraness/wordcell/releases/download/v0.24.0/hraness-wordcell-0.24.0.tgz wordcell refresh --root kb",
|
package/portfolio-inventory.json
CHANGED
package/src/design-gallery.css
CHANGED
|
@@ -7,6 +7,18 @@
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
@layer components.hraness-design-kit.legacy {
|
|
10
|
+
.design-gallery__walkthrough-preview { display: grid; gap: 1rem; padding: 1.5rem; font-size: 0.875rem; }
|
|
11
|
+
.design-gallery__walkthrough-context { margin: 0; color: var(--hkm-muted); font-size: 0.75rem; letter-spacing: 0.06em; }
|
|
12
|
+
.design-gallery__walkthrough-title { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin: 0; font-size: 1.125rem; font-weight: 600; }
|
|
13
|
+
.design-gallery__walkthrough-state { padding: 0.25rem 0.5rem; border: 1px solid var(--hkm-line); border-radius: 0.375rem; color: var(--hkm-muted); font-size: 0.75rem; font-weight: 500; }
|
|
14
|
+
.design-gallery__walkthrough-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; margin: 0; }
|
|
15
|
+
.design-gallery__walkthrough-facts dt { margin-block-end: 0.25rem; color: var(--hkm-muted); font-size: 0.75rem; }
|
|
16
|
+
.design-gallery__walkthrough-facts dd { margin: 0; }
|
|
17
|
+
.design-gallery__walkthrough-checks { margin: 0; padding: 0; list-style: none; }
|
|
18
|
+
.design-gallery__walkthrough-checks li { display: flex; justify-content: space-between; gap: 1rem; padding-block: 0.75rem; border-block-start: 1px solid var(--hkm-line); }
|
|
19
|
+
.design-gallery__walkthrough-checks li > :last-child { color: var(--hkm-muted); }
|
|
20
|
+
.design-gallery__walkthrough-result { display: grid; gap: 0.375rem; padding: 1rem; border: 1px solid var(--hkm-line); border-radius: 0.625rem; background: var(--hkm-surface); }
|
|
21
|
+
.design-gallery__walkthrough-result > span { color: var(--hkm-muted); font-size: 0.75rem; }
|
|
10
22
|
.design-gallery__lantern-intro {
|
|
11
23
|
margin: 0;
|
|
12
24
|
max-inline-size: 60ch;
|
package/src/mockups/client.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
useMemo,
|
|
10
10
|
useRef,
|
|
11
11
|
useState,
|
|
12
|
+
version,
|
|
12
13
|
} from "react";
|
|
13
14
|
|
|
14
15
|
import { joinMockupClasses, type MockupTheme } from "./core.js";
|
|
@@ -23,6 +24,18 @@ import { joinMockupClasses, type MockupTheme } from "./core.js";
|
|
|
23
24
|
// measure only in the browser.
|
|
24
25
|
const useIsomorphicLayoutEffect: typeof useLayoutEffect = typeof document === "undefined" ? () => undefined : useLayoutEffect;
|
|
25
26
|
|
|
27
|
+
// React 18 drops unknown boolean attributes; React 19 recognizes inert as
|
|
28
|
+
// boolean. Keep the actual HTML attribute in both supported runtimes. The
|
|
29
|
+
// record describes compatibility attributes without claiming React 18's
|
|
30
|
+
// HTMLAttributes (or React 19's boolean-only inert prop) accepts both forms.
|
|
31
|
+
const inertAttribute: Readonly<Record<string, true | "">> = {
|
|
32
|
+
inert: Number.parseInt(version, 10) >= 19 ? true : "",
|
|
33
|
+
};
|
|
34
|
+
const noInertAttribute: Readonly<Record<string, true | "">> = {};
|
|
35
|
+
function inertAttributes(inactive: boolean): Readonly<Record<string, true | "">> {
|
|
36
|
+
return inactive ? inertAttribute : noInertAttribute;
|
|
37
|
+
}
|
|
38
|
+
|
|
26
39
|
function assertUniqueIds(items: readonly Readonly<{ id: string }>[], what: string): void {
|
|
27
40
|
if (items.length === 0) throw new RangeError(`${what} needs at least one entry.`);
|
|
28
41
|
const seen = new Set<string>();
|
|
@@ -128,6 +141,8 @@ export type ShowcaseState<M extends string, O extends string = string> = Readonl
|
|
|
128
141
|
export type ShowcaseSurface<S extends string, M extends string, O extends string = string> = Readonly<{
|
|
129
142
|
id: S;
|
|
130
143
|
label: string;
|
|
144
|
+
/** Optional explanation of what this surface illustrates. */
|
|
145
|
+
hint?: string;
|
|
131
146
|
render: (state: ShowcaseState<M, O>) => ReactNode;
|
|
132
147
|
}>;
|
|
133
148
|
|
|
@@ -208,7 +223,7 @@ export function ModeShowcase<S extends string, M extends string, O extends strin
|
|
|
208
223
|
// Pure, nonanimated authored states reserve one maximum before any choice.
|
|
209
224
|
// They stay hidden and inert; only a detached measurement clone reveals them.
|
|
210
225
|
const measurements = useMemo(() => fit !== "fill" ? null : surfaces.flatMap((entry) => modes.flatMap((choice) => (options ?? [{ id: undefined }]).map((optionChoice) => (
|
|
211
|
-
<div aria-hidden="true" className="hkm-mode-surface" data-hkm-measurement="" hidden
|
|
226
|
+
<div aria-hidden="true" className="hkm-mode-surface" data-hkm-measurement="" hidden {...inertAttributes(true)} key={JSON.stringify([entry.id, choice.id, optionChoice.id])}>
|
|
212
227
|
<div className="hkm-fit"><div className="hkm-fit-inner">
|
|
213
228
|
{entry.render({ animated: false, mode: choice.id, option: optionChoice.id, previousMode: choice.id, theme })}
|
|
214
229
|
</div></div>
|
|
@@ -238,10 +253,12 @@ export function ModeShowcase<S extends string, M extends string, O extends strin
|
|
|
238
253
|
};
|
|
239
254
|
|
|
240
255
|
const statusNode = status?.({ mode, option, surface: surface.id });
|
|
241
|
-
const
|
|
242
|
-
.map((text) => optionalText(text, "ModeShowcase hint"))
|
|
243
|
-
|
|
244
|
-
|
|
256
|
+
const describeChoice = (surfaceHint: string | undefined, modeHint: string | undefined, optionHint: string | undefined) =>
|
|
257
|
+
[surfaceHint, modeHint, optionHint].map((text) => optionalText(text, "ModeShowcase hint")).filter((text) => text !== undefined).join(" ");
|
|
258
|
+
const hint = describeChoice(surface.hint, modeChoice.hint, optionInactive ? undefined : optionChoice?.hint);
|
|
259
|
+
const hints = [...new Set(surfaces.flatMap((entry) => modes.flatMap((choice) =>
|
|
260
|
+
(optionInactiveModes.includes(choice.id) ? [undefined] : (options ?? [undefined])).map((optionEntry) =>
|
|
261
|
+
describeChoice(entry.hint, choice.hint, optionEntry?.hint)))))].filter((text) => text !== "");
|
|
245
262
|
const hasStatus = statusNode !== undefined && statusNode !== null && statusNode !== false && statusNode !== "";
|
|
246
263
|
return (
|
|
247
264
|
<figure
|
|
@@ -307,7 +324,12 @@ export function ModeShowcase<S extends string, M extends string, O extends strin
|
|
|
307
324
|
</div>
|
|
308
325
|
)}
|
|
309
326
|
</div>
|
|
310
|
-
{
|
|
327
|
+
{hints.length === 0 ? null : (
|
|
328
|
+
<div className="hkm-showcase-hints">
|
|
329
|
+
<p className="hkm-showcase-hint">{hint}</p>
|
|
330
|
+
{hints.filter((text) => text !== hint).map((text) => <p aria-hidden="true" className="hkm-showcase-hint" data-hkm-reserving="" {...inertAttributes(true)} key={text}>{text}</p>)}
|
|
331
|
+
</div>
|
|
332
|
+
)}
|
|
311
333
|
</div>
|
|
312
334
|
<div className="hkm-mode-stage" ref={stage}>
|
|
313
335
|
{measurements}
|
|
@@ -317,7 +339,7 @@ export function ModeShowcase<S extends string, M extends string, O extends strin
|
|
|
317
339
|
className="hkm-mode-surface"
|
|
318
340
|
data-hkm-animated={entry.id === surface.id && animated ? "" : undefined}
|
|
319
341
|
data-hkm-from={entry.id === surface.id && animated ? previousMode : undefined}
|
|
320
|
-
|
|
342
|
+
{...inertAttributes(entry.id !== surface.id)}
|
|
321
343
|
key={entry.id}
|
|
322
344
|
>
|
|
323
345
|
<FitToWidth minWidth={fit === "fill" ? 1 : minWidth}>
|
|
@@ -345,7 +367,7 @@ export function ModeShowcase<S extends string, M extends string, O extends strin
|
|
|
345
367
|
export type ThroughStep = Readonly<{
|
|
346
368
|
id: string;
|
|
347
369
|
label: string;
|
|
348
|
-
/** Optional
|
|
370
|
+
/** Optional explanation beside the label, and below the preview on compact screens. */
|
|
349
371
|
hint?: string;
|
|
350
372
|
render: (state: Readonly<{ animated: boolean; theme: MockupTheme | undefined }>) => ReactNode;
|
|
351
373
|
}>;
|
|
@@ -416,14 +438,14 @@ function fitShowcaseStage(stage: HTMLDivElement, minimumHeight: number): void {
|
|
|
416
438
|
// The tallest complete slide establishes the floor. Fitted type never feeds
|
|
417
439
|
// back into it, and changing the active tab never requests a measurement.
|
|
418
440
|
stage.style.setProperty("--hkm-showcase-fill-height", `${naturalHeight}px`);
|
|
419
|
-
for (const fixture of probe.querySelectorAll("[data-hkm-measurement]")) fixture.remove();
|
|
420
441
|
probe.style.blockSize = `${naturalHeight}px`;
|
|
421
442
|
probe.removeAttribute("data-hkm-measuring");
|
|
422
443
|
const bodies = presentationBodies(stage).filter((body) => body.closest("[data-hkm-measurement]") === null);
|
|
423
444
|
const copies = presentationBodies(probe);
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
445
|
+
// Every authored state uses one readable size. A short terminal must not
|
|
446
|
+
// become a different typographic scale when the next tab has more lines.
|
|
447
|
+
let sharedSize = 4;
|
|
448
|
+
for (const copy of copies) {
|
|
427
449
|
const lines = [...copy.querySelectorAll<HTMLElement>(".hkm-terminal-line")];
|
|
428
450
|
const wrappedRows = (line: HTMLElement) => Math.ceil((line.getBoundingClientRect().height - 0.5) / Number.parseFloat(getComputedStyle(line).lineHeight));
|
|
429
451
|
const baselineRows = lines.map(wrappedRows);
|
|
@@ -435,8 +457,9 @@ function fitShowcaseStage(stage: HTMLDivElement, minimumHeight: number): void {
|
|
|
435
457
|
if (copy.scrollHeight <= copy.clientHeight + 1 && copy.scrollWidth <= copy.clientWidth + 1 && lines.every((line, index) => { const baseline = baselineRows[index]; return baseline !== undefined && wrappedRows(line) <= baseline; })) low = candidate;
|
|
436
458
|
else high = candidate;
|
|
437
459
|
}
|
|
438
|
-
|
|
460
|
+
sharedSize = Math.min(sharedSize, low);
|
|
439
461
|
}
|
|
462
|
+
for (const body of bodies) body.style.setProperty("--hkm-terminal-presentation-size", `${Math.floor(sharedSize * 1000) / 1000}rem`);
|
|
440
463
|
stage.setAttribute("data-hkm-fitted", "");
|
|
441
464
|
} finally {
|
|
442
465
|
probe.remove();
|
|
@@ -485,9 +508,12 @@ function useFittedShowcaseStage(fit: "natural" | "fill", source: unknown, variat
|
|
|
485
508
|
}
|
|
486
509
|
|
|
487
510
|
/**
|
|
488
|
-
*
|
|
489
|
-
* to
|
|
490
|
-
*
|
|
511
|
+
* A tabbed walkthrough: the steps and the preview share one edge. Compact
|
|
512
|
+
* layouts attach a tab strip to the preview's top edge between Back and Next;
|
|
513
|
+
* at 720px and wider the steps stack on its start edge with their
|
|
514
|
+
* explanations. All render functions stay mounted to reserve the tallest
|
|
515
|
+
* panel; inactive panels are inert and visually hidden. The keyboard model
|
|
516
|
+
* matches `ModeShowcase`.
|
|
491
517
|
*/
|
|
492
518
|
export function StepThrough({
|
|
493
519
|
caption,
|
|
@@ -500,7 +526,7 @@ export function StepThrough({
|
|
|
500
526
|
theme,
|
|
501
527
|
}: Readonly<{
|
|
502
528
|
steps: readonly ThroughStep[];
|
|
503
|
-
/** Fill the tallest natural slide and fit presentation terminals
|
|
529
|
+
/** Fill the tallest natural slide and fit presentation terminals at one shared size. */
|
|
504
530
|
fit?: "natural" | "fill";
|
|
505
531
|
caption?: string;
|
|
506
532
|
initial?: string;
|
|
@@ -516,17 +542,68 @@ export function StepThrough({
|
|
|
516
542
|
const id = useId();
|
|
517
543
|
const [index, setIndex] = useState(() => Math.max(0, steps.findIndex((step) => step.id === initial)));
|
|
518
544
|
const [animated, setAnimated] = useState(false);
|
|
545
|
+
const [vertical, setVertical] = useState(false);
|
|
546
|
+
const layout = useRef<HTMLDivElement>(null);
|
|
547
|
+
const tablist = useRef<HTMLDivElement>(null);
|
|
519
548
|
const tabs = useRef(new Map<number, HTMLButtonElement>());
|
|
520
549
|
const current = Math.min(index, steps.length - 1);
|
|
521
|
-
const
|
|
550
|
+
const hints = steps.map((entry) => optionalText(entry.hint, "StepThrough hint"));
|
|
551
|
+
const currentHint = hints[current];
|
|
552
|
+
|
|
553
|
+
useIsomorphicLayoutEffect(() => {
|
|
554
|
+
const node = layout.current;
|
|
555
|
+
if (node === null || typeof ResizeObserver === "undefined") return undefined;
|
|
556
|
+
const measure = () => setVertical(node.clientWidth >= 720);
|
|
557
|
+
measure();
|
|
558
|
+
const observer = new ResizeObserver(measure);
|
|
559
|
+
observer.observe(node);
|
|
560
|
+
return () => observer.disconnect();
|
|
561
|
+
}, []);
|
|
562
|
+
|
|
563
|
+
// A strip that cannot show every label in full keeps only the selected
|
|
564
|
+
// label visible; the rest show their numbers and keep their accessible names.
|
|
565
|
+
useIsomorphicLayoutEffect(() => {
|
|
566
|
+
const list = tablist.current;
|
|
567
|
+
if (list === null || typeof ResizeObserver === "undefined") return undefined;
|
|
568
|
+
let disposed = false;
|
|
569
|
+
const measure = () => {
|
|
570
|
+
if (disposed) return;
|
|
571
|
+
list.removeAttribute("data-hkm-compact");
|
|
572
|
+
if (vertical) return;
|
|
573
|
+
const truncated = [...list.querySelectorAll<HTMLElement>(".hkm-step-label")].some((label) => label.scrollWidth > label.clientWidth + 1);
|
|
574
|
+
if (truncated) list.setAttribute("data-hkm-compact", "");
|
|
575
|
+
};
|
|
576
|
+
measure();
|
|
577
|
+
const observer = new ResizeObserver(measure);
|
|
578
|
+
observer.observe(list);
|
|
579
|
+
void document.fonts.ready.then(measure);
|
|
580
|
+
return () => {
|
|
581
|
+
disposed = true;
|
|
582
|
+
observer.disconnect();
|
|
583
|
+
list.removeAttribute("data-hkm-compact");
|
|
584
|
+
};
|
|
585
|
+
}, [current, vertical]);
|
|
586
|
+
|
|
587
|
+
useIsomorphicLayoutEffect(() => {
|
|
588
|
+
const list = tablist.current;
|
|
589
|
+
const selected = tabs.current.get(current);
|
|
590
|
+
if (vertical || list === null || selected === undefined) return;
|
|
591
|
+
const bounds = list.getBoundingClientRect();
|
|
592
|
+
const active = selected.getBoundingClientRect();
|
|
593
|
+
// Scroll only the owned strip, without moving the surrounding page.
|
|
594
|
+
if (active.left < bounds.left) list.scrollLeft += active.left - bounds.left;
|
|
595
|
+
else if (active.right > bounds.right) list.scrollLeft += active.right - bounds.right;
|
|
596
|
+
}, [current, vertical]);
|
|
522
597
|
|
|
523
598
|
const go = (next: number, focus = false) => {
|
|
524
599
|
if (next < 0 || next >= steps.length || next === current) return;
|
|
525
600
|
setIndex(next);
|
|
526
601
|
setAnimated(true);
|
|
527
|
-
if (focus) tabs.current.get(next)?.focus();
|
|
602
|
+
if (focus) tabs.current.get(next)?.focus({ preventScroll: true });
|
|
528
603
|
};
|
|
529
604
|
const onTabKey = (event: KeyboardEvent<HTMLButtonElement>) => {
|
|
605
|
+
if (vertical && (event.key === "ArrowLeft" || event.key === "ArrowRight")) return;
|
|
606
|
+
if (!vertical && (event.key === "ArrowUp" || event.key === "ArrowDown")) return;
|
|
530
607
|
const next = nextTabIndex(event.key, current, steps.length);
|
|
531
608
|
if (next === undefined) return;
|
|
532
609
|
event.preventDefault();
|
|
@@ -535,61 +612,75 @@ export function StepThrough({
|
|
|
535
612
|
|
|
536
613
|
return (
|
|
537
614
|
<figure aria-label={`Illustration: ${label.toLowerCase()}`} className={joinMockupClasses("hkm-showcase", "hkm-steps", className)} data-hkm-fit={fit === "fill" ? fit : undefined} data-hkm-theme={theme} data-nosnippet="">
|
|
538
|
-
<div className="hkm-
|
|
539
|
-
<div
|
|
615
|
+
<div className="hkm-step-layout" ref={layout}>
|
|
616
|
+
<div className="hkm-showcase-controls hkm-step-controls">
|
|
617
|
+
<div className="hkm-step-nav">
|
|
618
|
+
<button aria-label="Back" className="hkm-step-button" disabled={current === 0} onClick={() => go(current - 1)} type="button">
|
|
619
|
+
<svg aria-hidden="true" focusable="false" height="20" viewBox="0 0 24 24" width="20"><path d="m14.5 5-7 7 7 7" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.75" /></svg>
|
|
620
|
+
</button>
|
|
621
|
+
<span aria-hidden="true" className="hkm-step-count">{current + 1} / {steps.length}</span>
|
|
622
|
+
<div aria-label={label} aria-orientation={vertical ? "vertical" : "horizontal"} className="hkm-tabs hkm-step-tabs" ref={tablist} role="tablist">
|
|
623
|
+
{steps.map((entry, position) => (
|
|
624
|
+
<button
|
|
625
|
+
aria-controls={`${id}-panel-${entry.id}`}
|
|
626
|
+
aria-describedby={hints[position] === undefined ? undefined : `${id}-hint-${entry.id}`}
|
|
627
|
+
aria-label={entry.label}
|
|
628
|
+
aria-selected={position === current}
|
|
629
|
+
className="hkm-tab"
|
|
630
|
+
data-hkm-done={position < current ? "" : undefined}
|
|
631
|
+
id={`${id}-tab-${entry.id}`}
|
|
632
|
+
key={entry.id}
|
|
633
|
+
onClick={() => go(position)}
|
|
634
|
+
onKeyDown={onTabKey}
|
|
635
|
+
ref={(element) => {
|
|
636
|
+
if (element === null) tabs.current.delete(position);
|
|
637
|
+
else tabs.current.set(position, element);
|
|
638
|
+
}}
|
|
639
|
+
role="tab"
|
|
640
|
+
tabIndex={position === current ? 0 : -1}
|
|
641
|
+
type="button"
|
|
642
|
+
>
|
|
643
|
+
<span aria-hidden="true" className="hkm-step-number">{position + 1}</span>
|
|
644
|
+
<span className="hkm-step-copy">
|
|
645
|
+
<span className="hkm-step-label">{entry.label}</span>
|
|
646
|
+
{hints[position] === undefined ? null : <span className="hkm-step-hint" id={`${id}-hint-${entry.id}`}>{hints[position]}</span>}
|
|
647
|
+
</span>
|
|
648
|
+
</button>
|
|
649
|
+
))}
|
|
650
|
+
</div>
|
|
651
|
+
<button aria-label="Next" className="hkm-step-button" disabled={current === steps.length - 1} onClick={() => go(current + 1)} type="button">
|
|
652
|
+
<svg aria-hidden="true" focusable="false" height="20" viewBox="0 0 24 24" width="20"><path d="m9.5 5 7 7-7 7" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.75" /></svg>
|
|
653
|
+
</button>
|
|
654
|
+
</div>
|
|
655
|
+
</div>
|
|
656
|
+
<div className="hkm-showcase-stage hkm-step-stage" ref={stage}>
|
|
540
657
|
{steps.map((entry, position) => (
|
|
541
|
-
<
|
|
542
|
-
aria-
|
|
543
|
-
aria-
|
|
544
|
-
className="hkm-
|
|
545
|
-
data-hkm-
|
|
546
|
-
id={`${id}-
|
|
658
|
+
<div
|
|
659
|
+
aria-hidden={position !== current}
|
|
660
|
+
aria-labelledby={`${id}-tab-${entry.id}`}
|
|
661
|
+
className="hkm-step-panel"
|
|
662
|
+
data-hkm-animated={position === current && animated ? "" : undefined}
|
|
663
|
+
id={`${id}-panel-${entry.id}`}
|
|
664
|
+
{...inertAttributes(position !== current)}
|
|
547
665
|
key={entry.id}
|
|
548
|
-
|
|
549
|
-
onKeyDown={onTabKey}
|
|
550
|
-
ref={(element) => {
|
|
551
|
-
if (element === null) tabs.current.delete(position);
|
|
552
|
-
else tabs.current.set(position, element);
|
|
553
|
-
}}
|
|
554
|
-
role="tab"
|
|
666
|
+
role="tabpanel"
|
|
555
667
|
tabIndex={position === current ? 0 : -1}
|
|
556
|
-
type="button"
|
|
557
668
|
>
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
669
|
+
{fit === "fill"
|
|
670
|
+
? <div className="hkm-fit"><div className="hkm-fit-inner">{entry.render({ animated: position === current && animated, theme })}</div></div>
|
|
671
|
+
: <FitToWidth minWidth={minWidth}>{entry.render({ animated: position === current && animated, theme })}</FitToWidth>}
|
|
672
|
+
</div>
|
|
561
673
|
))}
|
|
562
674
|
</div>
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
<div
|
|
567
|
-
aria-hidden={position !== current}
|
|
568
|
-
aria-labelledby={`${id}-tab-${entry.id}`}
|
|
569
|
-
className="hkm-step-panel"
|
|
570
|
-
data-hkm-animated={position === current && animated ? "" : undefined}
|
|
571
|
-
id={`${id}-panel-${entry.id}`}
|
|
572
|
-
inert={position !== current}
|
|
573
|
-
key={entry.id}
|
|
574
|
-
role="tabpanel"
|
|
575
|
-
>
|
|
576
|
-
{fit === "fill"
|
|
577
|
-
? <div className="hkm-fit"><div className="hkm-fit-inner">{entry.render({ animated: position === current && animated, theme })}</div></div>
|
|
578
|
-
: <FitToWidth minWidth={minWidth}>{entry.render({ animated: position === current && animated, theme })}</FitToWidth>}
|
|
675
|
+
{hints.some((hint) => hint !== undefined) ? (
|
|
676
|
+
<div aria-hidden="true" className="hkm-step-descriptions">
|
|
677
|
+
{steps.map((entry, position) => <p className="hkm-step-description" data-hkm-active={position === current ? "" : undefined} key={entry.id}>{hints[position]}</p>)}
|
|
579
678
|
</div>
|
|
580
|
-
)
|
|
581
|
-
</div>
|
|
582
|
-
<div className="hkm-step-nav">
|
|
583
|
-
<button aria-label="Back" className="hkm-step-button" disabled={current === 0} onClick={() => go(current - 1)} type="button">
|
|
584
|
-
<svg aria-hidden="true" focusable="false" height="28" viewBox="0 0 24 24" width="28"><path d="m14.5 5-7 7 7 7" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.75" /></svg>
|
|
585
|
-
</button>
|
|
586
|
-
<span aria-live="polite" className="hkm-showcase-status hkm-step-announcement">
|
|
587
|
-
Step {current + 1} of {steps.length}{step.hint === undefined ? "" : `. ${step.hint}`}
|
|
588
|
-
</span>
|
|
589
|
-
<button aria-label="Next" className="hkm-step-button" data-hkm-primary="" disabled={current === steps.length - 1} onClick={() => go(current + 1)} type="button">
|
|
590
|
-
<svg aria-hidden="true" focusable="false" height="28" viewBox="0 0 24 24" width="28"><path d="m9.5 5 7 7-7 7" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.75" /></svg>
|
|
591
|
-
</button>
|
|
679
|
+
) : null}
|
|
592
680
|
</div>
|
|
681
|
+
<span aria-live="polite" className="hkm-showcase-status hkm-step-announcement">
|
|
682
|
+
Step {current + 1} of {steps.length}{currentHint === undefined ? "" : `. ${currentHint}`}
|
|
683
|
+
</span>
|
|
593
684
|
{captionText === undefined ? null : (
|
|
594
685
|
<figcaption className="hkm-showcase-caption"><span>{captionText}</span></figcaption>
|
|
595
686
|
)}
|
package/src/mockups/core.tsx
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { CSSProperties, ReactNode } from "react";
|
|
2
2
|
|
|
3
|
+
import { providerMark, providerMarkMonogram, providerMarkOnAccent } from "../provider-marks.js";
|
|
4
|
+
|
|
3
5
|
/*
|
|
4
6
|
* Shared pieces for the code-built mockups. Server-safe plain React: no hooks,
|
|
5
7
|
* no context, no StyleX, and no runtime dependency other than React, so a film
|
|
@@ -306,6 +308,34 @@ export function MockupGlyph({
|
|
|
306
308
|
);
|
|
307
309
|
}
|
|
308
310
|
|
|
311
|
+
/**
|
|
312
|
+
* A product's brand mark from the shared provider-mark registry, such as
|
|
313
|
+
* "Apple Contacts", "LinkedIn", or "iMessage". `tile` draws the vendor glyph
|
|
314
|
+
* in readable ink on the brand color, like an app icon; `glyph` follows the
|
|
315
|
+
* surrounding text color. Names without a registered mark get a neutral
|
|
316
|
+
* monogram tile, never an invented logo. Decorative: always aria-hidden, so
|
|
317
|
+
* name the product in nearby text.
|
|
318
|
+
*/
|
|
319
|
+
export function MockupBrandMark({
|
|
320
|
+
className,
|
|
321
|
+
name,
|
|
322
|
+
size = 32,
|
|
323
|
+
variant = "tile",
|
|
324
|
+
}: Readonly<{ name: string; size?: number; variant?: "tile" | "glyph"; className?: string }>) {
|
|
325
|
+
if (!(size > 0)) throw new RangeError("MockupBrandMark size must be positive.");
|
|
326
|
+
const mark = providerMark(name);
|
|
327
|
+
const style = { "--hkm-mark-size": `${String(size)}px`, ...(mark === undefined || variant !== "tile" ? {} : { "--hkm-mark-accent": mark.accent, "--hkm-mark-ink": providerMarkOnAccent(mark) }) } as CSSProperties;
|
|
328
|
+
if (mark === undefined) {
|
|
329
|
+
return <span aria-hidden="true" className={joinMockupClasses("hkm-brand-mark", className)} data-hkm-monogram="" style={style}>{providerMarkMonogram(name)}</span>;
|
|
330
|
+
}
|
|
331
|
+
return (
|
|
332
|
+
<span aria-hidden="true" className={joinMockupClasses("hkm-brand-mark", className)} data-hkm-mark={mark.id} data-hkm-variant={variant} style={style}>
|
|
333
|
+
{/* Generated from vendored, sanitized artwork in provider-marks.generated.ts. */}
|
|
334
|
+
<svg dangerouslySetInnerHTML={{ __html: mark.glyph.body }} fill="currentColor" focusable="false" viewBox={mark.glyph.viewBox} />
|
|
335
|
+
</span>
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
309
339
|
/** The three window buttons at the start of a title bar. Decorative. */
|
|
310
340
|
export function WindowLights() {
|
|
311
341
|
return (
|