@hraness/design-kit 0.31.1
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 +208 -0
- package/HERO_FIELDS.md +55 -0
- package/ICONS.md +56 -0
- package/LANTERN_MATERIAL.md +60 -0
- package/LICENSE +21 -0
- package/MARKETING_COPY.md +49 -0
- package/MARKETING_PRESET.md +108 -0
- package/PALETTES.md +90 -0
- package/PAPER_THEME.md +128 -0
- package/README.md +745 -0
- package/STATUS_PAGES.md +67 -0
- package/STYLE.md +245 -0
- package/WRITING.md +72 -0
- package/dist/browser/index.js +2345 -0
- package/dist/chunk-44djxk16.js +6872 -0
- package/dist/chunk-5gtx3pza.js +9 -0
- package/dist/chunk-77391vmq.js +454 -0
- package/dist/chunk-7b1586eb.js +1209 -0
- package/dist/chunk-8834fh4n.js +6 -0
- package/dist/chunk-9t8xyqte.js +870 -0
- package/dist/chunk-cejpzyfh.js +446 -0
- package/dist/chunk-eh71jz57.js +429 -0
- package/dist/chunk-evb02bc1.js +516 -0
- package/dist/chunk-h4k7yv6x.js +368 -0
- package/dist/chunk-he8eznb1.js +760 -0
- package/dist/chunk-mr1vdcjq.js +404 -0
- package/dist/chunk-wzvdn8ey.js +116 -0
- package/dist/fonts/nebula-sans/social-fonts.generated.js +53 -0
- package/dist/icons.js +48 -0
- package/dist/index.js +237 -0
- package/dist/launch.js +41 -0
- package/dist/mockups/client.js +422 -0
- package/dist/mockups/index.js +1850 -0
- package/dist/portfolio.js +1549 -0
- package/dist/provider-marks.js +19 -0
- package/dist/react/charts.js +11 -0
- package/dist/react/hero-backdrop.js +8 -0
- package/dist/react/index.js +7252 -0
- package/dist/react/platform-install.js +10 -0
- package/dist/react/server.js +137 -0
- package/dist/stylex-manifest.json +1 -0
- package/dist/stylex.css +5392 -0
- package/dist/syntax-highlighting.js +15 -0
- package/dist/testing.js +250 -0
- package/package.json +377 -0
- package/portfolio-inventory.json +31 -0
- package/scripts/check-lantern-material-snapshot.d.mts +26 -0
- package/scripts/check-lantern-material-snapshot.mjs +126 -0
- package/scripts/check-marketing-snapshot.d.mts +12 -0
- package/scripts/check-marketing-snapshot.mjs +58 -0
- package/scripts/lantern-material-snapshot.ts +101 -0
- package/scripts/marketing-textures.ts +48 -0
- package/scripts/paper-theme-snapshot.ts +97 -0
- package/scripts/product-marketing-snapshot.ts +149 -0
- package/src/appearance-menu.css +266 -0
- package/src/appearance.ts +32 -0
- package/src/article-html.ts +269 -0
- package/src/article.ts +686 -0
- package/src/browser/appearance-menu.ts +604 -0
- package/src/browser/artifact-share.ts +164 -0
- package/src/browser/design-palette.ts +197 -0
- package/src/browser/foil.ts +124 -0
- package/src/browser/hero-light.ts +11 -0
- package/src/browser/index.ts +29 -0
- package/src/browser/status-field.ts +432 -0
- package/src/browser/status-page.ts +100 -0
- package/src/browser/sticky-offset.ts +82 -0
- package/src/browser/theme-color-sync.ts +213 -0
- package/src/charts.css +7 -0
- package/src/compiler-components.css +34 -0
- package/src/compiler-foundation.css +18 -0
- package/src/compiler-palettes.css +7 -0
- package/src/compiler-tokens.css +342 -0
- package/src/components.css +3 -0
- package/src/design-gallery.css +431 -0
- package/src/effects.css +14 -0
- package/src/fonts/geist-mono/GeistMono[wght].woff2 +0 -0
- package/src/fonts/geist-mono/OFL.txt +93 -0
- package/src/fonts/geist-mono/PROVENANCE.md +12 -0
- package/src/fonts/instrument-serif/OFL.txt +93 -0
- package/src/fonts/instrument-serif/UPSTREAM.md +14 -0
- package/src/fonts/instrument-serif/instrument-serif-latin-400.woff2 +0 -0
- package/src/fonts/nebula-sans/LICENSE.txt +96 -0
- package/src/fonts/nebula-sans/NebulaSans-Black.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-BlackItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Bold.otf +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Bold.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-BoldItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Book.otf +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Book.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-BookItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Light.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-LightItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Medium.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-MediumItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Semibold.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-SemiboldItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/PROVENANCE.md +14 -0
- package/src/fonts/nebula-sans/social-fonts.generated.ts +3873 -0
- package/src/fonts.css +106 -0
- package/src/icons/act60/act60.svg +5 -0
- package/src/icons/act60/application.svg +9 -0
- package/src/icons/act60/chapter-compare.svg +3 -0
- package/src/icons/act60/decree-residence.svg +6 -0
- package/src/icons/act60/estimate.svg +7 -0
- package/src/icons/act60/export-services.svg +4 -0
- package/src/icons/act60/investor.svg +13 -0
- package/src/icons/act60/presence.svg +7 -0
- package/src/icons/aicharts/aicharts.svg +6 -0
- package/src/icons/aicharts/cost-compare.svg +4 -0
- package/src/icons/aicharts/task-audio.svg +7 -0
- package/src/icons/aicharts/task-coding.svg +6 -0
- package/src/icons/aicharts/task-images.svg +3 -0
- package/src/icons/aicharts/task-reasoning.svg +9 -0
- package/src/icons/aicharts/task-research.svg +5 -0
- package/src/icons/aicharts/task-video.svg +4 -0
- package/src/icons/manifest.json +1371 -0
- package/src/icons/platonik/conversation.svg +3 -0
- package/src/icons/platonik/creature.svg +7 -0
- package/src/icons/platonik/economy.svg +4 -0
- package/src/icons/platonik/keep-light.svg +6 -0
- package/src/icons/platonik/platonik.svg +7 -0
- package/src/icons/platonik/spark-route.svg +15 -0
- package/src/icons/receipts/act60-retry.receipt.json +888 -0
- package/src/icons/receipts/act60.receipt.json +1366 -0
- package/src/icons/receipts/aicharts.receipt.json +1546 -0
- package/src/icons/receipts/platonik.receipt.json +1011 -0
- package/src/icons/receipts/roughday.receipt.json +770 -0
- package/src/icons/receipts/shared.receipt.json +1296 -0
- package/src/icons/receipts/slopcamera.receipt.json +1495 -0
- package/src/icons/receipts/soundfish-retry.receipt.json +460 -0
- package/src/icons/receipts/soundfish.receipt.json +1709 -0
- package/src/icons/receipts/sponge.receipt.json +1743 -0
- package/src/icons/receipts/stripe-history-retry.receipt.json +145 -0
- package/src/icons/receipts/stripe-history.receipt.json +1202 -0
- package/src/icons/receipts/wordcell.receipt.json +1924 -0
- package/src/icons/roughday/finance.svg +7 -0
- package/src/icons/roughday/roughday.svg +6 -0
- package/src/icons/roughday/technology.svg +4 -0
- package/src/icons/roughday/world.svg +4 -0
- package/src/icons/sets/act60.json +63 -0
- package/src/icons/sets/aicharts.json +63 -0
- package/src/icons/sets/platonik.json +39 -0
- package/src/icons/sets/roughday.json +25 -0
- package/src/icons/sets/shared.json +12 -0
- package/src/icons/sets/slopcamera.json +47 -0
- package/src/icons/sets/soundfish.json +43 -0
- package/src/icons/sets/sponge.json +51 -0
- package/src/icons/sets/stripe-history.json +39 -0
- package/src/icons/sets/wordcell.json +51 -0
- package/src/icons/shared/agent-skill.svg +4 -0
- package/src/icons/shared/cli.svg +5 -0
- package/src/icons/shared/privacy.svg +3 -0
- package/src/icons/shared/research.svg +7 -0
- package/src/icons/shared/sdk.svg +3 -0
- package/src/icons/slopcamera/diagram.svg +3 -0
- package/src/icons/slopcamera/direct-scene.svg +5 -0
- package/src/icons/slopcamera/edit-video.svg +17 -0
- package/src/icons/slopcamera/mcp.svg +3 -0
- package/src/icons/slopcamera/native-world.svg +3 -0
- package/src/icons/slopcamera/slopcamera.svg +11 -0
- package/src/icons/soundfish/agent-ui.svg +7 -0
- package/src/icons/soundfish/collaborate.svg +5 -0
- package/src/icons/soundfish/compose.svg +3 -0
- package/src/icons/soundfish/hear.svg +6 -0
- package/src/icons/soundfish/human-ui.svg +6 -0
- package/src/icons/soundfish/share.svg +3 -0
- package/src/icons/soundfish/soundfish.svg +6 -0
- package/src/icons/sponge/api.svg +3 -0
- package/src/icons/sponge/documents.svg +3 -0
- package/src/icons/sponge/library.svg +3 -0
- package/src/icons/sponge/one-question.svg +5 -0
- package/src/icons/sponge/people-direct.svg +5 -0
- package/src/icons/sponge/read.svg +3 -0
- package/src/icons/sponge/save.svg +4 -0
- package/src/icons/sponge/sponge.svg +7 -0
- package/src/icons/stripe-history/company-history.svg +3 -0
- package/src/icons/stripe-history/evidence.svg +4 -0
- package/src/icons/stripe-history/independence.svg +7 -0
- package/src/icons/stripe-history/publications.svg +11 -0
- package/src/icons/stripe-history/sources.svg +6 -0
- package/src/icons/stripe-history/stripe-history.svg +3 -0
- package/src/icons/wordcell/backlinks.svg +6 -0
- package/src/icons/wordcell/capture.svg +6 -0
- package/src/icons/wordcell/git-provenance.svg +4 -0
- package/src/icons/wordcell/kb.svg +4 -0
- package/src/icons/wordcell/markdown.svg +4 -0
- package/src/icons/wordcell/scopes.svg +3 -0
- package/src/icons/wordcell/search.svg +7 -0
- package/src/icons.generated.ts +734 -0
- package/src/icons.ts +51 -0
- package/src/index.ts +275 -0
- package/src/lantern-material.css +213 -0
- package/src/launch.ts +624 -0
- package/src/marketing-assets/UPSTREAM.md +18 -0
- package/src/marketing-assets/cells.svg +1 -0
- package/src/marketing-assets/grain.svg +1 -0
- package/src/marketing-forced-colors.css +12 -0
- package/src/mockups/client.tsx +415 -0
- package/src/mockups/core.tsx +318 -0
- package/src/mockups/frames.tsx +368 -0
- package/src/mockups/index.ts +14 -0
- package/src/mockups/surfaces.tsx +681 -0
- package/src/mockups.css +2607 -0
- package/src/palette-appearance.ts +45 -0
- package/src/palette-bridge.css +124 -0
- package/src/palette-color.ts +41 -0
- package/src/palette-system.css +721 -0
- package/src/palette-themes.ts +23 -0
- package/src/palette-tokens.stylex.ts +519 -0
- package/src/palettes.css +3 -0
- package/src/palettes.ts +161 -0
- package/src/paper-theme.css +244 -0
- package/src/plain-publication.css +1241 -0
- package/src/plain-site.css +397 -0
- package/src/platforms.ts +151 -0
- package/src/portfolio.generated.json +1595 -0
- package/src/portfolio.generated.ts +1596 -0
- package/src/portfolio.ts +226 -0
- package/src/product-marketing-foundation.css +433 -0
- package/src/product-marketing-preset.css +261 -0
- package/src/product-marketing.css +2186 -0
- package/src/provider-marks.generated.ts +112 -0
- package/src/provider-marks.ts +143 -0
- package/src/react/animated-rail-stage.stylex.ts +15 -0
- package/src/react/animated-rail-stage.tsx +51 -0
- package/src/react/app-shell.stylex.ts +104 -0
- package/src/react/app-shell.tsx +121 -0
- package/src/react/article.tsx +602 -0
- package/src/react/aurora-dots-background.tsx +34 -0
- package/src/react/charts.stylex.ts +563 -0
- package/src/react/charts.tsx +610 -0
- package/src/react/chat.stylex.ts +34 -0
- package/src/react/chat.tsx +155 -0
- package/src/react/design-gallery.tsx +943 -0
- package/src/react/design-palette.stylex.ts +124 -0
- package/src/react/design-palette.tsx +196 -0
- package/src/react/design-theme-context.tsx +41 -0
- package/src/react/effects.stylex.ts +325 -0
- package/src/react/fader.stylex.ts +71 -0
- package/src/react/fader.tsx +151 -0
- package/src/react/foil-card-math.ts +100 -0
- package/src/react/foil-card-surface.stylex.ts +326 -0
- package/src/react/foil-card-surface.tsx +742 -0
- package/src/react/foil-mark.tsx +35 -0
- package/src/react/foil.stylex.ts +163 -0
- package/src/react/haptics.ts +228 -0
- package/src/react/hero-backdrop.tsx +22 -0
- package/src/react/index.ts +38 -0
- package/src/react/keyboard-shortcuts.ts +228 -0
- package/src/react/lantern-material-gallery.tsx +166 -0
- package/src/react/lantern-material.stylex.ts +39 -0
- package/src/react/launch-beats.tsx +73 -0
- package/src/react/navigation-rail.stylex.ts +130 -0
- package/src/react/navigation-rail.tsx +228 -0
- package/src/react/particle-halo.tsx +115 -0
- package/src/react/phaser-dots.tsx +501 -0
- package/src/react/platform-icons.tsx +94 -0
- package/src/react/platform-install.stylex.ts +385 -0
- package/src/react/platform-install.tsx +389 -0
- package/src/react/playback-transport.stylex.ts +14 -0
- package/src/react/playback-transport.tsx +112 -0
- package/src/react/procedural-backdrop.tsx +192 -0
- package/src/react/procedural-recipe.ts +323 -0
- package/src/react/product-marketing.stylex.ts +4469 -0
- package/src/react/product-marketing.tsx +1611 -0
- package/src/react/production-data-preview-notice.stylex.ts +40 -0
- package/src/react/production-data-preview-notice.tsx +34 -0
- package/src/react/provider-mark.stylex.ts +132 -0
- package/src/react/provider-mark.tsx +120 -0
- package/src/react/relative-time.tsx +117 -0
- package/src/react/route-state.stylex.ts +39 -0
- package/src/react/route-state.tsx +350 -0
- package/src/react/server.ts +11 -0
- package/src/react/social-kit-panel.tsx +103 -0
- package/src/react/sticky-offset.tsx +33 -0
- package/src/react/surfaces.stylex.ts +179 -0
- package/src/react/surfaces.tsx +330 -0
- package/src/react/syntax-code.tsx +33 -0
- package/src/react/theme-color-sync.ts +1 -0
- package/src/react/theme-resolution.ts +11 -0
- package/src/react/theme.stylex.ts +276 -0
- package/src/react/theme.tsx +402 -0
- package/src/reading.css +72 -0
- package/src/relative-time.ts +185 -0
- package/src/reset.css +2 -0
- package/src/site-shell.css +16 -0
- package/src/status-page-html.ts +63 -0
- package/src/status-page.css +276 -0
- package/src/status-page.ts +323 -0
- package/src/styles.css +21 -0
- package/src/syntax-code-lexers.ts +303 -0
- package/src/syntax-highlighting.css +78 -0
- package/src/syntax-highlighting.ts +477 -0
- package/src/syntax-tokens.ts +33 -0
- package/src/testing.ts +307 -0
- package/src/tokens.css +3 -0
- package/src/typography.css +86 -0
- package/vendor/evilcharts/LICENSE +21 -0
- package/vendor/evilcharts/UPSTREAM.md +14 -0
- package/vendor/platform-marks/LICENSE +30 -0
- package/vendor/platform-marks/UPSTREAM.md +15 -0
- package/vendor/platform-marks/apple.svg +1 -0
- package/vendor/platform-marks/linux.svg +1 -0
- package/vendor/provider-marks/CRUSH-LICENSE.md +134 -0
- package/vendor/provider-marks/LICENSE +21 -0
- package/vendor/provider-marks/UPSTREAM.md +27 -0
- package/vendor/provider-marks/aider.svg +3 -0
- package/vendor/provider-marks/crush-heartbit.svg +8 -0
- package/vendor/provider-marks/lobehub/alibabacloud-color.svg +1 -0
- package/vendor/provider-marks/lobehub/alibabacloud.svg +1 -0
- package/vendor/provider-marks/lobehub/amp-color.svg +1 -0
- package/vendor/provider-marks/lobehub/amp.svg +1 -0
- package/vendor/provider-marks/lobehub/anthropic.svg +1 -0
- package/vendor/provider-marks/lobehub/claude-color.svg +1 -0
- package/vendor/provider-marks/lobehub/claude.svg +1 -0
- package/vendor/provider-marks/lobehub/claudecode-color.svg +1 -0
- package/vendor/provider-marks/lobehub/claudecode.svg +1 -0
- package/vendor/provider-marks/lobehub/codex-color.svg +1 -0
- package/vendor/provider-marks/lobehub/codex.svg +1 -0
- package/vendor/provider-marks/lobehub/cursor.svg +1 -0
- package/vendor/provider-marks/lobehub/deepseek-color.svg +1 -0
- package/vendor/provider-marks/lobehub/deepseek.svg +1 -0
- package/vendor/provider-marks/lobehub/devin-color.svg +1 -0
- package/vendor/provider-marks/lobehub/devin.svg +1 -0
- package/vendor/provider-marks/lobehub/gemini-color.svg +1 -0
- package/vendor/provider-marks/lobehub/gemini.svg +1 -0
- package/vendor/provider-marks/lobehub/geminicli-color.svg +1 -0
- package/vendor/provider-marks/lobehub/geminicli.svg +1 -0
- package/vendor/provider-marks/lobehub/goose.svg +1 -0
- package/vendor/provider-marks/lobehub/meta-color.svg +1 -0
- package/vendor/provider-marks/lobehub/meta.svg +1 -0
- package/vendor/provider-marks/lobehub/mistral-color.svg +1 -0
- package/vendor/provider-marks/lobehub/mistral.svg +1 -0
- package/vendor/provider-marks/lobehub/moonshot.svg +1 -0
- package/vendor/provider-marks/lobehub/nvidia-color.svg +1 -0
- package/vendor/provider-marks/lobehub/nvidia.svg +1 -0
- package/vendor/provider-marks/lobehub/openai.svg +1 -0
- package/vendor/provider-marks/lobehub/opencode.svg +1 -0
- package/vendor/provider-marks/lobehub/perplexity-color.svg +1 -0
- package/vendor/provider-marks/lobehub/perplexity.svg +1 -0
- package/vendor/provider-marks/lobehub/qwen-color.svg +1 -0
- package/vendor/provider-marks/lobehub/qwen.svg +1 -0
- package/vendor/provider-marks/lobehub/xai.svg +1 -0
- package/vendor/provider-marks/lobehub/zai.svg +1 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type CSSProperties,
|
|
5
|
+
type KeyboardEvent,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
useId,
|
|
8
|
+
useLayoutEffect,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
} from "react";
|
|
12
|
+
|
|
13
|
+
import { joinMockupClasses, type MockupTheme } from "./core.js";
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* Interactive shells for the code-built mockups. They hold selection state
|
|
17
|
+
* and nothing else: every surface is a render function over plain props, so
|
|
18
|
+
* the same surface renders statically in a film or a server page.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// React warns about useLayoutEffect during server rendering; these shells
|
|
22
|
+
// measure only in the browser.
|
|
23
|
+
const useIsomorphicLayoutEffect: typeof useLayoutEffect = typeof document === "undefined" ? () => undefined : useLayoutEffect;
|
|
24
|
+
|
|
25
|
+
function assertUniqueIds(items: readonly Readonly<{ id: string }>[], what: string): void {
|
|
26
|
+
if (items.length === 0) throw new RangeError(`${what} needs at least one entry.`);
|
|
27
|
+
const seen = new Set<string>();
|
|
28
|
+
for (const item of items) {
|
|
29
|
+
if (typeof item.id !== "string" || !/^[a-z0-9][a-z0-9-]*$/u.test(item.id)) {
|
|
30
|
+
throw new TypeError(`${what} ids must be lowercase letters, digits, and dashes: ${JSON.stringify(item.id)}`);
|
|
31
|
+
}
|
|
32
|
+
if (seen.has(item.id)) throw new RangeError(`${what} ids must be unique: ${item.id}`);
|
|
33
|
+
seen.add(item.id);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function itemAt<T>(items: readonly T[], index: number, what: string): T {
|
|
38
|
+
const item = items[index];
|
|
39
|
+
if (item === undefined) throw new RangeError(`${what} has no entry at ${String(index)}.`);
|
|
40
|
+
return item;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function assertCaption(caption: string, component: string): void {
|
|
44
|
+
if (typeof caption !== "string" || caption.trim() === "") {
|
|
45
|
+
throw new TypeError(`${component} needs a caption that says the picture is an illustration.`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Arrow, Home, and End move between tabs, as in the WAI-ARIA tabs pattern. */
|
|
50
|
+
function nextTabIndex(key: string, index: number, count: number): number | undefined {
|
|
51
|
+
if (key === "ArrowRight" || key === "ArrowDown") return (index + 1) % count;
|
|
52
|
+
if (key === "ArrowLeft" || key === "ArrowUp") return (index - 1 + count) % count;
|
|
53
|
+
if (key === "Home") return 0;
|
|
54
|
+
if (key === "End") return count - 1;
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* ------------------------------------------------------------------ */
|
|
59
|
+
/* FitToWidth */
|
|
60
|
+
/* ------------------------------------------------------------------ */
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Lays its child out at no less than `minWidth` pixels and scales it down to
|
|
64
|
+
* fit a narrower container, so a desktop mockup stays whole on a phone
|
|
65
|
+
* instead of reflowing into something no product looks like.
|
|
66
|
+
*/
|
|
67
|
+
export function FitToWidth({ children, className, minWidth = 400 }: Readonly<{ children: ReactNode; minWidth?: number; className?: string }>) {
|
|
68
|
+
if (!(minWidth > 0)) throw new RangeError("FitToWidth minWidth must be positive.");
|
|
69
|
+
const outer = useRef<HTMLDivElement>(null);
|
|
70
|
+
const inner = useRef<HTMLDivElement>(null);
|
|
71
|
+
const [fit, setFit] = useState<Readonly<{ width: number; scale: number; height: number }> | null>(null);
|
|
72
|
+
|
|
73
|
+
useIsomorphicLayoutEffect(() => {
|
|
74
|
+
const stage = outer.current;
|
|
75
|
+
const content = inner.current;
|
|
76
|
+
if (stage === null || content === null || typeof ResizeObserver === "undefined") return undefined;
|
|
77
|
+
const measure = () => {
|
|
78
|
+
const available = stage.clientWidth;
|
|
79
|
+
if (available <= 0) return;
|
|
80
|
+
const width = Math.max(available, minWidth);
|
|
81
|
+
const scale = available / width;
|
|
82
|
+
const height = content.offsetHeight * scale;
|
|
83
|
+
setFit((current) =>
|
|
84
|
+
current !== null && current.width === width && current.scale === scale && current.height === height ? current : { height, scale, width });
|
|
85
|
+
};
|
|
86
|
+
measure();
|
|
87
|
+
const observer = new ResizeObserver(measure);
|
|
88
|
+
observer.observe(stage);
|
|
89
|
+
observer.observe(content);
|
|
90
|
+
return () => observer.disconnect();
|
|
91
|
+
}, [minWidth]);
|
|
92
|
+
|
|
93
|
+
const scaled = fit !== null && fit.scale < 1;
|
|
94
|
+
return (
|
|
95
|
+
<div className={joinMockupClasses("hkm-fit", className)} ref={outer} style={scaled ? { height: fit.height } : undefined}>
|
|
96
|
+
<div
|
|
97
|
+
className="hkm-fit-inner"
|
|
98
|
+
data-hkm-scaled={scaled ? "" : undefined}
|
|
99
|
+
ref={inner}
|
|
100
|
+
style={scaled ? { transform: `scale(${fit.scale})`, width: fit.width } : undefined}
|
|
101
|
+
>
|
|
102
|
+
{children}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* ------------------------------------------------------------------ */
|
|
109
|
+
/* ModeShowcase */
|
|
110
|
+
/* ------------------------------------------------------------------ */
|
|
111
|
+
|
|
112
|
+
/** One choice on a showcase axis. `hint` says in one sentence what the choice does. */
|
|
113
|
+
export type ShowcaseChoice<I extends string> = Readonly<{ id: I; label: string; hint: string }>;
|
|
114
|
+
|
|
115
|
+
/** What a surface's render function receives. */
|
|
116
|
+
export type ShowcaseState<M extends string, O extends string = string> = Readonly<{
|
|
117
|
+
mode: M;
|
|
118
|
+
/** The mode before the last change; equals `mode` until the first change. */
|
|
119
|
+
previousMode: M;
|
|
120
|
+
option: O | undefined;
|
|
121
|
+
/** False on first paint, true after the first user change, so the first paint never animates. */
|
|
122
|
+
animated: boolean;
|
|
123
|
+
theme: MockupTheme | undefined;
|
|
124
|
+
}>;
|
|
125
|
+
|
|
126
|
+
/** One tab in a showcase. `render` returns a mockup from `@hraness/design-kit/mockups` or a product skin. */
|
|
127
|
+
export type ShowcaseSurface<S extends string, M extends string, O extends string = string> = Readonly<{
|
|
128
|
+
id: S;
|
|
129
|
+
label: string;
|
|
130
|
+
render: (state: ShowcaseState<M, O>) => ReactNode;
|
|
131
|
+
}>;
|
|
132
|
+
|
|
133
|
+
export type ModeShowcaseProps<S extends string, M extends string, O extends string = string> = Readonly<{
|
|
134
|
+
surfaces: readonly ShowcaseSurface<S, M, O>[];
|
|
135
|
+
modes: readonly ShowcaseChoice<M>[];
|
|
136
|
+
/** Label before the mode buttons, such as "Show AI text as". */
|
|
137
|
+
modeLabel?: string;
|
|
138
|
+
/** An optional second axis, such as sensitivity. */
|
|
139
|
+
options?: readonly ShowcaseChoice<O>[];
|
|
140
|
+
optionLabel?: string;
|
|
141
|
+
/** Modes in which the option axis does nothing; its buttons are disabled then. */
|
|
142
|
+
optionInactiveModes?: readonly M[];
|
|
143
|
+
initial?: Readonly<{ surface?: S; mode?: M; option?: O }>;
|
|
144
|
+
/** A live line under the stage that says what the current state shows. */
|
|
145
|
+
status?: (state: Readonly<{ surface: S; mode: M; option: O | undefined }>) => ReactNode;
|
|
146
|
+
/** Required. Say it is an illustration, such as "Illustration. Names and text are made up." */
|
|
147
|
+
caption: string;
|
|
148
|
+
/** Page-window height in CSS pixels at full scale. */
|
|
149
|
+
height?: number;
|
|
150
|
+
/** Narrowest layout width before the stage scales down. */
|
|
151
|
+
minWidth?: number;
|
|
152
|
+
theme?: MockupTheme;
|
|
153
|
+
/** Accessible name for the whole figure. Defaults to "Illustration of <surface>". */
|
|
154
|
+
label?: (surface: ShowcaseSurface<S, M, O>) => string;
|
|
155
|
+
className?: string;
|
|
156
|
+
}>;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* An interactive illustration: tabs pick a surface, a segmented control picks
|
|
160
|
+
* a mode, and an optional second control picks an option. Tabs follow the
|
|
161
|
+
* WAI-ARIA tabs pattern with automatic activation; mode and option buttons are
|
|
162
|
+
* toggle buttons in labelled groups.
|
|
163
|
+
*/
|
|
164
|
+
export function ModeShowcase<S extends string, M extends string, O extends string = string>({
|
|
165
|
+
caption,
|
|
166
|
+
className,
|
|
167
|
+
height = 440,
|
|
168
|
+
initial,
|
|
169
|
+
label,
|
|
170
|
+
minWidth = 400,
|
|
171
|
+
modeLabel = "Mode",
|
|
172
|
+
modes,
|
|
173
|
+
optionInactiveModes = [],
|
|
174
|
+
optionLabel = "Option",
|
|
175
|
+
options,
|
|
176
|
+
status,
|
|
177
|
+
surfaces,
|
|
178
|
+
theme,
|
|
179
|
+
}: ModeShowcaseProps<S, M, O>) {
|
|
180
|
+
assertUniqueIds(surfaces, "ModeShowcase surface");
|
|
181
|
+
assertUniqueIds(modes, "ModeShowcase mode");
|
|
182
|
+
if (options !== undefined) assertUniqueIds(options, "ModeShowcase option");
|
|
183
|
+
assertCaption(caption, "ModeShowcase");
|
|
184
|
+
if (!(height > 0)) throw new RangeError("ModeShowcase height must be positive.");
|
|
185
|
+
|
|
186
|
+
const id = useId();
|
|
187
|
+
const firstSurface = surfaces.find((surface) => surface.id === initial?.surface) ?? itemAt(surfaces, 0, "ModeShowcase surfaces");
|
|
188
|
+
const firstMode = modes.find((mode) => mode.id === initial?.mode) ?? itemAt(modes, 0, "ModeShowcase modes");
|
|
189
|
+
const firstOption = options === undefined ? undefined : (options.find((option) => option.id === initial?.option) ?? itemAt(options, 0, "ModeShowcase options"));
|
|
190
|
+
const [surfaceId, setSurfaceId] = useState<S>(firstSurface.id);
|
|
191
|
+
const [mode, setMode] = useState<M>(firstMode.id);
|
|
192
|
+
const [previousMode, setPreviousMode] = useState<M>(firstMode.id);
|
|
193
|
+
const [option, setOption] = useState<O | undefined>(firstOption?.id);
|
|
194
|
+
const [animated, setAnimated] = useState(false);
|
|
195
|
+
const tabs = useRef(new Map<S, HTMLButtonElement>());
|
|
196
|
+
|
|
197
|
+
const surface = surfaces.find((entry) => entry.id === surfaceId) ?? itemAt(surfaces, 0, "ModeShowcase surfaces");
|
|
198
|
+
const modeChoice = modes.find((entry) => entry.id === mode) ?? itemAt(modes, 0, "ModeShowcase modes");
|
|
199
|
+
const optionChoice = options?.find((entry) => entry.id === option);
|
|
200
|
+
const optionInactive = optionInactiveModes.includes(mode);
|
|
201
|
+
|
|
202
|
+
const chooseMode = (next: M) => {
|
|
203
|
+
if (next === mode) return;
|
|
204
|
+
setPreviousMode(mode);
|
|
205
|
+
setMode(next);
|
|
206
|
+
setAnimated(true);
|
|
207
|
+
};
|
|
208
|
+
const chooseOption = (next: O) => {
|
|
209
|
+
if (next === option) return;
|
|
210
|
+
setOption(next);
|
|
211
|
+
setAnimated(true);
|
|
212
|
+
};
|
|
213
|
+
const onTabKey = (event: KeyboardEvent<HTMLButtonElement>) => {
|
|
214
|
+
const index = surfaces.findIndex((entry) => entry.id === surface.id);
|
|
215
|
+
const next = nextTabIndex(event.key, index, surfaces.length);
|
|
216
|
+
if (next === undefined) return;
|
|
217
|
+
event.preventDefault();
|
|
218
|
+
const target = itemAt(surfaces, next, "ModeShowcase surfaces").id;
|
|
219
|
+
setSurfaceId(target);
|
|
220
|
+
tabs.current.get(target)?.focus();
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const statusNode = status?.({ mode, option, surface: surface.id });
|
|
224
|
+
return (
|
|
225
|
+
<figure
|
|
226
|
+
aria-label={label?.(surface) ?? `Illustration of ${surface.label.toLowerCase()}`}
|
|
227
|
+
className={joinMockupClasses("hkm-showcase", className)}
|
|
228
|
+
data-hkm-theme={theme}
|
|
229
|
+
data-nosnippet=""
|
|
230
|
+
>
|
|
231
|
+
<div className="hkm-showcase-controls">
|
|
232
|
+
{surfaces.length > 1 ? (
|
|
233
|
+
<div aria-label="Surface" className="hkm-tabs" role="tablist">
|
|
234
|
+
{surfaces.map((entry) => (
|
|
235
|
+
<button
|
|
236
|
+
aria-controls={`${id}-panel`}
|
|
237
|
+
aria-selected={entry.id === surface.id}
|
|
238
|
+
className="hkm-tab"
|
|
239
|
+
id={`${id}-tab-${entry.id}`}
|
|
240
|
+
key={entry.id}
|
|
241
|
+
onClick={() => setSurfaceId(entry.id)}
|
|
242
|
+
onKeyDown={onTabKey}
|
|
243
|
+
ref={(element) => {
|
|
244
|
+
if (element === null) tabs.current.delete(entry.id);
|
|
245
|
+
else tabs.current.set(entry.id, element);
|
|
246
|
+
}}
|
|
247
|
+
role="tab"
|
|
248
|
+
tabIndex={entry.id === surface.id ? 0 : -1}
|
|
249
|
+
type="button"
|
|
250
|
+
>
|
|
251
|
+
{entry.label}
|
|
252
|
+
</button>
|
|
253
|
+
))}
|
|
254
|
+
</div>
|
|
255
|
+
) : null}
|
|
256
|
+
<div className="hkm-showcase-settings">
|
|
257
|
+
<div className="hkm-showcase-row">
|
|
258
|
+
<span className="hkm-showcase-label" id={`${id}-mode`}>{modeLabel}</span>
|
|
259
|
+
<div aria-labelledby={`${id}-mode`} className="hkm-segmented" role="group">
|
|
260
|
+
{modes.map((entry) => (
|
|
261
|
+
<button aria-pressed={entry.id === mode} key={entry.id} onClick={() => chooseMode(entry.id)} type="button">
|
|
262
|
+
{entry.label}
|
|
263
|
+
</button>
|
|
264
|
+
))}
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
{options === undefined ? null : (
|
|
268
|
+
<div className="hkm-showcase-row">
|
|
269
|
+
<span className="hkm-showcase-label" id={`${id}-option`}>{optionLabel}</span>
|
|
270
|
+
<div aria-labelledby={`${id}-option`} className="hkm-segmented" role="group">
|
|
271
|
+
{options.map((entry) => (
|
|
272
|
+
<button aria-pressed={entry.id === option} disabled={optionInactive} key={entry.id} onClick={() => chooseOption(entry.id)} type="button">
|
|
273
|
+
{entry.label}
|
|
274
|
+
</button>
|
|
275
|
+
))}
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
)}
|
|
279
|
+
</div>
|
|
280
|
+
<p className="hkm-showcase-hint">
|
|
281
|
+
{modeChoice.hint}
|
|
282
|
+
{optionChoice === undefined || optionInactive ? null : ` ${optionChoice.hint}`}
|
|
283
|
+
</p>
|
|
284
|
+
</div>
|
|
285
|
+
<div
|
|
286
|
+
aria-labelledby={surfaces.length > 1 ? `${id}-tab-${surface.id}` : undefined}
|
|
287
|
+
className="hkm-showcase-stage"
|
|
288
|
+
data-hkm-animated={animated ? "" : undefined}
|
|
289
|
+
data-hkm-from={animated ? previousMode : undefined}
|
|
290
|
+
id={`${id}-panel`}
|
|
291
|
+
role={surfaces.length > 1 ? "tabpanel" : undefined}
|
|
292
|
+
style={{ "--hkm-page-height": `${height}px` } as CSSProperties}
|
|
293
|
+
>
|
|
294
|
+
<FitToWidth minWidth={minWidth}>
|
|
295
|
+
{surface.render({ animated, mode, option, previousMode, theme })}
|
|
296
|
+
</FitToWidth>
|
|
297
|
+
</div>
|
|
298
|
+
<figcaption className="hkm-showcase-caption">
|
|
299
|
+
{statusNode === undefined ? null : <span aria-live="polite" className="hkm-showcase-status">{statusNode}</span>}
|
|
300
|
+
<span>{caption}</span>
|
|
301
|
+
</figcaption>
|
|
302
|
+
</figure>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/* ------------------------------------------------------------------ */
|
|
307
|
+
/* StepThrough */
|
|
308
|
+
/* ------------------------------------------------------------------ */
|
|
309
|
+
|
|
310
|
+
/** One step in a step-through. `render` gets whether transitions may run. */
|
|
311
|
+
export type ThroughStep = Readonly<{
|
|
312
|
+
id: string;
|
|
313
|
+
label: string;
|
|
314
|
+
/** One sentence under the stage that says what this step shows. */
|
|
315
|
+
hint?: string;
|
|
316
|
+
render: (state: Readonly<{ animated: boolean; theme: MockupTheme | undefined }>) => ReactNode;
|
|
317
|
+
}>;
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Walks a flow one step at a time: numbered step tabs, Back and Next buttons,
|
|
321
|
+
* and a live hint. The keyboard model matches `ModeShowcase`.
|
|
322
|
+
*/
|
|
323
|
+
export function StepThrough({
|
|
324
|
+
caption,
|
|
325
|
+
className,
|
|
326
|
+
initial,
|
|
327
|
+
label = "Steps",
|
|
328
|
+
minWidth = 400,
|
|
329
|
+
steps,
|
|
330
|
+
theme,
|
|
331
|
+
}: Readonly<{
|
|
332
|
+
steps: readonly ThroughStep[];
|
|
333
|
+
caption: string;
|
|
334
|
+
initial?: string;
|
|
335
|
+
label?: string;
|
|
336
|
+
minWidth?: number;
|
|
337
|
+
theme?: MockupTheme;
|
|
338
|
+
className?: string;
|
|
339
|
+
}>) {
|
|
340
|
+
assertUniqueIds(steps, "StepThrough step");
|
|
341
|
+
assertCaption(caption, "StepThrough");
|
|
342
|
+
const id = useId();
|
|
343
|
+
const [index, setIndex] = useState(() => Math.max(0, steps.findIndex((step) => step.id === initial)));
|
|
344
|
+
const [animated, setAnimated] = useState(false);
|
|
345
|
+
const tabs = useRef(new Map<number, HTMLButtonElement>());
|
|
346
|
+
const current = Math.min(index, steps.length - 1);
|
|
347
|
+
const step = itemAt(steps, current, "StepThrough steps");
|
|
348
|
+
|
|
349
|
+
const go = (next: number, focus = false) => {
|
|
350
|
+
if (next < 0 || next >= steps.length || next === current) return;
|
|
351
|
+
setIndex(next);
|
|
352
|
+
setAnimated(true);
|
|
353
|
+
if (focus) tabs.current.get(next)?.focus();
|
|
354
|
+
};
|
|
355
|
+
const onTabKey = (event: KeyboardEvent<HTMLButtonElement>) => {
|
|
356
|
+
const next = nextTabIndex(event.key, current, steps.length);
|
|
357
|
+
if (next === undefined) return;
|
|
358
|
+
event.preventDefault();
|
|
359
|
+
go(next, true);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
return (
|
|
363
|
+
<figure aria-label={`Illustration: ${label.toLowerCase()}`} className={joinMockupClasses("hkm-showcase", "hkm-steps", className)} data-hkm-theme={theme} data-nosnippet="">
|
|
364
|
+
<div className="hkm-showcase-controls">
|
|
365
|
+
<div aria-label={label} className="hkm-tabs hkm-step-tabs" role="tablist">
|
|
366
|
+
{steps.map((entry, position) => (
|
|
367
|
+
<button
|
|
368
|
+
aria-controls={`${id}-panel`}
|
|
369
|
+
aria-selected={position === current}
|
|
370
|
+
className="hkm-tab"
|
|
371
|
+
data-hkm-done={position < current ? "" : undefined}
|
|
372
|
+
id={`${id}-tab-${entry.id}`}
|
|
373
|
+
key={entry.id}
|
|
374
|
+
onClick={() => go(position)}
|
|
375
|
+
onKeyDown={onTabKey}
|
|
376
|
+
ref={(element) => {
|
|
377
|
+
if (element === null) tabs.current.delete(position);
|
|
378
|
+
else tabs.current.set(position, element);
|
|
379
|
+
}}
|
|
380
|
+
role="tab"
|
|
381
|
+
tabIndex={position === current ? 0 : -1}
|
|
382
|
+
type="button"
|
|
383
|
+
>
|
|
384
|
+
<span aria-hidden="true" className="hkm-step-number">{position + 1}</span>
|
|
385
|
+
{entry.label}
|
|
386
|
+
</button>
|
|
387
|
+
))}
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
<div
|
|
391
|
+
aria-labelledby={`${id}-tab-${step.id}`}
|
|
392
|
+
className="hkm-showcase-stage"
|
|
393
|
+
data-hkm-animated={animated ? "" : undefined}
|
|
394
|
+
id={`${id}-panel`}
|
|
395
|
+
role="tabpanel"
|
|
396
|
+
>
|
|
397
|
+
<FitToWidth minWidth={minWidth}>{step.render({ animated, theme })}</FitToWidth>
|
|
398
|
+
</div>
|
|
399
|
+
<div className="hkm-step-nav">
|
|
400
|
+
<button className="hkm-step-button" disabled={current === 0} onClick={() => go(current - 1)} type="button">
|
|
401
|
+
Back
|
|
402
|
+
</button>
|
|
403
|
+
<span aria-live="polite" className="hkm-showcase-status">
|
|
404
|
+
Step {current + 1} of {steps.length}{step.hint === undefined ? "" : `. ${step.hint}`}
|
|
405
|
+
</span>
|
|
406
|
+
<button className="hkm-step-button" data-hkm-primary="" disabled={current === steps.length - 1} onClick={() => go(current + 1)} type="button">
|
|
407
|
+
Next
|
|
408
|
+
</button>
|
|
409
|
+
</div>
|
|
410
|
+
<figcaption className="hkm-showcase-caption">
|
|
411
|
+
<span>{caption}</span>
|
|
412
|
+
</figcaption>
|
|
413
|
+
</figure>
|
|
414
|
+
);
|
|
415
|
+
}
|