UNPKG

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
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, }; @group(0) @binding(0) 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; } @vertex fn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) 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); } @fragment fn fs_main(@builtin(position) fragCoord: vec4<f32>) -> @location(0) 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 };