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.
333 lines
36 KiB
JavaScript
333 lines
36 KiB
JavaScript
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 };
|