@glossarist/concept-browser 0.7.77 → 0.7.80
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/astro.config.mjs +1 -0
- package/package.json +2 -2
- package/scripts/bridge-to-astro.mjs +6 -1
- package/scripts/build-edges.js +154 -1
- package/scripts/generate-data.mjs +138 -54
- package/src/__tests__/build-edges-redundancy.test.ts +101 -0
- package/src/__tests__/composables/use-concept-diff.test.ts +240 -0
- package/src/__tests__/composables/use-concept-edges-v2.test.ts +132 -0
- package/src/__tests__/composables/use-dataset-stats.test.ts +96 -0
- package/src/__tests__/config/page-types.test.ts +82 -0
- package/src/__tests__/model-bridge-bridges.test.ts +3 -2
- package/src/__tests__/model-bridge-citation.test.ts +1 -1
- package/src/__tests__/ontology-registry.test.ts +2 -2
- package/src/__tests__/partitive-relation-styling.test.ts +82 -0
- package/src/__tests__/partitive-relation.test.ts +285 -0
- package/src/__tests__/sphere-math.test.ts +56 -0
- package/src/adapters/GraphDataSource.ts +22 -5
- package/src/adapters/UriRouter.ts +5 -0
- package/src/adapters/dataset-stats.ts +13 -0
- package/src/adapters/model-bridge.ts +1 -1
- package/src/adapters/non-verbal/glossarist-augment.d.ts +155 -4
- package/src/adapters/types.ts +36 -0
- package/src/components/AppSidebar.vue +14 -57
- package/src/components/ConceptDetail.vue +29 -0
- package/src/components/ConceptDiffModal.vue +120 -0
- package/src/components/ConceptDiffView.vue +192 -0
- package/src/components/ConceptEditionRail.vue +61 -36
- package/src/components/PartitiveRelationList.vue +139 -0
- package/src/components/RelationSphere.vue +2 -36
- package/src/components/groups/DatasetGroupRenderer.vue +14 -4
- package/src/components/groups/DefaultGroupSidebar.vue +28 -11
- package/src/components/groups/LineageGroupSidebar.vue +17 -29
- package/src/composables/use-concept-diff.ts +231 -0
- package/src/composables/use-concept-edges.ts +98 -5
- package/src/composables/use-dataset-stats.ts +52 -0
- package/src/config/page-types.ts +67 -0
- package/src/config/types.ts +6 -1
- package/src/config/use-site-config.ts +13 -29
- package/src/data/taxonomies.json +24 -1
- package/src/i18n/locales/eng.yml +6 -0
- package/src/utils/partitive-relation-styling.ts +62 -0
- package/src/utils/sphere-math.ts +54 -0
- package/src/views/DatasetView.vue +5 -3
- package/src/views/StatsView.vue +9 -22
- package/src/islands/BibliographyIsland.vue +0 -26
- package/src/islands/ConceptDetailIsland.vue +0 -27
- package/src/islands/ConceptDiffIsland.vue +0 -214
- package/src/islands/ConceptIsland.vue +0 -102
- package/src/islands/GraphIsland.vue +0 -15
- package/src/islands/GroupRendererIsland.vue +0 -15
- package/src/islands/HeaderIsland.vue +0 -10
- package/src/islands/NonVerbalIsland.vue +0 -75
- package/src/islands/RelationSphereIsland.vue +0 -20
- package/src/islands/SearchIsland.vue +0 -23
- package/src/islands/SeriesCardIsland.vue +0 -12
- package/src/islands/ShortcutsIsland.vue +0 -13
- package/src/islands/SidebarIsland.vue +0 -15
- package/src/islands/SupersessionDiffIsland.vue +0 -267
|
@@ -10,14 +10,15 @@
|
|
|
10
10
|
// This file declares the runtime shape so consumer code can be type-checked.
|
|
11
11
|
// DELETE this file when upstream ships proper declarations — tracked by
|
|
12
12
|
// PR glossarist/glossarist-js#31 (targets v0.4.3+).
|
|
13
|
+
//
|
|
14
|
+
// v2 PartitiveRelation block (below) tracks glossarist 0.4.20 — the runtime
|
|
15
|
+
// exports PartitiveRelation/PartitiveMember/TypeSharedPlurality from
|
|
16
|
+
// src/models/index.js, but the d.ts has not been updated. Remove the v2
|
|
17
|
+
// block when upstream PRs the d.ts changes.
|
|
13
18
|
|
|
14
19
|
import type { ConceptSource, Citation, GlossaristModel } from 'glossarist';
|
|
15
20
|
|
|
16
21
|
declare module 'glossarist' {
|
|
17
|
-
interface ConceptSource {
|
|
18
|
-
sourced_from?: Citation[];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
22
|
class RegistrableModel extends GlossaristModel {
|
|
22
23
|
static register(type: string, cls: typeof RegistrableModel): void;
|
|
23
24
|
static fromData(data: Record<string, unknown>): RegistrableModel;
|
|
@@ -134,4 +135,154 @@ declare module 'glossarist' {
|
|
|
134
135
|
static fromYAML(yamlString: string): BibliographyData;
|
|
135
136
|
static fromJSON(data: Record<string, unknown>): BibliographyData;
|
|
136
137
|
}
|
|
138
|
+
|
|
139
|
+
// ── v2 PartitiveRelation (glossarist 0.4.20) ────────────────────────────
|
|
140
|
+
// Runtime exports these from src/models/index.js; d.ts is stale.
|
|
141
|
+
|
|
142
|
+
type Completeness = 'complete' | 'partial';
|
|
143
|
+
const COMPLETENESS: { readonly COMPLETE: 'complete'; readonly PARTIAL: 'partial' };
|
|
144
|
+
const COMPLETENESS_VALUES: readonly Completeness[];
|
|
145
|
+
const DEFAULT_COMPLETENESS: Completeness;
|
|
146
|
+
function isValidCompleteness(value: unknown): value is Completeness;
|
|
147
|
+
|
|
148
|
+
type MemberCertainty = 'confirmed' | 'possible';
|
|
149
|
+
const MEMBER_CERTAINTY: { readonly CONFIRMED: 'confirmed'; readonly POSSIBLE: 'possible' };
|
|
150
|
+
const MEMBER_CERTAINTY_VALUES: readonly MemberCertainty[];
|
|
151
|
+
const DEFAULT_MEMBER_CERTAINTY: MemberCertainty;
|
|
152
|
+
function isValidMemberCertainty(value: unknown): value is MemberCertainty;
|
|
153
|
+
|
|
154
|
+
class TypeSharedPlurality extends GlossaristModel {
|
|
155
|
+
constructor(data?: {
|
|
156
|
+
isShared?: boolean;
|
|
157
|
+
is_shared?: boolean;
|
|
158
|
+
isUncertain?: boolean;
|
|
159
|
+
is_uncertain?: boolean;
|
|
160
|
+
sharedType?: ConceptRef;
|
|
161
|
+
shared_type?: ConceptRef;
|
|
162
|
+
});
|
|
163
|
+
readonly isShared: boolean;
|
|
164
|
+
readonly isUncertain: boolean;
|
|
165
|
+
readonly sharedType: ConceptRef | null;
|
|
166
|
+
hasSharedType(): boolean;
|
|
167
|
+
toJSON(): { is_shared: boolean; is_uncertain?: boolean; shared_type?: ReturnType<ConceptRef['toJSON']> };
|
|
168
|
+
static fromJSON(data: Record<string, unknown>): TypeSharedPlurality;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
class PartitiveMember extends GlossaristModel {
|
|
172
|
+
constructor(data?: { ref?: ConceptRef; certainty?: MemberCertainty });
|
|
173
|
+
readonly ref: ConceptRef;
|
|
174
|
+
readonly certainty: MemberCertainty;
|
|
175
|
+
readonly isConfirmed: boolean;
|
|
176
|
+
readonly isPossible: boolean;
|
|
177
|
+
toJSON(): { ref: ReturnType<ConceptRef['toJSON']>; certainty?: MemberCertainty };
|
|
178
|
+
static fromJSON(data: Record<string, unknown>): PartitiveMember;
|
|
179
|
+
static identityOf(value: unknown): string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
class PartitiveRelation extends GlossaristModel {
|
|
183
|
+
constructor(data?: {
|
|
184
|
+
comprehensive?: ConceptRef;
|
|
185
|
+
partitives?: PartitiveMember[];
|
|
186
|
+
completeness?: Completeness;
|
|
187
|
+
plurality?: TypeSharedPlurality;
|
|
188
|
+
criterion?: Record<string, string> | string;
|
|
189
|
+
});
|
|
190
|
+
readonly comprehensive: ConceptRef;
|
|
191
|
+
readonly partitives: PartitiveMember[];
|
|
192
|
+
readonly completeness: Completeness;
|
|
193
|
+
readonly plurality: TypeSharedPlurality | null;
|
|
194
|
+
readonly criterion: Record<string, string> | null;
|
|
195
|
+
readonly isComplete: boolean;
|
|
196
|
+
readonly isPartial: boolean;
|
|
197
|
+
readonly isCoordinate: boolean;
|
|
198
|
+
hasPlurality(): boolean;
|
|
199
|
+
hasCriterion(): boolean;
|
|
200
|
+
toJSON(): {
|
|
201
|
+
comprehensive: ReturnType<ConceptRef['toJSON']>;
|
|
202
|
+
partitives: ReturnType<PartitiveMember['toJSON']>[];
|
|
203
|
+
completeness: Completeness;
|
|
204
|
+
plurality?: ReturnType<TypeSharedPlurality['toJSON']>;
|
|
205
|
+
criterion?: Record<string, string>;
|
|
206
|
+
};
|
|
207
|
+
static fromJSON(data: Record<string, unknown>): PartitiveRelation;
|
|
208
|
+
static identityOf(value: unknown): string;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
interface Concept {
|
|
212
|
+
readonly partitiveRelations: PartitiveRelation[];
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Re-declare the same exports under the 'glossarist/models' subpath so
|
|
217
|
+
// deep-import callers can use them. Upstream's d.ts is stale on this
|
|
218
|
+
// subpath too.
|
|
219
|
+
declare module 'glossarist/models' {
|
|
220
|
+
export type Completeness = 'complete' | 'partial';
|
|
221
|
+
export const COMPLETENESS: { readonly COMPLETE: 'complete'; readonly PARTIAL: 'partial' };
|
|
222
|
+
export const COMPLETENESS_VALUES: readonly Completeness[];
|
|
223
|
+
export const DEFAULT_COMPLETENESS: Completeness;
|
|
224
|
+
export function isValidCompleteness(value: unknown): value is Completeness;
|
|
225
|
+
|
|
226
|
+
export type MemberCertainty = 'confirmed' | 'possible';
|
|
227
|
+
export const MEMBER_CERTAINTY: { readonly CONFIRMED: 'confirmed'; readonly POSSIBLE: 'possible' };
|
|
228
|
+
export const MEMBER_CERTAINTY_VALUES: readonly MemberCertainty[];
|
|
229
|
+
export const DEFAULT_MEMBER_CERTAINTY: MemberCertainty;
|
|
230
|
+
export function isValidMemberCertainty(value: unknown): value is MemberCertainty;
|
|
231
|
+
|
|
232
|
+
export class TypeSharedPlurality extends GlossaristModel {
|
|
233
|
+
constructor(data?: {
|
|
234
|
+
isShared?: boolean;
|
|
235
|
+
is_shared?: boolean;
|
|
236
|
+
isUncertain?: boolean;
|
|
237
|
+
is_uncertain?: boolean;
|
|
238
|
+
sharedType?: ConceptRef;
|
|
239
|
+
shared_type?: ConceptRef;
|
|
240
|
+
});
|
|
241
|
+
readonly isShared: boolean;
|
|
242
|
+
readonly isUncertain: boolean;
|
|
243
|
+
readonly sharedType: ConceptRef | null;
|
|
244
|
+
hasSharedType(): boolean;
|
|
245
|
+
toJSON(): { is_shared: boolean; is_uncertain?: boolean; shared_type?: ReturnType<ConceptRef['toJSON']> };
|
|
246
|
+
static fromJSON(data: Record<string, unknown>): TypeSharedPlurality;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export class PartitiveMember extends GlossaristModel {
|
|
250
|
+
constructor(data?: { ref?: ConceptRef; certainty?: MemberCertainty });
|
|
251
|
+
readonly ref: ConceptRef;
|
|
252
|
+
readonly certainty: MemberCertainty;
|
|
253
|
+
readonly isConfirmed: boolean;
|
|
254
|
+
readonly isPossible: boolean;
|
|
255
|
+
toJSON(): { ref: ReturnType<ConceptRef['toJSON']>; certainty?: MemberCertainty };
|
|
256
|
+
static fromJSON(data: Record<string, unknown>): PartitiveMember;
|
|
257
|
+
static identityOf(value: unknown): string;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export class PartitiveRelation extends GlossaristModel {
|
|
261
|
+
constructor(data?: {
|
|
262
|
+
comprehensive?: ConceptRef;
|
|
263
|
+
partitives?: PartitiveMember[];
|
|
264
|
+
completeness?: Completeness;
|
|
265
|
+
plurality?: TypeSharedPlurality;
|
|
266
|
+
criterion?: Record<string, string> | string;
|
|
267
|
+
});
|
|
268
|
+
readonly comprehensive: ConceptRef;
|
|
269
|
+
readonly partitives: PartitiveMember[];
|
|
270
|
+
readonly completeness: Completeness;
|
|
271
|
+
readonly plurality: TypeSharedPlurality | null;
|
|
272
|
+
readonly criterion: Record<string, string> | null;
|
|
273
|
+
readonly isComplete: boolean;
|
|
274
|
+
readonly isPartial: boolean;
|
|
275
|
+
readonly isCoordinate: boolean;
|
|
276
|
+
hasPlurality(): boolean;
|
|
277
|
+
hasCriterion(): boolean;
|
|
278
|
+
toJSON(): {
|
|
279
|
+
comprehensive: ReturnType<ConceptRef['toJSON']>;
|
|
280
|
+
partitives: ReturnType<PartitiveMember['toJSON']>[];
|
|
281
|
+
completeness: Completeness;
|
|
282
|
+
plurality?: ReturnType<TypeSharedPlurality['toJSON']>;
|
|
283
|
+
criterion?: Record<string, string>;
|
|
284
|
+
};
|
|
285
|
+
static fromJSON(data: Record<string, unknown>): PartitiveRelation;
|
|
286
|
+
static identityOf(value: unknown): string;
|
|
287
|
+
}
|
|
137
288
|
}
|
package/src/adapters/types.ts
CHANGED
|
@@ -148,6 +148,42 @@ export interface GraphEdge {
|
|
|
148
148
|
lang?: string;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Wire-shape projection of a PartitiveRelation — used by:
|
|
153
|
+
* - build-edges.js output (partitive_relations.json)
|
|
154
|
+
* - GraphDataSource (loads the wire file for tools/future consumers)
|
|
155
|
+
* - use-concept-edges composable (projects the glossarist-js model
|
|
156
|
+
* into this shape for display, resolving ConceptRef → URI)
|
|
157
|
+
*
|
|
158
|
+
* The glossarist-js `PartitiveRelation` class is the model SSOT
|
|
159
|
+
* (ConceptRef-based). This interface is the *resolved-for-display*
|
|
160
|
+
* shape — keep the two concerns separate.
|
|
161
|
+
*
|
|
162
|
+
* Renamed from `PartitiveRelation` to avoid collision with the
|
|
163
|
+
* upstream model class. v2 shape per concept-model
|
|
164
|
+
* TODO.partitive-relation-v2.
|
|
165
|
+
*/
|
|
166
|
+
export interface PartitiveRelationWire {
|
|
167
|
+
source: string;
|
|
168
|
+
comprehensive: string;
|
|
169
|
+
partitives: PartitiveMemberWire[];
|
|
170
|
+
completeness: 'complete' | 'partial';
|
|
171
|
+
plurality: TypeSharedPluralityWire | null;
|
|
172
|
+
criterion?: Record<string, string>;
|
|
173
|
+
register: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface PartitiveMemberWire {
|
|
177
|
+
uri: string;
|
|
178
|
+
certainty: 'confirmed' | 'possible';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface TypeSharedPluralityWire {
|
|
182
|
+
isShared: boolean;
|
|
183
|
+
isUncertain: boolean;
|
|
184
|
+
sharedType?: string | null;
|
|
185
|
+
}
|
|
186
|
+
|
|
151
187
|
export interface GraphNode {
|
|
152
188
|
uri: string;
|
|
153
189
|
register: string;
|
|
@@ -6,6 +6,7 @@ import { useRoute, useRouter } from 'vue-router';
|
|
|
6
6
|
import { useDsStyle } from '../utils/dataset-style';
|
|
7
7
|
import { useSiteConfig } from '../config/use-site-config';
|
|
8
8
|
import NavIcon from './NavIcon.vue';
|
|
9
|
+
import DatasetGroupRenderer from './groups/DatasetGroupRenderer.vue';
|
|
9
10
|
import { useI18n, locale } from '../i18n';
|
|
10
11
|
import type { SectionNode } from '../adapters/types';
|
|
11
12
|
import { toSectionTree } from '../utils/section-tree';
|
|
@@ -284,61 +285,17 @@ const activeSectionId = computed(() => {
|
|
|
284
285
|
<span class="text-[9px] uppercase tracking-wide text-ink-300 dark:text-ink-500 font-sans">{{ groupTypeMeta(group).label }}</span>
|
|
285
286
|
</button>
|
|
286
287
|
|
|
287
|
-
<!-- Group entries
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
<
|
|
298
|
-
v-if="group.kind === 'lineage'"
|
|
299
|
-
@click="goToDataset(ds.id)"
|
|
300
|
-
class="series-entry w-full text-left flex items-center gap-2 pl-6 pr-3 py-1.5 rounded-md text-sm border-l-2 transition-all duration-150"
|
|
301
|
-
:class="currentDataset === ds.id
|
|
302
|
-
? 'bg-amber-50/70 dark:bg-amber-400/10 border-l-[3px] text-ink-900 dark:text-ink-50 font-semibold'
|
|
303
|
-
: 'border-transparent text-ink-600 dark:text-ink-300 hover:bg-ink-50 dark:hover:bg-ink-700/40 hover:text-ink-900 dark:hover:text-ink-50'"
|
|
304
|
-
:style="currentDataset === ds.id ? { borderLeftColor: 'var(--gold-accent, #B8935A)' } : {}"
|
|
305
|
-
>
|
|
306
|
-
<span class="flex-1 truncate text-[13.5px] font-medium leading-snug">{{ ds.ref || ds.title || ds.id }}</span>
|
|
307
|
-
<span
|
|
308
|
-
v-if="ds.status && ds.status !== 'valid'"
|
|
309
|
-
class="text-[9px] uppercase tracking-wide italic text-ink-400 dark:text-ink-400"
|
|
310
|
-
>{{ ds.status }}</span>
|
|
311
|
-
<span
|
|
312
|
-
v-if="ds.isCurrent"
|
|
313
|
-
class="current-star flex-shrink-0"
|
|
314
|
-
title="Current edition"
|
|
315
|
-
>✦</span>
|
|
316
|
-
</button>
|
|
317
|
-
|
|
318
|
-
<!-- DEFAULT/OTHER: standard entry button -->
|
|
319
|
-
<button
|
|
320
|
-
v-else
|
|
321
|
-
@click="goToDataset(ds.id)"
|
|
322
|
-
class="w-full text-left px-3 py-2 rounded-lg text-sm border-l-2 flex items-start gap-2"
|
|
323
|
-
:class="[
|
|
324
|
-
currentDataset === ds.id
|
|
325
|
-
? 'text-ink-800 dark:text-ink-50'
|
|
326
|
-
: 'border-transparent text-ink-600 dark:text-ink-300 hover:bg-ink-50 dark:hover:bg-ink-700 hover:text-ink-800 dark:hover:text-ink-50'
|
|
327
|
-
]"
|
|
328
|
-
:style="{ borderLeftColor: currentDataset === ds.id ? getColor(ds.id) : 'transparent' }"
|
|
329
|
-
>
|
|
330
|
-
<span class="w-2 h-2 rounded-full flex-shrink-0 mt-1.5" :style="{ backgroundColor: getColor(ds.id) }"></span>
|
|
331
|
-
<div class="min-w-0 flex-1">
|
|
332
|
-
<div class="font-medium truncate leading-snug">{{ localizedDatasetField(ds.id, 'title', ds.title) }}</div>
|
|
333
|
-
<div v-if="ds.loaded" class="text-xs mt-0.5" :class="currentDataset === ds.id ? 'text-ink-400 dark:text-ink-300' : 'text-ink-300 dark:text-ink-400'">
|
|
334
|
-
{{ ds.conceptCount.toLocaleString() }} {{ t('home.concepts').toLowerCase() }}
|
|
335
|
-
</div>
|
|
336
|
-
</div>
|
|
337
|
-
</button>
|
|
338
|
-
|
|
339
|
-
<!-- SHARED expansion content: sub-pages + sections + provenance.
|
|
340
|
-
Appears for the active dataset in ALL group kinds (DRY). -->
|
|
341
|
-
<div v-if="currentDataset === ds.id && (filteredDatasetPages.length || provenance.owner)" class="px-2 pb-2">
|
|
288
|
+
<!-- Group entries: dispatch to kind-specific renderer (OCP).
|
|
289
|
+
Shared expansion content lives in the `expanded` slot so
|
|
290
|
+
it stays identical across kinds (DRY/MECE). -->
|
|
291
|
+
<DatasetGroupRenderer
|
|
292
|
+
v-if="isGroupExpanded(group.id)"
|
|
293
|
+
:kind="group.kind"
|
|
294
|
+
:entries="group.entries"
|
|
295
|
+
:current-dataset="currentDataset"
|
|
296
|
+
>
|
|
297
|
+
<template #expanded="{ entry: ds, isCurrent }">
|
|
298
|
+
<div v-if="isCurrent && (filteredDatasetPages.length || provenance.owner)" class="px-2 pb-2">
|
|
342
299
|
<nav v-if="filteredDatasetPages.length" class="space-y-0.5 mt-1">
|
|
343
300
|
<router-link
|
|
344
301
|
v-for="page in filteredDatasetPages"
|
|
@@ -410,8 +367,8 @@ const activeSectionId = computed(() => {
|
|
|
410
367
|
</div>
|
|
411
368
|
</div>
|
|
412
369
|
</div>
|
|
413
|
-
</
|
|
414
|
-
</
|
|
370
|
+
</template>
|
|
371
|
+
</DatasetGroupRenderer>
|
|
415
372
|
</div>
|
|
416
373
|
</template>
|
|
417
374
|
|
|
@@ -27,6 +27,8 @@ import FormatDownloads from './FormatDownloads.vue';
|
|
|
27
27
|
import ConceptEditionRail from './ConceptEditionRail.vue';
|
|
28
28
|
import NonVerbalRepDisplay from './NonVerbalRepDisplay.vue';
|
|
29
29
|
import NonVerbalList from './non-verbal/NonVerbalList.vue';
|
|
30
|
+
import PartitiveRelationList from './PartitiveRelationList.vue';
|
|
31
|
+
import ConceptDiffModal from './ConceptDiffModal.vue';
|
|
30
32
|
import CitationDisplay from './CitationDisplay.vue';
|
|
31
33
|
import DesignationList from './DesignationList.vue';
|
|
32
34
|
import { useI18n } from '../i18n';
|
|
@@ -72,6 +74,7 @@ const {
|
|
|
72
74
|
edgeBadgeColor,
|
|
73
75
|
inverseEdgeType,
|
|
74
76
|
conceptRelated,
|
|
77
|
+
conceptPartitiveRelations,
|
|
75
78
|
getResolvedRef,
|
|
76
79
|
relatedLabel,
|
|
77
80
|
navigateEdge,
|
|
@@ -80,6 +83,11 @@ const {
|
|
|
80
83
|
|
|
81
84
|
const hoveredEdgeDisplay = ref<{ designation: string; conceptId: string; tooltip: string } | null>(null);
|
|
82
85
|
|
|
86
|
+
const diffTarget = ref<{ registerId: string; conceptId: string; label: string } | null>(null);
|
|
87
|
+
function onCompareEdition(target: { registerId: string; conceptId: string; label: string }) {
|
|
88
|
+
diffTarget.value = target;
|
|
89
|
+
}
|
|
90
|
+
|
|
83
91
|
const uriCopied = ref(false);
|
|
84
92
|
function copyUri() {
|
|
85
93
|
const uri = conceptUri(props.concept, props.registerId, props.manifest.uriBase);
|
|
@@ -561,11 +569,32 @@ const nonVerbalReps = computed(() => {
|
|
|
561
569
|
</Transition>
|
|
562
570
|
</div>
|
|
563
571
|
|
|
572
|
+
<!-- Partitive relations — one-to-many decompositions (ISO 704) -->
|
|
573
|
+
<PartitiveRelationList
|
|
574
|
+
v-if="conceptPartitiveRelations.length"
|
|
575
|
+
:relations="conceptPartitiveRelations"
|
|
576
|
+
:manifest="manifest"
|
|
577
|
+
:register-id="registerId"
|
|
578
|
+
/>
|
|
579
|
+
|
|
564
580
|
<!-- Edition series — supersession chain across vocabulary editions -->
|
|
565
581
|
<ConceptEditionRail
|
|
566
582
|
:concept-uri="conceptUri(props.concept, props.registerId, props.manifest.uriBase)"
|
|
567
583
|
:register-id="registerId"
|
|
568
584
|
:concept-id="conceptId"
|
|
585
|
+
@compare="onCompareEdition"
|
|
586
|
+
/>
|
|
587
|
+
|
|
588
|
+
<!-- Edition diff modal — opened from the rail's compare buttons -->
|
|
589
|
+
<ConceptDiffModal
|
|
590
|
+
v-if="diffTarget"
|
|
591
|
+
:open="true"
|
|
592
|
+
:current-concept="concept"
|
|
593
|
+
:current-register-id="registerId"
|
|
594
|
+
:target-register-id="diffTarget.registerId"
|
|
595
|
+
:target-concept-id="diffTarget.conceptId"
|
|
596
|
+
:target-label="diffTarget.label"
|
|
597
|
+
@close="diffTarget = null"
|
|
569
598
|
/>
|
|
570
599
|
|
|
571
600
|
<!-- Domains -->
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* ConceptDiffModal — modal wrapper around ConceptDiffView that fetches
|
|
4
|
+
* the comparison concept on demand.
|
|
5
|
+
*
|
|
6
|
+
* Triggered from ConceptEditionRail when the user clicks "compare"
|
|
7
|
+
* next to a non-current edition. The current concept (props.concept)
|
|
8
|
+
* becomes the "new" side; the selected edition becomes the "old" side.
|
|
9
|
+
*/
|
|
10
|
+
import { ref, watch, computed } from 'vue';
|
|
11
|
+
import { useVocabularyStore } from '../stores/vocabulary';
|
|
12
|
+
import ConceptDiffView from './ConceptDiffView.vue';
|
|
13
|
+
import type { ConceptLikeData } from '../composables/use-concept-diff';
|
|
14
|
+
import type { Concept } from 'glossarist';
|
|
15
|
+
import { useI18n } from '../i18n';
|
|
16
|
+
|
|
17
|
+
const props = defineProps<{
|
|
18
|
+
open: boolean;
|
|
19
|
+
/** The concept currently being viewed. */
|
|
20
|
+
currentConcept: Concept | null;
|
|
21
|
+
currentRegisterId: string;
|
|
22
|
+
/** Edition to compare against. */
|
|
23
|
+
targetRegisterId: string;
|
|
24
|
+
targetConceptId: string;
|
|
25
|
+
targetLabel?: string;
|
|
26
|
+
}>();
|
|
27
|
+
|
|
28
|
+
const emit = defineEmits<{ close: [] }>();
|
|
29
|
+
|
|
30
|
+
const store = useVocabularyStore();
|
|
31
|
+
const { t } = useI18n();
|
|
32
|
+
|
|
33
|
+
const targetConcept = ref<Concept | null>(null);
|
|
34
|
+
const loadError = ref<string | null>(null);
|
|
35
|
+
|
|
36
|
+
function conceptToData(c: Concept | null, registerId: string): ConceptLikeData | null {
|
|
37
|
+
if (!c) return null;
|
|
38
|
+
const langs = c.languages ?? [];
|
|
39
|
+
const localizations: Record<string, unknown> = {};
|
|
40
|
+
for (const lang of langs) {
|
|
41
|
+
const lc = c.localization(lang);
|
|
42
|
+
if (lc) localizations[lang] = lc;
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
conceptId: c.id,
|
|
46
|
+
uri: c.uri ?? undefined,
|
|
47
|
+
status: c.status ?? undefined,
|
|
48
|
+
languages: langs,
|
|
49
|
+
localizations,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const oldData = computed(() => conceptToData(targetConcept.value, props.targetRegisterId));
|
|
54
|
+
const newData = computed(() => conceptToData(props.currentConcept, props.currentRegisterId));
|
|
55
|
+
|
|
56
|
+
async function loadTarget() {
|
|
57
|
+
if (!props.open || !props.targetRegisterId || !props.targetConceptId) return;
|
|
58
|
+
loadError.value = null;
|
|
59
|
+
targetConcept.value = null;
|
|
60
|
+
try {
|
|
61
|
+
const adapter = store.datasets.get(props.targetRegisterId);
|
|
62
|
+
if (!adapter) throw new Error(`dataset ${props.targetRegisterId} not loaded`);
|
|
63
|
+
if (!adapter.index) await store.loadDataset(props.targetRegisterId);
|
|
64
|
+
targetConcept.value = await adapter.fetchConcept(props.targetConceptId);
|
|
65
|
+
} catch (e: unknown) {
|
|
66
|
+
loadError.value = e instanceof Error ? e.message : String(e);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
watch(() => [props.open, props.targetRegisterId, props.targetConceptId] as const, () => {
|
|
71
|
+
if (props.open) loadTarget();
|
|
72
|
+
}, { immediate: true });
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<template>
|
|
76
|
+
<Teleport to="body">
|
|
77
|
+
<Transition name="fade">
|
|
78
|
+
<div
|
|
79
|
+
v-if="open"
|
|
80
|
+
class="fixed inset-0 z-50 bg-black/40 flex items-center justify-center p-4"
|
|
81
|
+
@click.self="$emit('close')"
|
|
82
|
+
>
|
|
83
|
+
<div class="bg-surface rounded-lg shadow-xl max-w-3xl w-full max-h-[85vh] overflow-hidden flex flex-col">
|
|
84
|
+
<header class="px-5 py-3 border-b border-ink-100 dark:border-ink-700 flex items-center justify-between">
|
|
85
|
+
<h2 class="text-sm font-semibold text-ink-700 dark:text-ink-200">
|
|
86
|
+
{{ t('concept.compareEditions') || 'Compare editions' }}
|
|
87
|
+
</h2>
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
class="text-ink-400 hover:text-ink-700 dark:hover:text-ink-200"
|
|
91
|
+
@click="$emit('close')"
|
|
92
|
+
:aria-label="t('common.close') || 'Close'"
|
|
93
|
+
>
|
|
94
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
95
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
96
|
+
</svg>
|
|
97
|
+
</button>
|
|
98
|
+
</header>
|
|
99
|
+
<div class="p-5 overflow-y-auto">
|
|
100
|
+
<div v-if="loadError" class="card p-4 border-red-200 bg-red-50/50">
|
|
101
|
+
<p class="text-sm text-red-700">{{ loadError }}</p>
|
|
102
|
+
</div>
|
|
103
|
+
<ConceptDiffView
|
|
104
|
+
v-else
|
|
105
|
+
:old-concept="oldData"
|
|
106
|
+
:new-concept="newData"
|
|
107
|
+
:old-label="targetLabel || targetRegisterId"
|
|
108
|
+
:new-label="currentRegisterId"
|
|
109
|
+
/>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</Transition>
|
|
114
|
+
</Teleport>
|
|
115
|
+
</template>
|
|
116
|
+
|
|
117
|
+
<style scoped>
|
|
118
|
+
.fade-enter-active, .fade-leave-active { transition: opacity 0.2s ease; }
|
|
119
|
+
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
|
120
|
+
</style>
|