121 lines
3.5 KiB
Vue
121 lines
3.5 KiB
Vue
<script setup lang="ts">
|
|
import { t } from '@/i18n'
|
|
|
|
import { computed, ref } from 'vue'
|
|
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
|
import type { CodeSnippetTab } from '@/lib/codeSnippet'
|
|
|
|
const props = defineProps<{
|
|
tabs: CodeSnippetTab[]
|
|
}>()
|
|
|
|
const active = defineModel<string>('active')
|
|
|
|
const { copied, copy } = useCopyToClipboard()
|
|
|
|
const tabList = ref<HTMLElement | null>(null)
|
|
const copyButton = ref<{ $el: HTMLElement } | null>(null)
|
|
|
|
const current = computed(() => props.tabs.find((tab) => tab.k === active.value) ?? props.tabs[0])
|
|
const single = computed(() => props.tabs.length === 1)
|
|
const copyLabel = computed(() => (copied.value ? t('Copied') : t('Copy')))
|
|
const copyIcon = computed(() => (copied.value ? 'i-lucide-check' : 'i-lucide-copy'))
|
|
|
|
const TAB_BASE =
|
|
'flex h-8 cursor-pointer items-center px-2.5 text-sm whitespace-nowrap focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary'
|
|
const TAB_SELECTED = 'font-medium text-highlighted shadow-[inset_0_-2px_0_var(--ui-primary)]'
|
|
const TAB_IDLE = 'font-normal text-toned'
|
|
|
|
function isSelected(tab: CodeSnippetTab) {
|
|
return tab.k === current.value?.k
|
|
}
|
|
|
|
function select(tab: CodeSnippetTab) {
|
|
active.value = tab.k
|
|
}
|
|
|
|
function targetIndex(key: string, from: number) {
|
|
const last = props.tabs.length - 1
|
|
const targets: Record<string, number> = {
|
|
ArrowRight: from === last ? 0 : from + 1,
|
|
ArrowLeft: from === 0 ? last : from - 1,
|
|
Home: 0,
|
|
End: last,
|
|
}
|
|
return targets[key]
|
|
}
|
|
|
|
function onTabKeydown(event: KeyboardEvent, index: number) {
|
|
const target = targetIndex(event.key, index)
|
|
if (target === undefined) {
|
|
return
|
|
}
|
|
event.preventDefault()
|
|
select(props.tabs[target]!)
|
|
const buttons = tabList.value?.querySelectorAll<HTMLElement>('[role="tab"]')
|
|
buttons?.[target]?.focus()
|
|
}
|
|
|
|
function onCopy() {
|
|
copy(current.value?.code ?? '')
|
|
}
|
|
|
|
function focusCopy() {
|
|
copyButton.value?.$el?.focus()
|
|
}
|
|
|
|
defineExpose({ focusCopy })
|
|
</script>
|
|
|
|
<template>
|
|
<div class="flex min-w-0 flex-col border border-default bg-elevated text-default">
|
|
<div class="flex min-h-11 items-center gap-2 border-b border-default pr-1 pl-2">
|
|
<div
|
|
ref="tabList"
|
|
role="tablist"
|
|
:aria-label="t('Language')"
|
|
class="flex min-w-0 flex-1 gap-0.5 overflow-x-auto"
|
|
>
|
|
<template v-if="single">
|
|
<span class="flex h-8 items-center px-2.5 text-sm font-normal text-muted">{{
|
|
current?.label
|
|
}}</span>
|
|
</template>
|
|
<template v-else>
|
|
<button
|
|
v-for="(tab, index) in tabs"
|
|
:key="tab.k"
|
|
type="button"
|
|
role="tab"
|
|
:aria-selected="isSelected(tab)"
|
|
:tabindex="isSelected(tab) ? 0 : -1"
|
|
:class="[TAB_BASE, isSelected(tab) ? TAB_SELECTED : TAB_IDLE]"
|
|
@click="select(tab)"
|
|
@keydown="onTabKeydown($event, index)"
|
|
>
|
|
{{ tab.label }}
|
|
</button>
|
|
</template>
|
|
</div>
|
|
<UButton
|
|
ref="copyButton"
|
|
:icon="copyIcon"
|
|
color="neutral"
|
|
variant="ghost"
|
|
:aria-label="t('Copy code')"
|
|
class="h-8 min-w-11 justify-center gap-1.5 rounded-none px-2.5 text-sm font-medium focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary"
|
|
:class="copied ? 'text-success' : 'text-highlighted'"
|
|
:ui="{ leadingIcon: 'size-3.5' }"
|
|
@click="onCopy"
|
|
>
|
|
<span aria-live="polite">{{ copyLabel }}</span>
|
|
</UButton>
|
|
</div>
|
|
<pre
|
|
tabindex="0"
|
|
class="m-0 overflow-x-auto px-4 py-3 font-mono text-code whitespace-pre text-highlighted"
|
|
>{{ current?.code }}</pre
|
|
>
|
|
</div>
|
|
</template>
|