@maccesar/aiskills 1.7.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 (73) hide show
  1. package/README.md +531 -0
  2. package/bin/aiskills.js +76 -0
  3. package/lib/cache.js +49 -0
  4. package/lib/cleanup.js +77 -0
  5. package/lib/commands/auto-update.js +131 -0
  6. package/lib/commands/doctor.js +139 -0
  7. package/lib/commands/list.js +77 -0
  8. package/lib/commands/skills.js +263 -0
  9. package/lib/commands/status.js +94 -0
  10. package/lib/commands/uninstall.js +182 -0
  11. package/lib/commands/update.js +149 -0
  12. package/lib/config.js +90 -0
  13. package/lib/downloader.js +110 -0
  14. package/lib/hooks.js +74 -0
  15. package/lib/installer.js +114 -0
  16. package/lib/platform.js +112 -0
  17. package/lib/prompts/checkboxCancel.js +264 -0
  18. package/lib/prompts/selectCancel.js +204 -0
  19. package/lib/symlink.js +154 -0
  20. package/lib/utils.js +49 -0
  21. package/package.json +61 -0
  22. package/skills/humaniza/SKILL.md +51 -0
  23. package/skills/humaniza/agents/openai.yaml +4 -0
  24. package/skills/humaniza/references/ai-patterns-es.md +51 -0
  25. package/skills/humaniza/references/checklist.md +9 -0
  26. package/skills/humaniza/references/examples.md +17 -0
  27. package/skills/humaniza/references/lexicon-es-mx.md +36 -0
  28. package/skills/humaniza/references/modes-es-mx.md +41 -0
  29. package/skills/humaniza/references/voice-es-mx.md +24 -0
  30. package/skills/refactoring-ui/SKILL.md +59 -0
  31. package/skills/refactoring-ui/references/01-design-process.md +72 -0
  32. package/skills/refactoring-ui/references/02-visual-hierarchy.md +84 -0
  33. package/skills/refactoring-ui/references/03-layout-spacing.md +69 -0
  34. package/skills/refactoring-ui/references/04-typography.md +70 -0
  35. package/skills/refactoring-ui/references/05-color.md +96 -0
  36. package/skills/refactoring-ui/references/06-depth-shadows.md +74 -0
  37. package/skills/refactoring-ui/references/07-images.md +75 -0
  38. package/skills/refactoring-ui/references/08-finishing-touches.md +91 -0
  39. package/skills/stitch-showcase/SKILL.md +411 -0
  40. package/skills/stitch-showcase/references/01-navbar.md +52 -0
  41. package/skills/stitch-showcase/references/02-hero.md +56 -0
  42. package/skills/stitch-showcase/references/03-design-system.md +102 -0
  43. package/skills/stitch-showcase/references/04-screen-gallery.md +102 -0
  44. package/skills/stitch-showcase/references/05-viewer-web.md +105 -0
  45. package/skills/stitch-showcase/references/06-viewer-mobile.md +104 -0
  46. package/skills/stitch-showcase/references/07-theme-system.md +77 -0
  47. package/skills/stitch-showcase/references/08-type-detection.md +81 -0
  48. package/skills/stitch-showcase/references/09-quality-standards.md +126 -0
  49. package/skills/stitch-showcase/references/10-component-standardization.md +40 -0
  50. package/skills/stitch-showcase/references/11-component-catalog.md +70 -0
  51. package/skills/stitch-showcase/references/catalog-template.html +841 -0
  52. package/skills/stitch-showcase/references/index.html +299 -0
  53. package/skills/stitch-showcase/references/viewer.html +412 -0
  54. package/skills/stitch-showcase/scripts/__pycache__/build_showcase.cpython-314.pyc +0 -0
  55. package/skills/stitch-showcase/scripts/__pycache__/component_utils.cpython-313.pyc +0 -0
  56. package/skills/stitch-showcase/scripts/__pycache__/detect_components.cpython-313.pyc +0 -0
  57. package/skills/stitch-showcase/scripts/__pycache__/extract_catalog.cpython-313.pyc +0 -0
  58. package/skills/stitch-showcase/scripts/__pycache__/extract_text.cpython-313.pyc +0 -0
  59. package/skills/stitch-showcase/scripts/__pycache__/extract_zips.cpython-313.pyc +0 -0
  60. package/skills/stitch-showcase/scripts/__pycache__/parse_design_md.cpython-313.pyc +0 -0
  61. package/skills/stitch-showcase/scripts/apply_canonical.py +238 -0
  62. package/skills/stitch-showcase/scripts/build_showcase.py +2103 -0
  63. package/skills/stitch-showcase/scripts/component_utils.py +398 -0
  64. package/skills/stitch-showcase/scripts/detect_components.py +284 -0
  65. package/skills/stitch-showcase/scripts/extract_catalog.py +913 -0
  66. package/skills/stitch-showcase/scripts/extract_text.py +268 -0
  67. package/skills/stitch-showcase/scripts/extract_zips.py +178 -0
  68. package/skills/stitch-showcase/scripts/parse_design_md.py +397 -0
  69. package/skills/vscode-extension-dev/SKILL.md +114 -0
  70. package/skills/vscode-extension-dev/references/api-patterns.md +625 -0
  71. package/skills/vscode-extension-dev/references/architecture.md +287 -0
  72. package/skills/vscode-extension-dev/references/package-json-schema.md +345 -0
  73. package/skills/vscode-extension-dev/references/publishing.md +251 -0
@@ -0,0 +1,412 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Viewer — {{PROJECT_NAME}}</title>
8
+ {{FONT_LINK}}
9
+ <script>
10
+ (function () {
11
+ var s = localStorage.getItem('showcase-theme');
12
+ var d = '{{DEFAULT_THEME}}';
13
+ if ((s || d) === 'dark') document.documentElement.classList.add('dark');
14
+ // Apply view mode before render — URL param > localStorage > default
15
+ var params = new URLSearchParams(location.search);
16
+ var viewKey = 'showcase-view-mode-' + '{{PROJECT_SLUG}}';
17
+ var view = localStorage.getItem(viewKey) || params.get('view') || '{{DEFAULT_VIEW}}';
18
+ document.documentElement.classList.add('view-' + view);
19
+ })();
20
+ </script>
21
+ <script src="https://cdn.tailwindcss.com"></script>
22
+ <script>tailwind.config = { darkMode: 'class', theme: { extend: { fontFamily: { sans: ['{{FONT_FAMILY}}', 'system-ui', 'sans-serif'] } } } }</script>
23
+ <style>
24
+ body { margin: 0; overflow: hidden; }
25
+
26
+ /* ── Phone frame (mobile view) ── */
27
+ #phone-wrap {
28
+ width: 390px;
29
+ height: 844px;
30
+ border: 8px solid #2a2a2a;
31
+ border-radius: 44px;
32
+ overflow: hidden;
33
+ box-shadow: 0 25px 60px rgba(0,0,0,0.4);
34
+ flex-shrink: 0;
35
+ position: relative;
36
+ background: #000;
37
+ box-sizing: content-box;
38
+ }
39
+ .dark #phone-wrap { border-color: #2a2a2a; box-shadow: 0 25px 60px rgba(0,0,0,0.4); }
40
+ html:not(.dark) #phone-wrap { border-color: #d1d5db; box-shadow: 0 25px 60px rgba(0,0,0,0.15); }
41
+ #phone-container { transform-origin: top center; }
42
+
43
+ /* ── Browser chrome (web view) ── */
44
+ #browser-chrome {
45
+ height: 36px;
46
+ border-radius: 12px 12px 0 0;
47
+ display: flex;
48
+ align-items: center;
49
+ padding: 0 14px;
50
+ gap: 8px;
51
+ background: #e8e8e8;
52
+ }
53
+ html.dark #browser-chrome { background: #1a1a1a; }
54
+
55
+ .traffic-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
56
+
57
+ #url-pill {
58
+ flex: 1; max-width: 480px; margin: 0 auto; height: 22px;
59
+ border-radius: 11px; display: flex; align-items: center;
60
+ justify-content: center; padding: 0 12px;
61
+ background: rgba(0, 0, 0, 0.06); overflow: hidden;
62
+ }
63
+ html.dark #url-pill { background: rgba(255, 255, 255, 0.06); }
64
+ #url-pill span { font-size: 11px; color: #888; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
65
+ html.dark #url-pill span { color: #666; }
66
+
67
+ #iframe-wrap-web {
68
+ border-left: 1px solid #d4d4d4;
69
+ border-right: 1px solid #d4d4d4;
70
+ border-bottom: 1px solid #d4d4d4;
71
+ }
72
+ html.dark #iframe-wrap-web { border-color: #2a2a2a; }
73
+
74
+ /* ── View mode visibility ── */
75
+ .view-mobile #mobile-frame { display: flex; }
76
+ .view-mobile #web-frame { display: none; }
77
+ .view-web #mobile-frame { display: none; }
78
+ .view-web #web-frame { display: flex; }
79
+
80
+ #viewer-frame {
81
+ display: block; width: 100%; height: 100%; border: none;
82
+ background: #fff;
83
+ }
84
+ html.dark #viewer-frame { background: #111; }
85
+ </style>
86
+ </head>
87
+
88
+ <body class="flex h-screen flex-col overflow-hidden text-gray-900 transition-colors duration-200 dark:text-gray-100 bg-[#f0f0f0] dark:bg-[#0a0a0a]">
89
+
90
+ <script>
91
+ var SCREENS = {{SCREENS_JSON}};
92
+ </script>
93
+
94
+ <!-- ── Header ──────────────────────────────────────────── -->
95
+ <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">
97
+
98
+ <!-- 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">
100
+ <svg class="h-3.5 w-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
101
+ <path d="M10 13L5 8l5-5" />
102
+ </svg>
103
+ Back
104
+ </a>
105
+
106
+ <div class="h-4 w-px shrink-0 bg-gray-200 dark:bg-white/10"></div>
107
+
108
+ <!-- Screen title -->
109
+ <span id="screen-title" class="shrink-0 text-[13px] font-semibold leading-none">--</span>
110
+
111
+ <!-- Screen description (inline after title) -->
112
+ <span id="screen-desc" class="hidden text-[11px] text-gray-400 dark:text-gray-500 truncate sm:inline"></span>
113
+
114
+ <!-- Spacer -->
115
+ <div class="flex-1"></div>
116
+
117
+ <!-- Position counter -->
118
+ <span id="nav-pos" class="shrink-0 tabular-nums text-[11px] text-gray-400 dark:text-gray-500">-- / --</span>
119
+
120
+ <!-- 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>
122
+ <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
+ </button>
124
+
125
+ <!-- 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>
127
+ <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
+ </button>
129
+
130
+ <!-- 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">
132
+ <svg class="block h-3.5 w-3.5 dark:hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
133
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
134
+ </svg>
135
+ <svg class="hidden h-3.5 w-3.5 dark:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
136
+ <circle cx="12" cy="12" r="5" />
137
+ <line x1="12" y1="1" x2="12" y2="3" />
138
+ <line x1="12" y1="21" x2="12" y2="23" />
139
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
140
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
141
+ <line x1="1" y1="12" x2="3" y2="12" />
142
+ <line x1="21" y1="12" x2="23" y2="12" />
143
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
144
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
145
+ </svg>
146
+ </button>
147
+
148
+ <!-- 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">
150
+ <svg id="icon-phone" class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
151
+ <rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/>
152
+ </svg>
153
+ <svg id="icon-monitor" class="hidden h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
154
+ <rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/>
155
+ </svg>
156
+ </button>
157
+
158
+ <!-- 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">
160
+ <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
+ <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
+ </svg>
163
+ <svg id="icon-compress" class="hidden h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
164
+ <path d="M4 14h6v6m10-10h-6V4M4 10h6V4m10 10h-6v6"/>
165
+ </svg>
166
+ </button>
167
+
168
+ </div>
169
+ </header>
170
+
171
+ <!-- ── Fullscreen container ──────────────────────────────── -->
172
+ <div id="fs-container" class="flex flex-1 flex-col overflow-hidden">
173
+
174
+ <!-- ── Mobile frame (phone) ──────────────────────────── -->
175
+ <main id="mobile-frame" class="hidden flex-1 items-start justify-center overflow-hidden pt-6">
176
+ <div id="phone-container">
177
+ <div id="phone-wrap">
178
+ <iframe id="viewer-frame-mobile" class="viewer-frame" title="Screen preview" style="width:100%;height:100%;border:none;display:block;background:#000"></iframe>
179
+ </div>
180
+ </div>
181
+ </main>
182
+
183
+ <!-- ── Web frame (browser chrome) ────────────────────── -->
184
+ <div id="web-frame" class="hidden mx-auto w-full max-w-[1440px] flex-1 flex-col overflow-hidden px-5 py-4">
185
+ <div id="browser-chrome" class="flex-shrink-0">
186
+ <div class="traffic-dot" style="background:#FF5F57"></div>
187
+ <div class="traffic-dot" style="background:#FEBC2E"></div>
188
+ <div class="traffic-dot" style="background:#28C840"></div>
189
+ <div id="url-pill"><span id="url-display">localhost</span></div>
190
+ <div style="width:60px;flex-shrink:0"></div>
191
+ </div>
192
+ <div id="iframe-wrap-web" class="flex-1 overflow-hidden rounded-b-xl">
193
+ <iframe id="viewer-frame-web" class="viewer-frame" src="" title="Screen preview" style="width:100%;height:100%;border:none;display:block;background:#fff"></iframe>
194
+ </div>
195
+ </div>
196
+
197
+ </div>
198
+
199
+ <script>
200
+ (function () {
201
+ 'use strict';
202
+
203
+ // ── Elements ──
204
+ var header = document.getElementById('main-header');
205
+ var titleEl = document.getElementById('screen-title');
206
+ var descEl = document.getElementById('screen-desc');
207
+ var navPos = document.getElementById('nav-pos');
208
+ var btnBack = document.getElementById('btn-back');
209
+ var btnPrev = document.getElementById('btn-prev');
210
+ var btnNext = document.getElementById('btn-next');
211
+ var btnFullscreen = document.getElementById('btn-fullscreen');
212
+ var iconExpand = document.getElementById('icon-expand');
213
+ var iconCompress = document.getElementById('icon-compress');
214
+ var iconPhone = document.getElementById('icon-phone');
215
+ var iconMonitor = document.getElementById('icon-monitor');
216
+ var phoneContainer = document.getElementById('phone-container');
217
+ var urlDisplay = document.getElementById('url-display');
218
+
219
+ var frameMobile = document.getElementById('viewer-frame-mobile');
220
+ var frameWeb = document.getElementById('viewer-frame-web');
221
+
222
+ var screens = SCREENS;
223
+ var currentIndex = 0;
224
+ var VIEW_KEY = 'showcase-view-mode-' + '{{PROJECT_SLUG}}';
225
+ var currentView = localStorage.getItem(VIEW_KEY) || '{{DEFAULT_VIEW}}';
226
+
227
+ // ── View mode ──
228
+ function applyViewMode() {
229
+ var root = document.documentElement;
230
+ root.classList.remove('view-mobile', 'view-web');
231
+ root.classList.add('view-' + currentView);
232
+
233
+ // Toggle icon shows what you'll switch TO
234
+ if (currentView === 'mobile') {
235
+ iconPhone.classList.add('hidden');
236
+ iconMonitor.classList.remove('hidden');
237
+ } else {
238
+ iconPhone.classList.remove('hidden');
239
+ iconMonitor.classList.add('hidden');
240
+ }
241
+
242
+ // Recalc phone scaling when switching to mobile
243
+ if (currentView === 'mobile') recalcScale();
244
+ }
245
+ applyViewMode();
246
+
247
+ document.getElementById('view-mode-toggle').addEventListener('click', function () {
248
+ currentView = currentView === 'mobile' ? 'web' : 'mobile';
249
+ localStorage.setItem(VIEW_KEY, currentView);
250
+ applyViewMode();
251
+ // Sync iframe src to the active frame
252
+ syncFrameSrc();
253
+ });
254
+
255
+ // ── Helpers ──
256
+ function activeFrame() {
257
+ return currentView === 'mobile' ? frameMobile : frameWeb;
258
+ }
259
+
260
+ function syncFrameSrc() {
261
+ // Ensure both frames have the same src
262
+ var src = frameMobile.src || frameWeb.src;
263
+ if (frameMobile.src !== src) frameMobile.src = src;
264
+ if (frameWeb.src !== src) frameWeb.src = src;
265
+ }
266
+
267
+ function getParam(name) {
268
+ return new URLSearchParams(window.location.search).get(name) || '';
269
+ }
270
+
271
+ function findScreenIndex(file) {
272
+ for (var i = 0; i < screens.length; i++) {
273
+ if (screens[i].html_file === file) return i;
274
+ }
275
+ return 0;
276
+ }
277
+
278
+ // ── Load screen ──
279
+ function loadScreen(index) {
280
+ if (screens.length === 0) return;
281
+ if (index < 0) index = screens.length - 1;
282
+ if (index >= screens.length) index = 0;
283
+ currentIndex = index;
284
+
285
+ var s = screens[currentIndex];
286
+ frameMobile.src = s.html_file;
287
+ frameWeb.src = s.html_file;
288
+ titleEl.textContent = s.title || '';
289
+ descEl.textContent = s.desc || '';
290
+ navPos.textContent = (currentIndex + 1) + ' / ' + screens.length;
291
+
292
+ // Update URL
293
+ var url = new URL(window.location);
294
+ url.searchParams.set('screen', s.html_file);
295
+ history.replaceState(null, '', url);
296
+
297
+ // Page title
298
+ document.title = (s.title || 'Screen') + ' — {{PROJECT_NAME}}';
299
+
300
+ // URL bar in browser chrome
301
+ var slug = s.html_file.replace('assets/', '').replace('.html', '').replace(/_/g, '-');
302
+ urlDisplay.textContent = 'app.{{PROJECT_NAME}} / ' + slug;
303
+
304
+ // Enable/disable nav buttons
305
+ btnPrev.disabled = currentIndex <= 0;
306
+ btnNext.disabled = currentIndex >= screens.length - 1;
307
+ }
308
+
309
+ // ── Phone scaling ──
310
+ function recalcScale() {
311
+ if (currentView !== 'mobile') return;
312
+ var vw = window.innerWidth;
313
+ var vh = window.innerHeight;
314
+ var headerH = header.offsetHeight;
315
+ var padX = 48;
316
+ var padY = 48;
317
+ var phoneW = 390 + 16;
318
+ var phoneH = 844 + 16;
319
+
320
+ var scaleX = (vw - padX) / phoneW;
321
+ var scaleY = (vh - headerH - padY) / phoneH;
322
+ var scale = Math.min(1, scaleX, scaleY);
323
+
324
+ phoneContainer.style.transform = 'scale(' + scale + ')';
325
+ phoneContainer.style.width = phoneW + 'px';
326
+ phoneContainer.style.height = phoneH + 'px';
327
+ }
328
+
329
+ // ── Fullscreen ──
330
+ function isFullscreen() {
331
+ return !!(document.fullscreenElement || document.webkitFullscreenElement);
332
+ }
333
+
334
+ function toggleFullscreen() {
335
+ var el = document.getElementById('fs-container');
336
+ if (isFullscreen()) {
337
+ (document.exitFullscreen || document.webkitExitFullscreen).call(document);
338
+ } else {
339
+ (el.requestFullscreen || el.webkitRequestFullscreen).call(el);
340
+ }
341
+ }
342
+
343
+ function updateFullscreenIcon() {
344
+ var fs = isFullscreen();
345
+ iconExpand.style.display = fs ? 'none' : 'block';
346
+ iconCompress.style.display = fs ? 'block' : 'none';
347
+ }
348
+
349
+ // Back button is now an <a href="index.html"> — no JS needed
350
+
351
+ // ── Navigation ──
352
+ btnPrev.addEventListener('click', function () { loadScreen(currentIndex - 1); });
353
+ btnNext.addEventListener('click', function () { loadScreen(currentIndex + 1); });
354
+ btnFullscreen.addEventListener('click', toggleFullscreen);
355
+
356
+ // ── Keyboard shortcuts ──
357
+ document.addEventListener('keydown', function (e) {
358
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
359
+ switch (e.key) {
360
+ case 'ArrowLeft':
361
+ e.preventDefault();
362
+ loadScreen(currentIndex - 1);
363
+ break;
364
+ case 'ArrowRight':
365
+ e.preventDefault();
366
+ loadScreen(currentIndex + 1);
367
+ break;
368
+ case 'Escape':
369
+ if (isFullscreen()) {
370
+ document.exitFullscreen();
371
+ } else {
372
+ btnBack.click();
373
+ }
374
+ break;
375
+ case 'f':
376
+ case 'F':
377
+ if (!e.ctrlKey && !e.metaKey) {
378
+ e.preventDefault();
379
+ toggleFullscreen();
380
+ }
381
+ break;
382
+ }
383
+ });
384
+
385
+ // ── Fullscreen change ──
386
+ document.addEventListener('fullscreenchange', updateFullscreenIcon);
387
+ document.addEventListener('webkitfullscreenchange', updateFullscreenIcon);
388
+
389
+ // ── Theme toggle ──
390
+ document.getElementById('theme-toggle').addEventListener('click', function () {
391
+ document.documentElement.classList.toggle('dark');
392
+ localStorage.setItem('showcase-theme', document.documentElement.classList.contains('dark') ? 'dark' : 'light');
393
+ });
394
+
395
+ // ── Resize ──
396
+ var resizeTimer;
397
+ window.addEventListener('resize', function () {
398
+ clearTimeout(resizeTimer);
399
+ resizeTimer = setTimeout(recalcScale, 50);
400
+ });
401
+
402
+ // ── Init ──
403
+ var initialScreen = getParam('screen');
404
+ currentIndex = initialScreen ? findScreenIndex(initialScreen) : 0;
405
+ loadScreen(currentIndex);
406
+ recalcScale();
407
+ })();
408
+ </script>
409
+
410
+ </body>
411
+
412
+ </html>
@@ -0,0 +1,238 @@
1
+ """
2
+ apply_canonical.py — Replace component variants with a canonical version across screens.
3
+
4
+ Reads the canonical component from a source screen HTML and replaces matching
5
+ components in target screen HTMLs. Supports both structural (navbar, footer, etc.)
6
+ and atomic (buttons, inputs, etc.) components.
7
+
8
+ Uses only stdlib. No external dependencies.
9
+
10
+ Usage:
11
+ # As a module (from AI agent):
12
+ from apply_canonical import apply_canonical
13
+
14
+ # Standalone:
15
+ python apply_canonical.py /path/to/assets/ navbar home_screen
16
+ python apply_canonical.py /path/to/assets/ button home_screen --targets screen1 screen2
17
+ """
18
+ import re
19
+ import sys
20
+ import json
21
+ from pathlib import Path
22
+
23
+ # Sibling import
24
+ sys.path.insert(0, str(Path(__file__).parent))
25
+ import component_utils as cu
26
+
27
+ # Structural component types handled by semantic block extraction
28
+ STRUCTURAL_TYPES = {"navbar", "navbars", "header", "headers", "footer", "footers",
29
+ "sidebar", "sidebars", "tabbar", "tabbars"}
30
+
31
+ # Map plural type names to singular for semantic block lookup
32
+ _TYPE_SINGULAR = {
33
+ "navbars": "navbar", "headers": "header", "footers": "footer",
34
+ "sidebars": "sidebar", "tabbars": "tabbar",
35
+ }
36
+
37
+
38
+ def apply_canonical(
39
+ assets_dir: Path,
40
+ component_type: str,
41
+ canonical_slug: str,
42
+ target_slugs: list = None,
43
+ ) -> dict:
44
+ """
45
+ Replace component variants in target HTMLs with the canonical version.
46
+
47
+ Args:
48
+ assets_dir: Directory containing screen .html files
49
+ component_type: Component type (e.g., "navbar", "footer", "button")
50
+ canonical_slug: Screen slug to use as the canonical source
51
+ target_slugs: List of screen slugs to update. If None, updates all
52
+ screens that have this component (except the canonical).
53
+
54
+ Returns:
55
+ Dict mapping slug → status: "replaced", "skipped", "not_found", or "error"
56
+ """
57
+ assets_dir = Path(assets_dir).resolve()
58
+ canonical_path = assets_dir / f"{canonical_slug}.html"
59
+
60
+ if not canonical_path.exists():
61
+ return {"_error": f"Canonical screen '{canonical_slug}' not found"}
62
+
63
+ canonical_html = canonical_path.read_text(encoding="utf-8", errors="replace")
64
+
65
+ # Normalize type name
66
+ comp_type = _TYPE_SINGULAR.get(component_type.lower(), component_type.lower())
67
+
68
+ if comp_type in {v for v in _TYPE_SINGULAR.values()}:
69
+ return _apply_structural(assets_dir, comp_type, canonical_html,
70
+ canonical_slug, target_slugs)
71
+ else:
72
+ return _apply_atomic(assets_dir, comp_type, canonical_html,
73
+ canonical_slug, target_slugs)
74
+
75
+
76
+ def _apply_structural(
77
+ assets_dir: Path,
78
+ comp_type: str,
79
+ canonical_html: str,
80
+ canonical_slug: str,
81
+ target_slugs: list = None,
82
+ ) -> dict:
83
+ """Replace structural components (navbar, footer, etc.) in target screens."""
84
+ # Extract the canonical component block
85
+ canonical_blocks = cu.extract_semantic_blocks(canonical_html)
86
+ if comp_type not in canonical_blocks or not canonical_blocks[comp_type]:
87
+ return {"_error": f"No {comp_type} found in canonical screen '{canonical_slug}'"}
88
+
89
+ # Use the first (usually only) block as the canonical version
90
+ canon_block = canonical_blocks[comp_type][0]
91
+
92
+ # Determine target files
93
+ html_files = sorted(assets_dir.glob("*.html"))
94
+ results = {}
95
+
96
+ for html_path in html_files:
97
+ slug = html_path.stem
98
+ if slug == canonical_slug:
99
+ continue
100
+ if target_slugs and slug not in target_slugs:
101
+ continue
102
+
103
+ try:
104
+ html = html_path.read_text(encoding="utf-8", errors="replace")
105
+ except Exception as e:
106
+ results[slug] = f"error: {e}"
107
+ continue
108
+
109
+ # Find the matching component block in this screen
110
+ target_blocks = cu.extract_semantic_blocks(html)
111
+ if comp_type not in target_blocks or not target_blocks[comp_type]:
112
+ results[slug] = "not_found"
113
+ continue
114
+
115
+ # Replace each matching block with the canonical version
116
+ updated = html
117
+ replaced = False
118
+ for target_block in target_blocks[comp_type]:
119
+ if target_block in updated:
120
+ updated = updated.replace(target_block, canon_block, 1)
121
+ replaced = True
122
+
123
+ if replaced and updated != html:
124
+ html_path.write_text(updated, encoding="utf-8")
125
+ results[slug] = "replaced"
126
+ else:
127
+ results[slug] = "skipped"
128
+
129
+ return results
130
+
131
+
132
+ def _apply_atomic(
133
+ assets_dir: Path,
134
+ comp_type: str,
135
+ canonical_html: str,
136
+ canonical_slug: str,
137
+ target_slugs: list = None,
138
+ ) -> dict:
139
+ """
140
+ Replace atomic components (buttons, inputs, etc.) in target screens.
141
+
142
+ Uses the component catalog JSON to find cluster members, then replaces
143
+ matching snippets in each target screen.
144
+ """
145
+ # Try to load catalog for cluster info
146
+ catalog_path = assets_dir.parent / "component_catalog.json"
147
+ catalog = {}
148
+ if catalog_path.exists():
149
+ try:
150
+ catalog = json.loads(catalog_path.read_text(encoding="utf-8"))
151
+ except Exception:
152
+ pass
153
+
154
+ # Find the canonical component snippet from catalog clusters
155
+ plural_type = f"{comp_type}s"
156
+ clusters = catalog.get("clusters", {}).get(plural_type, [])
157
+
158
+ # Find cluster where canonical_slug is in the canonical's found_in
159
+ target_cluster = None
160
+ canon_snippet = None
161
+
162
+ for cluster in clusters:
163
+ canonical = cluster.get("canonical", {})
164
+ if canonical_slug in canonical.get("found_in", []):
165
+ target_cluster = cluster
166
+ canon_snippet = canonical.get("html", "")
167
+ break
168
+
169
+ if not canon_snippet:
170
+ return {"_error": f"No {comp_type} cluster found with canonical from '{canonical_slug}'. "
171
+ f"Run build first to generate component_catalog.json."}
172
+
173
+ # Collect target slugs from cluster variants
174
+ variant_snippets = {}
175
+ for v in target_cluster.get("variants", []):
176
+ for slug in v.get("found_in", []):
177
+ if slug != canonical_slug:
178
+ variant_snippets[slug] = v.get("html", "")
179
+
180
+ html_files = sorted(assets_dir.glob("*.html"))
181
+ results = {}
182
+
183
+ for html_path in html_files:
184
+ slug = html_path.stem
185
+ if slug == canonical_slug:
186
+ continue
187
+ if target_slugs and slug not in target_slugs:
188
+ continue
189
+ if slug not in variant_snippets:
190
+ continue
191
+
192
+ try:
193
+ html = html_path.read_text(encoding="utf-8", errors="replace")
194
+ except Exception as e:
195
+ results[slug] = f"error: {e}"
196
+ continue
197
+
198
+ old_snippet = variant_snippets[slug]
199
+ if old_snippet and old_snippet in html:
200
+ updated = html.replace(old_snippet, canon_snippet, 1)
201
+ html_path.write_text(updated, encoding="utf-8")
202
+ results[slug] = "replaced"
203
+ else:
204
+ results[slug] = "not_found"
205
+
206
+ return results
207
+
208
+
209
+ # ─── CLI ──────────────────────────────────────────────────────────────────────
210
+
211
+ if __name__ == "__main__":
212
+ import argparse
213
+
214
+ parser = argparse.ArgumentParser(
215
+ description="Replace component variants with a canonical version."
216
+ )
217
+ parser.add_argument("assets_dir", help="Directory containing screen .html files")
218
+ parser.add_argument("component_type", help="Component type (navbar, footer, button, etc.)")
219
+ parser.add_argument("canonical_slug", help="Screen slug to use as canonical source")
220
+ parser.add_argument("--targets", nargs="*", help="Specific screen slugs to update (default: all)")
221
+ args = parser.parse_args()
222
+
223
+ assets = Path(args.assets_dir).resolve()
224
+ if not assets.is_dir():
225
+ print(f"Error: '{assets}' is not a directory.", file=sys.stderr)
226
+ sys.exit(1)
227
+
228
+ result = apply_canonical(assets, args.component_type, args.canonical_slug, args.targets)
229
+
230
+ if "_error" in result:
231
+ print(f"Error: {result['_error']}", file=sys.stderr)
232
+ sys.exit(1)
233
+
234
+ print(json.dumps(result, ensure_ascii=False, indent=2))
235
+
236
+ replaced = sum(1 for v in result.values() if v == "replaced")
237
+ total = len(result)
238
+ print(f"\n--- {replaced}/{total} screens updated ---", file=sys.stderr)