@bitcobblers/wod-wiki-ui 0.37.2137 → 0.37.2140

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.
@@ -1035,6 +1035,8 @@ var embedPreviewDecorations = [
1035
1035
  embedPreviewField
1036
1036
  ];
1037
1037
  var frontmatterSuggestions = state.Facet.define({ combine: (providers) => providers[0] ?? null });
1038
+ var frontmatterReferenceIndex = state.Facet.define({ combine: (providers) => providers[0] ?? null });
1039
+ var referenceIndexReload = state.StateEffect.define();
1038
1040
  var frontmatterVisibility = state.Facet.define({
1039
1041
  combine: (values2) => values2[0] ?? true
1040
1042
  });
@@ -1044,7 +1046,7 @@ var suggestionId = 0;
1044
1046
  function isTagProperty(key) {
1045
1047
  return ["tag", "tags", "category"].includes(key.trim().toLowerCase());
1046
1048
  }
1047
- function attachSuggestions(input, view, root, kind, key, excluded, commit) {
1049
+ function attachSuggestions(input, view, root, kind, key, excluded, commit, valueKind = "tags") {
1048
1050
  const provider = view.state.facet(frontmatterSuggestions);
1049
1051
  if (!provider || !input.parentElement) return;
1050
1052
  const parent = input.parentElement;
@@ -1070,8 +1072,8 @@ function attachSuggestions(input, view, root, kind, key, excluded, commit) {
1070
1072
  input.setAttribute("aria-expanded", "false");
1071
1073
  input.removeAttribute("aria-activedescendant");
1072
1074
  };
1073
- const select = (label) => {
1074
- input.value = label;
1075
+ const select = (choice) => {
1076
+ input.value = choice.value;
1075
1077
  close();
1076
1078
  commit();
1077
1079
  };
@@ -1094,21 +1096,35 @@ function attachSuggestions(input, view, root, kind, key, excluded, commit) {
1094
1096
  try {
1095
1097
  const catalog = await provider();
1096
1098
  if (request !== generation || !input.isConnected || document.activeElement !== input) return;
1097
- const candidates = kind === "key" ? catalog.types : catalog.tags.filter((tag) => isTagProperty(key) || tag.type?.toLowerCase() === key.trim().toLowerCase()).map((tag) => tag.label);
1099
+ let candidates;
1100
+ if (kind === "key") {
1101
+ candidates = catalog.types.map((name) => ({ value: name, label: name, haystack: name }));
1102
+ } else if (valueKind === "effort" || valueKind === "equipment") {
1103
+ const entries = valueKind === "effort" ? catalog.efforts ?? [] : catalog.equipment ?? [];
1104
+ candidates = entries.map((entry) => ({
1105
+ value: entry.slug,
1106
+ label: `${entry.label} (${entry.slug})`,
1107
+ haystack: [entry.label, entry.slug, ...entry.aliases].join(" ").toLowerCase()
1108
+ }));
1109
+ } else {
1110
+ candidates = catalog.tags.filter((tag) => isTagProperty(key) || tag.type?.toLowerCase() === key.trim().toLowerCase()).map((tag) => ({ value: tag.label, label: tag.label, haystack: tag.label.toLowerCase() }));
1111
+ }
1098
1112
  const selected = new Set(excluded.map((label) => label.toLowerCase()));
1099
- choices = [...new Set(candidates)].filter((label) => label.toLowerCase().includes(query) && !selected.has(label.toLowerCase()));
1113
+ choices = candidates.filter(
1114
+ (candidate) => (query === "" || candidate.haystack.includes(query)) && !selected.has(candidate.value.toLowerCase())
1115
+ );
1100
1116
  active = -1;
1101
1117
  input.removeAttribute("aria-activedescendant");
1102
1118
  list.replaceChildren();
1103
- choices.forEach((label, index) => {
1119
+ choices.forEach((choice, index) => {
1104
1120
  const option = document.createElement("div");
1105
1121
  option.id = `${list.id}-${index}`;
1106
1122
  option.setAttribute("role", "option");
1107
1123
  option.setAttribute("aria-selected", "false");
1108
1124
  option.className = "cursor-pointer px-2 py-1.5 hover:bg-muted";
1109
- option.textContent = label;
1125
+ option.textContent = choice.label;
1110
1126
  option.addEventListener("mousedown", (event) => event.preventDefault());
1111
- option.addEventListener("click", () => select(label));
1127
+ option.addEventListener("click", () => select(choice));
1112
1128
  list.appendChild(option);
1113
1129
  });
1114
1130
  list.hidden = choices.length === 0;
@@ -1151,45 +1167,66 @@ function parseTagList(val) {
1151
1167
  }
1152
1168
  return [];
1153
1169
  }
1154
- function updateFrontmatter(view, sectionId, meta) {
1170
+ function patchPropertyInView(view, _sectionId, path, value) {
1155
1171
  if (view.state.readOnly) return;
1156
- const { sections } = view.state.field(sectionField);
1157
- const section = sections.find((s) => s.id === sectionId && s.type === "frontmatter") || sections.find((s) => s.type === "frontmatter");
1158
- if (!section) {
1159
- const nextBody2 = wodWikiWql.serializeFrontmatter(meta);
1160
- view.dispatch({
1161
- changes: {
1162
- from: 0,
1163
- to: 0,
1164
- insert: `---
1165
- ${nextBody2}
1166
- ---
1167
-
1168
- `
1169
- }
1170
- });
1171
- return;
1172
- }
1173
- const nextBody = wodWikiWql.serializeFrontmatter(meta);
1174
- if (section.contentFrom !== void 0 && section.contentTo !== void 0) {
1175
- view.dispatch({
1176
- changes: {
1177
- from: section.contentFrom,
1178
- to: section.contentTo,
1179
- insert: nextBody
1180
- }
1181
- });
1182
- } else {
1183
- view.dispatch({
1184
- changes: {
1185
- from: section.from,
1186
- to: section.to,
1187
- insert: `---
1188
- ${nextBody}
1189
- ---`
1190
- }
1191
- });
1172
+ const wholeDoc = view.state.doc.toString();
1173
+ const next = wodWikiCore.patchFrontmatterProperty(wholeDoc, path, value);
1174
+ if (next === wholeDoc) return;
1175
+ view.dispatch({ changes: { from: 0, to: wholeDoc.length, insert: next } });
1176
+ }
1177
+ function renamePropertyInView(view, _sectionId, fromKey, toKey, value) {
1178
+ if (view.state.readOnly) return;
1179
+ const wholeDoc = view.state.doc.toString();
1180
+ const next = wodWikiCore.patchFrontmatterProperty(
1181
+ wodWikiCore.patchFrontmatterProperty(wholeDoc, [toKey], value),
1182
+ [fromKey],
1183
+ null
1184
+ );
1185
+ if (next === wholeDoc) return;
1186
+ view.dispatch({ changes: { from: 0, to: wholeDoc.length, insert: next } });
1187
+ }
1188
+ function detectDefinitionKind(meta) {
1189
+ const type = typeof meta.type === "string" ? meta.type.trim().toLowerCase() : "";
1190
+ if (type === "effort") return "effort";
1191
+ if (type === "equipment") return "equipment";
1192
+ if (meta.registrySource !== void 0 || meta.baseAttributes !== void 0) return "effort";
1193
+ return null;
1194
+ }
1195
+ function readBaseAttributes(body) {
1196
+ const out = {};
1197
+ let inBase = false;
1198
+ for (const line of body.split(/\r?\n/)) {
1199
+ if (/^baseAttributes:\s*(#.*)?$/.test(line)) {
1200
+ inBase = true;
1201
+ continue;
1202
+ }
1203
+ if (!inBase) continue;
1204
+ if (/^\S/.test(line)) break;
1205
+ const match = line.match(/^\s+([\w-]+):\s*(.*)$/);
1206
+ if (!match) continue;
1207
+ if (match[1] === "met" || match[1] === "discipline" || match[1] === "intensityTier") {
1208
+ out[match[1]] = unquoteYamlScalar(match[2].trim());
1209
+ }
1192
1210
  }
1211
+ return out;
1212
+ }
1213
+ function unquoteYamlScalar(val) {
1214
+ if (val.startsWith('"') && val.endsWith('"')) return val.slice(1, -1).replace(/\\(["\\])/g, "$1");
1215
+ if (val.startsWith("'") && val.endsWith("'")) return val.slice(1, -1).replace(/\\'/g, "'");
1216
+ return val;
1217
+ }
1218
+ function referenceValueKind(key, docKind) {
1219
+ const normalized = key.trim().toLowerCase();
1220
+ if (normalized === "metrics.effort") return "effort";
1221
+ if (normalized === "metrics.equipment" || normalized === "equipment") return "equipment";
1222
+ if (normalized === "aliases" && docKind === "equipment") return "equipment";
1223
+ if (normalized === "aliases" && docKind === "effort") return "effort";
1224
+ return "tags";
1225
+ }
1226
+ function isReferenceKey(key, docKind) {
1227
+ const normalized = key.trim().toLowerCase();
1228
+ if (normalized === "metrics.effort" || normalized === "metrics.equipment") return true;
1229
+ return normalized === "equipment" && docKind === "effort";
1193
1230
  }
1194
1231
  function createTagDraftRow(view, root, key, onCommit) {
1195
1232
  const row = document.createElement("div");
@@ -1244,6 +1281,117 @@ function createTagDraftRow(view, root, key, onCommit) {
1244
1281
  }));
1245
1282
  return row;
1246
1283
  }
1284
+ function ownsNestedBlock(body, key) {
1285
+ const lines = body.split(/\r?\n/);
1286
+ for (let i = 0; i < lines.length; i++) {
1287
+ if (!new RegExp(`^${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}:\\s*$`).test(lines[i])) continue;
1288
+ return i + 1 < lines.length && /^\s+\S/.test(lines[i + 1]);
1289
+ }
1290
+ return false;
1291
+ }
1292
+ function renderEffortValueDiagnostics(provider, token, onPick) {
1293
+ const trimmed = token.trim();
1294
+ if (!trimmed || provider.resolveEffort(trimmed)) return null;
1295
+ const diagnostics = provider.resolve(trimmed).diagnostics;
1296
+ const box = document.createElement("div");
1297
+ box.setAttribute("data-testid", "editor-ref-unknown");
1298
+ box.className = "w-full basis-full flex flex-wrap items-center gap-2 text-[10px] text-destructive";
1299
+ const message = document.createElement("span");
1300
+ message.textContent = diagnostics[0]?.message || `Unknown effort: ${trimmed}`;
1301
+ box.appendChild(message);
1302
+ for (const candidate of diagnostics[0]?.candidates ?? []) {
1303
+ const pickBtn = document.createElement("button");
1304
+ pickBtn.type = "button";
1305
+ pickBtn.textContent = `Use ${candidate}`;
1306
+ pickBtn.className = "underline hover:text-foreground cursor-pointer";
1307
+ pickBtn.addEventListener("click", (e) => {
1308
+ e.preventDefault();
1309
+ onPick(candidate);
1310
+ });
1311
+ box.appendChild(pickBtn);
1312
+ }
1313
+ const createLink = document.createElement("a");
1314
+ createLink.href = provider.createHref("effort");
1315
+ createLink.textContent = "Create page";
1316
+ createLink.className = "underline hover:text-foreground";
1317
+ box.appendChild(createLink);
1318
+ return box;
1319
+ }
1320
+ var METRIC_DRAFT_KINDS = {
1321
+ effort: { label: "Effort", path: "metrics.effort", kind: "effort" },
1322
+ equipment: { label: "Equipment", path: "metrics.equipment", kind: "equipment" },
1323
+ rpe: { label: "RPE", path: "metrics.rpe", kind: "number" }
1324
+ };
1325
+ function createMetricDraftRow(view, root, referenceProvider, commit) {
1326
+ const row = document.createElement("div");
1327
+ row.className = "flex items-center min-h-[34px] border-b border-dashed border-border/60 last:border-b-0 group hover:bg-muted/10 transition-colors max-[479px]:flex-col max-[479px]:items-stretch";
1328
+ row.setAttribute("data-frontmatter-metric-draft", "true");
1329
+ const select = document.createElement("select");
1330
+ select.setAttribute("aria-label", "Metric kind");
1331
+ select.className = "mx-3 my-1.5 h-7 w-28 shrink-0 rounded-md border border-input bg-background px-1.5 text-[11px] text-muted-foreground outline-none";
1332
+ for (const [value, spec] of Object.entries(METRIC_DRAFT_KINDS)) {
1333
+ const option = document.createElement("option");
1334
+ option.value = value;
1335
+ option.textContent = spec.label;
1336
+ select.appendChild(option);
1337
+ }
1338
+ row.appendChild(select);
1339
+ const valCol = document.createElement("div");
1340
+ valCol.className = "flex items-center flex-wrap gap-1.5 px-3 py-1.5 flex-1 min-w-0";
1341
+ const input = document.createElement("input");
1342
+ input.className = "flex-1 min-w-[70px] bg-transparent text-xs text-foreground outline-none px-1 py-0.5 placeholder:text-muted-foreground/40";
1343
+ input.placeholder = "Value\u2026";
1344
+ input.setAttribute("aria-label", "Metric value");
1345
+ valCol.appendChild(input);
1346
+ row.appendChild(valCol);
1347
+ let settled = false;
1348
+ const settle = (doCommit) => {
1349
+ if (settled) return;
1350
+ settled = true;
1351
+ const spec = METRIC_DRAFT_KINDS[select.value];
1352
+ const raw = input.value.trim();
1353
+ if (doCommit && spec && raw) {
1354
+ if (spec.kind === "equipment") commit([spec.path], [raw]);
1355
+ else if (spec.kind === "number") {
1356
+ const num = Number(raw);
1357
+ if (!Number.isNaN(num)) commit([spec.path], num);
1358
+ } else commit([spec.path], raw);
1359
+ }
1360
+ row.remove();
1361
+ view.requestMeasure();
1362
+ };
1363
+ const attachFor = () => {
1364
+ const spec = METRIC_DRAFT_KINDS[select.value];
1365
+ if (spec && (spec.kind === "effort" || spec.kind === "equipment") && referenceProvider) {
1366
+ attachSuggestions(input, view, root, "value", spec.path, [], () => settle(true), spec.kind);
1367
+ }
1368
+ };
1369
+ attachFor();
1370
+ select.addEventListener("change", attachFor);
1371
+ input.addEventListener("keydown", (e) => {
1372
+ if (e.defaultPrevented && e.key !== "Escape") return;
1373
+ if (e.key === "Escape") {
1374
+ e.preventDefault();
1375
+ settle(false);
1376
+ return;
1377
+ }
1378
+ if (e.key === "Enter") {
1379
+ e.preventDefault();
1380
+ settle(true);
1381
+ }
1382
+ });
1383
+ input.addEventListener("blur", () => queueMicrotask(() => {
1384
+ if (!input.isConnected) return;
1385
+ settle(input.value.trim().length > 0);
1386
+ }));
1387
+ select.addEventListener("keydown", (e) => {
1388
+ if (e.key === "Escape") {
1389
+ e.preventDefault();
1390
+ settle(false);
1391
+ }
1392
+ });
1393
+ return row;
1394
+ }
1247
1395
  function createTagIcon() {
1248
1396
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1249
1397
  svg.setAttribute("viewBox", "0 0 24 24");
@@ -1356,26 +1504,11 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
1356
1504
  sectionId;
1357
1505
  rawContent;
1358
1506
  readOnly;
1359
- constructor(sectionIdOrProps, rawContent, readOnly = false) {
1507
+ constructor(sectionId, rawContent, readOnly = false) {
1360
1508
  super();
1361
- if (typeof sectionIdOrProps === "string") {
1362
- this.sectionId = sectionIdOrProps;
1363
- this.rawContent = rawContent ?? "";
1364
- this.readOnly = readOnly;
1365
- } else {
1366
- this.sectionId = "legacy";
1367
- this.rawContent = wodWikiWql.serializeFrontmatter(sectionIdOrProps);
1368
- this.readOnly = readOnly;
1369
- }
1370
- }
1371
- get props() {
1372
- const body = this.rawContent.replace(/^---\r?\n?/, "").replace(/\r?\n?---\r?$/, "");
1373
- const meta = wodWikiWql.parseFrontmatterBody(body);
1374
- const result = {};
1375
- for (const [k, v] of Object.entries(meta)) {
1376
- result[k] = Array.isArray(v) ? v.join(", ") : String(v ?? "");
1377
- }
1378
- return result;
1509
+ this.sectionId = sectionId;
1510
+ this.rawContent = rawContent ?? "";
1511
+ this.readOnly = readOnly;
1379
1512
  }
1380
1513
  eq(other) {
1381
1514
  return this.sectionId === other.sectionId && this.rawContent === other.rawContent && this.readOnly === other.readOnly;
@@ -1397,6 +1530,9 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
1397
1530
  const body = this.rawContent.replace(/^---\r?\n?/, "").replace(/\r?\n?---\r?$/, "");
1398
1531
  const meta = wodWikiWql.parseFrontmatterBody(body);
1399
1532
  const entries = Object.entries(meta);
1533
+ const docKind = detectDefinitionKind(meta);
1534
+ const referenceProvider = view.state.facet(frontmatterReferenceIndex)?.() ?? null;
1535
+ const base = docKind === "effort" ? readBaseAttributes(body) : {};
1400
1536
  if (entries.length === 0) {
1401
1537
  const emptyRow = document.createElement("div");
1402
1538
  emptyRow.className = "px-3 py-2 text-xs text-muted-foreground/60 italic";
@@ -1405,13 +1541,16 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
1405
1541
  }
1406
1542
  for (const [key, value] of entries) {
1407
1543
  const isTags = isTagProperty(key);
1544
+ const valueKind = referenceValueKind(key, docKind);
1545
+ const isReferenceList = (isReferenceKey(key, docKind) || key === "aliases") && Array.isArray(value);
1546
+ const nested = value === "" && ownsNestedBlock(body, key);
1408
1547
  const row = document.createElement("div");
1409
1548
  row.className = "flex items-center min-h-[34px] border-b border-dashed border-border/60 last:border-b-0 group hover:bg-muted/10 transition-colors max-[479px]:flex-col max-[479px]:items-stretch";
1410
1549
  box.appendChild(row);
1411
1550
  const keyCol = document.createElement("div");
1412
1551
  keyCol.className = "flex items-center gap-2 px-3 py-1.5 w-36 shrink-0 border-r border-dashed border-border/60 text-muted-foreground max-[479px]:w-auto max-[479px]:border-r-0 max-[479px]:border-b";
1413
- keyCol.appendChild(isTags ? createTagIcon() : createListIcon());
1414
- if (this.readOnly) {
1552
+ keyCol.appendChild(isTags || isReferenceList ? createTagIcon() : createListIcon());
1553
+ if (this.readOnly || nested) {
1415
1554
  const keyLabel = document.createElement("span");
1416
1555
  keyLabel.className = "truncate font-medium text-foreground/90";
1417
1556
  keyLabel.textContent = key;
@@ -1433,15 +1572,10 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
1433
1572
  keyInput.value = currentKey;
1434
1573
  return;
1435
1574
  }
1436
- const newMeta = {};
1437
- for (const [k, v] of Object.entries(meta)) {
1438
- if (k === currentKey) newMeta[nextKey] = v;
1439
- else newMeta[k] = v;
1440
- }
1575
+ renamePropertyInView(view, this.sectionId, currentKey, nextKey, value);
1441
1576
  delete meta[currentKey];
1442
- meta[nextKey] = newMeta[nextKey];
1577
+ meta[nextKey] = value;
1443
1578
  currentKey = nextKey;
1444
- updateFrontmatter(view, this.sectionId, newMeta);
1445
1579
  };
1446
1580
  const commitKeyIfAttached = () => queueMicrotask(() => {
1447
1581
  if (keyInput.isConnected) commitKey();
@@ -1470,71 +1604,117 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
1470
1604
  row.appendChild(keyCol);
1471
1605
  const valCol = document.createElement("div");
1472
1606
  valCol.className = "flex items-center flex-wrap gap-1.5 px-3 py-1.5 flex-1 min-w-0";
1473
- if (isTags) {
1474
- const tags7 = parseTagList(value);
1475
- for (let idx = 0; idx < tags7.length; idx++) {
1476
- const tag = tags7[idx];
1607
+ const commitList = (next) => {
1608
+ meta[key] = next;
1609
+ patchPropertyInView(view, this.sectionId, [key], next.length > 0 ? next : "");
1610
+ };
1611
+ const refMatch = (token) => {
1612
+ if (!referenceProvider) return null;
1613
+ return valueKind === "effort" ? referenceProvider.resolveEffort(token) : valueKind === "equipment" ? referenceProvider.resolveEquipment(token) : null;
1614
+ };
1615
+ if (isTags || isReferenceList) {
1616
+ const chips = parseTagList(value);
1617
+ const chipIsReference = valueKind !== "tags";
1618
+ for (let idx = 0; idx < chips.length; idx++) {
1619
+ const chip = chips[idx];
1620
+ const match = chipIsReference ? refMatch(chip) : null;
1477
1621
  const pill = document.createElement("span");
1478
- pill.className = "inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-medium bg-amber-500/10 text-amber-800 dark:text-amber-300 border border-amber-500/20 dark:border-amber-500/30";
1479
- const tagText = document.createElement("span");
1480
- tagText.textContent = tag;
1481
- pill.appendChild(tagText);
1622
+ pill.className = match ? "inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-medium bg-muted/60 text-foreground border border-border/60" : chipIsReference ? "inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-medium bg-destructive/10 text-destructive border border-destructive/30" : "inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-medium bg-amber-500/10 text-amber-800 dark:text-amber-300 border border-amber-500/20 dark:border-amber-500/30";
1623
+ const pillText = document.createElement("span");
1624
+ pillText.textContent = match ? match.label : chip;
1625
+ pill.appendChild(pillText);
1626
+ if (match && referenceProvider) {
1627
+ const openLink = document.createElement("a");
1628
+ openLink.href = referenceProvider.openHref(valueKind, match.slug);
1629
+ openLink.title = `Open ${match.label} page`;
1630
+ openLink.setAttribute("aria-label", `Open ${match.label} page`);
1631
+ openLink.className = "inline-flex items-center justify-center text-muted-foreground/60 hover:text-foreground cursor-pointer";
1632
+ openLink.appendChild(createExternalIcon());
1633
+ pill.appendChild(openLink);
1634
+ }
1482
1635
  if (!this.readOnly) {
1483
1636
  const rmBtn = document.createElement("button");
1484
1637
  rmBtn.type = "button";
1485
- rmBtn.setAttribute("aria-label", `Remove tag ${tag}`);
1486
- rmBtn.className = "inline-flex items-center justify-center text-amber-700/60 hover:text-amber-950 dark:text-amber-400/60 dark:hover:text-amber-100 cursor-pointer";
1638
+ rmBtn.setAttribute("aria-label", isTags ? `Remove tag ${chip}` : `Remove ${chip}`);
1639
+ rmBtn.className = "inline-flex items-center justify-center text-muted-foreground/60 hover:text-foreground dark:hover:text-foreground cursor-pointer";
1487
1640
  rmBtn.appendChild(createCloseIcon("sm"));
1488
1641
  rmBtn.addEventListener("click", (e) => {
1489
1642
  e.preventDefault();
1490
1643
  e.stopPropagation();
1491
- const nextTags = tags7.filter((_, i) => i !== idx);
1492
- meta[key] = nextTags;
1493
- updateFrontmatter(view, this.sectionId, meta);
1644
+ commitList(chips.filter((_, i) => i !== idx));
1494
1645
  });
1495
1646
  pill.appendChild(rmBtn);
1496
1647
  }
1497
1648
  valCol.appendChild(pill);
1498
1649
  }
1499
1650
  if (!this.readOnly) {
1500
- const tagInput = document.createElement("input");
1501
- tagInput.className = "flex-1 min-w-[70px] bg-transparent text-xs text-foreground outline-none px-1 py-0.5 placeholder:text-muted-foreground/40";
1502
- tagInput.placeholder = tags7.length === 0 ? "Add tags\u2026" : "Add tag\u2026";
1503
- tagInput.setAttribute("aria-label", `Add tag to ${key}`);
1504
- const commitNewTag = () => {
1505
- const val = tagInput.value.trim().replace(/^,|,$/g, "");
1506
- if (val && !tags7.some((tag) => tag.toLowerCase() === val.toLowerCase())) {
1507
- meta[key] = [...tags7, val];
1508
- tagInput.value = "";
1651
+ const chipInput = document.createElement("input");
1652
+ chipInput.className = "flex-1 min-w-[70px] bg-transparent text-xs text-foreground outline-none px-1 py-0.5 placeholder:text-muted-foreground/40";
1653
+ chipInput.placeholder = chips.length === 0 ? isReferenceList ? `Add ${valueKind === "effort" ? "effort" : "equipment"}\u2026` : "Add tags\u2026" : isTags ? "Add tag\u2026" : "Add\u2026";
1654
+ chipInput.setAttribute("aria-label", isTags ? `Add tag to ${key}` : `Add to ${key}`);
1655
+ const commitNewChip = () => {
1656
+ const val = chipInput.value.trim().replace(/^,|,$/g, "");
1657
+ if (val && !chips.some((chip) => chip.toLowerCase() === val.toLowerCase())) {
1658
+ chipInput.value = "";
1509
1659
  pendingFocus.set(view, { kind: "tag", key });
1510
- updateFrontmatter(view, this.sectionId, meta);
1660
+ commitList([...chips, val]);
1511
1661
  }
1512
1662
  };
1513
- valCol.appendChild(tagInput);
1514
- attachSuggestions(tagInput, view, root, "value", key, tags7, commitNewTag);
1515
- tagInput.addEventListener("keydown", (e) => {
1663
+ valCol.appendChild(chipInput);
1664
+ attachSuggestions(chipInput, view, root, "value", key, chips, commitNewChip, valueKind);
1665
+ chipInput.addEventListener("keydown", (e) => {
1516
1666
  if (e.defaultPrevented) return;
1517
1667
  if (e.key === "Enter" || e.key === ",") {
1518
1668
  e.preventDefault();
1519
- commitNewTag();
1520
- } else if (e.key === "Backspace" && !tagInput.value && tags7.length > 0) {
1669
+ commitNewChip();
1670
+ } else if (e.key === "Backspace" && !chipInput.value && chips.length > 0) {
1521
1671
  e.preventDefault();
1522
- meta[key] = tags7.slice(0, -1);
1523
1672
  pendingFocus.set(view, { kind: "tag", key });
1524
- updateFrontmatter(view, this.sectionId, meta);
1673
+ commitList(chips.slice(0, -1));
1525
1674
  }
1526
1675
  });
1527
- tagInput.addEventListener("blur", () => queueMicrotask(() => {
1528
- if (tagInput.isConnected && tagInput.value.trim()) commitNewTag();
1676
+ chipInput.addEventListener("blur", () => queueMicrotask(() => {
1677
+ if (chipInput.isConnected && chipInput.value.trim()) commitNewChip();
1529
1678
  }));
1530
1679
  const focus = pendingFocus.get(view);
1531
1680
  if (focus?.kind === "tag" && focus.key === key) {
1532
1681
  setTimeout(() => {
1533
- if (tagInput.isConnected) tagInput.focus();
1682
+ if (chipInput.isConnected) chipInput.focus();
1534
1683
  }, 0);
1535
1684
  pendingFocus.delete(view);
1536
1685
  }
1686
+ if (chipIsReference && referenceProvider) {
1687
+ const unknown = chips.filter((chip) => !refMatch(chip));
1688
+ if (unknown.length > 0) {
1689
+ const diag = document.createElement("div");
1690
+ diag.setAttribute("data-testid", "editor-ref-unknown");
1691
+ diag.className = "w-full basis-full flex flex-wrap items-center gap-2 text-[10px] text-destructive";
1692
+ const label = document.createElement("span");
1693
+ label.textContent = `Unknown ${valueKind === "effort" ? "effort" : "equipment"}: ${unknown.join(", ")}`;
1694
+ diag.appendChild(label);
1695
+ const selectBtn = document.createElement("button");
1696
+ selectBtn.type = "button";
1697
+ selectBtn.textContent = "Select existing";
1698
+ selectBtn.className = "underline hover:text-foreground cursor-pointer";
1699
+ selectBtn.addEventListener("click", (e) => {
1700
+ e.preventDefault();
1701
+ chipInput.focus();
1702
+ });
1703
+ diag.appendChild(selectBtn);
1704
+ const createLink = document.createElement("a");
1705
+ createLink.href = referenceProvider.createHref(valueKind);
1706
+ createLink.textContent = "Create page";
1707
+ createLink.className = "underline hover:text-foreground";
1708
+ diag.appendChild(createLink);
1709
+ valCol.appendChild(diag);
1710
+ }
1711
+ }
1537
1712
  }
1713
+ } else if (nested) {
1714
+ const valSpan = document.createElement("span");
1715
+ valSpan.className = "text-xs text-muted-foreground/60 italic py-0.5 truncate";
1716
+ valSpan.textContent = "Nested block \u2014 edit in YAML";
1717
+ valCol.appendChild(valSpan);
1538
1718
  } else {
1539
1719
  const strVal = Array.isArray(value) ? value.join(", ") : String(value ?? "");
1540
1720
  if (this.readOnly) {
@@ -1554,8 +1734,9 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
1554
1734
  if (cur !== committedVal) {
1555
1735
  committedVal = cur;
1556
1736
  const num = Number(cur);
1557
- meta[key] = cur !== "" && !isNaN(num) ? num : cur;
1558
- updateFrontmatter(view, this.sectionId, meta);
1737
+ const next = cur !== "" && !isNaN(num) ? num : cur;
1738
+ meta[key] = next;
1739
+ patchPropertyInView(view, this.sectionId, [key], next);
1559
1740
  }
1560
1741
  };
1561
1742
  const commitValIfAttached = () => queueMicrotask(() => {
@@ -1563,7 +1744,16 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
1563
1744
  });
1564
1745
  valInput.addEventListener("change", commitValIfAttached);
1565
1746
  valCol.appendChild(valInput);
1566
- attachSuggestions(valInput, view, root, "value", key, [strVal], commitVal);
1747
+ attachSuggestions(
1748
+ valInput,
1749
+ view,
1750
+ root,
1751
+ "value",
1752
+ key,
1753
+ [strVal],
1754
+ commitVal,
1755
+ key.trim().toLowerCase() === "metrics.effort" ? "effort" : "tags"
1756
+ );
1567
1757
  valInput.addEventListener("blur", commitValIfAttached);
1568
1758
  valInput.addEventListener("keydown", (e) => {
1569
1759
  if (e.defaultPrevented) return;
@@ -1572,6 +1762,34 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
1572
1762
  valInput.blur();
1573
1763
  }
1574
1764
  });
1765
+ if (key.trim().toLowerCase() === "metrics.effort" && referenceProvider) {
1766
+ const match = referenceProvider.resolveEffort(strVal);
1767
+ if (match) {
1768
+ const pill = document.createElement("span");
1769
+ pill.className = "inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-medium bg-muted/60 text-foreground border border-border/60 shrink-0";
1770
+ const pillText = document.createElement("span");
1771
+ pillText.textContent = match.label;
1772
+ pill.appendChild(pillText);
1773
+ const openLink = document.createElement("a");
1774
+ openLink.href = referenceProvider.openHref("effort", match.slug);
1775
+ openLink.title = `Open ${match.label} page`;
1776
+ openLink.setAttribute("aria-label", `Open ${match.label} page`);
1777
+ openLink.className = "inline-flex items-center justify-center text-muted-foreground/60 hover:text-foreground cursor-pointer";
1778
+ openLink.appendChild(createExternalIcon());
1779
+ pill.appendChild(openLink);
1780
+ valCol.insertBefore(pill, valInput);
1781
+ valInput.className = "flex-1 min-w-[70px] bg-transparent text-xs text-foreground outline-none py-0.5 hover:bg-muted/20 focus:bg-background focus:ring-1 focus:ring-ring rounded px-1 -mx-1 transition-colors";
1782
+ }
1783
+ const diag = renderEffortValueDiagnostics(
1784
+ referenceProvider,
1785
+ strVal,
1786
+ (slug) => {
1787
+ meta[key] = slug;
1788
+ patchPropertyInView(view, this.sectionId, [key], slug);
1789
+ }
1790
+ );
1791
+ if (diag) valCol.appendChild(diag);
1792
+ }
1575
1793
  }
1576
1794
  if (/^https?:\/\/\S+$/i.test(strVal)) {
1577
1795
  const openLink = document.createElement("a");
@@ -1586,7 +1804,7 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
1586
1804
  }
1587
1805
  }
1588
1806
  row.appendChild(valCol);
1589
- if (!this.readOnly) {
1807
+ if (!this.readOnly && !nested) {
1590
1808
  const actionsCol = document.createElement("div");
1591
1809
  actionsCol.className = "flex items-center pr-2";
1592
1810
  const delBtn = document.createElement("button");
@@ -1597,14 +1815,16 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
1597
1815
  delBtn.addEventListener("click", (e) => {
1598
1816
  e.preventDefault();
1599
1817
  e.stopPropagation();
1600
- const newMeta = { ...meta };
1601
- delete newMeta[key];
1602
- updateFrontmatter(view, this.sectionId, newMeta);
1818
+ delete meta[key];
1819
+ patchPropertyInView(view, this.sectionId, [key], null);
1603
1820
  });
1604
1821
  actionsCol.appendChild(delBtn);
1605
1822
  row.appendChild(actionsCol);
1606
1823
  }
1607
1824
  }
1825
+ if (docKind === "effort" && !this.readOnly) {
1826
+ box.appendChild(this.buildDefinitionRows(view, base));
1827
+ }
1608
1828
  if (!this.readOnly) {
1609
1829
  const existingTagKey = entries.find(([key]) => isTagProperty(key))?.[0];
1610
1830
  const actionsRow = document.createElement("div");
@@ -1627,10 +1847,36 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
1627
1847
  newKey = `property_${count2}`;
1628
1848
  }
1629
1849
  pendingFocus.set(view, { kind: "key", key: newKey });
1630
- const newMeta = { ...meta, [newKey]: "" };
1631
- updateFrontmatter(view, this.sectionId, newMeta);
1850
+ patchPropertyInView(view, this.sectionId, [newKey], "");
1632
1851
  });
1633
1852
  actionsRow.appendChild(addBtn);
1853
+ {
1854
+ const addMetricBtn = document.createElement("button");
1855
+ addMetricBtn.type = "button";
1856
+ addMetricBtn.setAttribute("aria-label", "Add metric");
1857
+ addMetricBtn.setAttribute("data-testid", "editor-add-metric-note");
1858
+ addMetricBtn.className = "inline-flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground px-1 py-1 cursor-pointer transition-colors select-none";
1859
+ addMetricBtn.appendChild(createPlusIcon());
1860
+ const addMetricLabel = document.createElement("span");
1861
+ addMetricLabel.textContent = "Add metric";
1862
+ addMetricBtn.appendChild(addMetricLabel);
1863
+ addMetricBtn.addEventListener("click", (e) => {
1864
+ e.preventDefault();
1865
+ e.stopPropagation();
1866
+ const existing = root.querySelector("[data-frontmatter-metric-draft] input");
1867
+ if (existing) {
1868
+ existing.focus();
1869
+ return;
1870
+ }
1871
+ const draftRow = createMetricDraftRow(view, root, referenceProvider, (path, value) => {
1872
+ patchPropertyInView(view, this.sectionId, path, value);
1873
+ });
1874
+ box.appendChild(draftRow);
1875
+ draftRow.querySelector("input")?.focus();
1876
+ view.requestMeasure();
1877
+ });
1878
+ actionsRow.appendChild(addMetricBtn);
1879
+ }
1634
1880
  {
1635
1881
  const addTagBtn = document.createElement("button");
1636
1882
  addTagBtn.type = "button";
@@ -1655,7 +1901,7 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
1655
1901
  }
1656
1902
  const draftRow = createTagDraftRow(view, root, "tags", (val) => {
1657
1903
  pendingFocus.set(view, { kind: "tag", key: "tags" });
1658
- updateFrontmatter(view, this.sectionId, { ...meta, tags: [val] });
1904
+ patchPropertyInView(view, this.sectionId, ["tags"], [val]);
1659
1905
  });
1660
1906
  box.appendChild(draftRow);
1661
1907
  draftRow.querySelector("input")?.focus();
@@ -1667,6 +1913,7 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
1667
1913
  const editYamlBtn = document.createElement("button");
1668
1914
  editYamlBtn.type = "button";
1669
1915
  editYamlBtn.setAttribute("aria-label", "Edit YAML");
1916
+ editYamlBtn.setAttribute("data-testid", "editor-property-edit-yaml");
1670
1917
  editYamlBtn.className = "inline-flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground px-1 py-1 cursor-pointer transition-colors select-none";
1671
1918
  const editYamlLabel = document.createElement("span");
1672
1919
  editYamlLabel.textContent = "Edit YAML";
@@ -1686,6 +1933,82 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
1686
1933
  }
1687
1934
  return root;
1688
1935
  }
1936
+ /**
1937
+ * Effort-definition attribute rows (MET / discipline / intensity tier).
1938
+ * Values live in the nested baseAttributes block; each edit patches that
1939
+ * one path, so unknown sibling keys and comments elsewhere survive.
1940
+ */
1941
+ buildDefinitionRows(view, base) {
1942
+ const wrap = document.createElement("div");
1943
+ wrap.setAttribute("data-frontmatter-definition", "true");
1944
+ wrap.className = "border-t border-dashed border-border/60";
1945
+ const header = document.createElement("div");
1946
+ header.className = "px-3 pt-2 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground";
1947
+ header.textContent = "Definition";
1948
+ wrap.appendChild(header);
1949
+ const commitField = (field, raw) => {
1950
+ if (field === "met") {
1951
+ const num = Number(raw);
1952
+ patchPropertyInView(view, this.sectionId, ["baseAttributes", "met"], raw !== "" && !Number.isNaN(num) ? num : null);
1953
+ return;
1954
+ }
1955
+ patchPropertyInView(view, this.sectionId, ["baseAttributes", field], raw === "" ? null : raw);
1956
+ };
1957
+ const fieldRow = (label, child) => {
1958
+ const row = document.createElement("label");
1959
+ row.className = "flex items-center gap-2 px-3 py-1.5 min-h-[34px] border-b border-dashed border-border/60 last:border-b-0 max-[479px]:flex-col max-[479px]:items-stretch";
1960
+ const name = document.createElement("span");
1961
+ name.className = "w-28 shrink-0 text-muted-foreground";
1962
+ name.textContent = label;
1963
+ row.appendChild(name);
1964
+ child.className = "flex-1 min-w-0 bg-transparent text-xs text-foreground outline-none py-0.5";
1965
+ row.appendChild(child);
1966
+ wrap.appendChild(row);
1967
+ };
1968
+ const met = document.createElement("input");
1969
+ met.type = "number";
1970
+ met.step = "0.1";
1971
+ met.inputMode = "decimal";
1972
+ met.value = base.met ?? "";
1973
+ met.setAttribute("aria-label", "MET value");
1974
+ met.addEventListener("change", () => queueMicrotask(() => {
1975
+ if (met.isConnected) commitField("met", met.value.trim());
1976
+ }));
1977
+ fieldRow("MET", met);
1978
+ const discipline = document.createElement("select");
1979
+ discipline.setAttribute("aria-label", "Discipline");
1980
+ discipline.className = "flex-1 min-w-0 rounded-md border border-input bg-background px-1.5 text-[11px] text-muted-foreground outline-none";
1981
+ const unset = document.createElement("option");
1982
+ unset.value = "";
1983
+ unset.textContent = "Unset";
1984
+ discipline.appendChild(unset);
1985
+ for (const option of wodWikiLang.EFFORT_DISCIPLINES) {
1986
+ const opt = document.createElement("option");
1987
+ opt.value = option;
1988
+ opt.textContent = option;
1989
+ discipline.appendChild(opt);
1990
+ }
1991
+ discipline.value = base.discipline ?? "";
1992
+ discipline.addEventListener("change", () => commitField("discipline", discipline.value));
1993
+ fieldRow("Discipline", discipline);
1994
+ const tier = document.createElement("select");
1995
+ tier.setAttribute("aria-label", "Intensity tier");
1996
+ tier.className = "flex-1 min-w-0 rounded-md border border-input bg-background px-1.5 text-[11px] text-muted-foreground outline-none";
1997
+ const tierUnset = document.createElement("option");
1998
+ tierUnset.value = "";
1999
+ tierUnset.textContent = "Unset";
2000
+ tier.appendChild(tierUnset);
2001
+ for (const option of ["low", "moderate", "high"]) {
2002
+ const opt = document.createElement("option");
2003
+ opt.value = option;
2004
+ opt.textContent = option;
2005
+ tier.appendChild(opt);
2006
+ }
2007
+ tier.value = base.intensityTier ?? "";
2008
+ tier.addEventListener("change", () => commitField("intensityTier", tier.value));
2009
+ fieldRow("Intensity tier", tier);
2010
+ return wrap;
2011
+ }
1689
2012
  destroy(dom) {
1690
2013
  widgetCleanup.get(dom)?.forEach((close) => close());
1691
2014
  widgetCleanup.delete(dom);
@@ -1719,7 +2042,7 @@ var EmptyFrontmatterWidget = class extends view.WidgetType {
1719
2042
  e.preventDefault();
1720
2043
  e.stopPropagation();
1721
2044
  pendingFocus.set(view, { kind: "key", key: "property" });
1722
- updateFrontmatter(view, "new", { property: "" });
2045
+ patchPropertyInView(view, "new", ["property"], "");
1723
2046
  });
1724
2047
  actionsRow.appendChild(addBtn);
1725
2048
  const addTagBtn = document.createElement("button");
@@ -1740,7 +2063,7 @@ var EmptyFrontmatterWidget = class extends view.WidgetType {
1740
2063
  }
1741
2064
  const draftRow = createTagDraftRow(view, root, "tags", (val) => {
1742
2065
  pendingFocus.set(view, { kind: "tag", key: "tags" });
1743
- updateFrontmatter(view, "new", { tags: [val] });
2066
+ patchPropertyInView(view, "new", ["tags"], [val]);
1744
2067
  });
1745
2068
  root.appendChild(draftRow);
1746
2069
  draftRow.querySelector("input")?.focus();
@@ -1794,7 +2117,7 @@ var frontmatterPreviewField = state.StateField.define({
1794
2117
  return buildFrontmatterDecos(state);
1795
2118
  },
1796
2119
  update(deco, tr) {
1797
- if (tr.docChanged || tr.reconfigured || tr.selection) {
2120
+ if (tr.docChanged || tr.reconfigured || tr.selection || tr.effects.some((e) => e.is(referenceIndexReload))) {
1798
2121
  return buildFrontmatterDecos(tr.state);
1799
2122
  }
1800
2123
  return deco;
@@ -4288,7 +4611,8 @@ var SOURCE_OPTIONS = wodWikiWql.WQL_SOURCE_VALUES.map((s) => ({
4288
4611
  guides: "Static guide pages",
4289
4612
  playground: "Playground scratch pages",
4290
4613
  dashboards: "Dashboard notes",
4291
- efforts: "Exercise library notes"
4614
+ efforts: "Exercise library notes",
4615
+ equipment: "Equipment library notes"
4292
4616
  }[s]
4293
4617
  }));
4294
4618
  var PLANE_OPTIONS = wodWikiWql.WQL_RESULT_PLANES.map((v) => ({ value: v, label: v }));
@@ -12196,6 +12520,7 @@ exports.findWorkoutErrors = findWorkoutErrors;
12196
12520
  exports.forceSectionParse = forceSectionParse;
12197
12521
  exports.frontmatterPreview = frontmatterPreview;
12198
12522
  exports.frontmatterPreviewField = frontmatterPreviewField;
12523
+ exports.frontmatterReferenceIndex = frontmatterReferenceIndex;
12199
12524
  exports.frontmatterSuggestions = frontmatterSuggestions;
12200
12525
  exports.frontmatterVisibility = frontmatterVisibility;
12201
12526
  exports.getCursorFocusState = getCursorFocusState;
@@ -12212,6 +12537,7 @@ exports.markdownTablePreview = markdownTablePreview;
12212
12537
  exports.navigationFacet = navigationFacet;
12213
12538
  exports.previewDecorations = previewDecorations;
12214
12539
  exports.queryBlockPreview = queryBlockPreview;
12540
+ exports.referenceIndexReload = referenceIndexReload;
12215
12541
  exports.registerCustomCompletionSource = registerCustomCompletionSource;
12216
12542
  exports.registeredSectionWidgets = registeredSectionWidgets;
12217
12543
  exports.removeRuntimePanel = removeRuntimePanel;