@spaethtech/svelte-ui 0.7.1-dev.42.aca96e0 → 0.7.1-dev.44.97afb0d
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,9 @@
|
|
|
121
121
|
open = $bindable(false),
|
|
122
122
|
stepLevel = $bindable(0),
|
|
123
123
|
hamburger = true,
|
|
124
|
+
iconHoverExpand = false,
|
|
125
|
+
hoverExpandCloseMs = 200,
|
|
126
|
+
side = "left",
|
|
124
127
|
api = $bindable(),
|
|
125
128
|
class: additionalClass = "",
|
|
126
129
|
}: {
|
|
@@ -171,6 +174,15 @@
|
|
|
171
174
|
* and binds to `open`.
|
|
172
175
|
*/
|
|
173
176
|
hamburger?: boolean;
|
|
177
|
+
/** In `icon` mode, expand the rail to `expanded` ON HOVER — as a floating overlay over a spacer
|
|
178
|
+
* that reserves the collapsed width, so page content never reflows. Default `false`. */
|
|
179
|
+
iconHoverExpand?: boolean;
|
|
180
|
+
/** Exit delay (ms) before a hover-expanded rail collapses on mouse-leave. Default `200`. */
|
|
181
|
+
hoverExpandCloseMs?: number;
|
|
182
|
+
/** Which edge the sidebar docks to. `"right"` mirrors everything: the frame border, the drawer
|
|
183
|
+
* slide direction, the hamburger corner, popout side + border, and item layout (icon on the
|
|
184
|
+
* right, labels right-aligned). Default `"left"`. */
|
|
185
|
+
side?: "left" | "right";
|
|
174
186
|
/** Imperative handle (bindable). Call `api.reset()` from your router's navigation lifecycle
|
|
175
187
|
* (SvelteKit `afterNavigate`, etc.) so a programmatic `goto()`/back-button closes the drawer +
|
|
176
188
|
* any open popout and returns a stepped drawer to the top. */
|
|
@@ -224,7 +236,45 @@
|
|
|
224
236
|
|
|
225
237
|
const isStepped = $derived(currentMode === "stepped");
|
|
226
238
|
const isDrawer = $derived(currentMode === "drawer" || currentMode === "stepped");
|
|
227
|
-
|
|
239
|
+
// Dock side. `right` mirrors the frame, drawer slide, hamburger corner, popout side, and item layout.
|
|
240
|
+
const dockRight = $derived(side === "right");
|
|
241
|
+
const popoutSide = $derived<"left" | "right">(dockRight ? "left" : "right");
|
|
242
|
+
const dockEdge = $derived(dockRight ? "right-0" : "left-0");
|
|
243
|
+
const dockOff = $derived(dockRight ? "translate-x-full" : "-translate-x-full");
|
|
244
|
+
const dockBorder = $derived(
|
|
245
|
+
dockRight
|
|
246
|
+
? "[border-left:1px_solid_color-mix(in_srgb,var(--ui-color-text)_15%,transparent)]"
|
|
247
|
+
: "[border-right:1px_solid_color-mix(in_srgb,var(--ui-color-text)_15%,transparent)]",
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
// ── Icon-mode hover-expand ──────────────────────────────────────
|
|
251
|
+
// When enabled + in `icon` mode, hovering the rail floats it out to the `expanded` render OVER a
|
|
252
|
+
// spacer that reserves the collapsed width — page content never reflows. Exit is delayed.
|
|
253
|
+
let hoverExpanded = $state(false);
|
|
254
|
+
let hoverExpandTimer: ReturnType<typeof setTimeout> | null = null;
|
|
255
|
+
const hoverExpandActive = $derived(iconHoverExpand && currentMode === "icon");
|
|
256
|
+
const effectivelyExpanded = $derived(hoverExpandActive && hoverExpanded);
|
|
257
|
+
function openHoverExpand() {
|
|
258
|
+
if (hoverExpandTimer) {
|
|
259
|
+
clearTimeout(hoverExpandTimer);
|
|
260
|
+
hoverExpandTimer = null;
|
|
261
|
+
}
|
|
262
|
+
if (hoverExpandActive) hoverExpanded = true;
|
|
263
|
+
}
|
|
264
|
+
function scheduleHoverCollapse() {
|
|
265
|
+
if (hoverExpandTimer) clearTimeout(hoverExpandTimer);
|
|
266
|
+
hoverExpandTimer = setTimeout(() => {
|
|
267
|
+
hoverExpanded = false;
|
|
268
|
+
openId = null;
|
|
269
|
+
hoverExpandTimer = null;
|
|
270
|
+
}, hoverExpandCloseMs);
|
|
271
|
+
}
|
|
272
|
+
$effect(() => {
|
|
273
|
+
if (!hoverExpandActive) hoverExpanded = false;
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Labels show in every mode except a collapsed (not hover-expanded) icon rail.
|
|
277
|
+
const labelsVisible = $derived(currentMode !== "icon" || effectivelyExpanded);
|
|
228
278
|
|
|
229
279
|
// ── DEV depth warning ───────────────────────────────────────────
|
|
230
280
|
if (DEV) {
|
|
@@ -470,7 +520,7 @@
|
|
|
470
520
|
// height get clamped to the viewport by the `anchored` engine
|
|
471
521
|
// boundary. That case also needs in-popout scroll (TODO) and
|
|
472
522
|
// the border rule may need to account for viewport-edge cases.
|
|
473
|
-
let borderClass = "border-r";
|
|
523
|
+
let borderClass = dockRight ? "border-l" : "border-r";
|
|
474
524
|
if (group === "bottom") {
|
|
475
525
|
borderClass += spans ? " border-t" : " border-t border-b";
|
|
476
526
|
} else {
|
|
@@ -529,7 +579,9 @@
|
|
|
529
579
|
what `truncate` (overflow-hidden + text-ellipsis) needs to
|
|
530
580
|
actually clip overflow. Without `min-w-0` the label refuses
|
|
531
581
|
to shrink past its natural width and the row overflows. -->
|
|
532
|
-
<span class="flex-1 min-w-0 truncate {labelClassResolved} {sz.text}"
|
|
582
|
+
<span class="flex-1 min-w-0 truncate {dockRight ? 'text-right' : ''} {labelClassResolved} {sz.text}"
|
|
583
|
+
>{item.label}</span
|
|
584
|
+
>
|
|
533
585
|
{#if isParent}
|
|
534
586
|
<!-- Chevron — visible only when the label is visible (no point
|
|
535
587
|
dangling a `>` next to an icon-only icon). Rotates 180° to
|
|
@@ -555,7 +607,8 @@
|
|
|
555
607
|
{@const isParent = (hasChildren || !!item.popout) && !opts.isPopoutChild}
|
|
556
608
|
{@const hasLink = !!item.link}
|
|
557
609
|
{@const isOpen = !!opts.id && openId === opts.id}
|
|
558
|
-
{@const
|
|
610
|
+
{@const popoutId = opts.id ? "sbm-popout-" + opts.id.replace(":", "-") : undefined}
|
|
611
|
+
{@const rowClass = `group flex items-center w-full ${sz.row} ${dockRight ? "flex-row-reverse lg:pl-3" : "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
612
|
{#if hasLink}
|
|
560
613
|
<a
|
|
561
614
|
href={item.link}
|
|
@@ -563,6 +616,9 @@
|
|
|
563
616
|
class={rowClass}
|
|
564
617
|
data-sveltekit-reload={item.reload ? "" : undefined}
|
|
565
618
|
aria-current={ownActive ? "page" : undefined}
|
|
619
|
+
aria-haspopup={isParent ? "menu" : undefined}
|
|
620
|
+
aria-expanded={isParent && opts.id ? isOpen : undefined}
|
|
621
|
+
aria-controls={isParent && isOpen ? popoutId : undefined}
|
|
566
622
|
use:tooltip={{
|
|
567
623
|
text: item.label,
|
|
568
624
|
side: "right",
|
|
@@ -601,6 +657,7 @@
|
|
|
601
657
|
class="{rowClass} text-left"
|
|
602
658
|
aria-haspopup="menu"
|
|
603
659
|
aria-expanded={openId === opts.id}
|
|
660
|
+
aria-controls={isOpen ? popoutId : undefined}
|
|
604
661
|
use:tooltip={{ text: item.label, side: "right", disabled: openId === opts.id }}
|
|
605
662
|
onmouseenter={!isStepped ? () => openOnHover(opts.id!) : undefined}
|
|
606
663
|
onmouseleave={!isStepped ? scheduleClose : undefined}
|
|
@@ -636,11 +693,12 @@
|
|
|
636
693
|
mirrors main strip styling. `borderClass` is computed per popout
|
|
637
694
|
by `geometryFor` so the visible edges get borders and the edges
|
|
638
695
|
that coincide with the sidebar's top/bottom don't. -->
|
|
639
|
-
{#snippet popout(item: SideBarMenuItem, borderClass: string)}
|
|
696
|
+
{#snippet popout(item: SideBarMenuItem, borderClass: string, popoutId: string)}
|
|
640
697
|
<!-- svelte-ignore a11y_interactive_supports_focus -->
|
|
641
698
|
<div
|
|
642
699
|
role="menu"
|
|
643
700
|
tabindex="-1"
|
|
701
|
+
id={popoutId}
|
|
644
702
|
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
703
|
onmouseenter={cancelClose}
|
|
646
704
|
onmouseleave={scheduleClose}
|
|
@@ -665,7 +723,9 @@
|
|
|
665
723
|
to suppress this when the consumer provides its own trigger. -->
|
|
666
724
|
<button
|
|
667
725
|
type="button"
|
|
668
|
-
class="fixed top-3
|
|
726
|
+
class="fixed top-3 {dockRight
|
|
727
|
+
? 'right-3'
|
|
728
|
+
: 'left-3'} z-[80] inline-flex w-10 h-10 items-center justify-center cursor-pointer [background-color:var(--ui-color-background)] border [border-color:color-mix(in_srgb,var(--ui-color-text)_15%,transparent)] shadow-md [&_svg]:w-5 [&_svg]:h-5"
|
|
669
729
|
aria-label={open ? "Close menu" : "Open menu"}
|
|
670
730
|
aria-expanded={open}
|
|
671
731
|
onclick={() => (open = !open)}
|
|
@@ -685,24 +745,34 @@
|
|
|
685
745
|
></div>
|
|
686
746
|
{/if}
|
|
687
747
|
|
|
688
|
-
|
|
689
|
-
|
|
748
|
+
<!-- Root wrapper. In icon hover-expand it becomes a `relative` spacer reserving the collapsed rail
|
|
749
|
+
width, so the expanded float below is positioned against the SIDEBAR's own box (container-relative,
|
|
750
|
+
never the viewport). In every other mode it's `display: contents` (no box) so layout is unchanged. -->
|
|
751
|
+
<div
|
|
752
|
+
class={hoverExpandActive ? "relative shrink-0 [width:var(--sbm-collapsed-w)]" : "contents"}
|
|
690
753
|
style="--sbm-collapsed-w: {collapsedWidth}px; --sbm-expanded-w: {expandedWidth}px;"
|
|
691
|
-
|
|
754
|
+
>
|
|
755
|
+
<aside
|
|
756
|
+
bind:this={asideRef}
|
|
757
|
+
onmouseenter={hoverExpandActive ? openHoverExpand : undefined}
|
|
758
|
+
onmouseleave={hoverExpandActive ? scheduleHoverCollapse : undefined}
|
|
759
|
+
class="
|
|
692
760
|
flex flex-col z-[60] overflow-hidden
|
|
693
761
|
[background-color:var(--ui-color-background)]
|
|
694
762
|
[color:var(--ui-color-text)]
|
|
695
|
-
|
|
763
|
+
{dockBorder}
|
|
696
764
|
{currentMode === 'stepped'
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
765
|
+
? `absolute inset-y-0 ${dockEdge} w-full z-[75] transition-transform duration-200 ${open ? 'translate-x-0' : dockOff}`
|
|
766
|
+
: currentMode === 'drawer'
|
|
767
|
+
? `absolute inset-y-0 ${dockEdge} w-[var(--sbm-expanded-w)] z-[75] transition-transform duration-200 ${open ? 'translate-x-0' : dockOff}`
|
|
768
|
+
: currentMode === 'icon'
|
|
769
|
+
? hoverExpandActive
|
|
770
|
+
? `absolute inset-y-0 ${dockEdge} z-[65] transition-[width] duration-200 ${effectivelyExpanded ? 'w-[var(--sbm-expanded-w)] shadow-lg' : 'w-[var(--sbm-collapsed-w)]'}`
|
|
771
|
+
: 'relative w-[var(--sbm-collapsed-w)]'
|
|
772
|
+
: 'relative w-[var(--sbm-expanded-w)]'}
|
|
703
773
|
{additionalClass}
|
|
704
774
|
"
|
|
705
|
-
>
|
|
775
|
+
>
|
|
706
776
|
{#if isStepped && stepLevel === 1 && currentParent}
|
|
707
777
|
<!-- Stepped sub-view (<2xs only): show just the children of the
|
|
708
778
|
parent the user tapped. The consumer's hamburger button is
|
|
@@ -729,14 +799,14 @@
|
|
|
729
799
|
<Popup
|
|
730
800
|
anchor={geom.anchor}
|
|
731
801
|
open={openId === id}
|
|
732
|
-
side=
|
|
802
|
+
side={popoutSide}
|
|
733
803
|
align={geom.align}
|
|
734
804
|
offset={0}
|
|
735
805
|
onclose={() => {
|
|
736
806
|
if (openId === id) openId = null;
|
|
737
807
|
}}
|
|
738
808
|
>
|
|
739
|
-
{@render popout(item, geom.borderClass)}
|
|
809
|
+
{@render popout(item, geom.borderClass, "sbm-popout-" + id.replace(":", "-"))}
|
|
740
810
|
</Popup>
|
|
741
811
|
{/if}
|
|
742
812
|
{/each}
|
|
@@ -765,18 +835,19 @@
|
|
|
765
835
|
<Popup
|
|
766
836
|
anchor={geom.anchor}
|
|
767
837
|
open={openId === id}
|
|
768
|
-
side=
|
|
838
|
+
side={popoutSide}
|
|
769
839
|
align={geom.align}
|
|
770
840
|
offset={0}
|
|
771
841
|
onclose={() => {
|
|
772
842
|
if (openId === id) openId = null;
|
|
773
843
|
}}
|
|
774
844
|
>
|
|
775
|
-
{@render popout(item, geom.borderClass)}
|
|
845
|
+
{@render popout(item, geom.borderClass, "sbm-popout-" + id.replace(":", "-"))}
|
|
776
846
|
</Popup>
|
|
777
847
|
{/if}
|
|
778
848
|
{/each}
|
|
779
849
|
</ul>
|
|
780
850
|
{/if}
|
|
781
851
|
{/if}
|
|
782
|
-
</aside>
|
|
852
|
+
</aside>
|
|
853
|
+
</div>
|
|
@@ -122,6 +122,15 @@ 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;
|
|
130
|
+
/** Which edge the sidebar docks to. `"right"` mirrors everything: the frame border, the drawer
|
|
131
|
+
* slide direction, the hamburger corner, popout side + border, and item layout (icon on the
|
|
132
|
+
* right, labels right-aligned). Default `"left"`. */
|
|
133
|
+
side?: "left" | "right";
|
|
125
134
|
/** Imperative handle (bindable). Call `api.reset()` from your router's navigation lifecycle
|
|
126
135
|
* (SvelteKit `afterNavigate`, etc.) so a programmatic `goto()`/back-button closes the drawer +
|
|
127
136
|
* any open popout and returns a stepped drawer to the top. */
|