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 @@

À la carte

- + @@ -357,7 +357,10 @@

Scripting

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 @@

🛠️ Create a custom build

- - - - + + + +
- - - - + + + +
- - - - + + + +
- - - - + + + +
@@ -86,128 +134,239 @@

🛠️ Create a custom build

-
- - Variables - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ColorsBackgrounds
NameLightDarkNameLightDark
--ly-brand--ly-bg-brand
--default--bg-default
--contrast--bg-contrast
--subtle--bg-subtle
--muted--bg-muted
--accent--bg-accent
--active--bg-active
--variant--bg-variant
--success--bg-success
--attention--bg-attention
--severe--bg-severe
--danger--bg-danger
-
-
- -
-
-
-
+
+ Customize CSS variables +
+ + Colors and backgrounds + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ColorsBackgroundsExample usage
NameLightDarkNameLightDark
--default--bg-defaultdefault text color and background color
--contrast--bg-contrastused to constrast with --default
--subtle--bg-subtle<progress> and <meter> backgrounds, etc.
--muted--bg-muted.disabled, <caption>, <blockquote>, etc.
--accent--bg-accent:hover, .selected, <a>, <button>, etc.
--active--bg-active:active, :focus, etc.
--variant--bg-variant
--success--bg-success:user-valid, <ins>, <meter>'s optimum value, etc.
--attention--bg-attention<meter>'s sub-optimum value, etc.
--severe--bg-severe
--danger--bg-danger:user-invalid, <del>, <meter>'s sub-sub-optimum value, <button type="reset">, etc.
+
+
+ +
+
+
+ Font related variables + + + +
+ +
+
+
+ Layouts +

+ These variables are only revelant when using a layout from @layouts extra feature. +

+ + + + +
+ +
+
+
+ Miscellaneous + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameLightDarkExample usage
ColorAlphaColorAlpha
--backdropdialog::backdrop, --shadow, etc.
--bd-muted<td>, <th>, <article>, <blockquote>, <fieldset>, <input>, <select>, <textarea>, etc.
+
+
+ +
+
+
Customization @@ -252,6 +411,22 @@

🛠️ Create a custom build

+
+ Preview +

+ Preview your custom build to ensure it fits your taste. +

+ +
  • + + +
  • +
    + +
    + +
    +
    Brew matcha!

    @@ -259,7 +434,7 @@

    🛠️ Create a custom build

    Click the button below to get started!

    - +
    @@ -280,6 +455,7 @@

    🛠️ Create a custom build

    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 @@

    🛠️ Create a custom build

    }) } }) - // 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 @@

    🕵️ Preview a Website

    - + +
  • + + +
  • +
    +