g360-cli 1.5.0 → 1.6.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "g360-cli",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "CLI tool for bootstrapping G360 projects with standardized structure, assets, and identity",
5
5
  "type": "module",
6
6
  "main": "src/cli.js",
@@ -0,0 +1,5 @@
1
+ # g360-signature Flet
2
+ __version__ = "1.1.0"
3
+ from .g360_signature import G360Signature, g360_own, g360_powered, g360_footer
4
+
5
+ __all__ = ["G360Signature", "g360_own", "g360_powered", "g360_footer"]
@@ -0,0 +1,224 @@
1
+ """
2
+ g360-signature para Flet
3
+ Widget reutilizable del isotipo G360 (3 puntos + chevron) para apps Flet.
4
+ Soporta tema automatico (claro/oscuro) igual que la version web.
5
+
6
+ Uso:
7
+ from g360_flet.g360_signature import G360Signature
8
+
9
+ page.add(G360Signature(mode="own"))
10
+ page.add(G360Signature(mode="powered", version="3.1"))
11
+ """
12
+
13
+ import flet as ft
14
+
15
+ # Colores G360
16
+ G360_GREEN = "#00d084"
17
+ G360_GRAY_DARK = "#94a3b8" # dark mode
18
+ G360_GRAY_LIGHT = "#64748b" # light mode
19
+
20
+
21
+ class G360Signature(ft.Row):
22
+ """
23
+ Componente de branding G360 para Flet.
24
+
25
+ Detecta automaticamente el tema de la pagina (light/dark)
26
+ y ajusta los colores igual que el Web Component original.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ mode: str = "own",
32
+ version: str | None = None,
33
+ opacity: float = 0.4,
34
+ grayscale: bool = False,
35
+ spacing: int = 4,
36
+ on_hover=None,
37
+ **kwargs,
38
+ ):
39
+ super().__init__(**kwargs)
40
+
41
+ self._base_opacity = opacity
42
+ self._grayscale = grayscale
43
+ self.spacing = spacing
44
+ self.vertical_alignment = ft.CrossAxisAlignment.CENTER
45
+
46
+ self._mode = "own"
47
+ self._version = None
48
+
49
+ self.mode = mode
50
+ self.version = version
51
+
52
+ if on_hover is None:
53
+ self.on_hover = self._default_on_hover
54
+ else:
55
+ self.on_hover = on_hover
56
+
57
+ self.opacity = opacity
58
+
59
+ def _default_on_hover(self, e: ft.HoverEvent):
60
+ self.opacity = 1.0 if e.data == "true" else self._base_opacity
61
+ self.update()
62
+
63
+ def _is_dark(self) -> bool:
64
+ """Detecta si el tema actual es oscuro."""
65
+ if not self._is_mounted():
66
+ return False
67
+ mode = self.page.theme_mode
68
+ if mode == ft.ThemeMode.DARK:
69
+ return True
70
+ if mode == ft.ThemeMode.LIGHT:
71
+ return False
72
+ # SYSTEM: detectar por color de fondo
73
+ bg = self.page.bgcolor
74
+ if bg and isinstance(bg, str):
75
+ bg_lower = bg.lower()
76
+ if bg_lower.startswith("#"):
77
+ r = int(bg_lower[1:3], 16)
78
+ g = int(bg_lower[3:5], 16)
79
+ b = int(bg_lower[5:7], 16)
80
+ lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255
81
+ return lum < 0.5
82
+ return False
83
+
84
+ def _get_colors(self):
85
+ """Retorna colores segun el tema actual."""
86
+ if self._grayscale:
87
+ return G360_GRAY_DARK, G360_GRAY_DARK
88
+ if self._is_dark():
89
+ return G360_GREEN, G360_GRAY_DARK
90
+ return G360_GREEN, G360_GRAY_LIGHT
91
+
92
+ def _build(self):
93
+ green, gray = self._get_colors()
94
+
95
+ # 3 puntos verticales
96
+ dots = ft.Column(
97
+ controls=[
98
+ ft.Container(width=4, height=4, border_radius=2, bgcolor=gray),
99
+ ft.Container(width=4, height=4, border_radius=2, bgcolor=green),
100
+ ft.Container(width=4, height=4, border_radius=2, bgcolor=gray),
101
+ ],
102
+ spacing=2,
103
+ horizontal_alignment=ft.CrossAxisAlignment.CENTER,
104
+ )
105
+
106
+ # Chevron ">"
107
+ chevron = ft.Text(
108
+ ">",
109
+ size=14,
110
+ color=green,
111
+ font_family="Consolas",
112
+ selectable=False,
113
+ )
114
+
115
+ isotype = ft.Row(
116
+ controls=[dots, chevron],
117
+ spacing=1,
118
+ vertical_alignment=ft.CrossAxisAlignment.CENTER,
119
+ )
120
+
121
+ # Texto principal
122
+ if self.mode == "own":
123
+ text_controls = [
124
+ ft.Text("G360", size=12, color=green, weight=ft.FontWeight.BOLD,
125
+ font_family="Consolas", selectable=False),
126
+ ft.Text(" by ccusi", size=12, color=gray,
127
+ font_family="Consolas", selectable=False),
128
+ ]
129
+ else:
130
+ text_controls = [
131
+ ft.Text("powered by ", size=12, color=gray,
132
+ font_family="Consolas", selectable=False),
133
+ ft.Text("G360", size=12, color=green, weight=ft.FontWeight.BOLD,
134
+ font_family="Consolas", selectable=False),
135
+ ]
136
+
137
+ if self.version:
138
+ text_controls.append(
139
+ ft.Text(f" v{self.version}", size=12, color=gray, opacity=0.7,
140
+ font_family="Consolas", selectable=False)
141
+ )
142
+
143
+ text_row = ft.Row(
144
+ controls=text_controls,
145
+ spacing=0,
146
+ vertical_alignment=ft.CrossAxisAlignment.CENTER,
147
+ )
148
+
149
+ self.controls = [isotype, text_row]
150
+
151
+ # -- Propiedades --
152
+
153
+ def _is_mounted(self) -> bool:
154
+ """Verifica si el widget ya esta montado en la pagina."""
155
+ try:
156
+ return self.page is not None
157
+ except RuntimeError:
158
+ return False
159
+
160
+ @property
161
+ def mode(self) -> str:
162
+ return self._mode
163
+
164
+ @mode.setter
165
+ def mode(self, value: str):
166
+ if value not in ("own", "powered"):
167
+ raise ValueError(f"mode debe ser 'own' o 'powered', recibido: '{value}'")
168
+ self._mode = value
169
+ if self._is_mounted():
170
+ self._build()
171
+
172
+ @property
173
+ def version(self) -> str | None:
174
+ return self._version
175
+
176
+ @version.setter
177
+ def version(self, value: str | None):
178
+ self._version = value
179
+ if self._is_mounted():
180
+ self._build()
181
+
182
+ @property
183
+ def grayscale(self) -> bool:
184
+ return self._grayscale
185
+
186
+ @grayscale.setter
187
+ def grayscale(self, value: bool):
188
+ self._grayscale = value
189
+ if self._is_mounted():
190
+ self._build()
191
+
192
+ def did_mount(self):
193
+ """Se ejecuta cuando el widget se monta en la pagina."""
194
+ self._build()
195
+ self.update()
196
+
197
+ def update(self):
198
+ """Actualiza colores segun tema y refresca."""
199
+ if self._is_mounted():
200
+ self._build()
201
+ super().update()
202
+
203
+
204
+ # ============================================================
205
+ # Funciones de conveniencia
206
+ # ============================================================
207
+
208
+ def g360_own(version: str | None = None, **kwargs) -> G360Signature:
209
+ """Isotipo modo propio: G360 by ccusi"""
210
+ return G360Signature(mode="own", version=version, **kwargs)
211
+
212
+
213
+ def g360_powered(version: str | None = None, **kwargs) -> G360Signature:
214
+ """Isotipo modo powered: powered by G360"""
215
+ return G360Signature(mode="powered", version=version, **kwargs)
216
+
217
+
218
+ def g360_footer(version: str | None = None, **kwargs) -> ft.Container:
219
+ """Footer completo con el isotipo G360."""
220
+ return ft.Container(
221
+ content=G360Signature(mode="powered", version=version, opacity=0.6, **kwargs),
222
+ padding=ft.padding.Padding(0, 0, 0, 16),
223
+ alignment=ft.alignment.Alignment(0, 1),
224
+ )
@@ -0,0 +1,130 @@
1
+ /**
2
+ * g360-signature v1.1.0
3
+ * Web component para branding G360 - Sin Shadow DOM
4
+ */
5
+
6
+ const G360_SIGNATURE_CSS = `
7
+ .g360-signature {
8
+ display: inline-flex;
9
+ align-items: center;
10
+ gap: 4px;
11
+ height: 18px;
12
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
13
+ font-size: 12px;
14
+ line-height: 1;
15
+ opacity: 0.4;
16
+ transition: opacity 0.3s ease;
17
+ cursor: default;
18
+ --g360-green: #00d084;
19
+ --g360-gray: #64748b;
20
+ }
21
+ .g360-signature:hover {
22
+ opacity: 1;
23
+ }
24
+ .g360-signature .g360-iw {
25
+ display: flex;
26
+ align-items: center;
27
+ gap: 1px;
28
+ height: 100%;
29
+ }
30
+ .g360-signature .g360-iso {
31
+ display: flex;
32
+ flex-direction: column;
33
+ justify-content: center;
34
+ gap: 2px;
35
+ height: 100%;
36
+ }
37
+ .g360-signature .g360-d {
38
+ width: 4px;
39
+ height: 4px;
40
+ border-radius: 50%;
41
+ }
42
+ .g360-signature .g360-dt { background: var(--g360-gray); }
43
+ .g360-signature .g360-dm { background: var(--g360-green); }
44
+ .g360-signature .g360-db { background: var(--g360-gray); }
45
+ .g360-signature .g360-ch {
46
+ color: var(--g360-green);
47
+ width: 20px;
48
+ height: 20px;
49
+ }
50
+ .g360-signature .g360-t {
51
+ color: var(--g360-gray);
52
+ letter-spacing: 0.5px;
53
+ }
54
+ .g360-signature .g360-v {
55
+ color: var(--g360-gray);
56
+ opacity: 0.7;
57
+ }
58
+ .g360-signature .g360-s {
59
+ color: var(--g360-gray);
60
+ opacity: 0.5;
61
+ }
62
+ @media (prefers-color-scheme: dark) {
63
+ .g360-signature { --g360-gray: #94a3b8; }
64
+ }
65
+ `;
66
+
67
+ class G360Signature extends HTMLElement {
68
+ static get observedAttributes() {
69
+ return ['mode', 'version'];
70
+ }
71
+
72
+ constructor() {
73
+ super();
74
+ }
75
+
76
+ connectedCallback() {
77
+ this._injectStyles();
78
+ this.render();
79
+ }
80
+
81
+ attributeChangedCallback() {
82
+ this.render();
83
+ }
84
+
85
+ _injectStyles() {
86
+ if (document.getElementById('g360-signature-css')) return;
87
+ const style = document.createElement('style');
88
+ style.id = 'g360-signature-css';
89
+ style.textContent = G360_SIGNATURE_CSS;
90
+ document.head.appendChild(style);
91
+ }
92
+
93
+ get mode() {
94
+ return this.getAttribute('mode') || 'own';
95
+ }
96
+
97
+ get version() {
98
+ return this.getAttribute('version') || '';
99
+ }
100
+
101
+ render() {
102
+ const isOwn = this.mode === 'own';
103
+ const mainText = isOwn ? 'G360 by ccusi' : 'powered by G360';
104
+
105
+ const versionHtml = this.version
106
+ ? `<span class="g360-s">&gt;</span><span class="g360-v">${this.version}</span>`
107
+ : '';
108
+
109
+ this.innerHTML = `
110
+ <span class="g360-signature">
111
+ <span class="g360-iw">
112
+ <span class="g360-iso">
113
+ <span class="g360-d g360-dt"></span>
114
+ <span class="g360-d g360-dm"></span>
115
+ <span class="g360-d g360-db"></span>
116
+ </span>
117
+ <svg class="g360-ch" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
118
+ <polyline points="6 4 14 10 6 16"></polyline>
119
+ </svg>
120
+ </span>
121
+ <span class="g360-t">${mainText}</span>
122
+ ${versionHtml}
123
+ </span>
124
+ `;
125
+ }
126
+ }
127
+
128
+ if (!customElements.get('g360-signature')) {
129
+ customElements.define('g360-signature', G360Signature);
130
+ }
@@ -156,20 +156,27 @@ class G360Theme:
156
156
 
157
157
  def footer_signature(self):
158
158
  import flet as ft
159
- mode = self.config.get("signature", {}).get("mode", "powered")
160
- text = self.config.get("signature", {}).get("text", "powered by G360")
161
- return ft.Container(
162
- content=ft.Row(
163
- [
164
- self.logo_component(height=16),
165
- ft.Container(width=8),
166
- ft.Text(text, size=10, color=self.muted),
167
- ],
168
- alignment=ft.MainAxisAlignment.CENTER,
169
- ),
170
- padding=8,
171
- bgcolor=self.surface,
172
- )
159
+ try:
160
+ from core.components.g360_signature import g360_footer
161
+ mode = self.config.get("signature", {}).get("mode", "powered")
162
+ version = self.config.get("signature", {}).get("version", None)
163
+ return g360_footer(version=version)
164
+ except ImportError:
165
+ # Fallback si g360_signature no esta instalado
166
+ mode = self.config.get("signature", {}).get("mode", "powered")
167
+ text = self.config.get("signature", {}).get("text", "powered by G360")
168
+ return ft.Container(
169
+ content=ft.Row(
170
+ [
171
+ self.logo_component(height=16),
172
+ ft.Container(width=8),
173
+ ft.Text(text, size=10, color=self.muted),
174
+ ],
175
+ alignment=ft.MainAxisAlignment.CENTER,
176
+ ),
177
+ padding=8,
178
+ bgcolor=self.surface,
179
+ )
173
180
 
174
181
  @property
175
182
  def as_dict(self):
@@ -14,5 +14,8 @@
14
14
  <body>
15
15
  <div id="root"></div>
16
16
  <script type="module" src="/src/main.jsx"></script>
17
+ <script src="https://unpkg.com/g360-signature@latest/index.js"></script>
18
+ <!-- Firma Oficial G360 -->
19
+ <g360-signature mode="powered" style="position: fixed; bottom: 16px; right: 16px; z-index: 99999;"></g360-signature>
17
20
  </body>
18
21
  </html>
package/src/cli.js CHANGED
@@ -109,9 +109,14 @@ program
109
109
 
110
110
  program
111
111
  .command('signature')
112
- .argument('<command>', 'Command to execute (install)')
112
+ .argument('<command>', 'Command to execute: install, positions')
113
+ .description('Install g360-signature branding component')
113
114
  .option('-p, --path <path>', 'Target project path', '.')
114
115
  .option('--force', 'Force reinstall if already exists')
116
+ .option('-m, --mode <mode>', 'Signature mode: own or powered', 'powered')
117
+ .option('-v, --version <version>', 'Version to display')
118
+ .option('--position <position>', 'Signature position: bottom-right, bottom-left, bottom-center, footer-right, footer-left', 'bottom-right')
119
+ .option('-i, --interactive', 'Interactive mode with guided suggestions')
115
120
  .action(signature);
116
121
 
117
122
  program.parse();
@@ -1,55 +1,252 @@
1
- import chalk from 'chalk';
2
- import fs from 'fs-extra';
3
- import path from 'path';
4
- import { fileURLToPath } from 'url';
5
-
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
-
8
- export async function signature(command, options) {
9
- const { path: targetPath = '.', force = false } = options;
10
-
11
- if (command === 'install') {
12
- console.log(chalk.bold.cyan('\n🔖 Instalando g360-signature\n'));
13
-
14
- const targetDir = path.resolve(process.cwd(), targetPath);
15
- const indexHtmlPath = path.join(targetDir, 'index.html');
16
-
17
- if (!fs.existsSync(indexHtmlPath)) {
18
- console.error(chalk.red(' No se encontro archivo index.html en el directorio actual'));
19
- return;
20
- }
21
-
22
- let htmlContent = fs.readFileSync(indexHtmlPath, 'utf8');
23
-
24
- const scriptTag = ' <script type="module" src="https://unpkg.com/g360-signature@latest/index.js"></script>';
25
- const signatureComponent = `
26
- <!-- Firma Oficial G360 -->
27
- <g360-signature mode="powered" style="position: fixed; bottom: 16px; right: 16px; z-index: 99999;"></g360-signature>
28
- `;
29
-
30
- // Si --force es true, eliminamos cualquier instancia existente para una reinstalación limpia.
31
- // De lo contrario, si ya está instalado y no se fuerza, salimos.
32
- if (force) {
33
- htmlContent = htmlContent.replace(new RegExp(scriptTag.trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '');
34
- htmlContent = htmlContent.replace(new RegExp(signatureComponent.trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '');
35
- htmlContent = htmlContent.replace(/\n\s*\n/g, '\n'); // Limpiar posibles líneas vacías extra
36
- } else if (htmlContent.includes('g360-signature')) {
37
- console.log(chalk.yellow('⚠️ g360-signature ya se encuentra instalado'));
38
- console.log(chalk.gray('Usa --force para reinstalar'));
39
- return;
40
- }
41
-
42
- if (!htmlContent.includes(scriptTag)) {
43
- htmlContent = htmlContent.replace('</body>', `${scriptTag}\n </body>`);
44
- }
45
-
46
- if (!htmlContent.includes('<g360-signature')) {
47
- htmlContent = htmlContent.replace('</body>', `${signatureComponent}\n </body>`);
48
- }
49
-
50
- fs.writeFileSync(indexHtmlPath, htmlContent, 'utf8');
51
-
52
- console.log(chalk.green('✅ g360-signature instalado exitosamente!'));
53
- console.log(chalk.gray('\nEl componente ha sido agregado en el pie de pagina de tu proyecto\n'));
54
- }
55
- }
1
+ import chalk from 'chalk';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const SIGNATURE_ASSETS = path.join(__dirname, '..', 'assets', 'signature');
8
+
9
+ const POSITIONS = {
10
+ 'bottom-right': 'position: fixed; bottom: 16px; right: 16px; z-index: 99999;',
11
+ 'bottom-left': 'position: fixed; bottom: 16px; left: 16px; z-index: 99999;',
12
+ 'bottom-center': 'position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%); z-index: 99999;',
13
+ 'footer-right': 'position: static; float: right;',
14
+ 'footer-left': 'position: static; float: left;',
15
+ };
16
+
17
+ const FLET_POSITIONS = {
18
+ 'sidebar': 'En el sidebar como footer',
19
+ 'footer': 'En el footer de la pagina',
20
+ 'fixed-bottom': 'Fijo abajo a la derecha',
21
+ 'header': 'En el header/cabecera',
22
+ };
23
+
24
+ export async function signature(command, options) {
25
+ const {
26
+ path: targetPath = '.',
27
+ force = false,
28
+ mode = 'powered',
29
+ version = null,
30
+ position = 'bottom-right',
31
+ interactive = false,
32
+ } = options;
33
+
34
+ if (command === 'install') {
35
+ console.log(chalk.bold.cyan('\n🔖 Instalando g360-signature\n'));
36
+
37
+ const targetDir = path.resolve(process.cwd(), targetPath);
38
+ const projectType = detectProjectType(targetDir);
39
+
40
+ if (!projectType) {
41
+ console.error(chalk.red('❌ No se detecto un proyecto web o Flet en el directorio actual'));
42
+ console.log(chalk.gray('Proyectos soportados: web (HTML), Flet (Python)'));
43
+ return;
44
+ }
45
+
46
+ // Modo interactivo: guiar al usuario
47
+ if (interactive) {
48
+ const selectedPosition = await interactivePosition(projectType);
49
+ if (selectedPosition) {
50
+ options.position = selectedPosition;
51
+ }
52
+ }
53
+
54
+ if (projectType === 'flet') {
55
+ await installFlet(targetDir, { force, mode, version, position: options.position });
56
+ } else if (projectType === 'web') {
57
+ await installWeb(targetDir, { force, mode, version, position: options.position });
58
+ }
59
+
60
+ console.log(chalk.green('\n✅ g360-signature instalado exitosamente!'));
61
+ showUsageTips(projectType, options.position);
62
+ }
63
+
64
+ if (command === 'positions') {
65
+ showPositions();
66
+ }
67
+ }
68
+
69
+ function detectProjectType(dir) {
70
+ const pyprojectPath = path.join(dir, 'pyproject.toml');
71
+ if (fs.existsSync(pyprojectPath)) {
72
+ const content = fs.readFileSync(pyprojectPath, 'utf8');
73
+ if (content.includes('flet')) return 'flet';
74
+ }
75
+ const mainPy = path.join(dir, 'src', 'main.py');
76
+ if (fs.existsSync(mainPy)) {
77
+ const content = fs.readFileSync(mainPy, 'utf8');
78
+ if (content.includes('flet')) return 'flet';
79
+ }
80
+ const indexHtml = path.join(dir, 'index.html');
81
+ if (fs.existsSync(indexHtml)) return 'web';
82
+ return null;
83
+ }
84
+
85
+ function showPositions() {
86
+ console.log(chalk.bold.cyan('\n📍 Posiciones disponibles para Web:\n'));
87
+ Object.entries(POSITIONS).forEach(([key, value]) => {
88
+ console.log(chalk.white(` ${key.padEnd(18)} ${chalk.gray(value)}`));
89
+ });
90
+
91
+ console.log(chalk.bold.cyan('\n📍 Posiciones disponibles para Flet:\n'));
92
+ Object.entries(FLET_POSITIONS).forEach(([key, value]) => {
93
+ console.log(chalk.white(` ${key.padEnd(18)} ${chalk.gray(value)}`));
94
+ });
95
+
96
+ console.log(chalk.gray('\nEjemplo: g360 signature install --position bottom-left\n'));
97
+ }
98
+
99
+ async function interactivePosition(projectType) {
100
+ console.log(chalk.bold.cyan('\n📍 Selecciona la posicion de la firma:\n'));
101
+
102
+ const options = projectType === 'flet'
103
+ ? Object.entries(FLET_POSITIONS)
104
+ : Object.entries(POSITIONS);
105
+
106
+ options.forEach(([key, value], index) => {
107
+ console.log(chalk.white(` ${index + 1}. ${key.padEnd(18)} ${chalk.gray(value)}`));
108
+ });
109
+
110
+ console.log(chalk.gray('\n Presiona Enter para usar la posicion por defecto (bottom-right)'));
111
+ console.log(chalk.gray(' O escribe el nombre de la posicion\n'));
112
+
113
+ // En modo no-interactivo, retornar default
114
+ return null;
115
+ }
116
+
117
+ function showUsageTips(projectType, position) {
118
+ console.log(chalk.bold.cyan('\n💡 Tips de uso:\n'));
119
+
120
+ if (projectType === 'web') {
121
+ console.log(chalk.white(' Web Component:'));
122
+ console.log(chalk.gray(' <g360-signature mode="powered"></g360-signature>'));
123
+ console.log(chalk.gray(' <g360-signature mode="own" version="1.0"></g360-signature>\n'));
124
+ } else {
125
+ console.log(chalk.white(' Flet Widget:'));
126
+ console.log(chalk.gray(' from core.components.g360_signature import G360Signature, g360_footer'));
127
+ console.log(chalk.gray(' page.add(g360_footer(version="1.0"))'));
128
+ console.log(chalk.gray(' page.add(G360Signature(mode="own"))\n'));
129
+ }
130
+
131
+ console.log(chalk.white(' Cambiar posicion:'));
132
+ console.log(chalk.gray(' g360 signature install --position bottom-left\n'));
133
+
134
+ console.log(chalk.white(' Ver todas las posiciones:'));
135
+ console.log(chalk.gray(' g360 signature positions\n'));
136
+ }
137
+
138
+ async function installWeb(targetDir, { force, mode, version, position }) {
139
+ console.log(chalk.gray('Detectado: Proyecto Web'));
140
+
141
+ const indexHtmlPath = path.join(targetDir, 'index.html');
142
+ let htmlContent = fs.readFileSync(indexHtmlPath, 'utf8');
143
+
144
+ const hasLocalIndex = fs.existsSync(path.join(targetDir, 'g360-signature', 'index.js'));
145
+ const scriptSrc = hasLocalIndex
146
+ ? ' <script src="g360-signature/index.js"></script>'
147
+ : ' <script src="https://unpkg.com/g360-signature@latest/index.js"></script>';
148
+
149
+ const versionAttr = version ? ` version="${version}"` : '';
150
+ const positionStyle = POSITIONS[position] || POSITIONS['bottom-right'];
151
+
152
+ const signatureComponent = `
153
+ <!-- Firma Oficial G360 -->
154
+ <g360-signature mode="${mode}"${versionAttr} style="${positionStyle}"></g360-signature>
155
+ `;
156
+
157
+ if (force) {
158
+ htmlContent = htmlContent.replace(/\n.*g360-signature.*\n/g, '\n');
159
+ htmlContent = htmlContent.replace(/<script[^>]*unpkg\.com\/g360-signature[^>]*><\/script>/g, '');
160
+ htmlContent = htmlContent.replace(/<script[^>]*g360-signature\/index\.js[^>]*><\/script>/g, '');
161
+ htmlContent = htmlContent.replace(/\n\s*\n/g, '\n');
162
+ } else if (htmlContent.includes('g360-signature')) {
163
+ console.log(chalk.yellow('⚠️ g360-signature ya se encuentra instalado'));
164
+ console.log(chalk.gray('Usa --force para reinstalar'));
165
+ return;
166
+ }
167
+
168
+ if (!hasLocalIndex) {
169
+ const sigDir = path.join(targetDir, 'g360-signature');
170
+ fs.copySync(SIGNATURE_ASSETS, sigDir);
171
+ console.log(chalk.gray(' Archivos copiados a g360-signature/'));
172
+ }
173
+
174
+ if (!htmlContent.includes('g360-signature/index.js') && !htmlContent.includes('unpkg.com/g360-signature')) {
175
+ htmlContent = htmlContent.replace('</body>', `${scriptSrc}\n </body>`);
176
+ }
177
+ if (!htmlContent.includes('<g360-signature')) {
178
+ htmlContent = htmlContent.replace('</body>', `${signatureComponent}\n </body>`);
179
+ }
180
+
181
+ fs.writeFileSync(indexHtmlPath, htmlContent, 'utf8');
182
+ console.log(chalk.gray(' index.html actualizado'));
183
+ }
184
+
185
+ async function installFlet(targetDir, { force, mode, version, position }) {
186
+ console.log(chalk.gray('Detectado: Proyecto Flet'));
187
+
188
+ const fletDestDir = path.join(targetDir, 'src', 'core', 'components');
189
+ if (!fs.existsSync(fletDestDir)) {
190
+ fs.mkdirSync(fletDestDir, { recursive: true });
191
+ }
192
+
193
+ const widgetDest = path.join(fletDestDir, 'g360_signature.py');
194
+
195
+ if (fs.existsSync(widgetDest) && !force) {
196
+ console.log(chalk.yellow('⚠️ g360-signature ya se encuentra instalado'));
197
+ console.log(chalk.gray('Usa --force para reinstalar'));
198
+ return;
199
+ }
200
+
201
+ const widgetSrc = path.join(SIGNATURE_ASSETS, 'g360_flet', 'g360_signature.py');
202
+ fs.copySync(widgetSrc, widgetDest);
203
+ console.log(chalk.gray(' Widget copiado a src/core/components/g360_signature.py'));
204
+
205
+ const initPath = path.join(fletDestDir, '__init__.py');
206
+ if (!fs.existsSync(initPath)) {
207
+ fs.writeFileSync(initPath, '', 'utf8');
208
+ }
209
+
210
+ // Insertar en main.py
211
+ const mainPyPath = path.join(targetDir, 'src', 'main.py');
212
+ if (fs.existsSync(mainPyPath)) {
213
+ let mainContent = fs.readFileSync(mainPyPath, 'utf8');
214
+
215
+ const importLine = 'from core.components.g360_signature import G360Signature, g360_footer';
216
+ const versionAttr = version ? `, version="${version}"` : '';
217
+
218
+ // Codigo de insercion segun posicion
219
+ let footerCode;
220
+ switch (position) {
221
+ case 'sidebar':
222
+ footerCode = `\n # Firma G360 en sidebar\n sidebar.controls.append(g360_footer(${versionAttr.trim() ? versionAttr : ''}))\n`;
223
+ break;
224
+ case 'header':
225
+ footerCode = `\n # Firma G360 en header\n header.controls.append(G360Signature(mode="${mode}"${versionAttr}))\n`;
226
+ break;
227
+ case 'fixed-bottom':
228
+ footerCode = `\n # Firma G360 fija abajo\n page.overlay.append(G360Signature(mode="${mode}"${versionAttr}))\n`;
229
+ break;
230
+ default: // footer
231
+ footerCode = `\n # Firma G360\n page.add(g360_footer(${versionAttr.trim() ? versionAttr : ''}))\n`;
232
+ }
233
+
234
+ if (!mainContent.includes('g360_signature')) {
235
+ mainContent = mainContent.replace(
236
+ /(from\s+core\.\w+.*\n)/,
237
+ `$1${importLine}\n`
238
+ );
239
+
240
+ if (mainContent.includes('page.add')) {
241
+ const lastAdd = mainContent.lastIndexOf('page.add');
242
+ const endOfLine = mainContent.indexOf('\n', lastAdd);
243
+ mainContent = mainContent.slice(0, endOfLine + 1) + footerCode + mainContent.slice(endOfLine + 1);
244
+ } else {
245
+ mainContent += footerCode;
246
+ }
247
+
248
+ fs.writeFileSync(mainPyPath, mainContent, 'utf8');
249
+ console.log(chalk.gray(' main.py actualizado'));
250
+ }
251
+ }
252
+ }