lutaml-jsonschema 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/frontend/src/App.vue +16 -0
- data/frontend/src/components/AppSidebar.vue +34 -8
- data/frontend/src/components/DetailPanel.vue +61 -3
- data/frontend/src/components/SchemaBuilder.vue +91 -29
- data/frontend/src/composables/useJsonViewer.ts +3 -2
- data/frontend/src/style.css +31 -0
- data/frontend/src/views/HomeView.vue +43 -3
- data/lib/lutaml/jsonschema/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c119b203d422df51dd4ef79c0e28ae59d1a3e8a01536c74baef3931f35481428
|
|
4
|
+
data.tar.gz: fd6e00bacd9f5d5ea9bab4afdff42adb786e1b0d7293f571c71e1e1dacb1d8f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 56edc028665efb3442c0ffe58eb7fe62576c02686cabf184fc2337749c6d460e9a7f817aef2e7737a8cc6b1a2dc1bd61ee0765ae5df558df1fde73e9ac0cc0a3
|
|
7
|
+
data.tar.gz: fed677f3dfb49ad12f056a494e73c2dceee12929f7f15c1753a6aac9d03387bba4ab1aa68c33bdb2ee152af46dd4a0a76898d3466575ad88cf0fdf944af766d1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.1.11] - 2026-05-09
|
|
4
|
+
|
|
5
|
+
### Redoc-style UX improvements (round 5)
|
|
6
|
+
|
|
7
|
+
- Dark JSON preview panel with themed syntax colors in SchemaBuilder
|
|
8
|
+
- Select-on-click for source viewer and builder JSON blocks (double-click to select all)
|
|
9
|
+
- Print stylesheet: hides sidebar, header, controls; content prints full-width
|
|
10
|
+
- Format badges display with angle brackets (`<email>`) and accent color
|
|
11
|
+
- Responsive builder layout: stacks vertically on mobile (<768px)
|
|
12
|
+
|
|
13
|
+
## [0.1.10] - 2026-05-09
|
|
14
|
+
|
|
15
|
+
### Redoc-style UX improvements
|
|
16
|
+
|
|
17
|
+
- JSON viewer collapsed objects show key names (e.g. `{a, b, c}`), arrays show item count
|
|
18
|
+
- Sidebar auto-scrolls to active schema node on selection
|
|
19
|
+
- Search results sorted by relevance: exact name > starts with > contains > description
|
|
20
|
+
- Copy button shows positioned tooltip overlay instead of text swap
|
|
21
|
+
- Mobile sidebar backdrop overlay (click-to-close)
|
|
22
|
+
- DetailPanel overview shows definition-level metadata: required fields, composition badges, property range, additionalProperties
|
|
23
|
+
- Nullable badge for union types (e.g. `string,null`) in SchemaBuilder
|
|
24
|
+
- Focus trap in DetailPanel: focus moves to panel on open, Tab/Shift+Tab trapped, Escape closes
|
|
25
|
+
|
|
3
26
|
## [0.1.0] - 2026-05-05
|
|
4
27
|
|
|
5
28
|
- Initial release
|
data/frontend/src/App.vue
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="app-layout">
|
|
3
3
|
<AppSidebar />
|
|
4
|
+
<div v-if="!uiStore.sidebarCollapsed" class="sidebar-overlay" @click="uiStore.toggleSidebar"></div>
|
|
4
5
|
<div class="main-content">
|
|
5
6
|
<AppHeader />
|
|
6
7
|
<div class="content-area">
|
|
@@ -100,4 +101,19 @@ function isInputFocused(): boolean {
|
|
|
100
101
|
padding: var(--space-6);
|
|
101
102
|
background: var(--bg-primary);
|
|
102
103
|
}
|
|
104
|
+
|
|
105
|
+
/* Mobile sidebar overlay — hidden on desktop, shown on mobile when sidebar open */
|
|
106
|
+
.sidebar-overlay {
|
|
107
|
+
display: none;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@media (max-width: 768px) {
|
|
111
|
+
.sidebar-overlay {
|
|
112
|
+
display: block;
|
|
113
|
+
position: fixed;
|
|
114
|
+
inset: 0;
|
|
115
|
+
background: rgba(0, 0, 0, 0.4);
|
|
116
|
+
z-index: 39;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
103
119
|
</style>
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
<div v-else-if="debouncedQuery && !searchResults.length && searchQuery" class="search-no-results text-muted">
|
|
70
70
|
No results found
|
|
71
71
|
</div>
|
|
72
|
-
<div v-else class="schema-tree">
|
|
72
|
+
<div v-else class="schema-tree" ref="sidebarTreeRef">
|
|
73
73
|
<div
|
|
74
74
|
v-for="schema in schemaStore.schemas"
|
|
75
75
|
:key="schema.name"
|
|
@@ -150,13 +150,14 @@
|
|
|
150
150
|
</template>
|
|
151
151
|
|
|
152
152
|
<script setup lang="ts">
|
|
153
|
-
import { ref, computed, watch } from 'vue'
|
|
153
|
+
import { ref, computed, watch, nextTick } from 'vue'
|
|
154
154
|
import { useSchemaStore } from '../stores/schemaStore'
|
|
155
155
|
import { useUiStore } from '../stores/uiStore'
|
|
156
156
|
import type { SpaSearchEntry } from '../types'
|
|
157
157
|
|
|
158
158
|
const schemaStore = useSchemaStore()
|
|
159
159
|
const uiStore = useUiStore()
|
|
160
|
+
const sidebarTreeRef = ref<HTMLElement | null>(null)
|
|
160
161
|
|
|
161
162
|
const searchQuery = ref('')
|
|
162
163
|
const debouncedQuery = ref('')
|
|
@@ -177,16 +178,41 @@ watch(searchQuery, (q) => {
|
|
|
177
178
|
const searchResults = computed(() => {
|
|
178
179
|
const q = debouncedQuery.value.trim().toLowerCase()
|
|
179
180
|
if (!q) return []
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
181
|
+
const scored = schemaStore.searchIndex
|
|
182
|
+
.map(entry => {
|
|
183
|
+
let score = 0
|
|
184
|
+
const nameLower = entry.name.toLowerCase()
|
|
185
|
+
const titleLower = (entry.title || '').toLowerCase()
|
|
186
|
+
const descLower = (entry.description || '').toLowerCase()
|
|
187
|
+
const schemaLower = entry.schemaName.toLowerCase()
|
|
188
|
+
if (nameLower === q) score += 100
|
|
189
|
+
else if (nameLower.startsWith(q)) score += 50
|
|
190
|
+
else if (nameLower.includes(q)) score += 30
|
|
191
|
+
if (titleLower === q) score += 80
|
|
192
|
+
else if (titleLower.startsWith(q)) score += 40
|
|
193
|
+
else if (titleLower.includes(q)) score += 20
|
|
194
|
+
if (descLower.includes(q)) score += 10
|
|
195
|
+
if (schemaLower.includes(q)) score += 5
|
|
196
|
+
return { entry, score }
|
|
197
|
+
})
|
|
198
|
+
.filter(r => r.score > 0)
|
|
199
|
+
.sort((a, b) => b.score - a.score)
|
|
200
|
+
.slice(0, 15)
|
|
201
|
+
.map(r => r.entry)
|
|
202
|
+
return scored
|
|
186
203
|
})
|
|
187
204
|
|
|
188
205
|
watch(searchResults, () => { activeResultIdx.value = -1 })
|
|
189
206
|
|
|
207
|
+
watch(() => schemaStore.selectedSchemaName, () => {
|
|
208
|
+
nextTick(() => {
|
|
209
|
+
const container = sidebarTreeRef.value
|
|
210
|
+
if (!container) return
|
|
211
|
+
const active = container.querySelector('.schema-node-header.active') as HTMLElement | null
|
|
212
|
+
if (active) active.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
190
216
|
function handleSearchKey(event: KeyboardEvent) {
|
|
191
217
|
if (event.key === 'ArrowDown') {
|
|
192
218
|
activeResultIdx.value = Math.min(activeResultIdx.value + 1, searchResults.value.length - 1)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="detail-panel-overlay" @click.self="uiStore.closeDetailPanel">
|
|
3
|
-
<aside class="detail-panel">
|
|
3
|
+
<aside class="detail-panel" ref="panelRef">
|
|
4
4
|
<div class="panel-header">
|
|
5
5
|
<div class="panel-title">
|
|
6
6
|
<h2 v-if="item">{{ itemTitle }}</h2>
|
|
7
7
|
</div>
|
|
8
|
-
<button class="btn btn-ghost" @click="uiStore.closeDetailPanel">
|
|
8
|
+
<button ref="closeBtnRef" class="btn btn-ghost" @click="uiStore.closeDetailPanel" aria-label="Close panel">
|
|
9
9
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
|
|
10
10
|
<path d="M5 5l8 8M13 5l-8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
11
11
|
</svg>
|
|
@@ -98,6 +98,32 @@
|
|
|
98
98
|
<span class="meta-label">Additional</span>
|
|
99
99
|
<span class="badge badge-locked-detail">Denied</span>
|
|
100
100
|
</div>
|
|
101
|
+
<div v-if="definitionItem && definitionItem.required?.length" class="meta-row">
|
|
102
|
+
<span class="meta-label">Required</span>
|
|
103
|
+
<div class="meta-tags">
|
|
104
|
+
<span v-for="r in definitionItem.required" :key="r" class="badge badge-required-sm">{{ r }}</span>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
<div v-if="definitionItem && (definitionItem.hasAllOf || definitionItem.hasAnyOf || definitionItem.hasOneOf)" class="meta-row">
|
|
108
|
+
<span class="meta-label">Composition</span>
|
|
109
|
+
<div class="meta-tags">
|
|
110
|
+
<span v-if="definitionItem.hasAllOf" class="badge badge-composition-detail">allOf</span>
|
|
111
|
+
<span v-if="definitionItem.hasAnyOf" class="badge badge-composition-detail">anyOf</span>
|
|
112
|
+
<span v-if="definitionItem.hasOneOf" class="badge badge-composition-detail">oneOf</span>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
<div v-if="definitionItem && (definitionItem.minProperties != null || definitionItem.maxProperties != null)" class="meta-row">
|
|
116
|
+
<span class="meta-label">Properties</span>
|
|
117
|
+
<span class="text-secondary">
|
|
118
|
+
<template v-if="definitionItem.minProperties != null && definitionItem.maxProperties != null">{{ definitionItem.minProperties }}..{{ definitionItem.maxProperties }}</template>
|
|
119
|
+
<template v-else-if="definitionItem.minProperties != null">≥ {{ definitionItem.minProperties }}</template>
|
|
120
|
+
<template v-else>≤ {{ definitionItem.maxProperties }}</template>
|
|
121
|
+
</span>
|
|
122
|
+
</div>
|
|
123
|
+
<div v-if="definitionItem && definitionItem.additionalProperties === false" class="meta-row">
|
|
124
|
+
<span class="meta-label">Additional</span>
|
|
125
|
+
<span class="badge badge-locked-detail">Denied</span>
|
|
126
|
+
</div>
|
|
101
127
|
</div>
|
|
102
128
|
</div>
|
|
103
129
|
|
|
@@ -241,7 +267,7 @@
|
|
|
241
267
|
</template>
|
|
242
268
|
|
|
243
269
|
<script setup lang="ts">
|
|
244
|
-
import { computed } from 'vue'
|
|
270
|
+
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
|
245
271
|
import { useSchemaStore, type SelectedItem } from '../stores/schemaStore'
|
|
246
272
|
import { useUiStore } from '../stores/uiStore'
|
|
247
273
|
import type { SpaProperty } from '../types'
|
|
@@ -249,6 +275,33 @@ import type { SpaProperty } from '../types'
|
|
|
249
275
|
const schemaStore = useSchemaStore()
|
|
250
276
|
const uiStore = useUiStore()
|
|
251
277
|
|
|
278
|
+
const panelRef = ref<HTMLElement | null>(null)
|
|
279
|
+
const closeBtnRef = ref<HTMLButtonElement | null>(null)
|
|
280
|
+
|
|
281
|
+
onMounted(() => {
|
|
282
|
+
closeBtnRef.value?.focus()
|
|
283
|
+
document.addEventListener('keydown', trapFocus)
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
onUnmounted(() => {
|
|
287
|
+
document.removeEventListener('keydown', trapFocus)
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
function trapFocus(e: KeyboardEvent) {
|
|
291
|
+
if (e.key !== 'Tab' || !panelRef.value) return
|
|
292
|
+
const focusable = panelRef.value.querySelectorAll<HTMLElement>(
|
|
293
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
|
|
294
|
+
)
|
|
295
|
+
if (!focusable.length) return
|
|
296
|
+
const first = focusable[0]
|
|
297
|
+
const last = focusable[focusable.length - 1]
|
|
298
|
+
if (e.shiftKey) {
|
|
299
|
+
if (document.activeElement === first) { e.preventDefault(); last.focus() }
|
|
300
|
+
} else {
|
|
301
|
+
if (document.activeElement === last) { e.preventDefault(); first.focus() }
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
252
305
|
const item = computed<SelectedItem | null>(() => schemaStore.selectedItem)
|
|
253
306
|
const schema = computed(() => schemaStore.selectedSchema)
|
|
254
307
|
|
|
@@ -379,6 +432,11 @@ const propertyItem = computed<SpaProperty | null>(() => {
|
|
|
379
432
|
if (item.value?.kind !== 'property') return null
|
|
380
433
|
return item.value.property
|
|
381
434
|
})
|
|
435
|
+
|
|
436
|
+
const definitionItem = computed(() => {
|
|
437
|
+
if (item.value?.kind !== 'definition') return null
|
|
438
|
+
return item.value.definition
|
|
439
|
+
})
|
|
382
440
|
</script>
|
|
383
441
|
|
|
384
442
|
<style scoped>
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
<span class="font-mono">{{ field.prop.name }}</span>
|
|
17
17
|
</button>
|
|
18
18
|
<span class="field-type-badge" :class="typeBadgeClass(field.prop)">{{ displayType(field.prop, field.resolvedDef?.title || field.resolvedDef?.name) }}</span>
|
|
19
|
-
<span v-if="field.prop.format" class="field-format-badge"
|
|
19
|
+
<span v-if="field.prop.format" class="field-format-badge"><{{ field.prop.format }}></span>
|
|
20
20
|
<span v-if="field.prop.contentMediaType" class="field-format-badge">content-type: {{ field.prop.contentMediaType }}</span>
|
|
21
21
|
<span v-if="field.prop.contentEncoding" class="field-format-badge">encoding: {{ field.prop.contentEncoding }}</span>
|
|
22
22
|
<span v-if="field.prop.const != null" class="const-badge font-mono">const: {{ field.prop.const }}</span>
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
<span v-if="field.prop.deprecated" class="deprecated-badge">deprecated</span>
|
|
26
26
|
<span v-if="field.prop.readOnly" class="readonly-badge">read-only</span>
|
|
27
27
|
<span v-if="field.prop.writeOnly" class="writeonly-badge">write-only</span>
|
|
28
|
+
<span v-if="isNullableType(field.prop.type)" class="nullable-badge">nullable</span>
|
|
28
29
|
<span v-if="field.prop.compositionSource" class="composition-badge">{{ field.prop.compositionSource }}</span>
|
|
29
30
|
|
|
30
31
|
<div class="field-control">
|
|
@@ -205,16 +206,17 @@
|
|
|
205
206
|
<div class="builder-preview">
|
|
206
207
|
<div class="preview-inner">
|
|
207
208
|
<div class="preview-toolbar">
|
|
208
|
-
<span class="toolbar-label
|
|
209
|
+
<span class="toolbar-label">JSON Preview</span>
|
|
209
210
|
<div class="toolbar-actions">
|
|
210
|
-
<button class="btn btn-
|
|
211
|
-
<button class="btn btn-
|
|
212
|
-
<button class="btn btn-
|
|
213
|
-
|
|
211
|
+
<button class="btn btn-sm btn-dark-panel" @click="expandAllJson">Expand all</button>
|
|
212
|
+
<button class="btn btn-sm btn-dark-panel" @click="collapseAllJson">Collapse all</button>
|
|
213
|
+
<button class="btn btn-sm btn-dark-panel copy-btn-wrap" @click="copyJson">
|
|
214
|
+
Copy
|
|
215
|
+
<span v-if="copied" class="copy-tooltip">Copied!</span>
|
|
214
216
|
</button>
|
|
215
217
|
</div>
|
|
216
218
|
</div>
|
|
217
|
-
<pre ref="jsonBlockRef" class="json-block" @click="handleJsonClick" v-html="highlightedJson"></pre>
|
|
219
|
+
<pre ref="jsonBlockRef" class="json-block" @click="handleJsonClick" @dblclick="selectJsonBlock" v-html="highlightedJson"></pre>
|
|
218
220
|
</div>
|
|
219
221
|
</div>
|
|
220
222
|
</div>
|
|
@@ -389,6 +391,18 @@ function handleJsonClick(event: MouseEvent) {
|
|
|
389
391
|
}
|
|
390
392
|
}
|
|
391
393
|
|
|
394
|
+
function selectJsonBlock() {
|
|
395
|
+
const el = jsonBlockRef.value
|
|
396
|
+
if (!el) return
|
|
397
|
+
const range = document.createRange()
|
|
398
|
+
range.selectNodeContents(el)
|
|
399
|
+
const selection = window.getSelection()
|
|
400
|
+
if (selection) {
|
|
401
|
+
selection.removeAllRanges()
|
|
402
|
+
selection.addRange(range)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
392
406
|
const MAX_PATTERN_LEN = 45
|
|
393
407
|
|
|
394
408
|
function typeBadgeClass(prop: SpaProperty): string {
|
|
@@ -405,6 +419,10 @@ function typeBadgeClass(prop: SpaProperty): string {
|
|
|
405
419
|
}
|
|
406
420
|
}
|
|
407
421
|
|
|
422
|
+
function isNullableType(type?: string): boolean {
|
|
423
|
+
return (type || '').split(',').map(s => s.trim()).includes('null')
|
|
424
|
+
}
|
|
425
|
+
|
|
408
426
|
function truncatedPattern(pattern: string): { text: string; truncated: boolean } {
|
|
409
427
|
if (pattern.length <= MAX_PATTERN_LEN) return { text: pattern, truncated: false }
|
|
410
428
|
if (expandedPatterns.value.has(pattern)) return { text: pattern, truncated: true }
|
|
@@ -545,11 +563,12 @@ async function copyJson() {
|
|
|
545
563
|
|
|
546
564
|
.field-format-badge {
|
|
547
565
|
font-size: 10px;
|
|
548
|
-
color: var(--
|
|
549
|
-
background: var(--
|
|
566
|
+
color: var(--color-accent);
|
|
567
|
+
background: var(--color-accent-alpha);
|
|
550
568
|
padding: 1px 5px;
|
|
551
569
|
border-radius: var(--radius-sm);
|
|
552
570
|
font-family: var(--font-mono);
|
|
571
|
+
font-weight: 500;
|
|
553
572
|
}
|
|
554
573
|
|
|
555
574
|
.deprecated-badge {
|
|
@@ -583,6 +602,18 @@ async function copyJson() {
|
|
|
583
602
|
background: var(--color-orange-alpha);
|
|
584
603
|
}
|
|
585
604
|
|
|
605
|
+
.nullable-badge {
|
|
606
|
+
font-size: 10px;
|
|
607
|
+
font-weight: 600;
|
|
608
|
+
text-transform: uppercase;
|
|
609
|
+
color: var(--text-muted);
|
|
610
|
+
background: var(--bg-secondary);
|
|
611
|
+
border: 1px dashed var(--border-medium);
|
|
612
|
+
padding: 1px 5px;
|
|
613
|
+
border-radius: 2px;
|
|
614
|
+
flex-shrink: 0;
|
|
615
|
+
}
|
|
616
|
+
|
|
586
617
|
.composition-badge {
|
|
587
618
|
font-size: 10px;
|
|
588
619
|
font-weight: 600;
|
|
@@ -982,6 +1013,7 @@ async function copyJson() {
|
|
|
982
1013
|
display: flex;
|
|
983
1014
|
align-items: center;
|
|
984
1015
|
justify-content: space-between;
|
|
1016
|
+
color: var(--panel-dark-muted);
|
|
985
1017
|
}
|
|
986
1018
|
|
|
987
1019
|
.toolbar-label {
|
|
@@ -993,9 +1025,18 @@ async function copyJson() {
|
|
|
993
1025
|
padding: var(--space-1) var(--space-2);
|
|
994
1026
|
}
|
|
995
1027
|
|
|
1028
|
+
.btn-dark-panel {
|
|
1029
|
+
color: var(--panel-dark-muted);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
.btn-dark-panel:hover {
|
|
1033
|
+
color: var(--panel-dark-text);
|
|
1034
|
+
background: rgba(255, 255, 255, 0.08);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
996
1037
|
.json-block {
|
|
997
|
-
background: var(--bg
|
|
998
|
-
border: 1px solid var(--
|
|
1038
|
+
background: var(--panel-dark-bg);
|
|
1039
|
+
border: 1px solid var(--panel-dark-bg);
|
|
999
1040
|
border-radius: var(--radius-md);
|
|
1000
1041
|
padding: var(--space-4);
|
|
1001
1042
|
overflow-x: auto;
|
|
@@ -1005,7 +1046,7 @@ async function copyJson() {
|
|
|
1005
1046
|
max-height: 70vh;
|
|
1006
1047
|
overflow-y: auto;
|
|
1007
1048
|
font-family: var(--font-mono);
|
|
1008
|
-
color: var(--text
|
|
1049
|
+
color: var(--panel-dark-text);
|
|
1009
1050
|
}
|
|
1010
1051
|
|
|
1011
1052
|
.json-block :deep(ul) {
|
|
@@ -1020,7 +1061,7 @@ async function copyJson() {
|
|
|
1020
1061
|
|
|
1021
1062
|
.json-block :deep(.jv-toggle) {
|
|
1022
1063
|
background: none;
|
|
1023
|
-
border: 1px solid var(--
|
|
1064
|
+
border: 1px solid var(--panel-dark-muted);
|
|
1024
1065
|
border-radius: 2px;
|
|
1025
1066
|
cursor: pointer;
|
|
1026
1067
|
width: 14px;
|
|
@@ -1046,15 +1087,12 @@ async function copyJson() {
|
|
|
1046
1087
|
|
|
1047
1088
|
.json-block :deep(.jv-ellipsis) {
|
|
1048
1089
|
display: none;
|
|
1049
|
-
color: var(--
|
|
1090
|
+
color: var(--panel-dark-muted);
|
|
1050
1091
|
font-size: var(--text-xs);
|
|
1092
|
+
font-style: italic;
|
|
1051
1093
|
margin-left: 4px;
|
|
1052
1094
|
}
|
|
1053
1095
|
|
|
1054
|
-
.json-block :deep(.jv-ellipsis::after) {
|
|
1055
|
-
content: ' … ';
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
1096
|
.json-block :deep(.jv-children.jv-collapsed) {
|
|
1059
1097
|
display: none;
|
|
1060
1098
|
}
|
|
@@ -1064,48 +1102,72 @@ async function copyJson() {
|
|
|
1064
1102
|
}
|
|
1065
1103
|
|
|
1066
1104
|
.json-block :deep(.jv-key) {
|
|
1067
|
-
color: var(--
|
|
1105
|
+
color: var(--panel-dark-key);
|
|
1068
1106
|
}
|
|
1069
1107
|
|
|
1070
1108
|
.json-block :deep(.jv-punct) {
|
|
1071
|
-
color: var(--
|
|
1109
|
+
color: var(--panel-dark-muted);
|
|
1072
1110
|
}
|
|
1073
1111
|
|
|
1074
1112
|
.json-block :deep(.jv-string) {
|
|
1075
|
-
color: var(--
|
|
1113
|
+
color: var(--panel-dark-string);
|
|
1076
1114
|
}
|
|
1077
1115
|
|
|
1078
1116
|
.json-block :deep(.jv-number) {
|
|
1079
|
-
color: var(--
|
|
1117
|
+
color: var(--panel-dark-number);
|
|
1080
1118
|
}
|
|
1081
1119
|
|
|
1082
1120
|
.json-block :deep(.jv-boolean) {
|
|
1083
|
-
color: var(--
|
|
1121
|
+
color: var(--panel-dark-boolean);
|
|
1084
1122
|
}
|
|
1085
1123
|
|
|
1086
1124
|
.json-block :deep(.jv-null) {
|
|
1087
|
-
color: var(--
|
|
1125
|
+
color: var(--panel-dark-null);
|
|
1088
1126
|
font-style: italic;
|
|
1089
1127
|
}
|
|
1090
1128
|
|
|
1091
1129
|
.json-block :deep(.jv-link) {
|
|
1092
|
-
color: var(--
|
|
1130
|
+
color: var(--panel-dark-string);
|
|
1093
1131
|
text-decoration: underline;
|
|
1094
1132
|
}
|
|
1095
1133
|
|
|
1096
1134
|
.json-block :deep(.jv-row:hover) {
|
|
1097
|
-
background:
|
|
1135
|
+
background: rgba(255, 255, 255, 0.06);
|
|
1098
1136
|
border-radius: 2px;
|
|
1099
1137
|
}
|
|
1100
1138
|
|
|
1101
|
-
:root[data-theme="dark"] .json-block :deep(.jv-key) { color: var(--color-primary-light); }
|
|
1102
|
-
:root[data-theme="dark"] .json-block :deep(.jv-string) { color: var(--color-teal); }
|
|
1103
|
-
|
|
1104
1139
|
.toolbar-actions {
|
|
1105
1140
|
display: flex;
|
|
1106
1141
|
gap: var(--space-1);
|
|
1107
1142
|
}
|
|
1108
1143
|
|
|
1144
|
+
/* Copy button tooltip */
|
|
1145
|
+
.copy-btn-wrap {
|
|
1146
|
+
position: relative;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
.copy-tooltip {
|
|
1150
|
+
position: absolute;
|
|
1151
|
+
bottom: calc(100% + 4px);
|
|
1152
|
+
left: 50%;
|
|
1153
|
+
transform: translateX(-50%);
|
|
1154
|
+
background: var(--bg-elevated);
|
|
1155
|
+
color: var(--text-primary);
|
|
1156
|
+
font-size: var(--text-xs);
|
|
1157
|
+
padding: 3px 8px;
|
|
1158
|
+
border-radius: var(--radius-sm);
|
|
1159
|
+
border: 1px solid var(--border-light);
|
|
1160
|
+
box-shadow: var(--shadow-md);
|
|
1161
|
+
white-space: nowrap;
|
|
1162
|
+
pointer-events: none;
|
|
1163
|
+
animation: tooltipFade var(--transition-slow);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
@keyframes tooltipFade {
|
|
1167
|
+
from { opacity: 0; transform: translateX(-50%) translateY(2px); }
|
|
1168
|
+
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1109
1171
|
.empty-hint {
|
|
1110
1172
|
padding: var(--space-8);
|
|
1111
1173
|
text-align: center;
|
|
@@ -56,7 +56,7 @@ function renderArray(arr: unknown[], maxExpand: number, level: number): string {
|
|
|
56
56
|
html += '</div></li>'
|
|
57
57
|
})
|
|
58
58
|
html += '</ul>'
|
|
59
|
-
html += `<span class="jv-ellipsis"
|
|
59
|
+
html += `<span class="jv-ellipsis">${arr.length} items</span>`
|
|
60
60
|
html += punct(']')
|
|
61
61
|
return html
|
|
62
62
|
}
|
|
@@ -76,7 +76,8 @@ function renderObject(obj: Record<string, unknown>, maxExpand: number, level: nu
|
|
|
76
76
|
html += '</div></li>'
|
|
77
77
|
})
|
|
78
78
|
html += '</ul>'
|
|
79
|
-
|
|
79
|
+
const previewKeys = keys.length <= 5 ? keys : [...keys.slice(0, 5), `+${keys.length - 5}`]
|
|
80
|
+
html += `<span class="jv-ellipsis">${previewKeys.map(k => escHtml(k)).join(', ')}</span>`
|
|
80
81
|
html += punct('}')
|
|
81
82
|
return html
|
|
82
83
|
}
|
data/frontend/src/style.css
CHANGED
|
@@ -96,6 +96,15 @@
|
|
|
96
96
|
--type-array-bg: rgba(139, 92, 246, 0.1);
|
|
97
97
|
--type-null: #A8A29E;
|
|
98
98
|
--type-null-bg: rgba(168, 162, 158, 0.1);
|
|
99
|
+
|
|
100
|
+
--panel-dark-bg: #263238;
|
|
101
|
+
--panel-dark-text: #e8edf4;
|
|
102
|
+
--panel-dark-muted: #8b9db5;
|
|
103
|
+
--panel-dark-key: #80cbc4;
|
|
104
|
+
--panel-dark-string: #a5d6a7;
|
|
105
|
+
--panel-dark-number: #ffcc80;
|
|
106
|
+
--panel-dark-boolean: #90caf9;
|
|
107
|
+
--panel-dark-null: #8b9db5;
|
|
99
108
|
}
|
|
100
109
|
|
|
101
110
|
:root[data-theme="dark"] {
|
|
@@ -143,6 +152,15 @@
|
|
|
143
152
|
--type-array-bg: rgba(167, 139, 250, 0.15);
|
|
144
153
|
--type-null: #6b7a8f;
|
|
145
154
|
--type-null-bg: rgba(107, 122, 143, 0.15);
|
|
155
|
+
|
|
156
|
+
--panel-dark-bg: #1e2a35;
|
|
157
|
+
--panel-dark-text: #dce4f0;
|
|
158
|
+
--panel-dark-muted: #7b8fa8;
|
|
159
|
+
--panel-dark-key: #4db6ac;
|
|
160
|
+
--panel-dark-string: #81c784;
|
|
161
|
+
--panel-dark-number: #ffb74d;
|
|
162
|
+
--panel-dark-boolean: #64b5f6;
|
|
163
|
+
--panel-dark-null: #7b8fa8;
|
|
146
164
|
}
|
|
147
165
|
|
|
148
166
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
@@ -233,6 +251,19 @@ button { font-family: inherit; font-size: inherit; cursor: pointer; border: none
|
|
|
233
251
|
}
|
|
234
252
|
}
|
|
235
253
|
|
|
254
|
+
@media print {
|
|
255
|
+
.sidebar, .sidebar-overlay, .app-header,
|
|
256
|
+
.view-toggle, .schema-actions,
|
|
257
|
+
.ctrl-expand, .field-check,
|
|
258
|
+
.source-toolbar, .copy-btn-wrap,
|
|
259
|
+
.section-actions { display: none !important; }
|
|
260
|
+
.app-layout { grid-template-columns: 1fr !important; }
|
|
261
|
+
.app-main { padding: 0 !important; max-width: 100% !important; }
|
|
262
|
+
.card, .def-card { break-inside: avoid; box-shadow: none; border: 1px solid #ccc; }
|
|
263
|
+
.field-row { break-inside: avoid; }
|
|
264
|
+
body { color: #000; background: #fff; }
|
|
265
|
+
}
|
|
266
|
+
|
|
236
267
|
.text-muted { color: var(--text-muted); }
|
|
237
268
|
.text-secondary { color: var(--text-secondary); }
|
|
238
269
|
.font-mono { font-family: var(--font-mono); }
|
|
@@ -136,13 +136,14 @@
|
|
|
136
136
|
<div v-if="schemaStore.selectedSchema.sourceJson" class="source-viewer">
|
|
137
137
|
<div class="source-toolbar">
|
|
138
138
|
<span class="text-muted">{{ sourceLineCount }} lines</span>
|
|
139
|
-
<button class="btn btn-ghost btn-sm" @click="copySource">
|
|
140
|
-
|
|
139
|
+
<button class="btn btn-ghost btn-sm copy-btn-wrap" @click="copySource">
|
|
140
|
+
Copy
|
|
141
|
+
<span v-if="sourceCopied" class="copy-tooltip">Copied!</span>
|
|
141
142
|
</button>
|
|
142
143
|
</div>
|
|
143
144
|
<div class="source-code-wrapper">
|
|
144
145
|
<div class="source-lines" aria-hidden="true"><span v-for="n in sourceLineCount" :key="n">{{ n }}</span></div>
|
|
145
|
-
<pre class="source-pre"><code v-html="highlightedSource"></code></pre>
|
|
146
|
+
<pre class="source-pre" @dblclick="selectSourceBlock"><code v-html="highlightedSource"></code></pre>
|
|
146
147
|
</div>
|
|
147
148
|
</div>
|
|
148
149
|
<div v-else class="source-empty">
|
|
@@ -284,6 +285,18 @@ function syntaxHighlight(json: string): string {
|
|
|
284
285
|
})
|
|
285
286
|
}
|
|
286
287
|
|
|
288
|
+
function selectSourceBlock() {
|
|
289
|
+
const el = document.querySelector('.source-pre') as HTMLElement | null
|
|
290
|
+
if (!el) return
|
|
291
|
+
const range = document.createRange()
|
|
292
|
+
range.selectNodeContents(el)
|
|
293
|
+
const selection = window.getSelection()
|
|
294
|
+
if (selection) {
|
|
295
|
+
selection.removeAllRanges()
|
|
296
|
+
selection.addRange(range)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
287
300
|
function selectSchema(name: string) {
|
|
288
301
|
schemaStore.selectSchema(name)
|
|
289
302
|
}
|
|
@@ -592,6 +605,33 @@ watch(() => schemaStore.selectedItemKey, (key) => {
|
|
|
592
605
|
font-size: var(--text-xs);
|
|
593
606
|
}
|
|
594
607
|
|
|
608
|
+
/* Copy button tooltip */
|
|
609
|
+
.copy-btn-wrap {
|
|
610
|
+
position: relative;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
.copy-tooltip {
|
|
614
|
+
position: absolute;
|
|
615
|
+
bottom: calc(100% + 4px);
|
|
616
|
+
left: 50%;
|
|
617
|
+
transform: translateX(-50%);
|
|
618
|
+
background: var(--bg-elevated);
|
|
619
|
+
color: var(--text-primary);
|
|
620
|
+
font-size: var(--text-xs);
|
|
621
|
+
padding: 3px 8px;
|
|
622
|
+
border-radius: var(--radius-sm);
|
|
623
|
+
border: 1px solid var(--border-light);
|
|
624
|
+
box-shadow: var(--shadow-md);
|
|
625
|
+
white-space: nowrap;
|
|
626
|
+
pointer-events: none;
|
|
627
|
+
animation: tooltipFade var(--transition-slow);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
@keyframes tooltipFade {
|
|
631
|
+
from { opacity: 0; transform: translateX(-50%) translateY(2px); }
|
|
632
|
+
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
633
|
+
}
|
|
634
|
+
|
|
595
635
|
.source-code-wrapper {
|
|
596
636
|
display: flex;
|
|
597
637
|
max-height: 70vh;
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lutaml-jsonschema
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: json
|