@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.
- package/README.md +42 -7
- package/lib/cleanup.js +29 -0
- package/lib/commands/skills.js +110 -9
- package/lib/config.js +17 -9
- package/lib/installer.js +5 -3
- package/lib/symlink.js +45 -3
- package/lib/utils.js +41 -0
- package/package.json +1 -1
- package/skills/audit-codebase/SKILL.md +70 -0
- package/skills/audit-codebase/agents/openai.yaml +4 -0
- package/skills/audit-codebase/references/comprehensive-audit.md +220 -0
- package/skills/audit-codebase/references/report-format.md +119 -0
- package/skills/humaniza/SKILL.md +55 -4
- package/skills/humaniza/references/ai-patterns-es.md +40 -0
- package/skills/humaniza/references/checklist.md +9 -0
- package/skills/humaniza/references/examples.md +16 -0
- package/skills/humaniza/references/lexicon-es-mx.md +18 -0
- package/skills/humaniza/references/structures-es.md +132 -0
- package/skills/humaniza/scripts/check_ai_patterns.py +216 -0
- package/skills/refactoring-ui/SKILL.md +65 -29
- package/skills/refactoring-ui/references/05-motion.md +124 -0
- package/skills/refactoring-ui/references/06-dark-mode.md +117 -0
- package/skills/refactoring-ui/references/07-component-patterns.md +181 -0
- package/skills/stitch-showcase/SKILL.md +24 -232
- package/skills/stitch-showcase/references/07-theme-system.md +12 -0
- package/skills/stitch-showcase/references/08-type-detection.md +9 -1
- package/skills/stitch-showcase/references/10-component-standardization.md +25 -0
- package/skills/stitch-showcase/references/12-video-embedding.md +113 -0
- package/skills/stitch-showcase/references/13-language-detection.md +82 -0
- package/skills/stitch-showcase/references/14-troubleshooting-known-issues.md +122 -0
- package/skills/stitch-showcase/references/15-build-flags.md +71 -0
- package/skills/stitch-showcase/references/16-design-md-format.md +107 -0
- package/skills/stitch-showcase/references/index.html +25 -19
- package/skills/stitch-showcase/references/viewer.html +24 -12
- package/skills/stitch-showcase/scripts/__pycache__/build_showcase.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/component_utils.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/detect_components.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/extract_catalog.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/extract_text.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/extract_zips.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/parse_design_md.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/slug_demangle.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/build_showcase.py +150 -10
- package/skills/stitch-showcase/scripts/parse_design_md.py +145 -12
- package/skills/stitch-showcase/scripts/slug_demangle.py +209 -0
- package/skills/vscode-extension-dev/SKILL.md +90 -41
- package/skills/vscode-extension-dev/references/api-additional.md +168 -0
- package/skills/vscode-extension-dev/references/api-progress.md +55 -0
- package/skills/vscode-extension-dev/references/api-quickpick.md +75 -0
- package/skills/vscode-extension-dev/references/api-secretstorage.md +57 -0
- package/skills/vscode-extension-dev/references/api-statusbar.md +38 -0
- package/skills/vscode-extension-dev/references/api-treeview.md +78 -0
- package/skills/vscode-extension-dev/references/api-webview.md +149 -0
- package/skills/vscode-extension-dev/references/architecture.md +67 -0
- package/skills/vscode-extension-dev/references/debugger.md +179 -0
- package/skills/vscode-extension-dev/references/lsp.md +175 -0
- package/skills/vscode-extension-dev/references/notebooks.md +208 -0
- package/skills/vscode-extension-dev/references/testing.md +208 -0
- package/skills/vscode-extension-dev/references/api-patterns.md +0 -625
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="
|
|
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="
|
|
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
|
-
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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 =
|
|
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(
|
|
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 (&, …).
|
|
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
|
+
("&", "&"), ("<", "<"), (">", ">"),
|
|
580
|
+
(" ", " "), ("'", "'"), (""", '"'),
|
|
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": "
|
|
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">
|
|
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
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
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":
|
|
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"] ==
|
|
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
|
|
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":
|
|
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":
|
|
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
|
|
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 =
|
|
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)
|