ngx-liquid-glazz 0.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/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # ngx-liquid-glazz
2
+
3
+ ![hero](https://raw.githubusercontent.com/z4kettan/ios_liquid_glass/main/docs/hero.gif)
4
+
5
+ **Real iOS-style "Liquid Glass" for Angular** — actual refraction via an SVG
6
+ displacement filter (`feTurbulence` + `feGaussianBlur` + `feDisplacementMap`), layered
7
+ with a frost/tint pass and specular edge highlights. Not another `backdrop-filter: blur()`
8
+ glassmorphism wrapper — the backdrop actually bends.
9
+
10
+ There was no Angular package for this effect (the space was React-only). This is a single
11
+ standalone directive: works on cards, buttons, navbars — anything.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install ngx-liquid-glazz
17
+ ```
18
+
19
+ Import the base stylesheet once, globally (`angular.json` `styles` array, or a global `@import`):
20
+
21
+ ```css
22
+ @import 'ngx-liquid-glazz/styles';
23
+ ```
24
+
25
+ ## Quickstart
26
+
27
+ ```ts
28
+ import { Component } from '@angular/core';
29
+ import { LiquidGlassDirective } from 'ngx-liquid-glazz';
30
+
31
+ @Component({
32
+ selector: 'app-card',
33
+ standalone: true,
34
+ imports: [LiquidGlassDirective],
35
+ template: `
36
+ <div liquidGlass [displacementScale]="90" [blur]="4" class="card">
37
+ <h2>Frosted &amp; refracted</h2>
38
+ <p>Real glass, not just blur.</p>
39
+ </div>
40
+ `,
41
+ styles: `
42
+ .card {
43
+ padding: 2rem;
44
+ border-radius: 24px;
45
+ }
46
+ `,
47
+ })
48
+ export class CardComponent {}
49
+ ```
50
+
51
+ That's it — no module, no providers required. The directive reacts to input changes live,
52
+ so binding sliders or state to any input updates the glass in real time.
53
+
54
+ ## Inputs
55
+
56
+ | Input | Type | Default | Description |
57
+ | --------------------- | --------- | --------------------------- | ------------------------------------------------------------- |
58
+ | `displacementScale` | `number` | `70` | Refraction strength — maps to the SVG `feDisplacementMap` `scale`. |
59
+ | `blur` | `number` | `3` | Frost blur, in px. |
60
+ | `saturation` | `number` | `180` | Backdrop saturation, in percent. |
61
+ | `brightness` | `number` | `1.08` | Backdrop brightness multiplier. |
62
+ | `tint` | `string` | `'rgba(255,255,255,0.04)'` | Tint color layered over the refracted backdrop. |
63
+ | `radius` | `string` | `'24px'` | Corner radius. |
64
+ | `specular` | `boolean` | `true` | Toggles the specular edge-highlight inset shadows. |
65
+
66
+ ## App-wide defaults
67
+
68
+ Override the built-in defaults once for the whole app with `provideLiquidGlassConfig()`.
69
+ Directive inputs still take precedence over whatever's configured here.
70
+
71
+ ```ts
72
+ import { bootstrapApplication } from '@angular/platform-browser';
73
+ import { provideLiquidGlassConfig } from 'ngx-liquid-glazz';
74
+
75
+ bootstrapApplication(AppComponent, {
76
+ providers: [
77
+ provideLiquidGlassConfig({
78
+ blur: 6,
79
+ tint: 'rgba(0,0,0,0.08)',
80
+ }),
81
+ ],
82
+ });
83
+ ```
84
+
85
+ Precedence: **directive input > `provideLiquidGlassConfig()` > built-in default.**
86
+
87
+ ## Browser support
88
+
89
+ The refraction (`backdrop-filter: url(#svg-filter)`) is a Chromium-only feature today.
90
+
91
+ | Browser | Result |
92
+ | ---------------- | ---------------------------------------------------- |
93
+ | Chrome / Edge | Full effect — real refraction + frost + specular. |
94
+ | Safari / Firefox | Graceful fallback to `blur(14px) saturate(180%)` — still a premium frosted-glass look, never broken. |
95
+
96
+ ## Angular version support
97
+
98
+ | ngx-liquid-glazz | Angular |
99
+ | ------------------ | -------------- |
100
+ | `0.1.x` | `17.1 – 22.x` |
101
+
102
+ - **Standalone-only** — no `NgModule`.
103
+ - **Zoneless-ready** — the directive updates purely through `effect()` writing CSS custom
104
+ properties; it never depends on `Zone.js` change detection.
105
+ - **`OnPush`-safe** — DOM updates aren't gated on change detection at all, so it's a natural
106
+ fit for Angular 22's `OnPush`-by-default and zoneless-by-default apps.
107
+
108
+ ## SSR
109
+
110
+ Works out of the box with Angular SSR (`ng new --ssr`). All DOM work happens inside
111
+ `afterNextRender()`, so the directive no-ops cleanly on the server and picks up on hydration.
112
+
113
+ ## How it works
114
+
115
+ - A root-provided service injects a single SVG `<filter>` (per distinct `displacementScale`
116
+ value in use) into `document.body`, ref-counted across directive instances.
117
+ - The directive applies `backdrop-filter: url(#the-filter) blur(...) saturate(...) brightness(...)`
118
+ to a `::before` layer, a tint to `::after`, and specular highlights via inset `box-shadow` —
119
+ all driven by CSS custom properties (`--lg-blur`, `--lg-tint`, etc.) so runtime input changes
120
+ update instantly with zero re-render cost.
121
+ - It never restructures your host element's content — apply it to anything.
122
+
123
+ ## License
124
+
125
+ MIT
@@ -0,0 +1,10 @@
1
+ export const DEFAULT_LIQUID_GLASS_CONFIG = {
2
+ displacementScale: 70,
3
+ blur: 3,
4
+ saturation: 180,
5
+ brightness: 1.08,
6
+ tint: 'rgba(255,255,255,0.04)',
7
+ radius: '24px',
8
+ specular: true,
9
+ };
10
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibGlxdWlkLWdsYXNzLWNvbmZpZy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3Byb2plY3RzL25neC1saXF1aWQtZ2xhc3Mvc3JjL2xpYi9saXF1aWQtZ2xhc3MtY29uZmlnLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQXNCQSxNQUFNLENBQUMsTUFBTSwyQkFBMkIsR0FBc0I7SUFDNUQsaUJBQWlCLEVBQUUsRUFBRTtJQUNyQixJQUFJLEVBQUUsQ0FBQztJQUNQLFVBQVUsRUFBRSxHQUFHO0lBQ2YsVUFBVSxFQUFFLElBQUk7SUFDaEIsSUFBSSxFQUFFLHdCQUF3QjtJQUM5QixNQUFNLEVBQUUsTUFBTTtJQUNkLFFBQVEsRUFBRSxJQUFJO0NBQ2YsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogQXBwLXdpZGUgdHVuYWJsZXMgZm9yIHRoZSBMaXF1aWQgR2xhc3MgZWZmZWN0LlxuICogRXZlcnkgZmllbGQgaXMgb3B0aW9uYWwgc28gY2FsbGVycyBjYW4gb3ZlcnJpZGUganVzdCB0aGUgZGVmYXVsdHMgdGhleSBjYXJlIGFib3V0LlxuICogUHJlY2VkZW5jZSAoaGlnaGVzdCB0byBsb3dlc3QpOiBkaXJlY3RpdmUgaW5wdXQgPiBwcm92aWRlTGlxdWlkR2xhc3NDb25maWcoKSA+IGJ1aWx0LWluIGRlZmF1bHQuXG4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgTGlxdWlkR2xhc3NDb25maWcge1xuICAvKiogUmVmcmFjdGlvbiBzdHJlbmd0aCwgbWFwcyB0byB0aGUgU1ZHIGBmZURpc3BsYWNlbWVudE1hcGAgYHNjYWxlYCBhdHRyaWJ1dGUuICovXG4gIGRpc3BsYWNlbWVudFNjYWxlOiBudW1iZXI7XG4gIC8qKiBGcm9zdCBibHVyIGluIHB4LCBtYXBzIHRvIGAtLWxnLWJsdXJgLiAqL1xuICBibHVyOiBudW1iZXI7XG4gIC8qKiBCYWNrZHJvcCBzYXR1cmF0aW9uIHBlcmNlbnQsIG1hcHMgdG8gYC0tbGctc2F0dXJhdGlvbmAuICovXG4gIHNhdHVyYXRpb246IG51bWJlcjtcbiAgLyoqIEJhY2tkcm9wIGJyaWdodG5lc3MgbXVsdGlwbGllciwgbWFwcyB0byBgLS1sZy1icmlnaHRuZXNzYC4gKi9cbiAgYnJpZ2h0bmVzczogbnVtYmVyO1xuICAvKiogVGludCBjb2xvciBsYXllcmVkIG92ZXIgdGhlIHJlZnJhY3RlZCBiYWNrZHJvcCwgbWFwcyB0byBgLS1sZy10aW50YC4gKi9cbiAgdGludDogc3RyaW5nO1xuICAvKiogQ29ybmVyIHJhZGl1cywgbWFwcyB0byBgLS1sZy1yYWRpdXNgLiAqL1xuICByYWRpdXM6IHN0cmluZztcbiAgLyoqIFdoZXRoZXIgdG8gcmVuZGVyIHRoZSBzcGVjdWxhciBlZGdlLWhpZ2hsaWdodCBpbnNldCBzaGFkb3dzLiAqL1xuICBzcGVjdWxhcjogYm9vbGVhbjtcbn1cblxuZXhwb3J0IGNvbnN0IERFRkFVTFRfTElRVUlEX0dMQVNTX0NPTkZJRzogTGlxdWlkR2xhc3NDb25maWcgPSB7XG4gIGRpc3BsYWNlbWVudFNjYWxlOiA3MCxcbiAgYmx1cjogMyxcbiAgc2F0dXJhdGlvbjogMTgwLFxuICBicmlnaHRuZXNzOiAxLjA4LFxuICB0aW50OiAncmdiYSgyNTUsMjU1LDI1NSwwLjA0KScsXG4gIHJhZGl1czogJzI0cHgnLFxuICBzcGVjdWxhcjogdHJ1ZSxcbn07XG4iXX0=
@@ -0,0 +1,20 @@
1
+ import { InjectionToken } from '@angular/core';
2
+ /**
3
+ * Holds only the overrides passed to `provideLiquidGlassConfig()`, never the built-in defaults.
4
+ * Keeping it partial lets the directive distinguish "not provided" from "explicitly set to a
5
+ * falsy value" when computing precedence against its own inputs.
6
+ */
7
+ export const LIQUID_GLASS_CONFIG = new InjectionToken('LIQUID_GLASS_CONFIG');
8
+ /**
9
+ * Overrides the app-wide Liquid Glass defaults. Directive inputs still take precedence over
10
+ * whatever is configured here.
11
+ *
12
+ * @example
13
+ * bootstrapApplication(AppComponent, {
14
+ * providers: [provideLiquidGlassConfig({ blur: 6, tint: 'rgba(0,0,0,0.08)' })],
15
+ * });
16
+ */
17
+ export function provideLiquidGlassConfig(config) {
18
+ return { provide: LIQUID_GLASS_CONFIG, useValue: config };
19
+ }
20
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibGlxdWlkLWdsYXNzLWNvbmZpZy50b2tlbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3Byb2plY3RzL25neC1saXF1aWQtZ2xhc3Mvc3JjL2xpYi9saXF1aWQtZ2xhc3MtY29uZmlnLnRva2VuLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxjQUFjLEVBQVksTUFBTSxlQUFlLENBQUM7QUFHekQ7Ozs7R0FJRztBQUNILE1BQU0sQ0FBQyxNQUFNLG1CQUFtQixHQUFHLElBQUksY0FBYyxDQUNuRCxxQkFBcUIsQ0FDdEIsQ0FBQztBQUVGOzs7Ozs7OztHQVFHO0FBQ0gsTUFBTSxVQUFVLHdCQUF3QixDQUFDLE1BQWtDO0lBQ3pFLE9BQU8sRUFBRSxPQUFPLEVBQUUsbUJBQW1CLEVBQUUsUUFBUSxFQUFFLE1BQU0sRUFBRSxDQUFDO0FBQzVELENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBJbmplY3Rpb25Ub2tlbiwgUHJvdmlkZXIgfSBmcm9tICdAYW5ndWxhci9jb3JlJztcbmltcG9ydCB7IExpcXVpZEdsYXNzQ29uZmlnIH0gZnJvbSAnLi9saXF1aWQtZ2xhc3MtY29uZmlnJztcblxuLyoqXG4gKiBIb2xkcyBvbmx5IHRoZSBvdmVycmlkZXMgcGFzc2VkIHRvIGBwcm92aWRlTGlxdWlkR2xhc3NDb25maWcoKWAsIG5ldmVyIHRoZSBidWlsdC1pbiBkZWZhdWx0cy5cbiAqIEtlZXBpbmcgaXQgcGFydGlhbCBsZXRzIHRoZSBkaXJlY3RpdmUgZGlzdGluZ3Vpc2ggXCJub3QgcHJvdmlkZWRcIiBmcm9tIFwiZXhwbGljaXRseSBzZXQgdG8gYVxuICogZmFsc3kgdmFsdWVcIiB3aGVuIGNvbXB1dGluZyBwcmVjZWRlbmNlIGFnYWluc3QgaXRzIG93biBpbnB1dHMuXG4gKi9cbmV4cG9ydCBjb25zdCBMSVFVSURfR0xBU1NfQ09ORklHID0gbmV3IEluamVjdGlvblRva2VuPFBhcnRpYWw8TGlxdWlkR2xhc3NDb25maWc+PihcbiAgJ0xJUVVJRF9HTEFTU19DT05GSUcnLFxuKTtcblxuLyoqXG4gKiBPdmVycmlkZXMgdGhlIGFwcC13aWRlIExpcXVpZCBHbGFzcyBkZWZhdWx0cy4gRGlyZWN0aXZlIGlucHV0cyBzdGlsbCB0YWtlIHByZWNlZGVuY2Ugb3ZlclxuICogd2hhdGV2ZXIgaXMgY29uZmlndXJlZCBoZXJlLlxuICpcbiAqIEBleGFtcGxlXG4gKiBib290c3RyYXBBcHBsaWNhdGlvbihBcHBDb21wb25lbnQsIHtcbiAqICAgcHJvdmlkZXJzOiBbcHJvdmlkZUxpcXVpZEdsYXNzQ29uZmlnKHsgYmx1cjogNiwgdGludDogJ3JnYmEoMCwwLDAsMC4wOCknIH0pXSxcbiAqIH0pO1xuICovXG5leHBvcnQgZnVuY3Rpb24gcHJvdmlkZUxpcXVpZEdsYXNzQ29uZmlnKGNvbmZpZzogUGFydGlhbDxMaXF1aWRHbGFzc0NvbmZpZz4pOiBQcm92aWRlciB7XG4gIHJldHVybiB7IHByb3ZpZGU6IExJUVVJRF9HTEFTU19DT05GSUcsIHVzZVZhbHVlOiBjb25maWcgfTtcbn1cbiJdfQ==
@@ -0,0 +1,113 @@
1
+ import { DOCUMENT } from '@angular/common';
2
+ import { Injectable, inject } from '@angular/core';
3
+ import * as i0 from "@angular/core";
4
+ const SVG_NS = 'http://www.w3.org/2000/svg';
5
+ const HOST_ID = 'ngx-liquid-glass-filter-host';
6
+ const FILTER_ID_PREFIX = 'ngx-liquid-glass-filter';
7
+ /**
8
+ * Owns the SVG `<filter>` definitions that power the refraction effect and injects them into
9
+ * `document.body`.
10
+ *
11
+ * `scale` (feDisplacementMap's `scale` attribute) is the one filter parameter that can't be
12
+ * shared across all directive instances the way the rest of the CSS-custom-property-driven
13
+ * styling is, because SVG filter primitives don't accept CSS custom properties as attribute
14
+ * values. Instead of generating a new filter per instance (wasteful) or a single shared filter
15
+ * (loses per-instance tuning), this service ref-counts one `<filter>` element per distinct scale
16
+ * value: directives that share a scale share a filter; a filter is removed from the DOM once
17
+ * its last consumer is destroyed.
18
+ */
19
+ export class LiquidGlassFilterService {
20
+ document = inject(DOCUMENT);
21
+ refCounts = new Map();
22
+ svgRoot = null;
23
+ /** Registers a consumer of the filter for `scale`, creating it if this is the first. Returns the element ID to reference via `url(#id)`. */
24
+ acquire(scale) {
25
+ const id = this.filterId(scale);
26
+ const count = this.refCounts.get(scale) ?? 0;
27
+ this.refCounts.set(scale, count + 1);
28
+ if (count === 0) {
29
+ this.injectFilter(id, scale);
30
+ }
31
+ return id;
32
+ }
33
+ /** Releases a consumer's hold on `scale`'s filter, removing it from the DOM once no consumers remain. */
34
+ release(scale) {
35
+ const count = this.refCounts.get(scale) ?? 0;
36
+ if (count <= 1) {
37
+ this.refCounts.delete(scale);
38
+ this.removeFilter(this.filterId(scale));
39
+ return;
40
+ }
41
+ this.refCounts.set(scale, count - 1);
42
+ }
43
+ filterId(scale) {
44
+ return `${FILTER_ID_PREFIX}-${scale}`;
45
+ }
46
+ injectFilter(id, scale) {
47
+ // Hydration / multiple-app-instance guard: another instance (or a prior SSR pass) may have
48
+ // already injected this exact filter into the DOM.
49
+ if (this.document.getElementById(id)) {
50
+ return;
51
+ }
52
+ const svg = this.getOrCreateSvgRoot();
53
+ const filter = this.document.createElementNS(SVG_NS, 'filter');
54
+ filter.setAttribute('id', id);
55
+ filter.setAttribute('x', '-20%');
56
+ filter.setAttribute('y', '-20%');
57
+ filter.setAttribute('width', '140%');
58
+ filter.setAttribute('height', '140%');
59
+ filter.setAttribute('color-interpolation-filters', 'sRGB');
60
+ const turbulence = this.document.createElementNS(SVG_NS, 'feTurbulence');
61
+ turbulence.setAttribute('type', 'fractalNoise');
62
+ turbulence.setAttribute('baseFrequency', '0.008 0.008');
63
+ turbulence.setAttribute('numOctaves', '2');
64
+ turbulence.setAttribute('seed', '92');
65
+ turbulence.setAttribute('result', 'noise');
66
+ const gaussianBlur = this.document.createElementNS(SVG_NS, 'feGaussianBlur');
67
+ gaussianBlur.setAttribute('in', 'noise');
68
+ gaussianBlur.setAttribute('stdDeviation', '2');
69
+ gaussianBlur.setAttribute('result', 'blurred');
70
+ const displacementMap = this.document.createElementNS(SVG_NS, 'feDisplacementMap');
71
+ displacementMap.setAttribute('in', 'SourceGraphic');
72
+ displacementMap.setAttribute('in2', 'blurred');
73
+ displacementMap.setAttribute('scale', String(scale));
74
+ displacementMap.setAttribute('xChannelSelector', 'R');
75
+ displacementMap.setAttribute('yChannelSelector', 'G');
76
+ filter.append(turbulence, gaussianBlur, displacementMap);
77
+ svg.querySelector('defs').appendChild(filter);
78
+ }
79
+ removeFilter(id) {
80
+ this.document.getElementById(id)?.remove();
81
+ }
82
+ getOrCreateSvgRoot() {
83
+ if (this.svgRoot?.isConnected) {
84
+ return this.svgRoot;
85
+ }
86
+ const existing = this.document.getElementById(HOST_ID);
87
+ if (existing instanceof SVGSVGElement) {
88
+ this.svgRoot = existing;
89
+ return existing;
90
+ }
91
+ const svg = this.document.createElementNS(SVG_NS, 'svg');
92
+ svg.setAttribute('id', HOST_ID);
93
+ svg.setAttribute('aria-hidden', 'true');
94
+ svg.setAttribute('focusable', 'false');
95
+ svg.style.position = 'absolute';
96
+ svg.style.width = '0';
97
+ svg.style.height = '0';
98
+ svg.style.overflow = 'hidden';
99
+ svg.style.pointerEvents = 'none';
100
+ const defs = this.document.createElementNS(SVG_NS, 'defs');
101
+ svg.appendChild(defs);
102
+ this.document.body.appendChild(svg);
103
+ this.svgRoot = svg;
104
+ return svg;
105
+ }
106
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LiquidGlassFilterService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
107
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LiquidGlassFilterService, providedIn: 'root' });
108
+ }
109
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LiquidGlassFilterService, decorators: [{
110
+ type: Injectable,
111
+ args: [{ providedIn: 'root' }]
112
+ }] });
113
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibGlxdWlkLWdsYXNzLWZpbHRlci5zZXJ2aWNlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vcHJvamVjdHMvbmd4LWxpcXVpZC1nbGFzcy9zcmMvbGliL2xpcXVpZC1nbGFzcy1maWx0ZXIuc2VydmljZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsUUFBUSxFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFDM0MsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLEVBQUUsTUFBTSxlQUFlLENBQUM7O0FBRW5ELE1BQU0sTUFBTSxHQUFHLDRCQUE0QixDQUFDO0FBQzVDLE1BQU0sT0FBTyxHQUFHLDhCQUE4QixDQUFDO0FBQy9DLE1BQU0sZ0JBQWdCLEdBQUcseUJBQXlCLENBQUM7QUFFbkQ7Ozs7Ozs7Ozs7O0dBV0c7QUFFSCxNQUFNLE9BQU8sd0JBQXdCO0lBQ2xCLFFBQVEsR0FBRyxNQUFNLENBQUMsUUFBUSxDQUFDLENBQUM7SUFDNUIsU0FBUyxHQUFHLElBQUksR0FBRyxFQUFrQixDQUFDO0lBQy9DLE9BQU8sR0FBeUIsSUFBSSxDQUFDO0lBRTdDLDRJQUE0STtJQUM1SSxPQUFPLENBQUMsS0FBYTtRQUNuQixNQUFNLEVBQUUsR0FBRyxJQUFJLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQ2hDLE1BQU0sS0FBSyxHQUFHLElBQUksQ0FBQyxTQUFTLENBQUMsR0FBRyxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUM3QyxJQUFJLENBQUMsU0FBUyxDQUFDLEdBQUcsQ0FBQyxLQUFLLEVBQUUsS0FBSyxHQUFHLENBQUMsQ0FBQyxDQUFDO1FBRXJDLElBQUksS0FBSyxLQUFLLENBQUMsRUFBRSxDQUFDO1lBQ2hCLElBQUksQ0FBQyxZQUFZLENBQUMsRUFBRSxFQUFFLEtBQUssQ0FBQyxDQUFDO1FBQy9CLENBQUM7UUFFRCxPQUFPLEVBQUUsQ0FBQztJQUNaLENBQUM7SUFFRCx5R0FB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LENBQUMsS0FBSyxDQUFDLEtBQUssR0FBRyxHQUFHLENBQUM7UUFDdEIsR0FBRyxDQUFDLEtBQUssQ0FBQyxNQUFNLEdBQUcsR0FBRyxDQUFDO1FBQ3ZCLEdBQUcsQ0FBQyxLQUFLLENBQUMsUUFBUSxHQUFHLFFBQVEsQ0FBQztRQUM5QixHQUFHLENBQUMsS0FBSyxDQUFDLGFBQWEsR0FBRyxNQUFNLENBQUM7UUFFakMsTUFBTSxJQUFJLEdBQUcsSUFBSSxDQUFDLFFBQVEsQ0FBQyxlQUFlLENBQUMsTUFBTSxFQUFFLE1BQU0sQ0FBQyxDQUFDO1FBQzNELEdBQUcsQ0FBQyxXQUFXLENBQUMsSUFBSSxDQUFDLENBQUM7UUFFdEIsSUFBSSxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsV0FBVyxDQUFDLEdBQUcsQ0FBQyxDQUFDO1FBQ3BDLElBQUksQ0FBQyxPQUFPLEdBQUcsR0FBRyxDQUFDO1FBQ25CLE9BQU8sR0FBRyxDQUFDO0lBQ2IsQ0FBQzt3R0F4R1Usd0JBQXdCOzRHQUF4Qix3QkFBd0IsY0FEWCxNQUFNOzs0RkFDbkIsd0JBQXdCO2tCQURwQyxVQUFVO21CQUFDLEVBQUUsVUFBVSxFQUFFLE1BQU0sRUFBRSIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IERPQ1VNRU5UIH0gZnJvbSAnQGFuZ3VsYXIvY29tbW9uJztcbmltcG9ydCB7IEluamVjdGFibGUsIGluamVjdCB9IGZyb20gJ0Bhbmd1bGFyL2NvcmUnO1xuXG5jb25zdCBTVkdfTlMgPSAnaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnO1xuY29uc3QgSE9TVF9JRCA9ICduZ3gtbGlxdWlkLWdsYXNzLWZpbHRlci1ob3N0JztcbmNvbnN0IEZJTFRFUl9JRF9QUkVGSVggPSAnbmd4LWxpcXVpZC1nbGFzcy1maWx0ZXInO1xuXG4vKipcbiAqIE93bnMgdGhlIFNWRyBgPGZpbHRlcj5gIGRlZmluaXRpb25zIHRoYXQgcG93ZXIgdGhlIHJlZnJhY3Rpb24gZWZmZWN0IGFuZCBpbmplY3RzIHRoZW0gaW50b1xuICogYGRvY3VtZW50LmJvZHlgLlxuICpcbiAqIGBzY2FsZWAgKGZlRGlzcGxhY2VtZW50TWFwJ3MgYHNjYWxlYCBhdHRyaWJ1dGUpIGlzIHRoZSBvbmUgZmlsdGVyIHBhcmFtZXRlciB0aGF0IGNhbid0IGJlXG4gKiBzaGFyZWQgYWNyb3NzIGFsbCBkaXJlY3RpdmUgaW5zdGFuY2VzIHRoZSB3YXkgdGhlIHJlc3Qgb2YgdGhlIENTUy1jdXN0b20tcHJvcGVydHktZHJpdmVuXG4gKiBzdHlsaW5nIGlzLCBiZWNhdXNlIFNWRyBmaWx0ZXIgcHJpbWl0aXZlcyBkb24ndCBhY2NlcHQgQ1NTIGN1c3RvbSBwcm9wZXJ0aWVzIGFzIGF0dHJpYnV0ZVxuICogdmFsdWVzLiBJbnN0ZWFkIG9mIGdlbmVyYXRpbmcgYSBuZXcgZmlsdGVyIHBlciBpbnN0YW5jZSAod2FzdGVmdWwpIG9yIGEgc2luZ2xlIHNoYXJlZCBmaWx0ZXJcbiAqIChsb3NlcyBwZXItaW5zdGFuY2UgdHVuaW5nKSwgdGhpcyBzZXJ2aWNlIHJlZi1jb3VudHMgb25lIGA8ZmlsdGVyPmAgZWxlbWVudCBwZXIgZGlzdGluY3Qgc2NhbGVcbiAqIHZhbHVlOiBkaXJlY3RpdmVzIHRoYXQgc2hhcmUgYSBzY2FsZSBzaGFyZSBhIGZpbHRlcjsgYSBmaWx0ZXIgaXMgcmVtb3ZlZCBmcm9tIHRoZSBET00gb25jZVxuICogaXRzIGxhc3QgY29uc3VtZXIgaXMgZGVzdHJveWVkLlxuICovXG5ASW5qZWN0YWJsZSh7IHByb3ZpZGVkSW46ICdyb290JyB9KVxuZXhwb3J0IGNsYXNzIExpcXVpZEdsYXNzRmlsdGVyU2VydmljZSB7XG4gIHByaXZhdGUgcmVhZG9ubHkgZG9jdW1lbnQgPSBpbmplY3QoRE9DVU1FTlQpO1xuICBwcml2YXRlIHJlYWRvbmx5IHJlZkNvdW50cyA9IG5ldyBNYXA8bnVtYmVyLCBudW1iZXI+KCk7XG4gIHByaXZhdGUgc3ZnUm9vdDogU1ZHU1ZHRWxlbWVudCB8IG51bGwgPSBudWxsO1xuXG4gIC8qKiBSZWdpc3RlcnMgYSBjb25zdW1lciBvZiB0aGUgZmlsdGVyIGZvciBgc2NhbGVgLCBjcmVhdGluZyBpdCBpZiB0aGlzIGlzIHRoZSBmaXJzdC4gUmV0dXJucyB0aGUgZWxlbWVudCBJRCB0byByZWZlcmVuY2UgdmlhIGB1cmwoI2lkKWAuICovXG4gIGFjcXVpcmUoc2NhbGU6IG51bWJlcik6IHN0cmluZyB7XG4gICAgY29uc3QgaWQgPSB0aGlzLmZpbHRlcklkKHNjYWxlKTtcbiAgICBjb25zdCBjb3VudCA9IHRoaXMucmVmQ291bnRzLmdldChzY2FsZSkgPz8gMDtcbiAgICB0aGlzLnJlZkNvdW50cy5zZXQoc2NhbGUsIGNvdW50ICsgMSk7XG5cbiAgICBpZiAoY291bnQgPT09IDApIHtcbiAgICAgIHRoaXMuaW5qZWN0RmlsdGVyKGlkLCBzY2FsZSk7XG4gICAgfVxuXG4gICAgcmV0dXJuIGlkO1xuICB9XG5cbiAgLyoqIFJlbGVhc2VzIGEgY29uc3VtZXIncyBob2xkIG9uIGBzY2FsZWAncyBmaWx0ZXIsIHJlbW92aW5nIGl0IGZyb20gdGhlIERPTSBvbmNlIG5vIGNvbnN1bWVycyByZW1haW4uICovXG4gIHJlbGVhc2Uoc2NhbGU6IG51bWJlcik6IHZvaWQge1xuICAgIGNvbnN0IGNvdW50ID0gdGhpcy5yZWZDb3VudHMuZ2V0KHNjYWxlKSA/PyAwO1xuICAgIGlmIChjb3VudCA8PSAxKSB7XG4gICAgICB0aGlzLnJlZkNvdW50cy5kZWxldGUoc2NhbGUpO1xuICAgICAgdGhpcy5yZW1vdmVGaWx0ZXIodGhpcy5maWx0ZXJJZChzY2FsZSkpO1xuICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICB0aGlzLnJlZkNvdW50cy5zZXQoc2NhbGUsIGNvdW50IC0gMSk7XG4gIH1cblxuICBwcml2YXRlIGZpbHRlcklkKHNjYWxlOiBudW1iZXIpOiBzdHJpbmcge1xuICAgIHJldHVybiBgJHtGSUxURVJfSURfUFJFRklYfS0ke3NjYWxlfWA7XG4gIH1cblxuICBwcml2YXRlIGluamVjdEZpbHRlcihpZDogc3RyaW5nLCBzY2FsZTogbnVtYmVyKTogdm9pZCB7XG4gICAgLy8gSHlkcmF0aW9uIC8gbXVsdGlwbGUtYXBwLWluc3RhbmNlIGd1YXJkOiBhbm90aGVyIGluc3RhbmNlIChvciBhIHByaW9yIFNTUiBwYXNzKSBtYXkgaGF2ZVxuICAgIC8vIGFscmVhZHkgaW5qZWN0ZWQgdGhpcyBleGFjdCBmaWx0ZXIgaW50byB0aGUgRE9NLlxuICAgIGlmICh0aGlzLmRvY3VtZW50LmdldEVsZW1lbnRCeUlkKGlkKSkge1xuICAgICAgcmV0dXJuO1xuICAgIH1cblxuICAgIGNvbnN0IHN2ZyA9IHRoaXMuZ2V0T3JDcmVhdGVTdmdSb290KCk7XG5cbiAgICBjb25zdCBmaWx0ZXIgPSB0aGlzLmRvY3VtZW50LmNyZWF0ZUVsZW1lbnROUyhTVkdfTlMsICdmaWx0ZXInKTtcbiAgICBmaWx0ZXIuc2V0QXR0cmlidXRlKCdpZCcsIGlkKTtcbiAgICBmaWx0ZXIuc2V0QXR0cmlidXRlKCd4JywgJy0yMCUnKTtcbiAgICBmaWx0ZXIuc2V0QXR0cmlidXRlKCd5JywgJy0yMCUnKTtcbiAgICBmaWx0ZXIuc2V0QXR0cmlidXRlKCd3aWR0aCcsICcxNDAlJyk7XG4gICAgZmlsdGVyLnNldEF0dHJpYnV0ZSgnaGVpZ2h0JywgJzE0MCUnKTtcbiAgICBmaWx0ZXIuc2V0QXR0cmlidXRlKCdjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnMnLCAnc1JHQicpO1xuXG4gICAgY29uc3QgdHVyYnVsZW5jZSA9IHRoaXMuZG9jdW1lbnQuY3JlYXRlRWxlbWVudE5TKFNWR19OUywgJ2ZlVHVyYnVsZW5jZScpO1xuICAgIHR1cmJ1bGVuY2Uuc2V0QXR0cmlidXRlKCd0eXBlJywgJ2ZyYWN0YWxOb2lzZScpO1xuICAgIHR1cmJ1bGVuY2Uuc2V0QXR0cmlidXRlKCdiYXNlRnJlcXVlbmN5JywgJzAuMDA4IDAuMDA4Jyk7XG4gICAgdHVyYnVsZW5jZS5zZXRBdHRyaWJ1dGUoJ251bU9jdGF2ZXMnLCAnMicpO1xuICAgIHR1cmJ1bGVuY2Uuc2V0QXR0cmlidXRlKCdzZWVkJywgJzkyJyk7XG4gICAgdHVyYnVsZW5jZS5zZXRBdHRyaWJ1dGUoJ3Jlc3VsdCcsICdub2lzZScpO1xuXG4gICAgY29uc3QgZ2F1c3NpYW5CbHVyID0gdGhpcy5kb2N1bWVudC5jcmVhdGVFbGVtZW50TlMoU1ZHX05TLCAnZmVHYXVzc2lhbkJsdXInKTtcbiAgICBnYXVzc2lhbkJsdXIuc2V0QXR0cmlidXRlKCdpbicsICdub2lzZScpO1xuICAgIGdhdXNzaWFuQmx1ci5zZXRBdHRyaWJ1dGUoJ3N0ZERldmlhdGlvbicsICcyJyk7XG4gICAgZ2F1c3NpYW5CbHVyLnNldEF0dHJpYnV0ZSgncmVzdWx0JywgJ2JsdXJyZWQnKTtcblxuICAgIGNvbnN0IGRpc3BsYWNlbWVudE1hcCA9IHRoaXMuZG9jdW1lbnQuY3JlYXRlRWxlbWVudE5TKFNWR19OUywgJ2ZlRGlzcGxhY2VtZW50TWFwJyk7XG4gICAgZGlzcGxhY2VtZW50TWFwLnNldEF0dHJpYnV0ZSgnaW4nLCAnU291cmNlR3JhcGhpYycpO1xuICAgIGRpc3BsYWNlbWVudE1hcC5zZXRBdHRyaWJ1dGUoJ2luMicsICdibHVycmVkJyk7XG4gICAgZGlzcGxhY2VtZW50TWFwLnNldEF0dHJpYnV0ZSgnc2NhbGUnLCBTdHJpbmcoc2NhbGUpKTtcbiAgICBkaXNwbGFjZW1lbnRNYXAuc2V0QXR0cmlidXRlKCd4Q2hhbm5lbFNlbGVjdG9yJywgJ1InKTtcbiAgICBkaXNwbGFjZW1lbnRNYXAuc2V0QXR0cmlidXRlKCd5Q2hhbm5lbFNlbGVjdG9yJywgJ0cnKTtcblxuICAgIGZpbHRlci5hcHBlbmQodHVyYnVsZW5jZSwgZ2F1c3NpYW5CbHVyLCBkaXNwbGFjZW1lbnRNYXApO1xuICAgIHN2Zy5xdWVyeVNlbGVjdG9yKCdkZWZzJykhLmFwcGVuZENoaWxkKGZpbHRlcik7XG4gIH1cblxuICBwcml2YXRlIHJlbW92ZUZpbHRlcihpZDogc3RyaW5nKTogdm9pZCB7XG4gICAgdGhpcy5kb2N1bWVudC5nZXRFbGVtZW50QnlJZChpZCk/LnJlbW92ZSgpO1xuICB9XG5cbiAgcHJpdmF0ZSBnZXRPckNyZWF0ZVN2Z1Jvb3QoKTogU1ZHU1ZHRWxlbWVudCB7XG4gICAgaWYgKHRoaXMuc3ZnUm9vdD8uaXNDb25uZWN0ZWQpIHtcbiAgICAgIHJldHVybiB0aGlzLnN2Z1Jvb3Q7XG4gICAgfVxuXG4gICAgY29uc3QgZXhpc3RpbmcgPSB0aGlzLmRvY3VtZW50LmdldEVsZW1lbnRCeUlkKEhPU1RfSUQpO1xuICAgIGlmIChleGlzdGluZyBpbnN0YW5jZW9mIFNWR1NWR0VsZW1lbnQpIHtcbiAgICAgIHRoaXMuc3ZnUm9vdCA9IGV4aXN0aW5nO1xuICAgICAgcmV0dXJuIGV4aXN0aW5nO1xuICAgIH1cblxuICAgIGNvbnN0IHN2ZyA9IHRoaXMuZG9jdW1lbnQuY3JlYXRlRWxlbWVudE5TKFNWR19OUywgJ3N2ZycpIGFzIFNWR1NWR0VsZW1lbnQ7XG4gICAgc3ZnLnNldEF0dHJpYnV0ZSgnaWQnLCBIT1NUX0lEKTtcbiAgICBzdmcuc2V0QXR0cmlidXRlKCdhcmlhLWhpZGRlbicsICd0cnVlJyk7XG4gICAgc3ZnLnNldEF0dHJpYnV0ZSgnZm9jdXNhYmxlJywgJ2ZhbHNlJyk7XG4gICAgc3ZnLnN0eWxlLnBvc2l0aW9uID0gJ2Fic29sdXRlJztcbiAgICBzdmcuc3R5bGUud2lkdGggPSAnMCc7XG4gICAgc3ZnLnN0eWxlLmhlaWdodCA9ICcwJztcbiAgICBzdmcuc3R5bGUub3ZlcmZsb3cgPSAnaGlkZGVuJztcbiAgICBzdmcuc3R5bGUucG9pbnRlckV2ZW50cyA9ICdub25lJztcblxuICAgIGNvbnN0IGRlZnMgPSB0aGlzLmRvY3VtZW50LmNyZWF0ZUVsZW1lbnROUyhTVkdfTlMsICdkZWZzJyk7XG4gICAgc3ZnLmFwcGVuZENoaWxkKGRlZnMpO1xuXG4gICAgdGhpcy5kb2N1bWVudC5ib2R5LmFwcGVuZENoaWxkKHN2Zyk7XG4gICAgdGhpcy5zdmdSb290ID0gc3ZnO1xuICAgIHJldHVybiBzdmc7XG4gIH1cbn1cbiJdfQ==
@@ -0,0 +1,110 @@
1
+ import { Directive, ElementRef, Renderer2, RendererStyleFlags2, afterNextRender, computed, effect, inject, input, signal, } from '@angular/core';
2
+ import { DEFAULT_LIQUID_GLASS_CONFIG } from './liquid-glass-config';
3
+ import { LIQUID_GLASS_CONFIG } from './liquid-glass-config.token';
4
+ import { LiquidGlassFilterService } from './liquid-glass-filter.service';
5
+ import * as i0 from "@angular/core";
6
+ /**
7
+ * Applies the iOS "Liquid Glass" effect to its host element: SVG-based refraction, a frost/tint
8
+ * pass, and specular edge highlights, all driven by CSS custom properties so runtime input
9
+ * changes update live with zero re-render cost. Pair with the shipped `styles/liquid-glass.css`
10
+ * stylesheet, which reads these custom properties on `::before`/`::after` pseudo-elements.
11
+ *
12
+ * Does not restructure host content — safe to apply to buttons, cards, navs, or any element.
13
+ */
14
+ export class LiquidGlassDirective {
15
+ elementRef = inject((ElementRef));
16
+ renderer = inject(Renderer2);
17
+ filterService = inject(LiquidGlassFilterService);
18
+ providedConfig = inject(LIQUID_GLASS_CONFIG, { optional: true }) ?? {};
19
+ /** Flipped by afterNextRender(); gates DOM work so nothing runs before the first browser render (SSR safety) or during it. */
20
+ browserRendered = signal(false);
21
+ displacementScale = input();
22
+ blur = input();
23
+ saturation = input();
24
+ brightness = input();
25
+ tint = input();
26
+ radius = input();
27
+ specular = input();
28
+ resolvedConfig = computed(() => ({
29
+ displacementScale: this.displacementScale() ??
30
+ this.providedConfig.displacementScale ??
31
+ DEFAULT_LIQUID_GLASS_CONFIG.displacementScale,
32
+ blur: this.blur() ?? this.providedConfig.blur ?? DEFAULT_LIQUID_GLASS_CONFIG.blur,
33
+ saturation: this.saturation() ?? this.providedConfig.saturation ?? DEFAULT_LIQUID_GLASS_CONFIG.saturation,
34
+ brightness: this.brightness() ?? this.providedConfig.brightness ?? DEFAULT_LIQUID_GLASS_CONFIG.brightness,
35
+ tint: this.tint() ?? this.providedConfig.tint ?? DEFAULT_LIQUID_GLASS_CONFIG.tint,
36
+ radius: this.radius() ?? this.providedConfig.radius ?? DEFAULT_LIQUID_GLASS_CONFIG.radius,
37
+ specular: this.specular() ?? this.providedConfig.specular ?? DEFAULT_LIQUID_GLASS_CONFIG.specular,
38
+ }));
39
+ acquiredScale = null;
40
+ constructor() {
41
+ afterNextRender(() => {
42
+ this.ensurePositioned();
43
+ this.browserRendered.set(true);
44
+ });
45
+ effect(() => {
46
+ if (!this.browserRendered()) {
47
+ return;
48
+ }
49
+ const config = this.resolvedConfig();
50
+ this.syncFilter(config.displacementScale);
51
+ this.applyCssProperties(config);
52
+ });
53
+ }
54
+ ngOnDestroy() {
55
+ if (this.acquiredScale !== null) {
56
+ this.filterService.release(this.acquiredScale);
57
+ this.acquiredScale = null;
58
+ }
59
+ }
60
+ /** Sets `position: relative` only if the host is unpositioned, so it never clobbers an existing absolute/fixed/sticky layout. */
61
+ ensurePositioned() {
62
+ const host = this.elementRef.nativeElement;
63
+ const currentPosition = getComputedStyle(host).position;
64
+ if (currentPosition === 'static') {
65
+ this.renderer.setStyle(host, 'position', 'relative');
66
+ }
67
+ this.renderer.setStyle(host, 'isolation', 'isolate');
68
+ }
69
+ syncFilter(scale) {
70
+ if (this.acquiredScale === scale) {
71
+ return;
72
+ }
73
+ const previousScale = this.acquiredScale;
74
+ const filterId = this.filterService.acquire(scale);
75
+ this.acquiredScale = scale;
76
+ if (previousScale !== null) {
77
+ this.filterService.release(previousScale);
78
+ }
79
+ this.setCustomProperty('--lg-filter-url', `url(#${filterId})`);
80
+ }
81
+ applyCssProperties(config) {
82
+ this.setCustomProperty('--lg-blur', `${config.blur}px`);
83
+ this.setCustomProperty('--lg-saturation', `${config.saturation}%`);
84
+ this.setCustomProperty('--lg-brightness', `${config.brightness}`);
85
+ this.setCustomProperty('--lg-tint', config.tint);
86
+ this.setCustomProperty('--lg-radius', config.radius);
87
+ this.setCustomProperty('--lg-specular-display', config.specular ? '1' : '0');
88
+ }
89
+ /**
90
+ * Renderer2.setStyle() only calls the DOM's setProperty() (required for custom properties)
91
+ * when RendererStyleFlags2.DashCase is passed; without it, it falls back to bracket-property
92
+ * assignment (`el.style[name] = value`), which is a silent no-op for `--`-prefixed names.
93
+ */
94
+ setCustomProperty(name, value) {
95
+ this.renderer.setStyle(this.elementRef.nativeElement, name, value, RendererStyleFlags2.DashCase);
96
+ }
97
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LiquidGlassDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
98
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "18.2.14", type: LiquidGlassDirective, isStandalone: true, selector: "[liquidGlass]", inputs: { displacementScale: { classPropertyName: "displacementScale", publicName: "displacementScale", isSignal: true, isRequired: false, transformFunction: null }, blur: { classPropertyName: "blur", publicName: "blur", isSignal: true, isRequired: false, transformFunction: null }, saturation: { classPropertyName: "saturation", publicName: "saturation", isSignal: true, isRequired: false, transformFunction: null }, brightness: { classPropertyName: "brightness", publicName: "brightness", isSignal: true, isRequired: false, transformFunction: null }, tint: { classPropertyName: "tint", publicName: "tint", isSignal: true, isRequired: false, transformFunction: null }, radius: { classPropertyName: "radius", publicName: "radius", isSignal: true, isRequired: false, transformFunction: null }, specular: { classPropertyName: "specular", publicName: "specular", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "lg-glass" }, ngImport: i0 });
99
+ }
100
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LiquidGlassDirective, decorators: [{
101
+ type: Directive,
102
+ args: [{
103
+ selector: '[liquidGlass]',
104
+ standalone: true,
105
+ host: {
106
+ class: 'lg-glass',
107
+ },
108
+ }]
109
+ }], ctorParameters: () => [] });
110
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibGlxdWlkLWdsYXNzLmRpcmVjdGl2ZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3Byb2plY3RzL25neC1saXF1aWQtZ2xhc3Mvc3JjL2xpYi9saXF1aWQtZ2xhc3MuZGlyZWN0aXZlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFDTCxTQUFTLEVBQ1QsVUFBVSxFQUNWLFNBQVMsRUFDVCxtQkFBbUIsRUFDbkIsZUFBZSxFQUNmLFFBQVEsRUFDUixNQUFNLEVBQ04sTUFBTSxFQUNOLEtBQUssRUFFTCxNQUFNLEdBRVAsTUFBTSxlQUFlLENBQUM7QUFDdkIsT0FBTyxFQUFFLDJCQUEyQixFQUFxQixNQUFNLHVCQUF1QixDQUFDO0FBQ3ZGLE9BQU8sRUFBRSxtQkFBbUIsRUFBRSxNQUFNLDZCQUE2QixDQUFDO0FBQ2xFLE9BQU8sRUFBRSx3QkFBd0IsRUFBRSxNQUFNLCtCQUErQixDQUFDOztBQUV6RTs7Ozs7OztHQU9HO0FBUUgsTUFBTSxPQUFPLG9CQUFvQjtJQUNkLFVBQVUsR0FBRyxNQUFNLENBQUMsQ0FBQSxVQUF1QixDQUFBLENBQUMsQ0FBQztJQUM3QyxRQUFRLEdBQUcsTUFBTSxDQUFDLFNBQVMsQ0FBQyxDQUFDO0lBQzdCLGFBQWEsR0FBRyxNQUFNLENBQUMsd0JBQXdCLENBQUMsQ0FBQztJQUNqRCxjQUFjLEdBQUcsTUFBTSxDQUFDLG1CQUFtQixFQUFFLEVBQUUsUUFBUSxFQUFFLElBQUksRUFBRSxDQUFDLElBQUksRUFBRSxDQUFDO0lBRXhGLDhIQUE4SDtJQUM3RyxlQUFlLEdBQUcsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDO0lBRXhDLGlCQUFpQixHQUFHLEtBQUssRUFBVSxDQUFDO0lBQ3BDLElBQUksR0FBRyxLQUFLLEVBQVUsQ0FBQztJQUN2QixVQUFVLEdBQUcsS0FBSyxFQUFVLENBQUM7SUFDN0IsVUFBVSxHQUFHLEtBQUssRUFBVSxDQUFDO0lBQzdCLElBQUksR0FBRyxLQUFL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QixJQUFJLEVBQ0osS0FBSyxFQUNMLG1CQUFtQixDQUFDLFFBQVEsQ0FDN0IsQ0FBQztJQUNKLENBQUM7d0dBeEdVLG9CQUFvQjs0RkFBcEIsb0JBQW9COzs0RkFBcEIsb0JBQW9CO2tCQVBoQyxTQUFTO21CQUFDO29CQUNULFFBQVEsRUFBRSxlQUFlO29CQUN6QixVQUFVLEVBQUUsSUFBSTtvQkFDaEIsSUFBSSxFQUFFO3dCQUNKLEtBQUssRUFBRSxVQUFVO3FCQUNsQjtpQkFDRiIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7XG4gIERpcmVjdGl2ZSxcbiAgRWxlbWVudFJlZixcbiAgUmVuZGVyZXIyLFxuICBSZW5kZXJlclN0eWxlRmxhZ3MyLFxuICBhZnRlck5leHRSZW5kZXIsXG4gIGNvbXB1dGVkLFxuICBlZmZlY3QsXG4gIGluamVjdCxcbiAgaW5wdXQsXG4gIE9uRGVzdHJveSxcbiAgc2lnbmFsLFxuICBTaWduYWwsXG59IGZyb20gJ0Bhbmd1bGFyL2NvcmUnO1xuaW1wb3J0IHsgREVGQVVMVF9MSVFVSURfR0xBU1NfQ09ORklHLCBMaXF1aWRHbGFzc0NvbmZpZyB9IGZyb20gJy4vbGlxdWlkLWdsYXNzLWNvbmZpZyc7XG5pbXBvcnQgeyBMSVFVSURfR0xBU1NfQ09ORklHIH0gZnJvbSAnLi9saXF1aWQtZ2xhc3MtY29uZmlnLnRva2VuJztcbmltcG9ydCB7IExpcXVpZEdsYXNzRmlsdGVyU2VydmljZSB9IGZyb20gJy4vbGlxdWlkLWdsYXNzLWZpbHRlci5zZXJ2aWNlJztcblxuLyoqXG4gKiBBcHBsaWVzIHRoZSBpT1MgXCJMaXF1aWQgR2xhc3NcIiBlZmZlY3QgdG8gaXRzIGhvc3QgZWxlbWVudDogU1ZHLWJhc2VkIHJlZnJhY3Rpb24sIGEgZnJvc3QvdGludFxuICogcGFzcywgYW5kIHNwZWN1bGFyIGVkZ2UgaGlnaGxpZ2h0cywgYWxsIGRyaXZlbiBieSBDU1MgY3VzdG9tIHByb3BlcnRpZXMgc28gcnVudGltZSBpbnB1dFxuICogY2hhbmdlcyB1cGRhdGUgbGl2ZSB3aXRoIHplcm8gcmUtcmVuZGVyIGNvc3QuIFBhaXIgd2l0aCB0aGUgc2hpcHBlZCBgc3R5bGVzL2xpcXVpZC1nbGFzcy5jc3NgXG4gKiBzdHlsZXNoZWV0LCB3aGljaCByZWFkcyB0aGVzZSBjdXN0b20gcHJvcGVydGllcyBvbiBgOjpiZWZvcmVgL2A6OmFmdGVyYCBwc2V1ZG8tZWxlbWVudHMuXG4gKlxuICogRG9lcyBub3QgcmVzdHJ1Y3R1cmUgaG9zdCBjb250ZW50IOKAlCBzYWZlIHRvIGFwcGx5IHRvIGJ1dHRvbnMsIGNhcmRzLCBuYXZzLCBvciBhbnkgZWxlbWVudC5cbiAqL1xuQERpcmVjdGl2ZSh7XG4gIHNlbGVjdG9yOiAnW2xpcXVpZEdsYXNzXScsXG4gIHN0YW5kYWxvbmU6IHRydWUsXG4gIGhvc3Q6IHtcbiAgICBjbGFzczogJ2xnLWdsYXNzJyxcbiAgfSxcbn0pXG5leHBvcnQgY2xhc3MgTGlxdWlkR2xhc3NEaXJlY3RpdmUgaW1wbGVtZW50cyBPbkRlc3Ryb3kge1xuICBwcml2YXRlIHJlYWRvbmx5IGVsZW1lbnRSZWYgPSBpbmplY3QoRWxlbWVudFJlZjxIVE1MRWxlbWVudD4pO1xuICBwcml2YXRlIHJlYWRvbmx5IHJlbmRlcmVyID0gaW5qZWN0KFJlbmRlcmVyMik7XG4gIHByaXZhdGUgcmVhZG9ubHkgZmlsdGVyU2VydmljZSA9IGluamVjdChMaXF1aWRHbGFzc0ZpbHRlclNlcnZpY2UpO1xuICBwcml2YXRlIHJlYWRvbmx5IHByb3ZpZGVkQ29uZmlnID0gaW5qZWN0KExJUVVJRF9HTEFTU19DT05GSUcsIHsgb3B0aW9uYWw6IHRydWUgfSkgPz8ge307XG5cbiAgLyoqIEZsaXBwZWQgYnkgYWZ0ZXJOZXh0UmVuZGVyKCk7IGdhdGVzIERPTSB3b3JrIHNvIG5vdGhpbmcgcnVucyBiZWZvcmUgdGhlIGZpcnN0IGJyb3dzZXIgcmVuZGVyIChTU1Igc2FmZXR5KSBvciBkdXJpbmcgaXQuICovXG4gIHByaXZhdGUgcmVhZG9ubHkgYnJvd3NlclJlbmRlcmVkID0gc2lnbmFsKGZhbHNlKTtcblxuICByZWFkb25seSBkaXNwbGFjZW1lbnRTY2FsZSA9IGlucHV0PG51bWJlcj4oKTtcbiAgcmVhZG9ubHkgYmx1ciA9IGlucHV0PG51bWJlcj4oKTtcbiAgcmVhZG9ubHkgc2F0dXJhdGlvbiA9IGlucHV0PG51bWJlcj4oKTtcbiAgcmVhZG9ubHkgYnJpZ2h0bmVzcyA9IGlucHV0PG51bWJlcj4oKTtcbiAgcmVhZG9ubHkgdGludCA9IGlucHV0PHN0cmluZz4oKTtcbiAgcmVhZG9ubHkgcmFkaXVzID0gaW5wdXQ8c3RyaW5nPigpO1xuICByZWFkb25seSBzcGVjdWxhciA9IGlucHV0PGJvb2xlYW4+KCk7XG5cbiAgcHJpdmF0ZSByZWFkb25seSByZXNvbHZlZENvbmZpZzogU2lnbmFsPExpcXVpZEdsYXNzQ29uZmlnPiA9IGNvbXB1dGVkKCgpID0+ICh7XG4gICAgZGlzcGxhY2VtZW50U2NhbGU6XG4gICAgICB0aGlzLmRpc3BsYWNlbWVudFNjYWxlKCkgPz9cbiAgICAgIHRoaXMucHJvdmlkZWRDb25maWcuZGlzcGxhY2VtZW50U2NhbGUgPz9cbiAgICAgIERFRkFVTFRfTElRVUlEX0dMQVNTX0NPTkZJRy5kaXNwbGFjZW1lbnRTY2FsZSxcbiAgICBibHVyOiB0aGlzLmJsdXIoKSA/PyB0aGlzLnByb3ZpZGVkQ29uZmlnLmJsdXIgPz8gREVGQVVMVF9MSVFVSURfR0xBU1NfQ09ORklHLmJsdXIsXG4gICAgc2F0dXJhdGlvbjpcbiAgICAgIHRoaXMuc2F0dXJhdGlvbigpID8/IHRoaXMucHJvdmlkZWRDb25maWcuc2F0dXJhdGlvbiA/PyBERUZBVUxUX0xJUVVJRF9HTEFTU19DT05GSUcuc2F0dXJhdGlvbixcbiAgICBicmlnaHRuZXNzOlxuICAgICAgdGhpcy5icmlnaHRuZXNzKCkgPz8gdGhpcy5wcm92aWRlZENvbmZpZy5icmlnaHRuZXNzID8/IERFRkFVTFRfTElRVUlEX0dMQVNTX0NPTkZJRy5icmlnaHRuZXNzLFxuICAgIHRpbnQ6IHRoaXMudGludCgpID8/IHRoaXMucHJvdmlkZWRDb25maWcudGludCA/PyBERUZBVUxUX0xJUVVJRF9HTEFTU19DT05GSUcudGludCxcbiAgICByYWRpdXM6IHRoaXMucmFkaXVzKCkgPz8gdGhpcy5wcm92aWRlZENvbmZpZy5yYWRpdXMgPz8gREVGQVVMVF9MSVFVSURfR0xBU1NfQ09ORklHLnJhZGl1cyxcbiAgICBzcGVjdWxhcjogdGhpcy5zcGVjdWxhcigpID8/IHRoaXMucHJvdmlkZWRDb25maWcuc3BlY3VsYXIgPz8gREVGQVVMVF9MSVFVSURfR0xBU1NfQ09ORklHLnNwZWN1bGFyLFxuICB9KSk7XG5cbiAgcHJpdmF0ZSBhY3F1aXJlZFNjYWxlOiBudW1iZXIgfCBudWxsID0gbnVsbDtcblxuICBjb25zdHJ1Y3RvcigpIHtcbiAgICBhZnRlck5leHRSZW5kZXIoKCkgPT4ge1xuICAgICAgdGhpcy5lbnN1cmVQb3NpdGlvbmVkKCk7XG4gICAgICB0aGlzLmJyb3dzZXJSZW5kZXJlZC5zZXQodHJ1ZSk7XG4gICAgfSk7XG5cbiAgICBlZmZlY3QoKCkgPT4ge1xuICAgICAgaWYgKCF0aGlzLmJyb3dzZXJSZW5kZXJlZCgpKSB7XG4gICAgICAgIHJldHVybjtcbiAgICAgIH1cbiAgICAgIGNvbnN0IGNvbmZpZyA9IHRoaXMucmVzb2x2ZWRDb25maWcoKTtcbiAgICAgIHRoaXMuc3luY0ZpbHRlcihjb25maWcuZGlzcGxhY2VtZW50U2NhbGUpO1xuICAgICAgdGhpcy5hcHBseUNzc1Byb3BlcnRpZXMoY29uZmlnKTtcbiAgICB9KTtcbiAgfVxuXG4gIG5nT25EZXN0cm95KCk6IHZvaWQge1xuICAgIGlmICh0aGlzLmFjcXVpcmVkU2NhbGUgIT09IG51bGwpIHtcbiAgICAgIHRoaXMuZmlsdGVyU2VydmljZS5yZWxlYXNlKHRoaXMuYWNxdWlyZWRTY2FsZSk7XG4gICAgICB0aGlzLmFjcXVpcmVkU2NhbGUgPSBudWxsO1xuICAgIH1cbiAgfVxuXG4gIC8qKiBTZXRzIGBwb3NpdGlvbjogcmVsYXRpdmVgIG9ubHkgaWYgdGhlIGhvc3QgaXMgdW5wb3NpdGlvbmVkLCBzbyBpdCBuZXZlciBjbG9iYmVycyBhbiBleGlzdGluZyBhYnNvbHV0ZS9maXhlZC9zdGlja3kgbGF5b3V0LiAqL1xuICBwcml2YXRlIGVuc3VyZVBvc2l0aW9uZWQoKTogdm9pZCB7XG4gICAgY29uc3QgaG9zdCA9IHRoaXMuZWxlbWVudFJlZi5uYXRpdmVFbGVtZW50O1xuICAgIGNvbnN0IGN1cnJlbnRQb3NpdGlvbiA9IGdldENvbXB1dGVkU3R5bGUoaG9zdCkucG9zaXRpb247XG4gICAgaWYgKGN1cnJlbnRQb3NpdGlvbiA9PT0gJ3N0YXRpYycpIHtcbiAgICAgIHRoaXMucmVuZGVyZXIuc2V0U3R5bGUoaG9zdCwgJ3Bvc2l0aW9uJywgJ3JlbGF0aXZlJyk7XG4gICAgfVxuICAgIHRoaXMucmVuZGVyZXIuc2V0U3R5bGUoaG9zdCwgJ2lzb2xhdGlvbicsICdpc29sYXRlJyk7XG4gIH1cblxuICBwcml2YXRlIHN5bmNGaWx0ZXIoc2NhbGU6IG51bWJlcik6IHZvaWQge1xuICAgIGlmICh0aGlzLmFjcXVpcmVkU2NhbGUgPT09IHNjYWxlKSB7XG4gICAgICByZXR1cm47XG4gICAgfVxuXG4gICAgY29uc3QgcHJldmlvdXNTY2FsZSA9IHRoaXMuYWNxdWlyZWRTY2FsZTtcbiAgICBjb25zdCBmaWx0ZXJJZCA9IHRoaXMuZmlsdGVyU2VydmljZS5hY3F1aXJlKHNjYWxlKTtcbiAgICB0aGlzLmFjcXVpcmVkU2NhbGUgPSBzY2FsZTtcblxuICAgIGlmIChwcmV2aW91c1NjYWxlICE9PSBudWxsKSB7XG4gICAgICB0aGlzLmZpbHRlclNlcnZpY2UucmVsZWFzZShwcmV2aW91c1NjYWxlKTtcbiAgICB9XG5cbiAgICB0aGlzLnNldEN1c3RvbVByb3BlcnR5KCctLWxnLWZpbHRlci11cmwnLCBgdXJsKCMke2ZpbHRlcklkfSlgKTtcbiAgfVxuXG4gIHByaXZhdGUgYXBwbHlDc3NQcm9wZXJ0aWVzKGNvbmZpZzogTGlxdWlkR2xhc3NDb25maWcpOiB2b2lkIHtcbiAgICB0aGlzLnNldEN1c3RvbVByb3BlcnR5KCctLWxnLWJsdXInLCBgJHtjb25maWcuYmx1cn1weGApO1xuICAgIHRoaXMuc2V0Q3VzdG9tUHJvcGVydHkoJy0tbGctc2F0dXJhdGlvbicsIGAke2NvbmZpZy5zYXR1cmF0aW9ufSVgKTtcbiAgICB0aGlzLnNldEN1c3RvbVByb3BlcnR5KCctLWxnLWJyaWdodG5lc3MnLCBgJHtjb25maWcuYnJpZ2h0bmVzc31gKTtcbiAgICB0aGlzLnNldEN1c3RvbVByb3BlcnR5KCctLWxnLXRpbnQnLCBjb25maWcudGludCk7XG4gICAgdGhpcy5zZXRDdXN0b21Qcm9wZXJ0eSgnLS1sZy1yYWRpdXMnLCBjb25maWcucmFkaXVzKTtcbiAgICB0aGlzLnNldEN1c3RvbVByb3BlcnR5KCctLWxnLXNwZWN1bGFyLWRpc3BsYXknLCBjb25maWcuc3BlY3VsYXIgPyAnMScgOiAnMCcpO1xuICB9XG5cbiAgLyoqXG4gICAqIFJlbmRlcmVyMi5zZXRTdHlsZSgpIG9ubHkgY2FsbHMgdGhlIERPTSdzIHNldFByb3BlcnR5KCkgKHJlcXVpcmVkIGZvciBjdXN0b20gcHJvcGVydGllcylcbiAgICogd2hlbiBSZW5kZXJlclN0eWxlRmxhZ3MyLkRhc2hDYXNlIGlzIHBhc3NlZDsgd2l0aG91dCBpdCwgaXQgZmFsbHMgYmFjayB0byBicmFja2V0LXByb3BlcnR5XG4gICAqIGFzc2lnbm1lbnQgKGBlbC5zdHlsZVtuYW1lXSA9IHZhbHVlYCksIHdoaWNoIGlzIGEgc2lsZW50IG5vLW9wIGZvciBgLS1gLXByZWZpeGVkIG5hbWVzLlxuICAgKi9cbiAgcHJpdmF0ZSBzZXRDdXN0b21Qcm9wZXJ0eShuYW1lOiBzdHJpbmcsIHZhbHVlOiBzdHJpbmcpOiB2b2lkIHtcbiAgICB0aGlzLnJlbmRlcmVyLnNldFN0eWxlKFxuICAgICAgdGhpcy5lbGVtZW50UmVmLm5hdGl2ZUVsZW1lbnQsXG4gICAgICBuYW1lLFxuICAgICAgdmFsdWUsXG4gICAgICBSZW5kZXJlclN0eWxlRmxhZ3MyLkRhc2hDYXNlLFxuICAgICk7XG4gIH1cbn1cbiJdfQ==
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Generated bundle index. Do not edit.
3
+ */
4
+ export * from './public-api';
5
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibmd4LWxpcXVpZC1nbGF6ei5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3Byb2plY3RzL25neC1saXF1aWQtZ2xhc3Mvc3JjL25neC1saXF1aWQtZ2xhenoudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7O0dBRUc7QUFFSCxjQUFjLGNBQWMsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogR2VuZXJhdGVkIGJ1bmRsZSBpbmRleC4gRG8gbm90IGVkaXQuXG4gKi9cblxuZXhwb3J0ICogZnJvbSAnLi9wdWJsaWMtYXBpJztcbiJdfQ==
@@ -0,0 +1,7 @@
1
+ /*
2
+ * Public API Surface of ngx-liquid-glass
3
+ */
4
+ export * from './lib/liquid-glass.directive';
5
+ export * from './lib/liquid-glass-config';
6
+ export * from './lib/liquid-glass-config.token';
7
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHVibGljLWFwaS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3Byb2plY3RzL25neC1saXF1aWQtZ2xhc3Mvc3JjL3B1YmxpYy1hcGkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7O0dBRUc7QUFFSCxjQUFjLDhCQUE4QixDQUFDO0FBQzdDLGNBQWMsMkJBQTJCLENBQUM7QUFDMUMsY0FBYyxpQ0FBaUMsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8qXG4gKiBQdWJsaWMgQVBJIFN1cmZhY2Ugb2Ygbmd4LWxpcXVpZC1nbGFzc1xuICovXG5cbmV4cG9ydCAqIGZyb20gJy4vbGliL2xpcXVpZC1nbGFzcy5kaXJlY3RpdmUnO1xuZXhwb3J0ICogZnJvbSAnLi9saWIvbGlxdWlkLWdsYXNzLWNvbmZpZyc7XG5leHBvcnQgKiBmcm9tICcuL2xpYi9saXF1aWQtZ2xhc3MtY29uZmlnLnRva2VuJztcbiJdfQ==
@@ -0,0 +1,258 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, inject, Injectable, ElementRef, Renderer2, signal, input, computed, afterNextRender, effect, RendererStyleFlags2, Directive } from '@angular/core';
3
+ import { DOCUMENT } from '@angular/common';
4
+
5
+ const DEFAULT_LIQUID_GLASS_CONFIG = {
6
+ displacementScale: 70,
7
+ blur: 3,
8
+ saturation: 180,
9
+ brightness: 1.08,
10
+ tint: 'rgba(255,255,255,0.04)',
11
+ radius: '24px',
12
+ specular: true,
13
+ };
14
+
15
+ /**
16
+ * Holds only the overrides passed to `provideLiquidGlassConfig()`, never the built-in defaults.
17
+ * Keeping it partial lets the directive distinguish "not provided" from "explicitly set to a
18
+ * falsy value" when computing precedence against its own inputs.
19
+ */
20
+ const LIQUID_GLASS_CONFIG = new InjectionToken('LIQUID_GLASS_CONFIG');
21
+ /**
22
+ * Overrides the app-wide Liquid Glass defaults. Directive inputs still take precedence over
23
+ * whatever is configured here.
24
+ *
25
+ * @example
26
+ * bootstrapApplication(AppComponent, {
27
+ * providers: [provideLiquidGlassConfig({ blur: 6, tint: 'rgba(0,0,0,0.08)' })],
28
+ * });
29
+ */
30
+ function provideLiquidGlassConfig(config) {
31
+ return { provide: LIQUID_GLASS_CONFIG, useValue: config };
32
+ }
33
+
34
+ const SVG_NS = 'http://www.w3.org/2000/svg';
35
+ const HOST_ID = 'ngx-liquid-glass-filter-host';
36
+ const FILTER_ID_PREFIX = 'ngx-liquid-glass-filter';
37
+ /**
38
+ * Owns the SVG `<filter>` definitions that power the refraction effect and injects them into
39
+ * `document.body`.
40
+ *
41
+ * `scale` (feDisplacementMap's `scale` attribute) is the one filter parameter that can't be
42
+ * shared across all directive instances the way the rest of the CSS-custom-property-driven
43
+ * styling is, because SVG filter primitives don't accept CSS custom properties as attribute
44
+ * values. Instead of generating a new filter per instance (wasteful) or a single shared filter
45
+ * (loses per-instance tuning), this service ref-counts one `<filter>` element per distinct scale
46
+ * value: directives that share a scale share a filter; a filter is removed from the DOM once
47
+ * its last consumer is destroyed.
48
+ */
49
+ class LiquidGlassFilterService {
50
+ document = inject(DOCUMENT);
51
+ refCounts = new Map();
52
+ svgRoot = null;
53
+ /** Registers a consumer of the filter for `scale`, creating it if this is the first. Returns the element ID to reference via `url(#id)`. */
54
+ acquire(scale) {
55
+ const id = this.filterId(scale);
56
+ const count = this.refCounts.get(scale) ?? 0;
57
+ this.refCounts.set(scale, count + 1);
58
+ if (count === 0) {
59
+ this.injectFilter(id, scale);
60
+ }
61
+ return id;
62
+ }
63
+ /** Releases a consumer's hold on `scale`'s filter, removing it from the DOM once no consumers remain. */
64
+ release(scale) {
65
+ const count = this.refCounts.get(scale) ?? 0;
66
+ if (count <= 1) {
67
+ this.refCounts.delete(scale);
68
+ this.removeFilter(this.filterId(scale));
69
+ return;
70
+ }
71
+ this.refCounts.set(scale, count - 1);
72
+ }
73
+ filterId(scale) {
74
+ return `${FILTER_ID_PREFIX}-${scale}`;
75
+ }
76
+ injectFilter(id, scale) {
77
+ // Hydration / multiple-app-instance guard: another instance (or a prior SSR pass) may have
78
+ // already injected this exact filter into the DOM.
79
+ if (this.document.getElementById(id)) {
80
+ return;
81
+ }
82
+ const svg = this.getOrCreateSvgRoot();
83
+ const filter = this.document.createElementNS(SVG_NS, 'filter');
84
+ filter.setAttribute('id', id);
85
+ filter.setAttribute('x', '-20%');
86
+ filter.setAttribute('y', '-20%');
87
+ filter.setAttribute('width', '140%');
88
+ filter.setAttribute('height', '140%');
89
+ filter.setAttribute('color-interpolation-filters', 'sRGB');
90
+ const turbulence = this.document.createElementNS(SVG_NS, 'feTurbulence');
91
+ turbulence.setAttribute('type', 'fractalNoise');
92
+ turbulence.setAttribute('baseFrequency', '0.008 0.008');
93
+ turbulence.setAttribute('numOctaves', '2');
94
+ turbulence.setAttribute('seed', '92');
95
+ turbulence.setAttribute('result', 'noise');
96
+ const gaussianBlur = this.document.createElementNS(SVG_NS, 'feGaussianBlur');
97
+ gaussianBlur.setAttribute('in', 'noise');
98
+ gaussianBlur.setAttribute('stdDeviation', '2');
99
+ gaussianBlur.setAttribute('result', 'blurred');
100
+ const displacementMap = this.document.createElementNS(SVG_NS, 'feDisplacementMap');
101
+ displacementMap.setAttribute('in', 'SourceGraphic');
102
+ displacementMap.setAttribute('in2', 'blurred');
103
+ displacementMap.setAttribute('scale', String(scale));
104
+ displacementMap.setAttribute('xChannelSelector', 'R');
105
+ displacementMap.setAttribute('yChannelSelector', 'G');
106
+ filter.append(turbulence, gaussianBlur, displacementMap);
107
+ svg.querySelector('defs').appendChild(filter);
108
+ }
109
+ removeFilter(id) {
110
+ this.document.getElementById(id)?.remove();
111
+ }
112
+ getOrCreateSvgRoot() {
113
+ if (this.svgRoot?.isConnected) {
114
+ return this.svgRoot;
115
+ }
116
+ const existing = this.document.getElementById(HOST_ID);
117
+ if (existing instanceof SVGSVGElement) {
118
+ this.svgRoot = existing;
119
+ return existing;
120
+ }
121
+ const svg = this.document.createElementNS(SVG_NS, 'svg');
122
+ svg.setAttribute('id', HOST_ID);
123
+ svg.setAttribute('aria-hidden', 'true');
124
+ svg.setAttribute('focusable', 'false');
125
+ svg.style.position = 'absolute';
126
+ svg.style.width = '0';
127
+ svg.style.height = '0';
128
+ svg.style.overflow = 'hidden';
129
+ svg.style.pointerEvents = 'none';
130
+ const defs = this.document.createElementNS(SVG_NS, 'defs');
131
+ svg.appendChild(defs);
132
+ this.document.body.appendChild(svg);
133
+ this.svgRoot = svg;
134
+ return svg;
135
+ }
136
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LiquidGlassFilterService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
137
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LiquidGlassFilterService, providedIn: 'root' });
138
+ }
139
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LiquidGlassFilterService, decorators: [{
140
+ type: Injectable,
141
+ args: [{ providedIn: 'root' }]
142
+ }] });
143
+
144
+ /**
145
+ * Applies the iOS "Liquid Glass" effect to its host element: SVG-based refraction, a frost/tint
146
+ * pass, and specular edge highlights, all driven by CSS custom properties so runtime input
147
+ * changes update live with zero re-render cost. Pair with the shipped `styles/liquid-glass.css`
148
+ * stylesheet, which reads these custom properties on `::before`/`::after` pseudo-elements.
149
+ *
150
+ * Does not restructure host content — safe to apply to buttons, cards, navs, or any element.
151
+ */
152
+ class LiquidGlassDirective {
153
+ elementRef = inject((ElementRef));
154
+ renderer = inject(Renderer2);
155
+ filterService = inject(LiquidGlassFilterService);
156
+ providedConfig = inject(LIQUID_GLASS_CONFIG, { optional: true }) ?? {};
157
+ /** Flipped by afterNextRender(); gates DOM work so nothing runs before the first browser render (SSR safety) or during it. */
158
+ browserRendered = signal(false);
159
+ displacementScale = input();
160
+ blur = input();
161
+ saturation = input();
162
+ brightness = input();
163
+ tint = input();
164
+ radius = input();
165
+ specular = input();
166
+ resolvedConfig = computed(() => ({
167
+ displacementScale: this.displacementScale() ??
168
+ this.providedConfig.displacementScale ??
169
+ DEFAULT_LIQUID_GLASS_CONFIG.displacementScale,
170
+ blur: this.blur() ?? this.providedConfig.blur ?? DEFAULT_LIQUID_GLASS_CONFIG.blur,
171
+ saturation: this.saturation() ?? this.providedConfig.saturation ?? DEFAULT_LIQUID_GLASS_CONFIG.saturation,
172
+ brightness: this.brightness() ?? this.providedConfig.brightness ?? DEFAULT_LIQUID_GLASS_CONFIG.brightness,
173
+ tint: this.tint() ?? this.providedConfig.tint ?? DEFAULT_LIQUID_GLASS_CONFIG.tint,
174
+ radius: this.radius() ?? this.providedConfig.radius ?? DEFAULT_LIQUID_GLASS_CONFIG.radius,
175
+ specular: this.specular() ?? this.providedConfig.specular ?? DEFAULT_LIQUID_GLASS_CONFIG.specular,
176
+ }));
177
+ acquiredScale = null;
178
+ constructor() {
179
+ afterNextRender(() => {
180
+ this.ensurePositioned();
181
+ this.browserRendered.set(true);
182
+ });
183
+ effect(() => {
184
+ if (!this.browserRendered()) {
185
+ return;
186
+ }
187
+ const config = this.resolvedConfig();
188
+ this.syncFilter(config.displacementScale);
189
+ this.applyCssProperties(config);
190
+ });
191
+ }
192
+ ngOnDestroy() {
193
+ if (this.acquiredScale !== null) {
194
+ this.filterService.release(this.acquiredScale);
195
+ this.acquiredScale = null;
196
+ }
197
+ }
198
+ /** Sets `position: relative` only if the host is unpositioned, so it never clobbers an existing absolute/fixed/sticky layout. */
199
+ ensurePositioned() {
200
+ const host = this.elementRef.nativeElement;
201
+ const currentPosition = getComputedStyle(host).position;
202
+ if (currentPosition === 'static') {
203
+ this.renderer.setStyle(host, 'position', 'relative');
204
+ }
205
+ this.renderer.setStyle(host, 'isolation', 'isolate');
206
+ }
207
+ syncFilter(scale) {
208
+ if (this.acquiredScale === scale) {
209
+ return;
210
+ }
211
+ const previousScale = this.acquiredScale;
212
+ const filterId = this.filterService.acquire(scale);
213
+ this.acquiredScale = scale;
214
+ if (previousScale !== null) {
215
+ this.filterService.release(previousScale);
216
+ }
217
+ this.setCustomProperty('--lg-filter-url', `url(#${filterId})`);
218
+ }
219
+ applyCssProperties(config) {
220
+ this.setCustomProperty('--lg-blur', `${config.blur}px`);
221
+ this.setCustomProperty('--lg-saturation', `${config.saturation}%`);
222
+ this.setCustomProperty('--lg-brightness', `${config.brightness}`);
223
+ this.setCustomProperty('--lg-tint', config.tint);
224
+ this.setCustomProperty('--lg-radius', config.radius);
225
+ this.setCustomProperty('--lg-specular-display', config.specular ? '1' : '0');
226
+ }
227
+ /**
228
+ * Renderer2.setStyle() only calls the DOM's setProperty() (required for custom properties)
229
+ * when RendererStyleFlags2.DashCase is passed; without it, it falls back to bracket-property
230
+ * assignment (`el.style[name] = value`), which is a silent no-op for `--`-prefixed names.
231
+ */
232
+ setCustomProperty(name, value) {
233
+ this.renderer.setStyle(this.elementRef.nativeElement, name, value, RendererStyleFlags2.DashCase);
234
+ }
235
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LiquidGlassDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
236
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "18.2.14", type: LiquidGlassDirective, isStandalone: true, selector: "[liquidGlass]", inputs: { displacementScale: { classPropertyName: "displacementScale", publicName: "displacementScale", isSignal: true, isRequired: false, transformFunction: null }, blur: { classPropertyName: "blur", publicName: "blur", isSignal: true, isRequired: false, transformFunction: null }, saturation: { classPropertyName: "saturation", publicName: "saturation", isSignal: true, isRequired: false, transformFunction: null }, brightness: { classPropertyName: "brightness", publicName: "brightness", isSignal: true, isRequired: false, transformFunction: null }, tint: { classPropertyName: "tint", publicName: "tint", isSignal: true, isRequired: false, transformFunction: null }, radius: { classPropertyName: "radius", publicName: "radius", isSignal: true, isRequired: false, transformFunction: null }, specular: { classPropertyName: "specular", publicName: "specular", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "lg-glass" }, ngImport: i0 });
237
+ }
238
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LiquidGlassDirective, decorators: [{
239
+ type: Directive,
240
+ args: [{
241
+ selector: '[liquidGlass]',
242
+ standalone: true,
243
+ host: {
244
+ class: 'lg-glass',
245
+ },
246
+ }]
247
+ }], ctorParameters: () => [] });
248
+
249
+ /*
250
+ * Public API Surface of ngx-liquid-glass
251
+ */
252
+
253
+ /**
254
+ * Generated bundle index. Do not edit.
255
+ */
256
+
257
+ export { DEFAULT_LIQUID_GLASS_CONFIG, LIQUID_GLASS_CONFIG, LiquidGlassDirective, provideLiquidGlassConfig };
258
+ //# sourceMappingURL=ngx-liquid-glazz.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ngx-liquid-glazz.mjs","sources":["../../../projects/ngx-liquid-glass/src/lib/liquid-glass-config.ts","../../../projects/ngx-liquid-glass/src/lib/liquid-glass-config.token.ts","../../../projects/ngx-liquid-glass/src/lib/liquid-glass-filter.service.ts","../../../projects/ngx-liquid-glass/src/lib/liquid-glass.directive.ts","../../../projects/ngx-liquid-glass/src/public-api.ts","../../../projects/ngx-liquid-glass/src/ngx-liquid-glazz.ts"],"sourcesContent":["/**\n * App-wide tunables for the Liquid Glass effect.\n * Every field is optional so callers can override just the defaults they care about.\n * Precedence (highest to lowest): directive input > provideLiquidGlassConfig() > built-in default.\n */\nexport interface LiquidGlassConfig {\n /** Refraction strength, maps to the SVG `feDisplacementMap` `scale` attribute. */\n displacementScale: number;\n /** Frost blur in px, maps to `--lg-blur`. */\n blur: number;\n /** Backdrop saturation percent, maps to `--lg-saturation`. */\n saturation: number;\n /** Backdrop brightness multiplier, maps to `--lg-brightness`. */\n brightness: number;\n /** Tint color layered over the refracted backdrop, maps to `--lg-tint`. */\n tint: string;\n /** Corner radius, maps to `--lg-radius`. */\n radius: string;\n /** Whether to render the specular edge-highlight inset shadows. */\n specular: boolean;\n}\n\nexport const DEFAULT_LIQUID_GLASS_CONFIG: LiquidGlassConfig = {\n displacementScale: 70,\n blur: 3,\n saturation: 180,\n brightness: 1.08,\n tint: 'rgba(255,255,255,0.04)',\n radius: '24px',\n specular: true,\n};\n","import { InjectionToken, Provider } from '@angular/core';\nimport { LiquidGlassConfig } from './liquid-glass-config';\n\n/**\n * Holds only the overrides passed to `provideLiquidGlassConfig()`, never the built-in defaults.\n * Keeping it partial lets the directive distinguish \"not provided\" from \"explicitly set to a\n * falsy value\" when computing precedence against its own inputs.\n */\nexport const LIQUID_GLASS_CONFIG = new InjectionToken<Partial<LiquidGlassConfig>>(\n 'LIQUID_GLASS_CONFIG',\n);\n\n/**\n * Overrides the app-wide Liquid Glass defaults. Directive inputs still take precedence over\n * whatever is configured here.\n *\n * @example\n * bootstrapApplication(AppComponent, {\n * providers: [provideLiquidGlassConfig({ blur: 6, tint: 'rgba(0,0,0,0.08)' })],\n * });\n */\nexport function provideLiquidGlassConfig(config: Partial<LiquidGlassConfig>): Provider {\n return { provide: LIQUID_GLASS_CONFIG, useValue: config };\n}\n","import { DOCUMENT } from '@angular/common';\nimport { Injectable, inject } from '@angular/core';\n\nconst SVG_NS = 'http://www.w3.org/2000/svg';\nconst HOST_ID = 'ngx-liquid-glass-filter-host';\nconst FILTER_ID_PREFIX = 'ngx-liquid-glass-filter';\n\n/**\n * Owns the SVG `<filter>` definitions that power the refraction effect and injects them into\n * `document.body`.\n *\n * `scale` (feDisplacementMap's `scale` attribute) is the one filter parameter that can't be\n * shared across all directive instances the way the rest of the CSS-custom-property-driven\n * styling is, because SVG filter primitives don't accept CSS custom properties as attribute\n * values. Instead of generating a new filter per instance (wasteful) or a single shared filter\n * (loses per-instance tuning), this service ref-counts one `<filter>` element per distinct scale\n * value: directives that share a scale share a filter; a filter is removed from the DOM once\n * its last consumer is destroyed.\n */\n@Injectable({ providedIn: 'root' })\nexport class LiquidGlassFilterService {\n private readonly document = inject(DOCUMENT);\n private readonly refCounts = new Map<number, number>();\n private svgRoot: SVGSVGElement | null = null;\n\n /** Registers a consumer of the filter for `scale`, creating it if this is the first. Returns the element ID to reference via `url(#id)`. */\n acquire(scale: number): string {\n const id = this.filterId(scale);\n const count = this.refCounts.get(scale) ?? 0;\n this.refCounts.set(scale, count + 1);\n\n if (count === 0) {\n this.injectFilter(id, scale);\n }\n\n return id;\n }\n\n /** Releases a consumer's hold on `scale`'s filter, removing it from the DOM once no consumers remain. */\n release(scale: number): void {\n const count = this.refCounts.get(scale) ?? 0;\n if (count <= 1) {\n this.refCounts.delete(scale);\n this.removeFilter(this.filterId(scale));\n return;\n }\n this.refCounts.set(scale, count - 1);\n }\n\n private filterId(scale: number): string {\n return `${FILTER_ID_PREFIX}-${scale}`;\n }\n\n private injectFilter(id: string, scale: number): void {\n // Hydration / multiple-app-instance guard: another instance (or a prior SSR pass) may have\n // already injected this exact filter into the DOM.\n if (this.document.getElementById(id)) {\n return;\n }\n\n const svg = this.getOrCreateSvgRoot();\n\n const filter = this.document.createElementNS(SVG_NS, 'filter');\n filter.setAttribute('id', id);\n filter.setAttribute('x', '-20%');\n filter.setAttribute('y', '-20%');\n filter.setAttribute('width', '140%');\n filter.setAttribute('height', '140%');\n filter.setAttribute('color-interpolation-filters', 'sRGB');\n\n const turbulence = this.document.createElementNS(SVG_NS, 'feTurbulence');\n turbulence.setAttribute('type', 'fractalNoise');\n turbulence.setAttribute('baseFrequency', '0.008 0.008');\n turbulence.setAttribute('numOctaves', '2');\n turbulence.setAttribute('seed', '92');\n turbulence.setAttribute('result', 'noise');\n\n const gaussianBlur = this.document.createElementNS(SVG_NS, 'feGaussianBlur');\n gaussianBlur.setAttribute('in', 'noise');\n gaussianBlur.setAttribute('stdDeviation', '2');\n gaussianBlur.setAttribute('result', 'blurred');\n\n const displacementMap = this.document.createElementNS(SVG_NS, 'feDisplacementMap');\n displacementMap.setAttribute('in', 'SourceGraphic');\n displacementMap.setAttribute('in2', 'blurred');\n displacementMap.setAttribute('scale', String(scale));\n displacementMap.setAttribute('xChannelSelector', 'R');\n displacementMap.setAttribute('yChannelSelector', 'G');\n\n filter.append(turbulence, gaussianBlur, displacementMap);\n svg.querySelector('defs')!.appendChild(filter);\n }\n\n private removeFilter(id: string): void {\n this.document.getElementById(id)?.remove();\n }\n\n private getOrCreateSvgRoot(): SVGSVGElement {\n if (this.svgRoot?.isConnected) {\n return this.svgRoot;\n }\n\n const existing = this.document.getElementById(HOST_ID);\n if (existing instanceof SVGSVGElement) {\n this.svgRoot = existing;\n return existing;\n }\n\n const svg = this.document.createElementNS(SVG_NS, 'svg') as SVGSVGElement;\n svg.setAttribute('id', HOST_ID);\n svg.setAttribute('aria-hidden', 'true');\n svg.setAttribute('focusable', 'false');\n svg.style.position = 'absolute';\n svg.style.width = '0';\n svg.style.height = '0';\n svg.style.overflow = 'hidden';\n svg.style.pointerEvents = 'none';\n\n const defs = this.document.createElementNS(SVG_NS, 'defs');\n svg.appendChild(defs);\n\n this.document.body.appendChild(svg);\n this.svgRoot = svg;\n return svg;\n }\n}\n","import {\n Directive,\n ElementRef,\n Renderer2,\n RendererStyleFlags2,\n afterNextRender,\n computed,\n effect,\n inject,\n input,\n OnDestroy,\n signal,\n Signal,\n} from '@angular/core';\nimport { DEFAULT_LIQUID_GLASS_CONFIG, LiquidGlassConfig } from './liquid-glass-config';\nimport { LIQUID_GLASS_CONFIG } from './liquid-glass-config.token';\nimport { LiquidGlassFilterService } from './liquid-glass-filter.service';\n\n/**\n * Applies the iOS \"Liquid Glass\" effect to its host element: SVG-based refraction, a frost/tint\n * pass, and specular edge highlights, all driven by CSS custom properties so runtime input\n * changes update live with zero re-render cost. Pair with the shipped `styles/liquid-glass.css`\n * stylesheet, which reads these custom properties on `::before`/`::after` pseudo-elements.\n *\n * Does not restructure host content — safe to apply to buttons, cards, navs, or any element.\n */\n@Directive({\n selector: '[liquidGlass]',\n standalone: true,\n host: {\n class: 'lg-glass',\n },\n})\nexport class LiquidGlassDirective implements OnDestroy {\n private readonly elementRef = inject(ElementRef<HTMLElement>);\n private readonly renderer = inject(Renderer2);\n private readonly filterService = inject(LiquidGlassFilterService);\n private readonly providedConfig = inject(LIQUID_GLASS_CONFIG, { optional: true }) ?? {};\n\n /** Flipped by afterNextRender(); gates DOM work so nothing runs before the first browser render (SSR safety) or during it. */\n private readonly browserRendered = signal(false);\n\n readonly displacementScale = input<number>();\n readonly blur = input<number>();\n readonly saturation = input<number>();\n readonly brightness = input<number>();\n readonly tint = input<string>();\n readonly radius = input<string>();\n readonly specular = input<boolean>();\n\n private readonly resolvedConfig: Signal<LiquidGlassConfig> = computed(() => ({\n displacementScale:\n this.displacementScale() ??\n this.providedConfig.displacementScale ??\n DEFAULT_LIQUID_GLASS_CONFIG.displacementScale,\n blur: this.blur() ?? this.providedConfig.blur ?? DEFAULT_LIQUID_GLASS_CONFIG.blur,\n saturation:\n this.saturation() ?? this.providedConfig.saturation ?? DEFAULT_LIQUID_GLASS_CONFIG.saturation,\n brightness:\n this.brightness() ?? this.providedConfig.brightness ?? DEFAULT_LIQUID_GLASS_CONFIG.brightness,\n tint: this.tint() ?? this.providedConfig.tint ?? DEFAULT_LIQUID_GLASS_CONFIG.tint,\n radius: this.radius() ?? this.providedConfig.radius ?? DEFAULT_LIQUID_GLASS_CONFIG.radius,\n specular: this.specular() ?? this.providedConfig.specular ?? DEFAULT_LIQUID_GLASS_CONFIG.specular,\n }));\n\n private acquiredScale: number | null = null;\n\n constructor() {\n afterNextRender(() => {\n this.ensurePositioned();\n this.browserRendered.set(true);\n });\n\n effect(() => {\n if (!this.browserRendered()) {\n return;\n }\n const config = this.resolvedConfig();\n this.syncFilter(config.displacementScale);\n this.applyCssProperties(config);\n });\n }\n\n ngOnDestroy(): void {\n if (this.acquiredScale !== null) {\n this.filterService.release(this.acquiredScale);\n this.acquiredScale = null;\n }\n }\n\n /** Sets `position: relative` only if the host is unpositioned, so it never clobbers an existing absolute/fixed/sticky layout. */\n private ensurePositioned(): void {\n const host = this.elementRef.nativeElement;\n const currentPosition = getComputedStyle(host).position;\n if (currentPosition === 'static') {\n this.renderer.setStyle(host, 'position', 'relative');\n }\n this.renderer.setStyle(host, 'isolation', 'isolate');\n }\n\n private syncFilter(scale: number): void {\n if (this.acquiredScale === scale) {\n return;\n }\n\n const previousScale = this.acquiredScale;\n const filterId = this.filterService.acquire(scale);\n this.acquiredScale = scale;\n\n if (previousScale !== null) {\n this.filterService.release(previousScale);\n }\n\n this.setCustomProperty('--lg-filter-url', `url(#${filterId})`);\n }\n\n private applyCssProperties(config: LiquidGlassConfig): void {\n this.setCustomProperty('--lg-blur', `${config.blur}px`);\n this.setCustomProperty('--lg-saturation', `${config.saturation}%`);\n this.setCustomProperty('--lg-brightness', `${config.brightness}`);\n this.setCustomProperty('--lg-tint', config.tint);\n this.setCustomProperty('--lg-radius', config.radius);\n this.setCustomProperty('--lg-specular-display', config.specular ? '1' : '0');\n }\n\n /**\n * Renderer2.setStyle() only calls the DOM's setProperty() (required for custom properties)\n * when RendererStyleFlags2.DashCase is passed; without it, it falls back to bracket-property\n * assignment (`el.style[name] = value`), which is a silent no-op for `--`-prefixed names.\n */\n private setCustomProperty(name: string, value: string): void {\n this.renderer.setStyle(\n this.elementRef.nativeElement,\n name,\n value,\n RendererStyleFlags2.DashCase,\n );\n }\n}\n","/*\n * Public API Surface of ngx-liquid-glass\n */\n\nexport * from './lib/liquid-glass.directive';\nexport * from './lib/liquid-glass-config';\nexport * from './lib/liquid-glass-config.token';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
package/index.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Generated bundle index. Do not edit.
3
+ */
4
+ /// <amd-module name="ngx-liquid-glazz" />
5
+ export * from './public-api';
@@ -0,0 +1,22 @@
1
+ /**
2
+ * App-wide tunables for the Liquid Glass effect.
3
+ * Every field is optional so callers can override just the defaults they care about.
4
+ * Precedence (highest to lowest): directive input > provideLiquidGlassConfig() > built-in default.
5
+ */
6
+ export interface LiquidGlassConfig {
7
+ /** Refraction strength, maps to the SVG `feDisplacementMap` `scale` attribute. */
8
+ displacementScale: number;
9
+ /** Frost blur in px, maps to `--lg-blur`. */
10
+ blur: number;
11
+ /** Backdrop saturation percent, maps to `--lg-saturation`. */
12
+ saturation: number;
13
+ /** Backdrop brightness multiplier, maps to `--lg-brightness`. */
14
+ brightness: number;
15
+ /** Tint color layered over the refracted backdrop, maps to `--lg-tint`. */
16
+ tint: string;
17
+ /** Corner radius, maps to `--lg-radius`. */
18
+ radius: string;
19
+ /** Whether to render the specular edge-highlight inset shadows. */
20
+ specular: boolean;
21
+ }
22
+ export declare const DEFAULT_LIQUID_GLASS_CONFIG: LiquidGlassConfig;
@@ -0,0 +1,18 @@
1
+ import { InjectionToken, Provider } from '@angular/core';
2
+ import { LiquidGlassConfig } from './liquid-glass-config';
3
+ /**
4
+ * Holds only the overrides passed to `provideLiquidGlassConfig()`, never the built-in defaults.
5
+ * Keeping it partial lets the directive distinguish "not provided" from "explicitly set to a
6
+ * falsy value" when computing precedence against its own inputs.
7
+ */
8
+ export declare const LIQUID_GLASS_CONFIG: InjectionToken<Partial<LiquidGlassConfig>>;
9
+ /**
10
+ * Overrides the app-wide Liquid Glass defaults. Directive inputs still take precedence over
11
+ * whatever is configured here.
12
+ *
13
+ * @example
14
+ * bootstrapApplication(AppComponent, {
15
+ * providers: [provideLiquidGlassConfig({ blur: 6, tint: 'rgba(0,0,0,0.08)' })],
16
+ * });
17
+ */
18
+ export declare function provideLiquidGlassConfig(config: Partial<LiquidGlassConfig>): Provider;
@@ -0,0 +1,28 @@
1
+ import * as i0 from "@angular/core";
2
+ /**
3
+ * Owns the SVG `<filter>` definitions that power the refraction effect and injects them into
4
+ * `document.body`.
5
+ *
6
+ * `scale` (feDisplacementMap's `scale` attribute) is the one filter parameter that can't be
7
+ * shared across all directive instances the way the rest of the CSS-custom-property-driven
8
+ * styling is, because SVG filter primitives don't accept CSS custom properties as attribute
9
+ * values. Instead of generating a new filter per instance (wasteful) or a single shared filter
10
+ * (loses per-instance tuning), this service ref-counts one `<filter>` element per distinct scale
11
+ * value: directives that share a scale share a filter; a filter is removed from the DOM once
12
+ * its last consumer is destroyed.
13
+ */
14
+ export declare class LiquidGlassFilterService {
15
+ private readonly document;
16
+ private readonly refCounts;
17
+ private svgRoot;
18
+ /** Registers a consumer of the filter for `scale`, creating it if this is the first. Returns the element ID to reference via `url(#id)`. */
19
+ acquire(scale: number): string;
20
+ /** Releases a consumer's hold on `scale`'s filter, removing it from the DOM once no consumers remain. */
21
+ release(scale: number): void;
22
+ private filterId;
23
+ private injectFilter;
24
+ private removeFilter;
25
+ private getOrCreateSvgRoot;
26
+ static ɵfac: i0.ɵɵFactoryDeclaration<LiquidGlassFilterService, never>;
27
+ static ɵprov: i0.ɵɵInjectableDeclaration<LiquidGlassFilterService>;
28
+ }
@@ -0,0 +1,41 @@
1
+ import { OnDestroy } from '@angular/core';
2
+ import * as i0 from "@angular/core";
3
+ /**
4
+ * Applies the iOS "Liquid Glass" effect to its host element: SVG-based refraction, a frost/tint
5
+ * pass, and specular edge highlights, all driven by CSS custom properties so runtime input
6
+ * changes update live with zero re-render cost. Pair with the shipped `styles/liquid-glass.css`
7
+ * stylesheet, which reads these custom properties on `::before`/`::after` pseudo-elements.
8
+ *
9
+ * Does not restructure host content — safe to apply to buttons, cards, navs, or any element.
10
+ */
11
+ export declare class LiquidGlassDirective implements OnDestroy {
12
+ private readonly elementRef;
13
+ private readonly renderer;
14
+ private readonly filterService;
15
+ private readonly providedConfig;
16
+ /** Flipped by afterNextRender(); gates DOM work so nothing runs before the first browser render (SSR safety) or during it. */
17
+ private readonly browserRendered;
18
+ readonly displacementScale: import("@angular/core").InputSignal<number | undefined>;
19
+ readonly blur: import("@angular/core").InputSignal<number | undefined>;
20
+ readonly saturation: import("@angular/core").InputSignal<number | undefined>;
21
+ readonly brightness: import("@angular/core").InputSignal<number | undefined>;
22
+ readonly tint: import("@angular/core").InputSignal<string | undefined>;
23
+ readonly radius: import("@angular/core").InputSignal<string | undefined>;
24
+ readonly specular: import("@angular/core").InputSignal<boolean | undefined>;
25
+ private readonly resolvedConfig;
26
+ private acquiredScale;
27
+ constructor();
28
+ ngOnDestroy(): void;
29
+ /** Sets `position: relative` only if the host is unpositioned, so it never clobbers an existing absolute/fixed/sticky layout. */
30
+ private ensurePositioned;
31
+ private syncFilter;
32
+ private applyCssProperties;
33
+ /**
34
+ * Renderer2.setStyle() only calls the DOM's setProperty() (required for custom properties)
35
+ * when RendererStyleFlags2.DashCase is passed; without it, it falls back to bracket-property
36
+ * assignment (`el.style[name] = value`), which is a silent no-op for `--`-prefixed names.
37
+ */
38
+ private setCustomProperty;
39
+ static ɵfac: i0.ɵɵFactoryDeclaration<LiquidGlassDirective, never>;
40
+ static ɵdir: i0.ɵɵDirectiveDeclaration<LiquidGlassDirective, "[liquidGlass]", never, { "displacementScale": { "alias": "displacementScale"; "required": false; "isSignal": true; }; "blur": { "alias": "blur"; "required": false; "isSignal": true; }; "saturation": { "alias": "saturation"; "required": false; "isSignal": true; }; "brightness": { "alias": "brightness"; "required": false; "isSignal": true; }; "tint": { "alias": "tint"; "required": false; "isSignal": true; }; "radius": { "alias": "radius"; "required": false; "isSignal": true; }; "specular": { "alias": "specular"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
41
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "ngx-liquid-glazz",
3
+ "version": "0.1.0",
4
+ "description": "Real iOS-style Liquid Glass (refraction, not just blur) for any Angular element via an SVG displacement filter directive.",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "angular",
8
+ "liquid-glass",
9
+ "glassmorphism",
10
+ "backdrop-filter",
11
+ "ios",
12
+ "ui",
13
+ "directive",
14
+ "frosted-glass",
15
+ "refraction"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/z4kettan/ios_liquid_glass.git"
20
+ },
21
+ "homepage": "https://github.com/z4kettan/ios_liquid_glass#readme",
22
+ "bugs": {
23
+ "url": "https://github.com/z4kettan/ios_liquid_glass/issues"
24
+ },
25
+ "peerDependencies": {
26
+ "@angular/common": ">=17.1.0 <23.0.0",
27
+ "@angular/core": ">=17.1.0 <23.0.0"
28
+ },
29
+ "dependencies": {
30
+ "tslib": "^2.3.0"
31
+ },
32
+ "sideEffects": false,
33
+ "exports": {
34
+ "./styles": {
35
+ "default": "./styles/liquid-glass.css"
36
+ },
37
+ "./package.json": {
38
+ "default": "./package.json"
39
+ },
40
+ ".": {
41
+ "types": "./index.d.ts",
42
+ "esm2022": "./esm2022/ngx-liquid-glazz.mjs",
43
+ "esm": "./esm2022/ngx-liquid-glazz.mjs",
44
+ "default": "./fesm2022/ngx-liquid-glazz.mjs"
45
+ }
46
+ },
47
+ "module": "fesm2022/ngx-liquid-glazz.mjs",
48
+ "typings": "index.d.ts"
49
+ }
@@ -0,0 +1,3 @@
1
+ export * from './lib/liquid-glass.directive';
2
+ export * from './lib/liquid-glass-config';
3
+ export * from './lib/liquid-glass-config.token';
@@ -0,0 +1,78 @@
1
+ /*
2
+ * ngx-liquid-glass — base stylesheet.
3
+ * Import once, globally: `@import 'ngx-liquid-glass/styles';` (or the equivalent for your
4
+ * bundler). The [liquidGlass] directive sets the --lg-* custom properties consumed below; it
5
+ * does not restructure host content, so the effect is built entirely from ::before/::after
6
+ * pseudo-elements plus one inset box-shadow set on the host itself.
7
+ */
8
+
9
+ .lg-glass {
10
+ --lg-blur: 3px;
11
+ --lg-saturation: 180%;
12
+ --lg-brightness: 1.08;
13
+ --lg-tint: rgba(255, 255, 255, 0.04);
14
+ --lg-radius: 24px;
15
+ --lg-specular-display: 1;
16
+ --lg-filter-url: none;
17
+
18
+ border-radius: var(--lg-radius);
19
+ overflow: visible;
20
+ /* Neutralize UA form-control chrome (buttons in particular ship an opaque background and
21
+ border) so the glass layers, sitting behind the host in z-index, are actually visible. */
22
+ background: none;
23
+ border: none;
24
+ }
25
+
26
+ /* Layer 1: refraction. Chromium-only (backdrop-filter: url(#svg-filter) has no effect anywhere
27
+ else), so it is additive with the frost/tint layer below rather than a hard dependency. */
28
+ .lg-glass::before {
29
+ content: '';
30
+ position: absolute;
31
+ inset: 0;
32
+ z-index: -2;
33
+ border-radius: inherit;
34
+ backdrop-filter: var(--lg-filter-url) blur(var(--lg-blur)) saturate(var(--lg-saturation))
35
+ brightness(var(--lg-brightness));
36
+ -webkit-backdrop-filter: blur(var(--lg-blur)) saturate(var(--lg-saturation))
37
+ brightness(var(--lg-brightness));
38
+ pointer-events: none;
39
+ }
40
+
41
+ /* Layer 2: frost/tint pass, always present regardless of backdrop-filter(url()) support. */
42
+ .lg-glass::after {
43
+ content: '';
44
+ position: absolute;
45
+ inset: 0;
46
+ z-index: -1;
47
+ border-radius: inherit;
48
+ background: var(--lg-tint);
49
+ pointer-events: none;
50
+ }
51
+
52
+ /* Layer 3: specular edge highlights, toggled via --lg-specular-display (set by the `specular`
53
+ input) since a bare custom property can't be used as a CSS property value directly. */
54
+ .lg-glass {
55
+ box-shadow:
56
+ inset 0 1px 1px rgba(255, 255, 255, calc(0.35 * var(--lg-specular-display))),
57
+ inset 0 -1px 1px rgba(255, 255, 255, calc(0.12 * var(--lg-specular-display))),
58
+ inset 1px 0 1px rgba(255, 255, 255, calc(0.08 * var(--lg-specular-display))),
59
+ inset -1px 0 1px rgba(255, 255, 255, calc(0.08 * var(--lg-specular-display)));
60
+ transition: box-shadow 0.2s ease-out;
61
+ }
62
+
63
+ /* Refraction (feDisplacementMap via backdrop-filter: url()) is Chromium-only today. Safari and
64
+ Firefox either ignore the url() filter function or reject the whole backdrop-filter
65
+ declaration; either way they fall through to a plain frosted-glass look rather than looking
66
+ broken. */
67
+ @supports not (backdrop-filter: url('#x')) {
68
+ .lg-glass::before {
69
+ backdrop-filter: blur(14px) saturate(180%);
70
+ -webkit-backdrop-filter: blur(14px) saturate(180%);
71
+ }
72
+ }
73
+
74
+ @media (prefers-reduced-motion: reduce) {
75
+ .lg-glass {
76
+ transition: none;
77
+ }
78
+ }