@glossarist/concept-browser 0.7.51 → 0.7.52
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.
- package/cli/index.mjs +32 -0
- package/env.d.ts +15 -0
- package/package.json +12 -2
- package/scripts/__tests__/doctor.test.mjs +147 -0
- package/scripts/doctor.mjs +327 -0
- package/scripts/generate-data.mjs +136 -0
- package/scripts/generate-ontology-data.mjs +3 -3
- package/scripts/generate-ontology-schema.mjs +3 -3
- package/scripts/lib/agents-turtle.mjs +64 -0
- package/scripts/lib/bibliography-turtle.mjs +54 -0
- package/scripts/lib/build-activity-turtle.mjs +92 -0
- package/scripts/lib/build-cache.mjs +70 -0
- package/scripts/lib/dataset-turtle.mjs +79 -0
- package/scripts/lib/turtle-escape.mjs +0 -0
- package/scripts/lib/version-turtle.mjs +56 -0
- package/scripts/lib/vocab-turtle.mjs +64 -0
- package/scripts/normalize-yaml.mjs +99 -0
- package/scripts/sync-concept-model.mjs +86 -0
- package/scripts/validate-shacl.mjs +194 -0
- package/src/__fixtures__/concept-shape.ttl +20 -0
- package/src/__fixtures__/shacl/bad/concept.ttl +7 -0
- package/src/__fixtures__/shacl/empty/.gitkeep +0 -0
- package/src/__fixtures__/shacl/good/concept.ttl +8 -0
- package/src/__tests__/__fixtures__/concepts.ts +221 -0
- package/src/__tests__/adapters/concept-identity.test.ts +76 -0
- package/src/__tests__/components/error-boundary.test.ts +109 -0
- package/src/__tests__/composables/use-dataset-series.test.ts +262 -0
- package/src/__tests__/concept-rdf/agents-emitter.test.ts +110 -0
- package/src/__tests__/concept-rdf/bibliography-emitter.test.ts +159 -0
- package/src/__tests__/concept-rdf/build-activity-emitter.test.ts +119 -0
- package/src/__tests__/concept-rdf/coerce-date.test.ts +97 -0
- package/src/__tests__/concept-rdf/concept-emitter.test.ts +258 -0
- package/src/__tests__/concept-rdf/dataset-emitter.test.ts +224 -0
- package/src/__tests__/concept-rdf/differential.test.ts +96 -0
- package/src/__tests__/concept-rdf/group-emitter.test.ts +109 -0
- package/src/__tests__/concept-rdf/image-variant-emitter.test.ts +135 -0
- package/src/__tests__/concept-rdf/jsonld-writer.test.ts +109 -0
- package/src/__tests__/concept-rdf/nonverbal-rep.test.ts +177 -0
- package/src/__tests__/concept-rdf/property-based.test.ts +179 -0
- package/src/__tests__/concept-rdf/provenance.test.ts +110 -0
- package/src/__tests__/concept-rdf/quad-isomorphism.test.ts +43 -0
- package/src/__tests__/concept-rdf/quad-isomorphism.ts +47 -0
- package/src/__tests__/concept-rdf/rdf-components.test.ts +145 -0
- package/src/__tests__/concept-rdf/rdf-graph.test.ts +115 -0
- package/src/__tests__/concept-rdf/round-trip.test.ts +243 -0
- package/src/__tests__/concept-rdf/scoped-examples.test.ts +126 -0
- package/src/__tests__/concept-rdf/sections-builder.test.ts +94 -0
- package/src/__tests__/concept-rdf/shacl-conformance.test.ts +110 -0
- package/src/__tests__/concept-rdf/shape-consistency.test.ts +138 -0
- package/src/__tests__/concept-rdf/snapshot-generation.test.ts +75 -0
- package/src/__tests__/concept-rdf/table-formula-emitter.test.ts +142 -0
- package/src/__tests__/concept-rdf/turtle-writer.test.ts +114 -0
- package/src/__tests__/concept-rdf/use-rdf-document.test.ts +246 -0
- package/src/__tests__/concept-rdf/version-emitter.test.ts +120 -0
- package/src/__tests__/concept-rdf/vocabulary-ssot.test.ts +100 -0
- package/src/__tests__/concept-rdf-view.test.ts +136 -0
- package/src/__tests__/dataset-style.test.ts +12 -7
- package/src/__tests__/errors/errors.test.ts +142 -0
- package/src/__tests__/format-downloads.test.ts +47 -65
- package/src/__tests__/markdown-lite.test.ts +19 -0
- package/src/__tests__/perf/bundle-layout.test.ts +50 -0
- package/src/__tests__/perf/serialization-perf.test.ts +121 -0
- package/src/__tests__/scripts/agents-turtle.test.ts +61 -0
- package/src/__tests__/scripts/bibliography-turtle.test.ts +59 -0
- package/src/__tests__/scripts/build-activity-turtle.test.ts +75 -0
- package/src/__tests__/scripts/build-cache.test.ts +78 -0
- package/src/__tests__/scripts/build-integration.test.ts +134 -0
- package/src/__tests__/scripts/dataset-turtle.test.ts +94 -0
- package/src/__tests__/scripts/normalize-yaml.test.ts +98 -0
- package/src/__tests__/scripts/stryker-config.test.ts +33 -0
- package/src/__tests__/scripts/turtle-escape.test.ts +63 -0
- package/src/__tests__/scripts/version-turtle.test.ts +72 -0
- package/src/__tests__/scripts/vocab-turtle.test.ts +63 -0
- package/src/__tests__/use-format-registry.test.ts +125 -0
- package/src/__tests__/utils/bcp47.test.ts +166 -0
- package/src/__tests__/utils/color-theme.test.ts +143 -0
- package/src/__tests__/utils/url-safety.test.ts +65 -0
- package/src/__tests__/validate-shacl.test.ts +100 -0
- package/src/adapters/DatasetAdapter.ts +11 -5
- package/src/adapters/GraphDataSource.ts +2 -1
- package/src/adapters/UriRouter.ts +2 -1
- package/src/adapters/concept-identity.ts +69 -0
- package/src/adapters/factory.ts +3 -2
- package/src/adapters/model-bridge.ts +2 -1
- package/src/adapters/non-verbal/glossarist-augment.d.ts +7 -0
- package/src/adapters/non-verbal-resolver.ts +2 -1
- package/src/components/AppSidebar.vue +189 -93
- package/src/components/ConceptDetail.vue +8 -0
- package/src/components/ConceptEditionRail.vue +222 -0
- package/src/components/ConceptRdfView.vue +37 -377
- package/src/components/DatasetSeriesCard.vue +270 -0
- package/src/components/ErrorBoundary.vue +95 -0
- package/src/components/FormatDownloads.vue +17 -13
- package/src/components/HomeSeriesSection.vue +277 -0
- package/src/components/RelationSphere.vue +1672 -0
- package/src/components/SidebarSeriesSection.vue +239 -0
- package/src/components/concept-rdf/RdfInstanceHeader.vue +47 -0
- package/src/components/concept-rdf/RdfInstanceSection.vue +54 -0
- package/src/components/concept-rdf/RdfPrefixLegend.vue +27 -0
- package/src/components/concept-rdf/RdfSourcePanel.vue +72 -0
- package/src/components/concept-rdf/agents-emitter.ts +82 -0
- package/src/components/concept-rdf/bibliography-emitter.ts +83 -0
- package/src/components/concept-rdf/build-activity-emitter.ts +89 -0
- package/src/components/concept-rdf/concept-emitter.ts +443 -0
- package/src/components/concept-rdf/dataset-emitter.ts +95 -0
- package/src/components/concept-rdf/group-emitter.ts +69 -0
- package/src/components/concept-rdf/image-variant-emitter.ts +46 -0
- package/src/components/concept-rdf/jsonld-writer.ts +82 -0
- package/src/components/concept-rdf/predicates.ts +261 -0
- package/src/components/concept-rdf/provenance.ts +80 -0
- package/src/components/concept-rdf/rdf-graph.ts +211 -0
- package/src/components/concept-rdf/rdf-prefixes.ts +23 -0
- package/src/components/concept-rdf/sections-builder.ts +62 -0
- package/src/components/concept-rdf/table-formula-emitter.ts +101 -0
- package/src/components/concept-rdf/turtle-writer.ts +116 -0
- package/src/components/concept-rdf/use-rdf-document.ts +72 -0
- package/src/components/concept-rdf/version-emitter.ts +65 -0
- package/src/components/concept-rdf/vocabulary-emitter.ts +62 -0
- package/src/composables/use-color-theme.ts +82 -0
- package/src/composables/use-format-registry.ts +42 -0
- package/src/composables/useDatasetSeries.ts +258 -0
- package/src/composables/useSphereProjection.ts +125 -0
- package/src/config/group-types.ts +92 -0
- package/src/config/types.ts +81 -2
- package/src/config/use-site-config.ts +2 -1
- package/src/errors.ts +136 -0
- package/src/i18n/locales/eng.yml +24 -0
- package/src/i18n/locales/fra.yml +24 -0
- package/src/stores/vocabulary.ts +3 -1
- package/src/style.css +17 -2
- package/src/types/agents-version-turtle.d.ts +27 -0
- package/src/types/bibliography-turtle.d.ts +12 -0
- package/src/types/build-activity-turtle.d.ts +16 -0
- package/src/types/build-cache.d.ts +20 -0
- package/src/types/dataset-turtle.d.ts +32 -0
- package/src/types/normalize-yaml.d.ts +16 -0
- package/src/types/turtle-escape.d.ts +6 -0
- package/src/types/vocab-turtle.d.ts +13 -0
- package/src/utils/asciidoc-lite.ts +11 -6
- package/src/utils/bcp47.ts +141 -0
- package/src/utils/color-theme-integration.ts +11 -0
- package/src/utils/color-theme.ts +129 -0
- package/src/utils/dataset-style.ts +31 -6
- package/src/utils/locale.ts +6 -14
- package/src/utils/markdown-lite.ts +6 -1
- package/src/utils/relation-sphere-styling.ts +63 -0
- package/src/utils/relationship-categories.ts +30 -0
- package/src/utils/url-safety.ts +30 -0
- package/src/views/ConceptView.vue +183 -9
- package/src/views/DatasetView.vue +6 -0
- package/src/views/HomeView.vue +5 -0
- package/vite.config.ts +7 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* ConceptEditionRail — sidebar card showing a concept's supersession chain
|
|
4
|
+
* across vocabulary editions. Rendered INSIDE ConceptDetail's right sidebar
|
|
5
|
+
* (between Relations and other cards), styled identically to its neighbors.
|
|
6
|
+
*
|
|
7
|
+
* Reads `supersedes` edges from the graph engine and walks the full chain
|
|
8
|
+
* in both directions. Filter out malformed URIs that sometimes appear in
|
|
9
|
+
* stub-data scenarios.
|
|
10
|
+
*/
|
|
11
|
+
import { computed } from 'vue';
|
|
12
|
+
import { useRouter } from 'vue-router';
|
|
13
|
+
import { useVocabularyStore } from '../stores/vocabulary';
|
|
14
|
+
import { useDatasetSeries, type DatasetSeriesMember } from '../composables/useDatasetSeries';
|
|
15
|
+
import { useI18n } from '../i18n';
|
|
16
|
+
import { UriRouter } from '../adapters/UriRouter';
|
|
17
|
+
|
|
18
|
+
const { t } = useI18n();
|
|
19
|
+
|
|
20
|
+
const props = defineProps<{
|
|
21
|
+
conceptUri: string;
|
|
22
|
+
registerId: string;
|
|
23
|
+
conceptId: string;
|
|
24
|
+
}>();
|
|
25
|
+
|
|
26
|
+
const store = useVocabularyStore();
|
|
27
|
+
const router = useRouter();
|
|
28
|
+
|
|
29
|
+
const { seriesForActive } = useDatasetSeries(() => props.registerId);
|
|
30
|
+
const series = computed(() => seriesForActive.value);
|
|
31
|
+
|
|
32
|
+
interface EditionEntry {
|
|
33
|
+
member: DatasetSeriesMember;
|
|
34
|
+
conceptUri: string;
|
|
35
|
+
conceptId: string;
|
|
36
|
+
edgeType: 'self' | 'supersedes' | 'superseded_by';
|
|
37
|
+
hops: number;
|
|
38
|
+
isCurrentEdition: boolean; // newest valid edition in the series
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Resolve a concept URI via the project's UriRouter SSOT. Returns null for
|
|
42
|
+
* malformed or non-concept URIs (URN-form refs, external IRIs, stub data). */
|
|
43
|
+
function parseStrict(uri: string): { registerId: string; conceptId: string } | null {
|
|
44
|
+
return UriRouter.parseUri(uri);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Bidirectional BFS through supersedes edges from the start URI. */
|
|
48
|
+
function walkChain(startUri: string): Map<string, { type: 'supersedes' | 'superseded_by'; hops: number }> {
|
|
49
|
+
const out = new Map<string, { type: 'supersedes' | 'superseded_by'; hops: number }>();
|
|
50
|
+
|
|
51
|
+
/* Forward (concepts THIS supersedes — older editions it replaced) */
|
|
52
|
+
const forwardQueue: Array<{ uri: string; hops: number }> = [{ uri: startUri, hops: 0 }];
|
|
53
|
+
const forwardSeen = new Set<string>([startUri]);
|
|
54
|
+
while (forwardQueue.length > 0) {
|
|
55
|
+
const { uri, hops } = forwardQueue.shift()!;
|
|
56
|
+
for (const e of store.graph.getEdges(uri)) {
|
|
57
|
+
if (e.type === 'supersedes' && !forwardSeen.has(e.target)) {
|
|
58
|
+
forwardSeen.add(e.target);
|
|
59
|
+
out.set(e.target, { type: 'supersedes', hops: hops + 1 });
|
|
60
|
+
forwardQueue.push({ uri: e.target, hops: hops + 1 });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* Backward (concepts that SUPERSEDED this — newer editions) */
|
|
66
|
+
const backwardQueue: Array<{ uri: string; hops: number }> = [{ uri: startUri, hops: 0 }];
|
|
67
|
+
const backwardSeen = new Set<string>([startUri]);
|
|
68
|
+
while (backwardQueue.length > 0) {
|
|
69
|
+
const { uri, hops } = backwardQueue.shift()!;
|
|
70
|
+
for (const e of store.graph.getIncomingEdges(uri)) {
|
|
71
|
+
if (e.type === 'supersedes' && !backwardSeen.has(e.source)) {
|
|
72
|
+
backwardSeen.add(e.source);
|
|
73
|
+
out.set(e.source, { type: 'superseded_by', hops: hops + 1 });
|
|
74
|
+
backwardQueue.push({ uri: e.source, hops: hops + 1 });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const editionChain = computed<EditionEntry[]>(() => {
|
|
83
|
+
const s = series.value;
|
|
84
|
+
const entries: EditionEntry[] = [];
|
|
85
|
+
|
|
86
|
+
/* Always include self — use props directly so we don't depend on URI parsing. */
|
|
87
|
+
const selfMember: DatasetSeriesMember = s?.members.find(m => m.id === props.registerId) ?? {
|
|
88
|
+
id: props.registerId,
|
|
89
|
+
ref: props.registerId,
|
|
90
|
+
year: undefined,
|
|
91
|
+
status: 'valid',
|
|
92
|
+
isCurrent: false,
|
|
93
|
+
isActive: true,
|
|
94
|
+
conceptCount: undefined,
|
|
95
|
+
registerId: props.registerId,
|
|
96
|
+
};
|
|
97
|
+
entries.push({
|
|
98
|
+
member: selfMember,
|
|
99
|
+
conceptUri: props.conceptUri,
|
|
100
|
+
conceptId: props.conceptId,
|
|
101
|
+
edgeType: 'self',
|
|
102
|
+
hops: 0,
|
|
103
|
+
isCurrentEdition: !!selfMember.isCurrent,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/* Walk the chain. Drop malformed URIs (no real concept id). */
|
|
107
|
+
const chain = walkChain(props.conceptUri);
|
|
108
|
+
for (const [uri, info] of chain) {
|
|
109
|
+
if (uri === props.conceptUri) continue;
|
|
110
|
+
const parsed = parseStrict(uri);
|
|
111
|
+
if (!parsed) continue;
|
|
112
|
+
if (entries.some(e => e.conceptUri === uri)) continue;
|
|
113
|
+
|
|
114
|
+
const member = s?.members.find(m => m.id === parsed.registerId) ?? {
|
|
115
|
+
id: parsed.registerId,
|
|
116
|
+
ref: parsed.registerId,
|
|
117
|
+
year: undefined,
|
|
118
|
+
status: 'unknown',
|
|
119
|
+
isCurrent: false,
|
|
120
|
+
isActive: false,
|
|
121
|
+
conceptCount: undefined,
|
|
122
|
+
registerId: parsed.registerId,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
entries.push({
|
|
126
|
+
member,
|
|
127
|
+
conceptUri: uri,
|
|
128
|
+
conceptId: parsed.conceptId,
|
|
129
|
+
edgeType: info.type,
|
|
130
|
+
hops: info.hops,
|
|
131
|
+
isCurrentEdition: !!member.isCurrent,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* Sort by year ascending (oldest first). */
|
|
136
|
+
entries.sort((a, b) => (a.member.year ?? 9999) - (b.member.year ?? 9999));
|
|
137
|
+
|
|
138
|
+
return entries;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const hasChain = computed(() => editionChain.value.length > 1);
|
|
142
|
+
|
|
143
|
+
function navigate(entry: EditionEntry) {
|
|
144
|
+
if (entry.edgeType === 'self') return;
|
|
145
|
+
router.push({
|
|
146
|
+
name: 'concept',
|
|
147
|
+
params: { registerId: entry.member.id, conceptId: entry.conceptId },
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function edgeLabel(entry: EditionEntry): string {
|
|
152
|
+
switch (entry.edgeType) {
|
|
153
|
+
case 'supersedes': return t('edge.supersedes');
|
|
154
|
+
case 'superseded_by': return t('edge.superseded_by');
|
|
155
|
+
default: return '';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
</script>
|
|
159
|
+
|
|
160
|
+
<template>
|
|
161
|
+
<div v-if="series" class="card p-5">
|
|
162
|
+
<div class="section-label">{{ t('concept.editionSeries') }}</div>
|
|
163
|
+
<div class="mt-1 text-xs text-ink-400 italic">{{ series.title }}</div>
|
|
164
|
+
|
|
165
|
+
<div class="mt-3 space-y-1">
|
|
166
|
+
<button
|
|
167
|
+
v-for="entry in editionChain"
|
|
168
|
+
:key="entry.conceptUri"
|
|
169
|
+
type="button"
|
|
170
|
+
class="concept-link block w-full text-left rounded-md px-1.5 py-1.5 transition-colors"
|
|
171
|
+
:class="entry.edgeType === 'self'
|
|
172
|
+
? 'bg-blue-50 dark:bg-blue-900/20'
|
|
173
|
+
: 'hover:bg-ink-50 dark:hover:bg-ink-700/40'"
|
|
174
|
+
:disabled="entry.edgeType === 'self'"
|
|
175
|
+
@click="navigate(entry)"
|
|
176
|
+
>
|
|
177
|
+
<div class="flex items-center gap-1 mb-0.5">
|
|
178
|
+
<span
|
|
179
|
+
v-if="entry.isCurrentEdition"
|
|
180
|
+
class="badge text-[9px] flex-shrink-0"
|
|
181
|
+
:class="entry.edgeType === 'self' ? 'badge-blue' : 'badge-gray'"
|
|
182
|
+
style="background: rgba(184, 147, 90, 0.18); color: #8C6A3A; border: 1px solid rgba(184, 147, 90, 0.35);"
|
|
183
|
+
:title="t('concept.currentEdition')"
|
|
184
|
+
>✦ {{ t('concept.currentEdition') }}</span>
|
|
185
|
+
<span
|
|
186
|
+
v-if="entry.edgeType !== 'self'"
|
|
187
|
+
class="badge text-[9px] flex-shrink-0 badge-gray"
|
|
188
|
+
>{{ edgeLabel(entry) }}</span>
|
|
189
|
+
<span
|
|
190
|
+
v-if="entry.edgeType === 'self'"
|
|
191
|
+
class="badge text-[9px] flex-shrink-0 badge-blue"
|
|
192
|
+
>{{ t('concept.viewing') }}</span>
|
|
193
|
+
</div>
|
|
194
|
+
<div class="flex items-baseline gap-2">
|
|
195
|
+
<span class="font-mono text-xs text-ink-500 dark:text-ink-400 flex-shrink-0">
|
|
196
|
+
{{ entry.member.year ?? '—' }}
|
|
197
|
+
</span>
|
|
198
|
+
<span class="text-sm text-ink-700 dark:text-ink-200 leading-snug truncate">
|
|
199
|
+
{{ entry.member.ref }}
|
|
200
|
+
</span>
|
|
201
|
+
</div>
|
|
202
|
+
<div
|
|
203
|
+
v-if="entry.edgeType !== 'self' || entry.conceptId !== props.conceptId"
|
|
204
|
+
class="font-mono text-[10px] text-ink-300 dark:text-ink-500 mt-0.5 leading-tight"
|
|
205
|
+
>
|
|
206
|
+
{{ entry.member.id }} · {{ entry.conceptId }}
|
|
207
|
+
</div>
|
|
208
|
+
</button>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<div v-if="!hasChain" class="mt-3 pt-3 border-t border-ink-100/60 dark:border-ink-700/40">
|
|
212
|
+
<div class="text-xs text-ink-400 italic">
|
|
213
|
+
{{ t('concept.noChain') }}
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
</template>
|
|
218
|
+
|
|
219
|
+
<style scoped>
|
|
220
|
+
/* No scoped styles — uses global `card`, `section-label`, `badge`, `concept-link` classes
|
|
221
|
+
to match the rest of ConceptDetail's sidebar. */
|
|
222
|
+
</style>
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import type { Concept } from 'glossarist';
|
|
4
|
+
import RdfInstanceHeader from './concept-rdf/RdfInstanceHeader.vue';
|
|
5
|
+
import RdfInstanceSection from './concept-rdf/RdfInstanceSection.vue';
|
|
6
|
+
import RdfSourcePanel from './concept-rdf/RdfSourcePanel.vue';
|
|
7
|
+
import ErrorBoundary from './ErrorBoundary.vue';
|
|
8
|
+
import { useRdfDocument } from './concept-rdf/use-rdf-document';
|
|
5
9
|
|
|
6
10
|
const props = defineProps<{
|
|
7
11
|
concept: Concept;
|
|
@@ -9,389 +13,45 @@ const props = defineProps<{
|
|
|
9
13
|
conceptUriValue: string;
|
|
10
14
|
}>();
|
|
11
15
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
const { sections, turtle, jsonld, typeChain } = useRdfDocument(
|
|
17
|
+
() => props.concept,
|
|
18
|
+
() => props.conceptUriValue,
|
|
19
|
+
);
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
uriCopied.value = true;
|
|
19
|
-
setTimeout(() => { uriCopied.value = false; }, 2000);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function designationClassLabel(type: string): string {
|
|
23
|
-
const map: Record<string, string> = {
|
|
24
|
-
expression: 'gloss:Expression',
|
|
25
|
-
abbreviation: 'gloss:Abbreviation',
|
|
26
|
-
symbol: 'gloss:Symbol',
|
|
27
|
-
letter_symbol: 'gloss:LetterSymbol',
|
|
28
|
-
graphical_symbol: 'gloss:GraphicalSymbol',
|
|
29
|
-
};
|
|
30
|
-
return map[type] ?? 'gloss:Designation';
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function desigSlug(designation: string, index: number): string {
|
|
34
|
-
const slug = designation.replace(/[^a-zA-Z0-9]/g, '_');
|
|
35
|
-
if (/^_+$/.test(slug)) return `d${index}`;
|
|
36
|
-
return slug;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function formatCitation(c: any): string {
|
|
40
|
-
if (!c) return '';
|
|
41
|
-
if (c.source && c.id) return `${c.source} ${c.id}`;
|
|
42
|
-
if (c.ref?.source) {
|
|
43
|
-
const r = c.ref;
|
|
44
|
-
return r.id ? `${r.source} ${r.id}` : r.source;
|
|
45
|
-
}
|
|
46
|
-
return '';
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// ── Instance data extraction ─────────────────────────────────────────────
|
|
50
|
-
|
|
51
|
-
interface PropValue {
|
|
52
|
-
predicate: string;
|
|
53
|
-
values: string[];
|
|
54
|
-
nested?: boolean;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
interface ClassInstance {
|
|
58
|
-
classId: string;
|
|
59
|
-
classLabel: string;
|
|
60
|
-
label: string;
|
|
61
|
-
props: PropValue[];
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function conceptInstance(): ClassInstance {
|
|
65
|
-
const c = props.concept;
|
|
66
|
-
const pv: PropValue[] = [];
|
|
67
|
-
const add = (pred: string, ...vals: string[]) => {
|
|
68
|
-
const filtered = vals.filter(Boolean);
|
|
69
|
-
if (filtered.length) pv.push({ predicate: pred, values: filtered });
|
|
70
|
-
};
|
|
71
|
-
const addNested = (pred: string, ...vals: string[]) => {
|
|
72
|
-
const filtered = vals.filter(Boolean);
|
|
73
|
-
if (filtered.length) pv.push({ predicate: pred, values: filtered, nested: true });
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
add('gloss:identifier', c.id);
|
|
77
|
-
if (c.status) add('gloss:hasStatus', `gloss:status/${c.status}`);
|
|
78
|
-
for (const d of c.domains) addNested('gloss:hasDomain', d.conceptId || d.urn || '');
|
|
79
|
-
for (const s of c.sources) addNested('gloss:hasSource', formatCitation(s.origin));
|
|
80
|
-
for (const d of c.dates) addNested('gloss:hasDate', `${d.type}: ${d.date}`);
|
|
81
|
-
for (const r of c.relatedConcepts) {
|
|
82
|
-
const refLabel = r.content || (r.ref ? `${r.ref.source || ''} ${r.ref.id || ''}`.trim() : '');
|
|
83
|
-
addNested('gloss:hasRelatedConcept', `${r.type}: ${refLabel}`);
|
|
84
|
-
}
|
|
85
|
-
for (const lang of c.languages) addNested('gloss:hasLocalization', `${lang}: ${c.localization(lang)?.primaryDesignation ?? ''}`);
|
|
86
|
-
|
|
87
|
-
return { classId: 'gloss:Concept', classLabel: 'Concept', label: c.id, props: pv };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function localizedInstance(lc: LocalizedConcept): ClassInstance {
|
|
91
|
-
const pv: PropValue[] = [];
|
|
92
|
-
const add = (pred: string, ...vals: string[]) => {
|
|
93
|
-
const filtered = vals.filter(Boolean);
|
|
94
|
-
if (filtered.length) pv.push({ predicate: pred, values: filtered });
|
|
95
|
-
};
|
|
96
|
-
const addNested = (pred: string, ...vals: string[]) => {
|
|
97
|
-
const filtered = vals.filter(Boolean);
|
|
98
|
-
if (filtered.length) pv.push({ predicate: pred, values: filtered, nested: true });
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
add('dcterms:language', lc.languageCode ?? '');
|
|
102
|
-
if (lc.entryStatus) add('gloss:hasEntryStatus', `gloss:entstatus/${lc.entryStatus}`);
|
|
103
|
-
addNested('gloss:isLocalizationOf', props.conceptUriValue);
|
|
104
|
-
for (const d of lc.terms) addNested(d.normativeStatus === 'preferred' ? 'skosxl:prefLabel' : 'skosxl:altLabel', d.designation);
|
|
105
|
-
for (const d of lc.definitions) if (d.content) addNested('gloss:hasDefinition', d.content);
|
|
106
|
-
for (const n of lc.notes) if (n.content) addNested('gloss:hasNote', n.content);
|
|
107
|
-
for (const e of lc.examples) if (e.content) addNested('gloss:hasExample', e.content);
|
|
108
|
-
for (const s of lc.sources) addNested('gloss:hasSource', formatCitation(s.origin));
|
|
109
|
-
if (lc.domain) add('gloss:domain', lc.domain);
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
classId: 'gloss:LocalizedConcept',
|
|
113
|
-
classLabel: 'LocalizedConcept',
|
|
114
|
-
label: `${lc.languageCode}: ${lc.primaryDesignation ?? ''}`,
|
|
115
|
-
props: pv,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function designationInstance(d: Designation): ClassInstance {
|
|
120
|
-
const pv: PropValue[] = [];
|
|
121
|
-
const add = (pred: string, ...vals: string[]) => {
|
|
122
|
-
const filtered = vals.filter(Boolean);
|
|
123
|
-
if (filtered.length) pv.push({ predicate: pred, values: filtered });
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
add('xl:literalForm', `${d.designation}${d.language ? '@' + d.language : ''}`);
|
|
127
|
-
if (d.normativeStatus) add('gloss:normativeStatus', `gloss:norm/${d.normativeStatus}`);
|
|
128
|
-
if (d.geographicalArea) add('gloss:geographicalArea', d.geographicalArea);
|
|
129
|
-
if (d.international) add('gloss:isInternational', 'true');
|
|
130
|
-
if (d.absent) add('gloss:isAbsent', 'true');
|
|
131
|
-
if (d.termType) add('gloss:hasTermType', d.termType);
|
|
132
|
-
for (const p of d.pronunciations ?? []) add('gloss:hasPronunciation', p.content || '');
|
|
133
|
-
|
|
134
|
-
if (d.type === 'expression' || d.type === 'abbreviation') {
|
|
135
|
-
const expr = d as ExpressionType;
|
|
136
|
-
if (expr.prefix) add('gloss:prefix', expr.prefix);
|
|
137
|
-
if (expr.usageInfo) add('gloss:usageInfo', expr.usageInfo);
|
|
138
|
-
if (expr.fieldOfApplication) add('gloss:fieldOfApplication', expr.fieldOfApplication);
|
|
139
|
-
for (const gi of expr.grammarInfo ?? []) {
|
|
140
|
-
const parts: string[] = [];
|
|
141
|
-
if (gi.gender) parts.push(`gender:${gi.gender}`);
|
|
142
|
-
if (gi.number) parts.push(`number:${gi.number}`);
|
|
143
|
-
if (gi.partOfSpeech) parts.push(`pos:${gi.partOfSpeech}`);
|
|
144
|
-
if (parts.length) add('gloss:hasGrammarInfo', parts.join(', '));
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (d.type === 'abbreviation') {
|
|
149
|
-
const abbr = d as AbbreviationType;
|
|
150
|
-
if (abbr.acronym) add('gloss:isAcronym', 'true');
|
|
151
|
-
if (abbr.initialism) add('gloss:isInitialism', 'true');
|
|
152
|
-
if (abbr.truncation) add('gloss:isTruncation', 'true');
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (d.type === 'graphical_symbol') {
|
|
156
|
-
const gs = d as GraphicalSymbolType;
|
|
157
|
-
if (gs.text) add('gloss:text', gs.text);
|
|
158
|
-
if (gs.image) add('gloss:image', gs.image);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return {
|
|
162
|
-
classId: designationClassLabel(d.type),
|
|
163
|
-
classLabel: designationClassLabel(d.type).replace('gloss:', ''),
|
|
164
|
-
label: d.designation,
|
|
165
|
-
props: pv,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// ── Build all sections ──────────────────────────────────────────────
|
|
170
|
-
|
|
171
|
-
const sections = computed<ClassInstance[]>(() => {
|
|
172
|
-
const result: ClassInstance[] = [];
|
|
173
|
-
result.push(conceptInstance());
|
|
174
|
-
|
|
175
|
-
for (const lang of props.concept.languages) {
|
|
176
|
-
const lc = props.concept.localization(lang);
|
|
177
|
-
if (!lc) continue;
|
|
178
|
-
result.push(localizedInstance(lc));
|
|
179
|
-
for (const d of lc.terms) {
|
|
180
|
-
result.push(designationInstance(d));
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return result;
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// ── Type chain for hierarchy ─────────────────────────────────────────
|
|
188
|
-
|
|
189
|
-
const typeChain = computed(() => {
|
|
190
|
-
const conceptCls = getClass('gloss:Concept');
|
|
191
|
-
if (!conceptCls) return ['owl:Thing', 'skos:Concept', 'gloss:Concept'];
|
|
192
|
-
return ['owl:Thing', ...conceptCls.ancestors, 'gloss:Concept'];
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
// ── Turtle source ────────────────────────────────────────────────────
|
|
196
|
-
|
|
197
|
-
const turtleSource = computed(() => {
|
|
198
|
-
const lines: string[] = [];
|
|
199
|
-
const ind = ' ';
|
|
200
|
-
const c = props.concept;
|
|
201
|
-
const uri = props.conceptUriValue;
|
|
202
|
-
|
|
203
|
-
lines.push('@prefix gloss: <https://www.glossarist.org/ontologies/> .');
|
|
204
|
-
lines.push('@prefix skos: <http://www.w3.org/2004/02/skos/core#> .');
|
|
205
|
-
lines.push('@prefix xl: <http://www.w3.org/2008/05/skos-xl#> .');
|
|
206
|
-
lines.push('@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .');
|
|
207
|
-
lines.push('@prefix dcterms: <http://purl.org/dc/terms/> .');
|
|
208
|
-
lines.push('');
|
|
209
|
-
|
|
210
|
-
lines.push(`<${uri}> a gloss:Concept, skos:Concept ;`);
|
|
211
|
-
lines.push(`${ind}gloss:identifier "${c.id}" ;`);
|
|
212
|
-
if (c.status) lines.push(`${ind}gloss:hasStatus gloss:status/${c.status} ;`);
|
|
213
|
-
for (const lang of c.languages) lines.push(`${ind}gloss:hasLocalization <${uri}/${lang}> ;`);
|
|
214
|
-
for (const r of c.relatedConcepts) {
|
|
215
|
-
lines.push(`${ind}gloss:hasRelatedConcept [`);
|
|
216
|
-
lines.push(`${ind}${ind}gloss:relationshipType gloss:rel/${r.type} ;`);
|
|
217
|
-
if (r.content) lines.push(`${ind}${ind}gloss:relationshipContent "${r.content}" ;`);
|
|
218
|
-
if (r.ref) {
|
|
219
|
-
if (r.ref.source) lines.push(`${ind}${ind}gloss:conceptSource "${r.ref.source}" ;`);
|
|
220
|
-
if (r.ref.id) lines.push(`${ind}${ind}gloss:conceptId "${r.ref.id}" ;`);
|
|
221
|
-
}
|
|
222
|
-
lines.push(`${ind}] ;`);
|
|
223
|
-
}
|
|
224
|
-
lines[lines.length - 1] = lines[lines.length - 1].replace(/ ;$/, ' .');
|
|
225
|
-
|
|
226
|
-
for (const lang of c.languages) {
|
|
227
|
-
const lc = c.localization(lang);
|
|
228
|
-
if (!lc) continue;
|
|
229
|
-
lines.push('');
|
|
230
|
-
lines.push(`<${uri}/${lang}> a gloss:LocalizedConcept, skos:Concept ;`);
|
|
231
|
-
lines.push(`${ind}dcterms:language "${lang}" ;`);
|
|
232
|
-
lines.push(`${ind}gloss:isLocalizationOf <${uri}> ;`);
|
|
233
|
-
if (lc.entryStatus) lines.push(`${ind}gloss:hasEntryStatus gloss:entstatus/${lc.entryStatus} ;`);
|
|
234
|
-
for (let di = 0; di < lc.terms.length; di++) {
|
|
235
|
-
const d = lc.terms[di];
|
|
236
|
-
const normPrefix = d.normativeStatus === 'preferred' ? 'skosxl:prefLabel' : 'skosxl:altLabel';
|
|
237
|
-
lines.push(`${ind}${normPrefix} <${uri}/${lang}/desig/${desigSlug(d.designation, di)}> ;`);
|
|
238
|
-
}
|
|
239
|
-
for (const def of lc.definitions) {
|
|
240
|
-
if (def.content) lines.push(`${ind}gloss:hasDefinition [ rdf:value "${def.content}" ] ;`);
|
|
241
|
-
}
|
|
242
|
-
lines[lines.length - 1] = lines[lines.length - 1].replace(/ ;$/, ' .');
|
|
243
|
-
|
|
244
|
-
for (let di = 0; di < lc.terms.length; di++) {
|
|
245
|
-
const d = lc.terms[di];
|
|
246
|
-
const desigUri = `${uri}/${lang}/desig/${desigSlug(d.designation, di)}`;
|
|
247
|
-
const dc = designationClassLabel(d.type);
|
|
248
|
-
lines.push('');
|
|
249
|
-
lines.push(`<${desigUri}> a ${dc}, xl:Label ;`);
|
|
250
|
-
lines.push(`${ind}xl:literalForm "${d.designation}"@${lang} ;`);
|
|
251
|
-
if (d.normativeStatus) lines.push(`${ind}gloss:normativeStatus gloss:norm/${d.normativeStatus} ;`);
|
|
252
|
-
lines[lines.length - 1] = lines[lines.length - 1].replace(/ ;$/, ' .');
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return lines.join('\n');
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
// ── JSON-LD source ──────────────────────────────────────────────────
|
|
260
|
-
|
|
261
|
-
const jsonldSource = computed(() => {
|
|
262
|
-
const c = props.concept;
|
|
263
|
-
const uri = props.conceptUriValue;
|
|
264
|
-
|
|
265
|
-
const doc: any = {
|
|
266
|
-
'@context': {
|
|
267
|
-
gloss: 'https://www.glossarist.org/ontologies/',
|
|
268
|
-
skos: 'http://www.w3.org/2004/02/skos/core#',
|
|
269
|
-
xl: 'http://www.w3.org/2008/05/skos-xl#',
|
|
270
|
-
dcterms: 'http://purl.org/dc/terms/',
|
|
271
|
-
},
|
|
272
|
-
'@graph': [],
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
const conceptNode: any = {
|
|
276
|
-
'@id': uri,
|
|
277
|
-
'@type': ['gloss:Concept', 'skos:Concept'],
|
|
278
|
-
'gloss:identifier': c.id,
|
|
279
|
-
};
|
|
280
|
-
if (c.status) conceptNode['gloss:hasStatus'] = { '@id': `gloss:status/${c.status}` };
|
|
281
|
-
conceptNode['gloss:hasLocalization'] = c.languages.map(l => ({ '@id': `${uri}/${l}` }));
|
|
282
|
-
doc['@graph'].push(conceptNode);
|
|
283
|
-
|
|
284
|
-
for (const lang of c.languages) {
|
|
285
|
-
const lc = c.localization(lang);
|
|
286
|
-
if (!lc) continue;
|
|
287
|
-
const lcNode: any = {
|
|
288
|
-
'@id': `${uri}/${lang}`,
|
|
289
|
-
'@type': ['gloss:LocalizedConcept', 'skos:Concept'],
|
|
290
|
-
'dcterms:language': lang,
|
|
291
|
-
'gloss:isLocalizationOf': { '@id': uri },
|
|
292
|
-
};
|
|
293
|
-
if (lc.entryStatus) lcNode['gloss:hasEntryStatus'] = { '@id': `gloss:entstatus/${lc.entryStatus}` };
|
|
294
|
-
for (let di = 0; di < lc.terms.length; di++) {
|
|
295
|
-
const d = lc.terms[di];
|
|
296
|
-
const key = d.normativeStatus === 'preferred' ? 'skosxl:prefLabel' : 'skosxl:altLabel';
|
|
297
|
-
lcNode[key] = lcNode[key] || [];
|
|
298
|
-
lcNode[key].push({ '@id': `${uri}/${lang}/desig/${desigSlug(d.designation, di)}` });
|
|
299
|
-
}
|
|
300
|
-
doc['@graph'].push(lcNode);
|
|
301
|
-
|
|
302
|
-
for (let di = 0; di < lc.terms.length; di++) {
|
|
303
|
-
const d = lc.terms[di];
|
|
304
|
-
const desigUri = `${uri}/${lang}/desig/${desigSlug(d.designation, di)}`;
|
|
305
|
-
const desigNode: any = {
|
|
306
|
-
'@id': desigUri,
|
|
307
|
-
'@type': [designationClassLabel(d.type), 'xl:Label'],
|
|
308
|
-
'xl:literalForm': { '@value': d.designation, '@language': lang },
|
|
309
|
-
};
|
|
310
|
-
if (d.normativeStatus) desigNode['gloss:normativeStatus'] = { '@id': `gloss:norm/${d.normativeStatus}` };
|
|
311
|
-
doc['@graph'].push(desigNode);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
return JSON.stringify(doc, null, 2);
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
const rdfSource = computed(() => rdfFormat.value === 'turtle' ? turtleSource.value : jsonldSource.value);
|
|
21
|
+
const resourceCount = computed(() => sections.value.length);
|
|
22
|
+
const conceptId = computed(() => props.concept.id);
|
|
319
23
|
</script>
|
|
320
24
|
|
|
321
25
|
<template>
|
|
322
|
-
<
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
<div class="text-[10px] uppercase tracking-widest text-ink-300 font-medium mb-2">RDF Instance</div>
|
|
328
|
-
<div class="flex items-center gap-2 flex-wrap">
|
|
329
|
-
<code class="text-sm font-mono text-ink-700 break-all">{{ conceptUriValue }}</code>
|
|
330
|
-
<button @click="copyUri" class="p-1.5 rounded text-ink-300 hover:text-ink-600 hover:bg-ink-50 transition-colors flex-shrink-0" :title="uriCopied ? 'Copied!' : 'Copy URI'">
|
|
331
|
-
<svg v-if="!uriCopied" class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10a2 2 0 01-2-2v-1m6 4v-3a2 2 0 00-2-2H8"/></svg>
|
|
332
|
-
<svg v-else class="w-3.5 h-3.5 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
|
333
|
-
</button>
|
|
334
|
-
</div>
|
|
335
|
-
<div class="flex gap-1.5 mt-2.5">
|
|
336
|
-
<router-link to="/ontology/class/gloss-Concept" class="inline-flex items-center px-2 py-0.5 rounded text-[11px] font-medium bg-blue-50 text-blue-700 border border-blue-100 hover:bg-blue-100 transition-colors">gloss:Concept</router-link>
|
|
337
|
-
<router-link to="/ontology/class/gloss-Concept" class="inline-flex items-center px-2 py-0.5 rounded text-[11px] font-medium bg-emerald-50 text-emerald-700 border border-emerald-100 hover:bg-emerald-100 transition-colors">skos:Concept</router-link>
|
|
338
|
-
</div>
|
|
339
|
-
</div>
|
|
340
|
-
</div>
|
|
341
|
-
|
|
342
|
-
<!-- Mini hierarchy -->
|
|
343
|
-
<div class="mt-4 pt-3 border-t border-ink-100/60">
|
|
344
|
-
<div class="flex items-center gap-1.5 flex-wrap text-xs text-ink-400">
|
|
345
|
-
<template v-for="(t, i) in typeChain" :key="i">
|
|
346
|
-
<span v-if="i > 0" class="text-ink-200 mx-0.5">→</span>
|
|
347
|
-
<code class="text-[11px] text-ink-400">{{ t }}</code>
|
|
348
|
-
</template>
|
|
349
|
-
<span class="text-ink-200 mx-0.5">→</span>
|
|
350
|
-
<code class="text-[11px] text-ink-700 font-semibold bg-ink-50 px-1.5 py-0.5 rounded">{{ concept.id }}</code>
|
|
26
|
+
<ErrorBoundary title="RDF view failed" :retry-key="conceptUriValue">
|
|
27
|
+
<div class="space-y-6">
|
|
28
|
+
<div class="card p-5">
|
|
29
|
+
<div class="flex items-start justify-between gap-3">
|
|
30
|
+
<RdfInstanceHeader :uri="conceptUriValue" :concept-id="conceptId" />
|
|
351
31
|
</div>
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
<div class="flex items-center gap-2 mb-3">
|
|
358
|
-
<div class="w-1 h-4 rounded-full" :class="section.classId === 'gloss:Concept' ? 'bg-blue-500' : section.classId === 'gloss:LocalizedConcept' ? 'bg-emerald-500' : 'bg-amber-500'"></div>
|
|
359
|
-
<router-link :to="`/ontology/class/${section.classId.replace(/:/g, '-')}`" class="text-xs font-semibold text-ink-700 hover:text-blue-600 transition-colors">{{ section.classId }}</router-link>
|
|
360
|
-
<span class="text-xs text-ink-400">·</span>
|
|
361
|
-
<span class="text-xs text-ink-500">{{ section.label }}</span>
|
|
362
|
-
</div>
|
|
363
|
-
|
|
364
|
-
<div class="space-y-1.5">
|
|
365
|
-
<div v-for="prop in section.props" :key="prop.predicate" class="grid grid-cols-[160px_1fr] gap-x-3 gap-y-0.5 py-1.5 border-b border-ink-100/30 last:border-0">
|
|
366
|
-
<code class="text-xs text-blue-600 font-medium leading-relaxed self-start pt-0.5">{{ prop.predicate }}</code>
|
|
367
|
-
<div class="flex flex-col gap-0.5">
|
|
368
|
-
<template v-for="(val, vi) in prop.values" :key="vi">
|
|
369
|
-
<span v-if="prop.nested" class="text-xs text-ink-600 bg-ink-50/60 px-2 py-1 rounded border-l-2 border-ink-200 leading-relaxed break-words">{{ val }}</span>
|
|
370
|
-
<span v-else class="text-xs text-ink-600 leading-relaxed break-words">{{ val }}</span>
|
|
32
|
+
<div class="mt-4 pt-3 border-t border-ink-100/60">
|
|
33
|
+
<div class="flex items-center gap-1.5 flex-wrap text-xs text-ink-400">
|
|
34
|
+
<template v-for="(t, i) in typeChain" :key="i">
|
|
35
|
+
<span v-if="i > 0" class="text-ink-200 mx-0.5">→</span>
|
|
36
|
+
<code class="text-[11px] text-ink-400">{{ t }}</code>
|
|
371
37
|
</template>
|
|
38
|
+
<span class="text-ink-200 mx-0.5">→</span>
|
|
39
|
+
<code class="text-[11px] text-ink-700 font-semibold bg-ink-50 px-1.5 py-0.5 rounded">{{ concept.id }}</code>
|
|
372
40
|
</div>
|
|
373
41
|
</div>
|
|
374
42
|
</div>
|
|
375
|
-
</div>
|
|
376
43
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
</select>
|
|
389
|
-
<span class="text-[10px] text-ink-300">{{ sections.length }} resources</span>
|
|
390
|
-
</div>
|
|
391
|
-
</button>
|
|
392
|
-
<div v-if="showSource" class="border-t border-ink-100/60">
|
|
393
|
-
<pre class="p-4 text-xs font-mono text-ink-700 bg-ink-50/30 overflow-x-auto leading-relaxed max-h-[600px] overflow-y-auto">{{ rdfSource }}</pre>
|
|
394
|
-
</div>
|
|
44
|
+
<RdfInstanceSection
|
|
45
|
+
v-for="(section, si) in sections"
|
|
46
|
+
:key="si"
|
|
47
|
+
:section="section"
|
|
48
|
+
/>
|
|
49
|
+
|
|
50
|
+
<RdfSourcePanel
|
|
51
|
+
:turtle="turtle"
|
|
52
|
+
:jsonld="jsonld"
|
|
53
|
+
:resource-count="resourceCount"
|
|
54
|
+
/>
|
|
395
55
|
</div>
|
|
396
|
-
</
|
|
56
|
+
</ErrorBoundary>
|
|
397
57
|
</template>
|