better formatting
This commit is contained in:
parent
f18a8fd409
commit
8d0d645487
188 changed files with 11503 additions and 6286 deletions
|
|
@ -24,10 +24,21 @@ export interface WranglerSetupOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Creates a minimal Wrangler config when a project has not supplied one. */
|
/** Creates a minimal Wrangler config when a project has not supplied one. */
|
||||||
export async function setupWrangler(root: string, options: WranglerSetupOptions = {}): Promise<string> {
|
export async function setupWrangler(
|
||||||
|
root: string,
|
||||||
|
options: WranglerSetupOptions = {},
|
||||||
|
): Promise<string> {
|
||||||
const path = join(resolve(root), "wrangler.jsonc");
|
const path = join(resolve(root), "wrangler.jsonc");
|
||||||
if (existsSync(path)) return path;
|
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");
|
await writeFile(path, source, "utf8");
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,9 @@ export interface ReflectiveAnchorGroup {
|
||||||
const measureScript = (input: { remeasure: boolean; selectors?: string[] }) => {
|
const measureScript = (input: { remeasure: boolean; selectors?: string[] }) => {
|
||||||
const { remeasure, selectors = [] } = input;
|
const { remeasure, selectors = [] } = input;
|
||||||
const document = (globalThis as unknown as { document: any }).document;
|
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 = {
|
type Entry = {
|
||||||
selector: string;
|
selector: string;
|
||||||
index: number;
|
index: number;
|
||||||
|
|
@ -89,15 +91,26 @@ const measureScript = (input: { remeasure: boolean; selectors?: string[] }) => {
|
||||||
"transition",
|
"transition",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const propertyValue = (element: any, style: any, property: string): string => {
|
const propertyValue = (
|
||||||
|
element: any,
|
||||||
|
style: any,
|
||||||
|
property: string,
|
||||||
|
): string => {
|
||||||
switch (property) {
|
switch (property) {
|
||||||
case "scroll-height": return `${element.scrollHeight}px`;
|
case "scroll-height":
|
||||||
case "scroll-width": return `${element.scrollWidth}px`;
|
return `${element.scrollHeight}px`;
|
||||||
case "client-height": return `${element.clientHeight}px`;
|
case "scroll-width":
|
||||||
case "client-width": return `${element.clientWidth}px`;
|
return `${element.scrollWidth}px`;
|
||||||
case "offset-height": return `${element.offsetHeight}px`;
|
case "client-height":
|
||||||
case "offset-width": return `${element.offsetWidth}px`;
|
return `${element.clientHeight}px`;
|
||||||
default: return style.getPropertyValue(property) ?? "";
|
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) {
|
if (!remeasure) {
|
||||||
for (const element of anchors) {
|
for (const element of anchors) {
|
||||||
const name = element.getAttribute("data-muddle-anchor") as string | null;
|
const name = element.getAttribute("data-muddle-anchor") as string | null;
|
||||||
if (!name?.includes(".")) continue;
|
if (!name?.includes(".")) continue;
|
||||||
const parts = name.split(".");
|
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(" "));
|
element.setAttribute("data-muddle-group", groups.join(" "));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -139,11 +156,16 @@ const measureScript = (input: { remeasure: boolean; selectors?: string[] }) => {
|
||||||
const needle = `--muddle-relate-${key}-`;
|
const needle = `--muddle-relate-${key}-`;
|
||||||
let index = text.indexOf(needle);
|
let index = text.indexOf(needle);
|
||||||
while (index !== -1) {
|
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)) {
|
if (property && !requests.get(key)?.includes(property)) {
|
||||||
requests.set(key, [...(requests.get(key) ?? []), 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) {
|
if (!remeasure) {
|
||||||
const selected = new Set<any>();
|
const selected = new Set<any>();
|
||||||
for (const selector of selectors) {
|
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()) {
|
for (const [index, element] of [...selected].entries()) {
|
||||||
element.setAttribute("data-muddle-predicted", `predict-${index}`);
|
element.setAttribute("data-muddle-predicted", `predict-${index}`);
|
||||||
|
|
@ -160,7 +183,9 @@ const measureScript = (input: { remeasure: boolean; selectors?: string[] }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const entries: Entry[] = [];
|
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 box = element.getBoundingClientRect();
|
||||||
const style = getComputedStyle(element);
|
const style = getComputedStyle(element);
|
||||||
const entry: Entry = {
|
const entry: Entry = {
|
||||||
|
|
@ -197,7 +222,10 @@ const measureScript = (input: { remeasure: boolean; selectors?: string[] }) => {
|
||||||
return entries;
|
return entries;
|
||||||
};
|
};
|
||||||
|
|
||||||
function predictionVariables(prediction: LayoutPrediction, variant: string): string {
|
function predictionVariables(
|
||||||
|
prediction: LayoutPrediction,
|
||||||
|
variant: string,
|
||||||
|
): string {
|
||||||
return [
|
return [
|
||||||
`${prediction.selector} {`,
|
`${prediction.selector} {`,
|
||||||
`--muddle-predicted-${variant}-width: ${prediction.width}px;`,
|
`--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
|
* The suffixed variables hold the raw measurement of the variant, the plain
|
||||||
* alias resolves to whichever variant applies at the current viewport.
|
* 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 "";
|
if (!prediction.anchor) return "";
|
||||||
const key = prediction.anchor.replace(/[^a-zA-Z0-9_-]/g, "_");
|
const key = prediction.anchor.replace(/[^a-zA-Z0-9_-]/g, "_");
|
||||||
const prefix = `--muddle-relate-${key}-${variant}`;
|
const prefix = `--muddle-relate-${key}-${variant}`;
|
||||||
|
|
@ -227,21 +258,33 @@ function relationVariables(prediction: LayoutPrediction, variant: string): strin
|
||||||
`${prefix}-y: ${prediction.y}px`,
|
`${prefix}-y: ${prediction.y}px`,
|
||||||
`${prefix}-width: ${prediction.width}px`,
|
`${prefix}-width: ${prediction.width}px`,
|
||||||
`${prefix}-height: ${prediction.height}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.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}-x: var(${prefix}-x)`,
|
||||||
`--muddle-relate-${key}-y: var(${prefix}-y)`,
|
`--muddle-relate-${key}-y: var(${prefix}-y)`,
|
||||||
`--muddle-relate-${key}-width: var(${prefix}-width)`,
|
`--muddle-relate-${key}-width: var(${prefix}-width)`,
|
||||||
`--muddle-relate-${key}-height: var(${prefix}-height)`,
|
`--muddle-relate-${key}-height: var(${prefix}-height)`,
|
||||||
prediction.backgroundColor ? `--muddle-relate-${key}-background-color: var(${prefix}-background-color)` : "",
|
prediction.backgroundColor
|
||||||
prediction.color ? `--muddle-relate-${key}-color: var(${prefix}-color)` : "",
|
? `--muddle-relate-${key}-background-color: var(${prefix}-background-color)`
|
||||||
prediction.transition ? `--muddle-relate-${key}-transition: var(${prefix}-transition)` : "",
|
: "",
|
||||||
|
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]) => [
|
.concat(
|
||||||
`${prefix}-${property}: ${value}`,
|
Object.entries(prediction.relate ?? {}).flatMap(([property, value]) => [
|
||||||
`--muddle-relate-${key}-${property}: var(${prefix}-${property})`,
|
`${prefix}-${property}: ${value}`,
|
||||||
]))
|
`--muddle-relate-${key}-${property}: var(${prefix}-${property})`,
|
||||||
|
]),
|
||||||
|
)
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
return `:root {\n\t${values.join(";\n")};\n}`;
|
return `:root {\n\t${values.join(";\n")};\n}`;
|
||||||
}
|
}
|
||||||
|
|
@ -256,7 +299,9 @@ function selfVariables(prediction: LayoutPrediction): string {
|
||||||
if (!prediction.anchor) return "";
|
if (!prediction.anchor) return "";
|
||||||
const entries = Object.entries(prediction.self ?? {});
|
const entries = Object.entries(prediction.self ?? {});
|
||||||
if (entries.length === 0) return "";
|
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}`;
|
return `[data-muddle-anchor="${prediction.anchor}"] {\n\t${values.join(";\n")};\n}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -267,7 +312,9 @@ interface PredictionLayer {
|
||||||
predictions: LayoutPrediction[];
|
predictions: LayoutPrediction[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function reflectiveAnchorGroups(predictions: LayoutPrediction[]): ReflectiveAnchorGroup[] {
|
function reflectiveAnchorGroups(
|
||||||
|
predictions: LayoutPrediction[],
|
||||||
|
): ReflectiveAnchorGroup[] {
|
||||||
const groups = new Map<string, string[]>();
|
const groups = new Map<string, string[]>();
|
||||||
for (const prediction of predictions) {
|
for (const prediction of predictions) {
|
||||||
const anchor = prediction.anchor;
|
const anchor = prediction.anchor;
|
||||||
|
|
@ -288,31 +335,48 @@ function reflectionCss(groups: ReflectiveAnchorGroup[]): string[] {
|
||||||
const key = group.name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
const key = group.name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
||||||
return [
|
return [
|
||||||
`[data-muddle-group~="${group.name}"] { --muddle-reflect-${key}-count: ${group.anchors.length}; }`,
|
`[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 {
|
function predictionCss(
|
||||||
const rules = (predictions: LayoutPrediction[], variant: string) => predictions.flatMap((entry) => [
|
desktop: LayoutPrediction[],
|
||||||
predictionVariables(entry, variant),
|
layers: PredictionLayer[],
|
||||||
relationVariables(entry, variant),
|
groups: ReflectiveAnchorGroup[],
|
||||||
selfVariables(entry),
|
): string {
|
||||||
]);
|
const rules = (predictions: LayoutPrediction[], variant: string) =>
|
||||||
|
predictions.flatMap((entry) => [
|
||||||
|
predictionVariables(entry, variant),
|
||||||
|
relationVariables(entry, variant),
|
||||||
|
selfVariables(entry),
|
||||||
|
]);
|
||||||
return [
|
return [
|
||||||
...reflectionCss(groups),
|
...reflectionCss(groups),
|
||||||
"@media (min-width: 0) {",
|
"@media (min-width: 0) {",
|
||||||
...rules(desktop, "desktop"),
|
...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)
|
.filter((line) => line.trim().length > 0)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
function mobileMediaQuery(html: string, explicit?: string): string {
|
function mobileMediaQuery(html: string, explicit?: string): string {
|
||||||
if (explicit) return explicit.trim().startsWith("(") ? explicit.trim() : `(${explicit.trim()})`;
|
if (explicit)
|
||||||
const candidates = [...html.matchAll(/@media\s*\(\s*max-width\s*:\s*([^)]+)\)/gi)]
|
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())
|
.map((match) => match[1]!.trim())
|
||||||
.sort((left, right) => mediaWidth(right) - mediaWidth(left));
|
.sort((left, right) => mediaWidth(right) - mediaWidth(left));
|
||||||
return candidates[0] ? `(max-width: ${candidates[0]})` : "(max-width: 48rem)";
|
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);
|
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 match = /max-width\s*:\s*([^)\s]+)/i.exec(query);
|
||||||
const width = match ? mediaWidth(match[1]!) : 0;
|
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<BreakpointAcc, { width: number; media: string }> = {
|
const BREAKPOINTS: Record<BreakpointAcc, { width: number; media: string }> = {
|
||||||
|
|
@ -338,9 +410,18 @@ const BREAKPOINTS: Record<BreakpointAcc, { width: number; media: string }> = {
|
||||||
SM: { width: 640, media: "(max-width: 40rem)" },
|
SM: { width: 640, media: "(max-width: 40rem)" },
|
||||||
};
|
};
|
||||||
|
|
||||||
function predictionLayers(options: LayoutPredictionOptions, html: string): Omit<PredictionLayer, "predictions">[] {
|
function predictionLayers(
|
||||||
|
options: LayoutPredictionOptions,
|
||||||
|
html: string,
|
||||||
|
): Omit<PredictionLayer, "predictions">[] {
|
||||||
const requested = options.breakpointAcc
|
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) {
|
if (requested.length) {
|
||||||
return requested
|
return requested
|
||||||
|
|
@ -352,14 +433,28 @@ function predictionLayers(options: LayoutPredictionOptions, html: string): Omit<
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
const media = mobileMediaQuery(html, options.mobileMediaQuery);
|
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. */
|
/** Reads the `@measure` targets emitted into stylesheets by the STCSS compiler. */
|
||||||
export function extractPredictSelectors(html: string): string[] {
|
export function extractPredictSelectors(html: string): string[] {
|
||||||
return [...html.matchAll(/\/\*\s*muddle-predict\s*:([\s\S]*?)\*\//g)].flatMap((match) => (match[1]
|
return [...html.matchAll(/\/\*\s*muddle-predict\s*:([\s\S]*?)\*\//g)].flatMap(
|
||||||
? match[1].split(/[|,]/).map((selector) => selector.trim()).filter(Boolean)
|
(match) =>
|
||||||
: []));
|
match[1]
|
||||||
|
? match[1]
|
||||||
|
.split(/[|,]/)
|
||||||
|
.map((selector) => selector.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
: [],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -370,30 +465,69 @@ export function extractPredictSelectors(html: string): string[] {
|
||||||
* `<style>` block in the `<head>` of the returned page, so the shipped bundle
|
* `<style>` block in the `<head>` of the returned page, so the shipped bundle
|
||||||
* contains no scripting at all.
|
* contains no scripting at all.
|
||||||
*/
|
*/
|
||||||
export async function predictLayout(html: string, options: LayoutPredictionOptions): Promise<{ html: string; predictions: LayoutPrediction[]; groups: ReflectiveAnchorGroup[] }> {
|
export async function predictLayout(
|
||||||
|
html: string,
|
||||||
|
options: LayoutPredictionOptions,
|
||||||
|
): Promise<{
|
||||||
|
html: string;
|
||||||
|
predictions: LayoutPrediction[];
|
||||||
|
groups: ReflectiveAnchorGroup[];
|
||||||
|
}> {
|
||||||
let browser: import("playwright").Browser | undefined;
|
let browser: import("playwright").Browser | undefined;
|
||||||
try {
|
try {
|
||||||
const { chromium } = await import("playwright");
|
const { chromium } = await import("playwright");
|
||||||
browser = await chromium.launch({ headless: true });
|
browser = await chromium.launch({ headless: true });
|
||||||
const page = await browser.newPage({ viewport: options.viewport ?? { width: 1440, height: 900 } });
|
const page = await browser.newPage({
|
||||||
|
viewport: options.viewport ?? { width: 1440, height: 900 },
|
||||||
|
});
|
||||||
await page.setContent(html, { waitUntil: "networkidle" });
|
await page.setContent(html, { waitUntil: "networkidle" });
|
||||||
const layers = predictionLayers(options, html);
|
const layers = predictionLayers(options, html);
|
||||||
|
|
||||||
// Disclosure bodies are measured in their open state.
|
// Disclosure bodies are measured in their open state.
|
||||||
const openStates = await page.locator("details").evaluateAll((elements) => elements.map((element) => (element as unknown as { hasAttribute(name: string): boolean }).hasAttribute("open")));
|
const openStates = await page
|
||||||
|
.locator("details")
|
||||||
|
.evaluateAll((elements) =>
|
||||||
|
elements.map((element) =>
|
||||||
|
(
|
||||||
|
element as unknown as { hasAttribute(name: string): boolean }
|
||||||
|
).hasAttribute("open"),
|
||||||
|
),
|
||||||
|
);
|
||||||
await page.locator("details").evaluateAll((elements) => {
|
await page.locator("details").evaluateAll((elements) => {
|
||||||
for (const element of elements) (element as unknown as { setAttribute(name: string, value: string): void }).setAttribute("open", "");
|
for (const element of elements)
|
||||||
|
(
|
||||||
|
element as unknown as {
|
||||||
|
setAttribute(name: string, value: string): void;
|
||||||
|
}
|
||||||
|
).setAttribute("open", "");
|
||||||
});
|
});
|
||||||
|
|
||||||
const selected = [...new Set([...(options.selectors ?? []), ...extractPredictSelectors(html), "[data-muddle-anchor]"])];
|
const selected = [
|
||||||
|
...new Set([
|
||||||
|
...(options.selectors ?? []),
|
||||||
|
...extractPredictSelectors(html),
|
||||||
|
"[data-muddle-anchor]",
|
||||||
|
]),
|
||||||
|
];
|
||||||
// First collect the independent anchor geometry. Relation-driven targets
|
// First collect the independent anchor geometry. Relation-driven targets
|
||||||
// cannot have a meaningful size until those values exist in the page.
|
// cannot have a meaningful size until those values exist in the page.
|
||||||
const initialDesktop = await page.evaluate(measureScript, { remeasure: false, selectors: selected }) as LayoutPrediction[];
|
const initialDesktop = (await page.evaluate(measureScript, {
|
||||||
const groups = options.reflective === false ? [] : reflectiveAnchorGroups(initialDesktop);
|
remeasure: false,
|
||||||
|
selectors: selected,
|
||||||
|
})) as LayoutPrediction[];
|
||||||
|
const groups =
|
||||||
|
options.reflective === false
|
||||||
|
? []
|
||||||
|
: reflectiveAnchorGroups(initialDesktop);
|
||||||
const initialLayers: PredictionLayer[] = [];
|
const initialLayers: PredictionLayer[] = [];
|
||||||
for (const layer of layers) {
|
for (const layer of layers) {
|
||||||
await page.setViewportSize(layer.viewport);
|
await page.setViewportSize(layer.viewport);
|
||||||
initialLayers.push({ ...layer, predictions: await page.evaluate(measureScript, { remeasure: true }) as LayoutPrediction[] });
|
initialLayers.push({
|
||||||
|
...layer,
|
||||||
|
predictions: (await page.evaluate(measureScript, {
|
||||||
|
remeasure: true,
|
||||||
|
})) as LayoutPrediction[],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Propagate the measurements until the emitted relation stylesheet stops
|
// Propagate the measurements until the emitted relation stylesheet stops
|
||||||
|
|
@ -405,12 +539,21 @@ export async function predictLayout(html: string, options: LayoutPredictionOptio
|
||||||
let settled = false;
|
let settled = false;
|
||||||
for (let pass = 0; pass < (options.maxPasses ?? 8); pass++) {
|
for (let pass = 0; pass < (options.maxPasses ?? 8); pass++) {
|
||||||
const provisional = await page.addStyleTag({ content: css });
|
const provisional = await page.addStyleTag({ content: css });
|
||||||
await page.setViewportSize(options.viewport ?? { width: 1440, height: 900 });
|
await page.setViewportSize(
|
||||||
const nextDesktop = await page.evaluate(measureScript, { remeasure: true }) as LayoutPrediction[];
|
options.viewport ?? { width: 1440, height: 900 },
|
||||||
|
);
|
||||||
|
const nextDesktop = (await page.evaluate(measureScript, {
|
||||||
|
remeasure: true,
|
||||||
|
})) as LayoutPrediction[];
|
||||||
const nextLayers: PredictionLayer[] = [];
|
const nextLayers: PredictionLayer[] = [];
|
||||||
for (const layer of measuredLayers) {
|
for (const layer of measuredLayers) {
|
||||||
await page.setViewportSize(layer.viewport);
|
await page.setViewportSize(layer.viewport);
|
||||||
nextLayers.push({ ...layer, predictions: await page.evaluate(measureScript, { remeasure: true }) as LayoutPrediction[] });
|
nextLayers.push({
|
||||||
|
...layer,
|
||||||
|
predictions: (await page.evaluate(measureScript, {
|
||||||
|
remeasure: true,
|
||||||
|
})) as LayoutPrediction[],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
await provisional.evaluate((element) => element.remove());
|
await provisional.evaluate((element) => element.remove());
|
||||||
const nextCss = predictionCss(nextDesktop, nextLayers, groups);
|
const nextCss = predictionCss(nextDesktop, nextLayers, groups);
|
||||||
|
|
@ -422,19 +565,34 @@ export async function predictLayout(html: string, options: LayoutPredictionOptio
|
||||||
}
|
}
|
||||||
css = nextCss;
|
css = nextCss;
|
||||||
}
|
}
|
||||||
if (!settled) throw new Error(`relation prediction did not settle within ${options.maxPasses ?? 8} passes`);
|
if (!settled)
|
||||||
|
throw new Error(
|
||||||
|
`relation prediction did not settle within ${options.maxPasses ?? 8} passes`,
|
||||||
|
);
|
||||||
css = predictionCss(desktop, measuredLayers, groups);
|
css = predictionCss(desktop, measuredLayers, groups);
|
||||||
|
|
||||||
await page.locator("details").evaluateAll((elements, states) => {
|
await page.locator("details").evaluateAll((elements, states) => {
|
||||||
elements.forEach((element, index) => {
|
elements.forEach((element, index) => {
|
||||||
if (!states[index]) (element as unknown as { removeAttribute(name: string): void }).removeAttribute("open");
|
if (!states[index])
|
||||||
|
(
|
||||||
|
element as unknown as { removeAttribute(name: string): void }
|
||||||
|
).removeAttribute("open");
|
||||||
});
|
});
|
||||||
}, openStates);
|
}, openStates);
|
||||||
const rendered = await page.content();
|
const rendered = await page.content();
|
||||||
return { html: rendered.replace("</head>", `<style>${css}</style></head>`), predictions: [...desktop, ...measuredLayers.flatMap((layer) => layer.predictions)], groups };
|
return {
|
||||||
|
html: rendered.replace("</head>", `<style>${css}</style></head>`),
|
||||||
|
predictions: [
|
||||||
|
...desktop,
|
||||||
|
...measuredLayers.flatMap((layer) => layer.predictions),
|
||||||
|
],
|
||||||
|
groups,
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (options.strict) throw 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: [] };
|
return { html, predictions: [], groups: [] };
|
||||||
} finally {
|
} finally {
|
||||||
await browser?.close();
|
await browser?.close();
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,8 @@ export function serve(app: App, options: BunServerOptions = {}) {
|
||||||
const asset = await serveAsset(request, publicDir);
|
const asset = await serveAsset(request, publicDir);
|
||||||
if (asset) return asset;
|
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;
|
const key = request.url;
|
||||||
let predicted = rendered.get(key);
|
let predicted = rendered.get(key);
|
||||||
if (!predicted) {
|
if (!predicted) {
|
||||||
|
|
@ -58,14 +59,28 @@ export function serve(app: App, options: BunServerOptions = {}) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderPredictedView(app: App, request: Request): Promise<Response> {
|
async function renderPredictedView(
|
||||||
|
app: App,
|
||||||
|
request: Request,
|
||||||
|
): Promise<Response> {
|
||||||
const response = await app.fetch(request);
|
const response = await app.fetch(request);
|
||||||
const contentType = response.headers.get("content-type") ?? "";
|
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 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);
|
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 });
|
const predicted = await predictLayout(assetHtml, { strict: true });
|
||||||
return responseFromText(predicted.html, response);
|
return responseFromText(predicted.html, response);
|
||||||
}
|
}
|
||||||
|
|
@ -73,7 +88,11 @@ async function renderPredictedView(app: App, request: Request): Promise<Response
|
||||||
function responseFromText(html: string, source: Response): Response {
|
function responseFromText(html: string, source: Response): Response {
|
||||||
const headers = new Headers(source.headers);
|
const headers = new Headers(source.headers);
|
||||||
headers.delete("content-length");
|
headers.delete("content-length");
|
||||||
return new Response(html, { status: source.status, statusText: source.statusText, headers });
|
return new Response(html, {
|
||||||
|
status: source.status,
|
||||||
|
statusText: source.statusText,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloneResponse(source: CachedResponse): Response {
|
function cloneResponse(source: CachedResponse): Response {
|
||||||
|
|
@ -101,10 +120,16 @@ export async function serveAsset(
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
if (request.method !== "GET" && request.method !== "HEAD") return undefined;
|
if (request.method !== "GET" && request.method !== "HEAD") return undefined;
|
||||||
|
|
||||||
const requested = resolve(publicDir, decodeURIComponent(url.pathname).replace(/^[/\\]+/, ""));
|
const requested = resolve(
|
||||||
|
publicDir,
|
||||||
|
decodeURIComponent(url.pathname).replace(/^[/\\]+/, ""),
|
||||||
|
);
|
||||||
const relativePath = relative(publicDir, requested);
|
const relativePath = relative(publicDir, requested);
|
||||||
if (relativePath.startsWith("..") || isAbsolute(relativePath)) return undefined;
|
if (relativePath.startsWith("..") || isAbsolute(relativePath))
|
||||||
const path = existsSync(join(requested, "index.html")) ? join(requested, "index.html") : requested;
|
return undefined;
|
||||||
|
const path = existsSync(join(requested, "index.html"))
|
||||||
|
? join(requested, "index.html")
|
||||||
|
: requested;
|
||||||
if (!existsSync(path)) return undefined;
|
if (!existsSync(path)) return undefined;
|
||||||
|
|
||||||
let file: Uint8Array;
|
let file: Uint8Array;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,10 @@ import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
||||||
import { dirname, join, resolve } from "node:path";
|
import { dirname, join, resolve } from "node:path";
|
||||||
import type { App } from "../core/app.ts";
|
import type { App } from "../core/app.ts";
|
||||||
import { getRenderAhead } from "../decorators/RenderAhead.ts";
|
import { getRenderAhead } from "../decorators/RenderAhead.ts";
|
||||||
import { getRenderRoutes, type RenderRouteSource } from "../decorators/RenderRoute.ts";
|
import {
|
||||||
|
getRenderRoutes,
|
||||||
|
type RenderRouteSource,
|
||||||
|
} from "../decorators/RenderRoute.ts";
|
||||||
import { ContentRegistry } from "../core/content.ts";
|
import { ContentRegistry } from "../core/content.ts";
|
||||||
import { predictLayout, type LayoutPredictionOptions } from "./prediction.ts";
|
import { predictLayout, type LayoutPredictionOptions } from "./prediction.ts";
|
||||||
|
|
||||||
|
|
@ -43,18 +46,33 @@ export async function prerender(
|
||||||
const results: PrerenderResult[] = [];
|
const results: PrerenderResult[] = [];
|
||||||
|
|
||||||
const routes = new Set(options.routes ?? []);
|
const routes = new Set(options.routes ?? []);
|
||||||
const content = app.resolvedConfig ? new ContentRegistry(app.resolvedConfig) : undefined;
|
const content = app.resolvedConfig
|
||||||
|
? new ContentRegistry(app.resolvedConfig)
|
||||||
|
: undefined;
|
||||||
if (content) await content.load();
|
if (content) await content.load();
|
||||||
for (const route of app.router.list()) {
|
for (const route of app.router.list()) {
|
||||||
if (route.method !== "GET") continue;
|
if (route.method !== "GET") continue;
|
||||||
if (!route.path.includes(":")) routes.add(route.path);
|
if (!route.path.includes(":")) routes.add(route.path);
|
||||||
else {
|
else {
|
||||||
for (const source of getRenderRoutes(route.controller, route.methodName)) {
|
for (const source of getRenderRoutes(
|
||||||
for (const path of resolveRenderRoute(source, content)) routes.add(path);
|
route.controller,
|
||||||
|
route.methodName,
|
||||||
|
)) {
|
||||||
|
for (const path of resolveRenderRoute(source, content))
|
||||||
|
routes.add(path);
|
||||||
}
|
}
|
||||||
const ahead = getRenderAhead(route.controller, route.methodName);
|
const ahead = getRenderAhead(route.controller, route.methodName);
|
||||||
const values = ahead ? (typeof ahead === "function" ? await ahead() : ahead) : await options.discover?.(route.path);
|
const values = ahead
|
||||||
for (const value of values ?? []) routes.add(value.startsWith("/") ? value : route.path.replace(/:[^/]+\??/, value));
|
? typeof ahead === "function"
|
||||||
|
? await ahead()
|
||||||
|
: ahead
|
||||||
|
: await options.discover?.(route.path);
|
||||||
|
for (const value of values ?? [])
|
||||||
|
routes.add(
|
||||||
|
value.startsWith("/")
|
||||||
|
? value
|
||||||
|
: route.path.replace(/:[^/]+\??/, value),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const orderedRoutes = [...routes].sort();
|
const orderedRoutes = [...routes].sort();
|
||||||
|
|
@ -62,7 +80,11 @@ export async function prerender(
|
||||||
if (app.router.match("GET", route).kind === "not-found") {
|
if (app.router.match("GET", route).kind === "not-found") {
|
||||||
throw new Error(`Cannot prerender unknown route "${route}"`);
|
throw new Error(`Cannot prerender unknown route "${route}"`);
|
||||||
}
|
}
|
||||||
options.progress?.({ current: index + 1, total: orderedRoutes.length, route });
|
options.progress?.({
|
||||||
|
current: index + 1,
|
||||||
|
total: orderedRoutes.length,
|
||||||
|
route,
|
||||||
|
});
|
||||||
const request = new Request(new URL(route, origin).toString());
|
const request = new Request(new URL(route, origin).toString());
|
||||||
const response = await app.fetch(request);
|
const response = await app.fetch(request);
|
||||||
const contentType = response.headers.get("content-type") ?? "";
|
const contentType = response.headers.get("content-type") ?? "";
|
||||||
|
|
@ -83,16 +105,28 @@ export async function prerender(
|
||||||
// Relation anchors and STCSS prediction markers are static inputs to the
|
// Relation anchors and STCSS prediction markers are static inputs to the
|
||||||
// layout pass. Run Playwright automatically so consumers never need to
|
// layout pass. Run Playwright automatically so consumers never need to
|
||||||
// ship a geometry bridge merely because a page uses `$refer`.
|
// ship a geometry bridge merely because a page uses `$refer`.
|
||||||
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 (options.predict || hasRelations) {
|
if (options.predict || hasRelations) {
|
||||||
const assetHtml = await inlinePredictionStyles(app, html, origin);
|
const assetHtml = await inlinePredictionStyles(app, html, origin);
|
||||||
html = (await predictLayout(assetHtml, options.predict ?? {})).html;
|
html = (await predictLayout(assetHtml, options.predict ?? {})).html;
|
||||||
}
|
}
|
||||||
const file = fileFor(stageDir, route);
|
const file = fileFor(stageDir, route);
|
||||||
await mkdir(dirname(file), { recursive: true });
|
await mkdir(dirname(file), { recursive: true });
|
||||||
const asset = await app.transformAsset({ path: file, contentType, content: html });
|
const asset = await app.transformAsset({
|
||||||
const reports = await app.analyzePage({ route, html: await inlinePredictionStyles(app, html, origin) });
|
path: file,
|
||||||
if (Object.keys(reports).length) await writeFile(`${file}.analysis.json`, JSON.stringify(reports, null, 2));
|
contentType,
|
||||||
|
content: html,
|
||||||
|
});
|
||||||
|
const reports = await app.analyzePage({
|
||||||
|
route,
|
||||||
|
html: await inlinePredictionStyles(app, html, origin),
|
||||||
|
});
|
||||||
|
if (Object.keys(reports).length)
|
||||||
|
await writeFile(
|
||||||
|
`${file}.analysis.json`,
|
||||||
|
JSON.stringify(reports, null, 2),
|
||||||
|
);
|
||||||
await writeFile(file, asset.content);
|
await writeFile(file, asset.content);
|
||||||
results.push({ route, file, status: response.status });
|
results.push({ route, file, status: response.status });
|
||||||
}
|
}
|
||||||
|
|
@ -104,8 +138,13 @@ export async function prerender(
|
||||||
const response = await app.fetch(request);
|
const response = await app.fetch(request);
|
||||||
const file = join(stageDir, route.path.replace(/^\//, ""));
|
const file = join(stageDir, route.path.replace(/^\//, ""));
|
||||||
await mkdir(dirname(file), { recursive: true });
|
await mkdir(dirname(file), { recursive: true });
|
||||||
const contentType = response.headers.get("content-type") ?? "application/octet-stream";
|
const contentType =
|
||||||
const asset = await app.transformAsset({ path: file, contentType, content: new Uint8Array(await response.arrayBuffer()) });
|
response.headers.get("content-type") ?? "application/octet-stream";
|
||||||
|
const asset = await app.transformAsset({
|
||||||
|
path: file,
|
||||||
|
contentType,
|
||||||
|
content: new Uint8Array(await response.arrayBuffer()),
|
||||||
|
});
|
||||||
await writeFile(file, asset.content);
|
await writeFile(file, asset.content);
|
||||||
results.push({ route: route.path, file, status: response.status });
|
results.push({ route: route.path, file, status: response.status });
|
||||||
}
|
}
|
||||||
|
|
@ -122,20 +161,31 @@ export async function prerender(
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Inlines same-origin styles so Playwright can measure a complete document. */
|
/** Inlines same-origin styles so Playwright can measure a complete document. */
|
||||||
export async function inlinePredictionStyles(app: App, html: string, origin: string): Promise<string> {
|
export async function inlinePredictionStyles(
|
||||||
|
app: App,
|
||||||
|
html: string,
|
||||||
|
origin: string,
|
||||||
|
): Promise<string> {
|
||||||
for (const match of [...html.matchAll(/<link\b[^>]*href="([^"]+)"[^>]*>/g)]) {
|
for (const match of [...html.matchAll(/<link\b[^>]*href="([^"]+)"[^>]*>/g)]) {
|
||||||
const url = new URL(match[1]!, origin);
|
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));
|
const response = await app.fetch(new Request(url.href));
|
||||||
if (response.ok) html = html.replace(match[0], `<style>${await response.text()}</style>`);
|
if (response.ok)
|
||||||
|
html = html.replace(match[0], `<style>${await response.text()}</style>`);
|
||||||
}
|
}
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveRenderRoute(source: RenderRouteSource, content?: ContentRegistry): string[] {
|
function resolveRenderRoute(
|
||||||
|
source: RenderRouteSource,
|
||||||
|
content?: ContentRegistry,
|
||||||
|
): string[] {
|
||||||
if (typeof source === "string") return [source];
|
if (typeof source === "string") return [source];
|
||||||
if (Array.isArray(source)) 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. */
|
/** Path of the html file of a route. */
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,11 @@ export type NodeHandler = (
|
||||||
export async function setupVercel(root: string): Promise<string> {
|
export async function setupVercel(root: string): Promise<string> {
|
||||||
const path = join(resolve(root), "vercel.json");
|
const path = join(resolve(root), "vercel.json");
|
||||||
if (existsSync(path)) return path;
|
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;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
88
cli.ts
88
cli.ts
|
|
@ -9,12 +9,22 @@ import { loadConfig } from "./core/config.ts";
|
||||||
import { setupWrangler } from "./adapters/cloudflare-wrangler.ts";
|
import { setupWrangler } from "./adapters/cloudflare-wrangler.ts";
|
||||||
import { setupVercel } from "./adapters/vercel-vercel.ts";
|
import { setupVercel } from "./adapters/vercel-vercel.ts";
|
||||||
import { backportProject } from "./core/backport.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 width = 20;
|
||||||
const filled = Math.round((event.current / Math.max(1, event.total)) * width);
|
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<void> {
|
async function dev(entry: string, root: string, port: number): Promise<void> {
|
||||||
|
|
@ -36,24 +46,34 @@ async function main(): Promise<void> {
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
const command = args[0] ?? "help";
|
const command = args[0] ?? "help";
|
||||||
const portFlag = args.indexOf("--port");
|
const portFlag = args.indexOf("--port");
|
||||||
const requestedPort = portFlag === -1 ? undefined : Number(args[portFlag + 1]);
|
const requestedPort =
|
||||||
const root = args.slice(1).find((argument, index, values) => {
|
portFlag === -1 ? undefined : Number(args[portFlag + 1]);
|
||||||
if (argument.startsWith("--")) return false;
|
const root =
|
||||||
return values[index - 1] !== "--port";
|
args.slice(1).find((argument, index, values) => {
|
||||||
}) ?? process.cwd();
|
if (argument.startsWith("--")) return false;
|
||||||
if (requestedPort !== undefined && (!Number.isInteger(requestedPort) || requestedPort < 1 || requestedPort > 65_535)) {
|
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");
|
throw new Error("--port must be an integer from 1 through 65535");
|
||||||
}
|
}
|
||||||
if (command === "typegen") {
|
if (command === "typegen") {
|
||||||
const result = await generate({ root });
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (command === "backport") {
|
if (command === "backport") {
|
||||||
const source = args[1] ? resolve(args[1]) : process.cwd();
|
const source = args[1] ? resolve(args[1]) : process.cwd();
|
||||||
const destination = args[2] ? resolve(args[2]) : resolve(source, "muddle");
|
const destination = args[2] ? resolve(args[2]) : resolve(source, "muddle");
|
||||||
const result = await backportProject(source, destination);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (command === "templates") {
|
if (command === "templates") {
|
||||||
|
|
@ -63,9 +83,17 @@ async function main(): Promise<void> {
|
||||||
console.log("Usage: muddle templates <docs|minimal> [destination]");
|
console.log("Usage: muddle templates <docs|minimal> [destination]");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!muddleTemplates.includes(name)) throw new Error(`Unknown template ${name}. Choose one of: ${muddleTemplates.join(", ")}`);
|
if (!muddleTemplates.includes(name))
|
||||||
const result = await createTemplate(name, args[2] ? resolve(args[2]) : resolve(process.cwd(), name));
|
throw new Error(
|
||||||
console.log(`muddle created ${result.name} template at ${result.output} (${result.files.length} files)`);
|
`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;
|
return;
|
||||||
}
|
}
|
||||||
if (command === "repo") {
|
if (command === "repo") {
|
||||||
|
|
@ -91,8 +119,12 @@ async function main(): Promise<void> {
|
||||||
}
|
}
|
||||||
if (command === "fmt") {
|
if (command === "fmt") {
|
||||||
const check = process.argv.includes("--check");
|
const check = process.argv.includes("--check");
|
||||||
const changed = await format(resolve(root === "--check" ? process.cwd() : root), { check });
|
const changed = await format(
|
||||||
for (const file of changed) console.log(`${check ? "Unformatted" : "Formatted"} ${file}`);
|
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;
|
if (check && changed.length) process.exitCode = 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -101,23 +133,35 @@ async function main(): Promise<void> {
|
||||||
const config = await loadConfig(target);
|
const config = await loadConfig(target);
|
||||||
const entry = resolve(target, config.entry ?? "index.ts");
|
const entry = resolve(target, config.entry ?? "index.ts");
|
||||||
if (!existsSync(entry)) throw new Error(`No index.ts found in ${root}`);
|
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);
|
if (config.adapter?.name === "vercel-vercel") await setupVercel(target);
|
||||||
const project = await import(entry) as { build?: (options?: { progress?: typeof progress }) => Promise<void>; app?: unknown };
|
const project = (await import(entry)) as {
|
||||||
|
build?: (options?: { progress?: typeof progress }) => Promise<void>;
|
||||||
|
app?: unknown;
|
||||||
|
};
|
||||||
if (command === "build") {
|
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 });
|
await project.build({ progress });
|
||||||
return;
|
return;
|
||||||
} else if (command === "dev") {
|
} else if (command === "dev") {
|
||||||
if (!project.app || typeof (project.app as { fetch?: unknown }).fetch !== "function") {
|
if (
|
||||||
throw new Error("Project entry must export an app with fetch(request) for muddle dev");
|
!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);
|
await dev(entry, target, requestedPort ?? config.adapter?.port ?? 3000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log("muddle <typegen|repo|fmt|build|dev|backport|templates> [project-root] [--port number]");
|
console.log(
|
||||||
|
"muddle <typegen|repo|fmt|build|dev|backport|templates> [project-root] [--port number]",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await main();
|
await main();
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,9 @@ export interface MountableViews {
|
||||||
/** Registers a named view component. */
|
/** Registers a named view component. */
|
||||||
component(
|
component(
|
||||||
name: string,
|
name: string,
|
||||||
component: { render: (props: unknown, context: unknown) => string | Promise<string> },
|
component: {
|
||||||
|
render: (props: unknown, context: unknown) => string | Promise<string>;
|
||||||
|
},
|
||||||
): unknown;
|
): unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,7 +93,11 @@ export interface Mounts {
|
||||||
views?(views: MountableViews): void | Promise<void>;
|
views?(views: MountableViews): void | Promise<void>;
|
||||||
/** Adds routes, e.g. assets, fonts and endpoints. */
|
/** Adds routes, e.g. assets, fonts and endpoints. */
|
||||||
routes?(
|
routes?(
|
||||||
route: (method: string, path: string, handler: (request: Request) => Response | Promise<Response>) => void,
|
route: (
|
||||||
|
method: string,
|
||||||
|
path: string,
|
||||||
|
handler: (request: Request) => Response | Promise<Response>,
|
||||||
|
) => void,
|
||||||
): void | Promise<void>;
|
): void | Promise<void>;
|
||||||
/** Served inline stylesheets by route. */
|
/** Served inline stylesheets by route. */
|
||||||
styles?: Record<string, string | (() => string | Promise<string>)>;
|
styles?: Record<string, string | (() => string | Promise<string>)>;
|
||||||
|
|
|
||||||
|
|
@ -323,16 +323,28 @@ export async function generate(
|
||||||
|
|
||||||
await mkdir(join(out, "orm"), { recursive: true });
|
await mkdir(join(out, "orm"), { recursive: true });
|
||||||
const minifyFile = join(out, "minify.ts");
|
const minifyFile = join(out, "minify.ts");
|
||||||
await writeFile(minifyFile, `export type Simplify<T> = { [K in keyof T]: T[K] } & {};\nexport type Minify<T> = Simplify<T>;\n`, "utf8");
|
await writeFile(
|
||||||
|
minifyFile,
|
||||||
|
`export type Simplify<T> = { [K in keyof T]: T[K] } & {};\nexport type Minify<T> = Simplify<T>;\n`,
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
const viewsFile = join(out, "views.ts");
|
const viewsFile = join(out, "views.ts");
|
||||||
await writeFile(viewsFile, renderViewContracts(views), "utf8");
|
await writeFile(viewsFile, renderViewContracts(views), "utf8");
|
||||||
|
|
||||||
const tablesFile = join(out, "orm", "tables.ts");
|
const tablesFile = join(out, "orm", "tables.ts");
|
||||||
await writeFile(tablesFile, renderTableContracts(tables), "utf8");
|
await writeFile(tablesFile, renderTableContracts(tables), "utf8");
|
||||||
const indexFile = join(out, "index.ts");
|
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. */
|
/** 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<string, unknown>;
|
const module = (await import(file)) as Record<string, unknown>;
|
||||||
for (const exported of Object.values(module)) {
|
for (const exported of Object.values(module)) {
|
||||||
if (typeof exported !== "function") continue;
|
if (typeof exported !== "function") continue;
|
||||||
const metadata = getMetadata("muddle:orm:table", exported as object) as { name: string; fields: Map<string, { property: string; column: string; type: string; nullable: boolean }> } | 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;
|
if (!metadata) continue;
|
||||||
tables.push({
|
tables.push({
|
||||||
name: metadata.name,
|
name: metadata.name,
|
||||||
|
|
@ -402,7 +427,9 @@ export async function collectTables(
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listFiles(dir: string, suffix: string): Promise<string[]> {
|
async function listFiles(dir: string, suffix: string): Promise<string[]> {
|
||||||
const entries = existsSync(dir) ? await readdir(dir, { withFileTypes: true }) : [];
|
const entries = existsSync(dir)
|
||||||
|
? await readdir(dir, { withFileTypes: true })
|
||||||
|
: [];
|
||||||
const files: string[] = [];
|
const files: string[] = [];
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const path = join(dir, entry.name);
|
const path = join(dir, entry.name);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import type { Minify } from "./minify.ts";
|
import type { Minify } from "./minify.ts";
|
||||||
|
|
||||||
/** Fallback view contracts. Project contracts are generated under `_muddle/contracts`. */
|
/** Fallback view contracts. Project contracts are generated under `_muddle/contracts`. */
|
||||||
export interface ViewProps { [path: string]: Record<string, unknown> }
|
export interface ViewProps {
|
||||||
|
[path: string]: Record<string, unknown>;
|
||||||
|
}
|
||||||
export type ViewPath = string;
|
export type ViewPath = string;
|
||||||
export type ViewPropsOf<P extends ViewPath> = Minify<ViewProps[P]>;
|
export type ViewPropsOf<P extends ViewPath> = Minify<ViewProps[P]>;
|
||||||
|
|
|
||||||
47
core/app.ts
47
core/app.ts
|
|
@ -23,7 +23,10 @@ export interface ViewRenderer {
|
||||||
context: RequestContext,
|
context: RequestContext,
|
||||||
): Promise<string | RenderedView> | string | RenderedView;
|
): Promise<string | RenderedView> | string | RenderedView;
|
||||||
/** Registry of named components, mount targets of the integrations. */
|
/** Registry of named components, mount targets of the integrations. */
|
||||||
components?: Map<string, { name: string; component: unknown; deferred: boolean }>;
|
components?: Map<
|
||||||
|
string,
|
||||||
|
{ name: string; component: unknown; deferred: boolean }
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppOptions {
|
export interface AppOptions {
|
||||||
|
|
@ -71,13 +74,19 @@ export class App {
|
||||||
}
|
}
|
||||||
|
|
||||||
async transformAsset(asset: BuildAsset): Promise<BuildAsset> {
|
async transformAsset(asset: BuildAsset): Promise<BuildAsset> {
|
||||||
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;
|
return asset;
|
||||||
}
|
}
|
||||||
|
|
||||||
async analyzePage(page: { route: string; html: string }): Promise<Record<string, unknown>> {
|
async analyzePage(page: {
|
||||||
|
route: string;
|
||||||
|
html: string;
|
||||||
|
}): Promise<Record<string, unknown>> {
|
||||||
const reports: Record<string, unknown> = {};
|
const reports: Record<string, unknown> = {};
|
||||||
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;
|
return reports;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,19 +110,31 @@ export class App {
|
||||||
await integration.setup?.(integrationContext);
|
await integration.setup?.(integrationContext);
|
||||||
for (const mount of integration.mounts ?? []) {
|
for (const mount of integration.mounts ?? []) {
|
||||||
if (mount.views && this.views) {
|
if (mount.views && this.views) {
|
||||||
await mount.views(this.views as unknown as Parameters<NonNullable<Mounts["views"]>>[0]);
|
await mount.views(
|
||||||
|
this.views as unknown as Parameters<
|
||||||
|
NonNullable<Mounts["views"]>
|
||||||
|
>[0],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (mount.routes) {
|
if (mount.routes) {
|
||||||
await mount.routes((method, path, handler) =>
|
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) {
|
if (mount.styles) {
|
||||||
for (const [path, value] of Object.entries(mount.styles)) {
|
for (const [path, value] of Object.entries(mount.styles)) {
|
||||||
this.router.raw("GET", path, async () =>
|
this.router.raw(
|
||||||
new Response(typeof value === "function" ? await value() : value, {
|
"GET",
|
||||||
headers: { "content-type": "text/css; charset=utf-8" },
|
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);
|
Object.assign(context.locals, this.server.locals);
|
||||||
const response = await this.handle(request, context);
|
const response = await this.handle(request, context);
|
||||||
const fetchResponse = toFetchResponse(response);
|
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;
|
return fetchResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,7 +180,8 @@ export class App {
|
||||||
request: Request,
|
request: Request,
|
||||||
context: RequestContext,
|
context: RequestContext,
|
||||||
): Promise<HttpResponse> {
|
): Promise<HttpResponse> {
|
||||||
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.body = await parseRequestBody(request);
|
||||||
context.form = toFormFields(
|
context.form = toFormFields(
|
||||||
context.body &&
|
context.body &&
|
||||||
|
|
|
||||||
340
core/backport.ts
340
core/backport.ts
|
|
@ -31,13 +31,15 @@ const ASSET_EXTENSIONS = new Set([
|
||||||
export function detectBackportSource(root: string): BackportSource {
|
export function detectBackportSource(root: string): BackportSource {
|
||||||
if (
|
if (
|
||||||
existsSync(join(root, "config.toml")) ||
|
existsSync(join(root, "config.toml")) ||
|
||||||
existsSync(join(root, "content")) && existsSync(join(root, "templates"))
|
(existsSync(join(root, "content")) && existsSync(join(root, "templates")))
|
||||||
) return "zola";
|
)
|
||||||
|
return "zola";
|
||||||
if (
|
if (
|
||||||
existsSync(join(root, "astro.config.mjs")) ||
|
existsSync(join(root, "astro.config.mjs")) ||
|
||||||
existsSync(join(root, "astro.config.ts")) ||
|
existsSync(join(root, "astro.config.ts")) ||
|
||||||
existsSync(join(root, "src", "pages"))
|
existsSync(join(root, "src", "pages"))
|
||||||
) return "astro";
|
)
|
||||||
|
return "astro";
|
||||||
throw new Error(`Could not identify a Zola or Astro project in ${root}`);
|
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("\\", "/"));
|
files.push(relative(outputRoot, path).replaceAll("\\", "/"));
|
||||||
};
|
};
|
||||||
|
|
||||||
const contentRoot = join(inputRoot, source === "astro" ? "src/content" : "content");
|
const contentRoot = join(
|
||||||
await writeProjectShell(outputRoot, write, await collectionNames(contentRoot), source === "zola" ? await zolaSiteConfig(join(inputRoot, "config.toml")) : {});
|
inputRoot,
|
||||||
const mappings = source === "astro"
|
source === "astro" ? "src/content" : "content",
|
||||||
? [
|
);
|
||||||
{ from: "src/pages", to: "modules/app/views" },
|
await writeProjectShell(
|
||||||
{ from: "src/layouts", to: "modules/app/views/layouts" },
|
outputRoot,
|
||||||
{ from: "src/components", to: "modules/app/views/components" },
|
write,
|
||||||
{ from: "src/content", to: "content" },
|
await collectionNames(contentRoot),
|
||||||
{ from: "public", to: "public" },
|
source === "zola"
|
||||||
{ from: "src/assets", to: "public/assets" },
|
? await zolaSiteConfig(join(inputRoot, "config.toml"))
|
||||||
{ from: "src/styles", to: "styles" },
|
: {},
|
||||||
]
|
);
|
||||||
: [
|
const mappings =
|
||||||
{ from: "templates", to: "modules/app/views" },
|
source === "astro"
|
||||||
{ from: "content", to: "content" },
|
? [
|
||||||
{ from: "static", to: "public" },
|
{ from: "src/pages", to: "modules/app/views" },
|
||||||
{ from: "sass", to: "styles" },
|
{ 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) {
|
for (const mapping of mappings) {
|
||||||
const folder = join(inputRoot, mapping.from);
|
const folder = join(inputRoot, mapping.from);
|
||||||
if (existsSync(folder)) {
|
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 };
|
return { source, output: outputRoot, files };
|
||||||
|
|
@ -101,7 +120,10 @@ async function zolaSiteConfig(path: string): Promise<Record<string, string>> {
|
||||||
const source = await readFile(path, "utf8");
|
const source = await readFile(path, "utf8");
|
||||||
const values: Record<string, string> = {};
|
const values: Record<string, string> = {};
|
||||||
for (const key of ["title", "base_url", "description", "default_language"]) {
|
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];
|
if (match?.[1] !== undefined) values[key] = match[1];
|
||||||
}
|
}
|
||||||
return values;
|
return values;
|
||||||
|
|
@ -162,8 +184,10 @@ async function convertFolder(
|
||||||
}
|
}
|
||||||
const extension = extname(entry.name).toLowerCase();
|
const extension = extname(entry.name).toLowerCase();
|
||||||
if (
|
if (
|
||||||
extension === ".astro" || extension === ".html" ||
|
extension === ".astro" ||
|
||||||
extension === ".htm" || extension === ".tera"
|
extension === ".html" ||
|
||||||
|
extension === ".htm" ||
|
||||||
|
extension === ".tera"
|
||||||
) {
|
) {
|
||||||
await write(
|
await write(
|
||||||
to.replace(/\.(astro|html?|tera)$/i, ".spug"),
|
to.replace(/\.(astro|html?|tera)$/i, ".spug"),
|
||||||
|
|
@ -172,7 +196,10 @@ async function convertFolder(
|
||||||
: zolaToSpug(await readFile(from, "utf8")),
|
: zolaToSpug(await readFile(from, "utf8")),
|
||||||
);
|
);
|
||||||
} else if (extension === ".tsx" || extension === ".jsx") {
|
} 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") {
|
} else if (extension === ".css") {
|
||||||
await write(
|
await write(
|
||||||
to.replace(/\.css$/i, ".stcss"),
|
to.replace(/\.css$/i, ".stcss"),
|
||||||
|
|
@ -184,7 +211,8 @@ async function convertFolder(
|
||||||
await readFile(from, "utf8"),
|
await readFile(from, "utf8"),
|
||||||
);
|
);
|
||||||
} else if (
|
} else if (
|
||||||
extension === ".md" || extension === ".markdown" ||
|
extension === ".md" ||
|
||||||
|
extension === ".markdown" ||
|
||||||
ASSET_EXTENSIONS.has(extension)
|
ASSET_EXTENSIONS.has(extension)
|
||||||
) {
|
) {
|
||||||
await mkdir(dirname(to), { recursive: true });
|
await mkdir(dirname(to), { recursive: true });
|
||||||
|
|
@ -193,16 +221,36 @@ async function convertFolder(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function reactToSpug(source: string, filename = "component.tsx"): string {
|
export function reactToSpug(
|
||||||
const file = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
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 analysis = analyzeReactComponent(source, filename);
|
||||||
const imports = new Set<string>();
|
const imports = new Set<string>();
|
||||||
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 => {
|
const visit = (node: ts.Node): void => {
|
||||||
if (ts.isImportDeclaration(node) && node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) {
|
if (
|
||||||
for (const element of node.importClause.namedBindings.elements) imports.add(element.name.text);
|
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) {
|
if (ts.isReturnStatement(node) && node.expression && !root) {
|
||||||
const expression = unwrapJsxExpression(node.expression);
|
const expression = unwrapJsxExpression(node.expression);
|
||||||
if (isJsxNode(expression)) root = expression;
|
if (isJsxNode(expression)) root = expression;
|
||||||
|
|
@ -215,10 +263,13 @@ export function reactToSpug(source: string, filename = "component.tsx"): string
|
||||||
};
|
};
|
||||||
visit(file);
|
visit(file);
|
||||||
if (!root) throw new Error(`No JSX return value found in ${filename}`);
|
if (!root) throw new Error(`No JSX return value found in ${filename}`);
|
||||||
const aot = analysis.state.map((state) => ` const ${state.name} = ${state.initial};`);
|
const aot = analysis.state.map(
|
||||||
const script = aot.length || analysis.hooks.length
|
(state) => ` const ${state.name} = ${state.initial};`,
|
||||||
? `javascript!\n // React hooks lowered during static component composition: ${analysis.hooks.join(", ") || "none"}\n${aot.join("\n")}\nend\n`
|
);
|
||||||
: "";
|
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`;
|
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. */
|
/** Finds React hooks and extracts AOT-safe initial state from a component. */
|
||||||
export function analyzeReactComponent(source: string, filename = "component.tsx"): ReactHookAnalysis {
|
export function analyzeReactComponent(
|
||||||
const file = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
source: string,
|
||||||
|
filename = "component.tsx",
|
||||||
|
): ReactHookAnalysis {
|
||||||
|
const file = ts.createSourceFile(
|
||||||
|
filename,
|
||||||
|
source,
|
||||||
|
ts.ScriptTarget.Latest,
|
||||||
|
true,
|
||||||
|
ts.ScriptKind.TSX,
|
||||||
|
);
|
||||||
const hooks = new Set<string>();
|
const hooks = new Set<string>();
|
||||||
const state: { name: string; initial: string }[] = [];
|
const state: { name: string; initial: string }[] = [];
|
||||||
const visit = (node: ts.Node): void => {
|
const visit = (node: ts.Node): void => {
|
||||||
if (ts.isCallExpression(node)) {
|
if (ts.isCallExpression(node)) {
|
||||||
const name = ts.isIdentifier(node.expression) ? node.expression.text
|
const name = ts.isIdentifier(node.expression)
|
||||||
: ts.isPropertyAccessExpression(node.expression) ? node.expression.name.text : "";
|
? node.expression.text
|
||||||
|
: ts.isPropertyAccessExpression(node.expression)
|
||||||
|
? node.expression.name.text
|
||||||
|
: "";
|
||||||
if (/^use[A-Z]/.test(name)) hooks.add(name);
|
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];
|
const binding = node.parent.name.elements[0];
|
||||||
if (binding && ts.isBindingElement(binding) && ts.isIdentifier(binding.name)) {
|
if (
|
||||||
state.push({ name: binding.name.text, initial: node.arguments[0]?.getText() ?? "undefined" });
|
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 {
|
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 {
|
function isJsxNode(
|
||||||
return ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node);
|
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<string>, depth = 0): string {
|
function jsxToSpug(
|
||||||
|
node: ts.JsxChild | ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment,
|
||||||
|
imports: Set<string>,
|
||||||
|
depth = 0,
|
||||||
|
): string {
|
||||||
const pad = " ".repeat(depth);
|
const pad = " ".repeat(depth);
|
||||||
if (ts.isJsxText(node)) {
|
if (ts.isJsxText(node)) {
|
||||||
const text = node.getText().replace(/\s+/g, " ").trim();
|
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 "";
|
if (!node.expression) return "";
|
||||||
return `${pad}= ${node.expression.getText()}`;
|
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 opening = ts.isJsxElement(node) ? node.openingElement : node;
|
||||||
const rawName = opening.tagName.getText();
|
const rawName = opening.tagName.getText();
|
||||||
const name = rawName === "Comp" ? "button" : rawName;
|
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 attributes = jsxAttributes(opening.attributes.properties, component);
|
||||||
const line = component ? `${pad}component ${kebab(name)}` : `${pad}${name}${attributes.inline}`;
|
const line = component
|
||||||
const props = component && attributes.props.length ? `\n${attributes.props.map((property) => `${pad} ${property}`).join("\n")}` : "";
|
? `${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)) {
|
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");
|
return [line + props, children].filter(Boolean).join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
function jsxAttributes(properties: ts.NodeArray<ts.JsxAttributeLike>, component = false): { inline: string; props: string[] } {
|
function jsxAttributes(
|
||||||
|
properties: ts.NodeArray<ts.JsxAttributeLike>,
|
||||||
|
component = false,
|
||||||
|
): { inline: string; props: string[] } {
|
||||||
const inline: string[] = [];
|
const inline: string[] = [];
|
||||||
const props: string[] = [];
|
const props: string[] = [];
|
||||||
for (const property of properties) {
|
for (const property of properties) {
|
||||||
|
|
@ -291,14 +395,23 @@ function jsxAttributes(properties: ts.NodeArray<ts.JsxAttributeLike>, component
|
||||||
const name = property.name.getText();
|
const name = property.name.getText();
|
||||||
if (name === "ref") continue;
|
if (name === "ref") continue;
|
||||||
const initializer = property.initializer;
|
const initializer = property.initializer;
|
||||||
if (!initializer) { inline.push(`${name}=true`); continue; }
|
if (!initializer) {
|
||||||
const value = ts.isStringLiteral(initializer) ? JSON.stringify(initializer.text)
|
inline.push(`${name}=true`);
|
||||||
: ts.isJsxExpression(initializer) && initializer.expression ? `(${initializer.expression.getText()})`
|
continue;
|
||||||
: JSON.stringify(initializer.getText());
|
}
|
||||||
|
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 normalized = name === "className" ? "class" : name;
|
||||||
const classValue = normalized === "class" && ts.isJsxExpression(initializer) && initializer.expression
|
const classValue =
|
||||||
? staticClassValue(initializer.expression) ?? JSON.stringify("muddle-react")
|
normalized === "class" &&
|
||||||
: value;
|
ts.isJsxExpression(initializer) &&
|
||||||
|
initializer.expression
|
||||||
|
? (staticClassValue(initializer.expression) ??
|
||||||
|
JSON.stringify("muddle-react"))
|
||||||
|
: value;
|
||||||
if (component) props.push(`${normalized}: ${classValue}`);
|
if (component) props.push(`${normalized}: ${classValue}`);
|
||||||
else inline.push(`${normalized}=${classValue}`);
|
else inline.push(`${normalized}=${classValue}`);
|
||||||
}
|
}
|
||||||
|
|
@ -308,12 +421,17 @@ function jsxAttributes(properties: ts.NodeArray<ts.JsxAttributeLike>, component
|
||||||
function staticClassValue(expression: ts.Expression): string | undefined {
|
function staticClassValue(expression: ts.Expression): string | undefined {
|
||||||
if (ts.isStringLiteral(expression)) return JSON.stringify(expression.text);
|
if (ts.isStringLiteral(expression)) return JSON.stringify(expression.text);
|
||||||
if (!ts.isCallExpression(expression)) return undefined;
|
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;
|
return classes.length ? JSON.stringify(classes.join(" ")) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function kebab(value: string): string {
|
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 {
|
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 {
|
export function htmlToSpug(source: string): string {
|
||||||
const body = source.replace(/^---[\s\S]*?---\s*/m, "").replace(
|
const body = source
|
||||||
/^---[\s\S]*?---\s*/m,
|
.replace(/^---[\s\S]*?---\s*/m, "")
|
||||||
"",
|
.replace(/^---[\s\S]*?---\s*/m, "")
|
||||||
).replace(/<!--[\s\S]*?-->/g, "");
|
.replace(/<!--[\s\S]*?-->/g, "");
|
||||||
const tokens = body.match(/<[^>]+>|[^<]+/g) ?? [];
|
const tokens = body.match(/<[^>]+>|[^<]+/g) ?? [];
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
let depth = 0;
|
let depth = 0;
|
||||||
|
|
@ -385,14 +503,15 @@ export function htmlToSpug(source: string): string {
|
||||||
"track",
|
"track",
|
||||||
"wbr",
|
"wbr",
|
||||||
]).has(tag)
|
]).has(tag)
|
||||||
) depth += 1;
|
)
|
||||||
|
depth += 1;
|
||||||
} else {
|
} else {
|
||||||
const text = token.replace(/\s+/g, " ").trim();
|
const text = token.replace(/\s+/g, " ").trim();
|
||||||
if (text) {
|
if (text) {
|
||||||
lines.push(
|
lines.push(
|
||||||
`${" ".repeat(depth)}| ${
|
`${" ".repeat(depth)}| ${text
|
||||||
text.replace(/\{\{\s*(.*?)\s*\}\}/g, "#{ $1 }").replace(/\{([A-Za-z_$][\w$]*(?:\.[\w$]+)*)\}/g, "#{ $1 }")
|
.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. */
|
/** 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] ?? "";
|
const frontmatter = /^---\s*\n([\s\S]*?)\n---\s*\n?/.exec(source)?.[1] ?? "";
|
||||||
let body = source.replace(/^---\s*\n[\s\S]*?\n---\s*\n?/, "");
|
let body = source.replace(/^---\s*\n[\s\S]*?\n---\s*\n?/, "");
|
||||||
const imports = new Map<string, string>();
|
const imports = new Map<string, string>();
|
||||||
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]!);
|
imports.set(entry[1]!, entry[2]!);
|
||||||
}
|
}
|
||||||
const layout = /^\s*<([A-Z][\w$]*)\b([^>]*)>([\s\S]*)<\/\1>\s*$/.exec(body);
|
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)) {
|
if (/(?:^|\/)layouts\//.test(imported)) {
|
||||||
const absoluteLayout = resolve(dirname(file), imported);
|
const absoluteLayout = resolve(dirname(file), imported);
|
||||||
const pageRelative = relative(pagesRoot, file);
|
const pageRelative = relative(pagesRoot, file);
|
||||||
const layoutRelative = relative(dirname(pagesRoot), absoluteLayout).replace(/\.astro$/, ".spug");
|
const layoutRelative = relative(
|
||||||
const target = relative(dirname(pageRelative), layoutRelative).replaceAll("\\", "/");
|
dirname(pagesRoot),
|
||||||
|
absoluteLayout,
|
||||||
|
).replace(/\.astro$/, ".spug");
|
||||||
|
const target = relative(dirname(pageRelative), layoutRelative).replaceAll(
|
||||||
|
"\\",
|
||||||
|
"/",
|
||||||
|
);
|
||||||
prefix = `extends ${target}\n\nblock content\n`;
|
prefix = `extends ${target}\n\nblock content\n`;
|
||||||
body = layout[3]!;
|
body = layout[3]!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
body = body
|
body = body
|
||||||
.replace(/<slot\s+name=["']([\w-]+)["']\s*\/?\s*>/g, "<muddle-slot data-name=\"$1\"></muddle-slot>")
|
.replace(
|
||||||
|
/<slot\s+name=["']([\w-]+)["']\s*\/?\s*>/g,
|
||||||
|
'<muddle-slot data-name="$1"></muddle-slot>',
|
||||||
|
)
|
||||||
.replace(/<slot\s*\/?\s*>/g, "<muddle-slot></muddle-slot>");
|
.replace(/<slot\s*\/?\s*>/g, "<muddle-slot></muddle-slot>");
|
||||||
const converted = htmlToSpug(body)
|
const converted = htmlToSpug(body)
|
||||||
.replace(/^(\s*)muddle-slot\(data-name="([\w-]+)"\)\s*$/gm, "$1block $2")
|
.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 {
|
export function zolaToSpug(source: string): string {
|
||||||
let result = source
|
let result = source
|
||||||
.replace(/\{#([\s\S]*?)#\}/g, "")
|
.replace(/\{#([\s\S]*?)#\}/g, "")
|
||||||
.replace(/\{%\s*extends\s+["']([^"']+)["']\s*%\}/g, (_all, path: string) => `extends ./${path.replace(/\.html?$|\.tera$/i, ".spug")}`)
|
.replace(
|
||||||
.replace(/\{%\s*include\s+["']([^"']+)["']\s*%\}/g, (_all, path: string) => `include ./${path.replace(/\.html?$|\.tera$/i, ".spug")}`)
|
/\{%\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*block\s+([\w-]+)\s*%\}/g, "block $1")
|
||||||
.replace(/\{%\s*endblock\s*%\}/g, "")
|
.replace(/\{%\s*endblock\s*%\}/g, "")
|
||||||
.replace(/\{%\s*if\s+(.+?)\s*%\}/g, "if $1")
|
.replace(/\{%\s*if\s+(.+?)\s*%\}/g, "if $1")
|
||||||
|
|
@ -452,33 +594,50 @@ function componentCalls(source: string, imports: Map<string, string>): string {
|
||||||
const output: string[] = [];
|
const output: string[] = [];
|
||||||
for (const line of source.split("\n")) {
|
for (const line of source.split("\n")) {
|
||||||
const match = /^(\s*)([A-Z][\w$]*)(.*)$/.exec(line);
|
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 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];
|
const attributeText = /\((.*)\)$/.exec(match[3]!)?.[1];
|
||||||
output.push(`${match[1]}component ${name}`);
|
output.push(`${match[1]}component ${name}`);
|
||||||
for (const pair of attributeText?.split(/\s*,\s*/) ?? []) {
|
for (const pair of attributeText?.split(/\s*,\s*/) ?? []) {
|
||||||
const [key = "", raw = "true"] = pair.split(/=(.+)/);
|
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("(");
|
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`;
|
return `${output.filter(Boolean).join("\n").trim()}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function indent(source: string): string {
|
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 {
|
function attributesToSpug(raw: string): string {
|
||||||
const classes: string[] = [];
|
const classes: string[] = [];
|
||||||
const attributes: string[] = [];
|
const attributes: string[] = [];
|
||||||
for (
|
for (const match of raw.matchAll(
|
||||||
const match of raw.matchAll(
|
/([\w:-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g,
|
||||||
/([\w:-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g,
|
)) {
|
||||||
)
|
|
||||||
) {
|
|
||||||
const name = match[1]!;
|
const name = match[1]!;
|
||||||
const value = match[2] ?? match[3] ?? match[4];
|
const value = match[2] ?? match[3] ?? match[4];
|
||||||
if (name === "class" && value) {
|
if (name === "class" && value) {
|
||||||
|
|
@ -490,7 +649,8 @@ function attributesToSpug(raw: string): string {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (value === undefined) attributes.push(`${name}=true`);
|
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)}`);
|
else attributes.push(`${name}=${JSON.stringify(value)}`);
|
||||||
}
|
}
|
||||||
return `${classes.map((value) => `.${value}`).join("")}${attributes.length ? `(${attributes.join(", ")})` : ""}`;
|
return `${classes.map((value) => `.${value}`).join("")}${attributes.length ? `(${attributes.join(", ")})` : ""}`;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,11 @@ import { join } from "node:path";
|
||||||
|
|
||||||
export interface Cache {
|
export interface Cache {
|
||||||
get(key: string): Promise<Uint8Array | undefined>;
|
get(key: string): Promise<Uint8Array | undefined>;
|
||||||
put(key: string, value: Uint8Array, options?: { ttl?: number }): Promise<void>;
|
put(
|
||||||
|
key: string,
|
||||||
|
value: Uint8Array,
|
||||||
|
options?: { ttl?: number },
|
||||||
|
): Promise<void>;
|
||||||
delete(key: string): Promise<void>;
|
delete(key: string): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,17 +43,27 @@ export function filesystemCache(dir: string): Cache {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cloudflareCache(cache: CloudflareCacheLike, origin = "https://muddle.cache"): Cache {
|
export function cloudflareCache(
|
||||||
const request = (key: string) => new Request(new URL(`/${encodeURIComponent(key)}`, origin).toString());
|
cache: CloudflareCacheLike,
|
||||||
|
origin = "https://muddle.cache",
|
||||||
|
): Cache {
|
||||||
|
const request = (key: string) =>
|
||||||
|
new Request(new URL(`/${encodeURIComponent(key)}`, origin).toString());
|
||||||
return {
|
return {
|
||||||
async get(key) {
|
async get(key) {
|
||||||
const response = await cache.match(request(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) {
|
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 }));
|
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));
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -131,12 +131,24 @@ export class ContentRegistry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listCollectionFiles(dir: string, extensions: string[], prefix = ""): Promise<string[]> {
|
async function listCollectionFiles(
|
||||||
|
dir: string,
|
||||||
|
extensions: string[],
|
||||||
|
prefix = "",
|
||||||
|
): Promise<string[]> {
|
||||||
const files: string[] = [];
|
const files: string[] = [];
|
||||||
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
||||||
const relativePath = join(prefix, entry.name);
|
const relativePath = join(prefix, entry.name);
|
||||||
if (entry.isDirectory()) files.push(...(await listCollectionFiles(join(dir, entry.name), extensions, relativePath)));
|
if (entry.isDirectory())
|
||||||
else if (extensions.some((extension) => entry.name.endsWith(extension))) files.push(relativePath);
|
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();
|
return files.sort();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,87 @@
|
||||||
import type { Headers } from "../contracts/typegen/http/headers.ts";
|
import type { Headers } from "../contracts/typegen/http/headers.ts";
|
||||||
|
|
||||||
export class ServerBuildContext {
|
export class ServerBuildContext {
|
||||||
private readonly values: { headers: Headers; locals: Record<string, unknown> } = { headers: {}, locals: {} };
|
private readonly values: {
|
||||||
|
headers: Headers;
|
||||||
|
locals: Record<string, unknown>;
|
||||||
|
} = { headers: {}, locals: {} };
|
||||||
|
|
||||||
header(name: string, value: string): this { this.values.headers[name.toLowerCase()] = value; return this; }
|
header(name: string, value: string): this {
|
||||||
headers(values: Headers): this { Object.assign(this.values.headers, values); return this; }
|
this.values.headers[name.toLowerCase()] = value;
|
||||||
local(name: string, value: unknown): this { this.values.locals[name] = value; return this; }
|
return this;
|
||||||
build(): Readonly<{ headers: Headers; locals: Record<string, unknown> }> { return { headers: { ...this.values.headers }, locals: { ...this.values.locals } }; }
|
}
|
||||||
|
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<string, unknown> }> {
|
||||||
|
return {
|
||||||
|
headers: { ...this.values.headers },
|
||||||
|
locals: { ...this.values.locals },
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TemplateContext {
|
export class TemplateContext {
|
||||||
readonly imports = new Map<string, { render(props: Record<string, unknown>, context: { locals: Record<string, unknown>; params: Record<string, string> }): Promise<string> | string }>();
|
readonly imports = new Map<
|
||||||
import(name: string, component: { render(props: Record<string, unknown>, context: { locals: Record<string, unknown>; params: Record<string, string> }): Promise<string> | string }): this {
|
string,
|
||||||
if (!/^[A-Za-z][\w-]*$/.test(name)) throw new Error("Invalid component import name");
|
{
|
||||||
|
render(
|
||||||
|
props: Record<string, unknown>,
|
||||||
|
context: {
|
||||||
|
locals: Record<string, unknown>;
|
||||||
|
params: Record<string, string>;
|
||||||
|
},
|
||||||
|
): Promise<string> | string;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
import(
|
||||||
|
name: string,
|
||||||
|
component: {
|
||||||
|
render(
|
||||||
|
props: Record<string, unknown>,
|
||||||
|
context: {
|
||||||
|
locals: Record<string, unknown>;
|
||||||
|
params: Record<string, string>;
|
||||||
|
},
|
||||||
|
): Promise<string> | string;
|
||||||
|
},
|
||||||
|
): this {
|
||||||
|
if (!/^[A-Za-z][\w-]*$/.test(name))
|
||||||
|
throw new Error("Invalid component import name");
|
||||||
this.imports.set(name, component);
|
this.imports.set(name, component);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
private readonly values: { templates: string[]; inputs: Record<string, unknown>; contracts: Record<string, string> } = { templates: [], inputs: {}, contracts: {} };
|
private readonly values: {
|
||||||
template(path: string): this { this.values.templates.push(path); return this; }
|
templates: string[];
|
||||||
input(name: string, value: unknown): this { this.values.inputs[name] = value; return this; }
|
inputs: Record<string, unknown>;
|
||||||
contract(name: string, type: string): this { this.values.contracts[name] = type; return this; }
|
contracts: Record<string, string>;
|
||||||
build(): Readonly<{ templates: string[]; inputs: Record<string, unknown>; contracts: Record<string, string> }> { return { templates: [...this.values.templates], inputs: { ...this.values.inputs }, contracts: { ...this.values.contracts } }; }
|
} = { 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<string, unknown>;
|
||||||
|
contracts: Record<string, string>;
|
||||||
|
}> {
|
||||||
|
return {
|
||||||
|
templates: [...this.values.templates],
|
||||||
|
inputs: { ...this.values.inputs },
|
||||||
|
contracts: { ...this.values.contracts },
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,58 @@
|
||||||
import { foldLines } from "../views/engine/lexer.ts";
|
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";
|
export type FormatLanguage = "spug" | "stpug" | "stug" | "stcss";
|
||||||
|
|
||||||
/** Shared by the CLI and Prettier plugin, with no subprocesses or write side effects. */
|
/** 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<string> {
|
export async function formatSource(
|
||||||
|
source: string,
|
||||||
|
language: FormatLanguage,
|
||||||
|
options: FormatOptions = {},
|
||||||
|
): Promise<string> {
|
||||||
const prettier = await import("prettier");
|
const prettier = await import("prettier");
|
||||||
const scripts: string[] = [];
|
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) => {
|
let protectedSource = source.replace(
|
||||||
const index = scripts.length;
|
/(^[\t ]*)(javascript!|typescript!|---)\s*\n([\s\S]*?)^\s*(end|---)\s*$/gm,
|
||||||
scripts.push(JSON.stringify({ directive, code, ending }));
|
(
|
||||||
return language === "stcss" ? `/* MUDDLE_SCRIPT_${index} */` : `${indentation}MUDDLE_SCRIPT_${index}`;
|
_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 }[] = [];
|
const rawBlocks: { marker: string; body: string[]; prefix: string }[] = [];
|
||||||
if (language !== "stcss") {
|
if (language !== "stcss") {
|
||||||
const sourceLines = protectedSource.split("\n");
|
const sourceLines = protectedSource.split("\n");
|
||||||
const output: string[] = [];
|
const output: string[] = [];
|
||||||
for (let index = 0; index < sourceLines.length; index++) {
|
for (let index = 0; index < sourceLines.length; index++) {
|
||||||
const line = sourceLines[index]!;
|
const line = sourceLines[index]!;
|
||||||
const match = /^(\s*)(?:[\w.#-]+(?:\([^]*\))?\.|\/\/-?|\$\$\$)\s*$/.exec(line);
|
const match = /^(\s*)(?:[\w.#-]+(?:\([^]*\))?\.|\/\/-?|\$\$\$)\s*$/.exec(
|
||||||
if (!match) { output.push(line); continue; }
|
line,
|
||||||
|
);
|
||||||
|
if (!match) {
|
||||||
|
output.push(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const prefix = match[1]!;
|
const prefix = match[1]!;
|
||||||
const body: string[] = [line.slice(prefix.length)];
|
const body: string[] = [line.slice(prefix.length)];
|
||||||
while (index + 1 < sourceLines.length) {
|
while (index + 1 < sourceLines.length) {
|
||||||
const next = sourceLines[index + 1]!;
|
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);
|
body.push(next.startsWith(prefix) ? next.slice(prefix.length) : next);
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
|
|
@ -37,12 +65,19 @@ export async function formatSource(source: string, language: FormatLanguage, opt
|
||||||
const unit = options.useTabs ? "\t" : " ".repeat(options.tabWidth ?? 2);
|
const unit = options.useTabs ? "\t" : " ".repeat(options.tabWidth ?? 2);
|
||||||
let formatted: string;
|
let formatted: string;
|
||||||
if (language === "stcss") {
|
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 {
|
} else {
|
||||||
const stack = [0];
|
const stack = [0];
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
for (const line of foldLines(protectedSource)) {
|
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);
|
if (line.indent > stack[stack.length - 1]!) stack.push(line.indent);
|
||||||
const indent = unit.repeat(stack.length - 1);
|
const indent = unit.repeat(stack.length - 1);
|
||||||
lines.push(`${indent}${line.text.trimEnd()}`);
|
lines.push(`${indent}${line.text.trimEnd()}`);
|
||||||
|
|
@ -50,14 +85,35 @@ export async function formatSource(source: string, language: FormatLanguage, opt
|
||||||
formatted = `${lines.join("\n")}\n`;
|
formatted = `${lines.join("\n")}\n`;
|
||||||
}
|
}
|
||||||
for (let index = 0; index < scripts.length; index++) {
|
for (let index = 0; index < scripts.length; index++) {
|
||||||
const script = JSON.parse(scripts[index]!) as { directive: string; code: string; ending: string };
|
const script = JSON.parse(scripts[index]!) as {
|
||||||
const code = await prettier.format(script.code, { parser: "typescript", plugins: [], tabWidth: options.tabWidth ?? 2, useTabs: options.useTabs ?? false });
|
directive: string;
|
||||||
const marker = language === "stcss" ? new RegExp(`^[\\t ]*/\\* MUDDLE_SCRIPT_${index} \\*/`, "m") : new RegExp(`^([\\t ]*)MUDDLE_SCRIPT_${index}`, "m");
|
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) => {
|
formatted = formatted.replace(marker, (match) => {
|
||||||
const indentation = match.match(/^[\t ]*/)?.[0] ?? "";
|
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;
|
return formatted;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,42 @@
|
||||||
import { readdir, readFile, writeFile, stat } from "node:fs/promises";
|
import { readdir, readFile, writeFile, stat } from "node:fs/promises";
|
||||||
import { join, extname } from "node:path";
|
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<string[]> {
|
export async function format(
|
||||||
|
root: string,
|
||||||
|
options: FormatOptions & { check?: boolean } = {},
|
||||||
|
): Promise<string[]> {
|
||||||
const changed: string[] = [];
|
const changed: string[] = [];
|
||||||
if ((await stat(root)).isFile()) {
|
if ((await stat(root)).isFile()) {
|
||||||
const language = extname(root).slice(1) as FormatLanguage;
|
const language = extname(root).slice(1) as FormatLanguage;
|
||||||
if (!["spug", "stpug", "stug", "stcss"].includes(language)) return changed;
|
if (!["spug", "stpug", "stug", "stcss"].includes(language)) return changed;
|
||||||
const source = await readFile(root, "utf8");
|
const source = await readFile(root, "utf8");
|
||||||
const next = await formatSource(source, language, options);
|
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;
|
return changed;
|
||||||
}
|
}
|
||||||
for (const entry of await readdir(root, { withFileTypes: true })) {
|
for (const entry of await readdir(root, { withFileTypes: true })) {
|
||||||
if (entry.isSymbolicLink()) continue;
|
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);
|
const path = join(root, entry.name);
|
||||||
changed.push(...await format(path, options));
|
changed.push(...(await format(path, options)));
|
||||||
}
|
}
|
||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,10 @@ export async function serveStaticFile(
|
||||||
path: string,
|
path: string,
|
||||||
): Promise<HttpResponse | undefined> {
|
): Promise<HttpResponse | undefined> {
|
||||||
const relative = decodeURIComponent(path.split("?")[0] ?? "");
|
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 (!target.startsWith(resolve(root))) return undefined;
|
||||||
if (!existsSync(target)) return undefined;
|
if (!existsSync(target)) return undefined;
|
||||||
if (!statSync(target).isFile()) return undefined;
|
if (!statSync(target).isFile()) return undefined;
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,25 @@ import type { HttpResponse } from "../contracts/typegen/http/response.ts";
|
||||||
import type { RequestContext } from "../contracts/typegen/http/request.ts";
|
import type { RequestContext } from "../contracts/typegen/http/request.ts";
|
||||||
import type { RouteDefinition } from "./router.ts";
|
import type { RouteDefinition } from "./router.ts";
|
||||||
|
|
||||||
export type RouteGuard = (context: RequestContext, route: RouteDefinition) => Promise<void | HttpResponse> | void | HttpResponse;
|
export type RouteGuard = (
|
||||||
|
context: RequestContext,
|
||||||
|
route: RouteDefinition,
|
||||||
|
) => Promise<void | HttpResponse> | void | HttpResponse;
|
||||||
|
|
||||||
export class RoutePipeline {
|
export class RoutePipeline {
|
||||||
private readonly guards: RouteGuard[] = [];
|
private readonly guards: RouteGuard[] = [];
|
||||||
use(guard: RouteGuard): this { this.guards.push(guard); return this; }
|
use(guard: RouteGuard): this {
|
||||||
async run(context: RequestContext, route: RouteDefinition): Promise<HttpResponse | undefined> {
|
this.guards.push(guard);
|
||||||
for (const guard of this.guards) { const result = await guard(context, route); if (result) return result; }
|
return this;
|
||||||
|
}
|
||||||
|
async run(
|
||||||
|
context: RequestContext,
|
||||||
|
route: RouteDefinition,
|
||||||
|
): Promise<HttpResponse | undefined> {
|
||||||
|
for (const guard of this.guards) {
|
||||||
|
const result = await guard(context, route);
|
||||||
|
if (result) return result;
|
||||||
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 { getRoot, resolveViewRoot } from "../decorators/Root.ts";
|
||||||
import { getParamMetadata, type ParamMetadata } from "../decorators/Params.ts";
|
import { getParamMetadata, type ParamMetadata } from "../decorators/Params.ts";
|
||||||
import { getRoutes, type RouteOptions } from "../decorators/Route.ts";
|
import { getRoutes, type RouteOptions } from "../decorators/Route.ts";
|
||||||
|
|
@ -139,7 +142,10 @@ export interface ResolvedView {
|
||||||
/**
|
/**
|
||||||
* Resolves the template of a view.
|
* 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)
|
const absolute = isAbsolute(view.path)
|
||||||
? view.path
|
? view.path
|
||||||
: resolve(viewRoot, view.path);
|
: resolve(viewRoot, view.path);
|
||||||
|
|
@ -151,7 +157,7 @@ export function resolveView(view: ViewMetadata, viewRoot: string): ResolvedView
|
||||||
|
|
||||||
let relative = path.startsWith(`${viewRoot}/`)
|
let relative = path.startsWith(`${viewRoot}/`)
|
||||||
? path.slice(viewRoot.length + 1)
|
? path.slice(viewRoot.length + 1)
|
||||||
: path.split("/").pop() ?? path;
|
: (path.split("/").pop() ?? path);
|
||||||
relative = relative.replace(/\.[^./]+$/, "");
|
relative = relative.replace(/\.[^./]+$/, "");
|
||||||
|
|
||||||
const segments = relative.split("/").filter((segment) => segment.length > 0);
|
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.
|
* Adds a route that is not bound to a controller.
|
||||||
*/
|
*/
|
||||||
raw(
|
raw(method: string, path: string, handler: RawRoute["handler"]): void {
|
||||||
method: string,
|
|
||||||
path: string,
|
|
||||||
handler: RawRoute["handler"],
|
|
||||||
): void {
|
|
||||||
this.rawRoutes.push({ method: method.toUpperCase(), path, handler });
|
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. */
|
/** Public route metadata for client navigation and route-aware components. */
|
||||||
clientRoutes(): { name: string; description?: string; path: string; method: HttpMethod; params: Record<string, string> }[] {
|
clientRoutes(): {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
path: string;
|
||||||
|
method: HttpMethod;
|
||||||
|
params: Record<string, string>;
|
||||||
|
}[] {
|
||||||
return this.routes.map((route) => {
|
return this.routes.map((route) => {
|
||||||
const view = route.view;
|
const view = route.view;
|
||||||
const params = Object.fromEntries(pathSegments(route.path).filter((part) => part.startsWith(":")).map((part) => {
|
const params = Object.fromEntries(
|
||||||
const key = part.slice(1).replace(/[?*]$/, "");
|
pathSegments(route.path)
|
||||||
return [key, route.options.params?.[key] ?? view?.params?.[key] ?? ""];
|
.filter((part) => part.startsWith(":"))
|
||||||
}));
|
.map((part) => {
|
||||||
return { name: route.options.name ?? view?.name ?? route.methodName, description: route.options.description ?? view?.description, path: route.path, method: route.method, params };
|
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<HttpResponse> {
|
async handle(context: RequestContext): Promise<HttpResponse> {
|
||||||
for (const route of this.rawRoutes) {
|
for (const route of this.rawRoutes) {
|
||||||
if (route.method !== context.method) continue;
|
if (route.method !== context.method) continue;
|
||||||
if (route.path.endsWith("*") ? !context.path.startsWith(route.path.slice(0, -1)) : route.path !== context.path) continue;
|
if (
|
||||||
const response = await route.handler(
|
route.path.endsWith("*")
|
||||||
context.raw as Request,
|
? !context.path.startsWith(route.path.slice(0, -1))
|
||||||
context,
|
: route.path !== context.path
|
||||||
);
|
)
|
||||||
|
continue;
|
||||||
|
const response = await route.handler(context.raw as Request, context);
|
||||||
return {
|
return {
|
||||||
status: response.status as StatusCode,
|
status: response.status as StatusCode,
|
||||||
headers: Object.fromEntries(response.headers.entries()),
|
headers: Object.fromEntries(response.headers.entries()),
|
||||||
|
|
@ -332,8 +355,17 @@ export class Router {
|
||||||
const auth = getAuth(match.route.controller, match.route.methodName);
|
const auth = getAuth(match.route.controller, match.route.methodName);
|
||||||
if (auth) {
|
if (auth) {
|
||||||
const identity = context.locals.auth as { roles?: string[] } | undefined;
|
const identity = context.locals.auth as { roles?: string[] } | undefined;
|
||||||
if (!identity || (auth.roles?.some((role) => !identity.roles?.includes(role)) ?? false)) {
|
if (
|
||||||
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" };
|
!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);
|
const result = await match.route.invoke(context);
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,23 @@ export class MemorySessionStore implements SessionStore {
|
||||||
private readonly records = new Map<string, SessionRecord>();
|
private readonly records = new Map<string, SessionRecord>();
|
||||||
async get(key: string): Promise<SessionRecord | undefined> {
|
async get(key: string): Promise<SessionRecord | undefined> {
|
||||||
const record = this.records.get(key);
|
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);
|
return structuredClone(record);
|
||||||
}
|
}
|
||||||
async set(key: string, record: SessionRecord): Promise<void> { this.records.set(key, structuredClone(record)); }
|
async set(key: string, record: SessionRecord): Promise<void> {
|
||||||
async delete(key: string): Promise<void> { this.records.delete(key); }
|
this.records.set(key, structuredClone(record));
|
||||||
|
}
|
||||||
|
async delete(key: string): Promise<void> {
|
||||||
|
this.records.delete(key);
|
||||||
|
}
|
||||||
async consume(key: string): Promise<SessionRecord | undefined> {
|
async consume(key: string): Promise<SessionRecord | undefined> {
|
||||||
const record = this.records.get(key);
|
const record = this.records.get(key);
|
||||||
this.records.delete(key);
|
this.records.delete(key);
|
||||||
return record && record.expires > Date.now() ? structuredClone(record) : undefined;
|
return record && record.expires > Date.now()
|
||||||
|
? structuredClone(record)
|
||||||
|
: undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,16 +11,32 @@ export interface TemplateResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
const sharedFiles: Record<string, string> = {
|
const sharedFiles: Record<string, string> = {
|
||||||
"package.json": `${JSON.stringify({
|
"package.json": `${JSON.stringify(
|
||||||
name: "my-muddle-site",
|
{
|
||||||
private: true,
|
name: "my-muddle-site",
|
||||||
type: "module",
|
private: true,
|
||||||
scripts: { dev: "muddle dev .", build: "muddle build ." },
|
type: "module",
|
||||||
dependencies: { muddle: "latest" },
|
scripts: { dev: "muddle dev .", build: "muddle build ." },
|
||||||
}, null, 2)}\n`,
|
dependencies: { muddle: "latest" },
|
||||||
"tsconfig.json": `${JSON.stringify({
|
},
|
||||||
compilerOptions: { target: "ESNext", module: "Preserve", moduleResolution: "bundler", strict: true, noEmit: true, experimentalDecorators: true, allowImportingTsExtensions: true },
|
null,
|
||||||
}, null, 2)}\n`,
|
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`,
|
"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`,
|
"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<string, string> = {
|
||||||
|
|
||||||
export const muddleTemplates: readonly MuddleTemplate[] = ["docs", "minimal"];
|
export const muddleTemplates: readonly MuddleTemplate[] = ["docs", "minimal"];
|
||||||
|
|
||||||
export async function createTemplate(name: MuddleTemplate, destination: string): Promise<TemplateResult> {
|
export async function createTemplate(
|
||||||
|
name: MuddleTemplate,
|
||||||
|
destination: string,
|
||||||
|
): Promise<TemplateResult> {
|
||||||
const output = resolve(destination);
|
const output = resolve(destination);
|
||||||
if (existsSync(output) && (await readdir(output)).length > 0) {
|
if (existsSync(output) && (await readdir(output)).length > 0) {
|
||||||
throw new Error(`Refusing to overwrite non-empty directory ${output}`);
|
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[] = [];
|
const written: string[] = [];
|
||||||
for (const [relative, source] of Object.entries(files)) {
|
for (const [relative, source] of Object.entries(files)) {
|
||||||
const file = join(output, relative);
|
const file = join(output, relative);
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,10 @@ export default defineConfig({
|
||||||
components: "samples/hello-world/modules/app/views/components",
|
components: "samples/hello-world/modules/app/views/components",
|
||||||
viewRoots: ["samples/hello-world/modules/app/views"],
|
viewRoots: ["samples/hello-world/modules/app/views"],
|
||||||
|
|
||||||
integrations: [
|
integrations: [stcssIntegration({ dir: "samples/hello-world/styles" })],
|
||||||
stcssIntegration({ dir: "samples/hello-world/styles" }),
|
|
||||||
],
|
|
||||||
|
|
||||||
prerender: {
|
prerender: {
|
||||||
routes: ["/", "/posts/test1", "/posts/test2", "/posts/test3"],
|
routes: ["/", "/posts/test1", "/posts/test2", "/posts/test3"],
|
||||||
outDir: "samples/hello-world/dist",
|
outDir: "samples/hello-world/dist",
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,17 @@
|
||||||
import { Route, type RouteOptions } from "./Route.ts";
|
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. */
|
/** Declares an Astro-compatible endpoint. */
|
||||||
export function Astro(path: string, options: AstroOptions = {}): MethodDecorator {
|
export function Astro(
|
||||||
return Route(path, { ...options, method: options.action ? "POST" : options.method ?? "GET" });
|
path: string,
|
||||||
|
options: AstroOptions = {},
|
||||||
|
): MethodDecorator {
|
||||||
|
return Route(path, {
|
||||||
|
...options,
|
||||||
|
method: options.action ? "POST" : (options.method ?? "GET"),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,21 @@
|
||||||
import { defineMemberMetadata, getMemberMetadata } from "../core/metadata.ts";
|
import { defineMemberMetadata, getMemberMetadata } from "../core/metadata.ts";
|
||||||
|
|
||||||
const KEY = "muddle:auth";
|
const KEY = "muddle:auth";
|
||||||
export interface AuthOptions { roles?: string[]; redirect?: string }
|
export interface AuthOptions {
|
||||||
|
roles?: string[];
|
||||||
|
redirect?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function Auth(options: AuthOptions = {}): MethodDecorator {
|
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 {
|
export function getAuth(
|
||||||
return getMemberMetadata(KEY, target, propertyKey)[0] as AuthOptions | undefined;
|
target: object,
|
||||||
|
propertyKey: string,
|
||||||
|
): AuthOptions | undefined {
|
||||||
|
return getMemberMetadata(KEY, target, propertyKey)[0] as
|
||||||
|
| AuthOptions
|
||||||
|
| undefined;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
return getMetadata(KEY, target, propertyKey) as RenderAheadSource | undefined;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,31 @@ export interface CollectionRenderRoute {
|
||||||
export type RenderRouteSource = string | string[] | CollectionRenderRoute;
|
export type RenderRouteSource = string | string[] | CollectionRenderRoute;
|
||||||
|
|
||||||
export function routeFolder(...segments: string[]): string {
|
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 };
|
return { collection, path };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RenderRoute(routes: RenderRouteSource[]): MethodDecorator {
|
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[] {
|
export function getRenderRoutes(
|
||||||
return (getMetadata(KEY, target, propertyKey) as RenderRouteSource[] | undefined) ?? [];
|
target: object,
|
||||||
|
propertyKey: string,
|
||||||
|
): RenderRouteSource[] {
|
||||||
|
return (
|
||||||
|
(getMetadata(KEY, target, propertyKey) as
|
||||||
|
| RenderRouteSource[]
|
||||||
|
| undefined) ?? []
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import "../core/metadata.ts";
|
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 { callerFile } from "../core/paths.ts";
|
||||||
import { dirname } from "node:path";
|
import { dirname } from "node:path";
|
||||||
import type { HttpMethod } from "../contracts/typegen/http/method.ts";
|
import type { HttpMethod } from "../contracts/typegen/http/method.ts";
|
||||||
|
|
@ -35,7 +38,10 @@ export interface ViewMetadata {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Declares the template rendered by a controller method. */
|
/** 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) => {
|
return (target, propertyKey, descriptor) => {
|
||||||
const caller = callerFile();
|
const caller = callerFile();
|
||||||
const method = String(propertyKey);
|
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.
|
* When a property key is given, only the views of that method are returned.
|
||||||
*/
|
*/
|
||||||
export function getViews(
|
export function getViews(target: object, propertyKey?: string): ViewMetadata[] {
|
||||||
target: object,
|
|
||||||
propertyKey?: string,
|
|
||||||
): ViewMetadata[] {
|
|
||||||
return getAllMemberMetadata(VIEW_METADATA_KEY, target)
|
return getAllMemberMetadata(VIEW_METADATA_KEY, target)
|
||||||
.filter(
|
.filter(
|
||||||
(entry) => propertyKey === undefined || entry.propertyKey === propertyKey,
|
(entry) => propertyKey === undefined || entry.propertyKey === propertyKey,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,11 @@
|
||||||
{ "open": "\"", "close": "\"", "notIn": ["string", "comment"] }
|
{ "open": "\"", "close": "\"", "notIn": ["string", "comment"] }
|
||||||
],
|
],
|
||||||
"surroundingPairs": [
|
"surroundingPairs": [
|
||||||
["{", "}"], ["(", ")"], ["[", "]"], ["'", "'"], ["\"", "\""]
|
["{", "}"],
|
||||||
|
["(", ")"],
|
||||||
|
["[", "]"],
|
||||||
|
["'", "'"],
|
||||||
|
["\"", "\""]
|
||||||
],
|
],
|
||||||
"indentationRules": {
|
"indentationRules": {
|
||||||
"increaseIndentPattern": "^\\s*(?:@\\w+[^;{]*)?[^{};]*\\{\\s*$|^(?:mixin|if|each|for|component)\\s+.*$",
|
"increaseIndentPattern": "^\\s*(?:@\\w+[^;{]*)?[^{};]*\\{\\s*$|^(?:mixin|if|each|for|component)\\s+.*$",
|
||||||
|
|
|
||||||
|
|
@ -1,69 +1,75 @@
|
||||||
{
|
{
|
||||||
"name": "muddle-language-tools",
|
"name": "muddle-language-tools",
|
||||||
"displayName": "Muddle Language Tools",
|
"displayName": "Muddle Language Tools",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.crit.rip/mdl/muddle.git"
|
"url": "https://git.crit.rip/mdl/muddle.git"
|
||||||
},
|
},
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Language support for Muddle SPUG templates and STCSS stylesheets",
|
"description": "Language support for Muddle SPUG templates and STCSS stylesheets",
|
||||||
"main": "./src/extension.js",
|
"main": "./src/extension.js",
|
||||||
"activationEvents": [
|
"activationEvents": [
|
||||||
"onLanguage:spug",
|
"onLanguage:spug",
|
||||||
"onLanguage:stcss"
|
"onLanguage:stcss"
|
||||||
],
|
],
|
||||||
"capabilities": {
|
"capabilities": {
|
||||||
"untrustedWorkspaces": {
|
"untrustedWorkspaces": {
|
||||||
"supported": true
|
"supported": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.85.0"
|
"vscode": "^1.85.0"
|
||||||
},
|
},
|
||||||
"categories": [
|
"categories": [
|
||||||
"Programming Languages"
|
"Programming Languages"
|
||||||
],
|
],
|
||||||
"contributes": {
|
"contributes": {
|
||||||
"languages": [
|
"languages": [
|
||||||
{
|
{
|
||||||
"id": "spug",
|
"id": "spug",
|
||||||
"extensions": [".spug", ".stpug", ".stug"],
|
"extensions": [
|
||||||
"configuration": "./language-configuration.json"
|
".spug",
|
||||||
},
|
".stpug",
|
||||||
{
|
".stug"
|
||||||
"id": "stcss",
|
],
|
||||||
"extensions": [".stcss"],
|
"configuration": "./language-configuration.json"
|
||||||
"configuration": "./language-configuration.json"
|
},
|
||||||
}
|
{
|
||||||
],
|
"id": "stcss",
|
||||||
"grammars": [
|
"extensions": [
|
||||||
{
|
".stcss"
|
||||||
"language": "spug",
|
],
|
||||||
"scopeName": "source.spug",
|
"configuration": "./language-configuration.json"
|
||||||
"path": "./syntaxes/spug.tmLanguage.json"
|
}
|
||||||
},
|
],
|
||||||
{
|
"grammars": [
|
||||||
"language": "stcss",
|
{
|
||||||
"scopeName": "source.stcss",
|
"language": "spug",
|
||||||
"path": "./syntaxes/stcss.tmLanguage.json"
|
"scopeName": "source.spug",
|
||||||
}
|
"path": "./syntaxes/spug.tmLanguage.json"
|
||||||
],
|
},
|
||||||
"snippets": [
|
{
|
||||||
{
|
"language": "stcss",
|
||||||
"language": "spug",
|
"scopeName": "source.stcss",
|
||||||
"path": "./snippets/spug.json"
|
"path": "./syntaxes/stcss.tmLanguage.json"
|
||||||
},
|
}
|
||||||
{
|
],
|
||||||
"language": "stcss",
|
"snippets": [
|
||||||
"path": "./snippets/stcss.json"
|
{
|
||||||
}
|
"language": "spug",
|
||||||
]
|
"path": "./snippets/spug.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
{
|
||||||
"@vscode/vsce": "^4.0.0"
|
"language": "stcss",
|
||||||
},
|
"path": "./snippets/stcss.json"
|
||||||
"scripts": {
|
}
|
||||||
"package": "vsce package",
|
]
|
||||||
"publish": "vsce publish"
|
},
|
||||||
}
|
"dependencies": {
|
||||||
|
"@vscode/vsce": "^4.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"package": "vsce package",
|
||||||
|
"publish": "vsce publish"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,88 @@
|
||||||
{
|
{
|
||||||
"Typed props": { "prefix": "props", "body": ["props", " ${1:name}: ${2:string}"], "description": "Declare typed SPUG props" },
|
"Typed props": {
|
||||||
"Optional prop": { "prefix": "props?", "body": ["props", " ${1:name}: ${2:string}?", ""], "description": "Declare an optional SPUG prop" },
|
"prefix": "props",
|
||||||
"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" },
|
"body": ["props", " ${1:name}: ${2:string}"],
|
||||||
"Dynamic anchor": { "prefix": "anchor+", "body": ["li@row(item.id)(${0})"], "description": "Dynamic relation anchor from an each-loop item" },
|
"description": "Declare typed SPUG props"
|
||||||
"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:<p>raw</p>}\nend"], "description": "Embed a raw HTML block" },
|
"Optional prop": {
|
||||||
"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" },
|
"prefix": "props?",
|
||||||
"Extend layout": { "prefix": "extends", "body": ["extends ./layout.spug", "", "block content", " ${0}"], "description": "Extend a layout and fill the content block" },
|
"body": ["props", " ${1:name}: ${2:string}?", ""],
|
||||||
"Render slot": { "prefix": "render", "body": ["render ${1:content}", " ${0}"], "description": "Pass a slot into a component" },
|
"description": "Declare an optional SPUG prop"
|
||||||
"Frontmatter": { "prefix": "fm", "body": ["---", "${0}", "---"], "description": "Front matter block with TypeScript imports" },
|
},
|
||||||
"Escape expression": { "prefix": "expr", "body": ["#{${1:expression}}"], "description": "Inline interpolated expression" },
|
"Relation anchor": {
|
||||||
"Raw expression": { "prefix": "raw", "body": ["!{${1:expression}}"], "description": "Inline raw unescaped expression" },
|
"prefix": "anchor",
|
||||||
"Iterate": { "prefix": "each", "body": ["each ${1:item} in ${2:items}", " ${0}"], "description": "Iterate over a list" },
|
"body": ["${1:a}.class@${2:name}(${3:id})(${0})"],
|
||||||
"Condition": { "prefix": "if", "body": ["if ${1:condition}", " ${0}"], "description": "Conditional block" },
|
"description": "Spawn a relation anchor consumed by $refer / :with / :when in STCSS"
|
||||||
"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" }
|
"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:<p>raw</p>}\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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,121 @@
|
||||||
{
|
{
|
||||||
"Predicted geometry": { "prefix": "predict", "body": ["@predict max-height max-width;"], "description": "Transition prerendered geometry" },
|
"Predicted geometry": {
|
||||||
"Predicted transitions": { "prefix": "predict-full", "body": ["@predict left top width height opacity transform;"], "description": "Full prediction transition list (pill/ribbon style)" },
|
"prefix": "predict",
|
||||||
"Measure prediction target": { "prefix": "measure", "body": ["@measure;"], "description": "Mark this selector for prerender geometry prefill (inline prediction source)" },
|
"body": ["@predict max-height max-width;"],
|
||||||
"Measure anchors": { "prefix": "measure-anchor", "body": ["@measure @${1:anchor}, ${0:.selector};"], "description": "Measure elements by relation anchor or class" },
|
"description": "Transition prerendered geometry"
|
||||||
"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" },
|
"Predicted transitions": {
|
||||||
"Each loop": { "prefix": "each", "body": ["@each $${1:item} in ${2:items} {", " ${0}", "}"], "description": "Repeat a block over a value list" },
|
"prefix": "predict-full",
|
||||||
"For loop": { "prefix": "for", "body": ["@for $${1:i} from ${2:1} through ${3:5} {", " ${0}", "}"], "description": "Count a range" },
|
"body": ["@predict left top width height opacity transform;"],
|
||||||
"Mixin declaration": { "prefix": "mixin", "body": ["@mixin ${1:name}($${2:arg}) {", " ${0}", "}"], "description": "Declare an STCSS mixin" },
|
"description": "Full prediction transition list (pill/ribbon style)"
|
||||||
"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" },
|
"Measure prediction target": {
|
||||||
"d-string": { "prefix": "d", "body": ["d\"${1:calc(#{} )}\""], "description": "String with brace expressions evaluated at compile time" },
|
"prefix": "measure",
|
||||||
"$refer geometry": { "prefix": "refer", "body": ["$refer[${1:left}](@${2:anchor})"], "description": "Read pre-computed anchor geometry (left/top/width/height)" },
|
"body": ["@measure;"],
|
||||||
"$refer interpolated location": { "prefix": "refer-between", "body": ["calc($refer[${1:left}](@${2:anchor}) - $refer[${3:left}](@${4:relative}))"], "description": "Compute location between two anchors" },
|
"description": "Mark this selector for prerender geometry prefill (inline prediction source)"
|
||||||
"$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" },
|
"Measure anchors": {
|
||||||
"Relation state selector": { "prefix": "with-state", "body": [":with(@${1:anchor}:${2|active,clicked,focus|})"], "description": "Anchor state selection (:has under the hood)" },
|
"prefix": "measure-anchor",
|
||||||
"When selector": { "prefix": "when-state", "body": [":when(${1:root} @${2:anchor}:${3|active,clicked,focus|})"], "description": "Scoped anchor state selection" },
|
"body": ["@measure @${1:anchor}, ${0:.selector};"],
|
||||||
"Concentric radius": { "prefix": "concentric", "body": ["border-radius: concentric ${1:0.75rem};"], "description": "Concentric nested corner radii" },
|
"description": "Measure elements by relation anchor or class"
|
||||||
"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" },
|
"Responsive rule": {
|
||||||
"View transition": { "prefix": "view-transition", "body": ["@view-transition ${1:name};"], "description": "Emit a css view-transition-name" },
|
"prefix": "responsive",
|
||||||
"Compile-time values": { "prefix": "javascript", "body": ["javascript!", " const ${1:space} = ${2:1};", "end"], "description": "Add compile-time values" }
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,46 @@
|
||||||
const vscode = require("vscode");
|
const vscode = require("vscode");
|
||||||
|
|
||||||
const AT_RULES = new Set([
|
const AT_RULES = new Set([
|
||||||
"each", "for", "from", "through", "include", "responsive", "media", "supports",
|
"each",
|
||||||
"concentric", "view-transition", "predict", "measure", "property", "mixin",
|
"for",
|
||||||
"stcss", "import", "charset", "document", "font-face", "keyframes", "layer",
|
"from",
|
||||||
"namespace", "page", "scope", "starting-style", "container",
|
"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([
|
const AT_RULE_DOCS = new Map([
|
||||||
["mixin", "Declare a reusable STCSS mixin: `@mixin name($param) { … }`"],
|
["mixin", "Declare a reusable STCSS mixin: `@mixin name($param) { … }`"],
|
||||||
|
|
@ -19,28 +52,98 @@ const AT_RULE_DOCS = new Map([
|
||||||
["supports", "Standard `@supports` feature wrapper."],
|
["supports", "Standard `@supports` feature wrapper."],
|
||||||
["concentric", "Emit concentric nested radii: `@concentric 0.75rem { … }`"],
|
["concentric", "Emit concentric nested radii: `@concentric 0.75rem { … }`"],
|
||||||
["view-transition", "Emit `view-transition-name` on the attached rule."],
|
["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 = [
|
const CSS_PROPERTIES = [
|
||||||
"background", "background-color", "background-image", "border", "border-color", "border-radius", "border-width",
|
"background",
|
||||||
"bottom", "box-shadow", "box-sizing", "color", "cursor", "display", "flex", "flex-basis", "flex-direction",
|
"background-color",
|
||||||
"flex-shrink", "flex-wrap", "font-family", "font-size", "font-weight", "gap", "grid-area", "height", "inset",
|
"background-image",
|
||||||
"justify-content", "left", "letter-spacing", "line-height", "margin", "max-height", "max-width", "min-height",
|
"border",
|
||||||
"min-width", "opacity", "overflow", "padding", "pointer-events", "position", "right", "text-decoration",
|
"border-color",
|
||||||
"text-transform", "top", "transform", "transition", "vertical-align", "visibility", "white-space", "width",
|
"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",
|
"z-index",
|
||||||
];
|
];
|
||||||
|
|
||||||
const HTML_ATTRIBUTES = [
|
const HTML_ATTRIBUTES = [
|
||||||
"alt", "aria-controls", "aria-current", "aria-description", "aria-expanded", "aria-hidden", "aria-label",
|
"alt",
|
||||||
"aria-live", "autocomplete", "class", "content", "href", "id", "placeholder", "rel", "role", "src", "style",
|
"aria-controls",
|
||||||
"tabindex", "target", "title", "type",
|
"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 = {
|
const INDEX = {
|
||||||
|
|
@ -78,11 +181,17 @@ async function readText(uri) {
|
||||||
function posFromOffset(text, offset) {
|
function posFromOffset(text, offset) {
|
||||||
const before = text.slice(0, offset);
|
const before = text.slice(0, offset);
|
||||||
const newline = before.lastIndexOf("\n");
|
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) {
|
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) {
|
function isStcss(document) {
|
||||||
|
|
@ -121,7 +230,12 @@ function indexStcssDocument(uri, text) {
|
||||||
const anchorRegex = /@([A-Za-z_][\w-]*)/g;
|
const anchorRegex = /@([A-Za-z_][\w-]*)/g;
|
||||||
while ((match = anchorRegex.exec(text))) {
|
while ((match = anchorRegex.exec(text))) {
|
||||||
if (AT_RULES.has(match[1])) continue;
|
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.spugFiles = spugFiles;
|
||||||
INDEX.styleFiles = styleFiles;
|
INDEX.styleFiles = styleFiles;
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
...spugFiles.map(async (uri) => indexSpugDocument(uri, await readText(uri))),
|
...spugFiles.map(async (uri) =>
|
||||||
...styleFiles.map(async (uri) => indexStcssDocument(uri, await readText(uri))),
|
indexSpugDocument(uri, await readText(uri)),
|
||||||
|
),
|
||||||
|
...styleFiles.map(async (uri) =>
|
||||||
|
indexStcssDocument(uri, await readText(uri)),
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function anchorCompletions() {
|
function anchorCompletions() {
|
||||||
return [...INDEX.anchors.keys()].map((name) => {
|
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";
|
item.detail = "relation anchor";
|
||||||
const first = INDEX.anchors.get(name)[0];
|
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;
|
return item;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function stcssCompletions() {
|
function stcssCompletions() {
|
||||||
const atRules = [...AT_RULE_DOCS.keys()].map((name) => {
|
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.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));
|
item.documentation = new vscode.MarkdownString(AT_RULE_DOCS.get(name));
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
const metrics = ["left", "top", "width", "height"].map((metric) => {
|
const metrics = ["left", "top", "width", "height"].map((metric) => {
|
||||||
const item = new vscode.CompletionItem(`$refer[${metric}](@)`, vscode.CompletionItemKind.Function);
|
const item = new vscode.CompletionItem(
|
||||||
item.insertText = new vscode.SnippetString(`$refer[${metric}](@\${1:anchor})`);
|
`$refer[${metric}](@)`,
|
||||||
|
vscode.CompletionItemKind.Function,
|
||||||
|
);
|
||||||
|
item.insertText = new vscode.SnippetString(
|
||||||
|
`$refer[${metric}](@\${1:anchor})`,
|
||||||
|
);
|
||||||
item.documentation = new vscode.MarkdownString(REFERENCE_DOC);
|
item.documentation = new vscode.MarkdownString(REFERENCE_DOC);
|
||||||
return item;
|
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.insertText = new vscode.SnippetString("$relate(@${1:anchor})");
|
||||||
relate.documentation = new vscode.MarkdownString(RELATE_DOC);
|
relate.documentation = new vscode.MarkdownString(RELATE_DOC);
|
||||||
const states = [...STATE_WORDS].map((state) => {
|
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";
|
item.detail = "anchor state";
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
const properties = CSS_PROPERTIES.map((name) => {
|
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;`);
|
item.insertText = new vscode.SnippetString(`${name}: $0;`);
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
const variables = [...INDEX.variables.keys()].map((name) => {
|
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";
|
item.detail = "stcss variable";
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
return [...atRules, ...metrics, relate, ...states, ...properties, ...variables];
|
return [
|
||||||
|
...atRules,
|
||||||
|
...metrics,
|
||||||
|
relate,
|
||||||
|
...states,
|
||||||
|
...properties,
|
||||||
|
...variables,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function spugCompletions() {
|
function spugCompletions() {
|
||||||
const keywords = ["extends", "include", "block", "append", "prepend", "mixin", "slot", "yield", "island", "component", "render", "doctype"].map((word) => {
|
const keywords = [
|
||||||
const item = new vscode.CompletionItem(word, vscode.CompletionItemKind.Keyword);
|
"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;
|
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");
|
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 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`);
|
item.insertText = new vscode.SnippetString(`${name}=$0`);
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
return [...keywords, javascript, ...control, ...attributes, ...anchorCompletions()];
|
return [
|
||||||
|
...keywords,
|
||||||
|
javascript,
|
||||||
|
...control,
|
||||||
|
...attributes,
|
||||||
|
...anchorCompletions(),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function stcssHover(document, position) {
|
function stcssHover(document, position) {
|
||||||
|
|
@ -212,14 +408,20 @@ function stcssHover(document, position) {
|
||||||
const variable = /\$[\w-]+$/.exec(preceding)?.[0];
|
const variable = /\$[\w-]+$/.exec(preceding)?.[0];
|
||||||
if (variable) {
|
if (variable) {
|
||||||
const hits = INDEX.variables.get(variable) ?? [];
|
const hits = INDEX.variables.get(variable) ?? [];
|
||||||
return new vscode.Hover(new vscode.MarkdownString(
|
return new vscode.Hover(
|
||||||
hits.length ? `**${variable}**: defined in ${hits.map((h) => `${relativeToWorkspace(h.uri)}:${h.line}`).join(", ")}` : `**${variable}**`,
|
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];
|
const refer = /(?:\$\w+(?:\[[\w-]+\])?)$/.exec(preceding)?.[0];
|
||||||
if (refer) {
|
if (refer) {
|
||||||
if (refer.startsWith("$refer")) return new vscode.Hover(new vscode.MarkdownString(REFERENCE_DOC));
|
if (refer.startsWith("$refer"))
|
||||||
if (refer.startsWith("$relate")) return new vscode.Hover(new vscode.MarkdownString(RELATE_DOC));
|
return new vscode.Hover(new vscode.MarkdownString(REFERENCE_DOC));
|
||||||
|
if (refer.startsWith("$relate"))
|
||||||
|
return new vscode.Hover(new vscode.MarkdownString(RELATE_DOC));
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -227,15 +429,30 @@ function stcssHover(document, position) {
|
||||||
function hoverAnchor(name) {
|
function hoverAnchor(name) {
|
||||||
const hits = INDEX.anchors.get(name) ?? [];
|
const hits = INDEX.anchors.get(name) ?? [];
|
||||||
if (!hits.length) return null;
|
if (!hits.length) return null;
|
||||||
const body = hits.slice(0, 8).map((hit) => `- ${hit.kind}: \`${relativeToWorkspace(hit.uri)}:${hit.line}\``).join("\n");
|
const body = hits
|
||||||
return new vscode.Hover(new vscode.MarkdownString(`**@${name}**: relation anchor\n\n${body}`));
|
.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) {
|
function spugHover(document, position) {
|
||||||
const range = document.getWordRangeAtPosition(position, /@?[A-Za-z_][\w-]*/);
|
const range = document.getWordRangeAtPosition(position, /@?[A-Za-z_][\w-]*/);
|
||||||
if (!range) return null;
|
if (!range) return null;
|
||||||
const word = document.getText(range);
|
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 hoverAnchor(word.replace(/^@/, ""));
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -244,23 +461,38 @@ function spugHover(document, position) {
|
||||||
function anchorDefinition(name) {
|
function anchorDefinition(name) {
|
||||||
return (INDEX.anchors.get(name) ?? [])
|
return (INDEX.anchors.get(name) ?? [])
|
||||||
.filter((hit) => hit.kind === "declared in SPUG")
|
.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) {
|
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];
|
const anchor = /@([A-Za-z_][\w-]*)[^\w]?$/.exec(line)?.[1];
|
||||||
if (anchor) return anchorDefinition(anchor);
|
if (anchor) return anchorDefinition(anchor);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function spugDefinition(document, position) {
|
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];
|
const anchor = /@([A-Za-z_][\w-]*)[^\w]?$/.exec(before)?.[1];
|
||||||
if (anchor) {
|
if (anchor) {
|
||||||
// jump from anchor to its STCSS references
|
// jump from anchor to its STCSS references
|
||||||
const refs = (INDEX.anchors.get(anchor) ?? []).filter((hit) => hit.kind === "referenced in STCSS");
|
const refs = (INDEX.anchors.get(anchor) ?? []).filter(
|
||||||
return refs.map((hit) => new vscode.Location(hit.uri, new vscode.Range(hit.line - 1, 0, hit.line - 1, 200)));
|
(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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -270,7 +502,8 @@ function diagnosticCollection() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function lintDocument(document, collection) {
|
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 text = document.getText();
|
||||||
const issues = [];
|
const issues = [];
|
||||||
|
|
||||||
|
|
@ -285,13 +518,25 @@ function lintDocument(document, collection) {
|
||||||
} else if (char === "}") {
|
} else if (char === "}") {
|
||||||
depth -= 1;
|
depth -= 1;
|
||||||
if (depth < 0) {
|
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;
|
depth = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (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") {
|
if (document.languageId === "stcss") {
|
||||||
|
|
@ -301,14 +546,23 @@ function lintDocument(document, collection) {
|
||||||
const name = match[1];
|
const name = match[1];
|
||||||
const offset = match.index;
|
const offset = match.index;
|
||||||
const previous = text[offset - 1];
|
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 (AT_RULES.has(name) || previous === "$" || dynamic) continue;
|
||||||
if (!INDEX.anchors.has(name)) {
|
if (!INDEX.anchors.has(name)) {
|
||||||
issues.push(new vscode.Diagnostic(
|
issues.push(
|
||||||
rangeOf(text, offset, match[0].length),
|
new vscode.Diagnostic(
|
||||||
`No SPUG file declares relation anchor \`@${name}\`.`,
|
rangeOf(text, offset, match[0].length),
|
||||||
vscode.DiagnosticSeverity.Warning,
|
`No SPUG file declares relation anchor \`@${name}\`.`,
|
||||||
));
|
vscode.DiagnosticSeverity.Warning,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -318,7 +572,8 @@ function lintDocument(document, collection) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function lintVisible() {
|
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) {
|
function symbolKindFor(document) {
|
||||||
|
|
@ -339,15 +594,22 @@ function symbolsOf(document) {
|
||||||
"",
|
"",
|
||||||
symbolKindFor(document),
|
symbolKindFor(document),
|
||||||
new vscode.Range(line, 0, line, text.length),
|
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 });
|
flat.push({ symbol, indent: match[1].length });
|
||||||
}
|
}
|
||||||
const roots = [];
|
const roots = [];
|
||||||
const stack = [];
|
const stack = [];
|
||||||
for (const entry of flat) {
|
for (const entry of flat) {
|
||||||
while (stack.length && entry.indent <= stack[stack.length - 1].indent) stack.pop();
|
while (stack.length && entry.indent <= stack[stack.length - 1].indent)
|
||||||
if (stack.length) stack[stack.length - 1].symbol.children.push(entry.symbol);
|
stack.pop();
|
||||||
|
if (stack.length)
|
||||||
|
stack[stack.length - 1].symbol.children.push(entry.symbol);
|
||||||
else roots.push(entry.symbol);
|
else roots.push(entry.symbol);
|
||||||
stack.push(entry);
|
stack.push(entry);
|
||||||
}
|
}
|
||||||
|
|
@ -372,14 +634,19 @@ function foldingOf(document) {
|
||||||
if (!text.trim()) continue;
|
if (!text.trim()) continue;
|
||||||
while (stack.length && indent <= stack[stack.length - 1].indent) {
|
while (stack.length && indent <= stack[stack.length - 1].indent) {
|
||||||
const start = stack.pop();
|
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 });
|
stack.push({ indent, line });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
while (stack.length) {
|
while (stack.length) {
|
||||||
const start = typeof stack[stack.length - 1] === "number" ? stack.pop() : stack.pop().line;
|
const start =
|
||||||
if (document.lineCount - 1 > start) ranges.push(new vscode.FoldingRange(start, document.lineCount - 1));
|
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;
|
return ranges;
|
||||||
}
|
}
|
||||||
|
|
@ -393,7 +660,12 @@ function completionsFor(document, position) {
|
||||||
const prefix = before.trim();
|
const prefix = before.trim();
|
||||||
if (prefix.length === 0 && position.character > 0) return [];
|
if (prefix.length === 0 && position.character > 0) return [];
|
||||||
// completions popped while just typing whitespace mid-line are noise
|
// 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);
|
return isStcss(document) ? stcssCompletions(before) : spugCompletions(before);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -412,8 +684,17 @@ function activate(context) {
|
||||||
|
|
||||||
vscode.languages.registerCompletionItemProvider(
|
vscode.languages.registerCompletionItemProvider(
|
||||||
["stcss", "spug"],
|
["stcss", "spug"],
|
||||||
{ provideCompletionItems(document, position) { return completionsFor(document, position); } },
|
{
|
||||||
"@", "$", ".", "-", ":", "=",
|
provideCompletionItems(document, position) {
|
||||||
|
return completionsFor(document, position);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"@",
|
||||||
|
"$",
|
||||||
|
".",
|
||||||
|
"-",
|
||||||
|
":",
|
||||||
|
"=",
|
||||||
),
|
),
|
||||||
|
|
||||||
vscode.languages.registerHoverProvider(["stcss", "spug"], {
|
vscode.languages.registerHoverProvider(["stcss", "spug"], {
|
||||||
|
|
@ -421,7 +702,9 @@ function activate(context) {
|
||||||
const word = document.getWordRangeAtPosition(position, /[@$]?[\w-]+/);
|
const word = document.getWordRangeAtPosition(position, /[@$]?[\w-]+/);
|
||||||
if (!word) return null;
|
if (!word) return null;
|
||||||
try {
|
try {
|
||||||
return isStcss(document) ? stcssHover(document, position) : spugHover(document, position);
|
return isStcss(document)
|
||||||
|
? stcssHover(document, position)
|
||||||
|
: spugHover(document, position);
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -431,7 +714,9 @@ function activate(context) {
|
||||||
vscode.languages.registerDefinitionProvider(["stcss", "spug"], {
|
vscode.languages.registerDefinitionProvider(["stcss", "spug"], {
|
||||||
provideDefinition(document, position) {
|
provideDefinition(document, position) {
|
||||||
try {
|
try {
|
||||||
return isStcss(document) ? stcssDefinition(document, position) : spugDefinition(document, position);
|
return isStcss(document)
|
||||||
|
? stcssDefinition(document, position)
|
||||||
|
: spugDefinition(document, position);
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -439,14 +724,20 @@ function activate(context) {
|
||||||
}),
|
}),
|
||||||
|
|
||||||
vscode.languages.registerDocumentSymbolProvider(["stcss", "spug"], {
|
vscode.languages.registerDocumentSymbolProvider(["stcss", "spug"], {
|
||||||
provideDocumentSymbols(document) { return symbolsOf(document); },
|
provideDocumentSymbols(document) {
|
||||||
|
return symbolsOf(document);
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
vscode.languages.registerFoldingRangeProvider(["stcss", "spug"], {
|
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) => {
|
vscode.workspace.onDidChangeTextDocument((event) => {
|
||||||
lintDocument(event.document, diagnostics);
|
lintDocument(event.document, diagnostics);
|
||||||
refresh();
|
refresh();
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,13 @@
|
||||||
"frontmatter": {
|
"frontmatter": {
|
||||||
"name": "meta.embedded.block.ts.spug",
|
"name": "meta.embedded.block.ts.spug",
|
||||||
"begin": "^---\\s*$",
|
"begin": "^---\\s*$",
|
||||||
"beginCaptures": { "0": { "name": "punctuation.definition.frontmatter.begin.spug" } },
|
"beginCaptures": {
|
||||||
|
"0": { "name": "punctuation.definition.frontmatter.begin.spug" }
|
||||||
|
},
|
||||||
"end": "^---\\s*$",
|
"end": "^---\\s*$",
|
||||||
"endCaptures": { "0": { "name": "punctuation.definition.frontmatter.end.spug" } },
|
"endCaptures": {
|
||||||
|
"0": { "name": "punctuation.definition.frontmatter.end.spug" }
|
||||||
|
},
|
||||||
"patterns": [{ "include": "source.ts" }]
|
"patterns": [{ "include": "source.ts" }]
|
||||||
},
|
},
|
||||||
"javascript-block": {
|
"javascript-block": {
|
||||||
|
|
@ -133,7 +137,9 @@
|
||||||
"2": { "name": "punctuation.definition.string.begin.stcss" }
|
"2": { "name": "punctuation.definition.string.begin.stcss" }
|
||||||
},
|
},
|
||||||
"end": "(?<!\\\\)\\2",
|
"end": "(?<!\\\\)\\2",
|
||||||
"endCaptures": { "0": { "name": "punctuation.definition.string.end.stcss" } },
|
"endCaptures": {
|
||||||
|
"0": { "name": "punctuation.definition.string.end.stcss" }
|
||||||
|
},
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{ "include": "#interpolation" },
|
{ "include": "#interpolation" },
|
||||||
{ "include": "#expression" }
|
{ "include": "#expression" }
|
||||||
|
|
@ -142,24 +148,37 @@
|
||||||
"output": {
|
"output": {
|
||||||
"contentName": "meta.embedded.expression.ts",
|
"contentName": "meta.embedded.expression.ts",
|
||||||
"begin": "(!?=)",
|
"begin": "(!?=)",
|
||||||
"beginCaptures": { "1": { "name": "punctuation.separator.filters.spug" } },
|
"beginCaptures": {
|
||||||
|
"1": { "name": "punctuation.separator.filters.spug" }
|
||||||
|
},
|
||||||
"end": "$",
|
"end": "$",
|
||||||
"patterns": [{ "include": "#expression" }]
|
"patterns": [{ "include": "#expression" }]
|
||||||
},
|
},
|
||||||
"interpolation": {
|
"interpolation": {
|
||||||
"name": "meta.interpolation.spug",
|
"name": "meta.interpolation.spug",
|
||||||
"begin": "([#!])\\{",
|
"begin": "([#!])\\{",
|
||||||
"beginCaptures": { "0": { "name": "punctuation.definition.interpolation.begin.spug" } },
|
"beginCaptures": {
|
||||||
|
"0": { "name": "punctuation.definition.interpolation.begin.spug" }
|
||||||
|
},
|
||||||
"end": "\\}",
|
"end": "\\}",
|
||||||
"endCaptures": { "0": { "name": "punctuation.definition.interpolation.end.spug" } },
|
"endCaptures": {
|
||||||
|
"0": { "name": "punctuation.definition.interpolation.end.spug" }
|
||||||
|
},
|
||||||
"contentName": "meta.embedded.expression.ts",
|
"contentName": "meta.embedded.expression.ts",
|
||||||
"patterns": [{ "include": "#curly-nesting" }, { "include": "#expression" }]
|
"patterns": [
|
||||||
|
{ "include": "#curly-nesting" },
|
||||||
|
{ "include": "#expression" }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"curly-nesting": {
|
"curly-nesting": {
|
||||||
"begin": "\\{",
|
"begin": "\\{",
|
||||||
"beginCaptures": { "0": { "name": "punctuation.definition.brace.curly.spug" } },
|
"beginCaptures": {
|
||||||
|
"0": { "name": "punctuation.definition.brace.curly.spug" }
|
||||||
|
},
|
||||||
"end": "\\}",
|
"end": "\\}",
|
||||||
"endCaptures": { "0": { "name": "punctuation.definition.brace.curly.spug" } },
|
"endCaptures": {
|
||||||
|
"0": { "name": "punctuation.definition.brace.curly.spug" }
|
||||||
|
},
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{ "include": "#curly-nesting" },
|
{ "include": "#curly-nesting" },
|
||||||
{ "include": "#expression" }
|
{ "include": "#expression" }
|
||||||
|
|
@ -191,9 +210,15 @@
|
||||||
"match": "\\b(?:if|else|each|while|return|function|const|let|var|import|from|export)\\b",
|
"match": "\\b(?:if|else|each|while|return|function|const|let|var|import|from|export)\\b",
|
||||||
"name": "keyword.control.ts"
|
"name": "keyword.control.ts"
|
||||||
},
|
},
|
||||||
{ "match": "\\b\\d+(?:\\.\\d+)?\\b", "name": "constant.numeric.decimal.js" },
|
{
|
||||||
|
"match": "\\b\\d+(?:\\.\\d+)?\\b",
|
||||||
|
"name": "constant.numeric.decimal.js"
|
||||||
|
},
|
||||||
{ "match": "[A-Za-z_$][\\w$]*", "name": "variable.other.js" },
|
{ "match": "[A-Za-z_$][\\w$]*", "name": "variable.other.js" },
|
||||||
{ "match": "&&|\\|\\||[?:,+\\-*/%<>=!]=[?]?|[?:,+\\-*/%<>=!]", "name": "keyword.operator.js" }
|
{
|
||||||
|
"match": "&&|\\|\\||[?:,+\\-*/%<>=!]=[?]?|[?:,+\\-*/%<>=!]",
|
||||||
|
"name": "keyword.operator.js"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,9 +53,14 @@
|
||||||
"begin": "^([^\\n{}]*?)\\s*\\{",
|
"begin": "^([^\\n{}]*?)\\s*\\{",
|
||||||
"end": "\\}",
|
"end": "\\}",
|
||||||
"beginCaptures": {
|
"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": [
|
"patterns": [
|
||||||
{ "include": "#comment" },
|
{ "include": "#comment" },
|
||||||
{ "include": "#javascript-block" },
|
{ "include": "#javascript-block" },
|
||||||
|
|
@ -132,9 +137,13 @@
|
||||||
"attribute-selector": {
|
"attribute-selector": {
|
||||||
"name": "meta.attribute-selector.stcss",
|
"name": "meta.attribute-selector.stcss",
|
||||||
"begin": "\\[",
|
"begin": "\\[",
|
||||||
"beginCaptures": { "0": { "name": "punctuation.definition.attribute-selector.begin.stcss" } },
|
"beginCaptures": {
|
||||||
|
"0": { "name": "punctuation.definition.attribute-selector.begin.stcss" }
|
||||||
|
},
|
||||||
"end": "\\]",
|
"end": "\\]",
|
||||||
"endCaptures": { "0": { "name": "punctuation.definition.attribute-selector.end.stcss" } },
|
"endCaptures": {
|
||||||
|
"0": { "name": "punctuation.definition.attribute-selector.end.stcss" }
|
||||||
|
},
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{ "include": "#interpolation" },
|
{ "include": "#interpolation" },
|
||||||
{ "include": "#variable" },
|
{ "include": "#variable" },
|
||||||
|
|
@ -169,16 +178,27 @@
|
||||||
"interpolation": {
|
"interpolation": {
|
||||||
"name": "meta.interpolation.stcss",
|
"name": "meta.interpolation.stcss",
|
||||||
"begin": "#\\{",
|
"begin": "#\\{",
|
||||||
"beginCaptures": { "0": { "name": "punctuation.definition.interpolation.begin.stcss" } },
|
"beginCaptures": {
|
||||||
|
"0": { "name": "punctuation.definition.interpolation.begin.stcss" }
|
||||||
|
},
|
||||||
"end": "\\}",
|
"end": "\\}",
|
||||||
"endCaptures": { "0": { "name": "punctuation.definition.interpolation.end.stcss" } },
|
"endCaptures": {
|
||||||
"patterns": [{ "include": "#curly-nesting" }, { "include": "#expression" }]
|
"0": { "name": "punctuation.definition.interpolation.end.stcss" }
|
||||||
|
},
|
||||||
|
"patterns": [
|
||||||
|
{ "include": "#curly-nesting" },
|
||||||
|
{ "include": "#expression" }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"curly-nesting": {
|
"curly-nesting": {
|
||||||
"begin": "\\{",
|
"begin": "\\{",
|
||||||
"beginCaptures": { "0": { "name": "punctuation.definition.brace.curly.stcss" } },
|
"beginCaptures": {
|
||||||
|
"0": { "name": "punctuation.definition.brace.curly.stcss" }
|
||||||
|
},
|
||||||
"end": "\\}",
|
"end": "\\}",
|
||||||
"endCaptures": { "0": { "name": "punctuation.definition.brace.curly.stcss" } },
|
"endCaptures": {
|
||||||
|
"0": { "name": "punctuation.definition.brace.curly.stcss" }
|
||||||
|
},
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{ "include": "#curly-nesting" },
|
{ "include": "#curly-nesting" },
|
||||||
{ "include": "#expression" }
|
{ "include": "#expression" }
|
||||||
|
|
@ -192,15 +212,24 @@
|
||||||
"2": { "name": "punctuation.definition.string.begin.stcss" }
|
"2": { "name": "punctuation.definition.string.begin.stcss" }
|
||||||
},
|
},
|
||||||
"end": "(?<!\\\\)\\2",
|
"end": "(?<!\\\\)\\2",
|
||||||
"endCaptures": { "0": { "name": "punctuation.definition.string.end.stcss" } },
|
"endCaptures": {
|
||||||
"patterns": [{ "include": "#interpolation" }, { "include": "#expression" }]
|
"0": { "name": "punctuation.definition.string.end.stcss" }
|
||||||
|
},
|
||||||
|
"patterns": [
|
||||||
|
{ "include": "#interpolation" },
|
||||||
|
{ "include": "#expression" }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"string": {
|
"string": {
|
||||||
"name": "string.quoted.stcss",
|
"name": "string.quoted.stcss",
|
||||||
"begin": "(['\"])",
|
"begin": "(['\"])",
|
||||||
"beginCaptures": { "0": { "name": "punctuation.definition.string.begin.stcss" } },
|
"beginCaptures": {
|
||||||
|
"0": { "name": "punctuation.definition.string.begin.stcss" }
|
||||||
|
},
|
||||||
"end": "(?<!\\\\)\\1",
|
"end": "(?<!\\\\)\\1",
|
||||||
"endCaptures": { "0": { "name": "punctuation.definition.string.end.stcss" } }
|
"endCaptures": {
|
||||||
|
"0": { "name": "punctuation.definition.string.end.stcss" }
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"number": {
|
"number": {
|
||||||
"match": "(-)?(?<!\\w)(\\d+\\.?\\d*)(rem|em|px|vh|vw|%|ms|s|vmin|vmax|fr|deg)?\\b",
|
"match": "(-)?(?<!\\w)(\\d+\\.?\\d*)(rem|em|px|vh|vw|%|ms|s|vmin|vmax|fr|deg)?\\b",
|
||||||
|
|
@ -239,7 +268,10 @@
|
||||||
{ "include": "#number" },
|
{ "include": "#number" },
|
||||||
{ "include": "#color" },
|
{ "include": "#color" },
|
||||||
{ "include": "#variable" },
|
{ "include": "#variable" },
|
||||||
{ "match": "--[\\w-]+", "name": "variable.other.css.custom-property.stcss" },
|
{
|
||||||
|
"match": "--[\\w-]+",
|
||||||
|
"name": "variable.other.css.custom-property.stcss"
|
||||||
|
},
|
||||||
{ "include": "#function-call" },
|
{ "include": "#function-call" },
|
||||||
{ "include": "#operator" }
|
{ "include": "#operator" }
|
||||||
]
|
]
|
||||||
|
|
|
||||||
105
index.ts
105
index.ts
|
|
@ -58,10 +58,19 @@ export {
|
||||||
} from "./core/container.ts";
|
} from "./core/container.ts";
|
||||||
export { resolveConfig, loadConfig } from "./core/config.ts";
|
export { resolveConfig, loadConfig } from "./core/config.ts";
|
||||||
export { ServerBuildContext, TemplateContext } from "./core/context.ts";
|
export { ServerBuildContext, TemplateContext } from "./core/context.ts";
|
||||||
export { MemorySessionStore, type SessionStore, type SessionRecord } from "./core/sessions.ts";
|
export {
|
||||||
|
MemorySessionStore,
|
||||||
|
type SessionStore,
|
||||||
|
type SessionRecord,
|
||||||
|
} from "./core/sessions.ts";
|
||||||
export { RoutePipeline, type RouteGuard } from "./core/pipeline.ts";
|
export { RoutePipeline, type RouteGuard } from "./core/pipeline.ts";
|
||||||
export { format } from "./core/format.ts";
|
export { format } from "./core/format.ts";
|
||||||
export { createTemplate, muddleTemplates, type MuddleTemplate, type TemplateResult } from "./core/templates.ts";
|
export {
|
||||||
|
createTemplate,
|
||||||
|
muddleTemplates,
|
||||||
|
type MuddleTemplate,
|
||||||
|
type TemplateResult,
|
||||||
|
} from "./core/templates.ts";
|
||||||
export {
|
export {
|
||||||
MuddleError,
|
MuddleError,
|
||||||
RouteNotFoundError,
|
RouteNotFoundError,
|
||||||
|
|
@ -122,8 +131,19 @@ export {
|
||||||
export { Route, getRoutes } from "./decorators/Route.ts";
|
export { Route, getRoutes } from "./decorators/Route.ts";
|
||||||
export { Service } from "./decorators/Service.ts";
|
export { Service } from "./decorators/Service.ts";
|
||||||
export { View, getViews } from "./decorators/View.ts";
|
export { View, getViews } from "./decorators/View.ts";
|
||||||
export { RenderAhead, getRenderAhead, type RenderAheadSource } from "./decorators/RenderAhead.ts";
|
export {
|
||||||
export { RenderRoute, getRenderRoutes, routeFolder, collectionRoute, type CollectionRenderRoute, type RenderRouteSource } from "./decorators/RenderRoute.ts";
|
RenderAhead,
|
||||||
|
getRenderAhead,
|
||||||
|
type RenderAheadSource,
|
||||||
|
} from "./decorators/RenderAhead.ts";
|
||||||
|
export {
|
||||||
|
RenderRoute,
|
||||||
|
getRenderRoutes,
|
||||||
|
routeFolder,
|
||||||
|
collectionRoute,
|
||||||
|
type CollectionRenderRoute,
|
||||||
|
type RenderRouteSource,
|
||||||
|
} from "./decorators/RenderRoute.ts";
|
||||||
export { Astro, type AstroOptions } from "./decorators/Astro.ts";
|
export { Astro, type AstroOptions } from "./decorators/Astro.ts";
|
||||||
export { Auth, getAuth, type AuthOptions } from "./decorators/Auth.ts";
|
export { Auth, getAuth, type AuthOptions } from "./decorators/Auth.ts";
|
||||||
|
|
||||||
|
|
@ -145,8 +165,24 @@ export {
|
||||||
type ServerComponent,
|
type ServerComponent,
|
||||||
type SpugComponentRender,
|
type SpugComponentRender,
|
||||||
} from "./views/components.ts";
|
} from "./views/components.ts";
|
||||||
export { compileCss, stcssLoader, sassLoader, styleTag, type SassLoaderOptions } from "./views/css/index.ts";
|
export {
|
||||||
export { analyzeReactComponent, backportProject, cssToStcss, detectBackportSource, htmlToSpug, reactToSpug, type BackportResult, type BackportSource, type ReactHookAnalysis } from "./core/backport.ts";
|
compileCss,
|
||||||
|
stcssLoader,
|
||||||
|
sassLoader,
|
||||||
|
styleTag,
|
||||||
|
type SassLoaderOptions,
|
||||||
|
} from "./views/css/index.ts";
|
||||||
|
export {
|
||||||
|
analyzeReactComponent,
|
||||||
|
backportProject,
|
||||||
|
cssToStcss,
|
||||||
|
detectBackportSource,
|
||||||
|
htmlToSpug,
|
||||||
|
reactToSpug,
|
||||||
|
type BackportResult,
|
||||||
|
type BackportSource,
|
||||||
|
type ReactHookAnalysis,
|
||||||
|
} from "./core/backport.ts";
|
||||||
export {
|
export {
|
||||||
markdownLoader,
|
markdownLoader,
|
||||||
parseFrontmatter,
|
parseFrontmatter,
|
||||||
|
|
@ -162,8 +198,18 @@ export {
|
||||||
type RenderContext,
|
type RenderContext,
|
||||||
type TemplateRuntime,
|
type TemplateRuntime,
|
||||||
} from "./views/engine/runtime.ts";
|
} from "./views/engine/runtime.ts";
|
||||||
export { SPUG_COMPLETIONS, STCSS_COMPLETIONS, type LanguageCompletion } from "./views/language.ts";
|
export {
|
||||||
export { stage, StageManager, type KeyframeStep, type ScrollSceneOptions, type TransitionPortalOptions } from "./views/stage.ts";
|
SPUG_COMPLETIONS,
|
||||||
|
STCSS_COMPLETIONS,
|
||||||
|
type LanguageCompletion,
|
||||||
|
} from "./views/language.ts";
|
||||||
|
export {
|
||||||
|
stage,
|
||||||
|
StageManager,
|
||||||
|
type KeyframeStep,
|
||||||
|
type ScrollSceneOptions,
|
||||||
|
type TransitionPortalOptions,
|
||||||
|
} from "./views/stage.ts";
|
||||||
export {
|
export {
|
||||||
createHooks,
|
createHooks,
|
||||||
defineHooks,
|
defineHooks,
|
||||||
|
|
@ -181,7 +227,12 @@ export {
|
||||||
type StcssValueHookContext,
|
type StcssValueHookContext,
|
||||||
} from "./views/hooks.ts";
|
} from "./views/hooks.ts";
|
||||||
|
|
||||||
export { filesystemCache, cloudflareCache, type Cache, type CloudflareCacheLike } from "./core/cache.ts";
|
export {
|
||||||
|
filesystemCache,
|
||||||
|
cloudflareCache,
|
||||||
|
type Cache,
|
||||||
|
type CloudflareCacheLike,
|
||||||
|
} from "./core/cache.ts";
|
||||||
|
|
||||||
// Adapters
|
// Adapters
|
||||||
export { serve, type BunServerOptions } from "./adapters/server-bun.ts";
|
export { serve, type BunServerOptions } from "./adapters/server-bun.ts";
|
||||||
|
|
@ -191,13 +242,23 @@ export {
|
||||||
type WorkerEnv,
|
type WorkerEnv,
|
||||||
type WranglerSetupOptions,
|
type WranglerSetupOptions,
|
||||||
} from "./adapters/cloudflare-wrangler.ts";
|
} from "./adapters/cloudflare-wrangler.ts";
|
||||||
export { createHandler, setupVercel, type NodeHandler } from "./adapters/vercel-vercel.ts";
|
export {
|
||||||
|
createHandler,
|
||||||
|
setupVercel,
|
||||||
|
type NodeHandler,
|
||||||
|
} from "./adapters/vercel-vercel.ts";
|
||||||
export {
|
export {
|
||||||
prerender,
|
prerender,
|
||||||
type PrerenderOptions,
|
type PrerenderOptions,
|
||||||
type PrerenderResult,
|
type PrerenderResult,
|
||||||
} from "./adapters/static.ts";
|
} from "./adapters/static.ts";
|
||||||
export { predictLayout, type BreakpointAcc, type LayoutPrediction, type LayoutPredictionOptions, type ReflectiveAnchorGroup } from "./adapters/prediction.ts";
|
export {
|
||||||
|
predictLayout,
|
||||||
|
type BreakpointAcc,
|
||||||
|
type LayoutPrediction,
|
||||||
|
type LayoutPredictionOptions,
|
||||||
|
type ReflectiveAnchorGroup,
|
||||||
|
} from "./adapters/prediction.ts";
|
||||||
|
|
||||||
// Integrations
|
// Integrations
|
||||||
export {
|
export {
|
||||||
|
|
@ -211,10 +272,26 @@ export {
|
||||||
type ReactIntegrationOptions,
|
type ReactIntegrationOptions,
|
||||||
type TrackerOptions,
|
type TrackerOptions,
|
||||||
} from "./integrations/react.ts";
|
} from "./integrations/react.ts";
|
||||||
export { concentric, layoutHooks, type Utility, type UtilityContext } from "./integrations/hooks.ts";
|
export {
|
||||||
|
concentric,
|
||||||
|
layoutHooks,
|
||||||
|
type Utility,
|
||||||
|
type UtilityContext,
|
||||||
|
} from "./integrations/hooks.ts";
|
||||||
export { googleFonts, type GoogleFontsOptions } from "./integrations/fonts.ts";
|
export { googleFonts, type GoogleFontsOptions } from "./integrations/fonts.ts";
|
||||||
export { responsivityIntegration, DEFAULT_BREAKPOINTS, type ResponsivityOptions } from "./integrations/responsivity.ts";
|
export {
|
||||||
export { authIntegration, authFromRequest, authGuard, type AuthIdentity, type OAuthProvider, type AuthIntegrationOptions } from "./integrations/auth.ts";
|
responsivityIntegration,
|
||||||
|
DEFAULT_BREAKPOINTS,
|
||||||
|
type ResponsivityOptions,
|
||||||
|
} from "./integrations/responsivity.ts";
|
||||||
|
export {
|
||||||
|
authIntegration,
|
||||||
|
authFromRequest,
|
||||||
|
authGuard,
|
||||||
|
type AuthIdentity,
|
||||||
|
type OAuthProvider,
|
||||||
|
type AuthIntegrationOptions,
|
||||||
|
} from "./integrations/auth.ts";
|
||||||
|
|
||||||
// Testing
|
// Testing
|
||||||
export {
|
export {
|
||||||
|
|
|
||||||
|
|
@ -3,93 +3,246 @@ import type { RouteGuard } from "../core/pipeline.ts";
|
||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import { MemorySessionStore, type SessionStore } from "../core/sessions.ts";
|
import { MemorySessionStore, type SessionStore } from "../core/sessions.ts";
|
||||||
|
|
||||||
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 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 AuthIdentity {
|
||||||
export interface OAuthProvider { name: string; authorizationUrl: string; clientId: string; redirectUri: string; scope?: string[]; tokenUrl: string; clientSecret?: string; userInfoUrl: string; identity(profile: Record<string, unknown>): 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<string, unknown>): AuthIdentity;
|
||||||
|
}
|
||||||
export interface AuthIntegrationOptions {
|
export interface AuthIntegrationOptions {
|
||||||
login?: (email: string, password: string) => Promise<AuthIdentity | undefined> | AuthIdentity | undefined;
|
login?: (
|
||||||
|
email: string,
|
||||||
|
password: string,
|
||||||
|
) => Promise<AuthIdentity | undefined> | AuthIdentity | undefined;
|
||||||
providers?: OAuthProvider[];
|
providers?: OAuthProvider[];
|
||||||
route?: string;
|
route?: string;
|
||||||
store?: SessionStore | ((context: IntegrationContext) => Promise<SessionStore> | SessionStore);
|
store?:
|
||||||
|
| SessionStore
|
||||||
|
| ((context: IntegrationContext) => Promise<SessionStore> | SessionStore);
|
||||||
sessionTtl?: number;
|
sessionTtl?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultStore = new MemorySessionStore();
|
const defaultStore = new MemorySessionStore();
|
||||||
const sessionKey = (id: string) => `session:${createHash("sha256").update(id).digest("hex")}`;
|
const sessionKey = (id: string) =>
|
||||||
async function cookie(identity: AuthIdentity, store: SessionStore, ttl: number): Promise<string> {
|
`session:${createHash("sha256").update(id).digest("hex")}`;
|
||||||
|
async function cookie(
|
||||||
|
identity: AuthIdentity,
|
||||||
|
store: SessionStore,
|
||||||
|
ttl: number,
|
||||||
|
): Promise<string> {
|
||||||
const id = crypto.randomUUID();
|
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}`;
|
return `muddle_auth=${id}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${ttl}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Form and OAuth entry points. Token exchange remains provider-owned. */
|
/** 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";
|
const route = options.route ?? "/auth";
|
||||||
let store: SessionStore = defaultStore;
|
let store: SessionStore = defaultStore;
|
||||||
const ttl = options.sessionTtl ?? 3600;
|
const ttl = options.sessionTtl ?? 3600;
|
||||||
return {
|
return {
|
||||||
name: "auth",
|
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) {
|
async setup(context: IntegrationContext) {
|
||||||
store = typeof options.store === "function" ? await options.store(context) : options.store ?? defaultStore;
|
store =
|
||||||
context.route("GET", `${route}/login`, () => new Response('<form method="post"><label>Email <input name="email" type="email"></label><label>Password <input name="password" type="password"></label><button>Sign in</button></form>', { headers: { "content-type": "text/html; charset=utf-8" } }));
|
typeof options.store === "function"
|
||||||
|
? await options.store(context)
|
||||||
|
: (options.store ?? defaultStore);
|
||||||
|
context.route(
|
||||||
|
"GET",
|
||||||
|
`${route}/login`,
|
||||||
|
() =>
|
||||||
|
new Response(
|
||||||
|
'<form method="post"><label>Email <input name="email" type="email"></label><label>Password <input name="password" type="password"></label><button>Sign in</button></form>',
|
||||||
|
{ headers: { "content-type": "text/html; charset=utf-8" } },
|
||||||
|
),
|
||||||
|
);
|
||||||
context.route("POST", `${route}/login`, async (request, ctx) => {
|
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 (request.headers.get("origin") !== new URL(request.url).origin)
|
||||||
if (!ctx.body || typeof ctx.body !== "object" || Array.isArray(ctx.body)) return new Response("Invalid credentials", { status: 400 });
|
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<string, string>;
|
const data = ctx.body as Record<string, string>;
|
||||||
const identity = await options.login?.(String(data.email ?? ""), String(data.password ?? ""));
|
const identity = await options.login?.(
|
||||||
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 });
|
String(data.email ?? ""),
|
||||||
return identity ? new Response(null, { status: 302, headers: { location: "/", "set-cookie": await cookie(identity, store, ttl) } }) : new Response("Unauthorized", { status: 401 });
|
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) => {
|
context.route("POST", `${route}/logout`, async (request) => {
|
||||||
if (request.headers.get("origin") !== new URL(request.url).origin) return new Response("Invalid origin", { status: 403 });
|
if (request.headers.get("origin") !== new URL(request.url).origin)
|
||||||
const id = request.headers.get("cookie")?.match(/(?:^|;\s*)muddle_auth=([^;]+)/)?.[1];
|
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));
|
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 ?? []) {
|
for (const provider of options.providers ?? []) {
|
||||||
context.route("GET", `${route}/${provider.name}`, async () => {
|
context.route("GET", `${route}/${provider.name}`, async () => {
|
||||||
const state = crypto.randomUUID();
|
const state = crypto.randomUUID();
|
||||||
const verifier = Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("base64url");
|
const verifier = Buffer.from(
|
||||||
await store.set(`oauth:${state}`, { value: { verifier, provider: provider.name }, expires: Date.now() + 600_000 });
|
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);
|
const url = new URL(provider.authorizationUrl);
|
||||||
url.searchParams.set("client_id", provider.clientId);
|
url.searchParams.set("client_id", provider.clientId);
|
||||||
url.searchParams.set("redirect_uri", provider.redirectUri);
|
url.searchParams.set("redirect_uri", provider.redirectUri);
|
||||||
url.searchParams.set("response_type", "code");
|
url.searchParams.set("response_type", "code");
|
||||||
url.searchParams.set("scope", (provider.scope ?? []).join(" "));
|
url.searchParams.set("scope", (provider.scope ?? []).join(" "));
|
||||||
url.searchParams.set("state", state);
|
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");
|
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` } });
|
return new Response(null, {
|
||||||
});
|
status: 302,
|
||||||
context.route("GET", `${route}/${provider.name}/callback`, async (request) => {
|
headers: {
|
||||||
const url = new URL(request.url);
|
location: url.toString(),
|
||||||
const state = url.searchParams.get("state") ?? "";
|
"set-cookie": `muddle_oauth=${state}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=600`,
|
||||||
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<string, unknown>), store, ttl) } });
|
|
||||||
});
|
});
|
||||||
|
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<string, unknown>,
|
||||||
|
),
|
||||||
|
store,
|
||||||
|
ttl,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function authFromRequest(request: Request, store: SessionStore = defaultStore): Promise<AuthIdentity | undefined> {
|
export async function authFromRequest(
|
||||||
const value = request.headers.get("cookie")?.match(/(?:^|;\s*)muddle_auth=([^;]+)/)?.[1];
|
request: Request,
|
||||||
|
store: SessionStore = defaultStore,
|
||||||
|
): Promise<AuthIdentity | undefined> {
|
||||||
|
const value = request.headers
|
||||||
|
.get("cookie")
|
||||||
|
?.match(/(?:^|;\s*)muddle_auth=([^;]+)/)?.[1];
|
||||||
if (!value) return undefined;
|
if (!value) return undefined;
|
||||||
const session = await store.get(sessionKey(value));
|
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;
|
return session.value.identity as AuthIdentity;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,15 @@ export interface GoogleFontsOptions {
|
||||||
function memoryCache(): Cache {
|
function memoryCache(): Cache {
|
||||||
const entries = new Map<string, Uint8Array>();
|
const entries = new Map<string, Uint8Array>();
|
||||||
return {
|
return {
|
||||||
async get(key) { return entries.get(key); },
|
async get(key) {
|
||||||
async put(key, value) { entries.set(key, value); },
|
return entries.get(key);
|
||||||
async delete(key) { entries.delete(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 {
|
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"}`;
|
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());
|
css = new Uint8Array(await response.arrayBuffer());
|
||||||
await cache.put(key, css, { ttl: 86400 });
|
await cache.put(key, css, { ttl: 86400 });
|
||||||
}
|
}
|
||||||
const proxied = text(css).replace(/url\((https:\/\/[^)]+)\)/g, (_match, url: string) => `url(${proxyPrefix}${encodeURIComponent(url)})`);
|
const proxied = text(css).replace(
|
||||||
return new Response(proxied, { headers: { "content-type": "text/css; charset=utf-8", "cache-control": "public, max-age=86400" } });
|
/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) => {
|
context.route("GET", `${proxyPrefix}*`, async (request) => {
|
||||||
const encoded = new URL(request.url).pathname.slice(proxyPrefix.length);
|
const encoded = new URL(request.url).pathname.slice(proxyPrefix.length);
|
||||||
const url = decodeURIComponent(encoded);
|
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}`;
|
const key = `font:${url}`;
|
||||||
let font = await cache.get(key);
|
let font = await cache.get(key);
|
||||||
if (!font) {
|
if (!font) {
|
||||||
|
|
@ -58,7 +76,12 @@ export function googleFonts(options: GoogleFontsOptions): Integration {
|
||||||
font = new Uint8Array(await response.arrayBuffer());
|
font = new Uint8Array(await response.arrayBuffer());
|
||||||
await cache.put(key, font, { ttl: 31536000 });
|
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",
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,8 @@ export function layoutHooks(
|
||||||
const utilities = new Map<string, Utility>();
|
const utilities = new Map<string, Utility>();
|
||||||
hook({
|
hook({
|
||||||
utilities: (name, utility) => utilities.set(name, utility),
|
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", {
|
context.loader("utilities", {
|
||||||
name: "utilities",
|
name: "utilities",
|
||||||
|
|
@ -38,7 +39,10 @@ export function layoutHooks(
|
||||||
const css = file.source.split(/\s+/).flatMap((token) => {
|
const css = file.source.split(/\s+/).flatMap((token) => {
|
||||||
const [name, value] = token.split(":", 2);
|
const [name, value] = token.split(":", 2);
|
||||||
const utility = utilities.get(name ?? "");
|
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") };
|
return { data: {}, content: css.join("\n") };
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ export function mouseTrackerCss(options: TrackerOptions = {}): string {
|
||||||
const rules = Array.from({ length: columns * rows }, (_, index) => {
|
const rules = Array.from({ length: columns * rows }, (_, index) => {
|
||||||
const x = Math.floor(index / rows) + 1;
|
const x = Math.floor(index / rows) + 1;
|
||||||
const y = (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")}`;
|
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, () =>
|
context.route(
|
||||||
new Response(reactCss(options), {
|
"GET",
|
||||||
headers: {
|
route,
|
||||||
"content-type": "text/css; charset=utf-8",
|
() =>
|
||||||
"cache-control": "public, max-age=3600",
|
new Response(reactCss(options), {
|
||||||
},
|
headers: {
|
||||||
}),
|
"content-type": "text/css; charset=utf-8",
|
||||||
|
"cache-control": "public, max-age=3600",
|
||||||
|
},
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,17 @@ export interface ResponsivityOptions {
|
||||||
route?: string;
|
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. */
|
/** 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 };
|
const breakpoints = { ...DEFAULT_BREAKPOINTS, ...options.breakpoints };
|
||||||
return {
|
return {
|
||||||
name: "responsivity",
|
name: "responsivity",
|
||||||
|
|
@ -17,7 +24,12 @@ export function responsivityIntegration(options: ResponsivityOptions = {}): Inte
|
||||||
context.loader("responsive-stcss", {
|
context.loader("responsive-stcss", {
|
||||||
name: "responsive-stcss",
|
name: "responsive-stcss",
|
||||||
extensions: [".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 }),
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,5 @@
|
||||||
/** Compatibility export. The implementation lives in the isolated package. */
|
/** 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";
|
||||||
|
|
|
||||||
3
justfile
3
justfile
|
|
@ -7,6 +7,9 @@ default:
|
||||||
test:
|
test:
|
||||||
bun test ./tests
|
bun test ./tests
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
bunx @biomejs/biome format --fix
|
||||||
|
|
||||||
loc:
|
loc:
|
||||||
@echo "Loc: counting source lines in the codebase..."
|
@echo "Loc: counting source lines in the codebase..."
|
||||||
@find . \
|
@find . \
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
# @muddle/amezing
|
# @muddle/amezing
|
||||||
|
|
||||||
Port of the Ametrine theme (by Daudix) as a muddle module.
|
Port of the Ametrine theme (by Daudix) as a muddle module.
|
||||||
|
|
|
||||||
|
|
@ -6,38 +6,109 @@
|
||||||
*/
|
*/
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { readFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
import type {
|
import type { Integration, Mounts } from "muddle";
|
||||||
Integration,
|
|
||||||
Mounts,
|
|
||||||
} from "muddle";
|
|
||||||
import type { Views } from "muddle/views";
|
import type { Views } from "muddle/views";
|
||||||
|
|
||||||
|
export interface AmezingAccentPreset {
|
||||||
export interface AmezingAccentPreset { name: string; light: string; dark: string }
|
name: string;
|
||||||
|
light: string;
|
||||||
|
dark: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** Accent presets from ametrine/data/accent_presets.toml. */
|
/** Accent presets from ametrine/data/accent_presets.toml. */
|
||||||
export const ACCENT_PRESETS: Record<string, AmezingAccentPreset> = {
|
export const ACCENT_PRESETS: Record<string, AmezingAccentPreset> = {
|
||||||
red: { name: "red", light: "hsl(342.0779, 72%, 58%)", dark: "hsl(345.18988, 100%, 69%)" },
|
red: {
|
||||||
orange: { name: "orange", light: "hsl(15.771428, 74%, 54%)", dark: "hsl(19.731544, 96%, 70%)" },
|
name: "red",
|
||||||
yellow: { name: "yellow", light: "hsl(34.639175, 91%, 42%)", dark: "hsl(44.70588, 100%, 70%)" },
|
light: "hsl(342.0779, 72%, 58%)",
|
||||||
green: { name: "green", light: "hsl(153.7815, 61%, 38%)", dark: "hsl(90, 59%, 66%)" },
|
dark: "hsl(345.18988, 100%, 69%)",
|
||||||
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%)" },
|
orange: {
|
||||||
teal: { name: "teal", light: "hsl(180, 65%, 35%)", dark: "hsl(183, 85%, 65%)" },
|
name: "orange",
|
||||||
pink: { name: "pink", light: "hsl(333.88236, 69%, 58%)", dark: "hsl(336.78634, 100%, 70%)" },
|
light: "hsl(15.771428, 74%, 54%)",
|
||||||
cyan: { name: "cyan", light: "hsl(195, 71%, 35%)", dark: "hsl(193.41046, 72%, 70%)" },
|
dark: "hsl(19.731544, 96%, 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%)" },
|
yellow: {
|
||||||
ochrea: { name: "ochrea", light: "hsl(45.75298, 95%, 45%)", dark: "hsl(46.09091, 100%, 78%)" },
|
name: "yellow",
|
||||||
lime: { name: "lime", light: "hsl(86.25406, 60%, 42%)", dark: "hsl(86.84211, 93%, 71%)" },
|
light: "hsl(34.639175, 91%, 42%)",
|
||||||
maroon: { name: "maroon", light: "hsl(321.81819, 26%, 41%)", dark: "hsl(322.10526, 66%, 66%)" },
|
dark: "hsl(44.70588, 100%, 70%)",
|
||||||
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%)" },
|
green: {
|
||||||
slate: { name: "slate", light: "hsl(212.53197, 15%, 44%)", dark: "hsl(212.90323, 61%, 72%)" },
|
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 {
|
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;
|
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 {
|
export function accentStyle(accent: string | [string, string?]): string {
|
||||||
const inputs = Array.isArray(accent) ? accent : [accent];
|
const inputs = Array.isArray(accent) ? accent : [accent];
|
||||||
const light = ACCENT_PRESETS[inputs[0] ?? ""]?.light ?? inputs[0];
|
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 lightHsl = parseHsl(light ?? "");
|
||||||
const darkHsl = parseHsl(dark ?? "");
|
const darkHsl = parseHsl(dark ?? "");
|
||||||
if (!lightHsl || !darkHsl) throw new Error(`Unknown accent color: ${accent}`);
|
if (!lightHsl || !darkHsl) throw new Error(`Unknown accent color: ${accent}`);
|
||||||
|
|
@ -86,27 +161,55 @@ const COMPONENTS = [
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
function componentTemplates(): [string, string][] {
|
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 {
|
interface TemplateViews {
|
||||||
component(name: string, component: unknown): unknown;
|
component(name: string, component: unknown): unknown;
|
||||||
engine?: { renderComponent(path: string, props: Record<string, unknown>, context: { locals: Record<string, unknown>; params: Record<string, string> }): Promise<string> };
|
engine?: {
|
||||||
|
renderComponent(
|
||||||
|
path: string,
|
||||||
|
props: Record<string, unknown>,
|
||||||
|
context: {
|
||||||
|
locals: Record<string, unknown>;
|
||||||
|
params: Record<string, string>;
|
||||||
|
},
|
||||||
|
): Promise<string>;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerAll(views: TemplateViews, accent: string | undefined): void {
|
function registerAll(views: TemplateViews, accent: string | undefined): void {
|
||||||
for (const [name, template] of componentTemplates()) {
|
for (const [name, template] of componentTemplates()) {
|
||||||
views.component(name, {
|
views.component(name, {
|
||||||
async render(props: Record<string, unknown>, context: { locals: Record<string, unknown>; params: Record<string, string> }) {
|
async render(
|
||||||
return views.engine!.renderComponent(template, { accentStyle: accent, ...props }, context);
|
props: Record<string, unknown>,
|
||||||
|
context: {
|
||||||
|
locals: Record<string, unknown>;
|
||||||
|
params: Record<string, string>;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
return views.engine!.renderComponent(
|
||||||
|
template,
|
||||||
|
{ accentStyle: accent, ...props },
|
||||||
|
context,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Registers the Ametrine components on the views registry. */
|
/** Registers the Ametrine components on the views registry. */
|
||||||
export function registerAmezing(views: Views, options: { accentColor?: string | [string, string?] } = {}): void {
|
export function registerAmezing(
|
||||||
registerAll(views as unknown as Parameters<typeof registerAll>[0], options.accentColor ? accentStyle(options.accentColor) : undefined);
|
views: Views,
|
||||||
|
options: { accentColor?: string | [string, string?] } = {},
|
||||||
|
): void {
|
||||||
|
registerAll(
|
||||||
|
views as unknown as Parameters<typeof registerAll>[0],
|
||||||
|
options.accentColor ? accentStyle(options.accentColor) : undefined,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Compiled theme stylesheet (Citrine base + Ametrine layout). */
|
/** Compiled theme stylesheet (Citrine base + Ametrine layout). */
|
||||||
|
|
@ -133,22 +236,44 @@ export const amezingFonts: Record<string, string> = {
|
||||||
"/fonts/commit-mono.woff2": "fonts/commit-mono.woff2",
|
"/fonts/commit-mono.woff2": "fonts/commit-mono.woff2",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function amezingMount(options: { accentColor?: string | [string, string?] } = {}): Mounts {
|
export function amezingMount(
|
||||||
const accent = options.accentColor ? accentStyle(options.accentColor) : undefined;
|
options: { accentColor?: string | [string, string?] } = {},
|
||||||
|
): Mounts {
|
||||||
|
const accent = options.accentColor
|
||||||
|
? accentStyle(options.accentColor)
|
||||||
|
: undefined;
|
||||||
return {
|
return {
|
||||||
views: (views) => registerAll(views as unknown as Parameters<typeof registerAll>[0], accent),
|
views: (views) =>
|
||||||
|
registerAll(
|
||||||
|
views as unknown as Parameters<typeof registerAll>[0],
|
||||||
|
accent,
|
||||||
|
),
|
||||||
routes: (route) => {
|
routes: (route) => {
|
||||||
for (const [routePath, file] of Object.entries(amezingScripts)) {
|
for (const [routePath, file] of Object.entries(amezingScripts)) {
|
||||||
route("GET", `/_muddle/amezing/${routePath}`, async () =>
|
route(
|
||||||
new Response(await readFile(new URL(`./${file}`, import.meta.url), "utf8"), {
|
"GET",
|
||||||
headers: { "content-type": "text/javascript; charset=utf-8" },
|
`/_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)) {
|
for (const [routePath, file] of Object.entries(amezingFonts)) {
|
||||||
route("GET", routePath, async () =>
|
route(
|
||||||
new Response(await readFile(new URL(`./${file}`, import.meta.url)), {
|
"GET",
|
||||||
headers: { "content-type": "font/woff2" },
|
routePath,
|
||||||
}));
|
async () =>
|
||||||
|
new Response(
|
||||||
|
await readFile(new URL(`./${file}`, import.meta.url)),
|
||||||
|
{
|
||||||
|
headers: { "content-type": "font/woff2" },
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
styles: { "/_muddle/amezing.css": () => amezingStyles() },
|
styles: { "/_muddle/amezing.css": () => amezingStyles() },
|
||||||
|
|
@ -156,6 +281,8 @@ export function amezingMount(options: { accentColor?: string | [string, string?]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Convenience integration. */
|
/** 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)] };
|
return { name: "amezing", mounts: [amezingMount(options)] };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@muddle/amezing",
|
"name": "@muddle/amezing",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": { ".": "./index.ts", "./styles": "./styles/ametrine.css" },
|
"exports": {
|
||||||
"peerDependencies": { "muddle": "*" }
|
".": "./index.ts",
|
||||||
|
"./styles": "./styles/ametrine.css"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"muddle": "*"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Converts px values to rem
|
// Converts px values to rem
|
||||||
// rpx(1) --> 0.0625rem
|
// rpx(1) --> 0.0625rem
|
||||||
@function rpx($px-value) {
|
@function rpx($px-value) {
|
||||||
@return ($px-value / 16) + rem;
|
@return ($px-value / 16) + rem;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,82 +1,80 @@
|
||||||
@mixin icon($mode: normal) {
|
@mixin icon($mode: normal) {
|
||||||
@if $mode == normal {
|
@if $mode == normal {
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-variant: normal;
|
font-variant: normal;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
} @else if $mode == pseudo {
|
} @else if $mode == pseudo {
|
||||||
-webkit-mask-image: var(--icon);
|
-webkit-mask-image: var(--icon);
|
||||||
-webkit-mask-size: cover;
|
-webkit-mask-size: cover;
|
||||||
display: block;
|
display: block;
|
||||||
mask-image: var(--icon);
|
mask-image: var(--icon);
|
||||||
mask-size: cover;
|
mask-size: cover;
|
||||||
background-color: currentColor;
|
background-color: currentColor;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
content: "";
|
content: "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin gradient-fill($mode: normal) {
|
@mixin gradient-fill($mode: normal) {
|
||||||
@if $mode == normal {
|
@if $mode == normal {
|
||||||
box-shadow: var(--edge-highlight), var(--shadow-glow);
|
box-shadow: var(--edge-highlight), var(--shadow-glow);
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
to bottom right,
|
to bottom right,
|
||||||
var(--accent-color),
|
var(--accent-color),
|
||||||
var(--accent-highlight)
|
var(--accent-highlight)
|
||||||
);
|
);
|
||||||
color: var(--contrast-color);
|
color: var(--contrast-color);
|
||||||
text-shadow: var(--text-shadow-glow);
|
text-shadow: var(--text-shadow-glow);
|
||||||
} @else if $mode == hover {
|
} @else if $mode == hover {
|
||||||
box-shadow: var(--edge-highlight);
|
box-shadow: var(--edge-highlight);
|
||||||
background-color: var(--accent-color);
|
background-color: var(--accent-color);
|
||||||
color: var(--contrast-color);
|
color: var(--contrast-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin card {
|
@mixin card {
|
||||||
box-shadow: var(--edge-highlight), var(--shadow);
|
box-shadow: var(--edge-highlight), var(--shadow);
|
||||||
border-radius: var(--rounded-corner);
|
border-radius: var(--rounded-corner);
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin chip {
|
@mixin chip {
|
||||||
transition:
|
transition: all var(--transition), transform var(--transition-bezier);
|
||||||
all var(--transition),
|
box-shadow: var(--edge-highlight);
|
||||||
transform var(--transition-bezier);
|
border-radius: var(--rounded-corner-small);
|
||||||
box-shadow: var(--edge-highlight);
|
background-color: var(--bg-muted-1);
|
||||||
border-radius: var(--rounded-corner-small);
|
padding-inline: 0.375rem;
|
||||||
background-color: var(--bg-muted-1);
|
text-decoration: none;
|
||||||
padding-inline: 0.375rem;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin easing-mask($direction: to bottom) {
|
@mixin easing-mask($direction: to bottom) {
|
||||||
--mask-image: linear-gradient(
|
--mask-image: linear-gradient(
|
||||||
to #{$direction},
|
to #{$direction},
|
||||||
hsl(0, 0%, 0%) 0%,
|
hsl(0, 0%, 0%) 0%,
|
||||||
hsla(0, 0%, 0%, 0.987) 8.1%,
|
hsla(0, 0%, 0%, 0.987) 8.1%,
|
||||||
hsla(0, 0%, 0%, 0.951) 15.5%,
|
hsla(0, 0%, 0%, 0.951) 15.5%,
|
||||||
hsla(0, 0%, 0%, 0.896) 22.5%,
|
hsla(0, 0%, 0%, 0.896) 22.5%,
|
||||||
hsla(0, 0%, 0%, 0.825) 29%,
|
hsla(0, 0%, 0%, 0.825) 29%,
|
||||||
hsla(0, 0%, 0%, 0.741) 35.3%,
|
hsla(0, 0%, 0%, 0.741) 35.3%,
|
||||||
hsla(0, 0%, 0%, 0.648) 41.2%,
|
hsla(0, 0%, 0%, 0.648) 41.2%,
|
||||||
hsla(0, 0%, 0%, 0.55) 47.1%,
|
hsla(0, 0%, 0%, 0.55) 47.1%,
|
||||||
hsla(0, 0%, 0%, 0.45) 52.9%,
|
hsla(0, 0%, 0%, 0.45) 52.9%,
|
||||||
hsla(0, 0%, 0%, 0.352) 58.8%,
|
hsla(0, 0%, 0%, 0.352) 58.8%,
|
||||||
hsla(0, 0%, 0%, 0.259) 64.7%,
|
hsla(0, 0%, 0%, 0.259) 64.7%,
|
||||||
hsla(0, 0%, 0%, 0.175) 71%,
|
hsla(0, 0%, 0%, 0.175) 71%,
|
||||||
hsla(0, 0%, 0%, 0.104) 77.5%,
|
hsla(0, 0%, 0%, 0.104) 77.5%,
|
||||||
hsla(0, 0%, 0%, 0.049) 84.5%,
|
hsla(0, 0%, 0%, 0.049) 84.5%,
|
||||||
hsla(0, 0%, 0%, 0.013) 91.9%,
|
hsla(0, 0%, 0%, 0.013) 91.9%,
|
||||||
hsla(0, 0%, 0%, 0) 100%
|
hsla(0, 0%, 0%, 0) 100%
|
||||||
);
|
);
|
||||||
mask-image: var(--mask-image);
|
mask-image: var(--mask-image);
|
||||||
-webkit-mask-image: var(--mask-image);
|
-webkit-mask-image: var(--mask-image);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,83 +1,83 @@
|
||||||
@keyframes slide-in-up {
|
@keyframes slide-in-up {
|
||||||
from {
|
from {
|
||||||
transform: translateY(-1rem);
|
transform: translateY(-1rem);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slide-in-left {
|
@keyframes slide-in-left {
|
||||||
from {
|
from {
|
||||||
transform: translateX(1rem);
|
transform: translateX(1rem);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slide-in-right {
|
@keyframes slide-in-right {
|
||||||
from {
|
from {
|
||||||
transform: translateX(-1rem);
|
transform: translateX(-1rem);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slide-in-up-longer {
|
@keyframes slide-in-up-longer {
|
||||||
from {
|
from {
|
||||||
transform: translateY(-2rem);
|
transform: translateY(-2rem);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slide-in-down-longer {
|
@keyframes slide-in-down-longer {
|
||||||
from {
|
from {
|
||||||
transform: translateY(2rem);
|
transform: translateY(2rem);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slide-in-left-longer {
|
@keyframes slide-in-left-longer {
|
||||||
from {
|
from {
|
||||||
transform: translateX(-2rem);
|
transform: translateX(-2rem);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slide-in-right-longer {
|
@keyframes slide-in-right-longer {
|
||||||
from {
|
from {
|
||||||
transform: translateX(2rem);
|
transform: translateX(2rem);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slide-out-down-longer {
|
@keyframes slide-out-down-longer {
|
||||||
to {
|
to {
|
||||||
transform: translateY(-2rem);
|
transform: translateY(-2rem);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes expand-up-left {
|
@keyframes expand-up-left {
|
||||||
from {
|
from {
|
||||||
transform: scale(0.5) translate(-1rem, -1rem);
|
transform: scale(0.5) translate(-1rem, -1rem);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes expand-up-right {
|
@keyframes expand-up-right {
|
||||||
from {
|
from {
|
||||||
transform: scale(0.5) translate(1rem, -1rem);
|
transform: scale(0.5) translate(1rem, -1rem);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes expand-down-left {
|
@keyframes expand-down-left {
|
||||||
from {
|
from {
|
||||||
transform: scale(0.5) translate(-1rem, 1rem);
|
transform: scale(0.5) translate(-1rem, 1rem);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes expand-down-right {
|
@keyframes expand-down-right {
|
||||||
from {
|
from {
|
||||||
transform: scale(0.5) translate(1rem, 1rem);
|
transform: scale(0.5) translate(1rem, 1rem);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,58 +2,58 @@
|
||||||
|
|
||||||
// Make focused anchor not get covered by header
|
// Make focused anchor not get covered by header
|
||||||
:target:not(#main-content) {
|
:target:not(#main-content) {
|
||||||
scroll-margin-block-start: 6.5rem;
|
scroll-margin-block-start: 6.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
:focus-visible,
|
:focus-visible,
|
||||||
:target:not(#main-content) {
|
:target:not(#main-content) {
|
||||||
animation: focus-in var(--transition);
|
animation: focus-in var(--transition);
|
||||||
outline: 0.125rem solid var(--accent-color);
|
outline: 0.125rem solid var(--accent-color);
|
||||||
outline-offset: 0.125rem;
|
outline-offset: 0.125rem;
|
||||||
|
|
||||||
@keyframes focus-in {
|
@keyframes focus-in {
|
||||||
from {
|
from {
|
||||||
outline: 0.5rem solid transparent;
|
outline: 0.5rem solid transparent;
|
||||||
outline-offset: 0.25rem;
|
outline-offset: 0.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Style text selection to use accent color
|
// Style text selection to use accent color
|
||||||
::selection {
|
::selection {
|
||||||
background-color: var(--accent-color);
|
background-color: var(--accent-color);
|
||||||
color: var(--contrast-color);
|
color: var(--contrast-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
color: var(--fg-color);
|
color: var(--fg-color);
|
||||||
font-family: var(--font-sans-serif), var(--font-system-ui), var(--font-emoji);
|
font-family: var(--font-sans-serif), var(--font-system-ui), var(--font-emoji);
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
|
||||||
@supports (min-height: 100dvh) {
|
@supports (min-height: 100dvh) {
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion) {
|
@media (prefers-reduced-motion) {
|
||||||
*,
|
*,
|
||||||
*::before,
|
*::before,
|
||||||
*::after {
|
*::after {
|
||||||
animation-duration: 0s !important;
|
animation-duration: 0s !important;
|
||||||
transition-duration: 0s !important;
|
transition-duration: 0s !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable blur on Firefox Mobile
|
// Disable blur on Firefox Mobile
|
||||||
@-moz-document url-prefix() {
|
@-moz-document url-prefix() {
|
||||||
@media only screen and (max-width: 960px) {
|
@media only screen and (max-width: 960px) {
|
||||||
.firefox-unblur,
|
.firefox-unblur,
|
||||||
.firefox-unblur::before,
|
.firefox-unblur::before,
|
||||||
.firefox-unblur::after {
|
.firefox-unblur::after {
|
||||||
backdrop-filter: none !important;
|
backdrop-filter: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,34 +3,34 @@
|
||||||
body > main,
|
body > main,
|
||||||
body > nav,
|
body > nav,
|
||||||
body > footer {
|
body > footer {
|
||||||
--padding: 1rem;
|
--padding: 1rem;
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
padding-inline: 1rem;
|
padding-inline: 1rem;
|
||||||
max-width: calc(var(--content-width) + var(--padding) * 2);
|
max-width: calc(var(--content-width) + var(--padding) * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
body > footer {
|
body > footer {
|
||||||
margin-block-start: 4rem;
|
margin-block-start: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
body > nav {
|
body > nav {
|
||||||
margin-block-end: 4rem;
|
margin-block-end: 4rem;
|
||||||
border-block-end: rpx(1) solid var(--bg-muted-2);
|
border-block-end: rpx(1) solid var(--bg-muted-2);
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
margin-block: 0.625rem;
|
margin-block: 0.625rem;
|
||||||
padding-inline-start: 0;
|
padding-inline-start: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
|
||||||
@media only screen and (max-width: 480px) {
|
@media only screen and (max-width: 480px) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
margin-block: 0;
|
margin-block: 0;
|
||||||
padding-inline-start: 0;
|
padding-inline-start: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,28 +4,28 @@
|
||||||
*,
|
*,
|
||||||
:after,
|
:after,
|
||||||
:before {
|
:before {
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
select {
|
select {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
-webkit-text-size-adjust: none;
|
-webkit-text-size-adjust: none;
|
||||||
text-size-adjust: none;
|
text-size-adjust: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
a,
|
a,
|
||||||
|
|
@ -42,7 +42,7 @@ input[type="reset"],
|
||||||
input[type="submit"],
|
input[type="submit"],
|
||||||
label,
|
label,
|
||||||
select {
|
select {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
article,
|
article,
|
||||||
|
|
@ -60,7 +60,7 @@ rt,
|
||||||
ruby,
|
ruby,
|
||||||
section,
|
section,
|
||||||
summary {
|
summary {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
bdi,
|
bdi,
|
||||||
|
|
@ -69,14 +69,14 @@ meter,
|
||||||
output,
|
output,
|
||||||
progress,
|
progress,
|
||||||
time {
|
time {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
[hidden],
|
[hidden],
|
||||||
datalist,
|
datalist,
|
||||||
dialog:not([open]),
|
dialog:not([open]),
|
||||||
rp {
|
rp {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
embed,
|
embed,
|
||||||
|
|
@ -86,50 +86,50 @@ object,
|
||||||
picture,
|
picture,
|
||||||
svg,
|
svg,
|
||||||
video {
|
video {
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote,
|
blockquote,
|
||||||
q {
|
q {
|
||||||
quotes: none;
|
quotes: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote:after,
|
blockquote:after,
|
||||||
blockquote:before,
|
blockquote:before,
|
||||||
q:after,
|
q:after,
|
||||||
q:before {
|
q:before {
|
||||||
content: none;
|
content: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
strong {
|
strong {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
em {
|
em {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
code,
|
code,
|
||||||
kbd,
|
kbd,
|
||||||
pre,
|
pre,
|
||||||
samp {
|
samp {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub {
|
sub {
|
||||||
vertical-align: sub;
|
vertical-align: sub;
|
||||||
}
|
}
|
||||||
|
|
||||||
sup {
|
sup {
|
||||||
vertical-align: super;
|
vertical-align: super;
|
||||||
}
|
}
|
||||||
|
|
||||||
small {
|
small {
|
||||||
font-size: small;
|
font-size: small;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
@use "../abstracts/variables" as *;
|
@use "../abstracts/variables" as *;
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@include variables(("light", "common"));
|
@include variables(("light", "common"));
|
||||||
@include variables-accent(("light", "common"));
|
@include variables-accent(("light", "common"));
|
||||||
scrollbar-color: var(--accent-color) transparent;
|
scrollbar-color: var(--accent-color) transparent;
|
||||||
accent-color: var(--accent-color);
|
accent-color: var(--accent-color);
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
& {
|
& {
|
||||||
@include variables("dark");
|
@include variables("dark");
|
||||||
@include variables-accent(("dark"));
|
@include variables-accent(("dark"));
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,25 +7,25 @@ ol,
|
||||||
dl,
|
dl,
|
||||||
blockquote,
|
blockquote,
|
||||||
pre {
|
pre {
|
||||||
margin-block: 1rem;
|
margin-block: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul ul,
|
ul ul,
|
||||||
ul ol,
|
ul ol,
|
||||||
ol ul,
|
ol ul,
|
||||||
ol ol {
|
ol ol {
|
||||||
margin-block: 0;
|
margin-block: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
text-wrap: pretty;
|
text-wrap: pretty;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
:disabled,
|
:disabled,
|
||||||
[aria-disabled="true"] {
|
[aria-disabled="true"] {
|
||||||
opacity: var(--disabled-opacity);
|
opacity: var(--disabled-opacity);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
|
|
@ -34,367 +34,363 @@ h3,
|
||||||
h4,
|
h4,
|
||||||
h5,
|
h5,
|
||||||
h6 {
|
h6 {
|
||||||
transition-duration: var(--transition);
|
transition-duration: var(--transition);
|
||||||
transition-property: text-shadow, font-weight;
|
transition-property: text-shadow, font-weight;
|
||||||
margin-block: 2rem 1rem;
|
margin-block: 2rem 1rem;
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
font-variation-settings: "FLAR" 100;
|
font-variation-settings: "FLAR" 100;
|
||||||
letter-spacing: -0.05em;
|
letter-spacing: -0.05em;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-decoration-color: var(--bg-muted-1);
|
text-decoration-color: var(--bg-muted-1);
|
||||||
text-decoration-thickness: 0.125em;
|
text-decoration-thickness: 0.125em;
|
||||||
text-wrap: balance;
|
text-wrap: balance;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
|
||||||
&:target {
|
&:target {
|
||||||
animation: none !important;
|
animation: none !important;
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
font-weight: var(--font-weight-light);
|
font-weight: var(--font-weight-light);
|
||||||
text-shadow: var(--text-shadow-glow);
|
text-shadow: var(--text-shadow-glow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: var(--font-size-xxx-large);
|
font-size: var(--font-size-xxx-large);
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: var(--font-size-xx-large);
|
font-size: var(--font-size-xx-large);
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
font-size: var(--font-size-x-large);
|
font-size: var(--font-size-x-large);
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
font-size: var(--font-size-large);
|
font-size: var(--font-size-large);
|
||||||
}
|
}
|
||||||
|
|
||||||
h5 {
|
h5 {
|
||||||
font-size: var(--font-size-medium);
|
font-size: var(--font-size-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
h6 {
|
h6 {
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 480px) {
|
@media only screen and (max-width: 480px) {
|
||||||
h1 {
|
h1 {
|
||||||
font-size: var(--font-size-xx-large);
|
font-size: var(--font-size-xx-large);
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: var(--font-size-x-large);
|
font-size: var(--font-size-x-large);
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
font-size: var(--font-size-large);
|
font-size: var(--font-size-large);
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
font-size: var(--font-size-medium);
|
font-size: var(--font-size-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
h5 {
|
h5 {
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
h6 {
|
h6 {
|
||||||
font-size: var(--font-size-x-small);
|
font-size: var(--font-size-x-small);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
small {
|
small {
|
||||||
color: var(--fg-muted-2);
|
color: var(--fg-muted-2);
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
abbr[title] {
|
abbr[title] {
|
||||||
cursor: help;
|
cursor: help;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-decoration-style: dotted;
|
text-decoration-style: dotted;
|
||||||
text-decoration-thickness: rpx(1);
|
text-decoration-thickness: rpx(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
figcaption {
|
figcaption {
|
||||||
color: var(--fg-muted-2);
|
color: var(--fg-muted-2);
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
border-inline-start: 0.25rem solid var(--accent-color);
|
border-inline-start: 0.25rem solid var(--accent-color);
|
||||||
border-start-start-radius: 0.25rem;
|
border-start-start-radius: 0.25rem;
|
||||||
border-end-start-radius: 0.25rem;
|
border-end-start-radius: 0.25rem;
|
||||||
padding-inline-start: 0.75rem;
|
padding-inline-start: 0.75rem;
|
||||||
color: var(--fg-muted-2);
|
color: var(--fg-muted-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
pre,
|
pre,
|
||||||
code,
|
code,
|
||||||
kbd,
|
kbd,
|
||||||
samp {
|
samp {
|
||||||
font-family: var(--font-monospace), var(--font-ui-monospace);
|
font-family: var(--font-monospace), var(--font-ui-monospace);
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
box-shadow: var(--edge-highlight), var(--shadow);
|
box-shadow: var(--edge-highlight), var(--shadow);
|
||||||
border-radius: var(--rounded-corner);
|
border-radius: var(--rounded-corner);
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
code:not(pre code),
|
code:not(pre code),
|
||||||
mark,
|
mark,
|
||||||
samp,
|
samp,
|
||||||
q {
|
q {
|
||||||
@include chip;
|
@include chip;
|
||||||
}
|
}
|
||||||
|
|
||||||
code:not(pre code) {
|
code:not(pre code) {
|
||||||
background-color: var(--red-bg-1);
|
background-color: var(--red-bg-1);
|
||||||
padding-block: rpx(3);
|
padding-block: rpx(3);
|
||||||
color: var(--red-fg);
|
color: var(--red-fg);
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
mark {
|
mark {
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
del {
|
del {
|
||||||
color: var(--red-fg);
|
color: var(--red-fg);
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
text-decoration-thickness: rpx(1);
|
text-decoration-thickness: rpx(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
ins {
|
ins {
|
||||||
color: var(--green-fg);
|
color: var(--green-fg);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-decoration-thickness: rpx(1);
|
text-decoration-thickness: rpx(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
samp {
|
samp {
|
||||||
color: var(--fg-muted-2);
|
color: var(--fg-muted-2);
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
q {
|
q {
|
||||||
color: var(--fg-muted-2);
|
color: var(--fg-muted-2);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
u {
|
u {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-decoration-style: wavy;
|
text-decoration-style: wavy;
|
||||||
text-decoration-color: var(--red-fg);
|
text-decoration-color: var(--red-fg);
|
||||||
text-decoration-thickness: rpx(1);
|
text-decoration-thickness: rpx(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
progress {
|
progress {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
box-shadow: var(--edge-highlight);
|
box-shadow: var(--edge-highlight);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
|
|
||||||
&:indeterminate {
|
&:indeterminate {
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:indeterminate::-moz-progress-bar {
|
&:indeterminate::-moz-progress-bar {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-progress-bar {
|
&::-webkit-progress-bar {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-moz-progress-bar {
|
&::-moz-progress-bar {
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
background-color: var(--accent-color);
|
background-color: var(--accent-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-progress-value {
|
&::-webkit-progress-value {
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
background-color: var(--accent-color);
|
background-color: var(--accent-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kbd {
|
kbd {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
transition-duration: var(--transition);
|
transition-duration: var(--transition);
|
||||||
transition-property: transform, box-shadow, background-color;
|
transition-property: transform, box-shadow, background-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow:
|
box-shadow: var(--edge-highlight), inset 0 -0.125rem 0 var(--bg-muted-2);
|
||||||
var(--edge-highlight),
|
border-radius: var(--rounded-corner-small);
|
||||||
inset 0 -0.125rem 0 var(--bg-muted-2);
|
background-color: var(--bg-muted-1);
|
||||||
border-radius: var(--rounded-corner-small);
|
padding: 0 0.375rem;
|
||||||
background-color: var(--bg-muted-1);
|
font-size: var(--font-size-small);
|
||||||
padding: 0 0.375rem;
|
|
||||||
font-size: var(--font-size-small);
|
|
||||||
|
|
||||||
// Small nice thingy, keys can be pressed!
|
// Small nice thingy, keys can be pressed!
|
||||||
&:active {
|
&:active {
|
||||||
transform: translateY(0.125rem);
|
transform: translateY(0.125rem);
|
||||||
box-shadow: inset 0 rpx(1) 0 var(--bg-muted-2);
|
box-shadow: inset 0 rpx(1) 0 var(--bg-muted-2);
|
||||||
background-color: var(--bg-muted-2);
|
background-color: var(--bg-muted-2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
font-weight: var(--font-weight-semi-bold);
|
font-weight: var(--font-weight-semi-bold);
|
||||||
text-decoration-color: var(--accent-highlight);
|
text-decoration-color: var(--accent-highlight);
|
||||||
text-decoration-thickness: rpx(1);
|
text-decoration-thickness: rpx(1);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration-style: wavy;
|
text-decoration-style: wavy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
margin: 2rem auto;
|
margin: 2rem auto;
|
||||||
border: 0.125rem solid var(--bg-muted-2);
|
border: 0.125rem solid var(--bg-muted-2);
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
dt {
|
dt {
|
||||||
font-weight: var(--font-weight-semi-bold);
|
font-weight: var(--font-weight-semi-bold);
|
||||||
}
|
}
|
||||||
|
|
||||||
dd {
|
dd {
|
||||||
margin-inline-start: 1.5rem;
|
margin-inline-start: 1.5rem;
|
||||||
margin-block-end: 1rem;
|
margin-block-end: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
aside {
|
aside {
|
||||||
float: inline-end;
|
float: inline-end;
|
||||||
margin-inline-start: 1rem;
|
margin-inline-start: 1rem;
|
||||||
margin-block-end: 1rem;
|
margin-block-end: 1rem;
|
||||||
box-shadow: var(--edge-highlight), var(--shadow);
|
box-shadow: var(--edge-highlight), var(--shadow);
|
||||||
border-radius: var(--rounded-corner);
|
border-radius: var(--rounded-corner);
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
width: calc(var(--content-width) / 3);
|
width: calc(var(--content-width) / 3);
|
||||||
|
|
||||||
@media only screen and (max-width: 720px) {
|
@media only screen and (max-width: 720px) {
|
||||||
float: none;
|
float: none;
|
||||||
margin-inline-start: 0;
|
margin-inline-start: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
:first-child {
|
:first-child {
|
||||||
margin-block-start: 0;
|
margin-block-start: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:last-child {
|
:last-child {
|
||||||
margin-block-end: 0;
|
margin-block-end: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
details {
|
details {
|
||||||
display: flow-root;
|
display: flow-root;
|
||||||
transition: padding-block-end var(--transition);
|
transition: padding-block-end var(--transition);
|
||||||
margin-block-end: 1rem;
|
margin-block-end: 1rem;
|
||||||
box-shadow: var(--edge-highlight), var(--shadow);
|
box-shadow: var(--edge-highlight), var(--shadow);
|
||||||
border-radius: var(--rounded-corner);
|
border-radius: var(--rounded-corner);
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
|
|
||||||
&::details-content {
|
&::details-content {
|
||||||
display: block;
|
display: block;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition-behavior: allow-discrete;
|
transition-behavior: allow-discrete;
|
||||||
transition-duration: var(--transition);
|
transition-duration: var(--transition);
|
||||||
transition-property: opacity, block-size, content-visibility;
|
transition-property: opacity, block-size, content-visibility;
|
||||||
block-size: 0;
|
block-size: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[open] {
|
&[open] {
|
||||||
padding-block-end: 1rem;
|
padding-block-end: 1rem;
|
||||||
|
|
||||||
&::details-content {
|
&::details-content {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
block-size: auto;
|
block-size: auto;
|
||||||
block-size: calc-size(auto, size);
|
block-size: calc-size(auto, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
summary {
|
summary {
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
transform: scaleY(-1);
|
transform: scaleY(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@supports not (block-size: calc-size(auto, size)) {
|
@supports not (block-size: calc-size(auto, size)) {
|
||||||
& ~ * {
|
& ~ * {
|
||||||
animation: slide-in-up var(--transition-bezier);
|
animation: slide-in-up var(--transition-bezier);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
summary {
|
summary {
|
||||||
transition: var(--transition);
|
transition: var(--transition);
|
||||||
color: var(--fg-muted-1);
|
color: var(--fg-muted-1);
|
||||||
font-weight: var(--font-weight-semi-bold);
|
font-weight: var(--font-weight-semi-bold);
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
|
||||||
&::-webkit-details-marker {
|
&::-webkit-details-marker {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
@include icon(pseudo);
|
@include icon(pseudo);
|
||||||
--icon: var(--icon-caret-down);
|
--icon: var(--icon-caret-down);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: rpx(-2);
|
vertical-align: rpx(-2);
|
||||||
transition:
|
transition: all var(--transition), transform var(--transition-bezier);
|
||||||
all var(--transition),
|
margin-inline-end: 0.25rem;
|
||||||
transform var(--transition-bezier);
|
}
|
||||||
margin-inline-end: 0.25rem;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& > :last-child,
|
& > :last-child,
|
||||||
& > :last-child img {
|
& > :last-child img {
|
||||||
margin-block-end: 0;
|
margin-block-end: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
figure {
|
figure {
|
||||||
margin-inline: 0;
|
margin-inline: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ol,
|
ol,
|
||||||
ul {
|
ul {
|
||||||
padding-inline-start: 1.5rem;
|
padding-inline-start: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
margin-block: 0.25rem;
|
margin-block: 0.25rem;
|
||||||
padding-inline-start: 0.25rem;
|
padding-inline-start: 0.25rem;
|
||||||
|
|
||||||
&::marker {
|
&::marker {
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
font-weight: var(--font-weight-semi-bold);
|
font-weight: var(--font-weight-semi-bold);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
b,
|
b,
|
||||||
strong {
|
strong {
|
||||||
font-weight: var(--font-weight-semi-bold);
|
font-weight: var(--font-weight-semi-bold);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,100 +1,100 @@
|
||||||
@use "../abstracts/mixins" as *;
|
@use "../abstracts/mixins" as *;
|
||||||
|
|
||||||
.buttons {
|
.buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
margin-block-start: 2rem;
|
margin-block-start: 2rem;
|
||||||
|
|
||||||
&.centered {
|
&.centered {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.start {
|
&.start {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.end {
|
&.end {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.big {
|
&.big {
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttons a,
|
.buttons a,
|
||||||
a.button,
|
a.button,
|
||||||
button {
|
button {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
transition:
|
transition:
|
||||||
background-color var(--transition),
|
background-color var(--transition),
|
||||||
color var(--transition),
|
color var(--transition),
|
||||||
box-shadow var(--transition),
|
box-shadow var(--transition),
|
||||||
transform var(--transition-bezier);
|
transform var(--transition-bezier);
|
||||||
box-shadow: var(--edge-highlight);
|
box-shadow: var(--edge-highlight);
|
||||||
border-radius: var(--rounded-corner);
|
border-radius: var(--rounded-corner);
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
color: var(--fg-muted-1);
|
color: var(--fg-muted-1);
|
||||||
font-weight: var(--font-weight-semi-bold);
|
font-weight: var(--font-weight-semi-bold);
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&:not(:disabled, [aria-disabled="true"]):hover {
|
&:not(:disabled, [aria-disabled="true"]):hover {
|
||||||
background-color: var(--bg-muted-2);
|
background-color: var(--bg-muted-2);
|
||||||
color: var(--fg-muted-2);
|
color: var(--fg-muted-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:disabled, [aria-disabled="true"]):active {
|
&:not(:disabled, [aria-disabled="true"]):active {
|
||||||
transform: var(--active);
|
transform: var(--active);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.suggested {
|
&.suggested {
|
||||||
@include gradient-fill;
|
@include gradient-fill;
|
||||||
|
|
||||||
&:not(:disabled, [aria-disabled="true"]):hover {
|
&:not(:disabled, [aria-disabled="true"]):hover {
|
||||||
@include gradient-fill(hover);
|
@include gradient-fill(hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.subtle {
|
&.subtle {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|
||||||
&:not(:disabled, [aria-disabled="true"]):hover {
|
&:not(:disabled, [aria-disabled="true"]):hover {
|
||||||
box-shadow: var(--edge-highlight);
|
box-shadow: var(--edge-highlight);
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
color: var(--fg-muted-2);
|
color: var(--fg-muted-2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&[aria-busy="true"] {
|
&[aria-busy="true"] {
|
||||||
animation: progress-shimmer var(--transition-long) ease-in-out alternate
|
animation: progress-shimmer var(--transition-long) ease-in-out alternate
|
||||||
infinite;
|
infinite;
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
to right,
|
to right,
|
||||||
transparent 50%,
|
transparent 50%,
|
||||||
var(--bg-muted-1) 75%,
|
var(--bg-muted-1) 75%,
|
||||||
transparent 100%
|
transparent 100%
|
||||||
);
|
);
|
||||||
background-size: 200%;
|
background-size: 200%;
|
||||||
|
|
||||||
@keyframes progress-shimmer {
|
@keyframes progress-shimmer {
|
||||||
to {
|
to {
|
||||||
background-position-x: -200%;
|
background-position-x: -200%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttons.big a,
|
.buttons.big a,
|
||||||
.buttons.big button,
|
.buttons.big button,
|
||||||
a.button.big,
|
a.button.big,
|
||||||
button.big {
|
button.big {
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
padding: 0.625rem 1.25rem;
|
padding: 0.625rem 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
@use "../abstracts/mixins" as *;
|
@use "../abstracts/mixins" as *;
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
@include card;
|
@include card;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,90 +1,90 @@
|
||||||
.crt,
|
.crt,
|
||||||
pre:has(code[data-crt], code[data-lang=""]) {
|
pre:has(code[data-crt], code[data-lang=""]) {
|
||||||
position: relative;
|
position: relative;
|
||||||
animation: flicker 0.25s alternate infinite;
|
animation: flicker 0.25s alternate infinite;
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
box-shadow: var(--shadow-glow);
|
box-shadow: var(--shadow-glow);
|
||||||
background-image: var(--crt-bg);
|
background-image: var(--crt-bg);
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--accent-color) !important;
|
color: var(--accent-color) !important;
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
text-shadow: var(--text-shadow-glow);
|
text-shadow: var(--text-shadow-glow);
|
||||||
|
|
||||||
@keyframes flicker {
|
@keyframes flicker {
|
||||||
25% {
|
25% {
|
||||||
opacity: 0.95;
|
opacity: 0.95;
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
75% {
|
75% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&::before,
|
&::before,
|
||||||
&::after {
|
&::after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
content: "";
|
content: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
opacity: var(--scanlines-opacity);
|
opacity: var(--scanlines-opacity);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
animation: scanlines 0.1s linear infinite;
|
animation: scanlines 0.1s linear infinite;
|
||||||
background-image: repeating-linear-gradient(
|
background-image: repeating-linear-gradient(
|
||||||
to bottom,
|
to bottom,
|
||||||
rgba(0, 0, 0, 0.25),
|
rgba(0, 0, 0, 0.25),
|
||||||
rgba(0, 0, 0, 0.25) 0.125rem,
|
rgba(0, 0, 0, 0.25) 0.125rem,
|
||||||
transparent 0.125rem,
|
transparent 0.125rem,
|
||||||
transparent 0.25rem
|
transparent 0.25rem
|
||||||
);
|
);
|
||||||
background-position-y: -0.125rem;
|
background-position-y: -0.125rem;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
@keyframes scanlines {
|
@keyframes scanlines {
|
||||||
to {
|
to {
|
||||||
background-position-y: 0.125rem;
|
background-position-y: 0.125rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
opacity: 0.05;
|
opacity: 0.05;
|
||||||
animation: scanline 5s linear infinite;
|
animation: scanline 5s linear infinite;
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
to bottom,
|
to bottom,
|
||||||
transparent,
|
transparent,
|
||||||
var(--accent-color) 16rem
|
var(--accent-color) 16rem
|
||||||
);
|
);
|
||||||
background-position-y: -16rem;
|
background-position-y: -16rem;
|
||||||
background-size: 100% 16rem;
|
background-size: 100% 16rem;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
@keyframes scanline {
|
@keyframes scanline {
|
||||||
to {
|
to {
|
||||||
background-position-y: calc(100% + 16rem);
|
background-position-y: calc(100% + 16rem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > code {
|
& > code {
|
||||||
display: block;
|
display: block;
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
@use "../abstracts/mixins" as *;
|
@use "../abstracts/mixins" as *;
|
||||||
|
|
||||||
a.external:not(:has(img, svg, video, picture, figure))::after {
|
a.external:not(:has(img, svg, video, picture, figure))::after {
|
||||||
@include icon(pseudo);
|
@include icon(pseudo);
|
||||||
--icon: var(--icon-arrow-square-out);
|
--icon: var(--icon-arrow-square-out);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: -0.125rem;
|
vertical-align: -0.125rem;
|
||||||
opacity: var(--dim-opacity);
|
opacity: var(--dim-opacity);
|
||||||
margin-inline-start: 0.125rem;
|
margin-inline-start: 0.125rem;
|
||||||
font-size: max(1rem, 1em);
|
font-size: max(1rem, 1em);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
iframe {
|
iframe {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 1rem auto;
|
margin: 1rem auto;
|
||||||
box-shadow: var(--edge-highlight), var(--shadow);
|
box-shadow: var(--edge-highlight), var(--shadow);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--rounded-corner);
|
border-radius: var(--rounded-corner);
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
||||||
&:fullscreen {
|
&:fullscreen {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:-webkit-full-screen {
|
&:-webkit-full-screen {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
@use "../abstracts/mixins" as *;
|
@use "../abstracts/mixins" as *;
|
||||||
|
|
||||||
input {
|
input {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
input:not([type]),
|
input:not([type]),
|
||||||
|
|
@ -10,252 +10,250 @@ input[type="text"],
|
||||||
input[type="search"],
|
input[type="search"],
|
||||||
input[type="password"],
|
input[type="password"],
|
||||||
input[type="submit"] {
|
input[type="submit"] {
|
||||||
box-shadow: var(--edge-highlight);
|
box-shadow: var(--edge-highlight);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--rounded-corner);
|
border-radius: var(--rounded-corner);
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:not([type]),
|
input:not([type]),
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
input[type="search"],
|
input[type="search"],
|
||||||
input[type="password"] {
|
input[type="password"] {
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
color: var(--fg-muted-1);
|
color: var(--fg-muted-1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="submit"] {
|
input[type="submit"] {
|
||||||
transition:
|
transition:
|
||||||
background-color var(--transition),
|
background-color var(--transition),
|
||||||
transform var(--transition-bezier);
|
transform var(--transition-bezier);
|
||||||
box-shadow: var(--edge-highlight);
|
box-shadow: var(--edge-highlight);
|
||||||
border-radius: var(--rounded-corner);
|
border-radius: var(--rounded-corner);
|
||||||
padding: 0.375rem 0.75rem;
|
padding: 0.375rem 0.75rem;
|
||||||
color: var(--fg-muted-1);
|
color: var(--fg-muted-1);
|
||||||
font-weight: var(--font-weight-semi-bold);
|
font-weight: var(--font-weight-semi-bold);
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
|
|
||||||
&:enabled:hover {
|
&:enabled:hover {
|
||||||
background-color: var(--bg-muted-2);
|
background-color: var(--bg-muted-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:enabled:active {
|
&:enabled:active {
|
||||||
transform: var(--active);
|
transform: var(--active);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="radio"],
|
input[type="radio"],
|
||||||
input[type="checkbox"],
|
input[type="checkbox"],
|
||||||
input[type="color"] {
|
input[type="color"] {
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: background-color var(--transition);
|
transition: background-color var(--transition);
|
||||||
border: 0.125rem solid var(--bg-muted-2);
|
border: 0.125rem solid var(--bg-muted-2);
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
|
|
||||||
&:not(:checked):enabled:hover {
|
&:not(:checked):enabled:hover {
|
||||||
background-color: var(--bg-muted-2);
|
background-color: var(--bg-muted-2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="radio"],
|
input[type="radio"],
|
||||||
input[type="checkbox"] {
|
input[type="checkbox"] {
|
||||||
vertical-align: -0.125em;
|
vertical-align: -0.125em;
|
||||||
margin-inline-end: 0.25rem;
|
margin-inline-end: 0.25rem;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transform: scale(0.5);
|
transform: scale(0.5);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition:
|
transition: opacity var(--transition), transform var(--transition-bezier);
|
||||||
opacity var(--transition),
|
content: "";
|
||||||
transform var(--transition-bezier);
|
}
|
||||||
content: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
&:checked {
|
&:checked {
|
||||||
border: 0.15rem solid transparent;
|
border: 0.15rem solid transparent;
|
||||||
background-color: var(--accent-color);
|
background-color: var(--accent-color);
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="radio"] {
|
input[type="radio"] {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
inset-block-start: 0.125rem;
|
inset-block-start: 0.125rem;
|
||||||
inset-inline-start: 0.125rem;
|
inset-inline-start: 0.125rem;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: var(--contrast-color);
|
background-color: var(--contrast-color);
|
||||||
width: 0.5rem;
|
width: 0.5rem;
|
||||||
height: 0.5rem;
|
height: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="checkbox"] {
|
input[type="checkbox"] {
|
||||||
border-radius: calc(var(--rounded-corner-small) / 2);
|
border-radius: calc(var(--rounded-corner-small) / 2);
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
@include icon(pseudo);
|
@include icon(pseudo);
|
||||||
--icon: var(--icon-check);
|
--icon: var(--icon-check);
|
||||||
transform-origin: bottom left;
|
transform-origin: bottom left;
|
||||||
inset-block-start: -0.125rem;
|
inset-block-start: -0.125rem;
|
||||||
inset-inline-start: -0.125rem;
|
inset-inline-start: -0.125rem;
|
||||||
color: var(--contrast-color);
|
color: var(--contrast-color);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.switch {
|
&.switch {
|
||||||
vertical-align: -0.375rem;
|
vertical-align: -0.375rem;
|
||||||
box-shadow: var(--edge-highlight);
|
box-shadow: var(--edge-highlight);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
width: 2.5rem;
|
width: 2.5rem;
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
|
|
||||||
&.big {
|
&.big {
|
||||||
vertical-align: -0.625rem;
|
vertical-align: -0.625rem;
|
||||||
width: 3rem;
|
width: 3rem;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
transform: none;
|
transform: none;
|
||||||
transform-origin: center;
|
transform-origin: center;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
mask-image: none;
|
mask-image: none;
|
||||||
inset-block-start: 0.25rem;
|
inset-block-start: 0.25rem;
|
||||||
inset-inline-start: 0.25rem;
|
inset-inline-start: 0.25rem;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
content: "";
|
content: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
&:checked {
|
&:checked {
|
||||||
background-color: var(--accent-color);
|
background-color: var(--accent-color);
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
transform: translateX(1rem);
|
transform: translateX(1rem);
|
||||||
background-color: var(--contrast-color);
|
background-color: var(--contrast-color);
|
||||||
|
|
||||||
:root[dir*="rtl"] & {
|
:root[dir*="rtl"] & {
|
||||||
transform: translateX(-1rem);
|
transform: translateX(-1rem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled::before {
|
&:disabled::before {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="color"] {
|
input[type="color"] {
|
||||||
vertical-align: -0.375em;
|
vertical-align: -0.375em;
|
||||||
box-shadow: var(--edge-highlight);
|
box-shadow: var(--edge-highlight);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--rounded-corner-small);
|
border-radius: var(--rounded-corner-small);
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
width: 3rem;
|
width: 3rem;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
|
|
||||||
&::-moz-color-swatch {
|
&::-moz-color-swatch {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: calc(var(--rounded-corner-small) - 0.25rem);
|
border-radius: calc(var(--rounded-corner-small) - 0.25rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-color-swatch-wrapper {
|
&::-webkit-color-swatch-wrapper {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-color-swatch {
|
&::-webkit-color-swatch {
|
||||||
border-radius: calc(var(--rounded-corner-small) - 0.25rem);
|
border-radius: calc(var(--rounded-corner-small) - 0.25rem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="range"] {
|
input[type="range"] {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
box-shadow: var(--edge-highlight);
|
box-shadow: var(--edge-highlight);
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
background: var(--bg-muted-1);
|
background: var(--bg-muted-1);
|
||||||
padding: 0.5rem 0.25rem;
|
padding: 0.5rem 0.25rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
|
|
||||||
&::-webkit-slider-runnable-track {
|
&::-webkit-slider-runnable-track {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
background: var(--accent-color);
|
background: var(--accent-color);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-moz-range-track {
|
&::-moz-range-track {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
background: var(--accent-color);
|
background: var(--accent-color);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 0.5rem;
|
height: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-slider-thumb {
|
&::-webkit-slider-thumb {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
cursor: ew-resize;
|
cursor: ew-resize;
|
||||||
margin-block: -0.5rem;
|
margin-block: -0.5rem;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
width: 0.5rem;
|
width: 0.5rem;
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-moz-range-thumb {
|
&::-moz-range-thumb {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
cursor: ew-resize;
|
cursor: ew-resize;
|
||||||
margin-block: -0.5rem;
|
margin-block: -0.5rem;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
width: 0.5rem;
|
width: 0.5rem;
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.hollow {
|
&.hollow {
|
||||||
&::-webkit-slider-thumb {
|
&::-webkit-slider-thumb {
|
||||||
margin-block: 0;
|
margin-block: 0;
|
||||||
outline: 0.25rem solid white;
|
outline: 0.25rem solid white;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
height: 0.5rem;
|
height: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-moz-range-thumb {
|
&::-moz-range-thumb {
|
||||||
margin-block: 0;
|
margin-block: 0;
|
||||||
outline: 0.25rem solid white;
|
outline: 0.25rem solid white;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
height: 0.5rem;
|
height: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,165 +1,165 @@
|
||||||
img,
|
img,
|
||||||
video {
|
video {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 1rem auto;
|
margin: 1rem auto;
|
||||||
box-shadow: var(--edge-highlight), var(--shadow);
|
box-shadow: var(--edge-highlight), var(--shadow);
|
||||||
border-radius: var(--rounded-corner);
|
border-radius: var(--rounded-corner);
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: calc(100vh - 2rem);
|
max-height: calc(100vh - 2rem);
|
||||||
|
|
||||||
&.full,
|
&.full,
|
||||||
&[src*="#full"] {
|
&[src*="#full"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.start,
|
&.start,
|
||||||
&.end,
|
&.end,
|
||||||
&[src*="#start"],
|
&[src*="#start"],
|
||||||
&[src*="#end"] {
|
&[src*="#end"] {
|
||||||
width: calc(var(--content-width) / 3);
|
width: calc(var(--content-width) / 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.start,
|
&.start,
|
||||||
&[src*="#start"] {
|
&[src*="#start"] {
|
||||||
float: inline-start;
|
float: inline-start;
|
||||||
transform-origin: left;
|
transform-origin: left;
|
||||||
margin-inline-end: 1rem;
|
margin-inline-end: 1rem;
|
||||||
|
|
||||||
:root[dir*="rtl"] & {
|
:root[dir*="rtl"] & {
|
||||||
transform-origin: right;
|
transform-origin: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.end,
|
&.end,
|
||||||
&[src*="#end"] {
|
&[src*="#end"] {
|
||||||
float: inline-end;
|
float: inline-end;
|
||||||
transform-origin: right;
|
transform-origin: right;
|
||||||
margin-inline-start: 1rem;
|
margin-inline-start: 1rem;
|
||||||
|
|
||||||
:root[dir*="rtl"] & {
|
:root[dir*="rtl"] & {
|
||||||
transform-origin: left;
|
transform-origin: left;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.pixels,
|
&.pixels,
|
||||||
&[src*="#pixels"] {
|
&[src*="#pixels"] {
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.has-alpha,
|
&.has-alpha,
|
||||||
&[src*="#has-alpha"] {
|
&[src*="#has-alpha"] {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.drop-shadow,
|
&.drop-shadow,
|
||||||
&[src*="#drop-shadow"] {
|
&[src*="#drop-shadow"] {
|
||||||
filter: var(--drop-shadow);
|
filter: var(--drop-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.spoiler,
|
&.spoiler,
|
||||||
&[src*="#spoiler"] {
|
&[src*="#spoiler"] {
|
||||||
opacity: var(--dim-opacity);
|
opacity: var(--dim-opacity);
|
||||||
clip-path: inset(0 0 0 0 round var(--rounded-corner));
|
clip-path: inset(0 0 0 0 round var(--rounded-corner));
|
||||||
filter: blur(1.5rem);
|
filter: blur(1.5rem);
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:active {
|
&:active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
clip-path: inset(
|
clip-path: inset(
|
||||||
-0.75rem -0.75rem -0.75rem -0.75rem round var(--rounded-corner-small)
|
-0.75rem -0.75rem -0.75rem -0.75rem round var(--rounded-corner-small)
|
||||||
);
|
);
|
||||||
filter: none;
|
filter: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.solid,
|
&.solid,
|
||||||
&[src*="#solid"] {
|
&[src*="#solid"] {
|
||||||
clip-path: none;
|
clip-path: none;
|
||||||
filter: brightness(0) contrast(0.5);
|
filter: brightness(0) contrast(0.5);
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:active {
|
&:active {
|
||||||
filter: none;
|
filter: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 720px) {
|
@media only screen and (max-width: 720px) {
|
||||||
&.start,
|
&.start,
|
||||||
&.end,
|
&.end,
|
||||||
&[src*="#start"],
|
&[src*="#start"],
|
||||||
&[src*="#end"] {
|
&[src*="#end"] {
|
||||||
float: none;
|
float: none;
|
||||||
margin-inline-start: auto;
|
margin-inline-start: auto;
|
||||||
margin-inline-end: auto;
|
margin-inline-end: auto;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
transition:
|
transition:
|
||||||
box-shadow var(--transition),
|
box-shadow var(--transition),
|
||||||
border-radius var(--transition),
|
border-radius var(--transition),
|
||||||
transform var(--transition-bezier);
|
transform var(--transition-bezier);
|
||||||
|
|
||||||
&:not(.no-hover, [src*="#no-hover"], .emoji) {
|
&:not(.no-hover, [src*="#no-hover"], .emoji) {
|
||||||
cursor: zoom-in;
|
cursor: zoom-in;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
position: relative;
|
position: relative;
|
||||||
transform: var(--hover);
|
transform: var(--hover);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
box-shadow: var(--edge-highlight), var(--shadow-raised);
|
box-shadow: var(--edge-highlight), var(--shadow-raised);
|
||||||
border-radius: var(--rounded-corner-small);
|
border-radius: var(--rounded-corner-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.start,
|
&.start,
|
||||||
&.end,
|
&.end,
|
||||||
&[src*="#start"],
|
&[src*="#start"],
|
||||||
&[src*="#end"] {
|
&[src*="#end"] {
|
||||||
transition: transform var(--transition-bezier-longer);
|
transition: transform var(--transition-bezier-longer);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: scale(2);
|
transform: scale(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 720px) {
|
@media only screen and (max-width: 720px) {
|
||||||
transform-origin: center;
|
transform-origin: center;
|
||||||
transition: transform var(--transition-bezier);
|
transition: transform var(--transition-bezier);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: var(--hover);
|
transform: var(--hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.has-alpha,
|
&.has-alpha,
|
||||||
&[src*="#has-alpha"] {
|
&[src*="#has-alpha"] {
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a img:not(.no-hover, [src*="#no-hover"]) {
|
a img:not(.no-hover, [src*="#no-hover"]) {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
video:fullscreen {
|
video:fullscreen {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
video:-webkit-full-screen {
|
video:-webkit-full-screen {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
picture {
|
picture {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
.overflow-auto {
|
.overflow-auto {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
&.overshoot {
|
&.overshoot {
|
||||||
margin-block: -1rem;
|
margin-block: -1rem;
|
||||||
padding-block: 1rem;
|
padding-block: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.overshoot-row {
|
&.overshoot-row {
|
||||||
margin-inline: -1rem;
|
margin-inline: -1rem;
|
||||||
padding-inline: 1rem;
|
padding-inline: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,23 @@
|
||||||
.overshoot {
|
.overshoot {
|
||||||
--mask: linear-gradient(
|
--mask: linear-gradient(
|
||||||
to bottom,
|
to bottom,
|
||||||
transparent,
|
transparent,
|
||||||
black 1rem,
|
black 1rem,
|
||||||
black calc(100% - 1rem),
|
black calc(100% - 1rem),
|
||||||
transparent
|
transparent
|
||||||
);
|
);
|
||||||
-webkit-mask-image: var(--mask);
|
-webkit-mask-image: var(--mask);
|
||||||
mask-image: var(--mask);
|
mask-image: var(--mask);
|
||||||
|
|
||||||
&-row {
|
&-row {
|
||||||
--mask: linear-gradient(
|
--mask: linear-gradient(
|
||||||
to right,
|
to right,
|
||||||
transparent,
|
transparent,
|
||||||
black 1rem,
|
black 1rem,
|
||||||
black calc(100% - 1rem),
|
black calc(100% - 1rem),
|
||||||
transparent
|
transparent
|
||||||
);
|
);
|
||||||
-webkit-mask-image: var(--mask);
|
-webkit-mask-image: var(--mask);
|
||||||
mask-image: var(--mask);
|
mask-image: var(--mask);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,23 @@
|
||||||
#skip-to-content {
|
#skip-to-content {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transform: translateX(-50%) translateY(calc(-100% - 1rem));
|
transform: translateX(-50%) translateY(calc(-100% - 1rem));
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
transition:
|
transition:
|
||||||
backdrop-filter var(--transition),
|
backdrop-filter var(--transition),
|
||||||
box-shadow var(--transition),
|
box-shadow var(--transition),
|
||||||
transform var(--transition-bezier);
|
transform var(--transition-bezier);
|
||||||
inset-inline-start: 50vw;
|
inset-inline-start: 50vw;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
background-color: var(--glass-bg-1);
|
background-color: var(--glass-bg-1);
|
||||||
padding: 0.375rem 0.75rem;
|
padding: 0.375rem 0.75rem;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&:focus-visible,
|
&:focus-visible,
|
||||||
&:focus {
|
&:focus {
|
||||||
-webkit-backdrop-filter: var(--blur);
|
-webkit-backdrop-filter: var(--blur);
|
||||||
transform: translateX(-50%) translateY(1rem);
|
transform: translateX(-50%) translateY(1rem);
|
||||||
backdrop-filter: var(--blur);
|
backdrop-filter: var(--blur);
|
||||||
box-shadow: var(--edge-highlight), var(--shadow);
|
box-shadow: var(--edge-highlight), var(--shadow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,23 @@
|
||||||
span.spoiler {
|
span.spoiler {
|
||||||
filter: blur(0.25rem);
|
filter: blur(0.25rem);
|
||||||
transition: filter var(--transition);
|
transition: filter var(--transition);
|
||||||
|
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
filter: none;
|
filter: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.solid {
|
&.solid {
|
||||||
filter: none;
|
filter: none;
|
||||||
transition:
|
transition: background-color var(--transition), color var(--transition);
|
||||||
background-color var(--transition),
|
border-radius: var(--rounded-corner-small);
|
||||||
color var(--transition);
|
background-color: var(--fg-muted-1);
|
||||||
border-radius: var(--rounded-corner-small);
|
color: transparent;
|
||||||
background-color: var(--fg-muted-1);
|
|
||||||
color: transparent;
|
|
||||||
|
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,36 @@
|
||||||
@use "../abstracts/functions" as *;
|
@use "../abstracts/functions" as *;
|
||||||
|
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
thead {
|
thead {
|
||||||
border-block-start: none;
|
border-block-start: none;
|
||||||
color: var(--fg-muted-2);
|
color: var(--fg-muted-2);
|
||||||
font-weight: var(--font-weight-semi-bold);
|
font-weight: var(--font-weight-semi-bold);
|
||||||
}
|
}
|
||||||
|
|
||||||
tr {
|
tr {
|
||||||
&:nth-child(even) {
|
&:nth-child(even) {
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
th,
|
th,
|
||||||
td {
|
td {
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody tr {
|
tbody tr {
|
||||||
border-block: rpx(1) solid var(--bg-muted-2);
|
border-block: rpx(1) solid var(--bg-muted-2);
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-block-end: none;
|
border-block-end: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.overflow-auto table {
|
.overflow-auto table {
|
||||||
width: max-content;
|
width: max-content;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
textarea {
|
textarea {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
box-shadow: var(--edge-highlight);
|
box-shadow: var(--edge-highlight);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--rounded-corner);
|
border-radius: var(--rounded-corner);
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
resize: none;
|
resize: none;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
color: var(--fg-muted-1);
|
color: var(--fg-muted-1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
strong.title {
|
strong.title {
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
font-size: var(--font-size-large);
|
font-size: var(--font-size-large);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,41 @@
|
||||||
// attr() hack from https://cyrneko.eu/css-tricks.html
|
// attr() hack from https://cyrneko.eu/css-tricks.html
|
||||||
@media (pointer: coarse), (hover: none) {
|
@media (pointer: coarse), (hover: none) {
|
||||||
.tooltip,
|
.tooltip,
|
||||||
abbr[title] {
|
abbr[title] {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(100% + 0.25rem);
|
top: calc(100% + 0.25rem);
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
backdrop-filter: var(--blur);
|
backdrop-filter: var(--blur);
|
||||||
transition:
|
transition:
|
||||||
visibility var(--transition),
|
visibility var(--transition),
|
||||||
opacity var(--transition),
|
opacity var(--transition),
|
||||||
transform var(--transition-bezier);
|
transform var(--transition-bezier);
|
||||||
box-shadow: var(--edge-highlight), var(--shadow);
|
box-shadow: var(--edge-highlight), var(--shadow);
|
||||||
border-radius: var(--rounded-corner-small);
|
border-radius: var(--rounded-corner-small);
|
||||||
background-color: var(--glass-bg-1);
|
background-color: var(--glass-bg-1);
|
||||||
padding: 0.125rem 0.375rem;
|
padding: 0.125rem 0.375rem;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
content: attr(title);
|
content: attr(title);
|
||||||
-webkit-backdrop-filter: var(--blur);
|
-webkit-backdrop-filter: var(--blur);
|
||||||
transform: translateY(-0.5rem) scale(0.5);
|
transform: translateY(-0.5rem) scale(0.5);
|
||||||
transform-origin: top;
|
transform-origin: top;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
max-width: calc(var(--content-width) / 3);
|
max-width: calc(var(--content-width) / 3);
|
||||||
color: var(--fg-muted-2);
|
color: var(--fg-muted-2);
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus::after,
|
&:focus::after,
|
||||||
&:hover::after {
|
&:hover::after {
|
||||||
transform: none;
|
transform: none;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
.visually-hidden {
|
.visually-hidden {
|
||||||
position: absolute !important;
|
position: absolute !important;
|
||||||
clip-path: inset(50%) !important;
|
clip-path: inset(50%) !important;
|
||||||
margin: -1px !important;
|
margin: -1px !important;
|
||||||
border-width: 0 !important;
|
border-width: 0 !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
width: 1px !important;
|
width: 1px !important;
|
||||||
height: 1px !important;
|
height: 1px !important;
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
white-space: nowrap !important;
|
white-space: nowrap !important;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,151 +3,151 @@
|
||||||
@use "../citrine/abstracts/variables" as *;
|
@use "../citrine/abstracts/variables" as *;
|
||||||
|
|
||||||
[class^="markdown-alert"] {
|
[class^="markdown-alert"] {
|
||||||
display: flow-root;
|
display: flow-root;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
margin-inline-start: 1.5rem;
|
margin-inline-start: 1.5rem;
|
||||||
content: var(--alert-title);
|
content: var(--alert-title);
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
font-weight: var(--font-weight-semi-bold);
|
font-weight: var(--font-weight-semi-bold);
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
@include icon(pseudo);
|
@include icon(pseudo);
|
||||||
--icon: var(--alert-icon);
|
--icon: var(--alert-icon);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
margin-inline-end: 0.25rem;
|
margin-inline-end: 0.25rem;
|
||||||
inset-block-start: rpx(1);
|
inset-block-start: rpx(1);
|
||||||
inset-inline-start: 0.75rem;
|
inset-inline-start: 0.75rem;
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > :first-child {
|
& > :first-child {
|
||||||
margin-block-start: 0.25rem;
|
margin-block-start: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > :last-child {
|
& > :last-child {
|
||||||
margin-block-end: 0;
|
margin-block-end: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-alert {
|
.markdown-alert {
|
||||||
// GitHub flavored alerts
|
// GitHub flavored alerts
|
||||||
&-note,
|
&-note,
|
||||||
&-tip,
|
&-tip,
|
||||||
&-important,
|
&-important,
|
||||||
&-warning,
|
&-warning,
|
||||||
&-caution,
|
&-caution,
|
||||||
// Generic colors
|
// Generic colors
|
||||||
&-red,
|
&-red,
|
||||||
&-orange,
|
&-orange,
|
||||||
&-yellow,
|
&-yellow,
|
||||||
&-green,
|
&-green,
|
||||||
&-blue,
|
&-blue,
|
||||||
&-purple {
|
&-purple {
|
||||||
@include variables-accent(("light", "common"));
|
@include variables-accent(("light", "common"));
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
@include variables-accent("dark");
|
@include variables-accent("dark");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-note {
|
&-note {
|
||||||
&::before {
|
&::before {
|
||||||
content: var(--alert-note-title);
|
content: var(--alert-note-title);
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
--icon: var(--icon-info);
|
--icon: var(--icon-info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-tip {
|
&-tip {
|
||||||
&::before {
|
&::before {
|
||||||
content: var(--alert-tip-title);
|
content: var(--alert-tip-title);
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
--icon: var(--icon-lightbulb);
|
--icon: var(--icon-lightbulb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-important {
|
&-important {
|
||||||
&::before {
|
&::before {
|
||||||
content: var(--alert-important-title);
|
content: var(--alert-important-title);
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
--icon: var(--icon-warning-diamond);
|
--icon: var(--icon-warning-diamond);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-warning {
|
&-warning {
|
||||||
&::before {
|
&::before {
|
||||||
content: var(--alert-warning-title);
|
content: var(--alert-warning-title);
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
--icon: var(--icon-warning);
|
--icon: var(--icon-warning);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-caution {
|
&-caution {
|
||||||
&::before {
|
&::before {
|
||||||
content: var(--alert-caution-title);
|
content: var(--alert-caution-title);
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
--icon: var(--icon-warning-octagon);
|
--icon: var(--icon-warning-octagon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-caution,
|
&-caution,
|
||||||
&-red {
|
&-red {
|
||||||
--accent-h: var(--red-h);
|
--accent-h: var(--red-h);
|
||||||
--accent-s: var(--red-s);
|
--accent-s: var(--red-s);
|
||||||
--accent-l: var(--red-l);
|
--accent-l: var(--red-l);
|
||||||
--accent-color: var(--red-fg);
|
--accent-color: var(--red-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
&-orange {
|
&-orange {
|
||||||
--accent-h: var(--orange-h);
|
--accent-h: var(--orange-h);
|
||||||
--accent-s: var(--orange-s);
|
--accent-s: var(--orange-s);
|
||||||
--accent-l: var(--orange-l);
|
--accent-l: var(--orange-l);
|
||||||
--accent-color: var(--orange-fg);
|
--accent-color: var(--orange-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
&-warning,
|
&-warning,
|
||||||
&-yellow {
|
&-yellow {
|
||||||
--accent-h: var(--yellow-h);
|
--accent-h: var(--yellow-h);
|
||||||
--accent-s: var(--yellow-s);
|
--accent-s: var(--yellow-s);
|
||||||
--accent-l: var(--yellow-l);
|
--accent-l: var(--yellow-l);
|
||||||
--accent-color: var(--yellow-fg);
|
--accent-color: var(--yellow-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
&-tip,
|
&-tip,
|
||||||
&-green {
|
&-green {
|
||||||
--accent-h: var(--green-h);
|
--accent-h: var(--green-h);
|
||||||
--accent-s: var(--green-s);
|
--accent-s: var(--green-s);
|
||||||
--accent-l: var(--green-l);
|
--accent-l: var(--green-l);
|
||||||
--accent-color: var(--green-fg);
|
--accent-color: var(--green-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
&-note,
|
&-note,
|
||||||
&-blue {
|
&-blue {
|
||||||
--accent-h: var(--blue-h);
|
--accent-h: var(--blue-h);
|
||||||
--accent-s: var(--blue-s);
|
--accent-s: var(--blue-s);
|
||||||
--accent-l: var(--blue-l);
|
--accent-l: var(--blue-l);
|
||||||
--accent-color: var(--blue-fg);
|
--accent-color: var(--blue-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
&-important,
|
&-important,
|
||||||
&-purple {
|
&-purple {
|
||||||
--accent-h: var(--purple-h);
|
--accent-h: var(--purple-h);
|
||||||
--accent-s: var(--purple-s);
|
--accent-s: var(--purple-s);
|
||||||
--accent-l: var(--purple-l);
|
--accent-l: var(--purple-l);
|
||||||
--accent-color: var(--purple-fg);
|
--accent-color: var(--purple-fg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,71 +1,71 @@
|
||||||
@use "../citrine/abstracts/functions" as *;
|
@use "../citrine/abstracts/functions" as *;
|
||||||
|
|
||||||
.article-details {
|
.article-details {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
margin-block-end: 2rem;
|
margin-block-end: 2rem;
|
||||||
color: var(--fg-muted-2);
|
color: var(--fg-muted-2);
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
|
|
||||||
.items {
|
.items {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 0.25rem 0.5rem;
|
gap: 0.25rem 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.375rem;
|
gap: 0.375rem;
|
||||||
border-radius: var(--rounded-corner-small);
|
border-radius: var(--rounded-corner-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font-weight: var(--font-weight-normal);
|
font-weight: var(--font-weight-normal);
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
text-decoration-color: transparent;
|
text-decoration-color: transparent;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration-style: solid;
|
text-decoration-style: solid;
|
||||||
text-decoration-color: var(--fg-muted-1);
|
text-decoration-color: var(--fg-muted-1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip {
|
.tooltip {
|
||||||
cursor: help;
|
cursor: help;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
box-shadow: var(--edge-highlight);
|
box-shadow: var(--edge-highlight);
|
||||||
border-radius: var(--rounded-corner-small);
|
border-radius: var(--rounded-corner-small);
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
padding: 0.375rem;
|
padding: 0.375rem;
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags {
|
.tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0 0.5rem;
|
gap: 0 0.5rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-block-start: rpx(1) solid var(--bg-muted-2);
|
border-block-start: rpx(1) solid var(--bg-muted-2);
|
||||||
padding-inline-start: 0;
|
padding-inline-start: 0;
|
||||||
padding-block-start: 0.5rem;
|
padding-block-start: 0.5rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
|
||||||
li {
|
li {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-block: 0;
|
margin-block: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
a::before {
|
a::before {
|
||||||
content: "#";
|
content: "#";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,252 +3,254 @@
|
||||||
@use "../citrine/abstracts/mixins" as *;
|
@use "../citrine/abstracts/mixins" as *;
|
||||||
|
|
||||||
.article-list {
|
.article-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(
|
grid-template-columns: repeat(
|
||||||
auto-fill,
|
auto-fill,
|
||||||
minmax(calc(var(--content-width) / 2.5), 1fr)
|
minmax(calc(var(--content-width) / 2.5), 1fr)
|
||||||
);
|
);
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-block-start: 2rem;
|
margin-block-start: 2rem;
|
||||||
|
|
||||||
&.categories article {
|
&.categories article {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&:hover .icon:last-child {
|
&:hover .icon:last-child {
|
||||||
transform: translateY(-50%) rotate(-10deg) scale(1.5);
|
transform: translateY(-50%) rotate(-10deg) scale(1.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon:last-child {
|
.icon:last-child {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
transition: transform var(--transition-bezier);
|
transition: transform var(--transition-bezier);
|
||||||
inset-block-start: 50%;
|
inset-block-start: 50%;
|
||||||
inset-inline-end: -3rem;
|
inset-inline-end: -3rem;
|
||||||
color: var(--bg-muted-1);
|
color: var(--bg-muted-1);
|
||||||
font-size: 12rem;
|
font-size: 12rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
article {
|
article {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
box-shadow: var(--edge-highlight), var(--shadow);
|
box-shadow: var(--edge-highlight), var(--shadow);
|
||||||
border-radius: 1.25rem;
|
border-radius: 1.25rem;
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&.draft {
|
&.draft {
|
||||||
opacity: var(--disabled-opacity);
|
opacity: var(--disabled-opacity);
|
||||||
filter: grayscale(1);
|
filter: grayscale(1);
|
||||||
transition-duration: var(--transition);
|
transition-duration: var(--transition);
|
||||||
transition-property: opacity, filter;
|
transition-property: opacity, filter;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
filter: none;
|
filter: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.has-banner.has-accent-color {
|
&.has-banner.has-accent-color {
|
||||||
background-image: var(--bg-overlay), var(--blurnail);
|
background-image: var(--bg-overlay), var(--blurnail);
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.has-accent-color {
|
&.has-accent-color {
|
||||||
@include variables-accent(("light", "common"));
|
@include variables-accent(("light", "common"));
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
& {
|
& {
|
||||||
@include variables-accent("dark");
|
@include variables-accent("dark");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
|
|
||||||
& > a {
|
& > a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-inline-start: 0.25rem solid;
|
border-inline-start: 0.25rem solid;
|
||||||
border-start-start-radius: 0.25rem;
|
border-start-start-radius: 0.25rem;
|
||||||
border-end-start-radius: 0.25rem;
|
border-end-start-radius: 0.25rem;
|
||||||
padding-inline-start: 0.5rem;
|
padding-inline-start: 0.5rem;
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
font-weight: var(--font-weight-semi-bold);
|
font-weight: var(--font-weight-semi-bold);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&:hover::after {
|
&:hover::after {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity var(--transition);
|
transition: opacity var(--transition);
|
||||||
content: " →";
|
content: " →";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.description p {
|
.description p {
|
||||||
margin-block-end: 0.75rem;
|
margin-block-end: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.open-post {
|
.open-post {
|
||||||
@include easing-mask(bottom);
|
@include easing-mask(bottom);
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-block-end: -2rem;
|
margin-block-end: -2rem;
|
||||||
box-shadow: var(--edge-highlight);
|
box-shadow: var(--edge-highlight);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
aspect-ratio: 2 / 1;
|
aspect-ratio: 2 / 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
&::before {
|
&::before {
|
||||||
background-color: var(--shade-color);
|
background-color: var(--shade-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
transform: var(--hover);
|
transform: var(--hover);
|
||||||
filter: blur(0.25rem);
|
filter: blur(0.25rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
transform: none;
|
transform: none;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active img {
|
&:active img {
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
transition: background-color var(--transition);
|
transition: background-color var(--transition);
|
||||||
inset: 0;
|
inset: 0;
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
content: "";
|
content: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transition-duration: var(--transition);
|
transition-duration: var(--transition);
|
||||||
transition-property: transform, filter;
|
transition-property: transform, filter;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
transform: scale(1.5);
|
transform: scale(1.5);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
transition:
|
transition:
|
||||||
transform var(--transition-bezier),
|
transform var(--transition-bezier),
|
||||||
opacity var(--transition);
|
opacity var(--transition);
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-details {
|
.article-details {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-block-end: 0;
|
margin-block-end: 0;
|
||||||
|
|
||||||
.tags {
|
.tags {
|
||||||
margin-block-start: auto;
|
margin-block-start: auto;
|
||||||
margin-block-end: -0.25rem;
|
margin-block-end: -0.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.paginator {
|
.paginator {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
margin-block-start: 4rem;
|
margin-block-start: 4rem;
|
||||||
|
|
||||||
&:has(a.paginator-item.previous:active) .paginator-counter {
|
&:has(a.paginator-item.previous:active) .paginator-counter {
|
||||||
border-start-start-radius: 1rem;
|
border-start-start-radius: 1rem;
|
||||||
border-end-start-radius: 1rem;
|
border-end-start-radius: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:has(a.paginator-item.next:active) .paginator-counter {
|
&:has(a.paginator-item.next:active) .paginator-counter {
|
||||||
border-start-end-radius: 1rem;
|
border-start-end-radius: 1rem;
|
||||||
border-end-end-radius: 1rem;
|
border-end-end-radius: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-item {
|
&-item {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
transition:
|
transition:
|
||||||
border-radius var(--transition),
|
border-radius var(--transition),
|
||||||
border-start-end-radius var(--transition),
|
border-start-end-radius var(--transition),
|
||||||
border-end-end-radius var(--transition),
|
border-end-end-radius var(--transition),
|
||||||
border-start-start-radius var(--transition),
|
border-start-start-radius var(--transition),
|
||||||
border-end-start-radius var(--transition),
|
border-end-start-radius var(--transition),
|
||||||
background-color var(--transition),
|
background-color var(--transition),
|
||||||
transform var(--transition-bezier);
|
transform var(--transition-bezier);
|
||||||
box-shadow: var(--edge-highlight);
|
box-shadow: var(--edge-highlight);
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&:not([aria-disabled="true"]):hover {
|
&:not([aria-disabled="true"]):hover {
|
||||||
background-color: var(--bg-muted-2);
|
background-color: var(--bg-muted-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not([aria-disabled="true"]):active {
|
&:not([aria-disabled="true"]):active {
|
||||||
transform: var(--active);
|
transform: var(--active);
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-previous {
|
&-previous {
|
||||||
border-start-end-radius: var(--rounded-corner-small);
|
border-start-end-radius: var(--rounded-corner-small);
|
||||||
border-end-end-radius: var(--rounded-corner-small);
|
border-end-end-radius: var(--rounded-corner-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
&-next {
|
&-next {
|
||||||
border-start-start-radius: var(--rounded-corner-small);
|
border-start-start-radius: var(--rounded-corner-small);
|
||||||
border-end-start-radius: var(--rounded-corner-small);
|
border-end-start-radius: var(--rounded-corner-small);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-counter {
|
&-counter {
|
||||||
@include gradient-fill;
|
@include gradient-fill;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
transition: var(--transition-bezier);
|
transition: var(--transition-bezier);
|
||||||
transition-property:
|
transition-property:
|
||||||
border-start-start-radius, border-end-start-radius,
|
border-start-start-radius,
|
||||||
border-start-end-radius, border-end-end-radius;
|
border-end-start-radius,
|
||||||
border-radius: var(--rounded-corner-small);
|
border-start-end-radius,
|
||||||
background-color: var(--bg-muted-1);
|
border-end-end-radius;
|
||||||
padding: 0.25rem 0.5rem;
|
border-radius: var(--rounded-corner-small);
|
||||||
font-weight: var(--font-weight-semi-bold);
|
background-color: var(--bg-muted-1);
|
||||||
font-variant-numeric: tabular-nums;
|
padding: 0.25rem 0.5rem;
|
||||||
}
|
font-weight: var(--font-weight-semi-bold);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,66 +1,66 @@
|
||||||
.article-nav {
|
.article-nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
|
|
||||||
@media only screen and (max-width: 720px) {
|
@media only screen and (max-width: 720px) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-item {
|
&-item {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
transition:
|
transition:
|
||||||
box-shadow var(--transition),
|
box-shadow var(--transition),
|
||||||
background-color var(--transition),
|
background-color var(--transition),
|
||||||
transform var(--transition-bezier);
|
transform var(--transition-bezier);
|
||||||
border-radius: var(--rounded-corner);
|
border-radius: var(--rounded-corner);
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: var(--edge-highlight), var(--shadow);
|
box-shadow: var(--edge-highlight), var(--shadow);
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
|
|
||||||
.article-title {
|
.article-title {
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
transform: var(--active);
|
transform: var(--active);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.prev {
|
&.prev {
|
||||||
text-align: start;
|
text-align: start;
|
||||||
|
|
||||||
.nav-arrow::before {
|
.nav-arrow::before {
|
||||||
content: "← ";
|
content: "← ";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.next {
|
&.next {
|
||||||
text-align: end;
|
text-align: end;
|
||||||
|
|
||||||
.nav-arrow::after {
|
.nav-arrow::after {
|
||||||
content: " →";
|
content: " →";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-arrow {
|
.nav-arrow {
|
||||||
margin-block-end: 0.5rem;
|
margin-block-end: 0.5rem;
|
||||||
color: var(--fg-muted-2);
|
color: var(--fg-muted-2);
|
||||||
font-weight: var(--font-weight-normal);
|
font-weight: var(--font-weight-normal);
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-title {
|
.article-title {
|
||||||
display: block;
|
display: block;
|
||||||
transition: color var(--transition);
|
transition: color var(--transition);
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--fg-muted-1);
|
color: var(--fg-muted-1);
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,41 +2,41 @@
|
||||||
@use "../citrine/abstracts/mixins" as *;
|
@use "../citrine/abstracts/mixins" as *;
|
||||||
|
|
||||||
.audio-button {
|
.audio-button {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
font-weight: var(--font-weight-semi-bold);
|
font-weight: var(--font-weight-semi-bold);
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
background-color: var(--accent-bg-1);
|
background-color: var(--accent-bg-1);
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition:
|
transition:
|
||||||
background-color var(--transition),
|
background-color var(--transition),
|
||||||
transform var(--transition-bezier),
|
transform var(--transition-bezier),
|
||||||
box-shadow var(--transition);
|
box-shadow var(--transition);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--accent-bg-2);
|
background-color: var(--accent-bg-2);
|
||||||
transform: scale(1.03);
|
transform: scale(1.03);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
transform: var(--active);
|
transform: var(--active);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
outline: 0.125rem solid var(--accent-color);
|
outline: 0.125rem solid var(--accent-color);
|
||||||
outline-offset: 0.25rem;
|
outline-offset: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio-button-label {
|
.audio-button-label {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
@media (min-width: 480px) {
|
@media (min-width: 480px) {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,39 +3,39 @@
|
||||||
|
|
||||||
// Who doesn't like a party popper mid article?
|
// Who doesn't like a party popper mid article?
|
||||||
.audio {
|
.audio {
|
||||||
@include chip;
|
@include chip;
|
||||||
padding-block: 0;
|
padding-block: 0;
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
font-size: var(--font-size-medium);
|
font-size: var(--font-size-medium);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--accent-color) !important;
|
color: var(--accent-color) !important;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
animation: speaker var(--transition-long) ease-out infinite;
|
animation: speaker var(--transition-long) ease-out infinite;
|
||||||
|
|
||||||
@keyframes speaker {
|
@keyframes speaker {
|
||||||
from {
|
from {
|
||||||
--icon: var(--icon-speaker-none);
|
--icon: var(--icon-speaker-none);
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
--icon: var(--icon-speaker-low);
|
--icon: var(--icon-speaker-low);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
--icon: var(--icon-speaker-high);
|
--icon: var(--icon-speaker-high);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
transform: var(--active);
|
transform: var(--active);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
vertical-align: rpx(-3);
|
vertical-align: rpx(-3);
|
||||||
margin-inline-start: 0.25rem;
|
margin-inline-start: 0.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,98 +1,98 @@
|
||||||
@use "../citrine/abstracts/functions" as *;
|
@use "../citrine/abstracts/functions" as *;
|
||||||
|
|
||||||
body.has-banner {
|
body.has-banner {
|
||||||
background-image: var(--bg-overlay), var(--blurnail);
|
background-image: var(--bg-overlay), var(--blurnail);
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
#banner {
|
#banner {
|
||||||
--mask: linear-gradient(black calc(100% - 2rem), transparent);
|
--mask: linear-gradient(black calc(100% - 2rem), transparent);
|
||||||
-webkit-mask-image: var(--mask);
|
-webkit-mask-image: var(--mask);
|
||||||
display: flex;
|
display: flex;
|
||||||
grid-area: banner;
|
grid-area: banner;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
mask-image: var(--mask);
|
mask-image: var(--mask);
|
||||||
margin-block-end: -2.5rem;
|
margin-block-end: -2.5rem;
|
||||||
padding-block: 3.25rem 2.5rem;
|
padding-block: 3.25rem 2.5rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
div {
|
div {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-inline: 1rem;
|
margin-inline: 1rem;
|
||||||
aspect-ratio: 2 / 1;
|
aspect-ratio: 2 / 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: var(--content-width);
|
max-width: var(--content-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
border-radius: var(--rounded-corner) var(--rounded-corner) 0 0 !important;
|
border-radius: var(--rounded-corner) var(--rounded-corner) 0 0 !important;
|
||||||
aspect-ratio: 2 / 1;
|
aspect-ratio: 2 / 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
img:nth-of-type(1) {
|
img:nth-of-type(1) {
|
||||||
opacity: var(--dim-opacity);
|
opacity: var(--dim-opacity);
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
filter: blur(1.5rem);
|
filter: blur(1.5rem);
|
||||||
animation: banner-glow-load calc(0.525s + var(--transition-long));
|
animation: banner-glow-load calc(0.525s + var(--transition-long));
|
||||||
inset-block-start: 1rem;
|
inset-block-start: 1rem;
|
||||||
|
|
||||||
@keyframes banner-glow-load {
|
@keyframes banner-glow-load {
|
||||||
from,
|
from,
|
||||||
52.5% {
|
52.5% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
img:nth-of-type(2) {
|
img:nth-of-type(2) {
|
||||||
transform-origin: bottom;
|
transform-origin: bottom;
|
||||||
animation: banner-load calc(0.525s + var(--transition-longer));
|
animation: banner-load calc(0.525s + var(--transition-longer));
|
||||||
|
|
||||||
@keyframes banner-load {
|
@keyframes banner-load {
|
||||||
from,
|
from,
|
||||||
52.5% {
|
52.5% {
|
||||||
transform: translateY(2rem) scale(0.95);
|
transform: translateY(2rem) scale(0.95);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& + #main-content {
|
& + #main-content {
|
||||||
margin-block-start: 0;
|
margin-block-start: 0;
|
||||||
|
|
||||||
article > h1:first-of-type,
|
article > h1:first-of-type,
|
||||||
h1:first-of-type {
|
h1:first-of-type {
|
||||||
margin-inline: -1rem;
|
margin-inline: -1rem;
|
||||||
margin-block: 0;
|
margin-block: 0;
|
||||||
border-radius: var(--rounded-corner) var(--rounded-corner) 0 0;
|
border-radius: var(--rounded-corner) var(--rounded-corner) 0 0;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
width: unset;
|
width: unset;
|
||||||
-webkit-backdrop-filter: blur(1rem);
|
-webkit-backdrop-filter: blur(1rem);
|
||||||
backdrop-filter: blur(1rem);
|
backdrop-filter: blur(1rem);
|
||||||
animation: banner-heading calc(0.525s + var(--transition-longer));
|
animation: banner-heading calc(0.525s + var(--transition-longer));
|
||||||
box-shadow: inset 0 rpx(1) 0 var(--accent-highlight);
|
box-shadow: inset 0 rpx(1) 0 var(--accent-highlight);
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
to bottom,
|
to bottom,
|
||||||
var(--bg-muted-1),
|
var(--bg-muted-1),
|
||||||
transparent 50%
|
transparent 50%
|
||||||
);
|
);
|
||||||
background-size: auto 200%;
|
background-size: auto 200%;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
@keyframes banner-heading {
|
@keyframes banner-heading {
|
||||||
from,
|
from,
|
||||||
52.5% {
|
52.5% {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
background-position-y: 100%;
|
background-position-y: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,458 +2,458 @@
|
||||||
@use "../citrine/abstracts/mixins" as *;
|
@use "../citrine/abstracts/mixins" as *;
|
||||||
|
|
||||||
#comments {
|
#comments {
|
||||||
#qrcode {
|
#qrcode {
|
||||||
float: inline-end;
|
float: inline-end;
|
||||||
cursor: help;
|
cursor: help;
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
margin-inline-start: 1rem;
|
margin-inline-start: 1rem;
|
||||||
margin-block-start: 3rem;
|
margin-block-start: 3rem;
|
||||||
box-shadow: var(--edge-highlight), var(--shadow);
|
box-shadow: var(--edge-highlight), var(--shadow);
|
||||||
border-radius: var(--rounded-corner);
|
border-radius: var(--rounded-corner);
|
||||||
background-color: white;
|
background-color: white;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
width: rpx(116);
|
width: rpx(116);
|
||||||
height: rpx(116);
|
height: rpx(116);
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 720px) {
|
@media only screen and (max-width: 720px) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#comments-wrapper {
|
#comments-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
margin-block-start: 2rem;
|
margin-block-start: 2rem;
|
||||||
|
|
||||||
#comments-status {
|
#comments-status {
|
||||||
color: var(--fg-muted-1);
|
color: var(--fg-muted-1);
|
||||||
font-weight: var(--font-weight-semi-bold);
|
font-weight: var(--font-weight-semi-bold);
|
||||||
font-size: var(--font-size-x-large);
|
font-size: var(--font-size-x-large);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment {
|
.comment {
|
||||||
display: grid;
|
display: grid;
|
||||||
// grid-template-columns: min-content;
|
// grid-template-columns: min-content;
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"avatar header "
|
"avatar header "
|
||||||
"...... post "
|
"...... post "
|
||||||
"...... media "
|
"...... media "
|
||||||
"...... card "
|
"...... card "
|
||||||
"...... interactions";
|
"...... interactions";
|
||||||
column-gap: 1rem;
|
column-gap: 1rem;
|
||||||
justify-items: start;
|
justify-items: start;
|
||||||
animation: slide-in-right var(--transition-bezier);
|
animation: slide-in-right var(--transition-bezier);
|
||||||
|
|
||||||
:root[dir*="rtl"] & {
|
:root[dir*="rtl"] & {
|
||||||
animation: slide-in-left var(--transition-bezier);
|
animation: slide-in-left var(--transition-bezier);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 720px) {
|
@media only screen and (max-width: 720px) {
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"avatar header "
|
"avatar header "
|
||||||
"post post "
|
"post post "
|
||||||
"media media "
|
"media media "
|
||||||
"card card "
|
"card card "
|
||||||
"interactions interactions";
|
"interactions interactions";
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.comment-reply {
|
&.comment-reply {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-inline-start: 0.25rem solid var(--bg-muted-2);
|
border-inline-start: 0.25rem solid var(--bg-muted-2);
|
||||||
border-start-start-radius: 0.25rem;
|
border-start-start-radius: 0.25rem;
|
||||||
border-end-start-radius: 0.25rem;
|
border-end-start-radius: 0.25rem;
|
||||||
padding-inline-start: 1rem;
|
padding-inline-start: 1rem;
|
||||||
|
|
||||||
&:has(+ .comment-reply) {
|
&:has(+ .comment-reply) {
|
||||||
border-end-start-radius: 0;
|
border-end-start-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
& + .comment-reply {
|
& + .comment-reply {
|
||||||
margin-block-start: -2rem;
|
margin-block-start: -2rem;
|
||||||
border-start-start-radius: 0;
|
border-start-start-radius: 0;
|
||||||
padding-block-start: 2rem;
|
padding-block-start: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-link {
|
.avatar-link {
|
||||||
grid-area: avatar;
|
grid-area: avatar;
|
||||||
width: 4rem;
|
width: 4rem;
|
||||||
height: 4rem;
|
height: 4rem;
|
||||||
|
|
||||||
@media only screen and (max-width: 480px) {
|
@media only screen and (max-width: 480px) {
|
||||||
width: 3rem;
|
width: 3rem;
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
transition: transform var(--transition-bezier);
|
transition: transform var(--transition-bezier);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: rotate(10deg) var(--hover);
|
transform: rotate(10deg) var(--hover);
|
||||||
border-radius: var(--rounded-corner);
|
border-radius: var(--rounded-corner);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
transform: var(--active);
|
transform: var(--active);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mention {
|
.mention {
|
||||||
@include chip;
|
@include chip;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--bg-muted-2);
|
background-color: var(--bg-muted-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
transform: var(--active);
|
transform: var(--active);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.invisible {
|
.invisible {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ellipsis::after {
|
.ellipsis::after {
|
||||||
content: "…";
|
content: "…";
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
display: flex;
|
display: flex;
|
||||||
grid-area: header;
|
grid-area: header;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
color: var(--fg-muted-1);
|
color: var(--fg-muted-1);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|
||||||
.display,
|
.display,
|
||||||
.instance {
|
.instance {
|
||||||
display: block;
|
display: block;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.display {
|
.display {
|
||||||
color: var(--fg-color);
|
color: var(--fg-color);
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
.emoji {
|
.emoji {
|
||||||
cursor: unset;
|
cursor: unset;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.instance {
|
.instance {
|
||||||
margin-block-start: 0.25rem;
|
margin-block-start: 0.25rem;
|
||||||
font-weight: var(--font-weight-semi-bold);
|
font-weight: var(--font-weight-semi-bold);
|
||||||
font-size: var(--font-size-x-small);
|
font-size: var(--font-size-x-small);
|
||||||
|
|
||||||
&.op {
|
&.op {
|
||||||
cursor: help;
|
cursor: help;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
@include icon(pseudo);
|
@include icon(pseudo);
|
||||||
--icon: var(--icon-seal-check);
|
--icon: var(--icon-seal-check);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: text-top;
|
vertical-align: text-top;
|
||||||
margin-inline-end: 0.25rem;
|
margin-inline-end: 0.25rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.timestamp {
|
.timestamp {
|
||||||
margin-block-start: 0.25rem;
|
margin-block-start: 0.25rem;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font-size: var(--font-size-x-small);
|
font-size: var(--font-size-x-small);
|
||||||
text-decoration-color: transparent;
|
text-decoration-color: transparent;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration-style: solid;
|
text-decoration-style: solid;
|
||||||
text-decoration-color: inherit;
|
text-decoration-color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
details {
|
details {
|
||||||
border-radius: var(--rounded-corner-small);
|
border-radius: var(--rounded-corner-small);
|
||||||
background-image:
|
background-image:
|
||||||
linear-gradient(
|
linear-gradient(
|
||||||
to right,
|
to right,
|
||||||
transparent,
|
transparent,
|
||||||
transparent 0.5rem,
|
transparent 0.5rem,
|
||||||
var(--bg-muted-1) 0.5rem,
|
var(--bg-muted-1) 0.5rem,
|
||||||
var(--bg-muted-1) calc(100% - 0.5rem),
|
var(--bg-muted-1) calc(100% - 0.5rem),
|
||||||
transparent calc(100% - 0.5rem),
|
transparent calc(100% - 0.5rem),
|
||||||
transparent
|
transparent
|
||||||
),
|
),
|
||||||
linear-gradient(
|
linear-gradient(
|
||||||
to right,
|
to right,
|
||||||
transparent,
|
transparent,
|
||||||
transparent 0.5rem,
|
transparent 0.5rem,
|
||||||
var(--bg-color) 0.5rem,
|
var(--bg-color) 0.5rem,
|
||||||
var(--bg-color) calc(100% - 0.5rem),
|
var(--bg-color) calc(100% - 0.5rem),
|
||||||
transparent calc(100% - 0.5rem),
|
transparent calc(100% - 0.5rem),
|
||||||
transparent
|
transparent
|
||||||
),
|
),
|
||||||
repeating-linear-gradient(
|
repeating-linear-gradient(
|
||||||
45deg,
|
45deg,
|
||||||
var(--contrast-color),
|
var(--contrast-color),
|
||||||
var(--contrast-color) 0.25rem,
|
var(--contrast-color) 0.25rem,
|
||||||
var(--accent-color) 0.25rem,
|
var(--accent-color) 0.25rem,
|
||||||
var(--accent-color) 0.5rem
|
var(--accent-color) 0.5rem
|
||||||
);
|
);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
summary {
|
summary {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background-image: none;
|
background-image: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
grid-area: post;
|
grid-area: post;
|
||||||
justify-self: stretch;
|
justify-self: stretch;
|
||||||
margin-block-start: 1rem;
|
margin-block-start: 1rem;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
:first-child {
|
:first-child {
|
||||||
margin-block-start: 0;
|
margin-block-start: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:last-child {
|
:last-child {
|
||||||
margin-block-end: 0;
|
margin-block-end: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachments {
|
.attachments {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
|
||||||
grid-area: media;
|
grid-area: media;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-block-start: 1rem;
|
margin-block-start: 1rem;
|
||||||
|
|
||||||
img,
|
img,
|
||||||
video {
|
video {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta-card {
|
.meta-card {
|
||||||
grid-area: card;
|
grid-area: card;
|
||||||
transition: transform var(--transition-bezier);
|
transition: transform var(--transition-bezier);
|
||||||
margin-block-start: 1rem;
|
margin-block-start: 1rem;
|
||||||
width: min(calc(var(--content-width) / 2), 100%);
|
width: min(calc(var(--content-width) / 2), 100%);
|
||||||
font-weight: var(--font-weight-normal);
|
font-weight: var(--font-weight-normal);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
img {
|
img {
|
||||||
transform: var(--hover);
|
transform: var(--hover);
|
||||||
box-shadow: var(--edge-highlight), var(--shadow-raised);
|
box-shadow: var(--edge-highlight), var(--shadow-raised);
|
||||||
border-radius: var(--rounded-corner-small);
|
border-radius: var(--rounded-corner-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
figcaption {
|
figcaption {
|
||||||
border-radius: var(--rounded-corner);
|
border-radius: var(--rounded-corner);
|
||||||
background-color: var(--bg-muted-2);
|
background-color: var(--bg-muted-2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
transform: var(--active);
|
transform: var(--active);
|
||||||
}
|
}
|
||||||
|
|
||||||
figure {
|
figure {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-radius: var(--rounded-corner) var(--rounded-corner)
|
border-radius: var(--rounded-corner) var(--rounded-corner)
|
||||||
var(--rounded-corner-small) var(--rounded-corner-small);
|
var(--rounded-corner-small) var(--rounded-corner-small);
|
||||||
aspect-ratio: 16 / 9;
|
aspect-ratio: 16 / 9;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
|
||||||
& + figcaption {
|
& + figcaption {
|
||||||
border-radius: var(--rounded-corner-small)
|
border-radius: var(--rounded-corner-small)
|
||||||
var(--rounded-corner-small) var(--rounded-corner)
|
var(--rounded-corner-small) var(--rounded-corner)
|
||||||
var(--rounded-corner);
|
var(--rounded-corner);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
figcaption {
|
figcaption {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
transition-duration: var(--transition);
|
transition-duration: var(--transition);
|
||||||
transition-property: border-radius, background-color;
|
transition-property: border-radius, background-color;
|
||||||
box-shadow: var(--edge-highlight), var(--shadow);
|
box-shadow: var(--edge-highlight), var(--shadow);
|
||||||
border-radius: var(--rounded-corner);
|
border-radius: var(--rounded-corner);
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
color: var(--fg-color);
|
color: var(--fg-color);
|
||||||
font-size: var(--font-size-medium);
|
font-size: var(--font-size-medium);
|
||||||
text-align: start;
|
text-align: start;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--fg-muted-2);
|
color: var(--fg-muted-2);
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
grid-area: interactions;
|
grid-area: interactions;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
margin-block-start: 1rem;
|
margin-block-start: 1rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
@media only screen and (max-width: 480px) {
|
@media only screen and (max-width: 480px) {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.boosts,
|
.boosts,
|
||||||
.faves,
|
.faves,
|
||||||
.reaction {
|
.reaction {
|
||||||
padding: 0.375rem 0.75rem;
|
padding: 0.375rem 0.75rem;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.boosts,
|
.boosts,
|
||||||
.faves {
|
.faves {
|
||||||
transition:
|
transition:
|
||||||
box-shadow var(--transition),
|
box-shadow var(--transition),
|
||||||
border-radius var(--transition),
|
border-radius var(--transition),
|
||||||
transform var(--transition-bezier);
|
transform var(--transition-bezier);
|
||||||
border-radius: rpx(18);
|
border-radius: rpx(18);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
vertical-align: rpx(-2);
|
vertical-align: rpx(-2);
|
||||||
transition: transform var(--transition-bezier);
|
transition: transform var(--transition-bezier);
|
||||||
margin-inline-end: 0.25rem;
|
margin-inline-end: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: var(--edge-highlight);
|
box-shadow: var(--edge-highlight);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
transform: var(--active);
|
transform: var(--active);
|
||||||
border-radius: rpx(18);
|
border-radius: rpx(18);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.boosts {
|
.boosts {
|
||||||
border-start-end-radius: var(--rounded-corner-small);
|
border-start-end-radius: var(--rounded-corner-small);
|
||||||
border-end-end-radius: var(--rounded-corner-small);
|
border-end-end-radius: var(--rounded-corner-small);
|
||||||
padding-inline-end: 0.625rem;
|
padding-inline-end: 0.625rem;
|
||||||
color: var(--purple-fg);
|
color: var(--purple-fg);
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
--icon: var(--icon-repeat);
|
--icon: var(--icon-repeat);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--purple-bg-1);
|
background-color: var(--purple-bg-1);
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.faves {
|
.faves {
|
||||||
border-start-start-radius: var(--rounded-corner-small);
|
border-start-start-radius: var(--rounded-corner-small);
|
||||||
border-end-start-radius: var(--rounded-corner-small);
|
border-end-start-radius: var(--rounded-corner-small);
|
||||||
padding-inline-start: 0.625rem;
|
padding-inline-start: 0.625rem;
|
||||||
color: var(--yellow-fg);
|
color: var(--yellow-fg);
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
--icon: var(--icon-star);
|
--icon: var(--icon-star);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--yellow-bg-1);
|
background-color: var(--yellow-bg-1);
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
transform: rotate(72deg);
|
transform: rotate(72deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.reactions {
|
.reactions {
|
||||||
display: flex;
|
display: flex;
|
||||||
// flex-wrap: nowrap;
|
// flex-wrap: nowrap;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
margin: -0.5rem -1rem;
|
margin: -0.5rem -1rem;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
font-weight: var(--font-weight-semi-bold);
|
font-weight: var(--font-weight-semi-bold);
|
||||||
|
|
||||||
.reaction {
|
.reaction {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
cursor: zoom-in;
|
cursor: zoom-in;
|
||||||
border: rpx(1) solid var(--bg-muted-2);
|
border: rpx(1) solid var(--bg-muted-2);
|
||||||
border-radius: rpx(19);
|
border-radius: rpx(19);
|
||||||
|
|
||||||
&:hover .emoji {
|
&:hover .emoji {
|
||||||
transform: scale(2);
|
transform: scale(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji {
|
.emoji {
|
||||||
cursor: unset;
|
cursor: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,17 @@
|
||||||
@use "../citrine/abstracts/mixins" as *;
|
@use "../citrine/abstracts/mixins" as *;
|
||||||
|
|
||||||
.deanchor {
|
.deanchor {
|
||||||
display: inline;
|
display: inline;
|
||||||
font-family: var(--font-family-mono);
|
font-family: var(--font-family-mono);
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
color: var(--fg-muted-2);
|
color: var(--fg-muted-2);
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
padding: 0.125rem 0.375rem;
|
padding: 0.125rem 0.375rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
transition:
|
transition: background-color var(--transition), color var(--transition);
|
||||||
background-color var(--transition),
|
|
||||||
color var(--transition);
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--bg-muted-2);
|
background-color: var(--bg-muted-2);
|
||||||
color: var(--fg-muted-1);
|
color: var(--fg-muted-1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,21 @@
|
||||||
.emoji {
|
.emoji {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
transition: transform var(--transition-bezier);
|
transition: transform var(--transition-bezier);
|
||||||
cursor: zoom-in;
|
cursor: zoom-in;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: scale(2);
|
transform: scale(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.big {
|
&.big {
|
||||||
width: 3rem;
|
width: 3rem;
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,61 +2,61 @@
|
||||||
@use "../citrine/abstracts/mixins" as *;
|
@use "../citrine/abstracts/mixins" as *;
|
||||||
|
|
||||||
.footnotes-list p {
|
.footnotes-list p {
|
||||||
margin-block: 0;
|
margin-block: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footnote-reference {
|
.footnote-reference {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
transition:
|
transition:
|
||||||
background-color var(--transition),
|
background-color var(--transition),
|
||||||
outline-offset calc(var(--transition-long) * 3),
|
outline-offset calc(var(--transition-long) * 3),
|
||||||
transform var(--transition-bezier);
|
transform var(--transition-bezier);
|
||||||
margin-inline-start: 0.25rem;
|
margin-inline-start: 0.25rem;
|
||||||
outline-offset: calc(var(--content-width) / 2) !important;
|
outline-offset: calc(var(--content-width) / 2) !important;
|
||||||
border-radius: 0.6875em;
|
border-radius: 0.6875em;
|
||||||
background-color: var(--fg-muted-1);
|
background-color: var(--fg-muted-1);
|
||||||
// width: 1.375em;
|
// width: 1.375em;
|
||||||
line-height: 1.375em;
|
line-height: 1.375em;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--accent-color);
|
background-color: var(--accent-color);
|
||||||
|
|
||||||
a::after {
|
a::after {
|
||||||
mask-position: center rpx(18);
|
mask-position: center rpx(18);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
transform: var(--active);
|
transform: var(--active);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:target {
|
&:target {
|
||||||
animation: none !important;
|
animation: none !important;
|
||||||
outline-offset: rpx(-3) !important;
|
outline-offset: rpx(-3) !important;
|
||||||
background-color: var(--accent-color);
|
background-color: var(--accent-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
padding-inline: rpx(9) rpx(6);
|
padding-inline: rpx(9) rpx(6);
|
||||||
color: var(--bg-color);
|
color: var(--bg-color);
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
@include icon(pseudo);
|
@include icon(pseudo);
|
||||||
--icon: var(--icon-caret-down);
|
--icon: var(--icon-caret-down);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: rpx(-7);
|
vertical-align: rpx(-7);
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
mask-size: 1em;
|
mask-size: 1em;
|
||||||
transition: mask-position var(--transition);
|
transition: mask-position var(--transition);
|
||||||
margin-inline-start: 0.25rem;
|
margin-inline-start: 0.25rem;
|
||||||
height: rpx(22);
|
height: rpx(22);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,49 +2,47 @@
|
||||||
@use "../citrine/abstracts/mixins" as *;
|
@use "../citrine/abstracts/mixins" as *;
|
||||||
|
|
||||||
.fuse {
|
.fuse {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
max-width: 30rem;
|
max-width: 30rem;
|
||||||
|
|
||||||
.fuse-input {
|
.fuse-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
border: 1px solid var(--bg-muted-2);
|
border: 1px solid var(--bg-muted-2);
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
color: var(--fg-color);
|
color: var(--fg-color);
|
||||||
font-size: var(--font-size-medium);
|
font-size: var(--font-size-medium);
|
||||||
transition:
|
transition: border-color var(--transition), box-shadow var(--transition);
|
||||||
border-color var(--transition),
|
|
||||||
box-shadow var(--transition);
|
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--accent-color);
|
border-color: var(--accent-color);
|
||||||
box-shadow: 0 0 0 2px var(--accent-bg-1), 0 0 0 4px var(--accent-color);
|
box-shadow: 0 0 0 2px var(--accent-bg-1), 0 0 0 4px var(--accent-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fuse-results {
|
.fuse-results {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
|
|
||||||
.fuse-result {
|
.fuse-result {
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--fg-color);
|
color: var(--fg-color);
|
||||||
transition:
|
transition:
|
||||||
background-color var(--transition),
|
background-color var(--transition),
|
||||||
padding-left var(--transition);
|
padding-left var(--transition);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--bg-muted-2);
|
background-color: var(--bg-muted-2);
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
.giallo:has(.giallo-ln) {
|
.giallo:has(.giallo-ln) {
|
||||||
padding-inline: 0;
|
padding-inline: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.giallo-l {
|
.giallo-l {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.giallo-ln {
|
.giallo-ln {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: 1ch;
|
margin-right: 1ch;
|
||||||
min-width: 3ch;
|
min-width: 3ch;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
@use "../citrine/abstracts/mixins" as *;
|
@use "../citrine/abstracts/mixins" as *;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
@include icon;
|
@include icon;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
@include icon(pseudo);
|
@include icon(pseudo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
iframe {
|
iframe {
|
||||||
&.vimeo-embed,
|
&.vimeo-embed,
|
||||||
&.youtube-embed {
|
&.youtube-embed {
|
||||||
aspect-ratio: 16 / 9;
|
aspect-ratio: 16 / 9;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,4 +22,4 @@
|
||||||
@use "audio-button";
|
@use "audio-button";
|
||||||
@use "deanchor";
|
@use "deanchor";
|
||||||
@use "fuse";
|
@use "fuse";
|
||||||
@use "search-fuse";
|
@use "search-fuse";
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,35 @@
|
||||||
.marquee {
|
.marquee {
|
||||||
margin-inline: -1rem;
|
margin-inline: -1rem;
|
||||||
padding-inline: 1rem;
|
padding-inline: 1rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&.overshoot-row > * {
|
&.overshoot-row > * {
|
||||||
animation: marquee-scroll
|
animation: marquee-scroll
|
||||||
calc((var(--text-width) + var(--container-width)) / 40 * 1s) linear
|
calc((var(--text-width) + var(--container-width)) / 40 * 1s) linear
|
||||||
infinite;
|
infinite;
|
||||||
|
|
||||||
@keyframes marquee-scroll {
|
@keyframes marquee-scroll {
|
||||||
0%,
|
0%,
|
||||||
10% {
|
10% {
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
45%,
|
45%,
|
||||||
55% {
|
55% {
|
||||||
transform: translateX(
|
transform: translateX(
|
||||||
calc((var(--container-width) - var(--text-width)) * 1px)
|
calc((var(--container-width) - var(--text-width)) * 1px)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
90%,
|
90%,
|
||||||
100% {
|
100% {
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
picture#not-found {
|
picture#not-found {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 8rem;
|
width: 8rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
img#not-found {
|
img#not-found {
|
||||||
width: 8rem;
|
width: 8rem;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,141 +1,141 @@
|
||||||
@use "../citrine/abstracts/functions" as *;
|
@use "../citrine/abstracts/functions" as *;
|
||||||
|
|
||||||
.pre-container {
|
.pre-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 1rem 0 1rem;
|
margin: 1rem 0 1rem;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
border-radius: var(--rounded-corner);
|
border-radius: var(--rounded-corner);
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
box-shadow: var(--edge-highlight);
|
box-shadow: var(--edge-highlight);
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
content: "";
|
content: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
transition: border-block-end-color var(--transition);
|
transition: border-block-end-color var(--transition);
|
||||||
border-radius: var(--rounded-corner) var(--rounded-corner) 0 0;
|
border-radius: var(--rounded-corner) var(--rounded-corner) 0 0;
|
||||||
border-block-end: rpx(1) solid transparent;
|
border-block-end: rpx(1) solid transparent;
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
to right,
|
to right,
|
||||||
transparent 50%,
|
transparent 50%,
|
||||||
var(--bg-muted-neutral-1) 75%,
|
var(--bg-muted-neutral-1) 75%,
|
||||||
transparent 100%
|
transparent 100%
|
||||||
);
|
);
|
||||||
background-size: 200%;
|
background-size: 200%;
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
|
|
||||||
&:not(.z-l-code),
|
&:not(.z-l-code),
|
||||||
&:not(.z-d-code) {
|
&:not(.z-d-code) {
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
border-block-end-color: var(--bg-muted-1);
|
border-block-end-color: var(--bg-muted-1);
|
||||||
|
|
||||||
button {
|
button {
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
margin-inline-start: 0.75rem;
|
margin-inline-start: 0.75rem;
|
||||||
font-weight: var(--font-weight-semi-bold);
|
font-weight: var(--font-weight-semi-bold);
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
position: relative;
|
position: relative;
|
||||||
transition:
|
transition:
|
||||||
box-shadow var(--transition),
|
box-shadow var(--transition),
|
||||||
background-color var(--transition),
|
background-color var(--transition),
|
||||||
transform var(--transition-bezier);
|
transform var(--transition-bezier);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border-radius: var(--rounded-corner-small);
|
border-radius: var(--rounded-corner-small);
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: var(--edge-highlight);
|
box-shadow: var(--edge-highlight);
|
||||||
background-color: var(--bg-muted-neutral-1);
|
background-color: var(--bg-muted-neutral-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
transform: var(--active);
|
transform: var(--active);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
transition:
|
transition:
|
||||||
opacity var(--transition-longer),
|
opacity var(--transition-longer),
|
||||||
transform var(--transition-bezier-longer);
|
transform var(--transition-bezier-longer);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
|
||||||
&.copy {
|
&.copy {
|
||||||
--icon: var(--icon-copy);
|
--icon: var(--icon-copy);
|
||||||
transform-origin: bottom left;
|
transform-origin: bottom left;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.check {
|
&.check {
|
||||||
--icon: var(--icon-check-square-offset);
|
--icon: var(--icon-check-square-offset);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transform: scale(0.5);
|
transform: scale(0.5);
|
||||||
transform-origin: top right;
|
transform-origin: top right;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
inset: 0.5rem;
|
inset: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
animation: active-shimmer var(--transition-long);
|
animation: active-shimmer var(--transition-long);
|
||||||
border-block-end-color: var(--bg-muted-neutral-1);
|
border-block-end-color: var(--bg-muted-neutral-1);
|
||||||
|
|
||||||
button {
|
button {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
||||||
.icon.copy {
|
.icon.copy {
|
||||||
transform: scale(0.5);
|
transform: scale(0.5);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.check {
|
.icon.check {
|
||||||
transform: none;
|
transform: none;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes active-shimmer {
|
@keyframes active-shimmer {
|
||||||
to {
|
to {
|
||||||
background-position-x: -200%;
|
background-position-x: -200%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border-radius: 0 0 var(--rounded-corner) var(--rounded-corner);
|
border-radius: 0 0 var(--rounded-corner) var(--rounded-corner);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,72 +2,70 @@
|
||||||
@use "../citrine/abstracts/mixins" as *;
|
@use "../citrine/abstracts/mixins" as *;
|
||||||
|
|
||||||
.search-fuse {
|
.search-fuse {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
max-width: 40rem;
|
max-width: 40rem;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
&-input-wrap {
|
&-input-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-icon {
|
&-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0.75rem;
|
left: 0.75rem;
|
||||||
color: var(--fg-muted-2);
|
color: var(--fg-muted-2);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transition:
|
transition: color var(--transition), opacity var(--transition-bezier);
|
||||||
color var(--transition),
|
opacity: 0.7;
|
||||||
opacity var(--transition-bezier);
|
}
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-input {
|
&-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.75rem 1rem 0.75rem 2.5rem;
|
padding: 0.75rem 1rem 0.75rem 2.5rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
border: 1px solid var(--bg-muted-2);
|
border: 1px solid var(--bg-muted-2);
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
color: var(--fg-color);
|
color: var(--fg-color);
|
||||||
font-size: var(--font-size-medium);
|
font-size: var(--font-size-medium);
|
||||||
transition:
|
transition:
|
||||||
border-color var(--transition),
|
border-color var(--transition),
|
||||||
box-shadow var(--transition),
|
box-shadow var(--transition),
|
||||||
background-color var(--transition);
|
background-color var(--transition);
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--accent-color);
|
border-color: var(--accent-color);
|
||||||
box-shadow: 0 0 0 2px var(--accent-bg-1), 0 0 0 4px var(--accent-color);
|
box-shadow: 0 0 0 2px var(--accent-bg-1), 0 0 0 4px var(--accent-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: var(--fg-muted-2);
|
color: var(--fg-muted-2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-results {
|
&-results {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
|
|
||||||
.search-fuse-result {
|
.search-fuse-result {
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--fg-color);
|
color: var(--fg-color);
|
||||||
transition:
|
transition:
|
||||||
background-color var(--transition),
|
background-color var(--transition),
|
||||||
padding-left var(--transition);
|
padding-left var(--transition);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--bg-muted-2);
|
background-color: var(--bg-muted-2);
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,128 +1,124 @@
|
||||||
#search-modal {
|
#search-modal {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
transition:
|
transition: visibility var(--transition), opacity var(--transition);
|
||||||
visibility var(--transition),
|
inset: 0;
|
||||||
opacity var(--transition);
|
background-color: var(--shade-color);
|
||||||
inset: 0;
|
padding-block-start: 15vh;
|
||||||
background-color: var(--shade-color);
|
width: 100%;
|
||||||
padding-block-start: 15vh;
|
height: 100vh;
|
||||||
width: 100%;
|
|
||||||
height: 100vh;
|
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
||||||
#search-modal-content {
|
#search-modal-content {
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-content {
|
&-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
transform: var(--hover);
|
transform: var(--hover);
|
||||||
backdrop-filter: var(--blur);
|
backdrop-filter: var(--blur);
|
||||||
-webkit-backdrop-filter: var(--blur);
|
-webkit-backdrop-filter: var(--blur);
|
||||||
transition:
|
transition:
|
||||||
border-radius var(--transition),
|
border-radius var(--transition),
|
||||||
padding var(--transition),
|
padding var(--transition),
|
||||||
max-width var(--transition),
|
max-width var(--transition),
|
||||||
transform var(--transition-bezier-longer);
|
transform var(--transition-bezier-longer);
|
||||||
margin: -0.5rem;
|
margin: -0.5rem;
|
||||||
box-shadow: var(--edge-highlight), var(--shadow-glass);
|
box-shadow: var(--edge-highlight), var(--shadow-glass);
|
||||||
border-radius: 2.25rem 2.25rem 1.75rem 1.75rem;
|
border-radius: 2.25rem 2.25rem 1.75rem 1.75rem;
|
||||||
background-color: var(--glass-bg-1);
|
background-color: var(--glass-bg-1);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
width: calc(100% - 2rem);
|
width: calc(100% - 2rem);
|
||||||
max-width: calc(var(--content-width) / 1.5);
|
max-width: calc(var(--content-width) / 1.5);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&:has(#search-results:empty) {
|
&:has(#search-results:empty) {
|
||||||
border-radius: 1.75rem;
|
border-radius: 1.75rem;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
max-width: calc(var(--content-width) / 2);
|
max-width: calc(var(--content-width) / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#search-input {
|
#search-input {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
box-shadow: var(--edge-highlight);
|
box-shadow: var(--edge-highlight);
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
|
|
||||||
&:placeholder-shown + div .icon {
|
&:placeholder-shown + div .icon {
|
||||||
transform: none;
|
transform: none;
|
||||||
opacity: var(--disabled-opacity);
|
opacity: var(--disabled-opacity);
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
& + div .icon {
|
& + div .icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transform: translateX(0.5rem);
|
transform: translateX(0.5rem);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition:
|
transition: opacity var(--transition), transform var(--transition-bezier);
|
||||||
opacity var(--transition),
|
cursor: help;
|
||||||
transform var(--transition-bezier);
|
inset-block-start: 0.625rem;
|
||||||
cursor: help;
|
inset-inline-end: 1rem;
|
||||||
inset-block-start: 0.625rem;
|
pointer-events: none;
|
||||||
inset-inline-end: 1rem;
|
color: var(--fg-muted-2);
|
||||||
pointer-events: none;
|
font-size: 1.25rem;
|
||||||
color: var(--fg-muted-2);
|
}
|
||||||
font-size: 1.25rem;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#search-results {
|
#search-results {
|
||||||
display: none;
|
display: none;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin: -1rem;
|
margin: -1rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
box-shadow: var(--edge-highlight);
|
box-shadow: var(--edge-highlight);
|
||||||
border-radius: var(--rounded-corner);
|
border-radius: var(--rounded-corner);
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: " →";
|
content: " →";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
margin-block-start: 0.25rem;
|
margin-block-start: 0.25rem;
|
||||||
border-block-start: rpx(1) solid var(--bg-muted-2);
|
border-block-start: rpx(1) solid var(--bg-muted-2);
|
||||||
padding-block-start: 0.25rem;
|
padding-block-start: 0.25rem;
|
||||||
color: var(--fg-muted-2);
|
color: var(--fg-muted-2);
|
||||||
|
|
||||||
strong {
|
strong {
|
||||||
color: var(--fg-color);
|
color: var(--fg-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,23 @@
|
||||||
#skip-to-content {
|
#skip-to-content {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transform: translateX(-50%) translateY(calc(-100% - 1rem));
|
transform: translateX(-50%) translateY(calc(-100% - 1rem));
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
transition:
|
transition:
|
||||||
backdrop-filter var(--transition),
|
backdrop-filter var(--transition),
|
||||||
box-shadow var(--transition),
|
box-shadow var(--transition),
|
||||||
transform var(--transition-bezier);
|
transform var(--transition-bezier);
|
||||||
inset-inline-start: 50vw;
|
inset-inline-start: 50vw;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
background-color: var(--glass-bg-1);
|
background-color: var(--glass-bg-1);
|
||||||
padding: 0.375rem 0.75rem;
|
padding: 0.375rem 0.75rem;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&:focus-visible,
|
&:focus-visible,
|
||||||
&:focus {
|
&:focus {
|
||||||
-webkit-backdrop-filter: var(--blur);
|
-webkit-backdrop-filter: var(--blur);
|
||||||
transform: translateX(-50%) translateY(1rem);
|
transform: translateX(-50%) translateY(1rem);
|
||||||
backdrop-filter: var(--blur);
|
backdrop-filter: var(--blur);
|
||||||
box-shadow: var(--edge-highlight), var(--shadow);
|
box-shadow: var(--edge-highlight), var(--shadow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,61 +1,61 @@
|
||||||
@use "../citrine/abstracts/functions" as *;
|
@use "../citrine/abstracts/functions" as *;
|
||||||
|
|
||||||
.statement-container {
|
.statement-container {
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
box-shadow: var(--edge-highlight), var(--shadow);
|
box-shadow: var(--edge-highlight), var(--shadow);
|
||||||
border-radius: var(--rounded-corner);
|
border-radius: var(--rounded-corner);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
|
||||||
:last-child {
|
:last-child {
|
||||||
margin-block-end: 0;
|
margin-block-end: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > :nth-child(2) {
|
& > :nth-child(2) {
|
||||||
margin-block-start: 0.5rem;
|
margin-block-start: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-weight: var(--font-weight-semi-bold);
|
font-weight: var(--font-weight-semi-bold);
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
vertical-align: rpx(-2);
|
vertical-align: rpx(-2);
|
||||||
margin-inline-end: 0.25rem;
|
margin-inline-end: 0.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.archive {
|
&.archive {
|
||||||
--accent-color: var(--purple-fg);
|
--accent-color: var(--purple-fg);
|
||||||
--accent-highlight: hsla(
|
--accent-highlight: hsla(
|
||||||
var(--purple-h),
|
var(--purple-h),
|
||||||
var(--purple-s),
|
var(--purple-s),
|
||||||
var(--purple-l),
|
var(--purple-l),
|
||||||
var(--disabled-opacity)
|
var(--disabled-opacity)
|
||||||
);
|
);
|
||||||
background-color: var(--purple-bg-1);
|
background-color: var(--purple-bg-1);
|
||||||
color: var(--purple-fg);
|
color: var(--purple-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.disclaimer {
|
&.disclaimer {
|
||||||
--accent-color: var(--red-fg);
|
--accent-color: var(--red-fg);
|
||||||
--accent-highlight: hsla(
|
--accent-highlight: hsla(
|
||||||
var(--red-h),
|
var(--red-h),
|
||||||
var(--red-s),
|
var(--red-s),
|
||||||
var(--red-l),
|
var(--red-l),
|
||||||
var(--disabled-opacity)
|
var(--disabled-opacity)
|
||||||
);
|
);
|
||||||
background-color: var(--red-bg-1);
|
background-color: var(--red-bg-1);
|
||||||
color: var(--red-fg);
|
color: var(--red-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.trigger {
|
&.trigger {
|
||||||
--accent-color: var(--yellow-fg);
|
--accent-color: var(--yellow-fg);
|
||||||
--accent-highlight: hsla(
|
--accent-highlight: hsla(
|
||||||
var(--yellow-h),
|
var(--yellow-h),
|
||||||
var(--yellow-s),
|
var(--yellow-s),
|
||||||
var(--yellow-l),
|
var(--yellow-l),
|
||||||
var(--disabled-opacity)
|
var(--disabled-opacity)
|
||||||
);
|
);
|
||||||
background-color: var(--yellow-bg-1);
|
background-color: var(--yellow-bg-1);
|
||||||
color: var(--yellow-fg);
|
color: var(--yellow-fg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,59 +1,59 @@
|
||||||
@use "../citrine/abstracts/functions" as *;
|
@use "../citrine/abstracts/functions" as *;
|
||||||
|
|
||||||
#tag-chips {
|
#tag-chips {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-inline-start: 0;
|
padding-inline-start: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
|
||||||
li {
|
li {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-block: 0;
|
margin-block: 0;
|
||||||
padding-inline-start: 0;
|
padding-inline-start: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
display: flex;
|
display: flex;
|
||||||
transition:
|
transition:
|
||||||
background-color var(--transition),
|
background-color var(--transition),
|
||||||
color var(--transition),
|
color var(--transition),
|
||||||
transform var(--transition-bezier);
|
transform var(--transition-bezier);
|
||||||
box-shadow: var(--edge-highlight);
|
box-shadow: var(--edge-highlight);
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
background-color: var(--bg-muted-1);
|
background-color: var(--bg-muted-1);
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
padding-inline-start: 0.75rem;
|
padding-inline-start: 0.75rem;
|
||||||
color: var(--fg-muted-1);
|
color: var(--fg-muted-1);
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--bg-muted-2);
|
background-color: var(--bg-muted-2);
|
||||||
color: var(--fg-muted-2);
|
color: var(--fg-muted-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
transform: var(--active);
|
transform: var(--active);
|
||||||
}
|
}
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: "#";
|
content: "#";
|
||||||
}
|
}
|
||||||
|
|
||||||
.counter {
|
.counter {
|
||||||
transition: inherit;
|
transition: inherit;
|
||||||
margin-inline-start: 0.375rem;
|
margin-inline-start: 0.375rem;
|
||||||
box-shadow: var(--edge-highlight);
|
box-shadow: var(--edge-highlight);
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
aspect-ratio: 1 / 1;
|
aspect-ratio: 1 / 1;
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,47 +4,47 @@ h3,
|
||||||
h4,
|
h4,
|
||||||
h5,
|
h5,
|
||||||
h6 {
|
h6 {
|
||||||
&:hover .zola-anchor {
|
&:hover .zola-anchor {
|
||||||
transform: none;
|
transform: none;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:has(.zola-anchor) {
|
&:has(.zola-anchor) {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.zola-anchor {
|
.zola-anchor {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transform: translateX(0.25em);
|
transform: translateX(0.25em);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition:
|
transition:
|
||||||
color var(--transition),
|
color var(--transition),
|
||||||
opacity var(--transition),
|
opacity var(--transition),
|
||||||
transform var(--transition-bezier);
|
transform var(--transition-bezier);
|
||||||
inset-inline-start: -1.25em;
|
inset-inline-start: -1.25em;
|
||||||
padding-inline-end: 0.25em;
|
padding-inline-end: 0.25em;
|
||||||
color: var(--fg-muted-1);
|
color: var(--fg-muted-1);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|
||||||
@media only screen and (max-width: 720px) {
|
@media only screen and (max-width: 720px) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--fg-muted-2);
|
color: var(--fg-muted-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
transform: var(--active);
|
transform: var(--active);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:target .zola-anchor {
|
:target .zola-anchor {
|
||||||
transform: none;
|
transform: none;
|
||||||
opacity: var(--disabled-opacity);
|
opacity: var(--disabled-opacity);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,55 +1,55 @@
|
||||||
// Based on https://github.com/welpo/tabi/blob/68c35b02f8c09e21ecff48f61bad6da3d96ce337/sass/parts/_zola-error.scss
|
// 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])) {
|
body > div:last-child:has(div:last-child[style]:not([class]):not([id])) {
|
||||||
z-index: 9999 !important;
|
z-index: 9999 !important;
|
||||||
background-color: var(--shade-color) !important;
|
background-color: var(--shade-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body > div:last-child > div:last-child[style]:not([class]):not([id]) {
|
body > div:last-child > div:last-child[style]:not([class]):not([id]) {
|
||||||
box-shadow: var(--edge-highlight), var(--shadow-glass);
|
box-shadow: var(--edge-highlight), var(--shadow-glass);
|
||||||
border-radius: calc(var(--rounded-corner) + 1rem) !important;
|
border-radius: calc(var(--rounded-corner) + 1rem) !important;
|
||||||
background-color: hsla(
|
background-color: hsla(
|
||||||
var(--red-h),
|
var(--red-h),
|
||||||
calc(var(--red-s) - var(--red-s) * 0.7),
|
calc(var(--red-s) - var(--red-s) * 0.7),
|
||||||
calc(var(--red-l) + (100% - var(--red-l)) * 0.9),
|
calc(var(--red-l) + (100% - var(--red-l)) * 0.9),
|
||||||
var(--dim-opacity)
|
var(--dim-opacity)
|
||||||
) !important;
|
) !important;
|
||||||
padding: 1rem !important;
|
padding: 1rem !important;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
max-width: calc(100% - 2rem);
|
max-width: calc(100% - 2rem);
|
||||||
-webkit-backdrop-filter: var(--blur);
|
-webkit-backdrop-filter: var(--blur);
|
||||||
backdrop-filter: var(--blur);
|
backdrop-filter: var(--blur);
|
||||||
filter: none !important;
|
filter: none !important;
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
& {
|
& {
|
||||||
background-color: hsla(
|
background-color: hsla(
|
||||||
var(--red-h),
|
var(--red-h),
|
||||||
calc(var(--red-s) - var(--red-s) * 0.6),
|
calc(var(--red-s) - var(--red-s) * 0.6),
|
||||||
calc(var(--red-l) - var(--red-l) * 0.8),
|
calc(var(--red-l) - var(--red-l) * 0.8),
|
||||||
var(--dim-opacity)
|
var(--dim-opacity)
|
||||||
) !important;
|
) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body
|
body
|
||||||
> div:last-child
|
> div:last-child
|
||||||
> div:last-child[style]:not([class]):not([id])
|
> div:last-child[style]:not([class]):not([id])
|
||||||
> p[style]:first-child {
|
> p[style]:first-child {
|
||||||
margin-block-end: 1rem !important;
|
margin-block-end: 1rem !important;
|
||||||
color: var(--red-fg) !important;
|
color: var(--red-fg) !important;
|
||||||
font-weight: var(--font-weight-semi-bold) !important;
|
font-weight: var(--font-weight-semi-bold) !important;
|
||||||
font-size: var(--font-size-large) !important;
|
font-size: var(--font-size-large) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body
|
body
|
||||||
> div:last-child
|
> div:last-child
|
||||||
> div:last-child[style]:not([class]):not([id])
|
> div:last-child[style]:not([class]):not([id])
|
||||||
> pre[style]:last-child {
|
> pre[style]:last-child {
|
||||||
border-radius: var(--rounded-corner) !important;
|
border-radius: var(--rounded-corner) !important;
|
||||||
background-color: var(--red-bg-1) !important;
|
background-color: var(--red-bg-1) !important;
|
||||||
padding: 0.75rem 1rem !important;
|
padding: 0.75rem 1rem !important;
|
||||||
color: var(--red-fg) !important;
|
color: var(--red-fg) !important;
|
||||||
font-weight: var(--font-weight-normal) !important;
|
font-weight: var(--font-weight-normal) !important;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,51 +2,51 @@ $commissioner-version: "1.012.1";
|
||||||
$commit-mono-version: "1.143";
|
$commit-mono-version: "1.143";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--font-sans-serif: "Commissioner";
|
--font-sans-serif: "Commissioner";
|
||||||
--font-monospace: "Commit Mono";
|
--font-monospace: "Commit Mono";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commissioner
|
// Commissioner
|
||||||
@font-face {
|
@font-face {
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 100 900;
|
font-weight: 100 900;
|
||||||
src: url("/fonts/commissioner.woff2?v=#{$commissioner-version}")
|
src: url("/fonts/commissioner.woff2?v=#{$commissioner-version}")
|
||||||
format("woff2");
|
format("woff2");
|
||||||
font-family: "Commissioner";
|
font-family: "Commissioner";
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
// Fix vertical alignment,
|
// Fix vertical alignment,
|
||||||
// See https://github.com/kosbarts/Commissioner/issues/55
|
// See https://github.com/kosbarts/Commissioner/issues/55
|
||||||
ascent-override: 96.3%;
|
ascent-override: 96.3%;
|
||||||
descent-override: 25%;
|
descent-override: 25%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 100 900;
|
font-weight: 100 900;
|
||||||
src: url("/fonts/commissioner.woff2?v=#{$commissioner-version}")
|
src: url("/fonts/commissioner.woff2?v=#{$commissioner-version}")
|
||||||
format("woff2");
|
format("woff2");
|
||||||
font-family: "Commissioner";
|
font-family: "Commissioner";
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-variation-settings: "slnt" -12;
|
font-variation-settings: "slnt" -12;
|
||||||
// Ditto
|
// Ditto
|
||||||
ascent-override: 96.3%;
|
ascent-override: 96.3%;
|
||||||
descent-override: 25%;
|
descent-override: 25%;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit Mono
|
// Commit Mono
|
||||||
@font-face {
|
@font-face {
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 100 900;
|
font-weight: 100 900;
|
||||||
src: url("/fonts/commit-mono.woff2?v=#{$commit-mono-version}") format("woff2");
|
src: url("/fonts/commit-mono.woff2?v=#{$commit-mono-version}") format("woff2");
|
||||||
font-family: "Commit Mono";
|
font-family: "Commit Mono";
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 100 900;
|
font-weight: 100 900;
|
||||||
src: url("/fonts/commit-mono.woff2?v=#{$commit-mono-version}") format("woff2");
|
src: url("/fonts/commit-mono.woff2?v=#{$commit-mono-version}") format("woff2");
|
||||||
font-family: "Commit Mono";
|
font-family: "Commit Mono";
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-variation-settings: "ital" 1;
|
font-variation-settings: "ital" 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,139 +2,137 @@
|
||||||
@use "../citrine/abstracts/mixins" as *;
|
@use "../citrine/abstracts/mixins" as *;
|
||||||
|
|
||||||
#site-footer {
|
#site-footer {
|
||||||
--padding: 1rem;
|
--padding: 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
grid-area: footer;
|
grid-area: footer;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-block: 3.25rem 1rem;
|
margin-block: 3.25rem 1rem;
|
||||||
border-block-start: rpx(1) solid var(--bg-muted-2);
|
border-block-start: rpx(1) solid var(--bg-muted-2);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
max-width: calc(var(--content-width) + var(--padding) * 2);
|
max-width: calc(var(--content-width) + var(--padding) * 2);
|
||||||
|
|
||||||
@media only screen and (max-width: 480px) {
|
@media only screen and (max-width: 480px) {
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:nth-of-type(1) {
|
&:nth-of-type(1) {
|
||||||
color: var(--fg-muted-2);
|
color: var(--fg-muted-2);
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:nth-of-type(2) {
|
&:nth-of-type(2) {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
text-align: end;
|
text-align: end;
|
||||||
|
|
||||||
@media only screen and (max-width: 480px) {
|
@media only screen and (max-width: 480px) {
|
||||||
align-items: normal;
|
align-items: normal;
|
||||||
text-align: start;
|
text-align: start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a.external::after {
|
a.external::after {
|
||||||
margin-inline-start: 0.25rem;
|
margin-inline-start: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
@include chip;
|
@include chip;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--bg-muted-2);
|
background-color: var(--bg-muted-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
transform: var(--active);
|
transform: var(--active);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IMO it's pretty cute
|
// IMO it's pretty cute
|
||||||
.webrings {
|
.webrings {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
margin-block: 0;
|
margin-block: 0;
|
||||||
padding-inline-start: 0;
|
padding-inline-start: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.webring {
|
.webring {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
margin-block: 0;
|
margin-block: 0;
|
||||||
padding-inline-start: 0;
|
padding-inline-start: 0;
|
||||||
|
|
||||||
.random,
|
.random,
|
||||||
.previous,
|
.previous,
|
||||||
.next {
|
.next {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.timestamp {
|
.timestamp {
|
||||||
color: var(--fg-muted-1);
|
color: var(--fg-muted-1);
|
||||||
font-size: var(--font-size-xx-small);
|
font-size: var(--font-size-xx-small);
|
||||||
font-family: var(--font-monospace);
|
font-family: var(--font-monospace);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.socials {
|
.socials {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-inline-start: 0;
|
padding-inline-start: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
|
||||||
@media only screen and (max-width: 480px) {
|
@media only screen and (max-width: 480px) {
|
||||||
justify-content: normal;
|
justify-content: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
margin-block: 0;
|
margin-block: 0;
|
||||||
padding-inline-start: 0;
|
padding-inline-start: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
display: flex;
|
display: flex;
|
||||||
transition:
|
transition: color var(--transition), transform var(--transition-bezier);
|
||||||
color var(--transition),
|
height: fit-content;
|
||||||
transform var(--transition-bezier);
|
color: var(--fg-muted-1);
|
||||||
height: fit-content;
|
|
||||||
color: var(--fg-muted-1);
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--fg-muted-2);
|
color: var(--fg-muted-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
transform: var(--active);
|
transform: var(--active);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
.full-bleed {
|
.full-bleed {
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
margin-inline-start: 50%;
|
margin-inline-start: 50%;
|
||||||
width: calc(100vw - var(--sidebar-width));
|
width: calc(100vw - var(--sidebar-width));
|
||||||
|
|
||||||
:root[dir*="rtl"] & {
|
:root[dir*="rtl"] & {
|
||||||
transform: translateX(50%);
|
transform: translateX(50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 960px) {
|
@media only screen and (max-width: 960px) {
|
||||||
width: calc(100vw - 3.5rem);
|
width: calc(100vw - 3.5rem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
body {
|
body {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto auto 1fr auto;
|
grid-template-rows: auto auto 1fr auto;
|
||||||
grid-template-columns: var(--sidebar-width) 1fr;
|
grid-template-columns: var(--sidebar-width) 1fr;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"sidebar header"
|
"sidebar header"
|
||||||
"sidebar banner"
|
"sidebar banner"
|
||||||
"sidebar main "
|
"sidebar main "
|
||||||
"sidebar footer";
|
"sidebar footer";
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
transition: grid-template-columns var(--transition);
|
transition: grid-template-columns var(--transition);
|
||||||
|
|
||||||
@media only screen and (max-width: 960px) {
|
@media only screen and (max-width: 960px) {
|
||||||
grid-template-columns: 0 1fr;
|
grid-template-columns: 0 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue