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.
Files changed (33) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +178 -165
  3. package/dist/adapters/react/index.d.ts +11 -1
  4. package/dist/adapters/react/index.d.ts.map +1 -1
  5. package/dist/adapters/react/index.js +4 -4
  6. package/dist/adapters/solid/index.d.ts +9 -2
  7. package/dist/adapters/solid/index.d.ts.map +1 -1
  8. package/dist/adapters/solid/index.js +5 -2
  9. package/dist/adapters/svelte/index.d.ts +8 -1
  10. package/dist/adapters/svelte/index.d.ts.map +1 -1
  11. package/dist/adapters/svelte/index.js +13 -7
  12. package/dist/adapters/vanilla/index.d.ts.map +1 -1
  13. package/dist/adapters/vanilla/index.js +11 -1
  14. package/dist/adapters/vue/index.d.ts +8 -4
  15. package/dist/adapters/vue/index.d.ts.map +1 -1
  16. package/dist/adapters/vue/index.js +16 -8
  17. package/dist/core/infer/inferNode.d.ts.map +1 -1
  18. package/dist/core/infer/inferNode.js +53 -9
  19. package/dist/core/infer/types.d.ts +11 -0
  20. package/dist/core/infer/types.d.ts.map +1 -1
  21. package/dist/core/lifecycle/SkeletonOverlayManager.js +1 -1
  22. package/dist/core/lifecycle/attachSkeleton.d.ts +2 -1
  23. package/dist/core/lifecycle/attachSkeleton.d.ts.map +1 -1
  24. package/dist/core/lifecycle/attachSkeleton.js +6 -3
  25. package/dist/core/render/renderSkeleton.d.ts +2 -2
  26. package/dist/core/render/renderSkeleton.d.ts.map +1 -1
  27. package/dist/core/render/renderSkeleton.js +52 -14
  28. package/dist/core/styles/styles.d.ts +3 -0
  29. package/dist/core/styles/styles.d.ts.map +1 -0
  30. package/dist/core/styles/styles.js +114 -0
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.d.ts.map +1 -1
  33. 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
- > **Automagic skeleton screens for any framework.**
3
- > Zero config. Zero manual styles. Just robust, layout-aware loading states.
4
-
5
- ## Why SkullDOM?
6
-
7
- Most skeleton libraries require you to manually build "skeleton versions" of your components. You end up maintaining two UIs: the real one and the fake one.
8
-
9
- **SkullDOM is different.**
10
- It looks at your **existing DOM**, infers its layout (text, images, grids, flex), and overlays a pixel-perfect skeleton on top of it.
11
-
12
- - **Framework Agnostic:** First-class adapters for React, Vue, Svelte, Solid, and Vanilla.
13
- - **Zero Layout Shift:** It overlays perfectly on your actual elements.
14
- - **Automagic Inference:** Detects text lines, border radii, and flex gaps automatically.
15
- - **Memory Safe:** Uses `WeakMap` to ensure skeletons are garbage collected automatically.
16
-
17
- ---
18
-
19
- ## 📦 Installation
20
-
21
- ```bash
22
- npm install skull-dom
23
- ```
24
-
25
- ---
26
-
27
- ## 🚀 Quick Start
28
-
29
- ### 1. Vanilla / HTML Variables
30
- Add attributes to your HTML. No JS required.
31
-
32
- ```html
33
- <!-- 1. Import the utility once in your app entry -->
34
- <script>
35
- import { initSkeletonObserver } from 'skull-dom/vanilla';
36
- initSkeletonObserver();
37
- </script>
38
-
39
- <!-- 2. Toggle the attribute when loading -->
40
- <div data-skeleton="loading">
41
- <h1>My Content</h1>
42
- <p>SkullDOM will infer this text structure and cover it.</p>
43
- </div>
44
- ```
45
-
46
- ### 2. React
47
- Use the `useSkeleton` hook.
48
-
49
- ```tsx
50
- import { useRef } from 'react';
51
- import { useSkeleton } from 'skull-dom/react';
52
-
53
- function UserCard({ isLoading, user }) {
54
- const ref = useRef(null);
55
-
56
- // Automagically handles attach/detach lifecycle
57
- useSkeleton(ref, isLoading);
58
-
59
- return (
60
- <div ref={ref} className="card">
61
- <img src={user.avatar} className="avatar" />
62
- <h3>{user.name}</h3>
63
- </div>
64
- );
65
- }
66
- ```
67
-
68
- ### 3. Vue
69
- Use the `v-skeleton` directive.
70
-
71
- ```vue
72
- <script setup>
73
- import { vSkeleton } from 'skull-dom/vue';
74
- defineProps(['loading']);
75
- </script>
76
-
77
- <template>
78
- <div v-skeleton="loading" class="card">
79
- <!-- content -->
80
- </div>
81
- </template>
82
- ```
83
-
84
- ### 4. Svelte
85
- Use the `skeleton` action.
86
-
87
- ```svelte
88
- <script>
89
- import { skeleton } from 'skull-dom/adapters/svelte';
90
- export let loading = true;
91
- </script>
92
-
93
- <div use:skeleton={loading}>
94
- <!-- content -->
95
- </div>
96
- ```
97
-
98
- ### 5. SolidJS
99
- Use the `skeleton` directive.
100
-
101
- ```jsx
102
- import { skeleton } from 'skull-dom/solid';
103
-
104
- <div use:skeleton={isLoading()}>
105
- {/* content */}
106
- </div>
107
- ```
108
-
109
- ---
110
-
111
- ## 🧠 How it Works
112
-
113
- SkullDOM operates on a simple "Overlay" mental model:
114
-
115
- 1. **Probe:** It scans your target DOM element and measures computed styles (width, height, margin, padding, line-height, etc.).
116
- 2. **Infer:** It creates a lightweight tree representation of what matters (Text lines? Block? Flex group?).
117
- 3. **Render:** It builds a mirrored DOM structure with gray backgrounds and shimmer animations.
118
- 4. **Overlay:** It sets your original element to `visibility: hidden` and absolutely positions the skeleton layer directly on top of it.
119
-
120
- ### Important: You need content!
121
- Because SkullDOM **infers** from your DOM, your elements need to exist in the DOM (even if empty).
122
- * ✅ **Good:** Render your component with empty strings or optional chaining.
123
- * ❌ **Bad:** `if (loading) return null;` (There is nothing to infer!)
124
-
125
- **Correct Pattern:**
126
- ```jsx
127
- // React Example
128
- function Card({ loading, data }) {
129
- const ref = useRef(null);
130
- useSkeleton(ref, loading);
131
-
132
- return (
133
- <div ref={ref}>
134
- {/* Render structure even if data is missing! */}
135
- <h1>{data?.title || 'Placeholder Title'}</h1>
136
- </div>
137
- )
138
- }
139
- ```
140
-
141
- ---
142
-
143
- ## 🧩 API Reference
144
-
145
- ### Core (Advanced)
146
- If you are building your own adapter, import from the root.
147
-
148
- ```ts
149
- import { attachSkeleton } from 'skull-dom';
150
-
151
- // Returns a cleanup function
152
- const cleanup = attachSkeleton(myElement);
153
-
154
- // Later...
155
- cleanup();
156
- ```
157
-
158
- ### Styling
159
- Currently, SkullDOM injects a default gray theme (`#d1d5db` base, `#9ca3af` text lines) with a modest border radius.
160
- Future versions will expose CSS variables for theming.
161
-
162
- ---
163
-
164
- ## 📄 License
165
- MIT
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
- export declare function useSkeleton(ref: React.RefObject<HTMLElement | null>, loading: boolean): void;
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,QAkBnB"}
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 loading state or ref changes
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: boolean;
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: () => boolean): void;
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,OAAO,CAAC;SACrB;KACJ;CACJ;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,EAAE,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,QAShE"}
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 loading = accessor();
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, boolean>;
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;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,WAAW,EAAE,OAAO,CAwBjD,CAAC"}
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, loading) => {
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 (loading) {
12
- cleanup = attachSkeleton(node);
14
+ if (isLoading(params)) {
15
+ cleanup = attachSkeleton(node, getConfig(params));
13
16
  }
14
17
  return {
15
- update(newLoading) {
16
- if (newLoading && !cleanup) {
17
- cleanup = attachSkeleton(node);
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 (!newLoading && cleanup) {
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,qBAsCnC"}
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
- const cleanup = attachSkeleton(el);
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
- * Usage:
9
- * <div v-skeleton="loading"></div>
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,CAwBtE,CAAC"}
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
- * Usage:
6
- * <div v-skeleton="loading"></div>
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
- if (binding.value) {
11
- el._skCleanup = attachSkeleton(el);
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 (binding.value && !el._skCleanup) {
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 (!binding.value && el._skCleanup) {
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,CAqF9D"}
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
- // "Single element should not be represented as multiple lines except it has a line height css property"
25
- // If line-height is 'normal', force lines = 1.
26
- // Otherwise calculate based on height.
27
- let lines = 1;
28
- if (lineHeightStr !== 'normal') {
29
- lines = Math.max(1, Math.round(probeData.height / lineHeight));
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;CACjB;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,YAAY,GAAG,aAAa,GAAG,YAAY,GAAG,aAAa,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("AutoSkeleton: Target element has no parent, cannot attach overlay.");
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":"AAIA;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,IAAI,CAwB9D"}
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("AutoSkeleton: Could not infer structure for element", target);
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;AAEjD,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,WAAW,CA2F9D"}
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"; // Darker gray for better visibility
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
- // Ensure valid line height
37
- const validLineHeight = isNaN(adjustedLineHeight) ? node.height : adjustedLineHeight;
38
- for (let i = 0; i < node.lines; i++) {
39
- const line = document.createElement("div");
40
- line.style.width = i === node.lines - 1 && node.lines > 1 ? "60%" : "100%"; // Last line short
41
- line.style.height = `${validLineHeight}px`;
42
- line.style.backgroundColor = "#9ca3af"; // Darker line color
43
- line.style.borderRadius = `${node.radius || 4}px`;
44
- el.appendChild(line);
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
@@ -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.1",
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",