glasskit-js 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LiquidGlass.d.ts CHANGED
@@ -9,5 +9,7 @@ export interface LiquidGlassProps
9
9
  children?: React.ReactNode;
10
10
  }
11
11
 
12
- declare const LiquidGlass: React.FC<LiquidGlassProps>;
12
+ declare const LiquidGlass: React.ForwardRefExoticComponent<
13
+ LiquidGlassProps & React.RefAttributes<HTMLElement>
14
+ >;
13
15
  export default LiquidGlass;
package/LiquidGlass.mjs CHANGED
@@ -19,10 +19,10 @@ import LG from "./liquid-glass.js";
19
19
  const GLASS_KEYS = [
20
20
  "mode", "frost", "refraction", "depth", "dispersion", "splay",
21
21
  "lightAngle", "lightIntensity", "curvature", "convexity",
22
- "tint", "tintOpacity", "sheen", "sheenColor", "saturate", "brightness", "radius", "background",
22
+ "tint", "tintOpacity", "sheen", "sheenColor", "saturate", "brightness", "shadow", "radius", "background",
23
23
  ];
24
24
 
25
- export default function LiquidGlass({ as: Tag = "div", children, ...props }) {
25
+ const LiquidGlass = React.forwardRef(function LiquidGlass({ as: Tag = "div", children, ...props }, forwardedRef) {
26
26
  const ref = useRef(null);
27
27
  const instRef = useRef(null);
28
28
 
@@ -47,5 +47,14 @@ export default function LiquidGlass({ as: Tag = "div", children, ...props }) {
47
47
  instRef.current?.update(JSON.parse(optsKey));
48
48
  }, [optsKey]);
49
49
 
50
- return React.createElement(Tag, { ref, ...rest }, children);
51
- }
50
+ // keep our internal ref and forward the node to the caller's ref too
51
+ const setRef = (node) => {
52
+ ref.current = node;
53
+ if (typeof forwardedRef === "function") forwardedRef(node);
54
+ else if (forwardedRef) forwardedRef.current = node;
55
+ };
56
+
57
+ return React.createElement(Tag, { ref: setRef, ...rest }, children);
58
+ });
59
+
60
+ export default LiquidGlass;
package/README.md CHANGED
@@ -73,7 +73,11 @@ It refracts DOM, not `<canvas>` pixels — use `webgl` for canvas/video backgrou
73
73
  | Depth | `depth` | | tint | `tint`, `tintOpacity` |
74
74
  | Dispersion | `dispersion` | | corner radius | `radius` |
75
75
  | Splay | `splay` | | | |
76
- | Light (angle / %) | `lightAngle` / `lightIntensity` | | | |
76
+ | Light (angle / %) | `lightAngle` / `lightIntensity` | | drop shadow | `shadow` |
77
+
78
+ - **`tint`** takes either `"r,g,b"` (paired with `tintOpacity`) **or a full CSS gradient** — `tint="linear-gradient(180deg, rgba(255,255,255,.2), rgba(19,19,19,.22))"`.
79
+ - **`radius`** sets `border-radius` on the element *and* the refraction map — `radius={999}` alone gives you a pill; no need to also set it in `style`.
80
+ - **`shadow`** is any CSS `box-shadow` (`"none"` removes it); the inner light border/bezel follow `lightIntensity`.
77
81
 
78
82
  ## Shapes
79
83
 
package/liquid-glass.d.ts CHANGED
@@ -31,7 +31,7 @@ export interface LiquidGlassOptions {
31
31
  curvature?: number;
32
32
  /** +1 convex (magnify) .. 0 flat .. -1 concave (shrink). @default 1 */
33
33
  convexity?: number;
34
- /** Glass tint as "r,g,b". @default "255,255,255" */
34
+ /** Glass tint either "r,g,b" (combined with `tintOpacity`) or a full CSS `gradient(...)` string (opacity baked into its color stops). @default "255,255,255" */
35
35
  tint?: string;
36
36
  /** Tint opacity 0..1. @default 0.08 */
37
37
  tintOpacity?: number;
@@ -43,7 +43,9 @@ export interface LiquidGlassOptions {
43
43
  saturate?: number;
44
44
  /** Backdrop brightness (css mode). @default 1.04 */
45
45
  brightness?: number;
46
- /** Override corner radius (px). null = read element's border-radius. @default null */
46
+ /** Outer drop shadow as any CSS box-shadow value; "none" or "" removes it. The inner light border/bezel are controlled by `lightIntensity`. @default "0 8px 30px rgba(0,0,0,0.18)" */
47
+ shadow?: string;
48
+ /** Corner radius in px. When set, it both feeds the refraction map AND applies `border-radius` to the element. null = read the element's existing border-radius. @default null */
47
49
  radius?: number | null;
48
50
  /** Element or selector to refract. Required for `svg-clone` and `webgl`. */
49
51
  background?: Element | string | null;
package/liquid-glass.js CHANGED
@@ -42,6 +42,7 @@
42
42
  sheenColor: '255,255,255', // "r,g,b" — recolor the gloss
43
43
  saturate: 1.4,
44
44
  brightness: 1.04,
45
+ shadow: '0 8px 30px rgba(0,0,0,0.18)', // outer drop shadow; 'none'/'' removes it, or pass any CSS box-shadow
45
46
  radius: null, // null = read element border-radius
46
47
  background: null // Element|selector — required for 'svg-clone' & 'webgl'
47
48
  };
@@ -58,7 +59,24 @@
58
59
  function clamp8(v) { return v < 0 ? 0 : v > 255 ? 255 : Math.round(v); }
59
60
  // only ever emit a clean "r,g,b" triplet into inline CSS — blocks url()/expression smuggling
60
61
  function safeRGB(v) { return (typeof v === 'string' && /^\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*$/.test(v)) ? v.replace(/\s+/g, '') : '255,255,255'; }
62
+ // allow only box-shadow-shaped CSS (lengths/colors); reject url()/expression/extra declarations
63
+ function safeShadow(v) {
64
+ if (typeof v !== 'string' || !v || v === 'none') return '';
65
+ return /url\(|expression|javascript:|[;{}<>]/i.test(v) ? '' : v;
66
+ }
67
+ // allow CSS gradient values only; block url()/image-set()/extra declarations
68
+ function safeGradient(v) {
69
+ if (typeof v !== 'string') return '';
70
+ if (/url\(|image-set|element\(|expression|javascript:|[;{}<>]/i.test(v)) return '';
71
+ return /(^|\s)(repeating-)?(linear|radial|conic)-gradient\(/i.test(v) ? v : '';
72
+ }
61
73
  function num(v, d) { var n = parseFloat(v); return isFinite(n) ? n : d; }
74
+ // tint background: a full CSS gradient string, else an rgba() built from "r,g,b" + opacity
75
+ function tintValue(o) {
76
+ var t = o.tint;
77
+ if (typeof t === 'string' && /gradient\(/i.test(t)) { var g = safeGradient(t); if (g) return g; }
78
+ return 'rgba(' + safeRGB(t) + ',' + num(o.tintOpacity, 0.08) + ')';
79
+ }
62
80
  function resolveEl(x) { return typeof x === 'string' ? document.querySelector(x) : x; }
63
81
  function readRadius(el) { return parseFloat(getComputedStyle(el).borderRadius) || 0; }
64
82
  function ns(tag) { return document.createElementNS('http://www.w3.org/2000/svg', tag); }
@@ -111,7 +129,7 @@
111
129
  if ((mode === 'svg-clone' || mode === 'webgl') && !bg) mode = isChromium() ? 'svg' : 'css';
112
130
 
113
131
  if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
114
- var prevBg = el.style.backgroundColor;
132
+ var prevBg = el.style.background, prevRadius = el.style.borderRadius;
115
133
 
116
134
  var overlay = document.createElement('div');
117
135
  overlay.style.cssText = 'position:absolute;inset:0;border-radius:inherit;pointer-events:none;z-index:2;';
@@ -245,17 +263,19 @@
245
263
  /* ---- tint + specular overlay (all modes) ---- */
246
264
  function applyOverlay() {
247
265
  var li = num(o.lightIntensity, 0.8), a = (num(o.lightAngle, -45) + 90) * Math.PI / 180;
248
- if (mode === 'css' || mode === 'svg') el.style.backgroundColor = 'rgba(' + safeRGB(o.tint) + ',' + num(o.tintOpacity, 0.08) + ')';
266
+ if (mode === 'css' || mode === 'svg') el.style.background = tintValue(o);
249
267
  // FACE gloss — controlled by `sheen`/`sheenColor`, independent of the border light below
250
268
  var sc = safeRGB(o.sheenColor), sh = num(o.sheen, 0.7);
251
269
  overlay.style.background = sh <= 0 ? 'none' :
252
270
  'linear-gradient(' + (num(o.lightAngle, -45) + 90) + 'deg,rgba(' + sc + ',' + (0.6 * sh).toFixed(3) +
253
271
  ') 0%,rgba(' + sc + ',0) 30%,rgba(' + sc + ',0) 70%,rgba(' + sc + ',' + (0.14 * sh).toFixed(3) + ') 100%)';
254
- overlay.style.boxShadow =
272
+ // inset light border + bezel (scale with lightIntensity) and a separately controllable outer drop shadow
273
+ var insets =
255
274
  'inset 0 0 0 1px rgba(255,255,255,' + (0.30 * li) + '),' +
256
275
  'inset ' + (Math.cos(a) * 2).toFixed(1) + 'px ' + (Math.sin(a) * 2).toFixed(1) + 'px 2px rgba(255,255,255,' + (0.5 * li) + '),' +
257
- 'inset ' + (-Math.cos(a) * 2).toFixed(1) + 'px ' + (-Math.sin(a) * 2).toFixed(1) + 'px 6px rgba(0,0,0,.15),' +
258
- '0 8px 30px rgba(0,0,0,.18)';
276
+ 'inset ' + (-Math.cos(a) * 2).toFixed(1) + 'px ' + (-Math.sin(a) * 2).toFixed(1) + 'px 6px rgba(0,0,0,.15)';
277
+ var drop = safeShadow(o.shadow);
278
+ overlay.style.boxShadow = drop ? insets + ',' + drop : insets;
259
279
  }
260
280
 
261
281
  /* ------------------------------ WebGL ------------------------------ */
@@ -363,14 +383,17 @@
363
383
  var ro = new ResizeObserver(function () { refreshMap(); if (mode === 'svg-clone') syncClone(); });
364
384
  ro.observe(el);
365
385
 
366
- refreshMap(); applyFilterParams(); applyOverlay();
386
+ // when an explicit radius is given, round the element itself (not just the refraction map)
387
+ function applyRadius() { if (o.radius != null) el.style.borderRadius = num(o.radius, 0) + 'px'; }
388
+
389
+ refreshMap(); applyFilterParams(); applyOverlay(); applyRadius();
367
390
 
368
391
  var instance = {
369
392
  el: el, mode: mode,
370
393
  update: function (patch) {
371
394
  Object.assign(o, patch || {});
372
395
  if (patch && patch.background != null && mode === 'webgl') loadGLBg(resolveEl(o.background));
373
- refreshMap(); applyFilterParams(); applyOverlay();
396
+ refreshMap(); applyFilterParams(); applyOverlay(); applyRadius();
374
397
  return instance;
375
398
  },
376
399
  refresh: function () { if (mode === 'svg-clone') reclone(); else if (mode === 'webgl') loadGLBg(resolveEl(o.background)); return instance; },
@@ -380,7 +403,7 @@
380
403
  // free the WebGL context explicitly — browsers cap ~16 per page
381
404
  if (gl) { var lc = gl.getExtension('WEBGL_lose_context'); if (lc) lc.loseContext(); gl = null; }
382
405
  [svg, lensWrap, glcanvas, overlay].forEach(function (n) { if (n && n.parentNode) n.parentNode.removeChild(n); });
383
- el.style.backdropFilter = ''; el.style.webkitBackdropFilter = ''; el.style.backgroundColor = prevBg;
406
+ el.style.backdropFilter = ''; el.style.webkitBackdropFilter = ''; el.style.background = prevBg; el.style.borderRadius = prevRadius;
384
407
  }
385
408
  };
386
409
  return instance;
@@ -389,7 +412,7 @@
389
412
  /* ===================== <glass-kit> web component ===================== */
390
413
  var ATTRS = ['mode', 'frost', 'refraction', 'depth', 'dispersion', 'splay', 'light-angle',
391
414
  'light-intensity', 'curvature', 'convexity', 'tint', 'tint-opacity', 'sheen', 'sheen-color',
392
- 'radius', 'background'];
415
+ 'shadow', 'radius', 'background'];
393
416
  function camel(s) { return s.replace(/-([a-z])/g, function (_, c) { return c.toUpperCase(); }); }
394
417
  function defineElement() {
395
418
  if (typeof customElements === 'undefined' || customElements.get('glass-kit')) return;
@@ -399,7 +422,7 @@
399
422
  ATTRS.forEach(function (a) {
400
423
  if (!node.hasAttribute(a)) return;
401
424
  var v = node.getAttribute(a), key = camel(a);
402
- opts[key] = (a === 'mode' || a === 'tint' || a === 'sheen-color' || a === 'background') ? v : parseFloat(v);
425
+ opts[key] = (a === 'mode' || a === 'tint' || a === 'sheen-color' || a === 'shadow' || a === 'background') ? v : parseFloat(v);
403
426
  });
404
427
  return opts;
405
428
  }
@@ -416,5 +439,5 @@
416
439
  }
417
440
  if (typeof window !== 'undefined') { if (document.readyState !== 'loading') defineElement(); else document.addEventListener('DOMContentLoaded', defineElement); }
418
441
 
419
- return { apply: apply, defineElement: defineElement, isChromium: isChromium, pickMode: pickMode, DEFAULTS: DEFAULTS, version: '1.0.1' };
442
+ return { apply: apply, defineElement: defineElement, isChromium: isChromium, pickMode: pickMode, DEFAULTS: DEFAULTS, version: '1.1.0' };
420
443
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glasskit-js",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Drop-in Apple/Figma 'Liquid Glass' for any element — switch between pure CSS, SVG displacement (live backdrop), cross-browser clone-mode, or WebGL. Real refraction, chromatic aberration, specular highlight. Zero dependencies. Class + <liquid-glass> web component.",
5
5
  "keywords": [
6
6
  "liquid-glass",