ranui
Version:
A framework-agnostic Web Components UI library built on native custom elements, with TypeScript types, light/dark theming, SSR and PWA support.
429 lines (411 loc) • 17.3 kB
JavaScript
import { T as y, i as v, p as w } from "./factory-WDxu11V0.js";
import { t as x } from "./events-Cqz3ramM.js";
import "./builder-EH76YCFS.js";
import { a as S, i as f, n as m, t as A } from "./utils-U-9uH7EX.js";
import { a as C, n as z } from "./ssr-BzykYefm.js";
import { n as k } from "./a11y-ZSlIUH9B.js";
var B = ':host{display:var(--ran-glass-display, inline-block);position:var(--ran-glass-position, relative);border-radius:var(--ran-glass-radius, 20px);isolation:isolate;-webkit-tap-highlight-color:transparent}.ran-glass-defs{position:absolute;width:0;height:0;overflow:hidden;pointer-events:none}.ran-glass{position:relative;box-sizing:border-box;min-width:var(--ran-glass-min-width, 0);min-height:var(--ran-glass-min-height, 0);padding:var(--ran-glass-padding, 0);border-radius:inherit;overflow:hidden;color:inherit;contain:layout paint;-webkit-backdrop-filter:blur(var(--ran-glass-blur, 16px)) saturate(var(--ran-glass-saturate, 180%)) brightness(var(--ran-glass-brightness, 1.05));backdrop-filter:blur(var(--ran-glass-blur, 16px)) saturate(var(--ran-glass-saturate, 180%)) brightness(var(--ran-glass-brightness, 1.05)) var(--ran-glass-refraction, url(#ran-glass-filter));background:var(--ran-glass-tint, rgba(255, 255, 255, .14));border:var(--ran-glass-border, 1px solid rgba(255, 255, 255, .35));box-shadow:var(--ran-glass-shadow, inset 0 1px 1px rgba(255, 255, 255, .6), inset 0 0 0 1px rgba(255, 255, 255, .06), inset 0 -20px 40px rgba(255, 255, 255, .04), 0 24px 60px -18px rgba(0, 0, 0, .5));transition:var(--ran-glass-transition, transform var(--ran-motion-duration-base, .2s) var(--ran-motion-ease-spring, cubic-bezier(.34, 1.26, .5, 1)))}.ran-glass-specular{position:absolute;inset:0;border-radius:inherit;pointer-events:none;opacity:var(--ran-glass-specular-opacity, 1);background:var(--ran-glass-specular-background, linear-gradient(180deg, rgba(255, 255, 255, .28), transparent 34%), radial-gradient(120% 80% at 50% 0%, rgba(255, 255, 255, .18), transparent 60%))}.ran-glass-rim{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}.ran-glass-specular:after{content:"";position:absolute;inset:0;border-radius:inherit;background:linear-gradient(105deg,transparent 42%,rgba(255,255,255,.4) 50%,transparent 58%);transform:translate(-130%);opacity:0}:host([sheen]) .ran-glass-specular:after{opacity:.7;animation:ran-glass-sheen var(--ran-glass-sheen-duration, 6s) ease-in-out infinite}@keyframes ran-glass-sheen{50%{transform:translate(130%)}}.ran-glass ::slotted(*){position:relative;z-index:1}:host([interactive]) .ran-glass{cursor:pointer}:host([interactive]:hover) .ran-glass{transform:translateY(-2px)}:host([interactive]:active) .ran-glass{transform:translateY(0) scale(.98);transition-duration:var(--ran-motion-duration-fast, .15s)}@media(prefers-reduced-motion:reduce){:host([sheen]) .ran-glass-specular:after{animation:none;opacity:0}.ran-glass{transition:none}}@media(prefers-reduced-transparency:reduce){.ran-glass{backdrop-filter:none;-webkit-backdrop-filter:none;background:var(--ran-glass-reduced-transparency-background, var(--ran-color-bg-elevated));box-shadow:var(--ran-glass-reduced-transparency-shadow, var(--ran-shadow-elevated))}.ran-glass-specular{display:none}}', F = `
struct Uniforms {
resolution: vec2<f32>,
radius: f32,
dpr: f32,
};
var<uniform> u: Uniforms;
fn sdRoundRect(p: vec2<f32>, halfSize: vec2<f32>, r: f32) -> f32 {
let q = abs(p) - halfSize + r;
return length(max(q, vec2<f32>(0.0, 0.0))) + min(max(q.x, q.y), 0.0) - r;
}
fn vs_main( idx: u32) -> vec4<f32> {
// Same one-triangle-covers-the-viewport trick as the WebGL vertex shader —
// clip space is identical between the two APIs, so this half needs no change.
var pos = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -1.0),
vec2<f32>(3.0, -1.0),
vec2<f32>(-1.0, 3.0),
);
return vec4<f32>(pos[idx], 0.0, 1.0);
}
fn fs_main( fragCoord: vec4<f32>) -> vec4<f32> {
let halfSize = u.resolution * 0.5;
// Unlike WebGL's gl_FragCoord (origin bottom-left, Y increasing upward),
// WGSL's fragment-stage @builtin(position) already has origin top-left with
// Y increasing downward — the same convention as CSS/screen space. No flip
// needed here; contrast with rim.ts's GLSL, which flips explicitly to match
// this same convention so the light vector means the same thing in both shaders.
let p = fragCoord.xy - halfSize;
let r = min(u.radius, min(halfSize.x, halfSize.y));
let d = sdRoundRect(p, halfSize, r);
let rimWidth = 6.0 * u.dpr;
if (abs(d) > rimWidth * 1.5) {
discard;
}
let eps = max(u.dpr, 1.0);
let n = normalize(vec2<f32>(
sdRoundRect(p + vec2<f32>(eps, 0.0), halfSize, r) - sdRoundRect(p - vec2<f32>(eps, 0.0), halfSize, r),
sdRoundRect(p + vec2<f32>(0.0, eps), halfSize, r) - sdRoundRect(p - vec2<f32>(0.0, eps), halfSize, r),
));
let light = normalize(vec2<f32>(-0.6, -0.8));
let lit = clamp(dot(n, light), 0.0, 1.0);
let rim = 1.0 - smoothstep(0.0, rimWidth, abs(d));
let glow = rim * mix(0.15, 1.0, lit);
// Chromatic fringe: same trick as the WebGL backend — split the rim mask
// along the surface normal by a couple of device pixels per channel.
let off = 1.4 * u.dpr;
let dR = sdRoundRect(p + n * off, halfSize, r);
let dB = sdRoundRect(p - n * off, halfSize, r);
let rimR = 1.0 - smoothstep(0.0, rimWidth, abs(dR));
let rimB = 1.0 - smoothstep(0.0, rimWidth, abs(dB));
let color = vec3<f32>(mix(glow, rimR, 0.5), glow, mix(glow, rimB, 0.5));
let alpha = clamp(glow * 0.85 + max(rimR, rimB) * 0.15, 0.0, 1.0);
return vec4<f32>(color * alpha, alpha);
}
`;
async function E(s) {
const e = navigator.gpu;
if (!e) return null;
try {
const t = await (await e.requestAdapter())?.requestDevice();
if (!t) return null;
const a = s.getContext("webgpu");
if (!a) return null;
const i = e.getPreferredCanvasFormat();
a.configure({
device: t,
format: i,
alphaMode: "premultiplied"
});
const l = t.createShaderModule({ code: F }), d = t.createRenderPipeline({
layout: "auto",
vertex: {
module: l,
entryPoint: "vs_main"
},
fragment: {
module: l,
entryPoint: "fs_main",
targets: [{
format: i,
blend: {
color: {
srcFactor: "one",
dstFactor: "one-minus-src-alpha",
operation: "add"
},
alpha: {
srcFactor: "one",
dstFactor: "one-minus-src-alpha",
operation: "add"
}
}
}]
},
primitive: { topology: "triangle-list" }
}), n = t.createBuffer({
size: 16,
usage: 72
}), p = t.createBindGroup({
layout: d.getBindGroupLayout(0),
entries: [{
binding: 0,
resource: { buffer: n }
}]
}), u = /* @__PURE__ */ new Float32Array(4);
return {
draw(o, r, c, h) {
if (o <= 0 || r <= 0) return;
s.width = o, s.height = r, u.set([
o,
r,
c * h,
h
]), t.queue.writeBuffer(n, 0, u);
const b = t.createCommandEncoder(), g = b.beginRenderPass({ colorAttachments: [{
view: a.getCurrentTexture().createView(),
clearValue: {
r: 0,
g: 0,
b: 0,
a: 0
},
loadOp: "clear",
storeOp: "store"
}] });
g.setPipeline(d), g.setBindGroup(0, p), g.draw(3), g.end(), t.queue.submit([b.finish()]);
},
destroy() {
t.destroy();
}
};
} catch {
return null;
}
}
var q = `
attribute vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
}
`, L = `
precision mediump float;
uniform vec2 u_resolution;
uniform float u_radius;
uniform float u_dpr;
float sdRoundRect(vec2 p, vec2 halfSize, float r) {
vec2 q = abs(p) - halfSize + r;
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
}
void main() {
vec2 halfSize = u_resolution * 0.5;
// gl_FragCoord is bottom-up; flip Y so "light from the top-left" reads as
// top-left on screen, matching the CSS specular gradient's own highlight.
vec2 p = vec2(gl_FragCoord.x, u_resolution.y - gl_FragCoord.y) - halfSize;
float r = min(u_radius, min(halfSize.x, halfSize.y));
float d = sdRoundRect(p, halfSize, r);
float rimWidth = 6.0 * u_dpr;
// Outside this band the pixel contributes nothing — skip the rest of the
// shader for it rather than computing a zero-alpha result.
if (abs(d) > rimWidth * 1.5) discard;
float eps = max(u_dpr, 1.0);
vec2 n = normalize(vec2(
sdRoundRect(p + vec2(eps, 0.0), halfSize, r) - sdRoundRect(p - vec2(eps, 0.0), halfSize, r),
sdRoundRect(p + vec2(0.0, eps), halfSize, r) - sdRoundRect(p - vec2(0.0, eps), halfSize, r)
));
vec2 light = normalize(vec2(-0.6, -0.8));
float lit = clamp(dot(n, light), 0.0, 1.0);
float rim = 1.0 - smoothstep(0.0, rimWidth, abs(d));
float glow = rim * mix(0.15, 1.0, lit);
// Chromatic fringe: split the same rim mask along the surface normal by a
// couple of device pixels per channel — a cheap stand-in for real light
// dispersion, since there is no refracted backdrop image here to disperse.
float off = 1.4 * u_dpr;
float dR = sdRoundRect(p + n * off, halfSize, r);
float dB = sdRoundRect(p - n * off, halfSize, r);
float rimR = 1.0 - smoothstep(0.0, rimWidth, abs(dR));
float rimB = 1.0 - smoothstep(0.0, rimWidth, abs(dB));
vec3 color = vec3(mix(glow, rimR, 0.5), glow, mix(glow, rimB, 0.5));
float alpha = clamp(glow * 0.85 + max(rimR, rimB) * 0.15, 0.0, 1.0);
gl_FragColor = vec4(color * alpha, alpha);
}
`;
function _(s, e, t) {
const a = s.createShader(e);
return a ? (s.shaderSource(a, t), s.compileShader(a), s.getShaderParameter(a, s.COMPILE_STATUS) ? a : (s.deleteShader(a), null)) : null;
}
function P(s) {
const e = s.getContext("webgl", {
alpha: !0,
premultipliedAlpha: !0,
antialias: !0
});
if (!e) return null;
const t = _(e, e.VERTEX_SHADER, q), a = _(e, e.FRAGMENT_SHADER, L), i = e.createProgram();
if (!t || !a || !i || (e.attachShader(i, t), e.attachShader(i, a), e.linkProgram(i), !e.getProgramParameter(i, e.LINK_STATUS))) return null;
const l = e.createBuffer();
e.bindBuffer(e.ARRAY_BUFFER, l), e.bufferData(e.ARRAY_BUFFER, new Float32Array([
-1,
-1,
3,
-1,
-1,
3
]), e.STATIC_DRAW);
const d = e.getAttribLocation(i, "a_position"), n = e.getUniformLocation(i, "u_resolution"), p = e.getUniformLocation(i, "u_radius"), u = e.getUniformLocation(i, "u_dpr");
return e.useProgram(i), e.enableVertexAttribArray(d), e.vertexAttribPointer(d, 2, e.FLOAT, !1, 0, 0), e.enable(e.BLEND), e.blendFunc(e.ONE, e.ONE_MINUS_SRC_ALPHA), {
draw(o, r, c, h) {
o <= 0 || r <= 0 || (s.width = o, s.height = r, e.viewport(0, 0, o, r), e.uniform2f(n, o, r), e.uniform1f(p, c * h), e.uniform1f(u, h), e.clearColor(0, 0, 0, 0), e.clear(e.COLOR_BUFFER_BIT), e.drawArrays(e.TRIANGLES, 0, 3));
},
destroy() {
e.getExtension("WEBGL_lose_context")?.loseContext();
}
};
}
var D = 2;
function G(s) {
let e = 20, t = !1;
const a = () => {
const r = document.createElement("canvas");
return r.className = "ran-glass-rim", r.setAttribute("part", "rim"), r.setAttribute("aria-hidden", "true"), s.appendChild(r), r;
}, i = a();
let l = P(i), d = null, n = null;
const p = () => {
const r = s.getBoundingClientRect(), c = Math.min(typeof window < "u" && window.devicePixelRatio || 1, D);
return {
width: Math.max(1, Math.round(r.width * c)),
height: Math.max(1, Math.round(r.height * c)),
dpr: c
};
}, u = () => {
const { width: r, height: c, dpr: h } = p();
(d ?? l)?.draw(r, c, e, h);
}, o = typeof ResizeObserver < "u" ? new ResizeObserver(u) : null;
return o?.observe(s), u(), typeof navigator < "u" && navigator.gpu && (n = a(), n.style.display = "none", E(n).then((r) => {
if (t) {
r?.destroy();
return;
}
if (!r) {
n?.remove(), n = null;
return;
}
d = r, l?.destroy(), l = null, i.remove(), n.style.display = "", u();
})), {
setRadius(r) {
e = r, u();
},
destroy() {
t = !0, o?.disconnect(), d?.destroy(), l?.destroy(), i.remove(), n?.remove();
}
};
}
var T = 0, W = class R extends z {
_shadowDom;
_glass;
_specular;
_turb = null;
_disp = null;
_uid = `ran-glass-${T += 1}`;
_events = new x();
_tabIndexOwnedByComponent = !1;
_rimRenderer = null;
static get observedAttributes() {
return [
"blur",
"saturate",
"displace",
"frequency",
"radius",
"tint",
"interactive",
"rim"
];
}
constructor() {
super(), this._shadowDom = A(this, B);
const e = y();
this._glass = v().class("ran-glass").attr("part", "glass").children(v().class("ran-glass-specular").ref(e).attr("part", "specular"), w()).build(), this._shadowDom.appendChild(this._glass), this._specular = S(e, "specular layer");
}
get saturate() {
return m(this, "saturate");
}
set saturate(e) {
f(this, "saturate", e);
}
get displace() {
return m(this, "displace");
}
set displace(e) {
f(this, "displace", e);
}
get frequency() {
return m(this, "frequency");
}
set frequency(e) {
f(this, "frequency", e);
}
get radius() {
return m(this, "radius");
}
set radius(e) {
f(this, "radius", e);
}
get tint() {
return m(this, "tint");
}
set tint(e) {
f(this, "tint", e);
}
get sheen() {
return this.hasAttribute("sheen");
}
set sheen(e) {
this.toggleAttribute("sheen", e);
}
get interactive() {
return this.hasAttribute("interactive");
}
set interactive(e) {
this.toggleAttribute("interactive", e);
}
get rim() {
return this.hasAttribute("rim");
}
set rim(e) {
this.toggleAttribute("rim", e);
}
_ensureFilter() {
if (!(this._disp || typeof document > "u")) {
if (!this._shadowDom.querySelector(".ran-glass-defs")) {
const e = document.createElement("div");
e.className = "ran-glass-defs", e.setAttribute("aria-hidden", "true"), e.innerHTML = `<svg><filter id="${this._uid}" x="-20%" y="-20%" width="140%" height="140%"><feTurbulence type="fractalNoise" baseFrequency="0.004 0.006" numOctaves="2" seed="7" result="n"/><feGaussianBlur in="n" stdDeviation="2" result="sn"/><feDisplacementMap in="SourceGraphic" in2="sn" scale="8" xChannelSelector="R" yChannelSelector="G"/></filter></svg>`, this._shadowDom.insertBefore(e, this._shadowDom.firstChild), this.style.setProperty("--ran-glass-refraction", `url(#${this._uid})`);
}
this._turb = this._shadowDom.querySelector("feTurbulence"), this._disp = this._shadowDom.querySelector("feDisplacementMap");
}
}
_ensureRim() {
this._rimRenderer || typeof document > "u" || (this._rimRenderer = G(this._specular), this._rimRenderer.setRadius(Number(this.radius) || 20));
}
_teardownRim() {
this._rimRenderer?.destroy(), this._rimRenderer = null;
}
_apply(e) {
const t = this.getAttribute(e);
switch (e) {
case "blur":
this._setVar("--ran-glass-blur", t, "px");
break;
case "saturate":
this._setVar("--ran-glass-saturate", t, "%");
break;
case "radius":
this._setVar("--ran-glass-radius", t, "px");
break;
case "tint":
this._setVar("--ran-glass-tint", t);
break;
case "displace":
this._ensureFilter(), this._disp && t != null && this._disp.setAttribute("scale", t);
break;
case "frequency":
this._ensureFilter(), this._turb && t != null && this._turb.setAttribute("baseFrequency", t);
}
}
_setVar(e, t, a = "") {
if (t == null || t === "") {
this.style.removeProperty(e);
return;
}
const i = a && /^-?\d*\.?\d+$/.test(t.trim());
this.style.setProperty(e, i ? `${t.trim()}${a}` : t);
}
_onKeydown = (e) => {
this.interactive && k(e) && (e.preventDefault(), this.click());
};
_syncInteractive() {
this.interactive ? (this.setAttribute("role", "button"), this.hasAttribute("tabindex") || (this.tabIndex = 0, this._tabIndexOwnedByComponent = !0)) : (this.removeAttribute("role"), this._tabIndexOwnedByComponent && (this.removeAttribute("tabindex"), this._tabIndexOwnedByComponent = !1));
}
connectedCallback() {
this._ensureFilter(), R.observedAttributes.forEach((e) => this._apply(e)), this._syncInteractive(), this.rim && this._ensureRim(), this._events.on(this, "keydown", this._onKeydown);
}
disconnectedCallback() {
this._events.abort();
}
attributeChangedCallback(e, t, a) {
if (t !== a) {
if (e === "interactive") {
this._syncInteractive();
return;
}
if (e === "rim") {
this.rim ? this._ensureRim() : this._teardownRim();
return;
}
this._apply(e), e === "radius" && this.rim && this._rimRenderer?.setRadius(Number(this.radius) || 20);
}
}
};
C("r-glass", W);
export {
W as t
};