@maccesar/aiskills 1.12.0 → 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 (59) hide show
  1. package/README.md +42 -7
  2. package/lib/cleanup.js +29 -0
  3. package/lib/commands/skills.js +110 -9
  4. package/lib/config.js +17 -9
  5. package/lib/installer.js +5 -3
  6. package/lib/symlink.js +45 -3
  7. package/lib/utils.js +41 -0
  8. package/package.json +1 -1
  9. package/skills/audit-codebase/SKILL.md +70 -0
  10. package/skills/audit-codebase/agents/openai.yaml +4 -0
  11. package/skills/audit-codebase/references/comprehensive-audit.md +220 -0
  12. package/skills/audit-codebase/references/report-format.md +119 -0
  13. package/skills/humaniza/SKILL.md +55 -4
  14. package/skills/humaniza/references/ai-patterns-es.md +40 -0
  15. package/skills/humaniza/references/checklist.md +9 -0
  16. package/skills/humaniza/references/examples.md +16 -0
  17. package/skills/humaniza/references/lexicon-es-mx.md +18 -0
  18. package/skills/humaniza/references/structures-es.md +132 -0
  19. package/skills/humaniza/scripts/check_ai_patterns.py +216 -0
  20. package/skills/refactoring-ui/SKILL.md +65 -29
  21. package/skills/refactoring-ui/references/05-motion.md +124 -0
  22. package/skills/refactoring-ui/references/06-dark-mode.md +117 -0
  23. package/skills/refactoring-ui/references/07-component-patterns.md +181 -0
  24. package/skills/stitch-showcase/SKILL.md +24 -232
  25. package/skills/stitch-showcase/references/07-theme-system.md +12 -0
  26. package/skills/stitch-showcase/references/08-type-detection.md +9 -1
  27. package/skills/stitch-showcase/references/10-component-standardization.md +25 -0
  28. package/skills/stitch-showcase/references/12-video-embedding.md +113 -0
  29. package/skills/stitch-showcase/references/13-language-detection.md +82 -0
  30. package/skills/stitch-showcase/references/14-troubleshooting-known-issues.md +122 -0
  31. package/skills/stitch-showcase/references/15-build-flags.md +71 -0
  32. package/skills/stitch-showcase/references/16-design-md-format.md +107 -0
  33. package/skills/stitch-showcase/references/index.html +25 -19
  34. package/skills/stitch-showcase/references/viewer.html +24 -12
  35. package/skills/stitch-showcase/scripts/__pycache__/build_showcase.cpython-314.pyc +0 -0
  36. package/skills/stitch-showcase/scripts/__pycache__/component_utils.cpython-314.pyc +0 -0
  37. package/skills/stitch-showcase/scripts/__pycache__/detect_components.cpython-314.pyc +0 -0
  38. package/skills/stitch-showcase/scripts/__pycache__/extract_catalog.cpython-314.pyc +0 -0
  39. package/skills/stitch-showcase/scripts/__pycache__/extract_text.cpython-314.pyc +0 -0
  40. package/skills/stitch-showcase/scripts/__pycache__/extract_zips.cpython-314.pyc +0 -0
  41. package/skills/stitch-showcase/scripts/__pycache__/parse_design_md.cpython-314.pyc +0 -0
  42. package/skills/stitch-showcase/scripts/__pycache__/slug_demangle.cpython-314.pyc +0 -0
  43. package/skills/stitch-showcase/scripts/build_showcase.py +150 -10
  44. package/skills/stitch-showcase/scripts/parse_design_md.py +145 -12
  45. package/skills/stitch-showcase/scripts/slug_demangle.py +209 -0
  46. package/skills/vscode-extension-dev/SKILL.md +90 -41
  47. package/skills/vscode-extension-dev/references/api-additional.md +168 -0
  48. package/skills/vscode-extension-dev/references/api-progress.md +55 -0
  49. package/skills/vscode-extension-dev/references/api-quickpick.md +75 -0
  50. package/skills/vscode-extension-dev/references/api-secretstorage.md +57 -0
  51. package/skills/vscode-extension-dev/references/api-statusbar.md +38 -0
  52. package/skills/vscode-extension-dev/references/api-treeview.md +78 -0
  53. package/skills/vscode-extension-dev/references/api-webview.md +149 -0
  54. package/skills/vscode-extension-dev/references/architecture.md +67 -0
  55. package/skills/vscode-extension-dev/references/debugger.md +179 -0
  56. package/skills/vscode-extension-dev/references/lsp.md +175 -0
  57. package/skills/vscode-extension-dev/references/notebooks.md +208 -0
  58. package/skills/vscode-extension-dev/references/testing.md +208 -0
  59. package/skills/vscode-extension-dev/references/api-patterns.md +0 -625
@@ -1,5 +1,5 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en">
2
+ <html lang="{{HTML_LANG}}">
3
3
 
4
4
  <head>
5
5
  <meta charset="UTF-8">
@@ -82,6 +82,18 @@
82
82
  background: #fff;
83
83
  }
84
84
  html.dark #viewer-frame { background: #111; }
85
+
86
+ /* ── Real mobile: hide simulator chrome on small viewports ── */
87
+ @media (max-width: 639px) {
88
+ .view-mobile #mobile-frame { padding-top: 0 !important; align-items: stretch !important; }
89
+ .view-mobile #phone-container { transform: none !important; width: 100% !important; height: 100% !important; }
90
+ .view-mobile #phone-wrap {
91
+ width: 100% !important; height: 100% !important;
92
+ border: none !important; border-radius: 0 !important; box-shadow: none !important;
93
+ }
94
+ .view-web #browser-chrome { display: none !important; }
95
+ .view-web #iframe-wrap-web { border: none !important; border-radius: 0 !important; }
96
+ }
85
97
  </style>
86
98
  </head>
87
99
 
@@ -93,20 +105,20 @@
93
105
 
94
106
  <!-- ── Header ──────────────────────────────────────────── -->
95
107
  <header id="main-header" class="flex-shrink-0 border-b border-gray-200/60 backdrop-blur-xl bg-white/80 dark:border-white/10 dark:bg-[#0a0a0a]/80" style="height:56px">
96
- <div class="mx-auto flex h-full max-w-[1440px] items-center gap-3 px-5">
108
+ <div class="mx-auto flex h-full max-w-[1440px] items-center gap-2 px-3 sm:gap-3 sm:px-5">
97
109
 
98
110
  <!-- Back button -->
99
- <a id="btn-back" href="index.html" title="Back to gallery (Esc)" class="flex shrink-0 items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[13px] text-gray-500 no-underline transition-colors hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-white/10 dark:hover:text-gray-100">
111
+ <a id="btn-back" href="index.html" title="Volver a la galería (Esc)" aria-label="Volver" class="flex shrink-0 items-center gap-1.5 rounded-lg px-2 py-1.5 text-[13px] text-gray-500 no-underline transition-colors hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-white/10 dark:hover:text-gray-100 sm:px-2.5">
100
112
  <svg class="h-3.5 w-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
101
113
  <path d="M10 13L5 8l5-5" />
102
114
  </svg>
103
- Back
115
+ <span class="hidden sm:inline">Volver</span>
104
116
  </a>
105
117
 
106
- <div class="h-4 w-px shrink-0 bg-gray-200 dark:bg-white/10"></div>
118
+ <div class="hidden sm:block h-4 w-px shrink-0 bg-gray-200 dark:bg-white/10"></div>
107
119
 
108
120
  <!-- Screen title -->
109
- <span id="screen-title" class="shrink-0 text-[13px] font-semibold leading-none">--</span>
121
+ <span id="screen-title" class="min-w-0 truncate text-[13px] font-semibold leading-none">--</span>
110
122
 
111
123
  <!-- Screen description (inline after title) -->
112
124
  <span id="screen-desc" class="hidden text-[11px] text-gray-400 dark:text-gray-500 truncate sm:inline"></span>
@@ -115,20 +127,20 @@
115
127
  <div class="flex-1"></div>
116
128
 
117
129
  <!-- Position counter -->
118
- <span id="nav-pos" class="shrink-0 tabular-nums text-[11px] text-gray-400 dark:text-gray-500">-- / --</span>
130
+ <span id="nav-pos" class="hidden sm:inline shrink-0 tabular-nums text-[11px] text-gray-400 dark:text-gray-500">-- / --</span>
119
131
 
120
132
  <!-- Prev button -->
121
- <button id="btn-prev" title="Previous screen (ArrowLeft)" class="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg text-gray-500 transition-colors hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-white/10 disabled:cursor-not-allowed disabled:opacity-30" disabled>
133
+ <button id="btn-prev" title="Pantalla anterior ()" class="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg text-gray-500 transition-colors hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-white/10 disabled:cursor-not-allowed disabled:opacity-30" disabled>
122
134
  <svg class="h-3.5 w-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M10 13L5 8l5-5"/></svg>
123
135
  </button>
124
136
 
125
137
  <!-- Next button -->
126
- <button id="btn-next" title="Next screen (ArrowRight)" class="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg text-gray-500 transition-colors hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-white/10 disabled:cursor-not-allowed disabled:opacity-30" disabled>
138
+ <button id="btn-next" title="Pantalla siguiente ()" class="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg text-gray-500 transition-colors hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-white/10 disabled:cursor-not-allowed disabled:opacity-30" disabled>
127
139
  <svg class="h-3.5 w-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 3l5 5-5 5"/></svg>
128
140
  </button>
129
141
 
130
142
  <!-- Theme toggle -->
131
- <button id="theme-toggle" title="Toggle light/dark" class="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg text-gray-500 transition-colors hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-white/10">
143
+ <button id="theme-toggle" title="Cambiar claro/oscuro" class="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg text-gray-500 transition-colors hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-white/10">
132
144
  <svg class="block h-3.5 w-3.5 dark:hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
133
145
  <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
134
146
  </svg>
@@ -146,7 +158,7 @@
146
158
  </button>
147
159
 
148
160
  <!-- View mode toggle -->
149
- <button id="view-mode-toggle" title="Toggle mobile/web view" class="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg text-gray-500 transition-colors hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-white/10">
161
+ <button id="view-mode-toggle" title="Cambiar vista móvil/web" class="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg text-gray-500 transition-colors hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-white/10">
150
162
  <svg id="icon-phone" class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
151
163
  <rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/>
152
164
  </svg>
@@ -156,7 +168,7 @@
156
168
  </button>
157
169
 
158
170
  <!-- Fullscreen button -->
159
- <button id="btn-fullscreen" title="Fullscreen (F)" class="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg text-gray-500 transition-colors hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-white/10">
171
+ <button id="btn-fullscreen" title="Pantalla completa (F)" class="hidden sm:flex h-7 w-7 shrink-0 items-center justify-center rounded-lg text-gray-500 transition-colors hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-white/10">
160
172
  <svg id="icon-expand" class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
161
173
  <path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
162
174
  </svg>
@@ -40,6 +40,7 @@ import parse_design_md
40
40
  import extract_text
41
41
  import detect_components
42
42
  import extract_catalog
43
+ from slug_demangle import slug_to_title
43
44
 
44
45
 
45
46
  def _extract_source_zip(zip_path: Path) -> Path:
@@ -150,6 +151,9 @@ def build(source_dir: str, project_type: str = None, project_name: str = None) -
150
151
  # Generate catalog.html (always included)
151
152
  _generate_catalog(output_dir, assets_dir, metadata)
152
153
 
154
+ # Sanity-check the generated output — warnings only, never aborts.
155
+ _validate_showcase(output_dir, screens)
156
+
153
157
  print(f"\n🎉 Showcase ready: {output_dir}/index.html\n")
154
158
  return output_dir
155
159
 
@@ -344,6 +348,11 @@ def _prepare_source(source_dir: str, project_type: str, project_name: str) -> tu
344
348
  # Defer type detection — will resolve after screen extraction via _detect_type_from_screens()
345
349
  metadata["_type_pending"] = True
346
350
 
351
+ # Language: showcase.json wins over DESIGN.md if both present, else fall through.
352
+ if config.get("lang"):
353
+ metadata["lang"] = str(config["lang"])
354
+ metadata.setdefault("lang", "en")
355
+
347
356
  ptype = metadata.get("type", "unknown")
348
357
  if ptype != "unknown":
349
358
  print(f"📱 Type: {ptype} | Project: {metadata['project_name']}")
@@ -518,16 +527,21 @@ def _enrich_screens(screens_raw: list, metadata: dict, source: Path) -> list:
518
527
  for raw in screens_raw:
519
528
  slug = raw["slug"]
520
529
  design = design_screens.get(slug, {})
530
+ html_path = raw.get("html_path", "")
521
531
 
522
- # Title
523
- title = design.get("title") or _slug_to_title(slug)
532
+ # Title resolution order: explicit DESIGN.md → <title> HTML → demangled slug
533
+ title = (
534
+ design.get("title")
535
+ or _read_html_title_tag(html_path)
536
+ or slug_to_title(slug)
537
+ )
524
538
 
525
539
  # Description priority: DESIGN.md → individual .md → meta/h1/title/visible body text → ""
526
540
  desc = design.get("description") or ""
527
541
  if not desc:
528
542
  desc = _read_md_desc(slug, source)
529
543
  if not desc:
530
- desc = _read_html_title(raw.get("html_path", ""))
544
+ desc = _read_html_title(html_path)
531
545
 
532
546
  enriched.append({
533
547
  "slug": slug,
@@ -540,6 +554,50 @@ def _enrich_screens(screens_raw: list, metadata: dict, source: Path) -> list:
540
554
  return enriched
541
555
 
542
556
 
557
+ def _read_html_title_tag(html_path: str) -> str:
558
+ """
559
+ Read the <title> tag of an HTML file and return a usable screen title.
560
+
561
+ Skips generic placeholders (Untitled, index, etc.). Strips a leading
562
+ "Brand - " or "Brand | " prefix when the title contains a separator,
563
+ so "Lumiere Dining - Rústico & Artesanal" becomes "Rústico & Artesanal".
564
+ Decodes basic HTML entities (&amp;, &nbsp;…).
565
+ """
566
+ if not html_path:
567
+ return ""
568
+ try:
569
+ text = Path(html_path).read_text(encoding="utf-8", errors="ignore")
570
+ except OSError:
571
+ return ""
572
+
573
+ m = re.search(r"<title[^>]*>(.+?)</title>", text, re.IGNORECASE | re.DOTALL)
574
+ if not m:
575
+ return ""
576
+
577
+ val = re.sub(r"<[^>]+>", "", m.group(1)).strip()
578
+ for entity, char in [
579
+ ("&amp;", "&"), ("&lt;", "<"), ("&gt;", ">"),
580
+ ("&nbsp;", " "), ("&#39;", "'"), ("&quot;", '"'),
581
+ ]:
582
+ val = val.replace(entity, char)
583
+ val = re.sub(r"\s+", " ", val).strip()
584
+
585
+ if not val:
586
+ return ""
587
+ if re.match(r"^(untitled|index|screen|page|document|\d+)$", val, re.IGNORECASE):
588
+ return ""
589
+
590
+ # Strip a "Brand - " / "Brand | " / "Brand: " prefix and return the last segment.
591
+ for sep in (" - ", " — ", " | ", ": "):
592
+ if sep in val:
593
+ tail = val.split(sep)[-1].strip()
594
+ if tail:
595
+ val = tail
596
+ break
597
+
598
+ return val
599
+
600
+
543
601
  def _read_md_desc(slug: str, source: Path) -> str:
544
602
  """Look for an individual .md file with the screen description."""
545
603
  for md_file in source.glob("*.md"):
@@ -718,6 +776,23 @@ def _generate_viewer(output_dir: Path, ptype: str, metadata: dict, screens: list
718
776
 
719
777
  template = template_path.read_text(encoding="utf-8")
720
778
 
779
+ # Reorder screens to follow DESIGN.md section order (mirrors _build_sections_html)
780
+ # so prev/next navigation matches the visual order in index.html.
781
+ sections = metadata.get("sections", [])
782
+ if sections:
783
+ slug_to_screen = {s["slug"]: s for s in screens}
784
+ ordered = []
785
+ seen = set()
786
+ for section in sections:
787
+ for slug in section.get("slugs", []):
788
+ if slug in slug_to_screen and slug not in seen:
789
+ ordered.append(slug_to_screen[slug])
790
+ seen.add(slug)
791
+ for s in screens:
792
+ if s["slug"] not in seen:
793
+ ordered.append(s)
794
+ screens = ordered
795
+
721
796
  # Build screens JSON for prev/next navigation
722
797
  screens_data = [
723
798
  {"title": s["title"], "desc": s["description"], "html_file": s["html_file"]}
@@ -738,6 +813,7 @@ def _generate_viewer(output_dir: Path, ptype: str, metadata: dict, screens: list
738
813
  font_family = font or "system-ui"
739
814
 
740
815
  html = template
816
+ html = html.replace("{{HTML_LANG}}", metadata.get("lang", "en"))
741
817
  html = html.replace("{{PROJECT_NAME}}", escape(metadata["project_name"]))
742
818
  html = html.replace("{{SCREENS_JSON}}", json.dumps(screens_data, ensure_ascii=False))
743
819
  html = html.replace("{{DEFAULT_THEME}}", metadata.get("default_theme", "light"))
@@ -796,12 +872,13 @@ def _generate_index(output_dir: Path, ptype: str, metadata: dict, screens: list)
796
872
  )
797
873
 
798
874
  # New template variables
799
- type_labels = {"mobile": "Mobile App", "web": "Web App"}
875
+ type_labels = {"mobile": "App Móvil", "web": "App Web"}
800
876
  type_label = type_labels.get(metadata.get("type", ""), "Showcase")
801
877
  project_desc = _project_description_html(metadata, filtered_screens)
802
878
  screens_intro = _screens_intro_text(metadata)
803
879
 
804
880
  html = template
881
+ html = html.replace("{{HTML_LANG}}", metadata.get("lang", "en"))
805
882
  html = html.replace("{{PROJECT_NAME}}", escape(metadata["project_name"]))
806
883
  html = html.replace("{{SCREEN_COUNT}}", str(len(filtered_screens)))
807
884
  html = html.replace("{{SCREENS_HTML}}", sections_html)
@@ -925,7 +1002,7 @@ def _design_system_html(ds_screen: dict | None, metadata: dict) -> str:
925
1002
  f'style="font-family:\'{escape(font)}\',sans-serif">Aa</span>'
926
1003
  f'<div>'
927
1004
  f'<span class="text-sm font-medium text-gray-700 dark:text-gray-200">{escape(font)}</span>'
928
- f'<span class="text-xs text-gray-400 dark:text-gray-500 ml-2">Primary typeface</span>'
1005
+ f'<span class="text-xs text-gray-400 dark:text-gray-500 ml-2">Tipografía principal</span>'
929
1006
  f'</div>'
930
1007
  f'</div>'
931
1008
  )
@@ -1292,10 +1369,73 @@ def _card_html(screen: dict, ptype: str) -> str:
1292
1369
  </a>"""
1293
1370
 
1294
1371
 
1295
- def _slug_to_title(slug: str) -> str:
1296
- """01_splash_screen → 'Splash Screen' (strips numeric prefix)."""
1297
- s = re.sub(r"^[\d_]+", "", slug)
1298
- return s.replace("_", " ").replace("-", " ").title().strip() or slug.title()
1372
+ # ─── Post-build validation ───────────────────────────────────────────────────
1373
+
1374
+ def _validate_showcase(output_dir: Path, screens: list) -> None:
1375
+ """
1376
+ Sanity-check the generated showcase. Prints warnings only — never aborts.
1377
+
1378
+ Checks:
1379
+ 1. JSON embedded in viewer.html (`var SCREENS = [...]`) parses.
1380
+ 2. Each screen has both `assets/<slug>.html` and `assets/<slug>.png`.
1381
+ 3. Local `src=` references (img / video / source) inside each screen
1382
+ resolve on disk (URLs starting with http/https/data: are skipped).
1383
+ """
1384
+ assets_dir = output_dir / "assets"
1385
+ issues: list[str] = []
1386
+
1387
+ # 1) Embedded SCREENS JSON in viewer.html
1388
+ viewer_path = output_dir / "viewer.html"
1389
+ if viewer_path.exists():
1390
+ viewer_text = viewer_path.read_text(encoding="utf-8")
1391
+ m = re.search(r"var\s+SCREENS\s*=\s*(\[.*?\]);", viewer_text, re.DOTALL)
1392
+ if not m:
1393
+ issues.append("viewer.html: no encontré `var SCREENS = [...]`")
1394
+ else:
1395
+ try:
1396
+ json.loads(m.group(1))
1397
+ except json.JSONDecodeError as e:
1398
+ issues.append(f"viewer.html: SCREENS JSON inválido — {e}")
1399
+
1400
+ # 2) Per-screen assets
1401
+ for s in screens:
1402
+ slug = s["slug"]
1403
+ if not (assets_dir / f"{slug}.html").exists():
1404
+ issues.append(f"{slug}: falta assets/{slug}.html")
1405
+ if not (assets_dir / f"{slug}.png").exists():
1406
+ issues.append(f"{slug}: falta assets/{slug}.png")
1407
+
1408
+ # 3) Local img / video / source references inside each screen HTML
1409
+ src_re = re.compile(
1410
+ r"""<(?:img|video|source)\s[^>]*src=["']([^"']+)["']""",
1411
+ re.IGNORECASE,
1412
+ )
1413
+ for s in screens:
1414
+ slug = s["slug"]
1415
+ html_path = assets_dir / f"{slug}.html"
1416
+ if not html_path.exists():
1417
+ continue
1418
+ try:
1419
+ text = html_path.read_text(encoding="utf-8", errors="ignore")
1420
+ except OSError:
1421
+ continue
1422
+ for src in src_re.findall(text):
1423
+ if src.startswith(("http://", "https://", "data:", "//")):
1424
+ continue
1425
+ ref = (html_path.parent / src).resolve()
1426
+ if not ref.exists():
1427
+ issues.append(f"{slug}: src inexistente '{src}'")
1428
+
1429
+ # 4) Report
1430
+ print()
1431
+ if not issues:
1432
+ print(f"✅ {len(screens)}/{len(screens)} screens válidos")
1433
+ else:
1434
+ print(f"⚠ {len(issues)} problema(s) detectado(s):")
1435
+ for issue in issues[:20]:
1436
+ print(f" - {issue}")
1437
+ if len(issues) > 20:
1438
+ print(f" ...({len(issues) - 20} más)")
1299
1439
 
1300
1440
 
1301
1441
  def _project_slug(name: str) -> str:
@@ -1422,7 +1562,7 @@ def _init(source_dir: str, project_type: str, project_name: str) -> None:
1422
1562
  sys.exit(1)
1423
1563
 
1424
1564
  # Auto-group slugs for skeleton
1425
- fake_screens = [{"slug": s, "title": _slug_to_title(s), "description": ""} for s in slugs]
1565
+ fake_screens = [{"slug": s, "title": slug_to_title(s), "description": ""} for s in slugs]
1426
1566
  groups = _auto_group_screens(fake_screens)
1427
1567
 
1428
1568
  pname = project_name or source.name.replace("_", " ").replace("-", " ").title()
@@ -3,13 +3,26 @@ parse_design_md.py — Extracts metadata from DESIGN.md for stitch-showcase.
3
3
 
4
4
  Usage:
5
5
  python parse_design_md.py /path/to/DESIGN.md
6
- → prints JSON with project_name, type, colors, color_tokens, default_theme, font_family, screens, sections
6
+ → prints JSON with project_name, type, lang, colors, color_tokens, default_theme, font_family, screens, sections
7
7
  """
8
8
  import re
9
9
  import sys
10
10
  import json
11
11
  from pathlib import Path
12
12
 
13
+ # Ensure sibling modules resolve when this file is invoked directly.
14
+ sys.path.insert(0, str(Path(__file__).parent))
15
+ from slug_demangle import slug_to_title
16
+
17
+
18
+ SPANISH_STOPWORDS = {
19
+ "de", "el", "la", "los", "las", "para", "con", "por", "una", "uno",
20
+ "del", "que", "es", "se", "su", "sus", "como", "más", "muy", "este",
21
+ "esta", "estos", "estas", "pantalla", "pantallas", "aplicación",
22
+ "aplicacion", "usuario", "usuarios", "diseño", "diseno", "móvil",
23
+ "movil", "perfil", "configuración", "configuracion",
24
+ }
25
+
13
26
 
14
27
  def parse(design_md_path: str) -> dict:
15
28
  path = Path(design_md_path)
@@ -17,6 +30,7 @@ def parse(design_md_path: str) -> dict:
17
30
  return {
18
31
  "project_name": "",
19
32
  "type": "unknown",
33
+ "lang": "en",
20
34
  "colors": {},
21
35
  "color_tokens": {},
22
36
  "default_theme": "light",
@@ -26,9 +40,17 @@ def parse(design_md_path: str) -> dict:
26
40
  }
27
41
 
28
42
  text = path.read_text(encoding="utf-8")
43
+ frontmatter = _parse_yaml_frontmatter(text)
44
+ lang = _extract_lang_override(text) or _detect_lang(text)
29
45
  colors = _extract_colors(text)
30
46
  color_tokens = _extract_color_tokens(text)
31
47
 
48
+ # Frontmatter fills in fields the Markdown body doesn't define.
49
+ if not colors and frontmatter.get("colors"):
50
+ colors = dict(frontmatter["colors"])
51
+ if not color_tokens and frontmatter.get("colors"):
52
+ color_tokens = _semantic_tokens_from_dict(frontmatter["colors"])
53
+
32
54
  # Determine default_theme from surface token
33
55
  surface_hex = color_tokens.get("surface") or color_tokens.get("background")
34
56
  if not surface_hex:
@@ -58,7 +80,7 @@ def parse(design_md_path: str) -> dict:
58
80
  # Apply titles/descriptions to screens already in the list
59
81
  for s in screens:
60
82
  slug = s["slug"]
61
- if not s.get("title") or s["title"] == _slug_to_title(slug):
83
+ if not s.get("title") or s["title"] == slug_to_title(slug):
62
84
  if slug in section_titles:
63
85
  s["title"] = section_titles[slug]
64
86
  if not s.get("description") and slug in section_descs:
@@ -69,29 +91,145 @@ def parse(design_md_path: str) -> dict:
69
91
  if slug not in existing_slugs:
70
92
  screens.append({
71
93
  "slug": slug,
72
- "title": section_titles.get(slug) or _slug_to_title(slug),
94
+ "title": section_titles.get(slug) or slug_to_title(slug),
73
95
  "description": section_descs.get(slug, ""),
74
96
  })
75
97
  existing_slugs.add(slug)
76
98
 
99
+ project_name = _extract_project_name(text) or frontmatter.get("name", "")
100
+ font_family = _extract_typography(text) or frontmatter.get("font_family")
101
+
77
102
  return {
78
- "project_name": _extract_project_name(text),
103
+ "project_name": project_name,
79
104
  "type": _detect_type(text),
105
+ "lang": lang,
80
106
  "colors": colors,
81
107
  "color_tokens": color_tokens,
82
108
  "default_theme": default_theme,
83
- "font_family": _extract_typography(text),
109
+ "font_family": font_family,
84
110
  "screens": screens,
85
111
  "sections": sections,
86
112
  }
87
113
 
88
114
 
115
+ def _parse_yaml_frontmatter(text: str) -> dict:
116
+ """
117
+ Parse a Stitch-style YAML frontmatter at the top of DESIGN.md.
118
+
119
+ Recognizes (everything else is ignored):
120
+ - top-level ``name:`` → ``"name"``
121
+ - ``colors:`` block with hex values → ``"colors"`` dict
122
+ - ``typography:`` block — returns the first ``fontFamily`` → ``"font_family"``
123
+
124
+ Returns an empty dict if no frontmatter delimiter is present.
125
+ """
126
+ m = re.match(r"^---\s*\n(.*?)\n---\s*\n", text, re.DOTALL)
127
+ if not m:
128
+ return {}
129
+ fm = m.group(1)
130
+ result: dict = {}
131
+
132
+ # name: <value>
133
+ name_m = re.search(r"^name:\s*(.+?)\s*$", fm, re.MULTILINE)
134
+ if name_m:
135
+ val = name_m.group(1).strip().strip("'\"")
136
+ if val:
137
+ result["name"] = val
138
+
139
+ # colors: block — collect indented "key: '#hex'" pairs until indentation drops.
140
+ colors_m = re.search(r"^colors:\s*\n((?:[ \t]+\S.*\n?)+)", fm, re.MULTILINE)
141
+ if colors_m:
142
+ colors: dict = {}
143
+ for line in colors_m.group(1).splitlines():
144
+ cm = re.match(
145
+ r"^\s+([a-z][a-z0-9\-]*):\s*['\"]?(#[0-9A-Fa-f]{3,8})['\"]?\s*$",
146
+ line,
147
+ )
148
+ if cm:
149
+ colors[cm.group(1).lower()] = "#" + cm.group(2).lstrip("#").upper()
150
+ if colors:
151
+ result["colors"] = colors
152
+
153
+ # typography: pick the first fontFamily declared anywhere in the block.
154
+ typo_m = re.search(r"fontFamily:\s*['\"]?([^'\"\n,]+)['\"]?", fm)
155
+ if typo_m:
156
+ font = typo_m.group(1).strip().rstrip(",;")
157
+ if font:
158
+ result["font_family"] = font
159
+
160
+ return result
161
+
162
+
163
+ def _semantic_tokens_from_dict(colors: dict) -> dict:
164
+ """
165
+ Compute `accent` / `surface` semantic tokens from a flat color dict.
166
+
167
+ Mirrors the rules used by :func:`_extract_color_tokens`:
168
+ - ``accent`` → first key containing ``primary`` (not ``on-primary``)
169
+ - ``surface`` → first key named ``surface`` or containing ``background``/``bg``
170
+ """
171
+ tokens = dict(colors)
172
+ for name, val in colors.items():
173
+ if "primary" in name and not name.startswith("on-"):
174
+ tokens["accent"] = val
175
+ break
176
+ for name, val in colors.items():
177
+ if name == "surface" or "background" in name or name == "bg":
178
+ tokens["surface"] = val
179
+ break
180
+ return tokens
181
+
182
+
89
183
  def _extract_project_name(text: str) -> str:
90
184
  """First H1 heading in the document."""
91
185
  m = re.search(r"^#\s+(.+)$", text, re.MULTILINE)
92
186
  return m.group(1).strip() if m else ""
93
187
 
94
188
 
189
+ def _extract_lang_override(text: str) -> str | None:
190
+ """
191
+ Explicit override via ``## Lang`` (or ``## Language``) section in DESIGN.md.
192
+
193
+ Expected format:
194
+ ## Lang
195
+ es
196
+
197
+ Returns a BCP-47-ish code (``es``, ``en``, ``pt-BR``…) or None if absent.
198
+ """
199
+ m = re.search(
200
+ r"^##\s+Lang(?:uage)?\s*\n\s*([A-Za-z]{2}(?:-[A-Za-z]{2,4})?)\s*$",
201
+ text, re.IGNORECASE | re.MULTILINE
202
+ )
203
+ if not m:
204
+ return None
205
+ raw = m.group(1)
206
+ # Normalize: language subtag lowercase, region subtag uppercase (BCP-47).
207
+ if "-" in raw:
208
+ lang, region = raw.split("-", 1)
209
+ return f"{lang.lower()}-{region.upper()}"
210
+ return raw.lower()
211
+
212
+
213
+ def _detect_lang(text: str) -> str:
214
+ """
215
+ Heuristic language detection. Distinguishes Spanish from everything else.
216
+
217
+ Signals:
218
+ - Spanish-only accented characters (á é í ó ú ñ ü).
219
+ - Common Spanish stop words from SPANISH_STOPWORDS.
220
+
221
+ Returns 'es' when either signal is present, else 'en'.
222
+ """
223
+ if not text:
224
+ return "en"
225
+ lower = text.lower()
226
+ if re.search(r"[áéíóúñü]", lower):
227
+ return "es"
228
+ words = re.findall(r"[a-záéíóúñü]+", lower)
229
+ stop_hits = sum(1 for w in words if w in SPANISH_STOPWORDS)
230
+ return "es" if stop_hits >= 3 else "en"
231
+
232
+
95
233
  def _detect_type(text: str) -> str:
96
234
  """Returns 'mobile', 'web', or 'unknown'.
97
235
 
@@ -292,7 +430,7 @@ def _parse_table(text: str) -> list:
292
430
  cols = [c.strip() for c in line.split("|") if c.strip()]
293
431
  if len(cols) >= 2:
294
432
  slug = _to_slug(cols[0])
295
- title = cols[1] if len(cols) > 1 else _slug_to_title(slug)
433
+ title = cols[1] if len(cols) > 1 else slug_to_title(slug)
296
434
  desc = cols[2] if len(cols) > 2 else ""
297
435
  rows.append({"slug": slug, "title": title, "description": desc})
298
436
 
@@ -324,7 +462,7 @@ def _parse_screen_list(text: str) -> list:
324
462
  title = title_part.strip()
325
463
  desc = desc_part.strip()
326
464
  else:
327
- title = _slug_to_title(slug)
465
+ title = slug_to_title(slug)
328
466
  desc = raw
329
467
  screens.append({"slug": slug, "title": title, "description": desc})
330
468
 
@@ -384,11 +522,6 @@ def _to_slug(s: str) -> str:
384
522
  return s
385
523
 
386
524
 
387
- def _slug_to_title(slug: str) -> str:
388
- """splash_screen → 'Splash Screen'"""
389
- return slug.replace("_", " ").replace("-", " ").title()
390
-
391
-
392
525
  if __name__ == "__main__":
393
526
  if len(sys.argv) < 2:
394
527
  print("Usage: python parse_design_md.py /path/to/DESIGN.md", file=sys.stderr)