@lastbrain/app 0.1.8 → 0.1.9

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 (39) hide show
  1. package/dist/scripts/init-app.js +0 -2
  2. package/package.json +3 -2
  3. package/src/app-shell/(admin)/layout.tsx +13 -0
  4. package/src/app-shell/(auth)/layout.tsx +13 -0
  5. package/src/app-shell/(public)/page.tsx +11 -0
  6. package/src/app-shell/layout.tsx +5 -0
  7. package/src/app-shell/not-found.tsx +28 -0
  8. package/src/auth/authHelpers.ts +24 -0
  9. package/src/auth/useAuthSession.ts +54 -0
  10. package/src/cli.ts +96 -0
  11. package/src/index.ts +21 -0
  12. package/src/layouts/AdminLayout.tsx +7 -0
  13. package/src/layouts/AppProviders.tsx +61 -0
  14. package/src/layouts/AuthLayout.tsx +7 -0
  15. package/src/layouts/PublicLayout.tsx +7 -0
  16. package/src/layouts/RootLayout.tsx +27 -0
  17. package/src/modules/module-loader.ts +14 -0
  18. package/src/scripts/README.md +262 -0
  19. package/src/scripts/db-init.ts +338 -0
  20. package/src/scripts/db-migrations-sync.ts +86 -0
  21. package/src/scripts/dev-sync.ts +218 -0
  22. package/src/scripts/init-app.ts +1077 -0
  23. package/src/scripts/module-add.ts +242 -0
  24. package/src/scripts/module-build.ts +502 -0
  25. package/src/scripts/module-create.ts +809 -0
  26. package/src/scripts/module-list.ts +37 -0
  27. package/src/scripts/module-remove.ts +367 -0
  28. package/src/scripts/readme-build.ts +60 -0
  29. package/src/styles.css +3 -0
  30. package/src/templates/AuthGuidePage.tsx +68 -0
  31. package/src/templates/DefaultDoc.tsx +462 -0
  32. package/src/templates/DocPage.tsx +381 -0
  33. package/src/templates/DocsPageWithModules.tsx +22 -0
  34. package/src/templates/MigrationsGuidePage.tsx +61 -0
  35. package/src/templates/ModuleGuidePage.tsx +71 -0
  36. package/src/templates/SimpleDocPage.tsx +587 -0
  37. package/src/templates/SimpleHomePage.tsx +385 -0
  38. package/src/templates/env.example/.env.example +6 -0
  39. package/src/templates/migrations/20201010100000_app_base.sql +228 -0
@@ -0,0 +1,37 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+ import chalk from "chalk";
4
+ import { AVAILABLE_MODULES } from "./module-add.js";
5
+
6
+ export async function listModules(targetDir: string) {
7
+ console.log(chalk.blue("\n📦 Modules disponibles:\n"));
8
+
9
+ // Lire la config des modules installés
10
+ const modulesConfigPath = path.join(targetDir, ".lastbrain", "modules.json");
11
+ let installedModules: string[] = [];
12
+
13
+ if (fs.existsSync(modulesConfigPath)) {
14
+ const modulesConfig = await fs.readJson(modulesConfigPath);
15
+ installedModules = modulesConfig.modules || [];
16
+ }
17
+
18
+ // Afficher tous les modules disponibles
19
+ AVAILABLE_MODULES.forEach((module) => {
20
+ const isInstalled = installedModules.includes(module.package);
21
+ const status = isInstalled
22
+ ? chalk.green("✓ installé")
23
+ : chalk.gray(" disponible");
24
+
25
+ console.log(chalk.bold(`${module.displayName}`));
26
+ console.log(chalk.gray(` Nom: ${module.name}`));
27
+ console.log(chalk.gray(` Package: ${module.package}`));
28
+ console.log(chalk.gray(` Description: ${module.description}`));
29
+ console.log(` Statut: ${status}`);
30
+ console.log();
31
+ });
32
+
33
+ console.log(chalk.gray("Pour ajouter un module:"));
34
+ console.log(chalk.cyan(" pnpm lastbrain add-module <nom>\n"));
35
+ console.log(chalk.gray("Pour supprimer un module:"));
36
+ console.log(chalk.cyan(" pnpm lastbrain remove-module <nom>\n"));
37
+ }
@@ -0,0 +1,367 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+ import chalk from "chalk";
4
+ import { execSync } from "child_process";
5
+ import inquirer from "inquirer";
6
+ import { AVAILABLE_MODULES } from "./module-add.js";
7
+
8
+ async function removeGeneratedFiles(
9
+ moduleName: string,
10
+ modulePackage: string,
11
+ targetDir: string
12
+ ) {
13
+ console.log(chalk.yellow("\n🗑️ Suppression des pages générées..."));
14
+
15
+ const appDirectory = path.join(targetDir, "app");
16
+ if (!fs.existsSync(appDirectory)) {
17
+ return;
18
+ }
19
+
20
+ // Fonction récursive pour trouver les fichiers
21
+ async function findAndRemoveFiles(dir: string) {
22
+ if (!fs.existsSync(dir)) return;
23
+
24
+ const entries = await fs.readdir(dir, { withFileTypes: true });
25
+
26
+ for (const entry of entries) {
27
+ const fullPath = path.join(dir, entry.name);
28
+
29
+ if (entry.isDirectory()) {
30
+ await findAndRemoveFiles(fullPath);
31
+ } else if (entry.name === "page.tsx" || entry.name === "route.ts") {
32
+ try {
33
+ const content = await fs.readFile(fullPath, "utf-8");
34
+ // Vérifier si le fichier importe depuis le module (y compris sous-chemins)
35
+ if (
36
+ content.includes(`"${modulePackage}`) ||
37
+ content.includes(`'${modulePackage}`)
38
+ ) {
39
+ await fs.remove(fullPath);
40
+ console.log(
41
+ chalk.gray(` ✓ Supprimé ${path.relative(targetDir, fullPath)}`)
42
+ );
43
+
44
+ // Supprimer le dossier parent si vide
45
+ const parentDir = path.dirname(fullPath);
46
+ const remainingFiles = await fs.readdir(parentDir);
47
+ if (remainingFiles.length === 0) {
48
+ await fs.remove(parentDir);
49
+ }
50
+ }
51
+ } catch (error) {
52
+ // Ignorer les erreurs de lecture
53
+ }
54
+ }
55
+ }
56
+ }
57
+
58
+ await findAndRemoveFiles(appDirectory);
59
+
60
+ // Supprimer le fichier navigation.generated.ts pour qu'il soit regénéré au prochain build
61
+ const navPath = path.join(appDirectory, "navigation.generated.ts");
62
+ if (fs.existsSync(navPath)) {
63
+ await fs.remove(navPath);
64
+ console.log(chalk.gray(" ✓ Navigation réinitialisée"));
65
+ }
66
+ }
67
+
68
+ export async function removeModule(moduleName: string, targetDir: string) {
69
+ console.log(chalk.blue(`\n🗑️ Suppression du module: ${moduleName}\n`));
70
+
71
+ const module = AVAILABLE_MODULES.find((m) => m.name === moduleName);
72
+ if (!module) {
73
+ console.error(
74
+ chalk.red(`❌ Module "${moduleName}" non trouvé. Modules disponibles:`)
75
+ );
76
+ AVAILABLE_MODULES.forEach((m) => {
77
+ console.log(chalk.gray(` - ${m.name}: ${m.description}`));
78
+ });
79
+ process.exit(1);
80
+ }
81
+
82
+ // 1. Vérifier qu'on est dans un projet LastBrain
83
+ const pkgPath = path.join(targetDir, "package.json");
84
+ if (!fs.existsSync(pkgPath)) {
85
+ console.error(chalk.red("❌ package.json non trouvé"));
86
+ process.exit(1);
87
+ }
88
+
89
+ const pkg = await fs.readJson(pkgPath);
90
+
91
+ // Vérifier si le module est installé (dans package.json OU modules.json)
92
+ const modulesConfigPath = path.join(targetDir, ".lastbrain", "modules.json");
93
+ let isInstalledInConfig = false;
94
+
95
+ if (fs.existsSync(modulesConfigPath)) {
96
+ const modulesConfig = await fs.readJson(modulesConfigPath);
97
+ isInstalledInConfig = modulesConfig.modules?.includes(module.package);
98
+ }
99
+
100
+ const isInstalledInPackage =
101
+ pkg.dependencies?.[module.package] || pkg.devDependencies?.[module.package];
102
+
103
+ if (!isInstalledInPackage && !isInstalledInConfig) {
104
+ console.log(
105
+ chalk.yellow(`⚠️ Module ${module.package} n'est pas installé`)
106
+ );
107
+ return;
108
+ }
109
+
110
+ // 2. Retirer la dépendance du package.json
111
+ console.log(
112
+ chalk.yellow(`📦 Suppression de ${module.package} des dépendances...`)
113
+ );
114
+ if (pkg.dependencies?.[module.package]) {
115
+ delete pkg.dependencies[module.package];
116
+ }
117
+ if (pkg.devDependencies?.[module.package]) {
118
+ delete pkg.devDependencies[module.package];
119
+ }
120
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
121
+
122
+ // 3. Retirer de la configuration
123
+ if (fs.existsSync(modulesConfigPath)) {
124
+ const modulesConfig = await fs.readJson(modulesConfigPath);
125
+ modulesConfig.modules = modulesConfig.modules.filter(
126
+ (m: string) => m !== module.package
127
+ );
128
+ await fs.writeJson(modulesConfigPath, modulesConfig, { spaces: 2 });
129
+ }
130
+
131
+ // 3.5 Supprimer les pages/APIs générées
132
+ await removeGeneratedFiles(module.name, module.package, targetDir);
133
+
134
+ // 4. Gérer les migrations
135
+ if (module.hasMigrations) {
136
+ console.log(chalk.yellow("\n🗄️ Gestion des migrations du module\n"));
137
+
138
+ const projectMigrationsDir = path.join(targetDir, "supabase", "migrations");
139
+
140
+ // Récupérer la liste des migrations depuis modules.json
141
+ const modulesConfigPath = path.join(targetDir, ".lastbrain", "modules.json");
142
+ let migrationFiles: string[] = [];
143
+
144
+ if (fs.existsSync(modulesConfigPath)) {
145
+ const modulesConfig = await fs.readJson(modulesConfigPath);
146
+ const moduleEntry = modulesConfig.modules.find((m: any) =>
147
+ (typeof m === 'string' ? m : m.package) === module.package
148
+ );
149
+
150
+ if (moduleEntry && typeof moduleEntry === 'object' && moduleEntry.migrations) {
151
+ migrationFiles = moduleEntry.migrations;
152
+ console.log(chalk.gray(`DEBUG: Migrations from config: ${migrationFiles.join(', ')}`));
153
+ }
154
+ }
155
+
156
+ // Fallback: essayer de lire depuis node_modules si disponible
157
+ if (migrationFiles.length === 0) {
158
+ const modulePackagePath = path.join(
159
+ targetDir,
160
+ "node_modules",
161
+ ...module.package.split("/")
162
+ );
163
+
164
+ const moduleMigrationsDir = path.join(
165
+ modulePackagePath,
166
+ module.migrationsPath || "supabase/migrations"
167
+ );
168
+
169
+ if (fs.existsSync(moduleMigrationsDir)) {
170
+ migrationFiles = fs
171
+ .readdirSync(moduleMigrationsDir)
172
+ .filter((f) => f.endsWith(".sql"));
173
+ console.log(chalk.gray(`DEBUG: Found ${migrationFiles.length} migration files from node_modules: ${migrationFiles.join(', ')}`));
174
+ } else {
175
+ console.log(chalk.gray(`DEBUG: No migrations found in config or node_modules`));
176
+ }
177
+ }
178
+
179
+ // Trouver le chemin du module pour les migrations down
180
+ const modulePackagePath = path.join(
181
+ targetDir,
182
+ "node_modules",
183
+ ...module.package.split("/")
184
+ );
185
+
186
+ const moduleMigrationsDownDir = path.join(
187
+ modulePackagePath,
188
+ module.migrationsDownPath || "supabase/migrations-down"
189
+ );
190
+
191
+ let hasDownMigrations = false;
192
+
193
+ // Vérifier si des fichiers down existent dans le dossier séparé
194
+ if (fs.existsSync(moduleMigrationsDownDir)) {
195
+ hasDownMigrations = fs
196
+ .readdirSync(moduleMigrationsDownDir)
197
+ .some((f) => f.endsWith(".sql"));
198
+ }
199
+
200
+ const choices = [
201
+ {
202
+ name: "🔄 Reset complet (supabase db reset) - Recommandé",
203
+ value: "reset",
204
+ },
205
+ ];
206
+
207
+ if (hasDownMigrations) {
208
+ choices.unshift({
209
+ name: "⬇️ Exécuter les migrations down du module",
210
+ value: "down",
211
+ });
212
+ }
213
+
214
+ choices.push({
215
+ name: "⏭️ Ignorer (garder les données en base)",
216
+ value: "skip",
217
+ });
218
+
219
+ const { migrationAction } = await inquirer.prompt([
220
+ {
221
+ type: "rawlist",
222
+ name: "migrationAction",
223
+ message: "Comment gérer les données du module en base ?",
224
+ choices,
225
+ },
226
+ ]);
227
+
228
+ if (migrationAction === "down" && hasDownMigrations) {
229
+ console.log(chalk.yellow("\n⬇️ Exécution des migrations down..."));
230
+
231
+ // Exécuter les fichiers .sql du dossier migrations-down dans l'ordre inverse
232
+ const downFiles = fs
233
+ .readdirSync(moduleMigrationsDownDir)
234
+ .filter((f) => f.endsWith(".sql"))
235
+ .sort()
236
+ .reverse();
237
+
238
+ for (const downFile of downFiles) {
239
+ const downFilePath = path.join(moduleMigrationsDownDir, downFile);
240
+
241
+ // Extraire le timestamp de la migration (14 premiers caractères)
242
+ const migrationVersion = downFile.split('_')[0]; // Prend la partie avant le premier underscore
243
+
244
+ try {
245
+ console.log(chalk.gray(` Exécution de ${downFile}...`));
246
+ execSync(`psql "postgresql://postgres:postgres@127.0.0.1:54322/postgres" -f "${downFilePath}"`, {
247
+ cwd: targetDir,
248
+ stdio: "inherit",
249
+ });
250
+
251
+ // Supprimer l'entrée de supabase_migrations.schema_migrations
252
+ try {
253
+ const deleteQuery = `DELETE FROM supabase_migrations.schema_migrations WHERE version = '${migrationVersion}';`;
254
+ const result = execSync(`psql "postgresql://postgres:postgres@127.0.0.1:54322/postgres" -c "${deleteQuery}"`, {
255
+ cwd: targetDir,
256
+ encoding: 'utf-8',
257
+ });
258
+ console.log(chalk.gray(` ✓ Supprimé ${migrationVersion} de schema_migrations (${result.trim()})`));
259
+ } catch (error: any) {
260
+ console.error(chalk.red(` ❌ Erreur lors de la suppression de ${migrationVersion}: ${error.message}`));
261
+ }
262
+
263
+ console.log(chalk.green(` ✓ ${downFile}`));
264
+ } catch (error) {
265
+ console.error(chalk.red(` ❌ Erreur avec ${downFile}`));
266
+ }
267
+ }
268
+ } else if (migrationAction === "reset") {
269
+ console.log(chalk.yellow("\n🔄 Reset de la base de données..."));
270
+
271
+ // D'abord supprimer les fichiers de migration du projet
272
+ console.log(
273
+ chalk.yellow("\n📋 Suppression des fichiers de migration...")
274
+ );
275
+ for (const file of migrationFiles) {
276
+ const filePath = path.join(projectMigrationsDir, file);
277
+ if (fs.existsSync(filePath)) {
278
+ await fs.remove(filePath);
279
+ console.log(chalk.gray(` ✓ Supprimé ${file}`));
280
+ }
281
+ }
282
+
283
+ try {
284
+ execSync("supabase db reset", { cwd: targetDir, stdio: "inherit" });
285
+ console.log(chalk.green("✓ Base de données réinitialisée"));
286
+ } catch (error) {
287
+ console.error(chalk.red("❌ Erreur lors du reset"));
288
+ }
289
+ } else {
290
+ console.log(
291
+ chalk.gray(
292
+ "\n⚠️ Données conservées en base. Nettoyez manuellement si nécessaire.\n"
293
+ )
294
+ );
295
+ }
296
+
297
+ // Supprimer les fichiers de migration du projet (si pas déjà fait)
298
+ if (migrationAction !== "reset") {
299
+ console.log(
300
+ chalk.yellow("\n📋 Suppression des fichiers de migration...")
301
+ );
302
+ for (const file of migrationFiles) {
303
+ const filePath = path.join(projectMigrationsDir, file);
304
+ if (fs.existsSync(filePath)) {
305
+ await fs.remove(filePath);
306
+ console.log(chalk.gray(` ✓ ${file}`));
307
+ }
308
+ }
309
+
310
+ // Supprimer les entrées de supabase_migrations.schema_migrations
311
+ if (migrationFiles.length > 0) {
312
+ console.log(chalk.yellow("\n🗄️ Nettoyage de schema_migrations..."));
313
+
314
+ // Extraire uniquement les timestamps (14 premiers caractères) de chaque fichier de migration
315
+ const versions = migrationFiles.map(f => {
316
+ const timestamp = f.split('_')[0]; // Prend la partie avant le premier underscore
317
+ return `'${timestamp}'`;
318
+ }).join(', ');
319
+ const deleteQuery = `DELETE FROM supabase_migrations.schema_migrations WHERE version IN (${versions});`;
320
+
321
+ try {
322
+ const result = execSync(`psql "postgresql://postgres:postgres@127.0.0.1:54322/postgres" -c "${deleteQuery}"`, {
323
+ cwd: targetDir,
324
+ encoding: 'utf-8',
325
+ });
326
+ console.log(chalk.gray(` ✓ Supprimé ${migrationFiles.length} entrées de schema_migrations`));
327
+ console.log(chalk.gray(` ${result.trim()}`));
328
+ } catch (error: any) {
329
+ console.error(chalk.red(` ❌ Erreur: ${error.message}`));
330
+ }
331
+ }
332
+ }
333
+ }
334
+
335
+ // 5. Marquer le module comme inactif dans modules.json
336
+ const modulesJsonPath = path.join(targetDir, ".lastbrain", "modules.json");
337
+ if (fs.existsSync(modulesJsonPath)) {
338
+ try {
339
+ const modulesConfig = JSON.parse(fs.readFileSync(modulesJsonPath, "utf-8"));
340
+ const moduleEntry = modulesConfig.modules.find((m: any) => m.package === module.package);
341
+ if (moduleEntry) {
342
+ moduleEntry.active = false;
343
+ moduleEntry.migrations = [];
344
+ }
345
+ fs.writeFileSync(modulesJsonPath, JSON.stringify(modulesConfig, null, 2));
346
+ console.log(chalk.gray("\n✓ Module marqué comme inactif dans modules.json"));
347
+ } catch (error) {
348
+ console.error(chalk.red("❌ Erreur lors de la mise à jour de modules.json"));
349
+ }
350
+ }
351
+
352
+ // 6. Nettoyer les dépendances
353
+ console.log(chalk.yellow("\n🧹 Nettoyage des dépendances..."));
354
+ try {
355
+ execSync("pnpm install", { cwd: targetDir, stdio: "inherit" });
356
+ } catch (error) {
357
+ console.error(chalk.red("❌ Erreur lors du nettoyage"));
358
+ process.exit(1);
359
+ }
360
+
361
+ console.log(
362
+ chalk.green(`\n✅ Module ${module.displayName} supprimé avec succès!\n`)
363
+ );
364
+ console.log(
365
+ chalk.gray("Le serveur de développement redémarrera automatiquement.\n")
366
+ );
367
+ }
@@ -0,0 +1,60 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import { getModuleConfigs } from "../modules/module-loader.js";
6
+
7
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
8
+ const packageRoot = path.join(scriptDir, "..", "..");
9
+ const projectRoot = path.join(packageRoot, "..", "..");
10
+ const webAppRoot = path.join(projectRoot, "apps/web");
11
+ const outputPath = path.join(webAppRoot, "README.md");
12
+
13
+ function resolveModuleReadme(moduleName: string) {
14
+ const fallbackPaths = [
15
+ path.join(
16
+ projectRoot,
17
+ "packages",
18
+ moduleName.replace("@lastbrain/", ""),
19
+ "README.md"
20
+ ),
21
+ path.join(
22
+ projectRoot,
23
+ "node_modules",
24
+ ...moduleName.split("/"),
25
+ "README.md"
26
+ ),
27
+ ];
28
+
29
+ for (const candidate of fallbackPaths) {
30
+ if (fs.existsSync(candidate)) {
31
+ return fs.readFileSync(candidate, "utf-8");
32
+ }
33
+ }
34
+
35
+ return null;
36
+ }
37
+
38
+ function buildGeneratedReadme() {
39
+ const baseReadmePath = path.join(projectRoot, "packages/app/README.md");
40
+ const baseReadme = fs.existsSync(baseReadmePath)
41
+ ? fs.readFileSync(baseReadmePath, "utf-8")
42
+ : "";
43
+
44
+ const modules = getModuleConfigs();
45
+ const moduleSections = modules
46
+ .map((module) => {
47
+ const readme =
48
+ resolveModuleReadme(module.moduleName) ||
49
+ "Aucun README disponible pour ce module.";
50
+ return `## Module ${module.moduleName}\n\n${readme}\n`;
51
+ })
52
+ .join("\n");
53
+
54
+ const content = `# LastBrain App Docs\n\n${baseReadme}\n\n---\n\n## Modules\n\n${moduleSections}\n\n## Commandes clés\n\n- pnpm build:modules\n- pnpm db:migrations:sync\n- pnpm db:init\n- pnpm dev\n`;
55
+
56
+ fs.writeFileSync(outputPath, content);
57
+ console.log(`📝 Generated aggregated README: ${outputPath}`);
58
+ }
59
+
60
+ buildGeneratedReadme();
package/src/styles.css ADDED
@@ -0,0 +1,3 @@
1
+ @import "tailwindcss";
2
+
3
+ /* Styles pour les composants @lastbrain/app */
@@ -0,0 +1,68 @@
1
+ // GENERATED BY LASTBRAIN TEMPLATE (Auth Guide)
2
+ import React from "react";
3
+ import { Card, Code, Divider, Button, Link, Chip } from "@lastbrain/ui";
4
+
5
+ export function AuthGuidePage() {
6
+ return (
7
+ <div className="max-w-5xl mx-auto px-6 py-10 space-y-10">
8
+ <header className="space-y-3">
9
+ <h1 className="text-3xl font-bold">Guide Authentification</h1>
10
+ <p className="text-slate-600 dark:text-slate-300 text-sm">
11
+ Pages publiques signin/signup/reset et endpoints API associés.
12
+ </p>
13
+ <div className="flex gap-2 flex-wrap">
14
+ <Chip color="primary">Pages</Chip>
15
+ <Chip color="secondary">API</Chip>
16
+ <Chip color="success">Sessions</Chip>
17
+ </div>
18
+ </header>
19
+ <Divider />
20
+ <Card className="p-6 space-y-3">
21
+ <h2 className="text-lg font-semibold">Pages disponibles</h2>
22
+ <ul className="list-disc pl-5 text-sm space-y-1 text-slate-600 dark:text-slate-400">
23
+ <li>
24
+ <code>/signin</code> : connexion
25
+ </li>
26
+ <li>
27
+ <code>/signup</code> : inscription
28
+ </li>
29
+ <li>
30
+ <code>/reset-password</code> (exemple)
31
+ </li>
32
+ </ul>
33
+ <Code className="text-xs block">{`import { SignInPage } from "@lastbrain/module-auth";`}</Code>
34
+ </Card>
35
+ <Card className="p-6 space-y-3">
36
+ <h2 className="text-lg font-semibold">API Route</h2>
37
+ <Code className="text-xs block">/api/auth/signin (POST)</Code>
38
+ <Code className="text-xs block">{`export { POST } from "@lastbrain/module-auth/api/public/signin";`}</Code>
39
+ </Card>
40
+ <Card className="p-6 space-y-4">
41
+ <h2 className="text-lg font-semibold">Personnalisation</h2>
42
+ <ol className="list-decimal pl-5 text-sm space-y-1 text-slate-600 dark:text-slate-400">
43
+ <li>Override wrapper route si besoin</li>
44
+ <li>
45
+ Modifier composant dans{" "}
46
+ <code>packages/module-auth/src/web/public</code>
47
+ </li>
48
+ <li>
49
+ Recompiler module (<Code className="text-xs inline">pnpm dev</Code>)
50
+ </li>
51
+ <li>Hot reload côté app</li>
52
+ </ol>
53
+ </Card>
54
+ <Card className="p-6 space-y-2">
55
+ <h2 className="text-lg font-semibold">Exemple d'intégration</h2>
56
+ <Code className="text-xs whitespace-pre-wrap">{`import { SignInPage } from "@lastbrain/module-auth";
57
+
58
+ export default function SignIn() {
59
+ return <SignInPage />
60
+ }`}</Code>
61
+ </Card>
62
+ <footer className="text-center text-xs text-slate-500 dark:text-slate-500">
63
+ Pour ajouter un autre provider OAuth: étendre module-auth ou créer
64
+ module séparé.
65
+ </footer>
66
+ </div>
67
+ );
68
+ }