diff --git a/adapters/cloudflare-wrangler.ts b/adapters/cloudflare-wrangler.ts index ea46b7c..47ec339 100644 --- a/adapters/cloudflare-wrangler.ts +++ b/adapters/cloudflare-wrangler.ts @@ -24,10 +24,21 @@ export interface WranglerSetupOptions { } /** Creates a minimal Wrangler config when a project has not supplied one. */ -export async function setupWrangler(root: string, options: WranglerSetupOptions = {}): Promise { +export async function setupWrangler( + root: string, + options: WranglerSetupOptions = {}, +): Promise { const path = join(resolve(root), "wrangler.jsonc"); if (existsSync(path)) return path; - const source = JSON.stringify({ name: options.name ?? "muddle-app", main: options.main ?? "src/worker.ts", compatibility_date: options.compatibilityDate ?? "2026-10-22" }, null, 2).concat("\n"); + const source = JSON.stringify( + { + name: options.name ?? "muddle-app", + main: options.main ?? "src/worker.ts", + compatibility_date: options.compatibilityDate ?? "2026-10-22", + }, + null, + 2, + ).concat("\n"); await writeFile(path, source, "utf8"); return path; } diff --git a/adapters/prediction.ts b/adapters/prediction.ts index 637495d..623987d 100644 --- a/adapters/prediction.ts +++ b/adapters/prediction.ts @@ -54,7 +54,9 @@ export interface ReflectiveAnchorGroup { const measureScript = (input: { remeasure: boolean; selectors?: string[] }) => { const { remeasure, selectors = [] } = input; const document = (globalThis as unknown as { document: any }).document; - const getComputedStyle = (globalThis as unknown as { getComputedStyle: (element: any) => any }).getComputedStyle; + const getComputedStyle = ( + globalThis as unknown as { getComputedStyle: (element: any) => any } + ).getComputedStyle; type Entry = { selector: string; index: number; @@ -89,15 +91,26 @@ const measureScript = (input: { remeasure: boolean; selectors?: string[] }) => { "transition", ]); - const propertyValue = (element: any, style: any, property: string): string => { + const propertyValue = ( + element: any, + style: any, + property: string, + ): string => { switch (property) { - case "scroll-height": return `${element.scrollHeight}px`; - case "scroll-width": return `${element.scrollWidth}px`; - case "client-height": return `${element.clientHeight}px`; - case "client-width": return `${element.clientWidth}px`; - case "offset-height": return `${element.offsetHeight}px`; - case "offset-width": return `${element.offsetWidth}px`; - default: return style.getPropertyValue(property) ?? ""; + case "scroll-height": + return `${element.scrollHeight}px`; + case "scroll-width": + return `${element.scrollWidth}px`; + case "client-height": + return `${element.clientHeight}px`; + case "client-width": + return `${element.clientWidth}px`; + case "offset-height": + return `${element.offsetHeight}px`; + case "offset-width": + return `${element.offsetWidth}px`; + default: + return style.getPropertyValue(property) ?? ""; } }; @@ -120,13 +133,17 @@ const measureScript = (input: { remeasure: boolean; selectors?: string[] }) => { } } } - const anchors = [...document.querySelectorAll("[data-muddle-anchor]")] as any[]; + const anchors = [ + ...document.querySelectorAll("[data-muddle-anchor]"), + ] as any[]; if (!remeasure) { for (const element of anchors) { const name = element.getAttribute("data-muddle-anchor") as string | null; if (!name?.includes(".")) continue; const parts = name.split("."); - const groups = parts.slice(0, -1).map((_part, index) => parts.slice(0, index + 1).join(".")); + const groups = parts + .slice(0, -1) + .map((_part, index) => parts.slice(0, index + 1).join(".")); element.setAttribute("data-muddle-group", groups.join(" ")); } } @@ -139,11 +156,16 @@ const measureScript = (input: { remeasure: boolean; selectors?: string[] }) => { const needle = `--muddle-relate-${key}-`; let index = text.indexOf(needle); while (index !== -1) { - const property = /^[a-zA-Z][\w-]*/.exec(text.slice(index + needle.length))?.[0]; + const property = /^[a-zA-Z][\w-]*/.exec( + text.slice(index + needle.length), + )?.[0]; if (property && !requests.get(key)?.includes(property)) { requests.set(key, [...(requests.get(key) ?? []), property]); } - index = text.indexOf(needle, index + needle.length + (property?.length ?? 0)); + index = text.indexOf( + needle, + index + needle.length + (property?.length ?? 0), + ); } } } @@ -152,7 +174,8 @@ const measureScript = (input: { remeasure: boolean; selectors?: string[] }) => { if (!remeasure) { const selected = new Set(); for (const selector of selectors) { - for (const element of document.querySelectorAll(selector)) selected.add(element); + for (const element of document.querySelectorAll(selector)) + selected.add(element); } for (const [index, element] of [...selected].entries()) { element.setAttribute("data-muddle-predicted", `predict-${index}`); @@ -160,7 +183,9 @@ const measureScript = (input: { remeasure: boolean; selectors?: string[] }) => { } const entries: Entry[] = []; - for (const [index, element] of ([...document.querySelectorAll("[data-muddle-predicted]")] as any[]).entries()) { + for (const [index, element] of ( + [...document.querySelectorAll("[data-muddle-predicted]")] as any[] + ).entries()) { const box = element.getBoundingClientRect(); const style = getComputedStyle(element); const entry: Entry = { @@ -197,7 +222,10 @@ const measureScript = (input: { remeasure: boolean; selectors?: string[] }) => { return entries; }; -function predictionVariables(prediction: LayoutPrediction, variant: string): string { +function predictionVariables( + prediction: LayoutPrediction, + variant: string, +): string { return [ `${prediction.selector} {`, `--muddle-predicted-${variant}-width: ${prediction.width}px;`, @@ -218,7 +246,10 @@ function predictionVariables(prediction: LayoutPrediction, variant: string): str * The suffixed variables hold the raw measurement of the variant, the plain * alias resolves to whichever variant applies at the current viewport. */ -function relationVariables(prediction: LayoutPrediction, variant: string): string { +function relationVariables( + prediction: LayoutPrediction, + variant: string, +): string { if (!prediction.anchor) return ""; const key = prediction.anchor.replace(/[^a-zA-Z0-9_-]/g, "_"); const prefix = `--muddle-relate-${key}-${variant}`; @@ -227,21 +258,33 @@ function relationVariables(prediction: LayoutPrediction, variant: string): strin `${prefix}-y: ${prediction.y}px`, `${prefix}-width: ${prediction.width}px`, `${prefix}-height: ${prediction.height}px`, - prediction.backgroundColor ? `${prefix}-background-color: ${prediction.backgroundColor}` : "", + prediction.backgroundColor + ? `${prefix}-background-color: ${prediction.backgroundColor}` + : "", prediction.color ? `${prefix}-color: ${prediction.color}` : "", - prediction.transition ? `${prefix}-transition: ${prediction.transition}` : "", + prediction.transition + ? `${prefix}-transition: ${prediction.transition}` + : "", `--muddle-relate-${key}-x: var(${prefix}-x)`, `--muddle-relate-${key}-y: var(${prefix}-y)`, `--muddle-relate-${key}-width: var(${prefix}-width)`, `--muddle-relate-${key}-height: var(${prefix}-height)`, - prediction.backgroundColor ? `--muddle-relate-${key}-background-color: var(${prefix}-background-color)` : "", - prediction.color ? `--muddle-relate-${key}-color: var(${prefix}-color)` : "", - prediction.transition ? `--muddle-relate-${key}-transition: var(${prefix}-transition)` : "", + prediction.backgroundColor + ? `--muddle-relate-${key}-background-color: var(${prefix}-background-color)` + : "", + prediction.color + ? `--muddle-relate-${key}-color: var(${prefix}-color)` + : "", + prediction.transition + ? `--muddle-relate-${key}-transition: var(${prefix}-transition)` + : "", ] - .concat(Object.entries(prediction.relate ?? {}).flatMap(([property, value]) => [ - `${prefix}-${property}: ${value}`, - `--muddle-relate-${key}-${property}: var(${prefix}-${property})`, - ])) + .concat( + Object.entries(prediction.relate ?? {}).flatMap(([property, value]) => [ + `${prefix}-${property}: ${value}`, + `--muddle-relate-${key}-${property}: var(${prefix}-${property})`, + ]), + ) .filter(Boolean); return `:root {\n\t${values.join(";\n")};\n}`; } @@ -256,7 +299,9 @@ function selfVariables(prediction: LayoutPrediction): string { if (!prediction.anchor) return ""; const entries = Object.entries(prediction.self ?? {}); if (entries.length === 0) return ""; - const values = entries.map(([property, value]) => `--muddle-self-${property}: ${value}`); + const values = entries.map( + ([property, value]) => `--muddle-self-${property}: ${value}`, + ); return `[data-muddle-anchor="${prediction.anchor}"] {\n\t${values.join(";\n")};\n}`; } @@ -267,7 +312,9 @@ interface PredictionLayer { predictions: LayoutPrediction[]; } -function reflectiveAnchorGroups(predictions: LayoutPrediction[]): ReflectiveAnchorGroup[] { +function reflectiveAnchorGroups( + predictions: LayoutPrediction[], +): ReflectiveAnchorGroup[] { const groups = new Map(); for (const prediction of predictions) { const anchor = prediction.anchor; @@ -288,31 +335,48 @@ function reflectionCss(groups: ReflectiveAnchorGroup[]): string[] { const key = group.name.replace(/[^a-zA-Z0-9_-]/g, "_"); return [ `[data-muddle-group~="${group.name}"] { --muddle-reflect-${key}-count: ${group.anchors.length}; }`, - ...group.anchors.map((anchor, index) => `[data-muddle-anchor="${anchor}"] { --muddle-reflect-${key}-index: ${index}; }`), + ...group.anchors.map( + (anchor, index) => + `[data-muddle-anchor="${anchor}"] { --muddle-reflect-${key}-index: ${index}; }`, + ), ]; }); } -function predictionCss(desktop: LayoutPrediction[], layers: PredictionLayer[], groups: ReflectiveAnchorGroup[]): string { - const rules = (predictions: LayoutPrediction[], variant: string) => predictions.flatMap((entry) => [ - predictionVariables(entry, variant), - relationVariables(entry, variant), - selfVariables(entry), - ]); +function predictionCss( + desktop: LayoutPrediction[], + layers: PredictionLayer[], + groups: ReflectiveAnchorGroup[], +): string { + const rules = (predictions: LayoutPrediction[], variant: string) => + predictions.flatMap((entry) => [ + predictionVariables(entry, variant), + relationVariables(entry, variant), + selfVariables(entry), + ]); return [ ...reflectionCss(groups), "@media (min-width: 0) {", ...rules(desktop, "desktop"), "}", - ...layers.flatMap((layer) => [`@media ${layer.media} {`, ...rules(layer.predictions, layer.name), "}"]), + ...layers.flatMap((layer) => [ + `@media ${layer.media} {`, + ...rules(layer.predictions, layer.name), + "}", + ]), ] .filter((line) => line.trim().length > 0) .join("\n"); } function mobileMediaQuery(html: string, explicit?: string): string { - if (explicit) return explicit.trim().startsWith("(") ? explicit.trim() : `(${explicit.trim()})`; - const candidates = [...html.matchAll(/@media\s*\(\s*max-width\s*:\s*([^)]+)\)/gi)] + if (explicit) + return explicit.trim().startsWith("(") + ? explicit.trim() + : `(${explicit.trim()})`; + const candidates = [ + ...html.matchAll(/@media\s*\(\s*max-width\s*:\s*([^)]+)\)/gi), + ] .map((match) => match[1]!.trim()) .sort((left, right) => mediaWidth(right) - mediaWidth(left)); return candidates[0] ? `(max-width: ${candidates[0]})` : "(max-width: 48rem)"; @@ -325,10 +389,18 @@ function mediaWidth(value: string): number { return amount * (match[2]?.toLowerCase() === "px" ? 1 : 16); } -function viewportForMobileMedia(query: string, fallback: { width: number; height: number }): { width: number; height: number } { +function viewportForMobileMedia( + query: string, + fallback: { width: number; height: number }, +): { width: number; height: number } { const match = /max-width\s*:\s*([^)\s]+)/i.exec(query); const width = match ? mediaWidth(match[1]!) : 0; - return width > 0 ? { ...fallback, width: Math.min(fallback.width, Math.max(1, Math.floor(width - 1))) } : fallback; + return width > 0 + ? { + ...fallback, + width: Math.min(fallback.width, Math.max(1, Math.floor(width - 1))), + } + : fallback; } const BREAKPOINTS: Record = { @@ -338,9 +410,18 @@ const BREAKPOINTS: Record = { SM: { width: 640, media: "(max-width: 40rem)" }, }; -function predictionLayers(options: LayoutPredictionOptions, html: string): Omit[] { +function predictionLayers( + options: LayoutPredictionOptions, + html: string, +): Omit[] { const requested = options.breakpointAcc - ? [...new Set(Array.isArray(options.breakpointAcc) ? options.breakpointAcc : [options.breakpointAcc])] + ? [ + ...new Set( + Array.isArray(options.breakpointAcc) + ? options.breakpointAcc + : [options.breakpointAcc], + ), + ] : []; if (requested.length) { return requested @@ -352,14 +433,28 @@ function predictionLayers(options: LayoutPredictionOptions, html: string): Omit< })); } const media = mobileMediaQuery(html, options.mobileMediaQuery); - return [{ name: "mobile", media, viewport: options.mobileViewport ?? viewportForMobileMedia(media, { width: 390, height: 844 }) }]; + return [ + { + name: "mobile", + media, + viewport: + options.mobileViewport ?? + viewportForMobileMedia(media, { width: 390, height: 844 }), + }, + ]; } /** Reads the `@measure` targets emitted into stylesheets by the STCSS compiler. */ export function extractPredictSelectors(html: string): string[] { - return [...html.matchAll(/\/\*\s*muddle-predict\s*:([\s\S]*?)\*\//g)].flatMap((match) => (match[1] - ? match[1].split(/[|,]/).map((selector) => selector.trim()).filter(Boolean) - : [])); + return [...html.matchAll(/\/\*\s*muddle-predict\s*:([\s\S]*?)\*\//g)].flatMap( + (match) => + match[1] + ? match[1] + .split(/[|,]/) + .map((selector) => selector.trim()) + .filter(Boolean) + : [], + ); } /** @@ -370,30 +465,69 @@ export function extractPredictSelectors(html: string): string[] { * ``), predictions: [...desktop, ...measuredLayers.flatMap((layer) => layer.predictions)], groups }; + return { + html: rendered.replace("", ``), + predictions: [ + ...desktop, + ...measuredLayers.flatMap((layer) => layer.predictions), + ], + groups, + }; } catch (error) { if (options.strict) throw error; - console.warn(`muddle prediction skipped: ${(error as Error).message.split("\n")[0]}`); + console.warn( + `muddle prediction skipped: ${(error as Error).message.split("\n")[0]}`, + ); return { html, predictions: [], groups: [] }; } finally { await browser?.close(); diff --git a/adapters/server-bun.ts b/adapters/server-bun.ts index 3cb8994..3676045 100644 --- a/adapters/server-bun.ts +++ b/adapters/server-bun.ts @@ -41,7 +41,8 @@ export function serve(app: App, options: BunServerOptions = {}) { const asset = await serveAsset(request, publicDir); if (asset) return asset; } - if (options.predictViews === false || request.method !== "GET") return app.fetch(request); + if (options.predictViews === false || request.method !== "GET") + return app.fetch(request); const key = request.url; let predicted = rendered.get(key); if (!predicted) { @@ -58,14 +59,28 @@ export function serve(app: App, options: BunServerOptions = {}) { }); } -async function renderPredictedView(app: App, request: Request): Promise { +async function renderPredictedView( + app: App, + request: Request, +): Promise { const response = await app.fetch(request); const contentType = response.headers.get("content-type") ?? ""; - if (!response.ok || !contentType.includes("text/html") || response.headers.has("set-cookie") || /no-store/i.test(response.headers.get("cache-control") ?? "")) return response; + if ( + !response.ok || + !contentType.includes("text/html") || + response.headers.has("set-cookie") || + /no-store/i.test(response.headers.get("cache-control") ?? "") + ) + return response; const html = await response.text(); - const hasRelations = /data-muddle-anchor|muddle-predict\s*:|--muddle-relate-/.test(html); + const hasRelations = + /data-muddle-anchor|muddle-predict\s*:|--muddle-relate-/.test(html); if (!hasRelations) return responseFromText(html, response); - const assetHtml = await inlinePredictionStyles(app, html, new URL(request.url).origin); + const assetHtml = await inlinePredictionStyles( + app, + html, + new URL(request.url).origin, + ); const predicted = await predictLayout(assetHtml, { strict: true }); return responseFromText(predicted.html, response); } @@ -73,7 +88,11 @@ async function renderPredictedView(app: App, request: Request): Promise { +export async function inlinePredictionStyles( + app: App, + html: string, + origin: string, +): Promise { for (const match of [...html.matchAll(/]*href="([^"]+)"[^>]*>/g)]) { const url = new URL(match[1]!, origin); - if (url.origin !== origin || !match[0].includes('rel="stylesheet"')) continue; + if (url.origin !== origin || !match[0].includes('rel="stylesheet"')) + continue; const response = await app.fetch(new Request(url.href)); - if (response.ok) html = html.replace(match[0], ``); + if (response.ok) + html = html.replace(match[0], ``); } return html; } -function resolveRenderRoute(source: RenderRouteSource, content?: ContentRegistry): string[] { +function resolveRenderRoute( + source: RenderRouteSource, + content?: ContentRegistry, +): string[] { if (typeof source === "string") return [source]; if (Array.isArray(source)) return source; - return (content?.entries(source.collection) ?? []).map((entry) => source.path.replace(/:id\??/g, encodeURIComponent(entry.id))); + return (content?.entries(source.collection) ?? []).map((entry) => + source.path.replace(/:id\??/g, encodeURIComponent(entry.id)), + ); } /** Path of the html file of a route. */ diff --git a/adapters/vercel-vercel.ts b/adapters/vercel-vercel.ts index 6f18243..f5c627e 100644 --- a/adapters/vercel-vercel.ts +++ b/adapters/vercel-vercel.ts @@ -13,7 +13,11 @@ export type NodeHandler = ( export async function setupVercel(root: string): Promise { const path = join(resolve(root), "vercel.json"); if (existsSync(path)) return path; - await writeFile(path, `${JSON.stringify({ cleanUrls: true }, null, 2)}\n`, "utf8"); + await writeFile( + path, + `${JSON.stringify({ cleanUrls: true }, null, 2)}\n`, + "utf8", + ); return path; } diff --git a/cli.ts b/cli.ts index 54cdfa9..3516931 100755 --- a/cli.ts +++ b/cli.ts @@ -9,12 +9,22 @@ import { loadConfig } from "./core/config.ts"; import { setupWrangler } from "./adapters/cloudflare-wrangler.ts"; import { setupVercel } from "./adapters/vercel-vercel.ts"; import { backportProject } from "./core/backport.ts"; -import { createTemplate, muddleTemplates, type MuddleTemplate } from "./core/templates.ts"; +import { + createTemplate, + muddleTemplates, + type MuddleTemplate, +} from "./core/templates.ts"; -function progress(event: { current: number; total: number; route: string }): void { +function progress(event: { + current: number; + total: number; + route: string; +}): void { const width = 20; const filled = Math.round((event.current / Math.max(1, event.total)) * width); - console.log(`[${"#".repeat(filled)}${".".repeat(width - filled)}] ${event.current}/${event.total} ${event.route}`); + console.log( + `[${"#".repeat(filled)}${".".repeat(width - filled)}] ${event.current}/${event.total} ${event.route}`, + ); } async function dev(entry: string, root: string, port: number): Promise { @@ -36,24 +46,34 @@ async function main(): Promise { const args = process.argv.slice(2); const command = args[0] ?? "help"; const portFlag = args.indexOf("--port"); - const requestedPort = portFlag === -1 ? undefined : Number(args[portFlag + 1]); - const root = args.slice(1).find((argument, index, values) => { - if (argument.startsWith("--")) return false; - return values[index - 1] !== "--port"; - }) ?? process.cwd(); - if (requestedPort !== undefined && (!Number.isInteger(requestedPort) || requestedPort < 1 || requestedPort > 65_535)) { + const requestedPort = + portFlag === -1 ? undefined : Number(args[portFlag + 1]); + const root = + args.slice(1).find((argument, index, values) => { + if (argument.startsWith("--")) return false; + return values[index - 1] !== "--port"; + }) ?? process.cwd(); + if ( + requestedPort !== undefined && + (!Number.isInteger(requestedPort) || + requestedPort < 1 || + requestedPort > 65_535) + ) { throw new Error("--port must be an integer from 1 through 65535"); } if (command === "typegen") { const result = await generate({ root }); - for (const file of result.written) console.log(`muddle generated ${relative(process.cwd(), file)}`); + for (const file of result.written) + console.log(`muddle generated ${relative(process.cwd(), file)}`); return; } if (command === "backport") { const source = args[1] ? resolve(args[1]) : process.cwd(); const destination = args[2] ? resolve(args[2]) : resolve(source, "muddle"); const result = await backportProject(source, destination); - console.log(`muddle backported ${result.source} project to ${result.output} (${result.files.length} files)`); + console.log( + `muddle backported ${result.source} project to ${result.output} (${result.files.length} files)`, + ); return; } if (command === "templates") { @@ -63,9 +83,17 @@ async function main(): Promise { console.log("Usage: muddle templates [destination]"); return; } - if (!muddleTemplates.includes(name)) throw new Error(`Unknown template ${name}. Choose one of: ${muddleTemplates.join(", ")}`); - const result = await createTemplate(name, args[2] ? resolve(args[2]) : resolve(process.cwd(), name)); - console.log(`muddle created ${result.name} template at ${result.output} (${result.files.length} files)`); + if (!muddleTemplates.includes(name)) + throw new Error( + `Unknown template ${name}. Choose one of: ${muddleTemplates.join(", ")}`, + ); + const result = await createTemplate( + name, + args[2] ? resolve(args[2]) : resolve(process.cwd(), name), + ); + console.log( + `muddle created ${result.name} template at ${result.output} (${result.files.length} files)`, + ); return; } if (command === "repo") { @@ -91,8 +119,12 @@ async function main(): Promise { } if (command === "fmt") { const check = process.argv.includes("--check"); - const changed = await format(resolve(root === "--check" ? process.cwd() : root), { check }); - for (const file of changed) console.log(`${check ? "Unformatted" : "Formatted"} ${file}`); + const changed = await format( + resolve(root === "--check" ? process.cwd() : root), + { check }, + ); + for (const file of changed) + console.log(`${check ? "Unformatted" : "Formatted"} ${file}`); if (check && changed.length) process.exitCode = 1; return; } @@ -101,23 +133,35 @@ async function main(): Promise { const config = await loadConfig(target); const entry = resolve(target, config.entry ?? "index.ts"); if (!existsSync(entry)) throw new Error(`No index.ts found in ${root}`); - if (config.adapter?.name === "cloudflare-wrangler") await setupWrangler(target); + if (config.adapter?.name === "cloudflare-wrangler") + await setupWrangler(target); if (config.adapter?.name === "vercel-vercel") await setupVercel(target); - const project = await import(entry) as { build?: (options?: { progress?: typeof progress }) => Promise; app?: unknown }; + const project = (await import(entry)) as { + build?: (options?: { progress?: typeof progress }) => Promise; + app?: unknown; + }; if (command === "build") { - if (!project.build) throw new Error("Project must export build() for muddle build"); + if (!project.build) + throw new Error("Project must export build() for muddle build"); await project.build({ progress }); return; } else if (command === "dev") { - if (!project.app || typeof (project.app as { fetch?: unknown }).fetch !== "function") { - throw new Error("Project entry must export an app with fetch(request) for muddle dev"); + if ( + !project.app || + typeof (project.app as { fetch?: unknown }).fetch !== "function" + ) { + throw new Error( + "Project entry must export an app with fetch(request) for muddle dev", + ); } await dev(entry, target, requestedPort ?? config.adapter?.port ?? 3000); return; } return; } - console.log("muddle [project-root] [--port number]"); + console.log( + "muddle [project-root] [--port number]", + ); } await main(); diff --git a/contracts/index.ts b/contracts/index.ts index a07cc70..e0ef3e2 100644 --- a/contracts/index.ts +++ b/contracts/index.ts @@ -81,7 +81,9 @@ export interface MountableViews { /** Registers a named view component. */ component( name: string, - component: { render: (props: unknown, context: unknown) => string | Promise }, + component: { + render: (props: unknown, context: unknown) => string | Promise; + }, ): unknown; } @@ -91,7 +93,11 @@ export interface Mounts { views?(views: MountableViews): void | Promise; /** Adds routes, e.g. assets, fonts and endpoints. */ routes?( - route: (method: string, path: string, handler: (request: Request) => Response | Promise) => void, + route: ( + method: string, + path: string, + handler: (request: Request) => Response | Promise, + ) => void, ): void | Promise; /** Served inline stylesheets by route. */ styles?: Record string | Promise)>; diff --git a/contracts/typegen/generator.ts b/contracts/typegen/generator.ts index 19a7187..9fb2315 100644 --- a/contracts/typegen/generator.ts +++ b/contracts/typegen/generator.ts @@ -323,16 +323,28 @@ export async function generate( await mkdir(join(out, "orm"), { recursive: true }); const minifyFile = join(out, "minify.ts"); - await writeFile(minifyFile, `export type Simplify = { [K in keyof T]: T[K] } & {};\nexport type Minify = Simplify;\n`, "utf8"); + await writeFile( + minifyFile, + `export type Simplify = { [K in keyof T]: T[K] } & {};\nexport type Minify = Simplify;\n`, + "utf8", + ); const viewsFile = join(out, "views.ts"); await writeFile(viewsFile, renderViewContracts(views), "utf8"); const tablesFile = join(out, "orm", "tables.ts"); await writeFile(tablesFile, renderTableContracts(tables), "utf8"); const indexFile = join(out, "index.ts"); - await writeFile(indexFile, `export * from "./views.ts";\nexport * from "./orm/tables.ts";\nexport * from "./minify.ts";\n`, "utf8"); + await writeFile( + indexFile, + `export * from "./views.ts";\nexport * from "./orm/tables.ts";\nexport * from "./minify.ts";\n`, + "utf8", + ); - return { views, tables, written: [minifyFile, viewsFile, tablesFile, indexFile] }; + return { + views, + tables, + written: [minifyFile, viewsFile, tablesFile, indexFile], + }; } /** Collects the tables of the project by importing the model files. */ @@ -384,7 +396,20 @@ export async function collectTables( const module = (await import(file)) as Record; for (const exported of Object.values(module)) { if (typeof exported !== "function") continue; - const metadata = getMetadata("muddle:orm:table", exported as object) as { name: string; fields: Map } | undefined; + const metadata = getMetadata("muddle:orm:table", exported as object) as + | { + name: string; + fields: Map< + string, + { + property: string; + column: string; + type: string; + nullable: boolean; + } + >; + } + | undefined; if (!metadata) continue; tables.push({ name: metadata.name, @@ -402,7 +427,9 @@ export async function collectTables( } async function listFiles(dir: string, suffix: string): Promise { - const entries = existsSync(dir) ? await readdir(dir, { withFileTypes: true }) : []; + const entries = existsSync(dir) + ? await readdir(dir, { withFileTypes: true }) + : []; const files: string[] = []; for (const entry of entries) { const path = join(dir, entry.name); diff --git a/contracts/typegen/views.ts b/contracts/typegen/views.ts index b255d6c..175fa9b 100644 --- a/contracts/typegen/views.ts +++ b/contracts/typegen/views.ts @@ -1,6 +1,8 @@ import type { Minify } from "./minify.ts"; /** Fallback view contracts. Project contracts are generated under `_muddle/contracts`. */ -export interface ViewProps { [path: string]: Record } +export interface ViewProps { + [path: string]: Record; +} export type ViewPath = string; export type ViewPropsOf

= Minify; diff --git a/core/app.ts b/core/app.ts index 645c308..1ab5470 100644 --- a/core/app.ts +++ b/core/app.ts @@ -23,7 +23,10 @@ export interface ViewRenderer { context: RequestContext, ): Promise | string | RenderedView; /** Registry of named components, mount targets of the integrations. */ - components?: Map; + components?: Map< + string, + { name: string; component: unknown; deferred: boolean } + >; } export interface AppOptions { @@ -71,13 +74,19 @@ export class App { } async transformAsset(asset: BuildAsset): Promise { - for (const integration of this.integrations) if (integration.transform) asset = await integration.transform(asset); + for (const integration of this.integrations) + if (integration.transform) asset = await integration.transform(asset); return asset; } - async analyzePage(page: { route: string; html: string }): Promise> { + async analyzePage(page: { + route: string; + html: string; + }): Promise> { const reports: Record = {}; - for (const integration of this.integrations) if (integration.analyze) reports[integration.name] = await integration.analyze(page); + for (const integration of this.integrations) + if (integration.analyze) + reports[integration.name] = await integration.analyze(page); return reports; } @@ -101,19 +110,31 @@ export class App { await integration.setup?.(integrationContext); for (const mount of integration.mounts ?? []) { if (mount.views && this.views) { - await mount.views(this.views as unknown as Parameters>[0]); + await mount.views( + this.views as unknown as Parameters< + NonNullable + >[0], + ); } if (mount.routes) { await mount.routes((method, path, handler) => - this.router.raw(method, path, (_request, _context) => handler(_request)), + this.router.raw(method, path, (_request, _context) => + handler(_request), + ), ); } if (mount.styles) { for (const [path, value] of Object.entries(mount.styles)) { - this.router.raw("GET", path, async () => - new Response(typeof value === "function" ? await value() : value, { - headers: { "content-type": "text/css; charset=utf-8" }, - }), + this.router.raw( + "GET", + path, + async () => + new Response( + typeof value === "function" ? await value() : value, + { + headers: { "content-type": "text/css; charset=utf-8" }, + }, + ), ); } } @@ -149,7 +170,8 @@ export class App { Object.assign(context.locals, this.server.locals); const response = await this.handle(request, context); const fetchResponse = toFetchResponse(response); - for (const [name, value] of Object.entries(this.server.headers)) fetchResponse.headers.set(name, value); + for (const [name, value] of Object.entries(this.server.headers)) + fetchResponse.headers.set(name, value); return fetchResponse; } @@ -158,7 +180,8 @@ export class App { request: Request, context: RequestContext, ): Promise { - for (const integration of this.integrations) await integration.request?.(context); + for (const integration of this.integrations) + await integration.request?.(context); context.body = await parseRequestBody(request); context.form = toFormFields( context.body && diff --git a/core/backport.ts b/core/backport.ts index ce490d6..31ae135 100644 --- a/core/backport.ts +++ b/core/backport.ts @@ -31,13 +31,15 @@ const ASSET_EXTENSIONS = new Set([ export function detectBackportSource(root: string): BackportSource { if ( existsSync(join(root, "config.toml")) || - existsSync(join(root, "content")) && existsSync(join(root, "templates")) - ) return "zola"; + (existsSync(join(root, "content")) && existsSync(join(root, "templates"))) + ) + return "zola"; if ( existsSync(join(root, "astro.config.mjs")) || existsSync(join(root, "astro.config.ts")) || existsSync(join(root, "src", "pages")) - ) return "astro"; + ) + return "astro"; throw new Error(`Could not identify a Zola or Astro project in ${root}`); } @@ -60,28 +62,45 @@ export async function backportProject( files.push(relative(outputRoot, path).replaceAll("\\", "/")); }; - const contentRoot = join(inputRoot, source === "astro" ? "src/content" : "content"); - await writeProjectShell(outputRoot, write, await collectionNames(contentRoot), source === "zola" ? await zolaSiteConfig(join(inputRoot, "config.toml")) : {}); - const mappings = source === "astro" - ? [ - { from: "src/pages", to: "modules/app/views" }, - { from: "src/layouts", to: "modules/app/views/layouts" }, - { from: "src/components", to: "modules/app/views/components" }, - { from: "src/content", to: "content" }, - { from: "public", to: "public" }, - { from: "src/assets", to: "public/assets" }, - { from: "src/styles", to: "styles" }, - ] - : [ - { from: "templates", to: "modules/app/views" }, - { from: "content", to: "content" }, - { from: "static", to: "public" }, - { from: "sass", to: "styles" }, - ]; + const contentRoot = join( + inputRoot, + source === "astro" ? "src/content" : "content", + ); + await writeProjectShell( + outputRoot, + write, + await collectionNames(contentRoot), + source === "zola" + ? await zolaSiteConfig(join(inputRoot, "config.toml")) + : {}, + ); + const mappings = + source === "astro" + ? [ + { from: "src/pages", to: "modules/app/views" }, + { from: "src/layouts", to: "modules/app/views/layouts" }, + { from: "src/components", to: "modules/app/views/components" }, + { from: "src/content", to: "content" }, + { from: "public", to: "public" }, + { from: "src/assets", to: "public/assets" }, + { from: "src/styles", to: "styles" }, + ] + : [ + { from: "templates", to: "modules/app/views" }, + { from: "content", to: "content" }, + { from: "static", to: "public" }, + { from: "sass", to: "styles" }, + ]; for (const mapping of mappings) { const folder = join(inputRoot, mapping.from); if (existsSync(folder)) { - await convertFolder(folder, join(outputRoot, mapping.to), write, source, folder); + await convertFolder( + folder, + join(outputRoot, mapping.to), + write, + source, + folder, + ); } } return { source, output: outputRoot, files }; @@ -101,7 +120,10 @@ async function zolaSiteConfig(path: string): Promise> { const source = await readFile(path, "utf8"); const values: Record = {}; for (const key of ["title", "base_url", "description", "default_language"]) { - const match = new RegExp(`^\\s*${key}\\s*=\\s*["']([^"']*)["']\\s*$`, "m").exec(source); + const match = new RegExp( + `^\\s*${key}\\s*=\\s*["']([^"']*)["']\\s*$`, + "m", + ).exec(source); if (match?.[1] !== undefined) values[key] = match[1]; } return values; @@ -162,8 +184,10 @@ async function convertFolder( } const extension = extname(entry.name).toLowerCase(); if ( - extension === ".astro" || extension === ".html" || - extension === ".htm" || extension === ".tera" + extension === ".astro" || + extension === ".html" || + extension === ".htm" || + extension === ".tera" ) { await write( to.replace(/\.(astro|html?|tera)$/i, ".spug"), @@ -172,7 +196,10 @@ async function convertFolder( : zolaToSpug(await readFile(from, "utf8")), ); } else if (extension === ".tsx" || extension === ".jsx") { - await write(to.replace(/\.(tsx|jsx)$/i, ".spug"), reactToSpug(await readFile(from, "utf8"), from)); + await write( + to.replace(/\.(tsx|jsx)$/i, ".spug"), + reactToSpug(await readFile(from, "utf8"), from), + ); } else if (extension === ".css") { await write( to.replace(/\.css$/i, ".stcss"), @@ -184,7 +211,8 @@ async function convertFolder( await readFile(from, "utf8"), ); } else if ( - extension === ".md" || extension === ".markdown" || + extension === ".md" || + extension === ".markdown" || ASSET_EXTENSIONS.has(extension) ) { await mkdir(dirname(to), { recursive: true }); @@ -193,16 +221,36 @@ async function convertFolder( } } -export function reactToSpug(source: string, filename = "component.tsx"): string { - const file = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); +export function reactToSpug( + source: string, + filename = "component.tsx", +): string { + const file = ts.createSourceFile( + filename, + source, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TSX, + ); const analysis = analyzeReactComponent(source, filename); const imports = new Set(); - let root: ts.JsxChild | ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment | undefined; + let root: + | ts.JsxChild + | ts.JsxElement + | ts.JsxSelfClosingElement + | ts.JsxFragment + | undefined; const visit = (node: ts.Node): void => { - if (ts.isImportDeclaration(node) && node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) { - for (const element of node.importClause.namedBindings.elements) imports.add(element.name.text); + if ( + ts.isImportDeclaration(node) && + node.importClause?.namedBindings && + ts.isNamedImports(node.importClause.namedBindings) + ) { + for (const element of node.importClause.namedBindings.elements) + imports.add(element.name.text); } - if (ts.isImportDeclaration(node) && node.importClause?.name) imports.add(node.importClause.name.text); + if (ts.isImportDeclaration(node) && node.importClause?.name) + imports.add(node.importClause.name.text); if (ts.isReturnStatement(node) && node.expression && !root) { const expression = unwrapJsxExpression(node.expression); if (isJsxNode(expression)) root = expression; @@ -215,10 +263,13 @@ export function reactToSpug(source: string, filename = "component.tsx"): string }; visit(file); if (!root) throw new Error(`No JSX return value found in ${filename}`); - const aot = analysis.state.map((state) => ` const ${state.name} = ${state.initial};`); - const script = aot.length || analysis.hooks.length - ? `javascript!\n // React hooks lowered during static component composition: ${analysis.hooks.join(", ") || "none"}\n${aot.join("\n")}\nend\n` - : ""; + const aot = analysis.state.map( + (state) => ` const ${state.name} = ${state.initial};`, + ); + const script = + aot.length || analysis.hooks.length + ? `javascript!\n // React hooks lowered during static component composition: ${analysis.hooks.join(", ") || "none"}\n${aot.join("\n")}\nend\n` + : ""; return `${script}${jsxToSpug(root, imports).trim()}\n`; } @@ -228,19 +279,42 @@ export interface ReactHookAnalysis { } /** Finds React hooks and extracts AOT-safe initial state from a component. */ -export function analyzeReactComponent(source: string, filename = "component.tsx"): ReactHookAnalysis { - const file = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); +export function analyzeReactComponent( + source: string, + filename = "component.tsx", +): ReactHookAnalysis { + const file = ts.createSourceFile( + filename, + source, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TSX, + ); const hooks = new Set(); const state: { name: string; initial: string }[] = []; const visit = (node: ts.Node): void => { if (ts.isCallExpression(node)) { - const name = ts.isIdentifier(node.expression) ? node.expression.text - : ts.isPropertyAccessExpression(node.expression) ? node.expression.name.text : ""; + const name = ts.isIdentifier(node.expression) + ? node.expression.text + : ts.isPropertyAccessExpression(node.expression) + ? node.expression.name.text + : ""; if (/^use[A-Z]/.test(name)) hooks.add(name); - if (name === "useState" && ts.isVariableDeclaration(node.parent) && ts.isArrayBindingPattern(node.parent.name)) { + if ( + name === "useState" && + ts.isVariableDeclaration(node.parent) && + ts.isArrayBindingPattern(node.parent.name) + ) { const binding = node.parent.name.elements[0]; - if (binding && ts.isBindingElement(binding) && ts.isIdentifier(binding.name)) { - state.push({ name: binding.name.text, initial: node.arguments[0]?.getText() ?? "undefined" }); + if ( + binding && + ts.isBindingElement(binding) && + ts.isIdentifier(binding.name) + ) { + state.push({ + name: binding.name.text, + initial: node.arguments[0]?.getText() ?? "undefined", + }); } } } @@ -251,14 +325,26 @@ export function analyzeReactComponent(source: string, filename = "component.tsx" } function unwrapJsxExpression(node: ts.Expression): ts.Expression { - return ts.isParenthesizedExpression(node) ? unwrapJsxExpression(node.expression) : node; + return ts.isParenthesizedExpression(node) + ? unwrapJsxExpression(node.expression) + : node; } -function isJsxNode(node: ts.Node): node is ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment { - return ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node); +function isJsxNode( + node: ts.Node, +): node is ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment { + return ( + ts.isJsxElement(node) || + ts.isJsxSelfClosingElement(node) || + ts.isJsxFragment(node) + ); } -function jsxToSpug(node: ts.JsxChild | ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment, imports: Set, depth = 0): string { +function jsxToSpug( + node: ts.JsxChild | ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment, + imports: Set, + depth = 0, +): string { const pad = " ".repeat(depth); if (ts.isJsxText(node)) { const text = node.getText().replace(/\s+/g, " ").trim(); @@ -268,22 +354,40 @@ function jsxToSpug(node: ts.JsxChild | ts.JsxElement | ts.JsxSelfClosingElement if (!node.expression) return ""; return `${pad}= ${node.expression.getText()}`; } - if (ts.isJsxFragment(node)) return node.children.map((child) => jsxToSpug(child, imports, depth)).filter(Boolean).join("\n"); + if (ts.isJsxFragment(node)) + return node.children + .map((child) => jsxToSpug(child, imports, depth)) + .filter(Boolean) + .join("\n"); const opening = ts.isJsxElement(node) ? node.openingElement : node; const rawName = opening.tagName.getText(); const name = rawName === "Comp" ? "button" : rawName; - const component = ( /^[A-Z]/.test(name) || imports.has(name)) && rawName !== "Comp"; + const component = + (/^[A-Z]/.test(name) || imports.has(name)) && rawName !== "Comp"; const attributes = jsxAttributes(opening.attributes.properties, component); - const line = component ? `${pad}component ${kebab(name)}` : `${pad}${name}${attributes.inline}`; - const props = component && attributes.props.length ? `\n${attributes.props.map((property) => `${pad} ${property}`).join("\n")}` : ""; + const line = component + ? `${pad}component ${kebab(name)}` + : `${pad}${name}${attributes.inline}`; + const props = + component && attributes.props.length + ? `\n${attributes.props.map((property) => `${pad} ${property}`).join("\n")}` + : ""; if (ts.isJsxSelfClosingElement(node)) { - return depth === 0 ? `${line}${props}\n${pad} if content\n${pad} != await content()` : `${line}${props}`; + return depth === 0 + ? `${line}${props}\n${pad} if content\n${pad} != await content()` + : `${line}${props}`; } - const children = node.children.map((child) => jsxToSpug(child, imports, depth + 1)).filter(Boolean).join("\n"); + const children = node.children + .map((child) => jsxToSpug(child, imports, depth + 1)) + .filter(Boolean) + .join("\n"); return [line + props, children].filter(Boolean).join("\n"); } -function jsxAttributes(properties: ts.NodeArray, component = false): { inline: string; props: string[] } { +function jsxAttributes( + properties: ts.NodeArray, + component = false, +): { inline: string; props: string[] } { const inline: string[] = []; const props: string[] = []; for (const property of properties) { @@ -291,14 +395,23 @@ function jsxAttributes(properties: ts.NodeArray, component const name = property.name.getText(); if (name === "ref") continue; const initializer = property.initializer; - if (!initializer) { inline.push(`${name}=true`); continue; } - const value = ts.isStringLiteral(initializer) ? JSON.stringify(initializer.text) - : ts.isJsxExpression(initializer) && initializer.expression ? `(${initializer.expression.getText()})` - : JSON.stringify(initializer.getText()); + if (!initializer) { + inline.push(`${name}=true`); + continue; + } + const value = ts.isStringLiteral(initializer) + ? JSON.stringify(initializer.text) + : ts.isJsxExpression(initializer) && initializer.expression + ? `(${initializer.expression.getText()})` + : JSON.stringify(initializer.getText()); const normalized = name === "className" ? "class" : name; - const classValue = normalized === "class" && ts.isJsxExpression(initializer) && initializer.expression - ? staticClassValue(initializer.expression) ?? JSON.stringify("muddle-react") - : value; + const classValue = + normalized === "class" && + ts.isJsxExpression(initializer) && + initializer.expression + ? (staticClassValue(initializer.expression) ?? + JSON.stringify("muddle-react")) + : value; if (component) props.push(`${normalized}: ${classValue}`); else inline.push(`${normalized}=${classValue}`); } @@ -308,12 +421,17 @@ function jsxAttributes(properties: ts.NodeArray, component function staticClassValue(expression: ts.Expression): string | undefined { if (ts.isStringLiteral(expression)) return JSON.stringify(expression.text); if (!ts.isCallExpression(expression)) return undefined; - const classes = expression.arguments.flatMap((argument) => ts.isStringLiteral(argument) ? [argument.text] : []); + const classes = expression.arguments.flatMap((argument) => + ts.isStringLiteral(argument) ? [argument.text] : [], + ); return classes.length ? JSON.stringify(classes.join(" ")) : undefined; } function kebab(value: string): string { - return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").toLowerCase(); + return value + .replace(/([a-z0-9])([A-Z])/g, "$1-$2") + .replace(/_/g, "-") + .toLowerCase(); } export function cssToStcss(source: string, filename = "style.css"): string { @@ -349,10 +467,10 @@ export function cssToStcss(source: string, filename = "style.css"): string { } export function htmlToSpug(source: string): string { - const body = source.replace(/^---[\s\S]*?---\s*/m, "").replace( - /^---[\s\S]*?---\s*/m, - "", - ).replace(//g, ""); + const body = source + .replace(/^---[\s\S]*?---\s*/m, "") + .replace(/^---[\s\S]*?---\s*/m, "") + .replace(//g, ""); const tokens = body.match(/<[^>]+>|[^<]+/g) ?? []; const lines: string[] = []; let depth = 0; @@ -385,14 +503,15 @@ export function htmlToSpug(source: string): string { "track", "wbr", ]).has(tag) - ) depth += 1; + ) + depth += 1; } else { const text = token.replace(/\s+/g, " ").trim(); if (text) { lines.push( - `${" ".repeat(depth)}| ${ - text.replace(/\{\{\s*(.*?)\s*\}\}/g, "#{ $1 }").replace(/\{([A-Za-z_$][\w$]*(?:\.[\w$]+)*)\}/g, "#{ $1 }") - }`, + `${" ".repeat(depth)}| ${text + .replace(/\{\{\s*(.*?)\s*\}\}/g, "#{ $1 }") + .replace(/\{([A-Za-z_$][\w$]*(?:\.[\w$]+)*)\}/g, "#{ $1 }")}`, ); } } @@ -401,11 +520,17 @@ export function htmlToSpug(source: string): string { } /** Converts an Astro source file without evaluating its frontmatter. */ -export function astroToSpug(source: string, file = "page.astro", pagesRoot = ""): string { +export function astroToSpug( + source: string, + file = "page.astro", + pagesRoot = "", +): string { const frontmatter = /^---\s*\n([\s\S]*?)\n---\s*\n?/.exec(source)?.[1] ?? ""; let body = source.replace(/^---\s*\n[\s\S]*?\n---\s*\n?/, ""); const imports = new Map(); - for (const entry of frontmatter.matchAll(/import\s+([A-Za-z_$][\w$]*)\s+from\s+["']([^"']+)["'];?/g)) { + for (const entry of frontmatter.matchAll( + /import\s+([A-Za-z_$][\w$]*)\s+from\s+["']([^"']+)["'];?/g, + )) { imports.set(entry[1]!, entry[2]!); } const layout = /^\s*<([A-Z][\w$]*)\b([^>]*)>([\s\S]*)<\/\1>\s*$/.exec(body); @@ -415,14 +540,23 @@ export function astroToSpug(source: string, file = "page.astro", pagesRoot = "") if (/(?:^|\/)layouts\//.test(imported)) { const absoluteLayout = resolve(dirname(file), imported); const pageRelative = relative(pagesRoot, file); - const layoutRelative = relative(dirname(pagesRoot), absoluteLayout).replace(/\.astro$/, ".spug"); - const target = relative(dirname(pageRelative), layoutRelative).replaceAll("\\", "/"); + const layoutRelative = relative( + dirname(pagesRoot), + absoluteLayout, + ).replace(/\.astro$/, ".spug"); + const target = relative(dirname(pageRelative), layoutRelative).replaceAll( + "\\", + "/", + ); prefix = `extends ${target}\n\nblock content\n`; body = layout[3]!; } } body = body - .replace(//g, "") + .replace( + //g, + '', + ) .replace(//g, ""); const converted = htmlToSpug(body) .replace(/^(\s*)muddle-slot\(data-name="([\w-]+)"\)\s*$/gm, "$1block $2") @@ -435,8 +569,16 @@ export function astroToSpug(source: string, file = "page.astro", pagesRoot = "") export function zolaToSpug(source: string): string { let result = source .replace(/\{#([\s\S]*?)#\}/g, "") - .replace(/\{%\s*extends\s+["']([^"']+)["']\s*%\}/g, (_all, path: string) => `extends ./${path.replace(/\.html?$|\.tera$/i, ".spug")}`) - .replace(/\{%\s*include\s+["']([^"']+)["']\s*%\}/g, (_all, path: string) => `include ./${path.replace(/\.html?$|\.tera$/i, ".spug")}`) + .replace( + /\{%\s*extends\s+["']([^"']+)["']\s*%\}/g, + (_all, path: string) => + `extends ./${path.replace(/\.html?$|\.tera$/i, ".spug")}`, + ) + .replace( + /\{%\s*include\s+["']([^"']+)["']\s*%\}/g, + (_all, path: string) => + `include ./${path.replace(/\.html?$|\.tera$/i, ".spug")}`, + ) .replace(/\{%\s*block\s+([\w-]+)\s*%\}/g, "block $1") .replace(/\{%\s*endblock\s*%\}/g, "") .replace(/\{%\s*if\s+(.+?)\s*%\}/g, "if $1") @@ -452,33 +594,50 @@ function componentCalls(source: string, imports: Map): string { const output: string[] = []; for (const line of source.split("\n")) { const match = /^(\s*)([A-Z][\w$]*)(.*)$/.exec(line); - if (!match || !imports.has(match[2]!)) { output.push(line); continue; } + if (!match || !imports.has(match[2]!)) { + output.push(line); + continue; + } const imported = imports.get(match[2]!)!; - const name = imported.replace(/^.*\//, "").replace(/\.astro$/, "").replace(/[A-Z]/g, (letter, index) => `${index ? "-" : ""}${letter.toLowerCase()}`); + const name = imported + .replace(/^.*\//, "") + .replace(/\.astro$/, "") + .replace( + /[A-Z]/g, + (letter, index) => `${index ? "-" : ""}${letter.toLowerCase()}`, + ); const attributeText = /\((.*)\)$/.exec(match[3]!)?.[1]; output.push(`${match[1]}component ${name}`); for (const pair of attributeText?.split(/\s*,\s*/) ?? []) { const [key = "", raw = "true"] = pair.split(/=(.+)/); - const expression = /^\((.*)\)$/.exec(raw.trim())?.[1] ?? raw.trim().replace(/^\{\s*|\s*\}$/g, ""); + const expression = + /^\((.*)\)$/.exec(raw.trim())?.[1] ?? + raw.trim().replace(/^\{\s*|\s*\}$/g, ""); const dynamic = raw.trim().startsWith("{") || raw.trim().startsWith("("); - output.push(`${match[1]} ${key.trim()}: ${dynamic ? expression : expression === "true" ? "true" : JSON.stringify(expression)}`); + output.push( + `${match[1]} ${key.trim()}: ${dynamic ? expression : expression === "true" ? "true" : JSON.stringify(expression)}`, + ); } } return `${output.filter(Boolean).join("\n").trim()}\n`; } function indent(source: string): string { - return source.split("\n").filter(Boolean).map((line) => ` ${line}`).join("\n") + "\n"; + return ( + source + .split("\n") + .filter(Boolean) + .map((line) => ` ${line}`) + .join("\n") + "\n" + ); } function attributesToSpug(raw: string): string { const classes: string[] = []; const attributes: string[] = []; - for ( - const match of raw.matchAll( - /([\w:-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g, - ) - ) { + for (const match of raw.matchAll( + /([\w:-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g, + )) { const name = match[1]!; const value = match[2] ?? match[3] ?? match[4]; if (name === "class" && value) { @@ -490,7 +649,8 @@ function attributesToSpug(raw: string): string { continue; } if (value === undefined) attributes.push(`${name}=true`); - else if (/^\{[\s\S]*\}$/.test(value)) attributes.push(`${name}=(${value.slice(1, -1).trim()})`); + else if (/^\{[\s\S]*\}$/.test(value)) + attributes.push(`${name}=(${value.slice(1, -1).trim()})`); else attributes.push(`${name}=${JSON.stringify(value)}`); } return `${classes.map((value) => `.${value}`).join("")}${attributes.length ? `(${attributes.join(", ")})` : ""}`; diff --git a/core/cache.ts b/core/cache.ts index 362cd08..9fe839e 100644 --- a/core/cache.ts +++ b/core/cache.ts @@ -4,7 +4,11 @@ import { join } from "node:path"; export interface Cache { get(key: string): Promise; - put(key: string, value: Uint8Array, options?: { ttl?: number }): Promise; + put( + key: string, + value: Uint8Array, + options?: { ttl?: number }, + ): Promise; delete(key: string): Promise; } @@ -39,17 +43,27 @@ export function filesystemCache(dir: string): Cache { }; } -export function cloudflareCache(cache: CloudflareCacheLike, origin = "https://muddle.cache"): Cache { - const request = (key: string) => new Request(new URL(`/${encodeURIComponent(key)}`, origin).toString()); +export function cloudflareCache( + cache: CloudflareCacheLike, + origin = "https://muddle.cache", +): Cache { + const request = (key: string) => + new Request(new URL(`/${encodeURIComponent(key)}`, origin).toString()); return { async get(key) { const response = await cache.match(request(key)); - return response ? new Uint8Array(await response.arrayBuffer()) : undefined; + return response + ? new Uint8Array(await response.arrayBuffer()) + : undefined; }, async put(key, value, options) { - const headers = options?.ttl ? { "cache-control": `max-age=${options.ttl}` } : undefined; + const headers = options?.ttl + ? { "cache-control": `max-age=${options.ttl}` } + : undefined; await cache.put(request(key), new Response(value, { headers })); }, - async delete(key) { await cache.delete(request(key)); }, + async delete(key) { + await cache.delete(request(key)); + }, }; } diff --git a/core/content.ts b/core/content.ts index 43d1554..bbcf77a 100644 --- a/core/content.ts +++ b/core/content.ts @@ -131,12 +131,24 @@ export class ContentRegistry { } } -async function listCollectionFiles(dir: string, extensions: string[], prefix = ""): Promise { +async function listCollectionFiles( + dir: string, + extensions: string[], + prefix = "", +): Promise { const files: string[] = []; for (const entry of await readdir(dir, { withFileTypes: true })) { const relativePath = join(prefix, entry.name); - if (entry.isDirectory()) files.push(...(await listCollectionFiles(join(dir, entry.name), extensions, relativePath))); - else if (extensions.some((extension) => entry.name.endsWith(extension))) files.push(relativePath); + if (entry.isDirectory()) + files.push( + ...(await listCollectionFiles( + join(dir, entry.name), + extensions, + relativePath, + )), + ); + else if (extensions.some((extension) => entry.name.endsWith(extension))) + files.push(relativePath); } return files.sort(); } diff --git a/core/context.ts b/core/context.ts index 4657acf..b936c0e 100644 --- a/core/context.ts +++ b/core/context.ts @@ -1,24 +1,87 @@ import type { Headers } from "../contracts/typegen/http/headers.ts"; export class ServerBuildContext { - private readonly values: { headers: Headers; locals: Record } = { headers: {}, locals: {} }; + private readonly values: { + headers: Headers; + locals: Record; + } = { headers: {}, locals: {} }; - header(name: string, value: string): this { this.values.headers[name.toLowerCase()] = value; return this; } - headers(values: Headers): this { Object.assign(this.values.headers, values); return this; } - local(name: string, value: unknown): this { this.values.locals[name] = value; return this; } - build(): Readonly<{ headers: Headers; locals: Record }> { return { headers: { ...this.values.headers }, locals: { ...this.values.locals } }; } + header(name: string, value: string): this { + this.values.headers[name.toLowerCase()] = value; + return this; + } + headers(values: Headers): this { + Object.assign(this.values.headers, values); + return this; + } + local(name: string, value: unknown): this { + this.values.locals[name] = value; + return this; + } + build(): Readonly<{ headers: Headers; locals: Record }> { + return { + headers: { ...this.values.headers }, + locals: { ...this.values.locals }, + }; + } } export class TemplateContext { - readonly imports = new Map, context: { locals: Record; params: Record }): Promise | string }>(); - import(name: string, component: { render(props: Record, context: { locals: Record; params: Record }): Promise | string }): this { - if (!/^[A-Za-z][\w-]*$/.test(name)) throw new Error("Invalid component import name"); + readonly imports = new Map< + string, + { + render( + props: Record, + context: { + locals: Record; + params: Record; + }, + ): Promise | string; + } + >(); + import( + name: string, + component: { + render( + props: Record, + context: { + locals: Record; + params: Record; + }, + ): Promise | string; + }, + ): this { + if (!/^[A-Za-z][\w-]*$/.test(name)) + throw new Error("Invalid component import name"); this.imports.set(name, component); return this; } - private readonly values: { templates: string[]; inputs: Record; contracts: Record } = { templates: [], inputs: {}, contracts: {} }; - template(path: string): this { this.values.templates.push(path); return this; } - input(name: string, value: unknown): this { this.values.inputs[name] = value; return this; } - contract(name: string, type: string): this { this.values.contracts[name] = type; return this; } - build(): Readonly<{ templates: string[]; inputs: Record; contracts: Record }> { return { templates: [...this.values.templates], inputs: { ...this.values.inputs }, contracts: { ...this.values.contracts } }; } + private readonly values: { + templates: string[]; + inputs: Record; + contracts: Record; + } = { templates: [], inputs: {}, contracts: {} }; + template(path: string): this { + this.values.templates.push(path); + return this; + } + input(name: string, value: unknown): this { + this.values.inputs[name] = value; + return this; + } + contract(name: string, type: string): this { + this.values.contracts[name] = type; + return this; + } + build(): Readonly<{ + templates: string[]; + inputs: Record; + contracts: Record; + }> { + return { + templates: [...this.values.templates], + inputs: { ...this.values.inputs }, + contracts: { ...this.values.contracts }, + }; + } } diff --git a/core/format-source.ts b/core/format-source.ts index 3120e4b..1773901 100644 --- a/core/format-source.ts +++ b/core/format-source.ts @@ -1,30 +1,58 @@ import { foldLines } from "../views/engine/lexer.ts"; -export interface FormatOptions { tabWidth?: number; useTabs?: boolean; printWidth?: number } +export interface FormatOptions { + tabWidth?: number; + useTabs?: boolean; + printWidth?: number; +} export type FormatLanguage = "spug" | "stpug" | "stug" | "stcss"; /** Shared by the CLI and Prettier plugin, with no subprocesses or write side effects. */ -export async function formatSource(source: string, language: FormatLanguage, options: FormatOptions = {}): Promise { +export async function formatSource( + source: string, + language: FormatLanguage, + options: FormatOptions = {}, +): Promise { const prettier = await import("prettier"); const scripts: string[] = []; - let protectedSource = source.replace(/(^[\t ]*)(javascript!|typescript!|---)\s*\n([\s\S]*?)^\s*(end|---)\s*$/gm, (_match, indentation: string, directive: string, code: string, ending: string) => { - const index = scripts.length; - scripts.push(JSON.stringify({ directive, code, ending })); - return language === "stcss" ? `/* MUDDLE_SCRIPT_${index} */` : `${indentation}MUDDLE_SCRIPT_${index}`; - }); + let protectedSource = source.replace( + /(^[\t ]*)(javascript!|typescript!|---)\s*\n([\s\S]*?)^\s*(end|---)\s*$/gm, + ( + _match, + indentation: string, + directive: string, + code: string, + ending: string, + ) => { + const index = scripts.length; + scripts.push(JSON.stringify({ directive, code, ending })); + return language === "stcss" + ? `/* MUDDLE_SCRIPT_${index} */` + : `${indentation}MUDDLE_SCRIPT_${index}`; + }, + ); const rawBlocks: { marker: string; body: string[]; prefix: string }[] = []; if (language !== "stcss") { const sourceLines = protectedSource.split("\n"); const output: string[] = []; for (let index = 0; index < sourceLines.length; index++) { const line = sourceLines[index]!; - const match = /^(\s*)(?:[\w.#-]+(?:\([^]*\))?\.|\/\/-?|\$\$\$)\s*$/.exec(line); - if (!match) { output.push(line); continue; } + const match = /^(\s*)(?:[\w.#-]+(?:\([^]*\))?\.|\/\/-?|\$\$\$)\s*$/.exec( + line, + ); + if (!match) { + output.push(line); + continue; + } const prefix = match[1]!; const body: string[] = [line.slice(prefix.length)]; while (index + 1 < sourceLines.length) { const next = sourceLines[index + 1]!; - if (next.trim() && (next.match(/^\s*/)?.[0].length ?? 0) <= prefix.length) break; + if ( + next.trim() && + (next.match(/^\s*/)?.[0].length ?? 0) <= prefix.length + ) + break; body.push(next.startsWith(prefix) ? next.slice(prefix.length) : next); index++; } @@ -37,12 +65,19 @@ export async function formatSource(source: string, language: FormatLanguage, opt const unit = options.useTabs ? "\t" : " ".repeat(options.tabWidth ?? 2); let formatted: string; if (language === "stcss") { - formatted = await prettier.format(protectedSource, { parser: "scss", plugins: [], tabWidth: options.tabWidth ?? 2, useTabs: options.useTabs ?? false, printWidth: options.printWidth ?? 80 }); + formatted = await prettier.format(protectedSource, { + parser: "scss", + plugins: [], + tabWidth: options.tabWidth ?? 2, + useTabs: options.useTabs ?? false, + printWidth: options.printWidth ?? 80, + }); } else { const stack = [0]; const lines: string[] = []; for (const line of foldLines(protectedSource)) { - while (stack.length > 1 && line.indent < stack[stack.length - 1]!) stack.pop(); + while (stack.length > 1 && line.indent < stack[stack.length - 1]!) + stack.pop(); if (line.indent > stack[stack.length - 1]!) stack.push(line.indent); const indent = unit.repeat(stack.length - 1); lines.push(`${indent}${line.text.trimEnd()}`); @@ -50,14 +85,35 @@ export async function formatSource(source: string, language: FormatLanguage, opt formatted = `${lines.join("\n")}\n`; } for (let index = 0; index < scripts.length; index++) { - const script = JSON.parse(scripts[index]!) as { directive: string; code: string; ending: string }; - const code = await prettier.format(script.code, { parser: "typescript", plugins: [], tabWidth: options.tabWidth ?? 2, useTabs: options.useTabs ?? false }); - const marker = language === "stcss" ? new RegExp(`^[\\t ]*/\\* MUDDLE_SCRIPT_${index} \\*/`, "m") : new RegExp(`^([\\t ]*)MUDDLE_SCRIPT_${index}`, "m"); + const script = JSON.parse(scripts[index]!) as { + directive: string; + code: string; + ending: string; + }; + const code = await prettier.format(script.code, { + parser: "typescript", + plugins: [], + tabWidth: options.tabWidth ?? 2, + useTabs: options.useTabs ?? false, + }); + const marker = + language === "stcss" + ? new RegExp(`^[\\t ]*/\\* MUDDLE_SCRIPT_${index} \\*/`, "m") + : new RegExp(`^([\\t ]*)MUDDLE_SCRIPT_${index}`, "m"); formatted = formatted.replace(marker, (match) => { const indentation = match.match(/^[\t ]*/)?.[0] ?? ""; - return `${indentation}${script.directive}\n${code.trimEnd().split("\n").map((line) => `${indentation}${line}`).join("\n")}\n${indentation}${script.ending}`; + return `${indentation}${script.directive}\n${code + .trimEnd() + .split("\n") + .map((line) => `${indentation}${line}`) + .join("\n")}\n${indentation}${script.ending}`; }); } - for (const block of rawBlocks) formatted = formatted.replace(new RegExp(`^([\\t ]*)${block.marker}$`, "m"), (_match, prefix: string) => block.body.map((line) => prefix + line).join("\n")); + for (const block of rawBlocks) + formatted = formatted.replace( + new RegExp(`^([\\t ]*)${block.marker}$`, "m"), + (_match, prefix: string) => + block.body.map((line) => prefix + line).join("\n"), + ); return formatted; } diff --git a/core/format.ts b/core/format.ts index 6fe6a02..eb85730 100644 --- a/core/format.ts +++ b/core/format.ts @@ -1,22 +1,42 @@ import { readdir, readFile, writeFile, stat } from "node:fs/promises"; import { join, extname } from "node:path"; -import { formatSource, type FormatLanguage, type FormatOptions } from "./format-source.ts"; +import { + formatSource, + type FormatLanguage, + type FormatOptions, +} from "./format-source.ts"; -export async function format(root: string, options: FormatOptions & { check?: boolean } = {}): Promise { +export async function format( + root: string, + options: FormatOptions & { check?: boolean } = {}, +): Promise { const changed: string[] = []; if ((await stat(root)).isFile()) { const language = extname(root).slice(1) as FormatLanguage; if (!["spug", "stpug", "stug", "stcss"].includes(language)) return changed; const source = await readFile(root, "utf8"); const next = await formatSource(source, language, options); - if (next !== source) { changed.push(root); if (!options.check) await writeFile(root, next, "utf8"); } + if (next !== source) { + changed.push(root); + if (!options.check) await writeFile(root, next, "utf8"); + } return changed; } for (const entry of await readdir(root, { withFileTypes: true })) { if (entry.isSymbolicLink()) continue; - if (entry.name.startsWith(".") || ["node_modules", "dist", "dist.previous", "shadcn-svelte", "_muddle"].includes(entry.name)) continue; + if ( + entry.name.startsWith(".") || + [ + "node_modules", + "dist", + "dist.previous", + "shadcn-svelte", + "_muddle", + ].includes(entry.name) + ) + continue; const path = join(root, entry.name); - changed.push(...await format(path, options)); + changed.push(...(await format(path, options))); } return changed; } diff --git a/core/http.ts b/core/http.ts index 3d9c1de..2ea995d 100644 --- a/core/http.ts +++ b/core/http.ts @@ -202,7 +202,10 @@ export async function serveStaticFile( path: string, ): Promise { const relative = decodeURIComponent(path.split("?")[0] ?? ""); - const target = resolve(root, `.${relative.startsWith("/") ? relative : `/${relative}`}`); + const target = resolve( + root, + `.${relative.startsWith("/") ? relative : `/${relative}`}`, + ); if (!target.startsWith(resolve(root))) return undefined; if (!existsSync(target)) return undefined; if (!statSync(target).isFile()) return undefined; diff --git a/core/pipeline.ts b/core/pipeline.ts index 0f2fc60..72eeb3f 100644 --- a/core/pipeline.ts +++ b/core/pipeline.ts @@ -2,13 +2,25 @@ import type { HttpResponse } from "../contracts/typegen/http/response.ts"; import type { RequestContext } from "../contracts/typegen/http/request.ts"; import type { RouteDefinition } from "./router.ts"; -export type RouteGuard = (context: RequestContext, route: RouteDefinition) => Promise | void | HttpResponse; +export type RouteGuard = ( + context: RequestContext, + route: RouteDefinition, +) => Promise | void | HttpResponse; export class RoutePipeline { private readonly guards: RouteGuard[] = []; - use(guard: RouteGuard): this { this.guards.push(guard); return this; } - async run(context: RequestContext, route: RouteDefinition): Promise { - for (const guard of this.guards) { const result = await guard(context, route); if (result) return result; } + use(guard: RouteGuard): this { + this.guards.push(guard); + return this; + } + async run( + context: RequestContext, + route: RouteDefinition, + ): Promise { + for (const guard of this.guards) { + const result = await guard(context, route); + if (result) return result; + } return undefined; } } diff --git a/core/router.ts b/core/router.ts index 8bf1254..9386a4c 100644 --- a/core/router.ts +++ b/core/router.ts @@ -1,4 +1,7 @@ -import { getControllerOptions, getControllerPrefix } from "../decorators/Controller.ts"; +import { + getControllerOptions, + getControllerPrefix, +} from "../decorators/Controller.ts"; import { getRoot, resolveViewRoot } from "../decorators/Root.ts"; import { getParamMetadata, type ParamMetadata } from "../decorators/Params.ts"; import { getRoutes, type RouteOptions } from "../decorators/Route.ts"; @@ -139,7 +142,10 @@ export interface ResolvedView { /** * Resolves the template of a view. */ -export function resolveView(view: ViewMetadata, viewRoot: string): ResolvedView { +export function resolveView( + view: ViewMetadata, + viewRoot: string, +): ResolvedView { const absolute = isAbsolute(view.path) ? view.path : resolve(viewRoot, view.path); @@ -151,7 +157,7 @@ export function resolveView(view: ViewMetadata, viewRoot: string): ResolvedView let relative = path.startsWith(`${viewRoot}/`) ? path.slice(viewRoot.length + 1) - : path.split("/").pop() ?? path; + : (path.split("/").pop() ?? path); relative = relative.replace(/\.[^./]+$/, ""); const segments = relative.split("/").filter((segment) => segment.length > 0); @@ -181,11 +187,7 @@ export class Router { /** * Adds a route that is not bound to a controller. */ - raw( - method: string, - path: string, - handler: RawRoute["handler"], - ): void { + raw(method: string, path: string, handler: RawRoute["handler"]): void { this.rawRoutes.push({ method: method.toUpperCase(), path, handler }); } @@ -259,14 +261,33 @@ export class Router { } /** Public route metadata for client navigation and route-aware components. */ - clientRoutes(): { name: string; description?: string; path: string; method: HttpMethod; params: Record }[] { + clientRoutes(): { + name: string; + description?: string; + path: string; + method: HttpMethod; + params: Record; + }[] { return this.routes.map((route) => { const view = route.view; - const params = Object.fromEntries(pathSegments(route.path).filter((part) => part.startsWith(":")).map((part) => { - const key = part.slice(1).replace(/[?*]$/, ""); - return [key, route.options.params?.[key] ?? view?.params?.[key] ?? ""]; - })); - return { name: route.options.name ?? view?.name ?? route.methodName, description: route.options.description ?? view?.description, path: route.path, method: route.method, params }; + const params = Object.fromEntries( + pathSegments(route.path) + .filter((part) => part.startsWith(":")) + .map((part) => { + const key = part.slice(1).replace(/[?*]$/, ""); + return [ + key, + route.options.params?.[key] ?? view?.params?.[key] ?? "", + ]; + }), + ); + return { + name: route.options.name ?? view?.name ?? route.methodName, + description: route.options.description ?? view?.description, + path: route.path, + method: route.method, + params, + }; }); } @@ -292,11 +313,13 @@ export class Router { async handle(context: RequestContext): Promise { for (const route of this.rawRoutes) { if (route.method !== context.method) continue; - if (route.path.endsWith("*") ? !context.path.startsWith(route.path.slice(0, -1)) : route.path !== context.path) continue; - const response = await route.handler( - context.raw as Request, - context, - ); + if ( + route.path.endsWith("*") + ? !context.path.startsWith(route.path.slice(0, -1)) + : route.path !== context.path + ) + continue; + const response = await route.handler(context.raw as Request, context); return { status: response.status as StatusCode, headers: Object.fromEntries(response.headers.entries()), @@ -332,8 +355,17 @@ export class Router { const auth = getAuth(match.route.controller, match.route.methodName); if (auth) { const identity = context.locals.auth as { roles?: string[] } | undefined; - if (!identity || (auth.roles?.some((role) => !identity.roles?.includes(role)) ?? false)) { - return { status: auth.redirect ? 302 : identity ? 403 : 401, headers: auth.redirect ? { location: auth.redirect } : { "content-type": "text/plain; charset=utf-8" }, body: auth.redirect ? undefined : "Unauthorized" }; + if ( + !identity || + (auth.roles?.some((role) => !identity.roles?.includes(role)) ?? false) + ) { + return { + status: auth.redirect ? 302 : identity ? 403 : 401, + headers: auth.redirect + ? { location: auth.redirect } + : { "content-type": "text/plain; charset=utf-8" }, + body: auth.redirect ? undefined : "Unauthorized", + }; } } const result = await match.route.invoke(context); diff --git a/core/sessions.ts b/core/sessions.ts index 128cab3..4229480 100644 --- a/core/sessions.ts +++ b/core/sessions.ts @@ -15,14 +15,23 @@ export class MemorySessionStore implements SessionStore { private readonly records = new Map(); async get(key: string): Promise { const record = this.records.get(key); - if (!record || record.expires <= Date.now()) { this.records.delete(key); return undefined; } + if (!record || record.expires <= Date.now()) { + this.records.delete(key); + return undefined; + } return structuredClone(record); } - async set(key: string, record: SessionRecord): Promise { this.records.set(key, structuredClone(record)); } - async delete(key: string): Promise { this.records.delete(key); } + async set(key: string, record: SessionRecord): Promise { + this.records.set(key, structuredClone(record)); + } + async delete(key: string): Promise { + this.records.delete(key); + } async consume(key: string): Promise { const record = this.records.get(key); this.records.delete(key); - return record && record.expires > Date.now() ? structuredClone(record) : undefined; + return record && record.expires > Date.now() + ? structuredClone(record) + : undefined; } } diff --git a/core/templates.ts b/core/templates.ts index 32d5f56..b6a8cb4 100644 --- a/core/templates.ts +++ b/core/templates.ts @@ -11,16 +11,32 @@ export interface TemplateResult { } const sharedFiles: Record = { - "package.json": `${JSON.stringify({ - name: "my-muddle-site", - private: true, - type: "module", - scripts: { dev: "muddle dev .", build: "muddle build ." }, - dependencies: { muddle: "latest" }, - }, null, 2)}\n`, - "tsconfig.json": `${JSON.stringify({ - compilerOptions: { target: "ESNext", module: "Preserve", moduleResolution: "bundler", strict: true, noEmit: true, experimentalDecorators: true, allowImportingTsExtensions: true }, - }, null, 2)}\n`, + "package.json": `${JSON.stringify( + { + name: "my-muddle-site", + private: true, + type: "module", + scripts: { dev: "muddle dev .", build: "muddle build ." }, + dependencies: { muddle: "latest" }, + }, + null, + 2, + )}\n`, + "tsconfig.json": `${JSON.stringify( + { + compilerOptions: { + target: "ESNext", + module: "Preserve", + moduleResolution: "bundler", + strict: true, + noEmit: true, + experimentalDecorators: true, + allowImportingTsExtensions: true, + }, + }, + null, + 2, + )}\n`, "config.ts": `import { resolveConfig } from "muddle";\nimport config from "./ct.config.ts";\n\nexport const resolved = resolveConfig(config);\n`, "modules/app/app.module.ts": `import { Module } from "muddle";\nimport { AppController } from "./app.controller.ts";\n\n@Module({ controllers: [AppController] })\nexport class AppModule {}\n`, }; @@ -45,12 +61,18 @@ const docsFiles: Record = { export const muddleTemplates: readonly MuddleTemplate[] = ["docs", "minimal"]; -export async function createTemplate(name: MuddleTemplate, destination: string): Promise { +export async function createTemplate( + name: MuddleTemplate, + destination: string, +): Promise { const output = resolve(destination); if (existsSync(output) && (await readdir(output)).length > 0) { throw new Error(`Refusing to overwrite non-empty directory ${output}`); } - const files = { ...sharedFiles, ...(name === "docs" ? docsFiles : minimalFiles) }; + const files = { + ...sharedFiles, + ...(name === "docs" ? docsFiles : minimalFiles), + }; const written: string[] = []; for (const [relative, source] of Object.entries(files)) { const file = join(output, relative); diff --git a/ct.config.ts b/ct.config.ts index ce33fb3..f050248 100644 --- a/ct.config.ts +++ b/ct.config.ts @@ -9,13 +9,10 @@ export default defineConfig({ components: "samples/hello-world/modules/app/views/components", viewRoots: ["samples/hello-world/modules/app/views"], - integrations: [ - stcssIntegration({ dir: "samples/hello-world/styles" }), - ], + integrations: [stcssIntegration({ dir: "samples/hello-world/styles" })], prerender: { routes: ["/", "/posts/test1", "/posts/test2", "/posts/test3"], outDir: "samples/hello-world/dist", }, - }); diff --git a/decorators/Astro.ts b/decorators/Astro.ts index 5d58f25..4bba0cd 100644 --- a/decorators/Astro.ts +++ b/decorators/Astro.ts @@ -1,8 +1,17 @@ import { Route, type RouteOptions } from "./Route.ts"; -export interface AstroOptions extends RouteOptions { partial?: boolean; action?: boolean } +export interface AstroOptions extends RouteOptions { + partial?: boolean; + action?: boolean; +} /** Declares an Astro-compatible endpoint. */ -export function Astro(path: string, options: AstroOptions = {}): MethodDecorator { - return Route(path, { ...options, method: options.action ? "POST" : options.method ?? "GET" }); +export function Astro( + path: string, + options: AstroOptions = {}, +): MethodDecorator { + return Route(path, { + ...options, + method: options.action ? "POST" : (options.method ?? "GET"), + }); } diff --git a/decorators/Auth.ts b/decorators/Auth.ts index 5ac178f..021ee42 100644 --- a/decorators/Auth.ts +++ b/decorators/Auth.ts @@ -1,12 +1,21 @@ import { defineMemberMetadata, getMemberMetadata } from "../core/metadata.ts"; const KEY = "muddle:auth"; -export interface AuthOptions { roles?: string[]; redirect?: string } +export interface AuthOptions { + roles?: string[]; + redirect?: string; +} export function Auth(options: AuthOptions = {}): MethodDecorator { - return (target, propertyKey) => defineMemberMetadata(KEY, options, target as object, propertyKey as string); + return (target, propertyKey) => + defineMemberMetadata(KEY, options, target as object, propertyKey as string); } -export function getAuth(target: object, propertyKey: string): AuthOptions | undefined { - return getMemberMetadata(KEY, target, propertyKey)[0] as AuthOptions | undefined; +export function getAuth( + target: object, + propertyKey: string, +): AuthOptions | undefined { + return getMemberMetadata(KEY, target, propertyKey)[0] as + | AuthOptions + | undefined; } diff --git a/decorators/RenderAhead.ts b/decorators/RenderAhead.ts index 1fea63f..3483d99 100644 --- a/decorators/RenderAhead.ts +++ b/decorators/RenderAhead.ts @@ -10,6 +10,9 @@ export function RenderAhead(routes: RenderAheadSource): MethodDecorator { }; } -export function getRenderAhead(target: object, propertyKey: string): RenderAheadSource | undefined { +export function getRenderAhead( + target: object, + propertyKey: string, +): RenderAheadSource | undefined { return getMetadata(KEY, target, propertyKey) as RenderAheadSource | undefined; } diff --git a/decorators/RenderRoute.ts b/decorators/RenderRoute.ts index 2c47574..c6789f8 100644 --- a/decorators/RenderRoute.ts +++ b/decorators/RenderRoute.ts @@ -10,17 +10,31 @@ export interface CollectionRenderRoute { export type RenderRouteSource = string | string[] | CollectionRenderRoute; export function routeFolder(...segments: string[]): string { - return `/${segments.filter(Boolean).map((segment) => segment.replace(/^\/+|\/+$/g, "")).join("/")}`; + return `/${segments + .filter(Boolean) + .map((segment) => segment.replace(/^\/+|\/+$/g, "")) + .join("/")}`; } -export function collectionRoute(collection: string, path: string): CollectionRenderRoute { +export function collectionRoute( + collection: string, + path: string, +): CollectionRenderRoute { return { collection, path }; } export function RenderRoute(routes: RenderRouteSource[]): MethodDecorator { - return (target, propertyKey) => defineMetadata(KEY, routes, target, propertyKey); + return (target, propertyKey) => + defineMetadata(KEY, routes, target, propertyKey); } -export function getRenderRoutes(target: object, propertyKey: string): RenderRouteSource[] { - return (getMetadata(KEY, target, propertyKey) as RenderRouteSource[] | undefined) ?? []; +export function getRenderRoutes( + target: object, + propertyKey: string, +): RenderRouteSource[] { + return ( + (getMetadata(KEY, target, propertyKey) as + | RenderRouteSource[] + | undefined) ?? [] + ); } diff --git a/decorators/View.ts b/decorators/View.ts index a413e3b..9931542 100644 --- a/decorators/View.ts +++ b/decorators/View.ts @@ -1,5 +1,8 @@ import "../core/metadata.ts"; -import { defineMemberMetadata, getAllMemberMetadata } from "../core/metadata.ts"; +import { + defineMemberMetadata, + getAllMemberMetadata, +} from "../core/metadata.ts"; import { callerFile } from "../core/paths.ts"; import { dirname } from "node:path"; import type { HttpMethod } from "../contracts/typegen/http/method.ts"; @@ -35,7 +38,10 @@ export interface ViewMetadata { } /** Declares the template rendered by a controller method. */ -export function View(path?: string, options: ViewOptions = {}): MethodDecorator { +export function View( + path?: string, + options: ViewOptions = {}, +): MethodDecorator { return (target, propertyKey, descriptor) => { const caller = callerFile(); const method = String(propertyKey); @@ -71,10 +77,7 @@ export function View(path?: string, options: ViewOptions = {}): MethodDecorator * * When a property key is given, only the views of that method are returned. */ -export function getViews( - target: object, - propertyKey?: string, -): ViewMetadata[] { +export function getViews(target: object, propertyKey?: string): ViewMetadata[] { return getAllMemberMetadata(VIEW_METADATA_KEY, target) .filter( (entry) => propertyKey === undefined || entry.propertyKey === propertyKey, diff --git a/editors/vscode/language-configuration.json b/editors/vscode/language-configuration.json index e3e27e6..ac07420 100644 --- a/editors/vscode/language-configuration.json +++ b/editors/vscode/language-configuration.json @@ -10,7 +10,11 @@ { "open": "\"", "close": "\"", "notIn": ["string", "comment"] } ], "surroundingPairs": [ - ["{", "}"], ["(", ")"], ["[", "]"], ["'", "'"], ["\"", "\""] + ["{", "}"], + ["(", ")"], + ["[", "]"], + ["'", "'"], + ["\"", "\""] ], "indentationRules": { "increaseIndentPattern": "^\\s*(?:@\\w+[^;{]*)?[^{};]*\\{\\s*$|^(?:mixin|if|each|for|component)\\s+.*$", diff --git a/editors/vscode/package.json b/editors/vscode/package.json index f852cd3..dd59d1e 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -1,69 +1,75 @@ { - "name": "muddle-language-tools", - "displayName": "Muddle Language Tools", - "repository": { - "type": "git", - "url": "https://git.crit.rip/mdl/muddle.git" - }, - "version": "0.1.0", - "description": "Language support for Muddle SPUG templates and STCSS stylesheets", - "main": "./src/extension.js", - "activationEvents": [ - "onLanguage:spug", - "onLanguage:stcss" - ], - "capabilities": { - "untrustedWorkspaces": { - "supported": true - } - }, - "engines": { - "vscode": "^1.85.0" - }, - "categories": [ - "Programming Languages" - ], - "contributes": { - "languages": [ - { - "id": "spug", - "extensions": [".spug", ".stpug", ".stug"], - "configuration": "./language-configuration.json" - }, - { - "id": "stcss", - "extensions": [".stcss"], - "configuration": "./language-configuration.json" - } - ], - "grammars": [ - { - "language": "spug", - "scopeName": "source.spug", - "path": "./syntaxes/spug.tmLanguage.json" - }, - { - "language": "stcss", - "scopeName": "source.stcss", - "path": "./syntaxes/stcss.tmLanguage.json" - } - ], - "snippets": [ - { - "language": "spug", - "path": "./snippets/spug.json" - }, - { - "language": "stcss", - "path": "./snippets/stcss.json" - } - ] - }, - "dependencies": { - "@vscode/vsce": "^4.0.0" - }, - "scripts": { - "package": "vsce package", - "publish": "vsce publish" - } + "name": "muddle-language-tools", + "displayName": "Muddle Language Tools", + "repository": { + "type": "git", + "url": "https://git.crit.rip/mdl/muddle.git" + }, + "version": "0.1.0", + "description": "Language support for Muddle SPUG templates and STCSS stylesheets", + "main": "./src/extension.js", + "activationEvents": [ + "onLanguage:spug", + "onLanguage:stcss" + ], + "capabilities": { + "untrustedWorkspaces": { + "supported": true + } + }, + "engines": { + "vscode": "^1.85.0" + }, + "categories": [ + "Programming Languages" + ], + "contributes": { + "languages": [ + { + "id": "spug", + "extensions": [ + ".spug", + ".stpug", + ".stug" + ], + "configuration": "./language-configuration.json" + }, + { + "id": "stcss", + "extensions": [ + ".stcss" + ], + "configuration": "./language-configuration.json" + } + ], + "grammars": [ + { + "language": "spug", + "scopeName": "source.spug", + "path": "./syntaxes/spug.tmLanguage.json" + }, + { + "language": "stcss", + "scopeName": "source.stcss", + "path": "./syntaxes/stcss.tmLanguage.json" + } + ], + "snippets": [ + { + "language": "spug", + "path": "./snippets/spug.json" + }, + { + "language": "stcss", + "path": "./snippets/stcss.json" + } + ] + }, + "dependencies": { + "@vscode/vsce": "^4.0.0" + }, + "scripts": { + "package": "vsce package", + "publish": "vsce publish" + } } diff --git a/editors/vscode/snippets/spug.json b/editors/vscode/snippets/spug.json index de72a79..e92df3a 100644 --- a/editors/vscode/snippets/spug.json +++ b/editors/vscode/snippets/spug.json @@ -1,18 +1,88 @@ { - "Typed props": { "prefix": "props", "body": ["props", " ${1:name}: ${2:string}"], "description": "Declare typed SPUG props" }, - "Optional prop": { "prefix": "props?", "body": ["props", " ${1:name}: ${2:string}?", ""], "description": "Declare an optional SPUG prop" }, - "Relation anchor": { "prefix": "anchor", "body": ["${1:a}.class@${2:name}(${3:id})(${0})"], "description": "Spawn a relation anchor consumed by $refer / :with / :when in STCSS" }, - "Dynamic anchor": { "prefix": "anchor+", "body": ["li@row(item.id)(${0})"], "description": "Dynamic relation anchor from an each-loop item" }, - "TypeScript block": { "prefix": "javascript", "body": ["javascript!", " ${1:const value = true;}", "end"], "description": "Embed a TypeScript block" }, - "HTML block": { "prefix": "html-block", "body": ["html!\n ${1:

raw

}\nend"], "description": "Embed a raw HTML block" }, - "Component render": { "prefix": "component", "body": ["component ${1:name}", " ${2:prop}: ${3:value}", " render ${4:slot}", " ${0}", " end? (see docs)"], "description": "Render a component with props and a slot" }, - "Extend layout": { "prefix": "extends", "body": ["extends ./layout.spug", "", "block content", " ${0}"], "description": "Extend a layout and fill the content block" }, - "Render slot": { "prefix": "render", "body": ["render ${1:content}", " ${0}"], "description": "Pass a slot into a component" }, - "Frontmatter": { "prefix": "fm", "body": ["---", "${0}", "---"], "description": "Front matter block with TypeScript imports" }, - "Escape expression": { "prefix": "expr", "body": ["#{${1:expression}}"], "description": "Inline interpolated expression" }, - "Raw expression": { "prefix": "raw", "body": ["!{${1:expression}}"], "description": "Inline raw unescaped expression" }, - "Iterate": { "prefix": "each", "body": ["each ${1:item} in ${2:items}", " ${0}"], "description": "Iterate over a list" }, - "Condition": { "prefix": "if", "body": ["if ${1:condition}", " ${0}"], "description": "Conditional block" }, - "Iso if-choice": { "prefix": "unless", "body": ["unless ${1:condition}", " ${0}"], "description": "Inverse conditional block" }, - "Configuration block": { "prefix": "config", "body": ["$$$", " ${1:block}", "notes", " ${0}"], "description": "Template test configuration and notes" } + "Typed props": { + "prefix": "props", + "body": ["props", " ${1:name}: ${2:string}"], + "description": "Declare typed SPUG props" + }, + "Optional prop": { + "prefix": "props?", + "body": ["props", " ${1:name}: ${2:string}?", ""], + "description": "Declare an optional SPUG prop" + }, + "Relation anchor": { + "prefix": "anchor", + "body": ["${1:a}.class@${2:name}(${3:id})(${0})"], + "description": "Spawn a relation anchor consumed by $refer / :with / :when in STCSS" + }, + "Dynamic anchor": { + "prefix": "anchor+", + "body": ["li@row(item.id)(${0})"], + "description": "Dynamic relation anchor from an each-loop item" + }, + "TypeScript block": { + "prefix": "javascript", + "body": ["javascript!", " ${1:const value = true;}", "end"], + "description": "Embed a TypeScript block" + }, + "HTML block": { + "prefix": "html-block", + "body": ["html!\n ${1:

raw

}\nend"], + "description": "Embed a raw HTML block" + }, + "Component render": { + "prefix": "component", + "body": [ + "component ${1:name}", + " ${2:prop}: ${3:value}", + " render ${4:slot}", + " ${0}", + " end? (see docs)" + ], + "description": "Render a component with props and a slot" + }, + "Extend layout": { + "prefix": "extends", + "body": ["extends ./layout.spug", "", "block content", " ${0}"], + "description": "Extend a layout and fill the content block" + }, + "Render slot": { + "prefix": "render", + "body": ["render ${1:content}", " ${0}"], + "description": "Pass a slot into a component" + }, + "Frontmatter": { + "prefix": "fm", + "body": ["---", "${0}", "---"], + "description": "Front matter block with TypeScript imports" + }, + "Escape expression": { + "prefix": "expr", + "body": ["#{${1:expression}}"], + "description": "Inline interpolated expression" + }, + "Raw expression": { + "prefix": "raw", + "body": ["!{${1:expression}}"], + "description": "Inline raw unescaped expression" + }, + "Iterate": { + "prefix": "each", + "body": ["each ${1:item} in ${2:items}", " ${0}"], + "description": "Iterate over a list" + }, + "Condition": { + "prefix": "if", + "body": ["if ${1:condition}", " ${0}"], + "description": "Conditional block" + }, + "Iso if-choice": { + "prefix": "unless", + "body": ["unless ${1:condition}", " ${0}"], + "description": "Inverse conditional block" + }, + "Configuration block": { + "prefix": "config", + "body": ["$$$", " ${1:block}", "notes", " ${0}"], + "description": "Template test configuration and notes" + } } diff --git a/editors/vscode/snippets/stcss.json b/editors/vscode/snippets/stcss.json index 98abbab..a6d0eb8 100644 --- a/editors/vscode/snippets/stcss.json +++ b/editors/vscode/snippets/stcss.json @@ -1,25 +1,121 @@ { - "Predicted geometry": { "prefix": "predict", "body": ["@predict max-height max-width;"], "description": "Transition prerendered geometry" }, - "Predicted transitions": { "prefix": "predict-full", "body": ["@predict left top width height opacity transform;"], "description": "Full prediction transition list (pill/ribbon style)" }, - "Measure prediction target": { "prefix": "measure", "body": ["@measure;"], "description": "Mark this selector for prerender geometry prefill (inline prediction source)" }, - "Measure anchors": { "prefix": "measure-anchor", "body": ["@measure @${1:anchor}, ${0:.selector};"], "description": "Measure elements by relation anchor or class" }, - "Responsive rule": { "prefix": "responsive", "body": ["@responsive ${1:md} {", " ${2}", "}"], "description": "Add a responsive STCSS rule" }, - "Media rule": { "prefix": "media", "body": ["@media ${1:(max-width: 48rem)} {", " ${2}", "}"], "description": "Add a media query" }, - "Each loop": { "prefix": "each", "body": ["@each $${1:item} in ${2:items} {", " ${0}", "}"], "description": "Repeat a block over a value list" }, - "For loop": { "prefix": "for", "body": ["@for $${1:i} from ${2:1} through ${3:5} {", " ${0}", "}"], "description": "Count a range" }, - "Mixin declaration": { "prefix": "mixin", "body": ["@mixin ${1:name}($${2:arg}) {", " ${0}", "}"], "description": "Declare an STCSS mixin" }, - "Include mixin": { "prefix": "include", "body": ["@include ${1:name}(${2:arg});"], "description": "Use an STCSS mixin" }, - "Interpolation": { "prefix": "interp", "body": ["#{$${1:scale} * 2}rem"], "description": "Compile-time value interpolation" }, - "d-string": { "prefix": "d", "body": ["d\"${1:calc(#{} )}\""], "description": "String with brace expressions evaluated at compile time" }, - "$refer geometry": { "prefix": "refer", "body": ["$refer[${1:left}](@${2:anchor})"], "description": "Read pre-computed anchor geometry (left/top/width/height)" }, - "$refer interpolated location": { "prefix": "refer-between", "body": ["calc($refer[${1:left}](@${2:anchor}) - $refer[${3:left}](@${4:relative}))"], "description": "Compute location between two anchors" }, - "$relate metrics": { "prefix": "relate", "body": ["$relate(${1:@anchor})"], "description": "Legacy relation value of an anchor" }, - "Computed mirror": { "prefix": "computed", "body": ["@${1:anchor}.computed.${2:backgroundColor}"], "description": "Mirror a live computed style of an anchor" }, - "Relation state selector": { "prefix": "with-state", "body": [":with(@${1:anchor}:${2|active,clicked,focus|})"], "description": "Anchor state selection (:has under the hood)" }, - "When selector": { "prefix": "when-state", "body": [":when(${1:root} @${2:anchor}:${3|active,clicked,focus|})"], "description": "Scoped anchor state selection" }, - "Concentric radius": { "prefix": "concentric", "body": ["border-radius: concentric ${1:0.75rem};"], "description": "Concentric nested corner radii" }, - "Freeform gradient": { "prefix": "freeform-gradient", "body": ["background-image: freeform-gradient(${1:#7c3aed} ${2:10%} ${3:50%}, ${4:#f00} ${5:70%} ${6:40%});"], "description": "Spawn layered freeform radial gradients" }, - "Concentric block": { "prefix": "concentric-block", "body": ["@concentric ${1:1rem} {", " ${0}", "}"], "description": "Concentric radius at-rule" }, - "View transition": { "prefix": "view-transition", "body": ["@view-transition ${1:name};"], "description": "Emit a css view-transition-name" }, - "Compile-time values": { "prefix": "javascript", "body": ["javascript!", " const ${1:space} = ${2:1};", "end"], "description": "Add compile-time values" } + "Predicted geometry": { + "prefix": "predict", + "body": ["@predict max-height max-width;"], + "description": "Transition prerendered geometry" + }, + "Predicted transitions": { + "prefix": "predict-full", + "body": ["@predict left top width height opacity transform;"], + "description": "Full prediction transition list (pill/ribbon style)" + }, + "Measure prediction target": { + "prefix": "measure", + "body": ["@measure;"], + "description": "Mark this selector for prerender geometry prefill (inline prediction source)" + }, + "Measure anchors": { + "prefix": "measure-anchor", + "body": ["@measure @${1:anchor}, ${0:.selector};"], + "description": "Measure elements by relation anchor or class" + }, + "Responsive rule": { + "prefix": "responsive", + "body": ["@responsive ${1:md} {", " ${2}", "}"], + "description": "Add a responsive STCSS rule" + }, + "Media rule": { + "prefix": "media", + "body": ["@media ${1:(max-width: 48rem)} {", " ${2}", "}"], + "description": "Add a media query" + }, + "Each loop": { + "prefix": "each", + "body": ["@each $${1:item} in ${2:items} {", " ${0}", "}"], + "description": "Repeat a block over a value list" + }, + "For loop": { + "prefix": "for", + "body": ["@for $${1:i} from ${2:1} through ${3:5} {", " ${0}", "}"], + "description": "Count a range" + }, + "Mixin declaration": { + "prefix": "mixin", + "body": ["@mixin ${1:name}($${2:arg}) {", " ${0}", "}"], + "description": "Declare an STCSS mixin" + }, + "Include mixin": { + "prefix": "include", + "body": ["@include ${1:name}(${2:arg});"], + "description": "Use an STCSS mixin" + }, + "Interpolation": { + "prefix": "interp", + "body": ["#{$${1:scale} * 2}rem"], + "description": "Compile-time value interpolation" + }, + "d-string": { + "prefix": "d", + "body": ["d\"${1:calc(#{} )}\""], + "description": "String with brace expressions evaluated at compile time" + }, + "$refer geometry": { + "prefix": "refer", + "body": ["$refer[${1:left}](@${2:anchor})"], + "description": "Read pre-computed anchor geometry (left/top/width/height)" + }, + "$refer interpolated location": { + "prefix": "refer-between", + "body": [ + "calc($refer[${1:left}](@${2:anchor}) - $refer[${3:left}](@${4:relative}))" + ], + "description": "Compute location between two anchors" + }, + "$relate metrics": { + "prefix": "relate", + "body": ["$relate(${1:@anchor})"], + "description": "Legacy relation value of an anchor" + }, + "Computed mirror": { + "prefix": "computed", + "body": ["@${1:anchor}.computed.${2:backgroundColor}"], + "description": "Mirror a live computed style of an anchor" + }, + "Relation state selector": { + "prefix": "with-state", + "body": [":with(@${1:anchor}:${2|active,clicked,focus|})"], + "description": "Anchor state selection (:has under the hood)" + }, + "When selector": { + "prefix": "when-state", + "body": [":when(${1:root} @${2:anchor}:${3|active,clicked,focus|})"], + "description": "Scoped anchor state selection" + }, + "Concentric radius": { + "prefix": "concentric", + "body": ["border-radius: concentric ${1:0.75rem};"], + "description": "Concentric nested corner radii" + }, + "Freeform gradient": { + "prefix": "freeform-gradient", + "body": [ + "background-image: freeform-gradient(${1:#7c3aed} ${2:10%} ${3:50%}, ${4:#f00} ${5:70%} ${6:40%});" + ], + "description": "Spawn layered freeform radial gradients" + }, + "Concentric block": { + "prefix": "concentric-block", + "body": ["@concentric ${1:1rem} {", " ${0}", "}"], + "description": "Concentric radius at-rule" + }, + "View transition": { + "prefix": "view-transition", + "body": ["@view-transition ${1:name};"], + "description": "Emit a css view-transition-name" + }, + "Compile-time values": { + "prefix": "javascript", + "body": ["javascript!", " const ${1:space} = ${2:1};", "end"], + "description": "Add compile-time values" + } } diff --git a/editors/vscode/src/extension.js b/editors/vscode/src/extension.js index 7bc1369..89cab42 100644 --- a/editors/vscode/src/extension.js +++ b/editors/vscode/src/extension.js @@ -1,13 +1,46 @@ const vscode = require("vscode"); const AT_RULES = new Set([ - "each", "for", "from", "through", "include", "responsive", "media", "supports", - "concentric", "view-transition", "predict", "measure", "property", "mixin", - "stcss", "import", "charset", "document", "font-face", "keyframes", "layer", - "namespace", "page", "scope", "starting-style", "container", + "each", + "for", + "from", + "through", + "include", + "responsive", + "media", + "supports", + "concentric", + "view-transition", + "predict", + "measure", + "property", + "mixin", + "stcss", + "import", + "charset", + "document", + "font-face", + "keyframes", + "layer", + "namespace", + "page", + "scope", + "starting-style", + "container", ]); -const STATE_WORDS = new Set(["active", "clicked", "focus", "hover", "open", "closed", "selected", "loading", "visible", "hidden"]); +const STATE_WORDS = new Set([ + "active", + "clicked", + "focus", + "hover", + "open", + "closed", + "selected", + "loading", + "visible", + "hidden", +]); const AT_RULE_DOCS = new Map([ ["mixin", "Declare a reusable STCSS mixin: `@mixin name($param) { … }`"], @@ -19,28 +52,98 @@ const AT_RULE_DOCS = new Map([ ["supports", "Standard `@supports` feature wrapper."], ["concentric", "Emit concentric nested radii: `@concentric 0.75rem { … }`"], ["view-transition", "Emit `view-transition-name` on the attached rule."], - ["predict", "Declare which properties transition when prediction prefill changes (emits a smooth transition rule)."], - ["measure", "Mark the attached rule (or listed selectors / `@anchor`s) as a prediction target. Prerender measures the elements and writes geometry. Bare `@anchor` arguments compile to `[data-muddle-anchor=\"anchor\"]`."], + [ + "predict", + "Declare which properties transition when prediction prefill changes (emits a smooth transition rule).", + ], + [ + "measure", + 'Mark the attached rule (or listed selectors / `@anchor`s) as a prediction target. Prerender measures the elements and writes geometry. Bare `@anchor` arguments compile to `[data-muddle-anchor="anchor"]`.', + ], ]); -const REFERENCE_DOC = "Reads pre-computed geometry recorded from a `@sample` anchor: compiles to `var(--muddle-relate-…)`.\n\nMetrics: `left`, `top`, `width`, `height`. `@anchor` compiles to `[data-muddle-anchor=\"anchor\"]`."; +const REFERENCE_DOC = + 'Reads pre-computed geometry recorded from a `@sample` anchor: compiles to `var(--muddle-relate-…)`.\n\nMetrics: `left`, `top`, `width`, `height`. `@anchor` compiles to `[data-muddle-anchor="anchor"]`.'; -const RELATE_DOC = "Legacy relation value of an anchor (deprecated in favour of `$refer[metric](@anchor)`)."; +const RELATE_DOC = + "Legacy relation value of an anchor (deprecated in favour of `$refer[metric](@anchor)`)."; const CSS_PROPERTIES = [ - "background", "background-color", "background-image", "border", "border-color", "border-radius", "border-width", - "bottom", "box-shadow", "box-sizing", "color", "cursor", "display", "flex", "flex-basis", "flex-direction", - "flex-shrink", "flex-wrap", "font-family", "font-size", "font-weight", "gap", "grid-area", "height", "inset", - "justify-content", "left", "letter-spacing", "line-height", "margin", "max-height", "max-width", "min-height", - "min-width", "opacity", "overflow", "padding", "pointer-events", "position", "right", "text-decoration", - "text-transform", "top", "transform", "transition", "vertical-align", "visibility", "white-space", "width", + "background", + "background-color", + "background-image", + "border", + "border-color", + "border-radius", + "border-width", + "bottom", + "box-shadow", + "box-sizing", + "color", + "cursor", + "display", + "flex", + "flex-basis", + "flex-direction", + "flex-shrink", + "flex-wrap", + "font-family", + "font-size", + "font-weight", + "gap", + "grid-area", + "height", + "inset", + "justify-content", + "left", + "letter-spacing", + "line-height", + "margin", + "max-height", + "max-width", + "min-height", + "min-width", + "opacity", + "overflow", + "padding", + "pointer-events", + "position", + "right", + "text-decoration", + "text-transform", + "top", + "transform", + "transition", + "vertical-align", + "visibility", + "white-space", + "width", "z-index", ]; const HTML_ATTRIBUTES = [ - "alt", "aria-controls", "aria-current", "aria-description", "aria-expanded", "aria-hidden", "aria-label", - "aria-live", "autocomplete", "class", "content", "href", "id", "placeholder", "rel", "role", "src", "style", - "tabindex", "target", "title", "type", + "alt", + "aria-controls", + "aria-current", + "aria-description", + "aria-expanded", + "aria-hidden", + "aria-label", + "aria-live", + "autocomplete", + "class", + "content", + "href", + "id", + "placeholder", + "rel", + "role", + "src", + "style", + "tabindex", + "target", + "title", + "type", ]; const INDEX = { @@ -78,11 +181,17 @@ async function readText(uri) { function posFromOffset(text, offset) { const before = text.slice(0, offset); const newline = before.lastIndexOf("\n"); - return new vscode.Position(before.split("\n").length - 1, offset - newline - 1); + return new vscode.Position( + before.split("\n").length - 1, + offset - newline - 1, + ); } function rangeOf(text, offset, length) { - return new vscode.Range(posFromOffset(text, offset), posFromOffset(text, offset + Math.max(length, 1))); + return new vscode.Range( + posFromOffset(text, offset), + posFromOffset(text, offset + Math.max(length, 1)), + ); } function isStcss(document) { @@ -121,7 +230,12 @@ function indexStcssDocument(uri, text) { const anchorRegex = /@([A-Za-z_][\w-]*)/g; while ((match = anchorRegex.exec(text))) { if (AT_RULES.has(match[1])) continue; - rememberAnchor(match[1], uri, posFromOffset(text, match.index).line + 1, "referenced in STCSS"); + rememberAnchor( + match[1], + uri, + posFromOffset(text, match.index).line + 1, + "referenced in STCSS", + ); } } @@ -138,70 +252,152 @@ async function indexWorkspace() { INDEX.spugFiles = spugFiles; INDEX.styleFiles = styleFiles; await Promise.all([ - ...spugFiles.map(async (uri) => indexSpugDocument(uri, await readText(uri))), - ...styleFiles.map(async (uri) => indexStcssDocument(uri, await readText(uri))), + ...spugFiles.map(async (uri) => + indexSpugDocument(uri, await readText(uri)), + ), + ...styleFiles.map(async (uri) => + indexStcssDocument(uri, await readText(uri)), + ), ]); } function anchorCompletions() { return [...INDEX.anchors.keys()].map((name) => { - const item = new vscode.CompletionItem(`@${name}`, vscode.CompletionItemKind.Keyword); + const item = new vscode.CompletionItem( + `@${name}`, + vscode.CompletionItemKind.Keyword, + ); item.detail = "relation anchor"; const first = INDEX.anchors.get(name)[0]; - if (first) item.documentation = new vscode.MarkdownString(`${first.kind}: \`${relativeToWorkspace(first.uri)}:${first.line}\``); + if (first) + item.documentation = new vscode.MarkdownString( + `${first.kind}: \`${relativeToWorkspace(first.uri)}:${first.line}\``, + ); return item; }); } function stcssCompletions() { const atRules = [...AT_RULE_DOCS.keys()].map((name) => { - const item = new vscode.CompletionItem(`@${name}`, vscode.CompletionItemKind.Struct); + const item = new vscode.CompletionItem( + `@${name}`, + vscode.CompletionItemKind.Struct, + ); item.detail = "stcss at-rule"; - item.insertText = new vscode.SnippetString(name === "concentric" || name === "media" ? `${name} $1 {\n\t$0\n}` : `${name};`); + item.insertText = new vscode.SnippetString( + name === "concentric" || name === "media" + ? `${name} $1 {\n\t$0\n}` + : `${name};`, + ); item.documentation = new vscode.MarkdownString(AT_RULE_DOCS.get(name)); return item; }); const metrics = ["left", "top", "width", "height"].map((metric) => { - const item = new vscode.CompletionItem(`$refer[${metric}](@)`, vscode.CompletionItemKind.Function); - item.insertText = new vscode.SnippetString(`$refer[${metric}](@\${1:anchor})`); + const item = new vscode.CompletionItem( + `$refer[${metric}](@)`, + vscode.CompletionItemKind.Function, + ); + item.insertText = new vscode.SnippetString( + `$refer[${metric}](@\${1:anchor})`, + ); item.documentation = new vscode.MarkdownString(REFERENCE_DOC); return item; }); - const relate = new vscode.CompletionItem("$relate(@)", vscode.CompletionItemKind.Function); + const relate = new vscode.CompletionItem( + "$relate(@)", + vscode.CompletionItemKind.Function, + ); relate.insertText = new vscode.SnippetString("$relate(@${1:anchor})"); relate.documentation = new vscode.MarkdownString(RELATE_DOC); const states = [...STATE_WORDS].map((state) => { - const item = new vscode.CompletionItem(state, vscode.CompletionItemKind.EnumMember); + const item = new vscode.CompletionItem( + state, + vscode.CompletionItemKind.EnumMember, + ); item.detail = "anchor state"; return item; }); const properties = CSS_PROPERTIES.map((name) => { - const item = new vscode.CompletionItem(name, vscode.CompletionItemKind.Property); + const item = new vscode.CompletionItem( + name, + vscode.CompletionItemKind.Property, + ); item.insertText = new vscode.SnippetString(`${name}: $0;`); return item; }); const variables = [...INDEX.variables.keys()].map((name) => { - const item = new vscode.CompletionItem(name, vscode.CompletionItemKind.Variable); + const item = new vscode.CompletionItem( + name, + vscode.CompletionItemKind.Variable, + ); item.detail = "stcss variable"; return item; }); - return [...atRules, ...metrics, relate, ...states, ...properties, ...variables]; + return [ + ...atRules, + ...metrics, + relate, + ...states, + ...properties, + ...variables, + ]; } function spugCompletions() { - const keywords = ["extends", "include", "block", "append", "prepend", "mixin", "slot", "yield", "island", "component", "render", "doctype"].map((word) => { - const item = new vscode.CompletionItem(word, vscode.CompletionItemKind.Keyword); + const keywords = [ + "extends", + "include", + "block", + "append", + "prepend", + "mixin", + "slot", + "yield", + "island", + "component", + "render", + "doctype", + ].map((word) => { + const item = new vscode.CompletionItem( + word, + vscode.CompletionItemKind.Keyword, + ); return item; }); - const javascript = new vscode.CompletionItem("javascript!", vscode.CompletionItemKind.Keyword); + const javascript = new vscode.CompletionItem( + "javascript!", + vscode.CompletionItemKind.Keyword, + ); javascript.insertText = new vscode.SnippetString("javascript!\n\t$0\nend"); - const control = ["if", "else", "unless", "each", "while", "case", "when", "default", "props"].map((word) => new vscode.CompletionItem(word, vscode.CompletionItemKind.Keyword)); + const control = [ + "if", + "else", + "unless", + "each", + "while", + "case", + "when", + "default", + "props", + ].map( + (word) => + new vscode.CompletionItem(word, vscode.CompletionItemKind.Keyword), + ); const attributes = HTML_ATTRIBUTES.map((name) => { - const item = new vscode.CompletionItem(name, vscode.CompletionItemKind.Property); + const item = new vscode.CompletionItem( + name, + vscode.CompletionItemKind.Property, + ); item.insertText = new vscode.SnippetString(`${name}=$0`); return item; }); - return [...keywords, javascript, ...control, ...attributes, ...anchorCompletions()]; + return [ + ...keywords, + javascript, + ...control, + ...attributes, + ...anchorCompletions(), + ]; } function stcssHover(document, position) { @@ -212,14 +408,20 @@ function stcssHover(document, position) { const variable = /\$[\w-]+$/.exec(preceding)?.[0]; if (variable) { const hits = INDEX.variables.get(variable) ?? []; - return new vscode.Hover(new vscode.MarkdownString( - hits.length ? `**${variable}**: defined in ${hits.map((h) => `${relativeToWorkspace(h.uri)}:${h.line}`).join(", ")}` : `**${variable}**`, - )); + return new vscode.Hover( + new vscode.MarkdownString( + hits.length + ? `**${variable}**: defined in ${hits.map((h) => `${relativeToWorkspace(h.uri)}:${h.line}`).join(", ")}` + : `**${variable}**`, + ), + ); } const refer = /(?:\$\w+(?:\[[\w-]+\])?)$/.exec(preceding)?.[0]; if (refer) { - if (refer.startsWith("$refer")) return new vscode.Hover(new vscode.MarkdownString(REFERENCE_DOC)); - if (refer.startsWith("$relate")) return new vscode.Hover(new vscode.MarkdownString(RELATE_DOC)); + if (refer.startsWith("$refer")) + return new vscode.Hover(new vscode.MarkdownString(REFERENCE_DOC)); + if (refer.startsWith("$relate")) + return new vscode.Hover(new vscode.MarkdownString(RELATE_DOC)); } return null; } @@ -227,15 +429,30 @@ function stcssHover(document, position) { function hoverAnchor(name) { const hits = INDEX.anchors.get(name) ?? []; if (!hits.length) return null; - const body = hits.slice(0, 8).map((hit) => `- ${hit.kind}: \`${relativeToWorkspace(hit.uri)}:${hit.line}\``).join("\n"); - return new vscode.Hover(new vscode.MarkdownString(`**@${name}**: relation anchor\n\n${body}`)); + const body = hits + .slice(0, 8) + .map( + (hit) => `- ${hit.kind}: \`${relativeToWorkspace(hit.uri)}:${hit.line}\``, + ) + .join("\n"); + return new vscode.Hover( + new vscode.MarkdownString(`**@${name}**: relation anchor\n\n${body}`), + ); } function spugHover(document, position) { const range = document.getWordRangeAtPosition(position, /@?[A-Za-z_][\w-]*/); if (!range) return null; const word = document.getText(range); - if (word.startsWith("@") || document.getText(new vscode.Range(range.start.translate({ characterDelta: -1 }), range.start)) === "@") { + if ( + word.startsWith("@") || + document.getText( + new vscode.Range( + range.start.translate({ characterDelta: -1 }), + range.start, + ), + ) === "@" + ) { return hoverAnchor(word.replace(/^@/, "")); } return null; @@ -244,23 +461,38 @@ function spugHover(document, position) { function anchorDefinition(name) { return (INDEX.anchors.get(name) ?? []) .filter((hit) => hit.kind === "declared in SPUG") - .map((hit) => new vscode.Location(hit.uri, new vscode.Position(hit.line - 1, 0))); + .map( + (hit) => + new vscode.Location(hit.uri, new vscode.Position(hit.line - 1, 0)), + ); } function stcssDefinition(document, position) { - const line = document.lineAt(position.line).text.slice(0, position.character + 1); + const line = document + .lineAt(position.line) + .text.slice(0, position.character + 1); const anchor = /@([A-Za-z_][\w-]*)[^\w]?$/.exec(line)?.[1]; if (anchor) return anchorDefinition(anchor); return null; } function spugDefinition(document, position) { - const before = document.lineAt(position.line).text.slice(0, position.character + 1); + const before = document + .lineAt(position.line) + .text.slice(0, position.character + 1); const anchor = /@([A-Za-z_][\w-]*)[^\w]?$/.exec(before)?.[1]; if (anchor) { // jump from anchor to its STCSS references - const refs = (INDEX.anchors.get(anchor) ?? []).filter((hit) => hit.kind === "referenced in STCSS"); - return refs.map((hit) => new vscode.Location(hit.uri, new vscode.Range(hit.line - 1, 0, hit.line - 1, 200))); + const refs = (INDEX.anchors.get(anchor) ?? []).filter( + (hit) => hit.kind === "referenced in STCSS", + ); + return refs.map( + (hit) => + new vscode.Location( + hit.uri, + new vscode.Range(hit.line - 1, 0, hit.line - 1, 200), + ), + ); } return null; } @@ -270,7 +502,8 @@ function diagnosticCollection() { } function lintDocument(document, collection) { - if (document.languageId !== "stcss" && document.languageId !== "spug") return []; + if (document.languageId !== "stcss" && document.languageId !== "spug") + return []; const text = document.getText(); const issues = []; @@ -285,13 +518,25 @@ function lintDocument(document, collection) { } else if (char === "}") { depth -= 1; if (depth < 0) { - issues.push(new vscode.Diagnostic(rangeOf(text, index, 1), "Unexpected `}`.", vscode.DiagnosticSeverity.Error)); + issues.push( + new vscode.Diagnostic( + rangeOf(text, index, 1), + "Unexpected `}`.", + vscode.DiagnosticSeverity.Error, + ), + ); depth = 0; } } } if (depth > 0) { - issues.push(new vscode.Diagnostic(rangeOf(text, lastOpen, 1), "Unclosed `{` block.", vscode.DiagnosticSeverity.Error)); + issues.push( + new vscode.Diagnostic( + rangeOf(text, lastOpen, 1), + "Unclosed `{` block.", + vscode.DiagnosticSeverity.Error, + ), + ); } if (document.languageId === "stcss") { @@ -301,14 +546,23 @@ function lintDocument(document, collection) { const name = match[1]; const offset = match.index; const previous = text[offset - 1]; - const dynamic = text.slice(offset, text.indexOf("\n", offset) === -1 ? text.length : text.indexOf("\n", offset)).includes("("); + const dynamic = text + .slice( + offset, + text.indexOf("\n", offset) === -1 + ? text.length + : text.indexOf("\n", offset), + ) + .includes("("); if (AT_RULES.has(name) || previous === "$" || dynamic) continue; if (!INDEX.anchors.has(name)) { - issues.push(new vscode.Diagnostic( - rangeOf(text, offset, match[0].length), - `No SPUG file declares relation anchor \`@${name}\`.`, - vscode.DiagnosticSeverity.Warning, - )); + issues.push( + new vscode.Diagnostic( + rangeOf(text, offset, match[0].length), + `No SPUG file declares relation anchor \`@${name}\`.`, + vscode.DiagnosticSeverity.Warning, + ), + ); } } } @@ -318,7 +572,8 @@ function lintDocument(document, collection) { } function lintVisible() { - for (const editor of vscode.window.visibleTextEditors) lintDocument(editor.document, diagnostics); + for (const editor of vscode.window.visibleTextEditors) + lintDocument(editor.document, diagnostics); } function symbolKindFor(document) { @@ -339,15 +594,22 @@ function symbolsOf(document) { "", symbolKindFor(document), new vscode.Range(line, 0, line, text.length), - new vscode.Range(line, match[1].length, line, match[1].length + label.length), + new vscode.Range( + line, + match[1].length, + line, + match[1].length + label.length, + ), ); flat.push({ symbol, indent: match[1].length }); } const roots = []; const stack = []; for (const entry of flat) { - while (stack.length && entry.indent <= stack[stack.length - 1].indent) stack.pop(); - if (stack.length) stack[stack.length - 1].symbol.children.push(entry.symbol); + while (stack.length && entry.indent <= stack[stack.length - 1].indent) + stack.pop(); + if (stack.length) + stack[stack.length - 1].symbol.children.push(entry.symbol); else roots.push(entry.symbol); stack.push(entry); } @@ -372,14 +634,19 @@ function foldingOf(document) { if (!text.trim()) continue; while (stack.length && indent <= stack[stack.length - 1].indent) { const start = stack.pop(); - if (line - 1 > start) ranges.push(new vscode.FoldingRange(start, line - 1)); + if (line - 1 > start) + ranges.push(new vscode.FoldingRange(start, line - 1)); } stack.push({ indent, line }); } } while (stack.length) { - const start = typeof stack[stack.length - 1] === "number" ? stack.pop() : stack.pop().line; - if (document.lineCount - 1 > start) ranges.push(new vscode.FoldingRange(start, document.lineCount - 1)); + const start = + typeof stack[stack.length - 1] === "number" + ? stack.pop() + : stack.pop().line; + if (document.lineCount - 1 > start) + ranges.push(new vscode.FoldingRange(start, document.lineCount - 1)); } return ranges; } @@ -393,7 +660,12 @@ function completionsFor(document, position) { const prefix = before.trim(); if (prefix.length === 0 && position.character > 0) return []; // completions popped while just typing whitespace mid-line are noise - if (before.length > 0 && /\s$/.test(before) && !/[$@:(\[=,{]\s*$/.test(before)) return []; + if ( + before.length > 0 && + /\s$/.test(before) && + !/[$@:(\[=,{]\s*$/.test(before) + ) + return []; return isStcss(document) ? stcssCompletions(before) : spugCompletions(before); } @@ -412,8 +684,17 @@ function activate(context) { vscode.languages.registerCompletionItemProvider( ["stcss", "spug"], - { provideCompletionItems(document, position) { return completionsFor(document, position); } }, - "@", "$", ".", "-", ":", "=", + { + provideCompletionItems(document, position) { + return completionsFor(document, position); + }, + }, + "@", + "$", + ".", + "-", + ":", + "=", ), vscode.languages.registerHoverProvider(["stcss", "spug"], { @@ -421,7 +702,9 @@ function activate(context) { const word = document.getWordRangeAtPosition(position, /[@$]?[\w-]+/); if (!word) return null; try { - return isStcss(document) ? stcssHover(document, position) : spugHover(document, position); + return isStcss(document) + ? stcssHover(document, position) + : spugHover(document, position); } catch { return null; } @@ -431,7 +714,9 @@ function activate(context) { vscode.languages.registerDefinitionProvider(["stcss", "spug"], { provideDefinition(document, position) { try { - return isStcss(document) ? stcssDefinition(document, position) : spugDefinition(document, position); + return isStcss(document) + ? stcssDefinition(document, position) + : spugDefinition(document, position); } catch { return null; } @@ -439,14 +724,20 @@ function activate(context) { }), vscode.languages.registerDocumentSymbolProvider(["stcss", "spug"], { - provideDocumentSymbols(document) { return symbolsOf(document); }, + provideDocumentSymbols(document) { + return symbolsOf(document); + }, }), vscode.languages.registerFoldingRangeProvider(["stcss", "spug"], { - provideFoldingRanges(document) { return foldingOf(document); }, + provideFoldingRanges(document) { + return foldingOf(document); + }, }), - vscode.workspace.onDidOpenTextDocument((document) => lintDocument(document, diagnostics)), + vscode.workspace.onDidOpenTextDocument((document) => + lintDocument(document, diagnostics), + ), vscode.workspace.onDidChangeTextDocument((event) => { lintDocument(event.document, diagnostics); refresh(); diff --git a/editors/vscode/syntaxes/spug.tmLanguage.json b/editors/vscode/syntaxes/spug.tmLanguage.json index 755d656..4f60d47 100644 --- a/editors/vscode/syntaxes/spug.tmLanguage.json +++ b/editors/vscode/syntaxes/spug.tmLanguage.json @@ -29,9 +29,13 @@ "frontmatter": { "name": "meta.embedded.block.ts.spug", "begin": "^---\\s*$", - "beginCaptures": { "0": { "name": "punctuation.definition.frontmatter.begin.spug" } }, + "beginCaptures": { + "0": { "name": "punctuation.definition.frontmatter.begin.spug" } + }, "end": "^---\\s*$", - "endCaptures": { "0": { "name": "punctuation.definition.frontmatter.end.spug" } }, + "endCaptures": { + "0": { "name": "punctuation.definition.frontmatter.end.spug" } + }, "patterns": [{ "include": "source.ts" }] }, "javascript-block": { @@ -133,7 +137,9 @@ "2": { "name": "punctuation.definition.string.begin.stcss" } }, "end": "(?=!]=[?]?|[?:,+\\-*/%<>=!]", "name": "keyword.operator.js" } + { + "match": "&&|\\|\\||[?:,+\\-*/%<>=!]=[?]?|[?:,+\\-*/%<>=!]", + "name": "keyword.operator.js" + } ] } } diff --git a/editors/vscode/syntaxes/stcss.tmLanguage.json b/editors/vscode/syntaxes/stcss.tmLanguage.json index 94fbcf9..05cabe2 100644 --- a/editors/vscode/syntaxes/stcss.tmLanguage.json +++ b/editors/vscode/syntaxes/stcss.tmLanguage.json @@ -53,9 +53,14 @@ "begin": "^([^\\n{}]*?)\\s*\\{", "end": "\\}", "beginCaptures": { - "1": { "name": "meta.selector.stcss", "patterns": [{ "include": "#selector-content" }] } + "1": { + "name": "meta.selector.stcss", + "patterns": [{ "include": "#selector-content" }] + } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.block.end.bracket.curly.stcss" } }, - "endCaptures": { "0": { "name": "punctuation.definition.block.end.bracket.curly.stcss" } }, "patterns": [ { "include": "#comment" }, { "include": "#javascript-block" }, @@ -132,9 +137,13 @@ "attribute-selector": { "name": "meta.attribute-selector.stcss", "begin": "\\[", - "beginCaptures": { "0": { "name": "punctuation.definition.attribute-selector.begin.stcss" } }, + "beginCaptures": { + "0": { "name": "punctuation.definition.attribute-selector.begin.stcss" } + }, "end": "\\]", - "endCaptures": { "0": { "name": "punctuation.definition.attribute-selector.end.stcss" } }, + "endCaptures": { + "0": { "name": "punctuation.definition.attribute-selector.end.stcss" } + }, "patterns": [ { "include": "#interpolation" }, { "include": "#variable" }, @@ -169,16 +178,27 @@ "interpolation": { "name": "meta.interpolation.stcss", "begin": "#\\{", - "beginCaptures": { "0": { "name": "punctuation.definition.interpolation.begin.stcss" } }, + "beginCaptures": { + "0": { "name": "punctuation.definition.interpolation.begin.stcss" } + }, "end": "\\}", - "endCaptures": { "0": { "name": "punctuation.definition.interpolation.end.stcss" } }, - "patterns": [{ "include": "#curly-nesting" }, { "include": "#expression" }] + "endCaptures": { + "0": { "name": "punctuation.definition.interpolation.end.stcss" } + }, + "patterns": [ + { "include": "#curly-nesting" }, + { "include": "#expression" } + ] }, "curly-nesting": { "begin": "\\{", - "beginCaptures": { "0": { "name": "punctuation.definition.brace.curly.stcss" } }, + "beginCaptures": { + "0": { "name": "punctuation.definition.brace.curly.stcss" } + }, "end": "\\}", - "endCaptures": { "0": { "name": "punctuation.definition.brace.curly.stcss" } }, + "endCaptures": { + "0": { "name": "punctuation.definition.brace.curly.stcss" } + }, "patterns": [ { "include": "#curly-nesting" }, { "include": "#expression" } @@ -192,15 +212,24 @@ "2": { "name": "punctuation.definition.string.begin.stcss" } }, "end": "(? { if (!context.locals.auth) { const identity = await authFromRequest(context.raw as Request); if (identity) context.locals.auth = identity; } }; +export const authGuard: RouteGuard = async (context) => { + if (!context.locals.auth) { + const identity = await authFromRequest(context.raw as Request); + if (identity) context.locals.auth = identity; + } +}; -export interface AuthIdentity { id: string; email?: string; roles?: string[] } -export interface OAuthProvider { name: string; authorizationUrl: string; clientId: string; redirectUri: string; scope?: string[]; tokenUrl: string; clientSecret?: string; userInfoUrl: string; identity(profile: Record): AuthIdentity } +export interface AuthIdentity { + id: string; + email?: string; + roles?: string[]; +} +export interface OAuthProvider { + name: string; + authorizationUrl: string; + clientId: string; + redirectUri: string; + scope?: string[]; + tokenUrl: string; + clientSecret?: string; + userInfoUrl: string; + identity(profile: Record): AuthIdentity; +} export interface AuthIntegrationOptions { - login?: (email: string, password: string) => Promise | AuthIdentity | undefined; + login?: ( + email: string, + password: string, + ) => Promise | AuthIdentity | undefined; providers?: OAuthProvider[]; route?: string; - store?: SessionStore | ((context: IntegrationContext) => Promise | SessionStore); + store?: + | SessionStore + | ((context: IntegrationContext) => Promise | SessionStore); sessionTtl?: number; } const defaultStore = new MemorySessionStore(); -const sessionKey = (id: string) => `session:${createHash("sha256").update(id).digest("hex")}`; -async function cookie(identity: AuthIdentity, store: SessionStore, ttl: number): Promise { +const sessionKey = (id: string) => + `session:${createHash("sha256").update(id).digest("hex")}`; +async function cookie( + identity: AuthIdentity, + store: SessionStore, + ttl: number, +): Promise { const id = crypto.randomUUID(); - await store.set(sessionKey(id), { value: { identity }, expires: Date.now() + ttl * 1000 }); + await store.set(sessionKey(id), { + value: { identity }, + expires: Date.now() + ttl * 1000, + }); return `muddle_auth=${id}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${ttl}`; } /** Form and OAuth entry points. Token exchange remains provider-owned. */ -export function authIntegration(options: AuthIntegrationOptions = {}): Integration { +export function authIntegration( + options: AuthIntegrationOptions = {}, +): Integration { const route = options.route ?? "/auth"; let store: SessionStore = defaultStore; const ttl = options.sessionTtl ?? 3600; return { name: "auth", - async request(context) { const identity = await authFromRequest(context.raw as Request, store); if (identity) context.locals.auth = identity; }, + async request(context) { + const identity = await authFromRequest(context.raw as Request, store); + if (identity) context.locals.auth = identity; + }, async setup(context: IntegrationContext) { - store = typeof options.store === "function" ? await options.store(context) : options.store ?? defaultStore; - context.route("GET", `${route}/login`, () => new Response('
', { headers: { "content-type": "text/html; charset=utf-8" } })); + store = + typeof options.store === "function" + ? await options.store(context) + : (options.store ?? defaultStore); + context.route( + "GET", + `${route}/login`, + () => + new Response( + '
', + { headers: { "content-type": "text/html; charset=utf-8" } }, + ), + ); context.route("POST", `${route}/login`, async (request, ctx) => { - if (request.headers.get("origin") !== new URL(request.url).origin) return new Response("Invalid origin", { status: 403 }); - if (!ctx.body || typeof ctx.body !== "object" || Array.isArray(ctx.body)) return new Response("Invalid credentials", { status: 400 }); + if (request.headers.get("origin") !== new URL(request.url).origin) + return new Response("Invalid origin", { status: 403 }); + if ( + !ctx.body || + typeof ctx.body !== "object" || + Array.isArray(ctx.body) + ) + return new Response("Invalid credentials", { status: 400 }); const data = ctx.body as Record; - const identity = await options.login?.(String(data.email ?? ""), String(data.password ?? "")); - if (request.headers.get("content-type")?.includes("application/json")) return identity ? Response.json({ user: identity }, { headers: { "set-cookie": await cookie(identity, store, ttl) } }) : Response.json({ error: "Unauthorized" }, { status: 401 }); - return identity ? new Response(null, { status: 302, headers: { location: "/", "set-cookie": await cookie(identity, store, ttl) } }) : new Response("Unauthorized", { status: 401 }); + const identity = await options.login?.( + String(data.email ?? ""), + String(data.password ?? ""), + ); + if (request.headers.get("content-type")?.includes("application/json")) + return identity + ? Response.json( + { user: identity }, + { + headers: { "set-cookie": await cookie(identity, store, ttl) }, + }, + ) + : Response.json({ error: "Unauthorized" }, { status: 401 }); + return identity + ? new Response(null, { + status: 302, + headers: { + location: "/", + "set-cookie": await cookie(identity, store, ttl), + }, + }) + : new Response("Unauthorized", { status: 401 }); }); context.route("POST", `${route}/logout`, async (request) => { - if (request.headers.get("origin") !== new URL(request.url).origin) return new Response("Invalid origin", { status: 403 }); - const id = request.headers.get("cookie")?.match(/(?:^|;\s*)muddle_auth=([^;]+)/)?.[1]; + if (request.headers.get("origin") !== new URL(request.url).origin) + return new Response("Invalid origin", { status: 403 }); + const id = request.headers + .get("cookie") + ?.match(/(?:^|;\s*)muddle_auth=([^;]+)/)?.[1]; if (id) await store.delete(sessionKey(id)); - return new Response(null, { status: 302, headers: { location: "/", "set-cookie": "muddle_auth=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax" } }); + return new Response(null, { + status: 302, + headers: { + location: "/", + "set-cookie": + "muddle_auth=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax", + }, + }); }); for (const provider of options.providers ?? []) { context.route("GET", `${route}/${provider.name}`, async () => { const state = crypto.randomUUID(); - const verifier = Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("base64url"); - await store.set(`oauth:${state}`, { value: { verifier, provider: provider.name }, expires: Date.now() + 600_000 }); + const verifier = Buffer.from( + crypto.getRandomValues(new Uint8Array(32)), + ).toString("base64url"); + await store.set(`oauth:${state}`, { + value: { verifier, provider: provider.name }, + expires: Date.now() + 600_000, + }); const url = new URL(provider.authorizationUrl); url.searchParams.set("client_id", provider.clientId); url.searchParams.set("redirect_uri", provider.redirectUri); url.searchParams.set("response_type", "code"); url.searchParams.set("scope", (provider.scope ?? []).join(" ")); url.searchParams.set("state", state); - url.searchParams.set("code_challenge", Buffer.from(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))).toString("base64url")); + url.searchParams.set( + "code_challenge", + Buffer.from( + await crypto.subtle.digest( + "SHA-256", + new TextEncoder().encode(verifier), + ), + ).toString("base64url"), + ); url.searchParams.set("code_challenge_method", "S256"); - return new Response(null, { status: 302, headers: { location: url.toString(), "set-cookie": `muddle_oauth=${state}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=600` } }); - }); - context.route("GET", `${route}/${provider.name}/callback`, async (request) => { - const url = new URL(request.url); - const state = url.searchParams.get("state") ?? ""; - const bound = request.headers.get("cookie")?.match(/(?:^|;\s*)muddle_oauth=([^;]+)/)?.[1]; - if (bound !== state || !url.searchParams.get("code")) return new Response("Invalid OAuth state", { status: 400 }); - const record = await store.consume(`oauth:${state}`); - const transaction = record?.value as { provider: string; verifier: string } | undefined; - if (!transaction || record!.expires < Date.now() || transaction.provider !== provider.name) return new Response("Invalid OAuth state", { status: 400 }); - const body = new URLSearchParams({ grant_type: "authorization_code", code: url.searchParams.get("code")!, client_id: provider.clientId, redirect_uri: provider.redirectUri, code_verifier: transaction.verifier }); - if (provider.clientSecret) body.set("client_secret", provider.clientSecret); - const tokenResponse = await fetch(provider.tokenUrl, { method: "POST", body }); - if (!tokenResponse.ok) return new Response("OAuth exchange failed", { status: 502 }); - const token = await tokenResponse.json() as { access_token?: string }; - if (!token.access_token) return new Response("Missing OAuth token", { status: 502 }); - const profile = await fetch(provider.userInfoUrl, { headers: { authorization: `Bearer ${token.access_token}` } }); - if (!profile.ok) return new Response("OAuth profile failed", { status: 502 }); - return new Response(null, { status: 302, headers: { location: "/", "set-cookie": await cookie(provider.identity(await profile.json() as Record), store, ttl) } }); + return new Response(null, { + status: 302, + headers: { + location: url.toString(), + "set-cookie": `muddle_oauth=${state}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=600`, + }, + }); }); + context.route( + "GET", + `${route}/${provider.name}/callback`, + async (request) => { + const url = new URL(request.url); + const state = url.searchParams.get("state") ?? ""; + const bound = request.headers + .get("cookie") + ?.match(/(?:^|;\s*)muddle_oauth=([^;]+)/)?.[1]; + if (bound !== state || !url.searchParams.get("code")) + return new Response("Invalid OAuth state", { status: 400 }); + const record = await store.consume(`oauth:${state}`); + const transaction = record?.value as + | { provider: string; verifier: string } + | undefined; + if ( + !transaction || + record!.expires < Date.now() || + transaction.provider !== provider.name + ) + return new Response("Invalid OAuth state", { status: 400 }); + const body = new URLSearchParams({ + grant_type: "authorization_code", + code: url.searchParams.get("code")!, + client_id: provider.clientId, + redirect_uri: provider.redirectUri, + code_verifier: transaction.verifier, + }); + if (provider.clientSecret) + body.set("client_secret", provider.clientSecret); + const tokenResponse = await fetch(provider.tokenUrl, { + method: "POST", + body, + }); + if (!tokenResponse.ok) + return new Response("OAuth exchange failed", { status: 502 }); + const token = (await tokenResponse.json()) as { + access_token?: string; + }; + if (!token.access_token) + return new Response("Missing OAuth token", { status: 502 }); + const profile = await fetch(provider.userInfoUrl, { + headers: { authorization: `Bearer ${token.access_token}` }, + }); + if (!profile.ok) + return new Response("OAuth profile failed", { status: 502 }); + return new Response(null, { + status: 302, + headers: { + location: "/", + "set-cookie": await cookie( + provider.identity( + (await profile.json()) as Record, + ), + store, + ttl, + ), + }, + }); + }, + ); } }, }; } -export async function authFromRequest(request: Request, store: SessionStore = defaultStore): Promise { - const value = request.headers.get("cookie")?.match(/(?:^|;\s*)muddle_auth=([^;]+)/)?.[1]; +export async function authFromRequest( + request: Request, + store: SessionStore = defaultStore, +): Promise { + const value = request.headers + .get("cookie") + ?.match(/(?:^|;\s*)muddle_auth=([^;]+)/)?.[1]; if (!value) return undefined; const session = await store.get(sessionKey(value)); - if (!session || session.expires < Date.now()) { await store.delete(sessionKey(value)); return undefined; } + if (!session || session.expires < Date.now()) { + await store.delete(sessionKey(value)); + return undefined; + } return session.value.identity as AuthIdentity; } diff --git a/integrations/fonts.ts b/integrations/fonts.ts index 8c1c734..4d351e5 100644 --- a/integrations/fonts.ts +++ b/integrations/fonts.ts @@ -11,9 +11,15 @@ export interface GoogleFontsOptions { function memoryCache(): Cache { const entries = new Map(); return { - async get(key) { return entries.get(key); }, - async put(key, value) { entries.set(key, value); }, - async delete(key) { entries.delete(key); }, + async get(key) { + return entries.get(key); + }, + async put(key, value) { + entries.set(key, value); + }, + async delete(key) { + entries.delete(key); + }, }; } @@ -22,7 +28,9 @@ function text(value: Uint8Array): string { } function googleUrl(options: GoogleFontsOptions): string { - const family = options.families.map((value) => `family=${encodeURIComponent(value.replace(/\s+/g, "+"))}`).join("&"); + const family = options.families + .map((value) => `family=${encodeURIComponent(value.replace(/\s+/g, "+"))}`) + .join("&"); return `https://fonts.googleapis.com/css2?${family}&display=${options.display ?? "swap"}`; } @@ -43,13 +51,23 @@ export function googleFonts(options: GoogleFontsOptions): Integration { css = new Uint8Array(await response.arrayBuffer()); await cache.put(key, css, { ttl: 86400 }); } - const proxied = text(css).replace(/url\((https:\/\/[^)]+)\)/g, (_match, url: string) => `url(${proxyPrefix}${encodeURIComponent(url)})`); - return new Response(proxied, { headers: { "content-type": "text/css; charset=utf-8", "cache-control": "public, max-age=86400" } }); + const proxied = text(css).replace( + /url\((https:\/\/[^)]+)\)/g, + (_match, url: string) => + `url(${proxyPrefix}${encodeURIComponent(url)})`, + ); + return new Response(proxied, { + headers: { + "content-type": "text/css; charset=utf-8", + "cache-control": "public, max-age=86400", + }, + }); }); context.route("GET", `${proxyPrefix}*`, async (request) => { const encoded = new URL(request.url).pathname.slice(proxyPrefix.length); const url = decodeURIComponent(encoded); - if (!url.startsWith("https://fonts.gstatic.com/")) return new Response("Not Found", { status: 404 }); + if (!url.startsWith("https://fonts.gstatic.com/")) + return new Response("Not Found", { status: 404 }); const key = `font:${url}`; let font = await cache.get(key); if (!font) { @@ -58,7 +76,12 @@ export function googleFonts(options: GoogleFontsOptions): Integration { font = new Uint8Array(await response.arrayBuffer()); await cache.put(key, font, { ttl: 31536000 }); } - return new Response(font, { headers: { "content-type": "font/woff2", "cache-control": "public, max-age=31536000, immutable" } }); + return new Response(font, { + headers: { + "content-type": "font/woff2", + "cache-control": "public, max-age=31536000, immutable", + }, + }); }); }, }; diff --git a/integrations/hooks.ts b/integrations/hooks.ts index 7455d2b..bd78415 100644 --- a/integrations/hooks.ts +++ b/integrations/hooks.ts @@ -29,7 +29,8 @@ export function layoutHooks( const utilities = new Map(); hook({ utilities: (name, utility) => utilities.set(name, utility), - concentric: (name) => utilities.set(name ?? "concentric", concentric(name)), + concentric: (name) => + utilities.set(name ?? "concentric", concentric(name)), }); context.loader("utilities", { name: "utilities", @@ -38,7 +39,10 @@ export function layoutHooks( const css = file.source.split(/\s+/).flatMap((token) => { const [name, value] = token.split(":", 2); const utility = utilities.get(name ?? ""); - return utility?.({ name: name ?? "", value, selector: `.${token}` }) ?? []; + return ( + utility?.({ name: name ?? "", value, selector: `.${token}` }) ?? + [] + ); }); return { data: {}, content: css.join("\n") }; }, diff --git a/integrations/react.ts b/integrations/react.ts index 3498bfd..d1a37e3 100644 --- a/integrations/react.ts +++ b/integrations/react.ts @@ -107,7 +107,7 @@ export function mouseTrackerCss(options: TrackerOptions = {}): string { const rules = Array.from({ length: columns * rows }, (_, index) => { const x = Math.floor(index / rows) + 1; const y = (index % rows) + 1; - return `${selector}:has(.mouse-tracker__cell:nth-child(${index + 1}):hover) { --muddle-mouse-x: ${x / columns * 100}%; --muddle-mouse-y: ${y / rows * 100}%; }`; + return `${selector}:has(.mouse-tracker__cell:nth-child(${index + 1}):hover) { --muddle-mouse-x: ${(x / columns) * 100}%; --muddle-mouse-y: ${(y / rows) * 100}%; }`; }); return `${selector} { position: relative; --muddle-mouse-x: 50%; --muddle-mouse-y: 50%; }\n${selector} > .mouse-tracker { position: absolute; inset: 0; display: grid; grid-template-columns: repeat(${columns}, 1fr); grid-template-rows: repeat(${rows}, 1fr); pointer-events: none; }\n${selector}[data-muddle-capture="true"] > .mouse-tracker .mouse-tracker__cell { pointer-events: auto; }\n${rules.join("\n")}`; } @@ -179,13 +179,16 @@ export function reactIntegration( }, }); - context.route("GET", route, () => - new Response(reactCss(options), { - headers: { - "content-type": "text/css; charset=utf-8", - "cache-control": "public, max-age=3600", - }, - }), + context.route( + "GET", + route, + () => + new Response(reactCss(options), { + headers: { + "content-type": "text/css; charset=utf-8", + "cache-control": "public, max-age=3600", + }, + }), ); }, }; diff --git a/integrations/responsivity.ts b/integrations/responsivity.ts index caf82fa..be7384f 100644 --- a/integrations/responsivity.ts +++ b/integrations/responsivity.ts @@ -6,10 +6,17 @@ export interface ResponsivityOptions { route?: string; } -export const DEFAULT_BREAKPOINTS = { sm: "40rem", md: "48rem", lg: "64rem", xl: "80rem" }; +export const DEFAULT_BREAKPOINTS = { + sm: "40rem", + md: "48rem", + lg: "64rem", + xl: "80rem", +}; /** Adds mobile-first `@responsive name {}` blocks to STCSS. */ -export function responsivityIntegration(options: ResponsivityOptions = {}): Integration { +export function responsivityIntegration( + options: ResponsivityOptions = {}, +): Integration { const breakpoints = { ...DEFAULT_BREAKPOINTS, ...options.breakpoints }; return { name: "responsivity", @@ -17,7 +24,12 @@ export function responsivityIntegration(options: ResponsivityOptions = {}): Inte context.loader("responsive-stcss", { name: "responsive-stcss", extensions: [".responsive.stcss"], - load(file) { return { data: { breakpoints }, content: compileCss(file.source, { breakpoints }) }; }, + load(file) { + return { + data: { breakpoints }, + content: compileCss(file.source, { breakpoints }), + }; + }, }); }, }; diff --git a/integrations/shadcn.ts b/integrations/shadcn.ts index e56161a..6215e18 100644 --- a/integrations/shadcn.ts +++ b/integrations/shadcn.ts @@ -1,2 +1,5 @@ /** Compatibility export. The implementation lives in the isolated package. */ -export { shadcnIntegration, type ShadcnIntegrationOptions } from "../modules/shadcn/src/integration.ts"; +export { + shadcnIntegration, + type ShadcnIntegrationOptions, +} from "../modules/shadcn/src/integration.ts"; diff --git a/justfile b/justfile index ec5a3c2..088f6ff 100644 --- a/justfile +++ b/justfile @@ -7,6 +7,9 @@ default: test: bun test ./tests +fmt: + bunx @biomejs/biome format --fix + loc: @echo "Loc: counting source lines in the codebase..." @find . \ diff --git a/modules/amezing/README.md b/modules/amezing/README.md index 76e888b..dd31c3a 100644 --- a/modules/amezing/README.md +++ b/modules/amezing/README.md @@ -1,3 +1,3 @@ # @muddle/amezing -Port of the Ametrine theme (by Daudix) as a muddle module. \ No newline at end of file +Port of the Ametrine theme (by Daudix) as a muddle module. diff --git a/modules/amezing/index.ts b/modules/amezing/index.ts index 2c358c5..5c1371e 100644 --- a/modules/amezing/index.ts +++ b/modules/amezing/index.ts @@ -6,38 +6,109 @@ */ import { fileURLToPath } from "node:url"; import { readFile } from "node:fs/promises"; -import type { - Integration, - Mounts, -} from "muddle"; +import type { Integration, Mounts } from "muddle"; import type { Views } from "muddle/views"; - -export interface AmezingAccentPreset { name: string; light: string; dark: string } +export interface AmezingAccentPreset { + name: string; + light: string; + dark: string; +} /** Accent presets from ametrine/data/accent_presets.toml. */ export const ACCENT_PRESETS: Record = { - red: { name: "red", light: "hsl(342.0779, 72%, 58%)", dark: "hsl(345.18988, 100%, 69%)" }, - orange: { name: "orange", light: "hsl(15.771428, 74%, 54%)", dark: "hsl(19.731544, 96%, 70%)" }, - yellow: { name: "yellow", light: "hsl(34.639175, 91%, 42%)", dark: "hsl(44.70588, 100%, 70%)" }, - green: { name: "green", light: "hsl(153.7815, 61%, 38%)", dark: "hsl(90, 59%, 66%)" }, - blue: { name: "blue", light: "hsl(192, 71%, 38%)", dark: "hsl(186.42857, 71%, 69%)" }, - purple: { name: "purple", light: "hsl(254.11766, 44%, 55%)", dark: "hsl(249.88234, 77%, 78%)" }, - teal: { name: "teal", light: "hsl(180, 65%, 35%)", dark: "hsl(183, 85%, 65%)" }, - pink: { name: "pink", light: "hsl(333.88236, 69%, 58%)", dark: "hsl(336.78634, 100%, 70%)" }, - cyan: { name: "cyan", light: "hsl(195, 71%, 35%)", dark: "hsl(193.41046, 72%, 70%)" }, - indigo: { name: "indigo", light: "hsl(232.05884, 61%, 42%)", dark: "hsl(234.61538, 73%, 71%)" }, - amber: { name: "amber", light: "hsl(38.57695, 94%, 44%)", dark: "hsl(40.35484, 100%, 72%)" }, - ochrea: { name: "ochrea", light: "hsl(45.75298, 95%, 45%)", dark: "hsl(46.09091, 100%, 78%)" }, - lime: { name: "lime", light: "hsl(86.25406, 60%, 42%)", dark: "hsl(86.84211, 93%, 71%)" }, - maroon: { name: "maroon", light: "hsl(321.81819, 26%, 41%)", dark: "hsl(322.10526, 66%, 66%)" }, - navy: { name: "navy", light: "hsl(216, 44%, 38%)", dark: "hsl(218, 69%, 69%)" }, - olive: { name: "olive", light: "hsl(81, 36%, 41%)", dark: "hsl(81, 64%, 65%)" }, - slate: { name: "slate", light: "hsl(212.53197, 15%, 44%)", dark: "hsl(212.90323, 61%, 72%)" }, + red: { + name: "red", + light: "hsl(342.0779, 72%, 58%)", + dark: "hsl(345.18988, 100%, 69%)", + }, + orange: { + name: "orange", + light: "hsl(15.771428, 74%, 54%)", + dark: "hsl(19.731544, 96%, 70%)", + }, + yellow: { + name: "yellow", + light: "hsl(34.639175, 91%, 42%)", + dark: "hsl(44.70588, 100%, 70%)", + }, + green: { + name: "green", + light: "hsl(153.7815, 61%, 38%)", + dark: "hsl(90, 59%, 66%)", + }, + blue: { + name: "blue", + light: "hsl(192, 71%, 38%)", + dark: "hsl(186.42857, 71%, 69%)", + }, + purple: { + name: "purple", + light: "hsl(254.11766, 44%, 55%)", + dark: "hsl(249.88234, 77%, 78%)", + }, + teal: { + name: "teal", + light: "hsl(180, 65%, 35%)", + dark: "hsl(183, 85%, 65%)", + }, + pink: { + name: "pink", + light: "hsl(333.88236, 69%, 58%)", + dark: "hsl(336.78634, 100%, 70%)", + }, + cyan: { + name: "cyan", + light: "hsl(195, 71%, 35%)", + dark: "hsl(193.41046, 72%, 70%)", + }, + indigo: { + name: "indigo", + light: "hsl(232.05884, 61%, 42%)", + dark: "hsl(234.61538, 73%, 71%)", + }, + amber: { + name: "amber", + light: "hsl(38.57695, 94%, 44%)", + dark: "hsl(40.35484, 100%, 72%)", + }, + ochrea: { + name: "ochrea", + light: "hsl(45.75298, 95%, 45%)", + dark: "hsl(46.09091, 100%, 78%)", + }, + lime: { + name: "lime", + light: "hsl(86.25406, 60%, 42%)", + dark: "hsl(86.84211, 93%, 71%)", + }, + maroon: { + name: "maroon", + light: "hsl(321.81819, 26%, 41%)", + dark: "hsl(322.10526, 66%, 66%)", + }, + navy: { + name: "navy", + light: "hsl(216, 44%, 38%)", + dark: "hsl(218, 69%, 69%)", + }, + olive: { + name: "olive", + light: "hsl(81, 36%, 41%)", + dark: "hsl(81, 64%, 65%)", + }, + slate: { + name: "slate", + light: "hsl(212.53197, 15%, 44%)", + dark: "hsl(212.90323, 61%, 72%)", + }, }; function parseHsl(value: string): [string, string, string] | undefined { - const match = /hsl\(\s*([\d.]+%?)\s*[, ]\s*([\d.]+%?)\s*[, ]\s*([\d.]+%?)\s*\)/.exec(value); + const match = + /hsl\(\s*([\d.]+%?)\s*[, ]\s*([\d.]+%?)\s*[, ]\s*([\d.]+%?)\s*\)/.exec( + value, + ); return match ? [match[1]!, match[2]!, match[3]!] : undefined; } @@ -45,7 +116,11 @@ function parseHsl(value: string): [string, string, string] | undefined { export function accentStyle(accent: string | [string, string?]): string { const inputs = Array.isArray(accent) ? accent : [accent]; const light = ACCENT_PRESETS[inputs[0] ?? ""]?.light ?? inputs[0]; - const dark = ACCENT_PRESETS[inputs[1] ?? inputs[0] ?? ""]?.dark ?? inputs[1] ?? ACCENT_PRESETS[inputs[0] ?? ""]?.dark ?? inputs[0]; + const dark = + ACCENT_PRESETS[inputs[1] ?? inputs[0] ?? ""]?.dark ?? + inputs[1] ?? + ACCENT_PRESETS[inputs[0] ?? ""]?.dark ?? + inputs[0]; const lightHsl = parseHsl(light ?? ""); const darkHsl = parseHsl(dark ?? ""); if (!lightHsl || !darkHsl) throw new Error(`Unknown accent color: ${accent}`); @@ -86,27 +161,55 @@ const COMPONENTS = [ ] as const; function componentTemplates(): [string, string][] { - return COMPONENTS.map((name) => [`amezing-${name}`, fileURLToPath(new URL(`./components/${name}.spug`, import.meta.url))]); + return COMPONENTS.map((name) => [ + `amezing-${name}`, + fileURLToPath(new URL(`./components/${name}.spug`, import.meta.url)), + ]); } interface TemplateViews { component(name: string, component: unknown): unknown; - engine?: { renderComponent(path: string, props: Record, context: { locals: Record; params: Record }): Promise }; + engine?: { + renderComponent( + path: string, + props: Record, + context: { + locals: Record; + params: Record; + }, + ): Promise; + }; } function registerAll(views: TemplateViews, accent: string | undefined): void { for (const [name, template] of componentTemplates()) { views.component(name, { - async render(props: Record, context: { locals: Record; params: Record }) { - return views.engine!.renderComponent(template, { accentStyle: accent, ...props }, context); + async render( + props: Record, + context: { + locals: Record; + params: Record; + }, + ) { + return views.engine!.renderComponent( + template, + { accentStyle: accent, ...props }, + context, + ); }, }); } } /** Registers the Ametrine components on the views registry. */ -export function registerAmezing(views: Views, options: { accentColor?: string | [string, string?] } = {}): void { - registerAll(views as unknown as Parameters[0], options.accentColor ? accentStyle(options.accentColor) : undefined); +export function registerAmezing( + views: Views, + options: { accentColor?: string | [string, string?] } = {}, +): void { + registerAll( + views as unknown as Parameters[0], + options.accentColor ? accentStyle(options.accentColor) : undefined, + ); } /** Compiled theme stylesheet (Citrine base + Ametrine layout). */ @@ -133,22 +236,44 @@ export const amezingFonts: Record = { "/fonts/commit-mono.woff2": "fonts/commit-mono.woff2", }; -export function amezingMount(options: { accentColor?: string | [string, string?] } = {}): Mounts { - const accent = options.accentColor ? accentStyle(options.accentColor) : undefined; +export function amezingMount( + options: { accentColor?: string | [string, string?] } = {}, +): Mounts { + const accent = options.accentColor + ? accentStyle(options.accentColor) + : undefined; return { - views: (views) => registerAll(views as unknown as Parameters[0], accent), + views: (views) => + registerAll( + views as unknown as Parameters[0], + accent, + ), routes: (route) => { for (const [routePath, file] of Object.entries(amezingScripts)) { - route("GET", `/_muddle/amezing/${routePath}`, async () => - new Response(await readFile(new URL(`./${file}`, import.meta.url), "utf8"), { - headers: { "content-type": "text/javascript; charset=utf-8" }, - })); + route( + "GET", + `/_muddle/amezing/${routePath}`, + async () => + new Response( + await readFile(new URL(`./${file}`, import.meta.url), "utf8"), + { + headers: { "content-type": "text/javascript; charset=utf-8" }, + }, + ), + ); } for (const [routePath, file] of Object.entries(amezingFonts)) { - route("GET", routePath, async () => - new Response(await readFile(new URL(`./${file}`, import.meta.url)), { - headers: { "content-type": "font/woff2" }, - })); + route( + "GET", + routePath, + async () => + new Response( + await readFile(new URL(`./${file}`, import.meta.url)), + { + headers: { "content-type": "font/woff2" }, + }, + ), + ); } }, styles: { "/_muddle/amezing.css": () => amezingStyles() }, @@ -156,6 +281,8 @@ export function amezingMount(options: { accentColor?: string | [string, string?] } /** Convenience integration. */ -export function amezingIntegration(options: { accentColor?: string | [string, string?] } = {}): Integration { +export function amezingIntegration( + options: { accentColor?: string | [string, string?] } = {}, +): Integration { return { name: "amezing", mounts: [amezingMount(options)] }; } diff --git a/modules/amezing/package.json b/modules/amezing/package.json index f153655..698c0a9 100644 --- a/modules/amezing/package.json +++ b/modules/amezing/package.json @@ -1,7 +1,12 @@ { - "name": "@muddle/amezing", - "version": "0.1.0", - "type": "module", - "exports": { ".": "./index.ts", "./styles": "./styles/ametrine.css" }, - "peerDependencies": { "muddle": "*" } + "name": "@muddle/amezing", + "version": "0.1.0", + "type": "module", + "exports": { + ".": "./index.ts", + "./styles": "./styles/ametrine.css" + }, + "peerDependencies": { + "muddle": "*" + } } diff --git a/modules/amezing/sass/citrine/abstracts/_functions.scss b/modules/amezing/sass/citrine/abstracts/_functions.scss index dc7e8f9..12da3e6 100644 --- a/modules/amezing/sass/citrine/abstracts/_functions.scss +++ b/modules/amezing/sass/citrine/abstracts/_functions.scss @@ -1,5 +1,5 @@ // Converts px values to rem // rpx(1) --> 0.0625rem @function rpx($px-value) { - @return ($px-value / 16) + rem; + @return ($px-value / 16) + rem; } diff --git a/modules/amezing/sass/citrine/abstracts/_mixins.scss b/modules/amezing/sass/citrine/abstracts/_mixins.scss index 6a0739b..d1b4527 100644 --- a/modules/amezing/sass/citrine/abstracts/_mixins.scss +++ b/modules/amezing/sass/citrine/abstracts/_mixins.scss @@ -1,82 +1,80 @@ @mixin icon($mode: normal) { - @if $mode == normal { - font-style: normal; - font-variant: normal; - font-weight: normal; - line-height: 1; - user-select: none; - text-transform: none; - -webkit-user-select: none; - display: inline-block; - box-sizing: content-box; - } @else if $mode == pseudo { - -webkit-mask-image: var(--icon); - -webkit-mask-size: cover; - display: block; - mask-image: var(--icon); - mask-size: cover; - background-color: currentColor; - width: 1em; - height: 1em; - content: ""; - } + @if $mode == normal { + font-style: normal; + font-variant: normal; + font-weight: normal; + line-height: 1; + user-select: none; + text-transform: none; + -webkit-user-select: none; + display: inline-block; + box-sizing: content-box; + } @else if $mode == pseudo { + -webkit-mask-image: var(--icon); + -webkit-mask-size: cover; + display: block; + mask-image: var(--icon); + mask-size: cover; + background-color: currentColor; + width: 1em; + height: 1em; + content: ""; + } } @mixin gradient-fill($mode: normal) { - @if $mode == normal { - box-shadow: var(--edge-highlight), var(--shadow-glow); - background-image: linear-gradient( - to bottom right, - var(--accent-color), - var(--accent-highlight) - ); - color: var(--contrast-color); - text-shadow: var(--text-shadow-glow); - } @else if $mode == hover { - box-shadow: var(--edge-highlight); - background-color: var(--accent-color); - color: var(--contrast-color); - } + @if $mode == normal { + box-shadow: var(--edge-highlight), var(--shadow-glow); + background-image: linear-gradient( + to bottom right, + var(--accent-color), + var(--accent-highlight) + ); + color: var(--contrast-color); + text-shadow: var(--text-shadow-glow); + } @else if $mode == hover { + box-shadow: var(--edge-highlight); + background-color: var(--accent-color); + color: var(--contrast-color); + } } @mixin card { - box-shadow: var(--edge-highlight), var(--shadow); - border-radius: var(--rounded-corner); - background-color: var(--bg-muted-1); - padding: 1rem; + box-shadow: var(--edge-highlight), var(--shadow); + border-radius: var(--rounded-corner); + background-color: var(--bg-muted-1); + padding: 1rem; } @mixin chip { - transition: - all var(--transition), - transform var(--transition-bezier); - box-shadow: var(--edge-highlight); - border-radius: var(--rounded-corner-small); - background-color: var(--bg-muted-1); - padding-inline: 0.375rem; - text-decoration: none; + transition: all var(--transition), transform var(--transition-bezier); + box-shadow: var(--edge-highlight); + border-radius: var(--rounded-corner-small); + background-color: var(--bg-muted-1); + padding-inline: 0.375rem; + text-decoration: none; } @mixin easing-mask($direction: to bottom) { - --mask-image: linear-gradient( - to #{$direction}, - hsl(0, 0%, 0%) 0%, - hsla(0, 0%, 0%, 0.987) 8.1%, - hsla(0, 0%, 0%, 0.951) 15.5%, - hsla(0, 0%, 0%, 0.896) 22.5%, - hsla(0, 0%, 0%, 0.825) 29%, - hsla(0, 0%, 0%, 0.741) 35.3%, - hsla(0, 0%, 0%, 0.648) 41.2%, - hsla(0, 0%, 0%, 0.55) 47.1%, - hsla(0, 0%, 0%, 0.45) 52.9%, - hsla(0, 0%, 0%, 0.352) 58.8%, - hsla(0, 0%, 0%, 0.259) 64.7%, - hsla(0, 0%, 0%, 0.175) 71%, - hsla(0, 0%, 0%, 0.104) 77.5%, - hsla(0, 0%, 0%, 0.049) 84.5%, - hsla(0, 0%, 0%, 0.013) 91.9%, - hsla(0, 0%, 0%, 0) 100% - ); - mask-image: var(--mask-image); - -webkit-mask-image: var(--mask-image); + --mask-image: linear-gradient( + to #{$direction}, + hsl(0, 0%, 0%) 0%, + hsla(0, 0%, 0%, 0.987) 8.1%, + hsla(0, 0%, 0%, 0.951) 15.5%, + hsla(0, 0%, 0%, 0.896) 22.5%, + hsla(0, 0%, 0%, 0.825) 29%, + hsla(0, 0%, 0%, 0.741) 35.3%, + hsla(0, 0%, 0%, 0.648) 41.2%, + hsla(0, 0%, 0%, 0.55) 47.1%, + hsla(0, 0%, 0%, 0.45) 52.9%, + hsla(0, 0%, 0%, 0.352) 58.8%, + hsla(0, 0%, 0%, 0.259) 64.7%, + hsla(0, 0%, 0%, 0.175) 71%, + hsla(0, 0%, 0%, 0.104) 77.5%, + hsla(0, 0%, 0%, 0.049) 84.5%, + hsla(0, 0%, 0%, 0.013) 91.9%, + hsla(0, 0%, 0%, 0) 100% + ); + mask-image: var(--mask-image); + -webkit-mask-image: var(--mask-image); } diff --git a/modules/amezing/sass/citrine/abstracts/_variables.scss b/modules/amezing/sass/citrine/abstracts/_variables.scss index 0a7019d..4e0ae94 100644 --- a/modules/amezing/sass/citrine/abstracts/_variables.scss +++ b/modules/amezing/sass/citrine/abstracts/_variables.scss @@ -2,569 +2,564 @@ @use "mixins" as *; @mixin variables($modes: "light") { - @each $mode in $modes { - @if $mode == "light" { - // COLORS - --bg-overlay: linear-gradient( - rgba(255, 255, 255, var(--dim-opacity)), - rgba(255, 255, 255, var(--dim-opacity)) - ); - --fg-color: #29242a; - --fg-contrast: #fcfcfa; - --red-fg: hsl(342.0779, 72%, 58%); - --red-h: 342.0779; - --red-s: 72%; - --red-l: 58%; - --orange-fg: hsl(15.771428, 74%, 54%); - --orange-h: 15.771428; - --orange-s: 74%; - --orange-l: 54%; - --yellow-fg: hsl(34.639175, 91%, 42%); - --yellow-h: 34.639175; - --yellow-s: 91%; - --yellow-l: 42%; - --green-fg: hsl(153.7815, 61%, 38%); - --green-h: 153.7815; - --green-s: 61%; - --green-l: 38%; - --blue-fg: hsl(192, 71%, 38%); - --blue-h: 192; - --blue-s: 71%; - --blue-l: 38%; - --purple-fg: hsl(254.11766, 44%, 55%); - --purple-h: 254.11766; - --purple-s: 44%; - --purple-l: 55%; - --red-bg-1: hsla( - var(--red-h), - var(--red-s), - var(--red-l), - var(--color-opacity-1) - ); - --red-bg-2: hsla( - var(--red-h), - var(--red-s), - var(--red-l), - var(--color-opacity-2) - ); - --orange-bg-1: hsla( - var(--orange-h), - var(--orange-s), - var(--orange-l), - var(--color-opacity-1) - ); - --orange-bg-2: hsla( - var(--orange-h), - var(--orange-s), - var(--orange-l), - var(--color-opacity-2) - ); - --yellow-bg-1: hsla( - var(--yellow-h), - var(--yellow-s), - var(--yellow-l), - var(--color-opacity-1) - ); - --yellow-bg-2: hsla( - var(--yellow-h), - var(--yellow-s), - var(--yellow-l), - var(--color-opacity-2) - ); - --green-bg-1: hsla( - var(--green-h), - var(--green-s), - var(--green-l), - var(--color-opacity-1) - ); - --green-bg-2: hsla( - var(--green-h), - var(--green-s), - var(--green-l), - var(--color-opacity-2) - ); - --blue-bg-1: hsla( - var(--blue-h), - var(--blue-s), - var(--blue-l), - var(--color-opacity-1) - ); - --blue-bg-2: hsla( - var(--blue-h), - var(--blue-s), - var(--blue-l), - var(--color-opacity-2) - ); - --purple-bg-1: hsla( - var(--purple-h), - var(--purple-s), - var(--purple-l), - var(--color-opacity-1) - ); - --purple-bg-2: hsla( - var(--purple-h), - var(--purple-s), - var(--purple-l), - var(--color-opacity-2) - ); - --red-fg-muted-1: hsla( - var(--red-h), - var(--red-s), - calc(var(--red-l) - var(--red-l) * 0.85), - var(--fg-muted-1-opacity) - ); - --red-fg-muted-2: hsla( - var(--red-h), - var(--red-s), - calc(var(--red-l) - var(--red-l) * 0.85), - var(--fg-muted-2-opacity) - ); - --orange-fg-muted-1: hsla( - var(--orange-h), - var(--orange-s), - calc(var(--orange-l) - var(--orange-l) * 0.85), - var(--fg-muted-1-opacity) - ); - --orange-fg-muted-2: hsla( - var(--orange-h), - var(--orange-s), - calc(var(--orange-l) - var(--orange-l) * 0.85), - var(--fg-muted-2-opacity) - ); - --yellow-fg-muted-1: hsla( - var(--yellow-h), - var(--yellow-s), - calc(var(--yellow-l) - var(--yellow-l) * 0.85), - var(--fg-muted-1-opacity) - ); - --yellow-fg-muted-2: hsla( - var(--yellow-h), - var(--yellow-s), - calc(var(--yellow-l) - var(--yellow-l) * 0.85), - var(--fg-muted-2-opacity) - ); - --green-fg-muted-1: hsla( - var(--green-h), - var(--green-s), - calc(var(--green-l) - var(--green-l) * 0.85), - var(--fg-muted-1-opacity) - ); - --green-fg-muted-2: hsla( - var(--green-h), - var(--green-s), - calc(var(--green-l) - var(--green-l) * 0.85), - var(--fg-muted-2-opacity) - ); - --blue-fg-muted-1: hsla( - var(--blue-h), - var(--blue-s), - calc(var(--blue-l) - var(--blue-l) * 0.85), - var(--fg-muted-1-opacity) - ); - --blue-fg-muted-2: hsla( - var(--blue-h), - var(--blue-s), - calc(var(--blue-l) - var(--blue-l) * 0.85), - var(--fg-muted-2-opacity) - ); - --purple-fg-muted-1: hsla( - var(--purple-h), - var(--purple-s), - calc(var(--purple-l) - var(--purple-l) * 0.85), - var(--fg-muted-1-opacity) - ); - --purple-fg-muted-2: hsla( - var(--purple-h), - var(--purple-s), - calc(var(--purple-l) - var(--purple-l) * 0.85), - var(--fg-muted-2-opacity) - ); - --shade-color: rgba(0, 0, 0, 0.07); + @each $mode in $modes { + @if $mode == "light" { + // COLORS + --bg-overlay: linear-gradient( + rgba(255, 255, 255, var(--dim-opacity)), + rgba(255, 255, 255, var(--dim-opacity)) + ); + --fg-color: #29242a; + --fg-contrast: #fcfcfa; + --red-fg: hsl(342.0779, 72%, 58%); + --red-h: 342.0779; + --red-s: 72%; + --red-l: 58%; + --orange-fg: hsl(15.771428, 74%, 54%); + --orange-h: 15.771428; + --orange-s: 74%; + --orange-l: 54%; + --yellow-fg: hsl(34.639175, 91%, 42%); + --yellow-h: 34.639175; + --yellow-s: 91%; + --yellow-l: 42%; + --green-fg: hsl(153.7815, 61%, 38%); + --green-h: 153.7815; + --green-s: 61%; + --green-l: 38%; + --blue-fg: hsl(192, 71%, 38%); + --blue-h: 192; + --blue-s: 71%; + --blue-l: 38%; + --purple-fg: hsl(254.11766, 44%, 55%); + --purple-h: 254.11766; + --purple-s: 44%; + --purple-l: 55%; + --red-bg-1: hsla( + var(--red-h), + var(--red-s), + var(--red-l), + var(--color-opacity-1) + ); + --red-bg-2: hsla( + var(--red-h), + var(--red-s), + var(--red-l), + var(--color-opacity-2) + ); + --orange-bg-1: hsla( + var(--orange-h), + var(--orange-s), + var(--orange-l), + var(--color-opacity-1) + ); + --orange-bg-2: hsla( + var(--orange-h), + var(--orange-s), + var(--orange-l), + var(--color-opacity-2) + ); + --yellow-bg-1: hsla( + var(--yellow-h), + var(--yellow-s), + var(--yellow-l), + var(--color-opacity-1) + ); + --yellow-bg-2: hsla( + var(--yellow-h), + var(--yellow-s), + var(--yellow-l), + var(--color-opacity-2) + ); + --green-bg-1: hsla( + var(--green-h), + var(--green-s), + var(--green-l), + var(--color-opacity-1) + ); + --green-bg-2: hsla( + var(--green-h), + var(--green-s), + var(--green-l), + var(--color-opacity-2) + ); + --blue-bg-1: hsla( + var(--blue-h), + var(--blue-s), + var(--blue-l), + var(--color-opacity-1) + ); + --blue-bg-2: hsla( + var(--blue-h), + var(--blue-s), + var(--blue-l), + var(--color-opacity-2) + ); + --purple-bg-1: hsla( + var(--purple-h), + var(--purple-s), + var(--purple-l), + var(--color-opacity-1) + ); + --purple-bg-2: hsla( + var(--purple-h), + var(--purple-s), + var(--purple-l), + var(--color-opacity-2) + ); + --red-fg-muted-1: hsla( + var(--red-h), + var(--red-s), + calc(var(--red-l) - var(--red-l) * 0.85), + var(--fg-muted-1-opacity) + ); + --red-fg-muted-2: hsla( + var(--red-h), + var(--red-s), + calc(var(--red-l) - var(--red-l) * 0.85), + var(--fg-muted-2-opacity) + ); + --orange-fg-muted-1: hsla( + var(--orange-h), + var(--orange-s), + calc(var(--orange-l) - var(--orange-l) * 0.85), + var(--fg-muted-1-opacity) + ); + --orange-fg-muted-2: hsla( + var(--orange-h), + var(--orange-s), + calc(var(--orange-l) - var(--orange-l) * 0.85), + var(--fg-muted-2-opacity) + ); + --yellow-fg-muted-1: hsla( + var(--yellow-h), + var(--yellow-s), + calc(var(--yellow-l) - var(--yellow-l) * 0.85), + var(--fg-muted-1-opacity) + ); + --yellow-fg-muted-2: hsla( + var(--yellow-h), + var(--yellow-s), + calc(var(--yellow-l) - var(--yellow-l) * 0.85), + var(--fg-muted-2-opacity) + ); + --green-fg-muted-1: hsla( + var(--green-h), + var(--green-s), + calc(var(--green-l) - var(--green-l) * 0.85), + var(--fg-muted-1-opacity) + ); + --green-fg-muted-2: hsla( + var(--green-h), + var(--green-s), + calc(var(--green-l) - var(--green-l) * 0.85), + var(--fg-muted-2-opacity) + ); + --blue-fg-muted-1: hsla( + var(--blue-h), + var(--blue-s), + calc(var(--blue-l) - var(--blue-l) * 0.85), + var(--fg-muted-1-opacity) + ); + --blue-fg-muted-2: hsla( + var(--blue-h), + var(--blue-s), + calc(var(--blue-l) - var(--blue-l) * 0.85), + var(--fg-muted-2-opacity) + ); + --purple-fg-muted-1: hsla( + var(--purple-h), + var(--purple-s), + calc(var(--purple-l) - var(--purple-l) * 0.85), + var(--fg-muted-1-opacity) + ); + --purple-fg-muted-2: hsla( + var(--purple-h), + var(--purple-s), + calc(var(--purple-l) - var(--purple-l) * 0.85), + var(--fg-muted-2-opacity) + ); + --shade-color: rgba(0, 0, 0, 0.07); - // COLORS (NEUTRAL) - --bg-muted-neutral-1: rgba(0, 0, 0, var(--color-opacity-1)); - --bg-muted-neutral-2: rgba(0, 0, 0, var(--color-opacity-2)); - --fg-muted-neutral-1: rgba(0, 0, 0, var(--fg-muted-1-opacity)); - --fg-muted-neutral-2: rgba(0, 0, 0, var(--fg-muted-2-opacity)); + // COLORS (NEUTRAL) + --bg-muted-neutral-1: rgba(0, 0, 0, var(--color-opacity-1)); + --bg-muted-neutral-2: rgba(0, 0, 0, var(--color-opacity-2)); + --fg-muted-neutral-1: rgba(0, 0, 0, var(--fg-muted-1-opacity)); + --fg-muted-neutral-2: rgba(0, 0, 0, var(--fg-muted-2-opacity)); - // OPACITY - --scanlines-opacity: 0.4; - --fg-muted-1-opacity: 0.5; - --fg-muted-2-opacity: 0.6; + // OPACITY + --scanlines-opacity: 0.4; + --fg-muted-1-opacity: 0.5; + --fg-muted-2-opacity: 0.6; - // SHADOWS - --edge-highlight: inset - 0 - #{rpx(-1)} - 0 - rgba(0, 0, 0, var(--color-opacity-1)); - } @else if $mode == "dark" { - // COLORS - --bg-overlay: linear-gradient( - rgba(0, 0, 0, var(--dim-opacity)), - rgba(0, 0, 0, var(--dim-opacity)) - ); - --fg-color: #fcfcfa; - --fg-contrast: #29242a; - --red-fg: hsl(345.18988, 100%, 69%); - --red-h: 345.18988; - --red-s: 100%; - --red-l: 69%; - --orange-fg: hsl(19.731544, 96%, 70%); - --orange-h: 19.731544; - --orange-s: 96%; - --orange-l: 70%; - --yellow-fg: hsl(44.70588, 100%, 70%); - --yellow-h: 44.70588; - --yellow-s: 100%; - --yellow-l: 70%; - --green-fg: hsl(90, 59%, 66%); - --green-h: 90; - --green-s: 59%; - --green-l: 66%; - --blue-fg: hsl(186.42857, 71%, 69%); - --blue-h: 186.42857; - --blue-s: 71%; - --blue-l: 69%; - --purple-fg: hsl(249.88234, 77%, 78%); - --purple-h: 249.88234; - --purple-s: 77%; - --purple-l: 78%; + // SHADOWS + --edge-highlight: inset + 0 + #{rpx(-1)} + 0 + rgba(0, 0, 0, var(--color-opacity-1)); + } @else if $mode == "dark" { + // COLORS + --bg-overlay: linear-gradient( + rgba(0, 0, 0, var(--dim-opacity)), + rgba(0, 0, 0, var(--dim-opacity)) + ); + --fg-color: #fcfcfa; + --fg-contrast: #29242a; + --red-fg: hsl(345.18988, 100%, 69%); + --red-h: 345.18988; + --red-s: 100%; + --red-l: 69%; + --orange-fg: hsl(19.731544, 96%, 70%); + --orange-h: 19.731544; + --orange-s: 96%; + --orange-l: 70%; + --yellow-fg: hsl(44.70588, 100%, 70%); + --yellow-h: 44.70588; + --yellow-s: 100%; + --yellow-l: 70%; + --green-fg: hsl(90, 59%, 66%); + --green-h: 90; + --green-s: 59%; + --green-l: 66%; + --blue-fg: hsl(186.42857, 71%, 69%); + --blue-h: 186.42857; + --blue-s: 71%; + --blue-l: 69%; + --purple-fg: hsl(249.88234, 77%, 78%); + --purple-h: 249.88234; + --purple-s: 77%; + --purple-l: 78%; - --red-fg-muted-1: hsla( - var(--red-h), - var(--red-s), - calc(var(--red-l) + (100% - var(--red-l)) * 0.85), - var(--fg-muted-1-opacity) - ); - --red-fg-muted-2: hsla( - var(--red-h), - var(--red-s), - calc(var(--red-l) + (100% - var(--red-l)) * 0.85), - var(--fg-muted-2-opacity) - ); - --orange-fg-muted-1: hsla( - var(--orange-h), - var(--orange-s), - calc(var(--orange-l) + (100% - var(--orange-l)) * 0.85), - var(--fg-muted-1-opacity) - ); - --orange-fg-muted-2: hsla( - var(--orange-h), - var(--orange-s), - calc(var(--orange-l) + (100% - var(--orange-l)) * 0.85), - var(--fg-muted-2-opacity) - ); - --yellow-fg-muted-1: hsla( - var(--yellow-h), - var(--yellow-s), - calc(var(--yellow-l) + (100% - var(--yellow-l)) * 0.85), - var(--fg-muted-1-opacity) - ); - --yellow-fg-muted-2: hsla( - var(--yellow-h), - var(--yellow-s), - calc(var(--yellow-l) + (100% - var(--yellow-l)) * 0.85), - var(--fg-muted-2-opacity) - ); - --green-fg-muted-1: hsla( - var(--green-h), - var(--green-s), - calc(var(--green-l) + (100% - var(--green-l)) * 0.85), - var(--fg-muted-1-opacity) - ); - --green-fg-muted-2: hsla( - var(--green-h), - var(--green-s), - calc(var(--green-l) + (100% - var(--green-l)) * 0.85), - var(--fg-muted-2-opacity) - ); - --blue-fg-muted-1: hsla( - var(--blue-h), - var(--blue-s), - calc(var(--blue-l) + (100% - var(--blue-l)) * 0.85), - var(--fg-muted-1-opacity) - ); - --blue-fg-muted-2: hsla( - var(--blue-h), - var(--blue-s), - calc(var(--blue-l) + (100% - var(--blue-l)) * 0.85), - var(--fg-muted-2-opacity) - ); - --purple-fg-muted-1: hsla( - var(--purple-h), - var(--purple-s), - calc(var(--purple-l) + (100% - var(--purple-l)) * 0.85), - var(--fg-muted-1-opacity) - ); - --purple-fg-muted-2: hsla( - var(--purple-h), - var(--purple-s), - calc(var(--purple-l) + (100% - var(--purple-l)) * 0.85), - var(--fg-muted-2-opacity) - ); - --shade-color: rgba(0, 0, 0, 0.25); + --red-fg-muted-1: hsla( + var(--red-h), + var(--red-s), + calc(var(--red-l) + (100% - var(--red-l)) * 0.85), + var(--fg-muted-1-opacity) + ); + --red-fg-muted-2: hsla( + var(--red-h), + var(--red-s), + calc(var(--red-l) + (100% - var(--red-l)) * 0.85), + var(--fg-muted-2-opacity) + ); + --orange-fg-muted-1: hsla( + var(--orange-h), + var(--orange-s), + calc(var(--orange-l) + (100% - var(--orange-l)) * 0.85), + var(--fg-muted-1-opacity) + ); + --orange-fg-muted-2: hsla( + var(--orange-h), + var(--orange-s), + calc(var(--orange-l) + (100% - var(--orange-l)) * 0.85), + var(--fg-muted-2-opacity) + ); + --yellow-fg-muted-1: hsla( + var(--yellow-h), + var(--yellow-s), + calc(var(--yellow-l) + (100% - var(--yellow-l)) * 0.85), + var(--fg-muted-1-opacity) + ); + --yellow-fg-muted-2: hsla( + var(--yellow-h), + var(--yellow-s), + calc(var(--yellow-l) + (100% - var(--yellow-l)) * 0.85), + var(--fg-muted-2-opacity) + ); + --green-fg-muted-1: hsla( + var(--green-h), + var(--green-s), + calc(var(--green-l) + (100% - var(--green-l)) * 0.85), + var(--fg-muted-1-opacity) + ); + --green-fg-muted-2: hsla( + var(--green-h), + var(--green-s), + calc(var(--green-l) + (100% - var(--green-l)) * 0.85), + var(--fg-muted-2-opacity) + ); + --blue-fg-muted-1: hsla( + var(--blue-h), + var(--blue-s), + calc(var(--blue-l) + (100% - var(--blue-l)) * 0.85), + var(--fg-muted-1-opacity) + ); + --blue-fg-muted-2: hsla( + var(--blue-h), + var(--blue-s), + calc(var(--blue-l) + (100% - var(--blue-l)) * 0.85), + var(--fg-muted-2-opacity) + ); + --purple-fg-muted-1: hsla( + var(--purple-h), + var(--purple-s), + calc(var(--purple-l) + (100% - var(--purple-l)) * 0.85), + var(--fg-muted-1-opacity) + ); + --purple-fg-muted-2: hsla( + var(--purple-h), + var(--purple-s), + calc(var(--purple-l) + (100% - var(--purple-l)) * 0.85), + var(--fg-muted-2-opacity) + ); + --shade-color: rgba(0, 0, 0, 0.25); - // COLORS (NEUTRAL) - --bg-muted-neutral-1: rgba(255, 255, 255, var(--color-opacity-1)); - --bg-muted-neutral-2: rgba(255, 255, 255, var(--color-opacity-2)); - --fg-muted-neutral-1: rgba(255, 255, 255, var(--fg-muted-1-opacity)); - --fg-muted-neutral-2: rgba(255, 255, 255, var(--fg-muted-2-opacity)); + // COLORS (NEUTRAL) + --bg-muted-neutral-1: rgba(255, 255, 255, var(--color-opacity-1)); + --bg-muted-neutral-2: rgba(255, 255, 255, var(--color-opacity-2)); + --fg-muted-neutral-1: rgba(255, 255, 255, var(--fg-muted-1-opacity)); + --fg-muted-neutral-2: rgba(255, 255, 255, var(--fg-muted-2-opacity)); - // OPACITY - --scanlines-opacity: 1; - --fg-muted-1-opacity: 0.6; - --fg-muted-2-opacity: 0.7; + // OPACITY + --scanlines-opacity: 1; + --fg-muted-1-opacity: 0.6; + --fg-muted-2-opacity: 0.7; - // SHADOWS - --edge-highlight: inset - 0 - #{rpx(1)} - 0 - rgba(255, 255, 255, var(--color-opacity-1)); - } @else if $mode == "common" { - // WIDTHS - --content-width: #{rpx(720)}; - --sidebar-width: clamp( - calc(var(--content-width) / 3), - 20vw, - calc(var(--content-width) / 2) - ); + // SHADOWS + --edge-highlight: inset + 0 + #{rpx(1)} + 0 + rgba(255, 255, 255, var(--color-opacity-1)); + } @else if $mode == "common" { + // WIDTHS + --content-width: #{rpx(720)}; + --sidebar-width: clamp( + calc(var(--content-width) / 3), + 20vw, + calc(var(--content-width) / 2) + ); - // CORNERS - --rounded-corner: 0.75rem; - --rounded-corner-small: 0.5rem; + // CORNERS + --rounded-corner: 0.75rem; + --rounded-corner-small: 0.5rem; - // FILTERS - --blur: blur(0.75rem) saturate(180%); - --printed: contrast(2) brightness(0.75) grayscale(1); - --drop-shadow: drop-shadow(0 0.08rem 0.1rem rgba(0, 0, 0, 0.35)) - drop-shadow(0 0.4rem 0.7rem rgba(0, 0, 0, 0.18)); + // FILTERS + --blur: blur(0.75rem) saturate(180%); + --printed: contrast(2) brightness(0.75) grayscale(1); + --drop-shadow: drop-shadow(0 0.08rem 0.1rem rgba(0, 0, 0, 0.35)) + drop-shadow(0 0.4rem 0.7rem rgba(0, 0, 0, 0.18)); - // FONTS - --font-system-ui: system-ui, sans-serif; - --font-ui-monospace: ui-monospace, monospace; - --font-emoji: - "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", - "Noto Color Emoji"; + // FONTS + --font-system-ui: system-ui, sans-serif; + --font-ui-monospace: ui-monospace, monospace; + --font-emoji: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", + "Noto Color Emoji"; - // FONT SIZES - --font-size-xx-small: 0.625rem; - --font-size-x-small: 0.75rem; - --font-size-small: 0.875rem; - --font-size-medium: 1rem; - --font-size-large: 1.25rem; - --font-size-x-large: 1.5rem; - --font-size-xx-large: 2rem; - --font-size-xxx-large: 3rem; + // FONT SIZES + --font-size-xx-small: 0.625rem; + --font-size-x-small: 0.75rem; + --font-size-small: 0.875rem; + --font-size-medium: 1rem; + --font-size-large: 1.25rem; + --font-size-x-large: 1.5rem; + --font-size-xx-large: 2rem; + --font-size-xxx-large: 3rem; - // FONT WEIGHTS - --font-weight-thin: 100; - --font-weight-extra-light: 200; - --font-weight-light: 300; - --font-weight-normal: 400; - --font-weight-medium: 500; - --font-weight-semi-bold: 600; - --font-weight-bold: 700; - --font-weight-extra-bold: 800; - --font-weight-black: 900; + // FONT WEIGHTS + --font-weight-thin: 100; + --font-weight-extra-light: 200; + --font-weight-light: 300; + --font-weight-normal: 400; + --font-weight-medium: 500; + --font-weight-semi-bold: 600; + --font-weight-bold: 700; + --font-weight-extra-bold: 800; + --font-weight-black: 900; - // OPACITY - --color-opacity-1: 0.1; - --color-opacity-2: 0.2; - --dim-opacity: 0.8; - --disabled-opacity: 0.4; + // OPACITY + --color-opacity-1: 0.1; + --color-opacity-2: 0.2; + --dim-opacity: 0.8; + --disabled-opacity: 0.4; - // SHADOWS - --shadow: - 0 0 0 #{rpx(1)} rgba(0, 0, 0, 0.03), - 0 #{rpx(1)} #{rpx(3)} #{rpx(1)} rgba(0, 0, 0, 0.07), - 0 #{rpx(2)} #{rpx(6)} #{rpx(2)} rgba(0, 0, 0, 0.03); - --shadow-inset: - inset 0 0 0 #{rpx(1)} rgba(0, 0, 0, 0.03), - inset 0 #{rpx(1)} #{rpx(3)} #{rpx(1)} rgba(0, 0, 0, 0.07), - inset 0 #{rpx(2)} #{rpx(6)} #{rpx(2)} rgba(0, 0, 0, 0.03); - --shadow-raised: - 0 0 0 #{rpx(1)} rgba(0, 0, 0, 0.06), - 0 #{rpx(2)} #{rpx(6)} #{rpx(2)} rgba(0, 0, 0, 0.14), - 0 #{rpx(4)} #{rpx(12)} #{rpx(4)} rgba(0, 0, 0, 0.06); - --shadow-glass: 0 #{rpx(6)} #{rpx(12)} #{rpx(-6)} rgba(0, 0, 0, 0.1); + // SHADOWS + --shadow: 0 0 0 #{rpx(1)} rgba(0, 0, 0, 0.03), + 0 #{rpx(1)} #{rpx(3)} #{rpx(1)} rgba(0, 0, 0, 0.07), + 0 #{rpx(2)} #{rpx(6)} #{rpx(2)} rgba(0, 0, 0, 0.03); + --shadow-inset: inset 0 0 0 #{rpx(1)} rgba(0, 0, 0, 0.03), + inset 0 #{rpx(1)} #{rpx(3)} #{rpx(1)} rgba(0, 0, 0, 0.07), + inset 0 #{rpx(2)} #{rpx(6)} #{rpx(2)} rgba(0, 0, 0, 0.03); + --shadow-raised: 0 0 0 #{rpx(1)} rgba(0, 0, 0, 0.06), + 0 #{rpx(2)} #{rpx(6)} #{rpx(2)} rgba(0, 0, 0, 0.14), + 0 #{rpx(4)} #{rpx(12)} #{rpx(4)} rgba(0, 0, 0, 0.06); + --shadow-glass: 0 #{rpx(6)} #{rpx(12)} #{rpx(-6)} rgba(0, 0, 0, 0.1); - // STATES - --hover: scale(1.1); - --active: scale(0.9); + // STATES + --hover: scale(1.1); + --active: scale(0.9); - // TRANSITIONS - --transition: 0.2s; - --transition-longer: 0.4s; - --transition-long: 0.8s; - --transition-bezier: 0.35s cubic-bezier(0.17, 0.89, 0.32, 1.28); - --transition-bezier-longer: 0.525s cubic-bezier(0.17, 0.89, 0.32, 1.28); + // TRANSITIONS + --transition: 0.2s; + --transition-longer: 0.4s; + --transition-long: 0.8s; + --transition-bezier: 0.35s cubic-bezier(0.17, 0.89, 0.32, 1.28); + --transition-bezier-longer: 0.525s cubic-bezier(0.17, 0.89, 0.32, 1.28); - // ICONS - --icon-arrow-square-out: url(data:image/svg+xml,%3Csvg%20role%3D%22img%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22currentColor%22%20viewBox%3D%220%200%20256%20256%22%3E%3Cpath%20d%3D%22M228%2C104a12%2C12%2C0%2C0%2C1-24%2C0V69l-59.51%2C59.51a12%2C12%2C0%2C0%2C1-17-17L187%2C52H152a12%2C12%2C0%2C0%2C1%2C0-24h64a12%2C12%2C0%2C0%2C1%2C12%2C12Zm-44%2C24a12%2C12%2C0%2C0%2C0-12%2C12v64H52V84h64a12%2C12%2C0%2C0%2C0%2C0-24H48A20%2C20%2C0%2C0%2C0%2C28%2C80V208a20%2C20%2C0%2C0%2C0%2C20%2C20H176a20%2C20%2C0%2C0%2C0%2C20-20V140A12%2C12%2C0%2C0%2C0%2C184%2C128Z%22/%3E%3C/svg%3E); - --icon-caret-down: url(data:image/svg+xml,%3Csvg%20role%3D%22img%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22currentColor%22%20viewBox%3D%220%200%20256%20256%22%3E%3Cpath%20d%3D%22M216.49%2C104.49l-80%2C80a12%2C12%2C0%2C0%2C1-17%2C0l-80-80a12%2C12%2C0%2C0%2C1%2C17-17L128%2C159l71.51-71.52a12%2C12%2C0%2C0%2C1%2C17%2C17Z%22/%3E%3C/svg%3E); - --icon-check: url(data:image/svg+xml,%3Csvg%20role%3D%22img%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22currentColor%22%20viewBox%3D%220%200%20256%20256%22%3E%3Cpath%20d%3D%22M232.49%2C80.49l-128%2C128a12%2C12%2C0%2C0%2C1-17%2C0l-56-56a12%2C12%2C0%2C1%2C1%2C17-17L96%2C183%2C215.51%2C63.51a12%2C12%2C0%2C0%2C1%2C17%2C17Z%22/%3E%3C/svg%3E); - } - } + // ICONS + --icon-arrow-square-out: url(data:image/svg+xml,%3Csvg%20role%3D%22img%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22currentColor%22%20viewBox%3D%220%200%20256%20256%22%3E%3Cpath%20d%3D%22M228%2C104a12%2C12%2C0%2C0%2C1-24%2C0V69l-59.51%2C59.51a12%2C12%2C0%2C0%2C1-17-17L187%2C52H152a12%2C12%2C0%2C0%2C1%2C0-24h64a12%2C12%2C0%2C0%2C1%2C12%2C12Zm-44%2C24a12%2C12%2C0%2C0%2C0-12%2C12v64H52V84h64a12%2C12%2C0%2C0%2C0%2C0-24H48A20%2C20%2C0%2C0%2C0%2C28%2C80V208a20%2C20%2C0%2C0%2C0%2C20%2C20H176a20%2C20%2C0%2C0%2C0%2C20-20V140A12%2C12%2C0%2C0%2C0%2C184%2C128Z%22/%3E%3C/svg%3E); + --icon-caret-down: url(data:image/svg+xml,%3Csvg%20role%3D%22img%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22currentColor%22%20viewBox%3D%220%200%20256%20256%22%3E%3Cpath%20d%3D%22M216.49%2C104.49l-80%2C80a12%2C12%2C0%2C0%2C1-17%2C0l-80-80a12%2C12%2C0%2C0%2C1%2C17-17L128%2C159l71.51-71.52a12%2C12%2C0%2C0%2C1%2C17%2C17Z%22/%3E%3C/svg%3E); + --icon-check: url(data:image/svg+xml,%3Csvg%20role%3D%22img%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2232%22%20height%3D%2232%22%20fill%3D%22currentColor%22%20viewBox%3D%220%200%20256%20256%22%3E%3Cpath%20d%3D%22M232.49%2C80.49l-128%2C128a12%2C12%2C0%2C0%2C1-17%2C0l-56-56a12%2C12%2C0%2C1%2C1%2C17-17L96%2C183%2C215.51%2C63.51a12%2C12%2C0%2C0%2C1%2C17%2C17Z%22/%3E%3C/svg%3E); + } + } } @mixin variables-accent($modes: "light") { - @each $mode in $modes { - @if $mode == "light" { - --accent-color: hsl( - var(--accent-light-h), - var(--accent-light-s), - var(--accent-light-l) - ); - --accent-h: var(--accent-light-h); - --accent-s: var(--accent-light-s); - --accent-l: var(--accent-light-l); + @each $mode in $modes { + @if $mode == "light" { + --accent-color: hsl( + var(--accent-light-h), + var(--accent-light-s), + var(--accent-light-l) + ); + --accent-h: var(--accent-light-h); + --accent-s: var(--accent-light-s); + --accent-l: var(--accent-light-l); - --bg-color: hsl( - var(--accent-h), - calc(var(--accent-s) - var(--accent-s) * 0.6), - calc(var(--accent-l) + (100% - var(--accent-l)) * 0.8) - ); - --fg-muted-1: hsla( - var(--accent-h), - var(--accent-s), - calc(var(--accent-l) - var(--accent-l) * 0.85), - var(--fg-muted-1-opacity) - ); - --fg-muted-2: hsla( - var(--accent-h), - var(--accent-s), - calc(var(--accent-l) - var(--accent-l) * 0.85), - var(--fg-muted-2-opacity) - ); - --glass-bg-1: hsla( - var(--accent-h), - calc(var(--accent-s) - var(--accent-s) * 0.7), - calc(var(--accent-l) + (100% - var(--accent-l)) * 0.9), - var(--dim-opacity) - ); - --glass-bg-2: hsla( - var(--accent-h), - calc(var(--accent-s) - var(--accent-s) * 0.6), - calc(var(--accent-l) + (100% - var(--accent-l)) * 0.8), - var(--dim-opacity) - ); - --crt-bg: radial-gradient( - hsl( - var(--accent-h), - calc(var(--accent-s) - var(--accent-s) * 0.5), - calc(var(--accent-l) + (100% - var(--accent-l)) * 0.9) - ), - hsl( - var(--accent-h), - calc(var(--accent-s) - var(--accent-s) * 0.5), - calc(var(--accent-l) + (100% - var(--accent-l)) * 0.75) - ) - 80%, - hsl( - var(--accent-h), - calc(var(--accent-s) - var(--accent-s) * 0.5), - calc(var(--accent-l) + (100% - var(--accent-l)) * 0.65) - ) - ); - } @else if $mode == "dark" { - --accent-color: hsl( - var(--accent-dark-h), - var(--accent-dark-s), - var(--accent-dark-l) - ); - --accent-h: var(--accent-dark-h); - --accent-s: var(--accent-dark-s); - --accent-l: var(--accent-dark-l); + --bg-color: hsl( + var(--accent-h), + calc(var(--accent-s) - var(--accent-s) * 0.6), + calc(var(--accent-l) + (100% - var(--accent-l)) * 0.8) + ); + --fg-muted-1: hsla( + var(--accent-h), + var(--accent-s), + calc(var(--accent-l) - var(--accent-l) * 0.85), + var(--fg-muted-1-opacity) + ); + --fg-muted-2: hsla( + var(--accent-h), + var(--accent-s), + calc(var(--accent-l) - var(--accent-l) * 0.85), + var(--fg-muted-2-opacity) + ); + --glass-bg-1: hsla( + var(--accent-h), + calc(var(--accent-s) - var(--accent-s) * 0.7), + calc(var(--accent-l) + (100% - var(--accent-l)) * 0.9), + var(--dim-opacity) + ); + --glass-bg-2: hsla( + var(--accent-h), + calc(var(--accent-s) - var(--accent-s) * 0.6), + calc(var(--accent-l) + (100% - var(--accent-l)) * 0.8), + var(--dim-opacity) + ); + --crt-bg: radial-gradient( + hsl( + var(--accent-h), + calc(var(--accent-s) - var(--accent-s) * 0.5), + calc(var(--accent-l) + (100% - var(--accent-l)) * 0.9) + ), + hsl( + var(--accent-h), + calc(var(--accent-s) - var(--accent-s) * 0.5), + calc(var(--accent-l) + (100% - var(--accent-l)) * 0.75) + ) + 80%, + hsl( + var(--accent-h), + calc(var(--accent-s) - var(--accent-s) * 0.5), + calc(var(--accent-l) + (100% - var(--accent-l)) * 0.65) + ) + ); + } @else if $mode == "dark" { + --accent-color: hsl( + var(--accent-dark-h), + var(--accent-dark-s), + var(--accent-dark-l) + ); + --accent-h: var(--accent-dark-h); + --accent-s: var(--accent-dark-s); + --accent-l: var(--accent-dark-l); - --bg-color: hsl( - var(--accent-h), - calc(var(--accent-s) - var(--accent-s) * 0.7), - calc(var(--accent-l) - var(--accent-l) * 0.9) - ); - --fg-muted-1: hsla( - var(--accent-h), - var(--accent-s), - calc(var(--accent-l) + (100% - var(--accent-l)) * 0.85), - var(--fg-muted-1-opacity) - ); - --fg-muted-2: hsla( - var(--accent-h), - var(--accent-s), - calc(var(--accent-l) + (100% - var(--accent-l)) * 0.85), - var(--fg-muted-2-opacity) - ); - --glass-bg-1: hsla( - var(--accent-h), - calc(var(--accent-s) - var(--accent-s) * 0.6), - calc(var(--accent-l) - var(--accent-l) * 0.8), - var(--dim-opacity) - ); - --glass-bg-2: hsla( - var(--accent-h), - calc(var(--accent-s) - var(--accent-s) * 0.5), - calc(var(--accent-l) - var(--accent-l) * 0.7), - var(--dim-opacity) - ); - --crt-bg: radial-gradient( - hsl( - var(--accent-h), - calc(var(--accent-s) - var(--accent-s) * 0.5), - calc(var(--accent-l) - var(--accent-l) * 0.7) - ), - hsl( - var(--accent-h), - calc(var(--accent-s) - var(--accent-s) * 0.5), - calc(var(--accent-l) - var(--accent-l) * 0.9) - ) - 80%, - hsl( - var(--accent-h), - calc(var(--accent-s) - var(--accent-s) * 0.5), - calc(var(--accent-l) - var(--accent-l) * 0.95) - ) - ); - } @else if $mode == "common" { - // COLORS - --accent-highlight: hsla( - var(--accent-h), - var(--accent-s), - var(--accent-l), - var(--disabled-opacity) - ); + --bg-color: hsl( + var(--accent-h), + calc(var(--accent-s) - var(--accent-s) * 0.7), + calc(var(--accent-l) - var(--accent-l) * 0.9) + ); + --fg-muted-1: hsla( + var(--accent-h), + var(--accent-s), + calc(var(--accent-l) + (100% - var(--accent-l)) * 0.85), + var(--fg-muted-1-opacity) + ); + --fg-muted-2: hsla( + var(--accent-h), + var(--accent-s), + calc(var(--accent-l) + (100% - var(--accent-l)) * 0.85), + var(--fg-muted-2-opacity) + ); + --glass-bg-1: hsla( + var(--accent-h), + calc(var(--accent-s) - var(--accent-s) * 0.6), + calc(var(--accent-l) - var(--accent-l) * 0.8), + var(--dim-opacity) + ); + --glass-bg-2: hsla( + var(--accent-h), + calc(var(--accent-s) - var(--accent-s) * 0.5), + calc(var(--accent-l) - var(--accent-l) * 0.7), + var(--dim-opacity) + ); + --crt-bg: radial-gradient( + hsl( + var(--accent-h), + calc(var(--accent-s) - var(--accent-s) * 0.5), + calc(var(--accent-l) - var(--accent-l) * 0.7) + ), + hsl( + var(--accent-h), + calc(var(--accent-s) - var(--accent-s) * 0.5), + calc(var(--accent-l) - var(--accent-l) * 0.9) + ) + 80%, + hsl( + var(--accent-h), + calc(var(--accent-s) - var(--accent-s) * 0.5), + calc(var(--accent-l) - var(--accent-l) * 0.95) + ) + ); + } @else if $mode == "common" { + // COLORS + --accent-highlight: hsla( + var(--accent-h), + var(--accent-s), + var(--accent-l), + var(--disabled-opacity) + ); - // TODO: put this all in sub-mixins - --bg-muted-1: hsla( - var(--accent-h), - var(--accent-s), - var(--accent-l), - var(--color-opacity-1) - ); + // TODO: put this all in sub-mixins + --bg-muted-1: hsla( + var(--accent-h), + var(--accent-s), + var(--accent-l), + var(--color-opacity-1) + ); - --bg-muted-2: hsla( - var(--accent-h), - var(--accent-s), - var(--accent-l), - var(--color-opacity-2) - ); + --bg-muted-2: hsla( + var(--accent-h), + var(--accent-s), + var(--accent-l), + var(--color-opacity-2) + ); - // CONTRAST COLOR - --l: clamp(0, (l / 0.623 - 1) * -infinity, 1); - --a: calc(var(--l) + (var(--dim-opacity) * (1 - var(--l)))); - --contrast-color: oklch(from var(--accent-color) var(--l) 0 h / var(--a)); + // CONTRAST COLOR + --l: clamp(0, (l / 0.623 - 1) * -infinity, 1); + --a: calc(var(--l) + (var(--dim-opacity) * (1 - var(--l)))); + --contrast-color: oklch(from var(--accent-color) var(--l) 0 h / var(--a)); - // SHADOWS - --shadow-glow: - 0 0 0 #{rpx(1)} var(--bg-muted-2), - 0 #{rpx(2)} #{rpx(6)} #{rpx(2)} var(--bg-muted-1), - 0 #{rpx(4)} #{rpx(24)} #{rpx(4)} var(--bg-muted-2); - --shadow-glow-inset: - inset 0 0 0 #{rpx(1)} var(--bg-muted-2), - inset 0 #{rpx(2)} #{rpx(6)} #{rpx(2)} var(--bg-muted-1), - inset 0 #{rpx(4)} #{rpx(24)} #{rpx(4)} var(--bg-muted-2); - --text-shadow-glow: - var(--bg-muted-2) 0 0 #{rpx(4)}, var(--accent-color) 0 0 #{rpx(12)}; - } - } + // SHADOWS + --shadow-glow: 0 0 0 #{rpx(1)} var(--bg-muted-2), + 0 #{rpx(2)} #{rpx(6)} #{rpx(2)} var(--bg-muted-1), + 0 #{rpx(4)} #{rpx(24)} #{rpx(4)} var(--bg-muted-2); + --shadow-glow-inset: inset 0 0 0 #{rpx(1)} var(--bg-muted-2), + inset 0 #{rpx(2)} #{rpx(6)} #{rpx(2)} var(--bg-muted-1), + inset 0 #{rpx(4)} #{rpx(24)} #{rpx(4)} var(--bg-muted-2); + --text-shadow-glow: var(--bg-muted-2) 0 0 #{rpx(4)}, var( + --accent-color + ) 0 0 #{rpx(12)}; + } + } } diff --git a/modules/amezing/sass/citrine/base/_animations.scss b/modules/amezing/sass/citrine/base/_animations.scss index 1c61ed2..18cd3a1 100644 --- a/modules/amezing/sass/citrine/base/_animations.scss +++ b/modules/amezing/sass/citrine/base/_animations.scss @@ -1,83 +1,83 @@ @keyframes slide-in-up { - from { - transform: translateY(-1rem); - opacity: 0; - } + from { + transform: translateY(-1rem); + opacity: 0; + } } @keyframes slide-in-left { - from { - transform: translateX(1rem); - opacity: 0; - } + from { + transform: translateX(1rem); + opacity: 0; + } } @keyframes slide-in-right { - from { - transform: translateX(-1rem); - opacity: 0; - } + from { + transform: translateX(-1rem); + opacity: 0; + } } @keyframes slide-in-up-longer { - from { - transform: translateY(-2rem); - opacity: 0; - } + from { + transform: translateY(-2rem); + opacity: 0; + } } @keyframes slide-in-down-longer { - from { - transform: translateY(2rem); - opacity: 0; - } + from { + transform: translateY(2rem); + opacity: 0; + } } @keyframes slide-in-left-longer { - from { - transform: translateX(-2rem); - opacity: 0; - } + from { + transform: translateX(-2rem); + opacity: 0; + } } @keyframes slide-in-right-longer { - from { - transform: translateX(2rem); - opacity: 0; - } + from { + transform: translateX(2rem); + opacity: 0; + } } @keyframes slide-out-down-longer { - to { - transform: translateY(-2rem); - opacity: 0; - } + to { + transform: translateY(-2rem); + opacity: 0; + } } @keyframes expand-up-left { - from { - transform: scale(0.5) translate(-1rem, -1rem); - opacity: 0; - } + from { + transform: scale(0.5) translate(-1rem, -1rem); + opacity: 0; + } } @keyframes expand-up-right { - from { - transform: scale(0.5) translate(1rem, -1rem); - opacity: 0; - } + from { + transform: scale(0.5) translate(1rem, -1rem); + opacity: 0; + } } @keyframes expand-down-left { - from { - transform: scale(0.5) translate(-1rem, 1rem); - opacity: 0; - } + from { + transform: scale(0.5) translate(-1rem, 1rem); + opacity: 0; + } } @keyframes expand-down-right { - from { - transform: scale(0.5) translate(1rem, 1rem); - opacity: 0; - } + from { + transform: scale(0.5) translate(1rem, 1rem); + opacity: 0; + } } diff --git a/modules/amezing/sass/citrine/base/_base.scss b/modules/amezing/sass/citrine/base/_base.scss index 01ea5eb..9908737 100644 --- a/modules/amezing/sass/citrine/base/_base.scss +++ b/modules/amezing/sass/citrine/base/_base.scss @@ -2,58 +2,58 @@ // Make focused anchor not get covered by header :target:not(#main-content) { - scroll-margin-block-start: 6.5rem; + scroll-margin-block-start: 6.5rem; } :focus-visible, :target:not(#main-content) { - animation: focus-in var(--transition); - outline: 0.125rem solid var(--accent-color); - outline-offset: 0.125rem; + animation: focus-in var(--transition); + outline: 0.125rem solid var(--accent-color); + outline-offset: 0.125rem; - @keyframes focus-in { - from { - outline: 0.5rem solid transparent; - outline-offset: 0.25rem; - } - } + @keyframes focus-in { + from { + outline: 0.5rem solid transparent; + outline-offset: 0.25rem; + } + } } // Style text selection to use accent color ::selection { - background-color: var(--accent-color); - color: var(--contrast-color); + background-color: var(--accent-color); + color: var(--contrast-color); } body { - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - background-color: var(--bg-color); - color: var(--fg-color); - font-family: var(--font-sans-serif), var(--font-system-ui), var(--font-emoji); - overflow-wrap: break-word; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background-color: var(--bg-color); + color: var(--fg-color); + font-family: var(--font-sans-serif), var(--font-system-ui), var(--font-emoji); + overflow-wrap: break-word; - @supports (min-height: 100dvh) { - min-height: 100dvh; - } + @supports (min-height: 100dvh) { + min-height: 100dvh; + } } @media (prefers-reduced-motion) { - *, - *::before, - *::after { - animation-duration: 0s !important; - transition-duration: 0s !important; - } + *, + *::before, + *::after { + animation-duration: 0s !important; + transition-duration: 0s !important; + } } // Disable blur on Firefox Mobile @-moz-document url-prefix() { - @media only screen and (max-width: 960px) { - .firefox-unblur, - .firefox-unblur::before, - .firefox-unblur::after { - backdrop-filter: none !important; - } - } + @media only screen and (max-width: 960px) { + .firefox-unblur, + .firefox-unblur::before, + .firefox-unblur::after { + backdrop-filter: none !important; + } + } } diff --git a/modules/amezing/sass/citrine/base/_layout.scss b/modules/amezing/sass/citrine/base/_layout.scss index ffbe1e5..01cd500 100644 --- a/modules/amezing/sass/citrine/base/_layout.scss +++ b/modules/amezing/sass/citrine/base/_layout.scss @@ -3,34 +3,34 @@ body > main, body > nav, body > footer { - --padding: 1rem; - margin-inline: auto; - padding-inline: 1rem; - max-width: calc(var(--content-width) + var(--padding) * 2); + --padding: 1rem; + margin-inline: auto; + padding-inline: 1rem; + max-width: calc(var(--content-width) + var(--padding) * 2); } body > footer { - margin-block-start: 4rem; + margin-block-start: 4rem; } body > nav { - margin-block-end: 4rem; - border-block-end: rpx(1) solid var(--bg-muted-2); + margin-block-end: 4rem; + border-block-end: rpx(1) solid var(--bg-muted-2); - ul { - display: flex; - gap: 0.25rem; - margin-block: 0.625rem; - padding-inline-start: 0; - list-style: none; + ul { + display: flex; + gap: 0.25rem; + margin-block: 0.625rem; + padding-inline-start: 0; + list-style: none; - @media only screen and (max-width: 480px) { - flex-direction: column; - } - } + @media only screen and (max-width: 480px) { + flex-direction: column; + } + } - li { - margin-block: 0; - padding-inline-start: 0; - } + li { + margin-block: 0; + padding-inline-start: 0; + } } diff --git a/modules/amezing/sass/citrine/base/_reset.scss b/modules/amezing/sass/citrine/base/_reset.scss index 2365866..68c8c87 100644 --- a/modules/amezing/sass/citrine/base/_reset.scss +++ b/modules/amezing/sass/citrine/base/_reset.scss @@ -4,28 +4,28 @@ *, :after, :before { - vertical-align: baseline; - box-sizing: border-box; - margin: 0; - border: none; - background: none; - padding: 0; - color: inherit; - font: inherit; - -webkit-tap-highlight-color: transparent; + vertical-align: baseline; + box-sizing: border-box; + margin: 0; + border: none; + background: none; + padding: 0; + color: inherit; + font: inherit; + -webkit-tap-highlight-color: transparent; } button, select { - appearance: none; + appearance: none; } body { - -webkit-text-size-adjust: none; - text-size-adjust: none; - width: 100%; - min-height: 100vh; - line-height: 1.5; + -webkit-text-size-adjust: none; + text-size-adjust: none; + width: 100%; + min-height: 100vh; + line-height: 1.5; } a, @@ -42,7 +42,7 @@ input[type="reset"], input[type="submit"], label, select { - cursor: pointer; + cursor: pointer; } article, @@ -60,7 +60,7 @@ rt, ruby, section, summary { - display: block; + display: block; } bdi, @@ -69,14 +69,14 @@ meter, output, progress, time { - display: inline-block; + display: inline-block; } [hidden], datalist, dialog:not([open]), rp { - display: none !important; + display: none !important; } embed, @@ -86,50 +86,50 @@ object, picture, svg, video { - display: block; - max-width: 100%; - height: auto; + display: block; + max-width: 100%; + height: auto; } blockquote, q { - quotes: none; + quotes: none; } blockquote:after, blockquote:before, q:after, q:before { - content: none; + content: none; } strong { - font-weight: 700; + font-weight: 700; } em { - font-style: italic; + font-style: italic; } code, kbd, pre, samp { - font-family: monospace; + font-family: monospace; } sub { - vertical-align: sub; + vertical-align: sub; } sup { - vertical-align: super; + vertical-align: super; } small { - font-size: small; + font-size: small; } table { - border-collapse: collapse; + border-collapse: collapse; } diff --git a/modules/amezing/sass/citrine/base/_root.scss b/modules/amezing/sass/citrine/base/_root.scss index e245c1d..5cb1ff3 100644 --- a/modules/amezing/sass/citrine/base/_root.scss +++ b/modules/amezing/sass/citrine/base/_root.scss @@ -1,20 +1,20 @@ @use "../abstracts/variables" as *; :root { - @include variables(("light", "common")); - @include variables-accent(("light", "common")); - scrollbar-color: var(--accent-color) transparent; - accent-color: var(--accent-color); + @include variables(("light", "common")); + @include variables-accent(("light", "common")); + scrollbar-color: var(--accent-color) transparent; + accent-color: var(--accent-color); - @media (prefers-reduced-motion: no-preference) { - scroll-behavior: smooth; - } + @media (prefers-reduced-motion: no-preference) { + scroll-behavior: smooth; + } - @media (prefers-color-scheme: dark) { - & { - @include variables("dark"); - @include variables-accent(("dark")); - color-scheme: dark; - } - } + @media (prefers-color-scheme: dark) { + & { + @include variables("dark"); + @include variables-accent(("dark")); + color-scheme: dark; + } + } } diff --git a/modules/amezing/sass/citrine/base/_typography.scss b/modules/amezing/sass/citrine/base/_typography.scss index 0415adb..d32dc5b 100644 --- a/modules/amezing/sass/citrine/base/_typography.scss +++ b/modules/amezing/sass/citrine/base/_typography.scss @@ -7,25 +7,25 @@ ol, dl, blockquote, pre { - margin-block: 1rem; + margin-block: 1rem; } ul ul, ul ol, ol ul, ol ol { - margin-block: 0; + margin-block: 0; } p { - text-wrap: pretty; - overflow-wrap: break-word; + text-wrap: pretty; + overflow-wrap: break-word; } :disabled, [aria-disabled="true"] { - opacity: var(--disabled-opacity); - cursor: not-allowed; + opacity: var(--disabled-opacity); + cursor: not-allowed; } h1, @@ -34,367 +34,363 @@ h3, h4, h5, h6 { - transition-duration: var(--transition); - transition-property: text-shadow, font-weight; - margin-block: 2rem 1rem; - color: var(--accent-color); - font-weight: var(--font-weight-bold); - line-height: 1; - font-variation-settings: "FLAR" 100; - letter-spacing: -0.05em; - text-decoration: underline; - text-decoration-color: var(--bg-muted-1); - text-decoration-thickness: 0.125em; - text-wrap: balance; - overflow-wrap: break-word; + transition-duration: var(--transition); + transition-property: text-shadow, font-weight; + margin-block: 2rem 1rem; + color: var(--accent-color); + font-weight: var(--font-weight-bold); + line-height: 1; + font-variation-settings: "FLAR" 100; + letter-spacing: -0.05em; + text-decoration: underline; + text-decoration-color: var(--bg-muted-1); + text-decoration-thickness: 0.125em; + text-wrap: balance; + overflow-wrap: break-word; - &:target { - animation: none !important; - outline: none !important; - font-weight: var(--font-weight-light); - text-shadow: var(--text-shadow-glow); - } + &:target { + animation: none !important; + outline: none !important; + font-weight: var(--font-weight-light); + text-shadow: var(--text-shadow-glow); + } } h1 { - font-size: var(--font-size-xxx-large); + font-size: var(--font-size-xxx-large); } h2 { - font-size: var(--font-size-xx-large); + font-size: var(--font-size-xx-large); } h3 { - font-size: var(--font-size-x-large); + font-size: var(--font-size-x-large); } h4 { - font-size: var(--font-size-large); + font-size: var(--font-size-large); } h5 { - font-size: var(--font-size-medium); + font-size: var(--font-size-medium); } h6 { - font-size: var(--font-size-small); + font-size: var(--font-size-small); } @media only screen and (max-width: 480px) { - h1 { - font-size: var(--font-size-xx-large); - } + h1 { + font-size: var(--font-size-xx-large); + } - h2 { - font-size: var(--font-size-x-large); - } + h2 { + font-size: var(--font-size-x-large); + } - h3 { - font-size: var(--font-size-large); - } + h3 { + font-size: var(--font-size-large); + } - h4 { - font-size: var(--font-size-medium); - } + h4 { + font-size: var(--font-size-medium); + } - h5 { - font-size: var(--font-size-small); - } + h5 { + font-size: var(--font-size-small); + } - h6 { - font-size: var(--font-size-x-small); - } + h6 { + font-size: var(--font-size-x-small); + } } small { - color: var(--fg-muted-2); - font-size: var(--font-size-small); + color: var(--fg-muted-2); + font-size: var(--font-size-small); } abbr[title] { - cursor: help; - text-decoration: underline; - text-decoration-style: dotted; - text-decoration-thickness: rpx(1); + cursor: help; + text-decoration: underline; + text-decoration-style: dotted; + text-decoration-thickness: rpx(1); } figcaption { - color: var(--fg-muted-2); - font-size: var(--font-size-small); - text-align: center; + color: var(--fg-muted-2); + font-size: var(--font-size-small); + text-align: center; } blockquote { - margin: 1rem 0; - border-inline-start: 0.25rem solid var(--accent-color); - border-start-start-radius: 0.25rem; - border-end-start-radius: 0.25rem; - padding-inline-start: 0.75rem; - color: var(--fg-muted-2); + margin: 1rem 0; + border-inline-start: 0.25rem solid var(--accent-color); + border-start-start-radius: 0.25rem; + border-end-start-radius: 0.25rem; + padding-inline-start: 0.75rem; + color: var(--fg-muted-2); } pre, code, kbd, samp { - font-family: var(--font-monospace), var(--font-ui-monospace); + font-family: var(--font-monospace), var(--font-ui-monospace); } pre { - margin: 1rem 0; - box-shadow: var(--edge-highlight), var(--shadow); - border-radius: var(--rounded-corner); - background-color: var(--bg-muted-1); - padding: 0.75rem 1rem; - overflow-x: auto; - scrollbar-width: thin; - line-height: 1.25; + margin: 1rem 0; + box-shadow: var(--edge-highlight), var(--shadow); + border-radius: var(--rounded-corner); + background-color: var(--bg-muted-1); + padding: 0.75rem 1rem; + overflow-x: auto; + scrollbar-width: thin; + line-height: 1.25; } code:not(pre code), mark, samp, q { - @include chip; + @include chip; } code:not(pre code) { - background-color: var(--red-bg-1); - padding-block: rpx(3); - color: var(--red-fg); - font-size: var(--font-size-small); + background-color: var(--red-bg-1); + padding-block: rpx(3); + color: var(--red-fg); + font-size: var(--font-size-small); } mark { - color: var(--accent-color); + color: var(--accent-color); } del { - color: var(--red-fg); - text-decoration: line-through; - text-decoration-thickness: rpx(1); + color: var(--red-fg); + text-decoration: line-through; + text-decoration-thickness: rpx(1); } ins { - color: var(--green-fg); - text-decoration: underline; - text-decoration-thickness: rpx(1); + color: var(--green-fg); + text-decoration: underline; + text-decoration-thickness: rpx(1); } samp { - color: var(--fg-muted-2); - font-size: var(--font-size-small); + color: var(--fg-muted-2); + font-size: var(--font-size-small); } q { - color: var(--fg-muted-2); - font-style: italic; + color: var(--fg-muted-2); + font-style: italic; } u { - text-decoration: underline; - text-decoration-style: wavy; - text-decoration-color: var(--red-fg); - text-decoration-thickness: rpx(1); + text-decoration: underline; + text-decoration-style: wavy; + text-decoration-color: var(--red-fg); + text-decoration-thickness: rpx(1); } progress { - appearance: none; - box-shadow: var(--edge-highlight); - border: none; - border-radius: 9999px; - background-color: var(--bg-muted-1); - padding: 0.25rem; - width: 100%; - height: 1rem; - color: var(--accent-color); + appearance: none; + box-shadow: var(--edge-highlight); + border: none; + border-radius: 9999px; + background-color: var(--bg-muted-1); + padding: 0.25rem; + width: 100%; + height: 1rem; + color: var(--accent-color); - &:indeterminate { - background-color: var(--bg-muted-1); - } + &:indeterminate { + background-color: var(--bg-muted-1); + } - &:indeterminate::-moz-progress-bar { - background-color: transparent; - } + &:indeterminate::-moz-progress-bar { + background-color: transparent; + } - &::-webkit-progress-bar { - background-color: transparent; - } + &::-webkit-progress-bar { + background-color: transparent; + } - &::-moz-progress-bar { - border-radius: 9999px; - background-color: var(--accent-color); - } + &::-moz-progress-bar { + border-radius: 9999px; + background-color: var(--accent-color); + } - &::-webkit-progress-value { - border-radius: 9999px; - background-color: var(--accent-color); - } + &::-webkit-progress-value { + border-radius: 9999px; + background-color: var(--accent-color); + } } kbd { - display: inline-block; - transition-duration: var(--transition); - transition-property: transform, box-shadow, background-color; - cursor: pointer; - box-shadow: - var(--edge-highlight), - inset 0 -0.125rem 0 var(--bg-muted-2); - border-radius: var(--rounded-corner-small); - background-color: var(--bg-muted-1); - padding: 0 0.375rem; - font-size: var(--font-size-small); + display: inline-block; + transition-duration: var(--transition); + transition-property: transform, box-shadow, background-color; + cursor: pointer; + box-shadow: var(--edge-highlight), inset 0 -0.125rem 0 var(--bg-muted-2); + border-radius: var(--rounded-corner-small); + background-color: var(--bg-muted-1); + padding: 0 0.375rem; + font-size: var(--font-size-small); - // Small nice thingy, keys can be pressed! - &:active { - transform: translateY(0.125rem); - box-shadow: inset 0 rpx(1) 0 var(--bg-muted-2); - background-color: var(--bg-muted-2); - } + // Small nice thingy, keys can be pressed! + &:active { + transform: translateY(0.125rem); + box-shadow: inset 0 rpx(1) 0 var(--bg-muted-2); + background-color: var(--bg-muted-2); + } } a { - color: var(--accent-color); - font-weight: var(--font-weight-semi-bold); - text-decoration-color: var(--accent-highlight); - text-decoration-thickness: rpx(1); + color: var(--accent-color); + font-weight: var(--font-weight-semi-bold); + text-decoration-color: var(--accent-highlight); + text-decoration-thickness: rpx(1); - &:hover { - text-decoration-style: wavy; - } + &:hover { + text-decoration-style: wavy; + } } hr { - margin: 2rem auto; - border: 0.125rem solid var(--bg-muted-2); - border-radius: 9999px; + margin: 2rem auto; + border: 0.125rem solid var(--bg-muted-2); + border-radius: 9999px; } dt { - font-weight: var(--font-weight-semi-bold); + font-weight: var(--font-weight-semi-bold); } dd { - margin-inline-start: 1.5rem; - margin-block-end: 1rem; + margin-inline-start: 1.5rem; + margin-block-end: 1rem; } aside { - float: inline-end; - margin-inline-start: 1rem; - margin-block-end: 1rem; - box-shadow: var(--edge-highlight), var(--shadow); - border-radius: var(--rounded-corner); - background-color: var(--bg-muted-1); - padding: 1rem; - width: calc(var(--content-width) / 3); + float: inline-end; + margin-inline-start: 1rem; + margin-block-end: 1rem; + box-shadow: var(--edge-highlight), var(--shadow); + border-radius: var(--rounded-corner); + background-color: var(--bg-muted-1); + padding: 1rem; + width: calc(var(--content-width) / 3); - @media only screen and (max-width: 720px) { - float: none; - margin-inline-start: 0; - width: 100%; - } + @media only screen and (max-width: 720px) { + float: none; + margin-inline-start: 0; + width: 100%; + } - :first-child { - margin-block-start: 0; - } + :first-child { + margin-block-start: 0; + } - :last-child { - margin-block-end: 0; - } + :last-child { + margin-block-end: 0; + } } details { - display: flow-root; - transition: padding-block-end var(--transition); - margin-block-end: 1rem; - box-shadow: var(--edge-highlight), var(--shadow); - border-radius: var(--rounded-corner); - background-color: var(--bg-muted-1); - padding: 0.75rem 1rem; + display: flow-root; + transition: padding-block-end var(--transition); + margin-block-end: 1rem; + box-shadow: var(--edge-highlight), var(--shadow); + border-radius: var(--rounded-corner); + background-color: var(--bg-muted-1); + padding: 0.75rem 1rem; - &::details-content { - display: block; - opacity: 0; - transition-behavior: allow-discrete; - transition-duration: var(--transition); - transition-property: opacity, block-size, content-visibility; - block-size: 0; - overflow: hidden; - } + &::details-content { + display: block; + opacity: 0; + transition-behavior: allow-discrete; + transition-duration: var(--transition); + transition-property: opacity, block-size, content-visibility; + block-size: 0; + overflow: hidden; + } - &[open] { - padding-block-end: 1rem; + &[open] { + padding-block-end: 1rem; - &::details-content { - opacity: 1; - block-size: auto; - block-size: calc-size(auto, size); - } + &::details-content { + opacity: 1; + block-size: auto; + block-size: calc-size(auto, size); + } - summary { - color: var(--accent-color); + summary { + color: var(--accent-color); - &::before { - transform: scaleY(-1); - } + &::before { + transform: scaleY(-1); + } - @supports not (block-size: calc-size(auto, size)) { - & ~ * { - animation: slide-in-up var(--transition-bezier); - } - } - } - } + @supports not (block-size: calc-size(auto, size)) { + & ~ * { + animation: slide-in-up var(--transition-bezier); + } + } + } + } - summary { - transition: var(--transition); - color: var(--fg-muted-1); - font-weight: var(--font-weight-semi-bold); - list-style: none; + summary { + transition: var(--transition); + color: var(--fg-muted-1); + font-weight: var(--font-weight-semi-bold); + list-style: none; - &::-webkit-details-marker { - display: none; - } + &::-webkit-details-marker { + display: none; + } - &::before { - @include icon(pseudo); - --icon: var(--icon-caret-down); - display: inline-block; - vertical-align: rpx(-2); - transition: - all var(--transition), - transform var(--transition-bezier); - margin-inline-end: 0.25rem; - } - } + &::before { + @include icon(pseudo); + --icon: var(--icon-caret-down); + display: inline-block; + vertical-align: rpx(-2); + transition: all var(--transition), transform var(--transition-bezier); + margin-inline-end: 0.25rem; + } + } - & > :last-child, - & > :last-child img { - margin-block-end: 0; - } + & > :last-child, + & > :last-child img { + margin-block-end: 0; + } } figure { - margin-inline: 0; + margin-inline: 0; } ol, ul { - padding-inline-start: 1.5rem; + padding-inline-start: 1.5rem; } li { - margin-block: 0.25rem; - padding-inline-start: 0.25rem; + margin-block: 0.25rem; + padding-inline-start: 0.25rem; - &::marker { - color: var(--accent-color); - font-weight: var(--font-weight-semi-bold); - } + &::marker { + color: var(--accent-color); + font-weight: var(--font-weight-semi-bold); + } } b, strong { - font-weight: var(--font-weight-semi-bold); + font-weight: var(--font-weight-semi-bold); } diff --git a/modules/amezing/sass/citrine/components/_buttons.scss b/modules/amezing/sass/citrine/components/_buttons.scss index 9667b7e..d34526b 100644 --- a/modules/amezing/sass/citrine/components/_buttons.scss +++ b/modules/amezing/sass/citrine/components/_buttons.scss @@ -1,100 +1,100 @@ @use "../abstracts/mixins" as *; .buttons { - display: flex; - flex-wrap: wrap; - justify-content: space-between; - align-items: center; - gap: 0.25rem; - margin-block-start: 2rem; + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + gap: 0.25rem; + margin-block-start: 2rem; - &.centered { - justify-content: center; - } + &.centered { + justify-content: center; + } - &.start { - justify-content: flex-start; - } + &.start { + justify-content: flex-start; + } - &.end { - justify-content: flex-end; - } + &.end { + justify-content: flex-end; + } - &.big { - gap: 0.5rem; - } + &.big { + gap: 0.5rem; + } } .buttons a, a.button, button { - display: inline-block; - transition: - background-color var(--transition), - color var(--transition), - box-shadow var(--transition), - transform var(--transition-bezier); - box-shadow: var(--edge-highlight); - border-radius: var(--rounded-corner); - background-color: var(--bg-muted-1); - padding: 0.5rem 1rem; - color: var(--fg-muted-1); - font-weight: var(--font-weight-semi-bold); - font-size: var(--font-size-small); - line-height: 1.5rem; - text-decoration: none; + display: inline-block; + transition: + background-color var(--transition), + color var(--transition), + box-shadow var(--transition), + transform var(--transition-bezier); + box-shadow: var(--edge-highlight); + border-radius: var(--rounded-corner); + background-color: var(--bg-muted-1); + padding: 0.5rem 1rem; + color: var(--fg-muted-1); + font-weight: var(--font-weight-semi-bold); + font-size: var(--font-size-small); + line-height: 1.5rem; + text-decoration: none; - &:not(:disabled, [aria-disabled="true"]):hover { - background-color: var(--bg-muted-2); - color: var(--fg-muted-2); - } + &:not(:disabled, [aria-disabled="true"]):hover { + background-color: var(--bg-muted-2); + color: var(--fg-muted-2); + } - &:not(:disabled, [aria-disabled="true"]):active { - transform: var(--active); - } + &:not(:disabled, [aria-disabled="true"]):active { + transform: var(--active); + } - &.suggested { - @include gradient-fill; + &.suggested { + @include gradient-fill; - &:not(:disabled, [aria-disabled="true"]):hover { - @include gradient-fill(hover); - } - } + &:not(:disabled, [aria-disabled="true"]):hover { + @include gradient-fill(hover); + } + } - &.subtle { - box-shadow: none; - background-color: transparent; + &.subtle { + box-shadow: none; + background-color: transparent; - &:not(:disabled, [aria-disabled="true"]):hover { - box-shadow: var(--edge-highlight); - background-color: var(--bg-muted-1); - color: var(--fg-muted-2); - } - } + &:not(:disabled, [aria-disabled="true"]):hover { + box-shadow: var(--edge-highlight); + background-color: var(--bg-muted-1); + color: var(--fg-muted-2); + } + } - &[aria-busy="true"] { - animation: progress-shimmer var(--transition-long) ease-in-out alternate - infinite; - background-image: linear-gradient( - to right, - transparent 50%, - var(--bg-muted-1) 75%, - transparent 100% - ); - background-size: 200%; + &[aria-busy="true"] { + animation: progress-shimmer var(--transition-long) ease-in-out alternate + infinite; + background-image: linear-gradient( + to right, + transparent 50%, + var(--bg-muted-1) 75%, + transparent 100% + ); + background-size: 200%; - @keyframes progress-shimmer { - to { - background-position-x: -200%; - } - } - } + @keyframes progress-shimmer { + to { + background-position-x: -200%; + } + } + } } .buttons.big a, .buttons.big button, a.button.big, button.big { - border-radius: 9999px; - padding: 0.625rem 1.25rem; + border-radius: 9999px; + padding: 0.625rem 1.25rem; } diff --git a/modules/amezing/sass/citrine/components/_card.scss b/modules/amezing/sass/citrine/components/_card.scss index 6b39ea9..bd58534 100644 --- a/modules/amezing/sass/citrine/components/_card.scss +++ b/modules/amezing/sass/citrine/components/_card.scss @@ -1,5 +1,5 @@ @use "../abstracts/mixins" as *; .card { - @include card; + @include card; } diff --git a/modules/amezing/sass/citrine/components/_crt.scss b/modules/amezing/sass/citrine/components/_crt.scss index 5ea199c..c94f517 100644 --- a/modules/amezing/sass/citrine/components/_crt.scss +++ b/modules/amezing/sass/citrine/components/_crt.scss @@ -1,90 +1,90 @@ .crt, pre:has(code[data-crt], code[data-lang=""]) { - position: relative; - animation: flicker 0.25s alternate infinite; - margin-inline: auto; - box-shadow: var(--shadow-glow); - background-image: var(--crt-bg); - background-color: transparent !important; - padding: 0; - width: fit-content; - max-width: 100%; - overflow: hidden; - color: var(--accent-color) !important; - font-weight: var(--font-weight-medium); - text-shadow: var(--text-shadow-glow); + position: relative; + animation: flicker 0.25s alternate infinite; + margin-inline: auto; + box-shadow: var(--shadow-glow); + background-image: var(--crt-bg); + background-color: transparent !important; + padding: 0; + width: fit-content; + max-width: 100%; + overflow: hidden; + color: var(--accent-color) !important; + font-weight: var(--font-weight-medium); + text-shadow: var(--text-shadow-glow); - @keyframes flicker { - 25% { - opacity: 0.95; - } + @keyframes flicker { + 25% { + opacity: 0.95; + } - 50% { - opacity: 0.85; - } + 50% { + opacity: 0.85; + } - 75% { - opacity: 1; - } + 75% { + opacity: 1; + } - to { - opacity: 0.9; - } - } + to { + opacity: 0.9; + } + } - &::before, - &::after { - position: absolute; - inset: 0; - content: ""; - } + &::before, + &::after { + position: absolute; + inset: 0; + content: ""; + } - &::before { - opacity: var(--scanlines-opacity); - z-index: 1; - animation: scanlines 0.1s linear infinite; - background-image: repeating-linear-gradient( - to bottom, - rgba(0, 0, 0, 0.25), - rgba(0, 0, 0, 0.25) 0.125rem, - transparent 0.125rem, - transparent 0.25rem - ); - background-position-y: -0.125rem; - background-repeat: no-repeat; - pointer-events: none; + &::before { + opacity: var(--scanlines-opacity); + z-index: 1; + animation: scanlines 0.1s linear infinite; + background-image: repeating-linear-gradient( + to bottom, + rgba(0, 0, 0, 0.25), + rgba(0, 0, 0, 0.25) 0.125rem, + transparent 0.125rem, + transparent 0.25rem + ); + background-position-y: -0.125rem; + background-repeat: no-repeat; + pointer-events: none; - @keyframes scanlines { - to { - background-position-y: 0.125rem; - } - } - } + @keyframes scanlines { + to { + background-position-y: 0.125rem; + } + } + } - &::after { - opacity: 0.05; - animation: scanline 5s linear infinite; - background-image: linear-gradient( - to bottom, - transparent, - var(--accent-color) 16rem - ); - background-position-y: -16rem; - background-size: 100% 16rem; - pointer-events: none; + &::after { + opacity: 0.05; + animation: scanline 5s linear infinite; + background-image: linear-gradient( + to bottom, + transparent, + var(--accent-color) 16rem + ); + background-position-y: -16rem; + background-size: 100% 16rem; + pointer-events: none; - @keyframes scanline { - to { - background-position-y: calc(100% + 16rem); - } - } - } + @keyframes scanline { + to { + background-position-y: calc(100% + 16rem); + } + } + } - & > code { - display: block; - border-radius: inherit; - padding: 0.75rem 1rem; - overflow-x: auto; - scrollbar-width: thin; - } + & > code { + display: block; + border-radius: inherit; + padding: 0.75rem 1rem; + overflow-x: auto; + scrollbar-width: thin; + } } diff --git a/modules/amezing/sass/citrine/components/_external.scss b/modules/amezing/sass/citrine/components/_external.scss index 494f317..a26a1da 100644 --- a/modules/amezing/sass/citrine/components/_external.scss +++ b/modules/amezing/sass/citrine/components/_external.scss @@ -2,11 +2,11 @@ @use "../abstracts/mixins" as *; a.external:not(:has(img, svg, video, picture, figure))::after { - @include icon(pseudo); - --icon: var(--icon-arrow-square-out); - display: inline-block; - vertical-align: -0.125rem; - opacity: var(--dim-opacity); - margin-inline-start: 0.125rem; - font-size: max(1rem, 1em); + @include icon(pseudo); + --icon: var(--icon-arrow-square-out); + display: inline-block; + vertical-align: -0.125rem; + opacity: var(--dim-opacity); + margin-inline-start: 0.125rem; + font-size: max(1rem, 1em); } diff --git a/modules/amezing/sass/citrine/components/_iframe.scss b/modules/amezing/sass/citrine/components/_iframe.scss index 2334efe..169b895 100644 --- a/modules/amezing/sass/citrine/components/_iframe.scss +++ b/modules/amezing/sass/citrine/components/_iframe.scss @@ -1,20 +1,20 @@ iframe { - display: block; - margin: 1rem auto; - box-shadow: var(--edge-highlight), var(--shadow); - border: none; - border-radius: var(--rounded-corner); - background-color: var(--bg-muted-1); - width: 100%; - max-width: 100%; + display: block; + margin: 1rem auto; + box-shadow: var(--edge-highlight), var(--shadow); + border: none; + border-radius: var(--rounded-corner); + background-color: var(--bg-muted-1); + width: 100%; + max-width: 100%; - &:fullscreen { - box-shadow: none; - border-radius: 0; - } + &:fullscreen { + box-shadow: none; + border-radius: 0; + } - &:-webkit-full-screen { - box-shadow: none; - border-radius: 0; - } + &:-webkit-full-screen { + box-shadow: none; + border-radius: 0; + } } diff --git a/modules/amezing/sass/citrine/components/_input.scss b/modules/amezing/sass/citrine/components/_input.scss index d696a13..c87b8ef 100644 --- a/modules/amezing/sass/citrine/components/_input.scss +++ b/modules/amezing/sass/citrine/components/_input.scss @@ -1,8 +1,8 @@ @use "../abstracts/mixins" as *; input { - appearance: none; - background-color: var(--bg-muted-1); + appearance: none; + background-color: var(--bg-muted-1); } input:not([type]), @@ -10,252 +10,250 @@ input[type="text"], input[type="search"], input[type="password"], input[type="submit"] { - box-shadow: var(--edge-highlight); - border: none; - border-radius: var(--rounded-corner); - background-color: var(--bg-muted-1); - font-size: inherit; - line-height: inherit; - font-family: inherit; + box-shadow: var(--edge-highlight); + border: none; + border-radius: var(--rounded-corner); + background-color: var(--bg-muted-1); + font-size: inherit; + line-height: inherit; + font-family: inherit; } input:not([type]), input[type="text"], input[type="search"], input[type="password"] { - padding: 0.5rem 0.75rem; - width: 100%; + padding: 0.5rem 0.75rem; + width: 100%; - &::placeholder { - opacity: 1; - color: var(--fg-muted-1); - } + &::placeholder { + opacity: 1; + color: var(--fg-muted-1); + } } input[type="submit"] { - transition: - background-color var(--transition), - transform var(--transition-bezier); - box-shadow: var(--edge-highlight); - border-radius: var(--rounded-corner); - padding: 0.375rem 0.75rem; - color: var(--fg-muted-1); - font-weight: var(--font-weight-semi-bold); - font-size: var(--font-size-small); + transition: + background-color var(--transition), + transform var(--transition-bezier); + box-shadow: var(--edge-highlight); + border-radius: var(--rounded-corner); + padding: 0.375rem 0.75rem; + color: var(--fg-muted-1); + font-weight: var(--font-weight-semi-bold); + font-size: var(--font-size-small); - &:enabled:hover { - background-color: var(--bg-muted-2); - } + &:enabled:hover { + background-color: var(--bg-muted-2); + } - &:enabled:active { - transform: var(--active); - } + &:enabled:active { + transform: var(--active); + } } input[type="radio"], input[type="checkbox"], input[type="color"] { - position: relative; - transition: background-color var(--transition); - border: 0.125rem solid var(--bg-muted-2); - width: 1rem; - height: 1rem; + position: relative; + transition: background-color var(--transition); + border: 0.125rem solid var(--bg-muted-2); + width: 1rem; + height: 1rem; - &:not(:checked):enabled:hover { - background-color: var(--bg-muted-2); - } + &:not(:checked):enabled:hover { + background-color: var(--bg-muted-2); + } } input[type="radio"], input[type="checkbox"] { - vertical-align: -0.125em; - margin-inline-end: 0.25rem; + vertical-align: -0.125em; + margin-inline-end: 0.25rem; - &::before { - display: block; - position: absolute; - transform: scale(0.5); - opacity: 0; - transition: - opacity var(--transition), - transform var(--transition-bezier); - content: ""; - } + &::before { + display: block; + position: absolute; + transform: scale(0.5); + opacity: 0; + transition: opacity var(--transition), transform var(--transition-bezier); + content: ""; + } - &:checked { - border: 0.15rem solid transparent; - background-color: var(--accent-color); + &:checked { + border: 0.15rem solid transparent; + background-color: var(--accent-color); - &::before { - transform: scale(1); - opacity: 1; - } - } + &::before { + transform: scale(1); + opacity: 1; + } + } } input[type="radio"] { - border-radius: 50%; + border-radius: 50%; - &::before { - inset-block-start: 0.125rem; - inset-inline-start: 0.125rem; - border-radius: 50%; - background-color: var(--contrast-color); - width: 0.5rem; - height: 0.5rem; - } + &::before { + inset-block-start: 0.125rem; + inset-inline-start: 0.125rem; + border-radius: 50%; + background-color: var(--contrast-color); + width: 0.5rem; + height: 0.5rem; + } } input[type="checkbox"] { - border-radius: calc(var(--rounded-corner-small) / 2); + border-radius: calc(var(--rounded-corner-small) / 2); - &::before { - @include icon(pseudo); - --icon: var(--icon-check); - transform-origin: bottom left; - inset-block-start: -0.125rem; - inset-inline-start: -0.125rem; - color: var(--contrast-color); - font-size: 1rem; - } + &::before { + @include icon(pseudo); + --icon: var(--icon-check); + transform-origin: bottom left; + inset-block-start: -0.125rem; + inset-inline-start: -0.125rem; + color: var(--contrast-color); + font-size: 1rem; + } - &.switch { - vertical-align: -0.375rem; - box-shadow: var(--edge-highlight); - border: none; - border-radius: 9999px; - width: 2.5rem; - height: 1.5rem; + &.switch { + vertical-align: -0.375rem; + box-shadow: var(--edge-highlight); + border: none; + border-radius: 9999px; + width: 2.5rem; + height: 1.5rem; - &.big { - vertical-align: -0.625rem; - width: 3rem; - height: 2rem; + &.big { + vertical-align: -0.625rem; + width: 3rem; + height: 2rem; - &::before { - width: 1.5rem; - height: 1.5rem; - } - } + &::before { + width: 1.5rem; + height: 1.5rem; + } + } - &::before { - transform: none; - transform-origin: center; - opacity: 1; - mask-image: none; - inset-block-start: 0.25rem; - inset-inline-start: 0.25rem; - box-shadow: var(--shadow); - border-radius: 50%; - background-color: white; - width: 1rem; - height: 1rem; - content: ""; - } + &::before { + transform: none; + transform-origin: center; + opacity: 1; + mask-image: none; + inset-block-start: 0.25rem; + inset-inline-start: 0.25rem; + box-shadow: var(--shadow); + border-radius: 50%; + background-color: white; + width: 1rem; + height: 1rem; + content: ""; + } - &:checked { - background-color: var(--accent-color); + &:checked { + background-color: var(--accent-color); - &::before { - transform: translateX(1rem); - background-color: var(--contrast-color); + &::before { + transform: translateX(1rem); + background-color: var(--contrast-color); - :root[dir*="rtl"] & { - transform: translateX(-1rem); - } - } - } + :root[dir*="rtl"] & { + transform: translateX(-1rem); + } + } + } - &:disabled::before { - box-shadow: none; - } - } + &:disabled::before { + box-shadow: none; + } + } } input[type="color"] { - vertical-align: -0.375em; - box-shadow: var(--edge-highlight); - border: none; - border-radius: var(--rounded-corner-small); - padding: 0.25rem; - width: 3rem; - height: 2rem; + vertical-align: -0.375em; + box-shadow: var(--edge-highlight); + border: none; + border-radius: var(--rounded-corner-small); + padding: 0.25rem; + width: 3rem; + height: 2rem; - &::-moz-color-swatch { - border: none; - border-radius: calc(var(--rounded-corner-small) - 0.25rem); - } + &::-moz-color-swatch { + border: none; + border-radius: calc(var(--rounded-corner-small) - 0.25rem); + } - &::-webkit-color-swatch-wrapper { - padding: 0; - } + &::-webkit-color-swatch-wrapper { + padding: 0; + } - &::-webkit-color-swatch { - border-radius: calc(var(--rounded-corner-small) - 0.25rem); - } + &::-webkit-color-swatch { + border-radius: calc(var(--rounded-corner-small) - 0.25rem); + } } input[type="range"] { - appearance: none; - box-shadow: var(--edge-highlight); - border-radius: 9999px; - background: var(--bg-muted-1); - padding: 0.5rem 0.25rem; - width: 100%; - height: 1rem; + appearance: none; + box-shadow: var(--edge-highlight); + border-radius: 9999px; + background: var(--bg-muted-1); + padding: 0.5rem 0.25rem; + width: 100%; + height: 1rem; - &::-webkit-slider-runnable-track { - cursor: pointer; - border-radius: 9999px; - background: var(--accent-color); - width: 100%; - } + &::-webkit-slider-runnable-track { + cursor: pointer; + border-radius: 9999px; + background: var(--accent-color); + width: 100%; + } - &::-moz-range-track { - cursor: pointer; - border-radius: 9999px; - background: var(--accent-color); - width: 100%; - height: 0.5rem; - } + &::-moz-range-track { + cursor: pointer; + border-radius: 9999px; + background: var(--accent-color); + width: 100%; + height: 0.5rem; + } - &::-webkit-slider-thumb { - appearance: none; - cursor: ew-resize; - margin-block: -0.5rem; - box-shadow: var(--shadow); - border: none; - border-radius: 0.25rem; - background-color: white; - width: 0.5rem; - height: 1.5rem; - } + &::-webkit-slider-thumb { + appearance: none; + cursor: ew-resize; + margin-block: -0.5rem; + box-shadow: var(--shadow); + border: none; + border-radius: 0.25rem; + background-color: white; + width: 0.5rem; + height: 1.5rem; + } - &::-moz-range-thumb { - appearance: none; - cursor: ew-resize; - margin-block: -0.5rem; - box-shadow: var(--shadow); - border: none; - border-radius: 0.25rem; - background-color: white; - width: 0.5rem; - height: 1.5rem; - } + &::-moz-range-thumb { + appearance: none; + cursor: ew-resize; + margin-block: -0.5rem; + box-shadow: var(--shadow); + border: none; + border-radius: 0.25rem; + background-color: white; + width: 0.5rem; + height: 1.5rem; + } - &.hollow { - &::-webkit-slider-thumb { - margin-block: 0; - outline: 0.25rem solid white; - background-color: transparent; - height: 0.5rem; - } + &.hollow { + &::-webkit-slider-thumb { + margin-block: 0; + outline: 0.25rem solid white; + background-color: transparent; + height: 0.5rem; + } - &::-moz-range-thumb { - margin-block: 0; - outline: 0.25rem solid white; - background-color: transparent; - height: 0.5rem; - } - } + &::-moz-range-thumb { + margin-block: 0; + outline: 0.25rem solid white; + background-color: transparent; + height: 0.5rem; + } + } } diff --git a/modules/amezing/sass/citrine/components/_media.scss b/modules/amezing/sass/citrine/components/_media.scss index 3409071..ca51dda 100644 --- a/modules/amezing/sass/citrine/components/_media.scss +++ b/modules/amezing/sass/citrine/components/_media.scss @@ -1,165 +1,165 @@ img, video { - display: block; - margin: 1rem auto; - box-shadow: var(--edge-highlight), var(--shadow); - border-radius: var(--rounded-corner); - background-color: var(--bg-muted-1); - max-width: 100%; - max-height: calc(100vh - 2rem); + display: block; + margin: 1rem auto; + box-shadow: var(--edge-highlight), var(--shadow); + border-radius: var(--rounded-corner); + background-color: var(--bg-muted-1); + max-width: 100%; + max-height: calc(100vh - 2rem); - &.full, - &[src*="#full"] { - width: 100%; - } + &.full, + &[src*="#full"] { + width: 100%; + } - &.start, - &.end, - &[src*="#start"], - &[src*="#end"] { - width: calc(var(--content-width) / 3); - } + &.start, + &.end, + &[src*="#start"], + &[src*="#end"] { + width: calc(var(--content-width) / 3); + } - &.start, - &[src*="#start"] { - float: inline-start; - transform-origin: left; - margin-inline-end: 1rem; + &.start, + &[src*="#start"] { + float: inline-start; + transform-origin: left; + margin-inline-end: 1rem; - :root[dir*="rtl"] & { - transform-origin: right; - } - } + :root[dir*="rtl"] & { + transform-origin: right; + } + } - &.end, - &[src*="#end"] { - float: inline-end; - transform-origin: right; - margin-inline-start: 1rem; + &.end, + &[src*="#end"] { + float: inline-end; + transform-origin: right; + margin-inline-start: 1rem; - :root[dir*="rtl"] & { - transform-origin: left; - } - } + :root[dir*="rtl"] & { + transform-origin: left; + } + } - &.pixels, - &[src*="#pixels"] { - image-rendering: pixelated; - } + &.pixels, + &[src*="#pixels"] { + image-rendering: pixelated; + } - &.has-alpha, - &[src*="#has-alpha"] { - box-shadow: none; - border-radius: 0; - background-color: transparent; - } + &.has-alpha, + &[src*="#has-alpha"] { + box-shadow: none; + border-radius: 0; + background-color: transparent; + } - &.drop-shadow, - &[src*="#drop-shadow"] { - filter: var(--drop-shadow); - } + &.drop-shadow, + &[src*="#drop-shadow"] { + filter: var(--drop-shadow); + } - &.spoiler, - &[src*="#spoiler"] { - opacity: var(--dim-opacity); - clip-path: inset(0 0 0 0 round var(--rounded-corner)); - filter: blur(1.5rem); + &.spoiler, + &[src*="#spoiler"] { + opacity: var(--dim-opacity); + clip-path: inset(0 0 0 0 round var(--rounded-corner)); + filter: blur(1.5rem); - &:hover, - &:active { - opacity: 1; - clip-path: inset( - -0.75rem -0.75rem -0.75rem -0.75rem round var(--rounded-corner-small) - ); - filter: none; - } + &:hover, + &:active { + opacity: 1; + clip-path: inset( + -0.75rem -0.75rem -0.75rem -0.75rem round var(--rounded-corner-small) + ); + filter: none; + } - &.solid, - &[src*="#solid"] { - clip-path: none; - filter: brightness(0) contrast(0.5); - box-shadow: none; + &.solid, + &[src*="#solid"] { + clip-path: none; + filter: brightness(0) contrast(0.5); + box-shadow: none; - &:hover, - &:active { - filter: none; - } - } - } + &:hover, + &:active { + filter: none; + } + } + } - @media only screen and (max-width: 720px) { - &.start, - &.end, - &[src*="#start"], - &[src*="#end"] { - float: none; - margin-inline-start: auto; - margin-inline-end: auto; - width: auto; - } - } + @media only screen and (max-width: 720px) { + &.start, + &.end, + &[src*="#start"], + &[src*="#end"] { + float: none; + margin-inline-start: auto; + margin-inline-end: auto; + width: auto; + } + } } img { - transition: - box-shadow var(--transition), - border-radius var(--transition), - transform var(--transition-bezier); + transition: + box-shadow var(--transition), + border-radius var(--transition), + transform var(--transition-bezier); - &:not(.no-hover, [src*="#no-hover"], .emoji) { - cursor: zoom-in; + &:not(.no-hover, [src*="#no-hover"], .emoji) { + cursor: zoom-in; - &:hover { - position: relative; - transform: var(--hover); - z-index: 1; - box-shadow: var(--edge-highlight), var(--shadow-raised); - border-radius: var(--rounded-corner-small); - } + &:hover { + position: relative; + transform: var(--hover); + z-index: 1; + box-shadow: var(--edge-highlight), var(--shadow-raised); + border-radius: var(--rounded-corner-small); + } - &.start, - &.end, - &[src*="#start"], - &[src*="#end"] { - transition: transform var(--transition-bezier-longer); + &.start, + &.end, + &[src*="#start"], + &[src*="#end"] { + transition: transform var(--transition-bezier-longer); - &:hover { - transform: scale(2); - } + &:hover { + transform: scale(2); + } - @media only screen and (max-width: 720px) { - transform-origin: center; - transition: transform var(--transition-bezier); + @media only screen and (max-width: 720px) { + transform-origin: center; + transition: transform var(--transition-bezier); - &:hover { - transform: var(--hover); - } - } - } + &:hover { + transform: var(--hover); + } + } + } - &.has-alpha, - &[src*="#has-alpha"] { - &:hover { - box-shadow: none; - } - } - } + &.has-alpha, + &[src*="#has-alpha"] { + &:hover { + box-shadow: none; + } + } + } } a img:not(.no-hover, [src*="#no-hover"]) { - cursor: pointer; + cursor: pointer; } video:fullscreen { - box-shadow: none; - border-radius: 0; + box-shadow: none; + border-radius: 0; } video:-webkit-full-screen { - box-shadow: none; - border-radius: 0; + box-shadow: none; + border-radius: 0; } picture { - display: inline-block; + display: inline-block; } diff --git a/modules/amezing/sass/citrine/components/_overflow-auto.scss b/modules/amezing/sass/citrine/components/_overflow-auto.scss index 712ba4b..20076ea 100644 --- a/modules/amezing/sass/citrine/components/_overflow-auto.scss +++ b/modules/amezing/sass/citrine/components/_overflow-auto.scss @@ -1,13 +1,13 @@ .overflow-auto { - overflow: auto; + overflow: auto; - &.overshoot { - margin-block: -1rem; - padding-block: 1rem; - } + &.overshoot { + margin-block: -1rem; + padding-block: 1rem; + } - &.overshoot-row { - margin-inline: -1rem; - padding-inline: 1rem; - } + &.overshoot-row { + margin-inline: -1rem; + padding-inline: 1rem; + } } diff --git a/modules/amezing/sass/citrine/components/_overshoot.scss b/modules/amezing/sass/citrine/components/_overshoot.scss index af8447f..6b71153 100644 --- a/modules/amezing/sass/citrine/components/_overshoot.scss +++ b/modules/amezing/sass/citrine/components/_overshoot.scss @@ -1,23 +1,23 @@ .overshoot { - --mask: linear-gradient( - to bottom, - transparent, - black 1rem, - black calc(100% - 1rem), - transparent - ); - -webkit-mask-image: var(--mask); - mask-image: var(--mask); + --mask: linear-gradient( + to bottom, + transparent, + black 1rem, + black calc(100% - 1rem), + transparent + ); + -webkit-mask-image: var(--mask); + mask-image: var(--mask); - &-row { - --mask: linear-gradient( - to right, - transparent, - black 1rem, - black calc(100% - 1rem), - transparent - ); - -webkit-mask-image: var(--mask); - mask-image: var(--mask); - } + &-row { + --mask: linear-gradient( + to right, + transparent, + black 1rem, + black calc(100% - 1rem), + transparent + ); + -webkit-mask-image: var(--mask); + mask-image: var(--mask); + } } diff --git a/modules/amezing/sass/citrine/components/_skip-to-content.scss b/modules/amezing/sass/citrine/components/_skip-to-content.scss index 1e217af..d2c64fe 100644 --- a/modules/amezing/sass/citrine/components/_skip-to-content.scss +++ b/modules/amezing/sass/citrine/components/_skip-to-content.scss @@ -1,23 +1,23 @@ #skip-to-content { - position: absolute; - transform: translateX(-50%) translateY(calc(-100% - 1rem)); - z-index: 9999; - transition: - backdrop-filter var(--transition), - box-shadow var(--transition), - transform var(--transition-bezier); - inset-inline-start: 50vw; - border-radius: 9999px; - background-color: var(--glass-bg-1); - padding: 0.375rem 0.75rem; - width: max-content; - text-decoration: none; + position: absolute; + transform: translateX(-50%) translateY(calc(-100% - 1rem)); + z-index: 9999; + transition: + backdrop-filter var(--transition), + box-shadow var(--transition), + transform var(--transition-bezier); + inset-inline-start: 50vw; + border-radius: 9999px; + background-color: var(--glass-bg-1); + padding: 0.375rem 0.75rem; + width: max-content; + text-decoration: none; - &:focus-visible, - &:focus { - -webkit-backdrop-filter: var(--blur); - transform: translateX(-50%) translateY(1rem); - backdrop-filter: var(--blur); - box-shadow: var(--edge-highlight), var(--shadow); - } + &:focus-visible, + &:focus { + -webkit-backdrop-filter: var(--blur); + transform: translateX(-50%) translateY(1rem); + backdrop-filter: var(--blur); + box-shadow: var(--edge-highlight), var(--shadow); + } } diff --git a/modules/amezing/sass/citrine/components/_spoiler.scss b/modules/amezing/sass/citrine/components/_spoiler.scss index 3b3c91b..5c95170 100644 --- a/modules/amezing/sass/citrine/components/_spoiler.scss +++ b/modules/amezing/sass/citrine/components/_spoiler.scss @@ -1,25 +1,23 @@ span.spoiler { - filter: blur(0.25rem); - transition: filter var(--transition); + filter: blur(0.25rem); + transition: filter var(--transition); - &:focus, - &:hover { - filter: none; - } + &:focus, + &:hover { + filter: none; + } - &.solid { - filter: none; - transition: - background-color var(--transition), - color var(--transition); - border-radius: var(--rounded-corner-small); - background-color: var(--fg-muted-1); - color: transparent; + &.solid { + filter: none; + transition: background-color var(--transition), color var(--transition); + border-radius: var(--rounded-corner-small); + background-color: var(--fg-muted-1); + color: transparent; - &:focus, - &:hover { - background-color: transparent; - color: inherit; - } - } + &:focus, + &:hover { + background-color: transparent; + color: inherit; + } + } } diff --git a/modules/amezing/sass/citrine/components/_table.scss b/modules/amezing/sass/citrine/components/_table.scss index 569a914..bda5f24 100644 --- a/modules/amezing/sass/citrine/components/_table.scss +++ b/modules/amezing/sass/citrine/components/_table.scss @@ -1,36 +1,36 @@ @use "../abstracts/functions" as *; table { - width: 100%; - overflow: hidden; + width: 100%; + overflow: hidden; } thead { - border-block-start: none; - color: var(--fg-muted-2); - font-weight: var(--font-weight-semi-bold); + border-block-start: none; + color: var(--fg-muted-2); + font-weight: var(--font-weight-semi-bold); } tr { - &:nth-child(even) { - background-color: var(--bg-muted-1); - } + &:nth-child(even) { + background-color: var(--bg-muted-1); + } - th, - td { - padding: 0.5rem 1rem; - } + th, + td { + padding: 0.5rem 1rem; + } } tbody tr { - border-block: rpx(1) solid var(--bg-muted-2); + border-block: rpx(1) solid var(--bg-muted-2); - &:last-child { - border-block-end: none; - } + &:last-child { + border-block-end: none; + } } .overflow-auto table { - width: max-content; - min-width: 100%; + width: max-content; + min-width: 100%; } diff --git a/modules/amezing/sass/citrine/components/_textarea.scss b/modules/amezing/sass/citrine/components/_textarea.scss index 58c355a..8405801 100644 --- a/modules/amezing/sass/citrine/components/_textarea.scss +++ b/modules/amezing/sass/citrine/components/_textarea.scss @@ -1,17 +1,17 @@ textarea { - appearance: none; - box-shadow: var(--edge-highlight); - border: none; - border-radius: var(--rounded-corner); - background-color: var(--bg-muted-1); - padding: 1rem; - resize: none; - font-size: inherit; - line-height: inherit; - font-family: inherit; + appearance: none; + box-shadow: var(--edge-highlight); + border: none; + border-radius: var(--rounded-corner); + background-color: var(--bg-muted-1); + padding: 1rem; + resize: none; + font-size: inherit; + line-height: inherit; + font-family: inherit; - &::placeholder { - opacity: 1; - color: var(--fg-muted-1); - } + &::placeholder { + opacity: 1; + color: var(--fg-muted-1); + } } diff --git a/modules/amezing/sass/citrine/components/_title.scss b/modules/amezing/sass/citrine/components/_title.scss index 16b8c89..a874e60 100644 --- a/modules/amezing/sass/citrine/components/_title.scss +++ b/modules/amezing/sass/citrine/components/_title.scss @@ -1,5 +1,5 @@ strong.title { - color: var(--accent-color); - font-size: var(--font-size-large); - line-height: 1; + color: var(--accent-color); + font-size: var(--font-size-large); + line-height: 1; } diff --git a/modules/amezing/sass/citrine/components/_tooltip.scss b/modules/amezing/sass/citrine/components/_tooltip.scss index 72aa4c1..8ba5533 100644 --- a/modules/amezing/sass/citrine/components/_tooltip.scss +++ b/modules/amezing/sass/citrine/components/_tooltip.scss @@ -1,41 +1,41 @@ // attr() hack from https://cyrneko.eu/css-tricks.html @media (pointer: coarse), (hover: none) { - .tooltip, - abbr[title] { - display: inline-flex; - position: relative; - justify-content: center; + .tooltip, + abbr[title] { + display: inline-flex; + position: relative; + justify-content: center; - &::after { - position: absolute; - top: calc(100% + 0.25rem); - z-index: 9999; - backdrop-filter: var(--blur); - transition: - visibility var(--transition), - opacity var(--transition), - transform var(--transition-bezier); - box-shadow: var(--edge-highlight), var(--shadow); - border-radius: var(--rounded-corner-small); - background-color: var(--glass-bg-1); - padding: 0.125rem 0.375rem; - width: max-content; - content: attr(title); - -webkit-backdrop-filter: var(--blur); - transform: translateY(-0.5rem) scale(0.5); - transform-origin: top; - visibility: hidden; - opacity: 0; - max-width: calc(var(--content-width) / 3); - color: var(--fg-muted-2); - font-size: var(--font-size-small); - } + &::after { + position: absolute; + top: calc(100% + 0.25rem); + z-index: 9999; + backdrop-filter: var(--blur); + transition: + visibility var(--transition), + opacity var(--transition), + transform var(--transition-bezier); + box-shadow: var(--edge-highlight), var(--shadow); + border-radius: var(--rounded-corner-small); + background-color: var(--glass-bg-1); + padding: 0.125rem 0.375rem; + width: max-content; + content: attr(title); + -webkit-backdrop-filter: var(--blur); + transform: translateY(-0.5rem) scale(0.5); + transform-origin: top; + visibility: hidden; + opacity: 0; + max-width: calc(var(--content-width) / 3); + color: var(--fg-muted-2); + font-size: var(--font-size-small); + } - &:focus::after, - &:hover::after { - transform: none; - visibility: visible; - opacity: 1; - } - } + &:focus::after, + &:hover::after { + transform: none; + visibility: visible; + opacity: 1; + } + } } diff --git a/modules/amezing/sass/citrine/components/_visually-hidden.scss b/modules/amezing/sass/citrine/components/_visually-hidden.scss index fbf292a..9646fa7 100644 --- a/modules/amezing/sass/citrine/components/_visually-hidden.scss +++ b/modules/amezing/sass/citrine/components/_visually-hidden.scss @@ -1,11 +1,11 @@ .visually-hidden { - position: absolute !important; - clip-path: inset(50%) !important; - margin: -1px !important; - border-width: 0 !important; - padding: 0 !important; - width: 1px !important; - height: 1px !important; - overflow: hidden !important; - white-space: nowrap !important; + position: absolute !important; + clip-path: inset(50%) !important; + margin: -1px !important; + border-width: 0 !important; + padding: 0 !important; + width: 1px !important; + height: 1px !important; + overflow: hidden !important; + white-space: nowrap !important; } diff --git a/modules/amezing/sass/components/_alerts.scss b/modules/amezing/sass/components/_alerts.scss index d9f5ed5..c4132f7 100644 --- a/modules/amezing/sass/components/_alerts.scss +++ b/modules/amezing/sass/components/_alerts.scss @@ -3,151 +3,151 @@ @use "../citrine/abstracts/variables" as *; [class^="markdown-alert"] { - display: flow-root; - position: relative; + display: flow-root; + position: relative; - &::before { - margin-inline-start: 1.5rem; - content: var(--alert-title); - color: var(--accent-color); - font-weight: var(--font-weight-semi-bold); - } + &::before { + margin-inline-start: 1.5rem; + content: var(--alert-title); + color: var(--accent-color); + font-weight: var(--font-weight-semi-bold); + } - &::after { - @include icon(pseudo); - --icon: var(--alert-icon); - position: absolute; - margin-inline-end: 0.25rem; - inset-block-start: rpx(1); - inset-inline-start: 0.75rem; - color: var(--accent-color); - font-size: 1.25rem; - } + &::after { + @include icon(pseudo); + --icon: var(--alert-icon); + position: absolute; + margin-inline-end: 0.25rem; + inset-block-start: rpx(1); + inset-inline-start: 0.75rem; + color: var(--accent-color); + font-size: 1.25rem; + } - & > :first-child { - margin-block-start: 0.25rem; - } + & > :first-child { + margin-block-start: 0.25rem; + } - & > :last-child { - margin-block-end: 0; - } + & > :last-child { + margin-block-end: 0; + } } .markdown-alert { - // GitHub flavored alerts - &-note, + // GitHub flavored alerts + &-note, &-tip, &-important, &-warning, &-caution, // Generic colors - &-red, + &-red, &-orange, &-yellow, &-green, &-blue, &-purple { - @include variables-accent(("light", "common")); + @include variables-accent(("light", "common")); - @media (prefers-color-scheme: dark) { - @include variables-accent("dark"); - } - } + @media (prefers-color-scheme: dark) { + @include variables-accent("dark"); + } + } - &-note { - &::before { - content: var(--alert-note-title); - } + &-note { + &::before { + content: var(--alert-note-title); + } - &::after { - --icon: var(--icon-info); - } - } + &::after { + --icon: var(--icon-info); + } + } - &-tip { - &::before { - content: var(--alert-tip-title); - } + &-tip { + &::before { + content: var(--alert-tip-title); + } - &::after { - --icon: var(--icon-lightbulb); - } - } + &::after { + --icon: var(--icon-lightbulb); + } + } - &-important { - &::before { - content: var(--alert-important-title); - } + &-important { + &::before { + content: var(--alert-important-title); + } - &::after { - --icon: var(--icon-warning-diamond); - } - } + &::after { + --icon: var(--icon-warning-diamond); + } + } - &-warning { - &::before { - content: var(--alert-warning-title); - } + &-warning { + &::before { + content: var(--alert-warning-title); + } - &::after { - --icon: var(--icon-warning); - } - } + &::after { + --icon: var(--icon-warning); + } + } - &-caution { - &::before { - content: var(--alert-caution-title); - } + &-caution { + &::before { + content: var(--alert-caution-title); + } - &::after { - --icon: var(--icon-warning-octagon); - } - } + &::after { + --icon: var(--icon-warning-octagon); + } + } - &-caution, - &-red { - --accent-h: var(--red-h); - --accent-s: var(--red-s); - --accent-l: var(--red-l); - --accent-color: var(--red-fg); - } + &-caution, + &-red { + --accent-h: var(--red-h); + --accent-s: var(--red-s); + --accent-l: var(--red-l); + --accent-color: var(--red-fg); + } - &-orange { - --accent-h: var(--orange-h); - --accent-s: var(--orange-s); - --accent-l: var(--orange-l); - --accent-color: var(--orange-fg); - } + &-orange { + --accent-h: var(--orange-h); + --accent-s: var(--orange-s); + --accent-l: var(--orange-l); + --accent-color: var(--orange-fg); + } - &-warning, - &-yellow { - --accent-h: var(--yellow-h); - --accent-s: var(--yellow-s); - --accent-l: var(--yellow-l); - --accent-color: var(--yellow-fg); - } + &-warning, + &-yellow { + --accent-h: var(--yellow-h); + --accent-s: var(--yellow-s); + --accent-l: var(--yellow-l); + --accent-color: var(--yellow-fg); + } - &-tip, - &-green { - --accent-h: var(--green-h); - --accent-s: var(--green-s); - --accent-l: var(--green-l); - --accent-color: var(--green-fg); - } + &-tip, + &-green { + --accent-h: var(--green-h); + --accent-s: var(--green-s); + --accent-l: var(--green-l); + --accent-color: var(--green-fg); + } - &-note, - &-blue { - --accent-h: var(--blue-h); - --accent-s: var(--blue-s); - --accent-l: var(--blue-l); - --accent-color: var(--blue-fg); - } + &-note, + &-blue { + --accent-h: var(--blue-h); + --accent-s: var(--blue-s); + --accent-l: var(--blue-l); + --accent-color: var(--blue-fg); + } - &-important, - &-purple { - --accent-h: var(--purple-h); - --accent-s: var(--purple-s); - --accent-l: var(--purple-l); - --accent-color: var(--purple-fg); - } + &-important, + &-purple { + --accent-h: var(--purple-h); + --accent-s: var(--purple-s); + --accent-l: var(--purple-l); + --accent-color: var(--purple-fg); + } } diff --git a/modules/amezing/sass/components/_article-details.scss b/modules/amezing/sass/components/_article-details.scss index b3ca1ff..765baf5 100644 --- a/modules/amezing/sass/components/_article-details.scss +++ b/modules/amezing/sass/components/_article-details.scss @@ -1,71 +1,71 @@ @use "../citrine/abstracts/functions" as *; .article-details { - display: flex; - flex-direction: column; - gap: 0.75rem; - margin-block-end: 2rem; - color: var(--fg-muted-2); - font-size: var(--font-size-small); + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-block-end: 2rem; + color: var(--fg-muted-2); + font-size: var(--font-size-small); - .items { - display: flex; - flex-wrap: wrap; - align-items: flex-start; - gap: 0.25rem 0.5rem; - } + .items { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: 0.25rem 0.5rem; + } - .item { - display: inline-flex; - align-items: center; - gap: 0.375rem; - border-radius: var(--rounded-corner-small); - } + .item { + display: inline-flex; + align-items: center; + gap: 0.375rem; + border-radius: var(--rounded-corner-small); + } - a { - color: inherit; - font-weight: var(--font-weight-normal); - font-size: var(--font-size-small); - text-decoration-color: transparent; + a { + color: inherit; + font-weight: var(--font-weight-normal); + font-size: var(--font-size-small); + text-decoration-color: transparent; - &:hover { - text-decoration-style: solid; - text-decoration-color: var(--fg-muted-1); - } - } + &:hover { + text-decoration-style: solid; + text-decoration-color: var(--fg-muted-1); + } + } - .tooltip { - cursor: help; - } + .tooltip { + cursor: help; + } - .icon { - box-shadow: var(--edge-highlight); - border-radius: var(--rounded-corner-small); - background-color: var(--bg-muted-1); - padding: 0.375rem; - color: var(--accent-color); - font-size: 1.25rem; - } + .icon { + box-shadow: var(--edge-highlight); + border-radius: var(--rounded-corner-small); + background-color: var(--bg-muted-1); + padding: 0.375rem; + color: var(--accent-color); + font-size: 1.25rem; + } - .tags { - display: flex; - flex-wrap: wrap; - gap: 0 0.5rem; - margin: 0; - border-block-start: rpx(1) solid var(--bg-muted-2); - padding-inline-start: 0; - padding-block-start: 0.5rem; - width: 100%; - list-style: none; + .tags { + display: flex; + flex-wrap: wrap; + gap: 0 0.5rem; + margin: 0; + border-block-start: rpx(1) solid var(--bg-muted-2); + padding-inline-start: 0; + padding-block-start: 0.5rem; + width: 100%; + list-style: none; - li { - display: flex; - margin-block: 0; - padding: 0; - } + li { + display: flex; + margin-block: 0; + padding: 0; + } - a::before { - content: "#"; - } - } + a::before { + content: "#"; + } + } } diff --git a/modules/amezing/sass/components/_article-list.scss b/modules/amezing/sass/components/_article-list.scss index e597ecc..7126273 100644 --- a/modules/amezing/sass/components/_article-list.scss +++ b/modules/amezing/sass/components/_article-list.scss @@ -3,252 +3,254 @@ @use "../citrine/abstracts/mixins" as *; .article-list { - display: grid; - grid-template-columns: repeat( - auto-fill, - minmax(calc(var(--content-width) / 2.5), 1fr) - ); - gap: 1rem; - margin-block-start: 2rem; + display: grid; + grid-template-columns: repeat( + auto-fill, + minmax(calc(var(--content-width) / 2.5), 1fr) + ); + gap: 1rem; + margin-block-start: 2rem; - &.categories article { - position: relative; + &.categories article { + position: relative; - &:hover .icon:last-child { - transform: translateY(-50%) rotate(-10deg) scale(1.5); - } + &:hover .icon:last-child { + transform: translateY(-50%) rotate(-10deg) scale(1.5); + } - .icon:last-child { - position: absolute; - transform: translateY(-50%); - z-index: -1; - transition: transform var(--transition-bezier); - inset-block-start: 50%; - inset-inline-end: -3rem; - color: var(--bg-muted-1); - font-size: 12rem; - } - } + .icon:last-child { + position: absolute; + transform: translateY(-50%); + z-index: -1; + transition: transform var(--transition-bezier); + inset-block-start: 50%; + inset-inline-end: -3rem; + color: var(--bg-muted-1); + font-size: 12rem; + } + } - article { - display: flex; - position: relative; - flex-direction: column; - justify-content: space-between; - box-shadow: var(--edge-highlight), var(--shadow); - border-radius: 1.25rem; - background-color: var(--bg-muted-1); - padding: 0; - overflow: hidden; + article { + display: flex; + position: relative; + flex-direction: column; + justify-content: space-between; + box-shadow: var(--edge-highlight), var(--shadow); + border-radius: 1.25rem; + background-color: var(--bg-muted-1); + padding: 0; + overflow: hidden; - &.draft { - opacity: var(--disabled-opacity); - filter: grayscale(1); - transition-duration: var(--transition); - transition-property: opacity, filter; + &.draft { + opacity: var(--disabled-opacity); + filter: grayscale(1); + transition-duration: var(--transition); + transition-property: opacity, filter; - &:hover { - opacity: 1; - filter: none; - } - } + &:hover { + opacity: 1; + filter: none; + } + } - &.has-banner.has-accent-color { - background-image: var(--bg-overlay), var(--blurnail); - background-position: center; - background-size: cover; - } + &.has-banner.has-accent-color { + background-image: var(--bg-overlay), var(--blurnail); + background-position: center; + background-size: cover; + } - &.has-accent-color { - @include variables-accent(("light", "common")); + &.has-accent-color { + @include variables-accent(("light", "common")); - @media (prefers-color-scheme: dark) { - & { - @include variables-accent("dark"); - } - } - } + @media (prefers-color-scheme: dark) { + & { + @include variables-accent("dark"); + } + } + } - & > div { - display: flex; - flex: 1; - flex-direction: column; - margin: 1rem; + & > div { + display: flex; + flex: 1; + flex-direction: column; + margin: 1rem; - & > a { - text-decoration: none; - } - } + & > a { + text-decoration: none; + } + } - h3 { - margin: 0; - border-inline-start: 0.25rem solid; - border-start-start-radius: 0.25rem; - border-end-start-radius: 0.25rem; - padding-inline-start: 0.5rem; - color: var(--accent-color); - font-weight: var(--font-weight-semi-bold); - text-decoration: none; + h3 { + margin: 0; + border-inline-start: 0.25rem solid; + border-start-start-radius: 0.25rem; + border-end-start-radius: 0.25rem; + padding-inline-start: 0.5rem; + color: var(--accent-color); + font-weight: var(--font-weight-semi-bold); + text-decoration: none; - &:hover::after { - opacity: 1; - } + &:hover::after { + opacity: 1; + } - &::after { - opacity: 0; - transition: opacity var(--transition); - content: " →"; - } - } + &::after { + opacity: 0; + transition: opacity var(--transition); + content: " →"; + } + } - .description p { - margin-block-end: 0.75rem; - } + .description p { + margin-block-end: 0.75rem; + } - .open-post { - @include easing-mask(bottom); - display: flex; - position: relative; - justify-content: center; - align-items: center; - margin-block-end: -2rem; - box-shadow: var(--edge-highlight); - border-radius: 0; - aspect-ratio: 2 / 1; - overflow: hidden; - text-decoration: none; + .open-post { + @include easing-mask(bottom); + display: flex; + position: relative; + justify-content: center; + align-items: center; + margin-block-end: -2rem; + box-shadow: var(--edge-highlight); + border-radius: 0; + aspect-ratio: 2 / 1; + overflow: hidden; + text-decoration: none; - &:hover { - &::before { - background-color: var(--shade-color); - } + &:hover { + &::before { + background-color: var(--shade-color); + } - img { - transform: var(--hover); - filter: blur(0.25rem); - } + img { + transform: var(--hover); + filter: blur(0.25rem); + } - .icon { - transform: none; - opacity: 1; - } - } + .icon { + transform: none; + opacity: 1; + } + } - &:active img { - transform: none; - } + &:active img { + transform: none; + } - &::before { - position: absolute; - z-index: 1; - transition: background-color var(--transition); - inset: 0; - border-radius: inherit; - content: ""; - } + &::before { + position: absolute; + z-index: 1; + transition: background-color var(--transition); + inset: 0; + border-radius: inherit; + content: ""; + } - img { - position: absolute; - transition-duration: var(--transition); - transition-property: transform, filter; - margin: 0; - inset: 0; - width: 100%; - height: 100%; - object-fit: cover; - } + img { + position: absolute; + transition-duration: var(--transition); + transition-property: transform, filter; + margin: 0; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + } - .icon { - transform: scale(1.5); - opacity: 0; - z-index: 1; - transition: - transform var(--transition-bezier), - opacity var(--transition); - color: white; - font-size: 1.25rem; - } - } + .icon { + transform: scale(1.5); + opacity: 0; + z-index: 1; + transition: + transform var(--transition-bezier), + opacity var(--transition); + color: white; + font-size: 1.25rem; + } + } - .article-details { - flex: 1; - justify-content: flex-end; - margin-block-end: 0; + .article-details { + flex: 1; + justify-content: flex-end; + margin-block-end: 0; - .tags { - margin-block-start: auto; - margin-block-end: -0.25rem; - } - } - } + .tags { + margin-block-start: auto; + margin-block-end: -0.25rem; + } + } + } } .paginator { - display: flex; - justify-content: center; - align-items: center; - gap: 0.25rem; - margin-block-start: 4rem; + display: flex; + justify-content: center; + align-items: center; + gap: 0.25rem; + margin-block-start: 4rem; - &:has(a.paginator-item.previous:active) .paginator-counter { - border-start-start-radius: 1rem; - border-end-start-radius: 1rem; - } + &:has(a.paginator-item.previous:active) .paginator-counter { + border-start-start-radius: 1rem; + border-end-start-radius: 1rem; + } - &:has(a.paginator-item.next:active) .paginator-counter { - border-start-end-radius: 1rem; - border-end-end-radius: 1rem; - } + &:has(a.paginator-item.next:active) .paginator-counter { + border-start-end-radius: 1rem; + border-end-end-radius: 1rem; + } - &-item { - display: inline-block; - transition: - border-radius var(--transition), - border-start-end-radius var(--transition), - border-end-end-radius var(--transition), - border-start-start-radius var(--transition), - border-end-start-radius var(--transition), - background-color var(--transition), - transform var(--transition-bezier); - box-shadow: var(--edge-highlight); - border-radius: 1rem; - background-color: var(--bg-muted-1); - padding: 0.5rem; - color: var(--accent-color); - line-height: 0; - text-decoration: none; + &-item { + display: inline-block; + transition: + border-radius var(--transition), + border-start-end-radius var(--transition), + border-end-end-radius var(--transition), + border-start-start-radius var(--transition), + border-end-start-radius var(--transition), + background-color var(--transition), + transform var(--transition-bezier); + box-shadow: var(--edge-highlight); + border-radius: 1rem; + background-color: var(--bg-muted-1); + padding: 0.5rem; + color: var(--accent-color); + line-height: 0; + text-decoration: none; - &:not([aria-disabled="true"]):hover { - background-color: var(--bg-muted-2); - } + &:not([aria-disabled="true"]):hover { + background-color: var(--bg-muted-2); + } - &:not([aria-disabled="true"]):active { - transform: var(--active); - border-radius: 1rem; - } + &:not([aria-disabled="true"]):active { + transform: var(--active); + border-radius: 1rem; + } - &-previous { - border-start-end-radius: var(--rounded-corner-small); - border-end-end-radius: var(--rounded-corner-small); - } + &-previous { + border-start-end-radius: var(--rounded-corner-small); + border-end-end-radius: var(--rounded-corner-small); + } - &-next { - border-start-start-radius: var(--rounded-corner-small); - border-end-start-radius: var(--rounded-corner-small); - } - } + &-next { + border-start-start-radius: var(--rounded-corner-small); + border-end-start-radius: var(--rounded-corner-small); + } + } - &-counter { - @include gradient-fill; - display: inline-block; - transition: var(--transition-bezier); - transition-property: - border-start-start-radius, border-end-start-radius, - border-start-end-radius, border-end-end-radius; - border-radius: var(--rounded-corner-small); - background-color: var(--bg-muted-1); - padding: 0.25rem 0.5rem; - font-weight: var(--font-weight-semi-bold); - font-variant-numeric: tabular-nums; - } + &-counter { + @include gradient-fill; + display: inline-block; + transition: var(--transition-bezier); + transition-property: + border-start-start-radius, + border-end-start-radius, + border-start-end-radius, + border-end-end-radius; + border-radius: var(--rounded-corner-small); + background-color: var(--bg-muted-1); + padding: 0.25rem 0.5rem; + font-weight: var(--font-weight-semi-bold); + font-variant-numeric: tabular-nums; + } } diff --git a/modules/amezing/sass/components/_article-nav.scss b/modules/amezing/sass/components/_article-nav.scss index 217e350..71ca968 100644 --- a/modules/amezing/sass/components/_article-nav.scss +++ b/modules/amezing/sass/components/_article-nav.scss @@ -1,66 +1,66 @@ .article-nav { - display: flex; - gap: 0.25rem; + display: flex; + gap: 0.25rem; - @media only screen and (max-width: 720px) { - flex-direction: column; - } + @media only screen and (max-width: 720px) { + flex-direction: column; + } - &-item { - flex: 1; - transition: - box-shadow var(--transition), - background-color var(--transition), - transform var(--transition-bezier); - border-radius: var(--rounded-corner); - padding: 0.75rem 1rem; - min-width: 0; - text-decoration: none; + &-item { + flex: 1; + transition: + box-shadow var(--transition), + background-color var(--transition), + transform var(--transition-bezier); + border-radius: var(--rounded-corner); + padding: 0.75rem 1rem; + min-width: 0; + text-decoration: none; - &:hover { - box-shadow: var(--edge-highlight), var(--shadow); - background-color: var(--bg-muted-1); + &:hover { + box-shadow: var(--edge-highlight), var(--shadow); + background-color: var(--bg-muted-1); - .article-title { - color: var(--accent-color); - } - } + .article-title { + color: var(--accent-color); + } + } - &:active { - transform: var(--active); - } + &:active { + transform: var(--active); + } - &.prev { - text-align: start; + &.prev { + text-align: start; - .nav-arrow::before { - content: "← "; - } - } + .nav-arrow::before { + content: "← "; + } + } - &.next { - text-align: end; + &.next { + text-align: end; - .nav-arrow::after { - content: " →"; - } - } - } + .nav-arrow::after { + content: " →"; + } + } + } - .nav-arrow { - margin-block-end: 0.5rem; - color: var(--fg-muted-2); - font-weight: var(--font-weight-normal); - font-size: var(--font-size-small); - } + .nav-arrow { + margin-block-end: 0.5rem; + color: var(--fg-muted-2); + font-weight: var(--font-weight-normal); + font-size: var(--font-size-small); + } - .article-title { - display: block; - transition: color var(--transition); - max-width: 100%; - overflow: hidden; - color: var(--fg-muted-1); - text-overflow: ellipsis; - white-space: nowrap; - } + .article-title { + display: block; + transition: color var(--transition); + max-width: 100%; + overflow: hidden; + color: var(--fg-muted-1); + text-overflow: ellipsis; + white-space: nowrap; + } } diff --git a/modules/amezing/sass/components/_audio-button.scss b/modules/amezing/sass/components/_audio-button.scss index 1f4fe43..56b3a81 100644 --- a/modules/amezing/sass/components/_audio-button.scss +++ b/modules/amezing/sass/components/_audio-button.scss @@ -2,41 +2,41 @@ @use "../citrine/abstracts/mixins" as *; .audio-button { - display: inline-flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 1rem; - border-radius: 0.5rem; - font-size: var(--font-size-small); - font-weight: var(--font-weight-semi-bold); - color: var(--accent-color); - background-color: var(--accent-bg-1); - border: none; - cursor: pointer; - transition: - background-color var(--transition), - transform var(--transition-bezier), - box-shadow var(--transition); + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + border-radius: 0.5rem; + font-size: var(--font-size-small); + font-weight: var(--font-weight-semi-bold); + color: var(--accent-color); + background-color: var(--accent-bg-1); + border: none; + cursor: pointer; + transition: + background-color var(--transition), + transform var(--transition-bezier), + box-shadow var(--transition); - &:hover { - background-color: var(--accent-bg-2); - transform: scale(1.03); - } + &:hover { + background-color: var(--accent-bg-2); + transform: scale(1.03); + } - &:active { - transform: var(--active); - } + &:active { + transform: var(--active); + } - &:focus-visible { - outline: 0.125rem solid var(--accent-color); - outline-offset: 0.25rem; - } + &:focus-visible { + outline: 0.125rem solid var(--accent-color); + outline-offset: 0.25rem; + } - .audio-button-label { - display: none; + .audio-button-label { + display: none; - @media (min-width: 480px) { - display: inline; - } - } + @media (min-width: 480px) { + display: inline; + } + } } diff --git a/modules/amezing/sass/components/_audio.scss b/modules/amezing/sass/components/_audio.scss index 18d8397..7b549a7 100644 --- a/modules/amezing/sass/components/_audio.scss +++ b/modules/amezing/sass/components/_audio.scss @@ -3,39 +3,39 @@ // Who doesn't like a party popper mid article? .audio { - @include chip; - padding-block: 0; - color: var(--accent-color); - font-size: var(--font-size-medium); + @include chip; + padding-block: 0; + color: var(--accent-color); + font-size: var(--font-size-medium); - &:hover { - color: var(--accent-color) !important; + &:hover { + color: var(--accent-color) !important; - .icon { - animation: speaker var(--transition-long) ease-out infinite; + .icon { + animation: speaker var(--transition-long) ease-out infinite; - @keyframes speaker { - from { - --icon: var(--icon-speaker-none); - } + @keyframes speaker { + from { + --icon: var(--icon-speaker-none); + } - 50% { - --icon: var(--icon-speaker-low); - } + 50% { + --icon: var(--icon-speaker-low); + } - to { - --icon: var(--icon-speaker-high); - } - } - } - } + to { + --icon: var(--icon-speaker-high); + } + } + } + } - &:active { - transform: var(--active); - } + &:active { + transform: var(--active); + } - .icon { - vertical-align: rpx(-3); - margin-inline-start: 0.25rem; - } + .icon { + vertical-align: rpx(-3); + margin-inline-start: 0.25rem; + } } diff --git a/modules/amezing/sass/components/_banner.scss b/modules/amezing/sass/components/_banner.scss index e261d07..d15fbd9 100644 --- a/modules/amezing/sass/components/_banner.scss +++ b/modules/amezing/sass/components/_banner.scss @@ -1,98 +1,98 @@ @use "../citrine/abstracts/functions" as *; body.has-banner { - background-image: var(--bg-overlay), var(--blurnail); - background-position: center; - background-size: cover; - background-attachment: fixed; + background-image: var(--bg-overlay), var(--blurnail); + background-position: center; + background-size: cover; + background-attachment: fixed; } #banner { - --mask: linear-gradient(black calc(100% - 2rem), transparent); - -webkit-mask-image: var(--mask); - display: flex; - grid-area: banner; - justify-content: center; - mask-image: var(--mask); - margin-block-end: -2.5rem; - padding-block: 3.25rem 2.5rem; - width: 100%; + --mask: linear-gradient(black calc(100% - 2rem), transparent); + -webkit-mask-image: var(--mask); + display: flex; + grid-area: banner; + justify-content: center; + mask-image: var(--mask); + margin-block-end: -2.5rem; + padding-block: 3.25rem 2.5rem; + width: 100%; - div { - position: relative; - margin-inline: 1rem; - aspect-ratio: 2 / 1; - width: 100%; - max-width: var(--content-width); - } + div { + position: relative; + margin-inline: 1rem; + aspect-ratio: 2 / 1; + width: 100%; + max-width: var(--content-width); + } - img { - position: absolute; - margin: 0; - inset: 0; - border-radius: var(--rounded-corner) var(--rounded-corner) 0 0 !important; - aspect-ratio: 2 / 1; - width: 100%; - object-fit: cover; - } + img { + position: absolute; + margin: 0; + inset: 0; + border-radius: var(--rounded-corner) var(--rounded-corner) 0 0 !important; + aspect-ratio: 2 / 1; + width: 100%; + object-fit: cover; + } - img:nth-of-type(1) { - opacity: var(--dim-opacity); - z-index: -1; - filter: blur(1.5rem); - animation: banner-glow-load calc(0.525s + var(--transition-long)); - inset-block-start: 1rem; + img:nth-of-type(1) { + opacity: var(--dim-opacity); + z-index: -1; + filter: blur(1.5rem); + animation: banner-glow-load calc(0.525s + var(--transition-long)); + inset-block-start: 1rem; - @keyframes banner-glow-load { - from, - 52.5% { - opacity: 0; - } - } - } + @keyframes banner-glow-load { + from, + 52.5% { + opacity: 0; + } + } + } - img:nth-of-type(2) { - transform-origin: bottom; - animation: banner-load calc(0.525s + var(--transition-longer)); + img:nth-of-type(2) { + transform-origin: bottom; + animation: banner-load calc(0.525s + var(--transition-longer)); - @keyframes banner-load { - from, - 52.5% { - transform: translateY(2rem) scale(0.95); - opacity: 0; - } - } - } + @keyframes banner-load { + from, + 52.5% { + transform: translateY(2rem) scale(0.95); + opacity: 0; + } + } + } - & + #main-content { - margin-block-start: 0; + & + #main-content { + margin-block-start: 0; - article > h1:first-of-type, - h1:first-of-type { - margin-inline: -1rem; - margin-block: 0; - border-radius: var(--rounded-corner) var(--rounded-corner) 0 0; - padding: 1rem; - width: unset; - -webkit-backdrop-filter: blur(1rem); - backdrop-filter: blur(1rem); - animation: banner-heading calc(0.525s + var(--transition-longer)); - box-shadow: inset 0 rpx(1) 0 var(--accent-highlight); - background-image: linear-gradient( - to bottom, - var(--bg-muted-1), - transparent 50% - ); - background-size: auto 200%; - text-decoration: none; + article > h1:first-of-type, + h1:first-of-type { + margin-inline: -1rem; + margin-block: 0; + border-radius: var(--rounded-corner) var(--rounded-corner) 0 0; + padding: 1rem; + width: unset; + -webkit-backdrop-filter: blur(1rem); + backdrop-filter: blur(1rem); + animation: banner-heading calc(0.525s + var(--transition-longer)); + box-shadow: inset 0 rpx(1) 0 var(--accent-highlight); + background-image: linear-gradient( + to bottom, + var(--bg-muted-1), + transparent 50% + ); + background-size: auto 200%; + text-decoration: none; - @keyframes banner-heading { - from, - 52.5% { - box-shadow: none; - background-position-y: 100%; - } - } - } - } + @keyframes banner-heading { + from, + 52.5% { + box-shadow: none; + background-position-y: 100%; + } + } + } + } } diff --git a/modules/amezing/sass/components/_comments.scss b/modules/amezing/sass/components/_comments.scss index e5cc946..be2f364 100644 --- a/modules/amezing/sass/components/_comments.scss +++ b/modules/amezing/sass/components/_comments.scss @@ -2,458 +2,458 @@ @use "../citrine/abstracts/mixins" as *; #comments { - #qrcode { - float: inline-end; - cursor: help; - box-sizing: content-box; - margin-inline-start: 1rem; - margin-block-start: 3rem; - box-shadow: var(--edge-highlight), var(--shadow); - border-radius: var(--rounded-corner); - background-color: white; - padding: 0.75rem; - width: rpx(116); - height: rpx(116); - - svg { - pointer-events: none; - } - - @media only screen and (max-width: 720px) { - display: none; - } - } - - #comments-wrapper { - display: flex; - flex-direction: column; - gap: 2rem; - margin-block-start: 2rem; - - #comments-status { - color: var(--fg-muted-1); - font-weight: var(--font-weight-semi-bold); - font-size: var(--font-size-x-large); - text-align: center; - } - - .comment { - display: grid; - // grid-template-columns: min-content; - grid-template-columns: auto 1fr; - grid-template-areas: - "avatar header " - "...... post " - "...... media " - "...... card " - "...... interactions"; - column-gap: 1rem; - justify-items: start; - animation: slide-in-right var(--transition-bezier); - - :root[dir*="rtl"] & { - animation: slide-in-left var(--transition-bezier); - } - - @media only screen and (max-width: 720px) { - grid-template-areas: - "avatar header " - "post post " - "media media " - "card card " - "interactions interactions"; - align-items: start; - } - - &.comment-reply { - position: relative; - border-inline-start: 0.25rem solid var(--bg-muted-2); - border-start-start-radius: 0.25rem; - border-end-start-radius: 0.25rem; - padding-inline-start: 1rem; - - &:has(+ .comment-reply) { - border-end-start-radius: 0; - } - - & + .comment-reply { - margin-block-start: -2rem; - border-start-start-radius: 0; - padding-block-start: 2rem; - } - } - - .avatar-link { - grid-area: avatar; - width: 4rem; - height: 4rem; - - @media only screen and (max-width: 480px) { - width: 3rem; - height: 3rem; - } - - .avatar { - transition: transform var(--transition-bezier); - margin: 0; - background-size: cover; - width: 100%; - height: 100%; - - &:hover { - transform: rotate(10deg) var(--hover); - border-radius: var(--rounded-corner); - } - - &:active { - transform: var(--active); - } - } - } - - .mention { - @include chip; - display: inline-block; - - &:hover { - background-color: var(--bg-muted-2); - } - - &:active { - transform: var(--active); - } - } - - .invisible { - display: none; - } - - .ellipsis::after { - content: "…"; - } - - header { - display: flex; - grid-area: header; - flex-direction: column; - width: 100%; - min-width: 0; - color: var(--fg-muted-1); - line-height: 1; - - .display, - .instance { - display: block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - .display { - color: var(--fg-color); - line-height: 1.5rem; - text-decoration: none; - - .emoji { - cursor: unset; - - &:hover { - transform: none; - } - } - } - - .instance { - margin-block-start: 0.25rem; - font-weight: var(--font-weight-semi-bold); - font-size: var(--font-size-x-small); - - &.op { - cursor: help; - - &::before { - @include icon(pseudo); - --icon: var(--icon-seal-check); - display: inline-block; - vertical-align: text-top; - margin-inline-end: 0.25rem; - font-size: 1rem; - } - } - } - - .timestamp { - margin-block-start: 0.25rem; - - a { - color: inherit; - font-size: var(--font-size-x-small); - text-decoration-color: transparent; - - &:hover { - text-decoration-style: solid; - text-decoration-color: inherit; - } - - &::after { - vertical-align: top; - } - } - } - } - - details { - border-radius: var(--rounded-corner-small); - background-image: - linear-gradient( - to right, - transparent, - transparent 0.5rem, - var(--bg-muted-1) 0.5rem, - var(--bg-muted-1) calc(100% - 0.5rem), - transparent calc(100% - 0.5rem), - transparent - ), - linear-gradient( - to right, - transparent, - transparent 0.5rem, - var(--bg-color) 0.5rem, - var(--bg-color) calc(100% - 0.5rem), - transparent calc(100% - 0.5rem), - transparent - ), - repeating-linear-gradient( - 45deg, - var(--contrast-color), - var(--contrast-color) 0.25rem, - var(--accent-color) 0.25rem, - var(--accent-color) 0.5rem - ); - width: 100%; - - summary { - border-radius: 0; - background-image: none; - } - } - - main { - grid-area: post; - justify-self: stretch; - margin-block-start: 1rem; - padding: 0; - - :first-child { - margin-block-start: 0; - } - - :last-child { - margin-block-end: 0; - } - } - - .attachments { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); - grid-area: media; - gap: 0.5rem; - margin-block-start: 1rem; - - img, - video { - margin: 0; - } - } - - .meta-card { - grid-area: card; - transition: transform var(--transition-bezier); - margin-block-start: 1rem; - width: min(calc(var(--content-width) / 2), 100%); - font-weight: var(--font-weight-normal); - text-decoration: none; - - &:hover { - img { - transform: var(--hover); - box-shadow: var(--edge-highlight), var(--shadow-raised); - border-radius: var(--rounded-corner-small); - } - - figcaption { - border-radius: var(--rounded-corner); - background-color: var(--bg-muted-2); - } - } - - &:active { - transform: var(--active); - } - - figure { - display: flex; - flex-direction: column; - gap: 0.25rem; - margin: 0; - - img { - margin: 0; - border-radius: var(--rounded-corner) var(--rounded-corner) - var(--rounded-corner-small) var(--rounded-corner-small); - aspect-ratio: 16 / 9; - object-fit: cover; - - & + figcaption { - border-radius: var(--rounded-corner-small) - var(--rounded-corner-small) var(--rounded-corner) - var(--rounded-corner); - } - } - - figcaption { - display: flex; - flex-direction: column; - gap: 0.25rem; - transition-duration: var(--transition); - transition-property: border-radius, background-color; - box-shadow: var(--edge-highlight), var(--shadow); - border-radius: var(--rounded-corner); - background-color: var(--bg-muted-1); - padding: 1rem; - color: var(--fg-color); - font-size: var(--font-size-medium); - text-align: start; - - p { - margin: 0; - color: var(--fg-muted-2); - font-size: var(--font-size-small); - } - } - } - } - - footer { - display: flex; - grid-area: interactions; - align-items: center; - gap: 0.25rem; - margin-block-start: 1rem; - width: 100%; - min-width: 0; - - @media only screen and (max-width: 480px) { - flex-wrap: wrap; - } - - .boosts, - .faves, - .reaction { - padding: 0.375rem 0.75rem; - font-variant-numeric: tabular-nums; - white-space: nowrap; - } - - .boosts, - .faves { - transition: - box-shadow var(--transition), - border-radius var(--transition), - transform var(--transition-bezier); - border-radius: rpx(18); - text-decoration: none; - - .icon { - vertical-align: rpx(-2); - transition: transform var(--transition-bezier); - margin-inline-end: 0.25rem; - } - - &:hover { - box-shadow: var(--edge-highlight); - text-decoration: none; - } - - &:active { - transform: var(--active); - border-radius: rpx(18); - } - } - - .boosts { - border-start-end-radius: var(--rounded-corner-small); - border-end-end-radius: var(--rounded-corner-small); - padding-inline-end: 0.625rem; - color: var(--purple-fg); - - .icon { - --icon: var(--icon-repeat); - } - - &:hover { - background-color: var(--purple-bg-1); - - .icon { - transform: rotate(180deg); - } - } - } - - .faves { - border-start-start-radius: var(--rounded-corner-small); - border-end-start-radius: var(--rounded-corner-small); - padding-inline-start: 0.625rem; - color: var(--yellow-fg); - - .icon { - --icon: var(--icon-star); - } - - &:hover { - background-color: var(--yellow-bg-1); - - .icon { - transform: rotate(72deg); - } - } - } - - .reactions { - display: flex; - // flex-wrap: nowrap; - gap: 0.25rem; - margin: -0.5rem -1rem; - padding: 0.5rem 1rem; - min-width: 0; - overflow-x: auto; - scrollbar-width: none; - color: var(--accent-color); - font-weight: var(--font-weight-semi-bold); - - .reaction { - display: flex; - flex-shrink: 0; - align-items: center; - gap: 0.25rem; - cursor: zoom-in; - border: rpx(1) solid var(--bg-muted-2); - border-radius: rpx(19); - - &:hover .emoji { - transform: scale(2); - } - - .emoji { - cursor: unset; - } - - span { - font-variant-numeric: tabular-nums; - } - } - } - } - } - } + #qrcode { + float: inline-end; + cursor: help; + box-sizing: content-box; + margin-inline-start: 1rem; + margin-block-start: 3rem; + box-shadow: var(--edge-highlight), var(--shadow); + border-radius: var(--rounded-corner); + background-color: white; + padding: 0.75rem; + width: rpx(116); + height: rpx(116); + + svg { + pointer-events: none; + } + + @media only screen and (max-width: 720px) { + display: none; + } + } + + #comments-wrapper { + display: flex; + flex-direction: column; + gap: 2rem; + margin-block-start: 2rem; + + #comments-status { + color: var(--fg-muted-1); + font-weight: var(--font-weight-semi-bold); + font-size: var(--font-size-x-large); + text-align: center; + } + + .comment { + display: grid; + // grid-template-columns: min-content; + grid-template-columns: auto 1fr; + grid-template-areas: + "avatar header " + "...... post " + "...... media " + "...... card " + "...... interactions"; + column-gap: 1rem; + justify-items: start; + animation: slide-in-right var(--transition-bezier); + + :root[dir*="rtl"] & { + animation: slide-in-left var(--transition-bezier); + } + + @media only screen and (max-width: 720px) { + grid-template-areas: + "avatar header " + "post post " + "media media " + "card card " + "interactions interactions"; + align-items: start; + } + + &.comment-reply { + position: relative; + border-inline-start: 0.25rem solid var(--bg-muted-2); + border-start-start-radius: 0.25rem; + border-end-start-radius: 0.25rem; + padding-inline-start: 1rem; + + &:has(+ .comment-reply) { + border-end-start-radius: 0; + } + + & + .comment-reply { + margin-block-start: -2rem; + border-start-start-radius: 0; + padding-block-start: 2rem; + } + } + + .avatar-link { + grid-area: avatar; + width: 4rem; + height: 4rem; + + @media only screen and (max-width: 480px) { + width: 3rem; + height: 3rem; + } + + .avatar { + transition: transform var(--transition-bezier); + margin: 0; + background-size: cover; + width: 100%; + height: 100%; + + &:hover { + transform: rotate(10deg) var(--hover); + border-radius: var(--rounded-corner); + } + + &:active { + transform: var(--active); + } + } + } + + .mention { + @include chip; + display: inline-block; + + &:hover { + background-color: var(--bg-muted-2); + } + + &:active { + transform: var(--active); + } + } + + .invisible { + display: none; + } + + .ellipsis::after { + content: "…"; + } + + header { + display: flex; + grid-area: header; + flex-direction: column; + width: 100%; + min-width: 0; + color: var(--fg-muted-1); + line-height: 1; + + .display, + .instance { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .display { + color: var(--fg-color); + line-height: 1.5rem; + text-decoration: none; + + .emoji { + cursor: unset; + + &:hover { + transform: none; + } + } + } + + .instance { + margin-block-start: 0.25rem; + font-weight: var(--font-weight-semi-bold); + font-size: var(--font-size-x-small); + + &.op { + cursor: help; + + &::before { + @include icon(pseudo); + --icon: var(--icon-seal-check); + display: inline-block; + vertical-align: text-top; + margin-inline-end: 0.25rem; + font-size: 1rem; + } + } + } + + .timestamp { + margin-block-start: 0.25rem; + + a { + color: inherit; + font-size: var(--font-size-x-small); + text-decoration-color: transparent; + + &:hover { + text-decoration-style: solid; + text-decoration-color: inherit; + } + + &::after { + vertical-align: top; + } + } + } + } + + details { + border-radius: var(--rounded-corner-small); + background-image: + linear-gradient( + to right, + transparent, + transparent 0.5rem, + var(--bg-muted-1) 0.5rem, + var(--bg-muted-1) calc(100% - 0.5rem), + transparent calc(100% - 0.5rem), + transparent + ), + linear-gradient( + to right, + transparent, + transparent 0.5rem, + var(--bg-color) 0.5rem, + var(--bg-color) calc(100% - 0.5rem), + transparent calc(100% - 0.5rem), + transparent + ), + repeating-linear-gradient( + 45deg, + var(--contrast-color), + var(--contrast-color) 0.25rem, + var(--accent-color) 0.25rem, + var(--accent-color) 0.5rem + ); + width: 100%; + + summary { + border-radius: 0; + background-image: none; + } + } + + main { + grid-area: post; + justify-self: stretch; + margin-block-start: 1rem; + padding: 0; + + :first-child { + margin-block-start: 0; + } + + :last-child { + margin-block-end: 0; + } + } + + .attachments { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); + grid-area: media; + gap: 0.5rem; + margin-block-start: 1rem; + + img, + video { + margin: 0; + } + } + + .meta-card { + grid-area: card; + transition: transform var(--transition-bezier); + margin-block-start: 1rem; + width: min(calc(var(--content-width) / 2), 100%); + font-weight: var(--font-weight-normal); + text-decoration: none; + + &:hover { + img { + transform: var(--hover); + box-shadow: var(--edge-highlight), var(--shadow-raised); + border-radius: var(--rounded-corner-small); + } + + figcaption { + border-radius: var(--rounded-corner); + background-color: var(--bg-muted-2); + } + } + + &:active { + transform: var(--active); + } + + figure { + display: flex; + flex-direction: column; + gap: 0.25rem; + margin: 0; + + img { + margin: 0; + border-radius: var(--rounded-corner) var(--rounded-corner) + var(--rounded-corner-small) var(--rounded-corner-small); + aspect-ratio: 16 / 9; + object-fit: cover; + + & + figcaption { + border-radius: var(--rounded-corner-small) + var(--rounded-corner-small) var(--rounded-corner) + var(--rounded-corner); + } + } + + figcaption { + display: flex; + flex-direction: column; + gap: 0.25rem; + transition-duration: var(--transition); + transition-property: border-radius, background-color; + box-shadow: var(--edge-highlight), var(--shadow); + border-radius: var(--rounded-corner); + background-color: var(--bg-muted-1); + padding: 1rem; + color: var(--fg-color); + font-size: var(--font-size-medium); + text-align: start; + + p { + margin: 0; + color: var(--fg-muted-2); + font-size: var(--font-size-small); + } + } + } + } + + footer { + display: flex; + grid-area: interactions; + align-items: center; + gap: 0.25rem; + margin-block-start: 1rem; + width: 100%; + min-width: 0; + + @media only screen and (max-width: 480px) { + flex-wrap: wrap; + } + + .boosts, + .faves, + .reaction { + padding: 0.375rem 0.75rem; + font-variant-numeric: tabular-nums; + white-space: nowrap; + } + + .boosts, + .faves { + transition: + box-shadow var(--transition), + border-radius var(--transition), + transform var(--transition-bezier); + border-radius: rpx(18); + text-decoration: none; + + .icon { + vertical-align: rpx(-2); + transition: transform var(--transition-bezier); + margin-inline-end: 0.25rem; + } + + &:hover { + box-shadow: var(--edge-highlight); + text-decoration: none; + } + + &:active { + transform: var(--active); + border-radius: rpx(18); + } + } + + .boosts { + border-start-end-radius: var(--rounded-corner-small); + border-end-end-radius: var(--rounded-corner-small); + padding-inline-end: 0.625rem; + color: var(--purple-fg); + + .icon { + --icon: var(--icon-repeat); + } + + &:hover { + background-color: var(--purple-bg-1); + + .icon { + transform: rotate(180deg); + } + } + } + + .faves { + border-start-start-radius: var(--rounded-corner-small); + border-end-start-radius: var(--rounded-corner-small); + padding-inline-start: 0.625rem; + color: var(--yellow-fg); + + .icon { + --icon: var(--icon-star); + } + + &:hover { + background-color: var(--yellow-bg-1); + + .icon { + transform: rotate(72deg); + } + } + } + + .reactions { + display: flex; + // flex-wrap: nowrap; + gap: 0.25rem; + margin: -0.5rem -1rem; + padding: 0.5rem 1rem; + min-width: 0; + overflow-x: auto; + scrollbar-width: none; + color: var(--accent-color); + font-weight: var(--font-weight-semi-bold); + + .reaction { + display: flex; + flex-shrink: 0; + align-items: center; + gap: 0.25rem; + cursor: zoom-in; + border: rpx(1) solid var(--bg-muted-2); + border-radius: rpx(19); + + &:hover .emoji { + transform: scale(2); + } + + .emoji { + cursor: unset; + } + + span { + font-variant-numeric: tabular-nums; + } + } + } + } + } + } } diff --git a/modules/amezing/sass/components/_deanchor.scss b/modules/amezing/sass/components/_deanchor.scss index 1fd7c89..b378a4a 100644 --- a/modules/amezing/sass/components/_deanchor.scss +++ b/modules/amezing/sass/components/_deanchor.scss @@ -2,19 +2,17 @@ @use "../citrine/abstracts/mixins" as *; .deanchor { - display: inline; - font-family: var(--font-family-mono); - font-size: var(--font-size-small); - color: var(--fg-muted-2); - background-color: var(--bg-muted-1); - padding: 0.125rem 0.375rem; - border-radius: 0.25rem; - transition: - background-color var(--transition), - color var(--transition); + display: inline; + font-family: var(--font-family-mono); + font-size: var(--font-size-small); + color: var(--fg-muted-2); + background-color: var(--bg-muted-1); + padding: 0.125rem 0.375rem; + border-radius: 0.25rem; + transition: background-color var(--transition), color var(--transition); - &:hover { - background-color: var(--bg-muted-2); - color: var(--fg-muted-1); - } + &:hover { + background-color: var(--bg-muted-2); + color: var(--fg-muted-1); + } } diff --git a/modules/amezing/sass/components/_emoji.scss b/modules/amezing/sass/components/_emoji.scss index 93570ae..4c39a3a 100644 --- a/modules/amezing/sass/components/_emoji.scss +++ b/modules/amezing/sass/components/_emoji.scss @@ -1,21 +1,21 @@ .emoji { - display: inline-block; - vertical-align: bottom; - transition: transform var(--transition-bezier); - cursor: zoom-in; - margin: 0; - box-shadow: none; - border-radius: 0; - background-color: transparent; - width: 1.5rem; - height: 1.5rem; + display: inline-block; + vertical-align: bottom; + transition: transform var(--transition-bezier); + cursor: zoom-in; + margin: 0; + box-shadow: none; + border-radius: 0; + background-color: transparent; + width: 1.5rem; + height: 1.5rem; - &:hover { - transform: scale(2); - } + &:hover { + transform: scale(2); + } - &.big { - width: 3rem; - height: 3rem; - } + &.big { + width: 3rem; + height: 3rem; + } } diff --git a/modules/amezing/sass/components/_footnotes.scss b/modules/amezing/sass/components/_footnotes.scss index c083897..d149cd3 100644 --- a/modules/amezing/sass/components/_footnotes.scss +++ b/modules/amezing/sass/components/_footnotes.scss @@ -2,61 +2,61 @@ @use "../citrine/abstracts/mixins" as *; .footnotes-list p { - margin-block: 0; + margin-block: 0; } .footnote-reference { - display: inline-flex; - vertical-align: baseline; - transition: - background-color var(--transition), - outline-offset calc(var(--transition-long) * 3), - transform var(--transition-bezier); - margin-inline-start: 0.25rem; - outline-offset: calc(var(--content-width) / 2) !important; - border-radius: 0.6875em; - background-color: var(--fg-muted-1); - // width: 1.375em; - line-height: 1.375em; + display: inline-flex; + vertical-align: baseline; + transition: + background-color var(--transition), + outline-offset calc(var(--transition-long) * 3), + transform var(--transition-bezier); + margin-inline-start: 0.25rem; + outline-offset: calc(var(--content-width) / 2) !important; + border-radius: 0.6875em; + background-color: var(--fg-muted-1); + // width: 1.375em; + line-height: 1.375em; - &:hover { - background-color: var(--accent-color); + &:hover { + background-color: var(--accent-color); - a::after { - mask-position: center rpx(18); - } - } + a::after { + mask-position: center rpx(18); + } + } - &:active { - transform: var(--active); - } + &:active { + transform: var(--active); + } - &:target { - animation: none !important; - outline-offset: rpx(-3) !important; - background-color: var(--accent-color); - } + &:target { + animation: none !important; + outline-offset: rpx(-3) !important; + background-color: var(--accent-color); + } - a { - flex: 1; - border-radius: inherit; - padding-inline: rpx(9) rpx(6); - color: var(--bg-color); - font-size: var(--font-size-small); - font-variant-numeric: tabular-nums; - text-align: center; - text-decoration: none; + a { + flex: 1; + border-radius: inherit; + padding-inline: rpx(9) rpx(6); + color: var(--bg-color); + font-size: var(--font-size-small); + font-variant-numeric: tabular-nums; + text-align: center; + text-decoration: none; - &::after { - @include icon(pseudo); - --icon: var(--icon-caret-down); - display: inline-block; - vertical-align: rpx(-7); - mask-position: center; - mask-size: 1em; - transition: mask-position var(--transition); - margin-inline-start: 0.25rem; - height: rpx(22); - } - } + &::after { + @include icon(pseudo); + --icon: var(--icon-caret-down); + display: inline-block; + vertical-align: rpx(-7); + mask-position: center; + mask-size: 1em; + transition: mask-position var(--transition); + margin-inline-start: 0.25rem; + height: rpx(22); + } + } } diff --git a/modules/amezing/sass/components/_fuse.scss b/modules/amezing/sass/components/_fuse.scss index 494ef74..19f85d0 100644 --- a/modules/amezing/sass/components/_fuse.scss +++ b/modules/amezing/sass/components/_fuse.scss @@ -2,49 +2,47 @@ @use "../citrine/abstracts/mixins" as *; .fuse { - display: flex; - flex-direction: column; - gap: 0.5rem; - max-width: 30rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + max-width: 30rem; - .fuse-input { - width: 100%; - padding: 0.75rem 1rem; - border-radius: 0.5rem; - border: 1px solid var(--bg-muted-2); - background-color: var(--bg-muted-1); - color: var(--fg-color); - font-size: var(--font-size-medium); - transition: - border-color var(--transition), - box-shadow var(--transition); + .fuse-input { + width: 100%; + padding: 0.75rem 1rem; + border-radius: 0.5rem; + border: 1px solid var(--bg-muted-2); + background-color: var(--bg-muted-1); + color: var(--fg-color); + font-size: var(--font-size-medium); + transition: border-color var(--transition), box-shadow var(--transition); - &:focus { - outline: none; - border-color: var(--accent-color); - box-shadow: 0 0 0 2px var(--accent-bg-1), 0 0 0 4px var(--accent-color); - } - } + &:focus { + outline: none; + border-color: var(--accent-color); + box-shadow: 0 0 0 2px var(--accent-bg-1), 0 0 0 4px var(--accent-color); + } + } - .fuse-results { - display: flex; - flex-direction: column; - gap: 0.25rem; + .fuse-results { + display: flex; + flex-direction: column; + gap: 0.25rem; - .fuse-result { - padding: 0.5rem 0.75rem; - border-radius: 0.25rem; - background-color: var(--bg-muted-1); - text-decoration: none; - color: var(--fg-color); - transition: - background-color var(--transition), - padding-left var(--transition); + .fuse-result { + padding: 0.5rem 0.75rem; + border-radius: 0.25rem; + background-color: var(--bg-muted-1); + text-decoration: none; + color: var(--fg-color); + transition: + background-color var(--transition), + padding-left var(--transition); - &:hover { - background-color: var(--bg-muted-2); - padding-left: 1rem; - } - } - } + &:hover { + background-color: var(--bg-muted-2); + padding-left: 1rem; + } + } + } } diff --git a/modules/amezing/sass/components/_giallo.scss b/modules/amezing/sass/components/_giallo.scss index 0c228b4..85a1a83 100644 --- a/modules/amezing/sass/components/_giallo.scss +++ b/modules/amezing/sass/components/_giallo.scss @@ -1,16 +1,16 @@ .giallo:has(.giallo-ln) { - padding-inline: 0; + padding-inline: 0; } .giallo-l { - display: inline-block; - width: 100%; + display: inline-block; + width: 100%; } .giallo-ln { - display: inline-block; - margin-right: 1ch; - min-width: 3ch; - user-select: none; - text-align: right; + display: inline-block; + margin-right: 1ch; + min-width: 3ch; + user-select: none; + text-align: right; } diff --git a/modules/amezing/sass/components/_icon.scss b/modules/amezing/sass/components/_icon.scss index 6768a12..b0e7def 100644 --- a/modules/amezing/sass/components/_icon.scss +++ b/modules/amezing/sass/components/_icon.scss @@ -1,9 +1,9 @@ @use "../citrine/abstracts/mixins" as *; .icon { - @include icon; + @include icon; - &::before { - @include icon(pseudo); - } + &::before { + @include icon(pseudo); + } } diff --git a/modules/amezing/sass/components/_iframe.scss b/modules/amezing/sass/components/_iframe.scss index 550da8d..fb57f31 100644 --- a/modules/amezing/sass/components/_iframe.scss +++ b/modules/amezing/sass/components/_iframe.scss @@ -1,6 +1,6 @@ iframe { - &.vimeo-embed, - &.youtube-embed { - aspect-ratio: 16 / 9; - } + &.vimeo-embed, + &.youtube-embed { + aspect-ratio: 16 / 9; + } } diff --git a/modules/amezing/sass/components/_index.scss b/modules/amezing/sass/components/_index.scss index 61889d1..d63d4d8 100644 --- a/modules/amezing/sass/components/_index.scss +++ b/modules/amezing/sass/components/_index.scss @@ -22,4 +22,4 @@ @use "audio-button"; @use "deanchor"; @use "fuse"; -@use "search-fuse"; \ No newline at end of file +@use "search-fuse"; diff --git a/modules/amezing/sass/components/_marquee.scss b/modules/amezing/sass/components/_marquee.scss index e995c33..34ad15b 100644 --- a/modules/amezing/sass/components/_marquee.scss +++ b/modules/amezing/sass/components/_marquee.scss @@ -1,35 +1,35 @@ .marquee { - margin-inline: -1rem; - padding-inline: 1rem; - overflow: hidden; + margin-inline: -1rem; + padding-inline: 1rem; + overflow: hidden; - &.overshoot-row > * { - animation: marquee-scroll - calc((var(--text-width) + var(--container-width)) / 40 * 1s) linear - infinite; + &.overshoot-row > * { + animation: marquee-scroll + calc((var(--text-width) + var(--container-width)) / 40 * 1s) linear + infinite; - @keyframes marquee-scroll { - 0%, - 10% { - transform: translateX(0); - } + @keyframes marquee-scroll { + 0%, + 10% { + transform: translateX(0); + } - 45%, - 55% { - transform: translateX( - calc((var(--container-width) - var(--text-width)) * 1px) - ); - } + 45%, + 55% { + transform: translateX( + calc((var(--container-width) - var(--text-width)) * 1px) + ); + } - 90%, - 100% { - transform: translateX(0); - } - } - } + 90%, + 100% { + transform: translateX(0); + } + } + } - & > * { - display: inline-block; - white-space: nowrap; - } + & > * { + display: inline-block; + white-space: nowrap; + } } diff --git a/modules/amezing/sass/components/_not-found.scss b/modules/amezing/sass/components/_not-found.scss index 34b92bf..6cc76b3 100644 --- a/modules/amezing/sass/components/_not-found.scss +++ b/modules/amezing/sass/components/_not-found.scss @@ -1,11 +1,11 @@ picture#not-found { - display: block; + display: block; - img { - width: 8rem; - } + img { + width: 8rem; + } } img#not-found { - width: 8rem; + width: 8rem; } diff --git a/modules/amezing/sass/components/_pre-container.scss b/modules/amezing/sass/components/_pre-container.scss index d011d77..e553914 100644 --- a/modules/amezing/sass/components/_pre-container.scss +++ b/modules/amezing/sass/components/_pre-container.scss @@ -1,141 +1,141 @@ @use "../citrine/abstracts/functions" as *; .pre-container { - position: relative; - margin: 1rem 0 1rem; - box-shadow: var(--shadow); - border-radius: var(--rounded-corner); + position: relative; + margin: 1rem 0 1rem; + box-shadow: var(--shadow); + border-radius: var(--rounded-corner); - &::after { - position: absolute; - inset: 0; - box-shadow: var(--edge-highlight); - border-radius: inherit; - pointer-events: none; - content: ""; - } + &::after { + position: absolute; + inset: 0; + box-shadow: var(--edge-highlight); + border-radius: inherit; + pointer-events: none; + content: ""; + } - .header { - display: flex; - justify-content: space-between; - align-items: center; - transition: border-block-end-color var(--transition); - border-radius: var(--rounded-corner) var(--rounded-corner) 0 0; - border-block-end: rpx(1) solid transparent; - background-image: linear-gradient( - to right, - transparent 50%, - var(--bg-muted-neutral-1) 75%, - transparent 100% - ); - background-size: 200%; - padding: 0.25rem; + .header { + display: flex; + justify-content: space-between; + align-items: center; + transition: border-block-end-color var(--transition); + border-radius: var(--rounded-corner) var(--rounded-corner) 0 0; + border-block-end: rpx(1) solid transparent; + background-image: linear-gradient( + to right, + transparent 50%, + var(--bg-muted-neutral-1) 75%, + transparent 100% + ); + background-size: 200%; + padding: 0.25rem; - &:not(.z-l-code), - &:not(.z-d-code) { - background-color: var(--bg-muted-1); - color: var(--accent-color); + &:not(.z-l-code), + &:not(.z-d-code) { + background-color: var(--bg-muted-1); + color: var(--accent-color); - button:hover { - background-color: var(--bg-muted-1); - } + button:hover { + background-color: var(--bg-muted-1); + } - &.active { - border-block-end-color: var(--bg-muted-1); + &.active { + border-block-end-color: var(--bg-muted-1); - button { - background-color: var(--bg-muted-1); - } - } - } + button { + background-color: var(--bg-muted-1); + } + } + } - span { - margin-inline-start: 0.75rem; - font-weight: var(--font-weight-semi-bold); - } + span { + margin-inline-start: 0.75rem; + font-weight: var(--font-weight-semi-bold); + } - button { - position: relative; - transition: - box-shadow var(--transition), - background-color var(--transition), - transform var(--transition-bezier); - cursor: pointer; - box-shadow: none; - border-radius: var(--rounded-corner-small); - background-color: transparent; - padding: 0.5rem; - color: inherit; - line-height: 0; + button { + position: relative; + transition: + box-shadow var(--transition), + background-color var(--transition), + transform var(--transition-bezier); + cursor: pointer; + box-shadow: none; + border-radius: var(--rounded-corner-small); + background-color: transparent; + padding: 0.5rem; + color: inherit; + line-height: 0; - &:hover { - box-shadow: var(--edge-highlight); - background-color: var(--bg-muted-neutral-1); - } + &:hover { + box-shadow: var(--edge-highlight); + background-color: var(--bg-muted-neutral-1); + } - &:active { - transform: var(--active); - } + &:active { + transform: var(--active); + } - &:disabled { - cursor: not-allowed; + &:disabled { + cursor: not-allowed; - &:active { - transform: none; - } - } + &:active { + transform: none; + } + } - .icon { - transition: - opacity var(--transition-longer), - transform var(--transition-bezier-longer); - font-size: 1rem; + .icon { + transition: + opacity var(--transition-longer), + transform var(--transition-bezier-longer); + font-size: 1rem; - &.copy { - --icon: var(--icon-copy); - transform-origin: bottom left; - } + &.copy { + --icon: var(--icon-copy); + transform-origin: bottom left; + } - &.check { - --icon: var(--icon-check-square-offset); - position: absolute; - transform: scale(0.5); - transform-origin: top right; - opacity: 0; - inset: 0.5rem; - } - } - } + &.check { + --icon: var(--icon-check-square-offset); + position: absolute; + transform: scale(0.5); + transform-origin: top right; + opacity: 0; + inset: 0.5rem; + } + } + } - &.active { - animation: active-shimmer var(--transition-long); - border-block-end-color: var(--bg-muted-neutral-1); + &.active { + animation: active-shimmer var(--transition-long); + border-block-end-color: var(--bg-muted-neutral-1); - button { - opacity: 1; + button { + opacity: 1; - .icon.copy { - transform: scale(0.5); - opacity: 0; - } + .icon.copy { + transform: scale(0.5); + opacity: 0; + } - .icon.check { - transform: none; - opacity: 1; - } - } + .icon.check { + transform: none; + opacity: 1; + } + } - @keyframes active-shimmer { - to { - background-position-x: -200%; - } - } - } - } + @keyframes active-shimmer { + to { + background-position-x: -200%; + } + } + } + } - pre { - margin: 0; - box-shadow: none; - border-radius: 0 0 var(--rounded-corner) var(--rounded-corner); - } + pre { + margin: 0; + box-shadow: none; + border-radius: 0 0 var(--rounded-corner) var(--rounded-corner); + } } diff --git a/modules/amezing/sass/components/_search-fuse.scss b/modules/amezing/sass/components/_search-fuse.scss index 20bb5c2..c20e120 100644 --- a/modules/amezing/sass/components/_search-fuse.scss +++ b/modules/amezing/sass/components/_search-fuse.scss @@ -2,72 +2,70 @@ @use "../citrine/abstracts/mixins" as *; .search-fuse { - display: flex; - flex-direction: column; - gap: 0.75rem; - max-width: 40rem; - margin: 0 auto; + display: flex; + flex-direction: column; + gap: 0.75rem; + max-width: 40rem; + margin: 0 auto; - &-input-wrap { - position: relative; - display: flex; - align-items: center; - } + &-input-wrap { + position: relative; + display: flex; + align-items: center; + } - &-icon { - position: absolute; - left: 0.75rem; - color: var(--fg-muted-2); - pointer-events: none; - transition: - color var(--transition), - opacity var(--transition-bezier); - opacity: 0.7; - } + &-icon { + position: absolute; + left: 0.75rem; + color: var(--fg-muted-2); + pointer-events: none; + transition: color var(--transition), opacity var(--transition-bezier); + opacity: 0.7; + } - &-input { - width: 100%; - padding: 0.75rem 1rem 0.75rem 2.5rem; - border-radius: 0.5rem; - border: 1px solid var(--bg-muted-2); - background-color: var(--bg-muted-1); - color: var(--fg-color); - font-size: var(--font-size-medium); - transition: - border-color var(--transition), - box-shadow var(--transition), - background-color var(--transition); + &-input { + width: 100%; + padding: 0.75rem 1rem 0.75rem 2.5rem; + border-radius: 0.5rem; + border: 1px solid var(--bg-muted-2); + background-color: var(--bg-muted-1); + color: var(--fg-color); + font-size: var(--font-size-medium); + transition: + border-color var(--transition), + box-shadow var(--transition), + background-color var(--transition); - &:focus { - outline: none; - border-color: var(--accent-color); - box-shadow: 0 0 0 2px var(--accent-bg-1), 0 0 0 4px var(--accent-color); - } + &:focus { + outline: none; + border-color: var(--accent-color); + box-shadow: 0 0 0 2px var(--accent-bg-1), 0 0 0 4px var(--accent-color); + } - &::placeholder { - color: var(--fg-muted-2); - } - } + &::placeholder { + color: var(--fg-muted-2); + } + } - &-results { - display: flex; - flex-direction: column; - gap: 0.25rem; + &-results { + display: flex; + flex-direction: column; + gap: 0.25rem; - .search-fuse-result { - padding: 0.5rem 0.75rem; - border-radius: 0.25rem; - background-color: var(--bg-muted-1); - text-decoration: none; - color: var(--fg-color); - transition: - background-color var(--transition), - padding-left var(--transition); + .search-fuse-result { + padding: 0.5rem 0.75rem; + border-radius: 0.25rem; + background-color: var(--bg-muted-1); + text-decoration: none; + color: var(--fg-color); + transition: + background-color var(--transition), + padding-left var(--transition); - &:hover { - background-color: var(--bg-muted-2); - padding-left: 1rem; - } - } - } + &:hover { + background-color: var(--bg-muted-2); + padding-left: 1rem; + } + } + } } diff --git a/modules/amezing/sass/components/_search-modal.scss b/modules/amezing/sass/components/_search-modal.scss index 2174293..6c97222 100644 --- a/modules/amezing/sass/components/_search-modal.scss +++ b/modules/amezing/sass/components/_search-modal.scss @@ -1,128 +1,124 @@ #search-modal { - display: flex; - position: fixed; - flex-direction: column; - align-items: center; - visibility: hidden; - opacity: 0; - z-index: 9999; - transition: - visibility var(--transition), - opacity var(--transition); - inset: 0; - background-color: var(--shade-color); - padding-block-start: 15vh; - width: 100%; - height: 100vh; + display: flex; + position: fixed; + flex-direction: column; + align-items: center; + visibility: hidden; + opacity: 0; + z-index: 9999; + transition: visibility var(--transition), opacity var(--transition); + inset: 0; + background-color: var(--shade-color); + padding-block-start: 15vh; + width: 100%; + height: 100vh; - &.active { - visibility: visible; - opacity: 1; + &.active { + visibility: visible; + opacity: 1; - #search-modal-content { - transform: none; - } - } + #search-modal-content { + transform: none; + } + } - &-content { - display: flex; - flex-direction: column; - gap: 1rem; - transform: var(--hover); - backdrop-filter: var(--blur); - -webkit-backdrop-filter: var(--blur); - transition: - border-radius var(--transition), - padding var(--transition), - max-width var(--transition), - transform var(--transition-bezier-longer); - margin: -0.5rem; - box-shadow: var(--edge-highlight), var(--shadow-glass); - border-radius: 2.25rem 2.25rem 1.75rem 1.75rem; - background-color: var(--glass-bg-1); - padding: 1rem; - width: calc(100% - 2rem); - max-width: calc(var(--content-width) / 1.5); - overflow: hidden; + &-content { + display: flex; + flex-direction: column; + gap: 1rem; + transform: var(--hover); + backdrop-filter: var(--blur); + -webkit-backdrop-filter: var(--blur); + transition: + border-radius var(--transition), + padding var(--transition), + max-width var(--transition), + transform var(--transition-bezier-longer); + margin: -0.5rem; + box-shadow: var(--edge-highlight), var(--shadow-glass); + border-radius: 2.25rem 2.25rem 1.75rem 1.75rem; + background-color: var(--glass-bg-1); + padding: 1rem; + width: calc(100% - 2rem); + max-width: calc(var(--content-width) / 1.5); + overflow: hidden; - &:has(#search-results:empty) { - border-radius: 1.75rem; - padding: 0.5rem; - max-width: calc(var(--content-width) / 2); - } + &:has(#search-results:empty) { + border-radius: 1.75rem; + padding: 0.5rem; + max-width: calc(var(--content-width) / 2); + } - & > div { - position: relative; - } - } + & > div { + position: relative; + } + } - #search-input { - flex-shrink: 0; - box-shadow: var(--edge-highlight); - border-radius: 9999px; - padding: 0 1rem; - width: 100%; - height: 2.5rem; + #search-input { + flex-shrink: 0; + box-shadow: var(--edge-highlight); + border-radius: 9999px; + padding: 0 1rem; + width: 100%; + height: 2.5rem; - &:placeholder-shown + div .icon { - transform: none; - opacity: var(--disabled-opacity); - pointer-events: all; - } + &:placeholder-shown + div .icon { + transform: none; + opacity: var(--disabled-opacity); + pointer-events: all; + } - & + div .icon { - position: absolute; - transform: translateX(0.5rem); - opacity: 0; - transition: - opacity var(--transition), - transform var(--transition-bezier); - cursor: help; - inset-block-start: 0.625rem; - inset-inline-end: 1rem; - pointer-events: none; - color: var(--fg-muted-2); - font-size: 1.25rem; - } - } + & + div .icon { + position: absolute; + transform: translateX(0.5rem); + opacity: 0; + transition: opacity var(--transition), transform var(--transition-bezier); + cursor: help; + inset-block-start: 0.625rem; + inset-inline-end: 1rem; + pointer-events: none; + color: var(--fg-muted-2); + font-size: 1.25rem; + } + } - #search-results { - display: none; - flex: 1; - flex-direction: column; - gap: 0.5rem; - margin: -1rem; - padding: 1rem; - max-height: 50vh; - overflow: auto; - scrollbar-width: thin; + #search-results { + display: none; + flex: 1; + flex-direction: column; + gap: 0.5rem; + margin: -1rem; + padding: 1rem; + max-height: 50vh; + overflow: auto; + scrollbar-width: thin; - .item { - display: inline-flex; - flex-direction: column; - box-shadow: var(--edge-highlight); - border-radius: var(--rounded-corner); - background-color: var(--bg-muted-1); - padding: 0.75rem 1rem; + .item { + display: inline-flex; + flex-direction: column; + box-shadow: var(--edge-highlight); + border-radius: var(--rounded-corner); + background-color: var(--bg-muted-1); + padding: 0.75rem 1rem; - a { - text-decoration: none; + a { + text-decoration: none; - &::after { - content: " →"; - } - } + &::after { + content: " →"; + } + } - span { - margin-block-start: 0.25rem; - border-block-start: rpx(1) solid var(--bg-muted-2); - padding-block-start: 0.25rem; - color: var(--fg-muted-2); + span { + margin-block-start: 0.25rem; + border-block-start: rpx(1) solid var(--bg-muted-2); + padding-block-start: 0.25rem; + color: var(--fg-muted-2); - strong { - color: var(--fg-color); - } - } - } - } + strong { + color: var(--fg-color); + } + } + } + } } diff --git a/modules/amezing/sass/components/_skip-to-content.scss b/modules/amezing/sass/components/_skip-to-content.scss index 1e217af..d2c64fe 100644 --- a/modules/amezing/sass/components/_skip-to-content.scss +++ b/modules/amezing/sass/components/_skip-to-content.scss @@ -1,23 +1,23 @@ #skip-to-content { - position: absolute; - transform: translateX(-50%) translateY(calc(-100% - 1rem)); - z-index: 9999; - transition: - backdrop-filter var(--transition), - box-shadow var(--transition), - transform var(--transition-bezier); - inset-inline-start: 50vw; - border-radius: 9999px; - background-color: var(--glass-bg-1); - padding: 0.375rem 0.75rem; - width: max-content; - text-decoration: none; + position: absolute; + transform: translateX(-50%) translateY(calc(-100% - 1rem)); + z-index: 9999; + transition: + backdrop-filter var(--transition), + box-shadow var(--transition), + transform var(--transition-bezier); + inset-inline-start: 50vw; + border-radius: 9999px; + background-color: var(--glass-bg-1); + padding: 0.375rem 0.75rem; + width: max-content; + text-decoration: none; - &:focus-visible, - &:focus { - -webkit-backdrop-filter: var(--blur); - transform: translateX(-50%) translateY(1rem); - backdrop-filter: var(--blur); - box-shadow: var(--edge-highlight), var(--shadow); - } + &:focus-visible, + &:focus { + -webkit-backdrop-filter: var(--blur); + transform: translateX(-50%) translateY(1rem); + backdrop-filter: var(--blur); + box-shadow: var(--edge-highlight), var(--shadow); + } } diff --git a/modules/amezing/sass/components/_statements.scss b/modules/amezing/sass/components/_statements.scss index f76e587..e23128a 100644 --- a/modules/amezing/sass/components/_statements.scss +++ b/modules/amezing/sass/components/_statements.scss @@ -1,61 +1,61 @@ @use "../citrine/abstracts/functions" as *; .statement-container { - margin: 1rem 0; - box-shadow: var(--edge-highlight), var(--shadow); - border-radius: var(--rounded-corner); - padding: 1rem; + margin: 1rem 0; + box-shadow: var(--edge-highlight), var(--shadow); + border-radius: var(--rounded-corner); + padding: 1rem; - :last-child { - margin-block-end: 0; - } + :last-child { + margin-block-end: 0; + } - & > :nth-child(2) { - margin-block-start: 0.5rem; - } + & > :nth-child(2) { + margin-block-start: 0.5rem; + } - .title { - font-weight: var(--font-weight-semi-bold); + .title { + font-weight: var(--font-weight-semi-bold); - .icon { - vertical-align: rpx(-2); - margin-inline-end: 0.25rem; - } - } + .icon { + vertical-align: rpx(-2); + margin-inline-end: 0.25rem; + } + } - &.archive { - --accent-color: var(--purple-fg); - --accent-highlight: hsla( - var(--purple-h), - var(--purple-s), - var(--purple-l), - var(--disabled-opacity) - ); - background-color: var(--purple-bg-1); - color: var(--purple-fg); - } + &.archive { + --accent-color: var(--purple-fg); + --accent-highlight: hsla( + var(--purple-h), + var(--purple-s), + var(--purple-l), + var(--disabled-opacity) + ); + background-color: var(--purple-bg-1); + color: var(--purple-fg); + } - &.disclaimer { - --accent-color: var(--red-fg); - --accent-highlight: hsla( - var(--red-h), - var(--red-s), - var(--red-l), - var(--disabled-opacity) - ); - background-color: var(--red-bg-1); - color: var(--red-fg); - } + &.disclaimer { + --accent-color: var(--red-fg); + --accent-highlight: hsla( + var(--red-h), + var(--red-s), + var(--red-l), + var(--disabled-opacity) + ); + background-color: var(--red-bg-1); + color: var(--red-fg); + } - &.trigger { - --accent-color: var(--yellow-fg); - --accent-highlight: hsla( - var(--yellow-h), - var(--yellow-s), - var(--yellow-l), - var(--disabled-opacity) - ); - background-color: var(--yellow-bg-1); - color: var(--yellow-fg); - } + &.trigger { + --accent-color: var(--yellow-fg); + --accent-highlight: hsla( + var(--yellow-h), + var(--yellow-s), + var(--yellow-l), + var(--disabled-opacity) + ); + background-color: var(--yellow-bg-1); + color: var(--yellow-fg); + } } diff --git a/modules/amezing/sass/components/_tag-chips.scss b/modules/amezing/sass/components/_tag-chips.scss index df38adc..cb009bf 100644 --- a/modules/amezing/sass/components/_tag-chips.scss +++ b/modules/amezing/sass/components/_tag-chips.scss @@ -1,59 +1,59 @@ @use "../citrine/abstracts/functions" as *; #tag-chips { - display: flex; - flex-wrap: wrap; - gap: 0.25rem; - margin: 0; - padding-inline-start: 0; - list-style: none; + display: flex; + flex-wrap: wrap; + gap: 0.25rem; + margin: 0; + padding-inline-start: 0; + list-style: none; - li { - display: flex; - margin-block: 0; - padding-inline-start: 0; - } + li { + display: flex; + margin-block: 0; + padding-inline-start: 0; + } - a { - display: flex; - transition: - background-color var(--transition), - color var(--transition), - transform var(--transition-bezier); - box-shadow: var(--edge-highlight); - border-radius: 1rem; - background-color: var(--bg-muted-1); - padding: 0.25rem; - padding-inline-start: 0.75rem; - color: var(--fg-muted-1); - font-size: var(--font-size-small); - line-height: 1.5rem; - text-decoration: none; + a { + display: flex; + transition: + background-color var(--transition), + color var(--transition), + transform var(--transition-bezier); + box-shadow: var(--edge-highlight); + border-radius: 1rem; + background-color: var(--bg-muted-1); + padding: 0.25rem; + padding-inline-start: 0.75rem; + color: var(--fg-muted-1); + font-size: var(--font-size-small); + line-height: 1.5rem; + text-decoration: none; - &:hover { - background-color: var(--bg-muted-2); - color: var(--fg-muted-2); - } + &:hover { + background-color: var(--bg-muted-2); + color: var(--fg-muted-2); + } - &:active { - transform: var(--active); - } + &:active { + transform: var(--active); + } - &::before { - content: "#"; - } + &::before { + content: "#"; + } - .counter { - transition: inherit; - margin-inline-start: 0.375rem; - box-shadow: var(--edge-highlight); - border-radius: inherit; - background-color: inherit; - aspect-ratio: 1 / 1; - height: 1.5rem; - line-height: 1.5rem; - font-variant-numeric: tabular-nums; - text-align: center; - } - } + .counter { + transition: inherit; + margin-inline-start: 0.375rem; + box-shadow: var(--edge-highlight); + border-radius: inherit; + background-color: inherit; + aspect-ratio: 1 / 1; + height: 1.5rem; + line-height: 1.5rem; + font-variant-numeric: tabular-nums; + text-align: center; + } + } } diff --git a/modules/amezing/sass/components/_zola-anchor.scss b/modules/amezing/sass/components/_zola-anchor.scss index f942600..e62fe50 100644 --- a/modules/amezing/sass/components/_zola-anchor.scss +++ b/modules/amezing/sass/components/_zola-anchor.scss @@ -4,47 +4,47 @@ h3, h4, h5, h6 { - &:hover .zola-anchor { - transform: none; - opacity: 1; - } + &:hover .zola-anchor { + transform: none; + opacity: 1; + } - &:has(.zola-anchor) { - position: relative; - } + &:has(.zola-anchor) { + position: relative; + } } .zola-anchor { - position: absolute; - transform: translateX(0.25em); - opacity: 0; - transition: - color var(--transition), - opacity var(--transition), - transform var(--transition-bezier); - inset-inline-start: -1.25em; - padding-inline-end: 0.25em; - color: var(--fg-muted-1); - line-height: 1; + position: absolute; + transform: translateX(0.25em); + opacity: 0; + transition: + color var(--transition), + opacity var(--transition), + transform var(--transition-bezier); + inset-inline-start: -1.25em; + padding-inline-end: 0.25em; + color: var(--fg-muted-1); + line-height: 1; - @media only screen and (max-width: 720px) { - display: none; - } + @media only screen and (max-width: 720px) { + display: none; + } - &:hover { - color: var(--fg-muted-2); - } + &:hover { + color: var(--fg-muted-2); + } - &:active { - transform: var(--active); - } + &:active { + transform: var(--active); + } - &:focus-visible { - opacity: 1; - } + &:focus-visible { + opacity: 1; + } } :target .zola-anchor { - transform: none; - opacity: var(--disabled-opacity); + transform: none; + opacity: var(--disabled-opacity); } diff --git a/modules/amezing/sass/components/_zola-error.scss b/modules/amezing/sass/components/_zola-error.scss index 433f6cc..45bb82c 100644 --- a/modules/amezing/sass/components/_zola-error.scss +++ b/modules/amezing/sass/components/_zola-error.scss @@ -1,55 +1,55 @@ // Based on https://github.com/welpo/tabi/blob/68c35b02f8c09e21ecff48f61bad6da3d96ce337/sass/parts/_zola-error.scss body > div:last-child:has(div:last-child[style]:not([class]):not([id])) { - z-index: 9999 !important; - background-color: var(--shade-color) !important; + z-index: 9999 !important; + background-color: var(--shade-color) !important; } body > div:last-child > div:last-child[style]:not([class]):not([id]) { - box-shadow: var(--edge-highlight), var(--shadow-glass); - border-radius: calc(var(--rounded-corner) + 1rem) !important; - background-color: hsla( - var(--red-h), - calc(var(--red-s) - var(--red-s) * 0.7), - calc(var(--red-l) + (100% - var(--red-l)) * 0.9), - var(--dim-opacity) - ) !important; - padding: 1rem !important; - width: fit-content; - max-width: calc(100% - 2rem); - -webkit-backdrop-filter: var(--blur); - backdrop-filter: var(--blur); - filter: none !important; + box-shadow: var(--edge-highlight), var(--shadow-glass); + border-radius: calc(var(--rounded-corner) + 1rem) !important; + background-color: hsla( + var(--red-h), + calc(var(--red-s) - var(--red-s) * 0.7), + calc(var(--red-l) + (100% - var(--red-l)) * 0.9), + var(--dim-opacity) + ) !important; + padding: 1rem !important; + width: fit-content; + max-width: calc(100% - 2rem); + -webkit-backdrop-filter: var(--blur); + backdrop-filter: var(--blur); + filter: none !important; - @media (prefers-color-scheme: dark) { - & { - background-color: hsla( - var(--red-h), - calc(var(--red-s) - var(--red-s) * 0.6), - calc(var(--red-l) - var(--red-l) * 0.8), - var(--dim-opacity) - ) !important; - } - } + @media (prefers-color-scheme: dark) { + & { + background-color: hsla( + var(--red-h), + calc(var(--red-s) - var(--red-s) * 0.6), + calc(var(--red-l) - var(--red-l) * 0.8), + var(--dim-opacity) + ) !important; + } + } } body - > div:last-child - > div:last-child[style]:not([class]):not([id]) - > p[style]:first-child { - margin-block-end: 1rem !important; - color: var(--red-fg) !important; - font-weight: var(--font-weight-semi-bold) !important; - font-size: var(--font-size-large) !important; + > div:last-child + > div:last-child[style]:not([class]):not([id]) + > p[style]:first-child { + margin-block-end: 1rem !important; + color: var(--red-fg) !important; + font-weight: var(--font-weight-semi-bold) !important; + font-size: var(--font-size-large) !important; } body - > div:last-child - > div:last-child[style]:not([class]):not([id]) - > pre[style]:last-child { - border-radius: var(--rounded-corner) !important; - background-color: var(--red-bg-1) !important; - padding: 0.75rem 1rem !important; - color: var(--red-fg) !important; - font-weight: var(--font-weight-normal) !important; + > div:last-child + > div:last-child[style]:not([class]):not([id]) + > pre[style]:last-child { + border-radius: var(--rounded-corner) !important; + background-color: var(--red-bg-1) !important; + padding: 0.75rem 1rem !important; + color: var(--red-fg) !important; + font-weight: var(--font-weight-normal) !important; } diff --git a/modules/amezing/sass/fonts.scss b/modules/amezing/sass/fonts.scss index a148e87..8dee45e 100644 --- a/modules/amezing/sass/fonts.scss +++ b/modules/amezing/sass/fonts.scss @@ -2,51 +2,51 @@ $commissioner-version: "1.012.1"; $commit-mono-version: "1.143"; :root { - --font-sans-serif: "Commissioner"; - --font-monospace: "Commit Mono"; + --font-sans-serif: "Commissioner"; + --font-monospace: "Commit Mono"; } // Commissioner @font-face { - font-style: normal; - font-weight: 100 900; - src: url("/fonts/commissioner.woff2?v=#{$commissioner-version}") - format("woff2"); - font-family: "Commissioner"; - font-display: swap; - // Fix vertical alignment, - // See https://github.com/kosbarts/Commissioner/issues/55 - ascent-override: 96.3%; - descent-override: 25%; + font-style: normal; + font-weight: 100 900; + src: url("/fonts/commissioner.woff2?v=#{$commissioner-version}") + format("woff2"); + font-family: "Commissioner"; + font-display: swap; + // Fix vertical alignment, + // See https://github.com/kosbarts/Commissioner/issues/55 + ascent-override: 96.3%; + descent-override: 25%; } @font-face { - font-style: italic; - font-weight: 100 900; - src: url("/fonts/commissioner.woff2?v=#{$commissioner-version}") - format("woff2"); - font-family: "Commissioner"; - font-display: swap; - font-variation-settings: "slnt" -12; - // Ditto - ascent-override: 96.3%; - descent-override: 25%; + font-style: italic; + font-weight: 100 900; + src: url("/fonts/commissioner.woff2?v=#{$commissioner-version}") + format("woff2"); + font-family: "Commissioner"; + font-display: swap; + font-variation-settings: "slnt" -12; + // Ditto + ascent-override: 96.3%; + descent-override: 25%; } // Commit Mono @font-face { - font-style: normal; - font-weight: 100 900; - src: url("/fonts/commit-mono.woff2?v=#{$commit-mono-version}") format("woff2"); - font-family: "Commit Mono"; - font-display: swap; + font-style: normal; + font-weight: 100 900; + src: url("/fonts/commit-mono.woff2?v=#{$commit-mono-version}") format("woff2"); + font-family: "Commit Mono"; + font-display: swap; } @font-face { - font-style: italic; - font-weight: 100 900; - src: url("/fonts/commit-mono.woff2?v=#{$commit-mono-version}") format("woff2"); - font-family: "Commit Mono"; - font-display: swap; - font-variation-settings: "ital" 1; + font-style: italic; + font-weight: 100 900; + src: url("/fonts/commit-mono.woff2?v=#{$commit-mono-version}") format("woff2"); + font-family: "Commit Mono"; + font-display: swap; + font-variation-settings: "ital" 1; } diff --git a/modules/amezing/sass/layout/_footer.scss b/modules/amezing/sass/layout/_footer.scss index 25a09fe..379c856 100644 --- a/modules/amezing/sass/layout/_footer.scss +++ b/modules/amezing/sass/layout/_footer.scss @@ -2,139 +2,137 @@ @use "../citrine/abstracts/mixins" as *; #site-footer { - --padding: 1rem; - display: flex; - grid-area: footer; - gap: 1rem; - margin-block: 3.25rem 1rem; - border-block-start: rpx(1) solid var(--bg-muted-2); - padding: 1rem; - width: 100%; - min-width: 0; - max-width: calc(var(--content-width) + var(--padding) * 2); + --padding: 1rem; + display: flex; + grid-area: footer; + gap: 1rem; + margin-block: 3.25rem 1rem; + border-block-start: rpx(1) solid var(--bg-muted-2); + padding: 1rem; + width: 100%; + min-width: 0; + max-width: calc(var(--content-width) + var(--padding) * 2); - @media only screen and (max-width: 480px) { - flex-direction: column-reverse; - } + @media only screen and (max-width: 480px) { + flex-direction: column-reverse; + } - & > div { - display: flex; - flex: 1; - flex-direction: column; - gap: 1rem; + & > div { + display: flex; + flex: 1; + flex-direction: column; + gap: 1rem; - p { - margin: 0; - } + p { + margin: 0; + } - &:nth-of-type(1) { - color: var(--fg-muted-2); - font-size: var(--font-size-small); - } + &:nth-of-type(1) { + color: var(--fg-muted-2); + font-size: var(--font-size-small); + } - &:nth-of-type(2) { - justify-content: space-between; - align-items: flex-end; - text-align: end; + &:nth-of-type(2) { + justify-content: space-between; + align-items: flex-end; + text-align: end; - @media only screen and (max-width: 480px) { - align-items: normal; - text-align: start; - } - } - } + @media only screen and (max-width: 480px) { + align-items: normal; + text-align: start; + } + } + } - a.external::after { - margin-inline-start: 0.25rem; - } + a.external::after { + margin-inline-start: 0.25rem; + } - .link { - @include chip; - display: inline-block; - line-height: 1.5rem; + .link { + @include chip; + display: inline-block; + line-height: 1.5rem; - &:hover { - background-color: var(--bg-muted-2); - } + &:hover { + background-color: var(--bg-muted-2); + } - &:active { - transform: var(--active); - } - } + &:active { + transform: var(--active); + } + } - // IMO it's pretty cute - .webrings { - display: flex; - flex-direction: column; - gap: 0.25rem; - margin-block: 0; - padding-inline-start: 0; - list-style: none; - } + // IMO it's pretty cute + .webrings { + display: flex; + flex-direction: column; + gap: 0.25rem; + margin-block: 0; + padding-inline-start: 0; + list-style: none; + } - .webring { - display: flex; - gap: 0.25rem; - margin-block: 0; - padding-inline-start: 0; + .webring { + display: flex; + gap: 0.25rem; + margin-block: 0; + padding-inline-start: 0; - .random, - .previous, - .next { - display: inline-flex; - justify-content: center; - align-items: center; - border-radius: 9999px; - padding: 0.25rem; - font-size: 1rem; - } - } + .random, + .previous, + .next { + display: inline-flex; + justify-content: center; + align-items: center; + border-radius: 9999px; + padding: 0.25rem; + font-size: 1rem; + } + } - .timestamp { - color: var(--fg-muted-1); - font-size: var(--font-size-xx-small); - font-family: var(--font-monospace); - text-transform: uppercase; - } + .timestamp { + color: var(--fg-muted-1); + font-size: var(--font-size-xx-small); + font-family: var(--font-monospace); + text-transform: uppercase; + } - .socials { - display: flex; - justify-content: flex-end; - gap: 0.75rem; - margin: 0; - padding-inline-start: 0; - list-style: none; + .socials { + display: flex; + justify-content: flex-end; + gap: 0.75rem; + margin: 0; + padding-inline-start: 0; + list-style: none; - @media only screen and (max-width: 480px) { - justify-content: normal; - } + @media only screen and (max-width: 480px) { + justify-content: normal; + } - li { - margin-block: 0; - padding-inline-start: 0; - } + li { + margin-block: 0; + padding-inline-start: 0; + } - a { - display: flex; - transition: - color var(--transition), - transform var(--transition-bezier); - height: fit-content; - color: var(--fg-muted-1); + a { + display: flex; + transition: color var(--transition), transform var(--transition-bezier); + height: fit-content; + color: var(--fg-muted-1); - &:hover { - color: var(--fg-muted-2); - } + &:hover { + color: var(--fg-muted-2); + } - &:active { - transform: var(--active); - } - } + &:active { + transform: var(--active); + } + } - svg { - fill: currentColor; - width: 1.5rem; - height: 1.5rem; - } - } + svg { + fill: currentColor; + width: 1.5rem; + height: 1.5rem; + } + } } diff --git a/modules/amezing/sass/layout/_full-bleed.scss b/modules/amezing/sass/layout/_full-bleed.scss index 85e9fd3..2405261 100644 --- a/modules/amezing/sass/layout/_full-bleed.scss +++ b/modules/amezing/sass/layout/_full-bleed.scss @@ -1,13 +1,13 @@ .full-bleed { - transform: translateX(-50%); - margin-inline-start: 50%; - width: calc(100vw - var(--sidebar-width)); + transform: translateX(-50%); + margin-inline-start: 50%; + width: calc(100vw - var(--sidebar-width)); - :root[dir*="rtl"] & { - transform: translateX(50%); - } + :root[dir*="rtl"] & { + transform: translateX(50%); + } - @media only screen and (max-width: 960px) { - width: calc(100vw - 3.5rem); - } + @media only screen and (max-width: 960px) { + width: calc(100vw - 3.5rem); + } } diff --git a/modules/amezing/sass/layout/_grid.scss b/modules/amezing/sass/layout/_grid.scss index 6a8b9db..40f85f8 100644 --- a/modules/amezing/sass/layout/_grid.scss +++ b/modules/amezing/sass/layout/_grid.scss @@ -1,16 +1,16 @@ body { - display: grid; - grid-template-rows: auto auto 1fr auto; - grid-template-columns: var(--sidebar-width) 1fr; - grid-template-areas: - "sidebar header" - "sidebar banner" - "sidebar main " - "sidebar footer"; - justify-items: center; - transition: grid-template-columns var(--transition); + display: grid; + grid-template-rows: auto auto 1fr auto; + grid-template-columns: var(--sidebar-width) 1fr; + grid-template-areas: + "sidebar header" + "sidebar banner" + "sidebar main " + "sidebar footer"; + justify-items: center; + transition: grid-template-columns var(--transition); - @media only screen and (max-width: 960px) { - grid-template-columns: 0 1fr; - } + @media only screen and (max-width: 960px) { + grid-template-columns: 0 1fr; + } } diff --git a/modules/amezing/sass/layout/_header.scss b/modules/amezing/sass/layout/_header.scss index 0ecab63..5e240a7 100644 --- a/modules/amezing/sass/layout/_header.scss +++ b/modules/amezing/sass/layout/_header.scss @@ -2,346 +2,344 @@ @use "../citrine/abstracts/mixins" as *; #site-header { - display: flex; - position: sticky; - grid-area: header; - // height: 3rem; - justify-content: space-between; - // flex-wrap: nowrap; - align-items: center; - gap: 0.25rem; - z-index: 2; - // height: 3.75rem; - inset-block-start: 0; - // padding: 0.5rem 0.75rem; - padding: 0.5rem; - width: 100%; - min-width: 0; + display: flex; + position: sticky; + grid-area: header; + // height: 3rem; + justify-content: space-between; + // flex-wrap: nowrap; + align-items: center; + gap: 0.25rem; + z-index: 2; + // height: 3.75rem; + inset-block-start: 0; + // padding: 0.5rem 0.75rem; + padding: 0.5rem; + width: 100%; + min-width: 0; - &::before, - &::after { - position: absolute; - inset: 0; - @include easing-mask(bottom); - inset-block-end: -100%; - pointer-events: none; - content: ""; - } + &::before, + &::after { + position: absolute; + inset: 0; + @include easing-mask(bottom); + inset-block-end: -100%; + pointer-events: none; + content: ""; + } - &::before { - z-index: -2; - backdrop-filter: var(--blur); - -webkit-backdrop-filter: var(--blur); - } + &::before { + z-index: -2; + backdrop-filter: var(--blur); + -webkit-backdrop-filter: var(--blur); + } - &::after { - z-index: -1; - background-color: var(--bg-color); - } + &::after { + z-index: -1; + background-color: var(--bg-color); + } - .title { - margin-inline: 0.75rem; - min-width: 0; - overflow: hidden; - color: var(--fg-muted-2); - font-size: var(--font-size-small); - line-height: 2rem; - text-overflow: ellipsis; - white-space: nowrap; - } + .title { + margin-inline: 0.75rem; + min-width: 0; + overflow: hidden; + color: var(--fg-muted-2); + font-size: var(--font-size-small); + line-height: 2rem; + text-overflow: ellipsis; + white-space: nowrap; + } - .buttons { - display: flex; - flex-wrap: nowrap; - gap: 0.25rem; - margin: 0; - // margin-inline-start: auto; - box-shadow: var(--edge-highlight), var(--shadow-glass); - border-radius: 1.125rem; - background-color: var(--glass-bg-1); - padding: 0.25rem; - list-style: none; + .buttons { + display: flex; + flex-wrap: nowrap; + gap: 0.25rem; + margin: 0; + // margin-inline-start: auto; + box-shadow: var(--edge-highlight), var(--shadow-glass); + border-radius: 1.125rem; + background-color: var(--glass-bg-1); + padding: 0.25rem; + list-style: none; - li { - flex: 0; - margin-block: 0; - padding-inline-start: 0; - } + li { + flex: 0; + margin-block: 0; + padding-inline-start: 0; + } - & > li { - height: fit-content; - line-height: 0; - } + & > li { + height: fit-content; + line-height: 0; + } - button, - details { - box-shadow: none; - background-color: transparent; - line-height: inherit; + button, + details { + box-shadow: none; + background-color: transparent; + line-height: inherit; - .icon { - margin-inline-end: 0; - box-shadow: none; - background-color: transparent; - padding: 0; - } - } + .icon { + margin-inline-end: 0; + box-shadow: none; + background-color: transparent; + padding: 0; + } + } - button, - summary { - border-radius: 50%; - padding: 0.375rem; + button, + summary { + border-radius: 50%; + padding: 0.375rem; - &:hover { - box-shadow: var(--edge-highlight); - background-color: var(--bg-muted-1); - color: var(--fg-muted-2); - } + &:hover { + box-shadow: var(--edge-highlight); + background-color: var(--bg-muted-1); + color: var(--fg-muted-2); + } - &:active { - transform: var(--active); - } - } + &:active { + transform: var(--active); + } + } - details { - box-shadow: none; - border-radius: 0; - padding: 0; - } + details { + box-shadow: none; + border-radius: 0; + padding: 0; + } - button { - font-size: var(--font-size-medium); - } + button { + font-size: var(--font-size-medium); + } - summary { - &:hover { - box-shadow: var(--edge-highlight); - background-color: var(--bg-muted-1); - } + summary { + &:hover { + box-shadow: var(--edge-highlight); + background-color: var(--bg-muted-1); + } - &::before { - display: none; - } - } + &::before { + display: none; + } + } - details { - display: block; - position: relative; - margin-block-end: 0; + details { + display: block; + position: relative; + margin-block-end: 0; - &::details-content { - block-size: auto; - } + &::details-content { + block-size: auto; + } - &[open] { - &::details-content { - block-size: auto; - } + &[open] { + &::details-content { + block-size: auto; + } - summary { - color: var(--fg-muted-1); + summary { + color: var(--fg-muted-1); - &:hover { - color: var(--fg-muted-2); - } - } + &:hover { + color: var(--fg-muted-2); + } + } - @supports not (block-size: calc-size(auto, size)) { - summary ~ * { - animation: none; - } - } - } - } - } + @supports not (block-size: calc-size(auto, size)) { + summary ~ * { + animation: none; + } + } + } + } + } } .buttons .dropdown { - display: flex; - position: absolute; - flex-direction: column; - gap: 0.25rem; - transform-origin: top right; - z-index: 1; - backdrop-filter: var(--blur); - -webkit-backdrop-filter: var(--blur); - animation: expand-up-right var(--transition-bezier); - inset-block-start: calc(100% + 0.75rem); - inset-inline-end: -0.25rem; - box-shadow: var(--edge-highlight), var(--shadow-glass); - border-radius: calc(var(--rounded-corner) + 0.25rem); - background-color: var(--glass-bg-1); - padding: 0.25rem; - width: max-content; - min-width: 8rem; - list-style: none; + display: flex; + position: absolute; + flex-direction: column; + gap: 0.25rem; + transform-origin: top right; + z-index: 1; + backdrop-filter: var(--blur); + -webkit-backdrop-filter: var(--blur); + animation: expand-up-right var(--transition-bezier); + inset-block-start: calc(100% + 0.75rem); + inset-inline-end: -0.25rem; + box-shadow: var(--edge-highlight), var(--shadow-glass); + border-radius: calc(var(--rounded-corner) + 0.25rem); + background-color: var(--glass-bg-1); + padding: 0.25rem; + width: max-content; + min-width: 8rem; + list-style: none; - :root[dir*="rtl"] & { - transform-origin: top left; - animation: expand-up-left var(--transition-bezier); - } + :root[dir*="rtl"] & { + transform-origin: top left; + animation: expand-up-left var(--transition-bezier); + } - li { - flex: 1; - } + li { + flex: 1; + } - a { - box-shadow: none; - border-radius: var(--rounded-corner); - background-color: transparent; - padding: 0.375rem 0.75rem; - width: 100%; + a { + box-shadow: none; + border-radius: var(--rounded-corner); + background-color: transparent; + padding: 0.375rem 0.75rem; + width: 100%; - &:hover { - box-shadow: var(--edge-highlight); - background-color: var(--bg-muted-1) !important; - } - } + &:hover { + box-shadow: var(--edge-highlight); + background-color: var(--bg-muted-1) !important; + } + } } #site-sidebar-controls { - display: flex; - flex-shrink: 0; - gap: 0.25rem; - transition: - width var(--transition), - visibility var(--transition); - margin-block: 0; - box-shadow: var(--edge-highlight), var(--shadow-glass); - border-radius: 1.125rem; - background-color: var(--glass-bg-1); - padding: 0.25rem; - height: fit-content; - line-height: 0; - list-style: none; + display: flex; + flex-shrink: 0; + gap: 0.25rem; + transition: width var(--transition), visibility var(--transition); + margin-block: 0; + box-shadow: var(--edge-highlight), var(--shadow-glass); + border-radius: 1.125rem; + background-color: var(--glass-bg-1); + padding: 0.25rem; + height: fit-content; + line-height: 0; + list-style: none; - @media only screen and (max-width: 960px) { - width: 2.25rem; + @media only screen and (max-width: 960px) { + width: 2.25rem; - &:has(#expand-sidebar:checked) { - width: 6.25rem; + &:has(#expand-sidebar:checked) { + width: 6.25rem; - #site-sidebar-modes { - transform: none; - visibility: visible; - opacity: 1; - } - } - } + #site-sidebar-modes { + transform: none; + visibility: visible; + opacity: 1; + } + } + } - ul { - padding-inline-start: 0; - list-style: none; - } + ul { + padding-inline-start: 0; + list-style: none; + } - li { - display: inline-block; - margin-block: 0; - padding-inline-start: 0; + li { + display: inline-block; + margin-block: 0; + padding-inline-start: 0; - label { - display: inline-block; - transition: - box-shadow var(--transition), - background-color var(--transition), - color var(--transition), - transform var(--transition-bezier); - cursor: pointer; - border-radius: 50%; - padding: 0.375rem; - height: fit-content; - color: var(--fg-muted-1); - line-height: 0; + label { + display: inline-block; + transition: + box-shadow var(--transition), + background-color var(--transition), + color var(--transition), + transform var(--transition-bezier); + cursor: pointer; + border-radius: 50%; + padding: 0.375rem; + height: fit-content; + color: var(--fg-muted-1); + line-height: 0; - &:hover { - box-shadow: var(--edge-highlight); - background-color: var(--bg-muted-1); - color: var(--fg-muted-2); - } + &:hover { + box-shadow: var(--edge-highlight); + background-color: var(--bg-muted-1); + color: var(--fg-muted-2); + } - &:active { - transform: var(--active); - } - } + &:active { + transform: var(--active); + } + } - input[type="radio"] { - & + label { - position: relative; - z-index: 1; + input[type="radio"] { + & + label { + position: relative; + z-index: 1; - &::before { - @include gradient-fill; - position: absolute; - opacity: 0; - z-index: -1; - transition: - opacity var(--transition), - box-shadow var(--transition), - background-color var(--transition); - inset: 0; - border-radius: inherit; - content: ""; - } - } + &::before { + @include gradient-fill; + position: absolute; + opacity: 0; + z-index: -1; + transition: + opacity var(--transition), + box-shadow var(--transition), + background-color var(--transition); + inset: 0; + border-radius: inherit; + content: ""; + } + } - &:checked + label { - color: var(--contrast-color); + &:checked + label { + color: var(--contrast-color); - &:hover { - box-shadow: none; - background-color: transparent; - } + &:hover { + box-shadow: none; + background-color: transparent; + } - &:hover::before { - box-shadow: var(--edge-highlight); - background-color: var(--accent-color); - } + &:hover::before { + box-shadow: var(--edge-highlight); + background-color: var(--accent-color); + } - &::before { - opacity: 1; - } - } - } + &::before { + opacity: 1; + } + } + } - input[type="radio"], - input[type="checkbox"] { - &:focus-visible + label { - animation: focus-in var(--transition); - outline: 0.125rem solid var(--accent-color); - outline-offset: 0.125rem; - } + input[type="radio"], + input[type="checkbox"] { + &:focus-visible + label { + animation: focus-in var(--transition); + outline: 0.125rem solid var(--accent-color); + outline-offset: 0.125rem; + } - @supports not selector(:focus-visible) { - &:focus + label { - animation: focus-in var(--transition); - outline: 0.125rem solid var(--accent-color); - outline-offset: 0.125rem; - } - } - } - } + @supports not selector(:focus-visible) { + &:focus + label { + animation: focus-in var(--transition); + outline: 0.125rem solid var(--accent-color); + outline-offset: 0.125rem; + } + } + } + } - #expand-sidebar-li { - display: none; + #expand-sidebar-li { + display: none; - @media only screen and (max-width: 960px) { - display: inline-block; - } - } + @media only screen and (max-width: 960px) { + display: inline-block; + } + } } #site-sidebar-modes { - display: flex; - gap: 0.25rem; - transform-origin: left; - transition: - visibility var(--transition), - opacity var(--transition), - transform var(--transition); + display: flex; + gap: 0.25rem; + transform-origin: left; + transition: + visibility var(--transition), + opacity var(--transition), + transform var(--transition); - @media only screen and (max-width: 960px) { - transform: translateX(-2.25rem); - visibility: hidden; - opacity: 0; - } + @media only screen and (max-width: 960px) { + transform: translateX(-2.25rem); + visibility: hidden; + opacity: 0; + } } diff --git a/modules/amezing/sass/layout/_index.scss b/modules/amezing/sass/layout/_index.scss index b6da0b7..0534650 100644 --- a/modules/amezing/sass/layout/_index.scss +++ b/modules/amezing/sass/layout/_index.scss @@ -3,4 +3,4 @@ @use "grid"; @use "header"; @use "main-content"; -@use "sidebar"; \ No newline at end of file +@use "sidebar"; diff --git a/modules/amezing/sass/layout/_main-content.scss b/modules/amezing/sass/layout/_main-content.scss index 1c7c3b7..c94a475 100644 --- a/modules/amezing/sass/layout/_main-content.scss +++ b/modules/amezing/sass/layout/_main-content.scss @@ -1,59 +1,59 @@ #main-content { - --padding: 1rem; - grid-area: main; - transform-origin: 50% 50vh; - animation: fade-in-zoom var(--transition-bezier-longer); - margin-block-start: 3.25rem; - padding-inline: 1rem; - width: 100%; - min-width: 0; - max-width: calc(var(--content-width) + var(--padding) * 2); + --padding: 1rem; + grid-area: main; + transform-origin: 50% 50vh; + animation: fade-in-zoom var(--transition-bezier-longer); + margin-block-start: 3.25rem; + padding-inline: 1rem; + width: 100%; + min-width: 0; + max-width: calc(var(--content-width) + var(--padding) * 2); - @supports (transform-origin: 50% 50dvh) { - transform-origin: 50% 50dvh; - } + @supports (transform-origin: 50% 50dvh) { + transform-origin: 50% 50dvh; + } - & > :first-child { - margin-block-start: 0; - } + & > :first-child { + margin-block-start: 0; + } - & > :last-child { - margin-block-end: 0; - } + & > :last-child { + margin-block-end: 0; + } - @keyframes fade-in-zoom { - from { - transform: scale(0.95); - opacity: 0; - } - } + @keyframes fade-in-zoom { + from { + transform: scale(0.95); + opacity: 0; + } + } } // Don't play animation when jumping to section :target ~ #main-content { - animation-name: none; + animation-name: none; } // Loading animation when going up in the sidebar #↑:target ~ #main-content, #up:target ~ #main-content { - animation-name: slide-in-up-longer; + animation-name: slide-in-up-longer; } // Loading animation when going down in the sidebar #↓:target ~ #main-content, #down:target ~ #main-content { - animation-name: slide-in-down-longer; + animation-name: slide-in-down-longer; } // Loading animation when going to the previous article #←:target ~ #main-content, #left:target ~ #main-content { - animation-name: slide-in-left-longer; + animation-name: slide-in-left-longer; } // Loading animation when going to the next article #→:target ~ #main-content, #right:target ~ #main-content { - animation-name: slide-in-right-longer; + animation-name: slide-in-right-longer; } diff --git a/modules/amezing/sass/layout/_sidebar.scss b/modules/amezing/sass/layout/_sidebar.scss index 6f7655e..a801b81 100644 --- a/modules/amezing/sass/layout/_sidebar.scss +++ b/modules/amezing/sass/layout/_sidebar.scss @@ -2,774 +2,780 @@ @use "../citrine/abstracts/mixins" as *; #site-sidebar { - display: flex; - position: sticky; - grid-area: sidebar; - flex-direction: column; - z-index: 3; - transition-duration: var(--transition); - transition-property: - visibility, transform, inset-block-start, box-shadow, border-inline-end, - border-start-end-radius, border-end-end-radius, height; - inset-block-start: 0; - inset-inline-start: 0; - border-inline-end: rpx(1) solid var(--bg-muted-2); - width: var(--sidebar-width); - height: 100vh; + display: flex; + position: sticky; + grid-area: sidebar; + flex-direction: column; + z-index: 3; + transition-duration: var(--transition); + transition-property: + visibility, + transform, + inset-block-start, + box-shadow, + border-inline-end, + border-start-end-radius, + border-end-end-radius, + height; + inset-block-start: 0; + inset-inline-start: 0; + border-inline-end: rpx(1) solid var(--bg-muted-2); + width: var(--sidebar-width); + height: 100vh; - @supports (height: 100dvh) { - height: 100dvh; - } + @supports (height: 100dvh) { + height: 100dvh; + } - @media only screen and (max-width: 960px) { - transform: translateX(-100%); - visibility: hidden; - inset-block-start: 3.25rem; - box-shadow: var(--edge-highlight), var(--shadow); - border-inline-end: 0 solid transparent; - border-start-end-radius: var(--rounded-corner); - border-end-end-radius: var(--rounded-corner); - height: calc(100vh - 3.75rem); + @media only screen and (max-width: 960px) { + transform: translateX(-100%); + visibility: hidden; + inset-block-start: 3.25rem; + box-shadow: var(--edge-highlight), var(--shadow); + border-inline-end: 0 solid transparent; + border-start-end-radius: var(--rounded-corner); + border-end-end-radius: var(--rounded-corner); + height: calc(100vh - 3.75rem); - :root[dir*="rtl"] & { - transform: translateX(100%); - } + :root[dir*="rtl"] & { + transform: translateX(100%); + } - @supports (height: 100dvh) { - height: calc(100dvh - 3.75rem); - } + @supports (height: 100dvh) { + height: calc(100dvh - 3.75rem); + } - #site-header:has(#expand-sidebar:checked) ~ & { - transform: none; - visibility: visible; - backdrop-filter: var(--blur); - -webkit-backdrop-filter: var(--blur); - background-color: var(--glass-bg-1); - } - } + #site-header:has(#expand-sidebar:checked) ~ & { + transform: none; + visibility: visible; + backdrop-filter: var(--blur); + -webkit-backdrop-filter: var(--blur); + background-color: var(--glass-bg-1); + } + } - &-content { - display: flex; - position: relative; - flex: 1; - overflow: hidden; - } + &-content { + display: flex; + position: relative; + flex: 1; + overflow: hidden; + } } #site-sidebar-header { - display: flex; - align-items: center; - transform-origin: rpx(22) 50%; - transition: - box-shadow var(--transition), - background-color var(--transition), - color var(--transition), - transform var(--transition-bezier); - margin: 1rem; - margin-block-end: 0; - border-radius: 1.375rem; - padding: 0.25rem; - padding-inline-end: 0.75rem; - min-width: 0; - color: var(--fg-muted-1); - text-decoration: none; + display: flex; + align-items: center; + transform-origin: rpx(22) 50%; + transition: + box-shadow var(--transition), + background-color var(--transition), + color var(--transition), + transform var(--transition-bezier); + margin: 1rem; + margin-block-end: 0; + border-radius: 1.375rem; + padding: 0.25rem; + padding-inline-end: 0.75rem; + min-width: 0; + color: var(--fg-muted-1); + text-decoration: none; - &:hover { - box-shadow: var(--edge-highlight); - background-color: var(--bg-muted-1); - color: var(--fg-muted-2); + &:hover { + box-shadow: var(--edge-highlight); + background-color: var(--bg-muted-1); + color: var(--fg-muted-2); - .icon { - background-color: var(--bg-muted-2); - } - } + .icon { + background-color: var(--bg-muted-2); + } + } - &:active { - transform: var(--active); - } + &:active { + transform: var(--active); + } - img { - margin: 0; - } + img { + margin: 0; + } - img, - .icon { - margin-inline-end: 0.5rem; - border-radius: 50%; - } + img, + .icon { + margin-inline-end: 0.5rem; + border-radius: 50%; + } - img { - width: 2.25rem; - height: 2.25rem; - } + img { + width: 2.25rem; + height: 2.25rem; + } - .icon { - transition: background-color var(--transition); - box-shadow: var(--edge-highlight); - background-color: var(--bg-muted-1); - padding: 0.5rem; - font-size: 1.25rem; - } + .icon { + transition: background-color var(--transition); + box-shadow: var(--edge-highlight); + background-color: var(--bg-muted-1); + padding: 0.5rem; + font-size: 1.25rem; + } - div { - display: flex; - flex: 1; - flex-direction: column; - gap: 0.125rem; - min-width: 0; - line-height: 1.25; + div { + display: flex; + flex: 1; + flex-direction: column; + gap: 0.125rem; + min-width: 0; + line-height: 1.25; - span:nth-of-type(1) { - overflow: hidden; - font-size: var(--font-size-small); - text-overflow: ellipsis; - white-space: nowrap; - } + span:nth-of-type(1) { + overflow: hidden; + font-size: var(--font-size-small); + text-overflow: ellipsis; + white-space: nowrap; + } - span:nth-of-type(2) { - font-weight: var(--font-weight-normal); - font-size: var(--font-size-x-small); - } - } + span:nth-of-type(2) { + font-weight: var(--font-weight-normal); + font-size: var(--font-size-x-small); + } + } } #sidebar-nav, #sidebar-meta { - display: flex; - flex: 1; - flex-direction: column; - transition: - visibility var(--transition), - opacity var(--transition), - filter var(--transition), - transform var(--transition-bezier); - padding: 1rem; - width: var(--sidebar-width); - min-width: 100%; - overflow: hidden auto; - scrollbar-width: thin; + display: flex; + flex: 1; + flex-direction: column; + transition: + visibility var(--transition), + opacity var(--transition), + filter var(--transition), + transform var(--transition-bezier); + padding: 1rem; + width: var(--sidebar-width); + min-width: 100%; + overflow: hidden auto; + scrollbar-width: thin; } #site-header:has(#site-sidebar-modes #mode-meta:checked) + #site-sidebar { - #sidebar-nav { - transform: perspective(var(--content-width)) rotateY(15deg); - opacity: var(--disabled-opacity); - filter: blur(0.25rem); - } + #sidebar-nav { + transform: perspective(var(--content-width)) rotateY(15deg); + opacity: var(--disabled-opacity); + filter: blur(0.25rem); + } - #sidebar-meta { - transform: perspective(var(--content-width)); - visibility: visible; - opacity: 1; - filter: none; - } + #sidebar-meta { + transform: perspective(var(--content-width)); + visibility: visible; + opacity: 1; + filter: none; + } } #sidebar-nav { - transform: perspective(var(--content-width)); - transform-origin: left; + transform: perspective(var(--content-width)); + transform-origin: left; - :root[dir*="rtl"] & { - transform-origin: right; - } + :root[dir*="rtl"] & { + transform-origin: right; + } - a { - display: flex; - flex: 1; - align-items: center; - transform-origin: rpx(22) 50%; - transition: - box-shadow var(--transition), - background-color var(--transition), - color var(--transition), - transform var(--transition-bezier); - border-radius: 1.375rem; - padding: 0.25rem; - padding-inline-end: 0.75rem; - overflow: hidden; - color: var(--fg-muted-1); - text-decoration: none; - text-overflow: ellipsis; - white-space: nowrap; + a { + display: flex; + flex: 1; + align-items: center; + transform-origin: rpx(22) 50%; + transition: + box-shadow var(--transition), + background-color var(--transition), + color var(--transition), + transform var(--transition-bezier); + border-radius: 1.375rem; + padding: 0.25rem; + padding-inline-end: 0.75rem; + overflow: hidden; + color: var(--fg-muted-1); + text-decoration: none; + text-overflow: ellipsis; + white-space: nowrap; - &:hover { - box-shadow: var(--edge-highlight); - background-color: var(--bg-muted-1); - color: var(--fg-muted-2); + &:hover { + box-shadow: var(--edge-highlight); + background-color: var(--bg-muted-1); + color: var(--fg-muted-2); - .icon { - background-color: var(--bg-muted-2); - } - } + .icon { + background-color: var(--bg-muted-2); + } + } - &:active { - transform: var(--active); - } + &:active { + transform: var(--active); + } - &[aria-current="page"] { - box-shadow: var(--edge-highlight); - background-color: var(--bg-muted-1); - color: var(--accent-color); + &[aria-current="page"] { + box-shadow: var(--edge-highlight); + background-color: var(--bg-muted-1); + color: var(--accent-color); - .icon { - @include gradient-fill; - text-shadow: none; - } + .icon { + @include gradient-fill; + text-shadow: none; + } - &:hover { - background-color: var(--bg-muted-2); + &:hover { + background-color: var(--bg-muted-2); - .icon { - @include gradient-fill(hover); - } - } - } + .icon { + @include gradient-fill(hover); + } + } + } - &.new .icon { - position: relative; + &.new .icon { + position: relative; - &::after { - position: absolute; - inset-block-start: rpx(1); - inset-inline-end: rpx(1); - border-radius: 50%; - background-color: var(--accent-color); - width: 0.75rem; - height: 0.75rem; - content: ""; - } - } - } + &::after { + position: absolute; + inset-block-start: rpx(1); + inset-inline-end: rpx(1); + border-radius: 50%; + background-color: var(--accent-color); + width: 0.75rem; + height: 0.75rem; + content: ""; + } + } + } - .icon { - transition: - background-color var(--transition), - color var(--transition), - box-shadow var(--transition); - margin-inline-end: 0.5rem; - box-shadow: var(--edge-highlight); - border-radius: 50%; - background-color: var(--bg-muted-1); - padding: 0.5rem; - font-size: 1.25rem; + .icon { + transition: + background-color var(--transition), + color var(--transition), + box-shadow var(--transition); + margin-inline-end: 0.5rem; + box-shadow: var(--edge-highlight); + border-radius: 50%; + background-color: var(--bg-muted-1); + padding: 0.5rem; + font-size: 1.25rem; - &.fallback { - width: 1.25rem; - font-weight: var(--font-weight-semi-bold); - font-size: var(--font-size-medium); - line-height: 1.25rem; - font-family: var(--font-monospace); - text-align: center; + &.fallback { + width: 1.25rem; + font-weight: var(--font-weight-semi-bold); + font-size: var(--font-size-medium); + line-height: 1.25rem; + font-family: var(--font-monospace); + text-align: center; - &::before { - content: none; - } - } - } + &::before { + content: none; + } + } + } - details { - flex: 1; - margin-block-end: 0; - box-shadow: none; - border-radius: 0; - background-color: transparent; - padding: 0; + details { + flex: 1; + margin-block-end: 0; + box-shadow: none; + border-radius: 0; + background-color: transparent; + padding: 0; - &[open] { - summary { - transform: none; - opacity: 1; - box-shadow: none; - background-color: transparent; + &[open] { + summary { + transform: none; + opacity: 1; + box-shadow: none; + background-color: transparent; - &::before { - transform: none; - } + &::before { + transform: none; + } - @supports not (block-size: calc-size(auto, size)) { - & ~ * { - animation: none; - } - } - } - } + @supports not (block-size: calc-size(auto, size)) { + & ~ * { + animation: none; + } + } + } + } - summary { - display: flex; - position: relative; - align-items: center; - border-radius: 0; - padding: 0.75rem 0; - color: var(--fg-muted-1); - font-weight: var(--font-weight-normal); - font-size: var(--font-size-small); - line-height: var(--font-size-small); - white-space: nowrap; + summary { + display: flex; + position: relative; + align-items: center; + border-radius: 0; + padding: 0.75rem 0; + color: var(--fg-muted-1); + font-weight: var(--font-weight-normal); + font-size: var(--font-size-small); + line-height: var(--font-size-small); + white-space: nowrap; - &:hover { - box-shadow: none; - background-color: transparent; - } + &:hover { + box-shadow: none; + background-color: transparent; + } - &:active { - transform: none; - } + &:active { + transform: none; + } - &::before { - position: absolute; - transform: rotate(-90deg); - transition: transform var(--transition-bezier); - margin-inline-end: 0; - inset-block: 0.5rem; - inset-inline-end: 0; - font-size: 1rem; - } + &::before { + position: absolute; + transform: rotate(-90deg); + transition: transform var(--transition-bezier); + margin-inline-end: 0; + inset-block: 0.5rem; + inset-inline-end: 0; + font-size: 1rem; + } - .line { - flex: 1; - margin-inline: 0.5rem 1.5rem; - background-color: var(--bg-muted-2); - height: rpx(1); - } - } + .line { + flex: 1; + margin-inline: 0.5rem 1.5rem; + background-color: var(--bg-muted-2); + height: rpx(1); + } + } - ul { - margin-block: 0; - } - } + ul { + margin-block: 0; + } + } - ul { - display: flex; - flex-direction: column; - gap: 0.25rem; - margin-block: auto; - padding-inline-start: 0; - list-style: none; - } + ul { + display: flex; + flex-direction: column; + gap: 0.25rem; + margin-block: auto; + padding-inline-start: 0; + list-style: none; + } - li { - display: flex; - transition: width var(--transition); - margin-block: 0; - padding-inline-start: 0; - width: 100%; - } + li { + display: flex; + transition: width var(--transition); + margin-block: 0; + padding-inline-start: 0; + width: 100%; + } } #sidebar-meta { - position: absolute; - transform: perspective(var(--content-width)) rotateY(-30deg); - transform-origin: left; - visibility: hidden; - opacity: 0; - filter: blur(0.25rem); - inset: 0; - - :root[dir*="rtl"] & { - transform-origin: right; - } - - &:has(> div:empty) { - .status-page { - display: flex; - } - - & > div { - display: none; - } - } - - & > div { - display: flex; - flex-direction: column; - gap: 1rem; - margin-block: auto; - } - - .card { - background-color: var(--glass-bg-1); - } - - .while-writing { - display: inherit; - position: relative; - flex-direction: inherit; - gap: inherit; - margin: calc(-0.25rem - rpx(1)); - margin-block-start: 1rem; - border: rpx(1) solid var(--bg-muted-2); - border-block-start: none; - border-end-start-radius: calc(var(--rounded-corner) + 0.25rem + #{rpx(1)}); - border-end-end-radius: calc(var(--rounded-corner) + 0.25rem + #{rpx(1)}); - padding: 0.25rem; - - .card-title { - display: flex; - position: absolute; - gap: 0.25rem; - inset-block-start: calc(var(--rounded-corner-small) * -1); - inset-inline: rpx(-1); - height: var(--rounded-corner-small); - - .line { - border: rpx(1) solid var(--bg-muted-2); - border-block-end: none; - } - - .line:nth-of-type(1) { - border-inline-end: none; - border-start-start-radius: var(--rounded-corner); - width: var(--rounded-corner); - } - - .line:nth-of-type(2) { - flex: 1; - border-inline-start: none; - border-start-end-radius: var(--rounded-corner); - } - - .title { - color: var(--fg-muted-2); - font-size: var(--font-size-small); - line-height: 0; - } - } - } - - .weather { - display: flex; - justify-content: space-between; - align-items: center; - gap: 1rem; - border-radius: 1.375rem; - padding: 0.25rem; - padding-inline-end: 1.25rem; - line-height: 1; - - &.cloudy { - animation: perlin 12s linear infinite; - background-image: url("/weather/perlin.png"); - - @media (prefers-color-scheme: dark) { - background-image: url("/weather/perlin-dark.png"); - } - } - - &.showers { - animation: rain 3s linear infinite; - background-image: url("/weather/rain.svg"); - - @media (prefers-color-scheme: dark) { - background-image: url("/weather/rain-dark.svg"); - } - } - - &.snowing { - animation: snow 12s linear infinite; - background-image: url("/weather/snow.svg"); - - @media (prefers-color-scheme: dark) { - background-image: url("/weather/snow-dark.svg"); - } - } - - div { - display: flex; - align-items: center; - gap: 0.5rem; - } - - .title { - color: inherit; - font-size: var(--font-size-medium); - } - - .icon { - box-shadow: var(--edge-highlight); - border-radius: 50%; - background-color: var(--bg-muted-1); - padding: 0.5rem; - font-size: 1.25rem; - } - - .condition { - color: var(--fg-muted-2); - font-size: var(--font-size-small); - } - - @keyframes rain { - to { - background-position: 256px 320px; - } - } - - @keyframes perlin { - to { - background-position: -256px -64px; - } - } - - @keyframes snow { - to { - background-position: 256px 512px; - } - } - } - - .music { - display: flex; - position: relative; - flex-direction: column; - gap: 0.5rem; - border-radius: 1rem; - - &::after { - position: absolute; - z-index: 1; - inset: 0; - box-shadow: var(--edge-highlight); - border-radius: inherit; - max-height: 5rem; - pointer-events: none; - content: ""; - } - - .carousel { - scroll-snap-type: x mandatory; - -webkit-overflow-scrolling: touch; - display: flex; - margin-block: 0; - box-shadow: var(--shadow); - border-radius: inherit; - background-color: var(--glass-bg-1); - padding-inline-start: 0; - overflow-x: auto; - scroll-behavior: smooth; - scrollbar-width: none; - list-style: none; - - li { - display: flex; - position: relative; - flex-shrink: 0; - gap: 1rem; - margin-block: 0; - outline: none; - padding: 0.5rem; - width: 100%; - overflow: hidden; - scroll-snap-align: start; - - &.has-cover { - animation: cover-alternate 60s ease-in-out infinite; - background-image: var(--bg-overlay), var(--blurnail); - background-position: center; - background-size: 2000%; - - @keyframes cover-alternate { - // X 100% - // Y ┌─────────────┐ - // │ 1,9 │ - // │ 8 2 │ - // │ 3,7 │ - // │ 6 4 │ - // │ 5 │ - // 100% └─────────────┘ - - from { - background-position: 50% 0%; - } - - 12.5% { - background-position: 100% 25%; - } - - 25% { - background-position: 50% 50%; - } - - 37.5% { - background-position: 100% 75%; - } - - 50% { - background-position: 50% 100%; - } - - 62.5% { - background-position: 0% 75%; - } - - 75% { - background-position: 50% 50%; - } - - 87.5% { - background-position: 0% 25%; - } - - to { - background-position: 50% 0%; - } - } - - .music-type { - color: var(--fg-muted-neutral-1); - } - - .music-artist { - color: var(--fg-muted-neutral-2); - } - } - - & > div { - display: flex; - flex: 1; - flex-direction: column; - justify-content: space-between; - gap: 0.25rem; - margin-inline-end: 0.5rem; - margin-block: 0.25rem; - min-width: 0; - min-width: 0; - line-height: 1; - } - - a { - position: absolute; - z-index: 1; - inset: 0; - border-radius: inherit; - } - } - } - - .music-type { - color: var(--fg-muted-1); - font-size: var(--font-size-xx-small); - } - - .music-artist { - color: var(--fg-muted-2); - font-size: var(--font-size-small); - } - - .title { - color: var(--fg-color); - font-size: var(--font-size-medium); - } - - img { - margin: 0; - border-radius: var(--rounded-corner-small); - } - - &-paginator { - display: flex; - justify-content: center; - gap: 0.25rem; - margin-inline: 1rem; - margin-block: 0; - padding-inline-start: 0; - list-style: none; - - li { - display: flex; - margin-block: 0; - padding-inline-start: 0; - height: 0.75rem; - } - - a { - transition: - width var(--transition), - height var(--transition), - background-color var(--transition), - transform var(--transition-bezier); - margin-block: auto; - border-radius: 0.375rem; - background-color: var(--fg-muted-1); - width: 1.5rem; - height: 0.375rem; - - &:hover { - background-color: var(--fg-muted-2); - } - - &:active { - transform: var(--active); - } - - &.active { - background-color: var(--fg-color); - width: 0.75rem; - height: 0.75rem; - } - } - } - } - - .toc { - display: flex; - flex-direction: column; - max-height: calc(var(--content-width) / 3); - - @media only screen and (max-width: 720px) { - max-height: calc(var(--content-width) / 2); - } - - .title { - margin-block-end: 1rem; - } - - .overshoot { - flex: 1; - margin: -1rem; - padding: 1rem; - overflow: auto; - scrollbar-width: thin; - - ul { - margin-block: 0; - padding-inline-start: 1rem; - color: var(--fg-muted-1); - font-size: var(--font-size-small); - } - - li { - margin-block: 0; - padding-inline-start: 0; - - &::marker { - color: inherit; - } - } - - a { - color: inherit; - text-decoration: none; - } - } - } - - .status-page { - display: none; - margin-block: auto; - text-align: center; - - .title, - p { - color: var(--fg-muted-2); - } - - .icon { - margin-inline: auto; - color: var(--fg-muted-1); - font-size: 3rem; - } - - p { - margin-block: 0; - } - } + position: absolute; + transform: perspective(var(--content-width)) rotateY(-30deg); + transform-origin: left; + visibility: hidden; + opacity: 0; + filter: blur(0.25rem); + inset: 0; + + :root[dir*="rtl"] & { + transform-origin: right; + } + + &:has(> div:empty) { + .status-page { + display: flex; + } + + & > div { + display: none; + } + } + + & > div { + display: flex; + flex-direction: column; + gap: 1rem; + margin-block: auto; + } + + .card { + background-color: var(--glass-bg-1); + } + + .while-writing { + display: inherit; + position: relative; + flex-direction: inherit; + gap: inherit; + margin: calc(-0.25rem - rpx(1)); + margin-block-start: 1rem; + border: rpx(1) solid var(--bg-muted-2); + border-block-start: none; + border-end-start-radius: calc(var(--rounded-corner) + 0.25rem + #{rpx(1)}); + border-end-end-radius: calc(var(--rounded-corner) + 0.25rem + #{rpx(1)}); + padding: 0.25rem; + + .card-title { + display: flex; + position: absolute; + gap: 0.25rem; + inset-block-start: calc(var(--rounded-corner-small) * -1); + inset-inline: rpx(-1); + height: var(--rounded-corner-small); + + .line { + border: rpx(1) solid var(--bg-muted-2); + border-block-end: none; + } + + .line:nth-of-type(1) { + border-inline-end: none; + border-start-start-radius: var(--rounded-corner); + width: var(--rounded-corner); + } + + .line:nth-of-type(2) { + flex: 1; + border-inline-start: none; + border-start-end-radius: var(--rounded-corner); + } + + .title { + color: var(--fg-muted-2); + font-size: var(--font-size-small); + line-height: 0; + } + } + } + + .weather { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + border-radius: 1.375rem; + padding: 0.25rem; + padding-inline-end: 1.25rem; + line-height: 1; + + &.cloudy { + animation: perlin 12s linear infinite; + background-image: url("/weather/perlin.png"); + + @media (prefers-color-scheme: dark) { + background-image: url("/weather/perlin-dark.png"); + } + } + + &.showers { + animation: rain 3s linear infinite; + background-image: url("/weather/rain.svg"); + + @media (prefers-color-scheme: dark) { + background-image: url("/weather/rain-dark.svg"); + } + } + + &.snowing { + animation: snow 12s linear infinite; + background-image: url("/weather/snow.svg"); + + @media (prefers-color-scheme: dark) { + background-image: url("/weather/snow-dark.svg"); + } + } + + div { + display: flex; + align-items: center; + gap: 0.5rem; + } + + .title { + color: inherit; + font-size: var(--font-size-medium); + } + + .icon { + box-shadow: var(--edge-highlight); + border-radius: 50%; + background-color: var(--bg-muted-1); + padding: 0.5rem; + font-size: 1.25rem; + } + + .condition { + color: var(--fg-muted-2); + font-size: var(--font-size-small); + } + + @keyframes rain { + to { + background-position: 256px 320px; + } + } + + @keyframes perlin { + to { + background-position: -256px -64px; + } + } + + @keyframes snow { + to { + background-position: 256px 512px; + } + } + } + + .music { + display: flex; + position: relative; + flex-direction: column; + gap: 0.5rem; + border-radius: 1rem; + + &::after { + position: absolute; + z-index: 1; + inset: 0; + box-shadow: var(--edge-highlight); + border-radius: inherit; + max-height: 5rem; + pointer-events: none; + content: ""; + } + + .carousel { + scroll-snap-type: x mandatory; + -webkit-overflow-scrolling: touch; + display: flex; + margin-block: 0; + box-shadow: var(--shadow); + border-radius: inherit; + background-color: var(--glass-bg-1); + padding-inline-start: 0; + overflow-x: auto; + scroll-behavior: smooth; + scrollbar-width: none; + list-style: none; + + li { + display: flex; + position: relative; + flex-shrink: 0; + gap: 1rem; + margin-block: 0; + outline: none; + padding: 0.5rem; + width: 100%; + overflow: hidden; + scroll-snap-align: start; + + &.has-cover { + animation: cover-alternate 60s ease-in-out infinite; + background-image: var(--bg-overlay), var(--blurnail); + background-position: center; + background-size: 2000%; + + @keyframes cover-alternate { + // X 100% + // Y ┌─────────────┐ + // │ 1,9 │ + // │ 8 2 │ + // │ 3,7 │ + // │ 6 4 │ + // │ 5 │ + // 100% └─────────────┘ + + from { + background-position: 50% 0%; + } + + 12.5% { + background-position: 100% 25%; + } + + 25% { + background-position: 50% 50%; + } + + 37.5% { + background-position: 100% 75%; + } + + 50% { + background-position: 50% 100%; + } + + 62.5% { + background-position: 0% 75%; + } + + 75% { + background-position: 50% 50%; + } + + 87.5% { + background-position: 0% 25%; + } + + to { + background-position: 50% 0%; + } + } + + .music-type { + color: var(--fg-muted-neutral-1); + } + + .music-artist { + color: var(--fg-muted-neutral-2); + } + } + + & > div { + display: flex; + flex: 1; + flex-direction: column; + justify-content: space-between; + gap: 0.25rem; + margin-inline-end: 0.5rem; + margin-block: 0.25rem; + min-width: 0; + min-width: 0; + line-height: 1; + } + + a { + position: absolute; + z-index: 1; + inset: 0; + border-radius: inherit; + } + } + } + + .music-type { + color: var(--fg-muted-1); + font-size: var(--font-size-xx-small); + } + + .music-artist { + color: var(--fg-muted-2); + font-size: var(--font-size-small); + } + + .title { + color: var(--fg-color); + font-size: var(--font-size-medium); + } + + img { + margin: 0; + border-radius: var(--rounded-corner-small); + } + + &-paginator { + display: flex; + justify-content: center; + gap: 0.25rem; + margin-inline: 1rem; + margin-block: 0; + padding-inline-start: 0; + list-style: none; + + li { + display: flex; + margin-block: 0; + padding-inline-start: 0; + height: 0.75rem; + } + + a { + transition: + width var(--transition), + height var(--transition), + background-color var(--transition), + transform var(--transition-bezier); + margin-block: auto; + border-radius: 0.375rem; + background-color: var(--fg-muted-1); + width: 1.5rem; + height: 0.375rem; + + &:hover { + background-color: var(--fg-muted-2); + } + + &:active { + transform: var(--active); + } + + &.active { + background-color: var(--fg-color); + width: 0.75rem; + height: 0.75rem; + } + } + } + } + + .toc { + display: flex; + flex-direction: column; + max-height: calc(var(--content-width) / 3); + + @media only screen and (max-width: 720px) { + max-height: calc(var(--content-width) / 2); + } + + .title { + margin-block-end: 1rem; + } + + .overshoot { + flex: 1; + margin: -1rem; + padding: 1rem; + overflow: auto; + scrollbar-width: thin; + + ul { + margin-block: 0; + padding-inline-start: 1rem; + color: var(--fg-muted-1); + font-size: var(--font-size-small); + } + + li { + margin-block: 0; + padding-inline-start: 0; + + &::marker { + color: inherit; + } + } + + a { + color: inherit; + text-decoration: none; + } + } + } + + .status-page { + display: none; + margin-block: auto; + text-align: center; + + .title, + p { + color: var(--fg-muted-2); + } + + .icon { + margin-inline: auto; + color: var(--fg-muted-1); + font-size: 3rem; + } + + p { + margin-block: 0; + } + } } diff --git a/modules/amezing/sass/mods/_compact-sidebar.scss b/modules/amezing/sass/mods/_compact-sidebar.scss index 3169af3..91eafa7 100644 --- a/modules/amezing/sass/mods/_compact-sidebar.scss +++ b/modules/amezing/sass/mods/_compact-sidebar.scss @@ -2,43 +2,43 @@ @use "../citrine/abstracts/mixins" as *; #site-sidebar-header { - transform-origin: rpx(18) 50%; - border-radius: var(--rounded-corner); + transform-origin: rpx(18) 50%; + border-radius: var(--rounded-corner); - img, - .icon { - border-radius: var(--rounded-corner-small); - } + img, + .icon { + border-radius: var(--rounded-corner-small); + } - img { - width: 2rem; - height: 2rem; - } + img { + width: 2rem; + height: 2rem; + } - .icon { - padding: 0.375rem; - } + .icon { + padding: 0.375rem; + } - div { - gap: 0; - line-height: 1.125; - } + div { + gap: 0; + line-height: 1.125; + } } #sidebar-nav { - a { - transform-origin: rpx(18) 50%; - border-radius: var(--rounded-corner); - padding: 0.25rem; + a { + transform-origin: rpx(18) 50%; + border-radius: var(--rounded-corner); + padding: 0.25rem; - &.new .icon::after { - inset-block-start: rpx(-2); - inset-inline-end: rpx(-2); - } - } + &.new .icon::after { + inset-block-start: rpx(-2); + inset-inline-end: rpx(-2); + } + } - .icon { - border-radius: var(--rounded-corner-small); - padding: 0.375rem; - } + .icon { + border-radius: var(--rounded-corner-small); + padding: 0.375rem; + } } diff --git a/modules/amezing/sass/mods/_flat.scss b/modules/amezing/sass/mods/_flat.scss index 92e04da..7b73cde 100644 --- a/modules/amezing/sass/mods/_flat.scss +++ b/modules/amezing/sass/mods/_flat.scss @@ -1,27 +1,27 @@ @use "../citrine/abstracts/functions" as *; :root { - --color-opacity-1: 0.15; - --color-opacity-2: 0.25; - --edge-highlight: 0 0 0 transparent; - --shadow: 0 0 0 transparent; - --shadow-raised: 0 0 0 transparent; - --shadow-glass: 0 0 0 transparent; - --shadow-glow: 0 0 0 transparent; + --color-opacity-1: 0.15; + --color-opacity-2: 0.25; + --edge-highlight: 0 0 0 transparent; + --shadow: 0 0 0 transparent; + --shadow-raised: 0 0 0 transparent; + --shadow-glass: 0 0 0 transparent; + --shadow-glow: 0 0 0 transparent; } .buttons .suggested, #site-nav a.active .icon { - box-shadow: none; - background-image: none; - background-color: var(--accent-color); - text-shadow: none; + box-shadow: none; + background-image: none; + background-color: var(--accent-color); + text-shadow: none; } .buttons .suggested:hover { - opacity: var(--dim-opacity); + opacity: var(--dim-opacity); } .crt { - outline: rpx(1) solid var(--bg-muted-2); + outline: rpx(1) solid var(--bg-muted-2); } diff --git a/modules/amezing/sass/mods/_pill-tags.scss b/modules/amezing/sass/mods/_pill-tags.scss index 37985cf..0dc78ff 100644 --- a/modules/amezing/sass/mods/_pill-tags.scss +++ b/modules/amezing/sass/mods/_pill-tags.scss @@ -1,27 +1,27 @@ @use "../citrine/abstracts/mixins" as *; .article-details .tags { - gap: 0.25rem; - margin-block-end: 0 !important; + gap: 0.25rem; + margin-block-end: 0 !important; - a { - @include chip; - display: flex; - color: var(--fg-muted-1); - font-weight: var(--font-weight-semi-bold); - line-height: 1.5rem; + a { + @include chip; + display: flex; + color: var(--fg-muted-1); + font-weight: var(--font-weight-semi-bold); + line-height: 1.5rem; - &:hover { - background-color: var(--bg-muted-2); - color: var(--fg-muted-2); - } + &:hover { + background-color: var(--bg-muted-2); + color: var(--fg-muted-2); + } - &:active { - transform: var(--active); - } + &:active { + transform: var(--active); + } - &::before { - content: none; - } - } + &::before { + content: none; + } + } } diff --git a/modules/amezing/sass/mods/_skeuomorphic.scss b/modules/amezing/sass/mods/_skeuomorphic.scss index 7b382ea..0328d25 100644 --- a/modules/amezing/sass/mods/_skeuomorphic.scss +++ b/modules/amezing/sass/mods/_skeuomorphic.scss @@ -2,29 +2,29 @@ @use "../citrine/abstracts/functions" as *; #site-nav { - a, - label, - button, - summary { - outline: rpx(1) solid var(--bg-muted-2); - box-shadow: var(--edge-highlight), var(--shadow); - background-image: linear-gradient( - to bottom, - var(--bg-muted-2), - var(--bg-muted-1) 50%, - transparent 50%, - var(--bg-muted-2) - ); + a, + label, + button, + summary { + outline: rpx(1) solid var(--bg-muted-2); + box-shadow: var(--edge-highlight), var(--shadow); + background-image: linear-gradient( + to bottom, + var(--bg-muted-2), + var(--bg-muted-1) 50%, + transparent 50%, + var(--bg-muted-2) + ); - .icon { - background-image: conic-gradient( - var(--bg-muted-2), - var(--bg-muted-1) 33%, - transparent, - var(--bg-muted-2) 66%, - transparent, - var(--bg-muted-2) - ); - } - } + .icon { + background-image: conic-gradient( + var(--bg-muted-2), + var(--bg-muted-1) 33%, + transparent, + var(--bg-muted-2) 66%, + transparent, + var(--bg-muted-2) + ); + } + } } diff --git a/modules/amezing/scripts/audio-button.js b/modules/amezing/scripts/audio-button.js index e76d3fc..60368ab 100644 --- a/modules/amezing/scripts/audio-button.js +++ b/modules/amezing/scripts/audio-button.js @@ -1,9 +1,9 @@ const audioButtons = document.querySelectorAll(".audio"); audioButtons.forEach((button) => { - button.addEventListener("click", (event) => playAudio(button.dataset.audio)); + button.addEventListener("click", (event) => playAudio(button.dataset.audio)); }); function playAudio(url) { - new Audio(url).play(); + new Audio(url).play(); } diff --git a/modules/amezing/scripts/closable.js b/modules/amezing/scripts/closable.js index a28774c..f65ad13 100644 --- a/modules/amezing/scripts/closable.js +++ b/modules/amezing/scripts/closable.js @@ -1,34 +1,34 @@ const closable = document.querySelectorAll("details.closable"); closable.forEach((detail) => { - detail.addEventListener("toggle", () => { - if (detail.open) setTargetDetail(detail); - }); + detail.addEventListener("toggle", () => { + if (detail.open) setTargetDetail(detail); + }); }); function setTargetDetail(targetDetail) { - closable.forEach((detail) => { - if (detail !== targetDetail) { - detail.open = false; - } - }); + closable.forEach((detail) => { + if (detail !== targetDetail) { + detail.open = false; + } + }); } document.addEventListener("click", function (event) { - const isClickInsideDetail = [...closable].some((detail) => - detail.contains(event.target), - ); + const isClickInsideDetail = [...closable].some((detail) => + detail.contains(event.target), + ); - if (!isClickInsideDetail) { - closable.forEach((detail) => { - detail.open = false; - }); - } + if (!isClickInsideDetail) { + closable.forEach((detail) => { + detail.open = false; + }); + } }); const expandSidebar = document.getElementById("expand-sidebar"); const mainContent = document.getElementById("main-content"); mainContent.addEventListener("click", () => { - if (expandSidebar.checked) expandSidebar.checked = false; + if (expandSidebar.checked) expandSidebar.checked = false; }); diff --git a/modules/amezing/scripts/deanchor.js b/modules/amezing/scripts/deanchor.js index 94fbee5..2726e89 100644 --- a/modules/amezing/scripts/deanchor.js +++ b/modules/amezing/scripts/deanchor.js @@ -1,10 +1,10 @@ const main = document.querySelector("#main-content"); main.addEventListener("animationend", () => { - const hash = decodeURIComponent(location.hash); - if ( - ["#↑", "#↓", "#←", "#→", "#up", "#down", "#left", "#right"].includes(hash) - ) { - history.replaceState(null, "", " "); - } + const hash = decodeURIComponent(location.hash); + if ( + ["#↑", "#↓", "#←", "#→", "#up", "#down", "#left", "#right"].includes(hash) + ) { + history.replaceState(null, "", " "); + } }); diff --git a/modules/amezing/scripts/fuse.js b/modules/amezing/scripts/fuse.js index 4357f5a..5b930ca 100644 --- a/modules/amezing/scripts/fuse.js +++ b/modules/amezing/scripts/fuse.js @@ -6,4 +6,1930 @@ * * http://www.apache.org/licenses/LICENSE-2.0 */ -var e,t;e=this,t=function(){"use strict";function e(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function t(t){for(var n=1;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n0&&void 0!==arguments[0]?arguments[0]:{},n=t.getFn,i=void 0===n?j.getFn:n,u=t.fieldNormWeight,o=void 0===u?j.fieldNormWeight:u;r(this,e),this.norm=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:3,n=new Map,r=Math.pow(10,t);return{get:function(t){var i=t.match(L).length;if(n.has(i))return n.get(i);var u=1/Math.pow(i,.5*e),o=parseFloat(Math.round(u*r)/r);return n.set(i,o),o},clear:function(){n.clear()}}}(o,3),this.getFn=i,this.isCreated=!1,this.setIndexRecords()}return u(e,[{key:"setSources",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.docs=e}},{key:"setIndexRecords",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.records=e}},{key:"setKeys",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.keys=t,this._keysMap={},t.forEach((function(t,n){e._keysMap[t.id]=n}))}},{key:"create",value:function(){var e=this;!this.isCreated&&this.docs.length&&(this.isCreated=!0,A(this.docs[0])?this.docs.forEach((function(t,n){e._addString(t,n)})):this.docs.forEach((function(t,n){e._addObject(t,n)})),this.norm.clear())}},{key:"add",value:function(e){var t=this.size();A(e)?this._addString(e,t):this._addObject(e,t)}},{key:"removeAt",value:function(e){this.records.splice(e,1);for(var t=e,n=this.size();t=0;t-=1)this.records.splice(e[t],1);for(var n=0,r=this.records.length;n2&&void 0!==arguments[2]?arguments[2]:{},r=n.getFn,i=void 0===r?j.getFn:r,u=n.fieldNormWeight,o=void 0===u?j.fieldNormWeight:u,c=new I({getFn:i,fieldNormWeight:o});return c.setKeys(e.map(_)),c.setSources(t),c.create(),c}var $=32;function R(e,t,n){var r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{},i=r.location,u=void 0===i?j.location:i,o=r.distance,c=void 0===o?j.distance:o,s=r.threshold,a=void 0===s?j.threshold:s,h=r.findAllMatches,l=void 0===h?j.findAllMatches:h,f=r.minMatchCharLength,d=void 0===f?j.minMatchCharLength:f,v=r.includeMatches,g=void 0===v?j.includeMatches:v,y=r.ignoreLocation,p=void 0===y?j.ignoreLocation:y;if(t.length>$)throw new Error("Pattern length exceeds max of ".concat($,"."));for(var A,m=t.length,C=e.length,k=Math.max(0,Math.min(u,C)),F=a,b=k,M=function(e,t){var n=e/m;if(p)return n;var r=Math.abs(k-t);return c?n+r/c:r?1:n},E=d>1||g,D=E?Array(C):[];(A=e.indexOf(t,b))>-1;){var B=M(0,A);if(F=Math.min(B,F),b=A+m,E)for(var x=0;x=R;z-=1){var T=z-1,K=n[e[T]];if(E&&(D[T]=+!!K),W[z]=(W[z+1]<<1|1)&K,L&&(W[z]|=(_[z+1]|_[z])<<1|1|_[z+1]),W[z]&O&&(S=M(L,T))<=F){if(F=S,(b=T)<=k)break;R=Math.max(1,2*k-b)}}if(M(L+1,k)>F)break;_=W}var U={isMatch:b>=0,score:Math.max(.001,S)};if(E){var q=function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:j.minMatchCharLength,n=[],r=-1,i=-1,u=0,o=e.length;u=t&&n.push([r,i]),r=-1)}return e[u-1]&&u-r>=t&&n.push([r,u-1]),n}(D,d);q.length?g&&(U.indices=q):U.isMatch=!1}return U}function P(e){for(var t={},n=0,r=e.length;n1&&void 0!==arguments[1]?arguments[1]:{},u=i.location,o=void 0===u?j.location:u,c=i.threshold,s=void 0===c?j.threshold:c,a=i.distance,h=void 0===a?j.distance:a,l=i.includeMatches,f=void 0===l?j.includeMatches:l,d=i.findAllMatches,v=void 0===d?j.findAllMatches:d,g=i.minMatchCharLength,y=void 0===g?j.minMatchCharLength:g,p=i.isCaseSensitive,A=void 0===p?j.isCaseSensitive:p,m=i.ignoreDiacritics,C=void 0===m?j.ignoreDiacritics:m,k=i.ignoreLocation,F=void 0===k?j.ignoreLocation:k;if(r(this,e),this.options={location:o,threshold:s,distance:h,includeMatches:f,findAllMatches:v,minMatchCharLength:y,isCaseSensitive:A,ignoreDiacritics:C,ignoreLocation:F},t=A?t:t.toLowerCase(),t=C?W(t):t,this.pattern=t,this.chunks=[],this.pattern.length){var b=function(e,t){n.chunks.push({pattern:e,alphabet:P(e),startIndex:t})},M=this.pattern.length;if(M>$){for(var E=0,D=M%$,B=M-D;E1&&void 0!==arguments[1]?arguments[1]:{},o=u.location,c=void 0===o?j.location:o,s=u.threshold,a=void 0===s?j.threshold:s,h=u.distance,l=void 0===h?j.distance:h,f=u.includeMatches,d=void 0===f?j.includeMatches:f,v=u.findAllMatches,g=void 0===v?j.findAllMatches:v,y=u.minMatchCharLength,p=void 0===y?j.minMatchCharLength:y,A=u.isCaseSensitive,m=void 0===A?j.isCaseSensitive:A,C=u.ignoreDiacritics,k=void 0===C?j.ignoreDiacritics:C,F=u.ignoreLocation,b=void 0===F?j.ignoreLocation:F;return r(this,n),(i=t.call(this,e))._bitapSearch=new z(e,{location:c,threshold:a,distance:l,includeMatches:d,findAllMatches:g,minMatchCharLength:p,isCaseSensitive:m,ignoreDiacritics:k,ignoreLocation:b}),i}return u(n,[{key:"search",value:function(e){return this._bitapSearch.searchIn(e)}}],[{key:"type",get:function(){return"fuzzy"}},{key:"multiRegex",get:function(){return/^"(.*)"$/}},{key:"singleRegex",get:function(){return/^(.*)$/}}]),n}(T),X=function(e){c(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return u(n,[{key:"search",value:function(e){for(var t,n=0,r=[],i=this.pattern.length;(t=e.indexOf(this.pattern,n))>-1;)n=t+i,r.push([t,n-1]);var u=!!r.length;return{isMatch:u,score:u?0:1,indices:r}}}],[{key:"type",get:function(){return"include"}},{key:"multiRegex",get:function(){return/^'"(.*)"$/}},{key:"singleRegex",get:function(){return/^'(.*)$/}}]),n}(T),Y=[U,X,J,Q,G,V,q,H],Z=Y.length,ee=/ +(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)/,te=new Set([H.type,X.type]),ne=function(){function e(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=n.isCaseSensitive,u=void 0===i?j.isCaseSensitive:i,o=n.ignoreDiacritics,c=void 0===o?j.ignoreDiacritics:o,s=n.includeMatches,a=void 0===s?j.includeMatches:s,h=n.minMatchCharLength,l=void 0===h?j.minMatchCharLength:h,f=n.ignoreLocation,d=void 0===f?j.ignoreLocation:f,v=n.findAllMatches,g=void 0===v?j.findAllMatches:v,y=n.location,p=void 0===y?j.location:y,A=n.threshold,m=void 0===A?j.threshold:A,C=n.distance,k=void 0===C?j.distance:C;r(this,e),this.query=null,this.options={isCaseSensitive:u,ignoreDiacritics:c,includeMatches:a,minMatchCharLength:l,findAllMatches:g,ignoreLocation:d,location:p,threshold:m,distance:k},t=u?t:t.toLowerCase(),t=c?W(t):t,this.pattern=t,this.query=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.split("|").map((function(e){for(var n=e.trim().split(ee).filter((function(e){return e&&!!e.trim()})),r=[],i=0,u=n.length;i2&&void 0!==arguments[2]?arguments[2]:{}).auto,r=void 0===n||n;return he(e)||(e=le(e)),function e(n){var i=Object.keys(n),u=function(e){return!!e[se]}(n);if(!u&&i.length>1&&!he(n))return e(le(n));if(function(e){return!g(e)&&k(e)&&!he(e)}(n)){var o=u?n[se]:i[0],c=u?n[ae]:n[o];if(!A(c))throw new Error(function(e){return"Invalid value for key ".concat(e)}(o));var s={keyId:w(o),pattern:c};return r&&(s.searcher=ue(c,t)),s}var a={children:[],operator:i[0]};return i.forEach((function(t){var r=n[t];g(r)&&r.forEach((function(t){a.children.push(e(t))}))})),a}(e)}function de(e,t){var n=t.ignoreFieldNorm,r=void 0===n?j.ignoreFieldNorm:n,i=1;e.matches.forEach((function(e){var t=e.key,n=e.norm,u=e.score,o=t?t.weight:null;i*=Math.pow(0===u&&o?Number.EPSILON:u,(o||1)*(r?1:n))})),e.score=i}var ve=function(){function e(t){r(this,e),this.limit=t,this.heap=[]}return u(e,[{key:"size",get:function(){return this.heap.length}},{key:"shouldInsert",value:function(e){return this.size0;){var n=e-1>>1;if(t[e].score<=t[n].score)break;var r=t[e];t[e]=t[n],t[n]=r,e=n}}},{key:"_sinkDown",value:function(e){var t=this.heap,n=t.length,r=e;do{var i=2*(e=r)+1,u=2*e+2;if(it[r].score&&(r=i),ut[r].score&&(r=u),r!==e){var o=t[e];t[e]=t[r],t[r]=o}}while(r!==e)}}]),e}();function ge(e,t){var n=e.matches;t.matches=[],F(n)&&n.forEach((function(e){if(F(e.indices)&&e.indices.length){var n={indices:e.indices,value:e.value};e.key&&(n.key=e.key.src),e.idx>-1&&(n.refIndex=e.idx),t.matches.push(n)}}))}function ye(e,t){t.score=e.score}var pe=function(){function e(n){var i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},u=arguments.length>2?arguments[2]:void 0;r(this,e),this.options=t(t({},j),i),this.options.useExtendedSearch,this._keyStore=new x(this.options.keys),this.setCollection(n,u),this._lastQuery=null,this._lastSearcher=null}return u(e,[{key:"_getSearcher",value:function(e){if(this._lastQuery===e)return this._lastSearcher;var t=ue(e,this.options);return this._lastQuery=e,this._lastSearcher=t,t}},{key:"setCollection",value:function(e,t){if(this._docs=e,t&&!(t instanceof I))throw new Error("Incorrect 'index' type");this._myIndex=t||N(this.options.keys,this._docs,{getFn:this.options.getFn,fieldNormWeight:this.options.fieldNormWeight})}},{key:"add",value:function(e){F(e)&&(this._docs.push(e),this._myIndex.add(e))}},{key:"remove",value:function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:function(){return!1},t=[],n=[],r=0,i=this._docs.length;r=0;u-=1)this._docs.splice(n[u],1);this._myIndex.removeAll(n)}return t}},{key:"removeAt",value:function(e){this._docs.splice(e,1),this._myIndex.removeAt(e)}},{key:"getIndex",value:function(){return this._myIndex}},{key:"search",value:function(e){var t,n=(arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).limit,r=void 0===n?-1:n,i=this.options,u=i.includeMatches,o=i.includeScore,c=i.shouldSort,s=i.sortFn,a=i.ignoreFieldNorm;if(m(r)&&r>0&&A(e)){var h=new ve(r);A(this._docs[0])?this._searchStringList(e,{heap:h,ignoreFieldNorm:a}):this._searchObjectList(e,{heap:h,ignoreFieldNorm:a}),t=h.extractSorted(s)}else(function(e,t){var n=t.ignoreFieldNorm,r=void 0===n?j.ignoreFieldNorm:n;e.forEach((function(e){de(e,{ignoreFieldNorm:r})}))})(t=A(e)?A(this._docs[0])?this._searchStringList(e):this._searchObjectList(e):this._searchLogical(e),{ignoreFieldNorm:a}),c&&t.sort(s),m(r)&&r>-1&&(t=t.slice(0,r));return function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=n.includeMatches,i=void 0===r?j.includeMatches:r,u=n.includeScore,o=void 0===u?j.includeScore:u,c=[];return i&&c.push(ge),o&&c.push(ye),e.map((function(e){var n=e.idx,r={item:t[n],refIndex:n};return c.length&&c.forEach((function(t){t(e,r)})),r}))}(t,this._docs,{includeMatches:u,includeScore:o})}},{key:"_searchStringList",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.heap,r=t.ignoreFieldNorm,i=this._getSearcher(e),u=this._myIndex.records,o=n?null:[];return u.forEach((function(e){var t=e.v,u=e.i,c=e.n;if(F(t)){var s=i.searchIn(t),a=s.isMatch,h=s.score,l=s.indices;if(a){var f={item:t,idx:u,matches:[{score:h,value:t,norm:c,indices:l}]};n?(de(f,{ignoreFieldNorm:r}),n.shouldInsert(f.score)&&n.insert(f)):o.push(f)}}})),o}},{key:"_searchLogical",value:function(e){var t=this,n=fe(e,this.options),r=function e(n,r,i){if(!n.children){var u=n.keyId,o=n.searcher,c=t._findMatches({key:t._keyStore.get(u),value:t._myIndex.getValueForItemAtKeyId(r,u),searcher:o});return c&&c.length?[{idx:i,item:r,matches:c}]:[]}for(var s=[],a=0,h=n.children.length;a1&&void 0!==arguments[1]?arguments[1]:{},r=n.heap,i=n.ignoreFieldNorm,u=this._getSearcher(e),o=this._myIndex,c=o.keys,s=o.records,a=r?null:[];return s.forEach((function(e){var n=e.$,o=e.i;if(F(n)){var s=[];if(c.forEach((function(e,r){s.push.apply(s,f(t._findMatches({key:e,value:n[r],searcher:u})))})),s.length){var h={idx:o,item:n,matches:s};r?(de(h,{ignoreFieldNorm:i}),r.shouldInsert(h.score)&&r.insert(h)):a.push(h)}}})),a}},{key:"_findMatches",value:function(e){var t=e.key,n=e.value,r=e.searcher;if(!F(n))return[];var i=[];if(g(n))n.forEach((function(e){var n=e.v,u=e.i,o=e.n;if(F(n)){var c=r.searchIn(n),s=c.isMatch,a=c.score,h=c.indices;s&&i.push({score:a,key:t,value:n,idx:u,norm:o,indices:h})}}));else{var u=n.v,o=n.n,c=r.searchIn(u),s=c.isMatch,a=c.score,h=c.indices;s&&i.push({score:a,key:t,value:u,norm:o,indices:h})}return i}}]),e}();return pe.version="7.2.0",pe.createIndex=N,pe.parseIndex=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.getFn,r=void 0===n?j.getFn:n,i=t.fieldNormWeight,u=void 0===i?j.fieldNormWeight:i,o=e.keys,c=e.records,s=new I({getFn:r,fieldNormWeight:u});return s.setKeys(o),s.setIndexRecords(c),s},pe.config=j,ie(ne),pe.use=function(){for(var e=arguments.length,t=new Array(e),n=0;n e.length) && (t = e.length); + for (var n = 0, r = new Array(t); n < t; n++) r[n] = e[n]; + return r; + } + function v(e) { + var t = (function (e, t) { + if ("object" != typeof e || null === e) return e; + var n = e[Symbol.toPrimitive]; + if (void 0 !== n) { + var r = n.call(e, t || "default"); + if ("object" != typeof r) return r; + throw new TypeError("@@toPrimitive must return a primitive value."); + } + return ("string" === t ? String : Number)(e); + })(e, "string"); + return "symbol" == typeof t ? t : String(t); + } + function g(e) { + return Array.isArray ? Array.isArray(e) : "[object Array]" === M(e); + } + var y = 1 / 0; + function p(e) { + return null == e + ? "" + : (function (e) { + if ("string" == typeof e) return e; + var t = e + ""; + return "0" == t && 1 / e == -y ? "-0" : t; + })(e); + } + function A(e) { + return "string" == typeof e; + } + function m(e) { + return "number" == typeof e; + } + function C(e) { + return ( + !0 === e || + !1 === e || + ((function (e) { + return k(e) && null !== e; + })(e) && + "[object Boolean]" == M(e)) + ); + } + function k(e) { + return "object" === n(e); + } + function F(e) { + return null != e; + } + function b(e) { + return !e.trim().length; + } + function M(e) { + return null == e + ? void 0 === e + ? "[object Undefined]" + : "[object Null]" + : Object.prototype.toString.call(e); + } + var E = function (e) { + return "Missing ".concat(e, " property in key"); + }, + D = function (e) { + return "Property 'weight' in key '".concat( + e, + "' must be a positive integer", + ); + }, + B = Object.prototype.hasOwnProperty, + x = (function () { + function e(t) { + var n = this; + r(this, e), (this._keys = []), (this._keyMap = {}); + var i = 0; + t.forEach(function (e) { + var t = _(e); + n._keys.push(t), (n._keyMap[t.id] = t), (i += t.weight); + }), + this._keys.forEach(function (e) { + e.weight /= i; + }); + } + return ( + u(e, [ + { + key: "get", + value: function (e) { + return this._keyMap[e]; + }, + }, + { + key: "keys", + value: function () { + return this._keys; + }, + }, + { + key: "toJSON", + value: function () { + return JSON.stringify(this._keys); + }, + }, + ]), + e + ); + })(); + function _(e) { + var t = null, + n = null, + r = null, + i = 1, + u = null; + if (A(e) || g(e)) (r = e), (t = S(e)), (n = w(e)); + else { + if (!B.call(e, "name")) throw new Error(E("name")); + var o = e.name; + if (((r = o), B.call(e, "weight") && (i = e.weight) <= 0)) + throw new Error(D(o)); + (t = S(o)), (n = w(o)), (u = e.getFn); + } + return { path: t, id: n, weight: i, src: r, getFn: u }; + } + function S(e) { + return g(e) ? e : e.split("."); + } + function w(e) { + return g(e) ? e.join(".") : e; + } + var O = { + useExtendedSearch: !1, + getFn: function (e, t) { + var n = [], + r = !1; + return ( + (function e(t, i, u, o) { + if (F(t)) + if (i[u]) { + var c = t[i[u]]; + if (!F(c)) return; + if (u === i.length - 1 && (A(c) || m(c) || C(c))) + n.push(void 0 !== o ? { v: p(c), i: o } : p(c)); + else if (g(c)) { + r = !0; + for (var s = 0, a = c.length; s < a; s += 1) + e(c[s], i, u + 1, s); + } else i.length && e(c, i, u + 1, o); + } else n.push(void 0 !== o ? { v: t, i: o } : t); + })(e, A(t) ? t.split(".") : t, 0), + r ? n : n[0] + ); + }, + ignoreLocation: !1, + ignoreFieldNorm: !1, + fieldNormWeight: 1, + }, + j = t( + t( + t( + t( + {}, + { + isCaseSensitive: !1, + ignoreDiacritics: !1, + includeScore: !1, + keys: [], + shouldSort: !0, + sortFn: function (e, t) { + return e.score === t.score + ? e.idx < t.idx + ? -1 + : 1 + : e.score < t.score + ? -1 + : 1; + }, + }, + ), + { includeMatches: !1, findAllMatches: !1, minMatchCharLength: 1 }, + ), + { location: 0, threshold: 0.6, distance: 100 }, + ), + O, + ), + L = /[^ ]+/g, + I = (function () { + function e() { + var t = + arguments.length > 0 && void 0 !== arguments[0] + ? arguments[0] + : {}, + n = t.getFn, + i = void 0 === n ? j.getFn : n, + u = t.fieldNormWeight, + o = void 0 === u ? j.fieldNormWeight : u; + r(this, e), + (this.norm = (function () { + var e = + arguments.length > 0 && void 0 !== arguments[0] + ? arguments[0] + : 1, + t = + arguments.length > 1 && void 0 !== arguments[1] + ? arguments[1] + : 3, + n = new Map(), + r = Math.pow(10, t); + return { + get: function (t) { + var i = t.match(L).length; + if (n.has(i)) return n.get(i); + var u = 1 / Math.pow(i, 0.5 * e), + o = parseFloat(Math.round(u * r) / r); + return (n.set(i, o), o); + }, + clear: function () { + n.clear(); + }, + }; + })(o, 3)), + (this.getFn = i), + (this.isCreated = !1), + this.setIndexRecords(); + } + return ( + u(e, [ + { + key: "setSources", + value: function () { + var e = + arguments.length > 0 && void 0 !== arguments[0] + ? arguments[0] + : []; + this.docs = e; + }, + }, + { + key: "setIndexRecords", + value: function () { + var e = + arguments.length > 0 && void 0 !== arguments[0] + ? arguments[0] + : []; + this.records = e; + }, + }, + { + key: "setKeys", + value: function () { + var e = this, + t = + arguments.length > 0 && void 0 !== arguments[0] + ? arguments[0] + : []; + (this.keys = t), + (this._keysMap = {}), + t.forEach(function (t, n) { + e._keysMap[t.id] = n; + }); + }, + }, + { + key: "create", + value: function () { + var e = this; + !this.isCreated && + this.docs.length && + ((this.isCreated = !0), + A(this.docs[0]) + ? this.docs.forEach(function (t, n) { + e._addString(t, n); + }) + : this.docs.forEach(function (t, n) { + e._addObject(t, n); + }), + this.norm.clear()); + }, + }, + { + key: "add", + value: function (e) { + var t = this.size(); + A(e) ? this._addString(e, t) : this._addObject(e, t); + }, + }, + { + key: "removeAt", + value: function (e) { + this.records.splice(e, 1); + for (var t = e, n = this.size(); t < n; t += 1) + this.records[t].i -= 1; + }, + }, + { + key: "removeAll", + value: function (e) { + for (var t = e.length - 1; t >= 0; t -= 1) + this.records.splice(e[t], 1); + for (var n = 0, r = this.records.length; n < r; n += 1) + this.records[n].i = n; + }, + }, + { + key: "getValueForItemAtKeyId", + value: function (e, t) { + return e[this._keysMap[t]]; + }, + }, + { + key: "size", + value: function () { + return this.records.length; + }, + }, + { + key: "_addString", + value: function (e, t) { + if (F(e) && !b(e)) { + var n = { v: e, i: t, n: this.norm.get(e) }; + this.records.push(n); + } + }, + }, + { + key: "_addObject", + value: function (e, t) { + var n = this, + r = { i: t, $: {} }; + this.keys.forEach(function (t, i) { + var u = t.getFn ? t.getFn(e) : n.getFn(e, t.path); + if (F(u)) + if (g(u)) { + for (var o = [], c = 0, s = u.length; c < s; c += 1) { + var a = u[c]; + if (F(a)) + if (A(a)) { + if (!b(a)) { + var h = { v: a, i: c, n: n.norm.get(a) }; + o.push(h); + } + } else if (A(a.v) && !b(a.v)) { + var l = { v: a.v, i: a.i, n: n.norm.get(a.v) }; + o.push(l); + } + } + r.$[i] = o; + } else if (A(u) && !b(u)) { + var f = { v: u, n: n.norm.get(u) }; + r.$[i] = f; + } + }), + this.records.push(r); + }, + }, + { + key: "toJSON", + value: function () { + return { keys: this.keys, records: this.records }; + }, + }, + ]), + e + ); + })(); + function N(e, t) { + var n = + arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : {}, + r = n.getFn, + i = void 0 === r ? j.getFn : r, + u = n.fieldNormWeight, + o = void 0 === u ? j.fieldNormWeight : u, + c = new I({ getFn: i, fieldNormWeight: o }); + return (c.setKeys(e.map(_)), c.setSources(t), c.create(), c); + } + var $ = 32; + function R(e, t, n) { + var r = + arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : {}, + i = r.location, + u = void 0 === i ? j.location : i, + o = r.distance, + c = void 0 === o ? j.distance : o, + s = r.threshold, + a = void 0 === s ? j.threshold : s, + h = r.findAllMatches, + l = void 0 === h ? j.findAllMatches : h, + f = r.minMatchCharLength, + d = void 0 === f ? j.minMatchCharLength : f, + v = r.includeMatches, + g = void 0 === v ? j.includeMatches : v, + y = r.ignoreLocation, + p = void 0 === y ? j.ignoreLocation : y; + if (t.length > $) + throw new Error("Pattern length exceeds max of ".concat($, ".")); + for ( + var A, + m = t.length, + C = e.length, + k = Math.max(0, Math.min(u, C)), + F = a, + b = k, + M = function (e, t) { + var n = e / m; + if (p) return n; + var r = Math.abs(k - t); + return c ? n + r / c : r ? 1 : n; + }, + E = d > 1 || g, + D = E ? Array(C) : []; + (A = e.indexOf(t, b)) > -1; + ) { + var B = M(0, A); + if (((F = Math.min(B, F)), (b = A + m), E)) + for (var x = 0; x < m; ) (D[A + x] = 1), (x += 1); + } + b = -1; + for ( + var _ = [], S = 1, w = m + C, O = 1 << (m - 1), L = 0; + L < m; + L += 1 + ) { + for (var I = 0, N = w; I < N; ) + M(L, k + N) <= F ? (I = N) : (w = N), + (N = Math.floor((w - I) / 2 + I)); + w = N; + var R = Math.max(1, k - N + 1), + P = l ? C : Math.min(k + N, C) + m, + W = Array(P + 2); + W[P + 1] = (1 << L) - 1; + for (var z = P; z >= R; z -= 1) { + var T = z - 1, + K = n[e[T]]; + if ( + (E && (D[T] = +!!K), + (W[z] = ((W[z + 1] << 1) | 1) & K), + L && (W[z] |= ((_[z + 1] | _[z]) << 1) | 1 | _[z + 1]), + W[z] & O && (S = M(L, T)) <= F) + ) { + if (((F = S), (b = T) <= k)) break; + R = Math.max(1, 2 * k - b); + } + } + if (M(L + 1, k) > F) break; + _ = W; + } + var U = { isMatch: b >= 0, score: Math.max(0.001, S) }; + if (E) { + var q = (function () { + for ( + var e = + arguments.length > 0 && void 0 !== arguments[0] + ? arguments[0] + : [], + t = + arguments.length > 1 && void 0 !== arguments[1] + ? arguments[1] + : j.minMatchCharLength, + n = [], + r = -1, + i = -1, + u = 0, + o = e.length; + u < o; + u += 1 + ) { + var c = e[u]; + c && -1 === r + ? (r = u) + : c || + -1 === r || + ((i = u - 1) - r + 1 >= t && n.push([r, i]), (r = -1)); + } + return (e[u - 1] && u - r >= t && n.push([r, u - 1]), n); + })(D, d); + q.length ? g && (U.indices = q) : (U.isMatch = !1); + } + return U; + } + function P(e) { + for (var t = {}, n = 0, r = e.length; n < r; n += 1) { + var i = e.charAt(n); + t[i] = (t[i] || 0) | (1 << (r - n - 1)); + } + return t; + } + var W = String.prototype.normalize + ? function (e) { + return e + .normalize("NFD") + .replace( + /[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C04\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F]/g, + "", + ); + } + : function (e) { + return e; + }, + z = (function () { + function e(t) { + var n = this, + i = + arguments.length > 1 && void 0 !== arguments[1] + ? arguments[1] + : {}, + u = i.location, + o = void 0 === u ? j.location : u, + c = i.threshold, + s = void 0 === c ? j.threshold : c, + a = i.distance, + h = void 0 === a ? j.distance : a, + l = i.includeMatches, + f = void 0 === l ? j.includeMatches : l, + d = i.findAllMatches, + v = void 0 === d ? j.findAllMatches : d, + g = i.minMatchCharLength, + y = void 0 === g ? j.minMatchCharLength : g, + p = i.isCaseSensitive, + A = void 0 === p ? j.isCaseSensitive : p, + m = i.ignoreDiacritics, + C = void 0 === m ? j.ignoreDiacritics : m, + k = i.ignoreLocation, + F = void 0 === k ? j.ignoreLocation : k; + if ( + (r(this, e), + (this.options = { + location: o, + threshold: s, + distance: h, + includeMatches: f, + findAllMatches: v, + minMatchCharLength: y, + isCaseSensitive: A, + ignoreDiacritics: C, + ignoreLocation: F, + }), + (t = A ? t : t.toLowerCase()), + (t = C ? W(t) : t), + (this.pattern = t), + (this.chunks = []), + this.pattern.length) + ) { + var b = function (e, t) { + n.chunks.push({ pattern: e, alphabet: P(e), startIndex: t }); + }, + M = this.pattern.length; + if (M > $) { + for (var E = 0, D = M % $, B = M - D; E < B; ) + b(this.pattern.substr(E, $), E), (E += $); + if (D) { + var x = M - $; + b(this.pattern.substr(x), x); + } + } else b(this.pattern, 0); + } + } + return ( + u(e, [ + { + key: "searchIn", + value: function (e) { + var t = this.options, + n = t.isCaseSensitive, + r = t.ignoreDiacritics, + i = t.includeMatches; + if ( + ((e = n ? e : e.toLowerCase()), + (e = r ? W(e) : e), + this.pattern === e) + ) { + var u = { isMatch: !0, score: 0 }; + return (i && (u.indices = [[0, e.length - 1]]), u); + } + var o = this.options, + c = o.location, + s = o.distance, + a = o.threshold, + h = o.findAllMatches, + l = o.minMatchCharLength, + d = o.ignoreLocation, + v = [], + g = 0, + y = !1; + this.chunks.forEach(function (t) { + var n = t.pattern, + r = t.alphabet, + u = t.startIndex, + o = R(e, n, r, { + location: c + u, + distance: s, + threshold: a, + findAllMatches: h, + minMatchCharLength: l, + includeMatches: i, + ignoreLocation: d, + }), + p = o.isMatch, + A = o.score, + m = o.indices; + p && (y = !0), (g += A), p && m && v.push.apply(v, f(m)); + }); + var p = { isMatch: y, score: y ? g / this.chunks.length : 1 }; + return ( + y && + i && + (p.indices = (function (e) { + if (e.length <= 1) return e; + e.sort(function (e, t) { + return e[0] - t[0] || e[1] - t[1]; + }); + for (var t = [e[0]], n = 1, r = e.length; n < r; n += 1) { + var i = t[t.length - 1], + u = e[n]; + u[0] <= i[1] + 1 + ? (i[1] = Math.max(i[1], u[1])) + : t.push(u); + } + return t; + })(v)), + p + ); + }, + }, + ]), + e + ); + })(), + T = (function () { + function e(t) { + r(this, e), (this.pattern = t); + } + return ( + u( + e, + [{ key: "search", value: function () {} }], + [ + { + key: "isMultiMatch", + value: function (e) { + return K(e, this.multiRegex); + }, + }, + { + key: "isSingleMatch", + value: function (e) { + return K(e, this.singleRegex); + }, + }, + ], + ), + e + ); + })(); + function K(e, t) { + var n = e.match(t); + return n ? n[1] : null; + } + var U = (function (e) { + c(n, e); + var t = l(n); + function n(e) { + return (r(this, n), t.call(this, e)); + } + return ( + u( + n, + [ + { + key: "search", + value: function (e) { + var t = e === this.pattern; + return { + isMatch: t, + score: t ? 0 : 1, + indices: [0, this.pattern.length - 1], + }; + }, + }, + ], + [ + { + key: "type", + get: function () { + return "exact"; + }, + }, + { + key: "multiRegex", + get: function () { + return /^="(.*)"$/; + }, + }, + { + key: "singleRegex", + get: function () { + return /^=(.*)$/; + }, + }, + ], + ), + n + ); + })(T), + q = (function (e) { + c(n, e); + var t = l(n); + function n(e) { + return (r(this, n), t.call(this, e)); + } + return ( + u( + n, + [ + { + key: "search", + value: function (e) { + var t = -1 === e.indexOf(this.pattern); + return { + isMatch: t, + score: t ? 0 : 1, + indices: [0, e.length - 1], + }; + }, + }, + ], + [ + { + key: "type", + get: function () { + return "inverse-exact"; + }, + }, + { + key: "multiRegex", + get: function () { + return /^!"(.*)"$/; + }, + }, + { + key: "singleRegex", + get: function () { + return /^!(.*)$/; + }, + }, + ], + ), + n + ); + })(T), + J = (function (e) { + c(n, e); + var t = l(n); + function n(e) { + return (r(this, n), t.call(this, e)); + } + return ( + u( + n, + [ + { + key: "search", + value: function (e) { + var t = e.startsWith(this.pattern); + return { + isMatch: t, + score: t ? 0 : 1, + indices: [0, this.pattern.length - 1], + }; + }, + }, + ], + [ + { + key: "type", + get: function () { + return "prefix-exact"; + }, + }, + { + key: "multiRegex", + get: function () { + return /^\^"(.*)"$/; + }, + }, + { + key: "singleRegex", + get: function () { + return /^\^(.*)$/; + }, + }, + ], + ), + n + ); + })(T), + Q = (function (e) { + c(n, e); + var t = l(n); + function n(e) { + return (r(this, n), t.call(this, e)); + } + return ( + u( + n, + [ + { + key: "search", + value: function (e) { + var t = !e.startsWith(this.pattern); + return { + isMatch: t, + score: t ? 0 : 1, + indices: [0, e.length - 1], + }; + }, + }, + ], + [ + { + key: "type", + get: function () { + return "inverse-prefix-exact"; + }, + }, + { + key: "multiRegex", + get: function () { + return /^!\^"(.*)"$/; + }, + }, + { + key: "singleRegex", + get: function () { + return /^!\^(.*)$/; + }, + }, + ], + ), + n + ); + })(T), + V = (function (e) { + c(n, e); + var t = l(n); + function n(e) { + return (r(this, n), t.call(this, e)); + } + return ( + u( + n, + [ + { + key: "search", + value: function (e) { + var t = e.endsWith(this.pattern); + return { + isMatch: t, + score: t ? 0 : 1, + indices: [e.length - this.pattern.length, e.length - 1], + }; + }, + }, + ], + [ + { + key: "type", + get: function () { + return "suffix-exact"; + }, + }, + { + key: "multiRegex", + get: function () { + return /^"(.*)"\$$/; + }, + }, + { + key: "singleRegex", + get: function () { + return /^(.*)\$$/; + }, + }, + ], + ), + n + ); + })(T), + G = (function (e) { + c(n, e); + var t = l(n); + function n(e) { + return (r(this, n), t.call(this, e)); + } + return ( + u( + n, + [ + { + key: "search", + value: function (e) { + var t = !e.endsWith(this.pattern); + return { + isMatch: t, + score: t ? 0 : 1, + indices: [0, e.length - 1], + }; + }, + }, + ], + [ + { + key: "type", + get: function () { + return "inverse-suffix-exact"; + }, + }, + { + key: "multiRegex", + get: function () { + return /^!"(.*)"\$$/; + }, + }, + { + key: "singleRegex", + get: function () { + return /^!(.*)\$$/; + }, + }, + ], + ), + n + ); + })(T), + H = (function (e) { + c(n, e); + var t = l(n); + function n(e) { + var i, + u = + arguments.length > 1 && void 0 !== arguments[1] + ? arguments[1] + : {}, + o = u.location, + c = void 0 === o ? j.location : o, + s = u.threshold, + a = void 0 === s ? j.threshold : s, + h = u.distance, + l = void 0 === h ? j.distance : h, + f = u.includeMatches, + d = void 0 === f ? j.includeMatches : f, + v = u.findAllMatches, + g = void 0 === v ? j.findAllMatches : v, + y = u.minMatchCharLength, + p = void 0 === y ? j.minMatchCharLength : y, + A = u.isCaseSensitive, + m = void 0 === A ? j.isCaseSensitive : A, + C = u.ignoreDiacritics, + k = void 0 === C ? j.ignoreDiacritics : C, + F = u.ignoreLocation, + b = void 0 === F ? j.ignoreLocation : F; + return ( + r(this, n), + ((i = t.call(this, e))._bitapSearch = new z(e, { + location: c, + threshold: a, + distance: l, + includeMatches: d, + findAllMatches: g, + minMatchCharLength: p, + isCaseSensitive: m, + ignoreDiacritics: k, + ignoreLocation: b, + })), + i + ); + } + return ( + u( + n, + [ + { + key: "search", + value: function (e) { + return this._bitapSearch.searchIn(e); + }, + }, + ], + [ + { + key: "type", + get: function () { + return "fuzzy"; + }, + }, + { + key: "multiRegex", + get: function () { + return /^"(.*)"$/; + }, + }, + { + key: "singleRegex", + get: function () { + return /^(.*)$/; + }, + }, + ], + ), + n + ); + })(T), + X = (function (e) { + c(n, e); + var t = l(n); + function n(e) { + return (r(this, n), t.call(this, e)); + } + return ( + u( + n, + [ + { + key: "search", + value: function (e) { + for ( + var t, n = 0, r = [], i = this.pattern.length; + (t = e.indexOf(this.pattern, n)) > -1; + ) + (n = t + i), r.push([t, n - 1]); + var u = !!r.length; + return { isMatch: u, score: u ? 0 : 1, indices: r }; + }, + }, + ], + [ + { + key: "type", + get: function () { + return "include"; + }, + }, + { + key: "multiRegex", + get: function () { + return /^'"(.*)"$/; + }, + }, + { + key: "singleRegex", + get: function () { + return /^'(.*)$/; + }, + }, + ], + ), + n + ); + })(T), + Y = [U, X, J, Q, G, V, q, H], + Z = Y.length, + ee = / +(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)/, + te = new Set([H.type, X.type]), + ne = (function () { + function e(t) { + var n = + arguments.length > 1 && void 0 !== arguments[1] + ? arguments[1] + : {}, + i = n.isCaseSensitive, + u = void 0 === i ? j.isCaseSensitive : i, + o = n.ignoreDiacritics, + c = void 0 === o ? j.ignoreDiacritics : o, + s = n.includeMatches, + a = void 0 === s ? j.includeMatches : s, + h = n.minMatchCharLength, + l = void 0 === h ? j.minMatchCharLength : h, + f = n.ignoreLocation, + d = void 0 === f ? j.ignoreLocation : f, + v = n.findAllMatches, + g = void 0 === v ? j.findAllMatches : v, + y = n.location, + p = void 0 === y ? j.location : y, + A = n.threshold, + m = void 0 === A ? j.threshold : A, + C = n.distance, + k = void 0 === C ? j.distance : C; + r(this, e), + (this.query = null), + (this.options = { + isCaseSensitive: u, + ignoreDiacritics: c, + includeMatches: a, + minMatchCharLength: l, + findAllMatches: g, + ignoreLocation: d, + location: p, + threshold: m, + distance: k, + }), + (t = u ? t : t.toLowerCase()), + (t = c ? W(t) : t), + (this.pattern = t), + (this.query = (function (e) { + var t = + arguments.length > 1 && void 0 !== arguments[1] + ? arguments[1] + : {}; + return e.split("|").map(function (e) { + for ( + var n = e + .trim() + .split(ee) + .filter(function (e) { + return e && !!e.trim(); + }), + r = [], + i = 0, + u = n.length; + i < u; + i += 1 + ) { + for (var o = n[i], c = !1, s = -1; !c && ++s < Z; ) { + var a = Y[s], + h = a.isMultiMatch(o); + h && (r.push(new a(h, t)), (c = !0)); + } + if (!c) + for (s = -1; ++s < Z; ) { + var l = Y[s], + f = l.isSingleMatch(o); + if (f) { + r.push(new l(f, t)); + break; + } + } + } + return r; + }); + })(this.pattern, this.options)); + } + return ( + u( + e, + [ + { + key: "searchIn", + value: function (e) { + var t = this.query; + if (!t) return { isMatch: !1, score: 1 }; + var n = this.options, + r = n.includeMatches, + i = n.isCaseSensitive, + u = n.ignoreDiacritics; + (e = i ? e : e.toLowerCase()), (e = u ? W(e) : e); + for ( + var o = 0, c = [], s = 0, a = 0, h = t.length; + a < h; + a += 1 + ) { + var l = t[a]; + (c.length = 0), (o = 0); + for (var d = 0, v = l.length; d < v; d += 1) { + var g = l[d], + y = g.search(e), + p = y.isMatch, + A = y.indices, + m = y.score; + if (!p) { + (s = 0), (o = 0), (c.length = 0); + break; + } + if (((o += 1), (s += m), r)) { + var C = g.constructor.type; + te.has(C) ? c.push.apply(c, f(A)) : c.push(A); + } + } + if (o) { + var k = { isMatch: !0, score: s / o }; + return (r && (k.indices = c), k); + } + } + return { isMatch: !1, score: 1 }; + }, + }, + ], + [ + { + key: "condition", + value: function (e, t) { + return t.useExtendedSearch; + }, + }, + ], + ), + e + ); + })(), + re = []; + function ie() { + re.push.apply(re, arguments); + } + function ue(e, t) { + for (var n = 0, r = re.length; n < r; n += 1) { + var i = re[n]; + if (i.condition(e, t)) return new i(e, t); + } + return new z(e, t); + } + var oe = "$and", + ce = "$or", + se = "$path", + ae = "$val", + he = function (e) { + return !(!e[oe] && !e[ce]); + }, + le = function (e) { + return o( + {}, + oe, + Object.keys(e).map(function (t) { + return o({}, t, e[t]); + }), + ); + }; + function fe(e, t) { + var n = ( + arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : {} + ).auto, + r = void 0 === n || n; + return ( + he(e) || (e = le(e)), + (function e(n) { + var i = Object.keys(n), + u = (function (e) { + return !!e[se]; + })(n); + if (!u && i.length > 1 && !he(n)) return e(le(n)); + if ( + (function (e) { + return !g(e) && k(e) && !he(e); + })(n) + ) { + var o = u ? n[se] : i[0], + c = u ? n[ae] : n[o]; + if (!A(c)) + throw new Error( + (function (e) { + return "Invalid value for key ".concat(e); + })(o), + ); + var s = { keyId: w(o), pattern: c }; + return (r && (s.searcher = ue(c, t)), s); + } + var a = { children: [], operator: i[0] }; + return ( + i.forEach(function (t) { + var r = n[t]; + g(r) && + r.forEach(function (t) { + a.children.push(e(t)); + }); + }), + a + ); + })(e) + ); + } + function de(e, t) { + var n = t.ignoreFieldNorm, + r = void 0 === n ? j.ignoreFieldNorm : n, + i = 1; + e.matches.forEach(function (e) { + var t = e.key, + n = e.norm, + u = e.score, + o = t ? t.weight : null; + i *= Math.pow( + 0 === u && o ? Number.EPSILON : u, + (o || 1) * (r ? 1 : n), + ); + }), + (e.score = i); + } + var ve = (function () { + function e(t) { + r(this, e), (this.limit = t), (this.heap = []); + } + return ( + u(e, [ + { + key: "size", + get: function () { + return this.heap.length; + }, + }, + { + key: "shouldInsert", + value: function (e) { + return this.size < this.limit || e < this.heap[0].score; + }, + }, + { + key: "insert", + value: function (e) { + this.size < this.limit + ? (this.heap.push(e), this._bubbleUp(this.size - 1)) + : e.score < this.heap[0].score && + ((this.heap[0] = e), this._sinkDown(0)); + }, + }, + { + key: "extractSorted", + value: function (e) { + return this.heap.sort(e); + }, + }, + { + key: "_bubbleUp", + value: function (e) { + for (var t = this.heap; e > 0; ) { + var n = (e - 1) >> 1; + if (t[e].score <= t[n].score) break; + var r = t[e]; + (t[e] = t[n]), (t[n] = r), (e = n); + } + }, + }, + { + key: "_sinkDown", + value: function (e) { + var t = this.heap, + n = t.length, + r = e; + do { + var i = 2 * (e = r) + 1, + u = 2 * e + 2; + if ( + (i < n && t[i].score > t[r].score && (r = i), + u < n && t[u].score > t[r].score && (r = u), + r !== e) + ) { + var o = t[e]; + (t[e] = t[r]), (t[r] = o); + } + } while (r !== e); + }, + }, + ]), + e + ); + })(); + function ge(e, t) { + var n = e.matches; + (t.matches = []), + F(n) && + n.forEach(function (e) { + if (F(e.indices) && e.indices.length) { + var n = { indices: e.indices, value: e.value }; + e.key && (n.key = e.key.src), + e.idx > -1 && (n.refIndex = e.idx), + t.matches.push(n); + } + }); + } + function ye(e, t) { + t.score = e.score; + } + var pe = (function () { + function e(n) { + var i = + arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}, + u = arguments.length > 2 ? arguments[2] : void 0; + r(this, e), + (this.options = t(t({}, j), i)), + this.options.useExtendedSearch, + (this._keyStore = new x(this.options.keys)), + this.setCollection(n, u), + (this._lastQuery = null), + (this._lastSearcher = null); + } + return ( + u(e, [ + { + key: "_getSearcher", + value: function (e) { + if (this._lastQuery === e) return this._lastSearcher; + var t = ue(e, this.options); + return ((this._lastQuery = e), (this._lastSearcher = t), t); + }, + }, + { + key: "setCollection", + value: function (e, t) { + if (((this._docs = e), t && !(t instanceof I))) + throw new Error("Incorrect 'index' type"); + this._myIndex = + t || + N(this.options.keys, this._docs, { + getFn: this.options.getFn, + fieldNormWeight: this.options.fieldNormWeight, + }); + }, + }, + { + key: "add", + value: function (e) { + F(e) && (this._docs.push(e), this._myIndex.add(e)); + }, + }, + { + key: "remove", + value: function () { + for ( + var e = + arguments.length > 0 && void 0 !== arguments[0] + ? arguments[0] + : function () { + return !1; + }, + t = [], + n = [], + r = 0, + i = this._docs.length; + r < i; + r += 1 + ) + e(this._docs[r], r) && (t.push(this._docs[r]), n.push(r)); + if (n.length) { + for (var u = n.length - 1; u >= 0; u -= 1) + this._docs.splice(n[u], 1); + this._myIndex.removeAll(n); + } + return t; + }, + }, + { + key: "removeAt", + value: function (e) { + this._docs.splice(e, 1), this._myIndex.removeAt(e); + }, + }, + { + key: "getIndex", + value: function () { + return this._myIndex; + }, + }, + { + key: "search", + value: function (e) { + var t, + n = ( + arguments.length > 1 && void 0 !== arguments[1] + ? arguments[1] + : {} + ).limit, + r = void 0 === n ? -1 : n, + i = this.options, + u = i.includeMatches, + o = i.includeScore, + c = i.shouldSort, + s = i.sortFn, + a = i.ignoreFieldNorm; + if (m(r) && r > 0 && A(e)) { + var h = new ve(r); + A(this._docs[0]) + ? this._searchStringList(e, { heap: h, ignoreFieldNorm: a }) + : this._searchObjectList(e, { heap: h, ignoreFieldNorm: a }), + (t = h.extractSorted(s)); + } else + (function (e, t) { + var n = t.ignoreFieldNorm, + r = void 0 === n ? j.ignoreFieldNorm : n; + e.forEach(function (e) { + de(e, { ignoreFieldNorm: r }); + }); + })( + (t = A(e) + ? A(this._docs[0]) + ? this._searchStringList(e) + : this._searchObjectList(e) + : this._searchLogical(e)), + { ignoreFieldNorm: a }, + ), + c && t.sort(s), + m(r) && r > -1 && (t = t.slice(0, r)); + return (function (e, t) { + var n = + arguments.length > 2 && void 0 !== arguments[2] + ? arguments[2] + : {}, + r = n.includeMatches, + i = void 0 === r ? j.includeMatches : r, + u = n.includeScore, + o = void 0 === u ? j.includeScore : u, + c = []; + return ( + i && c.push(ge), + o && c.push(ye), + e.map(function (e) { + var n = e.idx, + r = { item: t[n], refIndex: n }; + return ( + c.length && + c.forEach(function (t) { + t(e, r); + }), + r + ); + }) + ); + })(t, this._docs, { includeMatches: u, includeScore: o }); + }, + }, + { + key: "_searchStringList", + value: function (e) { + var t = + arguments.length > 1 && void 0 !== arguments[1] + ? arguments[1] + : {}, + n = t.heap, + r = t.ignoreFieldNorm, + i = this._getSearcher(e), + u = this._myIndex.records, + o = n ? null : []; + return ( + u.forEach(function (e) { + var t = e.v, + u = e.i, + c = e.n; + if (F(t)) { + var s = i.searchIn(t), + a = s.isMatch, + h = s.score, + l = s.indices; + if (a) { + var f = { + item: t, + idx: u, + matches: [{ score: h, value: t, norm: c, indices: l }], + }; + n + ? (de(f, { ignoreFieldNorm: r }), + n.shouldInsert(f.score) && n.insert(f)) + : o.push(f); + } + } + }), + o + ); + }, + }, + { + key: "_searchLogical", + value: function (e) { + var t = this, + n = fe(e, this.options), + r = function e(n, r, i) { + if (!n.children) { + var u = n.keyId, + o = n.searcher, + c = t._findMatches({ + key: t._keyStore.get(u), + value: t._myIndex.getValueForItemAtKeyId(r, u), + searcher: o, + }); + return c && c.length + ? [{ idx: i, item: r, matches: c }] + : []; + } + for ( + var s = [], a = 0, h = n.children.length; + a < h; + a += 1 + ) { + var l = e(n.children[a], r, i); + if (l.length) s.push.apply(s, f(l)); + else if (n.operator === oe) return []; + } + return s; + }, + i = this._myIndex.records, + u = new Map(), + o = []; + return ( + i.forEach(function (e) { + var t = e.$, + i = e.i; + if (F(t)) { + var c = r(n, t, i); + c.length && + (u.has(i) || + (u.set(i, { idx: i, item: t, matches: [] }), + o.push(u.get(i))), + c.forEach(function (e) { + var t, + n = e.matches; + (t = u.get(i).matches).push.apply(t, f(n)); + })); + } + }), + o + ); + }, + }, + { + key: "_searchObjectList", + value: function (e) { + var t = this, + n = + arguments.length > 1 && void 0 !== arguments[1] + ? arguments[1] + : {}, + r = n.heap, + i = n.ignoreFieldNorm, + u = this._getSearcher(e), + o = this._myIndex, + c = o.keys, + s = o.records, + a = r ? null : []; + return ( + s.forEach(function (e) { + var n = e.$, + o = e.i; + if (F(n)) { + var s = []; + if ( + (c.forEach(function (e, r) { + s.push.apply( + s, + f( + t._findMatches({ + key: e, + value: n[r], + searcher: u, + }), + ), + ); + }), + s.length) + ) { + var h = { idx: o, item: n, matches: s }; + r + ? (de(h, { ignoreFieldNorm: i }), + r.shouldInsert(h.score) && r.insert(h)) + : a.push(h); + } + } + }), + a + ); + }, + }, + { + key: "_findMatches", + value: function (e) { + var t = e.key, + n = e.value, + r = e.searcher; + if (!F(n)) return []; + var i = []; + if (g(n)) + n.forEach(function (e) { + var n = e.v, + u = e.i, + o = e.n; + if (F(n)) { + var c = r.searchIn(n), + s = c.isMatch, + a = c.score, + h = c.indices; + s && + i.push({ + score: a, + key: t, + value: n, + idx: u, + norm: o, + indices: h, + }); + } + }); + else { + var u = n.v, + o = n.n, + c = r.searchIn(u), + s = c.isMatch, + a = c.score, + h = c.indices; + s && + i.push({ score: a, key: t, value: u, norm: o, indices: h }); + } + return i; + }, + }, + ]), + e + ); + })(); + return ( + (pe.version = "7.2.0"), + (pe.createIndex = N), + (pe.parseIndex = function (e) { + var t = + arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}, + n = t.getFn, + r = void 0 === n ? j.getFn : n, + i = t.fieldNormWeight, + u = void 0 === i ? j.fieldNormWeight : i, + o = e.keys, + c = e.records, + s = new I({ getFn: r, fieldNormWeight: u }); + return (s.setKeys(o), s.setIndexRecords(c), s); + }), + (pe.config = j), + ie(ne), + (pe.use = function () { + for (var e = arguments.length, t = new Array(e), n = 0; n < e; n++) + t[n] = arguments[n]; + t.forEach(function (e) { + return ie(e); + }); + }), + pe + ); + }), + "object" == typeof exports && "undefined" != typeof module + ? (module.exports = t()) + : "function" == typeof define && define.amd + ? define(t) + : ((e = "undefined" != typeof globalThis ? globalThis : e || self).Fuse = + t()); diff --git a/modules/amezing/scripts/marquee.js b/modules/amezing/scripts/marquee.js index ad6e9de..82499b6 100644 --- a/modules/amezing/scripts/marquee.js +++ b/modules/amezing/scripts/marquee.js @@ -1,25 +1,25 @@ function updateMarquees() { - document.querySelectorAll(".marquee").forEach((container) => { - const text = container.firstElementChild; - if (!text) return; + document.querySelectorAll(".marquee").forEach((container) => { + const text = container.firstElementChild; + if (!text) return; - const style = getComputedStyle(container); - const paddingLeft = parseFloat(style.paddingLeft); - const paddingRight = parseFloat(style.paddingRight); + const style = getComputedStyle(container); + const paddingLeft = parseFloat(style.paddingLeft); + const paddingRight = parseFloat(style.paddingRight); - const containerInnerWidth = - container.clientWidth - paddingLeft - paddingRight; + const containerInnerWidth = + container.clientWidth - paddingLeft - paddingRight; - const textWidth = text.offsetWidth; + const textWidth = text.offsetWidth; - text.style.setProperty("--text-width", `${textWidth}`); - text.style.setProperty("--container-width", `${containerInnerWidth}`); + text.style.setProperty("--text-width", `${textWidth}`); + text.style.setProperty("--container-width", `${containerInnerWidth}`); - container.classList.toggle( - "overshoot-row", - textWidth > containerInnerWidth, - ); - }); + container.classList.toggle( + "overshoot-row", + textWidth > containerInnerWidth, + ); + }); } window.addEventListener("load", updateMarquees); diff --git a/modules/amezing/scripts/paginator.js b/modules/amezing/scripts/paginator.js index 334f85e..7226b6c 100644 --- a/modules/amezing/scripts/paginator.js +++ b/modules/amezing/scripts/paginator.js @@ -3,21 +3,21 @@ const items = document.querySelectorAll(".music .carousel > li"); const links = document.querySelectorAll(".music-paginator li a"); const observer = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - links.forEach((a) => a.classList.remove("active")); - const active = document.querySelector( - `.music-paginator a[href="#${entry.target.id}"]`, - ); - active?.classList.add("active"); - } - }); - }, - { - root: carousel, - threshold: 0.6, - }, + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + links.forEach((a) => a.classList.remove("active")); + const active = document.querySelector( + `.music-paginator a[href="#${entry.target.id}"]`, + ); + active?.classList.add("active"); + } + }); + }, + { + root: carousel, + threshold: 0.6, + }, ); items.forEach((item) => observer.observe(item)); diff --git a/modules/amezing/scripts/search-fuse.js b/modules/amezing/scripts/search-fuse.js index df56e7d..9a03269 100644 --- a/modules/amezing/scripts/search-fuse.js +++ b/modules/amezing/scripts/search-fuse.js @@ -3,176 +3,188 @@ let searchSetup = false; let fuse; async function initIndex() { - if (searchSetup) return; + if (searchSetup) return; - let data; - if (typeof window.searchIndex !== "undefined") { - data = window.searchIndex; - } else { - const response = await fetch( - `/search_index.${document.documentElement.lang}.json`, - ); - if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); - data = await response.json(); - } + let data; + if (typeof window.searchIndex !== "undefined") { + data = window.searchIndex; + } else { + const response = await fetch( + `/search_index.${document.documentElement.lang}.json`, + ); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + data = await response.json(); + } - const options = { - includeScore: false, - includeMatches: true, - ignoreLocation: true, - threshold: 0.15, - keys: [ - { name: "title", weight: 3 }, - { name: "description", weight: 2 }, - { name: "body", weight: 1 }, - ], - }; + const options = { + includeScore: false, + includeMatches: true, + ignoreLocation: true, + threshold: 0.15, + keys: [ + { name: "title", weight: 3 }, + { name: "description", weight: 2 }, + { name: "body", weight: 1 }, + ], + }; - fuse = new Fuse(data, options); - searchSetup = true; + fuse = new Fuse(data, options); + searchSetup = true; - console.log("Search index initialized successfully"); + console.log("Search index initialized successfully"); } function debounce(actual_fn, wait) { - let timeoutId; + let timeoutId; - return (...args) => { - clearTimeout(timeoutId); + return (...args) => { + clearTimeout(timeoutId); - timeoutId = setTimeout(() => { - actual_fn(...args); - }, wait); - }; + timeoutId = setTimeout(() => { + actual_fn(...args); + }, wait); + }; } function initSearch() { - const searchModal = document.getElementById("search-modal"); // Full-screen modal - const searchModalContent = document.getElementById("search-modal-content"); // Actual modal box - const searchInput = document.getElementById("search-input"); // Search input - const searchResults = document.getElementById("search-results"); // Search results - const searchButton = document.getElementById("search"); // Search button - const MAX_ITEMS = 10; - const MAX_RESULTS = 4; + const searchModal = document.getElementById("search-modal"); // Full-screen modal + const searchModalContent = document.getElementById("search-modal-content"); // Actual modal box + const searchInput = document.getElementById("search-input"); // Search input + const searchResults = document.getElementById("search-results"); // Search results + const searchButton = document.getElementById("search"); // Search button + const MAX_ITEMS = 10; + const MAX_RESULTS = 4; - let currentTerm = ""; + let currentTerm = ""; - // Open search modal when clicking the search button - if (searchButton) { - searchButton.addEventListener("click", function () { - searchModal.classList.add("active"); - initIndex(); - searchModal.addEventListener( - "transitionend", - function handler() { - searchInput.focus(); - searchModal.removeEventListener("transitionend", handler); - }, - { once: true }, - ); - }); - } + // Open search modal when clicking the search button + if (searchButton) { + searchButton.addEventListener("click", function () { + searchModal.classList.add("active"); + initIndex(); + searchModal.addEventListener( + "transitionend", + function handler() { + searchInput.focus(); + searchModal.removeEventListener("transitionend", handler); + }, + { once: true }, + ); + }); + } - // Open search modal on "/" key press - window.addEventListener("keydown", (event) => { - if ( - event.key === "/" && - document.activeElement.tagName !== "INPUT" && - document.activeElement.tagName !== "TEXTAREA" - ) { - event.preventDefault(); - initIndex(); - searchModal.classList.add("active"); - searchModal.addEventListener( - "transitionend", - function handler() { - searchInput.focus(); - searchModal.removeEventListener("transitionend", handler); - }, - { once: true }, - ); - } - }); + // Open search modal on "/" key press + window.addEventListener("keydown", (event) => { + if ( + event.key === "/" && + document.activeElement.tagName !== "INPUT" && + document.activeElement.tagName !== "TEXTAREA" + ) { + event.preventDefault(); + initIndex(); + searchModal.classList.add("active"); + searchModal.addEventListener( + "transitionend", + function handler() { + searchInput.focus(); + searchModal.removeEventListener("transitionend", handler); + }, + { once: true }, + ); + } + }); - // Close search modal on Escape key - window.addEventListener("keydown", (event) => { - if (event.key === "Escape") { - searchModal.classList.remove("active"); - } - }); + // Close search modal on Escape key + window.addEventListener("keydown", (event) => { + if (event.key === "Escape") { + searchModal.classList.remove("active"); + } + }); - // Close search modal when clicking outside search-modal-content - searchModal.addEventListener("click", function (e) { - if (!searchModalContent.contains(e.target)) { - searchModal.classList.remove("active"); - } - }); + // Close search modal when clicking outside search-modal-content + searchModal.addEventListener("click", function (e) { + if (!searchModalContent.contains(e.target)) { + searchModal.classList.remove("active"); + } + }); - // Prevent clicks inside modal content from closing it - searchModalContent.addEventListener("click", function (e) { - e.stopPropagation(); // Stops event from reaching searchModal click handler - }); + // Prevent clicks inside modal content from closing it + searchModalContent.addEventListener("click", function (e) { + e.stopPropagation(); // Stops event from reaching searchModal click handler + }); - // Search input event - searchInput.addEventListener( - "keyup", - debounce(async function () { - var term = searchInput.value.trim(); - if (term === currentTerm) return; + // Search input event + searchInput.addEventListener( + "keyup", + debounce(async function () { + var term = searchInput.value.trim(); + if (term === currentTerm) return; - searchResults.style.display = term === "" ? "none" : "flex"; - searchResults.innerHTML = ""; // Clear previous results - currentTerm = term; - if (term === "") return; + searchResults.style.display = term === "" ? "none" : "flex"; + searchResults.innerHTML = ""; // Clear previous results + currentTerm = term; + if (term === "") return; - await initIndex(); - var results = fuse.search(term, { limit: MAX_ITEMS }); - if (results.length === 0) { - searchResults.style.display = "none"; - return; - } + await initIndex(); + var results = fuse.search(term, { limit: MAX_ITEMS }); + if (results.length === 0) { + searchResults.style.display = "none"; + return; + } - // Insert formatted search result items - for (const result of results) { - searchResults.innerHTML += makeTeaser(result, term); - } - }, 150), - ); + // Insert formatted search result items + for (const result of results) { + searchResults.innerHTML += makeTeaser(result, term); + } + }, 150), + ); - function makeTeaser(result, searchVal) { - const TEASER_SIZE = 20; - let output = `
${result.item.title}`; + function makeTeaser(result, searchVal) { + const TEASER_SIZE = 20; + let output = `
${result.item.title}`; - for (const match of result.matches) { - if (match.key === "title") continue; + for (const match of result.matches) { + if (match.key === "title") continue; - const indices = match.indices.sort((a, b) => Math.abs(a[1] - a[0] - searchVal.length) - Math.abs(b[1] - b[0] - searchVal.length)).slice(0, MAX_RESULTS); - const value = match.value; + const indices = match.indices + .sort( + (a, b) => + Math.abs(a[1] - a[0] - searchVal.length) - + Math.abs(b[1] - b[0] - searchVal.length), + ) + .slice(0, MAX_RESULTS); + const value = match.value; - for (const ind of indices) { - const start = Math.max(0, ind[0] - TEASER_SIZE); - const end = Math.min(value.length - 1, ind[1] + TEASER_SIZE); - output += "" - + value.substring(start, ind[0]) - + `${value.substring(ind[0], ind[1] + 1)}` - + value.substring(ind[1] + 1, end) - + ""; - } + for (const ind of indices) { + const start = Math.max(0, ind[0] - TEASER_SIZE); + const end = Math.min(value.length - 1, ind[1] + TEASER_SIZE); + output += + "" + + value.substring(start, ind[0]) + + `${value.substring(ind[0], ind[1] + 1)}` + + value.substring(ind[1] + 1, end) + + ""; + } - if (match.indices.length > MAX_RESULTS) { - const moreMatchesText = document.getElementById("more-matches-text").textContent; - output += `${moreMatchesText}`.replace("$MATCHES", `+${match.indices.length - MAX_RESULTS}`); - } - } - return output + "
"; - } + if (match.indices.length > MAX_RESULTS) { + const moreMatchesText = + document.getElementById("more-matches-text").textContent; + output += + `${moreMatchesText}`.replace( + "$MATCHES", + `+${match.indices.length - MAX_RESULTS}`, + ); + } + } + return output + "
"; + } } if ( - document.readyState === "complete" || - (document.readyState !== "loading" && !document.documentElement.doScroll) + document.readyState === "complete" || + (document.readyState !== "loading" && !document.documentElement.doScroll) ) { - initSearch(); + initSearch(); } else { - document.addEventListener("DOMContentLoaded", initSearch); + document.addEventListener("DOMContentLoaded", initSearch); } diff --git a/modules/amezing/styles/vars.css b/modules/amezing/styles/vars.css index 82e9b14..5d7a026 100644 --- a/modules/amezing/styles/vars.css +++ b/modules/amezing/styles/vars.css @@ -1,20 +1,20 @@ :root { - --alert-note-title: "Note"; - --alert-tip-title: "Tip"; - --alert-important-title: "Important"; - --alert-warning-title: "Warning"; - --alert-caution-title: "Caution"; + --alert-note-title: "Note"; + --alert-tip-title: "Tip"; + --alert-important-title: "Important"; + --alert-warning-title: "Warning"; + --alert-caution-title: "Caution"; - --font-sans-serif: "Commissioner"; - --font-system-ui: ui-sans-serif, system-ui, sans-serif; - --font-emoji: "Noto Color Emoji"; - --font-monospace: "Commit Mono"; + --font-sans-serif: "Commissioner"; + --font-system-ui: ui-sans-serif, system-ui, sans-serif; + --font-emoji: "Noto Color Emoji"; + --font-monospace: "Commit Mono"; - --icon-info: url("https://unpkg.com/@phosphor-icons/web@2.1.1/src/regular/info.svg"); - --icon-lightbulb: url("https://unpkg.com/@phosphor-icons/web@2.1.1/src/regular/lightbulb.svg"); - --icon-warning-diamond: url("https://unpkg.com/@phosphor-icons/web@2.1.1/src/regular/warning-diamond.svg"); - --icon-warning: url("https://unpkg.com/@phosphor-icons/web@2.1.1/src/regular/warning.svg"); - --icon-warning-octagon: url("https://unpkg.com/@phosphor-icons/web@2.1.1/src/regular/warning-octagon.svg"); - --icon-copy: url("https://unpkg.com/@phosphor-icons/web@2.1.1/src/regular/copy.svg"); - --icon-check-square-offset: url("https://unpkg.com/@phosphor-icons/web@2.1.1/src/regular/check-square-offset.svg"); + --icon-info: url("https://unpkg.com/@phosphor-icons/web@2.1.1/src/regular/info.svg"); + --icon-lightbulb: url("https://unpkg.com/@phosphor-icons/web@2.1.1/src/regular/lightbulb.svg"); + --icon-warning-diamond: url("https://unpkg.com/@phosphor-icons/web@2.1.1/src/regular/warning-diamond.svg"); + --icon-warning: url("https://unpkg.com/@phosphor-icons/web@2.1.1/src/regular/warning.svg"); + --icon-warning-octagon: url("https://unpkg.com/@phosphor-icons/web@2.1.1/src/regular/warning-octagon.svg"); + --icon-copy: url("https://unpkg.com/@phosphor-icons/web@2.1.1/src/regular/copy.svg"); + --icon-check-square-offset: url("https://unpkg.com/@phosphor-icons/web@2.1.1/src/regular/check-square-offset.svg"); } diff --git a/modules/analysis/README.md b/modules/analysis/README.md index 95336bf..2d10e69 100644 --- a/modules/analysis/README.md +++ b/modules/analysis/README.md @@ -1 +1 @@ -# Static rendered analysis \ No newline at end of file +# Static rendered analysis diff --git a/modules/analysis/index.ts b/modules/analysis/index.ts index 2207c76..9612c70 100644 --- a/modules/analysis/index.ts +++ b/modules/analysis/index.ts @@ -24,33 +24,87 @@ export interface AuditOptions { includeDisclosures?: boolean; } -export async function analyzeHtml(html: string, kind: "accessible" | "overflow", options: AuditOptions = {}): Promise { +export async function analyzeHtml( + html: string, + kind: "accessible" | "overflow", + options: AuditOptions = {}, +): Promise { const browser = await chromium.launch(); try { const reports: AuditReport[] = []; - for (const viewport of options.viewports ?? [{ width: 1440, height: 900 }, { width: 390, height: 844 }]) { - const context = await browser.newContext({ viewport, javaScriptEnabled: false }); + for (const viewport of options.viewports ?? [ + { width: 1440, height: 900 }, + { width: 390, height: 844 }, + ]) { + const context = await browser.newContext({ + viewport, + javaScriptEnabled: false, + }); try { const page = await context.newPage(); await page.route("**/*", (route) => route.abort()); await page.setContent(html, { waitUntil: "load" }); - const result = await page.evaluate(inspectDocument, { kind, ignore: options.ignore ?? [], disclosures: options.includeDisclosures ?? false }); + const result = await page.evaluate(inspectDocument, { + kind, + ignore: options.ignore ?? [], + disclosures: options.includeDisclosures ?? false, + }); const findings = result.findings as AuditFinding[]; - const score = Math.max(0, Math.round(100 - findings.reduce((sum, finding) => sum + (finding.severity === "error" ? 10 : finding.severity === "warning" ? 4 : 2), 0))); - const report: AuditReport = { viewport, findings, score, checked: result.checked, method: "static-rendered-heuristic" }; + const score = Math.max( + 0, + Math.round( + 100 - + findings.reduce( + (sum, finding) => + sum + + (finding.severity === "error" + ? 10 + : finding.severity === "warning" + ? 4 + : 2), + 0, + ), + ), + ); + const report: AuditReport = { + viewport, + findings, + score, + checked: result.checked, + method: "static-rendered-heuristic", + }; reports.push(report); - if (options.failBelow !== undefined && score < options.failBelow) throw new Error(`${kind} score ${score} below ${options.failBelow} at ${viewport.width}px`); - if (options.failOnError && findings.some((finding) => finding.severity === "error")) throw new Error(`${kind} audit found errors at ${viewport.width}px`); - } finally { await context.close(); } + if (options.failBelow !== undefined && score < options.failBelow) { + throw new Error( + `${kind} score ${score} below ${options.failBelow} at ${viewport.width}px`, + ); + } + if ( + options.failOnError && + findings.some((finding) => finding.severity === "error") + ) { + throw new Error(`${kind} audit found errors at ${viewport.width}px`); + } + } finally { + await context.close(); + } } return reports; - } finally { await browser.close(); } + } finally { + await browser.close(); + } } export function accessibleIntegration(options: AuditOptions = {}): Integration { - return { name: "accessible", analyze: (page) => analyzeHtml(page.html, "accessible", options) }; + return { + name: "accessible", + analyze: (page) => analyzeHtml(page.html, "accessible", options), + }; } export function overflowIntegration(options: AuditOptions = {}): Integration { - return { name: "overflow", analyze: (page) => analyzeHtml(page.html, "overflow", options) }; + return { + name: "overflow", + analyze: (page) => analyzeHtml(page.html, "overflow", options), + }; } diff --git a/modules/analysis/inspect.ts b/modules/analysis/inspect.ts index d159e94..4fbea91 100644 --- a/modules/analysis/inspect.ts +++ b/modules/analysis/inspect.ts @@ -1,9 +1,22 @@ -/** Runs inside the browser against static DOM and resolved CSS, without executing page scripts. */ -export function inspectDocument(options: { kind: "accessible" | "overflow"; ignore: string[]; disclosures: boolean }) { +export function inspectDocument(options: { + kind: "accessible" | "overflow"; + ignore: string[]; + disclosures: boolean; +}) { const { document, getComputedStyle, innerWidth } = globalThis as any; - const findings: { rule: string; selector: string; severity: "error" | "warning" | "review"; message: string; evidence: Record }[] = []; + const findings: { + rule: string; + selector: string; + severity: "error" | "warning" | "review"; + message: string; + evidence: Record; + }[] = []; const elements = [...document.querySelectorAll("body *")]; - if (options.disclosures) for (const element of elements) if (element.tagName === "DETAILS") element.open = true; + if (options.disclosures) { + for (const element of elements) { + if (element.tagName === "DETAILS") element.open = true; + } + } const selector = (node: any): string => { if (node.id) return `[id=${JSON.stringify(node.id)}]`; const parts: string[] = []; @@ -14,62 +27,282 @@ export function inspectDocument(options: { kind: "accessible" | "overflow"; igno } return `body > ${parts.join(" > ")}`; }; - const add = (node: any, rule: string, message: string, evidence: Record = {}, severity: "error" | "warning" | "review" = "error") => findings.push({ selector: selector(node), rule, message, evidence, severity }); - const canvas = document.createElement("canvas"); canvas.width = canvas.height = 1; + const add = ( + node: any, + rule: string, + message: string, + evidence: Record = {}, + severity: "error" | "warning" | "review" = "error", + ) => + findings.push({ + selector: selector(node), + rule, + message, + evidence, + severity, + }); + const canvas = document.createElement("canvas"); + canvas.width = canvas.height = 1; const ctx = canvas.getContext("2d", { willReadFrequently: true }); - const color = (value: string): number[] => { ctx.clearRect(0, 0, 1, 1); ctx.fillStyle = value; ctx.fillRect(0, 0, 1, 1); return [...ctx.getImageData(0, 0, 1, 1).data]; }; - const composite = (front: number[], back: number[]): number[] => front.slice(0, 3).map((channel, index) => channel * front[3]! / 255 + back[index]! * (1 - front[3]! / 255)).concat(255); - const luminance = (rgb: number[]): number => rgb.slice(0, 3).map((value) => { const c = value / 255; return c <= .04045 ? c / 12.92 : ((c + .055) / 1.055) ** 2.4; }).reduce((sum, value, index) => sum + value * [.2126, .7152, .0722][index]!, 0); + const color = (value: string): number[] => { + ctx.clearRect(0, 0, 1, 1); + ctx.fillStyle = value; + ctx.fillRect(0, 0, 1, 1); + return [...ctx.getImageData(0, 0, 1, 1).data]; + }; + const composite = (front: number[], back: number[]): number[] => + front + .slice(0, 3) + .map( + (channel, index) => + (channel * front[3]!) / 255 + back[index]! * (1 - front[3]! / 255), + ) + .concat(255); + const luminance = (rgb: number[]): number => + rgb + .slice(0, 3) + .map((value) => { + const c = value / 255; + return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; + }) + .reduce( + (sum, value, index) => sum + value * [0.2126, 0.7152, 0.0722][index]!, + 0, + ); let checked = 0; for (const node of elements) { if (options.ignore.some((ignore) => node.closest(ignore))) continue; const style = getComputedStyle(node); const box = node.getBoundingClientRect(); - if (!box.width || !box.height || style.display === "none" || style.visibility !== "visible" || node.closest("[hidden], [inert]")) continue; + if ( + !box.width || + !box.height || + style.display === "none" || + style.visibility !== "visible" || + node.closest("[hidden], [inert]") + ) + continue; checked++; if (options.kind === "overflow") { - const clippedX = ["hidden", "clip"].includes(style.overflowX) && node.scrollWidth > node.clientWidth + 1; - const clippedY = ["hidden", "clip"].includes(style.overflowY) && node.scrollHeight > node.clientHeight + 1; - const truncated = style.textOverflow === "ellipsis" || (style.webkitLineClamp !== "none" && Number(style.webkitLineClamp) > 0); - if ((clippedX || clippedY) && !truncated) add(node, "clipping", "Content exceeds a clipping boundary", { overflowX: style.overflowX, overflowY: style.overflowY, clientWidth: node.clientWidth, scrollWidth: node.scrollWidth, clientHeight: node.clientHeight, scrollHeight: node.scrollHeight }); + const clippedX = + ["hidden", "clip"].includes(style.overflowX) && + node.scrollWidth > node.clientWidth + 1; + const clippedY = + ["hidden", "clip"].includes(style.overflowY) && + node.scrollHeight > node.clientHeight + 1; + const truncated = + style.textOverflow === "ellipsis" || + (style.webkitLineClamp !== "none" && Number(style.webkitLineClamp) > 0); + if ((clippedX || clippedY) && !truncated) { + add(node, "clipping", "Content exceeds a clipping boundary", { + overflowX: style.overflowX, + overflowY: style.overflowY, + clientWidth: node.clientWidth, + scrollWidth: node.scrollWidth, + clientHeight: node.clientHeight, + scrollHeight: node.scrollHeight, + }); + } let managed = false; - for (let parent = node.parentElement; parent; parent = parent.parentElement) if (["auto", "scroll", "hidden", "clip"].includes(getComputedStyle(parent).overflowX)) { managed = true; break; } - if (!managed && (box.right > innerWidth + 1 || box.left < -1)) add(node, "viewport-overflow", "Element extends outside the viewport without a containing scroll region", { left: box.left, right: box.right, viewportWidth: innerWidth }); + for ( + let parent = node.parentElement; + parent; + parent = parent.parentElement + ) { + if ( + ["auto", "scroll", "hidden", "clip"].includes( + getComputedStyle(parent).overflowX, + ) + ) { + managed = true; + break; + } + } + if (!managed && (box.right > innerWidth + 1 || box.left < -1)) { + add( + node, + "viewport-overflow", + "Element extends outside the viewport without a containing scroll region", + { + left: box.left, + right: box.right, + viewportWidth: innerWidth, + }, + ); + } continue; } - const label = node.getAttribute("aria-label") || (node.getAttribute("aria-labelledby") || "").split(/\s+/).map((id: string) => document.getElementById(id)?.textContent || "").join("").trim() || [...(node.labels || [])].map((label: any) => label.textContent).join("").trim(); - for (const id of (node.getAttribute("aria-labelledby") || "").split(/\s+/).filter(Boolean)) if (!document.getElementById(id)) add(node, "aria-reference", "aria-labelledby references a missing element", { id }); - if (["A", "BUTTON", "INPUT", "SELECT"].includes(node.tagName) && !node.disabled && (box.width < 24 || box.height < 24)) add(node, "target-size", "Interactive target is smaller than 24 CSS pixels; spacing exceptions require review", { width: box.width, height: box.height }, "review"); - if (node.tagName === "IMG" && !node.hasAttribute("alt")) add(node, "image-alt", "Image lacks an alt attribute"); - if (["INPUT", "SELECT", "TEXTAREA"].includes(node.tagName) && !["hidden", "submit", "button", "reset"].includes(node.type) && !label) add(node, "control-name", "Form control lacks a programmatic label"); - if (["A", "BUTTON"].includes(node.tagName) && !label && !node.textContent.trim() && !node.querySelector("img[alt]")) add(node, "control-name", "Interactive element lacks an accessible name"); - const ownText = [...node.childNodes].some((child: any) => child.nodeType === 3 && child.textContent.trim()); + const label = + node.getAttribute("aria-label") || + (node.getAttribute("aria-labelledby") || "") + .split(/\s+/) + .map((id: string) => document.getElementById(id)?.textContent || "") + .join("") + .trim() || + [...(node.labels || [])] + .map((label: any) => label.textContent) + .join("") + .trim(); + for (const id of (node.getAttribute("aria-labelledby") || "") + .split(/\s+/) + .filter(Boolean)) { + if (!document.getElementById(id)) { + add( + node, + "aria-reference", + "aria-labelledby references a missing element", + { id }, + ); + } + } + if ( + ["A", "BUTTON", "INPUT", "SELECT"].includes(node.tagName) && + !node.disabled && + (box.width < 24 || box.height < 24) + ) { + add( + node, + "target-size", + "Interactive target is smaller than 24 CSS pixels; spacing exceptions require review", + { width: box.width, height: box.height }, + "review", + ); + } + if (node.tagName === "IMG" && !node.hasAttribute("alt")) { + add(node, "image-alt", "Image lacks an alt attribute"); + } + if ( + ["INPUT", "SELECT", "TEXTAREA"].includes(node.tagName) && + !["hidden", "submit", "button", "reset"].includes(node.type) && + !label + ) + add(node, "control-name", "Form control lacks a programmatic label"); + if ( + ["A", "BUTTON"].includes(node.tagName) && + !label && + !node.textContent.trim() && + !node.querySelector("img[alt]") + ) { + add(node, "control-name", "Interactive element lacks an accessible name"); + } + const ownText = [...node.childNodes].some( + (child: any) => child.nodeType === 3 && child.textContent.trim(), + ); if (!ownText) continue; - const chain: any[] = []; let opacity = 1; - for (let parent = node; parent; parent = parent.parentElement) { chain.unshift(parent); opacity *= Number(getComputedStyle(parent).opacity); } + const chain: any[] = []; + let opacity = 1; + for (let parent = node; parent; parent = parent.parentElement) { + chain.unshift(parent); + opacity *= Number(getComputedStyle(parent).opacity); + } let background = [255, 255, 255, 255]; const effects: Record[] = []; for (const parent of chain) { const css = getComputedStyle(parent); background = composite(color(css.backgroundColor), background); - if (css.filter !== "none" || css.backdropFilter !== "none" || css.mixBlendMode !== "normal" || css.backgroundImage !== "none" || css.clipPath !== "none" || css.maskImage !== "none") effects.push({ selector: selector(parent), filter: css.filter, backdropFilter: css.backdropFilter, blend: css.mixBlendMode, backgroundImage: css.backgroundImage, clipPath: css.clipPath, maskImage: css.maskImage }); + if ( + css.filter !== "none" || + css.backdropFilter !== "none" || + css.mixBlendMode !== "normal" || + css.backgroundImage !== "none" || + css.clipPath !== "none" || + css.maskImage !== "none" + ) { + effects.push({ + selector: selector(parent), + filter: css.filter, + backdropFilter: css.backdropFilter, + blend: css.mixBlendMode, + backgroundImage: css.backgroundImage, + clipPath: css.clipPath, + maskImage: css.maskImage, + }); + } } - const foreground = color(style.color); foreground[3] = foreground[3]! * opacity; + const foreground = color(style.color); + foreground[3] = foreground[3]! * opacity; const text = composite(foreground, background); - const l1 = luminance(text), l2 = luminance(background); - const contrast = (Math.max(l1, l2) + .05) / (Math.min(l1, l2) + .05); - const threshold = parseFloat(style.fontSize) >= 24 || (parseFloat(style.fontSize) >= 18.667 && Number(style.fontWeight) >= 700) ? 3 : 4.5; - if (effects.length) add(node, "visibility-effects", "Filters, backdrop effects or masking require pixel-level review; nominal contrast is not conclusive", { effects, opacity, nominalContrast: contrast }, "review"); - if (contrast < threshold) add(node, "text-contrast", "Estimated text contrast falls below the configured text threshold", { contrast, threshold, opacity, estimate: effects.length > 0 }, effects.length ? "review" : "error"); - if (opacity < .5) add(node, "low-opacity", "Effective ancestor opacity reduces text visibility", { opacity }, "warning"); + const l1 = luminance(text), + l2 = luminance(background); + const contrast = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05); + const threshold = + parseFloat(style.fontSize) >= 24 || + (parseFloat(style.fontSize) >= 18.667 && Number(style.fontWeight) >= 700) + ? 3 + : 4.5; + if (effects.length) { + add( + node, + "visibility-effects", + "Filters, backdrop effects or masking require pixel-level review; nominal contrast is not conclusive", + { effects, opacity, nominalContrast: contrast }, + "review", + ); + } + if (contrast < threshold) { + add( + node, + "text-contrast", + "Estimated text contrast falls below the configured text threshold", + { contrast, threshold, opacity, estimate: effects.length > 0 }, + effects.length ? "review" : "error", + ); + } + if (opacity < 0.5) { + add( + node, + "low-opacity", + "Effective ancestor opacity reduces text visibility", + { opacity }, + "warning", + ); + } } if (options.kind === "accessible") { const ids = new Set(); - for (const node of document.querySelectorAll("[id]")) { if (ids.has(node.id)) add(node, "duplicate-id", "Duplicate identifiers make references ambiguous", { id: node.id }); ids.add(node.id); } + for (const node of document.querySelectorAll("[id]")) { + if (ids.has(node.id)) { + add( + node, + "duplicate-id", + "Duplicate identifiers make references ambiguous", + { id: node.id }, + ); + } + ids.add(node.id); + } let previous = 0; - for (const heading of document.querySelectorAll("h1,h2,h3,h4,h5,h6")) { const level = Number(heading.tagName.slice(1)); if (previous && level > previous + 1) add(heading, "heading-order", "Heading hierarchy skips a level", { previous, level }, "warning"); previous = level; } - if (!document.documentElement.lang) findings.push({ selector: "html", rule: "document-language", severity: "error", message: "Document language is missing", evidence: {} }); - if (!document.title.trim()) findings.push({ selector: "head", rule: "document-title", severity: "error", message: "Document title is missing", evidence: {} }); + for (const heading of document.querySelectorAll("h1,h2,h3,h4,h5,h6")) { + const level = Number(heading.tagName.slice(1)); + if (previous && level > previous + 1) { + add( + heading, + "heading-order", + "Heading hierarchy skips a level", + { previous, level }, + "warning", + ); + } + previous = level; + } + if (!document.documentElement.lang) { + findings.push({ + selector: "html", + rule: "document-language", + severity: "error", + message: "Document language is missing", + evidence: {}, + }); + } + if (!document.title.trim()) { + findings.push({ + selector: "head", + rule: "document-title", + severity: "error", + message: "Document title is missing", + evidence: {}, + }); + } } return { findings, checked }; } diff --git a/modules/analysis/package.json b/modules/analysis/package.json index 8062ef9..066a29b 100644 --- a/modules/analysis/package.json +++ b/modules/analysis/package.json @@ -1,7 +1,10 @@ { - "name": "@muddle/analysis", - "version": "0.1.0", - "type": "module", - "exports": "./index.ts", - "peerDependencies": { "muddle": "*", "playwright": "^1.0.0" } + "name": "@muddle/analysis", + "version": "0.1.0", + "type": "module", + "exports": "./index.ts", + "peerDependencies": { + "muddle": "*", + "playwright": "^1.0.0" + } } diff --git a/modules/astro/index.ts b/modules/astro/index.ts index 1f2665f..7949847 100644 --- a/modules/astro/index.ts +++ b/modules/astro/index.ts @@ -7,23 +7,51 @@ import { experimental_AstroContainer as AstroContainer } from "astro/container"; import ts from "typescript"; import type { Integration, IntegrationContext } from "muddle/contracts"; -export interface AstroIntegrationOptions { dir?: string; partials?: boolean } +export interface AstroIntegrationOptions { + dir?: string; + partials?: boolean; +} function routeFor(root: string, file: string): string { - const path = relative(root, file).replace(/\\/g, "/").replace(/\.astro$/, ""); + const path = relative(root, file) + .replace(/\\/g, "/") + .replace(/\.astro$/, ""); return `/${path.replace(/index$/, "").replace(/\/+/g, "/") || ""}` || "/"; } -export function astroIntegration(options: AstroIntegrationOptions = {}): Integration { +export function astroIntegration( + options: AstroIntegrationOptions = {}, +): Integration { return { name: "astro", async setup(context: IntegrationContext) { - context.loader("astro", { name: "astro", extensions: [".astro"], async load(file) { return { data: {}, content: await renderAstro(file.source, undefined, file.path) }; } }); + context.loader("astro", { + name: "astro", + extensions: [".astro"], + async load(file) { + return { + data: {}, + content: await renderAstro(file.source, undefined, file.path), + }; + }, + }); const dir = resolve(context.config.root, options.dir ?? "src/pages"); if (!existsSync(dir)) return; for (const file of await astroFiles(dir)) { const route = routeFor(dir, file); - context.route("GET", route, async (request) => new Response(await renderAstro(await readFile(file, "utf8"), request, file), { headers: { "content-type": "text/html; charset=utf-8" } })); + context.route( + "GET", + route, + async (request) => + new Response( + await renderAstro(await readFile(file, "utf8"), request, file), + { + headers: { + "content-type": "text/html; charset=utf-8", + }, + }, + ), + ); } }, }; @@ -39,42 +67,92 @@ async function astroFiles(dir: string): Promise { return files; } -export async function renderAstro(source: string, request = new Request("http://localhost/"), filename = resolve("inline.astro")): Promise { +export async function renderAstro( + source: string, + request = new Request("http://localhost/"), + filename = resolve("inline.astro"), +): Promise { const component = await compileAstro(source, filename); const container = await AstroContainer.create(); return container.renderToString(component, { request }); } -async function compileAstro(source: string, filename: string, ancestors: string[] = []): Promise { - if (ancestors.includes(filename)) throw new Error(`Circular Astro import ${filename}`); - const result = await transform(source, { filename, internalURL: "astro/runtime/server/index.js" }); - let code = result.code.replace(/,\s*createMetadata as \$\$createMetadata/, "").replace(/export const \$\$metadata = [^\n]+\n/g, ""); +async function compileAstro( + source: string, + filename: string, + ancestors: string[] = [], +): Promise { + if (ancestors.includes(filename)) { + throw new Error(`Circular Astro import ${filename}`); + } + const result = await transform(source, { + filename, + internalURL: "astro/runtime/server/index.js", + }); + let code = result.code + .replace(/,\s*createMetadata as \$\$createMetadata/, "") + .replace(/export const \$\$metadata = [^\n]+\n/g, ""); for (const match of [...code.matchAll(/from\s+["']([^"']+)["']/g)]) { const specifier = match[1]!; let url: string; if (specifier.endsWith(".astro")) { const file = resolve(filename, "..", specifier); - const child = await compileAstro(await readFile(file, "utf8"), file, [...ancestors, filename]); + const child = await compileAstro(await readFile(file, "utf8"), file, [ + ...ancestors, + filename, + ]); const key = `__muddle_astro_${crypto.randomUUID().replaceAll("-", "")}`; (globalThis as Record)[key] = child; - url = `data:text/javascript;base64,${Buffer.from(`export default globalThis[${JSON.stringify(key)}]`).toString("base64")}`; - } else url = specifier.startsWith(".") ? pathToFileURL(resolve(filename, "..", specifier)).href : import.meta.resolve(specifier); - code = code.replaceAll(`"${specifier}"`, JSON.stringify(url)).replaceAll(`'${specifier}'`, JSON.stringify(url)); + url = `data:text/javascript;base64,${Buffer.from( + `export default globalThis[${JSON.stringify(key)}]`, + ).toString("base64")}`; + } else { + url = specifier.startsWith(".") + ? pathToFileURL(resolve(filename, "..", specifier)).href + : import.meta.resolve(specifier); + } + code = code + .replaceAll(`"${specifier}"`, JSON.stringify(url)) + .replaceAll(`'${specifier}'`, JSON.stringify(url)); } - code = ts.transpileModule(code, { compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 } }).outputText; - return (await import(`data:text/javascript;base64,${Buffer.from(code).toString("base64")}`)).default; + code = ts.transpileModule(code, { + compilerOptions: { + module: ts.ModuleKind.ESNext, + target: ts.ScriptTarget.ES2022, + }, + }).outputText; + return ( + await import( + `data:text/javascript;base64,${Buffer.from(code).toString("base64")}` + ) + ).default; } /** Import into TemplateContext with `.import("Card", astroComponent("./Card.astro"))`. */ export function astroComponent(file: string) { return { - async render(props: Record, context: { params: Record; locals: Record }) { + async render( + props: Record, + context: { + params: Record; + locals: Record; + }, + ) { const path = resolve(file); const component = await compileAstro(await readFile(path, "utf8"), path); const container = await AstroContainer.create(); const slots: Record = {}; - for (const [key, value] of Object.entries(props)) if (typeof value === "function") slots[key === "content" ? "default" : key] = await value(); - return container.renderToString(component, { props, params: context.params, locals: context.locals, slots }); + for (const [key, value] of Object.entries(props)) { + if (typeof value === "function") { + slots[key === "content" ? "default" : key] = await value(); + } + } + return container.renderToString(component, { + props, + params: context.params, + locals: context.locals, + slots, + }); }, }; } diff --git a/modules/astro/package.json b/modules/astro/package.json index 8c3e392..b8c80cb 100644 --- a/modules/astro/package.json +++ b/modules/astro/package.json @@ -1,14 +1,14 @@ { - "name": "@muddle/astro", - "type": "module", - "exports": "./index.ts", - "version": "0.1.0", - "peerDependencies": { - "muddle": "*" - }, - "dependencies": { - "@astrojs/compiler": "2", - "astro": "5", - "typescript": "5.9.3" - } + "name": "@muddle/astro", + "type": "module", + "exports": "./index.ts", + "version": "0.1.0", + "peerDependencies": { + "muddle": "*" + }, + "dependencies": { + "@astrojs/compiler": "2", + "astro": "5", + "typescript": "5.9.3" + } } diff --git a/modules/database/package.json b/modules/database/package.json index 1ea59a8..e0c0144 100644 --- a/modules/database/package.json +++ b/modules/database/package.json @@ -1,12 +1,12 @@ { - "name": "@muddle/database", - "type": "module", - "exports": "./index.ts", - "version": "0.1.0", - "peerDependencies": { - "muddle": "*" - }, - "dependencies": { - "pg": "^8.23.1" - } + "name": "@muddle/database", + "type": "module", + "exports": "./index.ts", + "version": "0.1.0", + "peerDependencies": { + "muddle": "*" + }, + "dependencies": { + "pg": "^8.23.1" + } } diff --git a/modules/database/src/migrations.ts b/modules/database/src/migrations.ts index 0666974..7304ea7 100644 --- a/modules/database/src/migrations.ts +++ b/modules/database/src/migrations.ts @@ -1,19 +1,39 @@ import type { TableClass } from "muddle/contracts/typegen/orm/index.ts"; import { getTableMetadata } from "./decorators/Table.ts"; -export interface SchemaColumn { name: string; type: string; nullable: boolean } -export interface SchemaTable { name: string; columns: SchemaColumn[] } -export interface Migration { up: string[]; down: string[] } +export interface SchemaColumn { + name: string; + type: string; + nullable: boolean; +} +export interface SchemaTable { + name: string; + columns: SchemaColumn[]; +} +export interface Migration { + up: string[]; + down: string[]; +} export function schemaOf(tables: TableClass[]): SchemaTable[] { return tables.map((table) => { const metadata = getTableMetadata(table); if (!metadata) throw new Error(`Missing @Table metadata for ${table.name}`); - return { name: metadata.name, columns: [...metadata.fields.values()].map((field) => ({ name: field.column, type: field.type, nullable: field.nullable })) }; + return { + name: metadata.name, + columns: [...metadata.fields.values()].map((field) => ({ + name: field.column, + type: field.type, + nullable: field.nullable, + })), + }; }); } -export function diffSchema(previous: SchemaTable[], next: SchemaTable[]): Migration { +export function diffSchema( + previous: SchemaTable[], + next: SchemaTable[], +): Migration { const oldTables = new Map(previous.map((table) => [table.name, table])); const nextTables = new Map(next.map((table) => [table.name, table])); const up: string[] = []; @@ -21,20 +41,28 @@ export function diffSchema(previous: SchemaTable[], next: SchemaTable[]): Migrat for (const table of next) { const old = oldTables.get(table.name); if (!old) { - up.push(`CREATE TABLE ${table.name} (${table.columns.map(columnSql).join(", ")});`); + up.push( + `CREATE TABLE ${table.name} (${table.columns.map(columnSql).join(", ")});`, + ); down.unshift(`DROP TABLE ${table.name};`); continue; } - const oldColumns = new Map(old.columns.map((column) => [column.name, column])); - for (const column of table.columns) if (!oldColumns.has(column.name)) { - up.push(`ALTER TABLE ${table.name} ADD COLUMN ${columnSql(column)};`); - down.unshift(`ALTER TABLE ${table.name} DROP COLUMN ${column.name};`); + const oldColumns = new Map( + old.columns.map((column) => [column.name, column]), + ); + for (const column of table.columns) + if (!oldColumns.has(column.name)) { + up.push(`ALTER TABLE ${table.name} ADD COLUMN ${columnSql(column)};`); + down.unshift(`ALTER TABLE ${table.name} DROP COLUMN ${column.name};`); + } + } + for (const table of previous) + if (!nextTables.has(table.name)) { + up.push(`DROP TABLE ${table.name};`); + down.unshift( + `CREATE TABLE ${table.name} (${table.columns.map(columnSql).join(", ")});`, + ); } - } - for (const table of previous) if (!nextTables.has(table.name)) { - up.push(`DROP TABLE ${table.name};`); - down.unshift(`CREATE TABLE ${table.name} (${table.columns.map(columnSql).join(", ")});`); - } return { up, down }; } diff --git a/modules/database/src/sessions.ts b/modules/database/src/sessions.ts index f4e8898..b6edc45 100644 --- a/modules/database/src/sessions.ts +++ b/modules/database/src/sessions.ts @@ -3,33 +3,66 @@ import type { SqlDriver } from "muddle/contracts/typegen/orm/index.ts"; /** Persistent sessions and atomic OAuth transaction consumption for SQLite, D1 and Postgres. */ export class DatabaseSessionStore implements SessionStore { - private constructor(private readonly driver: SqlDriver, private readonly table: string) {} + private constructor( + private readonly driver: SqlDriver, + private readonly table: string, + ) {} - static async create(driver: SqlDriver, table = "muddle_sessions"): Promise { - if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table)) throw new Error("Invalid session table name"); - await driver.execute(`CREATE TABLE IF NOT EXISTS "${table}" (id TEXT PRIMARY KEY, value TEXT NOT NULL, expires BIGINT NOT NULL)`); + static async create( + driver: SqlDriver, + table = "muddle_sessions", + ): Promise { + if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table)) + throw new Error("Invalid session table name"); + await driver.execute( + `CREATE TABLE IF NOT EXISTS "${table}" (id TEXT PRIMARY KEY, value TEXT NOT NULL, expires BIGINT NOT NULL)`, + ); return new DatabaseSessionStore(driver, table); } async get(key: string): Promise { - const rows = await this.driver.query<{ value: string; expires: number }>(`SELECT value, expires FROM "${this.table}" WHERE id = ${this.driver.placeholder(1)}`, [key]); + const rows = await this.driver.query<{ value: string; expires: number }>( + `SELECT value, expires FROM "${this.table}" WHERE id = ${this.driver.placeholder(1)}`, + [key], + ); const row = rows[0]; - if (!row || Number(row.expires) <= Date.now()) { await this.delete(key); return undefined; } + if (!row || Number(row.expires) <= Date.now()) { + await this.delete(key); + return undefined; + } return { value: JSON.parse(row.value), expires: Number(row.expires) }; } async set(key: string, record: SessionRecord): Promise { const args = [key, JSON.stringify(record.value), record.expires]; - await this.driver.execute(`INSERT INTO "${this.table}" (id, value, expires) VALUES (${args.map((_, index) => this.driver.placeholder(index + 1)).join(", ")}) ON CONFLICT (id) DO UPDATE SET value = excluded.value, expires = excluded.expires`, args); + await this.driver.execute( + `INSERT INTO "${this.table}" (id, value, expires) VALUES (${args.map((_, index) => this.driver.placeholder(index + 1)).join(", ")}) ON CONFLICT (id) DO UPDATE SET value = excluded.value, expires = excluded.expires`, + args, + ); } - async delete(key: string): Promise { await this.driver.execute(`DELETE FROM "${this.table}" WHERE id = ${this.driver.placeholder(1)}`, [key]); } + async delete(key: string): Promise { + await this.driver.execute( + `DELETE FROM "${this.table}" WHERE id = ${this.driver.placeholder(1)}`, + [key], + ); + } async consume(key: string): Promise { - const rows = await this.driver.query<{ value: string; expires: number }>(`DELETE FROM "${this.table}" WHERE id = ${this.driver.placeholder(1)} RETURNING value, expires`, [key]); + const rows = await this.driver.query<{ value: string; expires: number }>( + `DELETE FROM "${this.table}" WHERE id = ${this.driver.placeholder(1)} RETURNING value, expires`, + [key], + ); const row = rows[0]; - return row && Number(row.expires) > Date.now() ? { value: JSON.parse(row.value), expires: Number(row.expires) } : undefined; + return row && Number(row.expires) > Date.now() + ? { value: JSON.parse(row.value), expires: Number(row.expires) } + : undefined; } - async prune(): Promise { await this.driver.execute(`DELETE FROM "${this.table}" WHERE expires <= ${this.driver.placeholder(1)}`, [Date.now()]); } + async prune(): Promise { + await this.driver.execute( + `DELETE FROM "${this.table}" WHERE expires <= ${this.driver.placeholder(1)}`, + [Date.now()], + ); + } } diff --git a/modules/latex/index.ts b/modules/latex/index.ts index 39dfa2d..8f74a9d 100644 --- a/modules/latex/index.ts +++ b/modules/latex/index.ts @@ -6,34 +6,61 @@ import { renderMarkdown } from "muddle/markdown"; import katex from "katex"; export function renderLatex(source: string, displayMode = false): string { - return katex.renderToString(source, { displayMode, throwOnError: false, trust: false }); + return katex.renderToString(source, { + displayMode, + throwOnError: false, + trust: false, + }); } export function renderMathMarkdown(source: string): string { const formulas: string[] = []; - const body = source.replace(/\$\$([\s\S]+?)\$\$|(? { - formulas.push(renderLatex(display ?? inline, display !== undefined)); - return `MUDDLEMATH${formulas.length - 1}END`; - }); - return renderMarkdown(body).replace(/MUDDLEMATH(\d+)END/g, (_match, index) => formulas[Number(index)] ?? ""); + const body = source.replace( + /\$\$([\s\S]+?)\$\$|(? { + formulas.push(renderLatex(display ?? inline, display !== undefined)); + return `MUDDLEMATH${formulas.length - 1}END`; + }, + ); + return renderMarkdown(body).replace( + /MUDDLEMATH(\d+)END/g, + (_match, index) => formulas[Number(index)] ?? "", + ); } -export interface LatexIntegrationOptions { route?: string } +export interface LatexIntegrationOptions { + route?: string; +} -export function latexIntegration(options: LatexIntegrationOptions = {}): Integration { +export function latexIntegration( + options: LatexIntegrationOptions = {}, +): Integration { const route = options.route ?? "/_muddle/katex.css"; return { name: "latex", async setup(context: IntegrationContext) { - const base = dirname(fileURLToPath(import.meta.resolve("katex/package.json"))); + const base = dirname( + fileURLToPath(import.meta.resolve("katex/package.json")), + ); const css = await readFile(join(base, "dist", "katex.min.css"), "utf8"); context.loader("latex-markdown", { name: "latex-markdown", extensions: [".latex.md", ".latex.markdown"], - load(file) { return { data: {}, content: renderMathMarkdown(file.source) }; }, + load(file) { + return { data: {}, content: renderMathMarkdown(file.source) }; + }, }); - context.route("GET", route, () => new Response(css, { headers: { "content-type": "text/css; charset=utf-8", "cache-control": "public, max-age=31536000, immutable" } })); + context.route( + "GET", + route, + () => + new Response(css, { + headers: { + "content-type": "text/css; charset=utf-8", + "cache-control": "public, max-age=31536000, immutable", + }, + }), + ); }, }; } - diff --git a/modules/latex/package.json b/modules/latex/package.json index 0f3ef7f..dd705fb 100644 --- a/modules/latex/package.json +++ b/modules/latex/package.json @@ -1,12 +1,12 @@ { - "name": "@muddle/latex", - "type": "module", - "exports": "./index.ts", - "version": "0.1.0", - "peerDependencies": { - "muddle": "*" - }, - "dependencies": { - "katex": "^0.19.0" - } + "name": "@muddle/latex", + "type": "module", + "exports": "./index.ts", + "version": "0.1.0", + "peerDependencies": { + "muddle": "*" + }, + "dependencies": { + "katex": "^0.19.0" + } } diff --git a/modules/mdx/index.ts b/modules/mdx/index.ts index c0f760d..5622731 100644 --- a/modules/mdx/index.ts +++ b/modules/mdx/index.ts @@ -10,5 +10,16 @@ export async function renderMdx(source: string): Promise { } export function mdxIntegration(): Integration { - return { name: "mdx", setup(context) { context.loader("mdx", { name: "mdx", extensions: [".mdx"], async load(file) { return { data: {}, content: await renderMdx(file.source) }; } }); } }; + return { + name: "mdx", + setup(context) { + context.loader("mdx", { + name: "mdx", + extensions: [".mdx"], + async load(file) { + return { data: {}, content: await renderMdx(file.source) }; + }, + }); + }, + }; } diff --git a/modules/mdx/package.json b/modules/mdx/package.json index e53c043..043f7e1 100644 --- a/modules/mdx/package.json +++ b/modules/mdx/package.json @@ -1,18 +1,18 @@ { - "name": "@muddle/mdx", - "type": "module", - "exports": "./index.ts", - "version": "0.1.0", - "peerDependencies": { - "muddle": "*" - }, - "dependencies": { - "@mdx-js/mdx": "^3.1.1", - "react": "^19.3.1", - "react-dom": "^19.3.1" - }, - "devDependencies": { - "@types/react": "^19.3.0", - "@types/react-dom": "^19.3.0" - } + "name": "@muddle/mdx", + "type": "module", + "exports": "./index.ts", + "version": "0.1.0", + "peerDependencies": { + "muddle": "*" + }, + "dependencies": { + "@mdx-js/mdx": "^3.1.1", + "react": "^19.3.1", + "react-dom": "^19.3.1" + }, + "devDependencies": { + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0" + } } diff --git a/modules/optimize/README.md b/modules/optimize/README.md index da43184..93515bf 100644 --- a/modules/optimize/README.md +++ b/modules/optimize/README.md @@ -1 +1 @@ -# Build optimization \ No newline at end of file +# Build optimization diff --git a/modules/optimize/index.ts b/modules/optimize/index.ts index 6e42db8..69a093c 100644 --- a/modules/optimize/index.ts +++ b/modules/optimize/index.ts @@ -19,9 +19,11 @@ export async function minifyHtml(source: string): Promise { }); } - export async function minifyJavaScript(source: string): Promise { - return (await terser.minify(source, { format: { comments: false } })).code ?? source; + return ( + (await terser.minify(source, { format: { comments: false } })).code ?? + source + ); } export function optimizeSvg(source: string): string { @@ -36,16 +38,29 @@ export function imageOptimization(optimizer: ImageOptimizer): ImageOptimizer { return optimizer; } -function loaderIntegration(name: string, extension: string, transform: (source: string) => string | Promise): Integration { +function loaderIntegration( + name: string, + extension: string, + transform: (source: string) => string | Promise, +): Integration { return { name, async transform(asset) { if (!asset.path.endsWith(extension)) return asset; - const source = typeof asset.content === "string" ? asset.content : new TextDecoder().decode(asset.content); + const source = + typeof asset.content === "string" + ? asset.content + : new TextDecoder().decode(asset.content); return { ...asset, content: await transform(source) }; }, setup(context: IntegrationContext) { - context.loader(name, { name, extensions: [extension], async load(file) { return { data: {}, content: await transform(file.source) }; } }); + context.loader(name, { + name, + extensions: [extension], + async load(file) { + return { data: {}, content: await transform(file.source) }; + }, + }); }, }; } @@ -66,20 +81,31 @@ export function svgoIntegration(): Integration { return loaderIntegration("svgo", ".svg", optimizeSvg); } -export function imageOptimizationIntegration(optimizer: ImageOptimizer): Integration { +export function imageOptimizationIntegration( + optimizer: ImageOptimizer, +): Integration { return { name: "image-optimization", async transform(asset) { if (!/\.(png|jpe?g|webp|avif)$/i.test(asset.path)) return asset; - const input = typeof asset.content === "string" ? new TextEncoder().encode(asset.content) : asset.content; - return { ...asset, content: await optimizer.optimize(input, asset.contentType) }; + const input = + typeof asset.content === "string" + ? new TextEncoder().encode(asset.content) + : asset.content; + return { + ...asset, + content: await optimizer.optimize(input, asset.contentType), + }; }, setup(context: IntegrationContext) { context.loader("image", { name: "image", extensions: [".png", ".jpg", ".jpeg", ".webp", ".avif"], async load(file) { - const optimized = await optimizer.optimize(new TextEncoder().encode(file.source), "image/*"); + const optimized = await optimizer.optimize( + new TextEncoder().encode(file.source), + "image/*", + ); return { data: { optimized }, content: file.source }; }, }); diff --git a/modules/optimize/package.json b/modules/optimize/package.json index 175d648..e36f9de 100644 --- a/modules/optimize/package.json +++ b/modules/optimize/package.json @@ -1,17 +1,17 @@ { - "name": "@muddle/optimize", - "version": "0.1.0", - "type": "module", - "exports": "./index.ts", - "dependencies": { - "terser": "^5.51.2", - "svgo": "^4.1.0", - "csso": "^5.0.5", - "html-minifier-terser": "^7.2.0", - "@types/csso": "^5.0.5", - "@types/html-minifier-terser": "^7.0.2" - }, - "peerDependencies": { - "muddle": "*" - } + "name": "@muddle/optimize", + "version": "0.1.0", + "type": "module", + "exports": "./index.ts", + "dependencies": { + "terser": "^5.51.2", + "svgo": "^4.1.0", + "csso": "^5.0.5", + "html-minifier-terser": "^7.2.0", + "@types/csso": "^5.0.5", + "@types/html-minifier-terser": "^7.0.2" + }, + "peerDependencies": { + "muddle": "*" + } } diff --git a/modules/prettier-plugin/index.ts b/modules/prettier-plugin/index.ts index bafeebd..8250839 100644 --- a/modules/prettier-plugin/index.ts +++ b/modules/prettier-plugin/index.ts @@ -1,18 +1,32 @@ import type { Plugin, Parser } from "prettier"; import { formatSource, type FormatLanguage } from "muddle/formatter"; -interface Document { formatted: string } +interface Document { + formatted: string; +} const parser = (language: FormatLanguage): Parser => ({ astFormat: "muddle-document", - async parse(source, options) { return { formatted: await formatSource(source, language, options) }; }, + async parse(source, options) { + return { formatted: await formatSource(source, language, options) }; + }, locStart: () => 0, locEnd: (node) => node.formatted.length, }); const plugin: Plugin = { languages: [ - { name: "SPUG", parsers: ["spug"], extensions: [".spug", ".stpug", ".stug"], vscodeLanguageIds: ["spug"] }, - { name: "STCSS", parsers: ["stcss"], extensions: [".stcss"], vscodeLanguageIds: ["stcss"] }, + { + name: "SPUG", + parsers: ["spug"], + extensions: [".spug", ".stpug", ".stug"], + vscodeLanguageIds: ["spug"], + }, + { + name: "STCSS", + parsers: ["stcss"], + extensions: [".stcss"], + vscodeLanguageIds: ["stcss"], + }, ], parsers: { spug: parser("spug"), stcss: parser("stcss") }, printers: { "muddle-document": { print: (path) => path.node.formatted } }, diff --git a/modules/prettier-plugin/package.json b/modules/prettier-plugin/package.json index fc124ef..ea90f8a 100644 --- a/modules/prettier-plugin/package.json +++ b/modules/prettier-plugin/package.json @@ -1,7 +1,10 @@ { - "name": "@muddle/prettier-plugin", - "version": "0.1.0", - "type": "module", - "exports": "./index.ts", - "peerDependencies": { "muddle": "*", "prettier": "^3.0.0" } + "name": "@muddle/prettier-plugin", + "version": "0.1.0", + "type": "module", + "exports": "./index.ts", + "peerDependencies": { + "muddle": "*", + "prettier": "^3.0.0" + } } diff --git a/modules/shadcn/index.ts b/modules/shadcn/index.ts index f93f5b4..8ce0548 100644 --- a/modules/shadcn/index.ts +++ b/modules/shadcn/index.ts @@ -1 +1,4 @@ -export { shadcnIntegration, type ShadcnIntegrationOptions } from "./src/integration.ts"; +export { + shadcnIntegration, + type ShadcnIntegrationOptions, +} from "./src/integration.ts"; diff --git a/modules/shadcn/package.json b/modules/shadcn/package.json index 06e46fe..e0533fc 100644 --- a/modules/shadcn/package.json +++ b/modules/shadcn/package.json @@ -1,7 +1,9 @@ { - "name": "@muddle/shadcn", - "type": "module", - "exports": "./index.ts", - "version": "0.1.0", - "peerDependencies": { "muddle": "*" } + "name": "@muddle/shadcn", + "type": "module", + "exports": "./index.ts", + "version": "0.1.0", + "peerDependencies": { + "muddle": "*" + } } diff --git a/modules/shadcn/src/integration.ts b/modules/shadcn/src/integration.ts index 9d51f81..a9cd130 100644 --- a/modules/shadcn/src/integration.ts +++ b/modules/shadcn/src/integration.ts @@ -1,6 +1,11 @@ import { mkdir, writeFile } from "node:fs/promises"; import { basename, extname, join, resolve } from "node:path"; -import { cssToStcss, reactToSpug, type Integration, type IntegrationContext } from "muddle"; +import { + cssToStcss, + reactToSpug, + type Integration, + type IntegrationContext, +} from "muddle"; export interface ShadcnIntegrationOptions { components: string[]; @@ -15,32 +20,51 @@ interface RegistryItem { } /** Pulls ShadCN registry entries and writes their Muddle ports into the host project. */ -export function shadcnIntegration(options: ShadcnIntegrationOptions): Integration { +export function shadcnIntegration( + options: ShadcnIntegrationOptions, +): Integration { return { name: "shadcn", async setup(context: IntegrationContext) { const fetcher = options.fetch ?? globalThis.fetch; - const destination = resolve(context.config.root, options.dir ?? "modules/app/views/components"); + const destination = resolve( + context.config.root, + options.dir ?? "modules/app/views/components", + ); const styles = resolve(context.config.root, "styles/shadcn"); const seen = new Set(); const pull = async (name: string): Promise => { if (seen.has(name)) return; seen.add(name); - const base = options.registry ?? "https://ui.shadcn.com/r/styles/new-york/{name}.json"; - const url = base.includes("{name}") ? base.replace("{name}", encodeURIComponent(name)) : `${base.replace(/\/$/, "")}/${encodeURIComponent(name)}.json`; + const base = + options.registry ?? + "https://ui.shadcn.com/r/styles/new-york/{name}.json"; + const url = base.includes("{name}") + ? base.replace("{name}", encodeURIComponent(name)) + : `${base.replace(/\/$/, "")}/${encodeURIComponent(name)}.json`; const response = await fetcher(url); - if (!response.ok) throw new Error(`shadcn registry request failed for ${name} with ${response.status}`); - const item = await response.json() as RegistryItem; - for (const dependency of item.registryDependencies ?? []) await pull(dependency.replace(/^@[^/]+\//, "")); + if (!response.ok) + throw new Error( + `shadcn registry request failed for ${name} with ${response.status}`, + ); + const item = (await response.json()) as RegistryItem; + for (const dependency of item.registryDependencies ?? []) + await pull(dependency.replace(/^@[^/]+\//, "")); for (const file of item.files ?? []) { const extension = extname(file.path).toLowerCase(); const stem = basename(file.path).replace(/\.(tsx|jsx|css)$/i, ""); if (extension === ".tsx" || extension === ".jsx") { await mkdir(destination, { recursive: true }); - await writeFile(join(destination, `${stem}.spug`), reactToSpug(file.content, file.path)); + await writeFile( + join(destination, `${stem}.spug`), + reactToSpug(file.content, file.path), + ); } else if (extension === ".css") { await mkdir(styles, { recursive: true }); - await writeFile(join(styles, `${stem}.stcss`), cssToStcss(file.content, file.path)); + await writeFile( + join(styles, `${stem}.stcss`), + cssToStcss(file.content, file.path), + ); } } }; diff --git a/modules/tailwind/index.ts b/modules/tailwind/index.ts index 0b5eaeb..8a949d3 100644 --- a/modules/tailwind/index.ts +++ b/modules/tailwind/index.ts @@ -10,20 +10,42 @@ export interface TailwindOptions { content?: string[]; } -const SCANNED_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".html", ".spug", ".mdx"]); +const SCANNED_EXTENSIONS = new Set([ + ".ts", + ".tsx", + ".js", + ".jsx", + ".html", + ".spug", + ".mdx", +]); -export async function tailwindCandidates(root: string, paths?: string[]): Promise { +export async function tailwindCandidates( + root: string, + paths?: string[], +): Promise { const candidates = new Set(); const visit = async (dir: string): Promise => { for (const entry of await readdir(dir, { withFileTypes: true })) { - if (entry.name === "node_modules" || entry.name === "dist" || entry.name.startsWith(".")) continue; + if ( + entry.name === "node_modules" || + entry.name === "dist" || + entry.name.startsWith(".") + ) + continue; const path = join(dir, entry.name); - if (entry.isDirectory()) { await visit(path); continue; } + if (entry.isDirectory()) { + await visit(path); + continue; + } if (!SCANNED_EXTENSIONS.has(extname(entry.name))) continue; if (paths && !paths.some((value) => path.includes(value))) continue; const source = await readFile(path, "utf8"); - for (const match of source.matchAll(/(?:class|className)\s*=\s*["'`]([^"'`]+)["'`]/g)) { - for (const value of (match[1] ?? "").split(/\s+/)) if (value) candidates.add(value); + for (const match of source.matchAll( + /(?:class|className)\s*=\s*["'`]([^"'`]+)["'`]/g, + )) { + for (const value of (match[1] ?? "").split(/\s+/)) + if (value) candidates.add(value); } } }; @@ -31,32 +53,75 @@ export async function tailwindCandidates(root: string, paths?: string[]): Promis return [...candidates]; } -export async function compileTailwind(css: string, candidates: string[] = []): Promise { - const tailwindCss = join(dirname(fileURLToPath(import.meta.resolve("tailwindcss/package.json"))), "index.css"); +export async function compileTailwind( + css: string, + candidates: string[] = [], +): Promise { + const tailwindCss = join( + dirname(fileURLToPath(import.meta.resolve("tailwindcss/package.json"))), + "index.css", + ); const compiler = await compile(css, { base: process.cwd(), loadStylesheet: async (id) => { - if (!["tailwindcss", "tailwindcss/theme.css", "tailwindcss/utilities.css"].includes(id)) throw new Error(`Unsupported Tailwind stylesheet "${id}"`); - const path = id === "tailwindcss" ? tailwindCss : join(dirname(tailwindCss), id.split("/")[1]!); - return { path, base: process.cwd(), content: await readFile(path, "utf8") }; + if ( + ![ + "tailwindcss", + "tailwindcss/theme.css", + "tailwindcss/utilities.css", + ].includes(id) + ) + throw new Error(`Unsupported Tailwind stylesheet "${id}"`); + const path = + id === "tailwindcss" + ? tailwindCss + : join(dirname(tailwindCss), id.split("/")[1]!); + return { + path, + base: process.cwd(), + content: await readFile(path, "utf8"), + }; }, }); return compiler.build(candidates); } -export function tailwindIntegration(options: TailwindOptions = {}): Integration { +export function tailwindIntegration( + options: TailwindOptions = {}, +): Integration { const route = options.route ?? "/_muddle/tailwind.css"; const source = options.css ?? '@import "tailwindcss";'; return { name: "tailwind", async setup(context: IntegrationContext) { - const build = () => tailwindCandidates(context.config.root, options.content).then((candidates) => compileTailwind(source, candidates)); + const build = () => + tailwindCandidates(context.config.root, options.content).then( + (candidates) => compileTailwind(source, candidates), + ); context.loader("tailwind", { name: "tailwind", extensions: [".tailwind.css"], - async load(file) { return { data: {}, content: await compileTailwind(file.source, await tailwindCandidates(context.config.root, options.content)) }; }, + async load(file) { + return { + data: {}, + content: await compileTailwind( + file.source, + await tailwindCandidates(context.config.root, options.content), + ), + }; + }, }); - context.route("GET", route, async () => new Response(await build(), { headers: { "content-type": "text/css; charset=utf-8", "cache-control": "public, max-age=60" } })); + context.route( + "GET", + route, + async () => + new Response(await build(), { + headers: { + "content-type": "text/css; charset=utf-8", + "cache-control": "public, max-age=60", + }, + }), + ); }, }; } diff --git a/modules/tailwind/package.json b/modules/tailwind/package.json index b3bb253..a26ecf9 100644 --- a/modules/tailwind/package.json +++ b/modules/tailwind/package.json @@ -1,12 +1,12 @@ { - "name": "@muddle/tailwind", - "type": "module", - "exports": "./index.ts", - "version": "0.1.0", - "peerDependencies": { - "muddle": "*" - }, - "dependencies": { - "tailwindcss": "^4.3.3" - } + "name": "@muddle/tailwind", + "type": "module", + "exports": "./index.ts", + "version": "0.1.0", + "peerDependencies": { + "muddle": "*" + }, + "dependencies": { + "tailwindcss": "^4.3.3" + } } diff --git a/package.json b/package.json index 54792be..0da52cc 100644 --- a/package.json +++ b/package.json @@ -1,63 +1,63 @@ { - "name": "muddle", - "module": "index.ts", - "type": "module", - "private": true, - "scripts": { - "typecheck": "bun check", - "typegen": "bun run contracts/typegen/generator.ts samples/hello-world", - "dev": "bun run samples/hello-world/index.ts", - "react": "bun run samples/react/index.ts", - "react:build": "bun run samples/react/index.ts --prerender", - "build": "bun run samples/hello-world/index.ts --prerender", - "test": "bun test ./tests", - "lint": "bunx eslint . --ignore-pattern node_modules --ignore-pattern ametrine --ignore-pattern .opencode --ignore-pattern dist --ignore-pattern .git --ignore-pattern .cache --ignore-pattern .next --ignore-pattern .turbo --ignore-pattern .vercel --ignore-pattern .vscode --ignore-pattern .idea", - "format": "bun run cli.ts fmt .", - "muddle": "bun run cli.ts", - "forgejo:modules": "bun scripts/forgejo-modules.ts --dry-run", - "forgejo:push": "bun scripts/forgejo-push.ts" - }, - "bin": { - "muddle": "./cli.ts" - }, - "dependencies": { - "@eslint/js": "^10.0.1", - "lightningcss": "^1.33.0", - "prettier": "^3.9.10", - "sass": "^1.106.1", - "typescript-eslint": "^8.72.0" - }, - "devDependencies": { - "@types/bun": "latest", - "@types/pg": "^8.15.5", - "playwright": "^1.64.1", - "@muddle/astro": "workspace:*", - "@muddle/database": "workspace:*", - "@muddle/latex": "workspace:*", - "@muddle/mdx": "workspace:*", - "@muddle/tailwind": "workspace:*", - "@muddle/prettier-plugin": "workspace:*", - "@muddle/optimize": "workspace:*", - "@muddle/amezing": "workspace:*", - "@muddle/analysis": "workspace:*" - }, - "peerDependencies": { - "@typescript/native": "npm:typescript@^7.0.2", - "typescript": "npm:@typescript/typescript6@^6.0.2" - }, - "workspaces": [ - "modules/*" - ], - "exports": { - ".": "./index.ts", - "./contracts": "./contracts/index.ts", - "./contracts/*": "./contracts/*", - "./metadata": "./core/metadata.ts", - "./markdown": "./views/markdown.ts", - "./views": "./views/api.ts", - "./views/integrations": "./views/integrations.ts", - "./stcss": "./views/css/index.ts", - "./sessions": "./core/sessions.ts", - "./formatter": "./core/format-source.ts" - } + "name": "muddle", + "module": "index.ts", + "type": "module", + "private": true, + "scripts": { + "typecheck": "bun check", + "typegen": "bun run contracts/typegen/generator.ts samples/hello-world", + "dev": "bun run samples/hello-world/index.ts", + "react": "bun run samples/react/index.ts", + "react:build": "bun run samples/react/index.ts --prerender", + "build": "bun run samples/hello-world/index.ts --prerender", + "test": "bun test ./tests", + "lint": "bunx eslint . --ignore-pattern node_modules --ignore-pattern ametrine --ignore-pattern .opencode --ignore-pattern dist --ignore-pattern .git --ignore-pattern .cache --ignore-pattern .next --ignore-pattern .turbo --ignore-pattern .vercel --ignore-pattern .vscode --ignore-pattern .idea", + "format": "bun run cli.ts fmt .", + "muddle": "bun run cli.ts", + "forgejo:modules": "bun scripts/forgejo-modules.ts --dry-run", + "forgejo:push": "bun scripts/forgejo-push.ts" + }, + "bin": { + "muddle": "./cli.ts" + }, + "dependencies": { + "@eslint/js": "^10.0.1", + "lightningcss": "^1.33.0", + "prettier": "^3.9.10", + "sass": "^1.106.1", + "typescript-eslint": "^8.72.0" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/pg": "^8.15.5", + "playwright": "^1.64.1", + "@muddle/astro": "workspace:*", + "@muddle/database": "workspace:*", + "@muddle/latex": "workspace:*", + "@muddle/mdx": "workspace:*", + "@muddle/tailwind": "workspace:*", + "@muddle/prettier-plugin": "workspace:*", + "@muddle/optimize": "workspace:*", + "@muddle/amezing": "workspace:*", + "@muddle/analysis": "workspace:*" + }, + "peerDependencies": { + "@typescript/native": "npm:typescript@^7.0.2", + "typescript": "npm:@typescript/typescript6@^6.0.2" + }, + "workspaces": [ + "modules/*" + ], + "exports": { + ".": "./index.ts", + "./contracts": "./contracts/index.ts", + "./contracts/*": "./contracts/*", + "./metadata": "./core/metadata.ts", + "./markdown": "./views/markdown.ts", + "./views": "./views/api.ts", + "./views/integrations": "./views/integrations.ts", + "./stcss": "./views/css/index.ts", + "./sessions": "./core/sessions.ts", + "./formatter": "./core/format-source.ts" + } } diff --git a/samples/hello-world/_muddle/contracts/orm/tables.ts b/samples/hello-world/_muddle/contracts/orm/tables.ts index 0edbd23..0d634d0 100644 --- a/samples/hello-world/_muddle/contracts/orm/tables.ts +++ b/samples/hello-world/_muddle/contracts/orm/tables.ts @@ -1,11 +1,7 @@ // AUTO-GENERATED by contracts/typegen/generator.ts, do not edit. // Run `bun run typegen ` to update this file. - - -export interface TableRows { - -} +export interface TableRows {} export type TableName = keyof TableRows; diff --git a/samples/hello-world/_muddle/contracts/views.ts b/samples/hello-world/_muddle/contracts/views.ts index 898dae0..19e530e 100644 --- a/samples/hello-world/_muddle/contracts/views.ts +++ b/samples/hello-world/_muddle/contracts/views.ts @@ -4,9 +4,21 @@ import type { Minify } from "./minify.ts"; export interface ViewProps { - "modules/app/views/index": { title: string; nav: {label:string,href:string}[]; posts: {id:string,title:string,excerpt:string}[] }; - "modules/app/views/layout": { title: string; nav: {label:string,href:string}[]; currentPath?: string }; - "modules/app/views/post": { title: string; nav: {label:string,href:string}[]; post: {id:string,title:string,excerpt:string}|null }; + "modules/app/views/index": { + title: string; + nav: { label: string; href: string }[]; + posts: { id: string; title: string; excerpt: string }[]; + }; + "modules/app/views/layout": { + title: string; + nav: { label: string; href: string }[]; + currentPath?: string; + }; + "modules/app/views/post": { + title: string; + nav: { label: string; href: string }[]; + post: { id: string; title: string; excerpt: string } | null; + }; } /** Path of a view of the project. */ diff --git a/samples/hello-world/ct.config.ts b/samples/hello-world/ct.config.ts index 2cc3615..d3d7baa 100644 --- a/samples/hello-world/ct.config.ts +++ b/samples/hello-world/ct.config.ts @@ -19,5 +19,4 @@ export default defineConfig({ routes: ["/", "/posts/test1", "/posts/test2", "/posts/test3"], outDir: "dist", }, - }); diff --git a/samples/hello-world/index.ts b/samples/hello-world/index.ts index 5e81e8a..b139cb4 100644 --- a/samples/hello-world/index.ts +++ b/samples/hello-world/index.ts @@ -20,12 +20,20 @@ const app = await App.create({ integrations: [...resolved.integrations, amezingIntegration()], }); -export async function build(options: { progress?: (event: { current: number; total: number; route: string }) => void } = {}): Promise { +export async function build( + options: { + progress?: (event: { + current: number; + total: number; + route: string; + }) => void; + } = {}, +): Promise { if (!resolved.prerender) return; const results = await prerender(app, { routes: resolved.prerender.routes, outDir: join(resolved.root, resolved.prerender.outDir ?? "dist"), - progress: options.progress + progress: options.progress, }); for (const result of results) { console.log(`muddle: prerendered ${result.route} -> ${result.file}`); diff --git a/samples/react/README.md b/samples/react/README.md index 810bd2e..ec4559d 100644 --- a/samples/react/README.md +++ b/samples/react/README.md @@ -1 +1 @@ -# reactor \ No newline at end of file +# reactor diff --git a/samples/react/_muddle/contracts/orm/tables.ts b/samples/react/_muddle/contracts/orm/tables.ts index 0edbd23..0d634d0 100644 --- a/samples/react/_muddle/contracts/orm/tables.ts +++ b/samples/react/_muddle/contracts/orm/tables.ts @@ -1,11 +1,7 @@ // AUTO-GENERATED by contracts/typegen/generator.ts, do not edit. // Run `bun run typegen ` to update this file. - - -export interface TableRows { - -} +export interface TableRows {} export type TableName = keyof TableRows; diff --git a/samples/react/_muddle/contracts/views.ts b/samples/react/_muddle/contracts/views.ts index 6f4f60c..becfa8f 100644 --- a/samples/react/_muddle/contracts/views.ts +++ b/samples/react/_muddle/contracts/views.ts @@ -4,10 +4,51 @@ import type { Minify } from "./minify.ts"; export interface ViewProps { - "modules/app/views/feature": { title: string; nav: {id:string,label:string,href:string,active?:boolean}[]; albums: {id:string,name:string,cover:string,tracks:{title:string,duration:string}[]}[]; ribbonStyle?: string; label: string; prose: string|null; features: {label:string,href:string}[]; current?: string }; - "modules/app/views/index": { title: string; nav: {id:string,label:string,href:string,active?:boolean}[]; albums: {id:string,name:string,cover:string,tracks:{title:string,duration:string}[]}[]; ribbonStyle?: string; intro: {id:string,label:string,note:string,tint:string}[]; features: {label:string,href:string}[] }; + "modules/app/views/feature": { + title: string; + nav: { id: string; label: string; href: string; active?: boolean }[]; + albums: { + id: string; + name: string; + cover: string; + tracks: { title: string; duration: string }[]; + }[]; + ribbonStyle?: string; + label: string; + prose: string | null; + features: { label: string; href: string }[]; + current?: string; + }; + "modules/app/views/index": { + title: string; + nav: { id: string; label: string; href: string; active?: boolean }[]; + albums: { + id: string; + name: string; + cover: string; + tracks: { title: string; duration: string }[]; + }[]; + ribbonStyle?: string; + intro: { id: string; label: string; note: string; tint: string }[]; + features: { label: string; href: string }[]; + }; "modules/app/views/layout": Record; - "modules/app/views/navbar": { nav: {id:string,label:string,href:string,active?:boolean,detail:string}[]; albums: {id:string,name:string,date:string,cover:string,tracks:{title:string,duration:string}[]}[] }; + "modules/app/views/navbar": { + nav: { + id: string; + label: string; + href: string; + active?: boolean; + detail: string; + }[]; + albums: { + id: string; + name: string; + date: string; + cover: string; + tracks: { title: string; duration: string }[]; + }[]; + }; } /** Path of a view of the project. */ diff --git a/samples/react/ct.config.ts b/samples/react/ct.config.ts index 8b76c91..b8bf4a7 100644 --- a/samples/react/ct.config.ts +++ b/samples/react/ct.config.ts @@ -11,7 +11,13 @@ export default defineConfig({ integrations: [stcssIntegration({ dir: "styles" })], prerender: { - routes: ["/", "/features/interpolate", "/features/refer", "/features/predict", "/features/route"], + routes: [ + "/", + "/features/interpolate", + "/features/refer", + "/features/predict", + "/features/route", + ], outDir: "dist", }, }); diff --git a/samples/react/modules/app/musicbrainz.ts b/samples/react/modules/app/musicbrainz.ts index 8bc391e..24c0200 100644 --- a/samples/react/modules/app/musicbrainz.ts +++ b/samples/react/modules/app/musicbrainz.ts @@ -5,7 +5,8 @@ const BASE = "https://musicbrainz.org/ws/2"; const ARTIST = "4ae36ade-1798-48c4-b06b-cc68b7d3d83f"; -const USER_AGENT = "muddle-react-sample/0.1 (discography demo; devnull@crit.rip)"; +const USER_AGENT = + "muddle-react-sample/0.1 (discography demo; devnull@crit.rip)"; const TTL = 1000 * 60 * 30; export interface Track { @@ -53,25 +54,31 @@ async function mbJson(path: string, attempt = 0): Promise { } throw new Error(`musicbrainz: ${response.status} ${path}`); } - return await response.json() as T; + return (await response.json()) as T; } function isStudioAlbum(entry: ReleaseGroup): boolean { const title = entry.title.toLowerCase(); - return !/(live|remix|commentary|edition|remastered|deluxe|second sky|secret sky|matt dipirro)/.test(title); + return !/(live|remix|commentary|edition|remastered|deluxe|second sky|secret sky|matt dipirro)/.test( + title, + ); } async function tracksFor(groupId: string): Promise { - const releases = await mbJson<{ releases?: { id: string; country?: string }[] }>( - `/release-group/${groupId}?inc=releases&fmt=json`, - ); + const releases = await mbJson<{ + releases?: { id: string; country?: string }[]; + }>(`/release-group/${groupId}?inc=releases&fmt=json`); // prefer a digital or worldwide edition: sort by shortest country code chain - const candidate = (releases.releases ?? []).slice() + const candidate = (releases.releases ?? []) + .slice() .sort((a, b) => (a.country?.length ?? 9) - (b.country?.length ?? 9))[0]; if (!candidate) return []; - const release = await mbJson(`/release/${candidate.id}?inc=recordings+media&fmt=json`); - const media = (release.media ?? []).find((medium) => (medium.tracks?.length ?? 0) > 0) - ?? release.media?.[0]; + const release = await mbJson( + `/release/${candidate.id}?inc=recordings+media&fmt=json`, + ); + const media = + (release.media ?? []).find((medium) => (medium.tracks?.length ?? 0) > 0) ?? + release.media?.[0]; return (media?.tracks ?? []).map((track) => ({ title: track.title, duration: formatDuration(track.length), @@ -85,10 +92,15 @@ export async function discography(): Promise { `/artist/${ARTIST}?inc=release-groups&fmt=json`, ); const groups = (data["release-groups"] ?? []) - .filter((entry) => entry["primary-type"] === "Album" - && (entry["secondary-types"]?.length ?? 0) === 0 - && isStudioAlbum(entry)) - .sort((a, b) => a["first-release-date"].localeCompare(b["first-release-date"])) + .filter( + (entry) => + entry["primary-type"] === "Album" && + (entry["secondary-types"]?.length ?? 0) === 0 && + isStudioAlbum(entry), + ) + .sort((a, b) => + a["first-release-date"].localeCompare(b["first-release-date"]), + ) .slice(0, 4); const albums: Album[] = []; for (const [index, group] of groups.entries()) { @@ -125,7 +137,8 @@ function offlineDiscography(): Album[] { id: "6df00711-624a-4960-9f15-39baf9332cbb", name: "Worlds", date: "2014-08-08", - cover: "https://coverartarchive.org/release-group/6df00711-624a-4960-9f15-39baf9332cbb/front-250", + cover: + "https://coverartarchive.org/release-group/6df00711-624a-4960-9f15-39baf9332cbb/front-250", tracks: [ { title: "Divinity", duration: "6:09" }, { title: "Sad Machine", duration: "5:54" }, @@ -144,7 +157,8 @@ function offlineDiscography(): Album[] { id: "e2cc165d-b935-4e7c-8728-77cf01ab21ed", name: "Nurture", date: "2021-04-23", - cover: "https://coverartarchive.org/release-group/e2cc165d-b935-4e7c-8728-77cf01ab21ed/front-250", + cover: + "https://coverartarchive.org/release-group/e2cc165d-b935-4e7c-8728-77cf01ab21ed/front-250", tracks: [ { title: "Lifelike", duration: "0:43" }, { title: "Look at the Sky", duration: "4:02" }, @@ -166,7 +180,8 @@ function offlineDiscography(): Album[] { id: "87964290-66b9-4fd9-bea3-1bfe9de92862", name: "SMILE! :D", date: "2024-07-26", - cover: "https://coverartarchive.org/release-group/87964290-66b9-4fd9-bea3-1bfe9de92862/front-250", + cover: + "https://coverartarchive.org/release-group/87964290-66b9-4fd9-bea3-1bfe9de92862/front-250", tracks: [ { title: "Knock Yourself Out XD", duration: "3:13" }, { title: "Cheerleader", duration: "3:01" }, diff --git a/samples/react/modules/app/registry.ts b/samples/react/modules/app/registry.ts index f84150d..c126c77 100644 --- a/samples/react/modules/app/registry.ts +++ b/samples/react/modules/app/registry.ts @@ -19,13 +19,20 @@ export interface Feature { const ROOT = import.meta.dir; -const DEFINITIONS: Feature[] = [ -]; +const DEFINITIONS: Feature[] = []; -export const FEATURES: Feature[] = DEFINITIONS.map((feature) => ({ ...feature, prose: (prose = feature.prose) => prose() })); +export const FEATURES: Feature[] = DEFINITIONS.map((feature) => ({ + ...feature, + prose: (prose = feature.prose) => prose(), +})); export const NAV: NavItem[] = [ - { id: "home", label: "Home", href: "/", detail: "Porter discography powered by muddle" }, + { + id: "home", + label: "Home", + href: "/", + detail: "Porter discography powered by muddle", + }, ...DEFINITIONS.map((feature) => ({ id: feature.slug, label: feature.label, @@ -75,4 +82,3 @@ export const INTRO: IntroTopic[] = [ tint: "amber", }, ]; - diff --git a/samples/shadcn-porter/dist.previous/_muddle/styles.css b/samples/shadcn-porter/dist.previous/_muddle/styles.css index 23bec7a..b253e7f 100644 --- a/samples/shadcn-porter/dist.previous/_muddle/styles.css +++ b/samples/shadcn-porter/dist.previous/_muddle/styles.css @@ -24,17 +24,19 @@ body { margin: 0 auto; padding: clamp(2rem, 9vw, 7rem) 0; } -.porter__intro, .porter__item { +.porter__intro, +.porter__item { padding: clamp(1.25rem, 4vw, 3rem); } .porter__intro h1 { max-width: 44rem; margin: 0; font-size: clamp(2.75rem, 7vw, 5.75rem); - line-height: .95; - letter-spacing: -.07em; + line-height: 0.95; + letter-spacing: -0.07em; } -.porter__intro p, .porter__item p { +.porter__intro p, +.porter__item p { max-width: 39rem; color: var(--muted-foreground); line-height: 1.65; @@ -55,7 +57,7 @@ body { font-size: 1.35rem; } .rounded-xl { - border-radius: .75rem; + border-radius: 0.75rem; } .border { border: 1px solid var(--border); @@ -67,7 +69,9 @@ body { color: var(--card-foreground); } .shadow { - box-shadow: 0 1px 3px rgb(0 0 0 / .1), 0 1px 2px rgb(0 0 0 / .06); + box-shadow: + 0 1px 3px rgb(0 0 0 / 0.1), + 0 1px 2px rgb(0 0 0 / 0.06); } .muddle-react { display: inline-flex; @@ -75,19 +79,19 @@ body { align-items: center; justify-content: center; border: 0; - border-radius: .375rem; + border-radius: 0.375rem; background: var(--primary); color: var(--primary-foreground); - padding: .55rem .9rem; + padding: 0.55rem 0.9rem; font: inherit; font-weight: 500; cursor: pointer; } .muddle-react:hover { - opacity: .9; + opacity: 0.9; } @media (max-width: 48rem) { .porter__grid { grid-template-columns: 1fr; } -} \ No newline at end of file +} diff --git a/scripts/forgejo-modules.ts b/scripts/forgejo-modules.ts index f3f9ea1..5a106e1 100644 --- a/scripts/forgejo-modules.ts +++ b/scripts/forgejo-modules.ts @@ -25,9 +25,7 @@ const dryRun = process.argv.includes("--dry-run"); const token = process.env.MUDDLE_FORGEJO_TOKEN; if (!dryRun && !token) { - throw new Error( - "Set MUDDLE_FORGEJO_TOKEN or use --dry-run.", - ); + throw new Error("Set MUDDLE_FORGEJO_TOKEN or use --dry-run."); } async function forgejo( @@ -53,19 +51,17 @@ async function ensureWorkspaceModule(name: string): Promise { if (!existsSync(manifest)) { await writeFile( manifest, - `${ - JSON.stringify( - { - name: `@muddle/${name}`, - version: "0.1.0", - type: "module", - exports: "./index.ts", - peerDependencies: { muddle: "*" }, - }, - null, - 2, - ) - }\n`, + `${JSON.stringify( + { + name: `@muddle/${name}`, + version: "0.1.0", + type: "module", + exports: "./index.ts", + peerDependencies: { muddle: "*" }, + }, + null, + 2, + )}\n`, ); await writeFile(join(dir, "index.ts"), "export {};\n"); } @@ -105,9 +101,7 @@ for (const name of moduleNames) { await ensureRemoteRepository(name); console.log( `${ - dryRun - ? "would provision" - : "provisioned" + dryRun ? "would provision" : "provisioned" } @muddle/${name} at ${api}/${owner}/${name}`, ); } diff --git a/scripts/forgejo-push.ts b/scripts/forgejo-push.ts index d8fb225..4e953c9 100644 --- a/scripts/forgejo-push.ts +++ b/scripts/forgejo-push.ts @@ -27,9 +27,7 @@ const selected = requested.length > 0 ? requested : modules; const token = process.env.MUDDLE_FORGEJO_TOKEN; if (push && !token) { - throw new Error( - "Set MUDDLE_FORGEJO_TOKEN before using --push.", - ); + throw new Error("Set MUDDLE_FORGEJO_TOKEN before using --push."); } for (const name of selected) { if (!modules.includes(name)) { @@ -88,22 +86,19 @@ try { await run(["git", "add", "--", prefix]); await run(["git", "commit", "-m", message, "--", prefix]); } - const branch = await run([ - "git", - "subtree", - "split", - "--prefix", - prefix, - ]); - await run([ - "git", - "push", - `${api}/${owner}/${name}.git`, - `${branch}:refs/heads/main`, - ], { - GIT_ASKPASS: askPass, - GIT_TERMINAL_PROMPT: "0", - }); + const branch = await run(["git", "subtree", "split", "--prefix", prefix]); + await run( + [ + "git", + "push", + `${api}/${owner}/${name}.git`, + `${branch}:refs/heads/main`, + ], + { + GIT_ASKPASS: askPass, + GIT_TERMINAL_PROMPT: "0", + }, + ); console.log(`pushed @muddle/${name}`); } } finally { diff --git a/tests/amezing.test.ts b/tests/amezing.test.ts index 1c4629f..5f61187 100644 --- a/tests/amezing.test.ts +++ b/tests/amezing.test.ts @@ -2,20 +2,34 @@ import { expect, test } from "bun:test"; import { App } from "../index.ts"; import { resolveConfig } from "../core/config.ts"; import { createViews } from "../views/api.ts"; -import { amezingIntegration, amezingStyles, accentStyle } from "../modules/amezing/index.ts"; +import { + amezingIntegration, + amezingStyles, + accentStyle, +} from "../modules/amezing/index.ts"; function createAmezingApp() { const views = createViews({ viewsDir: process.cwd() }); - return App.create({ config: resolveConfig(), views, integrations: [amezingIntegration()] }); + return App.create({ + config: resolveConfig(), + views, + integrations: [amezingIntegration()], + }); } test("amezing integration mounts the theme components and assets", async () => { const app = await createAmezingApp(); - const response = await app.fetch(new Request("https://example.test/_muddle/amezing/closable.js")); + const response = await app.fetch( + new Request("https://example.test/_muddle/amezing/closable.js"), + ); expect(response.status).toBe(200); - const font = await app.fetch(new Request("https://example.test/fonts/commissioner.woff2")); + const font = await app.fetch( + new Request("https://example.test/fonts/commissioner.woff2"), + ); expect(font.status).toBe(200); - const styles = await app.fetch(new Request("https://example.test/_muddle/amezing.css")); + const styles = await app.fetch( + new Request("https://example.test/_muddle/amezing.css"), + ); expect((await styles.text()).length).toBeGreaterThan(0); }); diff --git a/tests/analysis.test.ts b/tests/analysis.test.ts index d1b6f67..fda34d8 100644 --- a/tests/analysis.test.ts +++ b/tests/analysis.test.ts @@ -1,34 +1,84 @@ import { expect, test } from "bun:test"; import { analyzeHtml } from "../modules/analysis/index.ts"; import { App } from "../core/app.ts"; -import { terserIntegration, htmlMinifyIntegration, imageOptimizationIntegration } from "../integrations/optimize.ts"; +import { + terserIntegration, + htmlMinifyIntegration, + imageOptimizationIntegration, +} from "../integrations/optimize.ts"; test("accessibility scoring reports semantics, alpha contrast and visual-effect uncertainty", async () => { - const html = 'Audit

Low contrast

Filtered text

'; - const [report] = await analyzeHtml(html, "accessible", { viewports: [{ width: 390, height: 844 }] }); - expect(report!.findings.map((finding) => finding.rule)).toContain("control-name"); - expect(report!.findings.map((finding) => finding.rule)).toContain("text-contrast"); - expect(report!.findings.map((finding) => finding.rule)).toContain("visibility-effects"); + const html = + 'Audit

Low contrast

Filtered text

'; + const [report] = await analyzeHtml(html, "accessible", { + viewports: [{ width: 390, height: 844 }], + }); + expect(report!.findings.map((finding) => finding.rule)).toContain( + "control-name", + ); + expect(report!.findings.map((finding) => finding.rule)).toContain( + "text-contrast", + ); + expect(report!.findings.map((finding) => finding.rule)).toContain( + "visibility-effects", + ); expect(report!.score).toBeLessThan(100); - expect(report!.findings.find((finding) => finding.rule === "visibility-effects")!.severity).toBe("review"); + expect( + report!.findings.find((finding) => finding.rule === "visibility-effects")! + .severity, + ).toBe("review"); }); test("overflow analysis distinguishes intentional scrolling and clipped content", async () => { - const html = '
Wide
Scrollable
Clipped
'; + const html = + '
Wide
Scrollable
Clipped
'; const reports = await analyzeHtml(html, "overflow"); - expect(reports[0]!.findings.filter((finding) => finding.rule === "viewport-overflow")).toHaveLength(0); - expect(reports[1]!.findings.filter((finding) => finding.rule === "viewport-overflow")).toHaveLength(1); - expect(reports[1]!.findings.map((finding) => finding.rule)).toContain("clipping"); + expect( + reports[0]!.findings.filter( + (finding) => finding.rule === "viewport-overflow", + ), + ).toHaveLength(0); + expect( + reports[1]!.findings.filter( + (finding) => finding.rule === "viewport-overflow", + ), + ).toHaveLength(1); + expect(reports[1]!.findings.map((finding) => finding.rule)).toContain( + "clipping", + ); }); test("async optimization runs on staged assets and preserves binary image input", async () => { let bytes: number[] = []; - const app = await App.create({ integrations: [terserIntegration(), htmlMinifyIntegration(), imageOptimizationIntegration({ async optimize(input) { bytes = [...input]; return new Uint8Array([9, 8]); } })] }); - const js = await app.transformAsset({ path: "app.js", contentType: "text/javascript", content: 'const url = "https://example.test"; console.log(url);' }); + const app = await App.create({ + integrations: [ + terserIntegration(), + htmlMinifyIntegration(), + imageOptimizationIntegration({ + async optimize(input) { + bytes = [...input]; + return new Uint8Array([9, 8]); + }, + }), + ], + }); + const js = await app.transformAsset({ + path: "app.js", + contentType: "text/javascript", + content: 'const url = "https://example.test"; console.log(url);', + }); expect(String(js.content)).toContain("https://example.test"); - const html = await app.transformAsset({ path: "index.html", contentType: "text/html", content: '

Text

' }); + const html = await app.transformAsset({ + path: "index.html", + contentType: "text/html", + content: "

Text

", + }); expect(String(html.content)).not.toContain(" "); - const image = await app.transformAsset({ path: "image.png", contentType: "image/png", content: new Uint8Array([0, 255, 128]) }); + const image = await app.transformAsset({ + path: "image.png", + contentType: "image/png", + content: new Uint8Array([0, 255, 128]), + }); expect(bytes).toEqual([0, 255, 128]); - expect([...image.content as Uint8Array]).toEqual([9, 8]); + expect([...(image.content as Uint8Array)]).toEqual([9, 8]); }); diff --git a/tests/backport.test.ts b/tests/backport.test.ts index 480d7ae..2d0cd58 100644 --- a/tests/backport.test.ts +++ b/tests/backport.test.ts @@ -2,7 +2,15 @@ import { describe, expect, test } from "bun:test"; import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { analyzeReactComponent, astroToSpug, backportProject, cssToStcss, htmlToSpug, reactToSpug, zolaToSpug } from "../core/backport.ts"; +import { + analyzeReactComponent, + astroToSpug, + backportProject, + cssToStcss, + htmlToSpug, + reactToSpug, + zolaToSpug, +} from "../core/backport.ts"; import { shadcnIntegration } from "../integrations/shadcn.ts"; import { sassLoader } from "../views/css/sass.ts"; import { TemplateEngine } from "../views/engine/runtime.ts"; @@ -15,14 +23,20 @@ describe("backport", () => { }); test("turns HTML structures into indented SPUG", () => { - const result = htmlToSpug('

Hello

'); + const result = htmlToSpug( + '

Hello

', + ); expect(result).toContain("main.page"); expect(result).toContain(" h1"); expect(result).toContain('img(src="hero.png")'); }); test("ports Astro layouts, slots and imported components", () => { - const page = astroToSpug(`---\nimport Layout from "../layouts/Layout.astro";\nimport Card from "../components/Card.astro";\n---\n

Body

`, "/site/src/pages/index.astro", "/site/src/pages"); + const page = astroToSpug( + `---\nimport Layout from "../layouts/Layout.astro";\nimport Card from "../components/Card.astro";\n---\n

Body

`, + "/site/src/pages/index.astro", + "/site/src/pages", + ); expect(page).toContain("extends layouts/Layout.spug"); expect(page).toContain("block content"); expect(page).toContain("component card"); @@ -31,7 +45,9 @@ describe("backport", () => { }); test("ports Zola inheritance, partials and template expressions", () => { - const page = zolaToSpug(`{% extends "base.html" %}\n{% block content %}

{{ page.title }}

{% include "partials/nav.html" %}{% endblock %}`); + const page = zolaToSpug( + `{% extends "base.html" %}\n{% block content %}

{{ page.title }}

{% include "partials/nav.html" %}{% endblock %}`, + ); expect(page).toContain("extends ./base.spug"); expect(page).toContain("block content"); expect(page).toContain("include ./partials/nav.spug"); @@ -39,16 +55,22 @@ describe("backport", () => { }); test("ports React JSX components through the TypeScript AST", () => { - const component = reactToSpug('import { Button } from "./button"; export function Card({ title }) { return

{title}

; }'); - expect(component).toContain("section(class=\"card\")"); + const component = reactToSpug( + 'import { Button } from "./button"; export function Card({ title }) { return

{title}

; }', + ); + expect(component).toContain('section(class="card")'); expect(component).toContain("= title"); expect(component).toContain("component button"); expect(component).toContain('variant: "outline"'); }); test("analyzes hooks and lowers static state into a javascript block", () => { - const source = 'function Toggle() { const [open] = useState(false); useEffect(() => {}, []); return ; }'; - expect(analyzeReactComponent(source).hooks).toEqual(["useEffect", "useState"]); + const source = + "function Toggle() { const [open] = useState(false); useEffect(() => {}, []); return ; }"; + expect(analyzeReactComponent(source).hooks).toEqual([ + "useEffect", + "useState", + ]); expect(reactToSpug(source)).toContain("javascript!"); expect(reactToSpug(source)).toContain("const open = false;"); }); @@ -61,44 +83,96 @@ describe("backport", () => { await mkdir(join(root, "src/content/posts"), { recursive: true }); await mkdir(join(root, "src/layouts"), { recursive: true }); await mkdir(join(root, "src/components"), { recursive: true }); - await Bun.write(join(root, "src/pages/index.astro"), "

Hi

"); - await Bun.write(join(root, "src/pages/about.astro"), "---\nimport Shell from '../layouts/Shell.astro';\n---\n

About

"); - await Bun.write(join(root, "src/layouts/Shell.astro"), ""); - await Bun.write(join(root, "src/components/Card.astro"), "
"); + await Bun.write( + join(root, "src/pages/index.astro"), + "

Hi

", + ); + await Bun.write( + join(root, "src/pages/about.astro"), + "---\nimport Shell from '../layouts/Shell.astro';\n---\n

About

", + ); + await Bun.write( + join(root, "src/layouts/Shell.astro"), + "", + ); + await Bun.write( + join(root, "src/components/Card.astro"), + "
", + ); await Bun.write(join(root, "src/styles/site.css"), ".page { color: red; }"); - await Bun.write(join(root, "src/content/posts/hello.md"), "---\ntitle: Hello\n---\n\nBody"); + await Bun.write( + join(root, "src/content/posts/hello.md"), + "---\ntitle: Hello\n---\n\nBody", + ); const destination = join(root, "muddle"); const result = await backportProject(root, destination); expect(result.source).toBe("astro"); - expect(await readFile(join(destination, "modules/app/views/index.spug"), "utf8")).toContain("main"); - expect(await readFile(join(destination, "styles/site.stcss"), "utf8")).toContain(".page"); - expect(await readFile(join(destination, "content/posts/hello.md"), "utf8")).toContain("title: Hello"); - expect(await readFile(join(destination, "ct.config.ts"), "utf8")).toContain('name: "posts"'); - const engine = new TemplateEngine({ viewsDir: join(destination, "modules/app/views") }); + expect( + await readFile(join(destination, "modules/app/views/index.spug"), "utf8"), + ).toContain("main"); + expect( + await readFile(join(destination, "styles/site.stcss"), "utf8"), + ).toContain(".page"); + expect( + await readFile(join(destination, "content/posts/hello.md"), "utf8"), + ).toContain("title: Hello"); + expect(await readFile(join(destination, "ct.config.ts"), "utf8")).toContain( + 'name: "posts"', + ); + const engine = new TemplateEngine({ + viewsDir: join(destination, "modules/app/views"), + }); expect(await engine.render("index.spug")).toContain("

Hi

"); - expect(await engine.render("about.spug")).toContain("

About

"); + expect(await engine.render("about.spug")).toContain( + "

About

", + ); }); test("carries Zola configuration and partial templates into the Muddle project", async () => { const root = await mkdtemp(join(tmpdir(), "muddle-zola-")); await mkdir(join(root, "templates/partials"), { recursive: true }); await mkdir(join(root, "content/posts"), { recursive: true }); - await Bun.write(join(root, "config.toml"), 'title = "Zola notes"\nbase_url = "https://notes.example"'); - await Bun.write(join(root, "templates/index.html"), '{% extends "base.html" %}{% block content %}

{{ config.title }}

{% endblock %}'); - await Bun.write(join(root, "templates/partials/nav.html"), ""); - await Bun.write(join(root, "content/posts/first.md"), "---\ntitle: First\n---\n\nPost"); + await Bun.write( + join(root, "config.toml"), + 'title = "Zola notes"\nbase_url = "https://notes.example"', + ); + await Bun.write( + join(root, "templates/index.html"), + '{% extends "base.html" %}{% block content %}

{{ config.title }}

{% endblock %}', + ); + await Bun.write( + join(root, "templates/partials/nav.html"), + "", + ); + await Bun.write( + join(root, "content/posts/first.md"), + "---\ntitle: First\n---\n\nPost", + ); const destination = join(root, "muddle"); await backportProject(root, destination); - expect(await readFile(join(destination, "modules/app/site.config.ts"), "utf8")).toContain("Zola notes"); - expect(await readFile(join(destination, "modules/app/views/partials/nav.spug"), "utf8")).toContain("nav"); - expect(await readFile(join(destination, "modules/app/views/index.spug"), "utf8")).toContain("extends ./base.spug"); + expect( + await readFile(join(destination, "modules/app/site.config.ts"), "utf8"), + ).toContain("Zola notes"); + expect( + await readFile( + join(destination, "modules/app/views/partials/nav.spug"), + "utf8", + ), + ).toContain("nav"); + expect( + await readFile(join(destination, "modules/app/views/index.spug"), "utf8"), + ).toContain("extends ./base.spug"); }); test("compiles Sass before handing CSS to STCSS", async () => { const root = await mkdtemp(join(tmpdir(), "muddle-sass-")); const path = join(root, "site.scss"); await writeFile(path, "$ink: #123456; .card { color: $ink; }"); - const loaded = await sassLoader().load({ path, relativePath: "styles/site.scss", source: await readFile(path, "utf8") }); + const loaded = await sassLoader().load({ + path, + relativePath: "styles/site.scss", + source: await readFile(path, "utf8"), + }); expect(loaded.content).toContain("color: #123456;"); }); @@ -106,13 +180,33 @@ describe("backport", () => { const root = await mkdtemp(join(tmpdir(), "muddle-shadcn-")); const integration = shadcnIntegration({ components: ["button"], - fetch: async () => new Response(JSON.stringify({ files: [ - { path: "button.tsx", content: "export function Button() { return ; }" }, - { path: "button.css", content: ".btn { color: red; }" }, - ] })), + fetch: async () => + new Response( + JSON.stringify({ + files: [ + { + path: "button.tsx", + content: + 'export function Button() { return ; }', + }, + { path: "button.css", content: ".btn { color: red; }" }, + ], + }), + ), }); - await integration.setup?.({ config: { root } as any, route() {}, loader() {} }); - expect(await readFile(join(root, "modules/app/views/components/button.spug"), "utf8")).toContain("button"); - expect(await readFile(join(root, "styles/shadcn/button.stcss"), "utf8")).toContain(".btn"); + await integration.setup?.({ + config: { root } as any, + route() {}, + loader() {}, + }); + expect( + await readFile( + join(root, "modules/app/views/components/button.spug"), + "utf8", + ), + ).toContain("button"); + expect( + await readFile(join(root, "styles/shadcn/button.stcss"), "utf8"), + ).toContain(".btn"); }); }); diff --git a/tests/core.test.ts b/tests/core.test.ts index 3d7b7f9..7947eb8 100644 --- a/tests/core.test.ts +++ b/tests/core.test.ts @@ -49,10 +49,7 @@ describe("container", () => { } const container = new Container(); - container.registerFactory( - AsyncService, - async () => new AsyncService(), - ); + container.registerFactory(AsyncService, async () => new AsyncService()); const service = await container.resolveAsync(AsyncService); expect(await service.load()).toBe("loaded"); diff --git a/tests/css.test.ts b/tests/css.test.ts index 8dae563..b007a23 100644 --- a/tests/css.test.ts +++ b/tests/css.test.ts @@ -55,13 +55,24 @@ $ink #101828 test("runs application-defined STCSS expansions from a contextual registry", () => { const hooks = defineHooks((hook) => { - hook.stcss.value("double", ({ inner, resolve }) => `calc(${resolve(inner)} * 2)`); - hook.stcss.declaration(({ selector, declaration }) => declaration.property === "outline-token" - ? [`${selector}::before { content: "hook"; }`] - : undefined); - hook.stcss.atRule("paint", ({ selector, params }) => `${selector} { color: ${params}; }`); + hook.stcss.value( + "double", + ({ inner, resolve }) => `calc(${resolve(inner)} * 2)`, + ); + hook.stcss.declaration(({ selector, declaration }) => + declaration.property === "outline-token" + ? [`${selector}::before { content: "hook"; }`] + : undefined, + ); + hook.stcss.atRule( + "paint", + ({ selector, params }) => `${selector} { color: ${params}; }`, + ); }); - const css = compileCss(`.card { width: double(12px); outline-token: yes; @paint rebeccapurple; }`, { hooks }); + const css = compileCss( + `.card { width: double(12px); outline-token: yes; @paint rebeccapurple; }`, + { hooks }, + ); expect(css).toContain("width: calc(12px * 2);"); expect(css).toContain('.card::before { content: "hook"; }'); expect(css).toContain(".card { color: rebeccapurple; }"); @@ -81,7 +92,9 @@ $ink #101828 expect(css).toContain("left: 50%;"); expect(css).toContain("top: 50%;"); expect(css).toContain("transform: translate(-50%, -50%);"); - expect(css).toContain("margin-left: calc(calc(50% - (30rem / 2)) + var(--muddle-pad, 1rem));"); + expect(css).toContain( + "margin-left: calc(calc(50% - (30rem / 2)) + var(--muddle-pad, 1rem));", + ); expect(css).toContain("padding: calc(var(--muddle-pad, 1rem) * 2);"); expect(css).toContain("position: absolute;"); expect(css).toContain("transform: translate(-50%, -50%) scale(.96);"); @@ -89,9 +102,14 @@ $ink #101828 test("registers application calculation hooks", () => { const hooks = defineHooks((hook) => { - hook.stcss.calculation("third", ({ args, resolve }) => `calc(${resolve(args ?? "100%")} / 3)`); + hook.stcss.calculation( + "third", + ({ args, resolve }) => `calc(${resolve(args ?? "100%")} / 3)`, + ); }); - expect(compileCss(".column { width: $third(90%); }", { hooks })).toContain("width: calc(90% / 3);"); + expect(compileCss(".column { width: $third(90%); }", { hooks })).toContain( + "width: calc(90% / 3);", + ); }); test("resolves the variables", () => { @@ -192,14 +210,18 @@ $space: 1; } nav li:when(nav @hello1:clicked) { opacity: 1; } `); - expect(css).toContain('nav:has([data-muddle-anchor="hello0"]:active) li:nth-child(1)'); + expect(css).toContain( + 'nav:has([data-muddle-anchor="hello0"]:active) li:nth-child(1)', + ); expect(css).toContain("var(--muddle-relate-hello1-x)"); expect(css).toContain("var(--muddle-relate-hello1-width)"); expect(css).toContain('nav:has([data-muddle-anchor="hello1"]:active) li'); }); test("resolves $refer as a relation value", () => { - const css = compileCss(`.tooltip { left: $refer[left](@trigger); width: $refer[width](@trigger); }`); + const css = compileCss( + `.tooltip { left: $refer[left](@trigger); width: $refer[width](@trigger); }`, + ); expect(css).toContain("var(--muddle-relate-trigger-x)"); expect(css).toContain("var(--muddle-relate-trigger-width)"); }); @@ -225,9 +247,13 @@ nav li:when(nav @hello1:clicked) { opacity: 1; } border-radius: concentric var(--outer, 12px); } `); - expect(css).toContain(".panel > * {\n\tborder-radius: calc((0.75rem) - (1rem) - (2px));"); + expect(css).toContain( + ".panel > * {\n\tborder-radius: calc((0.75rem) - (1rem) - (2px));", + ); expect(css).toContain(".panel-default > * {\n\tborder-radius: inherit;"); - expect(css).toContain(".blind > * {\n\tborder-radius: calc((var(--outer, 12px)) - (12px) - (var(--bw, 1px)));"); + expect(css).toContain( + ".blind > * {\n\tborder-radius: calc((var(--outer, 12px)) - (12px) - (var(--bw, 1px)));", + ); }); test("expands freeform gradients into radial gradient stacks", () => { @@ -236,22 +262,35 @@ nav li:when(nav @hello1:clicked) { opacity: 1; } background-image: freeform-gradient(hsl(270 50% 60%) 10% 50%, red 70% 40%, rgb(0 128 0) 40% 80% 30%); } `); - expect(css).toContain("radial-gradient(circle farthest-side at 10% 50%, hsl(270 50% 60%), transparent)"); - expect(css).toContain("radial-gradient(circle farthest-side at 70% 40%, red, transparent)"); - expect(css).toContain("radial-gradient(circle at 40% 80%, rgb(0 128 0), rgb(0 128 0) calc(30% * 0.97), transparent 30%)"); - expect(() => compileCss(".x { background-image: freeform-gradient(red 1% 2% 3% 4%); }")).toThrow(); + expect(css).toContain( + "radial-gradient(circle farthest-side at 10% 50%, hsl(270 50% 60%), transparent)", + ); + expect(css).toContain( + "radial-gradient(circle farthest-side at 70% 40%, red, transparent)", + ); + expect(css).toContain( + "radial-gradient(circle at 40% 80%, rgb(0 128 0), rgb(0 128 0) calc(30% * 0.97), transparent 30%)", + ); + expect(() => + compileCss( + ".x { background-image: freeform-gradient(red 1% 2% 3% 4%); }", + ), + ).toThrow(); }); }); describe("stcss @measure", () => { test("records prediction targets and anchors them as relations", () => { const measured: string[] = []; - const css = compileCss(` + const css = compileCss( + ` .ribbon { @measure; } @measure @trigger, .card, panel; -`, { measured }); +`, + { measured }, + ); expect(css).toContain("/* muddle-predict:"); expect(measured).toEqual([ ".ribbon", diff --git a/tests/d1.test.ts b/tests/d1.test.ts index 3b3c6c9..8d46bf2 100644 --- a/tests/d1.test.ts +++ b/tests/d1.test.ts @@ -14,9 +14,7 @@ const server = Bun.serve({ success: true, result: [ { - results: [ - { id: "1", title: "First", position: 1 }, - ], + results: [{ id: "1", title: "First", position: 1 }], success: true, meta: { changes: 1 }, }, diff --git a/tests/hello-world.test.ts b/tests/hello-world.test.ts index c4506d0..4e147d7 100644 --- a/tests/hello-world.test.ts +++ b/tests/hello-world.test.ts @@ -32,5 +32,4 @@ describe("hello world sample", () => { const response = await get("/nope"); expect(response.status).toBe(404); }); - }); diff --git a/tests/hooks.test.ts b/tests/hooks.test.ts index e4f74eb..c365480 100644 --- a/tests/hooks.test.ts +++ b/tests/hooks.test.ts @@ -7,10 +7,20 @@ import { defineHooks } from "../views/hooks.ts"; test("Spug hook registry composes render expansions with route context", async () => { const hooks = defineHooks((hook) => { - hook.spug("stamp", ({ html, path, locals }) => `${html}`); + hook.spug( + "stamp", + ({ html, path, locals }) => + `${html}`, + ); }); - const html = await hooks.renderSpug({ html: "
hello
", path: "/views/index.spug", locals: { title: "Muddle" } }); - expect(html).toContain(''); + const html = await hooks.renderSpug({ + html: "
hello
", + path: "/views/index.spug", + locals: { title: "Muddle" }, + }); + expect(html).toContain( + '', + ); }); test("Views applies Spug hooks after template composition", async () => { @@ -21,7 +31,9 @@ test("Views applies Spug hooks after template composition", async () => { hook.spug("mark", ({ html }) => `${html}`); }); const views = createViews({ root, viewsDir: root, hooks }); - expect(await views.render("index.spug", { title: "Muddle" } as never)).toContain(""); + expect( + await views.render("index.spug", { title: "Muddle" } as never), + ).toContain(""); } finally { await rm(root, { recursive: true, force: true }); } diff --git a/tests/integrations.test.ts b/tests/integrations.test.ts index 6b030f6..ac3b8ef 100644 --- a/tests/integrations.test.ts +++ b/tests/integrations.test.ts @@ -39,7 +39,7 @@ describe("react integration", () => { test("renders the markup of the mouse tracker", () => { const markup = mouseTrackerMarkup({ columns: 2, rows: 2 }); - expect((markup.match(/mouse-tracker__cell/g) ?? [])).toHaveLength(4); + expect(markup.match(/mouse-tracker__cell/g) ?? []).toHaveLength(4); expect(markup).toContain('class="mouse-tracker"'); expect(markup).toContain('data-muddle-capture="false"'); }); @@ -76,8 +76,6 @@ describe("react integration", () => { loader: () => undefined, }); - expect(routes).toEqual([ - { method: "GET", path: "/_muddle/react.css" }, - ]); + expect(routes).toEqual([{ method: "GET", path: "/_muddle/react.css" }]); }); }); diff --git a/tests/markdown.test.ts b/tests/markdown.test.ts index 2337bb9..df56810 100644 --- a/tests/markdown.test.ts +++ b/tests/markdown.test.ts @@ -4,7 +4,11 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { ContentRegistry } from "../core/content.ts"; import { resolveConfig } from "../core/config.ts"; -import { markdownLoader, parseFrontmatter, renderMarkdown } from "../views/markdown.ts"; +import { + markdownLoader, + parseFrontmatter, + renderMarkdown, +} from "../views/markdown.ts"; /** * Tests of the markdown loader and of the content collections. @@ -23,7 +27,9 @@ describe("markdown", () => { }); test("renders the inline markup", () => { - expect(renderMarkdown("**bold** and *italic*")).toContain("bold"); + expect(renderMarkdown("**bold** and *italic*")).toContain( + "bold", + ); expect(renderMarkdown("`code`")).toContain("code"); expect(renderMarkdown("[link](/x)")).toContain('link'); }); @@ -36,7 +42,7 @@ describe("markdown", () => { expect(html).toContain("

A paragraph.

"); expect(html).toContain("
  • one
  • "); expect(html).toContain("
    "); - expect(html).toContain("
    ");
    +		expect(html).toContain('
    ');
     	});
     });
     
    @@ -81,7 +87,11 @@ describe("content collections", () => {
     
     	test("rejects an invalid frontmatter", async () => {
     		const dir = await mkdtemp(join(tmpdir(), "muddle-content-"));
    -		await writeFile(join(dir, "broken.md"), "---\ndraft: true\n---\n\nBody", "utf8");
    +		await writeFile(
    +			join(dir, "broken.md"),
    +			"---\ndraft: true\n---\n\nBody",
    +			"utf8",
    +		);
     
     		const registry = new ContentRegistry(
     			resolveConfig({
    diff --git a/tests/modules.test.ts b/tests/modules.test.ts
    index ad7a799..b697703 100644
    --- a/tests/modules.test.ts
    +++ b/tests/modules.test.ts
    @@ -5,7 +5,11 @@ import { tmpdir } from "node:os";
     import { astroComponent } from "../modules/astro/index.ts";
     import { TemplateContext } from "../core/context.ts";
     import { createViews } from "../views/api.ts";
    -import { authFromRequest, authGuard, authIntegration } from "../integrations/auth.ts";
    +import {
    +	authFromRequest,
    +	authGuard,
    +	authIntegration,
    +} from "../integrations/auth.ts";
     import { App } from "../core/app.ts";
     import { resolveConfig } from "../core/config.ts";
     import { renderMdx } from "../modules/mdx/index.ts";
    @@ -14,30 +18,73 @@ import { compileCss } from "../views/css/index.ts";
     test("Astro imports compose with typed props and SPUG slots", async () => {
     	const root = await mkdtemp(join(tmpdir(), "astro-compose-"));
     	try {
    -		await writeFile(join(root, "Title.astro"), '---\nconst { title } = Astro.props;\n---\n

    {title}

    '); - await writeFile(join(root, "Card.astro"), '---\nimport Title from "./Title.astro";\nconst { title } = Astro.props;\n---\n
    <slot /></article>'); - await writeFile(join(root, "index.spug"), 'component Card\n title: "Composed"\n render content\n p Slot body'); - const context = new TemplateContext().import("Card", astroComponent(join(root, "Card.astro"))); + await writeFile( + join(root, "Title.astro"), + "---\nconst { title } = Astro.props;\n---\n<h2>{title}</h2>", + ); + await writeFile( + join(root, "Card.astro"), + '---\nimport Title from "./Title.astro";\nconst { title } = Astro.props;\n---\n<article><Title title={title} /><slot /></article>', + ); + await writeFile( + join(root, "index.spug"), + 'component Card\n title: "Composed"\n render content\n p Slot body', + ); + const context = new TemplateContext().import( + "Card", + astroComponent(join(root, "Card.astro")), + ); const views = createViews({ viewsDir: root, context }); const html = await views.renderFile("index"); expect(html).toContain("<h2>Composed</h2>"); expect(html).toContain("<p>Slot body</p>"); - } finally { await rm(root, { recursive: true, force: true }); } + } finally { + await rm(root, { recursive: true, force: true }); + } }); test("form authentication issues an opaque session and rejects forged identity cookies", async () => { - const app = await App.create({ config: resolveConfig(), integrations: [authIntegration({ login: (email, password) => password === "secret" ? { id: email } : undefined })] }); - const response = await app.fetch(new Request("https://example.test/auth/login", { method: "POST", headers: { origin: "https://example.test" }, body: new URLSearchParams({ email: "ada", password: "secret" }) })); + const app = await App.create({ + config: resolveConfig(), + integrations: [ + authIntegration({ + login: (email, password) => + password === "secret" ? { id: email } : undefined, + }), + ], + }); + const response = await app.fetch( + new Request("https://example.test/auth/login", { + method: "POST", + headers: { origin: "https://example.test" }, + body: new URLSearchParams({ email: "ada", password: "secret" }), + }), + ); expect(response.status).toBe(302); const cookie = response.headers.get("set-cookie")!; - expect((await authFromRequest(new Request("https://example.test", { headers: { cookie } })))?.id).toBe("ada"); - expect(await authFromRequest(new Request("https://example.test", { headers: { cookie: 'muddle_auth=%7B%22id%22%3A%22admin%22%7D' } }))).toBeUndefined(); + expect( + ( + await authFromRequest( + new Request("https://example.test", { headers: { cookie } }), + ) + )?.id, + ).toBe("ada"); + expect( + await authFromRequest( + new Request("https://example.test", { + headers: { cookie: "muddle_auth=%7B%22id%22%3A%22admin%22%7D" }, + }), + ), + ).toBeUndefined(); }); test("OAuth callback validates browser-bound state and exchanges a PKCE code", async () => { const original = globalThis.fetch; let exchanged = false; - globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => { + globalThis.fetch = (async ( + input: string | URL | Request, + init?: RequestInit, + ) => { if (String(input).endsWith("/token")) { const body = init?.body as URLSearchParams; expect(body.get("code_verifier")?.length).toBeGreaterThan(40); @@ -47,15 +94,48 @@ test("OAuth callback validates browser-bound state and exchanges a PKCE code", a return Response.json({ sub: "oauth-user" }); }) as typeof fetch; try { - const app = await App.create({ config: resolveConfig(), integrations: [authIntegration({ providers: [{ name: "test", authorizationUrl: "https://provider.test/authorize", tokenUrl: "https://provider.test/token", userInfoUrl: "https://provider.test/user", clientId: "test", redirectUri: "https://app.test/auth/test/callback", identity: (profile) => ({ id: String(profile.sub) }) }] })] }); + const app = await App.create({ + config: resolveConfig(), + integrations: [ + authIntegration({ + providers: [ + { + name: "test", + authorizationUrl: "https://provider.test/authorize", + tokenUrl: "https://provider.test/token", + userInfoUrl: "https://provider.test/user", + clientId: "test", + redirectUri: "https://app.test/auth/test/callback", + identity: (profile) => ({ id: String(profile.sub) }), + }, + ], + }), + ], + }); const start = await app.fetch(new Request("https://app.test/auth/test")); const authorization = new URL(start.headers.get("location")!); - expect(authorization.searchParams.get("code_challenge_method")).toBe("S256"); + expect(authorization.searchParams.get("code_challenge_method")).toBe( + "S256", + ); const callback = `https://app.test/auth/test/callback?code=test-code&state=${authorization.searchParams.get("state")}`; expect((await app.fetch(new Request(callback))).status).toBe(400); - const response = await app.fetch(new Request(callback, { headers: { cookie: start.headers.get("set-cookie")! } })); + const response = await app.fetch( + new Request(callback, { + headers: { cookie: start.headers.get("set-cookie")! }, + }), + ); expect(response.status).toBe(302); expect(exchanged).toBe(true); - expect((await app.fetch(new Request(callback, { headers: { cookie: start.headers.get("set-cookie")! } }))).status).toBe(400); - } finally { globalThis.fetch = original; } + expect( + ( + await app.fetch( + new Request(callback, { + headers: { cookie: start.headers.get("set-cookie")! }, + }), + ) + ).status, + ).toBe(400); + } finally { + globalThis.fetch = original; + } }); diff --git a/tests/orm.test.ts b/tests/orm.test.ts index e5d6895..638c5a3 100644 --- a/tests/orm.test.ts +++ b/tests/orm.test.ts @@ -125,9 +125,9 @@ describe("orm", () => { expect(await users.count()).toBe(3); expect(await users.where("age", ">", 30).count()).toBe(2); - expect( - (await users.orderBy("age", "desc").limit(1).all())[0]?.name, - ).toBe("Alan"); + expect((await users.orderBy("age", "desc").limit(1).all())[0]?.name).toBe( + "Alan", + ); expect( await users .where([{ column: "name", operator: "=", value: "Ada" }]) diff --git a/tests/postgres.test.ts b/tests/postgres.test.ts index 2b8524b..af8f6f1 100644 --- a/tests/postgres.test.ts +++ b/tests/postgres.test.ts @@ -1,5 +1,8 @@ import { describe, expect, test } from "bun:test"; -import { createDatabase, type Database } from "../modules/database/src/index.ts"; +import { + createDatabase, + type Database, +} from "../modules/database/src/index.ts"; import { Field } from "../modules/database/src/decorators/Field.ts"; import { Hydrate } from "../modules/database/src/decorators/Hydrate.ts"; import { Table } from "../modules/database/src/decorators/Table.ts"; @@ -52,9 +55,11 @@ async function database(): Promise<Database> { describe.skipIf(!url)("postgres", () => { test("creates the table of a model", async () => { const db = await database(); - const rows = await db.model(User).query( - "SELECT table_name FROM information_schema.tables WHERE table_name = 'muddle_users'", - ); + const rows = await db + .model(User) + .query( + "SELECT table_name FROM information_schema.tables WHERE table_name = 'muddle_users'", + ); expect(rows).toHaveLength(1); await db.close(); }); diff --git a/tests/prediction.test.ts b/tests/prediction.test.ts index 658304d..1b7c3db 100644 --- a/tests/prediction.test.ts +++ b/tests/prediction.test.ts @@ -2,10 +2,18 @@ import { expect, test } from "bun:test"; import { predictLayout } from "../adapters/prediction.ts"; test("prediction measures desktop and mobile geometry and restores closed disclosure", async () => { - const html = '<!doctype html><html><head><style>.panel { width: 400px; height: 180px; } @media(max-width:48rem) { .panel { width: 200px; height: 240px; } }</style></head><body><details><summary>Menu</summary><div class="panel" data-muddle-anchor="panel">Content</div></details></body></html>'; - const result = await predictLayout(html, { selectors: [".panel"], strict: true }); - expect(result.predictions.map((prediction) => prediction.width)).toEqual([400, 200]); - expect(result.predictions.map((prediction) => prediction.height)).toEqual([180, 240]); + const html = + '<!doctype html><html><head><style>.panel { width: 400px; height: 180px; } @media(max-width:48rem) { .panel { width: 200px; height: 240px; } }</style></head><body><details><summary>Menu</summary><div class="panel" data-muddle-anchor="panel">Content</div></details></body></html>'; + const result = await predictLayout(html, { + selectors: [".panel"], + strict: true, + }); + expect(result.predictions.map((prediction) => prediction.width)).toEqual([ + 400, 200, + ]); + expect(result.predictions.map((prediction) => prediction.height)).toEqual([ + 180, 240, + ]); expect(result.html).not.toContain("<details open"); expect(result.html).toContain('data-muddle-predicted="predict-0"'); expect(result.html).toContain("--muddle-predicted-desktop-width: 400px"); @@ -26,8 +34,14 @@ test("prediction resolves a target that is sized from an ahead-of-time relation" <div class="source" data-muddle-anchor="source"></div> <div class="target"></div> </body></html>`; - const result = await predictLayout(html, { selectors: [".target"], strict: true }); - const target = result.predictions.filter((prediction) => prediction.selector === '[data-muddle-predicted="predict-0"]'); + const result = await predictLayout(html, { + selectors: [".target"], + strict: true, + }); + const target = result.predictions.filter( + (prediction) => + prediction.selector === '[data-muddle-predicted="predict-0"]', + ); expect(target.map((prediction) => prediction.width)).toEqual([240, 160]); expect(target.map((prediction) => prediction.x)).toEqual([100, 24]); expect(result.html).toContain("--muddle-predicted-desktop-width: 240px"); @@ -44,22 +58,39 @@ test("prediction settles chained relations before writing the static page", asyn <div class="middle" data-muddle-anchor="middle"></div> <div class="target"></div> </body></html>`; - const result = await predictLayout(html, { selectors: [".target"], strict: true }); - const target = result.predictions.filter((prediction) => prediction.selector === '[data-muddle-predicted="predict-0"]'); + const result = await predictLayout(html, { + selectors: [".target"], + strict: true, + }); + const target = result.predictions.filter( + (prediction) => + prediction.selector === '[data-muddle-predicted="predict-0"]', + ); expect(target.map((prediction) => prediction.width)).toEqual([216, 216]); expect(target.map((prediction) => prediction.x)).toEqual([72, 72]); }); test("prediction follows the page's widest mobile breakpoint", async () => { - const html = '<!doctype html><html><head><style>.panel { width: 500px; height: 24px; } @media (max-width: 40rem) { .panel { width: 280px; } }</style></head><body><div class="panel" data-muddle-anchor="panel"></div></body></html>'; - const result = await predictLayout(html, { selectors: [".panel"], strict: true }); - expect(result.predictions.map((prediction) => prediction.width)).toEqual([500, 280]); + const html = + '<!doctype html><html><head><style>.panel { width: 500px; height: 24px; } @media (max-width: 40rem) { .panel { width: 280px; } }</style></head><body><div class="panel" data-muddle-anchor="panel"></div></body></html>'; + const result = await predictLayout(html, { + selectors: [".panel"], + strict: true, + }); + expect(result.predictions.map((prediction) => prediction.width)).toEqual([ + 500, 280, + ]); expect(result.html).toContain("@media (max-width: 40rem)"); }); test("prediction emits each selected breakpointAcc relation layer", async () => { - const html = '<!doctype html><html><head><style>.panel { width: 600px; height: 24px; } @media (max-width: 64rem) { .panel { width: 420px; } } @media (max-width: 48rem) { .panel { width: 300px; } }</style></head><body><div class="panel" data-muddle-anchor="panel"></div></body></html>'; - const result = await predictLayout(html, { selectors: [".panel"], breakpointAcc: ["LG", "MD"], strict: true }); + const html = + '<!doctype html><html><head><style>.panel { width: 600px; height: 24px; } @media (max-width: 64rem) { .panel { width: 420px; } } @media (max-width: 48rem) { .panel { width: 300px; } }</style></head><body><div class="panel" data-muddle-anchor="panel"></div></body></html>'; + const result = await predictLayout(html, { + selectors: [".panel"], + breakpointAcc: ["LG", "MD"], + strict: true, + }); expect(result.html).toContain("--muddle-relate-panel-lg-width: 420px"); expect(result.html).toContain("--muddle-relate-panel-md-width: 300px"); expect(result.html).toContain("@media (max-width: 64rem)"); @@ -67,9 +98,12 @@ test("prediction emits each selected breakpointAcc relation layer", async () => }); test("prediction discovers dotted anchor groups and emits reflective metadata", async () => { - const html = '<!doctype html><html><head><style>.item { width: 200px; }</style></head><body><a class="item" data-muddle-anchor="docs.intro"></a><a class="item" data-muddle-anchor="docs.api"></a></body></html>'; + const html = + '<!doctype html><html><head><style>.item { width: 200px; }</style></head><body><a class="item" data-muddle-anchor="docs.intro"></a><a class="item" data-muddle-anchor="docs.api"></a></body></html>'; const result = await predictLayout(html, { strict: true }); - expect(result.groups).toEqual([{ name: "docs", anchors: ["docs.intro", "docs.api"] }]); + expect(result.groups).toEqual([ + { name: "docs", anchors: ["docs.intro", "docs.api"] }, + ]); expect(result.html).toContain('data-muddle-group="docs"'); - expect(result.html).toContain('--muddle-reflect-docs-count: 2'); + expect(result.html).toContain("--muddle-reflect-docs-count: 2"); }); diff --git a/tests/router.test.ts b/tests/router.test.ts index 775c458..f7c0aba 100644 --- a/tests/router.test.ts +++ b/tests/router.test.ts @@ -92,7 +92,9 @@ describe("file based router", () => { expect(page.status).toBe(200); expect(await page.text()).toBe("<h1>Hello</h1>"); - const asset = await app.fetch(new Request("http://localhost/assets/robots.txt")); + const asset = await app.fetch( + new Request("http://localhost/assets/robots.txt"), + ); expect(asset.status).toBe(200); expect(await asset.text()).toBe("Allow: /"); diff --git a/tests/sessions-format.test.ts b/tests/sessions-format.test.ts index 05baf20..2a2c685 100644 --- a/tests/sessions-format.test.ts +++ b/tests/sessions-format.test.ts @@ -16,36 +16,88 @@ import { compileCss } from "../views/css/index.ts"; test("database sessions authenticate across app instances, revoke and consume atomically", async () => { const driver = createSqliteDriver({ path: ":memory:" }); const store = await DatabaseSessionStore.create(driver); - @Controller("/") class PrivateController { - @Route("/private") @Auth() private() { return "protected"; } + @Controller("/") + class PrivateController { + @Route("/private") @Auth() private() { + return "protected"; + } } - @Module({ controllers: [PrivateController] }) class Root {} - const create = () => App.create({ config: resolveConfig(), modules: [Root], integrations: [authIntegration({ store, login: () => ({ id: "ada" }) })] }); + @Module({ controllers: [PrivateController] }) + class Root {} + const create = () => + App.create({ + config: resolveConfig(), + modules: [Root], + integrations: [authIntegration({ store, login: () => ({ id: "ada" }) })], + }); try { const first = await create(); const second = await create(); - const login = await first.fetch(new Request("https://app.test/auth/login", { method: "POST", headers: { origin: "https://app.test", "content-type": "application/json" }, body: JSON.stringify({ email: "ada", password: "secret" }) })); + const login = await first.fetch( + new Request("https://app.test/auth/login", { + method: "POST", + headers: { + origin: "https://app.test", + "content-type": "application/json", + }, + body: JSON.stringify({ email: "ada", password: "secret" }), + }), + ); const cookie = login.headers.get("set-cookie")!; - expect((await second.fetch(new Request("https://app.test/private", { headers: { cookie } }))).status).toBe(200); - await first.fetch(new Request("https://app.test/auth/logout", { method: "POST", headers: { origin: "https://app.test", cookie } })); - expect((await second.fetch(new Request("https://app.test/private", { headers: { cookie } }))).status).toBe(401); - await store.set("transaction", { value: { verifier: "once" }, expires: Date.now() + 10000 }); - expect((await Promise.all([store.consume("transaction"), store.consume("transaction")])).filter(Boolean)).toHaveLength(1); + expect( + ( + await second.fetch( + new Request("https://app.test/private", { headers: { cookie } }), + ) + ).status, + ).toBe(200); + await first.fetch( + new Request("https://app.test/auth/logout", { + method: "POST", + headers: { origin: "https://app.test", cookie }, + }), + ); + expect( + ( + await second.fetch( + new Request("https://app.test/private", { headers: { cookie } }), + ) + ).status, + ).toBe(401); + await store.set("transaction", { + value: { verifier: "once" }, + expires: Date.now() + 10000, + }); + expect( + ( + await Promise.all([ + store.consume("transaction"), + store.consume("transaction"), + ]) + ).filter(Boolean), + ).toHaveLength(1); await store.set("expired", { value: {}, expires: 1 }); expect(await store.get("expired")).toBeUndefined(); - } finally { await driver.close(); } + } finally { + await driver.close(); + } }); test("Prettier delegates to muddle formatting and preserves compiled STCSS", async () => { - const source = '.card{padding:1rem;@predict mobile max-height; &:hover{opacity:0.8;}}'; + const source = + ".card{padding:1rem;@predict mobile max-height; &:hover{opacity:0.8;}}"; const formatted = await formatSource(source, "stcss"); - expect(await prettier.format(source, { parser: "stcss", plugins: [plugin] })).toBe(formatted); + expect( + await prettier.format(source, { parser: "stcss", plugins: [plugin] }), + ).toBe(formatted); expect(await formatSource(formatted, "stcss")).toBe(formatted); expect(compileCss(formatted)).toBe(compileCss(source)); - const spug = 'javascript!\nconst title={text:"Hello"}\nend\ndiv\n p= title.text\n'; + const spug = + 'javascript!\nconst title={text:"Hello"}\nend\ndiv\n p= title.text\n'; const next = await formatSource(spug, "spug"); expect(next).toContain('const title = { text: "Hello" };'); expect(await formatSource(next, "spug")).toBe(next); - const raw = 'script.\n const text = "a b";\n\n console.log(text);\np End\n'; + const raw = + 'script.\n const text = "a b";\n\n console.log(text);\np End\n'; expect(await formatSource(raw, "spug")).toBe(raw); }); diff --git a/tests/stage.test.ts b/tests/stage.test.ts index c3f8c25..87b885b 100644 --- a/tests/stage.test.ts +++ b/tests/stage.test.ts @@ -6,8 +6,19 @@ import { stage } from "../views/stage.ts"; test("stage manager emits native scroll-driven scenes and portal transitions", () => { const css = stage() - .scene("intro-in", { selector: ".intro", keyframes: { "0%": { opacity: 0, transform: "translateY(2rem)" }, "100%": { opacity: 1, transform: "translateY(0)" } }, range: "entry 0% cover 45%" }) - .portal("main", { old: { opacity: 1 }, new: { opacity: 0 }, duration: "180ms" }) + .scene("intro-in", { + selector: ".intro", + keyframes: { + "0%": { opacity: 0, transform: "translateY(2rem)" }, + "100%": { opacity: 1, transform: "translateY(0)" }, + }, + range: "entry 0% cover 45%", + }) + .portal("main", { + old: { opacity: 1 }, + new: { opacity: 0 }, + duration: "180ms", + }) .css(); expect(css).toContain("animation-timeline: view()"); expect(css).toContain("animation-range: entry 0% cover 45%"); @@ -16,7 +27,10 @@ test("stage manager emits native scroll-driven scenes and portal transitions", ( test("portal blocks compile to named view-transition boundaries", () => { const parsed = parse(lex("portal main\n h1 Hello")); - const compiled = compileTemplate(parsed.nodes, { path: "/tmp/portal.spug", dir: "/tmp" }); - expect(compiled.source).toContain("data-muddle-portal=\\\"main\\\""); + const compiled = compileTemplate(parsed.nodes, { + path: "/tmp/portal.spug", + dir: "/tmp", + }); + expect(compiled.source).toContain('data-muddle-portal=\\"main\\"'); expect(compiled.source).toContain("view-transition-name:main"); }); diff --git a/tests/templates-generator.test.ts b/tests/templates-generator.test.ts index 97ddef2..a2bbe30 100644 --- a/tests/templates-generator.test.ts +++ b/tests/templates-generator.test.ts @@ -9,7 +9,10 @@ test("creates the responsive documentation template", async () => { try { const result = await createTemplate("docs", root); expect(result.files.length).toBeGreaterThan(6); - const view = await readFile(join(root, "modules/app/views/index.spug"), "utf8"); + const view = await readFile( + join(root, "modules/app/views/index.spug"), + "utf8", + ); const styles = await readFile(join(root, "styles/app.stcss"), "utf8"); expect(view).toContain("@docs.{section.id}"); expect(styles).toContain(":reflect(@docs)"); @@ -22,7 +25,9 @@ test("creates the minimal template", async () => { const root = await mkdtemp(join(tmpdir(), "muddle-minimal-")); try { await createTemplate("minimal", root); - expect(await readFile(join(root, "index.ts"), "utf8")).toContain("export const app"); + expect(await readFile(join(root, "index.ts"), "utf8")).toContain( + "export const app", + ); } finally { await rm(root, { recursive: true, force: true }); } diff --git a/tests/templates.test.ts b/tests/templates.test.ts index f96ed52..7c01543 100644 --- a/tests/templates.test.ts +++ b/tests/templates.test.ts @@ -12,15 +12,24 @@ const sample = join(root, "samples/hello-world"); describe("template tests", () => { test("SPUG anchors emit generated relation identities", () => { - const nodes = parse(lex("nav\n ul\n li@hello1() First\n li@row(item.id) Second")); - const firstAnchor = nodes.nodes[0]?.kind === "element" - ? nodes.nodes[0].children[0]?.kind === "element" - ? nodes.nodes[0].children[0].children[0] - : undefined - : undefined; - expect(firstAnchor).toMatchObject({ kind: "element", anchor: { name: "hello1" } }); - const compiled = compileTemplate(nodes.nodes, { path: "/tmp/relations.spug", dir: "/tmp" }); - expect(compiled.source).toContain('data-muddle-anchor'); + const nodes = parse( + lex("nav\n ul\n li@hello1() First\n li@row(item.id) Second"), + ); + const firstAnchor = + nodes.nodes[0]?.kind === "element" + ? nodes.nodes[0].children[0]?.kind === "element" + ? nodes.nodes[0].children[0].children[0] + : undefined + : undefined; + expect(firstAnchor).toMatchObject({ + kind: "element", + anchor: { name: "hello1" }, + }); + const compiled = compileTemplate(nodes.nodes, { + path: "/tmp/relations.spug", + dir: "/tmp", + }); + expect(compiled.source).toContain("data-muddle-anchor"); expect(compiled.source).toContain('"hello1"'); expect(compiled.source).toContain('"row" + String(item.id)'); expect(compiled.source).not.toContain("__muddleRelations"); @@ -28,38 +37,37 @@ describe("template tests", () => { test("SPUG derives a dotted anchor leaf from composition scope", () => { const nodes = parse(lex("a@docs.{section.id}(href='#' + section.id) Link")); - const compiled = compileTemplate(nodes.nodes, { path: "/tmp/reflective.spug", dir: "/tmp" }); + const compiled = compileTemplate(nodes.nodes, { + path: "/tmp/reflective.spug", + dir: "/tmp", + }); expect(compiled.source).toContain('"docs." + String(section.id)'); }); - test( - "the assertions of the templates pass", - async () => { - const views = createViews({ viewsDir: join(sample, "modules/app/views") }); - registerAmezing(views); - const run = await runTemplateTests({ - root: sample, - viewRoots: [join(sample, "modules/app/views")], - components: views.components, - locals: { - title: "muddle", - nav: [{ label: "Home", href: "/" }], + test("the assertions of the templates pass", async () => { + const views = createViews({ viewsDir: join(sample, "modules/app/views") }); + registerAmezing(views); + const run = await runTemplateTests({ + root: sample, + viewRoots: [join(sample, "modules/app/views")], + components: views.components, + locals: { + title: "muddle", + nav: [{ label: "Home", href: "/" }], posts: [{ id: "test1", title: "Test1", excerpt: "Test1" }], post: { id: "test1", title: "Test1", excerpt: "Test1" }, - }, - }); + }, + }); - if (run.skipped) { - console.log(`muddle: template tests skipped, ${run.reason}`); - return; - } + if (run.skipped) { + console.log(`muddle: template tests skipped, ${run.reason}`); + return; + } - expect(run.tested.length).toBeGreaterThan(0); - for (const failure of run.failures) { - console.error(`muddle: ${failure.key}\n${failure.error.message}`); - } - expect(run.failures).toEqual([]); - }, - 30_000, - ); + expect(run.tested.length).toBeGreaterThan(0); + for (const failure of run.failures) { + console.error(`muddle: ${failure.key}\n${failure.error.message}`); + } + expect(run.failures).toEqual([]); + }, 30_000); }); diff --git a/tests/views.test.ts b/tests/views.test.ts index 59c34be..11e62e9 100644 --- a/tests/views.test.ts +++ b/tests/views.test.ts @@ -59,10 +59,7 @@ describe("lexer", () => { const props = parsed.nodes.find((node) => node.kind === "props"); expect(props?.kind).toBe("props"); if (props?.kind === "props") { - expect(props.props.map((prop) => prop.name)).toEqual([ - "title", - "body", - ]); + expect(props.props.map((prop) => prop.name)).toEqual(["title", "body"]); expect(props.props[0]?.optional).toBe(false); expect(props.props[1]?.optional).toBe(true); expect(props.props[1]?.type).toBe("string"); @@ -91,10 +88,7 @@ describe("renderer", () => { }); test("renders a condition", async () => { - const name = await template( - "condition", - "if value\n p yes\nelse\n p no", - ); + const name = await template("condition", "if value\n p yes\nelse\n p no"); expect(await engine.render(name, { value: true })).toBe("<p>yes</p>"); expect(await engine.render(name, { value: false })).toBe("<p>no</p>"); }); @@ -122,10 +116,7 @@ describe("renderer", () => { }); test("merges the classes of the shorthand and of the attribute", async () => { - const name = await template( - "classes", - "div.card(class=extra) Text", - ); + const name = await template("classes", "div.card(class=extra) Text"); expect(await engine.render(name, { extra: "wide" })).toBe( '<div class="card wide">Text</div>', ); @@ -141,11 +132,10 @@ describe("renderer", () => { test("renders an included partial", async () => { await template("partial", "footer Footer"); - const name = await template( - "include", - "div\n include ./partial.spug", + const name = await template("include", "div\n include ./partial.spug"); + expect(await engine.render(name)).toBe( + "<div><footer>Footer</footer></div>", ); - expect(await engine.render(name)).toBe("<div><footer>Footer</footer></div>"); }); test("renders a layout with a block", async () => { @@ -183,9 +173,9 @@ describe("renderer", () => { componentsDir: components, root: dir, }); - expect( - await scoped.renderComponent("card", { title: "Card" }), - ).toBe('<article class="card"><h3>Card</h3></article>'); + expect(await scoped.renderComponent("card", { title: "Card" })).toBe( + '<article class="card"><h3>Card</h3></article>', + ); }); test("keeps the html of the optional props empty", async () => { diff --git a/views/api.ts b/views/api.ts index a9fcb3c..4f7bc90 100644 --- a/views/api.ts +++ b/views/api.ts @@ -166,7 +166,12 @@ export class Views { constructor(options: ViewsOptions) { this.hooks = options.hooks; this.components = options.components ?? new Map(); - for (const [name, component] of options.context?.imports ?? []) this.components.set(name, { name, component: component as ServerComponent, deferred: false }); + for (const [name, component] of options.context?.imports ?? []) + this.components.set(name, { + name, + component: component as ServerComponent, + deferred: false, + }); this.engine = new TemplateEngine({ viewsDir: options.viewsDir, componentsDir: options.componentsDir, @@ -195,17 +200,31 @@ export class Views { /** Registers a component rendered by a SPUG template and optional STCSS files. */ spugComponent(name: string, options: SpugComponentOptions = {}): this { const template = options.template ?? name; - const styles = Array.isArray(options.style) ? options.style : options.style ? [options.style] : []; - return this.component(name, { - render: async (props) => { - const html = await this.engine.renderComponent(template, props); - const css = await Promise.all(styles.map(async (style) => { - const path = resolve(this.componentsDir, style); - return existsSync(path) ? compileCss(await readFile(path, "utf8"), { hooks: this.hooks }) : ""; - })); - return `${css.filter(Boolean).map(styleTag).join("")}${html}`; + const styles = Array.isArray(options.style) + ? options.style + : options.style + ? [options.style] + : []; + return this.component( + name, + { + render: async (props) => { + const html = await this.engine.renderComponent(template, props); + const css = await Promise.all( + styles.map(async (style) => { + const path = resolve(this.componentsDir, style); + return existsSync(path) + ? compileCss(await readFile(path, "utf8"), { + hooks: this.hooks, + }) + : ""; + }), + ); + return `${css.filter(Boolean).map(styleTag).join("")}${html}`; + }, }, - }, { deferred: options.deferred }); + { deferred: options.deferred }, + ); } /** Starts a fluent render of a template. */ diff --git a/views/components.ts b/views/components.ts index 3cecd4f..83ac29f 100644 --- a/views/components.ts +++ b/views/components.ts @@ -42,8 +42,14 @@ export interface ServerComponent { ): Promise<string | SpugComponentRender> | string | SpugComponentRender; } -export function isSpugComponentRender(value: unknown): value is SpugComponentRender { - return typeof value === "object" && value !== null && typeof (value as { spug?: unknown }).spug === "string"; +export function isSpugComponentRender( + value: unknown, +): value is SpugComponentRender { + return ( + typeof value === "object" && + value !== null && + typeof (value as { spug?: unknown }).spug === "string" + ); } /** A registered server component. */ diff --git a/views/css/compiler.ts b/views/css/compiler.ts index 0112243..1d18678 100644 --- a/views/css/compiler.ts +++ b/views/css/compiler.ts @@ -31,13 +31,21 @@ export function compileCss( source: string, options: CompileOptions = {}, ): string { - const scripted = source.replace(/(?:javascript|typescript)!\s*\n([\s\S]*?)\nend\s*/g, (_match, code: string) => { - for (const entry of code.matchAll(/(?:const|let)\s+([\w-]+)\s*=\s*([^;\n]+);?/g)) { - options.variables ??= {}; - options.variables[`$${entry[1]}`] = String(entry[2]).replace(/^['"]|['"]$/g, ""); - } - return ""; - }); + const scripted = source.replace( + /(?:javascript|typescript)!\s*\n([\s\S]*?)\nend\s*/g, + (_match, code: string) => { + for (const entry of code.matchAll( + /(?:const|let)\s+([\w-]+)\s*=\s*([^;\n]+);?/g, + )) { + options.variables ??= {}; + options.variables[`$${entry[1]}`] = String(entry[2]).replace( + /^['"]|['"]$/g, + "", + ); + } + return ""; + }, + ); const stylesheet = parseCss(scripted); const context: Context = { scope: { ...(options.variables ?? {}) }, @@ -71,15 +79,18 @@ function compileNodes( resolveValue(node.selector, context), selector, ); - const declarationInput = node.declarations.map((declaration) => ({ ...declaration })); + const declarationInput = node.declarations.map((declaration) => ({ + ...declaration, + })); const expansions = context.hooks.expandStcssDeclarations({ selector: resolved, declarations: declarationInput, depth, resolve: (value) => resolveValue(value, context), }); - const declarations = declarationInput.map((declaration) => - `${declaration.property}: ${resolveValue(declaration.value, context)};`, + const declarations = declarationInput.map( + (declaration) => + `${declaration.property}: ${resolveValue(declaration.value, context)};`, ); const nested = compileNodes(node.children, resolved, context, depth + 1); @@ -130,7 +141,8 @@ function compileAtRule( depth, children: node.children, resolve: (value) => resolveValue(value, context), - renderChildren: () => compileNodes(node.children, selector, context, depth + 1), + renderChildren: () => + compileNodes(node.children, selector, context, depth + 1), }); if (expanded !== undefined) return expanded; @@ -200,14 +212,27 @@ function compileAtRule( } case "view-transition": { const name = node.params.trim() || "none"; - return selector ? `${indent}${selector} {\n${indent}\tview-transition-name: ${name};\n${indent}}` : ""; + return selector + ? `${indent}${selector} {\n${indent}\tview-transition-name: ${name};\n${indent}}` + : ""; } case "predict": { - const mobile = /\bmobile\b/.test(node.params); - const properties = node.params.replace(/\bmobile\b/g, "").trim() || "transform, max-width, max-height"; - const transitions = properties.split(/[\s,]+/).filter(Boolean).map((property) => `${property} var(--muddle-predict-duration, 280ms) var(--muddle-predict-easing, ease)`).join(", "); - const css = selector ? `${indent}${selector} {\n${indent}\ttransition: ${transitions};\n${indent}}` : ""; - return mobile ? `@media (max-width: 48rem) {\n${css}\n}` : css; + const mobile = /\bmobile\b/.test(node.params); + const properties = + node.params.replace(/\bmobile\b/g, "").trim() || + "transform, max-width, max-height"; + const transitions = properties + .split(/[\s,]+/) + .filter(Boolean) + .map( + (property) => + `${property} var(--muddle-predict-duration, 280ms) var(--muddle-predict-easing, ease)`, + ) + .join(", "); + const css = selector + ? `${indent}${selector} {\n${indent}\ttransition: ${transitions};\n${indent}}` + : ""; + return mobile ? `@media (max-width: 48rem) {\n${css}\n}` : css; } case "measure": { const spec = node.params.trim(); @@ -215,7 +240,10 @@ function compileAtRule( for (const candidate of candidates) { if (candidate.length === 0) continue; const resolved = resolveValue(candidate, context); - const related = resolveRelationSelector(resolved).replace(/@([\w.-]+)/g, (_all, name: string) => `[data-muddle-anchor="${name}"]`); + const related = resolveRelationSelector(resolved).replace( + /@([\w.-]+)/g, + (_all, name: string) => `[data-muddle-anchor="${name}"]`, + ); context.measured.push(related); } return ""; @@ -228,16 +256,20 @@ function compileAtRule( } case "responsive": { const width = context.breakpoints[node.params.trim()]; - if (!width) throw new Error(`Unknown responsive breakpoint "${node.params.trim()}"`); + if (!width) + throw new Error( + `Unknown responsive breakpoint "${node.params.trim()}"`, + ); const nested = compileNodes(node.children, selector, context, depth + 1); - return nested ? `${indent}@media (min-width: ${width}) {\n${nested}\n${indent}}` : ""; + return nested + ? `${indent}@media (min-width: ${width}) {\n${nested}\n${indent}}` + : ""; } default: return ""; } } - /** Resolves the selector of a nested rule. */ export function resolveSelector(selector: string, parent: string): string { selector = resolveRelationSelector(selector); @@ -261,10 +293,17 @@ export function resolveValue(value: string, context: Context): string { result = result.replace(/#\{([^}]*)\}/g, (_match, expression: string) => { return evaluate(expression, { scope: context.scope }); }); - result = result.replace(/d(["'])(.*?)\1/g, (_match, quote: string, name: string) => { - const resolved = name.replace(/\{([^}]*)\}/g, (_entry: string, expression: string) => evaluate(expression, { scope: context.scope })); - return `d${quote}${resolved}${quote}`; - }); + result = result.replace( + /d(["'])(.*?)\1/g, + (_match, quote: string, name: string) => { + const resolved = name.replace( + /\{([^}]*)\}/g, + (_entry: string, expression: string) => + evaluate(expression, { scope: context.scope }), + ); + return `d${quote}${resolved}${quote}`; + }, + ); result = result.replace(/\$[\w-]+/g, (match) => { const resolved = context.scope[match]; @@ -292,40 +331,70 @@ function relationState(name: string, state: string): string { /** Turns relation pseudo selectors into native relational selectors. */ function resolveRelationSelector(selector: string): string { let result = selector; - result = result.replace(/:reflect\(\s*@([\w.-]+)\s*\)/g, (_all, group: string) => - `[data-muddle-anchor^="${group}."]`, + result = result.replace( + /:reflect\(\s*@([\w.-]+)\s*\)/g, + (_all, group: string) => `[data-muddle-anchor^="${group}."]`, ); - result = result.replace(/:with\(\s*@([\w.-]+):([\w-]+)\s*\)/g, (_all, name: string, state: string) => - `:has(${relationState(name, state)})`, + result = result.replace( + /:with\(\s*@([\w.-]+):([\w-]+)\s*\)/g, + (_all, name: string, state: string) => + `:has(${relationState(name, state)})`, + ); + result = result.replace( + /([^,{]+?):when\(\s*([^\s()]+)\s+@([\w.-]+):([\w-]+)\s*\)/g, + (_all, target: string, root: string, name: string, state: string) => { + const normalized = target.trim(); + const suffix = normalized.startsWith(root) + ? normalized.slice(root.length).trim() + : normalized; + return `${root}:has(${relationState(name, state)})${suffix ? ` ${suffix}` : ""}`; + }, + ); + return result.replace( + /@([\w.-]+):(active|clicked|focus)\b/g, + (_all, name: string, state: string) => relationState(name, state), ); - result = result.replace(/([^,{]+?):when\(\s*([^\s()]+)\s+@([\w.-]+):([\w-]+)\s*\)/g, (_all, target: string, root: string, name: string, state: string) => { - const normalized = target.trim(); - const suffix = normalized.startsWith(root) - ? normalized.slice(root.length).trim() - : normalized; - return `${root}:has(${relationState(name, state)})${suffix ? ` ${suffix}` : ""}`; - }); - return result.replace(/@([\w.-]+):(active|clicked|focus)\b/g, (_all, name: string, state: string) => relationState(name, state)); } /** Resolves precomputed anchor metrics and live computed-style mirrors. */ function resolveRelationValue(value: string): string { - const self = value.replace(/\$self\[([a-zA-Z][\w-]*)\]/g, (_all, property: string) => { - return `var(--muddle-self-${relationKey(property)})`; - }); + const self = value.replace( + /\$self\[([a-zA-Z][\w-]*)\]/g, + (_all, property: string) => { + return `var(--muddle-self-${relationKey(property)})`; + }, + ); - let result = self.replace(/\$(?:relate|refer)(?:\[([\w-]+)\])?\(\s*d?["']?@([^\s)"']+)["']?\s*\)/g, (_all, field: string | undefined, name: string) => { - const metric = ({ left: "x", top: "y" } as Record<string, string>)[field ?? ""] ?? field ?? "x"; - return `var(--muddle-relate-${relationKey(name)}-${metric})`; - }); - result = result.replace(/@([A-Za-z_][\w.-]*)\.computed\.([\w-]+)(?:\.replicate\(&\))?/g, (_all, name: string, property: string) => { - const cssProperty = property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`); - return `var(--muddle-relate-${relationKey(name)}-${cssProperty})`; - }); - result = result.replace(/@([A-Za-z_][\w-]*)\.([\w-]+)(?:\.replicate\(&\))?/g, (_all, name: string, property: string) => { - const cssProperty = property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`); - return `var(--muddle-relate-${relationKey(name)}-${cssProperty})`; - }); + let result = self.replace( + /\$(?:relate|refer)(?:\[([\w-]+)\])?\(\s*d?["']?@([^\s)"']+)["']?\s*\)/g, + (_all, field: string | undefined, name: string) => { + const metric = + ({ left: "x", top: "y" } as Record<string, string>)[field ?? ""] ?? + field ?? + "x"; + return `var(--muddle-relate-${relationKey(name)}-${metric})`; + }, + ); + result = result.replace( + /@([A-Za-z_][\w.-]*)\.computed\.([\w-]+)(?:\.replicate\(&\))?/g, + (_all, name: string, property: string) => { + const cssProperty = property.replace( + /[A-Z]/g, + (letter) => `-${letter.toLowerCase()}`, + ); + return `var(--muddle-relate-${relationKey(name)}-${cssProperty})`; + }, + ); + result = result.replace( + /@([A-Za-z_][\w-]*)\.([\w-]+)(?:\.replicate\(&\))?/g, + (_all, name: string, property: string) => { + const cssProperty = property.replace( + /[A-Z]/g, + (letter) => `-${letter.toLowerCase()}`, + ); + return `var(--muddle-relate-${relationKey(name)}-${cssProperty})`; + }, + ); return result; } @@ -344,14 +413,20 @@ function replaceFunctions(value: string, context: Context): string { if (depth === -1) break; const inner = result.slice(index + 1, depth); if (name.startsWith("$")) { - const expandedCalculation = context.hooks.expandStcssCalculation(name.slice(1), inner, (entry) => resolveValue(entry, context)); + const expandedCalculation = context.hooks.expandStcssCalculation( + name.slice(1), + inner, + (entry) => resolveValue(entry, context), + ); if (expandedCalculation !== undefined) { result = `${result.slice(0, index - name.length)}${expandedCalculation}${result.slice(depth + 1)}`; index = result.indexOf("(", index + 1); continue; } } - const expanded = context.hooks.expandStcssValue(name, inner, (value) => resolveValue(value, context)); + const expanded = context.hooks.expandStcssValue(name, inner, (value) => + resolveValue(value, context), + ); if (expanded !== undefined) { result = `${result.slice(0, index - name.length)}${expanded}${result.slice(depth + 1)}`; index = result.indexOf("(", index + 1); @@ -367,7 +442,9 @@ function replaceFunctions(value: string, context: Context): string { index = result.indexOf("(", index + 1); } - return context.hooks.expandStcssCalculationTokens(result, (entry) => resolveValue(entry, context)); + return context.hooks.expandStcssCalculationTokens(result, (entry) => + resolveValue(entry, context), + ); } function matchParens(source: string, start: number): number { @@ -396,7 +473,12 @@ function splitValues(value: string): string[] { const to = Number(range[2]); const step = from <= to ? 1 : -1; const values: string[] = []; - for (let current = from; step > 0 ? current <= to : current >= to; current += step) values.push(String(current)); + for ( + let current = from; + step > 0 ? current <= to : current >= to; + current += step + ) + values.push(String(current)); return values; } const items: string[] = []; diff --git a/views/css/parser.ts b/views/css/parser.ts index 06dfe29..ec5af8d 100644 --- a/views/css/parser.ts +++ b/views/css/parser.ts @@ -40,19 +40,99 @@ export interface CssStylesheet { const COMMENT = /\/\*[\s\S]*?\*\/|\/\/[^\n]*/g; const PUG_PROPERTIES = new Set([ - "accent-color", "align-content", "align-items", "align-self", "animation", "appearance", "aspect-ratio", - "backdrop-filter", "background", "background-color", "background-image", "background-size", "block-size", - "border", "border-radius", "border-color", "border-width", "bottom", "box-shadow", "box-sizing", - "color", "columns", "contain", "content", "cursor", "display", "filter", "flex", "flex-basis", - "flex-direction", "flex-grow", "flex-shrink", "flex-wrap", "float", "font", "font-family", "font-size", - "font-weight", "gap", "grid", "grid-area", "grid-column", "grid-row", "grid-template-columns", - "grid-template-rows", "height", "inline-size", "inset", "justify-content", "justify-items", "justify-self", - "left", "letter-spacing", "line-height", "list-style", "margin", "max-height", "max-width", "min-height", - "min-width", "object-fit", "opacity", "order", "outline", "overflow", "overflow-x", "overflow-y", - "padding", "perspective", "place-content", "place-items", "pointer-events", "position", "right", "rotate", - "scale", "scroll-behavior", "scroll-margin-top", "text-align", "text-decoration", "text-transform", "top", - "transform", "transform-origin", "transition", "translate", "user-select", "visibility", "white-space", "width", - "will-change", "word-break", "z-index", + "accent-color", + "align-content", + "align-items", + "align-self", + "animation", + "appearance", + "aspect-ratio", + "backdrop-filter", + "background", + "background-color", + "background-image", + "background-size", + "block-size", + "border", + "border-radius", + "border-color", + "border-width", + "bottom", + "box-shadow", + "box-sizing", + "color", + "columns", + "contain", + "content", + "cursor", + "display", + "filter", + "flex", + "flex-basis", + "flex-direction", + "flex-grow", + "flex-shrink", + "flex-wrap", + "float", + "font", + "font-family", + "font-size", + "font-weight", + "gap", + "grid", + "grid-area", + "grid-column", + "grid-row", + "grid-template-columns", + "grid-template-rows", + "height", + "inline-size", + "inset", + "justify-content", + "justify-items", + "justify-self", + "left", + "letter-spacing", + "line-height", + "list-style", + "margin", + "max-height", + "max-width", + "min-height", + "min-width", + "object-fit", + "opacity", + "order", + "outline", + "overflow", + "overflow-x", + "overflow-y", + "padding", + "perspective", + "place-content", + "place-items", + "pointer-events", + "position", + "right", + "rotate", + "scale", + "scroll-behavior", + "scroll-margin-top", + "text-align", + "text-decoration", + "text-transform", + "top", + "transform", + "transform-origin", + "transition", + "translate", + "user-select", + "visibility", + "white-space", + "width", + "will-change", + "word-break", + "z-index", ]); function indentation(line: string): number { @@ -71,7 +151,8 @@ function pugDeclaration(line: string): string | undefined { if (variable) return `${variable[1]}: ${variable[2]}`; if (/^(?:--[\w-]+|[a-zA-Z][\w-]*)\s*:\s+.+$/.test(line)) return line; const match = /^(--[\w-]+|[a-zA-Z][\w-]*)\s+(.+)$/.exec(line); - if (match && PUG_PROPERTIES.has(match[1] as string)) return `${match[1]}: ${match[2]}`; + if (match && PUG_PROPERTIES.has(match[1] as string)) + return `${match[1]}: ${match[2]}`; return undefined; } @@ -377,5 +458,7 @@ export function parseMixinParams( /** Parses a stylesheet. */ export function parseCss(source: string): CssStylesheet { - return new StcssParser(normalizePugSyntax(source.replace(COMMENT, ""))).parse(); + return new StcssParser( + normalizePugSyntax(source.replace(COMMENT, "")), + ).parse(); } diff --git a/views/engine/compiler.ts b/views/engine/compiler.ts index 6123adc..51dbb8e 100644 --- a/views/engine/compiler.ts +++ b/views/engine/compiler.ts @@ -323,7 +323,9 @@ class Compiler { this.emitBlock(node.name, []); break; case "portal": - this.emit(`__push(${jsString(`<div data-muddle-portal="${node.name}" style="view-transition-name:${node.name}">`)});`); + this.emit( + `__push(${jsString(`<div data-muddle-portal="${node.name}" style="view-transition-name:${node.name}">`)});`, + ); this.emitBody(node.children); this.emit(`__push("</div>");`); break; @@ -515,7 +517,6 @@ class Compiler { } } - export function compileTemplate( nodes: Node[], options: CompileOptions, diff --git a/views/engine/lexer.ts b/views/engine/lexer.ts index 2f80388..b718a65 100644 --- a/views/engine/lexer.ts +++ b/views/engine/lexer.ts @@ -134,19 +134,37 @@ export function extractFrontmatter(source: string): { } /** Reads `javascript!` and `typescript!` blocks terminated by `end`. */ -export function extractScriptBlocks(source: string, frontmatter = ""): { frontmatter: string; body: string } { +export function extractScriptBlocks( + source: string, + frontmatter = "", +): { frontmatter: string; body: string } { const body: string[] = []; const scripts: string[] = []; const lines = source.split("\n"); let index = 0; while (index < lines.length) { - const directive = /^\s*(?:javascript|typescript)!\s*$/.test(lines[index] as string); - if (!directive) { body.push(lines[index] as string); index += 1; continue; } + const directive = /^\s*(?:javascript|typescript)!\s*$/.test( + lines[index] as string, + ); + if (!directive) { + body.push(lines[index] as string); + index += 1; + continue; + } index += 1; - while (index < lines.length && !/^\s*end\s*$/.test(lines[index] as string)) { scripts.push(lines[index] as string); index += 1; } + while ( + index < lines.length && + !/^\s*end\s*$/.test(lines[index] as string) + ) { + scripts.push(lines[index] as string); + index += 1; + } if (index < lines.length) index += 1; } - return { frontmatter: [frontmatter, ...scripts].filter(Boolean).join("\n"), body: body.join("\n") }; + return { + frontmatter: [frontmatter, ...scripts].filter(Boolean).join("\n"), + body: body.join("\n"), + }; } /** Finds the index of the `}` matching the `{` at `start`. */ @@ -351,7 +369,10 @@ export function parseTagLine(text: string): { if (end !== -1) { expression = rest.slice(length + 2, end).trim() || undefined; length = end + 1; - anchor = { name: `${match[1]}.`, ...(expression ? { expression } : {}) }; + anchor = { + name: `${match[1]}.`, + ...(expression ? { expression } : {}), + }; } } if (rest[length] === "(") { @@ -369,7 +390,10 @@ export function parseTagLine(text: string): { length = end + 1; } } - anchor ??= { name: match[1] as string, ...(expression ? { expression } : {}) }; + anchor ??= { + name: match[1] as string, + ...(expression ? { expression } : {}), + }; rest = rest.slice(length); } } diff --git a/views/engine/parser.ts b/views/engine/parser.ts index be3d234..de2671d 100644 --- a/views/engine/parser.ts +++ b/views/engine/parser.ts @@ -53,7 +53,12 @@ export function splitDefault(value: string): { index += 1; continue; } - if (previous === "=" || previous === "!" || previous === "<" || previous === ">") { + if ( + previous === "=" || + previous === "!" || + previous === "<" || + previous === ">" + ) { continue; } return { @@ -189,9 +194,7 @@ function transform(tokenNodes: TokenNode[]): Node[] { case "test": { nodes.push({ kind: "test", - code: entry.children - .map((child) => child.token.value) - .join("\n"), + code: entry.children.map((child) => child.token.value).join("\n"), }); index += 1; break; @@ -375,7 +378,9 @@ function transform(tokenNodes: TokenNode[]): Node[] { if (keyword === "portal") { nodes.push({ kind: "portal", - name: (token.keywordValue ?? "default").trim().replace(/^['"]|['"]$/g, ""), + name: (token.keywordValue ?? "default") + .trim() + .replace(/^['"]|['"]$/g, ""), children: transform(entry.children), }); index += 1; diff --git a/views/engine/runtime.ts b/views/engine/runtime.ts index a42a346..0ce83fc 100644 --- a/views/engine/runtime.ts +++ b/views/engine/runtime.ts @@ -5,7 +5,12 @@ import { compileTemplate } from "./compiler.ts"; import { lex } from "./lexer.ts"; import { collectProps, parse } from "./parser.ts"; import type { Node, PropDeclaration } from "./types.ts"; -import { isSpugComponentRender, type ComponentContext, type ComponentRegistry, type SpugComponentRender } from "../components.ts"; +import { + isSpugComponentRender, + type ComponentContext, + type ComponentRegistry, + type SpugComponentRender, +} from "../components.ts"; import { compileCss, styleTag } from "../css/index.ts"; import { isRaw, raw, styleValue } from "../raw.ts"; import type { DeferredChunk } from "../../contracts/typegen/http/response.ts"; @@ -339,21 +344,39 @@ export class TemplateEngine { html = await parent.template(parentRuntime, locals); } else { const blocks = mergeBlocks(compiled.blocks, new Map()); - const runtime = this.createRuntime(compiled, locals, blocks, renderContext); + const runtime = this.createRuntime( + compiled, + locals, + blocks, + renderContext, + ); await this.runFrontmatter(compiled, locals, runtime); html = await compiled.template(runtime, locals); } for (const layout of this.options.globalLayouts ?? []) { - html = await this.renderLayout(layout, { ...locals, content: raw(html) }, renderContext); + html = await this.renderLayout( + layout, + { ...locals, content: raw(html) }, + renderContext, + ); } return this.options.hooks ? this.options.hooks.renderSpug({ html, path: loaded.path, locals }) : html; } - private async renderLayout(path: string, locals: Record<string, unknown>, context: RenderContext): Promise<string> { + private async renderLayout( + path: string, + locals: Record<string, unknown>, + context: RenderContext, + ): Promise<string> { const compiled = await this.load(path); - const runtime = this.createRuntime(compiled, locals, compiled.blocks, context); + const runtime = this.createRuntime( + compiled, + locals, + compiled.blocks, + context, + ); await this.runFrontmatter(compiled, locals, runtime); return compiled.template(runtime, locals); } @@ -474,12 +497,25 @@ export class TemplateEngine { const id = `${name}-${deferredCount}`; componentContext.defer( id, - Promise.resolve(definition.component.render(props, componentContext)) - .then((output) => this.renderComponentOutput(output, props, renderContext, definition.requiresSpug)), + Promise.resolve( + definition.component.render(props, componentContext), + ).then((output) => + this.renderComponentOutput( + output, + props, + renderContext, + definition.requiresSpug, + ), + ), ); return `<div data-muddle-deferred="${id}"></div>`; } - return this.renderComponentOutput(await definition.component.render(props, componentContext), props, renderContext, definition.requiresSpug); + return this.renderComponentOutput( + await definition.component.render(props, componentContext), + props, + renderContext, + definition.requiresSpug, + ); }, renderIsland: (name, props) => this.renderIsland(name, props, renderContext), @@ -497,15 +533,30 @@ export class TemplateEngine { requiresSpug = false, ): Promise<string> { if (!isSpugComponentRender(output)) { - if (requiresSpug) throw new Error("@Component renderers must return { spug, stcss } rather than raw HTML"); + if (requiresSpug) + throw new Error( + "@Component renderers must return { spug, stcss } rather than raw HTML", + ); return output; } - const html = await this.renderComponent(output.spug, { ...props, ...(output.props ?? {}) }, context); - const styles = Array.isArray(output.stcss) ? output.stcss : output.stcss ? [output.stcss] : []; - const css = await Promise.all(styles.map(async (style) => { - const path = resolve(this.componentsDir, style); - return existsSync(path) ? compileCss(await readFile(path, "utf8")) : compileCss(style); - })); + const html = await this.renderComponent( + output.spug, + { ...props, ...(output.props ?? {}) }, + context, + ); + const styles = Array.isArray(output.stcss) + ? output.stcss + : output.stcss + ? [output.stcss] + : []; + const css = await Promise.all( + styles.map(async (style) => { + const path = resolve(this.componentsDir, style); + return existsSync(path) + ? compileCss(await readFile(path, "utf8")) + : compileCss(style); + }), + ); return `${css.filter(Boolean).map(styleTag).join("")}${html}`; } diff --git a/views/hooks.ts b/views/hooks.ts index e8192c7..3c9fbf5 100644 --- a/views/hooks.ts +++ b/views/hooks.ts @@ -6,7 +6,9 @@ export interface SpugHookContext { locals: Record<string, unknown>; } -export type SpugExpansion = (context: SpugHookContext) => string | Promise<string>; +export type SpugExpansion = ( + context: SpugHookContext, +) => string | Promise<string>; export interface StcssValueHookContext { name: string; @@ -14,7 +16,9 @@ export interface StcssValueHookContext { resolve(value: string): string; } -export type StcssValueExpansion = (context: StcssValueHookContext) => string | undefined; +export type StcssValueExpansion = ( + context: StcssValueHookContext, +) => string | undefined; export interface StcssCalculationHookContext { name: string; @@ -22,7 +26,9 @@ export interface StcssCalculationHookContext { resolve(value: string): string; } -export type StcssCalculationExpansion = (context: StcssCalculationHookContext) => string | undefined; +export type StcssCalculationExpansion = ( + context: StcssCalculationHookContext, +) => string | undefined; export interface StcssDeclarationHookContext { selector: string; @@ -32,7 +38,9 @@ export interface StcssDeclarationHookContext { resolve(value: string): string; } -export type StcssDeclarationExpansion = (context: StcssDeclarationHookContext) => string[] | undefined; +export type StcssDeclarationExpansion = ( + context: StcssDeclarationHookContext, +) => string[] | undefined; export interface StcssAtRuleHookContext { name: string; @@ -44,7 +52,9 @@ export interface StcssAtRuleHookContext { renderChildren(): string; } -export type StcssAtRuleExpansion = (context: StcssAtRuleHookContext) => string | undefined; +export type StcssAtRuleExpansion = ( + context: StcssAtRuleHookContext, +) => string | undefined; /** * A contextual registry for transformations performed while Spug renders or @@ -54,7 +64,10 @@ export type StcssAtRuleExpansion = (context: StcssAtRuleHookContext) => string | export class MuddleHooks { private readonly spugExpansions = new Map<string, SpugExpansion>(); private readonly valueExpansions = new Map<string, StcssValueExpansion>(); - private readonly calculationExpansions = new Map<string, StcssCalculationExpansion>(); + private readonly calculationExpansions = new Map< + string, + StcssCalculationExpansion + >(); private readonly declarationExpansions: StcssDeclarationExpansion[] = []; private readonly atRuleExpansions = new Map<string, StcssAtRuleExpansion>(); @@ -91,22 +104,45 @@ export class MuddleHooks { return html; } - expandStcssValue(name: string, inner: string, resolve: (value: string) => string): string | undefined { + expandStcssValue( + name: string, + inner: string, + resolve: (value: string) => string, + ): string | undefined { return this.valueExpansions.get(name)?.({ name, inner, resolve }); } - expandStcssCalculation(name: string, args: string | undefined, resolve: (value: string) => string): string | undefined { - return this.calculationExpansions.get(name.replace(/^\$/, ""))?.({ name, args, resolve }); + expandStcssCalculation( + name: string, + args: string | undefined, + resolve: (value: string) => string, + ): string | undefined { + return this.calculationExpansions.get(name.replace(/^\$/, ""))?.({ + name, + args, + resolve, + }); } - expandStcssCalculationTokens(value: string, resolve: (value: string) => string): string { - return value.replace(/\$([A-Za-z][\w-]*)\b(?!\s*\()/g, (token, name: string) => - this.expandStcssCalculation(name, undefined, resolve) ?? token); + expandStcssCalculationTokens( + value: string, + resolve: (value: string) => string, + ): string { + return value.replace( + /\$([A-Za-z][\w-]*)\b(?!\s*\()/g, + (token, name: string) => + this.expandStcssCalculation(name, undefined, resolve) ?? token, + ); } - expandStcssDeclarations(context: Omit<StcssDeclarationHookContext, "declaration">): string[] { - return context.declarations.flatMap((declaration) => this.declarationExpansions.flatMap((expansion) => - expansion({ ...context, declaration }) ?? [])); + expandStcssDeclarations( + context: Omit<StcssDeclarationHookContext, "declaration">, + ): string[] { + return context.declarations.flatMap((declaration) => + this.declarationExpansions.flatMap( + (expansion) => expansion({ ...context, declaration }) ?? [], + ), + ); } expandStcssAtRule(context: StcssAtRuleHookContext): string | undefined { @@ -125,15 +161,28 @@ export interface HookBuilder { } /** Creates an application-local hook registry through contextual builders. */ -export function defineHooks(build: (hooks: HookBuilder) => void, defaults = true): MuddleHooks { +export function defineHooks( + build: (hooks: HookBuilder) => void, + defaults = true, +): MuddleHooks { const hooks = createHooks(defaults); build({ - spug: (name, expansion) => { hooks.spug(name, expansion); }, + spug: (name, expansion) => { + hooks.spug(name, expansion); + }, stcss: { - value: (name, expansion) => { hooks.stcssValue(name, expansion); }, - calculation: (name, expansion) => { hooks.stcssCalculation(name, expansion); }, - declaration: (expansion) => { hooks.stcssDeclaration(expansion); }, - atRule: (name, expansion) => { hooks.stcssAtRule(name, expansion); }, + value: (name, expansion) => { + hooks.stcssValue(name, expansion); + }, + calculation: (name, expansion) => { + hooks.stcssCalculation(name, expansion); + }, + declaration: (expansion) => { + hooks.stcssDeclaration(expansion); + }, + atRule: (name, expansion) => { + hooks.stcssAtRule(name, expansion); + }, }, }); return hooks; @@ -151,39 +200,69 @@ function registerBuiltinStcssExpansions(hooks: MuddleHooks): void { return size ? `calc(50% - (${resolve(size)} / 2))` : "50%"; }); hooks.stcssCalculation("offset", ({ args, resolve }) => { - const values = splitList(args ?? "").map((value) => resolve(value.trim())).filter(Boolean); + const values = splitList(args ?? "") + .map((value) => resolve(value.trim())) + .filter(Boolean); if (values.length === 0) return "0px"; return values.length === 1 ? values[0] : `calc(${values.join(" + ")})`; }); hooks.stcssCalculation("pad", ({ args, resolve }) => { const multiplier = args?.trim(); - return multiplier ? `calc(var(--muddle-pad, 1rem) * ${resolve(multiplier)})` : "var(--muddle-pad, 1rem)"; + return multiplier + ? `calc(var(--muddle-pad, 1rem) * ${resolve(multiplier)})` + : "var(--muddle-pad, 1rem)"; }); hooks.stcssValue("freeform-gradient", ({ inner, resolve }) => { return splitList(resolve(inner)) .filter((item) => item.trim().length > 0) .map((item) => { const tokens = gradientTokens(item.trim()); - if (tokens.length !== 3 && tokens.length !== 4) throw new Error(`Invalid freeform-gradient item "${item.trim()}"`); - const [color, x, y, radius] = tokens as [string, string, string, string?]; + if (tokens.length !== 3 && tokens.length !== 4) + throw new Error(`Invalid freeform-gradient item "${item.trim()}"`); + const [color, x, y, radius] = tokens as [ + string, + string, + string, + string?, + ]; return radius !== undefined ? `radial-gradient(circle at ${x} ${y}, ${color}, ${color} calc(${radius} * 0.97), transparent ${radius})` : `radial-gradient(circle farthest-side at ${x} ${y}, ${color}, transparent)`; }) .join(", "); }); - hooks.stcssDeclaration(({ selector, declaration, declarations, depth, resolve }) => { - if (declaration.property !== "border-radius" || !resolve(declaration.value).startsWith("concentric") || !selector) return undefined; - const outer = resolve(declaration.value).slice("concentric".length).trim() || "var(--concentric-radius, 1rem)"; - declaration.value = outer; - const spacing = concentricSpacing(declarations, resolve); - const childRadius = spacing.length > 0 ? `calc(${[outer, ...spacing].map((token) => `(${token})`).join(" - ")})` : "inherit"; - const indent = "\t".repeat(depth); - return [`${indent}${selector} > * {\n${indent}\tborder-radius: ${childRadius};\n${indent}}`]; - }); + hooks.stcssDeclaration( + ({ selector, declaration, declarations, depth, resolve }) => { + if ( + declaration.property !== "border-radius" || + !resolve(declaration.value).startsWith("concentric") || + !selector + ) + return undefined; + const outer = + resolve(declaration.value).slice("concentric".length).trim() || + "var(--concentric-radius, 1rem)"; + declaration.value = outer; + const spacing = concentricSpacing(declarations, resolve); + const childRadius = + spacing.length > 0 + ? `calc(${[outer, ...spacing].map((token) => `(${token})`).join(" - ")})` + : "inherit"; + const indent = "\t".repeat(depth); + return [ + `${indent}${selector} > * {\n${indent}\tborder-radius: ${childRadius};\n${indent}}`, + ]; + }, + ); hooks.stcssDeclaration(({ declaration, declarations }) => { - if (declaration.property !== "position" || (declaration.value !== "fixed-center" && declaration.value !== "page-center")) return undefined; - declaration.value = declaration.value === "fixed-center" ? "fixed" : "absolute"; + if ( + declaration.property !== "position" || + (declaration.value !== "fixed-center" && + declaration.value !== "page-center") + ) + return undefined; + declaration.value = + declaration.value === "fixed-center" ? "fixed" : "absolute"; const set = (property: string, value: string) => { const current = declarations.find((entry) => entry.property === property); if (current) current.value = value; @@ -191,9 +270,15 @@ function registerBuiltinStcssExpansions(hooks: MuddleHooks): void { }; set("left", "50%"); set("top", "50%"); - const transform = declarations.find((entry) => entry.property === "transform"); + const transform = declarations.find( + (entry) => entry.property === "transform", + ); if (transform) transform.value = `translate(-50%, -50%) ${transform.value}`; - else declarations.push({ property: "transform", value: "translate(-50%, -50%)" }); + else + declarations.push({ + property: "transform", + value: "translate(-50%, -50%)", + }); return undefined; }); hooks.stcssAtRule("concentric", ({ params, selector, depth, resolve }) => { @@ -204,12 +289,34 @@ function registerBuiltinStcssExpansions(hooks: MuddleHooks): void { }); } -function concentricSpacing(declarations: CssDeclaration[], resolve: (value: string) => string): string[] { - const find = (property: string): string | undefined => declarations - .filter((declaration) => declaration.property === property || declaration.property === `${property}-top` || declaration.property === `${property}-left` || declaration.property === `${property}-block-start` || declaration.property === `${property}-inline-start`) - .flatMap((declaration) => gradientTokens(resolve(declaration.value).trim())) - .find((value) => value.length > 0 && value !== "solid" && value !== "none" && value !== "0"); - const spacing = [find("padding"), find("border-width") ?? find("border")].filter((value): value is string => value !== undefined); +function concentricSpacing( + declarations: CssDeclaration[], + resolve: (value: string) => string, +): string[] { + const find = (property: string): string | undefined => + declarations + .filter( + (declaration) => + declaration.property === property || + declaration.property === `${property}-top` || + declaration.property === `${property}-left` || + declaration.property === `${property}-block-start` || + declaration.property === `${property}-inline-start`, + ) + .flatMap((declaration) => + gradientTokens(resolve(declaration.value).trim()), + ) + .find( + (value) => + value.length > 0 && + value !== "solid" && + value !== "none" && + value !== "0", + ); + const spacing = [ + find("padding"), + find("border-width") ?? find("border"), + ].filter((value): value is string => value !== undefined); return spacing; } diff --git a/views/integrations.ts b/views/integrations.ts index a4594ae..5820bbe 100644 --- a/views/integrations.ts +++ b/views/integrations.ts @@ -10,7 +10,10 @@ import { compileCss, type CompileOptions } from "./css/index.ts"; import { markdownLoader } from "./markdown.ts"; import { sassLoader, type SassLoaderOptions } from "./css/sass.ts"; -export { shadcnIntegration, type ShadcnIntegrationOptions } from "../integrations/shadcn.ts"; +export { + shadcnIntegration, + type ShadcnIntegrationOptions, +} from "../integrations/shadcn.ts"; export interface MarkdownIntegrationOptions { /** Name of the collection, defaults to `notes`. */ @@ -92,13 +95,16 @@ export function stcssIntegration( } const css = parts.join("\n"); - context.route("GET", route, () => - new Response(css, { - headers: { - "content-type": "text/css; charset=utf-8", - "cache-control": "public, max-age=3600", - }, - }), + context.route( + "GET", + route, + () => + new Response(css, { + headers: { + "content-type": "text/css; charset=utf-8", + "cache-control": "public, max-age=3600", + }, + }), ); }, }; @@ -116,8 +122,23 @@ export function sassIntegration(options: SassLoaderOptions = {}): Integration { if (!existsSync(dir)) return; const files = await sassFiles(dir); context.route("GET", route, async () => { - const css = await Promise.all(files.filter((file) => !file.split("/").at(-1)?.startsWith("_")).map(async (file) => (await loader.load({ path: join(dir, file), relativePath: `styles/${file}`, source: await readFile(join(dir, file), "utf8") })).content ?? "")); - return new Response(css.join("\n"), { headers: { "content-type": "text/css; charset=utf-8" } }); + const css = await Promise.all( + files + .filter((file) => !file.split("/").at(-1)?.startsWith("_")) + .map( + async (file) => + ( + await loader.load({ + path: join(dir, file), + relativePath: `styles/${file}`, + source: await readFile(join(dir, file), "utf8"), + }) + ).content ?? "", + ), + ); + return new Response(css.join("\n"), { + headers: { "content-type": "text/css; charset=utf-8" }, + }); }); }, }; @@ -127,7 +148,8 @@ async function sassFiles(dir: string, prefix = ""): Promise<string[]> { const files: string[] = []; for (const entry of await readdir(dir, { withFileTypes: true })) { const relative = `${prefix}${entry.name}`; - if (entry.isDirectory()) files.push(...await sassFiles(join(dir, entry.name), `${relative}/`)); + if (entry.isDirectory()) + files.push(...(await sassFiles(join(dir, entry.name), `${relative}/`))); else if (/\.s[ac]ss$/i.test(entry.name)) files.push(relative); } return files.sort(); diff --git a/views/language.ts b/views/language.ts index 73e8820..f889146 100644 --- a/views/language.ts +++ b/views/language.ts @@ -1,24 +1,93 @@ -export interface LanguageCompletion { label: string; detail: string; insertText: string } +export interface LanguageCompletion { + label: string; + detail: string; + insertText: string; +} export const SPUG_COMPLETIONS: LanguageCompletion[] = [ - { label: "props", detail: "Typed component props", insertText: "props\n ${1:name}: ${2:string}" }, - { label: "javascript!", detail: "Embedded TypeScript block", insertText: "javascript!\n ${1:const value = true;}\nend" }, - { label: "component", detail: "SPUG component", insertText: "component ${1:Name}" }, - { label: "portal", detail: "Named view-transition boundary", insertText: "portal ${1:main}\n ${0}" }, - { label: "anchor", detail: "Relation identity", insertText: "${1:li}@${2:item}() ${3:content}" }, + { + label: "props", + detail: "Typed component props", + insertText: "props\n ${1:name}: ${2:string}", + }, + { + label: "javascript!", + detail: "Embedded TypeScript block", + insertText: "javascript!\n ${1:const value = true;}\nend", + }, + { + label: "component", + detail: "SPUG component", + insertText: "component ${1:Name}", + }, + { + label: "portal", + detail: "Named view-transition boundary", + insertText: "portal ${1:main}\n ${0}", + }, + { + label: "anchor", + detail: "Relation identity", + insertText: "${1:li}@${2:item}() ${3:content}", + }, ]; export const STCSS_COMPLETIONS: LanguageCompletion[] = [ - { label: "@responsive", detail: "Mobile-first breakpoint", insertText: "@responsive ${1:md} {\n ${2}\n}" }, - { label: "@concentric", detail: "Inherited child radius", insertText: "@concentric ${1:1rem};" }, - { label: "border-radius: concentric", detail: "Concentric radii for children", insertText: "border-radius: concentric ${1:1rem};" }, - { label: "freeform-gradient", detail: "Stacked radial gradients", insertText: "background-image: freeform-gradient(${1:#7c3aed} ${2:10%} ${3:50%}, ${4:f00} ${5:70%} ${6:40%});" }, - { label: "$center", detail: "Center a known size", insertText: "$center(${1:30rem})" }, - { label: "$offset", detail: "Compose positional offsets", insertText: "$offset(${1:$center(30rem)}, ${2:$pad})" }, + { + label: "@responsive", + detail: "Mobile-first breakpoint", + insertText: "@responsive ${1:md} {\n ${2}\n}", + }, + { + label: "@concentric", + detail: "Inherited child radius", + insertText: "@concentric ${1:1rem};", + }, + { + label: "border-radius: concentric", + detail: "Concentric radii for children", + insertText: "border-radius: concentric ${1:1rem};", + }, + { + label: "freeform-gradient", + detail: "Stacked radial gradients", + insertText: + "background-image: freeform-gradient(${1:#7c3aed} ${2:10%} ${3:50%}, ${4:f00} ${5:70%} ${6:40%});", + }, + { + label: "$center", + detail: "Center a known size", + insertText: "$center(${1:30rem})", + }, + { + label: "$offset", + detail: "Compose positional offsets", + insertText: "$offset(${1:$center(30rem)}, ${2:$pad})", + }, { label: "$pad", detail: "Shared spacing token", insertText: "$pad(${1:1})" }, - { label: "position: fixed-center", detail: "Fixed viewport center", insertText: "position: fixed-center;" }, - { label: "position: page-center", detail: "Absolute containing-block center", insertText: "position: page-center;" }, - { label: "@predict", detail: "Prerendered geometry", insertText: "@predict max-width max-height transform;" }, - { label: "$relate", detail: "Predicted anchor metric", insertText: "$relate[${1:width}](d\"@${2:item}\")" }, - { label: "javascript!", detail: "Compile-time values", insertText: "javascript!\n const ${1:space} = ${2:'1rem'};\nend" }, + { + label: "position: fixed-center", + detail: "Fixed viewport center", + insertText: "position: fixed-center;", + }, + { + label: "position: page-center", + detail: "Absolute containing-block center", + insertText: "position: page-center;", + }, + { + label: "@predict", + detail: "Prerendered geometry", + insertText: "@predict max-width max-height transform;", + }, + { + label: "$relate", + detail: "Predicted anchor metric", + insertText: '$relate[${1:width}](d"@${2:item}")', + }, + { + label: "javascript!", + detail: "Compile-time values", + insertText: "javascript!\n const ${1:space} = ${2:'1rem'};\nend", + }, ]; diff --git a/views/markdown.ts b/views/markdown.ts index acb8171..847e743 100644 --- a/views/markdown.ts +++ b/views/markdown.ts @@ -107,12 +107,17 @@ export function renderMarkdown(source: string): string { if (/^\s*\$\$\s*$/.test(line)) { const formula: string[] = []; index += 1; - while (index < lines.length && !/^\s*\$\$\s*$/.test(lines[index] as string)) { + while ( + index < lines.length && + !/^\s*\$\$\s*$/.test(lines[index] as string) + ) { formula.push(lines[index] as string); index += 1; } if (index < lines.length) index += 1; - output.push(`<div class="muddle-math">${renderLatex(formula.join("\n"), true)}</div>`); + output.push( + `<div class="muddle-math">${renderLatex(formula.join("\n"), true)}</div>`, + ); continue; } @@ -139,12 +144,17 @@ export function renderMarkdown(source: string): string { if (/^\s*@@@\s*$/.test(line)) { const graph: string[] = []; index += 1; - while (index < lines.length && !/^\s*@@@\s*$/.test(lines[index] as string)) { + while ( + index < lines.length && + !/^\s*@@@\s*$/.test(lines[index] as string) + ) { graph.push(lines[index] as string); index += 1; } index += 1; - output.push(`<pre class="mermaid" data-muddle-animate="true">${escapeHtml(graph.join("\n"))}</pre>`); + output.push( + `<pre class="mermaid" data-muddle-animate="true">${escapeHtml(graph.join("\n"))}</pre>`, + ); continue; } @@ -242,12 +252,17 @@ export function renderInline(source: string): string { text = text.replace(/\uE000(\d+)\uE000/g, (_match, index: string) => { return `<code>${escapeHtml(codes[Number(index)] ?? "")}</code>`; }); - return text.replace(/\uE001(\d+)\uE001/g, (_match, index: string) => maths[Number(index)] ?? ""); + return text.replace( + /\uE001(\d+)\uE001/g, + (_match, index: string) => maths[Number(index)] ?? "", + ); } /** Renders a trusted LaTex formula to KaTeX HTML and MathML. */ export function renderLatex(source: string, displayMode = false): string { - return escapeHtml(`${displayMode ? "$$" : "$"}${source}${displayMode ? "$$" : "$"}`); + return escapeHtml( + `${displayMode ? "$$" : "$"}${source}${displayMode ? "$$" : "$"}`, + ); } export interface MarkdownLoaderOptions { diff --git a/views/raw.ts b/views/raw.ts index d4dcc49..214cd33 100644 --- a/views/raw.ts +++ b/views/raw.ts @@ -74,12 +74,15 @@ export function cssPropertyName(property: string): string { */ export function styleValue(value: unknown): string { if (value === null || value === undefined || value === false) return ""; - if (typeof value === "string" || typeof value === "number") return String(value); + if (typeof value === "string" || typeof value === "number") + return String(value); if (isRaw(value)) return value.html; if (typeof value !== "object") return ""; const parts: string[] = []; - for (const [property, entry] of Object.entries(value as Record<string, unknown>)) { + for (const [property, entry] of Object.entries( + value as Record<string, unknown>, + )) { if (entry === null || entry === undefined || entry === false) continue; const name = cssPropertyName(property); if (typeof entry === "number") { diff --git a/views/stage.ts b/views/stage.ts index 00a784b..26f1b81 100644 --- a/views/stage.ts +++ b/views/stage.ts @@ -22,7 +22,12 @@ export interface TransitionPortalOptions { } function declarations(values: Record<string, string | number>): string { - return Object.entries(values).map(([property, value]) => `${property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}: ${value};`).join(" "); + return Object.entries(values) + .map( + ([property, value]) => + `${property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}: ${value};`, + ) + .join(" "); } /** Builds declarative native CSS scenes and view transitions without a client runtime. */ @@ -35,7 +40,10 @@ export class StageManager { return this; } - portal(name: string, options: Omit<TransitionPortalOptions, "name"> = {}): this { + portal( + name: string, + options: Omit<TransitionPortalOptions, "name"> = {}, + ): this { this.portals.push({ name, ...options }); return this; } @@ -46,18 +54,27 @@ export class StageManager { const animation = scene.animation ?? "muddle-scene"; const timeline = scene.timeline ?? "view"; return [ - `@keyframes ${animation} { ${Object.entries(scene.keyframes).map(([offset, step]) => { - const values = "to" in step && typeof step.to === "object" ? step.to : step as Record<string, string | number>; - return `${offset} { ${declarations(values)} }`; - }).join(" ")} }`, + `@keyframes ${animation} { ${Object.entries(scene.keyframes) + .map(([offset, step]) => { + const values = + "to" in step && typeof step.to === "object" + ? step.to + : (step as Record<string, string | number>); + return `${offset} { ${declarations(values)} }`; + }) + .join(" ")} }`, `${scene.selector} { animation-name: ${animation}; animation-duration: ${scene.duration ?? "1ms"}; animation-timing-function: ${scene.easing ?? "linear"}; animation-fill-mode: ${scene.fill ?? "both"}; animation-timeline: ${timeline === "view" ? "view()" : timeline === "scroll" ? "scroll()" : timeline}; animation-range: ${scene.range ?? "entry 0% cover 100%"}; }`, ]; }), ...this.portals.flatMap((portal) => { const timing = `animation-duration: ${portal.duration ?? "240ms"}; animation-timing-function: ${portal.easing ?? "ease"};`; return [ - portal.old ? `::view-transition-old(${portal.name}) { ${timing} ${declarations(portal.old)} }` : "", - portal.new ? `::view-transition-new(${portal.name}) { ${timing} ${declarations(portal.new)} }` : "", + portal.old + ? `::view-transition-old(${portal.name}) { ${timing} ${declarations(portal.old)} }` + : "", + portal.new + ? `::view-transition-new(${portal.name}) { ${timing} ${declarations(portal.new)} }` + : "", ].filter(Boolean); }), ].join("\n"); diff --git a/views/testing.ts b/views/testing.ts index 498f7a9..e45660a 100644 --- a/views/testing.ts +++ b/views/testing.ts @@ -229,10 +229,7 @@ export async function runTemplateTests( try { const page = await browser.newPage(); for (const test of tests) { - const html = await engine.render( - test.path, - options.locals ?? {}, - ); + const html = await engine.render(test.path, options.locals ?? {}); await page.setContent(html, { waitUntil: "load" }); try { await page.evaluate(