yaml-flow 5.2.8 → 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/browser/board-livegraph-engine.js +4 -1
- package/browser/board-livegraph-engine.js.map +1 -1
- package/browser/card-compute.js +1 -1
- package/browser/live-cards.js +178 -144
- package/browser/live-cards.schema.json +1 -1
- package/dist/board-livegraph-runtime/index.cjs +4 -1
- package/dist/board-livegraph-runtime/index.cjs.map +1 -1
- package/dist/board-livegraph-runtime/index.js +4 -1
- package/dist/board-livegraph-runtime/index.js.map +1 -1
- package/dist/card-compute/index.cjs +5 -1
- package/dist/card-compute/index.cjs.map +1 -1
- package/dist/card-compute/index.js +5 -1
- package/dist/card-compute/index.js.map +1 -1
- package/dist/cli/board-live-cards-cli.cjs +5 -1
- package/dist/cli/board-live-cards-cli.cjs.map +1 -1
- package/dist/cli/board-live-cards-cli.js +5 -1
- package/dist/cli/board-live-cards-cli.js.map +1 -1
- package/dist/continuous-event-graph/index.cjs +4 -1
- package/dist/continuous-event-graph/index.cjs.map +1 -1
- package/dist/continuous-event-graph/index.js +4 -1
- package/dist/continuous-event-graph/index.js.map +1 -1
- package/dist/index.cjs +5 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/examples/example-board/agent-instructions-cardlayout.md +28 -0
- package/examples/example-board/cards/card-rebalance-sim.json +13 -3
- package/examples/example-board/demo-server.js +20 -8
- package/examples/example-board/demo-shell-browser.html +3 -3
- package/examples/example-board/demo-shell-with-server.html +4 -4
- package/examples/example-board/demo-task-executor.js +21 -0
- package/package.json +1 -1
- package/schema/live-cards.schema.json +1 -1
package/browser/card-compute.js
CHANGED
|
@@ -124,7 +124,7 @@
|
|
|
124
124
|
// validate — lightweight structural validator (sync)
|
|
125
125
|
// ===========================================================================
|
|
126
126
|
|
|
127
|
-
var VALID_ELEMENT_KINDS = ['metric','table','chart','form','filter','list','notes','todo','alert','narrative','badge','text','markdown','custom'];
|
|
127
|
+
var VALID_ELEMENT_KINDS = ['metric','table','editable-table','chart','form','filter','list','notes','todo','alert','narrative','badge','text','markdown','ref','custom','actions'];
|
|
128
128
|
var VALID_STATUSES = ['fresh','stale','loading','error'];
|
|
129
129
|
var ALLOWED_KEYS = ['id','meta','requires','provides','view','card_data','compute','source_defs'];
|
|
130
130
|
|
package/browser/live-cards.js
CHANGED
|
@@ -212,9 +212,9 @@ var LiveCard = (function () {
|
|
|
212
212
|
|
|
213
213
|
const _cleanup = {}; // nodeId → { ac, timers, charts, unsubs }
|
|
214
214
|
const _subs = {}; // nodeId → Set<callback>
|
|
215
|
-
const _etState = {}; // stateKey → {
|
|
216
|
-
const _formState = {}; // stateKey → {
|
|
217
|
-
const _notesState = {}; // stateKey → {
|
|
215
|
+
const _etState = {}; // stateKey → { baseRows, journalRows|null }
|
|
216
|
+
const _formState = {}; // stateKey → { baseValues, journal }
|
|
217
|
+
const _notesState = {}; // stateKey → { baseContent, journal|null }
|
|
218
218
|
const _todoState = {}; // stateKey → { currentState, pending } for todo dirty tracking
|
|
219
219
|
const _renderers = {}; // kind → fn
|
|
220
220
|
const _nodeEls = {}; // nodeId → { container, resultEl, uid }
|
|
@@ -1008,45 +1008,50 @@ var LiveCard = (function () {
|
|
|
1008
1008
|
const props = schema.properties || {};
|
|
1009
1009
|
const required = schema.required || [];
|
|
1010
1010
|
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
// pending = user's local field values accumulated on top of currentState
|
|
1014
|
-
// dirty = JSON.stringify(currentState) !== JSON.stringify(pending)
|
|
1015
|
-
//
|
|
1016
|
-
// On SSE re-render:
|
|
1017
|
-
// - currentState updated to new server values
|
|
1018
|
-
// - if NOT dirty: pending follows currentState (no unsaved edits)
|
|
1019
|
-
// - if dirty: pending untouched, form is rebuilt from pending (edits survive)
|
|
1020
|
-
const stateKey = node.id + ':' + (writeTo || '');
|
|
1021
|
-
const incomingValues = writeTo ? (_resolveBind(node, writeTo) || {}) : (data && typeof data === 'object' ? data : {});
|
|
1022
|
-
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) : {};
|
|
1023
1013
|
|
|
1024
1014
|
if (!_formState[stateKey]) {
|
|
1025
|
-
_formState[stateKey] = {
|
|
1015
|
+
_formState[stateKey] = { baseValues, journal: {} };
|
|
1026
1016
|
} else {
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
}
|
|
1033
|
-
// 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
|
+
});
|
|
1034
1023
|
}
|
|
1035
1024
|
|
|
1036
1025
|
const st = _formState[stateKey];
|
|
1037
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
|
+
|
|
1038
1041
|
function isDirty() {
|
|
1039
|
-
return
|
|
1042
|
+
return Object.keys(st.journal).length > 0;
|
|
1040
1043
|
}
|
|
1041
1044
|
|
|
1042
|
-
//
|
|
1043
|
-
function
|
|
1045
|
+
// Capture user edits into a journal overlay (only changed keys).
|
|
1046
|
+
function captureJournal(form) {
|
|
1044
1047
|
form.querySelectorAll('[data-key]').forEach(inp => {
|
|
1045
|
-
const k = inp.dataset.key
|
|
1048
|
+
const k = inp.dataset.key;
|
|
1049
|
+
const p = props[k];
|
|
1046
1050
|
if (!p) return;
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
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;
|
|
1050
1055
|
});
|
|
1051
1056
|
}
|
|
1052
1057
|
|
|
@@ -1098,8 +1103,8 @@ var LiveCard = (function () {
|
|
|
1098
1103
|
|
|
1099
1104
|
input.dataset.key = key;
|
|
1100
1105
|
if (isReq) input.required = true;
|
|
1101
|
-
// Populate from
|
|
1102
|
-
const v =
|
|
1106
|
+
// Populate from effective values (base bind overlaid by local journal).
|
|
1107
|
+
const v = getEffectiveValues()[key];
|
|
1103
1108
|
if (v != null) {
|
|
1104
1109
|
if (prop.type === 'boolean') input.checked = !!v;
|
|
1105
1110
|
else if (prop.format === 'date') input.value = String(v).slice(0, 10);
|
|
@@ -1110,34 +1115,51 @@ var LiveCard = (function () {
|
|
|
1110
1115
|
|
|
1111
1116
|
const btnCol = document.createElement('div');
|
|
1112
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';
|
|
1113
1122
|
const btn = document.createElement('button');
|
|
1114
1123
|
btn.type = 'submit';
|
|
1115
1124
|
btn.className = 'btn btn-sm btn-primary' + (isDirty() ? '' : ' d-none');
|
|
1116
|
-
btn.textContent = '
|
|
1125
|
+
btn.textContent = 'Save';
|
|
1126
|
+
btnCol.appendChild(discardBtn);
|
|
1117
1127
|
btnCol.appendChild(btn);
|
|
1118
1128
|
form.appendChild(btnCol);
|
|
1119
1129
|
|
|
1120
1130
|
el.innerHTML = '';
|
|
1121
1131
|
el.appendChild(form);
|
|
1122
1132
|
|
|
1123
|
-
// Real-time input → update
|
|
1133
|
+
// Real-time input → update journal + toggle Save/Discard buttons
|
|
1124
1134
|
form.addEventListener('input', () => {
|
|
1125
|
-
|
|
1126
|
-
|
|
1135
|
+
captureJournal(form);
|
|
1136
|
+
const dirty = isDirty();
|
|
1137
|
+
btn.classList.toggle('d-none', !dirty);
|
|
1138
|
+
discardBtn.classList.toggle('d-none', !dirty);
|
|
1127
1139
|
}, { signal });
|
|
1128
1140
|
|
|
1129
1141
|
form.addEventListener('submit', e => {
|
|
1130
1142
|
e.preventDefault();
|
|
1131
1143
|
if (!form.checkValidity()) { form.classList.add('was-validated'); return; }
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
cfg.onPatchState(node.id, { fieldValues:
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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');
|
|
1138
1162
|
btn.classList.add('d-none');
|
|
1139
|
-
btn.textContent = '✓ Saved';
|
|
1140
|
-
setTimeout(() => { btn.textContent = 'Submit'; }, 1500);
|
|
1141
1163
|
}, { signal });
|
|
1142
1164
|
}
|
|
1143
1165
|
|
|
@@ -1150,54 +1172,63 @@ var LiveCard = (function () {
|
|
|
1150
1172
|
const writeTo = ed.writeTo;
|
|
1151
1173
|
const incomingContent = typeof data === 'string' ? data : '';
|
|
1152
1174
|
|
|
1153
|
-
//
|
|
1154
|
-
//
|
|
1155
|
-
|
|
1156
|
-
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) || '');
|
|
1157
1178
|
if (!_notesState[stateKey]) {
|
|
1158
|
-
_notesState[stateKey] = {
|
|
1179
|
+
_notesState[stateKey] = { baseContent: incomingContent, journal: null };
|
|
1159
1180
|
} else {
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
// 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
|
+
}
|
|
1165
1185
|
}
|
|
1166
1186
|
const st = _notesState[stateKey];
|
|
1167
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
|
+
|
|
1168
1200
|
el.innerHTML = `
|
|
1169
|
-
<
|
|
1170
|
-
|
|
1171
|
-
<button class="btn btn-outline-secondary lc-n-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
<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>`;
|
|
1175
1206
|
|
|
1176
1207
|
const textarea = el.querySelector('.lc-notes-textarea');
|
|
1177
|
-
const
|
|
1178
|
-
const
|
|
1179
|
-
|
|
1208
|
+
const discardBtn = el.querySelector('.lc-n-discard');
|
|
1209
|
+
const saveBtn = el.querySelector('.lc-n-save');
|
|
1210
|
+
|
|
1211
|
+
function syncDirtyButtons() {
|
|
1212
|
+
const dirty = isDirty();
|
|
1213
|
+
saveBtn.classList.toggle('d-none', !dirty);
|
|
1214
|
+
discardBtn.classList.toggle('d-none', !dirty);
|
|
1215
|
+
}
|
|
1180
1216
|
|
|
1181
|
-
|
|
1182
|
-
textarea.
|
|
1183
|
-
|
|
1217
|
+
textarea.addEventListener('input', () => {
|
|
1218
|
+
setJournal(textarea.value);
|
|
1219
|
+
syncDirtyButtons();
|
|
1184
1220
|
}, { signal });
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
textarea.
|
|
1188
|
-
|
|
1221
|
+
|
|
1222
|
+
saveBtn.addEventListener('click', () => {
|
|
1223
|
+
const nextValue = textarea.value;
|
|
1224
|
+
cfg.onPatchState(node.id, { notes: nextValue });
|
|
1225
|
+
saveBtn.textContent = 'Saving...';
|
|
1189
1226
|
}, { signal });
|
|
1190
1227
|
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
timer = setTimeout(() => {
|
|
1196
|
-
if (writeTo) _deepSet(node, writeTo, textarea.value);
|
|
1197
|
-
cfg.onPatchState(node.id, { notes: textarea.value });
|
|
1198
|
-
st.currentState = textarea.value; // saved — no longer dirty
|
|
1199
|
-
}, 800);
|
|
1200
|
-
cleanup.timers.push(timer);
|
|
1228
|
+
discardBtn.addEventListener('click', () => {
|
|
1229
|
+
st.journal = null;
|
|
1230
|
+
textarea.value = st.baseContent || '';
|
|
1231
|
+
syncDirtyButtons();
|
|
1201
1232
|
}, { signal });
|
|
1202
1233
|
}
|
|
1203
1234
|
|
|
@@ -1215,7 +1246,11 @@ var LiveCard = (function () {
|
|
|
1215
1246
|
const cleanup = _getCleanup(node.id);
|
|
1216
1247
|
const signal = cleanup.ac.signal;
|
|
1217
1248
|
const ed = elemDef.data || {};
|
|
1218
|
-
|
|
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);
|
|
1219
1254
|
const schemaProps = (ed.schema && ed.schema.properties) || {};
|
|
1220
1255
|
const canAdd = ed.addRow !== false;
|
|
1221
1256
|
const canDelete = ed.deleteRow !== false;
|
|
@@ -1228,57 +1263,59 @@ var LiveCard = (function () {
|
|
|
1228
1263
|
return [...s];
|
|
1229
1264
|
}
|
|
1230
1265
|
|
|
1231
|
-
//
|
|
1232
|
-
//
|
|
1233
|
-
//
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
// When SSE fires and re-renders this element:
|
|
1237
|
-
// - currentState is updated to the new server value
|
|
1238
|
-
// - if NOT dirty: pending follows currentState (no unsaved edits)
|
|
1239
|
-
// - if dirty: pending is left untouched (user's edits survive the SSE tick)
|
|
1240
|
-
const stateKey = node.id + ':' + (ed.bind || ed.writeTo || '');
|
|
1241
|
-
const incomingRows = Array.isArray(writeTo ? _resolveBind(node, writeTo) : data)
|
|
1242
|
-
? (writeTo ? _resolveBind(node, writeTo) : data)
|
|
1243
|
-
: [];
|
|
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 : [];
|
|
1244
1271
|
const incomingCopy = incomingRows.map(r => Object.assign({}, r));
|
|
1245
1272
|
|
|
1246
1273
|
if (!_etState[stateKey]) {
|
|
1247
|
-
|
|
1248
|
-
_etState[stateKey] = { currentState: incomingCopy, pending: incomingCopy.map(r => Object.assign({}, r)) };
|
|
1274
|
+
_etState[stateKey] = { baseRows: incomingCopy, journalRows: null };
|
|
1249
1275
|
} else {
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
if (!wasDirty) {
|
|
1254
|
-
// No unsaved edits — sync pending to new server state
|
|
1255
|
-
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;
|
|
1256
1279
|
}
|
|
1257
|
-
// If dirty, pending stays as-is so user's edits survive the SSE tick
|
|
1258
1280
|
}
|
|
1259
1281
|
|
|
1260
1282
|
const st = _etState[stateKey];
|
|
1261
1283
|
|
|
1262
1284
|
function isDirty() {
|
|
1263
|
-
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));
|
|
1264
1296
|
}
|
|
1265
1297
|
|
|
1266
1298
|
function markDirty() {
|
|
1267
1299
|
const saveBtn = el.querySelector('.lc-et-save');
|
|
1300
|
+
const discardBtn = el.querySelector('.lc-et-discard');
|
|
1268
1301
|
if (saveBtn) saveBtn.classList.remove('d-none');
|
|
1302
|
+
if (discardBtn) discardBtn.classList.remove('d-none');
|
|
1269
1303
|
}
|
|
1270
1304
|
|
|
1271
1305
|
function commitSave() {
|
|
1272
|
-
|
|
1273
|
-
cfg.onPatchState(node.id, { fieldValues:
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
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;
|
|
1277
1314
|
build();
|
|
1278
1315
|
}
|
|
1279
1316
|
|
|
1280
1317
|
function build() {
|
|
1281
|
-
const rows =
|
|
1318
|
+
const rows = getEffectiveRows();
|
|
1282
1319
|
const cols = getCols(rows);
|
|
1283
1320
|
|
|
1284
1321
|
if (!cols.length && !canAdd) {
|
|
@@ -1321,44 +1358,63 @@ var LiveCard = (function () {
|
|
|
1321
1358
|
h += '</tbody></table></div>';
|
|
1322
1359
|
let footer = '';
|
|
1323
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>`;
|
|
1324
1362
|
footer += `<button class="btn btn-sm btn-primary mt-1 lc-et-save${isDirty() ? '' : ' d-none'}">Save</button>`;
|
|
1325
1363
|
el.innerHTML = h + footer;
|
|
1326
1364
|
|
|
1327
|
-
// Cell edit → update
|
|
1365
|
+
// Cell edit → update journal overlay and toggle Save/Discard.
|
|
1328
1366
|
el.querySelectorAll('.lc-et-cell').forEach(inp => {
|
|
1329
1367
|
inp.addEventListener('change', () => {
|
|
1330
1368
|
const rowIdx = parseInt(inp.dataset.row);
|
|
1331
1369
|
const colName = inp.dataset.col;
|
|
1332
1370
|
const prop = schemaProps[colName] || {};
|
|
1333
1371
|
const isNum = prop.type === 'number' || prop.type === 'integer' || inp.type === 'number';
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
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);
|
|
1337
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
|
+
}
|
|
1338
1384
|
}, { signal });
|
|
1339
1385
|
});
|
|
1340
1386
|
|
|
1341
|
-
// Delete row — updates
|
|
1387
|
+
// Delete row — updates journal and rebuilds.
|
|
1342
1388
|
el.querySelectorAll('.lc-et-del').forEach(btn => {
|
|
1343
1389
|
btn.addEventListener('click', () => {
|
|
1344
1390
|
const rowIdx = parseInt(btn.dataset.row);
|
|
1345
|
-
|
|
1391
|
+
const nextRows = getEffectiveRows().filter((_, i) => i !== rowIdx);
|
|
1392
|
+
updateJournal(nextRows);
|
|
1346
1393
|
build();
|
|
1347
1394
|
}, { signal });
|
|
1348
1395
|
});
|
|
1349
1396
|
|
|
1350
|
-
// Add row — appends blank row to
|
|
1397
|
+
// Add row — appends blank row to journal and rebuilds.
|
|
1351
1398
|
const addBtn = el.querySelector('.lc-et-add');
|
|
1352
1399
|
if (addBtn) {
|
|
1353
1400
|
addBtn.addEventListener('click', () => {
|
|
1354
1401
|
const newRow = {};
|
|
1355
|
-
|
|
1356
|
-
|
|
1402
|
+
const nextRows = getEffectiveRows();
|
|
1403
|
+
getCols(nextRows).forEach(c => { newRow[c] = ''; });
|
|
1404
|
+
nextRows.push(newRow);
|
|
1405
|
+
updateJournal(nextRows);
|
|
1357
1406
|
build();
|
|
1358
1407
|
}, { signal });
|
|
1359
1408
|
}
|
|
1360
1409
|
|
|
1361
|
-
// 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
|
+
|
|
1362
1418
|
const saveBtn = el.querySelector('.lc-et-save');
|
|
1363
1419
|
if (saveBtn) {
|
|
1364
1420
|
saveBtn.addEventListener('click', () => {
|
|
@@ -2078,12 +2134,6 @@ var LiveCard = (function () {
|
|
|
2078
2134
|
// Elements area
|
|
2079
2135
|
h += `<div class="lc-result" id="${uid}-result"></div>`;
|
|
2080
2136
|
|
|
2081
|
-
// Notes section (feature toggle)
|
|
2082
|
-
if (features.notes && opts.showNotes !== false) {
|
|
2083
|
-
h += `<details class="mt-2"><summary class="small fw-medium">Notes</summary>`;
|
|
2084
|
-
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>`;
|
|
2085
|
-
}
|
|
2086
|
-
|
|
2087
2137
|
h += '</div>';
|
|
2088
2138
|
containerEl.innerHTML = h;
|
|
2089
2139
|
|
|
@@ -2123,21 +2173,6 @@ var LiveCard = (function () {
|
|
|
2123
2173
|
}, { signal });
|
|
2124
2174
|
}
|
|
2125
2175
|
|
|
2126
|
-
// ---- Wire notes ----
|
|
2127
|
-
const notesEl = document.getElementById(uid + '-notes');
|
|
2128
|
-
if (notesEl) {
|
|
2129
|
-
let nTimer;
|
|
2130
|
-
notesEl.addEventListener('input', () => {
|
|
2131
|
-
clearTimeout(nTimer);
|
|
2132
|
-
nTimer = setTimeout(() => {
|
|
2133
|
-
if (!node.card_data) node.card_data = {};
|
|
2134
|
-
node.card_data._notes = notesEl.value;
|
|
2135
|
-
cfg.onPatch(node.id, { _notes: notesEl.value });
|
|
2136
|
-
}, 800);
|
|
2137
|
-
cleanup.timers.push(nTimer);
|
|
2138
|
-
}, { signal });
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
2176
|
_autoSubscribe(node);
|
|
2142
2177
|
}
|
|
2143
2178
|
|
|
@@ -2293,7 +2328,6 @@ var LiveCard = (function () {
|
|
|
2293
2328
|
const nodeList = [];
|
|
2294
2329
|
const nodeMap = {}; // id → { node, colEl, bodyEl }
|
|
2295
2330
|
const _positions = {}; // id → { x, y, w, h } for canvas mode
|
|
2296
|
-
const showNotes = opts.showNotes !== false;
|
|
2297
2331
|
const showChat = opts.showChat || false;
|
|
2298
2332
|
const defaultCol = opts.defaultCol || 6;
|
|
2299
2333
|
|
|
@@ -2648,7 +2682,7 @@ var LiveCard = (function () {
|
|
|
2648
2682
|
col.appendChild(wrap);
|
|
2649
2683
|
gridEl.appendChild(col);
|
|
2650
2684
|
nodeMap[node.id] = { node, colEl: col, bodyEl: body };
|
|
2651
|
-
engine.render(node, body, {
|
|
2685
|
+
engine.render(node, body, { showChat });
|
|
2652
2686
|
});
|
|
2653
2687
|
}
|
|
2654
2688
|
|
|
@@ -2762,7 +2796,7 @@ var LiveCard = (function () {
|
|
|
2762
2796
|
if (movedHeader && movedHeader.dataset.gandalfCollapsed === '1') el.classList.add('lc-collapsed');
|
|
2763
2797
|
canvasInner.appendChild(el);
|
|
2764
2798
|
nodeMap[node.id] = { node, colEl: el, bodyEl: body };
|
|
2765
|
-
engine.render(node, body, {
|
|
2799
|
+
engine.render(node, body, { showChat: false });
|
|
2766
2800
|
_makeDraggable(el, node);
|
|
2767
2801
|
}
|
|
2768
2802
|
});
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"kind": {
|
|
127
127
|
"enum": ["metric", "table", "editable-table", "chart", "form", "filter", "list",
|
|
128
128
|
"notes", "todo", "alert", "narrative", "badge", "text",
|
|
129
|
-
"markdown", "custom", "actions"]
|
|
129
|
+
"markdown", "ref", "custom", "actions"]
|
|
130
130
|
},
|
|
131
131
|
"label": { "type": "string", "description": "Heading above this element" },
|
|
132
132
|
"className": { "type": "string", "description": "Bootstrap grid class, e.g. 'col-12 col-md-6'" },
|
|
@@ -70,6 +70,7 @@ function resolve(node, path) {
|
|
|
70
70
|
var VALID_ELEMENT_KINDS = /* @__PURE__ */ new Set([
|
|
71
71
|
"metric",
|
|
72
72
|
"table",
|
|
73
|
+
"editable-table",
|
|
73
74
|
"chart",
|
|
74
75
|
"form",
|
|
75
76
|
"filter",
|
|
@@ -81,7 +82,9 @@ var VALID_ELEMENT_KINDS = /* @__PURE__ */ new Set([
|
|
|
81
82
|
"badge",
|
|
82
83
|
"text",
|
|
83
84
|
"markdown",
|
|
84
|
-
"
|
|
85
|
+
"ref",
|
|
86
|
+
"custom",
|
|
87
|
+
"actions"
|
|
85
88
|
]);
|
|
86
89
|
var ALLOWED_KEYS = /* @__PURE__ */ new Set(["id", "meta", "requires", "provides", "view", "card_data", "compute", "source_defs"]);
|
|
87
90
|
function validateNode(node) {
|