lutaml-jsonschema 0.1.0

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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/README.md +39 -0
  5. data/Rakefile +26 -0
  6. data/exe/lutaml-jsonschema +6 -0
  7. data/frontend/index.html +60 -0
  8. data/frontend/package-lock.json +2715 -0
  9. data/frontend/package.json +27 -0
  10. data/frontend/public/lutaml-logo-dark.svg +1 -0
  11. data/frontend/public/lutaml-logo-full-dark.svg +1 -0
  12. data/frontend/public/lutaml-logo-full-light.svg +1 -0
  13. data/frontend/public/lutaml-logo-light.svg +1 -0
  14. data/frontend/src/App.vue +80 -0
  15. data/frontend/src/__tests__/useBuilderField.test.ts +137 -0
  16. data/frontend/src/__tests__/useDefinitionResolver.test.ts +46 -0
  17. data/frontend/src/__tests__/useSchemaTypes.test.ts +219 -0
  18. data/frontend/src/app.ts +10 -0
  19. data/frontend/src/components/AppHeader.vue +152 -0
  20. data/frontend/src/components/AppSidebar.vue +427 -0
  21. data/frontend/src/components/DetailPanel.vue +403 -0
  22. data/frontend/src/components/SchemaBuilder.vue +543 -0
  23. data/frontend/src/components/SchemaStructure.vue +168 -0
  24. data/frontend/src/components/SearchModal.vue +275 -0
  25. data/frontend/src/composables/useBuilderField.ts +92 -0
  26. data/frontend/src/composables/useDefinitionResolver.ts +17 -0
  27. data/frontend/src/composables/useSchemaTypes.ts +152 -0
  28. data/frontend/src/composables/useSearch.ts +104 -0
  29. data/frontend/src/router.ts +14 -0
  30. data/frontend/src/stores/schemaStore.ts +118 -0
  31. data/frontend/src/stores/uiStore.ts +78 -0
  32. data/frontend/src/style.css +194 -0
  33. data/frontend/src/types.ts +70 -0
  34. data/frontend/src/views/HomeView.vue +396 -0
  35. data/frontend/tsconfig.json +20 -0
  36. data/frontend/vite.config.ts +28 -0
  37. data/lib/lutaml/jsonschema/base.rb +11 -0
  38. data/lib/lutaml/jsonschema/cli.rb +102 -0
  39. data/lib/lutaml/jsonschema/combiner.rb +54 -0
  40. data/lib/lutaml/jsonschema/configuration.rb +47 -0
  41. data/lib/lutaml/jsonschema/link.rb +25 -0
  42. data/lib/lutaml/jsonschema/property_entry.rb +15 -0
  43. data/lib/lutaml/jsonschema/reference_resolver.rb +74 -0
  44. data/lib/lutaml/jsonschema/schema.rb +205 -0
  45. data/lib/lutaml/jsonschema/schema_set.rb +217 -0
  46. data/lib/lutaml/jsonschema/spa/generator.rb +22 -0
  47. data/lib/lutaml/jsonschema/spa/metadata.rb +23 -0
  48. data/lib/lutaml/jsonschema/spa/output_strategy.rb +17 -0
  49. data/lib/lutaml/jsonschema/spa/spa_builder.rb +178 -0
  50. data/lib/lutaml/jsonschema/spa/spa_definition.rb +27 -0
  51. data/lib/lutaml/jsonschema/spa/spa_document.rb +23 -0
  52. data/lib/lutaml/jsonschema/spa/spa_property.rb +47 -0
  53. data/lib/lutaml/jsonschema/spa/spa_schema.rb +29 -0
  54. data/lib/lutaml/jsonschema/spa/spa_search_entry.rb +21 -0
  55. data/lib/lutaml/jsonschema/spa/vue_inlined_strategy.rb +53 -0
  56. data/lib/lutaml/jsonschema/version.rb +7 -0
  57. data/lib/lutaml/jsonschema.rb +29 -0
  58. data/sig/lutaml/jsonschema.rbs +6 -0
  59. metadata +163 -0
@@ -0,0 +1,275 @@
1
+ <template>
2
+ <div class="search-modal-overlay" @click.self="closeSearch">
3
+ <div class="search-modal">
4
+ <div class="search-input-wrapper">
5
+ <svg width="18" height="18" viewBox="0 0 18 18" fill="none" class="search-icon">
6
+ <circle cx="8" cy="8" r="5.5" stroke="currentColor" stroke-width="1.5"/>
7
+ <path d="M12 12l4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
8
+ </svg>
9
+ <input
10
+ ref="inputRef"
11
+ v-model="search.query.value"
12
+ type="text"
13
+ class="search-input"
14
+ placeholder="Search schemas, properties, definitions..."
15
+ @keydown="handleKeydown"
16
+ />
17
+ <button v-if="search.query.value" class="clear-btn" @click="search.query.value = ''">
18
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
19
+ <path d="M4 4l6 6M10 4l-6 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
20
+ </svg>
21
+ </button>
22
+ </div>
23
+
24
+ <div class="search-results" v-if="search.query.value">
25
+ <div v-if="search.isSearching.value" class="search-loading">
26
+ Searching...
27
+ </div>
28
+ <div v-else-if="!search.hasResults.value" class="search-empty">
29
+ No results found for "{{ search.query.value }}"
30
+ </div>
31
+ <div v-else class="search-results-list">
32
+ <div
33
+ v-for="(result, index) in search.results.value"
34
+ :key="result.id"
35
+ class="search-result"
36
+ :class="{ focused: focusedIndex === index }"
37
+ @click="search.selectResult(result)"
38
+ @mouseenter="focusedIndex = index"
39
+ >
40
+ <span class="result-icon">
41
+ <svg v-if="result.type === 'schema'" width="16" height="16" viewBox="0 0 16 16" fill="none">
42
+ <rect x="2" y="2" width="12" height="12" rx="2" stroke="currentColor" stroke-width="1.3"/>
43
+ <path d="M5 6h6M5 8h4M5 10h5" stroke="currentColor" stroke-width="1" stroke-linecap="round"/>
44
+ </svg>
45
+ <svg v-else-if="result.type === 'definition'" width="16" height="16" viewBox="0 0 16 16" fill="none">
46
+ <rect x="2" y="2" width="12" height="12" rx="2" stroke="currentColor" stroke-width="1.3" stroke-dasharray="3 2"/>
47
+ </svg>
48
+ <svg v-else width="16" height="16" viewBox="0 0 16 16" fill="none">
49
+ <circle cx="8" cy="8" r="5" stroke="currentColor" stroke-width="1.3"/>
50
+ </svg>
51
+ </span>
52
+ <div class="result-content">
53
+ <span class="result-name" v-html="highlightMatch(result.name)"></span>
54
+ <span class="result-schema">{{ result.schemaName }}</span>
55
+ </div>
56
+ <span :class="['badge', `badge-${result.type}`]">{{ result.type }}</span>
57
+ </div>
58
+ </div>
59
+ </div>
60
+
61
+ <div class="search-footer">
62
+ <div class="search-hints">
63
+ <span><kbd>↑</kbd><kbd>↓</kbd> Navigate</span>
64
+ <span><kbd>↵</kbd> Select</span>
65
+ <span><kbd>Esc</kbd> Close</span>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </template>
71
+
72
+ <script setup lang="ts">
73
+ import { ref, onMounted, nextTick } from 'vue'
74
+ import { useSearch } from '../composables/useSearch'
75
+
76
+ const search = useSearch()
77
+ const inputRef = ref<HTMLInputElement | null>(null)
78
+ const focusedIndex = ref(0)
79
+
80
+ onMounted(() => {
81
+ nextTick(() => inputRef.value?.focus())
82
+ })
83
+
84
+ function handleKeydown(e: KeyboardEvent) {
85
+ if (e.key === 'ArrowDown') {
86
+ e.preventDefault()
87
+ focusedIndex.value = Math.min(focusedIndex.value + 1, search.results.value.length - 1)
88
+ } else if (e.key === 'ArrowUp') {
89
+ e.preventDefault()
90
+ focusedIndex.value = Math.max(focusedIndex.value - 1, 0)
91
+ } else if (e.key === 'Enter' && search.results.value[focusedIndex.value]) {
92
+ e.preventDefault()
93
+ search.selectResult(search.results.value[focusedIndex.value])
94
+ } else if (e.key === 'Escape') {
95
+ search.closeSearch()
96
+ }
97
+ }
98
+
99
+ function closeSearch() {
100
+ search.closeSearch()
101
+ }
102
+
103
+ function highlightMatch(text: string): string {
104
+ const query = search.query.value
105
+ if (!query) return text
106
+ const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi')
107
+ return text.replace(regex, '<mark>$1</mark>')
108
+ }
109
+ </script>
110
+
111
+ <style scoped>
112
+ .search-modal-overlay {
113
+ position: fixed;
114
+ inset: 0;
115
+ background: var(--bg-overlay);
116
+ display: flex;
117
+ align-items: flex-start;
118
+ justify-content: center;
119
+ padding-top: 10vh;
120
+ z-index: 1000;
121
+ animation: fadeIn var(--transition-fast);
122
+ }
123
+
124
+ .search-modal {
125
+ width: 100%;
126
+ max-width: 560px;
127
+ background: var(--bg-elevated);
128
+ border-radius: var(--radius-xl);
129
+ box-shadow: var(--shadow-lg);
130
+ overflow: hidden;
131
+ animation: slideIn var(--transition-slow);
132
+ }
133
+
134
+ .search-input-wrapper {
135
+ display: flex;
136
+ align-items: center;
137
+ gap: var(--space-3);
138
+ padding: var(--space-4);
139
+ border-bottom: 1px solid var(--border-light);
140
+ }
141
+
142
+ .search-icon {
143
+ color: var(--text-muted);
144
+ flex-shrink: 0;
145
+ }
146
+
147
+ .search-input {
148
+ flex: 1;
149
+ border: none;
150
+ background: none;
151
+ font-size: var(--text-base);
152
+ color: var(--text-primary);
153
+ outline: none;
154
+ }
155
+
156
+ .search-input::placeholder {
157
+ color: var(--text-muted);
158
+ }
159
+
160
+ .clear-btn {
161
+ padding: var(--space-1);
162
+ color: var(--text-muted);
163
+ border-radius: var(--radius-sm);
164
+ transition: all var(--transition-fast);
165
+ }
166
+
167
+ .clear-btn:hover {
168
+ background: var(--bg-hover);
169
+ color: var(--text-primary);
170
+ }
171
+
172
+ .search-results {
173
+ max-height: 400px;
174
+ overflow-y: auto;
175
+ }
176
+
177
+ .search-loading,
178
+ .search-empty {
179
+ padding: var(--space-8);
180
+ text-align: center;
181
+ color: var(--text-muted);
182
+ }
183
+
184
+ .search-results-list {
185
+ padding: var(--space-2);
186
+ }
187
+
188
+ .search-result {
189
+ display: flex;
190
+ align-items: center;
191
+ gap: var(--space-3);
192
+ padding: var(--space-2) var(--space-3);
193
+ border-radius: var(--radius-md);
194
+ cursor: pointer;
195
+ transition: background var(--transition-fast);
196
+ }
197
+
198
+ .search-result:hover,
199
+ .search-result.focused {
200
+ background: var(--bg-hover);
201
+ }
202
+
203
+ .result-icon {
204
+ color: var(--text-muted);
205
+ display: flex;
206
+ align-items: center;
207
+ }
208
+
209
+ .result-content {
210
+ flex: 1;
211
+ display: flex;
212
+ flex-direction: column;
213
+ gap: 2px;
214
+ min-width: 0;
215
+ }
216
+
217
+ .result-name {
218
+ font-size: var(--text-sm);
219
+ font-weight: 500;
220
+ color: var(--text-primary);
221
+ white-space: nowrap;
222
+ overflow: hidden;
223
+ text-overflow: ellipsis;
224
+ }
225
+
226
+ .result-name :deep(mark) {
227
+ background: var(--color-accent-alpha);
228
+ color: var(--color-accent);
229
+ padding: 0 2px;
230
+ border-radius: 2px;
231
+ }
232
+
233
+ .result-schema {
234
+ font-size: var(--text-xs);
235
+ color: var(--text-muted);
236
+ }
237
+
238
+ .badge-schema {
239
+ background: var(--badge-schema-bg);
240
+ color: var(--badge-schema);
241
+ }
242
+
243
+ .badge-property {
244
+ background: var(--badge-property-bg);
245
+ color: var(--badge-property);
246
+ }
247
+
248
+ .badge-definition {
249
+ background: var(--badge-definition-bg);
250
+ color: var(--badge-definition);
251
+ }
252
+
253
+ .search-footer {
254
+ padding: var(--space-3) var(--space-4);
255
+ border-top: 1px solid var(--border-light);
256
+ background: var(--bg-secondary);
257
+ }
258
+
259
+ .search-hints {
260
+ display: flex;
261
+ gap: var(--space-4);
262
+ font-size: var(--text-xs);
263
+ color: var(--text-muted);
264
+ }
265
+
266
+ .search-hints kbd {
267
+ padding: 2px 5px;
268
+ font-family: var(--font-mono);
269
+ font-size: 10px;
270
+ background: var(--bg-elevated);
271
+ border: 1px solid var(--border-medium);
272
+ border-radius: var(--radius-sm);
273
+ margin-right: 2px;
274
+ }
275
+ </style>
@@ -0,0 +1,92 @@
1
+ import { resolveSchemaRef } from './useDefinitionResolver'
2
+ import {
3
+ primaryType,
4
+ initialValue,
5
+ arrayDefaultValue,
6
+ parsePropertyValue,
7
+ parseArrayItem,
8
+ } from './useSchemaTypes'
9
+ import type { SpaProperty, SpaSchema, SpaDefinition } from '../types'
10
+
11
+ /**
12
+ * Typed field state for the SchemaBuilder.
13
+ * Each property in a schema becomes one BuilderField.
14
+ */
15
+ export interface BuilderField {
16
+ prop: SpaProperty
17
+ included: boolean
18
+ isRequired: boolean
19
+ rawValue: string
20
+ expanded: boolean
21
+ resolvedDef: SpaDefinition | null
22
+ nestedJson: Record<string, unknown>
23
+ arrayItems: string[]
24
+ }
25
+
26
+ /**
27
+ * Factory: create a BuilderField from a property definition.
28
+ * Resolves $ref, determines required status, initializes defaults.
29
+ */
30
+ export function createField(
31
+ prop: SpaProperty,
32
+ requiredNames: string[],
33
+ schema: SpaSchema,
34
+ ): BuilderField {
35
+ const isReq = requiredNames.includes(prop.name) || prop.required === true
36
+ const def = resolveSchemaRef(prop.ref, schema)
37
+ const isArray = primaryType(prop.type) === 'array'
38
+
39
+ return {
40
+ prop,
41
+ included: isReq,
42
+ isRequired: isReq,
43
+ rawValue: initialValue(prop),
44
+ expanded: false,
45
+ resolvedDef: def,
46
+ nestedJson: def ? buildDefaultJson(def.properties) : {},
47
+ arrayItems: isArray ? [arrayDefaultValue(prop.itemsType)] : [],
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Build a default JSON object from a list of properties.
53
+ * Uses initial values and type-based parsing.
54
+ */
55
+ export function buildDefaultJson(properties: SpaProperty[]): Record<string, unknown> {
56
+ const obj: Record<string, unknown> = {}
57
+ for (const prop of properties) {
58
+ const t = primaryType(prop.type)
59
+ const isArray = t === 'array'
60
+
61
+ if (isArray) {
62
+ const items = [arrayDefaultValue(prop.itemsType)]
63
+ obj[prop.name] = items.map(item => parseArrayItem(item, prop.itemsType))
64
+ } else {
65
+ obj[prop.name] = parsePropertyValue(initialValue(prop), prop)
66
+ }
67
+ }
68
+ return obj
69
+ }
70
+
71
+ /**
72
+ * Check whether expanding this field would create a circular reference.
73
+ */
74
+ export function isCircular(field: BuilderField, visited: ReadonlySet<string>): boolean {
75
+ return !!field.resolvedDef && visited.has(field.resolvedDef.name)
76
+ }
77
+
78
+ /**
79
+ * Parse a field's current state to its runtime JSON value.
80
+ * Handles nested objects (via nestedJson), arrays (via arrayItems),
81
+ * and primitive values (via parsePropertyValue).
82
+ */
83
+ export function parseFieldValue(field: BuilderField): unknown {
84
+ const prop = field.prop
85
+ const t = primaryType(prop.type)
86
+
87
+ if (t === 'array') {
88
+ return field.arrayItems.map(item => parseArrayItem(item, prop.itemsType))
89
+ }
90
+
91
+ return parsePropertyValue(field.rawValue, prop)
92
+ }
@@ -0,0 +1,17 @@
1
+ import type { SpaSchema, SpaDefinition } from '../types'
2
+
3
+ /**
4
+ * Resolve a JSON Schema $ref string to its SpaDefinition.
5
+ * Handles: #/definitions/NAME and #/$defs/NAME patterns.
6
+ * Returns null if the ref is empty, malformed, or not found.
7
+ */
8
+ export function resolveSchemaRef(ref: string | undefined, schema: SpaSchema): SpaDefinition | null {
9
+ if (!ref) return null
10
+
11
+ const match = ref.match(/^#\/(?:definitions|\$defs)\/([^/]+)$/)
12
+ if (match) {
13
+ return schema.definitions.find(d => d.name === match[1]) ?? null
14
+ }
15
+
16
+ return null
17
+ }
@@ -0,0 +1,152 @@
1
+ import type { SpaProperty } from '../types'
2
+
3
+ /**
4
+ * Extract the primary JSON Schema type from a type string.
5
+ * Handles union types like "string,integer" by returning the first component.
6
+ */
7
+ export function primaryType(type?: string): string {
8
+ const t = (type || '').split(',')[0].trim()
9
+ return t || 'any'
10
+ }
11
+
12
+ /**
13
+ * Human-readable type display for the type badge.
14
+ * Shows array<> notation for arrays and format suffix for strings.
15
+ */
16
+ export function displayType(prop: SpaProperty): string {
17
+ const t = primaryType(prop.type)
18
+ if (t === 'array' && prop.itemsType) return `array<${prop.itemsType}>`
19
+ if (prop.format) return `${t} (${prop.format})`
20
+ return t
21
+ }
22
+
23
+ /**
24
+ * HTML input type based on JSON Schema format keyword.
25
+ * Falls back to "text" for unrecognised formats.
26
+ */
27
+ export function formatInputType(format?: string): string {
28
+ switch (format) {
29
+ case 'email': return 'email'
30
+ case 'uri':
31
+ case 'uri-reference': return 'url'
32
+ case 'date': return 'date'
33
+ case 'time': return 'time'
34
+ case 'date-time': return 'datetime-local'
35
+ default: return 'text'
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Default string value based on format when no example/default is available.
41
+ */
42
+ export function formatDefault(format?: string): string {
43
+ switch (format) {
44
+ case 'uri':
45
+ case 'uri-reference': return 'https://example.com'
46
+ case 'date-time': return '2024-01-01T00:00:00Z'
47
+ case 'date': return '2024-01-01'
48
+ case 'time': return '00:00:00Z'
49
+ case 'email': return 'user@example.com'
50
+ case 'uuid': return '00000000-0000-0000-0000-000000000000'
51
+ default: return 'string'
52
+ }
53
+ }
54
+
55
+ /**
56
+ * The first meaningful value for a property: examples[0], default, first enum,
57
+ * or a type-based fallback.
58
+ */
59
+ export function initialValue(prop: SpaProperty): string {
60
+ if (prop.examples?.length) return prop.examples[0]
61
+ if (prop.default != null) return String(prop.default)
62
+ if (prop.enum?.length) return prop.enum[0]
63
+
64
+ const t = primaryType(prop.type)
65
+ switch (t) {
66
+ case 'integer': return '0'
67
+ case 'number': return '0.0'
68
+ case 'boolean': return 'false'
69
+ case 'string': return formatDefault(prop.format)
70
+ default: return ''
71
+ }
72
+ }
73
+
74
+ /**
75
+ * HTML input type for individual array item elements.
76
+ */
77
+ export function arrayItemInputType(itemsType?: string): string {
78
+ switch (itemsType) {
79
+ case 'integer':
80
+ case 'number': return 'number'
81
+ case 'boolean': return 'checkbox'
82
+ default: return 'text'
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Default string value for a new array item.
88
+ */
89
+ export function arrayDefaultValue(itemsType?: string): string {
90
+ switch (itemsType) {
91
+ case 'integer': return '0'
92
+ case 'number': return '0.0'
93
+ case 'boolean': return 'false'
94
+ default: return ''
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Parse a single array item string to its correct runtime type.
100
+ */
101
+ export function parseArrayItem(item: string, itemsType?: string): unknown {
102
+ switch (itemsType) {
103
+ case 'integer': return parseInt(item, 10) || 0
104
+ case 'number': return parseFloat(item) || 0.0
105
+ case 'boolean': return item === 'true'
106
+ default: return item
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Check whether a property is an object (type=object or has a $ref without a type).
112
+ */
113
+ export function isObjectProperty(prop: SpaProperty): boolean {
114
+ const t = primaryType(prop.type)
115
+ return t === 'object' || (!prop.type && !!prop.ref)
116
+ }
117
+
118
+ /**
119
+ * Check whether a property has constraints that should be displayed as chips.
120
+ */
121
+ export function hasConstraints(prop: SpaProperty): boolean {
122
+ return !!(
123
+ (prop.enum?.length && isObjectProperty(prop)) ||
124
+ prop.pattern ||
125
+ prop.minimum != null ||
126
+ prop.maximum != null ||
127
+ prop.minLength != null ||
128
+ prop.maxLength != null ||
129
+ prop.default != null ||
130
+ prop.examples?.length
131
+ )
132
+ }
133
+
134
+ /**
135
+ * Parse a property's raw string value to its correct runtime type.
136
+ * Handles enums, booleans, numbers, arrays, and objects.
137
+ */
138
+ export function parsePropertyValue(rawValue: string, prop: SpaProperty): unknown {
139
+ const t = primaryType(prop.type)
140
+ if (prop.enum?.length) return rawValue
141
+ if (t === 'boolean') return rawValue === 'true'
142
+ if (t === 'integer') {
143
+ const n = parseInt(rawValue, 10)
144
+ return isNaN(n) ? 0 : n
145
+ }
146
+ if (t === 'number') {
147
+ const n = parseFloat(rawValue)
148
+ return isNaN(n) ? 0 : n
149
+ }
150
+ if (t === 'object' && !prop.ref) return {}
151
+ return rawValue
152
+ }
@@ -0,0 +1,104 @@
1
+ import { ref, computed, watch } from 'vue'
2
+ import { useSchemaStore } from '../stores/schemaStore'
3
+ import { useUiStore } from '../stores/uiStore'
4
+
5
+ interface SearchResult {
6
+ id: string
7
+ type: 'schema' | 'property' | 'definition'
8
+ name: string
9
+ schemaName: string
10
+ doc?: string
11
+ }
12
+
13
+ export function useSearch() {
14
+ const schemaStore = useSchemaStore()
15
+ const uiStore = useUiStore()
16
+
17
+ const query = ref('')
18
+ const results = ref<SearchResult[]>([])
19
+ const isSearching = ref(false)
20
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null
21
+
22
+ function buildSearchEntries(): SearchResult[] {
23
+ const entries: SearchResult[] = []
24
+ for (const schema of schemaStore.schemas) {
25
+ entries.push({
26
+ id: `schema:${schema.name}`,
27
+ type: 'schema',
28
+ name: schema.title || schema.name,
29
+ schemaName: schema.name,
30
+ doc: schema.description,
31
+ })
32
+ for (const prop of schema.properties) {
33
+ entries.push({
34
+ id: `property:${schema.name}:${prop.name}`,
35
+ type: 'property',
36
+ name: prop.name,
37
+ schemaName: schema.name,
38
+ doc: prop.description,
39
+ })
40
+ }
41
+ for (const def of schema.definitions) {
42
+ entries.push({
43
+ id: `definition:${schema.name}:${def.name}`,
44
+ type: 'definition',
45
+ name: def.title || def.name,
46
+ schemaName: schema.name,
47
+ doc: def.description,
48
+ })
49
+ }
50
+ }
51
+ return entries
52
+ }
53
+
54
+ function search() {
55
+ if (!query.value.trim()) {
56
+ results.value = []
57
+ return
58
+ }
59
+
60
+ isSearching.value = true
61
+ const q = query.value.toLowerCase().trim()
62
+ const entries = buildSearchEntries()
63
+
64
+ results.value = entries.filter(e =>
65
+ e.name.toLowerCase().includes(q) ||
66
+ (e.doc && e.doc.toLowerCase().includes(q))
67
+ ).slice(0, 50)
68
+
69
+ isSearching.value = false
70
+ }
71
+
72
+ function debouncedSearch() {
73
+ if (debounceTimer) clearTimeout(debounceTimer)
74
+ debounceTimer = setTimeout(search, 150)
75
+ }
76
+
77
+ watch(query, debouncedSearch)
78
+
79
+ const hasResults = computed(() => results.value.length > 0)
80
+
81
+ function selectResult(result: SearchResult) {
82
+ schemaStore.selectSchema(result.schemaName)
83
+ if (result.type === 'definition') {
84
+ schemaStore.selectDefinition(result.name)
85
+ }
86
+ uiStore.openDetailPanel()
87
+ closeSearch()
88
+ }
89
+
90
+ function closeSearch() {
91
+ uiStore.closeSearch()
92
+ query.value = ''
93
+ results.value = []
94
+ }
95
+
96
+ return {
97
+ query,
98
+ results,
99
+ isSearching,
100
+ hasResults,
101
+ selectResult,
102
+ closeSearch,
103
+ }
104
+ }
@@ -0,0 +1,14 @@
1
+ import { createRouter, createWebHashHistory } from 'vue-router'
2
+
3
+ const router = createRouter({
4
+ history: createWebHashHistory(),
5
+ routes: [
6
+ {
7
+ path: '/',
8
+ name: 'home',
9
+ component: () => import('./views/HomeView.vue'),
10
+ },
11
+ ],
12
+ })
13
+
14
+ export default router