@lastbrain/app 2.0.31 → 2.0.35

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 (74) hide show
  1. package/dist/analytics/registry.d.ts +7 -0
  2. package/dist/analytics/registry.d.ts.map +1 -0
  3. package/dist/analytics/registry.js +11 -0
  4. package/dist/auth/useAuthSession.d.ts.map +1 -1
  5. package/dist/auth/useAuthSession.js +85 -1
  6. package/dist/cli.js +19 -3
  7. package/dist/components/LanguageSwitcher.d.ts.map +1 -1
  8. package/dist/components/LanguageSwitcher.js +89 -5
  9. package/dist/config/version.d.ts.map +1 -1
  10. package/dist/config/version.js +30 -19
  11. package/dist/i18n/useLink.d.ts.map +1 -1
  12. package/dist/i18n/useLink.js +15 -0
  13. package/dist/index.d.ts +3 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +4 -0
  16. package/dist/layouts/AdminLayoutWithSidebar.d.ts +3 -1
  17. package/dist/layouts/AdminLayoutWithSidebar.d.ts.map +1 -1
  18. package/dist/layouts/AdminLayoutWithSidebar.js +2 -2
  19. package/dist/layouts/AppProviders.d.ts +7 -1
  20. package/dist/layouts/AppProviders.d.ts.map +1 -1
  21. package/dist/layouts/AppProviders.js +24 -3
  22. package/dist/layouts/AuthLayout.js +1 -1
  23. package/dist/layouts/PublicLayout.js +1 -1
  24. package/dist/layouts/RootLayout.d.ts.map +1 -1
  25. package/dist/scripts/init-app.d.ts.map +1 -1
  26. package/dist/scripts/init-app.js +301 -138
  27. package/dist/scripts/module-build.d.ts.map +1 -1
  28. package/dist/scripts/module-build.js +402 -67
  29. package/dist/scripts/module-create.d.ts.map +1 -1
  30. package/dist/scripts/module-create.js +227 -10
  31. package/dist/scripts/sitemap-flat-generator.d.ts +39 -0
  32. package/dist/scripts/sitemap-flat-generator.d.ts.map +1 -0
  33. package/dist/scripts/sitemap-flat-generator.js +231 -0
  34. package/dist/scripts/sitemap-manifest-generator.d.ts +59 -0
  35. package/dist/scripts/sitemap-manifest-generator.d.ts.map +1 -0
  36. package/dist/scripts/sitemap-manifest-generator.js +290 -0
  37. package/dist/sitemap/manifest.d.ts +8 -0
  38. package/dist/sitemap/manifest.d.ts.map +1 -0
  39. package/dist/sitemap/manifest.js +6 -0
  40. package/dist/styles.css +2 -2
  41. package/dist/templates/AuthGuidePage.js +2 -0
  42. package/dist/templates/DefaultDoc.d.ts.map +1 -1
  43. package/dist/templates/DefaultDoc.js +9 -5
  44. package/dist/templates/DocPage.d.ts.map +1 -1
  45. package/dist/templates/DocPage.js +40 -0
  46. package/dist/templates/MigrationsGuidePage.js +2 -0
  47. package/dist/templates/ModuleGuidePage.d.ts.map +1 -1
  48. package/dist/templates/ModuleGuidePage.js +4 -1
  49. package/dist/templates/SimpleHomePage.js +2 -0
  50. package/package.json +11 -4
  51. package/src/analytics/registry.ts +14 -0
  52. package/src/auth/useAuthSession.ts +91 -1
  53. package/src/cli.ts +19 -3
  54. package/src/components/LanguageSwitcher.tsx +113 -23
  55. package/src/config/version.ts +30 -19
  56. package/src/i18n/useLink.ts +15 -0
  57. package/src/index.ts +17 -0
  58. package/src/layouts/AdminLayoutWithSidebar.tsx +4 -0
  59. package/src/layouts/AppProviders.tsx +66 -8
  60. package/src/layouts/AuthLayout.tsx +1 -1
  61. package/src/layouts/PublicLayout.tsx +1 -1
  62. package/src/layouts/RootLayout.tsx +0 -1
  63. package/src/scripts/init-app.ts +360 -149
  64. package/src/scripts/module-build.ts +458 -72
  65. package/src/scripts/module-create.ts +260 -10
  66. package/src/scripts/sitemap-flat-generator.ts +313 -0
  67. package/src/scripts/sitemap-manifest-generator.ts +476 -0
  68. package/src/sitemap/manifest.ts +17 -0
  69. package/src/templates/AuthGuidePage.tsx +1 -1
  70. package/src/templates/DefaultDoc.tsx +397 -6
  71. package/src/templates/DocPage.tsx +40 -0
  72. package/src/templates/MigrationsGuidePage.tsx +1 -1
  73. package/src/templates/ModuleGuidePage.tsx +3 -2
  74. package/src/templates/SimpleHomePage.tsx +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lastbrain/app",
3
- "version": "2.0.31",
3
+ "version": "2.0.35",
4
4
  "description": "Framework modulaire Next.js avec CLI et système de modules",
5
5
  "private": false,
6
6
  "type": "module",
@@ -48,11 +48,15 @@
48
48
  "types": "./dist/i18n/server-lang.d.ts",
49
49
  "import": "./dist/i18n/server-lang.js",
50
50
  "default": "./dist/i18n/server-lang.js"
51
+ },
52
+ "./sitemap/*": {
53
+ "types": "./dist/sitemap/*.d.ts",
54
+ "default": "./dist/sitemap/*.js"
51
55
  }
52
56
  },
53
57
  "dependencies": {
54
- "@lastbrain/core": "^2.0.27",
55
- "@lastbrain/ui": "^2.0.27",
58
+ "@lastbrain/core": "^2.0.31",
59
+ "@lastbrain/ui": "^2.0.31",
56
60
  "@supabase/supabase-js": "^2.84.0",
57
61
  "chalk": "^5.3.0",
58
62
  "commander": "^14.0.2",
@@ -60,7 +64,7 @@
60
64
  "glob": "^11.0.0",
61
65
  "inquirer": "^13.0.1",
62
66
  "lucide-react": "^0.554.0",
63
- "next": "^15.5.7",
67
+ "next": "^15.1.6",
64
68
  "next-themes": "^0.4.6",
65
69
  "react": "^19.2.1",
66
70
  "react-dom": "^19.2.1"
@@ -68,6 +72,9 @@
68
72
  "peerDependencies": {
69
73
  "next": ">=15.0.0"
70
74
  },
75
+ "peerDependenciesOptional": {
76
+ "@lastbrain-labs/module-billing-pro": "workspace:*"
77
+ },
71
78
  "devDependencies": {
72
79
  "@heroui/theme": "^2.4.23",
73
80
  "@tailwindcss/postcss": "^4.1.17",
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Global Analytics Registry
3
+ * Permet aux apps d'enregistrer un composant analytics global
4
+ */
5
+
6
+ let globalAnalyticsComponent: React.ComponentType | null = null;
7
+
8
+ export function registerAnalyticsComponent(component: React.ComponentType) {
9
+ globalAnalyticsComponent = component;
10
+ }
11
+
12
+ export function getAnalyticsComponent(): React.ComponentType | null {
13
+ return globalAnalyticsComponent;
14
+ }
@@ -1,9 +1,13 @@
1
1
  "use client";
2
2
 
3
3
  import { useEffect, useState } from "react";
4
- import { supabaseBrowserClient } from "@lastbrain/core";
4
+ import { supabaseBrowserClient, useRealtimeListener } from "@lastbrain/core";
5
5
  import type { User } from "@supabase/supabase-js";
6
6
 
7
+ // Note: this hook also listens to `user_profile_updated` realtime broadcasts
8
+ // and centralises cookie + redirect logic so the app reacts to profile language
9
+ // changes in a single place.
10
+
7
11
  export function useAuthSession() {
8
12
  const [user, setUser] = useState<User | null>(null);
9
13
  const [loading, setLoading] = useState(true);
@@ -54,5 +58,91 @@ export function useAuthSession() {
54
58
  return () => subscription.unsubscribe();
55
59
  }, []);
56
60
 
61
+ // Realtime listener for user_profile updates (broadcast from RealtimeProvider)
62
+ // This keeps NEXT_LOCALE in sync and performs a single, centralised redirect
63
+ // when the user's language changes remotely.
64
+
65
+ useRealtimeListener({
66
+ table: "user_profil",
67
+ // broadcast event produced by RealtimeProvider is `user_profile_updated`
68
+ eventName: "user_profile_updated",
69
+ onUpdate(payload) {
70
+ try {
71
+ // Recherche robuste d'un objet contenant la propriété `language` ou `locale`
72
+ const findLangRecord = (obj: any) => {
73
+ if (!obj || typeof obj !== "object") return null;
74
+ const seen = new WeakSet();
75
+ const queue: any[] = [obj];
76
+ while (queue.length) {
77
+ const cur = queue.shift();
78
+ if (!cur || typeof cur !== "object" || seen.has(cur)) continue;
79
+ seen.add(cur);
80
+ if ("language" in cur || "locale" in cur) return cur;
81
+ for (const k of Object.keys(cur)) {
82
+ const v = cur[k];
83
+ if (v && typeof v === "object") queue.push(v);
84
+ }
85
+ }
86
+ return null;
87
+ };
88
+
89
+ const candidate =
90
+ findLangRecord(payload) ||
91
+ payload?.new ||
92
+ payload?.record?.new ||
93
+ payload?.payload?.new ||
94
+ payload?.after ||
95
+ payload?.data ||
96
+ null;
97
+
98
+ const newLang = candidate?.language || candidate?.locale || null;
99
+
100
+ if (!newLang) {
101
+ console.debug(
102
+ "useAuthSession: realtime payload did not contain language",
103
+ payload
104
+ );
105
+ return;
106
+ }
107
+
108
+ // Read current cookie
109
+ const cookie = document.cookie
110
+ .split("; ")
111
+ .find((c) => c.startsWith("NEXT_LOCALE="));
112
+ const cookieLang = cookie ? cookie.split("=")[1] : null;
113
+ if (cookieLang === newLang) return;
114
+
115
+ // Set cookie and navigate to the language-prefixed URL
116
+ try {
117
+ const maxAge = 365 * 24 * 60 * 60;
118
+ document.cookie = `NEXT_LOCALE=${newLang}; Path=/; Max-Age=${maxAge}; SameSite=Lax`;
119
+ } catch (_e) {
120
+ // ignore
121
+ }
122
+
123
+ try {
124
+ sessionStorage.setItem("locale-change-ts", Date.now().toString());
125
+ } catch (_e) {
126
+ /* ignore */
127
+ }
128
+
129
+ try {
130
+ const pathname = window.location.pathname;
131
+ const pathWithoutLang =
132
+ pathname.replace(/^\/[a-z]{2}(?=\/|$)/, "") || "/";
133
+ const safePath = pathWithoutLang.startsWith("/api")
134
+ ? "/"
135
+ : pathWithoutLang;
136
+ const search = window.location.search || "";
137
+ window.location.href = `/${newLang}${safePath}${search}`;
138
+ } catch (_e) {
139
+ // ignore navigation errors
140
+ }
141
+ } catch (e) {
142
+ console.debug("useAuthSession: error handling realtime payload", e);
143
+ }
144
+ },
145
+ });
146
+
57
147
  return { user, loading, isSuperAdmin };
58
148
  }
package/src/cli.ts CHANGED
@@ -27,9 +27,25 @@ program
27
27
  )
28
28
  .action(async (directory: string | undefined, options) => {
29
29
  try {
30
- const targetDir = directory
31
- ? path.resolve(process.cwd(), directory)
32
- : process.cwd();
30
+ let targetDir: string;
31
+
32
+ if (directory) {
33
+ // Si on est dans un monorepo (détecté par pnpm-workspace.yaml à la racine)
34
+ const fs = await import("fs-extra");
35
+ const cwd = process.cwd();
36
+ const workspaceFile = path.join(cwd, "pnpm-workspace.yaml");
37
+ const isMonorepo = await fs.pathExists(workspaceFile);
38
+
39
+ if (isMonorepo) {
40
+ // Dans un monorepo, créer dans apps/
41
+ targetDir = path.resolve(cwd, "apps", directory);
42
+ } else {
43
+ // Hors monorepo, créer où demandé
44
+ targetDir = path.resolve(cwd, directory);
45
+ }
46
+ } else {
47
+ targetDir = process.cwd();
48
+ }
33
49
 
34
50
  await initApp({
35
51
  force: options.force || false,
@@ -3,6 +3,8 @@
3
3
  import { useEffect, useState } from "react";
4
4
  import { useRouter } from "next/navigation";
5
5
  import { useLanguage } from "../i18n/LanguageProvider";
6
+ import { eventBus } from "@lastbrain/core";
7
+ import { useAuth } from "@lastbrain/core";
6
8
  import {
7
9
  Button,
8
10
  Dropdown,
@@ -37,7 +39,9 @@ export function LanguageSwitcher({
37
39
  }, []);
38
40
 
39
41
  // Fonction pour changer la langue avec redirection correcte
40
- const handleLanguageChange = (newLang: string) => {
42
+ const { user } = useAuth();
43
+
44
+ const handleLanguageChange = async (newLang: string) => {
41
45
  const currentPath = window.location.pathname;
42
46
  // Extraire les segments du chemin
43
47
  const segments = currentPath.split("/").filter(Boolean);
@@ -52,8 +56,80 @@ export function LanguageSwitcher({
52
56
  const pathWithoutLang = segments.length > 0 ? "/" + segments.join("/") : "";
53
57
  const newPath = `/${newLang}${pathWithoutLang}`;
54
58
 
55
- // Naviguer directement - le LanguageProvider lira la nouvelle langue depuis l'URL
56
- window.location.href = newPath;
59
+ // Si l'utilisateur est connecté, mettre à jour sa préférence en BDD
60
+ if (user) {
61
+ try {
62
+ const resp = await fetch("/api/auth/profile", {
63
+ method: "PATCH",
64
+ headers: { "Content-Type": "application/json" },
65
+ credentials: "include",
66
+ body: JSON.stringify({ language: newLang }),
67
+ });
68
+
69
+ // attendre la confirmation du serveur que la locale a bien été mise à jour
70
+ // afin d'éviter un reload avant que le cookie soit appliqué côté serveur.
71
+ try {
72
+ const json = await resp.json();
73
+ if (json && json.data) {
74
+ // émettre localement l'événement realtime pour propager immédiatement
75
+ try {
76
+ eventBus.emit("user_profile_updated", { new: json.data });
77
+ } catch (e) {
78
+ /* ignore */
79
+ }
80
+ } else {
81
+ // si serveur n'a pas renvoyé de données attendues, on continue
82
+ }
83
+ } catch (e) {
84
+ // ignore json parse errors
85
+ }
86
+ } catch (e) {
87
+ console.debug("language switch: failed to update profile language", e);
88
+ }
89
+ }
90
+
91
+ // Mettre à jour le contexte local immédiatement (UX local)
92
+ try {
93
+ setLangFromContext(newLang);
94
+ } catch (_e) {
95
+ // ignore
96
+ }
97
+
98
+ // Si l'utilisateur n'est pas authentifié, écrire immédiatement
99
+ // le cookie NEXT_LOCALE et forcer un refresh pour que Next.js
100
+ // prenne en compte la nouvelle locale côté serveur.
101
+ if (!user) {
102
+ try {
103
+ // 1 an
104
+ document.cookie = `NEXT_LOCALE=${newLang}; Path=/; Max-Age=${31536000}; SameSite=Lax`;
105
+ } catch (e) {
106
+ // ignore
107
+ }
108
+
109
+ try {
110
+ // Navigate to the same path prefixed with the new locale,
111
+ // preserving search and hash.
112
+ const target = `${newPath}${window.location.search || ""}${
113
+ window.location.hash || ""
114
+ }`;
115
+ router.push(target);
116
+ } catch (e) {
117
+ // fallback: navigate to the same path but with the new locale
118
+ try {
119
+ window.location.assign(
120
+ `${newPath}${window.location.search || ""}${
121
+ window.location.hash || ""
122
+ }`
123
+ );
124
+ } catch (_err) {
125
+ /* ignore */
126
+ }
127
+ }
128
+ return;
129
+ }
130
+
131
+ // Pour utilisateurs authentifiés, la redirection et l'écriture
132
+ // du cookie NEXT_LOCALE sont gérées par le listener realtime centralisé.
57
133
  };
58
134
 
59
135
  useEffect(() => {
@@ -136,29 +212,37 @@ export function LanguageSwitcher({
136
212
 
137
213
  if (variant === "minimal") {
138
214
  return (
139
- <Dropdown size="sm" className="px-0 m-0">
140
- <DropdownTrigger className="px-0 m-0">
141
- {renderFlag(lang)}
142
- </DropdownTrigger>
143
- <DropdownMenu>
144
- {availableLanguages.map((code) => (
145
- <DropdownItem
146
- key={code}
147
- onPress={() => handleLanguageChange(code)}
148
- startContent={renderFlag(code)}
149
- className={lang === code ? "bg-primary/10" : ""}
150
- >
151
- {code === "fr" ? "Français" : code === "en" ? "English" : code}
152
- {isHydrated && lang === code && ""}
153
- </DropdownItem>
154
- ))}
155
- </DropdownMenu>
156
- </Dropdown>
215
+ <div className="">
216
+ <Dropdown size="sm" className="px-0 m-0">
217
+ <DropdownTrigger className="px-0 m-0">
218
+ {renderFlag(lang)}
219
+ </DropdownTrigger>
220
+ <DropdownMenu>
221
+ {availableLanguages.map((code) => (
222
+ <DropdownItem
223
+ key={code}
224
+ onPress={() => handleLanguageChange(code)}
225
+ startContent={renderFlag(code)}
226
+ className={lang === code ? "bg-primary/10" : ""}
227
+ >
228
+ {code === "fr"
229
+ ? "Français"
230
+ : code === "en"
231
+ ? "English"
232
+ : code === "es"
233
+ ? "Spanish"
234
+ : code}
235
+ {isHydrated && lang === code && " ✓"}
236
+ </DropdownItem>
237
+ ))}
238
+ </DropdownMenu>
239
+ </Dropdown>
240
+ </div>
157
241
  );
158
242
  }
159
243
 
160
244
  return (
161
- <Dropdown size="sm" className="px-0">
245
+ <Dropdown size="sm" className="px-0 ">
162
246
  <DropdownTrigger className="px-0 m-0">
163
247
  <Button
164
248
  variant="light"
@@ -179,7 +263,13 @@ export function LanguageSwitcher({
179
263
  className={lang === code ? "bg-primary/10" : ""}
180
264
  >
181
265
  {renderFlag(code)}{" "}
182
- {code === "fr" ? "Français" : code === "en" ? "English" : code}
266
+ {code === "fr"
267
+ ? "Français"
268
+ : code === "en"
269
+ ? "English"
270
+ : code === "es"
271
+ ? "Spanish"
272
+ : code}
183
273
  {isHydrated && lang === code && " ✓"}
184
274
  </DropdownItem>
185
275
  ))}
@@ -5,23 +5,34 @@
5
5
  */
6
6
 
7
7
  export const PACKAGE_VERSIONS: Record<string, string> = {
8
- "@lastbrain-labs/module-billing-pro": "^2.0.25",
9
- "@lastbrain-labs/module-cj-analyzer-pro": "^0.1.17",
10
- "@lastbrain-labs/module-core-cart-pro": "^2.0.25",
11
- "@lastbrain-labs/module-core-commerce-pro": "^2.0.25",
12
- "@lastbrain-labs/module-core-order-pro": "^2.0.25",
13
- "@lastbrain-labs/module-core-payment-pro": "^2.0.25",
14
- "@lastbrain-labs/module-core-product-pro": "^2.0.25",
15
- "@lastbrain-labs/module-recipes-pro": "^2.0.27",
16
- "@lastbrain-labs/module-shop-pro": "^0.1.17",
17
- "@lastbrain/app": "^2.0.30",
18
- "@lastbrain/core": "^2.0.26",
19
- "@lastbrain/module-ai": "^2.0.25",
20
- "@lastbrain/module-auth": "^2.0.26",
21
- "@lastbrain/module-legal": "^2.0.25",
22
- "@lastbrain/module-project-board": "^2.0.25",
23
- "@lastbrain/module-tasks": "^2.0.25",
24
- "@lastbrain/ui": "^2.0.26",
25
- "apps/recipe": "^2.0.13",
26
- "lastbrain": "^2.0.13",
8
+ "@lastbrain-labs/metrics-ui": "^1.0.2",
9
+ "@lastbrain-labs/module-billing-pro": "^2.0.29",
10
+ "@lastbrain-labs/module-cj-analyzer-pro": "^0.1.21",
11
+ "@lastbrain-labs/module-contact-pro": "^0.1.2",
12
+ "@lastbrain-labs/module-core-cart-pro": "^2.0.29",
13
+ "@lastbrain-labs/module-core-commerce-pro": "^2.0.29",
14
+ "@lastbrain-labs/module-core-order-pro": "^2.0.29",
15
+ "@lastbrain-labs/module-core-payment-pro": "^2.0.29",
16
+ "@lastbrain-labs/module-core-product-pro": "^2.0.29",
17
+ "@lastbrain-labs/module-metrics-pro": "^0.1.2",
18
+ "@lastbrain-labs/module-recipes-pro": "^2.0.31",
19
+ "@lastbrain-labs/module-shop-pro": "^0.1.21",
20
+ "@lastbrain/ai-ui-core": "^1.0.2",
21
+ "@lastbrain/ai-ui-react": "^1.0.2",
22
+ "@lastbrain/ai-ui-theme-heroui": "^1.0.2",
23
+ "@lastbrain/app": "^2.0.34",
24
+ "@lastbrain/core": "^2.0.30",
25
+ "@lastbrain/module-ai": "^2.0.29",
26
+ "@lastbrain/module-audit-pro": "^0.1.2",
27
+ "@lastbrain/module-auth": "^2.0.30",
28
+ "@lastbrain/module-blog": "^0.1.2",
29
+ "@lastbrain/module-legal": "^2.0.29",
30
+ "@lastbrain/module-project-board": "^2.0.29",
31
+ "@lastbrain/module-tasks": "^2.0.29",
32
+ "@lastbrain/module-tools": "^0.1.2",
33
+ "@lastbrain/ui": "^2.0.30",
34
+ "audit": "^2.0.14",
35
+ "prompt": "^0.1.1",
36
+ "recipe": "^2.0.14",
37
+ "tools": "^2.0.14",
27
38
  };
@@ -3,6 +3,16 @@
3
3
  import { useLanguage } from "./LanguageProvider";
4
4
  import type { Language } from "./types";
5
5
 
6
+ /**
7
+ * Détecte si un chemin commence par un code de langue ISO 639-1 (2 lettres)
8
+ * Ex: /fr/auth/billing -> true, /auth/billing -> false
9
+ */
10
+ function startsWithLocale(href: string): boolean {
11
+ // Pattern: /XX/ ou /XX où XX est un code langue de 2 lettres minuscules
12
+ const localePattern = /^\/[a-z]{2}(\/|$)/;
13
+ return localePattern.test(href);
14
+ }
15
+
6
16
  /**
7
17
  * Helper pour transformer un URL avec le paramètre [lang]
8
18
  * Utile pour les transformations statiques avant le rendu React
@@ -17,11 +27,16 @@ export function langHref(href: string, lang: Language): string {
17
27
  const normalizedHref = String(href);
18
28
  const isAbsolute = normalizedHref.startsWith("http");
19
29
  const hasLangPrefix = normalizedHref.startsWith(`/${lang}/`);
30
+ const hasAnyLangPrefix = startsWithLocale(normalizedHref);
20
31
 
21
32
  if (isAbsolute) {
22
33
  return normalizedHref;
23
34
  } else if (hasLangPrefix) {
35
+ // Déjà préfixé avec la bonne langue
24
36
  return normalizedHref;
37
+ } else if (hasAnyLangPrefix) {
38
+ // Préfixé avec une autre langue - remplacer par la langue courante
39
+ return `/${lang}${normalizedHref.slice(3)}`;
25
40
  } else if (normalizedHref === "/") {
26
41
  return `/${lang}`;
27
42
  } else if (normalizedHref.startsWith("/api/")) {
package/src/index.ts CHANGED
@@ -22,6 +22,9 @@ export { AdminLayout } from "./layouts/AdminLayout";
22
22
  export { AdminLayoutWithSidebar } from "./layouts/AdminLayoutWithSidebar";
23
23
  export { getModuleConfigs } from "./modules/module-loader";
24
24
 
25
+ // Analytics
26
+ export { registerAnalyticsComponent } from "./analytics/registry";
27
+
25
28
  // i18n
26
29
  export {
27
30
  LanguageProvider,
@@ -42,6 +45,17 @@ export type {
42
45
  ModuleTranslations,
43
46
  } from "./i18n/types";
44
47
 
48
+ // Re-export types from core for scripts
49
+ export type {
50
+ ModuleBuildConfig,
51
+ ModuleApiConfig,
52
+ ModuleMenuItemConfig,
53
+ ModulePageConfig,
54
+ ModuleSection,
55
+ ModuleRealtimeConfig,
56
+ ModuleSitemapConfig,
57
+ } from "@lastbrain/core";
58
+
45
59
  // Components
46
60
  export { AppAside } from "@lastbrain/ui";
47
61
  export type { AppAsideMenuItem, AppAsideMenuConfig } from "@lastbrain/ui";
@@ -53,3 +67,6 @@ export { SimpleDocPage } from "./templates/SimpleDocPage";
53
67
  export { ModuleGuidePage } from "./templates/ModuleGuidePage";
54
68
  export { AuthGuidePage } from "./templates/AuthGuidePage";
55
69
  export { MigrationsGuidePage } from "./templates/MigrationsGuidePage";
70
+
71
+ // Sitemap manifest
72
+ export { sitemapManifest } from "./sitemap/manifest";
@@ -10,12 +10,14 @@ import {
10
10
  import { useAuthSession } from "../auth/useAuthSession";
11
11
  import { usePathname } from "next/navigation";
12
12
  import { useEffect, useState } from "react";
13
+ import type { MenuIgnored } from "../types/menu";
13
14
 
14
15
  interface AdminLayoutWithSidebarProps {
15
16
  children: React.ReactNode;
16
17
  menuConfig?: MenuConfig;
17
18
  className?: string;
18
19
  menuCustom?: MenuItem[];
20
+ menuIgnored?: MenuIgnored;
19
21
  }
20
22
 
21
23
  export function AdminLayoutWithSidebar({
@@ -23,6 +25,7 @@ export function AdminLayoutWithSidebar({
23
25
  menuConfig,
24
26
  className = "",
25
27
  menuCustom,
28
+ menuIgnored,
26
29
  }: AdminLayoutWithSidebarProps) {
27
30
  const { isSuperAdmin, loading, user } = useAuthSession();
28
31
  const pathname = usePathname();
@@ -109,6 +112,7 @@ export function AdminLayoutWithSidebar({
109
112
  isSuperAdmin={isSuperAdmin}
110
113
  isAuthenticated={!!user}
111
114
  className={className}
115
+ {...(menuIgnored ? { menuIgnored } : {})}
112
116
  {...(menuCustom ? { menuCustom } : {})}
113
117
  />
114
118
  )}
@@ -11,6 +11,36 @@ import type { User } from "@supabase/supabase-js";
11
11
  import type { ModuleRealtimeConfig } from "@lastbrain/core";
12
12
  import type { NotificationsData } from "../hooks/useNotifications";
13
13
 
14
+ // Wrapper for optional EntitlementsProvider passed via props
15
+ function OptionalEntitlementsWrapper({
16
+ children,
17
+ EntitlementsProviderComponent,
18
+ }: {
19
+ children: React.ReactNode;
20
+ EntitlementsProviderComponent?: React.ComponentType<{
21
+ children: React.ReactNode;
22
+ }>;
23
+ }) {
24
+ if (EntitlementsProviderComponent) {
25
+ return (
26
+ <EntitlementsProviderComponent>{children}</EntitlementsProviderComponent>
27
+ );
28
+ }
29
+ return <>{children}</>;
30
+ }
31
+
32
+ // Wrapper for optional AnalyticsListener
33
+ function OptionalAnalyticsListener({
34
+ AnalyticsListenerComponent,
35
+ }: {
36
+ AnalyticsListenerComponent?: React.ComponentType;
37
+ }) {
38
+ if (AnalyticsListenerComponent) {
39
+ return <AnalyticsListenerComponent />;
40
+ }
41
+ return null;
42
+ }
43
+
14
44
  const ModuleContext = createContext(getModuleConfigs());
15
45
  const NotificationContext = createContext<{
16
46
  data: NotificationsData;
@@ -66,6 +96,8 @@ export function AppProviders({
66
96
  lang = "fr",
67
97
  translations = {},
68
98
  availableLanguages = ["fr", "en"],
99
+ EntitlementsProviderComponent,
100
+ AnalyticsListenerComponent,
69
101
  }: {
70
102
  children: React.ReactNode;
71
103
  realtimeConfig?: ModuleRealtimeConfig[];
@@ -73,6 +105,12 @@ export function AppProviders({
73
105
  translations?: Record<string, string>;
74
106
  /** Liste des langues disponibles depuis locales.generated.ts */
75
107
  availableLanguages?: string[];
108
+ /** Provider optionnel pour les entitlements (billing) */
109
+ EntitlementsProviderComponent?: React.ComponentType<{
110
+ children: React.ReactNode;
111
+ }>;
112
+ /** Composant optionnel pour le tracking analytics */
113
+ AnalyticsListenerComponent?: React.ComponentType;
76
114
  }) {
77
115
  const modules = useMemo(() => getModuleConfigs(), []);
78
116
  const { user, loading: authLoading, isSuperAdmin } = useAuthSession();
@@ -107,14 +145,34 @@ export function AppProviders({
107
145
  >
108
146
  <AppLinkProvider lang={lang || "fr"}>
109
147
  <AuthContext.Provider value={authValue}>
110
- <ModuleContext.Provider value={modules}>
111
- <NotificationContext.Provider value={notificationsData}>
112
- <RealtimeProvider userId={user?.id} config={realtimeConfig}>
113
- {children}
114
- <ToastProvider placement="bottom-right" toastOffset={5} />
115
- </RealtimeProvider>
116
- </NotificationContext.Provider>
117
- </ModuleContext.Provider>
148
+ <OptionalEntitlementsWrapper
149
+ EntitlementsProviderComponent={EntitlementsProviderComponent}
150
+ >
151
+ <ModuleContext.Provider value={modules}>
152
+ <NotificationContext.Provider value={notificationsData}>
153
+ <RealtimeProvider userId={user?.id} config={realtimeConfig}>
154
+ <OptionalAnalyticsListener
155
+ AnalyticsListenerComponent={AnalyticsListenerComponent}
156
+ />
157
+
158
+ {children}
159
+ <ToastProvider
160
+ toastProps={{
161
+ variant: "flat",
162
+ timeout: 5000,
163
+ shouldShowTimeoutProgress: true,
164
+ classNames: {
165
+ closeButton:
166
+ "opacity-100 absolute right-4 top-1/2 -translate-y-1/2",
167
+ },
168
+ }}
169
+ placement="bottom-right"
170
+ toastOffset={5}
171
+ />
172
+ </RealtimeProvider>
173
+ </NotificationContext.Provider>
174
+ </ModuleContext.Provider>
175
+ </OptionalEntitlementsWrapper>
118
176
  </AuthContext.Provider>
119
177
  </AppLinkProvider>
120
178
  </LanguageProvider>
@@ -1,3 +1,3 @@
1
1
  export function AuthLayout({ children }: { children: React.ReactNode }) {
2
- return <div className="pt-4 px-2 md:px-5 max-w-screen">{children}</div>;
2
+ return <div className="pt-16 px-2 md:px-5 max-w-screen">{children}</div>;
3
3
  }
@@ -10,7 +10,7 @@ interface PublicLayoutProps {
10
10
  export function PublicLayout({ children, footerConfig }: PublicLayoutProps) {
11
11
  return (
12
12
  <>
13
- <section className="pt-16 min-h-[calc(100vh)] max-w-screen">
13
+ <section className=" min-h-[calc(100vh)] max-w-screen">
14
14
  {children}
15
15
  </section>
16
16
  {footerConfig && <Footer config={footerConfig} />}
@@ -4,7 +4,6 @@ import { ThemeProvider } from "next-themes";
4
4
  import { AppProviders } from "./AppProviders";
5
5
  import type { ModuleRealtimeConfig } from "@lastbrain/core";
6
6
  import type { Language } from "../i18n/LanguageProvider";
7
-
8
7
  // Note: L'app Next.js doit importer son propre globals.css dans son layout
9
8
  // Note: La configuration realtime doit être fournie par l'app qui utilise ce layout
10
9
  export function RootLayout({