pome-ui 2.0.0-preview51 → 2.0.0-preview53

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": "pome-ui",
3
- "version": "2.0.0-preview51",
3
+ "version": "2.0.0-preview53",
4
4
  "description": "Front-end MVC library",
5
5
  "main": "pome-ui.js",
6
6
  "bin": {
package/pome-skeleton.css CHANGED
@@ -119,3 +119,18 @@
119
119
  background-size: 400% 100%;
120
120
  animation: _pome-sk-shimmer 1.4s ease infinite;
121
121
  }
122
+
123
+ /* Layout skeleton overlay – covers viewport while Layout created() resolves.
124
+ * The real layout content (including <render-body> container) stays in the DOM
125
+ * behind this overlay so that container.open() can mount the page immediately.
126
+ */
127
+ ._pome-layout-skeleton {
128
+ position: fixed;
129
+ top: 0;
130
+ left: 0;
131
+ right: 0;
132
+ bottom: 0;
133
+ z-index: 9999;
134
+ background: #fff;
135
+ overflow: hidden;
136
+ }
package/pome-ui.dev.js CHANGED
@@ -18201,6 +18201,37 @@ function build(options, exports) {
18201
18201
  // This function is kept as a hook point for future use.
18202
18202
  }
18203
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
+
18204
18235
  // --- Known HTML tags for skeleton (custom components will be unwrapped) ---
18205
18236
  var _knownHtmlTags = null;
18206
18237
  function _isKnownHtmlTag(tag) {
@@ -18304,9 +18335,57 @@ function build(options, exports) {
18304
18335
 
18305
18336
  // 4. Replace custom/unknown elements with <div>, keep children
18306
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
+
18307
18372
  var div = doc.createElement('div');
18308
18373
  if (child.getAttribute('class')) div.setAttribute('class', child.getAttribute('class'));
18309
- if (child.getAttribute('style')) div.setAttribute('style', child.getAttribute('style'));
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
+
18310
18389
  while (child.firstChild) div.appendChild(child.firstChild);
18311
18390
  node.replaceChild(div, child);
18312
18391
  _skeletonStructurePass(div, doc); // recurse into replacement
@@ -18427,6 +18506,24 @@ function build(options, exports) {
18427
18506
  });
18428
18507
  };
18429
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
+
18430
18527
  function _serializeOptionsToUrl(options) {
18431
18528
  var fields = Object.getOwnPropertyNames(options);
18432
18529
  var str = '';
@@ -18647,19 +18744,27 @@ function build(options, exports) {
18647
18744
  });
18648
18745
  })
18649
18746
  .then(function (component) {
18650
- var promise = null;
18747
+ var htmlPromise;
18651
18748
  if (mobile) {
18652
- promise = _httpExist(url + '.m.html').then(function (res) {
18749
+ htmlPromise = _httpExist(url + '.m.html').then(function (res) {
18653
18750
  return _httpGet(url + (res ? '.m.html' : '.html'));
18654
18751
  });
18655
18752
  } else {
18656
- promise = _httpGet(url + '.html');
18753
+ htmlPromise = _httpGet(url + '.html');
18657
18754
  }
18755
+ // Load .skeleton.html in parallel; null if not found
18756
+ var skeletonFilePromise = _httpGetSkeletonFile(url + '.skeleton.html');
18658
18757
 
18659
- return promise.then(function (template) {
18758
+ return Promise.all([htmlPromise, skeletonFilePromise]).then(function (results) {
18759
+ var template = results[0];
18760
+ var skeletonFileHtml = results[1];
18660
18761
  if (!component.template) {
18661
18762
  component.template = _sanitizeTemplate(template);
18662
18763
  }
18764
+ // .skeleton.html file takes top priority over skeleton: true / inline string
18765
+ if (skeletonFileHtml && component.skeleton) {
18766
+ component.skeleton = skeletonFileHtml;
18767
+ }
18663
18768
  if (component.delayOpen) {
18664
18769
  var dom = new DOMParser().parseFromString(component.template, 'text/html');
18665
18770
  var children = dom.querySelector('body').children;
@@ -18684,7 +18789,8 @@ function build(options, exports) {
18684
18789
  component.template = ret;
18685
18790
  }
18686
18791
  _patchComponent(url, component);
18687
- _wrapTemplateWithSkeleton(component);
18792
+ // _wrapTemplateWithSkeleton is deferred to after _loadComponents
18793
+ // so the component skeleton registry is fully populated.
18688
18794
  return Promise.resolve(component);
18689
18795
  });
18690
18796
  })
@@ -18718,6 +18824,8 @@ function build(options, exports) {
18718
18824
 
18719
18825
  return Promise.all([pageCssPromise, _loadComponents(components, url)]).then(function (results) {
18720
18826
  var components = results[1];
18827
+ // Wrap skeleton template now that component registry is fully populated.
18828
+ _wrapTemplateWithSkeleton(component);
18721
18829
  var ret = Vue.createApp(component);
18722
18830
 
18723
18831
  for (var i = 0; i < components.length; ++i) {
@@ -18947,6 +19055,48 @@ function build(options, exports) {
18947
19055
 
18948
19056
  return _loadComponents(options.components || [], layout)
18949
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
+ }
18950
19100
  var app = Vue.createApp(options || {});
18951
19101
  for (var i = 0; i < components.length; ++i) {
18952
19102
  var com = components[i];
@@ -19448,27 +19598,59 @@ function build(options, exports) {
19448
19598
  this.createdPromise = ret;
19449
19599
  if (options.skeleton) {
19450
19600
  var self = this;
19451
- // For skeleton + delayOpen: start the delayOpen animation
19452
- // IMMEDIATELY so the skeleton fades in right away.
19453
- if (options.delayOpen) {
19454
- self.$nextTick(function () {
19455
- requestAnimationFrame(function () {
19601
+ var skeletonShowing = false;
19602
+ var resolved = false;
19603
+ // Only activate the skeleton if the promise is still pending
19604
+ // after skeletonDelay ms. If it resolves faster, skip the
19605
+ // skeleton entirely to avoid a flash.
19606
+ var skeletonDelay = typeof options.skeletonDelay === 'number'
19607
+ ? options.skeletonDelay : 1000;
19608
+ var skeletonTimer = setTimeout(function () {
19609
+ if (resolved) return;
19610
+ skeletonShowing = true;
19611
+ self.pomeSkeletonLoading = true;
19612
+ // Skeleton is now visible. If delayOpen is also set, kick
19613
+ // off its animation so it's primed when skeleton disappears.
19614
+ if (options.delayOpen) {
19615
+ self.$nextTick(function () {
19456
19616
  requestAnimationFrame(function () {
19457
- self.pomeDelayOpening = false;
19617
+ requestAnimationFrame(function () {
19618
+ self.pomeDelayOpening = false;
19619
+ });
19458
19620
  });
19621
+ setTimeout(function () {
19622
+ self.pomeDelayOpened = false;
19623
+ }, options.delayOpen);
19459
19624
  });
19460
- setTimeout(function () {
19461
- self.pomeDelayOpened = false;
19462
- }, options.delayOpen);
19463
- });
19464
- }
19625
+ }
19626
+ }, skeletonDelay);
19465
19627
  ret.then(function () {
19628
+ resolved = true;
19629
+ clearTimeout(skeletonTimer);
19466
19630
  self.pomeSkeletonLoading = false;
19631
+ // Fast path: resolved before skeleton appeared – still need
19632
+ // to run the delayOpen animation now.
19633
+ if (!skeletonShowing && options.delayOpen) {
19634
+ self.$nextTick(function () {
19635
+ requestAnimationFrame(function () {
19636
+ requestAnimationFrame(function () {
19637
+ self.pomeDelayOpening = false;
19638
+ });
19639
+ });
19640
+ setTimeout(function () {
19641
+ self.pomeDelayOpened = false;
19642
+ }, options.delayOpen);
19643
+ });
19644
+ }
19467
19645
  }).catch(function (err) {
19646
+ resolved = true;
19647
+ clearTimeout(skeletonTimer);
19648
+ self.pomeSkeletonLoading = false;
19468
19649
  console.error('[SKELETON] created Promise REJECTED:', err);
19469
19650
  });
19470
19651
  }
19471
19652
  } else if (options.skeleton) {
19653
+ // Sync created – nothing to wait for, ensure skeleton is off.
19472
19654
  this.pomeSkeletonLoading = false;
19473
19655
  }
19474
19656
 
@@ -19608,9 +19790,17 @@ function build(options, exports) {
19608
19790
  if (layout) {
19609
19791
  var layoutName = layout + (mobile ? '.m' : '');
19610
19792
  var _layoutHtml;
19611
- return _httpGet(layoutName + '.html').then(function (layoutHtml) {
19612
- _layoutHtml = layoutHtml;
19613
- return _httpGet(layout + ".js");
19793
+ var _layoutSkeletonFileHtml; // hoisted so LayoutNext closure can access it
19794
+ // Load layout .html, .js and optional .skeleton.html all in parallel
19795
+ return Promise.all([
19796
+ _httpGet(layoutName + '.html'),
19797
+ _httpGet(layout + '.js'),
19798
+ _httpGetSkeletonFile(layoutName + '.skeleton.html')
19799
+ ]).then(function (layoutResults) {
19800
+ _layoutHtml = layoutResults[0];
19801
+ var js = layoutResults[1];
19802
+ _layoutSkeletonFileHtml = layoutResults[2]; // null when absent
19803
+ return js;
19614
19804
  }).then(function (js) {
19615
19805
  var _opt = null;
19616
19806
  var Layout = function (options) {
@@ -19634,6 +19824,37 @@ function build(options, exports) {
19634
19824
  var appId = 'pomelo-' + ticks;
19635
19825
  var containerId = 'container-' + ticks;
19636
19826
  var body = document.querySelector('body').innerHTML.replace('<render-body></render-body>', '<div id="' + containerId + '"></div>');
19827
+ // Layout skeleton: defer generation to Root→_loadComponents so the
19828
+ // component skeleton registry is fully populated before we generate HTML.
19829
+ if (options.skeleton) {
19830
+ _ensureSkeletonStyle();
19831
+ var bodyEl = document.querySelector('body');
19832
+ var bodyClass = bodyEl.getAttribute('class') || '';
19833
+ var bodyInlineStyle = bodyEl.getAttribute('style') || '';
19834
+ var bodyComputed = window.getComputedStyle(bodyEl);
19835
+ var overlayStyle = bodyInlineStyle;
19836
+ if (bodyComputed.display && bodyComputed.display.indexOf('flex') >= 0) {
19837
+ if (overlayStyle) overlayStyle += ';';
19838
+ overlayStyle += 'display:' + bodyComputed.display
19839
+ + ';flex-direction:' + bodyComputed.flexDirection
19840
+ + ';align-items:' + bodyComputed.alignItems;
19841
+ } else {
19842
+ if (overlayStyle) overlayStyle += ';';
19843
+ overlayStyle += 'display:flex;flex-direction:row;align-items:stretch';
19844
+ }
19845
+ var skeletonBody = body.replace('<div id="' + containerId + '"></div>', '<div style="flex:1;min-width:0"></div>');
19846
+ // Store pending data; Root will inject the overlay div after
19847
+ // _loadComponents so registered component skeletons are available.
19848
+ options._pomeLayoutSkeleton = {
19849
+ appId: '#' + appId,
19850
+ bodyClass: bodyClass,
19851
+ overlayStyle: overlayStyle,
19852
+ skeletonBody: skeletonBody,
19853
+ // Priority: .skeleton.html file > inline string in JS
19854
+ skeletonOverride: _layoutSkeletonFileHtml
19855
+ || (typeof options.skeleton === 'string' ? options.skeleton : null)
19856
+ };
19857
+ }
19637
19858
  _ensureVueCloakStyle();
19638
19859
  document.querySelector('body').innerHTML = '<div id="' + appId + '" class="_pome-vue-cloak">' + body + '</div>';
19639
19860
  options.template = null;
@@ -19807,10 +20028,15 @@ function build(options, exports) {
19807
20028
  var _html;
19808
20029
  var _name;
19809
20030
  var _opt;
19810
- return _httpGet(c + ".html").then(function (comHtml) {
19811
- _html = comHtml;
19812
- return _httpGet(c + ".js");
19813
- }).then(function (comJs) {
20031
+ // Load .html, .js and optional .skeleton.html in parallel
20032
+ return Promise.all([
20033
+ _httpGet(c + '.html'),
20034
+ _httpGet(c + '.js'),
20035
+ _httpGetSkeletonFile(c + '.skeleton.html')
20036
+ ]).then(function (results) {
20037
+ _html = results[0];
20038
+ var comJs = results[1];
20039
+ var skeletonFileHtml = results[2]; // null when file does not exist
19814
20040
  var Component = function (name, options) {
19815
20041
  _opt = options;
19816
20042
  _name = name;
@@ -19824,6 +20050,9 @@ function build(options, exports) {
19824
20050
  }
19825
20051
  _hookSetup(_opt);
19826
20052
  _patchComponent(c, _opt);
20053
+ // Register the component's skeleton BEFORE wrapping the template,
20054
+ // so _generateSkeletonHtml still sees the clean original template.
20055
+ _registerComponentSkeleton(_name, _opt, _opt.template, skeletonFileHtml);
19827
20056
  _wrapTemplateWithSkeleton(_opt);
19828
20057
  _hookData(_opt);
19829
20058
  var cssPreloadPromise = Promise.resolve();
@@ -19933,7 +20162,9 @@ function build(options, exports) {
19933
20162
  var data = func1.call(this, app);
19934
20163
  var data2 = { pomeUiSubTitles: [] };
19935
20164
  if (opt.skeleton) {
19936
- data2.pomeSkeletonLoading = true;
20165
+ // Start hidden; skeleton is only shown after skeletonDelay ms
20166
+ // to avoid a flash when loading completes quickly.
20167
+ data2.pomeSkeletonLoading = false;
19937
20168
  }
19938
20169
  if (opt.skeleton && opt.delayOpen) {
19939
20170
  data2.pomeDelayOpened = true;