| @@ -6,13 +6,24 @@ const { data, loading, error, refresh, exists, presets, presetUsedBy } = useCata |
| 6 | onMounted(refresh) | 6 | onMounted(refresh) |
| 7 | | 7 | |
| 8 | const sorted = computed(() => [...presets.value].sort((a, b) => a.name.localeCompare(b.name))) | 8 | const sorted = computed(() => [...presets.value].sort((a, b) => a.name.localeCompare(b.name))) |
| 9 | -const MAX_CHIPS = 12 | 9 | +const MAX_CHIPS = 6 |
| 10 | | 10 | |
| 11 | function chips(p: Preset) { | 11 | function chips(p: Preset) { |
| 12 | return CAT_ORDER.flatMap(t => (p.items[t] ?? []).map(name => ({ type: t as CatalogItemType, name }))) | 12 | return CAT_ORDER.flatMap(t => (p.items[t] ?? []).map(name => ({ type: t as CatalogItemType, name }))) |
| 13 | } | 13 | } |
| | 14 | +/** Item count per type, in display order, e.g. "35 skills · 2 subagents". */ |
| | 15 | +function counts(p: Preset) { |
| | 16 | + return CAT_ORDER |
| | 17 | + .map(t => ({ type: t, n: p.items[t]?.length ?? 0 })) |
| | 18 | + .filter(c => c.n) |
| | 19 | + .map((c) => { |
| | 20 | + const word = c.n > 1 ? CAT_META[c.type].label : CAT_META[c.type].singular |
| | 21 | + return { ...c, label: `${c.n} ${word.startsWith('MCP') ? word : word.toLowerCase()}` } |
| | 22 | + }) |
| | 23 | +} |
| | 24 | +const usedCount = (name: string) => presetUsedBy(name).length |
| 14 | const used = (name: string) => { | 25 | const used = (name: string) => { |
| 15 | - const n = presetUsedBy(name).length | 26 | + const n = usedCount(name) |
| 16 | return n ? `Enabled in ${n} project${n > 1 ? 's' : ''}` : 'Not enabled anywhere yet' | 27 | return n ? `Enabled in ${n} project${n > 1 ? 's' : ''}` : 'Not enabled anywhere yet' |
| 17 | } | 28 | } |
| 18 | </script> | 29 | </script> |
| @@ -22,7 +33,7 @@ const used = (name: string) => { |
| 22 | <CatalogHeader tab="presets" /> | 33 | <CatalogHeader tab="presets" /> |
| 23 | | 34 | |
| 24 | <div v-if="loading && !data" class="grid gap-3 [grid-template-columns:repeat(auto-fill,minmax(300px,1fr))]"> | 35 | <div v-if="loading && !data" class="grid gap-3 [grid-template-columns:repeat(auto-fill,minmax(300px,1fr))]"> |
| 25 | - <USkeleton v-for="i in 4" :key="i" class="h-40 rounded-[10px]" /> | 36 | + <USkeleton v-for="i in 4" :key="i" class="h-32 rounded-[10px]" /> |
| 26 | </div> | 37 | </div> |
| 27 | <SkNote v-else-if="error && !data" tone="accent"> | 38 | <SkNote v-else-if="error && !data" tone="accent"> |
| 28 | <p class="m-0 font-medium">Could not load the catalog</p> | 39 | <p class="m-0 font-medium">Could not load the catalog</p> |
| @@ -34,9 +45,11 @@ const used = (name: string) => { |
| 34 | <div v-else-if="data" class="grid gap-3 [grid-template-columns:repeat(auto-fill,minmax(300px,1fr))]"> | 45 | <div v-else-if="data" class="grid gap-3 [grid-template-columns:repeat(auto-fill,minmax(300px,1fr))]"> |
| 35 | <NuxtLink | 46 | <NuxtLink |
| 36 | to="/catalog/presets/new" | 47 | to="/catalog/presets/new" |
| 37 | - class="flex min-h-[150px] flex-col items-center justify-center gap-1 rounded-[10px] border border-dashed border-sk-line p-[18px] text-sk-muted transition-colors hover:border-sk-faint hover:text-sk-text" | 48 | + class="group flex min-h-[120px] flex-col items-center justify-center gap-2 rounded-[10px] border border-dashed border-sk-faint/50 bg-sk-panel/60 p-4 text-sk-muted transition-colors hover:border-sk-faint hover:bg-sk-panel hover:text-sk-text" |
| 38 | > | 49 | > |
| 39 | - <span class="text-xl leading-none" aria-hidden="true">+</span> | 50 | + <span class="grid size-8 place-items-center rounded-full bg-sk-sel text-sk-text transition-colors group-hover:bg-sk-ink group-hover:text-sk-onink"> |
| | 51 | + <UIcon name="i-lucide-plus" class="size-4" /> |
| | 52 | + </span> |
| 40 | <span class="font-medium">New preset</span> | 53 | <span class="font-medium">New preset</span> |
| 41 | </NuxtLink> | 54 | </NuxtLink> |
| 42 | | 55 | |
| @@ -44,22 +57,32 @@ const used = (name: string) => { |
| 44 | v-for="p in sorted" | 57 | v-for="p in sorted" |
| 45 | :key="p.name" | 58 | :key="p.name" |
| 46 | :to="`/catalog/presets/${encodeURIComponent(p.name)}`" | 59 | :to="`/catalog/presets/${encodeURIComponent(p.name)}`" |
| 47 | - class="flex flex-col gap-3 rounded-[10px] bg-sk-panel p-[18px] text-sk-text shadow-sk-ring transition-shadow hover:shadow-sk-ring-hover" | 60 | + class="flex flex-col rounded-[10px] bg-sk-panel text-sk-text shadow-sk-ring transition-shadow hover:shadow-sk-ring-hover" |
| 48 | > | 61 | > |
| 49 | - <div class="flex flex-col gap-[3px]"> | 62 | + <div class="flex flex-col gap-2.5 p-4"> |
| 50 | - <span class="text-sm font-semibold">{{ p.name }}</span> | 63 | + <div class="flex flex-col gap-[3px]"> |
| 51 | - <span v-if="p.description" class="text-pretty text-sk-muted">{{ p.description }}</span> | 64 | + <span class="text-sm font-semibold">{{ p.name }}</span> |
| | 65 | + <span v-if="p.description" class="line-clamp-2 text-pretty text-sk-muted">{{ p.description }}</span> |
| | 66 | + </div> |
| | 67 | + <div v-if="counts(p).length || p.includes.length" class="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-sk-muted"> |
| | 68 | + <span v-for="c in counts(p)" :key="c.type" class="inline-flex items-center gap-1"> |
| | 69 | + <UIcon :name="CAT_META[c.type].icon" class="size-3.5 text-sk-faint" />{{ c.label }} |
| | 70 | + </span> |
| | 71 | + <span v-if="p.includes.length" class="inline-flex items-center gap-1"> |
| | 72 | + <UIcon name="i-lucide-layers" class="size-3.5 text-sk-faint" /> |
| | 73 | + <span>Includes <template v-for="(inc, i) in p.includes" :key="inc"><span class="font-medium text-sk-text">{{ inc }}</span><template v-if="i < p.includes.length - 1">, </template></template></span> |
| | 74 | + </span> |
| | 75 | + </div> |
| | 76 | + <div class="flex flex-wrap gap-1"> |
| | 77 | + <SkItemChip v-for="c in chips(p).slice(0, MAX_CHIPS)" :key="`${c.type}/${c.name}`" :name="c.name" /> |
| | 78 | + <span v-if="chips(p).length > MAX_CHIPS" class="px-1 py-[3px] text-xs text-sk-faint">+{{ chips(p).length - MAX_CHIPS }} more</span> |
| | 79 | + <span v-if="!chips(p).length" class="text-xs text-sk-faint">{{ p.includes.length ? 'Only the included presets' : 'No items yet' }}</span> |
| | 80 | + </div> |
| 52 | </div> | 81 | </div> |
| 53 | - <span v-if="p.includes.length" class="text-xs text-sk-muted"> | 82 | + <div class="mt-auto flex items-center gap-1.5 border-t border-sk-line2 px-4 py-2.5 text-xs" :class="usedCount(p.name) ? 'text-sk-muted' : 'text-sk-faint'"> |
| 54 | - Includes preset | 83 | + <span class="size-1.5 rounded-full" :class="usedCount(p.name) ? 'bg-sk-ok' : 'bg-sk-line'" aria-hidden="true" /> |
| 55 | - <template v-for="(inc, i) in p.includes" :key="inc"><span class="font-medium text-sk-text">{{ inc }}</span><template v-if="i < p.includes.length - 1">, </template></template> | 84 | + {{ used(p.name) }} |
| 56 | - </span> | | |
| 57 | - <div class="flex flex-wrap gap-1"> | | |
| 58 | - <SkItemChip v-for="c in chips(p).slice(0, MAX_CHIPS)" :key="`${c.type}/${c.name}`" :name="c.name" :type="c.type" /> | | |
| 59 | - <span v-if="chips(p).length > MAX_CHIPS" class="px-1 py-[3px] text-xs text-sk-faint">+{{ chips(p).length - MAX_CHIPS }} more</span> | | |
| 60 | - <span v-if="!chips(p).length" class="text-xs text-sk-faint">{{ p.includes.length ? 'Only the included presets' : 'No items yet' }}</span> | | |
| 61 | </div> | 85 | </div> |
| 62 | - <span class="mt-auto text-xs text-sk-faint">{{ used(p.name) }}</span> | | |
| 63 | </NuxtLink> | 86 | </NuxtLink> |
| 64 | </div> | 87 | </div> |
| 65 | </SkPage> | 88 | </SkPage> |