Files
wehub-resource-sync e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

256 lines
8.9 KiB
JavaScript

#!/usr/bin/env node
/**
* Generates src/index.css for tw-glass.
*
* The displacement map SVG is base64-encoded once, then embedded inside each
* filter SVG's <feImage href>. The outer filter SVG is URL-encoded as a data
* URI for use in backdrop-filter: url("data:...#f").
*
* Run: node scripts/generate.mjs
*/
import { writeFileSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import {
buildDisplacementMapSvg,
encodeSvgUrl,
buildStandardFilter,
buildChromaticFilter,
toDataUri,
} from "./filter-builder.mjs";
const __dirname = dirname(fileURLToPath(import.meta.url));
// ─── Displacement Map ──────────────────────────────────────────────
const DISPLACEMENT_MAP_SVG = buildDisplacementMapSvg();
const mapUrlEncoded = encodeSvgUrl(DISPLACEMENT_MAP_SVG);
// ─── Strength Levels ───────────────────────────────────────────────
// Scale is in objectBoundingBox units (fraction of element size).
// strength-20 at scale 0.10 matches the tested "standard" look.
const STRENGTHS = [
{ name: "5", scale: 0.03 },
{ name: "10", scale: 0.05 },
{ name: "20", scale: 0.1 },
{ name: "30", scale: 0.15 },
{ name: "40", scale: 0.2 },
{ name: "50", scale: 0.25 },
];
const DEFAULT_STRENGTH = "20";
// ─── Filter Builders ───────────────────────────────────────────────
// (imported from filter-builder.mjs)
// ─── Generate CSS ──────────────────────────────────────────────────
const defaultScale = STRENGTHS.find((s) => s.name === DEFAULT_STRENGTH).scale;
const defaultFilterUri = toDataUri(
buildStandardFilter(mapUrlEncoded, defaultScale),
);
const lines = [];
const emit = (s = "") => lines.push(s);
emit(`/*`);
emit(` * tw-glass — Tailwind CSS v4 plugin for glass refraction effects`);
emit(` *`);
emit(
` * Uses SVG displacement maps with filterUnits="objectBoundingBox" to create`,
);
emit(
` * glass-like refraction that scales with element size. No JavaScript, no`,
);
emit(` * companion components — just CSS classes.`);
emit(` *`);
emit(` * Usage:`);
emit(` * @import "tw-glass";`);
emit(` *`);
emit(
` * <div class="glass"> <!-- default refraction -->`,
);
emit(
` * <div class="glass glass-strength-40"> <!-- stronger -->`,
);
emit(
` * <div class="glass glass-chromatic-20"> <!-- RGB splitting -->`,
);
emit(
` * <div class="glass glass-blur-4"> <!-- custom blur (px) -->`,
);
emit(
` * <div class="glass glass-saturation-150"> <!-- 150% saturation -->`,
);
emit(
` * <div class="glass glass-brightness-110"> <!-- 110% brightness -->`,
);
emit(
` * <div class="glass glass-surface"> <!-- frosted surface -->`,
);
emit(
` * <h1 class="glass-text"> <!-- glass text effect -->`,
);
emit(` */`);
emit();
// Custom properties
emit(`/* ── Custom Properties ──────────────────────────────────────── */`);
emit();
emit(`@property --tw-glass-blur {`);
emit(` syntax: "<length>";`);
emit(` inherits: false;`);
emit(` initial-value: 2px;`);
emit(`}`);
emit();
emit(`@property --tw-glass-brightness {`);
emit(` syntax: "<number>";`);
emit(` inherits: false;`);
emit(` initial-value: 1.05;`);
emit(`}`);
emit();
emit(`@property --tw-glass-saturation {`);
emit(` syntax: "<number>";`);
emit(` inherits: false;`);
emit(` initial-value: 1.2;`);
emit(`}`);
emit();
emit(`@property --glass-bg-opacity {`);
emit(` syntax: "<number>";`);
emit(` inherits: true;`);
emit(` initial-value: 0.08;`);
emit(`}`);
emit();
// ─── Tailwind v4 Backdrop Composition ────────────────────────────────
// Tailwind v4 "owns" the backdrop-filter property: it strips any
// backdrop-filter declaration from user CSS (@utility, @layer utilities)
// and only generates it via its own internal composition of --tw-backdrop-*
// variables. To work around this we:
//
// 1. Set Tailwind's --tw-backdrop-* vars inside the tree-shakeable @utility
// 2. Apply the actual backdrop-filter property via @layer components, which
// Tailwind passes through without stripping.
//
// The composition string mirrors what Tailwind generates for its built-in
// backdrop-* utilities — if the variable is unset the trailing comma makes
// var() resolve to an empty string which is ignored.
const TW_BACKDROP_COMPOSITION = [
"var(--tw-backdrop-blur,)",
"var(--tw-backdrop-brightness,)",
"var(--tw-backdrop-contrast,)",
"var(--tw-backdrop-grayscale,)",
"var(--tw-backdrop-hue-rotate,)",
"var(--tw-backdrop-invert,)",
"var(--tw-backdrop-opacity,)",
"var(--tw-backdrop-saturate,)",
"var(--tw-backdrop-sepia,)",
].join(" ");
emit(`/* ── Base Glass Utility ─────────────────────────────────────── */`);
emit();
emit(`@utility glass {`);
emit(` --tw-glass-filter: ${defaultFilterUri};`);
emit(
` --tw-backdrop-blur: var(--tw-glass-filter) blur(var(--tw-glass-blur));`,
);
emit(` --tw-backdrop-brightness: brightness(var(--tw-glass-brightness));`);
emit(` --tw-backdrop-saturate: saturate(var(--tw-glass-saturation));`);
emit(`}`);
emit();
emit(`/* Companion rule — @layer components is not stripped by Tailwind v4 */`);
emit(`@layer components {`);
emit(` .glass {`);
emit(` -webkit-backdrop-filter: ${TW_BACKDROP_COMPOSITION};`);
emit(` backdrop-filter: ${TW_BACKDROP_COMPOSITION};`);
emit(` }`);
emit(`}`);
emit();
// Surface styling
emit(`/* ── Surface Styling (compose with "glass") ────────────────── */`);
emit();
emit(`@utility glass-surface {`);
emit(` background: rgb(255 255 255 / var(--glass-bg-opacity));`);
emit(` box-shadow:`);
emit(` inset 0 0 0 1px rgb(255 255 255 / 0.15),`);
emit(` inset 0 1px 0 rgb(255 255 255 / 0.25),`);
emit(` 0 8px 32px rgb(0 0 0 / 0.12);`);
emit(`}`);
emit();
// Strength levels
emit(`/* ── Displacement Strength ──────────────────────────────────── */`);
emit();
for (const { name, scale } of STRENGTHS) {
const uri = toDataUri(buildStandardFilter(mapUrlEncoded, scale));
emit(`@utility glass-strength-${name} {`);
emit(` --tw-glass-filter: ${uri};`);
emit(`}`);
emit();
}
// Chromatic levels
emit(`/* ── Chromatic Aberration (RGB channel splitting) ──────────── */`);
emit();
for (const { name, scale } of STRENGTHS) {
const uri = toDataUri(buildChromaticFilter(mapUrlEncoded, scale));
emit(`@utility glass-chromatic-${name} {`);
emit(` --tw-glass-filter: ${uri};`);
emit(`}`);
emit();
}
// ─── Continuous Modifiers ─────────────────────────────────────
emit(`/* ── Continuous Modifiers ───────────────────────────────────── */`);
emit();
emit(`@utility glass-blur-* {`);
emit(` --tw-glass-blur: calc(--value(number) * 1px);`);
emit(`}`);
emit();
emit(`@utility glass-saturation-* {`);
emit(` --tw-glass-saturation: calc(--value(number) / 100);`);
emit(`}`);
emit();
emit(`@utility glass-brightness-* {`);
emit(` --tw-glass-brightness: calc(--value(number) / 100);`);
emit(`}`);
emit();
emit(`@utility glass-bg-* {`);
emit(` --glass-bg-opacity: calc(--value(number) * 0.01);`);
emit(`}`);
emit();
emit(`/* ── Glass Text Effect ─────────────────────────────────────── */`);
emit(`/*`);
emit(
` * Shows a background image through the text shape, like looking through`,
);
emit(` * glass letters. Set \`background-image\` on the element and use`);
emit(` * \`background-attachment: fixed\` for a parallax-window effect.`);
emit(` *`);
emit(` * Usage:`);
emit(` * <h1 class="glass-text" style="background-image: url(photo.jpg)">`);
emit(` * tw-glass`);
emit(` * </h1>`);
emit(` */`);
emit();
emit(`@utility glass-text {`);
emit(` color: transparent;`);
emit(` -webkit-background-clip: text;`);
emit(` background-clip: text;`);
emit(` background-size: cover;`);
emit(` background-position: center;`);
emit(`}`);
const css = `${lines.join("\n")}\n`;
const outPath = resolve(__dirname, "../src/index.css");
writeFileSync(outPath, css);
console.log(`✓ Generated ${outPath}`);
console.log(` ${STRENGTHS.length} standard strength levels`);
console.log(` ${STRENGTHS.length} chromatic strength levels`);
console.log(` ${(css.length / 1024).toFixed(1)}KB total`);