pome-ui 2.0.0-preview50 → 2.0.0-preview52

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/pome-ui.dev.js CHANGED
@@ -18196,6 +18196,274 @@ function build(options, exports) {
18196
18196
  document.head.appendChild(style);
18197
18197
  }
18198
18198
 
18199
+ function _ensureSkeletonStyle() {
18200
+ // pome-skeleton.css must be referenced by the host page manually.
18201
+ // This function is kept as a hook point for future use.
18202
+ }
18203
+
18204
+ // Registry: kebab-name → pre-rendered skeleton HTML string
18205
+ var _componentSkeletonRegistry = {};
18206
+
18207
+ function _toKebabCase(name) {
18208
+ // ScalableSidebar → scalable-sidebar, menuBar → menu-bar
18209
+ return name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
18210
+ }
18211
+
18212
+ function _registerComponentSkeleton(name, opt, rawHtml, skeletonFileHtml) {
18213
+ // skeletonFileHtml: content of <component>.skeleton.html (takes top priority)
18214
+ // opt.skeleton === string: inline HTML declared in the JS
18215
+ // opt.skeleton === true: auto-generate from rawHtml
18216
+ var hasFile = !!skeletonFileHtml;
18217
+ var hasInline = opt && typeof opt.skeleton === 'string';
18218
+ var hasAuto = opt && opt.skeleton === true;
18219
+ if (!hasFile && !hasInline && !hasAuto) return;
18220
+ var kebab = _toKebabCase(name);
18221
+ var skHtml = hasFile ? skeletonFileHtml
18222
+ : hasInline ? opt.skeleton
18223
+ : _generateSkeletonHtml(rawHtml || (opt && opt.template) || '');
18224
+ _componentSkeletonRegistry[kebab] = skHtml;
18225
+ }
18226
+
18227
+ // --- Extract a plain numeric value from a bound attribute like :default-width="230" ---
18228
+ function _extractBoundNumber(el, attr) {
18229
+ var val = el.getAttribute(':' + attr);
18230
+ if (!val) return null;
18231
+ var m = val.match(/^(\d+(?:\.\d+)?)/);
18232
+ return m ? m[1] : null;
18233
+ }
18234
+
18235
+ // --- Known HTML tags for skeleton (custom components will be unwrapped) ---
18236
+ var _knownHtmlTags = null;
18237
+ function _isKnownHtmlTag(tag) {
18238
+ if (!_knownHtmlTags) {
18239
+ _knownHtmlTags = {};
18240
+ 'div,span,p,a,h1,h2,h3,h4,h5,h6,ul,ol,li,table,thead,tbody,tfoot,tr,td,th,colgroup,col,img,video,canvas,iframe,svg,form,input,textarea,select,option,button,label,section,article,aside,header,footer,nav,main,figure,figcaption,blockquote,pre,code,em,strong,i,b,u,small,sub,sup,br,hr,dl,dt,dd,details,summary,fieldset,legend,caption,abbr,cite,mark,time,address'.split(',').forEach(function(t) { _knownHtmlTags[t] = true; });
18241
+ }
18242
+ return !!_knownHtmlTags[tag];
18243
+ }
18244
+
18245
+ function _generateSkeletonHtml(templateHtml) {
18246
+ if (!templateHtml) return '';
18247
+ var html = templateHtml;
18248
+ // Remove Vue mustache expressions – replace with non-breaking space
18249
+ html = html.replace(/\{\{[\s\S]*?\}\}/g, '\u00A0');
18250
+ // Parse as DOM
18251
+ var doc = new DOMParser().parseFromString(
18252
+ '<div id="_pome-sk-wrap">' + html + '</div>', 'text/html'
18253
+ );
18254
+ var root = doc.getElementById('_pome-sk-wrap');
18255
+ if (!root) return '';
18256
+ // Pass 1 – structural: unwrap <template> slots, replace custom elements,
18257
+ // clone v-for rows, remove v-else branches
18258
+ _skeletonStructurePass(root, doc);
18259
+ // Pass 2 – cleanup: strip Vue directives, replace text with skeleton blocks
18260
+ _skeletonCleanupPass(root);
18261
+ return root.innerHTML;
18262
+ }
18263
+
18264
+ function _wrapTemplateWithSkeleton(opt) {
18265
+ if (!opt.skeleton || !opt.template) return;
18266
+ _ensureSkeletonStyle();
18267
+ var skeletonHtml = typeof opt.skeleton === 'string'
18268
+ ? opt.skeleton
18269
+ : _generateSkeletonHtml(opt.template);
18270
+
18271
+ var inner = '<template v-if="!pomeSkeletonLoading">' + opt.template
18272
+ + '</template><div v-if="pomeSkeletonLoading" class="_pome-skeleton">'
18273
+ + skeletonHtml + '</div>';
18274
+
18275
+ if (opt.delayOpen) {
18276
+ // Wrap everything in a single div with reactive delayOpen classes
18277
+ // so the delayOpen animation applies to both skeleton and real content.
18278
+ opt.template = '<div :class="{\'_pome-ui-opened\': pomeDelayOpened, \'_pome-ui-opening\': pomeDelayOpening}">' + inner + '</div>';
18279
+ } else {
18280
+ opt.template = inner;
18281
+ }
18282
+ }
18283
+
18284
+ // --- Pass 1: structural transformations ---
18285
+ function _skeletonStructurePass(node, doc) {
18286
+ if (!node || node.nodeType !== 1) return;
18287
+ var i = 0;
18288
+ while (i < node.childNodes.length) {
18289
+ var child = node.childNodes[i];
18290
+ if (child.nodeType !== 1) { i++; continue; }
18291
+ var tag = child.tagName.toLowerCase();
18292
+
18293
+ // 1. Handle <template> tags (Vue slots)
18294
+ if (tag === 'template') {
18295
+ // Determine slot name from #xxx or v-slot:xxx
18296
+ var slotName = null;
18297
+ for (var a = 0; a < child.attributes.length; a++) {
18298
+ var attr = child.attributes[a].name;
18299
+ if (attr[0] === '#') { slotName = attr.substring(1); break; }
18300
+ if (attr.indexOf('v-slot:') === 0) { slotName = attr.substring(7); break; }
18301
+ }
18302
+ // Only keep the default slot (or unnamed templates); discard others
18303
+ if (slotName && slotName !== 'default') {
18304
+ node.removeChild(child);
18305
+ continue;
18306
+ }
18307
+ // Unwrap template content into parent
18308
+ if (child.content) {
18309
+ var tplNodes = Array.from(child.content.childNodes);
18310
+ for (var j = 0; j < tplNodes.length; j++) {
18311
+ node.insertBefore(doc.importNode(tplNodes[j], true), child);
18312
+ }
18313
+ }
18314
+ node.removeChild(child);
18315
+ continue; // re-examine index (new children inserted)
18316
+ }
18317
+
18318
+ // 2. Remove v-else / v-else-if branches entirely
18319
+ if (child.getAttribute('v-else') !== null || child.getAttribute('v-else-if') !== null) {
18320
+ node.removeChild(child);
18321
+ continue;
18322
+ }
18323
+
18324
+ // 3. Handle v-for – clone element to simulate repeated rows
18325
+ if (child.getAttribute('v-for')) {
18326
+ child.removeAttribute('v-for');
18327
+ var cloneCount = (tag === 'tr') ? 4 : (tag === 'li') ? 3 : 2;
18328
+ var refNode = child.nextSibling;
18329
+ for (var j = 0; j < cloneCount; j++) {
18330
+ var clone = child.cloneNode(true);
18331
+ node.insertBefore(clone, refNode);
18332
+ }
18333
+ // fall through to continue processing this element
18334
+ }
18335
+
18336
+ // 4. Replace custom/unknown elements with <div>, keep children
18337
+ if (!_isKnownHtmlTag(tag)) {
18338
+ // Check if this component has declared its own skeleton
18339
+ var registeredSkeleton = _componentSkeletonRegistry[tag];
18340
+ if (registeredSkeleton) {
18341
+ // Parse the registered skeleton HTML and replace the element with it
18342
+ var skWrap = doc.createElement('div');
18343
+ skWrap.innerHTML = registeredSkeleton;
18344
+ if (child.getAttribute('class')) skWrap.setAttribute('class', child.getAttribute('class'));
18345
+ var existingStyleReg = child.getAttribute('style') || '';
18346
+ var styleExtraReg = '';
18347
+ var defWReg = _extractBoundNumber(child, 'default-width') || child.getAttribute('default-width');
18348
+ var minWReg = _extractBoundNumber(child, 'min-width') || child.getAttribute('min-width');
18349
+ if (defWReg) styleExtraReg += ';width:' + defWReg + 'px;flex-shrink:0';
18350
+ if (minWReg) styleExtraReg += ';min-width:' + minWReg + 'px';
18351
+ var combinedStyleReg = (existingStyleReg + styleExtraReg).replace(/^;+/, '');
18352
+ if (combinedStyleReg) skWrap.setAttribute('style', combinedStyleReg);
18353
+ // Replace the custom element with a wrapper containing the skeleton.
18354
+ // Use a fragment-like approach: move skWrap's children directly
18355
+ // so we don't add an extra div when the registered HTML already has one.
18356
+ var skNodes = Array.from(skWrap.childNodes);
18357
+ if (skNodes.length === 1 && skNodes[0].nodeType === 1) {
18358
+ var skRoot = skNodes[0];
18359
+ // Merge class / style from host element onto the single root node
18360
+ if (child.getAttribute('class')) skRoot.classList && skRoot.setAttribute('class', (skRoot.getAttribute('class') || '') + ' ' + child.getAttribute('class'));
18361
+ if (combinedStyleReg) skRoot.setAttribute('style', (skRoot.getAttribute('style') ? skRoot.getAttribute('style') + ';' : '') + combinedStyleReg);
18362
+ node.replaceChild(skRoot, child);
18363
+ i++;
18364
+ } else {
18365
+ while (skWrap.firstChild) node.insertBefore(skWrap.firstChild, child);
18366
+ node.removeChild(child);
18367
+ // i stays the same since we removed child and inserted nodes before it
18368
+ }
18369
+ continue;
18370
+ }
18371
+
18372
+ var div = doc.createElement('div');
18373
+ if (child.getAttribute('class')) div.setAttribute('class', child.getAttribute('class'));
18374
+
18375
+ // Reconstruct inline style, preserving explicit style + any bound sizing attrs
18376
+ var existingStyle = child.getAttribute('style') || '';
18377
+ var styleExtra = '';
18378
+ // :default-width / default-width → width + flex-shrink:0 so sidebar-like
18379
+ // components keep their width in the skeleton
18380
+ var defW = _extractBoundNumber(child, 'default-width') || child.getAttribute('default-width');
18381
+ var minW = _extractBoundNumber(child, 'min-width') || child.getAttribute('min-width');
18382
+ var maxW = _extractBoundNumber(child, 'max-width') || child.getAttribute('max-width');
18383
+ if (defW) styleExtra += ';width:' + defW + 'px;flex-shrink:0';
18384
+ if (minW) styleExtra += ';min-width:' + minW + 'px';
18385
+ if (maxW) styleExtra += ';max-width:' + maxW + 'px';
18386
+ var combinedStyle = (existingStyle + styleExtra).replace(/^;+/, '');
18387
+ if (combinedStyle) div.setAttribute('style', combinedStyle);
18388
+
18389
+ while (child.firstChild) div.appendChild(child.firstChild);
18390
+ node.replaceChild(div, child);
18391
+ _skeletonStructurePass(div, doc); // recurse into replacement
18392
+ i++;
18393
+ continue;
18394
+ }
18395
+
18396
+ // 5. Recurse into known elements
18397
+ _skeletonStructurePass(child, doc);
18398
+ i++;
18399
+ }
18400
+ }
18401
+
18402
+ // --- Pass 2: cleanup directives, replace text, handle media ---
18403
+ function _skeletonCleanupPass(node) {
18404
+ if (!node) return;
18405
+ if (node.nodeType === 1) {
18406
+ // Strip Vue directives / bindings / event listeners
18407
+ var attrsToRemove = [];
18408
+ for (var i = 0; i < node.attributes.length; i++) {
18409
+ var attrName = node.attributes[i].name;
18410
+ if (attrName.indexOf('v-') === 0 || attrName[0] === '@' || attrName[0] === ':' || attrName[0] === '#') {
18411
+ attrsToRemove.push(attrName);
18412
+ }
18413
+ }
18414
+ for (var i = 0; i < attrsToRemove.length; i++) {
18415
+ node.removeAttribute(attrsToRemove[i]);
18416
+ }
18417
+ // Remove delayOpen classes
18418
+ node.classList.remove('_pome-ui-opened');
18419
+ node.classList.remove('_pome-ui-opening');
18420
+ // Add shimmer class to leaf-level text containers
18421
+ var tag = node.tagName.toLowerCase();
18422
+ var shimmerTags = {span:1,a:1,p:1,h1:1,h2:1,h3:1,h4:1,h5:1,h6:1,li:1,label:1,button:1,input:1,textarea:1,select:1,dt:1,dd:1,figcaption:1,blockquote:1};
18423
+ if (shimmerTags[tag]) {
18424
+ node.classList.add('_pome-sk-shimmer');
18425
+ }
18426
+ if (tag === 'img') {
18427
+ // Replace img with a shimmer skeleton block that preserves dimensions
18428
+ var imgBlock = node.ownerDocument.createElement('span');
18429
+ imgBlock.className = '_pome-sk-img';
18430
+ // Preserve sizing from class or explicit attributes
18431
+ if (node.getAttribute('class')) imgBlock.className += ' ' + node.getAttribute('class');
18432
+ if (node.getAttribute('width')) imgBlock.style.width = node.getAttribute('width') + (/\d$/.test(node.getAttribute('width')) ? 'px' : '');
18433
+ else imgBlock.style.width = '24px';
18434
+ if (node.getAttribute('height')) imgBlock.style.height = node.getAttribute('height') + (/\d$/.test(node.getAttribute('height')) ? 'px' : '');
18435
+ else imgBlock.style.height = '24px';
18436
+ node.parentNode.replaceChild(imgBlock, node);
18437
+ return; // node replaced, skip children
18438
+ }
18439
+ if (tag === 'video' || tag === 'canvas' || tag === 'iframe') {
18440
+ node.removeAttribute('src');
18441
+ node.removeAttribute('srcset');
18442
+ node.style.backgroundColor = '#e8e8e8';
18443
+ node.style.borderRadius = '4px';
18444
+ }
18445
+ }
18446
+ var children = Array.from(node.childNodes);
18447
+ for (var i = 0; i < children.length; i++) {
18448
+ var child = children[i];
18449
+ if (child.nodeType === 3) { // text node
18450
+ // \u00A0 comes from mustache replacement – treat it as content
18451
+ var text = child.textContent.replace(/[\s\u00A0]+/g, '');
18452
+ var hasNbsp = child.textContent.indexOf('\u00A0') >= 0;
18453
+ if (text || hasNbsp) {
18454
+ var parentTag = child.parentNode && child.parentNode.tagName
18455
+ ? child.parentNode.tagName.toLowerCase() : '';
18456
+ var block = child.ownerDocument.createElement('span');
18457
+ block.className = '_pome-sk-block';
18458
+ block.innerHTML = '\u00A0';
18459
+ child.parentNode.replaceChild(block, child);
18460
+ }
18461
+ } else if (child.nodeType === 1) {
18462
+ _skeletonCleanupPass(child);
18463
+ }
18464
+ }
18465
+ }
18466
+
18199
18467
  function _httpCached(url) {
18200
18468
  return !!_cache[url];
18201
18469
  }
@@ -18238,6 +18506,24 @@ function build(options, exports) {
18238
18506
  });
18239
18507
  };
18240
18508
 
18509
+ // Fetch a .skeleton.html file, returning null when the file does not
18510
+ // physically exist. Unlike _httpGet, this guards against SPA catch-all
18511
+ // routes that return the app entry page (status 200) for any URL:
18512
+ // if the response body starts with "<!DOCTYPE" or "<html" it is treated
18513
+ // as a missing file and null is returned.
18514
+ function _httpGetSkeletonFile(url) {
18515
+ return _httpGet(url).then(function (text) {
18516
+ if (!text) return null;
18517
+ var trimmed = text.replace(/^[\s\uFEFF]+/, ''); // strip BOM / whitespace
18518
+ if (/^<!DOCTYPE/i.test(trimmed) || /^<html/i.test(trimmed)) {
18519
+ return null; // SPA fallback page, not a real skeleton file
18520
+ }
18521
+ return text;
18522
+ }).catch(function () {
18523
+ return null;
18524
+ });
18525
+ };
18526
+
18241
18527
  function _serializeOptionsToUrl(options) {
18242
18528
  var fields = Object.getOwnPropertyNames(options);
18243
18529
  var str = '';
@@ -18458,19 +18744,27 @@ function build(options, exports) {
18458
18744
  });
18459
18745
  })
18460
18746
  .then(function (component) {
18461
- var promise = null;
18747
+ var htmlPromise;
18462
18748
  if (mobile) {
18463
- promise = _httpExist(url + '.m.html').then(function (res) {
18749
+ htmlPromise = _httpExist(url + '.m.html').then(function (res) {
18464
18750
  return _httpGet(url + (res ? '.m.html' : '.html'));
18465
18751
  });
18466
18752
  } else {
18467
- promise = _httpGet(url + '.html');
18753
+ htmlPromise = _httpGet(url + '.html');
18468
18754
  }
18755
+ // Load .skeleton.html in parallel; null if not found
18756
+ var skeletonFilePromise = _httpGetSkeletonFile(url + '.skeleton.html');
18469
18757
 
18470
- return promise.then(function (template) {
18758
+ return Promise.all([htmlPromise, skeletonFilePromise]).then(function (results) {
18759
+ var template = results[0];
18760
+ var skeletonFileHtml = results[1];
18471
18761
  if (!component.template) {
18472
18762
  component.template = _sanitizeTemplate(template);
18473
18763
  }
18764
+ // .skeleton.html file takes top priority over skeleton: true / inline string
18765
+ if (skeletonFileHtml && component.skeleton) {
18766
+ component.skeleton = skeletonFileHtml;
18767
+ }
18474
18768
  if (component.delayOpen) {
18475
18769
  var dom = new DOMParser().parseFromString(component.template, 'text/html');
18476
18770
  var children = dom.querySelector('body').children;
@@ -18481,14 +18775,22 @@ function build(options, exports) {
18481
18775
  if (children[i].getAttribute('v-if') != null || children[i].getAttribute('v-else-if') || children[i].getAttribute('v-else')) {
18482
18776
  continue;
18483
18777
  }
18484
- children[i].classList.add('_pome-ui-opened');
18485
- children[i].classList.add('_pome-ui-opening');
18778
+ if (component.skeleton) {
18779
+ // Skeleton mode: delayOpen classes will be added by
18780
+ // _wrapTemplateWithSkeleton on an outer wrapper div.
18781
+ // No need to add them to individual children here.
18782
+ } else {
18783
+ children[i].classList.add('_pome-ui-opened');
18784
+ children[i].classList.add('_pome-ui-opening');
18785
+ }
18486
18786
  }
18487
18787
 
18488
18788
  var ret = template.indexOf('<html') >= 0 ? new XMLSerializer().serializeToString(dom) : dom.querySelector('body').innerHTML;
18489
18789
  component.template = ret;
18490
18790
  }
18491
18791
  _patchComponent(url, component);
18792
+ // _wrapTemplateWithSkeleton is deferred to after _loadComponents
18793
+ // so the component skeleton registry is fully populated.
18492
18794
  return Promise.resolve(component);
18493
18795
  });
18494
18796
  })
@@ -18522,6 +18824,8 @@ function build(options, exports) {
18522
18824
 
18523
18825
  return Promise.all([pageCssPromise, _loadComponents(components, url)]).then(function (results) {
18524
18826
  var components = results[1];
18827
+ // Wrap skeleton template now that component registry is fully populated.
18828
+ _wrapTemplateWithSkeleton(component);
18525
18829
  var ret = Vue.createApp(component);
18526
18830
 
18527
18831
  for (var i = 0; i < components.length; ++i) {
@@ -18751,6 +19055,48 @@ function build(options, exports) {
18751
19055
 
18752
19056
  return _loadComponents(options.components || [], layout)
18753
19057
  .then(function (components) {
19058
+ // Inject layout skeleton overlay now that component registry is populated.
19059
+ if (options._pomeLayoutSkeleton) {
19060
+ var ls = options._pomeLayoutSkeleton;
19061
+ var skHtml = ls.skeletonOverride
19062
+ ? ls.skeletonOverride
19063
+ : (function () {
19064
+ var html = _generateSkeletonHtml(ls.skeletonBody);
19065
+ // Post-process: remove empty top-level nodes (modals etc.),
19066
+ // set flex:1 on the main content area next to the sidebar.
19067
+ var tmp = document.createElement('div');
19068
+ tmp.innerHTML = html;
19069
+ var topChildren = Array.from(tmp.children);
19070
+ var sidebarIdx = -1;
19071
+ for (var ci = 0; ci < topChildren.length; ci++) {
19072
+ var ch = topChildren[ci];
19073
+ if (!ch.innerHTML.trim()) {
19074
+ tmp.removeChild(ch);
19075
+ topChildren = Array.from(tmp.children);
19076
+ ci--;
19077
+ continue;
19078
+ }
19079
+ if (sidebarIdx < 0 && /\bwidth\s*:/.test(ch.getAttribute('style') || '')) {
19080
+ sidebarIdx = ci;
19081
+ } else if (sidebarIdx >= 0 && ci === sidebarIdx + 1) {
19082
+ ch.style.flex = '1';
19083
+ ch.style.minWidth = '0';
19084
+ ch.style.overflow = 'hidden';
19085
+ }
19086
+ }
19087
+ return tmp.innerHTML;
19088
+ })();
19089
+ var appEl = document.querySelector(ls.appId);
19090
+ if (appEl) {
19091
+ var skDiv = document.createElement('div');
19092
+ skDiv.setAttribute('v-if', 'pomeSkeletonLoading');
19093
+ skDiv.className = '_pome-skeleton _pome-layout-skeleton ' + ls.bodyClass;
19094
+ if (ls.overlayStyle) skDiv.setAttribute('style', ls.overlayStyle);
19095
+ skDiv.innerHTML = skHtml;
19096
+ appEl.insertBefore(skDiv, appEl.firstChild);
19097
+ }
19098
+ delete options._pomeLayoutSkeleton;
19099
+ }
18754
19100
  var app = Vue.createApp(options || {});
18755
19101
  for (var i = 0; i < components.length; ++i) {
18756
19102
  var com = components[i];
@@ -19229,24 +19575,51 @@ function build(options, exports) {
19229
19575
  options.created = function () { };
19230
19576
  }
19231
19577
 
19232
- if (options.style) {
19578
+ if (options.style || options.skeleton) {
19233
19579
  var originalCreated = options.created;
19234
19580
  options.created = function () {
19235
- if (!_css[view]) {
19236
- _css[view] = 0;
19237
- }
19238
- if (_css[view] == 0) {
19239
- if (_options.preloadCss) {
19240
- this._cssLoadPromise = appendCssReferenceAsync(view, options.style);
19241
- } else {
19242
- appendCssReference(view, options.style);
19581
+ if (options.style) {
19582
+ if (!_css[view]) {
19583
+ _css[view] = 0;
19243
19584
  }
19585
+ if (_css[view] == 0) {
19586
+ if (_options.preloadCss) {
19587
+ this._cssLoadPromise = appendCssReferenceAsync(view, options.style);
19588
+ } else {
19589
+ appendCssReference(view, options.style);
19590
+ }
19591
+ }
19592
+ ++_css[view];
19244
19593
  }
19245
- ++_css[view];
19594
+
19246
19595
  this.createdPromise = Promise.resolve();
19247
19596
  var ret = originalCreated.call(this);
19248
19597
  if (ret instanceof Promise) {
19249
19598
  this.createdPromise = ret;
19599
+ if (options.skeleton) {
19600
+ var self = this;
19601
+ // For skeleton + delayOpen: start the delayOpen animation
19602
+ // IMMEDIATELY so the skeleton fades in right away.
19603
+ if (options.delayOpen) {
19604
+ self.$nextTick(function () {
19605
+ requestAnimationFrame(function () {
19606
+ requestAnimationFrame(function () {
19607
+ self.pomeDelayOpening = false;
19608
+ });
19609
+ });
19610
+ setTimeout(function () {
19611
+ self.pomeDelayOpened = false;
19612
+ }, options.delayOpen);
19613
+ });
19614
+ }
19615
+ ret.then(function () {
19616
+ self.pomeSkeletonLoading = false;
19617
+ }).catch(function (err) {
19618
+ console.error('[SKELETON] created Promise REJECTED:', err);
19619
+ });
19620
+ }
19621
+ } else if (options.skeleton) {
19622
+ this.pomeSkeletonLoading = false;
19250
19623
  }
19251
19624
 
19252
19625
  return ret;
@@ -19268,11 +19641,12 @@ function build(options, exports) {
19268
19641
  }
19269
19642
 
19270
19643
  // Remove opening class if needed
19271
- if (options.delayOpen) {
19644
+ if (options.delayOpen && !options.skeleton) {
19272
19645
  var self = this;
19273
- (self.createdPromise || Promise.resolve()).then(function () {
19646
+ var delayOpenFunc = function () {
19274
19647
  requestAnimationFrame(function () {
19275
19648
  requestAnimationFrame(function () {
19649
+ if (!self.$el || !self.$el.parentNode) return;
19276
19650
  var doms = self.$el.parentNode.querySelectorAll('._pome-ui-opening');
19277
19651
  for (var i = 0; i < doms.length; ++i) {
19278
19652
  doms[i].classList.remove('_pome-ui-opening');
@@ -19281,11 +19655,16 @@ function build(options, exports) {
19281
19655
  });
19282
19656
 
19283
19657
  setTimeout(function () {
19658
+ if (!self.$el || !self.$el.parentNode) return;
19284
19659
  var doms = self.$el.parentNode.querySelectorAll('._pome-ui-opened');
19285
19660
  for (var i = 0; i < doms.length; ++i) {
19286
19661
  doms[i].classList.remove('_pome-ui-opened');
19287
19662
  }
19288
19663
  }, options.delayOpen);
19664
+ };
19665
+
19666
+ (self.createdPromise || Promise.resolve()).then(function () {
19667
+ delayOpenFunc();
19289
19668
  });
19290
19669
  }
19291
19670
 
@@ -19294,14 +19673,16 @@ function build(options, exports) {
19294
19673
 
19295
19674
  var originalUnmounted = options.unmounted;
19296
19675
  options.unmounted = function () {
19297
- if (!_css[view]) {
19298
- return;
19299
- }
19676
+ if (options.style) {
19677
+ if (!_css[view]) {
19678
+ return originalUnmounted.call(this);
19679
+ }
19300
19680
 
19301
- --_css[view];
19302
- if (_css[view] <= 0) {
19303
- removeCssReference(view);
19304
- delete _css[view];
19681
+ --_css[view];
19682
+ if (_css[view] <= 0) {
19683
+ removeCssReference(view);
19684
+ delete _css[view];
19685
+ }
19305
19686
  }
19306
19687
  return originalUnmounted.call(this);
19307
19688
  };
@@ -19377,9 +19758,17 @@ function build(options, exports) {
19377
19758
  if (layout) {
19378
19759
  var layoutName = layout + (mobile ? '.m' : '');
19379
19760
  var _layoutHtml;
19380
- return _httpGet(layoutName + '.html').then(function (layoutHtml) {
19381
- _layoutHtml = layoutHtml;
19382
- return _httpGet(layout + ".js");
19761
+ var _layoutSkeletonFileHtml; // hoisted so LayoutNext closure can access it
19762
+ // Load layout .html, .js and optional .skeleton.html all in parallel
19763
+ return Promise.all([
19764
+ _httpGet(layoutName + '.html'),
19765
+ _httpGet(layout + '.js'),
19766
+ _httpGetSkeletonFile(layoutName + '.skeleton.html')
19767
+ ]).then(function (layoutResults) {
19768
+ _layoutHtml = layoutResults[0];
19769
+ var js = layoutResults[1];
19770
+ _layoutSkeletonFileHtml = layoutResults[2]; // null when absent
19771
+ return js;
19383
19772
  }).then(function (js) {
19384
19773
  var _opt = null;
19385
19774
  var Layout = function (options) {
@@ -19403,6 +19792,37 @@ function build(options, exports) {
19403
19792
  var appId = 'pomelo-' + ticks;
19404
19793
  var containerId = 'container-' + ticks;
19405
19794
  var body = document.querySelector('body').innerHTML.replace('<render-body></render-body>', '<div id="' + containerId + '"></div>');
19795
+ // Layout skeleton: defer generation to Root→_loadComponents so the
19796
+ // component skeleton registry is fully populated before we generate HTML.
19797
+ if (options.skeleton) {
19798
+ _ensureSkeletonStyle();
19799
+ var bodyEl = document.querySelector('body');
19800
+ var bodyClass = bodyEl.getAttribute('class') || '';
19801
+ var bodyInlineStyle = bodyEl.getAttribute('style') || '';
19802
+ var bodyComputed = window.getComputedStyle(bodyEl);
19803
+ var overlayStyle = bodyInlineStyle;
19804
+ if (bodyComputed.display && bodyComputed.display.indexOf('flex') >= 0) {
19805
+ if (overlayStyle) overlayStyle += ';';
19806
+ overlayStyle += 'display:' + bodyComputed.display
19807
+ + ';flex-direction:' + bodyComputed.flexDirection
19808
+ + ';align-items:' + bodyComputed.alignItems;
19809
+ } else {
19810
+ if (overlayStyle) overlayStyle += ';';
19811
+ overlayStyle += 'display:flex;flex-direction:row;align-items:stretch';
19812
+ }
19813
+ var skeletonBody = body.replace('<div id="' + containerId + '"></div>', '<div style="flex:1;min-width:0"></div>');
19814
+ // Store pending data; Root will inject the overlay div after
19815
+ // _loadComponents so registered component skeletons are available.
19816
+ options._pomeLayoutSkeleton = {
19817
+ appId: '#' + appId,
19818
+ bodyClass: bodyClass,
19819
+ overlayStyle: overlayStyle,
19820
+ skeletonBody: skeletonBody,
19821
+ // Priority: .skeleton.html file > inline string in JS
19822
+ skeletonOverride: _layoutSkeletonFileHtml
19823
+ || (typeof options.skeleton === 'string' ? options.skeleton : null)
19824
+ };
19825
+ }
19406
19826
  _ensureVueCloakStyle();
19407
19827
  document.querySelector('body').innerHTML = '<div id="' + appId + '" class="_pome-vue-cloak">' + body + '</div>';
19408
19828
  options.template = null;
@@ -19576,10 +19996,15 @@ function build(options, exports) {
19576
19996
  var _html;
19577
19997
  var _name;
19578
19998
  var _opt;
19579
- return _httpGet(c + ".html").then(function (comHtml) {
19580
- _html = comHtml;
19581
- return _httpGet(c + ".js");
19582
- }).then(function (comJs) {
19999
+ // Load .html, .js and optional .skeleton.html in parallel
20000
+ return Promise.all([
20001
+ _httpGet(c + '.html'),
20002
+ _httpGet(c + '.js'),
20003
+ _httpGetSkeletonFile(c + '.skeleton.html')
20004
+ ]).then(function (results) {
20005
+ _html = results[0];
20006
+ var comJs = results[1];
20007
+ var skeletonFileHtml = results[2]; // null when file does not exist
19583
20008
  var Component = function (name, options) {
19584
20009
  _opt = options;
19585
20010
  _name = name;
@@ -19593,6 +20018,10 @@ function build(options, exports) {
19593
20018
  }
19594
20019
  _hookSetup(_opt);
19595
20020
  _patchComponent(c, _opt);
20021
+ // Register the component's skeleton BEFORE wrapping the template,
20022
+ // so _generateSkeletonHtml still sees the clean original template.
20023
+ _registerComponentSkeleton(_name, _opt, _opt.template, skeletonFileHtml);
20024
+ _wrapTemplateWithSkeleton(_opt);
19596
20025
  _hookData(_opt);
19597
20026
  var cssPreloadPromise = Promise.resolve();
19598
20027
  if (_options.preloadCss && _opt.style) {
@@ -19700,6 +20129,13 @@ function build(options, exports) {
19700
20129
  opt.data = function (app) {
19701
20130
  var data = func1.call(this, app);
19702
20131
  var data2 = { pomeUiSubTitles: [] };
20132
+ if (opt.skeleton) {
20133
+ data2.pomeSkeletonLoading = true;
20134
+ }
20135
+ if (opt.skeleton && opt.delayOpen) {
20136
+ data2.pomeDelayOpened = true;
20137
+ data2.pomeDelayOpening = true;
20138
+ }
19703
20139
  _combineObject(data2, data);
19704
20140
  return data;
19705
20141
  }