Skip to content

Commit 71197bb

Browse files
committed
Merge branch 'main' into feat/shiki-inline-code
2 parents 60819c4 + 8da3c75 commit 71197bb

56 files changed

Lines changed: 12275 additions & 11574 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/commit-signature.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717

1818
steps:
1919
- name: Check commit signatures
20-
uses: actions/github-script@v7
20+
uses: actions/github-script@v9
2121
with:
2222
script: |
2323
const { owner, repo } = context.repo

‎.github/workflows/docs-preview-comment.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525

2626
steps:
2727
- name: Create or update preview comment
28-
uses: actions/github-script@v8
28+
uses: actions/github-script@v9
2929
env:
3030
SITE_URL: https://comark.dev
3131
CONTENT_DIR: docs/content

‎docs/app/app.config.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export default defineAppConfig({
1818
{ label: 'Plugins', sections: ['plugins'], link: 'section' as const },
1919
{ label: 'Examples', sections: ['examples'], link: 'section' as const },
2020
{ label: 'Playground', to: '/play' },
21+
{ label: 'Ecosystem', to: '/ecosystem' },
2122
],
2223
},
2324

‎docs/app/components/playground/RatingBar.vue‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,9 @@ const fillPercent = computed(() => `${(ratingNum.value / 5) * 100}%`)
5757
<UIcon
5858
v-for="i in 5"
5959
:key="i"
60-
name="i-heroicons-star-20-solid"
61-
class="size-3 shrink-0 text-yellow-400"
60+
name="i-lucide-star"
61+
mode="svg"
62+
class="size-3 shrink-0 fill-current text-yellow-400"
6263
/>
6364
</div>
6465
</div>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<template>
2+
<div
3+
class="not-prose mt-14 flex flex-col gap-4 rounded-lg border border-default bg-muted p-6 md:flex-row md:items-center md:justify-between"
4+
>
5+
<div>
6+
<p
7+
v-if="$slots.title"
8+
class="text-base font-semibold text-highlighted"
9+
>
10+
<slot
11+
name="title"
12+
unwrap="p"
13+
/>
14+
</p>
15+
<p
16+
v-if="$slots.description"
17+
class="mt-1 text-sm text-muted"
18+
>
19+
<slot
20+
name="description"
21+
unwrap="p"
22+
/>
23+
</p>
24+
</div>
25+
26+
<div
27+
v-if="$slots.ctas"
28+
class="flex shrink-0 items-center gap-3"
29+
>
30+
<slot name="ctas" />
31+
</div>
32+
</div>
33+
</template>
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
<script setup lang="ts">
2+
import { computed, onMounted, ref, watch } from 'vue'
3+
import { refDebounced } from '@vueuse/core'
4+
5+
interface EcosystemPackage {
6+
name: string
7+
description: string
8+
author: string
9+
category: 'Renderers' | 'Plugins' | 'Integrations' | 'Tooling'
10+
/** Tooling only: UI, Extensions, Templates, CLI, Agents. Shown in the badge instead of the category. */
11+
subcategory?: string
12+
/** "owner/name" — powers the row link and the author avatar. */
13+
repo?: string
14+
/** Link fallback for entries without a public repo. */
15+
url?: string
16+
official?: boolean
17+
icon?: string
18+
/** Tints the icon and its hover glow. Omitted entries fall back to a neutral tile. */
19+
color?: string
20+
}
21+
22+
const props = defineProps<{ packages: EcosystemPackage[] }>()
23+
24+
const categories = ['All', 'Renderers', 'Plugins', 'Integrations', 'Tooling'] as const
25+
26+
const route = useRoute()
27+
const router = useRouter()
28+
29+
const query = ref('')
30+
const officialOnly = ref(false)
31+
const category = ref<(typeof categories)[number]>('All')
32+
33+
// Content is ISR-cached, so the query string can't drive the first render
34+
onMounted(() => {
35+
const q = route.query.q
36+
if (typeof q === 'string') query.value = q
37+
if (route.query.official === '1') officialOnly.value = true
38+
const cat = route.query.category
39+
if (typeof cat === 'string') {
40+
const match = categories.find((c) => c.toLowerCase() === cat)
41+
if (match) category.value = match
42+
}
43+
})
44+
45+
const debouncedQuery = refDebounced(query, 200)
46+
47+
watch([debouncedQuery, officialOnly, category], ([q, official, cat]) => {
48+
const next = { ...route.query }
49+
if (q) next.q = q
50+
else delete next.q
51+
if (official) next.official = '1'
52+
else delete next.official
53+
if (cat !== 'All') next.category = cat.toLowerCase()
54+
else delete next.category
55+
router.replace({ query: next })
56+
})
57+
58+
const searchPool = computed(() => {
59+
const q = debouncedQuery.value.trim().toLowerCase()
60+
return props.packages.filter((pkg) => {
61+
if (officialOnly.value && !pkg.official) return false
62+
if (!q) return true
63+
return (
64+
pkg.name.toLowerCase().includes(q) ||
65+
pkg.description.toLowerCase().includes(q) ||
66+
pkg.author.toLowerCase().includes(q)
67+
)
68+
})
69+
})
70+
71+
const counts = computed(() => {
72+
const map = new Map<string, number>([['All', searchPool.value.length]])
73+
for (const cat of categories.slice(1)) {
74+
map.set(cat, searchPool.value.filter((pkg) => pkg.category === cat).length)
75+
}
76+
return map
77+
})
78+
79+
const visible = computed(() => {
80+
const pool =
81+
category.value === 'All' ? searchPool.value : searchPool.value.filter((pkg) => pkg.category === category.value)
82+
return [...pool].sort((a, b) => {
83+
if (!!a.official !== !!b.official) return a.official ? -1 : 1
84+
return a.name.localeCompare(b.name)
85+
})
86+
})
87+
88+
function href(pkg: EcosystemPackage) {
89+
return pkg.url ?? (pkg.repo ? `https://github.com/${pkg.repo}` : undefined)
90+
}
91+
92+
function avatarUrl(pkg: EcosystemPackage) {
93+
return `https://github.com/${pkg.author}.png?size=64`
94+
}
95+
96+
function tag(pkg: EcosystemPackage) {
97+
return pkg.category === 'Tooling' && pkg.subcategory ? pkg.subcategory : pkg.category
98+
}
99+
100+
function monogram(name: string) {
101+
const cleaned = name.replace(/^@[^/]+\//, '').replace(/^comark-?/i, '')
102+
return (cleaned || name).slice(0, 2).toUpperCase()
103+
}
104+
</script>
105+
106+
<template>
107+
<div class="not-prose">
108+
<div class="flex gap-3">
109+
<UInput
110+
v-model="query"
111+
icon="i-lucide-search"
112+
size="lg"
113+
placeholder="Search packages…"
114+
class="flex-1"
115+
:ui="{ base: 'rounded-lg' }"
116+
/>
117+
<button
118+
type="button"
119+
role="switch"
120+
:aria-checked="officialOnly"
121+
class="flex shrink-0 items-center gap-2 rounded-lg border border-default px-3 text-sm font-medium text-toned transition-colors hover:text-highlighted"
122+
@click="officialOnly = !officialOnly"
123+
>
124+
<span
125+
class="flex size-3.5 items-center justify-center rounded-xs border border-default transition-colors"
126+
:class="officialOnly ? 'border-inverted bg-inverted' : 'bg-default'"
127+
>
128+
<UIcon
129+
v-if="officialOnly"
130+
name="i-lucide-check"
131+
class="size-2.5 text-inverted"
132+
/>
133+
</span>
134+
Official only
135+
</button>
136+
</div>
137+
138+
<div class="mt-4 flex flex-wrap gap-2">
139+
<button
140+
v-for="cat in categories"
141+
:key="cat"
142+
type="button"
143+
:aria-pressed="category === cat"
144+
class="flex h-8 items-center gap-1.5 rounded-full px-3 text-sm font-medium transition-colors"
145+
:class="
146+
category === cat ? 'bg-inverted text-inverted' : 'border border-default text-toned hover:border-accented'
147+
"
148+
@click="category = cat"
149+
>
150+
{{ cat }}
151+
<span
152+
class="font-mono text-xs"
153+
:class="category === cat ? 'text-inverted/60' : 'text-dimmed'"
154+
>
155+
{{ counts.get(cat) }}
156+
</span>
157+
</button>
158+
</div>
159+
160+
<UCard
161+
variant="outline"
162+
class="mt-6"
163+
:ui="{ body: 'p-0 sm:p-0 divide-y divide-default' }"
164+
>
165+
<a
166+
v-for="pkg in visible"
167+
:key="pkg.name"
168+
:href="href(pkg)"
169+
target="_blank"
170+
rel="noopener"
171+
:class="pkg.color ? 'eco-row' : 'hover:bg-muted'"
172+
:style="pkg.color ? { '--eco-color': pkg.color } : undefined"
173+
class="grid grid-cols-[2.25rem_minmax(0,1fr)_auto] items-center gap-x-4 px-4 py-3.5 transition-colors md:grid-cols-[2.25rem_minmax(0,1fr)_7rem_1.25rem]"
174+
>
175+
<div
176+
class="eco-tile flex size-9 shrink-0 items-center justify-center rounded-lg border border-default bg-muted"
177+
:style="pkg.color ? { color: pkg.color } : undefined"
178+
>
179+
<UIcon
180+
v-if="pkg.icon"
181+
:name="pkg.icon"
182+
class="size-5"
183+
:class="!pkg.color && 'text-toned'"
184+
/>
185+
<span
186+
v-else
187+
class="font-mono text-xs font-medium"
188+
:class="!pkg.color && 'text-toned'"
189+
>{{ monogram(pkg.name) }}</span
190+
>
191+
</div>
192+
193+
<div class="min-w-0">
194+
<div class="flex flex-wrap items-center gap-x-2 gap-y-0.5">
195+
<span class="text-sm font-medium text-highlighted">{{ pkg.name }}</span>
196+
<span
197+
v-if="pkg.official"
198+
class="rounded-xs bg-primary-50 px-1.5 py-px text-xs font-medium text-primary-700 dark:bg-primary-900/15 dark:text-primary-400"
199+
>
200+
Official
201+
</span>
202+
<span class="inline-flex min-w-0 items-center gap-1.5 text-xs text-muted">
203+
<img
204+
:src="avatarUrl(pkg)"
205+
:alt="pkg.author"
206+
width="16"
207+
height="16"
208+
loading="lazy"
209+
class="size-4 shrink-0 rounded-full"
210+
/>
211+
<span class="truncate">{{ pkg.author }}</span>
212+
</span>
213+
</div>
214+
<p class="mt-0.5 truncate text-sm text-muted">{{ pkg.description }}</p>
215+
</div>
216+
217+
<span class="justify-self-end rounded-md border border-default px-2 py-0.5 font-mono text-xs text-muted">
218+
{{ tag(pkg) }}
219+
</span>
220+
221+
<UIcon
222+
name="i-lucide-arrow-up-right"
223+
class="hidden size-4 text-dimmed md:block"
224+
/>
225+
</a>
226+
227+
<p
228+
v-if="!visible.length"
229+
class="py-10 text-center text-sm text-muted"
230+
>
231+
No packages match.
232+
</p>
233+
</UCard>
234+
</div>
235+
</template>
236+
237+
<style scoped>
238+
.eco-tile {
239+
transition:
240+
background-color 0.2s,
241+
border-color 0.2s,
242+
box-shadow 0.2s;
243+
}
244+
245+
.eco-row:hover {
246+
background-color: color-mix(in oklab, var(--eco-color) 6%, transparent);
247+
}
248+
249+
.eco-row:hover .eco-tile {
250+
border-color: color-mix(in oklab, var(--eco-color) 45%, var(--ui-border));
251+
box-shadow:
252+
0 0 0 3px color-mix(in oklab, var(--eco-color) 10%, transparent),
253+
0 6px 16px -8px color-mix(in oklab, var(--eco-color) 35%, transparent);
254+
}
255+
</style>

0 commit comments

Comments
 (0)