@flowselections/core 1.0.32 → 1.0.33

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.
@@ -9,6 +9,8 @@ export type AuthenticatedLayoutProps = SidebarProps & {
9
9
  type MatchLike = {
10
10
  staticData?: unknown;
11
11
  context?: unknown;
12
+ status?: string;
13
+ globalNotFound?: boolean;
12
14
  };
13
15
  /** Sinds 1.0.23: diepste match met een niet-lege titel. */
14
16
  export declare function pickTitle(matches: MatchLike[]): string;
@@ -1 +1 @@
1
- {"version":3,"file":"AuthenticatedLayout.d.ts","sourceRoot":"","sources":["../../../src/core/components/layout/AuthenticatedLayout.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAIjD,MAAM,MAAM,wBAAwB,GAAG,YAAY,GAAG;IACpD,yDAAyD;IACzD,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,KAAK,SAAS,GAAG;IAAE,UAAU,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE7D,2DAA2D;AAC3D,wBAAgB,SAAS,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,MAAM,CAMtD;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,MAAM,GAAG,SAAS,CAMzE;AAED,wBAAgB,QAAQ,4CAWvB;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,QAAQ,EACR,MAAa,EACb,WAAkB,EAClB,GAAG,YAAY,EAChB,EAAE,wBAAwB,2CA6B1B"}
1
+ {"version":3,"file":"AuthenticatedLayout.d.ts","sourceRoot":"","sources":["../../../src/core/components/layout/AuthenticatedLayout.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAIjD,MAAM,MAAM,wBAAwB,GAAG,YAAY,GAAG;IACpD,yDAAyD;IACzD,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,KAAK,SAAS,GAAG;IAAE,UAAU,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAExG,2DAA2D;AAC3D,wBAAgB,SAAS,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,MAAM,CAMtD;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,MAAM,GAAG,SAAS,CAMzE;AAED,wBAAgB,QAAQ,4CAWvB;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,QAAQ,EACR,MAAa,EACb,WAAkB,EAClB,GAAG,YAAY,EAChB,EAAE,wBAAwB,2CAsC1B"}
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useMemo } from "react";
3
- import { Outlet, useMatches } from "@tanstack/react-router";
2
+ import { useEffect, useMemo } from "react";
3
+ import { Outlet, useLocation, useMatches } from "@tanstack/react-router";
4
+ import { settleStartNavigation } from "../../navigation/start.js";
4
5
  import { Sidebar, useSidebarModules } from "./Sidebar.js";
5
6
  import { Header } from "./Header.js";
6
7
  import { ShellContext } from "../../shell/ShellContext.js";
@@ -39,11 +40,19 @@ export function AuthenticatedLayout({ children, header = true, permissions = tru
39
40
  const title = pickTitle(matches);
40
41
  const access = pickModuleAccess(matches);
41
42
  const { visibleModules, loading } = useSidebarModules(sidebarProps.modules, permissions);
43
+ const pathname = useLocation().pathname;
44
+ const notFound = matches.some((m) => m.status === "notFound" || m.globalNotFound === true);
45
+ const denied = permissions && access === "none";
46
+ // Rondjesbeveiliging: een navigatie vanuit /start die hier op 404 of
47
+ // "Geen toegang" uitkomt, wordt in deze sessie niet opnieuw gekozen.
48
+ useEffect(() => {
49
+ settleStartNavigation(pathname, notFound || denied);
50
+ }, [pathname, notFound, denied]);
42
51
  const shell = useMemo(() => ({
43
52
  shellId: sidebarProps.shellId,
44
53
  modules: sidebarProps.modules,
45
54
  visibleModules,
46
55
  permissionsLoading: loading,
47
56
  }), [sidebarProps.shellId, sidebarProps.modules, visibleModules, loading]);
48
- return (_jsx(ShellContext.Provider, { value: shell, children: _jsxs("div", { className: "min-h-screen bg-background [--core-topbar-h:3.5rem] md:[--core-topbar-h:0px]", children: [_jsx(Sidebar, { ...sidebarProps, permissions: permissions, visibleModules: visibleModules, loading: loading }), _jsxs("div", { className: "flex min-h-dvh flex-col pt-[var(--core-topbar-h)] md:ml-64 md:pt-0", children: [header && _jsx(Header, { title: title, auth: sidebarProps.auth }), _jsx("main", { className: "flex-1", children: permissions && access === "none" ? _jsx(NoAccess, {}) : (children ?? _jsx(Outlet, {})) })] })] }) }));
57
+ return (_jsx(ShellContext.Provider, { value: shell, children: _jsxs("div", { className: "min-h-screen bg-background [--core-topbar-h:3.5rem] md:[--core-topbar-h:0px]", children: [_jsx(Sidebar, { ...sidebarProps, permissions: permissions, visibleModules: visibleModules, loading: loading }), _jsxs("div", { className: "flex min-h-dvh flex-col pt-[var(--core-topbar-h)] md:ml-64 md:pt-0", children: [header && _jsx(Header, { title: title, auth: sidebarProps.auth }), _jsx("main", { className: "flex-1", children: denied ? _jsx(NoAccess, {}) : (children ?? _jsx(Outlet, {})) })] })] }) }));
49
58
  }
@@ -10,5 +10,5 @@ import { useHeaderGreeting } from "./useHeaderGreeting.js";
10
10
  export function Header({ title, auth }) {
11
11
  const [bugsDialogOpen, setBugsDialogOpen] = useState(false);
12
12
  const greeting = useHeaderGreeting(auth);
13
- return (_jsxs(_Fragment, { children: [_jsx("header", { className: "@container sticky top-[var(--core-topbar-h,0px)] md:top-0 z-30 min-h-[var(--app-header-height)] shrink-0 border-b border-border bg-card px-3 py-2 md:px-6", children: _jsxs("div", { className: "flex min-h-11 flex-wrap items-center justify-between gap-x-3 gap-y-1", children: [_jsx("h1", { className: "min-w-0 flex-1 truncate text-xl font-semibold text-foreground", children: title }), _jsxs("div", { className: "flex shrink-0 items-center gap-2 @[900px]:gap-4", children: [_jsxs("div", { className: "relative hidden @[1300px]:block", children: [_jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { placeholder: "Zoeken...", className: "w-64 pl-9" })] }), _jsxs(Button, { variant: "outline", size: "sm", "aria-label": "Bugs melden", title: "Bugs melden", onClick: () => setBugsDialogOpen(true), children: [_jsx(Bug, { className: "h-4 w-4" }), _jsx("span", { className: "hidden @[650px]:inline", children: "Bugs melden" })] }), _jsx(NotificationBell, {}), greeting && _jsx("p", { className: "hidden max-w-96 font-sans text-sm text-muted-foreground @[1100px]:block", children: greeting }), auth && _jsx(HeaderIdentity, { auth: auth })] }), greeting && _jsx("p", { className: "w-full break-words font-sans text-xs text-muted-foreground @[1100px]:hidden", children: greeting })] }) }), _jsx(BugsDialog, { open: bugsDialogOpen, onOpenChange: setBugsDialogOpen })] }));
13
+ return (_jsxs(_Fragment, { children: [_jsx("header", { className: "@container sticky top-[var(--core-topbar-h,0px)] md:top-0 z-30 min-h-[var(--app-header-height)] shrink-0 border-b border-border bg-card px-3 py-2 md:px-6", children: _jsxs("div", { className: "flex min-h-11 flex-wrap items-center justify-between gap-x-3 gap-y-1", children: [_jsx("h1", { className: "min-w-0 truncate text-xl font-semibold text-foreground", children: title }), _jsxs("div", { className: "flex min-w-0 flex-1 items-center justify-end gap-2 @[900px]:gap-4", children: [greeting && _jsx("p", { className: "hidden min-w-0 max-w-96 shrink-[10] truncate font-sans text-sm text-muted-foreground @[1100px]:block", children: greeting }), auth && _jsx(HeaderIdentity, { auth: auth }), _jsxs("div", { className: "relative hidden shrink-0 @[1300px]:block", children: [_jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { placeholder: "Zoeken...", className: "w-64 pl-9" })] }), _jsxs(Button, { variant: "outline", size: "sm", className: "shrink-0", "aria-label": "Bugs melden", title: "Bugs melden", onClick: () => setBugsDialogOpen(true), children: [_jsx(Bug, { className: "h-4 w-4" }), _jsx("span", { className: "hidden @[650px]:inline", children: "Bugs melden" })] }), _jsx("span", { className: "shrink-0", children: _jsx(NotificationBell, {}) })] }), greeting && _jsx("p", { className: "w-full break-words font-sans text-xs text-muted-foreground @[1100px]:hidden", children: greeting })] }) }), _jsx(BugsDialog, { open: bugsDialogOpen, onOpenChange: setBugsDialogOpen })] }));
14
14
  }
@@ -1,5 +1,5 @@
1
1
  import type { AuthContext } from "../../auth/useAuth.js";
2
- import type { FlowModule } from "../../types/module.js";
2
+ import type { FlowModule, NavItem } from "../../types/module.js";
3
3
  export interface SidebarProps {
4
4
  auth: AuthContext;
5
5
  modules: FlowModule[];
@@ -35,6 +35,31 @@ export declare function useSidebarModules(modules: FlowModule[], permissions: bo
35
35
  visibleModules: FlowModule[];
36
36
  loading: boolean;
37
37
  };
38
+ /**
39
+ * Sinds 1.0.32. Klik op een groepskop: dicht → naar het eerste zichtbare kind
40
+ * en openklappen; open → alleen dichtklappen.
41
+ */
42
+ export declare function groupHeaderClick(open: boolean, item: NavItem): {
43
+ open: boolean;
44
+ navigateTo: string | null;
45
+ };
46
+ /**
47
+ * Sinds 1.0.32. Wat de zijbalk per module rendert:
48
+ * - groep met ≥ 2 zichtbare kinderen → groep;
49
+ * - groep met precies één kind → link naar dat kind (label/icoon van de module);
50
+ * - module die oorspronkelijk een groep was maar geen kinderen meer heeft
51
+ * (eigen link) → alleen als `nav.href` een bestaande route is;
52
+ * - anders een gewone link.
53
+ */
54
+ export type SidebarEntry = {
55
+ kind: "group";
56
+ mod: FlowModule;
57
+ } | {
58
+ kind: "link";
59
+ mod: FlowModule;
60
+ item: Omit<NavItem, "children">;
61
+ };
62
+ export declare function sidebarEntries(visible: FlowModule[], wasGroup: (mod: FlowModule) => boolean, exists: (path: string) => boolean): SidebarEntry[];
38
63
  /**
39
64
  * Desktop (md+): vaste zijbalk links, w-64.
40
65
  * Mobiel (<md): vaste topbalk (h-14) met menuknop die de zijbalk als Sheet opent.
@@ -1 +1 @@
1
- {"version":3,"file":"Sidebar.d.ts","sourceRoot":"","sources":["../../../src/core/components/layout/Sidebar.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,KAAK,EAAE,UAAU,EAAW,MAAM,uBAAuB,CAAC;AAQjE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,8EAA8E;IAC9E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0FAA0F;IAC1F,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;OAGG;IACH,cAAc,CAAC,EAAE,UAAU,EAAE,CAAC;IAC9B,8EAA8E;IAC9E,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,WAAW,EAAE,OAAO;;;EAI5E;AAkKD;;;GAGG;AACH,wBAAgB,OAAO,CAAC,EACtB,IAAI,EACJ,OAAO,EACP,OAAO,EACP,WAAmB,EACnB,OAAO,EACP,YAAmB,EACnB,cAAc,EACd,OAAO,GACR,EAAE,YAAY,2CA+Dd"}
1
+ {"version":3,"file":"Sidebar.d.ts","sourceRoot":"","sources":["../../../src/core/components/layout/Sidebar.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AASjE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,8EAA8E;IAC9E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0FAA0F;IAC1F,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;OAGG;IACH,cAAc,CAAC,EAAE,UAAU,EAAE,CAAC;IAC9B,8EAA8E;IAC9E,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,WAAW,EAAE,OAAO;;;EAI5E;AAqCD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAG3G;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,UAAU,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CAAA;CAAE,CAAC;AAEvE,wBAAgB,cAAc,CAC5B,OAAO,EAAE,UAAU,EAAE,EACrB,QAAQ,EAAE,CAAC,GAAG,EAAE,UAAU,KAAK,OAAO,EACtC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GAChC,YAAY,EAAE,CAWhB;AA2ID;;;GAGG;AACH,wBAAgB,OAAO,CAAC,EACtB,IAAI,EACJ,OAAO,EACP,OAAO,EACP,WAAmB,EACnB,OAAO,EACP,YAAmB,EACnB,cAAc,EACd,OAAO,GACR,EAAE,YAAY,2CAuEd"}
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from "react";
3
- import { Link, useLocation } from "@tanstack/react-router";
3
+ import { Link, useLocation, useNavigate, useRouter } from "@tanstack/react-router";
4
4
  import { Settings, LogOut, ChevronDown, Menu } from "lucide-react";
5
5
  import { Sheet, SheetContent, SheetTitle } from "../ui/sheet.js";
6
6
  import { Skeleton } from "../ui/skeleton.js";
@@ -9,6 +9,7 @@ import { useFooterItems, useModuleListTransform } from "../../extensions/ShellEx
9
9
  import { usePermissionFilteredModules } from "../../permissions/useModulePermission.js";
10
10
  import { useLogoSrc } from "../../hooks/useLogoSrc.js";
11
11
  import { BrandLogo } from "./BrandLogo.js";
12
+ import { isPathActive, routeExists } from "../../navigation/start.js";
12
13
  /**
13
14
  * Sinds 1.0.23. Pipeline: eerst rechten, dan extensie-transforms (ordening).
14
15
  * Zichtbaarheid, geen beveiliging.
@@ -20,24 +21,58 @@ export function useSidebarModules(modules, permissions) {
20
21
  }
21
22
  // ─── Single flat nav link ─────────────────────────────────────────────────────
22
23
  function SidebarLink({ item, pathname, indent = false, onNavigate, }) {
23
- const isActive = pathname === item.href;
24
+ const isActive = isPathActive(pathname, item.href);
24
25
  return (_jsxs(Link, { to: item.href, onClick: onNavigate, className: cn("flex items-center gap-3 rounded-lg px-3 font-sans text-sm font-medium transition-colors", indent ? "py-2" : "py-2.5", isActive
25
26
  ? "bg-sidebar-accent text-sidebar-accent-foreground"
26
27
  : "text-sidebar-foreground hover:bg-sidebar-accent/50"), children: [_jsx(item.icon, { className: cn(indent ? "h-4 w-4" : "h-5 w-5") }), item.label] }));
27
28
  }
28
29
  // ─── Collapsible group (for modules with children) ────────────────────────────
30
+ /**
31
+ * Sinds 1.0.32. Klik op een groepskop: dicht → naar het eerste zichtbare kind
32
+ * en openklappen; open → alleen dichtklappen.
33
+ */
34
+ export function groupHeaderClick(open, item) {
35
+ if (open)
36
+ return { open: false, navigateTo: null };
37
+ return { open: true, navigateTo: item.children?.[0]?.href ?? null };
38
+ }
39
+ export function sidebarEntries(visible, wasGroup, exists) {
40
+ const out = [];
41
+ for (const mod of visible) {
42
+ const children = mod.nav.children ?? [];
43
+ const { children: _c, ...own } = mod.nav;
44
+ if (children.length > 1)
45
+ out.push({ kind: "group", mod });
46
+ else if (children.length === 1)
47
+ out.push({ kind: "link", mod, item: { ...own, href: children[0].href } });
48
+ else if (wasGroup(mod) && !exists(mod.nav.href))
49
+ continue;
50
+ else
51
+ out.push({ kind: "link", mod, item: own });
52
+ }
53
+ return out;
54
+ }
29
55
  function SidebarGroup({ item, pathname, onNavigate, }) {
30
- const groupActive = item.children?.some((child) => pathname === child.href || pathname.startsWith(child.href + "/")) ?? false;
56
+ const groupActive = item.children?.some((child) => isPathActive(pathname, child.href)) ?? false;
31
57
  const [open, setOpen] = useState(groupActive);
32
- return (_jsxs(_Fragment, { children: [_jsxs("button", { onClick: () => setOpen(!open), className: "flex w-full items-center gap-3 rounded-lg px-3 py-2.5 font-sans text-sm font-medium text-sidebar-foreground transition-colors hover:bg-sidebar-accent/50 cursor-pointer", children: [_jsx(item.icon, { className: "h-5 w-5" }), _jsx("span", { className: "flex-1 text-left", children: item.label }), _jsx(ChevronDown, { className: cn("h-4 w-4 text-sidebar-muted transition-transform", open && "rotate-180") })] }), open && (_jsx("div", { className: "ml-3 space-y-1", children: item.children.map((child) => (_jsx(SidebarLink, { item: child, pathname: pathname, indent: true, onNavigate: onNavigate }, child.href))) }))] }));
58
+ const navigate = useNavigate();
59
+ const onHeader = () => {
60
+ const next = groupHeaderClick(open, item);
61
+ setOpen(next.open);
62
+ if (next.navigateTo) {
63
+ onNavigate?.();
64
+ void navigate({ to: next.navigateTo });
65
+ }
66
+ };
67
+ return (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", "aria-expanded": open, onClick: onHeader, className: "flex w-full items-center gap-3 rounded-lg px-3 py-2.5 font-sans text-sm font-medium text-sidebar-foreground transition-colors hover:bg-sidebar-accent/50 cursor-pointer", children: [_jsx(item.icon, { className: "h-5 w-5" }), _jsx("span", { className: "flex-1 text-left", children: item.label }), _jsx(ChevronDown, { className: cn("h-4 w-4 text-sidebar-muted transition-transform", open && "rotate-180") })] }), open && (_jsx("div", { className: "ml-3 space-y-1", children: item.children.map((child) => (_jsx(SidebarLink, { item: child, pathname: pathname, indent: true, onNavigate: onNavigate }, child.href))) }))] }));
33
68
  }
34
69
  // ─── Shared content (desktop aside + mobile sheet) ────────────────────────────
35
- function SidebarContent({ auth, visibleModules, loading, logoSrc, showSettings, pathname, onNavigate, }) {
70
+ function SidebarContent({ auth, entries, loading, logoSrc, showSettings, pathname, onNavigate, }) {
36
71
  const handleSignOut = async () => {
37
72
  onNavigate?.();
38
73
  await auth.signOut();
39
74
  };
40
- return (_jsxs("div", { className: "flex h-full min-h-0 flex-col font-sans", children: [_jsx("div", { className: "flex h-24 shrink-0 items-center justify-center px-6 py-2", children: _jsx(BrandLogo, { src: logoSrc, className: "max-h-20 w-auto max-w-full object-contain", fallbackClassName: "flex h-full max-w-full items-center text-sidebar-foreground" }) }), _jsx("nav", { className: "min-h-0 flex-1 space-y-1 px-3 py-4 overflow-y-auto sidebar-scroll", "aria-busy": loading || undefined, children: loading ? (_jsx("div", { "data-testid": "sidebar-skeleton", className: "space-y-2", children: [0, 1, 2, 3].map((i) => (_jsx(Skeleton, { className: "h-9 w-full bg-sidebar-accent" }, i))) })) : (visibleModules.map((mod) => mod.nav.children?.length ? (_jsx(SidebarGroup, { item: mod.nav, pathname: pathname, onNavigate: onNavigate }, mod.id)) : (_jsx(SidebarLink, { item: mod.nav, pathname: pathname, onNavigate: onNavigate }, mod.id)))) }), _jsxs("div", { className: "shrink-0 border-t border-sidebar-border px-3 py-4", children: [showSettings && (_jsx(SidebarLink, { item: { label: "Instellingen", href: "/instellingen", icon: Settings }, pathname: pathname, onNavigate: onNavigate })), _jsxs("button", { onClick: handleSignOut, className: "mt-1 flex w-full items-center gap-3 rounded-lg px-3 py-2.5 font-sans text-sm font-medium text-sidebar-foreground transition-colors hover:bg-sidebar-accent/50 cursor-pointer", children: [_jsx(LogOut, { className: "h-5 w-5" }), "Uitloggen"] })] })] }));
75
+ return (_jsxs("div", { className: "flex h-full min-h-0 flex-col font-sans", children: [_jsx("div", { className: "flex h-24 shrink-0 items-center justify-center px-6 py-2", children: _jsx(BrandLogo, { src: logoSrc, className: "max-h-20 w-auto max-w-full object-contain", fallbackClassName: "flex h-full max-w-full items-center text-sidebar-foreground" }) }), _jsx("nav", { className: "min-h-0 flex-1 space-y-1 px-3 py-4 overflow-y-auto sidebar-scroll", "aria-busy": loading || undefined, children: loading ? (_jsx("div", { "data-testid": "sidebar-skeleton", className: "space-y-2", children: [0, 1, 2, 3].map((i) => (_jsx(Skeleton, { className: "h-9 w-full bg-sidebar-accent" }, i))) })) : (entries.map((entry) => entry.kind === "group" ? (_jsx(SidebarGroup, { item: entry.mod.nav, pathname: pathname, onNavigate: onNavigate }, entry.mod.id)) : (_jsx(SidebarLink, { item: entry.item, pathname: pathname, onNavigate: onNavigate }, entry.mod.id)))) }), _jsxs("div", { className: "shrink-0 border-t border-sidebar-border px-3 py-4", children: [showSettings && (_jsx(SidebarLink, { item: { label: "Instellingen", href: "/instellingen", icon: Settings }, pathname: pathname, onNavigate: onNavigate })), _jsxs("button", { onClick: handleSignOut, className: "mt-1 flex w-full items-center gap-3 rounded-lg px-3 py-2.5 font-sans text-sm font-medium text-sidebar-foreground transition-colors hover:bg-sidebar-accent/50 cursor-pointer", children: [_jsx(LogOut, { className: "h-5 w-5" }), "Uitloggen"] })] })] }));
41
76
  }
42
77
  // ─── Main Sidebar ─────────────────────────────────────────────────────────────
43
78
  /**
@@ -57,10 +92,13 @@ export function Sidebar({ auth, modules, shellId, permissions = false, logoSrc,
57
92
  useEffect(() => {
58
93
  setMobileOpen(false);
59
94
  }, [pathname]);
95
+ const router = useRouter();
96
+ const rawGroups = new Set(modules.filter((m) => m.nav.children?.length).map((m) => m.id));
97
+ const entries = sidebarEntries(list, (m) => rawGroups.has(m.id), (path) => routeExists(router, path));
60
98
  const close = () => setMobileOpen(false);
61
99
  const contentProps = {
62
100
  auth,
63
- visibleModules: list,
101
+ entries,
64
102
  loading: isLoading,
65
103
  logoSrc: logo,
66
104
  showSettings: footer.settings,
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Sinds 1.0.32. `/start`: bepaalt de bestemming (rol → shell → eerste module →
3
+ * /instellingen) en navigeert met replace. Wacht op auth, rechten en
4
+ * voorkeuren, max. 3 s. Zichtbaarheid, geen beveiliging.
5
+ */
6
+ export declare function StartPage(): import("react/jsx-runtime").JSX.Element;
7
+ //# sourceMappingURL=StartPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StartPage.d.ts","sourceRoot":"","sources":["../../../src/core/components/layout/StartPage.tsx"],"names":[],"mappings":"AASA;;;;GAIG;AACH,wBAAgB,SAAS,4CA6CxB"}
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { useRouteContext, useRouter } from "@tanstack/react-router";
4
+ import { useShell } from "../../shell/ShellContext.js";
5
+ import { useStartPathPreferences } from "../../extensions/ShellExtensions.js";
6
+ import { canAccessModule } from "../../auth/redirect.js";
7
+ import { getFailedStartPaths, markStartNavigation, pickStartPath, routeExists } from "../../navigation/start.js";
8
+ const WAIT_MS = 3000;
9
+ /**
10
+ * Sinds 1.0.32. `/start`: bepaalt de bestemming (rol → shell → eerste module →
11
+ * /instellingen) en navigeert met replace. Wacht op auth, rechten en
12
+ * voorkeuren, max. 3 s. Zichtbaarheid, geen beveiliging.
13
+ */
14
+ export function StartPage() {
15
+ const router = useRouter();
16
+ const context = useRouteContext({ strict: false });
17
+ const auth = context?.auth;
18
+ const shell = useShell();
19
+ const preferences = useStartPathPreferences();
20
+ const [timedOut, setTimedOut] = useState(false);
21
+ const done = useRef(false);
22
+ useEffect(() => {
23
+ const t = setTimeout(() => setTimedOut(true), WAIT_MS);
24
+ return () => clearTimeout(t);
25
+ }, []);
26
+ const ready = timedOut ||
27
+ (!auth?.loading && !(shell?.permissionsLoading ?? false) && preferences.every((p) => !p.laden));
28
+ const prefKey = JSON.stringify(preferences);
29
+ useEffect(() => {
30
+ if (!ready || done.current)
31
+ return;
32
+ done.current = true;
33
+ const opts = {
34
+ userId: auth?.user?.id ?? null,
35
+ accessToken: auth?.session?.access_token ?? null,
36
+ roles: Array.isArray(auth?.roles) ? auth.roles : [],
37
+ };
38
+ void pickStartPath({
39
+ preferences: preferences.filter((p) => !p.laden),
40
+ modules: shell?.visibleModules ?? [],
41
+ failed: getFailedStartPaths(),
42
+ canAccess: (path) => canAccessModule(path, opts),
43
+ exists: (path) => routeExists(router, path),
44
+ }).then(({ pad }) => {
45
+ markStartNavigation(pad);
46
+ void router.navigate({ href: pad, replace: true });
47
+ });
48
+ // eslint-disable-next-line react-hooks/exhaustive-deps
49
+ }, [ready, prefKey]);
50
+ return (_jsx("div", { className: "flex min-h-[60vh] items-center justify-center p-8", "aria-busy": "true", children: _jsx("p", { className: "text-sm text-muted-foreground", children: "Even geduld\u2026" }) }));
51
+ }
@@ -1,5 +1,6 @@
1
1
  import type { ComponentType, ReactNode } from "react";
2
2
  import type { FlowModule } from "../types/module.js";
3
+ import type { StartPathResult } from "../navigation/start.js";
3
4
  export interface ModuleListTransformContext {
4
5
  /** De ongefilterde modulelijst die de shell aan de core-layout gaf. */
5
6
  allModules: FlowModule[];
@@ -20,6 +21,12 @@ export interface FooterItems {
20
21
  }
21
22
  export type FooterTransform = (items: FooterItems) => FooterItems;
22
23
  export type UseFooterTransform = () => FooterTransform;
24
+ /**
25
+ * Sinds 1.0.32. Startpagina-voorkeur. Contract: zie AGENTS.md. Rol vóór shell;
26
+ * zolang `laden` true is wacht /start (max. 3 s). Stabiele referentie, geen
27
+ * side-effects.
28
+ */
29
+ export type UseStartPath = () => StartPathResult;
23
30
  /** Sinds 1.0.23. Past de footer-transform toe; fout → ongewijzigd. Zichtbaarheid, geen beveiliging. */
24
31
  export declare function useFooterItems(base: FooterItems): FooterItems;
25
32
  export interface ShellExtensionsProviderProps {
@@ -37,9 +44,11 @@ export interface ShellExtensionsProviderProps {
37
44
  }>;
38
45
  /** Sinds 1.0.23. Optionele footer-transform (stabiele referentie). Uitloggen blijft altijd. */
39
46
  useFooter?: UseFooterTransform;
47
+ /** Sinds 1.0.32. Startpagina-hooks (stabiele referenties); rol vóór shell. */
48
+ useStartPath?: UseStartPath | UseStartPath[];
40
49
  children: ReactNode;
41
50
  }
42
- export declare function ShellExtensionsProvider({ useModuleList, guard, useFooter, children, }: ShellExtensionsProviderProps): import("react/jsx-runtime").JSX.Element;
51
+ export declare function ShellExtensionsProvider({ useModuleList, guard, useFooter, useStartPath, children, }: ShellExtensionsProviderProps): import("react/jsx-runtime").JSX.Element;
43
52
  export declare function useShellGuard(): ComponentType<{
44
53
  children: ReactNode;
45
54
  }> | undefined;
@@ -52,4 +61,10 @@ export declare function useShellGuard(): ComponentType<{
52
61
  * zijbalk.
53
62
  */
54
63
  export declare function useModuleListTransform(modules: FlowModule[], allModules?: FlowModule[]): FlowModule[];
64
+ /**
65
+ * Sinds 1.0.32. Roept de startpagina-hooks aan (vaste volgorde) en geeft hun
66
+ * resultaten terug, rol vóór shell. Een fout of ongeldige vorm telt als
67
+ * "geen voorkeur" (fail-open).
68
+ */
69
+ export declare function useStartPathPreferences(): StartPathResult[];
55
70
  //# sourceMappingURL=ShellExtensions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ShellExtensions.d.ts","sourceRoot":"","sources":["../../src/core/extensions/ShellExtensions.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD,MAAM,WAAW,0BAA0B;IACzC,uEAAuE;IACvE,UAAU,EAAE,UAAU,EAAE,CAAC;CAC1B;AAED;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,CAChC,OAAO,EAAE,UAAU,EAAE,EACrB,OAAO,EAAE,0BAA0B,KAChC,UAAU,EAAE,CAAC;AAElB,MAAM,MAAM,sBAAsB,GAAG,MAAM,mBAAmB,CAAC;AAE/D;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,OAAO,CAAC;CACnB;AACD,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,WAAW,KAAK,WAAW,CAAC;AAClE,MAAM,MAAM,kBAAkB,GAAG,MAAM,eAAe,CAAC;AAYvD,uGAAuG;AACvG,wBAAgB,cAAc,CAAC,IAAI,EAAE,WAAW,GAAG,WAAW,CAc7D;AAED,MAAM,WAAW,4BAA4B;IAC3C;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,sBAAsB,GAAG,sBAAsB,EAAE,CAAC;IAClE,kFAAkF;IAClF,KAAK,CAAC,EAAE,aAAa,CAAC;QAAE,QAAQ,EAAE,SAAS,CAAA;KAAE,CAAC,CAAC;IAC/C,+FAA+F;IAC/F,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,wBAAgB,uBAAuB,CAAC,EACtC,aAAa,EACb,KAAK,EACL,SAAS,EACT,QAAQ,GACT,EAAE,4BAA4B,2CAY9B;AAED,wBAAgB,aAAa,IAAI,aAAa,CAAC;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,CAAC,GAAG,SAAS,CAElF;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,UAAU,EAAE,EACrB,UAAU,GAAE,UAAU,EAAY,GACjC,UAAU,EAAE,CAuBd"}
1
+ {"version":3,"file":"ShellExtensions.d.ts","sourceRoot":"","sources":["../../src/core/extensions/ShellExtensions.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D,MAAM,WAAW,0BAA0B;IACzC,uEAAuE;IACvE,UAAU,EAAE,UAAU,EAAE,CAAC;CAC1B;AAED;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,CAChC,OAAO,EAAE,UAAU,EAAE,EACrB,OAAO,EAAE,0BAA0B,KAChC,UAAU,EAAE,CAAC;AAElB,MAAM,MAAM,sBAAsB,GAAG,MAAM,mBAAmB,CAAC;AAE/D;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,OAAO,CAAC;CACnB;AACD,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,WAAW,KAAK,WAAW,CAAC;AAClE,MAAM,MAAM,kBAAkB,GAAG,MAAM,eAAe,CAAC;AAEvD;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,eAAe,CAAC;AAajD,uGAAuG;AACvG,wBAAgB,cAAc,CAAC,IAAI,EAAE,WAAW,GAAG,WAAW,CAc7D;AAED,MAAM,WAAW,4BAA4B;IAC3C;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,sBAAsB,GAAG,sBAAsB,EAAE,CAAC;IAClE,kFAAkF;IAClF,KAAK,CAAC,EAAE,aAAa,CAAC;QAAE,QAAQ,EAAE,SAAS,CAAA;KAAE,CAAC,CAAC;IAC/C,+FAA+F;IAC/F,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B,8EAA8E;IAC9E,YAAY,CAAC,EAAE,YAAY,GAAG,YAAY,EAAE,CAAC;IAC7C,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,wBAAgB,uBAAuB,CAAC,EACtC,aAAa,EACb,KAAK,EACL,SAAS,EACT,YAAY,EACZ,QAAQ,GACT,EAAE,4BAA4B,2CAqB9B;AAED,wBAAgB,aAAa,IAAI,aAAa,CAAC;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,CAAC,GAAG,SAAS,CAElF;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,UAAU,EAAE,EACrB,UAAU,GAAE,UAAU,EAAY,GACjC,UAAU,EAAE,CAuBd;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,IAAI,eAAe,EAAE,CAc3D"}
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createContext, useContext, useMemo } from "react";
3
- const ShellExtensionsContext = createContext({ transforms: [] });
3
+ const ShellExtensionsContext = createContext({ transforms: [], startPaths: [] });
4
4
  const identityFooter = () => (items) => items;
5
5
  /** Sinds 1.0.23. Past de footer-transform toe; fout → ongewijzigd. Zichtbaarheid, geen beveiliging. */
6
6
  export function useFooterItems(base) {
@@ -20,13 +20,16 @@ export function useFooterItems(base) {
20
20
  return base;
21
21
  }
22
22
  }
23
- export function ShellExtensionsProvider({ useModuleList, guard, useFooter, children, }) {
23
+ export function ShellExtensionsProvider({ useModuleList, guard, useFooter, useStartPath, children, }) {
24
24
  const transforms = useMemo(() => (Array.isArray(useModuleList) ? useModuleList : useModuleList ? [useModuleList] : []),
25
25
  // Bewust op de referentie(s): de hooks moeten stabiel zijn, zodat de
26
26
  // hook-volgorde tussen renders nooit verandert.
27
27
  // eslint-disable-next-line react-hooks/exhaustive-deps
28
28
  [useModuleList]);
29
- const value = useMemo(() => ({ transforms, guard, useFooter }), [transforms, guard, useFooter]);
29
+ const startPaths = useMemo(() => (Array.isArray(useStartPath) ? useStartPath : useStartPath ? [useStartPath] : []),
30
+ // eslint-disable-next-line react-hooks/exhaustive-deps
31
+ [useStartPath]);
32
+ const value = useMemo(() => ({ transforms, startPaths, guard, useFooter }), [transforms, startPaths, guard, useFooter]);
30
33
  return _jsx(ShellExtensionsContext.Provider, { value: value, children: children });
31
34
  }
32
35
  export function useShellGuard() {
@@ -65,3 +68,24 @@ export function useModuleListTransform(modules, allModules = modules) {
65
68
  }
66
69
  return result;
67
70
  }
71
+ /**
72
+ * Sinds 1.0.32. Roept de startpagina-hooks aan (vaste volgorde) en geeft hun
73
+ * resultaten terug, rol vóór shell. Een fout of ongeldige vorm telt als
74
+ * "geen voorkeur" (fail-open).
75
+ */
76
+ export function useStartPathPreferences() {
77
+ const { startPaths } = useContext(ShellExtensionsContext);
78
+ const out = [];
79
+ for (const useStart of startPaths) {
80
+ try {
81
+ const r = useStart();
82
+ if (r && (r.bron === "rol" || r.bron === "shell")) {
83
+ out.push({ bron: r.bron, pad: typeof r.pad === "string" ? r.pad : null, laden: r.laden === true });
84
+ }
85
+ }
86
+ catch {
87
+ /* fail-open */
88
+ }
89
+ }
90
+ return [...out.filter((r) => r.bron === "rol"), ...out.filter((r) => r.bron === "shell")];
91
+ }
package/dist/index.d.ts CHANGED
@@ -45,8 +45,11 @@ export { useIsMobile } from "./hooks/use-mobile.js";
45
45
  export { useHuisstijl } from "./hooks/useHuisstijl.js";
46
46
  export type { FlowModule, NavItem, SettingsCard, FeatureDefinition } from "./types/module.js";
47
47
  export { moduleKey, navKey, featureKey, moduleFeatures } from "./types/module.js";
48
- export { ShellExtensionsProvider, useModuleListTransform, useFooterItems } from "./extensions/ShellExtensions.js";
49
- export type { ModuleListTransform, ModuleListTransformContext, UseModuleListTransform, ShellExtensionsProviderProps, FooterItems, FooterTransform, UseFooterTransform, } from "./extensions/ShellExtensions.js";
48
+ export { ShellExtensionsProvider, useModuleListTransform, useFooterItems, useStartPathPreferences, } from "./extensions/ShellExtensions.js";
49
+ export { StartPage } from "./components/layout/StartPage.js";
50
+ export { modulePagePath, isPathActive, routeExists, pickStartPath, orderStartPreferences, getFailedStartPaths, START_PATH, } from "./navigation/start.js";
51
+ export type { StartPathResult, StartPathSource, StartDecision, PickStartArgs } from "./navigation/start.js";
52
+ export type { ModuleListTransform, ModuleListTransformContext, UseModuleListTransform, ShellExtensionsProviderProps, FooterItems, FooterTransform, UseFooterTransform, UseStartPath, } from "./extensions/ShellExtensions.js";
50
53
  export { ModuleGuardSlot } from "./extensions/ModuleGuardSlot.js";
51
54
  export { useMyModulePermissions, useModulePermission, useModulePermissionTransform, useCan, Can, usePermissionFilteredModules, } from "./permissions/useModulePermission.js";
52
55
  export type { PermissionLevel, ModulePermissionRow, CanProps } from "./permissions/useModulePermission.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/core/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/core/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -43,7 +43,9 @@ export { useIsMobile } from "./hooks/use-mobile.js";
43
43
  export { useHuisstijl } from "./hooks/useHuisstijl.js";
44
44
  export { moduleKey, navKey, featureKey, moduleFeatures } from "./types/module.js";
45
45
  // Shell-extensiepunten (allemaal optioneel; zonder provider ongewijzigd gedrag)
46
- export { ShellExtensionsProvider, useModuleListTransform, useFooterItems } from "./extensions/ShellExtensions.js";
46
+ export { ShellExtensionsProvider, useModuleListTransform, useFooterItems, useStartPathPreferences, } from "./extensions/ShellExtensions.js";
47
+ export { StartPage } from "./components/layout/StartPage.js";
48
+ export { modulePagePath, isPathActive, routeExists, pickStartPath, orderStartPreferences, getFailedStartPaths, START_PATH, } from "./navigation/start.js";
47
49
  export { ModuleGuardSlot } from "./extensions/ModuleGuardSlot.js";
48
50
  // Rechten (leeskant; fail-open, zie README)
49
51
  export { useMyModulePermissions, useModulePermission, useModulePermissionTransform, useCan, Can, usePermissionFilteredModules, } from "./permissions/useModulePermission.js";
@@ -0,0 +1,60 @@
1
+ import type { FlowModule } from "../types/module.js";
2
+ /** Standaardpad van de startpagina en de laatste terugval. */
3
+ export declare const START_PATH = "/start";
4
+ export declare const START_FALLBACK = "/instellingen";
5
+ /**
6
+ * Sinds 1.0.32. Bestemming van een module: bij een groep de href van het
7
+ * eerste (na het rechtenfilter zichtbare) kind, anders `nav.href`.
8
+ * Core gebruikt `nav.href` van een groep nooit als bestemming.
9
+ */
10
+ export declare function modulePagePath(mod: FlowModule): string;
11
+ /** Actief als het pad gelijk is aan `href` of eronder valt. */
12
+ export declare function isPathActive(pathname: string, href: string): boolean;
13
+ type RouterLike = {
14
+ getMatchedRoutes?: (pathname: string, routePathname?: string) => unknown;
15
+ };
16
+ /**
17
+ * Sinds 1.0.32. Bestaat het pad als route (zonder te navigeren)? Een
18
+ * vangroute (root-splat) of alleen de root telt niet. Zonder router-API: `true`
19
+ * (fail-open).
20
+ */
21
+ export declare function routeExists(router: RouterLike | null | undefined, path: string): boolean;
22
+ export type StartPathSource = "rol" | "shell";
23
+ export interface StartPathResult {
24
+ bron: StartPathSource;
25
+ pad: string | null;
26
+ laden: boolean;
27
+ }
28
+ /** Rol vóór shell; binnen dezelfde bron de registratievolgorde. */
29
+ export declare function orderStartPreferences(results: StartPathResult[]): StartPathResult[];
30
+ export interface PickStartArgs {
31
+ preferences: StartPathResult[];
32
+ /** Zichtbare modules in zijbalkvolgorde (na rechten en ordening). */
33
+ modules: FlowModule[];
34
+ failed?: string[];
35
+ canAccess: (path: string) => Promise<boolean> | boolean;
36
+ exists: (path: string) => boolean;
37
+ }
38
+ export interface StartDecision {
39
+ pad: string;
40
+ stap: "rol" | "shell" | "module" | "instellingen";
41
+ }
42
+ /**
43
+ * Sinds 1.0.32. Volgorde: rol → shell → eerste module → /instellingen.
44
+ * Een voorkeur telt alleen als hij bestaat, toegankelijk is en niet eerder in
45
+ * deze sessie mislukte.
46
+ */
47
+ export declare function pickStartPath({ preferences, modules, failed, canAccess, exists }: PickStartArgs): Promise<StartDecision>;
48
+ /** Paden die vanuit /start eerder een 404 of "Geen toegang" gaven. */
49
+ export declare function getFailedStartPaths(): string[];
50
+ /** /start zet dit vlak vóór het navigeren. */
51
+ export declare function markStartNavigation(path: string): void;
52
+ /**
53
+ * Afronding van een navigatie: alleen als hij vanuit /start kwam en op dit
54
+ * pad landde, wordt een mislukking onthouden. Andere 404's tellen niet.
55
+ */
56
+ export declare function settleStartNavigation(path: string, failed: boolean): void;
57
+ /** Alleen voor tests. */
58
+ export declare function __resetStartGuard(): void;
59
+ export {};
60
+ //# sourceMappingURL=start.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"start.d.ts","sourceRoot":"","sources":["../../src/core/navigation/start.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGrD,8DAA8D;AAC9D,eAAO,MAAM,UAAU,WAAW,CAAC;AACnC,eAAO,MAAM,cAAc,kBAAkB,CAAC;AAE9C;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAGtD;AAED,+DAA+D;AAC/D,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAKpE;AAED,KAAK,UAAU,GAAG;IAChB,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;CAC1E,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAexF;AAID,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,OAAO,CAAC;AAC9C,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,eAAe,CAAC;IACtB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,mEAAmE;AACnE,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,eAAe,EAAE,GAAG,eAAe,EAAE,CAEnF;AAED,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B,qEAAqE;IACrE,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IACxD,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;CACnC;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,cAAc,CAAC;CACnD;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,EAAE,WAAW,EAAE,OAAO,EAAE,MAAW,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAmBnI;AA8BD,sEAAsE;AACtE,wBAAgB,mBAAmB,IAAI,MAAM,EAAE,CAE9C;AAED,8CAA8C;AAC9C,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,QAE/C;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,QAOlE;AAED,yBAAyB;AACzB,wBAAgB,iBAAiB,SAQhC"}
@@ -0,0 +1,141 @@
1
+ import { safeRedirectPath } from "../auth/redirect.js";
2
+ /** Standaardpad van de startpagina en de laatste terugval. */
3
+ export const START_PATH = "/start";
4
+ export const START_FALLBACK = "/instellingen";
5
+ /**
6
+ * Sinds 1.0.32. Bestemming van een module: bij een groep de href van het
7
+ * eerste (na het rechtenfilter zichtbare) kind, anders `nav.href`.
8
+ * Core gebruikt `nav.href` van een groep nooit als bestemming.
9
+ */
10
+ export function modulePagePath(mod) {
11
+ const first = mod.nav.children?.[0];
12
+ return first ? first.href : mod.nav.href;
13
+ }
14
+ /** Actief als het pad gelijk is aan `href` of eronder valt. */
15
+ export function isPathActive(pathname, href) {
16
+ if (!href)
17
+ return false;
18
+ if (pathname === href)
19
+ return true;
20
+ const base = href.endsWith("/") ? href.slice(0, -1) : href;
21
+ return base !== "" && pathname.startsWith(base + "/");
22
+ }
23
+ /**
24
+ * Sinds 1.0.32. Bestaat het pad als route (zonder te navigeren)? Een
25
+ * vangroute (root-splat) of alleen de root telt niet. Zonder router-API: `true`
26
+ * (fail-open).
27
+ */
28
+ export function routeExists(router, path) {
29
+ if (!router || typeof router.getMatchedRoutes !== "function")
30
+ return true;
31
+ const pathname = path.split("?")[0].split("#")[0] || "/";
32
+ try {
33
+ // Oudere routerversies geven een object, nieuwere een tuple [routes, params, foundRoute].
34
+ const res = router.getMatchedRoutes(pathname, undefined);
35
+ const found = Array.isArray(res) ? res[2] : res?.foundRoute;
36
+ if (!found)
37
+ return false;
38
+ if (found.id === "__root__")
39
+ return false;
40
+ const full = found.fullPath ?? "";
41
+ if (full === "/$" || full === "$")
42
+ return false;
43
+ return true;
44
+ }
45
+ catch {
46
+ return true;
47
+ }
48
+ }
49
+ /** Rol vóór shell; binnen dezelfde bron de registratievolgorde. */
50
+ export function orderStartPreferences(results) {
51
+ return [...results.filter((r) => r.bron === "rol"), ...results.filter((r) => r.bron === "shell")];
52
+ }
53
+ /**
54
+ * Sinds 1.0.32. Volgorde: rol → shell → eerste module → /instellingen.
55
+ * Een voorkeur telt alleen als hij bestaat, toegankelijk is en niet eerder in
56
+ * deze sessie mislukte.
57
+ */
58
+ export async function pickStartPath({ preferences, modules, failed = [], canAccess, exists }) {
59
+ const skip = new Set(failed);
60
+ for (const pref of orderStartPreferences(preferences)) {
61
+ const pad = pref.pad ? safeRedirectPath(pref.pad, "") : "";
62
+ if (!pad || pad === START_PATH || skip.has(pad) || !exists(pad))
63
+ continue;
64
+ let ok = false;
65
+ try {
66
+ ok = await canAccess(pad);
67
+ }
68
+ catch {
69
+ ok = true;
70
+ }
71
+ if (ok)
72
+ return { pad, stap: pref.bron };
73
+ }
74
+ for (const mod of modules) {
75
+ const pad = modulePagePath(mod);
76
+ if (!pad || pad === START_PATH || skip.has(pad) || !exists(pad))
77
+ continue;
78
+ return { pad, stap: "module" };
79
+ }
80
+ return { pad: START_FALLBACK, stap: "instellingen" };
81
+ }
82
+ // ─── Bescherming tegen rondjes ───────────────────────────────────────────────
83
+ const FAILED_KEY = "core:start:failed";
84
+ let memoryFailed = [];
85
+ let pending = null;
86
+ function readFailed() {
87
+ try {
88
+ const raw = globalThis.sessionStorage?.getItem(FAILED_KEY);
89
+ if (raw) {
90
+ const list = JSON.parse(raw);
91
+ if (Array.isArray(list))
92
+ return list.filter((p) => typeof p === "string");
93
+ }
94
+ }
95
+ catch {
96
+ /* geen opslag */
97
+ }
98
+ return memoryFailed;
99
+ }
100
+ function writeFailed(list) {
101
+ memoryFailed = list;
102
+ try {
103
+ globalThis.sessionStorage?.setItem(FAILED_KEY, JSON.stringify(list));
104
+ }
105
+ catch {
106
+ /* geen opslag: alleen geheugen */
107
+ }
108
+ }
109
+ /** Paden die vanuit /start eerder een 404 of "Geen toegang" gaven. */
110
+ export function getFailedStartPaths() {
111
+ return [...new Set([...readFailed(), ...memoryFailed])];
112
+ }
113
+ /** /start zet dit vlak vóór het navigeren. */
114
+ export function markStartNavigation(path) {
115
+ pending = path;
116
+ }
117
+ /**
118
+ * Afronding van een navigatie: alleen als hij vanuit /start kwam en op dit
119
+ * pad landde, wordt een mislukking onthouden. Andere 404's tellen niet.
120
+ */
121
+ export function settleStartNavigation(path, failed) {
122
+ if (pending === null || pending !== path)
123
+ return;
124
+ pending = null;
125
+ if (failed) {
126
+ const list = getFailedStartPaths();
127
+ if (!list.includes(path))
128
+ writeFailed([...list, path]);
129
+ }
130
+ }
131
+ /** Alleen voor tests. */
132
+ export function __resetStartGuard() {
133
+ pending = null;
134
+ memoryFailed = [];
135
+ try {
136
+ globalThis.sessionStorage?.removeItem(FAILED_KEY);
137
+ }
138
+ catch {
139
+ /* */
140
+ }
141
+ }
@@ -1,19 +1,26 @@
1
1
  export interface SystemRouteOptions {
2
- /** Pad als er geen geldig `?redirect` is (na inloggen, handover, reset). */
3
- landing: string;
2
+ /**
3
+ * Pad als er geen geldig `?redirect` is (na inloggen, handover, reset).
4
+ * Sinds 1.0.32 optioneel: zonder → "/start" (startpagina per rol/shell).
5
+ */
6
+ landing?: string;
4
7
  /** Pad van de inlogpagina. Standaard "/login". */
5
8
  loginPath?: string;
6
9
  }
7
10
  export interface LoginSearch {
8
11
  redirect?: string;
9
12
  }
10
- /** `/login`: ingelogd → `?redirect` (alleen same-site) wint van `landing`. */
11
- export declare function loginRoute({ landing }: SystemRouteOptions): {
13
+ /**
14
+ * `/login`: ingelogd → `?redirect` (alleen same-site) wint van `landing`.
15
+ * Zonder `landing` (sinds 1.0.32): `?redirect` alleen als die toegankelijk is
16
+ * (`canAccessModule`), anders "/start".
17
+ */
18
+ export declare function loginRoute({ landing }?: SystemRouteOptions): {
12
19
  validateSearch: (search: Record<string, unknown>) => LoginSearch;
13
20
  beforeLoad: ({ context, search }: {
14
21
  context: unknown;
15
22
  search: LoginSearch;
16
- }) => void;
23
+ }) => void | Promise<void>;
17
24
  head: () => {
18
25
  meta: ({
19
26
  title: string;
@@ -35,7 +42,7 @@ export declare function loginRoute({ landing }: SystemRouteOptions): {
35
42
  component: () => import("react/jsx-runtime").JSX.Element;
36
43
  };
37
44
  /** `/reset-wachtwoord`: de core-pagina, daarna naar `landing`. */
38
- export declare function resetPasswordRoute({ landing, loginPath }: SystemRouteOptions): {
45
+ export declare function resetPasswordRoute({ landing, loginPath }?: SystemRouteOptions): {
39
46
  head: () => {
40
47
  meta: ({
41
48
  title: string;
@@ -57,7 +64,7 @@ export declare function resetPasswordRoute({ landing, loginPath }: SystemRouteOp
57
64
  component: () => import("react/jsx-runtime").JSX.Element;
58
65
  };
59
66
  /** `/`: redirect in beforeLoad, dus er wordt niets gerenderd (geen flits). */
60
- export declare function landingRoute({ landing }: Pick<SystemRouteOptions, "landing">): {
67
+ export declare function landingRoute({ landing }?: Pick<SystemRouteOptions, "landing">): {
61
68
  head: () => {
62
69
  meta: ({
63
70
  title: string;
@@ -104,4 +111,30 @@ export declare function handoverRoute({ landing, loginPath, exchangePath, }: Sys
104
111
  };
105
112
  component: () => import("react/jsx-runtime").JSX.Element;
106
113
  };
114
+ /**
115
+ * Sinds 1.0.32. `/start` onder `_authenticated` (door het dashboard
116
+ * gegenereerd): kiest rol → shell → eerste module → /instellingen.
117
+ */
118
+ export declare function startRoute(): {
119
+ ssr: false;
120
+ head: () => {
121
+ meta: ({
122
+ title: string;
123
+ name?: undefined;
124
+ content?: undefined;
125
+ property?: undefined;
126
+ } | {
127
+ name: string;
128
+ content: string;
129
+ title?: undefined;
130
+ property?: undefined;
131
+ } | {
132
+ property: string;
133
+ content: string;
134
+ title?: undefined;
135
+ name?: undefined;
136
+ })[];
137
+ };
138
+ component: () => import("react/jsx-runtime").JSX.Element;
139
+ };
107
140
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/routes/index.tsx"],"names":[],"mappings":"AA4BA,MAAM,WAAW,kBAAkB;IACjC,4EAA4E;IAC5E,OAAO,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAMD,8EAA8E;AAC9E,wBAAgB,UAAU,CAAC,EAAE,OAAO,EAAE,EAAE,kBAAkB;6BAE7B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,WAAW;sCAE5B;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,WAAW,CAAA;KAAE;;;;;;;;;;;;;;;;;;;;EAU9E;AAED,kEAAkE;AAClE,wBAAgB,kBAAkB,CAAC,EAAE,OAAO,EAAE,SAAoB,EAAE,EAAE,kBAAkB;;;;;;;;;;;;;;;;;;;;EAOvF;AAED,8EAA8E;AAC9E,wBAAgB,YAAY,CAAC,EAAE,OAAO,EAAE,EAAE,IAAI,CAAC,kBAAkB,EAAE,SAAS,CAAC;;;;;;;;;;;;;;;;;;;sBAGzD,KAAK;;EAOxB;AAED,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EACP,SAAoB,EACpB,YAAY,GACb,EAAE,kBAAkB,GAAG;IAAE,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE;;;;;;;;;;;;;;;;;;;;;EAQhD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/routes/index.tsx"],"names":[],"mappings":"AA8BA,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAMD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EAAE,OAAO,EAAE,GAAE,kBAAuB;6BAElC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,WAAW;sCAE5B;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,WAAW,CAAA;KAAE,KAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;EA0BrG;AAED,kEAAkE;AAClE,wBAAgB,kBAAkB,CAAC,EAAE,OAAoB,EAAE,SAAoB,EAAE,GAAE,kBAAuB;;;;;;;;;;;;;;;;;;;;EAOzG;AAED,8EAA8E;AAC9E,wBAAgB,YAAY,CAAC,EAAE,OAAoB,EAAE,GAAE,IAAI,CAAC,kBAAkB,EAAE,SAAS,CAAM;;;;;;;;;;;;;;;;;;;sBAG3E,KAAK;;EAOxB;AAED,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,EAC5B,OAAoB,EACpB,SAAoB,EACpB,YAAY,GACb,EAAE,kBAAkB,GAAG;IAAE,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE;;;;;;;;;;;;;;;;;;;;;EAQhD;AAED;;;GAGG;AACH,wBAAgB,UAAU;;;;;;;;;;;;;;;;;;;;;EAQzB"}
@@ -12,7 +12,9 @@ import { redirect } from "@tanstack/react-router";
12
12
  import { LoginPage } from "../components/layout/LoginPage.js";
13
13
  import { ResetWachtwoordPage } from "../components/layout/ResetWachtwoordPage.js";
14
14
  import { HandoverPage } from "../components/layout/HandoverPage.js";
15
- import { safeRedirectPath } from "../auth/redirect.js";
15
+ import { StartPage } from "../components/layout/StartPage.js";
16
+ import { canAccessModule, safeRedirectPath } from "../auth/redirect.js";
17
+ import { START_PATH } from "../navigation/start.js";
16
18
  function pageHead(title, description) {
17
19
  return {
18
20
  meta: [
@@ -28,23 +30,42 @@ function pageHead(title, description) {
28
30
  function isAuthenticated(context) {
29
31
  return Boolean(context?.auth?.isAuthenticated);
30
32
  }
31
- /** `/login`: ingelogd → `?redirect` (alleen same-site) wint van `landing`. */
32
- export function loginRoute({ landing }) {
33
+ /**
34
+ * `/login`: ingelogd → `?redirect` (alleen same-site) wint van `landing`.
35
+ * Zonder `landing` (sinds 1.0.32): `?redirect` alleen als die toegankelijk is
36
+ * (`canAccessModule`), anders "/start".
37
+ */
38
+ export function loginRoute({ landing } = {}) {
33
39
  return {
34
40
  validateSearch: (search) => typeof search.redirect === "string" ? { redirect: search.redirect } : {},
35
41
  beforeLoad: ({ context, search }) => {
36
- if (isAuthenticated(context)) {
42
+ if (!isAuthenticated(context))
43
+ return;
44
+ if (landing) {
37
45
  throw redirect({ href: safeRedirectPath(search?.redirect, landing) });
38
46
  }
47
+ const target = safeRedirectPath(search?.redirect, START_PATH);
48
+ if (target === START_PATH)
49
+ throw redirect({ href: START_PATH });
50
+ const auth = context.auth;
51
+ return canAccessModule(target, {
52
+ userId: auth?.user?.id ?? null,
53
+ accessToken: auth?.session?.access_token ?? null,
54
+ roles: Array.isArray(auth?.roles) ? auth.roles : [],
55
+ }).then((ok) => {
56
+ throw redirect({ href: ok ? target : START_PATH });
57
+ }, () => {
58
+ throw redirect({ href: target });
59
+ });
39
60
  },
40
61
  head: () => pageHead("Inloggen | Flora Select", "Log veilig in bij Flora Select."),
41
62
  component: function LoginRouteComponent() {
42
- return _jsx(LoginPage, { redirectTo: landing, routeHandlesRedirect: true });
63
+ return _jsx(LoginPage, { redirectTo: landing ?? START_PATH, routeHandlesRedirect: true });
43
64
  },
44
65
  };
45
66
  }
46
67
  /** `/reset-wachtwoord`: de core-pagina, daarna naar `landing`. */
47
- export function resetPasswordRoute({ landing, loginPath = "/login" }) {
68
+ export function resetPasswordRoute({ landing = START_PATH, loginPath = "/login" } = {}) {
48
69
  return {
49
70
  head: () => pageHead("Nieuw wachtwoord | Flora Select", "Stel een nieuw wachtwoord in voor Flora Select."),
50
71
  component: function ResetPasswordRouteComponent() {
@@ -53,7 +74,7 @@ export function resetPasswordRoute({ landing, loginPath = "/login" }) {
53
74
  };
54
75
  }
55
76
  /** `/`: redirect in beforeLoad, dus er wordt niets gerenderd (geen flits). */
56
- export function landingRoute({ landing }) {
77
+ export function landingRoute({ landing = START_PATH } = {}) {
57
78
  return {
58
79
  head: () => pageHead("Flora Select", "Open de Flora Select-omgeving."),
59
80
  beforeLoad: () => {
@@ -65,7 +86,7 @@ export function landingRoute({ landing }) {
65
86
  };
66
87
  }
67
88
  /** `/auth/handover`: client-only, want de sessie staat alleen in localStorage. */
68
- export function handoverRoute({ landing, loginPath = "/login", exchangePath, }) {
89
+ export function handoverRoute({ landing = START_PATH, loginPath = "/login", exchangePath, }) {
69
90
  return {
70
91
  ssr: false,
71
92
  head: () => pageHead("Aanmelden | Flora Select", "Rond de veilige aanmelding bij Flora Select af."),
@@ -74,3 +95,16 @@ export function handoverRoute({ landing, loginPath = "/login", exchangePath, })
74
95
  },
75
96
  };
76
97
  }
98
+ /**
99
+ * Sinds 1.0.32. `/start` onder `_authenticated` (door het dashboard
100
+ * gegenereerd): kiest rol → shell → eerste module → /instellingen.
101
+ */
102
+ export function startRoute() {
103
+ return {
104
+ ssr: false,
105
+ head: () => pageHead("Start | Flora Select", "Je startpagina in Flora Select wordt geopend."),
106
+ component: function StartRouteComponent() {
107
+ return _jsx(StartPage, {});
108
+ },
109
+ };
110
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowselections/core",
3
- "version": "1.0.32",
3
+ "version": "1.0.33",
4
4
  "private": false,
5
5
  "sideEffects": false,
6
6
  "type": "module",