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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auto-loading-skeleton",
3
- "version": "1.0.3",
3
+ "version": "2.0.0",
4
4
  "description": "Automatically generate loading skeleton UIs from your existing React components — no manual skeleton screens needed.",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -0,0 +1,117 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * AutoSkeleton.js — V2
5
+ *
6
+ * Improvements over V1:
7
+ * - Reads defaults from SkeletonContext (SkeletonProvider)
8
+ * - Memoized descriptor via cache.js
9
+ * - fadeIn prop smoothly transitions real content in after loading
10
+ * - theme preset + themeOverrides merged into CSS vars
11
+ * - aria-live="polite" so screen readers announce content arrival
12
+ * - count renders siblings separated by gap (configurable)
13
+ * - renderSkeleton render prop for full manual override
14
+ * - onLoaded callback fired when loading transitions false
15
+ * - SSR safe (injectStyles is no-op on server)
16
+ */
17
+
18
+ var React = require('react');
19
+ var analyzeElement = require('./analyzer').analyzeElement;
20
+ var renderNode = require('./renderer').renderNode;
21
+ var resetKeys = require('./renderer').resetKeys;
22
+ var injectStyles = require('./styles').injectStyles;
23
+ var buildThemeStyle = require('./styles').buildThemeStyle;
24
+ var cache = require('./cache');
25
+ var ctx = require('./context');
26
+
27
+ function AutoSkeleton(props) {
28
+ var loading = props.loading;
29
+ var children = props.children;
30
+ var className = props.className || '';
31
+ var style = props.style || {};
32
+ var count = props.count || 1;
33
+ var gap = props.gap || '16px';
34
+ var ariaLabel = props.ariaLabel || 'Loading…';
35
+ var onLoaded = props.onLoaded;
36
+ var renderSkeleton = props.renderSkeleton; // render prop override
37
+
38
+ // Read context
39
+ var context = React.useContext(ctx.SkeletonContext);
40
+ var animation = props.animation || context.animation;
41
+ var theme = props.theme || context.theme;
42
+ var themeOverrides = props.themeOverrides || context.themeOverrides;
43
+ var fadeIn = props.fadeIn != null ? props.fadeIn : context.fadeIn;
44
+ var maxDepth = props.maxDepth != null ? props.maxDepth : context.maxDepth;
45
+ var detectRepeats = props.detectRepeats != null ? props.detectRepeats : context.detectRepeats;
46
+
47
+ // Inject base CSS once
48
+ React.useEffect(function() { injectStyles(); }, []);
49
+ if (typeof document !== 'undefined') injectStyles();
50
+
51
+ // Fire onLoaded when loading transitions to false
52
+ var prevLoading = React.useRef(loading);
53
+ React.useEffect(function() {
54
+ if (prevLoading.current === true && loading === false && typeof onLoaded === 'function') {
55
+ onLoaded();
56
+ }
57
+ prevLoading.current = loading;
58
+ }, [loading]);
59
+
60
+ // Build theme CSS vars
61
+ var themeStyle = buildThemeStyle(theme, themeOverrides);
62
+ var wrapStyle = Object.assign({}, style, themeStyle);
63
+
64
+ if (!loading) {
65
+ // Render real children with optional fade-in animation
66
+ return React.createElement('div', {
67
+ 'data-ask': '',
68
+ className: 'ask-wrap' + (className ? ' ' + className : '') + (fadeIn ? ' ask-fade-in' : ''),
69
+ style: wrapStyle,
70
+ 'aria-live': 'polite',
71
+ 'aria-busy': 'false',
72
+ }, children);
73
+ }
74
+
75
+ // Use render prop if provided
76
+ if (typeof renderSkeleton === 'function') {
77
+ return React.createElement('div', {
78
+ 'data-ask': '',
79
+ className: 'ask-wrap' + (className ? ' ' + className : ''),
80
+ style: wrapStyle,
81
+ 'aria-busy': 'true',
82
+ 'aria-label': ariaLabel,
83
+ }, renderSkeleton({ animation: animation }));
84
+ }
85
+
86
+ // Analyze + render skeleton
87
+ var descriptor = cache.get(children);
88
+ if (!descriptor) {
89
+ descriptor = analyzeElement(children, 0, { maxDepth: maxDepth, detectRepeats: detectRepeats });
90
+ cache.set(children, descriptor);
91
+ }
92
+
93
+ resetKeys();
94
+ var skeletonEl = renderNode(descriptor, { animation: animation });
95
+
96
+ // Render count copies
97
+ var items = [];
98
+ for (var i = 0; i < Math.max(1, count); i++) {
99
+ items.push(React.createElement('div', {
100
+ key: 'ask-item-' + i,
101
+ style: i < count - 1 ? { marginBottom: gap } : {},
102
+ }, skeletonEl));
103
+ }
104
+
105
+ return React.createElement('div', {
106
+ 'data-ask': '',
107
+ className: 'ask-wrap' + (className ? ' ' + className : ''),
108
+ style: wrapStyle,
109
+ 'aria-busy': 'true',
110
+ 'aria-label': ariaLabel,
111
+ role: 'status',
112
+ }, items);
113
+ }
114
+
115
+ AutoSkeleton.displayName = 'AutoSkeleton';
116
+
117
+ module.exports = AutoSkeleton;
package/src/analyzer.js CHANGED
@@ -1,113 +1,336 @@
1
- const ELEMENT_TYPES = {
2
- TEXT: 'text',
3
- IMAGE: 'image',
4
- AVATAR: 'avatar',
5
- BUTTON: 'button',
6
- INPUT: 'input',
7
- CONTAINER: 'container',
8
- ICON: 'icon',
9
- };
10
-
11
- function inferType(element) {
12
- if (!element || typeof element !== 'object') return ELEMENT_TYPES.TEXT;
13
- var type = element.type;
14
- var props = element.props || {};
15
- var tag = typeof type === 'string' ? type.toLowerCase() : '';
16
- var cls = (props.className || '').toLowerCase();
17
- var alt = (props.alt || '').toLowerCase();
18
- var role = (props.role || '').toLowerCase();
19
-
20
- if (tag === 'img') {
21
- var isAvatar = cls.includes('avatar') || cls.includes('profile') || alt.includes('avatar');
22
- return isAvatar ? ELEMENT_TYPES.AVATAR : ELEMENT_TYPES.IMAGE;
23
- }
24
- if (tag === 'button' || role === 'button') return ELEMENT_TYPES.BUTTON;
25
- if (tag === 'input' || tag === 'textarea' || tag === 'select') return ELEMENT_TYPES.INPUT;
26
- if (tag === 'svg' || cls.includes('icon')) return ELEMENT_TYPES.ICON;
27
- if (cls.includes('avatar') || cls.includes('profile-pic')) return ELEMENT_TYPES.AVATAR;
28
- return ELEMENT_TYPES.CONTAINER;
29
- }
30
-
31
- function isTextLeaf(element) {
32
- if (!element || typeof element !== 'object') return false;
33
- var children = element.props && element.props.children;
34
- if (typeof children === 'string') return true;
35
- if (typeof children === 'number') return true;
36
- if (Array.isArray(children)) {
37
- return children.every(function(c) { return typeof c === 'string' || typeof c === 'number'; });
38
- }
39
- return false;
40
- }
41
-
42
- function extractStyleHints(element) {
43
- var props = (element && element.props) || {};
44
- var style = props.style || {};
45
- return {
46
- width: style.width,
47
- height: style.height,
48
- borderRadius: style.borderRadius,
49
- display: style.display,
50
- flexDirection: style.flexDirection,
51
- alignItems: style.alignItems,
52
- gap: style.gap,
53
- padding: style.padding,
54
- margin: style.margin,
55
- };
56
- }
57
-
58
- function analyzeElement(element, depth) {
59
- depth = depth || 0;
60
- if (element === null || element === undefined) return null;
61
- if (typeof element === 'string' || typeof element === 'number') {
62
- return { nodeType: ELEMENT_TYPES.TEXT, content: String(element), depth: depth };
63
- }
64
- if (typeof element === 'boolean') return null;
65
-
66
- if (Array.isArray(element)) {
67
- return element.map(function(el) { return analyzeElement(el, depth); }).filter(Boolean);
68
- }
69
-
70
- var type = element.type;
71
- var props = element.props || {};
72
- var styleHints = extractStyleHints(element);
73
-
74
- // Structural elements take priority over text-leaf detection
75
- var earlyType = inferType(element);
76
- if (earlyType === ELEMENT_TYPES.BUTTON || earlyType === ELEMENT_TYPES.INPUT) {
77
- var lbl = earlyType === ELEMENT_TYPES.BUTTON && typeof props.children === 'string' ? props.children : '';
78
- return { nodeType: earlyType, label: lbl, styleHints: styleHints, depth: depth };
79
- }
80
-
81
- if (isTextLeaf(element)) {
82
- var tag = typeof type === 'string' ? type.toLowerCase() : '';
83
- var isHeading = /^h[1-6]$/.test(tag);
84
- return { nodeType: ELEMENT_TYPES.TEXT, isHeading: isHeading, tag: tag, content: String(props.children), styleHints: styleHints, depth: depth };
85
- }
86
-
87
- var nodeType = inferType(element);
88
-
89
- if (nodeType === ELEMENT_TYPES.IMAGE || nodeType === ELEMENT_TYPES.AVATAR || nodeType === ELEMENT_TYPES.ICON) {
90
- return { nodeType: nodeType, styleHints: styleHints, depth: depth };
91
- }
92
- if (nodeType === ELEMENT_TYPES.BUTTON) {
93
- return { nodeType: ELEMENT_TYPES.BUTTON, label: typeof props.children === 'string' ? props.children : '', styleHints: styleHints, depth: depth };
94
- }
95
- if (nodeType === ELEMENT_TYPES.INPUT) {
96
- return { nodeType: ELEMENT_TYPES.INPUT, styleHints: styleHints, depth: depth };
97
- }
98
-
99
- var rawChildren = props.children;
100
- var children = [];
101
- if (rawChildren) {
102
- var arr = Array.isArray(rawChildren) ? rawChildren : [rawChildren];
103
- arr.forEach(function(child) {
104
- var result = analyzeElement(child, depth + 1);
105
- if (Array.isArray(result)) { result.forEach(function(r) { if (r) children.push(r); }); }
106
- else if (result) children.push(result);
107
- });
108
- }
109
-
110
- return { nodeType: ELEMENT_TYPES.CONTAINER, tag: typeof type === 'string' ? type : 'div', styleHints: styleHints, children: children, depth: depth };
111
- }
112
-
113
- module.exports = { analyzeElement: analyzeElement, ELEMENT_TYPES: ELEMENT_TYPES };
1
+ 'use strict';
2
+
3
+ /**
4
+ * analyzer.js — V2
5
+ *
6
+ * Improvements over V1:
7
+ * - Depth-limited recursion with configurable maxDepth
8
+ * - ARIA role awareness (role="img", role="button", etc.)
9
+ * - Smart width inference from text content length & element type
10
+ * - Detects flex/grid containers and replicates their direction
11
+ * - Detects repeat patterns (lists of similar children) for count-based skeletons
12
+ * - Extracts computed className hints (card, title, subtitle, badge, tag, chip…)
13
+ * - Inline style extraction: width, height, borderRadius, display, flexDirection, gap, padding, margin, aspectRatio
14
+ * - Handles React.Fragment and arrays
15
+ * - Skips purely decorative elements (aria-hidden, role="presentation")
16
+ * - Detects video/audio/iframe as media placeholders
17
+ * - Result memoization keyed on element type + props shape
18
+ */
19
+
20
+ const NODE = {
21
+ TEXT: 'text',
22
+ HEADING: 'heading',
23
+ IMAGE: 'image',
24
+ AVATAR: 'avatar',
25
+ MEDIA: 'media', // video / audio / iframe
26
+ BUTTON: 'button',
27
+ INPUT: 'input',
28
+ TEXTAREA: 'textarea',
29
+ SELECT: 'select',
30
+ BADGE: 'badge',
31
+ ICON: 'icon',
32
+ CARD: 'card',
33
+ CONTAINER: 'container',
34
+ REPEAT: 'repeat', // detected list pattern
35
+ SKIP: 'skip', // decorative / aria-hidden
36
+ };
37
+
38
+ /* ── helpers ── */
39
+
40
+ function tag(el) {
41
+ return typeof el.type === 'string' ? el.type.toLowerCase() : null;
42
+ }
43
+
44
+ function cls(el) {
45
+ return ((el.props && el.props.className) || '').toLowerCase();
46
+ }
47
+
48
+ function role(el) {
49
+ return ((el.props && el.props.role) || '').toLowerCase();
50
+ }
51
+
52
+ function aria(el, key) {
53
+ return el.props && el.props[key];
54
+ }
55
+
56
+ function styleOf(el) {
57
+ return (el.props && el.props.style) || {};
58
+ }
59
+
60
+ function isDecorativeEl(el) {
61
+ if (!el || typeof el !== 'object') return false;
62
+ return aria(el, 'aria-hidden') === true ||
63
+ aria(el, 'aria-hidden') === 'true' ||
64
+ role(el) === 'presentation' ||
65
+ role(el) === 'none';
66
+ }
67
+
68
+ function extractStyle(el) {
69
+ const s = styleOf(el);
70
+ return {
71
+ width: s.width || null,
72
+ height: s.height || null,
73
+ borderRadius: s.borderRadius || null,
74
+ display: s.display || null,
75
+ flexDir: s.flexDirection|| null,
76
+ gap: s.gap || null,
77
+ padding: s.padding || s.p || null,
78
+ margin: s.margin || s.m || null,
79
+ aspectRatio: s.aspectRatio || null,
80
+ maxWidth: s.maxWidth || null,
81
+ minHeight: s.minHeight || null,
82
+ alignItems: s.alignItems || null,
83
+ fontSize: s.fontSize || null,
84
+ fontWeight: s.fontWeight || null,
85
+ };
86
+ }
87
+
88
+ const CLASS_PATTERNS = {
89
+ avatar: /\b(avatar|profile[-_]?pic|user[-_]?img|user[-_]?photo|pfp)\b/,
90
+ badge: /\b(badge|chip|tag|label|pill|status[-_]?dot)\b/,
91
+ icon: /\b(icon|ico|svg[-_]?icon|material[-_]?icon|fa[-_]|bi[-_]|ri[-_]|heroicon)\b/,
92
+ heading: /\b(heading|title|headline|h[1-6]|display[-_]?(text|title))\b/,
93
+ card: /\b(card|panel|tile|widget|box[-_]?container)\b/,
94
+ button: /\b(btn|button)\b/,
95
+ input: /\b(input|field|text[-_]?box|form[-_]?control)\b/,
96
+ media: /\b(media|video|player|thumbnail|cover[-_]?img|hero[-_]?img|banner)\b/,
97
+ list: /\b(list|feed|grid|gallery|results)\b/,
98
+ };
99
+
100
+ function matchesClass(el, key) {
101
+ return CLASS_PATTERNS[key] && CLASS_PATTERNS[key].test(cls(el));
102
+ }
103
+
104
+ /* ── text content extraction ── */
105
+ function getTextContent(children) {
106
+ if (typeof children === 'string' || typeof children === 'number') {
107
+ return String(children);
108
+ }
109
+ if (Array.isArray(children)) {
110
+ return children.map(c => getTextContent(c && c.props ? c.props.children : c)).join('');
111
+ }
112
+ return '';
113
+ }
114
+
115
+ function isTextLeaf(el) {
116
+ const c = el.props && el.props.children;
117
+ if (typeof c === 'string' || typeof c === 'number') return true;
118
+ if (Array.isArray(c)) return c.every(x => typeof x === 'string' || typeof x === 'number' || x == null);
119
+ return false;
120
+ }
121
+
122
+ /* ── list/repeat pattern detection ── */
123
+ function detectRepeat(children) {
124
+ if (!Array.isArray(children) || children.length < 2) return null;
125
+ const real = children.filter(c => c && typeof c === 'object');
126
+ if (real.length < 2) return null;
127
+
128
+ // Check if all children share the same element type
129
+ const firstType = real[0].type;
130
+ const allSame = real.every(c => c.type === firstType);
131
+ if (allSame && real.length >= 2) {
132
+ return { count: real.length, template: real[0] };
133
+ }
134
+
135
+ // Check if all children share the same className prefix
136
+ const firstCls = cls(real[0]).split(' ')[0];
137
+ if (firstCls && real.every(c => cls(c).startsWith(firstCls))) {
138
+ return { count: real.length, template: real[0] };
139
+ }
140
+
141
+ return null;
142
+ }
143
+
144
+ /* ── main analyzer ── */
145
+ function analyzeElement(element, depth, opts) {
146
+ depth = depth || 0;
147
+ opts = opts || {};
148
+ var maxDepth = opts.maxDepth != null ? opts.maxDepth : 8;
149
+
150
+ if (element === null || element === undefined) return null;
151
+ if (typeof element === 'boolean') return null;
152
+
153
+ // String / number node
154
+ if (typeof element === 'string' || typeof element === 'number') {
155
+ var str = String(element).trim();
156
+ if (!str) return null;
157
+ return {
158
+ nodeType: NODE.TEXT,
159
+ content: str,
160
+ width: estimateTextWidth(str),
161
+ depth: depth,
162
+ };
163
+ }
164
+
165
+ // Array / Fragment
166
+ if (Array.isArray(element)) {
167
+ var results = [];
168
+ element.forEach(function(el) {
169
+ var r = analyzeElement(el, depth, opts);
170
+ if (Array.isArray(r)) results = results.concat(r);
171
+ else if (r) results.push(r);
172
+ });
173
+ return results.length === 1 ? results[0] : results;
174
+ }
175
+
176
+ // React.Fragment
177
+ if (element.type && (element.type === Symbol.for('react.fragment') || element.type.toString() === 'Symbol(react.fragment)')) {
178
+ return analyzeElement(element.props && element.props.children, depth, opts);
179
+ }
180
+
181
+ // Non-DOM component (function/class) — analyse its children if present
182
+ if (typeof element.type === 'function') {
183
+ var fc = element.props && element.props.children;
184
+ if (fc) return analyzeElement(fc, depth, opts);
185
+ // treat as opaque container
186
+ return { nodeType: NODE.CONTAINER, children: [], styleHints: {}, tag: 'div', depth: depth };
187
+ }
188
+
189
+ // Skip decorative
190
+ if (isDecorativeEl(element)) {
191
+ return { nodeType: NODE.SKIP, depth: depth };
192
+ }
193
+
194
+ var t = tag(element);
195
+ var c = cls(element);
196
+ var r = role(element);
197
+ var sHints = extractStyle(element);
198
+ var props = element.props || {};
199
+
200
+ // ── specific tag classification ──
201
+
202
+ if (t === 'img') {
203
+ var isAvatar = matchesClass(element, 'avatar') ||
204
+ /avatar|profile|pfp/i.test(props.alt || '') ||
205
+ (sHints.width && sHints.height && sHints.width === sHints.height && sHints.borderRadius);
206
+ return {
207
+ nodeType: isAvatar ? NODE.AVATAR : NODE.IMAGE,
208
+ styleHints: sHints,
209
+ alt: props.alt || '',
210
+ depth: depth,
211
+ };
212
+ }
213
+
214
+ if (t === 'video' || t === 'audio' || t === 'iframe') {
215
+ return { nodeType: NODE.MEDIA, styleHints: sHints, depth: depth };
216
+ }
217
+
218
+ if (t === 'svg' || r === 'img' && !sHints.width) {
219
+ return { nodeType: NODE.ICON, styleHints: sHints, depth: depth };
220
+ }
221
+
222
+ if (t === 'button' || r === 'button' || matchesClass(element, 'button')) {
223
+ return {
224
+ nodeType: NODE.BUTTON,
225
+ label: typeof props.children === 'string' ? props.children : '',
226
+ styleHints: sHints,
227
+ depth: depth,
228
+ };
229
+ }
230
+
231
+ if (t === 'input') {
232
+ var inputType = (props.type || 'text').toLowerCase();
233
+ if (inputType === 'checkbox' || inputType === 'radio') {
234
+ return { nodeType: NODE.ICON, styleHints: { width: '18px', height: '18px' }, depth: depth };
235
+ }
236
+ return { nodeType: NODE.INPUT, inputType: inputType, styleHints: sHints, depth: depth };
237
+ }
238
+
239
+ if (t === 'textarea') return { nodeType: NODE.TEXTAREA, styleHints: sHints, depth: depth };
240
+ if (t === 'select') return { nodeType: NODE.SELECT, styleHints: sHints, depth: depth };
241
+
242
+ // Heading tags
243
+ if (/^h[1-6]$/.test(t || '') || matchesClass(element, 'heading')) {
244
+ if (isTextLeaf(element)) {
245
+ var hContent = getTextContent(props.children);
246
+ return {
247
+ nodeType: NODE.HEADING,
248
+ level: t ? parseInt(t[1]) : 2,
249
+ content: hContent,
250
+ width: estimateTextWidth(hContent) || '60%',
251
+ styleHints: sHints,
252
+ depth: depth,
253
+ };
254
+ }
255
+ }
256
+
257
+ // Class-based inference
258
+ if (matchesClass(element, 'avatar')) {
259
+ return { nodeType: NODE.AVATAR, styleHints: sHints, depth: depth };
260
+ }
261
+ if (matchesClass(element, 'badge')) {
262
+ return { nodeType: NODE.BADGE, styleHints: sHints, depth: depth };
263
+ }
264
+ if (matchesClass(element, 'icon')) {
265
+ return { nodeType: NODE.ICON, styleHints: sHints, depth: depth };
266
+ }
267
+ if (matchesClass(element, 'media')) {
268
+ return { nodeType: NODE.IMAGE, styleHints: sHints, depth: depth };
269
+ }
270
+
271
+ // Text leaf inside any tag
272
+ if (isTextLeaf(element)) {
273
+ var content = getTextContent(props.children);
274
+ var isHead = matchesClass(element, 'heading') || /^h[1-6]$/.test(t || '');
275
+ var isBadge = matchesClass(element, 'badge');
276
+ if (isBadge) return { nodeType: NODE.BADGE, content: content, styleHints: sHints, depth: depth };
277
+ return {
278
+ nodeType: isHead ? NODE.HEADING : NODE.TEXT,
279
+ content: content,
280
+ width: sHints.width || estimateTextWidth(content),
281
+ styleHints: sHints,
282
+ depth: depth,
283
+ };
284
+ }
285
+
286
+ // Depth limit — treat as opaque block
287
+ if (depth >= maxDepth) {
288
+ return { nodeType: NODE.CONTAINER, children: [], styleHints: sHints, tag: t || 'div', depth: depth };
289
+ }
290
+
291
+ // ── Container: recurse ──
292
+ var rawChildren = props.children;
293
+ var childArr = rawChildren == null ? [] : (Array.isArray(rawChildren) ? rawChildren : [rawChildren]);
294
+
295
+ // Detect repeat pattern
296
+ var repeat = opts.detectRepeats !== false ? detectRepeat(childArr) : null;
297
+ if (repeat && depth > 0) {
298
+ return {
299
+ nodeType: NODE.REPEAT,
300
+ count: repeat.count,
301
+ template: analyzeElement(repeat.template, depth + 1, opts),
302
+ styleHints: sHints,
303
+ depth: depth,
304
+ };
305
+ }
306
+
307
+ var children = [];
308
+ childArr.forEach(function(child) {
309
+ var res = analyzeElement(child, depth + 1, opts);
310
+ if (Array.isArray(res)) { res.forEach(function(r2) { if (r2 && r2.nodeType !== NODE.SKIP) children.push(r2); }); }
311
+ else if (res && res.nodeType !== NODE.SKIP) children.push(res);
312
+ });
313
+
314
+ // Card heuristic: contains image + text children
315
+ var isCard = matchesClass(element, 'card') || t === 'article' || t === 'section';
316
+
317
+ return {
318
+ nodeType: isCard ? NODE.CARD : NODE.CONTAINER,
319
+ tag: t || 'div',
320
+ styleHints: sHints,
321
+ children: children,
322
+ depth: depth,
323
+ };
324
+ }
325
+
326
+ /* ── text width estimation ── */
327
+ function estimateTextWidth(text) {
328
+ if (!text) return '80%';
329
+ var len = text.length;
330
+ if (len <= 10) return Math.min(100, Math.max(20, len * 7)) + 'px';
331
+ if (len <= 30) return Math.min(90, 30 + len) + '%';
332
+ if (len <= 80) return '90%';
333
+ return '100%';
334
+ }
335
+
336
+ module.exports = { analyzeElement, NODE, estimateTextWidth };
package/src/cache.js ADDED
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * cache.js — V2
5
+ *
6
+ * LRU-style descriptor cache so repeated renders of the same component
7
+ * tree skip the recursive analyzeElement walk.
8
+ *
9
+ * Key: shallow fingerprint of the React element (type + prop keys + child count)
10
+ * Value: the analyzed descriptor
11
+ * Capacity: 32 entries (configurable)
12
+ */
13
+
14
+ var MAX = 32;
15
+ var _cache = new Map();
16
+
17
+ function fingerprint(element) {
18
+ if (!element || typeof element !== 'object') return String(element);
19
+ var t = typeof element.type === 'string' ? element.type : (element.type && element.type.displayName) || '?';
20
+ var p = element.props ? Object.keys(element.props).sort().join(',') : '';
21
+ var c = element.props && element.props.children;
22
+ var cl = Array.isArray(c) ? c.length : (c ? 1 : 0);
23
+ return t + '|' + p + '|' + cl;
24
+ }
25
+
26
+ function get(element) {
27
+ var key = fingerprint(element);
28
+ if (!_cache.has(key)) return null;
29
+ // LRU: move to end
30
+ var val = _cache.get(key);
31
+ _cache.delete(key);
32
+ _cache.set(key, val);
33
+ return val;
34
+ }
35
+
36
+ function set(element, descriptor) {
37
+ var key = fingerprint(element);
38
+ if (_cache.size >= MAX) {
39
+ // evict oldest entry
40
+ _cache.delete(_cache.keys().next().value);
41
+ }
42
+ _cache.set(key, descriptor);
43
+ }
44
+
45
+ function clear() { _cache.clear(); }
46
+
47
+ module.exports = { get, set, clear, fingerprint };
package/src/context.js ADDED
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * context.js — V2
5
+ *
6
+ * SkeletonProvider lets you set default animation, theme, and options
7
+ * globally so every <AutoSkeleton> in the tree inherits them without
8
+ * passing props repeatedly.
9
+ */
10
+
11
+ var React = require('react');
12
+
13
+ var DEFAULT_CTX = {
14
+ animation: 'shimmer',
15
+ theme: 'default',
16
+ themeOverrides: {},
17
+ fadeIn: true,
18
+ maxDepth: 8,
19
+ detectRepeats: true,
20
+ };
21
+
22
+ var SkeletonContext = React.createContext(DEFAULT_CTX);
23
+
24
+ function SkeletonProvider(props) {
25
+ var value = Object.assign({}, DEFAULT_CTX, {
26
+ animation: props.animation || DEFAULT_CTX.animation,
27
+ theme: props.theme || DEFAULT_CTX.theme,
28
+ themeOverrides: props.themeOverrides || DEFAULT_CTX.themeOverrides,
29
+ fadeIn: props.fadeIn != null ? props.fadeIn : DEFAULT_CTX.fadeIn,
30
+ maxDepth: props.maxDepth != null ? props.maxDepth : DEFAULT_CTX.maxDepth,
31
+ detectRepeats: props.detectRepeats != null ? props.detectRepeats : DEFAULT_CTX.detectRepeats,
32
+ });
33
+ return React.createElement(SkeletonContext.Provider, { value: value }, props.children);
34
+ }
35
+
36
+ SkeletonProvider.displayName = 'SkeletonProvider';
37
+
38
+ module.exports = { SkeletonContext, SkeletonProvider, DEFAULT_CTX };