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.
- blamcode/__init__.py +9 -0
- blamcode/cli.py +122 -0
- blamcode/layer/README.md +43 -0
- blamcode/layer/config/agent/build.md +118 -0
- blamcode/layer/config/agent/debugger.md +32 -0
- blamcode/layer/config/agent/designer.md +31 -0
- blamcode/layer/config/agent/vision.md +26 -0
- blamcode/layer/config/agent/writer.md +28 -0
- blamcode/layer/config/command/ask.md +41 -0
- blamcode/layer/config/command/blam.md +24 -0
- blamcode/layer/config/command/explore.md +14 -0
- blamcode/layer/config/command/fix.md +14 -0
- blamcode/layer/config/command/open.md +18 -0
- blamcode/layer/config/command/review.md +14 -0
- blamcode/layer/config/opencode.json +53 -0
- blamcode/layer/config/themes/bangladeshi.json +67 -0
- blamcode/layer/config/themes/blamcode.json +217 -0
- blamcode/layer/config/tui.json +4 -0
- blamcode/layer/install.sh +752 -0
- blamcode/layer/scripts/__pycache__/patch-brand.cpython-312.pyc +0 -0
- blamcode/layer/scripts/blamcode +518 -0
- blamcode/layer/scripts/blamcode-browser +315 -0
- blamcode/layer/scripts/blamcode-menu +140 -0
- blamcode/layer/scripts/blamcode-uninstall +71 -0
- blamcode/layer/scripts/blamcode-vision +542 -0
- blamcode/layer/scripts/oc-settings.sh +138 -0
- blamcode/layer/scripts/patch-brand.py +267 -0
- blamcode/layer/skills/android-app/SKILL.md +53 -0
- blamcode/layer/skills/api-integration/SKILL.md +49 -0
- blamcode/layer/skills/bash-cli-expert/SKILL.md +48 -0
- blamcode/layer/skills/bot-development/SKILL.md +53 -0
- blamcode/layer/skills/clean-code-performance/SKILL.md +45 -0
- blamcode/layer/skills/database/SKILL.md +56 -0
- blamcode/layer/skills/debugging-fixes/SKILL.md +45 -0
- blamcode/layer/skills/deploy-hosting/SKILL.md +38 -0
- blamcode/layer/skills/docker/SKILL.md +74 -0
- blamcode/layer/skills/firebase-supabase/SKILL.md +61 -0
- blamcode/layer/skills/git-workflow/SKILL.md +63 -0
- blamcode/layer/skills/lets-scroll/SKILL.md +877 -0
- blamcode/layer/skills/lets-scroll/references/index-template.html +73 -0
- blamcode/layer/skills/lets-scroll/references/knockout.py +89 -0
- blamcode/layer/skills/lets-scroll/references/pipeline.md +312 -0
- blamcode/layer/skills/lets-scroll/references/prompts.md +194 -0
- blamcode/layer/skills/lets-scroll/references/scrub-engine.js +448 -0
- blamcode/layer/skills/project-structure/SKILL.md +74 -0
- blamcode/layer/skills/python-automation/SKILL.md +52 -0
- blamcode/layer/skills/react-next-best-practices/SKILL.md +54 -0
- blamcode/layer/skills/security-review/SKILL.md +48 -0
- blamcode/layer/skills/seo-basics/SKILL.md +44 -0
- blamcode/layer/skills/testing/SKILL.md +58 -0
- blamcode/layer/skills/ui-ux-responsive/SKILL.md +53 -0
- blamcode/layer/skills/website-builder/SKILL.md +47 -0
- blamcode-0.4.0.dist-info/METADATA +62 -0
- blamcode-0.4.0.dist-info/RECORD +58 -0
- blamcode-0.4.0.dist-info/WHEEL +5 -0
- blamcode-0.4.0.dist-info/entry_points.txt +2 -0
- blamcode-0.4.0.dist-info/licenses/LICENSE +21 -0
- 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 => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[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
|