blamcode 0.4.0__py3-none-any.whl

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 (58) hide show
  1. blamcode/__init__.py +9 -0
  2. blamcode/cli.py +122 -0
  3. blamcode/layer/README.md +43 -0
  4. blamcode/layer/config/agent/build.md +118 -0
  5. blamcode/layer/config/agent/debugger.md +32 -0
  6. blamcode/layer/config/agent/designer.md +31 -0
  7. blamcode/layer/config/agent/vision.md +26 -0
  8. blamcode/layer/config/agent/writer.md +28 -0
  9. blamcode/layer/config/command/ask.md +41 -0
  10. blamcode/layer/config/command/blam.md +24 -0
  11. blamcode/layer/config/command/explore.md +14 -0
  12. blamcode/layer/config/command/fix.md +14 -0
  13. blamcode/layer/config/command/open.md +18 -0
  14. blamcode/layer/config/command/review.md +14 -0
  15. blamcode/layer/config/opencode.json +53 -0
  16. blamcode/layer/config/themes/bangladeshi.json +67 -0
  17. blamcode/layer/config/themes/blamcode.json +217 -0
  18. blamcode/layer/config/tui.json +4 -0
  19. blamcode/layer/install.sh +752 -0
  20. blamcode/layer/scripts/__pycache__/patch-brand.cpython-312.pyc +0 -0
  21. blamcode/layer/scripts/blamcode +518 -0
  22. blamcode/layer/scripts/blamcode-browser +315 -0
  23. blamcode/layer/scripts/blamcode-menu +140 -0
  24. blamcode/layer/scripts/blamcode-uninstall +71 -0
  25. blamcode/layer/scripts/blamcode-vision +542 -0
  26. blamcode/layer/scripts/oc-settings.sh +138 -0
  27. blamcode/layer/scripts/patch-brand.py +267 -0
  28. blamcode/layer/skills/android-app/SKILL.md +53 -0
  29. blamcode/layer/skills/api-integration/SKILL.md +49 -0
  30. blamcode/layer/skills/bash-cli-expert/SKILL.md +48 -0
  31. blamcode/layer/skills/bot-development/SKILL.md +53 -0
  32. blamcode/layer/skills/clean-code-performance/SKILL.md +45 -0
  33. blamcode/layer/skills/database/SKILL.md +56 -0
  34. blamcode/layer/skills/debugging-fixes/SKILL.md +45 -0
  35. blamcode/layer/skills/deploy-hosting/SKILL.md +38 -0
  36. blamcode/layer/skills/docker/SKILL.md +74 -0
  37. blamcode/layer/skills/firebase-supabase/SKILL.md +61 -0
  38. blamcode/layer/skills/git-workflow/SKILL.md +63 -0
  39. blamcode/layer/skills/lets-scroll/SKILL.md +877 -0
  40. blamcode/layer/skills/lets-scroll/references/index-template.html +73 -0
  41. blamcode/layer/skills/lets-scroll/references/knockout.py +89 -0
  42. blamcode/layer/skills/lets-scroll/references/pipeline.md +312 -0
  43. blamcode/layer/skills/lets-scroll/references/prompts.md +194 -0
  44. blamcode/layer/skills/lets-scroll/references/scrub-engine.js +448 -0
  45. blamcode/layer/skills/project-structure/SKILL.md +74 -0
  46. blamcode/layer/skills/python-automation/SKILL.md +52 -0
  47. blamcode/layer/skills/react-next-best-practices/SKILL.md +54 -0
  48. blamcode/layer/skills/security-review/SKILL.md +48 -0
  49. blamcode/layer/skills/seo-basics/SKILL.md +44 -0
  50. blamcode/layer/skills/testing/SKILL.md +58 -0
  51. blamcode/layer/skills/ui-ux-responsive/SKILL.md +53 -0
  52. blamcode/layer/skills/website-builder/SKILL.md +47 -0
  53. blamcode-0.4.0.dist-info/METADATA +62 -0
  54. blamcode-0.4.0.dist-info/RECORD +58 -0
  55. blamcode-0.4.0.dist-info/WHEEL +5 -0
  56. blamcode-0.4.0.dist-info/entry_points.txt +2 -0
  57. blamcode-0.4.0.dist-info/licenses/LICENSE +21 -0
  58. blamcode-0.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,448 @@
1
+ /* ============================================================================
2
+ lets-scroll — portable scroll-scrubbed camera-flight engine
3
+ ----------------------------------------------------------------------------
4
+ Framework-agnostic. Vanilla JS, zero dependencies. It builds its own DOM and
5
+ injects its own (namespaced) CSS into a container you give it, so it drops into
6
+ plain HTML, Next.js (call from a ref/useEffect), Vue (onMounted), a server-
7
+ rendered page, anything.
8
+
9
+ USAGE
10
+ mountLetsScroll(document.getElementById('world'), {
11
+ brand: { name: 'Pearl & Co.', href: '#top' },
12
+ diveScroll: 1.3, // viewport-heights of scroll per dive clip
13
+ connScroll: 0.9, // ...per connector clip
14
+ hint: 'scroll to fly in',
15
+ nav: true, // show the top section nav
16
+ atmosphere: true, // subtle gradient + drifting particles behind the clips
17
+ sections: [
18
+ { id, label, still, stillMobile, clip, clipMobile, accent,
19
+ scroll: 1.6, // optional per-section override of diveScroll — more scroll
20
+ // distance = a slower, longer dwell in this scene
21
+ linger: 0.5, // optional 0..1 — remaps time so the camera settles mid-scene
22
+ // (exactly where the copy peaks) and moves quicker at the
23
+ // edges. 0 = linear (default). Keep ≤ 0.6; 1 = full pause.
24
+ eyebrow, title, body, tags:[…],
25
+ cta:{ primary:{label,href}, secondary:{label,href} } }, // last section only
26
+
27
+ ],
28
+ connectors: [clipUrl, …], // length = sections.length - 1 (nulls allowed)
29
+ connectorsMobile: [clipUrl, …], // optional lighter connectors for phones (same length)
30
+
31
+ MOBILE (the clipMobile/connectorsMobile variants are the opt-in mobile version;
32
+ the rest of the phone handling below is always on)
33
+ The engine is phone-aware out of the box: on a coarse-pointer / ≤860px viewport it
34
+ - loads `clipMobile` / `connectorsMobile` when provided (encode these smaller +
35
+ tighter-GOP — seek cost on a phone decoder is dominated by frames-from-keyframe,
36
+ so a 720p, -g 4 file scrubs far smoother than the 1080p desktop master; see
37
+ pipeline.md). Falls back to the desktop `clip` if no mobile variant is given.
38
+ - uses `stillMobile` as the scene poster when provided (pair it with native 9:16
39
+ clipMobile renders so the poster matches the portrait video's first frame instead
40
+ of flashing from a landscape crop). Chosen once at mount; a desktop resize into
41
+ phone width keeps the desktop poster (clips still switch via isMobile()).
42
+ - coalesces seeks (never issues a new currentTime while the decoder is still
43
+ `seeking`) so fast flicks can't pile up and freeze the video.
44
+ - keeps the still as a live poster until the clip actually paints its first frame,
45
+ and primes each video (muted play→pause) on first touch — this is what stops iOS
46
+ from showing a blank scene before the first seek.
47
+ - drops the drifting particles and ignores URL-bar-only resizes (no scroll jump).
48
+ Nothing here is required — a config with only `clip`/`connectors` still works on
49
+ phones; the mobile variants just make it lighter and smoother.
50
+
51
+ THEME (CSS custom properties; set on the container or :root to override)
52
+ --sw-bg page background (match your scene bg for seamless posters)
53
+ --sw-ink primary text
54
+ --sw-ink-soft secondary text
55
+ --sw-accent default accent (each section overrides via its `accent`)
56
+ --sw-font-display / --sw-font-body
57
+
58
+ REQUIREMENTS ON YOUR ASSETS
59
+ - clips encoded native-res, crf~20, -g 8, +faststart, no audio (see pipeline.md)
60
+ - connectors' endpoints are the neighbouring dives' ACTUAL frames (see SKILL Step 5)
61
+ - (optional) mobile variants at ~720p, -g 4 for smoother phone scrubbing
62
+ The engine loads each clip as a Blob (always seekable) and scrubs currentTime; it does
63
+ NOT depend on HTTP byte-range support.
64
+ ========================================================================== */
65
+
66
+ function mountLetsScroll(container, config) {
67
+ const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
68
+ // Phone detection. `coarse` is captured once (input type doesn't change mid-session);
69
+ // the ≤860px query is read live via isMobile() so a desktop resize/DevTools toggle
70
+ // switches sources and seek behaviour without a reload.
71
+ const coarse = window.matchMedia('(hover: none) and (pointer: coarse)').matches;
72
+ const smallMQ = window.matchMedia('(max-width: 860px)');
73
+ const isMobile = () => coarse || smallMQ.matches;
74
+ const SECTIONS = config.sections || [];
75
+ const CONNECTORS = config.connectors || [];
76
+ const CONNECTORS_M = config.connectorsMobile || [];
77
+ const DIVE_W = config.diveScroll || 1.3;
78
+ const CONN_W = config.connScroll || 0.9;
79
+ const CROSSFADE = (config.crossfade != null) ? config.crossfade : 0.12; // seam dissolve width (vh)
80
+ const N = SECTIONS.length;
81
+ if (!N) return;
82
+
83
+ injectCSS();
84
+ container.classList.add('sw-root');
85
+
86
+ // ---- build the interleaved segment chain: dive0, conn0, dive1, … diveN-1 ----
87
+ const SEGMENTS = [];
88
+ SECTIONS.forEach((s, i) => {
89
+ const dive = { kind: 'dive', si: i, clip: s.clip, clipM: s.clipMobile, still: s.still, stillM: s.stillMobile,
90
+ accent: s.accent, w: s.scroll || DIVE_W, linger: s.linger || 0 };
91
+ SEGMENTS.push(dive);
92
+ s._seg = dive;
93
+ // A connector is optional: if connectors[i] is falsy, the two dives simply
94
+ // crossfade directly (no fly-over). Lets a page complete even when a
95
+ // connector can't be generated (e.g. a content-filter false-positive).
96
+ if (i < N - 1 && CONNECTORS[i]) {
97
+ SEGMENTS.push({ kind: 'conn', si: i, clip: CONNECTORS[i], clipM: CONNECTORS_M[i],
98
+ still: SECTIONS[i + 1].still, stillM: SECTIONS[i + 1].stillMobile,
99
+ accent: SECTIONS[i + 1].accent, w: CONN_W });
100
+ }
101
+ });
102
+ const NSEG = SEGMENTS.length;
103
+
104
+ // ---- DOM ----
105
+ const sky = el('div', 'sw-sky');
106
+ if (config.atmosphere !== false) {
107
+ sky.appendChild(el('div', 'sw-sky__grad'));
108
+ sky.appendChild(el('div', 'sw-sky__glow'));
109
+ }
110
+ const particles = el('div', 'sw-particles'); sky.appendChild(particles);
111
+
112
+ const scrollbar = el('div', 'sw-scrollbar');
113
+ const scrollbarFill = el('span'); scrollbar.appendChild(scrollbarFill);
114
+
115
+ const topbar = el('div', 'sw-topbar');
116
+ if (config.brand) {
117
+ const brand = el('a', 'sw-brand'); brand.href = (config.brand.href || '#');
118
+ brand.appendChild(el('span', 'sw-brand__mark'));
119
+ const nm = el('span', 'sw-brand__name'); nm.textContent = config.brand.name || ''; brand.appendChild(nm);
120
+ topbar.appendChild(brand);
121
+ }
122
+ const nav = el('nav', 'sw-nav'); if (config.nav !== false) topbar.appendChild(nav);
123
+ if (config.cta && config.cta.label) {
124
+ const c = el('a', 'sw-topcta'); c.href = config.cta.href || '#'; c.textContent = config.cta.label;
125
+ topbar.appendChild(c);
126
+ }
127
+
128
+ const stage = el('div', 'sw-stage');
129
+ const copylayer = el('div', 'sw-copylayer');
130
+ const route = el('div', 'sw-route');
131
+ const hint = el('div', 'sw-hint');
132
+ const hintText = el('span'); hintText.textContent = config.hint || 'scroll'; hint.appendChild(hintText);
133
+ hint.appendChild(el('i'));
134
+ const track = el('div', 'sw-track');
135
+
136
+ [sky, scrollbar, topbar, stage, copylayer, route, hint, track].forEach(n => container.appendChild(n));
137
+
138
+ // segment scenes
139
+ SEGMENTS.forEach(s => {
140
+ const scene = el('div', 'sw-scene'); scene.style.setProperty('--sw-accent', s.accent || '');
141
+ const img = el('img', 'sw-scene__still'); img.alt = ''; img.decoding = 'async'; img.loading = 'lazy';
142
+ const poster = (isMobile() && s.stillM) ? s.stillM : s.still;
143
+ if (poster) img.src = poster;
144
+ scene.appendChild(img); stage.appendChild(scene);
145
+ s.el = scene; s.img = img; s.video = null; s.hasClip = false;
146
+ s.loading = false; s.ready = false; s.cur = 0; s.target = 0; s.visible = false;
147
+ });
148
+
149
+ // per-section copy / route / nav
150
+ const copies = [], dots = [];
151
+ SECTIONS.forEach((s, i) => {
152
+ const c = el('article', 'sw-copy'); c.style.setProperty('--sw-accent', s.accent || '');
153
+ c.innerHTML =
154
+ `<span class="sw-copy__num">${pad(i + 1)} / ${pad(N)}</span>` +
155
+ (s.eyebrow ? `<span class="sw-copy__eyebrow">${esc(s.eyebrow)}</span>` : '') +
156
+ (s.title ? `<h2 class="sw-copy__title">${esc(s.title)}</h2>` : '') +
157
+ (s.body ? `<p class="sw-copy__body">${esc(s.body)}</p>` : '') +
158
+ (s.tags && s.tags.length ? `<ul class="sw-copy__tags">${s.tags.map(t => `<li>${esc(t)}</li>`).join('')}</ul>` : '') +
159
+ (s.cta ? `<div class="sw-copy__cta">${ctaBtns(s.cta)}</div>` : '');
160
+ copylayer.appendChild(c); copies.push(c);
161
+
162
+ const dot = el('button', 'sw-route__dot'); dot.style.setProperty('--sw-accent', s.accent || '');
163
+ dot.innerHTML = `<span class="sw-route__label">${esc(s.label || '')}</span><i></i>`;
164
+ dot.addEventListener('click', () => jumpTo(i)); route.appendChild(dot); dots.push(dot);
165
+
166
+ if (config.nav !== false) {
167
+ const b = el('button', 'sw-nav__item'); b.textContent = s.label || '';
168
+ b.addEventListener('click', () => jumpTo(i)); nav.appendChild(b);
169
+ }
170
+ });
171
+
172
+ // ---- math ----
173
+ const clamp = (x, a = 0, b = 1) => Math.min(b, Math.max(a, x));
174
+ const smooth = x => { x = clamp(x); return x * x * (3 - 2 * x); };
175
+ // Per-section dwell: monotone remap of scroll→time so the camera settles mid-scene
176
+ // (where the copy peaks) and moves quicker near the seams. L=0 linear, L=1 full
177
+ // mid-scene pause. f(0)=0, f(1)=1 always, so seam frames are untouched.
178
+ const lingerEase = (x, L) => { L = clamp(L); const c = x - 0.5; return (1 - L) * x + L * (4 * c * c * c + 0.5); };
179
+ let vh = window.innerHeight, stageX = 0, totalW = 0, activeIndex = -1, ticking = false;
180
+ let laidOutW = window.innerWidth; // width the current layout was computed at (see onResize)
181
+
182
+ function layout() {
183
+ vh = window.innerHeight;
184
+ laidOutW = window.innerWidth;
185
+ stageX = window.innerWidth > 860 ? 4 : 0;
186
+ let off = 0;
187
+ SEGMENTS.forEach(s => { s.start = off * vh; off += s.w; s.end = off * vh; });
188
+ totalW = off;
189
+ track.style.height = (totalW * vh + vh) + 'px'; // +1vh so the last flight completes
190
+ read();
191
+ }
192
+
193
+ function jumpTo(i) {
194
+ const seg = SECTIONS[i]._seg;
195
+ window.scrollTo({ top: seg.start + (seg.end - seg.start) * 0.5, behavior: reduce ? 'auto' : 'smooth' });
196
+ }
197
+
198
+ function loadClip(s) {
199
+ // Under prefers-reduced-motion we never load the clips at all — the stills stay up
200
+ // and simply cross-dissolve as you scroll. No scrubbed video motion, no decode cost.
201
+ if (reduce || s.loading || !s.clip) return;
202
+ s.loading = true;
203
+ // Serve the lighter mobile encode on phones when one was provided.
204
+ const url = (isMobile() && s.clipM) ? s.clipM : s.clip;
205
+ fetch(url).then(r => r.ok ? r.blob() : Promise.reject(new Error('404')))
206
+ .then(blob => {
207
+ const v = document.createElement('video');
208
+ v.className = 'sw-scene__video';
209
+ v.muted = true; v.playsInline = true; v.preload = 'auto';
210
+ v.setAttribute('muted', ''); v.setAttribute('playsinline', '');
211
+ v.src = URL.createObjectURL(blob);
212
+ v.addEventListener('loadedmetadata', () => { s.ready = true; read(); });
213
+ // Reveal the video (hide the still poster) only once a real frame has
214
+ // painted — on iOS a seeked-but-never-played muted video stays blank, so
215
+ // hiding the still on metadata alone would flash an empty scene.
216
+ v.addEventListener('seeked', () => { s.el.classList.add('has-clip'); }, { once: true });
217
+ v.addEventListener('loadeddata', () => { try { v.pause(); } catch (e) {} if (userReady) primeVideo(v); });
218
+ s.el.appendChild(v); s.video = v; s.hasClip = true;
219
+ }).catch(() => { s.loading = false; });
220
+ }
221
+
222
+ function read() {
223
+ const y = window.scrollY || window.pageYOffset;
224
+ const fade = CROSSFADE * vh;
225
+ let ci = 0;
226
+ for (let i = 0; i < NSEG; i++) if (y >= SEGMENTS[i].start) ci = i;
227
+
228
+ for (let i = 0; i < NSEG; i++) {
229
+ const s = SEGMENTS[i];
230
+ if (y > s.start - 1.6 * vh && y < s.end + 1.6 * vh) loadClip(s);
231
+ const local = clamp((y - s.start) / (s.end - s.start), 0, 1);
232
+ s.target = s.linger ? lingerEase(local, s.linger) : local;
233
+ let outside = 0;
234
+ if (y < s.start) outside = s.start - y; else if (y > s.end) outside = y - s.end;
235
+ const op = smooth(1 - outside / fade);
236
+ s.el.style.opacity = op; s.visible = op > 0.001;
237
+ s.el.style.zIndex = (i === ci) ? '120' : String(100 + Math.round(op * 10));
238
+ if (!s.hasClip || !s.ready) {
239
+ const sc = reduce ? 1 : 1.03 + local * 0.14;
240
+ s.img.style.transform = `translateX(${stageX - 2}vw) scale(${sc.toFixed(3)})`;
241
+ }
242
+ }
243
+
244
+ for (let i = 0; i < N; i++) {
245
+ const seg = SECTIONS[i]._seg;
246
+ const pr = clamp((y - seg.start) / (seg.end - seg.start), 0, 1);
247
+ const before = y < seg.start, after = y > seg.end;
248
+ let cop;
249
+ if (i === 0) cop = after ? 0 : smooth(1 - pr / 0.62); // greets on landing
250
+ else if (i === N - 1) cop = before ? 0 : smooth(pr / 0.4); // holds CTA at the end
251
+ else cop = (before || after) ? 0 : smooth(1 - Math.abs(pr - 0.5) / 0.5);
252
+ const c = copies[i];
253
+ c.style.opacity = cop;
254
+ c.style.transform = reduce ? 'none' : `translateY(${(0.5 - pr) * 4}vh)`;
255
+ c.style.pointerEvents = cop > 0.5 ? 'auto' : 'none';
256
+ }
257
+
258
+ const cur = SEGMENTS[ci];
259
+ const near = clamp(cur.kind === 'dive' ? cur.si
260
+ : (((y - cur.start) / (cur.end - cur.start)) > 0.5 ? cur.si + 1 : cur.si), 0, N - 1);
261
+ if (near !== activeIndex) {
262
+ activeIndex = near;
263
+ dots.forEach((d, k) => d.classList.toggle('is-active', k === near));
264
+ nav.querySelectorAll('.sw-nav__item').forEach((n, k) => n.classList.toggle('is-active', k === near));
265
+ container.style.setProperty('--sw-accent', SECTIONS[near].accent || '');
266
+ }
267
+ scrollbarFill.style.transform = `scaleX(${clamp(y / (totalW * vh))})`;
268
+ hint.style.opacity = clamp(1 - y / (0.5 * vh));
269
+ if (particles) particles.style.transform = `translate3d(0, ${-y * 0.05}px, 0)`;
270
+ ticking = false;
271
+ }
272
+
273
+ function raf() {
274
+ const eps = isMobile() ? 0.02 : 0.008; // coarser seek step on phones = fewer decodes
275
+ for (let i = 0; i < NSEG; i++) {
276
+ const s = SEGMENTS[i];
277
+ if (!s.hasClip || !s.ready || !s.video) continue;
278
+ // Never queue a seek while the decoder is still resolving the last one.
279
+ // On phones a fast flick would otherwise pile up seeks and freeze the clip;
280
+ // cur keeps lerping, so we snap to the latest target the moment it's free.
281
+ if (s.video.seeking) continue;
282
+ if (!s.visible && Math.abs(s.cur - s.target) < 0.002) continue;
283
+ s.cur += (s.target - s.cur) * (reduce ? 1 : 0.18);
284
+ const dur = s.video.duration || 1;
285
+ const t = clamp(s.cur, 0, 0.999) * dur;
286
+ if (Math.abs(s.video.currentTime - t) > eps) { try { s.video.currentTime = t; } catch (e) {} }
287
+ }
288
+ requestAnimationFrame(raf);
289
+ }
290
+
291
+ // iOS needs a user gesture before a muted video will decode/paint reliably. On the
292
+ // first touch we prime every loaded clip (muted play→pause) so the first seek is
293
+ // instant instead of showing a blank frame. `userReady` also makes freshly-loaded
294
+ // clips prime themselves (see loadClip).
295
+ let userReady = false;
296
+ function primeVideo(v) {
297
+ if (!isMobile() || !v) return;
298
+ try { const p = v.play(); if (p && p.then) p.then(() => { try { v.pause(); } catch (e) {} }).catch(() => {}); }
299
+ catch (e) {}
300
+ }
301
+ function onFirstGesture() {
302
+ if (userReady) return;
303
+ userReady = true;
304
+ SEGMENTS.forEach(s => primeVideo(s.video));
305
+ }
306
+ window.addEventListener('pointerdown', onFirstGesture, { once: true, passive: true });
307
+ window.addEventListener('touchstart', onFirstGesture, { once: true, passive: true });
308
+
309
+ // Particles are a per-frame cost we can't afford alongside video scrubbing on a phone.
310
+ seedParticles(particles, reduce || coarse);
311
+ window.addEventListener('scroll', () => { if (!ticking) { ticking = true; requestAnimationFrame(read); } }, { passive: true });
312
+ // Mobile browsers fire `resize` every time the URL bar slides in/out. Re-running
313
+ // layout() there rebuilds the track height and yanks the scroll position, so on
314
+ // touch we ignore height-only changes and only relayout when the width actually
315
+ // changes (rotation still comes through orientationchange). layout() records the
316
+ // width it laid out at.
317
+ function onResize() {
318
+ if (coarse && window.innerWidth === laidOutW) return;
319
+ layout();
320
+ }
321
+ window.addEventListener('resize', onResize);
322
+ window.addEventListener('orientationchange', layout);
323
+ window.addEventListener('load', layout);
324
+ layout();
325
+ requestAnimationFrame(raf);
326
+
327
+ // ---- helpers ----
328
+ function el(tag, cls) { const n = document.createElement(tag); if (cls) n.className = cls; return n; }
329
+ function pad(n) { return String(n).padStart(2, '0'); }
330
+ function esc(s) { return String(s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])); }
331
+ function ctaBtns(cta) {
332
+ let h = '';
333
+ if (cta.primary) h += `<a class="sw-btn sw-btn--primary" href="${esc(cta.primary.href || '#')}">${esc(cta.primary.label)}</a>`;
334
+ if (cta.secondary) h += `<a class="sw-btn sw-btn--ghost" href="${esc(cta.secondary.href || '#')}">${esc(cta.secondary.label)}</a>`;
335
+ return h;
336
+ }
337
+ }
338
+
339
+ function seedParticles(host, reduce) {
340
+ if (!host || reduce) return;
341
+ const kinds = ['dot', 'dot', 'ring'];
342
+ const seeds = [7, 23, 41, 58, 71, 88, 12, 34, 52, 66, 83, 95, 18, 29, 47, 63, 77, 91, 5, 38, 55, 69, 82, 97];
343
+ for (let k = 0; k < 20; k++) {
344
+ const s = document.createElement('span');
345
+ s.className = 'sw-pt sw-pt--' + kinds[k % kinds.length];
346
+ s.style.left = seeds[k % seeds.length] + 'vw';
347
+ s.style.top = ((seeds[(k * 3) % seeds.length] * 1.3) % 100) + 'vh';
348
+ s.style.setProperty('--sw-sc', (0.5 + ((seeds[(k * 5) % seeds.length] % 60) / 60) * 1.1).toFixed(2));
349
+ const dur = 14 + (seeds[(k * 7) % seeds.length] % 22);
350
+ s.style.animationDuration = dur + 's';
351
+ s.style.animationDelay = (-(seeds[(k * 2) % seeds.length] % dur)) + 's';
352
+ host.appendChild(s);
353
+ }
354
+ }
355
+
356
+ function injectCSS() {
357
+ if (document.getElementById('sw-css')) return;
358
+ const css = `
359
+ .sw-root{--sw-bg:#F5EDE0;--sw-ink:#241d2b;--sw-ink-soft:#6a6072;--sw-accent:#8a7bb5;
360
+ --sw-font-display:ui-rounded,"SF Pro Rounded","Segoe UI",system-ui,sans-serif;
361
+ --sw-font-body:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,system-ui,sans-serif;
362
+ color:var(--sw-ink);font-family:var(--sw-font-body);}
363
+ html,body{margin:0;background:var(--sw-bg,#F5EDE0);overflow-x:hidden;}
364
+ .sw-sky{position:fixed;inset:0;z-index:0;overflow:hidden;pointer-events:none;background:var(--sw-bg);}
365
+ .sw-sky__grad{position:absolute;inset:-10%;background:linear-gradient(178deg,color-mix(in srgb,var(--sw-accent) 12%,var(--sw-bg)) 0%,var(--sw-bg) 55%,color-mix(in srgb,var(--sw-accent) 6%,var(--sw-bg)) 100%);}
366
+ .sw-sky__glow{position:absolute;inset:0;background:radial-gradient(60% 42% at 74% 16%,color-mix(in srgb,var(--sw-accent) 22%,transparent),transparent 70%),radial-gradient(46% 34% at 50% 50%,color-mix(in srgb,#fff 45%,transparent),transparent 70%);}
367
+ .sw-particles{position:absolute;inset:-6% -2%;will-change:transform;}
368
+ .sw-pt{position:absolute;width:13px;height:13px;transform:scale(var(--sw-sc,1));opacity:0;animation:sw-drift linear infinite;}
369
+ .sw-pt::before{content:"";position:absolute;inset:0;border-radius:50%;}
370
+ .sw-pt--dot::before{background:radial-gradient(circle at 34% 30%,color-mix(in srgb,var(--sw-accent) 60%,#000),#000 82%);}
371
+ .sw-pt--ring::before{background:transparent;border:2px solid color-mix(in srgb,var(--sw-accent) 55%,transparent);}
372
+ @keyframes sw-drift{0%{opacity:0;transform:scale(var(--sw-sc)) translate(0,12vh) rotate(0)}12%{opacity:.5}88%{opacity:.45}100%{opacity:0;transform:scale(var(--sw-sc)) translate(4vw,-22vh) rotate(210deg)}}
373
+ .sw-scrollbar{position:fixed;top:0;left:0;right:0;height:3px;z-index:60;background:color-mix(in srgb,var(--sw-accent) 14%,transparent);}
374
+ .sw-scrollbar span{display:block;height:100%;width:100%;transform-origin:0 50%;transform:scaleX(0);background:var(--sw-accent);}
375
+ .sw-topbar{position:fixed;top:0;left:0;right:0;z-index:50;display:flex;align-items:center;justify-content:space-between;gap:16px;padding:clamp(14px,2.4vw,26px) clamp(18px,5vw,64px);}
376
+ .sw-brand{display:flex;align-items:center;gap:10px;text-decoration:none;color:var(--sw-ink);}
377
+ .sw-brand__mark{width:24px;height:28px;border-radius:7px 7px 10px 10px;background:linear-gradient(160deg,var(--sw-accent),color-mix(in srgb,var(--sw-accent) 60%,#000));box-shadow:0 6px 14px color-mix(in srgb,var(--sw-accent) 40%,transparent);}
378
+ .sw-brand__name{font-family:var(--sw-font-display);font-weight:700;font-size:1.1rem;}
379
+ .sw-nav{display:flex;gap:4px;padding:5px;background:color-mix(in srgb,#fff 55%,transparent);backdrop-filter:blur(10px);border:1px solid color-mix(in srgb,var(--sw-accent) 16%,transparent);border-radius:999px;}
380
+ .sw-nav__item{font:inherit;font-size:.82rem;color:var(--sw-ink-soft);border:0;background:transparent;cursor:pointer;padding:7px 14px;border-radius:999px;transition:color .25s,background .25s;}
381
+ .sw-nav__item:hover{color:var(--sw-ink);} .sw-nav__item.is-active{color:#fff;background:var(--sw-accent);}
382
+ .sw-topcta{text-decoration:none;font-weight:600;font-size:.9rem;color:#fff;background:var(--sw-ink);padding:10px 20px;border-radius:999px;white-space:nowrap;}
383
+ .sw-stage{position:fixed;inset:0;z-index:10;pointer-events:none;}
384
+ .sw-scene{position:absolute;inset:0;opacity:0;overflow:hidden;will-change:opacity;}
385
+ .sw-scene__video,.sw-scene__still{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center 42%;}
386
+ .sw-scene__still{will-change:transform;} .sw-scene.has-clip .sw-scene__still{opacity:0;} .sw-scene__video{z-index:1;}
387
+ .sw-copylayer{position:fixed;inset:0;z-index:20;pointer-events:none;}
388
+ .sw-copylayer::before{content:"";position:absolute;inset:0;width:min(58vw,780px);background:linear-gradient(90deg,var(--sw-bg) 0%,color-mix(in srgb,var(--sw-bg) 82%,transparent) 34%,color-mix(in srgb,var(--sw-bg) 40%,transparent) 62%,transparent 100%);}
389
+ .sw-copy{position:absolute;left:clamp(18px,5vw,64px);top:50%;transform:translateY(-50%);width:min(42vw,460px);opacity:0;will-change:opacity,transform;}
390
+ .sw-copy__num{font-family:ui-monospace,Menlo,monospace;font-size:.74rem;letter-spacing:.12em;color:var(--sw-ink-soft);}
391
+ .sw-copy__eyebrow{display:block;margin-top:18px;font-family:var(--sw-font-display);font-weight:700;font-size:.8rem;letter-spacing:.16em;text-transform:uppercase;color:var(--sw-accent);}
392
+ .sw-copy__title{font-family:var(--sw-font-display);font-weight:700;color:var(--sw-ink);font-size:clamp(2rem,4.4vw,3.5rem);line-height:1.03;margin:12px 0 0;letter-spacing:-.01em;text-shadow:0 2px 20px color-mix(in srgb,var(--sw-bg) 70%,transparent);}
393
+ .sw-copy__body{margin-top:18px;font-size:clamp(1rem,1.25vw,1.14rem);line-height:1.55;color:color-mix(in srgb,var(--sw-ink) 78%,var(--sw-ink-soft));max-width:40ch;text-shadow:0 1px 12px color-mix(in srgb,var(--sw-bg) 90%,transparent);}
394
+ .sw-copy__tags{list-style:none;display:flex;flex-wrap:wrap;gap:8px;margin:24px 0 0;padding:0;}
395
+ .sw-copy__tags li{font-size:.82rem;font-weight:600;color:color-mix(in srgb,var(--sw-accent) 70%,#000);padding:7px 14px;border-radius:999px;background:color-mix(in srgb,var(--sw-accent) 14%,#fff);border:1px solid color-mix(in srgb,var(--sw-accent) 30%,transparent);}
396
+ .sw-copy__cta{display:flex;flex-wrap:wrap;gap:12px;margin-top:28px;pointer-events:auto;}
397
+ .sw-btn{text-decoration:none;font-weight:600;font-size:.95rem;padding:13px 24px;border-radius:999px;transition:transform .2s;}
398
+ .sw-btn--primary{color:#fff;background:var(--sw-ink);} .sw-btn--primary:hover{transform:translateY(-2px);}
399
+ .sw-btn--ghost{color:var(--sw-ink);border:1.5px solid color-mix(in srgb,var(--sw-ink) 25%,transparent);} .sw-btn--ghost:hover{transform:translateY(-2px);}
400
+ .sw-route{position:fixed;right:clamp(14px,2.4vw,30px);top:50%;z-index:40;transform:translateY(-50%);display:flex;flex-direction:column;gap:22px;padding:18px 10px;}
401
+ .sw-route::before{content:"";position:absolute;left:50%;top:22px;bottom:22px;width:2px;transform:translateX(-50%);background:var(--sw-accent);opacity:.28;}
402
+ .sw-route__dot{position:relative;border:0;background:transparent;cursor:pointer;width:14px;height:14px;display:grid;place-items:center;}
403
+ .sw-route__dot i{width:9px;height:9px;border-radius:50%;background:color-mix(in srgb,var(--sw-accent) 40%,transparent);transition:transform .3s,background .3s,box-shadow .3s;}
404
+ .sw-route__dot:hover i{transform:scale(1.25);background:var(--sw-accent);}
405
+ .sw-route__dot.is-active i{background:var(--sw-accent);transform:scale(1.4);box-shadow:0 0 0 5px color-mix(in srgb,var(--sw-accent) 22%,transparent);}
406
+ .sw-route__label{position:absolute;right:24px;top:50%;transform:translateY(-50%) translateX(6px);white-space:nowrap;font-size:.78rem;font-weight:600;color:var(--sw-ink);background:color-mix(in srgb,#fff 85%,transparent);backdrop-filter:blur(6px);padding:5px 11px;border-radius:999px;opacity:0;pointer-events:none;transition:opacity .25s,transform .25s;border:1px solid color-mix(in srgb,var(--sw-accent) 14%,transparent);}
407
+ .sw-route__dot:hover .sw-route__label,.sw-route__dot.is-active .sw-route__label{opacity:1;transform:translateY(-50%) translateX(0);}
408
+ .sw-hint{position:fixed;left:50%;bottom:26px;z-index:30;transform:translateX(-50%);display:flex;flex-direction:column;align-items:center;gap:10px;font-size:.76rem;letter-spacing:.14em;text-transform:uppercase;color:var(--sw-ink-soft);transition:opacity .3s;}
409
+ .sw-hint i{width:22px;height:34px;border-radius:12px;border:2px solid color-mix(in srgb,var(--sw-ink) 28%,transparent);position:relative;}
410
+ .sw-hint i::after{content:"";position:absolute;left:50%;top:7px;width:4px;height:7px;border-radius:2px;background:var(--sw-accent);transform:translateX(-50%);animation:sw-wheel 1.7s ease-in-out infinite;}
411
+ @keyframes sw-wheel{0%{opacity:0;top:6px}40%{opacity:1}100%{opacity:0;top:17px}}
412
+ .sw-track{position:relative;z-index:1;width:100%;pointer-events:none;}
413
+ @media (max-width:860px){
414
+ .sw-nav{display:none;}
415
+ .sw-copylayer::before{width:100%;height:60%;top:auto;bottom:0;background:linear-gradient(0deg,var(--sw-bg) 8%,color-mix(in srgb,var(--sw-bg) 70%,transparent) 46%,transparent 100%);}
416
+ /* Anchor copy to the bottom, clear of the home indicator / collapsing URL bar.
417
+ dvh + env() are progressive: browsers that lack them keep the vh fallback line. */
418
+ .sw-copy{left:clamp(18px,5vw,64px);right:clamp(18px,5vw,64px);top:auto;bottom:clamp(64px,14vh,120px);transform:none;width:auto;max-width:560px;}
419
+ .sw-copy{bottom:calc(clamp(56px,12dvh,110px) + env(safe-area-inset-bottom));}
420
+ .sw-copy__title{font-size:clamp(1.9rem,7.5vw,2.7rem);}
421
+ .sw-copy__body{max-width:none;font-size:clamp(.98rem,3.6vw,1.1rem);} .sw-scene__video,.sw-scene__still{object-position:center 46%;}
422
+ .sw-hint{bottom:calc(20px + env(safe-area-inset-bottom));}
423
+ .sw-route{gap:16px;right:6px;} .sw-route__label{display:none;}
424
+ }
425
+ /* Portrait phones crop a 16:9 clip hard; keep the framing centred so the focal
426
+ subject (which the camera dives toward) stays in view. */
427
+ @media (max-width:860px) and (orientation:portrait){
428
+ .sw-scene__video,.sw-scene__still{object-position:center 44%;}
429
+ }
430
+ /* Touch: give the route dots a finger-sized hit area without growing the visible dot. */
431
+ @media (hover:none) and (pointer:coarse){
432
+ .sw-route{padding:14px 6px;}
433
+ .sw-route__dot{width:28px;height:28px;}
434
+ .sw-btn{padding:15px 26px;}
435
+ }
436
+ @media (prefers-reduced-motion:reduce){ .sw-hint i::after{animation:none;} .sw-pt{display:none;} }
437
+ `;
438
+ // Wrap in a cascade layer so the page's own theme tokens (unlayered
439
+ // :root / .sw-root { --sw-bg / --sw-ink / --sw-accent … }) always win over
440
+ // these defaults, regardless of injection order. Enables clean dark themes.
441
+ const style = document.createElement('style'); style.id = 'sw-css';
442
+ style.textContent = '@layer sw {\n' + css + '\n}';
443
+ document.head.appendChild(style);
444
+ }
445
+
446
+ // Expose for module + global use.
447
+ if (typeof module !== 'undefined' && module.exports) module.exports = { mountLetsScroll };
448
+ if (typeof window !== 'undefined') window.mountLetsScroll = mountLetsScroll;
@@ -0,0 +1,74 @@
1
+ ---
2
+ name: project-structure
3
+ description: Clean, scalable project folder structure — before starting a new project. Use when starting any new project or organizing an existing one.
4
+ ---
5
+
6
+ # Project Structure Skill (Full-Power)
7
+
8
+ ## Decision tree (decide this first — don't cross-question the user)
9
+ - 1-3 page simple site → **Flat HTML** (fastest!)
10
+ - Multi-page site, no build tool → **HTML + css/ + js/**
11
+ - Interactive app → **React + Vite**
12
+ - SEO-critical + full app → **Next.js**
13
+
14
+ ## Simple Website (HTML + CSS + JS)
15
+ ```
16
+ project/
17
+ ├── index.html
18
+ ├── about.html / contact.html ...
19
+ ├── css/style.css # ONE file — not 3 css files
20
+ ├── js/main.js # ONE file
21
+ ├── assets/images/
22
+ └── README.md # how to run it (one line)
23
+ ```
24
+
25
+ ## React + Vite
26
+ ```
27
+ project/
28
+ ├── public/ # static (favicon, images)
29
+ ├── src/
30
+ │ ├── components/ # small reusable (Navbar, Card)
31
+ │ ├── pages/ # route-level
32
+ │ ├── hooks/ # custom logic
33
+ │ ├── lib/ # api calls, utils
34
+ │ ├── App.jsx
35
+ │ └── main.jsx
36
+ ├── index.html
37
+ ├── package.json
38
+ └── .gitignore # node_modules, .env, dist
39
+ ```
40
+
41
+ ## Next.js (App Router)
42
+ ```
43
+ project/
44
+ ├── app/
45
+ │ ├── layout.tsx # root layout + fonts + metadata
46
+ │ ├── page.tsx
47
+ │ ├── globals.css
48
+ │ └── (dashboard)/ # route groups
49
+ ├── components/
50
+ ├── lib/ # db, api, auth
51
+ ├── public/
52
+ ├── types/
53
+ ├── .env.local # secrets (gitignored!)
54
+ ├── .env.example # template — commit this
55
+ └── package.json
56
+ ```
57
+
58
+ ## Python project
59
+ ```
60
+ project/
61
+ ├── main.py
62
+ ├── requirements.txt # pip freeze > requirements.txt
63
+ ├── .env # secrets (gitignored)
64
+ ├── utils/
65
+ └── README.md
66
+ ```
67
+
68
+ ## Rules
69
+ - Set up the structure FIRST — don't invent folders while creating files
70
+ - Don't pile files in `.` — group related files into folders
71
+ - Names: lowercase-kebab (`user-profile/`) or PascalCase components — pick one style
72
+ - Every project needs a README (one line: how to run) + `.gitignore`
73
+ - Keep data/output separate from source: `data/`, `output/` (gitignore output)
74
+ - Deep nesting (4+ levels) breaks structure signals — flatten it
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: python-automation
3
+ description: Automate daily tasks with Python — file organization, data processing, CSV/Excel, scraping, bots. Use when user says "write a script", "make this automatic", "organize these files", or wants automation.
4
+ ---
5
+
6
+ # Python Automation Skill
7
+
8
+ ## Termux setup
9
+ - `pkg install python` → `pip install <package>` works
10
+ - To access `/storage/emulated/0/` files, run `termux-setup-storage` first
11
+
12
+ ## Common automation patterns
13
+
14
+ ### File organizer (cleaning Downloads — very common request)
15
+ ```python
16
+ from pathlib import Path
17
+ import shutil
18
+ FOLDERS = {'.jpg': 'Pictures', '.png': 'Pictures', '.pdf': 'Documents',
19
+ '.mp4': 'Videos', '.mp3': 'Music', '.apk': 'APKs'}
20
+ src = Path('/storage/emulated/0/Download')
21
+ for f in src.iterdir():
22
+ if f.is_file() and not f.name.startswith('.'):
23
+ dest = src / FOLDERS.get(f.suffix.lower(), 'Others')
24
+ dest.mkdir(exist_ok=True)
25
+ shutil.move(str(f), str(dest / f.name))
26
+ ```
27
+
28
+ ### CSV → Excel report (openpyxl), bulk rename, folder backup —
29
+ same pattern: pathlib + shutil, dry-run first (print what it will do),
30
+ then the actual move.
31
+
32
+ ### Web scraping (requests + beautifulsoup4)
33
+ - Static pages: requests + bs4
34
+ - JS-rendered: playwright (heavy on Termux — desktop/proot is better)
35
+ - **Ethics/Rules:** respect robots.txt, `time.sleep(1)` between requests,
36
+ no personal data scraping, no login bypass — public data only
37
+
38
+ ## Script quality rules
39
+ - Every script: `if __name__ == '__main__':` pattern
40
+ - DRY-RUN flag: `python3 script.py --dry` — shows what will happen before the real run
41
+ - Log prints: say what it is doing (file count, skip count)
42
+ - On errors, continue to the next file instead of crashing (`try/except` per-file)
43
+ - English comments + usage instructions in the docstring
44
+
45
+ ## Scheduled runs (Termux)
46
+ - `pkg install cronie termux-services` → crontab
47
+ - The Termux app must stay in the background (tell the user to disable battery optimization)
48
+ - For simple cases, a `while True: ... time.sleep(3600)` script also works
49
+
50
+ ## Deliver
51
+ - The script file + how to run it (one line)
52
+ - Confirm before any dangerous operation (delete/move)
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: react-next-best-practices
3
+ description: React / Next.js — clean, scalable, performant apps with modern best practices. Use for any React or Next.js task.
4
+ ---
5
+
6
+ # React + Next.js Best Practices (Full-Power)
7
+
8
+ ## Component rules
9
+ - Functional components + hooks only — no class components
10
+ - Keep them small: one component does one thing; 150+ lines = split it
11
+ - Separate logic from UI: extract custom hooks (`useUserData`)
12
+ - Prop drilling beyond 3 levels = Context or a state lib (zustand is light and good)
13
+ - Early return pattern:
14
+ ```jsx
15
+ if (!user) return <Login />;
16
+ return <Dashboard user={user} />;
17
+ ```
18
+
19
+ ## State discipline
20
+ - Server data = a library (TanStack Query) — not your own useState+useEffect
21
+ - Local UI state in useState; keep global state minimal
22
+ - Don't store derived values in state — compute them in render
23
+ - useEffect: only for sync (with external systems). Never for data fetching.
24
+
25
+ ## Next.js (App Router)
26
+ - Server Components by default — `"use client"` only where interactivity lives
27
+ - Data fetching: await directly in an async Server Component — no API route needed
28
+ - Mutations: Server Actions
29
+ - `next/image` (with width/height), `next/font`, `next/link`
30
+ - Metadata: `export const metadata` in layout — per-page override
31
+ - Loading/error UI: `loading.tsx`, `error.tsx` files — never a blank screen for the user
32
+
33
+ ## Forms + validation
34
+ - Controlled inputs + inline validation (before submit)
35
+ - Zod-type schema validation on submit — same schema on the server side
36
+
37
+ ## Performance (phone users — take this seriously)
38
+ - Lists: stable `key` (not index, especially dynamic lists)
39
+ - Lazy-load big routes: `next/dynamic`, `React.lazy`
40
+ - Images: next/image + proper sizes — lazy everything except the hero
41
+ - Bundle: analyze after `npm run build` — check dependencies if it's big
42
+ - Re-renders: keep props stable (useCallback only on measured problems —
43
+ not everywhere)
44
+
45
+ ## TypeScript
46
+ - `any` is a red flag — use `unknown` + narrowing when needed
47
+ - Types co-located or in `types/`; export shared prop types
48
+ - API response types defined separately — no inline duplication
49
+
50
+ ## Common bugs guard
51
+ - Index as `key` (ghost bugs when reordering)
52
+ - Missing effect cleanup (event listener leaks — memory hits on phones)
53
+ - Conditional hook calls (rules of hooks)
54
+ - Stale closures in `useEffect` — keep dependencies right