Message Docs json schema

This commit is contained in:
simonredfern 2026-01-17 10:44:05 +01:00
parent f7b7dfb598
commit 6f9a5d14bd
4 changed files with 1064 additions and 0 deletions

View File

@ -0,0 +1,328 @@
<!--
- Open Bank Project - API Explorer II
- Copyright (C) 2023-2024, TESOBE GmbH
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
- Email: contact@tesobe.com
- TESOBE GmbH
- Osloerstrasse 16/17
- Berlin 13359, Germany
-
- This product includes software developed at
- TESOBE (http://www.tesobe.com/)
-
-->
<script setup lang="ts">
import { ref, computed } from 'vue'
import { DocumentCopy, Check } from '@element-plus/icons-vue'
interface Props {
schema: any
copyable?: boolean
}
const props = withDefaults(defineProps<Props>(), {
copyable: false
})
const emit = defineEmits<{
refClick: [refId: string]
}>()
const copied = ref(false)
const copyToClipboard = async () => {
try {
const text = JSON.stringify(props.schema, null, 2)
await navigator.clipboard.writeText(text)
copied.value = true
setTimeout(() => {
copied.value = false
}, 2000)
} catch (err) {
console.error('Failed to copy: ', err)
}
}
const handleRefClick = (event: Event, refId: string) => {
event.preventDefault()
emit('refClick', refId)
}
// Function to render JSON with clickable $ref links
const renderJsonWithRefs = (obj: any, indent: number = 0): any[] => {
const result: any[] = []
const indentStr = ' '.repeat(indent)
if (obj === null || obj === undefined) {
result.push({ type: 'value', text: String(obj) })
return result
}
if (typeof obj !== 'object') {
const valueClass = typeof obj === 'string' ? 'json-string' :
typeof obj === 'number' ? 'json-number' :
typeof obj === 'boolean' ? 'json-boolean' : 'json-value'
const displayValue = typeof obj === 'string' ? `"${obj}"` : String(obj)
result.push({ type: 'value', text: displayValue, class: valueClass })
return result
}
const isArray = Array.isArray(obj)
const openBracket = isArray ? '[' : '{'
const closeBracket = isArray ? ']' : '}'
result.push({ type: 'bracket', text: openBracket })
const entries = isArray ? obj.map((val, idx) => [idx, val]) : Object.entries(obj)
const totalEntries = entries.length
entries.forEach(([key, value], index) => {
const isLast = index === totalEntries - 1
const newIndent = indent + 1
const newIndentStr = ' '.repeat(newIndent)
result.push({ type: 'newline', text: '\n' })
result.push({ type: 'indent', text: newIndentStr })
// Add key for objects
if (!isArray) {
result.push({ type: 'key', text: `"${key}"`, class: 'json-key' })
result.push({ type: 'separator', text: ': ' })
}
// Check if this is a $ref
if (key === '$ref' && typeof value === 'string') {
// Extract definition name from $ref
const refMatch = value.match(/#\/definitions\/(.+)$/)
if (refMatch) {
const defName = refMatch[1]
result.push({ type: 'ref', text: `"${value}"`, href: `#def-${defName}`, defName })
} else {
result.push({ type: 'value', text: `"${value}"`, class: 'json-string' })
}
} else if (value && typeof value === 'object') {
// Recursively render nested objects/arrays
const nested = renderJsonWithRefs(value, newIndent)
result.push(...nested)
} else {
// Render primitive values
const valueClass = typeof value === 'string' ? 'json-string' :
typeof value === 'number' ? 'json-number' :
typeof value === 'boolean' ? 'json-boolean' :
value === null ? 'json-null' : 'json-value'
const displayValue = typeof value === 'string' ? `"${value}"` : String(value)
result.push({ type: 'value', text: displayValue, class: valueClass })
}
// Add comma if not last
if (!isLast) {
result.push({ type: 'comma', text: ',' })
}
})
if (totalEntries > 0) {
result.push({ type: 'newline', text: '\n' })
result.push({ type: 'indent', text: indentStr })
}
result.push({ type: 'bracket', text: closeBracket })
return result
}
const jsonElements = computed(() => {
return renderJsonWithRefs(props.schema)
})
</script>
<template>
<div class="json-schema-viewer">
<div v-if="copyable" class="schema-header">
<button
@click="copyToClipboard"
class="copy-button"
:class="{ 'copied': copied }"
>
<span v-if="!copied">
<el-icon :size="10">
<DocumentCopy />
</el-icon>
Copy
</span>
<span v-else>
<el-icon :size="10">
<Check />
</el-icon>
Copied!
</span>
</button>
</div>
<div class="schema-container">
<pre class="schema-pre"><code class="schema-code"><template v-for="(element, index) in jsonElements" :key="index"><span
v-if="element.type === 'ref'"
class="json-ref"
:title="`Jump to definition: ${element.defName}`"
><a :href="element.href" class="ref-link" @click="handleRefClick($event, element.href)">{{ element.text }}</a></span><span
v-else-if="element.type === 'key' || element.type === 'value'"
:class="element.class"
>{{ element.text }}</span><span v-else>{{ element.text }}</span></template></code></pre>
</div>
</div>
</template>
<style scoped>
.json-schema-viewer {
margin: 1rem 0;
border-radius: 8px;
overflow: hidden;
background: #1e1e1e;
border: 1px solid #333;
position: relative;
}
.schema-header {
background: #2d2d2d;
padding: 0.5rem 1rem;
border-bottom: 1px solid #333;
display: flex;
justify-content: flex-end;
}
.copy-button {
background: #444;
border: 1px solid #666;
color: #ddd;
padding: 0.25rem 0.75rem;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.25rem;
}
.copy-button:hover {
background: #555;
border-color: #777;
}
.copy-button.copied {
background: #4caf50;
border-color: #4caf50;
color: white;
}
.schema-container {
max-height: 500px;
overflow-y: auto;
}
.schema-pre {
margin: 0;
padding: 1.5rem;
background: #1e1e1e;
color: #ddd;
font-family: 'Fira Code', 'Courier New', monospace;
font-size: 14px;
line-height: 1.5;
overflow-x: auto;
}
.schema-code {
background: transparent;
padding: 0;
border-radius: 0;
font-family: inherit;
font-size: inherit;
white-space: pre;
}
/* JSON Syntax Highlighting */
.json-key {
color: #e06c75;
font-weight: 500;
}
.json-string {
color: #98c379;
}
.json-number {
color: #d19a66;
}
.json-boolean {
color: #56b6c2;
}
.json-null {
color: #c678dd;
}
.json-ref {
position: relative;
}
.ref-link {
color: #61afef;
text-decoration: underline;
text-decoration-style: dotted;
cursor: pointer;
transition: all 0.2s ease;
}
.ref-link:hover {
color: #84c5ff;
text-decoration-style: solid;
background-color: rgba(97, 175, 239, 0.1);
}
/* Custom scrollbar */
.schema-container::-webkit-scrollbar {
width: 8px;
}
.schema-container::-webkit-scrollbar-track {
background: #2d2d2d;
}
.schema-container::-webkit-scrollbar-thumb {
background: #555;
border-radius: 4px;
}
.schema-container::-webkit-scrollbar-thumb:hover {
background: #777;
}
.schema-pre::-webkit-scrollbar {
height: 8px;
}
.schema-pre::-webkit-scrollbar-track {
background: #2d2d2d;
}
.schema-pre::-webkit-scrollbar-thumb {
background: #555;
border-radius: 4px;
}
.schema-pre::-webkit-scrollbar-thumb:hover {
background: #777;
}
</style>

View File

@ -0,0 +1,183 @@
<!--
- Open Bank Project - API Explorer II
- Copyright (C) 2023-2024, TESOBE GmbH
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
- Email: contact@tesobe.com
- TESOBE GmbH
- Osloerstrasse 16/17
- Berlin 13359, Germany
-
- This product includes software developed at
- TESOBE (http://www.tesobe.com/)
-
-->
<script setup lang="ts">
import { reactive, ref, onBeforeMount, inject, watch } from 'vue'
import { Search } from '@element-plus/icons-vue'
import { useRoute } from 'vue-router'
import { SEARCH_LINKS_COLOR as searchLinksColorSetting } from '../obp/style-setting'
import { connectors } from '../obp/message-docs'
import { obpGroupedMessageDocsJsonSchemaKey } from '@/obp/keys'
let connector = connectors[0]
const route = useRoute()
const groupedMessageDocsJsonSchema = ref(inject(obpGroupedMessageDocsJsonSchemaKey) || {})
const docs = ref({})
const groups = ref({})
const sortedKeys = ref([])
const activeKeys = ref([])
const messageDocKeys = ref([])
const searchLinksColor = ref(searchLinksColorSetting)
const form = reactive({
search: ''
})
onBeforeMount(() => {
setDocs()
})
watch(
() => route.params.id,
async (id) => {
setDocs()
}
)
const isKeyFound = (keys, item) => keys.every((k) => item.toLowerCase().includes(k))
const filterKeys = (keys, key) => {
const splitKey = key.split(' ').map((k) => k.toLowerCase())
return keys.filter((title) => {
const isGroupFound = isKeyFound(splitKey, title)
const items = docs.value[title].filter((item) => isGroupFound || isKeyFound(splitKey, item))
groups.value[title] = items
return isGroupFound || items.length > 0
})
}
const searchEvent = (value) => {
if (value) {
messageDocKeys.value = filterKeys(activeKeys.value, value)
} else {
groups.value = JSON.parse(JSON.stringify(docs.value))
messageDocKeys.value = Object.keys(groups.value)
}
}
const setDocs = () => {
const paramConnector = route.params.id
if (connectors.includes(paramConnector)) {
connector = paramConnector
}
const messageDocsJsonSchemaData = groupedMessageDocsJsonSchema.value[connector]
const messageDocsJsonSchema = messageDocsJsonSchemaData?.grouped || messageDocsJsonSchemaData || {}
docs.value = Object.keys(messageDocsJsonSchema).reduce((doc, key) => {
doc[key] = messageDocsJsonSchema[key].map((group) => group.method_name)
return doc
}, {})
groups.value = JSON.parse(JSON.stringify(docs.value))
messageDocKeys.value = Object.keys(groups.value)
activeKeys.value = Object.keys(groups.value)
}
</script>
<template>
<el-row>
<el-col :span="24">
<el-input
v-model="form.search"
placeholder="Search"
:prefix-icon="Search"
@input="searchEvent"
/>
</el-col>
</el-row>
<el-collapse v-model="activeKeys">
<el-collapse-item v-for="key in messageDocKeys" :title="key" :key="key" :name="key">
<div class="el-tabs--right">
<div v-for="(value, key) of groups[key]" :key="value" class="message-docs-router-tab">
<a class="message-docs-router-link" :id="`${value}-quick-nav`" v-bind:href="`#${value}`">
{{ value }}
</a>
</div>
</div>
</el-collapse-item>
</el-collapse>
</template>
<style scoped>
.api-router-link {
width: 100%;
margin-left: 15px;
font-family: 'Roboto';
text-decoration: none;
color: #39455f;
display: inline-block;
word-wrap: break-word;
overflow-wrap: break-word;
word-break: break-word;
max-width: 100%;
}
.api-router-tab {
border-left: 2px solid var(--el-menu-border-color);
word-wrap: break-word;
overflow-wrap: break-word;
word-break: break-word;
}
.api-router-tab:hover,
.active-api-router-tab {
border-left: 2px solid v-bind(searchLinksColor);
}
.api-router-tab:hover .api-router-link,
.active-api-router-link {
color: v-bind(searchLinksColor);
}
.message-docs-router-link {
margin-left: 15px;
font-size: 13px;
font-family: 'Roboto';
text-decoration: none;
color: #39455f;
display: inline-block;
word-wrap: break-word;
overflow-wrap: break-word;
word-break: break-word;
max-width: 100%;
}
.message-docs-router-tab {
border-left: 2px solid var(--el-menu-border-color);
line-height: 30px;
word-wrap: break-word;
overflow-wrap: break-word;
word-break: break-word;
}
.message-docs-router-tab:hover,
.active-message-docs-router-tab {
border-left: 2px solid v-bind(searchLinksColor);
}
.message-docs-router-tab:hover .message-docs-router-link {
color: v-bind(searchLinksColor);
}
</style>

View File

@ -0,0 +1,114 @@
<!--
- Open Bank Project - API Explorer II
- Copyright (C) 2023-2024, TESOBE GmbH
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
- Email: contact@tesobe.com
- TESOBE GmbH
- Osloerstrasse 16/17
- Berlin 13359, Germany
-
- This product includes software developed at
- TESOBE (http://www.tesobe.com/)
-
-->
<script setup lang="ts">
import { ref, inject, computed } from 'vue'
import { useRouter } from 'vue-router'
import { obpGroupedMessageDocsJsonSchemaKey } from '@/obp/keys'
const router = useRouter()
const groupedMessageDocsJsonSchema = ref(inject(obpGroupedMessageDocsJsonSchemaKey) || {})
const connectorList = computed(() => {
return Object.keys(groupedMessageDocsJsonSchema.value || {}).sort()
})
function navigateToConnector(connectorId: string) {
router.push(`/message-docs-json-schema/${connectorId}`)
}
</script>
<template>
<el-container class="message-docs-list-container">
<el-main>
<h1>Message Documentation - JSON Schema</h1>
<p class="subtitle">Browse connector message documentation with JSON Schema definitions</p>
<div class="message-docs-list">
<div v-if="connectorList.length === 0" class="empty-message">
No JSON schema message documentation available
</div>
<div v-else>
<a
v-for="connector in connectorList"
:key="connector"
@click="navigateToConnector(connector)"
class="message-doc-link"
>
{{ connector }}
</a>
</div>
</div>
</el-main>
</el-container>
</template>
<style scoped>
.message-docs-list-container {
min-height: calc(100vh - 60px);
padding: 2rem;
}
h1 {
font-size: 1.5rem;
font-weight: 600;
color: #303133;
margin-bottom: 0.5rem;
}
.subtitle {
font-size: 0.9rem;
color: #909399;
margin-bottom: 1.5rem;
}
.message-docs-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.message-doc-link {
padding: 12px 16px;
color: #409eff;
text-decoration: none;
cursor: pointer;
border-radius: 4px;
transition: background-color 0.2s;
display: block;
}
.message-doc-link:hover {
background-color: #ecf5ff;
color: #337ecc;
}
.empty-message {
color: #909399;
font-style: italic;
}
</style>

View File

@ -0,0 +1,439 @@
<!--
- Open Bank Project - API Explorer II
- Copyright (C) 2023-2024, TESOBE GmbH
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
- Email: contact@tesobe.com
- TESOBE GmbH
- Osloerstrasse 16/17
- Berlin 13359, Germany
-
- This product includes software developed at
- TESOBE (http://www.tesobe.com/)
-
-->
<script setup lang="ts">
import { ref, onBeforeMount, inject, watch, onMounted, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import SearchNav from '../components/MessageDocsJsonSchemaSearchNav.vue'
import { connectors } from '../obp/message-docs'
import { obpGroupedMessageDocsJsonSchemaKey } from '@/obp/keys'
import JsonSchemaViewer from '../components/JsonSchemaViewer.vue'
let connector = connectors[0]
const route = useRoute()
const groupedMessageDocsJsonSchema = ref(inject(obpGroupedMessageDocsJsonSchemaKey) || {})
const messageDocsJsonSchema = ref(null as any)
const definitions = ref(null as any)
const definitionsPanelScrollbar = ref(null as any)
onBeforeMount(() => {
setDoc()
})
watch(
() => route.params.id,
async (id) => {
setDoc()
}
)
const setDoc = () => {
const paramConnector = route.params.id
if (connectors.includes(paramConnector)) {
connector = paramConnector
}
const data = groupedMessageDocsJsonSchema.value[connector]
// Handle both old and new data structures
if (data?.grouped) {
messageDocsJsonSchema.value = data.grouped
definitions.value = data.definitions
} else {
messageDocsJsonSchema.value = data
definitions.value = null
}
}
function hasSchema(value: any) {
return value && (value.outbound_schema || value.inbound_schema)
}
// Handle $ref link clicks from JsonSchemaViewer components
function handleRefClick(href: string) {
console.log('handleRefClick called with href:', href)
if (!href) {
console.log('No href provided')
return
}
// Extract the definition ID from the href (e.g., #def-BasicGeneralContext)
const targetId = href.substring(1) // Remove the #
console.log('Target ID:', targetId)
const targetElement = document.getElementById(targetId)
console.log('Target element:', targetElement)
console.log('Definitions panel scrollbar:', definitionsPanelScrollbar.value)
if (targetElement && definitionsPanelScrollbar.value) {
// Get the scrollbar's wrap element
const scrollWrap = definitionsPanelScrollbar.value.$refs.wrap as HTMLElement
console.log('Scroll wrap:', scrollWrap)
if (scrollWrap) {
// Calculate the position of the target element relative to the scrollable container
const containerTop = scrollWrap.getBoundingClientRect().top
const targetTop = targetElement.getBoundingClientRect().top
const currentScroll = scrollWrap.scrollTop
const offset = targetTop - containerTop + currentScroll - 20 // 20px padding from top
console.log('Scrolling to offset:', offset)
// Smooth scroll to the target
scrollWrap.scrollTo({
top: offset,
behavior: 'smooth'
})
// Add a highlight effect
targetElement.classList.add('highlight-definition')
setTimeout(() => {
targetElement.classList.remove('highlight-definition')
}, 2000)
} else {
console.log('No scroll wrap found')
}
} else {
console.log('Target element or scrollbar not found')
}
}
</script>
<template>
<el-container class="message-docs-container">
<el-aside class="search-nav" width="18%">
<el-scrollbar>
<SearchNav />
</el-scrollbar>
</el-aside>
<el-main class="message-docs-content">
<el-scrollbar>
<el-backtop :right="100" :bottom="100" />
<div class="message-docs-header">
<h1>{{ connector }}</h1>
<p class="connector-subtitle">Message Docs - JSON Schema</p>
<p class="version-indicator">v1.2.4 - Debug Click Events</p>
</div>
<div v-for="(group, key) of messageDocsJsonSchema" :key="key">
<div v-for="(value, key) of group" :key="value">
<el-divider></el-divider>
<a v-bind:href="`#${value.method_name}`" :id="value.method_name">
<h2>{{ value.method_name }}</h2>
</a>
<p v-if="value.description">{{ value.description }}</p>
<section class="topics">
<div>
<strong>Category: </strong>
<el-tag type="info" round>{{ value.category || 'Uncategorized' }}</el-tag>
</div>
<div v-if="value.message_format">
<strong>Message Format: </strong>
<el-tag type="success" round>{{ value.message_format }}</el-tag>
</div>
</section>
<section v-if="value.outbound_schema">
<h3>Outbound Schema</h3>
<JsonSchemaViewer :schema="value.outbound_schema" copyable @refClick="handleRefClick" />
</section>
<section v-if="value.inbound_schema">
<h3>Inbound Schema</h3>
<JsonSchemaViewer :schema="value.inbound_schema" copyable @refClick="handleRefClick" />
</section>
<section v-if="!hasSchema(value)">
<p class="no-schema-message">No schema information available for this method.</p>
</section>
</div>
</div>
</el-scrollbar>
</el-main>
<el-aside class="definitions-panel" width="28%">
<el-scrollbar ref="definitionsPanelScrollbar">
<div class="definitions-panel-content">
<div class="definitions-header">
<h2>Schema Definitions</h2>
<p class="definitions-subtitle">
Reference schemas used in messages
</p>
</div>
<!-- Debug info -->
<div v-if="false">
{{ console.log('Definitions:', definitions) }}
{{ console.log('Definitions keys:', definitions ? Object.keys(definitions) : 'null') }}
{{ console.log('Message docs:', messageDocsJsonSchema) }}
</div>
<div v-if="definitions && Object.keys(definitions).length > 0">
<div v-for="(defSchema, defName) in definitions" :key="defName" class="definition-item">
<a v-bind:href="`#def-${defName}`" :id="`def-${defName}`">
<h3>{{ defName }}</h3>
</a>
<JsonSchemaViewer :schema="defSchema" copyable @refClick="handleRefClick" />
</div>
</div>
<div v-else class="no-definitions">
<p>No schema definitions available</p>
<p style="font-size: 0.8rem; margin-top: 10px;">
Debug: definitions = {{ definitions ? 'exists' : 'null' }},
keys = {{ definitions ? Object.keys(definitions).length : 0 }}
</p>
</div>
</div>
</el-scrollbar>
</el-aside>
</el-container>
</template>
<style scoped>
.message-docs-container {
height: calc(100vh - 60px);
}
/* Left Sidebar - Search Navigation */
.search-nav {
border-right: 1px solid #e4e7ed;
}
.search-nav :deep(.el-scrollbar__wrap) {
overflow-x: hidden;
}
.search-nav :deep(.el-scrollbar__view) {
padding: 10px;
}
/* Main Content Area */
.message-docs-content {
color: #39455f;
font-family: 'Roboto';
padding: 0;
}
.message-docs-content :deep(.el-scrollbar__wrap) {
overflow-x: hidden;
}
.message-docs-content :deep(.el-scrollbar__view) {
padding: 25px 30px;
word-wrap: break-word;
overflow-wrap: break-word;
max-width: 100%;
box-sizing: border-box;
}
/* Right Sidebar - Definitions Panel */
.definitions-panel {
border-left: 2px solid #e4e7ed;
background-color: #f9fafb;
}
.definitions-panel :deep(.el-scrollbar__wrap) {
overflow-x: hidden;
}
.definitions-panel :deep(.el-scrollbar__view) {
padding: 0;
}
.definitions-panel-content {
padding: 20px;
}
.definitions-header {
position: sticky;
top: 0;
background-color: #f9fafb;
padding: 10px 0 20px 0;
margin-bottom: 10px;
border-bottom: 2px solid #e4e7ed;
z-index: 10;
}
.definitions-header h2 {
color: #303133;
margin: 0 0 8px 0;
font-size: 1.3rem;
font-family: 'Roboto';
font-weight: 600;
}
.definitions-subtitle {
color: #909399;
margin: 0;
font-size: 0.85rem;
font-family: 'Roboto';
}
h2 {
word-wrap: break-word;
overflow-wrap: break-word;
}
section {
word-wrap: break-word;
overflow-wrap: break-word;
max-width: 100%;
margin-bottom: 20px;
}
pre {
font-family: 'Roboto';
max-width: 100%;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
a {
text-decoration: none;
color: #39455f;
word-wrap: break-word;
overflow-wrap: break-word;
}
div {
font-size: 14px;
}
.content :deep(strong) {
font-family: 'Roboto';
}
.content :deep(a):hover {
background-color: #39455f;
}
.message-docs-header {
padding: 20px 0;
border-bottom: 2px solid #e4e7ed;
margin-bottom: 20px;
}
.message-docs-header h1 {
font-size: 1.75rem;
font-weight: 600;
color: #303133;
margin: 0 0 0.5rem 0;
word-wrap: break-word;
overflow-wrap: break-word;
}
.connector-subtitle {
font-size: 1rem;
color: #909399;
margin: 0;
}
.version-indicator {
font-size: 0.75rem;
color: #67c23a;
margin: 0.25rem 0 0 0;
font-weight: 600;
}
.topics {
display: flex;
flex-direction: column;
gap: 10px;
margin: 15px 0;
}
.topics > div {
display: flex;
align-items: center;
gap: 10px;
}
.no-schema-message {
color: #909399;
font-style: italic;
padding: 15px;
background-color: #f5f7fa;
border-radius: 4px;
}
.definition-item {
margin-bottom: 25px;
padding: 15px;
background-color: #ffffff;
border-radius: 6px;
border: 1px solid #e4e7ed;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
}
.definition-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-color: #409eff;
}
.definition-item h3 {
color: #409eff;
margin-top: 0;
margin-bottom: 12px;
font-size: 1rem;
word-wrap: break-word;
overflow-wrap: break-word;
font-weight: 600;
font-family: 'Roboto';
}
.definition-item a {
text-decoration: none;
}
.definition-item a:hover h3 {
text-decoration: underline;
}
.no-definitions {
text-align: center;
padding: 40px 20px;
color: #909399;
font-style: italic;
font-family: 'Roboto';
}
/* Highlight animation for scrolled-to definitions */
@keyframes highlight-pulse {
0% {
background-color: rgba(64, 158, 255, 0.15);
}
50% {
background-color: rgba(64, 158, 255, 0.25);
}
100% {
background-color: rgba(64, 158, 255, 0.15);
}
}
.definition-item.highlight-definition {
animation: highlight-pulse 0.6s ease-in-out 3;
border-color: #409eff;
box-shadow: 0 2px 12px rgba(64, 158, 255, 0.3);
}
</style>