gmoonc 0.0.9 → 0.0.11

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
@@ -48,6 +48,11 @@ The dashboard code is in `src/gmoonc/` and is independent. You can remove `gmoon
48
48
 
49
49
  ## Changelog
50
50
 
51
+ ### 0.0.10
52
+ - Fix: ensure BrowserRouter import when patched into App.tsx (definitive fix)
53
+ - Fix: prevent re-installation if src/gmoonc or marker file exists
54
+ - Fix: create marker file (.gmoonc-installed.json) after successful installation
55
+
51
56
  ### 0.0.9
52
57
  - Fix: ensure BrowserRouter import when patched into App.tsx
53
58
 
package/dist/index.cjs CHANGED
@@ -5,6 +5,7 @@
5
5
  var import_commander = require("commander");
6
6
  var import_process = require("process");
7
7
  var import_path6 = require("path");
8
+ var import_fs9 = require("fs");
8
9
 
9
10
  // src/cli/lib/detect.ts
10
11
  var import_fs = require("fs");
@@ -504,6 +505,78 @@ ${routeElements.join(",\n")}
504
505
  logSuccess("Generated src/gmoonc/router/AppRoutes.tsx");
505
506
  return { success: true };
506
507
  }
508
+ function ensureBrowserRouterImport(content) {
509
+ if (!content.includes("<BrowserRouter")) {
510
+ return content;
511
+ }
512
+ const lines = content.split("\n");
513
+ let hasBrowserRouterImport = false;
514
+ let reactRouterImportIndex = -1;
515
+ let reactRouterImportLine = null;
516
+ let quoteStyle = '"';
517
+ for (let i = 0; i < lines.length; i++) {
518
+ const line = lines[i];
519
+ const trimmed = line.trim();
520
+ if (trimmed.startsWith("import ") && (trimmed.includes("'") || trimmed.includes('"'))) {
521
+ if (trimmed.includes("'")) quoteStyle = "'";
522
+ else if (trimmed.includes('"')) quoteStyle = '"';
523
+ }
524
+ if (trimmed.includes("from") && trimmed.includes("react-router-dom")) {
525
+ reactRouterImportIndex = i;
526
+ reactRouterImportLine = line;
527
+ const browserRouterPattern = /\bBrowserRouter\b/;
528
+ if (browserRouterPattern.test(trimmed)) {
529
+ hasBrowserRouterImport = true;
530
+ }
531
+ }
532
+ }
533
+ if (hasBrowserRouterImport) {
534
+ return content;
535
+ }
536
+ if (reactRouterImportIndex >= 0 && reactRouterImportLine) {
537
+ const existingLine = reactRouterImportLine;
538
+ const indent = existingLine.match(/^(\s*)/)?.[1] || "";
539
+ const namedMatch = existingLine.match(/^(\s*)import\s+\{([^}]+)\}\s+from\s+(["'])react-router-dom\2/);
540
+ if (namedMatch) {
541
+ const importListStr = namedMatch[2];
542
+ const quote = namedMatch[3];
543
+ const importItems = importListStr.split(",").map((s) => s.trim()).filter(Boolean);
544
+ const hasBrowserRouter = importItems.some(
545
+ (item) => item === "BrowserRouter" || item === "type BrowserRouter" || item.includes("BrowserRouter")
546
+ );
547
+ if (!hasBrowserRouter) {
548
+ const typeImports = importItems.filter((item) => item.startsWith("type "));
549
+ const regularImports = importItems.filter((item) => !item.startsWith("type "));
550
+ regularImports.push("BrowserRouter");
551
+ const sortedImports = [...regularImports, ...typeImports];
552
+ const separator = importListStr.includes(",\n") ? ",\n" : ", ";
553
+ const formatted = sortedImports.join(separator);
554
+ const updatedLine = `${indent}import { ${formatted} } from ${quote}react-router-dom${quote};`;
555
+ lines[reactRouterImportIndex] = updatedLine;
556
+ }
557
+ } else {
558
+ const newImportLine = `${indent}import { BrowserRouter } from ${quoteStyle}react-router-dom${quoteStyle};`;
559
+ lines.splice(reactRouterImportIndex + 1, 0, newImportLine);
560
+ }
561
+ } else {
562
+ const importLine = `import { BrowserRouter } from ${quoteStyle}react-router-dom${quoteStyle};`;
563
+ let lastImportIndex = -1;
564
+ for (let i = 0; i < lines.length; i++) {
565
+ const line = lines[i].trim();
566
+ if (line.startsWith("import ") || line.startsWith("import{") || line.startsWith("import ")) {
567
+ lastImportIndex = i;
568
+ } else if (line && lastImportIndex >= 0 && !line.startsWith("//") && !line.startsWith("/*")) {
569
+ break;
570
+ }
571
+ }
572
+ if (lastImportIndex >= 0) {
573
+ lines.splice(lastImportIndex + 1, 0, importLine);
574
+ } else {
575
+ lines.unshift(importLine);
576
+ }
577
+ }
578
+ return lines.join("\n");
579
+ }
507
580
  function patchBrowserRouter(consumerDir, basePath, dryRun) {
508
581
  const appCandidates = ["src/App.tsx", "src/App.jsx", "src/App.ts", "src/App.js"];
509
582
  let appPath = null;
@@ -562,68 +635,6 @@ function patchBrowserRouter(consumerDir, basePath, dryRun) {
562
635
  generateAppRoutes(consumerDir, basePath, routes, appPath, false);
563
636
  const backupPath = writeFileSafe(appPath, "");
564
637
  const lines = appContent.split("\n");
565
- let hasBrowserRouterImport = false;
566
- let reactRouterImportIndex = -1;
567
- let reactRouterImportLine = null;
568
- let quoteStyle = '"';
569
- for (let i = 0; i < lines.length; i++) {
570
- const line = lines[i];
571
- const trimmed = line.trim();
572
- if (trimmed.startsWith("import ") && (trimmed.includes("'") || trimmed.includes('"'))) {
573
- if (trimmed.includes("'")) quoteStyle = "'";
574
- else if (trimmed.includes('"')) quoteStyle = '"';
575
- }
576
- if (trimmed.includes("from") && trimmed.includes("react-router-dom")) {
577
- reactRouterImportIndex = i;
578
- reactRouterImportLine = line;
579
- const browserRouterPattern = /\bBrowserRouter\b/;
580
- if (browserRouterPattern.test(trimmed)) {
581
- hasBrowserRouterImport = true;
582
- }
583
- }
584
- }
585
- if (!hasBrowserRouterImport) {
586
- if (reactRouterImportIndex >= 0 && reactRouterImportLine) {
587
- const existingLine = reactRouterImportLine;
588
- const indent2 = existingLine.match(/^(\s*)/)?.[1] || "";
589
- const namedMatch = existingLine.match(/^(\s*)import\s+\{([^}]+)\}\s+from\s+(["'])react-router-dom\2/);
590
- if (namedMatch) {
591
- const importListStr = namedMatch[2];
592
- const quote = namedMatch[3];
593
- const importItems = importListStr.split(",").map((s) => s.trim()).filter(Boolean);
594
- const hasBrowserRouter = importItems.some(
595
- (item) => item === "BrowserRouter" || item === "type BrowserRouter" || item.includes("BrowserRouter")
596
- );
597
- if (!hasBrowserRouter) {
598
- importItems.push("BrowserRouter");
599
- const hasTrailingComma = importListStr.trim().endsWith(",");
600
- const separator = importListStr.includes(",\n") ? ",\n" : ", ";
601
- const formatted = importItems.join(separator) + (hasTrailingComma ? "," : "");
602
- const updatedLine = `${indent2}import { ${formatted} } from ${quote}react-router-dom${quote};`;
603
- lines[reactRouterImportIndex] = updatedLine;
604
- }
605
- } else {
606
- const newImportLine = `${indent2}import { BrowserRouter } from ${quoteStyle}react-router-dom${quoteStyle};`;
607
- lines.splice(reactRouterImportIndex + 1, 0, newImportLine);
608
- }
609
- } else {
610
- const importLine2 = `import { BrowserRouter } from ${quoteStyle}react-router-dom${quoteStyle};`;
611
- let lastImportIndex2 = -1;
612
- for (let i = 0; i < lines.length; i++) {
613
- const line = lines[i].trim();
614
- if (line.startsWith("import ") || line.startsWith("import{") || line.startsWith("import ")) {
615
- lastImportIndex2 = i;
616
- } else if (line && lastImportIndex2 >= 0 && !line.startsWith("//") && !line.startsWith("/*")) {
617
- break;
618
- }
619
- }
620
- if (lastImportIndex2 >= 0) {
621
- lines.splice(lastImportIndex2 + 1, 0, importLine2);
622
- } else {
623
- lines.unshift(importLine2);
624
- }
625
- }
626
- }
627
638
  const importLine = `import { GMooncAppRoutes } from "./gmoonc/router/AppRoutes";`;
628
639
  let lastImportIndex = -1;
629
640
  for (let i = 0; i < lines.length; i++) {
@@ -682,6 +693,7 @@ function patchBrowserRouter(consumerDir, basePath, dryRun) {
682
693
  newContent = newContent.replace(/import\s+{[^}]*Routes[^}]*}\s+from\s+["']react-router-dom["'];?\n?/g, "");
683
694
  newContent = newContent.replace(/import\s+{[^}]*Route[^}]*}\s+from\s+["']react-router-dom["'];?\n?/g, "");
684
695
  }
696
+ newContent = ensureBrowserRouterImport(newContent);
685
697
  writeFileSafe(appPath, newContent);
686
698
  logSuccess(`Patched ${appPath} (backup: ${backupPath})`);
687
699
  return { success: true, backupPath };
@@ -689,7 +701,7 @@ function patchBrowserRouter(consumerDir, basePath, dryRun) {
689
701
 
690
702
  // src/cli/index.ts
691
703
  var program = new import_commander.Command();
692
- program.name("gmoonc").description("Goalmoon Ctrl (gmoonc): Install complete dashboard into your React project").version("0.0.9").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) => {
704
+ program.name("gmoonc").description("Goalmoon Ctrl (gmoonc): Install complete dashboard into your React project").version("0.0.11").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) => {
693
705
  try {
694
706
  logInfo("\u{1F680} Starting gmoonc installer...");
695
707
  logInfo("\u{1F4E6} Installing complete dashboard into your React project\n");
@@ -697,6 +709,13 @@ program.name("gmoonc").description("Goalmoon Ctrl (gmoonc): Install complete das
697
709
  const basePath = options.base || "/app";
698
710
  const dryRun = options.dryRun || false;
699
711
  const skipRouterPatch = options.skipRouterPatch || false;
712
+ const gmooncDir = (0, import_path6.join)(projectDir, "src/gmoonc");
713
+ const markerFile = (0, import_path6.join)(gmooncDir, ".gmoonc-installed.json");
714
+ if ((0, import_fs9.existsSync)(gmooncDir) || (0, import_fs9.existsSync)(markerFile)) {
715
+ logError("gmoonc already installed (src/gmoonc exists or marker file found).");
716
+ logError("Remove src/gmoonc and restore backups to reinstall.");
717
+ process.exit(1);
718
+ }
700
719
  const safeBasePath = basePath === "/" ? "/app" : basePath;
701
720
  logInfo("\u{1F50D} Detecting React project...");
702
721
  const project = detectProject(projectDir);
@@ -753,6 +772,26 @@ program.name("gmoonc").description("Goalmoon Ctrl (gmoonc): Install complete das
753
772
  } else {
754
773
  logInfo("\n\u23ED\uFE0F Skipping router patch (router not detected)");
755
774
  }
775
+ if (!dryRun) {
776
+ ensureDirectoryExists(markerFile);
777
+ const packageJsonPath = (0, import_path6.join)(projectDir, "package.json");
778
+ let version = "0.0.10";
779
+ try {
780
+ if ((0, import_fs9.existsSync)(packageJsonPath)) {
781
+ const packageJson = JSON.parse((0, import_fs9.readFileSync)(packageJsonPath, "utf-8"));
782
+ const gmooncVersion = packageJson.dependencies?.gmoonc || packageJson.devDependencies?.gmoonc;
783
+ if (gmooncVersion) {
784
+ version = gmooncVersion.replace(/^[\^~]/, "");
785
+ }
786
+ }
787
+ } catch {
788
+ }
789
+ const markerContent = JSON.stringify({
790
+ version,
791
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
792
+ }, null, 2);
793
+ (0, import_fs9.writeFileSync)(markerFile, markerContent, "utf-8");
794
+ }
756
795
  logSuccess("\n\u2705 Installation complete!");
757
796
  logInfo("\nYour dashboard is now available at:");
758
797
  logInfo(` - Home: ${safeBasePath}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gmoonc",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "description": "Goalmoon Ctrl (gmoonc): Complete dashboard installer for React projects",
5
5
  "license": "MIT",
6
6
  "homepage": "https://gmoonc.com",
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Regression test for BrowserRouter import patch
4
+ * Tests ensureBrowserRouterImport function with various fixtures
5
+ */
6
+
7
+ import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from 'fs';
8
+ import { join, dirname } from 'path';
9
+ import { fileURLToPath } from 'url';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(__filename);
13
+
14
+ // Import the ensureBrowserRouterImport function
15
+ // Since it's not exported, we'll test it indirectly through patchBrowserRouter
16
+ // For now, we'll create a test version of the function
17
+
18
+ function ensureBrowserRouterImport(content) {
19
+ // Check if <BrowserRouter> is used in the content
20
+ if (!content.includes('<BrowserRouter')) {
21
+ return content; // No BrowserRouter used, no need to add import
22
+ }
23
+
24
+ const lines = content.split('\n');
25
+ let hasBrowserRouterImport = false;
26
+ let reactRouterImportIndex = -1;
27
+ let reactRouterImportLine = null;
28
+ let quoteStyle = '"'; // Default to double quotes
29
+
30
+ // First pass: detect existing imports and quote style
31
+ for (let i = 0; i < lines.length; i++) {
32
+ const line = lines[i];
33
+ const trimmed = line.trim();
34
+
35
+ // Detect quote style from existing imports
36
+ if (trimmed.startsWith('import ') && (trimmed.includes("'") || trimmed.includes('"'))) {
37
+ if (trimmed.includes("'")) quoteStyle = "'";
38
+ else if (trimmed.includes('"')) quoteStyle = '"';
39
+ }
40
+
41
+ // Check for react-router-dom imports
42
+ if (trimmed.includes('from') && trimmed.includes('react-router-dom')) {
43
+ reactRouterImportIndex = i;
44
+ reactRouterImportLine = line;
45
+
46
+ // Check if BrowserRouter is already imported
47
+ const browserRouterPattern = /\bBrowserRouter\b/;
48
+ if (browserRouterPattern.test(trimmed)) {
49
+ hasBrowserRouterImport = true;
50
+ }
51
+ }
52
+ }
53
+
54
+ // If BrowserRouter is already imported, return content as-is
55
+ if (hasBrowserRouterImport) {
56
+ return content;
57
+ }
58
+
59
+ // Add or update BrowserRouter import
60
+ if (reactRouterImportIndex >= 0 && reactRouterImportLine) {
61
+ // Update existing react-router-dom import to include BrowserRouter
62
+ const existingLine = reactRouterImportLine;
63
+ const indent = existingLine.match(/^(\s*)/)?.[1] || '';
64
+
65
+ // Match: import { X, Y } from "react-router-dom" or import { type X } from "react-router-dom"
66
+ const namedMatch = existingLine.match(/^(\s*)import\s+\{([^}]+)\}\s+from\s+(["'])react-router-dom\2/);
67
+ if (namedMatch) {
68
+ const importListStr = namedMatch[2];
69
+ const quote = namedMatch[3];
70
+
71
+ // Parse imports, handling "type" keywords
72
+ const importItems = importListStr.split(',').map(s => s.trim()).filter(Boolean);
73
+ const hasBrowserRouter = importItems.some(item =>
74
+ item === 'BrowserRouter' ||
75
+ item === 'type BrowserRouter' ||
76
+ item.includes('BrowserRouter')
77
+ );
78
+
79
+ if (!hasBrowserRouter) {
80
+ // Add BrowserRouter to the list (before type imports if any)
81
+ const typeImports = importItems.filter(item => item.startsWith('type '));
82
+ const regularImports = importItems.filter(item => !item.startsWith('type '));
83
+
84
+ regularImports.push('BrowserRouter');
85
+ const sortedImports = [...regularImports, ...typeImports];
86
+
87
+ // Preserve original formatting
88
+ const separator = importListStr.includes(',\n') ? ',\n' : ', ';
89
+ const formatted = sortedImports.join(separator);
90
+ const updatedLine = `${indent}import { ${formatted} } from ${quote}react-router-dom${quote};`;
91
+ lines[reactRouterImportIndex] = updatedLine;
92
+ }
93
+ } else {
94
+ // Namespace import or default import
95
+ // Add separate named import for BrowserRouter
96
+ const newImportLine = `${indent}import { BrowserRouter } from ${quoteStyle}react-router-dom${quoteStyle};`;
97
+ lines.splice(reactRouterImportIndex + 1, 0, newImportLine);
98
+ }
99
+ } else {
100
+ // No react-router-dom import exists, add it
101
+ const importLine = `import { BrowserRouter } from ${quoteStyle}react-router-dom${quoteStyle};`;
102
+ let lastImportIndex = -1;
103
+ for (let i = 0; i < lines.length; i++) {
104
+ const line = lines[i].trim();
105
+ if (line.startsWith('import ') || line.startsWith('import{') || line.startsWith('import\t')) {
106
+ lastImportIndex = i;
107
+ } else if (line && lastImportIndex >= 0 && !line.startsWith('//') && !line.startsWith('/*')) {
108
+ break;
109
+ }
110
+ }
111
+ if (lastImportIndex >= 0) {
112
+ lines.splice(lastImportIndex + 1, 0, importLine);
113
+ } else {
114
+ lines.unshift(importLine);
115
+ }
116
+ }
117
+
118
+ return lines.join('\n');
119
+ }
120
+
121
+ // Test fixtures
122
+ const fixtures = [
123
+ {
124
+ name: 'Fixture 1: No react-router-dom import',
125
+ input: `import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
126
+ import { Toaster } from 'sonner';
127
+ import Index from './pages/Index';
128
+
129
+ function App() {
130
+ return (
131
+ <QueryClientProvider client={new QueryClient()}>
132
+ <Toaster />
133
+ <BrowserRouter>
134
+ <Routes>
135
+ <Route path="/" element={<Index />} />
136
+ </Routes>
137
+ </BrowserRouter>
138
+ </QueryClientProvider>
139
+ );
140
+ }
141
+
142
+ export default App;`,
143
+ expected: (output) => {
144
+ return output.includes('import { BrowserRouter }') &&
145
+ output.includes('react-router-dom') &&
146
+ output.includes('<BrowserRouter>');
147
+ }
148
+ },
149
+ {
150
+ name: 'Fixture 2: Existing Routes, Route import',
151
+ input: `import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
152
+ import { Routes, Route } from "react-router-dom";
153
+ import Index from './pages/Index';
154
+
155
+ function App() {
156
+ return (
157
+ <QueryClientProvider client={new QueryClient()}>
158
+ <BrowserRouter>
159
+ <Routes>
160
+ <Route path="/" element={<Index />} />
161
+ </Routes>
162
+ </BrowserRouter>
163
+ </QueryClientProvider>
164
+ );
165
+ }
166
+
167
+ export default App;`,
168
+ expected: (output) => {
169
+ return output.includes('import { BrowserRouter, Routes, Route }') ||
170
+ (output.includes('import { BrowserRouter }') && output.includes('import { Routes, Route }'));
171
+ }
172
+ },
173
+ {
174
+ name: 'Fixture 3: Type import only',
175
+ input: `import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
176
+ import { type RouteObject } from "react-router-dom";
177
+ import Index from './pages/Index';
178
+
179
+ function App() {
180
+ return (
181
+ <QueryClientProvider client={new QueryClient()}>
182
+ <BrowserRouter>
183
+ <Routes>
184
+ <Route path="/" element={<Index />} />
185
+ </Routes>
186
+ </BrowserRouter>
187
+ </QueryClientProvider>
188
+ );
189
+ }
190
+
191
+ export default App;`,
192
+ expected: (output) => {
193
+ return output.includes('BrowserRouter') &&
194
+ output.includes('type RouteObject') &&
195
+ output.includes('react-router-dom');
196
+ }
197
+ },
198
+ {
199
+ name: 'Fixture 4: Namespace import',
200
+ input: `import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
201
+ import * as ReactRouterDOM from "react-router-dom";
202
+ import Index from './pages/Index';
203
+
204
+ function App() {
205
+ return (
206
+ <QueryClientProvider client={new QueryClient()}>
207
+ <BrowserRouter>
208
+ <Routes>
209
+ <Route path="/" element={<Index />} />
210
+ </Routes>
211
+ </BrowserRouter>
212
+ </QueryClientProvider>
213
+ );
214
+ }
215
+
216
+ export default App;`,
217
+ expected: (output) => {
218
+ // Should have both namespace import and named BrowserRouter import
219
+ const hasNamespace = output.includes('import * as ReactRouterDOM');
220
+ const hasNamed = output.includes('import { BrowserRouter }');
221
+ return hasNamespace && hasNamed;
222
+ }
223
+ }
224
+ ];
225
+
226
+ let passed = 0;
227
+ let failed = 0;
228
+
229
+ console.log('🧪 Running BrowserRouter import regression tests...\n');
230
+
231
+ for (const fixture of fixtures) {
232
+ try {
233
+ const output = ensureBrowserRouterImport(fixture.input);
234
+ const result = fixture.expected(output);
235
+
236
+ if (result) {
237
+ console.log(`✅ ${fixture.name}`);
238
+ passed++;
239
+ } else {
240
+ console.error(`❌ ${fixture.name}`);
241
+ console.error(' Output:', output.split('\n').slice(0, 5).join('\n'));
242
+ failed++;
243
+ }
244
+ } catch (error) {
245
+ console.error(`❌ ${fixture.name}`);
246
+ console.error(` Error: ${error.message}`);
247
+ failed++;
248
+ }
249
+ }
250
+
251
+ console.log(`\n📊 Results: ${passed} passed, ${failed} failed`);
252
+
253
+ if (failed > 0) {
254
+ console.error('\n❌ Some tests failed!');
255
+ process.exit(1);
256
+ } else {
257
+ console.log('\n✅ All tests passed!');
258
+ process.exit(0);
259
+ }
@@ -1,10 +1,15 @@
1
1
  import { type MenuItem as CoreMenuItem } from '../../core/types';
2
+ import { Home, Users, Shield, MessageSquare, Settings, ChevronRight, ChevronDown } from 'lucide-react';
3
+ import type React from 'react';
2
4
 
3
5
  /**
4
6
  * Extended MenuItem interface with submenu support
5
7
  */
6
8
  export interface MenuItemWithSubmenu extends CoreMenuItem {
7
9
  submenu?: MenuItemWithSubmenu[];
10
+ icon?: React.ReactNode;
11
+ expandIcon?: React.ReactNode;
12
+ collapseIcon?: React.ReactNode;
8
13
  }
9
14
 
10
15
  /**
@@ -25,37 +30,45 @@ export function createDefaultMenu(basePath: string = '/app'): MenuItemWithSubmen
25
30
  id: 'home',
26
31
  label: 'Home',
27
32
  path: basePath,
28
- roles: []
33
+ roles: [],
34
+ icon: <Home size={18} strokeWidth={2.5} />
29
35
  },
30
36
  {
31
37
  id: 'admin',
32
38
  label: 'Admin',
33
39
  path: `${basePath}/admin`,
34
40
  roles: ['admin'],
41
+ icon: <Shield size={18} strokeWidth={2.5} />,
42
+ expandIcon: <ChevronRight size={16} strokeWidth={2.5} />,
43
+ collapseIcon: <ChevronDown size={16} strokeWidth={2.5} />,
35
44
  submenu: [
36
45
  {
37
46
  id: 'admin-users',
38
47
  label: 'Users',
39
48
  path: `${basePath}/admin/users`,
40
- roles: ['admin']
49
+ roles: ['admin'],
50
+ icon: <Users size={16} strokeWidth={2.5} />
41
51
  },
42
52
  {
43
53
  id: 'admin-permissions',
44
54
  label: 'Permissions',
45
55
  path: `${basePath}/admin/permissions`,
46
- roles: ['admin']
56
+ roles: ['admin'],
57
+ icon: <Shield size={16} strokeWidth={2.5} />
47
58
  },
48
59
  {
49
60
  id: 'admin-authorizations',
50
61
  label: 'Authorization Management',
51
62
  path: `${basePath}/admin/authorizations`,
52
- roles: ['admin']
63
+ roles: ['admin'],
64
+ icon: <Settings size={16} strokeWidth={2.5} />
53
65
  },
54
66
  {
55
67
  id: 'admin-notifications',
56
68
  label: 'Notification Management',
57
69
  path: `${basePath}/admin/notifications`,
58
- roles: ['admin']
70
+ roles: ['admin'],
71
+ icon: <MessageSquare size={16} strokeWidth={2.5} />
59
72
  }
60
73
  ]
61
74
  },
@@ -64,12 +77,16 @@ export function createDefaultMenu(basePath: string = '/app'): MenuItemWithSubmen
64
77
  label: 'Technical',
65
78
  path: `${basePath}/technical`,
66
79
  roles: [],
80
+ icon: <Settings size={18} strokeWidth={2.5} />,
81
+ expandIcon: <ChevronRight size={16} strokeWidth={2.5} />,
82
+ collapseIcon: <ChevronDown size={16} strokeWidth={2.5} />,
67
83
  submenu: [
68
84
  {
69
85
  id: 'technical-messages',
70
86
  label: 'Messages',
71
87
  path: `${basePath}/technical/messages`,
72
- roles: []
88
+ roles: [],
89
+ icon: <MessageSquare size={16} strokeWidth={2.5} />
73
90
  }
74
91
  ]
75
92
  },
@@ -78,12 +95,16 @@ export function createDefaultMenu(basePath: string = '/app'): MenuItemWithSubmen
78
95
  label: 'Customer',
79
96
  path: `${basePath}/customer`,
80
97
  roles: [],
98
+ icon: <Users size={18} strokeWidth={2.5} />,
99
+ expandIcon: <ChevronRight size={16} strokeWidth={2.5} />,
100
+ collapseIcon: <ChevronDown size={16} strokeWidth={2.5} />,
81
101
  submenu: [
82
102
  {
83
103
  id: 'customer-messages',
84
104
  label: 'Messages',
85
105
  path: `${basePath}/customer/messages`,
86
- roles: []
106
+ roles: [],
107
+ icon: <MessageSquare size={16} strokeWidth={2.5} />
87
108
  }
88
109
  ]
89
110
  }
@@ -1,3 +1,6 @@
1
+ /* Import Montserrat font from Google Fonts (ported from sicoop-app) */
2
+ @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap');
3
+
1
4
  /* App-specific styles (ported from legacy sicoop-app) */
2
5
 
3
6
  /* Auth pages */
@@ -25,15 +28,15 @@
25
28
  }
26
29
 
27
30
  .gmoonc-auth-header h1 {
28
- color: #374161;
31
+ color: var(--gmoonc-color-text-primary);
29
32
  font-size: 28px;
30
33
  font-weight: bold;
31
34
  margin: 0 0 10px 0;
32
- font-family: 'Montserrat', Arial, Helvetica, sans-serif;
35
+ font-family: var(--gmoonc-font-family);
33
36
  }
34
37
 
35
38
  .gmoonc-auth-header p {
36
- color: #6c757d;
39
+ color: var(--gmoonc-color-text-secondary);
37
40
  font-size: 16px;
38
41
  margin: 0;
39
42
  }
@@ -70,10 +73,10 @@
70
73
 
71
74
  .gmoonc-form-group label {
72
75
  display: block;
73
- color: #374161;
76
+ color: var(--gmoonc-color-text-primary);
74
77
  font-weight: 600;
75
78
  margin-bottom: 8px;
76
- font-family: 'Montserrat', Arial, Helvetica, sans-serif;
79
+ font-family: var(--gmoonc-font-family);
77
80
  }
78
81
 
79
82
  .gmoonc-form-group input,
@@ -84,28 +87,28 @@
84
87
  border-radius: 8px;
85
88
  font-size: 16px;
86
89
  transition: border-color 0.3s ease;
87
- font-family: 'Montserrat', Arial, Helvetica, sans-serif;
90
+ font-family: var(--gmoonc-font-family);
88
91
  background-color: white;
89
- color: #374161;
92
+ color: var(--gmoonc-color-text-primary);
90
93
  box-sizing: border-box;
91
94
  }
92
95
 
93
96
  .gmoonc-form-group input::placeholder {
94
- color: #879FED;
97
+ color: var(--gmoonc-color-primary);
95
98
  opacity: 0.7;
96
99
  }
97
100
 
98
101
  .gmoonc-form-group input:focus,
99
102
  .gmoonc-form-group select:focus {
100
103
  outline: none;
101
- border-color: #6374AD;
104
+ border-color: var(--gmoonc-color-primary-2);
102
105
  background-color: white;
103
- color: #374161;
106
+ color: var(--gmoonc-color-text-primary);
104
107
  }
105
108
 
106
109
  .gmoonc-form-group input:hover,
107
110
  .gmoonc-form-group select:hover {
108
- border-color: #879FED;
111
+ border-color: var(--gmoonc-color-primary);
109
112
  background-color: #f8f9fa;
110
113
  }
111
114
 
@@ -113,7 +116,7 @@
113
116
  .gmoonc-form-group select:disabled {
114
117
  background-color: #f8f9fa;
115
118
  cursor: not-allowed;
116
- color: #6c757d;
119
+ color: var(--gmoonc-color-text-secondary);
117
120
  border-color: #dee2e6;
118
121
  }
119
122
 
@@ -128,7 +131,7 @@
128
131
  font-weight: 600;
129
132
  cursor: pointer;
130
133
  transition: all 0.3s ease;
131
- font-family: 'Montserrat', Arial, Helvetica, sans-serif;
134
+ font-family: var(--gmoonc-font-family);
132
135
  margin-bottom: 20px;
133
136
  }
134
137
 
@@ -149,7 +152,7 @@
149
152
  }
150
153
 
151
154
  .gmoonc-auth-link {
152
- color: #6374AD;
155
+ color: var(--gmoonc-link);
153
156
  text-decoration: none;
154
157
  font-size: 14px;
155
158
  transition: color 0.3s ease;
@@ -158,7 +161,7 @@
158
161
  }
159
162
 
160
163
  .gmoonc-auth-link:hover {
161
- color: #374161;
164
+ color: var(--gmoonc-color-text-primary);
162
165
  text-decoration: underline;
163
166
  }
164
167
 
@@ -179,10 +182,10 @@
179
182
 
180
183
  .gmoonc-content-header h2 {
181
184
  margin: 0;
182
- color: #374161;
185
+ color: var(--gmoonc-color-text-primary);
183
186
  font-size: 24px;
184
187
  font-weight: 600;
185
- font-family: 'Montserrat', Arial, Helvetica, sans-serif;
188
+ font-family: var(--gmoonc-font-family);
186
189
  }
187
190
 
188
191
  .gmoonc-content-body {
@@ -198,15 +201,15 @@
198
201
  }
199
202
 
200
203
  .gmoonc-welcome-content h2 {
201
- color: #374161;
204
+ color: var(--gmoonc-color-text-primary);
202
205
  font-size: 32px;
203
206
  margin-bottom: 20px;
204
207
  font-weight: bold;
205
- font-family: 'Montserrat', Arial, Helvetica, sans-serif;
208
+ font-family: var(--gmoonc-font-family);
206
209
  }
207
210
 
208
211
  .gmoonc-welcome-content p {
209
- color: #6c757d;
212
+ color: var(--gmoonc-color-text-secondary);
210
213
  font-size: 18px;
211
214
  margin-bottom: 30px;
212
215
  line-height: 1.6;
@@ -332,24 +335,24 @@
332
335
  .user-name {
333
336
  font-size: 16px;
334
337
  font-weight: 600;
335
- color: #374161;
338
+ color: var(--gmoonc-color-text-primary);
336
339
  margin-bottom: 4px;
337
- font-family: 'Montserrat', Arial, Helvetica, sans-serif;
340
+ font-family: var(--gmoonc-font-family);
338
341
  }
339
342
 
340
343
  .user-email {
341
344
  font-size: 14px;
342
345
  color: #6c757d;
343
346
  margin-bottom: 4px;
344
- font-family: 'Montserrat', Arial, Helvetica, sans-serif;
347
+ font-family: var(--gmoonc-font-family);
345
348
  }
346
349
 
347
350
  .user-role {
348
351
  font-size: 12px;
349
- color: #879FED;
352
+ color: var(--gmoonc-color-primary);
350
353
  text-transform: capitalize;
351
354
  font-weight: 500;
352
- font-family: 'Montserrat', Arial, Helvetica, sans-serif;
355
+ font-family: var(--gmoonc-font-family);
353
356
  }
354
357
 
355
358
  .dropdown-divider {
@@ -373,9 +376,9 @@
373
376
  align-items: center;
374
377
  gap: 12px;
375
378
  font-size: 14px;
376
- color: #374161;
379
+ color: var(--gmoonc-color-text-primary, #374161);
377
380
  transition: background-color 0.2s ease;
378
- font-family: 'Montserrat', Arial, Helvetica, sans-serif;
381
+ font-family: var(--gmoonc-font-family);
379
382
  }
380
383
 
381
384
  .dropdown-option:hover {
@@ -28,6 +28,14 @@
28
28
  --gmoonc-color-text-muted: #dbe2ea;
29
29
  --gmoonc-color-text-white: #ffffff;
30
30
 
31
+ /* Colors - Text (Menu & Header specific) */
32
+ --gmoonc-sidebar-text: #ffffff;
33
+ --gmoonc-sidebar-text-active: #293047;
34
+ --gmoonc-sidebar-muted: #ffffff;
35
+ --gmoonc-header-text: #ffffff;
36
+ --gmoonc-surface-text: #374161;
37
+ --gmoonc-link: #6374AD;
38
+
31
39
  /* Colors - Primary */
32
40
  --gmoonc-color-primary: #879FED;
33
41
  --gmoonc-color-primary-2: #6374AD;
@@ -1,8 +1,11 @@
1
+ /* Import Montserrat font from Google Fonts (ported from sicoop-app) */
2
+ @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap');
3
+
1
4
  /* Gmoonc Menu Styles - Base for all devices */
2
5
  .gmoonc-menu {
3
6
  width: 250px;
4
7
  background-color: #eaf0f5;
5
- font-family: 'Montserrat', Arial, Helvetica, sans-serif;
8
+ font-family: var(--gmoonc-font-family, 'Montserrat', Arial, Helvetica, sans-serif);
6
9
  border-radius: 12px;
7
10
  box-shadow: 0 4px 6px rgba(55, 65, 97, 0.1);
8
11
  overflow: hidden;
@@ -73,7 +76,7 @@
73
76
  height: 3.375rem;
74
77
  padding: 0 1.25rem;
75
78
  background-color: #3F4A6E;
76
- color: #ffffff;
79
+ color: var(--gmoonc-sidebar-text, #ffffff);
77
80
  cursor: pointer;
78
81
  transition: all 0.3s ease;
79
82
  font-weight: 500;
@@ -82,20 +85,37 @@
82
85
  border: none;
83
86
  width: 100%;
84
87
  text-align: left;
85
- font-family: inherit;
88
+ font-family: var(--gmoonc-font-family, 'Montserrat', Arial, Helvetica, sans-serif);
89
+ gap: 0.75rem;
90
+ }
91
+
92
+ .gmoonc-menu-icon {
93
+ display: inline-flex;
94
+ align-items: center;
95
+ justify-content: center;
96
+ flex-shrink: 0;
97
+ width: 18px;
98
+ height: 18px;
99
+ color: inherit;
86
100
  }
87
101
 
88
102
  .gmoonc-menu-link:hover {
89
103
  background-color: #6374AD;
90
104
  transform: translateX(5px);
105
+ color: var(--gmoonc-sidebar-text, #ffffff);
91
106
  }
92
107
 
93
108
  .gmoonc-menu-link.active {
94
109
  background-color: #879FED;
95
- color: #293047;
110
+ color: var(--gmoonc-sidebar-text-active, #293047);
96
111
  font-weight: 600;
97
112
  }
98
113
 
114
+ .gmoonc-menu-label {
115
+ flex: 1;
116
+ color: inherit;
117
+ }
118
+
99
119
  .gmoonc-menu-link.has-submenu {
100
120
  position: relative;
101
121
  }
@@ -125,7 +145,7 @@
125
145
  width: 100%;
126
146
  min-height: 44px;
127
147
  padding: 0.8rem 1.25rem 0.8rem 2.5rem;
128
- color: #374161;
148
+ color: var(--gmoonc-surface-text, #374161);
129
149
  text-decoration: none;
130
150
  transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
131
151
  font-size: 0.9rem;
@@ -134,13 +154,24 @@
134
154
  border-radius: 0 10px 10px 0;
135
155
  border: none;
136
156
  cursor: pointer;
137
- font-family: inherit;
157
+ font-family: var(--gmoonc-font-family, 'Montserrat', Arial, Helvetica, sans-serif);
138
158
  text-align: left;
159
+ gap: 0.75rem;
160
+ }
161
+
162
+ .gmoonc-submenu-link .gmoonc-menu-icon {
163
+ width: 16px;
164
+ height: 16px;
139
165
  }
140
166
 
141
167
  .gmoonc-submenu-link:hover {
142
168
  background-color: rgba(219, 226, 234, 0.5);
143
- color: #374161;
169
+ color: var(--gmoonc-surface-text, #374161);
170
+ }
171
+
172
+ .gmoonc-submenu-link span:not(.gmoonc-menu-icon) {
173
+ flex: 1;
174
+ color: inherit;
144
175
  }
145
176
 
146
177
  .gmoonc-submenu-link:focus-visible {
@@ -151,9 +182,9 @@
151
182
 
152
183
  .gmoonc-submenu-link.active {
153
184
  background-color: #dbe2ea;
154
- color: #374161;
185
+ color: var(--gmoonc-surface-text, #374161);
155
186
  font-weight: 600;
156
- border-left-color: #374161;
187
+ border-left-color: var(--gmoonc-color-text-primary, #374161);
157
188
  box-shadow: inset 0 0 0 1px rgba(55, 65, 97, 0.12);
158
189
  }
159
190
 
@@ -173,7 +204,7 @@
173
204
  align-items: center;
174
205
  padding: 20px 30px;
175
206
  background: linear-gradient(135deg, #374161, #3F4A6E);
176
- color: white;
207
+ color: var(--gmoonc-header-text, #ffffff);
177
208
  box-shadow: 0 4px 20px rgba(55, 65, 97, 0.3);
178
209
  position: fixed;
179
210
  top: 0;
@@ -187,7 +218,8 @@
187
218
  margin: 0;
188
219
  font-size: 28px;
189
220
  font-weight: bold;
190
- font-family: 'Montserrat', Arial, Helvetica, sans-serif;
221
+ font-family: var(--gmoonc-font-family, 'Montserrat', Arial, Helvetica, sans-serif);
222
+ color: var(--gmoonc-header-text, #ffffff);
191
223
  position: absolute;
192
224
  left: 50%;
193
225
  transform: translateX(-50%);
@@ -203,6 +235,13 @@
203
235
  margin-left: auto;
204
236
  }
205
237
 
238
+ /* Ensure profile is on the right side on all breakpoints */
239
+ @media (max-width: 1366px) {
240
+ .gmoonc-header-right {
241
+ margin-left: auto;
242
+ }
243
+ }
244
+
206
245
  .gmoonc-content {
207
246
  display: flex;
208
247
  min-height: calc(100vh - 80px);
@@ -340,6 +379,7 @@
340
379
  text-align: center;
341
380
  margin: 0;
342
381
  white-space: nowrap;
382
+ color: var(--gmoonc-header-text, #ffffff);
343
383
  }
344
384
 
345
385
  .gmoonc-sidebar {