@spaethtech/svelte-ui 0.7.1-dev.41.d01009c → 0.7.1-dev.42.aca96e0

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.
@@ -4,7 +4,7 @@
4
4
  * bottom-anchored group (pushed to the strip's bottom edge), and hover-open
5
5
  * popouts for parent items.
6
6
  *
7
- * Driven by data — pass `topItems` / `bottomItems` arrays of {@link SideBarMenuItem}.
7
+ * Driven by data — pass `items` / `bottomItems` arrays of {@link SideBarMenuItem}.
8
8
  * Each item declares its own icon (Snippet), label, link, badge, and optional
9
9
  * children. Children render in a top-layer Popup on hover or click.
10
10
  *
@@ -33,6 +33,18 @@
33
33
  */
34
34
  export type SideBarMenuMode = "stepped" | "drawer" | "icon" | "expanded";
35
35
 
36
+ /** Imperative handle exposed via `bind:api`. */
37
+ export type SideBarMenuApi = {
38
+ /** Reset all transient state — close the drawer, clear the open popout, return step level to 0. */
39
+ reset: () => void;
40
+ /** Open the drawer (drawer/stepped modes). */
41
+ open: () => void;
42
+ /** Close the drawer. */
43
+ close: () => void;
44
+ /** Set the stepped-mode step level (`0` = top). */
45
+ setStepLevel: (n: number) => void;
46
+ };
47
+
36
48
  export type SideBarMenuBadgeVariant = "error" | "warning" | "info" | "success";
37
49
 
38
50
  export type SideBarMenuBadge = {
@@ -69,6 +81,15 @@
69
81
  badge?: SideBarMenuBadge;
70
82
  /** v1 renders only the first level; deeper levels emit a DEV warning. */
71
83
  children?: SideBarMenuItem[];
84
+ /** Action row (no `link`) — renders a `<button>` and calls this on click. For a "toggle"
85
+ * row (an AI panel, etc.) or a non-navigating action. Runs after the drawer/popout reset. */
86
+ onclick?: (e: MouseEvent) => void;
87
+ /** Custom popout content, rendered INSTEAD of `children` in the popout (separators, hotkeys,
88
+ * an account/identity block…). The row still needs an `icon`/`label`; this is just its popout. */
89
+ popout?: import("svelte").Snippet;
90
+ /** Opt this anchor out of client-side routing — stamps `data-sveltekit-reload` (SvelteKit) so
91
+ * the link forces a full-browser navigation (sign-out endpoints, downloads, external redirects). */
92
+ reload?: boolean;
72
93
  };
73
94
  </script>
74
95
 
@@ -89,7 +110,7 @@
89
110
  type ModesMap = Partial<Record<Breakpoint, SideBarMenuMode>>;
90
111
 
91
112
  let {
92
- topItems = [],
113
+ items = [],
93
114
  bottomItems = [],
94
115
  currentPath,
95
116
  size = "md",
@@ -100,9 +121,10 @@
100
121
  open = $bindable(false),
101
122
  stepLevel = $bindable(0),
102
123
  hamburger = true,
124
+ api = $bindable(),
103
125
  class: additionalClass = "",
104
126
  }: {
105
- topItems?: SideBarMenuItem[];
127
+ items?: SideBarMenuItem[];
106
128
  bottomItems?: SideBarMenuItem[];
107
129
  currentPath: string;
108
130
  size?: Size;
@@ -149,6 +171,10 @@
149
171
  * and binds to `open`.
150
172
  */
151
173
  hamburger?: boolean;
174
+ /** Imperative handle (bindable). Call `api.reset()` from your router's navigation lifecycle
175
+ * (SvelteKit `afterNavigate`, etc.) so a programmatic `goto()`/back-button closes the drawer +
176
+ * any open popout and returns a stepped drawer to the top. */
177
+ api?: SideBarMenuApi;
152
178
  class?: string;
153
179
  } = $props();
154
180
 
@@ -202,23 +228,23 @@
202
228
 
203
229
  // ── DEV depth warning ───────────────────────────────────────────
204
230
  if (DEV) {
205
- const walk = (items: SideBarMenuItem[], depth: number) => {
206
- for (const it of items) {
231
+ const walk = (list: SideBarMenuItem[], depth: number, path: string[]) => {
232
+ for (const it of list) {
233
+ const here = [...path, it.label];
207
234
  if (it.children?.length) {
208
235
  if (depth >= 1) {
209
236
  console.warn(
210
- 'SideBarMenu: nesting deeper than 1 level is not yet rendered; flattening grandchildren of "' +
211
- it.label +
212
- '"',
237
+ `SideBarMenu: nesting deeper than 1 level is not yet rendered; flattening ` +
238
+ `grandchildren of "${here.join(" › ")}"`,
213
239
  );
214
240
  return;
215
241
  }
216
- walk(it.children, depth + 1);
242
+ walk(it.children, depth + 1, here);
217
243
  }
218
244
  }
219
245
  };
220
- walk(topItems, 0);
221
- walk(bottomItems, 0);
246
+ walk(items, 0, []);
247
+ walk(bottomItems, 0, []);
222
248
  }
223
249
 
224
250
  // ── Size axis ───────────────────────────────────────────────────
@@ -327,6 +353,15 @@
327
353
  stepLevel = 1;
328
354
  }
329
355
 
356
+ // Imperative handle for consumers (bound via `bind:api`). Assigned once — the closures read the
357
+ // reactive `open`/`stepLevel`/`openId` live, so a single assignment stays correct.
358
+ api = {
359
+ reset: closeAfterNav,
360
+ open: () => (open = true),
361
+ close: () => (open = false),
362
+ setStepLevel: (n: number) => (stepLevel = n),
363
+ };
364
+
330
365
  // Label / chevron visibility are now driven by the active mode
331
366
  // instead of by responsive class strings — the matchMedia effect
332
367
  // above keeps `currentMode` (and therefore `labelsVisible`) in sync
@@ -503,18 +538,21 @@
503
538
  `chevronVisibility` (not `labelClassResolved`) so the span
504
539
  stays `inline-flex` and the SVG centers vertically. -->
505
540
  <span
506
- class="shrink-0 items-center justify-center opacity-60 {sz.chevronBox} {sz.chevronSvg} {chevronClass}"
541
+ class="shrink-0 items-center justify-center opacity-60 transition-transform duration-150 {isOpen
542
+ ? 'rotate-180'
543
+ : ''} {sz.chevronBox} {sz.chevronSvg} {chevronClass}"
507
544
  >
508
545
  <IconChevronRight />
509
546
  </span>
510
547
  {/if}
511
548
  {/snippet}
512
549
 
513
- <!-- Single row template — used by topItems / bottomItems and the popout. -->
550
+ <!-- Single row template — used by items / bottomItems and the popout. -->
514
551
  {#snippet row(item: SideBarMenuItem, opts: { id?: ItemId; isPopoutChild?: boolean })}
515
552
  {@const active = isItemActive(item)}
553
+ {@const ownActive = !!item.link && currentPath.startsWith(item.link)}
516
554
  {@const hasChildren = (item.children?.length ?? 0) > 0}
517
- {@const isParent = hasChildren && !opts.isPopoutChild}
555
+ {@const isParent = (hasChildren || !!item.popout) && !opts.isPopoutChild}
518
556
  {@const hasLink = !!item.link}
519
557
  {@const isOpen = !!opts.id && openId === opts.id}
520
558
  {@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)]"}`}
@@ -523,7 +561,8 @@
523
561
  href={item.link}
524
562
  use:refIntoMap={opts.id}
525
563
  class={rowClass}
526
- aria-current={active ? "page" : undefined}
564
+ data-sveltekit-reload={item.reload ? "" : undefined}
565
+ aria-current={ownActive ? "page" : undefined}
527
566
  use:tooltip={{
528
567
  text: item.label,
529
568
  side: "right",
@@ -569,6 +608,21 @@
569
608
  >
570
609
  {@render rowBody(item, isParent, !!opts.isPopoutChild, isOpen)}
571
610
  </button>
611
+ {:else if item.onclick}
612
+ <!-- Action row: no link, no children/popout — a `<button>` that runs `onclick`
613
+ (e.g. an AI-panel toggle), then resets the drawer/popout like a leaf nav. -->
614
+ <button
615
+ type="button"
616
+ use:refIntoMap={opts.id}
617
+ class="{rowClass} text-left"
618
+ use:tooltip={{ text: item.label, side: "right" }}
619
+ onclick={(e) => {
620
+ item.onclick!(e);
621
+ closeAfterNav();
622
+ }}
623
+ >
624
+ {@render rowBody(item, false, !!opts.isPopoutChild, false)}
625
+ </button>
572
626
  {:else}
573
627
  <!-- Malformed item: no link, no children. Render visibly so the
574
628
  consumer notices, but with disabled-style cursor + opacity. -->
@@ -591,13 +645,17 @@
591
645
  onmouseenter={cancelClose}
592
646
  onmouseleave={scheduleClose}
593
647
  >
594
- <ul class="flex flex-col">
595
- {#each item.children ?? [] as child (child.label)}
596
- <li class="list-none" role="none">
597
- {@render row(child, { isPopoutChild: true })}
598
- </li>
599
- {/each}
600
- </ul>
648
+ {#if item.popout}
649
+ {@render item.popout()}
650
+ {:else}
651
+ <ul class="flex flex-col">
652
+ {#each item.children ?? [] as child (child.label)}
653
+ <li class="list-none" role="none">
654
+ {@render row(child, { isPopoutChild: true })}
655
+ </li>
656
+ {/each}
657
+ </ul>
658
+ {/if}
601
659
  </div>
602
660
  {/snippet}
603
661
 
@@ -659,15 +717,15 @@
659
717
  {/each}
660
718
  </ul>
661
719
  {:else}
662
- {#if topItems.length > 0}
720
+ {#if items.length > 0}
663
721
  <ul class="flex flex-col">
664
- {#each topItems as item, idx (item.label)}
722
+ {#each items as item, idx (item.label)}
665
723
  {@const id = `top:${idx}` as ItemId}
666
724
  <li class="list-none">
667
725
  {@render row(item, { id })}
668
726
  </li>
669
- {#if (item.children?.length ?? 0) > 0 && !isStepped}
670
- {@const geom = geometryFor(id, "top", idx, topItems.length, item.children!.length)}
727
+ {#if ((item.children?.length ?? 0) > 0 || !!item.popout) && !isStepped}
728
+ {@const geom = geometryFor(id, "top", idx, items.length, item.children?.length ?? 1)}
671
729
  <Popup
672
730
  anchor={geom.anchor}
673
731
  open={openId === id}
@@ -685,7 +743,7 @@
685
743
  </ul>
686
744
  {/if}
687
745
 
688
- {#if topItems.length > 0 && bottomItems.length > 0}
746
+ {#if items.length > 0 && bottomItems.length > 0}
689
747
  <div class="flex-1"></div>
690
748
  {/if}
691
749
 
@@ -696,13 +754,13 @@
696
754
  <li class="list-none">
697
755
  {@render row(item, { id })}
698
756
  </li>
699
- {#if (item.children?.length ?? 0) > 0 && !isStepped}
757
+ {#if ((item.children?.length ?? 0) > 0 || !!item.popout) && !isStepped}
700
758
  {@const geom = geometryFor(
701
759
  id,
702
760
  "bottom",
703
761
  idx,
704
762
  bottomItems.length,
705
- item.children!.length,
763
+ item.children?.length ?? 1,
706
764
  )}
707
765
  <Popup
708
766
  anchor={geom.anchor}
@@ -18,6 +18,17 @@
18
18
  * Designed for wide desktop.
19
19
  */
20
20
  export type SideBarMenuMode = "stepped" | "drawer" | "icon" | "expanded";
21
+ /** Imperative handle exposed via `bind:api`. */
22
+ export type SideBarMenuApi = {
23
+ /** Reset all transient state — close the drawer, clear the open popout, return step level to 0. */
24
+ reset: () => void;
25
+ /** Open the drawer (drawer/stepped modes). */
26
+ open: () => void;
27
+ /** Close the drawer. */
28
+ close: () => void;
29
+ /** Set the stepped-mode step level (`0` = top). */
30
+ setStepLevel: (n: number) => void;
31
+ };
21
32
  export type SideBarMenuBadgeVariant = "error" | "warning" | "info" | "success";
22
33
  export type SideBarMenuBadge = {
23
34
  /** Color of the indicator. */
@@ -50,12 +61,21 @@ export type SideBarMenuItem = {
50
61
  badge?: SideBarMenuBadge;
51
62
  /** v1 renders only the first level; deeper levels emit a DEV warning. */
52
63
  children?: SideBarMenuItem[];
64
+ /** Action row (no `link`) — renders a `<button>` and calls this on click. For a "toggle"
65
+ * row (an AI panel, etc.) or a non-navigating action. Runs after the drawer/popout reset. */
66
+ onclick?: (e: MouseEvent) => void;
67
+ /** Custom popout content, rendered INSTEAD of `children` in the popout (separators, hotkeys,
68
+ * an account/identity block…). The row still needs an `icon`/`label`; this is just its popout. */
69
+ popout?: import("svelte").Snippet;
70
+ /** Opt this anchor out of client-side routing — stamps `data-sveltekit-reload` (SvelteKit) so
71
+ * the link forces a full-browser navigation (sign-out endpoints, downloads, external redirects). */
72
+ reload?: boolean;
53
73
  };
54
74
  import type { Size } from "../../types/sizes.js";
55
75
  import { type Breakpoint } from "../../types/breakpoints.js";
56
76
  type ModesMap = Partial<Record<Breakpoint, SideBarMenuMode>>;
57
77
  type $$ComponentProps = {
58
- topItems?: SideBarMenuItem[];
78
+ items?: SideBarMenuItem[];
59
79
  bottomItems?: SideBarMenuItem[];
60
80
  currentPath: string;
61
81
  size?: Size;
@@ -102,8 +122,12 @@ type $$ComponentProps = {
102
122
  * and binds to `open`.
103
123
  */
104
124
  hamburger?: boolean;
125
+ /** Imperative handle (bindable). Call `api.reset()` from your router's navigation lifecycle
126
+ * (SvelteKit `afterNavigate`, etc.) so a programmatic `goto()`/back-button closes the drawer +
127
+ * any open popout and returns a stepped drawer to the top. */
128
+ api?: SideBarMenuApi;
105
129
  class?: string;
106
130
  };
107
- declare const SideBarMenu: import("svelte").Component<$$ComponentProps, {}, "open" | "stepLevel">;
131
+ declare const SideBarMenu: import("svelte").Component<$$ComponentProps, {}, "open" | "stepLevel" | "api">;
108
132
  type SideBarMenu = ReturnType<typeof SideBarMenu>;
109
133
  export default SideBarMenu;
package/dist/index.d.ts CHANGED
@@ -41,7 +41,7 @@ export type { Columns, Gap } from "./components/field-group.js";
41
41
  export { default as Disclosure } from "./components/Disclosure.svelte";
42
42
  export { default as Accordion } from "./components/Accordion.svelte";
43
43
  export { default as SideBarMenu } from "./components/SideBarMenu/SideBarMenu.svelte";
44
- export type { SideBarMenuItem, SideBarMenuBadge, SideBarMenuBadgeVariant, } from "./components/SideBarMenu/SideBarMenu.svelte";
44
+ export type { SideBarMenuItem, SideBarMenuBadge, SideBarMenuBadgeVariant, SideBarMenuApi, } from "./components/SideBarMenu/SideBarMenu.svelte";
45
45
  export { default as TabStrip } from "./components/TabStrip/TabStrip.svelte";
46
46
  export type { TabDefinition } from "./components/TabStrip/TabStrip.svelte";
47
47
  export { default as DataTable } from "./components/DataTable.svelte";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaethtech/svelte-ui",
3
- "version": "0.7.1-dev.41.d01009c",
3
+ "version": "0.7.1-dev.42.aca96e0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/spaethtech/svelte-ui.git"