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/src/styles.js CHANGED
@@ -1,76 +1,190 @@
1
- const STYLE_ID = 'auto-skeleton-styles';
1
+ 'use strict';
2
2
 
3
- const CSS = `
4
- .ask-block {
5
- display: inline-block;
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-color: var(--ask-base-color, #e2e8f0);
9
- border-radius: var(--ask-border-radius, 4px);
10
- vertical-align: middle;
39
+ background: var(--ask-base);
40
+ border-radius: var(--ask-radius);
41
+ flex-shrink: 0;
11
42
  }
12
- .ask-shimmer::after {
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(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;
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 { 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 {
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(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;
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% { transform: translateX(-100%); } 100% { transform: translateX(150%); } }
33
- @media (prefers-color-scheme: dark) {
34
- .ask-block { background-color: var(--ask-base-color-dark, #2d3748); }
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
- .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; }
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
- let injected = false;
115
+ var injected = false;
43
116
 
44
117
  function injectStyles() {
45
- if (injected || typeof document === 'undefined') return;
118
+ if (injected) return;
119
+ if (typeof document === 'undefined') { injected = true; return; }
46
120
  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);
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
- animation = animation || 'shimmer';
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
- 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 };
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;