@jqhtml/core 2.3.68 → 2.3.70

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/dist/index.js CHANGED
@@ -1909,7 +1909,7 @@ Load_Coordinator._registry = new Map();
1909
1909
  const class_registry = Object.create(null);
1910
1910
  // Markers for serialized class instances (unique to avoid collisions with user data)
1911
1911
  // Replaced at build time by rollup (see rollup.config.js).
1912
- const CORE_VERSION = '2.3.68';
1912
+ const CORE_VERSION = '2.3.70';
1913
1913
  const CLASS_MARKER = '__jqhtml_class__';
1914
1914
  const PROPS_MARKER = '__jqhtml_props__';
1915
1915
  /**
@@ -3938,6 +3938,7 @@ function resolve_parent_template(template_def, ctor) {
3938
3938
  class Jqhtml_Component {
3939
3939
  constructor(element, args = {}) {
3940
3940
  this._ready_state = 0; // 0=created, 1=init, 2=loaded, 3=rendered, 4=ready
3941
+ this._debug_no_data = false; // Set by _reload_without_data(); cleared by any real load
3941
3942
  this._instantiator = null; // Component that instantiated this one
3942
3943
  this._dom_parent = null; // Closest component in DOM tree (for lifecycle)
3943
3944
  this._dom_children = new Set(); // Components in DOM subtree (for lifecycle)
@@ -4550,6 +4551,8 @@ class Jqhtml_Component {
4550
4551
  // The queued executor can start long after load() was called - re-check.
4551
4552
  if (this._stopped)
4552
4553
  return false;
4554
+ // Any real load returns the component to normal, whatever the debug tool did.
4555
+ this._debug_no_data = false;
4553
4556
  // Snapshot current data for change detection
4554
4557
  const data_before = JSON.stringify(this.data);
4555
4558
  // Execute on_load() on detached proxy (restores data to on_create snapshot)
@@ -4739,6 +4742,8 @@ class Jqhtml_Component {
4739
4742
  async _load() {
4740
4743
  if (this._stopped || this._ready_state >= 2)
4741
4744
  return;
4745
+ // Any real load returns the component to normal, whatever the debug tool did.
4746
+ this._debug_no_data = false;
4742
4747
  this._log_lifecycle('load', 'start');
4743
4748
  // use_cached_data feature: If cache hit occurred and use_cached_data=true, skip on_load() entirely
4744
4749
  // The cached data is already in this.data from create() phase
@@ -5171,6 +5176,51 @@ class Jqhtml_Component {
5171
5176
  async refresh() {
5172
5177
  return this.reload(false);
5173
5178
  }
5179
+ /**
5180
+ * Re-render this component from its on_create() state with its cache dropped and
5181
+ * WITHOUT running on_load()/on_loaded()/on_ready().
5182
+ *
5183
+ * @internal debug tool. Used by the debug overlay's lifecycle footer to see what a
5184
+ * component looks like with no loaded data at all. Not public API, not documented,
5185
+ * and no downstream code should call it: reload() or refresh() afterwards returns
5186
+ * the component to normal (the snapshot is intact and the cache is gone, so the
5187
+ * next load is a genuine fresh fetch).
5188
+ */
5189
+ _reload_without_data() {
5190
+ if (this._stopped)
5191
+ return Promise.resolve();
5192
+ // Same marker invalidation reload()/render() do: later ready() waiters must wait
5193
+ // for THIS cycle rather than resolve off the previous one.
5194
+ this.invalidate('ready');
5195
+ // Serialised with reload()/refresh() so it can never interleave with a real load.
5196
+ return this._queue.enqueue('reload', async () => {
5197
+ if (this._stopped)
5198
+ return;
5199
+ // Drop this component's own cache entries (data and html snapshot), so a later
5200
+ // reload() genuinely re-fetches instead of hydrating from what is on screen now.
5201
+ const { cache_key } = generate_cache_key(this);
5202
+ if (cache_key !== null) {
5203
+ Jqhtml_Local_Storage.remove(cache_key);
5204
+ Jqhtml_Local_Storage.remove(`${cache_key}::html`);
5205
+ }
5206
+ // Back to the on_create() state. Components with no custom on_load() take no
5207
+ // snapshot (nothing can have changed this.data), so they simply re-render.
5208
+ if (this.__initial_data_snapshot !== null) {
5209
+ this.__data_frozen = false;
5210
+ this.data = JSON.parse(JSON.stringify(this.__initial_data_snapshot));
5211
+ this.__data_frozen = true;
5212
+ }
5213
+ // Read by the debug overlay's lifecycle row; cleared by _reload()/load()/_load().
5214
+ this._debug_no_data = true;
5215
+ this._render();
5216
+ await this._wait_for_children_ready();
5217
+ if (this._stopped)
5218
+ return;
5219
+ this._ready_state = 4;
5220
+ this._update_debug_attrs();
5221
+ this.trigger('ready');
5222
+ });
5223
+ }
5174
5224
  /**
5175
5225
  * Internal reload implementation - re-fetch data and re-render
5176
5226
  *
@@ -5208,6 +5258,8 @@ class Jqhtml_Component {
5208
5258
  async _reload() {
5209
5259
  if (this._stopped)
5210
5260
  return;
5261
+ // Any real load returns the component to normal, whatever the debug tool did.
5262
+ this._debug_no_data = false;
5211
5263
  // Invalidate ready event so new handlers wait for this reload to complete
5212
5264
  // This prevents .on('ready') handlers from firing immediately based on previous lifecycle
5213
5265
  this.invalidate('ready');
@@ -5946,9 +5998,9 @@ Jqhtml_Component.__jqhtml_component = true; // Marker for unified register() det
5946
5998
  // A bare component name, as accepted by closest() in place of a class selector
5947
5999
  Jqhtml_Component.COMPONENT_NAME_PATTERN = COMPONENT_NAME_PATTERN;
5948
6000
 
5949
- var light_css = "html[data-jqhtml-debug] .jqhtml-debug-hit {\n outline: 2px solid #e5322d;\n outline-offset: -2px;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-0 {\n outline-color: #e5322d;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-1 {\n outline-color: #2f6fe4;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-2 {\n outline-color: #1f9d55;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-3 {\n outline-color: #d97706;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-4 {\n outline-color: #7c3aed;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-5 {\n outline-color: #0e7490;\n}";
6001
+ var light_css = "html[data-jqhtml-debug] .jqhtml-debug-selected {\n outline: 2px solid #b45309;\n outline-offset: -2px;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit {\n outline: 2px solid #e5322d;\n outline-offset: -2px;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-0 {\n outline-color: #e5322d;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-1 {\n outline-color: #2f6fe4;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-2 {\n outline-color: #1f9d55;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-3 {\n outline-color: #d97706;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-4 {\n outline-color: #7c3aed;\n}\n\nhtml[data-jqhtml-debug] .jqhtml-debug-hit.jqhtml-debug-depth-5 {\n outline-color: #0e7490;\n}";
5950
6002
 
5951
- var shadow_css = ":host {\n all: initial;\n display: block;\n position: fixed;\n box-sizing: border-box;\n top: 0;\n left: 0;\n width: 100vw;\n height: 100vh;\n z-index: 2147483647;\n pointer-events: none;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n font-weight: normal;\n font-style: normal;\n font-variant: normal;\n line-height: 1.4;\n color: #1c1c1c;\n text-align: left;\n text-transform: none;\n text-indent: 0;\n text-shadow: none;\n letter-spacing: normal;\n word-spacing: normal;\n white-space: normal;\n word-break: normal;\n overflow-wrap: anywhere;\n hyphens: none;\n tab-size: 4;\n direction: ltr;\n visibility: visible;\n cursor: default;\n list-style: none;\n quotes: none;\n}\n\n.jqhtml-debug-layer {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n overflow: hidden;\n pointer-events: none;\n}\n\n.jqhtml-debug-label {\n position: absolute;\n box-sizing: border-box;\n display: block;\n padding: 1px 5px;\n min-height: 18px;\n color: #ffffff;\n background: #e5322d;\n border-radius: 0 0 3px 0;\n font-size: 11px;\n line-height: 16px;\n white-space: normal;\n overflow-wrap: anywhere;\n overflow: hidden;\n pointer-events: none;\n}\n\n.jqhtml-debug-label.jqhtml-debug-depth-0 {\n background: #e5322d;\n}\n\n.jqhtml-debug-label.jqhtml-debug-depth-1 {\n background: #2f6fe4;\n}\n\n.jqhtml-debug-label.jqhtml-debug-depth-2 {\n background: #1f9d55;\n}\n\n.jqhtml-debug-label.jqhtml-debug-depth-3 {\n background: #d97706;\n}\n\n.jqhtml-debug-label.jqhtml-debug-depth-4 {\n background: #7c3aed;\n}\n\n.jqhtml-debug-label.jqhtml-debug-depth-5 {\n background: #0e7490;\n}\n\n.jqhtml-debug-label-name {\n font-weight: bold;\n}\n\n.jqhtml-debug-label-args {\n opacity: 0.9;\n margin-left: 6px;\n}\n\n.jqhtml-debug-modal {\n display: none;\n position: absolute;\n box-sizing: border-box;\n top: 24px;\n right: 24px;\n width: 520px;\n max-width: calc(100% - 48px);\n max-height: calc(100% - 48px);\n overflow: auto;\n padding: 0 0 12px 0;\n background: #ffffff;\n color: #1c1c1c;\n border: 1px solid #d0d0d0;\n border-radius: 6px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);\n pointer-events: auto;\n}\n\n.jqhtml-debug-modal.jqhtml-debug-modal-open {\n display: block;\n}\n\n.jqhtml-debug-title {\n display: flex;\n align-items: center;\n gap: 8px;\n position: sticky;\n top: 0;\n padding: 8px 12px;\n background: #1c1c1c;\n color: #ffffff;\n font-weight: bold;\n}\n\n.jqhtml-debug-title-name {\n flex: 1 1 auto;\n font-size: 13px;\n}\n\n.jqhtml-debug-button {\n all: unset;\n display: inline-block;\n box-sizing: border-box;\n padding: 2px 8px;\n border: 1px solid rgba(255, 255, 255, 0.6);\n border-radius: 3px;\n color: #ffffff;\n font-size: 11px;\n line-height: 16px;\n cursor: pointer;\n user-select: none;\n}\n\n.jqhtml-debug-button:hover {\n background: rgba(255, 255, 255, 0.15);\n}\n\n.jqhtml-debug-section {\n padding: 8px 12px 0 12px;\n}\n\n.jqhtml-debug-h {\n margin: 4px 0;\n color: #666666;\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n}\n\n.jqhtml-debug-table {\n display: grid;\n grid-template-columns: max-content max-content 1fr;\n column-gap: 12px;\n row-gap: 2px;\n align-items: baseline;\n}\n\n.jqhtml-debug-key {\n color: #005cc5;\n white-space: nowrap;\n}\n\n.jqhtml-debug-type {\n color: #666666;\n font-size: 11px;\n white-space: nowrap;\n}\n\n.jqhtml-debug-value {\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n\n.jqhtml-debug-empty {\n color: #666666;\n font-style: italic;\n}\n\n.jqhtml-debug-list {\n display: block;\n margin: 0;\n padding: 0;\n}\n\n.jqhtml-debug-link {\n display: block;\n padding: 2px 4px;\n margin: 0 -4px;\n border-radius: 3px;\n cursor: pointer;\n}\n\n.jqhtml-debug-link:hover {\n background: #eef3ff;\n}\n\n.jqhtml-debug-link-cid {\n color: #666666;\n margin-left: 6px;\n}\n\n.jqhtml-debug-note {\n margin-top: 4px;\n color: #92400e;\n font-size: 11px;\n}";
6003
+ var shadow_css = ":host {\n all: initial;\n display: block;\n position: fixed;\n box-sizing: border-box;\n top: 0;\n left: 0;\n width: 100vw;\n height: 100vh;\n z-index: 2147483647;\n pointer-events: none;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n font-weight: normal;\n font-style: normal;\n font-variant: normal;\n line-height: 1.4;\n color: #1c1c1c;\n text-align: left;\n text-transform: none;\n text-indent: 0;\n text-shadow: none;\n letter-spacing: normal;\n word-spacing: normal;\n white-space: normal;\n word-break: normal;\n overflow-wrap: anywhere;\n hyphens: none;\n tab-size: 4;\n direction: ltr;\n visibility: visible;\n cursor: default;\n list-style: none;\n quotes: none;\n}\n\n.jqhtml-debug-layer {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n overflow: hidden;\n pointer-events: none;\n}\n\n.jqhtml-debug-label {\n position: absolute;\n box-sizing: border-box;\n display: block;\n padding: 1px 5px;\n min-height: 18px;\n color: #ffffff;\n background: #e5322d;\n border-radius: 0 0 3px 0;\n font-size: 11px;\n line-height: 16px;\n white-space: normal;\n overflow-wrap: anywhere;\n overflow: hidden;\n pointer-events: auto;\n cursor: pointer;\n}\n\n.jqhtml-debug-label.jqhtml-debug-depth-0 {\n background: #e5322d;\n}\n\n.jqhtml-debug-label.jqhtml-debug-depth-1 {\n background: #2f6fe4;\n}\n\n.jqhtml-debug-label.jqhtml-debug-depth-2 {\n background: #1f9d55;\n}\n\n.jqhtml-debug-label.jqhtml-debug-depth-3 {\n background: #d97706;\n}\n\n.jqhtml-debug-label.jqhtml-debug-depth-4 {\n background: #7c3aed;\n}\n\n.jqhtml-debug-label.jqhtml-debug-depth-5 {\n background: #0e7490;\n}\n\n.jqhtml-debug-label-name {\n font-weight: bold;\n}\n\n.jqhtml-debug-label-args {\n opacity: 0.9;\n margin-left: 6px;\n}\n\n.jqhtml-debug-modal {\n display: none;\n position: absolute;\n box-sizing: border-box;\n top: 24px;\n right: 24px;\n width: 520px;\n max-width: calc(100% - 48px);\n overflow: hidden;\n padding: 0;\n background: #ffffff;\n color: #1c1c1c;\n border: 1px solid #d0d0d0;\n border-radius: 6px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);\n pointer-events: auto;\n}\n\n.jqhtml-debug-modal.jqhtml-debug-modal-open {\n display: block;\n}\n\n.jqhtml-debug-modal.jqhtml-debug-modal-left {\n left: 24px;\n right: auto;\n}\n\n.jqhtml-debug-title {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n background: #1c1c1c;\n color: #ffffff;\n font-weight: bold;\n}\n\n.jqhtml-debug-body {\n display: block;\n box-sizing: border-box;\n overflow-y: scroll;\n min-height: 300px;\n max-height: min(600px, 100vh - 200px);\n padding: 0 0 12px 0;\n}\n\n.jqhtml-debug-footer {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n background: #ffffff;\n border-top: 1px solid #d0d0d0;\n}\n\n.jqhtml-debug-footer-label {\n color: #666666;\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n}\n\n.jqhtml-debug-title-name {\n flex: 1 1 auto;\n font-size: 13px;\n}\n\n.jqhtml-debug-button {\n all: unset;\n display: inline-block;\n box-sizing: border-box;\n padding: 2px 8px;\n border: 1px solid rgba(255, 255, 255, 0.6);\n border-radius: 3px;\n color: #ffffff;\n font-size: 11px;\n line-height: 16px;\n cursor: pointer;\n user-select: none;\n}\n\n.jqhtml-debug-button:hover {\n background: rgba(255, 255, 255, 0.15);\n}\n\n.jqhtml-debug-button.jqhtml-debug-footer-button {\n color: #1c1c1c;\n border-color: #d0d0d0;\n}\n\n.jqhtml-debug-button.jqhtml-debug-footer-button:hover {\n background: #eef3ff;\n}\n\n.jqhtml-debug-section {\n padding: 8px 12px 0 12px;\n}\n\n.jqhtml-debug-h {\n margin: 4px 0;\n color: #666666;\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n}\n\n.jqhtml-debug-table {\n display: grid;\n grid-template-columns: max-content max-content 1fr;\n column-gap: 12px;\n row-gap: 2px;\n align-items: baseline;\n}\n\n.jqhtml-debug-key {\n color: #005cc5;\n white-space: nowrap;\n}\n\n.jqhtml-debug-type {\n color: #666666;\n font-size: 11px;\n white-space: nowrap;\n}\n\n.jqhtml-debug-value {\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n\n.jqhtml-debug-empty {\n color: #666666;\n font-style: italic;\n}\n\n.jqhtml-debug-list {\n display: block;\n margin: 0;\n padding: 0;\n}\n\n.jqhtml-debug-link {\n display: block;\n padding: 2px 4px;\n margin: 0 -4px;\n border-radius: 3px;\n cursor: pointer;\n}\n\n.jqhtml-debug-link:hover {\n background: #eef3ff;\n}\n\n.jqhtml-debug-link-cid {\n color: #666666;\n margin-left: 6px;\n}\n\n.jqhtml-debug-note {\n margin-top: 4px;\n color: #92400e;\n font-size: 11px;\n}";
5952
6004
 
5953
6005
  /**
5954
6006
  * Component debug overlay.
@@ -5960,6 +6012,12 @@ var shadow_css = ":host {\n all: initial;\n display: block;\n position: fixed
5960
6012
  * data, state, ancestry, instantiator) instead of firing the click. Alt+click
5961
6013
  * passes through. `disable()` puts everything back.
5962
6014
  *
6015
+ * The labels are part of the interaction, not just decoration: the pointer can
6016
+ * move from a component onto one of its tabs without the hover set collapsing,
6017
+ * and clicking a tab inspects that tab's component. The modal keeps an amber
6018
+ * outline on whatever it is showing, and its title bar carries Parent / Back so
6019
+ * the DOM chain can be walked without going back to the page.
6020
+ *
5963
6021
  * Non-invasiveness, the design goal (details in ./CLAUDE.md):
5964
6022
  * - The overlay's own UI lives in a shadow root on a host element appended to
5965
6023
  * <body>, styled by shadow.scss. Page CSS cannot select into it and :host
@@ -5978,8 +6036,11 @@ const HOST_ATTR = 'data-jqhtml-debug-root'; // the shadow host
5978
6036
  const LIGHT_STYLE_ID = 'jqhtml-debug-light-styles';
5979
6037
  const HIT_CLASS = `${PREFIX}hit`;
5980
6038
  const DEPTH_CLASS = `${PREFIX}depth-`;
6039
+ const SELECTED_CLASS = `${PREFIX}selected`; // on the element the modal is showing
5981
6040
  const DEPTH_COLORS = 6; // keep in step with $depth-colors in _tokens.scss
5982
6041
  const LABEL_HEIGHT = 18;
6042
+ const LABEL_MIN_WIDTH = 400; // a narrow component still gets a readable tab
6043
+ const VIEWPORT_MARGIN = 5; // gap kept between a label's right edge and the viewport
5983
6044
  const READY_STATES = ['created', 'init', 'loaded', 'rendered', 'ready'];
5984
6045
  const state = {
5985
6046
  enabled: false,
@@ -5989,7 +6050,10 @@ const state = {
5989
6050
  modal: null,
5990
6051
  chain: [],
5991
6052
  hit_elements: [],
6053
+ label_hovered: false,
5992
6054
  modal_component: null,
6055
+ nav_stack: [],
6056
+ selected_element: null,
5993
6057
  };
5994
6058
  // ---------------------------------------------------------------------------
5995
6059
  // Public API
@@ -6046,7 +6110,7 @@ const debug_overlay = {
6046
6110
  : chain_for(target && target.jquery ? target[0] : target)[0];
6047
6111
  if (!component)
6048
6112
  return false;
6049
- open_modal(component);
6113
+ select(component);
6050
6114
  return true;
6051
6115
  },
6052
6116
  };
@@ -6101,9 +6165,18 @@ function is_ours(event) {
6101
6165
  // ---------------------------------------------------------------------------
6102
6166
  // Hover
6103
6167
  // ---------------------------------------------------------------------------
6168
+ /**
6169
+ * A hover on the page replaces the hover set - except while the pointer is on
6170
+ * our own UI. An event from inside the shadow host (a label, the modal) never
6171
+ * clears or changes the hover, and neither does a page hover raised while the
6172
+ * pointer is parked on a label: that is what lets a label be reached and clicked
6173
+ * without the outlines it belongs to vanishing on the way.
6174
+ */
6104
6175
  function on_mouseover(event) {
6105
6176
  if (is_ours(event))
6106
6177
  return;
6178
+ if (state.label_hovered)
6179
+ return;
6107
6180
  const chain = chain_for(event.target);
6108
6181
  if (chain[0] === state.chain[0])
6109
6182
  return;
@@ -6113,6 +6186,32 @@ function on_mouseout(event) {
6113
6186
  if (!event.relatedTarget)
6114
6187
  clear_hover(); // pointer left the document
6115
6188
  }
6189
+ /**
6190
+ * The pointer left a label. If it landed back inside the component the label
6191
+ * belongs to (the usual case - the label sits on the component's own corner),
6192
+ * the hover set stays; anywhere else it goes. `elementFromPoint` reports our
6193
+ * shadow host for a point covered by another label, so the host is skipped and
6194
+ * the page element underneath is used.
6195
+ */
6196
+ function on_label_leave(event) {
6197
+ state.label_hovered = false;
6198
+ const innermost = state.chain[0];
6199
+ if (!innermost)
6200
+ return;
6201
+ const under = element_under(event.clientX, event.clientY);
6202
+ if (!under || !innermost.$[0].contains(under))
6203
+ clear_hover();
6204
+ }
6205
+ function element_under(x, y) {
6206
+ const all = document.elementsFromPoint;
6207
+ const candidates = all
6208
+ ? all.call(document, x, y)
6209
+ : [document.elementFromPoint(x, y)].filter(Boolean);
6210
+ for (const node of candidates)
6211
+ if (node !== state.host)
6212
+ return node;
6213
+ return null;
6214
+ }
6116
6215
  function on_reposition() {
6117
6216
  if (state.chain.length)
6118
6217
  render_labels();
@@ -6135,18 +6234,23 @@ function clear_hover() {
6135
6234
  }
6136
6235
  state.hit_elements = [];
6137
6236
  state.chain = [];
6237
+ state.label_hovered = false;
6138
6238
  if (state.layer)
6139
6239
  state.layer.textContent = '';
6140
6240
  }
6141
6241
  /**
6142
6242
  * One tab per component in the chain, pinned to the component's top-left
6143
- * corner and no wider than the component. Outermost first so the innermost
6144
- * paints on top; tabs that share a corner (a child filling its parent, the
6145
- * usual case) stack downward instead of hiding each other.
6243
+ * corner. Width: the component's, but a narrow component still gets up to
6244
+ * LABEL_MIN_WIDTH so its name and args stay legible; a label that would then
6245
+ * run off the right edge is slid back so its right edge sits at the margin.
6246
+ * Outermost first so the innermost paints on top; tabs that share a corner (a
6247
+ * child filling its parent, the usual case) stack downward instead of hiding
6248
+ * each other.
6146
6249
  */
6147
6250
  function render_labels() {
6148
6251
  const layer = state.layer;
6149
6252
  layer.textContent = '';
6253
+ state.label_hovered = false; // the element the pointer was on is gone
6150
6254
  const taken = new Map();
6151
6255
  for (let depth = state.chain.length - 1; depth >= 0; depth--) {
6152
6256
  const component = state.chain[depth];
@@ -6159,14 +6263,33 @@ function render_labels() {
6159
6263
  const stacked = taken.get(key) || 0;
6160
6264
  taken.set(key, stacked + 1);
6161
6265
  const label = el('div', `label ${PREFIX}depth-${depth % DEPTH_COLORS}`);
6162
- label.style.left = `${left}px`;
6266
+ // Laid out at the left edge FIRST, then moved. An absolutely positioned box
6267
+ // shrink-to-fits into the room its own `left` leaves, so measuring it at its
6268
+ // final position would report the squeezed width, and moving it would let it
6269
+ // re-expand - the two chase each other. At left 0 it has the whole viewport,
6270
+ // so the measurement below is the width it will actually keep.
6271
+ label.style.left = '0px';
6163
6272
  label.style.top = `${top + stacked * LABEL_HEIGHT}px`;
6164
- label.style.maxWidth = `${Math.max(24, Math.round(rect.width))}px`;
6273
+ label.style.maxWidth = `${rect.width < LABEL_MIN_WIDTH ? LABEL_MIN_WIDTH : Math.round(rect.width)}px`;
6165
6274
  label.appendChild(el('span', 'label-name', component.component_name()));
6166
6275
  const args = simple_args(component);
6167
6276
  if (args)
6168
6277
  label.appendChild(el('span', 'label-args', args));
6278
+ // Each label knows its own component: it is a hover target that holds the set
6279
+ // in place, and a click target that inspects THAT component, not the innermost.
6280
+ label.addEventListener('mouseenter', () => { state.label_hovered = true; });
6281
+ label.addEventListener('mouseleave', on_label_leave);
6282
+ label.addEventListener('click', (event) => {
6283
+ event.stopPropagation(); // the document handler already skips is_ours events
6284
+ select(component);
6285
+ });
6169
6286
  layer.appendChild(label);
6287
+ // A label on a narrow component may be wider than the component, so a component
6288
+ // near the right edge can push its label off-screen. Slide it back until its
6289
+ // right edge sits on the margin, never past the left edge.
6290
+ const width = label.getBoundingClientRect().width || label.offsetWidth;
6291
+ const limit = window.innerWidth - VIEWPORT_MARGIN;
6292
+ label.style.left = `${left + width > limit ? Math.max(0, Math.round(limit - width)) : left}px`;
6170
6293
  }
6171
6294
  }
6172
6295
  /** `$id=7 $title="Users"` - only strings, numbers and booleans, never internals. */
@@ -6196,7 +6319,7 @@ function on_click(event) {
6196
6319
  close_modal();
6197
6320
  return;
6198
6321
  }
6199
- open_modal(chain[0]);
6322
+ select(chain[0]);
6200
6323
  }
6201
6324
  function on_keydown(event) {
6202
6325
  if (event.key === 'Escape' && state.modal_component) {
@@ -6210,17 +6333,82 @@ function close_modal() {
6210
6333
  state.modal.classList.remove(`${PREFIX}modal-open`);
6211
6334
  state.modal.textContent = '';
6212
6335
  state.modal_component = null;
6336
+ state.nav_stack = [];
6337
+ set_selected(null);
6338
+ }
6339
+ // ---------------------------------------------------------------------------
6340
+ // Selection and navigation
6341
+ //
6342
+ // Three ways in, and they differ only in what they do to the navigation stack:
6343
+ // select() - a FRESH selection (page click, label click, inspect()): the stack
6344
+ // is dropped, so Back disappears.
6345
+ // navigate() - a step along the chain (Parent, an Ancestry entry, the
6346
+ // Instantiator link): the component being left is pushed.
6347
+ // show() - render only, stack untouched; what Back and the lifecycle
6348
+ // buttons re-open with.
6349
+ // ---------------------------------------------------------------------------
6350
+ function select(component) {
6351
+ state.nav_stack = [];
6352
+ show(component);
6353
+ }
6354
+ function navigate(component) {
6355
+ if (state.modal_component && state.modal_component !== component) {
6356
+ state.nav_stack.push(state.modal_component);
6357
+ }
6358
+ show(component);
6213
6359
  }
6214
- function open_modal(component) {
6360
+ /** Pop to the last component still alive; a stopped one is skipped, not shown. */
6361
+ function go_back() {
6362
+ while (state.nav_stack.length) {
6363
+ const previous = state.nav_stack.pop();
6364
+ if (previous.$.hasClass('_Component_Stopped'))
6365
+ continue;
6366
+ show(previous);
6367
+ return;
6368
+ }
6369
+ if (state.modal_component)
6370
+ show(state.modal_component); // nothing left: drop Back
6371
+ }
6372
+ /** Move SELECTED_CLASS to `element`, or take it off the page entirely for null. */
6373
+ function set_selected(element) {
6374
+ if (state.selected_element && state.selected_element !== element) {
6375
+ state.selected_element.classList.remove(SELECTED_CLASS);
6376
+ }
6377
+ state.selected_element = element;
6378
+ if (element)
6379
+ element.classList.add(SELECTED_CLASS);
6380
+ }
6381
+ /**
6382
+ * The inspector. Three children: a fixed title bar, a scrolling body holding every
6383
+ * section, and a fixed footer of lifecycle buttons. The modal sits top-right, but
6384
+ * moves to the left when the inspected component is itself in the right half of the
6385
+ * viewport - otherwise the panel covers the thing being inspected.
6386
+ */
6387
+ function show(component) {
6215
6388
  const modal = state.modal;
6216
6389
  modal.textContent = '';
6217
6390
  state.modal_component = component;
6218
6391
  const chain = chain_for(component.$[0]);
6219
6392
  const ancestors = chain.slice(1);
6220
6393
  const element = component.$[0];
6221
- // Title bar
6394
+ set_selected(element);
6395
+ modal.classList.toggle(`${PREFIX}modal-left`, element.getBoundingClientRect().left > window.innerWidth / 2);
6396
+ // Title bar: the name, then [Back] [Parent] [Log to console] [Close]. Back is
6397
+ // there only while something was navigated away from, Parent only while the
6398
+ // component has a DOM parent component - the first Ancestry entry.
6222
6399
  const title = el('div', 'title');
6223
6400
  title.appendChild(el('span', 'title-name', `<${component.component_name()}>`));
6401
+ if (state.nav_stack.length) {
6402
+ const back = el('button', 'button', 'Back');
6403
+ back.addEventListener('click', go_back);
6404
+ title.appendChild(back);
6405
+ }
6406
+ const parent = ancestors[0];
6407
+ if (parent) {
6408
+ const up = el('button', 'button', 'Parent');
6409
+ up.addEventListener('click', () => navigate(parent));
6410
+ title.appendChild(up);
6411
+ }
6224
6412
  const log = el('button', 'button', 'Log to console');
6225
6413
  log.addEventListener('click', () => {
6226
6414
  console.log(`[JQHTML debug] <${component.component_name()}>`, component, element);
@@ -6230,6 +6418,9 @@ function open_modal(component) {
6230
6418
  close.addEventListener('click', close_modal);
6231
6419
  title.appendChild(close);
6232
6420
  modal.appendChild(title);
6421
+ // The only scrolling element: the title bar and the footer stay put.
6422
+ const body = el('div', 'body');
6423
+ modal.appendChild(body);
6233
6424
  // Identity
6234
6425
  const ctor = component.constructor;
6235
6426
  const hierarchy = ctor.get_class_hierarchy ? ctor.get_class_hierarchy() : [ctor.name];
@@ -6238,15 +6429,18 @@ function open_modal(component) {
6238
6429
  row(identity, 'class', '', hierarchy.join(' → '));
6239
6430
  row(identity, 'element', '', describe_element(element));
6240
6431
  row(identity, '_cid', '', String(component._cid));
6241
- row(identity, 'lifecycle', '', READY_STATES[component._ready_state] || String(component._ready_state));
6432
+ const lifecycle = READY_STATES[component._ready_state] || String(component._ready_state);
6433
+ row(identity, 'lifecycle', '', component._debug_no_data
6434
+ ? `${lifecycle} (no data - reload w/o data)`
6435
+ : lifecycle);
6242
6436
  const nocache = element.getAttribute('data-nocache');
6243
6437
  if (nocache)
6244
6438
  row(identity, 'cache', '', `declined: ${nocache}`);
6245
- modal.appendChild(section('Identity', identity));
6439
+ body.appendChild(section('Identity', identity));
6246
6440
  // Args / data / state
6247
- modal.appendChild(section('Args', object_table(component.args)));
6248
- modal.appendChild(section('Data', object_table(component.data)));
6249
- modal.appendChild(section('State', object_table(component.state)));
6441
+ body.appendChild(section('Args', object_table(component.args)));
6442
+ body.appendChild(section('Data', object_table(component.data)));
6443
+ body.appendChild(section('State', object_table(component.state)));
6250
6444
  // Ancestry
6251
6445
  const list = el('div', 'list');
6252
6446
  if (!ancestors.length)
@@ -6255,10 +6449,10 @@ function open_modal(component) {
6255
6449
  const link = el('div', 'link');
6256
6450
  link.appendChild(el('span', 'link-name', `<${ancestor.component_name()}>`));
6257
6451
  link.appendChild(el('span', 'link-cid', String(ancestor._cid)));
6258
- link.addEventListener('click', () => open_modal(ancestor));
6452
+ link.addEventListener('click', () => navigate(ancestor));
6259
6453
  list.appendChild(link);
6260
6454
  }
6261
- modal.appendChild(section('Ancestry (DOM, nearest first)', list));
6455
+ body.appendChild(section('Ancestry (DOM, nearest first)', list));
6262
6456
  // Instantiator - the component whose template wrote this tag. Differs from
6263
6457
  // the DOM parent for markup written in a slot body or default content.
6264
6458
  const instantiator = component.instantiator ? component.instantiator() : null;
@@ -6267,7 +6461,7 @@ function open_modal(component) {
6267
6461
  const link = el('div', 'link');
6268
6462
  link.appendChild(el('span', 'link-name', `<${instantiator.component_name()}>`));
6269
6463
  link.appendChild(el('span', 'link-cid', String(instantiator._cid)));
6270
- link.addEventListener('click', () => open_modal(instantiator));
6464
+ link.addEventListener('click', () => navigate(instantiator));
6271
6465
  inst.appendChild(link);
6272
6466
  if (ancestors[0] && instantiator !== ancestors[0]) {
6273
6467
  inst.appendChild(el('div', 'note', 'Written in this component\'s template but rendered inside a different parent (slot or default content): handlers and $sid bind here.'));
@@ -6276,9 +6470,49 @@ function open_modal(component) {
6276
6470
  else {
6277
6471
  inst.appendChild(el('div', 'empty', 'none - created directly'));
6278
6472
  }
6279
- modal.appendChild(section('Instantiator', inst));
6473
+ body.appendChild(section('Instantiator', inst));
6474
+ // Footer: lifecycle controls. Clicks land inside the shadow host, so the
6475
+ // overlay's own click hijack lets them through (is_ours).
6476
+ const footer = el('div', 'footer');
6477
+ footer.appendChild(el('span', 'footer-label', 'Lifecycle:'));
6478
+ footer.appendChild(lifecycle_button(component, 'Reload', (c) => c.reload()));
6479
+ footer.appendChild(lifecycle_button(component, 'Refresh', (c) => c.refresh()));
6480
+ footer.appendChild(lifecycle_button(component, 'Rerender', (c) => c.render()));
6481
+ footer.appendChild(lifecycle_button(component, 'Reload w/o data', (c) => c._reload_without_data()));
6482
+ modal.appendChild(footer);
6280
6483
  modal.classList.add(`${PREFIX}modal-open`);
6281
6484
  }
6485
+ /**
6486
+ * One footer button. Runs its lifecycle call on a live component, logs whatever it
6487
+ * throws or rejects with, and re-opens the modal once the call settles so the panel
6488
+ * (lifecycle row above all) describes the state the button just produced.
6489
+ */
6490
+ function lifecycle_button(component, label, run) {
6491
+ const button = el('button', 'button footer-button', label);
6492
+ button.addEventListener('click', () => {
6493
+ if (component.$.hasClass('_Component_Stopped'))
6494
+ return; // dead component: nothing to do
6495
+ let result;
6496
+ try {
6497
+ result = run(component);
6498
+ }
6499
+ catch (error) {
6500
+ console.error(`[JQHTML debug] ${label} threw on <${component.component_name()}>`, error);
6501
+ return;
6502
+ }
6503
+ Promise.resolve(result)
6504
+ .catch((error) => {
6505
+ console.error(`[JQHTML debug] ${label} rejected on <${component.component_name()}>`, error);
6506
+ })
6507
+ .then(() => {
6508
+ // `show`, not `select`: the lifecycle buttons re-render the panel in place
6509
+ // and must not throw away the navigation stack behind it.
6510
+ if (state.enabled && state.modal_component === component)
6511
+ show(component);
6512
+ });
6513
+ });
6514
+ return button;
6515
+ }
6282
6516
  // ---------------------------------------------------------------------------
6283
6517
  // DOM helpers (shadow side)
6284
6518
  // ---------------------------------------------------------------------------
@@ -7009,7 +7243,7 @@ function init(jQuery, config) {
7009
7243
  }
7010
7244
  }
7011
7245
  // Version - will be replaced during build with actual version from package.json
7012
- const version = '2.3.68';
7246
+ const version = '2.3.70';
7013
7247
  // Default export with all functionality
7014
7248
  const jqhtml = {
7015
7249
  // Core