aura-components-mcp 1.5.3 → 1.6.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 +12 -1
- package/dist/server.js +164 -24
- package/flow/app.js +175 -37
- package/flow/compare.html +54 -18
- package/flow/docs.html +65 -18
- package/flow/how.html +55 -18
- package/flow/index.html +82 -19
- package/flow/setup.html +67 -19
- package/flow/styles.css +372 -81
- package/flow/tools.html +72 -19
- package/package.json +2 -1
- package/flow/b-compare.html +0 -1
- package/flow/b-docs.html +0 -1
- package/flow/b-how.html +0 -1
- package/flow/b-index.html +0 -1
- package/flow/b-setup.html +0 -4
- package/flow/b-tools.html +0 -1
- package/flow/hero-c.html +0 -1
- package/flow/logo-c.html +0 -1
- package/flow/shell-a.html +0 -7
- package/flow/shell-b.html +0 -7
- package/flow/shell-c.html +0 -12
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
> I got tired of opening twenty Aura tabs every time I started a landing page. So I built the MCP server I wanted: every free thing on aura.build, one stdio call away, no account, no key, no browser.
|
|
10
10
|
|
|
11
|
-
> **v1.
|
|
11
|
+
> **v1.6.0** — 20 tools. Search facets now equal detail facets (icons, fonts, weight), skills deduped by source_url, full DESIGN token surface with preview-mode warnings, legal asset plans, scoped CSS + paginated installs, transparent fallbacks. Search never empties (OR-token fallback + suggested queries), bulk-fetch with `aura_bulk_fetch` (`aura_bundle` alias),undle`, one-turn page builds with `aura_scaffold_page`, dark/light faceting, related items, legal asset install.
|
|
12
12
|
|
|
13
13
|
Point any MCP client at `dist/server.js` and your agent can search 2,495 free components, read 187 agent skills in full, pull 30,688 assets, and apply 725 DESIGN.md systems. It answers in seconds because there is nothing to log into and almost nothing to download: one bundled JS file, zero dependencies.
|
|
14
14
|
|
|
@@ -127,6 +127,17 @@ npm run build # rebundle src/*.mjs into dist/server.js
|
|
|
127
127
|
|
|
128
128
|
Layout: `src/config.mjs` holds env parsing. `src/http.mjs` hides timeouts and retries behind an injectable fetch. `src/catalog.mjs` is the deep module (queries, cache, shaping, counts). `src/guide.mjs` turns rows into install plans. `src/tools.mjs` validates args. `src/protocol.mjs` speaks JSON-RPC. `src/server.mjs` only wires stdio.
|
|
129
129
|
|
|
130
|
+
## Share
|
|
131
|
+
|
|
132
|
+
Star it if it saved you a tab or twenty: [github.com/swastiksingh-dev/Aura-Components-MCP](https://github.com/swastiksingh-dev/Aura-Components-MCP). Reposits welcome — see [DIRECTION.md](./DIRECTION.md) for where this is headed.
|
|
133
|
+
|
|
134
|
+
```text
|
|
135
|
+
built a free design MCP on aura.build: 2495 components, 187 skills, 30k assets, 20 tools. no login. resources are @MengTo's
|
|
136
|
+
|
|
137
|
+
npx -y aura-components-mcp
|
|
138
|
+
https://github.com/swastiksingh-dev/Aura-Components-MCP
|
|
139
|
+
```
|
|
140
|
+
|
|
130
141
|
## Credits and license
|
|
131
142
|
|
|
132
143
|
Server code by [swastiksingh-dev](https://github.com/swastiksingh-dev), MIT. Free to fork, use, and sell with. Catalogue text, code, and images belong to Aura and the creators listed in each `author` field; follow Aura's terms for that content. If you fork this, keep the credit line and point people at the original repo.
|
package/dist/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// aura-components-mcp v1.
|
|
2
|
+
// aura-components-mcp v1.6.0 — bundled (zero deps). Built by swastiksingh-dev.
|
|
3
3
|
|
|
4
4
|
// ---- config.mjs ----
|
|
5
5
|
// Module: config — one small interface (loadConfig) over all env parsing.
|
|
@@ -102,9 +102,9 @@ const CATALOG_SORTS = {
|
|
|
102
102
|
};
|
|
103
103
|
|
|
104
104
|
const LIST_COLS = {
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
components: "id,title,description,tags,premium,views,forks,slug,background,created_by,created_at,updated_at",
|
|
105
|
+
// PostgREST can't substring server-side: lists fetch code but shapeRow truncates to
|
|
106
|
+
// a 600-char probe for facets BEFORE the truncation marker (see shapeRow). ~0.7KB/row.
|
|
107
|
+
components: "id,title,description,tags,code,premium,views,forks,slug,background,created_by,created_at,updated_at",
|
|
108
108
|
skills: "id,title,description,source_url,views,forks,featured,created_by,created_at,updated_at",
|
|
109
109
|
assets: "id,title,description,keywords,media_type,premium,views,forks,image_800w,video_url,video_poster_url,created_by,created_at",
|
|
110
110
|
design_systems: "id,slug,title,description,views,forks,featured,created_by,created_at,updated_at",
|
|
@@ -176,20 +176,42 @@ const trunc = (s, n) => (s && s.length > n ? s.slice(0, n) + "…[truncated]" :
|
|
|
176
176
|
function pageUrl(kind, row) {
|
|
177
177
|
if (kind === "components") return SITE + "/component/" + row.slug;
|
|
178
178
|
if (kind === "skills") return SITE + "/skills/" + row.id;
|
|
179
|
+
// ISS9: asset pages are query-routed (no /assets/ID route exists). Keep the q= form
|
|
180
|
+
// but ALSO expose a stable key so agents can cite: id + slug + title together.
|
|
179
181
|
if (kind === "assets") return SITE + "/assets?q=" + encodeURIComponent(row.title ?? "");
|
|
180
182
|
return SITE + "/design-systems/" + (row.slug ?? row.id);
|
|
181
183
|
}
|
|
182
184
|
|
|
185
|
+
function assetKey(row) {
|
|
186
|
+
return { id: row.id, slug: row.slug ?? null, title: row.title ?? null };
|
|
187
|
+
}
|
|
188
|
+
|
|
183
189
|
function shapeRow(kind, row, cfg, withFacets) {
|
|
184
190
|
const base = { ...row };
|
|
185
|
-
|
|
191
|
+
// Facets probe the first 600 chars of FULL code before truncation, so search facets
|
|
192
|
+
// equal detail facets for icons/fonts/theme. Weight uses the true length (ISS1/2/3).
|
|
193
|
+
if (kind === "components" && typeof base.code === "string") {
|
|
194
|
+
base.code_chars = base.code.length;
|
|
195
|
+
if (withFacets) { try { base.facets = withFacets(kind, { ...base, code: base.code.slice(0, 600) }, { fullLength: base.code.length }); } catch { /* never break rows */ } }
|
|
196
|
+
base.code = trunc(base.code, cfg.codeChars);
|
|
197
|
+
}
|
|
186
198
|
if (kind === "skills" && typeof base.content === "string") base.content = trunc(base.content, cfg.contentChars);
|
|
187
199
|
if (kind === "design_systems") {
|
|
188
200
|
if (typeof base.content === "string") base.content = trunc(base.content, cfg.contentChars);
|
|
189
201
|
if (typeof base.preview_html === "string") base.preview_html = trunc(base.preview_html, cfg.codeChars);
|
|
190
202
|
}
|
|
191
203
|
base.page_url = pageUrl(kind, row);
|
|
192
|
-
if (
|
|
204
|
+
if (kind === "assets") base.asset_key = assetKey(row);
|
|
205
|
+
if (withFacets && !base.facets) { try { base.facets = withFacets(kind, base); } catch { /* facets never break rows */ } }
|
|
206
|
+
// ISS8: force absolute Supabase URLs — relative variant paths break fetch.
|
|
207
|
+
if (kind === "assets") {
|
|
208
|
+
const abs = (u) => (!u ? u : (/^https?:\/\//i.test(u) ? u : ("https://hoirqrkdgbmvpwutwuwj-all.supabase.co/storage/v1/object/public/assets/" + String(u).replace(/^\/+/, ""))));
|
|
209
|
+
for (const k of ["image_320w", "image_800w", "image_1600w", "image_3840w", "image_original", "image_url", "video_url", "video_poster_url"]) if (typeof base[k] === "string") base[k] = abs(base[k]);
|
|
210
|
+
if (base.facets) {
|
|
211
|
+
if (base.facets.download) base.facets.download = abs(base.facets.download);
|
|
212
|
+
if (base.facets.preview) base.facets.preview = abs(base.facets.preview);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
193
215
|
return base;
|
|
194
216
|
}
|
|
195
217
|
|
|
@@ -278,15 +300,39 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
|
|
|
278
300
|
} catch { /* keep original empty result */ }
|
|
279
301
|
}
|
|
280
302
|
}
|
|
303
|
+
// ISS7: dedupe skills by source_url (same SKILL.md listed twice). First (most-viewed)
|
|
304
|
+
// wins, gets canonical:true; dupes are dropped and counted in deduped.
|
|
305
|
+
let deduped = 0;
|
|
306
|
+
if (kind === "skills") {
|
|
307
|
+
const seen = new Set();
|
|
308
|
+
const kept = [];
|
|
309
|
+
for (const r of rows) {
|
|
310
|
+
const u = (r.source_url || "").trim().toLowerCase();
|
|
311
|
+
if (u && seen.has(u)) { deduped++; continue; }
|
|
312
|
+
if (u) seen.add(u);
|
|
313
|
+
kept.push(r);
|
|
314
|
+
}
|
|
315
|
+
const byUrl = new Map();
|
|
316
|
+
for (const r of kept) {
|
|
317
|
+
const u = (r.source_url || "").trim().toLowerCase();
|
|
318
|
+
if (u && (!byUrl.has(u) || (r.views || 0) > (byUrl.get(u).views || 0))) byUrl.set(u, r);
|
|
319
|
+
}
|
|
320
|
+
rows = kept.map((r) => {
|
|
321
|
+
const u = (r.source_url || "").trim().toLowerCase();
|
|
322
|
+
return (u && byUrl.get(u) === r) ? { ...r, canonical: true } : r;
|
|
323
|
+
});
|
|
324
|
+
}
|
|
281
325
|
const enriched = await enrichAuthors(rows);
|
|
282
326
|
const out = { kind, total, limit, offset, items: enriched.map((r) => shapeRow(kind, r, config, facetsFn)), cached: false };
|
|
327
|
+
if (deduped) out.deduped = deduped;
|
|
283
328
|
// Post-filter: theme (dark|light) is derived, not a column — filter shaped rows.
|
|
284
329
|
if (q.theme && (kind === "components")) {
|
|
285
330
|
const want = String(q.theme).toLowerCase();
|
|
286
331
|
out.items = out.items.filter((it) => (it.facets && it.facets.theme) === want);
|
|
287
332
|
}
|
|
288
|
-
|
|
289
|
-
|
|
333
|
+
// ISS15: transparent fallback — agents see what happened, not a magic string.
|
|
334
|
+
if (fallback) { out.fallback = fallback; out.isFallback = true; out.fallback_score = "token-overlap:" + toks.length + "-terms"; }
|
|
335
|
+
else if ((!rows || !rows.length) && toks.length) { out.isFallback = false; out.suggested_queries = suggest(toks).filter((s) => !/^[a-z0-9]{8,}$/i.test(s.replace(/\s/g, ""))); if (!out.suggested_queries.length) out.suggested_queries = ["hero", "pricing", "landing page"]; }
|
|
290
336
|
cacheSet(key, out);
|
|
291
337
|
return out;
|
|
292
338
|
})();
|
|
@@ -365,7 +411,8 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
|
|
|
365
411
|
}
|
|
366
412
|
|
|
367
413
|
// Related: same-tag / same-text overlap, views-ranked, excluding self.
|
|
368
|
-
|
|
414
|
+
// ISS10: freeOnly default true — free flows never get Pro recommendations.
|
|
415
|
+
async function relatedItems(kind, idOrSlug, limit = 3, freeOnly = true) {
|
|
369
416
|
const got = await getItem(kind, idOrSlug);
|
|
370
417
|
const item = got.item;
|
|
371
418
|
const toks = queryTokens(item.title + " " + (item.description || "")).slice(0, 3);
|
|
@@ -375,7 +422,8 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
|
|
|
375
422
|
const sel = LIST_COLS[kind];
|
|
376
423
|
const idCol = (kind === "components") ? "id" : "id";
|
|
377
424
|
const selfId = encodeURIComponent(String(item.id));
|
|
378
|
-
const
|
|
425
|
+
const premF = (freeOnly && (kind === "components" || kind === "assets")) ? "&premium=eq.false" : "";
|
|
426
|
+
const url = base + "/rest/v1/" + kind + "?select=" + encodeURIComponent(sel) + "&private=eq.false" + premF + tagF + "&" + idCol + "=neq." + selfId + (orParts.length ? "&or=(" + orParts.join(",") + ")" : "") + "&order=views.desc&limit=" + lim;
|
|
379
427
|
try {
|
|
380
428
|
const { rows } = await fetcher.getJson(url, headers);
|
|
381
429
|
const enriched = await enrichAuthors(rows);
|
|
@@ -387,10 +435,10 @@ function createCatalog({ fetcher, config, now = () => Date.now(), facetsFn = nul
|
|
|
387
435
|
}
|
|
388
436
|
|
|
389
437
|
// ---- protocol.mjs ----
|
|
438
|
+
const SERVER_INFO = { name: "aura-components-mcp", version: "1.6.0" };
|
|
390
439
|
// Module: protocol — stdio JSON-RPC framing + MCP handshake + error codes.
|
|
391
440
|
// One interface: createSession(send) -> { dispatch(msg) }. No business logic.
|
|
392
441
|
|
|
393
|
-
const SERVER_INFO = { name: "aura-components-mcp", version: "1.0.0" };
|
|
394
442
|
const PROTOCOL_VERSION = "2024-11-05";
|
|
395
443
|
|
|
396
444
|
const protocolFn_err = (code, message, data) => ({ code, message, ...(data === undefined ? {} : { data }) });
|
|
@@ -463,21 +511,39 @@ const GUIDE_CDN = {
|
|
|
463
511
|
iconify: 'https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js',
|
|
464
512
|
};
|
|
465
513
|
|
|
514
|
+
// Font allowlist: real families only. Tailwind weight/size utilities (medium, semibold,
|
|
515
|
+
// normal, light-as-weight, size-*) are NOT families — old regex leaked them into fonts[].
|
|
516
|
+
const KNOWN_FONTS = ['inter', 'geist', 'manrope', 'sora', 'space-grotesk', 'space-mono', 'poppins', 'montserrat', 'playfair-display', 'instrument-serif', 'dm-sans', 'dm-serif-display', 'jetbrains-mono', 'ibm-plex-mono', 'ibm-plex-serif', 'plus-jakarta-sans', 'bricolage-grotesque', 'newsreader', 'fraunces', 'lora', 'merriweather', 'libre-baskerville', 'cormorant-garamond', 'gfs-didot', 'bodoni-moda', 'cardo', 'marcellus', 'gloock', 'aboreto', 'syne', 'urbanist', 'sora', 'anton', 'bebas-neue', 'archivo-black', 'league-gothic', 'fredoka', 'chewy', 'foldit', 'oi', 'honk', 'nabla', 'quicksand', 'nunito', 'work-sans', 'oswald', 'lora', 'roboto', 'open-sans', 'figtree', 'outfit', 'lexend', 'gloria-hallelujah', 'caveat'];
|
|
517
|
+
const FONT_RE = /font-(?:family-)?\[?['"]?([a-z][a-z0-9-]*)/g;
|
|
518
|
+
|
|
466
519
|
function detectNeeds(code, tags) {
|
|
467
520
|
const c = String(code || '').toLowerCase();
|
|
468
521
|
const t = new Set((tags || []).map((x) => String(x).toLowerCase()));
|
|
469
522
|
const needsTailwind = c.includes('class=') || t.has('tailwind');
|
|
470
|
-
|
|
523
|
+
// Probes must work on a 2k prefix too: icon svgs usually appear in the first screen of markup.
|
|
524
|
+
// svg alone under-matches in lists (no code fetched) — but must not over-claim either:
|
|
525
|
+
// 'svg' counts only with an icon-ish neighbor (iconify, <svg, lucide, heroicons, feather).
|
|
526
|
+
const needsIcons = c.includes('iconify') || c.includes('<svg') || c.includes('lucide') || c.includes('heroicon') || c.includes('data-lucide');
|
|
471
527
|
const needsKeyframes = c.includes('keyframes') || c.includes('animation:');
|
|
472
528
|
const fonts = new Set();
|
|
473
|
-
for (const m of c.matchAll(
|
|
529
|
+
for (const m of c.matchAll(FONT_RE)) {
|
|
530
|
+
const fam = m[1].replace(/['"\]]/g, '');
|
|
531
|
+
if (KNOWN_FONTS.includes(fam)) fonts.add(fam);
|
|
532
|
+
}
|
|
533
|
+
// font-family: 'X', Y declarations (not utility classes)
|
|
534
|
+
for (const m of c.matchAll(/font-family\s*:\s*([^;}]{1,80})/g)) {
|
|
535
|
+
for (const part of m[1].split(',')) {
|
|
536
|
+
const fam = part.trim().replace(/['"]/g, '').toLowerCase().replace(/\s+/g, '-');
|
|
537
|
+
if (KNOWN_FONTS.includes(fam)) fonts.add(fam);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
474
540
|
return { needsTailwind, needsIcons, needsKeyframes, fonts: [...fonts].slice(0, 6) };
|
|
475
541
|
};
|
|
476
542
|
|
|
477
543
|
// Facets: cheap derived signals so search lists answer "dark? heavy? pro?" without a get.
|
|
478
544
|
// theme: dark|light|mixed|unknown from background field + code palette probes.
|
|
479
545
|
// weight: code_chars bucket (s/m/l) so agents can prefer light embeds.
|
|
480
|
-
function facets(kind, row) {
|
|
546
|
+
function facets(kind, row, opts) {
|
|
481
547
|
const f = {};
|
|
482
548
|
if (kind === 'components') {
|
|
483
549
|
const bg = String(row.background || '').toLowerCase();
|
|
@@ -490,7 +556,7 @@ function facets(kind, row) {
|
|
|
490
556
|
const bgDark = bg.includes('000') && !bg.includes('fff');
|
|
491
557
|
const bgLight = bg.includes('fff') && !bg.includes('000');
|
|
492
558
|
f.theme = bgDark || tagDark ? 'dark' : (bgLight || tagLight ? 'light' : (code ? (darkHits > lightHits * 2 ? 'dark' : (lightHits > darkHits * 2 ? 'light' : (darkHits || lightHits ? 'mixed' : 'unknown'))) : (tags.includes('saas') || tags.includes('minimal') ? 'light' : 'unknown')));
|
|
493
|
-
const n = String(row.code || '').length;
|
|
559
|
+
const n = (opts && opts.fullLength) || String(row.code || '').length;
|
|
494
560
|
f.weight = n > 20000 ? 'l' : (n > 8000 ? 'm' : 's');
|
|
495
561
|
f.code_chars = n;
|
|
496
562
|
const needs = detectNeeds(row.code || '', row.tags || []);
|
|
@@ -498,6 +564,13 @@ function facets(kind, row) {
|
|
|
498
564
|
f.needsIcons = needs.needsIcons;
|
|
499
565
|
f.fonts = needs.fonts;
|
|
500
566
|
}
|
|
567
|
+
// ISS14 lives here too: license is top-level on every asset row, not buried.
|
|
568
|
+
// ISS13: rewrite hotlinked demo images to stable asset references where possible.
|
|
569
|
+
// i.pravatar.cc + bare demo URLs 404 in production; flag them in the install plan.
|
|
570
|
+
if (kind === 'components' && typeof row.code === 'string') {
|
|
571
|
+
const hot = [...row.code.matchAll(/<img[^>]+src=["']([^"']+)["']/gi)].map((m) => m[1]).filter((u) => /pravatar|placehold|dummyimage|unsplash\/photos\/..\/download|example\.com/i.test(u));
|
|
572
|
+
if (hot.length) row = { ...row, _hotlinked: [...new Set(hot)].slice(0, 5) };
|
|
573
|
+
}
|
|
501
574
|
if (kind === 'assets' || row.image_800w || row.image_original || row.video_url) {
|
|
502
575
|
// Resolved 2026-09-13 from primary source https://www.aura.build/terms §4-5:
|
|
503
576
|
// catalogue content is the exclusive property of DESIGNCODE IO PTE. LTD.;
|
|
@@ -506,7 +579,11 @@ function facets(kind, row) {
|
|
|
506
579
|
f.download = row.image_original || row.image_1600w || row.image_800w || row.video_url || null;
|
|
507
580
|
f.preview = row.image_800w || row.video_poster_url || null;
|
|
508
581
|
}
|
|
509
|
-
if (kind === 'design_systems')
|
|
582
|
+
if (kind === 'design_systems') {
|
|
583
|
+
// ISS4: lists never fetch preview_html (24KB/row). has_preview:true is authoritative
|
|
584
|
+
// only on detail; on lists report 'unknown-list' so agents fetch instead of trusting false.
|
|
585
|
+
f.has_preview = row.preview_html !== undefined ? Boolean(row.preview_html) : 'unknown-list';
|
|
586
|
+
}
|
|
510
587
|
return f;
|
|
511
588
|
}
|
|
512
589
|
|
|
@@ -526,11 +603,23 @@ function installGuide(kind, row) {
|
|
|
526
603
|
steps.push('Add the iconify-icon CDN script so the icon tags render.');
|
|
527
604
|
}
|
|
528
605
|
if (needs.needsKeyframes) steps.push('Keep the embedded style block with the markup: that is where the keyframes live.');
|
|
606
|
+
// ISS11+12: pagination + CSS scoping. Code ships truncated at codeChars; the scope
|
|
607
|
+
// prefix below prevents .button/.inner/.point collisions in multi-component scaffolds.
|
|
608
|
+
const scope = 'aura-' + String(row.slug || row.id).toLowerCase().replace(/[^a-z0-9]+/g, '');
|
|
609
|
+
const fullLen = String(row.code || '').length;
|
|
610
|
+
const isTrunc = /…\[truncated\]$/.test(String(row.code || ''));
|
|
611
|
+
if (isTrunc) {
|
|
612
|
+
files.push({ path: 'components/' + (row.slug || row.id) + '.part1.html', contains: 'markup part 1 of N — fetch aura_get_component again and concatenate code chunks' });
|
|
613
|
+
steps.push('Code is paginated (' + fullLen + ' chars shown of more): install part files in order, or re-fetch for the full body.');
|
|
614
|
+
} else {
|
|
615
|
+
files.push({ path: 'components/' + (row.slug || row.id) + '.html', contains: 'section markup plus its style block' });
|
|
616
|
+
}
|
|
617
|
+
steps.push('Scope global CSS before pasting: prefix bare selectors (.button, .inner, .point) with .' + scope + ' to avoid collisions in multi-component pages.');
|
|
529
618
|
steps.push('Paste the markup where the section belongs and update the CTA link (it ships as #).');
|
|
530
619
|
if (url) steps.push('Preview first: ' + url);
|
|
531
|
-
|
|
620
|
+
if (row._hotlinked && row._hotlinked.length) steps.push('Replace demo images before shipping (' + row._hotlinked.slice(0, 3).join(', ') + '): hotlinked placeholders 404 in production — swap for aura_search_assets results.');
|
|
532
621
|
if (needs.fonts.length) files.push({ path: 'styles/fonts.css', contains: 'font families referenced: ' + needs.fonts.join(', ') });
|
|
533
|
-
return { kind, title: row.title, page_url: url, free: row.premium === false, steps, deps, files, needs };
|
|
622
|
+
return { kind, title: row.title, page_url: url, free: row.premium === false, steps, deps, files, needs, css_scope: scope, code_truncated: isTrunc };
|
|
534
623
|
}
|
|
535
624
|
if (kind === 'skills') {
|
|
536
625
|
steps.push('Read the SKILL.md content from aura_get_skill first: it names its own triggers and pitfalls.');
|
|
@@ -539,15 +628,37 @@ function installGuide(kind, row) {
|
|
|
539
628
|
return { kind, title: row.title, page_url: url, steps, deps, files: [{ path: 'skills/' + row.id + '/SKILL.md', contains: 'full skill content' }] };
|
|
540
629
|
}
|
|
541
630
|
if (kind === 'design_systems') {
|
|
631
|
+
// ISS6: derive preview deps from preview_html so agents don't ship static pages.
|
|
632
|
+
const ph = String(row.preview_html || '');
|
|
633
|
+
const pl = ph.toLowerCase();
|
|
634
|
+
const has = (...ss) => ss.some((s) => pl.includes(s));
|
|
635
|
+
if (has('cdn.tailwindcss.com', 'tailwind')) deps.push({ name: 'tailwindcss', via: 'CDN (in preview_html)', cdn: GUIDE_CDN.tailwind });
|
|
636
|
+
if (has('iconify', '<svg', 'lucide', 'data-lucide')) deps.push({ name: 'iconify-icon', via: 'CDN (in preview_html)', cdn: GUIDE_CDN.iconify });
|
|
637
|
+
if (has('gsap')) deps.push({ name: 'gsap', via: 'CDN (in preview_html)', cdn: 'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js' });
|
|
638
|
+
if (has('scrolltrigger')) deps.push({ name: 'gsap-ScrollTrigger', via: 'CDN (in preview_html)', cdn: 'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js' });
|
|
639
|
+
const fonts = new Set();
|
|
640
|
+
for (const m of pl.matchAll(/family=([a-z+]+)/g)) fonts.add(m[1].replace(/\+/g, '-'));
|
|
641
|
+
if (fonts.size) deps.push({ name: 'google-fonts:' + [...fonts].slice(0, 4).join(','), via: 'link (in preview_html)' });
|
|
542
642
|
steps.push('Copy the DESIGN.md content into your repo as DESIGN.md and treat it as the source of truth.');
|
|
543
643
|
steps.push('Apply the color, type, and spacing tokens before copying any component markup.');
|
|
644
|
+
if (deps.length) steps.push('Install preview deps first (' + deps.map((d) => d.name).join(', ') + ') — the preview_html needs them.');
|
|
544
645
|
return { kind, title: row.title, page_url: url, steps, deps, files: [{ path: 'DESIGN.md', contains: 'tokens and rules' }] };
|
|
545
646
|
}
|
|
546
647
|
steps.push('Use the image_800w URL for previews and image_original for production.');
|
|
547
648
|
return { kind, title: row.title, page_url: url, steps, deps, files };
|
|
548
649
|
};
|
|
549
650
|
|
|
550
|
-
|
|
651
|
+
// ISS5: full token surface — colors + typography + spacing + radius — so scaffolded
|
|
652
|
+
// pages don't lose the system. Also detects preview dark/light mode so agents don't
|
|
653
|
+
// apply light-cream tokens to a dark preview body.
|
|
654
|
+
function previewMode(previewHtml) {
|
|
655
|
+
const h = String(previewHtml || '').toLowerCase();
|
|
656
|
+
if (/bg-black|bg-neutral-950|bg-zinc-950|bg-slate-950|bg-\[#0/.test(h)) return 'dark';
|
|
657
|
+
if (/bg-white|bg-neutral-50|bg-slate-50/.test(h)) return 'light';
|
|
658
|
+
return 'unknown';
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function tokenHints(content, previewHtml) {
|
|
551
662
|
const Q = String.fromCharCode(39);
|
|
552
663
|
const lines = String(content || '').split(Q + 'n' === Q + 'n' ? '\n' : '\n');
|
|
553
664
|
const get = (key) => {
|
|
@@ -563,13 +674,42 @@ function tokenHints(content) {
|
|
|
563
674
|
};
|
|
564
675
|
const tokens = {
|
|
565
676
|
primary: get('primary'),
|
|
677
|
+
secondary: get('secondary'),
|
|
678
|
+
accent: get('accent'),
|
|
566
679
|
background: get('background'),
|
|
567
680
|
surface: get('surface'),
|
|
568
|
-
|
|
681
|
+
text_primary: get('text-primary') || get('text_primary'),
|
|
682
|
+
text_secondary: get('text-secondary') || get('text_secondary'),
|
|
683
|
+
border: get('border'),
|
|
684
|
+
display_font: get('display_lg_fontfamily') || get('display-font'),
|
|
685
|
+
body_font: get('body_md_fontfamily') || get('body-font'),
|
|
686
|
+
mono_font: get('label_md_fontfamily') || get('mono-font'),
|
|
687
|
+
section_padding: get('section-padding') || get('section_padding'),
|
|
688
|
+
card_padding: get('card-padding') || get('card_padding'),
|
|
689
|
+
card_radius: get('card') === null ? get('card_radius') : null,
|
|
690
|
+
control_radius: get('control'),
|
|
691
|
+
pill_radius: get('pill'),
|
|
569
692
|
};
|
|
570
|
-
const
|
|
693
|
+
const pick = (obj, keys) => { const o = {}; for (const k of keys) if (obj[k]) o[k] = obj[k]; return o; };
|
|
694
|
+
const colors = pick(tokens, ['primary', 'secondary', 'accent', 'background', 'surface', 'text_primary', 'text_secondary', 'border']);
|
|
695
|
+
const rows = [];
|
|
696
|
+
for (const [k, v] of Object.entries(colors)) rows.push(' --aura-' + k.replace(/_/g, '-') + ': ' + v + ';');
|
|
697
|
+
if (tokens.body_font) rows.push(' --aura-font-body: ' + tokens.body_font + ';');
|
|
698
|
+
if (tokens.display_font) rows.push(' --aura-font-display: ' + tokens.display_font + ';');
|
|
699
|
+
if (tokens.mono_font) rows.push(' --aura-font-mono: ' + tokens.mono_font + ';');
|
|
700
|
+
if (tokens.section_padding) rows.push(' --aura-section-padding: ' + tokens.section_padding + ';');
|
|
701
|
+
if (tokens.card_padding) rows.push(' --aura-card-padding: ' + tokens.card_padding + ';');
|
|
702
|
+
if (tokens.card_radius) rows.push(' --aura-radius-card: ' + tokens.card_radius + ';');
|
|
703
|
+
if (tokens.control_radius) rows.push(' --aura-radius-control: ' + tokens.control_radius + ';');
|
|
704
|
+
if (tokens.pill_radius) rows.push(' --aura-radius-pill: ' + tokens.pill_radius + ';');
|
|
571
705
|
const css = rows.length ? ':root{' + '\n' + rows.join('\n') + '\n}' : '';
|
|
572
|
-
|
|
706
|
+
const mode = previewMode(previewHtml);
|
|
707
|
+
const warn = (mode === 'dark' && tokens.background && !/000|0f1115|111827|1a1a1a|09090b/i.test(tokens.background))
|
|
708
|
+
? 'tokens say light background ' + tokens.background + ' but preview_html renders dark — follow the preview body, not the tokens, or ask which mode is wanted'
|
|
709
|
+
: ((mode === 'light' && tokens.background && /000000|0f1115/i.test(tokens.background))
|
|
710
|
+
? 'tokens say dark background but preview renders light — follow the preview body'
|
|
711
|
+
: null);
|
|
712
|
+
return { tokens, css, preview_mode: mode, mode_warning: warn };
|
|
573
713
|
};
|
|
574
714
|
|
|
575
715
|
// ---- tools.mjs ----
|
|
@@ -671,7 +811,7 @@ function createHandlers(catalog) {
|
|
|
671
811
|
aura_use_design_system: async (a) => { a = a || {}; const id = a.id !== undefined ? a.id : a.slug;
|
|
672
812
|
if (typeof id !== 'string' || !id) toolFn_bad('provide id or slug (string)');
|
|
673
813
|
const got = await catalog.getItem('design_systems', id);
|
|
674
|
-
return textResult({ item: got.item, install: installGuide('design_systems', got.item), tokens: tokenHints(got.item.content || '') }); },
|
|
814
|
+
return textResult({ item: got.item, install: installGuide('design_systems', got.item), tokens: tokenHints(got.item.content || '', got.item.preview_html || '') }); },
|
|
675
815
|
aura_trending: async (a) => { a = a || {}; const limit = toolFn_num(a.limit, 'limit') || 5;
|
|
676
816
|
const r = await Promise.all([
|
|
677
817
|
catalog.searchCatalog('components', { freeOnly: true, sort: 'trending', limit }),
|
|
@@ -679,7 +819,7 @@ function createHandlers(catalog) {
|
|
|
679
819
|
catalog.searchCatalog('assets', { freeOnly: true, sort: 'trending', limit }),
|
|
680
820
|
catalog.searchCatalog('design_systems', { sort: 'trending', limit }),
|
|
681
821
|
]);
|
|
682
|
-
return textResult({ window: 'last 90 days by views
|
|
822
|
+
return textResult({ window: 'last 90 days by views', window_note: '7-day seed is empty (newest catalogue rows are months old); 90d keeps trending meaningful. Components here are low-signal (views 0-5): prefer skills/assets trending.', components: r[0], skills: r[1], assets: r[2], design_systems: r[3] }); },
|
|
683
823
|
aura_categories: async () => textResult({ categories: await catalog.categoryCounts() }),
|
|
684
824
|
aura_bundle: async (a) => { a = a || {}; const ids = idList(a.ids, 'ids') || idList(a.slugs, 'slugs');
|
|
685
825
|
if (!ids || !ids.length) toolFn_bad('provide ids (array of 1-8 numbers/strings) or slugs (array of strings)');
|
package/flow/app.js
CHANGED
|
@@ -1,40 +1,178 @@
|
|
|
1
|
-
(
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
paint();
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if(!
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
1
|
+
(()=>{"use strict";const r=document.documentElement;
|
|
2
|
+
r.classList.remove("no-js");
|
|
3
|
+
const reduced=matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
4
|
+
function syncMeta(){const dark=r.dataset.theme!=="light";
|
|
5
|
+
let single=document.querySelector('meta[name="theme-color"]:not([media])');
|
|
6
|
+
if(!single){single=document.createElement("meta");single.name="theme-color";document.head.appendChild(single)}
|
|
7
|
+
single.content=dark?"#000000":"#ffffff"}
|
|
8
|
+
function paint(){const t=document.getElementById("themeBtn");
|
|
9
|
+
if(t){t.textContent=r.dataset.theme==="dark"?"\u263E":"\u2600"}}
|
|
10
|
+
function setTheme(n){r.dataset.theme=n;
|
|
11
|
+
try{localStorage.setItem("acm-theme",n)}catch(e){}paint();syncMeta()}
|
|
12
|
+
const tb=document.getElementById("themeBtn");
|
|
13
|
+
if(tb){tb.addEventListener("click",function(){
|
|
14
|
+
const n=r.dataset.theme==="dark"?"light":"dark";
|
|
15
|
+
if(document.startViewTransition&&!reduced){document.startViewTransition(function(){setTheme(n)})}
|
|
16
|
+
else{setTheme(n)}})}
|
|
17
|
+
paint();syncMeta();
|
|
18
|
+
const panel=document.getElementById("mpanel"),menu=document.getElementById("menuBtn");
|
|
19
|
+
function openM(o){if(!panel){return}panel.classList.toggle("open",o);
|
|
20
|
+
document.body.style.overflow=o?"hidden":""}
|
|
21
|
+
if(menu&&panel){menu.addEventListener("click",function(){openM(!panel.classList.contains("open"))})}
|
|
22
|
+
if(panel){panel.addEventListener("click",function(e){
|
|
23
|
+
if(e.target.closest("[data-close]")||e.target===panel){openM(false)}})}
|
|
24
|
+
addEventListener("keydown",function(e){if(e.key==="Escape"){openM(false)}});
|
|
25
|
+
const here=(location.pathname.split("/").pop()||"index.html").toLowerCase().split("?")[0];
|
|
26
|
+
function mark(a){const h=(a.getAttribute("href")||"").toLowerCase().replace("./","");
|
|
27
|
+
a.classList.toggle("on",h===here||(here===""&&h==="index.html"))}
|
|
28
|
+
document.querySelectorAll(".tabs a").forEach(mark);
|
|
29
|
+
document.querySelectorAll(".msheet a[data-nav]").forEach(mark);
|
|
30
|
+
const nav=document.querySelector(".nav");
|
|
31
|
+
addEventListener("scroll",function(){if(nav){nav.classList.toggle("scrolled",(scrollY||0)>8)}},{passive:true});
|
|
32
|
+
document.querySelectorAll(".card,.fnode").forEach(function(c){
|
|
33
|
+
c.addEventListener("pointermove",function(e){
|
|
34
|
+
const b=c.getBoundingClientRect();
|
|
35
|
+
c.style.setProperty("--fx",((e.clientX-b.left)/b.width*100)+"%");
|
|
36
|
+
c.style.setProperty("--fy",((e.clientY-b.top)/b.height*100)+"%")},{passive:true})});
|
|
37
|
+
document.querySelectorAll("[data-copy]").forEach(function(btn){
|
|
38
|
+
btn.addEventListener("click",function(){
|
|
39
|
+
const pre=btn.parentElement.querySelector("pre");if(!pre){return}
|
|
40
|
+
const done=function(t){btn.textContent=t;setTimeout(function(){btn.textContent="Copy"},1400)};
|
|
41
|
+
if(navigator.clipboard&&navigator.clipboard.writeText){
|
|
42
|
+
navigator.clipboard.writeText(pre.innerText).then(function(){done("Copied")},function(){done("Select + Ctrl+C")})}
|
|
43
|
+
else{done("Select + Ctrl+C")}})});
|
|
44
|
+
document.querySelectorAll(".ctabs").forEach(function(g){
|
|
45
|
+
const btns=Array.prototype.slice.call(g.querySelectorAll("button"));
|
|
46
|
+
btns.forEach(function(b){b.addEventListener("click",function(){
|
|
47
|
+
btns.forEach(function(x){x.classList.remove("on")});b.classList.add("on");
|
|
48
|
+
const id=b.getAttribute("data-tab");if(!id||!g.parentElement){return}
|
|
49
|
+
const panes=g.parentElement.querySelectorAll("[data-pane]");
|
|
50
|
+
panes.forEach(function(p){p.hidden=p.getAttribute("data-pane")!==id})})})});
|
|
51
|
+
const yr=document.getElementById("yr");if(yr){yr.textContent=new Date().getFullYear()}
|
|
52
|
+
const hasGsap=(typeof gsap!=="undefined")&&(typeof ScrollTrigger!=="undefined");
|
|
53
|
+
if(!hasGsap||reduced){r.classList.add("no-gsap");
|
|
54
|
+
const els=document.querySelectorAll(".reveal");
|
|
55
|
+
if(!("IntersectionObserver" in window)){els.forEach(function(e){e.classList.add("in")})}
|
|
56
|
+
else{const io=new IntersectionObserver(function(es){
|
|
57
|
+
es.forEach(function(en){if(en.isIntersecting){en.target.classList.add("in");io.unobserve(en.target)}})},{threshold:.1});
|
|
58
|
+
els.forEach(function(e){io.observe(e)})}}
|
|
59
|
+
if(hasGsap&&!reduced){try{
|
|
60
|
+
r.style.scrollBehavior="auto";
|
|
61
|
+
gsap.registerPlugin(ScrollTrigger);
|
|
62
|
+
gsap.from(".hero-in > *",{y:26,opacity:0,duration:.8,stagger:.09,ease:"power3.out",clearProps:"all"});
|
|
63
|
+
gsap.utils.toArray(".reveal").forEach(function(el){
|
|
64
|
+
if(el.matches(".grid3 .card")){return}
|
|
65
|
+
gsap.fromTo(el,{y:24,opacity:0},{y:0,opacity:1,duration:.7,ease:"power3.out",
|
|
66
|
+
scrollTrigger:{trigger:el,start:"top 88%",once:true}})});
|
|
67
|
+
const batchCards=gsap.utils.toArray(".grid3 .card");
|
|
68
|
+
if(batchCards.length){gsap.set(batchCards,{y:28,opacity:0});
|
|
69
|
+
ScrollTrigger.batch(batchCards,{start:"top 88%",once:true,interval:.1,batchMax:3,
|
|
70
|
+
onEnter:function(batch){gsap.to(batch,{y:0,opacity:1,duration:.7,
|
|
71
|
+
ease:"power3.out",stagger:.12,overwrite:true})}});}
|
|
72
|
+
document.querySelectorAll(".stat b[data-count]").forEach(function(b){
|
|
73
|
+
const end=parseFloat(b.getAttribute("data-count"));const o={v:0};
|
|
74
|
+
function tick(){b.textContent=Math.round(o.v).toLocaleString("en-US")}
|
|
75
|
+
function play(){gsap.to(o,{v:end,duration:1.4,ease:"power2.out",onUpdate:tick})}
|
|
76
|
+
ScrollTrigger.create({trigger:b,start:"top 92%",once:true,onEnter:play});
|
|
77
|
+
});
|
|
78
|
+
addEventListener("load",function(){ScrollTrigger.refresh()});
|
|
79
|
+
}catch(err){r.classList.add("no-gsap")}}
|
|
80
|
+
(function(){if(reduced){return}
|
|
81
|
+
const cv=document.createElement("canvas");cv.id="bg-net";cv.setAttribute("aria-hidden","true");
|
|
82
|
+
document.body.prepend(cv);const cx=cv.getContext("2d");
|
|
83
|
+
let W=0,H=0,pts=[],run=true,mx=-999,my=-999;
|
|
84
|
+
const dark=function(){return r.dataset.theme!=="light"};
|
|
85
|
+
function fit(){const d=Math.min(devicePixelRatio||1,1.5);
|
|
86
|
+
W=innerWidth;H=innerHeight;cv.width=W*d;cv.height=H*d;
|
|
87
|
+
cv.style.width=W+"px";cv.style.height=H+"px";cx.setTransform(d,0,0,d,0,0);
|
|
88
|
+
const n=Math.min(90,(W*H/22000)|0);pts=[];
|
|
89
|
+
for(let i=0;i<n;i++){pts.push({x:Math.random()*W,y:Math.random()*H,
|
|
90
|
+
vx:(Math.random()-.5)*.35,vy:(Math.random()-.5)*.35})}}
|
|
91
|
+
addEventListener("resize",fit,{passive:true});fit();
|
|
92
|
+
addEventListener("pointermove",function(e){mx=e.clientX;my=e.clientY},{passive:true});
|
|
93
|
+
new IntersectionObserver(function(es){run=es[0].isIntersecting},{threshold:0}).observe(cv);
|
|
94
|
+
document.addEventListener("visibilitychange",function(){run=!document.hidden});
|
|
95
|
+
(function frame(){requestAnimationFrame(frame);
|
|
96
|
+
if(!run||document.hidden){return}
|
|
97
|
+
cx.clearRect(0,0,W,H);
|
|
98
|
+
const mono=dark()?"255,255,255":"0,0,0";
|
|
99
|
+
for(const p of pts){p.x+=p.vx;p.y+=p.vy;
|
|
100
|
+
if(p.x<0||p.x>W){p.vx*=-1}if(p.y<0||p.y>H){p.vy*=-1}}
|
|
101
|
+
for(let i=0;i<pts.length;i++){const a=pts[i];
|
|
102
|
+
for(let j=i+1;j<pts.length;j++){const b=pts[j];
|
|
103
|
+
const dx=a.x-b.x,dy=a.y-b.y,d=dx*dx+dy*dy;
|
|
104
|
+
if(d<150*150){cx.strokeStyle="rgba("+mono+","+(0.10*(1-d/22500)).toFixed(3)+")";
|
|
105
|
+
cx.lineWidth=1;cx.beginPath();cx.moveTo(a.x,a.y);cx.lineTo(b.x,b.y);cx.stroke()}}}
|
|
106
|
+
for(const p of pts){const dm=(p.x-mx)*(p.x-mx)+(p.y-my)*(p.y-my);
|
|
107
|
+
const glow=dm<120*120?0.5:0.22;
|
|
108
|
+
cx.fillStyle="rgba("+mono+","+glow.toFixed(2)+")";
|
|
109
|
+
cx.beginPath();cx.arc(p.x,p.y,dm<120*120?1.8:1.2,0,6.283);cx.fill()}})();
|
|
38
110
|
})();
|
|
111
|
+
const prog=document.getElementById("progress"),topBtn=document.getElementById("toTop");
|
|
112
|
+
function onPos(){const h=document.documentElement;
|
|
113
|
+
const max=h.scrollHeight-h.clientHeight;
|
|
114
|
+
const p=max>0?(h.scrollTop||document.body.scrollTop)/max:0;
|
|
115
|
+
if(prog){prog.style.transform="scaleX("+p+")"}
|
|
116
|
+
if(topBtn){topBtn.classList.toggle("show",(h.scrollTop||0)>600)}}
|
|
117
|
+
addEventListener("scroll",onPos,{passive:true});onPos();
|
|
118
|
+
if(topBtn){topBtn.addEventListener("click",function(){
|
|
119
|
+
scrollTo({top:0,behavior:reduced?"auto":"smooth"})})}
|
|
120
|
+
if(hasGsap&&!reduced){try{
|
|
121
|
+
gsap.to(".hero-in",{yPercent:-10,autoAlpha:.25,ease:"none",
|
|
122
|
+
scrollTrigger:{trigger:".hero",start:"top top",end:"bottom top",scrub:true}});
|
|
123
|
+
gsap.utils.toArray(".flow").forEach(function(f){
|
|
124
|
+
const items=f.querySelectorAll(".fnode");
|
|
125
|
+
if(items.length){gsap.fromTo(items,{y:34,autoAlpha:0},
|
|
126
|
+
{y:0,autoAlpha:1,duration:.6,stagger:.18,ease:"power3.out",
|
|
127
|
+
scrollTrigger:{trigger:f,start:"top 82%",once:true}})}});
|
|
128
|
+
}catch(err){}}
|
|
129
|
+
const fine=matchMedia("(pointer:fine)").matches;
|
|
130
|
+
if(fine&&!reduced){
|
|
131
|
+
const dot=document.createElement("div");dot.id="cursor";dot.setAttribute("aria-hidden","true");
|
|
132
|
+
const ring=document.createElement("div");ring.id="aura";ring.setAttribute("aria-hidden","true");
|
|
133
|
+
document.body.append(dot,ring);
|
|
134
|
+
let x=innerWidth/2,y=innerHeight/2,rx=x,ry=y,sc=1,tsc=1;
|
|
135
|
+
addEventListener("pointermove",function(e){x=e.clientX;y=e.clientY;
|
|
136
|
+
const t=e.target.closest?e.target.closest("a,button,.card,.fnode"):null;
|
|
137
|
+
tsc=t?2.1:1},{passive:true});
|
|
138
|
+
(function loop(){rx+=(x-rx)*.16;ry+=(y-ry)*.16;sc+=(tsc-sc)*.2;
|
|
139
|
+
dot.style.transform="translate("+x+"px,"+y+"px)";
|
|
140
|
+
ring.style.transform="translate("+rx+"px,"+ry+"px) scale("+sc.toFixed(3)+")";
|
|
141
|
+
requestAnimationFrame(loop)})();
|
|
142
|
+
const hasG=typeof gsap!=="undefined";
|
|
143
|
+
document.querySelectorAll(".hero .btn").forEach(function(b){
|
|
144
|
+
const qx=hasG?gsap.quickTo(b,"x",{duration:.3,ease:"power3"}):null;
|
|
145
|
+
const qy=hasG?gsap.quickTo(b,"y",{duration:.3,ease:"power3"}):null;
|
|
146
|
+
b.addEventListener("pointermove",function(e){
|
|
147
|
+
if(!qx||!qy){return}
|
|
148
|
+
const bb=b.getBoundingClientRect();
|
|
149
|
+
qx((e.clientX-(bb.left+bb.width/2))*.25);
|
|
150
|
+
qy((e.clientY-(bb.top+bb.height/2))*.25)});
|
|
151
|
+
b.addEventListener("pointerleave",function(){if(qx&&qy){qx(0);qy(0)}})});
|
|
152
|
+
document.querySelectorAll(".hero .btn").forEach(function(b){
|
|
153
|
+
if(b.querySelector(".ch")){return}
|
|
154
|
+
const txt=b.textContent;b.setAttribute("aria-label",txt);b.textContent="";
|
|
155
|
+
Array.prototype.forEach.call(txt,function(ch){
|
|
156
|
+
const s=document.createElement("span");s.className="ch";s.setAttribute("aria-hidden","true");
|
|
157
|
+
s.textContent=ch===" "?" ":ch;b.appendChild(s)});
|
|
158
|
+
b.addEventListener("mouseenter",function(){
|
|
159
|
+
if(!hasG){return}
|
|
160
|
+
gsap.fromTo(b.querySelectorAll(".ch"),{y:0},
|
|
161
|
+
{y:-5,duration:.13,ease:"power2.out",stagger:{each:.018,yoyo:true,repeat:1},overwrite:"auto"})})});
|
|
162
|
+
const heroEl=document.querySelector(".hero"),statsEl=document.querySelector(".hero .stats");
|
|
163
|
+
if(heroEl&&statsEl){statsEl.style.transformStyle="preserve-3d";
|
|
164
|
+
heroEl.addEventListener("pointermove",function(e){
|
|
165
|
+
const bb=statsEl.getBoundingClientRect();
|
|
166
|
+
const px=(e.clientX-bb.left)/bb.width-.5,py=(e.clientY-bb.top)/bb.height-.5;
|
|
167
|
+
statsEl.style.transform="perspective(800px) rotateX("+(-py*7).toFixed(2)+"deg) rotateY("+(px*9).toFixed(2)+"deg)"},{passive:true});
|
|
168
|
+
heroEl.addEventListener("pointerleave",function(){
|
|
169
|
+
statsEl.style.transform="perspective(800px)"})}
|
|
170
|
+
let lastY=scrollY||0;
|
|
171
|
+
addEventListener("scroll",function(){
|
|
172
|
+
const yy=scrollY||0;
|
|
173
|
+
if(!(panel&&panel.classList.contains("open"))){
|
|
174
|
+
if(yy>140&&yy>lastY+4&&nav){nav.classList.add("hide")}
|
|
175
|
+
else if((yy<lastY-4||yy<140)&&nav){nav.classList.remove("hide")}}
|
|
176
|
+
lastY=yy},{passive:true});
|
|
39
177
|
}
|
|
40
178
|
})();
|