gmoonc 0.0.3 → 0.0.5

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.
package/README.md CHANGED
@@ -16,10 +16,28 @@ Run the installer in your React project root:
16
16
  npx gmoonc
17
17
  ```
18
18
 
19
- Or with options:
19
+ Or skip confirmations (recommended):
20
20
 
21
21
  ```bash
22
- npx gmoonc --yes --base /app
22
+ npx gmoonc -y
23
+ ```
24
+
25
+ **Note for Windows/npm 11+:** If you encounter issues with `--yes`, use the short flag `-y` instead:
26
+
27
+ ```bash
28
+ npx gmoonc -y
29
+ ```
30
+
31
+ Or use the pass-through syntax:
32
+
33
+ ```bash
34
+ npx gmoonc -- --yes
35
+ ```
36
+
37
+ With custom base path:
38
+
39
+ ```bash
40
+ npx gmoonc -y --base /dashboard
23
41
  ```
24
42
 
25
43
  ## What it does
@@ -31,7 +49,7 @@ npx gmoonc --yes --base /app
31
49
 
32
50
  ## Options
33
51
 
34
- - `--yes` / `-y`: Skip confirmations and install automatically
52
+ - `-y`, `--yes`, `--auto`: Skip confirmations and install automatically (recommended: use `-y`)
35
53
  - `--base <path>`: Base path for dashboard routes (default: `/app`)
36
54
  - `--skip-router-patch`: Skip automatic router integration
37
55
  - `--dry-run`: Show what would be done without making changes
package/dist/index.cjs CHANGED
@@ -244,20 +244,29 @@ function patchEntryCss(entrypointPath, dryRun) {
244
244
  return { success: true, backupPath: null };
245
245
  }
246
246
  const content = readFile(entrypointPath);
247
- if (content.includes("./gmoonc/styles/gmoonc.css") || content.includes("gmoonc/styles/gmoonc.css")) {
248
- logSuccess("CSS import already exists");
247
+ const hasThemeCss = content.includes("./gmoonc/styles/theme.css") || content.includes("gmoonc/styles/theme.css");
248
+ const hasGmooncCss = content.includes("./gmoonc/styles/gmoonc.css") || content.includes("gmoonc/styles/gmoonc.css");
249
+ if (hasThemeCss && hasGmooncCss) {
250
+ logSuccess("CSS imports already exist");
249
251
  return { success: true, backupPath: null };
250
252
  }
251
253
  const importRegex = /^import\s+.*$/gm;
252
254
  const imports = content.match(importRegex) || [];
253
255
  let newContent = content;
256
+ const cssImports = [];
257
+ if (!hasThemeCss) {
258
+ cssImports.push('import "./gmoonc/styles/theme.css";');
259
+ }
260
+ if (!hasGmooncCss) {
261
+ cssImports.push('import "./gmoonc/styles/gmoonc.css";');
262
+ }
254
263
  if (imports.length > 0) {
255
264
  const lastImport = imports[imports.length - 1];
256
265
  const lastImportIndex = content.lastIndexOf(lastImport);
257
266
  const insertIndex = lastImportIndex + lastImport.length;
258
- newContent = content.slice(0, insertIndex) + '\nimport "./gmoonc/styles/gmoonc.css";' + content.slice(insertIndex);
267
+ newContent = content.slice(0, insertIndex) + "\n" + cssImports.join("\n") + content.slice(insertIndex);
259
268
  } else {
260
- newContent = 'import "./gmoonc/styles/gmoonc.css";\n' + content;
269
+ newContent = cssImports.join("\n") + "\n" + content;
261
270
  }
262
271
  const backupPath = writeFileSafe(entrypointPath, newContent);
263
272
  if (backupPath) {
@@ -281,23 +290,58 @@ function extractRouteComponents(appContent) {
281
290
  const lines = appContent.split("\n");
282
291
  if (indexComponent) {
283
292
  for (const line of lines) {
284
- if (line.includes("import") && line.includes(indexComponent)) {
285
- indexImport = line.trim();
286
- break;
293
+ const trimmed = line.trim();
294
+ if (trimmed.startsWith("import")) {
295
+ const defaultImportMatch = trimmed.match(/import\s+(\w+)\s+from\s+["']([^"']+)["']/);
296
+ if (defaultImportMatch && defaultImportMatch[1] === indexComponent) {
297
+ indexImport = trimmed;
298
+ break;
299
+ }
300
+ const namedImportMatch = trimmed.match(/import\s+\{[^}]*\b(\w+)\b[^}]*\}\s+from\s+["']([^"']+)["']/);
301
+ if (namedImportMatch && namedImportMatch[1] === indexComponent) {
302
+ indexImport = trimmed;
303
+ break;
304
+ }
287
305
  }
288
306
  }
289
307
  }
290
308
  if (notFoundComponent) {
291
309
  for (const line of lines) {
292
- if (line.includes("import") && line.includes(notFoundComponent)) {
293
- notFoundImport = line.trim();
294
- break;
310
+ const trimmed = line.trim();
311
+ if (trimmed.startsWith("import")) {
312
+ const defaultImportMatch = trimmed.match(/import\s+(\w+)\s+from\s+["']([^"']+)["']/);
313
+ if (defaultImportMatch && defaultImportMatch[1] === notFoundComponent) {
314
+ notFoundImport = trimmed;
315
+ break;
316
+ }
317
+ const namedImportMatch = trimmed.match(/import\s+\{[^}]*\b(\w+)\b[^}]*\}\s+from\s+["']([^"']+)["']/);
318
+ if (namedImportMatch && namedImportMatch[1] === notFoundComponent) {
319
+ notFoundImport = trimmed;
320
+ break;
321
+ }
295
322
  }
296
323
  }
297
324
  }
298
325
  return { indexImport, notFoundImport, indexComponent, notFoundComponent };
299
326
  }
300
- function generateAppRoutes(consumerDir, basePath, indexImport, notFoundImport, indexComponent, notFoundComponent, dryRun) {
327
+ function convertImportToRelative(importLine, appRoutesPath, appPath) {
328
+ const pathMatch = importLine.match(/from\s+["']([^"']+)["']/);
329
+ if (!pathMatch) {
330
+ return importLine;
331
+ }
332
+ const originalPath = pathMatch[1];
333
+ if (!originalPath.startsWith("./") && !originalPath.startsWith("../")) {
334
+ return importLine;
335
+ }
336
+ const appDir = appPath.substring(0, appPath.lastIndexOf("/"));
337
+ const resolvedPath = (0, import_path5.join)(appDir, originalPath);
338
+ const appRoutesDir = appRoutesPath.substring(0, appRoutesPath.lastIndexOf("/"));
339
+ const relativePath = (0, import_path5.relative)(appRoutesDir, resolvedPath);
340
+ const normalizedPath = relativePath.replace(/\\/g, "/");
341
+ const finalPath = normalizedPath.startsWith(".") ? normalizedPath : "./" + normalizedPath;
342
+ return importLine.replace(pathMatch[1], finalPath);
343
+ }
344
+ function generateAppRoutes(consumerDir, basePath, indexImport, notFoundImport, indexComponent, notFoundComponent, appPath, dryRun) {
301
345
  const appRoutesPath = (0, import_path5.join)(consumerDir, "src/gmoonc/router/AppRoutes.tsx");
302
346
  if (dryRun) {
303
347
  logInfo(`[DRY RUN] Would generate ${appRoutesPath}`);
@@ -308,24 +352,26 @@ function generateAppRoutes(consumerDir, basePath, indexImport, notFoundImport, i
308
352
  if (indexComponent) {
309
353
  routes.push(` { path: "/", element: <${indexComponent} /> }`);
310
354
  }
311
- routes.push(` ...createGmooncRoutes({ basePath: "${basePath}" })`);
355
+ routes.push(` ...createGmooncRoutes({ basePath })`);
312
356
  if (notFoundComponent) {
313
357
  routes.push(` { path: "*", element: <${notFoundComponent} /> }`);
314
358
  }
315
359
  const imports = [
316
- "import { useRoutes } from 'react-router-dom';",
360
+ "import { useRoutes, type RouteObject } from 'react-router-dom';",
317
361
  "import { createGmooncRoutes } from './createGmooncRoutes';"
318
362
  ];
319
363
  if (indexImport) {
320
- imports.push(indexImport);
364
+ const convertedImport = convertImportToRelative(indexImport, appRoutesPath, appPath);
365
+ imports.push(convertedImport);
321
366
  }
322
367
  if (notFoundImport) {
323
- imports.push(notFoundImport);
368
+ const convertedImport = convertImportToRelative(notFoundImport, appRoutesPath, appPath);
369
+ imports.push(convertedImport);
324
370
  }
325
371
  const content = `${imports.join("\n")}
326
372
 
327
373
  export function GMooncAppRoutes({ basePath = "${basePath}" }: { basePath?: string }) {
328
- const allRoutes = [
374
+ const allRoutes: RouteObject[] = [
329
375
  ${routes.join(",\n")}
330
376
  ];
331
377
 
@@ -380,10 +426,54 @@ function patchBrowserRouter(consumerDir, basePath, dryRun) {
380
426
  logInfo(`[DRY RUN] Would patch ${appPath}`);
381
427
  return { success: true, backupPath: null };
382
428
  }
383
- generateAppRoutes(consumerDir, basePath, indexImport, notFoundImport, indexComponent, notFoundComponent, false);
429
+ generateAppRoutes(consumerDir, basePath, indexImport, notFoundImport, indexComponent, notFoundComponent, appPath, false);
384
430
  const backupPath = writeFileSafe(appPath, "");
385
- const importLine = `import { GMooncAppRoutes } from "./gmoonc/router/AppRoutes";`;
386
431
  const lines = appContent.split("\n");
432
+ let hasBrowserRouterImport = false;
433
+ let reactRouterImportIndex = -1;
434
+ for (let i = 0; i < lines.length; i++) {
435
+ const line = lines[i].trim();
436
+ if (line.includes("from") && line.includes("react-router-dom")) {
437
+ reactRouterImportIndex = i;
438
+ if (line.includes("BrowserRouter")) {
439
+ hasBrowserRouterImport = true;
440
+ }
441
+ }
442
+ }
443
+ if (!hasBrowserRouterImport) {
444
+ if (reactRouterImportIndex >= 0) {
445
+ const existingLine = lines[reactRouterImportIndex];
446
+ if (existingLine.includes("{") && existingLine.includes("}")) {
447
+ const updatedLine = existingLine.replace(/\{([^}]+)\}/, (match, imports) => {
448
+ const importList = imports.split(",").map((s) => s.trim());
449
+ if (!importList.includes("BrowserRouter")) {
450
+ importList.push("BrowserRouter");
451
+ }
452
+ return `{ ${importList.join(", ")} }`;
453
+ });
454
+ lines[reactRouterImportIndex] = updatedLine;
455
+ } else {
456
+ lines.splice(reactRouterImportIndex + 1, 0, `import { BrowserRouter } from "react-router-dom";`);
457
+ }
458
+ } else {
459
+ const importLine2 = `import { BrowserRouter } from "react-router-dom";`;
460
+ let lastImportIndex2 = -1;
461
+ for (let i = 0; i < lines.length; i++) {
462
+ const line = lines[i].trim();
463
+ if (line.startsWith("import ") || line.startsWith("import{") || line.startsWith("import ")) {
464
+ lastImportIndex2 = i;
465
+ } else if (line && lastImportIndex2 >= 0 && !line.startsWith("//") && !line.startsWith("/*")) {
466
+ break;
467
+ }
468
+ }
469
+ if (lastImportIndex2 >= 0) {
470
+ lines.splice(lastImportIndex2 + 1, 0, importLine2);
471
+ } else {
472
+ lines.unshift(importLine2);
473
+ }
474
+ }
475
+ }
476
+ const importLine = `import { GMooncAppRoutes } from "./gmoonc/router/AppRoutes";`;
387
477
  let lastImportIndex = -1;
388
478
  for (let i = 0; i < lines.length; i++) {
389
479
  const line = lines[i].trim();
@@ -448,12 +538,15 @@ function patchBrowserRouter(consumerDir, basePath, dryRun) {
448
538
 
449
539
  // src/cli/index.ts
450
540
  var program = new import_commander.Command();
451
- program.name("gmoonc").description("Goalmoon Ctrl (gmoonc): Install complete dashboard into your React project").version("0.0.3").option("-y, --yes", "Skip confirmations and install automatically").option("--base <path>", "Base path for dashboard routes", "/app").option("--skip-router-patch", "Skip automatic router integration (only copy files and inject CSS)").option("--dry-run", "Show what would be done without making changes").action(async (options) => {
541
+ program.name("gmoonc").description("Goalmoon Ctrl (gmoonc): Install complete dashboard into your React project").version("0.0.5").option("-y, --yes, --auto", "Skip confirmations and install automatically").option("--base <path>", "Base path for dashboard routes", "/app").option("--skip-router-patch", "Skip automatic router integration (only copy files and inject CSS)").option("--dry-run", "Show what would be done without making changes").action(async (options) => {
452
542
  try {
543
+ logInfo("\u{1F680} Starting gmoonc installer...");
544
+ logInfo("\u{1F4E6} Installing complete dashboard into your React project\n");
453
545
  const projectDir = (0, import_process.cwd)();
454
546
  const basePath = options.base || "/app";
455
547
  const dryRun = options.dryRun || false;
456
548
  const skipRouterPatch = options.skipRouterPatch || false;
549
+ const autoInstall = options.yes || options.auto || false;
457
550
  const safeBasePath = basePath === "/" ? "/app" : basePath;
458
551
  logInfo("\u{1F50D} Detecting React project...");
459
552
  const project = detectProject(projectDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gmoonc",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Goalmoon Ctrl (gmoonc): Complete dashboard installer for React projects",
5
5
  "license": "MIT",
6
6
  "homepage": "https://gmoonc.com",
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "type": "module",
16
16
  "bin": {
17
- "gmoonc": "./dist/cli.cjs"
17
+ "gmoonc": "./dist/index.cjs"
18
18
  },
19
19
  "files": [
20
20
  "dist",
@@ -25,7 +25,7 @@
25
25
  ],
26
26
  "scripts": {
27
27
  "sync:templates": "node scripts/sync-templates.mjs",
28
- "build": "tsup src/cli/index.ts --format cjs --no-splitting --out-dir dist && node -e \"const fs=require('fs');const f='dist/cli.js';const cjs='dist/cli.cjs';if(fs.existsSync(f)){fs.renameSync(f,cjs);fs.writeFileSync(cjs,'#!/usr/bin/env node\\n'+fs.readFileSync(cjs))}\"",
28
+ "build": "tsup src/cli/index.ts --format cjs --no-splitting --out-dir dist && node -e \"const fs=require('fs');const path=require('path');const distDir='dist';const files=fs.readdirSync(distDir);const indexFile=files.find(f=>f.startsWith('index')&&f.endsWith('.js'));if(indexFile){const oldPath=path.join(distDir,indexFile);const newPath=path.join(distDir,'index.cjs');fs.renameSync(oldPath,newPath);const content=fs.readFileSync(newPath,'utf8');if(!content.startsWith('#!/usr/bin/env node')){fs.writeFileSync(newPath,'#!/usr/bin/env node\\n'+content)}}\"",
29
29
  "clean": "rimraf dist",
30
30
  "prepublishOnly": "npm run clean && npm run build",
31
31
  "postinstall": "node scripts/block-global-install.cjs"
@@ -1,4 +1,4 @@
1
- import { type MenuItem as CoreMenuItem } from '../../core/core';
1
+ import { type MenuItem as CoreMenuItem } from '../../core/types';
2
2
 
3
3
  /**
4
4
  * Extended MenuItem interface with submenu support
@@ -1,4 +1,4 @@
1
- import { defineConfig } from '../core/core';
1
+ import { defineConfig } from '../core/types';
2
2
  import { createDefaultMenu } from '../app/menu/defaultMenu';
3
3
 
4
4
  /**
@@ -1,6 +1,6 @@
1
1
  import React, { useCallback } from 'react';
2
2
  import { Outlet, useLocation, useNavigate, Link } from 'react-router-dom';
3
- import { GmooncShell } from '../ui/ui';
3
+ import { GmooncShell } from '../ui/shell';
4
4
  import { defaultConfig } from '../config/defaultConfig';
5
5
  import { GMooncSessionProvider, useGMooncSession } from '../session/GMooncSessionContext';
6
6
  import { GMooncUserProfile } from '../components/GMooncUserProfile';
@@ -24,33 +24,36 @@ function GMooncAppLayoutInner() {
24
24
  }, [navigate]);
25
25
 
26
26
  return (
27
- <GmooncShell
28
- config={defaultConfig}
29
- roles={roles}
30
- activePath={location.pathname}
31
- onNavigate={(path) => navigate(path)}
32
- headerRight={
33
- <GMooncUserProfile
34
- onAccount={handleAccount}
35
- onAbout={handleAbout}
36
- onLogoutRequest={handleLogout}
37
- />
38
- }
39
- renderLink={({ path, label, isActive, onClick }) => (
40
- <Link
41
- to={path}
42
- onClick={onClick}
43
- style={{
44
- textDecoration: isActive ? 'underline' : 'none',
45
- color: 'inherit'
46
- }}
47
- >
48
- {label}
49
- </Link>
50
- )}
51
- >
52
- <Outlet />
53
- </GmooncShell>
27
+ <div className="gmoonc-root">
28
+ <GmooncShell
29
+ config={defaultConfig}
30
+ roles={roles}
31
+ activePath={location.pathname}
32
+ onNavigate={(path) => navigate(path)}
33
+ headerRight={
34
+ <GMooncUserProfile
35
+ onAccount={handleAccount}
36
+ onAbout={handleAbout}
37
+ onLogoutRequest={handleLogout}
38
+ />
39
+ }
40
+ renderLink={({ path, label, isActive, onClick, className, children }) => (
41
+ <Link
42
+ to={path}
43
+ onClick={onClick}
44
+ className={className}
45
+ style={{
46
+ textDecoration: isActive ? 'underline' : 'none',
47
+ color: 'inherit'
48
+ }}
49
+ >
50
+ {children || label}
51
+ </Link>
52
+ )}
53
+ >
54
+ <Outlet />
55
+ </GmooncShell>
56
+ </div>
54
57
  );
55
58
  }
56
59
 
@@ -0,0 +1,101 @@
1
+ /*
2
+ * Goalmoon Ctrl (gmoonc) Theme Variables
3
+ *
4
+ * This file contains CSS custom properties (variables) that define the visual theme
5
+ * of the gmoonc dashboard. You can customize these values to match your brand.
6
+ *
7
+ * All variables are scoped under .gmoonc-root to avoid conflicts with your app.
8
+ */
9
+
10
+ .gmoonc-root {
11
+ /* Colors - Background */
12
+ --gmoonc-color-bg: #293047;
13
+ --gmoonc-color-bg-light: #eaf0f5;
14
+ --gmoonc-color-bg-gradient-start: #eaf0f5;
15
+ --gmoonc-color-bg-gradient-end: #dbe2ea;
16
+
17
+ /* Colors - Surface */
18
+ --gmoonc-color-surface: #374161;
19
+ --gmoonc-color-surface-2: #3F4A6E;
20
+ --gmoonc-color-surface-3: #6374AD;
21
+ --gmoonc-color-surface-light: #f8f9fa;
22
+ --gmoonc-color-surface-white: #ffffff;
23
+
24
+ /* Colors - Text */
25
+ --gmoonc-color-text: #eaf0f5;
26
+ --gmoonc-color-text-primary: #374161;
27
+ --gmoonc-color-text-secondary: #6c757d;
28
+ --gmoonc-color-text-muted: #dbe2ea;
29
+ --gmoonc-color-text-white: #ffffff;
30
+
31
+ /* Colors - Primary */
32
+ --gmoonc-color-primary: #879FED;
33
+ --gmoonc-color-primary-2: #6374AD;
34
+ --gmoonc-color-primary-dark: #374161;
35
+
36
+ /* Colors - Accent */
37
+ --gmoonc-color-accent: #71b399;
38
+
39
+ /* Colors - Borders */
40
+ --gmoonc-color-border: #dbe2ea;
41
+ --gmoonc-color-border-light: #e9ecef;
42
+ --gmoonc-color-border-dark: #1d2436;
43
+
44
+ /* Colors - States */
45
+ --gmoonc-color-error: #dc2626;
46
+ --gmoonc-color-error-bg: #fef2f2;
47
+ --gmoonc-color-error-border: #fecaca;
48
+ --gmoonc-color-success: #166534;
49
+ --gmoonc-color-success-bg: #f0fdf4;
50
+ --gmoonc-color-success-border: #bbf7d0;
51
+ --gmoonc-color-disabled: #6c757d;
52
+ --gmoonc-color-disabled-bg: #f8f9fa;
53
+
54
+ /* Spacing */
55
+ --gmoonc-gap: 12px;
56
+ --gmoonc-gap-sm: 8px;
57
+ --gmoonc-gap-md: 16px;
58
+ --gmoonc-gap-lg: 20px;
59
+ --gmoonc-gap-xl: 30px;
60
+
61
+ /* Border Radius */
62
+ --gmoonc-radius: 12px;
63
+ --gmoonc-radius-sm: 8px;
64
+ --gmoonc-radius-md: 10px;
65
+ --gmoonc-radius-lg: 16px;
66
+
67
+ /* Typography */
68
+ --gmoonc-font-family: "Montserrat", system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
69
+ --gmoonc-font-weight-title: 700;
70
+ --gmoonc-font-weight-heading: 600;
71
+ --gmoonc-font-weight-body: 400;
72
+ --gmoonc-font-weight-medium: 500;
73
+
74
+ /* Font Sizes */
75
+ --gmoonc-font-size-xs: 12px;
76
+ --gmoonc-font-size-sm: 14px;
77
+ --gmoonc-font-size-base: 16px;
78
+ --gmoonc-font-size-lg: 18px;
79
+ --gmoonc-font-size-xl: 24px;
80
+ --gmoonc-font-size-2xl: 28px;
81
+ --gmoonc-font-size-3xl: 32px;
82
+
83
+ /* Shadows */
84
+ --gmoonc-shadow-sm: 0 4px 6px rgba(55, 65, 97, 0.1);
85
+ --gmoonc-shadow-md: 0 4px 20px rgba(55, 65, 97, 0.1);
86
+ --gmoonc-shadow-lg: 0 8px 32px rgba(55, 65, 97, 0.15);
87
+ --gmoonc-shadow-header: 0 4px 20px rgba(55, 65, 97, 0.3);
88
+ --gmoonc-shadow-menu-header: 0 8px 16px rgba(0, 0, 0, 0.22);
89
+
90
+ /* Transitions */
91
+ --gmoonc-transition: 0.3s ease;
92
+ --gmoonc-transition-fast: 0.2s ease;
93
+
94
+ /* Z-index */
95
+ --gmoonc-z-header: 1000;
96
+ --gmoonc-z-profile: 1001;
97
+ --gmoonc-z-dropdown: 1002;
98
+ --gmoonc-z-hamburger: 1002;
99
+ --gmoonc-z-backdrop: 1050;
100
+ --gmoonc-z-menu: 1100;
101
+ }
@@ -1,6 +1,6 @@
1
1
  import { useState, useCallback, useMemo, useEffect } from 'react';
2
2
  import { createPortal } from 'react-dom';
3
- import type { MenuItem as CoreMenuItem } from '@gmoonc/core';
3
+ import type { MenuItem as CoreMenuItem } from '../core/types';
4
4
  import './styles.css';
5
5
 
6
6
  /**
@@ -36,6 +36,8 @@ export interface GmooncMenuProps {
36
36
  label: string;
37
37
  isActive: boolean;
38
38
  onClick?: () => void;
39
+ className?: string;
40
+ children?: React.ReactNode;
39
41
  }) => React.ReactNode;
40
42
  /** Whether menu is open (for mobile/tablet) */
41
43
  isOpen?: boolean;
@@ -387,30 +389,42 @@ export function GmooncMenu({
387
389
  };
388
390
 
389
391
  // Render main item link/button
390
- const mainItemContent = item.path && renderLink ? (
392
+ // IMPORTANT: If item has submenu, always use button (never renderLink) to avoid navigation to non-existent routes
393
+ const mainItemContent = hasSubmenu ? (
394
+ // Item with submenu: always button (toggle only, no navigation)
395
+ <button
396
+ type="button"
397
+ className={`gmoonc-menu-link has-submenu ${isSubmenuOpen ? 'submenu-open' : ''} ${isItemActive ? 'active' : ''}`}
398
+ onClick={handleItemClick}
399
+ >
400
+ {item.icon && <span className="gmoonc-menu-icon">{item.icon}</span>}
401
+ <span className="gmoonc-menu-label">{item.label}</span>
402
+ <span className="gmoonc-submenu-arrow">
403
+ {isSubmenuOpen
404
+ ? (item.collapseIcon || null)
405
+ : (item.expandIcon || null)
406
+ }
407
+ </span>
408
+ </button>
409
+ ) : item.path && renderLink ? (
410
+ // Item without submenu and with path: use renderLink
391
411
  renderLink({
392
412
  path: item.path,
393
413
  label: item.label,
394
414
  isActive: isItemActive,
395
- onClick: handleItemClick
415
+ onClick: handleItemClick,
416
+ className: `gmoonc-menu-link ${isItemActive ? 'active' : ''}`
396
417
  })
397
418
  ) : (
419
+ // Item without submenu and without renderLink: use button
398
420
  <button
399
421
  type="button"
400
- className={`gmoonc-menu-link ${hasSubmenu ? 'has-submenu' : ''} ${isSubmenuOpen ? 'submenu-open' : ''} ${isItemActive ? 'active' : ''}`}
422
+ className={`gmoonc-menu-link ${isItemActive ? 'active' : ''}`}
401
423
  onClick={handleItemClick}
402
- disabled={!item.path && !hasSubmenu && !onNavigate}
424
+ disabled={!item.path && !onNavigate}
403
425
  >
404
426
  {item.icon && <span className="gmoonc-menu-icon">{item.icon}</span>}
405
427
  <span className="gmoonc-menu-label">{item.label}</span>
406
- {hasSubmenu && (
407
- <span className="gmoonc-submenu-arrow">
408
- {isSubmenuOpen
409
- ? (item.collapseIcon || null)
410
- : (item.expandIcon || null)
411
- }
412
- </span>
413
- )}
414
428
  </button>
415
429
  );
416
430
 
@@ -439,7 +453,8 @@ export function GmooncMenu({
439
453
  path: subItem.path,
440
454
  label: subItem.label,
441
455
  isActive: isSubActive,
442
- onClick: handleSubItemClick
456
+ onClick: handleSubItemClick,
457
+ className: `gmoonc-submenu-link ${isSubActive ? 'active' : ''}`
443
458
  }) : (
444
459
  <button
445
460
  type="button"
@@ -1,5 +1,5 @@
1
1
  import { useState, useEffect, useCallback } from 'react';
2
- import type { GmooncConfig } from '@gmoonc/core';
2
+ import type { GmooncConfig } from '../core/types';
3
3
  import { GmooncHeader } from './header';
4
4
  import { GmooncSidebar } from './sidebar';
5
5
  import { GmooncMenu, type GmooncMenuItem } from './menu';
@@ -1,20 +1,21 @@
1
1
  import React from 'react';
2
2
  import { RouteObject } from 'react-router-dom';
3
- import { GmooncAppLayout } from '../../app/GMooncAppLayout';
4
- import { GMooncLoginPage } from '../../pages/auth/GMooncLoginPage';
5
- import { GMooncRegisterPage } from '../../pages/auth/GMooncRegisterPage';
6
- import { GMooncForgotPasswordPage } from '../../pages/auth/GMooncForgotPasswordPage';
7
- import { GMooncResetPasswordPage } from '../../pages/auth/GMooncResetPasswordPage';
8
- import { GMooncLogoutPage } from '../../pages/auth/GMooncLogoutPage';
9
- import { GMooncAppHomePage } from '../../pages/app/GMooncAppHomePage';
10
- import { GMooncPermissionsPage } from '../../pages/app/admin/GMooncPermissionsPage';
11
- import { GMooncAdminUsersPage } from '../../pages/app/admin/GMooncAdminUsersPage';
12
- import { GMooncAdminAuthorizationsPage } from '../../pages/app/admin/GMooncAdminAuthorizationsPage';
13
- import { GMooncAdminNotificationsPage } from '../../pages/app/admin/GMooncAdminNotificationsPage';
14
- import { GMooncTechnicalMessagesPage } from '../../pages/app/technical/GMooncTechnicalMessagesPage';
15
- import { GMooncCustomerMessagesPage } from '../../pages/app/customer/GMooncCustomerMessagesPage';
16
- import { GMooncOfficeAccountPage } from '../../pages/app/office/GMooncOfficeAccountPage';
17
- import { GMooncOfficeAboutPage } from '../../pages/app/office/GMooncOfficeAboutPage';
3
+ import { GmooncAppLayout } from '../layout/GMooncAppLayout';
4
+ import { GMooncLoginPage } from '../pages/auth/GMooncLoginPage';
5
+ import { GMooncRegisterPage } from '../pages/auth/GMooncRegisterPage';
6
+ import { GMooncForgotPasswordPage } from '../pages/auth/GMooncForgotPasswordPage';
7
+ import { GMooncResetPasswordPage } from '../pages/auth/GMooncResetPasswordPage';
8
+ import { GMooncLogoutPage } from '../pages/auth/GMooncLogoutPage';
9
+ import { GMooncAppHomePage } from '../pages/app/GMooncAppHomePage';
10
+ import { GMooncPermissionsPage } from '../pages/app/admin/GMooncPermissionsPage';
11
+ import { GMooncAdminUsersPage } from '../pages/app/admin/GMooncAdminUsersPage';
12
+ import { GMooncAdminAuthorizationsPage } from '../pages/app/admin/GMooncAdminAuthorizationsPage';
13
+ import { GMooncAdminNotificationsPage } from '../pages/app/admin/GMooncAdminNotificationsPage';
14
+ import { GMooncTechnicalMessagesPage } from '../pages/app/technical/GMooncTechnicalMessagesPage';
15
+ import { GMooncCustomerMessagesPage } from '../pages/app/customer/GMooncCustomerMessagesPage';
16
+ import { GMooncOfficeAccountPage } from '../pages/app/office/GMooncOfficeAccountPage';
17
+ import { GMooncOfficeAboutPage } from '../pages/app/office/GMooncOfficeAboutPage';
18
+ import { GMooncSessionProvider } from '../session/GMooncSessionContext';
18
19
 
19
20
  /**
20
21
  * Creates route objects for the gmoonc dashboard.
@@ -37,7 +38,11 @@ export function createGmooncRoutes(options?: { basePath?: string }): RouteObject
37
38
  // Auth routes (outside basePath, but still avoid "/" to be safe)
38
39
  {
39
40
  path: '/login',
40
- element: <GMooncLoginPage />
41
+ element: (
42
+ <GMooncSessionProvider>
43
+ <GMooncLoginPage />
44
+ </GMooncSessionProvider>
45
+ )
41
46
  },
42
47
  {
43
48
  path: '/register',
@@ -53,7 +58,11 @@ export function createGmooncRoutes(options?: { basePath?: string }): RouteObject
53
58
  },
54
59
  {
55
60
  path: '/logout',
56
- element: <GMooncLogoutPage />
61
+ element: (
62
+ <GMooncSessionProvider>
63
+ <GMooncLogoutPage />
64
+ </GMooncSessionProvider>
65
+ )
57
66
  },
58
67
  // Dashboard routes (always nested under basePath, never "/")
59
68
  {