diff --git a/frontend/src/pages/LinkMLViewerPage.css b/frontend/src/pages/LinkMLViewerPage.css
index 9798795d96..9688ace2a8 100644
--- a/frontend/src/pages/LinkMLViewerPage.css
+++ b/frontend/src/pages/LinkMLViewerPage.css
@@ -672,6 +672,42 @@
color: var(--warning-color, #f57c00);
}
+.linkml-viewer__badge--usage {
+ background: var(--slot-usage-light, #e8f5e9);
+ color: var(--slot-usage-color, #388e3c);
+ font-weight: 500;
+}
+
+/* Visual indicator for slots with slot_usage overrides */
+.linkml-viewer__item--has-usage {
+ border-left: 3px solid var(--slot-usage-color, #388e3c);
+ padding-left: 0.75rem;
+}
+
+/* Override markers - show which properties come from slot_usage */
+.linkml-viewer__override-marker {
+ display: inline-block;
+ font-size: 0.75rem;
+ margin-right: 0.25rem;
+ font-weight: 600;
+}
+
+.linkml-viewer__override-marker--overridden {
+ color: var(--slot-usage-color, #388e3c);
+ cursor: help;
+}
+
+.linkml-viewer__override-marker--inherited {
+ /* Inherited properties don't show any marker */
+ display: none;
+}
+
+/* Label with inline override marker */
+.linkml-viewer__label--inline {
+ display: inline;
+ margin-right: 0;
+}
+
/* Custodian Type Badge - shows which GLAMORCUBESFIXPHDNT types apply */
.linkml-viewer__custodian-badge {
margin-left: 0.5rem;
diff --git a/frontend/src/pages/LinkMLViewerPage.tsx b/frontend/src/pages/LinkMLViewerPage.tsx
index 5b51b1dd8b..8491b1d5a6 100644
--- a/frontend/src/pages/LinkMLViewerPage.tsx
+++ b/frontend/src/pages/LinkMLViewerPage.tsx
@@ -968,6 +968,15 @@ const TEXT = {
broadMappings: { nl: 'Bredere mappings:', en: 'Broad Mappings:' },
relatedMappings: { nl: 'Gerelateerde mappings:', en: 'Related Mappings:' },
comments: { nl: 'Opmerkingen:', en: 'Comments:' },
+ // Slot usage indicator
+ slotUsageBadge: { nl: 'slot_usage', en: 'slot_usage' },
+ slotUsageTooltip: {
+ nl: 'Deze slot heeft klasse-specifieke overschrijvingen gedefinieerd in slot_usage. Eigenschappen met ✦ komen uit de slot_usage, andere zijn overgenomen van de generieke slot-definitie.',
+ en: 'This slot has class-specific overrides defined in slot_usage. Properties marked with ✦ come from slot_usage, others are inherited from the generic slot definition.'
+ },
+ slotUsageOverrideMarker: { nl: '✦', en: '✦' },
+ inheritedFromGeneric: { nl: 'overgenomen', en: 'inherited' },
+ overriddenInSlotUsage: { nl: 'overschreven in slot_usage', en: 'overridden in slot_usage' },
};
// Dynamically discover schema files from the modules directory
@@ -2048,11 +2057,22 @@ const LinkMLViewerPage: React.FC = () => {
{expandedClassSlots.has(cls.name) && (
{cls.slots.map(slotName => {
- const slot = slotLookupMap.get(slotName);
- if (slot) {
- return renderSlotDetails(slot);
+ // Get generic slot definition from the global slot lookup
+ const genericSlot = slotLookupMap.get(slotName);
+ // Get class-specific slot_usage overrides (if any)
+ const slotUsage = cls.slot_usage?.[slotName];
+
+ // Merge generic slot with slot_usage overrides
+ // slot_usage properties take precedence over generic slot properties
+ if (genericSlot || slotUsage) {
+ const mergedSlot: LinkMLSlot = {
+ name: slotName,
+ ...(genericSlot || {}),
+ ...(slotUsage || {}),
+ };
+ return renderSlotDetails(mergedSlot, slotUsage || null);
}
- // Fallback: show slot name as tag if definition not found
+ // Fallback: show slot name as tag if neither generic definition nor slot_usage found
return (
{slotName}
@@ -2578,7 +2598,8 @@ const LinkMLViewerPage: React.FC = () => {
);
};
- const renderSlotDetails = (slot: LinkMLSlot) => {
+ const renderSlotDetails = (slot: LinkMLSlot, slotUsage: Partial
| null = null) => {
+ const hasSlotUsageOverrides = slotUsage !== null;
const rangeIsEnum = slot.range && isEnumRange(slot.range);
const enumKey = slot.range ? `${slot.name}:${slot.range}` : '';
const isExpanded = expandedEnumRanges.has(enumKey);
@@ -2589,10 +2610,31 @@ const LinkMLViewerPage: React.FC = () => {
// Check if this slot matches the current custodian type filter (multi-select)
const matchesFilter = custodianTypeFilter.size === 0 || custodianTypes.some(t => custodianTypeFilter.has(t));
+ // Helper to check if a property is overridden in slot_usage
+ const isOverridden = (property: keyof LinkMLSlot): boolean => {
+ if (!slotUsage) return false;
+ return slotUsage[property] !== undefined;
+ };
+
+ // Helper to render override marker
+ const OverrideMarker = ({ property }: { property: keyof LinkMLSlot }) => {
+ if (!hasSlotUsageOverrides) return null;
+ const overridden = isOverridden(property);
+ return (
+
+ {overridden ? t('slotUsageOverrideMarker') : ''}
+
+ );
+ };
+
return (
-
+
{slot.name}
+ {hasSlotUsageOverrides && {t('slotUsageBadge')}}
{slot.required && {t('required')}}
{slot.multivalued && {t('multivalued')}}
{slot.identifier && {t('identifier')}}
@@ -2621,13 +2663,13 @@ const LinkMLViewerPage: React.FC = () => {
{/* Slot URI - semantic predicate */}
{slot.slot_uri && (
- {t('slotUri')}
+ {t('slotUri')}
{slot.slot_uri}
)}
{slot.range && (
-
{t('range')}
+
{t('range')}
{rangeIsEnum ? (