@joewinke/jatui 0.8.4 → 0.8.6

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.
@@ -66,6 +66,12 @@
66
66
  * ArrowRight from the parent explicitly focuses into the submenu.
67
67
  */
68
68
  autoFocus?: boolean;
69
+ /**
70
+ * Called when an action item (not a submenu owner) is chosen, before
71
+ * this menu closes. A nested submenu uses it to close the WHOLE menu
72
+ * chain on a pick; Escape/ArrowLeft in a submenu still close only it.
73
+ */
74
+ onItemSelected?: () => void;
69
75
  }
70
76
 
71
77
  let {
@@ -77,7 +83,8 @@
77
83
  zIndex = 70,
78
84
  class: className = '',
79
85
  onClose,
80
- autoFocus = true
86
+ autoFocus = true,
87
+ onItemSelected
81
88
  }: Props = $props();
82
89
 
83
90
  // Submenu state — index into `items` of the currently-open submenu (if any),
@@ -124,6 +131,7 @@
124
131
  submitAction(item.submit);
125
132
  }
126
133
  item.onSelect?.();
134
+ onItemSelected?.();
127
135
  close();
128
136
  }
129
137
 
@@ -397,6 +405,10 @@
397
405
  zIndex={zIndex + 1}
398
406
  autoFocus={false}
399
407
  onClose={closeSubmenu}
408
+ onItemSelected={() => {
409
+ onItemSelected?.();
410
+ close();
411
+ }}
400
412
  />
401
413
  {/if}
402
414
  </div>
@@ -24,6 +24,12 @@ interface Props {
24
24
  * ArrowRight from the parent explicitly focuses into the submenu.
25
25
  */
26
26
  autoFocus?: boolean;
27
+ /**
28
+ * Called when an action item (not a submenu owner) is chosen, before
29
+ * this menu closes. A nested submenu uses it to close the WHOLE menu
30
+ * chain on a pick; Escape/ArrowLeft in a submenu still close only it.
31
+ */
32
+ onItemSelected?: () => void;
27
33
  }
28
34
  declare const ContextMenu: import("svelte").Component<Props, {}, "open">;
29
35
  type ContextMenu = ReturnType<typeof ContextMenu>;
@@ -197,6 +197,35 @@
197
197
  }
198
198
  }
199
199
 
200
+ function positionPanel() {
201
+ if (!triggerRef) return;
202
+ const rect = triggerRef.getBoundingClientRect();
203
+ const width = Math.max(rect.width, 192);
204
+ // Viewport-aware flip: if there isn't room below the trigger but there
205
+ // is above, anchor the panel to the trigger's top and grow upward. This
206
+ // keeps the panel on-screen for rows near the bottom of a scroll area
207
+ // (e.g. suggested-task rows in the completed-session overlay).
208
+ const estPanelHeight = 300;
209
+ const spaceBelow = window.innerHeight - rect.bottom;
210
+ const spaceAbove = rect.top;
211
+ const flipUp = spaceBelow < estPanelHeight && spaceAbove > spaceBelow;
212
+ // Clamp left so a right-edge trigger doesn't push the panel off-screen.
213
+ const left = Math.max(8, Math.min(rect.left, window.innerWidth - width - 8));
214
+ // .sd-panel's class rule always sets `top: 100%`, so each branch must
215
+ // explicitly reset the property it isn't using — otherwise the flip-up
216
+ // branch ends up with both top (100% = viewport height) and bottom set
217
+ // while height is auto, which over-constrains to a negative (clamped
218
+ // to 0) height and the whole panel silently renders invisible.
219
+ const vertical = flipUp
220
+ ? `bottom: ${window.innerHeight - rect.top + 4}px; top: auto;`
221
+ : `top: ${rect.bottom + 4}px; bottom: auto;`;
222
+ // min-width override is required: .sd-panel's class rule sets
223
+ // `min-width: max(100%, 14rem)`, and for a position:fixed element
224
+ // that 100% resolves against the viewport, not the trigger — without
225
+ // this override the panel balloons to ~100vw wide.
226
+ panelStyle = `position: fixed; z-index: 9999; ${vertical} left: ${left}px; width: ${width}px; min-width: ${width}px; margin-top: 0;`;
227
+ }
228
+
200
229
  $effect(() => {
201
230
  if (embedded) {
202
231
  tick().then(() => { searchInput?.focus(); scrollHighlightedIntoView(); });
@@ -204,31 +233,26 @@
204
233
  }
205
234
  if (open) {
206
235
  if (appendToBody && triggerRef) {
207
- const rect = triggerRef.getBoundingClientRect();
208
- const width = Math.max(rect.width, 192);
209
- // Viewport-aware flip: if there isn't room below the trigger but there
210
- // is above, anchor the panel to the trigger's top and grow upward. This
211
- // keeps the panel on-screen for rows near the bottom of a scroll area
212
- // (e.g. suggested-task rows in the completed-session overlay).
213
- const estPanelHeight = 300;
214
- const spaceBelow = window.innerHeight - rect.bottom;
215
- const spaceAbove = rect.top;
216
- const flipUp = spaceBelow < estPanelHeight && spaceAbove > spaceBelow;
217
- // Clamp left so a right-edge trigger doesn't push the panel off-screen.
218
- const left = Math.max(8, Math.min(rect.left, window.innerWidth - width - 8));
219
- // .sd-panel's class rule always sets `top: 100%`, so each branch must
220
- // explicitly reset the property it isn't using — otherwise the flip-up
221
- // branch ends up with both top (100% = viewport height) and bottom set
222
- // while height is auto, which over-constrains to a negative (clamped
223
- // to 0) height and the whole panel silently renders invisible.
224
- const vertical = flipUp
225
- ? `bottom: ${window.innerHeight - rect.top + 4}px; top: auto;`
226
- : `top: ${rect.bottom + 4}px; bottom: auto;`;
227
- // min-width override is required: .sd-panel's class rule sets
228
- // `min-width: max(100%, 14rem)`, and for a position:fixed element
229
- // that 100% resolves against the viewport, not the trigger — without
230
- // this override the panel balloons to ~100vw wide.
231
- panelStyle = `position: fixed; z-index: 9999; ${vertical} left: ${left}px; width: ${width}px; min-width: ${width}px; margin-top: 0;`;
236
+ positionPanel();
237
+ // A position:fixed panel is anchored to the VIEWPORT, so without this it
238
+ // stays put while the page scrolls and appears to follow the reader down
239
+ // the page (Joe UAT 2026-09-25). Re-anchor on every scroll (capture: any
240
+ // scroll container) and resize; close once the trigger leaves the view.
241
+ const reanchor = (e?: Event) => {
242
+ if (e && e.target instanceof Node && containerRef?.contains(e.target)) return;
243
+ const r = triggerRef?.getBoundingClientRect();
244
+ if (!r || r.bottom < 0 || r.top > window.innerHeight) { open = false; searchQuery = ''; return; }
245
+ positionPanel();
246
+ };
247
+ window.addEventListener('scroll', reanchor, { capture: true, passive: true });
248
+ window.addEventListener('resize', reanchor, { passive: true });
249
+ document.addEventListener('mousedown', handleClickOutside);
250
+ tick().then(() => { searchInput?.focus(); scrollHighlightedIntoView(); });
251
+ return () => {
252
+ window.removeEventListener('scroll', reanchor, { capture: true });
253
+ window.removeEventListener('resize', reanchor);
254
+ document.removeEventListener('mousedown', handleClickOutside);
255
+ };
232
256
  }
233
257
  document.addEventListener('mousedown', handleClickOutside);
234
258
  tick().then(() => { searchInput?.focus(); scrollHighlightedIntoView(); });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joewinke/jatui",
3
- "version": "0.8.4",
3
+ "version": "0.8.6",
4
4
  "private": false,
5
5
  "description": "Shared Svelte 5 component library for JAT projects",
6
6
  "type": "module",