Files
RenewCMS/web/src/components/CodeSnippet.vue
T

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>