@@ -174,7 +208,7 @@ export function LanguagePicker({ value, onChange, disabled }: LanguagePickerProp
)}
- {FREQUENT_LANGUAGES.map((lang) => (
+ {frequentLanguages.map((lang) => (
- {OTHER_LANGUAGES.map((lang) => (
+ {otherLanguages.map((lang) => (
= {
object: { label: "Object Property", letter: "O", color: "bg-emerald-100 border-emerald-300 text-emerald-700 dark:bg-emerald-900/30 dark:border-emerald-700 dark:text-emerald-400" },
@@ -209,8 +203,8 @@ export function PropertyDetailPanel({
const initEditState = useCallback((d: ParsedPropertyDetail) => {
setEditPropertyType(d.propertyType);
setEditLabels(d.labels.length > 0 ? d.labels.map((l) => ({ ...l })) : [{ value: "", lang: "en" }]);
- setEditComments(ensureTrailingEmpty(d.comments.map((c) => ({ ...c }))));
- setEditDefinitions(ensureTrailingEmpty(d.definitions.map((def) => ({ ...def }))));
+ setEditComments(ensureTrailingPlaceholder(d.comments.map((c) => ({ ...c })), "multiple"));
+ setEditDefinitions(ensureTrailingPlaceholder(d.definitions.map((def) => ({ ...def })), "single-per-lang"));
setEditDomainIris([...d.domainIris]);
setEditRangeIris([...d.rangeIris]);
setEditParentIris([...d.parentIris]);
@@ -240,7 +234,7 @@ export function PropertyDetailPanel({
// Annotations: filter out definition (shown in its own section)
const regularAnnotations = d.annotations
.filter((a) => a.property_iri !== DEFINITION_IRI)
- .map((a) => ({ ...a, values: ensureTrailingEmpty(a.values.map((v) => ({ ...v }))) }));
+ .map((a) => ({ ...a, values: ensureTrailingPlaceholder(a.values.map((v) => ({ ...v })), getAnnotationCardinality(a.property_iri)) }));
if (!regularAnnotations.find((a) => a.property_iri === DEFINITION_IRI)) {
// Don't add definition here — it has its own section
@@ -292,8 +286,8 @@ export function PropertyDetailPanel({
const d = restoredDraft as PropertyDraftEntry;
setEditPropertyType(d.propertyType);
setEditLabels(d.labels);
- setEditComments(ensureTrailingEmpty(d.comments));
- setEditDefinitions(ensureTrailingEmpty(d.definitions));
+ setEditComments(ensureTrailingPlaceholder(d.comments, "multiple"));
+ setEditDefinitions(ensureTrailingPlaceholder(d.definitions, "single-per-lang"));
setEditDomainIris(d.domainIris);
setEditRangeIris(d.rangeIris);
setEditParentIris(d.parentIris);
@@ -321,20 +315,20 @@ export function PropertyDetailPanel({
}, [triggerSave]);
const updateComment = useCallback((index: number, field: "value" | "lang", val: string) => {
- setEditComments((prev) => ensureTrailingEmpty(prev.map((c, i) => (i === index ? { ...c, [field]: val } : c))));
+ setEditComments((prev) => ensureTrailingPlaceholder(prev.map((c, i) => (i === index ? { ...c, [field]: val } : c)), "multiple"));
}, []);
const removeComment = useCallback((index: number) => {
- setEditComments((prev) => ensureTrailingEmpty(prev.filter((_, i) => i !== index)));
+ setEditComments((prev) => ensureTrailingPlaceholder(prev.filter((_, i) => i !== index), "multiple"));
requestAnimationFrame(() => triggerSave());
}, [triggerSave]);
const updateDefinition = useCallback((index: number, field: "value" | "lang", val: string) => {
- setEditDefinitions((prev) => ensureTrailingEmpty(prev.map((d, i) => (i === index ? { ...d, [field]: val } : d))));
+ setEditDefinitions((prev) => ensureTrailingPlaceholder(prev.map((d, i) => (i === index ? { ...d, [field]: val } : d)), "single-per-lang"));
}, []);
const removeDefinition = useCallback((index: number) => {
- setEditDefinitions((prev) => ensureTrailingEmpty(prev.filter((_, i) => i !== index)));
+ setEditDefinitions((prev) => ensureTrailingPlaceholder(prev.filter((_, i) => i !== index), "single-per-lang"));
requestAnimationFrame(() => triggerSave());
}, [triggerSave]);
@@ -344,7 +338,7 @@ export function PropertyDetailPanel({
prev.map((a) => {
if (a.property_iri !== propertyIri) return a;
const updated = a.values.map((v, vi) => (vi === valueIdx ? { ...v, [field]: val } : v));
- return { ...a, values: ensureTrailingEmpty(updated) };
+ return { ...a, values: ensureTrailingPlaceholder(updated, getAnnotationCardinality(propertyIri)) };
})
);
},
@@ -357,7 +351,7 @@ export function PropertyDetailPanel({
prev.map((a) => {
if (a.property_iri !== propertyIri) return a;
const filtered = a.values.filter((_, vi) => vi !== valueIdx);
- return { ...a, values: ensureTrailingEmpty(filtered) };
+ return { ...a, values: ensureTrailingPlaceholder(filtered, getAnnotationCardinality(propertyIri)) };
})
);
requestAnimationFrame(() => triggerSave());
@@ -500,7 +494,7 @@ export function PropertyDetailPanel({
{editLabels.map((label, index) => (
updateLabel(index, "value", e.target.value)} onBlur={() => triggerSave()} placeholder="Label text" className="flex-1 rounded-md border border-slate-300 bg-white px-2.5 py-1.5 text-sm focus:border-primary-500 focus:outline-hidden focus:ring-1 focus:ring-primary-500 dark:border-slate-600 dark:bg-slate-700 dark:text-white" />
-
{ updateLabel(index, "lang", code); triggerSave(); }} />
+ { updateLabel(index, "lang", code); triggerSave(); }} />
{editLabels.length > 1 ? (
) : (
@@ -530,6 +524,7 @@ export function PropertyDetailPanel({
updateDefinition(index, "value", v)}
@@ -719,6 +714,7 @@ export function PropertyDetailPanel({
updateAnnotationValue(ann.property_iri, vi, "value", val)}
diff --git a/components/editor/standard/AnnotationRow.tsx b/components/editor/standard/AnnotationRow.tsx
index 415e906a..fceeb4ad 100644
--- a/components/editor/standard/AnnotationRow.tsx
+++ b/components/editor/standard/AnnotationRow.tsx
@@ -17,6 +17,11 @@ interface AnnotationRowProps {
showPropertyLabel?: boolean;
/** Custom placeholder for the value input (defaults to "Value") */
placeholder?: string;
+ /**
+ * Language tags to hide from this row's picker. Set for `single-per-lang`
+ * annotations so a language that already has a value can't be picked twice.
+ */
+ excludeLangs?: readonly string[];
}
export function AnnotationRow({
@@ -29,6 +34,7 @@ export function AnnotationRow({
onBlur,
showPropertyLabel = true,
placeholder = "Value",
+ excludeLangs,
}: AnnotationRowProps) {
const { displayLabel, curie } = getAnnotationPropertyInfo(propertyIri);
const isLongValue = value.length > 80;
@@ -65,6 +71,7 @@ export function AnnotationRow({
/>
)}
{
onLangChange(code);
diff --git a/lib/ontology/annotationCardinality.ts b/lib/ontology/annotationCardinality.ts
new file mode 100644
index 00000000..c8e0d750
--- /dev/null
+++ b/lib/ontology/annotationCardinality.ts
@@ -0,0 +1,59 @@
+import type { LocalizedString } from "@/lib/api/client";
+import type { AnnotationCardinality } from "./annotationProperties";
+
+/**
+ * Ensures that an array of localized strings has an appropriate trailing
+ * placeholder row, respecting the annotation property's cardinality:
+ *
+ * - `"single"`: no placeholder once a value is filled; one empty row when empty.
+ * - `"single-per-lang"`: one trailing empty row, but with no preset language —
+ * the user picks from a list that excludes the languages already present
+ * (see {@link usedLanguages} and `LanguagePicker`'s `excludeCodes`). Only when
+ * nothing is filled yet does the row default to `en`, since there is nothing
+ * to collide with.
+ * - `"multiple"`: always one trailing empty row defaulting to `en`.
+ */
+export function ensureTrailingPlaceholder(
+ values: LocalizedString[],
+ cardinality: AnnotationCardinality,
+): LocalizedString[] {
+ const hasFilled = values.some((v) => v.value.trim() !== "");
+
+ switch (cardinality) {
+ case "single":
+ // At most one value: drop stray empty rows once something is filled.
+ return hasFilled ? values.filter((v) => v.value.trim() !== "") : [{ value: "", lang: "en" }];
+
+ case "single-per-lang":
+ if (values.length > 0 && values[values.length - 1].value.trim() === "") return values;
+ // Blank lang forces an explicit pick from the filtered picker, so the
+ // user can't silently re-use a language that already has a value.
+ return [...values, { value: "", lang: hasFilled ? "" : "en" }];
+
+ case "multiple":
+ default:
+ if (values.length === 0 || values[values.length - 1].value.trim() !== "") {
+ return [...values, { value: "", lang: "en" }];
+ }
+ return values;
+ }
+}
+
+/**
+ * Language tags already carrying a value in `values`, normalized to lowercase.
+ *
+ * Passed to `LanguagePicker`'s `excludeCodes` for `single-per-lang` annotations
+ * so the picker can't offer a language that would violate the cardinality
+ * constraint. Returns an empty array for other cardinalities — `multiple`
+ * annotations show the unfiltered language list.
+ */
+export function usedLanguages(
+ values: LocalizedString[],
+ cardinality: AnnotationCardinality,
+): string[] {
+ if (cardinality !== "single-per-lang") return [];
+ return values
+ .filter((v) => v.value.trim() !== "")
+ .map((v) => v.lang.trim().toLowerCase())
+ .filter(Boolean);
+}
diff --git a/lib/ontology/annotationProperties.ts b/lib/ontology/annotationProperties.ts
index 24006cc1..e3f10085 100644
--- a/lib/ontology/annotationProperties.ts
+++ b/lib/ontology/annotationProperties.ts
@@ -7,67 +7,75 @@
* - `curie`: prefixed name (e.g., "skos:prefLabel") — shown in tooltips
* - `displayLabel`: plain-language name (e.g., "Preferred Label") — shown in UI
* - `vocabulary`: grouping label for the picker
+ * - `cardinality`: how many values the property allows
*/
+/** How many values an annotation property accepts. */
+export type AnnotationCardinality =
+ | "single" // at most one value (e.g. dcterms:created)
+ | "single-per-lang" // at most one value per language tag (e.g. skos:prefLabel, SKOS S14)
+ | "multiple"; // unbounded (e.g. skos:altLabel, rdfs:comment)
+
export interface KnownAnnotationProperty {
iri: string;
curie: string;
displayLabel: string;
vocabulary: string;
+ cardinality: AnnotationCardinality;
}
export const ANNOTATION_PROPERTIES: KnownAnnotationProperty[] = [
- // ── DC Elements 1.1 ──
- { iri: "http://purl.org/dc/elements/1.1/contributor", curie: "dc:contributor", displayLabel: "Contributor", vocabulary: "DC Elements" },
- { iri: "http://purl.org/dc/elements/1.1/coverage", curie: "dc:coverage", displayLabel: "Coverage", vocabulary: "DC Elements" },
- { iri: "http://purl.org/dc/elements/1.1/creator", curie: "dc:creator", displayLabel: "Creator", vocabulary: "DC Elements" },
- { iri: "http://purl.org/dc/elements/1.1/date", curie: "dc:date", displayLabel: "Date", vocabulary: "DC Elements" },
- { iri: "http://purl.org/dc/elements/1.1/description", curie: "dc:description", displayLabel: "Description", vocabulary: "DC Elements" },
- { iri: "http://purl.org/dc/elements/1.1/format", curie: "dc:format", displayLabel: "Format", vocabulary: "DC Elements" },
- { iri: "http://purl.org/dc/elements/1.1/identifier", curie: "dc:identifier", displayLabel: "Identifier", vocabulary: "DC Elements" },
- { iri: "http://purl.org/dc/elements/1.1/language", curie: "dc:language", displayLabel: "Language", vocabulary: "DC Elements" },
- { iri: "http://purl.org/dc/elements/1.1/publisher", curie: "dc:publisher", displayLabel: "Publisher", vocabulary: "DC Elements" },
- { iri: "http://purl.org/dc/elements/1.1/relation", curie: "dc:relation", displayLabel: "Relation", vocabulary: "DC Elements" },
- { iri: "http://purl.org/dc/elements/1.1/rights", curie: "dc:rights", displayLabel: "Rights", vocabulary: "DC Elements" },
- { iri: "http://purl.org/dc/elements/1.1/source", curie: "dc:source", displayLabel: "Source", vocabulary: "DC Elements" },
- { iri: "http://purl.org/dc/elements/1.1/subject", curie: "dc:subject", displayLabel: "Subject", vocabulary: "DC Elements" },
- { iri: "http://purl.org/dc/elements/1.1/title", curie: "dc:title", displayLabel: "Title", vocabulary: "DC Elements" },
- { iri: "http://purl.org/dc/elements/1.1/type", curie: "dc:type", displayLabel: "Type", vocabulary: "DC Elements" },
+ // ── DC Elements 1.1 — legacy, all multi-valued ──
+ { iri: "http://purl.org/dc/elements/1.1/contributor", curie: "dc:contributor", displayLabel: "Contributor", vocabulary: "DC Elements", cardinality: "multiple" },
+ { iri: "http://purl.org/dc/elements/1.1/coverage", curie: "dc:coverage", displayLabel: "Coverage", vocabulary: "DC Elements", cardinality: "multiple" },
+ { iri: "http://purl.org/dc/elements/1.1/creator", curie: "dc:creator", displayLabel: "Creator", vocabulary: "DC Elements", cardinality: "multiple" },
+ { iri: "http://purl.org/dc/elements/1.1/date", curie: "dc:date", displayLabel: "Date", vocabulary: "DC Elements", cardinality: "multiple" },
+ { iri: "http://purl.org/dc/elements/1.1/description", curie: "dc:description", displayLabel: "Description", vocabulary: "DC Elements", cardinality: "multiple" },
+ { iri: "http://purl.org/dc/elements/1.1/format", curie: "dc:format", displayLabel: "Format", vocabulary: "DC Elements", cardinality: "multiple" },
+ { iri: "http://purl.org/dc/elements/1.1/identifier", curie: "dc:identifier", displayLabel: "Identifier", vocabulary: "DC Elements", cardinality: "multiple" },
+ { iri: "http://purl.org/dc/elements/1.1/language", curie: "dc:language", displayLabel: "Language", vocabulary: "DC Elements", cardinality: "multiple" },
+ { iri: "http://purl.org/dc/elements/1.1/publisher", curie: "dc:publisher", displayLabel: "Publisher", vocabulary: "DC Elements", cardinality: "multiple" },
+ { iri: "http://purl.org/dc/elements/1.1/relation", curie: "dc:relation", displayLabel: "Relation", vocabulary: "DC Elements", cardinality: "multiple" },
+ { iri: "http://purl.org/dc/elements/1.1/rights", curie: "dc:rights", displayLabel: "Rights", vocabulary: "DC Elements", cardinality: "multiple" },
+ { iri: "http://purl.org/dc/elements/1.1/source", curie: "dc:source", displayLabel: "Source", vocabulary: "DC Elements", cardinality: "multiple" },
+ { iri: "http://purl.org/dc/elements/1.1/subject", curie: "dc:subject", displayLabel: "Subject", vocabulary: "DC Elements", cardinality: "multiple" },
+ { iri: "http://purl.org/dc/elements/1.1/title", curie: "dc:title", displayLabel: "Title", vocabulary: "DC Elements", cardinality: "multiple" },
+ { iri: "http://purl.org/dc/elements/1.1/type", curie: "dc:type", displayLabel: "Type", vocabulary: "DC Elements", cardinality: "multiple" },
// ── DC Terms ──
- { iri: "http://purl.org/dc/terms/contributor", curie: "dcterms:contributor", displayLabel: "Contributor", vocabulary: "DC Terms" },
- { iri: "http://purl.org/dc/terms/created", curie: "dcterms:created", displayLabel: "Date Created", vocabulary: "DC Terms" },
- { iri: "http://purl.org/dc/terms/creator", curie: "dcterms:creator", displayLabel: "Creator", vocabulary: "DC Terms" },
- { iri: "http://purl.org/dc/terms/date", curie: "dcterms:date", displayLabel: "Date", vocabulary: "DC Terms" },
- { iri: "http://purl.org/dc/terms/description", curie: "dcterms:description", displayLabel: "Description", vocabulary: "DC Terms" },
- { iri: "http://purl.org/dc/terms/format", curie: "dcterms:format", displayLabel: "Format", vocabulary: "DC Terms" },
- { iri: "http://purl.org/dc/terms/identifier", curie: "dcterms:identifier", displayLabel: "Identifier", vocabulary: "DC Terms" },
- { iri: "http://purl.org/dc/terms/language", curie: "dcterms:language", displayLabel: "Language", vocabulary: "DC Terms" },
- { iri: "http://purl.org/dc/terms/license", curie: "dcterms:license", displayLabel: "License", vocabulary: "DC Terms" },
- { iri: "http://purl.org/dc/terms/modified", curie: "dcterms:modified", displayLabel: "Date Modified", vocabulary: "DC Terms" },
- { iri: "http://purl.org/dc/terms/publisher", curie: "dcterms:publisher", displayLabel: "Publisher", vocabulary: "DC Terms" },
- { iri: "http://purl.org/dc/terms/rights", curie: "dcterms:rights", displayLabel: "Rights", vocabulary: "DC Terms" },
- { iri: "http://purl.org/dc/terms/source", curie: "dcterms:source", displayLabel: "Source", vocabulary: "DC Terms" },
- { iri: "http://purl.org/dc/terms/subject", curie: "dcterms:subject", displayLabel: "Subject", vocabulary: "DC Terms" },
- { iri: "http://purl.org/dc/terms/title", curie: "dcterms:title", displayLabel: "Title", vocabulary: "DC Terms" },
- { iri: "http://purl.org/dc/terms/type", curie: "dcterms:type", displayLabel: "Type", vocabulary: "DC Terms" },
- { iri: "http://purl.org/dc/terms/abstract", curie: "dcterms:abstract", displayLabel: "Abstract", vocabulary: "DC Terms" },
+ { iri: "http://purl.org/dc/terms/contributor", curie: "dcterms:contributor", displayLabel: "Contributor", vocabulary: "DC Terms", cardinality: "multiple" },
+ { iri: "http://purl.org/dc/terms/created", curie: "dcterms:created", displayLabel: "Date Created", vocabulary: "DC Terms", cardinality: "single" },
+ { iri: "http://purl.org/dc/terms/creator", curie: "dcterms:creator", displayLabel: "Creator", vocabulary: "DC Terms", cardinality: "multiple" },
+ { iri: "http://purl.org/dc/terms/date", curie: "dcterms:date", displayLabel: "Date", vocabulary: "DC Terms", cardinality: "multiple" },
+ { iri: "http://purl.org/dc/terms/description", curie: "dcterms:description", displayLabel: "Description", vocabulary: "DC Terms", cardinality: "multiple" },
+ { iri: "http://purl.org/dc/terms/format", curie: "dcterms:format", displayLabel: "Format", vocabulary: "DC Terms", cardinality: "multiple" },
+ { iri: "http://purl.org/dc/terms/identifier", curie: "dcterms:identifier", displayLabel: "Identifier", vocabulary: "DC Terms", cardinality: "single" },
+ { iri: "http://purl.org/dc/terms/language", curie: "dcterms:language", displayLabel: "Language", vocabulary: "DC Terms", cardinality: "multiple" },
+ { iri: "http://purl.org/dc/terms/license", curie: "dcterms:license", displayLabel: "License", vocabulary: "DC Terms", cardinality: "single" },
+ { iri: "http://purl.org/dc/terms/modified", curie: "dcterms:modified", displayLabel: "Date Modified", vocabulary: "DC Terms", cardinality: "single" },
+ { iri: "http://purl.org/dc/terms/publisher", curie: "dcterms:publisher", displayLabel: "Publisher", vocabulary: "DC Terms", cardinality: "multiple" },
+ { iri: "http://purl.org/dc/terms/rights", curie: "dcterms:rights", displayLabel: "Rights", vocabulary: "DC Terms", cardinality: "multiple" },
+ { iri: "http://purl.org/dc/terms/source", curie: "dcterms:source", displayLabel: "Source", vocabulary: "DC Terms", cardinality: "multiple" },
+ { iri: "http://purl.org/dc/terms/subject", curie: "dcterms:subject", displayLabel: "Subject", vocabulary: "DC Terms", cardinality: "multiple" },
+ { iri: "http://purl.org/dc/terms/title", curie: "dcterms:title", displayLabel: "Title", vocabulary: "DC Terms", cardinality: "single" },
+ { iri: "http://purl.org/dc/terms/type", curie: "dcterms:type", displayLabel: "Type", vocabulary: "DC Terms", cardinality: "multiple" },
+ { iri: "http://purl.org/dc/terms/abstract", curie: "dcterms:abstract", displayLabel: "Abstract", vocabulary: "DC Terms", cardinality: "multiple" },
// ── SKOS ──
- { iri: "http://www.w3.org/2004/02/skos/core#prefLabel", curie: "skos:prefLabel", displayLabel: "Preferred Label", vocabulary: "SKOS" },
- { iri: "http://www.w3.org/2004/02/skos/core#altLabel", curie: "skos:altLabel", displayLabel: "Synonym(s)", vocabulary: "SKOS" },
- { iri: "http://www.w3.org/2004/02/skos/core#hiddenLabel", curie: "skos:hiddenLabel", displayLabel: "Hidden Label", vocabulary: "SKOS" },
- { iri: "http://www.w3.org/2004/02/skos/core#definition", curie: "skos:definition", displayLabel: "Definition", vocabulary: "SKOS" },
- { iri: "http://www.w3.org/2004/02/skos/core#example", curie: "skos:example", displayLabel: "Example(s)", vocabulary: "SKOS" },
- { iri: "http://www.w3.org/2004/02/skos/core#scopeNote", curie: "skos:scopeNote", displayLabel: "Scope Note", vocabulary: "SKOS" },
- { iri: "http://www.w3.org/2004/02/skos/core#editorialNote", curie: "skos:editorialNote", displayLabel: "Editorial Note", vocabulary: "SKOS" },
- { iri: "http://www.w3.org/2004/02/skos/core#historyNote", curie: "skos:historyNote", displayLabel: "History Note", vocabulary: "SKOS" },
- { iri: "http://www.w3.org/2004/02/skos/core#changeNote", curie: "skos:changeNote", displayLabel: "Change Note", vocabulary: "SKOS" },
- { iri: "http://www.w3.org/2004/02/skos/core#notation", curie: "skos:notation", displayLabel: "Notation", vocabulary: "SKOS" },
+ { iri: "http://www.w3.org/2004/02/skos/core#prefLabel", curie: "skos:prefLabel", displayLabel: "Preferred Label", vocabulary: "SKOS", cardinality: "single-per-lang" },
+ { iri: "http://www.w3.org/2004/02/skos/core#altLabel", curie: "skos:altLabel", displayLabel: "Synonym(s)", vocabulary: "SKOS", cardinality: "multiple" },
+ { iri: "http://www.w3.org/2004/02/skos/core#hiddenLabel", curie: "skos:hiddenLabel", displayLabel: "Hidden Label", vocabulary: "SKOS", cardinality: "multiple" },
+ { iri: "http://www.w3.org/2004/02/skos/core#definition", curie: "skos:definition", displayLabel: "Definition", vocabulary: "SKOS", cardinality: "single-per-lang" },
+ { iri: "http://www.w3.org/2004/02/skos/core#example", curie: "skos:example", displayLabel: "Example(s)", vocabulary: "SKOS", cardinality: "multiple" },
+ { iri: "http://www.w3.org/2004/02/skos/core#scopeNote", curie: "skos:scopeNote", displayLabel: "Scope Note", vocabulary: "SKOS", cardinality: "multiple" },
+ { iri: "http://www.w3.org/2004/02/skos/core#editorialNote", curie: "skos:editorialNote", displayLabel: "Editorial Note", vocabulary: "SKOS", cardinality: "multiple" },
+ { iri: "http://www.w3.org/2004/02/skos/core#historyNote", curie: "skos:historyNote", displayLabel: "History Note", vocabulary: "SKOS", cardinality: "multiple" },
+ { iri: "http://www.w3.org/2004/02/skos/core#changeNote", curie: "skos:changeNote", displayLabel: "Change Note", vocabulary: "SKOS", cardinality: "multiple" },
+ { iri: "http://www.w3.org/2004/02/skos/core#notation", curie: "skos:notation", displayLabel: "Notation", vocabulary: "SKOS", cardinality: "single" },
// ── RDFS / OWL ──
- { iri: "http://www.w3.org/2000/01/rdf-schema#seeAlso", curie: "rdfs:seeAlso", displayLabel: "See Also", vocabulary: "RDFS" },
- { iri: "http://www.w3.org/2000/01/rdf-schema#isDefinedBy", curie: "rdfs:isDefinedBy", displayLabel: "Defined By", vocabulary: "RDFS" },
+ { iri: "http://www.w3.org/2000/01/rdf-schema#seeAlso", curie: "rdfs:seeAlso", displayLabel: "See Also", vocabulary: "RDFS", cardinality: "multiple" },
+ { iri: "http://www.w3.org/2000/01/rdf-schema#isDefinedBy", curie: "rdfs:isDefinedBy", displayLabel: "Defined By", vocabulary: "RDFS", cardinality: "multiple" },
];
/** Well-known IRIs excluded from the general "Annotations" section (shown in their own sections) */
@@ -108,3 +116,20 @@ export function getAnnotationPropertiesByVocabulary(): Record = {
+ [LABEL_IRI]: "single-per-lang",
+ [COMMENT_IRI]: "multiple",
+};
+
+/**
+ * Returns the cardinality for an annotation property IRI.
+ * Falls back to "multiple" for unknown IRIs — the safe default that never
+ * artificially restricts user-defined annotation properties.
+ */
+export function getAnnotationCardinality(iri: string): AnnotationCardinality {
+ const found = ANNOTATION_PROPERTIES.find((p) => p.iri === iri);
+ if (found) return found.cardinality;
+ return EXTRA_CARDINALITIES[iri] ?? "multiple";
+}