@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,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static HTML for the shared 404 page. Static-site builders that do not run
|
|
3
|
+
* React write this into their `404.html`, load `status-page.css` (or
|
|
4
|
+
* styles.css), and call `attachStatusPage` from the browser entry. For a
|
|
5
|
+
* missing page the markup matches `StatusPage` in `@hraness/design-kit/react`
|
|
6
|
+
* byte for byte; tests bind the two. React error pages add the focus and
|
|
7
|
+
* live-region attributes a runtime error needs. Text arguments are escaped.
|
|
8
|
+
* The next-links heading has a fixed id, so render one page per document.
|
|
9
|
+
*/
|
|
10
|
+
import { escapeArticleHtml as escape } from "./article-html.js";
|
|
11
|
+
import {
|
|
12
|
+
STATUS_PAGE_AGENT_PREFIX,
|
|
13
|
+
STATUS_PAGE_BACK_LABEL,
|
|
14
|
+
STATUS_PAGE_HINT_PREFIX,
|
|
15
|
+
STATUS_PAGE_NEXT_HEADING_ID,
|
|
16
|
+
resolveStatusPage,
|
|
17
|
+
statusPageRoutesAttribute,
|
|
18
|
+
type StatusPageContent,
|
|
19
|
+
} from "./status-page.js";
|
|
20
|
+
|
|
21
|
+
export type StatusPageHtmlOptions = StatusPageContent & Readonly<{
|
|
22
|
+
/** Heading level of the title; the next-links heading is one level lower. */
|
|
23
|
+
titleLevel?: 1 | 2 | 3 | 4 | 5;
|
|
24
|
+
/** Root element. Use "div" when the site layout already provides `main`. */
|
|
25
|
+
rootElement?: "main" | "div";
|
|
26
|
+
}>;
|
|
27
|
+
|
|
28
|
+
export function renderStatusPageHtml(options: StatusPageHtmlOptions = {}): string {
|
|
29
|
+
const page = resolveStatusPage(options);
|
|
30
|
+
const level = options.titleLevel ?? 1;
|
|
31
|
+
const root = options.rootElement ?? "main";
|
|
32
|
+
const notFound = page.kind === "not-found";
|
|
33
|
+
const routes = notFound ? statusPageRoutesAttribute(page.routes) : undefined;
|
|
34
|
+
const next = page.next.length === 0 ? "" : [
|
|
35
|
+
`<nav aria-labelledby="${STATUS_PAGE_NEXT_HEADING_ID}" class="hraness-status-page__next">`,
|
|
36
|
+
`<h${level + 1} class="hraness-status-page__next-heading" id="${STATUS_PAGE_NEXT_HEADING_ID}">${escape(page.nextHeading)}</h${level + 1}>`,
|
|
37
|
+
'<ul class="hraness-status-page__next-list">',
|
|
38
|
+
...page.next.map((link) => [
|
|
39
|
+
`<li><a class="hraness-status-page__next-link" href="${escape(link.href)}">`,
|
|
40
|
+
`<span class="hraness-status-page__next-label">${escape(link.label)}</span>`,
|
|
41
|
+
link.description === undefined ? "" : `<span class="hraness-status-page__next-description">${escape(link.description)}</span>`,
|
|
42
|
+
"</a></li>",
|
|
43
|
+
].join("")),
|
|
44
|
+
"</ul></nav>",
|
|
45
|
+
].join("");
|
|
46
|
+
const agent = page.agentIndexHref === undefined ? "" : `<p class="hraness-status-page__agent">${STATUS_PAGE_AGENT_PREFIX} <a href="${escape(page.agentIndexHref)}">${escape(page.agentIndexHref)}</a></p>`;
|
|
47
|
+
return [
|
|
48
|
+
`<${root} class="hraness-status-page"${routes === undefined ? "" : ` data-hraness-status-routes="${escape(routes)}"`} data-kind="${page.kind}">`,
|
|
49
|
+
'<div class="hraness-status-page__inner">',
|
|
50
|
+
`<div aria-hidden="true" class="hraness-status-page__code"><span class="hraness-status-page__glyph">${escape(page.glyph)}</span><canvas class="hraness-status-page__field"></canvas></div>`,
|
|
51
|
+
`<h${level} class="hraness-status-page__title">${escape(page.title)}</h${level}>`,
|
|
52
|
+
`<p class="hraness-status-page__summary">${escape(page.summary)}</p>`,
|
|
53
|
+
notFound ? `<p class="hraness-status-page__hint" hidden="">${STATUS_PAGE_HINT_PREFIX} <a class="hraness-status-page__hint-link" href="/"></a>?</p>` : "",
|
|
54
|
+
'<div class="hraness-status-page__actions">',
|
|
55
|
+
`<a class="hraness-status-page__action hraness-foil" data-emphasis="primary" data-foil="" href="${escape(page.primaryAction.href)}">${escape(page.primaryAction.label)}</a>`,
|
|
56
|
+
`<a class="hraness-status-page__back" hidden="" href="/">${STATUS_PAGE_BACK_LABEL}</a>`,
|
|
57
|
+
"</div>",
|
|
58
|
+
next,
|
|
59
|
+
agent,
|
|
60
|
+
"</div>",
|
|
61
|
+
`</${root}>`,
|
|
62
|
+
].join("");
|
|
63
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status pages
|
|
3
|
+
*
|
|
4
|
+
* One composition for the 404 page and recoverable error pages, shared by the
|
|
5
|
+
* React `StatusPage` and the static `renderStatusPageHtml` markup. The glyph is
|
|
6
|
+
* real text; `attachStatusPage` draws it as a dot field on a canvas and marks
|
|
7
|
+
* the root `data-hraness-status-field="live"` only after the canvas paints.
|
|
8
|
+
* Sites retheme through --hraness-status-ink, -muted, -accent, -line, -radius,
|
|
9
|
+
* and -glyph-font; everything else follows the palette tokens.
|
|
10
|
+
*/
|
|
11
|
+
@layer components.hraness-design-kit.legacy {
|
|
12
|
+
.hraness-status-page {
|
|
13
|
+
--_hraness-status-ink: var(--hraness-status-ink, var(--foreground, CanvasText));
|
|
14
|
+
--_hraness-status-muted: var(--hraness-status-muted, var(--muted, GrayText));
|
|
15
|
+
--_hraness-status-accent: var(--hraness-status-accent, var(--hraness-site-accent, var(--primary, var(--accent, LinkText))));
|
|
16
|
+
--_hraness-status-line: var(--hraness-status-line, color-mix(in srgb, var(--_hraness-status-ink) 14%, transparent));
|
|
17
|
+
--_hraness-status-radius: var(--hraness-status-radius, var(--hraness-marketing-radius, 0.75rem));
|
|
18
|
+
|
|
19
|
+
display: grid;
|
|
20
|
+
min-block-size: min(84svh, 52rem);
|
|
21
|
+
box-sizing: border-box;
|
|
22
|
+
place-items: center;
|
|
23
|
+
padding-block: clamp(2.5rem, 7vw, 5rem);
|
|
24
|
+
padding-inline: max(1rem, var(--layout-edge-inset, 1rem));
|
|
25
|
+
overflow-x: clip;
|
|
26
|
+
color: var(--_hraness-status-ink);
|
|
27
|
+
font-family: var(--font-text, inherit);
|
|
28
|
+
outline: none;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.hraness-status-page__inner {
|
|
32
|
+
display: flex;
|
|
33
|
+
inline-size: min(100%, 42rem);
|
|
34
|
+
min-inline-size: 0;
|
|
35
|
+
flex-direction: column;
|
|
36
|
+
align-items: center;
|
|
37
|
+
text-align: center;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.hraness-status-page__code {
|
|
41
|
+
position: relative;
|
|
42
|
+
display: grid;
|
|
43
|
+
inline-size: 100%;
|
|
44
|
+
place-items: center;
|
|
45
|
+
margin-block-end: clamp(0.75rem, 2.5vw, 1.5rem);
|
|
46
|
+
user-select: none;
|
|
47
|
+
-webkit-user-select: none;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.hraness-status-page__glyph {
|
|
51
|
+
padding-block: 0.06em;
|
|
52
|
+
color: color-mix(in srgb, var(--_hraness-status-ink) 82%, transparent);
|
|
53
|
+
font-family: var(--hraness-status-glyph-font, var(--hraness-type-heading-font, var(--font-heading, inherit)));
|
|
54
|
+
font-size: clamp(7rem, 30vw, 13.5rem);
|
|
55
|
+
font-variant-numeric: lining-nums;
|
|
56
|
+
font-weight: var(--hraness-type-heading-weight, 600);
|
|
57
|
+
letter-spacing: -0.02em;
|
|
58
|
+
line-height: 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* The canvas reads its accent from `color`. */
|
|
62
|
+
.hraness-status-page__field {
|
|
63
|
+
position: absolute;
|
|
64
|
+
inset: -2.5rem -1.5rem;
|
|
65
|
+
display: block;
|
|
66
|
+
inline-size: calc(100% + 3rem);
|
|
67
|
+
block-size: calc(100% + 5rem);
|
|
68
|
+
color: var(--_hraness-status-accent);
|
|
69
|
+
opacity: 0;
|
|
70
|
+
pointer-events: none;
|
|
71
|
+
transition: opacity 240ms ease-out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.hraness-status-page[data-hraness-status-field="live"] .hraness-status-page__glyph {
|
|
75
|
+
visibility: hidden;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.hraness-status-page[data-hraness-status-field="live"] .hraness-status-page__field {
|
|
79
|
+
opacity: 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.hraness-status-page__title {
|
|
83
|
+
max-inline-size: 22ch;
|
|
84
|
+
margin: 0;
|
|
85
|
+
color: var(--_hraness-status-ink);
|
|
86
|
+
font-family: var(--hraness-type-heading-font, var(--font-heading, inherit));
|
|
87
|
+
font-size: clamp(1.75rem, 4.2vw, 2.4rem);
|
|
88
|
+
font-weight: var(--hraness-type-heading-weight, 600);
|
|
89
|
+
letter-spacing: -0.012em;
|
|
90
|
+
line-height: 1.15;
|
|
91
|
+
text-wrap: balance;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.hraness-status-page__summary {
|
|
95
|
+
max-inline-size: 34rem;
|
|
96
|
+
margin: 0.65rem 0 0;
|
|
97
|
+
color: var(--_hraness-status-muted);
|
|
98
|
+
font-size: 1.0625rem;
|
|
99
|
+
line-height: 1.55;
|
|
100
|
+
text-wrap: pretty;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.hraness-status-page__hint {
|
|
104
|
+
margin: 0.85rem 0 0;
|
|
105
|
+
color: var(--_hraness-status-ink);
|
|
106
|
+
font-size: 1rem;
|
|
107
|
+
line-height: 1.5;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.hraness-status-page__hint-link {
|
|
111
|
+
color: var(--_hraness-status-accent);
|
|
112
|
+
font-weight: 600;
|
|
113
|
+
text-decoration-line: underline;
|
|
114
|
+
text-decoration-thickness: 1px;
|
|
115
|
+
text-underline-offset: 0.2em;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.hraness-status-page__hint[hidden],
|
|
119
|
+
.hraness-status-page__back[hidden] {
|
|
120
|
+
display: none;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.hraness-status-page__actions {
|
|
124
|
+
display: flex;
|
|
125
|
+
flex-wrap: wrap;
|
|
126
|
+
align-items: center;
|
|
127
|
+
justify-content: center;
|
|
128
|
+
gap: 0.5rem 1.25rem;
|
|
129
|
+
margin-block-start: 1.6rem;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* Zero specificity, so the shared foil treatment wins wherever it loads. */
|
|
133
|
+
:where(.hraness-status-page__action) {
|
|
134
|
+
display: inline-flex;
|
|
135
|
+
min-block-size: 2.75rem;
|
|
136
|
+
box-sizing: border-box;
|
|
137
|
+
align-items: center;
|
|
138
|
+
justify-content: center;
|
|
139
|
+
padding: 0.6rem 1.3rem;
|
|
140
|
+
border: 1px solid var(--_hraness-status-line);
|
|
141
|
+
border-radius: var(--_hraness-status-radius);
|
|
142
|
+
background: transparent;
|
|
143
|
+
color: var(--_hraness-status-ink);
|
|
144
|
+
cursor: pointer;
|
|
145
|
+
font: inherit;
|
|
146
|
+
font-size: 0.975rem;
|
|
147
|
+
font-weight: 600;
|
|
148
|
+
line-height: 1.2;
|
|
149
|
+
text-decoration: none;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
:where(.hraness-status-page__action[data-emphasis="primary"]) {
|
|
153
|
+
border: 2px solid var(--_hraness-status-accent);
|
|
154
|
+
background: var(--surface, var(--background, Canvas));
|
|
155
|
+
box-shadow: 0 1px 4px color-mix(in srgb, var(--_hraness-status-ink) 12%, transparent);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.hraness-status-page__back {
|
|
159
|
+
color: var(--_hraness-status-muted);
|
|
160
|
+
font-size: 0.95rem;
|
|
161
|
+
text-decoration-line: underline;
|
|
162
|
+
text-decoration-color: var(--_hraness-status-line);
|
|
163
|
+
text-underline-offset: 0.25em;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.hraness-status-page__back:hover {
|
|
167
|
+
color: var(--_hraness-status-ink);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.hraness-status-page__next {
|
|
171
|
+
inline-size: 100%;
|
|
172
|
+
margin-block-start: clamp(2.75rem, 7vw, 4rem);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.hraness-status-page__next-heading {
|
|
176
|
+
margin: 0 0 0.85rem;
|
|
177
|
+
color: var(--_hraness-status-muted);
|
|
178
|
+
font-family: var(--font-text, inherit);
|
|
179
|
+
font-size: 0.875rem;
|
|
180
|
+
font-weight: 500;
|
|
181
|
+
letter-spacing: 0;
|
|
182
|
+
line-height: 1.4;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.hraness-status-page__next-list {
|
|
186
|
+
display: grid;
|
|
187
|
+
gap: 0.65rem;
|
|
188
|
+
grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
|
|
189
|
+
margin: 0;
|
|
190
|
+
padding: 0;
|
|
191
|
+
list-style: none;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.hraness-status-page__next-link {
|
|
195
|
+
display: flex;
|
|
196
|
+
block-size: 100%;
|
|
197
|
+
box-sizing: border-box;
|
|
198
|
+
flex-direction: column;
|
|
199
|
+
gap: 0.2rem;
|
|
200
|
+
padding: 0.85rem 1rem;
|
|
201
|
+
border: 1px solid var(--_hraness-status-line);
|
|
202
|
+
border-radius: var(--_hraness-status-radius);
|
|
203
|
+
background: color-mix(in srgb, var(--surface, var(--background, Canvas)) 70%, transparent);
|
|
204
|
+
color: var(--_hraness-status-ink);
|
|
205
|
+
text-align: start;
|
|
206
|
+
text-decoration: none;
|
|
207
|
+
transition: border-color 160ms ease-out, transform 160ms ease-out;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.hraness-status-page__next-link:hover {
|
|
211
|
+
border-color: color-mix(in srgb, var(--_hraness-status-accent) 55%, var(--_hraness-status-line));
|
|
212
|
+
transform: translateY(-1px);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.hraness-status-page__next-label {
|
|
216
|
+
font-size: 0.975rem;
|
|
217
|
+
font-weight: 600;
|
|
218
|
+
line-height: 1.3;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.hraness-status-page__next-description {
|
|
222
|
+
color: var(--_hraness-status-muted);
|
|
223
|
+
font-size: 0.875rem;
|
|
224
|
+
line-height: 1.4;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.hraness-status-page__agent {
|
|
228
|
+
margin: 2rem 0 0;
|
|
229
|
+
color: var(--_hraness-status-muted);
|
|
230
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
231
|
+
font-size: 0.75rem;
|
|
232
|
+
line-height: 1.5;
|
|
233
|
+
overflow-wrap: anywhere;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.hraness-status-page__agent a {
|
|
237
|
+
color: inherit;
|
|
238
|
+
text-underline-offset: 0.2em;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.hraness-status-page a:focus-visible,
|
|
242
|
+
.hraness-status-page button:focus-visible {
|
|
243
|
+
outline: 2px solid var(--_hraness-status-accent);
|
|
244
|
+
outline-offset: 2px;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
@media (prefers-reduced-motion: reduce) {
|
|
248
|
+
.hraness-status-page__field,
|
|
249
|
+
.hraness-status-page__next-link {
|
|
250
|
+
transition: none;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.hraness-status-page__next-link:hover {
|
|
254
|
+
transform: none;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@media (forced-colors: active) {
|
|
259
|
+
.hraness-status-page__field {
|
|
260
|
+
display: none;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.hraness-status-page[data-hraness-status-field="live"] .hraness-status-page__glyph {
|
|
264
|
+
visibility: visible;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.hraness-status-page__glyph {
|
|
268
|
+
color: CanvasText;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
:where(.hraness-status-page__action),
|
|
272
|
+
.hraness-status-page__next-link {
|
|
273
|
+
border-color: ButtonText;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-neutral content for full-page status routes: the 404 page and the
|
|
3
|
+
* recoverable error pages. React (`@hraness/design-kit/react`) and static HTML
|
|
4
|
+
* (`renderStatusPageHtml`) render the same markup from these values, styled by
|
|
5
|
+
* `status-page.css`; `attachStatusPage` from the browser entry adds the dot
|
|
6
|
+
* field, the "Did you mean" hint, and the same-site Back link.
|
|
7
|
+
*/
|
|
8
|
+
import { assertArticleHref } from "./article.js";
|
|
9
|
+
|
|
10
|
+
export type StatusPageKind = "not-found" | "error";
|
|
11
|
+
|
|
12
|
+
export type StatusPageLink = Readonly<{
|
|
13
|
+
href: string;
|
|
14
|
+
label: string;
|
|
15
|
+
/** One short line under the label. Next links only. */
|
|
16
|
+
description?: string;
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
export type StatusPageContent = Readonly<{
|
|
20
|
+
kind?: StatusPageKind;
|
|
21
|
+
/** Product name used in the default primary action ("Go to Sponge"). */
|
|
22
|
+
siteName?: string;
|
|
23
|
+
/** Decorative glyph drawn by the dot field. Defaults to "404" or "!". */
|
|
24
|
+
glyph?: string;
|
|
25
|
+
title?: string;
|
|
26
|
+
summary?: string;
|
|
27
|
+
/**
|
|
28
|
+
* The one action most readers should take. Point it at the product's main
|
|
29
|
+
* conversion step, such as starting a library or installing the CLI.
|
|
30
|
+
*/
|
|
31
|
+
primaryAction?: StatusPageLink;
|
|
32
|
+
/** At most three places worth starting from, each with an optional line. */
|
|
33
|
+
next?: readonly StatusPageLink[];
|
|
34
|
+
nextHeading?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Known internal pages. They are never listed; the browser controller
|
|
37
|
+
* compares them with the missing address and offers the closest one.
|
|
38
|
+
*/
|
|
39
|
+
routes?: readonly StatusPageLink[];
|
|
40
|
+
/** A machine-readable index such as "/llms.txt", shown as one quiet line. */
|
|
41
|
+
agentIndexHref?: string;
|
|
42
|
+
}>;
|
|
43
|
+
|
|
44
|
+
export const STATUS_PAGE_MAX_NEXT = 3;
|
|
45
|
+
export const STATUS_PAGE_MAX_ROUTES = 2000;
|
|
46
|
+
export const STATUS_PAGE_BACK_LABEL = "Go back";
|
|
47
|
+
export const STATUS_PAGE_HINT_PREFIX = "Did you mean";
|
|
48
|
+
export const STATUS_PAGE_NEXT_HEADING_ID = "hraness-status-page-next";
|
|
49
|
+
export const STATUS_PAGE_AGENT_PREFIX = "AI agents can start at";
|
|
50
|
+
|
|
51
|
+
export type ResolvedStatusPage = Readonly<{
|
|
52
|
+
kind: StatusPageKind;
|
|
53
|
+
glyph: string;
|
|
54
|
+
title: string;
|
|
55
|
+
summary: string;
|
|
56
|
+
primaryAction: StatusPageLink;
|
|
57
|
+
next: readonly StatusPageLink[];
|
|
58
|
+
nextHeading: string;
|
|
59
|
+
routes: readonly StatusPageLink[];
|
|
60
|
+
agentIndexHref?: string;
|
|
61
|
+
}>;
|
|
62
|
+
|
|
63
|
+
const defaults = {
|
|
64
|
+
"not-found": {
|
|
65
|
+
glyph: "404",
|
|
66
|
+
title: "We can’t find that page",
|
|
67
|
+
summary: "The link may be out of date or mistyped.",
|
|
68
|
+
},
|
|
69
|
+
error: {
|
|
70
|
+
glyph: "!",
|
|
71
|
+
title: "This view could not load",
|
|
72
|
+
summary: "Retry this view, or return home and continue from there.",
|
|
73
|
+
},
|
|
74
|
+
} as const satisfies Record<StatusPageKind, Readonly<{ glyph: string; title: string; summary: string }>>;
|
|
75
|
+
|
|
76
|
+
function assertText(name: string, value: string, limit: number): void {
|
|
77
|
+
if (value.trim() === "" || value.length > limit) {
|
|
78
|
+
throw new RangeError(`Status page ${name} must be 1–${limit} characters.`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function assertLink(name: string, link: StatusPageLink): void {
|
|
83
|
+
assertArticleHref(link.href);
|
|
84
|
+
assertText(`${name} label`, link.label, 48);
|
|
85
|
+
if (link.description !== undefined) assertText(`${name} description`, link.description, 90);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const ROUTE_LABEL_LIMIT = 80;
|
|
89
|
+
|
|
90
|
+
/** True when `href` holds whitespace or a C0/C1 control character. */
|
|
91
|
+
function hasControlOrSpace(href: string): boolean {
|
|
92
|
+
for (const character of href) {
|
|
93
|
+
const code = character.codePointAt(0) ?? 0;
|
|
94
|
+
if (code <= 0x20 || (code >= 0x7f && code <= 0x9f) || /\s/u.test(character)) return true;
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Shorten a long page title at a word boundary for the "Did you mean" line. */
|
|
100
|
+
function shortRouteLabel(label: string): string {
|
|
101
|
+
const text = label.trim().replace(/\s+/gu, " ");
|
|
102
|
+
if (text.length <= ROUTE_LABEL_LIMIT) return text;
|
|
103
|
+
const cut = text.slice(0, ROUTE_LABEL_LIMIT - 1);
|
|
104
|
+
const space = cut.lastIndexOf(" ");
|
|
105
|
+
return `${(space > ROUTE_LABEL_LIMIT / 2 ? cut.slice(0, space) : cut).trimEnd()}…`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Routes usually come straight from a sitemap or article index, so they are
|
|
110
|
+
* cleaned rather than rejected: entries that are not same-site paths or have
|
|
111
|
+
* no label are dropped, and long titles are shortened.
|
|
112
|
+
*/
|
|
113
|
+
function usableRoutes(routes: readonly unknown[]): StatusPageLink[] {
|
|
114
|
+
const usable: StatusPageLink[] = [];
|
|
115
|
+
for (const route of routes.slice(0, STATUS_PAGE_MAX_ROUTES)) {
|
|
116
|
+
if (typeof route !== "object" || route === null) continue;
|
|
117
|
+
const { href, label } = route as { href?: unknown; label?: unknown };
|
|
118
|
+
if (typeof href !== "string" || typeof label !== "string" || label.trim() === "") continue;
|
|
119
|
+
if (!isSitePath(href) || hasControlOrSpace(href)) continue;
|
|
120
|
+
usable.push({ href, label: shortRouteLabel(label) });
|
|
121
|
+
}
|
|
122
|
+
return usable;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Apply defaults and reject content the page cannot show well. */
|
|
126
|
+
export function resolveStatusPage(content: StatusPageContent = {}): ResolvedStatusPage {
|
|
127
|
+
const kind = content.kind ?? "not-found";
|
|
128
|
+
const base = defaults[kind];
|
|
129
|
+
const siteName = content.siteName?.trim();
|
|
130
|
+
if (siteName !== undefined) assertText("siteName", siteName, 40);
|
|
131
|
+
const glyph = content.glyph ?? base.glyph;
|
|
132
|
+
assertText("glyph", glyph, 4);
|
|
133
|
+
const title = content.title ?? base.title;
|
|
134
|
+
assertText("title", title, 60);
|
|
135
|
+
const summary = content.summary ?? base.summary;
|
|
136
|
+
assertText("summary", summary, 160);
|
|
137
|
+
const primaryAction = content.primaryAction ?? {
|
|
138
|
+
href: "/",
|
|
139
|
+
label: siteName ? `Go to ${siteName}` : "Go to the homepage",
|
|
140
|
+
};
|
|
141
|
+
assertLink("primaryAction", primaryAction);
|
|
142
|
+
const next = content.next ?? [];
|
|
143
|
+
if (next.length > STATUS_PAGE_MAX_NEXT) {
|
|
144
|
+
throw new RangeError(`A status page lists at most ${STATUS_PAGE_MAX_NEXT} next links; pick the ones that lead to your product.`);
|
|
145
|
+
}
|
|
146
|
+
next.forEach((link) => assertLink("next", link));
|
|
147
|
+
const nextHeading = content.nextHeading ?? "Or start here";
|
|
148
|
+
assertText("nextHeading", nextHeading, 40);
|
|
149
|
+
const routes = usableRoutes(content.routes ?? []);
|
|
150
|
+
if (content.agentIndexHref !== undefined) assertArticleHref(content.agentIndexHref);
|
|
151
|
+
return {
|
|
152
|
+
kind,
|
|
153
|
+
glyph,
|
|
154
|
+
title,
|
|
155
|
+
summary,
|
|
156
|
+
primaryAction,
|
|
157
|
+
next,
|
|
158
|
+
nextHeading,
|
|
159
|
+
routes,
|
|
160
|
+
...(content.agentIndexHref === undefined ? {} : { agentIndexHref: content.agentIndexHref }),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** A path on this site: one leading slash, not followed by a slash or backslash. */
|
|
165
|
+
function isSitePath(href: string): boolean {
|
|
166
|
+
return /^\/(?![/\\])/u.test(href);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Serialized for the browser controller; the only route data in the markup. */
|
|
170
|
+
export function statusPageRoutesAttribute(routes: readonly StatusPageLink[]): string | undefined {
|
|
171
|
+
if (routes.length === 0) return undefined;
|
|
172
|
+
return JSON.stringify(routes.map(({ href, label }) => [href, label]));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Parse the attribute written by `statusPageRoutesAttribute`, ignoring anything malformed. */
|
|
176
|
+
export function parseStatusPageRoutes(value: string | null | undefined): readonly StatusPageLink[] {
|
|
177
|
+
if (!value) return [];
|
|
178
|
+
let parsed: unknown;
|
|
179
|
+
try { parsed = JSON.parse(value); } catch { return []; }
|
|
180
|
+
if (!Array.isArray(parsed)) return [];
|
|
181
|
+
const routes: StatusPageLink[] = [];
|
|
182
|
+
for (const entry of parsed.slice(0, STATUS_PAGE_MAX_ROUTES)) {
|
|
183
|
+
if (!Array.isArray(entry)) continue;
|
|
184
|
+
const [href, label] = entry as unknown[];
|
|
185
|
+
if (typeof href !== "string" || typeof label !== "string") continue;
|
|
186
|
+
if (!isSitePath(href) || label.trim() === "") continue;
|
|
187
|
+
routes.push({ href, label });
|
|
188
|
+
}
|
|
189
|
+
return routes;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const MAX_PATH = 160;
|
|
193
|
+
|
|
194
|
+
/** Lowercased pathname without query, fragment, index files, or trailing slash. */
|
|
195
|
+
export function normalizeStatusPath(value: string): string {
|
|
196
|
+
// Bound the input first; every step below is linear in its length.
|
|
197
|
+
let path = value.slice(0, MAX_PATH * 4);
|
|
198
|
+
const end = path.search(/[?#]/u);
|
|
199
|
+
if (end !== -1) path = path.slice(0, end);
|
|
200
|
+
try { path = decodeURIComponent(path); } catch { /* keep the raw path */ }
|
|
201
|
+
path = path.toLowerCase().split("/").filter((part, index) => index === 0 || part !== "").join("/");
|
|
202
|
+
path = path.replace(/(?:\/index)?\.html?$/u, "");
|
|
203
|
+
while (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1);
|
|
204
|
+
if (!path.startsWith("/")) path = `/${path}`;
|
|
205
|
+
return path.slice(0, MAX_PATH);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** The most edits a typo may carry; more than this is a different page. */
|
|
209
|
+
const MAX_EDITS = 3;
|
|
210
|
+
const BEYOND = MAX_EDITS + 1;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Optimal string alignment distance, computed only in the diagonal band the
|
|
214
|
+
* limit allows, so each comparison costs O(length × limit). Returns
|
|
215
|
+
* `limit + 1` once the distance is known to exceed the limit.
|
|
216
|
+
*/
|
|
217
|
+
function editDistance(a: string, b: string, limit: number): number {
|
|
218
|
+
if (Math.abs(a.length - b.length) > limit) return limit + 1;
|
|
219
|
+
const columns = b.length + 1;
|
|
220
|
+
let before = new Int32Array(columns).fill(BEYOND);
|
|
221
|
+
let previous = Int32Array.from({ length: columns }, (_, index) => Math.min(index, BEYOND));
|
|
222
|
+
let current = new Int32Array(columns).fill(BEYOND);
|
|
223
|
+
for (let i = 1; i <= a.length; i++) {
|
|
224
|
+
const low = Math.max(1, i - limit);
|
|
225
|
+
const high = Math.min(b.length, i + limit);
|
|
226
|
+
current[low - 1] = low === 1 ? Math.min(i, BEYOND) : BEYOND;
|
|
227
|
+
if (high < b.length) current[high + 1] = BEYOND;
|
|
228
|
+
let rowMin = BEYOND;
|
|
229
|
+
for (let j = low; j <= high; j++) {
|
|
230
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
231
|
+
let value = Math.min((previous[j] ?? BEYOND) + 1, (current[j - 1] ?? BEYOND) + 1, (previous[j - 1] ?? BEYOND) + cost);
|
|
232
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
233
|
+
value = Math.min(value, (before[j - 2] ?? BEYOND) + 1);
|
|
234
|
+
}
|
|
235
|
+
current[j] = Math.min(value, BEYOND);
|
|
236
|
+
rowMin = Math.min(rowMin, value);
|
|
237
|
+
}
|
|
238
|
+
if (rowMin > limit) return limit + 1;
|
|
239
|
+
[before, previous, current] = [previous, current, before];
|
|
240
|
+
}
|
|
241
|
+
return Math.min(previous[b.length] ?? BEYOND, limit + 1);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function words(path: string): readonly string[] {
|
|
245
|
+
return path.split(/[/\-_.]+/u).filter((word) => word.length > 0);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Edits a single word may carry and still be the same word misspelled. */
|
|
249
|
+
function wordAllowance(word: string): number {
|
|
250
|
+
if (word.length < 4) return 0;
|
|
251
|
+
return word.length < 8 ? 1 : 2;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Every word of the address is a word of the page, or that word misspelled.
|
|
256
|
+
* A different short word (v1/v2, oh/ai) or a different product name in the
|
|
257
|
+
* same slug template is a different page, not a typo.
|
|
258
|
+
*/
|
|
259
|
+
function wordsMatch(missing: readonly string[], candidate: readonly string[]): boolean {
|
|
260
|
+
return missing.every((word) => {
|
|
261
|
+
const allowance = wordAllowance(word);
|
|
262
|
+
return candidate.some((other) => other === word
|
|
263
|
+
|| (allowance > 0 && editDistance(word, other, allowance) <= allowance));
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
type Candidate = Readonly<{ path: string; words: readonly string[]; joined: string; last: string }>;
|
|
268
|
+
|
|
269
|
+
function candidate(path: string): Candidate {
|
|
270
|
+
const parts = words(path);
|
|
271
|
+
return { path, words: parts, joined: parts.join(""), last: path.slice(path.lastIndexOf("/") + 1) };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function score(missing: Candidate, page: Candidate): number | undefined {
|
|
275
|
+
if (page.path === missing.path) return 0;
|
|
276
|
+
// Same words with different separators: /gettingstarted, /getting_started.
|
|
277
|
+
if (missing.joined.length >= 6 && missing.joined === page.joined) return 0.1;
|
|
278
|
+
const longest = Math.max(missing.path.length, page.path.length);
|
|
279
|
+
const limit = Math.min(MAX_EDITS, Math.floor(longest / 4));
|
|
280
|
+
if (limit > 0 && wordsMatch(missing.words, page.words)) {
|
|
281
|
+
const distance = editDistance(missing.path, page.path, limit);
|
|
282
|
+
if (distance <= limit) return distance / longest;
|
|
283
|
+
}
|
|
284
|
+
// A page that kept its slug but moved section: /blog/x -> /writing/x.
|
|
285
|
+
if (missing.last.length >= 6 && missing.last === page.last) return 0.5;
|
|
286
|
+
// A shortened slug: every longer word of the address appears in the page.
|
|
287
|
+
const longer = missing.words.filter((word) => word.length >= 3);
|
|
288
|
+
if (longer.length >= 2 && longer.length === missing.words.length
|
|
289
|
+
&& longer.every((word) => page.words.includes(word))) return 0.6;
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The known internal page closest to a missing address, or nothing when no
|
|
295
|
+
* page is close enough to be a likely typo, move, or shortened link. The home
|
|
296
|
+
* page is never suggested; the primary action already leads there.
|
|
297
|
+
*/
|
|
298
|
+
export function suggestStatusRoute(
|
|
299
|
+
pathname: string,
|
|
300
|
+
routes: readonly StatusPageLink[],
|
|
301
|
+
): StatusPageLink | undefined {
|
|
302
|
+
const normalized = normalizeStatusPath(pathname);
|
|
303
|
+
if (normalized === "/") return undefined;
|
|
304
|
+
// Too short to tell a typo from a different page (/a, /do, /api), so a
|
|
305
|
+
// short address only matches a page that differs in case or slashes.
|
|
306
|
+
const exactOnly = normalized.length < 5;
|
|
307
|
+
const missing = candidate(normalized);
|
|
308
|
+
let best: { route: StatusPageLink; score: number; path: string } | undefined;
|
|
309
|
+
for (const route of routes.slice(0, STATUS_PAGE_MAX_ROUTES)) {
|
|
310
|
+
if (!isSitePath(route.href)) continue;
|
|
311
|
+
const path = normalizeStatusPath(route.href);
|
|
312
|
+
if (path === "/" || (exactOnly && path !== normalized)) continue;
|
|
313
|
+
const value = score(missing, candidate(path));
|
|
314
|
+
if (value === undefined) continue;
|
|
315
|
+
if (best === undefined
|
|
316
|
+
|| value < best.score
|
|
317
|
+
|| (value === best.score && (path.length < best.path.length
|
|
318
|
+
|| (path.length === best.path.length && path < best.path)))) {
|
|
319
|
+
best = { route, score: value, path };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return best?.route;
|
|
323
|
+
}
|
package/src/styles.css
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
@layer base, components;
|
|
2
|
+
@layer components.hraness-ui.legacy.base, components.hraness-ui.legacy, components.hraness-ui.priority1, components.hraness-ui.priority2, components.hraness-ui.priority3, components.hraness-ui.priority4, components.hraness-ui.priority5, components.hraness-ui.priority6, components.hraness-ui.priority7, components.hraness-design-kit.legacy, components.hraness-design-kit.priority1, components.hraness-design-kit.priority2, components.hraness-design-kit.priority3, components.hraness-design-kit.priority4, components.hraness-design-kit.priority5, components.hraness-design-kit.priority6, components.hraness-design-kit.priority7, components.hraness-design-kit.priority8;
|
|
3
|
+
|
|
4
|
+
@import "./tokens.css";
|
|
5
|
+
@import "./reset.css";
|
|
6
|
+
@import "./typography.css";
|
|
7
|
+
@import "./effects.css";
|
|
8
|
+
@import "@hraness/ui/components.css";
|
|
9
|
+
@import "@hraness/ui/stylex.css";
|
|
10
|
+
@import "./components.css";
|
|
11
|
+
@import "./appearance-menu.css";
|
|
12
|
+
@import "./charts.css";
|
|
13
|
+
@import "./plain-site.css";
|
|
14
|
+
@import "./plain-publication.css";
|
|
15
|
+
@import "./reading.css";
|
|
16
|
+
@import "./status-page.css";
|
|
17
|
+
@import "./product-marketing.css";
|
|
18
|
+
@import "./product-marketing-foundation.css";
|
|
19
|
+
@import "./design-gallery.css";
|
|
20
|
+
@import "./palette-bridge.css";
|
|
21
|
+
@import "./lantern-material.css";
|