ng-hub-ui-skeleton 22.2.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,754 @@
1
+ import { DOCUMENT, isPlatformBrowser, NgTemplateOutlet } from '@angular/common';
2
+ import * as i0 from '@angular/core';
3
+ import { InjectionToken, makeEnvironmentProviders, inject, computed, Injectable, input, PLATFORM_ID, signal, ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
4
+
5
+ /**
6
+ * Bundled preset catalogue used by the skeleton library.
7
+ */
8
+ const HUB_SKELETON_DEFAULT_PRESETS = [
9
+ {
10
+ name: 'card',
11
+ description: 'Generic content card with media, title and body lines.',
12
+ template: 'stack(gap:16)>block(height:{{mediaHeight}},radius:{{radius}})+line(height:{{titleHeight}},width:{{titleWidth}})+stack(gap:{{bodyGap}})>line(height:{{lineHeight}},width:100%)*{{rows}}+line(height:{{lineHeight}},width:{{lastLineWidth}})',
13
+ defaults: {
14
+ mediaHeight: 180,
15
+ radius: 18,
16
+ titleHeight: 18,
17
+ titleWidth: '56%',
18
+ bodyGap: 10,
19
+ lineHeight: 12,
20
+ rows: 2,
21
+ lastLineWidth: '76%'
22
+ },
23
+ variants: {
24
+ compact: {
25
+ defaults: {
26
+ mediaHeight: 120,
27
+ radius: 14,
28
+ titleHeight: 16,
29
+ bodyGap: 8,
30
+ lineHeight: 10,
31
+ rows: 1,
32
+ lastLineWidth: '68%'
33
+ }
34
+ }
35
+ }
36
+ },
37
+ {
38
+ name: 'list-item',
39
+ description: 'Avatar row with title and subtitle lines.',
40
+ template: 'stack(direction:row,gap:12,align:center)>circle(size:{{avatarSize}})+stack(gap:{{gap}},grow:1)>line(height:{{titleHeight}},width:{{titleWidth}})+line(height:{{lineHeight}},width:{{lineWidth}})',
41
+ defaults: {
42
+ avatarSize: 44,
43
+ gap: 8,
44
+ titleHeight: 14,
45
+ titleWidth: '42%',
46
+ lineHeight: 10,
47
+ lineWidth: '68%'
48
+ },
49
+ variants: {
50
+ compact: {
51
+ defaults: {
52
+ avatarSize: 36,
53
+ gap: 6,
54
+ titleHeight: 12,
55
+ lineHeight: 8
56
+ }
57
+ }
58
+ }
59
+ },
60
+ {
61
+ name: 'table-row',
62
+ description: 'Grid row that adapts to the configured column count.',
63
+ template: 'grid(columns:{{columns}},gap:12|md=16)>line(height:{{lineHeight}},width:{{cellWidth}})*{{columns}}',
64
+ defaults: {
65
+ columns: 4,
66
+ lineHeight: 12,
67
+ cellWidth: '88%'
68
+ }
69
+ },
70
+ {
71
+ name: 'detail-view',
72
+ description: 'Two-column detail layout with hero block and metadata rows.',
73
+ template: 'grid(columns:1|lg=2,gap:20)>block(height:220|lg=280,radius:20)+stack(gap:14)>line(height:20,width:48%)+line(height:12,width:78%)+line(height:12,width:92%)+line(height:12,width:70%)+grid(columns:2,gap:12)>block(height:64)*4'
74
+ },
75
+ {
76
+ name: 'form-section',
77
+ description: 'Grouped form controls with header and responsive fields.',
78
+ template: 'stack(gap:18)>line(height:18,width:32%)+grid(columns:1|md=2,gap:14)>stack(gap:8)>line(height:10,width:28%)+block(height:44,radius:12)*{{fields}}',
79
+ defaults: {
80
+ fields: 4
81
+ }
82
+ },
83
+ {
84
+ name: 'dashboard-widget',
85
+ description: 'Panel header with metric rows and footer actions.',
86
+ template: 'stack(gap:16)>stack(direction:row,gap:12,justify:space-between,align:center)>line(height:16,width:36%)+circle(size:20)+block(height:132,radius:18)+grid(columns:2,gap:12)>line(height:12,width:72%)*4'
87
+ },
88
+ {
89
+ name: 'stat-card',
90
+ description: 'Metric summary card with title, large value and trend.',
91
+ template: 'stack(gap:14)>line(height:12,width:34%)+line(height:34,width:44%)+line(height:10,width:28%)+block(height:44,radius:14)'
92
+ },
93
+ {
94
+ name: 'chart-panel',
95
+ description: 'Chart block with legend rows.',
96
+ template: 'stack(gap:16)>stack(direction:row,gap:12,justify:space-between,align:center)>line(height:16,width:40%)+line(height:10,width:18%)+block(height:220,radius:18)+grid(columns:2,gap:10)>stack(direction:row,gap:8,align:center)>circle(size:10)+line(height:10,width:60%)*4'
97
+ },
98
+ {
99
+ name: 'profile-summary',
100
+ description: 'Profile card with avatar, identity and summary metrics.',
101
+ template: 'stack(gap:16,align:center)>circle(size:88)+line(height:18,width:46%)+line(height:12,width:62%)+grid(columns:3,gap:12)>block(height:56,radius:14)*3'
102
+ },
103
+ {
104
+ name: 'master-detail',
105
+ description: 'List panel and detail panel skeleton pair.',
106
+ template: 'grid(columns:1|lg=3,gap:20)>stack(gap:12)>list-item*4+stack(gap:16)>block(height:220,radius:18)+line(height:18,width:42%)+line(height:12,width:92%)*3+line(height:12,width:70%)'
107
+ },
108
+ {
109
+ name: 'kanban-card',
110
+ description: 'Compact board card with title, metadata and footer chips.',
111
+ template: 'stack(gap:12)>line(height:14,width:58%)+line(height:10,width:80%)+stack(direction:row,gap:8)>block(height:24,width:76,radius:999)+block(height:24,width:92,radius:999)'
112
+ },
113
+ {
114
+ name: 'feed-item',
115
+ description: 'Activity feed row with avatar and content snippets.',
116
+ template: 'stack(direction:row,gap:12,align:flex-start)>circle(size:40)+stack(gap:8,grow:1)>line(height:12,width:34%)+line(height:12,width:94%)+line(height:12,width:74%)'
117
+ },
118
+ {
119
+ name: 'search-result',
120
+ description: 'Search result card with title, metadata and excerpt.',
121
+ template: 'stack(gap:12)>line(height:18,width:56%)+stack(direction:row,gap:10)>line(height:10,width:20%)+line(height:10,width:18%)+line(height:10,width:24%)+line(height:12,width:92%)*2+line(height:12,width:76%)'
122
+ },
123
+ {
124
+ name: 'table-toolbar',
125
+ description: 'Toolbar with title, filters and actions.',
126
+ template: 'grid(columns:1|md=2,gap:16,align:center)>line(height:18,width:38%)+stack(direction:row,gap:10,justify:flex-end)>block(height:38,width:180,radius:12)+block(height:38,width:110,radius:12)+block(height:38,width:92,radius:12)'
127
+ },
128
+ {
129
+ name: 'filter-bar',
130
+ description: 'Row of filter controls with mobile stacking.',
131
+ template: 'grid(columns:1|md=4,gap:12)>block(height:40,radius:12)*{{filters}}',
132
+ defaults: {
133
+ filters: 4
134
+ }
135
+ },
136
+ {
137
+ name: 'empty-state-skeleton',
138
+ description: 'Illustrative empty-state placeholder with title and actions.',
139
+ template: 'stack(gap:18,align:center)>block(height:160,width:240,radius:28)+line(height:18,width:36%)+line(height:12,width:64%)+line(height:12,width:52%)+stack(direction:row,gap:12)>block(height:40,width:120,radius:999)+block(height:40,width:84,radius:999)'
140
+ }
141
+ ];
142
+
143
+ /** Multi-provider token for consumer-defined skeleton preset groups. */
144
+ const HUB_SKELETON_PRESETS = new InjectionToken('HUB_SKELETON_PRESETS', {
145
+ factory: () => []
146
+ });
147
+ /**
148
+ * Registers one or more custom presets for the active injector tree.
149
+ *
150
+ * @param presets Custom presets to append to the bundled catalogue.
151
+ * @returns Environment providers ready for app-level registration.
152
+ */
153
+ function provideHubSkeletonPresets(presets) {
154
+ return makeEnvironmentProviders([
155
+ {
156
+ provide: HUB_SKELETON_PRESETS,
157
+ multi: true,
158
+ useValue: presets
159
+ }
160
+ ]);
161
+ }
162
+ /**
163
+ * Resolves the full preset catalogue available to the current injector.
164
+ */
165
+ class HubSkeletonPresetRegistryService {
166
+ customPresetGroups = inject(HUB_SKELETON_PRESETS);
167
+ /** All presets merged by name, where later entries override earlier ones. */
168
+ presets = computed(() => {
169
+ const merged = new Map();
170
+ for (const preset of HUB_SKELETON_DEFAULT_PRESETS) {
171
+ merged.set(preset.name, preset);
172
+ }
173
+ for (const presetGroup of this.customPresetGroups) {
174
+ for (const preset of presetGroup) {
175
+ merged.set(preset.name, preset);
176
+ }
177
+ }
178
+ return merged;
179
+ }, /* @ts-ignore */
180
+ ...(ngDevMode ? [{ debugName: "presets" }] : /* istanbul ignore next */ []));
181
+ /**
182
+ * Returns a single preset by name.
183
+ *
184
+ * @param name Preset identifier.
185
+ * @returns Matching preset, when available.
186
+ */
187
+ getPreset(name) {
188
+ return this.presets().get(name);
189
+ }
190
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: HubSkeletonPresetRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
191
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: HubSkeletonPresetRegistryService, providedIn: 'root' });
192
+ }
193
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: HubSkeletonPresetRegistryService, decorators: [{
194
+ type: Injectable,
195
+ args: [{ providedIn: 'root' }]
196
+ }] });
197
+
198
+ const BUILTIN_NODE_TYPES = new Set(['line', 'block', 'circle', 'stack', 'grid']);
199
+ const BREAKPOINT_ORDER = ['base', 'sm', 'md', 'lg', 'xl'];
200
+ const BREAKPOINT_WIDTHS = {
201
+ sm: 576,
202
+ md: 768,
203
+ lg: 992,
204
+ xl: 1280
205
+ };
206
+ /**
207
+ * Lightweight DSL parser for the compact skeleton syntax.
208
+ */
209
+ class HubSkeletonDslParser {
210
+ source;
211
+ index = 0;
212
+ constructor(source) {
213
+ this.source = source;
214
+ }
215
+ /**
216
+ * Parses the full source string into sibling root nodes.
217
+ *
218
+ * @returns Root nodes declared in the DSL.
219
+ */
220
+ parse() {
221
+ const nodes = this.parseSiblings();
222
+ this.skipWhitespace();
223
+ if (this.index < this.source.length) {
224
+ throw new Error(`Unexpected token "${this.source[this.index]}" at position ${this.index}.`);
225
+ }
226
+ return nodes;
227
+ }
228
+ parseSiblings() {
229
+ const nodes = [this.parseNode()];
230
+ while (true) {
231
+ this.skipWhitespace();
232
+ if (this.peek() !== '+') {
233
+ break;
234
+ }
235
+ this.index += 1;
236
+ nodes.push(this.parseNode());
237
+ }
238
+ return nodes;
239
+ }
240
+ parseNode() {
241
+ this.skipWhitespace();
242
+ const name = this.readIdentifier();
243
+ let variant;
244
+ if (this.peek() === '@') {
245
+ this.index += 1;
246
+ variant = this.readIdentifier();
247
+ }
248
+ const props = this.peek() === '(' ? this.readProps() : {};
249
+ let children = [];
250
+ this.skipWhitespace();
251
+ if (this.peek() === '>') {
252
+ this.index += 1;
253
+ children = this.parseSiblings();
254
+ }
255
+ this.skipWhitespace();
256
+ let repeat = 1;
257
+ if (this.peek() === '*') {
258
+ this.index += 1;
259
+ const rawRepeat = this.readUntilDelimiter(['+', '>', ')']).trim();
260
+ repeat = Number.parseInt(rawRepeat, 10);
261
+ if (!Number.isFinite(repeat) || repeat < 1) {
262
+ throw new Error(`Invalid repeat value "${rawRepeat}" near position ${this.index}.`);
263
+ }
264
+ }
265
+ return { name, props, variant, children, repeat };
266
+ }
267
+ readProps() {
268
+ const props = {};
269
+ this.expect('(');
270
+ let chunk = '';
271
+ let depth = 1;
272
+ while (this.index < this.source.length && depth > 0) {
273
+ const char = this.source[this.index++];
274
+ if (char === '(') {
275
+ depth += 1;
276
+ chunk += char;
277
+ continue;
278
+ }
279
+ if (char === ')') {
280
+ depth -= 1;
281
+ if (depth === 0) {
282
+ break;
283
+ }
284
+ chunk += char;
285
+ continue;
286
+ }
287
+ chunk += char;
288
+ }
289
+ if (depth !== 0) {
290
+ throw new Error('Unclosed modifier block in skeleton DSL.');
291
+ }
292
+ for (const entry of this.splitTopLevel(chunk, ',')) {
293
+ const trimmedEntry = entry.trim();
294
+ if (!trimmedEntry) {
295
+ continue;
296
+ }
297
+ const [rawKey, ...rawValueParts] = trimmedEntry.split(':');
298
+ if (!rawKey) {
299
+ continue;
300
+ }
301
+ props[rawKey.trim()] = rawValueParts.length > 0 ? rawValueParts.join(':').trim() : 'true';
302
+ }
303
+ return props;
304
+ }
305
+ splitTopLevel(value, separator) {
306
+ const parts = [];
307
+ let current = '';
308
+ let depth = 0;
309
+ for (const char of value) {
310
+ if (char === '(') {
311
+ depth += 1;
312
+ }
313
+ else if (char === ')') {
314
+ depth -= 1;
315
+ }
316
+ if (char === separator && depth === 0) {
317
+ parts.push(current);
318
+ current = '';
319
+ continue;
320
+ }
321
+ current += char;
322
+ }
323
+ if (current) {
324
+ parts.push(current);
325
+ }
326
+ return parts;
327
+ }
328
+ readIdentifier() {
329
+ this.skipWhitespace();
330
+ const start = this.index;
331
+ while (this.index < this.source.length && /[a-zA-Z0-9-]/.test(this.source[this.index])) {
332
+ this.index += 1;
333
+ }
334
+ if (start === this.index) {
335
+ throw new Error(`Expected identifier near position ${this.index}.`);
336
+ }
337
+ return this.source.slice(start, this.index);
338
+ }
339
+ readUntilDelimiter(delimiters) {
340
+ const start = this.index;
341
+ while (this.index < this.source.length && !delimiters.includes(this.source[this.index])) {
342
+ this.index += 1;
343
+ }
344
+ return this.source.slice(start, this.index);
345
+ }
346
+ expect(char) {
347
+ this.skipWhitespace();
348
+ if (this.peek() !== char) {
349
+ throw new Error(`Expected "${char}" near position ${this.index}.`);
350
+ }
351
+ this.index += 1;
352
+ }
353
+ skipWhitespace() {
354
+ while (this.index < this.source.length && /\s/.test(this.source[this.index])) {
355
+ this.index += 1;
356
+ }
357
+ }
358
+ peek() {
359
+ return this.source[this.index];
360
+ }
361
+ }
362
+ /**
363
+ * Parses a DSL string into a raw AST.
364
+ *
365
+ * @param dsl Source template string.
366
+ * @returns Parsed root nodes.
367
+ */
368
+ function parseHubSkeletonDsl(dsl) {
369
+ return new HubSkeletonDslParser(dsl).parse();
370
+ }
371
+ /**
372
+ * Interpolates `{{param}}` placeholders using a serializable params map.
373
+ *
374
+ * @param source Source DSL or modifier value.
375
+ * @param params Runtime params.
376
+ * @returns Interpolated string.
377
+ */
378
+ function interpolateHubSkeletonParams(source, params) {
379
+ return source.replace(/\{\{\s*([a-zA-Z0-9_-]+)\s*\}\}/g, (_, key) => String(params[key] ?? ''));
380
+ }
381
+ /**
382
+ * Resolves the concrete DSL string to use for the current breakpoint.
383
+ *
384
+ * @param template Source template definition.
385
+ * @param breakpoint Current active breakpoint.
386
+ * @returns DSL string to parse.
387
+ */
388
+ function resolveTemplateDsl(template, breakpoint) {
389
+ if (typeof template === 'string') {
390
+ return template;
391
+ }
392
+ if (breakpoint !== 'base' && template.responsive?.[breakpoint]) {
393
+ return template.responsive[breakpoint];
394
+ }
395
+ return template.dsl;
396
+ }
397
+ /**
398
+ * Resolves a primitive or responsive value token to a string for the active breakpoint.
399
+ *
400
+ * @param value Raw modifier value.
401
+ * @param breakpoint Current breakpoint.
402
+ * @returns Breakpoint-resolved string.
403
+ */
404
+ function resolveResponsiveToken(value, breakpoint) {
405
+ const parsed = parseResponsiveToken(value);
406
+ if (typeof parsed !== 'object' || parsed === null) {
407
+ return String(parsed);
408
+ }
409
+ let resolved = parsed.base;
410
+ for (const key of BREAKPOINT_ORDER) {
411
+ if (key === 'base') {
412
+ continue;
413
+ }
414
+ if (BREAKPOINT_ORDER.indexOf(key) <= BREAKPOINT_ORDER.indexOf(breakpoint) && parsed[key] !== undefined) {
415
+ resolved = parsed[key];
416
+ }
417
+ }
418
+ return String(resolved ?? '');
419
+ }
420
+ /**
421
+ * Maps the current viewport width to the nearest supported breakpoint id.
422
+ *
423
+ * @param width Viewport width in pixels.
424
+ * @returns Breakpoint identifier.
425
+ */
426
+ function resolveBreakpointFromWidth(width) {
427
+ if (width >= BREAKPOINT_WIDTHS.xl) {
428
+ return 'xl';
429
+ }
430
+ if (width >= BREAKPOINT_WIDTHS.lg) {
431
+ return 'lg';
432
+ }
433
+ if (width >= BREAKPOINT_WIDTHS.md) {
434
+ return 'md';
435
+ }
436
+ if (width >= BREAKPOINT_WIDTHS.sm) {
437
+ return 'sm';
438
+ }
439
+ return 'base';
440
+ }
441
+ /**
442
+ * Resolves an AST into render nodes, expanding preset aliases recursively.
443
+ *
444
+ * @param nodes AST nodes to resolve.
445
+ * @param options Resolution context.
446
+ * @returns Render nodes ready for the component template.
447
+ */
448
+ function resolveHubSkeletonNodes(nodes, options) {
449
+ const counter = { value: 0 };
450
+ const resolveNode = (node, inheritedParams, inheritedVariant) => {
451
+ const runtimeVariant = node.variant ?? inheritedVariant;
452
+ const runtimeParams = {
453
+ ...inheritedParams,
454
+ ...convertNodePropsToParams(node.props, options.breakpoint)
455
+ };
456
+ if (BUILTIN_NODE_TYPES.has(node.name)) {
457
+ const props = resolveProps(node.props, options.breakpoint);
458
+ const builtNode = {
459
+ id: `hub-skeleton-node-${counter.value++}`,
460
+ type: node.name,
461
+ props,
462
+ children: resolveNodes(node.children, runtimeParams, runtimeVariant),
463
+ isSurface: node.name === 'line' || node.name === 'block' || node.name === 'circle'
464
+ };
465
+ return Array.from({ length: node.repeat }, () => builtNode).map((entry, index) => ({
466
+ ...entry,
467
+ id: `${entry.id}-${index}`
468
+ }));
469
+ }
470
+ const preset = options.registry.getPreset(node.name);
471
+ if (!preset) {
472
+ throw new Error(`Unknown skeleton node or preset alias "${node.name}".`);
473
+ }
474
+ const activeVariant = runtimeVariant ?? options.fallbackVariant;
475
+ const variantDef = activeVariant ? preset.variants?.[activeVariant] : undefined;
476
+ const presetParams = {
477
+ ...preset.defaults,
478
+ ...variantDef?.defaults,
479
+ ...runtimeParams
480
+ };
481
+ const presetTemplate = variantDef?.template ?? preset.template;
482
+ const presetDsl = interpolateHubSkeletonParams(resolveTemplateDsl(presetTemplate, options.breakpoint), presetParams);
483
+ const presetAst = parseHubSkeletonDsl(presetDsl);
484
+ const resolvedPresetNodes = resolveNodes(presetAst, presetParams, activeVariant);
485
+ return Array.from({ length: node.repeat }, () => resolvedPresetNodes).flat();
486
+ };
487
+ const resolveNodes = (inputNodes, params, variant) => inputNodes.flatMap((node) => resolveNode(node, params, variant));
488
+ return resolveNodes(nodes, options.params, options.fallbackVariant);
489
+ }
490
+ function resolveProps(props, breakpoint) {
491
+ return Object.fromEntries(Object.entries(props).map(([key, value]) => [key, resolveResponsiveToken(value, breakpoint)]));
492
+ }
493
+ function convertNodePropsToParams(props, breakpoint) {
494
+ const entries = Object.entries(props).map(([key, value]) => {
495
+ const resolvedValue = resolveResponsiveToken(value, breakpoint);
496
+ return [key, parsePrimitiveToken(resolvedValue)];
497
+ });
498
+ return Object.fromEntries(entries);
499
+ }
500
+ function parseResponsiveToken(value) {
501
+ if (!value.includes('|')) {
502
+ return parsePrimitiveToken(value);
503
+ }
504
+ const responsive = {};
505
+ for (const part of value.split('|')) {
506
+ const trimmedPart = part.trim();
507
+ if (!trimmedPart) {
508
+ continue;
509
+ }
510
+ if (!trimmedPart.includes('=')) {
511
+ responsive.base = parsePrimitiveToken(trimmedPart);
512
+ continue;
513
+ }
514
+ const [breakpoint, rawToken] = trimmedPart.split('=');
515
+ const normalizedBreakpoint = breakpoint.trim();
516
+ if (isBreakpoint(normalizedBreakpoint)) {
517
+ responsive[normalizedBreakpoint] = parsePrimitiveToken(rawToken.trim());
518
+ }
519
+ }
520
+ return responsive;
521
+ }
522
+ function isBreakpoint(value) {
523
+ return BREAKPOINT_ORDER.includes(value);
524
+ }
525
+ function parsePrimitiveToken(value) {
526
+ const trimmedValue = value.trim();
527
+ if (trimmedValue === 'true') {
528
+ return true;
529
+ }
530
+ if (trimmedValue === 'false') {
531
+ return false;
532
+ }
533
+ if (/^-?\d+(\.\d+)?$/.test(trimmedValue)) {
534
+ return Number(trimmedValue);
535
+ }
536
+ return trimmedValue;
537
+ }
538
+
539
+ /**
540
+ * Dynamic skeleton component that renders bundled presets or compact inline DSL templates.
541
+ */
542
+ class HubSkeletonComponent {
543
+ /** Built-in or registered preset name. */
544
+ preset = input(null, /* @ts-ignore */
545
+ ...(ngDevMode ? [{ debugName: "preset" }] : /* istanbul ignore next */ []));
546
+ /** Inline compact DSL template or template definition object. */
547
+ template = input(null, /* @ts-ignore */
548
+ ...(ngDevMode ? [{ debugName: "template" }] : /* istanbul ignore next */ []));
549
+ /** Serializable params interpolated before parsing. */
550
+ params = input({}, /* @ts-ignore */
551
+ ...(ngDevMode ? [{ debugName: "params" }] : /* istanbul ignore next */ []));
552
+ /** Optional named variant. */
553
+ variant = input(null, /* @ts-ignore */
554
+ ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
555
+ /** Toggles shimmer animation. */
556
+ animated = input(true, /* @ts-ignore */
557
+ ...(ngDevMode ? [{ debugName: "animated" }] : /* istanbul ignore next */ []));
558
+ /** Visual appearance preset. */
559
+ appearance = input('default', /* @ts-ignore */
560
+ ...(ngDevMode ? [{ debugName: "appearance" }] : /* istanbul ignore next */ []));
561
+ /** Optional accessible label applied to the container. */
562
+ ariaLabel = input('Loading placeholder', /* @ts-ignore */
563
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
564
+ document = inject(DOCUMENT);
565
+ platformId = inject(PLATFORM_ID);
566
+ registry = inject(HubSkeletonPresetRegistryService);
567
+ resizeCleanup = null;
568
+ viewportWidth = signal(1200, /* @ts-ignore */
569
+ ...(ngDevMode ? [{ debugName: "viewportWidth" }] : /* istanbul ignore next */ []));
570
+ activeBreakpoint = computed(() => resolveBreakpointFromWidth(this.viewportWidth()), /* @ts-ignore */
571
+ ...(ngDevMode ? [{ debugName: "activeBreakpoint" }] : /* istanbul ignore next */ []));
572
+ /** Resolved render nodes after preset expansion and breakpoint evaluation. */
573
+ resolvedNodes = computed(() => {
574
+ const { template, params, variant } = this.resolveRuntimeTemplate();
575
+ const dsl = interpolateHubSkeletonParams(resolveTemplateDsl(template, this.activeBreakpoint()), params);
576
+ const ast = parseHubSkeletonDsl(dsl);
577
+ return resolveHubSkeletonNodes(ast, {
578
+ breakpoint: this.activeBreakpoint(),
579
+ params,
580
+ registry: this.registry,
581
+ fallbackVariant: variant ?? undefined
582
+ });
583
+ }, /* @ts-ignore */
584
+ ...(ngDevMode ? [{ debugName: "resolvedNodes" }] : /* istanbul ignore next */ []));
585
+ ngOnInit() {
586
+ if (!isPlatformBrowser(this.platformId) || !this.document.defaultView) {
587
+ return;
588
+ }
589
+ const win = this.document.defaultView;
590
+ this.viewportWidth.set(win.innerWidth);
591
+ const listener = () => this.viewportWidth.set(win.innerWidth);
592
+ win.addEventListener('resize', listener, { passive: true });
593
+ this.resizeCleanup = () => win.removeEventListener('resize', listener);
594
+ }
595
+ ngOnDestroy() {
596
+ this.resizeCleanup?.();
597
+ }
598
+ /**
599
+ * Converts node props into CSS custom properties consumed by the template.
600
+ *
601
+ * @param node Render node.
602
+ * @returns Style object.
603
+ */
604
+ buildStyles(node) {
605
+ const props = node.props;
606
+ return {
607
+ '--hub-skeleton-node-width': props['width'] ?? null,
608
+ '--hub-skeleton-node-height': this.toCssSize(props['height']),
609
+ '--hub-skeleton-node-size': this.toCssSize(props['size']),
610
+ '--hub-skeleton-node-radius': this.toCssSize(props['radius']),
611
+ '--hub-skeleton-node-gap': this.toCssSize(props['gap']),
612
+ '--hub-skeleton-node-columns': props['columns'] ?? null,
613
+ '--hub-skeleton-node-align': props['align'] ?? null,
614
+ '--hub-skeleton-node-justify': props['justify'] ?? null
615
+ };
616
+ }
617
+ resolveRuntimeTemplate() {
618
+ const presetName = this.preset();
619
+ const variantName = this.variant();
620
+ const inlineTemplate = this.template();
621
+ if (presetName) {
622
+ const preset = this.registry.getPreset(presetName);
623
+ if (!preset) {
624
+ throw new Error(`Unknown skeleton preset "${presetName}".`);
625
+ }
626
+ const activeVariant = variantName ? preset.variants?.[variantName] : undefined;
627
+ return {
628
+ template: activeVariant?.template ?? preset.template,
629
+ params: {
630
+ ...preset.defaults,
631
+ ...activeVariant?.defaults,
632
+ ...this.params()
633
+ },
634
+ variant: variantName
635
+ };
636
+ }
637
+ if (!inlineTemplate) {
638
+ throw new Error('hub-skeleton requires either `preset` or `template`.');
639
+ }
640
+ return {
641
+ template: inlineTemplate,
642
+ params: this.params(),
643
+ variant: variantName
644
+ };
645
+ }
646
+ toCssSize(value) {
647
+ if (!value) {
648
+ return null;
649
+ }
650
+ if (/^-?\d+(\.\d+)?$/.test(value)) {
651
+ return `${value}px`;
652
+ }
653
+ return value;
654
+ }
655
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: HubSkeletonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
656
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: HubSkeletonComponent, isStandalone: true, selector: "hub-skeleton", inputs: { preset: { classPropertyName: "preset", publicName: "preset", isSignal: true, isRequired: false, transformFunction: null }, template: { classPropertyName: "template", publicName: "template", isSignal: true, isRequired: false, transformFunction: null }, params: { classPropertyName: "params", publicName: "params", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, animated: { classPropertyName: "animated", publicName: "animated", isSignal: true, isRequired: false, transformFunction: null }, appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, providers: [HubSkeletonPresetRegistryService], ngImport: i0, template: `
657
+ <div
658
+ class="hub-skeleton"
659
+ [class.hub-skeleton--animated]="animated()"
660
+ [class.hub-skeleton--subtle]="appearance() === 'subtle'"
661
+ [class.hub-skeleton--contrast]="appearance() === 'contrast'"
662
+ [attr.aria-label]="ariaLabel()"
663
+ role="presentation"
664
+ >
665
+ <ng-container *ngTemplateOutlet="renderNodes; context: { $implicit: resolvedNodes() }"></ng-container>
666
+ </div>
667
+
668
+ <ng-template #renderNodes let-nodes>
669
+ @for (node of nodes; track node.id) {
670
+ <div
671
+ class="hub-skeleton__node"
672
+ [class.hub-skeleton__node--surface]="node.isSurface"
673
+ [class.hub-skeleton__node--line]="node.type === 'line'"
674
+ [class.hub-skeleton__node--block]="node.type === 'block'"
675
+ [class.hub-skeleton__node--circle]="node.type === 'circle'"
676
+ [class.hub-skeleton__node--stack]="node.type === 'stack'"
677
+ [class.hub-skeleton__node--grid]="node.type === 'grid'"
678
+ [class.hub-skeleton__node--row]="node.type === 'stack' && node.props['direction'] === 'row'"
679
+ [class.hub-skeleton__node--grow]="node.props['grow'] === 'true'"
680
+ [style]="buildStyles(node)"
681
+ aria-hidden="true"
682
+ >
683
+ @if (node.children.length > 0) {
684
+ <ng-container *ngTemplateOutlet="renderNodes; context: { $implicit: node.children }"></ng-container>
685
+ }
686
+ </div>
687
+ }
688
+ </ng-template>
689
+ `, isInline: true, styles: [":host{display:block}.hub-skeleton{--hub-skeleton-bg: rgba(148, 163, 184, .18);--hub-skeleton-highlight: rgba(255, 255, 255, .52);--hub-skeleton-radius: 12px;--hub-skeleton-gap: 12px;--hub-skeleton-animation-duration: 1.35s;display:block;width:100%}.hub-skeleton--subtle{--hub-skeleton-bg: rgba(148, 163, 184, .12);--hub-skeleton-highlight: rgba(255, 255, 255, .38)}.hub-skeleton--contrast{--hub-skeleton-bg: rgba(71, 85, 105, .28);--hub-skeleton-highlight: rgba(255, 255, 255, .28)}.hub-skeleton__node{box-sizing:border-box;min-width:0}.hub-skeleton__node--surface{background:linear-gradient(90deg,var(--hub-skeleton-bg) 0%,var(--hub-skeleton-bg) 32%,var(--hub-skeleton-highlight) 50%,var(--hub-skeleton-bg) 68%,var(--hub-skeleton-bg) 100%);background-size:220% 100%;border-radius:var(--hub-skeleton-node-radius, var(--hub-skeleton-radius))}.hub-skeleton--animated .hub-skeleton__node--surface{animation:hub-skeleton-shimmer var(--hub-skeleton-animation-duration) ease-in-out infinite}.hub-skeleton__node--line,.hub-skeleton__node--block,.hub-skeleton__node--circle{width:var(--hub-skeleton-node-width, 100%);height:var(--hub-skeleton-node-height, 12px)}.hub-skeleton__node--circle{width:var(--hub-skeleton-node-size, var(--hub-skeleton-node-width, 40px));height:var(--hub-skeleton-node-size, var(--hub-skeleton-node-height, 40px));border-radius:999px}.hub-skeleton__node--stack{display:flex;flex-direction:column;gap:var(--hub-skeleton-node-gap, var(--hub-skeleton-gap));align-items:var(--hub-skeleton-node-align, stretch);justify-content:var(--hub-skeleton-node-justify, flex-start)}.hub-skeleton__node--row{flex-direction:row}.hub-skeleton__node--grid{display:grid;grid-template-columns:repeat(var(--hub-skeleton-node-columns, 2),minmax(0,1fr));gap:var(--hub-skeleton-node-gap, var(--hub-skeleton-gap));align-items:var(--hub-skeleton-node-align, stretch)}.hub-skeleton__node--grow{flex:1 1 auto}@keyframes hub-skeleton-shimmer{0%{background-position:100% 0}to{background-position:-100% 0}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
690
+ }
691
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: HubSkeletonComponent, decorators: [{
692
+ type: Component,
693
+ args: [{ selector: 'hub-skeleton', standalone: true, imports: [NgTemplateOutlet], providers: [HubSkeletonPresetRegistryService], changeDetection: ChangeDetectionStrategy.OnPush, template: `
694
+ <div
695
+ class="hub-skeleton"
696
+ [class.hub-skeleton--animated]="animated()"
697
+ [class.hub-skeleton--subtle]="appearance() === 'subtle'"
698
+ [class.hub-skeleton--contrast]="appearance() === 'contrast'"
699
+ [attr.aria-label]="ariaLabel()"
700
+ role="presentation"
701
+ >
702
+ <ng-container *ngTemplateOutlet="renderNodes; context: { $implicit: resolvedNodes() }"></ng-container>
703
+ </div>
704
+
705
+ <ng-template #renderNodes let-nodes>
706
+ @for (node of nodes; track node.id) {
707
+ <div
708
+ class="hub-skeleton__node"
709
+ [class.hub-skeleton__node--surface]="node.isSurface"
710
+ [class.hub-skeleton__node--line]="node.type === 'line'"
711
+ [class.hub-skeleton__node--block]="node.type === 'block'"
712
+ [class.hub-skeleton__node--circle]="node.type === 'circle'"
713
+ [class.hub-skeleton__node--stack]="node.type === 'stack'"
714
+ [class.hub-skeleton__node--grid]="node.type === 'grid'"
715
+ [class.hub-skeleton__node--row]="node.type === 'stack' && node.props['direction'] === 'row'"
716
+ [class.hub-skeleton__node--grow]="node.props['grow'] === 'true'"
717
+ [style]="buildStyles(node)"
718
+ aria-hidden="true"
719
+ >
720
+ @if (node.children.length > 0) {
721
+ <ng-container *ngTemplateOutlet="renderNodes; context: { $implicit: node.children }"></ng-container>
722
+ }
723
+ </div>
724
+ }
725
+ </ng-template>
726
+ `, styles: [":host{display:block}.hub-skeleton{--hub-skeleton-bg: rgba(148, 163, 184, .18);--hub-skeleton-highlight: rgba(255, 255, 255, .52);--hub-skeleton-radius: 12px;--hub-skeleton-gap: 12px;--hub-skeleton-animation-duration: 1.35s;display:block;width:100%}.hub-skeleton--subtle{--hub-skeleton-bg: rgba(148, 163, 184, .12);--hub-skeleton-highlight: rgba(255, 255, 255, .38)}.hub-skeleton--contrast{--hub-skeleton-bg: rgba(71, 85, 105, .28);--hub-skeleton-highlight: rgba(255, 255, 255, .28)}.hub-skeleton__node{box-sizing:border-box;min-width:0}.hub-skeleton__node--surface{background:linear-gradient(90deg,var(--hub-skeleton-bg) 0%,var(--hub-skeleton-bg) 32%,var(--hub-skeleton-highlight) 50%,var(--hub-skeleton-bg) 68%,var(--hub-skeleton-bg) 100%);background-size:220% 100%;border-radius:var(--hub-skeleton-node-radius, var(--hub-skeleton-radius))}.hub-skeleton--animated .hub-skeleton__node--surface{animation:hub-skeleton-shimmer var(--hub-skeleton-animation-duration) ease-in-out infinite}.hub-skeleton__node--line,.hub-skeleton__node--block,.hub-skeleton__node--circle{width:var(--hub-skeleton-node-width, 100%);height:var(--hub-skeleton-node-height, 12px)}.hub-skeleton__node--circle{width:var(--hub-skeleton-node-size, var(--hub-skeleton-node-width, 40px));height:var(--hub-skeleton-node-size, var(--hub-skeleton-node-height, 40px));border-radius:999px}.hub-skeleton__node--stack{display:flex;flex-direction:column;gap:var(--hub-skeleton-node-gap, var(--hub-skeleton-gap));align-items:var(--hub-skeleton-node-align, stretch);justify-content:var(--hub-skeleton-node-justify, flex-start)}.hub-skeleton__node--row{flex-direction:row}.hub-skeleton__node--grid{display:grid;grid-template-columns:repeat(var(--hub-skeleton-node-columns, 2),minmax(0,1fr));gap:var(--hub-skeleton-node-gap, var(--hub-skeleton-gap));align-items:var(--hub-skeleton-node-align, stretch)}.hub-skeleton__node--grow{flex:1 1 auto}@keyframes hub-skeleton-shimmer{0%{background-position:100% 0}to{background-position:-100% 0}}\n"] }]
727
+ }], propDecorators: { preset: [{ type: i0.Input, args: [{ isSignal: true, alias: "preset", required: false }] }], template: [{ type: i0.Input, args: [{ isSignal: true, alias: "template", required: false }] }], params: [{ type: i0.Input, args: [{ isSignal: true, alias: "params", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], animated: [{ type: i0.Input, args: [{ isSignal: true, alias: "animated", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
728
+
729
+ /**
730
+ * NgModule wrapper kept for compatibility with module-based Angular apps.
731
+ */
732
+ class HubSkeletonModule {
733
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: HubSkeletonModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
734
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: HubSkeletonModule, imports: [HubSkeletonComponent], exports: [HubSkeletonComponent] });
735
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: HubSkeletonModule });
736
+ }
737
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: HubSkeletonModule, decorators: [{
738
+ type: NgModule,
739
+ args: [{
740
+ imports: [HubSkeletonComponent],
741
+ exports: [HubSkeletonComponent]
742
+ }]
743
+ }] });
744
+
745
+ /*
746
+ * Public API Surface of skeleton
747
+ */
748
+
749
+ /**
750
+ * Generated bundle index. Do not edit.
751
+ */
752
+
753
+ export { HUB_SKELETON_DEFAULT_PRESETS, HUB_SKELETON_PRESETS, HubSkeletonComponent, HubSkeletonModule, HubSkeletonPresetRegistryService, provideHubSkeletonPresets };
754
+ //# sourceMappingURL=ng-hub-ui-skeleton.mjs.map