Skip to content

Commit a281be6

Browse files
committed
feat: add package-playground button
1 parent 79f9d2c commit a281be6

2 files changed

Lines changed: 142 additions & 0 deletions

File tree

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<script setup lang="ts">
2+
import { onClickOutside } from '@vueuse/core'
3+
import type { PlaygroundLink } from '#shared/types'
4+
5+
const props = defineProps<{
6+
links: PlaygroundLink[]
7+
}>()
8+
9+
// Map provider id to icon class
10+
const providerIcons: Record<string, string> = {
11+
'stackblitz': 'i-simple-icons-stackblitz',
12+
'codesandbox': 'i-simple-icons-codesandbox',
13+
'codepen': 'i-simple-icons-codepen',
14+
'replit': 'i-simple-icons-replit',
15+
'gitpod': 'i-simple-icons-gitpod',
16+
'vue-playground': 'i-simple-icons-vuedotjs',
17+
'nuxt-new': 'i-simple-icons-nuxtdotjs',
18+
'vite-new': 'i-simple-icons-vite',
19+
'jsfiddle': 'i-carbon-code',
20+
}
21+
22+
// Map provider id to color class
23+
const providerColors: Record<string, string> = {
24+
'stackblitz': 'text-provider-stackblitz',
25+
'codesandbox': 'text-provider-codesandbox',
26+
'codepen': 'text-provider-codepen',
27+
'replit': 'text-provider-replit',
28+
'gitpod': 'text-provider-gitpod',
29+
'vue-playground': 'text-provider-vue',
30+
'nuxt-new': 'text-provider-nuxt',
31+
'vite-new': 'text-provider-vite',
32+
'jsfiddle': 'text-provider-jsfiddle',
33+
}
34+
35+
function getIcon(provider: string): string {
36+
return providerIcons[provider] || 'i-carbon-play'
37+
}
38+
39+
function getColor(provider: string): string {
40+
return providerColors[provider] || 'text-fg-muted'
41+
}
42+
43+
// Dropdown state
44+
const isOpen = ref(false)
45+
const dropdownRef = ref<HTMLElement>()
46+
47+
onClickOutside(dropdownRef, () => {
48+
isOpen.value = false
49+
})
50+
51+
// Single vs multiple
52+
const hasSingleLink = computed(() => props.links.length === 1)
53+
const hasMultipleLinks = computed(() => props.links.length > 1)
54+
const firstLink = computed(() => props.links[0])
55+
</script>
56+
57+
<template>
58+
<section v-if="links.length > 0" aria-labelledby="playgrounds-heading">
59+
<h2 id="playgrounds-heading" class="text-xs text-fg-subtle uppercase tracking-wider mb-3">
60+
Playgrounds
61+
</h2>
62+
63+
<div ref="dropdownRef" class="relative">
64+
<!-- Single link: direct button -->
65+
<AppTooltip v-if="hasSingleLink && firstLink" :text="firstLink.providerName" class="w-full">
66+
<a
67+
:href="firstLink.url"
68+
target="_blank"
69+
rel="noopener noreferrer"
70+
class="w-full flex items-center gap-2 px-3 py-2 text-sm font-mono bg-bg-muted border border-border rounded-md hover:border-border-hover hover:bg-bg-elevated transition-colors duration-200"
71+
>
72+
<span
73+
:class="[getIcon(firstLink.provider), getColor(firstLink.provider), 'w-4 h-4 shrink-0']"
74+
aria-hidden="true"
75+
/>
76+
<span class="truncate text-fg-muted">{{ firstLink.label }}</span>
77+
</a>
78+
</AppTooltip>
79+
80+
<!-- Multiple links: dropdown button -->
81+
<button
82+
v-if="hasMultipleLinks"
83+
type="button"
84+
class="w-full flex items-center justify-between gap-2 px-3 py-2 text-sm font-mono bg-bg-muted border border-border rounded-md hover:border-border-hover hover:bg-bg-elevated transition-colors duration-200"
85+
@click="isOpen = !isOpen"
86+
>
87+
<span class="flex items-center gap-2">
88+
<span class="i-carbon-play w-4 h-4 shrink-0 text-fg-muted" aria-hidden="true" />
89+
<span class="text-fg-muted">{{ links.length }} Playgrounds</span>
90+
</span>
91+
<span
92+
class="i-carbon-chevron-down w-3 h-3 text-fg-subtle transition-transform duration-200"
93+
:class="{ 'rotate-180': isOpen }"
94+
aria-hidden="true"
95+
/>
96+
</button>
97+
98+
<!-- Dropdown menu -->
99+
<Transition
100+
enter-active-class="transition duration-150 ease-out"
101+
enter-from-class="opacity-0 scale-95"
102+
enter-to-class="opacity-100 scale-100"
103+
leave-active-class="transition duration-100 ease-in"
104+
leave-from-class="opacity-100 scale-100"
105+
leave-to-class="opacity-0 scale-95"
106+
>
107+
<div
108+
v-if="isOpen && hasMultipleLinks"
109+
class="absolute top-full left-0 right-0 mt-1 bg-bg-elevated border border-border rounded-lg shadow-lg z-50 py-1 overflow-visible"
110+
>
111+
<AppTooltip v-for="link in links" :key="link.url" :text="link.providerName" class="block">
112+
<a
113+
:href="link.url"
114+
target="_blank"
115+
rel="noopener noreferrer"
116+
class="flex items-center gap-2 px-3 py-2 text-sm font-mono text-fg-muted hover:text-fg hover:bg-bg-muted transition-colors duration-150"
117+
@click="isOpen = false"
118+
>
119+
<span
120+
:class="[getIcon(link.provider), getColor(link.provider), 'w-4 h-4 shrink-0']"
121+
aria-hidden="true"
122+
/>
123+
<span class="truncate">{{ link.label }}</span>
124+
</a>
125+
</AppTooltip>
126+
</div>
127+
</Transition>
128+
</div>
129+
</section>
130+
</template>

uno.config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ export default defineConfig({
4949
kw: '#f97583', // keyword - red/pink
5050
comment: '#6a737d', // comment - gray
5151
},
52+
// Playground provider brand colors
53+
provider: {
54+
stackblitz: '#1389FD',
55+
codesandbox: '#FFCC00',
56+
codepen: '#47CF73',
57+
replit: '#F26207',
58+
gitpod: '#FFAE33',
59+
vue: '#4FC08D',
60+
nuxt: '#00DC82',
61+
vite: '#646CFF',
62+
jsfiddle: '#0084FF',
63+
},
5264
},
5365
animation: {
5466
keyframes: {

0 commit comments

Comments
 (0)