|
| 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> |
0 commit comments