@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.js CHANGED
@@ -1,4 +1,4 @@
1
- import { MINUTES_PER_DAY, TOOL_DEFINITION, actionVisible, assignLanes, boolSortValue, bucketRecords, buildMonthGrid, buildUpdatedRecord, coerceInlineValue, dateOf, dateSortValue, daySlice, defangForPrompt, deriveAll, draftToRecord, embedTargetId, emptyRow, enumSortValue, errorMessage, executePresentCollection, fieldVisible, firstMissingRequiredField, isSortableField, itemIdOf, itemLabelOf, labelFieldFor, nextSortDirection, numericSortValue, resolveEnumColor, rowFromItem, shortHexId, sortItems, stringSortValue, ymdKey } from "@mulmoclaude/core/collection";
1
+ import { MINUTES_PER_DAY, TOOL_DEFINITION, actionVisible, assignLanes, backlinkRows, boolSortValue, bucketRecords, buildMonthGrid, buildUpdatedRecord, coerceInlineValue, dateOf, dateSortValue, daySlice, defangForPrompt, deriveAll, draftToRecord, embedTargetId, emptyRow, enumSortValue, errorMessage, executePresentCollection, fieldVisible, firstMissingRequiredField, isSortableField, itemIdOf, itemLabelOf, labelFieldFor, nextSortDirection, numericSortValue, resolveEnumColor, rowFromItem, shortHexId, sortItems, stringSortValue, ymdKey } from "@mulmoclaude/core/collection";
2
2
  import { Fragment, Teleport, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, effectScope, mergeModels, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, onUnmounted, openBlock, ref, renderList, renderSlot, resolveDynamicComponent, toDisplayString, unref, useModel, vModelCheckbox, vModelDynamic, vModelSelect, vModelText, watch, watchEffect, withCtx, withDirectives, withKeys, withModifiers } from "vue";
3
3
  import { createI18n } from "vue-i18n";
4
4
  import draggable from "vuedraggable";
@@ -1210,18 +1210,18 @@ var CollectionRecordModal_default = /* @__PURE__ */ defineComponent({
1210
1210
  });
1211
1211
  //#endregion
1212
1212
  //#region src/vue/components/CollectionCalendarView.vue?vue&type=script&setup=true&lang.ts
1213
- var _hoisted_1$14 = {
1213
+ var _hoisted_1$15 = {
1214
1214
  class: "flex flex-col gap-3",
1215
1215
  "data-testid": "collection-calendar"
1216
1216
  };
1217
- var _hoisted_2$13 = { class: "flex items-center gap-2" };
1218
- var _hoisted_3$13 = ["aria-label"];
1219
- var _hoisted_4$13 = ["aria-label"];
1220
- var _hoisted_5$12 = {
1217
+ var _hoisted_2$14 = { class: "flex items-center gap-2" };
1218
+ var _hoisted_3$14 = ["aria-label"];
1219
+ var _hoisted_4$14 = ["aria-label"];
1220
+ var _hoisted_5$13 = {
1221
1221
  class: "text-sm font-bold text-slate-800 flex-1",
1222
1222
  "data-testid": "collection-calendar-month"
1223
1223
  };
1224
- var _hoisted_6$11 = { class: "grid grid-cols-7 gap-1 text-[10px] font-bold text-slate-400 uppercase tracking-wider select-none" };
1224
+ var _hoisted_6$12 = { class: "grid grid-cols-7 gap-1 text-[10px] font-bold text-slate-400 uppercase tracking-wider select-none" };
1225
1225
  var _hoisted_7$10 = { class: "grid grid-cols-7 gap-1" };
1226
1226
  var _hoisted_8$10 = [
1227
1227
  "aria-label",
@@ -1351,23 +1351,23 @@ var CollectionCalendarView_default = /* @__PURE__ */ defineComponent({
1351
1351
  viewMonth.value = now.getMonth() + 1;
1352
1352
  }
1353
1353
  return (_ctx, _cache) => {
1354
- return openBlock(), createElementBlock("div", _hoisted_1$14, [
1355
- createElementVNode("div", _hoisted_2$13, [
1354
+ return openBlock(), createElementBlock("div", _hoisted_1$15, [
1355
+ createElementVNode("div", _hoisted_2$14, [
1356
1356
  createElementVNode("button", {
1357
1357
  type: "button",
1358
1358
  class: "h-8 w-8 flex items-center justify-center rounded text-slate-500 hover:bg-slate-100 transition-colors",
1359
1359
  "aria-label": unref(t)("collectionsView.calendarPrevMonth"),
1360
1360
  "data-testid": "collection-calendar-prev",
1361
1361
  onClick: _cache[0] || (_cache[0] = ($event) => stepMonth(-1))
1362
- }, [..._cache[2] || (_cache[2] = [createElementVNode("span", { class: "material-icons text-lg" }, "chevron_left", -1)])], 8, _hoisted_3$13),
1362
+ }, [..._cache[2] || (_cache[2] = [createElementVNode("span", { class: "material-icons text-lg" }, "chevron_left", -1)])], 8, _hoisted_3$14),
1363
1363
  createElementVNode("button", {
1364
1364
  type: "button",
1365
1365
  class: "h-8 w-8 flex items-center justify-center rounded text-slate-500 hover:bg-slate-100 transition-colors",
1366
1366
  "aria-label": unref(t)("collectionsView.calendarNextMonth"),
1367
1367
  "data-testid": "collection-calendar-next",
1368
1368
  onClick: _cache[1] || (_cache[1] = ($event) => stepMonth(1))
1369
- }, [..._cache[3] || (_cache[3] = [createElementVNode("span", { class: "material-icons text-lg" }, "chevron_right", -1)])], 8, _hoisted_4$13),
1370
- createElementVNode("h3", _hoisted_5$12, toDisplayString(monthLabel.value), 1),
1369
+ }, [..._cache[3] || (_cache[3] = [createElementVNode("span", { class: "material-icons text-lg" }, "chevron_right", -1)])], 8, _hoisted_4$14),
1370
+ createElementVNode("h3", _hoisted_5$13, toDisplayString(monthLabel.value), 1),
1371
1371
  createElementVNode("button", {
1372
1372
  type: "button",
1373
1373
  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",
@@ -1375,7 +1375,7 @@ var CollectionCalendarView_default = /* @__PURE__ */ defineComponent({
1375
1375
  onClick: goToday
1376
1376
  }, toDisplayString(unref(t)("collectionsView.calendarToday")), 1)
1377
1377
  ]),
1378
- createElementVNode("div", _hoisted_6$11, [(openBlock(true), createElementBlock(Fragment, null, renderList(weekdayLabels.value, (label, idx) => {
1378
+ createElementVNode("div", _hoisted_6$12, [(openBlock(true), createElementBlock(Fragment, null, renderList(weekdayLabels.value, (label, idx) => {
1379
1379
  return openBlock(), createElementBlock("div", {
1380
1380
  key: idx,
1381
1381
  class: "px-1 py-1 text-center"
@@ -1416,18 +1416,18 @@ var CollectionCalendarView_default = /* @__PURE__ */ defineComponent({
1416
1416
  });
1417
1417
  //#endregion
1418
1418
  //#region src/vue/components/CollectionDayView.vue?vue&type=script&setup=true&lang.ts
1419
- var _hoisted_1$13 = { class: "flex items-center gap-2 border-b border-slate-200 px-4 py-3" };
1420
- var _hoisted_2$12 = {
1419
+ var _hoisted_1$14 = { class: "flex items-center gap-2 border-b border-slate-200 px-4 py-3" };
1420
+ var _hoisted_2$13 = {
1421
1421
  class: "flex-1 text-sm font-bold text-slate-800",
1422
1422
  "data-testid": "collection-day-view-title"
1423
1423
  };
1424
- var _hoisted_3$12 = ["aria-label"];
1425
- var _hoisted_4$12 = ["aria-label"];
1426
- var _hoisted_5$11 = {
1424
+ var _hoisted_3$13 = ["aria-label"];
1425
+ var _hoisted_4$13 = ["aria-label"];
1426
+ var _hoisted_5$12 = {
1427
1427
  key: 0,
1428
1428
  class: "px-4 py-10 text-center text-sm text-slate-400"
1429
1429
  };
1430
- var _hoisted_6$10 = { class: "absolute -top-2 left-0 w-10 pr-1 text-right text-[10px] tabular-nums text-slate-400" };
1430
+ var _hoisted_6$11 = { class: "absolute -top-2 left-0 w-10 pr-1 text-right text-[10px] tabular-nums text-slate-400" };
1431
1431
  var _hoisted_7$9 = {
1432
1432
  class: "absolute inset-y-0 right-0",
1433
1433
  style: { "left": "2.75rem" }
@@ -1513,6 +1513,7 @@ var CollectionDayView_default = /* @__PURE__ */ defineComponent({
1513
1513
  "datetime",
1514
1514
  "table",
1515
1515
  "embed",
1516
+ "backlinks",
1516
1517
  "image",
1517
1518
  "markdown"
1518
1519
  ]);
@@ -1608,8 +1609,8 @@ var CollectionDayView_default = /* @__PURE__ */ defineComponent({
1608
1609
  role: "dialog",
1609
1610
  "aria-modal": "true"
1610
1611
  }, [createElementVNode("div", { class: normalizeClass(["flex min-h-0 flex-col", __props.showDetail ? "w-80 shrink-0 border-r border-slate-200" : "w-full"]) }, [
1611
- createElementVNode("div", _hoisted_1$13, [
1612
- createElementVNode("h3", _hoisted_2$12, toDisplayString(dayLabel.value), 1),
1612
+ createElementVNode("div", _hoisted_1$14, [
1613
+ createElementVNode("h3", _hoisted_2$13, toDisplayString(dayLabel.value), 1),
1613
1614
  __props.canCreate ? (openBlock(), createElementBlock("button", {
1614
1615
  key: 0,
1615
1616
  type: "button",
@@ -1617,16 +1618,16 @@ var CollectionDayView_default = /* @__PURE__ */ defineComponent({
1617
1618
  "aria-label": unref(t)("collectionsView.calendarCreateOn", { date: dayKey.value }),
1618
1619
  "data-testid": "collection-day-view-create",
1619
1620
  onClick: onCreate
1620
- }, [..._cache[3] || (_cache[3] = [createElementVNode("span", { class: "material-icons text-lg" }, "add", -1)])], 8, _hoisted_3$12)) : createCommentVNode("", true),
1621
+ }, [..._cache[3] || (_cache[3] = [createElementVNode("span", { class: "material-icons text-lg" }, "add", -1)])], 8, _hoisted_3$13)) : createCommentVNode("", true),
1621
1622
  createElementVNode("button", {
1622
1623
  type: "button",
1623
1624
  class: "h-8 w-8 flex items-center justify-center rounded text-slate-500 hover:bg-slate-100 transition-colors",
1624
1625
  "aria-label": unref(t)("collectionsView.dayViewClose"),
1625
1626
  "data-testid": "collection-day-view-close",
1626
1627
  onClick: _cache[0] || (_cache[0] = ($event) => emit("close"))
1627
- }, [..._cache[4] || (_cache[4] = [createElementVNode("span", { class: "material-icons text-lg" }, "close", -1)])], 8, _hoisted_4$12)
1628
+ }, [..._cache[4] || (_cache[4] = [createElementVNode("span", { class: "material-icons text-lg" }, "close", -1)])], 8, _hoisted_4$13)
1628
1629
  ]),
1629
- timedEntries.value.length === 0 && allDayEntries.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_5$11, toDisplayString(unref(t)("collectionsView.dayViewEmpty")), 1)) : (openBlock(), createElementBlock("div", {
1630
+ timedEntries.value.length === 0 && allDayEntries.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_5$12, toDisplayString(unref(t)("collectionsView.dayViewEmpty")), 1)) : (openBlock(), createElementBlock("div", {
1630
1631
  key: 1,
1631
1632
  ref_key: "scrollEl",
1632
1633
  ref: scrollEl,
@@ -1640,7 +1641,7 @@ var CollectionDayView_default = /* @__PURE__ */ defineComponent({
1640
1641
  key: hour,
1641
1642
  class: "absolute left-0 right-0 border-t border-slate-100",
1642
1643
  style: normalizeStyle({ top: `${(hour - 1) * HOUR_PX}px` })
1643
- }, [createElementVNode("span", _hoisted_6$10, toDisplayString(hourLabel(hour - 1)), 1)], 4);
1644
+ }, [createElementVNode("span", _hoisted_6$11, toDisplayString(hourLabel(hour - 1)), 1)], 4);
1644
1645
  }), 64)), createElementVNode("div", _hoisted_7$9, [(openBlock(true), createElementBlock(Fragment, null, renderList(timedEntries.value, (entry) => {
1645
1646
  return openBlock(), createElementBlock("button", {
1646
1647
  key: entry.id,
@@ -1675,15 +1676,15 @@ var CollectionDayView_default = /* @__PURE__ */ defineComponent({
1675
1676
  });
1676
1677
  //#endregion
1677
1678
  //#region src/vue/components/CollectionKanbanView.vue?vue&type=script&setup=true&lang.ts
1678
- var _hoisted_1$12 = {
1679
+ var _hoisted_1$13 = {
1679
1680
  class: "h-full overflow-x-auto overflow-y-hidden",
1680
1681
  "data-testid": "collection-kanban"
1681
1682
  };
1682
- var _hoisted_2$11 = { class: "flex gap-3 h-full p-1 min-w-max" };
1683
- var _hoisted_3$11 = ["data-testid"];
1684
- var _hoisted_4$11 = { class: "flex items-center justify-between px-3 py-2 border-b border-slate-200" };
1685
- var _hoisted_5$10 = { class: "flex items-center gap-2 min-w-0" };
1686
- var _hoisted_6$9 = ["title"];
1683
+ var _hoisted_2$12 = { class: "flex gap-3 h-full p-1 min-w-max" };
1684
+ var _hoisted_3$12 = ["data-testid"];
1685
+ var _hoisted_4$12 = { class: "flex items-center justify-between px-3 py-2 border-b border-slate-200" };
1686
+ var _hoisted_5$11 = { class: "flex items-center gap-2 min-w-0" };
1687
+ var _hoisted_6$10 = ["title"];
1687
1688
  var _hoisted_7$8 = { class: "text-[11px] text-slate-400 shrink-0" };
1688
1689
  var _hoisted_8$8 = [
1689
1690
  "data-testid",
@@ -1802,15 +1803,15 @@ var CollectionKanbanView_default = /* @__PURE__ */ defineComponent({
1802
1803
  emit("move", itemId(item), next);
1803
1804
  }
1804
1805
  return (_ctx, _cache) => {
1805
- return openBlock(), createElementBlock("div", _hoisted_1$12, [createElementVNode("div", _hoisted_2$11, [(openBlock(true), createElementBlock(Fragment, null, renderList(columns.value, (column) => {
1806
+ return openBlock(), createElementBlock("div", _hoisted_1$13, [createElementVNode("div", _hoisted_2$12, [(openBlock(true), createElementBlock(Fragment, null, renderList(columns.value, (column) => {
1806
1807
  return openBlock(), createElementBlock("div", {
1807
1808
  key: column.value,
1808
1809
  "data-testid": `collection-kanban-column-${column.value || "uncategorized"}`,
1809
1810
  class: "w-72 shrink-0 flex flex-col bg-slate-100 rounded-lg"
1810
- }, [createElementVNode("div", _hoisted_4$11, [createElementVNode("div", _hoisted_5$10, [createElementVNode("span", { class: normalizeClass(["w-2 h-2 rounded-full shrink-0", unref(resolveEnumColor)(__props.schema, __props.groupField, column.value).dot]) }, null, 2), createElementVNode("span", {
1811
+ }, [createElementVNode("div", _hoisted_4$12, [createElementVNode("div", _hoisted_5$11, [createElementVNode("span", { class: normalizeClass(["w-2 h-2 rounded-full shrink-0", unref(resolveEnumColor)(__props.schema, __props.groupField, column.value).dot]) }, null, 2), createElementVNode("span", {
1811
1812
  class: "font-semibold text-xs text-slate-600 truncate",
1812
1813
  title: column.label
1813
- }, toDisplayString(column.label), 9, _hoisted_6$9)]), createElementVNode("span", _hoisted_7$8, toDisplayString(itemsByColumn(column.value).length), 1)]), createVNode(unref(draggable), {
1814
+ }, toDisplayString(column.label), 9, _hoisted_6$10)]), createElementVNode("span", _hoisted_7$8, toDisplayString(itemsByColumn(column.value).length), 1)]), createVNode(unref(draggable), {
1814
1815
  "model-value": itemsByColumn(column.value),
1815
1816
  "item-key": __props.schema.primaryKey,
1816
1817
  group: "collection-kanban-cards",
@@ -1841,7 +1842,7 @@ var CollectionKanbanView_default = /* @__PURE__ */ defineComponent({
1841
1842
  "model-value",
1842
1843
  "item-key",
1843
1844
  "onChange"
1844
- ])], 8, _hoisted_3$11);
1845
+ ])], 8, _hoisted_3$12);
1845
1846
  }), 128))])]);
1846
1847
  };
1847
1848
  }
@@ -1870,6 +1871,61 @@ function activatePathLink(event, path, stop = false) {
1870
1871
  nav(path);
1871
1872
  }
1872
1873
  //#endregion
1874
+ //#region src/vue/components/CollectionBacklinksView.vue?vue&type=script&setup=true&lang.ts
1875
+ var _hoisted_1$12 = ["data-testid"];
1876
+ var _hoisted_2$11 = { class: "w-full text-[11px] text-slate-600 bg-white" };
1877
+ var _hoisted_3$11 = { class: "bg-slate-50 border-b border-slate-200 text-slate-500 font-bold uppercase tracking-wider" };
1878
+ var _hoisted_4$11 = { class: "divide-y divide-slate-100" };
1879
+ var _hoisted_5$10 = [
1880
+ "data-testid",
1881
+ "onClick",
1882
+ "onKeydown"
1883
+ ];
1884
+ var _hoisted_6$9 = ["data-testid"];
1885
+ //#endregion
1886
+ //#region src/vue/components/CollectionBacklinksView.vue
1887
+ var CollectionBacklinksView_default = /* @__PURE__ */ defineComponent({
1888
+ __name: "CollectionBacklinksView",
1889
+ props: {
1890
+ view: {},
1891
+ fieldKey: {}
1892
+ },
1893
+ setup(__props) {
1894
+ const { t } = useCollectionI18n();
1895
+ return (_ctx, _cache) => {
1896
+ return __props.view.rows.length > 0 ? (openBlock(), createElementBlock("div", {
1897
+ key: 0,
1898
+ class: "border border-slate-200/80 rounded-xl overflow-hidden shadow-sm mt-1",
1899
+ "data-testid": `collections-backlinks-${__props.fieldKey}`
1900
+ }, [createElementVNode("table", _hoisted_2$11, [createElementVNode("thead", _hoisted_3$11, [createElementVNode("tr", null, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.view.columns, (column) => {
1901
+ return openBlock(), createElementBlock("th", {
1902
+ key: column.key,
1903
+ class: "text-left px-4 py-2 font-bold"
1904
+ }, toDisplayString(column.label), 1);
1905
+ }), 128))])]), createElementVNode("tbody", _hoisted_4$11, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.view.rows, (row) => {
1906
+ return openBlock(), createElementBlock("tr", {
1907
+ key: row.id,
1908
+ class: "group hover:bg-indigo-50/30 cursor-pointer transition-colors",
1909
+ role: "link",
1910
+ tabindex: "0",
1911
+ "data-testid": `collections-backlinks-${__props.fieldKey}-${row.id}`,
1912
+ onClick: ($event) => unref(activateRefLink)($event, __props.view.fromSlug, row.id),
1913
+ onKeydown: [withKeys(($event) => unref(activateRefLink)($event, __props.view.fromSlug, row.id), ["enter"]), withKeys(($event) => unref(activateRefLink)($event, __props.view.fromSlug, row.id), ["space"])]
1914
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(row.cells, (cell, cellIdx) => {
1915
+ return openBlock(), createElementBlock("td", {
1916
+ key: __props.view.columns[cellIdx]?.key ?? cellIdx,
1917
+ class: "px-4 py-2 align-middle font-medium"
1918
+ }, [createElementVNode("span", { class: normalizeClass(cellIdx === 0 ? "text-indigo-600 group-hover:text-indigo-800 font-bold" : "") }, toDisplayString(cell), 3)]);
1919
+ }), 128))], 40, _hoisted_5$10);
1920
+ }), 128))])])], 8, _hoisted_1$12)) : (openBlock(), createElementBlock("span", {
1921
+ key: 1,
1922
+ class: "text-slate-400 italic",
1923
+ "data-testid": `collections-backlinks-${__props.fieldKey}`
1924
+ }, toDisplayString(unref(t)("collectionsView.noRows")), 9, _hoisted_6$9));
1925
+ };
1926
+ }
1927
+ });
1928
+ //#endregion
1873
1929
  //#region src/vue/components/CollectionEmbedView.vue?vue&type=script&setup=true&lang.ts
1874
1930
  var _hoisted_1$11 = [
1875
1931
  "href",
@@ -2170,7 +2226,7 @@ var _hoisted_71$1 = [
2170
2226
  "onClick"
2171
2227
  ];
2172
2228
  var _hoisted_72$1 = {
2173
- key: 13,
2229
+ key: 14,
2174
2230
  class: "text-slate-800 font-semibold"
2175
2231
  };
2176
2232
  var _hoisted_73$1 = {
@@ -2243,6 +2299,7 @@ var CollectionRecordPanel_default = /* @__PURE__ */ defineComponent({
2243
2299
  * safe in the template. */
2244
2300
  const detailRecord = computed(() => editing.value ? props.liveDerived ?? props.liveRecord ?? {} : props.viewing ?? {});
2245
2301
  const embedViews = computed(() => props.render.embedViewsFor(detailRecord.value));
2302
+ const backlinksViews = computed(() => props.render.backlinksViewsFor(detailRecord.value));
2246
2303
  const embedOwnerByKey = computed(() => {
2247
2304
  const map = /* @__PURE__ */ new Map();
2248
2305
  for (const field of Object.values(props.collection.schema.fields)) if (field.type === "embed" && field.idField) map.set(field.idField, field);
@@ -2259,11 +2316,12 @@ var CollectionRecordPanel_default = /* @__PURE__ */ defineComponent({
2259
2316
  return editing.value.mode === "create" ? "collections-create" : "collections-edit";
2260
2317
  });
2261
2318
  /** Whether a field gets an editable control in edit mode. Toggle (a
2262
- * projection of an enum that has its own input), derived (computed), and
2263
- * embed (a foreign record) stay read-only in both modes, so the cell
2264
- * geometry never changes on the view↔edit toggle. */
2319
+ * projection of an enum that has its own input), derived (computed),
2320
+ * embed (a foreign record), and backlinks (reverse refs owned by OTHER
2321
+ * records) stay read-only in both modes, so the cell geometry never
2322
+ * changes on the view↔edit toggle. */
2265
2323
  function isEditableType(type) {
2266
- return type !== "toggle" && type !== "derived" && type !== "embed";
2324
+ return type !== "toggle" && type !== "derived" && type !== "embed" && type !== "backlinks";
2267
2325
  }
2268
2326
  /** Wide field types span the full grid width in BOTH modes — keeping
2269
2327
  * `image` full-width here (not just when viewing) is what stops a field
@@ -2273,6 +2331,7 @@ var CollectionRecordPanel_default = /* @__PURE__ */ defineComponent({
2273
2331
  "table",
2274
2332
  "markdown",
2275
2333
  "embed",
2334
+ "backlinks",
2276
2335
  "image"
2277
2336
  ].includes(field.type) ? "col-span-full" : "col-span-1";
2278
2337
  }
@@ -2575,28 +2634,32 @@ var CollectionRecordPanel_default = /* @__PURE__ */ defineComponent({
2575
2634
  key: 8,
2576
2635
  view: embedViews.value[key],
2577
2636
  "field-key": String(key)
2578
- }, null, 8, ["view", "field-key"])) : field.type === "image" && typeof detailRecord.value[key] === "string" && detailRecord.value[key] ? (openBlock(), createElementBlock("img", {
2637
+ }, null, 8, ["view", "field-key"])) : field.type === "backlinks" && backlinksViews.value[key] ? (openBlock(), createBlock(CollectionBacklinksView_default, {
2579
2638
  key: 9,
2639
+ view: backlinksViews.value[key],
2640
+ "field-key": String(key)
2641
+ }, null, 8, ["view", "field-key"])) : field.type === "image" && typeof detailRecord.value[key] === "string" && detailRecord.value[key] ? (openBlock(), createElementBlock("img", {
2642
+ key: 10,
2580
2643
  src: unref(resolveImageSrc)(String(detailRecord.value[key])),
2581
2644
  alt: field.label,
2582
2645
  class: "max-h-64 max-w-full object-contain rounded-lg border border-slate-200 bg-slate-50",
2583
2646
  "data-testid": `collections-detail-image-${key}`
2584
2647
  }, null, 8, _hoisted_68$1)) : field.type !== "file" && __props.render.isExternalUrl(detailRecord.value[key]) ? (openBlock(), createElementBlock("a", {
2585
- key: 10,
2648
+ key: 11,
2586
2649
  href: String(detailRecord.value[key]),
2587
2650
  target: "_blank",
2588
2651
  rel: "noopener noreferrer",
2589
2652
  class: "text-blue-600 hover:text-blue-800 font-semibold hover:underline break-all",
2590
2653
  "data-testid": `collections-detail-url-${key}`
2591
2654
  }, toDisplayString(String(detailRecord.value[key])), 9, _hoisted_69$1)) : field.type === "file" && __props.render.artifactUrl(detailRecord.value[key]) ? (openBlock(), createElementBlock("a", {
2592
- key: 11,
2655
+ key: 12,
2593
2656
  href: __props.render.artifactUrl(detailRecord.value[key]) ?? void 0,
2594
2657
  target: "_blank",
2595
2658
  rel: "noopener noreferrer",
2596
2659
  class: "text-blue-600 hover:text-blue-800 font-semibold hover:underline break-all",
2597
2660
  "data-testid": `collections-detail-file-${key}`
2598
2661
  }, toDisplayString(String(detailRecord.value[key])), 9, _hoisted_70$1)) : field.type === "file" && __props.render.fileRoutePath(detailRecord.value[key]) ? (openBlock(), createElementBlock("a", {
2599
- key: 12,
2662
+ key: 13,
2600
2663
  href: __props.render.fileRoutePath(detailRecord.value[key]) ?? void 0,
2601
2664
  class: "text-blue-600 hover:text-blue-800 font-semibold hover:underline break-all",
2602
2665
  "data-testid": `collections-detail-file-${key}`,
@@ -3116,6 +3179,14 @@ function uniqueEmbedTargets(schema) {
3116
3179
  for (const field of Object.values(schema.fields)) if (field.type === "embed" && typeof field.to === "string" && field.to.length > 0) targets.add(field.to);
3117
3180
  return [...targets];
3118
3181
  }
3182
+ /** Slugs of every SOURCE collection a `backlinks` field reverses over.
3183
+ * Top-level only, like `embed` (the schema rejects `backlinks` inside a
3184
+ * table's `of`). Mirrors the server's `uniqueBacklinkSources`. */
3185
+ function uniqueBacklinkSources(schema) {
3186
+ const sources = /* @__PURE__ */ new Set();
3187
+ for (const field of Object.values(schema.fields)) if (field.type === "backlinks" && field.from.length > 0) sources.add(field.from);
3188
+ return [...sources];
3189
+ }
3119
3190
  function buildRefDisplayMap(detail) {
3120
3191
  const { fields, primaryKey } = detail.collection.schema;
3121
3192
  const displayField = displayFieldFor(fields, primaryKey);
@@ -3128,15 +3199,23 @@ function buildRefDisplayMap(detail) {
3128
3199
  }
3129
3200
  return map;
3130
3201
  }
3131
- function buildRefRecordMap(detail) {
3132
- const { schema } = detail.collection;
3202
+ /** Index DERIVED records by primary key — the client mirror of the
3203
+ * server's `loadTarget` indexing: string non-empty ids only, one record
3204
+ * per id (a duplicate keeps the LAST record in its FIRST position —
3205
+ * plain-object key semantics). Shared by the ref-record cache and the
3206
+ * backlinks view so every client consumer agrees with server enrichment
3207
+ * on which source records exist. */
3208
+ function derivedRecordsById(schema, items) {
3133
3209
  const map = {};
3134
- for (const item of detail.items) {
3210
+ for (const item of items) {
3135
3211
  const slugRaw = item[schema.primaryKey];
3136
3212
  if (typeof slugRaw === "string" && slugRaw.length > 0) map[slugRaw] = deriveAll(schema, item, {});
3137
3213
  }
3138
3214
  return map;
3139
3215
  }
3216
+ function buildRefRecordMap(detail) {
3217
+ return derivedRecordsById(detail.collection.schema, detail.items);
3218
+ }
3140
3219
  function sortedRefOptions(map) {
3141
3220
  return Object.entries(map).map(([slug, display]) => ({
3142
3221
  slug,
@@ -3161,10 +3240,14 @@ function buildEmbedOptions(schema, items) {
3161
3240
  //#endregion
3162
3241
  //#region src/vue/useLinkedCollectionCaches.ts
3163
3242
  /** The de-duplicated ref + embed target slugs a schema links to. `allTargets`
3164
- * is the union (each target fetched once even when both ref'd and embedded). */
3243
+ * is the union (each target fetched once even when both ref'd and embedded).
3244
+ * `backlinks` SOURCE collections ride in `embedTargets`: a backlink needs
3245
+ * exactly what an embed target caches (the source's schema + items in
3246
+ * `embedCache`), so reverse sources reuse that cache rather than adding a
3247
+ * parallel one. */
3165
3248
  function linkedTargets(schema) {
3166
3249
  const refTargets = new Set(uniqueRefTargets(schema));
3167
- const embedTargets = new Set(uniqueEmbedTargets(schema));
3250
+ const embedTargets = /* @__PURE__ */ new Set([...uniqueEmbedTargets(schema), ...uniqueBacklinkSources(schema)]);
3168
3251
  return {
3169
3252
  refTargets,
3170
3253
  embedTargets,
@@ -3309,6 +3392,58 @@ function buildEmbedViews(schema, embedCache, record, locale) {
3309
3392
  }
3310
3393
  return out;
3311
3394
  }
3395
+ /** One backlinks table CELL: money formatted with its currency, anything
3396
+ * else through the same `formatCell` list tables use — so a markdown
3397
+ * source column (a worklog `notes`) shows the one-line 80-char preview,
3398
+ * not the whole text. Unknown `display` key ⇒ plain text (fail-soft). */
3399
+ function formatBacklinkCell(sourceField, value, row, locale) {
3400
+ if (!sourceField) return detailText(value);
3401
+ if (sourceField.type === "money") return formatMoney(value, resolveCurrency(sourceField, row), locale);
3402
+ return formatCell(value, sourceField.type);
3403
+ }
3404
+ /** Build the read-only backlinks view-models for one record: for each
3405
+ * `backlinks` field, the rows of `from` whose `via` points at the open
3406
+ * record (matched via the SHARED `backlinkRows`, on source records
3407
+ * derived exactly like the server's enrichment — so a `display`/`filter`
3408
+ * on a derived column such as an invoice `total` agrees on both sides).
3409
+ * Source data comes out of `embedCache` (reverse sources ride the embed
3410
+ * fan-out — see `linkedTargets`). Fail-soft: an unloadable source or an
3411
+ * unknown `display` key degrades to `found: false` / a raw-key column,
3412
+ * never a throw. */
3413
+ function buildBacklinksViews(schema, embedCache, record, locale) {
3414
+ const out = {};
3415
+ if (!schema) return out;
3416
+ for (const [key, field] of Object.entries(schema.fields)) {
3417
+ if (field.type !== "backlinks") continue;
3418
+ const columns = field.display.map((col) => ({
3419
+ key: col,
3420
+ label: col
3421
+ }));
3422
+ const data = embedCache[field.from];
3423
+ if (!data) {
3424
+ out[key] = {
3425
+ found: false,
3426
+ columns,
3427
+ rows: [],
3428
+ fromSlug: field.from
3429
+ };
3430
+ continue;
3431
+ }
3432
+ for (const column of columns) column.label = data.schema.fields[column.key]?.label ?? column.key;
3433
+ const selfId = String(record?.[schema.primaryKey] ?? "");
3434
+ const sourceById = derivedRecordsById(data.schema, data.items);
3435
+ out[key] = {
3436
+ found: true,
3437
+ columns,
3438
+ rows: backlinkRows(field, selfId, Object.values(sourceById)).map((row) => ({
3439
+ id: String(row[data.schema.primaryKey] ?? ""),
3440
+ cells: columns.map((column) => formatBacklinkCell(data.schema.fields[column.key], row[column.key], row, locale))
3441
+ })),
3442
+ fromSlug: field.from
3443
+ };
3444
+ }
3445
+ return out;
3446
+ }
3312
3447
  function renderSubCell(subField, value, record, refCache, locale) {
3313
3448
  if (subField.type === "money") return formatMoney(value, resolveCurrency(subField, record), locale);
3314
3449
  if (subField.type === "ref" && subField.to && typeof value === "string" && value.length > 0) return lookupRefDisplay(refCache, subField.to, value);
@@ -3349,6 +3484,7 @@ function useCollectionRendering(collection, locale) {
3349
3484
  refOptions: (targetSlug) => refOptionsFor(refCache.value, targetSlug),
3350
3485
  embedOptions: (targetSlug) => embedOptionsFor(embedCache.value, targetSlug),
3351
3486
  embedViewsFor: (record) => buildEmbedViews(collection.value?.schema ?? null, embedCache.value, record, locale.value),
3487
+ backlinksViewsFor: (record) => buildBacklinksViews(collection.value?.schema ?? null, embedCache.value, record, locale.value),
3352
3488
  currencySymbol: (currency) => currencySymbolForLocale(currency, locale.value),
3353
3489
  artifactUrl: (value) => collectionUi().fileAssetUrl(value),
3354
3490
  fileRoutePath: (value) => collectionUi().fileRoutePath(value),
@@ -4125,7 +4261,7 @@ var CollectionView_default = /* @__PURE__ */ defineComponent({
4125
4261
  * list table only (a whole embedded record doesn't fit a table cell,
4126
4262
  * and it'd be identical in every row). The detail modal and the edit
4127
4263
  * form iterate the full `schema.fields` so embeds render there too. */
4128
- const listColumnFields = computed(() => collection.value ? Object.entries(collection.value.schema.fields).filter(([key, field]) => field.type !== "embed" && field.type !== "image" && key !== collection.value?.schema.primaryKey) : []);
4264
+ const listColumnFields = 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) : []);
4129
4265
  /** True when the current collection declares `schema.singleton` —
4130
4266
  * exactly one record, its primary key fixed to the declared value. */
4131
4267
  const isSingleton = computed(() => Boolean(collection.value?.schema.singleton));
@@ -4314,7 +4450,7 @@ var CollectionView_default = /* @__PURE__ */ defineComponent({
4314
4450
  boolOriginallyPresent[key] = false;
4315
4451
  boolTouched[key] = false;
4316
4452
  } else if (field.type === "table") table[key] = [];
4317
- else if (field.type !== "derived" && field.type !== "embed" && field.type !== "toggle") text[key] = "";
4453
+ else if (field.type !== "derived" && field.type !== "embed" && field.type !== "backlinks" && field.type !== "toggle") text[key] = "";
4318
4454
  const { singleton, primaryKey } = collection.value.schema;
4319
4455
  if (singleton) text[primaryKey] = singleton;
4320
4456
  else if (primaryKey in text) text[primaryKey] = generateUniqueItemId(primaryKey);
@@ -4346,7 +4482,7 @@ var CollectionView_default = /* @__PURE__ */ defineComponent({
4346
4482
  } else if (field.type === "table" && field.of) {
4347
4483
  const sub = field.of;
4348
4484
  table[key] = (Array.isArray(raw) ? raw : []).filter((row) => Boolean(row) && typeof row === "object" && !Array.isArray(row)).map((row) => rowFromItem(row, sub));
4349
- } else if (field.type !== "derived" && field.type !== "embed" && field.type !== "toggle") text[key] = raw === void 0 || raw === null ? "" : String(raw);
4485
+ } else if (field.type !== "derived" && field.type !== "embed" && field.type !== "backlinks" && field.type !== "toggle") text[key] = raw === void 0 || raw === null ? "" : String(raw);
4350
4486
  }
4351
4487
  const primaryRaw = item[collection.value.schema.primaryKey];
4352
4488
  const originalId = typeof primaryRaw === "string" ? primaryRaw : String(primaryRaw ?? "");