@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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JQHTML Core v2.3.68
2
+ * JQHTML Core v2.3.70
3
3
  * (c) 2025 JQHTML Team
4
4
  * Released under the MIT License
5
5
  */
@@ -1914,7 +1914,7 @@ Load_Coordinator._registry = new Map();
1914
1914
  const class_registry = Object.create(null);
1915
1915
  // Markers for serialized class instances (unique to avoid collisions with user data)
1916
1916
  // Replaced at build time by rollup (see rollup.config.js).
1917
- const CORE_VERSION = '2.3.68';
1917
+ const CORE_VERSION = '2.3.70';
1918
1918
  const CLASS_MARKER = '__jqhtml_class__';
1919
1919
  const PROPS_MARKER = '__jqhtml_props__';
1920
1920
  /**
@@ -3943,6 +3943,7 @@ function resolve_parent_template(template_def, ctor) {
3943
3943
  class Jqhtml_Component {
3944
3944
  constructor(element, args = {}) {
3945
3945
  this._ready_state = 0; // 0=created, 1=init, 2=loaded, 3=rendered, 4=ready
3946
+ this._debug_no_data = false; // Set by _reload_without_data(); cleared by any real load
3946
3947
  this._instantiator = null; // Component that instantiated this one
3947
3948
  this._dom_parent = null; // Closest component in DOM tree (for lifecycle)
3948
3949
  this._dom_children = new Set(); // Components in DOM subtree (for lifecycle)
@@ -4555,6 +4556,8 @@ class Jqhtml_Component {
4555
4556
  // The queued executor can start long after load() was called - re-check.
4556
4557
  if (this._stopped)
4557
4558
  return false;
4559
+ // Any real load returns the component to normal, whatever the debug tool did.
4560
+ this._debug_no_data = false;
4558
4561
  // Snapshot current data for change detection
4559
4562
  const data_before = JSON.stringify(this.data);
4560
4563
  // Execute on_load() on detached proxy (restores data to on_create snapshot)
@@ -4744,6 +4747,8 @@ class Jqhtml_Component {
4744
4747
  async _load() {
4745
4748
  if (this._stopped || this._ready_state >= 2)
4746
4749
  return;
4750
+ // Any real load returns the component to normal, whatever the debug tool did.
4751
+ this._debug_no_data = false;
4747
4752
  this._log_lifecycle('load', 'start');
4748
4753
  // use_cached_data feature: If cache hit occurred and use_cached_data=true, skip on_load() entirely
4749
4754
  // The cached data is already in this.data from create() phase
@@ -5176,6 +5181,51 @@ class Jqhtml_Component {
5176
5181
  async refresh() {
5177
5182
  return this.reload(false);
5178
5183
  }
5184
+ /**
5185
+ * Re-render this component from its on_create() state with its cache dropped and
5186
+ * WITHOUT running on_load()/on_loaded()/on_ready().
5187
+ *
5188
+ * @internal debug tool. Used by the debug overlay's lifecycle footer to see what a
5189
+ * component looks like with no loaded data at all. Not public API, not documented,
5190
+ * and no downstream code should call it: reload() or refresh() afterwards returns
5191
+ * the component to normal (the snapshot is intact and the cache is gone, so the
5192
+ * next load is a genuine fresh fetch).
5193
+ */
5194
+ _reload_without_data() {
5195
+ if (this._stopped)
5196
+ return Promise.resolve();
5197
+ // Same marker invalidation reload()/render() do: later ready() waiters must wait
5198
+ // for THIS cycle rather than resolve off the previous one.
5199
+ this.invalidate('ready');
5200
+ // Serialised with reload()/refresh() so it can never interleave with a real load.
5201
+ return this._queue.enqueue('reload', async () => {
5202
+ if (this._stopped)
5203
+ return;
5204
+ // Drop this component's own cache entries (data and html snapshot), so a later
5205
+ // reload() genuinely re-fetches instead of hydrating from what is on screen now.
5206
+ const { cache_key } = generate_cache_key(this);
5207
+ if (cache_key !== null) {
5208
+ Jqhtml_Local_Storage.remove(cache_key);
5209
+ Jqhtml_Local_Storage.remove(`${cache_key}::html`);
5210
+ }
5211
+ // Back to the on_create() state. Components with no custom on_load() take no
5212
+ // snapshot (nothing can have changed this.data), so they simply re-render.
5213
+ if (this.__initial_data_snapshot !== null) {
5214
+ this.__data_frozen = false;
5215
+ this.data = JSON.parse(JSON.stringify(this.__initial_data_snapshot));
5216
+ this.__data_frozen = true;
5217
+ }
5218
+ // Read by the debug overlay's lifecycle row; cleared by _reload()/load()/_load().
5219
+ this._debug_no_data = true;
5220
+ this._render();
5221
+ await this._wait_for_children_ready();
5222
+ if (this._stopped)
5223
+ return;
5224
+ this._ready_state = 4;
5225
+ this._update_debug_attrs();
5226
+ this.trigger('ready');
5227
+ });
5228
+ }
5179
5229
  /**
5180
5230
  * Internal reload implementation - re-fetch data and re-render
5181
5231
  *
@@ -5213,6 +5263,8 @@ class Jqhtml_Component {
5213
5263
  async _reload() {
5214
5264
  if (this._stopped)
5215
5265
  return;
5266
+ // Any real load returns the component to normal, whatever the debug tool did.
5267
+ this._debug_no_data = false;
5216
5268
  // Invalidate ready event so new handlers wait for this reload to complete
5217
5269
  // This prevents .on('ready') handlers from firing immediately based on previous lifecycle
5218
5270
  this.invalidate('ready');
@@ -5951,9 +6003,9 @@ Jqhtml_Component.__jqhtml_component = true; // Marker for unified register() det
5951
6003
  // A bare component name, as accepted by closest() in place of a class selector
5952
6004
  Jqhtml_Component.COMPONENT_NAME_PATTERN = COMPONENT_NAME_PATTERN;
5953
6005
 
5954
- 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}";
6006
+ 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}";
5955
6007
 
5956
- 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}";
6008
+ 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}";
5957
6009
 
5958
6010
  /**
5959
6011
  * Component debug overlay.
@@ -5965,6 +6017,12 @@ var shadow_css = ":host {\n all: initial;\n display: block;\n position: fixed
5965
6017
  * data, state, ancestry, instantiator) instead of firing the click. Alt+click
5966
6018
  * passes through. `disable()` puts everything back.
5967
6019
  *
6020
+ * The labels are part of the interaction, not just decoration: the pointer can
6021
+ * move from a component onto one of its tabs without the hover set collapsing,
6022
+ * and clicking a tab inspects that tab's component. The modal keeps an amber
6023
+ * outline on whatever it is showing, and its title bar carries Parent / Back so
6024
+ * the DOM chain can be walked without going back to the page.
6025
+ *
5968
6026
  * Non-invasiveness, the design goal (details in ./CLAUDE.md):
5969
6027
  * - The overlay's own UI lives in a shadow root on a host element appended to
5970
6028
  * <body>, styled by shadow.scss. Page CSS cannot select into it and :host
@@ -5983,8 +6041,11 @@ const HOST_ATTR = 'data-jqhtml-debug-root'; // the shadow host
5983
6041
  const LIGHT_STYLE_ID = 'jqhtml-debug-light-styles';
5984
6042
  const HIT_CLASS = `${PREFIX}hit`;
5985
6043
  const DEPTH_CLASS = `${PREFIX}depth-`;
6044
+ const SELECTED_CLASS = `${PREFIX}selected`; // on the element the modal is showing
5986
6045
  const DEPTH_COLORS = 6; // keep in step with $depth-colors in _tokens.scss
5987
6046
  const LABEL_HEIGHT = 18;
6047
+ const LABEL_MIN_WIDTH = 400; // a narrow component still gets a readable tab
6048
+ const VIEWPORT_MARGIN = 5; // gap kept between a label's right edge and the viewport
5988
6049
  const READY_STATES = ['created', 'init', 'loaded', 'rendered', 'ready'];
5989
6050
  const state = {
5990
6051
  enabled: false,
@@ -5994,7 +6055,10 @@ const state = {
5994
6055
  modal: null,
5995
6056
  chain: [],
5996
6057
  hit_elements: [],
6058
+ label_hovered: false,
5997
6059
  modal_component: null,
6060
+ nav_stack: [],
6061
+ selected_element: null,
5998
6062
  };
5999
6063
  // ---------------------------------------------------------------------------
6000
6064
  // Public API
@@ -6051,7 +6115,7 @@ const debug_overlay = {
6051
6115
  : chain_for(target && target.jquery ? target[0] : target)[0];
6052
6116
  if (!component)
6053
6117
  return false;
6054
- open_modal(component);
6118
+ select(component);
6055
6119
  return true;
6056
6120
  },
6057
6121
  };
@@ -6106,9 +6170,18 @@ function is_ours(event) {
6106
6170
  // ---------------------------------------------------------------------------
6107
6171
  // Hover
6108
6172
  // ---------------------------------------------------------------------------
6173
+ /**
6174
+ * A hover on the page replaces the hover set - except while the pointer is on
6175
+ * our own UI. An event from inside the shadow host (a label, the modal) never
6176
+ * clears or changes the hover, and neither does a page hover raised while the
6177
+ * pointer is parked on a label: that is what lets a label be reached and clicked
6178
+ * without the outlines it belongs to vanishing on the way.
6179
+ */
6109
6180
  function on_mouseover(event) {
6110
6181
  if (is_ours(event))
6111
6182
  return;
6183
+ if (state.label_hovered)
6184
+ return;
6112
6185
  const chain = chain_for(event.target);
6113
6186
  if (chain[0] === state.chain[0])
6114
6187
  return;
@@ -6118,6 +6191,32 @@ function on_mouseout(event) {
6118
6191
  if (!event.relatedTarget)
6119
6192
  clear_hover(); // pointer left the document
6120
6193
  }
6194
+ /**
6195
+ * The pointer left a label. If it landed back inside the component the label
6196
+ * belongs to (the usual case - the label sits on the component's own corner),
6197
+ * the hover set stays; anywhere else it goes. `elementFromPoint` reports our
6198
+ * shadow host for a point covered by another label, so the host is skipped and
6199
+ * the page element underneath is used.
6200
+ */
6201
+ function on_label_leave(event) {
6202
+ state.label_hovered = false;
6203
+ const innermost = state.chain[0];
6204
+ if (!innermost)
6205
+ return;
6206
+ const under = element_under(event.clientX, event.clientY);
6207
+ if (!under || !innermost.$[0].contains(under))
6208
+ clear_hover();
6209
+ }
6210
+ function element_under(x, y) {
6211
+ const all = document.elementsFromPoint;
6212
+ const candidates = all
6213
+ ? all.call(document, x, y)
6214
+ : [document.elementFromPoint(x, y)].filter(Boolean);
6215
+ for (const node of candidates)
6216
+ if (node !== state.host)
6217
+ return node;
6218
+ return null;
6219
+ }
6121
6220
  function on_reposition() {
6122
6221
  if (state.chain.length)
6123
6222
  render_labels();
@@ -6140,18 +6239,23 @@ function clear_hover() {
6140
6239
  }
6141
6240
  state.hit_elements = [];
6142
6241
  state.chain = [];
6242
+ state.label_hovered = false;
6143
6243
  if (state.layer)
6144
6244
  state.layer.textContent = '';
6145
6245
  }
6146
6246
  /**
6147
6247
  * One tab per component in the chain, pinned to the component's top-left
6148
- * corner and no wider than the component. Outermost first so the innermost
6149
- * paints on top; tabs that share a corner (a child filling its parent, the
6150
- * usual case) stack downward instead of hiding each other.
6248
+ * corner. Width: the component's, but a narrow component still gets up to
6249
+ * LABEL_MIN_WIDTH so its name and args stay legible; a label that would then
6250
+ * run off the right edge is slid back so its right edge sits at the margin.
6251
+ * Outermost first so the innermost paints on top; tabs that share a corner (a
6252
+ * child filling its parent, the usual case) stack downward instead of hiding
6253
+ * each other.
6151
6254
  */
6152
6255
  function render_labels() {
6153
6256
  const layer = state.layer;
6154
6257
  layer.textContent = '';
6258
+ state.label_hovered = false; // the element the pointer was on is gone
6155
6259
  const taken = new Map();
6156
6260
  for (let depth = state.chain.length - 1; depth >= 0; depth--) {
6157
6261
  const component = state.chain[depth];
@@ -6164,14 +6268,33 @@ function render_labels() {
6164
6268
  const stacked = taken.get(key) || 0;
6165
6269
  taken.set(key, stacked + 1);
6166
6270
  const label = el('div', `label ${PREFIX}depth-${depth % DEPTH_COLORS}`);
6167
- label.style.left = `${left}px`;
6271
+ // Laid out at the left edge FIRST, then moved. An absolutely positioned box
6272
+ // shrink-to-fits into the room its own `left` leaves, so measuring it at its
6273
+ // final position would report the squeezed width, and moving it would let it
6274
+ // re-expand - the two chase each other. At left 0 it has the whole viewport,
6275
+ // so the measurement below is the width it will actually keep.
6276
+ label.style.left = '0px';
6168
6277
  label.style.top = `${top + stacked * LABEL_HEIGHT}px`;
6169
- label.style.maxWidth = `${Math.max(24, Math.round(rect.width))}px`;
6278
+ label.style.maxWidth = `${rect.width < LABEL_MIN_WIDTH ? LABEL_MIN_WIDTH : Math.round(rect.width)}px`;
6170
6279
  label.appendChild(el('span', 'label-name', component.component_name()));
6171
6280
  const args = simple_args(component);
6172
6281
  if (args)
6173
6282
  label.appendChild(el('span', 'label-args', args));
6283
+ // Each label knows its own component: it is a hover target that holds the set
6284
+ // in place, and a click target that inspects THAT component, not the innermost.
6285
+ label.addEventListener('mouseenter', () => { state.label_hovered = true; });
6286
+ label.addEventListener('mouseleave', on_label_leave);
6287
+ label.addEventListener('click', (event) => {
6288
+ event.stopPropagation(); // the document handler already skips is_ours events
6289
+ select(component);
6290
+ });
6174
6291
  layer.appendChild(label);
6292
+ // A label on a narrow component may be wider than the component, so a component
6293
+ // near the right edge can push its label off-screen. Slide it back until its
6294
+ // right edge sits on the margin, never past the left edge.
6295
+ const width = label.getBoundingClientRect().width || label.offsetWidth;
6296
+ const limit = window.innerWidth - VIEWPORT_MARGIN;
6297
+ label.style.left = `${left + width > limit ? Math.max(0, Math.round(limit - width)) : left}px`;
6175
6298
  }
6176
6299
  }
6177
6300
  /** `$id=7 $title="Users"` - only strings, numbers and booleans, never internals. */
@@ -6201,7 +6324,7 @@ function on_click(event) {
6201
6324
  close_modal();
6202
6325
  return;
6203
6326
  }
6204
- open_modal(chain[0]);
6327
+ select(chain[0]);
6205
6328
  }
6206
6329
  function on_keydown(event) {
6207
6330
  if (event.key === 'Escape' && state.modal_component) {
@@ -6215,17 +6338,82 @@ function close_modal() {
6215
6338
  state.modal.classList.remove(`${PREFIX}modal-open`);
6216
6339
  state.modal.textContent = '';
6217
6340
  state.modal_component = null;
6341
+ state.nav_stack = [];
6342
+ set_selected(null);
6343
+ }
6344
+ // ---------------------------------------------------------------------------
6345
+ // Selection and navigation
6346
+ //
6347
+ // Three ways in, and they differ only in what they do to the navigation stack:
6348
+ // select() - a FRESH selection (page click, label click, inspect()): the stack
6349
+ // is dropped, so Back disappears.
6350
+ // navigate() - a step along the chain (Parent, an Ancestry entry, the
6351
+ // Instantiator link): the component being left is pushed.
6352
+ // show() - render only, stack untouched; what Back and the lifecycle
6353
+ // buttons re-open with.
6354
+ // ---------------------------------------------------------------------------
6355
+ function select(component) {
6356
+ state.nav_stack = [];
6357
+ show(component);
6358
+ }
6359
+ function navigate(component) {
6360
+ if (state.modal_component && state.modal_component !== component) {
6361
+ state.nav_stack.push(state.modal_component);
6362
+ }
6363
+ show(component);
6218
6364
  }
6219
- function open_modal(component) {
6365
+ /** Pop to the last component still alive; a stopped one is skipped, not shown. */
6366
+ function go_back() {
6367
+ while (state.nav_stack.length) {
6368
+ const previous = state.nav_stack.pop();
6369
+ if (previous.$.hasClass('_Component_Stopped'))
6370
+ continue;
6371
+ show(previous);
6372
+ return;
6373
+ }
6374
+ if (state.modal_component)
6375
+ show(state.modal_component); // nothing left: drop Back
6376
+ }
6377
+ /** Move SELECTED_CLASS to `element`, or take it off the page entirely for null. */
6378
+ function set_selected(element) {
6379
+ if (state.selected_element && state.selected_element !== element) {
6380
+ state.selected_element.classList.remove(SELECTED_CLASS);
6381
+ }
6382
+ state.selected_element = element;
6383
+ if (element)
6384
+ element.classList.add(SELECTED_CLASS);
6385
+ }
6386
+ /**
6387
+ * The inspector. Three children: a fixed title bar, a scrolling body holding every
6388
+ * section, and a fixed footer of lifecycle buttons. The modal sits top-right, but
6389
+ * moves to the left when the inspected component is itself in the right half of the
6390
+ * viewport - otherwise the panel covers the thing being inspected.
6391
+ */
6392
+ function show(component) {
6220
6393
  const modal = state.modal;
6221
6394
  modal.textContent = '';
6222
6395
  state.modal_component = component;
6223
6396
  const chain = chain_for(component.$[0]);
6224
6397
  const ancestors = chain.slice(1);
6225
6398
  const element = component.$[0];
6226
- // Title bar
6399
+ set_selected(element);
6400
+ modal.classList.toggle(`${PREFIX}modal-left`, element.getBoundingClientRect().left > window.innerWidth / 2);
6401
+ // Title bar: the name, then [Back] [Parent] [Log to console] [Close]. Back is
6402
+ // there only while something was navigated away from, Parent only while the
6403
+ // component has a DOM parent component - the first Ancestry entry.
6227
6404
  const title = el('div', 'title');
6228
6405
  title.appendChild(el('span', 'title-name', `<${component.component_name()}>`));
6406
+ if (state.nav_stack.length) {
6407
+ const back = el('button', 'button', 'Back');
6408
+ back.addEventListener('click', go_back);
6409
+ title.appendChild(back);
6410
+ }
6411
+ const parent = ancestors[0];
6412
+ if (parent) {
6413
+ const up = el('button', 'button', 'Parent');
6414
+ up.addEventListener('click', () => navigate(parent));
6415
+ title.appendChild(up);
6416
+ }
6229
6417
  const log = el('button', 'button', 'Log to console');
6230
6418
  log.addEventListener('click', () => {
6231
6419
  console.log(`[JQHTML debug] <${component.component_name()}>`, component, element);
@@ -6235,6 +6423,9 @@ function open_modal(component) {
6235
6423
  close.addEventListener('click', close_modal);
6236
6424
  title.appendChild(close);
6237
6425
  modal.appendChild(title);
6426
+ // The only scrolling element: the title bar and the footer stay put.
6427
+ const body = el('div', 'body');
6428
+ modal.appendChild(body);
6238
6429
  // Identity
6239
6430
  const ctor = component.constructor;
6240
6431
  const hierarchy = ctor.get_class_hierarchy ? ctor.get_class_hierarchy() : [ctor.name];
@@ -6243,15 +6434,18 @@ function open_modal(component) {
6243
6434
  row(identity, 'class', '', hierarchy.join(' → '));
6244
6435
  row(identity, 'element', '', describe_element(element));
6245
6436
  row(identity, '_cid', '', String(component._cid));
6246
- row(identity, 'lifecycle', '', READY_STATES[component._ready_state] || String(component._ready_state));
6437
+ const lifecycle = READY_STATES[component._ready_state] || String(component._ready_state);
6438
+ row(identity, 'lifecycle', '', component._debug_no_data
6439
+ ? `${lifecycle} (no data - reload w/o data)`
6440
+ : lifecycle);
6247
6441
  const nocache = element.getAttribute('data-nocache');
6248
6442
  if (nocache)
6249
6443
  row(identity, 'cache', '', `declined: ${nocache}`);
6250
- modal.appendChild(section('Identity', identity));
6444
+ body.appendChild(section('Identity', identity));
6251
6445
  // Args / data / state
6252
- modal.appendChild(section('Args', object_table(component.args)));
6253
- modal.appendChild(section('Data', object_table(component.data)));
6254
- modal.appendChild(section('State', object_table(component.state)));
6446
+ body.appendChild(section('Args', object_table(component.args)));
6447
+ body.appendChild(section('Data', object_table(component.data)));
6448
+ body.appendChild(section('State', object_table(component.state)));
6255
6449
  // Ancestry
6256
6450
  const list = el('div', 'list');
6257
6451
  if (!ancestors.length)
@@ -6260,10 +6454,10 @@ function open_modal(component) {
6260
6454
  const link = el('div', 'link');
6261
6455
  link.appendChild(el('span', 'link-name', `<${ancestor.component_name()}>`));
6262
6456
  link.appendChild(el('span', 'link-cid', String(ancestor._cid)));
6263
- link.addEventListener('click', () => open_modal(ancestor));
6457
+ link.addEventListener('click', () => navigate(ancestor));
6264
6458
  list.appendChild(link);
6265
6459
  }
6266
- modal.appendChild(section('Ancestry (DOM, nearest first)', list));
6460
+ body.appendChild(section('Ancestry (DOM, nearest first)', list));
6267
6461
  // Instantiator - the component whose template wrote this tag. Differs from
6268
6462
  // the DOM parent for markup written in a slot body or default content.
6269
6463
  const instantiator = component.instantiator ? component.instantiator() : null;
@@ -6272,7 +6466,7 @@ function open_modal(component) {
6272
6466
  const link = el('div', 'link');
6273
6467
  link.appendChild(el('span', 'link-name', `<${instantiator.component_name()}>`));
6274
6468
  link.appendChild(el('span', 'link-cid', String(instantiator._cid)));
6275
- link.addEventListener('click', () => open_modal(instantiator));
6469
+ link.addEventListener('click', () => navigate(instantiator));
6276
6470
  inst.appendChild(link);
6277
6471
  if (ancestors[0] && instantiator !== ancestors[0]) {
6278
6472
  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.'));
@@ -6281,9 +6475,49 @@ function open_modal(component) {
6281
6475
  else {
6282
6476
  inst.appendChild(el('div', 'empty', 'none - created directly'));
6283
6477
  }
6284
- modal.appendChild(section('Instantiator', inst));
6478
+ body.appendChild(section('Instantiator', inst));
6479
+ // Footer: lifecycle controls. Clicks land inside the shadow host, so the
6480
+ // overlay's own click hijack lets them through (is_ours).
6481
+ const footer = el('div', 'footer');
6482
+ footer.appendChild(el('span', 'footer-label', 'Lifecycle:'));
6483
+ footer.appendChild(lifecycle_button(component, 'Reload', (c) => c.reload()));
6484
+ footer.appendChild(lifecycle_button(component, 'Refresh', (c) => c.refresh()));
6485
+ footer.appendChild(lifecycle_button(component, 'Rerender', (c) => c.render()));
6486
+ footer.appendChild(lifecycle_button(component, 'Reload w/o data', (c) => c._reload_without_data()));
6487
+ modal.appendChild(footer);
6285
6488
  modal.classList.add(`${PREFIX}modal-open`);
6286
6489
  }
6490
+ /**
6491
+ * One footer button. Runs its lifecycle call on a live component, logs whatever it
6492
+ * throws or rejects with, and re-opens the modal once the call settles so the panel
6493
+ * (lifecycle row above all) describes the state the button just produced.
6494
+ */
6495
+ function lifecycle_button(component, label, run) {
6496
+ const button = el('button', 'button footer-button', label);
6497
+ button.addEventListener('click', () => {
6498
+ if (component.$.hasClass('_Component_Stopped'))
6499
+ return; // dead component: nothing to do
6500
+ let result;
6501
+ try {
6502
+ result = run(component);
6503
+ }
6504
+ catch (error) {
6505
+ console.error(`[JQHTML debug] ${label} threw on <${component.component_name()}>`, error);
6506
+ return;
6507
+ }
6508
+ Promise.resolve(result)
6509
+ .catch((error) => {
6510
+ console.error(`[JQHTML debug] ${label} rejected on <${component.component_name()}>`, error);
6511
+ })
6512
+ .then(() => {
6513
+ // `show`, not `select`: the lifecycle buttons re-render the panel in place
6514
+ // and must not throw away the navigation stack behind it.
6515
+ if (state.enabled && state.modal_component === component)
6516
+ show(component);
6517
+ });
6518
+ });
6519
+ return button;
6520
+ }
6287
6521
  // ---------------------------------------------------------------------------
6288
6522
  // DOM helpers (shadow side)
6289
6523
  // ---------------------------------------------------------------------------
@@ -7014,7 +7248,7 @@ function init(jQuery, config) {
7014
7248
  }
7015
7249
  }
7016
7250
  // Version - will be replaced during build with actual version from package.json
7017
- const version = '2.3.68';
7251
+ const version = '2.3.70';
7018
7252
  // Default export with all functionality
7019
7253
  const jqhtml = {
7020
7254
  // Core