@spaethtech/svelte-ui 0.7.1-dev.42.aca96e0 → 0.7.1-dev.43.72e77ad

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.
@@ -121,6 +121,8 @@
121
121
  open = $bindable(false),
122
122
  stepLevel = $bindable(0),
123
123
  hamburger = true,
124
+ iconHoverExpand = false,
125
+ hoverExpandCloseMs = 200,
124
126
  api = $bindable(),
125
127
  class: additionalClass = "",
126
128
  }: {
@@ -171,6 +173,11 @@
171
173
  * and binds to `open`.
172
174
  */
173
175
  hamburger?: boolean;
176
+ /** In `icon` mode, expand the rail to `expanded` ON HOVER — as a floating overlay over a spacer
177
+ * that reserves the collapsed width, so page content never reflows. Default `false`. */
178
+ iconHoverExpand?: boolean;
179
+ /** Exit delay (ms) before a hover-expanded rail collapses on mouse-leave. Default `200`. */
180
+ hoverExpandCloseMs?: number;
174
181
  /** Imperative handle (bindable). Call `api.reset()` from your router's navigation lifecycle
175
182
  * (SvelteKit `afterNavigate`, etc.) so a programmatic `goto()`/back-button closes the drawer +
176
183
  * any open popout and returns a stepped drawer to the top. */
@@ -224,7 +231,35 @@
224
231
 
225
232
  const isStepped = $derived(currentMode === "stepped");
226
233
  const isDrawer = $derived(currentMode === "drawer" || currentMode === "stepped");
227
- const labelsVisible = $derived(currentMode !== "icon");
234
+
235
+ // ── Icon-mode hover-expand ──────────────────────────────────────
236
+ // When enabled + in `icon` mode, hovering the rail floats it out to the `expanded` render OVER a
237
+ // spacer that reserves the collapsed width — page content never reflows. Exit is delayed.
238
+ let hoverExpanded = $state(false);
239
+ let hoverExpandTimer: ReturnType<typeof setTimeout> | null = null;
240
+ const hoverExpandActive = $derived(iconHoverExpand && currentMode === "icon");
241
+ const effectivelyExpanded = $derived(hoverExpandActive && hoverExpanded);
242
+ function openHoverExpand() {
243
+ if (hoverExpandTimer) {
244
+ clearTimeout(hoverExpandTimer);
245
+ hoverExpandTimer = null;
246
+ }
247
+ if (hoverExpandActive) hoverExpanded = true;
248
+ }
249
+ function scheduleHoverCollapse() {
250
+ if (hoverExpandTimer) clearTimeout(hoverExpandTimer);
251
+ hoverExpandTimer = setTimeout(() => {
252
+ hoverExpanded = false;
253
+ openId = null;
254
+ hoverExpandTimer = null;
255
+ }, hoverExpandCloseMs);
256
+ }
257
+ $effect(() => {
258
+ if (!hoverExpandActive) hoverExpanded = false;
259
+ });
260
+
261
+ // Labels show in every mode except a collapsed (not hover-expanded) icon rail.
262
+ const labelsVisible = $derived(currentMode !== "icon" || effectivelyExpanded);
228
263
 
229
264
  // ── DEV depth warning ───────────────────────────────────────────
230
265
  if (DEV) {
@@ -555,6 +590,7 @@
555
590
  {@const isParent = (hasChildren || !!item.popout) && !opts.isPopoutChild}
556
591
  {@const hasLink = !!item.link}
557
592
  {@const isOpen = !!opts.id && openId === opts.id}
593
+ {@const popoutId = opts.id ? "sbm-popout-" + opts.id.replace(":", "-") : undefined}
558
594
  {@const rowClass = `group flex items-center w-full ${sz.row} lg:pr-3 transition-colors cursor-pointer ${active ? "[background-color:var(--ui-color-primary)] text-white" : isOpen ? "[background-color:color-mix(in_srgb,var(--ui-color-text)_10%,transparent)]" : "hover:[background-color:color-mix(in_srgb,var(--ui-color-text)_10%,transparent)]"}`}
559
595
  {#if hasLink}
560
596
  <a
@@ -563,6 +599,9 @@
563
599
  class={rowClass}
564
600
  data-sveltekit-reload={item.reload ? "" : undefined}
565
601
  aria-current={ownActive ? "page" : undefined}
602
+ aria-haspopup={isParent ? "menu" : undefined}
603
+ aria-expanded={isParent && opts.id ? isOpen : undefined}
604
+ aria-controls={isParent && isOpen ? popoutId : undefined}
566
605
  use:tooltip={{
567
606
  text: item.label,
568
607
  side: "right",
@@ -601,6 +640,7 @@
601
640
  class="{rowClass} text-left"
602
641
  aria-haspopup="menu"
603
642
  aria-expanded={openId === opts.id}
643
+ aria-controls={isOpen ? popoutId : undefined}
604
644
  use:tooltip={{ text: item.label, side: "right", disabled: openId === opts.id }}
605
645
  onmouseenter={!isStepped ? () => openOnHover(opts.id!) : undefined}
606
646
  onmouseleave={!isStepped ? scheduleClose : undefined}
@@ -636,11 +676,12 @@
636
676
  mirrors main strip styling. `borderClass` is computed per popout
637
677
  by `geometryFor` so the visible edges get borders and the edges
638
678
  that coincide with the sidebar's top/bottom don't. -->
639
- {#snippet popout(item: SideBarMenuItem, borderClass: string)}
679
+ {#snippet popout(item: SideBarMenuItem, borderClass: string, popoutId: string)}
640
680
  <!-- svelte-ignore a11y_interactive_supports_focus -->
641
681
  <div
642
682
  role="menu"
643
683
  tabindex="-1"
684
+ id={popoutId}
644
685
  class="w-[var(--sbm-expanded-w)] overflow-hidden shadow-lg [background-color:var(--ui-color-background)] [color:var(--ui-color-text)] [border-color:color-mix(in_srgb,var(--ui-color-text)_15%,transparent)] {borderClass}"
645
686
  onmouseenter={cancelClose}
646
687
  onmouseleave={scheduleClose}
@@ -685,24 +726,34 @@
685
726
  ></div>
686
727
  {/if}
687
728
 
688
- <aside
689
- bind:this={asideRef}
729
+ <!-- Root wrapper. In icon hover-expand it becomes a `relative` spacer reserving the collapsed rail
730
+ width, so the expanded float below is positioned against the SIDEBAR's own box (container-relative,
731
+ never the viewport). In every other mode it's `display: contents` (no box) so layout is unchanged. -->
732
+ <div
733
+ class={hoverExpandActive ? "relative shrink-0 [width:var(--sbm-collapsed-w)]" : "contents"}
690
734
  style="--sbm-collapsed-w: {collapsedWidth}px; --sbm-expanded-w: {expandedWidth}px;"
691
- class="
735
+ >
736
+ <aside
737
+ bind:this={asideRef}
738
+ onmouseenter={hoverExpandActive ? openHoverExpand : undefined}
739
+ onmouseleave={hoverExpandActive ? scheduleHoverCollapse : undefined}
740
+ class="
692
741
  flex flex-col z-[60] overflow-hidden
693
742
  [background-color:var(--ui-color-background)]
694
743
  [color:var(--ui-color-text)]
695
744
  [border-right:1px_solid_color-mix(in_srgb,var(--ui-color-text)_15%,transparent)]
696
745
  {currentMode === 'stepped'
697
- ? `absolute inset-y-0 left-0 w-full z-[75] transition-transform duration-200 ${open ? 'translate-x-0' : '-translate-x-full'}`
698
- : currentMode === 'drawer'
699
- ? `absolute inset-y-0 left-0 w-[var(--sbm-expanded-w)] z-[75] transition-transform duration-200 ${open ? 'translate-x-0' : '-translate-x-full'}`
700
- : currentMode === 'icon'
701
- ? 'relative w-[var(--sbm-collapsed-w)]'
702
- : 'relative w-[var(--sbm-expanded-w)]'}
746
+ ? `absolute inset-y-0 left-0 w-full z-[75] transition-transform duration-200 ${open ? 'translate-x-0' : '-translate-x-full'}`
747
+ : currentMode === 'drawer'
748
+ ? `absolute inset-y-0 left-0 w-[var(--sbm-expanded-w)] z-[75] transition-transform duration-200 ${open ? 'translate-x-0' : '-translate-x-full'}`
749
+ : currentMode === 'icon'
750
+ ? hoverExpandActive
751
+ ? `absolute inset-y-0 left-0 z-[65] transition-[width] duration-200 ${effectivelyExpanded ? 'w-[var(--sbm-expanded-w)] shadow-lg' : 'w-[var(--sbm-collapsed-w)]'}`
752
+ : 'relative w-[var(--sbm-collapsed-w)]'
753
+ : 'relative w-[var(--sbm-expanded-w)]'}
703
754
  {additionalClass}
704
755
  "
705
- >
756
+ >
706
757
  {#if isStepped && stepLevel === 1 && currentParent}
707
758
  <!-- Stepped sub-view (<2xs only): show just the children of the
708
759
  parent the user tapped. The consumer's hamburger button is
@@ -736,7 +787,7 @@
736
787
  if (openId === id) openId = null;
737
788
  }}
738
789
  >
739
- {@render popout(item, geom.borderClass)}
790
+ {@render popout(item, geom.borderClass, "sbm-popout-" + id.replace(":", "-"))}
740
791
  </Popup>
741
792
  {/if}
742
793
  {/each}
@@ -772,11 +823,12 @@
772
823
  if (openId === id) openId = null;
773
824
  }}
774
825
  >
775
- {@render popout(item, geom.borderClass)}
826
+ {@render popout(item, geom.borderClass, "sbm-popout-" + id.replace(":", "-"))}
776
827
  </Popup>
777
828
  {/if}
778
829
  {/each}
779
830
  </ul>
780
831
  {/if}
781
832
  {/if}
782
- </aside>
833
+ </aside>
834
+ </div>
@@ -122,6 +122,11 @@ type $$ComponentProps = {
122
122
  * and binds to `open`.
123
123
  */
124
124
  hamburger?: boolean;
125
+ /** In `icon` mode, expand the rail to `expanded` ON HOVER — as a floating overlay over a spacer
126
+ * that reserves the collapsed width, so page content never reflows. Default `false`. */
127
+ iconHoverExpand?: boolean;
128
+ /** Exit delay (ms) before a hover-expanded rail collapses on mouse-leave. Default `200`. */
129
+ hoverExpandCloseMs?: number;
125
130
  /** Imperative handle (bindable). Call `api.reset()` from your router's navigation lifecycle
126
131
  * (SvelteKit `afterNavigate`, etc.) so a programmatic `goto()`/back-button closes the drawer +
127
132
  * any open popout and returns a stepped drawer to the top. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaethtech/svelte-ui",
3
- "version": "0.7.1-dev.42.aca96e0",
3
+ "version": "0.7.1-dev.43.72e77ad",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/spaethtech/svelte-ui.git"