@aejkatappaja/phantom-ui 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aejkatappaja
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,346 @@
1
+ <p align="center">
2
+ <img src="logo-phantom.svg" alt="phantom-ui" width="200" />
3
+ </p>
4
+
5
+ <p align="center">
6
+ <strong>Structure-aware skeleton loader. One Web Component. Every framework.</strong>
7
+ </p>
8
+
9
+ <p align="center">
10
+ <a href="https://www.npmjs.com/package/phantom-ui"><img src="https://img.shields.io/npm/v/phantom-ui.svg?style=flat-square" alt="npm version" /></a>
11
+ <a href="https://bundlephobia.com/package/phantom-ui"><img src="https://img.shields.io/bundlephobia/minzip/phantom-ui?style=flat-square" alt="bundle size" /></a>
12
+ <a href="https://github.com/anthropics/phantom-ui/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/phantom-ui?style=flat-square" alt="license" /></a>
13
+ </p>
14
+
15
+ ---
16
+
17
+ Stop building skeleton screens by hand. Wrap your real UI in `<phantom-ui>` and it generates shimmer placeholders automatically by measuring your actual DOM at runtime.
18
+
19
+ No separate skeleton components to maintain. No copy-pasting layouts. The real component _is_ the skeleton template.
20
+
21
+ ## Why
22
+
23
+ Traditional skeleton loaders require you to build and maintain a second version of every component, just for the loading state. When the real component changes, the skeleton drifts out of sync.
24
+
25
+ `phantom-ui` takes a different approach. It renders your real component with invisible text, measures the position and size of every leaf element (`getBoundingClientRect`), and overlays animated shimmer blocks at the exact same coordinates. Container backgrounds and borders stay visible, giving a natural card outline while loading.
26
+
27
+ Because it is a standard Web Component (built with Lit), it works in React, Vue, Svelte, Angular, Solid, Qwik, or plain HTML. No framework adapters needed.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ bun add phantom-ui # bun
33
+ npm install phantom-ui # npm
34
+ pnpm add phantom-ui # pnpm
35
+ yarn add phantom-ui # yarn
36
+ ```
37
+
38
+ Or drop in a script tag with no build step:
39
+
40
+ ```html
41
+ <script src="https://cdn.jsdelivr.net/npm/phantom-ui/dist/phantom-ui.cdn.js"></script>
42
+ ```
43
+
44
+ ## Quick start
45
+
46
+ ```html
47
+ <phantom-ui loading>
48
+ <div class="card">
49
+ <img src="avatar.png" width="48" height="48" style="border-radius: 50%" />
50
+ <h3>Ada Lovelace</h3>
51
+ <p>First computer programmer, probably.</p>
52
+ </div>
53
+ </phantom-ui>
54
+ ```
55
+
56
+ Set `loading` to show the shimmer. Remove it to reveal the real content.
57
+
58
+ ## Framework examples
59
+
60
+ ### React
61
+
62
+ ```tsx
63
+ import "phantom-ui";
64
+
65
+ function ProfileCard({ user, isLoading }: Props) {
66
+ return (
67
+ <phantom-ui loading={isLoading || undefined}>
68
+ <div className="card">
69
+ <img src={user?.avatar ?? "/placeholder.png"} className="avatar" />
70
+ <h3>{user?.name ?? "Placeholder Name"}</h3>
71
+ <p>{user?.bio ?? "A few words about this person go here."}</p>
72
+ </div>
73
+ </phantom-ui>
74
+ );
75
+ }
76
+ ```
77
+
78
+ ### Vue
79
+
80
+ ```vue
81
+ <script setup lang="ts">
82
+ import "phantom-ui";
83
+
84
+ const props = defineProps<{ loading: boolean }>();
85
+ </script>
86
+
87
+ <template>
88
+ <phantom-ui :loading="props.loading">
89
+ <div class="card">
90
+ <img src="/avatar.png" class="avatar" />
91
+ <h3>Ada Lovelace</h3>
92
+ <p>First computer programmer, probably.</p>
93
+ </div>
94
+ </phantom-ui>
95
+ </template>
96
+ ```
97
+
98
+ ### Svelte
99
+
100
+ ```svelte
101
+ <script lang="ts">
102
+ import "phantom-ui";
103
+
104
+ export let loading = true;
105
+ </script>
106
+
107
+ <phantom-ui {loading}>
108
+ <div class="card">
109
+ <img src="/avatar.png" alt="avatar" class="avatar" />
110
+ <h3>Ada Lovelace</h3>
111
+ <p>First computer programmer, probably.</p>
112
+ </div>
113
+ </phantom-ui>
114
+ ```
115
+
116
+ ### Angular
117
+
118
+ ```typescript
119
+ import { Component, signal, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
120
+ import "phantom-ui";
121
+
122
+ @Component({
123
+ selector: "app-profile",
124
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
125
+ template: `
126
+ <phantom-ui [attr.loading]="loading() ? '' : null">
127
+ <div class="card">
128
+ <img src="/avatar.png" class="avatar" />
129
+ <h3>Ada Lovelace</h3>
130
+ <p>First computer programmer, probably.</p>
131
+ </div>
132
+ </phantom-ui>
133
+ `,
134
+ })
135
+ export class ProfileComponent {
136
+ loading = signal(true);
137
+ }
138
+ ```
139
+
140
+ ### Solid
141
+
142
+ ```tsx
143
+ import { createSignal } from "solid-js";
144
+ import "phantom-ui";
145
+
146
+ function ProfileCard() {
147
+ const [loading, setLoading] = createSignal(true);
148
+
149
+ return (
150
+ <phantom-ui attr:loading={loading() || undefined}>
151
+ <div class="card">
152
+ <img src="/avatar.png" class="avatar" />
153
+ <h3>Ada Lovelace</h3>
154
+ <p>First computer programmer, probably.</p>
155
+ </div>
156
+ </phantom-ui>
157
+ );
158
+ }
159
+ ```
160
+
161
+ ### SSR frameworks (Next.js, Nuxt, SvelteKit, Remix)
162
+
163
+ The component needs browser APIs to measure the DOM. Import it client-side only:
164
+
165
+ ```tsx
166
+ // Next.js
167
+ "use client";
168
+ import { useEffect } from "react";
169
+
170
+ export default function Page() {
171
+ useEffect(() => { import("phantom-ui"); }, []);
172
+ return <phantom-ui loading>...</phantom-ui>;
173
+ }
174
+ ```
175
+
176
+ ```vue
177
+ <!-- Nuxt -->
178
+ <script setup>
179
+ onMounted(() => import("phantom-ui"));
180
+ </script>
181
+
182
+ <template>
183
+ <ClientOnly>
184
+ <phantom-ui loading>...</phantom-ui>
185
+ </ClientOnly>
186
+ </template>
187
+ ```
188
+
189
+ ```svelte
190
+ <!-- SvelteKit -->
191
+ <script>
192
+ import { onMount } from "svelte";
193
+ onMount(() => import("phantom-ui"));
194
+ </script>
195
+ ```
196
+
197
+ The `<phantom-ui>` tag can exist in server-rendered HTML. The browser treats it as an unknown element until hydration, then the Web Component activates and measures the DOM. Content renders normally on the server, which is good for SEO.
198
+
199
+ ## TypeScript
200
+
201
+ The package ships full type definitions. A `postinstall` script automatically detects your framework and generates a `phantom-ui.d.ts` in your `src/` directory. No extra step needed.
202
+
203
+ Vue, Svelte, and Angular work out of the box without any type declaration.
204
+
205
+ If the postinstall did not run (CI, monorepos, `--ignore-scripts`), you can generate it manually:
206
+
207
+ ```bash
208
+ npx phantom-ui init # npm
209
+ bunx phantom-ui init # bun
210
+ pnpx phantom-ui init # pnpm
211
+ yarn dlx phantom-ui init # yarn
212
+ ```
213
+
214
+ <details>
215
+ <summary>Or create the file yourself:</summary>
216
+
217
+ **React**
218
+
219
+ ```typescript
220
+ import type { PhantomUiAttributes } from "phantom-ui";
221
+
222
+ declare module "react/jsx-runtime" {
223
+ export namespace JSX {
224
+ interface IntrinsicElements {
225
+ "phantom-ui": PhantomUiAttributes;
226
+ }
227
+ }
228
+ }
229
+ ```
230
+
231
+ **Solid**
232
+
233
+ ```typescript
234
+ import type { SolidPhantomUiAttributes } from "phantom-ui";
235
+
236
+ declare module "solid-js" {
237
+ namespace JSX {
238
+ interface IntrinsicElements {
239
+ "phantom-ui": SolidPhantomUiAttributes;
240
+ }
241
+ }
242
+ }
243
+ ```
244
+
245
+ **Qwik**
246
+
247
+ ```typescript
248
+ import type { PhantomUiAttributes } from "phantom-ui";
249
+
250
+ declare module "@builder.io/qwik" {
251
+ namespace QwikJSX {
252
+ interface IntrinsicElements {
253
+ "phantom-ui": PhantomUiAttributes & Record<string, unknown>;
254
+ }
255
+ }
256
+ }
257
+ ```
258
+
259
+
260
+ </details>
261
+
262
+ ## Attributes
263
+
264
+ | Attribute | Type | Default | Description |
265
+ | --- | --- | --- | --- |
266
+ | `loading` | `boolean` | `false` | Show shimmer overlay or real content |
267
+ | `shimmer-color` | `string` | `rgba(255,255,255,0.3)` | Color of the animated gradient sweep |
268
+ | `background-color` | `string` | `rgba(255,255,255,0.08)` | Background of each shimmer block |
269
+ | `duration` | `number` | `1.5` | Animation cycle in seconds |
270
+ | `fallback-radius` | `number` | `4` | Border radius (px) for flat elements like text |
271
+
272
+ ## Fine-grained control
273
+
274
+ Two data attributes let you control which elements get shimmer treatment:
275
+
276
+ **`data-shimmer-ignore`** keeps an element and all its descendants visible during loading. Useful for logos, brand marks, or live indicators that should always be shown.
277
+
278
+ **`data-shimmer-no-children`** captures the element as one single shimmer block instead of recursing into its children. Useful for dense metric groups that should appear as a single placeholder.
279
+
280
+ ```html
281
+ <phantom-ui loading>
282
+ <div class="dashboard">
283
+ <div class="logo" data-shimmer-ignore>ACME</div>
284
+ <div class="kpi-row" data-shimmer-no-children>
285
+ <span>$48.2k</span>
286
+ <span>2,847 users</span>
287
+ <span>42ms p99</span>
288
+ </div>
289
+ <div class="content">
290
+ <p>Each leaf element here gets its own shimmer block.</p>
291
+ </div>
292
+ </div>
293
+ </phantom-ui>
294
+ ```
295
+
296
+ ## How it works
297
+
298
+ 1. Your real content is rendered in the DOM with `color: transparent` and media elements hidden. Container backgrounds and borders stay visible, preserving the natural card/section outline.
299
+
300
+ 2. The component walks the DOM tree and identifies "leaf" elements: text nodes, images, buttons, inputs, and anything without child elements. Container divs are recursed into, not captured.
301
+
302
+ 3. Each leaf element is measured with `getBoundingClientRect()` relative to the host. Border radius is read from `getComputedStyle()`. Table cells get special handling to measure actual text width, not cell width.
303
+
304
+ 4. An absolutely-positioned overlay renders one shimmer block per measured element, with a CSS gradient animation sweeping across each block.
305
+
306
+ 5. A `ResizeObserver` and `MutationObserver` re-measure automatically when the layout changes (window resize, content injection, DOM mutations).
307
+
308
+ 6. When `loading` is removed, the overlay is destroyed and real content is revealed.
309
+
310
+ ## CSS custom properties
311
+
312
+ You can style the component from the outside using CSS custom properties instead of (or in addition to) attributes:
313
+
314
+ ```css
315
+ phantom-ui {
316
+ --shimmer-color: rgba(100, 200, 255, 0.3);
317
+ --shimmer-duration: 2s;
318
+ --shimmer-bg: rgba(100, 200, 255, 0.08);
319
+ }
320
+ ```
321
+
322
+ ## Custom Elements Manifest
323
+
324
+ The package ships a `custom-elements.json` manifest, which gives IDE autocomplete, Storybook autodocs, and framework tooling the full picture of attributes, properties, slots, and types.
325
+
326
+ ## Bundle size
327
+
328
+ The CDN build (Lit included) is ~22kb / ~8kb gzipped.
329
+
330
+ When used as an ES module with a bundler, Lit is likely already in your dependency tree, bringing the component cost down to under 2kb.
331
+
332
+ ## Development
333
+
334
+ ```bash
335
+ bun install
336
+ bun run storybook # dev server on :6006
337
+ bun run build # tsc + custom elements manifest + CDN bundle
338
+ bun run lint # biome check
339
+ bun run lint:fix # biome auto-fix
340
+ ```
341
+
342
+ The `examples/` directory contains test apps for React, Vue, Solid, Angular, and Qwik, each wired to the local package.
343
+
344
+ ## License
345
+
346
+ MIT
@@ -0,0 +1,228 @@
1
+ {
2
+ "schemaVersion": "1.0.0",
3
+ "readme": "",
4
+ "modules": [
5
+ {
6
+ "kind": "javascript-module",
7
+ "path": "src/phantom-ui.ts",
8
+ "declarations": [
9
+ {
10
+ "kind": "class",
11
+ "description": "`<phantom-ui>` — A structure-aware shimmer skeleton loader.\n\nWraps real content and, when `loading` is true, measures the DOM structure\nof the slotted children to generate perfectly-aligned shimmer overlay blocks.",
12
+ "name": "PhantomUi",
13
+ "slots": [
14
+ {
15
+ "description": "The real content to show (or measure for skeleton generation)",
16
+ "name": ""
17
+ }
18
+ ],
19
+ "members": [
20
+ {
21
+ "kind": "field",
22
+ "name": "loading",
23
+ "type": {
24
+ "text": "boolean"
25
+ },
26
+ "default": "false",
27
+ "description": "Whether to show the shimmer overlay or the real content",
28
+ "attribute": "loading",
29
+ "reflects": true
30
+ },
31
+ {
32
+ "kind": "field",
33
+ "name": "shimmerColor",
34
+ "type": {
35
+ "text": "string"
36
+ },
37
+ "default": "\"rgba(255, 255, 255, 0.3)\"",
38
+ "description": "Color of the animated shimmer gradient wave",
39
+ "attribute": "shimmer-color"
40
+ },
41
+ {
42
+ "kind": "field",
43
+ "name": "backgroundColor",
44
+ "type": {
45
+ "text": "string"
46
+ },
47
+ "default": "\"rgba(255, 255, 255, 0.08)\"",
48
+ "description": "Background color of each shimmer block",
49
+ "attribute": "background-color"
50
+ },
51
+ {
52
+ "kind": "field",
53
+ "name": "duration",
54
+ "type": {
55
+ "text": "number"
56
+ },
57
+ "default": "1.5",
58
+ "description": "Animation cycle duration in seconds",
59
+ "attribute": "duration"
60
+ },
61
+ {
62
+ "kind": "field",
63
+ "name": "fallbackRadius",
64
+ "type": {
65
+ "text": "number"
66
+ },
67
+ "default": "4",
68
+ "description": "Border radius (px) for elements with border-radius: 0",
69
+ "attribute": "fallback-radius"
70
+ },
71
+ {
72
+ "kind": "field",
73
+ "name": "_blocks",
74
+ "type": {
75
+ "text": "ElementInfo[]"
76
+ },
77
+ "privacy": "private",
78
+ "default": "[]"
79
+ },
80
+ {
81
+ "kind": "field",
82
+ "name": "_resizeObserver",
83
+ "type": {
84
+ "text": "ResizeObserver | null"
85
+ },
86
+ "privacy": "private",
87
+ "default": "null"
88
+ },
89
+ {
90
+ "kind": "field",
91
+ "name": "_mutationObserver",
92
+ "type": {
93
+ "text": "MutationObserver | null"
94
+ },
95
+ "privacy": "private",
96
+ "default": "null"
97
+ },
98
+ {
99
+ "kind": "field",
100
+ "name": "_measureScheduled",
101
+ "type": {
102
+ "text": "boolean"
103
+ },
104
+ "privacy": "private",
105
+ "default": "false"
106
+ },
107
+ {
108
+ "kind": "method",
109
+ "name": "_scheduleMeasure",
110
+ "privacy": "private",
111
+ "return": {
112
+ "type": {
113
+ "text": "void"
114
+ }
115
+ }
116
+ },
117
+ {
118
+ "kind": "method",
119
+ "name": "_measure",
120
+ "privacy": "private",
121
+ "return": {
122
+ "type": {
123
+ "text": "void"
124
+ }
125
+ }
126
+ },
127
+ {
128
+ "kind": "method",
129
+ "name": "_setupObservers",
130
+ "privacy": "private",
131
+ "return": {
132
+ "type": {
133
+ "text": "void"
134
+ }
135
+ }
136
+ },
137
+ {
138
+ "kind": "method",
139
+ "name": "_teardownObservers",
140
+ "privacy": "private",
141
+ "return": {
142
+ "type": {
143
+ "text": "void"
144
+ }
145
+ }
146
+ },
147
+ {
148
+ "kind": "method",
149
+ "name": "_renderBlocks",
150
+ "privacy": "private"
151
+ }
152
+ ],
153
+ "attributes": [
154
+ {
155
+ "name": "loading",
156
+ "type": {
157
+ "text": "boolean"
158
+ },
159
+ "default": "false",
160
+ "description": "Whether to show the shimmer overlay or the real content",
161
+ "fieldName": "loading"
162
+ },
163
+ {
164
+ "name": "shimmer-color",
165
+ "type": {
166
+ "text": "string"
167
+ },
168
+ "default": "\"rgba(255, 255, 255, 0.3)\"",
169
+ "description": "Color of the animated shimmer gradient wave",
170
+ "fieldName": "shimmerColor"
171
+ },
172
+ {
173
+ "name": "background-color",
174
+ "type": {
175
+ "text": "string"
176
+ },
177
+ "default": "\"rgba(255, 255, 255, 0.08)\"",
178
+ "description": "Background color of each shimmer block",
179
+ "fieldName": "backgroundColor"
180
+ },
181
+ {
182
+ "name": "duration",
183
+ "type": {
184
+ "text": "number"
185
+ },
186
+ "default": "1.5",
187
+ "description": "Animation cycle duration in seconds",
188
+ "fieldName": "duration"
189
+ },
190
+ {
191
+ "name": "fallback-radius",
192
+ "type": {
193
+ "text": "number"
194
+ },
195
+ "default": "4",
196
+ "description": "Border radius (px) for elements with border-radius: 0",
197
+ "fieldName": "fallbackRadius"
198
+ }
199
+ ],
200
+ "superclass": {
201
+ "name": "LitElement",
202
+ "package": "lit"
203
+ },
204
+ "tagName": "phantom-ui",
205
+ "customElement": true
206
+ }
207
+ ],
208
+ "exports": [
209
+ {
210
+ "kind": "js",
211
+ "name": "PhantomUi",
212
+ "declaration": {
213
+ "name": "PhantomUi",
214
+ "module": "src/phantom-ui.ts"
215
+ }
216
+ },
217
+ {
218
+ "kind": "custom-element-definition",
219
+ "name": "phantom-ui",
220
+ "declaration": {
221
+ "name": "PhantomUi",
222
+ "module": "src/phantom-ui.ts"
223
+ }
224
+ }
225
+ ]
226
+ }
227
+ ]
228
+ }
@@ -0,0 +1,135 @@
1
+ "use strict";(()=>{var Rt=Object.defineProperty;var Tt=Object.getOwnPropertyDescriptor;var g=(i,t,e,s)=>{for(var r=s>1?void 0:s?Tt(t,e):t,o=i.length-1,n;o>=0;o--)(n=i[o])&&(r=(s?n(t,e,r):n(r))||r);return s&&r&&Rt(t,e,r),r};var H=globalThis,I=H.ShadowRoot&&(H.ShadyCSS===void 0||H.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,V=Symbol(),rt=new WeakMap,w=class{constructor(t,e,s){if(this._$cssResult$=!0,s!==V)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=e}get styleSheet(){let t=this.o,e=this.t;if(I&&t===void 0){let s=e!==void 0&&e.length===1;s&&(t=rt.get(e)),t===void 0&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),s&&rt.set(e,t))}return t}toString(){return this.cssText}},it=i=>new w(typeof i=="string"?i:i+"",void 0,V),W=(i,...t)=>{let e=i.length===1?i[0]:t.reduce((s,r,o)=>s+(n=>{if(n._$cssResult$===!0)return n.cssText;if(typeof n=="number")return n;throw Error("Value passed to 'css' function must be a 'css' function result: "+n+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(r)+i[o+1],i[0]);return new w(e,i,V)},ot=(i,t)=>{if(I)i.adoptedStyleSheets=t.map(e=>e instanceof CSSStyleSheet?e:e.styleSheet);else for(let e of t){let s=document.createElement("style"),r=H.litNonce;r!==void 0&&s.setAttribute("nonce",r),s.textContent=e.cssText,i.appendChild(s)}},F=I?i=>i:i=>i instanceof CSSStyleSheet?(t=>{let e="";for(let s of t.cssRules)e+=s.cssText;return it(e)})(i):i;var{is:Pt,defineProperty:Mt,getOwnPropertyDescriptor:kt,getOwnPropertyNames:Nt,getOwnPropertySymbols:Ut,getPrototypeOf:Ht}=Object,L=globalThis,nt=L.trustedTypes,It=nt?nt.emptyScript:"",Lt=L.reactiveElementPolyfillSupport,O=(i,t)=>i,R={toAttribute(i,t){switch(t){case Boolean:i=i?It:null;break;case Object:case Array:i=i==null?i:JSON.stringify(i)}return i},fromAttribute(i,t){let e=i;switch(t){case Boolean:e=i!==null;break;case Number:e=i===null?null:Number(i);break;case Object:case Array:try{e=JSON.parse(i)}catch{e=null}}return e}},D=(i,t)=>!Pt(i,t),at={attribute:!0,type:String,converter:R,reflect:!1,useDefault:!1,hasChanged:D};Symbol.metadata??=Symbol("metadata"),L.litPropertyMetadata??=new WeakMap;var f=class extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,e=at){if(e.state&&(e.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(t)&&((e=Object.create(e)).wrapped=!0),this.elementProperties.set(t,e),!e.noAccessor){let s=Symbol(),r=this.getPropertyDescriptor(t,s,e);r!==void 0&&Mt(this.prototype,t,r)}}static getPropertyDescriptor(t,e,s){let{get:r,set:o}=kt(this.prototype,t)??{get(){return this[e]},set(n){this[e]=n}};return{get:r,set(n){let h=r?.call(this);o?.call(this,n),this.requestUpdate(t,h,s)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)??at}static _$Ei(){if(this.hasOwnProperty(O("elementProperties")))return;let t=Ht(this);t.finalize(),t.l!==void 0&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties)}static finalize(){if(this.hasOwnProperty(O("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(O("properties"))){let e=this.properties,s=[...Nt(e),...Ut(e)];for(let r of s)this.createProperty(r,e[r])}let t=this[Symbol.metadata];if(t!==null){let e=litPropertyMetadata.get(t);if(e!==void 0)for(let[s,r]of e)this.elementProperties.set(s,r)}this._$Eh=new Map;for(let[e,s]of this.elementProperties){let r=this._$Eu(e,s);r!==void 0&&this._$Eh.set(r,e)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(t){let e=[];if(Array.isArray(t)){let s=new Set(t.flat(1/0).reverse());for(let r of s)e.unshift(F(r))}else t!==void 0&&e.push(F(t));return e}static _$Eu(t,e){let s=e.attribute;return s===!1?void 0:typeof s=="string"?s:typeof t=="string"?t.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(t=>this.enableUpdating=t),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(t=>t(this))}addController(t){(this._$EO??=new Set).add(t),this.renderRoot!==void 0&&this.isConnected&&t.hostConnected?.()}removeController(t){this._$EO?.delete(t)}_$E_(){let t=new Map,e=this.constructor.elementProperties;for(let s of e.keys())this.hasOwnProperty(s)&&(t.set(s,this[s]),delete this[s]);t.size>0&&(this._$Ep=t)}createRenderRoot(){let t=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return ot(t,this.constructor.elementStyles),t}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(t=>t.hostConnected?.())}enableUpdating(t){}disconnectedCallback(){this._$EO?.forEach(t=>t.hostDisconnected?.())}attributeChangedCallback(t,e,s){this._$AK(t,s)}_$ET(t,e){let s=this.constructor.elementProperties.get(t),r=this.constructor._$Eu(t,s);if(r!==void 0&&s.reflect===!0){let o=(s.converter?.toAttribute!==void 0?s.converter:R).toAttribute(e,s.type);this._$Em=t,o==null?this.removeAttribute(r):this.setAttribute(r,o),this._$Em=null}}_$AK(t,e){let s=this.constructor,r=s._$Eh.get(t);if(r!==void 0&&this._$Em!==r){let o=s.getPropertyOptions(r),n=typeof o.converter=="function"?{fromAttribute:o.converter}:o.converter?.fromAttribute!==void 0?o.converter:R;this._$Em=r;let h=n.fromAttribute(e,o.type);this[r]=h??this._$Ej?.get(r)??h,this._$Em=null}}requestUpdate(t,e,s,r=!1,o){if(t!==void 0){let n=this.constructor;if(r===!1&&(o=this[t]),s??=n.getPropertyOptions(t),!((s.hasChanged??D)(o,e)||s.useDefault&&s.reflect&&o===this._$Ej?.get(t)&&!this.hasAttribute(n._$Eu(t,s))))return;this.C(t,e,s)}this.isUpdatePending===!1&&(this._$ES=this._$EP())}C(t,e,{useDefault:s,reflect:r,wrapped:o},n){s&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,n??e??this[t]),o!==!0||n!==void 0)||(this._$AL.has(t)||(this.hasUpdated||s||(e=void 0),this._$AL.set(t,e)),r===!0&&this._$Em!==t&&(this._$Eq??=new Set).add(t))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(e){Promise.reject(e)}let t=this.scheduleUpdate();return t!=null&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(let[r,o]of this._$Ep)this[r]=o;this._$Ep=void 0}let s=this.constructor.elementProperties;if(s.size>0)for(let[r,o]of s){let{wrapped:n}=o,h=this[r];n!==!0||this._$AL.has(r)||h===void 0||this.C(r,void 0,o,h)}}let t=!1,e=this._$AL;try{t=this.shouldUpdate(e),t?(this.willUpdate(e),this._$EO?.forEach(s=>s.hostUpdate?.()),this.update(e)):this._$EM()}catch(s){throw t=!1,this._$EM(),s}t&&this._$AE(e)}willUpdate(t){}_$AE(t){this._$EO?.forEach(e=>e.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(t){return!0}update(t){this._$Eq&&=this._$Eq.forEach(e=>this._$ET(e,this[e])),this._$EM()}updated(t){}firstUpdated(t){}};f.elementStyles=[],f.shadowRootOptions={mode:"open"},f[O("elementProperties")]=new Map,f[O("finalized")]=new Map,Lt?.({ReactiveElement:f}),(L.reactiveElementVersions??=[]).push("2.1.2");var Q=globalThis,ht=i=>i,B=Q.trustedTypes,lt=B?B.createPolicy("lit-html",{createHTML:i=>i}):void 0,ft="$lit$",y=`lit$${Math.random().toFixed(9).slice(2)}$`,$t="?"+y,Dt=`<${$t}>`,S=document,P=()=>S.createComment(""),M=i=>i===null||typeof i!="object"&&typeof i!="function",tt=Array.isArray,Bt=i=>tt(i)||typeof i?.[Symbol.iterator]=="function",K=`[
2
+ \f\r]`,T=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,ct=/-->/g,dt=/>/g,A=RegExp(`>|${K}(?:([^\\s"'>=/]+)(${K}*=${K}*(?:[^
3
+ \f\r"'\`<>=]|("|')|))|$)`,"g"),ut=/'/g,pt=/"/g,_t=/^(?:script|style|textarea|title)$/i,et=i=>(t,...e)=>({_$litType$:i,strings:t,values:e}),z=et(1),se=et(2),re=et(3),$=Symbol.for("lit-noChange"),u=Symbol.for("lit-nothing"),mt=new WeakMap,E=S.createTreeWalker(S,129);function gt(i,t){if(!tt(i)||!i.hasOwnProperty("raw"))throw Error("invalid template strings array");return lt!==void 0?lt.createHTML(t):t}var zt=(i,t)=>{let e=i.length-1,s=[],r,o=t===2?"<svg>":t===3?"<math>":"",n=T;for(let h=0;h<e;h++){let a=i[h],l,d,c=-1,m=0;for(;m<a.length&&(n.lastIndex=m,d=n.exec(a),d!==null);)m=n.lastIndex,n===T?d[1]==="!--"?n=ct:d[1]!==void 0?n=dt:d[2]!==void 0?(_t.test(d[2])&&(r=RegExp("</"+d[2],"g")),n=A):d[3]!==void 0&&(n=A):n===A?d[0]===">"?(n=r??T,c=-1):d[1]===void 0?c=-2:(c=n.lastIndex-d[2].length,l=d[1],n=d[3]===void 0?A:d[3]==='"'?pt:ut):n===pt||n===ut?n=A:n===ct||n===dt?n=T:(n=A,r=void 0);let _=n===A&&i[h+1].startsWith("/>")?" ":"";o+=n===T?a+Dt:c>=0?(s.push(l),a.slice(0,c)+ft+a.slice(c)+y+_):a+y+(c===-2?h:_)}return[gt(i,o+(i[e]||"<?>")+(t===2?"</svg>":t===3?"</math>":"")),s]},k=class i{constructor({strings:t,_$litType$:e},s){let r;this.parts=[];let o=0,n=0,h=t.length-1,a=this.parts,[l,d]=zt(t,e);if(this.el=i.createElement(l,s),E.currentNode=this.el.content,e===2||e===3){let c=this.el.content.firstChild;c.replaceWith(...c.childNodes)}for(;(r=E.nextNode())!==null&&a.length<h;){if(r.nodeType===1){if(r.hasAttributes())for(let c of r.getAttributeNames())if(c.endsWith(ft)){let m=d[n++],_=r.getAttribute(c).split(y),U=/([.?@])?(.*)/.exec(m);a.push({type:1,index:o,name:U[2],strings:_,ctor:U[1]==="."?J:U[1]==="?"?X:U[1]==="@"?Y:C}),r.removeAttribute(c)}else c.startsWith(y)&&(a.push({type:6,index:o}),r.removeAttribute(c));if(_t.test(r.tagName)){let c=r.textContent.split(y),m=c.length-1;if(m>0){r.textContent=B?B.emptyScript:"";for(let _=0;_<m;_++)r.append(c[_],P()),E.nextNode(),a.push({type:2,index:++o});r.append(c[m],P())}}}else if(r.nodeType===8)if(r.data===$t)a.push({type:2,index:o});else{let c=-1;for(;(c=r.data.indexOf(y,c+1))!==-1;)a.push({type:7,index:o}),c+=y.length-1}o++}}static createElement(t,e){let s=S.createElement("template");return s.innerHTML=t,s}};function x(i,t,e=i,s){if(t===$)return t;let r=s!==void 0?e._$Co?.[s]:e._$Cl,o=M(t)?void 0:t._$litDirective$;return r?.constructor!==o&&(r?._$AO?.(!1),o===void 0?r=void 0:(r=new o(i),r._$AT(i,e,s)),s!==void 0?(e._$Co??=[])[s]=r:e._$Cl=r),r!==void 0&&(t=x(i,r._$AS(i,t.values),r,s)),t}var G=class{constructor(t,e){this._$AV=[],this._$AN=void 0,this._$AD=t,this._$AM=e}get parentNode(){return this._$AM.parentNode}get _$AU(){return this._$AM._$AU}u(t){let{el:{content:e},parts:s}=this._$AD,r=(t?.creationScope??S).importNode(e,!0);E.currentNode=r;let o=E.nextNode(),n=0,h=0,a=s[0];for(;a!==void 0;){if(n===a.index){let l;a.type===2?l=new N(o,o.nextSibling,this,t):a.type===1?l=new a.ctor(o,a.name,a.strings,this,t):a.type===6&&(l=new Z(o,this,t)),this._$AV.push(l),a=s[++h]}n!==a?.index&&(o=E.nextNode(),n++)}return E.currentNode=S,r}p(t){let e=0;for(let s of this._$AV)s!==void 0&&(s.strings!==void 0?(s._$AI(t,s,e),e+=s.strings.length-2):s._$AI(t[e])),e++}},N=class i{get _$AU(){return this._$AM?._$AU??this._$Cv}constructor(t,e,s,r){this.type=2,this._$AH=u,this._$AN=void 0,this._$AA=t,this._$AB=e,this._$AM=s,this.options=r,this._$Cv=r?.isConnected??!0}get parentNode(){let t=this._$AA.parentNode,e=this._$AM;return e!==void 0&&t?.nodeType===11&&(t=e.parentNode),t}get startNode(){return this._$AA}get endNode(){return this._$AB}_$AI(t,e=this){t=x(this,t,e),M(t)?t===u||t==null||t===""?(this._$AH!==u&&this._$AR(),this._$AH=u):t!==this._$AH&&t!==$&&this._(t):t._$litType$!==void 0?this.$(t):t.nodeType!==void 0?this.T(t):Bt(t)?this.k(t):this._(t)}O(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}T(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t))}_(t){this._$AH!==u&&M(this._$AH)?this._$AA.nextSibling.data=t:this.T(S.createTextNode(t)),this._$AH=t}$(t){let{values:e,_$litType$:s}=t,r=typeof s=="number"?this._$AC(t):(s.el===void 0&&(s.el=k.createElement(gt(s.h,s.h[0]),this.options)),s);if(this._$AH?._$AD===r)this._$AH.p(e);else{let o=new G(r,this),n=o.u(this.options);o.p(e),this.T(n),this._$AH=o}}_$AC(t){let e=mt.get(t.strings);return e===void 0&&mt.set(t.strings,e=new k(t)),e}k(t){tt(this._$AH)||(this._$AH=[],this._$AR());let e=this._$AH,s,r=0;for(let o of t)r===e.length?e.push(s=new i(this.O(P()),this.O(P()),this,this.options)):s=e[r],s._$AI(o),r++;r<e.length&&(this._$AR(s&&s._$AB.nextSibling,r),e.length=r)}_$AR(t=this._$AA.nextSibling,e){for(this._$AP?.(!1,!0,e);t!==this._$AB;){let s=ht(t).nextSibling;ht(t).remove(),t=s}}setConnected(t){this._$AM===void 0&&(this._$Cv=t,this._$AP?.(t))}},C=class{get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}constructor(t,e,s,r,o){this.type=1,this._$AH=u,this._$AN=void 0,this.element=t,this.name=e,this._$AM=r,this.options=o,s.length>2||s[0]!==""||s[1]!==""?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=u}_$AI(t,e=this,s,r){let o=this.strings,n=!1;if(o===void 0)t=x(this,t,e,0),n=!M(t)||t!==this._$AH&&t!==$,n&&(this._$AH=t);else{let h=t,a,l;for(t=o[0],a=0;a<o.length-1;a++)l=x(this,h[s+a],e,a),l===$&&(l=this._$AH[a]),n||=!M(l)||l!==this._$AH[a],l===u?t=u:t!==u&&(t+=(l??"")+o[a+1]),this._$AH[a]=l}n&&!r&&this.j(t)}j(t){t===u?this.element.removeAttribute(this.name):this.element.setAttribute(this.name,t??"")}},J=class extends C{constructor(){super(...arguments),this.type=3}j(t){this.element[this.name]=t===u?void 0:t}},X=class extends C{constructor(){super(...arguments),this.type=4}j(t){this.element.toggleAttribute(this.name,!!t&&t!==u)}},Y=class extends C{constructor(t,e,s,r,o){super(t,e,s,r,o),this.type=5}_$AI(t,e=this){if((t=x(this,t,e,0)??u)===$)return;let s=this._$AH,r=t===u&&s!==u||t.capture!==s.capture||t.once!==s.once||t.passive!==s.passive,o=t!==u&&(s===u||r);r&&this.element.removeEventListener(this.name,this,s),o&&this.element.addEventListener(this.name,this,t),this._$AH=t}handleEvent(t){typeof this._$AH=="function"?this._$AH.call(this.options?.host??this.element,t):this._$AH.handleEvent(t)}},Z=class{constructor(t,e,s){this.element=t,this.type=6,this._$AN=void 0,this._$AM=e,this.options=s}get _$AU(){return this._$AM._$AU}_$AI(t){x(this,t)}};var jt=Q.litHtmlPolyfillSupport;jt?.(k,N),(Q.litHtmlVersions??=[]).push("3.3.2");var yt=(i,t,e)=>{let s=e?.renderBefore??t,r=s._$litPart$;if(r===void 0){let o=e?.renderBefore??null;s._$litPart$=r=new N(t.insertBefore(P(),o),o,void 0,e??{})}return r._$AI(i),r};var st=globalThis,b=class extends f{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){let t=super.createRenderRoot();return this.renderOptions.renderBefore??=t.firstChild,t}update(t){let e=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(t),this._$Do=yt(e,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return $}};b._$litElement$=!0,b.finalized=!0,st.litElementHydrateSupport?.({LitElement:b});var qt=st.litElementPolyfillSupport;qt?.({LitElement:b});(st.litElementVersions??=[]).push("4.2.2");var bt=i=>(t,e)=>{e!==void 0?e.addInitializer(()=>{customElements.define(i,t)}):customElements.define(i,t)};var Vt={attribute:!0,type:String,converter:R,reflect:!1,hasChanged:D},Wt=(i=Vt,t,e)=>{let{kind:s,metadata:r}=e,o=globalThis.litPropertyMetadata.get(r);if(o===void 0&&globalThis.litPropertyMetadata.set(r,o=new Map),s==="setter"&&((i=Object.create(i)).wrapped=!0),o.set(e.name,i),s==="accessor"){let{name:n}=e;return{set(h){let a=t.get.call(this);t.set.call(this,h),this.requestUpdate(n,a,i,!0,h)},init(h){return h!==void 0&&this.C(n,void 0,i,h),h}}}if(s==="setter"){let{name:n}=e;return function(h){let a=this[n];t.call(this,h),this.requestUpdate(n,a,i,!0,h)}}throw Error("Unsupported decorator location: "+s)};function v(i){return(t,e)=>typeof e=="object"?Wt(i,t,e):((s,r,o)=>{let n=r.hasOwnProperty(o);return r.constructor.createProperty(o,s),n?Object.getOwnPropertyDescriptor(r,o):void 0})(i,t,e)}function vt(i){return v({...i,state:!0,attribute:!1})}var At={ATTRIBUTE:1,CHILD:2,PROPERTY:3,BOOLEAN_ATTRIBUTE:4,EVENT:5,ELEMENT:6},Et=i=>(...t)=>({_$litDirective$:i,values:t}),q=class{constructor(t){}get _$AU(){return this._$AM._$AU}_$AT(t,e,s){this._$Ct=t,this._$AM=e,this._$Ci=s}_$AS(t,e){return this.update(t,e)}update(t,e){return this.render(...e)}};var St="important",Ft=" !"+St,xt=Et(class extends q{constructor(i){if(super(i),i.type!==At.ATTRIBUTE||i.name!=="style"||i.strings?.length>2)throw Error("The `styleMap` directive must be used in the `style` attribute and must be the only part in the attribute.")}render(i){return Object.keys(i).reduce((t,e)=>{let s=i[e];return s==null?t:t+`${e=e.includes("-")?e:e.replace(/(?:^(webkit|moz|ms|o)|)(?=[A-Z])/g,"-$&").toLowerCase()}:${s};`},"")}update(i,[t]){let{style:e}=i.element;if(this.ft===void 0)return this.ft=new Set(Object.keys(t)),this.render(t);for(let s of this.ft)t[s]==null&&(this.ft.delete(s),s.includes("-")?e.removeProperty(s):e[s]=null);for(let s in t){let r=t[s];if(r!=null){this.ft.add(s);let o=typeof r=="string"&&r.endsWith(Ft);s.includes("-")||o?e.setProperty(s,o?r.slice(0,-11):r,o?St:""):e[s]=r}}return $}});var Kt=new Set(["IMG","SVG","VIDEO","CANVAS","IFRAME","INPUT","TEXTAREA","BUTTON","HR"]),Gt=new Set(["BR","WBR","HR"]);function Jt(i){if(Kt.has(i.tagName))return!0;for(let t of i.children)if(!Gt.has(t.tagName))return!1;return!0}function Xt(i){for(let t of i.childNodes)if(t.nodeType===Node.TEXT_NODE&&t.textContent?.trim())return!0;return!1}function Ct(i,t){let e=[];function s(r){let o=r.getBoundingClientRect();if(o.width===0||o.height===0||r.hasAttribute("data-shimmer-ignore"))return;if(r.hasAttribute("data-shimmer-no-children")||Jt(r)){let a=getComputedStyle(r).borderRadius;if((r.tagName==="TD"||r.tagName==="TH")&&Xt(r)){let l=document.createElement("span");l.style.visibility="hidden",l.style.position="absolute",l.textContent=r.textContent,r.appendChild(l);let d=l.getBoundingClientRect();r.removeChild(l),e.push({x:o.left-t.left,y:o.top-t.top,width:Math.min(d.width,o.width),height:o.height,tag:r.tagName.toLowerCase(),borderRadius:a==="0px"?"":a});return}e.push({x:o.left-t.left,y:o.top-t.top,width:o.width,height:o.height,tag:r.tagName.toLowerCase(),borderRadius:a==="0px"?"":a});return}for(let h of r.children)s(h)}for(let r of i.children)s(r);return e}function wt(i,t){let e=null,s=new ResizeObserver(()=>{e!==null&&cancelAnimationFrame(e),e=requestAnimationFrame(()=>{e=null,t()})});return s.observe(i),s}var Ot=W`
4
+ :host {
5
+ display: block;
6
+ position: relative;
7
+ --shimmer-color: rgba(255, 255, 255, 0.3);
8
+ --shimmer-duration: 1.5s;
9
+ --shimmer-bg: rgba(255, 255, 255, 0.08);
10
+ }
11
+
12
+ :host([loading]) ::slotted(*) {
13
+ color: transparent !important;
14
+ -webkit-text-fill-color: transparent !important;
15
+ pointer-events: none;
16
+ user-select: none;
17
+ }
18
+
19
+ :host([loading]) ::slotted(img),
20
+ :host([loading]) ::slotted(svg),
21
+ :host([loading]) ::slotted(video),
22
+ :host([loading]) ::slotted(canvas) {
23
+ opacity: 0 !important;
24
+ }
25
+
26
+ .shimmer-overlay {
27
+ position: absolute;
28
+ inset: 0;
29
+ pointer-events: none;
30
+ overflow: hidden;
31
+ }
32
+
33
+ .shimmer-block {
34
+ position: absolute;
35
+ overflow: hidden;
36
+ }
37
+
38
+ .shimmer-block::after {
39
+ content: "";
40
+ position: absolute;
41
+ inset: 0;
42
+ animation: shimmer-sweep var(--shimmer-duration, 1.5s) ease-in-out infinite;
43
+ }
44
+
45
+ @keyframes shimmer-sweep {
46
+ 0% {
47
+ background: linear-gradient(
48
+ 90deg,
49
+ transparent 0%,
50
+ var(--shimmer-color) 50%,
51
+ transparent 100%
52
+ );
53
+ background-size: 200% 100%;
54
+ background-position: -100% 0;
55
+ }
56
+ 100% {
57
+ background: linear-gradient(
58
+ 90deg,
59
+ transparent 0%,
60
+ var(--shimmer-color) 50%,
61
+ transparent 100%
62
+ );
63
+ background-size: 200% 100%;
64
+ background-position: 200% 0;
65
+ }
66
+ }
67
+ `;var p=class extends b{constructor(){super(...arguments);this.loading=!1;this.shimmerColor="rgba(255, 255, 255, 0.3)";this.backgroundColor="rgba(255, 255, 255, 0.08)";this.duration=1.5;this.fallbackRadius=4;this._blocks=[];this._resizeObserver=null;this._mutationObserver=null;this._measureScheduled=!1}disconnectedCallback(){super.disconnectedCallback(),this._teardownObservers()}updated(e){e.has("loading")&&(this.loading?(this._scheduleMeasure(),this._setupObservers()):(this._blocks=[],this._teardownObservers()))}render(){let e=xt({"--shimmer-color":this.shimmerColor,"--shimmer-duration":`${this.duration}s`,"--shimmer-bg":this.backgroundColor});return z`
68
+ <slot></slot>
69
+ ${this.loading?z`
70
+ <div class="shimmer-overlay" style=${e} aria-hidden="true">
71
+ ${this._renderBlocks()}
72
+ </div>
73
+ `:""}
74
+ `}_scheduleMeasure(){this._measureScheduled||(this._measureScheduled=!0,requestAnimationFrame(()=>{this._measureScheduled=!1,this._measure()}))}_measure(){if(!this.loading)return;let e=this.getBoundingClientRect();if(e.width===0||e.height===0)return;let s=this.shadowRoot?.querySelector("slot");if(!s)return;let r=s.assignedElements({flatten:!0}),o=[];for(let n of r){let h=Ct(n,e);o.push(...h)}this._blocks=o}_setupObservers(){this._teardownObservers(),this._resizeObserver=wt(this,()=>{this._scheduleMeasure()}),this._mutationObserver=new MutationObserver(()=>{this._scheduleMeasure()}),this._mutationObserver.observe(this,{childList:!0,subtree:!0,attributes:!0})}_teardownObservers(){this._resizeObserver&&(this._resizeObserver.disconnect(),this._resizeObserver=null),this._mutationObserver&&(this._mutationObserver.disconnect(),this._mutationObserver=null)}_renderBlocks(){return this._blocks.map(e=>{let s=e.borderRadius||`${this.fallbackRadius}px`;return z`
75
+ <div
76
+ class="shimmer-block"
77
+ style="
78
+ left: ${e.x}px;
79
+ top: ${e.y}px;
80
+ width: ${e.width}px;
81
+ height: ${e.height}px;
82
+ border-radius: ${s};
83
+ background: var(--shimmer-bg, ${this.backgroundColor});
84
+ "
85
+ ></div>
86
+ `})}};p.styles=Ot,g([v({type:Boolean,reflect:!0})],p.prototype,"loading",2),g([v({attribute:"shimmer-color"})],p.prototype,"shimmerColor",2),g([v({attribute:"background-color"})],p.prototype,"backgroundColor",2),g([v({type:Number})],p.prototype,"duration",2),g([v({type:Number,attribute:"fallback-radius"})],p.prototype,"fallbackRadius",2),g([vt()],p.prototype,"_blocks",2),p=g([bt("phantom-ui")],p);})();
87
+ /*! Bundled license information:
88
+
89
+ @lit/reactive-element/css-tag.js:
90
+ (**
91
+ * @license
92
+ * Copyright 2019 Google LLC
93
+ * SPDX-License-Identifier: BSD-3-Clause
94
+ *)
95
+
96
+ @lit/reactive-element/reactive-element.js:
97
+ lit-html/lit-html.js:
98
+ lit-element/lit-element.js:
99
+ @lit/reactive-element/decorators/custom-element.js:
100
+ @lit/reactive-element/decorators/property.js:
101
+ @lit/reactive-element/decorators/state.js:
102
+ @lit/reactive-element/decorators/event-options.js:
103
+ @lit/reactive-element/decorators/base.js:
104
+ @lit/reactive-element/decorators/query.js:
105
+ @lit/reactive-element/decorators/query-all.js:
106
+ @lit/reactive-element/decorators/query-async.js:
107
+ @lit/reactive-element/decorators/query-assigned-nodes.js:
108
+ lit-html/directive.js:
109
+ (**
110
+ * @license
111
+ * Copyright 2017 Google LLC
112
+ * SPDX-License-Identifier: BSD-3-Clause
113
+ *)
114
+
115
+ lit-html/is-server.js:
116
+ (**
117
+ * @license
118
+ * Copyright 2022 Google LLC
119
+ * SPDX-License-Identifier: BSD-3-Clause
120
+ *)
121
+
122
+ @lit/reactive-element/decorators/query-assigned-elements.js:
123
+ (**
124
+ * @license
125
+ * Copyright 2021 Google LLC
126
+ * SPDX-License-Identifier: BSD-3-Clause
127
+ *)
128
+
129
+ lit-html/directives/style-map.js:
130
+ (**
131
+ * @license
132
+ * Copyright 2018 Google LLC
133
+ * SPDX-License-Identifier: BSD-3-Clause
134
+ *)
135
+ */
@@ -0,0 +1,78 @@
1
+ import { LitElement } from "lit";
2
+ import type { CSSResult } from "lit";
3
+ /**
4
+ * `<phantom-ui>` — A structure-aware shimmer skeleton loader.
5
+ *
6
+ * Wraps real content and, when `loading` is true, measures the DOM structure
7
+ * of the slotted children to generate perfectly-aligned shimmer overlay blocks.
8
+ *
9
+ * @slot - The real content to show (or measure for skeleton generation)
10
+ *
11
+ * @property {boolean} loading - Whether to show the shimmer overlay or the real content
12
+ * @property {string} shimmerColor - Color of the animated shimmer gradient wave
13
+ * @property {string} backgroundColor - Background color of each shimmer block
14
+ * @property {number} duration - Animation cycle duration in seconds
15
+ * @property {number} fallbackRadius - Border radius (px) for elements with border-radius: 0
16
+ *
17
+ * @example
18
+ * ```html
19
+ * <phantom-ui loading>
20
+ * <div class="card">
21
+ * <img src="avatar.png" width="48" height="48" />
22
+ * <h3>User Name</h3>
23
+ * <p>Some description text here</p>
24
+ * </div>
25
+ * </phantom-ui>
26
+ * ```
27
+ */
28
+ export declare class PhantomUi extends LitElement {
29
+ static styles: CSSResult;
30
+ /** Whether to show the shimmer overlay or the real content */
31
+ loading: boolean;
32
+ /** Color of the animated shimmer gradient wave */
33
+ shimmerColor: string;
34
+ /** Background color of each shimmer block */
35
+ backgroundColor: string;
36
+ /** Animation cycle duration in seconds */
37
+ duration: number;
38
+ /** Border radius applied to elements with border-radius: 0 (like text) */
39
+ fallbackRadius: number;
40
+ private _blocks;
41
+ private _resizeObserver;
42
+ private _mutationObserver;
43
+ private _measureScheduled;
44
+ disconnectedCallback(): void;
45
+ updated(changedProperties: Map<PropertyKey, unknown>): void;
46
+ render(): import("lit-html").TemplateResult<1>;
47
+ private _scheduleMeasure;
48
+ private _measure;
49
+ private _setupObservers;
50
+ private _teardownObservers;
51
+ private _renderBlocks;
52
+ }
53
+ export interface PhantomUiAttributes {
54
+ loading?: boolean;
55
+ "shimmer-color"?: string;
56
+ "background-color"?: string;
57
+ duration?: number;
58
+ "fallback-radius"?: number;
59
+ children?: unknown;
60
+ class?: string;
61
+ id?: string;
62
+ style?: string;
63
+ slot?: string;
64
+ }
65
+ /** Solid uses `attr:` prefix to set HTML attributes. This maps all PhantomUiAttributes to their `attr:` equivalents. */
66
+ export type SolidPhantomUiAttributes = PhantomUiAttributes & {
67
+ [K in keyof PhantomUiAttributes as `attr:${K & string}`]?: PhantomUiAttributes[K];
68
+ };
69
+ declare global {
70
+ interface HTMLElementTagNameMap {
71
+ "phantom-ui": PhantomUi;
72
+ }
73
+ namespace JSX {
74
+ interface IntrinsicElements {
75
+ "phantom-ui": PhantomUiAttributes;
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,84 @@
1
+ var v=Object.defineProperty;var _=Object.getOwnPropertyDescriptor;var a=(o,r,e,i)=>{for(var t=i>1?void 0:i?_(r,e):r,s=o.length-1,l;s>=0;s--)(l=o[s])&&(t=(i?l(r,e,t):l(t))||t);return i&&t&&v(r,e,t),t};import{LitElement as x,html as c}from"lit";import{customElement as R,property as d,state as C}from"lit/decorators.js";import{styleMap as S}from"lit/directives/style-map.js";var y=new Set(["IMG","SVG","VIDEO","CANVAS","IFRAME","INPUT","TEXTAREA","BUTTON","HR"]),k=new Set(["BR","WBR","HR"]);function w(o){if(y.has(o.tagName))return!0;for(let r of o.children)if(!k.has(r.tagName))return!1;return!0}function O(o){for(let r of o.childNodes)if(r.nodeType===Node.TEXT_NODE&&r.textContent?.trim())return!0;return!1}function b(o,r){let e=[];function i(t){let s=t.getBoundingClientRect();if(s.width===0||s.height===0||t.hasAttribute("data-shimmer-ignore"))return;if(t.hasAttribute("data-shimmer-no-children")||w(t)){let h=getComputedStyle(t).borderRadius;if((t.tagName==="TD"||t.tagName==="TH")&&O(t)){let u=document.createElement("span");u.style.visibility="hidden",u.style.position="absolute",u.textContent=t.textContent,t.appendChild(u);let g=u.getBoundingClientRect();t.removeChild(u),e.push({x:s.left-r.left,y:s.top-r.top,width:Math.min(g.width,s.width),height:s.height,tag:t.tagName.toLowerCase(),borderRadius:h==="0px"?"":h});return}e.push({x:s.left-r.left,y:s.top-r.top,width:s.width,height:s.height,tag:t.tagName.toLowerCase(),borderRadius:h==="0px"?"":h});return}for(let m of t.children)i(m)}for(let t of o.children)i(t);return e}function p(o,r){let e=null,i=new ResizeObserver(()=>{e!==null&&cancelAnimationFrame(e),e=requestAnimationFrame(()=>{e=null,r()})});return i.observe(o),i}import{css as E}from"lit";var f=E`
2
+ :host {
3
+ display: block;
4
+ position: relative;
5
+ --shimmer-color: rgba(255, 255, 255, 0.3);
6
+ --shimmer-duration: 1.5s;
7
+ --shimmer-bg: rgba(255, 255, 255, 0.08);
8
+ }
9
+
10
+ :host([loading]) ::slotted(*) {
11
+ color: transparent !important;
12
+ -webkit-text-fill-color: transparent !important;
13
+ pointer-events: none;
14
+ user-select: none;
15
+ }
16
+
17
+ :host([loading]) ::slotted(img),
18
+ :host([loading]) ::slotted(svg),
19
+ :host([loading]) ::slotted(video),
20
+ :host([loading]) ::slotted(canvas) {
21
+ opacity: 0 !important;
22
+ }
23
+
24
+ .shimmer-overlay {
25
+ position: absolute;
26
+ inset: 0;
27
+ pointer-events: none;
28
+ overflow: hidden;
29
+ }
30
+
31
+ .shimmer-block {
32
+ position: absolute;
33
+ overflow: hidden;
34
+ }
35
+
36
+ .shimmer-block::after {
37
+ content: "";
38
+ position: absolute;
39
+ inset: 0;
40
+ animation: shimmer-sweep var(--shimmer-duration, 1.5s) ease-in-out infinite;
41
+ }
42
+
43
+ @keyframes shimmer-sweep {
44
+ 0% {
45
+ background: linear-gradient(
46
+ 90deg,
47
+ transparent 0%,
48
+ var(--shimmer-color) 50%,
49
+ transparent 100%
50
+ );
51
+ background-size: 200% 100%;
52
+ background-position: -100% 0;
53
+ }
54
+ 100% {
55
+ background: linear-gradient(
56
+ 90deg,
57
+ transparent 0%,
58
+ var(--shimmer-color) 50%,
59
+ transparent 100%
60
+ );
61
+ background-size: 200% 100%;
62
+ background-position: 200% 0;
63
+ }
64
+ }
65
+ `;var n=class extends x{constructor(){super(...arguments);this.loading=!1;this.shimmerColor="rgba(255, 255, 255, 0.3)";this.backgroundColor="rgba(255, 255, 255, 0.08)";this.duration=1.5;this.fallbackRadius=4;this._blocks=[];this._resizeObserver=null;this._mutationObserver=null;this._measureScheduled=!1}disconnectedCallback(){super.disconnectedCallback(),this._teardownObservers()}updated(e){e.has("loading")&&(this.loading?(this._scheduleMeasure(),this._setupObservers()):(this._blocks=[],this._teardownObservers()))}render(){let e=S({"--shimmer-color":this.shimmerColor,"--shimmer-duration":`${this.duration}s`,"--shimmer-bg":this.backgroundColor});return c`
66
+ <slot></slot>
67
+ ${this.loading?c`
68
+ <div class="shimmer-overlay" style=${e} aria-hidden="true">
69
+ ${this._renderBlocks()}
70
+ </div>
71
+ `:""}
72
+ `}_scheduleMeasure(){this._measureScheduled||(this._measureScheduled=!0,requestAnimationFrame(()=>{this._measureScheduled=!1,this._measure()}))}_measure(){if(!this.loading)return;let e=this.getBoundingClientRect();if(e.width===0||e.height===0)return;let i=this.shadowRoot?.querySelector("slot");if(!i)return;let t=i.assignedElements({flatten:!0}),s=[];for(let l of t){let m=b(l,e);s.push(...m)}this._blocks=s}_setupObservers(){this._teardownObservers(),this._resizeObserver=p(this,()=>{this._scheduleMeasure()}),this._mutationObserver=new MutationObserver(()=>{this._scheduleMeasure()}),this._mutationObserver.observe(this,{childList:!0,subtree:!0,attributes:!0})}_teardownObservers(){this._resizeObserver&&(this._resizeObserver.disconnect(),this._resizeObserver=null),this._mutationObserver&&(this._mutationObserver.disconnect(),this._mutationObserver=null)}_renderBlocks(){return this._blocks.map(e=>{let i=e.borderRadius||`${this.fallbackRadius}px`;return c`
73
+ <div
74
+ class="shimmer-block"
75
+ style="
76
+ left: ${e.x}px;
77
+ top: ${e.y}px;
78
+ width: ${e.width}px;
79
+ height: ${e.height}px;
80
+ border-radius: ${i};
81
+ background: var(--shimmer-bg, ${this.backgroundColor});
82
+ "
83
+ ></div>
84
+ `})}};n.styles=f,a([d({type:Boolean,reflect:!0})],n.prototype,"loading",2),a([d({attribute:"shimmer-color"})],n.prototype,"shimmerColor",2),a([d({attribute:"background-color"})],n.prototype,"backgroundColor",2),a([d({type:Number})],n.prototype,"duration",2),a([d({type:Number,attribute:"fallback-radius"})],n.prototype,"fallbackRadius",2),a([C()],n.prototype,"_blocks",2),n=a([R("phantom-ui")],n);export{n as PhantomUi};
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@aejkatappaja/phantom-ui",
3
+ "version": "0.1.1",
4
+ "description": "Structure-aware shimmer skeleton loader as a universal Web Component built with Lit. Works with React, Vue, Svelte, Angular, Solid, or vanilla JS.",
5
+ "type": "module",
6
+ "main": "dist/phantom-ui.js",
7
+ "module": "dist/phantom-ui.js",
8
+ "types": "dist/phantom-ui.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/phantom-ui.js",
12
+ "types": "./dist/phantom-ui.d.ts"
13
+ }
14
+ },
15
+ "bin": {
16
+ "phantom-ui": "./src/cli/init.mjs"
17
+ },
18
+ "files": [
19
+ "dist/phantom-ui.js",
20
+ "dist/phantom-ui.cdn.js",
21
+ "dist/phantom-ui.d.ts",
22
+ "src/cli/init.mjs",
23
+ "custom-elements.json"
24
+ ],
25
+ "postinstall": "node ./src/cli/init.mjs || true",
26
+ "scripts": {
27
+ "build": "npm run build:esm && npm run build:cdn && npm run build:types && cem analyze",
28
+ "build:esm": "esbuild src/phantom-ui.ts --bundle --format=esm --outfile=dist/phantom-ui.js --minify --target=es2022 --packages=external",
29
+ "build:cdn": "esbuild src/phantom-ui.ts --bundle --format=iife --outfile=dist/phantom-ui.cdn.js --minify --target=es2022",
30
+ "build:types": "tsc --emitDeclarationOnly --declaration --outDir dist",
31
+ "dev": "tsc --watch",
32
+ "lint": "biome check .",
33
+ "lint:fix": "biome check --write .",
34
+ "format": "biome format --write .",
35
+ "storybook": "storybook dev -p 6006",
36
+ "build-storybook": "storybook build",
37
+ "prepare": "husky"
38
+ },
39
+ "keywords": [
40
+ "skeleton",
41
+ "shimmer",
42
+ "loading",
43
+ "placeholder",
44
+ "web-component",
45
+ "custom-element",
46
+ "lit",
47
+ "framework-agnostic"
48
+ ],
49
+ "license": "MIT",
50
+ "dependencies": {
51
+ "lit": "^3.2.0"
52
+ },
53
+ "devDependencies": {
54
+ "@biomejs/biome": "^1.9.0",
55
+ "@custom-elements-manifest/analyzer": "^0.11.0",
56
+ "@storybook/addon-essentials": "^8.4.0",
57
+ "@storybook/web-components": "^8.4.0",
58
+ "@storybook/web-components-vite": "^8.4.0",
59
+ "husky": "^9.1.7",
60
+ "lint-staged": "^16.4.0",
61
+ "storybook": "^8.4.0",
62
+ "typescript": "^5.7.0"
63
+ },
64
+ "customElements": "custom-elements.json",
65
+ "lint-staged": {
66
+ "*.{ts,mjs}": [
67
+ "biome check --write --no-errors-on-unmatched"
68
+ ]
69
+ }
70
+ }
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { dirname, join, resolve } from "node:path";
5
+
6
+ const FILENAME = "phantom-ui.d.ts";
7
+
8
+ const templates = {
9
+ react: `import type { PhantomUiAttributes } from "phantom-ui";
10
+
11
+ declare module "react/jsx-runtime" {
12
+ \texport namespace JSX {
13
+ \t\tinterface IntrinsicElements {
14
+ \t\t\t"phantom-ui": PhantomUiAttributes;
15
+ \t\t}
16
+ \t}
17
+ }
18
+ `,
19
+ solid: `import type { SolidPhantomUiAttributes } from "phantom-ui";
20
+
21
+ declare module "solid-js" {
22
+ \tnamespace JSX {
23
+ \t\tinterface IntrinsicElements {
24
+ \t\t\t"phantom-ui": SolidPhantomUiAttributes;
25
+ \t\t}
26
+ \t}
27
+ }
28
+ `,
29
+ qwik: `import type { PhantomUiAttributes } from "phantom-ui";
30
+
31
+ declare module "@builder.io/qwik" {
32
+ \tnamespace QwikJSX {
33
+ \t\tinterface IntrinsicElements {
34
+ \t\t\t"phantom-ui": PhantomUiAttributes & Record<string, unknown>;
35
+ \t\t}
36
+ \t}
37
+ }
38
+ `,
39
+ };
40
+
41
+ function findProjectRoot() {
42
+ // During postinstall, INIT_CWD is set to the directory where `npm install` was run
43
+ if (process.env.INIT_CWD && existsSync(join(process.env.INIT_CWD, "package.json"))) {
44
+ return process.env.INIT_CWD;
45
+ }
46
+
47
+ let dir = process.cwd();
48
+ // If we're inside node_modules, walk up to the project root
49
+ if (dir.includes("node_modules")) {
50
+ dir = dir.slice(0, dir.indexOf("node_modules") - 1);
51
+ }
52
+ if (existsSync(join(dir, "package.json"))) return dir;
53
+ return null;
54
+ }
55
+
56
+ function detectFramework(root) {
57
+ try {
58
+ const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
59
+ const deps = Object.keys({ ...pkg.dependencies, ...pkg.devDependencies });
60
+ const has = (name) => deps.includes(name);
61
+
62
+ if (has("react") || has("next") || has("@remix-run/react")) return "react";
63
+ if (has("solid-js")) return "solid";
64
+ if (has("@builder.io/qwik")) return "qwik";
65
+ if (has("vue") || has("nuxt")) return "vue";
66
+ if (has("svelte") || has("@sveltejs/kit")) return "svelte";
67
+ if (has("@angular/core")) return "angular";
68
+ } catch {
69
+ // no package.json
70
+ }
71
+ return null;
72
+ }
73
+
74
+ function findSrcDir(root) {
75
+ for (const dir of ["src", "app"]) {
76
+ if (existsSync(join(root, dir))) return join(root, dir);
77
+ }
78
+ return root;
79
+ }
80
+
81
+ const root = findProjectRoot();
82
+ if (!root) {
83
+ // Silent exit during postinstall if we can't find the project
84
+ process.exit(0);
85
+ }
86
+
87
+ const framework = detectFramework(root);
88
+
89
+ if (!framework) {
90
+ // Silent exit during postinstall if framework is unknown
91
+ if (process.env.npm_lifecycle_event === "postinstall") process.exit(0);
92
+ console.log("Could not detect framework from package.json.");
93
+ console.log("Run this command from your project root.");
94
+ process.exit(1);
95
+ }
96
+
97
+ if (framework === "vue" || framework === "svelte" || framework === "angular") {
98
+ if (process.env.npm_lifecycle_event !== "postinstall") {
99
+ console.log(`Detected ${framework}. No type declaration needed - types work automatically.`);
100
+ }
101
+ process.exit(0);
102
+ }
103
+
104
+ const template = templates[framework];
105
+ const srcDir = findSrcDir(root);
106
+ const outPath = join(srcDir, FILENAME);
107
+
108
+ if (existsSync(outPath)) {
109
+ if (process.env.npm_lifecycle_event !== "postinstall") {
110
+ console.log(`${outPath} already exists. Skipping.`);
111
+ }
112
+ process.exit(0);
113
+ }
114
+
115
+ writeFileSync(outPath, template);
116
+ console.log(`phantom-ui: created ${outPath} (${framework} JSX types)`);