From 6ce485f6d3f6ec9e1b61c82577b618e80df4e92b Mon Sep 17 00:00:00 2001 From: Hellings Date: Fri, 23 Oct 2026 09:04:18 -0400 Subject: [PATCH] Repo automation and scrolltrans/viewtrans APIS --- README.md | 4 +- docs/install.md | 29 + integrations/shadcn.ts | 62 +- modules/amezing/components/audio-button.spug | 2 +- modules/amezing/components/comments.spug | 2 +- modules/amezing/components/deanchor.spug | 2 +- modules/amezing/components/footnotes.spug | 2 +- modules/amezing/components/fuse.spug | 2 +- modules/amezing/components/search-fuse.spug | 2 +- .../amezing/components/skip-to-content.spug | 2 +- modules/shadcn/index.ts | 2 +- modules/shadcn/src/integration.ts | 50 + package.json | 3 +- .../hello-world/modules/app/views/index.spug | 3 - .../hello-world/modules/app/views/layout.spug | 1 - .../hello-world/modules/app/views/post.spug | 3 - samples/hello-world/styles/app.stcss | 28 +- samples/react/modules/app/views/feature.spug | 3 - samples/react/modules/app/views/index.spug | 4 - samples/react/modules/app/views/layout.spug | 1 - samples/react/modules/app/views/navbar.spug | 1 - samples/react/styles/app.stcss | 1062 +++++++++-------- scripts/forgejo-modules.ts | 113 ++ 23 files changed, 780 insertions(+), 603 deletions(-) create mode 100644 docs/install.md create mode 100644 modules/shadcn/src/integration.ts create mode 100644 scripts/forgejo-modules.ts diff --git a/README.md b/README.md index 720ed16..6339380 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # muddle +Forgejo installation and isolated integration module instructions are in [docs/install.md](docs/install.md). + > [!CAUTION] > MUDDLE IS EXPERIMENTAL SOFTWARE. EXPECT LITERALLY ANYTHING TO BREAK AT ANY TIME. > DURING TESTING THINGS HAVE WORKED. THEY WILL NOT ALWAYS. -Muddle is a Muddle Dark Wet Dub SSG. \ No newline at end of file +Muddle is a Muddle Dark Wet Dub SSG. diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 0000000..08c4496 --- /dev/null +++ b/docs/install.md @@ -0,0 +1,29 @@ +# Install Muddle from Forgejo + +Install the framework directly from the Muddle Forgejo repository. + +```sh +bun add muddle@git+https://git.crit.rip/mdl/muddle.git +``` + +```sh +npm install muddle@git+https://git.crit.rip/mdl/muddle.git +``` + +Integrations are isolated `@muddle/*` modules. After their Forgejo repositories are provisioned, install one independently. + +```sh +bun add @muddle/shadcn@git+https://git.crit.rip/mdl/shadcn.git +``` + +Create a Forgejo personal access token with repository creation rights, then provision the namespace repositories. The script only reads `MUDDLE_FORGEJO_TOKEN` from the environment and never prints it. + +```sh +MUDDLE_FORGEJO_TOKEN=token bun scripts/forgejo-modules.ts +``` + +Preview without network writes. + +```sh +bun scripts/forgejo-modules.ts --dry-run +``` diff --git a/integrations/shadcn.ts b/integrations/shadcn.ts index 1817e03..e56161a 100644 --- a/integrations/shadcn.ts +++ b/integrations/shadcn.ts @@ -1,60 +1,2 @@ -import { mkdir, writeFile } from "node:fs/promises"; -import { basename, extname, join, resolve } from "node:path"; -import type { Integration, IntegrationContext } from "../contracts/index.ts"; -import { cssToStcss, reactToSpug } from "../core/backport.ts"; - -export interface ShadcnIntegrationOptions { - /** Registry component names, for example `button`, `card`, or `dialog`. */ - components: string[]; - /** Registry origin or `{name}` URL template. Defaults to ShadCN's New York registry. */ - registry?: string; - /** Destination relative to the Muddle project root. */ - dir?: string; - /** Injectable for offline registries and tests. */ - fetch?: (input: string) => Promise; -} - -interface RegistryItem { - files?: { path: string; content: string; type?: string }[]; - registryDependencies?: string[]; -} - -/** - * Pulls declaratively selected ShadCN React components and backports them - * into SPUG and STCSS during application setup. - */ -export function shadcnIntegration(options: ShadcnIntegrationOptions): Integration { - return { - name: "shadcn", - async setup(context: IntegrationContext) { - const fetcher = options.fetch ?? globalThis.fetch; - const destination = resolve(context.config.root, options.dir ?? "modules/app/views/components"); - const styles = resolve(context.config.root, "styles/shadcn"); - const seen = new Set(); - const pull = async (name: string): Promise => { - if (seen.has(name)) return; - seen.add(name); - const base = options.registry ?? "https://ui.shadcn.com/r/styles/new-york/{name}.json"; - const url = base.includes("{name}") - ? base.replace("{name}", encodeURIComponent(name)) - : `${base.replace(/\/$/, "")}/${encodeURIComponent(name)}.json`; - const response = await fetcher(url); - if (!response.ok) throw new Error(`shadcn registry request failed for ${name} with ${response.status}`); - const item = await response.json() as RegistryItem; - for (const dependency of item.registryDependencies ?? []) await pull(dependency.replace(/^@[^/]+\//, "")); - for (const file of item.files ?? []) { - const extension = extname(file.path).toLowerCase(); - const stem = basename(file.path).replace(/\.(tsx|jsx|css)$/i, ""); - if (extension === ".tsx" || extension === ".jsx") { - await mkdir(destination, { recursive: true }); - await writeFile(join(destination, `${stem}.spug`), reactToSpug(file.content, file.path)); - } else if (extension === ".css") { - await mkdir(styles, { recursive: true }); - await writeFile(join(styles, `${stem}.stcss`), cssToStcss(file.content, file.path)); - } - } - }; - for (const component of options.components) await pull(component); - }, - }; -} +/** Compatibility export. The implementation lives in the isolated package. */ +export { shadcnIntegration, type ShadcnIntegrationOptions } from "../modules/shadcn/src/integration.ts"; diff --git a/modules/amezing/components/audio-button.spug b/modules/amezing/components/audio-button.spug index 4980a7c..06b0061 100644 --- a/modules/amezing/components/audio-button.spug +++ b/modules/amezing/components/audio-button.spug @@ -2,4 +2,4 @@ {%- set icon = load_data(path="icons/phosphor/" ~ "music" ~ ".svg", required=false) -%} {%- if icon -%}{{ icon | safe }}{{ else }}🎵{{ end }} Audio - \ No newline at end of file + diff --git a/modules/amezing/components/comments.spug b/modules/amezing/components/comments.spug index 6ac2b62..f28ac89 100644 --- a/modules/amezing/components/comments.spug +++ b/modules/amezing/components/comments.spug @@ -29,4 +29,4 @@ {%- endif -%} - \ No newline at end of file + diff --git a/modules/amezing/components/deanchor.spug b/modules/amezing/components/deanchor.spug index 0fea7f5..14e7498 100644 --- a/modules/amezing/components/deanchor.spug +++ b/modules/amezing/components/deanchor.spug @@ -1 +1 @@ -{{ text | striptags }} \ No newline at end of file +{{ text | striptags }} diff --git a/modules/amezing/components/footnotes.spug b/modules/amezing/components/footnotes.spug index a99881a..f3408a2 100644 --- a/modules/amezing/components/footnotes.spug +++ b/modules/amezing/components/footnotes.spug @@ -12,4 +12,4 @@ {%- endfor -%} {%- endif -%} - \ No newline at end of file + diff --git a/modules/amezing/components/fuse.spug b/modules/amezing/components/fuse.spug index 7f6c9f0..f456a7f 100644 --- a/modules/amezing/components/fuse.spug +++ b/modules/amezing/components/fuse.spug @@ -1,4 +1,4 @@
-
\ No newline at end of file + diff --git a/modules/amezing/components/search-fuse.spug b/modules/amezing/components/search-fuse.spug index 5c1bf7e..3b4150e 100644 --- a/modules/amezing/components/search-fuse.spug +++ b/modules/amezing/components/search-fuse.spug @@ -7,4 +7,4 @@
- \ No newline at end of file + diff --git a/modules/amezing/components/skip-to-content.spug b/modules/amezing/components/skip-to-content.spug index ffde958..1d530b2 100644 --- a/modules/amezing/components/skip-to-content.spug +++ b/modules/amezing/components/skip-to-content.spug @@ -1,4 +1,4 @@ {%- set icon = load_data(path="icons/phosphor/" ~ "arrow-up-right" ~ ".svg", required=false) -%} {%- if icon -%}{{ icon | safe }}{{ else }}Skip to content{{ end }} - \ No newline at end of file + diff --git a/modules/shadcn/index.ts b/modules/shadcn/index.ts index 37ef6f5..f93f5b4 100644 --- a/modules/shadcn/index.ts +++ b/modules/shadcn/index.ts @@ -1 +1 @@ -export { shadcnIntegration, type ShadcnIntegrationOptions } from "../../integrations/shadcn.ts"; +export { shadcnIntegration, type ShadcnIntegrationOptions } from "./src/integration.ts"; diff --git a/modules/shadcn/src/integration.ts b/modules/shadcn/src/integration.ts new file mode 100644 index 0000000..9d51f81 --- /dev/null +++ b/modules/shadcn/src/integration.ts @@ -0,0 +1,50 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { basename, extname, join, resolve } from "node:path"; +import { cssToStcss, reactToSpug, type Integration, type IntegrationContext } from "muddle"; + +export interface ShadcnIntegrationOptions { + components: string[]; + registry?: string; + dir?: string; + fetch?: (input: string) => Promise; +} + +interface RegistryItem { + files?: { path: string; content: string; type?: string }[]; + registryDependencies?: string[]; +} + +/** Pulls ShadCN registry entries and writes their Muddle ports into the host project. */ +export function shadcnIntegration(options: ShadcnIntegrationOptions): Integration { + return { + name: "shadcn", + async setup(context: IntegrationContext) { + const fetcher = options.fetch ?? globalThis.fetch; + const destination = resolve(context.config.root, options.dir ?? "modules/app/views/components"); + const styles = resolve(context.config.root, "styles/shadcn"); + const seen = new Set(); + const pull = async (name: string): Promise => { + if (seen.has(name)) return; + seen.add(name); + const base = options.registry ?? "https://ui.shadcn.com/r/styles/new-york/{name}.json"; + const url = base.includes("{name}") ? base.replace("{name}", encodeURIComponent(name)) : `${base.replace(/\/$/, "")}/${encodeURIComponent(name)}.json`; + const response = await fetcher(url); + if (!response.ok) throw new Error(`shadcn registry request failed for ${name} with ${response.status}`); + const item = await response.json() as RegistryItem; + for (const dependency of item.registryDependencies ?? []) await pull(dependency.replace(/^@[^/]+\//, "")); + for (const file of item.files ?? []) { + const extension = extname(file.path).toLowerCase(); + const stem = basename(file.path).replace(/\.(tsx|jsx|css)$/i, ""); + if (extension === ".tsx" || extension === ".jsx") { + await mkdir(destination, { recursive: true }); + await writeFile(join(destination, `${stem}.spug`), reactToSpug(file.content, file.path)); + } else if (extension === ".css") { + await mkdir(styles, { recursive: true }); + await writeFile(join(styles, `${stem}.stcss`), cssToStcss(file.content, file.path)); + } + } + }; + for (const component of options.components) await pull(component); + }, + }; +} diff --git a/package.json b/package.json index d0c69e8..c0a7579 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "test": "bun test ./tests", "lint": "bunx eslint . --ignore-pattern node_modules --ignore-pattern ametrine --ignore-pattern .opencode --ignore-pattern dist --ignore-pattern .git --ignore-pattern .cache --ignore-pattern .next --ignore-pattern .turbo --ignore-pattern .vercel --ignore-pattern .vscode --ignore-pattern .idea", "format": "bun run cli.ts fmt .", - "muddle": "bun run cli.ts" + "muddle": "bun run cli.ts", + "forgejo:modules": "bun scripts/forgejo-modules.ts --dry-run" }, "bin": { "muddle": "./cli.ts" diff --git a/samples/hello-world/modules/app/views/index.spug b/samples/hello-world/modules/app/views/index.spug index 6af0c48..321f0ba 100644 --- a/samples/hello-world/modules/app/views/index.spug +++ b/samples/hello-world/modules/app/views/index.spug @@ -1,10 +1,8 @@ extends ./layout.spug - props title: string nav: { label: string, href: string }[] posts: { id: string, title: string, excerpt: string }[] - block content section.primary-section@hero() h1= title @@ -17,7 +15,6 @@ block content div.line component amezing-article-list pages: posts.map((post) => ({ title: post.title, href: `/posts/${post.id}`, description: post.excerpt })) - $$$ assertExists("h1") assertExists(".article-list") diff --git a/samples/hello-world/modules/app/views/layout.spug b/samples/hello-world/modules/app/views/layout.spug index 3263997..f2f4533 100644 --- a/samples/hello-world/modules/app/views/layout.spug +++ b/samples/hello-world/modules/app/views/layout.spug @@ -2,7 +2,6 @@ props title: string nav: { label: string, href: string }[] currentPath?: string - component amezing-base title: title siteTitle: title diff --git a/samples/hello-world/modules/app/views/post.spug b/samples/hello-world/modules/app/views/post.spug index 5976681..e119e38 100644 --- a/samples/hello-world/modules/app/views/post.spug +++ b/samples/hello-world/modules/app/views/post.spug @@ -1,10 +1,8 @@ extends ./layout.spug - props title: string nav: { label: string, href: string }[] post: { id: string, title: string, excerpt: string } | null - block content if post article@post() @@ -24,7 +22,6 @@ block content section.primary-section h1 Not found p The post does not exist. - $$$ assertExists("article .card-title") assertText("article h1", "Test1") diff --git a/samples/hello-world/styles/app.stcss b/samples/hello-world/styles/app.stcss index 442b783..bd5bf28 100644 --- a/samples/hello-world/styles/app.stcss +++ b/samples/hello-world/styles/app.stcss @@ -1,28 +1,28 @@ main#main-content { - max-width: 72rem; - margin: 0 auto; - padding: 0 1.25rem 3rem; + max-width: 72rem; + margin: 0 auto; + padding: 0 1.25rem 3rem; } section.primary-section { - max-width: 48rem; - padding: 4rem 0 2rem; + max-width: 48rem; + padding: 4rem 0 2rem; - h1 { - margin: 0.75rem 0 0; - } + h1 { + margin: 0.75rem 0 0; + } - p { - max-width: 36rem; - } + p { + max-width: 36rem; + } - @predict mobile max-width max-height transform; + @predict mobile max-width max-height transform; } section .card { - border-radius: concentric 0.75rem; + border-radius: concentric 0.75rem; } .hero-blob { - background-image: freeform-gradient(#7c3aed 10% 50%, #f00 70% 40%); + background-image: freeform-gradient(#7c3aed 10% 50%, #f00 70% 40%); } diff --git a/samples/react/modules/app/views/feature.spug b/samples/react/modules/app/views/feature.spug index d924165..c8a484a 100644 --- a/samples/react/modules/app/views/feature.spug +++ b/samples/react/modules/app/views/feature.spug @@ -1,5 +1,4 @@ extends ./layout.spug - props title: string nav: { id: string, label: string, href: string, active?: boolean }[] @@ -9,13 +8,11 @@ props prose: string | null features: { label: string, href: string }[] current?: string - block content section.demo-feature h1= label if prose p.prose= prose - nav(aria-label="Feature routes") each feature in features a(href=feature.href aria-current=feature.href === current ? 'page' : '')= feature.label diff --git a/samples/react/modules/app/views/index.spug b/samples/react/modules/app/views/index.spug index ce7615f..f4e5f12 100644 --- a/samples/react/modules/app/views/index.spug +++ b/samples/react/modules/app/views/index.spug @@ -1,5 +1,4 @@ extends ./layout.spug - props title: string nav: { id: string, label: string, href: string, active?: boolean }[] @@ -7,12 +6,10 @@ props ribbonStyle?: string intro: { id: string, label: string, note: string, tint: string }[] features: { label: string, href: string }[] - block content section.demo-hero h1= title p - section.demo-intro(aria-label="Intro scene") div.demo-intro__stage@introstage() ul.demo-intro__topics(role="tablist") @@ -25,7 +22,6 @@ block content each step in intro p.demo-intro__note@intropane(step.id)() = step.note - section.demo-meter.demo-meter--one i section.demo-meter.demo-meter--two diff --git a/samples/react/modules/app/views/layout.spug b/samples/react/modules/app/views/layout.spug index d8a62b5..273436d 100644 --- a/samples/react/modules/app/views/layout.spug +++ b/samples/react/modules/app/views/layout.spug @@ -10,6 +10,5 @@ html(lang="en") component navbar nav: nav albums: albums - main.page block content diff --git a/samples/react/modules/app/views/navbar.spug b/samples/react/modules/app/views/navbar.spug index f81dc12..e12f87b 100644 --- a/samples/react/modules/app/views/navbar.spug +++ b/samples/react/modules/app/views/navbar.spug @@ -1,7 +1,6 @@ props nav: { id: string, label: string, href: string, active?: boolean, detail: string }[] albums: { id: string, name: string, cover: string, tracks: { title: string, duration: string }[] }[] - nav.navbar() div.navbar__row@navbar() a.navbar__brand(href="/") reactor diff --git a/samples/react/styles/app.stcss b/samples/react/styles/app.stcss index 03612d1..717dd9b 100644 --- a/samples/react/styles/app.stcss +++ b/samples/react/styles/app.stcss @@ -2,716 +2,772 @@ $scale: 3; $ease: cubic-bezier(0.32, 0.72, 0.22, 1); :root { - --bg: #fff; - --ink: #17130e; - --ink-soft: #71695c; - --line: #e4ded3; - --card: #ffffff; - --accent: #7c3aed; - --ease: $ease; - --dur: 0.32s; - --radius: 0.75rem; + --bg: #fff; + --ink: #17130e; + --ink-soft: #71695c; + --line: #e4ded3; + --card: #ffffff; + --accent: #7c3aed; + --ease: $ease; + --dur: 0.32s; + --radius: 0.75rem; } .navbar { - position: sticky; - top: 0; - z-index: 20; - isolation: isolate; - background: var(--card); - border-bottom: 1px solid var(--line); + position: sticky; + top: 0; + z-index: 20; + isolation: isolate; + background: var(--card); + border-bottom: 1px solid var(--line); - &::after { - content: ""; - position: absolute; - top: 100%; - left: 0; - width: 100%; - height: 0.5rem; - } + &::after { + content: ""; + position: absolute; + top: 100%; + left: 0; + width: 100%; + height: 0.5rem; + } } .navbar__row { - position: relative; - max-width: 72rem; - margin: 0 auto; - display: flex; - align-items: center; - gap: 0.9rem; - padding: 0.55rem 1.25rem 0.45rem; + position: relative; + max-width: 72rem; + margin: 0 auto; + display: flex; + align-items: center; + gap: 0.9rem; + padding: 0.55rem 1.25rem 0.45rem; } .navbar__brand { - color: var(--ink); - text-decoration: none; - font-weight: 650; - letter-spacing: -0.02em; - font-size: 0.95rem; + color: var(--ink); + text-decoration: none; + font-weight: 650; + letter-spacing: -0.02em; + font-size: 0.95rem; } .navbar { - --pill-x: $refer[left](@navitemhome); - --pill-y: $refer[top](@navitemhome); - --pill-w: $refer[width](@navitemhome); - --pill-h: $refer[height](@navitemhome); + --pill-x: $refer[left](@navitemhome); + --pill-y: $refer[top](@navitemhome); + --pill-w: $refer[width](@navitemhome); + --pill-h: $refer[height](@navitemhome); } @each $item in naviteminterpolate navitemrefer navitempredict navitemroute { - .navbar:has([data-muddle-anchor="#{$item}"][aria-current="page"]) { - --pill-x: $refer[left](@#{$item}); - --pill-y: $refer[top](@#{$item}); - --pill-w: $refer[width](@#{$item}); - --pill-h: $refer[height](@#{$item}); - } + .navbar:has([data-muddle-anchor="#{$item}"][aria-current="page"]) { + --pill-x: $refer[left](@#{$item}); + --pill-y: $refer[top](@#{$item}); + --pill-w: $refer[width](@#{$item}); + --pill-h: $refer[height](@#{$item}); + } } .navbar__pill { - position: absolute; - left: calc(var(--pill-x) - $refer[left](@navbar)); - top: calc(var(--pill-y) - $refer[top](@navbar)); - width: var(--pill-w); - height: var(--pill-h); - box-sizing: border-box; - border: 1px solid var(--accent); - border-radius: 999px; - pointer-events: none; - transition: left var(--dur) $ease, top var(--dur) $ease, width var(--dur) $ease, height var(--dur) $ease; + position: absolute; + left: calc(var(--pill-x) - $refer[left](@navbar)); + top: calc(var(--pill-y) - $refer[top](@navbar)); + width: var(--pill-w); + height: var(--pill-h); + box-sizing: border-box; + border: 1px solid var(--accent); + border-radius: 999px; + pointer-events: none; + transition: + left var(--dur) $ease, + top var(--dur) $ease, + width var(--dur) $ease, + height var(--dur) $ease; } .navbar__items { - display: flex; - gap: 0.15rem; - margin-left: auto; - list-style: none; - scroll-behavior: smooth; - scroll-snap-type: x proximity; - overscroll-behavior-inline: contain; + display: flex; + gap: 0.15rem; + margin-left: auto; + list-style: none; + scroll-behavior: smooth; + scroll-snap-type: x proximity; + overscroll-behavior-inline: contain; } .navbar__item { - display: block; - scroll-snap-align: center; - padding: 0.3rem 0.62rem; - border-radius: 0.55rem; - color: var(--ink-soft); - text-decoration: none; - font-size: 0.82rem; - transition: color var(--dur) $ease; + display: block; + scroll-snap-align: center; + padding: 0.3rem 0.62rem; + border-radius: 0.55rem; + color: var(--ink-soft); + text-decoration: none; + font-size: 0.82rem; + transition: color var(--dur) $ease; - &[aria-current="page"] { - color: var(--ink); - font-weight: 620; - } + &[aria-current="page"] { + color: var(--ink); + font-weight: 620; + } - &:hover { - color: var(--ink); - } + &:hover { + color: var(--ink); + } } :root { - --muddle-predict-duration: 300ms; - --muddle-predict-easing: $ease; + --muddle-predict-duration: 300ms; + --muddle-predict-easing: $ease; } .navbar { - --ribbon-x: calc($refer[left](@navitemhome) + $refer[width](@navitemhome) / 2 - $refer[left](@navbar) - ($refer[width](@panehome) + 1.6rem + 2px) / 2); - --ribbon-w: calc($refer[width](@panehome) + 1.6rem + 2px); - --ribbon-h: calc($refer[height](@panehome) + 1.24rem + 2px); - --pointer-x: calc($refer[left](@navitemhome) + $refer[width](@navitemhome) / 2 - $refer[left](@navbar) - var(--ribbon-x)); + --ribbon-x: calc( + $refer[left](@navitemhome) + $refer[width](@navitemhome) / + 2 - $refer[left](@navbar) - ($refer[width](@panehome) + 1.6rem + 2px) / 2 + ); + --ribbon-w: calc($refer[width](@panehome) + 1.6rem + 2px); + --ribbon-h: calc($refer[height](@panehome) + 1.24rem + 2px); + --pointer-x: calc( + $refer[left](@navitemhome) + $refer[width](@navitemhome) / + 2 - $refer[left](@navbar) - var(--ribbon-x) + ); } @each $id in home interpolate refer predict route { - .navbar:has([data-muddle-anchor="navitem#{$id}"]:hover) .navbar__ribbon, - .navbar:has([data-muddle-anchor="navitem#{$id}"]:focus-visible) .navbar__ribbon { - --ribbon-x: calc($refer[left](@navitem#{$id}) + $refer[width](@navitem#{$id}) / 2 - $refer[left](@navbar) - ($refer[width](@pane#{$id}) + 1.6rem + 2px) / 2); - --ribbon-w: calc($refer[width](@pane#{$id}) + 1.6rem + 2px); - --ribbon-h: calc($refer[height](@pane#{$id}) + 1.24rem + 2px); - --pointer-x: calc($refer[left](@navitem#{$id}) + $refer[width](@navitem#{$id}) / 2 - $refer[left](@navbar) - var(--ribbon-x)); - } + .navbar:has([data-muddle-anchor="navitem#{$id}"]:hover) .navbar__ribbon, + .navbar:has([data-muddle-anchor="navitem#{$id}"]:focus-visible) + .navbar__ribbon { + --ribbon-x: calc( + $refer[left](@navitem#{$id}) + + $refer[width](@navitem#{$id}) / + 2 - $refer[left](@navbar) - + ($refer[width](@pane#{$id}) + 1.6rem + 2px) / + 2 + ); + --ribbon-w: calc($refer[width](@pane#{$id}) + 1.6rem + 2px); + --ribbon-h: calc($refer[height](@pane#{$id}) + 1.24rem + 2px); + --pointer-x: calc( + $refer[left](@navitem#{$id}) + + $refer[width](@navitem#{$id}) / + 2 - $refer[left](@navbar) - var(--ribbon-x) + ); + } } .navbar__ribbon { - position: fixed; - top: calc($refer[top](@navbar) + $refer[height](@navbar)); - left: calc($refer[left](@navbar) + var(--ribbon-x)); - width: var(--ribbon-w); - height: var(--ribbon-h); - max-width: min(24rem, calc(100vw - 3rem)); - box-sizing: border-box; - overflow: clip; - clip-path: inset(-10px 0 0 round 0.6rem); - border: 1px solid var(--line); - border-radius: 0.6rem; - background: var(--card); - box-shadow: 0 14px 30px -20px rgba(23, 19, 14, 0.45); - opacity: 0; - visibility: visible; - transform: translateY(6px); - pointer-events: auto; - z-index: 30; - @predict left top width height opacity transform visibility; - @measure; + position: fixed; + top: calc($refer[top](@navbar) + $refer[height](@navbar)); + left: calc($refer[left](@navbar) + var(--ribbon-x)); + width: var(--ribbon-w); + height: var(--ribbon-h); + max-width: min(24rem, calc(100vw - 3rem)); + box-sizing: border-box; + overflow: clip; + clip-path: inset(-10px 0 0 round 0.6rem); + border: 1px solid var(--line); + border-radius: 0.6rem; + background: var(--card); + box-shadow: 0 14px 30px -20px rgba(23, 19, 14, 0.45); + opacity: 0; + visibility: visible; + transform: translateY(6px); + pointer-events: auto; + z-index: 30; + @predict left top width height opacity transform visibility; + @measure; - &::before, - &::after { - content: ""; - position: absolute; - top: -6px; - left: calc(var(--pointer-x) - 6px); - width: 12px; - height: 12px; - transform: rotate(45deg); - pointer-events: none; - @predict transform; - } + &::before, + &::after { + content: ""; + position: absolute; + top: -6px; + left: calc(var(--pointer-x) - 6px); + width: 12px; + height: 12px; + transform: rotate(45deg); + pointer-events: none; + @predict transform; + } - &::before { - z-index: 1; - background: var(--card); - border-left: 1px solid var(--line); - border-top: 1px solid var(--line); - } + &::before { + z-index: 1; + background: var(--card); + border-left: 1px solid var(--line); + border-top: 1px solid var(--line); + } - &::after { - z-index: 2; - width: 8px; - height: 8px; - top: -5px; - left: calc(var(--pointer-x) - 4px); - background: var(--card); - } + &::after { + z-index: 2; + width: 8px; + height: 8px; + top: -5px; + left: calc(var(--pointer-x) - 4px); + background: var(--card); + } } - .navbar:has(.navbar__item:hover) .navbar__ribbon, .navbar:has(.navbar__ribbon:hover) .navbar__ribbon, .navbar:has(.navbar__items:focus-within) .navbar__ribbon { - opacity: 1; - visibility: visible; - transform: translateY(0); - pointer-events: auto; + opacity: 1; + visibility: visible; + transform: translateY(0); + pointer-events: auto; } .navbar__pane { - position: absolute; - top: 0.62rem; - left: 0.8rem; - width: max-content; - max-width: 24rem; - margin: 0; - font-size: 0.8rem; - line-height: 1.5; - color: var(--ink-soft); - opacity: 0; - visibility: hidden; - transition: opacity var(--dur) $ease, visibility var(--dur) step-end; + position: absolute; + top: 0.62rem; + left: 0.8rem; + width: max-content; + max-width: 24rem; + margin: 0; + font-size: 0.8rem; + line-height: 1.5; + color: var(--ink-soft); + opacity: 0; + visibility: hidden; + transition: + opacity var(--dur) $ease, + visibility var(--dur) step-end; } @each $id in home interpolate refer predict route { - .navbar:has([data-muddle-anchor="navitem#{$id}"]:hover) [data-muddle-anchor="pane#{$id}"], - .navbar:has([data-muddle-anchor="navitem#{$id}"]:focus-visible) [data-muddle-anchor="pane#{$id}"] { - opacity: 1; - visibility: visible; - animation: fadein var(--dur) $ease; - } + .navbar:has([data-muddle-anchor="navitem#{$id}"]:hover) + [data-muddle-anchor="pane#{$id}"], + .navbar:has([data-muddle-anchor="navitem#{$id}"]:focus-visible) + [data-muddle-anchor="pane#{$id}"] { + opacity: 1; + visibility: visible; + animation: fadein var(--dur) $ease; + } } @keyframes fadein { - from { - opacity: 0; - } - to { - opacity: 1; - } + from { + opacity: 0; + } + to { + opacity: 1; + } } .navbar__pane-cover { - margin: 0; + margin: 0; - img { - width: 3rem; - height: 3rem; - border-radius: 0.45rem; - border: 1px solid var(--line); - display: block; - background: var(--bg); - } + img { + width: 3rem; + height: 3rem; + border-radius: 0.45rem; + border: 1px solid var(--line); + display: block; + background: var(--bg); + } } .navbar__pane--album { - width: min(19rem, calc(100vw - 4rem)); - max-height: min(20rem, calc(100vh - 6rem)); - overflow-y: auto; - overscroll-behavior: contain; - scrollbar-width: thin; - scrollbar-color: var(--line) transparent; - padding-right: 0.2rem; + width: min(19rem, calc(100vw - 4rem)); + max-height: min(20rem, calc(100vh - 6rem)); + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-width: thin; + scrollbar-color: var(--line) transparent; + padding-right: 0.2rem; } .navbar__album-header { - display: grid; - grid-template-columns: 3rem minmax(0, 1fr); - gap: 0.7rem; - align-items: center; + display: grid; + grid-template-columns: 3rem minmax(0, 1fr); + gap: 0.7rem; + align-items: center; } .navbar__pane-head { - margin: 0; - font-size: 0.8rem; - font-weight: 600; - color: var(--ink); + margin: 0; + font-size: 0.8rem; + font-weight: 600; + color: var(--ink); } .navbar__album-date { - display: block; - margin-top: 0.18rem; - font-size: 0.7rem; - color: var(--ink-soft); - font-variant-numeric: tabular-nums; + display: block; + margin-top: 0.18rem; + font-size: 0.7rem; + color: var(--ink-soft); + font-variant-numeric: tabular-nums; } .navbar__tracks { - margin: 0.7rem 0 0; - padding: 0; - list-style: none; - font-size: 0.74rem; - line-height: 1.6; - color: var(--ink-soft); - counter-reset: track; + margin: 0.7rem 0 0; + padding: 0; + list-style: none; + font-size: 0.74rem; + line-height: 1.6; + color: var(--ink-soft); + counter-reset: track; - li { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 0.8rem; - align-items: baseline; - counter-increment: track; - border-top: 1px solid color-mix(in srgb, var(--line) 72%, transparent); - padding: 0.28rem 0; + li { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 0.8rem; + align-items: baseline; + counter-increment: track; + border-top: 1px solid color-mix(in srgb, var(--line) 72%, transparent); + padding: 0.28rem 0; - span:first-child::before { - content: counter(track, decimal-leading-zero); - display: inline-block; - width: 1.65rem; - color: color-mix(in srgb, var(--ink-soft) 62%, transparent); - font-variant-numeric: tabular-nums; - } - } + span:first-child::before { + content: counter(track, decimal-leading-zero); + display: inline-block; + width: 1.65rem; + color: color-mix(in srgb, var(--ink-soft) 62%, transparent); + font-variant-numeric: tabular-nums; + } + } } .navbar__track-duration { - margin-left: 0; - color: color-mix(in srgb, var(--ink-soft) 70%, transparent); - font-variant-numeric: tabular-nums; + margin-left: 0; + color: color-mix(in srgb, var(--ink-soft) 70%, transparent); + font-variant-numeric: tabular-nums; } body { - margin: 0; - background: var(--bg); - color: var(--ink); - font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; - -webkit-font-smoothing: antialiased; + margin: 0; + background: var(--bg); + color: var(--ink); + font-family: + ui-sans-serif, + system-ui, + -apple-system, + "Segoe UI", + sans-serif; + -webkit-font-smoothing: antialiased; } a { - color: var(--accent); + color: var(--accent); } .page { - max-width: 72rem; - margin: 0 auto; - padding: 2.2rem 1.25rem 3rem; + max-width: 72rem; + margin: 0 auto; + padding: 2.2rem 1.25rem 3rem; } .card { - background: var(--card); - border: 1px solid var(--line); - border-radius: var(--radius); - padding: 1.15rem 1.25rem; + background: var(--card); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 1.15rem 1.25rem; } .demo-hero { - max-width: 44rem; - margin: 0.8rem 0 2rem; + max-width: 44rem; + margin: 0.8rem 0 2rem; - h1 { - margin: 0; - font-size: clamp(1.9rem, 4vw, 2.9rem); - letter-spacing: -0.03em; - } + h1 { + margin: 0; + font-size: clamp(1.9rem, 4vw, 2.9rem); + letter-spacing: -0.03em; + } - p { - margin: 0.85rem 0 0; - max-width: 34rem; - color: var(--ink-soft); - line-height: 1.65; + p { + margin: 0.85rem 0 0; + max-width: 34rem; + color: var(--ink-soft); + line-height: 1.65; - kbd { - font-size: 0.72rem; - background: var(--card); - border: 1px solid var(--line); - border-bottom-width: 2px; - border-radius: 0.3rem; - padding: 0.05rem 0.3rem; - } - } + kbd { + font-size: 0.72rem; + background: var(--card); + border: 1px solid var(--line); + border-bottom-width: 2px; + border-radius: 0.3rem; + padding: 0.05rem 0.3rem; + } + } } .demo-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); - gap: 0.8rem; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); + gap: 0.8rem; - a { - display: block; - height: 100%; - text-decoration: none; - color: inherit; - } + a { + display: block; + height: 100%; + text-decoration: none; + color: inherit; + } - .card { - height: 100%; - } + .card { + height: 100%; + } - strong { - display: block; - margin-bottom: 0.3rem; - font-size: 0.9rem; - } + strong { + display: block; + margin-bottom: 0.3rem; + font-size: 0.9rem; + } - span { - font-size: 0.8rem; - color: var(--ink-soft); - line-height: 1.5; - } + span { + font-size: 0.8rem; + color: var(--ink-soft); + line-height: 1.5; + } } .demo-aside { - margin-top: 1rem; - font-size: 0.8rem; - color: var(--ink-soft); + margin-top: 1rem; + font-size: 0.8rem; + color: var(--ink-soft); - code { - background: var(--card); - border: 1px solid var(--line); - border-radius: 0.3rem; - padding: 0.05rem 0.3rem; - } + code { + background: var(--card); + border: 1px solid var(--line); + border-radius: 0.3rem; + padding: 0.05rem 0.3rem; + } } .demo-feature { - max-width: 44rem; + max-width: 44rem; - h1 { - margin: 0.6rem 0 0; - font-size: clamp(1.7rem, 3.4vw, 2.4rem); - letter-spacing: -0.02em; - } + h1 { + margin: 0.6rem 0 0; + font-size: clamp(1.7rem, 3.4vw, 2.4rem); + letter-spacing: -0.02em; + } - nav { - display: flex; - flex-wrap: wrap; - gap: 0.35rem; - margin-top: 1.4rem; + nav { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + margin-top: 1.4rem; - a { - font-size: 0.8rem; - text-decoration: none; - border: 1px solid var(--line); - border-radius: 999px; - background: var(--card); - color: var(--ink-soft); - padding: 0.2rem 0.65rem; - transition: color var(--dur) $ease, border-color var(--dur) $ease; + a { + font-size: 0.8rem; + text-decoration: none; + border: 1px solid var(--line); + border-radius: 999px; + background: var(--card); + color: var(--ink-soft); + padding: 0.2rem 0.65rem; + transition: + color var(--dur) $ease, + border-color var(--dur) $ease; - &:hover { - color: var(--ink); - border-color: var(--ink); - } + &:hover { + color: var(--ink); + border-color: var(--ink); + } - &[aria-current="page"] { - color: var(--card); - border-color: var(--ink); - background: var(--ink); - } - } - } + &[aria-current="page"] { + color: var(--card); + border-color: var(--ink); + background: var(--ink); + } + } + } - .demo-grid { - margin-top: 2rem; - } + .demo-grid { + margin-top: 2rem; + } } .demo-meter { - display: flex; - flex-direction: column; - gap: 0.45rem; - margin-top: 1.2rem; + display: flex; + flex-direction: column; + gap: 0.45rem; + margin-top: 1.2rem; - i { - display: block; - height: 0.55rem; - border-radius: 999px; - background: color-mix(in srgb, var(--accent) 18%, var(--card)); - border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--line)); - } + i { + display: block; + height: 0.55rem; + border-radius: 999px; + background: color-mix(in srgb, var(--accent) 18%, var(--card)); + border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--line)); + } } .demo-meter--one i { - width: #{($scale * 2) * 2}rem; + width: #{($scale * 2) * 2}rem; } .demo-meter--two i { - width: #{$scale * 2.5}rem; + width: #{$scale * 2.5}rem; } .demo-meter--three i { - width: calc($scale * 3rem); + width: calc($scale * 3rem); } .demo-intro { - margin-top: 1.6rem; - max-width: 44rem; + margin-top: 1.6rem; + max-width: 44rem; } .demo-intro__stage { - --intro-tint: var(--accent); - --intro-x: calc($refer[left](@introtopicscaffold) + $refer[width](@introtopicscaffold) / 2 - $refer[left](@introstage)); - --intro-w: calc($refer[width](@intropanescaffold) + 2rem); - --intro-h: calc($refer[height](@intropanescaffold) + 1.4rem); - position: relative; - display: flex; - flex-direction: column; - gap: 0.9rem; - padding: 0.9rem 1rem 1.5rem; - border: 1px solid var(--line); - border-radius: 1rem; - background: - linear-gradient(to bottom, color-mix(in srgb, var(--intro-tint) 16%, transparent) 0%, transparent 60%), - var(--card); + --intro-tint: var(--accent); + --intro-x: calc( + $refer[left](@introtopicscaffold) + $refer[width](@introtopicscaffold) / + 2 - $refer[left](@introstage) + ); + --intro-w: calc($refer[width](@intropanescaffold) + 2rem); + --intro-h: calc($refer[height](@intropanescaffold) + 1.4rem); + position: relative; + display: flex; + flex-direction: column; + gap: 0.9rem; + padding: 0.9rem 1rem 1.5rem; + border: 1px solid var(--line); + border-radius: 1rem; + background: + linear-gradient( + to bottom, + color-mix(in srgb, var(--intro-tint) 16%, transparent) 0%, + transparent 60% + ), + var(--card); } .demo-intro__topics { - display: flex; - flex-wrap: wrap; - gap: 0.4rem; - list-style: none; - margin: 0; - padding: 0; + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + list-style: none; + margin: 0; + padding: 0; } .demo-intro__topic { - display: inline-flex; - font: inherit; - font-size: 0.8rem; - padding: 0.28rem 0.7rem; - border: 1px solid var(--line); - border-radius: 999px; - background: transparent; - color: var(--ink-soft); - cursor: pointer; - transition: color var(--dur) $ease, border-color var(--dur) $ease, background-color var(--dur) $ease; + display: inline-flex; + font: inherit; + font-size: 0.8rem; + padding: 0.28rem 0.7rem; + border: 1px solid var(--line); + border-radius: 999px; + background: transparent; + color: var(--ink-soft); + cursor: pointer; + transition: + color var(--dur) $ease, + border-color var(--dur) $ease, + background-color var(--dur) $ease; - &:hover { - color: var(--ink); - border-color: var(--ink); - } + &:hover { + color: var(--ink); + border-color: var(--ink); + } - .demo-intro__toggle:checked + & { - color: var(--card); - background: var(--ink); - border-color: var(--ink); - } + .demo-intro__toggle:checked + & { + color: var(--card); + background: var(--ink); + border-color: var(--ink); + } - &:focus-visible { - outline: 2px solid var(--intro-tint); - outline-offset: 1px; - } + &:focus-visible { + outline: 2px solid var(--intro-tint); + outline-offset: 1px; + } } .demo-intro__toggle { - position: absolute; - opacity: 0; - pointer-events: none; + position: absolute; + opacity: 0; + pointer-events: none; } .demo-intro__panel { - position: absolute; - left: max(0.75rem, calc(var(--intro-x) - var(--intro-w) / 2)); - top: calc($refer[top](@introtopicscaffold) - $refer[top](@introstage) + $refer[height](@introtopicscaffold) + 0.75rem); - width: var(--intro-w); - height: var(--intro-h); - box-sizing: border-box; - overflow: hidden; - border: 1px solid color-mix(in srgb, var(--intro-tint) 45%, var(--line)); - border-radius: 0.7rem; - background: var(--card); - box-shadow: 0 12px 26px -18px rgba(23, 19, 14, 0.5); - opacity: 0; - visibility: hidden; - transform: translateY(5px); - pointer-events: none; - @predict left top width height opacity transform visibility; - @measure; + position: absolute; + left: max(0.75rem, calc(var(--intro-x) - var(--intro-w) / 2)); + top: calc( + $refer[top](@introtopicscaffold) - $refer[top](@introstage) + + $refer[height](@introtopicscaffold) + 0.75rem + ); + width: var(--intro-w); + height: var(--intro-h); + box-sizing: border-box; + overflow: hidden; + border: 1px solid color-mix(in srgb, var(--intro-tint) 45%, var(--line)); + border-radius: 0.7rem; + background: var(--card); + box-shadow: 0 12px 26px -18px rgba(23, 19, 14, 0.5); + opacity: 0; + visibility: hidden; + transform: translateY(5px); + pointer-events: none; + @predict left top width height opacity transform visibility; + @measure; } .demo-intro__note { - position: absolute; - left: 1rem; - top: 0.7rem; - width: max-content; - max-width: 24rem; - margin: 0; - font-size: 0.78rem; - line-height: 1.55; - color: var(--ink-soft); - opacity: 0; - visibility: hidden; - @measure; + position: absolute; + left: 1rem; + top: 0.7rem; + width: max-content; + max-width: 24rem; + margin: 0; + font-size: 0.78rem; + line-height: 1.55; + color: var(--ink-soft); + opacity: 0; + visibility: hidden; + @measure; } .demo-intro__stage:has(.demo-intro__topics:focus-within) .demo-intro__panel { - opacity: 1; - visibility: visible; - transform: translateY(0); + opacity: 1; + visibility: visible; + transform: translateY(0); } .demo-intro__stage:has([data-tint="violet"]:hover), .demo-intro__stage:has(#intro-step-scaffold:checked) { - --intro-tint: #7c3aed; + --intro-tint: #7c3aed; } .demo-intro__stage:has([data-tint="rose"]:hover), .demo-intro__stage:has(#intro-step-relations:checked) { - --intro-tint: #e11d48; + --intro-tint: #e11d48; } .demo-intro__stage:has([data-tint="sky"]:hover), .demo-intro__stage:has(#intro-step-predict:checked) { - --intro-tint: #0284c7; + --intro-tint: #0284c7; } .demo-intro__stage:has([data-tint="amber"]:hover), .demo-intro__stage:has(#intro-step-momentum:checked) { - --intro-tint: #d97706; + --intro-tint: #d97706; } @each $id in scaffold relations predict momentum { - .demo-intro__stage:has([data-muddle-anchor="introtopic#{$id}"]:hover) .demo-intro__panel, - .demo-intro__stage:has(#intro-step-#{$id}:checked) .demo-intro__panel { - --intro-x: calc($refer[left](@introtopic#{$id}) + $refer[width](@introtopic#{$id}) / 2 - $refer[left](@introstage)); - --intro-w: calc($refer[width](@intropane#{$id}) + 2rem); - --intro-h: calc($refer[height](@intropane#{$id}) + 1.4rem); - } + .demo-intro__stage:has([data-muddle-anchor="introtopic#{$id}"]:hover) + .demo-intro__panel, + .demo-intro__stage:has(#intro-step-#{$id}:checked) .demo-intro__panel { + --intro-x: calc( + $refer[left](@introtopic#{$id}) + + $refer[width](@introtopic#{$id}) / + 2 - $refer[left](@introstage) + ); + --intro-w: calc($refer[width](@intropane#{$id}) + 2rem); + --intro-h: calc($refer[height](@intropane#{$id}) + 1.4rem); + } } @each $id in scaffold relations predict momentum { - .demo-intro__stage:has([data-muddle-anchor="introtopic#{$id}"]:hover) [data-muddle-anchor="intropane#{$id}"], - .demo-intro__stage:has(#intro-step-#{$id}:checked) [data-muddle-anchor="intropane#{$id}"], - .demo-intro__stage:has([data-muddle-anchor="introtopic#{$id}"]:focus-visible) [data-muddle-anchor="intropane#{$id}"] { - opacity: 1; - visibility: visible; - } + .demo-intro__stage:has([data-muddle-anchor="introtopic#{$id}"]:hover) + [data-muddle-anchor="intropane#{$id}"], + .demo-intro__stage:has(#intro-step-#{$id}:checked) + [data-muddle-anchor="intropane#{$id}"], + .demo-intro__stage:has([data-muddle-anchor="introtopic#{$id}"]:focus-visible) + [data-muddle-anchor="intropane#{$id}"] { + opacity: 1; + visibility: visible; + } } @each $id in scaffold relations predict momentum { - .demo-intro__stage:has(.demo-intro__topic:hover):has(#intro-step-#{$id}:checked) [data-muddle-anchor="intropane#{$id}"] { - opacity: 0; - visibility: hidden; - } + .demo-intro__stage:has(.demo-intro__topic:hover):has( + #intro-step-#{$id}:checked + ) + [data-muddle-anchor="intropane#{$id}"] { + opacity: 0; + visibility: hidden; + } - .demo-intro__stage:has([data-muddle-anchor="introtopic#{$id}"]:hover) [data-muddle-anchor="intropane#{$id}"] { - opacity: 1; - visibility: visible; - } + .demo-intro__stage:has([data-muddle-anchor="introtopic#{$id}"]:hover) + [data-muddle-anchor="intropane#{$id}"] { + opacity: 1; + visibility: visible; + } } .demo-intro__stage:has(.demo-intro__toggle:checked) .demo-intro__panel { - opacity: 1; - visibility: visible; - transform: translateY(0); - pointer-events: auto; + opacity: 1; + visibility: visible; + transform: translateY(0); + pointer-events: auto; } .prose { - max-width: 40rem; - line-height: 1.7; - font-size: 0.95rem; - margin-top: 1.1rem; + max-width: 40rem; + line-height: 1.7; + font-size: 0.95rem; + margin-top: 1.1rem; } .demo-footer { - max-width: 72rem; - margin: 0 auto; - padding: 1rem 1.25rem 2rem; - border-top: 1px solid var(--line); - font-size: 0.75rem; - color: var(--ink-soft); + max-width: 72rem; + margin: 0 auto; + padding: 1rem 1.25rem 2rem; + border-top: 1px solid var(--line); + font-size: 0.75rem; + color: var(--ink-soft); } @media (max-width: 48rem) { - .navbar__row { - flex-wrap: wrap; - padding: 0.5rem 1rem 0.35rem; - } + .navbar__row { + flex-wrap: wrap; + padding: 0.5rem 1rem 0.35rem; + } - .navbar__brand { - margin-right: auto; - } + .navbar__brand { + margin-right: auto; + } - .navbar__items { - margin-left: 0; - order: 3; - width: 100%; - gap: 0.05rem; - overflow-x: auto; - scrollbar-width: none; - padding-bottom: 0.25rem; - } + .navbar__items { + margin-left: 0; + order: 3; + width: 100%; + gap: 0.05rem; + overflow-x: auto; + scrollbar-width: none; + padding-bottom: 0.25rem; + } - .navbar__item { - font-size: 0.72rem; - padding: 0.25rem 0.5rem; - white-space: nowrap; - } + .navbar__item { + font-size: 0.72rem; + padding: 0.25rem 0.5rem; + white-space: nowrap; + } - .navbar__ribbon { - max-width: calc(100vw - 1.5rem); - } + .navbar__ribbon { + max-width: calc(100vw - 1.5rem); + } - .page { - padding: 1.4rem 1rem 2.2rem; - } + .page { + padding: 1.4rem 1rem 2.2rem; + } - .demo-hero h1 { - font-size: clamp(1.6rem, 7vw, 2.2rem); - } + .demo-hero h1 { + font-size: clamp(1.6rem, 7vw, 2.2rem); + } - .demo-intro__stage { - padding: 0.8rem 0.8rem 1.2rem; - } + .demo-intro__stage { + padding: 0.8rem 0.8rem 1.2rem; + } - .demo-intro__note { - max-width: calc(100vw - 5rem); - } + .demo-intro__note { + max-width: calc(100vw - 5rem); + } - .demo-intro__panel { - max-width: calc(100vw - 2rem); - } + .demo-intro__panel { + max-width: calc(100vw - 2rem); + } - .demo-meter i { - height: 0.5rem; - } + .demo-meter i { + height: 0.5rem; + } - .navbar__pane-cover img { - width: 2.4rem; - height: 2.4rem; - } + .navbar__pane-cover img { + width: 2.4rem; + height: 2.4rem; + } - .navbar__pane-head { - font-size: 0.72rem; - } + .navbar__pane-head { + font-size: 0.72rem; + } - .navbar__tracks { - font-size: 0.68rem; - } + .navbar__tracks { + font-size: 0.68rem; + } } diff --git a/scripts/forgejo-modules.ts b/scripts/forgejo-modules.ts new file mode 100644 index 0000000..f3f9ea1 --- /dev/null +++ b/scripts/forgejo-modules.ts @@ -0,0 +1,113 @@ +#!/usr/bin/env bun +import { existsSync } from "node:fs"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join, resolve } from "node:path"; + +const moduleNames = [ + "amezing", + "analysis", + "astro", + "database", + "latex", + "mdx", + "optimize", + "prettier-plugin", + "shadcn", + "tailwind", +] as const; +const root = resolve(import.meta.dir, ".."); +const api = (process.env.MUDDLE_FORGEJO_URL ?? "https://git.crit.rip").replace( + /\/$/, + "", +); +const owner = process.env.MUDDLE_FORGEJO_OWNER ?? "mdl"; +const dryRun = process.argv.includes("--dry-run"); +const token = process.env.MUDDLE_FORGEJO_TOKEN; + +if (!dryRun && !token) { + throw new Error( + "Set MUDDLE_FORGEJO_TOKEN or use --dry-run.", + ); +} + +async function forgejo( + path: string, + init: RequestInit = {}, +): Promise { + if (dryRun) return new Response(null, { status: 204 }); + return fetch(`${api}/api/v1${path}`, { + ...init, + headers: { + Authorization: `token ${token}`, + Accept: "application/json", + "Content-Type": "application/json", + ...init.headers, + }, + }); +} + +async function ensureWorkspaceModule(name: string): Promise { + const dir = join(root, "modules", name); + await mkdir(dir, { recursive: true }); + const manifest = join(dir, "package.json"); + if (!existsSync(manifest)) { + await writeFile( + manifest, + `${ + JSON.stringify( + { + name: `@muddle/${name}`, + version: "0.1.0", + type: "module", + exports: "./index.ts", + peerDependencies: { muddle: "*" }, + }, + null, + 2, + ) + }\n`, + ); + await writeFile(join(dir, "index.ts"), "export {};\n"); + } + const parsed = JSON.parse(await readFile(manifest, "utf8")) as { + name?: string; + }; + if (parsed.name !== `@muddle/${name}`) { + throw new Error(`${manifest} must declare @muddle/${name}`); + } +} + +async function ensureRemoteRepository(name: string): Promise { + const response = await forgejo(`/repos/${owner}/${name}`); + if (response.status === 200 || response.status === 204) return; + if (response.status !== 404) { + throw new Error( + `Unable to inspect ${owner}/${name}, Forgejo returned ${response.status}`, + ); + } + const created = await forgejo(`/orgs/${owner}/repos`, { + method: "POST", + body: JSON.stringify({ + name, + private: false, + description: `@muddle/${name} integration module`, + }), + }); + if (!created.ok) { + throw new Error( + `Unable to create ${owner}/${name}, Forgejo returned ${created.status}`, + ); + } +} + +for (const name of moduleNames) { + await ensureWorkspaceModule(name); + await ensureRemoteRepository(name); + console.log( + `${ + dryRun + ? "would provision" + : "provisioned" + } @muddle/${name} at ${api}/${owner}/${name}`, + ); +}