mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-14 15:16:35 +03:00
Major skill consolidation for v2.0:
- Rename source/skills/frontend-design/ to source/skills/impeccable/
with user-invocable: true and argument-hint: "[teach]"
- Fold teach-impeccable body into impeccable as "Teach Mode" section,
activated via /impeccable teach
- Create deprecation shims:
- frontend-design: redirects to /impeccable
- teach-impeccable: redirects to /impeccable teach
- Update all 16 skill cross-references from {{command_prefix}}frontend-design
to {{command_prefix}}impeccable and {{command_prefix}}teach-impeccable to
{{command_prefix}}impeccable teach
- Update CLI sentinel detection to use 'impeccable' (with teach-impeccable
as legacy fallback)
- Update build system readPatterns() path and EXCLUDED_FROM_SUGGESTIONS
- Update all public files (data.js, cheatsheet, index, viz, demos)
- Update all documentation (README, NOTICE, AGENTS, plugin.json)
- Update all test expectations
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
143 lines
4.4 KiB
JavaScript
143 lines
4.4 KiB
JavaScript
import { readySkills, skillFocusAreas } from "../data.js";
|
|
import { renderSkillDemo, setupDemoTabs } from "../demo-renderer.js";
|
|
import { setupDemoToggles } from "../demo-toggles.js";
|
|
|
|
export function initArtGallery() {
|
|
// Initial setup if needed
|
|
}
|
|
|
|
export function renderGallery(skills) {
|
|
const container = document.querySelector(".skills-gallery");
|
|
if (!container) return;
|
|
|
|
// Filter skills (hide impeccable as per original)
|
|
const filteredSkills = skills.filter((s) => s.id !== "impeccable");
|
|
|
|
container.innerHTML = `
|
|
<div class="gallery-track">
|
|
${filteredSkills.map((skill, index) => renderFrame(skill, index)).join("")}
|
|
</div>
|
|
<div class="gallery-map" role="tablist" aria-label="Skill gallery navigation">
|
|
${filteredSkills.map((skill, index) => `<button class="gallery-dot ${index === 0 ? "active" : ""}" data-index="${index}" role="tab" aria-selected="${index === 0 ? "true" : "false"}" aria-label="View ${formatName(skill.id)} skill"></button>`).join("")}
|
|
</div>
|
|
`;
|
|
|
|
setupInteractions();
|
|
setupDemoTabs();
|
|
setupDemoToggles();
|
|
}
|
|
|
|
function renderFrame(skill, index) {
|
|
const isReady = readySkills.includes(skill.id);
|
|
const focusAreas = skillFocusAreas[skill.id] || [];
|
|
const displayName = formatName(skill.id);
|
|
|
|
return `
|
|
<article class="gallery-frame ${index === 0 ? "active" : ""}" data-index="${index}" id="skill-${skill.id}">
|
|
<div class="gallery-content">
|
|
<div class="gallery-visual">
|
|
${isReady ? renderSkillDemo(skill.id) : renderComingSoonVisual(skill.id)}
|
|
</div>
|
|
<div class="gallery-info">
|
|
<div class="gallery-header">
|
|
<h3 class="gallery-title">${displayName}</h3>
|
|
<div class="gallery-meta">
|
|
Skill · ${isReady ? "Available" : "Coming Soon"}
|
|
</div>
|
|
</div>
|
|
<p class="gallery-desc">${skill.description}</p>
|
|
|
|
${
|
|
focusAreas.length > 0
|
|
? `
|
|
<div class="gallery-tags">
|
|
${focusAreas
|
|
.slice(0, 4)
|
|
.map(
|
|
(area) => `
|
|
<span class="gallery-tag">${area.area}</span>
|
|
`,
|
|
)
|
|
.join("")}
|
|
</div>
|
|
`
|
|
: ""
|
|
}
|
|
</div>
|
|
</div>
|
|
</article>
|
|
`;
|
|
}
|
|
|
|
function renderComingSoonVisual(id) {
|
|
return `
|
|
<div class="coming-soon-placeholder" style="text-align: center;">
|
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" style="opacity: 0.3">
|
|
<path d="M12 6v6l4 2M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/>
|
|
</svg>
|
|
<p style="margin-top: 1rem; color: var(--color-ash); font-size: 0.875rem;">Coming Soon</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function formatName(id) {
|
|
return id
|
|
.split("-")
|
|
.map((word) =>
|
|
word === "ux" ? "UX" : word.charAt(0).toUpperCase() + word.slice(1),
|
|
)
|
|
.join(" ");
|
|
}
|
|
|
|
function setupInteractions() {
|
|
const track = document.querySelector(".gallery-track");
|
|
const frames = document.querySelectorAll(".gallery-frame");
|
|
const dots = document.querySelectorAll(".gallery-dot");
|
|
|
|
if (!track) return;
|
|
|
|
// Intersection Observer for Active State
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
entries.forEach((entry) => {
|
|
if (entry.isIntersecting) {
|
|
const index = entry.target.dataset.index;
|
|
|
|
// Update frames
|
|
frames.forEach((f) => f.classList.remove("active"));
|
|
entry.target.classList.add("active");
|
|
|
|
// Update dots
|
|
dots.forEach((d) => {
|
|
d.classList.remove("active");
|
|
d.setAttribute("aria-selected", "false");
|
|
});
|
|
if (dots[index]) {
|
|
dots[index].classList.add("active");
|
|
dots[index].setAttribute("aria-selected", "true");
|
|
}
|
|
}
|
|
});
|
|
},
|
|
{
|
|
root: track,
|
|
threshold: 0.6,
|
|
},
|
|
);
|
|
|
|
frames.forEach((frame) => observer.observe(frame));
|
|
|
|
// Dot navigation
|
|
dots.forEach((dot, index) => {
|
|
dot.addEventListener("click", () => {
|
|
const frame = frames[index];
|
|
if (frame) {
|
|
frame.scrollIntoView({ behavior: "smooth", inline: "center" });
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
|
|
|