@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.
package/dist/index.cjs CHANGED
@@ -2382,7 +2382,8 @@ var SOURCE_OPTIONS = wodWikiWql.WQL_SOURCE_VALUES.map((s) => ({
2382
2382
  guides: "Static guide pages",
2383
2383
  playground: "Playground scratch pages",
2384
2384
  dashboards: "Dashboard notes",
2385
- efforts: "Exercise library notes"
2385
+ efforts: "Exercise library notes",
2386
+ equipment: "Equipment library notes"
2386
2387
  }[s]
2387
2388
  }));
2388
2389
  var PLANE_OPTIONS = wodWikiWql.WQL_RESULT_PLANES.map((v) => ({ value: v, label: v }));
@@ -5475,6 +5476,8 @@ var embedPreviewDecorations = [
5475
5476
  embedPreviewField
5476
5477
  ];
5477
5478
  var frontmatterSuggestions = state.Facet.define({ combine: (providers) => providers[0] ?? null });
5479
+ var frontmatterReferenceIndex = state.Facet.define({ combine: (providers) => providers[0] ?? null });
5480
+ var referenceIndexReload = state.StateEffect.define();
5478
5481
  var frontmatterVisibility = state.Facet.define({
5479
5482
  combine: (values2) => values2[0] ?? true
5480
5483
  });
@@ -5484,7 +5487,7 @@ var suggestionId = 0;
5484
5487
  function isTagProperty(key) {
5485
5488
  return ["tag", "tags", "category"].includes(key.trim().toLowerCase());
5486
5489
  }
5487
- function attachSuggestions(input, view, root, kind, key, excluded, commit) {
5490
+ function attachSuggestions(input, view, root, kind, key, excluded, commit, valueKind = "tags") {
5488
5491
  const provider = view.state.facet(frontmatterSuggestions);
5489
5492
  if (!provider || !input.parentElement) return;
5490
5493
  const parent = input.parentElement;
@@ -5510,8 +5513,8 @@ function attachSuggestions(input, view, root, kind, key, excluded, commit) {
5510
5513
  input.setAttribute("aria-expanded", "false");
5511
5514
  input.removeAttribute("aria-activedescendant");
5512
5515
  };
5513
- const select = (label) => {
5514
- input.value = label;
5516
+ const select = (choice) => {
5517
+ input.value = choice.value;
5515
5518
  close();
5516
5519
  commit();
5517
5520
  };
@@ -5534,21 +5537,35 @@ function attachSuggestions(input, view, root, kind, key, excluded, commit) {
5534
5537
  try {
5535
5538
  const catalog = await provider();
5536
5539
  if (request !== generation || !input.isConnected || document.activeElement !== input) return;
5537
- const candidates = kind === "key" ? catalog.types : catalog.tags.filter((tag) => isTagProperty(key) || tag.type?.toLowerCase() === key.trim().toLowerCase()).map((tag) => tag.label);
5540
+ let candidates;
5541
+ if (kind === "key") {
5542
+ candidates = catalog.types.map((name) => ({ value: name, label: name, haystack: name }));
5543
+ } else if (valueKind === "effort" || valueKind === "equipment") {
5544
+ const entries = valueKind === "effort" ? catalog.efforts ?? [] : catalog.equipment ?? [];
5545
+ candidates = entries.map((entry) => ({
5546
+ value: entry.slug,
5547
+ label: `${entry.label} (${entry.slug})`,
5548
+ haystack: [entry.label, entry.slug, ...entry.aliases].join(" ").toLowerCase()
5549
+ }));
5550
+ } else {
5551
+ 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() }));
5552
+ }
5538
5553
  const selected = new Set(excluded.map((label) => label.toLowerCase()));
5539
- choices = [...new Set(candidates)].filter((label) => label.toLowerCase().includes(query) && !selected.has(label.toLowerCase()));
5554
+ choices = candidates.filter(
5555
+ (candidate) => (query === "" || candidate.haystack.includes(query)) && !selected.has(candidate.value.toLowerCase())
5556
+ );
5540
5557
  active = -1;
5541
5558
  input.removeAttribute("aria-activedescendant");
5542
5559
  list.replaceChildren();
5543
- choices.forEach((label, index) => {
5560
+ choices.forEach((choice, index) => {
5544
5561
  const option = document.createElement("div");
5545
5562
  option.id = `${list.id}-${index}`;
5546
5563
  option.setAttribute("role", "option");
5547
5564
  option.setAttribute("aria-selected", "false");
5548
5565
  option.className = "cursor-pointer px-2 py-1.5 hover:bg-muted";
5549
- option.textContent = label;
5566
+ option.textContent = choice.label;
5550
5567
  option.addEventListener("mousedown", (event) => event.preventDefault());
5551
- option.addEventListener("click", () => select(label));
5568
+ option.addEventListener("click", () => select(choice));
5552
5569
  list.appendChild(option);
5553
5570
  });
5554
5571
  list.hidden = choices.length === 0;
@@ -5591,45 +5608,66 @@ function parseTagList(val) {
5591
5608
  }
5592
5609
  return [];
5593
5610
  }
5594
- function updateFrontmatter(view, sectionId, meta) {
5611
+ function patchPropertyInView(view, _sectionId, path, value) {
5595
5612
  if (view.state.readOnly) return;
5596
- const { sections } = view.state.field(sectionField);
5597
- const section = sections.find((s) => s.id === sectionId && s.type === "frontmatter") || sections.find((s) => s.type === "frontmatter");
5598
- if (!section) {
5599
- const nextBody2 = wodWikiWql.serializeFrontmatter(meta);
5600
- view.dispatch({
5601
- changes: {
5602
- from: 0,
5603
- to: 0,
5604
- insert: `---
5605
- ${nextBody2}
5606
- ---
5607
-
5608
- `
5609
- }
5610
- });
5611
- return;
5612
- }
5613
- const nextBody = wodWikiWql.serializeFrontmatter(meta);
5614
- if (section.contentFrom !== void 0 && section.contentTo !== void 0) {
5615
- view.dispatch({
5616
- changes: {
5617
- from: section.contentFrom,
5618
- to: section.contentTo,
5619
- insert: nextBody
5620
- }
5621
- });
5622
- } else {
5623
- view.dispatch({
5624
- changes: {
5625
- from: section.from,
5626
- to: section.to,
5627
- insert: `---
5628
- ${nextBody}
5629
- ---`
5630
- }
5631
- });
5613
+ const wholeDoc = view.state.doc.toString();
5614
+ const next = wodWikiCore.patchFrontmatterProperty(wholeDoc, path, value);
5615
+ if (next === wholeDoc) return;
5616
+ view.dispatch({ changes: { from: 0, to: wholeDoc.length, insert: next } });
5617
+ }
5618
+ function renamePropertyInView(view, _sectionId, fromKey, toKey, value) {
5619
+ if (view.state.readOnly) return;
5620
+ const wholeDoc = view.state.doc.toString();
5621
+ const next = wodWikiCore.patchFrontmatterProperty(
5622
+ wodWikiCore.patchFrontmatterProperty(wholeDoc, [toKey], value),
5623
+ [fromKey],
5624
+ null
5625
+ );
5626
+ if (next === wholeDoc) return;
5627
+ view.dispatch({ changes: { from: 0, to: wholeDoc.length, insert: next } });
5628
+ }
5629
+ function detectDefinitionKind(meta) {
5630
+ const type = typeof meta.type === "string" ? meta.type.trim().toLowerCase() : "";
5631
+ if (type === "effort") return "effort";
5632
+ if (type === "equipment") return "equipment";
5633
+ if (meta.registrySource !== void 0 || meta.baseAttributes !== void 0) return "effort";
5634
+ return null;
5635
+ }
5636
+ function readBaseAttributes(body) {
5637
+ const out = {};
5638
+ let inBase = false;
5639
+ for (const line of body.split(/\r?\n/)) {
5640
+ if (/^baseAttributes:\s*(#.*)?$/.test(line)) {
5641
+ inBase = true;
5642
+ continue;
5643
+ }
5644
+ if (!inBase) continue;
5645
+ if (/^\S/.test(line)) break;
5646
+ const match = line.match(/^\s+([\w-]+):\s*(.*)$/);
5647
+ if (!match) continue;
5648
+ if (match[1] === "met" || match[1] === "discipline" || match[1] === "intensityTier") {
5649
+ out[match[1]] = unquoteYamlScalar(match[2].trim());
5650
+ }
5632
5651
  }
5652
+ return out;
5653
+ }
5654
+ function unquoteYamlScalar(val) {
5655
+ if (val.startsWith('"') && val.endsWith('"')) return val.slice(1, -1).replace(/\\(["\\])/g, "$1");
5656
+ if (val.startsWith("'") && val.endsWith("'")) return val.slice(1, -1).replace(/\\'/g, "'");
5657
+ return val;
5658
+ }
5659
+ function referenceValueKind(key, docKind) {
5660
+ const normalized = key.trim().toLowerCase();
5661
+ if (normalized === "metrics.effort") return "effort";
5662
+ if (normalized === "metrics.equipment" || normalized === "equipment") return "equipment";
5663
+ if (normalized === "aliases" && docKind === "equipment") return "equipment";
5664
+ if (normalized === "aliases" && docKind === "effort") return "effort";
5665
+ return "tags";
5666
+ }
5667
+ function isReferenceKey(key, docKind) {
5668
+ const normalized = key.trim().toLowerCase();
5669
+ if (normalized === "metrics.effort" || normalized === "metrics.equipment") return true;
5670
+ return normalized === "equipment" && docKind === "effort";
5633
5671
  }
5634
5672
  function createTagDraftRow(view, root, key, onCommit) {
5635
5673
  const row = document.createElement("div");
@@ -5684,6 +5722,117 @@ function createTagDraftRow(view, root, key, onCommit) {
5684
5722
  }));
5685
5723
  return row;
5686
5724
  }
5725
+ function ownsNestedBlock(body, key) {
5726
+ const lines = body.split(/\r?\n/);
5727
+ for (let i = 0; i < lines.length; i++) {
5728
+ if (!new RegExp(`^${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}:\\s*$`).test(lines[i])) continue;
5729
+ return i + 1 < lines.length && /^\s+\S/.test(lines[i + 1]);
5730
+ }
5731
+ return false;
5732
+ }
5733
+ function renderEffortValueDiagnostics(provider, token, onPick) {
5734
+ const trimmed = token.trim();
5735
+ if (!trimmed || provider.resolveEffort(trimmed)) return null;
5736
+ const diagnostics = provider.resolve(trimmed).diagnostics;
5737
+ const box = document.createElement("div");
5738
+ box.setAttribute("data-testid", "editor-ref-unknown");
5739
+ box.className = "w-full basis-full flex flex-wrap items-center gap-2 text-[10px] text-destructive";
5740
+ const message = document.createElement("span");
5741
+ message.textContent = diagnostics[0]?.message || `Unknown effort: ${trimmed}`;
5742
+ box.appendChild(message);
5743
+ for (const candidate of diagnostics[0]?.candidates ?? []) {
5744
+ const pickBtn = document.createElement("button");
5745
+ pickBtn.type = "button";
5746
+ pickBtn.textContent = `Use ${candidate}`;
5747
+ pickBtn.className = "underline hover:text-foreground cursor-pointer";
5748
+ pickBtn.addEventListener("click", (e) => {
5749
+ e.preventDefault();
5750
+ onPick(candidate);
5751
+ });
5752
+ box.appendChild(pickBtn);
5753
+ }
5754
+ const createLink = document.createElement("a");
5755
+ createLink.href = provider.createHref("effort");
5756
+ createLink.textContent = "Create page";
5757
+ createLink.className = "underline hover:text-foreground";
5758
+ box.appendChild(createLink);
5759
+ return box;
5760
+ }
5761
+ var METRIC_DRAFT_KINDS = {
5762
+ effort: { label: "Effort", path: "metrics.effort", kind: "effort" },
5763
+ equipment: { label: "Equipment", path: "metrics.equipment", kind: "equipment" },
5764
+ rpe: { label: "RPE", path: "metrics.rpe", kind: "number" }
5765
+ };
5766
+ function createMetricDraftRow(view, root, referenceProvider, commit) {
5767
+ const row = document.createElement("div");
5768
+ 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";
5769
+ row.setAttribute("data-frontmatter-metric-draft", "true");
5770
+ const select = document.createElement("select");
5771
+ select.setAttribute("aria-label", "Metric kind");
5772
+ 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";
5773
+ for (const [value, spec] of Object.entries(METRIC_DRAFT_KINDS)) {
5774
+ const option = document.createElement("option");
5775
+ option.value = value;
5776
+ option.textContent = spec.label;
5777
+ select.appendChild(option);
5778
+ }
5779
+ row.appendChild(select);
5780
+ const valCol = document.createElement("div");
5781
+ valCol.className = "flex items-center flex-wrap gap-1.5 px-3 py-1.5 flex-1 min-w-0";
5782
+ const input = document.createElement("input");
5783
+ 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";
5784
+ input.placeholder = "Value\u2026";
5785
+ input.setAttribute("aria-label", "Metric value");
5786
+ valCol.appendChild(input);
5787
+ row.appendChild(valCol);
5788
+ let settled = false;
5789
+ const settle = (doCommit) => {
5790
+ if (settled) return;
5791
+ settled = true;
5792
+ const spec = METRIC_DRAFT_KINDS[select.value];
5793
+ const raw = input.value.trim();
5794
+ if (doCommit && spec && raw) {
5795
+ if (spec.kind === "equipment") commit([spec.path], [raw]);
5796
+ else if (spec.kind === "number") {
5797
+ const num = Number(raw);
5798
+ if (!Number.isNaN(num)) commit([spec.path], num);
5799
+ } else commit([spec.path], raw);
5800
+ }
5801
+ row.remove();
5802
+ view.requestMeasure();
5803
+ };
5804
+ const attachFor = () => {
5805
+ const spec = METRIC_DRAFT_KINDS[select.value];
5806
+ if (spec && (spec.kind === "effort" || spec.kind === "equipment") && referenceProvider) {
5807
+ attachSuggestions(input, view, root, "value", spec.path, [], () => settle(true), spec.kind);
5808
+ }
5809
+ };
5810
+ attachFor();
5811
+ select.addEventListener("change", attachFor);
5812
+ input.addEventListener("keydown", (e) => {
5813
+ if (e.defaultPrevented && e.key !== "Escape") return;
5814
+ if (e.key === "Escape") {
5815
+ e.preventDefault();
5816
+ settle(false);
5817
+ return;
5818
+ }
5819
+ if (e.key === "Enter") {
5820
+ e.preventDefault();
5821
+ settle(true);
5822
+ }
5823
+ });
5824
+ input.addEventListener("blur", () => queueMicrotask(() => {
5825
+ if (!input.isConnected) return;
5826
+ settle(input.value.trim().length > 0);
5827
+ }));
5828
+ select.addEventListener("keydown", (e) => {
5829
+ if (e.key === "Escape") {
5830
+ e.preventDefault();
5831
+ settle(false);
5832
+ }
5833
+ });
5834
+ return row;
5835
+ }
5687
5836
  function createTagIcon() {
5688
5837
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
5689
5838
  svg.setAttribute("viewBox", "0 0 24 24");
@@ -5796,26 +5945,11 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
5796
5945
  sectionId;
5797
5946
  rawContent;
5798
5947
  readOnly;
5799
- constructor(sectionIdOrProps, rawContent, readOnly = false) {
5948
+ constructor(sectionId, rawContent, readOnly = false) {
5800
5949
  super();
5801
- if (typeof sectionIdOrProps === "string") {
5802
- this.sectionId = sectionIdOrProps;
5803
- this.rawContent = rawContent ?? "";
5804
- this.readOnly = readOnly;
5805
- } else {
5806
- this.sectionId = "legacy";
5807
- this.rawContent = wodWikiWql.serializeFrontmatter(sectionIdOrProps);
5808
- this.readOnly = readOnly;
5809
- }
5810
- }
5811
- get props() {
5812
- const body = this.rawContent.replace(/^---\r?\n?/, "").replace(/\r?\n?---\r?$/, "");
5813
- const meta = wodWikiWql.parseFrontmatterBody(body);
5814
- const result = {};
5815
- for (const [k, v] of Object.entries(meta)) {
5816
- result[k] = Array.isArray(v) ? v.join(", ") : String(v ?? "");
5817
- }
5818
- return result;
5950
+ this.sectionId = sectionId;
5951
+ this.rawContent = rawContent ?? "";
5952
+ this.readOnly = readOnly;
5819
5953
  }
5820
5954
  eq(other) {
5821
5955
  return this.sectionId === other.sectionId && this.rawContent === other.rawContent && this.readOnly === other.readOnly;
@@ -5837,6 +5971,9 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
5837
5971
  const body = this.rawContent.replace(/^---\r?\n?/, "").replace(/\r?\n?---\r?$/, "");
5838
5972
  const meta = wodWikiWql.parseFrontmatterBody(body);
5839
5973
  const entries = Object.entries(meta);
5974
+ const docKind = detectDefinitionKind(meta);
5975
+ const referenceProvider = view.state.facet(frontmatterReferenceIndex)?.() ?? null;
5976
+ const base = docKind === "effort" ? readBaseAttributes(body) : {};
5840
5977
  if (entries.length === 0) {
5841
5978
  const emptyRow = document.createElement("div");
5842
5979
  emptyRow.className = "px-3 py-2 text-xs text-muted-foreground/60 italic";
@@ -5845,13 +5982,16 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
5845
5982
  }
5846
5983
  for (const [key, value] of entries) {
5847
5984
  const isTags = isTagProperty(key);
5985
+ const valueKind = referenceValueKind(key, docKind);
5986
+ const isReferenceList = (isReferenceKey(key, docKind) || key === "aliases") && Array.isArray(value);
5987
+ const nested = value === "" && ownsNestedBlock(body, key);
5848
5988
  const row = document.createElement("div");
5849
5989
  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";
5850
5990
  box.appendChild(row);
5851
5991
  const keyCol = document.createElement("div");
5852
5992
  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";
5853
- keyCol.appendChild(isTags ? createTagIcon() : createListIcon());
5854
- if (this.readOnly) {
5993
+ keyCol.appendChild(isTags || isReferenceList ? createTagIcon() : createListIcon());
5994
+ if (this.readOnly || nested) {
5855
5995
  const keyLabel = document.createElement("span");
5856
5996
  keyLabel.className = "truncate font-medium text-foreground/90";
5857
5997
  keyLabel.textContent = key;
@@ -5873,15 +6013,10 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
5873
6013
  keyInput.value = currentKey;
5874
6014
  return;
5875
6015
  }
5876
- const newMeta = {};
5877
- for (const [k, v] of Object.entries(meta)) {
5878
- if (k === currentKey) newMeta[nextKey] = v;
5879
- else newMeta[k] = v;
5880
- }
6016
+ renamePropertyInView(view, this.sectionId, currentKey, nextKey, value);
5881
6017
  delete meta[currentKey];
5882
- meta[nextKey] = newMeta[nextKey];
6018
+ meta[nextKey] = value;
5883
6019
  currentKey = nextKey;
5884
- updateFrontmatter(view, this.sectionId, newMeta);
5885
6020
  };
5886
6021
  const commitKeyIfAttached = () => queueMicrotask(() => {
5887
6022
  if (keyInput.isConnected) commitKey();
@@ -5910,71 +6045,117 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
5910
6045
  row.appendChild(keyCol);
5911
6046
  const valCol = document.createElement("div");
5912
6047
  valCol.className = "flex items-center flex-wrap gap-1.5 px-3 py-1.5 flex-1 min-w-0";
5913
- if (isTags) {
5914
- const tags7 = parseTagList(value);
5915
- for (let idx = 0; idx < tags7.length; idx++) {
5916
- const tag = tags7[idx];
6048
+ const commitList = (next) => {
6049
+ meta[key] = next;
6050
+ patchPropertyInView(view, this.sectionId, [key], next.length > 0 ? next : "");
6051
+ };
6052
+ const refMatch = (token) => {
6053
+ if (!referenceProvider) return null;
6054
+ return valueKind === "effort" ? referenceProvider.resolveEffort(token) : valueKind === "equipment" ? referenceProvider.resolveEquipment(token) : null;
6055
+ };
6056
+ if (isTags || isReferenceList) {
6057
+ const chips = parseTagList(value);
6058
+ const chipIsReference = valueKind !== "tags";
6059
+ for (let idx = 0; idx < chips.length; idx++) {
6060
+ const chip = chips[idx];
6061
+ const match = chipIsReference ? refMatch(chip) : null;
5917
6062
  const pill = document.createElement("span");
5918
- 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";
5919
- const tagText = document.createElement("span");
5920
- tagText.textContent = tag;
5921
- pill.appendChild(tagText);
6063
+ 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";
6064
+ const pillText = document.createElement("span");
6065
+ pillText.textContent = match ? match.label : chip;
6066
+ pill.appendChild(pillText);
6067
+ if (match && referenceProvider) {
6068
+ const openLink = document.createElement("a");
6069
+ openLink.href = referenceProvider.openHref(valueKind, match.slug);
6070
+ openLink.title = `Open ${match.label} page`;
6071
+ openLink.setAttribute("aria-label", `Open ${match.label} page`);
6072
+ openLink.className = "inline-flex items-center justify-center text-muted-foreground/60 hover:text-foreground cursor-pointer";
6073
+ openLink.appendChild(createExternalIcon());
6074
+ pill.appendChild(openLink);
6075
+ }
5922
6076
  if (!this.readOnly) {
5923
6077
  const rmBtn = document.createElement("button");
5924
6078
  rmBtn.type = "button";
5925
- rmBtn.setAttribute("aria-label", `Remove tag ${tag}`);
5926
- 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";
6079
+ rmBtn.setAttribute("aria-label", isTags ? `Remove tag ${chip}` : `Remove ${chip}`);
6080
+ rmBtn.className = "inline-flex items-center justify-center text-muted-foreground/60 hover:text-foreground dark:hover:text-foreground cursor-pointer";
5927
6081
  rmBtn.appendChild(createCloseIcon("sm"));
5928
6082
  rmBtn.addEventListener("click", (e) => {
5929
6083
  e.preventDefault();
5930
6084
  e.stopPropagation();
5931
- const nextTags = tags7.filter((_, i) => i !== idx);
5932
- meta[key] = nextTags;
5933
- updateFrontmatter(view, this.sectionId, meta);
6085
+ commitList(chips.filter((_, i) => i !== idx));
5934
6086
  });
5935
6087
  pill.appendChild(rmBtn);
5936
6088
  }
5937
6089
  valCol.appendChild(pill);
5938
6090
  }
5939
6091
  if (!this.readOnly) {
5940
- const tagInput = document.createElement("input");
5941
- 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";
5942
- tagInput.placeholder = tags7.length === 0 ? "Add tags\u2026" : "Add tag\u2026";
5943
- tagInput.setAttribute("aria-label", `Add tag to ${key}`);
5944
- const commitNewTag = () => {
5945
- const val = tagInput.value.trim().replace(/^,|,$/g, "");
5946
- if (val && !tags7.some((tag) => tag.toLowerCase() === val.toLowerCase())) {
5947
- meta[key] = [...tags7, val];
5948
- tagInput.value = "";
6092
+ const chipInput = document.createElement("input");
6093
+ 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";
6094
+ chipInput.placeholder = chips.length === 0 ? isReferenceList ? `Add ${valueKind === "effort" ? "effort" : "equipment"}\u2026` : "Add tags\u2026" : isTags ? "Add tag\u2026" : "Add\u2026";
6095
+ chipInput.setAttribute("aria-label", isTags ? `Add tag to ${key}` : `Add to ${key}`);
6096
+ const commitNewChip = () => {
6097
+ const val = chipInput.value.trim().replace(/^,|,$/g, "");
6098
+ if (val && !chips.some((chip) => chip.toLowerCase() === val.toLowerCase())) {
6099
+ chipInput.value = "";
5949
6100
  pendingFocus.set(view, { kind: "tag", key });
5950
- updateFrontmatter(view, this.sectionId, meta);
6101
+ commitList([...chips, val]);
5951
6102
  }
5952
6103
  };
5953
- valCol.appendChild(tagInput);
5954
- attachSuggestions(tagInput, view, root, "value", key, tags7, commitNewTag);
5955
- tagInput.addEventListener("keydown", (e) => {
6104
+ valCol.appendChild(chipInput);
6105
+ attachSuggestions(chipInput, view, root, "value", key, chips, commitNewChip, valueKind);
6106
+ chipInput.addEventListener("keydown", (e) => {
5956
6107
  if (e.defaultPrevented) return;
5957
6108
  if (e.key === "Enter" || e.key === ",") {
5958
6109
  e.preventDefault();
5959
- commitNewTag();
5960
- } else if (e.key === "Backspace" && !tagInput.value && tags7.length > 0) {
6110
+ commitNewChip();
6111
+ } else if (e.key === "Backspace" && !chipInput.value && chips.length > 0) {
5961
6112
  e.preventDefault();
5962
- meta[key] = tags7.slice(0, -1);
5963
6113
  pendingFocus.set(view, { kind: "tag", key });
5964
- updateFrontmatter(view, this.sectionId, meta);
6114
+ commitList(chips.slice(0, -1));
5965
6115
  }
5966
6116
  });
5967
- tagInput.addEventListener("blur", () => queueMicrotask(() => {
5968
- if (tagInput.isConnected && tagInput.value.trim()) commitNewTag();
6117
+ chipInput.addEventListener("blur", () => queueMicrotask(() => {
6118
+ if (chipInput.isConnected && chipInput.value.trim()) commitNewChip();
5969
6119
  }));
5970
6120
  const focus = pendingFocus.get(view);
5971
6121
  if (focus?.kind === "tag" && focus.key === key) {
5972
6122
  setTimeout(() => {
5973
- if (tagInput.isConnected) tagInput.focus();
6123
+ if (chipInput.isConnected) chipInput.focus();
5974
6124
  }, 0);
5975
6125
  pendingFocus.delete(view);
5976
6126
  }
6127
+ if (chipIsReference && referenceProvider) {
6128
+ const unknown = chips.filter((chip) => !refMatch(chip));
6129
+ if (unknown.length > 0) {
6130
+ const diag = document.createElement("div");
6131
+ diag.setAttribute("data-testid", "editor-ref-unknown");
6132
+ diag.className = "w-full basis-full flex flex-wrap items-center gap-2 text-[10px] text-destructive";
6133
+ const label = document.createElement("span");
6134
+ label.textContent = `Unknown ${valueKind === "effort" ? "effort" : "equipment"}: ${unknown.join(", ")}`;
6135
+ diag.appendChild(label);
6136
+ const selectBtn = document.createElement("button");
6137
+ selectBtn.type = "button";
6138
+ selectBtn.textContent = "Select existing";
6139
+ selectBtn.className = "underline hover:text-foreground cursor-pointer";
6140
+ selectBtn.addEventListener("click", (e) => {
6141
+ e.preventDefault();
6142
+ chipInput.focus();
6143
+ });
6144
+ diag.appendChild(selectBtn);
6145
+ const createLink = document.createElement("a");
6146
+ createLink.href = referenceProvider.createHref(valueKind);
6147
+ createLink.textContent = "Create page";
6148
+ createLink.className = "underline hover:text-foreground";
6149
+ diag.appendChild(createLink);
6150
+ valCol.appendChild(diag);
6151
+ }
6152
+ }
5977
6153
  }
6154
+ } else if (nested) {
6155
+ const valSpan = document.createElement("span");
6156
+ valSpan.className = "text-xs text-muted-foreground/60 italic py-0.5 truncate";
6157
+ valSpan.textContent = "Nested block \u2014 edit in YAML";
6158
+ valCol.appendChild(valSpan);
5978
6159
  } else {
5979
6160
  const strVal = Array.isArray(value) ? value.join(", ") : String(value ?? "");
5980
6161
  if (this.readOnly) {
@@ -5994,8 +6175,9 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
5994
6175
  if (cur !== committedVal) {
5995
6176
  committedVal = cur;
5996
6177
  const num = Number(cur);
5997
- meta[key] = cur !== "" && !isNaN(num) ? num : cur;
5998
- updateFrontmatter(view, this.sectionId, meta);
6178
+ const next = cur !== "" && !isNaN(num) ? num : cur;
6179
+ meta[key] = next;
6180
+ patchPropertyInView(view, this.sectionId, [key], next);
5999
6181
  }
6000
6182
  };
6001
6183
  const commitValIfAttached = () => queueMicrotask(() => {
@@ -6003,7 +6185,16 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
6003
6185
  });
6004
6186
  valInput.addEventListener("change", commitValIfAttached);
6005
6187
  valCol.appendChild(valInput);
6006
- attachSuggestions(valInput, view, root, "value", key, [strVal], commitVal);
6188
+ attachSuggestions(
6189
+ valInput,
6190
+ view,
6191
+ root,
6192
+ "value",
6193
+ key,
6194
+ [strVal],
6195
+ commitVal,
6196
+ key.trim().toLowerCase() === "metrics.effort" ? "effort" : "tags"
6197
+ );
6007
6198
  valInput.addEventListener("blur", commitValIfAttached);
6008
6199
  valInput.addEventListener("keydown", (e) => {
6009
6200
  if (e.defaultPrevented) return;
@@ -6012,6 +6203,34 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
6012
6203
  valInput.blur();
6013
6204
  }
6014
6205
  });
6206
+ if (key.trim().toLowerCase() === "metrics.effort" && referenceProvider) {
6207
+ const match = referenceProvider.resolveEffort(strVal);
6208
+ if (match) {
6209
+ const pill = document.createElement("span");
6210
+ 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";
6211
+ const pillText = document.createElement("span");
6212
+ pillText.textContent = match.label;
6213
+ pill.appendChild(pillText);
6214
+ const openLink = document.createElement("a");
6215
+ openLink.href = referenceProvider.openHref("effort", match.slug);
6216
+ openLink.title = `Open ${match.label} page`;
6217
+ openLink.setAttribute("aria-label", `Open ${match.label} page`);
6218
+ openLink.className = "inline-flex items-center justify-center text-muted-foreground/60 hover:text-foreground cursor-pointer";
6219
+ openLink.appendChild(createExternalIcon());
6220
+ pill.appendChild(openLink);
6221
+ valCol.insertBefore(pill, valInput);
6222
+ 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";
6223
+ }
6224
+ const diag = renderEffortValueDiagnostics(
6225
+ referenceProvider,
6226
+ strVal,
6227
+ (slug) => {
6228
+ meta[key] = slug;
6229
+ patchPropertyInView(view, this.sectionId, [key], slug);
6230
+ }
6231
+ );
6232
+ if (diag) valCol.appendChild(diag);
6233
+ }
6015
6234
  }
6016
6235
  if (/^https?:\/\/\S+$/i.test(strVal)) {
6017
6236
  const openLink = document.createElement("a");
@@ -6026,7 +6245,7 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
6026
6245
  }
6027
6246
  }
6028
6247
  row.appendChild(valCol);
6029
- if (!this.readOnly) {
6248
+ if (!this.readOnly && !nested) {
6030
6249
  const actionsCol = document.createElement("div");
6031
6250
  actionsCol.className = "flex items-center pr-2";
6032
6251
  const delBtn = document.createElement("button");
@@ -6037,14 +6256,16 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
6037
6256
  delBtn.addEventListener("click", (e) => {
6038
6257
  e.preventDefault();
6039
6258
  e.stopPropagation();
6040
- const newMeta = { ...meta };
6041
- delete newMeta[key];
6042
- updateFrontmatter(view, this.sectionId, newMeta);
6259
+ delete meta[key];
6260
+ patchPropertyInView(view, this.sectionId, [key], null);
6043
6261
  });
6044
6262
  actionsCol.appendChild(delBtn);
6045
6263
  row.appendChild(actionsCol);
6046
6264
  }
6047
6265
  }
6266
+ if (docKind === "effort" && !this.readOnly) {
6267
+ box.appendChild(this.buildDefinitionRows(view, base));
6268
+ }
6048
6269
  if (!this.readOnly) {
6049
6270
  const existingTagKey = entries.find(([key]) => isTagProperty(key))?.[0];
6050
6271
  const actionsRow = document.createElement("div");
@@ -6067,10 +6288,36 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
6067
6288
  newKey = `property_${count2}`;
6068
6289
  }
6069
6290
  pendingFocus.set(view, { kind: "key", key: newKey });
6070
- const newMeta = { ...meta, [newKey]: "" };
6071
- updateFrontmatter(view, this.sectionId, newMeta);
6291
+ patchPropertyInView(view, this.sectionId, [newKey], "");
6072
6292
  });
6073
6293
  actionsRow.appendChild(addBtn);
6294
+ {
6295
+ const addMetricBtn = document.createElement("button");
6296
+ addMetricBtn.type = "button";
6297
+ addMetricBtn.setAttribute("aria-label", "Add metric");
6298
+ addMetricBtn.setAttribute("data-testid", "editor-add-metric-note");
6299
+ 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";
6300
+ addMetricBtn.appendChild(createPlusIcon());
6301
+ const addMetricLabel = document.createElement("span");
6302
+ addMetricLabel.textContent = "Add metric";
6303
+ addMetricBtn.appendChild(addMetricLabel);
6304
+ addMetricBtn.addEventListener("click", (e) => {
6305
+ e.preventDefault();
6306
+ e.stopPropagation();
6307
+ const existing = root.querySelector("[data-frontmatter-metric-draft] input");
6308
+ if (existing) {
6309
+ existing.focus();
6310
+ return;
6311
+ }
6312
+ const draftRow = createMetricDraftRow(view, root, referenceProvider, (path, value) => {
6313
+ patchPropertyInView(view, this.sectionId, path, value);
6314
+ });
6315
+ box.appendChild(draftRow);
6316
+ draftRow.querySelector("input")?.focus();
6317
+ view.requestMeasure();
6318
+ });
6319
+ actionsRow.appendChild(addMetricBtn);
6320
+ }
6074
6321
  {
6075
6322
  const addTagBtn = document.createElement("button");
6076
6323
  addTagBtn.type = "button";
@@ -6095,7 +6342,7 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
6095
6342
  }
6096
6343
  const draftRow = createTagDraftRow(view, root, "tags", (val) => {
6097
6344
  pendingFocus.set(view, { kind: "tag", key: "tags" });
6098
- updateFrontmatter(view, this.sectionId, { ...meta, tags: [val] });
6345
+ patchPropertyInView(view, this.sectionId, ["tags"], [val]);
6099
6346
  });
6100
6347
  box.appendChild(draftRow);
6101
6348
  draftRow.querySelector("input")?.focus();
@@ -6107,6 +6354,7 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
6107
6354
  const editYamlBtn = document.createElement("button");
6108
6355
  editYamlBtn.type = "button";
6109
6356
  editYamlBtn.setAttribute("aria-label", "Edit YAML");
6357
+ editYamlBtn.setAttribute("data-testid", "editor-property-edit-yaml");
6110
6358
  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";
6111
6359
  const editYamlLabel = document.createElement("span");
6112
6360
  editYamlLabel.textContent = "Edit YAML";
@@ -6126,6 +6374,82 @@ var DefaultFrontmatterWidget = class extends view.WidgetType {
6126
6374
  }
6127
6375
  return root;
6128
6376
  }
6377
+ /**
6378
+ * Effort-definition attribute rows (MET / discipline / intensity tier).
6379
+ * Values live in the nested baseAttributes block; each edit patches that
6380
+ * one path, so unknown sibling keys and comments elsewhere survive.
6381
+ */
6382
+ buildDefinitionRows(view, base) {
6383
+ const wrap = document.createElement("div");
6384
+ wrap.setAttribute("data-frontmatter-definition", "true");
6385
+ wrap.className = "border-t border-dashed border-border/60";
6386
+ const header = document.createElement("div");
6387
+ header.className = "px-3 pt-2 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground";
6388
+ header.textContent = "Definition";
6389
+ wrap.appendChild(header);
6390
+ const commitField = (field, raw) => {
6391
+ if (field === "met") {
6392
+ const num = Number(raw);
6393
+ patchPropertyInView(view, this.sectionId, ["baseAttributes", "met"], raw !== "" && !Number.isNaN(num) ? num : null);
6394
+ return;
6395
+ }
6396
+ patchPropertyInView(view, this.sectionId, ["baseAttributes", field], raw === "" ? null : raw);
6397
+ };
6398
+ const fieldRow = (label, child) => {
6399
+ const row = document.createElement("label");
6400
+ 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";
6401
+ const name = document.createElement("span");
6402
+ name.className = "w-28 shrink-0 text-muted-foreground";
6403
+ name.textContent = label;
6404
+ row.appendChild(name);
6405
+ child.className = "flex-1 min-w-0 bg-transparent text-xs text-foreground outline-none py-0.5";
6406
+ row.appendChild(child);
6407
+ wrap.appendChild(row);
6408
+ };
6409
+ const met = document.createElement("input");
6410
+ met.type = "number";
6411
+ met.step = "0.1";
6412
+ met.inputMode = "decimal";
6413
+ met.value = base.met ?? "";
6414
+ met.setAttribute("aria-label", "MET value");
6415
+ met.addEventListener("change", () => queueMicrotask(() => {
6416
+ if (met.isConnected) commitField("met", met.value.trim());
6417
+ }));
6418
+ fieldRow("MET", met);
6419
+ const discipline = document.createElement("select");
6420
+ discipline.setAttribute("aria-label", "Discipline");
6421
+ discipline.className = "flex-1 min-w-0 rounded-md border border-input bg-background px-1.5 text-[11px] text-muted-foreground outline-none";
6422
+ const unset = document.createElement("option");
6423
+ unset.value = "";
6424
+ unset.textContent = "Unset";
6425
+ discipline.appendChild(unset);
6426
+ for (const option of wodWikiLang.EFFORT_DISCIPLINES) {
6427
+ const opt = document.createElement("option");
6428
+ opt.value = option;
6429
+ opt.textContent = option;
6430
+ discipline.appendChild(opt);
6431
+ }
6432
+ discipline.value = base.discipline ?? "";
6433
+ discipline.addEventListener("change", () => commitField("discipline", discipline.value));
6434
+ fieldRow("Discipline", discipline);
6435
+ const tier = document.createElement("select");
6436
+ tier.setAttribute("aria-label", "Intensity tier");
6437
+ tier.className = "flex-1 min-w-0 rounded-md border border-input bg-background px-1.5 text-[11px] text-muted-foreground outline-none";
6438
+ const tierUnset = document.createElement("option");
6439
+ tierUnset.value = "";
6440
+ tierUnset.textContent = "Unset";
6441
+ tier.appendChild(tierUnset);
6442
+ for (const option of ["low", "moderate", "high"]) {
6443
+ const opt = document.createElement("option");
6444
+ opt.value = option;
6445
+ opt.textContent = option;
6446
+ tier.appendChild(opt);
6447
+ }
6448
+ tier.value = base.intensityTier ?? "";
6449
+ tier.addEventListener("change", () => commitField("intensityTier", tier.value));
6450
+ fieldRow("Intensity tier", tier);
6451
+ return wrap;
6452
+ }
6129
6453
  destroy(dom) {
6130
6454
  widgetCleanup.get(dom)?.forEach((close) => close());
6131
6455
  widgetCleanup.delete(dom);
@@ -6159,7 +6483,7 @@ var EmptyFrontmatterWidget = class extends view.WidgetType {
6159
6483
  e.preventDefault();
6160
6484
  e.stopPropagation();
6161
6485
  pendingFocus.set(view, { kind: "key", key: "property" });
6162
- updateFrontmatter(view, "new", { property: "" });
6486
+ patchPropertyInView(view, "new", ["property"], "");
6163
6487
  });
6164
6488
  actionsRow.appendChild(addBtn);
6165
6489
  const addTagBtn = document.createElement("button");
@@ -6180,7 +6504,7 @@ var EmptyFrontmatterWidget = class extends view.WidgetType {
6180
6504
  }
6181
6505
  const draftRow = createTagDraftRow(view, root, "tags", (val) => {
6182
6506
  pendingFocus.set(view, { kind: "tag", key: "tags" });
6183
- updateFrontmatter(view, "new", { tags: [val] });
6507
+ patchPropertyInView(view, "new", ["tags"], [val]);
6184
6508
  });
6185
6509
  root.appendChild(draftRow);
6186
6510
  draftRow.querySelector("input")?.focus();
@@ -6234,7 +6558,7 @@ var frontmatterPreviewField = state.StateField.define({
6234
6558
  return buildFrontmatterDecos(state);
6235
6559
  },
6236
6560
  update(deco, tr) {
6237
- if (tr.docChanged || tr.reconfigured || tr.selection) {
6561
+ if (tr.docChanged || tr.reconfigured || tr.selection || tr.effects.some((e) => e.is(referenceIndexReload))) {
6238
6562
  return buildFrontmatterDecos(tr.state);
6239
6563
  }
6240
6564
  return deco;
@@ -13025,13 +13349,13 @@ function editorPreset(optionsOrDialect = "markdown") {
13025
13349
  }
13026
13350
 
13027
13351
  // src/version.ts
13028
- var VERSION = "0.37.2137";
13029
- var GIT_SHA = "b82a721";
13030
- var BUILD_TIME = "2026-10-16T20:56:02.597Z";
13352
+ var VERSION = "0.37.2140";
13353
+ var GIT_SHA = "bcf7960";
13354
+ var BUILD_TIME = "2026-10-18T21:43:29.081Z";
13031
13355
  var SEMVER = {
13032
- version: "0.37.2137",
13033
- gitSha: "b82a721",
13034
- buildTime: "2026-10-16T20:56:02.597Z"
13356
+ version: "0.37.2140",
13357
+ gitSha: "bcf7960",
13358
+ buildTime: "2026-10-18T21:43:29.081Z"
13035
13359
  };
13036
13360
 
13037
13361
  Object.defineProperty(exports, "CONTENT_GROUPING_DIMENSIONS", {
@@ -13136,6 +13460,7 @@ exports.formatMMSS = formatMMSS;
13136
13460
  exports.formatTimestamp = formatTimestamp;
13137
13461
  exports.frontmatterPreview = frontmatterPreview;
13138
13462
  exports.frontmatterPreviewField = frontmatterPreviewField;
13463
+ exports.frontmatterReferenceIndex = frontmatterReferenceIndex;
13139
13464
  exports.frontmatterSuggestions = frontmatterSuggestions;
13140
13465
  exports.frontmatterVisibility = frontmatterVisibility;
13141
13466
  exports.getClauseMeta = getClauseMeta;
@@ -13168,6 +13493,7 @@ exports.previewDecorations = previewDecorations;
13168
13493
  exports.queryBlockPreview = queryBlockPreview;
13169
13494
  exports.readSessionRpe = readSessionRpe;
13170
13495
  exports.readStoredUnit = readStoredUnit;
13496
+ exports.referenceIndexReload = referenceIndexReload;
13171
13497
  exports.registerCustomCompletionSource = registerCustomCompletionSource;
13172
13498
  exports.registeredSectionWidgets = registeredSectionWidgets;
13173
13499
  exports.removeRuntimePanel = removeRuntimePanel;