feat: add module logos, bundle codemods into registry, and build out the web UI
- **Module logos**: `registry-build.ts` now auto-detects `logo.svg` / `logo-light.svg` + `logo-dark.svg` in each module directory and inlines them as `mod.logo` (string or `{ light, dark }`) — no `module.ts` declarations needed; first-party SVGs added for all current modules
- **Codemod bundling**: `registry-build.ts` bundles each module's `codemods/index.ts` via `Bun.build` (ESM, externals: `@stanza/codemods`, `ts-morph`, `node:*`) and embeds the output as `mod.codemodBundle`; the CLI runner materializes it to `node_modules/.cache/stanza-codemods/` so externals resolve normally, replacing the old "first-party only" approach
- **Web builder UI**: split monolithic `builder.tsx` into `builder/{index,slot-cards,command-bar,file-preview}.tsx`; add `Header`, `Footer`, `ModeToggle`, `ThemeProvider`; scaffold shadcn `Badge`, `Card`, `DropdownMenu`, `Input`, `Separator`, `Sonner`, `Tabs`, `Tooltip` components
- **Server-side Shiki**: `apps/web/src/server/highlighter.ts` keeps a warm Shiki instance; `builder-state.ts` (`createServerFn`) computes selections from URL params and pre-renders `{ light, dark }` HTML per file — Shiki is never imported client-side
- **Selection logic**: new `apps/web/src/lib/selection.ts` with full unit tests
- **copy-registry script**: `predev`/`prebuild` hook copies `dist/registry/` → `apps/web/public/registry/` so the same-domain registry path resolves without a separate CDN
- Add `shiki`, `sonner`, `@pierre/trees`, `@fontsource-variable/geist-mono` to `apps/web`; update oxfmt ignore patterns and swap `react-hooks` for `react-perf` + `promise` in oxlint web config
No files matched your search
@@ -8,5 +8,16 @@
|
||||
"preserveWhitespace": false
|
||||
},
|
||||
"sortPackageJson": true,
|
||||
"ignorePatterns": []
|
||||
"ignorePatterns": [
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
"**/.output/**",
|
||||
"**/.turbo/**",
|
||||
"**/.vercel/**",
|
||||
"**/routeTree.gen.ts",
|
||||
"**/coverage/**",
|
||||
"apps/web/public/registry/**",
|
||||
"registry/modules/*/logo*.svg",
|
||||
"registry/modules/*/templates/**"
|
||||
]
|
||||
}
|
||||
@@ -33,7 +33,7 @@
|
||||
},
|
||||
{
|
||||
"files": ["apps/web/**/*.tsx"],
|
||||
"plugins": ["typescript", "react", "react-hooks", "jsx-a11y", "unicorn", "import"],
|
||||
"plugins": ["typescript", "react", "react-perf", "promise", "jsx-a11y", "unicorn", "import"],
|
||||
"rules": {
|
||||
"react/react-in-jsx-scope": "off"
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ Three things differentiate stanza from other scaffolders:
|
||||
- `bun apps/cli/src/bin.ts <verb>` — run CLI directly without build
|
||||
- `pnpm registry:build` (or `bun packages/internal/src/registry-build.ts`) — regenerate `dist/registry/{index,modules/*}.json`
|
||||
- `pnpm module:new [slot] [id]` — scaffold a new module under `registry/modules/`
|
||||
- `pnpm --filter @stanza/web dev` — TanStack Start dev server; `predev` hook auto-copies `dist/registry/` → `apps/web/public/registry/` so the same-domain registry path resolves
|
||||
- `pnpm lint` / `pnpm lint:fix` — Oxlint across the whole repo (config: `.oxlintrc.json`)
|
||||
- `pnpm fmt` / `pnpm fmt:check` — oxfmt across the whole repo (config: `.oxfmtrc.json`)
|
||||
- `cd packages/<x> && node_modules/.bin/vitest run` — unit tests (per workspace; no root `vitest` binary)
|
||||
@@ -49,6 +50,9 @@ Three things differentiate stanza from other scaffolders:
|
||||
|
||||
- **Modules are vendored**: their templates land in the user's repo verbatim; no `@stanza/runtime` dep
|
||||
- **Template distribution**: `packages/internal`'s `registry-build.ts` inlines each template file's contents into the per-module JSON's `templates[].content` field so HTTP-loaded manifests are self-contained. The runner prefers `tpl.content` and only reads from `registry/modules/<x>/templates/` when it's absent (local dev). New templates need no build wiring — they're picked up automatically
|
||||
- **Module logos**: drop `logo.svg` (theme-agnostic) or `logo-light.svg` + `logo-dark.svg` (theme pair) in a module's directory. The registry build auto-detects and inlines as `mod.logo` (string or `{ light, dark }`) — module authors don't declare anything in `module.ts`. First-party logos come from [svgl.app](https://svgl.app). The web builder renders inline via `dangerouslySetInnerHTML`
|
||||
- **Codemod distribution**: each module's `codemods/index.ts` is bundled by `registry-build.ts` via `Bun.build` (`format: "esm"`, externals: `@stanza/codemods`, `@stanza/registry`, `ts-morph`, `node:*`) and embedded as `mod.codemodBundle` (string) in the per-module JSON. The CLI runner writes the bundle to `node_modules/.cache/stanza-codemods/` (located by walking up from `import.meta.url` looking for `@stanza/codemods` in a `node_modules`) and dynamic-imports it — that placement is what makes the externals resolve. Local dev still imports the `.ts` source off disk when no bundle is present
|
||||
- **apps/web previews are server-rendered**: Shiki runs in `apps/web/src/server/highlighter.ts` (module-singleton, kept warm). The builder loader (`createServerFn` in `apps/web/src/server/builder-state.ts`) computes selected files from URL search params, pre-renders Shiki HTML for each, and ships `Record<path, { light, dark }>` to the client. `shiki` must NEVER be imported from a client component — verified by `vite build` followed by `grep shiki .output/public/assets/*.js` (should return nothing)
|
||||
- **Slot taxonomy** is currently `framework | styling | db | orm | auth` (see `KNOWN_SLOTS`); adding a slot is a manifest schema bump — update `KNOWN_SLOTS`, `slotOrder`, and the Zod manifest schema together
|
||||
- **Adapter keys** encode peer choices (e.g., `next+drizzle`); the resolver picks the most specific match
|
||||
- **Region ownership** in `stanza.json` is the source of truth for `remove`/future-`swap`; two modules claiming the same region is a hard error (`RegionConflictError`)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { openProject } from "@stanza/codemods";
|
||||
import {
|
||||
@@ -100,7 +101,7 @@ export async function applyModule(args: {
|
||||
|
||||
// 4. Imperative codemods — resolved against the module's local codemod registry.
|
||||
if (adapter.codemods?.length) {
|
||||
const codemods = await loadCodemods(moduleDir);
|
||||
const codemods = await loadCodemods(module, moduleDir);
|
||||
const project = lazyProject(appRoot);
|
||||
const ctx = buildContext({
|
||||
projectRoot,
|
||||
@@ -157,13 +158,60 @@ function readTemplateSource(tpl: TemplateRef, moduleDir: string): string {
|
||||
|
||||
type CodemodMap = Record<string, import("@stanza/codemods").Codemod>;
|
||||
|
||||
async function loadCodemods(moduleDir: string): Promise<CodemodMap> {
|
||||
/**
|
||||
* Resolve a module's imperative codemods. HTTP-loaded modules carry a
|
||||
* pre-bundled JS string in `module.codemodBundle` (set by the registry build
|
||||
* step, with `@stanza/codemods` + `ts-morph` externalized so the CLI provides
|
||||
* them); we materialize it inside the CLI's own `node_modules/.cache/` so the
|
||||
* import can resolve those externals up the standard tree. Local dev imports
|
||||
* the source straight off disk.
|
||||
*/
|
||||
async function loadCodemods(module: Module, moduleDir: string): Promise<CodemodMap> {
|
||||
if (module.codemodBundle) {
|
||||
const cacheDir = getBundleCacheDir();
|
||||
const file = path.join(cacheDir, `${module.slot}-${module.id}-${Date.now()}.mjs`);
|
||||
fs.writeFileSync(file, module.codemodBundle, "utf8");
|
||||
try {
|
||||
const mod = (await import(file)) as { default?: CodemodMap; codemods?: CodemodMap };
|
||||
return mod.default ?? mod.codemods ?? {};
|
||||
} finally {
|
||||
// Best-effort cleanup — the import already cached the module so deleting
|
||||
// the source file is safe. Leaving it on a failed import for debugging.
|
||||
try {
|
||||
fs.unlinkSync(file);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
const entry = path.join(moduleDir, "codemods", "index.ts");
|
||||
if (!fs.existsSync(entry)) return {};
|
||||
const mod = (await import(entry)) as { default?: CodemodMap; codemods?: CodemodMap };
|
||||
return mod.default ?? mod.codemods ?? {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a `node_modules/` tree upstream of the CLI source that has
|
||||
* `@stanza/codemods` installed, and return its `.cache/stanza-codemods/`
|
||||
* subdir. Temp bundle files written here can resolve their `@stanza/*` and
|
||||
* `ts-morph` imports the normal way.
|
||||
*/
|
||||
function getBundleCacheDir(): string {
|
||||
let dir = path.dirname(fileURLToPath(import.meta.url));
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const candidate = path.join(dir, "node_modules");
|
||||
if (fs.existsSync(path.join(candidate, "@stanza", "codemods"))) {
|
||||
const out = path.join(candidate, ".cache", "stanza-codemods");
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
return out;
|
||||
}
|
||||
const parent = path.dirname(dir);
|
||||
if (parent === dir) break;
|
||||
dir = parent;
|
||||
}
|
||||
throw new Error(
|
||||
"Could not locate @stanza/codemods relative to the CLI — bundled codemods need it in the resolution graph.",
|
||||
);
|
||||
}
|
||||
|
||||
function lazyProject(appRoot: string): { (): Project; save?: () => Promise<void> } {
|
||||
let project: Project | undefined;
|
||||
const fn = (() => {
|
||||
|
||||
@@ -99,11 +99,10 @@ async function loadHttpRegistry(baseUrl: string): Promise<Registry> {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(`Module fetch failed: ${url} (${res.status})`);
|
||||
return (await res.json()) as Module;
|
||||
// NOTE: HTTP-loaded modules can't ship codemod *functions* — they
|
||||
// contain only the manifest + codemod ids. The CLI ships a bundled
|
||||
// registry of codemod implementations keyed by id, populated at build
|
||||
// time from the first-party modules. Custom codemod bundles for
|
||||
// third-party modules are a future addition (signed JS payloads).
|
||||
// HTTP-loaded modules carry their imperative codemods as a pre-bundled
|
||||
// ESM string in `module.codemodBundle` (populated by registry-build,
|
||||
// with `@stanza/codemods` + `ts-morph` externalized). The runner
|
||||
// materializes it to a temp file and dynamic-imports.
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,13 +6,17 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"predev": "bun run scripts/copy-registry.ts",
|
||||
"build": "vite build",
|
||||
"prebuild": "bun run scripts/copy-registry.ts",
|
||||
"start": "node .output/server/index.mjs",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.5.0",
|
||||
"@fontsource-variable/geist": "^5.2.9",
|
||||
"@fontsource-variable/geist-mono": "^5.2.8",
|
||||
"@pierre/trees": "1.0.0-beta.4",
|
||||
"@stanza/registry": "workspace:*",
|
||||
"@tabler/icons-react": "^3.44.0",
|
||||
"@tailwindcss/vite": "^4.3.0",
|
||||
@@ -28,6 +32,8 @@
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6",
|
||||
"shadcn": "^4.7.0",
|
||||
"shiki": "^4.1.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tailwindcss": "^4.3.0",
|
||||
"tw-animate-css": "^1.4.0"
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Copies the registry CDN output (`dist/registry/`) into `apps/web/public/registry/`
|
||||
* so the web builder can serve it from the same domain. Runs as a predev/prebuild
|
||||
* hook; safe to re-run.
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const appDir = path.resolve(here, "..");
|
||||
const repoRoot = path.resolve(appDir, "..", "..");
|
||||
|
||||
const src = path.join(repoRoot, "dist", "registry");
|
||||
const dest = path.join(appDir, "public", "registry");
|
||||
|
||||
if (!fs.existsSync(src)) {
|
||||
console.warn(
|
||||
`[copy-registry] ${path.relative(repoRoot, src)} doesn't exist yet — run \`pnpm registry:build\` first. Skipping.`,
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
fs.rmSync(dest, { recursive: true, force: true });
|
||||
fs.cpSync(src, dest, { recursive: true });
|
||||
console.log(`[copy-registry] ${path.relative(repoRoot, src)} → ${path.relative(repoRoot, dest)}`);
|
||||
@@ -1,110 +0,0 @@
|
||||
import type { ModuleSummary, RegistryIndex, SlotId } from "@stanza/registry";
|
||||
import { resolveAdapter, slotOrder, emptyManifest } from "@stanza/registry";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
type Selections = Partial<Record<SlotId, string>>;
|
||||
|
||||
export function Builder({ index }: { index: RegistryIndex }) {
|
||||
const [selections, setSelections] = useState<Selections>({});
|
||||
const [name, setName] = useState("my-app");
|
||||
|
||||
const moduleById = useMemo(() => {
|
||||
const m = new Map<string, ModuleSummary>();
|
||||
for (const mod of index.modules) m.set(`${mod.slot}:${mod.id}`, mod);
|
||||
return m;
|
||||
}, [index]);
|
||||
|
||||
const compatibleBySlot = useMemo(() => {
|
||||
const out: Record<SlotId, ModuleSummary[]> = {
|
||||
framework: [],
|
||||
styling: [],
|
||||
db: [],
|
||||
orm: [],
|
||||
auth: [],
|
||||
};
|
||||
const pending = pendingFromSelections(selections, moduleById);
|
||||
for (const slot of slotOrder) {
|
||||
out[slot] = index.modules
|
||||
.filter((m) => m.slot === slot)
|
||||
.filter((m) => {
|
||||
const synthetic = { ...m, adapters: m.adapters.map((a) => ({ ...a })) };
|
||||
return resolveAdapter(synthetic, {
|
||||
manifest: emptyManifest({ name: "t" }),
|
||||
pending,
|
||||
}).ok;
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}, [index, selections, moduleById]);
|
||||
|
||||
const command = useMemo(() => buildCommand(name, selections), [name, selections]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label style={{ display: "block", marginBottom: "1rem" }}>
|
||||
<div>Project name</div>
|
||||
<input
|
||||
aria-label="Project name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
style={{ padding: "0.5rem", width: "100%" }}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{slotOrder.map((slot) => (
|
||||
<fieldset
|
||||
key={slot}
|
||||
style={{ margin: "0 0 1rem 0", padding: "0.75rem", border: "1px solid #ddd" }}
|
||||
>
|
||||
<legend>{slotLabel(slot)}</legend>
|
||||
{compatibleBySlot[slot].length === 0 ? (
|
||||
<div style={{ opacity: 0.6 }}>No compatible modules with current picks.</div>
|
||||
) : (
|
||||
compatibleBySlot[slot].map((m) => (
|
||||
<label key={m.id} style={{ display: "block" }}>
|
||||
<input
|
||||
type="radio"
|
||||
name={slot}
|
||||
aria-label={m.label}
|
||||
checked={selections[slot] === m.id}
|
||||
onChange={() => setSelections((s) => ({ ...s, [slot]: m.id }))}
|
||||
/>{" "}
|
||||
<strong>{m.label}</strong> <span style={{ opacity: 0.7 }}>{m.description}</span>
|
||||
</label>
|
||||
))
|
||||
)}
|
||||
</fieldset>
|
||||
))}
|
||||
|
||||
<pre style={{ background: "#111", color: "#eee", padding: "1rem", borderRadius: "0.25rem" }}>
|
||||
{command}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function pendingFromSelections(selections: Selections, moduleById: Map<string, ModuleSummary>) {
|
||||
const pending: Record<string, ModuleSummary> = {};
|
||||
for (const [slot, id] of Object.entries(selections)) {
|
||||
if (!id) continue;
|
||||
const mod = moduleById.get(`${slot}:${id}`);
|
||||
if (mod) pending[slot] = mod;
|
||||
}
|
||||
// The resolver expects full Modules; the empty `adapters` slot bodies don't
|
||||
// matter for peer-validation, so cast is safe.
|
||||
return pending as Parameters<typeof resolveAdapter>[1]["pending"];
|
||||
}
|
||||
|
||||
function buildCommand(name: string, selections: Selections): string {
|
||||
const flags = slotOrder
|
||||
.map((s) => (selections[s] ? `--${s}=${selections[s]}` : null))
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
return `pnpm create stanza ${name} ${flags}`.trim();
|
||||
}
|
||||
|
||||
function slotLabel(slot: SlotId): string {
|
||||
return { framework: "Framework", styling: "Styling", db: "Database", orm: "ORM", auth: "Auth" }[
|
||||
slot
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { IconCopy } from "@tabler/icons-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
|
||||
export function CommandBar({
|
||||
name,
|
||||
defaultName,
|
||||
command,
|
||||
onNameChange,
|
||||
}: {
|
||||
name: string;
|
||||
defaultName: string;
|
||||
command: string;
|
||||
onNameChange: (name: string) => void;
|
||||
}) {
|
||||
const onCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(command);
|
||||
toast.success("Copied to clipboard");
|
||||
} catch {
|
||||
toast.error("Couldn't access the clipboard");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="gap-4 p-5">
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="stanza-project-name" className="text-xs font-medium text-muted-foreground">
|
||||
Project name
|
||||
</label>
|
||||
<Input
|
||||
id="stanza-project-name"
|
||||
value={name}
|
||||
placeholder={defaultName}
|
||||
onChange={(e) => onNameChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<span className="text-xs font-medium text-muted-foreground">Run this</span>
|
||||
<div className="flex items-stretch gap-2">
|
||||
<pre className="flex-1 overflow-x-auto rounded-md border border-border bg-muted/50 px-3 py-2 font-mono text-xs">
|
||||
<code>{command}</code>
|
||||
</pre>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button variant="outline" size="icon" onClick={onCopy} aria-label="Copy command">
|
||||
<IconCopy className="size-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>Copy</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { FileTree, useFileTree, useFileTreeSelection } from "@pierre/trees/react";
|
||||
import type { Module, ModuleAdapter, SlotId } from "@stanza/registry";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { useTheme } from "@/components/theme-provider";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import type { Preview } from "@/server/highlighter";
|
||||
|
||||
export function FilePreview({
|
||||
filePaths,
|
||||
previews,
|
||||
resolved,
|
||||
}: {
|
||||
filePaths: string[];
|
||||
previews: Record<string, Preview>;
|
||||
resolved: Partial<Record<SlotId, { module: Module; adapter: ModuleAdapter }>>;
|
||||
}) {
|
||||
// @pierre/trees is path-driven. We pass our derived file list straight in.
|
||||
// When the parent's loader reruns with new selections, this component
|
||||
// re-mounts and the tree model rebuilds with the fresh paths.
|
||||
const { model } = useFileTree({
|
||||
paths: filePaths,
|
||||
search: false,
|
||||
});
|
||||
|
||||
// The model owns selection internally; `useFileTreeSelection` exposes the
|
||||
// currently-selected paths as a stable readonly array we subscribe to.
|
||||
const selectedPaths = useFileTreeSelection(model);
|
||||
const activePath = selectedPaths[0] ?? filePaths[0];
|
||||
const preview = activePath ? previews[activePath] : undefined;
|
||||
|
||||
const slotCount = Object.keys(resolved).length;
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden p-0">
|
||||
<div className="flex items-center justify-between gap-3 border-b border-border bg-muted/30 px-4 py-2.5">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
{filePaths.length > 0
|
||||
? `${filePaths.length} file${filePaths.length === 1 ? "" : "s"} from ${slotCount} module${slotCount === 1 ? "" : "s"}`
|
||||
: "Pick a module to preview generated files"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{filePaths.length === 0 ? (
|
||||
<EmptyState />
|
||||
) : (
|
||||
<div className="grid min-h-[420px] grid-cols-1 sm:grid-cols-[minmax(0,260px)_minmax(0,1fr)]">
|
||||
<div className="border-b border-border sm:border-r sm:border-b-0">
|
||||
<FileTree model={model} className="h-full max-h-[420px] overflow-auto" />
|
||||
</div>
|
||||
<PreviewPane preview={preview} path={activePath} />
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyState() {
|
||||
return (
|
||||
<div className="flex min-h-[280px] items-center justify-center px-6 py-10 text-center text-sm text-muted-foreground">
|
||||
The file tree will populate as you pick modules.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PreviewPane({
|
||||
preview,
|
||||
path,
|
||||
}: {
|
||||
preview: Preview | undefined;
|
||||
path: string | undefined;
|
||||
}) {
|
||||
const { theme } = useTheme();
|
||||
const resolved = useResolvedTheme(theme);
|
||||
const html = useMemo(() => {
|
||||
if (!preview) return "";
|
||||
return resolved === "dark" ? preview.dark : preview.light;
|
||||
}, [preview, resolved]);
|
||||
|
||||
if (!preview || !path) {
|
||||
return (
|
||||
<div className="flex items-center justify-center px-6 py-10 text-sm text-muted-foreground">
|
||||
Select a file
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="border-b border-border bg-muted/20 px-4 py-2 font-mono text-[11px] text-muted-foreground">
|
||||
{path}
|
||||
</div>
|
||||
<div
|
||||
className="overflow-auto text-xs leading-relaxed [&_pre]:bg-transparent! [&_pre]:p-4!"
|
||||
// Shiki HTML is server-rendered from our trusted registry payload.
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useResolvedTheme(theme: string): "light" | "dark" {
|
||||
// Match the same `light` / `dark` / `system` semantics the ThemeProvider
|
||||
// applies to <html>, without re-reading the DOM (this runs server-side too).
|
||||
if (theme === "dark") return "dark";
|
||||
if (theme === "light") return "light";
|
||||
if (typeof window === "undefined") return "light";
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { SlotId } from "@stanza/registry";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
import { CommandBar } from "@/components/builder/command-bar";
|
||||
import { FilePreview } from "@/components/builder/file-preview";
|
||||
import { SlotCards } from "@/components/builder/slot-cards";
|
||||
import {
|
||||
type BuilderSearch,
|
||||
type Selections,
|
||||
buildCommand,
|
||||
DEFAULT_NAME,
|
||||
parseSelections,
|
||||
resolveSelectedAdapters,
|
||||
toSearchParams,
|
||||
} from "@/lib/selection";
|
||||
import type { BuilderState } from "@/server/builder-state";
|
||||
|
||||
export function Builder({ state, search }: { state: BuilderState; search: BuilderSearch }) {
|
||||
const navigate = useNavigate({ from: "/" });
|
||||
const { name, selections } = parseSelections(search);
|
||||
const resolved = resolveSelectedAdapters(state.modules, selections);
|
||||
const command = buildCommand({ name, selections });
|
||||
|
||||
const setName = (next: string) => {
|
||||
void navigate({
|
||||
search: toSearchParams({ name: next, selections }),
|
||||
replace: true,
|
||||
});
|
||||
};
|
||||
|
||||
const setSelection = (slot: SlotId, id: string | undefined) => {
|
||||
const next: Selections = { ...selections };
|
||||
if (id) next[slot] = id;
|
||||
else delete next[slot];
|
||||
void navigate({
|
||||
search: toSearchParams({ name, selections: next }),
|
||||
replace: true,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid gap-10 lg:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)]">
|
||||
<section className="space-y-8">
|
||||
<SlotCards
|
||||
modules={state.modules}
|
||||
summaries={state.index.modules}
|
||||
selections={selections}
|
||||
onSelect={setSelection}
|
||||
/>
|
||||
</section>
|
||||
<section className="space-y-6 lg:sticky lg:top-20 lg:self-start">
|
||||
<CommandBar
|
||||
name={name}
|
||||
defaultName={DEFAULT_NAME}
|
||||
command={command}
|
||||
onNameChange={setName}
|
||||
/>
|
||||
<FilePreview filePaths={state.filePaths} previews={state.previews} resolved={resolved} />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
import type { Logo, Module, ModuleSummary, SlotId } from "@stanza/registry";
|
||||
import { KNOWN_SLOTS, emptyManifest, resolveAdapter } from "@stanza/registry";
|
||||
import { IconCheck } from "@tabler/icons-react";
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import type { Selections } from "@/lib/selection";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const SLOT_LABELS: Record<SlotId, string> = {
|
||||
framework: "Framework",
|
||||
styling: "Styling",
|
||||
db: "Database",
|
||||
orm: "ORM",
|
||||
auth: "Auth",
|
||||
};
|
||||
|
||||
export function SlotCards({
|
||||
modules,
|
||||
summaries,
|
||||
selections,
|
||||
onSelect,
|
||||
}: {
|
||||
modules: Record<string, Module>;
|
||||
summaries: ModuleSummary[];
|
||||
selections: Selections;
|
||||
onSelect: (slot: SlotId, id: string | undefined) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{KNOWN_SLOTS.map((slot, index) => {
|
||||
const slotModules = summaries.filter((m) => m.slot === slot);
|
||||
if (slotModules.length === 0) return null;
|
||||
return (
|
||||
<SlotSection
|
||||
key={slot}
|
||||
slot={slot}
|
||||
label={SLOT_LABELS[slot]}
|
||||
modules={slotModules}
|
||||
modulesById={modules}
|
||||
selections={selections}
|
||||
index={index + 1}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SlotSection({
|
||||
slot,
|
||||
label,
|
||||
modules,
|
||||
modulesById,
|
||||
selections,
|
||||
index,
|
||||
onSelect,
|
||||
}: {
|
||||
slot: SlotId;
|
||||
label: string;
|
||||
modules: ModuleSummary[];
|
||||
modulesById: Record<string, Module>;
|
||||
selections: Selections;
|
||||
index: number;
|
||||
onSelect: (slot: SlotId, id: string | undefined) => void;
|
||||
}) {
|
||||
const pending: Partial<Record<SlotId, Module>> = {};
|
||||
for (const s of KNOWN_SLOTS) {
|
||||
const id = selections[s];
|
||||
if (id && modulesById[`${s}:${id}`]) pending[s] = modulesById[`${s}:${id}`];
|
||||
}
|
||||
const manifest = emptyManifest({ name: "t" });
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-3 flex items-baseline gap-3">
|
||||
<span className="font-mono text-xs text-muted-foreground tabular-nums">
|
||||
{String(index).padStart(2, "0")}
|
||||
</span>
|
||||
<h2 className="text-lg font-semibold tracking-tight">{label}</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{modules.map((m) => {
|
||||
const full = modulesById[`${m.slot}:${m.id}`];
|
||||
const result = full
|
||||
? resolveAdapter(full, { manifest, pending })
|
||||
: { ok: false, error: { kind: "no-adapter" as const } };
|
||||
const disabled = !result.ok;
|
||||
const selected = selections[slot] === m.id;
|
||||
const reason = !result.ok ? describeError(result.error.kind) : undefined;
|
||||
return (
|
||||
<ModuleCard
|
||||
key={m.id}
|
||||
module={m}
|
||||
selected={selected}
|
||||
disabled={disabled}
|
||||
reason={reason}
|
||||
onClick={() => {
|
||||
if (disabled) return;
|
||||
onSelect(slot, selected ? undefined : m.id);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ModuleCard({
|
||||
module: m,
|
||||
selected,
|
||||
disabled,
|
||||
reason,
|
||||
onClick,
|
||||
}: {
|
||||
module: ModuleSummary;
|
||||
selected: boolean;
|
||||
disabled: boolean;
|
||||
reason?: string;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
const card = (
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
aria-pressed={selected}
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"relative flex flex-col gap-3 rounded-xl border border-border bg-card p-4 text-left text-card-foreground shadow-sm transition-colors",
|
||||
"outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
||||
!disabled && "cursor-pointer hover:bg-accent/40",
|
||||
selected && "border-foreground ring-1 ring-foreground",
|
||||
disabled && "cursor-not-allowed opacity-50",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<ModuleLogo logo={m.logo} label={m.label} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<h3 className="text-sm leading-tight font-semibold">{m.label}</h3>
|
||||
{selected && <IconCheck className="size-4 shrink-0 text-foreground" aria-hidden />}
|
||||
</div>
|
||||
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">{m.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="font-mono text-[10px] text-muted-foreground/70">
|
||||
{m.slot}/{m.id} <span className="text-muted-foreground/50">·</span> v{m.version}
|
||||
</p>
|
||||
</button>
|
||||
);
|
||||
|
||||
if (!disabled || !reason) return card;
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={card} />
|
||||
<TooltipContent side="top">{reason}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function ModuleLogo({ logo, label }: { logo: Logo | undefined; label: string }) {
|
||||
if (!logo) {
|
||||
// Fallback: a subtle initial-letter tile. Keeps card geometry consistent.
|
||||
return (
|
||||
<div
|
||||
aria-hidden
|
||||
className="flex size-9 shrink-0 items-center justify-center rounded-md border border-border bg-muted/40 text-xs font-semibold text-muted-foreground"
|
||||
>
|
||||
{label.slice(0, 1)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const wrapClass =
|
||||
"flex size-9 shrink-0 items-center justify-center [&_svg]:size-7 [&_svg]:max-h-7 [&_svg]:max-w-7";
|
||||
if (typeof logo === "string") {
|
||||
return (
|
||||
<div
|
||||
aria-hidden
|
||||
className={wrapClass}
|
||||
// Inlined SVG from our trusted first-party registry payload.
|
||||
dangerouslySetInnerHTML={{ __html: logo }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
aria-hidden
|
||||
className={cn(wrapClass, "dark:hidden")}
|
||||
dangerouslySetInnerHTML={{ __html: logo.light }}
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className={cn(wrapClass, "hidden dark:flex")}
|
||||
dangerouslySetInnerHTML={{ __html: logo.dark }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function describeError(kind: string): string {
|
||||
switch (kind) {
|
||||
case "missing-peer":
|
||||
return "Needs another slot filled in first.";
|
||||
case "incompatible-peer":
|
||||
return "Doesn't pair with one of your current picks.";
|
||||
case "no-adapter":
|
||||
return "No adapter matches your current stack.";
|
||||
default:
|
||||
return "Not compatible with the current stack.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="border-t border-border bg-background">
|
||||
<div className="mx-auto flex max-w-6xl flex-col items-center justify-between gap-2 px-4 py-6 text-sm text-muted-foreground sm:flex-row sm:px-6">
|
||||
<p>stanza — modular monorepo template CLI</p>
|
||||
<nav className="flex items-center gap-4">
|
||||
<a
|
||||
href="https://github.com/jakejarvis/stanza"
|
||||
className="hover:text-foreground"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { IconBrandGithub } from "@tabler/icons-react";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
|
||||
import { ModeToggle } from "@/components/mode-toggle";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export function Header() {
|
||||
return (
|
||||
<header className="sticky top-0 z-40 border-b border-border bg-background/80 backdrop-blur">
|
||||
<div className="mx-auto flex h-14 max-w-6xl items-center justify-between px-4 sm:px-6">
|
||||
<Link to="/" className="font-semibold tracking-tight">
|
||||
stanza
|
||||
</Link>
|
||||
<nav className="flex items-center gap-1">
|
||||
<Button
|
||||
render={
|
||||
<a
|
||||
href="https://github.com/jakejarvis/stanza"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label="GitHub"
|
||||
/>
|
||||
}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
>
|
||||
<IconBrandGithub className="size-4" />
|
||||
<span className="hidden sm:inline">GitHub</span>
|
||||
</Button>
|
||||
<ModeToggle />
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { IconMoon, IconSun } from "@tabler/icons-react";
|
||||
|
||||
import { useTheme } from "@/components/theme-provider";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
export function ModeToggle() {
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button variant="outline" size="icon" aria-label="Toggle theme">
|
||||
<IconSun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
|
||||
<IconMoon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>Light</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>Dark</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>System</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { ScriptOnce } from "@tanstack/react-router";
|
||||
import { createContext, use, useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
type Theme = "dark" | "light" | "system";
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: React.ReactNode;
|
||||
defaultTheme?: Theme;
|
||||
storageKey?: string;
|
||||
};
|
||||
|
||||
type ThemeProviderState = {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
};
|
||||
|
||||
const ThemeProviderContext = createContext<ThemeProviderState>({
|
||||
theme: "system",
|
||||
setTheme: () => {},
|
||||
});
|
||||
|
||||
function getThemeScript(storageKey: string, defaultTheme: Theme) {
|
||||
const key = JSON.stringify(storageKey);
|
||||
const fallback = JSON.stringify(defaultTheme);
|
||||
// This script runs before React hydrates so the first paint already has the
|
||||
// right `.light` / `.dark` class on <html>. Keeps the FOUC away on reload.
|
||||
return `(function(){try{var t=localStorage.getItem(${key});if(t!=='light'&&t!=='dark'&&t!=='system'){t=${fallback}}var d=matchMedia('(prefers-color-scheme: dark)').matches;var r=t==='system'?(d?'dark':'light'):t;var e=document.documentElement;e.classList.add(r);e.style.colorScheme=r}catch(e){}})();`;
|
||||
}
|
||||
|
||||
function applyTheme(theme: Theme) {
|
||||
const root = document.documentElement;
|
||||
root.classList.remove("light", "dark");
|
||||
const resolved =
|
||||
theme === "system"
|
||||
? window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: theme;
|
||||
root.classList.add(resolved);
|
||||
root.style.colorScheme = resolved;
|
||||
}
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = "system",
|
||||
storageKey = "theme",
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setThemeState] = useState<Theme>(defaultTheme);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const stored = localStorage.getItem(storageKey);
|
||||
setThemeState(
|
||||
stored === "light" || stored === "dark" || stored === "system" ? stored : defaultTheme,
|
||||
);
|
||||
setMounted(true);
|
||||
}, [defaultTheme, storageKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mounted) return;
|
||||
applyTheme(theme);
|
||||
}, [theme, mounted]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mounted || theme !== "system") return;
|
||||
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const onChange = () => applyTheme("system");
|
||||
media.addEventListener("change", onChange);
|
||||
return () => media.removeEventListener("change", onChange);
|
||||
}, [theme, mounted]);
|
||||
|
||||
const setTheme = useCallback(
|
||||
(next: Theme) => {
|
||||
localStorage.setItem(storageKey, next);
|
||||
setThemeState(next);
|
||||
},
|
||||
[storageKey],
|
||||
);
|
||||
|
||||
const value = useMemo(() => ({ theme, setTheme }), [theme, setTheme]);
|
||||
|
||||
return (
|
||||
<ThemeProviderContext value={value}>
|
||||
<ScriptOnce>{getThemeScript(storageKey, defaultTheme)}</ScriptOnce>
|
||||
{children}
|
||||
</ThemeProviderContext>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const context = use(ThemeProviderContext);
|
||||
if (context === undefined) throw new Error("useTheme must be used within a ThemeProvider");
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { mergeProps } from "@base-ui/react/merge-props";
|
||||
import { useRender } from "@base-ui/react/use-render";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-none border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
|
||||
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
||||
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant = "default",
|
||||
render,
|
||||
...props
|
||||
}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
|
||||
return useRender({
|
||||
defaultTagName: "span",
|
||||
props: mergeProps<"span">(
|
||||
{
|
||||
className: cn(badgeVariants({ variant }), className),
|
||||
},
|
||||
props,
|
||||
),
|
||||
render,
|
||||
state: {
|
||||
slot: "badge",
|
||||
variant,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
@@ -0,0 +1,92 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Card({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/card flex flex-col gap-4 overflow-hidden rounded-none bg-card py-4 text-xs/relaxed text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-2 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-none *:[img:last-child]:rounded-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-none px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn(
|
||||
"font-heading text-sm font-medium group-data-[size=sm]/card:text-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-xs/relaxed text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn(
|
||||
"flex items-center rounded-none border-t p-4 group-data-[size=sm]/card:p-3",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };
|
||||
@@ -0,0 +1,253 @@
|
||||
import { Menu as MenuPrimitive } from "@base-ui/react/menu";
|
||||
import { IconChevronRight, IconCheck } from "@tabler/icons-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
|
||||
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {
|
||||
return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {
|
||||
return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
align = "start",
|
||||
alignOffset = 0,
|
||||
side = "bottom",
|
||||
sideOffset = 4,
|
||||
className,
|
||||
...props
|
||||
}: MenuPrimitive.Popup.Props &
|
||||
Pick<MenuPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">) {
|
||||
return (
|
||||
<MenuPrimitive.Portal>
|
||||
<MenuPrimitive.Positioner
|
||||
className="isolate z-50 outline-none"
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
>
|
||||
<MenuPrimitive.Popup
|
||||
data-slot="dropdown-menu-content"
|
||||
className={cn(
|
||||
"relative z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) animate-none! overflow-x-hidden overflow-y-auto rounded-none bg-popover/70 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:**:text-accent-foreground! **:data-[variant=destructive]:text-accent-foreground! **:data-[variant=destructive]:focus:bg-foreground/10! data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:overflow-hidden data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</MenuPrimitive.Positioner>
|
||||
</MenuPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {
|
||||
return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: MenuPrimitive.GroupLabel.Props & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<MenuPrimitive.GroupLabel
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn("px-2 py-2 text-xs text-muted-foreground data-inset:pl-7", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: MenuPrimitive.Item.Props & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<MenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"group/dropdown-menu-item relative flex cursor-default items-center gap-2 rounded-none px-2 py-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {
|
||||
return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: MenuPrimitive.SubmenuTrigger.Props & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<MenuPrimitive.SubmenuTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"flex cursor-default items-center gap-2 rounded-none px-2 py-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-popup-open:bg-accent data-popup-open:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<IconChevronRight className="ml-auto" />
|
||||
</MenuPrimitive.SubmenuTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
align = "start",
|
||||
alignOffset = -3,
|
||||
side = "right",
|
||||
sideOffset = 0,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuContent>) {
|
||||
return (
|
||||
<DropdownMenuContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"relative w-auto min-w-[96px] animate-none! rounded-none bg-popover/70 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:**:text-accent-foreground! **:data-[variant=destructive]:text-accent-foreground! **:data-[variant=destructive]:focus:bg-foreground/10! data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
className,
|
||||
)}
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
inset,
|
||||
...props
|
||||
}: MenuPrimitive.CheckboxItem.Props & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<MenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-none py-2 pr-8 pl-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
className="pointer-events-none absolute right-2 flex items-center justify-center"
|
||||
data-slot="dropdown-menu-checkbox-item-indicator"
|
||||
>
|
||||
<MenuPrimitive.CheckboxItemIndicator>
|
||||
<IconCheck />
|
||||
</MenuPrimitive.CheckboxItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenuPrimitive.CheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {
|
||||
return <MenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
inset,
|
||||
...props
|
||||
}: MenuPrimitive.RadioItem.Props & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<MenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-none py-2 pr-8 pl-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
className="pointer-events-none absolute right-2 flex items-center justify-center"
|
||||
data-slot="dropdown-menu-radio-item-indicator"
|
||||
>
|
||||
<MenuPrimitive.RadioItemIndicator>
|
||||
<IconCheck />
|
||||
</MenuPrimitive.RadioItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenuPrimitive.RadioItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({ className, ...props }: MenuPrimitive.Separator.Props) {
|
||||
return (
|
||||
<MenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("-mx-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Input as InputPrimitive } from "@base-ui/react/input";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<InputPrimitive
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"h-8 w-full min-w-0 rounded-none border border-input bg-transparent px-2.5 py-1 text-xs transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-xs file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 md:text-xs dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Input };
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Separator({ className, orientation = "horizontal", ...props }: SeparatorPrimitive.Props) {
|
||||
return (
|
||||
<SeparatorPrimitive
|
||||
data-slot="separator"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Separator };
|
||||
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
IconCircleCheck,
|
||||
IconInfoCircle,
|
||||
IconAlertTriangle,
|
||||
IconAlertOctagon,
|
||||
IconLoader,
|
||||
} from "@tabler/icons-react";
|
||||
import { Toaster as Sonner, type ToasterProps } from "sonner";
|
||||
|
||||
import { useTheme } from "@/components/theme-provider";
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme();
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
icons={{
|
||||
success: <IconCircleCheck className="size-4" />,
|
||||
info: <IconInfoCircle className="size-4" />,
|
||||
warning: <IconAlertTriangle className="size-4" />,
|
||||
error: <IconAlertOctagon className="size-4" />,
|
||||
loading: <IconLoader className="size-4 animate-spin" />,
|
||||
}}
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
"--border-radius": "var(--radius)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast: "cn-toast",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { Toaster };
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Tabs({ className, orientation = "horizontal", ...props }: TabsPrimitive.Root.Props) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
data-orientation={orientation}
|
||||
className={cn("group/tabs flex gap-2 data-horizontal:flex-col", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const tabsListVariants = cva(
|
||||
"group/tabs-list inline-flex w-fit items-center justify-center rounded-none p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-muted",
|
||||
line: "gap-1 bg-transparent",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
variant = "default",
|
||||
...props
|
||||
}: TabsPrimitive.List.Props & VariantProps<typeof tabsListVariants>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
data-variant={variant}
|
||||
className={cn(tabsListVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
|
||||
return (
|
||||
<TabsPrimitive.Tab
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-none border border-transparent px-1.5 py-0.5 text-xs font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start group-data-vertical/tabs:py-[calc(--spacing(1.25))] hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
|
||||
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
|
||||
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {
|
||||
return (
|
||||
<TabsPrimitive.Panel
|
||||
data-slot="tabs-content"
|
||||
className={cn("flex-1 text-xs/relaxed outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };
|
||||
@@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
|
||||
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function TooltipProvider({ delay = 0, ...props }: TooltipPrimitive.Provider.Props) {
|
||||
return <TooltipPrimitive.Provider data-slot="tooltip-provider" delay={delay} {...props} />;
|
||||
}
|
||||
|
||||
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
|
||||
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />;
|
||||
}
|
||||
|
||||
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
side = "top",
|
||||
sideOffset = 4,
|
||||
align = "center",
|
||||
alignOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: TooltipPrimitive.Popup.Props &
|
||||
Pick<TooltipPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Positioner
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
className="isolate z-50"
|
||||
>
|
||||
<TooltipPrimitive.Popup
|
||||
data-slot="tooltip-content"
|
||||
className={cn(
|
||||
"z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-none bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-none data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-none bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
|
||||
</TooltipPrimitive.Popup>
|
||||
</TooltipPrimitive.Positioner>
|
||||
</TooltipPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
@@ -0,0 +1,161 @@
|
||||
import { defineModule, type Module } from "@stanza/registry";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
buildCommand,
|
||||
DEFAULT_NAME,
|
||||
parseSelections,
|
||||
resolveSelectedAdapters,
|
||||
selectedFiles,
|
||||
toSearchParams,
|
||||
} from "./selection";
|
||||
|
||||
describe("parseSelections", () => {
|
||||
it("returns defaults when search is empty", () => {
|
||||
const { name, selections } = parseSelections({});
|
||||
expect(name).toBe(DEFAULT_NAME);
|
||||
expect(selections).toEqual({});
|
||||
});
|
||||
|
||||
it("preserves recognized slot keys and ignores unknown keys", () => {
|
||||
const { name, selections } = parseSelections({
|
||||
name: "my-thing",
|
||||
framework: "next",
|
||||
orm: "drizzle",
|
||||
// @ts-expect-error unknown slot — should be silently dropped
|
||||
flooble: "ignored",
|
||||
});
|
||||
expect(name).toBe("my-thing");
|
||||
expect(selections).toEqual({ framework: "next", orm: "drizzle" });
|
||||
});
|
||||
|
||||
it("drops empty string slot values", () => {
|
||||
const { selections } = parseSelections({ framework: "" });
|
||||
expect(selections.framework).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("toSearchParams", () => {
|
||||
it("round-trips a populated state", () => {
|
||||
const input = {
|
||||
name: "my-app",
|
||||
selections: { framework: "next" as const, orm: "drizzle" as const },
|
||||
};
|
||||
const search = toSearchParams(input);
|
||||
expect(parseSelections(search)).toEqual(input);
|
||||
});
|
||||
|
||||
it("omits the default name to keep the URL terse", () => {
|
||||
expect(toSearchParams({ name: DEFAULT_NAME, selections: {} })).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildCommand", () => {
|
||||
it("emits only the selected flags", () => {
|
||||
expect(
|
||||
buildCommand({
|
||||
name: "my-app",
|
||||
selections: { framework: "next", db: "sqlite" },
|
||||
}),
|
||||
).toBe("pnpm create stanza my-app --framework=next --db=sqlite");
|
||||
});
|
||||
|
||||
it("keeps the bare command when nothing is selected", () => {
|
||||
expect(buildCommand({ name: "my-app", selections: {} })).toBe("pnpm create stanza my-app");
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectedFiles", () => {
|
||||
const drizzle: Module = defineModule({
|
||||
id: "drizzle",
|
||||
slot: "orm",
|
||||
label: "Drizzle",
|
||||
description: "",
|
||||
version: "0.1.0",
|
||||
adapters: [
|
||||
{
|
||||
key: "default",
|
||||
match: {},
|
||||
templates: [
|
||||
{ src: "schema.ts", dest: "src/db/schema.ts", scope: "app" },
|
||||
{ src: "drizzle.config.ts", dest: "drizzle.config.ts", scope: "app" },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
it("prefixes scope:app with the active app dir and leaves scope:repo bare", () => {
|
||||
const adapter = drizzle.adapters[0]!;
|
||||
// Add a repo-scoped template to the same fixture
|
||||
const adapterWithRepo = {
|
||||
...adapter,
|
||||
templates: [
|
||||
...(adapter.templates ?? []),
|
||||
{ src: "turbo.json", dest: "turbo.json", scope: "repo" as const },
|
||||
],
|
||||
};
|
||||
const files = selectedFiles({
|
||||
orm: { module: drizzle, adapter: adapterWithRepo },
|
||||
});
|
||||
expect(files.map((f) => f.path)).toEqual([
|
||||
"apps/web/src/db/schema.ts",
|
||||
"apps/web/drizzle.config.ts",
|
||||
"turbo.json",
|
||||
]);
|
||||
});
|
||||
|
||||
it("groups files by slot order", () => {
|
||||
const framework: Module = defineModule({
|
||||
id: "next",
|
||||
slot: "framework",
|
||||
label: "Next.js",
|
||||
description: "",
|
||||
version: "0.1.0",
|
||||
adapters: [
|
||||
{
|
||||
key: "default",
|
||||
match: {},
|
||||
templates: [{ src: "layout.tsx", dest: "app/layout.tsx", scope: "app" }],
|
||||
},
|
||||
],
|
||||
});
|
||||
const files = selectedFiles({
|
||||
orm: { module: drizzle, adapter: drizzle.adapters[0]! },
|
||||
framework: { module: framework, adapter: framework.adapters[0]! },
|
||||
});
|
||||
// framework comes before orm in slotOrder, so its files should appear first
|
||||
expect(files[0]!.path).toBe("apps/web/app/layout.tsx");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveSelectedAdapters", () => {
|
||||
it("picks the adapter whose match aligns with active peers", () => {
|
||||
const drizzleMod: Module = defineModule({
|
||||
id: "drizzle",
|
||||
slot: "orm",
|
||||
label: "Drizzle",
|
||||
description: "",
|
||||
version: "0.1.0",
|
||||
peers: { db: ["postgres", "sqlite"] },
|
||||
adapters: [
|
||||
{ key: "postgres", match: { db: "postgres" } },
|
||||
{ key: "sqlite", match: { db: "sqlite" } },
|
||||
],
|
||||
});
|
||||
const postgresMod: Module = defineModule({
|
||||
id: "postgres",
|
||||
slot: "db",
|
||||
label: "Postgres",
|
||||
description: "",
|
||||
version: "0.1.0",
|
||||
adapters: [{ key: "default", match: {} }],
|
||||
});
|
||||
const modules = {
|
||||
"orm:drizzle": drizzleMod,
|
||||
"db:postgres": postgresMod,
|
||||
};
|
||||
const out = resolveSelectedAdapters(modules, { orm: "drizzle", db: "postgres" });
|
||||
expect(out.orm?.adapter.key).toBe("postgres");
|
||||
expect(out.db?.adapter.key).toBe("default");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
import type { Module, ModuleAdapter, SlotId, TemplateRef } from "@stanza/registry";
|
||||
import { KNOWN_SLOTS, emptyManifest, resolveAdapter, slotOrder } from "@stanza/registry";
|
||||
|
||||
export type Selections = Partial<Record<SlotId, string>>;
|
||||
|
||||
export type BuilderSearch = {
|
||||
name?: string;
|
||||
framework?: string;
|
||||
styling?: string;
|
||||
db?: string;
|
||||
orm?: string;
|
||||
auth?: string;
|
||||
};
|
||||
|
||||
export const DEFAULT_NAME = "my-app";
|
||||
|
||||
/**
|
||||
* Parse the URL search params into a name + selections record. Any unrecognized
|
||||
* slot keys are dropped. Missing values default to `undefined`, which the rest
|
||||
* of the builder treats as "not picked yet".
|
||||
*/
|
||||
export function parseSelections(search: BuilderSearch): { name: string; selections: Selections } {
|
||||
const selections: Selections = {};
|
||||
for (const slot of KNOWN_SLOTS) {
|
||||
const value = search[slot];
|
||||
if (typeof value === "string" && value.length > 0) {
|
||||
selections[slot] = value;
|
||||
}
|
||||
}
|
||||
const name =
|
||||
typeof search.name === "string" && search.name.length > 0 ? search.name : DEFAULT_NAME;
|
||||
return { name, selections };
|
||||
}
|
||||
|
||||
/**
|
||||
* Inverse of `parseSelections`. We omit empty fields so the URL stays terse —
|
||||
* a brand-new visit shows no query string, not `?name=my-app&framework=`.
|
||||
*/
|
||||
export function toSearchParams(input: { name: string; selections: Selections }): BuilderSearch {
|
||||
const out: BuilderSearch = {};
|
||||
if (input.name && input.name !== DEFAULT_NAME) out.name = input.name;
|
||||
for (const slot of KNOWN_SLOTS) {
|
||||
const v = input.selections[slot];
|
||||
if (v) out[slot] = v;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the adapter for each selected slot against the current selections —
|
||||
* same logic the CLI uses on `stanza add`. Returns `null` for a slot whose
|
||||
* peers aren't satisfied yet (rare while typing).
|
||||
*/
|
||||
export function resolveSelectedAdapters(
|
||||
modules: Record<string, Module>,
|
||||
selections: Selections,
|
||||
): Partial<Record<SlotId, { module: Module; adapter: ModuleAdapter }>> {
|
||||
const pending: Partial<Record<SlotId, Module>> = {};
|
||||
for (const slot of KNOWN_SLOTS) {
|
||||
const id = selections[slot];
|
||||
if (!id) continue;
|
||||
const mod = modules[`${slot}:${id}`];
|
||||
if (mod) pending[slot] = mod;
|
||||
}
|
||||
const out: Partial<Record<SlotId, { module: Module; adapter: ModuleAdapter }>> = {};
|
||||
for (const slot of slotOrder) {
|
||||
const mod = pending[slot];
|
||||
if (!mod) continue;
|
||||
const r = resolveAdapter(mod, { manifest: emptyManifest({ name: "t" }), pending });
|
||||
if (r.ok) out[slot] = { module: mod, adapter: r.adapter };
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export type SelectedFile = {
|
||||
path: string;
|
||||
template: TemplateRef;
|
||||
owner: { slot: SlotId; module: string };
|
||||
};
|
||||
|
||||
/**
|
||||
* Derive the full file list stanza will write for the current selection.
|
||||
* Mirrors codemod-runner's resolution: `scope: "app"` files are prefixed by
|
||||
* the active app dir (we default to `apps/web/`); `scope: "repo"` files land
|
||||
* at the repo root. Returns a deterministic order grouped by slot order.
|
||||
*/
|
||||
export function selectedFiles(
|
||||
resolved: Partial<Record<SlotId, { module: Module; adapter: ModuleAdapter }>>,
|
||||
appDir = "apps/web",
|
||||
): SelectedFile[] {
|
||||
const out: SelectedFile[] = [];
|
||||
for (const slot of slotOrder) {
|
||||
const entry = resolved[slot];
|
||||
if (!entry) continue;
|
||||
for (const tpl of entry.adapter.templates ?? []) {
|
||||
const path = tpl.scope === "repo" ? tpl.dest : `${appDir.replace(/\/$/, "")}/${tpl.dest}`;
|
||||
out.push({ path, template: tpl, owner: { slot, module: entry.module.id } });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the `pnpm create stanza` command from the current state. Used both by
|
||||
* the command-bar display and by the copy-to-clipboard action.
|
||||
*/
|
||||
export function buildCommand(input: { name: string; selections: Selections }): string {
|
||||
const flags = slotOrder
|
||||
.map((slot) => {
|
||||
const v = input.selections[slot];
|
||||
return v ? `--${slot}=${v}` : null;
|
||||
})
|
||||
.filter((s): s is string => Boolean(s));
|
||||
return `pnpm create stanza ${input.name}${flags.length ? " " + flags.join(" ") : ""}`;
|
||||
}
|
||||
@@ -8,62 +8,61 @@
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from "./routes/__root";
|
||||
import { Route as IndexRouteImport } from "./routes/index";
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: "/",
|
||||
path: "/",
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
"/": typeof IndexRoute;
|
||||
'/': typeof IndexRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
"/": typeof IndexRoute;
|
||||
'/': typeof IndexRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport;
|
||||
"/": typeof IndexRoute;
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath;
|
||||
fullPaths: "/";
|
||||
fileRoutesByTo: FileRoutesByTo;
|
||||
to: "/";
|
||||
id: "__root__" | "/";
|
||||
fileRoutesById: FileRoutesById;
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/'
|
||||
id: '__root__' | '/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute;
|
||||
IndexRoute: typeof IndexRoute
|
||||
}
|
||||
|
||||
declare module "@tanstack/react-router" {
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
"/": {
|
||||
id: "/";
|
||||
path: "/";
|
||||
fullPath: "/";
|
||||
preLoaderRoute: typeof IndexRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
};
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>();
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
|
||||
import type { createStart } from "@tanstack/react-start";
|
||||
|
||||
import type { getRouter } from "./router.tsx";
|
||||
declare module "@tanstack/react-start" {
|
||||
import type { getRouter } from './router.tsx'
|
||||
import type { createStart } from '@tanstack/react-start'
|
||||
declare module '@tanstack/react-start' {
|
||||
interface Register {
|
||||
ssr: true;
|
||||
router: Awaited<ReturnType<typeof getRouter>>;
|
||||
ssr: true
|
||||
router: Awaited<ReturnType<typeof getRouter>>
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,12 @@ import { TanStackDevtools } from "@tanstack/react-devtools";
|
||||
import { HeadContent, Outlet, Scripts, createRootRoute } from "@tanstack/react-router";
|
||||
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
|
||||
|
||||
import { Footer } from "@/components/footer";
|
||||
import { Header } from "@/components/header";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
|
||||
import appCss from "../styles.css?url";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
@@ -12,34 +18,33 @@ export const Route = createRootRoute({
|
||||
{ title: "stanza" },
|
||||
{ name: "description", content: "Modular monorepo template builder." },
|
||||
],
|
||||
links: [
|
||||
{
|
||||
rel: "stylesheet",
|
||||
href: appCss,
|
||||
},
|
||||
],
|
||||
links: [{ rel: "stylesheet", href: appCss }],
|
||||
}),
|
||||
component: RootComponent,
|
||||
});
|
||||
|
||||
function RootComponent() {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body>
|
||||
<body className="min-h-svh bg-background font-sans text-foreground antialiased">
|
||||
<ThemeProvider defaultTheme="system" storageKey="stanza-theme">
|
||||
<TooltipProvider>
|
||||
<div className="flex min-h-svh flex-col">
|
||||
<Header />
|
||||
<main className="flex-1">
|
||||
<Outlet />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
<Toaster />
|
||||
</TooltipProvider>
|
||||
</ThemeProvider>
|
||||
<TanStackDevtools
|
||||
config={{
|
||||
position: "bottom-right",
|
||||
}}
|
||||
plugins={[
|
||||
{
|
||||
name: "Tanstack Router",
|
||||
render: <TanStackRouterDevtoolsPanel />,
|
||||
},
|
||||
]}
|
||||
config={{ position: "bottom-right" }}
|
||||
plugins={[{ name: "Tanstack Router", render: <TanStackRouterDevtoolsPanel /> }]}
|
||||
/>
|
||||
<Scripts />
|
||||
</body>
|
||||
|
||||
@@ -1,30 +1,38 @@
|
||||
import type { RegistryIndex } from "@stanza/registry";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
|
||||
import { Builder } from "@/components/builder";
|
||||
import type { BuilderSearch } from "@/lib/selection";
|
||||
import { getBuilderState } from "@/server/builder-state";
|
||||
|
||||
const getRegistryIndex = createServerFn({ method: "GET" }).handler(async () => {
|
||||
const url = process.env.STANZA_REGISTRY ?? "https://stanza.dev/registry";
|
||||
const res = await fetch(`${url}/index.json`);
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to load stanza registry: ${res.status}`);
|
||||
function validateSearch(input: Record<string, unknown>): BuilderSearch {
|
||||
const out: BuilderSearch = {};
|
||||
for (const key of ["name", "framework", "styling", "db", "orm", "auth"] as const) {
|
||||
const v = input[key];
|
||||
if (typeof v === "string" && v.length > 0) out[key] = v;
|
||||
}
|
||||
return (await res.json()) as RegistryIndex;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
loader: () => getRegistryIndex(),
|
||||
validateSearch,
|
||||
loaderDeps: ({ search }) => search,
|
||||
loader: ({ deps }) => getBuilderState({ data: deps }),
|
||||
component: Page,
|
||||
});
|
||||
|
||||
function Page() {
|
||||
const index = Route.useLoaderData();
|
||||
const state = Route.useLoaderData();
|
||||
const search = Route.useSearch();
|
||||
return (
|
||||
<main style={{ maxWidth: "48rem", margin: "4rem auto", padding: "0 1rem" }}>
|
||||
<h1>stanza</h1>
|
||||
<p>Pick your stack. Get an idiomatic monorepo.</p>
|
||||
<Builder index={index} />
|
||||
</main>
|
||||
<div className="mx-auto max-w-6xl px-4 py-10 sm:px-6">
|
||||
<header className="mb-10 max-w-2xl">
|
||||
<h1 className="text-3xl font-semibold tracking-tight">Build your stack</h1>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
Pick a framework, ORM, database, auth provider, and styling. Get a clean monorepo with
|
||||
idiomatic, vendored code. Add modules later with <code>stanza add</code>.
|
||||
</p>
|
||||
</header>
|
||||
<Builder state={state} search={search} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { Module, RegistryIndex } from "@stanza/registry";
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
|
||||
import { parseSelections, resolveSelectedAdapters, selectedFiles } from "@/lib/selection";
|
||||
import type { BuilderSearch } from "@/lib/selection";
|
||||
import { renderPreview, type Preview } from "@/server/highlighter";
|
||||
|
||||
const REGISTRY_BASE = process.env.STANZA_REGISTRY ?? "/registry";
|
||||
|
||||
export type BuilderState = {
|
||||
index: RegistryIndex;
|
||||
/** Keyed by `${slot}:${id}` for direct lookup. */
|
||||
modules: Record<string, Module>;
|
||||
/** Pre-rendered Shiki HTML, keyed by file path (relative to repo root). */
|
||||
previews: Record<string, Preview>;
|
||||
/** Ordered list of file paths derived from current selections. */
|
||||
filePaths: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Server function that powers the builder. Runs on every search-param change
|
||||
* via the route's `loaderDeps`. Pulls the registry, derives the selected file
|
||||
* list, and pre-renders Shiki HTML for each — keeping shiki off the client.
|
||||
*/
|
||||
export const getBuilderState = createServerFn({ method: "GET" })
|
||||
.inputValidator((data: BuilderSearch) => data)
|
||||
.handler(async ({ data }): Promise<BuilderState> => {
|
||||
const base = await resolveBase();
|
||||
|
||||
const indexRes = await fetch(`${base}/index.json`);
|
||||
if (!indexRes.ok) {
|
||||
throw new Error(`Failed to load stanza registry index: ${indexRes.status}`);
|
||||
}
|
||||
const index = (await indexRes.json()) as RegistryIndex;
|
||||
|
||||
// Fetch every module in parallel. The dataset is small (10s of KB at most)
|
||||
// and having everything client-side lets the user toggle slots without
|
||||
// extra roundtrips. The Shiki preview map is the only piece that changes
|
||||
// per-selection — recomputed each loader run.
|
||||
const fullModules = await Promise.all(
|
||||
index.modules.map(async (summary) => {
|
||||
const url = `${base}/modules/${summary.slot}-${summary.id}.json`;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(`Module fetch failed: ${url} (${res.status})`);
|
||||
const mod = (await res.json()) as Module;
|
||||
return [`${mod.slot}:${mod.id}`, mod] as const;
|
||||
}),
|
||||
);
|
||||
const modules: Record<string, Module> = Object.fromEntries(fullModules);
|
||||
|
||||
const { selections } = parseSelections(data);
|
||||
const resolved = resolveSelectedAdapters(modules, selections);
|
||||
const files = selectedFiles(resolved);
|
||||
|
||||
const previewEntries = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const content = file.template.content ?? "";
|
||||
const preview = await renderPreview(content, file.path);
|
||||
return [file.path, preview] as const;
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
index,
|
||||
modules,
|
||||
previews: Object.fromEntries(previewEntries),
|
||||
filePaths: files.map((f) => f.path),
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* `STANZA_REGISTRY` overrides the base. Without an override we hit the
|
||||
* same-domain `/registry/...` path served from `apps/web/public/registry/`.
|
||||
* In Node fetch we need an absolute URL — derive one from the request when
|
||||
* possible, fall back to localhost for dev.
|
||||
*/
|
||||
async function resolveBase(): Promise<string> {
|
||||
if (REGISTRY_BASE.startsWith("http")) return REGISTRY_BASE;
|
||||
// Server-rendering: convert the relative path to an absolute URL using the
|
||||
// incoming request. `getRequest()` is the TanStack Start server-side handle.
|
||||
try {
|
||||
const { getRequest } = await import("@tanstack/react-start/server");
|
||||
const url = new URL(getRequest().url);
|
||||
return `${url.origin}${REGISTRY_BASE}`;
|
||||
} catch {
|
||||
// Fallback for non-request contexts (e.g. unit tests). Dev server default.
|
||||
return `http://localhost:3000${REGISTRY_BASE}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
createHighlighter,
|
||||
type Highlighter,
|
||||
type BundledLanguage,
|
||||
type BundledTheme,
|
||||
} from "shiki";
|
||||
|
||||
/**
|
||||
* Languages we expect from the registry's templates. New module templates that
|
||||
* use a language outside this set will fall through to plaintext — add the
|
||||
* language here and rebuild.
|
||||
*/
|
||||
const LANGS = [
|
||||
"tsx",
|
||||
"ts",
|
||||
"json",
|
||||
"jsonc",
|
||||
"css",
|
||||
"html",
|
||||
"md",
|
||||
"mdx",
|
||||
"yaml",
|
||||
"bash",
|
||||
"shell",
|
||||
"prisma",
|
||||
"sql",
|
||||
] as const satisfies readonly BundledLanguage[];
|
||||
|
||||
const THEMES = ["github-light", "github-dark"] as const satisfies readonly BundledTheme[];
|
||||
|
||||
let highlighterPromise: Promise<Highlighter> | undefined;
|
||||
|
||||
/**
|
||||
* Module-singleton Shiki highlighter. Keeps the grammar/theme load warm across
|
||||
* server function invocations on the same server instance. This whole module
|
||||
* runs server-side only — never imported from a client component.
|
||||
*/
|
||||
export function getHighlighter(): Promise<Highlighter> {
|
||||
if (!highlighterPromise) {
|
||||
highlighterPromise = createHighlighter({
|
||||
themes: [...THEMES],
|
||||
langs: [...LANGS],
|
||||
});
|
||||
}
|
||||
return highlighterPromise;
|
||||
}
|
||||
|
||||
const EXT_TO_LANG: Record<string, BundledLanguage> = {
|
||||
ts: "ts",
|
||||
tsx: "tsx",
|
||||
js: "ts",
|
||||
jsx: "tsx",
|
||||
mjs: "ts",
|
||||
cjs: "ts",
|
||||
json: "json",
|
||||
jsonc: "jsonc",
|
||||
css: "css",
|
||||
html: "html",
|
||||
md: "md",
|
||||
mdx: "mdx",
|
||||
yaml: "yaml",
|
||||
yml: "yaml",
|
||||
sh: "bash",
|
||||
bash: "bash",
|
||||
prisma: "prisma",
|
||||
sql: "sql",
|
||||
// anything else returns undefined → highlighter renders as plaintext
|
||||
};
|
||||
|
||||
export function langForPath(path: string): BundledLanguage | undefined {
|
||||
const ext = path.split(".").pop()?.toLowerCase() ?? "";
|
||||
return EXT_TO_LANG[ext];
|
||||
}
|
||||
|
||||
export type Preview = { light: string; dark: string };
|
||||
|
||||
/**
|
||||
* Render a single source string to a `{ light, dark }` pair of pre-themed HTML
|
||||
* blocks. The client renders one via dangerouslySetInnerHTML based on the
|
||||
* active theme.
|
||||
*/
|
||||
export async function renderPreview(source: string, filePath: string): Promise<Preview> {
|
||||
const highlighter = await getHighlighter();
|
||||
const lang = langForPath(filePath) ?? "plaintext";
|
||||
return {
|
||||
light: highlighter.codeToHtml(source, { lang, theme: "github-light" }),
|
||||
dark: highlighter.codeToHtml(source, { lang, theme: "github-dark" }),
|
||||
};
|
||||
}
|
||||
@@ -2,10 +2,12 @@
|
||||
@import "tw-animate-css";
|
||||
@import "shadcn/tailwind.css";
|
||||
@import "@fontsource-variable/geist";
|
||||
@import "@fontsource-variable/geist-mono";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--font-mono: "Geist Mono Variable", monospace;
|
||||
--font-heading: var(--font-sans);
|
||||
--font-sans: "Geist Variable", sans-serif;
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
@@ -131,7 +133,4 @@
|
||||
[role="button"]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
html {
|
||||
@apply font-sans;
|
||||
}
|
||||
}
|
||||
@@ -11,5 +11,5 @@
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts"]
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts", "scripts/**/*.ts"]
|
||||
}
|
||||
@@ -67,8 +67,9 @@ async function main() {
|
||||
"Next steps:",
|
||||
` ${kleur.dim("1.")} Edit ${kleur.cyan(`registry/modules/${args.slot}-${args.id}/module.ts`)}`,
|
||||
` ${kleur.dim("2.")} Add templates under ${kleur.cyan("templates/")}`,
|
||||
` ${kleur.dim("3.")} ${kleur.cyan("pnpm install --force")} to link the workspace`,
|
||||
` ${kleur.dim("4.")} ${kleur.cyan("pnpm registry:build")} to verify the manifest`,
|
||||
` ${kleur.dim("3.")} Drop a ${kleur.cyan("logo.svg")} (or ${kleur.cyan("logo-light.svg")} + ${kleur.cyan("logo-dark.svg")}) — try ${kleur.dim("https://svgl.app")}`,
|
||||
` ${kleur.dim("4.")} ${kleur.cyan("pnpm install --force")} to link the workspace`,
|
||||
` ${kleur.dim("5.")} ${kleur.cyan("pnpm registry:build")} to verify the manifest`,
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import type { Module, RegistryIndex } from "@stanza/registry";
|
||||
import type { Logo, Module, RegistryIndex } from "@stanza/registry";
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = findRepoRoot(here);
|
||||
@@ -40,8 +40,12 @@ async function main() {
|
||||
// no follow-up requests to retrieve template files. Local dev still works
|
||||
// because the runner falls back to disk when `content` is absent.
|
||||
const templatesDir = path.join(modulesDir, dir, "templates");
|
||||
const logo = readLogo(path.join(modulesDir, dir));
|
||||
const codemodBundle = await bundleCodemods(path.join(modulesDir, dir));
|
||||
const inlined: Module = {
|
||||
...mod,
|
||||
...(logo ? { logo } : {}),
|
||||
...(codemodBundle ? { codemodBundle } : {}),
|
||||
adapters: mod.adapters.map((adapter) => ({
|
||||
...adapter,
|
||||
templates: adapter.templates?.map((tpl) => ({
|
||||
@@ -57,11 +61,12 @@ async function main() {
|
||||
);
|
||||
|
||||
// The index keeps a lightweight summary — no `content`, no per-adapter
|
||||
// payloads. The wizard/web builder uses this for filtering; the full
|
||||
// module JSON is only fetched when a user actually picks something.
|
||||
// payloads — but it DOES carry top-level metadata like `logo` so the
|
||||
// wizard / web builder can render module cards without fetching the
|
||||
// full per-module JSON.
|
||||
summaries.push({
|
||||
...mod,
|
||||
adapters: mod.adapters.map((a) => ({ key: a.key, match: a.match })),
|
||||
...inlined,
|
||||
adapters: inlined.adapters.map((a) => ({ key: a.key, match: a.match })),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -83,6 +88,64 @@ async function main() {
|
||||
console.log(`Wrote ${summaries.length} modules to ${outDir}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convention: a module ships a logo by dropping either `logo.svg` (a single
|
||||
* theme-agnostic SVG) or `logo-light.svg` + `logo-dark.svg` (a theme pair)
|
||||
* in its directory. We read whichever is present and return the inlined
|
||||
* markup; if neither exists the module just renders without one.
|
||||
*/
|
||||
function readLogo(moduleDir: string): Logo | undefined {
|
||||
const light = path.join(moduleDir, "logo-light.svg");
|
||||
const dark = path.join(moduleDir, "logo-dark.svg");
|
||||
if (fs.existsSync(light) && fs.existsSync(dark)) {
|
||||
return {
|
||||
light: fs.readFileSync(light, "utf8"),
|
||||
dark: fs.readFileSync(dark, "utf8"),
|
||||
};
|
||||
}
|
||||
const single = path.join(moduleDir, "logo.svg");
|
||||
if (fs.existsSync(single)) return fs.readFileSync(single, "utf8");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* If a module ships imperative codemods (`codemods/index.ts`), bundle them
|
||||
* into a self-contained ESM string and embed in the registry JSON. We mark
|
||||
* the runtime helpers (`@stanza/codemods`, `ts-morph`, Node built-ins) as
|
||||
* external — the CLI provides them at install time. Local dev (FS-loaded
|
||||
* modules) can still ignore this and import the source directly off disk.
|
||||
*/
|
||||
async function bundleCodemods(moduleDir: string): Promise<string | undefined> {
|
||||
const entry = path.join(moduleDir, "codemods", "index.ts");
|
||||
if (!fs.existsSync(entry)) return undefined;
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: [entry],
|
||||
target: "bun",
|
||||
format: "esm",
|
||||
external: [
|
||||
"@stanza/codemods",
|
||||
"@stanza/registry",
|
||||
"ts-morph",
|
||||
"node:*",
|
||||
"fs",
|
||||
"path",
|
||||
"os",
|
||||
"url",
|
||||
],
|
||||
minify: false,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
const log = result.logs.map((l: { message?: string }) => l.message ?? String(l)).join("\n");
|
||||
throw new Error(`Failed to bundle codemods at ${entry}:\n${log}`);
|
||||
}
|
||||
|
||||
const out = result.outputs[0];
|
||||
if (!out) throw new Error(`Bundler produced no output for ${entry}`);
|
||||
return await out.text();
|
||||
}
|
||||
|
||||
function findRepoRoot(start: string): string {
|
||||
let dir = start;
|
||||
for (let i = 0; i < 8; i++) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"types": ["node"]
|
||||
"types": ["node", "bun"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export type {
|
||||
RegistryIndex,
|
||||
TemplateRef,
|
||||
EnvVar,
|
||||
Logo,
|
||||
} from "./module.ts";
|
||||
export { defineModule, KNOWN_SLOTS } from "./module.ts";
|
||||
|
||||
|
||||
@@ -69,6 +69,15 @@ export type EnvVar = {
|
||||
description?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Inlined SVG markup for a module's logo. A bare string is theme-agnostic;
|
||||
* `{ light, dark }` lets the UI swap based on the active theme. The registry
|
||||
* build step auto-detects `logo.svg` (single) or `logo-light.svg` +
|
||||
* `logo-dark.svg` (pair) in the module directory and populates this field —
|
||||
* module authors don't write it manually.
|
||||
*/
|
||||
export type Logo = string | { light: string; dark: string };
|
||||
|
||||
export type Module = {
|
||||
id: ModuleId;
|
||||
slot: SlotId;
|
||||
@@ -92,6 +101,17 @@ export type Module = {
|
||||
homepage?: string;
|
||||
/** Optional maintainer attribution. */
|
||||
author?: string;
|
||||
/** Inlined SVG logo, populated by the registry build step. */
|
||||
logo?: Logo;
|
||||
/**
|
||||
* Bundled JS source of this module's `codemods/index.ts`, populated by the
|
||||
* registry build step. Self-contained ESM with `@stanza/codemods`,
|
||||
* `@stanza/registry`, `ts-morph`, and Node built-ins externalized — the CLI
|
||||
* provides them at runtime. The runner writes this to a temp file and
|
||||
* dynamic-imports it; for local dev (no bundle) it falls back to reading
|
||||
* `codemods/index.ts` off disk.
|
||||
*/
|
||||
codemodBundle?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -166,4 +186,6 @@ export const ModuleSchema = z.object({
|
||||
),
|
||||
homepage: z.string().optional(),
|
||||
author: z.string().optional(),
|
||||
logo: z.union([z.string(), z.object({ light: z.string(), dark: z.string() })]).optional(),
|
||||
codemodBundle: z.string().optional(),
|
||||
}) satisfies z.ZodType<Module>;
|
||||
@@ -69,6 +69,12 @@ importers:
|
||||
'@fontsource-variable/geist':
|
||||
specifier: ^5.2.9
|
||||
version: 5.2.9
|
||||
'@fontsource-variable/geist-mono':
|
||||
specifier: ^5.2.8
|
||||
version: 5.2.8
|
||||
'@pierre/trees':
|
||||
specifier: 1.0.0-beta.4
|
||||
version: 1.0.0-beta.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||
'@stanza/registry':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/registry
|
||||
@@ -114,6 +120,12 @@ importers:
|
||||
shadcn:
|
||||
specifier: ^4.7.0
|
||||
version: 4.7.0(@types/node@25.9.1)(typescript@6.0.3)
|
||||
shiki:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
sonner:
|
||||
specifier: ^2.0.7
|
||||
version: 2.0.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||
tailwind-merge:
|
||||
specifier: ^3.6.0
|
||||
version: 3.6.0
|
||||
@@ -591,6 +603,9 @@ packages:
|
||||
'@floating-ui/utils@0.2.11':
|
||||
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
|
||||
|
||||
'@fontsource-variable/geist-mono@5.2.8':
|
||||
resolution: {integrity: sha512-KI5bj+hkkRiHttYHmccotUZ80ZuZyai+RwI1d7UId0clkx/jXxlo8qYK8j54WzmpBjtMoEMPyllV7faDcj+6RA==}
|
||||
|
||||
'@fontsource-variable/geist@5.2.9':
|
||||
resolution: {integrity: sha512-TP+QSBG3wxKGPE33CbMy/L0Nu3qvJ6Fy81Yc4LnQ95xH+i+cfEp8fyU8/kfV14YwszxIFPhnoMTbjL71waVpyQ==}
|
||||
|
||||
@@ -1076,6 +1091,12 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@pierre/trees@1.0.0-beta.4':
|
||||
resolution: {integrity: sha512-OfT1yk9ne8Te5+GB5zUY8yqE6B8BqjBHQJleH4lu8ltwNpoocZl4vXt1AzlEExpxI/pp+AFX5QG+lR3JjtTEag==}
|
||||
peerDependencies:
|
||||
react: ^18.3.1 || ^19.0.0
|
||||
react-dom: ^18.3.1 || ^19.0.0
|
||||
|
||||
'@posthog/core@1.29.6':
|
||||
resolution: {integrity: sha512-qLA/4xTzxG1FabliYjsOy5pTC9XZWA225MHnpCmqGBoDVGL4sKKgLixMK2dpsHZbSo6AHpBLUfqkvTsh2YxZcQ==}
|
||||
|
||||
@@ -1177,6 +1198,37 @@ packages:
|
||||
'@sec-ant/readable-stream@0.4.1':
|
||||
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
|
||||
|
||||
'@shikijs/core@4.1.0':
|
||||
resolution: {integrity: sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
'@shikijs/engine-javascript@4.1.0':
|
||||
resolution: {integrity: sha512-YquhawCUgaBfhsS72e2Y/dI59gCBNPHu3fEO/tvLaXrTssxZrY5ddjtNLTwndrMgPo8b3IscE+xoICDzpTmlFQ==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
'@shikijs/engine-oniguruma@4.1.0':
|
||||
resolution: {integrity: sha512-axLpjVs45YBvvINa+dJF+NPW+KtFkNXsFr4SDw2BMj9GdeMnGxVB9PQb2xXlJYovslt/nz6giedAyOANkfc7hg==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
'@shikijs/langs@4.1.0':
|
||||
resolution: {integrity: sha512-nwOMruEkbgdZfQ/b8CgpNBVOpvG1k0N5tbmgiFeqsan401+x3ILqlzZJowSla4Agmq4hG2Uf2wh5jLTEhR8VSg==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
'@shikijs/primitive@4.1.0':
|
||||
resolution: {integrity: sha512-zx2/2Uwj2q9X3KSyYREEhXO23xBw5WUhP4orK2lE4r+t9JGITmEe0JH+wPmJhqHpOT2bRRs6lAL945+LDvOAGw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
'@shikijs/themes@4.1.0':
|
||||
resolution: {integrity: sha512-emCcTnUM7yO2wltYbaxm+yLvcCI4+h8XBKc4KmJ7EZUXoSGjcCHifkI//R4OFit9ewpg7H2/9tjOuXrT2v/Knw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
'@shikijs/types@4.1.0':
|
||||
resolution: {integrity: sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
'@shikijs/vscode-textmate@10.0.2':
|
||||
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
|
||||
|
||||
'@sindresorhus/merge-streams@4.0.0':
|
||||
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1607,6 +1659,12 @@ packages:
|
||||
'@types/estree@1.0.9':
|
||||
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
||||
|
||||
'@types/hast@3.0.4':
|
||||
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
|
||||
|
||||
'@types/mdast@4.0.4':
|
||||
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
|
||||
|
||||
'@types/node@25.9.1':
|
||||
resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==}
|
||||
|
||||
@@ -1627,9 +1685,15 @@ packages:
|
||||
'@types/statuses@2.0.6':
|
||||
resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==}
|
||||
|
||||
'@types/unist@3.0.3':
|
||||
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
||||
|
||||
'@types/validate-npm-package-name@4.0.2':
|
||||
resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==}
|
||||
|
||||
'@ungap/structured-clone@1.3.1':
|
||||
resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==}
|
||||
|
||||
'@vitejs/plugin-react@6.0.2':
|
||||
resolution: {integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -1786,6 +1850,9 @@ packages:
|
||||
caniuse-lite@1.0.30001793:
|
||||
resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==}
|
||||
|
||||
ccount@2.0.1:
|
||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||
|
||||
chai@6.2.2:
|
||||
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1794,6 +1861,12 @@ packages:
|
||||
resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
|
||||
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
||||
|
||||
character-entities-html4@2.1.0:
|
||||
resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
|
||||
|
||||
character-entities-legacy@3.0.0:
|
||||
resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==}
|
||||
|
||||
cheerio-select@2.1.0:
|
||||
resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
|
||||
|
||||
@@ -1838,6 +1911,9 @@ packages:
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
comma-separated-tokens@2.0.3:
|
||||
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
|
||||
|
||||
commander@11.1.0:
|
||||
resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
|
||||
engines: {node: '>=16'}
|
||||
@@ -2006,6 +2082,9 @@ packages:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
devlop@1.1.0:
|
||||
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
|
||||
|
||||
diff@8.0.4:
|
||||
resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==}
|
||||
engines: {node: '>=0.3.1'}
|
||||
@@ -2310,6 +2389,12 @@ packages:
|
||||
resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hast-util-to-html@9.0.5:
|
||||
resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==}
|
||||
|
||||
hast-util-whitespace@3.0.0:
|
||||
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
|
||||
|
||||
headers-polyfill@5.0.1:
|
||||
resolution: {integrity: sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==}
|
||||
|
||||
@@ -2324,6 +2409,9 @@ packages:
|
||||
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
|
||||
html-void-elements@3.0.0:
|
||||
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
||||
|
||||
htmlparser2@10.1.0:
|
||||
resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==}
|
||||
|
||||
@@ -2613,6 +2701,9 @@ packages:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
mdast-util-to-hast@13.2.1:
|
||||
resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==}
|
||||
|
||||
mdn-data@2.27.1:
|
||||
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
|
||||
|
||||
@@ -2631,6 +2722,21 @@ packages:
|
||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
micromark-util-character@2.1.1:
|
||||
resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==}
|
||||
|
||||
micromark-util-encode@2.0.1:
|
||||
resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==}
|
||||
|
||||
micromark-util-sanitize-uri@2.0.1:
|
||||
resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==}
|
||||
|
||||
micromark-util-symbol@2.0.1:
|
||||
resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==}
|
||||
|
||||
micromark-util-types@2.0.2:
|
||||
resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==}
|
||||
|
||||
micromatch@4.0.8:
|
||||
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
||||
engines: {node: '>=8.6'}
|
||||
@@ -2784,6 +2890,12 @@ packages:
|
||||
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
oniguruma-parser@0.12.2:
|
||||
resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==}
|
||||
|
||||
oniguruma-to-es@4.3.6:
|
||||
resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==}
|
||||
|
||||
open@11.0.0:
|
||||
resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==}
|
||||
engines: {node: '>=20'}
|
||||
@@ -2903,6 +3015,14 @@ packages:
|
||||
resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
preact-render-to-string@6.6.5:
|
||||
resolution: {integrity: sha512-O6MHzYNIKYaiSX3bOw0gGZfEbOmlIDtDfWwN1JJdc/T3ihzRT6tGGSEWE088dWrEDGa1u7101q+6fzQnO9XCPA==}
|
||||
peerDependencies:
|
||||
preact: '>=10 || >= 11.0.0-0'
|
||||
|
||||
preact@11.0.0-beta.0:
|
||||
resolution: {integrity: sha512-IcODoASASYwJ9kxz7+MJeiJhvLriwSb4y4mHIyxdgaRZp6kPUud7xytrk/6GZw8U3y6EFJaRb5wi9SrEK+8+lg==}
|
||||
|
||||
prettier@3.8.3:
|
||||
resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -2920,6 +3040,9 @@ packages:
|
||||
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
property-information@7.1.0:
|
||||
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
|
||||
|
||||
proxy-addr@2.0.7:
|
||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||
engines: {node: '>= 0.10'}
|
||||
@@ -2963,6 +3086,15 @@ packages:
|
||||
resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
regex-recursion@6.0.2:
|
||||
resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==}
|
||||
|
||||
regex-utilities@2.3.0:
|
||||
resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==}
|
||||
|
||||
regex@6.1.0:
|
||||
resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==}
|
||||
|
||||
require-directory@2.1.1:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -3067,6 +3199,10 @@ packages:
|
||||
resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
shiki@4.1.0:
|
||||
resolution: {integrity: sha512-l/ABZPUR5v70jI10EzqfMS/I96vjSGv2y0ihUV+WYFzv0EfvW4s54m0Lg8wCrrL+2IkwBzFTuxkZjPf8b2NX9Q==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
side-channel-list@1.0.1:
|
||||
resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -3099,6 +3235,12 @@ packages:
|
||||
solid-js@1.9.13:
|
||||
resolution: {integrity: sha512-6hJeJMOcEX8ktqjpDoJZEmld3ijvcvWBDtiXBm7f4332SiFN66QeAQI1REQshvyUoISsSeJ4PHDauKYbwao9JQ==}
|
||||
|
||||
sonner@2.0.7:
|
||||
resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
|
||||
peerDependencies:
|
||||
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -3111,6 +3253,9 @@ packages:
|
||||
resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
space-separated-tokens@2.0.2:
|
||||
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
|
||||
|
||||
srvx@0.11.15:
|
||||
resolution: {integrity: sha512-iXsux0UcOjdvs0LCMa2Ws3WwcDUozA3JN3BquNXkaFPP7TpRqgunKdEgoZ/uwb1J6xaYHfxtz9Twlh6yzwM6Tg==}
|
||||
engines: {node: '>=20.16.0'}
|
||||
@@ -3141,6 +3286,9 @@ packages:
|
||||
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
stringify-entities@4.0.4:
|
||||
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
||||
|
||||
stringify-object@5.0.0:
|
||||
resolution: {integrity: sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==}
|
||||
engines: {node: '>=14.16'}
|
||||
@@ -3227,6 +3375,9 @@ packages:
|
||||
resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
trim-lines@3.0.1:
|
||||
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
|
||||
|
||||
ts-morph@26.0.0:
|
||||
resolution: {integrity: sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==}
|
||||
|
||||
@@ -3277,6 +3428,21 @@ packages:
|
||||
resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
unist-util-is@6.0.1:
|
||||
resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==}
|
||||
|
||||
unist-util-position@5.0.0:
|
||||
resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
|
||||
|
||||
unist-util-stringify-position@4.0.0:
|
||||
resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
|
||||
|
||||
unist-util-visit-parents@6.0.2:
|
||||
resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==}
|
||||
|
||||
unist-util-visit@5.1.0:
|
||||
resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==}
|
||||
|
||||
universalify@2.0.1:
|
||||
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
@@ -3388,6 +3554,12 @@ packages:
|
||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
vfile-message@4.0.3:
|
||||
resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
|
||||
|
||||
vfile@6.0.3:
|
||||
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
|
||||
|
||||
vite@8.0.13:
|
||||
resolution: {integrity: sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -3598,6 +3770,9 @@ packages:
|
||||
zod@4.4.3:
|
||||
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
|
||||
|
||||
zwitch@2.0.4:
|
||||
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@asamuzakjp/css-color@5.1.11':
|
||||
@@ -3931,6 +4106,8 @@ snapshots:
|
||||
|
||||
'@floating-ui/utils@0.2.11': {}
|
||||
|
||||
'@fontsource-variable/geist-mono@5.2.8': {}
|
||||
|
||||
'@fontsource-variable/geist@5.2.9': {}
|
||||
|
||||
'@hono/node-server@1.19.14(hono@4.12.21)':
|
||||
@@ -4252,6 +4429,13 @@ snapshots:
|
||||
'@oxlint/binding-win32-x64-msvc@1.66.0':
|
||||
optional: true
|
||||
|
||||
'@pierre/trees@1.0.0-beta.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
|
||||
dependencies:
|
||||
preact: 11.0.0-beta.0
|
||||
preact-render-to-string: 6.6.5(preact@11.0.0-beta.0)
|
||||
react: 19.2.6
|
||||
react-dom: 19.2.6(react@19.2.6)
|
||||
|
||||
'@posthog/core@1.29.6':
|
||||
dependencies:
|
||||
'@posthog/types': 1.374.3
|
||||
@@ -4311,6 +4495,46 @@ snapshots:
|
||||
|
||||
'@sec-ant/readable-stream@0.4.1': {}
|
||||
|
||||
'@shikijs/core@4.1.0':
|
||||
dependencies:
|
||||
'@shikijs/primitive': 4.1.0
|
||||
'@shikijs/types': 4.1.0
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
'@types/hast': 3.0.4
|
||||
hast-util-to-html: 9.0.5
|
||||
|
||||
'@shikijs/engine-javascript@4.1.0':
|
||||
dependencies:
|
||||
'@shikijs/types': 4.1.0
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
oniguruma-to-es: 4.3.6
|
||||
|
||||
'@shikijs/engine-oniguruma@4.1.0':
|
||||
dependencies:
|
||||
'@shikijs/types': 4.1.0
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
|
||||
'@shikijs/langs@4.1.0':
|
||||
dependencies:
|
||||
'@shikijs/types': 4.1.0
|
||||
|
||||
'@shikijs/primitive@4.1.0':
|
||||
dependencies:
|
||||
'@shikijs/types': 4.1.0
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
'@shikijs/themes@4.1.0':
|
||||
dependencies:
|
||||
'@shikijs/types': 4.1.0
|
||||
|
||||
'@shikijs/types@4.1.0':
|
||||
dependencies:
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
'@shikijs/vscode-textmate@10.0.2': {}
|
||||
|
||||
'@sindresorhus/merge-streams@4.0.0': {}
|
||||
|
||||
'@solid-primitives/event-listener@2.4.5(solid-js@1.9.13)':
|
||||
@@ -4805,6 +5029,14 @@ snapshots:
|
||||
|
||||
'@types/estree@1.0.9': {}
|
||||
|
||||
'@types/hast@3.0.4':
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
'@types/mdast@4.0.4':
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
'@types/node@25.9.1':
|
||||
dependencies:
|
||||
undici-types: 7.24.6
|
||||
@@ -4825,8 +5057,12 @@ snapshots:
|
||||
|
||||
'@types/statuses@2.0.6': {}
|
||||
|
||||
'@types/unist@3.0.3': {}
|
||||
|
||||
'@types/validate-npm-package-name@4.0.2': {}
|
||||
|
||||
'@ungap/structured-clone@1.3.1': {}
|
||||
|
||||
'@vitejs/plugin-react@6.0.2(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0))':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.1
|
||||
@@ -4989,10 +5225,16 @@ snapshots:
|
||||
|
||||
caniuse-lite@1.0.30001793: {}
|
||||
|
||||
ccount@2.0.1: {}
|
||||
|
||||
chai@6.2.2: {}
|
||||
|
||||
chalk@5.6.2: {}
|
||||
|
||||
character-entities-html4@2.1.0: {}
|
||||
|
||||
character-entities-legacy@3.0.0: {}
|
||||
|
||||
cheerio-select@2.1.0:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
@@ -5048,6 +5290,8 @@ snapshots:
|
||||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
comma-separated-tokens@2.0.3: {}
|
||||
|
||||
commander@11.1.0: {}
|
||||
|
||||
commander@14.0.3: {}
|
||||
@@ -5151,6 +5395,10 @@ snapshots:
|
||||
|
||||
detect-libc@2.1.2: {}
|
||||
|
||||
devlop@1.1.0:
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
|
||||
diff@8.0.4: {}
|
||||
|
||||
dom-accessibility-api@0.5.16: {}
|
||||
@@ -5463,6 +5711,24 @@ snapshots:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
hast-util-to-html@9.0.5:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/unist': 3.0.3
|
||||
ccount: 2.0.1
|
||||
comma-separated-tokens: 2.0.3
|
||||
hast-util-whitespace: 3.0.0
|
||||
html-void-elements: 3.0.0
|
||||
mdast-util-to-hast: 13.2.1
|
||||
property-information: 7.1.0
|
||||
space-separated-tokens: 2.0.2
|
||||
stringify-entities: 4.0.4
|
||||
zwitch: 2.0.4
|
||||
|
||||
hast-util-whitespace@3.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
headers-polyfill@5.0.1:
|
||||
dependencies:
|
||||
'@types/set-cookie-parser': 2.4.10
|
||||
@@ -5478,6 +5744,8 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@noble/hashes'
|
||||
|
||||
html-void-elements@3.0.0: {}
|
||||
|
||||
htmlparser2@10.1.0:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
@@ -5710,6 +5978,18 @@ snapshots:
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
mdast-util-to-hast@13.2.1:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/mdast': 4.0.4
|
||||
'@ungap/structured-clone': 1.3.1
|
||||
devlop: 1.1.0
|
||||
micromark-util-sanitize-uri: 2.0.1
|
||||
trim-lines: 3.0.1
|
||||
unist-util-position: 5.0.0
|
||||
unist-util-visit: 5.1.0
|
||||
vfile: 6.0.3
|
||||
|
||||
mdn-data@2.27.1: {}
|
||||
|
||||
media-typer@1.1.0: {}
|
||||
@@ -5720,6 +6000,23 @@ snapshots:
|
||||
|
||||
merge2@1.4.1: {}
|
||||
|
||||
micromark-util-character@2.1.1:
|
||||
dependencies:
|
||||
micromark-util-symbol: 2.0.1
|
||||
micromark-util-types: 2.0.2
|
||||
|
||||
micromark-util-encode@2.0.1: {}
|
||||
|
||||
micromark-util-sanitize-uri@2.0.1:
|
||||
dependencies:
|
||||
micromark-util-character: 2.1.1
|
||||
micromark-util-encode: 2.0.1
|
||||
micromark-util-symbol: 2.0.1
|
||||
|
||||
micromark-util-symbol@2.0.1: {}
|
||||
|
||||
micromark-util-types@2.0.2: {}
|
||||
|
||||
micromatch@4.0.8:
|
||||
dependencies:
|
||||
braces: 3.0.3
|
||||
@@ -5884,6 +6181,14 @@ snapshots:
|
||||
dependencies:
|
||||
mimic-function: 5.0.1
|
||||
|
||||
oniguruma-parser@0.12.2: {}
|
||||
|
||||
oniguruma-to-es@4.3.6:
|
||||
dependencies:
|
||||
oniguruma-parser: 0.12.2
|
||||
regex: 6.1.0
|
||||
regex-recursion: 6.0.2
|
||||
|
||||
open@11.0.0:
|
||||
dependencies:
|
||||
default-browser: 5.5.0
|
||||
@@ -6050,6 +6355,12 @@ snapshots:
|
||||
|
||||
powershell-utils@0.1.0: {}
|
||||
|
||||
preact-render-to-string@6.6.5(preact@11.0.0-beta.0):
|
||||
dependencies:
|
||||
preact: 11.0.0-beta.0
|
||||
|
||||
preact@11.0.0-beta.0: {}
|
||||
|
||||
prettier@3.8.3: {}
|
||||
|
||||
pretty-format@27.5.1:
|
||||
@@ -6067,6 +6378,8 @@ snapshots:
|
||||
kleur: 3.0.3
|
||||
sisteransi: 1.0.5
|
||||
|
||||
property-information@7.1.0: {}
|
||||
|
||||
proxy-addr@2.0.7:
|
||||
dependencies:
|
||||
forwarded: 0.2.0
|
||||
@@ -6108,6 +6421,16 @@ snapshots:
|
||||
tiny-invariant: 1.3.3
|
||||
tslib: 2.8.1
|
||||
|
||||
regex-recursion@6.0.2:
|
||||
dependencies:
|
||||
regex-utilities: 2.3.0
|
||||
|
||||
regex-utilities@2.3.0: {}
|
||||
|
||||
regex@6.1.0:
|
||||
dependencies:
|
||||
regex-utilities: 2.3.0
|
||||
|
||||
require-directory@2.1.1: {}
|
||||
|
||||
require-from-string@2.0.2: {}
|
||||
@@ -6262,6 +6585,17 @@ snapshots:
|
||||
|
||||
shell-quote@1.8.3: {}
|
||||
|
||||
shiki@4.1.0:
|
||||
dependencies:
|
||||
'@shikijs/core': 4.1.0
|
||||
'@shikijs/engine-javascript': 4.1.0
|
||||
'@shikijs/engine-oniguruma': 4.1.0
|
||||
'@shikijs/langs': 4.1.0
|
||||
'@shikijs/themes': 4.1.0
|
||||
'@shikijs/types': 4.1.0
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
side-channel-list@1.0.1:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
@@ -6304,12 +6638,19 @@ snapshots:
|
||||
seroval: 1.5.4
|
||||
seroval-plugins: 1.5.4(seroval@1.5.4)
|
||||
|
||||
sonner@2.0.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
|
||||
dependencies:
|
||||
react: 19.2.6
|
||||
react-dom: 19.2.6(react@19.2.6)
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
source-map@0.6.1: {}
|
||||
|
||||
source-map@0.7.6: {}
|
||||
|
||||
space-separated-tokens@2.0.2: {}
|
||||
|
||||
srvx@0.11.15: {}
|
||||
|
||||
stackback@0.0.2: {}
|
||||
@@ -6334,6 +6675,11 @@ snapshots:
|
||||
get-east-asian-width: 1.6.0
|
||||
strip-ansi: 7.2.0
|
||||
|
||||
stringify-entities@4.0.4:
|
||||
dependencies:
|
||||
character-entities-html4: 2.1.0
|
||||
character-entities-legacy: 3.0.0
|
||||
|
||||
stringify-object@5.0.0:
|
||||
dependencies:
|
||||
get-own-enumerable-keys: 1.0.0
|
||||
@@ -6399,6 +6745,8 @@ snapshots:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
trim-lines@3.0.1: {}
|
||||
|
||||
ts-morph@26.0.0:
|
||||
dependencies:
|
||||
'@ts-morph/common': 0.27.0
|
||||
@@ -6452,6 +6800,29 @@ snapshots:
|
||||
|
||||
unicorn-magic@0.3.0: {}
|
||||
|
||||
unist-util-is@6.0.1:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
unist-util-position@5.0.0:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
unist-util-stringify-position@4.0.0:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
unist-util-visit-parents@6.0.2:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
unist-util-is: 6.0.1
|
||||
|
||||
unist-util-visit@5.1.0:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
unist-util-is: 6.0.1
|
||||
unist-util-visit-parents: 6.0.2
|
||||
|
||||
universalify@2.0.1: {}
|
||||
|
||||
unpipe@1.0.0: {}
|
||||
@@ -6487,6 +6858,16 @@ snapshots:
|
||||
|
||||
vary@1.1.2: {}
|
||||
|
||||
vfile-message@4.0.3:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
unist-util-stringify-position: 4.0.0
|
||||
|
||||
vfile@6.0.3:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
vfile-message: 4.0.3
|
||||
|
||||
vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0):
|
||||
dependencies:
|
||||
lightningcss: 1.32.0
|
||||
@@ -6627,3 +7008,5 @@ snapshots:
|
||||
zod@3.25.76: {}
|
||||
|
||||
zod@4.4.3: {}
|
||||
|
||||
zwitch@2.0.4: {}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 500 500"><path fill="#fff" d="M0 0h500v500H0z"/><path fill="#000" d="M69 121h86.988v259H69zM337.575 121H430v259h-92.425z"/><path fill="#000" d="M427.282 121v83.456h-174.52V121zM430 296.544V380H252.762v-83.456z"/><path fill="#000" d="M252.762 204.455v92.089h-96.774v-92.089z"/></svg>
|
||||
|
After Width: | Height: | Size: 347 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 500 500"><path fill="#000" d="M0 0h500v500H0z"/><path fill="#fff" d="M69 121h86.988v259H69zM337.575 121H430v259h-92.425z"/><path fill="#fff" d="M427.282 121v83.456h-174.52V121zM430 296.544V380H252.762v-83.456z"/><path fill="#fff" d="M252.762 204.455v92.089h-96.774v-92.089z"/></svg>
|
||||
|
After Width: | Height: | Size: 347 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128"><circle cx="64" cy="64" r="20" fill="#fff"/><path fill="#fff" fill-opacity=".4" d="M99.572 10.788c1.999 1.34 2.17 4.156.468 5.858L85.424 31.262c-1.32 1.32-3.37 1.53-5.033.678A35.846 35.846 0 0 0 64 28c-19.882 0-36 16.118-36 36a35.846 35.846 0 0 0 3.94 16.391c.851 1.663.643 3.712-.678 5.033L16.646 100.04c-1.702 1.702-4.519 1.531-5.858-.468C3.974 89.399 0 77.163 0 64 0 28.654 28.654 0 64 0c13.163 0 25.399 3.974 35.572 10.788Z"/><path fill="#fff" d="M100.04 111.354c1.702 1.702 1.531 4.519-.468 5.858C89.399 124.026 77.164 128 64 128c-13.164 0-25.399-3.974-35.572-10.788-2-1.339-2.17-4.156-.468-5.858l14.615-14.616c1.322-1.32 3.37-1.53 5.033-.678A35.847 35.847 0 0 0 64 100a35.846 35.846 0 0 0 16.392-3.94c1.662-.852 3.712-.643 5.032.678l14.616 14.616Z"/></svg>
|
||||
|
After Width: | Height: | Size: 836 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128"><circle cx="64" cy="64" r="20" fill="#6C47FF"/><path fill="#BAB1FF" d="M99.572 10.788c1.999 1.34 2.17 4.156.468 5.858L85.424 31.262c-1.32 1.32-3.37 1.53-5.033.678A35.846 35.846 0 0 0 64 28c-19.882 0-36 16.118-36 36a35.846 35.846 0 0 0 3.94 16.391c.851 1.663.643 3.712-.678 5.033L16.646 100.04c-1.702 1.702-4.519 1.531-5.858-.468C3.974 89.399 0 77.163 0 64 0 28.654 28.654 0 64 0c13.163 0 25.399 3.974 35.572 10.788Z"/><path fill="#6C47FF" d="M100.04 111.354c1.702 1.702 1.531 4.519-.468 5.858C89.399 124.026 77.164 128 64 128c-13.164 0-25.399-3.974-35.572-10.788-2-1.339-2.17-4.156-.468-5.858l14.615-14.616c1.322-1.32 3.37-1.53 5.033-.678A35.847 35.847 0 0 0 64 100a35.846 35.846 0 0 0 16.392-3.94c1.662-.852 3.712-.643 5.032.678l14.616 14.616Z"/></svg>
|
||||
|
After Width: | Height: | Size: 827 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 432.071 445.383"><g style="fill-rule:nonzero;clip-rule:nonzero;fill:none;stroke:#fff;stroke-width:12.4651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4"><path d="M323.205 324.227c2.833-23.601 1.984-27.062 19.563-23.239l4.463.392c13.517.615 31.199-2.174 41.587-7 22.362-10.376 35.622-27.7 13.572-23.148-50.297 10.376-53.755-6.655-53.755-6.655 53.111-78.803 75.313-178.836 56.149-203.322-52.27-66.789-142.748-35.206-144.262-34.386l-.482.089c-9.938-2.062-21.06-3.294-33.554-3.496-22.761-.374-40.032 5.967-53.133 15.904 0 0-161.408-66.498-153.899 83.628 1.597 31.936 45.777 241.655 98.47 178.31 19.259-23.163 37.871-42.748 37.871-42.748 9.242 6.14 20.307 9.272 31.912 8.147l.897-.765c-.281 2.876-.157 5.689.359 9.019-13.572 15.167-9.584 17.83-36.723 23.416-27.457 5.659-11.326 15.734-.797 18.367 12.768 3.193 42.305 7.716 62.268-20.224l-.795 3.188c5.325 4.26 4.965 30.619 5.72 49.452.756 18.834 2.017 36.409 5.856 46.771 3.839 10.36 8.369 37.05 44.036 29.406 29.809-6.388 52.6-15.582 54.677-101.107" style="fill:#000;stroke:#000;stroke-width:37.3953;stroke-linecap:butt;stroke-linejoin:miter"/><path stroke="none" d="M402.395 271.23c-50.302 10.376-53.76-6.655-53.76-6.655 53.111-78.808 75.313-178.843 56.153-203.326-52.27-66.785-142.752-35.2-144.262-34.38l-.486.087c-9.938-2.063-21.06-3.292-33.56-3.496-22.761-.373-40.026 5.967-53.127 15.902 0 0-161.411-66.495-153.904 83.63 1.597 31.938 45.776 241.657 98.471 178.312 19.26-23.163 37.869-42.748 37.869-42.748 9.243 6.14 20.308 9.272 31.908 8.147l.901-.765c-.28 2.876-.152 5.689.361 9.019-13.575 15.167-9.586 17.83-36.723 23.416-27.459 5.659-11.328 15.734-.796 18.367 12.768 3.193 42.307 7.716 62.266-20.224l-.796 3.188c5.319 4.26 9.054 27.711 8.428 48.969-.626 21.259-1.044 35.854 3.147 47.254 4.191 11.4 8.368 37.05 44.042 29.406 29.809-6.388 45.256-22.942 47.405-50.555 1.525-19.631 4.976-16.729 5.194-34.28l2.768-8.309c3.192-26.611.507-35.196 18.872-31.203l4.463.392c13.517.615 31.208-2.174 41.591-7 22.358-10.376 35.618-27.7 13.573-23.148z" style="fill:#336791;stroke:none"/><path d="M215.866 286.484c-1.385 49.516.348 99.377 5.193 111.495 4.848 12.118 15.223 35.688 50.9 28.045 29.806-6.39 40.651-18.756 45.357-46.051 3.466-20.082 10.148-75.854 11.005-87.281M173.104 38.256S11.583-27.76 19.092 122.365c1.597 31.938 45.779 241.664 98.473 178.316 19.256-23.166 36.671-41.335 36.671-41.335M260.349 26.207c-5.591 1.753 89.848-34.889 144.087 34.417 19.159 24.484-3.043 124.519-56.153 203.329"/><path d="M348.282 263.953s3.461 17.036 53.764 6.653c22.04-4.552 8.776 12.774-13.577 23.155-18.345 8.514-59.474 10.696-60.146-1.069-1.729-30.355 21.647-21.133 19.96-28.739-1.525-6.85-11.979-13.573-18.894-30.338-6.037-14.633-82.796-126.849 21.287-110.183 3.813-.789-27.146-99.002-124.553-100.599-97.385-1.597-94.19 119.762-94.19 119.762" style="stroke-linejoin:bevel"/><path d="M188.604 274.334c-13.577 15.166-9.584 17.829-36.723 23.417-27.459 5.66-11.326 15.733-.797 18.365 12.768 3.195 42.307 7.718 62.266-20.229 6.078-8.509-.036-22.086-8.385-25.547-4.034-1.671-9.428-3.765-16.361 3.994z"/><path d="M187.715 274.069c-1.368-8.917 2.93-19.528 7.536-31.942 6.922-18.626 22.893-37.255 10.117-96.339-9.523-44.029-73.396-9.163-73.436-3.193-.039 5.968 2.889 30.26-1.067 58.548-5.162 36.913 23.488 68.132 56.479 64.938"/><path d="M172.517 141.7c-.288 2.039 3.733 7.48 8.976 8.207 5.234.73 9.714-3.522 9.998-5.559.284-2.039-3.732-4.285-8.977-5.015-5.237-.731-9.719.333-9.996 2.367z" style="fill:#fff;stroke-width:4.155;stroke-linecap:butt;stroke-linejoin:miter"/><path d="M331.941 137.543c.284 2.039-3.732 7.48-8.976 8.207-5.238.73-9.718-3.522-10.005-5.559-.277-2.039 3.74-4.285 8.979-5.015 5.239-.73 9.718.333 10.002 2.368z" style="fill:#fff;stroke-width:2.0775;stroke-linecap:butt;stroke-linejoin:miter"/><path d="M350.676 123.432c.863 15.994-3.445 26.888-3.988 43.914-.804 24.748 11.799 53.074-7.191 81.435"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid" viewBox="0 0 512 228">
|
||||
<defs>
|
||||
<linearGradient x1="57.7%" y1="2%" x2="57.7%" y2="94.4%" id="a">
|
||||
<stop stop-color="#97D9F6" offset="0%"/>
|
||||
<stop stop-color="#0F80CC" offset="92%"/>
|
||||
<stop stop-color="#0F80CC" offset="100%"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M194.5 112c-6.8 0-12.3 2-16.6 6.1-4.3 4-6.4 9.3-6.4 15.8a23.3 23.3 0 0 0 6.5 17c2.2 2.4 6.6 5.6 13.2 9.7 8 5 13.4 9 16 12 2.5 3.2 3.7 6.4 3.7 9.9 0 4.5-1.5 8.2-4.6 11a18 18 0 0 1-12.4 4 20.1 20.1 0 0 1-20.3-20.8h-2.5v22.9h2.5c.8-2.2 1.8-3.3 3.2-3.3.7 0 2.3.4 4.7 1.3 6 2.1 10.9 3.2 14.7 3.2a23 23 0 0 0 17-7 23.2 23.2 0 0 0 2.4-30.8c-3-4-9-8.6-18-14a59.5 59.5 0 0 1-15-11.3c-2.3-3-3.5-6.1-3.5-9.7 0-3.8 1.5-7 4.2-9.2 2.8-2.3 6.5-3.5 11-3.5 5.2 0 9.5 1.5 12.9 4.6 3.3 3 5.3 7.4 5.9 12.8h2.5V113h-2.3a6 6 0 0 1-.8 2c-.3.3-.7.4-1.4.4a17 17 0 0 1-4.3-1 38.6 38.6 0 0 0-12.3-2.3Zm82.4 0a44.6 44.6 0 0 0-39 22.2 43.5 43.5 0 0 0 4.5 50.2c7 8.2 15.3 13.3 25 15.3 2.1 1.1 5.3 4.1 9.4 9 4.7 5.3 8.6 9.3 11.9 11.6A35.6 35.6 0 0 0 311 227c5.3 0 10-.9 14-2.7l-.9-2.3a20.1 20.1 0 0 1-18.7-2.3c-3.6-2.5-8.3-7.1-13.8-14a47.6 47.6 0 0 0-5.4-6.1c10.6-2 19.3-7.2 26-15.4a42.8 42.8 0 0 0 10.2-28 42 42 0 0 0-13.2-31.4 44.8 44.8 0 0 0-32.4-12.9Zm51.4 0 .1 2.7c5.5 0 8.6 1.7 9.3 5 .3 1.1.4 3.3.4 6.4v59.6c0 4.5-.7 7.3-2 8.6-1.1 1.2-3.2 2-6.2 2.3l-.1 2.6h55.1l1.4-13.5h-2.5c-.7 3.7-2.3 6.3-5 7.8a30.5 30.5 0 0 1-14 2.3h-5.2c-6 0-9.5-2.2-10.5-6.6-.2-.9-.2-1.8-.2-2.8l.2-60.3c0-4.4.5-7.4 1.7-8.9 1.2-1.4 3.3-2.3 6.4-2.5l-.2-2.7h-28.7Zm-50.6 3.3a28 28 0 0 1 23 11.2 48 48 0 0 1 8.8 30.5c0 12.3-3 22.1-8.9 29.5a29 29 0 0 1-23.7 11c-9.5 0-17.2-3.8-23-11.4a47.1 47.1 0 0 1-8.9-29.6c0-12.5 3-22.5 9-30 5.9-7.5 13.8-11.2 23.7-11.2Zm126.9 12.8c-1.3 0-2.4.5-3.1 1.4-.8 1-1 2-.8 3.4a6 6 0 0 0 2.2 3.4c1.1 1 2.4 1.4 3.7 1.4 1.3 0 2.3-.5 3-1.4.7-1 1-2.1.7-3.4-.3-1.4-1-2.5-2-3.4a5.6 5.6 0 0 0-3.7-1.4Zm36.4 9.2c-2.3 8.8-7.3 13.5-14.9 14.3l.1 2.5h8.9l-.2 29.8c0 5.1.2 8.5.5 10.2 1 4.2 3.6 6.3 8.1 6.3 6.6 0 13.4-4 20.6-12l-2.2-1.8c-5.2 5.2-9.7 7.8-13.7 7.8-2.5 0-4-1.4-4.6-4.2l-.2-2.4v-33.7h13.7l-.2-4h-13.4v-12.8H441Zm52.4 11.2c-7.6 0-13.7 3.7-18.5 11a31.3 31.3 0 0 0-4.4 24.4 21 21 0 0 0 6.7 12.2 19 19 0 0 0 12.7 4.3c4.7 0 11.4-1.2 14.1-3.7 2.8-2.4 5.4-6.3 7.8-11.7l-2-2a20.6 20.6 0 0 1-17.1 10.5c-8 0-12.8-4.3-14.6-13l-.5-3.5a46 46 0 0 0 21.4-8c4.8-3.8 9.6-7.9 8.7-12.1a9.9 9.9 0 0 0-3.9-6c-2-1.6-7.4-2.4-10.4-2.4Zm-83 .3-16.3 3.8v2.9l5.7-.7c2.7 0 4.3 1.2 4.8 3.7.2.8.3 2 .4 3.4l-.2 26.8c0 3.7-.5 5.8-1.3 6.5-.8.6-3 1-6.6 1v2.5h25.9v-2.5c-3.7 0-6-.3-7-.9-1-.5-1.8-1.5-2.1-3-.2-1.2-.3-3.1-.4-5.7l.1-37.8h-3Zm78.9 5c1.6 0 3 .6 4.6 1.8a7 7 0 0 1 2.8 4c1.4 7-4.9 12-19 14.7a25 25 0 0 1 2.7-14.1c2.3-4.3 5.3-6.4 8.9-6.4Z" fill="#003B57"/>
|
||||
<path d="M157.9 10H17C7.7 10 0 17.7 0 27v155.2c0 9.4 7.7 17.2 17.1 17.2h92.7c-1-46.2 14.7-135.7 48-189.5Z" fill="#0F80CC"/>
|
||||
<path d="M152.8 15H17C10.5 15 5 20.4 5 27V171c30.7-11.8 76.8-22 108.7-21.5a989.7 989.7 0 0 1 39-134.5Z" fill="url(#a)"/>
|
||||
<path d="M190.7 4.9c-9.6-8.6-21.3-5.2-32.8 5a81.4 81.4 0 0 0-5.1 5c-19.7 21-38 59.7-43.7 89.2a81.5 81.5 0 0 1 5.8 17.7l.8 3.5-.9-2.8a173.8 173.8 0 0 0-.8-2 172 172 0 0 0-6.4-12.1l-3.5 11c4.5 8.2 7.3 22.4 7.3 22.4l-1.4-4.1c-1-2.9-6-11.7-7.2-13.7-2 7.5-2.8 12.6-2.1 13.8 1.4 2.4 2.7 6.5 4 11a257.6 257.6 0 0 1 4.6 25c-.3 8.6-.1 17.6.5 25.7a91 91 0 0 0 4.7 24.8l1.5-.8a111 111 0 0 1-3.9-37c.9-22.5 6-49.5 15.6-77.7 16-42.5 38.4-76.6 58.8-93-18.6 17-43.8 71.4-51.4 91.6a365.7 365.7 0 0 0-18 64c6.2-19 26.4-27.2 26.4-27.2s9.8-12.2 21.4-29.6a210 210 0 0 0-22.1 6l-7.1 3s18.1-11 33.7-16C191 73.8 214.2 25.9 190.7 4.9" fill="#003B57"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180"><mask height="180" id=":r8:mask0_408_134" maskUnits="userSpaceOnUse" width="180" x="0" y="0" style="mask-type: alpha;"><circle cx="90" cy="90" fill="black" r="90"></circle></mask><g mask="url(#:r8:mask0_408_134)"><circle cx="90" cy="90" data-circle="true" fill="black" r="90"></circle><path d="M149.508 157.52L69.142 54H54V125.97H66.1136V69.3836L139.999 164.845C143.333 162.614 146.509 160.165 149.508 157.52Z" fill="url(#:r8:paint0_linear_408_134)"></path><rect fill="url(#:r8:paint1_linear_408_134)" height="72" width="12" x="115" y="54"></rect></g><defs><linearGradient gradientUnits="userSpaceOnUse" id=":r8:paint0_linear_408_134" x1="109" x2="144.5" y1="116.5" y2="160.5"><stop stop-color="white"></stop><stop offset="1" stop-color="white" stop-opacity="0"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id=":r8:paint1_linear_408_134" x1="121" x2="120.799" y1="54" y2="106.875"><stop stop-color="white"></stop><stop offset="1" stop-color="white" stop-opacity="0"></stop></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 633 633"><defs><linearGradient id="b" x1="50%" x2="50%" y1="0%" y2="71.65%"><stop offset="0%" stop-color="#6BDAFF"/><stop offset="31.922%" stop-color="#F9FFB5"/><stop offset="70.627%" stop-color="#FFA770"/><stop offset="100%" stop-color="#FF7373"/></linearGradient><linearGradient id="d" x1="43.996%" x2="53.441%" y1="8.54%" y2="93.872%"><stop offset="0%" stop-color="#673800"/><stop offset="100%" stop-color="#B65E00"/></linearGradient><linearGradient id="e" x1="50%" x2="50%" y1="0%" y2="100%"><stop offset="0%" stop-color="#2F8A00"/><stop offset="100%" stop-color="#90FF57"/></linearGradient><linearGradient id="f" x1="50%" x2="50%" y1="0%" y2="100%"><stop offset="0%" stop-color="#2F8A00"/><stop offset="100%" stop-color="#90FF57"/></linearGradient><linearGradient id="g" x1="50%" x2="50%" y1="0%" y2="100%"><stop offset="0%" stop-color="#2F8A00"/><stop offset="100%" stop-color="#90FF57"/></linearGradient><linearGradient id="h" x1="50%" x2="50%" y1="0%" y2="100%"><stop offset="0%" stop-color="#2F8A00"/><stop offset="100%" stop-color="#90FF57"/></linearGradient><linearGradient id="i" x1="50%" x2="50%" y1="0%" y2="100%"><stop offset="0%" stop-color="#2F8A00"/><stop offset="100%" stop-color="#90FF57"/></linearGradient><linearGradient id="j" x1="50%" x2="50%" y1="0%" y2="100%"><stop offset="0%" stop-color="#2F8A00"/><stop offset="100%" stop-color="#90FF57"/></linearGradient><linearGradient id="k" x1="92.9%" x2="8.641%" y1="45.768%" y2="54.892%"><stop offset="0%" stop-color="#EE2700"/><stop offset="100%" stop-color="#FF008E"/></linearGradient><linearGradient id="l" x1="61.109%" x2="43.717%" y1="3.633%" y2="43.072%"><stop offset="0%" stop-color="#FFF400"/><stop offset="100%" stop-color="#3C8700"/></linearGradient><linearGradient id="m" x1="50%" x2="50%" y1="0%" y2="100%"><stop offset="0%" stop-color="#FFDF00"/><stop offset="100%" stop-color="#FF9D00"/></linearGradient><linearGradient id="n" x1="127.279%" x2="0%" y1="49.778%" y2="50.222%"><stop offset="0%" stop-color="#FFA400"/><stop offset="100%" stop-color="#FF5E00"/></linearGradient><linearGradient id="o" x1="127.279%" x2="0%" y1="47.531%" y2="52.469%"><stop offset="0%" stop-color="#FFA400"/><stop offset="100%" stop-color="#FF5E00"/></linearGradient><linearGradient id="p" x1="127.279%" x2="0%" y1="46.195%" y2="53.805%"><stop offset="0%" stop-color="#FFA400"/><stop offset="100%" stop-color="#FF5E00"/></linearGradient><linearGradient id="q" x1="127.279%" x2="0%" y1="35.33%" y2="64.67%"><stop offset="0%" stop-color="#FFA400"/><stop offset="100%" stop-color="#FF5E00"/></linearGradient><linearGradient id="r" x1="127.279%" x2="0%" y1="4.875%" y2="95.125%"><stop offset="0%" stop-color="#FFA400"/><stop offset="100%" stop-color="#FF5E00"/></linearGradient><linearGradient id="s" x1="78.334%" x2="31.668%" y1="0%" y2="100%"><stop offset="0%" stop-color="#FFA400"/><stop offset="100%" stop-color="#FF5E00"/></linearGradient><linearGradient id="t" x1="57.913%" x2="44.88%" y1="0%" y2="100%"><stop offset="0%" stop-color="#FFA400"/><stop offset="100%" stop-color="#FF5E00"/></linearGradient><linearGradient id="u" x1="50.495%" x2="49.68%" y1="0%" y2="100%"><stop offset="0%" stop-color="#FFA400"/><stop offset="100%" stop-color="#FF5E00"/></linearGradient><circle id="a" cx="308.5" cy="308.5" r="308.5"/><circle id="v" cx="307.5" cy="308.5" r="316.5"/></defs><g fill="none" fill-rule="evenodd" transform="translate(9 8)"><mask id="c" fill="#fff"><use xlink:href="#a"/></mask><use xlink:href="#a" fill="url(#b)"/><ellipse cx="81.5" cy="602.5" fill="#015064" stroke="#00CFE2" stroke-width="25" mask="url(#c)" rx="214.5" ry="185.968"/><ellipse cx="535.5" cy="602.5" fill="#015064" stroke="#00CFE2" stroke-width="25" mask="url(#c)" rx="214.5" ry="185.968"/><ellipse cx="81.5" cy="640.5" fill="#015064" stroke="#00A8B8" stroke-width="25" mask="url(#c)" rx="214.5" ry="185.968"/><ellipse cx="535.5" cy="640.5" fill="#015064" stroke="#00A8B8" stroke-width="25" mask="url(#c)" rx="214.5" ry="185.968"/><ellipse cx="81.5" cy="676.5" fill="#015064" stroke="#007782" stroke-width="25" mask="url(#c)" rx="214.5" ry="185.968"/><ellipse cx="535.5" cy="676.5" fill="#015064" stroke="#007782" stroke-width="25" mask="url(#c)" rx="214.5" ry="185.968"/><g mask="url(#c)"><path fill="url(#d)" stroke="#6E3A00" stroke-width="6.088" d="M98.318 88.007c7.691 37.104 16.643 72.442 26.856 106.013 10.212 33.571 25.57 68.934 46.07 106.088l-51.903 11.67c-10.057-60.01-17.232-99.172-21.525-117.487-4.293-18.315-10.989-51.434-20.089-99.357l20.591-6.927" transform="scale(-1 1) rotate(25 -300.37 -553.013)"/><g stroke="#2F8A00"><path fill="url(#e)" stroke-width="9.343" d="M108.544 66.538s-13.54-21.305-37.417-27.785c-15.917-4.321-33.933.31-54.048 13.892C33.464 65.975 47.24 73.52 58.405 75.28c16.749 2.64 50.14-8.74 50.14-8.74Z" transform="rotate(1 -6061.691 5926.397)"/><path fill="url(#f)" stroke-width="9.343" d="M108.544 67.138s-47.187-5.997-81.077 19.93Line truncated
|
||||
|
After Width: | Height: | Size: 8.6 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160" fill="none" viewBox="0 0 160 160"><rect width="9.631" height="40.852" fill="#C5F74F" rx="4.816" transform="matrix(.87303 .48767 -.49721 .86763 43.48 67.304)"/><rect width="9.631" height="40.852" fill="#C5F74F" rx="4.816" transform="matrix(.87303 .48767 -.49721 .86763 76.94 46.534)"/><rect width="9.631" height="40.852" fill="#C5F74F" rx="4.816" transform="matrix(.87303 .48767 -.49721 .86763 128.424 46.535)"/><rect width="9.631" height="40.852" fill="#C5F74F" rx="4.816" transform="matrix(.87303 .48767 -.49721 .86763 94.957 67.304)"/></svg>
|
||||
|
After Width: | Height: | Size: 608 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160" fill="none" viewBox="0 0 160 160"><rect width="9.631" height="40.852" fill="#121212" rx="4.816" transform="matrix(.87303 .48767 -.49721 .86763 43.48 67.304)"/><rect width="9.631" height="40.852" fill="#121212" rx="4.816" transform="matrix(.87303 .48767 -.49721 .86763 76.94 46.534)"/><rect width="9.631" height="40.852" fill="#121212" rx="4.816" transform="matrix(.87303 .48767 -.49721 .86763 128.424 46.535)"/><rect width="9.631" height="40.852" fill="#121212" rx="4.816" transform="matrix(.87303 .48767 -.49721 .86763 94.957 67.304)"/></svg>
|
||||
|
After Width: | Height: | Size: 608 B |
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 256 310" width="256" height="310" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path fill="#fff" d="M254.313 235.519L148 9.749A17.063 17.063 0 00133.473.037a16.87 16.87 0 00-15.533 8.052L2.633 194.848a17.465 17.465 0 00.193 18.747L59.2 300.896a18.13 18.13 0 0020.363 7.489l163.599-48.392a17.929 17.929 0 0011.26-9.722 17.542 17.542 0 00-.101-14.76l-.008.008zm-23.802 9.683l-138.823 41.05c-4.235 1.26-8.3-2.411-7.419-6.685l49.598-237.484c.927-4.443 7.063-5.147 9.003-1.035l91.814 194.973a6.63 6.63 0 01-4.18 9.18h.007z"/></svg>
|
||||
|
After Width: | Height: | Size: 565 B |
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 256 310" width="256" height="310" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path fill="#000" d="M254.313 235.519L148 9.749A17.063 17.063 0 00133.473.037a16.87 16.87 0 00-15.533 8.052L2.633 194.848a17.465 17.465 0 00.193 18.747L59.2 300.896a18.13 18.13 0 0020.363 7.489l163.599-48.392a17.929 17.929 0 0011.26-9.722 17.542 17.542 0 00-.101-14.76l-.008.008zm-23.802 9.683l-138.823 41.05c-4.235 1.26-8.3-2.411-7.419-6.685l49.598-237.484c.927-4.443 7.063-5.147 9.003-1.035l91.814 194.973a6.63 6.63 0 01-4.18 9.18h.007z"/></svg>
|
||||
|
After Width: | Height: | Size: 565 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 54 33"><g clip-path="url(#a)"><path fill="#38bdf8" fill-rule="evenodd" d="M27 0c-7.2 0-11.7 3.6-13.5 10.8 2.7-3.6 5.85-4.95 9.45-4.05 2.054.513 3.522 2.004 5.147 3.653C30.744 13.09 33.808 16.2 40.5 16.2c7.2 0 11.7-3.6 13.5-10.8-2.7 3.6-5.85 4.95-9.45 4.05-2.054-.513-3.522-2.004-5.147-3.653C36.756 3.11 33.692 0 27 0zM13.5 16.2C6.3 16.2 1.8 19.8 0 27c2.7-3.6 5.85-4.95 9.45-4.05 2.054.514 3.522 2.004 5.147 3.653C17.244 29.29 20.308 32.4 27 32.4c7.2 0 11.7-3.6 13.5-10.8-2.7 3.6-5.85 4.95-9.45 4.05-2.054-.513-3.522-2.004-5.147-3.653C23.256 19.31 20.192 16.2 13.5 16.2z" clip-rule="evenodd"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h54v32.4H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 747 B |