Files
skills-here-run-place/dist/server/pages/index.astro.mjs
Alejandro Martinez aa477a553b Add author auth, forking, tags, and stats tracking
Introduce token-based author authentication (register/verify API),
   skill forking with EditGate protection, tag metadata on skills,
   and download/push stats. Enhanced push scripts with token auth
   and per-skill filtering. Updated UI with stats, tags, and
   author info on skill cards.
2026-02-12 14:37:40 +01:00

333 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { e as createAstro, f as createComponent, m as maybeRenderHead, h as addAttribute, r as renderTemplate, k as renderComponent, l as renderScript } from '../chunks/astro/server_CF97kUu8.mjs';
import 'piccolore';
import { _ as _export_sfc, $ as $$Base } from '../chunks/_plugin-vue_export-helper_CEgY73aA.mjs';
import 'clsx';
import { useSSRContext, defineComponent, ref, computed, onMounted, watch, nextTick, mergeProps } from 'vue';
import { ssrRenderAttrs, ssrRenderAttr, ssrRenderList, ssrInterpolate, ssrIncludeBooleanAttr, ssrLooseContain, ssrLooseEqual, ssrRenderClass } from 'vue/server-renderer';
import { l as listSkills } from '../chunks/skills_BacVQUiS.mjs';
import { b as getAllStats } from '../chunks/stats_CaDi9y9J.mjs';
import { i as isPowerShell, a as buildSyncScriptPS, b as buildSyncScript } from '../chunks/sync_BEq_wzpT.mjs';
/* empty css */
export { renderers } from '../renderers.mjs';
const $$Astro$1 = createAstro("https://skills.here.run.place");
const $$SkillCard = createComponent(($$result, $$props, $$slots) => {
const Astro2 = $$result.createAstro($$Astro$1, $$props, $$slots);
Astro2.self = $$SkillCard;
const { slug, name, description, "allowed-tools": allowedTools, tags = [], author, forkCount = 0, downloads = 0, pushes = 0, lastPushedAt } = Astro2.props;
const updatedLabel = lastPushedAt ? new Date(lastPushedAt).toLocaleDateString("en-US", { month: "short", day: "numeric" }) : null;
const truncated = description.length > 120 ? description.slice(0, 120) + "..." : description;
return renderTemplate`${maybeRenderHead()}<a${addAttribute(`/${slug}`, "href")} class="group relative block rounded-2xl border border-white/[0.06] bg-surface-100 p-6 hover:border-accent-500/30 hover:bg-surface-200/80 transition-all duration-300"> <div class="absolute inset-0 rounded-2xl bg-gradient-to-br from-accent-500/[0.03] to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div> <div class="relative"> <div class="flex items-start justify-between mb-2"> <h2 class="text-[15px] font-semibold text-white group-hover:text-accent-400 transition-colors">${name}</h2> <svg class="h-4 w-4 text-gray-600 group-hover:text-accent-500 group-hover:translate-x-0.5 transition-all shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"></path> </svg> </div> ${truncated && renderTemplate`<p class="text-sm text-gray-500 leading-relaxed mb-3">${truncated}</p>`} ${allowedTools.length > 0 && renderTemplate`<div class="flex flex-wrap gap-1.5 mb-3"> ${allowedTools.map((tool) => renderTemplate`<span class="rounded-md bg-white/[0.04] border border-white/[0.06] px-2 py-0.5 text-xs font-medium text-gray-400"> ${tool} </span>`)} </div>`} ${tags.length > 0 && renderTemplate`<div class="flex flex-wrap gap-1.5"> ${tags.map((tag) => renderTemplate`<span class="rounded-full bg-[var(--color-accent-500)]/10 border border-[var(--color-accent-500)]/20 px-2.5 py-0.5 text-[11px] font-medium text-[var(--color-accent-400)]"> ${tag} </span>`)} </div>`} <div class="flex items-center gap-3 mt-3"> ${author && renderTemplate`<p class="text-xs text-gray-600">by ${author}</p>`} ${forkCount > 0 && renderTemplate`<span class="inline-flex items-center gap-1 text-xs text-gray-600"> <svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <path stroke-linecap="round" stroke-linejoin="round" d="M7.217 10.907a2.25 2.25 0 1 0 0 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186 9.566-5.314m-9.566 7.5 9.566 5.314m0-12.814a2.25 2.25 0 1 0 0-2.186m0 2.186a2.25 2.25 0 1 0 0 2.186"></path> </svg> ${forkCount} </span>`} ${downloads > 0 && renderTemplate`<span class="inline-flex items-center gap-1 text-xs text-gray-600"> <svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3"></path> </svg> ${downloads} </span>`} ${pushes > 0 && renderTemplate`<span class="inline-flex items-center gap-1 text-xs text-gray-600"> <svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5"></path> </svg> ${pushes} </span>`} ${updatedLabel && renderTemplate`<span class="text-[11px] text-gray-600">${updatedLabel}</span>`} </div> </div> </a>`;
}, "/Users/alex/projects/skillit/src/components/SkillCard.astro", void 0);
const PER_PAGE_GRID = 12;
const PER_PAGE_TABLE = 20;
const _sfc_main = /* @__PURE__ */ defineComponent({
__name: "SkillSearch",
props: {
authors: {},
tags: {},
totalCount: {}
},
setup(__props, { expose: __expose }) {
__expose();
const props = __props;
const authorList = props.authors ? props.authors.split(",").map((a) => a.trim()).filter(Boolean) : [];
const tagList = props.tags ? props.tags.split(",").map((t) => t.trim()).filter(Boolean) : [];
const query = ref("");
const forkFilter = ref("");
const savedView = typeof localStorage !== "undefined" && localStorage.getItem("skillsViewMode");
const viewMode = ref(savedView === "table" ? "table" : "grid");
const currentPage = ref(1);
const filteredCount = ref(props.totalCount || 0);
const perPage = computed(() => viewMode.value === "grid" ? PER_PAGE_GRID : PER_PAGE_TABLE);
const totalPages = computed(() => Math.max(1, Math.ceil(filteredCount.value / perPage.value)));
const rangeStart = computed(() => filteredCount.value === 0 ? 0 : (currentPage.value - 1) * perPage.value + 1);
const rangeEnd = computed(() => Math.min(currentPage.value * perPage.value, filteredCount.value));
const visiblePages = computed(() => {
const pages = [];
const total = totalPages.value;
const cur = currentPage.value;
const maxVisible = 7;
if (total <= maxVisible) {
for (let i = 1; i <= total; i++) pages.push(i);
} else {
const half = Math.floor(maxVisible / 2);
let start = Math.max(1, cur - half);
let end = start + maxVisible - 1;
if (end > total) {
end = total;
start = end - maxVisible + 1;
}
for (let i = start; i <= end; i++) pages.push(i);
}
return pages;
});
const selectedAuthors = ref([]);
const authorQuery = ref("");
const authorOpen = ref(false);
const authorInputEl = ref();
const authorSuggestions = computed(() => {
const q = authorQuery.value.toLowerCase().trim();
const selected = new Set(selectedAuthors.value.map((a) => a.toLowerCase()));
return authorList.filter((a) => !selected.has(a.toLowerCase()) && (!q || a.toLowerCase().includes(q)));
});
let authorBlurTimer;
function onAuthorBlur() {
authorBlurTimer = setTimeout(() => {
authorOpen.value = false;
}, 200);
}
function addAuthor(author) {
clearTimeout(authorBlurTimer);
if (!selectedAuthors.value.some((a) => a.toLowerCase() === author.toLowerCase())) {
selectedAuthors.value.push(author);
}
authorQuery.value = "";
authorInputEl.value?.focus();
}
function removeAuthor(author) {
selectedAuthors.value = selectedAuthors.value.filter((a) => a !== author);
}
function onAuthorEnter() {
if (authorSuggestions.value.length > 0) {
addAuthor(authorSuggestions.value[0]);
}
}
function onAuthorBackspace() {
if (!authorQuery.value && selectedAuthors.value.length > 0) {
selectedAuthors.value.pop();
}
}
const selectedTags = ref([]);
const tagQuery = ref("");
const tagOpen = ref(false);
const tagInputEl = ref();
const tagSuggestions = computed(() => {
const q = tagQuery.value.toLowerCase().trim();
const selected = new Set(selectedTags.value.map((t) => t.toLowerCase()));
return tagList.filter((t) => !selected.has(t.toLowerCase()) && (!q || t.toLowerCase().includes(q)));
});
let tagBlurTimer;
function onTagBlur() {
tagBlurTimer = setTimeout(() => {
tagOpen.value = false;
}, 200);
}
function addTag(tag) {
clearTimeout(tagBlurTimer);
if (!selectedTags.value.some((t) => t.toLowerCase() === tag.toLowerCase())) {
selectedTags.value.push(tag);
}
tagQuery.value = "";
tagInputEl.value?.focus();
}
function removeTag(tag) {
selectedTags.value = selectedTags.value.filter((t) => t !== tag);
}
function onTagEnter() {
if (tagSuggestions.value.length > 0) {
addTag(tagSuggestions.value[0]);
}
}
function onTagBackspace() {
if (!tagQuery.value && selectedTags.value.length > 0) {
selectedTags.value.pop();
}
}
function setView(mode) {
viewMode.value = mode;
localStorage.setItem("skillsViewMode", mode);
const grid = document.getElementById("skills-grid");
const table = document.getElementById("skills-table");
if (grid && table) {
grid.classList.toggle("hidden", mode !== "grid");
table.classList.toggle("hidden", mode !== "table");
}
currentPage.value = 1;
nextTick(() => applyFilters());
}
onMounted(() => {
if (viewMode.value !== "grid") {
setView(viewMode.value);
}
});
const hasActiveFilters = computed(
() => query.value || selectedAuthors.value.length > 0 || selectedTags.value.length > 0 || forkFilter.value
);
function applyFilters() {
const q = query.value.toLowerCase().trim();
const authors = selectedAuthors.value.map((a) => a.toLowerCase());
const tags = selectedTags.value.map((t) => t.toLowerCase());
const minForks = forkFilter.value ? parseInt(forkFilter.value) : 0;
const activeId = viewMode.value === "grid" ? "skills-grid" : "skills-table";
const inactiveId = viewMode.value === "grid" ? "skills-table" : "skills-grid";
const activeItems = Array.from(document.querySelectorAll(`#${activeId} [data-skill]`));
const inactiveItems = document.querySelectorAll(`#${inactiveId} [data-skill]`);
inactiveItems.forEach((el) => el.style.display = "none");
const matching = [];
activeItems.forEach((card) => {
const name = card.dataset.name || "";
const desc = card.dataset.description || "";
const tools = card.dataset.tools || "";
const cardAuthor = card.dataset.author || "";
const cardTags = (card.dataset.tags || "").split(",").filter(Boolean);
const forks = parseInt(card.dataset.forks || "0");
const matchText = !q || name.includes(q) || desc.includes(q) || tools.includes(q) || cardTags.some((t) => t.includes(q));
const matchAuthor = authors.length === 0 || authors.some((a) => cardAuthor.includes(a));
const matchTag = tags.length === 0 || tags.every((t) => cardTags.includes(t));
const matchForks = forks >= minForks;
if (matchText && matchAuthor && matchTag && matchForks) {
matching.push(card);
}
});
filteredCount.value = matching.length;
const maxPage = Math.max(1, Math.ceil(matching.length / perPage.value));
if (currentPage.value > maxPage) {
currentPage.value = maxPage;
}
const start = (currentPage.value - 1) * perPage.value;
const end = start + perPage.value;
activeItems.forEach((card) => {
const idx = matching.indexOf(card);
if (idx === -1) {
card.style.display = "none";
} else if (idx >= start && idx < end) {
card.style.display = "";
} else {
card.style.display = "none";
}
});
}
function goToPage(page) {
if (page < 1 || page > totalPages.value) return;
currentPage.value = page;
applyFilters();
}
function reset() {
query.value = "";
selectedAuthors.value = [];
authorQuery.value = "";
selectedTags.value = [];
tagQuery.value = "";
forkFilter.value = "";
currentPage.value = 1;
}
watch([query, selectedAuthors, selectedTags, forkFilter], () => {
currentPage.value = 1;
applyFilters();
}, { deep: true });
const __returned__ = { props, authorList, tagList, query, forkFilter, savedView, viewMode, currentPage, filteredCount, PER_PAGE_GRID, PER_PAGE_TABLE, perPage, totalPages, rangeStart, rangeEnd, visiblePages, selectedAuthors, authorQuery, authorOpen, authorInputEl, authorSuggestions, get authorBlurTimer() {
return authorBlurTimer;
}, set authorBlurTimer(v) {
authorBlurTimer = v;
}, onAuthorBlur, addAuthor, removeAuthor, onAuthorEnter, onAuthorBackspace, selectedTags, tagQuery, tagOpen, tagInputEl, tagSuggestions, get tagBlurTimer() {
return tagBlurTimer;
}, set tagBlurTimer(v) {
tagBlurTimer = v;
}, onTagBlur, addTag, removeTag, onTagEnter, onTagBackspace, setView, hasActiveFilters, applyFilters, goToPage, reset };
Object.defineProperty(__returned__, "__isScriptSetup", { enumerable: false, value: true });
return __returned__;
}
});
function _sfc_ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
_push(`<div${ssrRenderAttrs(mergeProps({ class: "mb-6 space-y-4" }, _attrs))}><div class="flex flex-wrap items-end gap-3"><div class="w-full sm:w-auto sm:flex-1 sm:min-w-[180px]"><label class="block text-xs font-medium uppercase tracking-wider text-gray-600 mb-1">Search</label><div class="relative"><svg class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"></path></svg><input${ssrRenderAttr("value", $setup.query)} type="text" placeholder="Name, description, tools..." class="w-full rounded-xl border border-white/[0.06] bg-[var(--color-surface-100)] pl-10 pr-4 py-2.5 text-sm text-white placeholder-gray-600 focus:border-[var(--color-accent-500)]/50 focus:outline-none focus:ring-1 focus:ring-[var(--color-accent-500)]/20 transition-all"></div></div><div class="w-[calc(50%-6px)] sm:w-auto sm:flex-1 sm:min-w-[160px] relative"><label class="block text-xs font-medium uppercase tracking-wider text-gray-600 mb-1">Authors</label><div class="flex flex-wrap items-center gap-1.5 rounded-xl border border-white/[0.06] bg-[var(--color-surface-100)] px-3 py-2 min-h-[42px] focus-within:border-[var(--color-accent-500)]/50 focus-within:ring-1 focus-within:ring-[var(--color-accent-500)]/20 transition-all"><!--[-->`);
ssrRenderList($setup.selectedAuthors, (a) => {
_push(`<span class="inline-flex items-center gap-1 rounded-full bg-[var(--color-accent-500)]/15 text-[var(--color-accent-400)] px-2.5 py-0.5 text-xs font-medium">${ssrInterpolate(a)} <button class="hover:text-white transition-colors">×</button></span>`);
});
_push(`<!--]--><input${ssrRenderAttr("value", $setup.authorQuery)} type="text"${ssrRenderAttr("placeholder", $setup.selectedAuthors.length ? "" : "Filter by author...")} class="flex-1 min-w-[80px] bg-transparent text-sm text-white placeholder-gray-600 outline-none"></div>`);
if ($setup.authorOpen && $setup.authorSuggestions.length > 0) {
_push(`<div class="absolute z-20 mt-1 w-full max-h-48 overflow-y-auto rounded-xl border border-white/[0.06] bg-[var(--color-surface-100)] shadow-xl"><!--[-->`);
ssrRenderList($setup.authorSuggestions, (a) => {
_push(`<button class="block w-full text-left px-4 py-2 text-sm text-gray-400 hover:bg-white/[0.06] hover:text-white transition-colors">${ssrInterpolate(a)}</button>`);
});
_push(`<!--]--></div>`);
} else {
_push(`<!---->`);
}
_push(`</div><div class="w-[calc(50%-6px)] sm:w-auto sm:flex-1 sm:min-w-[160px] relative"><label class="block text-xs font-medium uppercase tracking-wider text-gray-600 mb-1">Tags</label><div class="flex flex-wrap items-center gap-1.5 rounded-xl border border-white/[0.06] bg-[var(--color-surface-100)] px-3 py-2 min-h-[42px] focus-within:border-[var(--color-accent-500)]/50 focus-within:ring-1 focus-within:ring-[var(--color-accent-500)]/20 transition-all"><!--[-->`);
ssrRenderList($setup.selectedTags, (t) => {
_push(`<span class="inline-flex items-center gap-1 rounded-full bg-[var(--color-accent-500)]/15 text-[var(--color-accent-400)] px-2.5 py-0.5 text-xs font-medium">${ssrInterpolate(t)} <button class="hover:text-white transition-colors">×</button></span>`);
});
_push(`<!--]--><input${ssrRenderAttr("value", $setup.tagQuery)} type="text"${ssrRenderAttr("placeholder", $setup.selectedTags.length ? "" : "Filter by tag...")} class="flex-1 min-w-[80px] bg-transparent text-sm text-white placeholder-gray-600 outline-none"></div>`);
if ($setup.tagOpen && $setup.tagSuggestions.length > 0) {
_push(`<div class="absolute z-20 mt-1 w-full max-h-48 overflow-y-auto rounded-xl border border-white/[0.06] bg-[var(--color-surface-100)] shadow-xl"><!--[-->`);
ssrRenderList($setup.tagSuggestions, (t) => {
_push(`<button class="block w-full text-left px-4 py-2 text-sm text-gray-400 hover:bg-white/[0.06] hover:text-white transition-colors">${ssrInterpolate(t)}</button>`);
});
_push(`<!--]--></div>`);
} else {
_push(`<!---->`);
}
_push(`</div><div class="w-[calc(50%-6px)] sm:w-auto sm:min-w-[130px]"><label class="block text-xs font-medium uppercase tracking-wider text-gray-600 mb-1">Forks</label><select class="w-full rounded-xl border border-white/[0.06] bg-[var(--color-surface-100)] px-4 py-2.5 text-sm text-white focus:border-[var(--color-accent-500)]/50 focus:outline-none focus:ring-1 focus:ring-[var(--color-accent-500)]/20 transition-all"><option value=""${ssrIncludeBooleanAttr(Array.isArray($setup.forkFilter) ? ssrLooseContain($setup.forkFilter, "") : ssrLooseEqual($setup.forkFilter, "")) ? " selected" : ""}>Any</option><option value="1"${ssrIncludeBooleanAttr(Array.isArray($setup.forkFilter) ? ssrLooseContain($setup.forkFilter, "1") : ssrLooseEqual($setup.forkFilter, "1")) ? " selected" : ""}>1+ forks</option><option value="3"${ssrIncludeBooleanAttr(Array.isArray($setup.forkFilter) ? ssrLooseContain($setup.forkFilter, "3") : ssrLooseEqual($setup.forkFilter, "3")) ? " selected" : ""}>3+ forks</option><option value="5"${ssrIncludeBooleanAttr(Array.isArray($setup.forkFilter) ? ssrLooseContain($setup.forkFilter, "5") : ssrLooseEqual($setup.forkFilter, "5")) ? " selected" : ""}>5+ forks</option></select></div>`);
if ($setup.hasActiveFilters) {
_push(`<button class="rounded-xl border border-white/[0.06] bg-[var(--color-surface-100)] px-4 py-2.5 text-sm text-gray-500 hover:text-white hover:bg-white/[0.06] transition-all"> Clear </button>`);
} else {
_push(`<!---->`);
}
_push(`<div class="flex rounded-xl border border-white/[0.06] overflow-hidden"><button class="${ssrRenderClass(["px-3 py-2.5 transition-all", $setup.viewMode === "grid" ? "bg-white/[0.08] text-white" : "text-gray-600 hover:text-gray-400 hover:bg-white/[0.03]"])}" title="Grid view"><svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 0 1 6 3.75h2.25A2.25 2.25 0 0 1 10.5 6v2.25a2.25 2.25 0 0 1-2.25 2.25H6a2.25 2.25 0 0 1-2.25-2.25V6ZM3.75 15.75A2.25 2.25 0 0 1 6 13.5h2.25a2.25 2.25 0 0 1 2.25 2.25V18a2.25 2.25 0 0 1-2.25 2.25H6A2.25 2.25 0 0 1 3.75 18v-2.25ZM13.5 6a2.25 2.25 0 0 1 2.25-2.25H18A2.25 2.25 0 0 1 20.25 6v2.25A2.25 2.25 0 0 1 18 10.5h-2.25a2.25 2.25 0 0 1-2.25-2.25V6ZM13.5 15.75a2.25 2.25 0 0 1 2.25-2.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-2.25A2.25 2.25 0 0 1 13.5 18v-2.25Z"></path></svg></button><button class="${ssrRenderClass(["px-3 py-2.5 transition-all", $setup.viewMode === "table" ? "bg-white/[0.08] text-white" : "text-gray-600 hover:text-gray-400 hover:bg-white/[0.03]"])}" title="Table view"><svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 0 1 0 3.75H5.625a1.875 1.875 0 0 1 0-3.75Z"></path></svg></button></div></div>`);
if ($setup.totalPages > 1) {
_push(`<div class="flex flex-wrap items-center justify-between gap-3"><span class="text-sm text-gray-500"> Showing ${ssrInterpolate($setup.rangeStart)}${ssrInterpolate($setup.rangeEnd)} of ${ssrInterpolate($setup.filteredCount)}</span><div class="flex items-center gap-1"><button${ssrIncludeBooleanAttr($setup.currentPage === 1) ? " disabled" : ""} class="rounded-lg border border-white/[0.06] px-3 py-1.5 text-sm transition-all disabled:opacity-30 disabled:cursor-not-allowed text-gray-400 hover:text-white hover:bg-white/[0.06]"> Prev </button><!--[-->`);
ssrRenderList($setup.visiblePages, (p) => {
_push(`<button class="${ssrRenderClass(["rounded-lg px-3 py-1.5 text-sm transition-all", p === $setup.currentPage ? "bg-[var(--color-accent-500)] text-white font-medium" : "border border-white/[0.06] text-gray-400 hover:text-white hover:bg-white/[0.06]"])}">${ssrInterpolate(p)}</button>`);
});
_push(`<!--]--><button${ssrIncludeBooleanAttr($setup.currentPage === $setup.totalPages) ? " disabled" : ""} class="rounded-lg border border-white/[0.06] px-3 py-1.5 text-sm transition-all disabled:opacity-30 disabled:cursor-not-allowed text-gray-400 hover:text-white hover:bg-white/[0.06]"> Next </button></div></div>`);
} else {
_push(`<!---->`);
}
_push(`</div>`);
}
const _sfc_setup = _sfc_main.setup;
_sfc_main.setup = (props, ctx) => {
const ssrContext = useSSRContext();
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("src/components/SkillSearch.vue");
return _sfc_setup ? _sfc_setup(props, ctx) : void 0;
};
const SkillSearch = /* @__PURE__ */ _export_sfc(_sfc_main, [["ssrRender", _sfc_ssrRender]]);
const $$Astro = createAstro("https://skills.here.run.place");
const $$Index = createComponent(async ($$result, $$props, $$slots) => {
const Astro2 = $$result.createAstro($$Astro, $$props, $$slots);
Astro2.self = $$Index;
const accept = Astro2.request.headers.get("accept") || "";
if (!accept.includes("text/html")) {
const ps = isPowerShell(Astro2.request);
const script = ps ? await buildSyncScriptPS(Astro2.url.origin, ".claude\\skills") : await buildSyncScript(Astro2.url.origin, ".claude/skills");
return new Response(script, {
headers: { "Content-Type": "text/plain; charset=utf-8" }
});
}
const skills = await listSkills();
const forkCounts = /* @__PURE__ */ new Map();
for (const s of skills) {
if (s["fork-of"]) {
forkCounts.set(s["fork-of"], (forkCounts.get(s["fork-of"]) || 0) + 1);
}
}
const authors = [...new Set(skills.map((s) => s.author).filter(Boolean))].sort();
const allTags = [...new Set(skills.flatMap((s) => s.tags))].sort();
const allStats = await getAllStats();
return renderTemplate`${renderComponent($$result, "Base", $$Base, { "title": "Skills", "data-astro-cid-j7pv25f6": true }, { "default": async ($$result2) => renderTemplate`${skills.length === 0 ? renderTemplate`${maybeRenderHead()}<div class="text-center py-24" data-astro-cid-j7pv25f6> <div class="inline-flex h-16 w-16 items-center justify-center rounded-2xl bg-surface-200 border border-white/[0.06] mb-6" data-astro-cid-j7pv25f6> <svg class="h-7 w-7 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" data-astro-cid-j7pv25f6> <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m3.75 9v6m3-3H9m1.5-12H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" data-astro-cid-j7pv25f6></path> </svg> </div> <p class="text-gray-500 text-lg mb-2" data-astro-cid-j7pv25f6>No skills yet</p> <p class="text-gray-600 text-sm mb-6" data-astro-cid-j7pv25f6>Create your first skill to get started.</p> <a href="/new" class="inline-flex items-center gap-1.5 rounded-lg bg-accent-500 px-5 py-2.5 text-sm font-semibold text-white shadow-lg shadow-accent-500/20 hover:bg-accent-600 transition-all" data-astro-cid-j7pv25f6> <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5" data-astro-cid-j7pv25f6> <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" data-astro-cid-j7pv25f6></path> </svg>
Create your first skill
</a> </div>` : renderTemplate`<div data-astro-cid-j7pv25f6> <!-- Hero / Quick install --> <div class="mb-10 grid gap-6 lg:grid-cols-2 lg:items-start" data-astro-cid-j7pv25f6> <div class="rounded-2xl border border-white/[0.06] bg-surface-100 p-6" data-astro-cid-j7pv25f6> <h1 class="text-3xl font-extrabold tracking-tight text-white mb-2" data-astro-cid-j7pv25f6>Skills</h1> <p class="text-gray-500 mb-3" data-astro-cid-j7pv25f6>Manage and distribute Claude Code skills. Skills are prompt files (<code class="text-gray-400 font-mono bg-white/[0.04] px-1 py-0.5 rounded text-xs" data-astro-cid-j7pv25f6>.md</code>) that Claude loads automatically to learn custom behaviors and workflows.</p> <p class="text-gray-600 text-sm leading-relaxed" data-astro-cid-j7pv25f6>Create reusable skills to standardize how Claude handles commits, code reviews, testing, deployments, and more across your team. Share them instantly with a single curl command.</p> </div> <!-- Quick install + Quick push --> <div class="space-y-2" data-astro-cid-j7pv25f6> <!-- Quick install --> <details class="group rounded-2xl border border-white/[0.06] bg-surface-100" data-astro-cid-j7pv25f6> <summary class="flex items-center justify-between cursor-pointer px-6 py-4 select-none" data-astro-cid-j7pv25f6> <h2 class="text-sm font-semibold text-white" data-astro-cid-j7pv25f6>Quick install</h2> <div class="flex items-center gap-3" data-astro-cid-j7pv25f6> <div class="hidden group-open:flex rounded-lg border border-white/[0.06] overflow-hidden os-tabs" onclick="event.stopPropagation()" data-astro-cid-j7pv25f6> <button data-os="unix" class="os-tab px-2.5 py-1 text-[11px] font-medium transition-all" data-astro-cid-j7pv25f6>macOS / Linux</button> <button data-os="win" class="os-tab px-2.5 py-1 text-[11px] font-medium transition-all" data-astro-cid-j7pv25f6>Windows</button> </div> <svg class="h-4 w-4 text-gray-600 group-open:rotate-180 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" data-astro-cid-j7pv25f6> <path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" data-astro-cid-j7pv25f6></path> </svg> </div> </summary> <div class="px-6 pb-5 space-y-3" data-astro-cid-j7pv25f6> <p class="text-xs text-gray-500 leading-relaxed" data-astro-cid-j7pv25f6>Sync all skills to your project. They'll be saved to <code class="text-gray-400 font-mono bg-white/[0.04] px-1 py-0.5 rounded" data-astro-cid-j7pv25f6>.claude/skills/</code> and Claude Code picks them up on the next conversation.</p> <div class="flex items-center gap-3 rounded-xl bg-surface-50 border border-white/[0.06] px-4 py-3" data-astro-cid-j7pv25f6> <code data-cmd="unix" class="flex-1 text-sm font-mono text-gray-400 select-all truncate" data-astro-cid-j7pv25f6>curl -fsSL ${Astro2.url.origin} | bash</code> <code data-cmd="win" class="flex-1 text-sm font-mono text-gray-400 select-all truncate hidden" data-astro-cid-j7pv25f6>irm ${Astro2.url.origin} | iex</code> <button data-copy class="shrink-0 rounded-md bg-white/[0.06] border border-white/[0.06] px-2.5 py-1 text-xs font-medium text-gray-400 hover:text-white hover:bg-white/[0.1] transition-all" data-astro-cid-j7pv25f6>Copy</button> </div> <details class="group/inner" data-astro-cid-j7pv25f6> <summary class="text-xs text-gray-600 cursor-pointer hover:text-gray-400 transition-colors" data-astro-cid-j7pv25f6>Install globally</summary> <div class="mt-2" data-astro-cid-j7pv25f6> <div class="flex items-center gap-3 rounded-lg bg-surface-50 border border-white/[0.06] px-3 py-2" data-astro-cid-j7pv25f6> <code data-cmd="unix" class="flex-1 text-xs font-mono text-gray-400 select-all truncate" data-astro-cid-j7pv25f6>curl -fsSL ${Astro2.url.origin}/gi | bash</code> <code data-cmd="win" class="flex-1 text-xs font-mono text-gray-400 select-all truncate hidden" data-astro-cid-j7pv25f6>irm ${Astro2.url.origin}/gi | iex</code> <button data-copy class="shrink-0 rounded bg-white/[0.06] border border-white/[0.06] px-2 py-0.5 text-xs font-medium text-gray-500 hover:text-white hover:bg-white/[0.1] transition-all" data-astro-cid-j7pv25f6>Copy</button> </div> </div> </details> </div> </details> <!-- Quick push --> <details class="group rounded-2xl border border-white/[0.06] bg-surface-100" data-astro-cid-j7pv25f6> <summary class="flex items-center justify-between cursor-pointer px-6 py-4 select-none" data-astro-cid-j7pv25f6> <h2 class="text-sm font-semibold text-white" data-astro-cid-j7pv25f6>Quick push</h2> <div class="flex items-center gap-3" data-astro-cid-j7pv25f6> <div class="hidden group-open:flex rounded-lg border border-white/[0.06] overflow-hidden os-tabs" onclick="event.stopPropagation()" data-astro-cid-j7pv25f6> <button data-os="unix" class="os-tab px-2.5 py-1 text-[11px] font-medium transition-all" data-astro-cid-j7pv25f6>macOS / Linux</button> <button data-os="win" class="os-tab px-2.5 py-1 text-[11px] font-medium transition-all" data-astro-cid-j7pv25f6>Windows</button> </div> <svg class="h-4 w-4 text-gray-600 group-open:rotate-180 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" data-astro-cid-j7pv25f6> <path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" data-astro-cid-j7pv25f6></path> </svg> </div> </summary> <div class="px-6 pb-5 space-y-3" data-astro-cid-j7pv25f6> <p class="text-xs text-gray-500 leading-relaxed" data-astro-cid-j7pv25f6>Push your local skills to the server. Run from your project root — it reads <code class="text-gray-400 font-mono bg-white/[0.04] px-1 py-0.5 rounded" data-astro-cid-j7pv25f6>.claude/skills/*.md</code> and uploads them.</p> <div class="flex items-center gap-3 rounded-xl bg-surface-50 border border-white/[0.06] px-4 py-3" data-astro-cid-j7pv25f6> <code data-cmd="unix" class="flex-1 text-sm font-mono text-gray-400 select-all truncate" data-astro-cid-j7pv25f6>curl -fsSL ${Astro2.url.origin}/p | bash</code> <code data-cmd="win" class="flex-1 text-sm font-mono text-gray-400 select-all truncate hidden" data-astro-cid-j7pv25f6>irm ${Astro2.url.origin}/p | iex</code> <button data-copy class="shrink-0 rounded-md bg-white/[0.06] border border-white/[0.06] px-2.5 py-1 text-xs font-medium text-gray-400 hover:text-white hover:bg-white/[0.1] transition-all" data-astro-cid-j7pv25f6>Copy</button> </div> <details class="group/inner" data-astro-cid-j7pv25f6> <summary class="text-xs text-gray-600 cursor-pointer hover:text-gray-400 transition-colors" data-astro-cid-j7pv25f6>Push a specific skill</summary> <div class="mt-2" data-astro-cid-j7pv25f6> <div class="flex items-center gap-3 rounded-lg bg-surface-50 border border-white/[0.06] px-3 py-2" data-astro-cid-j7pv25f6> <code data-cmd="unix" class="flex-1 text-xs font-mono text-gray-400 select-all truncate" data-astro-cid-j7pv25f6>curl -fsSL ${Astro2.url.origin}/p | bash -s skill-name</code> <code data-cmd="win" class="flex-1 text-xs font-mono text-gray-400 select-all truncate hidden" data-astro-cid-j7pv25f6>irm ${Astro2.url.origin}/p | iex -Args skill-name</code> <button data-copy class="shrink-0 rounded bg-white/[0.06] border border-white/[0.06] px-2 py-0.5 text-xs font-medium text-gray-500 hover:text-white hover:bg-white/[0.1] transition-all" data-astro-cid-j7pv25f6>Copy</button> </div> </div> </details> </div> </details> </div> </div> <!-- Search + Grid + Table --> ${renderComponent($$result2, "SkillSearch", SkillSearch, { "authors": authors.join(","), "tags": allTags.join(","), "totalCount": skills.length, "client:load": true, "client:component-hydration": "load", "client:component-path": "/Users/alex/projects/skillit/src/components/SkillSearch.vue", "client:component-export": "default", "data-astro-cid-j7pv25f6": true })} <div id="skills-grid" class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3" data-astro-cid-j7pv25f6> ${skills.map((skill) => renderTemplate`<div data-skill${addAttribute(skill.name.toLowerCase(), "data-name")}${addAttribute(skill.description.toLowerCase(), "data-description")}${addAttribute(skill["allowed-tools"].join(" ").toLowerCase(), "data-tools")}${addAttribute(skill.author.toLowerCase(), "data-author")}${addAttribute(skill.tags.join(",").toLowerCase(), "data-tags")}${addAttribute(String(forkCounts.get(skill.slug) || 0), "data-forks")} data-astro-cid-j7pv25f6> ${renderComponent($$result2, "SkillCard", $$SkillCard, { ...skill, "forkCount": forkCounts.get(skill.slug) || 0, "downloads": allStats[skill.slug]?.downloads || 0, "pushes": allStats[skill.slug]?.pushes || 0, "lastPushedAt": allStats[skill.slug]?.lastPushedAt || null, "data-astro-cid-j7pv25f6": true })} </div>`)} </div> <div id="skills-table" class="hidden overflow-x-auto rounded-2xl border border-white/[0.06]" data-astro-cid-j7pv25f6> <table class="w-full text-sm text-left" data-astro-cid-j7pv25f6> <thead class="bg-surface-100 border-b border-white/[0.06]" data-astro-cid-j7pv25f6> <tr data-astro-cid-j7pv25f6> <th class="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-gray-500" data-astro-cid-j7pv25f6>Name</th> <th class="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-gray-500" data-astro-cid-j7pv25f6>Description</th> <th class="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-gray-500" data-astro-cid-j7pv25f6>Tools</th> <th class="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-gray-500" data-astro-cid-j7pv25f6>Tags</th> <th class="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-gray-500" data-astro-cid-j7pv25f6>Author</th> <th class="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-gray-500" data-astro-cid-j7pv25f6>Updated</th> </tr> </thead> <tbody class="divide-y divide-white/[0.04]" data-astro-cid-j7pv25f6> ${skills.map((skill) => {
const desc = skill.description.length > 80 ? skill.description.slice(0, 80) + "..." : skill.description;
const fc = forkCounts.get(skill.slug) || 0;
allStats[skill.slug] || { };
return renderTemplate`<tr data-skill${addAttribute(skill.name.toLowerCase(), "data-name")}${addAttribute(skill.description.toLowerCase(), "data-description")}${addAttribute(skill["allowed-tools"].join(" ").toLowerCase(), "data-tools")}${addAttribute(skill.author.toLowerCase(), "data-author")}${addAttribute(skill.tags.join(",").toLowerCase(), "data-tags")}${addAttribute(String(fc), "data-forks")} class="bg-surface-50 hover:bg-surface-100 transition-colors" data-astro-cid-j7pv25f6> <td class="px-4 py-3 whitespace-nowrap" data-astro-cid-j7pv25f6> <a${addAttribute(`/${skill.slug}`, "href")} class="font-medium text-white hover:text-accent-400 transition-colors" data-astro-cid-j7pv25f6>${skill.name}</a> </td> <td class="px-4 py-3 text-gray-500 max-w-xs truncate" data-astro-cid-j7pv25f6>${desc}</td> <td class="px-4 py-3" data-astro-cid-j7pv25f6> <div class="flex flex-wrap gap-1" data-astro-cid-j7pv25f6> ${skill["allowed-tools"].map((tool) => renderTemplate`<span class="rounded-md bg-white/[0.04] border border-white/[0.06] px-1.5 py-0.5 text-[11px] font-medium text-gray-400" data-astro-cid-j7pv25f6>${tool}</span>`)} </div> </td> <td class="px-4 py-3" data-astro-cid-j7pv25f6> <div class="flex flex-wrap gap-1" data-astro-cid-j7pv25f6> ${skill.tags.map((tag) => renderTemplate`<span class="rounded-full bg-[var(--color-accent-500)]/10 border border-[var(--color-accent-500)]/20 px-2 py-0.5 text-[11px] font-medium text-[var(--color-accent-400)]" data-astro-cid-j7pv25f6>${tag}</span>`)} </div> </td> <td class="px-4 py-3 text-gray-500 whitespace-nowrap" data-astro-cid-j7pv25f6>${skill.author || "\u2014"}</td> <td class="px-4 py-3 text-gray-500 whitespace-nowrap text-xs" data-astro-cid-j7pv25f6>${allStats[skill.slug]?.lastPushedAt ? new Date(allStats[skill.slug].lastPushedAt).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) : "\u2014"}</td> </tr>`;
})} </tbody> </table> </div> </div>`}` })} ${renderScript($$result, "/Users/alex/projects/skillit/src/pages/index.astro?astro&type=script&index=0&lang.ts")}`;
}, "/Users/alex/projects/skillit/src/pages/index.astro", void 0);
const $$file = "/Users/alex/projects/skillit/src/pages/index.astro";
const $$url = "";
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
default: $$Index,
file: $$file,
url: $$url
}, Symbol.toStringTag, { value: 'Module' }));
const page = () => _page;
export { page };