@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.
- package/README.md +531 -0
- package/bin/aiskills.js +76 -0
- package/lib/cache.js +49 -0
- package/lib/cleanup.js +77 -0
- package/lib/commands/auto-update.js +131 -0
- package/lib/commands/doctor.js +139 -0
- package/lib/commands/list.js +77 -0
- package/lib/commands/skills.js +263 -0
- package/lib/commands/status.js +94 -0
- package/lib/commands/uninstall.js +182 -0
- package/lib/commands/update.js +149 -0
- package/lib/config.js +90 -0
- package/lib/downloader.js +110 -0
- package/lib/hooks.js +74 -0
- package/lib/installer.js +114 -0
- package/lib/platform.js +112 -0
- package/lib/prompts/checkboxCancel.js +264 -0
- package/lib/prompts/selectCancel.js +204 -0
- package/lib/symlink.js +154 -0
- package/lib/utils.js +49 -0
- package/package.json +61 -0
- package/skills/humaniza/SKILL.md +51 -0
- package/skills/humaniza/agents/openai.yaml +4 -0
- package/skills/humaniza/references/ai-patterns-es.md +51 -0
- package/skills/humaniza/references/checklist.md +9 -0
- package/skills/humaniza/references/examples.md +17 -0
- package/skills/humaniza/references/lexicon-es-mx.md +36 -0
- package/skills/humaniza/references/modes-es-mx.md +41 -0
- package/skills/humaniza/references/voice-es-mx.md +24 -0
- package/skills/refactoring-ui/SKILL.md +59 -0
- package/skills/refactoring-ui/references/01-design-process.md +72 -0
- package/skills/refactoring-ui/references/02-visual-hierarchy.md +84 -0
- package/skills/refactoring-ui/references/03-layout-spacing.md +69 -0
- package/skills/refactoring-ui/references/04-typography.md +70 -0
- package/skills/refactoring-ui/references/05-color.md +96 -0
- package/skills/refactoring-ui/references/06-depth-shadows.md +74 -0
- package/skills/refactoring-ui/references/07-images.md +75 -0
- package/skills/refactoring-ui/references/08-finishing-touches.md +91 -0
- package/skills/stitch-showcase/SKILL.md +411 -0
- package/skills/stitch-showcase/references/01-navbar.md +52 -0
- package/skills/stitch-showcase/references/02-hero.md +56 -0
- package/skills/stitch-showcase/references/03-design-system.md +102 -0
- package/skills/stitch-showcase/references/04-screen-gallery.md +102 -0
- package/skills/stitch-showcase/references/05-viewer-web.md +105 -0
- package/skills/stitch-showcase/references/06-viewer-mobile.md +104 -0
- package/skills/stitch-showcase/references/07-theme-system.md +77 -0
- package/skills/stitch-showcase/references/08-type-detection.md +81 -0
- package/skills/stitch-showcase/references/09-quality-standards.md +126 -0
- package/skills/stitch-showcase/references/10-component-standardization.md +40 -0
- package/skills/stitch-showcase/references/11-component-catalog.md +70 -0
- package/skills/stitch-showcase/references/catalog-template.html +841 -0
- package/skills/stitch-showcase/references/index.html +299 -0
- package/skills/stitch-showcase/references/viewer.html +412 -0
- package/skills/stitch-showcase/scripts/__pycache__/build_showcase.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/component_utils.cpython-313.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/detect_components.cpython-313.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/extract_catalog.cpython-313.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/extract_text.cpython-313.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/extract_zips.cpython-313.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/parse_design_md.cpython-313.pyc +0 -0
- package/skills/stitch-showcase/scripts/apply_canonical.py +238 -0
- package/skills/stitch-showcase/scripts/build_showcase.py +2103 -0
- package/skills/stitch-showcase/scripts/component_utils.py +398 -0
- package/skills/stitch-showcase/scripts/detect_components.py +284 -0
- package/skills/stitch-showcase/scripts/extract_catalog.py +913 -0
- package/skills/stitch-showcase/scripts/extract_text.py +268 -0
- package/skills/stitch-showcase/scripts/extract_zips.py +178 -0
- package/skills/stitch-showcase/scripts/parse_design_md.py +397 -0
- package/skills/vscode-extension-dev/SKILL.md +114 -0
- package/skills/vscode-extension-dev/references/api-patterns.md +625 -0
- package/skills/vscode-extension-dev/references/architecture.md +287 -0
- package/skills/vscode-extension-dev/references/package-json-schema.md +345 -0
- 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>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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)
|