@mulmoclaude/collection-plugin 0.7.5 → 0.8.0

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/vue.cjs CHANGED
@@ -1234,18 +1234,18 @@ var CollectionRecordModal_default = /* @__PURE__ */ (0, vue.defineComponent)({
1234
1234
  });
1235
1235
  //#endregion
1236
1236
  //#region src/vue/components/CollectionCalendarView.vue?vue&type=script&setup=true&lang.ts
1237
- var _hoisted_1$14 = {
1237
+ var _hoisted_1$15 = {
1238
1238
  class: "flex flex-col gap-3",
1239
1239
  "data-testid": "collection-calendar"
1240
1240
  };
1241
- var _hoisted_2$13 = { class: "flex items-center gap-2" };
1242
- var _hoisted_3$13 = ["aria-label"];
1243
- var _hoisted_4$13 = ["aria-label"];
1244
- var _hoisted_5$12 = {
1241
+ var _hoisted_2$14 = { class: "flex items-center gap-2" };
1242
+ var _hoisted_3$14 = ["aria-label"];
1243
+ var _hoisted_4$14 = ["aria-label"];
1244
+ var _hoisted_5$13 = {
1245
1245
  class: "text-sm font-bold text-slate-800 flex-1",
1246
1246
  "data-testid": "collection-calendar-month"
1247
1247
  };
1248
- var _hoisted_6$11 = { class: "grid grid-cols-7 gap-1 text-[10px] font-bold text-slate-400 uppercase tracking-wider select-none" };
1248
+ var _hoisted_6$12 = { class: "grid grid-cols-7 gap-1 text-[10px] font-bold text-slate-400 uppercase tracking-wider select-none" };
1249
1249
  var _hoisted_7$10 = { class: "grid grid-cols-7 gap-1" };
1250
1250
  var _hoisted_8$10 = [
1251
1251
  "aria-label",
@@ -1375,23 +1375,23 @@ var CollectionCalendarView_default = /* @__PURE__ */ (0, vue.defineComponent)({
1375
1375
  viewMonth.value = now.getMonth() + 1;
1376
1376
  }
1377
1377
  return (_ctx, _cache) => {
1378
- return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1$14, [
1379
- (0, vue.createElementVNode)("div", _hoisted_2$13, [
1378
+ return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1$15, [
1379
+ (0, vue.createElementVNode)("div", _hoisted_2$14, [
1380
1380
  (0, vue.createElementVNode)("button", {
1381
1381
  type: "button",
1382
1382
  class: "h-8 w-8 flex items-center justify-center rounded text-slate-500 hover:bg-slate-100 transition-colors",
1383
1383
  "aria-label": (0, vue.unref)(t)("collectionsView.calendarPrevMonth"),
1384
1384
  "data-testid": "collection-calendar-prev",
1385
1385
  onClick: _cache[0] || (_cache[0] = ($event) => stepMonth(-1))
1386
- }, [..._cache[2] || (_cache[2] = [(0, vue.createElementVNode)("span", { class: "material-icons text-lg" }, "chevron_left", -1)])], 8, _hoisted_3$13),
1386
+ }, [..._cache[2] || (_cache[2] = [(0, vue.createElementVNode)("span", { class: "material-icons text-lg" }, "chevron_left", -1)])], 8, _hoisted_3$14),
1387
1387
  (0, vue.createElementVNode)("button", {
1388
1388
  type: "button",
1389
1389
  class: "h-8 w-8 flex items-center justify-center rounded text-slate-500 hover:bg-slate-100 transition-colors",
1390
1390
  "aria-label": (0, vue.unref)(t)("collectionsView.calendarNextMonth"),
1391
1391
  "data-testid": "collection-calendar-next",
1392
1392
  onClick: _cache[1] || (_cache[1] = ($event) => stepMonth(1))
1393
- }, [..._cache[3] || (_cache[3] = [(0, vue.createElementVNode)("span", { class: "material-icons text-lg" }, "chevron_right", -1)])], 8, _hoisted_4$13),
1394
- (0, vue.createElementVNode)("h3", _hoisted_5$12, (0, vue.toDisplayString)(monthLabel.value), 1),
1393
+ }, [..._cache[3] || (_cache[3] = [(0, vue.createElementVNode)("span", { class: "material-icons text-lg" }, "chevron_right", -1)])], 8, _hoisted_4$14),
1394
+ (0, vue.createElementVNode)("h3", _hoisted_5$13, (0, vue.toDisplayString)(monthLabel.value), 1),
1395
1395
  (0, vue.createElementVNode)("button", {
1396
1396
  type: "button",
1397
1397
  class: "h-8 px-2.5 flex items-center gap-1 rounded border border-slate-200 bg-white text-slate-600 hover:bg-slate-50 text-xs font-bold transition-colors",
@@ -1399,7 +1399,7 @@ var CollectionCalendarView_default = /* @__PURE__ */ (0, vue.defineComponent)({
1399
1399
  onClick: goToday
1400
1400
  }, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.calendarToday")), 1)
1401
1401
  ]),
1402
- (0, vue.createElementVNode)("div", _hoisted_6$11, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(weekdayLabels.value, (label, idx) => {
1402
+ (0, vue.createElementVNode)("div", _hoisted_6$12, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(weekdayLabels.value, (label, idx) => {
1403
1403
  return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
1404
1404
  key: idx,
1405
1405
  class: "px-1 py-1 text-center"
@@ -1440,18 +1440,18 @@ var CollectionCalendarView_default = /* @__PURE__ */ (0, vue.defineComponent)({
1440
1440
  });
1441
1441
  //#endregion
1442
1442
  //#region src/vue/components/CollectionDayView.vue?vue&type=script&setup=true&lang.ts
1443
- var _hoisted_1$13 = { class: "flex items-center gap-2 border-b border-slate-200 px-4 py-3" };
1444
- var _hoisted_2$12 = {
1443
+ var _hoisted_1$14 = { class: "flex items-center gap-2 border-b border-slate-200 px-4 py-3" };
1444
+ var _hoisted_2$13 = {
1445
1445
  class: "flex-1 text-sm font-bold text-slate-800",
1446
1446
  "data-testid": "collection-day-view-title"
1447
1447
  };
1448
- var _hoisted_3$12 = ["aria-label"];
1449
- var _hoisted_4$12 = ["aria-label"];
1450
- var _hoisted_5$11 = {
1448
+ var _hoisted_3$13 = ["aria-label"];
1449
+ var _hoisted_4$13 = ["aria-label"];
1450
+ var _hoisted_5$12 = {
1451
1451
  key: 0,
1452
1452
  class: "px-4 py-10 text-center text-sm text-slate-400"
1453
1453
  };
1454
- var _hoisted_6$10 = { class: "absolute -top-2 left-0 w-10 pr-1 text-right text-[10px] tabular-nums text-slate-400" };
1454
+ var _hoisted_6$11 = { class: "absolute -top-2 left-0 w-10 pr-1 text-right text-[10px] tabular-nums text-slate-400" };
1455
1455
  var _hoisted_7$9 = {
1456
1456
  class: "absolute inset-y-0 right-0",
1457
1457
  style: { "left": "2.75rem" }
@@ -1537,6 +1537,7 @@ var CollectionDayView_default = /* @__PURE__ */ (0, vue.defineComponent)({
1537
1537
  "datetime",
1538
1538
  "table",
1539
1539
  "embed",
1540
+ "backlinks",
1540
1541
  "image",
1541
1542
  "markdown"
1542
1543
  ]);
@@ -1632,8 +1633,8 @@ var CollectionDayView_default = /* @__PURE__ */ (0, vue.defineComponent)({
1632
1633
  role: "dialog",
1633
1634
  "aria-modal": "true"
1634
1635
  }, [(0, vue.createElementVNode)("div", { class: (0, vue.normalizeClass)(["flex min-h-0 flex-col", __props.showDetail ? "w-80 shrink-0 border-r border-slate-200" : "w-full"]) }, [
1635
- (0, vue.createElementVNode)("div", _hoisted_1$13, [
1636
- (0, vue.createElementVNode)("h3", _hoisted_2$12, (0, vue.toDisplayString)(dayLabel.value), 1),
1636
+ (0, vue.createElementVNode)("div", _hoisted_1$14, [
1637
+ (0, vue.createElementVNode)("h3", _hoisted_2$13, (0, vue.toDisplayString)(dayLabel.value), 1),
1637
1638
  __props.canCreate ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
1638
1639
  key: 0,
1639
1640
  type: "button",
@@ -1641,16 +1642,16 @@ var CollectionDayView_default = /* @__PURE__ */ (0, vue.defineComponent)({
1641
1642
  "aria-label": (0, vue.unref)(t)("collectionsView.calendarCreateOn", { date: dayKey.value }),
1642
1643
  "data-testid": "collection-day-view-create",
1643
1644
  onClick: onCreate
1644
- }, [..._cache[3] || (_cache[3] = [(0, vue.createElementVNode)("span", { class: "material-icons text-lg" }, "add", -1)])], 8, _hoisted_3$12)) : (0, vue.createCommentVNode)("", true),
1645
+ }, [..._cache[3] || (_cache[3] = [(0, vue.createElementVNode)("span", { class: "material-icons text-lg" }, "add", -1)])], 8, _hoisted_3$13)) : (0, vue.createCommentVNode)("", true),
1645
1646
  (0, vue.createElementVNode)("button", {
1646
1647
  type: "button",
1647
1648
  class: "h-8 w-8 flex items-center justify-center rounded text-slate-500 hover:bg-slate-100 transition-colors",
1648
1649
  "aria-label": (0, vue.unref)(t)("collectionsView.dayViewClose"),
1649
1650
  "data-testid": "collection-day-view-close",
1650
1651
  onClick: _cache[0] || (_cache[0] = ($event) => emit("close"))
1651
- }, [..._cache[4] || (_cache[4] = [(0, vue.createElementVNode)("span", { class: "material-icons text-lg" }, "close", -1)])], 8, _hoisted_4$12)
1652
+ }, [..._cache[4] || (_cache[4] = [(0, vue.createElementVNode)("span", { class: "material-icons text-lg" }, "close", -1)])], 8, _hoisted_4$13)
1652
1653
  ]),
1653
- timedEntries.value.length === 0 && allDayEntries.value.length === 0 ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_5$11, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.dayViewEmpty")), 1)) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
1654
+ timedEntries.value.length === 0 && allDayEntries.value.length === 0 ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_5$12, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.dayViewEmpty")), 1)) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
1654
1655
  key: 1,
1655
1656
  ref_key: "scrollEl",
1656
1657
  ref: scrollEl,
@@ -1664,7 +1665,7 @@ var CollectionDayView_default = /* @__PURE__ */ (0, vue.defineComponent)({
1664
1665
  key: hour,
1665
1666
  class: "absolute left-0 right-0 border-t border-slate-100",
1666
1667
  style: (0, vue.normalizeStyle)({ top: `${(hour - 1) * HOUR_PX}px` })
1667
- }, [(0, vue.createElementVNode)("span", _hoisted_6$10, (0, vue.toDisplayString)(hourLabel(hour - 1)), 1)], 4);
1668
+ }, [(0, vue.createElementVNode)("span", _hoisted_6$11, (0, vue.toDisplayString)(hourLabel(hour - 1)), 1)], 4);
1668
1669
  }), 64)), (0, vue.createElementVNode)("div", _hoisted_7$9, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(timedEntries.value, (entry) => {
1669
1670
  return (0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
1670
1671
  key: entry.id,
@@ -1699,15 +1700,15 @@ var CollectionDayView_default = /* @__PURE__ */ (0, vue.defineComponent)({
1699
1700
  });
1700
1701
  //#endregion
1701
1702
  //#region src/vue/components/CollectionKanbanView.vue?vue&type=script&setup=true&lang.ts
1702
- var _hoisted_1$12 = {
1703
+ var _hoisted_1$13 = {
1703
1704
  class: "h-full overflow-x-auto overflow-y-hidden",
1704
1705
  "data-testid": "collection-kanban"
1705
1706
  };
1706
- var _hoisted_2$11 = { class: "flex gap-3 h-full p-1 min-w-max" };
1707
- var _hoisted_3$11 = ["data-testid"];
1708
- var _hoisted_4$11 = { class: "flex items-center justify-between px-3 py-2 border-b border-slate-200" };
1709
- var _hoisted_5$10 = { class: "flex items-center gap-2 min-w-0" };
1710
- var _hoisted_6$9 = ["title"];
1707
+ var _hoisted_2$12 = { class: "flex gap-3 h-full p-1 min-w-max" };
1708
+ var _hoisted_3$12 = ["data-testid"];
1709
+ var _hoisted_4$12 = { class: "flex items-center justify-between px-3 py-2 border-b border-slate-200" };
1710
+ var _hoisted_5$11 = { class: "flex items-center gap-2 min-w-0" };
1711
+ var _hoisted_6$10 = ["title"];
1711
1712
  var _hoisted_7$8 = { class: "text-[11px] text-slate-400 shrink-0" };
1712
1713
  var _hoisted_8$8 = [
1713
1714
  "data-testid",
@@ -1741,7 +1742,10 @@ var CollectionKanbanView_default = /* @__PURE__ */ (0, vue.defineComponent)({
1741
1742
  const emit = __emit;
1742
1743
  const { t } = useCollectionI18n();
1743
1744
  /** The Uncategorized column uses the empty string as its sentinel value. */
1744
- const groupSpec = (0, vue.computed)(() => props.schema.fields[props.groupField]);
1745
+ const groupSpec = (0, vue.computed)(() => {
1746
+ const spec = props.schema.fields[props.groupField];
1747
+ return spec?.type === "enum" ? spec : void 0;
1748
+ });
1745
1749
  /** Declared enum values become columns in order, with a trailing
1746
1750
  * Uncategorized column for empty/unknown values (also a drop target that
1747
1751
  * clears the field). The Uncategorized column is omitted when the group
@@ -1788,7 +1792,10 @@ var CollectionKanbanView_default = /* @__PURE__ */ (0, vue.defineComponent)({
1788
1792
  const value = String(raw);
1789
1793
  return (groupSpec.value?.values ?? []).includes(value) ? value : UNCATEGORIZED;
1790
1794
  }
1791
- const visibleItems = (0, vue.computed)(() => groupSpec.value ? props.items.filter((item) => (0, _mulmoclaude_core_collection.fieldVisible)(groupSpec.value, item)) : []);
1795
+ const visibleItems = (0, vue.computed)(() => {
1796
+ const spec = groupSpec.value;
1797
+ return spec ? props.items.filter((item) => (0, _mulmoclaude_core_collection.fieldVisible)(spec, item)) : [];
1798
+ });
1792
1799
  const itemsByColumnMap = (0, vue.computed)(() => {
1793
1800
  const map = /* @__PURE__ */ new Map();
1794
1801
  for (const column of columns.value) map.set(column.value, []);
@@ -1820,15 +1827,15 @@ var CollectionKanbanView_default = /* @__PURE__ */ (0, vue.defineComponent)({
1820
1827
  emit("move", itemId(item), next);
1821
1828
  }
1822
1829
  return (_ctx, _cache) => {
1823
- return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1$12, [(0, vue.createElementVNode)("div", _hoisted_2$11, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(columns.value, (column) => {
1830
+ return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1$13, [(0, vue.createElementVNode)("div", _hoisted_2$12, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(columns.value, (column) => {
1824
1831
  return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
1825
1832
  key: column.value,
1826
1833
  "data-testid": `collection-kanban-column-${column.value || "uncategorized"}`,
1827
1834
  class: "w-72 shrink-0 flex flex-col bg-slate-100 rounded-lg"
1828
- }, [(0, vue.createElementVNode)("div", _hoisted_4$11, [(0, vue.createElementVNode)("div", _hoisted_5$10, [(0, vue.createElementVNode)("span", { class: (0, vue.normalizeClass)(["w-2 h-2 rounded-full shrink-0", (0, vue.unref)(_mulmoclaude_core_collection.resolveEnumColor)(__props.schema, __props.groupField, column.value).dot]) }, null, 2), (0, vue.createElementVNode)("span", {
1835
+ }, [(0, vue.createElementVNode)("div", _hoisted_4$12, [(0, vue.createElementVNode)("div", _hoisted_5$11, [(0, vue.createElementVNode)("span", { class: (0, vue.normalizeClass)(["w-2 h-2 rounded-full shrink-0", (0, vue.unref)(_mulmoclaude_core_collection.resolveEnumColor)(__props.schema, __props.groupField, column.value).dot]) }, null, 2), (0, vue.createElementVNode)("span", {
1829
1836
  class: "font-semibold text-xs text-slate-600 truncate",
1830
1837
  title: column.label
1831
- }, (0, vue.toDisplayString)(column.label), 9, _hoisted_6$9)]), (0, vue.createElementVNode)("span", _hoisted_7$8, (0, vue.toDisplayString)(itemsByColumn(column.value).length), 1)]), (0, vue.createVNode)((0, vue.unref)(vuedraggable.default), {
1838
+ }, (0, vue.toDisplayString)(column.label), 9, _hoisted_6$10)]), (0, vue.createElementVNode)("span", _hoisted_7$8, (0, vue.toDisplayString)(itemsByColumn(column.value).length), 1)]), (0, vue.createVNode)((0, vue.unref)(vuedraggable.default), {
1832
1839
  "model-value": itemsByColumn(column.value),
1833
1840
  "item-key": __props.schema.primaryKey,
1834
1841
  group: "collection-kanban-cards",
@@ -1859,7 +1866,7 @@ var CollectionKanbanView_default = /* @__PURE__ */ (0, vue.defineComponent)({
1859
1866
  "model-value",
1860
1867
  "item-key",
1861
1868
  "onChange"
1862
- ])], 8, _hoisted_3$11);
1869
+ ])], 8, _hoisted_3$12);
1863
1870
  }), 128))])]);
1864
1871
  };
1865
1872
  }
@@ -1888,6 +1895,61 @@ function activatePathLink(event, path, stop = false) {
1888
1895
  nav(path);
1889
1896
  }
1890
1897
  //#endregion
1898
+ //#region src/vue/components/CollectionBacklinksView.vue?vue&type=script&setup=true&lang.ts
1899
+ var _hoisted_1$12 = ["data-testid"];
1900
+ var _hoisted_2$11 = { class: "w-full text-[11px] text-slate-600 bg-white" };
1901
+ var _hoisted_3$11 = { class: "bg-slate-50 border-b border-slate-200 text-slate-500 font-bold uppercase tracking-wider" };
1902
+ var _hoisted_4$11 = { class: "divide-y divide-slate-100" };
1903
+ var _hoisted_5$10 = [
1904
+ "data-testid",
1905
+ "onClick",
1906
+ "onKeydown"
1907
+ ];
1908
+ var _hoisted_6$9 = ["data-testid"];
1909
+ //#endregion
1910
+ //#region src/vue/components/CollectionBacklinksView.vue
1911
+ var CollectionBacklinksView_default = /* @__PURE__ */ (0, vue.defineComponent)({
1912
+ __name: "CollectionBacklinksView",
1913
+ props: {
1914
+ view: {},
1915
+ fieldKey: {}
1916
+ },
1917
+ setup(__props) {
1918
+ const { t } = useCollectionI18n();
1919
+ return (_ctx, _cache) => {
1920
+ return __props.view.rows.length > 0 ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
1921
+ key: 0,
1922
+ class: "border border-slate-200/80 rounded-xl overflow-hidden shadow-sm mt-1",
1923
+ "data-testid": `collections-backlinks-${__props.fieldKey}`
1924
+ }, [(0, vue.createElementVNode)("table", _hoisted_2$11, [(0, vue.createElementVNode)("thead", _hoisted_3$11, [(0, vue.createElementVNode)("tr", null, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(__props.view.columns, (column) => {
1925
+ return (0, vue.openBlock)(), (0, vue.createElementBlock)("th", {
1926
+ key: column.key,
1927
+ class: "text-left px-4 py-2 font-bold"
1928
+ }, (0, vue.toDisplayString)(column.label), 1);
1929
+ }), 128))])]), (0, vue.createElementVNode)("tbody", _hoisted_4$11, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(__props.view.rows, (row) => {
1930
+ return (0, vue.openBlock)(), (0, vue.createElementBlock)("tr", {
1931
+ key: row.id,
1932
+ class: "group hover:bg-indigo-50/30 cursor-pointer transition-colors",
1933
+ role: "link",
1934
+ tabindex: "0",
1935
+ "data-testid": `collections-backlinks-${__props.fieldKey}-${row.id}`,
1936
+ onClick: ($event) => (0, vue.unref)(activateRefLink)($event, __props.view.fromSlug, row.id),
1937
+ onKeydown: [(0, vue.withKeys)(($event) => (0, vue.unref)(activateRefLink)($event, __props.view.fromSlug, row.id), ["enter"]), (0, vue.withKeys)(($event) => (0, vue.unref)(activateRefLink)($event, __props.view.fromSlug, row.id), ["space"])]
1938
+ }, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(row.cells, (cell, cellIdx) => {
1939
+ return (0, vue.openBlock)(), (0, vue.createElementBlock)("td", {
1940
+ key: __props.view.columns[cellIdx]?.key ?? cellIdx,
1941
+ class: "px-4 py-2 align-middle font-medium"
1942
+ }, [(0, vue.createElementVNode)("span", { class: (0, vue.normalizeClass)(cellIdx === 0 ? "text-indigo-600 group-hover:text-indigo-800 font-bold" : "") }, (0, vue.toDisplayString)(cell), 3)]);
1943
+ }), 128))], 40, _hoisted_5$10);
1944
+ }), 128))])])], 8, _hoisted_1$12)) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", {
1945
+ key: 1,
1946
+ class: "text-slate-400 italic",
1947
+ "data-testid": `collections-backlinks-${__props.fieldKey}`
1948
+ }, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.noRows")), 9, _hoisted_6$9));
1949
+ };
1950
+ }
1951
+ });
1952
+ //#endregion
1891
1953
  //#region src/vue/components/CollectionEmbedView.vue?vue&type=script&setup=true&lang.ts
1892
1954
  var _hoisted_1$11 = [
1893
1955
  "href",
@@ -2188,7 +2250,7 @@ var _hoisted_71$1 = [
2188
2250
  "onClick"
2189
2251
  ];
2190
2252
  var _hoisted_72$1 = {
2191
- key: 13,
2253
+ key: 14,
2192
2254
  class: "text-slate-800 font-semibold"
2193
2255
  };
2194
2256
  var _hoisted_73$1 = {
@@ -2261,6 +2323,7 @@ var CollectionRecordPanel_default = /* @__PURE__ */ (0, vue.defineComponent)({
2261
2323
  * safe in the template. */
2262
2324
  const detailRecord = (0, vue.computed)(() => editing.value ? props.liveDerived ?? props.liveRecord ?? {} : props.viewing ?? {});
2263
2325
  const embedViews = (0, vue.computed)(() => props.render.embedViewsFor(detailRecord.value));
2326
+ const backlinksViews = (0, vue.computed)(() => props.render.backlinksViewsFor(detailRecord.value));
2264
2327
  const embedOwnerByKey = (0, vue.computed)(() => {
2265
2328
  const map = /* @__PURE__ */ new Map();
2266
2329
  for (const field of Object.values(props.collection.schema.fields)) if (field.type === "embed" && field.idField) map.set(field.idField, field);
@@ -2277,11 +2340,12 @@ var CollectionRecordPanel_default = /* @__PURE__ */ (0, vue.defineComponent)({
2277
2340
  return editing.value.mode === "create" ? "collections-create" : "collections-edit";
2278
2341
  });
2279
2342
  /** Whether a field gets an editable control in edit mode. Toggle (a
2280
- * projection of an enum that has its own input), derived (computed), and
2281
- * embed (a foreign record) stay read-only in both modes, so the cell
2282
- * geometry never changes on the view↔edit toggle. */
2343
+ * projection of an enum that has its own input), derived (computed),
2344
+ * embed (a foreign record), and backlinks (reverse refs owned by OTHER
2345
+ * records) stay read-only in both modes, so the cell geometry never
2346
+ * changes on the view↔edit toggle. */
2283
2347
  function isEditableType(type) {
2284
- return type !== "toggle" && type !== "derived" && type !== "embed";
2348
+ return type !== "toggle" && type !== "derived" && type !== "embed" && type !== "backlinks";
2285
2349
  }
2286
2350
  /** Wide field types span the full grid width in BOTH modes — keeping
2287
2351
  * `image` full-width here (not just when viewing) is what stops a field
@@ -2291,6 +2355,7 @@ var CollectionRecordPanel_default = /* @__PURE__ */ (0, vue.defineComponent)({
2291
2355
  "table",
2292
2356
  "markdown",
2293
2357
  "embed",
2358
+ "backlinks",
2294
2359
  "image"
2295
2360
  ].includes(field.type) ? "col-span-full" : "col-span-1";
2296
2361
  }
@@ -2315,7 +2380,8 @@ var CollectionRecordPanel_default = /* @__PURE__ */ (0, vue.defineComponent)({
2315
2380
  /** Required flag for an embed's per-record picker — read off the storage
2316
2381
  * field it writes (`idField`), since the embed itself stores nothing. */
2317
2382
  function embedPickerRequired(field) {
2318
- const target = field.idField ? props.collection.schema.fields[field.idField] : void 0;
2383
+ const idField = field.type === "embed" ? field.idField : void 0;
2384
+ const target = idField ? props.collection.schema.fields[idField] : void 0;
2319
2385
  return target ? isFieldRequiredInUi(target) : false;
2320
2386
  }
2321
2387
  /** Tailwind fill/text/border classes tinting an enum `<select>` by its current
@@ -2592,28 +2658,32 @@ var CollectionRecordPanel_default = /* @__PURE__ */ (0, vue.defineComponent)({
2592
2658
  key: 8,
2593
2659
  view: embedViews.value[key],
2594
2660
  "field-key": String(key)
2595
- }, null, 8, ["view", "field-key"])) : field.type === "image" && typeof detailRecord.value[key] === "string" && detailRecord.value[key] ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("img", {
2661
+ }, null, 8, ["view", "field-key"])) : field.type === "backlinks" && backlinksViews.value[key] ? ((0, vue.openBlock)(), (0, vue.createBlock)(CollectionBacklinksView_default, {
2596
2662
  key: 9,
2663
+ view: backlinksViews.value[key],
2664
+ "field-key": String(key)
2665
+ }, null, 8, ["view", "field-key"])) : field.type === "image" && typeof detailRecord.value[key] === "string" && detailRecord.value[key] ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("img", {
2666
+ key: 10,
2597
2667
  src: (0, vue.unref)(resolveImageSrc)(String(detailRecord.value[key])),
2598
2668
  alt: field.label,
2599
2669
  class: "max-h-64 max-w-full object-contain rounded-lg border border-slate-200 bg-slate-50",
2600
2670
  "data-testid": `collections-detail-image-${key}`
2601
2671
  }, null, 8, _hoisted_68$1)) : field.type !== "file" && __props.render.isExternalUrl(detailRecord.value[key]) ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("a", {
2602
- key: 10,
2672
+ key: 11,
2603
2673
  href: String(detailRecord.value[key]),
2604
2674
  target: "_blank",
2605
2675
  rel: "noopener noreferrer",
2606
2676
  class: "text-blue-600 hover:text-blue-800 font-semibold hover:underline break-all",
2607
2677
  "data-testid": `collections-detail-url-${key}`
2608
2678
  }, (0, vue.toDisplayString)(String(detailRecord.value[key])), 9, _hoisted_69$1)) : field.type === "file" && __props.render.artifactUrl(detailRecord.value[key]) ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("a", {
2609
- key: 11,
2679
+ key: 12,
2610
2680
  href: __props.render.artifactUrl(detailRecord.value[key]) ?? void 0,
2611
2681
  target: "_blank",
2612
2682
  rel: "noopener noreferrer",
2613
2683
  class: "text-blue-600 hover:text-blue-800 font-semibold hover:underline break-all",
2614
2684
  "data-testid": `collections-detail-file-${key}`
2615
2685
  }, (0, vue.toDisplayString)(String(detailRecord.value[key])), 9, _hoisted_70$1)) : field.type === "file" && __props.render.fileRoutePath(detailRecord.value[key]) ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("a", {
2616
- key: 12,
2686
+ key: 13,
2617
2687
  href: __props.render.fileRoutePath(detailRecord.value[key]) ?? void 0,
2618
2688
  class: "text-blue-600 hover:text-blue-800 font-semibold hover:underline break-all",
2619
2689
  "data-testid": `collections-detail-file-${key}`,
@@ -3067,8 +3137,11 @@ function formatCell(value, type) {
3067
3137
  }
3068
3138
  /** Resolve the ISO 4217 code for a money field: a per-record
3069
3139
  * `currencyField` (when present and non-blank) wins over the field's
3070
- * literal `currency`. */
3140
+ * literal `currency`. Only `money` / `derived` variants carry currency
3141
+ * keys; any other field resolves to undefined (the formatter's USD
3142
+ * fallback), as before. */
3071
3143
  function resolveCurrency(field, record) {
3144
+ if (field.type !== "money" && field.type !== "derived") return void 0;
3072
3145
  if (field.currencyField && record) {
3073
3146
  const code = record[field.currencyField];
3074
3147
  if (typeof code === "string" && code.trim().length > 0) return code;
@@ -3130,6 +3203,14 @@ function uniqueEmbedTargets(schema) {
3130
3203
  for (const field of Object.values(schema.fields)) if (field.type === "embed" && typeof field.to === "string" && field.to.length > 0) targets.add(field.to);
3131
3204
  return [...targets];
3132
3205
  }
3206
+ /** Slugs of every SOURCE collection a `backlinks` field reverses over.
3207
+ * Top-level only, like `embed` (the schema rejects `backlinks` inside a
3208
+ * table's `of`). Mirrors the server's `uniqueBacklinkSources`. */
3209
+ function uniqueBacklinkSources(schema) {
3210
+ const sources = /* @__PURE__ */ new Set();
3211
+ for (const field of Object.values(schema.fields)) if (field.type === "backlinks" && field.from.length > 0) sources.add(field.from);
3212
+ return [...sources];
3213
+ }
3133
3214
  function buildRefDisplayMap(detail) {
3134
3215
  const { fields, primaryKey } = detail.collection.schema;
3135
3216
  const displayField = displayFieldFor(fields, primaryKey);
@@ -3142,15 +3223,23 @@ function buildRefDisplayMap(detail) {
3142
3223
  }
3143
3224
  return map;
3144
3225
  }
3145
- function buildRefRecordMap(detail) {
3146
- const { schema } = detail.collection;
3226
+ /** Index DERIVED records by primary key — the client mirror of the
3227
+ * server's `loadTarget` indexing: string non-empty ids only, one record
3228
+ * per id (a duplicate keeps the LAST record in its FIRST position —
3229
+ * plain-object key semantics). Shared by the ref-record cache and the
3230
+ * backlinks view so every client consumer agrees with server enrichment
3231
+ * on which source records exist. */
3232
+ function derivedRecordsById(schema, items) {
3147
3233
  const map = {};
3148
- for (const item of detail.items) {
3234
+ for (const item of items) {
3149
3235
  const slugRaw = item[schema.primaryKey];
3150
3236
  if (typeof slugRaw === "string" && slugRaw.length > 0) map[slugRaw] = (0, _mulmoclaude_core_collection.deriveAll)(schema, item, {});
3151
3237
  }
3152
3238
  return map;
3153
3239
  }
3240
+ function buildRefRecordMap(detail) {
3241
+ return derivedRecordsById(detail.collection.schema, detail.items);
3242
+ }
3154
3243
  function sortedRefOptions(map) {
3155
3244
  return Object.entries(map).map(([slug, display]) => ({
3156
3245
  slug,
@@ -3173,8 +3262,64 @@ function buildEmbedOptions(schema, items) {
3173
3262
  }).filter((opt) => opt.slug.length > 0).sort((left, right) => left.display.localeCompare(right.display));
3174
3263
  }
3175
3264
  //#endregion
3176
- //#region src/vue/useCollectionRendering.ts
3177
- function useCollectionRendering(collection, locale) {
3265
+ //#region src/vue/useLinkedCollectionCaches.ts
3266
+ /** The de-duplicated ref + embed target slugs a schema links to. `allTargets`
3267
+ * is the union (each target fetched once even when both ref'd and embedded).
3268
+ * `backlinks` SOURCE collections ride in `embedTargets`: a backlink needs
3269
+ * exactly what an embed target caches (the source's schema + items in
3270
+ * `embedCache`), so reverse sources reuse that cache rather than adding a
3271
+ * parallel one. */
3272
+ function linkedTargets(schema) {
3273
+ const refTargets = new Set(uniqueRefTargets(schema));
3274
+ const embedTargets = /* @__PURE__ */ new Set([...uniqueEmbedTargets(schema), ...uniqueBacklinkSources(schema)]);
3275
+ return {
3276
+ refTargets,
3277
+ embedTargets,
3278
+ allTargets: [.../* @__PURE__ */ new Set([...refTargets, ...embedTargets])]
3279
+ };
3280
+ }
3281
+ /** Fan-out fetch that hydrates the linked-collection caches. Best-effort: a
3282
+ * target whose fetch *rejects* (vs. resolving `{ ok: false }`) is coerced to a
3283
+ * skip and must not abort the others. Returns null when a quicker subsequent
3284
+ * load has already moved on (stale-write guard) so the caller drops the write.
3285
+ * Pure + injectable (`fetchDetail`, `currentSlug`) so both paths are testable. */
3286
+ async function fetchLinkedCaches(targets, fetchDetail, currentSlug, expectedSlug) {
3287
+ const { refTargets, embedTargets, allTargets } = targets;
3288
+ const results = await Promise.all(allTargets.map(async (target) => {
3289
+ try {
3290
+ return {
3291
+ target,
3292
+ result: await fetchDetail(target)
3293
+ };
3294
+ } catch {
3295
+ return {
3296
+ target,
3297
+ result: { ok: false }
3298
+ };
3299
+ }
3300
+ }));
3301
+ if (currentSlug() !== expectedSlug) return null;
3302
+ const refCache = {};
3303
+ const refRecordCache = {};
3304
+ const embedCache = {};
3305
+ for (const { target, result } of results) {
3306
+ if (!result.ok) continue;
3307
+ if (refTargets.has(target)) {
3308
+ refCache[target] = buildRefDisplayMap(result.data);
3309
+ refRecordCache[target] = buildRefRecordMap(result.data);
3310
+ }
3311
+ if (embedTargets.has(target)) embedCache[target] = {
3312
+ schema: result.data.collection.schema,
3313
+ items: result.data.items
3314
+ };
3315
+ }
3316
+ return {
3317
+ refCache,
3318
+ refRecordCache,
3319
+ embedCache
3320
+ };
3321
+ }
3322
+ function useLinkedCollectionCaches(collection) {
3178
3323
  const refCache = (0, vue.ref)({});
3179
3324
  const refRecordCache = (0, vue.ref)({});
3180
3325
  const embedCache = (0, vue.ref)({});
@@ -3184,161 +3329,192 @@ function useCollectionRendering(collection, locale) {
3184
3329
  embedCache.value = {};
3185
3330
  }
3186
3331
  async function loadLinkedCollections(schema, expectedSlug) {
3187
- const refTargets = new Set(uniqueRefTargets(schema));
3188
- const embedTargets = new Set(uniqueEmbedTargets(schema));
3189
- const allTargets = [.../* @__PURE__ */ new Set([...refTargets, ...embedTargets])];
3190
- if (allTargets.length === 0) return;
3332
+ const targets = linkedTargets(schema);
3333
+ if (targets.allTargets.length === 0) return;
3191
3334
  const binding = collectionUi();
3192
- const results = await Promise.all(allTargets.map(async (target) => {
3193
- try {
3194
- return {
3195
- target,
3196
- result: await binding.fetchCollectionDetail(target)
3197
- };
3198
- } catch {
3199
- return {
3200
- target,
3201
- result: { ok: false }
3202
- };
3203
- }
3204
- }));
3205
- if (collection.value?.slug !== expectedSlug) return;
3206
- const nextRef = {};
3207
- const nextRefRecords = {};
3208
- const nextEmbed = {};
3209
- for (const { target, result } of results) {
3210
- if (!result.ok) continue;
3211
- if (refTargets.has(target)) {
3212
- nextRef[target] = buildRefDisplayMap(result.data);
3213
- nextRefRecords[target] = buildRefRecordMap(result.data);
3214
- }
3215
- if (embedTargets.has(target)) nextEmbed[target] = {
3216
- schema: result.data.collection.schema,
3217
- items: result.data.items
3218
- };
3219
- }
3220
- refCache.value = nextRef;
3221
- refRecordCache.value = nextRefRecords;
3222
- embedCache.value = nextEmbed;
3223
- }
3224
- function refDisplay(targetSlug, itemSlug) {
3225
- const map = refCache.value[targetSlug];
3226
- return map && map[itemSlug] || itemSlug;
3227
- }
3228
- function refOptions(targetSlug) {
3229
- const map = refCache.value[targetSlug];
3230
- return map ? sortedRefOptions(map) : [];
3231
- }
3232
- /** Dropdown options for an `embed` field's per-record picker (`idField`):
3233
- * every record in the target collection, labelled by its name/title (or
3234
- * primary key). Built from `embedCache` so it works for embed targets
3235
- * that aren't also `ref` targets (the profile collection, say). */
3236
- function embedOptions(targetSlug) {
3237
- const data = embedCache.value[targetSlug];
3238
- return data ? buildEmbedOptions(data.schema, data.items) : [];
3335
+ const snapshot = await fetchLinkedCaches(targets, (slug) => binding.fetchCollectionDetail(slug), () => collection.value?.slug, expectedSlug);
3336
+ if (!snapshot) return;
3337
+ refCache.value = snapshot.refCache;
3338
+ refRecordCache.value = snapshot.refRecordCache;
3339
+ embedCache.value = snapshot.embedCache;
3239
3340
  }
3240
- function resolveEmbed(field, record) {
3241
- if (field.type !== "embed" || !field.to) return {
3242
- schema: null,
3243
- item: null
3244
- };
3245
- const targetId = (0, _mulmoclaude_core_collection.embedTargetId)(field, record);
3246
- const data = targetId ? embedCache.value[field.to] : void 0;
3247
- if (!data) return {
3248
- schema: null,
3249
- item: null
3250
- };
3251
- const item = data.items.find((entry) => String(entry[data.schema.primaryKey] ?? "") === targetId) ?? null;
3252
- return {
3253
- schema: data.schema,
3254
- item
3341
+ return {
3342
+ refCache,
3343
+ refRecordCache,
3344
+ embedCache,
3345
+ resetLinkedCaches,
3346
+ loadLinkedCollections
3347
+ };
3348
+ }
3349
+ //#endregion
3350
+ //#region src/vue/useCollectionRendering.renderers.ts
3351
+ function lookupRefDisplay(refCache, targetSlug, itemSlug) {
3352
+ const map = refCache[targetSlug];
3353
+ return map && map[itemSlug] || itemSlug;
3354
+ }
3355
+ function refOptionsFor(refCache, targetSlug) {
3356
+ const map = refCache[targetSlug];
3357
+ return map ? sortedRefOptions(map) : [];
3358
+ }
3359
+ /** Dropdown options for an `embed` field's per-record picker (`idField`):
3360
+ * every record in the target collection, labelled by its name/title (or
3361
+ * primary key). Built from `embedCache` so it works for embed targets
3362
+ * that aren't also `ref` targets (the profile collection, say). */
3363
+ function embedOptionsFor(embedCache, targetSlug) {
3364
+ const data = embedCache[targetSlug];
3365
+ return data ? buildEmbedOptions(data.schema, data.items) : [];
3366
+ }
3367
+ function resolveEmbed(field, record, embedCache) {
3368
+ if (field.type !== "embed" || !field.to) return {
3369
+ schema: null,
3370
+ item: null
3371
+ };
3372
+ const targetId = (0, _mulmoclaude_core_collection.embedTargetId)(field, record);
3373
+ const data = targetId ? embedCache[field.to] : void 0;
3374
+ if (!data) return {
3375
+ schema: null,
3376
+ item: null
3377
+ };
3378
+ const item = data.items.find((entry) => String(entry[data.schema.primaryKey] ?? "") === targetId) ?? null;
3379
+ return {
3380
+ schema: data.schema,
3381
+ item
3382
+ };
3383
+ }
3384
+ function formatEmbedValue(field, value, record, locale) {
3385
+ if (field.type === "money") return formatMoney(value, resolveCurrency(field, record), locale);
3386
+ return detailText(value);
3387
+ }
3388
+ /** Build the read-only embed view-models for one record. A function of the
3389
+ * open record (not a bare computed) because a per-record `idField` embed
3390
+ * resolves a different target per row. `schema` is the OPEN collection's
3391
+ * schema (null when no collection is loaded → no views). */
3392
+ function buildEmbedViews(schema, embedCache, record, locale) {
3393
+ const out = {};
3394
+ if (!schema) return out;
3395
+ for (const [key, field] of Object.entries(schema.fields)) {
3396
+ if (field.type !== "embed") continue;
3397
+ const { schema: targetSchema, item } = resolveEmbed(field, record, embedCache);
3398
+ const rows = [];
3399
+ if (targetSchema && item) for (const [subKey, subField] of Object.entries(targetSchema.fields)) {
3400
+ const value = item[subKey];
3401
+ if (value === void 0 || value === null || value === "") continue;
3402
+ rows.push({
3403
+ key: subKey,
3404
+ label: subField.label,
3405
+ type: subField.type,
3406
+ value,
3407
+ display: formatEmbedValue(subField, value, item, locale)
3408
+ });
3409
+ }
3410
+ out[key] = {
3411
+ found: Boolean(item),
3412
+ rows,
3413
+ targetSlug: field.to ?? "",
3414
+ recordId: (0, _mulmoclaude_core_collection.embedTargetId)(field, record)
3255
3415
  };
3256
3416
  }
3257
- function embedValue(field, value, record) {
3258
- if (field.type === "money") return formatMoney(value, resolveCurrency(field, record), locale.value);
3259
- return detailText(value);
3260
- }
3261
- /** Build the read-only embed view-models for one record. A function of
3262
- * the open record (not a bare computed) because a per-record `idField`
3263
- * embed resolves a different target per row. */
3264
- function embedViewsFor(record) {
3265
- const out = {};
3266
- if (!collection.value) return out;
3267
- for (const [key, field] of Object.entries(collection.value.schema.fields)) {
3268
- if (field.type !== "embed") continue;
3269
- const { schema, item } = resolveEmbed(field, record);
3270
- const rows = [];
3271
- if (schema && item) for (const [subKey, subField] of Object.entries(schema.fields)) {
3272
- const value = item[subKey];
3273
- if (value === void 0 || value === null || value === "") continue;
3274
- rows.push({
3275
- key: subKey,
3276
- label: subField.label,
3277
- type: subField.type,
3278
- value,
3279
- display: embedValue(subField, value, item)
3280
- });
3281
- }
3417
+ return out;
3418
+ }
3419
+ /** One backlinks table CELL: money formatted with its currency, anything
3420
+ * else through the same `formatCell` list tables use — so a markdown
3421
+ * source column (a worklog `notes`) shows the one-line 80-char preview,
3422
+ * not the whole text. Unknown `display` key plain text (fail-soft). */
3423
+ function formatBacklinkCell(sourceField, value, row, locale) {
3424
+ if (!sourceField) return detailText(value);
3425
+ if (sourceField.type === "money") return formatMoney(value, resolveCurrency(sourceField, row), locale);
3426
+ return formatCell(value, sourceField.type);
3427
+ }
3428
+ /** Build the read-only backlinks view-models for one record: for each
3429
+ * `backlinks` field, the rows of `from` whose `via` points at the open
3430
+ * record (matched via the SHARED `backlinkRows`, on source records
3431
+ * derived exactly like the server's enrichment so a `display`/`filter`
3432
+ * on a derived column such as an invoice `total` agrees on both sides).
3433
+ * Source data comes out of `embedCache` (reverse sources ride the embed
3434
+ * fan-out — see `linkedTargets`). Fail-soft: an unloadable source or an
3435
+ * unknown `display` key degrades to `found: false` / a raw-key column,
3436
+ * never a throw. */
3437
+ function buildBacklinksViews(schema, embedCache, record, locale) {
3438
+ const out = {};
3439
+ if (!schema) return out;
3440
+ for (const [key, field] of Object.entries(schema.fields)) {
3441
+ if (field.type !== "backlinks") continue;
3442
+ const columns = field.display.map((col) => ({
3443
+ key: col,
3444
+ label: col
3445
+ }));
3446
+ const data = embedCache[field.from];
3447
+ if (!data) {
3282
3448
  out[key] = {
3283
- found: Boolean(item),
3284
- rows,
3285
- targetSlug: field.to ?? "",
3286
- recordId: (0, _mulmoclaude_core_collection.embedTargetId)(field, record)
3449
+ found: false,
3450
+ columns,
3451
+ rows: [],
3452
+ fromSlug: field.from
3287
3453
  };
3288
- }
3289
- return out;
3290
- }
3291
- function currencySymbol(currency) {
3292
- return currencySymbolForLocale(currency, locale.value);
3293
- }
3294
- function artifactUrl(value) {
3295
- return collectionUi().fileAssetUrl(value);
3296
- }
3297
- function fileRoutePath(value) {
3298
- return collectionUi().fileRoutePath(value);
3299
- }
3300
- function formatSubCell(subField, value, record) {
3301
- if (subField.type === "money") return formatMoney(value, resolveCurrency(subField, record), locale.value);
3302
- if (subField.type === "ref" && subField.to && typeof value === "string" && value.length > 0) return refDisplay(subField.to, value);
3303
- return formatCell(value, subField.type);
3304
- }
3305
- const stepFor = stepForFieldType;
3306
- function evaluateDerivedAgainstItem(field, fieldKey, item) {
3307
- if (!field.formula || !collection.value) return null;
3308
- const result = (0, _mulmoclaude_core_collection.deriveAll)(collection.value.schema, item, refRecordCache.value)[fieldKey];
3309
- return typeof result === "number" && Number.isFinite(result) ? result : null;
3310
- }
3311
- function derivedDisplay(field, computedValue, record) {
3312
- if (computedValue === null || computedValue === void 0) return "—";
3313
- if (field.display === "money") return formatMoney(computedValue, resolveCurrency(field, record), locale.value);
3314
- return formatCell(computedValue, field.display ?? "number");
3454
+ continue;
3455
+ }
3456
+ for (const column of columns) column.label = data.schema.fields[column.key]?.label ?? column.key;
3457
+ const selfId = String(record?.[schema.primaryKey] ?? "");
3458
+ const sourceById = derivedRecordsById(data.schema, data.items);
3459
+ out[key] = {
3460
+ found: true,
3461
+ columns,
3462
+ rows: (0, _mulmoclaude_core_collection.backlinkRows)(field, selfId, Object.values(sourceById)).map((row) => ({
3463
+ id: String(row[data.schema.primaryKey] ?? ""),
3464
+ cells: columns.map((column) => formatBacklinkCell(data.schema.fields[column.key], row[column.key], row, locale))
3465
+ })),
3466
+ fromSlug: field.from
3467
+ };
3315
3468
  }
3469
+ return out;
3470
+ }
3471
+ function renderSubCell(subField, value, record, refCache, locale) {
3472
+ if (subField.type === "money") return formatMoney(value, resolveCurrency(subField, record), locale);
3473
+ if (subField.type === "ref" && subField.to && typeof value === "string" && value.length > 0) return lookupRefDisplay(refCache, subField.to, value);
3474
+ return formatCell(value, subField.type);
3475
+ }
3476
+ function evaluateDerived(field, fieldKey, item, schema, refRecords) {
3477
+ if (field.type !== "derived" || !schema) return null;
3478
+ const result = (0, _mulmoclaude_core_collection.deriveAll)(schema, item, refRecords)[fieldKey];
3479
+ return typeof result === "number" && Number.isFinite(result) ? result : null;
3480
+ }
3481
+ function renderDerived(field, computedValue, record, locale) {
3482
+ if (computedValue === null || computedValue === void 0) return "—";
3483
+ const display = field.type === "derived" ? field.display : void 0;
3484
+ if (display === "money") return formatMoney(computedValue, resolveCurrency(field, record), locale);
3485
+ return formatCell(computedValue, display ?? "number");
3486
+ }
3487
+ //#endregion
3488
+ //#region src/vue/useCollectionRendering.ts
3489
+ var STATELESS_RENDERERS = {
3490
+ resolveCurrency,
3491
+ formatMoney,
3492
+ formatCell,
3493
+ detailText,
3494
+ isExternalUrl,
3495
+ tableRows,
3496
+ hasTableRows,
3497
+ inputTypeFor,
3498
+ stepFor: stepForFieldType,
3499
+ deriveAll: _mulmoclaude_core_collection.deriveAll
3500
+ };
3501
+ function useCollectionRendering(collection, locale) {
3502
+ const caches = useLinkedCollectionCaches(collection);
3503
+ const { refCache, refRecordCache, embedCache } = caches;
3316
3504
  return {
3317
- refCache,
3318
- refRecordCache,
3319
- embedCache,
3320
- resetLinkedCaches,
3321
- loadLinkedCollections,
3322
- refDisplay,
3323
- refOptions,
3324
- embedOptions,
3325
- embedViewsFor,
3326
- resolveCurrency,
3327
- currencySymbol,
3328
- formatMoney,
3329
- formatCell,
3330
- detailText,
3331
- isExternalUrl,
3332
- artifactUrl,
3333
- fileRoutePath,
3334
- tableRows,
3335
- hasTableRows,
3336
- formatSubCell,
3337
- inputTypeFor,
3338
- stepFor,
3339
- deriveAll: _mulmoclaude_core_collection.deriveAll,
3340
- evaluateDerivedAgainstItem,
3341
- derivedDisplay
3505
+ ...caches,
3506
+ ...STATELESS_RENDERERS,
3507
+ refDisplay: (targetSlug, itemSlug) => lookupRefDisplay(refCache.value, targetSlug, itemSlug),
3508
+ refOptions: (targetSlug) => refOptionsFor(refCache.value, targetSlug),
3509
+ embedOptions: (targetSlug) => embedOptionsFor(embedCache.value, targetSlug),
3510
+ embedViewsFor: (record) => buildEmbedViews(collection.value?.schema ?? null, embedCache.value, record, locale.value),
3511
+ backlinksViewsFor: (record) => buildBacklinksViews(collection.value?.schema ?? null, embedCache.value, record, locale.value),
3512
+ currencySymbol: (currency) => currencySymbolForLocale(currency, locale.value),
3513
+ artifactUrl: (value) => collectionUi().fileAssetUrl(value),
3514
+ fileRoutePath: (value) => collectionUi().fileRoutePath(value),
3515
+ formatSubCell: (subField, value, record) => renderSubCell(subField, value, record, refCache.value, locale.value),
3516
+ evaluateDerivedAgainstItem: (field, fieldKey, item) => evaluateDerived(field, fieldKey, item, collection.value?.schema ?? null, refRecordCache.value),
3517
+ derivedDisplay: (field, computedValue, record) => renderDerived(field, computedValue, record, locale.value)
3342
3518
  };
3343
3519
  }
3344
3520
  //#endregion
@@ -3829,12 +4005,12 @@ var CollectionView_default = /* @__PURE__ */ (0, vue.defineComponent)({
3829
4005
  return scalarSortValue(field, item[key]);
3830
4006
  }
3831
4007
  /** Derived rows sort by their display type: money/number → numeric,
3832
- * date/datetime → epoch, anything else → the enriched value as a string. */
4008
+ * date → epoch, anything else → the enriched value as a string. */
3833
4009
  function derivedSortValue(field, key, item) {
3834
- const { display } = field;
4010
+ const display = field.type === "derived" ? field.display : void 0;
3835
4011
  if (display === void 0 || display === "number" || display === "money") return (0, _mulmoclaude_core_collection.numericSortValue)(evaluateDerivedAgainstItem(field, key, item));
3836
4012
  const enriched = collection.value ? render.deriveAll(collection.value.schema, item, render.refRecordCache.value) : item;
3837
- if (display === "date" || display === "datetime") return (0, _mulmoclaude_core_collection.dateSortValue)(enriched[key]);
4013
+ if (display === "date") return (0, _mulmoclaude_core_collection.dateSortValue)(enriched[key]);
3838
4014
  return (0, _mulmoclaude_core_collection.stringSortValue)(enriched[key]);
3839
4015
  }
3840
4016
  const sortedItems = (0, vue.computed)(() => {
@@ -4012,10 +4188,12 @@ var CollectionView_default = /* @__PURE__ */ (0, vue.defineComponent)({
4012
4188
  function buildChatSeed(slug, message, itemId) {
4013
4189
  const current = collection.value;
4014
4190
  if (current?.source !== "feed") return itemId ? `/${slug} id=${itemId} ${message}` : `/${slug} ${message}`;
4191
+ const dataPath = current.schema.dataPath ?? `data/feeds/${slug}`;
4192
+ const scoped = itemId ? `(for record \`${itemId}\`) ${message}` : message;
4015
4193
  return t("collectionsView.feedChatSeed", {
4016
4194
  slug,
4017
- dataPath: current.schema.dataPath ?? `data/feeds/${slug}`,
4018
- message: itemId ? `(for record \`${itemId}\`) ${message}` : message
4195
+ dataPath,
4196
+ message: scoped
4019
4197
  });
4020
4198
  }
4021
4199
  /** Start a new general-role chat seeded from the current view. */
@@ -4090,7 +4268,10 @@ var CollectionView_default = /* @__PURE__ */ (0, vue.defineComponent)({
4090
4268
  enumOriginallyEmpty.value = snapshotEmptyEnums(result.data.collection.schema, result.data.items);
4091
4269
  await render.loadLinkedCollections(result.data.collection.schema, slug);
4092
4270
  if (activeSlug.value !== slug) return;
4093
- if (viewing.value) viewing.value = findItemById(String(viewing.value[result.data.collection.schema.primaryKey] ?? "")) ?? null;
4271
+ if (viewing.value) {
4272
+ const openId = String(viewing.value[result.data.collection.schema.primaryKey] ?? "");
4273
+ viewing.value = findItemById(openId) ?? null;
4274
+ }
4094
4275
  }
4095
4276
  function maybeAutoRefreshFeed(slug) {
4096
4277
  if (embedded.value) return;
@@ -4104,7 +4285,7 @@ var CollectionView_default = /* @__PURE__ */ (0, vue.defineComponent)({
4104
4285
  * list table only (a whole embedded record doesn't fit a table cell,
4105
4286
  * and it'd be identical in every row). The detail modal and the edit
4106
4287
  * form iterate the full `schema.fields` so embeds render there too. */
4107
- const listColumnFields = (0, vue.computed)(() => collection.value ? Object.entries(collection.value.schema.fields).filter(([key, field]) => field.type !== "embed" && field.type !== "image" && key !== collection.value?.schema.primaryKey) : []);
4288
+ const listColumnFields = (0, vue.computed)(() => collection.value ? Object.entries(collection.value.schema.fields).filter(([key, field]) => field.type !== "embed" && field.type !== "backlinks" && field.type !== "image" && key !== collection.value?.schema.primaryKey) : []);
4108
4289
  /** True when the current collection declares `schema.singleton` —
4109
4290
  * exactly one record, its primary key fixed to the declared value. */
4110
4291
  const isSingleton = (0, vue.computed)(() => Boolean(collection.value?.schema.singleton));
@@ -4268,7 +4449,8 @@ var CollectionView_default = /* @__PURE__ */ (0, vue.defineComponent)({
4268
4449
  }
4269
4450
  /** Select a custom view by id (builds the `custom:<id>` mode key). */
4270
4451
  function setCustomView(viewId) {
4271
- view.value = `custom:${viewId}`;
4452
+ const mode = `custom:${viewId}`;
4453
+ view.value = mode;
4272
4454
  }
4273
4455
  /** A short, slug-safe id not already used by a loaded record. Collisions
4274
4456
  * are astronomically unlikely (32 bits), but we still re-roll a few
@@ -4292,7 +4474,7 @@ var CollectionView_default = /* @__PURE__ */ (0, vue.defineComponent)({
4292
4474
  boolOriginallyPresent[key] = false;
4293
4475
  boolTouched[key] = false;
4294
4476
  } else if (field.type === "table") table[key] = [];
4295
- else if (field.type !== "derived" && field.type !== "embed" && field.type !== "toggle") text[key] = "";
4477
+ else if (field.type !== "derived" && field.type !== "embed" && field.type !== "backlinks" && field.type !== "toggle") text[key] = "";
4296
4478
  const { singleton, primaryKey } = collection.value.schema;
4297
4479
  if (singleton) text[primaryKey] = singleton;
4298
4480
  else if (primaryKey in text) text[primaryKey] = generateUniqueItemId(primaryKey);
@@ -4324,7 +4506,7 @@ var CollectionView_default = /* @__PURE__ */ (0, vue.defineComponent)({
4324
4506
  } else if (field.type === "table" && field.of) {
4325
4507
  const sub = field.of;
4326
4508
  table[key] = (Array.isArray(raw) ? raw : []).filter((row) => Boolean(row) && typeof row === "object" && !Array.isArray(row)).map((row) => (0, _mulmoclaude_core_collection.rowFromItem)(row, sub));
4327
- } else if (field.type !== "derived" && field.type !== "embed" && field.type !== "toggle") text[key] = raw === void 0 || raw === null ? "" : String(raw);
4509
+ } else if (field.type !== "derived" && field.type !== "embed" && field.type !== "backlinks" && field.type !== "toggle") text[key] = raw === void 0 || raw === null ? "" : String(raw);
4328
4510
  }
4329
4511
  const primaryRaw = item[collection.value.schema.primaryKey];
4330
4512
  const originalId = typeof primaryRaw === "string" ? primaryRaw : String(primaryRaw ?? "");
@@ -4426,7 +4608,8 @@ var CollectionView_default = /* @__PURE__ */ (0, vue.defineComponent)({
4426
4608
  viewing.value = null;
4427
4609
  return;
4428
4610
  }
4429
- viewing.value = findItemById(selected) ?? null;
4611
+ const match = findItemById(selected) ?? null;
4612
+ viewing.value = match;
4430
4613
  }
4431
4614
  /** Title for the open-mode header: the record's primary-key value
4432
4615
  * (e.g. `INV-2026-0001`), falling back to the collection title.
@@ -4520,19 +4703,17 @@ var CollectionView_default = /* @__PURE__ */ (0, vue.defineComponent)({
4520
4703
  /** Whether a `toggle` field reads as checked: its projected enum field
4521
4704
  * currently equals `onValue`. The toggle stores nothing itself. */
4522
4705
  function toggleChecked(item, field) {
4523
- return field.field !== void 0 && String(item[field.field] ?? "") === field.onValue;
4706
+ return field.type === "toggle" && String(item[field.field] ?? "") === field.onValue;
4524
4707
  }
4525
4708
  /** Flip a `toggle`: write the projected enum field to `offValue` when
4526
4709
  * currently checked, else `onValue`. Reuses the inline-edit PUT path
4527
4710
  * (optimistic + rollback) — the toggle has no value of its own. */
4528
4711
  function commitToggle(item, field) {
4712
+ if (field.type !== "toggle" || !collection.value) return;
4529
4713
  const targetKey = field.field;
4530
- if (!targetKey || !collection.value) return;
4531
4714
  const enumField = collection.value.schema.fields[targetKey];
4532
4715
  if (!enumField) return;
4533
- const next = toggleChecked(item, field) ? field.offValue : field.onValue;
4534
- if (next === void 0) return;
4535
- commitInlineEdit(item, targetKey, enumField, next);
4716
+ commitInlineEdit(item, targetKey, enumField, toggleChecked(item, field) ? field.offValue : field.onValue);
4536
4717
  }
4537
4718
  async function confirmDelete(item) {
4538
4719
  if (!collection.value) return;