skull-dom 1.0.1 → 1.0.3
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 +21 -21
- package/README.md +178 -165
- package/dist/adapters/react/index.d.ts +11 -1
- package/dist/adapters/react/index.d.ts.map +1 -1
- package/dist/adapters/react/index.js +4 -4
- package/dist/adapters/solid/index.d.ts +9 -2
- package/dist/adapters/solid/index.d.ts.map +1 -1
- package/dist/adapters/solid/index.js +5 -2
- package/dist/adapters/svelte/index.d.ts +8 -1
- package/dist/adapters/svelte/index.d.ts.map +1 -1
- package/dist/adapters/svelte/index.js +13 -7
- package/dist/adapters/vanilla/index.d.ts.map +1 -1
- package/dist/adapters/vanilla/index.js +11 -1
- package/dist/adapters/vue/index.d.ts +8 -4
- package/dist/adapters/vue/index.d.ts.map +1 -1
- package/dist/adapters/vue/index.js +16 -8
- package/dist/core/infer/inferNode.d.ts.map +1 -1
- package/dist/core/infer/inferNode.js +53 -9
- package/dist/core/infer/types.d.ts +11 -0
- package/dist/core/infer/types.d.ts.map +1 -1
- package/dist/core/lifecycle/SkeletonOverlayManager.js +1 -1
- package/dist/core/lifecycle/attachSkeleton.d.ts +2 -1
- package/dist/core/lifecycle/attachSkeleton.d.ts.map +1 -1
- package/dist/core/lifecycle/attachSkeleton.js +6 -3
- package/dist/core/render/renderSkeleton.d.ts +2 -2
- package/dist/core/render/renderSkeleton.d.ts.map +1 -1
- package/dist/core/render/renderSkeleton.js +52 -14
- package/dist/core/styles/styles.d.ts +3 -0
- package/dist/core/styles/styles.d.ts.map +1 -0
- package/dist/core/styles/styles.js +114 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/package.json +4 -1
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2024 AutoSkeleton
|
|
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.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 AutoSkeleton
|
|
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
CHANGED
|
@@ -1,165 +1,178 @@
|
|
|
1
|
-
# 💀 SkullDOM
|
|
2
|
-
|
|
3
|
-
>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Most skeleton libraries require you to manually build "skeleton versions" of your components.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
<
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
<div use:skeleton={isLoading()}>
|
|
105
|
-
|
|
106
|
-
</div>
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
1
|
+
# 💀 SkullDOM
|
|
2
|
+
|
|
3
|
+
> **Automagic skeleton screens for any framework.**
|
|
4
|
+
> Zero config. Zero manual styles. Just robust, layout-aware loading states.
|
|
5
|
+
|
|
6
|
+
## Why SkullDOM?
|
|
7
|
+
Most skeleton libraries require you to manually build "skeleton versions" of your components.
|
|
8
|
+
**SkullDOM is different.** It looks at your **existing DOM**, infers its layout (text, images, grids, flex), and overlays a pixel-perfect skeleton on top of it.
|
|
9
|
+
|
|
10
|
+
- **Framework Agnostic:** First-class adapters for React, Vue, Svelte, Solid, and Vanilla.
|
|
11
|
+
- **0 Layout Shift:** Uses `Range` API to capture exact text wrapping and dimensions.
|
|
12
|
+
- **12+ Animations:** Built-in premium animations (Wave, Neon, Glimmer, etc.).
|
|
13
|
+
- **Automagic Inference:** Detects text lines, border radii, and flex gaps automatically.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 📦 Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install skull-dom
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 🚀 Usage
|
|
26
|
+
|
|
27
|
+
### React
|
|
28
|
+
Use the `useSkeleton` hook.
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
import { useRef } from 'react';
|
|
32
|
+
import { useSkeleton } from 'skull-dom/react';
|
|
33
|
+
|
|
34
|
+
function UserCard({ isLoading, user }) {
|
|
35
|
+
const ref = useRef(null);
|
|
36
|
+
|
|
37
|
+
// 1. Basic Usage
|
|
38
|
+
useSkeleton(ref, isLoading);
|
|
39
|
+
|
|
40
|
+
// 2. With Configuration
|
|
41
|
+
// useSkeleton(ref, isLoading, {
|
|
42
|
+
// animation: 'neon',
|
|
43
|
+
// color: '#3b82f6'
|
|
44
|
+
// });
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div ref={ref} className="card">
|
|
48
|
+
<img src={user.avatar} />
|
|
49
|
+
<h3>{user.name}</h3>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Vue
|
|
56
|
+
Use the `v-skeleton` directive.
|
|
57
|
+
|
|
58
|
+
```vue
|
|
59
|
+
<script setup>
|
|
60
|
+
import { vSkeleton } from 'skull-dom/vue';
|
|
61
|
+
defineProps(['loading']);
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<template>
|
|
65
|
+
<!-- Basic -->
|
|
66
|
+
<div v-skeleton="loading" class="card">
|
|
67
|
+
<h1>Content</h1>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<!-- With Configuration -->
|
|
71
|
+
<div v-skeleton="{ loading, config: { animation: 'pulse' } }">
|
|
72
|
+
<h1>Content</h1>
|
|
73
|
+
</div>
|
|
74
|
+
</template>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Svelte
|
|
78
|
+
Use the `skeleton` action.
|
|
79
|
+
|
|
80
|
+
```svelte
|
|
81
|
+
<script>
|
|
82
|
+
import { skeleton } from 'skull-dom/svelte';
|
|
83
|
+
export let loading = true;
|
|
84
|
+
</script>
|
|
85
|
+
|
|
86
|
+
<!-- Basic -->
|
|
87
|
+
<div use:skeleton={loading}>
|
|
88
|
+
<h1>Content</h1>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<!-- With Configuration -->
|
|
92
|
+
<div use:skeleton={{ loading, config: { animation: 'glimmer' } }}>
|
|
93
|
+
<h1>Content</h1>
|
|
94
|
+
</div>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### SolidJS
|
|
98
|
+
Use the `skeleton` directive.
|
|
99
|
+
|
|
100
|
+
```jsx
|
|
101
|
+
import { skeleton } from 'skull-dom/solid';
|
|
102
|
+
|
|
103
|
+
// Basic
|
|
104
|
+
<div use:skeleton={isLoading()}>
|
|
105
|
+
<h1>Content</h1>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
// With Configuration
|
|
109
|
+
<div use:skeleton={{ loading: isLoading(), config: { animation: 'wave', borderRadius: 8 } }}>
|
|
110
|
+
<h1>Content</h1>
|
|
111
|
+
</div>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Vanilla / HTML
|
|
115
|
+
Add `data-skeleton="loading"` to any element.
|
|
116
|
+
|
|
117
|
+
```html
|
|
118
|
+
<!-- 1. Initialize once -->
|
|
119
|
+
<script type="module">
|
|
120
|
+
import { initSkeletonObserver } from 'skull-dom/adapters/vanilla';
|
|
121
|
+
initSkeletonObserver();
|
|
122
|
+
</script>
|
|
123
|
+
|
|
124
|
+
<!-- 2. Toggle attribute to show skeleton -->
|
|
125
|
+
<div data-skeleton="loading">
|
|
126
|
+
<h1>I am covered!</h1>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<!-- 3. With Configuration (JSON) -->
|
|
130
|
+
<div
|
|
131
|
+
data-skeleton="loading"
|
|
132
|
+
data-skeleton-config='{ "animation": "neon", "color": "#10b981" }'
|
|
133
|
+
>
|
|
134
|
+
<h1>Custom Style</h1>
|
|
135
|
+
</div>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## 🎨 Animations & Config
|
|
141
|
+
|
|
142
|
+
You can customize the look and feel by passing a config object.
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
interface SkeletonConfig {
|
|
146
|
+
animation?: 'wave' | 'pulse' | 'glimmer' | 'scan' | 'breathing'
|
|
147
|
+
| 'fade-in' | 'slide-in' | 'rumble' | 'neon'
|
|
148
|
+
| 'classic' | 'shimmer-vertical' | 'bounce';
|
|
149
|
+
|
|
150
|
+
color?: string; // Base background color (e.g. #d1d5db)
|
|
151
|
+
highlightColor?: string; // Shimmer/Highlight color
|
|
152
|
+
borderRadius?: number; // Force global border radius
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Animation Presets
|
|
157
|
+
- **`wave`** (Default): Standard left-to-right gradient shimmer.
|
|
158
|
+
- **`pulse`**: Opacity fades in and out.
|
|
159
|
+
- **`glimmer`**: Thin white reflection shoots across.
|
|
160
|
+
- **`scan`**: Cyberpunk-style vertical scanner.
|
|
161
|
+
- **`neon`**: Glowing box-shadow pulse.
|
|
162
|
+
- **`breathing`**: Subtle scale up/down.
|
|
163
|
+
- **`classic`**: Static gray box (no animation).
|
|
164
|
+
- And more! (`slide-in`, `fade-in`, `rumble`, `bounce`...)
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## 🧠 How it Works regarding "Zero Layout Shift"
|
|
169
|
+
|
|
170
|
+
SkullDOM uses the browser's **Range API** to create a pixel-perfect "snapshot" of your text nodes.
|
|
171
|
+
Instead of guessing how many lines of text you have, it measures the **exact bounding box of every line** as rendered by the browser.
|
|
172
|
+
|
|
173
|
+
This means if your text wraps in a unique way around an image, the skeleton will wrap exactly the same way.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## 📄 License
|
|
178
|
+
MIT
|
|
@@ -7,5 +7,15 @@
|
|
|
7
7
|
* useSkeleton(ref, loading);
|
|
8
8
|
* <div ref={ref}>Content</div>
|
|
9
9
|
*/
|
|
10
|
-
|
|
10
|
+
import { SkeletonConfig } from "../../core/infer/types.js";
|
|
11
|
+
/**
|
|
12
|
+
* A React hook that automatically attaches a skeleton overlay to the referenced element
|
|
13
|
+
* when `loading` is true.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* const ref = useRef(null);
|
|
17
|
+
* useSkeleton(ref, loading, { animation: 'pulse' });
|
|
18
|
+
* <div ref={ref}>Content</div>
|
|
19
|
+
*/
|
|
20
|
+
export declare function useSkeleton(ref: React.RefObject<HTMLElement | null>, loading: boolean, config?: SkeletonConfig): void;
|
|
11
21
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/adapters/react/index.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CACvB,GAAG,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,EACxC,OAAO,EAAE,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/adapters/react/index.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAE3D;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CACvB,GAAG,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,EACxC,OAAO,EAAE,OAAO,EAChB,MAAM,CAAC,EAAE,cAAc,QAkB1B"}
|
|
@@ -6,22 +6,22 @@ import { attachSkeleton } from "../../core/lifecycle/attachSkeleton.js";
|
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
8
|
* const ref = useRef(null);
|
|
9
|
-
* useSkeleton(ref, loading);
|
|
9
|
+
* useSkeleton(ref, loading, { animation: 'pulse' });
|
|
10
10
|
* <div ref={ref}>Content</div>
|
|
11
11
|
*/
|
|
12
|
-
export function useSkeleton(ref, loading) {
|
|
12
|
+
export function useSkeleton(ref, loading, config) {
|
|
13
13
|
useEffect(() => {
|
|
14
14
|
const element = ref.current;
|
|
15
15
|
// Only attach if we have an element AND we are loading
|
|
16
16
|
if (!element || !loading)
|
|
17
17
|
return;
|
|
18
18
|
// Attach returns a cleanup function
|
|
19
|
-
const cleanup = attachSkeleton(element);
|
|
19
|
+
const cleanup = attachSkeleton(element, config);
|
|
20
20
|
// React calls this when:
|
|
21
21
|
// 1. Component unmounts
|
|
22
22
|
// 2. dependencies change (loading becomes false)
|
|
23
23
|
return () => {
|
|
24
24
|
cleanup();
|
|
25
25
|
};
|
|
26
|
-
}, [loading, ref]); // re-run if
|
|
26
|
+
}, [loading, ref, JSON.stringify(config)]); // re-run if config changes (deep compare via stringify for simple objects)
|
|
27
27
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import { SkeletonConfig } from "../../core/infer/types.js";
|
|
2
|
+
type SkeletonAccessor = boolean | {
|
|
3
|
+
loading: boolean;
|
|
4
|
+
config?: SkeletonConfig;
|
|
5
|
+
};
|
|
1
6
|
declare module "solid-js" {
|
|
2
7
|
namespace JSX {
|
|
3
8
|
interface Directives {
|
|
4
|
-
skeleton:
|
|
9
|
+
skeleton: SkeletonAccessor;
|
|
5
10
|
}
|
|
6
11
|
}
|
|
7
12
|
}
|
|
@@ -10,6 +15,8 @@ declare module "solid-js" {
|
|
|
10
15
|
*
|
|
11
16
|
* Usage:
|
|
12
17
|
* <div use:skeleton={loading()}>...</div>
|
|
18
|
+
* <div use:skeleton={{ loading: loading(), config: { animation: 'pulse' } }}>...</div>
|
|
13
19
|
*/
|
|
14
|
-
export declare function skeleton(el: HTMLElement, accessor: () =>
|
|
20
|
+
export declare function skeleton(el: HTMLElement, accessor: () => SkeletonAccessor): void;
|
|
21
|
+
export {};
|
|
15
22
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/adapters/solid/index.ts"],"names":[],"mappings":"AAGA,OAAO,QAAQ,UAAU,CAAC;IACtB,UAAU,GAAG,CAAC;QACV,UAAU,UAAU;YAChB,QAAQ,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/adapters/solid/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAE3D,KAAK,gBAAgB,GAAG,OAAO,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,cAAc,CAAA;CAAE,CAAC;AAEhF,OAAO,QAAQ,UAAU,CAAC;IACtB,UAAU,GAAG,CAAC;QACV,UAAU,UAAU;YAChB,QAAQ,EAAE,gBAAgB,CAAC;SAC9B;KACJ;CACJ;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,EAAE,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,gBAAgB,QAWzE"}
|
|
@@ -5,12 +5,15 @@ import { attachSkeleton } from "../../core/lifecycle/attachSkeleton.js";
|
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
7
|
* <div use:skeleton={loading()}>...</div>
|
|
8
|
+
* <div use:skeleton={{ loading: loading(), config: { animation: 'pulse' } }}>...</div>
|
|
8
9
|
*/
|
|
9
10
|
export function skeleton(el, accessor) {
|
|
10
11
|
createEffect(() => {
|
|
11
|
-
const
|
|
12
|
+
const value = accessor();
|
|
13
|
+
const loading = typeof value === 'boolean' ? value : value.loading;
|
|
14
|
+
const config = typeof value === 'boolean' ? undefined : value.config;
|
|
12
15
|
if (loading) {
|
|
13
|
-
const detach = attachSkeleton(el);
|
|
16
|
+
const detach = attachSkeleton(el, config);
|
|
14
17
|
onCleanup(() => detach());
|
|
15
18
|
}
|
|
16
19
|
});
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { Action } from "svelte/action";
|
|
2
|
+
import { SkeletonConfig } from "../../core/infer/types.js";
|
|
3
|
+
type SkeletonParams = boolean | {
|
|
4
|
+
loading: boolean;
|
|
5
|
+
config?: SkeletonConfig;
|
|
6
|
+
};
|
|
2
7
|
/**
|
|
3
8
|
* A Svelte Action that attaches a skeleton when `loading` is true.
|
|
4
9
|
*
|
|
5
10
|
* Usage:
|
|
6
11
|
* <div use:skeleton={loading}>...</div>
|
|
12
|
+
* <div use:skeleton={{ loading, config: { animation: 'wave' } }}>...</div>
|
|
7
13
|
*/
|
|
8
|
-
export declare const skeleton: Action<HTMLElement,
|
|
14
|
+
export declare const skeleton: Action<HTMLElement, SkeletonParams>;
|
|
15
|
+
export {};
|
|
9
16
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/adapters/svelte/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAGvC
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/adapters/svelte/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAGvC,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAE3D,KAAK,cAAc,GAAG,OAAO,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,cAAc,CAAA;CAAE,CAAC;AAE9E;;;;;;GAMG;AACH,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,WAAW,EAAE,cAAc,CA+BxD,CAAC"}
|
|
@@ -4,22 +4,28 @@ import { attachSkeleton } from "../../core/lifecycle/attachSkeleton.js";
|
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
6
|
* <div use:skeleton={loading}>...</div>
|
|
7
|
+
* <div use:skeleton={{ loading, config: { animation: 'wave' } }}>...</div>
|
|
7
8
|
*/
|
|
8
|
-
export const skeleton = (node,
|
|
9
|
+
export const skeleton = (node, params) => {
|
|
9
10
|
let cleanup = null;
|
|
11
|
+
const isLoading = (p) => typeof p === 'boolean' ? p : p.loading;
|
|
12
|
+
const getConfig = (p) => typeof p === 'boolean' ? undefined : p.config;
|
|
10
13
|
// Initial state
|
|
11
|
-
if (
|
|
12
|
-
cleanup = attachSkeleton(node);
|
|
14
|
+
if (isLoading(params)) {
|
|
15
|
+
cleanup = attachSkeleton(node, getConfig(params));
|
|
13
16
|
}
|
|
14
17
|
return {
|
|
15
|
-
update(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
update(newParams) {
|
|
19
|
+
const loading = isLoading(newParams);
|
|
20
|
+
const config = getConfig(newParams);
|
|
21
|
+
if (loading && !cleanup) {
|
|
22
|
+
cleanup = attachSkeleton(node, config);
|
|
18
23
|
}
|
|
19
|
-
else if (!
|
|
24
|
+
else if (!loading && cleanup) {
|
|
20
25
|
cleanup();
|
|
21
26
|
cleanup = null;
|
|
22
27
|
}
|
|
28
|
+
// Ideally we also handle config updates while loading by re-attaching
|
|
23
29
|
},
|
|
24
30
|
destroy() {
|
|
25
31
|
if (cleanup) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/adapters/vanilla/index.ts"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/adapters/vanilla/index.ts"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,qBAgDnC"}
|
|
@@ -16,7 +16,17 @@ export function initSkeletonObserver() {
|
|
|
16
16
|
const state = el.getAttribute("data-skeleton");
|
|
17
17
|
if (state === "loading" && !cleanupMap.has(el)) {
|
|
18
18
|
// Start loading
|
|
19
|
-
|
|
19
|
+
let config = undefined;
|
|
20
|
+
const configStr = el.getAttribute("data-skeleton-config");
|
|
21
|
+
if (configStr) {
|
|
22
|
+
try {
|
|
23
|
+
config = JSON.parse(configStr);
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
console.warn("SkullDOM: Invalid JSON in data-skeleton-config", configStr);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const cleanup = attachSkeleton(el, config);
|
|
20
30
|
cleanupMap.set(el, cleanup);
|
|
21
31
|
}
|
|
22
32
|
else if (state !== "loading" && cleanupMap.has(el)) {
|
|
@@ -2,12 +2,16 @@ import { ObjectDirective } from "vue";
|
|
|
2
2
|
interface HTMLElementWithCleanup extends HTMLElement {
|
|
3
3
|
_skCleanup?: () => void;
|
|
4
4
|
}
|
|
5
|
+
import { SkeletonConfig } from "../../core/infer/types.js";
|
|
5
6
|
/**
|
|
6
7
|
* A Vue directive that attaches a skeleton when the value is true.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Supports:
|
|
9
|
+
* - v-skeleton="loading" (boolean)
|
|
10
|
+
* - v-skeleton="{ loading, config }" (object)
|
|
10
11
|
*/
|
|
11
|
-
export declare const vSkeleton: ObjectDirective<HTMLElementWithCleanup, boolean
|
|
12
|
+
export declare const vSkeleton: ObjectDirective<HTMLElementWithCleanup, boolean | {
|
|
13
|
+
loading: boolean;
|
|
14
|
+
config?: SkeletonConfig;
|
|
15
|
+
}>;
|
|
12
16
|
export {};
|
|
13
17
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/adapters/vue/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,KAAK,CAAC;AAItC,UAAU,sBAAuB,SAAQ,WAAW;IAChD,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B;AAED;;;;;GAKG;AACH,eAAO,MAAM,SAAS,EAAE,eAAe,CAAC,sBAAsB,EAAE,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/adapters/vue/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,KAAK,CAAC;AAItC,UAAU,sBAAuB,SAAQ,WAAW;IAChD,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B;AAED,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAE3D;;;;;GAKG;AACH,eAAO,MAAM,SAAS,EAAE,eAAe,CAAC,sBAAsB,EAAE,OAAO,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,cAAc,CAAA;CAAE,CAkCtH,CAAC"}
|
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
import { attachSkeleton } from "../../core/lifecycle/attachSkeleton.js";
|
|
2
2
|
/**
|
|
3
3
|
* A Vue directive that attaches a skeleton when the value is true.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - v-skeleton="loading" (boolean)
|
|
6
|
+
* - v-skeleton="{ loading, config }" (object)
|
|
7
7
|
*/
|
|
8
8
|
export const vSkeleton = {
|
|
9
9
|
mounted(el, binding) {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const value = binding.value;
|
|
11
|
+
const isLoading = typeof value === 'boolean' ? value : value === null || value === void 0 ? void 0 : value.loading;
|
|
12
|
+
const config = typeof value === 'object' ? value.config : undefined;
|
|
13
|
+
if (isLoading) {
|
|
14
|
+
el._skCleanup = attachSkeleton(el, config);
|
|
12
15
|
}
|
|
13
16
|
},
|
|
14
17
|
updated(el, binding) {
|
|
18
|
+
const value = binding.value;
|
|
19
|
+
const isLoading = typeof value === 'boolean' ? value : value === null || value === void 0 ? void 0 : value.loading;
|
|
20
|
+
const config = typeof value === 'object' ? value.config : undefined;
|
|
21
|
+
// Note: Changes to config while loading might require re-attaching, but simpler for now implies just on/off toggling.
|
|
22
|
+
// For full reactivity on config, we'd need to detach and re-attach if config changed.
|
|
15
23
|
// If value turned true and we don't have cleanup, attach
|
|
16
|
-
if (
|
|
17
|
-
el._skCleanup = attachSkeleton(el);
|
|
24
|
+
if (isLoading && !el._skCleanup) {
|
|
25
|
+
el._skCleanup = attachSkeleton(el, config);
|
|
18
26
|
} // If value turned false and we HAVE cleanup, detach
|
|
19
|
-
else if (!
|
|
27
|
+
else if (!isLoading && el._skCleanup) {
|
|
20
28
|
el._skCleanup();
|
|
21
29
|
el._skCleanup = undefined;
|
|
22
30
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inferNode.d.ts","sourceRoot":"","sources":["../../../src/core/infer/inferNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAiB,MAAM,YAAY,CAAC;AAIzD,wBAAgB,SAAS,CAAC,EAAE,EAAE,WAAW,GAAG,YAAY,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"inferNode.d.ts","sourceRoot":"","sources":["../../../src/core/infer/inferNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAiB,MAAM,YAAY,CAAC;AAIzD,wBAAgB,SAAS,CAAC,EAAE,EAAE,WAAW,GAAG,YAAY,GAAG,IAAI,CAsI9D"}
|
|
@@ -10,29 +10,73 @@ export function inferNode(el) {
|
|
|
10
10
|
const fontSize = parseFloat(probeData.styles.fontSize) || 16;
|
|
11
11
|
let lineHeight;
|
|
12
12
|
if (lineHeightStr === 'normal') {
|
|
13
|
-
// Browser default is typically 1.2
|
|
14
13
|
lineHeight = fontSize * 1.2;
|
|
15
14
|
}
|
|
16
15
|
else if (lineHeightStr.endsWith('px')) {
|
|
17
|
-
// Explicit pixel value like "24px"
|
|
18
16
|
lineHeight = parseFloat(lineHeightStr);
|
|
19
17
|
}
|
|
20
18
|
else {
|
|
21
|
-
// Unitless multiplier like "1.5"
|
|
22
19
|
lineHeight = parseFloat(lineHeightStr) * fontSize;
|
|
23
20
|
}
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
21
|
+
// Use Range API for precise line detection
|
|
22
|
+
const range = document.createRange();
|
|
23
|
+
range.selectNodeContents(el);
|
|
24
|
+
const rects = Array.from(range.getClientRects());
|
|
25
|
+
let textLines = [];
|
|
26
|
+
if (rects.length > 0) {
|
|
27
|
+
// Group rects by vertical position (Y) to identify lines
|
|
28
|
+
const linesMap = new Map();
|
|
29
|
+
const threshold = 4; // Pixel threshold to consider rects on the same line
|
|
30
|
+
rects.forEach(rect => {
|
|
31
|
+
if (rect.width === 0 || rect.height === 0)
|
|
32
|
+
return; // Ignore invisible rects
|
|
33
|
+
// Find an existing line group that matches this top
|
|
34
|
+
let foundKey = -1;
|
|
35
|
+
for (const key of linesMap.keys()) {
|
|
36
|
+
if (Math.abs(key - rect.top) < threshold) {
|
|
37
|
+
foundKey = key;
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (foundKey !== -1) {
|
|
42
|
+
const group = linesMap.get(foundKey);
|
|
43
|
+
group.minX = Math.min(group.minX, rect.left);
|
|
44
|
+
group.maxX = Math.max(group.maxX, rect.right);
|
|
45
|
+
group.bottom = Math.max(group.bottom, rect.bottom); // Track max bottom to calc height
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
linesMap.set(rect.top, {
|
|
49
|
+
minX: rect.left,
|
|
50
|
+
maxX: rect.right,
|
|
51
|
+
top: rect.top,
|
|
52
|
+
bottom: rect.bottom
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
// Convert groups to sorted lines
|
|
57
|
+
textLines = Array.from(linesMap.values())
|
|
58
|
+
.sort((a, b) => a.top - b.top)
|
|
59
|
+
.map(g => ({
|
|
60
|
+
width: g.maxX - g.minX,
|
|
61
|
+
height: g.bottom - g.top // Calculate exact height of the line
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
let lines = textLines.length;
|
|
65
|
+
// Fallback if Range API failed to detect specific lines or returned 0
|
|
66
|
+
if (lines === 0) {
|
|
67
|
+
if (lineHeightStr !== 'normal') {
|
|
68
|
+
lines = Math.max(1, Math.round(probeData.height / lineHeight));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
lines = 1;
|
|
72
|
+
}
|
|
30
73
|
}
|
|
31
74
|
return {
|
|
32
75
|
kind: "text",
|
|
33
76
|
width: probeData.width,
|
|
34
77
|
height: lineHeight, // Height of a single line
|
|
35
78
|
lines,
|
|
79
|
+
textLines,
|
|
36
80
|
radius: parseFloat(probeData.borderRadius) || 4, // Default radius for text
|
|
37
81
|
padding: probeData.styles.padding,
|
|
38
82
|
margin: probeData.styles.margin,
|
|
@@ -9,6 +9,10 @@ export interface SkeletonBase {
|
|
|
9
9
|
export interface SkeletonText extends SkeletonBase {
|
|
10
10
|
kind: "text";
|
|
11
11
|
lines: number;
|
|
12
|
+
textLines?: {
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
}[];
|
|
12
16
|
}
|
|
13
17
|
export interface SkeletonBlock extends SkeletonBase {
|
|
14
18
|
kind: "block";
|
|
@@ -24,5 +28,12 @@ export interface SkeletonGroup extends SkeletonBase {
|
|
|
24
28
|
gridTemplateColumns?: string;
|
|
25
29
|
gridTemplateRows?: string;
|
|
26
30
|
}
|
|
31
|
+
export type AnimationType = 'pulse' | 'wave' | 'glimmer' | 'scan' | 'breathing' | 'fade-in' | 'slide-in' | 'rumble' | 'neon' | 'classic' | 'shimmer-vertical' | 'bounce';
|
|
32
|
+
export interface SkeletonConfig {
|
|
33
|
+
animation?: AnimationType;
|
|
34
|
+
color?: string;
|
|
35
|
+
highlightColor?: string;
|
|
36
|
+
borderRadius?: number;
|
|
37
|
+
}
|
|
27
38
|
export type SkeletonNode = SkeletonBlock | SkeletonText | SkeletonGroup;
|
|
28
39
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/infer/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAa,SAAQ,YAAY;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/infer/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAa,SAAQ,YAAY;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACnD;AAED,MAAM,WAAW,aAAc,SAAQ,YAAY;IAC/C,IAAI,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,aAAc,SAAQ,YAAY;IAC/C,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,KAAK,GAAG,QAAQ,CAAC;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,YAAY,EAAE,CAAC;IAEzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,MAAM,aAAa,GACnB,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,WAAW,GACnD,SAAS,GAAG,UAAU,GAAG,QAAQ,GAAG,MAAM,GAC1C,SAAS,GAAG,kBAAkB,GAAG,QAAQ,CAAC;AAEhD,MAAM,WAAW,cAAc;IAC3B,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,YAAY,GAAG,aAAa,GAAG,YAAY,GAAG,aAAa,CAAC"}
|
|
@@ -15,7 +15,7 @@ export class SkeletonOverlayManager {
|
|
|
15
15
|
}
|
|
16
16
|
const parent = target.parentElement;
|
|
17
17
|
if (!parent) {
|
|
18
|
-
console.warn("
|
|
18
|
+
console.warn("SkullDOM: Target element has no parent, cannot attach overlay.");
|
|
19
19
|
// Return a dummy record so the caller doesn't crash, but nothing happened.
|
|
20
20
|
return { skeletonEl, restoreVisibility: "" };
|
|
21
21
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { SkeletonConfig } from '../infer/types.js';
|
|
1
2
|
/**
|
|
2
3
|
* Attaches a skeleton overlay to the target element.
|
|
3
4
|
* Hides the target element (visibility: hidden) and places the skeleton on top.
|
|
4
5
|
* Returns a cleanup function to remove the skeleton and show the target element.
|
|
5
6
|
*/
|
|
6
|
-
export declare function attachSkeleton(target: HTMLElement): () => void;
|
|
7
|
+
export declare function attachSkeleton(target: HTMLElement, config?: SkeletonConfig): () => void;
|
|
7
8
|
//# sourceMappingURL=attachSkeleton.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachSkeleton.d.ts","sourceRoot":"","sources":["../../../src/core/lifecycle/attachSkeleton.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"attachSkeleton.d.ts","sourceRoot":"","sources":["../../../src/core/lifecycle/attachSkeleton.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGnD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,CAAC,EAAE,cAAc,GAAG,MAAM,IAAI,CA2BvF"}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { inferNode } from '../infer/inferNode.js';
|
|
2
2
|
import { renderSkeleton } from '../render/renderSkeleton.js';
|
|
3
3
|
import { skeletonOverlayManager } from './SkeletonOverlayManager.js';
|
|
4
|
+
import { injectStyles } from '../styles/styles.js';
|
|
4
5
|
/**
|
|
5
6
|
* Attaches a skeleton overlay to the target element.
|
|
6
7
|
* Hides the target element (visibility: hidden) and places the skeleton on top.
|
|
7
8
|
* Returns a cleanup function to remove the skeleton and show the target element.
|
|
8
9
|
*/
|
|
9
|
-
export function attachSkeleton(target) {
|
|
10
|
+
export function attachSkeleton(target, config) {
|
|
11
|
+
// 0. Ensure styles are injected
|
|
12
|
+
injectStyles();
|
|
10
13
|
// 1. Check if already controlled
|
|
11
14
|
if (skeletonOverlayManager.has(target)) {
|
|
12
15
|
// If we want to allow re-attaching or updating, we'd handle it here.
|
|
@@ -16,10 +19,10 @@ export function attachSkeleton(target) {
|
|
|
16
19
|
// 2. Infer & Render
|
|
17
20
|
const tree = inferNode(target);
|
|
18
21
|
if (!tree) {
|
|
19
|
-
console.warn("
|
|
22
|
+
console.warn("SkullDOM: Could not infer structure for element", target);
|
|
20
23
|
return () => { };
|
|
21
24
|
}
|
|
22
|
-
const skeletonEl = renderSkeleton(tree);
|
|
25
|
+
const skeletonEl = renderSkeleton(tree, config);
|
|
23
26
|
// 3. Delegate to Manager
|
|
24
27
|
skeletonOverlayManager.attach(target, skeletonEl);
|
|
25
28
|
// 4. Return detach closure
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { SkeletonNode } from "../infer/types.js";
|
|
2
|
-
export declare function renderSkeleton(node: SkeletonNode): HTMLElement;
|
|
1
|
+
import { SkeletonNode, SkeletonConfig } from "../infer/types.js";
|
|
2
|
+
export declare function renderSkeleton(node: SkeletonNode, config?: SkeletonConfig): HTMLElement;
|
|
3
3
|
//# sourceMappingURL=renderSkeleton.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderSkeleton.d.ts","sourceRoot":"","sources":["../../../src/core/render/renderSkeleton.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"renderSkeleton.d.ts","sourceRoot":"","sources":["../../../src/core/render/renderSkeleton.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEjE,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,cAAc,GAAG,WAAW,CAqIvF"}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
export function renderSkeleton(node) {
|
|
1
|
+
export function renderSkeleton(node, config) {
|
|
2
2
|
try {
|
|
3
3
|
const el = document.createElement("div");
|
|
4
4
|
el.style.boxSizing = "border-box"; // Critical for sizing
|
|
5
5
|
// Default styles
|
|
6
|
-
el.style.backgroundColor = "#d1d5db"; //
|
|
6
|
+
el.style.backgroundColor = (config === null || config === void 0 ? void 0 : config.color) || "#d1d5db"; // Configurable base color
|
|
7
7
|
el.style.overflow = "hidden"; // Clip children if needed
|
|
8
|
+
// CSS Variables for animations
|
|
9
|
+
if (config === null || config === void 0 ? void 0 : config.color)
|
|
10
|
+
el.style.setProperty('--sk-color', config.color);
|
|
11
|
+
if (config === null || config === void 0 ? void 0 : config.highlightColor)
|
|
12
|
+
el.style.setProperty('--sk-highlight', config.highlightColor);
|
|
8
13
|
// Apply common layout styles
|
|
9
14
|
if (node.padding)
|
|
10
15
|
el.style.padding = node.padding;
|
|
@@ -12,17 +17,28 @@ export function renderSkeleton(node) {
|
|
|
12
17
|
el.style.margin = node.margin;
|
|
13
18
|
if (node.border)
|
|
14
19
|
el.style.border = node.border;
|
|
20
|
+
// Apply Animation Class
|
|
21
|
+
const anim = (config === null || config === void 0 ? void 0 : config.animation) || 'wave';
|
|
22
|
+
// For container-based animations
|
|
23
|
+
if (['fade-in', 'slide-in', 'breathing', 'bounce', 'neon'].includes(anim)) {
|
|
24
|
+
el.classList.add(`sk-anim-${anim}`);
|
|
25
|
+
}
|
|
15
26
|
if (node.kind === "block") {
|
|
16
27
|
el.style.width = `${node.width}px`;
|
|
17
28
|
el.style.height = `${node.height}px`;
|
|
18
|
-
el.style.borderRadius = `${node.radius || 0}px`;
|
|
29
|
+
el.style.borderRadius = `${typeof (config === null || config === void 0 ? void 0 : config.borderRadius) === 'number' ? config.borderRadius : (node.radius || 0)}px`;
|
|
19
30
|
el.setAttribute("data-sk", "block");
|
|
31
|
+
// Leaf node animations
|
|
32
|
+
if (!['fade-in', 'slide-in', 'breathing', 'bounce'].includes(anim)) {
|
|
33
|
+
el.classList.add(`sk-anim-${anim}`);
|
|
34
|
+
}
|
|
20
35
|
return el;
|
|
21
36
|
}
|
|
22
37
|
if (node.kind === "text") {
|
|
23
38
|
el.style.width = `${node.width}px`;
|
|
24
39
|
el.style.display = "flex";
|
|
25
40
|
el.style.flexDirection = "column";
|
|
41
|
+
el.style.alignItems = "flex-start"; // Ensure lines don't stretch to container width
|
|
26
42
|
// Add small gap for visual separation between skeleton lines
|
|
27
43
|
const visualGap = 4;
|
|
28
44
|
el.style.gap = `${visualGap}px`;
|
|
@@ -32,16 +48,38 @@ export function renderSkeleton(node) {
|
|
|
32
48
|
const totalGapHeight = visualGap * (node.lines - 1);
|
|
33
49
|
const adjustedLineHeight = node.lines > 1
|
|
34
50
|
? (node.height * node.lines - totalGapHeight) / node.lines
|
|
35
|
-
: node.height;
|
|
36
|
-
//
|
|
37
|
-
const validLineHeight =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
51
|
+
: node.height; // Note: node.height here comes from inferNode which is set to lineHeight
|
|
52
|
+
// Use the inferred line height, or fallback if something went wrong
|
|
53
|
+
const validLineHeight = node.height;
|
|
54
|
+
// Use exact text lines if available
|
|
55
|
+
if (node.textLines && node.textLines.length > 0) {
|
|
56
|
+
node.textLines.forEach((lineData, i) => {
|
|
57
|
+
const line = document.createElement("div");
|
|
58
|
+
line.style.width = `${lineData.width}px`;
|
|
59
|
+
line.style.height = `${lineData.height}px`; // Use exact captured height
|
|
60
|
+
line.style.backgroundColor = (config === null || config === void 0 ? void 0 : config.color) || "#9ca3af";
|
|
61
|
+
line.style.borderRadius = `${typeof (config === null || config === void 0 ? void 0 : config.borderRadius) === 'number' ? config.borderRadius : (node.radius || 4)}px`;
|
|
62
|
+
line.style.flexShrink = "0"; // Prevent shrinking
|
|
63
|
+
// Leaf node animations
|
|
64
|
+
if (!['fade-in', 'slide-in', 'breathing', 'bounce'].includes(anim)) {
|
|
65
|
+
line.classList.add(`sk-anim-${anim}`);
|
|
66
|
+
}
|
|
67
|
+
el.appendChild(line);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// Fallback for when textLines extraction failed
|
|
72
|
+
for (let i = 0; i < node.lines; i++) {
|
|
73
|
+
const line = document.createElement("div");
|
|
74
|
+
line.style.width = i === node.lines - 1 && node.lines > 1 ? "60%" : "100%"; // Last line short
|
|
75
|
+
line.style.height = `${validLineHeight}px`;
|
|
76
|
+
line.style.backgroundColor = (config === null || config === void 0 ? void 0 : config.color) || "#9ca3af"; // Darker line color
|
|
77
|
+
line.style.borderRadius = `${typeof (config === null || config === void 0 ? void 0 : config.borderRadius) === 'number' ? config.borderRadius : (node.radius || 4)}px`;
|
|
78
|
+
if (!['fade-in', 'slide-in', 'breathing', 'bounce'].includes(anim)) {
|
|
79
|
+
line.classList.add(`sk-anim-${anim}`);
|
|
80
|
+
}
|
|
81
|
+
el.appendChild(line);
|
|
82
|
+
}
|
|
45
83
|
}
|
|
46
84
|
return el;
|
|
47
85
|
}
|
|
@@ -69,7 +107,7 @@ export function renderSkeleton(node) {
|
|
|
69
107
|
}
|
|
70
108
|
el.setAttribute("data-sk", "group");
|
|
71
109
|
node.children.forEach(child => {
|
|
72
|
-
el.appendChild(renderSkeleton(child));
|
|
110
|
+
el.appendChild(renderSkeleton(child, config));
|
|
73
111
|
});
|
|
74
112
|
return el;
|
|
75
113
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const CSS_ANIMATIONS = "\n /* Keyframes */\n @keyframes sk-pulse {\n 0% { opacity: 0.6; }\n 50% { opacity: 1; }\n 100% { opacity: 0.6; }\n }\n @keyframes sk-wave {\n 0% { background-position: -200% 0; }\n 100% { background-position: 200% 0; }\n }\n @keyframes sk-glimmer {\n 0% { mask-position: -200%; -webkit-mask-position: -200%; }\n 100% { mask-position: 200%; -webkit-mask-position: 200%; }\n }\n @keyframes sk-scan {\n 0% { transform: translateY(-100%); }\n 100% { transform: translateY(100%); }\n }\n @keyframes sk-breathing {\n 0% { transform: scale(0.99); opacity: 0.8; }\n 50% { transform: scale(1.0); opacity: 1; }\n 100% { transform: scale(0.99); opacity: 0.8; }\n }\n @keyframes sk-fade-in {\n 0% { opacity: 0; }\n 100% { opacity: 1; }\n }\n @keyframes sk-slide-in {\n 0% { transform: translateY(10px); opacity: 0; }\n 100% { transform: translateY(0); opacity: 1; }\n }\n @keyframes sk-rumble {\n 0% { transform: translateX(-1px); }\n 25% { transform: translateX(1px); }\n 50% { transform: translateX(-1px); }\n 75% { transform: translateX(1px); }\n 100% { transform: translateX(-1px); }\n }\n @keyframes sk-neon {\n 0% { box-shadow: 0 0 5px #e5e7eb; }\n 50% { box-shadow: 0 0 15px #9ca3af; }\n 100% { box-shadow: 0 0 5px #e5e7eb; }\n }\n @keyframes sk-shimmer-vertical {\n 0% { background-position: 0 -200%; }\n 100% { background-position: 0 200%; }\n }\n @keyframes sk-bounce {\n 0%, 100% { transform: translateY(0); }\n 50% { transform: translateY(-2px); }\n }\n\n /* Classes */\n .sk-anim-pulse { animation: sk-pulse 1.5s ease-in-out infinite; }\n \n .sk-anim-wave { \n background: linear-gradient(90deg, var(--sk-color, #d1d5db) 25%, var(--sk-highlight, #e5e7eb) 50%, var(--sk-color, #d1d5db) 75%);\n background-size: 200% 100%;\n animation: sk-wave 2s infinite linear; \n }\n\n .sk-anim-glimmer {\n position: relative;\n overflow: hidden;\n }\n .sk-anim-glimmer::after {\n content: '';\n position: absolute;\n top: 0; left: 0; width: 100%; height: 100%;\n background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);\n transform: skewX(-20deg);\n animation: sk-wave 1.5s infinite;\n }\n\n /* Scan uses a pseudo-element overlay */\n .sk-anim-scan {\n position: relative;\n overflow: hidden;\n }\n .sk-anim-scan::after {\n content: '';\n position: absolute;\n top: 0; left: 0; width: 100%; height: 2px;\n background: #fff;\n box-shadow: 0 0 10px #fff;\n animation: sk-scan 2s linear infinite;\n }\n\n .sk-anim-breathing { animation: sk-breathing 3s ease-in-out infinite; }\n .sk-anim-fade-in { animation: sk-fade-in 0.5s ease-out forwards; }\n .sk-anim-slide-in { animation: sk-slide-in 0.4s ease-out forwards; }\n .sk-anim-rumble { animation: sk-rumble 0.2s linear infinite; }\n .sk-anim-neon { animation: sk-neon 2s ease-in-out infinite; }\n \n .sk-anim-shimmer-vertical {\n background: linear-gradient(180deg, var(--sk-color, #d1d5db) 25%, var(--sk-highlight, #e5e7eb) 50%, var(--sk-color, #d1d5db) 75%);\n background-size: 100% 200%;\n animation: sk-shimmer-vertical 2s infinite linear;\n }\n \n .sk-anim-bounce { animation: sk-bounce 2s ease-in-out infinite; }\n .sk-anim-classic { /* No animation */ }\n";
|
|
2
|
+
export declare function injectStyles(): void;
|
|
3
|
+
//# sourceMappingURL=styles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../../src/core/styles/styles.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,cAAc,gwGAuG1B,CAAC;AAEF,wBAAgB,YAAY,SAQ3B"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export const CSS_ANIMATIONS = `
|
|
2
|
+
/* Keyframes */
|
|
3
|
+
@keyframes sk-pulse {
|
|
4
|
+
0% { opacity: 0.6; }
|
|
5
|
+
50% { opacity: 1; }
|
|
6
|
+
100% { opacity: 0.6; }
|
|
7
|
+
}
|
|
8
|
+
@keyframes sk-wave {
|
|
9
|
+
0% { background-position: -200% 0; }
|
|
10
|
+
100% { background-position: 200% 0; }
|
|
11
|
+
}
|
|
12
|
+
@keyframes sk-glimmer {
|
|
13
|
+
0% { mask-position: -200%; -webkit-mask-position: -200%; }
|
|
14
|
+
100% { mask-position: 200%; -webkit-mask-position: 200%; }
|
|
15
|
+
}
|
|
16
|
+
@keyframes sk-scan {
|
|
17
|
+
0% { transform: translateY(-100%); }
|
|
18
|
+
100% { transform: translateY(100%); }
|
|
19
|
+
}
|
|
20
|
+
@keyframes sk-breathing {
|
|
21
|
+
0% { transform: scale(0.99); opacity: 0.8; }
|
|
22
|
+
50% { transform: scale(1.0); opacity: 1; }
|
|
23
|
+
100% { transform: scale(0.99); opacity: 0.8; }
|
|
24
|
+
}
|
|
25
|
+
@keyframes sk-fade-in {
|
|
26
|
+
0% { opacity: 0; }
|
|
27
|
+
100% { opacity: 1; }
|
|
28
|
+
}
|
|
29
|
+
@keyframes sk-slide-in {
|
|
30
|
+
0% { transform: translateY(10px); opacity: 0; }
|
|
31
|
+
100% { transform: translateY(0); opacity: 1; }
|
|
32
|
+
}
|
|
33
|
+
@keyframes sk-rumble {
|
|
34
|
+
0% { transform: translateX(-1px); }
|
|
35
|
+
25% { transform: translateX(1px); }
|
|
36
|
+
50% { transform: translateX(-1px); }
|
|
37
|
+
75% { transform: translateX(1px); }
|
|
38
|
+
100% { transform: translateX(-1px); }
|
|
39
|
+
}
|
|
40
|
+
@keyframes sk-neon {
|
|
41
|
+
0% { box-shadow: 0 0 5px #e5e7eb; }
|
|
42
|
+
50% { box-shadow: 0 0 15px #9ca3af; }
|
|
43
|
+
100% { box-shadow: 0 0 5px #e5e7eb; }
|
|
44
|
+
}
|
|
45
|
+
@keyframes sk-shimmer-vertical {
|
|
46
|
+
0% { background-position: 0 -200%; }
|
|
47
|
+
100% { background-position: 0 200%; }
|
|
48
|
+
}
|
|
49
|
+
@keyframes sk-bounce {
|
|
50
|
+
0%, 100% { transform: translateY(0); }
|
|
51
|
+
50% { transform: translateY(-2px); }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Classes */
|
|
55
|
+
.sk-anim-pulse { animation: sk-pulse 1.5s ease-in-out infinite; }
|
|
56
|
+
|
|
57
|
+
.sk-anim-wave {
|
|
58
|
+
background: linear-gradient(90deg, var(--sk-color, #d1d5db) 25%, var(--sk-highlight, #e5e7eb) 50%, var(--sk-color, #d1d5db) 75%);
|
|
59
|
+
background-size: 200% 100%;
|
|
60
|
+
animation: sk-wave 2s infinite linear;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.sk-anim-glimmer {
|
|
64
|
+
position: relative;
|
|
65
|
+
overflow: hidden;
|
|
66
|
+
}
|
|
67
|
+
.sk-anim-glimmer::after {
|
|
68
|
+
content: '';
|
|
69
|
+
position: absolute;
|
|
70
|
+
top: 0; left: 0; width: 100%; height: 100%;
|
|
71
|
+
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
|
|
72
|
+
transform: skewX(-20deg);
|
|
73
|
+
animation: sk-wave 1.5s infinite;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Scan uses a pseudo-element overlay */
|
|
77
|
+
.sk-anim-scan {
|
|
78
|
+
position: relative;
|
|
79
|
+
overflow: hidden;
|
|
80
|
+
}
|
|
81
|
+
.sk-anim-scan::after {
|
|
82
|
+
content: '';
|
|
83
|
+
position: absolute;
|
|
84
|
+
top: 0; left: 0; width: 100%; height: 2px;
|
|
85
|
+
background: #fff;
|
|
86
|
+
box-shadow: 0 0 10px #fff;
|
|
87
|
+
animation: sk-scan 2s linear infinite;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.sk-anim-breathing { animation: sk-breathing 3s ease-in-out infinite; }
|
|
91
|
+
.sk-anim-fade-in { animation: sk-fade-in 0.5s ease-out forwards; }
|
|
92
|
+
.sk-anim-slide-in { animation: sk-slide-in 0.4s ease-out forwards; }
|
|
93
|
+
.sk-anim-rumble { animation: sk-rumble 0.2s linear infinite; }
|
|
94
|
+
.sk-anim-neon { animation: sk-neon 2s ease-in-out infinite; }
|
|
95
|
+
|
|
96
|
+
.sk-anim-shimmer-vertical {
|
|
97
|
+
background: linear-gradient(180deg, var(--sk-color, #d1d5db) 25%, var(--sk-highlight, #e5e7eb) 50%, var(--sk-color, #d1d5db) 75%);
|
|
98
|
+
background-size: 100% 200%;
|
|
99
|
+
animation: sk-shimmer-vertical 2s infinite linear;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.sk-anim-bounce { animation: sk-bounce 2s ease-in-out infinite; }
|
|
103
|
+
.sk-anim-classic { /* No animation */ }
|
|
104
|
+
`;
|
|
105
|
+
export function injectStyles() {
|
|
106
|
+
if (typeof document === 'undefined')
|
|
107
|
+
return;
|
|
108
|
+
if (document.getElementById('skull-dom-styles'))
|
|
109
|
+
return;
|
|
110
|
+
const style = document.createElement('style');
|
|
111
|
+
style.id = 'skull-dom-styles';
|
|
112
|
+
style.textContent = CSS_ANIMATIONS;
|
|
113
|
+
document.head.appendChild(style);
|
|
114
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,5 +2,5 @@ export { attachSkeleton } from "./core/lifecycle/attachSkeleton.js";
|
|
|
2
2
|
export { skeletonOverlayManager } from "./core/lifecycle/SkeletonOverlayManager.js";
|
|
3
3
|
export { inferNode } from "./core/infer/inferNode.js";
|
|
4
4
|
export { renderSkeleton } from "./core/render/renderSkeleton.js";
|
|
5
|
-
export type { SkeletonNode } from "./core/infer/types.js";
|
|
5
|
+
export type { SkeletonNode, SkeletonConfig } from "./core/infer/types.js";
|
|
6
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACpE,OAAO,EAAE,sBAAsB,EAAE,MAAM,4CAA4C,CAAC;AAGpF,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAGjE,YAAY,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACpE,OAAO,EAAE,sBAAsB,EAAE,MAAM,4CAA4C,CAAC;AAGpF,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAGjE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skull-dom",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"files": [
|
|
@@ -51,8 +51,11 @@
|
|
|
51
51
|
"license": "MIT",
|
|
52
52
|
"description": "Automagic skeleton screens for any framework",
|
|
53
53
|
"devDependencies": {
|
|
54
|
+
"@testing-library/dom": "^10.4.1",
|
|
55
|
+
"@testing-library/react": "^16.3.1",
|
|
54
56
|
"@types/node": "^25.0.3",
|
|
55
57
|
"@types/react": "^19.2.7",
|
|
58
|
+
"happy-dom": "^20.0.11",
|
|
56
59
|
"jsdom": "^27.4.0",
|
|
57
60
|
"react": "^19.2.3",
|
|
58
61
|
"solid-js": "^1.9.10",
|