@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
|
-
|
|
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
|
-
|
|
689
|
-
|
|
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
|
-
|
|
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
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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. */
|