@mulmoclaude/collection-plugin 0.7.6 → 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",
@@ -1826,15 +1827,15 @@ var CollectionKanbanView_default = /* @__PURE__ */ (0, vue.defineComponent)({
1826
1827
  emit("move", itemId(item), next);
1827
1828
  }
1828
1829
  return (_ctx, _cache) => {
1829
- 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) => {
1830
1831
  return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
1831
1832
  key: column.value,
1832
1833
  "data-testid": `collection-kanban-column-${column.value || "uncategorized"}`,
1833
1834
  class: "w-72 shrink-0 flex flex-col bg-slate-100 rounded-lg"
1834
- }, [(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", {
1835
1836
  class: "font-semibold text-xs text-slate-600 truncate",
1836
1837
  title: column.label
1837
- }, (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), {
1838
1839
  "model-value": itemsByColumn(column.value),
1839
1840
  "item-key": __props.schema.primaryKey,
1840
1841
  group: "collection-kanban-cards",
@@ -1865,7 +1866,7 @@ var CollectionKanbanView_default = /* @__PURE__ */ (0, vue.defineComponent)({
1865
1866
  "model-value",
1866
1867
  "item-key",
1867
1868
  "onChange"
1868
- ])], 8, _hoisted_3$11);
1869
+ ])], 8, _hoisted_3$12);
1869
1870
  }), 128))])]);
1870
1871
  };
1871
1872
  }
@@ -1894,6 +1895,61 @@ function activatePathLink(event, path, stop = false) {
1894
1895
  nav(path);
1895
1896
  }
1896
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
1897
1953
  //#region src/vue/components/CollectionEmbedView.vue?vue&type=script&setup=true&lang.ts
1898
1954
  var _hoisted_1$11 = [
1899
1955
  "href",
@@ -2194,7 +2250,7 @@ var _hoisted_71$1 = [
2194
2250
  "onClick"
2195
2251
  ];
2196
2252
  var _hoisted_72$1 = {
2197
- key: 13,
2253
+ key: 14,
2198
2254
  class: "text-slate-800 font-semibold"
2199
2255
  };
2200
2256
  var _hoisted_73$1 = {
@@ -2267,6 +2323,7 @@ var CollectionRecordPanel_default = /* @__PURE__ */ (0, vue.defineComponent)({
2267
2323
  * safe in the template. */
2268
2324
  const detailRecord = (0, vue.computed)(() => editing.value ? props.liveDerived ?? props.liveRecord ?? {} : props.viewing ?? {});
2269
2325
  const embedViews = (0, vue.computed)(() => props.render.embedViewsFor(detailRecord.value));
2326
+ const backlinksViews = (0, vue.computed)(() => props.render.backlinksViewsFor(detailRecord.value));
2270
2327
  const embedOwnerByKey = (0, vue.computed)(() => {
2271
2328
  const map = /* @__PURE__ */ new Map();
2272
2329
  for (const field of Object.values(props.collection.schema.fields)) if (field.type === "embed" && field.idField) map.set(field.idField, field);
@@ -2283,11 +2340,12 @@ var CollectionRecordPanel_default = /* @__PURE__ */ (0, vue.defineComponent)({
2283
2340
  return editing.value.mode === "create" ? "collections-create" : "collections-edit";
2284
2341
  });
2285
2342
  /** Whether a field gets an editable control in edit mode. Toggle (a
2286
- * projection of an enum that has its own input), derived (computed), and
2287
- * embed (a foreign record) stay read-only in both modes, so the cell
2288
- * 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. */
2289
2347
  function isEditableType(type) {
2290
- return type !== "toggle" && type !== "derived" && type !== "embed";
2348
+ return type !== "toggle" && type !== "derived" && type !== "embed" && type !== "backlinks";
2291
2349
  }
2292
2350
  /** Wide field types span the full grid width in BOTH modes — keeping
2293
2351
  * `image` full-width here (not just when viewing) is what stops a field
@@ -2297,6 +2355,7 @@ var CollectionRecordPanel_default = /* @__PURE__ */ (0, vue.defineComponent)({
2297
2355
  "table",
2298
2356
  "markdown",
2299
2357
  "embed",
2358
+ "backlinks",
2300
2359
  "image"
2301
2360
  ].includes(field.type) ? "col-span-full" : "col-span-1";
2302
2361
  }
@@ -2599,28 +2658,32 @@ var CollectionRecordPanel_default = /* @__PURE__ */ (0, vue.defineComponent)({
2599
2658
  key: 8,
2600
2659
  view: embedViews.value[key],
2601
2660
  "field-key": String(key)
2602
- }, 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, {
2603
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,
2604
2667
  src: (0, vue.unref)(resolveImageSrc)(String(detailRecord.value[key])),
2605
2668
  alt: field.label,
2606
2669
  class: "max-h-64 max-w-full object-contain rounded-lg border border-slate-200 bg-slate-50",
2607
2670
  "data-testid": `collections-detail-image-${key}`
2608
2671
  }, null, 8, _hoisted_68$1)) : field.type !== "file" && __props.render.isExternalUrl(detailRecord.value[key]) ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("a", {
2609
- key: 10,
2672
+ key: 11,
2610
2673
  href: String(detailRecord.value[key]),
2611
2674
  target: "_blank",
2612
2675
  rel: "noopener noreferrer",
2613
2676
  class: "text-blue-600 hover:text-blue-800 font-semibold hover:underline break-all",
2614
2677
  "data-testid": `collections-detail-url-${key}`
2615
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", {
2616
- key: 11,
2679
+ key: 12,
2617
2680
  href: __props.render.artifactUrl(detailRecord.value[key]) ?? void 0,
2618
2681
  target: "_blank",
2619
2682
  rel: "noopener noreferrer",
2620
2683
  class: "text-blue-600 hover:text-blue-800 font-semibold hover:underline break-all",
2621
2684
  "data-testid": `collections-detail-file-${key}`
2622
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", {
2623
- key: 12,
2686
+ key: 13,
2624
2687
  href: __props.render.fileRoutePath(detailRecord.value[key]) ?? void 0,
2625
2688
  class: "text-blue-600 hover:text-blue-800 font-semibold hover:underline break-all",
2626
2689
  "data-testid": `collections-detail-file-${key}`,
@@ -3140,6 +3203,14 @@ function uniqueEmbedTargets(schema) {
3140
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);
3141
3204
  return [...targets];
3142
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
+ }
3143
3214
  function buildRefDisplayMap(detail) {
3144
3215
  const { fields, primaryKey } = detail.collection.schema;
3145
3216
  const displayField = displayFieldFor(fields, primaryKey);
@@ -3152,15 +3223,23 @@ function buildRefDisplayMap(detail) {
3152
3223
  }
3153
3224
  return map;
3154
3225
  }
3155
- function buildRefRecordMap(detail) {
3156
- 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) {
3157
3233
  const map = {};
3158
- for (const item of detail.items) {
3234
+ for (const item of items) {
3159
3235
  const slugRaw = item[schema.primaryKey];
3160
3236
  if (typeof slugRaw === "string" && slugRaw.length > 0) map[slugRaw] = (0, _mulmoclaude_core_collection.deriveAll)(schema, item, {});
3161
3237
  }
3162
3238
  return map;
3163
3239
  }
3240
+ function buildRefRecordMap(detail) {
3241
+ return derivedRecordsById(detail.collection.schema, detail.items);
3242
+ }
3164
3243
  function sortedRefOptions(map) {
3165
3244
  return Object.entries(map).map(([slug, display]) => ({
3166
3245
  slug,
@@ -3185,10 +3264,14 @@ function buildEmbedOptions(schema, items) {
3185
3264
  //#endregion
3186
3265
  //#region src/vue/useLinkedCollectionCaches.ts
3187
3266
  /** The de-duplicated ref + embed target slugs a schema links to. `allTargets`
3188
- * is the union (each target fetched once even when both ref'd and embedded). */
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. */
3189
3272
  function linkedTargets(schema) {
3190
3273
  const refTargets = new Set(uniqueRefTargets(schema));
3191
- const embedTargets = new Set(uniqueEmbedTargets(schema));
3274
+ const embedTargets = /* @__PURE__ */ new Set([...uniqueEmbedTargets(schema), ...uniqueBacklinkSources(schema)]);
3192
3275
  return {
3193
3276
  refTargets,
3194
3277
  embedTargets,
@@ -3333,6 +3416,58 @@ function buildEmbedViews(schema, embedCache, record, locale) {
3333
3416
  }
3334
3417
  return out;
3335
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) {
3448
+ out[key] = {
3449
+ found: false,
3450
+ columns,
3451
+ rows: [],
3452
+ fromSlug: field.from
3453
+ };
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
+ };
3468
+ }
3469
+ return out;
3470
+ }
3336
3471
  function renderSubCell(subField, value, record, refCache, locale) {
3337
3472
  if (subField.type === "money") return formatMoney(value, resolveCurrency(subField, record), locale);
3338
3473
  if (subField.type === "ref" && subField.to && typeof value === "string" && value.length > 0) return lookupRefDisplay(refCache, subField.to, value);
@@ -3373,6 +3508,7 @@ function useCollectionRendering(collection, locale) {
3373
3508
  refOptions: (targetSlug) => refOptionsFor(refCache.value, targetSlug),
3374
3509
  embedOptions: (targetSlug) => embedOptionsFor(embedCache.value, targetSlug),
3375
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),
3376
3512
  currencySymbol: (currency) => currencySymbolForLocale(currency, locale.value),
3377
3513
  artifactUrl: (value) => collectionUi().fileAssetUrl(value),
3378
3514
  fileRoutePath: (value) => collectionUi().fileRoutePath(value),
@@ -4149,7 +4285,7 @@ var CollectionView_default = /* @__PURE__ */ (0, vue.defineComponent)({
4149
4285
  * list table only (a whole embedded record doesn't fit a table cell,
4150
4286
  * and it'd be identical in every row). The detail modal and the edit
4151
4287
  * form iterate the full `schema.fields` so embeds render there too. */
4152
- 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) : []);
4153
4289
  /** True when the current collection declares `schema.singleton` —
4154
4290
  * exactly one record, its primary key fixed to the declared value. */
4155
4291
  const isSingleton = (0, vue.computed)(() => Boolean(collection.value?.schema.singleton));
@@ -4338,7 +4474,7 @@ var CollectionView_default = /* @__PURE__ */ (0, vue.defineComponent)({
4338
4474
  boolOriginallyPresent[key] = false;
4339
4475
  boolTouched[key] = false;
4340
4476
  } else if (field.type === "table") table[key] = [];
4341
- 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] = "";
4342
4478
  const { singleton, primaryKey } = collection.value.schema;
4343
4479
  if (singleton) text[primaryKey] = singleton;
4344
4480
  else if (primaryKey in text) text[primaryKey] = generateUniqueItemId(primaryKey);
@@ -4370,7 +4506,7 @@ var CollectionView_default = /* @__PURE__ */ (0, vue.defineComponent)({
4370
4506
  } else if (field.type === "table" && field.of) {
4371
4507
  const sub = field.of;
4372
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));
4373
- } 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);
4374
4510
  }
4375
4511
  const primaryRaw = item[collection.value.schema.primaryKey];
4376
4512
  const originalId = typeof primaryRaw === "string" ? primaryRaw : String(primaryRaw ?? "");