|
| 1 | +<template> |
| 2 | + <div v-if="isMermaid" class="mermaid-wrapper my-4"> |
| 3 | + <div |
| 4 | + class="mermaid-container group relative bg-stone-50 border border-stone-200 rounded-lg p-4" |
| 5 | + > |
| 6 | + <div ref="mermaidRef" class="mermaid"></div> |
| 7 | + <Icon |
| 8 | + name="lucide:expand" |
| 9 | + class="w-10 h-10 p-2 absolute top-3 right-3 z-50 cursor-pointer rounded-lg bg-stone-800 border-2 border-stone-600 text-white shadow-lg hover:bg-stone-900 opacity-0 group-hover:opacity-100 transition-all" |
| 10 | + title="Ver em tela cheia" |
| 11 | + @click="isModalOpen = true" |
| 12 | + /> |
| 13 | + </div> |
| 14 | + |
| 15 | + <Dialog v-model:open="isModalOpen"> |
| 16 | + <DialogScrollContent class="max-w-[95vw] h-[90vh] p-6"> |
| 17 | + <DialogTitle class="sr-only">Diagrama Mermaid</DialogTitle> |
| 18 | + <div |
| 19 | + class="modal-diagram w-full h-full flex items-center justify-center" |
| 20 | + v-html="svgContent" |
| 21 | + ></div> |
| 22 | + </DialogScrollContent> |
| 23 | + </Dialog> |
| 24 | + </div> |
| 25 | + <pre v-else :class="$attrs.class"><slot /></pre> |
| 26 | +</template> |
| 27 | + |
| 28 | +<script setup lang="ts"> |
| 29 | +import { |
| 30 | + Dialog, |
| 31 | + DialogScrollContent, |
| 32 | + DialogTitle, |
| 33 | +} from "@/components/ui/dialog"; |
| 34 | +import { computed, onMounted, ref, useSlots, type VNode } from "vue"; |
| 35 | +
|
| 36 | +type VNodeChild = VNode | string | number | boolean | null | undefined; |
| 37 | +const props = defineProps<{ |
| 38 | + language?: string; |
| 39 | + code?: string; |
| 40 | +}>(); |
| 41 | +
|
| 42 | +const slots = useSlots(); |
| 43 | +const mermaidRef = ref<HTMLElement | null>(null); |
| 44 | +const isModalOpen = ref(false); |
| 45 | +const svgContent = ref(""); |
| 46 | +
|
| 47 | +const isMermaid = computed(() => props.language === "mermaid"); |
| 48 | +
|
| 49 | +const getMermaidCode = (): string => { |
| 50 | + if (props.code) return props.code; |
| 51 | +
|
| 52 | + const slot = slots.default?.(); |
| 53 | + if (!slot) return ""; |
| 54 | +
|
| 55 | + const extractText = (node: VNodeChild): string => { |
| 56 | + if (node == null || typeof node === "boolean") return ""; |
| 57 | + if (typeof node === "string") return node; |
| 58 | + if (typeof node === "number") return String(node); |
| 59 | +
|
| 60 | + const vnode = node as VNode; |
| 61 | + if (vnode.children) { |
| 62 | + if (typeof vnode.children === "string") return vnode.children; |
| 63 | + if (Array.isArray(vnode.children)) { |
| 64 | + return (vnode.children as VNodeChild[]).map(extractText).join(""); |
| 65 | + } |
| 66 | + } |
| 67 | + return ""; |
| 68 | + }; |
| 69 | +
|
| 70 | + return slot.map(extractText).join(""); |
| 71 | +}; |
| 72 | +
|
| 73 | +onMounted(async () => { |
| 74 | + if (isMermaid.value && mermaidRef.value) { |
| 75 | + const mermaid = (await import("mermaid")).default; |
| 76 | + mermaid.initialize({ |
| 77 | + startOnLoad: false, |
| 78 | + theme: "neutral", |
| 79 | + securityLevel: "loose", |
| 80 | + }); |
| 81 | +
|
| 82 | + const code = getMermaidCode(); |
| 83 | + const { svg } = await mermaid.render( |
| 84 | + `mermaid-${Math.random().toString(36).substr(2, 9)}`, |
| 85 | + code |
| 86 | + ); |
| 87 | + mermaidRef.value.innerHTML = svg; |
| 88 | + svgContent.value = svg; |
| 89 | + } |
| 90 | +}); |
| 91 | +</script> |
| 92 | + |
| 93 | +<style scoped> |
| 94 | +.mermaid-wrapper { |
| 95 | + position: relative; |
| 96 | +} |
| 97 | +
|
| 98 | +.modal-diagram :deep(svg) { |
| 99 | + width: 100% !important; |
| 100 | + height: auto !important; |
| 101 | + max-height: 100%; |
| 102 | +} |
| 103 | +</style> |
0 commit comments