@flowselections/core 1.0.21 → 1.0.23

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 (60) hide show
  1. package/README.md +61 -0
  2. package/dist/auth/redirect.d.ts +52 -0
  3. package/dist/auth/redirect.d.ts.map +1 -0
  4. package/dist/auth/redirect.js +98 -0
  5. package/dist/auth/useAuth.d.ts +1 -1
  6. package/dist/auth/useAuth.d.ts.map +1 -1
  7. package/dist/auth/useIsSuperadmin.d.ts +19 -0
  8. package/dist/auth/useIsSuperadmin.d.ts.map +1 -0
  9. package/dist/auth/useIsSuperadmin.js +16 -0
  10. package/dist/components/layout/AuthenticatedLayout.d.ts +29 -0
  11. package/dist/components/layout/AuthenticatedLayout.d.ts.map +1 -0
  12. package/dist/components/layout/AuthenticatedLayout.js +49 -0
  13. package/dist/components/layout/HandoverPage.d.ts.map +1 -1
  14. package/dist/components/layout/HandoverPage.js +4 -9
  15. package/dist/components/layout/Header.js +1 -1
  16. package/dist/components/layout/InstellingenPage.d.ts.map +1 -1
  17. package/dist/components/layout/InstellingenPage.js +10 -2
  18. package/dist/components/layout/LoginPage.d.ts.map +1 -1
  19. package/dist/components/layout/LoginPage.js +6 -6
  20. package/dist/components/layout/ResetWachtwoordPage.d.ts +3 -1
  21. package/dist/components/layout/ResetWachtwoordPage.d.ts.map +1 -1
  22. package/dist/components/layout/ResetWachtwoordPage.js +5 -3
  23. package/dist/components/layout/Sidebar.d.ts +37 -3
  24. package/dist/components/layout/Sidebar.d.ts.map +1 -1
  25. package/dist/components/layout/Sidebar.js +52 -13
  26. package/dist/extensions/ShellExtensions.d.ts +14 -1
  27. package/dist/extensions/ShellExtensions.d.ts.map +1 -1
  28. package/dist/extensions/ShellExtensions.js +21 -2
  29. package/dist/hooks/useLogoSrc.d.ts +7 -0
  30. package/dist/hooks/useLogoSrc.d.ts.map +1 -0
  31. package/dist/hooks/useLogoSrc.js +29 -0
  32. package/dist/index.d.ts +20 -6
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +12 -3
  35. package/dist/permissions/resolve.d.ts +22 -0
  36. package/dist/permissions/resolve.d.ts.map +1 -0
  37. package/dist/permissions/resolve.js +76 -0
  38. package/dist/permissions/store.d.ts +38 -0
  39. package/dist/permissions/store.d.ts.map +1 -0
  40. package/dist/permissions/store.js +180 -0
  41. package/dist/permissions/useModulePermission.d.ts +52 -32
  42. package/dist/permissions/useModulePermission.d.ts.map +1 -1
  43. package/dist/permissions/useModulePermission.js +72 -104
  44. package/dist/routes/index.d.ts +48 -0
  45. package/dist/routes/index.d.ts.map +1 -0
  46. package/dist/routes/index.js +63 -0
  47. package/dist/server/index.d.ts +1 -1
  48. package/dist/server/index.d.ts.map +1 -1
  49. package/dist/server/index.js +1 -1
  50. package/dist/server/permissions.server.d.ts +21 -8
  51. package/dist/server/permissions.server.d.ts.map +1 -1
  52. package/dist/server/permissions.server.js +54 -9
  53. package/dist/shell/ShellContext.d.ts +26 -0
  54. package/dist/shell/ShellContext.d.ts.map +1 -0
  55. package/dist/shell/ShellContext.js +14 -0
  56. package/dist/styles.css +127 -0
  57. package/dist/types/module.d.ts +39 -4
  58. package/dist/types/module.d.ts.map +1 -1
  59. package/dist/types/module.js +33 -5
  60. package/package.json +115 -107
@@ -0,0 +1,63 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Route-opties voor de systeemroutes. Shells houden per route een dun,
4
+ * gegenereerd omhulsel:
5
+ *
6
+ * export const Route = createFileRoute("/login")(loginRoute({ landing: "/voorraad" }));
7
+ *
8
+ * Core levert de volledige opties, zodat een wijziging aan een systeempagina
9
+ * met één core-update in alle shells landt.
10
+ */
11
+ import { redirect } from "@tanstack/react-router";
12
+ import { LoginPage } from "../components/layout/LoginPage.js";
13
+ import { ResetWachtwoordPage } from "../components/layout/ResetWachtwoordPage.js";
14
+ import { HandoverPage } from "../components/layout/HandoverPage.js";
15
+ import { safeRedirectPath } from "../auth/redirect.js";
16
+ function isAuthenticated(context) {
17
+ return Boolean(context?.auth?.isAuthenticated);
18
+ }
19
+ /** `/login`: ingelogd → `?redirect` (alleen same-site) wint van `landing`. */
20
+ export function loginRoute({ landing }) {
21
+ return {
22
+ validateSearch: (search) => typeof search.redirect === "string" ? { redirect: search.redirect } : {},
23
+ beforeLoad: ({ context, search }) => {
24
+ if (isAuthenticated(context)) {
25
+ throw redirect({ href: safeRedirectPath(search?.redirect, landing) });
26
+ }
27
+ },
28
+ head: () => ({ meta: [{ title: "Inloggen" }] }),
29
+ component: function LoginRouteComponent() {
30
+ return _jsx(LoginPage, { redirectTo: landing });
31
+ },
32
+ };
33
+ }
34
+ /** `/reset-wachtwoord`: de core-pagina, daarna naar `landing`. */
35
+ export function resetPasswordRoute({ landing, loginPath = "/login" }) {
36
+ return {
37
+ head: () => ({ meta: [{ title: "Nieuw wachtwoord instellen" }] }),
38
+ component: function ResetPasswordRouteComponent() {
39
+ return _jsx(ResetWachtwoordPage, { redirectTo: landing, loginPath: loginPath });
40
+ },
41
+ };
42
+ }
43
+ /** `/`: redirect in beforeLoad, dus er wordt niets gerenderd (geen flits). */
44
+ export function landingRoute({ landing }) {
45
+ return {
46
+ beforeLoad: () => {
47
+ throw redirect({ href: landing });
48
+ },
49
+ component: function LandingRouteComponent() {
50
+ return null;
51
+ },
52
+ };
53
+ }
54
+ /** `/auth/handover`: client-only, want de sessie staat alleen in localStorage. */
55
+ export function handoverRoute({ landing, loginPath = "/login" }) {
56
+ return {
57
+ ssr: false,
58
+ head: () => ({ meta: [{ title: "Aanmelden" }] }),
59
+ component: function HandoverRouteComponent() {
60
+ return _jsx(HandoverPage, { redirectTo: landing, loginPath: loginPath });
61
+ },
62
+ };
63
+ }
@@ -3,6 +3,6 @@ export type { ProviderAccount } from "./provider-token.js";
3
3
  export { exchangeHandoverCode, HandoverExchangeError } from "./provider-token.js";
4
4
  export type { HandoverSession } from "./provider-token.js";
5
5
  export { CentralAuthConfigError, ProviderNotConnectedError, ProviderUpstreamError, CentralAuthUnavailableError, AmbiguousAccountError, } from "./provider-token.js";
6
- export { requireModulePermission } from "./permissions.server.js";
6
+ export { requireModulePermission, getRoleFlags, isSuperadminServer } from "./permissions.server.js";
7
7
  export type { PermissionLevel } from "./permissions.server.js";
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACpG,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAClF,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,qBAAqB,EACrB,2BAA2B,EAC3B,qBAAqB,GACtB,MAAM,qBAAqB,CAAC;AAI7B,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAClE,YAAY,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACpG,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAClF,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,qBAAqB,EACrB,2BAA2B,EAC3B,qBAAqB,GACtB,MAAM,qBAAqB,CAAC;AAI7B,OAAO,EAAE,uBAAuB,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AACpG,YAAY,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC"}
@@ -3,4 +3,4 @@ export { exchangeHandoverCode, HandoverExchangeError } from "./provider-token.js
3
3
  export { CentralAuthConfigError, ProviderNotConnectedError, ProviderUpstreamError, CentralAuthUnavailableError, AmbiguousAccountError, } from "./provider-token.js";
4
4
  // Server-only rechtencheck. Uitsluitend via het "./server" subpath, nooit via
5
5
  // de hoofdbarrel.
6
- export { requireModulePermission } from "./permissions.server.js";
6
+ export { requireModulePermission, getRoleFlags, isSuperadminServer } from "./permissions.server.js";
@@ -1,16 +1,29 @@
1
1
  import type { SupabaseClient } from "@supabase/supabase-js";
2
2
  export type PermissionLevel = "none" | "view" | "edit";
3
3
  /**
4
- * Server-side rechtencheck via RPC `effective_module_permission()`.
4
+ * Server-side rechtencheck via RPC `effective_module_permission(_user_id,
5
+ * _module_id, _feature_id)` (3 argumenten).
5
6
  *
6
- * Bedoeld om te combineren met `requireSupabaseAuth`: geef de authenticated
7
- * client uit `context.supabase` mee.
7
+ * Sinds 1.0.23: `_user_id` komt uitsluitend uit de geverifieerde sessie van het
8
+ * verzoek; moduleniveau = `_feature_id ''`. **Vóór 1.0.23 liet deze functie
9
+ * alles door** (de RPC werd nooit gevonden → default-allow).
8
10
  *
9
- * - Ontbreekt de RPC (rechten-add-on niet geïnstalleerd) → toegestaan
10
- * (default-allow), zodat bestaande shells blijven werken.
11
- * - Onvoldoende rechten → gooit een `Response` met status 403.
12
- *
13
- * Core raakt géén enkele tabel uit het rechtenschema; alleen deze RPC.
11
+ * - Ontbreekt de RPC (add-on niet geïnstalleerd) → toegestaan (default-allow).
12
+ * - Geen geldige sessie → 401. Onvoldoende rechten → 403.
14
13
  */
15
14
  export declare function requireModulePermission(supabase: SupabaseClient<any, any, any>, moduleId: string, featureId?: string | null, level?: PermissionLevel): Promise<PermissionLevel>;
15
+ /**
16
+ * Sinds 1.0.23. Rolvlaggen uit geverifieerde claims (bijv. `context.claims` uit
17
+ * requireSupabaseAuth). superadmin ⇒ admin. Zichtbaarheid, geen beveiliging.
18
+ */
19
+ export declare function getRoleFlags(claims: Record<string, unknown> | undefined | null): {
20
+ roles: string[];
21
+ isSuperadmin: boolean;
22
+ isAdmin: boolean;
23
+ };
24
+ /**
25
+ * Sinds 1.0.23. Superadmin? Claims eerst, terugval tabel `user_roles` (RLS:
26
+ * eigen rijen). Zichtbaarheid, geen beveiliging.
27
+ */
28
+ export declare function isSuperadminServer(supabase: SupabaseClient<any, any, any>, userId: string, claims?: Record<string, unknown> | null): Promise<boolean>;
16
29
  //# sourceMappingURL=permissions.server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"permissions.server.d.ts","sourceRoot":"","sources":["../../src/core/server/permissions.server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAkBvD;;;;;;;;;;;GAWG;AACH,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EACvC,QAAQ,EAAE,MAAM,EAChB,SAAS,GAAE,MAAM,GAAG,IAAW,EAC/B,KAAK,GAAE,eAAwB,GAC9B,OAAO,CAAC,eAAe,CAAC,CAuB1B"}
1
+ {"version":3,"file":"permissions.server.d.ts","sourceRoot":"","sources":["../../src/core/server/permissions.server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAG5D,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAuCvD;;;;;;;;;;GAUG;AACH,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EACvC,QAAQ,EAAE,MAAM,EAChB,SAAS,GAAE,MAAM,GAAG,IAAW,EAC/B,KAAK,GAAE,eAAwB,GAC9B,OAAO,CAAC,eAAe,CAAC,CAyB1B;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,IAAI;;;;EAK9E;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EACvC,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GACtC,OAAO,CAAC,OAAO,CAAC,CAKlB"}
@@ -1,3 +1,4 @@
1
+ import { getRequest } from "@tanstack/react-start/server";
1
2
  const RANK = { none: 0, view: 1, edit: 2 };
2
3
  function isMissingRpc(error) {
3
4
  const e = (error ?? {});
@@ -10,22 +11,44 @@ function isMissingRpc(error) {
10
11
  message.includes("does not exist") ||
11
12
  message.includes("could not find the function"));
12
13
  }
14
+ /** Bearer-token van het huidige verzoek, of null. */
15
+ function requestToken() {
16
+ try {
17
+ const header = getRequest()?.headers?.get("authorization") ?? "";
18
+ return header.startsWith("Bearer ") ? header.slice(7) || null : null;
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
13
24
  /**
14
- * Server-side rechtencheck via RPC `effective_module_permission()`.
15
- *
16
- * Bedoeld om te combineren met `requireSupabaseAuth`: geef de authenticated
17
- * client uit `context.supabase` mee.
25
+ * Geverifieerde user-id: uitsluitend uit het token van het verzoek, gevalideerd
26
+ * bij Supabase Auth (`getUser(token)`). Geen aanroeper-input.
27
+ */
28
+ async function verifiedUserId(supabase) {
29
+ const token = requestToken();
30
+ const { data, error } = token ? await supabase.auth.getUser(token) : await supabase.auth.getUser();
31
+ if (error || !data?.user?.id)
32
+ throw new Response("Unauthorized", { status: 401 });
33
+ return data.user.id;
34
+ }
35
+ /**
36
+ * Server-side rechtencheck via RPC `effective_module_permission(_user_id,
37
+ * _module_id, _feature_id)` (3 argumenten).
18
38
  *
19
- * - Ontbreekt de RPC (rechten-add-on niet geïnstalleerd) → toegestaan
20
- * (default-allow), zodat bestaande shells blijven werken.
21
- * - Onvoldoende rechten → gooit een `Response` met status 403.
39
+ * Sinds 1.0.23: `_user_id` komt uitsluitend uit de geverifieerde sessie van het
40
+ * verzoek; moduleniveau = `_feature_id ''`. **Vóór 1.0.23 liet deze functie
41
+ * alles door** (de RPC werd nooit gevonden → default-allow).
22
42
  *
23
- * Core raakt géén enkele tabel uit het rechtenschema; alleen deze RPC.
43
+ * - Ontbreekt de RPC (add-on niet geïnstalleerd) → toegestaan (default-allow).
44
+ * - Geen geldige sessie → 401. Onvoldoende rechten → 403.
24
45
  */
25
46
  export async function requireModulePermission(supabase, moduleId, featureId = null, level = "view") {
47
+ const userId = await verifiedUserId(supabase);
26
48
  const { data, error } = await supabase.rpc("effective_module_permission", {
49
+ _user_id: userId,
27
50
  _module_id: moduleId,
28
- _feature_id: featureId,
51
+ _feature_id: featureId ?? "",
29
52
  });
30
53
  if (error) {
31
54
  if (isMissingRpc(error))
@@ -39,3 +62,25 @@ export async function requireModulePermission(supabase, moduleId, featureId = nu
39
62
  }
40
63
  return effective;
41
64
  }
65
+ /**
66
+ * Sinds 1.0.23. Rolvlaggen uit geverifieerde claims (bijv. `context.claims` uit
67
+ * requireSupabaseAuth). superadmin ⇒ admin. Zichtbaarheid, geen beveiliging.
68
+ */
69
+ export function getRoleFlags(claims) {
70
+ const raw = claims?.["user_roles"];
71
+ const roles = Array.isArray(raw) ? raw.map(String) : [];
72
+ const isSuperadmin = roles.includes("superadmin");
73
+ return { roles, isSuperadmin, isAdmin: isSuperadmin || roles.includes("admin") };
74
+ }
75
+ /**
76
+ * Sinds 1.0.23. Superadmin? Claims eerst, terugval tabel `user_roles` (RLS:
77
+ * eigen rijen). Zichtbaarheid, geen beveiliging.
78
+ */
79
+ export async function isSuperadminServer(supabase, userId, claims) {
80
+ if (getRoleFlags(claims).isSuperadmin)
81
+ return true;
82
+ const { data, error } = await supabase.from("user_roles").select("role").eq("user_id", userId);
83
+ if (error || !Array.isArray(data))
84
+ return false;
85
+ return data.some((r) => r.role === "superadmin");
86
+ }
@@ -0,0 +1,26 @@
1
+ import type { FlowModule } from "../types/module.js";
2
+ /**
3
+ * Sinds 1.0.23. Context vanuit AuthenticatedLayout.
4
+ * `modules` = de ruwe lijst van de shell (vóór transforms);
5
+ * `visibleModules` = wat de zijbalk toont (na rechten + extensies).
6
+ * Zichtbaarheid, geen beveiliging.
7
+ */
8
+ export interface ShellContextValue {
9
+ shellId?: string;
10
+ modules: FlowModule[];
11
+ visibleModules: FlowModule[];
12
+ /** True tijdens het eerste laden van de rechten. */
13
+ permissionsLoading: boolean;
14
+ }
15
+ export declare const ShellContext: import("react").Context<ShellContextValue | null>;
16
+ /** Sinds 1.0.23. `null` buiten AuthenticatedLayout. Zichtbaarheid, geen beveiliging. */
17
+ export declare function useShell(): ShellContextValue | null;
18
+ /**
19
+ * Sinds 1.0.23. Vorm uit contract §6.1. `null` = geen provider; een lege of
20
+ * ontbrekende lijst mag nooit iets deactiveren. Zichtbaarheid, geen beveiliging.
21
+ */
22
+ export declare function useShellModules(): {
23
+ modules: FlowModule[];
24
+ shellId?: string;
25
+ } | null;
26
+ //# sourceMappingURL=ShellContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ShellContext.d.ts","sourceRoot":"","sources":["../../src/core/shell/ShellContext.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,cAAc,EAAE,UAAU,EAAE,CAAC;IAC7B,oDAAoD;IACpD,kBAAkB,EAAE,OAAO,CAAC;CAC7B;AAED,eAAO,MAAM,YAAY,mDAAgD,CAAC;AAE1E,wFAAwF;AACxF,wBAAgB,QAAQ,IAAI,iBAAiB,GAAG,IAAI,CAEnD;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI;IAAE,OAAO,EAAE,UAAU,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAGpF"}
@@ -0,0 +1,14 @@
1
+ import { createContext, useContext } from "react";
2
+ export const ShellContext = createContext(null);
3
+ /** Sinds 1.0.23. `null` buiten AuthenticatedLayout. Zichtbaarheid, geen beveiliging. */
4
+ export function useShell() {
5
+ return useContext(ShellContext);
6
+ }
7
+ /**
8
+ * Sinds 1.0.23. Vorm uit contract §6.1. `null` = geen provider; een lege of
9
+ * ontbrekende lijst mag nooit iets deactiveren. Zichtbaarheid, geen beveiliging.
10
+ */
11
+ export function useShellModules() {
12
+ const ctx = useContext(ShellContext);
13
+ return ctx ? { modules: ctx.modules, shellId: ctx.shellId } : null;
14
+ }
@@ -0,0 +1,127 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+
4
+
5
+ @custom-variant dark (&:is(.dark *));
6
+
7
+ @layer base {
8
+ button, [role="button"], a, [type="button"], [type="submit"], [type="reset"] {
9
+ cursor: pointer;
10
+ }
11
+ }
12
+
13
+ @theme inline {
14
+ --radius-sm: calc(var(--radius) - 4px);
15
+ --radius-md: calc(var(--radius) - 2px);
16
+ --radius-lg: var(--radius);
17
+ --radius-xl: calc(var(--radius) + 4px);
18
+ --radius-2xl: calc(var(--radius) + 8px);
19
+ --radius-3xl: calc(var(--radius) + 12px);
20
+ --radius-4xl: calc(var(--radius) + 16px);
21
+ --color-background: var(--background);
22
+ --color-foreground: var(--foreground);
23
+ --color-card: var(--card);
24
+ --color-card-foreground: var(--card-foreground);
25
+ --color-popover: var(--popover);
26
+ --color-popover-foreground: var(--popover-foreground);
27
+ --color-primary: var(--primary);
28
+ --color-primary-foreground: var(--primary-foreground);
29
+ --color-secondary: var(--secondary);
30
+ --color-secondary-foreground: var(--secondary-foreground);
31
+ --color-muted: var(--muted);
32
+ --color-muted-foreground: var(--muted-foreground);
33
+ --color-accent: var(--accent);
34
+ --color-accent-foreground: var(--accent-foreground);
35
+ --color-destructive: var(--destructive);
36
+ --color-destructive-foreground: var(--destructive-foreground);
37
+ --color-success: var(--success);
38
+ --color-success-foreground: var(--success-foreground);
39
+ --color-warning: var(--warning);
40
+ --color-warning-foreground: var(--warning-foreground);
41
+ --color-border: var(--border);
42
+ --color-input: var(--input);
43
+ --color-ring: var(--ring);
44
+ --color-chart-1: var(--chart-1);
45
+ --color-chart-2: var(--chart-2);
46
+ --color-chart-3: var(--chart-3);
47
+ --color-chart-4: var(--chart-4);
48
+ --color-chart-5: var(--chart-5);
49
+ --color-sidebar: var(--sidebar);
50
+ --color-sidebar-foreground: var(--sidebar-foreground);
51
+ --color-sidebar-primary: var(--sidebar-primary);
52
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
53
+ --color-sidebar-accent: var(--sidebar-accent);
54
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
55
+ --color-sidebar-border: var(--sidebar-border);
56
+ --color-sidebar-ring: var(--sidebar-ring);
57
+ --color-sidebar-muted: var(--sidebar-muted);
58
+ --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
59
+ }
60
+
61
+ :root {
62
+ --radius: 0.5rem;
63
+ --background: hsl(210 20% 98%);
64
+ --foreground: hsl(214 79% 15%);
65
+ --card: hsl(0 0% 100%);
66
+ --card-foreground: hsl(214 79% 15%);
67
+ --popover: hsl(0 0% 100%);
68
+ --popover-foreground: hsl(214 79% 15%);
69
+ --primary: hsl(214 79% 33%);
70
+ --primary-foreground: hsl(0 0% 100%);
71
+ --secondary: hsl(214 30% 93%);
72
+ --secondary-foreground: hsl(214 79% 33%);
73
+ --muted: hsl(210 20% 96%);
74
+ --muted-foreground: hsl(214 20% 46%);
75
+ --accent: hsl(214 60% 90%);
76
+ --accent-foreground: hsl(214 79% 25%);
77
+ --destructive: hsl(0 72% 51%);
78
+ --destructive-foreground: hsl(0 0% 100%);
79
+ --success: hsl(142 71% 45%);
80
+ --success-foreground: hsl(0 0% 100%);
81
+ --warning: hsl(38 92% 50%);
82
+ --warning-foreground: hsl(0 0% 100%);
83
+ --border: hsl(214 20% 88%);
84
+ --input: hsl(214 20% 88%);
85
+ --ring: hsl(214 79% 33%);
86
+ --chart-1: hsl(214 79% 33%);
87
+ --chart-2: hsl(142 71% 45%);
88
+ --chart-3: hsl(38 92% 50%);
89
+ --chart-4: hsl(0 72% 51%);
90
+ --chart-5: hsl(280 60% 50%);
91
+ --sidebar: hsl(214 79% 33%);
92
+ --sidebar-foreground: hsl(0 0% 100%);
93
+ --sidebar-primary: hsl(0 0% 100%);
94
+ --sidebar-primary-foreground: hsl(214 79% 33%);
95
+ --sidebar-accent: hsl(214 79% 40%);
96
+ --sidebar-accent-foreground: hsl(0 0% 100%);
97
+ --sidebar-border: hsl(214 79% 45%);
98
+ --sidebar-ring: hsl(0 0% 100%);
99
+ --sidebar-muted: hsl(214 60% 70%);
100
+ }
101
+
102
+ @layer base {
103
+ * {
104
+ border-color: var(--color-border);
105
+ }
106
+
107
+ body {
108
+ background-color: var(--color-background);
109
+ color: var(--color-foreground);
110
+ font-family: var(--font-sans);
111
+ }
112
+
113
+ .sidebar-scroll::-webkit-scrollbar {
114
+ width: 6px;
115
+ }
116
+ .sidebar-scroll::-webkit-scrollbar-track {
117
+ background: var(--color-sidebar);
118
+ }
119
+ .sidebar-scroll::-webkit-scrollbar-thumb {
120
+ background: var(--color-sidebar-accent);
121
+ border-radius: 3px;
122
+ }
123
+ .sidebar-scroll {
124
+ scrollbar-color: var(--color-sidebar-accent) var(--color-sidebar);
125
+ scrollbar-width: thin;
126
+ }
127
+ }
@@ -4,11 +4,35 @@ export interface NavItem {
4
4
  label: string;
5
5
  href: string;
6
6
  icon: LucideIcon;
7
+ /**
8
+ * Optionele feature-id voor rechten (sinds 1.0.23). Zonder deze waarde leidt
9
+ * core hem af uit `href` via `featureKey()`.
10
+ * Zichtbaarheid, geen beveiliging.
11
+ */
12
+ featureId?: string;
7
13
  children?: Omit<NavItem, "children">[];
8
14
  }
9
15
  export interface SettingsCard {
10
16
  component: ComponentType;
11
17
  order: number;
18
+ /**
19
+ * Optioneel (sinds 1.0.23): module waar de kaart rechten van erft. Standaard
20
+ * de module waarin de kaart staat. Zichtbaarheid, geen beveiliging.
21
+ */
22
+ moduleId?: string;
23
+ /** Optioneel (sinds 1.0.23): feature-id; zonder → moduleniveau. Zichtbaarheid, geen beveiliging. */
24
+ featureId?: string;
25
+ }
26
+ /**
27
+ * Featureregistratie (sinds 1.0.23). Alleen typen; schrijven doet de adminmodule
28
+ * (die `label` naar `name` mapt). Zichtbaarheid, geen beveiliging.
29
+ */
30
+ export interface FeatureDefinition {
31
+ /** Pad onder het modulesegment, `/`-gescheiden, bijv. `prijsgroepen/nieuw`. */
32
+ id: string;
33
+ label: string;
34
+ /** Alleen UI-groepering; de database kent geen hiërarchie. */
35
+ parent?: string;
12
36
  }
13
37
  export interface FlowModule {
14
38
  id: string;
@@ -22,6 +46,8 @@ export interface FlowModule {
22
46
  * gelijk is aan de registry-slug.
23
47
  */
24
48
  registryKey?: string;
49
+ /** Optioneel (sinds 1.0.23). Zonder → afgeleid uit `nav.children` (`moduleFeatures`). */
50
+ features?: FeatureDefinition[];
25
51
  }
26
52
  /**
27
53
  * De externe modulesleutel.
@@ -32,12 +58,21 @@ export interface FlowModule {
32
58
  * 3. afsluitende underscores worden gestript.
33
59
  *
34
60
  * Waarom (3): een afsluitende underscore in een routepad is een TanStack
35
- * un-nesting-conventie (`/voorraad_/detail`), geen betekenisvol segment. Zonder
36
- * strippen levert dezelfde module twee sleutels op (`voorraad` en `voorraad_`)
37
- * en verdwijnt hij half uit de zijbalk. Strippen is gekozen boven "registryKey
38
- * verplicht maken", omdat dat laatste bestaande modules stil zou breken.
61
+ * un-nesting-conventie (`/voorraad_/detail`), geen betekenisvol segment.
39
62
  */
40
63
  export declare function moduleKey(mod: FlowModule): string;
41
64
  /** Zelfde afleidingsregel, maar voor een los pad (nav-children, URL-pathname). */
42
65
  export declare function navKey(href: string): string;
66
+ /**
67
+ * Feature-id uit een pad (sinds 1.0.23): alles na het modulesegment,
68
+ * `/`-gescheiden; `index` en `$param`-segmenten vallen weg.
69
+ * `/klanten/prijsgroepen/nieuw` → `prijsgroepen/nieuw`; `/klanten` → `""`.
70
+ * Zichtbaarheid, geen beveiliging.
71
+ */
72
+ export declare function featureKey(href: string): string;
73
+ /**
74
+ * Features van een module (sinds 1.0.23): `features` als opgegeven, anders
75
+ * afgeleid uit `nav.children`. Zichtbaarheid, geen beveiliging.
76
+ */
77
+ export declare function moduleFeatures(mod: FlowModule): FeatureDefinition[];
43
78
  //# sourceMappingURL=module.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../../src/core/types/module.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,CAAC;CACxC;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,aAAa,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,OAAO,CAAC;IACb,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAGjD;AAED,kFAAkF;AAClF,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG3C"}
1
+ {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../../src/core/types/module.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,CAAC;IACjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,CAAC;CACxC;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,aAAa,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oGAAoG;IACpG,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,+EAA+E;IAC/E,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,OAAO,CAAC;IACb,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yFAAyF;IACzF,QAAQ,CAAC,EAAE,iBAAiB,EAAE,CAAC;CAChC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAGjD;AAMD,kFAAkF;AAClF,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG3C;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM/C;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,UAAU,GAAG,iBAAiB,EAAE,CAQnE"}
@@ -7,18 +7,46 @@
7
7
  * 3. afsluitende underscores worden gestript.
8
8
  *
9
9
  * Waarom (3): een afsluitende underscore in een routepad is een TanStack
10
- * un-nesting-conventie (`/voorraad_/detail`), geen betekenisvol segment. Zonder
11
- * strippen levert dezelfde module twee sleutels op (`voorraad` en `voorraad_`)
12
- * en verdwijnt hij half uit de zijbalk. Strippen is gekozen boven "registryKey
13
- * verplicht maken", omdat dat laatste bestaande modules stil zou breken.
10
+ * un-nesting-conventie (`/voorraad_/detail`), geen betekenisvol segment.
14
11
  */
15
12
  export function moduleKey(mod) {
16
13
  if (mod.registryKey)
17
14
  return mod.registryKey;
18
15
  return navKey(mod.nav.href);
19
16
  }
17
+ function segments(href) {
18
+ return href.split("?")[0].split("#")[0].split("/").filter(Boolean);
19
+ }
20
20
  /** Zelfde afleidingsregel, maar voor een los pad (nav-children, URL-pathname). */
21
21
  export function navKey(href) {
22
- const segment = href.split("?")[0].split("#")[0].split("/").filter(Boolean)[0] ?? "";
22
+ const segment = segments(href)[0] ?? "";
23
23
  return segment.replace(/_+$/, "");
24
24
  }
25
+ /**
26
+ * Feature-id uit een pad (sinds 1.0.23): alles na het modulesegment,
27
+ * `/`-gescheiden; `index` en `$param`-segmenten vallen weg.
28
+ * `/klanten/prijsgroepen/nieuw` → `prijsgroepen/nieuw`; `/klanten` → `""`.
29
+ * Zichtbaarheid, geen beveiliging.
30
+ */
31
+ export function featureKey(href) {
32
+ return segments(href)
33
+ .slice(1)
34
+ .filter((s) => s !== "index" && !s.startsWith("$"))
35
+ .map((s) => s.replace(/_+$/, ""))
36
+ .join("/");
37
+ }
38
+ /**
39
+ * Features van een module (sinds 1.0.23): `features` als opgegeven, anders
40
+ * afgeleid uit `nav.children`. Zichtbaarheid, geen beveiliging.
41
+ */
42
+ export function moduleFeatures(mod) {
43
+ if (mod.features)
44
+ return mod.features;
45
+ const out = [];
46
+ for (const child of mod.nav.children ?? []) {
47
+ const id = child.featureId ?? featureKey(child.href);
48
+ if (id && !out.some((f) => f.id === id))
49
+ out.push({ id, label: child.label });
50
+ }
51
+ return out;
52
+ }