Repo automation and scrolltrans/viewtrans APIS
This commit is contained in:
parent
743eb6a8e8
commit
6ce485f6d3
23 changed files with 780 additions and 603 deletions
|
|
@ -1,5 +1,7 @@
|
||||||
# muddle
|
# muddle
|
||||||
|
|
||||||
|
Forgejo installation and isolated integration module instructions are in [docs/install.md](docs/install.md).
|
||||||
|
|
||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
> MUDDLE IS EXPERIMENTAL SOFTWARE. EXPECT LITERALLY ANYTHING TO BREAK AT ANY TIME.
|
> MUDDLE IS EXPERIMENTAL SOFTWARE. EXPECT LITERALLY ANYTHING TO BREAK AT ANY TIME.
|
||||||
> DURING TESTING THINGS HAVE WORKED. THEY WILL NOT ALWAYS.
|
> DURING TESTING THINGS HAVE WORKED. THEY WILL NOT ALWAYS.
|
||||||
|
|
|
||||||
29
docs/install.md
Normal file
29
docs/install.md
Normal file
|
|
@ -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
|
||||||
|
```
|
||||||
|
|
@ -1,60 +1,2 @@
|
||||||
import { mkdir, writeFile } from "node:fs/promises";
|
/** Compatibility export. The implementation lives in the isolated package. */
|
||||||
import { basename, extname, join, resolve } from "node:path";
|
export { shadcnIntegration, type ShadcnIntegrationOptions } from "../modules/shadcn/src/integration.ts";
|
||||||
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<Response>;
|
|
||||||
}
|
|
||||||
|
|
||||||
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<string>();
|
|
||||||
const pull = async (name: string): Promise<void> => {
|
|
||||||
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);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
export { shadcnIntegration, type ShadcnIntegrationOptions } from "../../integrations/shadcn.ts";
|
export { shadcnIntegration, type ShadcnIntegrationOptions } from "./src/integration.ts";
|
||||||
|
|
|
||||||
50
modules/shadcn/src/integration.ts
Normal file
50
modules/shadcn/src/integration.ts
Normal file
|
|
@ -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<Response>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<string>();
|
||||||
|
const pull = async (name: string): Promise<void> => {
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,8 @@
|
||||||
"test": "bun test ./tests",
|
"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",
|
"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 .",
|
"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": {
|
"bin": {
|
||||||
"muddle": "./cli.ts"
|
"muddle": "./cli.ts"
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
extends ./layout.spug
|
extends ./layout.spug
|
||||||
|
|
||||||
props
|
props
|
||||||
title: string
|
title: string
|
||||||
nav: { label: string, href: string }[]
|
nav: { label: string, href: string }[]
|
||||||
posts: { id: string, title: string, excerpt: string }[]
|
posts: { id: string, title: string, excerpt: string }[]
|
||||||
|
|
||||||
block content
|
block content
|
||||||
section.primary-section@hero()
|
section.primary-section@hero()
|
||||||
h1= title
|
h1= title
|
||||||
|
|
@ -17,7 +15,6 @@ block content
|
||||||
div.line
|
div.line
|
||||||
component amezing-article-list
|
component amezing-article-list
|
||||||
pages: posts.map((post) => ({ title: post.title, href: `/posts/${post.id}`, description: post.excerpt }))
|
pages: posts.map((post) => ({ title: post.title, href: `/posts/${post.id}`, description: post.excerpt }))
|
||||||
|
|
||||||
$$$
|
$$$
|
||||||
assertExists("h1")
|
assertExists("h1")
|
||||||
assertExists(".article-list")
|
assertExists(".article-list")
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ props
|
||||||
title: string
|
title: string
|
||||||
nav: { label: string, href: string }[]
|
nav: { label: string, href: string }[]
|
||||||
currentPath?: string
|
currentPath?: string
|
||||||
|
|
||||||
component amezing-base
|
component amezing-base
|
||||||
title: title
|
title: title
|
||||||
siteTitle: title
|
siteTitle: title
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
extends ./layout.spug
|
extends ./layout.spug
|
||||||
|
|
||||||
props
|
props
|
||||||
title: string
|
title: string
|
||||||
nav: { label: string, href: string }[]
|
nav: { label: string, href: string }[]
|
||||||
post: { id: string, title: string, excerpt: string } | null
|
post: { id: string, title: string, excerpt: string } | null
|
||||||
|
|
||||||
block content
|
block content
|
||||||
if post
|
if post
|
||||||
article@post()
|
article@post()
|
||||||
|
|
@ -24,7 +22,6 @@ block content
|
||||||
section.primary-section
|
section.primary-section
|
||||||
h1 Not found
|
h1 Not found
|
||||||
p The post does not exist.
|
p The post does not exist.
|
||||||
|
|
||||||
$$$
|
$$$
|
||||||
assertExists("article .card-title")
|
assertExists("article .card-title")
|
||||||
assertText("article h1", "Test1")
|
assertText("article h1", "Test1")
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
extends ./layout.spug
|
extends ./layout.spug
|
||||||
|
|
||||||
props
|
props
|
||||||
title: string
|
title: string
|
||||||
nav: { id: string, label: string, href: string, active?: boolean }[]
|
nav: { id: string, label: string, href: string, active?: boolean }[]
|
||||||
|
|
@ -9,13 +8,11 @@ props
|
||||||
prose: string | null
|
prose: string | null
|
||||||
features: { label: string, href: string }[]
|
features: { label: string, href: string }[]
|
||||||
current?: string
|
current?: string
|
||||||
|
|
||||||
block content
|
block content
|
||||||
section.demo-feature
|
section.demo-feature
|
||||||
h1= label
|
h1= label
|
||||||
if prose
|
if prose
|
||||||
p.prose= prose
|
p.prose= prose
|
||||||
|
|
||||||
nav(aria-label="Feature routes")
|
nav(aria-label="Feature routes")
|
||||||
each feature in features
|
each feature in features
|
||||||
a(href=feature.href aria-current=feature.href === current ? 'page' : '')= feature.label
|
a(href=feature.href aria-current=feature.href === current ? 'page' : '')= feature.label
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
extends ./layout.spug
|
extends ./layout.spug
|
||||||
|
|
||||||
props
|
props
|
||||||
title: string
|
title: string
|
||||||
nav: { id: string, label: string, href: string, active?: boolean }[]
|
nav: { id: string, label: string, href: string, active?: boolean }[]
|
||||||
|
|
@ -7,12 +6,10 @@ props
|
||||||
ribbonStyle?: string
|
ribbonStyle?: string
|
||||||
intro: { id: string, label: string, note: string, tint: string }[]
|
intro: { id: string, label: string, note: string, tint: string }[]
|
||||||
features: { label: string, href: string }[]
|
features: { label: string, href: string }[]
|
||||||
|
|
||||||
block content
|
block content
|
||||||
section.demo-hero
|
section.demo-hero
|
||||||
h1= title
|
h1= title
|
||||||
p
|
p
|
||||||
|
|
||||||
section.demo-intro(aria-label="Intro scene")
|
section.demo-intro(aria-label="Intro scene")
|
||||||
div.demo-intro__stage@introstage()
|
div.demo-intro__stage@introstage()
|
||||||
ul.demo-intro__topics(role="tablist")
|
ul.demo-intro__topics(role="tablist")
|
||||||
|
|
@ -25,7 +22,6 @@ block content
|
||||||
each step in intro
|
each step in intro
|
||||||
p.demo-intro__note@intropane(step.id)()
|
p.demo-intro__note@intropane(step.id)()
|
||||||
= step.note
|
= step.note
|
||||||
|
|
||||||
section.demo-meter.demo-meter--one
|
section.demo-meter.demo-meter--one
|
||||||
i
|
i
|
||||||
section.demo-meter.demo-meter--two
|
section.demo-meter.demo-meter--two
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,5 @@ html(lang="en")
|
||||||
component navbar
|
component navbar
|
||||||
nav: nav
|
nav: nav
|
||||||
albums: albums
|
albums: albums
|
||||||
|
|
||||||
main.page
|
main.page
|
||||||
block content
|
block content
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
props
|
props
|
||||||
nav: { id: string, label: string, href: string, active?: boolean, detail: string }[]
|
nav: { id: string, label: string, href: string, active?: boolean, detail: string }[]
|
||||||
albums: { id: string, name: string, cover: string, tracks: { title: string, duration: string }[] }[]
|
albums: { id: string, name: string, cover: string, tracks: { title: string, duration: string }[] }[]
|
||||||
|
|
||||||
nav.navbar()
|
nav.navbar()
|
||||||
div.navbar__row@navbar()
|
div.navbar__row@navbar()
|
||||||
a.navbar__brand(href="/") reactor
|
a.navbar__brand(href="/") reactor
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,11 @@ $ease: cubic-bezier(0.32, 0.72, 0.22, 1);
|
||||||
border: 1px solid var(--accent);
|
border: 1px solid var(--accent);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transition: left var(--dur) $ease, top var(--dur) $ease, width var(--dur) $ease, height var(--dur) $ease;
|
transition:
|
||||||
|
left var(--dur) $ease,
|
||||||
|
top var(--dur) $ease,
|
||||||
|
width var(--dur) $ease,
|
||||||
|
height var(--dur) $ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar__items {
|
.navbar__items {
|
||||||
|
|
@ -114,19 +118,36 @@ $ease: cubic-bezier(0.32, 0.72, 0.22, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar {
|
.navbar {
|
||||||
--ribbon-x: calc($refer[left](@navitemhome) + $refer[width](@navitemhome) / 2 - $refer[left](@navbar) - ($refer[width](@panehome) + 1.6rem + 2px) / 2);
|
--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-w: calc($refer[width](@panehome) + 1.6rem + 2px);
|
||||||
--ribbon-h: calc($refer[height](@panehome) + 1.24rem + 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));
|
--pointer-x: calc(
|
||||||
|
$refer[left](@navitemhome) + $refer[width](@navitemhome) /
|
||||||
|
2 - $refer[left](@navbar) - var(--ribbon-x)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@each $id in home interpolate refer predict route {
|
@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}"]:hover) .navbar__ribbon,
|
||||||
.navbar:has([data-muddle-anchor="navitem#{$id}"]:focus-visible) .navbar__ribbon {
|
.navbar:has([data-muddle-anchor="navitem#{$id}"]:focus-visible)
|
||||||
--ribbon-x: calc($refer[left](@navitem#{$id}) + $refer[width](@navitem#{$id}) / 2 - $refer[left](@navbar) - ($refer[width](@pane#{$id}) + 1.6rem + 2px) / 2);
|
.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-w: calc($refer[width](@pane#{$id}) + 1.6rem + 2px);
|
||||||
--ribbon-h: calc($refer[height](@pane#{$id}) + 1.24rem + 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));
|
--pointer-x: calc(
|
||||||
|
$refer[left](@navitem#{$id}) +
|
||||||
|
$refer[width](@navitem#{$id}) /
|
||||||
|
2 - $refer[left](@navbar) - var(--ribbon-x)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -182,7 +203,6 @@ $ease: cubic-bezier(0.32, 0.72, 0.22, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.navbar:has(.navbar__item:hover) .navbar__ribbon,
|
.navbar:has(.navbar__item:hover) .navbar__ribbon,
|
||||||
.navbar:has(.navbar__ribbon:hover) .navbar__ribbon,
|
.navbar:has(.navbar__ribbon:hover) .navbar__ribbon,
|
||||||
.navbar:has(.navbar__items:focus-within) .navbar__ribbon {
|
.navbar:has(.navbar__items:focus-within) .navbar__ribbon {
|
||||||
|
|
@ -204,12 +224,16 @@ $ease: cubic-bezier(0.32, 0.72, 0.22, 1);
|
||||||
color: var(--ink-soft);
|
color: var(--ink-soft);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transition: opacity var(--dur) $ease, visibility var(--dur) step-end;
|
transition:
|
||||||
|
opacity var(--dur) $ease,
|
||||||
|
visibility var(--dur) step-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@each $id in home interpolate refer predict route {
|
@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}"]:hover)
|
||||||
.navbar:has([data-muddle-anchor="navitem#{$id}"]:focus-visible) [data-muddle-anchor="pane#{$id}"] {
|
[data-muddle-anchor="pane#{$id}"],
|
||||||
|
.navbar:has([data-muddle-anchor="navitem#{$id}"]:focus-visible)
|
||||||
|
[data-muddle-anchor="pane#{$id}"] {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
animation: fadein var(--dur) $ease;
|
animation: fadein var(--dur) $ease;
|
||||||
|
|
@ -308,7 +332,12 @@ body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
font-family:
|
||||||
|
ui-sans-serif,
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
"Segoe UI",
|
||||||
|
sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -421,7 +450,9 @@ a {
|
||||||
background: var(--card);
|
background: var(--card);
|
||||||
color: var(--ink-soft);
|
color: var(--ink-soft);
|
||||||
padding: 0.2rem 0.65rem;
|
padding: 0.2rem 0.65rem;
|
||||||
transition: color var(--dur) $ease, border-color var(--dur) $ease;
|
transition:
|
||||||
|
color var(--dur) $ease,
|
||||||
|
border-color var(--dur) $ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
|
|
@ -475,7 +506,10 @@ a {
|
||||||
|
|
||||||
.demo-intro__stage {
|
.demo-intro__stage {
|
||||||
--intro-tint: var(--accent);
|
--intro-tint: var(--accent);
|
||||||
--intro-x: calc($refer[left](@introtopicscaffold) + $refer[width](@introtopicscaffold) / 2 - $refer[left](@introstage));
|
--intro-x: calc(
|
||||||
|
$refer[left](@introtopicscaffold) + $refer[width](@introtopicscaffold) /
|
||||||
|
2 - $refer[left](@introstage)
|
||||||
|
);
|
||||||
--intro-w: calc($refer[width](@intropanescaffold) + 2rem);
|
--intro-w: calc($refer[width](@intropanescaffold) + 2rem);
|
||||||
--intro-h: calc($refer[height](@intropanescaffold) + 1.4rem);
|
--intro-h: calc($refer[height](@intropanescaffold) + 1.4rem);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -486,7 +520,11 @@ a {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
background:
|
background:
|
||||||
linear-gradient(to bottom, color-mix(in srgb, var(--intro-tint) 16%, transparent) 0%, transparent 60%),
|
linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
color-mix(in srgb, var(--intro-tint) 16%, transparent) 0%,
|
||||||
|
transparent 60%
|
||||||
|
),
|
||||||
var(--card);
|
var(--card);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -509,7 +547,10 @@ a {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--ink-soft);
|
color: var(--ink-soft);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color var(--dur) $ease, border-color var(--dur) $ease, background-color var(--dur) $ease;
|
transition:
|
||||||
|
color var(--dur) $ease,
|
||||||
|
border-color var(--dur) $ease,
|
||||||
|
background-color var(--dur) $ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
|
|
@ -537,7 +578,10 @@ a {
|
||||||
.demo-intro__panel {
|
.demo-intro__panel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: max(0.75rem, calc(var(--intro-x) - var(--intro-w) / 2));
|
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);
|
top: calc(
|
||||||
|
$refer[top](@introtopicscaffold) - $refer[top](@introstage) +
|
||||||
|
$refer[height](@introtopicscaffold) + 0.75rem
|
||||||
|
);
|
||||||
width: var(--intro-w);
|
width: var(--intro-w);
|
||||||
height: var(--intro-h);
|
height: var(--intro-h);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
@ -596,30 +640,42 @@ a {
|
||||||
}
|
}
|
||||||
|
|
||||||
@each $id in scaffold relations predict momentum {
|
@each $id in scaffold relations predict momentum {
|
||||||
.demo-intro__stage:has([data-muddle-anchor="introtopic#{$id}"]:hover) .demo-intro__panel,
|
.demo-intro__stage:has([data-muddle-anchor="introtopic#{$id}"]:hover)
|
||||||
|
.demo-intro__panel,
|
||||||
.demo-intro__stage:has(#intro-step-#{$id}:checked) .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-x: calc(
|
||||||
|
$refer[left](@introtopic#{$id}) +
|
||||||
|
$refer[width](@introtopic#{$id}) /
|
||||||
|
2 - $refer[left](@introstage)
|
||||||
|
);
|
||||||
--intro-w: calc($refer[width](@intropane#{$id}) + 2rem);
|
--intro-w: calc($refer[width](@intropane#{$id}) + 2rem);
|
||||||
--intro-h: calc($refer[height](@intropane#{$id}) + 1.4rem);
|
--intro-h: calc($refer[height](@intropane#{$id}) + 1.4rem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@each $id in scaffold relations predict momentum {
|
@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([data-muddle-anchor="introtopic#{$id}"]:hover)
|
||||||
.demo-intro__stage:has(#intro-step-#{$id}:checked) [data-muddle-anchor="intropane#{$id}"],
|
[data-muddle-anchor="intropane#{$id}"],
|
||||||
.demo-intro__stage:has([data-muddle-anchor="introtopic#{$id}"]:focus-visible) [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;
|
opacity: 1;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@each $id in scaffold relations predict momentum {
|
@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}"] {
|
.demo-intro__stage:has(.demo-intro__topic:hover):has(
|
||||||
|
#intro-step-#{$id}:checked
|
||||||
|
)
|
||||||
|
[data-muddle-anchor="intropane#{$id}"] {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-intro__stage:has([data-muddle-anchor="introtopic#{$id}"]:hover) [data-muddle-anchor="intropane#{$id}"] {
|
.demo-intro__stage:has([data-muddle-anchor="introtopic#{$id}"]:hover)
|
||||||
|
[data-muddle-anchor="intropane#{$id}"] {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
113
scripts/forgejo-modules.ts
Normal file
113
scripts/forgejo-modules.ts
Normal file
|
|
@ -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<Response> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue