diff --git a/app/build/html.ts b/app/build/html.ts
index bea1567..fc307a4 100644
--- a/app/build/html.ts
+++ b/app/build/html.ts
@@ -108,7 +108,7 @@ export async function html_builder() {
const html = await Deno.readTextFile(new URL("app/mod.html", root))
const document = new DOMParser().parseFromString(html, "text/html")!
document.querySelector('header nav menu a[href="/build"]')?.parentElement?.remove()
- for (const selection of ["body > aside", "body > script", " main > section:not(.matcha)", "section.matcha section"]) {
+ for (const selection of ["body > aside", " main > section:not(.matcha)", "section.matcha section"]) {
document.querySelectorAll(selection).forEach((element) => (element as unknown as HTMLElement).remove())
}
// Include uncollapsed builder
@@ -125,3 +125,61 @@ export async function html_builder() {
})
return `${document.documentElement!.outerHTML}`
}
+
+/** Generate HTML for custom builder demo */
+export async function html_builder_demo() {
+ // Include mod.html files and clean it
+ let html = await Deno.readTextFile(new URL("app/mod.html", root))
+ for (const _ of [1, 2]) {
+ for (const match of html.matchAll(//g)) {
+ const path = match.groups!.path.trim()
+ const content = await Deno.readTextFile(new URL(path, root))
+ html = html.replace(match[0], content)
+ }
+ }
+ const document = new DOMParser().parseFromString(html, "text/html")!
+ document.querySelector('header nav menu a[href="/build"]')?.parentElement?.remove()
+ document.querySelector('header nav menu a[href="/"]')?.parentElement?.remove()
+ document.querySelector('link[rel="stylesheet"][href="/matcha.css"]')?.remove()
+ for (
+ const selection of [
+ "body > aside",
+ "body > header",
+ "body > footer",
+ "body > script",
+ "section.matcha",
+ '[id="nav"] ~ p',
+ '[id="utilities"] ~ :is(p, div)',
+ '[id="utilities-colors"] ~ :is(p, div)',
+ '[id="syntax-highlighting"] ~ p',
+ ]
+ ) {
+ document.querySelectorAll(selection).forEach((element) => (element as unknown as HTMLElement).remove())
+ }
+ for (const id of ["html", "layouts", "utilities-classes", "utilities-synergies", "code-editor", "istanbul-coverage", "unstyled"]) {
+ document.querySelector(`[id="${id}"]`)?.parentElement?.remove()
+ }
+ document.querySelectorAll(".example").forEach((_element) => {
+ const element = _element as unknown as HTMLElement
+ Array.from(element.parentElement?.children ?? []).forEach((element) => {
+ if (element.classList.contains("example")) {
+ return
+ }
+ if (/^H[1-6]$/.test(element.tagName)) {
+ return
+ }
+ element.remove()
+ })
+ })
+ // Clean background image
+ const style = document.createElement("style")
+ style.innerText = `body { background-image: none; }`
+ document.head.append(style)
+ // Syntax highlighting
+ Array.from(document.querySelectorAll("[data-hl]")).forEach((_element) => {
+ const element = _element as unknown as HTMLElement
+ element.innerHTML = syntax.highlight(element.innerText, { language: element.getAttribute("data-hl")! }).value.trim()
+ element.removeAttribute("data-hl")
+ })
+ return `${document.documentElement!.outerHTML}`
+}
diff --git a/app/build/ssg.ts b/app/build/ssg.ts
index 561a1d1..6c38e29 100644
--- a/app/build/ssg.ts
+++ b/app/build/ssg.ts
@@ -2,7 +2,7 @@
import { copy, emptyDir, ensureDir, expandGlob } from "jsr:@std/fs@0.229.1"
import { dirname, fromFileUrl } from "jsr:@std/path@0.225.1"
import { root } from "./root.ts"
-import { html, html_builder } from "./html.ts"
+import { html, html_builder, html_builder_demo } from "./html.ts"
import { compatibility } from "jsr:@libs/bundle@5/css"
/** Static site generation */
@@ -34,6 +34,9 @@ export async function ssg() {
console.log("Created .pages/index.html")
await Deno.writeTextFile(new URL(".pages/build.html", root), await html_builder())
console.log("Created .pages/build.html")
+ await ensureDir(new URL(".pages/build", root))
+ await Deno.writeTextFile(new URL(".pages/build/demo.html", root), await html_builder_demo())
+ console.log("Created .pages/build/demo.html")
// Copy styles
await ensureDir(new URL(".pages/styles", root))
console.log("Created .pages/styles")
diff --git a/app/mod.css b/app/mod.css
index a24d944..743b278 100644
--- a/app/mod.css
+++ b/app/mod.css
@@ -46,6 +46,15 @@ aside nav ul ul ul li :is(a, [class^="hljs-"], var) {
color: var(--muted) !important;
}
+:is(section) > :is(h1, h2, h3, h4, h5, h6)[id]::before, .hx[id]::before, #matcha::before {
+ content: "";
+ display: block;
+ height: calc(1rem + var(--ly-header-size));
+ margin-top: calc(-1 * (1rem + var(--ly-header-size)));
+ visibility: hidden;
+ pointer-events: none;
+}
+
/* Matcha header and footer */
body > header svg,
body > footer svg {
@@ -92,19 +101,32 @@ details summary a {
.matcha-build .variables input[name$="opacity"] {
margin-left: .25rem;
- max-width: 4rem;
+ width: 3rem;
+ text-align: center;
}
.matcha-build .variables td code:only-child {
white-space: nowrap;
}
+.matcha-build .variables input[type="color"] {
+ width: 3rem;
+}
+
.matcha-build .styling {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
+.matcha-build .styling label code {
+ white-space: nowrap;
+}
+
+.matcha-build .styling label small {
+ opacity: .75;
+}
+
.matcha-build .styling > div {
flex: 1 1 0;
}
@@ -118,13 +140,8 @@ details summary a {
word-break: keep-all;
}
-:is(section) > :is(h1, h2, h3, h4, h5, h6)[id]::before, .hx[id]::before, #matcha::before {
- content: "";
- display: block;
- height: calc(1rem + var(--ly-header-size));
- margin-top: calc(-1 * (1rem + var(--ly-header-size)));
- visibility: hidden;
- pointer-events: none;
+.matcha-preview {
+ background: var(--bg-subtle);
}
/* CSS compatibility */
diff --git a/app/mod.html b/app/mod.html
index 6baf1ce..5cf07c4 100644
--- a/app/mod.html
+++ b/app/mod.html
@@ -129,8 +129,8 @@
-
+
@@ -357,7 +357,10 @@
document.querySelectorAll(".example-tabs li.color-scheme").forEach(element => {
element.querySelector(`svg.${prefers}`).style.display = "inline-block"
element.addEventListener("click", event => {
- const example = element.parentNode.nextSibling
+ const example = element.parentNode.nextElementSibling
+ if (example.tagName === "IFRAME") {
+ example.contentWindow.document.documentElement.dataset.colorScheme = (example.contentWindow.document.documentElement.dataset.colorScheme ?? prefers) === 'light' ? 'dark' : 'light'
+ }
example.dataset.colorScheme = (example.dataset.colorScheme ?? prefers) === 'light' ? 'dark' : 'light'
element.querySelector("svg.light").style.display = example.dataset.colorScheme === 'light' ? "inline-block" : "none"
element.querySelector("svg.dark").style.display = example.dataset.colorScheme === 'dark' ? "inline-block" : "none"
diff --git a/app/mod.ts b/app/mod.ts
index f7a2d53..772d5a0 100644
--- a/app/mod.ts
+++ b/app/mod.ts
@@ -1,7 +1,7 @@
///
// Imports
import { css } from "./build/css.ts"
-import { html, html_builder } from "./build/html.ts"
+import { html, html_builder, html_builder_demo } from "./build/html.ts"
import { ssg } from "./build/ssg.ts"
import { dist } from "./build/dist.ts"
import { STATUS_CODE, STATUS_TEXT } from "jsr:@std/http@0.224.1"
@@ -22,6 +22,8 @@ switch (Deno.args[0]) {
return new Response(await html_builder(), { headers: { "Content-Type": "text/html" } })
case new URLPattern("/matcha.css", url.origin).test(url.href):
return new Response(await css(), { headers: { "Content-Type": "text/css" } })
+ case new URLPattern("/build/demo{.html}?", url.origin).test(url.href):
+ return new Response(await html_builder_demo(), { headers: { "Content-Type": "text/html" } })
case new URLPattern("/mod.css", url.origin).test(url.href):
return new Response(await Deno.readFile(new URL("mod.css", import.meta.url)), { headers: { "Content-Type": "text/css" } })
case new URLPattern("/mod.svg", url.origin).test(url.href):
diff --git a/app/sections/custom-build.html b/app/sections/custom-build.html
index 6d517f5..cd37594 100644
--- a/app/sections/custom-build.html
+++ b/app/sections/custom-build.html
@@ -18,28 +18,76 @@
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
+
+ Customize CSS variables
+
+
+
+
+
+
@@ -280,6 +455,7 @@
form.querySelectorAll(`input[name="${input.dataset.for}"]`).forEach(child => child.checked = input.checked)
})
})
+ // Manage checkboxes
form.querySelectorAll('input[type="checkbox"]').forEach(input => {
if (input.name) {
input.addEventListener("change", () => {
@@ -288,59 +464,81 @@
})
}
})
- // Manage colors
- const variables = {
- light:form.querySelector('[data-color-scheme="light"]'),
- dark:form.querySelector('[data-color-scheme="dark"]'),
- }
- function reset() {
- form.querySelectorAll('.variables input[type="color"]').forEach(input => {
- if (!input.name)
+ // Manage variables
+ function reset(element) {
+ element.querySelectorAll("input[name]").forEach(input => {
+ if ((!input.name)||(input.name.endsWith("@opacity")))
return
- const value = getComputedStyle(variables[input.dataset.mode]).getPropertyValue(input.name)
+ let value = getComputedStyle(element).getPropertyValue(input.name)
+ if ((input.type === "color")&&(/^#[a-f0-9]{8}$/i.test(value))) {
+ const [color, opacity] = [value.slice(0, 7), value.slice(7)]
+ value = color
+ element.querySelector(`input[name="${input.name}@opacity"]`).value = opacity
+ element.querySelector(`input[name="${input.name}@opacity"]`).dataset.default = opacity
+ }
input.value = value
input.dataset.default = value
})
}
- form.querySelector('.variables button').addEventListener("click", event => {
+ form.querySelectorAll('.variables button[type="reset"]').forEach(button => button.addEventListener("click", event => {
event.preventDefault()
- reset()
+ reset(button.closest(".variables"))
+ }))
+ form.querySelectorAll(".variables").forEach(group => reset(group))
+ // Manage preview
+ const preview = form.querySelector(".preview button")
+ preview.addEventListener("click", async event => {
+ event.preventDefault()
+ const iframe = form.querySelector(".preview iframe")
+ iframe.src = "/build/demo"
+ iframe.onload = async function() {
+ const style = iframe.contentDocument.createElement("style")
+ style.innerText = await brew()
+ iframe.contentDocument.head.appendChild(style)
+ }
})
- reset()
// Manage brewing
const brewing = form.querySelector(".brewing button")
const teapot = {styling:[], extra:[], variables:[]}
form.addEventListener("change", () => {
const styling = Array.from(form.querySelectorAll('input[name="styling"]'))
const extra = Array.from(form.querySelectorAll('input[name="extra"]'))
- const variables = Array.from(form.querySelectorAll('.variables input[type="color"]'))
+ const variables = Array.from(form.querySelectorAll('.variables input'))
Object.assign(teapot, {
styling:styling.filter(({checked}) => checked).map(({value}) => value),
extra:extra.filter(({checked}) => checked).map(({value}) => value),
- variables:variables.filter(({value, dataset}) => value !== dataset.default).map(({name, value, dataset:{mode}}) => ({mode, name, value}))
+ variables:variables.filter(({name, value, dataset}) => (!name.endsWith("@opacity"))&&(value !== dataset.default)).map(({name, value}) => ({name, value}))
+ })
+ variables.filter(({name, value, dataset}) => (name.endsWith("@opacity"))&&(value !== dataset.default)).forEach(({name, value:opacity}) => {
+ const color = form.querySelector(`input[name="${name.replace("@opacity", "")}"]`).value
+ if (!teapot.variables.find(variable => variable.name === name.replace("@opacity", "")))
+ teapot.variables.push({name:name.replace("@opacity", ""), value:`${color}${opacity}`})
+ teapot.variables.find(variable => variable.name === name.replace("@opacity", "")).value = `${color}${opacity}`
})
- brewing.toggleAttribute("disabled", (teapot.styling.length === styling.length)&&(teapot.extra.length === extra.length)&&(!teapot.variables.length))
})
- form.querySelector(".brewing button").addEventListener("click", async event => {
- event.preventDefault()
- let stylesheet = ""
- const minify = form.querySelector('input[name="minify"]').checked
+ async function brew() {
const custom = form.querySelector('textarea[name="custom"]').value
+ const root = await fetch("/styles/@root/mod.css").then(response => response.text())
const parts = await Promise.all([...teapot.styling, ...teapot.extra].sort((a, b) => a.localeCompare(b)).map(async name => await fetch(`/styles/${name}/mod.css`).then(response => response.text())))
- stylesheet = [banner, ...parts, custom.trim()].join("\n")
- console.log(stylesheet)
if (teapot.variables.length) {
- const light = teapot.variables.filter(({mode}) => mode === "light").map(({name, value}) => `${name}: ${value};`).join("\n")
- const dark = teapot.variables.filter(({mode}) => mode === "dark").map(({name, value}) => `${name}: ${value};`).join("\n")
- stylesheet += "/* Variables */"
- if (light.trim().length) {
- stylesheet += `:root,[data-color-scheme="light"] {\n${light}\n}\n`
- }
- if (dark.trim().length) {
- stylesheet += `[data-color-scheme="dark"] {\n${dark}\n}\n`
- stylesheet += `@media (prefers-color-scheme: dark) {\n :root:not([data-color-scheme="light"]) {\n${dark.split("\n").map(line => ` ${line}`).join("\n")}\n }\n}\n`
+ const [_, defined, computed] = root.match(/^(?:root\s*\{[\s\S]*?\})(?[\s\S*]*)$/)
+ const stylesheet = new CSSStyleSheet()
+ stylesheet.insertRule(defined)
+ for (const {name, value} of teapot.variables) {
+ stylesheet.rules[0].style.setProperty(name, value)
}
+ parts.unshift(stylesheet.rules[0].cssText, computed)
+ }
+ else {
+ parts.unshift(root)
}
+ return [banner, ...parts, custom.trim()].join("\n")
+ }
+ // Manage brewed download
+ form.querySelector(".brewing button").addEventListener("click", async event => {
+ event.preventDefault()
+ const minify = form.querySelector('input[name="minify"]').checked
+ const stylesheet = await brew()
try {
brewing.toggleAttribute("disabled", true)
brewing.style.cursor = "loading"
diff --git a/app/sections/preview-website.html b/app/sections/preview-website.html
index ada2e08..1f29047 100644
--- a/app/sections/preview-website.html
+++ b/app/sections/preview-website.html
@@ -24,7 +24,13 @@
-
+
+