yaml-flow 5.2.6 → 5.3.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/README.md +6 -6
- package/board-livecards-server-runtime.js +260 -35
- package/browser/board-livegraph-engine.js +61 -33
- package/browser/board-livegraph-engine.js.map +1 -1
- package/browser/card-compute.js +18 -18
- package/browser/live-cards.js +317 -156
- package/browser/live-cards.schema.json +15 -10
- package/dist/board-livegraph-runtime/index.cjs +61 -33
- package/dist/board-livegraph-runtime/index.cjs.map +1 -1
- package/dist/board-livegraph-runtime/index.d.cts +1 -1
- package/dist/board-livegraph-runtime/index.d.ts +1 -1
- package/dist/board-livegraph-runtime/index.js +61 -33
- package/dist/board-livegraph-runtime/index.js.map +1 -1
- package/dist/card-compute/index.cjs +101 -39
- package/dist/card-compute/index.cjs.map +1 -1
- package/dist/card-compute/index.d.cts +13 -8
- package/dist/card-compute/index.d.ts +13 -8
- package/dist/card-compute/index.js +101 -39
- package/dist/card-compute/index.js.map +1 -1
- package/dist/cli/board-live-cards-cli.cjs +7205 -202
- package/dist/cli/board-live-cards-cli.cjs.map +1 -1
- package/dist/cli/board-live-cards-cli.d.cts +6 -6
- package/dist/cli/board-live-cards-cli.d.ts +6 -6
- package/dist/cli/board-live-cards-cli.js +7204 -202
- package/dist/cli/board-live-cards-cli.js.map +1 -1
- package/dist/continuous-event-graph/index.cjs +59 -31
- package/dist/continuous-event-graph/index.cjs.map +1 -1
- package/dist/continuous-event-graph/index.d.cts +2 -2
- package/dist/continuous-event-graph/index.d.ts +2 -2
- package/dist/continuous-event-graph/index.js +59 -31
- package/dist/continuous-event-graph/index.js.map +1 -1
- package/dist/index.cjs +126 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +126 -54
- package/dist/index.js.map +1 -1
- package/dist/{live-cards-bridge-CeNxiVcm.d.ts → live-cards-bridge-EQjytzI_.d.ts} +10 -5
- package/dist/{live-cards-bridge-z_rJCSbi.d.cts → live-cards-bridge-x5XREkXm.d.cts} +10 -5
- package/examples/browser/boards/portfolio-tracker/cards/holdings-table.json +1 -1
- package/examples/browser/boards/portfolio-tracker/cards/portfolio-form.json +1 -1
- package/examples/browser/boards/portfolio-tracker/cards/portfolio-risk-assessment.json +1 -1
- package/examples/browser/boards/portfolio-tracker/cards/portfolio-value.json +1 -1
- package/examples/browser/boards/portfolio-tracker/cards/price-fetch.json +2 -2
- package/examples/browser/boards/portfolio-tracker/cards/rebalancing-strategy.json +1 -1
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker.js +10 -10
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/holdings-table.json +1 -1
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +1 -1
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +1 -1
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/price-fetch.json +2 -2
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +1 -1
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +1 -1
- package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +1 -1
- package/examples/example-board/agent-instructions-cardlayout.md +29 -1
- package/examples/example-board/agent-instructions.md +271 -45
- package/examples/example-board/cards/card-concentration.json +8 -5
- package/examples/example-board/cards/card-market-prices.json +14 -9
- package/examples/example-board/cards/card-my-identity.json +28 -0
- package/examples/example-board/cards/card-portfolio-value.json +1 -1
- package/examples/example-board/cards/card-portfolio.json +1 -1
- package/examples/example-board/cards/card-rebalance-impact.json +65 -0
- package/examples/example-board/cards/card-rebalance-sim.json +67 -0
- package/examples/example-board/demo-chat-handler.js +2 -1
- package/examples/example-board/demo-server-config.json +6 -1
- package/examples/example-board/demo-server.js +91 -8
- package/examples/example-board/demo-shell-browser.html +6 -6
- package/examples/example-board/demo-shell-with-server.html +4 -4
- package/examples/example-board/demo-task-executor.js +457 -246
- package/examples/example-board/scripts/copilot_wrapper.bat +16 -0
- package/examples/example-board/scripts/copilot_wrapper_helper.ps1 +19 -10
- package/examples/example-board/scripts/workiq_wrapper.mjs +66 -0
- package/examples/npm-libs/continuous-event-graph/live-cards-board.ts +5 -5
- package/examples/npm-libs/continuous-event-graph/soc-incident-board.ts +3 -3
- package/examples/npm-libs/event-graph/research-pipeline.ts +5 -5
- package/examples/npm-libs/graph-of-graphs/multi-stage-etl.ts +9 -9
- package/examples/step-machine-cli/portfolio-tracker/cards/holdings-table.json +1 -1
- package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +1 -1
- package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +1 -1
- package/examples/step-machine-cli/portfolio-tracker/cards/price-fetch.json +3 -3
- package/examples/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +1 -1
- package/examples/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +1 -1
- package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +1 -1
- package/package.json +2 -2
- package/schema/live-cards.schema.json +15 -10
package/browser/live-cards.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// live-cards.js — LiveCards v3: Node-based Board/Canvas engine
|
|
2
2
|
//
|
|
3
3
|
// Schema: Each node has { id } required; all else optional.
|
|
4
|
-
// id, meta, card_data, requires, provides,
|
|
5
|
-
// Nodes with view render as cards; nodes with
|
|
4
|
+
// id, meta, card_data, requires, provides, source_defs, compute, view
|
|
5
|
+
// Nodes with view render as cards; nodes with source_defs but no view render as source pills in canvas.
|
|
6
6
|
// compute[] — ordered array of { bindTo, expr } JSONata steps → writes to node.computed_values (ephemeral)
|
|
7
|
-
//
|
|
7
|
+
// source_defs[] — open objects: only bindTo + outputFile matter to the engine; all other fields are
|
|
8
8
|
// passed verbatim to the board's task-executor (--in JSON). Users define their own
|
|
9
9
|
// shape (kind, url, mailbox, channel, model, ...) per executor.
|
|
10
10
|
// requires[] — upstream node IDs; engine subscribes automatically
|
|
@@ -89,6 +89,15 @@ var LiveCard = (function () {
|
|
|
89
89
|
.lc-files-modal-backdrop .modal-dialog { max-height:90vh; }
|
|
90
90
|
.lc-files-modal-backdrop .modal-content { display:flex; flex-direction:column; max-height:90vh; }
|
|
91
91
|
.lc-files-modal-backdrop .modal-body { overflow-y:auto; flex:1; min-height:200px; padding:1rem; }
|
|
92
|
+
.lc-simulation-card { background:#fdf6ec; border-color:#e0c97f !important; }
|
|
93
|
+
.lc-simulation-card .card-header { background:#faecc8; border-bottom-color:#e0c97f; }
|
|
94
|
+
.lc-gandalf-card { background:#eef4ff; border-color:#6ea4e0 !important; }
|
|
95
|
+
.lc-gandalf-card .card-header { background:#d7e8fa; border-bottom-color:#6ea4e0; cursor:pointer; user-select:none; }
|
|
96
|
+
.lc-gandalf-card .card-header:hover { background:#c8dcf5; }
|
|
97
|
+
.lc-gandalf-caret { transition:transform .2s; display:inline-flex; align-items:center; margin-left:auto; opacity:.6; flex-shrink:0; cursor:pointer; padding:2px; }
|
|
98
|
+
.lc-gandalf-caret:hover { opacity:1; }
|
|
99
|
+
.lc-gandalf-card.lc-collapsed .lc-gandalf-caret { transform:rotate(-90deg); }
|
|
100
|
+
.lc-gandalf-card.lc-collapsed .card-body { display:none !important; }
|
|
92
101
|
@media (max-width:576px) {
|
|
93
102
|
.lc-metric-value { font-size:1.5rem; }
|
|
94
103
|
.lc-chart-wrap { min-height:150px; }
|
|
@@ -203,9 +212,9 @@ var LiveCard = (function () {
|
|
|
203
212
|
|
|
204
213
|
const _cleanup = {}; // nodeId → { ac, timers, charts, unsubs }
|
|
205
214
|
const _subs = {}; // nodeId → Set<callback>
|
|
206
|
-
const _etState = {}; // stateKey → {
|
|
207
|
-
const _formState = {}; // stateKey → {
|
|
208
|
-
const _notesState = {}; // stateKey → {
|
|
215
|
+
const _etState = {}; // stateKey → { baseRows, journalRows|null }
|
|
216
|
+
const _formState = {}; // stateKey → { baseValues, journal }
|
|
217
|
+
const _notesState = {}; // stateKey → { baseContent, journal|null }
|
|
209
218
|
const _todoState = {}; // stateKey → { currentState, pending } for todo dirty tracking
|
|
210
219
|
const _renderers = {}; // kind → fn
|
|
211
220
|
const _nodeEls = {}; // nodeId → { container, resultEl, uid }
|
|
@@ -999,45 +1008,50 @@ var LiveCard = (function () {
|
|
|
999
1008
|
const props = schema.properties || {};
|
|
1000
1009
|
const required = schema.required || [];
|
|
1001
1010
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
// pending = user's local field values accumulated on top of currentState
|
|
1005
|
-
// dirty = JSON.stringify(currentState) !== JSON.stringify(pending)
|
|
1006
|
-
//
|
|
1007
|
-
// On SSE re-render:
|
|
1008
|
-
// - currentState updated to new server values
|
|
1009
|
-
// - if NOT dirty: pending follows currentState (no unsaved edits)
|
|
1010
|
-
// - if dirty: pending untouched, form is rebuilt from pending (edits survive)
|
|
1011
|
-
const stateKey = node.id + ':' + (writeTo || '');
|
|
1012
|
-
const incomingValues = writeTo ? (_resolveBind(node, writeTo) || {}) : (data && typeof data === 'object' ? data : {});
|
|
1013
|
-
const incomingCopy = Object.assign({}, incomingValues);
|
|
1011
|
+
const stateKey = node.id + ':' + (ed.bind || writeTo || '');
|
|
1012
|
+
const baseValues = (data && typeof data === 'object' && !Array.isArray(data)) ? Object.assign({}, data) : {};
|
|
1014
1013
|
|
|
1015
1014
|
if (!_formState[stateKey]) {
|
|
1016
|
-
_formState[stateKey] = {
|
|
1015
|
+
_formState[stateKey] = { baseValues, journal: {} };
|
|
1017
1016
|
} else {
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
}
|
|
1024
|
-
// if dirty, pending stays so user's in-progress edits survive the SSE tick
|
|
1017
|
+
_formState[stateKey].baseValues = baseValues;
|
|
1018
|
+
Object.keys(_formState[stateKey].journal).forEach(key => {
|
|
1019
|
+
if (_same(_formState[stateKey].journal[key], baseValues[key])) {
|
|
1020
|
+
delete _formState[stateKey].journal[key];
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1025
1023
|
}
|
|
1026
1024
|
|
|
1027
1025
|
const st = _formState[stateKey];
|
|
1028
1026
|
|
|
1027
|
+
function _toInputValue(prop, inp) {
|
|
1028
|
+
if (prop.type === 'boolean') return !!inp.checked;
|
|
1029
|
+
if (prop.type === 'number' || prop.type === 'integer') return inp.value !== '' ? parseFloat(inp.value) : 0;
|
|
1030
|
+
return inp.value;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function _same(a, b) {
|
|
1034
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function getEffectiveValues() {
|
|
1038
|
+
return Object.assign({}, st.baseValues, st.journal);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1029
1041
|
function isDirty() {
|
|
1030
|
-
return
|
|
1042
|
+
return Object.keys(st.journal).length > 0;
|
|
1031
1043
|
}
|
|
1032
1044
|
|
|
1033
|
-
//
|
|
1034
|
-
function
|
|
1045
|
+
// Capture user edits into a journal overlay (only changed keys).
|
|
1046
|
+
function captureJournal(form) {
|
|
1035
1047
|
form.querySelectorAll('[data-key]').forEach(inp => {
|
|
1036
|
-
const k = inp.dataset.key
|
|
1048
|
+
const k = inp.dataset.key;
|
|
1049
|
+
const p = props[k];
|
|
1037
1050
|
if (!p) return;
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1051
|
+
const nextVal = _toInputValue(p, inp);
|
|
1052
|
+
const baseVal = st.baseValues[k];
|
|
1053
|
+
if (_same(nextVal, baseVal)) delete st.journal[k];
|
|
1054
|
+
else st.journal[k] = nextVal;
|
|
1041
1055
|
});
|
|
1042
1056
|
}
|
|
1043
1057
|
|
|
@@ -1089,8 +1103,8 @@ var LiveCard = (function () {
|
|
|
1089
1103
|
|
|
1090
1104
|
input.dataset.key = key;
|
|
1091
1105
|
if (isReq) input.required = true;
|
|
1092
|
-
// Populate from
|
|
1093
|
-
const v =
|
|
1106
|
+
// Populate from effective values (base bind overlaid by local journal).
|
|
1107
|
+
const v = getEffectiveValues()[key];
|
|
1094
1108
|
if (v != null) {
|
|
1095
1109
|
if (prop.type === 'boolean') input.checked = !!v;
|
|
1096
1110
|
else if (prop.format === 'date') input.value = String(v).slice(0, 10);
|
|
@@ -1101,34 +1115,51 @@ var LiveCard = (function () {
|
|
|
1101
1115
|
|
|
1102
1116
|
const btnCol = document.createElement('div');
|
|
1103
1117
|
btnCol.className = 'col-12 mt-1';
|
|
1118
|
+
const discardBtn = document.createElement('button');
|
|
1119
|
+
discardBtn.type = 'button';
|
|
1120
|
+
discardBtn.className = 'btn btn-sm btn-outline-secondary me-2' + (isDirty() ? '' : ' d-none');
|
|
1121
|
+
discardBtn.textContent = 'Discard';
|
|
1104
1122
|
const btn = document.createElement('button');
|
|
1105
1123
|
btn.type = 'submit';
|
|
1106
1124
|
btn.className = 'btn btn-sm btn-primary' + (isDirty() ? '' : ' d-none');
|
|
1107
|
-
btn.textContent = '
|
|
1125
|
+
btn.textContent = 'Save';
|
|
1126
|
+
btnCol.appendChild(discardBtn);
|
|
1108
1127
|
btnCol.appendChild(btn);
|
|
1109
1128
|
form.appendChild(btnCol);
|
|
1110
1129
|
|
|
1111
1130
|
el.innerHTML = '';
|
|
1112
1131
|
el.appendChild(form);
|
|
1113
1132
|
|
|
1114
|
-
// Real-time input → update
|
|
1133
|
+
// Real-time input → update journal + toggle Save/Discard buttons
|
|
1115
1134
|
form.addEventListener('input', () => {
|
|
1116
|
-
|
|
1117
|
-
|
|
1135
|
+
captureJournal(form);
|
|
1136
|
+
const dirty = isDirty();
|
|
1137
|
+
btn.classList.toggle('d-none', !dirty);
|
|
1138
|
+
discardBtn.classList.toggle('d-none', !dirty);
|
|
1118
1139
|
}, { signal });
|
|
1119
1140
|
|
|
1120
1141
|
form.addEventListener('submit', e => {
|
|
1121
1142
|
e.preventDefault();
|
|
1122
1143
|
if (!form.checkValidity()) { form.classList.add('was-validated'); return; }
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
cfg.onPatchState(node.id, { fieldValues:
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1144
|
+
captureJournal(form);
|
|
1145
|
+
const nextValues = getEffectiveValues();
|
|
1146
|
+
cfg.onPatchState(node.id, { fieldValues: nextValues });
|
|
1147
|
+
btn.textContent = 'Saving...';
|
|
1148
|
+
}, { signal });
|
|
1149
|
+
|
|
1150
|
+
discardBtn.addEventListener('click', () => {
|
|
1151
|
+
st.journal = {};
|
|
1152
|
+
form.querySelectorAll('[data-key]').forEach(inp => {
|
|
1153
|
+
const k = inp.dataset.key;
|
|
1154
|
+
const p = props[k];
|
|
1155
|
+
if (!p) return;
|
|
1156
|
+
const v = st.baseValues[k];
|
|
1157
|
+
if (p.type === 'boolean') inp.checked = !!v;
|
|
1158
|
+
else if (p.format === 'date') inp.value = v != null ? String(v).slice(0, 10) : '';
|
|
1159
|
+
else inp.value = v != null ? v : '';
|
|
1160
|
+
});
|
|
1161
|
+
discardBtn.classList.add('d-none');
|
|
1129
1162
|
btn.classList.add('d-none');
|
|
1130
|
-
btn.textContent = '✓ Saved';
|
|
1131
|
-
setTimeout(() => { btn.textContent = 'Submit'; }, 1500);
|
|
1132
1163
|
}, { signal });
|
|
1133
1164
|
}
|
|
1134
1165
|
|
|
@@ -1141,54 +1172,63 @@ var LiveCard = (function () {
|
|
|
1141
1172
|
const writeTo = ed.writeTo;
|
|
1142
1173
|
const incomingContent = typeof data === 'string' ? data : '';
|
|
1143
1174
|
|
|
1144
|
-
//
|
|
1145
|
-
//
|
|
1146
|
-
|
|
1147
|
-
const stateKey = node.id + ':' + (writeTo || '');
|
|
1175
|
+
// Base + journal overlay model:
|
|
1176
|
+
// effective = journal when present, else baseContent from bind.
|
|
1177
|
+
const stateKey = node.id + ':' + ((ed.bind || writeTo) || '');
|
|
1148
1178
|
if (!_notesState[stateKey]) {
|
|
1149
|
-
_notesState[stateKey] = {
|
|
1179
|
+
_notesState[stateKey] = { baseContent: incomingContent, journal: null };
|
|
1150
1180
|
} else {
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
// if dirty, pending stays so user's typing survives the SSE tick
|
|
1181
|
+
_notesState[stateKey].baseContent = incomingContent;
|
|
1182
|
+
if (_notesState[stateKey].journal === incomingContent) {
|
|
1183
|
+
_notesState[stateKey].journal = null;
|
|
1184
|
+
}
|
|
1156
1185
|
}
|
|
1157
1186
|
const st = _notesState[stateKey];
|
|
1158
1187
|
|
|
1188
|
+
function isDirty() {
|
|
1189
|
+
return st.journal != null;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function getEffectiveContent() {
|
|
1193
|
+
return st.journal != null ? st.journal : st.baseContent;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function setJournal(nextValue) {
|
|
1197
|
+
st.journal = nextValue === st.baseContent ? null : nextValue;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1159
1200
|
el.innerHTML = `
|
|
1160
|
-
<
|
|
1161
|
-
|
|
1162
|
-
<button class="btn btn-outline-secondary lc-n-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
<div class="lc-notes-preview d-none border rounded p-2 small"></div>`;
|
|
1201
|
+
<textarea class="form-control form-control-sm lc-notes-textarea" rows="8" placeholder="Write markdown...">${_esc(getEffectiveContent())}</textarea>
|
|
1202
|
+
<div class="mt-2">
|
|
1203
|
+
<button class="btn btn-sm btn-outline-secondary me-2 lc-n-discard${isDirty() ? '' : ' d-none'}" type="button">Discard</button>
|
|
1204
|
+
<button class="btn btn-sm btn-primary lc-n-save${isDirty() ? '' : ' d-none'}" type="button">Save</button>
|
|
1205
|
+
</div>`;
|
|
1166
1206
|
|
|
1167
1207
|
const textarea = el.querySelector('.lc-notes-textarea');
|
|
1168
|
-
const
|
|
1169
|
-
const
|
|
1170
|
-
const previewBtn = el.querySelector('.lc-n-preview');
|
|
1208
|
+
const discardBtn = el.querySelector('.lc-n-discard');
|
|
1209
|
+
const saveBtn = el.querySelector('.lc-n-save');
|
|
1171
1210
|
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1211
|
+
function syncDirtyButtons() {
|
|
1212
|
+
const dirty = isDirty();
|
|
1213
|
+
saveBtn.classList.toggle('d-none', !dirty);
|
|
1214
|
+
discardBtn.classList.toggle('d-none', !dirty);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
textarea.addEventListener('input', () => {
|
|
1218
|
+
setJournal(textarea.value);
|
|
1219
|
+
syncDirtyButtons();
|
|
1175
1220
|
}, { signal });
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
textarea.
|
|
1179
|
-
|
|
1221
|
+
|
|
1222
|
+
saveBtn.addEventListener('click', () => {
|
|
1223
|
+
const nextValue = textarea.value;
|
|
1224
|
+
cfg.onPatchState(node.id, { notes: nextValue });
|
|
1225
|
+
saveBtn.textContent = 'Saving...';
|
|
1180
1226
|
}, { signal });
|
|
1181
1227
|
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
timer = setTimeout(() => {
|
|
1187
|
-
if (writeTo) _deepSet(node, writeTo, textarea.value);
|
|
1188
|
-
cfg.onPatchState(node.id, { notes: textarea.value });
|
|
1189
|
-
st.currentState = textarea.value; // saved — no longer dirty
|
|
1190
|
-
}, 800);
|
|
1191
|
-
cleanup.timers.push(timer);
|
|
1228
|
+
discardBtn.addEventListener('click', () => {
|
|
1229
|
+
st.journal = null;
|
|
1230
|
+
textarea.value = st.baseContent || '';
|
|
1231
|
+
syncDirtyButtons();
|
|
1192
1232
|
}, { signal });
|
|
1193
1233
|
}
|
|
1194
1234
|
|
|
@@ -1206,7 +1246,11 @@ var LiveCard = (function () {
|
|
|
1206
1246
|
const cleanup = _getCleanup(node.id);
|
|
1207
1247
|
const signal = cleanup.ac.signal;
|
|
1208
1248
|
const ed = elemDef.data || {};
|
|
1209
|
-
|
|
1249
|
+
// Standard convention:
|
|
1250
|
+
// - bind = read source
|
|
1251
|
+
// - writeTo = explicit write target for editable views
|
|
1252
|
+
// If bind already points at card_data, default writeTo to bind.
|
|
1253
|
+
const writeTo = ed.writeTo || ((typeof ed.bind === 'string' && ed.bind.startsWith('card_data.')) ? ed.bind : undefined);
|
|
1210
1254
|
const schemaProps = (ed.schema && ed.schema.properties) || {};
|
|
1211
1255
|
const canAdd = ed.addRow !== false;
|
|
1212
1256
|
const canDelete = ed.deleteRow !== false;
|
|
@@ -1219,57 +1263,59 @@ var LiveCard = (function () {
|
|
|
1219
1263
|
return [...s];
|
|
1220
1264
|
}
|
|
1221
1265
|
|
|
1222
|
-
//
|
|
1223
|
-
//
|
|
1224
|
-
//
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
// When SSE fires and re-renders this element:
|
|
1228
|
-
// - currentState is updated to the new server value
|
|
1229
|
-
// - if NOT dirty: pending follows currentState (no unsaved edits)
|
|
1230
|
-
// - if dirty: pending is left untouched (user's edits survive the SSE tick)
|
|
1231
|
-
const stateKey = node.id + ':' + (ed.bind || ed.writeTo || '');
|
|
1232
|
-
const incomingRows = Array.isArray(writeTo ? _resolveBind(node, writeTo) : data)
|
|
1233
|
-
? (writeTo ? _resolveBind(node, writeTo) : data)
|
|
1234
|
-
: [];
|
|
1266
|
+
// Base + journal overlay model:
|
|
1267
|
+
// effectiveRows = journalRows if present, else baseRows(bind).
|
|
1268
|
+
// Dirty is determined by journal presence (supports Save/Discard UX).
|
|
1269
|
+
const stateKey = node.id + ':' + (ed.bind || writeTo || '');
|
|
1270
|
+
const incomingRows = Array.isArray(data) ? data : [];
|
|
1235
1271
|
const incomingCopy = incomingRows.map(r => Object.assign({}, r));
|
|
1236
1272
|
|
|
1237
1273
|
if (!_etState[stateKey]) {
|
|
1238
|
-
|
|
1239
|
-
_etState[stateKey] = { currentState: incomingCopy, pending: incomingCopy.map(r => Object.assign({}, r)) };
|
|
1274
|
+
_etState[stateKey] = { baseRows: incomingCopy, journalRows: null };
|
|
1240
1275
|
} else {
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
if (!wasDirty) {
|
|
1245
|
-
// No unsaved edits — sync pending to new server state
|
|
1246
|
-
s.pending = incomingCopy.map(r => Object.assign({}, r));
|
|
1276
|
+
_etState[stateKey].baseRows = incomingCopy;
|
|
1277
|
+
if (_etState[stateKey].journalRows && JSON.stringify(_etState[stateKey].journalRows) === JSON.stringify(incomingCopy)) {
|
|
1278
|
+
_etState[stateKey].journalRows = null;
|
|
1247
1279
|
}
|
|
1248
|
-
// If dirty, pending stays as-is so user's edits survive the SSE tick
|
|
1249
1280
|
}
|
|
1250
1281
|
|
|
1251
1282
|
const st = _etState[stateKey];
|
|
1252
1283
|
|
|
1253
1284
|
function isDirty() {
|
|
1254
|
-
return
|
|
1285
|
+
return Array.isArray(st.journalRows);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function getEffectiveRows() {
|
|
1289
|
+
const rows = Array.isArray(st.journalRows) ? st.journalRows : st.baseRows;
|
|
1290
|
+
return rows.map(r => Object.assign({}, r));
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function updateJournal(nextRows) {
|
|
1294
|
+
if (JSON.stringify(nextRows) === JSON.stringify(st.baseRows)) st.journalRows = null;
|
|
1295
|
+
else st.journalRows = nextRows.map(r => Object.assign({}, r));
|
|
1255
1296
|
}
|
|
1256
1297
|
|
|
1257
1298
|
function markDirty() {
|
|
1258
1299
|
const saveBtn = el.querySelector('.lc-et-save');
|
|
1300
|
+
const discardBtn = el.querySelector('.lc-et-discard');
|
|
1259
1301
|
if (saveBtn) saveBtn.classList.remove('d-none');
|
|
1302
|
+
if (discardBtn) discardBtn.classList.remove('d-none');
|
|
1260
1303
|
}
|
|
1261
1304
|
|
|
1262
1305
|
function commitSave() {
|
|
1263
|
-
|
|
1264
|
-
cfg.onPatchState(node.id, { fieldValues:
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1306
|
+
const rows = getEffectiveRows();
|
|
1307
|
+
cfg.onPatchState(node.id, { fieldValues: rows });
|
|
1308
|
+
const saveBtn = el.querySelector('.lc-et-save');
|
|
1309
|
+
if (saveBtn) saveBtn.textContent = 'Saving...';
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function commitDiscard() {
|
|
1313
|
+
st.journalRows = null;
|
|
1268
1314
|
build();
|
|
1269
1315
|
}
|
|
1270
1316
|
|
|
1271
1317
|
function build() {
|
|
1272
|
-
const rows =
|
|
1318
|
+
const rows = getEffectiveRows();
|
|
1273
1319
|
const cols = getCols(rows);
|
|
1274
1320
|
|
|
1275
1321
|
if (!cols.length && !canAdd) {
|
|
@@ -1312,44 +1358,63 @@ var LiveCard = (function () {
|
|
|
1312
1358
|
h += '</tbody></table></div>';
|
|
1313
1359
|
let footer = '';
|
|
1314
1360
|
if (canAdd) footer += '<button class="btn btn-sm btn-outline-secondary mt-1 me-1 lc-et-add">+ Add row</button>';
|
|
1361
|
+
footer += `<button class="btn btn-sm btn-outline-secondary mt-1 me-1 lc-et-discard${isDirty() ? '' : ' d-none'}">Discard</button>`;
|
|
1315
1362
|
footer += `<button class="btn btn-sm btn-primary mt-1 lc-et-save${isDirty() ? '' : ' d-none'}">Save</button>`;
|
|
1316
1363
|
el.innerHTML = h + footer;
|
|
1317
1364
|
|
|
1318
|
-
// Cell edit → update
|
|
1365
|
+
// Cell edit → update journal overlay and toggle Save/Discard.
|
|
1319
1366
|
el.querySelectorAll('.lc-et-cell').forEach(inp => {
|
|
1320
1367
|
inp.addEventListener('change', () => {
|
|
1321
1368
|
const rowIdx = parseInt(inp.dataset.row);
|
|
1322
1369
|
const colName = inp.dataset.col;
|
|
1323
1370
|
const prop = schemaProps[colName] || {};
|
|
1324
1371
|
const isNum = prop.type === 'number' || prop.type === 'integer' || inp.type === 'number';
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1372
|
+
const nextRows = getEffectiveRows();
|
|
1373
|
+
if (!nextRows[rowIdx]) return;
|
|
1374
|
+
nextRows[rowIdx] = Object.assign({}, nextRows[rowIdx]);
|
|
1375
|
+
nextRows[rowIdx][colName] = isNum ? (inp.value !== '' ? parseFloat(inp.value) : 0) : inp.value;
|
|
1376
|
+
updateJournal(nextRows);
|
|
1328
1377
|
if (isDirty()) markDirty();
|
|
1378
|
+
else {
|
|
1379
|
+
const saveBtn = el.querySelector('.lc-et-save');
|
|
1380
|
+
const discardBtn = el.querySelector('.lc-et-discard');
|
|
1381
|
+
if (saveBtn) saveBtn.classList.add('d-none');
|
|
1382
|
+
if (discardBtn) discardBtn.classList.add('d-none');
|
|
1383
|
+
}
|
|
1329
1384
|
}, { signal });
|
|
1330
1385
|
});
|
|
1331
1386
|
|
|
1332
|
-
// Delete row — updates
|
|
1387
|
+
// Delete row — updates journal and rebuilds.
|
|
1333
1388
|
el.querySelectorAll('.lc-et-del').forEach(btn => {
|
|
1334
1389
|
btn.addEventListener('click', () => {
|
|
1335
1390
|
const rowIdx = parseInt(btn.dataset.row);
|
|
1336
|
-
|
|
1391
|
+
const nextRows = getEffectiveRows().filter((_, i) => i !== rowIdx);
|
|
1392
|
+
updateJournal(nextRows);
|
|
1337
1393
|
build();
|
|
1338
1394
|
}, { signal });
|
|
1339
1395
|
});
|
|
1340
1396
|
|
|
1341
|
-
// Add row — appends blank row to
|
|
1397
|
+
// Add row — appends blank row to journal and rebuilds.
|
|
1342
1398
|
const addBtn = el.querySelector('.lc-et-add');
|
|
1343
1399
|
if (addBtn) {
|
|
1344
1400
|
addBtn.addEventListener('click', () => {
|
|
1345
1401
|
const newRow = {};
|
|
1346
|
-
|
|
1347
|
-
|
|
1402
|
+
const nextRows = getEffectiveRows();
|
|
1403
|
+
getCols(nextRows).forEach(c => { newRow[c] = ''; });
|
|
1404
|
+
nextRows.push(newRow);
|
|
1405
|
+
updateJournal(nextRows);
|
|
1348
1406
|
build();
|
|
1349
1407
|
}, { signal });
|
|
1350
1408
|
}
|
|
1351
1409
|
|
|
1352
|
-
// Save
|
|
1410
|
+
// Save/Discard controls.
|
|
1411
|
+
const discardBtn = el.querySelector('.lc-et-discard');
|
|
1412
|
+
if (discardBtn) {
|
|
1413
|
+
discardBtn.addEventListener('click', () => {
|
|
1414
|
+
commitDiscard();
|
|
1415
|
+
}, { signal });
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1353
1418
|
const saveBtn = el.querySelector('.lc-et-save');
|
|
1354
1419
|
if (saveBtn) {
|
|
1355
1420
|
saveBtn.addEventListener('click', () => {
|
|
@@ -1847,6 +1912,68 @@ var LiveCard = (function () {
|
|
|
1847
1912
|
};
|
|
1848
1913
|
}
|
|
1849
1914
|
|
|
1915
|
+
// ---- ref ----
|
|
1916
|
+
// Indirection element: resolves a bind path to get the view definition,
|
|
1917
|
+
// then dispatches to the real renderer. The resolved value may be:
|
|
1918
|
+
// - a string → treated directly as the element kind ("table", "chart", etc.)
|
|
1919
|
+
// - an object → { kind, label, data: { columns, chartType, chartOptions, writeTo } }
|
|
1920
|
+
// merged with static elemDef (static fields win for protection)
|
|
1921
|
+
// - null/undefined → falls back to elemDef.data.fallbackKind or shape-inferred kind
|
|
1922
|
+
//
|
|
1923
|
+
// Allowed kinds from resolved value (whitelist, unknown → "table"):
|
|
1924
|
+
// table, editable-table, chart, metric, list, badge, text, narrative, markdown
|
|
1925
|
+
//
|
|
1926
|
+
// Usage:
|
|
1927
|
+
// { "kind": "ref",
|
|
1928
|
+
// "data": { "bind": "fetched_sources.rebalance.proposed_trades",
|
|
1929
|
+
// "viewBind": "card_data.display_mode",
|
|
1930
|
+
// "fallbackKind": "table" } }
|
|
1931
|
+
//
|
|
1932
|
+
// viewBind can point to any namespace: card_data, fetched_sources, requires, computed_values.
|
|
1933
|
+
// If the resolved view object contains a "bind" sub-path, that overrides data.bind.
|
|
1934
|
+
const _REF_KIND_WHITELIST = new Set([
|
|
1935
|
+
'table','editable-table','chart','metric','list','badge',
|
|
1936
|
+
'text','narrative','markdown','form','filter','todo','alert',
|
|
1937
|
+
]);
|
|
1938
|
+
function _renderRef(data, el, elemDef, node) {
|
|
1939
|
+
const ed = elemDef.data || {};
|
|
1940
|
+
|
|
1941
|
+
// Resolve the view hint
|
|
1942
|
+
const viewRaw = ed.viewBind ? _resolveBind(node, ed.viewBind) : undefined;
|
|
1943
|
+
|
|
1944
|
+
let resolvedKind, resolvedExtra;
|
|
1945
|
+
if (typeof viewRaw === 'string' && viewRaw) {
|
|
1946
|
+
resolvedKind = viewRaw;
|
|
1947
|
+
resolvedExtra = {};
|
|
1948
|
+
} else if (viewRaw && typeof viewRaw === 'object' && !Array.isArray(viewRaw)) {
|
|
1949
|
+
resolvedKind = typeof viewRaw.kind === 'string' ? viewRaw.kind : undefined;
|
|
1950
|
+
resolvedExtra = viewRaw.data && typeof viewRaw.data === 'object' ? viewRaw.data : {};
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
// Validate kind against whitelist; fall back to shape inference
|
|
1954
|
+
if (!resolvedKind || !_REF_KIND_WHITELIST.has(resolvedKind)) {
|
|
1955
|
+
resolvedKind = ed.fallbackKind && _REF_KIND_WHITELIST.has(ed.fallbackKind)
|
|
1956
|
+
? ed.fallbackKind
|
|
1957
|
+
: (Array.isArray(data) ? 'table' : typeof data === 'string' ? 'text' : 'narrative');
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
// Build effective elemDef: resolved hints first, static elemDef fields override (card author wins)
|
|
1961
|
+
const mergedData = Object.assign({}, resolvedExtra, ed);
|
|
1962
|
+
delete mergedData.viewBind;
|
|
1963
|
+
delete mergedData.fallbackKind;
|
|
1964
|
+
|
|
1965
|
+
// If the resolved hint provided its own bind path, honour it (but static ed.bind still wins)
|
|
1966
|
+
if (!mergedData.bind && resolvedExtra.bind) mergedData.bind = resolvedExtra.bind;
|
|
1967
|
+
|
|
1968
|
+
const effectiveElemDef = Object.assign({}, elemDef, { kind: resolvedKind }, { data: mergedData });
|
|
1969
|
+
|
|
1970
|
+
// Re-resolve data using effective bind (may have changed)
|
|
1971
|
+
const effectiveData = mergedData.bind ? _resolveBind(node, mergedData.bind) : data;
|
|
1972
|
+
|
|
1973
|
+
const renderer = _renderers[resolvedKind] || _renderers.table;
|
|
1974
|
+
renderer(effectiveData, el, effectiveElemDef, node);
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1850
1977
|
// ---- Register built-in renderers ----
|
|
1851
1978
|
|
|
1852
1979
|
_renderers.table = _renderTable;
|
|
@@ -1867,6 +1994,7 @@ var LiveCard = (function () {
|
|
|
1867
1994
|
_renderers['file-upload'] = _renderFileUpload;
|
|
1868
1995
|
_renderers['chat'] = _renderChatEl;
|
|
1869
1996
|
_renderers.actions = _renderActions;
|
|
1997
|
+
_renderers.ref = _renderRef;
|
|
1870
1998
|
|
|
1871
1999
|
// ===========================================================================
|
|
1872
2000
|
// _renderElements — render all view.elements for a card node
|
|
@@ -2006,12 +2134,6 @@ var LiveCard = (function () {
|
|
|
2006
2134
|
// Elements area
|
|
2007
2135
|
h += `<div class="lc-result" id="${uid}-result"></div>`;
|
|
2008
2136
|
|
|
2009
|
-
// Notes section (feature toggle)
|
|
2010
|
-
if (features.notes && opts.showNotes !== false) {
|
|
2011
|
-
h += `<details class="mt-2"><summary class="small fw-medium">Notes</summary>`;
|
|
2012
|
-
h += `<textarea class="form-control form-control-sm mt-1" id="${uid}-notes" rows="3" placeholder="Add notes...">${_esc((node.card_data && node.card_data._notes) || '')}</textarea></details>`;
|
|
2013
|
-
}
|
|
2014
|
-
|
|
2015
2137
|
h += '</div>';
|
|
2016
2138
|
containerEl.innerHTML = h;
|
|
2017
2139
|
|
|
@@ -2051,21 +2173,6 @@ var LiveCard = (function () {
|
|
|
2051
2173
|
}, { signal });
|
|
2052
2174
|
}
|
|
2053
2175
|
|
|
2054
|
-
// ---- Wire notes ----
|
|
2055
|
-
const notesEl = document.getElementById(uid + '-notes');
|
|
2056
|
-
if (notesEl) {
|
|
2057
|
-
let nTimer;
|
|
2058
|
-
notesEl.addEventListener('input', () => {
|
|
2059
|
-
clearTimeout(nTimer);
|
|
2060
|
-
nTimer = setTimeout(() => {
|
|
2061
|
-
if (!node.card_data) node.card_data = {};
|
|
2062
|
-
node.card_data._notes = notesEl.value;
|
|
2063
|
-
cfg.onPatch(node.id, { _notes: notesEl.value });
|
|
2064
|
-
}, 800);
|
|
2065
|
-
cleanup.timers.push(nTimer);
|
|
2066
|
-
}, { signal });
|
|
2067
|
-
}
|
|
2068
|
-
|
|
2069
2176
|
_autoSubscribe(node);
|
|
2070
2177
|
}
|
|
2071
2178
|
|
|
@@ -2221,7 +2328,6 @@ var LiveCard = (function () {
|
|
|
2221
2328
|
const nodeList = [];
|
|
2222
2329
|
const nodeMap = {}; // id → { node, colEl, bodyEl }
|
|
2223
2330
|
const _positions = {}; // id → { x, y, w, h } for canvas mode
|
|
2224
|
-
const showNotes = opts.showNotes !== false;
|
|
2225
2331
|
const showChat = opts.showChat || false;
|
|
2226
2332
|
const defaultCol = opts.defaultCol || 6;
|
|
2227
2333
|
|
|
@@ -2453,26 +2559,75 @@ var LiveCard = (function () {
|
|
|
2453
2559
|
|
|
2454
2560
|
function _buildCardWrapper(node) {
|
|
2455
2561
|
const wrap = document.createElement('div');
|
|
2456
|
-
|
|
2562
|
+
const card = node && node.card ? node.card : {};
|
|
2563
|
+
const isSimulation = card.meta && card.meta.simulation === true;
|
|
2564
|
+
const isGandalfCard = card.meta && card.meta._gandalfCard === true;
|
|
2565
|
+
const extraClass = isSimulation ? ' lc-simulation-card' : (isGandalfCard ? ' lc-gandalf-card' : '');
|
|
2566
|
+
wrap.className = 'card shadow-sm h-100' + extraClass;
|
|
2457
2567
|
const header = document.createElement('div');
|
|
2458
2568
|
header.className = 'card-header d-flex align-items-center gap-2 py-2';
|
|
2459
|
-
const card = node && node.card ? node.card : {};
|
|
2460
2569
|
const title = (card.meta && card.meta.title) || node.id;
|
|
2461
2570
|
const tags = (card.meta && card.meta.tags) || [];
|
|
2462
2571
|
let badgeHtml = '';
|
|
2463
|
-
if ((card.
|
|
2464
|
-
var src = card.
|
|
2572
|
+
if ((card.source_defs && card.source_defs.length) && !card.view) {
|
|
2573
|
+
var src = card.source_defs[0] || {};
|
|
2465
2574
|
badgeHtml = '<span class="badge bg-info text-dark ms-auto">' + _esc(src.kind || 'source') + '</span>';
|
|
2466
2575
|
} else if (tags.length) {
|
|
2467
2576
|
badgeHtml = tags.map(t => '<span class="badge bg-secondary ms-1">' + _esc(t) + '</span>').join('');
|
|
2468
2577
|
}
|
|
2469
2578
|
header.innerHTML = '<strong class="small">' + _esc(title) + '</strong>' + badgeHtml;
|
|
2470
|
-
|
|
2579
|
+
|
|
2580
|
+
// Gandalf cards: collapsible via caret — caret gets its own click listener,
|
|
2581
|
+
// header is left alone for dragging in canvas mode.
|
|
2582
|
+
if (isGandalfCard) {
|
|
2583
|
+
const caret = document.createElement('span');
|
|
2584
|
+
caret.className = 'lc-gandalf-caret';
|
|
2585
|
+
caret.title = 'Collapse / expand';
|
|
2586
|
+
caret.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>';
|
|
2587
|
+
header.appendChild(caret);
|
|
2588
|
+
|
|
2589
|
+
const storageKey = 'lc-gandalf-collapsed:' + (node.id || title);
|
|
2590
|
+
if (sessionStorage.getItem(storageKey) === '1') {
|
|
2591
|
+
wrap.classList.add('lc-collapsed');
|
|
2592
|
+
header.dataset.gandalfCollapsed = '1';
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
caret.addEventListener('click', function(e) {
|
|
2596
|
+
e.stopPropagation();
|
|
2597
|
+
const cardEl = caret.closest('.lc-gandalf-card') || wrap;
|
|
2598
|
+
cardEl.classList.toggle('lc-collapsed');
|
|
2599
|
+
sessionStorage.setItem(storageKey, cardEl.classList.contains('lc-collapsed') ? '1' : '0');
|
|
2600
|
+
});
|
|
2601
|
+
caret.addEventListener('pointerdown', e => e.stopPropagation()); // prevent drag start
|
|
2602
|
+
}
|
|
2603
|
+
if (isSimulation) {
|
|
2604
|
+
const simBtns = document.createElement('span');
|
|
2605
|
+
simBtns.className = 'd-inline-flex align-items-center gap-1 ms-auto';
|
|
2606
|
+
|
|
2607
|
+
const pinBtn = document.createElement('button');
|
|
2608
|
+
pinBtn.className = 'btn btn-sm btn-outline-success lc-sim-pin';
|
|
2609
|
+
pinBtn.style.cssText = 'padding: 2px 6px;';
|
|
2610
|
+
pinBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 17v5"/><path d="M9 2h6l-1 7h-4L9 2z"/><path d="M6 17h12l-2-4H8L6 17z"/></svg>';
|
|
2611
|
+
pinBtn.title = 'Pin this simulation card';
|
|
2612
|
+
pinBtn.dataset.nodeId = node.id;
|
|
2613
|
+
|
|
2614
|
+
const discardBtn = document.createElement('button');
|
|
2615
|
+
discardBtn.className = 'btn btn-sm btn-outline-danger lc-sim-discard';
|
|
2616
|
+
discardBtn.style.cssText = 'padding: 2px 6px;';
|
|
2617
|
+
discardBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
|
|
2618
|
+
discardBtn.title = 'Discard this simulation card';
|
|
2619
|
+
discardBtn.dataset.nodeId = node.id;
|
|
2620
|
+
|
|
2621
|
+
simBtns.appendChild(pinBtn);
|
|
2622
|
+
simBtns.appendChild(discardBtn);
|
|
2623
|
+
header.appendChild(simBtns);
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2471
2626
|
// Add dev mode code icon button if devMode is enabled
|
|
2472
2627
|
if (devMode.current) {
|
|
2473
2628
|
const codeBtn = document.createElement('button');
|
|
2474
2629
|
codeBtn.className = 'btn btn-sm btn-outline-secondary';
|
|
2475
|
-
codeBtn.style.cssText = 'padding: 2px 6px; margin-left: auto;';
|
|
2630
|
+
codeBtn.style.cssText = 'padding: 2px 6px;' + (isSimulation ? '' : ' margin-left: auto;');
|
|
2476
2631
|
codeBtn.innerHTML = '</>';
|
|
2477
2632
|
codeBtn.title = 'Inspect card data';
|
|
2478
2633
|
codeBtn.addEventListener('click', function(e) {
|
|
@@ -2495,7 +2650,7 @@ var LiveCard = (function () {
|
|
|
2495
2650
|
const status = (node.card_data && node.card_data.status) || 'fresh';
|
|
2496
2651
|
const card = node && node.card ? node.card : {};
|
|
2497
2652
|
const title = (card.meta && card.meta.title) || node.id;
|
|
2498
|
-
const kind = (card.
|
|
2653
|
+
const kind = (card.source_defs && card.source_defs[0] && card.source_defs[0].kind) || 'source';
|
|
2499
2654
|
el.innerHTML = `<div class="lc-source-pill shadow-sm">
|
|
2500
2655
|
${_statusDot(status)}
|
|
2501
2656
|
<span class="fw-medium">${_esc(title)}</span>
|
|
@@ -2527,7 +2682,7 @@ var LiveCard = (function () {
|
|
|
2527
2682
|
col.appendChild(wrap);
|
|
2528
2683
|
gridEl.appendChild(col);
|
|
2529
2684
|
nodeMap[node.id] = { node, colEl: col, bodyEl: body };
|
|
2530
|
-
engine.render(node, body, {
|
|
2685
|
+
engine.render(node, body, { showChat });
|
|
2531
2686
|
});
|
|
2532
2687
|
}
|
|
2533
2688
|
|
|
@@ -2615,7 +2770,7 @@ var LiveCard = (function () {
|
|
|
2615
2770
|
nodeList.forEach(node => {
|
|
2616
2771
|
const pos = _positions[node.id] || { x: 0, y: 0 };
|
|
2617
2772
|
|
|
2618
|
-
if ((!node.card || !node.card.view) && (node.card && node.card.
|
|
2773
|
+
if ((!node.card || !node.card.view) && (node.card && node.card.source_defs && node.card.source_defs.length)) {
|
|
2619
2774
|
const el = _buildSourcePill(node);
|
|
2620
2775
|
el.dataset.nodeId = node.id;
|
|
2621
2776
|
el.style.left = pos.x + 'px';
|
|
@@ -2625,7 +2780,10 @@ var LiveCard = (function () {
|
|
|
2625
2780
|
_makeDraggable(el, node);
|
|
2626
2781
|
} else {
|
|
2627
2782
|
const el = document.createElement('div');
|
|
2628
|
-
|
|
2783
|
+
const isSimCanvas = node.card && node.card.meta && node.card.meta.simulation === true;
|
|
2784
|
+
const isGandalfCanvas = node.card && node.card.meta && node.card.meta._gandalfCard === true;
|
|
2785
|
+
const canvasExtra = isSimCanvas ? ' lc-simulation-card' : (isGandalfCanvas ? ' lc-gandalf-card' : '');
|
|
2786
|
+
el.className = 'lc-canvas-card card shadow-sm' + canvasExtra;
|
|
2629
2787
|
el.dataset.nodeId = node.id;
|
|
2630
2788
|
el.style.left = pos.x + 'px';
|
|
2631
2789
|
el.style.top = pos.y + 'px';
|
|
@@ -2633,9 +2791,12 @@ var LiveCard = (function () {
|
|
|
2633
2791
|
|
|
2634
2792
|
const { wrap, body } = _buildCardWrapper(node);
|
|
2635
2793
|
while (wrap.firstChild) el.appendChild(wrap.firstChild);
|
|
2794
|
+
// Re-apply collapsed state: in canvas mode el is the card container, not wrap
|
|
2795
|
+
const movedHeader = el.querySelector('.card-header');
|
|
2796
|
+
if (movedHeader && movedHeader.dataset.gandalfCollapsed === '1') el.classList.add('lc-collapsed');
|
|
2636
2797
|
canvasInner.appendChild(el);
|
|
2637
2798
|
nodeMap[node.id] = { node, colEl: el, bodyEl: body };
|
|
2638
|
-
engine.render(node, body, {
|
|
2799
|
+
engine.render(node, body, { showChat: false });
|
|
2639
2800
|
_makeDraggable(el, node);
|
|
2640
2801
|
}
|
|
2641
2802
|
});
|