auto-loading-skeleton 1.0.3 → 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/src/styles.js CHANGED
@@ -1,76 +1,190 @@
1
- const STYLE_ID = 'auto-skeleton-styles';
2
-
3
- const CSS = `
4
- .ask-block {
5
- display: inline-block;
6
- position: relative;
7
- overflow: hidden;
8
- background-color: var(--ask-base-color, #e2e8f0);
9
- border-radius: var(--ask-border-radius, 4px);
10
- vertical-align: middle;
11
- }
12
- .ask-shimmer::after {
13
- content: '';
14
- position: absolute;
15
- inset: 0;
16
- transform: translateX(-100%);
17
- background: linear-gradient(90deg, transparent 0%, var(--ask-shimmer-color, rgba(255,255,255,0.55)) 50%, transparent 100%);
18
- animation: ask-shimmer var(--ask-duration, 1.4s) infinite;
19
- }
20
- @keyframes ask-shimmer { 100% { transform: translateX(100%); } }
21
- .ask-pulse { animation: ask-pulse var(--ask-duration, 1.8s) ease-in-out infinite; }
22
- @keyframes ask-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }
23
- .ask-wave::after {
24
- content: '';
25
- position: absolute;
26
- inset: 0;
27
- transform: translateX(-100%);
28
- background: linear-gradient(90deg, transparent 0%, var(--ask-shimmer-color, rgba(255,255,255,0.8)) 50%, transparent 100%);
29
- animation: ask-wave var(--ask-duration, 1.6s) ease-in-out infinite;
30
- overflow: hidden;
31
- }
32
- @keyframes ask-wave { 0% { transform: translateX(-100%); } 100% { transform: translateX(150%); } }
33
- @media (prefers-color-scheme: dark) {
34
- .ask-block { background-color: var(--ask-base-color-dark, #2d3748); }
35
- }
36
- .ask-circle { border-radius: 50%; }
37
- .ask-rounded { border-radius: 8px; }
38
- .ask-pill { border-radius: 9999px; }
39
- .ask-wrapper { pointer-events: none; user-select: none; }
40
- `;
41
-
42
- let injected = false;
43
-
44
- function injectStyles() {
45
- if (injected || typeof document === 'undefined') return;
46
- if (document.getElementById(STYLE_ID)) { injected = true; return; }
47
- const style = document.createElement('style');
48
- style.id = STYLE_ID;
49
- style.textContent = CSS;
50
- document.head.appendChild(style);
51
- injected = true;
52
- }
53
-
54
- function blockClass(animation, shape) {
55
- animation = animation || 'shimmer';
56
- const parts = ['ask-block'];
57
- if (animation !== 'none') parts.push('ask-' + animation);
58
- if (shape) parts.push('ask-' + shape);
59
- return parts.join(' ');
60
- }
61
-
62
- function buildThemeVars(theme) {
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 };
1
+ 'use strict';
2
+
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;
37
+ position: relative;
38
+ overflow: hidden;
39
+ background: var(--ask-base);
40
+ border-radius: var(--ask-radius);
41
+ flex-shrink: 0;
42
+ }
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 {
49
+ content: '';
50
+ position: absolute;
51
+ inset: 0;
52
+ transform: translateX(-100%);
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;
60
+ }
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 {
69
+ content: '';
70
+ position: absolute;
71
+ inset: 0;
72
+ transform: translateX(-100%);
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;
80
+ }
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); }
89
+ }
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} }
113
+ `;
114
+
115
+ var injected = false;
116
+
117
+ function injectStyles() {
118
+ if (injected) return;
119
+ if (typeof document === 'undefined') { injected = true; return; }
120
+ if (document.getElementById(STYLE_ID)) { injected = true; return; }
121
+ var s = document.createElement('style');
122
+ s.id = STYLE_ID;
123
+ s.textContent = BASE_CSS;
124
+ document.head.appendChild(s);
125
+ injected = true;
126
+ }
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
+
184
+ function blockClass(animation, shape) {
185
+ var parts = ['ask-b', 'ask-' + (animation || 'shimmer')];
186
+ if (shape) parts.push('ask-' + shape);
187
+ return parts.join(' ');
188
+ }
189
+
190
+ module.exports = { injectStyles, blockClass, buildThemeStyle, THEMES, BASE_CSS };
@@ -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;
@@ -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;