@auto-skeleton/lit 0.0.5

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,166 @@
1
+ # @auto-skeleton/lit
2
+
3
+ Zero-config skeleton loaders for Lit and Web Components. It scans your live DOM at runtime to generate pixel-accurate skeleton bones automatically.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @auto-skeleton/lit @auto-skeleton/core
9
+ ```
10
+
11
+ ## Setup
12
+
13
+ Import the package once at your app's entry point to register the `<auto-skeleton>` custom element.
14
+
15
+ ```typescript
16
+ import '@auto-skeleton/lit';
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ### Simple Example
22
+ Wrap any component or HTML fragment with `<auto-skeleton>`. Use the `loading` property to toggle between the skeleton and the actual content.
23
+
24
+ > **Note:** The `id` property is required for layout caching.
25
+
26
+ ```typescript
27
+ import { html, LitElement } from 'lit';
28
+ import { customElement, state } from 'lit/decorators.js';
29
+ import '@auto-skeleton/lit';
30
+
31
+ @customElement('simple-card')
32
+ export class SimpleCard extends LitElement {
33
+ @state() private isLoading = true;
34
+
35
+ render() {
36
+ return html`
37
+ <auto-skeleton id="card-1" .loading=${this.isLoading}>
38
+ <div class="card">
39
+ <h2>Hello World</h2>
40
+ <p>This content will be automatically scanned.</p>
41
+ </div>
42
+ </auto-skeleton>
43
+ `;
44
+ }
45
+ }
46
+ ```
47
+
48
+ ### Advanced Configuration
49
+ Pass an `options` object to customize the animation, debugging, and scanning behavior.
50
+
51
+ ```typescript
52
+ render() {
53
+ const skeletonOptions = {
54
+ animation: 'pulse', // 'wave' | 'pulse' | 'none'
55
+ debug: true, // Show dashed outlines around detected bones
56
+ cache: true, // Enable/disable sessionStorage caching
57
+ watch: true, // Re-scan automatically on resize/DOM changes
58
+ minSize: 10, // Ignore elements smaller than 10px
59
+ ignoreSelectors: ['.badge'] // CSS selectors to exclude
60
+ };
61
+
62
+ return html`
63
+ <auto-skeleton id="adv-card" .loading=${this.isLoading} .options=${skeletonOptions}>
64
+ <div class="card">
65
+ <span class="badge">New</span>
66
+ <h3>Advanced Card</h3>
67
+ </div>
68
+ </auto-skeleton>
69
+ `;
70
+ }
71
+ ```
72
+
73
+ ### Fine-tuning with Data Attributes
74
+ Use standard data attributes on your HTML elements to guide the scanner for complex layouts.
75
+
76
+ | Attribute | Effect |
77
+ | :--- | :--- |
78
+ | `data-skeleton-ignore` | Skip this element entirely. |
79
+ | `data-skeleton-shape="circle"` | Force a circular bone (ideal for avatars). |
80
+ | `data-skeleton-lines="3"` | Force a specific number of text lines for a paragraph. |
81
+ | `data-skeleton-container` | Skip this element itself but scan its children. |
82
+
83
+ ```html
84
+ <div class="profile">
85
+ <!-- Force a circle for the avatar -->
86
+ <div class="avatar" data-skeleton-shape="circle">JD</div>
87
+
88
+ <!-- Force 2 lines for the bio -->
89
+ <p data-skeleton-lines="2">${this.bio}</p>
90
+
91
+ <!-- Hide the "Follow" button from the skeleton -->
92
+ <button data-skeleton-ignore>Follow</button>
93
+ </div>
94
+ ```
95
+
96
+ ### Custom Theming
97
+ Override colors and animations using CSS variables.
98
+
99
+ ```css
100
+ auto-skeleton {
101
+ --as-base: #e4e4e7; /* Bone background */
102
+ --as-highlight: #ffffff; /* Wave animation shimmer */
103
+ }
104
+
105
+ /* Dark Mode Example */
106
+ @media (prefers-color-scheme: dark) {
107
+ auto-skeleton {
108
+ --as-base: #27272a;
109
+ --as-highlight: rgba(255, 255, 255, 0.05);
110
+ }
111
+ }
112
+ ```
113
+
114
+ ### Complex Implementation
115
+ A full-featured example using nested components and multiple skeleton regions.
116
+
117
+ ```typescript
118
+ @customElement('social-feed')
119
+ export class SocialFeed extends LitElement {
120
+ @state() private loading = true;
121
+
122
+ render() {
123
+ return html`
124
+ <!-- Navigation Shell -->
125
+ <auto-skeleton id="nav" .loading=${this.loading}>
126
+ <nav>
127
+ <div class="logo">App</div>
128
+ <div class="user-profile" data-skeleton-shape="circle">JD</div>
129
+ </nav>
130
+ </auto-skeleton>
131
+
132
+ <!-- Main Content Area -->
133
+ <auto-skeleton id="feed" .loading=${this.loading}>
134
+ <div class="feed-container" data-skeleton-container>
135
+ ${[1, 2, 3].map(() => html`
136
+ <div class="post">
137
+ <div class="header">
138
+ <div class="avatar" data-skeleton-shape="circle"></div>
139
+ <div class="meta">
140
+ <div class="name">User Name</div>
141
+ <div class="date">2 hours ago</div>
142
+ </div>
143
+ </div>
144
+ <div class="body" data-skeleton-lines="3">
145
+ Post content goes here with multiple lines of text...
146
+ </div>
147
+ </div>
148
+ `)}
149
+ </div>
150
+ </auto-skeleton>
151
+ `;
152
+ }
153
+ }
154
+ ```
155
+
156
+ ## TypeScript Support
157
+
158
+ The package includes full TypeScript definitions:
159
+
160
+ ```typescript
161
+ import type { AutoSkeletonOptions } from '@auto-skeleton/lit';
162
+ ```
163
+
164
+ ## License
165
+
166
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,317 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key2 of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key2) && key2 !== except)
14
+ __defProp(to, key2, { get: () => from[key2], enumerable: !(desc = __getOwnPropDesc(from, key2)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var __decorateClass = (decorators, target, key2, kind) => {
20
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key2) : target;
21
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
22
+ if (decorator = decorators[i])
23
+ result = (kind ? decorator(target, key2, result) : decorator(result)) || result;
24
+ if (kind && result) __defProp(target, key2, result);
25
+ return result;
26
+ };
27
+
28
+ // src/index.ts
29
+ var index_exports = {};
30
+ __export(index_exports, {
31
+ AutoSkeleton: () => AutoSkeleton
32
+ });
33
+ module.exports = __toCommonJS(index_exports);
34
+
35
+ // src/AutoSkeleton.ts
36
+ var import_lit = require("lit");
37
+ var import_decorators = require("lit/decorators.js");
38
+ var import_core = require("@auto-skeleton/core");
39
+
40
+ // src/cache.ts
41
+ var memory = /* @__PURE__ */ new Map();
42
+ function key(id) {
43
+ const bp = typeof window !== "undefined" ? window.innerWidth : 0;
44
+ return `${id}::${bp}`;
45
+ }
46
+ function getCachedBones(id) {
47
+ const k = key(id);
48
+ if (memory.has(k)) return memory.get(k) ?? null;
49
+ if (typeof window === "undefined") return null;
50
+ const raw = window.sessionStorage.getItem(`auto-skeleton:${k}`);
51
+ if (!raw) return null;
52
+ try {
53
+ const parsed = JSON.parse(raw);
54
+ if (!Array.isArray(parsed)) return null;
55
+ memory.set(k, parsed);
56
+ return parsed;
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+ function setCachedBones(id, bones, enabled = true) {
62
+ if (!enabled) return;
63
+ const k = key(id);
64
+ memory.set(k, bones);
65
+ if (typeof window === "undefined") return;
66
+ window.sessionStorage.setItem(`auto-skeleton:${k}`, JSON.stringify(bones));
67
+ }
68
+ function clearCachedBones(id) {
69
+ const k = key(id);
70
+ memory.delete(k);
71
+ if (typeof window === "undefined") return;
72
+ window.sessionStorage.removeItem(`auto-skeleton:${k}`);
73
+ }
74
+
75
+ // src/AutoSkeleton.ts
76
+ var AutoSkeleton = class extends import_lit.LitElement {
77
+ constructor() {
78
+ super(...arguments);
79
+ this.skeletonId = "";
80
+ this.loading = false;
81
+ this.options = {};
82
+ this.bones = null;
83
+ this.scanTimer = null;
84
+ this.resizeObserver = null;
85
+ this.mutationObserver = null;
86
+ this._windowResizeHandler = null;
87
+ }
88
+ connectedCallback() {
89
+ super.connectedCallback();
90
+ if (this.cacheEnabled) {
91
+ this.bones = getCachedBones(this.skeletonId);
92
+ }
93
+ this.setupWatchers();
94
+ }
95
+ disconnectedCallback() {
96
+ super.disconnectedCallback();
97
+ this.cleanupWatchers();
98
+ }
99
+ willUpdate(changedProperties) {
100
+ const idChanged = changedProperties.has("skeletonId");
101
+ const optionsChanged = changedProperties.has("options");
102
+ const loadingChanged = changedProperties.has("loading");
103
+ if (idChanged || optionsChanged) {
104
+ if (this.cacheEnabled) {
105
+ this.bones = getCachedBones(this.skeletonId);
106
+ } else {
107
+ this.bones = null;
108
+ clearCachedBones(this.skeletonId);
109
+ }
110
+ }
111
+ if (idChanged || optionsChanged || loadingChanged) {
112
+ if (this.loading) {
113
+ this.cleanupWatchers();
114
+ } else {
115
+ this.setupWatchers();
116
+ }
117
+ }
118
+ }
119
+ updated(changedProperties) {
120
+ if (changedProperties.has("loading") && !this.loading) {
121
+ this.runScan();
122
+ }
123
+ if (changedProperties.has("loading") || changedProperties.has("skeletonId") || changedProperties.has("options")) {
124
+ this.updateHostClass();
125
+ }
126
+ }
127
+ get cacheEnabled() {
128
+ return this.options.cache ?? true;
129
+ }
130
+ updateHostClass() {
131
+ if (this.shouldShow) {
132
+ this.classList.add("as-loading");
133
+ } else {
134
+ this.classList.remove("as-loading");
135
+ }
136
+ }
137
+ runScan() {
138
+ const slot = this.shadowRoot?.querySelector("slot");
139
+ const assigned = slot ? slot.assignedElements({ flatten: true }) : Array.from(this.children);
140
+ let next;
141
+ if (assigned.length === 0) {
142
+ next = (0, import_core.scanBones)(this, {
143
+ ignoreSelectors: this.options.ignoreSelectors,
144
+ minSize: this.options.minSize
145
+ });
146
+ } else {
147
+ const opts = {
148
+ ignoreSelectors: this.options.ignoreSelectors,
149
+ minSize: this.options.minSize
150
+ };
151
+ next = assigned.flatMap((el) => (0, import_core.scanBones)(el, opts));
152
+ }
153
+ this.bones = next;
154
+ setCachedBones(this.skeletonId, next, this.cacheEnabled);
155
+ this.updateHostClass();
156
+ }
157
+ setupWatchers() {
158
+ this.cleanupWatchers();
159
+ if (this.loading) return;
160
+ if ((this.options.watch ?? true) === false) return;
161
+ const debounceMs = this.options.watchDebounceMs ?? 120;
162
+ const schedule = () => {
163
+ if (this.scanTimer !== null) window.clearTimeout(this.scanTimer);
164
+ this.scanTimer = window.setTimeout(() => {
165
+ this.runScan();
166
+ }, debounceMs);
167
+ };
168
+ if (typeof ResizeObserver !== "undefined") {
169
+ this.resizeObserver = new ResizeObserver(() => schedule());
170
+ this.resizeObserver.observe(this);
171
+ } else {
172
+ window.addEventListener("resize", schedule);
173
+ this._windowResizeHandler = schedule;
174
+ }
175
+ this.mutationObserver = new MutationObserver(() => schedule());
176
+ this.mutationObserver.observe(this, {
177
+ childList: true,
178
+ subtree: true,
179
+ characterData: true,
180
+ attributes: true
181
+ });
182
+ }
183
+ cleanupWatchers() {
184
+ if (this.scanTimer !== null) {
185
+ window.clearTimeout(this.scanTimer);
186
+ this.scanTimer = null;
187
+ }
188
+ if (this.resizeObserver) {
189
+ this.resizeObserver.disconnect();
190
+ this.resizeObserver = null;
191
+ }
192
+ if (this.mutationObserver) {
193
+ this.mutationObserver.disconnect();
194
+ this.mutationObserver = null;
195
+ }
196
+ if (this._windowResizeHandler) {
197
+ window.removeEventListener("resize", this._windowResizeHandler);
198
+ this._windowResizeHandler = null;
199
+ }
200
+ }
201
+ get shouldShow() {
202
+ return this.loading && !!this.bones && this.bones.length > 0;
203
+ }
204
+ render() {
205
+ const animation = this.options.animation ?? "wave";
206
+ const debug = this.options.debug ?? false;
207
+ return import_lit.html`
208
+ <div class="as-root">
209
+ <slot></slot>
210
+ ${this.shouldShow && this.bones ? import_lit.html`
211
+ <div class="as-overlay" aria-hidden="true">
212
+ ${this.bones.map(
213
+ (b, i) => import_lit.html`
214
+ <span
215
+ class="as-bone as-${animation}${debug ? " as-debug" : ""}"
216
+ style="left: ${b.x}px; top: ${b.y}px; width: ${b.width}px; height: ${b.height}px; border-radius: ${b.kind === "circle" ? "50%" : `${b.radius}px`}"
217
+ ></span>
218
+ `
219
+ )}
220
+ </div>
221
+ ` : null}
222
+ </div>
223
+ `;
224
+ }
225
+ };
226
+ AutoSkeleton.styles = import_lit.css`
227
+ :host {
228
+ display: block;
229
+ position: relative;
230
+ }
231
+
232
+ .as-root {
233
+ position: relative;
234
+ }
235
+
236
+ :host(.as-loading) slot {
237
+ visibility: hidden;
238
+ }
239
+
240
+ .as-overlay {
241
+ position: absolute;
242
+ inset: 0;
243
+ pointer-events: none;
244
+ z-index: 10;
245
+ }
246
+
247
+ .as-bone {
248
+ position: absolute;
249
+ background: var(--as-base, #e4e4e7);
250
+ overflow: hidden;
251
+ }
252
+
253
+ .as-bone.as-wave::after {
254
+ content: "";
255
+ position: absolute;
256
+ inset: 0;
257
+ transform: translateX(-100%);
258
+ background: linear-gradient(
259
+ 90deg,
260
+ transparent 0%,
261
+ var(--as-highlight, rgba(255, 255, 255, 0.9)) 45%,
262
+ transparent 100%
263
+ );
264
+ animation: as-wave 1.2s infinite;
265
+ }
266
+
267
+ .as-bone.as-pulse {
268
+ animation: as-pulse 1.1s ease-in-out infinite;
269
+ }
270
+
271
+ .as-bone.as-debug {
272
+ outline: 1px dashed var(--as-debug, rgba(255, 99, 71, 0.45));
273
+ outline-offset: -1px;
274
+ }
275
+
276
+ @keyframes as-wave {
277
+ 100% {
278
+ transform: translateX(100%);
279
+ }
280
+ }
281
+
282
+ @keyframes as-pulse {
283
+ 0%,
284
+ 100% {
285
+ opacity: 1;
286
+ }
287
+ 50% {
288
+ opacity: 0.55;
289
+ }
290
+ }
291
+
292
+ @media (prefers-reduced-motion: reduce) {
293
+ .as-bone.as-wave::after,
294
+ .as-bone.as-pulse {
295
+ animation: none;
296
+ }
297
+ }
298
+ `;
299
+ __decorateClass([
300
+ (0, import_decorators.property)({ type: String, attribute: "skeleton-id" })
301
+ ], AutoSkeleton.prototype, "skeletonId", 2);
302
+ __decorateClass([
303
+ (0, import_decorators.property)({ type: Boolean })
304
+ ], AutoSkeleton.prototype, "loading", 2);
305
+ __decorateClass([
306
+ (0, import_decorators.property)({ type: Object })
307
+ ], AutoSkeleton.prototype, "options", 2);
308
+ __decorateClass([
309
+ (0, import_decorators.state)()
310
+ ], AutoSkeleton.prototype, "bones", 2);
311
+ AutoSkeleton = __decorateClass([
312
+ (0, import_decorators.customElement)("auto-skeleton")
313
+ ], AutoSkeleton);
314
+ // Annotate the CommonJS export names for ESM import in node:
315
+ 0 && (module.exports = {
316
+ AutoSkeleton
317
+ });
@@ -0,0 +1,46 @@
1
+ import * as lit_html from 'lit-html';
2
+ import * as lit from 'lit';
3
+ import { LitElement, PropertyValues } from 'lit';
4
+ import { ScanOptions } from '@auto-skeleton/core';
5
+
6
+ type AutoSkeletonOptions = ScanOptions & {
7
+ animation?: "wave" | "pulse" | "none";
8
+ debug?: boolean;
9
+ /** Re-scan when layout/content mutates while not loading. */
10
+ watch?: boolean;
11
+ /** Debounce for watcher-driven re-scan. */
12
+ watchDebounceMs?: number;
13
+ /** Disable sessionStorage caching if needed. */
14
+ cache?: boolean;
15
+ };
16
+
17
+ declare class AutoSkeleton extends LitElement {
18
+ /** Unique cache key — distinct from the native HTML `id` attribute. */
19
+ skeletonId: string;
20
+ loading: boolean;
21
+ options: AutoSkeletonOptions;
22
+ private bones;
23
+ private scanTimer;
24
+ private resizeObserver;
25
+ private mutationObserver;
26
+ static styles: lit.CSSResult;
27
+ connectedCallback(): void;
28
+ disconnectedCallback(): void;
29
+ willUpdate(changedProperties: PropertyValues<this>): void;
30
+ updated(changedProperties: PropertyValues<this>): void;
31
+ private get cacheEnabled();
32
+ private updateHostClass;
33
+ private runScan;
34
+ private setupWatchers;
35
+ private _windowResizeHandler;
36
+ private cleanupWatchers;
37
+ private get shouldShow();
38
+ render(): lit_html.TemplateResult<1>;
39
+ }
40
+ declare global {
41
+ interface HTMLElementTagNameMap {
42
+ "auto-skeleton": AutoSkeleton;
43
+ }
44
+ }
45
+
46
+ export { AutoSkeleton, type AutoSkeletonOptions };
@@ -0,0 +1,46 @@
1
+ import * as lit_html from 'lit-html';
2
+ import * as lit from 'lit';
3
+ import { LitElement, PropertyValues } from 'lit';
4
+ import { ScanOptions } from '@auto-skeleton/core';
5
+
6
+ type AutoSkeletonOptions = ScanOptions & {
7
+ animation?: "wave" | "pulse" | "none";
8
+ debug?: boolean;
9
+ /** Re-scan when layout/content mutates while not loading. */
10
+ watch?: boolean;
11
+ /** Debounce for watcher-driven re-scan. */
12
+ watchDebounceMs?: number;
13
+ /** Disable sessionStorage caching if needed. */
14
+ cache?: boolean;
15
+ };
16
+
17
+ declare class AutoSkeleton extends LitElement {
18
+ /** Unique cache key — distinct from the native HTML `id` attribute. */
19
+ skeletonId: string;
20
+ loading: boolean;
21
+ options: AutoSkeletonOptions;
22
+ private bones;
23
+ private scanTimer;
24
+ private resizeObserver;
25
+ private mutationObserver;
26
+ static styles: lit.CSSResult;
27
+ connectedCallback(): void;
28
+ disconnectedCallback(): void;
29
+ willUpdate(changedProperties: PropertyValues<this>): void;
30
+ updated(changedProperties: PropertyValues<this>): void;
31
+ private get cacheEnabled();
32
+ private updateHostClass;
33
+ private runScan;
34
+ private setupWatchers;
35
+ private _windowResizeHandler;
36
+ private cleanupWatchers;
37
+ private get shouldShow();
38
+ render(): lit_html.TemplateResult<1>;
39
+ }
40
+ declare global {
41
+ interface HTMLElementTagNameMap {
42
+ "auto-skeleton": AutoSkeleton;
43
+ }
44
+ }
45
+
46
+ export { AutoSkeleton, type AutoSkeletonOptions };
package/dist/index.js ADDED
@@ -0,0 +1,293 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __decorateClass = (decorators, target, key2, kind) => {
4
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key2) : target;
5
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
6
+ if (decorator = decorators[i])
7
+ result = (kind ? decorator(target, key2, result) : decorator(result)) || result;
8
+ if (kind && result) __defProp(target, key2, result);
9
+ return result;
10
+ };
11
+
12
+ // src/AutoSkeleton.ts
13
+ import { LitElement, html, css } from "lit";
14
+ import { customElement, property, state } from "lit/decorators.js";
15
+ import { scanBones } from "@auto-skeleton/core";
16
+
17
+ // src/cache.ts
18
+ var memory = /* @__PURE__ */ new Map();
19
+ function key(id) {
20
+ const bp = typeof window !== "undefined" ? window.innerWidth : 0;
21
+ return `${id}::${bp}`;
22
+ }
23
+ function getCachedBones(id) {
24
+ const k = key(id);
25
+ if (memory.has(k)) return memory.get(k) ?? null;
26
+ if (typeof window === "undefined") return null;
27
+ const raw = window.sessionStorage.getItem(`auto-skeleton:${k}`);
28
+ if (!raw) return null;
29
+ try {
30
+ const parsed = JSON.parse(raw);
31
+ if (!Array.isArray(parsed)) return null;
32
+ memory.set(k, parsed);
33
+ return parsed;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+ function setCachedBones(id, bones, enabled = true) {
39
+ if (!enabled) return;
40
+ const k = key(id);
41
+ memory.set(k, bones);
42
+ if (typeof window === "undefined") return;
43
+ window.sessionStorage.setItem(`auto-skeleton:${k}`, JSON.stringify(bones));
44
+ }
45
+ function clearCachedBones(id) {
46
+ const k = key(id);
47
+ memory.delete(k);
48
+ if (typeof window === "undefined") return;
49
+ window.sessionStorage.removeItem(`auto-skeleton:${k}`);
50
+ }
51
+
52
+ // src/AutoSkeleton.ts
53
+ var AutoSkeleton = class extends LitElement {
54
+ constructor() {
55
+ super(...arguments);
56
+ this.skeletonId = "";
57
+ this.loading = false;
58
+ this.options = {};
59
+ this.bones = null;
60
+ this.scanTimer = null;
61
+ this.resizeObserver = null;
62
+ this.mutationObserver = null;
63
+ this._windowResizeHandler = null;
64
+ }
65
+ connectedCallback() {
66
+ super.connectedCallback();
67
+ if (this.cacheEnabled) {
68
+ this.bones = getCachedBones(this.skeletonId);
69
+ }
70
+ this.setupWatchers();
71
+ }
72
+ disconnectedCallback() {
73
+ super.disconnectedCallback();
74
+ this.cleanupWatchers();
75
+ }
76
+ willUpdate(changedProperties) {
77
+ const idChanged = changedProperties.has("skeletonId");
78
+ const optionsChanged = changedProperties.has("options");
79
+ const loadingChanged = changedProperties.has("loading");
80
+ if (idChanged || optionsChanged) {
81
+ if (this.cacheEnabled) {
82
+ this.bones = getCachedBones(this.skeletonId);
83
+ } else {
84
+ this.bones = null;
85
+ clearCachedBones(this.skeletonId);
86
+ }
87
+ }
88
+ if (idChanged || optionsChanged || loadingChanged) {
89
+ if (this.loading) {
90
+ this.cleanupWatchers();
91
+ } else {
92
+ this.setupWatchers();
93
+ }
94
+ }
95
+ }
96
+ updated(changedProperties) {
97
+ if (changedProperties.has("loading") && !this.loading) {
98
+ this.runScan();
99
+ }
100
+ if (changedProperties.has("loading") || changedProperties.has("skeletonId") || changedProperties.has("options")) {
101
+ this.updateHostClass();
102
+ }
103
+ }
104
+ get cacheEnabled() {
105
+ return this.options.cache ?? true;
106
+ }
107
+ updateHostClass() {
108
+ if (this.shouldShow) {
109
+ this.classList.add("as-loading");
110
+ } else {
111
+ this.classList.remove("as-loading");
112
+ }
113
+ }
114
+ runScan() {
115
+ const slot = this.shadowRoot?.querySelector("slot");
116
+ const assigned = slot ? slot.assignedElements({ flatten: true }) : Array.from(this.children);
117
+ let next;
118
+ if (assigned.length === 0) {
119
+ next = scanBones(this, {
120
+ ignoreSelectors: this.options.ignoreSelectors,
121
+ minSize: this.options.minSize
122
+ });
123
+ } else {
124
+ const opts = {
125
+ ignoreSelectors: this.options.ignoreSelectors,
126
+ minSize: this.options.minSize
127
+ };
128
+ next = assigned.flatMap((el) => scanBones(el, opts));
129
+ }
130
+ this.bones = next;
131
+ setCachedBones(this.skeletonId, next, this.cacheEnabled);
132
+ this.updateHostClass();
133
+ }
134
+ setupWatchers() {
135
+ this.cleanupWatchers();
136
+ if (this.loading) return;
137
+ if ((this.options.watch ?? true) === false) return;
138
+ const debounceMs = this.options.watchDebounceMs ?? 120;
139
+ const schedule = () => {
140
+ if (this.scanTimer !== null) window.clearTimeout(this.scanTimer);
141
+ this.scanTimer = window.setTimeout(() => {
142
+ this.runScan();
143
+ }, debounceMs);
144
+ };
145
+ if (typeof ResizeObserver !== "undefined") {
146
+ this.resizeObserver = new ResizeObserver(() => schedule());
147
+ this.resizeObserver.observe(this);
148
+ } else {
149
+ window.addEventListener("resize", schedule);
150
+ this._windowResizeHandler = schedule;
151
+ }
152
+ this.mutationObserver = new MutationObserver(() => schedule());
153
+ this.mutationObserver.observe(this, {
154
+ childList: true,
155
+ subtree: true,
156
+ characterData: true,
157
+ attributes: true
158
+ });
159
+ }
160
+ cleanupWatchers() {
161
+ if (this.scanTimer !== null) {
162
+ window.clearTimeout(this.scanTimer);
163
+ this.scanTimer = null;
164
+ }
165
+ if (this.resizeObserver) {
166
+ this.resizeObserver.disconnect();
167
+ this.resizeObserver = null;
168
+ }
169
+ if (this.mutationObserver) {
170
+ this.mutationObserver.disconnect();
171
+ this.mutationObserver = null;
172
+ }
173
+ if (this._windowResizeHandler) {
174
+ window.removeEventListener("resize", this._windowResizeHandler);
175
+ this._windowResizeHandler = null;
176
+ }
177
+ }
178
+ get shouldShow() {
179
+ return this.loading && !!this.bones && this.bones.length > 0;
180
+ }
181
+ render() {
182
+ const animation = this.options.animation ?? "wave";
183
+ const debug = this.options.debug ?? false;
184
+ return html`
185
+ <div class="as-root">
186
+ <slot></slot>
187
+ ${this.shouldShow && this.bones ? html`
188
+ <div class="as-overlay" aria-hidden="true">
189
+ ${this.bones.map(
190
+ (b, i) => html`
191
+ <span
192
+ class="as-bone as-${animation}${debug ? " as-debug" : ""}"
193
+ style="left: ${b.x}px; top: ${b.y}px; width: ${b.width}px; height: ${b.height}px; border-radius: ${b.kind === "circle" ? "50%" : `${b.radius}px`}"
194
+ ></span>
195
+ `
196
+ )}
197
+ </div>
198
+ ` : null}
199
+ </div>
200
+ `;
201
+ }
202
+ };
203
+ AutoSkeleton.styles = css`
204
+ :host {
205
+ display: block;
206
+ position: relative;
207
+ }
208
+
209
+ .as-root {
210
+ position: relative;
211
+ }
212
+
213
+ :host(.as-loading) slot {
214
+ visibility: hidden;
215
+ }
216
+
217
+ .as-overlay {
218
+ position: absolute;
219
+ inset: 0;
220
+ pointer-events: none;
221
+ z-index: 10;
222
+ }
223
+
224
+ .as-bone {
225
+ position: absolute;
226
+ background: var(--as-base, #e4e4e7);
227
+ overflow: hidden;
228
+ }
229
+
230
+ .as-bone.as-wave::after {
231
+ content: "";
232
+ position: absolute;
233
+ inset: 0;
234
+ transform: translateX(-100%);
235
+ background: linear-gradient(
236
+ 90deg,
237
+ transparent 0%,
238
+ var(--as-highlight, rgba(255, 255, 255, 0.9)) 45%,
239
+ transparent 100%
240
+ );
241
+ animation: as-wave 1.2s infinite;
242
+ }
243
+
244
+ .as-bone.as-pulse {
245
+ animation: as-pulse 1.1s ease-in-out infinite;
246
+ }
247
+
248
+ .as-bone.as-debug {
249
+ outline: 1px dashed var(--as-debug, rgba(255, 99, 71, 0.45));
250
+ outline-offset: -1px;
251
+ }
252
+
253
+ @keyframes as-wave {
254
+ 100% {
255
+ transform: translateX(100%);
256
+ }
257
+ }
258
+
259
+ @keyframes as-pulse {
260
+ 0%,
261
+ 100% {
262
+ opacity: 1;
263
+ }
264
+ 50% {
265
+ opacity: 0.55;
266
+ }
267
+ }
268
+
269
+ @media (prefers-reduced-motion: reduce) {
270
+ .as-bone.as-wave::after,
271
+ .as-bone.as-pulse {
272
+ animation: none;
273
+ }
274
+ }
275
+ `;
276
+ __decorateClass([
277
+ property({ type: String, attribute: "skeleton-id" })
278
+ ], AutoSkeleton.prototype, "skeletonId", 2);
279
+ __decorateClass([
280
+ property({ type: Boolean })
281
+ ], AutoSkeleton.prototype, "loading", 2);
282
+ __decorateClass([
283
+ property({ type: Object })
284
+ ], AutoSkeleton.prototype, "options", 2);
285
+ __decorateClass([
286
+ state()
287
+ ], AutoSkeleton.prototype, "bones", 2);
288
+ AutoSkeleton = __decorateClass([
289
+ customElement("auto-skeleton")
290
+ ], AutoSkeleton);
291
+ export {
292
+ AutoSkeleton
293
+ };
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@auto-skeleton/lit",
3
+ "version": "0.0.5",
4
+ "main": "dist/index.cjs",
5
+ "module": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "require": "./dist/index.cjs"
12
+ }
13
+ },
14
+ "keywords": [
15
+ "skeleton",
16
+ "skeleton-loader",
17
+ "loading",
18
+ "placeholder",
19
+ "shimmer",
20
+ "lit",
21
+ "web-components",
22
+ "auto-skeleton",
23
+ "dom",
24
+ "zero-config",
25
+ "ui",
26
+ "ux"
27
+ ],
28
+ "description": "Zero-config skeleton loaders for Lit and Web Components. Scans the live DOM at runtime — no manual shapes, no CLI, no config files.",
29
+ "license": "MIT",
30
+ "files": [
31
+ "dist",
32
+ "README.md"
33
+ ],
34
+ "scripts": {
35
+ "build": "tsup",
36
+ "typecheck": "tsc -p tsconfig.json --noEmit"
37
+ },
38
+ "peerDependencies": {
39
+ "lit": ">=2"
40
+ },
41
+ "dependencies": {
42
+ "@auto-skeleton/core": "0.0.5"
43
+ }
44
+ }