g360-cli 1.13.1 → 1.15.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.
Files changed (77) hide show
  1. package/package.json +71 -71
  2. package/src/assets/config/g360-skills.json +165 -0
  3. package/src/assets/config/project-types.json +23 -4
  4. package/src/assets/snippets/snippets.json +4 -0
  5. package/src/assets/templates/lit-web/src/index.js +1 -1
  6. package/src/assets/templates/python-cli/src/core/__init__.py +0 -0
  7. package/src/assets/templates/python-flet/README.md +6 -1
  8. package/src/assets/templates/python-flet/pyproject.toml +2 -2
  9. package/src/assets/templates/python-flet/src/core/skill.json +1 -1
  10. package/src/assets/templates/python-flet-migrate/pyproject.toml +1 -3
  11. package/src/assets/templates/python-flet-polished/README.md +170 -0
  12. package/src/assets/templates/python-flet-polished/assets/data/sample_data.json +12 -0
  13. package/src/assets/templates/python-flet-polished/assets/fonts/Inter-Variable.ttf +0 -0
  14. package/src/assets/templates/python-flet-polished/assets/fonts/JetBrainsMono-Variable.ttf +0 -0
  15. package/src/assets/templates/python-flet-polished/assets/images/app.ico +0 -0
  16. package/src/assets/templates/python-flet-polished/assets/images/logo.svg +9 -0
  17. package/src/assets/templates/python-flet-polished/build-portable.bat +42 -0
  18. package/src/assets/templates/python-flet-polished/create_shortcut.vbs +15 -0
  19. package/src/assets/templates/python-flet-polished/g360_flet/__init__.py +0 -0
  20. package/src/assets/templates/python-flet-polished/g360_flet/g360_signature.py +210 -0
  21. package/src/assets/templates/python-flet-polished/launch.vbs +3 -0
  22. package/src/assets/templates/python-flet-polished/launch_minimized.bat +3 -0
  23. package/src/assets/templates/python-flet-polished/main.py +47 -0
  24. package/src/assets/templates/python-flet-polished/pyproject.toml +18 -0
  25. package/src/assets/templates/python-flet-polished/requirements.txt +3 -0
  26. package/src/assets/templates/python-flet-polished/run.bat +189 -0
  27. package/src/assets/templates/python-flet-polished/skill.json +28 -0
  28. package/src/assets/templates/python-flet-polished/src/__init__.py +0 -0
  29. package/src/assets/templates/python-flet-polished/src/app.py +343 -0
  30. package/src/assets/templates/python-flet-polished/src/config/__init__.py +0 -0
  31. package/src/assets/templates/python-flet-polished/src/config/theme.py +122 -0
  32. package/src/assets/templates/python-flet-polished/src/core/__init__.py +0 -0
  33. package/src/assets/templates/python-flet-polished/src/core/constants.py +49 -0
  34. package/src/assets/templates/python-flet-polished/src/core/processor.py +128 -0
  35. package/src/assets/templates/python-flet-polished/src/core/skill.json +40 -0
  36. package/src/assets/templates/python-flet-polished/src/ui/__init__.py +0 -0
  37. package/src/assets/templates/python-flet-polished/src/ui/dashboard.py +308 -0
  38. package/src/assets/templates/python-flet-polished/src/ui/kpi_card.py +69 -0
  39. package/src/assets/templates/python-flet-polished/src/ui/search_overlay.py +143 -0
  40. package/src/assets/templates/python-flet-polished/sync_portable.py +71 -0
  41. package/src/assets/templates/solid-web/src/components/App.jsx +2 -2
  42. package/src/cli.js +4 -4
  43. package/src/commands/clean.js +0 -12
  44. package/src/commands/docs.js +105 -1
  45. package/src/commands/ingest.js +11 -61
  46. package/src/commands/init.js +34 -3
  47. package/src/commands/lint.js +35 -77
  48. package/src/commands/scan.js +9 -37
  49. package/src/commands/validate.js +6 -36
  50. package/src/lib/file-utils.js +89 -2
  51. package/src/lib/python-runner.js +77 -0
  52. package/py/src/g360_core/__pycache__/__init__.cpython-312.pyc +0 -0
  53. package/py/src/g360_core/__pycache__/__init__.cpython-314.pyc +0 -0
  54. package/py/src/g360_core/__pycache__/batch_processor.cpython-312.pyc +0 -0
  55. package/py/src/g360_core/__pycache__/batch_processor.cpython-314.pyc +0 -0
  56. package/py/src/g360_core/__pycache__/commercial_engine.cpython-314.pyc +0 -0
  57. package/py/src/g360_core/__pycache__/logger.cpython-312.pyc +0 -0
  58. package/py/src/g360_core/__pycache__/logger.cpython-314.pyc +0 -0
  59. package/py/src/g360_core/__pycache__/pipeline.cpython-312.pyc +0 -0
  60. package/py/src/g360_core/__pycache__/pipeline.cpython-314.pyc +0 -0
  61. package/py/src/g360_core/__pycache__/processor.cpython-312.pyc +0 -0
  62. package/py/src/g360_core/__pycache__/processor.cpython-314.pyc +0 -0
  63. package/py/src/g360_core/__pycache__/processor_segmentacion.cpython-312.pyc +0 -0
  64. package/py/src/g360_core/__pycache__/processor_segmentacion.cpython-314.pyc +0 -0
  65. package/py/src/g360_core/__pycache__/processor_sku.cpython-312.pyc +0 -0
  66. package/py/src/g360_core/__pycache__/processor_sku.cpython-314.pyc +0 -0
  67. package/py/src/g360_core/__pycache__/scanner.cpython-312.pyc +0 -0
  68. package/py/src/g360_core/__pycache__/scanner.cpython-314.pyc +0 -0
  69. package/py/src/g360_core/__pycache__/utils.cpython-312.pyc +0 -0
  70. package/py/src/g360_core/__pycache__/utils.cpython-314.pyc +0 -0
  71. package/src/assets/ingestion/core/__pycache__/ingestion.cpython-312.pyc +0 -0
  72. package/src/assets/signature/g360_flet/__pycache__/__init__.cpython-312.pyc +0 -0
  73. package/src/assets/signature/g360_flet/__pycache__/__init__.cpython-314.pyc +0 -0
  74. package/src/assets/signature/g360_flet/__pycache__/g360_signature.cpython-312.pyc +0 -0
  75. package/src/assets/signature/g360_flet/__pycache__/g360_signature.cpython-314.pyc +0 -0
  76. package/src/assets/templates/python-cli/package.json +0 -7
  77. /package/src/assets/config/{skills.json → agent-skills.json} +0 -0
@@ -0,0 +1,308 @@
1
+ """
2
+ G360 Dashboard - Template base con patrones pulidos.
3
+
4
+ Patrones disponibles:
5
+ - Sidebar colapsable con chips de filtro
6
+ - KPIs con glow backlight por color
7
+ - Buscador flotante con debounce
8
+ - Tablas paginadas con sort en headers
9
+ - Export a Excel con openpyxl
10
+ - Health filter por umbrales configurables
11
+ - Stale data detection
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import threading
16
+ from datetime import datetime
17
+
18
+ import flet as ft
19
+ from src.config.theme import get_colors, rgba
20
+ from src.ui.kpi_card import KPICard
21
+ from src.ui.search_overlay import SearchOverlay
22
+
23
+
24
+ class Dashboard:
25
+ def __init__(self, page: ft.Page, theme_mode: str = "dark"):
26
+ self.page = page
27
+ self._theme_mode = theme_mode
28
+ self.c = get_colors(self._theme_mode)
29
+ self._search_timer = None
30
+ self._theme_button: ft.IconButton | None = None
31
+
32
+ # State
33
+ self._raw_data: dict | None = None
34
+ self._kpis: dict | None = None
35
+ self._cache_timestamp: str | None = None
36
+ self._stale_data: bool = False
37
+
38
+ # UI refs
39
+ self._kpi_row: ft.Row | None = None
40
+ self._main_content: ft.Container | None = None
41
+ self._sidebar: ft.Container | None = None
42
+ self._status_text = ft.Text(
43
+ "Cargue datos para comenzar",
44
+ size=13, color=self.c["text_muted"], weight=ft.FontWeight.W_500,
45
+ )
46
+ self._ts_text = ft.Text("", size=11, color=self.c["text_muted"], weight=ft.FontWeight.W_500)
47
+ self._stale_badge = ft.Container(
48
+ content=ft.Row([
49
+ ft.Icon(ft.Icons.WARNING_AMBER, size=14, color=self.c["warning"]),
50
+ ft.Text("Datos en caché", size=11, color=self.c["warning"], weight=ft.FontWeight.W_500),
51
+ ], spacing=4, vertical_alignment=ft.CrossAxisAlignment.CENTER),
52
+ visible=False,
53
+ padding=ft.Padding(left=10, right=10, top=5, bottom=5),
54
+ bgcolor=rgba(self.c["warning"], 0.07),
55
+ border_radius=8,
56
+ )
57
+ self._empty_state = ft.Container(
58
+ visible=False,
59
+ content=ft.Column([
60
+ ft.Icon(ft.Icons.CLOUD_OFF, size=48, color=rgba(self.c["text_muted"], 0.35)),
61
+ ft.Text("Sin datos disponibles", size=16, color=self.c["text_muted"], weight=ft.FontWeight.W_600),
62
+ ft.Text("Verificando conexión...", size=13, color=rgba(self.c["text_muted"], 0.65)),
63
+ ], horizontal_alignment=ft.CrossAxisAlignment.CENTER, spacing=12),
64
+ )
65
+ self._loading_overlay = ft.Container(
66
+ visible=False,
67
+ content=ft.Column([
68
+ ft.ProgressRing(width=40, height=40, color=self.c["accent"]),
69
+ ft.Container(height=12),
70
+ ft.Text("Cargando...", size=14, color=self.c["text_primary"]),
71
+ ], horizontal_alignment=ft.CrossAxisAlignment.CENTER, spacing=0),
72
+ bgcolor=rgba(self.c["background"], 0.85),
73
+ expand=True,
74
+ alignment=ft.alignment.center,
75
+ )
76
+
77
+ # Callbacks
78
+ self._on_theme_toggle = None
79
+ self._on_refresh = None
80
+ self._file_picker = ft.FilePicker()
81
+
82
+ def set_on_theme_toggle(self, cb):
83
+ self._on_theme_toggle = cb
84
+
85
+ def set_on_refresh(self, cb):
86
+ self._on_refresh = cb
87
+
88
+ def register_overlay(self):
89
+ self.page.overlay.append(self._file_picker)
90
+
91
+ def build(self) -> ft.Container:
92
+ self._sidebar = self._build_sidebar()
93
+ self._main_content = self._build_main_content()
94
+
95
+ return ft.Row(
96
+ controls=[
97
+ self._sidebar,
98
+ ft.VerticalDivider(width=1, color=self.c["border"]),
99
+ self._main_content,
100
+ ],
101
+ expand=True,
102
+ )
103
+
104
+ def _build_header(self) -> ft.Container:
105
+ theme_icon = ft.Icons.DARK_MODE if self._theme_mode == "dark" else ft.Icons.LIGHT_MODE
106
+ self._theme_button = ft.IconButton(
107
+ icon=theme_icon,
108
+ icon_size=18,
109
+ tooltip="Cambiar tema",
110
+ on_click=lambda _: self._on_theme_toggle() if self._on_theme_toggle else None,
111
+ )
112
+
113
+ return ft.Container(
114
+ content=ft.Row([
115
+ ft.Text("G360 Dashboard", size=16, weight=ft.FontWeight.W_700, color=self.c["text_primary"]),
116
+ ft.Container(expand=True),
117
+ self._ts_text,
118
+ ft.Container(width=16),
119
+ self._stale_badge,
120
+ ft.Container(width=16),
121
+ self._status_text,
122
+ ft.Container(width=16),
123
+ self._theme_button,
124
+ ], vertical_alignment=ft.CrossAxisAlignment.CENTER),
125
+ padding=ft.Padding(left=20, right=20, top=12, bottom=12),
126
+ bgcolor=self.c["surface"],
127
+ border_bottom=ft.BorderSide(1, self.c["border"]),
128
+ )
129
+
130
+ def _build_kpi_row(self, kpis: dict) -> ft.Row:
131
+ kpi_keys = ["almacenes", "skus", "disponible", "predespacho", "sin_catalogo", "alertas", "criticos", "alto_stock"]
132
+ kpi_colors = self.c.get("kpis", {})
133
+
134
+ controls = []
135
+ for i, key in enumerate(kpi_keys):
136
+ data = kpis.get(key, {})
137
+ card = KPICard(
138
+ label=key.replace("_", " ").title(),
139
+ value=str(data.get("value", "-")),
140
+ color=kpi_colors.get(key, list(kpi_colors.values())[0]),
141
+ subtext=data.get("subtext", ""),
142
+ )
143
+ controls.append(card)
144
+ if i < len(kpi_keys) - 1:
145
+ controls.append(ft.Container(width=8))
146
+
147
+ return ft.Row(
148
+ controls=controls,
149
+ wrap=True,
150
+ spacing=0,
151
+ run_spacing=8,
152
+ )
153
+
154
+ def _build_sidebar(self) -> ft.Container:
155
+ search_field = ft.TextField(
156
+ hint_text="Buscar SKU, descripción...",
157
+ border_radius=8,
158
+ height=36,
159
+ text_size=12,
160
+ dense=True,
161
+ border=ft.InputBorder.OUTLINE,
162
+ border_color=self.c["border"],
163
+ focused_border_color=self.c["accent"],
164
+ cursor_color=self.c["accent"],
165
+ hint_style=ft.TextStyle(size=11, color=self.c["text_muted"]),
166
+ suffix_icon=ft.Icon(ft.Icons.KEYBOARD_RETURN, size=14, color=rgba(self.c["text_muted"], 0.5)),
167
+ )
168
+
169
+ return ft.Container(
170
+ content=ft.Column([
171
+ ft.Container(height=16),
172
+ ft.Text("Filtros", size=12, weight=ft.FontWeight.W_600, color=self.c["text_muted"]),
173
+ ft.Container(height=8),
174
+ search_field,
175
+ ft.Container(height=16),
176
+ ft.Divider(color=self.c["border"]),
177
+ ft.Container(height=8),
178
+ ft.Text("Almacenes", size=12, weight=ft.FontWeight.W_600, color=self.c["text_muted"]),
179
+ ft.Container(height=8),
180
+ ft.Column([], scroll=ft.ScrollMode.AUTO, expand=True, id="sidebar_alms"),
181
+ ], spacing=0, expand=True),
182
+ width=220,
183
+ bgcolor=self.c["surface"],
184
+ padding=ft.Padding(left=12, right=12, top=16, bottom=12),
185
+ )
186
+
187
+ def _build_main_content(self) -> ft.Container:
188
+ return ft.Container(
189
+ content=ft.Column([
190
+ self._build_header(),
191
+ ft.Container(content=self._kpi_row if self._kpi_row else self._build_kpi_row({}), expand=False),
192
+ ft.Container(height=12),
193
+ ft.Container(
194
+ content=ft.Column([], scroll=ft.ScrollMode.AUTO, expand=True, id="main_cards"),
195
+ expand=True,
196
+ ),
197
+ self._empty_state,
198
+ self._loading_overlay,
199
+ ], spacing=0, expand=True),
200
+ expand=True,
201
+ bgcolor=self.c["background"],
202
+ padding=ft.Padding(left=16, right=16, top=16, bottom=16),
203
+ )
204
+
205
+ # --- Update methods ---
206
+
207
+ def update_data(self, raw_data: dict, cache_timestamp: str | None = None, api_timestamp: str | None = None, stale: bool = False):
208
+ self._raw_data = raw_data
209
+ self._cache_timestamp = cache_timestamp
210
+ self._stale_data = stale
211
+ # Subclases implementan la logica de renderizado
212
+ self._render()
213
+
214
+ def _render(self):
215
+ # Implementar en subclase segun el dominio
216
+ pass
217
+
218
+ def update_theme(self, mode: str):
219
+ self._theme_mode = mode
220
+ self.c = get_colors(mode)
221
+ # Reconstruir UI con nuevos colores
222
+ self.page.bgcolor = self.c["background"]
223
+ self._rebuild_ui()
224
+
225
+ def _rebuild_ui(self):
226
+ if self._main_content:
227
+ self._main_content.content = self._build_main_content().content
228
+ self.page.update()
229
+
230
+ def set_loading(self, loading: bool, message: str = "Cargando..."):
231
+ self._loading_overlay.visible = loading
232
+ if loading:
233
+ self._loading_overlay.content.controls[2].value = message
234
+ self.page.update()
235
+
236
+ def set_offline(self, offline: bool):
237
+ if offline:
238
+ self._status_text.value = "Sin conexión"
239
+ self._status_text.color = self.c["error"]
240
+ else:
241
+ self._status_text.value = "Conectado"
242
+ self._status_text.color = self.c["success"]
243
+ self.page.update()
244
+
245
+ def _show_snack(self, msg: str, is_error: bool = False):
246
+ self.page.snack_bar = ft.SnackBar(
247
+ content=ft.Text(msg, size=13),
248
+ bgcolor=self.c["error"] if is_error else self.c["accent"],
249
+ duration=3000,
250
+ )
251
+ self.page.open(self.page.snack_bar)
252
+
253
+ def _show_empty_state(self, title: str, subtitle: str = ""):
254
+ self._empty_state.content.controls[1].value = title
255
+ if subtitle:
256
+ if len(self._empty_state.content.controls) > 2:
257
+ self._empty_state.content.controls[2].value = subtitle
258
+ else:
259
+ self._empty_state.content.controls.append(
260
+ ft.Text(subtitle, size=13, color=rgba(self.c["text_muted"], 0.65))
261
+ )
262
+ self._empty_state.visible = True
263
+ self.page.update()
264
+
265
+ def _hide_empty_state(self):
266
+ self._empty_state.visible = False
267
+ self.page.update()
268
+
269
+ def _set_empty_state_status(self, msg: str, color: str):
270
+ for ctrl in self._empty_state.content.controls:
271
+ if isinstance(ctrl, ft.Text) and ctrl.size == 13:
272
+ ctrl.value = msg
273
+ ctrl.color = color
274
+ break
275
+ self.page.update()
276
+
277
+ def _update_refresh_status(self, cache_ts: str | None):
278
+ if cache_ts:
279
+ try:
280
+ dt = datetime.fromisoformat(cache_ts)
281
+ age_min = (datetime.now() - dt).total_seconds() / 60
282
+ self._ts_text.value = f"Hace {int(age_min)} min"
283
+ except Exception:
284
+ self._ts_text.value = ""
285
+ else:
286
+ self._ts_text.value = ""
287
+
288
+ def format_cache_timestamp(self, ts: str | None) -> str:
289
+ if not ts:
290
+ return ""
291
+ try:
292
+ return datetime.fromisoformat(ts).strftime("%H:%M:%S")
293
+ except Exception:
294
+ return ts[:19]
295
+
296
+ def _show_stale_warning(self):
297
+ self._stale_badge.visible = True
298
+ self.page.update()
299
+
300
+ def _hide_stale_warning(self):
301
+ self._stale_badge.visible = False
302
+ self.page.update()
303
+
304
+ def show_search_overlay(self):
305
+ """Abre el buscador flotante."""
306
+ overlay = SearchOverlay(self.page, self.c)
307
+ self.page.overlay.append(overlay)
308
+ self.page.open(overlay)
@@ -0,0 +1,69 @@
1
+ """
2
+ G360 KPI Card - Componente reutilizable para indicadores clave.
3
+
4
+ Patron: Card con glow backlight por color, valor numerico grande,
5
+ subtexto opcional. Clickable para abrir dialogos detallados.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import flet as ft
10
+ from src.config.theme import rgba
11
+
12
+
13
+ class KPICard(ft.Container):
14
+ def __init__(
15
+ self,
16
+ label: str,
17
+ value: str,
18
+ color: str,
19
+ subtext: str = "",
20
+ on_click=None,
21
+ ):
22
+ super().__init__()
23
+ self._label = label
24
+ self._value = value
25
+ self._color = color
26
+ self._subtext = subtext
27
+ self._on_click = on_click
28
+
29
+ self.content = ft.Column(
30
+ controls=[
31
+ ft.Text(label, size=11, color=rgba(color, 0.8), weight=ft.FontWeight.W_500),
32
+ ft.Container(height=4),
33
+ ft.Text(
34
+ value,
35
+ size=22,
36
+ color=color,
37
+ weight=ft.FontWeight.W_700,
38
+ font_family="JetBrains Mono",
39
+ ),
40
+ ft.Container(height=2),
41
+ ft.Text(subtext, size=10, color=rgba(color, 0.6)),
42
+ ],
43
+ spacing=0,
44
+ horizontal_alignment=ft.CrossAxisAlignment.START,
45
+ )
46
+ self.padding = ft.Padding(left=14, right=14, top=10, bottom=10)
47
+ self.bgcolor = rgba(color, 0.08)
48
+ self.border_radius = 10
49
+ self.shadow = ft.BoxShadow(
50
+ spread_radius=0.5,
51
+ blur_radius=12,
52
+ color=rgba(color, 0.15),
53
+ offset=ft.Offset(0, 2),
54
+ )
55
+ self.border = ft.Border(bottom=ft.BorderSide(2, rgba(color, 0.4)))
56
+ self.cursor_type = ft.CursorType.CLICK if on_click else ft.CursorType.NONE
57
+
58
+ if on_click:
59
+ self.on_click = on_click
60
+ else:
61
+ self.on_click = None
62
+
63
+ def update_value(self, value: str, subtext: str = ""):
64
+ self._value = value
65
+ self._subtext = subtext
66
+ self.content.controls[2].value = value
67
+ if len(self.content.controls) > 3:
68
+ self.content.controls[3].value = subtext
69
+ self.update()
@@ -0,0 +1,143 @@
1
+ """
2
+ G360 Search Overlay - Buscador flotante con debounce.
3
+
4
+ Busqueda contextual en tiempo real con resultados en vivo.
5
+ Enter abre el primer resultado, Escape o click fuera cierra.
6
+ Auto-limpieza al enfocar con texto previo.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+
12
+ import flet as ft
13
+ from src.config.theme import rgba
14
+
15
+
16
+ class SearchOverlay(ft.Container):
17
+ def __init__(self, page: ft.Page, colors: dict):
18
+ super().__init__()
19
+ self.page = page
20
+ self.c = colors
21
+ self._results: list[dict] = []
22
+ self._debounce_task = None
23
+ self._selected_index = 0
24
+
25
+ self.field = ft.TextField(
26
+ hint_text="Buscar SKU, descripción, línea...",
27
+ border_radius=10,
28
+ height=44,
29
+ text_size=13,
30
+ dense=True,
31
+ border=ft.InputBorder.OUTLINE,
32
+ border_color=self.c["border"],
33
+ focused_border_color=self.c["accent"],
34
+ cursor_color=self.c["accent"],
35
+ hint_style=ft.TextStyle(size=12, color=self.c["text_muted"]),
36
+ autofocus=True,
37
+ on_change=self._on_change,
38
+ on_submit=self._on_submit,
39
+ on_focus=self._on_focus,
40
+ )
41
+
42
+ self.content = ft.Column(
43
+ controls=[
44
+ self.field,
45
+ ft.Container(
46
+ content=ft.Column([], scroll=ft.ScrollMode.AUTO, max_height=300, id="search_results"),
47
+ bgcolor=self.c["surface"],
48
+ border_radius=ft.BorderRadius.only(bottom_left=10, bottom_right=10),
49
+ border=ft.border.Top(border_color=self.c["border"], border_style=ft.BorderSide(1)),
50
+ visible=False,
51
+ padding=ft.Padding(left=0, right=0, top=0, bottom=0),
52
+ max_height=300,
53
+ height=300,
54
+ ),
55
+ ],
56
+ spacing=0,
57
+ )
58
+
59
+ self.bgcolor = self.c["surface"]
60
+ self.border_radius = 10
61
+ self.padding = ft.Padding(left=16, right=16, top=12, bottom=12)
62
+ self.width = 480
63
+ self.alignment = ft.alignment.top_center
64
+ self.margin = ft.margin.only(top=60)
65
+
66
+ def _on_focus(self, e):
67
+ if self.field.value:
68
+ self.field.value = ""
69
+ self.field.update()
70
+
71
+ async def _on_change(self, e):
72
+ query = (e.control.value or "").strip().lower()
73
+ if self._debounce_task:
74
+ self._debounce_task.cancel()
75
+ if len(query) < 2:
76
+ self._hide_results()
77
+ return
78
+ self._debounce_task = asyncio.create_task(self._debounce_search(query))
79
+
80
+ async def _debounce_search(self, query: str):
81
+ await asyncio.sleep(0.25)
82
+ self._results = await self._do_search(query)
83
+ self._show_results(self._results)
84
+
85
+ async def _do_search(self, query: str) -> list[dict]:
86
+ """Implementar en subclase. Retorna lista de dicts {sku, desc, ...}."""
87
+ return []
88
+
89
+ def _show_results(self, results: list[dict]):
90
+ if not results:
91
+ self._hide_results()
92
+ return
93
+ controls = []
94
+ for i, item in enumerate(results[:10]):
95
+ is_selected = i == self._selected_index
96
+ controls.append(
97
+ ft.Container(
98
+ content=ft.Row([
99
+ ft.Text(item.get("sku", ""), size=12, color=self.c["accent"], weight=ft.FontWeight.W_600),
100
+ ft.Container(width=8),
101
+ ft.Text(item.get("desc", item.get("descripcion", "")), size=12, color=self.c["text_primary"]),
102
+ ], spacing=0),
103
+ bgcolor=rgba(self.c["accent"], 0.1) if is_selected else "transparent",
104
+ border_radius=6,
105
+ padding=ft.Padding(left=12, right=12, top=8, bottom=8),
106
+ on_click=lambda _, r=item: self._on_select(r),
107
+ mouse_cursor=ft.CursorType.CLICK,
108
+ )
109
+ )
110
+ results_col = self.content.controls[1]
111
+ results_col.content.controls = controls
112
+ results_col.visible = True
113
+ self.update()
114
+
115
+ def _hide_results(self):
116
+ results_col = self.content.controls[1]
117
+ results_col.content.controls = []
118
+ results_col.visible = False
119
+ self.update()
120
+
121
+ def _on_submit(self, e):
122
+ if self._results:
123
+ self._on_select(self._results[self._selected_index])
124
+
125
+ def _on_select(self, item: dict):
126
+ self._hide_results()
127
+ self.page.close(self)
128
+
129
+ def did_mount(self):
130
+ """Captura Escape para cerrar."""
131
+ def on_keyboard(e: ft.KeyboardEvent):
132
+ if e.key == "Escape":
133
+ self.page.close(self)
134
+ elif e.key == "ArrowDown":
135
+ self._selected_index = min(self._selected_index + 1, len(self._results) - 1)
136
+ self._show_results(self._results)
137
+ elif e.key == "ArrowUp":
138
+ self._selected_index = max(self._selected_index - 1, 0)
139
+ self._show_results(self._results)
140
+ elif e.key == "Enter" and self._results:
141
+ self._on_select(self._results[self._selected_index])
142
+
143
+ self.page.on_keyboard_event = on_keyboard
@@ -0,0 +1,71 @@
1
+ import os
2
+ import shutil
3
+ from pathlib import Path
4
+
5
+
6
+ def sync_dir(src, dst):
7
+ if not os.path.exists(src):
8
+ return
9
+ if not os.path.exists(dst):
10
+ os.makedirs(dst)
11
+ for item in os.listdir(dst):
12
+ dst_item = os.path.join(dst, item)
13
+ src_item = os.path.join(src, item)
14
+ if not os.path.exists(src_item):
15
+ if os.path.isdir(dst_item):
16
+ shutil.rmtree(dst_item)
17
+ print(f"Eliminado directorio obsoleto: {dst_item}")
18
+ else:
19
+ os.remove(dst_item)
20
+ print(f"Eliminado archivo obsoleto: {dst_item}")
21
+ for item in os.listdir(src):
22
+ src_item = os.path.join(src, item)
23
+ dst_item = os.path.join(dst, item)
24
+ if os.path.isdir(src_item):
25
+ if item == "__pycache__":
26
+ continue
27
+ sync_dir(src_item, dst_item)
28
+ else:
29
+ if (not os.path.exists(dst_item) or
30
+ os.stat(src_item).st_mtime != os.stat(dst_item).st_mtime or
31
+ os.stat(src_item).st_size != os.stat(dst_item).st_size):
32
+ shutil.copy2(src_item, dst_item)
33
+ print(f"Sincronizado: {src_item} -> {dst_item}")
34
+
35
+
36
+ def main():
37
+ base_dir = os.path.dirname(os.path.abspath(__file__))
38
+ portable_dir = os.path.join(base_dir, "g360-app-polished-portable")
39
+
40
+ if not os.path.exists(portable_dir):
41
+ print(f"Error: No se encontro la carpeta portable '{portable_dir}'")
42
+ return
43
+
44
+ print("Iniciando sincronizacion con la version portable...")
45
+
46
+ files_to_sync = [
47
+ "main.py",
48
+ "run.bat", "launch.vbs", "launch_minimized.bat", "create_shortcut.vbs",
49
+ "build-portable.bat", "README.md",
50
+ ]
51
+ for filename in files_to_sync:
52
+ src_file = os.path.join(base_dir, filename)
53
+ dst_file = os.path.join(portable_dir, filename)
54
+ if os.path.exists(src_file):
55
+ shutil.copy2(src_file, dst_file)
56
+ print(f"Copiado: {filename} -> portable/{filename}")
57
+ else:
58
+ print(f"Omitido (no existe en origen): {filename}")
59
+
60
+ sync_dir(os.path.join(base_dir, "src"), os.path.join(portable_dir, "src"))
61
+ sync_dir(os.path.join(base_dir, "assets"), os.path.join(portable_dir, "assets"))
62
+ sync_dir(os.path.join(base_dir, "g360_flet"), os.path.join(portable_dir, "g360_flet"))
63
+
64
+ for pyc in Path(portable_dir).rglob("__pycache__"):
65
+ shutil.rmtree(pyc)
66
+
67
+ print("\nSincronizacion finalizada con exito!")
68
+
69
+
70
+ if __name__ == "__main__":
71
+ main()
@@ -5,7 +5,7 @@
5
5
  * @version 1.0.0
6
6
  */
7
7
 
8
- export default function App() {
8
+ export default function App(props) {
9
9
  const titulo = 'Mi Proyecto G360';
10
10
 
11
11
  return (
@@ -14,7 +14,7 @@ export default function App() {
14
14
  <h1>{titulo}</h1>
15
15
  </header>
16
16
  <main>
17
- <slot />
17
+ {props.children}
18
18
  </main>
19
19
  </div>
20
20
  );
package/src/cli.js CHANGED
@@ -33,7 +33,7 @@ function configAction(options) {
33
33
  console.log(chalk.gray(' positions: bottom-right, bottom-left, bottom-center, footer-right, footer-left'));
34
34
  console.log(chalk.white(' templates:'));
35
35
  console.log(chalk.gray(' web-pwa, lit-web, solid-web, svelte-web,'));
36
- console.log(chalk.gray(' python-cli, python-flet, python-flet-migrate, python-customtkinter, vba-excel'));
36
+ console.log(chalk.gray(' python-cli, python-flet-polished (default), python-flet-legacy, python-flet-migrate, python-customtkinter, vba-excel'));
37
37
  console.log(chalk.white(' skills:'));
38
38
  console.log(chalk.gray(' corporativo, corporativo-movil, corporativo-g360, corporativo-g360-movil,'));
39
39
  console.log(chalk.gray(' moderno, moderno-movil, minimalista, custom,'));
@@ -72,8 +72,8 @@ program
72
72
  program
73
73
  .command('init')
74
74
  .argument('<name>', 'Project name')
75
- .option('-t, --template <type>', 'Project template type', 'web-pwa')
76
- .option('-s, --skill <skill>', 'Skill to use (corporativo, corporativo-movil, moderno, moderno-movil, minimalista, custom, flet-desktop, flet-desktop-corporativo, cipsa, cipsa-movil)', 'corporativo-movil')
75
+ .option('-t, --template <type>', 'Project template type (default: auto segun skill — python-flet-polished para Flet)', 'auto')
76
+ .option('-s, --skill <skill>', 'Skill to use (corporativo, corporativo-movil, moderno, moderno-movil, minimalista, custom, flet-desktop, flet-desktop-corporativo, flet-desktop-polished, cipsa, cipsa-movil)', 'corporativo-movil')
77
77
  .option('-d, --dir <path>', 'Target directory', '.')
78
78
  .option('--dry-run', 'Preview without creating files')
79
79
  .option('--force', 'Overwrite existing files')
@@ -143,7 +143,7 @@ program
143
143
  program
144
144
  .command('convert')
145
145
  .argument('[path]', 'Path to convert', '.')
146
- .option('-s, --skill <skill>', 'Skill to apply (corporativo, corporativo-movil, moderno, moderno-movil, minimalista, custom, flet-desktop, flet-desktop-corporativo, cipsa, cipsa-movil)', 'corporativo-movil')
146
+ .option('-s, --skill <skill>', 'Skill to apply (corporativo, corporativo-movil, moderno, moderno-movil, minimalista, custom, flet-desktop, flet-desktop-corporativo, flet-desktop-polished, cipsa, cipsa-movil)', 'corporativo-movil')
147
147
  .option('--dry-run', 'Preview without applying')
148
148
  .option('--restructure', 'Restructure project files')
149
149
  .option('--force', 'Force dangerous changes')
@@ -7,11 +7,8 @@
7
7
  import chalk from 'chalk';
8
8
  import fs from 'fs-extra';
9
9
  import path from 'path';
10
- import { fileURLToPath } from 'url';
11
10
  import { getAllFiles } from '../lib/file-utils.js';
12
11
 
13
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
-
15
12
  const DEAD_PATTERNS = [
16
13
  'backup', 'backup-old', 'backups',
17
14
  'temp', 'tmp', '.tmp',
@@ -292,15 +289,6 @@ async function findOrphanFiles(projectDir, allFiles) {
292
289
 
293
290
  function analyzeFileOrganization(files) {
294
291
  const misplaced = [];
295
- const structureRules = {
296
- 'src/components': ['.js', '.jsx', '.ts', '.tsx', '.svelte', '.vue'],
297
- 'src/pages': ['.js', '.jsx', '.ts', '.tsx', '.svelte', '.vue'],
298
- 'src/utils': ['.js', '.ts'],
299
- 'src/hooks': ['.js', '.ts'],
300
- 'src/services': ['.js', '.ts'],
301
- 'src/styles': ['.css', '.scss', '.less'],
302
- 'src/assets': ['.png', '.jpg', '.svg', '.gif', '.webp']
303
- };
304
292
 
305
293
  const components = files.filter(f => {
306
294
  const name = path.basename(f);