mdboard 2.0.0 → 2.1.1
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/index.html +106 -9
- package/package.json +1 -1
- package/presets/shape-up/entities.json +4 -4
- package/src/core/yaml.js +43 -3
package/index.html
CHANGED
|
@@ -202,12 +202,16 @@ th.sorted .sort-arrow{opacity:1;color:var(--accent)}
|
|
|
202
202
|
/* ── Detail Panel — Notion-style ─────────────────────────── */
|
|
203
203
|
.panel-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:99;opacity:0;pointer-events:none;transition:opacity .2s}
|
|
204
204
|
.panel-overlay.open{opacity:1;pointer-events:auto}
|
|
205
|
-
.detail-panel{top: 1rem;right: 1rem;height: calc(100vh - 2rem);border-radius: var(--radius);position:fixed;width:600px;max-width:92vw;background:var(--bg);border-left:1px solid var(--border);z-index:100;transform:translateX(100%);transition:transform .25s ease;display:flex;flex-direction:column;overflow:hidden}
|
|
205
|
+
.detail-panel{top: 1rem;right: 1rem;height: calc(100vh - 2rem);border-radius: var(--radius);position:fixed;width:600px;max-width:92vw;background:var(--bg);border-left:1px solid var(--border);z-index:100;transform:translateX(100%);transition:transform .25s ease,top .25s ease,left .25s ease,right .25s ease,width .25s ease,height .25s ease;display:flex;flex-direction:column;overflow:hidden}
|
|
206
206
|
.detail-panel.open{transform:translateX(0)}
|
|
207
|
+
.detail-panel.expanded{top:50%;left:50%;right:auto;width:900px;height:85vh;border:1px solid var(--border);box-shadow:0 16px 48px rgba(0,0,0,.4)}
|
|
208
|
+
.detail-panel.expanded.open{transform:translate(-50%,-50%)}
|
|
207
209
|
.panel-header{padding:12px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;flex-shrink:0;background:var(--surface)}
|
|
208
210
|
.panel-type{font-size:10px;padding:3px 8px;border-radius:var(--radius-sm);background:var(--accent-dim);color:var(--accent);font-weight:700;text-transform:uppercase;letter-spacing:.04em}
|
|
209
211
|
.panel-item-id{font-family:var(--mono);font-size:13px;color:var(--text2)}
|
|
210
|
-
.panel-
|
|
212
|
+
.panel-expand{margin-left:auto;background:none;border:none;color:var(--text2);cursor:pointer;padding:4px 8px;border-radius:var(--radius-sm);line-height:1;display:flex;align-items:center}
|
|
213
|
+
.panel-expand:hover{background:var(--surface2);color:var(--text)}
|
|
214
|
+
.panel-close{background:none;border:none;color:var(--text2);cursor:pointer;font-size:18px;padding:4px 8px;border-radius:var(--radius-sm);line-height:1}
|
|
211
215
|
.panel-close:hover{background:var(--surface2);color:var(--text)}
|
|
212
216
|
.panel-title-wrap{padding:20px 24px 8px;flex-shrink:0}
|
|
213
217
|
.panel-title-input{width:100%;font-size:28px;font-weight:700;background:transparent;border:none;color:var(--text);font-family:var(--font);outline:none;padding:0;line-height:1.3}
|
|
@@ -223,6 +227,14 @@ th.sorted .sort-arrow{opacity:1;color:var(--accent)}
|
|
|
223
227
|
.prop-value select:disabled,.prop-value input:disabled{opacity:.6;cursor:default}
|
|
224
228
|
.prop-value select:disabled:hover,.prop-value input:disabled:hover{border-color:transparent}
|
|
225
229
|
.prop-text{font-size:13px;color:var(--text2);padding:4px 8px}
|
|
230
|
+
.prop-longtext{width:100%}
|
|
231
|
+
.prop-longtext-preview{display:flex;align-items:center;gap:6px;cursor:pointer;padding:4px 8px;border-radius:var(--radius-sm);transition:background .15s;min-height:28px}
|
|
232
|
+
.prop-longtext-preview:hover{background:var(--surface2)}
|
|
233
|
+
.prop-longtext-text{flex:1;font-size:13px;color:var(--text2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
234
|
+
.prop-longtext-toggle{font-size:9px;color:var(--text3);flex-shrink:0;transition:transform .15s}
|
|
235
|
+
.prop-longtext-editor{width:100%;min-height:80px;resize:vertical;background:var(--surface);border:1px solid var(--border);color:var(--text);padding:8px;border-radius:var(--radius-sm);font-family:var(--font);font-size:13px;line-height:1.5;outline:none;margin-top:4px;transition:border-color .2s}
|
|
236
|
+
.prop-longtext-editor:focus{border-color:var(--accent)}
|
|
237
|
+
.prop-longtext-editor:disabled{opacity:.6;cursor:default}
|
|
226
238
|
.prop-icon{flex-shrink:0}
|
|
227
239
|
.prop-row-toggle{cursor:pointer;user-select:none}
|
|
228
240
|
.prop-row-toggle:hover{background:var(--surface2);border-radius:var(--radius-sm)}
|
|
@@ -467,6 +479,7 @@ h4.ce-header{font-size:15px;line-height:1.4}
|
|
|
467
479
|
.header{padding:12px 16px}
|
|
468
480
|
.content{padding:16px}
|
|
469
481
|
.detail-panel{width:100%;max-width:100%}
|
|
482
|
+
.detail-panel.expanded{width:96vw;height:90vh}
|
|
470
483
|
.notes-sidebar{width:200px}
|
|
471
484
|
.notes-header-bar{padding:24px 24px 0}
|
|
472
485
|
.notes-editorjs-wrap{padding:12px 16px 24px}
|
|
@@ -1156,6 +1169,12 @@ function renderFieldPill(item, entityType, field) {
|
|
|
1156
1169
|
}
|
|
1157
1170
|
}
|
|
1158
1171
|
|
|
1172
|
+
if (fieldDef && fieldDef.type === 'longtext') {
|
|
1173
|
+
var ltText = String(value);
|
|
1174
|
+
var ltTrunc = ltText.length > 50 ? ltText.substring(0, 50) + '...' : ltText;
|
|
1175
|
+
return '<span class="pill" style="background:var(--surface2);color:var(--text2)">' + escHtml(ltTrunc) + '</span>';
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1159
1178
|
if (fieldDef && fieldDef.type === 'number' && value != null) {
|
|
1160
1179
|
return '<span class="pill pill-points">' + value + (fieldDef.label === 'Points' ? ' pts' : '') + '</span>';
|
|
1161
1180
|
}
|
|
@@ -1185,6 +1204,12 @@ function renderFieldCell(item, entityType, col) {
|
|
|
1185
1204
|
statusIcon(entityType, value) + ' ' + escHtml(getFieldLabel(entityType, col, value)) + '</span>';
|
|
1186
1205
|
}
|
|
1187
1206
|
|
|
1207
|
+
if (fieldDef && fieldDef.type === 'longtext') {
|
|
1208
|
+
var ltText = String(value);
|
|
1209
|
+
var ltTrunc = ltText.length > 50 ? ltText.substring(0, 50) + '...' : ltText;
|
|
1210
|
+
return '<span style="color:var(--text2)">' + escHtml(ltTrunc) + '</span>';
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1188
1213
|
if (fieldDef && fieldDef.type === 'list') {
|
|
1189
1214
|
var arr = Array.isArray(value) ? value : [value];
|
|
1190
1215
|
return escHtml(arr.join(', '));
|
|
@@ -2590,22 +2615,43 @@ async function deleteCurrentNote(id) {
|
|
|
2590
2615
|
/* ══════════════════════════════════════════════════════════════
|
|
2591
2616
|
PANEL — Dynamic detail panel from config
|
|
2592
2617
|
══════════════════════════════════════════════════════════════ */
|
|
2593
|
-
var panelState = { open: false, entityType: null, item: null, isCreate: false, editor: null };
|
|
2618
|
+
var panelState = { open: false, entityType: null, item: null, isCreate: false, editor: null, expanded: false };
|
|
2594
2619
|
|
|
2595
|
-
function openPanel(entityType, item) {
|
|
2596
|
-
panelState = { open: true, entityType: entityType, item: JSON.parse(JSON.stringify(item)), isCreate: false, editor: null };
|
|
2597
|
-
renderPanel();
|
|
2620
|
+
async function openPanel(entityType, item) {
|
|
2621
|
+
panelState = { open: true, entityType: entityType, item: JSON.parse(JSON.stringify(item)), isCreate: false, editor: null, expanded: false };
|
|
2598
2622
|
document.getElementById('detail-panel').classList.add('open');
|
|
2599
2623
|
document.getElementById('panel-overlay').classList.add('open');
|
|
2624
|
+
|
|
2625
|
+
// Fetch full item with content from single endpoint
|
|
2626
|
+
var plural = getEntityPlural(entityType);
|
|
2627
|
+
var base = apiBase();
|
|
2628
|
+
var full = await fetchJson(base + '/' + plural + '/' + encodeURIComponent(item.id));
|
|
2629
|
+
if (full && full.content !== undefined) {
|
|
2630
|
+
panelState.item.content = full.content;
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
renderPanel();
|
|
2600
2634
|
}
|
|
2601
2635
|
|
|
2602
2636
|
function closePanel() {
|
|
2603
2637
|
if (panelState.editor) { destroyEditor(panelState.editor); panelState.editor = null; }
|
|
2604
|
-
panelState = { open: false, entityType: null, item: null, isCreate: false, editor: null };
|
|
2605
|
-
document.getElementById('detail-panel').classList.remove('open');
|
|
2638
|
+
panelState = { open: false, entityType: null, item: null, isCreate: false, editor: null, expanded: false };
|
|
2639
|
+
document.getElementById('detail-panel').classList.remove('open', 'expanded');
|
|
2606
2640
|
document.getElementById('panel-overlay').classList.remove('open');
|
|
2607
2641
|
}
|
|
2608
2642
|
|
|
2643
|
+
function togglePanelExpand() {
|
|
2644
|
+
panelState.expanded = !panelState.expanded;
|
|
2645
|
+
document.getElementById('detail-panel').classList.toggle('expanded', panelState.expanded);
|
|
2646
|
+
var btn = document.getElementById('panel-expand-btn');
|
|
2647
|
+
if (btn) {
|
|
2648
|
+
btn.innerHTML = panelState.expanded
|
|
2649
|
+
? '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M14 6h-4V2M2 10h4v4M10 6l4.5-4.5M6 10L1.5 14.5"/></svg>'
|
|
2650
|
+
: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M10 2h4v4M6 14H2v-4M14 2L9.5 6.5M2 14l4.5-4.5"/></svg>';
|
|
2651
|
+
btn.title = panelState.expanded ? 'Collapse' : 'Expand';
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2609
2655
|
function openCreateDialog(entityType) {
|
|
2610
2656
|
var fields = getEntityFields(entityType);
|
|
2611
2657
|
var item = { _isNew: true, title: '' };
|
|
@@ -2630,7 +2676,7 @@ function openCreateDialog(entityType) {
|
|
|
2630
2676
|
}
|
|
2631
2677
|
}
|
|
2632
2678
|
|
|
2633
|
-
panelState = { open: true, entityType: entityType, item: item, isCreate: true, editor: null };
|
|
2679
|
+
panelState = { open: true, entityType: entityType, item: item, isCreate: true, editor: null, expanded: false };
|
|
2634
2680
|
renderPanel();
|
|
2635
2681
|
document.getElementById('detail-panel').classList.add('open');
|
|
2636
2682
|
document.getElementById('panel-overlay').classList.add('open');
|
|
@@ -2657,6 +2703,10 @@ function renderPanel() {
|
|
|
2657
2703
|
html += '<span class="pill" style="background:' + item._sourceColor + '20;color:' + item._sourceColor + ';font-size:10px">' + escHtml(item._sourceLabel || item._source) + '</span>';
|
|
2658
2704
|
}
|
|
2659
2705
|
|
|
2706
|
+
var expandIcon = panelState.expanded
|
|
2707
|
+
? '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M14 6h-4V2M2 10h4v4M10 6l4.5-4.5M6 10L1.5 14.5"/></svg>'
|
|
2708
|
+
: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M10 2h4v4M6 14H2v-4M14 2L9.5 6.5M2 14l4.5-4.5"/></svg>';
|
|
2709
|
+
html += '<button class="panel-expand" id="panel-expand-btn" title="' + (panelState.expanded ? 'Collapse' : 'Expand') + '">' + expandIcon + '</button>';
|
|
2660
2710
|
html += '<button class="panel-close" id="panel-close-btn">×</button></div>';
|
|
2661
2711
|
|
|
2662
2712
|
// ── Title ──
|
|
@@ -2743,6 +2793,7 @@ function renderPanel() {
|
|
|
2743
2793
|
|
|
2744
2794
|
// Event listeners
|
|
2745
2795
|
document.getElementById('panel-close-btn').addEventListener('click', closePanel);
|
|
2796
|
+
document.getElementById('panel-expand-btn').addEventListener('click', togglePanelExpand);
|
|
2746
2797
|
document.getElementById('panel-cancel-btn').addEventListener('click', closePanel);
|
|
2747
2798
|
var saveBtn = document.getElementById('panel-save-btn');
|
|
2748
2799
|
if (saveBtn) saveBtn.addEventListener('click', isCreate ? saveCreatePanel : savePanel);
|
|
@@ -2773,6 +2824,38 @@ function renderPanel() {
|
|
|
2773
2824
|
});
|
|
2774
2825
|
}
|
|
2775
2826
|
|
|
2827
|
+
// Longtext expand/collapse
|
|
2828
|
+
panel.querySelectorAll('.prop-longtext-preview').forEach(function(prev) {
|
|
2829
|
+
prev.addEventListener('click', function() {
|
|
2830
|
+
var wrap = prev.parentNode;
|
|
2831
|
+
var textarea = wrap.querySelector('.prop-longtext-editor');
|
|
2832
|
+
var toggle = prev.querySelector('.prop-longtext-toggle');
|
|
2833
|
+
if (textarea.style.display === 'none') {
|
|
2834
|
+
textarea.style.display = '';
|
|
2835
|
+
prev.style.display = 'none';
|
|
2836
|
+
textarea.style.height = 'auto';
|
|
2837
|
+
textarea.style.height = Math.max(80, textarea.scrollHeight) + 'px';
|
|
2838
|
+
textarea.focus();
|
|
2839
|
+
}
|
|
2840
|
+
});
|
|
2841
|
+
});
|
|
2842
|
+
panel.querySelectorAll('.prop-longtext-editor').forEach(function(ta) {
|
|
2843
|
+
ta.addEventListener('blur', function() {
|
|
2844
|
+
var wrap = ta.parentNode;
|
|
2845
|
+
var prev = wrap.querySelector('.prop-longtext-preview');
|
|
2846
|
+
var textSpan = prev.querySelector('.prop-longtext-text');
|
|
2847
|
+
var val = ta.value || '';
|
|
2848
|
+
var truncated = val.length > 80 ? val.substring(0, 80) + '...' : (val || 'Empty');
|
|
2849
|
+
textSpan.textContent = truncated;
|
|
2850
|
+
ta.style.display = 'none';
|
|
2851
|
+
prev.style.display = '';
|
|
2852
|
+
});
|
|
2853
|
+
ta.addEventListener('input', function() {
|
|
2854
|
+
ta.style.height = 'auto';
|
|
2855
|
+
ta.style.height = Math.max(80, ta.scrollHeight) + 'px';
|
|
2856
|
+
});
|
|
2857
|
+
});
|
|
2858
|
+
|
|
2776
2859
|
// Live icon updates for enum fields
|
|
2777
2860
|
panel.querySelectorAll('select[id^="p-"]').forEach(function(sel) {
|
|
2778
2861
|
sel.addEventListener('change', function() {
|
|
@@ -2827,6 +2910,18 @@ function renderPropRow(name, field, value, entityType, isReadonly) {
|
|
|
2827
2910
|
html += '<input type="text" id="p-' + name + '" value="' + escHtml(value || '') + '" placeholder=""' + (isReadonly ? ' disabled' : '') + '>';
|
|
2828
2911
|
break;
|
|
2829
2912
|
|
|
2913
|
+
case 'longtext':
|
|
2914
|
+
var ltVal = value || '';
|
|
2915
|
+
var ltTruncated = ltVal.length > 80 ? ltVal.substring(0, 80) + '...' : ltVal;
|
|
2916
|
+
html += '<div class="prop-longtext" id="p-lt-wrap-' + name + '">';
|
|
2917
|
+
html += '<div class="prop-longtext-preview" id="p-lt-preview-' + name + '" title="Click to expand">';
|
|
2918
|
+
html += '<span class="prop-longtext-text">' + escHtml(ltTruncated || 'Empty') + '</span>';
|
|
2919
|
+
html += '<span class="prop-longtext-toggle">▼</span>';
|
|
2920
|
+
html += '</div>';
|
|
2921
|
+
html += '<textarea id="p-' + name + '" class="prop-longtext-editor" style="display:none" placeholder="Enter text..."' + (isReadonly ? ' disabled' : '') + '>' + escHtml(ltVal) + '</textarea>';
|
|
2922
|
+
html += '</div>';
|
|
2923
|
+
break;
|
|
2924
|
+
|
|
2830
2925
|
case 'list':
|
|
2831
2926
|
var listVal = Array.isArray(value) ? value.join(', ') : (value || '');
|
|
2832
2927
|
html += '<input type="text" id="p-' + name + '" value="' + escHtml(listVal) + '" placeholder="Comma-separated"' + (isReadonly ? ' disabled' : '') + '>';
|
|
@@ -2944,6 +3039,8 @@ function readFieldValue(el, fieldDef) {
|
|
|
2944
3039
|
return el.value.trim() || null;
|
|
2945
3040
|
case 'date':
|
|
2946
3041
|
return el.value || null;
|
|
3042
|
+
case 'longtext':
|
|
3043
|
+
return el.value || '';
|
|
2947
3044
|
case 'enum':
|
|
2948
3045
|
case 'string':
|
|
2949
3046
|
case 'text':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mdboard",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Git-based project management dashboard. Reads markdown files with YAML frontmatter and serves a visual kanban board, table, milestones, and metrics views.",
|
|
5
5
|
"main": "./src/server/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
],
|
|
49
49
|
"default": "big"
|
|
50
50
|
},
|
|
51
|
-
"problem": { "type": "
|
|
52
|
-
"solution": { "type": "
|
|
51
|
+
"problem": { "type": "longtext", "label": "Problem" },
|
|
52
|
+
"solution": { "type": "longtext", "label": "Solution" },
|
|
53
53
|
"no_gos": { "type": "list", "label": "No-gos" }
|
|
54
54
|
}
|
|
55
55
|
},
|
|
@@ -115,8 +115,8 @@
|
|
|
115
115
|
],
|
|
116
116
|
"default": "big"
|
|
117
117
|
},
|
|
118
|
-
"problem": { "type": "
|
|
119
|
-
"solution": { "type": "
|
|
118
|
+
"problem": { "type": "longtext", "label": "Problem" },
|
|
119
|
+
"solution": { "type": "longtext", "label": "Solution" },
|
|
120
120
|
"no_gos": { "type": "list", "label": "No-gos" }
|
|
121
121
|
}
|
|
122
122
|
},
|
package/src/core/yaml.js
CHANGED
|
@@ -58,8 +58,21 @@ function parseYaml(text) {
|
|
|
58
58
|
continue;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
// Check for continuation lines (multiline plain scalar)
|
|
62
|
+
let fullValue = rawValue;
|
|
63
|
+
let j = i + 1;
|
|
64
|
+
while (j < lines.length) {
|
|
65
|
+
const nextLine = lines[j];
|
|
66
|
+
if (nextLine.match(/^\s/) && nextLine.trim() !== '' &&
|
|
67
|
+
!nextLine.match(/^\s+-\s/) && !nextLine.match(/^\s+\w[\w.-]*\s*:/)) {
|
|
68
|
+
fullValue += ' ' + nextLine.trim();
|
|
69
|
+
j++;
|
|
70
|
+
} else {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
obj[key] = parseValue(fullValue);
|
|
75
|
+
i = j;
|
|
63
76
|
}
|
|
64
77
|
|
|
65
78
|
return obj;
|
|
@@ -99,6 +112,29 @@ function serializeValue(v) {
|
|
|
99
112
|
return String(v);
|
|
100
113
|
}
|
|
101
114
|
|
|
115
|
+
function wrapPlainScalar(text) {
|
|
116
|
+
// Normalize newlines to spaces
|
|
117
|
+
const normalized = text.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
|
|
118
|
+
const words = normalized.split(' ');
|
|
119
|
+
const lines = [];
|
|
120
|
+
let currentLine = '';
|
|
121
|
+
|
|
122
|
+
for (const word of words) {
|
|
123
|
+
if (currentLine === '') {
|
|
124
|
+
currentLine = word;
|
|
125
|
+
} else if (currentLine.length + 1 + word.length > 78) {
|
|
126
|
+
lines.push(currentLine);
|
|
127
|
+
currentLine = word;
|
|
128
|
+
} else {
|
|
129
|
+
currentLine += ' ' + word;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (currentLine) lines.push(currentLine);
|
|
133
|
+
|
|
134
|
+
// First line is bare, continuation lines indented with 2 spaces
|
|
135
|
+
return lines.map((l, i) => (i === 0 ? l : ' ' + l)).join('\n');
|
|
136
|
+
}
|
|
137
|
+
|
|
102
138
|
function serializeYaml(obj) {
|
|
103
139
|
const lines = [];
|
|
104
140
|
for (const [key, value] of Object.entries(obj)) {
|
|
@@ -124,7 +160,11 @@ function serializeYaml(obj) {
|
|
|
124
160
|
}
|
|
125
161
|
}
|
|
126
162
|
} else {
|
|
127
|
-
|
|
163
|
+
if (typeof value === 'string' && (value.length > 80 || value.indexOf('\n') !== -1)) {
|
|
164
|
+
lines.push(key + ': ' + wrapPlainScalar(value));
|
|
165
|
+
} else {
|
|
166
|
+
lines.push(key + ': ' + serializeValue(value));
|
|
167
|
+
}
|
|
128
168
|
}
|
|
129
169
|
}
|
|
130
170
|
return lines.join('\n');
|