@hraness/design-kit 0.35.2 → 0.36.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 +57 -7
- package/dist/{chunk-vst6p4wd.js → chunk-sabcr66p.js} +68 -0
- package/dist/{chunk-fmeg5cxf.js → chunk-thv5t11w.js} +1 -1
- package/dist/{chunk-4tgt8egt.js → chunk-v8byd3hw.js} +106 -23
- package/dist/index.js +2 -2
- package/dist/mockups/client.js +249 -91
- package/dist/mockups/index.js +4 -0
- package/dist/provider-marks.js +1 -1
- package/dist/react/index.js +58 -3
- package/dist/react/server.js +2 -2
- package/dist/stylex-manifest.json +1 -1
- package/dist/stylex.css +30 -0
- package/package.json +1 -1
- package/portfolio-inventory.json +1 -1
- package/src/mockups/client.tsx +187 -63
- package/src/mockups/frames.tsx +4 -2
- package/src/mockups.css +58 -7
- package/src/product-marketing-foundation.css +12 -0
- package/src/product-marketing.css +59 -0
- package/src/provider-marks.generated.ts +20 -0
- package/src/provider-marks.ts +5 -0
- package/src/react/design-gallery.tsx +11 -0
- package/src/react/product-marketing.stylex.ts +50 -2
- package/src/react/product-marketing.tsx +38 -28
- package/vendor/provider-marks/BEEPER-NOTICE.md +16 -0
- package/vendor/provider-marks/SIMPLE-ICONS-LICENSE.md +30 -0
- package/vendor/provider-marks/UPSTREAM.md +24 -0
- package/vendor/provider-marks/beeper-glyph.svg +3 -0
- package/vendor/provider-marks/beeper.svg +16 -0
- package/vendor/provider-marks/lobehub/ollama.svg +1 -0
- package/vendor/provider-marks/lobehub/vercel.svg +1 -0
- package/vendor/provider-marks/simple-icons/imessage.svg +1 -0
- package/vendor/provider-marks/simple-icons/whatsapp.svg +1 -0
package/dist/stylex.css
CHANGED
|
@@ -109,6 +109,10 @@
|
|
|
109
109
|
padding: 0;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
.x1aetswf {
|
|
113
|
+
padding: 1.125rem;
|
|
114
|
+
}
|
|
115
|
+
|
|
112
116
|
.x194jd1b {
|
|
113
117
|
padding: 1.1rem 7rem 1rem 1rem;
|
|
114
118
|
}
|
|
@@ -1084,6 +1088,10 @@
|
|
|
1084
1088
|
accent-color: var(--primary, Highlight);
|
|
1085
1089
|
}
|
|
1086
1090
|
|
|
1091
|
+
.xc26acl {
|
|
1092
|
+
align-content: center;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1087
1095
|
.xcdzlcm {
|
|
1088
1096
|
align-content: space-between;
|
|
1089
1097
|
}
|
|
@@ -1675,6 +1683,10 @@
|
|
|
1675
1683
|
block-size: 1rem;
|
|
1676
1684
|
}
|
|
1677
1685
|
|
|
1686
|
+
.x1vlpzvb {
|
|
1687
|
+
block-size: 3.5rem;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1678
1690
|
.x18o2qet {
|
|
1679
1691
|
block-size: 4.5rem;
|
|
1680
1692
|
}
|
|
@@ -2523,6 +2535,10 @@
|
|
|
2523
2535
|
grid-template-columns: 2.25rem minmax(0, 1fr);
|
|
2524
2536
|
}
|
|
2525
2537
|
|
|
2538
|
+
.xrdwe0 {
|
|
2539
|
+
grid-template-columns: 3.5rem minmax(0, 1fr);
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2526
2542
|
.xx9t13c {
|
|
2527
2543
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
2528
2544
|
}
|
|
@@ -2631,6 +2647,10 @@
|
|
|
2631
2647
|
inline-size: 2rem;
|
|
2632
2648
|
}
|
|
2633
2649
|
|
|
2650
|
+
.x1pf0uk5 {
|
|
2651
|
+
inline-size: 3.5rem;
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2634
2654
|
.x6qm275 {
|
|
2635
2655
|
inline-size: 4.5rem;
|
|
2636
2656
|
}
|
|
@@ -2966,6 +2986,10 @@
|
|
|
2966
2986
|
max-block-size: min(38rem, 75dvh);
|
|
2967
2987
|
}
|
|
2968
2988
|
|
|
2989
|
+
.x1vj640n {
|
|
2990
|
+
max-block-size: none;
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2969
2993
|
.xgyk9h7 {
|
|
2970
2994
|
max-inline-size: 100%;
|
|
2971
2995
|
}
|
|
@@ -4101,6 +4125,12 @@
|
|
|
4101
4125
|
}
|
|
4102
4126
|
}
|
|
4103
4127
|
|
|
4128
|
+
@media (width >= 64rem) {
|
|
4129
|
+
.x13f99nf.x13f99nf {
|
|
4130
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, .8fr);
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
|
|
4104
4134
|
@media (width >= 50rem) {
|
|
4105
4135
|
.x2k1869.x2k1869 {
|
|
4106
4136
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hraness/design-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.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
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type ReactNode,
|
|
7
7
|
useId,
|
|
8
8
|
useLayoutEffect,
|
|
9
|
+
useMemo,
|
|
9
10
|
useRef,
|
|
10
11
|
useState,
|
|
11
12
|
} from "react";
|
|
@@ -145,8 +146,10 @@ export type ModeShowcaseProps<S extends string, M extends string, O extends stri
|
|
|
145
146
|
status?: (state: Readonly<{ surface: S; mode: M; option: O | undefined }>) => ReactNode;
|
|
146
147
|
/** Optional context that adds to the visual. Omit repeated descriptions or generic disclaimers. */
|
|
147
148
|
caption?: string;
|
|
148
|
-
/** Page-window
|
|
149
|
+
/** Page-window cap in natural mode; minimum complete stage height in fill mode. */
|
|
149
150
|
height?: number;
|
|
151
|
+
/** Reserve every authored choice at readable size, up to 128 combinations. */
|
|
152
|
+
fit?: "natural" | "fill";
|
|
150
153
|
/** Narrowest layout width before the stage scales down. */
|
|
151
154
|
minWidth?: number;
|
|
152
155
|
theme?: MockupTheme;
|
|
@@ -164,6 +167,7 @@ export type ModeShowcaseProps<S extends string, M extends string, O extends stri
|
|
|
164
167
|
export function ModeShowcase<S extends string, M extends string, O extends string = string>({
|
|
165
168
|
caption,
|
|
166
169
|
className,
|
|
170
|
+
fit = "natural",
|
|
167
171
|
height = 440,
|
|
168
172
|
initial,
|
|
169
173
|
label,
|
|
@@ -181,7 +185,9 @@ export function ModeShowcase<S extends string, M extends string, O extends strin
|
|
|
181
185
|
assertUniqueIds(modes, "ModeShowcase mode");
|
|
182
186
|
if (options !== undefined) assertUniqueIds(options, "ModeShowcase option");
|
|
183
187
|
const captionText = optionalText(caption, "ModeShowcase");
|
|
184
|
-
if (!(height > 0)) throw new RangeError("ModeShowcase height must be positive.");
|
|
188
|
+
if (!Number.isFinite(height) || !(height > 0)) throw new RangeError("ModeShowcase height must be finite and positive.");
|
|
189
|
+
if (fit !== "natural" && fit !== "fill") throw new RangeError("ModeShowcase fit must be natural or fill.");
|
|
190
|
+
if (fit === "fill" && surfaces.length * modes.length * (options?.length ?? 1) > 128) throw new RangeError("ModeShowcase fill supports up to 128 surface, mode, and option combinations.");
|
|
185
191
|
|
|
186
192
|
const id = useId();
|
|
187
193
|
const firstSurface = surfaces.find((surface) => surface.id === initial?.surface) ?? itemAt(surfaces, 0, "ModeShowcase surfaces");
|
|
@@ -199,6 +205,17 @@ export function ModeShowcase<S extends string, M extends string, O extends strin
|
|
|
199
205
|
const optionChoice = options?.find((entry) => entry.id === option);
|
|
200
206
|
const optionInactive = optionInactiveModes.includes(mode);
|
|
201
207
|
|
|
208
|
+
// Pure, nonanimated authored states reserve one maximum before any choice.
|
|
209
|
+
// They stay hidden and inert; only a detached measurement clone reveals them.
|
|
210
|
+
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 inert key={JSON.stringify([entry.id, choice.id, optionChoice.id])}>
|
|
212
|
+
<div className="hkm-fit"><div className="hkm-fit-inner">
|
|
213
|
+
{entry.render({ animated: false, mode: choice.id, option: optionChoice.id, previousMode: choice.id, theme })}
|
|
214
|
+
</div></div>
|
|
215
|
+
</div>
|
|
216
|
+
)))), [fit, modes, options, surfaces, theme]);
|
|
217
|
+
const stage = useFittedShowcaseStage(fit, measurements, JSON.stringify([mode, option]), height);
|
|
218
|
+
|
|
202
219
|
const chooseMode = (next: M) => {
|
|
203
220
|
if (next === mode) return;
|
|
204
221
|
setPreviousMode(mode);
|
|
@@ -229,73 +246,86 @@ export function ModeShowcase<S extends string, M extends string, O extends strin
|
|
|
229
246
|
return (
|
|
230
247
|
<figure
|
|
231
248
|
aria-label={label?.(surface) ?? `Illustration of ${surface.label.toLowerCase()}`}
|
|
232
|
-
className={joinMockupClasses("hkm-showcase", className)}
|
|
249
|
+
className={joinMockupClasses("hkm-showcase", "hkm-modes", className)}
|
|
250
|
+
data-hkm-fit={fit === "fill" ? fit : undefined}
|
|
233
251
|
data-hkm-theme={theme}
|
|
234
252
|
data-nosnippet=""
|
|
235
253
|
>
|
|
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
|
-
)}
|
|
254
|
+
{surfaces.length > 1 ? (
|
|
255
|
+
<div aria-label="Surface" className="hkm-tabs hkm-folder-tabs" role="tablist">
|
|
256
|
+
{surfaces.map((entry) => (
|
|
257
|
+
<button
|
|
258
|
+
aria-controls={`${id}-panel`}
|
|
259
|
+
aria-selected={entry.id === surface.id}
|
|
260
|
+
className="hkm-tab"
|
|
261
|
+
id={`${id}-tab-${entry.id}`}
|
|
262
|
+
key={entry.id}
|
|
263
|
+
onClick={() => setSurfaceId(entry.id)}
|
|
264
|
+
onKeyDown={onTabKey}
|
|
265
|
+
ref={(element) => {
|
|
266
|
+
if (element === null) tabs.current.delete(entry.id);
|
|
267
|
+
else tabs.current.set(entry.id, element);
|
|
268
|
+
}}
|
|
269
|
+
role="tab"
|
|
270
|
+
tabIndex={entry.id === surface.id ? 0 : -1}
|
|
271
|
+
type="button"
|
|
272
|
+
>
|
|
273
|
+
{entry.label}
|
|
274
|
+
</button>
|
|
275
|
+
))}
|
|
284
276
|
</div>
|
|
285
|
-
|
|
286
|
-
</div>
|
|
277
|
+
) : null}
|
|
287
278
|
<div
|
|
288
279
|
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}
|
|
280
|
+
className="hkm-showcase-stage hkm-mode-panel"
|
|
292
281
|
id={`${id}-panel`}
|
|
293
282
|
role={surfaces.length > 1 ? "tabpanel" : undefined}
|
|
294
283
|
style={{ "--hkm-page-height": `${height}px` } as CSSProperties}
|
|
295
284
|
>
|
|
296
|
-
<
|
|
297
|
-
|
|
298
|
-
|
|
285
|
+
<div className="hkm-showcase-controls">
|
|
286
|
+
<div className="hkm-showcase-settings">
|
|
287
|
+
<div className="hkm-showcase-row">
|
|
288
|
+
<span className="hkm-showcase-label" id={`${id}-mode`}>{modeLabel}</span>
|
|
289
|
+
<div aria-labelledby={`${id}-mode`} className="hkm-segmented" role="group">
|
|
290
|
+
{modes.map((entry) => (
|
|
291
|
+
<button aria-pressed={entry.id === mode} key={entry.id} onClick={() => chooseMode(entry.id)} type="button">
|
|
292
|
+
{entry.label}
|
|
293
|
+
</button>
|
|
294
|
+
))}
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
{options === undefined ? null : (
|
|
298
|
+
<div className="hkm-showcase-row">
|
|
299
|
+
<span className="hkm-showcase-label" id={`${id}-option`}>{optionLabel}</span>
|
|
300
|
+
<div aria-labelledby={`${id}-option`} className="hkm-segmented" role="group">
|
|
301
|
+
{options.map((entry) => (
|
|
302
|
+
<button aria-pressed={entry.id === option} disabled={optionInactive} key={entry.id} onClick={() => chooseOption(entry.id)} type="button">
|
|
303
|
+
{entry.label}
|
|
304
|
+
</button>
|
|
305
|
+
))}
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
</div>
|
|
310
|
+
{hint === "" ? null : <p className="hkm-showcase-hint">{hint}</p>}
|
|
311
|
+
</div>
|
|
312
|
+
<div className="hkm-mode-stage" ref={stage}>
|
|
313
|
+
{measurements}
|
|
314
|
+
{surfaces.map((entry) => (
|
|
315
|
+
<div
|
|
316
|
+
aria-hidden={entry.id !== surface.id}
|
|
317
|
+
className="hkm-mode-surface"
|
|
318
|
+
data-hkm-animated={entry.id === surface.id && animated ? "" : undefined}
|
|
319
|
+
data-hkm-from={entry.id === surface.id && animated ? previousMode : undefined}
|
|
320
|
+
inert={entry.id !== surface.id}
|
|
321
|
+
key={entry.id}
|
|
322
|
+
>
|
|
323
|
+
<FitToWidth minWidth={fit === "fill" ? 1 : minWidth}>
|
|
324
|
+
{entry.render({ animated: entry.id === surface.id && animated, mode, option, previousMode, theme })}
|
|
325
|
+
</FitToWidth>
|
|
326
|
+
</div>
|
|
327
|
+
))}
|
|
328
|
+
</div>
|
|
299
329
|
</div>
|
|
300
330
|
{!hasStatus && captionText === undefined ? null : (
|
|
301
331
|
<figcaption className="hkm-showcase-caption">
|
|
@@ -320,6 +350,93 @@ export type ThroughStep = Readonly<{
|
|
|
320
350
|
render: (state: Readonly<{ animated: boolean; theme: MockupTheme | undefined }>) => ReactNode;
|
|
321
351
|
}>;
|
|
322
352
|
|
|
353
|
+
/** Measure complete mounted slides at a readable baseline, separately from fitted paint. */
|
|
354
|
+
function fitShowcaseStage(stage: HTMLDivElement, minimumHeight: number): void {
|
|
355
|
+
const owner = stage.parentElement;
|
|
356
|
+
if (owner === null || stage.clientWidth <= 0) return;
|
|
357
|
+
const probe = stage.cloneNode(true) as HTMLDivElement;
|
|
358
|
+
probe.setAttribute("aria-hidden", "true");
|
|
359
|
+
probe.setAttribute("inert", "");
|
|
360
|
+
probe.setAttribute("data-hkm-measuring", "");
|
|
361
|
+
probe.style.cssText = `position:absolute;visibility:hidden;pointer-events:none;inset:0 auto auto 0;inline-size:${stage.getBoundingClientRect().width}px;block-size:auto;`;
|
|
362
|
+
for (const sentinel of probe.querySelectorAll("[data-hkm-font-sentinel]")) sentinel.remove();
|
|
363
|
+
for (const node of probe.querySelectorAll("[id]")) node.removeAttribute("id");
|
|
364
|
+
for (const node of probe.querySelectorAll("[data-hkm-animated]")) node.removeAttribute("data-hkm-animated");
|
|
365
|
+
for (const body of probe.querySelectorAll<HTMLElement>('[data-hkm-density="presentation"]')) body.style.removeProperty("--hkm-terminal-presentation-size");
|
|
366
|
+
for (const fixture of probe.querySelectorAll<HTMLElement>("[data-hkm-measurement]")) fixture.hidden = false;
|
|
367
|
+
owner.append(probe);
|
|
368
|
+
try {
|
|
369
|
+
const naturalHeight = Math.max(minimumHeight, Math.ceil(probe.getBoundingClientRect().height));
|
|
370
|
+
// The tallest complete slide establishes the floor. Fitted type never feeds
|
|
371
|
+
// back into it, and changing the active tab never requests a measurement.
|
|
372
|
+
stage.style.setProperty("--hkm-showcase-fill-height", `${naturalHeight}px`);
|
|
373
|
+
for (const fixture of probe.querySelectorAll("[data-hkm-measurement]")) fixture.remove();
|
|
374
|
+
probe.style.blockSize = `${naturalHeight}px`;
|
|
375
|
+
probe.removeAttribute("data-hkm-measuring");
|
|
376
|
+
const bodies = [...stage.querySelectorAll<HTMLElement>('[data-hkm-density="presentation"]')].filter((body) => body.closest("[data-hkm-measurement]") === null);
|
|
377
|
+
const copies = [...probe.querySelectorAll<HTMLElement>('[data-hkm-density="presentation"]')];
|
|
378
|
+
for (const [index, copy] of copies.entries()) {
|
|
379
|
+
const body = bodies[index];
|
|
380
|
+
if (body === undefined) continue;
|
|
381
|
+
const lines = [...copy.querySelectorAll<HTMLElement>(".hkm-terminal-line")];
|
|
382
|
+
const wrappedRows = (line: HTMLElement) => Math.ceil((line.getBoundingClientRect().height - 0.5) / Number.parseFloat(getComputedStyle(line).lineHeight));
|
|
383
|
+
const baselineRows = lines.map(wrappedRows);
|
|
384
|
+
let low = 1;
|
|
385
|
+
let high = 4;
|
|
386
|
+
for (let attempt = 0; attempt < 9; attempt += 1) {
|
|
387
|
+
const candidate = (low + high) / 2;
|
|
388
|
+
copy.style.setProperty("--hkm-terminal-presentation-size", `${candidate}rem`);
|
|
389
|
+
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;
|
|
390
|
+
else high = candidate;
|
|
391
|
+
}
|
|
392
|
+
body.style.setProperty("--hkm-terminal-presentation-size", `${Math.floor(low * 1000) / 1000}rem`);
|
|
393
|
+
}
|
|
394
|
+
stage.setAttribute("data-hkm-fitted", "");
|
|
395
|
+
} finally {
|
|
396
|
+
probe.remove();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Share one measurement lifecycle across surface and walkthrough selectors. */
|
|
401
|
+
function useFittedShowcaseStage(fit: "natural" | "fill", source: unknown, variation = "", minimumHeight = 0) {
|
|
402
|
+
const stage = useRef<HTMLDivElement>(null);
|
|
403
|
+
useIsomorphicLayoutEffect(() => {
|
|
404
|
+
const node = stage.current;
|
|
405
|
+
if (fit !== "fill" || node === null || typeof ResizeObserver === "undefined") return undefined;
|
|
406
|
+
// A rem-sized box changes even when both the document and stage are fixed.
|
|
407
|
+
const fontSentinel = document.createElement("span");
|
|
408
|
+
fontSentinel.setAttribute("data-hkm-font-sentinel", "");
|
|
409
|
+
fontSentinel.setAttribute("aria-hidden", "true");
|
|
410
|
+
fontSentinel.inert = true;
|
|
411
|
+
fontSentinel.style.cssText = "position:absolute;inline-size:1rem;block-size:1rem;visibility:hidden;pointer-events:none;inset:0 auto auto 0;";
|
|
412
|
+
node.append(fontSentinel);
|
|
413
|
+
let previous = "";
|
|
414
|
+
let disposed = false;
|
|
415
|
+
const measure = () => {
|
|
416
|
+
if (disposed) return;
|
|
417
|
+
const key = `${node.clientWidth}/${getComputedStyle(document.documentElement).fontSize}`;
|
|
418
|
+
if (key === previous) return;
|
|
419
|
+
previous = key;
|
|
420
|
+
fitShowcaseStage(node, minimumHeight);
|
|
421
|
+
};
|
|
422
|
+
measure();
|
|
423
|
+
const observer = new ResizeObserver(measure);
|
|
424
|
+
observer.observe(node);
|
|
425
|
+
observer.observe(document.documentElement);
|
|
426
|
+
observer.observe(fontSentinel);
|
|
427
|
+
void document.fonts.ready.then(() => { previous = ""; measure(); });
|
|
428
|
+
return () => {
|
|
429
|
+
disposed = true;
|
|
430
|
+
observer.disconnect();
|
|
431
|
+
fontSentinel.remove();
|
|
432
|
+
node.style.removeProperty("--hkm-showcase-fill-height");
|
|
433
|
+
node.removeAttribute("data-hkm-fitted");
|
|
434
|
+
for (const body of node.querySelectorAll<HTMLElement>('[data-hkm-density="presentation"]')) body.style.removeProperty("--hkm-terminal-presentation-size");
|
|
435
|
+
};
|
|
436
|
+
}, [fit, source, variation, minimumHeight]);
|
|
437
|
+
return stage;
|
|
438
|
+
}
|
|
439
|
+
|
|
323
440
|
/**
|
|
324
441
|
* Folder tabs and chevron controls walk a flow. All render functions stay mounted
|
|
325
442
|
* to reserve the tallest panel; inactive panels are inert and visually hidden.
|
|
@@ -329,12 +446,15 @@ export function StepThrough({
|
|
|
329
446
|
caption,
|
|
330
447
|
className,
|
|
331
448
|
initial,
|
|
449
|
+
fit = "natural",
|
|
332
450
|
label = "Steps",
|
|
333
451
|
minWidth = 400,
|
|
334
452
|
steps,
|
|
335
453
|
theme,
|
|
336
454
|
}: Readonly<{
|
|
337
455
|
steps: readonly ThroughStep[];
|
|
456
|
+
/** Fill the tallest natural slide and fit presentation terminals without scaling. */
|
|
457
|
+
fit?: "natural" | "fill";
|
|
338
458
|
caption?: string;
|
|
339
459
|
initial?: string;
|
|
340
460
|
label?: string;
|
|
@@ -343,6 +463,8 @@ export function StepThrough({
|
|
|
343
463
|
className?: string;
|
|
344
464
|
}>) {
|
|
345
465
|
assertUniqueIds(steps, "StepThrough step");
|
|
466
|
+
if (fit !== "natural" && fit !== "fill") throw new RangeError("StepThrough fit must be natural or fill.");
|
|
467
|
+
const stage = useFittedShowcaseStage(fit, steps);
|
|
346
468
|
const captionText = optionalText(caption, "StepThrough");
|
|
347
469
|
const id = useId();
|
|
348
470
|
const [index, setIndex] = useState(() => Math.max(0, steps.findIndex((step) => step.id === initial)));
|
|
@@ -365,9 +487,9 @@ export function StepThrough({
|
|
|
365
487
|
};
|
|
366
488
|
|
|
367
489
|
return (
|
|
368
|
-
<figure aria-label={`Illustration: ${label.toLowerCase()}`} className={joinMockupClasses("hkm-showcase", "hkm-steps", className)} data-hkm-theme={theme} data-nosnippet="">
|
|
490
|
+
<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="">
|
|
369
491
|
<div className="hkm-showcase-controls">
|
|
370
|
-
<div aria-label={label} className="hkm-tabs hkm-step-tabs" role="tablist">
|
|
492
|
+
<div aria-label={label} className="hkm-tabs hkm-folder-tabs hkm-step-tabs" role="tablist">
|
|
371
493
|
{steps.map((entry, position) => (
|
|
372
494
|
<button
|
|
373
495
|
aria-controls={`${id}-panel-${entry.id}`}
|
|
@@ -392,7 +514,7 @@ export function StepThrough({
|
|
|
392
514
|
))}
|
|
393
515
|
</div>
|
|
394
516
|
</div>
|
|
395
|
-
<div className="hkm-showcase-stage hkm-step-stage">
|
|
517
|
+
<div className="hkm-showcase-stage hkm-step-stage" ref={stage}>
|
|
396
518
|
{steps.map((entry, position) => (
|
|
397
519
|
<div
|
|
398
520
|
aria-hidden={position !== current}
|
|
@@ -404,7 +526,9 @@ export function StepThrough({
|
|
|
404
526
|
key={entry.id}
|
|
405
527
|
role="tabpanel"
|
|
406
528
|
>
|
|
407
|
-
|
|
529
|
+
{fit === "fill"
|
|
530
|
+
? <div className="hkm-fit"><div className="hkm-fit-inner">{entry.render({ animated: position === current && animated, theme })}</div></div>
|
|
531
|
+
: <FitToWidth minWidth={minWidth}>{entry.render({ animated: position === current && animated, theme })}</FitToWidth>}
|
|
408
532
|
</div>
|
|
409
533
|
))}
|
|
410
534
|
</div>
|
package/src/mockups/frames.tsx
CHANGED
|
@@ -106,13 +106,15 @@ const TERMINAL_KINDS = new Set(["input", "output", "comment"]);
|
|
|
106
106
|
* Commands and output are sample text; keep them to commands the product ships.
|
|
107
107
|
*/
|
|
108
108
|
export function TerminalFrame({
|
|
109
|
+
density = "standard",
|
|
109
110
|
fade,
|
|
110
111
|
height,
|
|
111
112
|
lines,
|
|
112
113
|
prompt = "$",
|
|
113
114
|
title = "Terminal",
|
|
114
115
|
...root
|
|
115
|
-
}: MockupRootProps & Readonly<{ title?: string; prompt?: string; lines: readonly TerminalLine[]; height?: number; fade?: boolean }>) {
|
|
116
|
+
}: MockupRootProps & Readonly<{ title?: string; prompt?: string; lines: readonly TerminalLine[]; height?: number; fade?: boolean; density?: "standard" | "presentation" }>) {
|
|
117
|
+
if (density !== "standard" && density !== "presentation") throw new RangeError("Terminal density must be standard or presentation.");
|
|
116
118
|
for (const line of lines) {
|
|
117
119
|
if (!TERMINAL_KINDS.has(line.kind)) throw new TypeError(`Unknown terminal line kind ${JSON.stringify(line.kind)}.`);
|
|
118
120
|
}
|
|
@@ -125,7 +127,7 @@ export function TerminalFrame({
|
|
|
125
127
|
<span className="hkm-title">{title}</span>
|
|
126
128
|
<span />
|
|
127
129
|
</div>
|
|
128
|
-
<div className="hkm-page hkm-terminal-body" data-hkm-fade={fades ? "" : undefined} style={pageStyle(height)}>
|
|
130
|
+
<div className="hkm-page hkm-terminal-body" data-hkm-density={density === "presentation" ? density : undefined} data-hkm-fade={fades ? "" : undefined} style={pageStyle(height)}>
|
|
129
131
|
<div className="hkm-terminal-lines">
|
|
130
132
|
{lines.map((line, index) => (
|
|
131
133
|
<div className="hkm-terminal-line" data-hkm-beat={line.beat} data-hkm-line={line.kind} data-hkm-tone={line.tone} key={index}>
|
package/src/mockups.css
CHANGED
|
@@ -333,6 +333,15 @@
|
|
|
333
333
|
font-variant-ligatures: none;
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
+
.hkm-root .hkm-terminal-body[data-hkm-density="presentation"] {
|
|
337
|
+
font-size: var(--hkm-terminal-presentation-size, 1rem);
|
|
338
|
+
line-height: 1.5;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.hkm-root .hkm-terminal-body[data-hkm-density="presentation"] .hkm-terminal-line {
|
|
342
|
+
min-block-size: 1.5em;
|
|
343
|
+
}
|
|
344
|
+
|
|
336
345
|
.hkm-root .hkm-terminal-lines {
|
|
337
346
|
display: grid;
|
|
338
347
|
gap: 1px;
|
|
@@ -2503,24 +2512,66 @@
|
|
|
2503
2512
|
}
|
|
2504
2513
|
}
|
|
2505
2514
|
|
|
2506
|
-
/* Folder tabs share an edge with
|
|
2507
|
-
.hkm-showcase.hkm-steps { gap: 0; }
|
|
2515
|
+
/* Folder tabs share an edge with each showcase panel. */
|
|
2516
|
+
.hkm-showcase:is(.hkm-steps, .hkm-modes) { gap: 0; }
|
|
2508
2517
|
.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-
|
|
2518
|
+
.hkm-showcase .hkm-folder-tabs { gap: 6px; margin: 0; padding: 0; border: 0; position: relative; z-index: 1; }
|
|
2519
|
+
.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); }
|
|
2520
|
+
.hkm-showcase .hkm-folder-tabs .hkm-tab[aria-selected="true"] { background: var(--hkm-sc-bg); color: var(--hkm-sc-fg); }
|
|
2521
|
+
.hkm-showcase .hkm-folder-tabs .hkm-tab:hover { color: var(--hkm-sc-fg); }
|
|
2513
2522
|
.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; }
|
|
2523
|
+
.hkm-showcase.hkm-modes .hkm-mode-panel { border: 1px solid var(--hkm-sc-line); border-radius: 12px; background: var(--hkm-sc-bg); overflow: clip; }
|
|
2524
|
+
.hkm-showcase.hkm-modes .hkm-folder-tabs + .hkm-mode-panel { border-start-start-radius: 0; margin-block-start: -1px; }
|
|
2525
|
+
.hkm-showcase.hkm-modes .hkm-showcase-controls { border: 0; border-block-end: 1px solid var(--hkm-sc-line); border-radius: 0; }
|
|
2526
|
+
.hkm-mode-stage { display: grid; min-inline-size: 0; }
|
|
2527
|
+
.hkm-mode-surface { grid-area: 1 / 1; min-inline-size: 0; align-self: start; }
|
|
2528
|
+
.hkm-mode-surface[aria-hidden="true"] { visibility: hidden; pointer-events: none; }
|
|
2529
|
+
.hkm-showcase.hkm-modes .hkm-mode-surface > .hkm-fit > .hkm-fit-inner > .hkm-root,
|
|
2530
|
+
.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
2531
|
.hkm-step-panel { grid-area: 1 / 1; min-inline-size: 0; align-self: start; }
|
|
2515
2532
|
.hkm-step-panel[aria-hidden="true"] { visibility: hidden; pointer-events: none; }
|
|
2516
2533
|
.hkm-showcase.hkm-steps .hkm-step-panel .hkm-root { border-start-start-radius: 0; border-start-end-radius: 0; box-shadow: none; }
|
|
2534
|
+
/* Fill uses the tallest complete slide as its natural height. It does not
|
|
2535
|
+
* scale text or ask fitted font sizes to determine the stage height. */
|
|
2536
|
+
.hkm-showcase[data-hkm-fit="fill"] [data-hkm-measuring] .hkm-page { max-block-size: none; }
|
|
2537
|
+
|
|
2538
|
+
.hkm-showcase[data-hkm-fit="fill"] :is(.hkm-step-stage, .hkm-mode-stage):not([data-hkm-measuring]) {
|
|
2539
|
+
block-size: var(--hkm-showcase-fill-height, auto);
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
.hkm-showcase[data-hkm-fit="fill"] :is(.hkm-step-stage, .hkm-mode-stage):not([data-hkm-measuring]) :is(.hkm-step-panel, .hkm-mode-surface) {
|
|
2543
|
+
align-self: stretch;
|
|
2544
|
+
min-block-size: 0;
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
.hkm-showcase[data-hkm-fit="fill"] :is(.hkm-step-stage, .hkm-mode-stage):not([data-hkm-measuring]) :is(.hkm-fit, .hkm-fit-inner, .hkm-root, .hkm-root > .hkm-window) {
|
|
2548
|
+
block-size: 100%;
|
|
2549
|
+
min-block-size: 0;
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
.hkm-showcase[data-hkm-fit="fill"] :is(.hkm-step-stage, .hkm-mode-stage):not([data-hkm-measuring]) .hkm-window:has(> .hkm-page) {
|
|
2553
|
+
display: flex;
|
|
2554
|
+
flex-direction: column;
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
.hkm-showcase[data-hkm-fit="fill"] :is(.hkm-step-stage, .hkm-mode-stage):not([data-hkm-measuring]) .hkm-window > .hkm-page {
|
|
2558
|
+
flex: 1 1 auto;
|
|
2559
|
+
block-size: auto;
|
|
2560
|
+
min-block-size: 0;
|
|
2561
|
+
max-block-size: none;
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
.hkm-showcase[data-hkm-fit="fill"] :is(.hkm-step-stage, .hkm-mode-stage):not([data-hkm-measuring]) :is(.hkm-title-bar, .hkm-browser-bar) {
|
|
2565
|
+
flex: none;
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2517
2568
|
.hkm-showcase.hkm-steps .hkm-step-nav { justify-content: space-between; padding-block-start: 18px; gap: 16px; }
|
|
2518
2569
|
.hkm-showcase.hkm-steps .hkm-step-button { display: inline-grid; place-items: center; min-inline-size: 52px; min-block-size: 52px; padding: 10px; border: 0; border-radius: 12px; background: transparent; color: var(--hkm-sc-fg); }
|
|
2519
2570
|
.hkm-showcase.hkm-steps .hkm-step-button[data-hkm-primary] { background: var(--hkm-sc-fg); color: var(--hkm-sc-bg); }
|
|
2520
2571
|
.hkm-showcase.hkm-steps .hkm-step-button:disabled { color: var(--hkm-sc-muted); background: transparent; }
|
|
2521
2572
|
.hkm-showcase.hkm-steps .hkm-step-button[data-hkm-primary]:disabled { background: var(--hkm-sc-field); }
|
|
2522
2573
|
.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; }
|
|
2574
|
+
.hkm-showcase:is(.hkm-steps, .hkm-modes) .hkm-showcase-caption { margin-block-start: 1rem; }
|
|
2524
2575
|
|
|
2525
2576
|
/* ---------------------------------------------------------------- */
|
|
2526
2577
|
/* Motion */
|
|
@@ -415,6 +415,13 @@ html:has(:is(.hraness-marketing-header, .hraness-marketing-header-surface):not([
|
|
|
415
415
|
z-index: 30;
|
|
416
416
|
}
|
|
417
417
|
|
|
418
|
+
/* Intrinsic media sizing is shared with raw HTML card icons. */
|
|
419
|
+
.hraness-marketing-card__icon > :is(img, svg) {
|
|
420
|
+
inline-size: 100%;
|
|
421
|
+
block-size: 100%;
|
|
422
|
+
object-fit: contain;
|
|
423
|
+
}
|
|
424
|
+
|
|
418
425
|
.hraness-marketing-card-row,
|
|
419
426
|
.hraness-marketing-interface-grid,
|
|
420
427
|
.hraness-marketing-trust-grid,
|
|
@@ -439,6 +446,11 @@ html:has(:is(.hraness-marketing-header, .hraness-marketing-header-surface):not([
|
|
|
439
446
|
display: none;
|
|
440
447
|
}
|
|
441
448
|
|
|
449
|
+
/* Supplemental proof belongs below both columns of a split hero. */
|
|
450
|
+
.hraness-marketing-hero[data-layout="split"] > :where(.hraness-marketing-proof, .hraness-marketing-facts) {
|
|
451
|
+
grid-column: 1 / -1;
|
|
452
|
+
}
|
|
453
|
+
|
|
442
454
|
/* Keep all meaningful hero content above the inert decorative backdrop. */
|
|
443
455
|
.hraness-marketing-hero > :where(.hraness-marketing-hero__copy, .hraness-marketing-hero__frame, .hraness-marketing-proof, .hraness-marketing-facts) {
|
|
444
456
|
position: relative;
|
|
@@ -707,6 +707,17 @@ html:has(:is(.hraness-marketing-header, .hraness-marketing-header-surface):not([
|
|
|
707
707
|
padding-block: var(--hraness-marketing-hero-space, clamp(3.5rem, 9vw, 7rem) clamp(2.5rem, 6vw, 4.5rem));
|
|
708
708
|
}
|
|
709
709
|
|
|
710
|
+
.hraness-marketing-hero[data-layout="split"] {
|
|
711
|
+
grid-template-columns: minmax(0, 1fr);
|
|
712
|
+
align-items: center;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
@media (min-width: 64rem) {
|
|
716
|
+
.hraness-marketing-hero[data-layout="split"] {
|
|
717
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 0.8fr);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
710
721
|
/* Hero backdrops are retired. Hand-authored markup that still ships the old
|
|
711
722
|
* backdrop hooks paints nothing and takes no space. */
|
|
712
723
|
.hraness-marketing-hero-backdrop {
|
|
@@ -1698,6 +1709,49 @@ html:has(:is(.hraness-marketing-header, .hraness-marketing-header-surface):not([
|
|
|
1698
1709
|
box-shadow: var(--hraness-marketing-surface-shadow);
|
|
1699
1710
|
}
|
|
1700
1711
|
|
|
1712
|
+
.hraness-marketing-card[data-layout="icon"] {
|
|
1713
|
+
display: grid;
|
|
1714
|
+
grid-template-columns: 3.5rem minmax(0, 1fr);
|
|
1715
|
+
align-items: center;
|
|
1716
|
+
gap: 1rem;
|
|
1717
|
+
padding: 1.125rem;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
.hraness-marketing-card[data-layout="icon"]:focus-visible {
|
|
1721
|
+
outline: 2px solid var(--hraness-marketing-ink);
|
|
1722
|
+
outline-offset: 3px;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
.hraness-marketing-card__icon {
|
|
1726
|
+
display: flex;
|
|
1727
|
+
align-items: center;
|
|
1728
|
+
justify-content: center;
|
|
1729
|
+
inline-size: 3.5rem;
|
|
1730
|
+
block-size: 3.5rem;
|
|
1731
|
+
min-inline-size: 0;
|
|
1732
|
+
pointer-events: none;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
.hraness-marketing-card__icon > :is(img, svg) {
|
|
1736
|
+
inline-size: 100%;
|
|
1737
|
+
block-size: 100%;
|
|
1738
|
+
object-fit: contain;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
.hraness-marketing-card__copy {
|
|
1742
|
+
display: grid;
|
|
1743
|
+
align-content: center;
|
|
1744
|
+
min-inline-size: 0;
|
|
1745
|
+
gap: 0.35rem;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
.hraness-marketing-card[data-layout="icon"] .hraness-marketing-card__meta {
|
|
1749
|
+
overflow: visible;
|
|
1750
|
+
min-block-size: 0;
|
|
1751
|
+
max-block-size: none;
|
|
1752
|
+
overflow-wrap: anywhere;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1701
1755
|
.hraness-marketing-card__art {
|
|
1702
1756
|
display: flex;
|
|
1703
1757
|
flex: 0 0 auto;
|
|
@@ -2266,6 +2320,11 @@ html:has(:is(.hraness-marketing-header, .hraness-marketing-header-surface):not([
|
|
|
2266
2320
|
}
|
|
2267
2321
|
}
|
|
2268
2322
|
|
|
2323
|
+
/* Supplemental proof belongs below both columns of a split hero. */
|
|
2324
|
+
.hraness-marketing-hero[data-layout="split"] > :where(.hraness-marketing-proof, .hraness-marketing-facts) {
|
|
2325
|
+
grid-column: 1 / -1;
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2269
2328
|
/* Keep all meaningful hero content above the inert decorative backdrop. */
|
|
2270
2329
|
.hraness-marketing-hero > :where(.hraness-marketing-hero__copy, .hraness-marketing-hero__frame, .hraness-marketing-proof, .hraness-marketing-facts) {
|
|
2271
2330
|
position: relative;
|