@glossarist/concept-browser 0.2.7 → 0.2.9

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.2.7",
3
+ "version": "0.2.9",
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": {
@@ -23,26 +23,13 @@ function findConfigFile(args = []) {
23
23
  return resolve(process.env.SITE_CONFIG);
24
24
  }
25
25
 
26
- const siteId = process.env.SITE_ID || args.find(a => !a.startsWith('-')) || null;
27
- if (siteId) {
28
- // Check project configs/ dir first
29
- const p = resolve(projectRoot, 'configs', `${siteId}.yml`);
30
- if (existsSync(p)) return p;
31
- // Check CWD (deployment repo may have configs locally)
32
- for (const name of [`${siteId}.yml`, 'site-config.yml']) {
33
- const cwdP = resolve(process.cwd(), name);
34
- if (existsSync(cwdP)) return cwdP;
35
- }
36
- throw new Error(`Site config not found for '${siteId}'. Checked configs/${siteId}.yml, ${siteId}.yml, site-config.yml in CWD`);
37
- }
38
-
39
26
  // Check CWD first (deployment repos), then project root
40
27
  for (const dir of [process.cwd(), projectRoot]) {
41
28
  const p = resolve(dir, 'site-config.yml');
42
29
  if (existsSync(p)) return p;
43
30
  }
44
31
 
45
- throw new Error('No site config found. Set SITE_CONFIG, SITE_ID, or create site-config.yml');
32
+ throw new Error('No site config found. Set SITE_CONFIG or create site-config.yml');
46
33
  }
47
34
 
48
35
  export function loadSiteConfig(args = []) {
@@ -204,6 +204,31 @@ function scrollToLang(lang: string) {
204
204
  const outgoingEdges = computed(() => props.edges.filter(e => e.source === props.concept['@id']));
205
205
  const incomingEdges = computed(() => props.edges.filter(e => e.target === props.concept['@id']));
206
206
 
207
+ function isLocalRef(uri: string): boolean {
208
+ const resolution = factory.resolve(uri, props.registerId);
209
+ return resolution.type === 'internal' && resolution.registerId === props.registerId;
210
+ }
211
+
212
+ function edgeConceptId(uri: string): string {
213
+ const m = uri.match(/\/concept\/([^/]+)$/);
214
+ return m ? m[1] : uri.split('/').pop() || uri;
215
+ }
216
+
217
+ function edgeNodeData(uri: string) {
218
+ return store.graph.getNode(uri);
219
+ }
220
+
221
+ function edgeTooltip(uri: string): string {
222
+ const node = edgeNodeData(uri);
223
+ const lines: string[] = [uri];
224
+ if (node) {
225
+ for (const [lang, designation] of Object.entries(node.designations)) {
226
+ lines.push(`${langLabel(lang)}: ${designation}`);
227
+ }
228
+ }
229
+ return lines.join('\n');
230
+ }
231
+
207
232
  function edgeDatasetBadge(uri: string): { id: string; title: string } | null {
208
233
  const resolution = factory.resolve(uri, props.registerId);
209
234
  if (resolution.type === 'internal' && resolution.registerId !== props.registerId) {
@@ -474,10 +499,14 @@ function plainTruncate(html: string, max: number = 120): string {
474
499
  v-for="edge in outgoingEdges"
475
500
  :key="edge.target"
476
501
  @click="navigateEdge(edge)"
502
+ :title="edgeTooltip(edge.target)"
477
503
  class="text-sm concept-link block truncate w-full text-left flex items-center gap-1.5"
504
+ :class="isLocalRef(edge.target) ? '' : 'xref-external'"
478
505
  >
479
- {{ edge.label || edge.target.split('/').pop() }}
506
+ {{ edgeConceptId(edge.target) }}
480
507
  <span v-if="edgeDatasetBadge(edge.target)" class="badge badge-gray text-[9px] flex-shrink-0 truncate max-w-[100px]">{{ edgeDatasetBadge(edge.target)!.title }}</span>
508
+ <span v-if="isLocalRef(edge.target)" class="text-[9px] text-ink-200 flex-shrink-0">local</span>
509
+ <span v-else class="text-[9px] text-amber-500 flex-shrink-0">external</span>
481
510
  </button>
482
511
  </div>
483
512
  </div>
@@ -488,10 +517,14 @@ function plainTruncate(html: string, max: number = 120): string {
488
517
  v-for="edge in incomingEdges"
489
518
  :key="edge.source"
490
519
  @click="navigateEdge(edge)"
520
+ :title="edgeTooltip(edge.source)"
491
521
  class="text-sm concept-link block truncate w-full text-left flex items-center gap-1.5"
522
+ :class="isLocalRef(edge.source) ? '' : 'xref-external'"
492
523
  >
493
- {{ edge.label || edge.source.split('/').pop() }}
524
+ {{ edgeConceptId(edge.source) }}
494
525
  <span v-if="edgeDatasetBadge(edge.source)" class="badge badge-gray text-[9px] flex-shrink-0 truncate max-w-[100px]">{{ edgeDatasetBadge(edge.source)!.title }}</span>
526
+ <span v-if="isLocalRef(edge.source)" class="text-[9px] text-ink-200 flex-shrink-0">local</span>
527
+ <span v-else class="text-[9px] text-amber-500 flex-shrink-0">external</span>
495
528
  </button>
496
529
  </div>
497
530
  </div>
@@ -27,6 +27,7 @@ const svgRef = ref<SVGSVGElement | null>(null);
27
27
  const containerRef = ref<HTMLDivElement | null>(null);
28
28
  const selectedNode = ref<GraphNode | null>(null);
29
29
  const detailCloseRef = ref<HTMLButtonElement | null>(null);
30
+ const labelMode = ref<'designation' | 'identifier'>('designation');
30
31
 
31
32
  // Dataset enable/disable state
32
33
  const registerEnabled = reactive<Record<string, boolean>>({});
@@ -219,7 +220,7 @@ function buildSimulation(width: number, height: number) {
219
220
  .attr('font-weight', '500')
220
221
  .attr('fill', '#636588') // ink-400
221
222
  .attr('pointer-events', 'none')
222
- .text(d => d.designation.slice(0, 18));
223
+ .text(d => (labelMode.value === 'identifier' ? d.conceptId : d.designation).slice(0, 18));
223
224
 
224
225
  const dragBehavior = drag<SVGGElement, SimNode>()
225
226
  .on('start', (event: D3DragEvent<SVGGElement, SimNode, SimNode>, d) => {
@@ -434,6 +435,22 @@ function selectedNodeColor(): string {
434
435
  <button @click="rebuildGraph" class="text-[10px] font-semibold text-ink-500 hover:text-ink-700 uppercase tracking-wide transition-colors">Re-layout</button>
435
436
  </div>
436
437
 
438
+ <div v-if="nodeCount > 0" class="mt-3 pt-3 border-t border-ink-100/40">
439
+ <div class="text-[10px] font-semibold text-ink-400 uppercase tracking-wide mb-2">Node labels</div>
440
+ <div class="flex gap-1">
441
+ <button
442
+ @click="labelMode = 'designation'; rebuildGraph()"
443
+ class="text-[10px] px-2 py-1 rounded font-medium transition-colors"
444
+ :class="labelMode === 'designation' ? 'bg-ink-800 text-white' : 'text-ink-500 hover:bg-ink-50'"
445
+ >Designation</button>
446
+ <button
447
+ @click="labelMode = 'identifier'; rebuildGraph()"
448
+ class="text-[10px] px-2 py-1 rounded font-medium transition-colors"
449
+ :class="labelMode === 'identifier' ? 'bg-ink-800 text-white' : 'text-ink-500 hover:bg-ink-50'"
450
+ >Identifier</button>
451
+ </div>
452
+ </div>
453
+
437
454
  <div v-if="nodeCount === 0" class="text-xs text-ink-300 mt-3 leading-relaxed">
438
455
  {{ props.edges.length > 0 ? 'Enable datasets to see their graph.' : 'Browse concepts with cross-references to populate the graph.' }}
439
456
  </div>