auto-loading-skeleton 1.0.2 → 2.0.0
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 +242 -242
- package/package.json +48 -48
- package/src/AutoSkeleton.js +117 -0
- package/src/analyzer.js +302 -79
- package/src/cache.js +47 -0
- package/src/context.js +38 -0
- package/src/hooks.js +161 -0
- package/src/index.d.ts +149 -33
- package/src/index.js +52 -102
- package/src/primitives.js +293 -0
- package/src/renderer.js +231 -56
- package/src/styles.js +162 -48
- package/src/AutoSkeleton.jsx +0 -72
- package/src/SkeletonItem.jsx +0 -99
package/src/styles.js
CHANGED
|
@@ -1,76 +1,190 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
.
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* styles.js — V2
|
|
5
|
+
*
|
|
6
|
+
* Improvements over V1:
|
|
7
|
+
* - Full dark-mode awareness via prefers-color-scheme
|
|
8
|
+
* - prefers-reduced-motion: replaces shimmer/wave with fade-only
|
|
9
|
+
* - Named theme presets (default, dark, minimal, brand, soft)
|
|
10
|
+
* - Per-instance CSS variable injection via data-ask-id attribute
|
|
11
|
+
* - All skeleton CSS vars scoped under [data-ask] to avoid global leakage
|
|
12
|
+
* - SSR-safe (guards typeof document)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
var STYLE_ID = 'ask-v2-styles';
|
|
16
|
+
|
|
17
|
+
var BASE_CSS = `
|
|
18
|
+
/* ── auto-skeleton-v2 base ── */
|
|
19
|
+
[data-ask] {
|
|
20
|
+
--ask-base: #e2e8f0;
|
|
21
|
+
--ask-highlight: rgba(255,255,255,0.65);
|
|
22
|
+
--ask-dur: 1.4s;
|
|
23
|
+
--ask-radius: 4px;
|
|
24
|
+
--ask-radius-lg: 8px;
|
|
25
|
+
--ask-radius-xl: 9999px;
|
|
26
|
+
}
|
|
27
|
+
@media (prefers-color-scheme: dark) {
|
|
28
|
+
[data-ask] {
|
|
29
|
+
--ask-base: #2d3748;
|
|
30
|
+
--ask-highlight: rgba(255,255,255,0.06);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* block primitive */
|
|
35
|
+
.ask-b {
|
|
36
|
+
display: block;
|
|
6
37
|
position: relative;
|
|
7
38
|
overflow: hidden;
|
|
8
|
-
background
|
|
9
|
-
border-radius: var(--ask-
|
|
10
|
-
|
|
39
|
+
background: var(--ask-base);
|
|
40
|
+
border-radius: var(--ask-radius);
|
|
41
|
+
flex-shrink: 0;
|
|
11
42
|
}
|
|
12
|
-
.ask-
|
|
43
|
+
.ask-b.ask-circle { border-radius: 50%; }
|
|
44
|
+
.ask-b.ask-pill { border-radius: var(--ask-radius-xl); }
|
|
45
|
+
.ask-b.ask-rounded { border-radius: var(--ask-radius-lg); }
|
|
46
|
+
|
|
47
|
+
/* animations */
|
|
48
|
+
.ask-shimmer.ask-b::after {
|
|
13
49
|
content: '';
|
|
14
50
|
position: absolute;
|
|
15
51
|
inset: 0;
|
|
16
52
|
transform: translateX(-100%);
|
|
17
|
-
background: linear-gradient(
|
|
18
|
-
|
|
53
|
+
background: linear-gradient(
|
|
54
|
+
90deg,
|
|
55
|
+
transparent 0%,
|
|
56
|
+
var(--ask-highlight) 50%,
|
|
57
|
+
transparent 100%
|
|
58
|
+
);
|
|
59
|
+
animation: ask-shimmer-kf var(--ask-dur) infinite;
|
|
19
60
|
}
|
|
20
|
-
@keyframes ask-shimmer {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
61
|
+
@keyframes ask-shimmer-kf { to { transform: translateX(100%); } }
|
|
62
|
+
|
|
63
|
+
.ask-pulse.ask-b {
|
|
64
|
+
animation: ask-pulse-kf var(--ask-dur) ease-in-out infinite;
|
|
65
|
+
}
|
|
66
|
+
@keyframes ask-pulse-kf { 0%,100%{opacity:1} 50%{opacity:.35} }
|
|
67
|
+
|
|
68
|
+
.ask-wave.ask-b::after {
|
|
24
69
|
content: '';
|
|
25
70
|
position: absolute;
|
|
26
71
|
inset: 0;
|
|
27
72
|
transform: translateX(-100%);
|
|
28
|
-
background: linear-gradient(
|
|
29
|
-
|
|
30
|
-
|
|
73
|
+
background: linear-gradient(
|
|
74
|
+
90deg,
|
|
75
|
+
transparent 0%,
|
|
76
|
+
var(--ask-highlight) 50%,
|
|
77
|
+
transparent 100%
|
|
78
|
+
);
|
|
79
|
+
animation: ask-wave-kf calc(var(--ask-dur) * 1.15) ease-in-out infinite;
|
|
31
80
|
}
|
|
32
|
-
@keyframes ask-wave { 0%
|
|
33
|
-
|
|
34
|
-
|
|
81
|
+
@keyframes ask-wave-kf { 0%{transform:translateX(-100%)} 100%{transform:translateX(150%)} }
|
|
82
|
+
|
|
83
|
+
.ask-glow.ask-b {
|
|
84
|
+
animation: ask-glow-kf var(--ask-dur) ease-in-out infinite;
|
|
85
|
+
}
|
|
86
|
+
@keyframes ask-glow-kf {
|
|
87
|
+
0%,100%{ box-shadow: 0 0 0px 0px transparent; }
|
|
88
|
+
50% { box-shadow: 0 0 12px 2px var(--ask-highlight); }
|
|
35
89
|
}
|
|
36
|
-
|
|
37
|
-
.ask-
|
|
38
|
-
|
|
39
|
-
|
|
90
|
+
|
|
91
|
+
.ask-none.ask-b { opacity: .55; }
|
|
92
|
+
|
|
93
|
+
/* reduced motion: replace all animation with simple fade */
|
|
94
|
+
@media (prefers-reduced-motion: reduce) {
|
|
95
|
+
.ask-shimmer.ask-b::after,
|
|
96
|
+
.ask-wave.ask-b::after { animation: none; background: none; }
|
|
97
|
+
.ask-shimmer.ask-b,
|
|
98
|
+
.ask-wave.ask-b,
|
|
99
|
+
.ask-pulse.ask-b,
|
|
100
|
+
.ask-glow.ask-b {
|
|
101
|
+
animation: ask-pulse-kf calc(var(--ask-dur)*2) ease-in-out infinite;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* wrapper utilities */
|
|
106
|
+
.ask-wrap { pointer-events: none; user-select: none; }
|
|
107
|
+
.ask-row { display: flex; flex-direction: row; }
|
|
108
|
+
.ask-col { display: flex; flex-direction: column; }
|
|
109
|
+
|
|
110
|
+
/* transition helper applied to real children */
|
|
111
|
+
.ask-fade-in { animation: ask-fadein 0.25s ease both; }
|
|
112
|
+
@keyframes ask-fadein { from{opacity:0; transform:translateY(4px)} to{opacity:1; transform:none} }
|
|
40
113
|
`;
|
|
41
114
|
|
|
42
|
-
|
|
115
|
+
var injected = false;
|
|
43
116
|
|
|
44
117
|
function injectStyles() {
|
|
45
|
-
if (injected
|
|
118
|
+
if (injected) return;
|
|
119
|
+
if (typeof document === 'undefined') { injected = true; return; }
|
|
46
120
|
if (document.getElementById(STYLE_ID)) { injected = true; return; }
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
document.head.appendChild(
|
|
121
|
+
var s = document.createElement('style');
|
|
122
|
+
s.id = STYLE_ID;
|
|
123
|
+
s.textContent = BASE_CSS;
|
|
124
|
+
document.head.appendChild(s);
|
|
51
125
|
injected = true;
|
|
52
126
|
}
|
|
53
127
|
|
|
128
|
+
/* ── theme presets ── */
|
|
129
|
+
var THEMES = {
|
|
130
|
+
default: {},
|
|
131
|
+
dark: {
|
|
132
|
+
'--ask-base': '#1a202c',
|
|
133
|
+
'--ask-highlight': 'rgba(255,255,255,0.04)',
|
|
134
|
+
},
|
|
135
|
+
minimal: {
|
|
136
|
+
'--ask-base': '#f1f5f9',
|
|
137
|
+
'--ask-highlight': 'rgba(255,255,255,0.9)',
|
|
138
|
+
'--ask-dur': '1.8s',
|
|
139
|
+
},
|
|
140
|
+
soft: {
|
|
141
|
+
'--ask-base': '#ede9fe',
|
|
142
|
+
'--ask-highlight': 'rgba(255,255,255,0.7)',
|
|
143
|
+
'--ask-radius': '8px',
|
|
144
|
+
},
|
|
145
|
+
brand: {
|
|
146
|
+
'--ask-base': '#dbeafe',
|
|
147
|
+
'--ask-highlight': 'rgba(255,255,255,0.75)',
|
|
148
|
+
'--ask-radius': '6px',
|
|
149
|
+
'--ask-dur': '1.2s',
|
|
150
|
+
},
|
|
151
|
+
warm: {
|
|
152
|
+
'--ask-base': '#fef3c7',
|
|
153
|
+
'--ask-highlight': 'rgba(255,255,255,0.7)',
|
|
154
|
+
'--ask-radius': '6px',
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Build an inline style object merging a named preset + user overrides.
|
|
160
|
+
* Returns a plain object suitable for React style={} prop.
|
|
161
|
+
*/
|
|
162
|
+
function buildThemeStyle(preset, overrides) {
|
|
163
|
+
preset = preset || 'default';
|
|
164
|
+
overrides = overrides || {};
|
|
165
|
+
var base = THEMES[preset] || {};
|
|
166
|
+
var merged = Object.assign({}, base);
|
|
167
|
+
|
|
168
|
+
// Map user-facing keys → CSS vars
|
|
169
|
+
var keyMap = {
|
|
170
|
+
baseColor: '--ask-base',
|
|
171
|
+
highlightColor: '--ask-highlight',
|
|
172
|
+
shimmerColor: '--ask-highlight', // alias
|
|
173
|
+
duration: '--ask-dur',
|
|
174
|
+
borderRadius: '--ask-radius',
|
|
175
|
+
};
|
|
176
|
+
Object.keys(overrides).forEach(function(k) {
|
|
177
|
+
var cssVar = keyMap[k] || k;
|
|
178
|
+
merged[cssVar] = overrides[k];
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return merged;
|
|
182
|
+
}
|
|
183
|
+
|
|
54
184
|
function blockClass(animation, shape) {
|
|
55
|
-
|
|
56
|
-
const parts = ['ask-block'];
|
|
57
|
-
if (animation !== 'none') parts.push('ask-' + animation);
|
|
185
|
+
var parts = ['ask-b', 'ask-' + (animation || 'shimmer')];
|
|
58
186
|
if (shape) parts.push('ask-' + shape);
|
|
59
187
|
return parts.join(' ');
|
|
60
188
|
}
|
|
61
189
|
|
|
62
|
-
|
|
63
|
-
theme = theme || {};
|
|
64
|
-
const map = {
|
|
65
|
-
baseColor: '--ask-base-color',
|
|
66
|
-
shimmerColor: '--ask-shimmer-color',
|
|
67
|
-
duration: '--ask-duration',
|
|
68
|
-
borderRadius: '--ask-border-radius',
|
|
69
|
-
};
|
|
70
|
-
return Object.entries(theme)
|
|
71
|
-
.filter(function(pair) { return map[pair[0]]; })
|
|
72
|
-
.map(function(pair) { return map[pair[0]] + ':' + pair[1]; })
|
|
73
|
-
.join(';');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
module.exports = { injectStyles, blockClass, buildThemeVars, CSS };
|
|
190
|
+
module.exports = { injectStyles, blockClass, buildThemeStyle, THEMES, BASE_CSS };
|
package/src/AutoSkeleton.jsx
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AutoSkeleton.jsx
|
|
3
|
-
* Main wrapper component.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* <AutoSkeleton loading={isLoading}>
|
|
7
|
-
* <ProductCard />
|
|
8
|
-
* </AutoSkeleton>
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import React, { useEffect, useMemo } from 'react';
|
|
12
|
-
import { analyzeTree } from './analyzer.js';
|
|
13
|
-
import { renderSkeletonNodes } from './renderer.js';
|
|
14
|
-
import { injectStyles } from './styles.js';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* @typedef {Object} AutoSkeletonProps
|
|
18
|
-
* @property {boolean} loading - Show skeleton when true
|
|
19
|
-
* @property {React.ReactNode} children - The real UI component(s)
|
|
20
|
-
* @property {'shimmer'|'pulse'|'wave'|'none'} [animation='shimmer']
|
|
21
|
-
* @property {number} [repeat=1] - Repeat skeleton N times (e.g. list items)
|
|
22
|
-
* @property {object} [theme] - { baseColor, highlightColor }
|
|
23
|
-
* @property {React.CSSProperties} [style] - Wrapper style
|
|
24
|
-
* @property {string} [className] - Wrapper className
|
|
25
|
-
* @property {string} [ariaLabel] - aria-label for the skeleton wrapper
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
export function AutoSkeleton({
|
|
29
|
-
loading = false,
|
|
30
|
-
children,
|
|
31
|
-
animation = 'shimmer',
|
|
32
|
-
repeat = 1,
|
|
33
|
-
theme = {},
|
|
34
|
-
style,
|
|
35
|
-
className,
|
|
36
|
-
ariaLabel = 'Loading…',
|
|
37
|
-
}) {
|
|
38
|
-
// Inject global CSS once
|
|
39
|
-
useEffect(() => { injectStyles(); }, []);
|
|
40
|
-
|
|
41
|
-
// Analyse the child tree once (memoised — re-runs only when children change)
|
|
42
|
-
const skeletonNodes = useMemo(() => {
|
|
43
|
-
if (!loading) return null;
|
|
44
|
-
const childArray = React.Children.toArray(children);
|
|
45
|
-
return childArray.flatMap(child => analyzeTree(child));
|
|
46
|
-
}, [loading, children]);
|
|
47
|
-
|
|
48
|
-
if (!loading) return children;
|
|
49
|
-
|
|
50
|
-
const options = { animation, theme };
|
|
51
|
-
const units = Array.from({ length: repeat }, (_, i) =>
|
|
52
|
-
React.createElement(
|
|
53
|
-
'div',
|
|
54
|
-
{ key: i, style: i > 0 ? { marginTop: 16 } : undefined },
|
|
55
|
-
renderSkeletonNodes(skeletonNodes, options)
|
|
56
|
-
)
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
return React.createElement(
|
|
60
|
-
'div',
|
|
61
|
-
{
|
|
62
|
-
role: 'status',
|
|
63
|
-
'aria-busy': true,
|
|
64
|
-
'aria-label': ariaLabel,
|
|
65
|
-
style,
|
|
66
|
-
className,
|
|
67
|
-
},
|
|
68
|
-
...units
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export default AutoSkeleton;
|
package/src/SkeletonItem.jsx
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SkeletonItem.jsx
|
|
3
|
-
* Low-level primitive for manual skeleton building if needed.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* <SkeletonItem type="text" lines={2} />
|
|
7
|
-
* <SkeletonItem type="avatar" size={48} />
|
|
8
|
-
* <SkeletonItem type="image" width="100%" height={200} />
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import React, { useEffect } from 'react';
|
|
12
|
-
import { buildClassName, injectStyles } from './styles.js';
|
|
13
|
-
|
|
14
|
-
export function SkeletonItem({
|
|
15
|
-
type = 'text',
|
|
16
|
-
lines = 1,
|
|
17
|
-
width,
|
|
18
|
-
height,
|
|
19
|
-
size,
|
|
20
|
-
animation = 'shimmer',
|
|
21
|
-
theme = {},
|
|
22
|
-
style: extraStyle = {},
|
|
23
|
-
}) {
|
|
24
|
-
useEffect(() => { injectStyles(); }, []);
|
|
25
|
-
|
|
26
|
-
const cssVars = {
|
|
27
|
-
...(theme.baseColor && { '--skeleton-base-color': theme.baseColor }),
|
|
28
|
-
...(theme.highlightColor && { '--skeleton-highlight-color': theme.highlightColor }),
|
|
29
|
-
...extraStyle,
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
switch (type) {
|
|
33
|
-
case 'text': {
|
|
34
|
-
const cls = buildClassName(animation, 'auto-skeleton-text-line');
|
|
35
|
-
if (lines === 1) {
|
|
36
|
-
return React.createElement('div', {
|
|
37
|
-
className: cls,
|
|
38
|
-
style: { width: width || '80%', ...cssVars },
|
|
39
|
-
'aria-hidden': true,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
const lineEls = Array.from({ length: lines }, (_, i) =>
|
|
43
|
-
React.createElement('div', {
|
|
44
|
-
key: i,
|
|
45
|
-
className: cls,
|
|
46
|
-
style: { width: i === lines - 1 ? '65%' : '100%', ...cssVars },
|
|
47
|
-
'aria-hidden': true,
|
|
48
|
-
})
|
|
49
|
-
);
|
|
50
|
-
return React.createElement('div', {
|
|
51
|
-
className: 'auto-skeleton-text-block',
|
|
52
|
-
style: cssVars,
|
|
53
|
-
'aria-hidden': true,
|
|
54
|
-
}, ...lineEls);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
case 'avatar': {
|
|
58
|
-
const px = size ? `${size}px` : (width || '40px');
|
|
59
|
-
return React.createElement('div', {
|
|
60
|
-
className: buildClassName(animation, 'auto-skeleton-avatar-block'),
|
|
61
|
-
style: { width: px, height: size ? `${size}px` : (height || px), ...cssVars },
|
|
62
|
-
'aria-hidden': true,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
case 'image': {
|
|
67
|
-
return React.createElement('div', {
|
|
68
|
-
className: buildClassName(animation, 'auto-skeleton-image-block'),
|
|
69
|
-
style: {
|
|
70
|
-
width: width || '100%',
|
|
71
|
-
...(height ? { paddingTop: 0, height } : {}),
|
|
72
|
-
...cssVars,
|
|
73
|
-
},
|
|
74
|
-
'aria-hidden': true,
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
case 'button': {
|
|
79
|
-
return React.createElement('div', {
|
|
80
|
-
className: buildClassName(animation, 'auto-skeleton-button-block'),
|
|
81
|
-
style: { width: width || 100, ...cssVars },
|
|
82
|
-
'aria-hidden': true,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
case 'input': {
|
|
87
|
-
return React.createElement('div', {
|
|
88
|
-
className: 'auto-skeleton-input-block',
|
|
89
|
-
style: cssVars,
|
|
90
|
-
'aria-hidden': true,
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
default:
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export default SkeletonItem;
|