mdboard 1.1.0 → 1.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/index.html CHANGED
@@ -25,9 +25,23 @@ a{color:var(--accent);text-decoration:none}
25
25
 
26
26
  /* ── Layout ──────────────────────────────────────────────── */
27
27
  .app{display:flex;height:100vh;overflow:hidden}
28
+ .source-rail{width:56px;background:var(--bg);border-right:1px solid var(--border);display:flex;flex-direction:column;align-items:center;padding:12px 0;gap:8px;flex-shrink:0;overflow-y:auto}
29
+ .source-rail:empty{display:none}
30
+ .rail-icon{width:40px;height:40px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:18px;cursor:pointer;transition:all .15s;position:relative;color:var(--text2);background:var(--surface2);border:2px solid transparent;flex-shrink:0}
31
+ .rail-icon:hover{border-radius:10px;background:var(--surface);color:var(--text)}
32
+ .rail-icon.active{border-color:var(--accent);border-radius:10px;color:var(--text)}
33
+ .rail-icon.active::before{content:'';position:absolute;left:-14px;top:50%;transform:translateY(-50%);width:4px;height:24px;border-radius:0 4px 4px 0;background:var(--accent)}
34
+ .rail-icon img{width:24px;height:24px;border-radius:4px;object-fit:contain}
35
+ .rail-icon svg{width:20px;height:20px}
36
+ .rail-divider{width:24px;height:2px;background:var(--border);border-radius:1px;flex-shrink:0}
37
+ .rail-icon[data-tooltip]{position:relative}
38
+ .rail-icon[data-tooltip]:hover::after{content:attr(data-tooltip);position:absolute;left:calc(100% + 10px);top:50%;transform:translateY(-50%);background:var(--surface);border:1px solid var(--border);color:var(--text);padding:4px 10px;border-radius:var(--radius-sm);font-size:12px;font-weight:500;white-space:nowrap;z-index:50;pointer-events:none;box-shadow:0 4px 16px rgba(0,0,0,.3)}
28
39
  .sidebar{width:var(--sidebar-w);background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0}
29
- .sidebar-logo{padding:20px 16px 16px;font-family:var(--mono);font-size:15px;font-weight:700;color:var(--text);letter-spacing:-.02em}
30
- .sidebar-logo span{color:var(--accent)}
40
+ .sidebar-logo{padding:20px 16px 16px;font-size:14px;font-weight:700;color:var(--text);cursor:pointer;display:flex;align-items:center;gap:10px;transition:opacity .15s}
41
+ .sidebar-logo:hover{opacity:.8}
42
+ .sidebar-logo-icon{width:28px;height:28px;border-radius:var(--radius-sm);display:flex;align-items:center;justify-content:center;font-size:16px;color:var(--accent);background:var(--accent-dim);flex-shrink:0}
43
+ .sidebar-logo img{width:28px;height:28px;border-radius:var(--radius-sm);object-fit:contain}
44
+ .sidebar-logo-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
31
45
  #sidebar-nav{flex:1;padding:8px}
32
46
  #sidebar-nav a{display:flex;align-items:center;gap:10px;padding:8px 12px;border-radius:var(--radius-sm);color:var(--text2);font-size:13px;font-weight:500;transition:all .15s;border-left:2px solid transparent;margin-bottom:2px}
33
47
  #sidebar-nav a:hover{color:var(--text);background:var(--surface2)}
@@ -39,9 +53,6 @@ a{color:var(--accent);text-decoration:none}
39
53
 
40
54
  /* ── Header ──────────────────────────────────────────────── */
41
55
  .header{padding:16px 24px;border-bottom:1px solid var(--border);background:var(--surface);display:flex;align-items:center;gap:24px;flex-wrap:wrap}
42
- .header-title{flex-shrink:0}
43
- .header-title h1{font-size:18px;font-weight:700;line-height:1.3}
44
- .header-title p{font-size:12px;color:var(--text2);margin-top:2px}
45
56
  .header-section{display:flex;flex-direction:column;gap:4px;min-width:120px}
46
57
  .header-section-label{font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--text3);font-weight:600}
47
58
  .header-section-value{font-size:13px;font-weight:600}
@@ -60,6 +71,8 @@ a{color:var(--accent);text-decoration:none}
60
71
  .content{flex:1;overflow-y:auto;padding:20px 24px}
61
72
  .view{display:none}
62
73
  .view.active{display:block}
74
+ #view-notes.active{display:flex;flex-direction:column;margin:-20px -24px;height:calc(100% + 40px)}
75
+ #view-notes #notes-container{flex:1;overflow:hidden}
63
76
 
64
77
  /* ── Icons ───────────────────────────────────────────────── */
65
78
  .icon{width:16px;height:16px;flex-shrink:0;vertical-align:middle;display:inline-block}
@@ -163,26 +176,60 @@ th.sorted .sort-arrow{opacity:1;color:var(--accent)}
163
176
  .health-label{flex:1;font-size:13px;color:var(--text2)}
164
177
  .health-val{font-family:var(--mono);font-size:13px;font-weight:600}
165
178
 
166
- /* ── Detail Panel ────────────────────────────────────────── */
179
+ /* ── Detail Panel Notion-style ─────────────────────────── */
167
180
  .panel-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:99;opacity:0;pointer-events:none;transition:opacity .2s}
168
181
  .panel-overlay.open{opacity:1;pointer-events:auto}
169
- .detail-panel{position:fixed;top:0;right:0;width:520px;max-width:92vw;height:100vh;background:var(--surface);border-left:1px solid var(--border);z-index:100;transform:translateX(100%);transition:transform .25s ease;display:flex;flex-direction:column;overflow:hidden}
182
+ .detail-panel{position:fixed;top:0;right:0;width:600px;max-width:92vw;height:100vh;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}
170
183
  .detail-panel.open{transform:translateX(0)}
171
- .panel-header{padding:16px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;flex-shrink:0}
184
+ .panel-header{padding:12px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;flex-shrink:0;background:var(--surface)}
172
185
  .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}
173
186
  .panel-item-id{font-family:var(--mono);font-size:13px;color:var(--text2)}
174
187
  .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}
175
188
  .panel-close:hover{background:var(--surface2);color:var(--text)}
176
- .panel-body{flex:1;overflow-y:auto;padding:20px}
177
- .panel-field{margin-bottom:16px}
178
- .panel-field label{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--text3);font-weight:600;margin-bottom:6px}
179
- .panel-field input,.panel-field select,.panel-field textarea{width:100%;background:var(--bg);border:1px solid var(--border);color:var(--text);padding:8px 12px;border-radius:var(--radius-sm);font-family:var(--font);font-size:13px}
180
- .panel-field input:focus,.panel-field select:focus,.panel-field textarea:focus{outline:none;border-color:var(--accent)}
181
- .panel-field textarea{min-height:120px;resize:vertical;font-family:var(--mono);font-size:12px;line-height:1.6}
182
- .panel-field .field-with-icon{display:flex;align-items:center;gap:8px}
183
- .panel-field .field-with-icon select{flex:1}
184
- .panel-props{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px}
185
- .panel-footer{padding:16px 20px;border-top:1px solid var(--border);display:flex;gap:8px;justify-content:flex-end;flex-shrink:0}
189
+
190
+ /* Panel title — large Notion-style */
191
+ .panel-title-wrap{padding:20px 24px 8px;flex-shrink:0}
192
+ .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}
193
+ .panel-title-input::placeholder{color:var(--text3)}
194
+ .panel-title-input:disabled{opacity:.7}
195
+
196
+ /* Panel properties — compact Notion-style rows */
197
+ .panel-properties{padding:4px 24px 8px;flex-shrink:0;border-bottom:1px solid var(--border)}
198
+ .prop-row{display:flex;align-items:center;padding:4px 0;min-height:32px}
199
+ .prop-label{width:100px;flex-shrink:0;font-size:12px;color:var(--text3);font-weight:500}
200
+ .prop-value{flex:1;display:flex;align-items:center;gap:6px}
201
+ .prop-value select,.prop-value input[type="text"],.prop-value input[type="number"],.prop-value input[type="date"]{background:transparent;border:1px solid transparent;color:var(--text);padding:4px 8px;border-radius:var(--radius-sm);font-family:var(--font);font-size:13px;width:100%}
202
+ .prop-value select:hover,.prop-value input:hover{border-color:var(--border)}
203
+ .prop-value select:focus,.prop-value input:focus{outline:none;border-color:var(--accent);background:var(--surface)}
204
+ .prop-value select:disabled,.prop-value input:disabled{opacity:.6;cursor:default}
205
+ .prop-value select:disabled:hover,.prop-value input:disabled:hover{border-color:transparent}
206
+ .prop-text{font-size:13px;color:var(--text2);padding:4px 8px}
207
+
208
+ /* AI Tag Input */
209
+ .prop-row-divider{margin-top:4px;border-top:1px solid var(--border);padding-top:8px}
210
+ .prop-row-toggle{cursor:pointer;user-select:none}
211
+ .prop-row-toggle:hover{background:var(--surface2);border-radius:var(--radius-sm)}
212
+ .ai-toggle-arrow{display:inline-block;font-size:10px;width:14px;transition:transform .15s}
213
+ .ai-tag-input{flex:1;display:flex;flex-wrap:wrap;align-items:center;gap:4px;padding:2px 4px;min-height:28px;border:1px solid transparent;border-radius:var(--radius-sm);cursor:text;position:relative;transition:border-color .15s}
214
+ .ai-tag-input:hover{border-color:var(--border)}
215
+ .ai-tag-input:focus-within{border-color:var(--accent);background:var(--surface)}
216
+ .ai-tag{display:inline-flex;align-items:center;gap:2px;padding:1px 8px;border-radius:10px;font-size:11px;font-weight:500;white-space:nowrap;background:var(--surface2);color:var(--text2);border:1px solid var(--border)}
217
+ .ai-tag-own{background:var(--accent-dim);color:var(--accent);border-color:var(--accent)}
218
+ .ai-tag-inherited{border-style:dashed;opacity:.6}
219
+ .ai-tag-remove{background:none;border:none;color:inherit;cursor:pointer;font-size:13px;line-height:1;padding:0 0 0 2px;opacity:.6}
220
+ .ai-tag-remove:hover{opacity:1}
221
+ .ai-tag-text{border:none!important;background:transparent!important;padding:2px 4px!important;font-size:12px;min-width:60px;flex:1;outline:none;color:var(--text)}
222
+ .ai-tag-text::placeholder{color:var(--text3);font-size:11px}
223
+ .ai-autocomplete{position:absolute;top:100%;left:0;right:0;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);box-shadow:0 8px 24px rgba(0,0,0,.4);z-index:50;max-height:160px;overflow-y:auto;margin-top:2px;display:none}
224
+ .ai-autocomplete.open{display:block}
225
+ .ai-autocomplete-item{padding:6px 10px;font-size:12px;color:var(--text2);cursor:pointer;transition:background .1s}
226
+ .ai-autocomplete-item:hover,.ai-autocomplete-item.active{background:var(--accent-dim);color:var(--text)}
227
+
228
+ /* Panel content area — fills remaining space */
229
+ .panel-content-area{flex:1;overflow-y:auto;padding:0}
230
+ .panel-editor-zone{padding:16px 24px;min-height:200px}
231
+
232
+ .panel-footer{padding:12px 20px;border-top:1px solid var(--border);display:flex;gap:8px;align-items:center;flex-shrink:0}
186
233
  .btn{padding:8px 16px;border-radius:var(--radius-sm);font-size:13px;font-weight:500;cursor:pointer;border:1px solid var(--border);background:var(--surface2);color:var(--text);transition:all .15s;font-family:var(--font)}
187
234
  .btn:hover{background:var(--border)}
188
235
  .btn-primary{background:var(--accent);border-color:var(--accent);color:#fff}
@@ -207,31 +254,244 @@ th.sorted .sort-arrow{opacity:1;color:var(--accent)}
207
254
  .loading-container{padding:20px}
208
255
  @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
209
256
 
257
+ /* ── Workspace: Source Icon (reused in header chips) ──────── */
258
+ .source-icon{width:20px;height:20px;border-radius:var(--radius-sm);display:flex;align-items:center;justify-content:center;font-size:12px;flex-shrink:0;font-weight:700;line-height:1}
259
+
260
+ /* ── Link Chips ──────────────────────────────────────────── */
261
+ .link-chip{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:500;cursor:pointer;border:1px solid var(--border);background:var(--surface2);color:var(--text2);transition:all .15s;white-space:nowrap}
262
+ .link-chip:hover{background:var(--border);color:var(--text)}
263
+ .link-chip-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
264
+ .link-chips{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}
265
+
266
+ /* ── CRUD Buttons ────────────────────────────────────────── */
267
+ .btn-sm{padding:5px 12px;font-size:12px;border-radius:var(--radius-sm)}
268
+ .btn-create{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:600}
269
+ .btn-create:hover{opacity:.9}
270
+ .card-readonly{opacity:.75;cursor:default}
271
+ .card-readonly:hover{border-color:var(--border);background:var(--bg)}
272
+
273
+ /* ── Overview: Tracked Milestones ────────────────────────── */
274
+ .tracked-ms{margin-top:12px;padding:12px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm)}
275
+ .tracked-ms-header{font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--text3);font-weight:600;margin-bottom:8px}
276
+ .tracked-ms-item{display:flex;align-items:center;gap:8px;padding:4px 0}
277
+ .tracked-ms-item .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
278
+ .tracked-ms-item .progress{flex:1;max-width:120px}
279
+ .tracked-ms-item span{font-size:12px}
280
+ .tracked-ms-pct{font-family:var(--mono);font-weight:600;font-size:12px;min-width:36px;text-align:right}
281
+
282
+ /* ── Notes View — Notion-style ───────────────────────────── */
283
+ .notes-layout{display:flex;height:100%;gap:0}
284
+ .notes-sidebar{width:260px;flex-shrink:0;border-right:1px solid var(--border);display:flex;flex-direction:column;background:var(--surface)}
285
+ .notes-toolbar{padding:10px 12px;display:flex;gap:8px;border-bottom:1px solid var(--border)}
286
+ .notes-toolbar input{flex:1;background:var(--bg);border:1px solid var(--border);color:var(--text);padding:6px 10px;border-radius:var(--radius-sm);font-size:12px;font-family:var(--font)}
287
+ .notes-toolbar input:focus{outline:none;border-color:var(--accent)}
288
+ .notes-list{flex:1;overflow-y:auto}
289
+ .notes-list-item{padding:10px 16px;border-bottom:1px solid var(--border);cursor:pointer;transition:background .15s}
290
+ .notes-list-item:hover{background:var(--surface2)}
291
+ .notes-list-item.active{background:var(--accent-dim);border-left:3px solid var(--accent)}
292
+ .notes-list-title{font-size:13px;font-weight:600;margin-bottom:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
293
+ .notes-list-date{font-size:11px;color:var(--text3);font-family:var(--mono)}
294
+ .notes-list-empty{padding:40px 20px;text-align:center;color:var(--text3);font-size:13px}
295
+ .notes-editor{flex:1;display:flex;flex-direction:column;overflow:hidden;background:var(--bg)}
296
+ .notes-header-bar{padding:32px 48px 0;flex-shrink:0}
297
+ .notes-title-input{width:100%;font-size:32px;font-weight:700;background:transparent;border:none;color:var(--text);font-family:var(--font);outline:none;padding:0;margin-bottom:4px;line-height:1.3}
298
+ .notes-title-input::placeholder{color:var(--text3)}
299
+ .notes-meta{font-size:11px;color:var(--text3);font-family:var(--mono)}
300
+ .notes-editorjs-wrap{flex:1;overflow-y:auto;padding:16px 40px 40px}
301
+ .notes-editor-footer{padding:10px 24px;border-top:1px solid var(--border);display:flex;align-items:center;gap:8px;flex-shrink:0;background:var(--surface)}
302
+ .notes-empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--text3);font-size:14px}
303
+ .notes-empty-state kbd{display:inline-block;padding:2px 6px;background:var(--surface2);border:1px solid var(--border);border-radius:3px;font-family:var(--mono);font-size:12px}
304
+ .notes-editor-loading{display:flex;align-items:center;justify-content:center;height:100%;color:var(--text3)}
305
+
306
+ /* ── Editor.js — Dark theme overrides ────────────────────── */
307
+ .codex-editor{color:var(--text);font-family:var(--font)}
308
+ .codex-editor .ce-block__content{max-width:100%;margin:0}
309
+ .codex-editor .ce-toolbar__content{max-width:100%}
310
+ .codex-editor .ce-toolbar__plus{color:var(--text3);background:transparent;border:1px solid var(--border);width:26px;height:26px}
311
+ .codex-editor .ce-toolbar__plus:hover{background:var(--surface2);color:var(--text)}
312
+ .codex-editor .ce-toolbar__plus svg{color:inherit}
313
+ .codex-editor .ce-toolbar__settings-btn{color:var(--text3);background:transparent;width:26px;height:26px}
314
+ .codex-editor .ce-toolbar__settings-btn:hover{background:var(--surface2);color:var(--text)}
315
+ .codex-editor .ce-toolbar__actions{background:transparent}
316
+ /* Inline toolbar */
317
+ .codex-editor .ce-inline-toolbar{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);box-shadow:0 8px 24px rgba(0,0,0,.4)}
318
+ .codex-editor .ce-inline-toolbar__buttons{color:var(--text)}
319
+ .codex-editor .ce-inline-tool{color:var(--text2)}
320
+ .codex-editor .ce-inline-tool:hover{color:var(--text);background:var(--surface2)}
321
+ .codex-editor .ce-inline-tool--active{color:var(--accent)}
322
+ .codex-editor .ce-inline-tool-input{background:var(--bg);color:var(--text);border:none;border-top:1px solid var(--border)}
323
+ /* Conversion toolbar */
324
+ .codex-editor .ce-conversion-toolbar{background:var(--surface);border:1px solid var(--border);box-shadow:0 8px 24px rgba(0,0,0,.4)}
325
+ .codex-editor .ce-conversion-tool{color:var(--text2)}
326
+ .codex-editor .ce-conversion-tool:hover{background:var(--surface2);color:var(--text)}
327
+ .codex-editor .ce-conversion-tool--active{color:var(--accent)}
328
+ .codex-editor .ce-conversion-tool__icon{background:var(--surface2);color:var(--text2)}
329
+ /* Settings panel */
330
+ .codex-editor .ce-settings{background:var(--surface);border:1px solid var(--border);box-shadow:0 8px 24px rgba(0,0,0,.4)}
331
+ .codex-editor .cdx-settings-button{color:var(--text2)}
332
+ .codex-editor .cdx-settings-button:hover{background:var(--surface2);color:var(--text)}
333
+ .codex-editor .cdx-settings-button--active{color:var(--accent)}
334
+ /* Popover (slash commands menu) */
335
+ .ce-popover{background:var(--surface)!important;border:1px solid var(--border)!important;box-shadow:0 8px 24px rgba(0,0,0,.4)!important;color:var(--text)}
336
+ .ce-popover__container{background:var(--surface)!important;border:1px solid var(--border)!important;box-shadow:0 8px 24px rgba(0,0,0,.4)!important}
337
+ .ce-popover-item{color:var(--text2)!important}
338
+ .ce-popover-item:hover{background:var(--surface2)!important;color:var(--text)!important}
339
+ .ce-popover-item--focused{background:var(--surface2)!important;color:var(--text)!important}
340
+ .ce-popover-item__icon{background:var(--surface2)!important;color:var(--text2)!important;border:none!important}
341
+ .ce-popover-item:hover .ce-popover-item__icon{color:var(--text)!important}
342
+ .ce-popover-item__title{color:var(--text)!important}
343
+ .ce-popover-item__secondary-title{color:var(--text3)!important}
344
+ .ce-popover__search{background:transparent!important;border-bottom:1px solid var(--border)!important}
345
+ .ce-popover__search input,.cdx-search-field__input{color:var(--text)!important;background:var(--bg)!important;border:1px solid var(--border)!important;border-radius:var(--radius-sm)!important}
346
+ .cdx-search-field{background:transparent!important}
347
+ .ce-popover__nothing-found-message{color:var(--text3)!important}
348
+ .ce-popover__items::-webkit-scrollbar{width:4px}
349
+ .ce-popover__items::-webkit-scrollbar-track{background:transparent}
350
+ .ce-popover__items::-webkit-scrollbar-thumb{background:var(--border2);border-radius:2px}
351
+ .ce-block{padding:3px 0}
352
+ .ce-paragraph{line-height:1.65;font-size:15px}
353
+ .ce-paragraph[data-placeholder]:empty::before{color:var(--text3)!important}
354
+ .ce-header{font-weight:700}
355
+ h1.ce-header{font-size:28px;line-height:1.2;margin:8px 0 2px}
356
+ h2.ce-header{font-size:22px;line-height:1.25;margin:6px 0 2px}
357
+ h3.ce-header{font-size:18px;line-height:1.3;margin:4px 0 2px}
358
+ h4.ce-header{font-size:15px;line-height:1.4}
359
+ .ce-code__textarea{background:var(--surface)!important;color:var(--text)!important;border:none!important;border-radius:var(--radius-sm)!important;font-family:var(--mono)!important;font-size:13px!important;padding:16px!important;line-height:1.5!important}
360
+ .cdx-list{font-size:15px;line-height:1.65}
361
+ .cdx-list__item{color:var(--text)}
362
+ /* Checklist in list v2 */
363
+ .cdx-checklist{font-size:15px}
364
+ .cdx-checklist__item-checkbox{background:var(--surface2);border:2px solid var(--border2);border-radius:3px}
365
+ .cdx-checklist__item--checked .cdx-checklist__item-checkbox{background:var(--accent);border-color:var(--accent)}
366
+ .cdx-checklist__item--checked .cdx-checklist__item-text{color:var(--text2);text-decoration:line-through}
367
+ /* List v2 checklist style */
368
+ .cdx-list--checklist .cdx-list__item-checkbox{background:var(--surface2);border-color:var(--border2)}
369
+ .cdx-list--checklist .cdx-list__item--checked .cdx-list__item-checkbox{background:var(--accent);border-color:var(--accent)}
370
+ .cdx-list--checklist .cdx-list__item--checked .cdx-list__item-content{color:var(--text2);text-decoration:line-through}
371
+ .cdx-quote{border-left:3px solid var(--accent);padding-left:16px;font-style:italic;color:var(--text2);font-size:15px;line-height:1.65}
372
+ .cdx-quote [contenteditable]{outline:none}
373
+ .cdx-quote__caption{display:none!important}
374
+ .ce-delimiter{line-height:1;padding:12px 0;color:var(--text3);letter-spacing:8px}
375
+ .cdx-marker{background:rgba(91,110,245,.2);padding:2px 0}
376
+
377
+ /* Fallback textarea (when CDN not available) */
378
+ .editor-fallback-textarea{width:100%;min-height:200px;resize:vertical;background:transparent;border:none;color:var(--text);padding:0;font-family:var(--font);font-size:15px;line-height:1.65;outline:none}
379
+
380
+ /* ── Markdown Rendered (readonly) ────────────────────────── */
381
+ .md-rendered{font-size:15px;line-height:1.65;color:var(--text)}
382
+ .md-rendered h1,.md-rendered h2,.md-rendered h3{font-weight:700;margin:16px 0 6px}
383
+ .md-rendered h1{font-size:28px;line-height:1.2}
384
+ .md-rendered h2{font-size:22px;line-height:1.25}
385
+ .md-rendered h3{font-size:18px;line-height:1.3}
386
+ .md-rendered p{margin-bottom:8px}
387
+ .md-rendered ul,.md-rendered ol{padding-left:24px;margin-bottom:8px}
388
+ .md-rendered li{margin-bottom:4px}
389
+ .md-rendered code{background:var(--surface2);padding:2px 6px;border-radius:var(--radius-sm);font-family:var(--mono);font-size:13px}
390
+ .md-rendered pre{background:var(--surface);padding:16px;border-radius:var(--radius-sm);overflow-x:auto;margin-bottom:12px}
391
+ .md-rendered pre code{background:transparent;padding:0}
392
+ .md-rendered blockquote{border-left:3px solid var(--accent);padding-left:16px;color:var(--text2);margin:8px 0;font-style:italic}
393
+ .md-rendered hr{border:none;border-top:1px solid var(--border);margin:20px 0}
394
+ .md-rendered a{color:var(--accent)}
395
+ .md-rendered .md-check{display:flex;align-items:center;gap:8px;padding:2px 0}
396
+ .md-rendered .md-check-box{width:16px;height:16px;border:2px solid var(--border2);border-radius:3px;display:inline-flex;align-items:center;justify-content:center;font-size:11px;flex-shrink:0;color:var(--accent)}
397
+ .md-rendered .md-check-box.checked{background:var(--accent);border-color:var(--accent);color:#fff}
398
+
399
+ /* ── Settings Panel ──────────────────────────────────────── */
400
+ .sidebar-settings{padding:8px}
401
+ .sidebar-settings a{display:flex;align-items:center;gap:10px;padding:8px 12px;border-radius:var(--radius-sm);color:var(--text3);font-size:13px;font-weight:500;transition:all .15s;cursor:pointer}
402
+ .sidebar-settings a:hover{color:var(--text);background:var(--surface2)}
403
+ .sidebar-settings svg{width:16px;height:16px;flex-shrink:0}
404
+ .settings-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:199;opacity:0;pointer-events:none;transition:opacity .2s}
405
+ .settings-overlay.open{opacity:1;pointer-events:auto}
406
+ .settings-panel{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%) scale(.95);width:680px;max-width:92vw;max-height:85vh;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);z-index:200;display:flex;flex-direction:column;opacity:0;pointer-events:none;transition:all .2s ease;box-shadow:0 16px 48px rgba(0,0,0,.4)}
407
+ .settings-panel.open{opacity:1;pointer-events:auto;transform:translate(-50%,-50%) scale(1)}
408
+ .settings-header{padding:16px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
409
+ .settings-header h2{font-size:16px;font-weight:700}
410
+ .settings-content{flex:1;overflow-y:auto;padding:20px}
411
+ .settings-section h3{font-size:13px;font-weight:600;color:var(--text2);text-transform:uppercase;letter-spacing:.04em;margin-bottom:8px}
412
+ .settings-theme-info{font-size:12px;color:var(--text3);margin-bottom:12px}
413
+ .theme-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px}
414
+ .theme-card{background:var(--surface);border:2px solid var(--border);border-radius:var(--radius);padding:10px;cursor:pointer;transition:all .15s}
415
+ .theme-card:hover{border-color:var(--border2)}
416
+ .theme-card.active{border-color:var(--accent)}
417
+ .theme-card-preview{display:flex;gap:3px;height:32px;border-radius:var(--radius-sm);overflow:hidden;margin-bottom:8px}
418
+ .theme-card-preview span{flex:1}
419
+ .theme-card-name{font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
420
+ .theme-card-type{font-size:10px;color:var(--text3);text-transform:uppercase;letter-spacing:.03em}
421
+ .settings-footer{padding:14px 20px;border-top:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;flex-shrink:0;gap:12px}
422
+ .toggle-label{display:flex;align-items:center;gap:10px;cursor:pointer;font-size:13px;color:var(--text2);user-select:none}
423
+ .toggle-label input{display:none}
424
+ .toggle-switch{position:relative;width:36px;height:20px;background:var(--surface2);border:1px solid var(--border);border-radius:10px;transition:all .2s;flex-shrink:0}
425
+ .toggle-switch::after{content:'';position:absolute;top:2px;left:2px;width:14px;height:14px;background:var(--text3);border-radius:50%;transition:all .2s}
426
+ .toggle-label input:checked+.toggle-switch{background:var(--accent);border-color:var(--accent)}
427
+ .toggle-label input:checked+.toggle-switch::after{left:18px;background:#fff}
428
+
429
+ /* ── Rail Switch Icon ────────────────────────────────────── */
430
+ .rail-switch{background:transparent;color:var(--text3);border:1px dashed var(--border2)}
431
+ .rail-switch:hover{color:var(--text);border-color:var(--accent);background:var(--accent-dim);border-style:solid}
432
+
433
+ /* ── History Modal ──────────────────────────────────────── */
434
+ .history-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:199;opacity:0;pointer-events:none;transition:opacity .2s}
435
+ .history-overlay.open{opacity:1;pointer-events:auto}
436
+ .history-modal{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%) scale(.95);width:560px;max-width:92vw;max-height:70vh;background:var(--bg);border:1px solid var(--border);border-radius:12px;z-index:200;display:flex;flex-direction:column;opacity:0;pointer-events:none;transition:all .2s ease;box-shadow:0 16px 48px rgba(0,0,0,.5)}
437
+ .history-modal.open{opacity:1;pointer-events:auto;transform:translate(-50%,-50%) scale(1)}
438
+ .history-header{padding:16px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
439
+ .history-header h2{font-size:16px;font-weight:700}
440
+ .history-body{flex:1;overflow-y:auto;padding:8px 0}
441
+ .history-loading,.history-empty{padding:40px 20px;text-align:center;color:var(--text3);font-size:13px}
442
+ .history-item{display:flex;align-items:center;gap:12px;padding:10px 20px;cursor:pointer;transition:background .15s}
443
+ .history-item:hover{background:var(--surface2)}
444
+ .history-item-current{background:var(--accent-dim)}
445
+ .history-item-current:hover{background:var(--accent-dim)}
446
+ .history-item-icon{width:36px;height:36px;border-radius:var(--radius);background:var(--surface2);display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:700;color:var(--accent);flex-shrink:0}
447
+ .history-item-current .history-item-icon{background:var(--accent);color:#fff}
448
+ .history-item-info{flex:1;min-width:0}
449
+ .history-item-name{font-size:13px;font-weight:600;display:flex;align-items:center;gap:8px}
450
+ .history-item-path{font-size:11px;color:var(--text3);font-family:var(--mono);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
451
+ .history-item-time{font-size:11px;color:var(--text3);font-family:var(--mono);flex-shrink:0;white-space:nowrap}
452
+ .history-current-badge{display:inline-block;padding:1px 8px;border-radius:10px;font-size:10px;font-weight:600;background:var(--accent);color:#fff}
453
+
210
454
  /* ── Responsive ──────────────────────────────────────────── */
211
455
  @media(max-width:1024px){
456
+ .source-rail{width:48px;padding:8px 0;gap:6px}
457
+ .rail-icon{width:34px;height:34px;font-size:15px;border-radius:10px}
458
+ .rail-icon.active::before{left:-12px;height:18px}
212
459
  .sidebar{width:56px}
213
- .sidebar-logo span,.sidebar-footer,#sidebar-nav a span{display:none}
214
- .sidebar-logo{padding:16px 12px;text-align:center}
460
+ .sidebar-logo-text,.sidebar-footer,#sidebar-nav a span{display:none}
461
+ .sidebar-logo{padding:16px 12px;justify-content:center}
215
462
  #sidebar-nav a{justify-content:center;padding:10px}
216
463
  .header{padding:12px 16px}
217
464
  .content{padding:16px}
218
465
  .detail-panel{width:100%;max-width:100%}
466
+ #view-notes.active{margin:-16px}
467
+ .notes-sidebar{width:200px}
468
+ .notes-header-bar{padding:24px 24px 0}
469
+ .notes-editorjs-wrap{padding:12px 16px 24px}
219
470
  }
220
471
  @media(max-width:768px){
221
472
  #h-stats{flex-wrap:wrap;gap:12px}
222
473
  .header{gap:12px}
223
474
  .metrics-grid{grid-template-columns:1fr}
224
- .panel-props{grid-template-columns:1fr}
475
+ .notes-sidebar{display:none}
476
+ .notes-header-bar{padding:20px 16px 0}
477
+ .notes-editorjs-wrap{padding:8px 8px 24px}
225
478
  }
226
479
  </style>
227
480
  <style id="dynamic-styles"></style>
481
+ <style id="theme-styles"></style>
228
482
  <link id="custom-theme" rel="stylesheet" href="/mdboard.css">
229
483
  </head>
230
484
  <body>
231
485
  <div class="app">
486
+ <!-- Source Rail (Discord/Slack style) -->
487
+ <nav class="source-rail" id="source-rail"></nav>
488
+
232
489
  <!-- Sidebar -->
233
490
  <aside class="sidebar">
234
- <div class="sidebar-logo"><span>&#9632;</span> mdboard</div>
491
+ <div class="sidebar-logo" id="sidebar-logo">
492
+ <span class="sidebar-logo-icon" id="sidebar-logo-icon">&#9632;</span>
493
+ <span class="sidebar-logo-text" id="sidebar-logo-text">mdboard</span>
494
+ </div>
235
495
  <nav id="sidebar-nav">
236
496
  <a href="#board" data-view="board" class="active">
237
497
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
@@ -249,17 +509,23 @@ th.sorted .sort-arrow{opacity:1;color:var(--accent)}
249
509
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M18 20V10M12 20V4M6 20v-6"/></svg>
250
510
  <span>Metrics</span>
251
511
  </a>
512
+ <a href="#notes" data-view="notes">
513
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
514
+ <span>Notes</span>
515
+ </a>
252
516
  </nav>
517
+ <div class="sidebar-settings" id="sidebar-settings">
518
+ <a href="#" id="settings-toggle">
519
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 01-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
520
+ <span>Settings</span>
521
+ </a>
522
+ </div>
253
523
  <div class="sidebar-footer">mdboard</div>
254
524
  </aside>
255
525
 
256
526
  <div class="main">
257
527
  <!-- Header -->
258
528
  <header class="header">
259
- <div class="header-title">
260
- <h1 id="h-project-name">Loading...</h1>
261
- <p id="h-project-desc"></p>
262
- </div>
263
529
  <div class="header-section" id="h-milestone-wrap" style="display:none">
264
530
  <span class="header-section-label">Milestone</span>
265
531
  <span class="header-section-value" id="h-milestone-name"></span>
@@ -291,6 +557,12 @@ th.sorted .sort-arrow{opacity:1;color:var(--accent)}
291
557
  <div id="view-metrics" class="view">
292
558
  <div id="metrics-container"></div>
293
559
  </div>
560
+ <div id="view-overview" class="view">
561
+ <div id="overview-container"></div>
562
+ </div>
563
+ <div id="view-notes" class="view">
564
+ <div id="notes-container"></div>
565
+ </div>
294
566
  </div>
295
567
  </div>
296
568
  </div>
@@ -299,7 +571,306 @@ th.sorted .sort-arrow{opacity:1;color:var(--accent)}
299
571
  <div id="panel-overlay" class="panel-overlay"></div>
300
572
  <div id="detail-panel" class="detail-panel"></div>
301
573
 
574
+ <!-- Settings Panel -->
575
+ <div id="settings-overlay" class="settings-overlay"></div>
576
+ <div id="settings-panel" class="settings-panel">
577
+ <div class="settings-header">
578
+ <h2>Settings</h2>
579
+ <button class="panel-close" id="settings-close">&times;</button>
580
+ </div>
581
+ <div class="settings-content">
582
+ <div class="settings-section">
583
+ <h3>Theme</h3>
584
+ <p id="settings-theme-info" class="settings-theme-info"></p>
585
+ <div id="theme-grid" class="theme-grid"></div>
586
+ </div>
587
+ </div>
588
+ <div class="settings-footer">
589
+ <label class="toggle-label">
590
+ <input type="checkbox" id="theme-default-toggle">
591
+ <span class="toggle-switch"></span>
592
+ <span>Set as default for all projects</span>
593
+ </label>
594
+ <button class="btn btn-primary" id="settings-save">Save</button>
595
+ </div>
596
+ </div>
597
+
598
+ <!-- History Modal -->
599
+ <div id="history-overlay" class="history-overlay"></div>
600
+ <div id="history-modal" class="history-modal"></div>
601
+
602
+ <script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@2/dist/editorjs.umd.js"></script>
603
+ <script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest/dist/header.umd.js"></script>
604
+ <script src="https://cdn.jsdelivr.net/npm/@editorjs/list@latest/dist/editorjs-list.umd.js"></script>
605
+ <script src="https://cdn.jsdelivr.net/npm/@editorjs/code@latest/dist/code.umd.js"></script>
606
+ <script src="https://cdn.jsdelivr.net/npm/@editorjs/quote@latest/dist/quote.umd.js"></script>
607
+ <script src="https://cdn.jsdelivr.net/npm/@editorjs/delimiter@latest/dist/delimiter.umd.js"></script>
608
+ <script src="https://cdn.jsdelivr.net/npm/@editorjs/marker@latest/dist/marker.umd.js"></script>
609
+ <script src="https://cdn.jsdelivr.net/npm/@editorjs/inline-code@latest/dist/inline-code.umd.js"></script>
302
610
  <script>
611
+ /* ══════════════════════════════════════════════════════════════
612
+ THEMES — Predefined color themes for mdboard
613
+ ══════════════════════════════════════════════════════════════ */
614
+ var THEMES = {
615
+ 'default-dark': {
616
+ name: 'Default Dark', type: 'dark',
617
+ vars: {
618
+ bg:'#0A0A0B',surface:'#141415',surface2:'#1A1A1C',border:'#232326',border2:'#2E2E33',
619
+ text:'#E8E8ED',text2:'#8B8B93',text3:'#5A5A63',
620
+ accent:'#5B6EF5','accent-dim':'rgba(91,110,245,.15)',
621
+ success:'#2EA043','success-dim':'rgba(46,160,67,.15)',
622
+ warning:'#D4A72C','warning-dim':'rgba(212,167,44,.15)',
623
+ danger:'#DA3633','danger-dim':'rgba(218,54,51,.15)',
624
+ purple:'#8B5CF6','purple-dim':'rgba(139,92,246,.15)'
625
+ }
626
+ },
627
+ 'linear-dark': {
628
+ name: 'Linear Dark', type: 'dark',
629
+ vars: {
630
+ bg:'#101012',surface:'#1A1A1F',surface2:'#222228',border:'#2C2C35',border2:'#38384A',
631
+ text:'#E2E2E9',text2:'#8E8E9E',text3:'#5C5C6E',
632
+ accent:'#5E6AD2','accent-dim':'rgba(94,106,210,.15)',
633
+ success:'#4DA771','success-dim':'rgba(77,167,113,.15)',
634
+ warning:'#F2C94C','warning-dim':'rgba(242,201,76,.15)',
635
+ danger:'#EB5757','danger-dim':'rgba(235,87,87,.15)',
636
+ purple:'#BB87FC','purple-dim':'rgba(187,135,252,.15)'
637
+ }
638
+ },
639
+ 'linear-light': {
640
+ name: 'Linear Light', type: 'light',
641
+ vars: {
642
+ bg:'#F9F9FB',surface:'#FFFFFF',surface2:'#F0F0F4',border:'#E0E0E6',border2:'#D0D0D8',
643
+ text:'#1A1A2E',text2:'#6B6B80',text3:'#9090A0',
644
+ accent:'#5E6AD2','accent-dim':'rgba(94,106,210,.1)',
645
+ success:'#4DA771','success-dim':'rgba(77,167,113,.1)',
646
+ warning:'#D4A017','warning-dim':'rgba(212,160,23,.1)',
647
+ danger:'#EB5757','danger-dim':'rgba(235,87,87,.1)',
648
+ purple:'#9B6DD7','purple-dim':'rgba(155,109,215,.1)'
649
+ }
650
+ },
651
+ 'jira-dark': {
652
+ name: 'Jira Dark', type: 'dark',
653
+ vars: {
654
+ bg:'#0D1117',surface:'#161B22',surface2:'#1C2333',border:'#293040',border2:'#344050',
655
+ text:'#DEE4EC',text2:'#8C96A5',text3:'#5A6577',
656
+ accent:'#2684FF','accent-dim':'rgba(38,132,255,.15)',
657
+ success:'#36B37E','success-dim':'rgba(54,179,126,.15)',
658
+ warning:'#FFAB00','warning-dim':'rgba(255,171,0,.15)',
659
+ danger:'#FF5630','danger-dim':'rgba(255,86,48,.15)',
660
+ purple:'#6554C0','purple-dim':'rgba(101,84,192,.15)'
661
+ }
662
+ },
663
+ 'jira-light': {
664
+ name: 'Jira Light', type: 'light',
665
+ vars: {
666
+ bg:'#F4F5F7',surface:'#FFFFFF',surface2:'#EBECF0',border:'#DFE1E6',border2:'#C1C7D0',
667
+ text:'#172B4D',text2:'#6B778C',text3:'#97A0AF',
668
+ accent:'#0052CC','accent-dim':'rgba(0,82,204,.1)',
669
+ success:'#36B37E','success-dim':'rgba(54,179,126,.1)',
670
+ warning:'#FF991F','warning-dim':'rgba(255,153,31,.1)',
671
+ danger:'#FF5630','danger-dim':'rgba(255,86,48,.1)',
672
+ purple:'#6554C0','purple-dim':'rgba(101,84,192,.1)'
673
+ }
674
+ },
675
+ 'catppuccin-mocha': {
676
+ name: 'Catppuccin Mocha', type: 'dark',
677
+ vars: {
678
+ bg:'#1E1E2E',surface:'#262637',surface2:'#313244',border:'#3B3B52',border2:'#45475A',
679
+ text:'#CDD6F4',text2:'#A6ADC8',text3:'#6C7086',
680
+ accent:'#89B4FA','accent-dim':'rgba(137,180,250,.15)',
681
+ success:'#A6E3A1','success-dim':'rgba(166,227,161,.15)',
682
+ warning:'#F9E2AF','warning-dim':'rgba(249,226,175,.15)',
683
+ danger:'#F38BA8','danger-dim':'rgba(243,139,168,.15)',
684
+ purple:'#CBA6F7','purple-dim':'rgba(203,166,247,.15)'
685
+ }
686
+ },
687
+ 'catppuccin-latte': {
688
+ name: 'Catppuccin Latte', type: 'light',
689
+ vars: {
690
+ bg:'#EFF1F5',surface:'#FFFFFF',surface2:'#E6E9EF',border:'#CCD0DA',border2:'#BCC0CC',
691
+ text:'#4C4F69',text2:'#6C6F85',text3:'#9CA0B0',
692
+ accent:'#1E66F5','accent-dim':'rgba(30,102,245,.1)',
693
+ success:'#40A02B','success-dim':'rgba(64,160,43,.1)',
694
+ warning:'#DF8E1D','warning-dim':'rgba(223,142,29,.1)',
695
+ danger:'#D20F39','danger-dim':'rgba(210,15,57,.1)',
696
+ purple:'#8839EF','purple-dim':'rgba(136,57,239,.1)'
697
+ }
698
+ },
699
+ 'ayu-dark': {
700
+ name: 'Ayu Dark', type: 'dark',
701
+ vars: {
702
+ bg:'#0B0E14',surface:'#0F1219',surface2:'#151920',border:'#1E222A',border2:'#272D38',
703
+ text:'#BFBDB6',text2:'#6C7380',text3:'#4A5060',
704
+ accent:'#E6B450','accent-dim':'rgba(230,180,80,.15)',
705
+ success:'#7FD962','success-dim':'rgba(127,217,98,.15)',
706
+ warning:'#FFB454','warning-dim':'rgba(255,180,84,.15)',
707
+ danger:'#F07178','danger-dim':'rgba(240,113,120,.15)',
708
+ purple:'#D2A6FF','purple-dim':'rgba(210,166,255,.15)'
709
+ }
710
+ },
711
+ 'ayu-light': {
712
+ name: 'Ayu Light', type: 'light',
713
+ vars: {
714
+ bg:'#F8F9FA',surface:'#FFFFFF',surface2:'#F0F1F2',border:'#E0E1E2',border2:'#D4D5D6',
715
+ text:'#5C6166',text2:'#787B80',text3:'#ACB0B5',
716
+ accent:'#FF9940','accent-dim':'rgba(255,153,64,.1)',
717
+ success:'#6CBF43','success-dim':'rgba(108,191,67,.1)',
718
+ warning:'#F2AE49','warning-dim':'rgba(242,174,73,.1)',
719
+ danger:'#F07171','danger-dim':'rgba(240,113,113,.1)',
720
+ purple:'#A37ACC','purple-dim':'rgba(163,122,204,.1)'
721
+ }
722
+ },
723
+ 'tokyo-night': {
724
+ name: 'Tokyo Night', type: 'dark',
725
+ vars: {
726
+ bg:'#1A1B26',surface:'#1F2335',surface2:'#24283B',border:'#2F3449',border2:'#3B4261',
727
+ text:'#C0CAF5',text2:'#7982A9',text3:'#565F89',
728
+ accent:'#7AA2F7','accent-dim':'rgba(122,162,247,.15)',
729
+ success:'#9ECE6A','success-dim':'rgba(158,206,106,.15)',
730
+ warning:'#E0AF68','warning-dim':'rgba(224,175,104,.15)',
731
+ danger:'#F7768E','danger-dim':'rgba(247,118,142,.15)',
732
+ purple:'#BB9AF7','purple-dim':'rgba(187,154,247,.15)'
733
+ }
734
+ },
735
+ 'tokyo-night-storm': {
736
+ name: 'Tokyo Night Storm', type: 'dark',
737
+ vars: {
738
+ bg:'#24283B',surface:'#292E42',surface2:'#2F3549',border:'#3B4261',border2:'#464F78',
739
+ text:'#C0CAF5',text2:'#7982A9',text3:'#565F89',
740
+ accent:'#7AA2F7','accent-dim':'rgba(122,162,247,.15)',
741
+ success:'#9ECE6A','success-dim':'rgba(158,206,106,.15)',
742
+ warning:'#E0AF68','warning-dim':'rgba(224,175,104,.15)',
743
+ danger:'#F7768E','danger-dim':'rgba(247,118,142,.15)',
744
+ purple:'#BB9AF7','purple-dim':'rgba(187,154,247,.15)'
745
+ }
746
+ },
747
+ 'notion-light': {
748
+ name: 'Notion Light', type: 'light',
749
+ vars: {
750
+ bg:'#F7F6F3',surface:'#FFFFFF',surface2:'#EEEEEC',border:'#E3E2DE',border2:'#D4D3CF',
751
+ text:'#37352F',text2:'#6B6B6B',text3:'#9B9A97',
752
+ accent:'#2383E2','accent-dim':'rgba(35,131,226,.1)',
753
+ success:'#0F7B0F','success-dim':'rgba(15,123,15,.1)',
754
+ warning:'#CB7B00','warning-dim':'rgba(203,123,0,.1)',
755
+ danger:'#E03E3E','danger-dim':'rgba(224,62,62,.1)',
756
+ purple:'#6940A5','purple-dim':'rgba(105,64,165,.1)'
757
+ }
758
+ },
759
+ 'notion-dark': {
760
+ name: 'Notion Dark', type: 'dark',
761
+ vars: {
762
+ bg:'#191919',surface:'#202020',surface2:'#2B2B2B',border:'#363636',border2:'#444444',
763
+ text:'#E3E2DE',text2:'#9B9B9B',text3:'#6B6B6B',
764
+ accent:'#529CCA','accent-dim':'rgba(82,156,202,.15)',
765
+ success:'#4DAB9A','success-dim':'rgba(77,171,154,.15)',
766
+ warning:'#CB7B00','warning-dim':'rgba(203,123,0,.15)',
767
+ danger:'#E03E3E','danger-dim':'rgba(224,62,62,.15)',
768
+ purple:'#9A6DD7','purple-dim':'rgba(154,109,215,.15)'
769
+ }
770
+ },
771
+ 'github-dark': {
772
+ name: 'GitHub Dark', type: 'dark',
773
+ vars: {
774
+ bg:'#0D1117',surface:'#161B22',surface2:'#1C2128',border:'#30363D',border2:'#3D444D',
775
+ text:'#E6EDF3',text2:'#8B949E',text3:'#6E7681',
776
+ accent:'#58A6FF','accent-dim':'rgba(88,166,255,.15)',
777
+ success:'#3FB950','success-dim':'rgba(63,185,80,.15)',
778
+ warning:'#D29922','warning-dim':'rgba(210,153,34,.15)',
779
+ danger:'#F85149','danger-dim':'rgba(248,81,73,.15)',
780
+ purple:'#BC8CFF','purple-dim':'rgba(188,140,255,.15)'
781
+ }
782
+ },
783
+ 'github-light': {
784
+ name: 'GitHub Light', type: 'light',
785
+ vars: {
786
+ bg:'#F6F8FA',surface:'#FFFFFF',surface2:'#EAEEF2',border:'#D0D7DE',border2:'#BCC3CB',
787
+ text:'#1F2328',text2:'#656D76',text3:'#8C959F',
788
+ accent:'#0969DA','accent-dim':'rgba(9,105,218,.1)',
789
+ success:'#1A7F37','success-dim':'rgba(26,127,55,.1)',
790
+ warning:'#9A6700','warning-dim':'rgba(154,103,0,.1)',
791
+ danger:'#CF222E','danger-dim':'rgba(207,34,46,.1)',
792
+ purple:'#8250DF','purple-dim':'rgba(130,80,223,.1)'
793
+ }
794
+ },
795
+ 'dracula': {
796
+ name: 'Dracula', type: 'dark',
797
+ vars: {
798
+ bg:'#1E1F29',surface:'#282A36',surface2:'#2E303E',border:'#3A3C4E',border2:'#44475A',
799
+ text:'#F8F8F2',text2:'#ADB0C0',text3:'#6272A4',
800
+ accent:'#BD93F9','accent-dim':'rgba(189,147,249,.15)',
801
+ success:'#50FA7B','success-dim':'rgba(80,250,123,.15)',
802
+ warning:'#F1FA8C','warning-dim':'rgba(241,250,140,.15)',
803
+ danger:'#FF5555','danger-dim':'rgba(255,85,85,.15)',
804
+ purple:'#FF79C6','purple-dim':'rgba(255,121,198,.15)'
805
+ }
806
+ },
807
+ 'nord': {
808
+ name: 'Nord', type: 'dark',
809
+ vars: {
810
+ bg:'#242933',surface:'#2E3440',surface2:'#353C4A',border:'#3B4252',border2:'#434C5E',
811
+ text:'#ECEFF4',text2:'#A5B1C2',text3:'#697688',
812
+ accent:'#88C0D0','accent-dim':'rgba(136,192,208,.15)',
813
+ success:'#A3BE8C','success-dim':'rgba(163,190,140,.15)',
814
+ warning:'#EBCB8B','warning-dim':'rgba(235,203,139,.15)',
815
+ danger:'#BF616A','danger-dim':'rgba(191,97,106,.15)',
816
+ purple:'#B48EAD','purple-dim':'rgba(180,142,173,.15)'
817
+ }
818
+ },
819
+ 'solarized-dark': {
820
+ name: 'Solarized Dark', type: 'dark',
821
+ vars: {
822
+ bg:'#002129',surface:'#002B36',surface2:'#073642',border:'#0A4050',border2:'#1A5060',
823
+ text:'#93A1A1',text2:'#839496',text3:'#586E75',
824
+ accent:'#268BD2','accent-dim':'rgba(38,139,210,.15)',
825
+ success:'#859900','success-dim':'rgba(133,153,0,.15)',
826
+ warning:'#B58900','warning-dim':'rgba(181,137,0,.15)',
827
+ danger:'#DC322F','danger-dim':'rgba(220,50,47,.15)',
828
+ purple:'#6C71C4','purple-dim':'rgba(108,113,196,.15)'
829
+ }
830
+ },
831
+ 'solarized-light': {
832
+ name: 'Solarized Light', type: 'light',
833
+ vars: {
834
+ bg:'#FDF6E3',surface:'#FFFFFF',surface2:'#EEE8D5',border:'#DDD6C1',border2:'#C9C2AD',
835
+ text:'#586E75',text2:'#657B83',text3:'#93A1A1',
836
+ accent:'#268BD2','accent-dim':'rgba(38,139,210,.1)',
837
+ success:'#859900','success-dim':'rgba(133,153,0,.1)',
838
+ warning:'#B58900','warning-dim':'rgba(181,137,0,.1)',
839
+ danger:'#DC322F','danger-dim':'rgba(220,50,47,.1)',
840
+ purple:'#6C71C4','purple-dim':'rgba(108,113,196,.1)'
841
+ }
842
+ },
843
+ 'one-dark': {
844
+ name: 'One Dark', type: 'dark',
845
+ vars: {
846
+ bg:'#1E2127',surface:'#282C34',surface2:'#2C313C',border:'#3A3F4B',border2:'#4B5263',
847
+ text:'#ABB2BF',text2:'#7F848E',text3:'#5C6370',
848
+ accent:'#61AFEF','accent-dim':'rgba(97,175,239,.15)',
849
+ success:'#98C379','success-dim':'rgba(152,195,121,.15)',
850
+ warning:'#E5C07B','warning-dim':'rgba(229,192,123,.15)',
851
+ danger:'#E06C75','danger-dim':'rgba(224,108,117,.15)',
852
+ purple:'#C678DD','purple-dim':'rgba(198,120,221,.15)'
853
+ }
854
+ },
855
+ 'gruvbox-dark': {
856
+ name: 'Gruvbox Dark', type: 'dark',
857
+ vars: {
858
+ bg:'#1D2021',surface:'#282828',surface2:'#32302F',border:'#3C3836',border2:'#504945',
859
+ text:'#EBDBB2',text2:'#A89984',text3:'#7C6F64',
860
+ accent:'#FE8019','accent-dim':'rgba(254,128,25,.15)',
861
+ success:'#B8BB26','success-dim':'rgba(184,187,38,.15)',
862
+ warning:'#FABD2F','warning-dim':'rgba(250,189,47,.15)',
863
+ danger:'#FB4934','danger-dim':'rgba(251,73,52,.15)',
864
+ purple:'#D3869B','purple-dim':'rgba(211,134,155,.15)'
865
+ }
866
+ }
867
+ };
868
+
869
+ var THEME_LIST = Object.keys(THEMES).map(function(id) {
870
+ return { id: id, name: THEMES[id].name, type: THEMES[id].type };
871
+ });
872
+
873
+
303
874
  /* ══════════════════════════════════════════════════════════════
304
875
  ICON SHAPES — SVG generators by icon name
305
876
  ══════════════════════════════════════════════════════════════ */
@@ -371,7 +942,9 @@ function milestoneIcon(status) {
371
942
  ══════════════════════════════════════════════════════════════ */
372
943
  var D = {
373
944
  config: null, project: null, milestones: [], epics: [], tasks: [],
374
- sprints: [], allSprints: [], metrics: null, health: null, loaded: false
945
+ sprints: [], allSprints: [], metrics: null, health: null, loaded: false,
946
+ sources: [], activeSource: null, overviewLinks: null, notes: [],
947
+ aiSuggestions: null, theme: null
375
948
  };
376
949
 
377
950
  /* ── Helpers ─────────────────────────────────────────────── */
@@ -421,7 +994,8 @@ async function fetchJson(url) {
421
994
 
422
995
  async function patchItem(type, id, updates) {
423
996
  try {
424
- var r = await fetch('/api/' + type + '/' + encodeURIComponent(id), {
997
+ var base = apiBase();
998
+ var r = await fetch(base + '/' + type + '/' + encodeURIComponent(id), {
425
999
  method: 'PATCH',
426
1000
  headers: { 'Content-Type': 'application/json' },
427
1001
  body: JSON.stringify(updates),
@@ -440,22 +1014,83 @@ async function patchItem(type, id, updates) {
440
1014
  }
441
1015
  }
442
1016
 
1017
+ /* ── CRUD API helpers ─────────────────────────────────────── */
1018
+ function apiBase() {
1019
+ if (D.activeSource) return '/api/sources/' + encodeURIComponent(D.activeSource);
1020
+ return '/api';
1021
+ }
1022
+
1023
+ async function createItem(collection, data) {
1024
+ try {
1025
+ var url = apiBase() + '/' + collection;
1026
+ var r = await fetch(url, {
1027
+ method: 'POST',
1028
+ headers: { 'Content-Type': 'application/json' },
1029
+ body: JSON.stringify(data),
1030
+ });
1031
+ var result = await r.json();
1032
+ if (result && result.ok) {
1033
+ showToast('Created ' + (result.id || ''), 'success');
1034
+ return result;
1035
+ } else {
1036
+ showToast('Error: ' + (result.error || 'Unknown'), 'error');
1037
+ return null;
1038
+ }
1039
+ } catch (e) {
1040
+ showToast('Error: ' + e.message, 'error');
1041
+ return null;
1042
+ }
1043
+ }
1044
+
1045
+ async function deleteItem(collection, id) {
1046
+ try {
1047
+ var url = apiBase() + '/' + collection + '/' + encodeURIComponent(id);
1048
+ var r = await fetch(url, { method: 'DELETE' });
1049
+ var result = await r.json();
1050
+ if (result && result.ok) {
1051
+ showToast('Archived successfully', 'success');
1052
+ return true;
1053
+ } else {
1054
+ showToast('Error: ' + (result.error || 'Unknown'), 'error');
1055
+ return false;
1056
+ }
1057
+ } catch (e) {
1058
+ showToast('Error: ' + e.message, 'error');
1059
+ return false;
1060
+ }
1061
+ }
1062
+
1063
+ /* ── Data Loading ────────────────────────────────────────── */
443
1064
  async function loadAll() {
1065
+ var base = apiBase();
444
1066
  var results = await Promise.all([
445
- fetchJson('/api/project'), fetchJson('/api/milestones'), fetchJson('/api/epics'),
446
- fetchJson('/api/tasks'), fetchJson('/api/sprint'), fetchJson('/api/metrics'),
447
- fetchJson('/api/health'), fetchJson('/api/sprints'), fetchJson('/api/config')
1067
+ fetchJson(base + '/project'), fetchJson(base + '/milestones'), fetchJson(base + '/epics'),
1068
+ fetchJson(base + '/tasks'), fetchJson(base + '/sprint'), fetchJson(base + '/metrics'),
1069
+ fetchJson(base + '/health'), fetchJson(base + '/sprints'), fetchJson('/api/config'),
1070
+ fetchJson('/api/sources'), fetchJson(base + '/notes')
448
1071
  ]);
449
1072
  D.project = results[0]; D.milestones = results[1] || []; D.epics = results[2] || [];
450
1073
  D.tasks = results[3] || [];
451
- if (results[4]) D.sprints = [results[4]];
1074
+ if (results[4]) D.sprints = [results[4]]; else D.sprints = [];
452
1075
  D.metrics = results[5]; D.health = results[6];
453
1076
  D.allSprints = results[7] || [];
454
1077
  D.config = results[8];
1078
+ D.sources = results[9] || [];
1079
+ D.notes = results[10] || [];
455
1080
  D.loaded = true;
456
1081
  }
457
1082
 
458
- function refreshData() { loadAll().then(function() { initFromConfig(); renderAll(); }); }
1083
+ function refreshData() {
1084
+ loadAll().then(function() {
1085
+ initFromConfig();
1086
+ renderAll();
1087
+ if (hasWorkspace()) buildSourceRail();
1088
+ });
1089
+ }
1090
+
1091
+ function hasWorkspace() {
1092
+ return D.config && D.config.workspace && D.config.workspace.sources && D.config.workspace.sources.length > 0;
1093
+ }
459
1094
 
460
1095
  /* ── Config-driven initialization ────────────────────────── */
461
1096
  function initFromConfig() {
@@ -531,6 +1166,169 @@ function generateDynamicStyles() {
531
1166
  if (styleEl) styleEl.textContent = css;
532
1167
  }
533
1168
 
1169
+ /* ══════════════════════════════════════════════════════════════
1170
+ THEME ENGINE
1171
+ ══════════════════════════════════════════════════════════════ */
1172
+ function generateThemeCss(themeId) {
1173
+ var theme = THEMES[themeId];
1174
+ if (!theme) return '';
1175
+ var css = '/* mdboard theme: ' + theme.name + ' */\n:root {\n';
1176
+ for (var key in theme.vars) {
1177
+ if (theme.vars.hasOwnProperty(key)) {
1178
+ css += ' --' + key + ': ' + theme.vars[key] + ';\n';
1179
+ }
1180
+ }
1181
+ css += '}\n';
1182
+ return css;
1183
+ }
1184
+
1185
+ function applyTheme(themeId) {
1186
+ var theme = THEMES[themeId];
1187
+ if (!theme) return;
1188
+ var el = document.getElementById('theme-styles');
1189
+ if (el) el.textContent = generateThemeCss(themeId);
1190
+ }
1191
+
1192
+ function revertThemePreview() {
1193
+ var el = document.getElementById('theme-styles');
1194
+ if (el) el.textContent = '';
1195
+ }
1196
+
1197
+ function reloadCustomCss() {
1198
+ var link = document.getElementById('custom-theme');
1199
+ if (link) link.href = '/mdboard.css?t=' + Date.now();
1200
+ }
1201
+
1202
+ function getActiveTheme() {
1203
+ return D.config && D.config.theme ? D.config.theme : 'default-dark';
1204
+ }
1205
+
1206
+ /* ══════════════════════════════════════════════════════════════
1207
+ WORKSPACE — Source switching & sidebar
1208
+ ══════════════════════════════════════════════════════════════ */
1209
+ function renderSidebarLogo() {
1210
+ var p = D.project || {};
1211
+ var logoEl = document.getElementById('sidebar-logo');
1212
+ var iconEl = document.getElementById('sidebar-logo-icon');
1213
+ var textEl = document.getElementById('sidebar-logo-text');
1214
+
1215
+ textEl.textContent = p.name || 'Project';
1216
+
1217
+ if (D.config && D.config.logo) {
1218
+ var logoSrc = D.config.logo.startsWith('http') ? D.config.logo : '/logo';
1219
+ iconEl.innerHTML = '<img src="' + escHtml(logoSrc) + '" alt="">';
1220
+ } else {
1221
+ var initial = (p.name || 'P').charAt(0).toUpperCase();
1222
+ iconEl.textContent = initial;
1223
+ }
1224
+
1225
+ logoEl.onclick = function() {
1226
+ if (hasWorkspace()) {
1227
+ switchSource('overview');
1228
+ }
1229
+ };
1230
+ }
1231
+
1232
+ function buildSourceRail() {
1233
+ var rail = document.getElementById('source-rail');
1234
+ if (!rail) return;
1235
+ rail.innerHTML = '';
1236
+
1237
+ if (!hasWorkspace()) return;
1238
+
1239
+ // Home icon (overview)
1240
+ var home = document.createElement('div');
1241
+ home.className = 'rail-icon' + (D.activeSource === 'overview' ? ' active' : '');
1242
+ home.dataset.source = 'overview';
1243
+ home.setAttribute('data-tooltip', 'Overview');
1244
+ home.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>';
1245
+ home.onclick = function() { switchSource('overview'); };
1246
+ rail.appendChild(home);
1247
+
1248
+ // Divider
1249
+ var divider = document.createElement('div');
1250
+ divider.className = 'rail-divider';
1251
+ rail.appendChild(divider);
1252
+
1253
+ // Source icons
1254
+ var wsSources = D.config.workspace.sources;
1255
+ for (var i = 0; i < wsSources.length; i++) {
1256
+ var s = wsSources[i];
1257
+ if (s.name === 'overview') continue;
1258
+
1259
+ var color = s.color || 'var(--accent)';
1260
+ var icon = document.createElement('div');
1261
+ icon.className = 'rail-icon' + (D.activeSource === s.name ? ' active' : '');
1262
+ icon.dataset.source = s.name;
1263
+ icon.setAttribute('data-tooltip', s.label || s.name);
1264
+ icon.style.background = color + '20';
1265
+ icon.style.color = color;
1266
+ icon.innerHTML = escHtml(s.icon || s.name.charAt(0).toUpperCase());
1267
+ icon.onclick = (function(name) {
1268
+ return function() { switchSource(name); };
1269
+ })(s.name);
1270
+ rail.appendChild(icon);
1271
+ }
1272
+
1273
+ // Switch project icon (bottom of rail)
1274
+ var switchDivider = document.createElement('div');
1275
+ switchDivider.className = 'rail-divider';
1276
+ switchDivider.style.marginTop = 'auto';
1277
+ rail.appendChild(switchDivider);
1278
+
1279
+ var switchIcon = document.createElement('div');
1280
+ switchIcon.className = 'rail-icon rail-switch';
1281
+ switchIcon.setAttribute('data-tooltip', 'Switch workspace');
1282
+ switchIcon.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 3h5v5"/><path d="M21 3l-7 7"/><path d="M8 21H3v-5"/><path d="M3 21l7-7"/></svg>';
1283
+ switchIcon.onclick = function() { openHistoryModal(); };
1284
+ rail.appendChild(switchIcon);
1285
+ }
1286
+
1287
+ function switchSource(sourceName) {
1288
+ if (sourceName === D.activeSource) return;
1289
+
1290
+ // Update active source marker
1291
+ D.activeSource = sourceName;
1292
+ D.loaded = false;
1293
+
1294
+ // Persist selection
1295
+ try { localStorage.setItem('mdboard-source', sourceName); } catch(e) {}
1296
+
1297
+ // Update source rail active state
1298
+ document.querySelectorAll('.rail-icon').forEach(function(el) {
1299
+ el.classList.toggle('active', el.dataset.source === sourceName);
1300
+ });
1301
+
1302
+ // If switching to overview, show overview tabs; otherwise show normal tabs
1303
+ var isOverview = sourceName === 'overview';
1304
+ var viewTabs = document.querySelectorAll('#sidebar-nav a[data-view]');
1305
+ viewTabs.forEach(function(tab) {
1306
+ var view = tab.dataset.view;
1307
+ if (isOverview) {
1308
+ tab.style.display = (view === 'overview' || view === 'metrics') ? '' : 'none';
1309
+ } else {
1310
+ tab.style.display = (view === 'overview') ? 'none' : '';
1311
+ }
1312
+ });
1313
+
1314
+ // Switch to appropriate view
1315
+ if (isOverview) {
1316
+ switchView('overview');
1317
+ } else {
1318
+ var currentView = document.querySelector('.view.active');
1319
+ if (currentView && currentView.id === 'view-overview') {
1320
+ switchView('board');
1321
+ }
1322
+ }
1323
+
1324
+ // Reload data for this source
1325
+ loadAll().then(function() {
1326
+ initFromConfig();
1327
+ renderAll();
1328
+ buildSourceRail();
1329
+ });
1330
+ }
1331
+
534
1332
  /* ── SSE — Hot Reload ────────────────────────────────────── */
535
1333
  function connectSSE() {
536
1334
  try {
@@ -538,6 +1336,28 @@ function connectSSE() {
538
1336
  src.onmessage = function(e) {
539
1337
  var data;
540
1338
  try { data = JSON.parse(e.data); } catch { data = {}; }
1339
+
1340
+ if (data.type === 'project-changed') {
1341
+ // Full reload on project switch (from another tab or API call)
1342
+ D.activeSource = null;
1343
+ D.loaded = false;
1344
+ loadAll().then(function() {
1345
+ initFromConfig();
1346
+ if (hasWorkspace()) {
1347
+ buildSourceRail();
1348
+ D.activeSource = null;
1349
+ switchSource('overview');
1350
+ } else {
1351
+ // Hide source rail if no workspace
1352
+ var rail = document.getElementById('source-rail');
1353
+ if (rail) rail.innerHTML = '';
1354
+ renderAll();
1355
+ }
1356
+ renderSidebarLogo();
1357
+ });
1358
+ return;
1359
+ }
1360
+
541
1361
  if (data.cssReload) {
542
1362
  var link = document.getElementById('custom-theme');
543
1363
  if (link) link.href = '/mdboard.css?' + Date.now();
@@ -549,46 +1369,325 @@ function connectSSE() {
549
1369
  }
550
1370
 
551
1371
  /* ══════════════════════════════════════════════════════════════
552
- RENDER
1372
+ EDITOR.JS WRAPPER — md↔blocks conversion + Notion-like UX
553
1373
  ══════════════════════════════════════════════════════════════ */
554
- function renderAll() { renderHeader(); renderBoardFilters(); renderBoard(); renderTableControls(); renderTableBody(); renderMsFilters(); renderMilestones(); renderMetrics(); }
555
1374
 
556
- function renderHeader() {
557
- var p = D.project || {};
558
- document.getElementById('h-project-name').textContent = p.name || 'Project';
559
- document.getElementById('h-project-desc').textContent = p.description || '';
1375
+ /* ── Inline Markdown ↔ HTML ──────────────────────────────── */
1376
+ function inlineMdToHtml(text) {
1377
+ if (!text) return '';
1378
+ text = text.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
1379
+ text = text.replace(/\*(.+?)\*/g, '<i>$1</i>');
1380
+ text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
1381
+ text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
1382
+ return text;
1383
+ }
560
1384
 
561
- var mw = document.getElementById('h-milestone-wrap');
562
- var am = D.milestones.find(function(m) { return m.status === 'active'; });
563
- if (am) {
564
- mw.style.display = '';
565
- document.getElementById('h-milestone-name').textContent = am.title || am.id || '';
566
- document.getElementById('h-milestone-progress').style.width = (am.progress || 0) + '%';
567
- } else { mw.style.display = 'none'; }
1385
+ function inlineHtmlToMd(html) {
1386
+ if (!html) return '';
1387
+ var text = html.replace(/<br\s*\/?>/gi, '\n');
1388
+ text = text.replace(/<a[^>]+href="([^"]*)"[^>]*>([^<]*)<\/a>/gi, '[$2]($1)');
1389
+ text = text.replace(/<b>([^<]*)<\/b>/gi, '**$1**');
1390
+ text = text.replace(/<strong>([^<]*)<\/strong>/gi, '**$1**');
1391
+ text = text.replace(/<i>([^<]*)<\/i>/gi, '*$1*');
1392
+ text = text.replace(/<em>([^<]*)<\/em>/gi, '*$1*');
1393
+ text = text.replace(/<code>([^<]*)<\/code>/gi, '`$1`');
1394
+ text = text.replace(/<mark[^>]*>([^<]*)<\/mark>/gi, '==$1==');
1395
+ text = text.replace(/<[^>]+>/g, '');
1396
+ return text;
1397
+ }
568
1398
 
569
- var sw = document.getElementById('h-sprint-wrap');
570
- var as = D.sprints.find(function(s) { return s.status === 'active'; });
571
- if (as) {
572
- sw.style.display = '';
573
- document.getElementById('h-sprint-name').textContent = as.goal || as.id || '';
574
- var dr = daysRemaining(as.end_date);
575
- document.getElementById('h-sprint-days').textContent = dr !== null ? (dr >= 0 ? dr + ' days left' : Math.abs(dr) + ' days over') : '';
576
- } else { sw.style.display = 'none'; }
1399
+ /* ── Markdown → Editor.js Blocks ────────────────────────── */
1400
+ function markdownToBlocks(md) {
1401
+ if (!md || !md.trim()) return [{ type: 'paragraph', data: { text: '' } }];
577
1402
 
578
- var taskPlural = ENTITY_NAMES.task ? ENTITY_NAMES.task.plural : 'Tasks';
579
- var total = D.tasks.length;
580
- var done = D.tasks.filter(function(f) { return f.status === COMPLETED_STATUS; }).length;
581
- var inProgStatus = (D.config && D.config.statuses && D.config.statuses.task) ?
582
- ((D.config.statuses.task.find(function(s) { return s.icon === 'half-circle'; }) || {}).key || 'in-progress') : 'in-progress';
583
- var inProg = D.tasks.filter(function(f) { return f.status === inProgStatus; }).length;
584
- var vel = D.health && D.health.velocity != null ? D.health.velocity : (total ? Math.round(done / total * 100) : 0);
585
- document.getElementById('h-stats').innerHTML =
586
- '<div class="stat"><span class="stat-val">' + total + '</span><span class="stat-label">' + escHtml(taskPlural) + '</span></div>' +
587
- '<div class="stat"><span class="stat-val" style="color:var(--success)">' + done + '</span><span class="stat-label">Done</span></div>' +
588
- '<div class="stat"><span class="stat-val" style="color:var(--warning)">' + inProg + '</span><span class="stat-label">In Progress</span></div>' +
589
- '<div class="stat"><span class="stat-val" style="color:var(--accent)">' + vel + '%</span><span class="stat-label">Velocity</span></div>';
1403
+ var lines = md.split('\n');
1404
+ var blocks = [];
1405
+ var i = 0;
1406
+
1407
+ while (i < lines.length) {
1408
+ var line = lines[i];
1409
+
1410
+ if (line.trim() === '') { i++; continue; }
1411
+
1412
+ if (/^---\s*$/.test(line.trim()) || /^\*\*\*\s*$/.test(line.trim())) {
1413
+ blocks.push({ type: 'delimiter', data: {} });
1414
+ i++; continue;
1415
+ }
1416
+
1417
+ var hMatch = line.match(/^(#{1,6})\s+(.+)$/);
1418
+ if (hMatch) {
1419
+ var lvl = Math.min(hMatch[1].length, 6);
1420
+ blocks.push({ type: 'header', data: { text: inlineMdToHtml(hMatch[2]), level: lvl } });
1421
+ i++; continue;
1422
+ }
1423
+
1424
+ if (line.trim().startsWith('```')) {
1425
+ var code = [];
1426
+ i++;
1427
+ while (i < lines.length && !lines[i].trim().startsWith('```')) {
1428
+ code.push(lines[i]);
1429
+ i++;
1430
+ }
1431
+ blocks.push({ type: 'code', data: { code: code.join('\n') } });
1432
+ i++; continue;
1433
+ }
1434
+
1435
+ if (line.trim().startsWith('> ')) {
1436
+ var quoteLines = [];
1437
+ while (i < lines.length && lines[i].trim().startsWith('> ')) {
1438
+ quoteLines.push(lines[i].trim().substring(2));
1439
+ i++;
1440
+ }
1441
+ blocks.push({ type: 'quote', data: { text: inlineMdToHtml(quoteLines.join('<br>')), caption: '' } });
1442
+ continue;
1443
+ }
1444
+
1445
+ if (/^\s*-\s+\[[ x]\]\s+/.test(line)) {
1446
+ var checkItems = [];
1447
+ while (i < lines.length && /^\s*-\s+\[[ x]\]\s+/.test(lines[i])) {
1448
+ var cm = lines[i].match(/^\s*-\s+\[([ x])\]\s+(.+)$/);
1449
+ if (cm) checkItems.push({ content: inlineMdToHtml(cm[2]), meta: { checked: cm[1] === 'x' }, items: [] });
1450
+ i++;
1451
+ }
1452
+ blocks.push({ type: 'list', data: { style: 'checklist', items: checkItems } });
1453
+ continue;
1454
+ }
1455
+
1456
+ if (/^\s*[-*]\s+/.test(line) && !/^\s*-\s+\[/.test(line)) {
1457
+ var listItems = [];
1458
+ while (i < lines.length && /^\s*[-*]\s+/.test(lines[i]) && !/^\s*-\s+\[/.test(lines[i])) {
1459
+ var lm = lines[i].match(/^\s*[-*]\s+(.+)$/);
1460
+ if (lm) listItems.push({ content: inlineMdToHtml(lm[1]), items: [] });
1461
+ i++;
1462
+ }
1463
+ blocks.push({ type: 'list', data: { style: 'unordered', items: listItems } });
1464
+ continue;
1465
+ }
1466
+
1467
+ if (/^\s*\d+\.\s+/.test(line)) {
1468
+ var olItems = [];
1469
+ while (i < lines.length && /^\s*\d+\.\s+/.test(lines[i])) {
1470
+ var om = lines[i].match(/^\s*\d+\.\s+(.+)$/);
1471
+ if (om) olItems.push({ content: inlineMdToHtml(om[1]), items: [] });
1472
+ i++;
1473
+ }
1474
+ blocks.push({ type: 'list', data: { style: 'ordered', items: olItems } });
1475
+ continue;
1476
+ }
1477
+
1478
+ var paraLines = [];
1479
+ while (i < lines.length && lines[i].trim() !== '' &&
1480
+ !/^#{1,6}\s/.test(lines[i]) && !/^```/.test(lines[i]) &&
1481
+ !/^>\s/.test(lines[i]) && !/^\s*[-*]\s/.test(lines[i]) &&
1482
+ !/^\s*\d+\.\s/.test(lines[i]) && !/^---\s*$/.test(lines[i].trim()) &&
1483
+ !/^\*\*\*\s*$/.test(lines[i].trim())) {
1484
+ paraLines.push(lines[i]);
1485
+ i++;
1486
+ }
1487
+ if (paraLines.length > 0) {
1488
+ blocks.push({ type: 'paragraph', data: { text: inlineMdToHtml(paraLines.join('<br>')) } });
1489
+ }
1490
+ }
1491
+
1492
+ return blocks.length > 0 ? blocks : [{ type: 'paragraph', data: { text: '' } }];
1493
+ }
1494
+
1495
+ /* ── Editor.js Blocks → Markdown ────────────────────────── */
1496
+ function blocksToMarkdown(blocks) {
1497
+ if (!blocks || blocks.length === 0) return '';
1498
+
1499
+ var parts = [];
1500
+ for (var i = 0; i < blocks.length; i++) {
1501
+ var b = blocks[i];
1502
+ switch (b.type) {
1503
+ case 'header':
1504
+ var prefix = '';
1505
+ for (var h = 0; h < (b.data.level || 2); h++) prefix += '#';
1506
+ parts.push(prefix + ' ' + inlineHtmlToMd(b.data.text));
1507
+ break;
1508
+ case 'paragraph':
1509
+ parts.push(inlineHtmlToMd(b.data.text));
1510
+ break;
1511
+ case 'list':
1512
+ var items = b.data.items || [];
1513
+ var listLines = [];
1514
+ for (var li = 0; li < items.length; li++) {
1515
+ var itemText = typeof items[li] === 'string' ? items[li] : (items[li].content || items[li].text || '');
1516
+ if (b.data.style === 'checklist') {
1517
+ var chk = (typeof items[li] === 'object' && items[li].meta && items[li].meta.checked) ? 'x' : ' ';
1518
+ listLines.push('- [' + chk + '] ' + inlineHtmlToMd(itemText));
1519
+ } else if (b.data.style === 'ordered') {
1520
+ listLines.push((li + 1) + '. ' + inlineHtmlToMd(itemText));
1521
+ } else {
1522
+ listLines.push('- ' + inlineHtmlToMd(itemText));
1523
+ }
1524
+ }
1525
+ parts.push(listLines.join('\n'));
1526
+ break;
1527
+ case 'checklist':
1528
+ var cItems = b.data.items || [];
1529
+ var checkLines = [];
1530
+ for (var ci = 0; ci < cItems.length; ci++) {
1531
+ var check = cItems[ci].checked ? 'x' : ' ';
1532
+ checkLines.push('- [' + check + '] ' + inlineHtmlToMd(cItems[ci].text || cItems[ci].content || ''));
1533
+ }
1534
+ parts.push(checkLines.join('\n'));
1535
+ break;
1536
+ case 'code':
1537
+ parts.push('```\n' + (b.data.code || '') + '\n```');
1538
+ break;
1539
+ case 'quote':
1540
+ var qText = inlineHtmlToMd(b.data.text || '');
1541
+ var qLines = qText.split('\n');
1542
+ var quoteParts = [];
1543
+ for (var qi = 0; qi < qLines.length; qi++) {
1544
+ quoteParts.push('> ' + qLines[qi]);
1545
+ }
1546
+ parts.push(quoteParts.join('\n'));
1547
+ break;
1548
+ case 'delimiter':
1549
+ parts.push('---');
1550
+ break;
1551
+ default:
1552
+ if (b.data && b.data.text) parts.push(inlineHtmlToMd(b.data.text));
1553
+ }
1554
+ }
1555
+ return parts.join('\n\n');
1556
+ }
1557
+
1558
+ /* ── Editor.js Instance Management ──────────────────────── */
1559
+ function initEditor(holderId, markdown, options) {
1560
+ if (typeof EditorJS === 'undefined') {
1561
+ var el = document.getElementById(holderId);
1562
+ if (el) {
1563
+ var ta = document.createElement('textarea');
1564
+ ta.value = markdown || '';
1565
+ ta.className = 'editor-fallback-textarea';
1566
+ ta.placeholder = (options && options.placeholder) || 'Type / for commands...';
1567
+ ta.id = holderId + '-fallback';
1568
+ el.appendChild(ta);
1569
+ }
1570
+ return { _fallback: true, _holderId: holderId };
1571
+ }
1572
+
1573
+ var opts = options || {};
1574
+ var tools = {};
1575
+
1576
+ if (typeof Header !== 'undefined') {
1577
+ tools.header = {
1578
+ class: Header,
1579
+ inlineToolbar: true,
1580
+ config: { levels: [1, 2, 3, 4], defaultLevel: 2 },
1581
+ };
1582
+ }
1583
+ if (typeof EditorjsList !== 'undefined') tools.list = { class: EditorjsList, inlineToolbar: true, config: { maxLevel: 3 } };
1584
+ if (typeof CodeTool !== 'undefined') tools.code = { class: CodeTool };
1585
+ if (typeof Quote !== 'undefined') tools.quote = { class: Quote, inlineToolbar: true, config: { quotePlaceholder: 'Write a quote...', captionPlaceholder: '' } };
1586
+ if (typeof Delimiter !== 'undefined') tools.delimiter = { class: Delimiter };
1587
+ if (typeof Marker !== 'undefined') tools.marker = { class: Marker };
1588
+ if (typeof InlineCode !== 'undefined') tools.inlineCode = { class: InlineCode };
1589
+
1590
+ var blocks = markdownToBlocks(markdown || '');
1591
+
1592
+ var editor = new EditorJS({
1593
+ holder: holderId,
1594
+ tools: tools,
1595
+ data: { blocks: blocks },
1596
+ placeholder: opts.placeholder || 'Type / for commands...',
1597
+ minHeight: opts.minHeight || 0,
1598
+ autofocus: opts.autofocus || false,
1599
+ onChange: opts.onChange || function() {},
1600
+ defaultBlock: 'paragraph',
1601
+ inlineToolbar: ['bold', 'italic', 'link', 'marker', 'inlineCode'],
1602
+ });
1603
+
1604
+ return editor;
1605
+ }
1606
+
1607
+ async function getEditorMarkdown(editorInstance) {
1608
+ if (!editorInstance) return '';
1609
+ if (editorInstance._fallback) {
1610
+ var ta = document.getElementById(editorInstance._holderId + '-fallback');
1611
+ return ta ? ta.value : '';
1612
+ }
1613
+ try {
1614
+ var data = await editorInstance.save();
1615
+ return blocksToMarkdown(data.blocks);
1616
+ } catch (e) {
1617
+ return '';
1618
+ }
1619
+ }
1620
+
1621
+ function destroyEditor(editorInstance) {
1622
+ if (!editorInstance) return;
1623
+ if (editorInstance._fallback) return;
1624
+ if (typeof editorInstance.destroy === 'function') {
1625
+ try { editorInstance.destroy(); } catch (e) { /* ignore */ }
1626
+ }
1627
+ }
1628
+
1629
+ /* ── Simple Markdown → HTML Renderer (readonly mode) ──── */
1630
+ function simpleMarkdownRender(md) {
1631
+ if (!md) return '';
1632
+ var lines = md.split('\n');
1633
+ var html = '';
1634
+ var inCode = false;
1635
+ var inList = false;
1636
+ var listType = '';
1637
+
1638
+ for (var i = 0; i < lines.length; i++) {
1639
+ var line = lines[i];
1640
+
1641
+ if (line.trim().startsWith('```')) {
1642
+ if (inCode) { html += '</code></pre>'; inCode = false; }
1643
+ else { html += '<pre><code>'; inCode = true; }
1644
+ continue;
1645
+ }
1646
+ if (inCode) { html += escHtml(line) + '\n'; continue; }
1647
+
1648
+ if (inList && !/^\s*[-*]\s/.test(line) && !/^\s*\d+\.\s/.test(line)) {
1649
+ html += '</' + listType + '>'; inList = false;
1650
+ }
1651
+
1652
+ if (line.trim() === '') { if (!inList) html += '<br>'; continue; }
1653
+ if (/^---\s*$/.test(line.trim())) { html += '<hr>'; continue; }
1654
+
1655
+ var hm = line.match(/^(#{1,6})\s+(.+)$/);
1656
+ if (hm) { html += '<h' + hm[1].length + '>' + inlineMdToHtml(hm[2]) + '</h' + hm[1].length + '>'; continue; }
1657
+
1658
+ if (/^>\s/.test(line)) { html += '<blockquote>' + inlineMdToHtml(line.substring(2)) + '</blockquote>'; continue; }
1659
+
1660
+ if (/^\s*-\s+\[[ x]\]\s/.test(line)) {
1661
+ var chm = line.match(/^\s*-\s+\[([ x])\]\s+(.+)$/);
1662
+ if (chm) {
1663
+ var checked = chm[1] === 'x';
1664
+ html += '<div class="md-check"><span class="md-check-box' + (checked ? ' checked' : '') + '">' + (checked ? '&#10003;' : '') + '</span> ' + inlineMdToHtml(chm[2]) + '</div>';
1665
+ }
1666
+ continue;
1667
+ }
1668
+
1669
+ if (/^\s*[-*]\s+/.test(line)) {
1670
+ if (!inList || listType !== 'ul') { if (inList) html += '</' + listType + '>'; html += '<ul>'; inList = true; listType = 'ul'; }
1671
+ var lmatch = line.match(/^\s*[-*]\s+(.+)$/);
1672
+ if (lmatch) html += '<li>' + inlineMdToHtml(lmatch[1]) + '</li>';
1673
+ continue;
1674
+ }
1675
+ if (/^\s*\d+\.\s+/.test(line)) {
1676
+ if (!inList || listType !== 'ol') { if (inList) html += '</' + listType + '>'; html += '<ol>'; inList = true; listType = 'ol'; }
1677
+ var omatch = line.match(/^\s*\d+\.\s+(.+)$/);
1678
+ if (omatch) html += '<li>' + inlineMdToHtml(omatch[1]) + '</li>';
1679
+ continue;
1680
+ }
1681
+
1682
+ html += '<p>' + inlineMdToHtml(line) + '</p>';
1683
+ }
1684
+
1685
+ if (inCode) html += '</code></pre>';
1686
+ if (inList) html += '</' + listType + '>';
1687
+ return html;
590
1688
  }
591
1689
 
1690
+
592
1691
  /* ══════════════════════════════════════════════════════════════
593
1692
  BOARD VIEW
594
1693
  ══════════════════════════════════════════════════════════════ */
@@ -610,7 +1709,11 @@ function renderBoardFilters() {
610
1709
  arr.map(function(v) { return '<option value="' + escHtml(v) + '"' + (boardFilters[key] === v ? ' selected' : '') + '>' + escHtml(v) + '</option>'; }).join('') + '</select>';
611
1710
  }
612
1711
 
613
- c.innerHTML = '<input type="text" data-filter="search" placeholder="Search cards..." value="' + escHtml(boardFilters.search) + '">' +
1712
+ var createBtn = isSourceWritable() ?
1713
+ '<button class="btn btn-sm btn-create" id="board-create-btn">+ New ' + escHtml(ENTITY_NAMES.task ? ENTITY_NAMES.task.singular : 'Task') + '</button>' : '';
1714
+
1715
+ c.innerHTML = createBtn +
1716
+ '<input type="text" data-filter="search" placeholder="Search cards..." value="' + escHtml(boardFilters.search) + '">' +
614
1717
  '<select data-filter="priority"><option value="">All Priorities</option>' +
615
1718
  PRIORITIES.map(function(p) { return '<option value="' + p + '"' + (boardFilters.priority === p ? ' selected' : '') + '>' + (PRIORITY_LABELS[p] || p) + '</option>'; }).join('') + '</select>' +
616
1719
  opts(ep, 'epic', epicPlural) + opts(ms, 'milestone', msPlural);
@@ -621,6 +1724,9 @@ function renderBoardFilters() {
621
1724
  c.querySelectorAll('select[data-filter]').forEach(function(el) {
622
1725
  el.addEventListener('change', function() { boardFilters[el.dataset.filter] = el.value; renderBoard(); });
623
1726
  });
1727
+
1728
+ var btn = document.getElementById('board-create-btn');
1729
+ if (btn) btn.addEventListener('click', function() { openCreateDialog('tasks'); });
624
1730
  }
625
1731
 
626
1732
  function getFilteredBoardTasks() {
@@ -687,6 +1793,7 @@ function renderCard(f) {
687
1793
  '<div class="card-meta">' +
688
1794
  (f.points != null ? '<span class="pill pill-points">' + f.points + ' pts</span>' : '') +
689
1795
  (f.epic ? '<span class="pill pill-epic" style="background:' + ec + '">' + escHtml(f.epic) + '</span>' : '') +
1796
+ (f.ai && Object.keys(f.ai).length > 0 ? '<span class="pill pill-ai" title="AI properties">&#9889;</span>' : '') +
690
1797
  (assigned ? '<span class="card-assigned">' + escHtml(assigned) + '</span>' : '') +
691
1798
  '</div></div>';
692
1799
  }
@@ -769,7 +1876,11 @@ function renderTableControls() {
769
1876
  return '<select data-tf="' + filter + '"><option value="">All ' + label + '</option>' +
770
1877
  arr.map(function(v) { return '<option value="' + escHtml(v) + '"' + (tableFilters[filter] === v ? ' selected' : '') + '>' + escHtml(v) + '</option>'; }).join('') + '</select>';
771
1878
  }
772
- c.innerHTML = '<input type="text" data-tf="search" placeholder="Search ID or title..." value="' + escHtml(tableFilters.search) + '">' +
1879
+ var createBtn = isSourceWritable() ?
1880
+ '<button class="btn btn-sm btn-create" id="table-create-btn">+ New ' + escHtml(ENTITY_NAMES.task ? ENTITY_NAMES.task.singular : 'Task') + '</button>' : '';
1881
+
1882
+ c.innerHTML = createBtn +
1883
+ '<input type="text" data-tf="search" placeholder="Search ID or title..." value="' + escHtml(tableFilters.search) + '">' +
773
1884
  opts(ms, 'milestone', msPlural) + opts(ep, 'epic', epicPlural) + opts(st, 'status', 'Statuses') + opts(sp, 'sprint', sprintPlural) +
774
1885
  '<select data-tf="priority"><option value="">All Priorities</option>' +
775
1886
  PRIORITIES.map(function(p) { return '<option value="' + p + '"' + (tableFilters.priority === p ? ' selected' : '') + '>' + (PRIORITY_LABELS[p] || p) + '</option>'; }).join('') + '</select>';
@@ -778,6 +1889,9 @@ function renderTableControls() {
778
1889
  c.querySelectorAll('select[data-tf]').forEach(function(sel) {
779
1890
  sel.addEventListener('change', function() { tableFilters[sel.dataset.tf] = sel.value; renderTableBody(); });
780
1891
  });
1892
+
1893
+ var btn = document.getElementById('table-create-btn');
1894
+ if (btn) btn.addEventListener('click', function() { openCreateDialog('tasks'); });
781
1895
  }
782
1896
 
783
1897
  function getFilteredTasks() {
@@ -962,49 +2076,396 @@ function renderMetrics() {
962
2076
  }
963
2077
 
964
2078
  /* ══════════════════════════════════════════════════════════════
965
- DETAIL PANEL — Edit tasks, epics, milestones
2079
+ OVERVIEW VIEW
966
2080
  ══════════════════════════════════════════════════════════════ */
967
- var panelState = { open: false, type: null, item: null };
2081
+ async function renderOverview() {
2082
+ var c = document.getElementById('overview-container');
2083
+ if (!D.loaded) { c.innerHTML = '<div class="loading-container"><div class="skeleton skeleton-card" style="height:200px"></div></div>'; return; }
968
2084
 
969
- function openPanel(type, item) {
970
- panelState = { open: true, type: type, item: JSON.parse(JSON.stringify(item)) };
971
- renderPanel();
972
- document.getElementById('detail-panel').classList.add('open');
973
- document.getElementById('panel-overlay').classList.add('open');
974
- }
2085
+ // Fetch overview data
2086
+ var overviewMs = await fetchJson('/api/overview/milestones') || [];
2087
+ var overviewLinks = await fetchJson('/api/overview/links');
2088
+ var overviewMetrics = await fetchJson('/api/overview/metrics');
2089
+
2090
+ D.overviewLinks = overviewLinks;
2091
+
2092
+ var html = '<h2 style="margin-bottom:16px;font-size:16px;font-weight:700">Workspace Overview</h2>';
2093
+
2094
+ // Global Milestones
2095
+ html += '<h3 style="font-size:13px;text-transform:uppercase;letter-spacing:.04em;color:var(--text2);font-weight:600;margin:16px 0 8px">Global Milestones</h3>';
2096
+ if (overviewMs.length > 0) {
2097
+ html += overviewMs.map(function(ms) {
2098
+ var pct = ms.combinedProgress != null ? ms.combinedProgress : (ms.progress || 0);
2099
+ var tracked = ms.tracked || [];
2100
+ var trackedHtml = '';
2101
+ if (tracked.length > 0) {
2102
+ trackedHtml = '<div class="tracked-ms"><div class="tracked-ms-header">Tracked Sub-Milestones</div>' +
2103
+ tracked.map(function(t) {
2104
+ return '<div class="tracked-ms-item"><span class="dot" style="background:' + (t.sourceColor || 'var(--accent)') + '"></span>' +
2105
+ '<span style="font-size:12px;flex:1">' + escHtml(t.title || t.id || '') + '</span>' +
2106
+ '<div class="progress progress-accent" style="width:80px"><div class="progress-fill" style="width:' + t.progress + '%;background:' + (t.sourceColor || 'var(--accent)') + '"></div></div>' +
2107
+ '<span class="tracked-ms-pct">' + t.progress + '%</span></div>';
2108
+ }).join('') + '</div>';
2109
+ }
2110
+ return '<div class="ms-card">' +
2111
+ '<div class="ms-header"><h2>' + milestoneIcon(ms.status) + ' ' + escHtml(ms.title || ms.id || '') + '</h2>' +
2112
+ '<span class="badge ' + badgeClass('badge', ms.status) + '">' + escHtml(ms.status || '') + '</span>' +
2113
+ (ms.deadline ? '<span class="ms-deadline">' + fmtDate(ms.deadline) + '</span>' : '') + '</div>' +
2114
+ '<div class="ms-progress"><div class="progress-label"><span>' + (ms.completedCount || 0) + ' / ' + (ms.featureCount || 0) + ' tasks</span><span>' + pct + '%</span></div>' +
2115
+ '<div class="progress progress-lg progress-success"><div class="progress-fill" style="width:' + pct + '%"></div></div></div>' +
2116
+ trackedHtml + '</div>';
2117
+ }).join('');
2118
+ } else {
2119
+ html += '<div style="color:var(--text3);padding:8px 0">No milestones found across sources.</div>';
2120
+ }
975
2121
 
976
- function closePanel() {
977
- panelState = { open: false, type: null, item: null };
978
- document.getElementById('detail-panel').classList.remove('open');
979
- document.getElementById('panel-overlay').classList.remove('open');
980
- }
2122
+ // Cross-project links
2123
+ if (overviewLinks && overviewLinks.links && overviewLinks.links.length > 0) {
2124
+ html += '<h3 style="font-size:13px;text-transform:uppercase;letter-spacing:.04em;color:var(--text2);font-weight:600;margin:24px 0 8px">Cross-Project Links</h3>';
2125
+ html += '<div class="metrics-grid">';
2126
+ var linkGroups = {};
2127
+ overviewLinks.links.forEach(function(l) {
2128
+ var key = (l.fromSource || 'unknown');
2129
+ if (!linkGroups[key]) linkGroups[key] = [];
2130
+ linkGroups[key].push(l);
2131
+ });
2132
+ Object.keys(linkGroups).forEach(function(src) {
2133
+ var links = linkGroups[src];
2134
+ html += '<div class="metric-card"><h3>' + escHtml(src) + ' Links</h3>';
2135
+ html += links.map(function(l) {
2136
+ return '<div class="health-row"><span style="font-size:12px;font-family:var(--mono)">' + escHtml(l.from || '') + '</span>' +
2137
+ '<span style="color:var(--text3);margin:0 4px">&rarr;</span>' +
2138
+ '<span class="link-chip" data-link="' + escHtml(l.to || '') + '">' +
2139
+ '<span class="link-chip-dot" style="background:var(--accent)"></span>' + escHtml(l.to || '') + '</span></div>';
2140
+ }).join('');
2141
+ html += '</div>';
2142
+ });
2143
+ html += '</div>';
2144
+ }
981
2145
 
982
- function renderPanel() {
983
- var panel = document.getElementById('detail-panel');
984
- var t = panelState.type;
985
- var item = panelState.item;
986
- if (!item) return;
2146
+ // Source metrics
2147
+ if (overviewMetrics && overviewMetrics.sources) {
2148
+ html += '<h3 style="font-size:13px;text-transform:uppercase;letter-spacing:.04em;color:var(--text2);font-weight:600;margin:24px 0 8px">Source Metrics</h3>';
2149
+ html += '<div class="metrics-grid">';
2150
+ Object.keys(overviewMetrics.sources).forEach(function(key) {
2151
+ var m = overviewMetrics.sources[key];
2152
+ var pct = m.totalTasks > 0 ? Math.round((m.completedTasks / m.totalTasks) * 100) : 0;
2153
+ html += '<div class="metric-card" style="border-left:3px solid ' + (m.color || 'var(--accent)') + '">' +
2154
+ '<h3>' + escHtml(m.label || key) + '</h3>' +
2155
+ '<div class="health-row"><div class="health-label">Tasks</div><div class="health-val">' + m.totalTasks + '</div></div>' +
2156
+ '<div class="health-row"><div class="health-label">Completed</div><div class="health-val">' + m.completedTasks + '</div></div>' +
2157
+ '<div class="health-row"><div class="health-label">Points</div><div class="health-val">' + m.totalPoints + '</div></div>' +
2158
+ '<div class="progress progress-lg progress-success" style="margin-top:8px"><div class="progress-fill" style="width:' + pct + '%;background:' + (m.color || 'var(--success)') + '"></div></div>' +
2159
+ '</div>';
2160
+ });
2161
+ html += '</div>';
2162
+ }
987
2163
 
988
- var typeLabel;
989
- if (t === 'tasks') typeLabel = ENTITY_NAMES.task ? ENTITY_NAMES.task.singular : 'Task';
990
- else if (t === 'epics') typeLabel = ENTITY_NAMES.epic ? ENTITY_NAMES.epic.singular : 'Epic';
991
- else typeLabel = ENTITY_NAMES.milestone ? ENTITY_NAMES.milestone.singular : 'Milestone';
2164
+ c.innerHTML = html;
2165
+
2166
+ // Click handlers for link chips
2167
+ c.querySelectorAll('.link-chip[data-link]').forEach(function(chip) {
2168
+ chip.addEventListener('click', function() {
2169
+ var ref = chip.dataset.link;
2170
+ var parts = ref.split(':');
2171
+ if (parts.length === 2) {
2172
+ switchSource(parts[0]);
2173
+ // After switch, try to find and open the item
2174
+ setTimeout(function() {
2175
+ var task = D.tasks.find(function(t) { return t.id === ref || t.id === parts[1]; });
2176
+ if (task) openPanel('tasks', task);
2177
+ }, 500);
2178
+ }
2179
+ });
2180
+ });
2181
+ }
992
2182
 
993
- var html = '<div class="panel-header">' +
994
- '<span class="panel-type">' + escHtml(typeLabel) + '</span>' +
995
- '<span class="panel-item-id">' + escHtml(item.id || '') + '</span>' +
996
- '<button class="panel-close" id="panel-close-btn">&times;</button>' +
2183
+ /* ══════════════════════════════════════════════════════════════
2184
+ NOTES VIEW Notion-style split layout
2185
+ ══════════════════════════════════════════════════════════════ */
2186
+
2187
+ var notesState = { notes: [], activeId: null, editor: null, dirty: false };
2188
+
2189
+ function renderNotes() {
2190
+ var container = document.getElementById('notes-container');
2191
+ if (!container) return;
2192
+
2193
+ container.innerHTML =
2194
+ '<div class="notes-layout">' +
2195
+ '<div class="notes-sidebar">' +
2196
+ '<div class="notes-toolbar">' +
2197
+ '<input type="text" placeholder="Search..." id="notes-search">' +
2198
+ '<button class="btn btn-sm btn-create" id="notes-new">+ New</button>' +
2199
+ '</div>' +
2200
+ '<div class="notes-list" id="notes-list"></div>' +
2201
+ '</div>' +
2202
+ '<div class="notes-editor" id="notes-editor-pane">' +
2203
+ '<div class="notes-empty-state">' +
2204
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="width:48px;height:48px;opacity:.3;margin-bottom:12px"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>' +
2205
+ '<p>Select a note or create a new one</p>' +
2206
+ '<p style="font-size:12px;margin-top:4px;color:var(--text3)">Use <kbd>/</kbd> for block commands</p>' +
2207
+ '</div>' +
2208
+ '</div>' +
997
2209
  '</div>';
998
2210
 
999
- html += '<div class="panel-body">';
2211
+ loadNoteList();
2212
+
2213
+ document.getElementById('notes-new').addEventListener('click', createNewNote);
2214
+ document.getElementById('notes-search').addEventListener('input', function() {
2215
+ renderNoteList(this.value.toLowerCase());
2216
+ });
2217
+ }
2218
+
2219
+ async function loadNoteList() {
2220
+ var base = apiBase();
2221
+ var notes = await fetchJson(base + '/notes');
2222
+ notesState.notes = notes || [];
2223
+ renderNoteList('');
2224
+ }
2225
+
2226
+ function renderNoteList(filter) {
2227
+ var list = document.getElementById('notes-list');
2228
+ if (!list) return;
2229
+
2230
+ var notes = notesState.notes;
2231
+ if (filter) {
2232
+ notes = notes.filter(function(n) {
2233
+ return (n.title || '').toLowerCase().indexOf(filter) !== -1;
2234
+ });
2235
+ }
2236
+
2237
+ notes.sort(function(a, b) {
2238
+ return (b.updated || b.created || '').localeCompare(a.updated || a.created || '');
2239
+ });
2240
+
2241
+ if (notes.length === 0) {
2242
+ list.innerHTML = '<div class="notes-list-empty">No notes yet</div>';
2243
+ return;
2244
+ }
2245
+
2246
+ var html = '';
2247
+ for (var i = 0; i < notes.length; i++) {
2248
+ var n = notes[i];
2249
+ var active = n.id === notesState.activeId ? ' active' : '';
2250
+ html += '<div class="notes-list-item' + active + '" data-id="' + escHtml(n.id) + '">' +
2251
+ '<div class="notes-list-title">' + escHtml(n.title || 'Untitled') + '</div>' +
2252
+ '<div class="notes-list-date">' + escHtml(n.updated || n.created || '') + '</div>' +
2253
+ '</div>';
2254
+ }
2255
+ list.innerHTML = html;
2256
+
2257
+ list.querySelectorAll('.notes-list-item').forEach(function(el) {
2258
+ el.addEventListener('click', function() {
2259
+ selectNote(el.dataset.id);
2260
+ });
2261
+ });
2262
+ }
2263
+
2264
+ async function selectNote(id) {
2265
+ if (notesState.editor) {
2266
+ destroyEditor(notesState.editor);
2267
+ notesState.editor = null;
2268
+ }
2269
+
2270
+ notesState.activeId = id;
2271
+ renderNoteList(document.getElementById('notes-search') ? document.getElementById('notes-search').value.toLowerCase() : '');
2272
+
2273
+ var pane = document.getElementById('notes-editor-pane');
2274
+ if (!pane) return;
2275
+
2276
+ pane.innerHTML = '<div class="notes-editor-loading">Loading...</div>';
2277
+
2278
+ var base = apiBase();
2279
+ var note = await fetchJson(base + '/notes/' + encodeURIComponent(id));
2280
+ if (!note) {
2281
+ pane.innerHTML = '<div class="notes-empty-state"><p>Note not found</p></div>';
2282
+ return;
2283
+ }
2284
+
2285
+ pane.innerHTML =
2286
+ '<div class="notes-header-bar">' +
2287
+ '<input type="text" class="notes-title-input" id="notes-title" placeholder="Untitled" value="' + escHtml(note.title || '') + '">' +
2288
+ '<span class="notes-meta">' + escHtml(note.updated || note.created || '') + '</span>' +
2289
+ '</div>' +
2290
+ '<div class="notes-editorjs-wrap" id="notes-editorjs"></div>' +
2291
+ '<div class="notes-editor-footer">' +
2292
+ '<button class="btn btn-danger btn-sm" id="notes-delete">Delete</button>' +
2293
+ '<span style="flex:1"></span>' +
2294
+ '<button class="btn btn-primary" id="notes-save">Save</button>' +
2295
+ '</div>';
2296
+
2297
+ notesState.editor = initEditor('notes-editorjs', note.content || '', {
2298
+ placeholder: 'Type / for commands...',
2299
+ autofocus: true,
2300
+ onChange: function() { notesState.dirty = true; }
2301
+ });
2302
+ notesState.dirty = false;
2303
+
2304
+ document.getElementById('notes-save').addEventListener('click', saveCurrentNote);
2305
+ document.getElementById('notes-delete').addEventListener('click', function() {
2306
+ deleteCurrentNote(id);
2307
+ });
2308
+
2309
+ // Ctrl/Cmd+S to save
2310
+ pane.addEventListener('keydown', function(e) {
2311
+ if ((e.metaKey || e.ctrlKey) && e.key === 's') {
2312
+ e.preventDefault();
2313
+ saveCurrentNote();
2314
+ }
2315
+ });
2316
+ }
2317
+
2318
+ async function saveCurrentNote() {
2319
+ if (!notesState.activeId) return;
2320
+
2321
+ var titleEl = document.getElementById('notes-title');
2322
+ var title = titleEl ? titleEl.value : '';
2323
+ var content = notesState.editor ? await getEditorMarkdown(notesState.editor) : '';
2324
+
2325
+ var updates = {};
2326
+ var current = notesState.notes.find(function(n) { return n.id === notesState.activeId; });
2327
+ if (title && (!current || title !== current.title)) updates.title = title;
2328
+ updates.content = content;
2329
+ updates.updated = new Date().toISOString().split('T')[0];
2330
+
2331
+ var ok = await patchItem('notes', notesState.activeId, updates);
2332
+ if (ok) {
2333
+ notesState.dirty = false;
2334
+ await loadNoteList();
2335
+ }
2336
+ }
2337
+
2338
+ async function createNewNote() {
2339
+ var result = await createItem('notes', { title: 'Untitled', content: '' });
2340
+ if (result && result.id) {
2341
+ await loadNoteList();
2342
+ selectNote(result.id);
2343
+ }
2344
+ }
2345
+
2346
+ async function deleteCurrentNote(id) {
2347
+ if (!confirm('Delete this note?')) return;
2348
+ var ok = await deleteItem('notes', id);
2349
+ if (ok) {
2350
+ if (notesState.editor) {
2351
+ destroyEditor(notesState.editor);
2352
+ notesState.editor = null;
2353
+ }
2354
+ notesState.activeId = null;
2355
+ await loadNoteList();
2356
+ var pane = document.getElementById('notes-editor-pane');
2357
+ if (pane) {
2358
+ pane.innerHTML =
2359
+ '<div class="notes-empty-state">' +
2360
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="width:48px;height:48px;opacity:.3;margin-bottom:12px"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>' +
2361
+ '<p>Select a note or create a new one</p>' +
2362
+ '</div>';
2363
+ }
2364
+ }
2365
+ }
2366
+
2367
+
2368
+ /* ══════════════════════════════════════════════════════════════
2369
+ CRUD HELPERS
2370
+ ══════════════════════════════════════════════════════════════ */
2371
+ function isSourceWritable() {
2372
+ if (!hasWorkspace() || !D.activeSource) return true;
2373
+ var src = D.config.workspace.sources.find(function(s) { return s.name === D.activeSource; });
2374
+ return src ? !src.readonly : true;
2375
+ }
2376
+
2377
+ function openCreateDialog(collection) {
2378
+ var item = { _isNew: true };
2379
+
2380
+ if (collection === 'tasks') {
2381
+ var ms = D.milestones.length > 0 ? (D.milestones[0].id || D.milestones[0]._dir || '') : '';
2382
+ var ep = D.epics.length > 0 ? (D.epics[0]._dir || D.epics[0].id || '') : '';
2383
+ item.title = '';
2384
+ item.status = 'backlog';
2385
+ item.priority = '';
2386
+ item.points = null;
2387
+ item.assigned = '';
2388
+ item.sprint = '';
2389
+ item.milestone = ms;
2390
+ item.epic = ep;
2391
+ item.content = '';
2392
+ } else if (collection === 'milestones') {
2393
+ item.title = '';
2394
+ item.status = 'planned';
2395
+ item.deadline = '';
2396
+ item.content = '';
2397
+ } else if (collection === 'epics') {
2398
+ var ms = D.milestones.length > 0 ? (D.milestones[0]._dir || D.milestones[0].id || '') : '';
2399
+ item.title = '';
2400
+ item.status = 'active';
2401
+ item.priority = '';
2402
+ item.milestone = ms;
2403
+ item.content = '';
2404
+ }
2405
+
2406
+ panelState = { open: true, type: collection, item: item, isCreate: true, editor: null };
2407
+ renderPanel();
2408
+ document.getElementById('detail-panel').classList.add('open');
2409
+ document.getElementById('panel-overlay').classList.add('open');
2410
+ }
2411
+
2412
+ /* ══════════════════════════════════════════════════════════════
2413
+ DETAIL PANEL — Notion-style: form top + editor bottom
2414
+ ══════════════════════════════════════════════════════════════ */
2415
+ var panelState = { open: false, type: null, item: null, isCreate: false, editor: null };
2416
+
2417
+ function openPanel(type, item) {
2418
+ panelState = { open: true, type: type, item: JSON.parse(JSON.stringify(item)), isCreate: false, editor: null };
2419
+ renderPanel();
2420
+ document.getElementById('detail-panel').classList.add('open');
2421
+ document.getElementById('panel-overlay').classList.add('open');
2422
+ }
2423
+
2424
+ function closePanel() {
2425
+ if (panelState.editor) { destroyEditor(panelState.editor); panelState.editor = null; }
2426
+ panelState = { open: false, type: null, item: null, editor: null };
2427
+ document.getElementById('detail-panel').classList.remove('open');
2428
+ document.getElementById('panel-overlay').classList.remove('open');
2429
+ }
2430
+
2431
+ function renderPanel() {
2432
+ var panel = document.getElementById('detail-panel');
2433
+ var t = panelState.type;
2434
+ var item = panelState.item;
2435
+ var isCreate = panelState.isCreate;
2436
+ if (!item) return;
1000
2437
 
1001
- // Title
1002
- html += '<div class="panel-field"><label>Title</label><input type="text" id="p-title" value="' + escHtml(item.title || '') + '"></div>';
2438
+ var isReadonly = !isCreate && item.readonly;
1003
2439
 
1004
- // Status + Priority row
1005
- html += '<div class="panel-props">';
2440
+ var typeLabel;
2441
+ if (t === 'tasks') typeLabel = ENTITY_NAMES.task ? ENTITY_NAMES.task.singular : 'Task';
2442
+ else if (t === 'epics') typeLabel = ENTITY_NAMES.epic ? ENTITY_NAMES.epic.singular : 'Epic';
2443
+ else typeLabel = ENTITY_NAMES.milestone ? ENTITY_NAMES.milestone.singular : 'Milestone';
1006
2444
 
1007
- // Status derived from config
2445
+ // ── Header ──
2446
+ var html = '<div class="panel-header">' +
2447
+ '<span class="panel-type">' + escHtml(isCreate ? 'New ' + typeLabel : typeLabel) + '</span>' +
2448
+ '<span class="panel-item-id">' + escHtml(isCreate ? '' : (item.id || '')) + '</span>';
2449
+
2450
+ if (item.source && item.sourceColor) {
2451
+ html += '<span class="pill" style="background:' + item.sourceColor + '20;color:' + item.sourceColor + ';font-size:10px">' + escHtml(item.sourceLabel || item.source) + '</span>';
2452
+ }
2453
+
2454
+ html += '<button class="panel-close" id="panel-close-btn">&times;</button></div>';
2455
+
2456
+ // ── Title (large, Notion-style) ──
2457
+ html += '<div class="panel-title-wrap">' +
2458
+ '<input type="text" id="p-title" class="panel-title-input" value="' + escHtml(item.title || '') + '" placeholder="Untitled"' + (isReadonly ? ' disabled' : '') + '>' +
2459
+ '</div>';
2460
+
2461
+ // ── Properties (compact inline) ──
2462
+ html += '<div class="panel-properties">';
2463
+
2464
+ // Properties toggle header
2465
+ html += '<div class="prop-row prop-row-toggle" id="props-toggle"><span class="prop-label" style="font-weight:600"><span class="ai-toggle-arrow" id="props-arrow">&#9654;</span> Properties</span><div class="prop-value"></div></div>';
2466
+ html += '<div class="props-fields-body" id="props-fields-body" style="display:none">';
2467
+
2468
+ // Status
1008
2469
  var statusOptions, statusLabels;
1009
2470
  if (t === 'tasks' && D.config && D.config.statuses && D.config.statuses.task) {
1010
2471
  statusOptions = D.config.statuses.task.map(function(s) { return s.key; });
@@ -1026,61 +2487,322 @@ function renderPanel() {
1026
2487
  statusLabels = { planned: 'Planned', active: 'Active', completed: 'Completed', cancelled: 'Cancelled' };
1027
2488
  }
1028
2489
 
1029
- html += '<div class="panel-field"><label>Status</label><div class="field-with-icon"><span id="p-status-icon">' + statusIcon(item.status) + '</span>' +
1030
- '<select id="p-status">' + statusOptions.map(function(s) {
2490
+ html += '<div class="prop-row"><span class="prop-label">Status</span><div class="prop-value"><span id="p-status-icon">' + statusIcon(item.status) + '</span>' +
2491
+ '<select id="p-status"' + (isReadonly ? ' disabled' : '') + '>' + statusOptions.map(function(s) {
1031
2492
  return '<option value="' + s + '"' + (item.status === s ? ' selected' : '') + '>' + (statusLabels[s] || s) + '</option>';
1032
2493
  }).join('') + '</select></div></div>';
1033
2494
 
1034
- // Priority
1035
2495
  if (t === 'tasks' || t === 'epics') {
1036
- html += '<div class="panel-field"><label>Priority</label><div class="field-with-icon"><span id="p-priority-icon">' + priorityIcon(item.priority) + '</span>' +
1037
- '<select id="p-priority"><option value="">None</option>' + PRIORITIES.map(function(p) {
2496
+ html += '<div class="prop-row"><span class="prop-label">Priority</span><div class="prop-value"><span id="p-priority-icon">' + priorityIcon(item.priority) + '</span>' +
2497
+ '<select id="p-priority"' + (isReadonly ? ' disabled' : '') + '><option value="">None</option>' + PRIORITIES.map(function(p) {
1038
2498
  return '<option value="' + p + '"' + (item.priority === p ? ' selected' : '') + '>' + (PRIORITY_LABELS[p] || p) + '</option>';
1039
2499
  }).join('') + '</select></div></div>';
1040
2500
  }
1041
2501
 
1042
- html += '</div>'; // close panel-props
1043
-
1044
- // Type-specific fields
1045
2502
  if (t === 'tasks') {
1046
- html += '<div class="panel-props">';
1047
- html += '<div class="panel-field"><label>Points</label><input type="number" id="p-points" min="0" max="100" value="' + (item.points != null ? item.points : '') + '"></div>';
2503
+ html += '<div class="prop-row"><span class="prop-label">Points</span><div class="prop-value"><input type="number" id="p-points" min="0" max="100" value="' + (item.points != null ? item.points : '') + '" placeholder="0"' + (isReadonly ? ' disabled' : '') + '></div></div>';
1048
2504
 
1049
2505
  var assignedVal = Array.isArray(item.assigned) ? item.assigned.join(', ') : (item.assigned || '');
1050
- html += '<div class="panel-field"><label>Assigned</label><input type="text" id="p-assigned" value="' + escHtml(assignedVal) + '" placeholder="agent-name"></div>';
1051
- html += '</div>';
2506
+ html += '<div class="prop-row"><span class="prop-label">Assigned</span><div class="prop-value"><input type="text" id="p-assigned" value="' + escHtml(assignedVal) + '" placeholder="Unassigned"' + (isReadonly ? ' disabled' : '') + '></div></div>';
1052
2507
 
1053
- html += '<div class="panel-props">';
1054
- // Sprint
1055
- html += '<div class="panel-field"><label>Sprint</label><select id="p-sprint"><option value="">None</option>' +
2508
+ html += '<div class="prop-row"><span class="prop-label">Sprint</span><div class="prop-value"><select id="p-sprint"' + (isReadonly ? ' disabled' : '') + '><option value="">None</option>' +
1056
2509
  D.allSprints.map(function(s) { return '<option value="' + escHtml(s.id || '') + '"' + (item.sprint === s.id ? ' selected' : '') + '>' + escHtml(s.id || '') + '</option>'; }).join('') +
1057
- '</select></div>';
1058
-
1059
- // Epic (read-only info)
1060
- html += '<div class="panel-field"><label>Epic</label><input type="text" id="p-epic" value="' + escHtml(item.epic || '') + '" disabled style="opacity:.6" title="Epic is determined by file location"></div>';
1061
- html += '</div>';
2510
+ '</select></div></div>';
2511
+
2512
+ if (isCreate) {
2513
+ html += '<div class="prop-row"><span class="prop-label">Milestone</span><div class="prop-value"><select id="p-milestone">' +
2514
+ D.milestones.map(function(m) { return '<option value="' + escHtml(m._dir || m.id || '') + '"' + (item.milestone === (m._dir || m.id) ? ' selected' : '') + '>' + escHtml(m.title || m.id || '') + '</option>'; }).join('') +
2515
+ '</select></div></div>';
2516
+ html += '<div class="prop-row"><span class="prop-label">Epic</span><div class="prop-value"><select id="p-epic-select">' +
2517
+ D.epics.map(function(e) { return '<option value="' + escHtml(e._dir || e.id || '') + '"' + (item.epic === (e._dir || e.id) ? ' selected' : '') + '>' + escHtml(e.title || e.id || '') + '</option>'; }).join('') +
2518
+ '</select></div></div>';
2519
+ } else {
2520
+ html += '<div class="prop-row"><span class="prop-label">Epic</span><div class="prop-value"><span class="prop-text">' + escHtml(item.epic || 'None') + '</span></div></div>';
2521
+ }
1062
2522
  }
1063
2523
 
1064
2524
  if (t === 'milestones') {
1065
- html += '<div class="panel-field"><label>Deadline</label><input type="date" id="p-deadline" value="' + fmtDate(item.deadline) + '"></div>';
2525
+ html += '<div class="prop-row"><span class="prop-label">Deadline</span><div class="prop-value"><input type="date" id="p-deadline" value="' + fmtDate(item.deadline) + '"' + (isReadonly ? ' disabled' : '') + '></div></div>';
1066
2526
  }
1067
2527
 
1068
- // Description / Content
1069
- html += '<div class="panel-field"><label>Description</label><textarea id="p-content">' + escHtml(item.content || '') + '</textarea></div>';
2528
+ if (isCreate && (t === 'epics' || t === 'milestones')) {
2529
+ if (t === 'epics') {
2530
+ html += '<div class="prop-row"><span class="prop-label">Milestone</span><div class="prop-value"><select id="p-milestone">' +
2531
+ D.milestones.map(function(m) { return '<option value="' + escHtml(m._dir || m.id || '') + '">' + escHtml(m.title || m.id || '') + '</option>'; }).join('') +
2532
+ '</select></div></div>';
2533
+ }
2534
+ }
1070
2535
 
1071
- html += '</div>'; // close panel-body
2536
+ // Links
2537
+ if (t === 'tasks' && !isCreate && item.links && Array.isArray(item.links) && item.links.length > 0) {
2538
+ html += '<div class="prop-row"><span class="prop-label">Links</span><div class="prop-value"><div class="link-chips">';
2539
+ item.links.forEach(function(link) {
2540
+ var parts = String(link).split(':');
2541
+ var srcName = parts.length === 2 ? parts[0] : null;
2542
+ var srcColor = 'var(--accent)';
2543
+ if (srcName && D.config && D.config.workspace && D.config.workspace.sources) {
2544
+ var src = D.config.workspace.sources.find(function(s) { return s.name === srcName; });
2545
+ if (src && src.color) srcColor = src.color;
2546
+ }
2547
+ html += '<span class="link-chip" data-link="' + escHtml(String(link)) + '">' +
2548
+ '<span class="link-chip-dot" style="background:' + srcColor + '"></span>' +
2549
+ escHtml(String(link)) + '</span>';
2550
+ });
2551
+ html += '</div></div></div>';
2552
+ }
1072
2553
 
1073
- html += '<div class="panel-footer">' +
1074
- '<button class="btn" id="panel-cancel-btn">Cancel</button>' +
1075
- '<button class="btn btn-primary" id="panel-save-btn">Save Changes</button>' +
1076
- '</div>';
2554
+ // Reverse links
2555
+ if (t === 'tasks' && !isCreate && D.overviewLinks && D.overviewLinks.reverseLinks && item.id) {
2556
+ var reverseRefs = D.overviewLinks.reverseLinks[item.id] || [];
2557
+ if (reverseRefs.length > 0) {
2558
+ html += '<div class="prop-row"><span class="prop-label">Referenced By</span><div class="prop-value"><div class="link-chips">';
2559
+ reverseRefs.forEach(function(ref) {
2560
+ var srcColor = ref.sourceColor || 'var(--accent)';
2561
+ html += '<span class="link-chip" data-link="' + escHtml(ref.from || '') + '">' +
2562
+ '<span class="link-chip-dot" style="background:' + srcColor + '"></span>' +
2563
+ escHtml(ref.from || '') + '</span>';
2564
+ });
2565
+ html += '</div></div></div>';
2566
+ }
2567
+ }
2568
+
2569
+ html += '</div>'; // close props-fields-body
2570
+
2571
+ // ── AI Properties — integrated as prop-rows ──
2572
+ var aiData = item.ai || {};
2573
+ var aiOwnData = item.aiOwn || {};
2574
+ var aiFieldDefs = [
2575
+ { key: 'skills', label: 'Skills', icon: '<svg class="icon icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>' },
2576
+ { key: 'agents', label: 'Agents', icon: '<svg class="icon icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="10" rx="2"/><circle cx="12" cy="5" r="3"/><circle cx="9" cy="16" r="1" fill="currentColor"/><circle cx="15" cy="16" r="1" fill="currentColor"/></svg>' },
2577
+ { key: 'mcps', label: 'MCPs', icon: '<svg class="icon icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16v16H4z"/><path d="M9 9h6v6H9z"/><path d="M9 1v3M15 1v3M9 20v3M15 20v3M1 9h3M1 15h3M20 9h3M20 15h3"/></svg>' },
2578
+ { key: 'commands', label: 'Commands', icon: '<svg class="icon icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>' },
2579
+ { key: 'context', label: 'Context', icon: '<svg class="icon icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>' }
2580
+ ];
2581
+ var aiTotalCount = 0;
2582
+ for (var ai = 0; ai < aiFieldDefs.length; ai++) {
2583
+ var arr = aiData[aiFieldDefs[ai].key];
2584
+ if (arr) aiTotalCount += arr.length;
2585
+ }
2586
+ var showAiSection = aiTotalCount > 0 || !isReadonly;
2587
+ if (showAiSection) {
2588
+ // AI divider row — clickable toggle
2589
+ html += '<div class="prop-row prop-row-divider prop-row-toggle" id="ai-toggle"><span class="prop-label" style="font-weight:600"><span class="ai-toggle-arrow" id="ai-arrow">&#9654;</span> AI</span><div class="prop-value">';
2590
+ if (aiTotalCount > 0) html += '<span class="pill" style="font-size:10px;padding:1px 6px">' + aiTotalCount + '</span>';
2591
+ html += '</div></div>';
2592
+ html += '<div class="ai-fields-body" id="ai-fields-body" style="display:none">';
2593
+ for (var ag = 0; ag < aiFieldDefs.length; ag++) {
2594
+ var def = aiFieldDefs[ag];
2595
+ var resolved = aiData[def.key] || [];
2596
+ var own = aiOwnData[def.key] || [];
2597
+ var inherited = resolved.filter(function(v) { return own.indexOf(v) === -1; });
2598
+ if (isReadonly && resolved.length === 0) continue;
2599
+ html += '<div class="prop-row"><span class="prop-label">' + def.icon + ' ' + escHtml(def.label) + '</span><div class="prop-value">';
2600
+ if (isReadonly) {
2601
+ // Readonly: just show tags
2602
+ for (var ci = 0; ci < resolved.length; ci++) {
2603
+ var isOwn = own.indexOf(resolved[ci]) !== -1;
2604
+ html += '<span class="ai-tag' + (isOwn ? ' ai-tag-own' : ' ai-tag-inherited') + '">' + escHtml(resolved[ci]) + '</span>';
2605
+ }
2606
+ } else {
2607
+ // Editable: tag input container
2608
+ html += '<div class="ai-tag-input" data-ai-field="' + def.key + '">';
2609
+ // Inherited tags (dashed, no remove)
2610
+ for (var ii = 0; ii < inherited.length; ii++) {
2611
+ html += '<span class="ai-tag ai-tag-inherited">' + escHtml(inherited[ii]) + '</span>';
2612
+ }
2613
+ // Own tags (accent, with remove button)
2614
+ for (var oi = 0; oi < own.length; oi++) {
2615
+ html += '<span class="ai-tag ai-tag-own"><span class="ai-tag-val">' + escHtml(own[oi]) + '</span><button class="ai-tag-remove" type="button">&times;</button></span>';
2616
+ }
2617
+ // Text input for adding
2618
+ html += '<input type="text" class="ai-tag-text" placeholder="Add...">';
2619
+ // Autocomplete dropdown
2620
+ html += '<div class="ai-autocomplete"></div>';
2621
+ html += '</div>';
2622
+ }
2623
+ html += '</div></div>';
2624
+ }
2625
+ html += '</div>'; // close ai-fields-body
2626
+ }
2627
+
2628
+ html += '</div>'; // close panel-properties
2629
+
2630
+ // ── Content Editor (fills remaining space) ──
2631
+ if (isReadonly) {
2632
+ html += '<div class="panel-content-area"><div id="p-content-editor" class="panel-editor-zone md-rendered">' + simpleMarkdownRender(item.content || '') + '</div></div>';
2633
+ } else {
2634
+ html += '<div class="panel-content-area"><div id="p-content-editor" class="panel-editor-zone"></div></div>';
2635
+ }
2636
+
2637
+ // ── Footer ──
2638
+ html += '<div class="panel-footer">';
2639
+ if (!isCreate && !isReadonly && isSourceWritable()) {
2640
+ html += '<button class="btn btn-danger btn-sm" id="panel-archive-btn">Archive</button>';
2641
+ }
2642
+ html += '<span style="flex:1"></span>';
2643
+ html += '<button class="btn" id="panel-cancel-btn">Cancel</button>';
2644
+ if (!isReadonly) {
2645
+ html += '<button class="btn btn-primary" id="panel-save-btn">' + (isCreate ? 'Create' : 'Save') + '</button>';
2646
+ }
2647
+ html += '</div>';
1077
2648
 
1078
2649
  panel.innerHTML = html;
1079
2650
 
2651
+ // Initialize Editor.js for content (if not readonly)
2652
+ if (!isReadonly) {
2653
+ panelState.editor = initEditor('p-content-editor', item.content || '', {
2654
+ placeholder: 'Type / for commands, or start writing...',
2655
+ minHeight: 0,
2656
+ });
2657
+ }
2658
+
1080
2659
  // Event listeners
1081
2660
  document.getElementById('panel-close-btn').addEventListener('click', closePanel);
1082
2661
  document.getElementById('panel-cancel-btn').addEventListener('click', closePanel);
1083
- document.getElementById('panel-save-btn').addEventListener('click', savePanel);
2662
+ var saveBtn = document.getElementById('panel-save-btn');
2663
+ if (saveBtn) saveBtn.addEventListener('click', isCreate ? saveCreatePanel : savePanel);
2664
+
2665
+ var archiveBtn = document.getElementById('panel-archive-btn');
2666
+ if (archiveBtn) {
2667
+ archiveBtn.addEventListener('click', function() {
2668
+ if (!confirm('Archive this ' + typeLabel.toLowerCase() + '?')) return;
2669
+ deleteItem(t, item.id).then(function(ok) {
2670
+ if (ok) { closePanel(); refreshData(); }
2671
+ });
2672
+ });
2673
+ }
2674
+
2675
+ // Properties section toggle
2676
+ var propsToggle = document.getElementById('props-toggle');
2677
+ if (propsToggle) {
2678
+ propsToggle.addEventListener('click', function() {
2679
+ var body = document.getElementById('props-fields-body');
2680
+ var arrow = document.getElementById('props-arrow');
2681
+ if (body.style.display === 'none') {
2682
+ body.style.display = '';
2683
+ arrow.innerHTML = '&#9660;';
2684
+ } else {
2685
+ body.style.display = 'none';
2686
+ arrow.innerHTML = '&#9654;';
2687
+ }
2688
+ });
2689
+ }
2690
+
2691
+ // AI section toggle
2692
+ var aiToggle = document.getElementById('ai-toggle');
2693
+ if (aiToggle) {
2694
+ aiToggle.addEventListener('click', function() {
2695
+ var body = document.getElementById('ai-fields-body');
2696
+ var arrow = document.getElementById('ai-arrow');
2697
+ if (body.style.display === 'none') {
2698
+ body.style.display = '';
2699
+ arrow.innerHTML = '&#9660;';
2700
+ } else {
2701
+ body.style.display = 'none';
2702
+ arrow.innerHTML = '&#9654;';
2703
+ }
2704
+ });
2705
+ }
2706
+
2707
+ // AI tag input event listeners
2708
+ panel.querySelectorAll('.ai-tag-input').forEach(function(container) {
2709
+ var field = container.dataset.aiField;
2710
+ var textInput = container.querySelector('.ai-tag-text');
2711
+ var dropdown = container.querySelector('.ai-autocomplete');
2712
+ if (!textInput || !dropdown) return;
2713
+
2714
+ function getOwnValues() {
2715
+ var vals = [];
2716
+ container.querySelectorAll('.ai-tag-own .ai-tag-val').forEach(function(el) {
2717
+ vals.push(el.textContent);
2718
+ });
2719
+ return vals;
2720
+ }
2721
+
2722
+ function getAllValues() {
2723
+ var vals = [];
2724
+ container.querySelectorAll('.ai-tag').forEach(function(el) {
2725
+ var valEl = el.querySelector('.ai-tag-val');
2726
+ vals.push(valEl ? valEl.textContent : el.textContent);
2727
+ });
2728
+ return vals;
2729
+ }
2730
+
2731
+ function addTag(value) {
2732
+ var v = value.trim();
2733
+ if (!v) return;
2734
+ var existing = getAllValues();
2735
+ if (existing.indexOf(v) !== -1) return;
2736
+ var tag = document.createElement('span');
2737
+ tag.className = 'ai-tag ai-tag-own';
2738
+ tag.innerHTML = '<span class="ai-tag-val">' + escHtml(v) + '</span><button class="ai-tag-remove" type="button">&times;</button>';
2739
+ tag.querySelector('.ai-tag-remove').addEventListener('click', function() { tag.remove(); });
2740
+ container.insertBefore(tag, textInput);
2741
+ textInput.value = '';
2742
+ closeDropdown();
2743
+ }
2744
+
2745
+ function closeDropdown() {
2746
+ dropdown.classList.remove('open');
2747
+ dropdown.innerHTML = '';
2748
+ }
2749
+
2750
+ function showDropdown() {
2751
+ var suggestions = D.aiSuggestions ? (D.aiSuggestions[field] || []) : [];
2752
+ var present = getAllValues();
2753
+ var query = textInput.value.trim().toLowerCase();
2754
+ var filtered = suggestions.filter(function(s) {
2755
+ return present.indexOf(s) === -1 && (!query || s.toLowerCase().indexOf(query) !== -1);
2756
+ });
2757
+ if (filtered.length === 0) { closeDropdown(); return; }
2758
+ dropdown.innerHTML = '';
2759
+ filtered.forEach(function(s) {
2760
+ var item = document.createElement('div');
2761
+ item.className = 'ai-autocomplete-item';
2762
+ item.textContent = s;
2763
+ item.addEventListener('mousedown', function(e) {
2764
+ e.preventDefault();
2765
+ addTag(s);
2766
+ });
2767
+ dropdown.appendChild(item);
2768
+ });
2769
+ dropdown.classList.add('open');
2770
+ }
2771
+
2772
+ // Click on × to remove own tag
2773
+ container.querySelectorAll('.ai-tag-remove').forEach(function(btn) {
2774
+ btn.addEventListener('click', function() { btn.closest('.ai-tag').remove(); });
2775
+ });
2776
+
2777
+ // Click on container focuses input
2778
+ container.addEventListener('click', function(e) {
2779
+ if (e.target === container) textInput.focus();
2780
+ });
2781
+
2782
+ // Input typing → show autocomplete
2783
+ textInput.addEventListener('input', showDropdown);
2784
+ textInput.addEventListener('focus', showDropdown);
2785
+
2786
+ // Enter → add tag
2787
+ textInput.addEventListener('keydown', function(e) {
2788
+ if (e.key === 'Enter') {
2789
+ e.preventDefault();
2790
+ if (textInput.value.trim()) addTag(textInput.value);
2791
+ } else if (e.key === 'Backspace' && textInput.value === '') {
2792
+ // Remove last own tag
2793
+ var ownTags = container.querySelectorAll('.ai-tag-own');
2794
+ if (ownTags.length > 0) ownTags[ownTags.length - 1].remove();
2795
+ } else if (e.key === 'Escape') {
2796
+ closeDropdown();
2797
+ textInput.blur();
2798
+ }
2799
+ });
2800
+
2801
+ // Close dropdown on blur
2802
+ textInput.addEventListener('blur', function() {
2803
+ setTimeout(closeDropdown, 150);
2804
+ });
2805
+ });
1084
2806
 
1085
2807
  // Live icon updates
1086
2808
  var statusSel = document.getElementById('p-status');
@@ -1097,6 +2819,31 @@ function renderPanel() {
1097
2819
  if (iconEl) iconEl.innerHTML = priorityIcon(prioritySel.value);
1098
2820
  });
1099
2821
  }
2822
+
2823
+ // Ctrl/Cmd+S to save
2824
+ panel.addEventListener('keydown', function(e) {
2825
+ if ((e.metaKey || e.ctrlKey) && e.key === 's') {
2826
+ e.preventDefault();
2827
+ if (isCreate) saveCreatePanel();
2828
+ else savePanel();
2829
+ }
2830
+ });
2831
+
2832
+ // Link chip click handlers
2833
+ panel.querySelectorAll('.link-chip[data-link]').forEach(function(chip) {
2834
+ chip.addEventListener('click', function() {
2835
+ var ref = chip.dataset.link;
2836
+ var parts = ref.split(':');
2837
+ if (parts.length === 2 && hasWorkspace()) {
2838
+ closePanel();
2839
+ switchSource(parts[0]);
2840
+ setTimeout(function() {
2841
+ var task = D.tasks.find(function(t) { return t.id === ref || t.id === parts[1]; });
2842
+ if (task) openPanel('tasks', task);
2843
+ }, 500);
2844
+ }
2845
+ });
2846
+ });
1100
2847
  }
1101
2848
 
1102
2849
  async function savePanel() {
@@ -1140,8 +2887,30 @@ async function savePanel() {
1140
2887
  if (deadlineEl && deadlineEl.value !== fmtDate(item.deadline)) updates.deadline = deadlineEl.value || null;
1141
2888
  }
1142
2889
 
1143
- var contentEl = document.getElementById('p-content');
1144
- if (contentEl && contentEl.value !== (item.content || '')) updates.content = contentEl.value;
2890
+ if (panelState.editor) {
2891
+ var newContent = await getEditorMarkdown(panelState.editor);
2892
+ if (newContent !== (item.content || '')) updates.content = newContent;
2893
+ }
2894
+
2895
+ // AI properties — read from tag inputs
2896
+ var aiTagInputs = document.querySelectorAll('.ai-tag-input[data-ai-field]');
2897
+ if (aiTagInputs.length > 0) {
2898
+ var aiObj = {};
2899
+ aiTagInputs.forEach(function(container) {
2900
+ var field = container.dataset.aiField;
2901
+ var vals = [];
2902
+ container.querySelectorAll('.ai-tag-own .ai-tag-val').forEach(function(el) {
2903
+ var v = el.textContent.trim();
2904
+ if (v) vals.push(v);
2905
+ });
2906
+ if (vals.length > 0) aiObj[field] = vals;
2907
+ });
2908
+ if (Object.keys(aiObj).length > 0) {
2909
+ updates.ai = aiObj;
2910
+ } else if (item.aiOwn && Object.keys(item.aiOwn).length > 0) {
2911
+ updates.ai = null;
2912
+ }
2913
+ }
1145
2914
 
1146
2915
  if (Object.keys(updates).length === 0) {
1147
2916
  showToast('No changes to save', 'success');
@@ -1156,22 +2925,290 @@ async function savePanel() {
1156
2925
  }
1157
2926
  }
1158
2927
 
2928
+ async function saveCreatePanel() {
2929
+ var t = panelState.type;
2930
+ if (!t) return;
2931
+
2932
+ var data = {};
2933
+
2934
+ var titleEl = document.getElementById('p-title');
2935
+ if (titleEl) data.title = titleEl.value || 'Untitled';
2936
+
2937
+ var statusEl = document.getElementById('p-status');
2938
+ if (statusEl) data.status = statusEl.value;
2939
+
2940
+ if (t === 'tasks' || t === 'epics') {
2941
+ var priorityEl = document.getElementById('p-priority');
2942
+ if (priorityEl && priorityEl.value) data.priority = priorityEl.value;
2943
+ }
2944
+
2945
+ if (t === 'tasks') {
2946
+ var pointsEl = document.getElementById('p-points');
2947
+ if (pointsEl && pointsEl.value) data.points = Number(pointsEl.value);
2948
+
2949
+ var assignedEl = document.getElementById('p-assigned');
2950
+ if (assignedEl && assignedEl.value.trim()) data.assigned = assignedEl.value.trim();
2951
+
2952
+ var sprintEl = document.getElementById('p-sprint');
2953
+ if (sprintEl && sprintEl.value) data.sprint = sprintEl.value;
2954
+
2955
+ var milestoneEl = document.getElementById('p-milestone');
2956
+ if (milestoneEl) data.milestone = milestoneEl.value;
2957
+
2958
+ var epicEl = document.getElementById('p-epic-select');
2959
+ if (epicEl) data.epic = epicEl.value;
2960
+ }
2961
+
2962
+ if (t === 'epics' || t === 'milestones') {
2963
+ var milestoneEl = document.getElementById('p-milestone');
2964
+ if (milestoneEl) data.milestone = milestoneEl.value;
2965
+ }
2966
+
2967
+ if (t === 'milestones') {
2968
+ var deadlineEl = document.getElementById('p-deadline');
2969
+ if (deadlineEl && deadlineEl.value) data.deadline = deadlineEl.value;
2970
+ }
2971
+
2972
+ if (panelState.editor) {
2973
+ var editorContent = await getEditorMarkdown(panelState.editor);
2974
+ if (editorContent) data.content = editorContent;
2975
+ }
2976
+
2977
+ // AI properties — read from tag inputs
2978
+ var aiTagInputs = document.querySelectorAll('.ai-tag-input[data-ai-field]');
2979
+ if (aiTagInputs.length > 0) {
2980
+ var aiObj = {};
2981
+ aiTagInputs.forEach(function(container) {
2982
+ var field = container.dataset.aiField;
2983
+ var vals = [];
2984
+ container.querySelectorAll('.ai-tag-own .ai-tag-val').forEach(function(el) {
2985
+ var v = el.textContent.trim();
2986
+ if (v) vals.push(v);
2987
+ });
2988
+ if (vals.length > 0) aiObj[field] = vals;
2989
+ });
2990
+ if (Object.keys(aiObj).length > 0) data.ai = aiObj;
2991
+ }
2992
+
2993
+ var result = await createItem(t, data);
2994
+ if (result) {
2995
+ closePanel();
2996
+ refreshData();
2997
+ }
2998
+ }
2999
+
1159
3000
  // Close panel on overlay click or Escape
1160
3001
  document.getElementById('panel-overlay').addEventListener('click', closePanel);
1161
3002
  document.addEventListener('keydown', function(e) {
1162
3003
  if (e.key === 'Escape' && panelState.open) closePanel();
1163
3004
  });
1164
3005
 
3006
+
3007
+ /* ══════════════════════════════════════════════════════════════
3008
+ HISTORY — Project switch modal
3009
+ ══════════════════════════════════════════════════════════════ */
3010
+
3011
+ function openHistoryModal() {
3012
+ var overlay = document.getElementById('history-overlay');
3013
+ var modal = document.getElementById('history-modal');
3014
+ if (!overlay || !modal) return;
3015
+
3016
+ overlay.classList.add('open');
3017
+ modal.classList.add('open');
3018
+ modal.innerHTML = '<div class="history-header"><h2>Switch Project</h2><button class="panel-close" id="history-close">&times;</button></div><div class="history-body"><div class="history-loading">Loading...</div></div>';
3019
+
3020
+ document.getElementById('history-close').onclick = closeHistoryModal;
3021
+ overlay.onclick = closeHistoryModal;
3022
+
3023
+ fetchJson('/api/history').then(function(entries) {
3024
+ renderHistoryList(entries || []);
3025
+ });
3026
+ }
3027
+
3028
+ function closeHistoryModal() {
3029
+ var overlay = document.getElementById('history-overlay');
3030
+ var modal = document.getElementById('history-modal');
3031
+ if (overlay) overlay.classList.remove('open');
3032
+ if (modal) modal.classList.remove('open');
3033
+ }
3034
+
3035
+ function renderHistoryList(entries) {
3036
+ var modal = document.getElementById('history-modal');
3037
+ if (!modal) return;
3038
+
3039
+ var body = modal.querySelector('.history-body');
3040
+ if (!body) return;
3041
+
3042
+ if (entries.length === 0) {
3043
+ body.innerHTML = '<div class="history-empty">No projects in history yet.<br>Open mdboard from a project directory to add it.</div>';
3044
+ return;
3045
+ }
3046
+
3047
+ // Sort: current first, then by lastOpened descending
3048
+ entries.sort(function(a, b) {
3049
+ if (a.isCurrent && !b.isCurrent) return -1;
3050
+ if (!a.isCurrent && b.isCurrent) return 1;
3051
+ return new Date(b.lastOpened) - new Date(a.lastOpened);
3052
+ });
3053
+
3054
+ var html = '';
3055
+ for (var i = 0; i < entries.length; i++) {
3056
+ var e = entries[i];
3057
+ var initial = (e.name || 'P').charAt(0).toUpperCase();
3058
+ var relTime = timeAgo(e.lastOpened);
3059
+ var currentClass = e.isCurrent ? ' history-item-current' : '';
3060
+ var currentBadge = e.isCurrent ? '<span class="history-current-badge">Current</span>' : '';
3061
+
3062
+ html += '<div class="history-item' + currentClass + '" data-path="' + escHtml(e.path) + '">' +
3063
+ '<div class="history-item-icon">' + escHtml(initial) + '</div>' +
3064
+ '<div class="history-item-info">' +
3065
+ '<div class="history-item-name">' + escHtml(e.name) + currentBadge + '</div>' +
3066
+ '<div class="history-item-path">' + escHtml(e.path) + '</div>' +
3067
+ '</div>' +
3068
+ '<div class="history-item-time">' + escHtml(relTime) + '</div>' +
3069
+ '</div>';
3070
+ }
3071
+
3072
+ body.innerHTML = html;
3073
+
3074
+ // Attach click handlers
3075
+ var items = body.querySelectorAll('.history-item');
3076
+ for (var j = 0; j < items.length; j++) {
3077
+ items[j].addEventListener('click', function() {
3078
+ var itemPath = this.getAttribute('data-path');
3079
+ if (this.classList.contains('history-item-current')) return;
3080
+ switchProject(itemPath);
3081
+ });
3082
+ }
3083
+ }
3084
+
3085
+ function switchProject(projectPath) {
3086
+ var body = document.querySelector('#history-modal .history-body');
3087
+ if (body) body.innerHTML = '<div class="history-loading">Switching project...</div>';
3088
+
3089
+ fetch('/api/history/switch', {
3090
+ method: 'POST',
3091
+ headers: { 'Content-Type': 'application/json' },
3092
+ body: JSON.stringify({ path: projectPath }),
3093
+ })
3094
+ .then(function(r) { return r.json(); })
3095
+ .then(function(data) {
3096
+ if (data && data.ok) {
3097
+ closeHistoryModal();
3098
+ // Reload everything — the SSE event will also trigger for other tabs
3099
+ D.activeSource = null;
3100
+ D.loaded = false;
3101
+ loadAll().then(function() {
3102
+ initFromConfig();
3103
+ if (hasWorkspace()) {
3104
+ buildSourceRail();
3105
+ D.activeSource = null;
3106
+ switchSource('overview');
3107
+ } else {
3108
+ renderAll();
3109
+ renderSidebarLogo();
3110
+ // Hide source rail if no workspace
3111
+ var rail = document.getElementById('source-rail');
3112
+ if (rail) rail.innerHTML = '';
3113
+ }
3114
+ });
3115
+ showToast('Switched to ' + (data.name || 'project'), 'success');
3116
+ } else {
3117
+ showToast('Error: ' + (data.error || 'Switch failed'), 'error');
3118
+ closeHistoryModal();
3119
+ }
3120
+ })
3121
+ .catch(function(err) {
3122
+ showToast('Error: ' + err.message, 'error');
3123
+ closeHistoryModal();
3124
+ });
3125
+ }
3126
+
3127
+ function checkAutoOpenHistory() {
3128
+ if (D.config && D.config.hasProject === false) {
3129
+ openHistoryModal();
3130
+ }
3131
+ }
3132
+
3133
+ function timeAgo(dateStr) {
3134
+ if (!dateStr) return '';
3135
+ var now = Date.now();
3136
+ var then = new Date(dateStr).getTime();
3137
+ var diff = Math.floor((now - then) / 1000);
3138
+ if (diff < 60) return 'just now';
3139
+ if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
3140
+ if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
3141
+ if (diff < 604800) return Math.floor(diff / 86400) + 'd ago';
3142
+ return new Date(dateStr).toLocaleDateString();
3143
+ }
3144
+
3145
+
3146
+ /* ══════════════════════════════════════════════════════════════
3147
+ RENDER
3148
+ ══════════════════════════════════════════════════════════════ */
3149
+ function renderAll() {
3150
+ renderHeader(); renderBoardFilters(); renderBoard(); renderTableControls(); renderTableBody(); renderMsFilters(); renderMilestones(); renderMetrics();
3151
+ // Render overview if active
3152
+ var overviewView = document.getElementById('view-overview');
3153
+ if (overviewView && overviewView.classList.contains('active')) {
3154
+ renderOverview();
3155
+ }
3156
+ }
3157
+
3158
+ function renderHeader() {
3159
+ renderSidebarLogo();
3160
+
3161
+ var mw = document.getElementById('h-milestone-wrap');
3162
+ var am = D.milestones.find(function(m) { return m.status === 'active'; });
3163
+ if (am) {
3164
+ mw.style.display = '';
3165
+ document.getElementById('h-milestone-name').textContent = am.title || am.id || '';
3166
+ document.getElementById('h-milestone-progress').style.width = (am.progress || 0) + '%';
3167
+ } else { mw.style.display = 'none'; }
3168
+
3169
+ var sw = document.getElementById('h-sprint-wrap');
3170
+ var as = D.sprints.find(function(s) { return s.status === 'active'; });
3171
+ if (as) {
3172
+ sw.style.display = '';
3173
+ document.getElementById('h-sprint-name').textContent = as.goal || as.id || '';
3174
+ var dr = daysRemaining(as.end_date);
3175
+ document.getElementById('h-sprint-days').textContent = dr !== null ? (dr >= 0 ? dr + ' days left' : Math.abs(dr) + ' days over') : '';
3176
+ } else { sw.style.display = 'none'; }
3177
+
3178
+ var taskPlural = ENTITY_NAMES.task ? ENTITY_NAMES.task.plural : 'Tasks';
3179
+ var total = D.tasks.length;
3180
+ var done = D.tasks.filter(function(f) { return f.status === COMPLETED_STATUS; }).length;
3181
+ var inProgStatus = (D.config && D.config.statuses && D.config.statuses.task) ?
3182
+ ((D.config.statuses.task.find(function(s) { return s.icon === 'half-circle'; }) || {}).key || 'in-progress') : 'in-progress';
3183
+ var inProg = D.tasks.filter(function(f) { return f.status === inProgStatus; }).length;
3184
+ var vel = D.health && D.health.velocity != null ? D.health.velocity : (total ? Math.round(done / total * 100) : 0);
3185
+ document.getElementById('h-stats').innerHTML =
3186
+ '<div class="stat"><span class="stat-val">' + total + '</span><span class="stat-label">' + escHtml(taskPlural) + '</span></div>' +
3187
+ '<div class="stat"><span class="stat-val" style="color:var(--success)">' + done + '</span><span class="stat-label">Done</span></div>' +
3188
+ '<div class="stat"><span class="stat-val" style="color:var(--warning)">' + inProg + '</span><span class="stat-label">In Progress</span></div>' +
3189
+ '<div class="stat"><span class="stat-val" style="color:var(--accent)">' + vel + '%</span><span class="stat-label">Velocity</span></div>';
3190
+ }
3191
+
1165
3192
  /* ══════════════════════════════════════════════════════════════
1166
3193
  NAVIGATION
1167
3194
  ══════════════════════════════════════════════════════════════ */
1168
3195
  function switchView(name) {
1169
3196
  document.querySelectorAll('.view').forEach(function(v) { v.classList.remove('active'); });
1170
- document.querySelectorAll('#sidebar-nav a').forEach(function(a) { a.classList.remove('active'); });
3197
+ document.querySelectorAll('#sidebar-nav a[data-view]').forEach(function(a) { a.classList.remove('active'); });
1171
3198
  var t = document.getElementById('view-' + name);
1172
3199
  if (t) t.classList.add('active');
1173
3200
  var l = document.querySelector('#sidebar-nav a[data-view="' + name + '"]');
1174
3201
  if (l) l.classList.add('active');
3202
+ // Persist view selection
3203
+ try { localStorage.setItem('mdboard-view', name); } catch(e) {}
3204
+ // Trigger overview rendering when switching to it
3205
+ if (name === 'overview' && D.loaded) {
3206
+ renderOverview();
3207
+ }
3208
+ // Trigger notes rendering when switching to it
3209
+ if (name === 'notes' && D.loaded) {
3210
+ renderNotes();
3211
+ }
1175
3212
  }
1176
3213
 
1177
3214
  document.getElementById('sidebar-nav').addEventListener('click', function(e) {
@@ -1183,12 +3220,124 @@ document.getElementById('sidebar-nav').addEventListener('click', function(e) {
1183
3220
  });
1184
3221
 
1185
3222
  function handleHash() {
1186
- var hash = window.location.hash.replace('#', '') || 'board';
1187
- var valid = ['board','table','milestones','metrics'];
3223
+ var hash = window.location.hash.replace('#', '');
3224
+ if (!hash) {
3225
+ try { hash = localStorage.getItem('mdboard-view') || ''; } catch(e) {}
3226
+ }
3227
+ hash = hash || 'board';
3228
+ var valid = ['board','table','milestones','metrics','overview','notes'];
1188
3229
  switchView(valid.indexOf(hash) !== -1 ? hash : 'board');
1189
3230
  }
1190
3231
  window.addEventListener('hashchange', handleHash);
1191
3232
 
3233
+ /* ══════════════════════════════════════════════════════════════
3234
+ SETTINGS PANEL
3235
+ ══════════════════════════════════════════════════════════════ */
3236
+ var _settingsState = { selectedTheme: null, originalTheme: null };
3237
+
3238
+ function openSettings() {
3239
+ var active = getActiveTheme();
3240
+ _settingsState.originalTheme = active;
3241
+ _settingsState.selectedTheme = null;
3242
+ document.getElementById('settings-overlay').classList.add('open');
3243
+ document.getElementById('settings-panel').classList.add('open');
3244
+ var toggle = document.getElementById('theme-default-toggle');
3245
+ if (toggle) toggle.checked = false;
3246
+ renderThemeGrid();
3247
+ }
3248
+
3249
+ function closeSettings() {
3250
+ if (_settingsState.selectedTheme) {
3251
+ revertThemePreview();
3252
+ _settingsState.selectedTheme = null;
3253
+ }
3254
+ document.getElementById('settings-overlay').classList.remove('open');
3255
+ document.getElementById('settings-panel').classList.remove('open');
3256
+ }
3257
+
3258
+ function renderThemeGrid() {
3259
+ var grid = document.getElementById('theme-grid');
3260
+ var info = document.getElementById('settings-theme-info');
3261
+ var active = getActiveTheme();
3262
+ var selected = _settingsState.selectedTheme;
3263
+
3264
+ if (info) {
3265
+ var displayId = selected || active;
3266
+ var displayTheme = THEMES[displayId];
3267
+ info.textContent = 'Active: ' + (THEMES[active] ? THEMES[active].name : active) +
3268
+ (selected && selected !== active ? ' → Preview: ' + (displayTheme ? displayTheme.name : selected) : '');
3269
+ }
3270
+
3271
+ var html = '';
3272
+ THEME_LIST.forEach(function(t) {
3273
+ var theme = THEMES[t.id];
3274
+ var v = theme.vars;
3275
+ var isSelected = selected ? t.id === selected : t.id === active;
3276
+ var cls = isSelected ? ' active' : '';
3277
+ html += '<div class="theme-card' + cls + '" data-theme="' + t.id + '">' +
3278
+ '<div class="theme-card-preview">' +
3279
+ '<span style="background:' + v.bg + '"></span>' +
3280
+ '<span style="background:' + v.surface + '"></span>' +
3281
+ '<span style="background:' + v.accent + '"></span>' +
3282
+ '<span style="background:' + v.success + '"></span>' +
3283
+ '<span style="background:' + v.warning + '"></span>' +
3284
+ '</div>' +
3285
+ '<div class="theme-card-name">' + escHtml(theme.name) + '</div>' +
3286
+ '<div class="theme-card-type">' + theme.type + '</div>' +
3287
+ '</div>';
3288
+ });
3289
+ grid.innerHTML = html;
3290
+ }
3291
+
3292
+ function saveTheme() {
3293
+ var themeId = _settingsState.selectedTheme;
3294
+ if (!themeId) {
3295
+ closeSettings();
3296
+ return;
3297
+ }
3298
+ var toggle = document.getElementById('theme-default-toggle');
3299
+ var setDefault = toggle ? toggle.checked : false;
3300
+ var css = generateThemeCss(themeId);
3301
+
3302
+ fetch('/api/theme', {
3303
+ method: 'POST',
3304
+ headers: { 'Content-Type': 'application/json' },
3305
+ body: JSON.stringify({ themeId: themeId, css: css, setDefault: setDefault })
3306
+ }).then(function(r) { return r.json(); }).then(function(data) {
3307
+ if (data && data.ok) {
3308
+ revertThemePreview();
3309
+ reloadCustomCss();
3310
+ D.config.theme = themeId;
3311
+ _settingsState.selectedTheme = null;
3312
+ showToast('Theme saved: ' + (THEMES[themeId] ? THEMES[themeId].name : themeId), 'success');
3313
+ closeSettings();
3314
+ } else {
3315
+ showToast('Error: ' + (data.error || 'Unknown'), 'error');
3316
+ }
3317
+ }).catch(function(e) {
3318
+ showToast('Error: ' + e.message, 'error');
3319
+ });
3320
+ }
3321
+
3322
+ document.getElementById('settings-toggle').addEventListener('click', function(e) {
3323
+ e.preventDefault();
3324
+ openSettings();
3325
+ });
3326
+
3327
+ document.getElementById('settings-close').addEventListener('click', closeSettings);
3328
+ document.getElementById('settings-overlay').addEventListener('click', closeSettings);
3329
+
3330
+ document.getElementById('theme-grid').addEventListener('click', function(e) {
3331
+ var card = e.target.closest('.theme-card');
3332
+ if (!card) return;
3333
+ var themeId = card.dataset.theme;
3334
+ _settingsState.selectedTheme = themeId;
3335
+ applyTheme(themeId);
3336
+ renderThemeGrid();
3337
+ });
3338
+
3339
+ document.getElementById('settings-save').addEventListener('click', saveTheme);
3340
+
1192
3341
  /* ══════════════════════════════════════════════════════════════
1193
3342
  INIT
1194
3343
  ══════════════════════════════════════════════════════════════ */
@@ -1196,8 +3345,69 @@ window.addEventListener('hashchange', handleHash);
1196
3345
  handleHash();
1197
3346
  await loadAll();
1198
3347
  initFromConfig();
3348
+
3349
+ // Sidebar logo always shows project name
3350
+ renderSidebarLogo();
3351
+
3352
+ // Workspace detection: build source rail + restore last source
3353
+ if (hasWorkspace()) {
3354
+ // Create overview tab (hidden by default)
3355
+ var overviewTab = document.querySelector('#sidebar-nav a[data-view="overview"]');
3356
+ if (!overviewTab) {
3357
+ var nav = document.getElementById('sidebar-nav');
3358
+ var metricsLink = document.querySelector('#sidebar-nav a[data-view="metrics"]');
3359
+ var overviewLink = document.createElement('a');
3360
+ overviewLink.href = '#overview';
3361
+ overviewLink.dataset.view = 'overview';
3362
+ overviewLink.style.display = 'none';
3363
+ overviewLink.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="9"/><path d="M12 3v18M3 12h18"/></svg><span>Overview</span>';
3364
+ if (metricsLink && metricsLink.nextSibling) {
3365
+ nav.insertBefore(overviewLink, metricsLink.nextSibling);
3366
+ } else {
3367
+ nav.appendChild(overviewLink);
3368
+ }
3369
+ }
3370
+
3371
+ // Restore last source or default to overview
3372
+ var savedSource = null;
3373
+ try { savedSource = localStorage.getItem('mdboard-source'); } catch(e) {}
3374
+ var validSource = savedSource && (savedSource === 'overview' || D.config.workspace.sources.some(function(s) { return s.name === savedSource; }));
3375
+ var initialSource = validSource ? savedSource : 'overview';
3376
+
3377
+ buildSourceRail();
3378
+ // switchSource skips if same as current, so set activeSource first then trigger
3379
+ D.activeSource = null;
3380
+ switchSource(initialSource);
3381
+ // Wait for switchSource data reload before continuing
3382
+ await loadAll();
3383
+ initFromConfig();
3384
+ }
3385
+
1199
3386
  renderAll();
1200
3387
  connectSSE();
3388
+
3389
+ // Auto-open history modal if no project
3390
+ checkAutoOpenHistory();
3391
+
3392
+ // Keyboard shortcut: Ctrl/Cmd+K to open history modal
3393
+ document.addEventListener('keydown', function(e) {
3394
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
3395
+ e.preventDefault();
3396
+ openHistoryModal();
3397
+ }
3398
+ });
3399
+
3400
+ // Load AI suggestions for autocomplete
3401
+ fetchJson('/api/ai-suggestions').then(function(data) {
3402
+ D.aiSuggestions = data || { skills: [], agents: [], mcps: [], commands: [], context: [] };
3403
+ });
3404
+
3405
+ // Load overview links in background for reverse link display
3406
+ if (hasWorkspace()) {
3407
+ fetchJson('/api/overview/links').then(function(data) {
3408
+ D.overviewLinks = data;
3409
+ });
3410
+ }
1201
3411
  })();
1202
3412
  </script>
1203
3413
  </body>