From cb5e68498842e91c37fca4a591e137249ad4d0a5 Mon Sep 17 00:00:00 2001 From: Ryu <114303361+ryuapp@users.noreply.github.com> Date: Sun, 13 Sep 2026 03:38:57 +0900 Subject: [PATCH] chore: redesign website --- .github/workflows/ci.yaml | 14 +- ...oy-playground.yaml => deploy-website.yaml} | 11 +- playground/plugins/og-image.ts | 52 -- playground/src/404.tsx | 26 - playground/src/app.tsx | 217 -------- playground/src/components/result-card.tsx | 99 ---- playground/src/main.tsx | 18 - playground/src/prerender.tsx | 120 ----- playground/src/styles.css | 470 ------------------ {playground => website}/.gitignore | 0 {playground => website}/deno.jsonc | 3 + {playground => website}/deno.lock | 300 ++++++++++- {playground => website}/index.html | 15 +- website/plugins/og-image.ts | 54 ++ website/src/404.tsx | 24 + website/src/app.tsx | 292 +++++++++++ website/src/components/annotated-text.tsx | 57 +++ .../src/components/compat-card.tsx | 55 +- website/src/components/github-logo.tsx | 13 + .../src/components/input.tsx | 80 ++- website/src/components/result-card.tsx | 132 +++++ website/src/components/site-footer.tsx | 46 ++ .../src/components/status-detail.tsx | 19 +- website/src/main.tsx | 26 + website/src/prerender.tsx | 30 ++ website/src/styles.css | 23 + {playground => website}/src/utils/commit.ts | 0 {playground => website}/src/utils/issues.ts | 0 .../src/utils/js-semver.ts | 0 .../src/utils/js-semver_test.ts | 0 {playground => website}/src/utils/query.ts | 0 {playground => website}/src/utils/result.tsx | 0 {playground => website}/src/vite-env.d.ts | 0 {playground => website}/vite.config.ts | 2 + {playground => website}/wasm/Cargo.lock | 2 +- {playground => website}/wasm/Cargo.toml | 2 +- {playground => website}/wasm/src/lib.rs | 0 37 files changed, 1099 insertions(+), 1103 deletions(-) rename .github/workflows/{deploy-playground.yaml => deploy-website.yaml} (88%) delete mode 100644 playground/plugins/og-image.ts delete mode 100644 playground/src/404.tsx delete mode 100644 playground/src/app.tsx delete mode 100644 playground/src/components/result-card.tsx delete mode 100644 playground/src/main.tsx delete mode 100644 playground/src/prerender.tsx delete mode 100644 playground/src/styles.css rename {playground => website}/.gitignore (100%) rename {playground => website}/deno.jsonc (86%) rename {playground => website}/deno.lock (70%) rename {playground => website}/index.html (52%) create mode 100644 website/plugins/og-image.ts create mode 100644 website/src/404.tsx create mode 100644 website/src/app.tsx create mode 100644 website/src/components/annotated-text.tsx rename {playground => website}/src/components/compat-card.tsx (67%) create mode 100644 website/src/components/github-logo.tsx rename {playground => website}/src/components/input.tsx (58%) create mode 100644 website/src/components/result-card.tsx create mode 100644 website/src/components/site-footer.tsx rename {playground => website}/src/components/status-detail.tsx (71%) create mode 100644 website/src/main.tsx create mode 100644 website/src/prerender.tsx create mode 100644 website/src/styles.css rename {playground => website}/src/utils/commit.ts (100%) rename {playground => website}/src/utils/issues.ts (100%) rename {playground => website}/src/utils/js-semver.ts (100%) rename {playground => website}/src/utils/js-semver_test.ts (100%) rename {playground => website}/src/utils/query.ts (100%) rename {playground => website}/src/utils/result.tsx (100%) rename {playground => website}/src/vite-env.d.ts (100%) rename {playground => website}/vite.config.ts (89%) rename {playground => website}/wasm/Cargo.lock (99%) rename {playground => website}/wasm/Cargo.toml (89%) rename {playground => website}/wasm/src/lib.rs (100%) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a857d1c..8e8e416 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -107,7 +107,7 @@ jobs: fail_ci_if_error: true use_oidc: true - playground: + website: runs-on: ubuntu-latest steps: - name: Harden Runner @@ -140,16 +140,16 @@ jobs: - uses: jdx/mise-action@3c2e0cf82a5b2e5249f0d3635a4d83d0ae861518 # v4.2.5 - run: deno task wasm:build - working-directory: playground + working-directory: website - run: deno fmt --check - working-directory: playground + working-directory: website - run: deno lint - working-directory: playground + working-directory: website - run: deno task test - working-directory: playground + working-directory: website - run: deno check --unstable-tsgo . - working-directory: playground + working-directory: website - run: deno task build - working-directory: playground + working-directory: website env: VITE_COMMIT_HASH: ${{ github.sha }} diff --git a/.github/workflows/deploy-playground.yaml b/.github/workflows/deploy-website.yaml similarity index 88% rename from .github/workflows/deploy-playground.yaml rename to .github/workflows/deploy-website.yaml index 31942bf..5a74d1e 100644 --- a/.github/workflows/deploy-playground.yaml +++ b/.github/workflows/deploy-website.yaml @@ -1,4 +1,4 @@ -name: Deploy Playground +name: Deploy Website on: workflow_run: @@ -36,7 +36,6 @@ jobs: github.com:443 api.github.com:443 release-assets.githubusercontent.com:443 - *.ryu.app:443 index.crates.io:443 static.crates.io:443 static.rust-lang.org:443 @@ -51,16 +50,14 @@ jobs: with: target: wasm32-unknown-unknown - uses: jdx/mise-action@3c2e0cf82a5b2e5249f0d3635a4d83d0ae861518 # v4.2.5 - - name: Build playground - working-directory: playground + - name: Build website + working-directory: website run: deno task build env: VITE_COMMIT_HASH: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }} - JS_SEMVER_OG_URL: ${{ secrets.JS_SEMVER_OG_URL }} - JS_SEMVER_OG_AUTH: ${{ secrets.JS_SEMVER_OG_AUTH }} - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: - path: playground/dist + path: website/dist deploy: runs-on: ubuntu-latest diff --git a/playground/plugins/og-image.ts b/playground/plugins/og-image.ts deleted file mode 100644 index 6ac2782..0000000 --- a/playground/plugins/og-image.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { Plugin } from "vite"; - -async function writeOgImage(outputPath: string): Promise { - const imageUrl = Deno.env.get("JS_SEMVER_OG_URL"); - const authHeader = Deno.env.get("JS_SEMVER_OG_AUTH"); - - if (imageUrl === undefined || imageUrl.length === 0) { - console.error("JS_SEMVER_OG_URL is not set"); - return; - } - - const resolvedOutputPath = new URL(`../${outputPath}`, import.meta.url); - - await Deno.mkdir(new URL("./", resolvedOutputPath), { recursive: true }); - - let response: Response; - try { - response = await fetch(imageUrl, { - headers: authHeader === undefined ? undefined : { - Authorization: authHeader, - }, - }); - } catch (error) { - console.error("Failed to fetch OGP image:", error); - return; - } - - if (!response.ok) { - console.error( - `Failed to fetch OGP image: ${response.status} ${response.statusText}`, - ); - return; - } - - const contentType = response.headers.get("content-type") ?? ""; - if (!contentType.startsWith("image/")) { - console.error(`JS_SEMVER_OG_URL did not return an image: ${contentType}`); - return; - } - - const image = new Uint8Array(await response.arrayBuffer()); - await Deno.writeFile(resolvedOutputPath, image); -} - -export default function ogImage(): Plugin { - return { - name: "download-og-image", - async closeBundle() { - await writeOgImage("dist/og.png"); - }, - }; -} diff --git a/playground/src/404.tsx b/playground/src/404.tsx deleted file mode 100644 index a0c36b7..0000000 --- a/playground/src/404.tsx +++ /dev/null @@ -1,26 +0,0 @@ -const COPYRIGHT_YEAR = new Date().getFullYear(); - -export function NotFound() { - return ( -
-
-

404

-

- The page you requested could not be found. -

- -
- - -
- ); -} diff --git a/playground/src/app.tsx b/playground/src/app.tsx deleted file mode 100644 index 136afc7..0000000 --- a/playground/src/app.tsx +++ /dev/null @@ -1,217 +0,0 @@ -import { useEffect, useMemo, useState } from "preact/hooks"; - -import { CompatCard } from "./components/compat-card.tsx"; -import { Input } from "./components/input.tsx"; -import { - RangeStatusDetail, - VersionStatusDetail, -} from "./components/status-detail.tsx"; -import { getCommitHash, getCommitLink } from "./utils/commit.ts"; -import { - getInitErrorMessage, - initJsSemver, - parseRange, - parseVersion, - satisfies, -} from "./utils/js-semver.ts"; -import { - getDefaultRange, - getDefaultVersion, - readInputsFromQuery, - writeInputsToQuery, -} from "./utils/query.ts"; -import { - getInputTone, - getTrailingVisual, - type ParseResult, - type SatisfiesResult, -} from "./utils/result.tsx"; - -const COPYRIGHT_YEAR = new Date().getFullYear(); -const COMMIT_HASH_FULL = import.meta.env.VITE_COMMIT_HASH; -const COMMIT_HASH = getCommitHash(COMMIT_HASH_FULL); - -export function App() { - const [rangeInput, setRangeInput] = useState(() => - readInputsFromQuery().rangeInput - ); - const [versionInput, setVersionInput] = useState(() => - readInputsFromQuery().versionInput - ); - const [isReady, setIsReady] = useState(false); - const [initError, setInitError] = useState(null); - - useEffect(() => { - let active = true; - - void (async () => { - try { - await initJsSemver(); - if (active) { - setIsReady(true); - } - } catch (error) { - const message = getInitErrorMessage(error); - if (active) { - setInitError(message); - } - } - })(); - - return () => { - active = false; - }; - }, []); - - useEffect(() => { - const onPopState = () => { - const next = readInputsFromQuery(); - setRangeInput(next.rangeInput); - setVersionInput(next.versionInput); - }; - - addEventListener("popstate", onPopState); - - return () => { - removeEventListener("popstate", onPopState); - }; - }, []); - - const rangeResult = useMemo(() => { - if (!isReady) { - return null; - } - try { - return { canonical: parseRange(rangeInput) }; - } catch (error) { - return { error: getInitErrorMessage(error) }; - } - }, [isReady, rangeInput]); - - const versionResult = useMemo(() => { - if (!isReady) { - return null; - } - try { - return { canonical: parseVersion(versionInput) }; - } catch (error) { - return { error: getInitErrorMessage(error) }; - } - }, [isReady, versionInput]); - - const satisfiesResult = useMemo(() => { - if (!isReady) { - return null; - } - - if (rangeResult === null || versionResult === null) { - return null; - } - - if ("error" in rangeResult || "error" in versionResult) { - return { - rangeCanonical: "", - versionCanonical: "", - value: null, - }; - } - - return { - rangeCanonical: rangeResult.canonical, - versionCanonical: versionResult.canonical, - value: satisfies(rangeInput, versionInput), - }; - }, [isReady, rangeInput, rangeResult, versionInput, versionResult]); - - const handleRangeInputChange = (value: string) => { - setRangeInput(value); - writeInputsToQuery(value, versionInput); - }; - - const handleVersionInputChange = (value: string) => { - setVersionInput(value); - writeInputsToQuery(rangeInput, value); - }; - - return ( -
-
-

js-semver playground

-

- Parser and evaluator for npm's flavor of Semantic Versioning, - compliant with node-semver -

- -
- -
-
- } - /> - - - } - /> -
- - {initError && ( - - )} - -
- -
- - -
- -
- ); -} diff --git a/playground/src/components/result-card.tsx b/playground/src/components/result-card.tsx deleted file mode 100644 index 0186a2c..0000000 --- a/playground/src/components/result-card.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import type { ComponentChildren } from "preact"; -import { Check, CircleAlert, LoaderCircle } from "lucide-preact"; - -export type CardTone = "good" | "bad" | "neutral"; - -type ResultCardProps = { - title?: string; - tone: CardTone; - label?: string; - detail: ComponentChildren; - className?: string; - icon?: ComponentChildren; - pending?: boolean; -}; - -export function ResultCard( - { title, tone, label, detail, className, icon, pending = false }: - ResultCardProps, -) { - const StatusIcon = getStatusIcon(tone, pending); - const resultCardClassName = getResultCardClassName(tone, className); - const pillIconClassName = getPillIconClassName(pending); - const detailClassName = getDetailClassName(pending); - - return ( -
-
- {renderTitle(title, icon)} - {label && ( - - {StatusIcon && ( - - )} -
-

{detail}

-
- ); -} - -function getStatusIcon(tone: CardTone, pending: boolean) { - if (pending) { - return LoaderCircle; - } - - if (tone === "good") { - return Check; - } - - if (tone === "bad") { - return CircleAlert; - } - - return null; -} - -function getResultCardClassName(tone: CardTone, className?: string): string { - if (className) { - return `result-card ${tone} ${className}`; - } - - return `result-card ${tone}`; -} - -function renderTitle(title?: string, icon?: ComponentChildren) { - if (!title) { - return ; - } - - return ( -
- {icon && {icon}} -

{title}

-
- ); -} - -function getPillIconClassName(pending: boolean): string { - if (pending) { - return "pill-icon pill-icon-spin"; - } - - return "pill-icon"; -} - -function getDetailClassName(pending: boolean): string { - if (pending) { - return "detail detail-pending"; - } - - return "detail"; -} diff --git a/playground/src/main.tsx b/playground/src/main.tsx deleted file mode 100644 index 160900d..0000000 --- a/playground/src/main.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { render } from "preact"; - -import { NotFound } from "./404.tsx"; -import { App } from "./app.tsx"; -import "./styles.css"; - -const root = document.getElementById("root"); - -if (root === null) { - throw new Error("Missing #root element"); -} - -const pathname = globalThis.location.pathname; -const isPlaygroundPath = pathname === "/" || pathname === "/index.html"; - -document.title = isPlaygroundPath ? "js-semver playground" : "Not Found"; - -render(isPlaygroundPath ? : , root); diff --git a/playground/src/prerender.tsx b/playground/src/prerender.tsx deleted file mode 100644 index bd70dd9..0000000 --- a/playground/src/prerender.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { renderToString } from "preact-render-to-string"; - -function StaticApp() { - const currentYear = new Date().getFullYear(); - - return ( -
-
-

js-semver playground

-

- Parser and evaluator for npm's flavor of Semantic Versioning, - compliant with node-semver -

- -
- -
-
- - - -
- -
-
-
-

Range Parse

- Pending -
-

Canonical:

-
- -
-
-

Version Parse

- Pending -
-

Canonical:

-
-
- -
-
-

Satisfies

- Pending -
-

Result pending.

-
-
- -
- © {currentYear}{" "} - Ryu -
-
- ); -} - -function StaticNotFound() { - const currentYear = new Date().getFullYear(); - - return ( -
-
-

404

-

- The page you requested could not be found. -

- -
- -
- © {currentYear}{" "} - Ryu -
-
- ); -} - -export function prerender(data: { url: string }) { - const isNotFoundPage = data.url === "/404.html"; - - return { - html: isNotFoundPage - ? renderToString() - : renderToString(), - head: { - title: isNotFoundPage ? "Not Found" : "js-semver playground", - elements: isNotFoundPage - ? new Set([ - , - ]) - : new Set(), - }, - }; -} diff --git a/playground/src/styles.css b/playground/src/styles.css deleted file mode 100644 index 271715a..0000000 --- a/playground/src/styles.css +++ /dev/null @@ -1,470 +0,0 @@ -:root { - --color-accent-soft: oklch(45% 0.11 60 / 8%); - --color-accent-wash: oklch(45% 0.11 60 / 12%); - --color-background: oklch(95.8% 0.015 83); - --color-border: oklch(58% 0.01 248); - --color-border-soft: oklch(24% 0.027 248 / 8%); - --color-danger: oklch(50% 0.15 25 / 22%); - --color-danger-ring: oklch(50% 0.15 25 / 10%); - --color-danger-soft: oklch(96.6% 0.024 23 / 90%); - --color-danger-weak: oklch(50% 0.15 25); - --color-detail: oklch(24% 0.027 248 / 78%); - --color-field: oklch(100% 0 0); - --color-field-hover: oklch(24% 0.027 248); - --color-footer: oklch(24% 0.027 248 / 62%); - --color-grid: oklch(24% 0.027 248 / 6%); - --color-label: oklch(24% 0.027 248 / 90%); - --color-neutral: oklch(24% 0.027 248 / 10%); - --color-neutral-soft: oklch(97.2% 0.005 248 / 90%); - --color-panel: oklch(95% 0.015 83 / 94%); - --color-panel-decoration: oklch(98% 0.018 83 / 52%); - --color-placeholder: oklch(24% 0.027 248 / 38%); - --color-ring: oklch(24% 0.027 248 / 6%); - --color-success: oklch(76.44% 0.166 149.48 / 22%); - --color-success-ring: oklch(76.44% 0.166 149.48 / 10%); - --color-success-soft: oklch(96.8% 0.05 149.48 / 88%); - --color-success-weak: oklch(76.44% 0.166 149.48); - --color-surface: oklch(100% 0 0 / 76%); - --color-text: oklch(24% 0.027 248); - --color-white: oklch(100% 0 0); - --font-size-body: 1rem; - --font-size-copy: 1.05rem; - --font-size-footer: 0.88rem; - --font-size-label: 0.78rem; - --font-size-link: 0.95rem; - --font-size-pill: 0.8rem; - --font-size-title: clamp(3.5rem, 9vw, 5.6rem); - - color-scheme: light; - font-family: "IBM Plex Sans", "Segoe UI", sans-serif; - background: var(--color-background); - color: var(--color-text); -} - -* { - box-sizing: border-box; -} - -body { - margin: 0; - min-height: 100svh; - background: - radial-gradient( - circle at top left, - var(--color-accent-soft), - transparent 32% - ), - radial-gradient( - circle at top right, - var(--color-border-soft), - transparent 28% - ), - var(--color-background); -} - -body::before { - position: fixed; - inset: 0; - background-image: - linear-gradient(var(--color-grid) 1px, transparent 1px), - linear-gradient(90deg, var(--color-grid) 1px, transparent 1px); - background-size: 32px 32px; - content: ""; - pointer-events: none; -} - -.page-shell { - position: relative; - margin: 0 auto; - max-width: 960px; - padding: 56px 20px 72px; -} - -.page-shell::before, -.page-shell::after { - position: absolute; - z-index: -1; - border: 1px solid var(--color-border); - content: ""; - pointer-events: none; -} - -.page-shell::before { - top: 20px; - right: 20px; - width: 144px; - height: 144px; - background: var(--color-panel-decoration); -} - -.page-shell::after { - top: 146px; - right: 86px; - width: 64px; - height: 64px; - background: var(--color-accent-wash); -} - -.hero { - margin-bottom: 0; -} - -.hero h1 { - margin: 0; - max-width: 12ch; - font-family: Georgia, "Times New Roman", serif; - font-size: var(--font-size-title); - line-height: 0.94; - letter-spacing: -0.04em; -} - -.hero-copy { - margin: 18px 0 0; - max-width: 52ch; - font-size: var(--font-size-copy); - line-height: 1.7; -} - -.hero-link-row { - margin: 6px 0 0; -} - -.hero-link { - color: inherit; - font-size: var(--font-size-link); - font-weight: 600; - text-decoration-thickness: 1px; - text-underline-offset: 0.18em; -} - -.page-footer { - margin-top: 20px; - color: var(--color-footer); - font-size: var(--font-size-footer); -} - -.page-footer-link { - color: inherit; - text-decoration-thickness: 1px; - text-underline-offset: 0.18em; -} - -.inline-link { - color: inherit; - text-decoration-thickness: 1px; - text-underline-offset: 0.18em; -} - -.panel { - margin-top: 20px; - background: var(--color-panel); - border: 1px solid oklch(24% 0.027 248 / 0.1); - border-radius: 4px; - padding: 24px; -} - -.grid { - display: grid; - column-gap: 16px; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); -} - -.result-grid { - display: grid; - gap: 16px; - grid-template-columns: repeat(2, minmax(0, 1fr)); - margin-top: 16px; -} - -.result-stack { - margin-top: 16px; -} - -.panel-meta { - display: flex; - justify-content: flex-end; - margin-top: 16px; -} - -.panel-meta-link { - color: var(--color-footer); - font-size: var(--font-size-footer); - text-decoration-thickness: 1px; - text-underline-offset: 0.18em; -} - -.field { - display: grid; - gap: 8px; - position: relative; -} - -.field-input-wrap { - position: relative; -} - -.field-label { - color: var(--color-label); - font-size: var(--font-size-label); - font-weight: 700; - letter-spacing: 0.08em; - text-transform: uppercase; -} - -.field-detail { - color: var(--color-detail); - min-height: 22px; -} - -.field-detail .status-line.good { - color: var(--color-detail); -} - -.field-control { - width: 100%; - border: 1px solid var(--color-border); - border-radius: 6px; - background: var(--color-field); - color: inherit; - font: inherit; - font-size: var(--font-size-body); - line-height: 1.5; - padding: 11px 12px; - transition: - background-color 180ms ease, - border-color 180ms ease, - transform 180ms ease, - box-shadow 180ms ease; -} - -.field-trailing-visual { - position: absolute; - top: 50%; - right: 12px; - display: inline-flex; - align-items: center; - justify-content: center; - color: oklch(64% 0.18 149.48); - transform: translateY(-50%); - pointer-events: none; -} - -.field[data-tone="bad"] .field-trailing-visual { - color: oklch(44% 0.16 25); -} - -.field-control::placeholder { - color: var(--color-placeholder); -} - -.field:hover .field-control { - border-color: var(--color-field-hover); -} - -.field-control:focus { - outline: none; - background: var(--color-white); - box-shadow: 0 0 0 4px var(--color-ring); -} - -.field[data-focused] .field-control { - border-color: oklch(34% 0.022 248); -} - -.field[data-tone="bad"] .field-control { - border-color: var(--color-danger-weak); -} - -.field[data-tone="bad"][data-focused] .field-control { - box-shadow: 0 0 0 4px var(--color-danger-ring); -} - -.banner { - margin-top: 16px; -} - -.banner, -.result-card { - padding: 12px 14px; -} - -.banner { - border: 1px solid var(--color-border-soft); - border-radius: 8px; - min-height: 58px; - transition: - border-color 180ms ease, - background-color 180ms ease, - color 180ms ease; -} - -.result-card { - display: flex; - flex-direction: column; - justify-content: space-between; - border: 1px solid var(--color-border-soft); - border-radius: 4px; - background: var(--color-surface); - min-height: 72px; -} - -.result-card-plain { - border-color: var(--color-border-soft); - background: var(--color-panel); -} - -.result-card header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - flex-wrap: wrap; -} - -.result-card-title { - display: inline-flex; - align-items: center; - gap: 8px; - margin-bottom: 4px; -} - -.result-card-icon, -.pill-icon { - display: inline-flex; - align-items: center; - justify-content: center; -} - -.result-card h2 { - margin: 0; - font-size: var(--font-size-body); - letter-spacing: 0.01em; -} - -.detail { - margin: 0; - color: var(--color-detail); - line-height: 1.55; - overflow-wrap: anywhere; -} - -.status-list { - display: grid; - gap: 4px; -} - -.status-line { - display: flex; - align-items: center; - gap: 8px; - line-height: 1.45; -} - -.status-line.good { - color: oklch(76.44% 0.166 149.48); -} - -.status-line.bad { - color: oklch(53% 0.12 25); -} - -.status-line.neutral { - color: var(--color-detail); -} - -.status-line-spacer { - visibility: hidden; -} - -.status-line-label { - min-width: 72px; - color: var(--color-text); - font-weight: 400; -} - -.pill { - display: inline-flex; - align-items: center; - gap: 6px; - border-radius: 4px; - border: 1px solid transparent; - font-size: var(--font-size-pill); - font-weight: 700; - letter-spacing: 0.03em; - padding: 6px 10px; -} - -.pill.good { - border-color: transparent; - background: transparent; - color: oklch(76.44% 0.166 149.48); - padding-right: 0; -} - -.result-card:not(.result-card-plain) .pill.good { - color: var(--color-text); -} - -.result-card:not(.result-card-plain) .pill.good .pill-icon { - color: var(--color-text); -} - -.pill-icon-spin { - animation: pill-spin 1s linear infinite; -} - -.banner.good, -.banner.bad, -.banner.neutral, -.result-card.good, -.result-card.bad, -.result-card.neutral { - background: var(--color-surface); -} - -.result-card:not(.result-card-plain).good { - border-color: oklch(52% 0.13 150 / 22%); - background: oklch(96.2% 0.038 150 / 88%); -} - -.result-card:not(.result-card-plain).bad { - border-color: var(--color-danger); - background: var(--color-danger-soft); -} - -.result-card:not(.result-card-plain).neutral { - border-color: var(--color-neutral); - background: var(--color-neutral-soft); -} - -@keyframes pill-spin { - to { - transform: rotate(360deg); - } -} - -@media (max-width: 640px) { - .page-shell { - padding: 32px 16px 48px; - } - - .page-shell::before { - top: 12px; - right: 12px; - width: 104px; - height: 104px; - } - - .page-shell::after { - top: 94px; - right: 56px; - } - - .panel { - padding: 18px; - } - - .result-grid { - grid-template-columns: 1fr; - } -} diff --git a/playground/.gitignore b/website/.gitignore similarity index 100% rename from playground/.gitignore rename to website/.gitignore diff --git a/playground/deno.jsonc b/website/deno.jsonc similarity index 86% rename from playground/deno.jsonc rename to website/deno.jsonc index 111cec5..c799eec 100644 --- a/playground/deno.jsonc +++ b/website/deno.jsonc @@ -17,11 +17,14 @@ }, "imports": { "@preact/preset-vite": "npm:@preact/preset-vite@2.10.6", // dev + "@tailwindcss/vite": "npm:@tailwindcss/vite@4.3.3", // dev "@std/assert": "jsr:@std/assert@1.0.19", "lucide-preact": "npm:lucide-preact@1.7.0", "preact": "npm:preact@10.29.8", "preact-render-to-string": "npm:preact-render-to-string@6.7.0", "semver": "npm:semver@7.8.5", + "takumi-js": "npm:takumi-js@2.13.7", // dev + "tailwindcss": "npm:tailwindcss@4.3.3", // dev "vite": "npm:vite@8.2.2", // dev "vite-prerender-plugin": "npm:vite-prerender-plugin@0.5.13" // dev }, diff --git a/playground/deno.lock b/website/deno.lock similarity index 70% rename from playground/deno.lock rename to website/deno.lock index 216aa30..9ffb566 100644 --- a/playground/deno.lock +++ b/website/deno.lock @@ -4,10 +4,13 @@ "jsr:@std/assert@1.0.19": "1.0.19", "jsr:@std/internal@^1.0.12": "1.0.12", "npm:@preact/preset-vite@2.10.6": "2.10.6_@babel+core@7.29.7_vite@8.2.2_preact@10.29.8_preact-render-to-string@6.7.0", + "npm:@tailwindcss/vite@4.3.3": "4.3.3_vite@8.2.2", "npm:lucide-preact@1.7.0": "1.7.0_preact@10.29.8_preact-render-to-string@6.7.0", "npm:preact-render-to-string@6.7.0": "6.7.0_preact@10.29.8", "npm:preact@10.29.8": "10.29.8_preact-render-to-string@6.7.0", "npm:semver@7.8.5": "7.8.5", + "npm:tailwindcss@4.3.3": "4.3.3", + "npm:takumi-js@2.13.7": "2.13.7_preact@10.29.8", "npm:vite-prerender-plugin@0.5.13": "0.5.13_vite@8.2.2", "npm:vite@8.2.2": "8.2.2" }, @@ -341,6 +344,174 @@ "picomatch@4.0.7" ] }, + "@tailwindcss/node@4.3.3": { + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dependencies": [ + "@jridgewell/remapping", + "enhanced-resolve", + "jiti", + "lightningcss@1.32.0", + "magic-string", + "source-map-js", + "tailwindcss" + ] + }, + "@tailwindcss/oxide-android-arm64@4.3.3": { + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "os": ["android"], + "cpu": ["arm64"] + }, + "@tailwindcss/oxide-darwin-arm64@4.3.3": { + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@tailwindcss/oxide-darwin-x64@4.3.3": { + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@tailwindcss/oxide-freebsd-x64@4.3.3": { + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3": { + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@tailwindcss/oxide-linux-arm64-gnu@4.3.3": { + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@tailwindcss/oxide-linux-arm64-musl@4.3.3": { + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@tailwindcss/oxide-linux-x64-gnu@4.3.3": { + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@tailwindcss/oxide-linux-x64-musl@4.3.3": { + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@tailwindcss/oxide-wasm32-wasi@4.3.3": { + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "cpu": ["wasm32"] + }, + "@tailwindcss/oxide-win32-arm64-msvc@4.3.3": { + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@tailwindcss/oxide-win32-x64-msvc@4.3.3": { + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@tailwindcss/oxide@4.3.3": { + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "optionalDependencies": [ + "@tailwindcss/oxide-android-arm64", + "@tailwindcss/oxide-darwin-arm64", + "@tailwindcss/oxide-darwin-x64", + "@tailwindcss/oxide-freebsd-x64", + "@tailwindcss/oxide-linux-arm-gnueabihf", + "@tailwindcss/oxide-linux-arm64-gnu", + "@tailwindcss/oxide-linux-arm64-musl", + "@tailwindcss/oxide-linux-x64-gnu", + "@tailwindcss/oxide-linux-x64-musl", + "@tailwindcss/oxide-wasm32-wasi", + "@tailwindcss/oxide-win32-arm64-msvc", + "@tailwindcss/oxide-win32-x64-msvc" + ] + }, + "@tailwindcss/vite@4.3.3_vite@8.2.2": { + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "dependencies": [ + "@tailwindcss/node", + "@tailwindcss/oxide", + "tailwindcss", + "vite" + ] + }, + "@takumi-rs/core-darwin-arm64@2.13.7": { + "integrity": "sha512-Jbe1ik4bcVOOHDpa5RCjm5fe+LfJjjyebvvPBMekhEKXg6hlTbIEFmhr/GCxMOKUfaNsvb4amZ/bVCo0NDSCjA==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@takumi-rs/core-darwin-x64@2.13.7": { + "integrity": "sha512-PvmSmhjbSosM3XaHZ9VhFm1x7Xc9oZ5GqXlskiwZblKIi5S4EI43buhUd28xzcBlKkB/vC3c15Cj417XjnUk4g==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@takumi-rs/core-linux-arm64-gnu@2.13.7": { + "integrity": "sha512-TGvDidIF3dtb7TMoJRF+0BET/v6eAuQxV8AXo/57GqO3TM9wjLt3AbkrFkzxSsaIRXlBQxCfb+GsS0YdGnpo4w==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@takumi-rs/core-linux-arm64-musl@2.13.7": { + "integrity": "sha512-CL92+JeFbqlPfWI2SSQqLtcjWpxEsPYQ+wNhaqUWLnlTMfZfj+Fl3ERFLbmK+lYnGs7bzpPXLisd3GJQ37rTYw==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@takumi-rs/core-linux-x64-gnu@2.13.7": { + "integrity": "sha512-ZxoapyUDlInik93WIA9+Sk/ByOb3QGO7GaL9PA7OOdIlK5ovK9v9q88tZb1cvfxtIVzdE9HPBUhb5quNMcuXWg==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@takumi-rs/core-linux-x64-musl@2.13.7": { + "integrity": "sha512-OqoVTd7mrlIwjKYYFizUwz+3zpnVsbrpLFPXFC+BmUxdFYNZwKa0R+Pk56EslyBZS3CuBy+RnBusHCvNC7+09g==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@takumi-rs/core-win32-arm64-msvc@2.13.7": { + "integrity": "sha512-evSyi/y3uV03U+PJfLeG2V3aRsmmWJ6elrFpkLtHebWwi49kXazTL5TYsCkPj5hA4i+rNs6z/t/BygLj1Krp8Q==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@takumi-rs/core-win32-x64-msvc@2.13.7": { + "integrity": "sha512-GawtljFxuJemQynlF5wryxtSLv18XNuKSjMt52xf66WMaBKt4eN6/b7yIZp9QqKPof8ip8uqFOt7fWVSUN+gHQ==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@takumi-rs/core@2.13.7_preact@10.29.8": { + "integrity": "sha512-FBkglt8kGBZwT2CHr+kppN+FH72Oy8gLIZz1BIbD9vNcocLrvtLsh1Y0nB18mOPmaROKGUVUXLPnIuWo49CRng==", + "dependencies": [ + "@takumi-rs/helpers" + ], + "optionalDependencies": [ + "@takumi-rs/core-darwin-arm64", + "@takumi-rs/core-darwin-x64", + "@takumi-rs/core-linux-arm64-gnu", + "@takumi-rs/core-linux-arm64-musl", + "@takumi-rs/core-linux-x64-gnu", + "@takumi-rs/core-linux-x64-musl", + "@takumi-rs/core-win32-arm64-msvc", + "@takumi-rs/core-win32-x64-msvc" + ] + }, + "@takumi-rs/helpers@2.13.7_preact@10.29.8": { + "integrity": "sha512-6AKk/ixuvoCEZD9/LPAz3kxTcwWHeZh15quhHIHYiVxFRl4x2LyKN8j6eZngiepYYsORXAuJ5YlTGA7uIRfeMQ==", + "dependencies": [ + "preact" + ], + "optionalPeers": [ + "preact" + ] + }, + "@takumi-rs/wasm@2.13.7_preact@10.29.8": { + "integrity": "sha512-tc2IFETJ9HtZpS94cL3qSTiecJhhR4RUTXbb3E8cyz6WKdyoZSFkL2r67xnidVtcv22RKTyfVJ2gxV7eWvF+mA==", + "dependencies": [ + "@takumi-rs/helpers" + ] + }, "@types/estree@1.0.9": { "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==" }, @@ -424,6 +595,13 @@ "electron-to-chromium@1.5.414": { "integrity": "sha512-aYlviXiaXBbzvKgyALpcMmqa3Np3sDr0XnZbEG62n2UpZFbEcjQ4EEMOLGzVPhwVnwTz0lvKY+GcARbunuHekw==" }, + "enhanced-resolve@5.24.5": { + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "dependencies": [ + "graceful-fs", + "tapable" + ] + }, "entities@4.5.0": { "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" }, @@ -450,10 +628,17 @@ "gensync@1.0.0-beta.2": { "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" }, + "graceful-fs@4.2.11": { + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, "he@1.2.0": { "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "bin": true }, + "jiti@2.7.0": { + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "bin": true + }, "js-tokens@4.0.0": { "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, @@ -468,78 +653,152 @@ "kolorist@1.8.0": { "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==" }, + "lightningcss-android-arm64@1.32.0": { + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "os": ["android"], + "cpu": ["arm64"] + }, "lightningcss-android-arm64@1.33.0": { "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", "os": ["android"], "cpu": ["arm64"] }, + "lightningcss-darwin-arm64@1.32.0": { + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "os": ["darwin"], + "cpu": ["arm64"] + }, "lightningcss-darwin-arm64@1.33.0": { "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", "os": ["darwin"], "cpu": ["arm64"] }, + "lightningcss-darwin-x64@1.32.0": { + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "os": ["darwin"], + "cpu": ["x64"] + }, "lightningcss-darwin-x64@1.33.0": { "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", "os": ["darwin"], "cpu": ["x64"] }, + "lightningcss-freebsd-x64@1.32.0": { + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "os": ["freebsd"], + "cpu": ["x64"] + }, "lightningcss-freebsd-x64@1.33.0": { "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", "os": ["freebsd"], "cpu": ["x64"] }, + "lightningcss-linux-arm-gnueabihf@1.32.0": { + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "os": ["linux"], + "cpu": ["arm"] + }, "lightningcss-linux-arm-gnueabihf@1.33.0": { "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", "os": ["linux"], "cpu": ["arm"] }, + "lightningcss-linux-arm64-gnu@1.32.0": { + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "os": ["linux"], + "cpu": ["arm64"] + }, "lightningcss-linux-arm64-gnu@1.33.0": { "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", "os": ["linux"], "cpu": ["arm64"] }, + "lightningcss-linux-arm64-musl@1.32.0": { + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "os": ["linux"], + "cpu": ["arm64"] + }, "lightningcss-linux-arm64-musl@1.33.0": { "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", "os": ["linux"], "cpu": ["arm64"] }, + "lightningcss-linux-x64-gnu@1.32.0": { + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "os": ["linux"], + "cpu": ["x64"] + }, "lightningcss-linux-x64-gnu@1.33.0": { "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", "os": ["linux"], "cpu": ["x64"] }, + "lightningcss-linux-x64-musl@1.32.0": { + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "os": ["linux"], + "cpu": ["x64"] + }, "lightningcss-linux-x64-musl@1.33.0": { "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", "os": ["linux"], "cpu": ["x64"] }, + "lightningcss-win32-arm64-msvc@1.32.0": { + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "os": ["win32"], + "cpu": ["arm64"] + }, "lightningcss-win32-arm64-msvc@1.33.0": { "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", "os": ["win32"], "cpu": ["arm64"] }, + "lightningcss-win32-x64-msvc@1.32.0": { + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "os": ["win32"], + "cpu": ["x64"] + }, "lightningcss-win32-x64-msvc@1.33.0": { "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", "os": ["win32"], "cpu": ["x64"] }, + "lightningcss@1.32.0": { + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dependencies": [ + "detect-libc" + ], + "optionalDependencies": [ + "lightningcss-android-arm64@1.32.0", + "lightningcss-darwin-arm64@1.32.0", + "lightningcss-darwin-x64@1.32.0", + "lightningcss-freebsd-x64@1.32.0", + "lightningcss-linux-arm-gnueabihf@1.32.0", + "lightningcss-linux-arm64-gnu@1.32.0", + "lightningcss-linux-arm64-musl@1.32.0", + "lightningcss-linux-x64-gnu@1.32.0", + "lightningcss-linux-x64-musl@1.32.0", + "lightningcss-win32-arm64-msvc@1.32.0", + "lightningcss-win32-x64-msvc@1.32.0" + ] + }, "lightningcss@1.33.0": { "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "dependencies": [ "detect-libc" ], "optionalDependencies": [ - "lightningcss-android-arm64", - "lightningcss-darwin-arm64", - "lightningcss-darwin-x64", - "lightningcss-freebsd-x64", - "lightningcss-linux-arm-gnueabihf", - "lightningcss-linux-arm64-gnu", - "lightningcss-linux-arm64-musl", - "lightningcss-linux-x64-gnu", - "lightningcss-linux-x64-musl", - "lightningcss-win32-arm64-msvc", - "lightningcss-win32-x64-msvc" + "lightningcss-android-arm64@1.33.0", + "lightningcss-darwin-arm64@1.33.0", + "lightningcss-darwin-x64@1.33.0", + "lightningcss-freebsd-x64@1.33.0", + "lightningcss-linux-arm-gnueabihf@1.33.0", + "lightningcss-linux-arm64-gnu@1.33.0", + "lightningcss-linux-arm64-musl@1.33.0", + "lightningcss-linux-x64-gnu@1.33.0", + "lightningcss-linux-x64-musl@1.33.0", + "lightningcss-win32-arm64-msvc@1.33.0", + "lightningcss-win32-x64-msvc@1.33.0" ] }, "lru-cache@5.1.1": { @@ -663,6 +922,20 @@ "stack-trace@1.0.0-pre2": { "integrity": "sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==" }, + "tailwindcss@4.3.3": { + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==" + }, + "takumi-js@2.13.7_preact@10.29.8": { + "integrity": "sha512-6CuATUv1O6X/aD3T0OQIkv0nyesefJibamskfQxcQqKVl2CCk6kIX9E0e+glMTILTJC2I1IcrcFEPlmDlcexgQ==", + "dependencies": [ + "@takumi-rs/core", + "@takumi-rs/helpers", + "@takumi-rs/wasm" + ] + }, + "tapable@2.3.3": { + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==" + }, "tinyglobby@0.2.17": { "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dependencies": [ @@ -694,7 +967,7 @@ "vite@8.2.2": { "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", "dependencies": [ - "lightningcss", + "lightningcss@1.33.0", "picomatch@4.0.7", "postcss", "rolldown", @@ -716,10 +989,13 @@ "dependencies": [ "jsr:@std/assert@1.0.19", "npm:@preact/preset-vite@2.10.6", + "npm:@tailwindcss/vite@4.3.3", "npm:lucide-preact@1.7.0", "npm:preact-render-to-string@6.7.0", "npm:preact@10.29.8", "npm:semver@7.8.5", + "npm:tailwindcss@4.3.3", + "npm:takumi-js@2.13.7", "npm:vite-prerender-plugin@0.5.13", "npm:vite@8.2.2" ] diff --git a/playground/index.html b/website/index.html similarity index 52% rename from playground/index.html rename to website/index.html index 7cb4baf..1e46263 100644 --- a/playground/index.html +++ b/website/index.html @@ -2,23 +2,23 @@ - js-semver playground + js-semver - semver crate compliant with node-semver - - + @@ -26,10 +26,11 @@ - + diff --git a/website/plugins/og-image.ts b/website/plugins/og-image.ts new file mode 100644 index 0000000..c8acfaa --- /dev/null +++ b/website/plugins/og-image.ts @@ -0,0 +1,54 @@ +import { render } from "takumi-js"; +import type { Plugin } from "vite"; + +async function renderOgImage(): Promise { + return await render( + ` +
+
js-semver
+
+
Parser and evaluator for npm's flavor of Semantic Versioning, compliant with node-semver.
+
It maintains high compatibility and performance, and has zero dependencies by default.
+
+
+ `, + { width: 1200, height: 630 }, + ); +} + +async function writeOgImage(outputPath: string): Promise { + const resolvedOutputPath = new URL(`../${outputPath}`, import.meta.url); + + await Deno.mkdir(new URL("./", resolvedOutputPath), { recursive: true }); + const image = await renderOgImage(); + await Deno.writeFile(resolvedOutputPath, image); +} + +export default function ogImage(): Plugin { + return { + name: "render-og-image", + configureServer(server) { + server.middlewares.use(async (request, response, next) => { + const url = new URL(request.url ?? "/", "http://localhost"); + if (url.pathname !== "/og.png") { + next(); + return; + } + + try { + const image = await renderOgImage(); + response.statusCode = 200; + response.setHeader("Content-Type", "image/png"); + response.setHeader("Content-Length", image.byteLength); + response.setHeader("Cache-Control", "no-store"); + response.end(image); + } catch (error) { + next(error); + } + }); + }, + async closeBundle() { + await writeOgImage("dist/og.png"); + }, + }; +} diff --git a/website/src/404.tsx b/website/src/404.tsx new file mode 100644 index 0000000..9651c28 --- /dev/null +++ b/website/src/404.tsx @@ -0,0 +1,24 @@ +import { SiteFooter } from "./components/site-footer.tsx"; + +export function NotFound() { + return ( +
+
+

404

+

+ The page you requested could not be found. +

+

+ + Back to playground + +

+
+ + +
+ ); +} diff --git a/website/src/app.tsx b/website/src/app.tsx new file mode 100644 index 0000000..c0708f4 --- /dev/null +++ b/website/src/app.tsx @@ -0,0 +1,292 @@ +import { useEffect, useMemo, useRef, useState } from "preact/hooks"; +import { ArrowDown } from "lucide-preact"; + +import { AnnotatedText } from "./components/annotated-text.tsx"; +import { CompatCard } from "./components/compat-card.tsx"; +import { GitHubLogo } from "./components/github-logo.tsx"; +import { Input } from "./components/input.tsx"; +import { SiteFooter } from "./components/site-footer.tsx"; +import { + RangeStatusDetail, + VersionStatusDetail, +} from "./components/status-detail.tsx"; +import { getCommitHash, getCommitLink } from "./utils/commit.ts"; +import { + getInitErrorMessage, + initJsSemver, + parseRange, + parseVersion, + satisfies, +} from "./utils/js-semver.ts"; +import { + getDefaultRange, + getDefaultVersion, + readInputsFromQuery, + writeInputsToQuery, +} from "./utils/query.ts"; +import { + getInputTone, + getTrailingVisual, + type ParseResult, + type SatisfiesResult, +} from "./utils/result.tsx"; + +const COMMIT_HASH_FULL = import.meta.env.VITE_COMMIT_HASH; +const COMMIT_HASH = getCommitHash(COMMIT_HASH_FULL); +const EXAMPLES = [ + { label: "Caret", range: "^1.2.3", version: "1.5.0" }, + { label: "Tilde", range: "~1.2.3", version: "1.3.0" }, + { label: "Prerelease", range: "^1.0.0-rc.2", version: "1.0.0-rc.3" }, + { label: "Union", range: "^1.0.0 || ^3.0.0", version: "3.2.1" }, +]; + +type AppProps = { + initialRangeInput?: string; + initialVersionInput?: string; +}; + +export function App({ initialRangeInput, initialVersionInput }: AppProps = {}) { + const [rangeInput, setRangeInput] = useState(() => + initialRangeInput ?? readInputsFromQuery().rangeInput + ); + const [versionInput, setVersionInput] = useState(() => + initialVersionInput ?? readInputsFromQuery().versionInput + ); + const [isCompatLoading, setIsCompatLoading] = useState(false); + const [isReady, setIsReady] = useState(false); + const [initError, setInitError] = useState(null); + const compatTimer = useRef | null>(null); + + const startCompatLoading = () => { + if (compatTimer.current !== null) { + clearTimeout(compatTimer.current); + } + + setIsCompatLoading(true); + compatTimer.current = setTimeout(() => { + setIsCompatLoading(false); + compatTimer.current = null; + }, 400); + }; + + useEffect(() => { + let active = true; + + void (async () => { + try { + await initJsSemver(); + if (active) { + setIsReady(true); + } + } catch (error) { + const message = getInitErrorMessage(error); + if (active) { + setInitError(message); + } + } + })(); + + return () => { + active = false; + }; + }, []); + + useEffect(() => { + const onPopState = () => { + const next = readInputsFromQuery(); + startCompatLoading(); + setRangeInput(next.rangeInput); + setVersionInput(next.versionInput); + }; + + addEventListener("popstate", onPopState); + + return () => { + removeEventListener("popstate", onPopState); + if (compatTimer.current !== null) { + clearTimeout(compatTimer.current); + } + }; + }, []); + + const rangeResult = useMemo(() => { + if (!isReady) { + return null; + } + try { + return { canonical: parseRange(rangeInput) }; + } catch (error) { + return { error: getInitErrorMessage(error) }; + } + }, [isReady, rangeInput]); + + const versionResult = useMemo(() => { + if (!isReady) { + return null; + } + try { + return { canonical: parseVersion(versionInput) }; + } catch (error) { + return { error: getInitErrorMessage(error) }; + } + }, [isReady, versionInput]); + + const satisfiesResult = useMemo(() => { + if (!isReady) { + return null; + } + + if (rangeResult === null || versionResult === null) { + return null; + } + + if ("error" in rangeResult || "error" in versionResult) { + return { + rangeCanonical: "", + versionCanonical: "", + value: null, + }; + } + + return { + rangeCanonical: rangeResult.canonical, + versionCanonical: versionResult.canonical, + value: satisfies(rangeInput, versionInput), + }; + }, [isReady, rangeInput, rangeResult, versionInput, versionResult]); + + const handleRangeInputChange = (value: string) => { + startCompatLoading(); + setRangeInput(value); + writeInputsToQuery(value, versionInput); + }; + + const handleVersionInputChange = (value: string) => { + startCompatLoading(); + setVersionInput(value); + writeInputsToQuery(rangeInput, value); + }; + + return ( +
+
+

js-semver

+ + + GitHub + +
+
+

+ + Parser and evaluator for npm's flavor of Semantic Versioning, + compliant with node-semver. + + + This crate is designed for the JavaScript ecosystem and follows{" "} + + node-semver + {" "} + (the one npm uses) parsing and range semantics. It maintains high + compatibility and performance, and has zero dependencies by default. + +

+
+ +
+

Playground

+ + {COMMIT_HASH} + +
+
+ TRY IT + {EXAMPLES.map((example) => ( + + ))} +
+
+ {initError && ( +
+ WASM init failed: {initError} +
+ )} +
+
+ } + /> + + + + } + /> +
+
+ +
+
+
+ +
+ ); +} diff --git a/website/src/components/annotated-text.tsx b/website/src/components/annotated-text.tsx new file mode 100644 index 0000000..1b6e31d --- /dev/null +++ b/website/src/components/annotated-text.tsx @@ -0,0 +1,57 @@ +import type { ComponentChildren } from "preact"; + +type AnnotatedTextProps = { + children: ComponentChildren; +}; + +export function AnnotatedText({ children }: AnnotatedTextProps) { + return ( + <> + + + {children} + + + + ); +} diff --git a/playground/src/components/compat-card.tsx b/website/src/components/compat-card.tsx similarity index 67% rename from playground/src/components/compat-card.tsx rename to website/src/components/compat-card.tsx index 6707005..5e05ded 100644 --- a/playground/src/components/compat-card.tsx +++ b/website/src/components/compat-card.tsx @@ -16,6 +16,7 @@ type CompatCardProps = { rangeResult: ParseResult | null; versionResult: ParseResult | null; satisfiesResult: SatisfiesResult | null; + loading?: boolean; commitHash?: string; }; @@ -26,12 +27,44 @@ export function CompatCard( rangeResult, versionResult, satisfiesResult, + loading = false, commitHash, }: CompatCardProps, ) { - if ( + const hasResults = !( rangeResult === null || versionResult === null || satisfiesResult === null - ) { + ); + let allMatch: boolean | null = null; + + if (hasResults) { + const nodeRangeOk = semver.validRange(rangeInput) !== null; + const nodeVersionOk = semver.valid(versionInput) !== null; + const nodeSatisfies = getNodeSatisfies( + nodeRangeOk, + nodeVersionOk, + versionInput, + rangeInput, + ); + + allMatch = isParseOk(rangeResult) === nodeRangeOk && + isParseOk(versionResult) === nodeVersionOk && + satisfiesResult.value === nodeSatisfies; + } + + if (loading) { + return ( +