@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 +166 -0
- package/dist/index.cjs +317 -0
- package/dist/index.d.mts +46 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +293 -0
- package/package.json +44 -0
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
|
+
});
|
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|