@hraness/design-kit 0.39.0 → 0.40.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/MARKETING_COPY.md +1 -0
- package/README.md +15 -2
- package/dist/{chunk-fxwed3e5.js → chunk-eqd57m4y.js} +53 -7
- package/dist/mockups/client.js +1 -1
- package/dist/react/index.js +4 -4
- package/dist/stylex-manifest.json +1 -1
- package/dist/testing.js +20 -0
- package/package.json +1 -1
- package/portfolio-inventory.json +1 -1
- package/src/mockups/client.tsx +57 -5
- package/src/mockups.css +53 -2
- package/src/react/design-gallery.tsx +3 -3
- package/src/testing.ts +19 -0
package/dist/testing.js
CHANGED
|
@@ -237,14 +237,34 @@ function assertFakeHandles(html, allowedHandles, label = "markup") {
|
|
|
237
237
|
fail(`${label} links to ${match[1] ?? ""}, which is not a reserved example domain.`);
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
|
+
var WALKTHROUGH_COPY_LIMITS = Object.freeze({
|
|
241
|
+
labelWords: 3,
|
|
242
|
+
hintCharacters: 60
|
|
243
|
+
});
|
|
244
|
+
function assertWalkthroughCopy(steps, label = "walkthrough") {
|
|
245
|
+
for (const step of steps) {
|
|
246
|
+
const words = step.label.trim().split(/\s+/u).filter(Boolean).length;
|
|
247
|
+
if (words === 0 || words > WALKTHROUGH_COPY_LIMITS.labelWords)
|
|
248
|
+
fail(`${label} step "${step.id}" label has ${String(words)} words; use one to ${String(WALKTHROUGH_COPY_LIMITS.labelWords)}.`);
|
|
249
|
+
const hint = step.hint?.trim();
|
|
250
|
+
if (hint === undefined || hint === "")
|
|
251
|
+
continue;
|
|
252
|
+
if (hint.length > WALKTHROUGH_COPY_LIMITS.hintCharacters)
|
|
253
|
+
fail(`${label} step "${step.id}" hint is ${String(hint.length)} characters; keep it within ${String(WALKTHROUGH_COPY_LIMITS.hintCharacters)}.`);
|
|
254
|
+
if ((hint.match(/[.!?](\s|$)/gu) ?? []).length > 1)
|
|
255
|
+
fail(`${label} step "${step.id}" hint has more than one sentence.`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
240
258
|
export {
|
|
241
259
|
stripMockupSamples,
|
|
242
260
|
renderMatrix,
|
|
243
261
|
htmlText,
|
|
244
262
|
blogConformance,
|
|
263
|
+
assertWalkthroughCopy,
|
|
245
264
|
assertRoleImgWithLabel,
|
|
246
265
|
assertNoHeadings,
|
|
247
266
|
assertFakeHandles,
|
|
267
|
+
WALKTHROUGH_COPY_LIMITS,
|
|
248
268
|
ConformanceError,
|
|
249
269
|
BLOG_CONFORMANCE_LIMITS
|
|
250
270
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hraness/design-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.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",
|
package/portfolio-inventory.json
CHANGED
package/src/mockups/client.tsx
CHANGED
|
@@ -373,17 +373,61 @@ export type ThroughStep = Readonly<{
|
|
|
373
373
|
}>;
|
|
374
374
|
|
|
375
375
|
/** Fill owns the outer shell and its top-level frames, never nested width-fit graphics. */
|
|
376
|
-
function
|
|
377
|
-
const
|
|
376
|
+
function panelFillFrames(stage: HTMLElement): HTMLElement[][] {
|
|
377
|
+
const panels: HTMLElement[][] = [];
|
|
378
378
|
for (const panel of stage.querySelectorAll<HTMLElement>(":scope > .hkm-step-panel, :scope > .hkm-mode-surface")) {
|
|
379
379
|
const fit = panel.querySelector<HTMLElement>(":scope > .hkm-fit");
|
|
380
380
|
if (fit === null) continue;
|
|
381
|
+
const frames: HTMLElement[] = [];
|
|
381
382
|
for (const root of fit.querySelectorAll<HTMLElement>(".hkm-root")) {
|
|
382
383
|
const enclosingRoot = root.parentElement?.closest(".hkm-root");
|
|
383
384
|
if (root.closest(".hkm-fit") === fit && (enclosingRoot === null || enclosingRoot === undefined || !fit.contains(enclosingRoot))) frames.push(root);
|
|
384
385
|
}
|
|
386
|
+
panels.push(frames);
|
|
387
|
+
}
|
|
388
|
+
return panels;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function fillFrames(stage: HTMLElement): HTMLElement[] {
|
|
392
|
+
return panelFillFrames(stage).flat();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Mark each panel's frame, and the product wrappers between it and the fit,
|
|
397
|
+
* so a frame wrapped in product markup still grows to the full stage instead
|
|
398
|
+
* of leaving an empty band under a shorter slide. A panel with several
|
|
399
|
+
* top-level frames keeps its own arrangement.
|
|
400
|
+
*/
|
|
401
|
+
function markFillFrames(stage: HTMLElement): void {
|
|
402
|
+
for (const frames of panelFillFrames(stage)) {
|
|
403
|
+
for (const frame of frames) frame.setAttribute("data-hkm-fill-frame", "");
|
|
404
|
+
const [frame] = frames;
|
|
405
|
+
if (frames.length !== 1 || frame === undefined) continue;
|
|
406
|
+
const path: HTMLElement[] = [];
|
|
407
|
+
for (let node = frame.parentElement; node !== null; node = node.parentElement) {
|
|
408
|
+
path.push(node);
|
|
409
|
+
if (node.classList.contains("hkm-fit-inner")) break;
|
|
410
|
+
}
|
|
411
|
+
// Only single-column wrappers become a stack; a side-by-side product
|
|
412
|
+
// layout keeps its own arrangement.
|
|
413
|
+
if (path.every(stacksVertically)) for (const node of path) node.setAttribute("data-hkm-fill-path", "");
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function stacksVertically(node: HTMLElement): boolean {
|
|
418
|
+
if (node.hasAttribute("data-hkm-fill-path")) return true;
|
|
419
|
+
const style = getComputedStyle(node);
|
|
420
|
+
if (style.display === "block" || style.display === "flow-root") return true;
|
|
421
|
+
if (style.display === "grid") return style.gridTemplateColumns.trim().split(/\s+/u).length === 1;
|
|
422
|
+
if (style.display === "flex") return style.flexDirection.startsWith("column");
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function clearFillFrames(stage: HTMLElement): void {
|
|
427
|
+
for (const node of stage.querySelectorAll("[data-hkm-fill-frame], [data-hkm-fill-path]")) {
|
|
428
|
+
node.removeAttribute("data-hkm-fill-frame");
|
|
429
|
+
node.removeAttribute("data-hkm-fill-path");
|
|
385
430
|
}
|
|
386
|
-
return frames;
|
|
387
431
|
}
|
|
388
432
|
|
|
389
433
|
function presentationBodies(stage: HTMLElement): HTMLElement[] {
|
|
@@ -413,7 +457,7 @@ function measureNestedFits(stage: HTMLElement): void {
|
|
|
413
457
|
function fitShowcaseStage(stage: HTMLDivElement, minimumHeight: number): void {
|
|
414
458
|
const owner = stage.parentElement;
|
|
415
459
|
if (owner === null || stage.clientWidth <= 0) return;
|
|
416
|
-
|
|
460
|
+
markFillFrames(stage);
|
|
417
461
|
const probe = stage.cloneNode(true) as HTMLDivElement;
|
|
418
462
|
probe.setAttribute("aria-hidden", "true");
|
|
419
463
|
probe.setAttribute("inert", "");
|
|
@@ -501,7 +545,7 @@ function useFittedShowcaseStage(fit: "natural" | "fill", source: unknown, variat
|
|
|
501
545
|
node.style.removeProperty("--hkm-showcase-fill-height");
|
|
502
546
|
node.removeAttribute("data-hkm-fitted");
|
|
503
547
|
for (const body of presentationBodies(node)) body.style.removeProperty("--hkm-terminal-presentation-size");
|
|
504
|
-
|
|
548
|
+
clearFillFrames(node);
|
|
505
549
|
};
|
|
506
550
|
}, [fit, source, variation, minimumHeight]);
|
|
507
551
|
return stage;
|
|
@@ -538,6 +582,14 @@ export function StepThrough({
|
|
|
538
582
|
assertUniqueIds(steps, "StepThrough step");
|
|
539
583
|
if (fit !== "natural" && fit !== "fill") throw new RangeError("StepThrough fit must be natural or fill.");
|
|
540
584
|
const stage = useFittedShowcaseStage(fit, steps);
|
|
585
|
+
// Natural walkthroughs fit by width only, but still stretch each slide's
|
|
586
|
+
// frame to the shared stage height. Fill walkthroughs mark frames while fitting.
|
|
587
|
+
useIsomorphicLayoutEffect(() => {
|
|
588
|
+
const node = stage.current;
|
|
589
|
+
if (fit === "fill" || node === null) return undefined;
|
|
590
|
+
markFillFrames(node);
|
|
591
|
+
return () => clearFillFrames(node);
|
|
592
|
+
}, [fit, steps]);
|
|
541
593
|
const captionText = optionalText(caption, "StepThrough");
|
|
542
594
|
const id = useId();
|
|
543
595
|
const [index, setIndex] = useState(() => Math.max(0, steps.findIndex((step) => step.id === initial)));
|
package/src/mockups.css
CHANGED
|
@@ -2593,11 +2593,37 @@
|
|
|
2593
2593
|
min-block-size: 0;
|
|
2594
2594
|
}
|
|
2595
2595
|
|
|
2596
|
-
|
|
2596
|
+
/* A slide never leaves an empty band under a shorter frame. Each panel's
|
|
2597
|
+
* single top-level frame grows to the stage through any product wrappers
|
|
2598
|
+
* (marked as the fill path), and its window body takes the remaining height. */
|
|
2599
|
+
/* Important so a product wrapper's own single-column grid or block layout
|
|
2600
|
+
* cannot leave the extra height in its rows instead of the frame. */
|
|
2601
|
+
.hkm-showcase :is(.hkm-step-stage, .hkm-mode-stage):not([data-hkm-measuring]) [data-hkm-fill-path] {
|
|
2602
|
+
display: flex !important;
|
|
2603
|
+
flex-direction: column !important;
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
.hkm-showcase :is(.hkm-step-stage, .hkm-mode-stage):not([data-hkm-measuring]) [data-hkm-fill-path] > :is([data-hkm-fill-path], [data-hkm-fill-frame]) {
|
|
2607
|
+
flex: 1 1 auto;
|
|
2608
|
+
block-size: auto;
|
|
2609
|
+
min-block-size: 0;
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
.hkm-showcase :is(.hkm-step-stage, .hkm-mode-stage):not([data-hkm-measuring]) [data-hkm-fill-path] > [data-hkm-fill-frame] > .hkm-window {
|
|
2613
|
+
block-size: 100%;
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
.hkm-showcase :is(.hkm-step-stage, .hkm-mode-stage):not([data-hkm-measuring]) [data-hkm-fill-frame] > .hkm-window:has(> .hkm-page),
|
|
2617
|
+
.hkm-showcase :is(.hkm-step-stage, .hkm-mode-stage):not([data-hkm-measuring]) [data-hkm-fill-path] > [data-hkm-fill-frame] > .hkm-window {
|
|
2597
2618
|
display: flex;
|
|
2598
2619
|
flex-direction: column;
|
|
2599
2620
|
}
|
|
2600
2621
|
|
|
2622
|
+
.hkm-showcase :is(.hkm-step-stage, .hkm-mode-stage):not([data-hkm-measuring]) [data-hkm-fill-path] > [data-hkm-fill-frame] > .hkm-window > :last-child:not(.hkm-title-bar, .hkm-browser-bar) {
|
|
2623
|
+
flex: 1 1 auto;
|
|
2624
|
+
min-block-size: 0;
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2601
2627
|
.hkm-showcase[data-hkm-fit="fill"] :is(.hkm-step-stage, .hkm-mode-stage):not([data-hkm-measuring]) [data-hkm-fill-frame] > .hkm-window > .hkm-page {
|
|
2602
2628
|
flex: 1 1 auto;
|
|
2603
2629
|
block-size: auto;
|
|
@@ -2605,10 +2631,35 @@
|
|
|
2605
2631
|
max-block-size: none;
|
|
2606
2632
|
}
|
|
2607
2633
|
|
|
2608
|
-
.hkm-showcase
|
|
2634
|
+
.hkm-showcase :is(.hkm-step-stage, .hkm-mode-stage):not([data-hkm-measuring]) [data-hkm-fill-frame] > .hkm-window > :is(.hkm-title-bar, .hkm-browser-bar) {
|
|
2609
2635
|
flex: none;
|
|
2610
2636
|
}
|
|
2611
2637
|
|
|
2638
|
+
/* The panel draws the frame, so a wrapped window drops its own edge too. */
|
|
2639
|
+
.hkm-showcase :is(.hkm-step-panel, .hkm-mode-surface) [data-hkm-fill-path] > [data-hkm-fill-frame],
|
|
2640
|
+
.hkm-showcase :is(.hkm-step-panel, .hkm-mode-surface) [data-hkm-fill-path] > [data-hkm-fill-frame] > .hkm-window {
|
|
2641
|
+
border: 0;
|
|
2642
|
+
border-radius: 0;
|
|
2643
|
+
box-shadow: none;
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
/* A walkthrough stage is at least as tall as its tallest slide and its step
|
|
2647
|
+
* list, so a selected step never hangs past the preview's edge. Natural
|
|
2648
|
+
* walkthroughs stretch their unscaled slides the same way. */
|
|
2649
|
+
.hkm-showcase.hkm-steps[data-hkm-fit="fill"] .hkm-step-stage:not([data-hkm-measuring]) {
|
|
2650
|
+
block-size: auto;
|
|
2651
|
+
min-block-size: var(--hkm-showcase-fill-height, 0px);
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
.hkm-showcase.hkm-steps:not([data-hkm-fit="fill"]) .hkm-step-stage > .hkm-step-panel {
|
|
2655
|
+
align-self: stretch;
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
.hkm-showcase.hkm-steps:not([data-hkm-fit="fill"]) .hkm-step-panel > .hkm-fit:not(:has(> [data-hkm-scaled])),
|
|
2659
|
+
.hkm-showcase.hkm-steps:not([data-hkm-fit="fill"]) .hkm-step-panel > .hkm-fit > .hkm-fit-inner:not([data-hkm-scaled]) {
|
|
2660
|
+
block-size: 100%;
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2612
2663
|
/* A walkthrough is one tabbed selector: the steps and the preview share an
|
|
2613
2664
|
* edge. Compact layouts attach a tab strip to the preview's top edge between
|
|
2614
2665
|
* Back and Next. Wide layouts stack the steps on its start edge, flush with
|
|
@@ -468,9 +468,9 @@ export function DesignSystemGallery({
|
|
|
468
468
|
</MarketingSection>
|
|
469
469
|
<MarketingSection heading="Show the result at each step." headingId="gallery-walkthrough" headingLevel={3} label="Walkthrough" summary="Use short labels and visible explanations beside a compact illustration of what changes.">
|
|
470
470
|
<StepThrough fit="fill" label="Run a Relay job" steps={[
|
|
471
|
-
{ id: "choose", label: "Choose a job", hint: "Pick a saved job and
|
|
472
|
-
{ id: "run", label: "Watch the run", hint: "
|
|
473
|
-
{ id: "inspect", label: "Read the result", hint: "Open the
|
|
471
|
+
{ id: "choose", label: "Choose a job", hint: "Pick a saved job and check the file it reads.", render: () => <GalleryJobPreview state="ready" /> },
|
|
472
|
+
{ id: "run", label: "Watch the run", hint: "The input check, the transform, and the saved output.", render: () => <GalleryJobPreview state="running" /> },
|
|
473
|
+
{ id: "inspect", label: "Read the result", hint: "Open the report beside its run ID and source.", render: () => <GalleryJobPreview state="saved" /> },
|
|
474
474
|
]} />
|
|
475
475
|
</MarketingSection>
|
|
476
476
|
<MarketingPrimitives
|
package/src/testing.ts
CHANGED
|
@@ -305,3 +305,22 @@ export function assertFakeHandles(html: string, allowedHandles: readonly string[
|
|
|
305
305
|
if (!RESERVED_HOST.test(match[1] ?? "")) fail(`${label} links to ${match[1] ?? ""}, which is not a reserved example domain.`);
|
|
306
306
|
}
|
|
307
307
|
}
|
|
308
|
+
|
|
309
|
+
/** Limits from MARKETING_COPY.md for `StepThrough` steps. */
|
|
310
|
+
export const WALKTHROUGH_COPY_LIMITS = Object.freeze({ labelWords: 3, hintCharacters: 60 });
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Walkthrough labels name a step in at most three words, and each hint is one
|
|
314
|
+
* sentence of at most 60 characters, so the wide step list stays no taller
|
|
315
|
+
* than the illustration it sits beside.
|
|
316
|
+
*/
|
|
317
|
+
export function assertWalkthroughCopy(steps: readonly Readonly<{ id: string; label: string; hint?: string }>[], label = "walkthrough"): void {
|
|
318
|
+
for (const step of steps) {
|
|
319
|
+
const words = step.label.trim().split(/\s+/u).filter(Boolean).length;
|
|
320
|
+
if (words === 0 || words > WALKTHROUGH_COPY_LIMITS.labelWords) fail(`${label} step "${step.id}" label has ${String(words)} words; use one to ${String(WALKTHROUGH_COPY_LIMITS.labelWords)}.`);
|
|
321
|
+
const hint = step.hint?.trim();
|
|
322
|
+
if (hint === undefined || hint === "") continue;
|
|
323
|
+
if (hint.length > WALKTHROUGH_COPY_LIMITS.hintCharacters) fail(`${label} step "${step.id}" hint is ${String(hint.length)} characters; keep it within ${String(WALKTHROUGH_COPY_LIMITS.hintCharacters)}.`);
|
|
324
|
+
if ((hint.match(/[.!?](\s|$)/gu) ?? []).length > 1) fail(`${label} step "${step.id}" hint has more than one sentence.`);
|
|
325
|
+
}
|
|
326
|
+
}
|