@glossarist/concept-browser 0.7.37 → 0.7.42

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glossarist/concept-browser",
3
- "version": "0.7.37",
3
+ "version": "0.7.42",
4
4
  "description": "Vue SPA for browsing Glossarist terminology datasets with cross-reference resolution, graph visualization, and multi-language support",
5
5
  "type": "module",
6
6
  "bin": {
@@ -74,6 +74,8 @@ const {
74
74
  navigateRelated,
75
75
  } = useConceptEdges(conceptComputed, registerIdComputed, manifestComputed, edgesComputed, router);
76
76
 
77
+ const hoveredEdgeDisplay = ref<{ designation: string; conceptId: string; tooltip: string } | null>(null);
78
+
77
79
  const uriCopied = ref(false);
78
80
  function copyUri() {
79
81
  const uri = conceptUri(props.concept, props.registerId, props.manifest.uriBase);
@@ -473,42 +475,63 @@ const nonVerbalReps = computed(() => {
473
475
  <!-- Relations -->
474
476
  <div v-if="outgoingEdges.length || incomingEdges.length" class="card p-5">
475
477
  <div class="section-label">{{ t('concept.relations') }}</div>
478
+
479
+ <!-- Outgoing -->
476
480
  <div v-if="outgoingEdges.length" class="mt-3">
477
- <div class="text-xs text-ink-300 mb-2">{{ t('concept.outgoing') }} ({{ outgoingEdges.length }})</div>
478
- <div class="space-y-1 max-h-64 overflow-y-auto">
481
+ <div class="text-xs text-ink-300 mb-1.5">{{ t('concept.outgoing') }} ({{ outgoingEdges.length }})</div>
482
+ <div class="space-y-0.5 max-h-56 overflow-y-auto pr-1 -mr-1">
479
483
  <button
480
484
  v-for="edge in outgoingEdges"
481
485
  :key="edge.target + edge.type"
482
486
  @click="navigateEdge(edge)"
483
- :title="getEdgeDisplay(edge.target).tooltip"
484
- class="text-sm concept-link block truncate w-full text-left flex items-center gap-1.5"
487
+ @mouseenter="hoveredEdgeDisplay = getEdgeDisplay(edge.target)"
488
+ @mouseleave="hoveredEdgeDisplay = null"
489
+ @focus="hoveredEdgeDisplay = getEdgeDisplay(edge.target)"
490
+ class="concept-link block w-full text-left rounded-md px-1.5 py-1 hover:bg-ink-50 transition-colors"
485
491
  :class="getEdgeDisplay(edge.target).isLocal ? '' : 'xref-external'"
486
492
  >
487
- <span class="badge text-[9px] flex-shrink-0" :class="edgeBadgeColor(edge.type, 'out')">{{ relationshipLabel(edge.type) }} →</span>
488
- <span class="truncate">{{ getEdgeDisplay(edge.target).designation || edge.label || getEdgeDisplay(edge.target).conceptId }}</span>
489
- <span class="text-[9px] text-ink-300 flex-shrink-0 font-mono">{{ getEdgeDisplay(edge.target).conceptId }}</span>
490
- <span v-if="getEdgeDisplay(edge.target).badge" class="badge badge-gray text-[9px] flex-shrink-0 truncate max-w-[100px]">{{ getEdgeDisplay(edge.target).badge!.title }}</span>
493
+ <div class="flex items-center gap-1 mb-0.5">
494
+ <span class="badge text-[9px] flex-shrink-0" :class="edgeBadgeColor(edge.type, 'out')">{{ relationshipLabel(edge.type) }} →</span>
495
+ <span v-if="getEdgeDisplay(edge.target).badge" class="badge badge-gray text-[9px] flex-shrink-0 truncate">{{ getEdgeDisplay(edge.target).badge!.title }}</span>
496
+ </div>
497
+ <div class="text-sm text-ink-700 leading-snug line-clamp-2">{{ getEdgeDisplay(edge.target).designation || edge.label || getEdgeDisplay(edge.target).conceptId }}</div>
491
498
  </button>
492
499
  </div>
493
500
  </div>
501
+
502
+ <!-- Incoming -->
494
503
  <div v-if="incomingEdges.length" class="mt-3 pt-3 border-t border-ink-100/60">
495
- <div class="text-xs text-ink-300 mb-2">{{ t('concept.incoming') }} ({{ incomingEdges.length }})</div>
496
- <div class="space-y-1 max-h-48 overflow-y-auto">
504
+ <div class="text-xs text-ink-300 mb-1.5">{{ t('concept.incoming') }} ({{ incomingEdges.length }})</div>
505
+ <div class="space-y-0.5 max-h-40 overflow-y-auto pr-1 -mr-1">
497
506
  <button
498
507
  v-for="edge in incomingEdges"
499
508
  :key="edge.source + edge.type"
500
509
  @click="navigateEdge(edge)"
501
- :title="getEdgeDisplay(edge.source).tooltip"
502
- class="text-sm concept-link block truncate w-full text-left flex items-center gap-1.5"
510
+ @mouseenter="hoveredEdgeDisplay = getEdgeDisplay(edge.source)"
511
+ @mouseleave="hoveredEdgeDisplay = null"
512
+ @focus="hoveredEdgeDisplay = getEdgeDisplay(edge.source)"
513
+ class="concept-link block w-full text-left rounded-md px-1.5 py-1 hover:bg-ink-50 transition-colors"
503
514
  :class="getEdgeDisplay(edge.source).isLocal ? '' : 'xref-external'"
504
515
  >
505
- <span class="badge text-[9px] flex-shrink-0" :class="edgeBadgeColor(edge.type, 'in')">← {{ relationshipLabel(inverseEdgeType(edge.type)) }}</span>
506
- <span class="truncate">{{ getEdgeDisplay(edge.source).designation || edge.label || getEdgeDisplay(edge.source).conceptId }}</span>
507
- <span class="text-[9px] text-ink-300 flex-shrink-0 font-mono">{{ getEdgeDisplay(edge.source).conceptId }}</span>
508
- <span v-if="getEdgeDisplay(edge.source).badge" class="badge badge-gray text-[9px] flex-shrink-0 truncate max-w-[100px]">{{ getEdgeDisplay(edge.source).badge!.title }}</span>
516
+ <div class="flex items-center gap-1 mb-0.5">
517
+ <span class="badge text-[9px] flex-shrink-0" :class="edgeBadgeColor(edge.type, 'in')">← {{ relationshipLabel(inverseEdgeType(edge.type)) }}</span>
518
+ <span v-if="getEdgeDisplay(edge.source).badge" class="badge badge-gray text-[9px] flex-shrink-0 truncate">{{ getEdgeDisplay(edge.source).badge!.title }}</span>
519
+ </div>
520
+ <div class="text-sm text-ink-700 leading-snug line-clamp-2">{{ getEdgeDisplay(edge.source).designation || edge.label || getEdgeDisplay(edge.source).conceptId }}</div>
509
521
  </button>
510
522
  </div>
511
523
  </div>
524
+
525
+ <!-- Hover detail strip (fixed area at bottom of card) -->
526
+ <Transition name="fade">
527
+ <div
528
+ v-if="hoveredEdgeDisplay"
529
+ class="mt-3 pt-3 border-t border-ink-100/60"
530
+ >
531
+ <div class="text-xs text-ink-300 mb-0.5">{{ t('concept.identifier') || 'Identifier' }}</div>
532
+ <div class="font-mono text-[10px] text-ink-500 break-all leading-tight">{{ hoveredEdgeDisplay.conceptId }}</div>
533
+ </div>
534
+ </Transition>
512
535
  </div>
513
536
 
514
537
  <!-- Domains -->
@@ -659,3 +682,14 @@ const nonVerbalReps = computed(() => {
659
682
  </div>
660
683
  </div>
661
684
  </template>
685
+
686
+ <style scoped>
687
+ .fade-enter-active,
688
+ .fade-leave-active {
689
+ transition: opacity 0.15s ease;
690
+ }
691
+ .fade-enter-from,
692
+ .fade-leave-to {
693
+ opacity: 0;
694
+ }
695
+ </style>
@@ -58,12 +58,13 @@ export function useConceptEdges(
58
58
  const designation = node
59
59
  ? (node.designations[locale.value] || node.designations.eng || Object.values(node.designations)[0] || '')
60
60
  : '';
61
- const tooltipLines: string[] = [uri];
61
+ const tooltipLines: string[] = [conceptId];
62
62
  if (node) {
63
63
  for (const [lang, des] of Object.entries(node.designations)) {
64
64
  tooltipLines.push(`${langLabel(lang)}: ${des}`);
65
65
  }
66
66
  }
67
+ tooltipLines.push(uri);
67
68
  let badge: { id: string; title: string } | null = null;
68
69
  if (resolution.type === 'internal' && resolution.registerId !== registerId.value) {
69
70
  const m = store.manifests.get(resolution.registerId);
package/src/style.css CHANGED
@@ -334,6 +334,35 @@
334
334
  color: #dddde6;
335
335
  }
336
336
 
337
+ /* ── Brand colors: lighten for dark backgrounds ── */
338
+ /* ISO red (#e3000f) is too harsh on dark — use a softer coral */
339
+ .dark {
340
+ --brand-primary: #ff5d5d;
341
+ --brand-primary-rgb: 255, 93, 93;
342
+ --brand-dark: #1a1b2e;
343
+ }
344
+
345
+ /* Concept/xref links: lighten in dark mode (override pure red) */
346
+ .dark .concept-link,
347
+ .dark .xref-link {
348
+ color: #ff6b6b;
349
+ }
350
+ .dark .concept-link:hover,
351
+ .dark .xref-link:hover {
352
+ filter: brightness(1.15);
353
+ }
354
+
355
+ /* Brand badges: readable background in dark mode */
356
+ .dark .badge-brand {
357
+ background-color: rgba(255, 93, 93, 0.15);
358
+ color: #ff8a8a;
359
+ }
360
+
361
+ /* Primary buttons: avoid harsh red hover in dark mode */
362
+ .dark .btn-primary:hover {
363
+ background-color: #c41818;
364
+ }
365
+
337
366
  /* ── Surfaces ── */
338
367
  .dark .bg-surface { background-color: #0f1020 !important; }
339
368
  .dark .bg-surface-alt { background-color: #161728 !important; }
@@ -30,10 +30,17 @@ const chunkLoading = ref(false);
30
30
  // Background chunk preloading via requestIdleCallback
31
31
  let idlePreloadHandle: ReturnType<typeof requestIdleCallback> | ReturnType<typeof setTimeout> | null = null;
32
32
 
33
- watch(adapter, (a) => {
34
- if (idlePreloadHandle !== null) return;
33
+ watch(adapter, async (a) => {
35
34
  if (!a || !a.index) return;
36
35
 
36
+ // If a section filter is active, load all chunks immediately (not idle)
37
+ if (sectionQuery.value && !allChunksLoaded.value) {
38
+ await ensureAllChunksForFilter(true);
39
+ return;
40
+ }
41
+
42
+ if (idlePreloadHandle !== null) return;
43
+
37
44
  const schedule: (cb: () => void) => number = typeof requestIdleCallback !== 'undefined'
38
45
  ? (cb) => requestIdleCallback(cb, { timeout: 2000 })
39
46
  : (cb) => window.setTimeout(cb, 0);
@@ -87,6 +94,7 @@ const allChunksLoaded = ref(false);
87
94
  const selectedLang = ref<string | null>(null);
88
95
  const viewMode = ref<'systematic' | 'alphabetical'>('systematic');
89
96
  const sectionQuery = computed(() => (route.query.section as string) || null);
97
+ const page = ref(1);
90
98
 
91
99
  interface LangOption {
92
100
  code: string;
@@ -124,10 +132,14 @@ onUnmounted(() => window.removeEventListener('keydown', onGlobalKeydown));
124
132
 
125
133
  async function ensureAllChunksForFilter(needsLoad: boolean) {
126
134
  page.value = 1;
127
- if (needsLoad && !allChunksLoaded.value && adapter.value) {
128
- chunkLoading.value = true;
129
- await adapter.value.ensureAllChunksLoaded();
135
+ if (!needsLoad || allChunksLoaded.value) return;
136
+ const a = adapter.value;
137
+ if (!a?.index) return;
138
+ chunkLoading.value = true;
139
+ try {
140
+ await a.ensureAllChunksLoaded();
130
141
  allChunksLoaded.value = true;
142
+ } finally {
131
143
  chunkLoading.value = false;
132
144
  }
133
145
  }
@@ -138,7 +150,7 @@ watch(filter, async (q) => {
138
150
 
139
151
  watch(sectionQuery, async () => {
140
152
  await ensureAllChunksForFilter(!!sectionQuery.value);
141
- });
153
+ }, { immediate: true });
142
154
 
143
155
  watch(selectedLang, async (lang) => {
144
156
  await ensureAllChunksForFilter(!!lang);
@@ -202,7 +214,6 @@ const alphabetGroups = computed(() => {
202
214
  return [...groups.entries()].sort((a, b) => a[0].localeCompare(b[0]));
203
215
  });
204
216
 
205
- const page = ref(1);
206
217
  const perPage = 50;
207
218
 
208
219
  // Check if the current page range is loaded in the index