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.
@@ -0,0 +1,293 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * primitives.js — V2
5
+ *
6
+ * Standalone skeleton building blocks for manual composition.
7
+ * All respect the global SkeletonContext for animation / theme.
8
+ *
9
+ * Exports:
10
+ * SkeletonBlock — generic configurable rectangle
11
+ * SkeletonText — multi-line text placeholder
12
+ * SkeletonAvatar — circular avatar
13
+ * SkeletonImage — rectangular image placeholder
14
+ * SkeletonBadge — inline pill badge
15
+ * SkeletonButton — button placeholder
16
+ * SkeletonInput — input field placeholder
17
+ * SkeletonList — repeated item skeleton
18
+ * SkeletonForm — form with label+input rows
19
+ * SkeletonCard — card with header row + body lines + optional footer
20
+ */
21
+
22
+ var React = require('react');
23
+ var styles = require('./styles');
24
+ var ctx = require('./context');
25
+
26
+ function useAnim(propAnim) {
27
+ var context = React.useContext(ctx.SkeletonContext);
28
+ React.useEffect(function() { styles.injectStyles(); }, []);
29
+ if (typeof document !== 'undefined') styles.injectStyles();
30
+ return propAnim || context.animation || 'shimmer';
31
+ }
32
+
33
+ /* ── SkeletonBlock ── */
34
+ function SkeletonBlock(props) {
35
+ var anim = useAnim(props.animation);
36
+ var shape = props.shape; // 'circle' | 'pill' | 'rounded' | undefined
37
+ return React.createElement('span', {
38
+ 'data-ask': '',
39
+ className: styles.blockClass(anim, shape) + (props.className ? ' ' + props.className : ''),
40
+ style: Object.assign({
41
+ width: props.width || '100%',
42
+ height: props.height || '16px',
43
+ display: 'block',
44
+ }, props.style || {}),
45
+ 'aria-hidden': 'true',
46
+ });
47
+ }
48
+ SkeletonBlock.displayName = 'SkeletonBlock';
49
+
50
+ /* ── SkeletonText ── */
51
+ function SkeletonText(props) {
52
+ var anim = useAnim(props.animation);
53
+ var lines = props.lines || 3;
54
+ var lineH = props.lineHeight || '1em';
55
+ var gap = props.gap || '7px';
56
+ var lastWidth = props.lastLineWidth || '60%';
57
+ var lineEls = [];
58
+ for (var i = 0; i < lines; i++) {
59
+ lineEls.push(React.createElement('span', {
60
+ key: 'tl-' + i,
61
+ 'data-ask': '',
62
+ className: styles.blockClass(anim),
63
+ style: {
64
+ display: 'block',
65
+ width: i === lines - 1 ? lastWidth : (i === 0 ? '95%' : '100%'),
66
+ height: lineH,
67
+ marginBottom: i < lines - 1 ? gap : 0,
68
+ },
69
+ 'aria-hidden': 'true',
70
+ }));
71
+ }
72
+ return React.createElement('div', { 'aria-hidden': 'true', style: props.style || {} }, lineEls);
73
+ }
74
+ SkeletonText.displayName = 'SkeletonText';
75
+
76
+ /* ── SkeletonAvatar ── */
77
+ function SkeletonAvatar(props) {
78
+ var anim = useAnim(props.animation);
79
+ var size = props.size || '40px';
80
+ return React.createElement('span', {
81
+ 'data-ask': '',
82
+ className: styles.blockClass(anim, 'circle'),
83
+ style: Object.assign({ width: size, height: size, display: 'block', flexShrink: 0 }, props.style || {}),
84
+ 'aria-hidden': 'true',
85
+ });
86
+ }
87
+ SkeletonAvatar.displayName = 'SkeletonAvatar';
88
+
89
+ /* ── SkeletonImage ── */
90
+ function SkeletonImage(props) {
91
+ var anim = useAnim(props.animation);
92
+ return React.createElement('span', {
93
+ 'data-ask': '',
94
+ className: styles.blockClass(anim, 'rounded'),
95
+ style: Object.assign({ width: '100%', height: props.height || '200px', display: 'block' }, props.style || {}),
96
+ 'aria-hidden': 'true',
97
+ });
98
+ }
99
+ SkeletonImage.displayName = 'SkeletonImage';
100
+
101
+ /* ── SkeletonBadge ── */
102
+ function SkeletonBadge(props) {
103
+ var anim = useAnim(props.animation);
104
+ return React.createElement('span', {
105
+ 'data-ask': '',
106
+ className: styles.blockClass(anim, 'pill'),
107
+ style: Object.assign({ width: props.width || '60px', height: '20px', display: 'inline-block' }, props.style || {}),
108
+ 'aria-hidden': 'true',
109
+ });
110
+ }
111
+ SkeletonBadge.displayName = 'SkeletonBadge';
112
+
113
+ /* ── SkeletonButton ── */
114
+ function SkeletonButton(props) {
115
+ var anim = useAnim(props.animation);
116
+ return React.createElement('span', {
117
+ 'data-ask': '',
118
+ className: styles.blockClass(anim, 'pill'),
119
+ style: Object.assign({ width: props.width || '100px', height: props.height || '36px', display: 'block' }, props.style || {}),
120
+ 'aria-hidden': 'true',
121
+ });
122
+ }
123
+ SkeletonButton.displayName = 'SkeletonButton';
124
+
125
+ /* ── SkeletonInput ── */
126
+ function SkeletonInput(props) {
127
+ var anim = useAnim(props.animation);
128
+ return React.createElement('span', {
129
+ 'data-ask': '',
130
+ className: styles.blockClass(anim, 'rounded'),
131
+ style: Object.assign({ width: '100%', height: props.height || '38px', display: 'block' }, props.style || {}),
132
+ 'aria-hidden': 'true',
133
+ });
134
+ }
135
+ SkeletonInput.displayName = 'SkeletonInput';
136
+
137
+ /* ── SkeletonList ── */
138
+ function SkeletonList(props) {
139
+ var anim = useAnim(props.animation);
140
+ var count = props.count || 3;
141
+ var gap = props.gap || '12px';
142
+ var rowH = props.rowHeight || '60px';
143
+ var items = [];
144
+ for (var i = 0; i < count; i++) {
145
+ items.push(
146
+ props.renderItem
147
+ ? React.createElement('div', { key: i }, props.renderItem({ animation: anim, index: i }))
148
+ : React.createElement('span', {
149
+ key: i,
150
+ 'data-ask': '',
151
+ className: styles.blockClass(anim, 'rounded'),
152
+ style: { width: '100%', height: rowH, display: 'block', marginBottom: i < count - 1 ? gap : 0 },
153
+ 'aria-hidden': 'true',
154
+ })
155
+ );
156
+ }
157
+ return React.createElement('div', { role: 'status', 'aria-label': 'Loading list', 'aria-busy': 'true' }, items);
158
+ }
159
+ SkeletonList.displayName = 'SkeletonList';
160
+
161
+ /* ── SkeletonForm ── */
162
+ function SkeletonForm(props) {
163
+ var anim = useAnim(props.animation);
164
+ var fields = props.fields || 3;
165
+ var rows = [];
166
+ for (var i = 0; i < fields; i++) {
167
+ rows.push(React.createElement('div', { key: i, style: { marginBottom: '16px' } },
168
+ // label line
169
+ React.createElement('span', {
170
+ 'data-ask': '',
171
+ className: styles.blockClass(anim),
172
+ style: { width: '120px', height: '0.8em', display: 'block', marginBottom: '6px' },
173
+ 'aria-hidden': 'true',
174
+ }),
175
+ // input box
176
+ React.createElement('span', {
177
+ 'data-ask': '',
178
+ className: styles.blockClass(anim, 'rounded'),
179
+ style: { width: '100%', height: '38px', display: 'block' },
180
+ 'aria-hidden': 'true',
181
+ })
182
+ ));
183
+ }
184
+ // Submit button
185
+ rows.push(React.createElement('span', {
186
+ key: 'submit',
187
+ 'data-ask': '',
188
+ className: styles.blockClass(anim, 'pill'),
189
+ style: { width: '120px', height: '38px', display: 'block', marginTop: '8px' },
190
+ 'aria-hidden': 'true',
191
+ }));
192
+ return React.createElement('div', { role: 'status', 'aria-busy': 'true', 'aria-label': 'Loading form' }, rows);
193
+ }
194
+ SkeletonForm.displayName = 'SkeletonForm';
195
+
196
+ /* ── SkeletonCard ── */
197
+ function SkeletonCard(props) {
198
+ var anim = useAnim(props.animation);
199
+ var showImg = props.image !== false;
200
+ var showAvatar = props.avatar !== false;
201
+ var lines = props.lines || 3;
202
+ var showFooter = props.footer !== false;
203
+ var imgH = props.imageHeight || '180px';
204
+
205
+ var parts = [];
206
+ if (showImg) parts.push(
207
+ React.createElement('span', {
208
+ key: 'img',
209
+ 'data-ask': '',
210
+ className: styles.blockClass(anim, 'rounded'),
211
+ style: { width: '100%', height: imgH, display: 'block', marginBottom: '12px' },
212
+ 'aria-hidden': 'true',
213
+ })
214
+ );
215
+
216
+ // header row: avatar + two text lines
217
+ parts.push(React.createElement('div', {
218
+ key: 'hdr',
219
+ style: { display: 'flex', gap: '10px', alignItems: 'center', marginBottom: '12px' },
220
+ },
221
+ showAvatar && React.createElement('span', {
222
+ 'data-ask': '',
223
+ className: styles.blockClass(anim, 'circle'),
224
+ style: { width: '36px', height: '36px', flexShrink: 0 },
225
+ 'aria-hidden': 'true',
226
+ }),
227
+ React.createElement('div', { style: { flex: 1 } },
228
+ React.createElement('span', {
229
+ 'data-ask': '',
230
+ className: styles.blockClass(anim),
231
+ style: { width: '55%', height: '0.9em', display: 'block', marginBottom: '5px' },
232
+ 'aria-hidden': 'true',
233
+ }),
234
+ React.createElement('span', {
235
+ 'data-ask': '',
236
+ className: styles.blockClass(anim),
237
+ style: { width: '35%', height: '0.75em', display: 'block' },
238
+ 'aria-hidden': 'true',
239
+ })
240
+ )
241
+ ));
242
+
243
+ // body text
244
+ for (var i = 0; i < lines; i++) {
245
+ parts.push(React.createElement('span', {
246
+ key: 'line-' + i,
247
+ 'data-ask': '',
248
+ className: styles.blockClass(anim),
249
+ style: {
250
+ width: i === lines - 1 ? '65%' : '100%',
251
+ height: '0.85em',
252
+ display: 'block',
253
+ marginBottom: i < lines - 1 ? '6px' : '12px',
254
+ },
255
+ 'aria-hidden': 'true',
256
+ }));
257
+ }
258
+
259
+ if (showFooter) {
260
+ parts.push(React.createElement('div', {
261
+ key: 'footer',
262
+ style: { display: 'flex', gap: '8px' },
263
+ },
264
+ React.createElement('span', {
265
+ 'data-ask': '', className: styles.blockClass(anim, 'pill'),
266
+ style: { width: '60px', height: '24px' }, 'aria-hidden': 'true',
267
+ }),
268
+ React.createElement('span', {
269
+ 'data-ask': '', className: styles.blockClass(anim, 'pill'),
270
+ style: { width: '60px', height: '24px' }, 'aria-hidden': 'true',
271
+ })
272
+ ));
273
+ }
274
+
275
+ return React.createElement('div', {
276
+ role: 'status', 'aria-busy': 'true', 'aria-label': 'Loading card',
277
+ style: props.style || {},
278
+ }, parts);
279
+ }
280
+ SkeletonCard.displayName = 'SkeletonCard';
281
+
282
+ module.exports = {
283
+ SkeletonBlock,
284
+ SkeletonText,
285
+ SkeletonAvatar,
286
+ SkeletonImage,
287
+ SkeletonBadge,
288
+ SkeletonButton,
289
+ SkeletonInput,
290
+ SkeletonList,
291
+ SkeletonForm,
292
+ SkeletonCard,
293
+ };
package/src/renderer.js CHANGED
@@ -1,84 +1,259 @@
1
- const React = require('react');
2
- const { ELEMENT_TYPES } = require('./analyzer');
3
- const { blockClass } = require('./styles');
4
-
5
- const DEFAULTS = {
6
- text: { width: '80%', height: '1em' },
7
- heading: { width: '60%', height: '1.4em' },
8
- image: { width: '100%', height: '200px' },
9
- avatar: { width: '40px', height: '40px' },
10
- button: { width: '100px',height: '36px' },
11
- input: { width: '100%', height: '38px' },
12
- icon: { width: '24px', height: '24px' },
13
- };
14
-
15
- let _key = 0;
16
- function nk() { return 'ask-' + (++_key); }
17
-
18
- function bStyle(def, hints, extra) {
19
- hints = hints || {};
20
- extra = extra || {};
21
- return Object.assign({ width: hints.width || def.width, height: hints.height || def.height, display: 'block', margin: hints.margin || '0' }, extra);
22
- }
23
-
24
- function renderNode(node, options) {
25
- if (!node) return null;
26
- options = options || {};
27
- var animation = options.animation || 'shimmer';
28
- var nodeType = node.nodeType;
29
- var hints = node.styleHints || {};
30
-
31
- if (nodeType === ELEMENT_TYPES.TEXT) {
32
- var isHeading = node.isHeading;
33
- var def = isHeading ? DEFAULTS.heading : DEFAULTS.text;
34
- var len = (node.content || '').length;
35
- var lines = Math.min(Math.max(1, Math.round(len / 40)), 4);
36
- if (lines <= 1) {
37
- return React.createElement('span', { key: nk(), className: blockClass(animation), style: bStyle(def, hints, { width: hints.width || (len > 60 ? '95%' : def.width) }), 'aria-hidden': 'true' });
38
- }
39
- var lineEls = [];
40
- for (var i = 0; i < lines; i++) {
41
- lineEls.push(React.createElement('span', { key: nk(), className: blockClass(animation), style: { display: 'block', width: i === lines-1 ? '65%' : '100%', height: def.height, marginBottom: '6px' }, 'aria-hidden': 'true' }));
42
- }
43
- return React.createElement('div', { key: nk() }, lineEls);
44
- }
45
-
46
- if (nodeType === ELEMENT_TYPES.IMAGE) {
47
- return React.createElement('span', { key: nk(), className: blockClass(animation, 'rounded'), style: bStyle(DEFAULTS.image, hints), 'aria-hidden': 'true' });
48
- }
49
-
50
- if (nodeType === ELEMENT_TYPES.AVATAR) {
51
- return React.createElement('span', { key: nk(), className: blockClass(animation, 'circle'), style: bStyle(DEFAULTS.avatar, hints), 'aria-hidden': 'true' });
52
- }
53
-
54
- if (nodeType === ELEMENT_TYPES.ICON) {
55
- return React.createElement('span', { key: nk(), className: blockClass(animation, 'circle'), style: bStyle(DEFAULTS.icon, hints), 'aria-hidden': 'true' });
56
- }
57
-
58
- if (nodeType === ELEMENT_TYPES.BUTTON) {
59
- return React.createElement('span', { key: nk(), className: blockClass(animation, 'pill'), style: bStyle(DEFAULTS.button, hints), 'aria-hidden': 'true' });
60
- }
61
-
62
- if (nodeType === ELEMENT_TYPES.INPUT) {
63
- return React.createElement('span', { key: nk(), className: blockClass(animation, 'rounded'), style: bStyle(DEFAULTS.input, hints), 'aria-hidden': 'true' });
64
- }
65
-
66
- if (nodeType === ELEMENT_TYPES.CONTAINER) {
67
- var children = (node.children || []).map(function(c) { return renderNode(c, options); }).filter(Boolean);
68
- var tag = node.tag || 'div';
69
- var cStyle = {};
70
- if (hints.display) cStyle.display = hints.display;
71
- if (hints.flexDirection) cStyle.flexDirection = hints.flexDirection;
72
- if (hints.gap) cStyle.gap = hints.gap;
73
- if (hints.padding) cStyle.padding = hints.padding;
74
- if (hints.margin) cStyle.margin = hints.margin;
75
- if (children.length === 0) {
76
- return React.createElement('span', { key: nk(), className: blockClass(animation), style: bStyle({ width: '100%', height: '20px' }, hints), 'aria-hidden': 'true' });
77
- }
78
- return React.createElement(tag, { key: nk(), style: cStyle, className: 'ask-wrapper' }, children);
79
- }
80
-
81
- return null;
82
- }
83
-
84
- module.exports = { renderNode };
1
+ 'use strict';
2
+
3
+ /**
4
+ * renderer.js — V2
5
+ *
6
+ * Improvements over V1:
7
+ * - Handles REPEAT node type (renders N identical skeleton children)
8
+ * - Handles HEADING with level-aware sizing
9
+ * - Handles MEDIA (video/iframe) as wide rectangle
10
+ * - Handles BADGE as inline pill
11
+ * - Handles TEXTAREA as tall input
12
+ * - Handles SELECT as input with chevron indicator
13
+ * - Handles CARD node type with appropriate padding wrapper
14
+ * - Smart sizing: prefers inline style hints, falls back to type defaults
15
+ * - Aspect ratio support from style hints
16
+ * - Passes aria-busy + aria-label to all wrappers for accessibility
17
+ * - Minimal key counter reset per render call for SSR safety
18
+ */
19
+
20
+ var React = require('react');
21
+ var N = require('./analyzer').NODE;
22
+ var styles = require('./styles');
23
+
24
+ /* ── size defaults per node type ── */
25
+ var DEFAULTS = {
26
+ [N.TEXT]: { width: '80%', height: '1em' },
27
+ [N.HEADING]: { width: '55%', height: '1.4em' },
28
+ [N.IMAGE]: { width: '100%', height: '200px' },
29
+ [N.AVATAR]: { width: '40px', height: '40px' },
30
+ [N.MEDIA]: { width: '100%', height: '220px' },
31
+ [N.BUTTON]: { width: '100px', height: '36px' },
32
+ [N.INPUT]: { width: '100%', height: '38px' },
33
+ [N.TEXTAREA]: { width: '100%', height: '100px' },
34
+ [N.SELECT]: { width: '100%', height: '38px' },
35
+ [N.BADGE]: { width: '60px', height: '20px' },
36
+ [N.ICON]: { width: '24px', height: '24px' },
37
+ };
38
+
39
+ /* heading level → size */
40
+ var HEADING_SIZES = ['1.8em','1.5em','1.3em','1.15em','1em','0.9em'];
41
+
42
+ /* ── key factory ── */
43
+ var _k = 0;
44
+ function nk() { return 'sk2-' + (++_k); }
45
+ function resetKeys() { _k = 0; }
46
+
47
+ /* ── style builder ── */
48
+ function sz(defaults, hints, extra) {
49
+ hints = hints || {};
50
+ extra = extra || {};
51
+ var s = {
52
+ width: hints.width || defaults.width,
53
+ height: hints.height || defaults.height,
54
+ display: 'block',
55
+ };
56
+ if (hints.aspectRatio) { delete s.height; s.aspectRatio = hints.aspectRatio; }
57
+ if (hints.margin) s.margin = hints.margin;
58
+ if (hints.borderRadius) s.borderRadius = hints.borderRadius;
59
+ return Object.assign(s, extra);
60
+ }
61
+
62
+ /* ── multi-line text skeleton ── */
63
+ function textLines(node, anim, extra) {
64
+ var content = node.content || '';
65
+ var len = content.length;
66
+ var lines = Math.min(Math.max(1, Math.ceil(len / 45)), 5);
67
+ var h = (node.styleHints && node.styleHints.height) || (node.styleHints && node.styleHints.fontSize) || '1em';
68
+
69
+ if (lines <= 1) {
70
+ return React.createElement('span', {
71
+ key: nk(),
72
+ className: styles.blockClass(anim),
73
+ style: Object.assign(sz(DEFAULTS[N.TEXT], node.styleHints, extra), { width: node.width || '80%' }),
74
+ 'aria-hidden': 'true',
75
+ });
76
+ }
77
+
78
+ var lineEls = [];
79
+ for (var i = 0; i < lines; i++) {
80
+ lineEls.push(React.createElement('span', {
81
+ key: nk(),
82
+ className: styles.blockClass(anim),
83
+ style: {
84
+ display: 'block',
85
+ width: i === lines - 1 ? '60%' : (i === 0 ? '95%' : '100%'),
86
+ height: h,
87
+ marginBottom: i < lines - 1 ? '6px' : 0,
88
+ },
89
+ 'aria-hidden': 'true',
90
+ }));
91
+ }
92
+ return React.createElement('div', { key: nk(), 'aria-hidden': 'true' }, lineEls);
93
+ }
94
+
95
+ /* ── main render function ── */
96
+ function renderNode(node, options) {
97
+ if (!node) return null;
98
+ options = options || {};
99
+ var anim = options.animation || 'shimmer';
100
+ var hints = node.styleHints || {};
101
+ var nt = node.nodeType;
102
+
103
+ /* TEXT */
104
+ if (nt === N.TEXT) return textLines(node, anim, {});
105
+
106
+ /* HEADING */
107
+ if (nt === N.HEADING) {
108
+ var lvl = node.level || 2;
109
+ var hStyle = sz(DEFAULTS[N.HEADING], hints, {
110
+ width: node.width || hints.width || (lvl === 1 ? '70%' : lvl <= 3 ? '55%' : '45%'),
111
+ height: hints.fontSize || HEADING_SIZES[Math.min(lvl - 1, 5)],
112
+ });
113
+ return React.createElement('span', {
114
+ key: nk(),
115
+ className: styles.blockClass(anim),
116
+ style: hStyle,
117
+ 'aria-hidden': 'true',
118
+ });
119
+ }
120
+
121
+ /* IMAGE */
122
+ if (nt === N.IMAGE) {
123
+ return React.createElement('span', {
124
+ key: nk(),
125
+ className: styles.blockClass(anim, 'rounded'),
126
+ style: sz(DEFAULTS[N.IMAGE], hints),
127
+ 'aria-hidden': 'true',
128
+ });
129
+ }
130
+
131
+ /* AVATAR */
132
+ if (nt === N.AVATAR) {
133
+ var avSize = hints.width || hints.height || '40px';
134
+ return React.createElement('span', {
135
+ key: nk(),
136
+ className: styles.blockClass(anim, 'circle'),
137
+ style: { width: avSize, height: avSize, display: 'block', flexShrink: 0 },
138
+ 'aria-hidden': 'true',
139
+ });
140
+ }
141
+
142
+ /* MEDIA (video / iframe) */
143
+ if (nt === N.MEDIA) {
144
+ return React.createElement('span', {
145
+ key: nk(),
146
+ className: styles.blockClass(anim, 'rounded'),
147
+ style: sz(DEFAULTS[N.MEDIA], hints),
148
+ 'aria-hidden': 'true',
149
+ });
150
+ }
151
+
152
+ /* BUTTON */
153
+ if (nt === N.BUTTON) {
154
+ var btnW = hints.width || (node.label ? Math.max(80, node.label.length * 9) + 'px' : '100px');
155
+ return React.createElement('span', {
156
+ key: nk(),
157
+ className: styles.blockClass(anim, 'pill'),
158
+ style: sz(DEFAULTS[N.BUTTON], hints, { width: btnW }),
159
+ 'aria-hidden': 'true',
160
+ });
161
+ }
162
+
163
+ /* INPUT */
164
+ if (nt === N.INPUT) {
165
+ return React.createElement('span', {
166
+ key: nk(),
167
+ className: styles.blockClass(anim, 'rounded'),
168
+ style: sz(DEFAULTS[N.INPUT], hints),
169
+ 'aria-hidden': 'true',
170
+ });
171
+ }
172
+
173
+ /* TEXTAREA */
174
+ if (nt === N.TEXTAREA) {
175
+ return React.createElement('span', {
176
+ key: nk(),
177
+ className: styles.blockClass(anim, 'rounded'),
178
+ style: sz(DEFAULTS[N.TEXTAREA], hints),
179
+ 'aria-hidden': 'true',
180
+ });
181
+ }
182
+
183
+ /* SELECT */
184
+ if (nt === N.SELECT) {
185
+ return React.createElement('span', {
186
+ key: nk(),
187
+ className: styles.blockClass(anim, 'rounded'),
188
+ style: sz(DEFAULTS[N.SELECT], hints),
189
+ 'aria-hidden': 'true',
190
+ });
191
+ }
192
+
193
+ /* BADGE */
194
+ if (nt === N.BADGE) {
195
+ return React.createElement('span', {
196
+ key: nk(),
197
+ className: styles.blockClass(anim, 'pill'),
198
+ style: sz(DEFAULTS[N.BADGE], hints),
199
+ 'aria-hidden': 'true',
200
+ });
201
+ }
202
+
203
+ /* ICON */
204
+ if (nt === N.ICON) {
205
+ var iconSz = hints.width || hints.height || hints.fontSize || '24px';
206
+ return React.createElement('span', {
207
+ key: nk(),
208
+ className: styles.blockClass(anim, 'circle'),
209
+ style: { width: iconSz, height: iconSz, display: 'block', flexShrink: 0 },
210
+ 'aria-hidden': 'true',
211
+ });
212
+ }
213
+
214
+ /* REPEAT — detected list pattern */
215
+ if (nt === N.REPEAT) {
216
+ var items = [];
217
+ for (var ri = 0; ri < node.count; ri++) {
218
+ items.push(React.createElement('div', { key: nk() }, renderNode(node.template, options)));
219
+ }
220
+ var repStyle = {};
221
+ if (hints.display) repStyle.display = hints.display;
222
+ if (hints.gap) repStyle.gap = hints.gap;
223
+ return React.createElement('div', { key: nk(), className: 'ask-wrap', style: repStyle }, items);
224
+ }
225
+
226
+ /* CARD */
227
+ if (nt === N.CARD || nt === N.CONTAINER) {
228
+ var kids = (node.children || []).map(function(c) { return renderNode(c, options); }).filter(Boolean);
229
+ var t = node.tag || 'div';
230
+
231
+ // Empty container → show a single block
232
+ if (kids.length === 0) {
233
+ return React.createElement('span', {
234
+ key: nk(),
235
+ className: styles.blockClass(anim),
236
+ style: sz({ width: '100%', height: '24px' }, hints),
237
+ 'aria-hidden': 'true',
238
+ });
239
+ }
240
+
241
+ var cStyle = {};
242
+ if (hints.display) cStyle.display = hints.display;
243
+ if (hints.flexDir) cStyle.flexDirection = hints.flexDir;
244
+ if (hints.gap) cStyle.gap = hints.gap;
245
+ if (hints.padding) cStyle.padding = hints.padding;
246
+ if (hints.margin) cStyle.margin = hints.margin;
247
+ if (hints.alignItems) cStyle.alignItems = hints.alignItems;
248
+
249
+ return React.createElement(t, {
250
+ key: nk(),
251
+ className: 'ask-wrap',
252
+ style: cStyle,
253
+ }, kids);
254
+ }
255
+
256
+ return null;
257
+ }
258
+
259
+ module.exports = { renderNode, resetKeys };