ea 0.6.3 → 0.6.5

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +72 -0
  3. data/config/themes/119.yml +4 -4
  4. data/frontend/dist/app.iife.js +8 -8
  5. data/frontend/src/App.vue +6 -2
  6. data/frontend/src/components/AppHeader.vue +35 -8
  7. data/frontend/src/components/AppSidebar.vue +2 -2
  8. data/frontend/src/components/ClassDetails.vue +103 -120
  9. data/frontend/src/components/DiagramView.vue +19 -6
  10. data/frontend/src/components/PackageDetails.vue +67 -26
  11. data/frontend/src/components/PackageTreeNode.vue +25 -20
  12. data/frontend/src/components/SearchResults.vue +21 -8
  13. data/frontend/src/components/WelcomeView.vue +2 -2
  14. data/frontend/src/stores/dataStore.ts +142 -48
  15. data/frontend/src/stores/uiStore.ts +3 -4
  16. data/frontend/src/types.ts +160 -163
  17. data/lib/ea/model/diagram_connector.rb +9 -0
  18. data/lib/ea/model/property.rb +6 -1
  19. data/lib/ea/sources/qea/classifier_builder.rb +3 -1
  20. data/lib/ea/sources/qea/diagram_builder.rb +91 -9
  21. data/lib/ea/sources/qea/property_builder.rb +12 -2
  22. data/lib/ea/spa/output/single_file_strategy.rb +1 -1
  23. data/lib/ea/spa/projector.rb +108 -6
  24. data/lib/ea/svg/ea_emitter/compartment/attributes.rb +1 -0
  25. data/lib/ea/svg/ea_emitter/compartment/constraints.rb +1 -0
  26. data/lib/ea/svg/ea_emitter/compartment/enum_literals.rb +9 -25
  27. data/lib/ea/svg/ea_emitter/compartment/header.rb +1 -0
  28. data/lib/ea/svg/ea_emitter/element/attribute_line_builder.rb +5 -0
  29. data/lib/ea/svg/ea_emitter/element/attribute_renderer.rb +51 -11
  30. data/lib/ea/svg/ea_emitter/element/compartment_geometry.rb +12 -10
  31. data/lib/ea/svg/ea_emitter/element/header_line_provider/name.rb +6 -15
  32. data/lib/ea/svg/ea_emitter/element/header_line_provider/stereotype_label.rb +25 -8
  33. data/lib/ea/svg/ea_emitter/element/header_renderer.rb +16 -4
  34. data/lib/ea/svg/ea_emitter/element/legend_renderer.rb +29 -4
  35. data/lib/ea/svg/ea_emitter/elements.rb +70 -20
  36. data/lib/ea/svg/ea_emitter/font_resolver.rb +5 -6
  37. data/lib/ea/svg/ea_emitter/label/end_label.rb +3 -1
  38. data/lib/ea/svg/ea_emitter/labels.rb +38 -2
  39. data/lib/ea/svg/ea_emitter/text_renderer.rb +97 -8
  40. data/lib/ea/svg/parity/checker.rb +19 -9
  41. data/lib/ea/svg/parity/corpus_report.rb +83 -0
  42. data/lib/ea/svg/parity/differ.rb +231 -0
  43. data/lib/ea/svg/parity.rb +2 -0
  44. data/lib/ea/theme/definition.rb +3 -3
  45. data/lib/ea/theme/loader.rb +2 -2
  46. data/lib/ea/version.rb +1 -1
  47. metadata +4 -2
data/frontend/src/App.vue CHANGED
@@ -17,8 +17,12 @@ onMounted(async () => {
17
17
  const win = window as any
18
18
  if (win.__SPA_DATA__) {
19
19
  data.loadFromEmbedded()
20
- } else if (win.__SPA_DATA_URL__) {
21
- await data.loadFromUrl(win.__SPA_DATA_URL__, win.__SPA_SEARCH_URL__)
20
+ } else if (win.__SPA_SKELETON_URL__) {
21
+ await data.loadFromSharded(
22
+ win.__SPA_SKELETON_URL__,
23
+ win.__SPA_SEARCH_URL__,
24
+ win.__SPA_SHARD_BASE__,
25
+ )
22
26
  }
23
27
 
24
28
  ui.navigateToHash()
@@ -1,14 +1,27 @@
1
1
  <script setup lang="ts">
2
- import { ref, onMounted, onUnmounted } from 'vue'
2
+ import { ref, computed, onMounted, onUnmounted } from 'vue'
3
3
  import { useUiStore } from '../stores/uiStore'
4
+ import { useDataStore } from '../stores/dataStore'
4
5
 
5
6
  const ui = useUiStore()
7
+ const data = useDataStore()
6
8
  const searchInput = ref<HTMLInputElement | null>(null)
7
9
  const showSearchModal = ref(false)
8
10
  const searchQuery = ref('')
9
- const searchResults = ref<any[]>([])
10
11
  const selectedIndex = ref(0)
11
12
 
13
+ const searchResults = computed(() => {
14
+ const q = searchQuery.value.trim().toLowerCase()
15
+ if (!q) return []
16
+ return data.searchEntries
17
+ .filter((e) =>
18
+ e.name.toLowerCase().includes(q) ||
19
+ (e.qualifiedName || '').toLowerCase().includes(q) ||
20
+ (e.content || '').toLowerCase().includes(q))
21
+ .sort((a, b) => (b.boost || 1) - (a.boost || 1))
22
+ .slice(0, 20)
23
+ })
24
+
12
25
  function openSearch() {
13
26
  showSearchModal.value = true
14
27
  setTimeout(() => searchInput.value?.focus(), 50)
@@ -17,7 +30,20 @@ function openSearch() {
17
30
  function closeSearch() {
18
31
  showSearchModal.value = false
19
32
  searchQuery.value = ''
20
- searchResults.value = []
33
+ selectedIndex.value = 0
34
+ }
35
+
36
+ function onSearchKeydown(e: KeyboardEvent) {
37
+ if (e.key === 'ArrowDown') {
38
+ e.preventDefault()
39
+ selectedIndex.value = Math.min(selectedIndex.value + 1, searchResults.value.length - 1)
40
+ } else if (e.key === 'ArrowUp') {
41
+ e.preventDefault()
42
+ selectedIndex.value = Math.max(selectedIndex.value - 1, 0)
43
+ } else if (e.key === 'Enter') {
44
+ const result = searchResults.value[selectedIndex.value]
45
+ if (result) navigateTo(result.id, result.kind)
46
+ }
21
47
  }
22
48
 
23
49
  function handleKeydown(e: KeyboardEvent) {
@@ -35,8 +61,9 @@ function handleKeydown(e: KeyboardEvent) {
35
61
  }
36
62
 
37
63
  function navigateTo(id: string, type: string) {
38
- if (type === 'class') ui.selectClass(id)
39
- else if (type === 'package') ui.selectPackage(id)
64
+ if (type === 'package') ui.selectPackage(id, data.entriesById[id]?.name)
65
+ else if (type === 'diagram') ui.selectDiagram(id, data.entriesById[id]?.name)
66
+ else if (type !== 'property') ui.selectClass(id, data.entriesById[id]?.name)
40
67
  closeSearch()
41
68
  }
42
69
 
@@ -97,12 +124,12 @@ onUnmounted(() => window.removeEventListener('keydown', handleKeydown))
97
124
  <div class="search-modal">
98
125
  <div class="search-input-wrapper">
99
126
  <input ref="searchInput" v-model="searchQuery" placeholder="Search classes, packages, attributes..."
100
- @keydown.escape="closeSearch" autofocus />
127
+ @keydown.escape="closeSearch" @keydown="onSearchKeydown" autofocus />
101
128
  </div>
102
129
  <div class="search-results" v-if="searchResults.length">
103
130
  <div v-for="(result, i) in searchResults" :key="result.id"
104
131
  class="search-result" :class="{ focused: i === selectedIndex }"
105
- @click="navigateTo(result.entityId, result.type)">
132
+ @click="navigateTo(result.id, result.kind)">
106
133
  <span class="result-icon">
107
134
  <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
108
135
  <circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.2"/>
@@ -110,7 +137,7 @@ onUnmounted(() => window.removeEventListener('keydown', handleKeydown))
110
137
  </span>
111
138
  <div class="result-content">
112
139
  <span class="result-name">{{ result.name }}</span>
113
- <span class="result-path">{{ result.entityType }}</span>
140
+ <span class="result-path">{{ result.kind }}</span>
114
141
  </div>
115
142
  </div>
116
143
  </div>
@@ -78,13 +78,13 @@ function formatDate(isoString?: string): string {
78
78
  </div>
79
79
  </div>
80
80
  <div class="package-tree" v-if="data.packageTree">
81
- <PackageTreeNode :node="data.packageTree" />
81
+ <PackageTreeNode v-for="id in data.packageTree.rootIds" :key="id" :node-id="id" />
82
82
  </div>
83
83
  <div v-else class="loading">Loading...</div>
84
84
  </div>
85
85
 
86
86
  <!-- Stats -->
87
- <div class="sidebar-section stats-section" v-if="data.metadata">
87
+ <div class="sidebar-section stats-section" v-if="data.metadata?.statistics">
88
88
  <div class="stats-grid">
89
89
  <div class="stat-item">
90
90
  <span class="stat-value">{{ data.metadata.statistics.packages }}</span>
@@ -1,39 +1,67 @@
1
1
  <script setup lang="ts">
2
- import { computed } from 'vue'
2
+ import { computed, watch } from 'vue'
3
3
  import { useDataStore } from '../stores/dataStore'
4
4
  import { useUiStore } from '../stores/uiStore'
5
+ import type { SpaClassifierPayload, SpaGeneralizationStub } from '../types'
5
6
 
6
7
  const data = useDataStore()
7
8
  const ui = useUiStore()
8
9
 
10
+ const clsId = computed(() => ui.currentClassId)
9
11
  const cls = computed(() =>
10
- ui.currentClassId ? data.getClassById(ui.currentClassId) : null,
12
+ clsId.value ? (data.elementFor(clsId.value)?.payload as SpaClassifierPayload | undefined) ?? null : null,
11
13
  )
12
14
 
13
- function formatCardinality(c: any): string {
14
- if (!c) return ''
15
- return `${c.min || '0'}..${c.max || '*'}`
15
+ watch(
16
+ clsId,
17
+ async (id) => {
18
+ if (!id) return
19
+ const shard = await data.ensureElement(id)
20
+ if (!shard) return
21
+ const parents = (shard.payload.generalizations || []).map(
22
+ (g: SpaGeneralizationStub) => g.targetId,
23
+ )
24
+ await data.ensureElements(parents)
25
+ },
26
+ { immediate: true },
27
+ )
28
+
29
+ const definition = computed(
30
+ () => cls.value?.annotations?.find((a) => a.kind === 'documentation')?.body || '',
31
+ )
32
+
33
+ const BASIC_TYPES = new Set([
34
+ 'String', 'Integer', 'Boolean', 'Real', 'UnlimitedNatural',
35
+ 'DateTime', 'URI', 'Any', 'Object',
36
+ ])
37
+
38
+ function resolveTypeEntry(typeName?: string) {
39
+ if (!typeName) return null
40
+ return data.classifierEntryByName(typeName)
16
41
  }
17
42
 
18
- function resolveType(typeName: string): { isBasic: boolean; classId: string | null } {
19
- const basicTypes = new Set([
20
- 'String', 'Integer', 'Boolean', 'Real', 'UnlimitedNatural',
21
- 'DateTime', 'URI', 'Any', 'Object',
22
- ])
23
- if (basicTypes.has(typeName)) return { isBasic: true, classId: null }
24
- const found = data.findClassByName(typeName)
25
- return { isBasic: false, classId: found ? found.id : null }
43
+ function isBasicType(typeName?: string): boolean {
44
+ return !!typeName && BASIC_TYPES.has(typeName)
26
45
  }
27
46
 
28
- function associationTarget(assoc: any): any {
29
- if (!cls.value) return null
30
- if (assoc.source?.class !== cls.value.xmiId) return assoc.source
31
- return assoc.target
47
+ function entryName(id?: string): string {
48
+ if (!id) return ''
49
+ return data.entriesById[id]?.name || id
32
50
  }
33
51
 
34
- function badgeType(c: any): string {
35
- if (c.literals && c.literals.length > 0) return 'ENUMERATION'
36
- return c.type?.toUpperCase() || 'CLASS'
52
+ function formatCardinality(m?: [number?, number?]): string {
53
+ if (!m) return ''
54
+ return `${m[0] ?? 0}..${m[1] ?? '*'}`
55
+ }
56
+
57
+ function formatParameters(op: { parameters: { name?: string; typeName?: string }[] }): string {
58
+ return op.parameters
59
+ .map((p) => `${p.name || ''}: ${p.typeName || '?'}`)
60
+ .join(', ')
61
+ }
62
+
63
+ function badgeType(c: SpaClassifierPayload): string {
64
+ return (c.modelKind || 'class').toUpperCase()
37
65
  }
38
66
  </script>
39
67
 
@@ -42,51 +70,52 @@ function badgeType(c: any): string {
42
70
  <div class="entity-header">
43
71
  <div class="entity-title">
44
72
  <h2 class="entity-name">{{ cls.qualifiedName }}</h2>
45
- <div class="entity-subtitle" v-if="cls.package">
46
- <a href="#" class="link-button" @click.prevent="ui.selectPackage(cls.package!, data.getPackageById(cls.package!)?.name)">
47
- {{ data.getPackageById(cls.package)?.name || cls.package }}
73
+ <div class="entity-subtitle" v-if="cls.packageId">
74
+ <a href="#" class="link-button"
75
+ @click.prevent="ui.selectPackage(cls.packageId, data.nodesById[cls.packageId]?.name)">
76
+ {{ data.nodesById[cls.packageId]?.name || cls.packageName || cls.packageId }}
48
77
  </a>
49
78
  </div>
50
79
  </div>
51
- <span class="entity-badge" :class="'badge-' + badgeType(cls)">{{ badgeType(cls) }}</span>
80
+ <span class="entity-badge" :class="'badge-' + cls.modelKind">{{ badgeType(cls) }}</span>
52
81
  <span class="entity-badge badge-abstract" v-if="cls.isAbstract">abstract</span>
53
82
  </div>
54
83
 
55
- <div class="entity-metadata" v-if="cls.stereotypes.length">
84
+ <div class="entity-metadata" v-if="cls.stereotypeRefs.length">
56
85
  <div class="metadata-item">
57
86
  <span class="metadata-label">Stereotypes</span>
58
87
  <span class="metadata-value">
59
- <span v-for="s in cls.stereotypes" :key="s" class="stereotype-tag">&laquo;{{ s }}&raquo;</span>
88
+ <span v-for="s in cls.stereotypeRefs" :key="s" class="stereotype-tag">&laquo;{{ s }}&raquo;</span>
60
89
  </span>
61
90
  </div>
62
91
  </div>
63
92
 
64
- <div class="entity-definition" v-if="cls.definition">
65
- <div class="definition-content">{{ cls.definition }}</div>
93
+ <div class="entity-definition" v-if="definition">
94
+ <div class="definition-content">{{ definition }}</div>
66
95
  </div>
67
96
 
68
97
  <!-- Inheritance -->
69
- <div class="section" v-if="cls.generalizations.length || cls.specializations.length">
98
+ <div class="section" v-if="cls.generalizations?.length || cls.specializations?.length">
70
99
  <h3 class="section-title">Inheritance</h3>
71
- <div v-if="cls.generalizations.length" class="inheritance-group">
100
+ <div v-if="cls.generalizations?.length" class="inheritance-group">
72
101
  <div class="inheritance-header">&#8593; Extends</div>
73
- <div v-for="parentId in cls.generalizations" :key="parentId" class="list-item clickable-row"
74
- @click="ui.selectClass(parentId)">
75
- <span class="list-item-name">{{ data.getClassById(parentId)?.name || parentId }}</span>
102
+ <div v-for="g in cls.generalizations" :key="g.id" class="list-item clickable-row"
103
+ @click="ui.selectClass(g.targetId, entryName(g.targetId))">
104
+ <span class="list-item-name">{{ entryName(g.targetId) }}</span>
76
105
  </div>
77
106
  </div>
78
- <div v-if="cls.specializations.length" class="inheritance-group">
107
+ <div v-if="cls.specializations?.length" class="inheritance-group">
79
108
  <div class="inheritance-header">&#8595; Extended by</div>
80
- <div v-for="childId in cls.specializations" :key="childId" class="list-item clickable-row"
81
- @click="ui.selectClass(childId)">
82
- <span class="list-item-name">{{ data.getClassById(childId)?.name || childId }}</span>
109
+ <div v-for="g in cls.specializations" :key="g.id" class="list-item clickable-row"
110
+ @click="ui.selectClass(g.targetId, entryName(g.targetId))">
111
+ <span class="list-item-name">{{ entryName(g.targetId) }}</span>
83
112
  </div>
84
113
  </div>
85
114
  </div>
86
115
 
87
116
  <!-- Attributes -->
88
- <div class="section" v-if="cls.attributes.length">
89
- <h3 class="section-title">Attributes <span class="section-count">{{ cls.attributes.length }}</span></h3>
117
+ <div class="section" v-if="cls.properties.length">
118
+ <h3 class="section-title">Attributes <span class="section-count">{{ cls.properties.length }}</span></h3>
90
119
  <div class="table-wrapper">
91
120
  <table class="data-table">
92
121
  <thead>
@@ -99,33 +128,30 @@ function badgeType(c: any): string {
99
128
  </tr>
100
129
  </thead>
101
130
  <tbody>
102
- <tr v-for="attrId in cls.attributes" :key="attrId" class="clickable-row">
103
- <td>{{ data.getAttributeById(attrId)?.name }}</td>
131
+ <tr v-for="attr in cls.properties" :key="attr.id" class="clickable-row">
132
+ <td>{{ attr.name }}</td>
104
133
  <td>
105
- <template v-if="data.getAttributeById(attrId)?.type">
106
- <a v-if="resolveType(data.getAttributeById(attrId)!.type).classId"
134
+ <template v-if="attr.typeName">
135
+ <a v-if="resolveTypeEntry(attr.typeName)"
107
136
  href="#" class="type-link"
108
- @click.prevent="ui.selectClass(resolveType(data.getAttributeById(attrId)!.type).classId!)">
109
- {{ data.getAttributeById(attrId)?.type }}
137
+ @click.prevent="ui.selectClass(resolveTypeEntry(attr.typeName)!.id, resolveTypeEntry(attr.typeName)!.name)">
138
+ {{ attr.typeName }}
110
139
  </a>
111
- <span v-else-if="resolveType(data.getAttributeById(attrId)!.type).isBasic"
112
- class="uml-basic-type">
113
- {{ data.getAttributeById(attrId)?.type }}
140
+ <span v-else-if="isBasicType(attr.typeName)" class="uml-basic-type">
141
+ {{ attr.typeName }}
114
142
  </span>
115
- <span v-else class="type-unresolved">{{ data.getAttributeById(attrId)?.type }}</span>
143
+ <span v-else class="type-unresolved">{{ attr.typeName }}</span>
116
144
  </template>
117
145
  </td>
118
146
  <td>
119
- <span v-if="data.getAttributeById(attrId)?.visibility"
120
- class="visibility-badge"
121
- :data-visibility="data.getAttributeById(attrId)?.visibility">
122
- {{ data.getAttributeById(attrId)?.visibility }}
147
+ <span v-if="attr.visibility" class="visibility-badge" :data-visibility="attr.visibility">
148
+ {{ attr.visibility }}
123
149
  </span>
124
150
  </td>
125
- <td>{{ formatCardinality(data.getAttributeById(attrId)?.cardinality) }}</td>
151
+ <td>{{ formatCardinality([attr.multiplicityLower, attr.multiplicityUpper]) }}</td>
126
152
  <td>
127
- <span v-if="data.getAttributeById(attrId)?.isStatic" class="modifier-badge">static</span>
128
- <span v-if="data.getAttributeById(attrId)?.isReadOnly" class="modifier-badge">readonly</span>
153
+ <span v-if="attr.isReadonly" class="modifier-badge">readonly</span>
154
+ <span v-if="attr.isDerived" class="modifier-badge">derived</span>
129
155
  </td>
130
156
  </tr>
131
157
  </tbody>
@@ -133,28 +159,6 @@ function badgeType(c: any): string {
133
159
  </div>
134
160
  </div>
135
161
 
136
- <!-- Inherited Attributes -->
137
- <div class="section" v-if="cls.inheritedAttributes.length">
138
- <h3 class="section-title">Inherited Attributes</h3>
139
- <div v-for="ia in cls.inheritedAttributes" :key="ia.attributeId" class="inheritance-group">
140
- <div class="inheritance-header">From {{ ia.inheritedFromName }}</div>
141
- <div class="table-wrapper">
142
- <table class="data-table inherited-table">
143
- <thead>
144
- <tr><th>Name</th><th>Type</th><th>Cardinality</th></tr>
145
- </thead>
146
- <tbody>
147
- <tr>
148
- <td>{{ ia.attribute.name }}</td>
149
- <td>{{ ia.attribute.type }}</td>
150
- <td>{{ formatCardinality(ia.attribute.cardinality) }}</td>
151
- </tr>
152
- </tbody>
153
- </table>
154
- </div>
155
- </div>
156
- </div>
157
-
158
162
  <!-- Operations -->
159
163
  <div class="section" v-if="cls.operations.length">
160
164
  <h3 class="section-title">Operations <span class="section-count">{{ cls.operations.length }}</span></h3>
@@ -164,25 +168,21 @@ function badgeType(c: any): string {
164
168
  <tr><th>Name</th><th>Return</th><th>Visibility</th><th>Modifiers</th></tr>
165
169
  </thead>
166
170
  <tbody>
167
- <tr v-for="opId in cls.operations" :key="opId" class="clickable-row">
171
+ <tr v-for="op in cls.operations" :key="op.id" class="clickable-row">
168
172
  <td>
169
- {{ data.getOperationById(opId)?.name }}(
170
- <span v-if="data.getOperationById(opId)?.parameters.length">
171
- {{ data.getOperationById(opId)?.parameters.map(p => `${p.name}: ${p.type || '?'}`).join(', ') }}
172
- </span>
173
+ {{ op.name }}(
174
+ <span v-if="op.parameters.length">{{ formatParameters(op) }}</span>
173
175
  )
174
176
  </td>
175
- <td>{{ data.getOperationById(opId)?.returnType || 'void' }}</td>
177
+ <td>{{ op.returnTypeName || 'void' }}</td>
176
178
  <td>
177
- <span v-if="data.getOperationById(opId)?.visibility"
178
- class="visibility-badge"
179
- :data-visibility="data.getOperationById(opId)?.visibility">
180
- {{ data.getOperationById(opId)?.visibility }}
179
+ <span v-if="op.visibility" class="visibility-badge" :data-visibility="op.visibility">
180
+ {{ op.visibility }}
181
181
  </span>
182
182
  </td>
183
183
  <td>
184
- <span v-if="data.getOperationById(opId)?.isStatic" class="modifier-badge">static</span>
185
- <span v-if="data.getOperationById(opId)?.isAbstract" class="modifier-badge">abstract</span>
184
+ <span v-if="op.isStatic" class="modifier-badge">static</span>
185
+ <span v-if="op.isAbstract" class="modifier-badge">abstract</span>
186
186
  </td>
187
187
  </tr>
188
188
  </tbody>
@@ -191,7 +191,7 @@ function badgeType(c: any): string {
191
191
  </div>
192
192
 
193
193
  <!-- Associations -->
194
- <div class="section" v-if="cls.associations.length">
194
+ <div class="section" v-if="cls.associations?.length">
195
195
  <h3 class="section-title">Associations <span class="section-count">{{ cls.associations.length }}</span></h3>
196
196
  <div class="table-wrapper">
197
197
  <table class="data-table">
@@ -199,47 +199,30 @@ function badgeType(c: any): string {
199
199
  <tr><th>Name</th><th>Target</th><th>Cardinality</th><th>Aggregation</th></tr>
200
200
  </thead>
201
201
  <tbody>
202
- <tr v-for="assocId in cls.associations" :key="assocId">
203
- <td>{{ data.getAssociationById(assocId)?.name }}</td>
202
+ <tr v-for="assoc in cls.associations" :key="assoc.id">
203
+ <td>{{ assoc.name || assoc.thisEndRoleName || '' }}</td>
204
204
  <td>
205
- <template v-if="associationTarget(data.getAssociationById(assocId))">
206
- <a v-if="associationTarget(data.getAssociationById(assocId))?.className"
207
- href="#" class="type-link"
208
- @click.prevent="() => {
209
- const found = data.findClassByName(associationTarget(data.getAssociationById(assocId))?.className || '')
210
- if (found) ui.selectClass(found.id)
211
- }">
212
- {{ associationTarget(data.getAssociationById(assocId))?.className }}
213
- </a>
214
- </template>
205
+ <a v-if="data.entriesById[assoc.otherEndId]" href="#" class="type-link"
206
+ @click.prevent="ui.selectClass(assoc.otherEndId, entryName(assoc.otherEndId))">
207
+ {{ entryName(assoc.otherEndId) }}
208
+ </a>
209
+ <span v-else>{{ assoc.otherEndId }}</span>
215
210
  </td>
216
- <td>{{ formatCardinality(associationTarget(data.getAssociationById(assocId))?.cardinality) }}</td>
217
- <td>{{ associationTarget(data.getAssociationById(assocId))?.aggregation }}</td>
211
+ <td>{{ formatCardinality(assoc.otherEndMultiplicity) }}</td>
212
+ <td>{{ assoc.otherEndAggregation }}</td>
218
213
  </tr>
219
214
  </tbody>
220
215
  </table>
221
216
  </div>
222
217
  </div>
223
218
 
224
- <!-- Inherited Associations -->
225
- <div class="section" v-if="cls.inheritedAssociations.length">
226
- <h3 class="section-title">Inherited Associations</h3>
227
- <div v-for="ia in cls.inheritedAssociations" :key="ia.associationId" class="inheritance-group">
228
- <div class="inheritance-header">From {{ ia.inheritedFromName }}</div>
229
- <div class="list-item">
230
- <span class="list-item-name">{{ data.getAssociationById(ia.associationId)?.name }}</span>
231
- <span class="list-item-meta">{{ ia.localRole }}</span>
232
- </div>
233
- </div>
234
- </div>
235
-
236
219
  <!-- Enum Literals -->
237
- <div class="section" v-if="cls.literals.length">
220
+ <div class="section" v-if="cls.literals?.length">
238
221
  <h3 class="section-title">Literals <span class="section-count">{{ cls.literals.length }}</span></h3>
239
222
  <div class="item-list">
240
- <div v-for="lit in cls.literals" :key="lit.name" class="list-item">
223
+ <div v-for="lit in cls.literals" :key="lit.id" class="list-item">
241
224
  <span class="list-item-name">{{ lit.name }}</span>
242
- <span v-if="lit.definition" class="list-item-meta">{{ lit.definition }}</span>
225
+ <span v-if="lit.value && lit.value !== lit.name" class="list-item-meta">{{ lit.value }}</span>
243
226
  </div>
244
227
  </div>
245
228
  </div>
@@ -1,15 +1,28 @@
1
1
  <script setup lang="ts">
2
- import { computed, ref } from 'vue'
2
+ import { computed, ref, watch } from 'vue'
3
3
  import { useDataStore } from '../stores/dataStore'
4
4
  import { useUiStore } from '../stores/uiStore'
5
+ import type { SpaDiagramPayload } from '../types'
5
6
 
6
7
  const data = useDataStore()
7
8
  const ui = useUiStore()
8
9
 
10
+ const diagId = computed(() => ui.currentDiagramId)
9
11
  const diagram = computed(() =>
10
- ui.currentDiagramId ? data.getDiagramById(ui.currentDiagramId) : null,
12
+ diagId.value ? (data.elementFor(diagId.value)?.payload as SpaDiagramPayload | undefined) ?? null : null,
11
13
  )
12
14
 
15
+ watch(
16
+ diagId,
17
+ (id) => {
18
+ if (id) data.ensureElement(id)
19
+ },
20
+ { immediate: true },
21
+ )
22
+
23
+ const objectCount = computed(() => diagram.value?.elements?.length ?? 0)
24
+ const linkCount = computed(() => diagram.value?.connectors?.length ?? 0)
25
+
13
26
  const zoom = ref(1)
14
27
  const panX = ref(0)
15
28
  const panY = ref(0)
@@ -50,17 +63,17 @@ const svgTransform = computed(() =>
50
63
  </div>
51
64
 
52
65
  <div class="entity-metadata">
53
- <div class="metadata-item" v-if="diagram.type">
66
+ <div class="metadata-item" v-if="diagram.diagramType">
54
67
  <span class="metadata-label">Type</span>
55
- <span class="metadata-value">{{ diagram.type }}</span>
68
+ <span class="metadata-value">{{ diagram.diagramType }}</span>
56
69
  </div>
57
70
  <div class="metadata-item">
58
71
  <span class="metadata-label">Elements</span>
59
- <span class="metadata-value">{{ diagram.objectCount }}</span>
72
+ <span class="metadata-value">{{ objectCount }}</span>
60
73
  </div>
61
74
  <div class="metadata-item">
62
75
  <span class="metadata-label">Connectors</span>
63
- <span class="metadata-value">{{ diagram.linkCount }}</span>
76
+ <span class="metadata-value">{{ linkCount }}</span>
64
77
  </div>
65
78
  </div>
66
79