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 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-close{margin-left:auto;background:none;border:none;color:var(--text2);cursor:pointer;font-size:18px;padding:4px 8px;border-radius:var(--radius-sm);line-height:1}
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">&times;</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">&#9660;</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.0.0",
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": "text", "label": "Problem" },
52
- "solution": { "type": "text", "label": "Solution" },
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": "text", "label": "Problem" },
119
- "solution": { "type": "text", "label": "Solution" },
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
- obj[key] = parseValue(rawValue);
62
- i++;
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
- lines.push(key + ': ' + serializeValue(value));
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');