@flowselections/core 1.0.31 → 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.
Files changed (45) hide show
  1. package/dist/auth/header-identity.d.ts +23 -0
  2. package/dist/auth/header-identity.d.ts.map +1 -0
  3. package/dist/auth/header-identity.js +53 -0
  4. package/dist/components/layout/AuthenticatedLayout.d.ts +2 -0
  5. package/dist/components/layout/AuthenticatedLayout.d.ts.map +1 -1
  6. package/dist/components/layout/AuthenticatedLayout.js +12 -3
  7. package/dist/components/layout/Header.d.ts +3 -1
  8. package/dist/components/layout/Header.d.ts.map +1 -1
  9. package/dist/components/layout/Header.js +5 -2
  10. package/dist/components/layout/HeaderIdentity.d.ts +5 -0
  11. package/dist/components/layout/HeaderIdentity.d.ts.map +1 -0
  12. package/dist/components/layout/HeaderIdentity.js +32 -0
  13. package/dist/components/layout/Sidebar.d.ts +26 -1
  14. package/dist/components/layout/Sidebar.d.ts.map +1 -1
  15. package/dist/components/layout/Sidebar.js +45 -7
  16. package/dist/components/layout/StartPage.d.ts +7 -0
  17. package/dist/components/layout/StartPage.d.ts.map +1 -0
  18. package/dist/components/layout/StartPage.js +51 -0
  19. package/dist/components/layout/useHeaderGreeting.d.ts +3 -0
  20. package/dist/components/layout/useHeaderGreeting.d.ts.map +1 -0
  21. package/dist/components/layout/useHeaderGreeting.js +40 -0
  22. package/dist/components/ui/command.d.ts +7 -7
  23. package/dist/extensions/ShellExtensions.d.ts +16 -1
  24. package/dist/extensions/ShellExtensions.d.ts.map +1 -1
  25. package/dist/extensions/ShellExtensions.js +27 -3
  26. package/dist/index.d.ts +5 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +3 -1
  29. package/dist/navigation/start.d.ts +60 -0
  30. package/dist/navigation/start.d.ts.map +1 -0
  31. package/dist/navigation/start.js +141 -0
  32. package/dist/routes/index.d.ts +40 -7
  33. package/dist/routes/index.d.ts.map +1 -1
  34. package/dist/routes/index.js +42 -8
  35. package/dist/server/shell-runtime.d.ts.map +1 -1
  36. package/dist/server/shell-runtime.js +59 -13
  37. package/dist/supabase/auth-middleware.d.ts +30 -0
  38. package/dist/supabase/auth-middleware.d.ts.map +1 -1
  39. package/dist/supabase/client.d.ts +30 -0
  40. package/dist/supabase/client.d.ts.map +1 -1
  41. package/dist/supabase/client.server.d.ts +30 -0
  42. package/dist/supabase/client.server.d.ts.map +1 -1
  43. package/dist/supabase/types.d.ts +30 -0
  44. package/dist/supabase/types.d.ts.map +1 -1
  45. package/package.json +1 -1
@@ -0,0 +1,23 @@
1
+ type IdentitySource = {
2
+ profile?: {
3
+ display_name?: unknown;
4
+ } | null;
5
+ user?: {
6
+ email?: string | null;
7
+ user_metadata?: Record<string, unknown>;
8
+ } | null;
9
+ };
10
+ export declare function displayName(auth: IdentitySource): string;
11
+ export declare function primaryRole(roles: readonly string[]): string | undefined;
12
+ export declare function roleLabel(key: string | undefined, name?: string | null): string;
13
+ export declare function initials(name: string): string;
14
+ export declare function greetingAtHour(hour: number): string;
15
+ export declare function amsterdamGreeting(now: Date, name: string): {
16
+ date: string;
17
+ text: string;
18
+ };
19
+ export type GreetingStorage = Pick<Storage, "getItem" | "setItem">;
20
+ /** Storage is obtained inside the callback so even localStorage access can fail safely. */
21
+ export declare function claimGreeting(userId: string, date: string, storage: () => GreetingStorage, seen: Map<string, string>): boolean;
22
+ export {};
23
+ //# sourceMappingURL=header-identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"header-identity.d.ts","sourceRoot":"","sources":["../../src/core/auth/header-identity.ts"],"names":[],"mappings":"AAAA,KAAK,cAAc,GAAG;IACpB,OAAO,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IAC5C,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,GAAG,IAAI,CAAC;CAClF,CAAC;AAEF,wBAAgB,WAAW,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAIxD;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAExE;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAG/E;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK7C;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAUzF;AAED,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,CAAC,CAAC;AAEnE,2FAA2F;AAC3F,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,eAAe,EAAE,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAgB9H"}
@@ -0,0 +1,53 @@
1
+ export function displayName(auth) {
2
+ const metadata = auth.user?.user_metadata;
3
+ const candidates = [auth.profile?.display_name, metadata?.display_name, metadata?.full_name, auth.user?.email?.split("@")[0]];
4
+ return candidates.find((value) => typeof value === "string" && !!value.trim())?.trim() ?? "Gebruiker";
5
+ }
6
+ export function primaryRole(roles) {
7
+ return ["superadmin", "admin", "planner", "monteur"].find((role) => roles.includes(role)) ?? roles.find((role) => !!role.trim());
8
+ }
9
+ export function roleLabel(key, name) {
10
+ const value = name?.trim() || key || "";
11
+ return value ? value.charAt(0).toLocaleUpperCase("nl-NL") + value.slice(1) : "";
12
+ }
13
+ export function initials(name) {
14
+ const words = name.trim().split(/\s+/);
15
+ const first = words[0]?.charAt(0) ?? "";
16
+ const last = words.length > 1 ? words[words.length - 1]?.charAt(0) ?? "" : "";
17
+ return (first + last).toLocaleUpperCase("nl-NL");
18
+ }
19
+ export function greetingAtHour(hour) {
20
+ return hour >= 5 && hour < 12 ? "Goedemorgen" : hour >= 12 && hour < 18 ? "Goedemiddag" : "Goedenavond";
21
+ }
22
+ export function amsterdamGreeting(now, name) {
23
+ const parts = new Intl.DateTimeFormat("nl-NL", {
24
+ timeZone: "Europe/Amsterdam", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", hourCycle: "h23",
25
+ }).formatToParts(now);
26
+ const part = (type) => parts.find((value) => value.type === type)?.value ?? "";
27
+ const date = `${part("year")}-${part("month")}-${part("day")}`;
28
+ const label = new Intl.DateTimeFormat("nl-NL", {
29
+ timeZone: "Europe/Amsterdam", weekday: "long", day: "numeric", month: "long",
30
+ }).format(now);
31
+ return { date, text: `${greetingAtHour(Number(part("hour")))}, ${name.split(/\s+/)[0]} · ${label}` };
32
+ }
33
+ /** Storage is obtained inside the callback so even localStorage access can fail safely. */
34
+ export function claimGreeting(userId, date, storage, seen) {
35
+ if (seen.get(userId) === date)
36
+ return false;
37
+ try {
38
+ const target = storage();
39
+ const key = `core:greeted:${userId}`;
40
+ if (target.getItem(key) === date) {
41
+ seen.set(userId, date);
42
+ return false;
43
+ }
44
+ target.setItem(key, date);
45
+ }
46
+ catch {
47
+ // Without storage, only one greeting per user for this page session.
48
+ if (seen.has(userId))
49
+ return false;
50
+ }
51
+ seen.set(userId, date);
52
+ return true;
53
+ }
@@ -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 }), _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
  }
@@ -1,6 +1,8 @@
1
+ import type { AuthContext } from "../../auth/useAuth.js";
1
2
  interface HeaderProps {
2
3
  title: string;
4
+ auth?: AuthContext;
3
5
  }
4
- export declare function Header({ title }: HeaderProps): import("react/jsx-runtime").JSX.Element;
6
+ export declare function Header({ title, auth }: HeaderProps): import("react/jsx-runtime").JSX.Element;
5
7
  export {};
6
8
  //# sourceMappingURL=Header.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Header.d.ts","sourceRoot":"","sources":["../../../src/core/components/layout/Header.tsx"],"names":[],"mappings":"AAOA,UAAU,WAAW;IACnB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,MAAM,CAAC,EAAE,KAAK,EAAE,EAAE,WAAW,2CAsB5C"}
1
+ {"version":3,"file":"Header.d.ts","sourceRoot":"","sources":["../../../src/core/components/layout/Header.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAIzD,UAAU,WAAW;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,WAAW,CAAC;CACpB;AAED,wBAAgB,MAAM,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,WAAW,2CA4BlD"}
@@ -5,7 +5,10 @@ import { Input } from "../ui/input.js";
5
5
  import { Button } from "../ui/button.js";
6
6
  import { BugsDialog } from "./BugsDialog.js";
7
7
  import { NotificationBell } from "./NotificationBell.js";
8
- export function Header({ title }) {
8
+ import { HeaderIdentity } from "./HeaderIdentity.js";
9
+ import { useHeaderGreeting } from "./useHeaderGreeting.js";
10
+ export function Header({ title, auth }) {
9
11
  const [bugsDialogOpen, setBugsDialogOpen] = useState(false);
10
- return (_jsxs(_Fragment, { children: [_jsxs("header", { className: "sticky top-[var(--core-topbar-h,0px)] md:top-0 z-30 flex h-[var(--app-header-height)] shrink-0 items-center justify-between border-b border-border bg-card px-6", children: [_jsx("h1", { className: "text-xl font-semibold text-foreground", children: title }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsxs("div", { className: "relative hidden md: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", onClick: () => setBugsDialogOpen(true), children: [_jsx(Bug, { className: "mr-2 h-4 w-4" }), "Bugs melden"] }), _jsx(NotificationBell, {})] })] }), _jsx(BugsDialog, { open: bugsDialogOpen, onOpenChange: setBugsDialogOpen })] }));
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 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 })] }));
11
14
  }
@@ -0,0 +1,5 @@
1
+ import type { AuthContext } from "../../auth/useAuth.js";
2
+ export declare function HeaderIdentity({ auth }: {
3
+ auth: AuthContext;
4
+ }): import("react/jsx-runtime").JSX.Element | null;
5
+ //# sourceMappingURL=HeaderIdentity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HeaderIdentity.d.ts","sourceRoot":"","sources":["../../../src/core/components/layout/HeaderIdentity.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAOzD,wBAAgB,cAAc,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,kDAkC7D"}
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { displayName, primaryRole, roleLabel, initials } from "../../auth/header-identity.js";
4
+ import { supabase } from "../../supabase/client.js";
5
+ import { Button } from "../ui/button.js";
6
+ import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover.js";
7
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip.js";
8
+ export function HeaderIdentity({ auth }) {
9
+ const name = displayName(auth);
10
+ const key = primaryRole(auth.roles);
11
+ const [resolved, setResolved] = useState(null);
12
+ useEffect(() => {
13
+ let active = true;
14
+ if (!key || !auth.user)
15
+ return;
16
+ const userId = auth.user.id;
17
+ void (async () => {
18
+ try {
19
+ const { data } = await supabase.from("roles").select("name").eq("key", key).maybeSingle();
20
+ if (active && data?.name)
21
+ setResolved({ key, name: data.name, userId });
22
+ }
23
+ catch { /* Existing permissions only; the key remains the fallback. */ }
24
+ })();
25
+ return () => { active = false; };
26
+ }, [key, auth.user?.id]);
27
+ if (!auth.user)
28
+ return null;
29
+ const role = roleLabel(key, resolved && resolved.key === key && resolved.userId === auth.user.id ? resolved.name : undefined);
30
+ const label = role ? `${name} · ${role}` : name;
31
+ return _jsxs(_Fragment, { children: [_jsxs("div", { className: "hidden max-w-64 min-w-0 items-center gap-1 font-sans text-sm @[1100px]:flex", title: label, children: [_jsx("span", { className: "truncate text-foreground", children: name }), role && _jsxs("span", { className: "shrink-0 text-muted-foreground", children: ["\u00B7 ", role] })] }), _jsx(TooltipProvider, { children: _jsx(Tooltip, { children: _jsxs(Popover, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(PopoverTrigger, { asChild: true, children: _jsx(Button, { variant: "secondary", size: "icon", className: "shrink-0 rounded-full font-sans @[1100px]:hidden", "aria-label": label, children: initials(name) }) }) }), _jsxs(PopoverContent, { align: "end", className: "w-64 break-words font-sans", children: [_jsx("p", { children: name }), role && _jsx("p", { className: "text-sm text-muted-foreground", children: role })] }), _jsx(TooltipContent, { children: label })] }) }) })] });
32
+ }
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ import type { AuthContext } from "../../auth/useAuth.js";
2
+ export declare function useHeaderGreeting(auth?: AuthContext): string | null;
3
+ //# sourceMappingURL=useHeaderGreeting.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useHeaderGreeting.d.ts","sourceRoot":"","sources":["../../../src/core/components/layout/useHeaderGreeting.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAKzD,wBAAgB,iBAAiB,CAAC,IAAI,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,CA+BnE"}
@@ -0,0 +1,40 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { useLocation } from "@tanstack/react-router";
3
+ import { amsterdamGreeting, claimGreeting, displayName } from "../../auth/header-identity.js";
4
+ const seen = new Map();
5
+ export function useHeaderGreeting(auth) {
6
+ const href = useLocation({ select: (location) => location.href });
7
+ const [greeting, setGreeting] = useState(null);
8
+ const previousHref = useRef(href);
9
+ const name = auth ? displayName(auth) : "";
10
+ const userId = auth?.user?.id;
11
+ useEffect(() => {
12
+ if (previousHref.current !== href) {
13
+ previousHref.current = href;
14
+ setGreeting(null);
15
+ }
16
+ }, [href]);
17
+ useEffect(() => {
18
+ if (!userId || auth?.loading) {
19
+ setGreeting(null);
20
+ return;
21
+ }
22
+ const greet = () => {
23
+ const result = amsterdamGreeting(new Date(), name);
24
+ if (claimGreeting(userId, result.date, () => window.localStorage, seen)) {
25
+ setGreeting({ userId, text: result.text });
26
+ }
27
+ else {
28
+ // Profile data may arrive after session metadata; refresh only an
29
+ // already visible greeting, never resurrect one hidden by navigation.
30
+ setGreeting((current) => current?.userId === userId ? { userId, text: result.text } : current);
31
+ }
32
+ };
33
+ greet();
34
+ // Also cover an open tab crossing Amsterdam midnight.
35
+ const timer = window.setInterval(greet, 60000);
36
+ window.addEventListener("focus", greet);
37
+ return () => { window.clearInterval(timer); window.removeEventListener("focus", greet); };
38
+ }, [userId, auth?.loading, name]);
39
+ return greeting?.userId === userId ? greeting?.text ?? null : null;
40
+ }
@@ -6,7 +6,7 @@ declare const Command: React.ForwardRefExoticComponent<Omit<{
6
6
  ref?: React.Ref<HTMLDivElement>;
7
7
  } & {
8
8
  asChild?: boolean;
9
- }, "asChild" | "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
9
+ }, "key" | "asChild" | keyof React.HTMLAttributes<HTMLDivElement>> & {
10
10
  label?: string;
11
11
  shouldFilter?: boolean;
12
12
  filter?: (value: string, search: string, keywords?: string[]) => number;
@@ -22,7 +22,7 @@ declare const CommandInput: React.ForwardRefExoticComponent<Omit<Omit<Pick<Pick<
22
22
  ref?: React.Ref<HTMLInputElement>;
23
23
  } & {
24
24
  asChild?: boolean;
25
- }, "asChild" | "key" | keyof React.InputHTMLAttributes<HTMLInputElement>>, "type" | "value" | "onChange"> & {
25
+ }, "key" | "asChild" | keyof React.InputHTMLAttributes<HTMLInputElement>>, "type" | "value" | "onChange"> & {
26
26
  value?: string;
27
27
  onValueChange?: (search: string) => void;
28
28
  } & React.RefAttributes<HTMLInputElement>, "ref"> & React.RefAttributes<HTMLInputElement>>;
@@ -32,7 +32,7 @@ declare const CommandList: React.ForwardRefExoticComponent<Omit<{
32
32
  ref?: React.Ref<HTMLDivElement>;
33
33
  } & {
34
34
  asChild?: boolean;
35
- }, "asChild" | "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
35
+ }, "key" | "asChild" | keyof React.HTMLAttributes<HTMLDivElement>> & {
36
36
  label?: string;
37
37
  } & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
38
38
  declare const CommandEmpty: React.ForwardRefExoticComponent<Omit<{
@@ -41,14 +41,14 @@ declare const CommandEmpty: React.ForwardRefExoticComponent<Omit<{
41
41
  ref?: React.Ref<HTMLDivElement>;
42
42
  } & {
43
43
  asChild?: boolean;
44
- }, "asChild" | "key" | keyof React.HTMLAttributes<HTMLDivElement>> & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
44
+ }, "key" | "asChild" | keyof React.HTMLAttributes<HTMLDivElement>> & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
45
45
  declare const CommandGroup: React.ForwardRefExoticComponent<Omit<{
46
46
  children?: React.ReactNode;
47
47
  } & Omit<Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
48
48
  ref?: React.Ref<HTMLDivElement>;
49
49
  } & {
50
50
  asChild?: boolean;
51
- }, "asChild" | "key" | keyof React.HTMLAttributes<HTMLDivElement>>, "value" | "heading"> & {
51
+ }, "key" | "asChild" | keyof React.HTMLAttributes<HTMLDivElement>>, "value" | "heading"> & {
52
52
  heading?: React.ReactNode;
53
53
  value?: string;
54
54
  forceMount?: boolean;
@@ -57,7 +57,7 @@ declare const CommandSeparator: React.ForwardRefExoticComponent<Omit<Pick<Pick<R
57
57
  ref?: React.Ref<HTMLDivElement>;
58
58
  } & {
59
59
  asChild?: boolean;
60
- }, "asChild" | "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
60
+ }, "key" | "asChild" | keyof React.HTMLAttributes<HTMLDivElement>> & {
61
61
  alwaysRender?: boolean;
62
62
  } & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
63
63
  declare const CommandItem: React.ForwardRefExoticComponent<Omit<{
@@ -66,7 +66,7 @@ declare const CommandItem: React.ForwardRefExoticComponent<Omit<{
66
66
  ref?: React.Ref<HTMLDivElement>;
67
67
  } & {
68
68
  asChild?: boolean;
69
- }, "asChild" | "key" | keyof React.HTMLAttributes<HTMLDivElement>>, "disabled" | "value" | "onSelect"> & {
69
+ }, "key" | "asChild" | keyof React.HTMLAttributes<HTMLDivElement>>, "disabled" | "value" | "onSelect"> & {
70
70
  disabled?: boolean;
71
71
  onSelect?: (value: string) => void;
72
72
  value?: string;
@@ -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"}