mdboard 1.3.0 → 2.1.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/bin.js +117 -59
- package/index.html +2161 -1579
- package/package.json +7 -5
- package/presets/kanban/api.json +91 -0
- package/presets/kanban/cli.json +69 -0
- package/presets/kanban/docs.json +29 -0
- package/presets/kanban/entities.json +128 -0
- package/presets/kanban/structure.json +15 -0
- package/presets/kanban/ui.json +86 -0
- package/presets/scrum/api.json +98 -0
- package/presets/scrum/cli.json +120 -0
- package/presets/scrum/docs.json +43 -0
- package/presets/scrum/entities.json +268 -0
- package/presets/scrum/structure.json +32 -0
- package/presets/scrum/ui.json +201 -0
- package/presets/shape-up/api.json +40 -0
- package/presets/shape-up/cli.json +44 -0
- package/presets/shape-up/docs.json +32 -0
- package/presets/shape-up/entities.json +140 -0
- package/presets/shape-up/structure.json +28 -0
- package/presets/shape-up/ui.json +114 -0
- package/src/cli/cli.js +186 -210
- package/src/cli/config.js +234 -0
- package/src/cli/init.js +128 -76
- package/src/cli/preset.js +849 -0
- package/src/cli/skill.js +417 -0
- package/src/cli/status.js +126 -96
- package/src/core/config.js +491 -38
- package/src/core/history.js +17 -1
- package/src/core/scanner.js +373 -463
- package/src/core/workspace.js +0 -15
- package/src/server/api.js +464 -741
- package/src/server/server.js +105 -130
- package/build.js +0 -44
- package/defaults.json +0 -43
- package/src/cli/sync.js +0 -194
- package/src/cli/theme.js +0 -142
- package/src/client/app.js +0 -266
- package/src/client/board.js +0 -157
- package/src/client/core.js +0 -331
- package/src/client/editor.js +0 -318
- package/src/client/history.js +0 -137
- package/src/client/metrics.js +0 -38
- package/src/client/milestones.js +0 -77
- package/src/client/notes.js +0 -183
- package/src/client/overview.js +0 -104
- package/src/client/panel.js +0 -637
- package/src/client/styles.css +0 -471
- package/src/client/table.js +0 -111
- package/src/client/template.html +0 -144
- package/src/client/themes.js +0 -261
- package/src/client/workspace.js +0 -164
- package/src/core/agent-scanner.js +0 -260
package/index.html
CHANGED
|
@@ -67,31 +67,45 @@ a{color:var(--accent);text-decoration:none}
|
|
|
67
67
|
.stat-val{font-size:20px;font-weight:700;font-family:var(--mono);line-height:1.2}
|
|
68
68
|
.stat-label{font-size:10px;text-transform:uppercase;letter-spacing:.04em;color:var(--text3);font-weight:500}
|
|
69
69
|
|
|
70
|
+
/* ── Global Search ───────────────────────────────────────── */
|
|
71
|
+
.global-search-wrap{position:relative;flex:1;max-width:400px}
|
|
72
|
+
.global-search-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);width:16px;height:16px;color:var(--text3);pointer-events:none}
|
|
73
|
+
.global-search-input{width:100%;padding:8px 12px 8px 34px;font-size:13px;border:1px solid var(--border);border-radius:8px;background:var(--surface2);color:var(--text);outline:none;font-family:inherit;transition:border-color .2s,box-shadow .2s}
|
|
74
|
+
.global-search-input::placeholder{color:var(--text3)}
|
|
75
|
+
.global-search-input:focus{border-color:var(--accent);box-shadow:0 0 0 2px color-mix(in srgb, var(--accent) 20%, transparent)}
|
|
76
|
+
.global-search-results{display:none;position:absolute;top:calc(100% + 4px);left:0;right:0;background:var(--surface);border:1px solid var(--border);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.3);max-height:360px;overflow-y:auto;z-index:100}
|
|
77
|
+
.gs-item{display:flex;align-items:center;gap:8px;padding:8px 12px;cursor:pointer;font-size:13px;transition:background .1s}
|
|
78
|
+
.gs-item:hover,.gs-item.active{background:var(--surface2)}
|
|
79
|
+
.gs-item .icon{width:16px;height:16px;flex-shrink:0}
|
|
80
|
+
.gs-title{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)}
|
|
81
|
+
.gs-meta{font-size:11px;color:var(--text3);font-family:var(--mono);flex-shrink:0}
|
|
82
|
+
.gs-type{font-size:10px;text-transform:uppercase;letter-spacing:.04em;color:var(--text3);background:var(--surface2);padding:2px 6px;border-radius:4px;flex-shrink:0}
|
|
83
|
+
.gs-empty{padding:16px;text-align:center;color:var(--text3);font-size:13px}
|
|
84
|
+
|
|
70
85
|
/* ── Content ─────────────────────────────────────────────── */
|
|
71
86
|
.content{flex:1;overflow-y:auto;padding:20px 24px}
|
|
72
87
|
.view{display:none}
|
|
73
88
|
.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}
|
|
76
89
|
|
|
77
90
|
/* ── Icons ───────────────────────────────────────────────── */
|
|
78
91
|
.icon{width:16px;height:16px;flex-shrink:0;vertical-align:middle;display:inline-block}
|
|
79
92
|
.icon-sm{width:14px;height:14px}
|
|
80
93
|
|
|
81
|
-
/* ── Filters
|
|
82
|
-
.
|
|
83
|
-
.
|
|
84
|
-
.
|
|
85
|
-
.
|
|
86
|
-
.
|
|
87
|
-
|
|
94
|
+
/* ── View Toolbar & Filters ─────────────────────────────── */
|
|
95
|
+
.view-toolbar{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap;align-items:center}
|
|
96
|
+
.view-toolbar input,.view-toolbar select{background:var(--surface);border:1px solid var(--border);color:var(--text);padding:6px 10px;border-radius:var(--radius-sm);font-size:12px;font-family:var(--font)}
|
|
97
|
+
.view-toolbar input{min-width:160px;flex:1;max-width:280px}
|
|
98
|
+
.view-toolbar select{min-width:110px}
|
|
99
|
+
.view-toolbar input:focus,.view-toolbar select:focus{outline:none;border-color:var(--accent)}
|
|
100
|
+
|
|
101
|
+
/* ── Layout Switcher ────────────────────────────────────── */
|
|
102
|
+
.layout-switcher{display:flex;gap:2px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);padding:2px}
|
|
103
|
+
.layout-btn{padding:4px 12px;border:none;background:transparent;color:var(--text2);font-size:12px;font-weight:500;cursor:pointer;border-radius:var(--radius-sm);font-family:var(--font);transition:all .15s}
|
|
104
|
+
.layout-btn:hover{color:var(--text)}
|
|
105
|
+
.layout-btn.active{background:var(--accent-dim);color:var(--accent)}
|
|
88
106
|
|
|
89
107
|
/* ── Board View ──────────────────────────────────────────── */
|
|
90
|
-
|
|
91
|
-
.sprint-goal{flex:1}
|
|
92
|
-
.sprint-goal b{color:var(--text)}
|
|
93
|
-
.sprint-dates{font-size:12px;color:var(--text2);font-family:var(--mono)}
|
|
94
|
-
#board{display:flex;gap:12px;min-height:400px;overflow-x:auto;padding-bottom:16px}
|
|
108
|
+
.board-container{display:flex;gap:12px;min-height:400px;overflow-x:auto;padding-bottom:16px}
|
|
95
109
|
.column{flex:1;min-width:220px;max-width:300px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);display:flex;flex-direction:column;transition:border-color .2s,background .2s}
|
|
96
110
|
.column.drag-over{border-color:var(--accent);background:var(--accent-dim)}
|
|
97
111
|
.column-header{padding:12px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;gap:8px}
|
|
@@ -114,7 +128,6 @@ a{color:var(--accent);text-decoration:none}
|
|
|
114
128
|
.pill-points{background:var(--surface2);color:var(--text2);font-family:var(--mono)}
|
|
115
129
|
.pill-epic{color:#fff;font-size:10px;font-weight:600}
|
|
116
130
|
.badge{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:var(--radius-sm);font-size:11px;font-weight:600;text-transform:capitalize}
|
|
117
|
-
/* Fallback badge styles — overridden by dynamic styles once config loads */
|
|
118
131
|
.badge-backlog{background:var(--surface2);color:var(--text2)}
|
|
119
132
|
.badge-todo{background:var(--accent-dim);color:var(--accent)}
|
|
120
133
|
.badge-in-progress{background:var(--warning-dim);color:var(--warning)}
|
|
@@ -139,32 +152,42 @@ a{color:var(--accent);text-decoration:none}
|
|
|
139
152
|
th.sorted .sort-arrow{opacity:1;color:var(--accent)}
|
|
140
153
|
.ftable td{padding:8px 12px;border-bottom:1px solid var(--border)}
|
|
141
154
|
.ftable tr:hover td{background:var(--surface)}
|
|
142
|
-
.ftable tr.expanded td{background:var(--surface);border-bottom-color:transparent}
|
|
143
155
|
.td-id{font-family:var(--mono);font-size:12px;color:var(--text2);white-space:nowrap}
|
|
144
156
|
.td-title{cursor:pointer;font-weight:500}
|
|
145
157
|
.td-title:hover{color:var(--accent)}
|
|
146
|
-
.detail-row td{background:var(--surface)!important;padding:16px 24px}
|
|
147
|
-
.detail-content{font-size:13px;line-height:1.6}
|
|
148
|
-
.detail-content h4{font-size:12px;text-transform:uppercase;letter-spacing:.04em;color:var(--text2);margin-bottom:8px}
|
|
149
|
-
.detail-content ul{padding-left:20px;margin-top:4px}
|
|
150
|
-
.detail-content li{margin-bottom:4px;color:var(--text2)}
|
|
151
158
|
.clickable-row{cursor:pointer}
|
|
152
159
|
.clickable-row:hover td{background:var(--accent-dim)!important}
|
|
153
160
|
|
|
154
|
-
/* ──
|
|
155
|
-
.
|
|
156
|
-
.
|
|
157
|
-
.
|
|
158
|
-
.
|
|
159
|
-
.
|
|
160
|
-
.
|
|
161
|
-
.
|
|
162
|
-
.
|
|
163
|
-
.
|
|
164
|
-
.
|
|
165
|
-
.
|
|
166
|
-
.
|
|
167
|
-
.
|
|
161
|
+
/* ── Cards View (new) ───────────────────────────────────── */
|
|
162
|
+
.cards-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:16px}
|
|
163
|
+
.entity-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px;cursor:pointer;transition:border-color .15s}
|
|
164
|
+
.entity-card:hover{border-color:var(--border2)}
|
|
165
|
+
.entity-card-header{display:flex;align-items:center;gap:8px;margin-bottom:8px}
|
|
166
|
+
.entity-card-id{font-family:var(--mono);font-size:11px;color:var(--text3)}
|
|
167
|
+
.entity-card-title{font-size:15px;font-weight:600;margin-bottom:10px;line-height:1.4}
|
|
168
|
+
.entity-card-meta{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px}
|
|
169
|
+
.entity-card-progress{margin-top:8px}
|
|
170
|
+
.entity-card-children{margin-top:12px;border-top:1px solid var(--border);padding-top:10px;display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:8px}
|
|
171
|
+
.child-card{background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);padding:8px 10px;cursor:pointer;transition:border-color .15s}
|
|
172
|
+
.child-card:hover{border-color:var(--border2)}
|
|
173
|
+
.child-card-header{display:flex;align-items:center;gap:6px;margin-bottom:4px}
|
|
174
|
+
.child-card-title{font-size:12px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
175
|
+
|
|
176
|
+
/* ── List View (new) ────────────────────────────────────── */
|
|
177
|
+
.entity-list{display:flex;flex-direction:column;gap:2px}
|
|
178
|
+
.entity-list-item{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);padding:0;cursor:pointer;transition:border-color .15s}
|
|
179
|
+
.entity-list-item:hover{border-color:var(--border2)}
|
|
180
|
+
.list-item-main{display:flex;align-items:center;gap:10px;padding:12px 16px}
|
|
181
|
+
.list-item-status{flex-shrink:0}
|
|
182
|
+
.list-item-title{font-size:14px;font-weight:600;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
183
|
+
.list-item-fields{display:flex;gap:6px;align-items:center;flex-shrink:0}
|
|
184
|
+
.list-item-progress{display:flex;align-items:center;gap:6px}
|
|
185
|
+
.list-expand-btn{background:none;border:none;color:var(--text3);cursor:pointer;font-size:10px;padding:2px 4px;border-radius:var(--radius-sm);width:20px;flex-shrink:0}
|
|
186
|
+
.list-expand-btn:hover{color:var(--text);background:var(--surface2)}
|
|
187
|
+
.list-children{padding:4px 16px 12px 46px;display:flex;flex-direction:column;gap:4px}
|
|
188
|
+
.child-list-item{display:flex;align-items:center;gap:8px;padding:6px 10px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;font-size:13px;transition:border-color .15s}
|
|
189
|
+
.child-list-item:hover{border-color:var(--border2)}
|
|
190
|
+
.child-list-title{font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
|
|
168
191
|
|
|
169
192
|
/* ── Metrics View ────────────────────────────────────────── */
|
|
170
193
|
.metrics-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px}
|
|
@@ -179,21 +202,17 @@ th.sorted .sort-arrow{opacity:1;color:var(--accent)}
|
|
|
179
202
|
/* ── Detail Panel — Notion-style ─────────────────────────── */
|
|
180
203
|
.panel-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:99;opacity:0;pointer-events:none;transition:opacity .2s}
|
|
181
204
|
.panel-overlay.open{opacity:1;pointer-events:auto}
|
|
182
|
-
.detail-panel{
|
|
205
|
+
.detail-panel{top: 1rem;right: 1rem;height: calc(100vh - 2rem);border-radius: var(--radius);position:fixed;width:600px;max-width:92vw;background:var(--bg);border-left:1px solid var(--border);z-index:100;transform:translateX(100%);transition:transform .25s ease;display:flex;flex-direction:column;overflow:hidden}
|
|
183
206
|
.detail-panel.open{transform:translateX(0)}
|
|
184
207
|
.panel-header{padding:12px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;flex-shrink:0;background:var(--surface)}
|
|
185
208
|
.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}
|
|
186
209
|
.panel-item-id{font-family:var(--mono);font-size:13px;color:var(--text2)}
|
|
187
210
|
.panel-close{margin-left:auto;background:none;border:none;color:var(--text2);cursor:pointer;font-size:18px;padding:4px 8px;border-radius:var(--radius-sm);line-height:1}
|
|
188
211
|
.panel-close:hover{background:var(--surface2);color:var(--text)}
|
|
189
|
-
|
|
190
|
-
/* Panel title — large Notion-style */
|
|
191
212
|
.panel-title-wrap{padding:20px 24px 8px;flex-shrink:0}
|
|
192
213
|
.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
214
|
.panel-title-input::placeholder{color:var(--text3)}
|
|
194
215
|
.panel-title-input:disabled{opacity:.7}
|
|
195
|
-
|
|
196
|
-
/* Panel properties — compact Notion-style rows */
|
|
197
216
|
.panel-properties{padding:4px 24px 8px;flex-shrink:0;border-bottom:1px solid var(--border)}
|
|
198
217
|
.prop-row{display:flex;align-items:center;padding:4px 0;min-height:32px}
|
|
199
218
|
.prop-label{width:100px;flex-shrink:0;font-size:12px;color:var(--text3);font-weight:500}
|
|
@@ -204,31 +223,20 @@ th.sorted .sort-arrow{opacity:1;color:var(--accent)}
|
|
|
204
223
|
.prop-value select:disabled,.prop-value input:disabled{opacity:.6;cursor:default}
|
|
205
224
|
.prop-value select:disabled:hover,.prop-value input:disabled:hover{border-color:transparent}
|
|
206
225
|
.prop-text{font-size:13px;color:var(--text2);padding:4px 8px}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
.prop-
|
|
226
|
+
.prop-longtext{width:100%}
|
|
227
|
+
.prop-longtext-preview{display:flex;align-items:center;gap:6px;cursor:pointer;padding:4px 8px;border-radius:var(--radius-sm);transition:background .15s;min-height:28px}
|
|
228
|
+
.prop-longtext-preview:hover{background:var(--surface2)}
|
|
229
|
+
.prop-longtext-text{flex:1;font-size:13px;color:var(--text2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
230
|
+
.prop-longtext-toggle{font-size:9px;color:var(--text3);flex-shrink:0;transition:transform .15s}
|
|
231
|
+
.prop-longtext-editor{width:100%;min-height:80px;resize:vertical;background:var(--surface);border:1px solid var(--border);color:var(--text);padding:8px;border-radius:var(--radius-sm);font-family:var(--font);font-size:13px;line-height:1.5;outline:none;margin-top:4px;transition:border-color .2s}
|
|
232
|
+
.prop-longtext-editor:focus{border-color:var(--accent)}
|
|
233
|
+
.prop-longtext-editor:disabled{opacity:.6;cursor:default}
|
|
234
|
+
.prop-icon{flex-shrink:0}
|
|
210
235
|
.prop-row-toggle{cursor:pointer;user-select:none}
|
|
211
236
|
.prop-row-toggle:hover{background:var(--surface2);border-radius:var(--radius-sm)}
|
|
212
237
|
.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
238
|
.panel-content-area{flex:1;overflow-y:auto;padding:0}
|
|
230
239
|
.panel-editor-zone{padding:16px 24px;min-height:200px}
|
|
231
|
-
|
|
232
240
|
.panel-footer{padding:12px 20px;border-top:1px solid var(--border);display:flex;gap:8px;align-items:center;flex-shrink:0}
|
|
233
241
|
.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)}
|
|
234
242
|
.btn:hover{background:var(--border)}
|
|
@@ -254,30 +262,13 @@ th.sorted .sort-arrow{opacity:1;color:var(--accent)}
|
|
|
254
262
|
.loading-container{padding:20px}
|
|
255
263
|
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
|
256
264
|
|
|
257
|
-
/* ── Workspace: Source Icon
|
|
265
|
+
/* ── Workspace: Source Icon ──────────────────────────────── */
|
|
258
266
|
.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
267
|
|
|
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
268
|
/* ── CRUD Buttons ────────────────────────────────────────── */
|
|
267
269
|
.btn-sm{padding:5px 12px;font-size:12px;border-radius:var(--radius-sm)}
|
|
268
270
|
.btn-create{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:600}
|
|
269
271
|
.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
272
|
|
|
282
273
|
/* ── Notes View — Notion-style ───────────────────────────── */
|
|
283
274
|
.notes-layout{display:flex;height:100%;gap:0}
|
|
@@ -313,25 +304,21 @@ th.sorted .sort-arrow{opacity:1;color:var(--accent)}
|
|
|
313
304
|
.codex-editor .ce-toolbar__settings-btn{color:var(--text3);background:transparent;width:26px;height:26px}
|
|
314
305
|
.codex-editor .ce-toolbar__settings-btn:hover{background:var(--surface2);color:var(--text)}
|
|
315
306
|
.codex-editor .ce-toolbar__actions{background:transparent}
|
|
316
|
-
/* Inline toolbar */
|
|
317
307
|
.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
308
|
.codex-editor .ce-inline-toolbar__buttons{color:var(--text)}
|
|
319
309
|
.codex-editor .ce-inline-tool{color:var(--text2)}
|
|
320
310
|
.codex-editor .ce-inline-tool:hover{color:var(--text);background:var(--surface2)}
|
|
321
311
|
.codex-editor .ce-inline-tool--active{color:var(--accent)}
|
|
322
312
|
.codex-editor .ce-inline-tool-input{background:var(--bg);color:var(--text);border:none;border-top:1px solid var(--border)}
|
|
323
|
-
/* Conversion toolbar */
|
|
324
313
|
.codex-editor .ce-conversion-toolbar{background:var(--surface);border:1px solid var(--border);box-shadow:0 8px 24px rgba(0,0,0,.4)}
|
|
325
314
|
.codex-editor .ce-conversion-tool{color:var(--text2)}
|
|
326
315
|
.codex-editor .ce-conversion-tool:hover{background:var(--surface2);color:var(--text)}
|
|
327
316
|
.codex-editor .ce-conversion-tool--active{color:var(--accent)}
|
|
328
317
|
.codex-editor .ce-conversion-tool__icon{background:var(--surface2);color:var(--text2)}
|
|
329
|
-
/* Settings panel */
|
|
330
318
|
.codex-editor .ce-settings{background:var(--surface);border:1px solid var(--border);box-shadow:0 8px 24px rgba(0,0,0,.4)}
|
|
331
319
|
.codex-editor .cdx-settings-button{color:var(--text2)}
|
|
332
320
|
.codex-editor .cdx-settings-button:hover{background:var(--surface2);color:var(--text)}
|
|
333
321
|
.codex-editor .cdx-settings-button--active{color:var(--accent)}
|
|
334
|
-
/* Popover (slash commands menu) */
|
|
335
322
|
.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
323
|
.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
324
|
.ce-popover-item{color:var(--text2)!important}
|
|
@@ -359,12 +346,10 @@ h4.ce-header{font-size:15px;line-height:1.4}
|
|
|
359
346
|
.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
347
|
.cdx-list{font-size:15px;line-height:1.65}
|
|
361
348
|
.cdx-list__item{color:var(--text)}
|
|
362
|
-
/* Checklist in list v2 */
|
|
363
349
|
.cdx-checklist{font-size:15px}
|
|
364
350
|
.cdx-checklist__item-checkbox{background:var(--surface2);border:2px solid var(--border2);border-radius:3px}
|
|
365
351
|
.cdx-checklist__item--checked .cdx-checklist__item-checkbox{background:var(--accent);border-color:var(--accent)}
|
|
366
352
|
.cdx-checklist__item--checked .cdx-checklist__item-text{color:var(--text2);text-decoration:line-through}
|
|
367
|
-
/* List v2 checklist style */
|
|
368
353
|
.cdx-list--checklist .cdx-list__item-checkbox{background:var(--surface2);border-color:var(--border2)}
|
|
369
354
|
.cdx-list--checklist .cdx-list__item--checked .cdx-list__item-checkbox{background:var(--accent);border-color:var(--accent)}
|
|
370
355
|
.cdx-list--checklist .cdx-list__item--checked .cdx-list__item-content{color:var(--text2);text-decoration:line-through}
|
|
@@ -373,8 +358,6 @@ h4.ce-header{font-size:15px;line-height:1.4}
|
|
|
373
358
|
.cdx-quote__caption{display:none!important}
|
|
374
359
|
.ce-delimiter{line-height:1;padding:12px 0;color:var(--text3);letter-spacing:8px}
|
|
375
360
|
.cdx-marker{background:rgba(91,110,245,.2);padding:2px 0}
|
|
376
|
-
|
|
377
|
-
/* Fallback textarea (when CDN not available) */
|
|
378
361
|
.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
362
|
|
|
380
363
|
/* ── Markdown Rendered (readonly) ────────────────────────── */
|
|
@@ -450,6 +433,35 @@ h4.ce-header{font-size:15px;line-height:1.4}
|
|
|
450
433
|
.history-item-path{font-size:11px;color:var(--text3);font-family:var(--mono);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
451
434
|
.history-item-time{font-size:11px;color:var(--text3);font-family:var(--mono);flex-shrink:0;white-space:nowrap}
|
|
452
435
|
.history-current-badge{display:inline-block;padding:1px 8px;border-radius:10px;font-size:10px;font-weight:600;background:var(--accent);color:#fff}
|
|
436
|
+
.history-open-row{display:flex;gap:8px;padding:12px 20px;border-top:1px solid var(--border)}
|
|
437
|
+
.history-path-input{flex:1;padding:8px 12px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:13px;font-family:var(--font);outline:none;transition:border-color .2s}
|
|
438
|
+
.history-path-input:focus{border-color:var(--accent)}
|
|
439
|
+
.history-path-input::placeholder{color:var(--text3)}
|
|
440
|
+
|
|
441
|
+
/* ── Add Source Modal ───────────────────────────────────── */
|
|
442
|
+
.rail-add{background:transparent;color:var(--text3);border:2px dashed var(--border2);font-size:20px;font-weight:400}
|
|
443
|
+
.rail-add:hover{color:var(--accent);border-color:var(--accent);background:var(--accent-dim);border-style:solid}
|
|
444
|
+
.addsource-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:199;opacity:0;pointer-events:none;transition:opacity .2s}
|
|
445
|
+
.addsource-overlay.open{opacity:1;pointer-events:auto}
|
|
446
|
+
.addsource-modal{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%) scale(.95);width:520px;max-width:92vw;max-height:85vh;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)}
|
|
447
|
+
.addsource-modal.open{opacity:1;pointer-events:auto;transform:translate(-50%,-50%) scale(1)}
|
|
448
|
+
.addsource-header{padding:16px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
|
|
449
|
+
.addsource-header h2{font-size:16px;font-weight:700}
|
|
450
|
+
.addsource-body{flex:1;overflow-y:auto;padding:20px}
|
|
451
|
+
.addsource-type-toggle{display:flex;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:3px;margin-bottom:20px}
|
|
452
|
+
.addsource-type-btn{flex:1;padding:8px;border:none;background:transparent;color:var(--text2);font-size:13px;font-weight:600;cursor:pointer;border-radius:6px;font-family:var(--font);transition:all .15s}
|
|
453
|
+
.addsource-type-btn.active{background:var(--accent);color:#fff}
|
|
454
|
+
.addsource-field{margin-bottom:16px}
|
|
455
|
+
.addsource-field label{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--text3);font-weight:600;margin-bottom:6px}
|
|
456
|
+
.addsource-field input{width:100%;padding:8px 12px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:13px;font-family:var(--font);outline:none;transition:border-color .2s}
|
|
457
|
+
.addsource-field input:focus{border-color:var(--accent)}
|
|
458
|
+
.addsource-colors{display:flex;gap:8px;flex-wrap:wrap}
|
|
459
|
+
.addsource-color{width:28px;height:28px;border-radius:50%;cursor:pointer;border:3px solid transparent;transition:all .15s;flex-shrink:0}
|
|
460
|
+
.addsource-color:hover{transform:scale(1.15)}
|
|
461
|
+
.addsource-color.active{border-color:var(--text);transform:scale(1.15)}
|
|
462
|
+
.addsource-footer{padding:14px 20px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:8px;flex-shrink:0}
|
|
463
|
+
.addsource-error{color:var(--danger);font-size:12px;margin-bottom:12px;display:none}
|
|
464
|
+
.addsource-error.visible{display:block}
|
|
453
465
|
|
|
454
466
|
/* ── Responsive ──────────────────────────────────────────── */
|
|
455
467
|
@media(max-width:1024px){
|
|
@@ -463,19 +475,21 @@ h4.ce-header{font-size:15px;line-height:1.4}
|
|
|
463
475
|
.header{padding:12px 16px}
|
|
464
476
|
.content{padding:16px}
|
|
465
477
|
.detail-panel{width:100%;max-width:100%}
|
|
466
|
-
#view-notes.active{margin:-16px}
|
|
467
478
|
.notes-sidebar{width:200px}
|
|
468
479
|
.notes-header-bar{padding:24px 24px 0}
|
|
469
480
|
.notes-editorjs-wrap{padding:12px 16px 24px}
|
|
481
|
+
.cards-grid{grid-template-columns:1fr}
|
|
470
482
|
}
|
|
471
483
|
@media(max-width:768px){
|
|
472
484
|
#h-stats{flex-wrap:wrap;gap:12px}
|
|
473
485
|
.header{gap:12px}
|
|
474
486
|
.metrics-grid{grid-template-columns:1fr}
|
|
487
|
+
.cards-grid{grid-template-columns:1fr}
|
|
475
488
|
.notes-sidebar{display:none}
|
|
476
489
|
.notes-header-bar{padding:20px 16px 0}
|
|
477
490
|
.notes-editorjs-wrap{padding:8px 8px 24px}
|
|
478
491
|
}
|
|
492
|
+
|
|
479
493
|
</style>
|
|
480
494
|
<style id="dynamic-styles"></style>
|
|
481
495
|
<style id="theme-styles"></style>
|
|
@@ -483,7 +497,7 @@ h4.ce-header{font-size:15px;line-height:1.4}
|
|
|
483
497
|
</head>
|
|
484
498
|
<body>
|
|
485
499
|
<div class="app">
|
|
486
|
-
<!-- Source Rail
|
|
500
|
+
<!-- Source Rail -->
|
|
487
501
|
<nav class="source-rail" id="source-rail"></nav>
|
|
488
502
|
|
|
489
503
|
<!-- Sidebar -->
|
|
@@ -492,78 +506,16 @@ h4.ce-header{font-size:15px;line-height:1.4}
|
|
|
492
506
|
<span class="sidebar-logo-icon" id="sidebar-logo-icon">■</span>
|
|
493
507
|
<span class="sidebar-logo-text" id="sidebar-logo-text">mdboard</span>
|
|
494
508
|
</div>
|
|
495
|
-
<nav id="sidebar-nav">
|
|
496
|
-
<a href="#board" data-view="board" class="active">
|
|
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>
|
|
498
|
-
<span>Board</span>
|
|
499
|
-
</a>
|
|
500
|
-
<a href="#table" data-view="table">
|
|
501
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
|
502
|
-
<span>Table</span>
|
|
503
|
-
</a>
|
|
504
|
-
<a href="#milestones" data-view="milestones">
|
|
505
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/></svg>
|
|
506
|
-
<span>Milestones</span>
|
|
507
|
-
</a>
|
|
508
|
-
<a href="#metrics" data-view="metrics">
|
|
509
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M18 20V10M12 20V4M6 20v-6"/></svg>
|
|
510
|
-
<span>Metrics</span>
|
|
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>
|
|
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>
|
|
509
|
+
<nav id="sidebar-nav"></nav>
|
|
523
510
|
<div class="sidebar-footer">mdboard</div>
|
|
524
511
|
</aside>
|
|
525
512
|
|
|
526
513
|
<div class="main">
|
|
527
514
|
<!-- Header -->
|
|
528
|
-
<header class="header">
|
|
529
|
-
<div class="header-section" id="h-milestone-wrap" style="display:none">
|
|
530
|
-
<span class="header-section-label">Milestone</span>
|
|
531
|
-
<span class="header-section-value" id="h-milestone-name"></span>
|
|
532
|
-
<div class="progress progress-accent"><div class="progress-fill" id="h-milestone-progress" style="width:0%"></div></div>
|
|
533
|
-
</div>
|
|
534
|
-
<div class="header-section" id="h-sprint-wrap" style="display:none">
|
|
535
|
-
<span class="header-section-label">Sprint</span>
|
|
536
|
-
<span class="header-section-value" id="h-sprint-name"></span>
|
|
537
|
-
<span style="font-size:11px;color:var(--text2)" id="h-sprint-days"></span>
|
|
538
|
-
</div>
|
|
539
|
-
<div id="h-stats"></div>
|
|
540
|
-
</header>
|
|
515
|
+
<header class="header" id="app-header"></header>
|
|
541
516
|
|
|
542
517
|
<!-- Content -->
|
|
543
|
-
<div class="content">
|
|
544
|
-
<div id="view-board" class="view active">
|
|
545
|
-
<div id="sprint-bar" style="display:none"></div>
|
|
546
|
-
<div id="board-filters" class="filter-bar"></div>
|
|
547
|
-
<div id="board"></div>
|
|
548
|
-
</div>
|
|
549
|
-
<div id="view-table" class="view">
|
|
550
|
-
<div id="table-controls" class="filter-bar"></div>
|
|
551
|
-
<div id="table-wrapper"></div>
|
|
552
|
-
</div>
|
|
553
|
-
<div id="view-milestones" class="view">
|
|
554
|
-
<div id="ms-filters" class="filter-bar"></div>
|
|
555
|
-
<div id="milestones-container"></div>
|
|
556
|
-
</div>
|
|
557
|
-
<div id="view-metrics" class="view">
|
|
558
|
-
<div id="metrics-container"></div>
|
|
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>
|
|
566
|
-
</div>
|
|
518
|
+
<div class="content" id="app-content"></div>
|
|
567
519
|
</div>
|
|
568
520
|
</div>
|
|
569
521
|
|
|
@@ -599,6 +551,10 @@ h4.ce-header{font-size:15px;line-height:1.4}
|
|
|
599
551
|
<div id="history-overlay" class="history-overlay"></div>
|
|
600
552
|
<div id="history-modal" class="history-modal"></div>
|
|
601
553
|
|
|
554
|
+
<!-- Add Source Modal -->
|
|
555
|
+
<div id="addsource-overlay" class="addsource-overlay"></div>
|
|
556
|
+
<div id="addsource-modal" class="addsource-modal"></div>
|
|
557
|
+
|
|
602
558
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@2/dist/editorjs.umd.js"></script>
|
|
603
559
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest/dist/header.umd.js"></script>
|
|
604
560
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/list@latest/dist/editorjs-list.umd.js"></script>
|
|
@@ -881,21 +837,110 @@ var ICON_SHAPES = {
|
|
|
881
837
|
'three-quarter-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5"/><path d="M8 2A6 6 0 1 1 2 8L8 8Z" fill="' + c + '"/></svg>'; },
|
|
882
838
|
'check-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16"><circle cx="8" cy="8" r="7" fill="' + c + '"/><path d="M5 8.5L7 10.5L11 5.5" stroke="#fff" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>'; },
|
|
883
839
|
'x-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5"/><path d="M5.5 5.5L10.5 10.5M10.5 5.5L5.5 10.5" stroke="' + c + '" stroke-width="1.5" stroke-linecap="round"/></svg>'; },
|
|
884
|
-
'slash-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5"/><path d="M4.5 11.5L11.5 4.5" stroke="' + c + '" stroke-width="1.5" stroke-linecap="round"/></svg>'; }
|
|
840
|
+
'slash-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5"/><path d="M4.5 11.5L11.5 4.5" stroke="' + c + '" stroke-width="1.5" stroke-linecap="round"/></svg>'; },
|
|
841
|
+
'pause-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5"/><path d="M6.5 5.5v5M9.5 5.5v5" stroke="' + c + '" stroke-width="1.5" stroke-linecap="round"/></svg>'; }
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
/* Sidebar tab icons */
|
|
845
|
+
var TAB_ICONS = {
|
|
846
|
+
'check-square': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="3"/><path d="M9 12l2 2 4-4"/></svg>',
|
|
847
|
+
'target': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1"/></svg>',
|
|
848
|
+
'refresh-cw': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></svg>',
|
|
849
|
+
'lightbulb': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 18h6M10 22h4"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A7 7 0 1 0 7.5 11.5c.76.76 1.23 1.52 1.41 2.5"/></svg>',
|
|
850
|
+
'file-text': '<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>',
|
|
851
|
+
'bar-chart': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M18 20V10M12 20V4M6 20v-6"/></svg>',
|
|
852
|
+
'board': '<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>',
|
|
853
|
+
'table': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>',
|
|
854
|
+
'overview': '<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>'
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
/* ══════════════════════════════════════════════════════════════
|
|
858
|
+
DATA STORE — fully dynamic, config-driven
|
|
859
|
+
══════════════════════════════════════════════════════════════ */
|
|
860
|
+
var D = {
|
|
861
|
+
config: null,
|
|
862
|
+
project: null,
|
|
863
|
+
entities: {},
|
|
864
|
+
metrics: null,
|
|
865
|
+
health: null,
|
|
866
|
+
loaded: false,
|
|
867
|
+
sources: [],
|
|
868
|
+
activeSource: null,
|
|
869
|
+
theme: null,
|
|
870
|
+
activeTab: null,
|
|
871
|
+
activeLayouts: {},
|
|
872
|
+
tabFilters: {},
|
|
873
|
+
tabSort: {},
|
|
885
874
|
};
|
|
886
875
|
|
|
887
|
-
/*
|
|
888
|
-
var
|
|
889
|
-
|
|
890
|
-
|
|
876
|
+
/* ── Helpers ─────────────────────────────────────────────── */
|
|
877
|
+
function escHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
878
|
+
|
|
879
|
+
function hashColor(name) {
|
|
880
|
+
var colors = ['#5B6EF5','#8B5CF6','#EC4899','#F59E0B','#10B981','#06B6D4','#F97316','#6366F1'];
|
|
881
|
+
var hash = 0;
|
|
882
|
+
for (var i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
|
883
|
+
return colors[Math.abs(hash) % colors.length];
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function badgeClass(prefix, value) {
|
|
887
|
+
if (!value) return prefix + '-backlog';
|
|
888
|
+
return prefix + '-' + value.toLowerCase().replace(/\s+/g, '-');
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function fmtDate(d) { return d ? String(d).substring(0, 10) : ''; }
|
|
892
|
+
|
|
893
|
+
/* ── Toast ───────────────────────────────────────────────── */
|
|
894
|
+
function showToast(msg, type) {
|
|
895
|
+
var el = document.createElement('div');
|
|
896
|
+
el.className = 'toast toast-' + (type || 'success');
|
|
897
|
+
el.textContent = msg;
|
|
898
|
+
document.body.appendChild(el);
|
|
899
|
+
setTimeout(function() { el.remove(); }, 3000);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/* ── Status & Priority Icons ──────────────────────────────── */
|
|
903
|
+
function statusIcon(entityType, statusKey) {
|
|
904
|
+
if (!D.config || !D.config.entities) return ICON_SHAPES['dashed-circle']('#5A5A63');
|
|
905
|
+
var entity = D.config.entities[entityType];
|
|
906
|
+
if (!entity || !entity.fields) return ICON_SHAPES['dashed-circle']('#5A5A63');
|
|
907
|
+
|
|
908
|
+
// Try status field first, then check all enum fields
|
|
909
|
+
var fields = entity.fields;
|
|
910
|
+
for (var fname in fields) {
|
|
911
|
+
var f = fields[fname];
|
|
912
|
+
if (f.type === 'enum' && f.values) {
|
|
913
|
+
for (var i = 0; i < f.values.length; i++) {
|
|
914
|
+
if (f.values[i].key === statusKey) {
|
|
915
|
+
var v = f.values[i];
|
|
916
|
+
var fn = ICON_SHAPES[v.icon || 'dashed-circle'];
|
|
917
|
+
return fn ? fn(v.color || '#5A5A63') : ICON_SHAPES['dashed-circle'](v.color || '#5A5A63');
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
return ICON_SHAPES['dashed-circle']('#5A5A63');
|
|
923
|
+
}
|
|
891
924
|
|
|
892
|
-
function
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
var
|
|
896
|
-
if (
|
|
925
|
+
function statusIconByKey(statusKey) {
|
|
926
|
+
if (!D.config || !D.config.entities) return ICON_SHAPES['dashed-circle']('#5A5A63');
|
|
927
|
+
for (var type in D.config.entities) {
|
|
928
|
+
var entity = D.config.entities[type];
|
|
929
|
+
if (entity.fields) {
|
|
930
|
+
for (var fname in entity.fields) {
|
|
931
|
+
var f = entity.fields[fname];
|
|
932
|
+
if (f.type === 'enum' && f.values) {
|
|
933
|
+
for (var i = 0; i < f.values.length; i++) {
|
|
934
|
+
if (f.values[i].key === statusKey) {
|
|
935
|
+
var v = f.values[i];
|
|
936
|
+
var fn = ICON_SHAPES[v.icon || 'dashed-circle'];
|
|
937
|
+
return fn ? fn(v.color || '#5A5A63') : ICON_SHAPES['dashed-circle'](v.color || '#5A5A63');
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
897
943
|
}
|
|
898
|
-
// Fallback: dashed circle
|
|
899
944
|
return ICON_SHAPES['dashed-circle']('#5A5A63');
|
|
900
945
|
}
|
|
901
946
|
|
|
@@ -904,13 +949,6 @@ function priorityIcon(priority) {
|
|
|
904
949
|
if (D.config && D.config.priorities) {
|
|
905
950
|
var entry = D.config.priorities.find(function(p) { return p.key === priority; });
|
|
906
951
|
if (entry) { c = entry.color; n = entry.bars; }
|
|
907
|
-
} else {
|
|
908
|
-
switch (priority) {
|
|
909
|
-
case 'urgent': c = '#F97316'; n = 4; break;
|
|
910
|
-
case 'high': c = '#F97316'; n = 3; break;
|
|
911
|
-
case 'medium': c = '#D4A72C'; n = 2; break;
|
|
912
|
-
case 'low': c = '#5B6EF5'; n = 1; break;
|
|
913
|
-
}
|
|
914
952
|
}
|
|
915
953
|
var bars = '';
|
|
916
954
|
for (var i = 0; i < 4; i++) {
|
|
@@ -922,74 +960,280 @@ function priorityIcon(priority) {
|
|
|
922
960
|
return '<svg class="icon" viewBox="0 0 16 16">' + bars + '</svg>';
|
|
923
961
|
}
|
|
924
962
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
963
|
+
/* ── Config-driven entity helpers ─────────────────────────── */
|
|
964
|
+
function getEntityDef(type) {
|
|
965
|
+
return D.config && D.config.entities ? D.config.entities[type] || null : null;
|
|
966
|
+
}
|
|
929
967
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
968
|
+
function getEntityFields(type) {
|
|
969
|
+
var def = getEntityDef(type);
|
|
970
|
+
return def ? def.fields || {} : {};
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function getEntityStatuses(type) {
|
|
974
|
+
var fields = getEntityFields(type);
|
|
975
|
+
return fields.status && fields.status.type === 'enum' ? fields.status.values : null;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function getEntities(type) {
|
|
979
|
+
return D.entities[type] || [];
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function getEntityPlural(type) {
|
|
983
|
+
var def = getEntityDef(type);
|
|
984
|
+
return def ? def.plural.toLowerCase() : type + 's';
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function getEntitySingular(type) {
|
|
988
|
+
var def = getEntityDef(type);
|
|
989
|
+
return def ? def.singular : type;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function getTabConfig(tabId) {
|
|
993
|
+
if (!D.config || !D.config.ui || !D.config.ui.tabs) return null;
|
|
994
|
+
return D.config.ui.tabs.find(function(t) { return t.id === tabId; }) || null;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function getActiveLayout(tabId) {
|
|
998
|
+
if (D.activeLayouts[tabId]) return D.activeLayouts[tabId];
|
|
999
|
+
var tab = getTabConfig(tabId);
|
|
1000
|
+
return tab ? tab.defaultLayout : 'board';
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/* ── Filtering helpers ───────────────────────────────────── */
|
|
1004
|
+
function getAutoFilters(entityType) {
|
|
1005
|
+
if (!D.config || !D.config.entities) return [];
|
|
1006
|
+
var def = getEntityDef(entityType);
|
|
1007
|
+
if (!def || !def.fields) return [];
|
|
1008
|
+
var filters = [];
|
|
1009
|
+
for (var name in def.fields) {
|
|
1010
|
+
var f = def.fields[name];
|
|
1011
|
+
if (f.type === 'enum') filters.push(name);
|
|
937
1012
|
}
|
|
1013
|
+
// Add ancestor types as filters
|
|
1014
|
+
var ancestors = findAncestors(entityType);
|
|
1015
|
+
for (var i = 0; i < ancestors.length; i++) {
|
|
1016
|
+
filters.push(ancestors[i]);
|
|
1017
|
+
}
|
|
1018
|
+
return filters;
|
|
938
1019
|
}
|
|
939
1020
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
var
|
|
944
|
-
config: null, project: null, milestones: [], epics: [], tasks: [],
|
|
945
|
-
sprints: [], allSprints: [], metrics: null, health: null, loaded: false,
|
|
946
|
-
sources: [], activeSource: null, overviewLinks: null, notes: [],
|
|
947
|
-
aiSuggestions: null, theme: null
|
|
948
|
-
};
|
|
1021
|
+
function getFilterValues(entityType, field) {
|
|
1022
|
+
var items = getEntities(entityType);
|
|
1023
|
+
var def = getEntityDef(entityType);
|
|
1024
|
+
var fieldDef = def && def.fields ? def.fields[field] : null;
|
|
949
1025
|
|
|
950
|
-
|
|
951
|
-
|
|
1026
|
+
// Enum field — return all possible values
|
|
1027
|
+
if (fieldDef && fieldDef.type === 'enum' && fieldDef.values) {
|
|
1028
|
+
return fieldDef.values.map(function(v) { return v.key; });
|
|
1029
|
+
}
|
|
952
1030
|
|
|
953
|
-
|
|
954
|
-
var
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1031
|
+
// Ancestor ref — collect unique values from items
|
|
1032
|
+
var vals = [];
|
|
1033
|
+
items.forEach(function(item) {
|
|
1034
|
+
var v = item['_' + field] || item[field];
|
|
1035
|
+
if (v && vals.indexOf(v) === -1) vals.push(v);
|
|
1036
|
+
});
|
|
1037
|
+
return vals;
|
|
958
1038
|
}
|
|
959
1039
|
|
|
960
|
-
function
|
|
961
|
-
|
|
962
|
-
|
|
1040
|
+
function getFieldLabel(entityType, fieldName, value) {
|
|
1041
|
+
var def = getEntityDef(entityType);
|
|
1042
|
+
if (!def || !def.fields) return value || '';
|
|
1043
|
+
var field = def.fields[fieldName];
|
|
1044
|
+
if (field && field.type === 'enum' && field.values) {
|
|
1045
|
+
var match = field.values.find(function(v) { return v.key === value; });
|
|
1046
|
+
if (match) return match.label;
|
|
1047
|
+
}
|
|
1048
|
+
// Ancestor label
|
|
1049
|
+
if (!field) {
|
|
1050
|
+
var ancDef = getEntityDef(fieldName);
|
|
1051
|
+
if (ancDef) return value || '';
|
|
1052
|
+
}
|
|
1053
|
+
return value || '';
|
|
963
1054
|
}
|
|
964
1055
|
|
|
965
|
-
function
|
|
1056
|
+
function getFieldColor(entityType, fieldName, value) {
|
|
1057
|
+
var def = getEntityDef(entityType);
|
|
1058
|
+
if (!def || !def.fields) return null;
|
|
1059
|
+
var field = def.fields[fieldName];
|
|
1060
|
+
if (field && field.type === 'enum' && field.values) {
|
|
1061
|
+
var match = field.values.find(function(v) { return v.key === value; });
|
|
1062
|
+
if (match) return match.color || null;
|
|
1063
|
+
}
|
|
1064
|
+
return null;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function getFilteredItems(tabConfig) {
|
|
1068
|
+
if (!tabConfig || !tabConfig.entity) return [];
|
|
1069
|
+
var type = tabConfig.entity;
|
|
1070
|
+
var items = getEntities(type).slice();
|
|
1071
|
+
var tabId = tabConfig.id;
|
|
1072
|
+
var filters = D.tabFilters[tabId] || {};
|
|
1073
|
+
|
|
1074
|
+
// Apply search filter
|
|
1075
|
+
if (filters._search) {
|
|
1076
|
+
var q = filters._search.toLowerCase();
|
|
1077
|
+
items = items.filter(function(x) {
|
|
1078
|
+
return (x.id || '').toLowerCase().indexOf(q) !== -1 ||
|
|
1079
|
+
(x.title || '').toLowerCase().indexOf(q) !== -1;
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// Apply field filters
|
|
1084
|
+
for (var field in filters) {
|
|
1085
|
+
if (field === '_search') continue;
|
|
1086
|
+
var val = filters[field];
|
|
1087
|
+
if (!val) continue;
|
|
1088
|
+
items = items.filter(function(x) {
|
|
1089
|
+
return x[field] === val || x['_' + field] === val;
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
966
1092
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1093
|
+
// Apply sort
|
|
1094
|
+
var sort = D.tabSort[tabId];
|
|
1095
|
+
if (sort && sort.col) {
|
|
1096
|
+
var col = sort.col, dir = sort.asc ? 1 : -1;
|
|
1097
|
+
items.sort(function(a, b) {
|
|
1098
|
+
var va = a[col] == null ? '' : a[col], vb = b[col] == null ? '' : b[col];
|
|
1099
|
+
if (typeof va === 'number' && typeof vb === 'number') return (va - vb) * dir;
|
|
1100
|
+
return String(va).localeCompare(String(vb)) * dir;
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
return items;
|
|
971
1105
|
}
|
|
972
1106
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
var
|
|
977
|
-
|
|
978
|
-
var
|
|
1107
|
+
/* ── Hierarchy helpers ───────────────────────────────────── */
|
|
1108
|
+
function findAncestors(type) {
|
|
1109
|
+
if (!D.config || !D.config.hierarchy) return [];
|
|
1110
|
+
var ancestors = [];
|
|
1111
|
+
function walk(node, target, path) {
|
|
1112
|
+
for (var t in node) {
|
|
1113
|
+
if (t === target) { return path.slice(); }
|
|
1114
|
+
if (node[t].children) {
|
|
1115
|
+
var result = walk(node[t].children, target, path.concat(t));
|
|
1116
|
+
if (result) return result;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
return walk(D.config.hierarchy, type, []) || [];
|
|
1122
|
+
}
|
|
979
1123
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1124
|
+
function findChildren(type) {
|
|
1125
|
+
if (!D.config || !D.config.hierarchy) return [];
|
|
1126
|
+
function walk(node) {
|
|
1127
|
+
for (var t in node) {
|
|
1128
|
+
if (t === type && node[t].children) {
|
|
1129
|
+
return Object.keys(node[t].children);
|
|
1130
|
+
}
|
|
1131
|
+
if (node[t].children) {
|
|
1132
|
+
var result = walk(node[t].children);
|
|
1133
|
+
if (result) return result;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
return [];
|
|
1137
|
+
}
|
|
1138
|
+
return walk(D.config.hierarchy);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function isCompletedStatus(status) {
|
|
1142
|
+
if (!D.config || !D.config.completedStatuses) return status === 'done';
|
|
1143
|
+
return D.config.completedStatuses.indexOf(status) !== -1;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function isStandaloneType(type) {
|
|
1147
|
+
return D.config && D.config.standalone && !!D.config.standalone[type];
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/* ── Rendering helpers ───────────────────────────────────── */
|
|
1151
|
+
function renderFieldPill(item, entityType, field) {
|
|
1152
|
+
var value = item[field];
|
|
1153
|
+
if (value == null || value === '') return '';
|
|
1154
|
+
var def = getEntityDef(entityType);
|
|
1155
|
+
var fieldDef = def && def.fields ? def.fields[field] : null;
|
|
1156
|
+
|
|
1157
|
+
if (fieldDef && fieldDef.type === 'enum' && fieldDef.values) {
|
|
1158
|
+
var match = fieldDef.values.find(function(v) { return v.key === value; });
|
|
1159
|
+
if (match) {
|
|
1160
|
+
var color = match.color || 'var(--text2)';
|
|
1161
|
+
return '<span class="pill" style="background:' + color + '20;color:' + color + '">' +
|
|
1162
|
+
(match.icon ? statusIcon(entityType, value) + ' ' : '') +
|
|
1163
|
+
escHtml(match.label) + '</span>';
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
if (fieldDef && fieldDef.type === 'longtext') {
|
|
1168
|
+
var ltText = String(value);
|
|
1169
|
+
var ltTrunc = ltText.length > 50 ? ltText.substring(0, 50) + '...' : ltText;
|
|
1170
|
+
return '<span class="pill" style="background:var(--surface2);color:var(--text2)">' + escHtml(ltTrunc) + '</span>';
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
if (fieldDef && fieldDef.type === 'number' && value != null) {
|
|
1174
|
+
return '<span class="pill pill-points">' + value + (fieldDef.label === 'Points' ? ' pts' : '') + '</span>';
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
if (fieldDef && fieldDef.type === 'list') {
|
|
1178
|
+
var arr = Array.isArray(value) ? value : String(value).split(',').map(function(s) { return s.trim(); });
|
|
1179
|
+
return arr.filter(Boolean).map(function(v) {
|
|
1180
|
+
return '<span class="pill" style="background:var(--surface2);color:var(--text2)">' + escHtml(v) + '</span>';
|
|
1181
|
+
}).join('');
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
return '<span class="pill" style="background:var(--surface2);color:var(--text2)">' + escHtml(String(value)) + '</span>';
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function renderFieldCell(item, entityType, col) {
|
|
1188
|
+
if (col === 'id') return '<span class="td-id">' + escHtml(item.id || '') + '</span>';
|
|
1189
|
+
if (col === 'title') return '<span class="td-title">' + escHtml(item.title || '') + '</span>';
|
|
1190
|
+
|
|
1191
|
+
var value = item[col] || item['_' + col];
|
|
1192
|
+
if (value == null || value === '') return '';
|
|
1193
|
+
|
|
1194
|
+
var def = getEntityDef(entityType);
|
|
1195
|
+
var fieldDef = def && def.fields ? def.fields[col] : null;
|
|
1196
|
+
|
|
1197
|
+
if (fieldDef && fieldDef.type === 'enum') {
|
|
1198
|
+
return '<span class="badge ' + badgeClass('badge', value) + '">' +
|
|
1199
|
+
statusIcon(entityType, value) + ' ' + escHtml(getFieldLabel(entityType, col, value)) + '</span>';
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
if (fieldDef && fieldDef.type === 'longtext') {
|
|
1203
|
+
var ltText = String(value);
|
|
1204
|
+
var ltTrunc = ltText.length > 50 ? ltText.substring(0, 50) + '...' : ltText;
|
|
1205
|
+
return '<span style="color:var(--text2)">' + escHtml(ltTrunc) + '</span>';
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
if (fieldDef && fieldDef.type === 'list') {
|
|
1209
|
+
var arr = Array.isArray(value) ? value : [value];
|
|
1210
|
+
return escHtml(arr.join(', '));
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// Ancestor ref column
|
|
1214
|
+
if (!fieldDef && value) {
|
|
1215
|
+
return escHtml(String(value));
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
return escHtml(String(value));
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function emptyState(entityType) {
|
|
1222
|
+
var singular = getEntitySingular(entityType);
|
|
1223
|
+
return '<div class="empty"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">' +
|
|
1224
|
+
'<rect x="3" y="3" width="18" height="18" rx="3"/><path d="M9 12h6M12 9v6"/></svg>' +
|
|
1225
|
+
'<p>No ' + escHtml(singular.toLowerCase()) + 's yet.</p></div>';
|
|
987
1226
|
}
|
|
988
1227
|
|
|
989
1228
|
/* ── Data Fetching ───────────────────────────────────────── */
|
|
1229
|
+
function apiBase() {
|
|
1230
|
+
if (D.activeSource) return '/api/sources/' + encodeURIComponent(D.activeSource);
|
|
1231
|
+
return '/api';
|
|
1232
|
+
}
|
|
1233
|
+
|
|
990
1234
|
async function fetchJson(url) {
|
|
991
1235
|
try { var r = await fetch(url); return r.ok ? await r.json() : null; }
|
|
992
|
-
catch { return null; }
|
|
1236
|
+
catch(e) { return null; }
|
|
993
1237
|
}
|
|
994
1238
|
|
|
995
1239
|
async function patchItem(type, id, updates) {
|
|
@@ -1014,12 +1258,6 @@ async function patchItem(type, id, updates) {
|
|
|
1014
1258
|
}
|
|
1015
1259
|
}
|
|
1016
1260
|
|
|
1017
|
-
/* ── CRUD API helpers ─────────────────────────────────────── */
|
|
1018
|
-
function apiBase() {
|
|
1019
|
-
if (D.activeSource) return '/api/sources/' + encodeURIComponent(D.activeSource);
|
|
1020
|
-
return '/api';
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
1261
|
async function createItem(collection, data) {
|
|
1024
1262
|
try {
|
|
1025
1263
|
var url = apiBase() + '/' + collection;
|
|
@@ -1062,28 +1300,41 @@ async function deleteItem(collection, id) {
|
|
|
1062
1300
|
|
|
1063
1301
|
/* ── Data Loading ────────────────────────────────────────── */
|
|
1064
1302
|
async function loadAll() {
|
|
1303
|
+
D.config = await fetchJson('/api/config');
|
|
1304
|
+
if (!D.config) return;
|
|
1065
1305
|
var base = apiBase();
|
|
1066
|
-
var
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1306
|
+
var types = D.config.entities ? Object.keys(D.config.entities) : [];
|
|
1307
|
+
|
|
1308
|
+
var fetches = types.map(function(t) {
|
|
1309
|
+
var def = D.config.entities[t];
|
|
1310
|
+
return fetchJson(base + '/' + def.plural.toLowerCase());
|
|
1311
|
+
});
|
|
1312
|
+
fetches.push(fetchJson('/api/project'));
|
|
1313
|
+
fetches.push(fetchJson('/api/metrics'));
|
|
1314
|
+
fetches.push(fetchJson('/api/health'));
|
|
1315
|
+
fetches.push(fetchJson('/api/sources'));
|
|
1316
|
+
|
|
1317
|
+
var results = await Promise.all(fetches);
|
|
1318
|
+
|
|
1319
|
+
types.forEach(function(t, i) {
|
|
1320
|
+
D.entities[t] = results[i] || [];
|
|
1321
|
+
});
|
|
1322
|
+
var offset = types.length;
|
|
1323
|
+
D.project = results[offset] || {};
|
|
1324
|
+
D.metrics = results[offset + 1];
|
|
1325
|
+
D.health = results[offset + 2];
|
|
1326
|
+
D.sources = results[offset + 3] || [];
|
|
1080
1327
|
D.loaded = true;
|
|
1328
|
+
|
|
1329
|
+
// Apply the project's theme + reload custom CSS
|
|
1330
|
+
applyTheme(getActiveTheme());
|
|
1331
|
+
reloadCustomCss();
|
|
1081
1332
|
}
|
|
1082
1333
|
|
|
1083
1334
|
function refreshData() {
|
|
1084
1335
|
loadAll().then(function() {
|
|
1085
|
-
|
|
1086
|
-
|
|
1336
|
+
generateDynamicStyles();
|
|
1337
|
+
renderCurrentView();
|
|
1087
1338
|
if (hasWorkspace()) buildSourceRail();
|
|
1088
1339
|
});
|
|
1089
1340
|
}
|
|
@@ -1092,72 +1343,30 @@ function hasWorkspace() {
|
|
|
1092
1343
|
return D.config && D.config.workspace && D.config.workspace.sources && D.config.workspace.sources.length > 0;
|
|
1093
1344
|
}
|
|
1094
1345
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
var cfg = D.config;
|
|
1100
|
-
|
|
1101
|
-
// Derive entity names
|
|
1102
|
-
ENTITY_NAMES = cfg.entities || {};
|
|
1103
|
-
|
|
1104
|
-
// Derive statuses, labels, board columns
|
|
1105
|
-
if (cfg.statuses && cfg.statuses.task) {
|
|
1106
|
-
STATUSES = cfg.statuses.task.map(function(s) { return s.key; });
|
|
1107
|
-
STATUS_LABELS = {};
|
|
1108
|
-
cfg.statuses.task.forEach(function(s) { STATUS_LABELS[s.key] = s.label; });
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
// Build icon/color maps for all status types
|
|
1112
|
-
STATUS_ICON_MAP = {};
|
|
1113
|
-
STATUS_COLOR_MAP = {};
|
|
1114
|
-
if (cfg.statuses) {
|
|
1115
|
-
Object.keys(cfg.statuses).forEach(function(entityType) {
|
|
1116
|
-
cfg.statuses[entityType].forEach(function(s) {
|
|
1117
|
-
if (!STATUS_ICON_MAP[s.key]) {
|
|
1118
|
-
STATUS_ICON_MAP[s.key] = { icon: s.icon || 'dashed-circle', color: s.color || '#5A5A63' };
|
|
1119
|
-
STATUS_COLOR_MAP[s.key] = s.color || '#5A5A63';
|
|
1120
|
-
}
|
|
1121
|
-
});
|
|
1122
|
-
});
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
// Derive priorities
|
|
1126
|
-
if (cfg.priorities) {
|
|
1127
|
-
PRIORITIES = cfg.priorities.map(function(p) { return p.key; });
|
|
1128
|
-
PRIORITY_LABELS = {};
|
|
1129
|
-
cfg.priorities.forEach(function(p) { PRIORITY_LABELS[p.key] = p.label; });
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
// Board columns
|
|
1133
|
-
if (cfg.boardColumns) BOARD_COLS = cfg.boardColumns;
|
|
1134
|
-
|
|
1135
|
-
// Completed status
|
|
1136
|
-
if (cfg.completedStatus) COMPLETED_STATUS = cfg.completedStatus;
|
|
1137
|
-
|
|
1138
|
-
generateDynamicStyles();
|
|
1346
|
+
function isSourceWritable() {
|
|
1347
|
+
if (!hasWorkspace() || !D.activeSource) return true;
|
|
1348
|
+
var src = D.config.workspace.sources.find(function(s) { return s.name === D.activeSource; });
|
|
1349
|
+
return src ? !src.readonly : true;
|
|
1139
1350
|
}
|
|
1140
1351
|
|
|
1352
|
+
/* ── Config-driven initialization ────────────────────────── */
|
|
1141
1353
|
function generateDynamicStyles() {
|
|
1354
|
+
if (!D.config || !D.config.entities) return;
|
|
1142
1355
|
var css = '';
|
|
1143
|
-
|
|
1144
|
-
D.config.
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
D.config.statuses[entityType].forEach(function(s) {
|
|
1155
|
-
css += '.badge-' + s.key + '{background:' + s.color + '20;color:' + s.color + '}\n';
|
|
1156
|
-
});
|
|
1356
|
+
for (var type in D.config.entities) {
|
|
1357
|
+
var entity = D.config.entities[type];
|
|
1358
|
+
if (!entity.fields) continue;
|
|
1359
|
+
for (var fname in entity.fields) {
|
|
1360
|
+
var field = entity.fields[fname];
|
|
1361
|
+
if (field.type === 'enum' && field.values) {
|
|
1362
|
+
field.values.forEach(function(v) {
|
|
1363
|
+
css += '.badge-' + v.key + '{background:' + v.color + '20;color:' + v.color + '}\n';
|
|
1364
|
+
css += '.card[data-status="' + v.key + '"]{border-left-color:' + v.color + '}\n';
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1157
1367
|
}
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
if (D.config && D.config.priorities) {
|
|
1368
|
+
}
|
|
1369
|
+
if (D.config.priorities) {
|
|
1161
1370
|
D.config.priorities.forEach(function(p) {
|
|
1162
1371
|
css += '.badge-' + p.key + '{background:' + p.color + '20;color:' + p.color + '}\n';
|
|
1163
1372
|
});
|
|
@@ -1166,13 +1375,11 @@ function generateDynamicStyles() {
|
|
|
1166
1375
|
if (styleEl) styleEl.textContent = css;
|
|
1167
1376
|
}
|
|
1168
1377
|
|
|
1169
|
-
/*
|
|
1170
|
-
THEME ENGINE
|
|
1171
|
-
══════════════════════════════════════════════════════════════ */
|
|
1378
|
+
/* ── Theme Engine ────────────────────────────────────────── */
|
|
1172
1379
|
function generateThemeCss(themeId) {
|
|
1173
1380
|
var theme = THEMES[themeId];
|
|
1174
1381
|
if (!theme) return '';
|
|
1175
|
-
var css = '
|
|
1382
|
+
var css = ':root {\n';
|
|
1176
1383
|
for (var key in theme.vars) {
|
|
1177
1384
|
if (theme.vars.hasOwnProperty(key)) {
|
|
1178
1385
|
css += ' --' + key + ': ' + theme.vars[key] + ';\n';
|
|
@@ -1203,206 +1410,42 @@ function getActiveTheme() {
|
|
|
1203
1410
|
return D.config && D.config.theme ? D.config.theme : 'default-dark';
|
|
1204
1411
|
}
|
|
1205
1412
|
|
|
1413
|
+
|
|
1206
1414
|
/* ══════════════════════════════════════════════════════════════
|
|
1207
|
-
|
|
1415
|
+
EDITOR.JS WRAPPER — md↔blocks conversion + Notion-like UX
|
|
1208
1416
|
══════════════════════════════════════════════════════════════ */
|
|
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
1417
|
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
if (
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1418
|
+
/* ── Inline Markdown ↔ HTML ──────────────────────────────── */
|
|
1419
|
+
function inlineMdToHtml(text) {
|
|
1420
|
+
if (!text) return '';
|
|
1421
|
+
text = text.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
|
|
1422
|
+
text = text.replace(/\*(.+?)\*/g, '<i>$1</i>');
|
|
1423
|
+
text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
1424
|
+
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
|
1425
|
+
return text;
|
|
1426
|
+
}
|
|
1224
1427
|
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1428
|
+
function inlineHtmlToMd(html) {
|
|
1429
|
+
if (!html) return '';
|
|
1430
|
+
var text = html.replace(/<br\s*\/?>/gi, '\n');
|
|
1431
|
+
text = text.replace(/<a[^>]+href="([^"]*)"[^>]*>([^<]*)<\/a>/gi, '[$2]($1)');
|
|
1432
|
+
text = text.replace(/<b>([^<]*)<\/b>/gi, '**$1**');
|
|
1433
|
+
text = text.replace(/<strong>([^<]*)<\/strong>/gi, '**$1**');
|
|
1434
|
+
text = text.replace(/<i>([^<]*)<\/i>/gi, '*$1*');
|
|
1435
|
+
text = text.replace(/<em>([^<]*)<\/em>/gi, '*$1*');
|
|
1436
|
+
text = text.replace(/<code>([^<]*)<\/code>/gi, '`$1`');
|
|
1437
|
+
text = text.replace(/<mark[^>]*>([^<]*)<\/mark>/gi, '==$1==');
|
|
1438
|
+
text = text.replace(/<[^>]+>/g, '');
|
|
1439
|
+
return text;
|
|
1230
1440
|
}
|
|
1231
1441
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
if (!
|
|
1235
|
-
rail.innerHTML = '';
|
|
1442
|
+
/* ── Markdown → Editor.js Blocks ────────────────────────── */
|
|
1443
|
+
function markdownToBlocks(md) {
|
|
1444
|
+
if (!md || !md.trim()) return [{ type: 'paragraph', data: { text: '' } }];
|
|
1236
1445
|
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
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
|
-
|
|
1332
|
-
/* ── SSE — Hot Reload ────────────────────────────────────── */
|
|
1333
|
-
function connectSSE() {
|
|
1334
|
-
try {
|
|
1335
|
-
var src = new EventSource('/api/events');
|
|
1336
|
-
src.onmessage = function(e) {
|
|
1337
|
-
var data;
|
|
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
|
-
|
|
1361
|
-
if (data.cssReload) {
|
|
1362
|
-
var link = document.getElementById('custom-theme');
|
|
1363
|
-
if (link) link.href = '/mdboard.css?' + Date.now();
|
|
1364
|
-
}
|
|
1365
|
-
refreshData();
|
|
1366
|
-
};
|
|
1367
|
-
src.onerror = function() { setTimeout(connectSSE, 5000); src.close(); };
|
|
1368
|
-
} catch { /* no SSE support */ }
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
/* ══════════════════════════════════════════════════════════════
|
|
1372
|
-
EDITOR.JS WRAPPER — md↔blocks conversion + Notion-like UX
|
|
1373
|
-
══════════════════════════════════════════════════════════════ */
|
|
1374
|
-
|
|
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
|
-
}
|
|
1384
|
-
|
|
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
|
-
}
|
|
1398
|
-
|
|
1399
|
-
/* ── Markdown → Editor.js Blocks ────────────────────────── */
|
|
1400
|
-
function markdownToBlocks(md) {
|
|
1401
|
-
if (!md || !md.trim()) return [{ type: 'paragraph', data: { text: '' } }];
|
|
1402
|
-
|
|
1403
|
-
var lines = md.split('\n');
|
|
1404
|
-
var blocks = [];
|
|
1405
|
-
var i = 0;
|
|
1446
|
+
var lines = md.split('\n');
|
|
1447
|
+
var blocks = [];
|
|
1448
|
+
var i = 0;
|
|
1406
1449
|
|
|
1407
1450
|
while (i < lines.length) {
|
|
1408
1451
|
var line = lines[i];
|
|
@@ -1689,118 +1732,111 @@ function simpleMarkdownRender(md) {
|
|
|
1689
1732
|
|
|
1690
1733
|
|
|
1691
1734
|
/* ══════════════════════════════════════════════════════════════
|
|
1692
|
-
BOARD VIEW
|
|
1735
|
+
BOARD VIEW — Generic kanban board from tabConfig
|
|
1693
1736
|
══════════════════════════════════════════════════════════════ */
|
|
1694
|
-
var boardFilters = { search: '', priority: '', epic: '', milestone: '' };
|
|
1695
|
-
|
|
1696
|
-
function renderBoardFilters() {
|
|
1697
|
-
var c = document.getElementById('board-filters');
|
|
1698
|
-
var ep = [], ms = [];
|
|
1699
|
-
D.tasks.forEach(function(f) {
|
|
1700
|
-
if (f.epic && ep.indexOf(f.epic) === -1) ep.push(f.epic);
|
|
1701
|
-
if (f.milestone && ms.indexOf(f.milestone) === -1) ms.push(f.milestone);
|
|
1702
|
-
});
|
|
1703
1737
|
|
|
1704
|
-
|
|
1705
|
-
|
|
1738
|
+
function renderBoard(container, tabConfig) {
|
|
1739
|
+
if (!D.loaded || !tabConfig || !tabConfig.entity) {
|
|
1740
|
+
container.innerHTML = '<div class="loading-container">' +
|
|
1741
|
+
'<div class="skeleton skeleton-card"></div>'.repeat(3) + '</div>';
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1706
1744
|
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1745
|
+
var entityType = tabConfig.entity;
|
|
1746
|
+
var boardConfig = tabConfig.board || {};
|
|
1747
|
+
var groupBy = boardConfig.groupBy || 'status';
|
|
1748
|
+
var cardFields = boardConfig.cardFields || ['id', 'title'];
|
|
1749
|
+
var items = getFilteredItems(tabConfig);
|
|
1750
|
+
|
|
1751
|
+
// Get columns from the groupBy field's enum values
|
|
1752
|
+
var fields = getEntityFields(entityType);
|
|
1753
|
+
var groupField = fields[groupBy];
|
|
1754
|
+
var columns = [];
|
|
1755
|
+
if (groupField && groupField.type === 'enum' && groupField.values) {
|
|
1756
|
+
columns = groupField.values.map(function(v) { return { key: v.key, label: v.label, color: v.color }; });
|
|
1757
|
+
} else {
|
|
1758
|
+
// Fallback: derive columns from data
|
|
1759
|
+
var seen = {};
|
|
1760
|
+
items.forEach(function(item) {
|
|
1761
|
+
var v = item[groupBy] || 'none';
|
|
1762
|
+
if (!seen[v]) { seen[v] = true; columns.push({ key: v, label: v, color: null }); }
|
|
1763
|
+
});
|
|
1710
1764
|
}
|
|
1711
1765
|
|
|
1712
|
-
|
|
1713
|
-
|
|
1766
|
+
if (items.length === 0 && getEntities(entityType).length === 0) {
|
|
1767
|
+
container.innerHTML = emptyState(entityType);
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1714
1770
|
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
opts(ep, 'epic', epicPlural) + opts(ms, 'milestone', msPlural);
|
|
1771
|
+
var html = '<div id="board-' + tabConfig.id + '" class="board-container">';
|
|
1772
|
+
for (var ci = 0; ci < columns.length; ci++) {
|
|
1773
|
+
var col = columns[ci];
|
|
1774
|
+
var colItems = items.filter(function(item) { return (item[groupBy] || '') === col.key; });
|
|
1720
1775
|
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
c.querySelectorAll('select[data-filter]').forEach(function(el) {
|
|
1725
|
-
el.addEventListener('change', function() { boardFilters[el.dataset.filter] = el.value; renderBoard(); });
|
|
1726
|
-
});
|
|
1776
|
+
html += '<div class="column" data-status="' + escHtml(col.key) + '">';
|
|
1777
|
+
html += '<div class="column-header"><span class="col-title">' + statusIcon(entityType, col.key) + ' ' + escHtml(col.label) + '</span><span class="col-count">' + colItems.length + '</span></div>';
|
|
1778
|
+
html += '<div class="column-body">';
|
|
1727
1779
|
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
var list = D.tasks.slice();
|
|
1734
|
-
var f = boardFilters;
|
|
1735
|
-
if (f.search) { var q = f.search.toLowerCase(); list = list.filter(function(x) { return (x.id || '').toLowerCase().indexOf(q) !== -1 || (x.title || '').toLowerCase().indexOf(q) !== -1; }); }
|
|
1736
|
-
if (f.priority) list = list.filter(function(x) { return x.priority === f.priority; });
|
|
1737
|
-
if (f.epic) list = list.filter(function(x) { return x.epic === f.epic; });
|
|
1738
|
-
if (f.milestone) list = list.filter(function(x) { return x.milestone === f.milestone; });
|
|
1739
|
-
return list;
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
function renderBoard() {
|
|
1743
|
-
var sprint = D.sprints.find(function(s) { return s.status === 'active'; });
|
|
1744
|
-
var sbar = document.getElementById('sprint-bar');
|
|
1745
|
-
if (sprint) {
|
|
1746
|
-
var planned = sprint.planned_points || 0;
|
|
1747
|
-
var completed = sprint.completed_points || 0;
|
|
1748
|
-
var pct = planned ? Math.round(completed / planned * 100) : 0;
|
|
1749
|
-
sbar.style.display = '';
|
|
1750
|
-
sbar.innerHTML =
|
|
1751
|
-
'<div class="sprint-goal"><b>' + escHtml(sprint.id || '') + '</b>' + (sprint.goal ? ' — ' + escHtml(sprint.goal) : '') + '</div>' +
|
|
1752
|
-
'<div style="width:160px"><div class="progress progress-accent"><div class="progress-fill" style="width:' + pct + '%"></div></div>' +
|
|
1753
|
-
'<div style="font-size:11px;color:var(--text3);margin-top:2px;font-family:var(--mono)">' + completed + '/' + planned + ' pts</div></div>' +
|
|
1754
|
-
'<div class="sprint-dates">' + fmtDate(sprint.start_date) + ' — ' + fmtDate(sprint.end_date) + '</div>';
|
|
1755
|
-
} else { sbar.style.display = 'none'; }
|
|
1756
|
-
|
|
1757
|
-
var board = document.getElementById('board');
|
|
1758
|
-
if (!D.loaded) {
|
|
1759
|
-
board.innerHTML = BOARD_COLS.map(function() {
|
|
1760
|
-
return '<div class="column"><div class="column-header"><div class="skeleton skeleton-line" style="width:80px;height:16px"></div></div><div class="column-body">' +
|
|
1761
|
-
'<div class="skeleton skeleton-card"></div><div class="skeleton skeleton-card"></div></div></div>';
|
|
1762
|
-
}).join('');
|
|
1763
|
-
return;
|
|
1780
|
+
for (var ii = 0; ii < colItems.length; ii++) {
|
|
1781
|
+
html += renderBoardCard(colItems[ii], entityType, cardFields, tabConfig);
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
html += '</div></div>';
|
|
1764
1785
|
}
|
|
1786
|
+
html += '</div>';
|
|
1787
|
+
container.innerHTML = html;
|
|
1765
1788
|
|
|
1766
|
-
|
|
1767
|
-
|
|
1789
|
+
// Setup drag-drop and click handlers
|
|
1790
|
+
setupBoardDragDrop(container, entityType, groupBy, tabConfig);
|
|
1791
|
+
}
|
|
1768
1792
|
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1793
|
+
function renderBoardCard(item, entityType, cardFields, tabConfig) {
|
|
1794
|
+
var html = '<div class="card" draggable="true" data-id="' + escHtml(item.id || '') + '" data-status="' + escHtml(item.status || '') + '">';
|
|
1795
|
+
|
|
1796
|
+
// Header: ID + priority
|
|
1797
|
+
html += '<div class="card-header">';
|
|
1798
|
+
if (item.priority) html += '<span class="card-priority">' + priorityIcon(item.priority) + '</span>';
|
|
1799
|
+
html += '<span class="card-id">' + escHtml(item.id || '') + '</span>';
|
|
1800
|
+
html += '</div>';
|
|
1801
|
+
|
|
1802
|
+
// Title
|
|
1803
|
+
html += '<div class="card-title">' + escHtml(item.title || '') + '</div>';
|
|
1804
|
+
|
|
1805
|
+
// Meta fields
|
|
1806
|
+
html += '<div class="card-meta">';
|
|
1807
|
+
for (var i = 0; i < cardFields.length; i++) {
|
|
1808
|
+
var f = cardFields[i];
|
|
1809
|
+
if (f === 'id' || f === 'title' || f === 'status') continue;
|
|
1810
|
+
var val = item[f];
|
|
1811
|
+
if (val == null || val === '') continue;
|
|
1812
|
+
|
|
1813
|
+
if (f === 'assigned') {
|
|
1814
|
+
var arr = Array.isArray(val) ? val : [val];
|
|
1815
|
+
html += '<span class="card-assigned">' + escHtml(arr.join(', ')) + '</span>';
|
|
1816
|
+
} else if (f === 'points' && val != null) {
|
|
1817
|
+
html += '<span class="pill pill-points">' + val + ' pts</span>';
|
|
1818
|
+
} else {
|
|
1819
|
+
html += renderFieldPill(item, entityType, f);
|
|
1820
|
+
}
|
|
1772
1821
|
}
|
|
1773
1822
|
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1823
|
+
// Show ancestor refs as pills
|
|
1824
|
+
var ancestors = findAncestors(entityType);
|
|
1825
|
+
for (var ai = 0; ai < ancestors.length; ai++) {
|
|
1826
|
+
var ancVal = item['_' + ancestors[ai]];
|
|
1827
|
+
if (ancVal) {
|
|
1828
|
+
html += '<span class="pill" style="background:' + hashColor(ancVal) + ';color:#fff;font-size:10px">' + escHtml(ancVal) + '</span>';
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1780
1831
|
|
|
1781
|
-
|
|
1832
|
+
html += '</div></div>';
|
|
1833
|
+
return html;
|
|
1782
1834
|
}
|
|
1783
1835
|
|
|
1784
|
-
function
|
|
1785
|
-
var
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
'<div class="card-header">' +
|
|
1789
|
-
'<span class="card-priority">' + priorityIcon(f.priority) + '</span>' +
|
|
1790
|
-
'<span class="card-id">' + escHtml(f.id || '') + '</span>' +
|
|
1791
|
-
'</div>' +
|
|
1792
|
-
'<div class="card-title">' + escHtml(f.title || '') + '</div>' +
|
|
1793
|
-
'<div class="card-meta">' +
|
|
1794
|
-
(f.points != null ? '<span class="pill pill-points">' + f.points + ' pts</span>' : '') +
|
|
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">⚡</span>' : '') +
|
|
1797
|
-
(assigned ? '<span class="card-assigned">' + escHtml(assigned) + '</span>' : '') +
|
|
1798
|
-
'</div></div>';
|
|
1799
|
-
}
|
|
1800
|
-
|
|
1801
|
-
/* ── Drag & Drop ─────────────────────────────────────────── */
|
|
1802
|
-
function setupDragDrop() {
|
|
1803
|
-
document.querySelectorAll('.card[draggable]').forEach(function(card) {
|
|
1836
|
+
function setupBoardDragDrop(container, entityType, groupBy, tabConfig) {
|
|
1837
|
+
var plural = getEntityPlural(entityType);
|
|
1838
|
+
|
|
1839
|
+
container.querySelectorAll('.card[draggable]').forEach(function(card) {
|
|
1804
1840
|
card.addEventListener('dragstart', function(e) {
|
|
1805
1841
|
e.dataTransfer.setData('text/plain', card.dataset.id);
|
|
1806
1842
|
e.dataTransfer.effectAllowed = 'move';
|
|
@@ -1808,16 +1844,16 @@ function setupDragDrop() {
|
|
|
1808
1844
|
});
|
|
1809
1845
|
card.addEventListener('dragend', function() {
|
|
1810
1846
|
card.classList.remove('dragging');
|
|
1811
|
-
|
|
1847
|
+
container.querySelectorAll('.column.drag-over').forEach(function(c) { c.classList.remove('drag-over'); });
|
|
1812
1848
|
});
|
|
1813
1849
|
card.addEventListener('click', function(e) {
|
|
1814
1850
|
if (e.defaultPrevented) return;
|
|
1815
|
-
var
|
|
1816
|
-
if (
|
|
1851
|
+
var item = getEntities(entityType).find(function(i) { return i.id === card.dataset.id; });
|
|
1852
|
+
if (item) openPanel(entityType, item);
|
|
1817
1853
|
});
|
|
1818
1854
|
});
|
|
1819
1855
|
|
|
1820
|
-
|
|
1856
|
+
container.querySelectorAll('.column').forEach(function(col) {
|
|
1821
1857
|
col.addEventListener('dragover', function(e) {
|
|
1822
1858
|
e.preventDefault();
|
|
1823
1859
|
e.dataTransfer.dropEffect = 'move';
|
|
@@ -1831,411 +1867,614 @@ function setupDragDrop() {
|
|
|
1831
1867
|
col.addEventListener('drop', function(e) {
|
|
1832
1868
|
e.preventDefault();
|
|
1833
1869
|
col.classList.remove('drag-over');
|
|
1834
|
-
var
|
|
1835
|
-
var
|
|
1836
|
-
if (!
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1870
|
+
var itemId = e.dataTransfer.getData('text/plain');
|
|
1871
|
+
var newValue = col.dataset.status;
|
|
1872
|
+
if (!itemId || !newValue) return;
|
|
1873
|
+
|
|
1874
|
+
var item = getEntities(entityType).find(function(i) { return i.id === itemId; });
|
|
1875
|
+
if (item && item[groupBy] !== newValue) {
|
|
1876
|
+
item[groupBy] = newValue;
|
|
1877
|
+
renderCurrentView();
|
|
1878
|
+
var updates = {};
|
|
1879
|
+
updates[groupBy] = newValue;
|
|
1880
|
+
patchItem(plural, itemId, updates);
|
|
1844
1881
|
}
|
|
1845
1882
|
});
|
|
1846
1883
|
});
|
|
1847
1884
|
}
|
|
1848
1885
|
|
|
1886
|
+
|
|
1849
1887
|
/* ══════════════════════════════════════════════════════════════
|
|
1850
|
-
TABLE VIEW
|
|
1888
|
+
TABLE VIEW — Generic sortable table from tabConfig
|
|
1851
1889
|
══════════════════════════════════════════════════════════════ */
|
|
1852
|
-
var tableSort = { col: 'id', asc: true };
|
|
1853
|
-
var tableFilters = { milestone: '', epic: '', status: '', sprint: '', search: '', priority: '' };
|
|
1854
|
-
var expandedRow = null;
|
|
1855
|
-
var TABLE_COLS = [
|
|
1856
|
-
{key:'id',label:'ID'},{key:'title',label:'Title'},{key:'epic',label:'Epic'},
|
|
1857
|
-
{key:'milestone',label:'Milestone'},{key:'sprint',label:'Sprint'},{key:'status',label:'Status'},
|
|
1858
|
-
{key:'points',label:'Pts'},{key:'priority',label:'Priority'},{key:'assigned',label:'Assigned'}
|
|
1859
|
-
];
|
|
1860
|
-
|
|
1861
|
-
function renderTableControls() {
|
|
1862
|
-
var c = document.getElementById('table-controls');
|
|
1863
|
-
var ms = [], ep = [], st = [], sp = [];
|
|
1864
|
-
D.tasks.forEach(function(f) {
|
|
1865
|
-
if (f.milestone && ms.indexOf(f.milestone) === -1) ms.push(f.milestone);
|
|
1866
|
-
if (f.epic && ep.indexOf(f.epic) === -1) ep.push(f.epic);
|
|
1867
|
-
if (f.status && st.indexOf(f.status) === -1) st.push(f.status);
|
|
1868
|
-
if (f.sprint && sp.indexOf(f.sprint) === -1) sp.push(f.sprint);
|
|
1869
|
-
});
|
|
1870
1890
|
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
return '<select data-tf="' + filter + '"><option value="">All ' + label + '</option>' +
|
|
1877
|
-
arr.map(function(v) { return '<option value="' + escHtml(v) + '"' + (tableFilters[filter] === v ? ' selected' : '') + '>' + escHtml(v) + '</option>'; }).join('') + '</select>';
|
|
1891
|
+
function renderTable(container, tabConfig) {
|
|
1892
|
+
if (!D.loaded || !tabConfig || !tabConfig.entity) {
|
|
1893
|
+
container.innerHTML = '<div class="loading-container">' +
|
|
1894
|
+
'<div class="skeleton skeleton-line" style="width:100%;height:32px"></div>'.repeat(6) + '</div>';
|
|
1895
|
+
return;
|
|
1878
1896
|
}
|
|
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
1897
|
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1898
|
+
var entityType = tabConfig.entity;
|
|
1899
|
+
var tableConfig = tabConfig.table || {};
|
|
1900
|
+
var columns = tableConfig.columns || ['id', 'title', 'status'];
|
|
1901
|
+
var sortable = tableConfig.sortable !== false;
|
|
1902
|
+
var items = getFilteredItems(tabConfig);
|
|
1903
|
+
var tabId = tabConfig.id;
|
|
1887
1904
|
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
var btn = document.getElementById('table-create-btn');
|
|
1894
|
-
if (btn) btn.addEventListener('click', function() { openCreateDialog('tasks'); });
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
function getFilteredTasks() {
|
|
1898
|
-
var list = D.tasks.slice();
|
|
1899
|
-
var f = tableFilters;
|
|
1900
|
-
if (f.search) { var q = f.search.toLowerCase(); list = list.filter(function(x) { return (x.id || '').toLowerCase().indexOf(q) !== -1 || (x.title || '').toLowerCase().indexOf(q) !== -1; }); }
|
|
1901
|
-
if (f.milestone) list = list.filter(function(x) { return x.milestone === f.milestone; });
|
|
1902
|
-
if (f.epic) list = list.filter(function(x) { return x.epic === f.epic; });
|
|
1903
|
-
if (f.status) list = list.filter(function(x) { return x.status === f.status; });
|
|
1904
|
-
if (f.sprint) list = list.filter(function(x) { return x.sprint === f.sprint; });
|
|
1905
|
-
if (f.priority) list = list.filter(function(x) { return x.priority === f.priority; });
|
|
1906
|
-
var col = tableSort.col, dir = tableSort.asc ? 1 : -1;
|
|
1907
|
-
list.sort(function(a, b) {
|
|
1908
|
-
var va = a[col] == null ? '' : a[col], vb = b[col] == null ? '' : b[col];
|
|
1909
|
-
if (typeof va === 'number' && typeof vb === 'number') return (va - vb) * dir;
|
|
1910
|
-
return String(va).localeCompare(String(vb)) * dir;
|
|
1911
|
-
});
|
|
1912
|
-
return list;
|
|
1913
|
-
}
|
|
1914
|
-
|
|
1915
|
-
function renderTableBody() {
|
|
1916
|
-
var w = document.getElementById('table-wrapper');
|
|
1917
|
-
var taskPlural = ENTITY_NAMES.task ? ENTITY_NAMES.task.plural.toLowerCase() : 'tasks';
|
|
1918
|
-
if (!D.loaded) { w.innerHTML = '<div class="loading-container">' + '<div class="skeleton skeleton-line" style="width:100%;height:32px"></div>'.repeat(6) + '</div>'; return; }
|
|
1919
|
-
var tasks = getFilteredTasks();
|
|
1920
|
-
if (!tasks.length) {
|
|
1921
|
-
w.innerHTML = '<div class="empty"><p>' + (D.tasks.length ? 'No ' + escHtml(taskPlural) + ' match filters.' : 'No ' + escHtml(taskPlural) + ' yet.') + '</p></div>';
|
|
1905
|
+
if (items.length === 0) {
|
|
1906
|
+
var allItems = getEntities(entityType);
|
|
1907
|
+
container.innerHTML = '<div class="empty"><p>' +
|
|
1908
|
+
(allItems.length ? 'No items match filters.' : 'No ' + escHtml(getEntitySingular(entityType).toLowerCase()) + 's yet.') +
|
|
1909
|
+
'</p></div>';
|
|
1922
1910
|
return;
|
|
1923
1911
|
}
|
|
1912
|
+
|
|
1913
|
+
// Apply table sort
|
|
1914
|
+
var sort = D.tabSort[tabId] || { col: columns[0], asc: true };
|
|
1915
|
+
if (sort.col) {
|
|
1916
|
+
var col = sort.col, dir = sort.asc ? 1 : -1;
|
|
1917
|
+
items.sort(function(a, b) {
|
|
1918
|
+
var va = a[col] != null ? a[col] : (a['_' + col] || '');
|
|
1919
|
+
var vb = b[col] != null ? b[col] : (b['_' + col] || '');
|
|
1920
|
+
if (typeof va === 'number' && typeof vb === 'number') return (va - vb) * dir;
|
|
1921
|
+
return String(va).localeCompare(String(vb)) * dir;
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1924
1925
|
var html = '<table class="ftable"><thead><tr>';
|
|
1925
|
-
|
|
1926
|
-
var
|
|
1927
|
-
|
|
1928
|
-
|
|
1926
|
+
for (var ci = 0; ci < columns.length; ci++) {
|
|
1927
|
+
var c = columns[ci];
|
|
1928
|
+
var sorted = sort.col === c;
|
|
1929
|
+
var label = c;
|
|
1930
|
+
// Get label from field def or entity name for ancestor refs
|
|
1931
|
+
var fieldDef = getEntityFields(entityType)[c];
|
|
1932
|
+
if (fieldDef && fieldDef.label) label = fieldDef.label;
|
|
1933
|
+
else if (c === 'id') label = 'ID';
|
|
1934
|
+
else if (c === 'title') label = 'Title';
|
|
1935
|
+
else {
|
|
1936
|
+
var ancDef = getEntityDef(c);
|
|
1937
|
+
if (ancDef) label = ancDef.singular;
|
|
1938
|
+
else label = c.charAt(0).toUpperCase() + c.slice(1);
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
html += '<th class="' + (sorted ? 'sorted' : '') + '"' + (sortable ? ' data-col="' + c + '"' : '') + '>' +
|
|
1942
|
+
escHtml(label) + '<span class="sort-arrow">' + (sorted ? (sort.asc ? '\u25B2' : '\u25BC') : '') + '</span></th>';
|
|
1943
|
+
}
|
|
1929
1944
|
html += '</tr></thead><tbody>';
|
|
1930
|
-
tasks.forEach(function(f) {
|
|
1931
|
-
var assigned = Array.isArray(f.assigned) ? f.assigned.join(', ') : (f.assigned || '');
|
|
1932
|
-
html += '<tr class="clickable-row" data-fid="' + escHtml(f.id || '') + '">' +
|
|
1933
|
-
'<td class="td-id">' + escHtml(f.id || '') + '</td>' +
|
|
1934
|
-
'<td class="td-title">' + escHtml(f.title || '') + '</td>' +
|
|
1935
|
-
'<td>' + (f.epic ? '<span class="pill pill-epic" style="background:' + epicColor(f.epic) + '">' + escHtml(f.epic) + '</span>' : '') + '</td>' +
|
|
1936
|
-
'<td>' + escHtml(f.milestone || '') + '</td>' +
|
|
1937
|
-
'<td>' + escHtml(f.sprint || '') + '</td>' +
|
|
1938
|
-
'<td><span class="badge ' + badgeClass('badge', f.status) + '">' + statusIcon(f.status) + ' ' + escHtml(f.status || '') + '</span></td>' +
|
|
1939
|
-
'<td>' + (f.points != null ? f.points : '') + '</td>' +
|
|
1940
|
-
'<td>' + (f.priority ? '<span class="badge ' + badgeClass('badge', f.priority) + '">' + priorityIcon(f.priority) + ' ' + escHtml(f.priority) + '</span>' : '') + '</td>' +
|
|
1941
|
-
'<td>' + escHtml(assigned) + '</td></tr>';
|
|
1942
|
-
});
|
|
1943
|
-
html += '</tbody></table>';
|
|
1944
|
-
w.innerHTML = html;
|
|
1945
1945
|
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1946
|
+
for (var ii = 0; ii < items.length; ii++) {
|
|
1947
|
+
var item = items[ii];
|
|
1948
|
+
html += '<tr class="clickable-row" data-fid="' + escHtml(item.id || '') + '">';
|
|
1949
|
+
for (var cj = 0; cj < columns.length; cj++) {
|
|
1950
|
+
html += '<td>' + renderFieldCell(item, entityType, columns[cj]) + '</td>';
|
|
1951
|
+
}
|
|
1952
|
+
html += '</tr>';
|
|
1953
|
+
}
|
|
1954
|
+
html += '</tbody></table>';
|
|
1955
|
+
container.innerHTML = html;
|
|
1956
|
+
|
|
1957
|
+
// Sort handlers
|
|
1958
|
+
if (sortable) {
|
|
1959
|
+
container.querySelectorAll('th[data-col]').forEach(function(th) {
|
|
1960
|
+
th.addEventListener('click', function() {
|
|
1961
|
+
var s = D.tabSort[tabId] || { col: null, asc: true };
|
|
1962
|
+
if (s.col === th.dataset.col) s.asc = !s.asc;
|
|
1963
|
+
else { s.col = th.dataset.col; s.asc = true; }
|
|
1964
|
+
D.tabSort[tabId] = s;
|
|
1965
|
+
renderCurrentView();
|
|
1966
|
+
});
|
|
1951
1967
|
});
|
|
1952
|
-
}
|
|
1953
|
-
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// Row click handlers
|
|
1971
|
+
container.querySelectorAll('.clickable-row').forEach(function(tr) {
|
|
1954
1972
|
tr.addEventListener('click', function() {
|
|
1955
|
-
var
|
|
1956
|
-
if (
|
|
1973
|
+
var item = getEntities(entityType).find(function(i) { return i.id === tr.dataset.fid; });
|
|
1974
|
+
if (item) openPanel(entityType, item);
|
|
1957
1975
|
});
|
|
1958
1976
|
});
|
|
1959
1977
|
}
|
|
1960
1978
|
|
|
1979
|
+
|
|
1961
1980
|
/* ══════════════════════════════════════════════════════════════
|
|
1962
|
-
|
|
1981
|
+
CARDS VIEW — Generic card grid with expandChildren
|
|
1963
1982
|
══════════════════════════════════════════════════════════════ */
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
var c = document.getElementById('milestones-container');
|
|
1982
|
-
var taskPlural = ENTITY_NAMES.task ? ENTITY_NAMES.task.plural.toLowerCase() : 'tasks';
|
|
1983
|
-
var msDir = ENTITY_NAMES.milestone ? ENTITY_NAMES.milestone.dir : 'milestones';
|
|
1984
|
-
var msPlural = ENTITY_NAMES.milestone ? ENTITY_NAMES.milestone.plural.toLowerCase() : 'milestones';
|
|
1985
|
-
if (!D.loaded) { c.innerHTML = '<div class="loading-container"><div class="skeleton skeleton-card" style="height:200px"></div></div>'; return; }
|
|
1986
|
-
|
|
1987
|
-
var milestones = D.milestones;
|
|
1988
|
-
if (msFilter.status) milestones = milestones.filter(function(m) { return m.status === msFilter.status; });
|
|
1989
|
-
|
|
1990
|
-
if (!milestones.length) {
|
|
1991
|
-
c.innerHTML = '<div class="empty"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/></svg><p>' + (D.milestones.length ? 'No ' + escHtml(msPlural) + ' match filter.' : 'No ' + escHtml(msPlural) + ' yet. Create milestone directories under project/' + escHtml(msDir) + '/ to get started.') + '</p></div>';
|
|
1983
|
+
|
|
1984
|
+
function renderCards(container, tabConfig) {
|
|
1985
|
+
if (!D.loaded || !tabConfig || !tabConfig.entity) {
|
|
1986
|
+
container.innerHTML = '<div class="loading-container">' +
|
|
1987
|
+
'<div class="skeleton skeleton-card" style="height:120px"></div>'.repeat(4) + '</div>';
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
var entityType = tabConfig.entity;
|
|
1992
|
+
var cardsConfig = tabConfig.cards || {};
|
|
1993
|
+
var fields = cardsConfig.fields || ['title', 'status'];
|
|
1994
|
+
var showProgress = cardsConfig.showProgress;
|
|
1995
|
+
var expandChildren = cardsConfig.expandChildren;
|
|
1996
|
+
var items = getFilteredItems(tabConfig);
|
|
1997
|
+
|
|
1998
|
+
if (items.length === 0) {
|
|
1999
|
+
container.innerHTML = emptyState(entityType);
|
|
1992
2000
|
return;
|
|
1993
2001
|
}
|
|
1994
2002
|
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
var
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
'<div class="epic-meta">' +
|
|
2006
|
-
(e.status ? '<span class="badge ' + badgeClass('badge', e.status) + '">' + statusIcon(e.status) + ' ' + escHtml(e.status) + '</span>' : '') +
|
|
2007
|
-
(e.priority ? '<span class="badge ' + badgeClass('badge', e.priority) + '">' + priorityIcon(e.priority) + ' ' + escHtml(e.priority) + '</span>' : '') +
|
|
2008
|
-
'</div></div>';
|
|
2009
|
-
}).join('') : '<div style="color:var(--text3);font-size:.82rem">No epics yet.</div>';
|
|
2010
|
-
|
|
2011
|
-
return '<div class="ms-card" data-ms-id="' + escHtml(ms.id || '') + '">' +
|
|
2012
|
-
'<div class="ms-header"><h2>' + milestoneIcon(ms.status) + ' ' + escHtml(ms.title || ms.id || '') + '</h2>' +
|
|
2013
|
-
'<span class="badge ' + badgeClass('badge', ms.status) + '">' + escHtml(ms.status || '') + '</span>' +
|
|
2014
|
-
(ms.deadline ? '<span class="ms-deadline">' + fmtDate(ms.deadline) + '</span>' : '') + '</div>' +
|
|
2015
|
-
'<div class="ms-progress"><div class="progress-label"><span>' + cc + ' / ' + fc + ' ' + escHtml(taskPlural) + '</span><span>' + pct + '%</span></div>' +
|
|
2016
|
-
'<div class="progress progress-lg progress-success"><div class="progress-fill" style="width:' + pct + '%"></div></div></div>' +
|
|
2017
|
-
'<div class="ms-epics">' + epicCards + '</div></div>';
|
|
2018
|
-
}).join('');
|
|
2019
|
-
|
|
2020
|
-
// Click handlers for milestones
|
|
2021
|
-
c.querySelectorAll('.ms-card[data-ms-id]').forEach(function(card) {
|
|
2003
|
+
var html = '<div class="cards-grid">';
|
|
2004
|
+
for (var i = 0; i < items.length; i++) {
|
|
2005
|
+
var item = items[i];
|
|
2006
|
+
html += renderEntityCard(item, entityType, fields, showProgress, expandChildren, tabConfig);
|
|
2007
|
+
}
|
|
2008
|
+
html += '</div>';
|
|
2009
|
+
container.innerHTML = html;
|
|
2010
|
+
|
|
2011
|
+
// Click handlers
|
|
2012
|
+
container.querySelectorAll('.entity-card[data-id]').forEach(function(card) {
|
|
2022
2013
|
card.addEventListener('click', function(e) {
|
|
2023
|
-
if (e.target.closest('.
|
|
2024
|
-
var
|
|
2025
|
-
if (
|
|
2014
|
+
if (e.target.closest('.child-card')) return;
|
|
2015
|
+
var item = getEntities(entityType).find(function(i) { return i.id === card.dataset.id; });
|
|
2016
|
+
if (item) openPanel(entityType, item);
|
|
2026
2017
|
});
|
|
2027
2018
|
});
|
|
2028
2019
|
|
|
2029
|
-
//
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2020
|
+
// Child card click handlers
|
|
2021
|
+
if (expandChildren) {
|
|
2022
|
+
container.querySelectorAll('.child-card[data-id]').forEach(function(card) {
|
|
2023
|
+
card.addEventListener('click', function(e) {
|
|
2024
|
+
e.stopPropagation();
|
|
2025
|
+
var childType = card.dataset.type;
|
|
2026
|
+
var childItem = getEntities(childType).find(function(i) { return i.id === card.dataset.id; });
|
|
2027
|
+
if (childItem) openPanel(childType, childItem);
|
|
2028
|
+
});
|
|
2035
2029
|
});
|
|
2036
|
-
}
|
|
2030
|
+
}
|
|
2037
2031
|
}
|
|
2038
2032
|
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2033
|
+
function renderEntityCard(item, entityType, fields, showProgress, expandChildren, tabConfig) {
|
|
2034
|
+
var html = '<div class="entity-card" data-id="' + escHtml(item.id || '') + '">';
|
|
2035
|
+
|
|
2036
|
+
// Header with status icon
|
|
2037
|
+
html += '<div class="entity-card-header">';
|
|
2038
|
+
if (item.status) html += '<span>' + statusIcon(entityType, item.status) + '</span>';
|
|
2039
|
+
html += '<span class="entity-card-id">' + escHtml(item.id || '') + '</span>';
|
|
2040
|
+
html += '</div>';
|
|
2041
|
+
|
|
2042
|
+
// Title
|
|
2043
|
+
html += '<div class="entity-card-title">' + escHtml(item.title || '') + '</div>';
|
|
2044
|
+
|
|
2045
|
+
// Fields as pills
|
|
2046
|
+
html += '<div class="entity-card-meta">';
|
|
2047
|
+
for (var i = 0; i < fields.length; i++) {
|
|
2048
|
+
var f = fields[i];
|
|
2049
|
+
if (f === 'title' || f === 'id') continue;
|
|
2050
|
+
if (f === 'progress' && item._progress != null) {
|
|
2051
|
+
html += '<span class="pill pill-points">' + item._progress + '%</span>';
|
|
2052
|
+
continue;
|
|
2053
|
+
}
|
|
2054
|
+
html += renderFieldPill(item, entityType, f);
|
|
2055
|
+
}
|
|
2056
|
+
html += '</div>';
|
|
2046
2057
|
|
|
2047
|
-
|
|
2048
|
-
|
|
2058
|
+
// Progress bar
|
|
2059
|
+
if (showProgress && item._progress != null) {
|
|
2060
|
+
var pct = item._progress || 0;
|
|
2049
2061
|
var clr = pct >= 100 ? 'var(--success)' : pct > 50 ? 'var(--warning)' : 'var(--accent)';
|
|
2050
|
-
|
|
2051
|
-
|
|
2062
|
+
html += '<div class="entity-card-progress">';
|
|
2063
|
+
html += '<div class="progress-label"><span>' + (item._completedCount || 0) + '/' + (item._childCount || 0) + '</span><span>' + pct + '%</span></div>';
|
|
2064
|
+
html += '<div class="progress progress-lg"><div class="progress-fill" style="width:' + pct + '%;background:' + clr + '"></div></div>';
|
|
2065
|
+
html += '</div>';
|
|
2066
|
+
}
|
|
2052
2067
|
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2068
|
+
// Expand children
|
|
2069
|
+
if (expandChildren && expandChildren.entity) {
|
|
2070
|
+
var childType = expandChildren.entity;
|
|
2071
|
+
var childFields = expandChildren.fields || ['title'];
|
|
2072
|
+
var childShowProgress = expandChildren.showProgress;
|
|
2073
|
+
|
|
2074
|
+
// Find children that belong to this parent
|
|
2075
|
+
var children = getEntities(childType).filter(function(child) {
|
|
2076
|
+
return child['_' + entityType] === item.id ||
|
|
2077
|
+
child['_' + entityType] === (item._dir || item.id);
|
|
2078
|
+
});
|
|
2058
2079
|
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2080
|
+
if (children.length > 0) {
|
|
2081
|
+
html += '<div class="entity-card-children">';
|
|
2082
|
+
for (var ci = 0; ci < children.length; ci++) {
|
|
2083
|
+
var child = children[ci];
|
|
2084
|
+
html += '<div class="child-card" data-id="' + escHtml(child.id || '') + '" data-type="' + escHtml(childType) + '">';
|
|
2085
|
+
html += '<div class="child-card-header">';
|
|
2086
|
+
if (child.status || child.hill) {
|
|
2087
|
+
var statusField = child.status ? 'status' : 'hill';
|
|
2088
|
+
html += statusIcon(childType, child[statusField] || child.status);
|
|
2089
|
+
}
|
|
2090
|
+
html += '<span class="child-card-title">' + escHtml(child.title || child.id || '') + '</span>';
|
|
2091
|
+
html += '</div>';
|
|
2064
2092
|
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2093
|
+
// Child fields
|
|
2094
|
+
for (var fi = 0; fi < childFields.length; fi++) {
|
|
2095
|
+
var cf = childFields[fi];
|
|
2096
|
+
if (cf === 'title') continue;
|
|
2097
|
+
if (cf === 'hill' && child.hill) {
|
|
2098
|
+
html += renderFieldPill(child, childType, 'hill');
|
|
2099
|
+
} else {
|
|
2100
|
+
html += renderFieldPill(child, childType, cf);
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2070
2103
|
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
}
|
|
2104
|
+
// Child progress
|
|
2105
|
+
if (childShowProgress && child._progress != null) {
|
|
2106
|
+
var cpct = child._progress || 0;
|
|
2107
|
+
var cclr = cpct >= 100 ? 'var(--success)' : cpct > 50 ? 'var(--warning)' : 'var(--accent)';
|
|
2108
|
+
html += '<div class="progress" style="margin-top:4px"><div class="progress-fill" style="width:' + cpct + '%;background:' + cclr + '"></div></div>';
|
|
2109
|
+
}
|
|
2077
2110
|
|
|
2078
|
-
|
|
2079
|
-
OVERVIEW VIEW
|
|
2080
|
-
══════════════════════════════════════════════════════════════ */
|
|
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; }
|
|
2084
|
-
|
|
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>';
|
|
2111
|
+
html += '</div>';
|
|
2109
2112
|
}
|
|
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
|
-
}
|
|
2121
|
-
|
|
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">→</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
2113
|
html += '</div>';
|
|
2142
|
-
}
|
|
2143
|
-
html += '</div>';
|
|
2144
|
-
}
|
|
2145
|
-
|
|
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>';
|
|
2114
|
+
}
|
|
2162
2115
|
}
|
|
2163
2116
|
|
|
2164
|
-
|
|
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
|
-
});
|
|
2117
|
+
html += '</div>';
|
|
2118
|
+
return html;
|
|
2181
2119
|
}
|
|
2182
2120
|
|
|
2121
|
+
|
|
2183
2122
|
/* ══════════════════════════════════════════════════════════════
|
|
2184
|
-
|
|
2123
|
+
LIST VIEW — Expandable list with children
|
|
2185
2124
|
══════════════════════════════════════════════════════════════ */
|
|
2186
2125
|
|
|
2187
|
-
|
|
2126
|
+
function renderList(container, tabConfig) {
|
|
2127
|
+
if (!D.loaded || !tabConfig || !tabConfig.entity) {
|
|
2128
|
+
container.innerHTML = '<div class="loading-container">' +
|
|
2129
|
+
'<div class="skeleton skeleton-line" style="width:100%;height:48px;margin-bottom:8px"></div>'.repeat(4) + '</div>';
|
|
2130
|
+
return;
|
|
2131
|
+
}
|
|
2188
2132
|
|
|
2189
|
-
|
|
2190
|
-
var
|
|
2191
|
-
|
|
2133
|
+
var entityType = tabConfig.entity;
|
|
2134
|
+
var listConfig = tabConfig.list || {};
|
|
2135
|
+
var fields = listConfig.fields || ['title', 'status'];
|
|
2136
|
+
var expandChildren = listConfig.expandChildren;
|
|
2137
|
+
var items = getFilteredItems(tabConfig);
|
|
2192
2138
|
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
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>' +
|
|
2209
|
-
'</div>';
|
|
2139
|
+
if (items.length === 0) {
|
|
2140
|
+
container.innerHTML = emptyState(entityType);
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2210
2143
|
|
|
2211
|
-
|
|
2144
|
+
var html = '<div class="entity-list">';
|
|
2145
|
+
for (var i = 0; i < items.length; i++) {
|
|
2146
|
+
var item = items[i];
|
|
2147
|
+
html += renderListItem(item, entityType, fields, expandChildren, tabConfig);
|
|
2148
|
+
}
|
|
2149
|
+
html += '</div>';
|
|
2150
|
+
container.innerHTML = html;
|
|
2151
|
+
|
|
2152
|
+
// Click handlers for main items
|
|
2153
|
+
container.querySelectorAll('.entity-list-item[data-id]').forEach(function(el) {
|
|
2154
|
+
el.addEventListener('click', function(e) {
|
|
2155
|
+
if (e.target.closest('.child-list-item')) return;
|
|
2156
|
+
if (e.target.closest('.list-expand-btn')) return;
|
|
2157
|
+
var item = getEntities(entityType).find(function(i) { return i.id === el.dataset.id; });
|
|
2158
|
+
if (item) openPanel(entityType, item);
|
|
2159
|
+
});
|
|
2160
|
+
});
|
|
2212
2161
|
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2162
|
+
// Expand toggle handlers
|
|
2163
|
+
container.querySelectorAll('.list-expand-btn').forEach(function(btn) {
|
|
2164
|
+
btn.addEventListener('click', function(e) {
|
|
2165
|
+
e.stopPropagation();
|
|
2166
|
+
var children = btn.closest('.entity-list-item').querySelector('.list-children');
|
|
2167
|
+
if (children) {
|
|
2168
|
+
var hidden = children.style.display === 'none';
|
|
2169
|
+
children.style.display = hidden ? '' : 'none';
|
|
2170
|
+
btn.textContent = hidden ? '\u25BC' : '\u25B6';
|
|
2171
|
+
}
|
|
2172
|
+
});
|
|
2216
2173
|
});
|
|
2217
|
-
}
|
|
2218
2174
|
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2175
|
+
// Child click handlers
|
|
2176
|
+
if (expandChildren) {
|
|
2177
|
+
container.querySelectorAll('.child-list-item[data-id]').forEach(function(el) {
|
|
2178
|
+
el.addEventListener('click', function(e) {
|
|
2179
|
+
e.stopPropagation();
|
|
2180
|
+
var childType = el.dataset.type;
|
|
2181
|
+
var childItem = getEntities(childType).find(function(i) { return i.id === el.dataset.id; });
|
|
2182
|
+
if (childItem) openPanel(childType, childItem);
|
|
2183
|
+
});
|
|
2184
|
+
});
|
|
2185
|
+
}
|
|
2224
2186
|
}
|
|
2225
2187
|
|
|
2226
|
-
function
|
|
2227
|
-
var
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2188
|
+
function renderListItem(item, entityType, fields, expandChildren, tabConfig) {
|
|
2189
|
+
var hasChildren = false;
|
|
2190
|
+
var children = [];
|
|
2191
|
+
if (expandChildren && expandChildren.entity) {
|
|
2192
|
+
var childType = expandChildren.entity;
|
|
2193
|
+
children = getEntities(childType).filter(function(child) {
|
|
2194
|
+
return child['_' + entityType] === item.id ||
|
|
2195
|
+
child['_' + entityType] === (item._dir || item.id);
|
|
2234
2196
|
});
|
|
2197
|
+
hasChildren = children.length > 0;
|
|
2235
2198
|
}
|
|
2236
2199
|
|
|
2237
|
-
|
|
2238
|
-
|
|
2200
|
+
var html = '<div class="entity-list-item" data-id="' + escHtml(item.id || '') + '">';
|
|
2201
|
+
html += '<div class="list-item-main">';
|
|
2202
|
+
|
|
2203
|
+
// Expand button
|
|
2204
|
+
if (hasChildren) {
|
|
2205
|
+
html += '<button class="list-expand-btn">\u25B6</button>';
|
|
2206
|
+
} else if (expandChildren) {
|
|
2207
|
+
html += '<span style="width:20px;display:inline-block"></span>';
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
// Status icon
|
|
2211
|
+
if (item.status) {
|
|
2212
|
+
html += '<span class="list-item-status">' + statusIcon(entityType, item.status) + '</span>';
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
// Title
|
|
2216
|
+
html += '<span class="list-item-title">' + escHtml(item.title || item.id || '') + '</span>';
|
|
2217
|
+
|
|
2218
|
+
// Fields
|
|
2219
|
+
html += '<span class="list-item-fields">';
|
|
2220
|
+
for (var i = 0; i < fields.length; i++) {
|
|
2221
|
+
var f = fields[i];
|
|
2222
|
+
if (f === 'title' || f === 'id') continue;
|
|
2223
|
+
if (f === 'progress' && item._progress != null) {
|
|
2224
|
+
var pct = item._progress || 0;
|
|
2225
|
+
var clr = pct >= 100 ? 'var(--success)' : pct > 50 ? 'var(--warning)' : 'var(--accent)';
|
|
2226
|
+
html += '<span class="list-item-progress"><div class="progress" style="width:80px"><div class="progress-fill" style="width:' + pct + '%;background:' + clr + '"></div></div><span class="pill pill-points">' + pct + '%</span></span>';
|
|
2227
|
+
continue;
|
|
2228
|
+
}
|
|
2229
|
+
html += renderFieldPill(item, entityType, f);
|
|
2230
|
+
}
|
|
2231
|
+
html += '</span>';
|
|
2232
|
+
html += '</div>';
|
|
2233
|
+
|
|
2234
|
+
// Children
|
|
2235
|
+
if (hasChildren && expandChildren) {
|
|
2236
|
+
var childType = expandChildren.entity;
|
|
2237
|
+
var childFields = expandChildren.fields || ['title', 'status'];
|
|
2238
|
+
html += '<div class="list-children" style="display:none">';
|
|
2239
|
+
for (var ci = 0; ci < children.length; ci++) {
|
|
2240
|
+
var child = children[ci];
|
|
2241
|
+
html += '<div class="child-list-item" data-id="' + escHtml(child.id || '') + '" data-type="' + escHtml(childType) + '">';
|
|
2242
|
+
if (child.status) html += statusIcon(childType, child.status);
|
|
2243
|
+
html += '<span class="child-list-title">' + escHtml(child.title || child.id || '') + '</span>';
|
|
2244
|
+
for (var fi = 0; fi < childFields.length; fi++) {
|
|
2245
|
+
var cf = childFields[fi];
|
|
2246
|
+
if (cf === 'title') continue;
|
|
2247
|
+
html += renderFieldPill(child, childType, cf);
|
|
2248
|
+
}
|
|
2249
|
+
html += '</div>';
|
|
2250
|
+
}
|
|
2251
|
+
html += '</div>';
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
html += '</div>';
|
|
2255
|
+
return html;
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
|
|
2259
|
+
/* ══════════════════════════════════════════════════════════════
|
|
2260
|
+
METRICS VIEW — Config-driven metric cards
|
|
2261
|
+
══════════════════════════════════════════════════════════════ */
|
|
2262
|
+
|
|
2263
|
+
function renderMetrics(container, tabConfig) {
|
|
2264
|
+
if (!D.loaded) {
|
|
2265
|
+
container.innerHTML = '<div class="metrics-grid">' +
|
|
2266
|
+
'<div class="skeleton skeleton-card" style="height:200px"></div>'.repeat(4) + '</div>';
|
|
2267
|
+
return;
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
var metricsConfig = tabConfig && tabConfig.metrics ? tabConfig.metrics : null;
|
|
2271
|
+
var cards = metricsConfig && metricsConfig.cards ? metricsConfig.cards : [];
|
|
2272
|
+
|
|
2273
|
+
// If no config, render default cards
|
|
2274
|
+
if (cards.length === 0) {
|
|
2275
|
+
cards = [
|
|
2276
|
+
{ title: 'Status Breakdown', type: 'status-breakdown', entity: 'task' },
|
|
2277
|
+
{ title: 'Project Health', type: 'health' }
|
|
2278
|
+
];
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
var html = '<div class="metrics-grid">';
|
|
2282
|
+
for (var i = 0; i < cards.length; i++) {
|
|
2283
|
+
html += renderMetricCard(cards[i]);
|
|
2284
|
+
}
|
|
2285
|
+
html += '</div>';
|
|
2286
|
+
container.innerHTML = html;
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
function renderMetricCard(cardConfig) {
|
|
2290
|
+
var html = '<div class="metric-card"><h3>' + escHtml(cardConfig.title) + '</h3>';
|
|
2291
|
+
|
|
2292
|
+
switch (cardConfig.type) {
|
|
2293
|
+
case 'progress-by-parent':
|
|
2294
|
+
html += renderProgressByParent(cardConfig);
|
|
2295
|
+
break;
|
|
2296
|
+
case 'status-breakdown':
|
|
2297
|
+
html += renderStatusBreakdown(cardConfig);
|
|
2298
|
+
break;
|
|
2299
|
+
case 'health':
|
|
2300
|
+
html += renderHealthCard(cardConfig);
|
|
2301
|
+
break;
|
|
2302
|
+
case 'hill-chart':
|
|
2303
|
+
html += renderHillChart(cardConfig);
|
|
2304
|
+
break;
|
|
2305
|
+
default:
|
|
2306
|
+
html += '<div style="color:var(--text3);padding:8px 0">Unknown card type: ' + escHtml(cardConfig.type) + '</div>';
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
html += '</div>';
|
|
2310
|
+
return html;
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
function renderProgressByParent(config) {
|
|
2314
|
+
var parentType = config.parent;
|
|
2315
|
+
if (!parentType) return '<div style="color:var(--text3)">No parent type configured.</div>';
|
|
2316
|
+
|
|
2317
|
+
var parents = getEntities(parentType);
|
|
2318
|
+
if (parents.length === 0) {
|
|
2319
|
+
var singular = getEntitySingular(parentType);
|
|
2320
|
+
return '<div style="color:var(--text3);padding:8px 0">No ' + escHtml(singular.toLowerCase()) + 's.</div>';
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
var html = '';
|
|
2324
|
+
for (var i = 0; i < parents.length; i++) {
|
|
2325
|
+
var p = parents[i];
|
|
2326
|
+
var pct = p._progress || 0;
|
|
2327
|
+
var clr = pct >= 100 ? 'var(--success)' : pct > 50 ? 'var(--warning)' : 'var(--accent)';
|
|
2328
|
+
html += '<div class="health-row">' +
|
|
2329
|
+
'<div class="health-dot" style="background:' + clr + '"></div>' +
|
|
2330
|
+
statusIcon(parentType, p.status) +
|
|
2331
|
+
'<div class="health-label">' + escHtml(p.title || p.id || '') + '</div>' +
|
|
2332
|
+
'<div class="health-val">' + pct + '%</div>' +
|
|
2333
|
+
'</div>';
|
|
2334
|
+
}
|
|
2335
|
+
return html;
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
function renderStatusBreakdown(config) {
|
|
2339
|
+
var entityType = config.entity;
|
|
2340
|
+
if (!entityType) return '<div style="color:var(--text3)">No entity type configured.</div>';
|
|
2341
|
+
|
|
2342
|
+
var items = getEntities(entityType);
|
|
2343
|
+
var statuses = {};
|
|
2344
|
+
items.forEach(function(item) {
|
|
2345
|
+
var s = item.status || 'unknown';
|
|
2346
|
+
statuses[s] = (statuses[s] || 0) + 1;
|
|
2347
|
+
});
|
|
2348
|
+
|
|
2349
|
+
var keys = Object.keys(statuses);
|
|
2350
|
+
if (keys.length === 0) return '<div style="color:var(--text3);padding:8px 0">No data.</div>';
|
|
2351
|
+
|
|
2352
|
+
var html = '';
|
|
2353
|
+
for (var i = 0; i < keys.length; i++) {
|
|
2354
|
+
var s = keys[i];
|
|
2355
|
+
html += '<div class="health-row">' +
|
|
2356
|
+
statusIcon(entityType, s) +
|
|
2357
|
+
'<div class="health-label" style="text-transform:capitalize">' + escHtml(getFieldLabel(entityType, 'status', s)) + '</div>' +
|
|
2358
|
+
'<div class="health-val">' + statuses[s] + '</div>' +
|
|
2359
|
+
'</div>';
|
|
2360
|
+
}
|
|
2361
|
+
return html;
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
function renderHealthCard() {
|
|
2365
|
+
var h = D.health || {};
|
|
2366
|
+
var totals = h.totals || {};
|
|
2367
|
+
var total = totals.total || 0;
|
|
2368
|
+
var completed = totals.completed || 0;
|
|
2369
|
+
var inProgress = totals.inProgress || 0;
|
|
2370
|
+
|
|
2371
|
+
return '<div class="health-row"><div class="health-dot" style="background:var(--text2)"></div><div class="health-label">Total Items</div><div class="health-val">' + total + '</div></div>' +
|
|
2372
|
+
'<div class="health-row"><div class="health-dot" style="background:var(--success)"></div><div class="health-label">Completed</div><div class="health-val">' + completed + '</div></div>' +
|
|
2373
|
+
'<div class="health-row"><div class="health-dot" style="background:var(--warning)"></div><div class="health-label">In Progress</div><div class="health-val">' + inProgress + '</div></div>' +
|
|
2374
|
+
'<div class="health-row"><div class="health-dot" style="background:var(--accent)"></div><div class="health-label">Progress</div><div class="health-val">' + (total > 0 ? Math.round(completed / total * 100) + '%' : 'N/A') + '</div></div>';
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
function renderHillChart(config) {
|
|
2378
|
+
var entityType = config.entity;
|
|
2379
|
+
if (!entityType) return '<div style="color:var(--text3)">No entity type configured.</div>';
|
|
2380
|
+
|
|
2381
|
+
var items = getEntities(entityType);
|
|
2382
|
+
if (items.length === 0) {
|
|
2383
|
+
return '<div style="color:var(--text3);padding:8px 0">No ' + escHtml(getEntitySingular(entityType).toLowerCase()) + 's.</div>';
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
// Hill chart: figuring out (uphill) → making it happen (downhill) → done
|
|
2387
|
+
var fields = getEntityFields(entityType);
|
|
2388
|
+
var hillField = fields.hill;
|
|
2389
|
+
if (!hillField || !hillField.values) {
|
|
2390
|
+
return '<div style="color:var(--text3);padding:8px 0">No hill field defined.</div>';
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
var html = '';
|
|
2394
|
+
for (var i = 0; i < items.length; i++) {
|
|
2395
|
+
var item = items[i];
|
|
2396
|
+
var hillVal = item.hill || 'unknown';
|
|
2397
|
+
html += '<div class="health-row">' +
|
|
2398
|
+
statusIcon(entityType, hillVal) +
|
|
2399
|
+
'<div class="health-label">' + escHtml(item.title || item.id || '') + '</div>' +
|
|
2400
|
+
'<div class="health-val" style="font-size:11px">' + escHtml(getFieldLabel(entityType, 'hill', hillVal)) + '</div>' +
|
|
2401
|
+
'</div>';
|
|
2402
|
+
}
|
|
2403
|
+
return html;
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
|
|
2407
|
+
|
|
2408
|
+
/* ══════════════════════════════════════════════════════════════
|
|
2409
|
+
NOTES VIEW — Notion-style split layout (config-driven)
|
|
2410
|
+
══════════════════════════════════════════════════════════════ */
|
|
2411
|
+
|
|
2412
|
+
var notesState = { notes: [], activeId: null, editor: null, dirty: false, entityType: 'note' };
|
|
2413
|
+
|
|
2414
|
+
function renderNotes(container, tabConfig) {
|
|
2415
|
+
if (!container) return;
|
|
2416
|
+
var entityType = tabConfig && tabConfig.entity ? tabConfig.entity : 'note';
|
|
2417
|
+
notesState.entityType = entityType;
|
|
2418
|
+
var plural = getEntityPlural(entityType);
|
|
2419
|
+
|
|
2420
|
+
// Notes view needs special full-height layout
|
|
2421
|
+
var viewEl = container.closest('.view');
|
|
2422
|
+
if (viewEl) {
|
|
2423
|
+
viewEl.style.display = 'flex';
|
|
2424
|
+
viewEl.style.flexDirection = 'column';
|
|
2425
|
+
viewEl.style.margin = '-20px -24px';
|
|
2426
|
+
viewEl.style.height = 'calc(100% + 40px)';
|
|
2427
|
+
}
|
|
2428
|
+
container.style.flex = '1';
|
|
2429
|
+
container.style.overflow = 'hidden';
|
|
2430
|
+
|
|
2431
|
+
container.innerHTML =
|
|
2432
|
+
'<div class="notes-layout">' +
|
|
2433
|
+
'<div class="notes-sidebar">' +
|
|
2434
|
+
'<div class="notes-toolbar">' +
|
|
2435
|
+
'<input type="text" placeholder="Search..." id="notes-search">' +
|
|
2436
|
+
'<button class="btn btn-sm btn-create" id="notes-new">+ New</button>' +
|
|
2437
|
+
'</div>' +
|
|
2438
|
+
'<div class="notes-list" id="notes-list"></div>' +
|
|
2439
|
+
'</div>' +
|
|
2440
|
+
'<div class="notes-editor" id="notes-editor-pane">' +
|
|
2441
|
+
'<div class="notes-empty-state">' +
|
|
2442
|
+
'<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>' +
|
|
2443
|
+
'<p>Select a note or create a new one</p>' +
|
|
2444
|
+
'<p style="font-size:12px;margin-top:4px;color:var(--text3)">Use <kbd>/</kbd> for block commands</p>' +
|
|
2445
|
+
'</div>' +
|
|
2446
|
+
'</div>' +
|
|
2447
|
+
'</div>';
|
|
2448
|
+
|
|
2449
|
+
loadNoteList();
|
|
2450
|
+
|
|
2451
|
+
document.getElementById('notes-new').addEventListener('click', createNewNote);
|
|
2452
|
+
document.getElementById('notes-search').addEventListener('input', function() {
|
|
2453
|
+
renderNoteList(this.value.toLowerCase());
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
async function loadNoteList() {
|
|
2458
|
+
var base = apiBase();
|
|
2459
|
+
var plural = getEntityPlural(notesState.entityType);
|
|
2460
|
+
var notes = await fetchJson(base + '/' + plural);
|
|
2461
|
+
notesState.notes = notes || [];
|
|
2462
|
+
renderNoteList('');
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
function renderNoteList(filter) {
|
|
2466
|
+
var list = document.getElementById('notes-list');
|
|
2467
|
+
if (!list) return;
|
|
2468
|
+
|
|
2469
|
+
var notes = notesState.notes;
|
|
2470
|
+
if (filter) {
|
|
2471
|
+
notes = notes.filter(function(n) {
|
|
2472
|
+
return (n.title || '').toLowerCase().indexOf(filter) !== -1;
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
notes.sort(function(a, b) {
|
|
2477
|
+
return (b.updated || b.created || '').localeCompare(a.updated || a.created || '');
|
|
2239
2478
|
});
|
|
2240
2479
|
|
|
2241
2480
|
if (notes.length === 0) {
|
|
@@ -2276,7 +2515,8 @@ async function selectNote(id) {
|
|
|
2276
2515
|
pane.innerHTML = '<div class="notes-editor-loading">Loading...</div>';
|
|
2277
2516
|
|
|
2278
2517
|
var base = apiBase();
|
|
2279
|
-
var
|
|
2518
|
+
var plural = getEntityPlural(notesState.entityType);
|
|
2519
|
+
var note = await fetchJson(base + '/' + plural + '/' + encodeURIComponent(id));
|
|
2280
2520
|
if (!note) {
|
|
2281
2521
|
pane.innerHTML = '<div class="notes-empty-state"><p>Note not found</p></div>';
|
|
2282
2522
|
return;
|
|
@@ -2306,7 +2546,6 @@ async function selectNote(id) {
|
|
|
2306
2546
|
deleteCurrentNote(id);
|
|
2307
2547
|
});
|
|
2308
2548
|
|
|
2309
|
-
// Ctrl/Cmd+S to save
|
|
2310
2549
|
pane.addEventListener('keydown', function(e) {
|
|
2311
2550
|
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
|
|
2312
2551
|
e.preventDefault();
|
|
@@ -2317,6 +2556,7 @@ async function selectNote(id) {
|
|
|
2317
2556
|
|
|
2318
2557
|
async function saveCurrentNote() {
|
|
2319
2558
|
if (!notesState.activeId) return;
|
|
2559
|
+
var plural = getEntityPlural(notesState.entityType);
|
|
2320
2560
|
|
|
2321
2561
|
var titleEl = document.getElementById('notes-title');
|
|
2322
2562
|
var title = titleEl ? titleEl.value : '';
|
|
@@ -2328,7 +2568,7 @@ async function saveCurrentNote() {
|
|
|
2328
2568
|
updates.content = content;
|
|
2329
2569
|
updates.updated = new Date().toISOString().split('T')[0];
|
|
2330
2570
|
|
|
2331
|
-
var ok = await patchItem(
|
|
2571
|
+
var ok = await patchItem(plural, notesState.activeId, updates);
|
|
2332
2572
|
if (ok) {
|
|
2333
2573
|
notesState.dirty = false;
|
|
2334
2574
|
await loadNoteList();
|
|
@@ -2336,7 +2576,8 @@ async function saveCurrentNote() {
|
|
|
2336
2576
|
}
|
|
2337
2577
|
|
|
2338
2578
|
async function createNewNote() {
|
|
2339
|
-
var
|
|
2579
|
+
var plural = getEntityPlural(notesState.entityType);
|
|
2580
|
+
var result = await createItem(plural, { title: 'Untitled', content: '' });
|
|
2340
2581
|
if (result && result.id) {
|
|
2341
2582
|
await loadNoteList();
|
|
2342
2583
|
selectNote(result.id);
|
|
@@ -2345,7 +2586,8 @@ async function createNewNote() {
|
|
|
2345
2586
|
|
|
2346
2587
|
async function deleteCurrentNote(id) {
|
|
2347
2588
|
if (!confirm('Delete this note?')) return;
|
|
2348
|
-
var
|
|
2589
|
+
var plural = getEntityPlural(notesState.entityType);
|
|
2590
|
+
var ok = await deleteItem(plural, id);
|
|
2349
2591
|
if (ok) {
|
|
2350
2592
|
if (notesState.editor) {
|
|
2351
2593
|
destroyEditor(notesState.editor);
|
|
@@ -2366,268 +2608,140 @@ async function deleteCurrentNote(id) {
|
|
|
2366
2608
|
|
|
2367
2609
|
|
|
2368
2610
|
/* ══════════════════════════════════════════════════════════════
|
|
2369
|
-
|
|
2611
|
+
PANEL — Dynamic detail panel from config
|
|
2370
2612
|
══════════════════════════════════════════════════════════════ */
|
|
2371
|
-
|
|
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
|
-
}
|
|
2613
|
+
var panelState = { open: false, entityType: null, item: null, isCreate: false, editor: null };
|
|
2376
2614
|
|
|
2377
|
-
function
|
|
2378
|
-
|
|
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();
|
|
2615
|
+
async function openPanel(entityType, item) {
|
|
2616
|
+
panelState = { open: true, entityType: entityType, item: JSON.parse(JSON.stringify(item)), isCreate: false, editor: null };
|
|
2408
2617
|
document.getElementById('detail-panel').classList.add('open');
|
|
2409
2618
|
document.getElementById('panel-overlay').classList.add('open');
|
|
2410
|
-
}
|
|
2411
2619
|
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
var
|
|
2620
|
+
// Fetch full item with content from single endpoint
|
|
2621
|
+
var plural = getEntityPlural(entityType);
|
|
2622
|
+
var base = apiBase();
|
|
2623
|
+
var full = await fetchJson(base + '/' + plural + '/' + encodeURIComponent(item.id));
|
|
2624
|
+
if (full && full.content !== undefined) {
|
|
2625
|
+
panelState.item.content = full.content;
|
|
2626
|
+
}
|
|
2416
2627
|
|
|
2417
|
-
function openPanel(type, item) {
|
|
2418
|
-
panelState = { open: true, type: type, item: JSON.parse(JSON.stringify(item)), isCreate: false, editor: null };
|
|
2419
2628
|
renderPanel();
|
|
2420
|
-
document.getElementById('detail-panel').classList.add('open');
|
|
2421
|
-
document.getElementById('panel-overlay').classList.add('open');
|
|
2422
2629
|
}
|
|
2423
2630
|
|
|
2424
2631
|
function closePanel() {
|
|
2425
2632
|
if (panelState.editor) { destroyEditor(panelState.editor); panelState.editor = null; }
|
|
2426
|
-
panelState = { open: false,
|
|
2633
|
+
panelState = { open: false, entityType: null, item: null, isCreate: false, editor: null };
|
|
2427
2634
|
document.getElementById('detail-panel').classList.remove('open');
|
|
2428
2635
|
document.getElementById('panel-overlay').classList.remove('open');
|
|
2429
2636
|
}
|
|
2430
2637
|
|
|
2638
|
+
function openCreateDialog(entityType) {
|
|
2639
|
+
var fields = getEntityFields(entityType);
|
|
2640
|
+
var item = { _isNew: true, title: '' };
|
|
2641
|
+
|
|
2642
|
+
// Set defaults from field definitions
|
|
2643
|
+
for (var name in fields) {
|
|
2644
|
+
var f = fields[name];
|
|
2645
|
+
if (f.default !== undefined) item[name] = f.default;
|
|
2646
|
+
else if (f.type === 'enum' && f.values && f.values.length > 0) item[name] = f.values[0].key;
|
|
2647
|
+
else if (f.type === 'number') item[name] = null;
|
|
2648
|
+
else if (f.type === 'list') item[name] = '';
|
|
2649
|
+
else item[name] = '';
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
// Set ancestor defaults
|
|
2653
|
+
var ancestors = findAncestors(entityType);
|
|
2654
|
+
for (var i = 0; i < ancestors.length; i++) {
|
|
2655
|
+
var ancType = ancestors[i];
|
|
2656
|
+
var ancItems = getEntities(ancType);
|
|
2657
|
+
if (ancItems.length > 0) {
|
|
2658
|
+
item[ancType] = ancItems[0]._dir || ancItems[0].id || '';
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
panelState = { open: true, entityType: entityType, item: item, isCreate: true, editor: null };
|
|
2663
|
+
renderPanel();
|
|
2664
|
+
document.getElementById('detail-panel').classList.add('open');
|
|
2665
|
+
document.getElementById('panel-overlay').classList.add('open');
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2431
2668
|
function renderPanel() {
|
|
2432
2669
|
var panel = document.getElementById('detail-panel');
|
|
2433
|
-
var
|
|
2670
|
+
var entityType = panelState.entityType;
|
|
2434
2671
|
var item = panelState.item;
|
|
2435
2672
|
var isCreate = panelState.isCreate;
|
|
2436
|
-
if (!item) return;
|
|
2437
|
-
|
|
2438
|
-
var isReadonly = !isCreate && item.readonly;
|
|
2673
|
+
if (!item || !entityType) return;
|
|
2439
2674
|
|
|
2440
|
-
var
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2675
|
+
var isReadonly = !isCreate && (item._readonly || !isSourceWritable());
|
|
2676
|
+
var typeLabel = getEntitySingular(entityType);
|
|
2677
|
+
var fields = getEntityFields(entityType);
|
|
2678
|
+
var plural = getEntityPlural(entityType);
|
|
2444
2679
|
|
|
2445
2680
|
// ── Header ──
|
|
2446
2681
|
var html = '<div class="panel-header">' +
|
|
2447
2682
|
'<span class="panel-type">' + escHtml(isCreate ? 'New ' + typeLabel : typeLabel) + '</span>' +
|
|
2448
2683
|
'<span class="panel-item-id">' + escHtml(isCreate ? '' : (item.id || '')) + '</span>';
|
|
2449
2684
|
|
|
2450
|
-
if (item.
|
|
2451
|
-
html += '<span class="pill" style="background:' + item.
|
|
2685
|
+
if (item._source && item._sourceColor) {
|
|
2686
|
+
html += '<span class="pill" style="background:' + item._sourceColor + '20;color:' + item._sourceColor + ';font-size:10px">' + escHtml(item._sourceLabel || item._source) + '</span>';
|
|
2452
2687
|
}
|
|
2453
2688
|
|
|
2454
2689
|
html += '<button class="panel-close" id="panel-close-btn">×</button></div>';
|
|
2455
2690
|
|
|
2456
|
-
// ── Title
|
|
2691
|
+
// ── Title ──
|
|
2457
2692
|
html += '<div class="panel-title-wrap">' +
|
|
2458
2693
|
'<input type="text" id="p-title" class="panel-title-input" value="' + escHtml(item.title || '') + '" placeholder="Untitled"' + (isReadonly ? ' disabled' : '') + '>' +
|
|
2459
2694
|
'</div>';
|
|
2460
2695
|
|
|
2461
|
-
// ── Properties
|
|
2696
|
+
// ── Properties ──
|
|
2462
2697
|
html += '<div class="panel-properties">';
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
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) {
|
|
2492
|
-
return '<option value="' + s + '"' + (item.status === s ? ' selected' : '') + '>' + (statusLabels[s] || s) + '</option>';
|
|
2493
|
-
}).join('') + '</select></div></div>';
|
|
2494
|
-
|
|
2495
|
-
if (t === 'tasks' || t === 'epics') {
|
|
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) {
|
|
2498
|
-
return '<option value="' + p + '"' + (item.priority === p ? ' selected' : '') + '>' + (PRIORITY_LABELS[p] || p) + '</option>';
|
|
2499
|
-
}).join('') + '</select></div></div>';
|
|
2500
|
-
}
|
|
2501
|
-
|
|
2502
|
-
if (t === 'tasks') {
|
|
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>';
|
|
2504
|
-
|
|
2505
|
-
var assignedVal = Array.isArray(item.assigned) ? item.assigned.join(', ') : (item.assigned || '');
|
|
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>';
|
|
2507
|
-
|
|
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>' +
|
|
2509
|
-
D.allSprints.map(function(s) { return '<option value="' + escHtml(s.id || '') + '"' + (item.sprint === s.id ? ' selected' : '') + '>' + escHtml(s.id || '') + '</option>'; }).join('') +
|
|
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
|
-
}
|
|
2522
|
-
}
|
|
2523
|
-
|
|
2524
|
-
if (t === 'milestones') {
|
|
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>';
|
|
2526
|
-
}
|
|
2527
|
-
|
|
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
|
-
}
|
|
2535
|
-
|
|
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;
|
|
2698
|
+
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">▼</span> Properties</span><div class="prop-value"></div></div>';
|
|
2699
|
+
html += '<div class="props-fields-body" id="props-fields-body">';
|
|
2700
|
+
|
|
2701
|
+
// Generate property rows from field definitions
|
|
2702
|
+
for (var name in fields) {
|
|
2703
|
+
var field = fields[name];
|
|
2704
|
+
var value = item[name];
|
|
2705
|
+
html += renderPropRow(name, field, value, entityType, isReadonly);
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
// Ancestor fields for create
|
|
2709
|
+
if (isCreate) {
|
|
2710
|
+
var ancestors = findAncestors(entityType);
|
|
2711
|
+
for (var ai = 0; ai < ancestors.length; ai++) {
|
|
2712
|
+
var ancType = ancestors[ai];
|
|
2713
|
+
var ancDef = getEntityDef(ancType);
|
|
2714
|
+
var ancItems = getEntities(ancType);
|
|
2715
|
+
var ancLabel = ancDef ? ancDef.singular : ancType;
|
|
2716
|
+
var ancVal = item[ancType] || '';
|
|
2717
|
+
|
|
2718
|
+
html += '<div class="prop-row"><span class="prop-label">' + escHtml(ancLabel) + '</span><div class="prop-value">';
|
|
2719
|
+
html += '<select id="p-ancestor-' + ancType + '">';
|
|
2720
|
+
for (var aci = 0; aci < ancItems.length; aci++) {
|
|
2721
|
+
var a = ancItems[aci];
|
|
2722
|
+
var aId = a._dir || a.id || '';
|
|
2723
|
+
html += '<option value="' + escHtml(aId) + '"' + (ancVal === aId ? ' selected' : '') + '>' + escHtml(a.title || a.id || '') + '</option>';
|
|
2546
2724
|
}
|
|
2547
|
-
html += '
|
|
2548
|
-
'<span class="link-chip-dot" style="background:' + srcColor + '"></span>' +
|
|
2549
|
-
escHtml(String(link)) + '</span>';
|
|
2550
|
-
});
|
|
2551
|
-
html += '</div></div></div>';
|
|
2552
|
-
}
|
|
2553
|
-
|
|
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>';
|
|
2725
|
+
html += '</select></div></div>';
|
|
2566
2726
|
}
|
|
2567
|
-
}
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
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">▶</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">×</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>';
|
|
2727
|
+
} else {
|
|
2728
|
+
// Show ancestor refs as readonly
|
|
2729
|
+
var ancestors = findAncestors(entityType);
|
|
2730
|
+
for (var ai = 0; ai < ancestors.length; ai++) {
|
|
2731
|
+
var ancType = ancestors[ai];
|
|
2732
|
+
var ancVal = item['_' + ancType];
|
|
2733
|
+
if (ancVal) {
|
|
2734
|
+
var ancDef = getEntityDef(ancType);
|
|
2735
|
+
var ancLabel = ancDef ? ancDef.singular : ancType;
|
|
2736
|
+
html += '<div class="prop-row"><span class="prop-label">' + escHtml(ancLabel) + '</span><div class="prop-value"><span class="prop-text">' + escHtml(ancVal) + '</span></div></div>';
|
|
2622
2737
|
}
|
|
2623
|
-
html += '</div></div>';
|
|
2624
2738
|
}
|
|
2625
|
-
html += '</div>'; // close ai-fields-body
|
|
2626
2739
|
}
|
|
2627
2740
|
|
|
2741
|
+
html += '</div>'; // close props-fields-body
|
|
2628
2742
|
html += '</div>'; // close panel-properties
|
|
2629
2743
|
|
|
2630
|
-
// ── Content Editor
|
|
2744
|
+
// ── Content Editor ──
|
|
2631
2745
|
if (isReadonly) {
|
|
2632
2746
|
html += '<div class="panel-content-area"><div id="p-content-editor" class="panel-editor-zone md-rendered">' + simpleMarkdownRender(item.content || '') + '</div></div>';
|
|
2633
2747
|
} else {
|
|
@@ -2636,7 +2750,7 @@ function renderPanel() {
|
|
|
2636
2750
|
|
|
2637
2751
|
// ── Footer ──
|
|
2638
2752
|
html += '<div class="panel-footer">';
|
|
2639
|
-
if (!isCreate && !isReadonly
|
|
2753
|
+
if (!isCreate && !isReadonly) {
|
|
2640
2754
|
html += '<button class="btn btn-danger btn-sm" id="panel-archive-btn">Archive</button>';
|
|
2641
2755
|
}
|
|
2642
2756
|
html += '<span style="flex:1"></span>';
|
|
@@ -2648,7 +2762,7 @@ function renderPanel() {
|
|
|
2648
2762
|
|
|
2649
2763
|
panel.innerHTML = html;
|
|
2650
2764
|
|
|
2651
|
-
// Initialize Editor.js
|
|
2765
|
+
// Initialize Editor.js
|
|
2652
2766
|
if (!isReadonly) {
|
|
2653
2767
|
panelState.editor = initEditor('p-content-editor', item.content || '', {
|
|
2654
2768
|
placeholder: 'Type / for commands, or start writing...',
|
|
@@ -2666,13 +2780,13 @@ function renderPanel() {
|
|
|
2666
2780
|
if (archiveBtn) {
|
|
2667
2781
|
archiveBtn.addEventListener('click', function() {
|
|
2668
2782
|
if (!confirm('Archive this ' + typeLabel.toLowerCase() + '?')) return;
|
|
2669
|
-
deleteItem(
|
|
2783
|
+
deleteItem(plural, item.id).then(function(ok) {
|
|
2670
2784
|
if (ok) { closePanel(); refreshData(); }
|
|
2671
2785
|
});
|
|
2672
2786
|
});
|
|
2673
2787
|
}
|
|
2674
2788
|
|
|
2675
|
-
// Properties
|
|
2789
|
+
// Properties toggle
|
|
2676
2790
|
var propsToggle = document.getElementById('props-toggle');
|
|
2677
2791
|
if (propsToggle) {
|
|
2678
2792
|
propsToggle.addEventListener('click', function() {
|
|
@@ -2688,139 +2802,50 @@ function renderPanel() {
|
|
|
2688
2802
|
});
|
|
2689
2803
|
}
|
|
2690
2804
|
|
|
2691
|
-
//
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
var
|
|
2696
|
-
var
|
|
2697
|
-
if (
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2805
|
+
// Longtext expand/collapse
|
|
2806
|
+
panel.querySelectorAll('.prop-longtext-preview').forEach(function(prev) {
|
|
2807
|
+
prev.addEventListener('click', function() {
|
|
2808
|
+
var wrap = prev.parentNode;
|
|
2809
|
+
var textarea = wrap.querySelector('.prop-longtext-editor');
|
|
2810
|
+
var toggle = prev.querySelector('.prop-longtext-toggle');
|
|
2811
|
+
if (textarea.style.display === 'none') {
|
|
2812
|
+
textarea.style.display = '';
|
|
2813
|
+
prev.style.display = 'none';
|
|
2814
|
+
textarea.style.height = 'auto';
|
|
2815
|
+
textarea.style.height = Math.max(80, textarea.scrollHeight) + 'px';
|
|
2816
|
+
textarea.focus();
|
|
2703
2817
|
}
|
|
2704
2818
|
});
|
|
2705
|
-
}
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
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">×</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(); });
|
|
2819
|
+
});
|
|
2820
|
+
panel.querySelectorAll('.prop-longtext-editor').forEach(function(ta) {
|
|
2821
|
+
ta.addEventListener('blur', function() {
|
|
2822
|
+
var wrap = ta.parentNode;
|
|
2823
|
+
var prev = wrap.querySelector('.prop-longtext-preview');
|
|
2824
|
+
var textSpan = prev.querySelector('.prop-longtext-text');
|
|
2825
|
+
var val = ta.value || '';
|
|
2826
|
+
var truncated = val.length > 80 ? val.substring(0, 80) + '...' : (val || 'Empty');
|
|
2827
|
+
textSpan.textContent = truncated;
|
|
2828
|
+
ta.style.display = 'none';
|
|
2829
|
+
prev.style.display = '';
|
|
2775
2830
|
});
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
if (e.target === container) textInput.focus();
|
|
2831
|
+
ta.addEventListener('input', function() {
|
|
2832
|
+
ta.style.height = 'auto';
|
|
2833
|
+
ta.style.height = Math.max(80, ta.scrollHeight) + 'px';
|
|
2780
2834
|
});
|
|
2835
|
+
});
|
|
2781
2836
|
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
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();
|
|
2837
|
+
// Live icon updates for enum fields
|
|
2838
|
+
panel.querySelectorAll('select[id^="p-"]').forEach(function(sel) {
|
|
2839
|
+
sel.addEventListener('change', function() {
|
|
2840
|
+
var iconEl = sel.parentNode.querySelector('.prop-icon');
|
|
2841
|
+
if (iconEl) {
|
|
2842
|
+
var fieldName = sel.id.replace('p-', '');
|
|
2843
|
+
iconEl.innerHTML = statusIcon(entityType, sel.value);
|
|
2798
2844
|
}
|
|
2799
2845
|
});
|
|
2800
|
-
|
|
2801
|
-
// Close dropdown on blur
|
|
2802
|
-
textInput.addEventListener('blur', function() {
|
|
2803
|
-
setTimeout(closeDropdown, 150);
|
|
2804
|
-
});
|
|
2805
2846
|
});
|
|
2806
2847
|
|
|
2807
|
-
//
|
|
2808
|
-
var statusSel = document.getElementById('p-status');
|
|
2809
|
-
if (statusSel) {
|
|
2810
|
-
statusSel.addEventListener('change', function() {
|
|
2811
|
-
var iconEl = document.getElementById('p-status-icon');
|
|
2812
|
-
if (iconEl) iconEl.innerHTML = statusIcon(statusSel.value);
|
|
2813
|
-
});
|
|
2814
|
-
}
|
|
2815
|
-
var prioritySel = document.getElementById('p-priority');
|
|
2816
|
-
if (prioritySel) {
|
|
2817
|
-
prioritySel.addEventListener('change', function() {
|
|
2818
|
-
var iconEl = document.getElementById('p-priority-icon');
|
|
2819
|
-
if (iconEl) iconEl.innerHTML = priorityIcon(prioritySel.value);
|
|
2820
|
-
});
|
|
2821
|
-
}
|
|
2822
|
-
|
|
2823
|
-
// Ctrl/Cmd+S to save
|
|
2848
|
+
// Ctrl/Cmd+S
|
|
2824
2849
|
panel.addEventListener('keydown', function(e) {
|
|
2825
2850
|
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
|
|
2826
2851
|
e.preventDefault();
|
|
@@ -2828,180 +2853,496 @@ function renderPanel() {
|
|
|
2828
2853
|
else savePanel();
|
|
2829
2854
|
}
|
|
2830
2855
|
});
|
|
2856
|
+
}
|
|
2831
2857
|
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2858
|
+
function renderPropRow(name, field, value, entityType, isReadonly) {
|
|
2859
|
+
var label = field.label || name.charAt(0).toUpperCase() + name.slice(1);
|
|
2860
|
+
var html = '<div class="prop-row"><span class="prop-label">' + escHtml(label) + '</span><div class="prop-value">';
|
|
2861
|
+
|
|
2862
|
+
switch (field.type) {
|
|
2863
|
+
case 'enum':
|
|
2864
|
+
if (field.values) {
|
|
2865
|
+
html += '<span class="prop-icon">' + statusIcon(entityType, value) + '</span>';
|
|
2866
|
+
html += '<select id="p-' + name + '"' + (isReadonly ? ' disabled' : '') + '>';
|
|
2867
|
+
for (var i = 0; i < field.values.length; i++) {
|
|
2868
|
+
var v = field.values[i];
|
|
2869
|
+
html += '<option value="' + escHtml(v.key) + '"' + (value === v.key ? ' selected' : '') + '>' + escHtml(v.label) + '</option>';
|
|
2870
|
+
}
|
|
2871
|
+
html += '</select>';
|
|
2844
2872
|
}
|
|
2845
|
-
|
|
2846
|
-
|
|
2873
|
+
break;
|
|
2874
|
+
|
|
2875
|
+
case 'number':
|
|
2876
|
+
html += '<input type="number" id="p-' + name + '" value="' + (value != null ? value : '') + '" placeholder="0"' + (isReadonly ? ' disabled' : '') + '>';
|
|
2877
|
+
break;
|
|
2878
|
+
|
|
2879
|
+
case 'date':
|
|
2880
|
+
html += '<input type="date" id="p-' + name + '" value="' + fmtDate(value) + '"' + (isReadonly ? ' disabled' : '') + '>';
|
|
2881
|
+
break;
|
|
2882
|
+
|
|
2883
|
+
case 'string':
|
|
2884
|
+
html += '<input type="text" id="p-' + name + '" value="' + escHtml(value || '') + '" placeholder=""' + (isReadonly ? ' disabled' : '') + '>';
|
|
2885
|
+
break;
|
|
2886
|
+
|
|
2887
|
+
case 'text':
|
|
2888
|
+
html += '<input type="text" id="p-' + name + '" value="' + escHtml(value || '') + '" placeholder=""' + (isReadonly ? ' disabled' : '') + '>';
|
|
2889
|
+
break;
|
|
2890
|
+
|
|
2891
|
+
case 'longtext':
|
|
2892
|
+
var ltVal = value || '';
|
|
2893
|
+
var ltTruncated = ltVal.length > 80 ? ltVal.substring(0, 80) + '...' : ltVal;
|
|
2894
|
+
html += '<div class="prop-longtext" id="p-lt-wrap-' + name + '">';
|
|
2895
|
+
html += '<div class="prop-longtext-preview" id="p-lt-preview-' + name + '" title="Click to expand">';
|
|
2896
|
+
html += '<span class="prop-longtext-text">' + escHtml(ltTruncated || 'Empty') + '</span>';
|
|
2897
|
+
html += '<span class="prop-longtext-toggle">▼</span>';
|
|
2898
|
+
html += '</div>';
|
|
2899
|
+
html += '<textarea id="p-' + name + '" class="prop-longtext-editor" style="display:none" placeholder="Enter text..."' + (isReadonly ? ' disabled' : '') + '>' + escHtml(ltVal) + '</textarea>';
|
|
2900
|
+
html += '</div>';
|
|
2901
|
+
break;
|
|
2902
|
+
|
|
2903
|
+
case 'list':
|
|
2904
|
+
var listVal = Array.isArray(value) ? value.join(', ') : (value || '');
|
|
2905
|
+
html += '<input type="text" id="p-' + name + '" value="' + escHtml(listVal) + '" placeholder="Comma-separated"' + (isReadonly ? ' disabled' : '') + '>';
|
|
2906
|
+
break;
|
|
2907
|
+
|
|
2908
|
+
case 'ref':
|
|
2909
|
+
if (isReadonly || !field.entity) {
|
|
2910
|
+
var refVal = Array.isArray(value) ? value.join(', ') : (value || '');
|
|
2911
|
+
html += '<span class="prop-text">' + escHtml(refVal || 'None') + '</span>';
|
|
2912
|
+
} else {
|
|
2913
|
+
html += '<input type="text" id="p-' + name + '" value="' + escHtml(Array.isArray(value) ? value.join(', ') : (value || '')) + '" placeholder="IDs"' + (isReadonly ? ' disabled' : '') + '>';
|
|
2914
|
+
}
|
|
2915
|
+
break;
|
|
2916
|
+
|
|
2917
|
+
default:
|
|
2918
|
+
html += '<span class="prop-text">' + escHtml(value || '') + '</span>';
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
html += '</div></div>';
|
|
2922
|
+
return html;
|
|
2847
2923
|
}
|
|
2848
2924
|
|
|
2925
|
+
/* ── Save (Update) ───────────────────────────────────────── */
|
|
2849
2926
|
async function savePanel() {
|
|
2850
|
-
var
|
|
2927
|
+
var entityType = panelState.entityType;
|
|
2851
2928
|
var item = panelState.item;
|
|
2852
|
-
if (!
|
|
2929
|
+
if (!entityType || !item) return;
|
|
2853
2930
|
|
|
2931
|
+
var fields = getEntityFields(entityType);
|
|
2932
|
+
var plural = getEntityPlural(entityType);
|
|
2854
2933
|
var updates = {};
|
|
2855
2934
|
|
|
2935
|
+
// Title
|
|
2856
2936
|
var titleEl = document.getElementById('p-title');
|
|
2857
2937
|
if (titleEl && titleEl.value !== (item.title || '')) updates.title = titleEl.value;
|
|
2858
2938
|
|
|
2859
|
-
|
|
2860
|
-
|
|
2939
|
+
// Fields
|
|
2940
|
+
for (var name in fields) {
|
|
2941
|
+
var el = document.getElementById('p-' + name);
|
|
2942
|
+
if (!el) continue;
|
|
2943
|
+
var newVal = readFieldValue(el, fields[name]);
|
|
2944
|
+
var oldVal = item[name];
|
|
2945
|
+
|
|
2946
|
+
if (fields[name].type === 'number') {
|
|
2947
|
+
if (newVal !== oldVal) updates[name] = newVal;
|
|
2948
|
+
} else if (fields[name].type === 'list') {
|
|
2949
|
+
var oldList = Array.isArray(oldVal) ? oldVal.join(', ') : (oldVal || '');
|
|
2950
|
+
if (el.value.trim() !== oldList) updates[name] = newVal;
|
|
2951
|
+
} else if (fields[name].type === 'date') {
|
|
2952
|
+
if (el.value !== fmtDate(oldVal)) updates[name] = newVal;
|
|
2953
|
+
} else {
|
|
2954
|
+
if (newVal !== (oldVal || '')) updates[name] = newVal;
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2861
2957
|
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2958
|
+
// Content
|
|
2959
|
+
if (panelState.editor) {
|
|
2960
|
+
var newContent = await getEditorMarkdown(panelState.editor);
|
|
2961
|
+
if (newContent !== (item.content || '')) updates.content = newContent;
|
|
2865
2962
|
}
|
|
2866
2963
|
|
|
2867
|
-
if (
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2964
|
+
if (Object.keys(updates).length === 0) {
|
|
2965
|
+
showToast('No changes to save', 'success');
|
|
2966
|
+
closePanel();
|
|
2967
|
+
return;
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
var ok = await patchItem(plural, item.id, updates);
|
|
2971
|
+
if (ok) { closePanel(); refreshData(); }
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
/* ── Save (Create) ───────────────────────────────────────── */
|
|
2975
|
+
async function saveCreatePanel() {
|
|
2976
|
+
var entityType = panelState.entityType;
|
|
2977
|
+
if (!entityType) return;
|
|
2978
|
+
|
|
2979
|
+
var fields = getEntityFields(entityType);
|
|
2980
|
+
var plural = getEntityPlural(entityType);
|
|
2981
|
+
var data = {};
|
|
2982
|
+
|
|
2983
|
+
// Title
|
|
2984
|
+
var titleEl = document.getElementById('p-title');
|
|
2985
|
+
if (titleEl) data.title = titleEl.value || 'Untitled';
|
|
2986
|
+
|
|
2987
|
+
// Fields
|
|
2988
|
+
for (var name in fields) {
|
|
2989
|
+
var el = document.getElementById('p-' + name);
|
|
2990
|
+
if (!el) continue;
|
|
2991
|
+
var val = readFieldValue(el, fields[name]);
|
|
2992
|
+
if (val != null && val !== '') data[name] = val;
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
// Ancestors
|
|
2996
|
+
var ancestors = findAncestors(entityType);
|
|
2997
|
+
for (var i = 0; i < ancestors.length; i++) {
|
|
2998
|
+
var ancEl = document.getElementById('p-ancestor-' + ancestors[i]);
|
|
2999
|
+
if (ancEl && ancEl.value) data[ancestors[i]] = ancEl.value;
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
// Content
|
|
3003
|
+
if (panelState.editor) {
|
|
3004
|
+
var content = await getEditorMarkdown(panelState.editor);
|
|
3005
|
+
if (content) data.content = content;
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
var result = await createItem(plural, data);
|
|
3009
|
+
if (result) { closePanel(); refreshData(); }
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
function readFieldValue(el, fieldDef) {
|
|
3013
|
+
switch (fieldDef.type) {
|
|
3014
|
+
case 'number':
|
|
3015
|
+
return el.value ? Number(el.value) : null;
|
|
3016
|
+
case 'list':
|
|
3017
|
+
return el.value.trim() || null;
|
|
3018
|
+
case 'date':
|
|
3019
|
+
return el.value || null;
|
|
3020
|
+
case 'longtext':
|
|
3021
|
+
return el.value || '';
|
|
3022
|
+
case 'enum':
|
|
3023
|
+
case 'string':
|
|
3024
|
+
case 'text':
|
|
3025
|
+
default:
|
|
3026
|
+
return el.value;
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
// Close panel on overlay click or Escape
|
|
3031
|
+
document.getElementById('panel-overlay').addEventListener('click', closePanel);
|
|
3032
|
+
document.addEventListener('keydown', function(e) {
|
|
3033
|
+
if (e.key === 'Escape' && panelState.open) closePanel();
|
|
3034
|
+
});
|
|
3035
|
+
|
|
3036
|
+
|
|
3037
|
+
/* ══════════════════════════════════════════════════════════════
|
|
3038
|
+
WORKSPACE — Source switching & sidebar
|
|
3039
|
+
══════════════════════════════════════════════════════════════ */
|
|
3040
|
+
function renderSidebarLogo() {
|
|
3041
|
+
var p = D.project || {};
|
|
3042
|
+
var logoEl = document.getElementById('sidebar-logo');
|
|
3043
|
+
var iconEl = document.getElementById('sidebar-logo-icon');
|
|
3044
|
+
var textEl = document.getElementById('sidebar-logo-text');
|
|
3045
|
+
if (!iconEl || !textEl) return;
|
|
3046
|
+
|
|
3047
|
+
textEl.textContent = p.name || 'Project';
|
|
3048
|
+
|
|
3049
|
+
if (D.config && D.config.logo) {
|
|
3050
|
+
var logoSrc = D.config.logo.startsWith('http') ? D.config.logo : '/logo';
|
|
3051
|
+
iconEl.innerHTML = '<img src="' + escHtml(logoSrc) + '" alt="">';
|
|
3052
|
+
} else {
|
|
3053
|
+
var initial = (p.name || 'P').charAt(0).toUpperCase();
|
|
3054
|
+
iconEl.textContent = initial;
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
logoEl.onclick = function() {};
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
function buildSourceRail() {
|
|
3061
|
+
var rail = document.getElementById('source-rail');
|
|
3062
|
+
if (!rail) return;
|
|
3063
|
+
rail.innerHTML = '';
|
|
3064
|
+
|
|
3065
|
+
if (hasWorkspace()) {
|
|
3066
|
+
var wsSources = D.config.workspace.sources;
|
|
3067
|
+
for (var i = 0; i < wsSources.length; i++) {
|
|
3068
|
+
var s = wsSources[i];
|
|
3069
|
+
var color = s.color || 'var(--accent)';
|
|
3070
|
+
var icon = document.createElement('div');
|
|
3071
|
+
icon.className = 'rail-icon' + (D.activeSource === s.name ? ' active' : '');
|
|
3072
|
+
icon.dataset.source = s.name;
|
|
3073
|
+
icon.setAttribute('data-tooltip', s.label || s.name);
|
|
3074
|
+
icon.style.background = color + '20';
|
|
3075
|
+
icon.style.color = color;
|
|
3076
|
+
icon.innerHTML = escHtml(s.icon || s.name.charAt(0).toUpperCase());
|
|
3077
|
+
icon.onclick = (function(name) {
|
|
3078
|
+
return function() { switchSource(name); };
|
|
3079
|
+
})(s.name);
|
|
3080
|
+
rail.appendChild(icon);
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
// Add source button — always visible
|
|
3085
|
+
var addBtn = document.createElement('div');
|
|
3086
|
+
addBtn.className = 'rail-icon rail-add';
|
|
3087
|
+
addBtn.setAttribute('data-tooltip', 'Add source');
|
|
3088
|
+
addBtn.textContent = '+';
|
|
3089
|
+
addBtn.onclick = function() { openAddSourceModal(); };
|
|
3090
|
+
rail.appendChild(addBtn);
|
|
3091
|
+
|
|
3092
|
+
// Switch project icon — always visible
|
|
3093
|
+
var switchDivider = document.createElement('div');
|
|
3094
|
+
switchDivider.className = 'rail-divider';
|
|
3095
|
+
switchDivider.style.marginTop = 'auto';
|
|
3096
|
+
rail.appendChild(switchDivider);
|
|
3097
|
+
|
|
3098
|
+
var settingsIcon = document.createElement('div');
|
|
3099
|
+
settingsIcon.className = 'rail-icon rail-switch';
|
|
3100
|
+
settingsIcon.setAttribute('data-tooltip', 'Settings');
|
|
3101
|
+
settingsIcon.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>';
|
|
3102
|
+
settingsIcon.onclick = function() { openSettings(); };
|
|
3103
|
+
rail.appendChild(settingsIcon);
|
|
3104
|
+
|
|
3105
|
+
var switchIcon = document.createElement('div');
|
|
3106
|
+
switchIcon.className = 'rail-icon rail-switch';
|
|
3107
|
+
switchIcon.setAttribute('data-tooltip', 'Switch workspace');
|
|
3108
|
+
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>';
|
|
3109
|
+
switchIcon.onclick = function() { openHistoryModal(); };
|
|
3110
|
+
rail.appendChild(switchIcon);
|
|
3111
|
+
}
|
|
2873
3112
|
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
var av = assignedEl.value.trim();
|
|
2877
|
-
var origAssigned = Array.isArray(item.assigned) ? item.assigned.join(', ') : (item.assigned || '');
|
|
2878
|
-
if (av !== origAssigned) updates.assigned = av || null;
|
|
2879
|
-
}
|
|
3113
|
+
function switchSource(sourceName) {
|
|
3114
|
+
if (sourceName === D.activeSource) return;
|
|
2880
3115
|
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
}
|
|
3116
|
+
D.activeSource = sourceName;
|
|
3117
|
+
D.loaded = false;
|
|
2884
3118
|
|
|
2885
|
-
|
|
2886
|
-
var deadlineEl = document.getElementById('p-deadline');
|
|
2887
|
-
if (deadlineEl && deadlineEl.value !== fmtDate(item.deadline)) updates.deadline = deadlineEl.value || null;
|
|
2888
|
-
}
|
|
3119
|
+
try { localStorage.setItem('mdboard-source', sourceName); } catch(e) {}
|
|
2889
3120
|
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
}
|
|
3121
|
+
document.querySelectorAll('.rail-icon').forEach(function(el) {
|
|
3122
|
+
el.classList.toggle('active', el.dataset.source === sourceName);
|
|
3123
|
+
});
|
|
2894
3124
|
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
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
|
-
}
|
|
3125
|
+
loadAll().then(function() {
|
|
3126
|
+
generateDynamicStyles();
|
|
3127
|
+
renderCurrentView();
|
|
3128
|
+
buildSourceRail();
|
|
3129
|
+
});
|
|
3130
|
+
}
|
|
2914
3131
|
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
return;
|
|
2919
|
-
}
|
|
3132
|
+
/* ── Add Source Modal ────────────────────────────────────── */
|
|
3133
|
+
var _addSourceState = { type: 'local', color: '#5B6EF5' };
|
|
3134
|
+
var _addSourceColors = ['#F97316','#8B5CF6','#EC4899','#10B981','#06B6D4','#5B6EF5','#6366F1','#D4A72C'];
|
|
2920
3135
|
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
3136
|
+
function openAddSourceModal() {
|
|
3137
|
+
_addSourceState = { type: 'local', color: '#5B6EF5' };
|
|
3138
|
+
renderAddSourceModal();
|
|
3139
|
+
var overlay = document.getElementById('addsource-overlay');
|
|
3140
|
+
var modal = document.getElementById('addsource-modal');
|
|
3141
|
+
if (overlay) { overlay.classList.add('open'); overlay.onclick = closeAddSourceModal; }
|
|
3142
|
+
if (modal) modal.classList.add('open');
|
|
2926
3143
|
}
|
|
2927
3144
|
|
|
2928
|
-
|
|
2929
|
-
var
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
3145
|
+
function closeAddSourceModal() {
|
|
3146
|
+
var overlay = document.getElementById('addsource-overlay');
|
|
3147
|
+
var modal = document.getElementById('addsource-modal');
|
|
3148
|
+
if (overlay) overlay.classList.remove('open');
|
|
3149
|
+
if (modal) modal.classList.remove('open');
|
|
3150
|
+
}
|
|
2933
3151
|
|
|
2934
|
-
|
|
2935
|
-
|
|
3152
|
+
function renderAddSourceModal() {
|
|
3153
|
+
var modal = document.getElementById('addsource-modal');
|
|
3154
|
+
if (!modal) return;
|
|
3155
|
+
var st = _addSourceState;
|
|
3156
|
+
var isLocal = st.type === 'local';
|
|
3157
|
+
|
|
3158
|
+
var html = '<div class="addsource-header"><h2>Add Source</h2>'
|
|
3159
|
+
+ '<button class="panel-close" id="addsource-close">×</button></div>'
|
|
3160
|
+
+ '<div class="addsource-body">'
|
|
3161
|
+
+ '<div class="addsource-type-toggle">'
|
|
3162
|
+
+ '<button class="addsource-type-btn' + (isLocal ? ' active' : '') + '" data-type="local">Local Folder</button>'
|
|
3163
|
+
+ '<button class="addsource-type-btn' + (!isLocal ? ' active' : '') + '" data-type="remote">Remote Repo</button>'
|
|
3164
|
+
+ '</div>'
|
|
3165
|
+
+ '<div class="addsource-error" id="addsource-error"></div>';
|
|
3166
|
+
|
|
3167
|
+
// Name field
|
|
3168
|
+
html += '<div class="addsource-field"><label>Name</label>'
|
|
3169
|
+
+ '<input type="text" id="addsource-name" placeholder="e.g. my-project" value=""></div>';
|
|
3170
|
+
|
|
3171
|
+
if (isLocal) {
|
|
3172
|
+
// Path input
|
|
3173
|
+
html += '<div class="addsource-field"><label>Path</label>'
|
|
3174
|
+
+ '<input type="text" id="addsource-path" placeholder="/absolute/path/to/folder" value=""></div>';
|
|
3175
|
+
} else {
|
|
3176
|
+
// URL
|
|
3177
|
+
html += '<div class="addsource-field"><label>URL</label>'
|
|
3178
|
+
+ '<input type="text" id="addsource-url" placeholder="https://github.com/user/repo.git" value=""></div>';
|
|
3179
|
+
// Branch
|
|
3180
|
+
html += '<div class="addsource-field"><label>Branch</label>'
|
|
3181
|
+
+ '<input type="text" id="addsource-branch" placeholder="main" value="main"></div>';
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
// Root
|
|
3185
|
+
html += '<div class="addsource-field"><label>Root directory</label>'
|
|
3186
|
+
+ '<input type="text" id="addsource-root" placeholder="project" value="project"></div>';
|
|
3187
|
+
|
|
3188
|
+
// Color picker
|
|
3189
|
+
html += '<div class="addsource-field"><label>Color</label><div class="addsource-colors">';
|
|
3190
|
+
for (var i = 0; i < _addSourceColors.length; i++) {
|
|
3191
|
+
var c = _addSourceColors[i];
|
|
3192
|
+
html += '<div class="addsource-color' + (c === st.color ? ' active' : '') + '" data-color="' + c + '" style="background:' + c + '"></div>';
|
|
3193
|
+
}
|
|
3194
|
+
html += '</div></div>';
|
|
3195
|
+
|
|
3196
|
+
html += '</div>'; // close body
|
|
3197
|
+
html += '<div class="addsource-footer">'
|
|
3198
|
+
+ '<button class="btn" id="addsource-cancel">Cancel</button>'
|
|
3199
|
+
+ '<button class="btn btn-primary" id="addsource-submit">Add Source</button>'
|
|
3200
|
+
+ '</div>';
|
|
3201
|
+
|
|
3202
|
+
modal.innerHTML = html;
|
|
3203
|
+
|
|
3204
|
+
// Bind events
|
|
3205
|
+
document.getElementById('addsource-close').onclick = closeAddSourceModal;
|
|
3206
|
+
document.getElementById('addsource-cancel').onclick = closeAddSourceModal;
|
|
3207
|
+
document.getElementById('addsource-submit').onclick = submitAddSource;
|
|
3208
|
+
|
|
3209
|
+
// Type toggle
|
|
3210
|
+
modal.querySelectorAll('.addsource-type-btn').forEach(function(btn) {
|
|
3211
|
+
btn.onclick = function() {
|
|
3212
|
+
_addSourceState.type = btn.dataset.type;
|
|
3213
|
+
renderAddSourceModal();
|
|
3214
|
+
};
|
|
3215
|
+
});
|
|
2936
3216
|
|
|
2937
|
-
|
|
2938
|
-
|
|
3217
|
+
// Color picker
|
|
3218
|
+
modal.querySelectorAll('.addsource-color').forEach(function(swatch) {
|
|
3219
|
+
swatch.onclick = function() {
|
|
3220
|
+
_addSourceState.color = swatch.dataset.color;
|
|
3221
|
+
modal.querySelectorAll('.addsource-color').forEach(function(s) { s.classList.remove('active'); });
|
|
3222
|
+
swatch.classList.add('active');
|
|
3223
|
+
};
|
|
3224
|
+
});
|
|
2939
3225
|
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
3226
|
+
// Auto-name from path/url
|
|
3227
|
+
var nameInput = document.getElementById('addsource-name');
|
|
3228
|
+
if (isLocal) {
|
|
3229
|
+
var pathInput = document.getElementById('addsource-path');
|
|
3230
|
+
if (pathInput) pathInput.addEventListener('input', function() {
|
|
3231
|
+
if (!nameInput.dataset.manual) {
|
|
3232
|
+
var parts = pathInput.value.replace(/\/+$/, '').split('/');
|
|
3233
|
+
nameInput.value = parts[parts.length - 1] || '';
|
|
3234
|
+
}
|
|
3235
|
+
});
|
|
3236
|
+
} else {
|
|
3237
|
+
var urlInput = document.getElementById('addsource-url');
|
|
3238
|
+
if (urlInput) urlInput.addEventListener('input', function() {
|
|
3239
|
+
if (!nameInput.dataset.manual) {
|
|
3240
|
+
var val = urlInput.value.replace(/\.git$/, '').replace(/\/+$/, '');
|
|
3241
|
+
var parts = val.split('/');
|
|
3242
|
+
nameInput.value = parts[parts.length - 1] || '';
|
|
3243
|
+
}
|
|
3244
|
+
});
|
|
2943
3245
|
}
|
|
3246
|
+
if (nameInput) nameInput.addEventListener('input', function() {
|
|
3247
|
+
nameInput.dataset.manual = nameInput.value ? '1' : '';
|
|
3248
|
+
});
|
|
3249
|
+
}
|
|
2944
3250
|
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
var assignedEl = document.getElementById('p-assigned');
|
|
2950
|
-
if (assignedEl && assignedEl.value.trim()) data.assigned = assignedEl.value.trim();
|
|
3251
|
+
function submitAddSource() {
|
|
3252
|
+
var errEl = document.getElementById('addsource-error');
|
|
3253
|
+
if (errEl) { errEl.textContent = ''; errEl.classList.remove('visible'); }
|
|
2951
3254
|
|
|
2952
|
-
|
|
2953
|
-
|
|
3255
|
+
var name = (document.getElementById('addsource-name') || {}).value || '';
|
|
3256
|
+
var root = (document.getElementById('addsource-root') || {}).value || '';
|
|
3257
|
+
var isLocal = _addSourceState.type === 'local';
|
|
2954
3258
|
|
|
2955
|
-
|
|
2956
|
-
|
|
3259
|
+
var body = { name: name.trim(), type: _addSourceState.type, color: _addSourceState.color };
|
|
3260
|
+
if (root.trim()) body.root = root.trim();
|
|
2957
3261
|
|
|
2958
|
-
|
|
2959
|
-
|
|
3262
|
+
if (isLocal) {
|
|
3263
|
+
body.path = ((document.getElementById('addsource-path') || {}).value || '').trim();
|
|
3264
|
+
} else {
|
|
3265
|
+
body.url = ((document.getElementById('addsource-url') || {}).value || '').trim();
|
|
3266
|
+
body.branch = ((document.getElementById('addsource-branch') || {}).value || 'main').trim();
|
|
2960
3267
|
}
|
|
2961
3268
|
|
|
2962
|
-
if (
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
}
|
|
3269
|
+
if (!body.name) return showAddSourceError('Name is required');
|
|
3270
|
+
if (isLocal && !body.path) return showAddSourceError('Path is required');
|
|
3271
|
+
if (!isLocal && !body.url) return showAddSourceError('URL is required');
|
|
2966
3272
|
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
if (deadlineEl && deadlineEl.value) data.deadline = deadlineEl.value;
|
|
2970
|
-
}
|
|
3273
|
+
var submitBtn = document.getElementById('addsource-submit');
|
|
3274
|
+
if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Adding...'; }
|
|
2971
3275
|
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
3276
|
+
fetch('/api/sources', {
|
|
3277
|
+
method: 'POST',
|
|
3278
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3279
|
+
body: JSON.stringify(body),
|
|
3280
|
+
}).then(function(r) { return r.json().then(function(d) { return { ok: r.ok, data: d }; }); })
|
|
3281
|
+
.then(function(res) {
|
|
3282
|
+
if (!res.ok) {
|
|
3283
|
+
showAddSourceError(res.data.error || 'Failed to add source');
|
|
3284
|
+
if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Add Source'; }
|
|
3285
|
+
return;
|
|
3286
|
+
}
|
|
3287
|
+
closeAddSourceModal();
|
|
3288
|
+
// Reload everything
|
|
3289
|
+
D.loaded = false;
|
|
3290
|
+
loadAll().then(function() {
|
|
3291
|
+
generateDynamicStyles();
|
|
3292
|
+
buildSidebar();
|
|
3293
|
+
buildViews();
|
|
3294
|
+
buildSourceRail();
|
|
3295
|
+
if (body.name) switchSource(body.name);
|
|
3296
|
+
renderSidebarLogo();
|
|
2987
3297
|
});
|
|
2988
|
-
|
|
3298
|
+
}).catch(function(err) {
|
|
3299
|
+
showAddSourceError(err.message || 'Network error');
|
|
3300
|
+
if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Add Source'; }
|
|
2989
3301
|
});
|
|
2990
|
-
|
|
2991
|
-
}
|
|
3302
|
+
}
|
|
2992
3303
|
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
refreshData();
|
|
2997
|
-
}
|
|
3304
|
+
function showAddSourceError(msg) {
|
|
3305
|
+
var el = document.getElementById('addsource-error');
|
|
3306
|
+
if (el) { el.textContent = msg; el.classList.add('visible'); }
|
|
2998
3307
|
}
|
|
2999
3308
|
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3309
|
+
/* ── SSE — Hot Reload ────────────────────────────────────── */
|
|
3310
|
+
function connectSSE() {
|
|
3311
|
+
try {
|
|
3312
|
+
var src = new EventSource('/api/events');
|
|
3313
|
+
src.onmessage = function(e) {
|
|
3314
|
+
var data;
|
|
3315
|
+
try { data = JSON.parse(e.data); } catch(ex) { data = {}; }
|
|
3316
|
+
|
|
3317
|
+
if (data.type === 'project-changed') {
|
|
3318
|
+
D.activeSource = null;
|
|
3319
|
+
D.loaded = false;
|
|
3320
|
+
loadAll().then(function() {
|
|
3321
|
+
generateDynamicStyles();
|
|
3322
|
+
buildSidebar();
|
|
3323
|
+
buildViews();
|
|
3324
|
+
buildSourceRail();
|
|
3325
|
+
if (hasWorkspace()) {
|
|
3326
|
+
var firstSrc = D.config.workspace.sources[0];
|
|
3327
|
+
D.activeSource = null;
|
|
3328
|
+
switchSource(firstSrc ? firstSrc.name : null);
|
|
3329
|
+
} else {
|
|
3330
|
+
renderCurrentView();
|
|
3331
|
+
}
|
|
3332
|
+
renderSidebarLogo();
|
|
3333
|
+
});
|
|
3334
|
+
return;
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
if (data.cssReload) {
|
|
3338
|
+
var link = document.getElementById('custom-theme');
|
|
3339
|
+
if (link) link.href = '/mdboard.css?' + Date.now();
|
|
3340
|
+
}
|
|
3341
|
+
refreshData();
|
|
3342
|
+
};
|
|
3343
|
+
src.onerror = function() { setTimeout(connectSSE, 5000); src.close(); };
|
|
3344
|
+
} catch(ex) { /* no SSE support */ }
|
|
3345
|
+
}
|
|
3005
3346
|
|
|
3006
3347
|
|
|
3007
3348
|
/* ══════════════════════════════════════════════════════════════
|
|
@@ -3040,7 +3381,23 @@ function renderHistoryList(entries) {
|
|
|
3040
3381
|
if (!body) return;
|
|
3041
3382
|
|
|
3042
3383
|
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>'
|
|
3384
|
+
body.innerHTML = '<div class="history-empty">No projects in history yet.<br>Open mdboard from a project directory to add it.</div>'
|
|
3385
|
+
+ '<div class="history-open-row">'
|
|
3386
|
+
+ '<input type="text" id="history-path-input" class="history-path-input" placeholder="/absolute/path/to/folder or https://github.com/user/repo">'
|
|
3387
|
+
+ '<button class="btn btn-primary btn-sm" id="history-open-btn">Open</button>'
|
|
3388
|
+
+ '</div>';
|
|
3389
|
+
var openBtn = document.getElementById('history-open-btn');
|
|
3390
|
+
var pathInput = document.getElementById('history-path-input');
|
|
3391
|
+
if (openBtn && pathInput) {
|
|
3392
|
+
openBtn.onclick = function() {
|
|
3393
|
+
var val = pathInput.value.trim();
|
|
3394
|
+
if (!val) return;
|
|
3395
|
+
switchProject(val);
|
|
3396
|
+
};
|
|
3397
|
+
pathInput.addEventListener('keydown', function(e) {
|
|
3398
|
+
if (e.key === 'Enter') { openBtn.click(); }
|
|
3399
|
+
});
|
|
3400
|
+
}
|
|
3044
3401
|
return;
|
|
3045
3402
|
}
|
|
3046
3403
|
|
|
@@ -3069,6 +3426,12 @@ function renderHistoryList(entries) {
|
|
|
3069
3426
|
'</div>';
|
|
3070
3427
|
}
|
|
3071
3428
|
|
|
3429
|
+
// Path input for opening a new project
|
|
3430
|
+
html += '<div class="history-open-row">'
|
|
3431
|
+
+ '<input type="text" id="history-path-input" class="history-path-input" placeholder="/absolute/path/to/folder or https://github.com/user/repo">'
|
|
3432
|
+
+ '<button class="btn btn-primary btn-sm" id="history-open-btn">Open</button>'
|
|
3433
|
+
+ '</div>';
|
|
3434
|
+
|
|
3072
3435
|
body.innerHTML = html;
|
|
3073
3436
|
|
|
3074
3437
|
// Attach click handlers
|
|
@@ -3080,6 +3443,20 @@ function renderHistoryList(entries) {
|
|
|
3080
3443
|
switchProject(itemPath);
|
|
3081
3444
|
});
|
|
3082
3445
|
}
|
|
3446
|
+
|
|
3447
|
+
// Open button handler
|
|
3448
|
+
var openBtn = document.getElementById('history-open-btn');
|
|
3449
|
+
var pathInput = document.getElementById('history-path-input');
|
|
3450
|
+
if (openBtn && pathInput) {
|
|
3451
|
+
openBtn.onclick = function() {
|
|
3452
|
+
var val = pathInput.value.trim();
|
|
3453
|
+
if (!val) return;
|
|
3454
|
+
switchProject(val);
|
|
3455
|
+
};
|
|
3456
|
+
pathInput.addEventListener('keydown', function(e) {
|
|
3457
|
+
if (e.key === 'Enter') { openBtn.click(); }
|
|
3458
|
+
});
|
|
3459
|
+
}
|
|
3083
3460
|
}
|
|
3084
3461
|
|
|
3085
3462
|
function switchProject(projectPath) {
|
|
@@ -3094,24 +3471,8 @@ function switchProject(projectPath) {
|
|
|
3094
3471
|
.then(function(r) { return r.json(); })
|
|
3095
3472
|
.then(function(data) {
|
|
3096
3473
|
if (data && data.ok) {
|
|
3097
|
-
|
|
3098
|
-
|
|
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
|
-
});
|
|
3474
|
+
// Full reload to pick up new project's config, theme, and CSS
|
|
3475
|
+
window.location.reload();
|
|
3115
3476
|
showToast('Switched to ' + (data.name || 'project'), 'success');
|
|
3116
3477
|
} else {
|
|
3117
3478
|
showToast('Error: ' + (data.error || 'Switch failed'), 'error');
|
|
@@ -3144,95 +3505,330 @@ function timeAgo(dateStr) {
|
|
|
3144
3505
|
|
|
3145
3506
|
|
|
3146
3507
|
/* ══════════════════════════════════════════════════════════════
|
|
3147
|
-
|
|
3508
|
+
APP — Dynamic sidebar, view routing, settings, init
|
|
3148
3509
|
══════════════════════════════════════════════════════════════ */
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
var
|
|
3153
|
-
if (
|
|
3154
|
-
|
|
3510
|
+
|
|
3511
|
+
/* ── Sidebar ─────────────────────────────────────────────── */
|
|
3512
|
+
function buildSidebar() {
|
|
3513
|
+
var nav = document.getElementById('sidebar-nav');
|
|
3514
|
+
if (!nav || !D.config || !D.config.ui || !D.config.ui.tabs) return;
|
|
3515
|
+
|
|
3516
|
+
var tabs = D.config.ui.tabs;
|
|
3517
|
+
var html = '';
|
|
3518
|
+
for (var i = 0; i < tabs.length; i++) {
|
|
3519
|
+
var tab = tabs[i];
|
|
3520
|
+
var icon = TAB_ICONS[tab.icon] || TAB_ICONS['board'];
|
|
3521
|
+
var cls = (D.activeTab === tab.id) ? ' class="active"' : '';
|
|
3522
|
+
var tooltip = tab.description ? ' title="' + escHtml(tab.description) + '"' : '';
|
|
3523
|
+
html += '<a href="#' + tab.id + '" data-view="' + tab.id + '"' + cls + tooltip + '>' + icon + '<span>' + escHtml(tab.label) + '</span></a>';
|
|
3155
3524
|
}
|
|
3525
|
+
nav.innerHTML = html;
|
|
3156
3526
|
}
|
|
3157
3527
|
|
|
3158
|
-
function
|
|
3159
|
-
|
|
3528
|
+
function buildViews() {
|
|
3529
|
+
var content = document.getElementById('app-content');
|
|
3530
|
+
if (!content || !D.config || !D.config.ui || !D.config.ui.tabs) return;
|
|
3160
3531
|
|
|
3161
|
-
var
|
|
3162
|
-
var
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
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>';
|
|
3532
|
+
var tabs = D.config.ui.tabs;
|
|
3533
|
+
var html = '';
|
|
3534
|
+
for (var i = 0; i < tabs.length; i++) {
|
|
3535
|
+
var tab = tabs[i];
|
|
3536
|
+
var cls = (D.activeTab === tab.id) ? ' active' : '';
|
|
3537
|
+
html += '<div id="view-' + tab.id + '" class="view' + cls + '">';
|
|
3538
|
+
html += '<div class="view-toolbar" id="toolbar-' + tab.id + '"></div>';
|
|
3539
|
+
html += '<div class="view-body" id="body-' + tab.id + '"></div>';
|
|
3540
|
+
html += '</div>';
|
|
3541
|
+
}
|
|
3542
|
+
content.innerHTML = html;
|
|
3190
3543
|
}
|
|
3191
3544
|
|
|
3192
|
-
/*
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
function switchView(name) {
|
|
3545
|
+
/* ── View Switching ──────────────────────────────────────── */
|
|
3546
|
+
function switchView(tabId) {
|
|
3547
|
+
D.activeTab = tabId;
|
|
3196
3548
|
document.querySelectorAll('.view').forEach(function(v) { v.classList.remove('active'); });
|
|
3197
3549
|
document.querySelectorAll('#sidebar-nav a[data-view]').forEach(function(a) { a.classList.remove('active'); });
|
|
3198
|
-
var t = document.getElementById('view-' +
|
|
3550
|
+
var t = document.getElementById('view-' + tabId);
|
|
3199
3551
|
if (t) t.classList.add('active');
|
|
3200
|
-
var l = document.querySelector('#sidebar-nav a[data-view="' +
|
|
3552
|
+
var l = document.querySelector('#sidebar-nav a[data-view="' + tabId + '"]');
|
|
3201
3553
|
if (l) l.classList.add('active');
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3554
|
+
try { localStorage.setItem('mdboard-view', tabId); } catch(e) {}
|
|
3555
|
+
if (D.loaded) renderCurrentView();
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
function cleanupNotesView() {
|
|
3559
|
+
// Destroy editor if active
|
|
3560
|
+
if (notesState.editor) {
|
|
3561
|
+
destroyEditor(notesState.editor);
|
|
3562
|
+
notesState.editor = null;
|
|
3207
3563
|
}
|
|
3208
|
-
//
|
|
3209
|
-
|
|
3210
|
-
|
|
3564
|
+
// Reset inline styles that renderNotes applies to .view elements
|
|
3565
|
+
document.querySelectorAll('.view').forEach(function(v) {
|
|
3566
|
+
v.style.display = '';
|
|
3567
|
+
v.style.flexDirection = '';
|
|
3568
|
+
v.style.margin = '';
|
|
3569
|
+
v.style.height = '';
|
|
3570
|
+
});
|
|
3571
|
+
document.querySelectorAll('.view-body').forEach(function(b) {
|
|
3572
|
+
b.style.flex = '';
|
|
3573
|
+
b.style.overflow = '';
|
|
3574
|
+
});
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3577
|
+
function renderCurrentView() {
|
|
3578
|
+
if (!D.config || !D.config.ui || !D.activeTab) return;
|
|
3579
|
+
var tabConfig = getTabConfig(D.activeTab);
|
|
3580
|
+
if (!tabConfig) return;
|
|
3581
|
+
|
|
3582
|
+
var toolbarEl = document.getElementById('toolbar-' + D.activeTab);
|
|
3583
|
+
var bodyEl = document.getElementById('body-' + D.activeTab);
|
|
3584
|
+
if (!toolbarEl || !bodyEl) return;
|
|
3585
|
+
|
|
3586
|
+
var layout = getActiveLayout(D.activeTab);
|
|
3587
|
+
|
|
3588
|
+
// Clean up notes view styles before rendering any view
|
|
3589
|
+
cleanupNotesView();
|
|
3590
|
+
|
|
3591
|
+
// Render toolbar: layout switcher + filters
|
|
3592
|
+
renderToolbar(toolbarEl, tabConfig, layout);
|
|
3593
|
+
|
|
3594
|
+
// Render header
|
|
3595
|
+
renderAppHeader();
|
|
3596
|
+
|
|
3597
|
+
// Dispatch to layout renderer
|
|
3598
|
+
switch (layout) {
|
|
3599
|
+
case 'board': renderBoard(bodyEl, tabConfig); break;
|
|
3600
|
+
case 'table': renderTable(bodyEl, tabConfig); break;
|
|
3601
|
+
case 'cards': renderCards(bodyEl, tabConfig); break;
|
|
3602
|
+
case 'list': renderList(bodyEl, tabConfig); break;
|
|
3603
|
+
case 'editor': renderNotes(bodyEl, tabConfig); break;
|
|
3604
|
+
case 'metrics': renderMetrics(bodyEl, tabConfig); break;
|
|
3605
|
+
default: bodyEl.innerHTML = '<div class="empty"><p>Unknown layout: ' + escHtml(layout) + '</p></div>';
|
|
3211
3606
|
}
|
|
3212
3607
|
}
|
|
3213
3608
|
|
|
3214
|
-
|
|
3215
|
-
var
|
|
3216
|
-
if (!a) return;
|
|
3217
|
-
e.preventDefault();
|
|
3218
|
-
switchView(a.dataset.view);
|
|
3219
|
-
window.location.hash = a.dataset.view;
|
|
3220
|
-
});
|
|
3609
|
+
function renderToolbar(container, tabConfig, activeLayout) {
|
|
3610
|
+
var html = '';
|
|
3221
3611
|
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3612
|
+
// Create button
|
|
3613
|
+
if (tabConfig.entity && isSourceWritable() && activeLayout !== 'editor' && activeLayout !== 'metrics') {
|
|
3614
|
+
html += '<button class="btn btn-sm btn-create" id="create-btn-' + tabConfig.id + '">+ New ' + escHtml(getEntitySingular(tabConfig.entity)) + '</button>';
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
// Layout switcher
|
|
3618
|
+
if (tabConfig.layouts && tabConfig.layouts.length > 1) {
|
|
3619
|
+
html += '<div class="layout-switcher">';
|
|
3620
|
+
for (var i = 0; i < tabConfig.layouts.length; i++) {
|
|
3621
|
+
var l = tabConfig.layouts[i];
|
|
3622
|
+
var active = l === activeLayout ? ' active' : '';
|
|
3623
|
+
html += '<button class="layout-btn' + active + '" data-layout="' + l + '">' + escHtml(l.charAt(0).toUpperCase() + l.slice(1)) + '</button>';
|
|
3624
|
+
}
|
|
3625
|
+
html += '</div>';
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3628
|
+
// Filters
|
|
3629
|
+
if (tabConfig.entity && activeLayout !== 'editor' && activeLayout !== 'metrics') {
|
|
3630
|
+
html += '<input type="text" class="filter-search" data-tab="' + tabConfig.id + '" placeholder="Search..." value="' + escHtml((D.tabFilters[tabConfig.id] || {})._search || '') + '">';
|
|
3631
|
+
|
|
3632
|
+
var filterFields = [];
|
|
3633
|
+
if (tabConfig.filters === 'auto') {
|
|
3634
|
+
filterFields = getAutoFilters(tabConfig.entity);
|
|
3635
|
+
} else if (Array.isArray(tabConfig.filters)) {
|
|
3636
|
+
filterFields = tabConfig.filters;
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
var currentFilters = D.tabFilters[tabConfig.id] || {};
|
|
3640
|
+
for (var fi = 0; fi < filterFields.length; fi++) {
|
|
3641
|
+
var field = filterFields[fi];
|
|
3642
|
+
var values = getFilterValues(tabConfig.entity, field);
|
|
3643
|
+
var label = field.charAt(0).toUpperCase() + field.slice(1);
|
|
3644
|
+
var ancDef = getEntityDef(field);
|
|
3645
|
+
if (ancDef) label = ancDef.plural || label;
|
|
3646
|
+
var fieldDef = getEntityFields(tabConfig.entity)[field];
|
|
3647
|
+
if (fieldDef && fieldDef.label) label = fieldDef.label;
|
|
3648
|
+
|
|
3649
|
+
html += '<select class="filter-select" data-tab="' + tabConfig.id + '" data-field="' + field + '">';
|
|
3650
|
+
html += '<option value="">All ' + escHtml(label) + '</option>';
|
|
3651
|
+
for (var vi = 0; vi < values.length; vi++) {
|
|
3652
|
+
var v = values[vi];
|
|
3653
|
+
var sel = currentFilters[field] === v ? ' selected' : '';
|
|
3654
|
+
html += '<option value="' + escHtml(v) + '"' + sel + '>' + escHtml(getFieldLabel(tabConfig.entity, field, v)) + '</option>';
|
|
3655
|
+
}
|
|
3656
|
+
html += '</select>';
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
|
|
3660
|
+
container.innerHTML = html;
|
|
3661
|
+
|
|
3662
|
+
// Event listeners
|
|
3663
|
+
var createBtn = document.getElementById('create-btn-' + tabConfig.id);
|
|
3664
|
+
if (createBtn) {
|
|
3665
|
+
createBtn.addEventListener('click', function() { openCreateDialog(tabConfig.entity); });
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3668
|
+
container.querySelectorAll('.layout-btn').forEach(function(btn) {
|
|
3669
|
+
btn.addEventListener('click', function() {
|
|
3670
|
+
D.activeLayouts[tabConfig.id] = btn.dataset.layout;
|
|
3671
|
+
renderCurrentView();
|
|
3672
|
+
});
|
|
3673
|
+
});
|
|
3674
|
+
|
|
3675
|
+
var searchInput = container.querySelector('.filter-search[data-tab="' + tabConfig.id + '"]');
|
|
3676
|
+
if (searchInput) {
|
|
3677
|
+
searchInput.addEventListener('input', function() {
|
|
3678
|
+
if (!D.tabFilters[tabConfig.id]) D.tabFilters[tabConfig.id] = {};
|
|
3679
|
+
D.tabFilters[tabConfig.id]._search = searchInput.value;
|
|
3680
|
+
renderCurrentView();
|
|
3681
|
+
});
|
|
3226
3682
|
}
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3683
|
+
|
|
3684
|
+
container.querySelectorAll('.filter-select').forEach(function(sel) {
|
|
3685
|
+
sel.addEventListener('change', function() {
|
|
3686
|
+
var tabId = sel.dataset.tab;
|
|
3687
|
+
var field = sel.dataset.field;
|
|
3688
|
+
if (!D.tabFilters[tabId]) D.tabFilters[tabId] = {};
|
|
3689
|
+
D.tabFilters[tabId][field] = sel.value;
|
|
3690
|
+
renderCurrentView();
|
|
3691
|
+
});
|
|
3692
|
+
});
|
|
3230
3693
|
}
|
|
3231
|
-
window.addEventListener('hashchange', handleHash);
|
|
3232
3694
|
|
|
3233
|
-
/*
|
|
3234
|
-
|
|
3235
|
-
|
|
3695
|
+
/* ── Header ──────────────────────────────────────────────── */
|
|
3696
|
+
function renderAppHeader() {
|
|
3697
|
+
var header = document.getElementById('app-header');
|
|
3698
|
+
if (!header || !D.loaded) return;
|
|
3699
|
+
|
|
3700
|
+
renderSidebarLogo();
|
|
3701
|
+
|
|
3702
|
+
if (!D.config || !D.config.entities) { header.innerHTML = ''; return; }
|
|
3703
|
+
|
|
3704
|
+
// Find the leaf entity type for stats
|
|
3705
|
+
var leafType = null;
|
|
3706
|
+
var hierarchy = D.config.hierarchy || {};
|
|
3707
|
+
function findLeaf(node) {
|
|
3708
|
+
for (var t in node) {
|
|
3709
|
+
if (node[t].children) findLeaf(node[t].children);
|
|
3710
|
+
else leafType = t;
|
|
3711
|
+
}
|
|
3712
|
+
}
|
|
3713
|
+
findLeaf(hierarchy);
|
|
3714
|
+
|
|
3715
|
+
if (!leafType) { header.innerHTML = ''; return; }
|
|
3716
|
+
|
|
3717
|
+
var items = getEntities(leafType);
|
|
3718
|
+
var total = items.length;
|
|
3719
|
+
var done = items.filter(function(i) { return isCompletedStatus(i.status); }).length;
|
|
3720
|
+
var inProg = items.filter(function(i) {
|
|
3721
|
+
return i.status && !isCompletedStatus(i.status) && i.status !== 'todo' && i.status !== 'planned';
|
|
3722
|
+
}).length;
|
|
3723
|
+
var vel = total > 0 ? Math.round(done / total * 100) : 0;
|
|
3724
|
+
|
|
3725
|
+
var leafPlural = getEntityDef(leafType) ? getEntityDef(leafType).plural : 'Items';
|
|
3726
|
+
|
|
3727
|
+
header.innerHTML =
|
|
3728
|
+
'<div class="global-search-wrap" id="global-search-wrap">' +
|
|
3729
|
+
'<svg class="global-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>' +
|
|
3730
|
+
'<input type="text" class="global-search-input" id="global-search" placeholder="Search everything... (Ctrl+K)" autocomplete="off">' +
|
|
3731
|
+
'<div class="global-search-results" id="global-search-results"></div>' +
|
|
3732
|
+
'</div>' +
|
|
3733
|
+
'<div id="h-stats" style="display:flex;gap:20px;margin-left:auto">' +
|
|
3734
|
+
'<div class="stat"><span class="stat-val">' + total + '</span><span class="stat-label">' + escHtml(leafPlural) + '</span></div>' +
|
|
3735
|
+
'<div class="stat"><span class="stat-val" style="color:var(--success)">' + done + '</span><span class="stat-label">Done</span></div>' +
|
|
3736
|
+
'<div class="stat"><span class="stat-val" style="color:var(--warning)">' + inProg + '</span><span class="stat-label">Active</span></div>' +
|
|
3737
|
+
'<div class="stat"><span class="stat-val" style="color:var(--accent)">' + vel + '%</span><span class="stat-label">Progress</span></div>' +
|
|
3738
|
+
'</div>';
|
|
3739
|
+
|
|
3740
|
+
var searchInput = document.getElementById('global-search');
|
|
3741
|
+
if (searchInput && !searchInput._bound) {
|
|
3742
|
+
searchInput._bound = true;
|
|
3743
|
+
searchInput.addEventListener('input', function() { globalSearch(searchInput.value); });
|
|
3744
|
+
searchInput.addEventListener('focus', function() { if (searchInput.value) globalSearch(searchInput.value); });
|
|
3745
|
+
searchInput.addEventListener('keydown', function(e) {
|
|
3746
|
+
var results = document.getElementById('global-search-results');
|
|
3747
|
+
var items = results ? results.querySelectorAll('.gs-item') : [];
|
|
3748
|
+
var active = results ? results.querySelector('.gs-item.active') : null;
|
|
3749
|
+
var idx = active ? Array.prototype.indexOf.call(items, active) : -1;
|
|
3750
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); selectGsItem(items, idx + 1); }
|
|
3751
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); selectGsItem(items, idx - 1); }
|
|
3752
|
+
else if (e.key === 'Enter' && active) { e.preventDefault(); active.click(); }
|
|
3753
|
+
else if (e.key === 'Escape') { searchInput.blur(); closeGlobalSearch(); }
|
|
3754
|
+
});
|
|
3755
|
+
document.addEventListener('click', function(e) {
|
|
3756
|
+
if (!e.target.closest('#global-search-wrap')) closeGlobalSearch();
|
|
3757
|
+
});
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
function selectGsItem(items, idx) {
|
|
3762
|
+
if (!items.length) return;
|
|
3763
|
+
items.forEach(function(el) { el.classList.remove('active'); });
|
|
3764
|
+
if (idx < 0) idx = items.length - 1;
|
|
3765
|
+
if (idx >= items.length) idx = 0;
|
|
3766
|
+
items[idx].classList.add('active');
|
|
3767
|
+
items[idx].scrollIntoView({ block: 'nearest' });
|
|
3768
|
+
}
|
|
3769
|
+
|
|
3770
|
+
function globalSearch(query) {
|
|
3771
|
+
var results = document.getElementById('global-search-results');
|
|
3772
|
+
if (!results) return;
|
|
3773
|
+
if (!query || query.length < 2) { results.innerHTML = ''; results.style.display = 'none'; return; }
|
|
3774
|
+
|
|
3775
|
+
var q = query.toLowerCase();
|
|
3776
|
+
var matches = [];
|
|
3777
|
+
var types = D.config && D.config.entities ? Object.keys(D.config.entities) : [];
|
|
3778
|
+
|
|
3779
|
+
for (var ti = 0; ti < types.length; ti++) {
|
|
3780
|
+
var type = types[ti];
|
|
3781
|
+
var items = getEntities(type);
|
|
3782
|
+
for (var i = 0; i < items.length; i++) {
|
|
3783
|
+
var item = items[i];
|
|
3784
|
+
var idMatch = (item.id || '').toLowerCase().indexOf(q) !== -1;
|
|
3785
|
+
var titleMatch = (item.title || '').toLowerCase().indexOf(q) !== -1;
|
|
3786
|
+
if (idMatch || titleMatch) {
|
|
3787
|
+
matches.push({ type: type, item: item });
|
|
3788
|
+
if (matches.length >= 12) break;
|
|
3789
|
+
}
|
|
3790
|
+
}
|
|
3791
|
+
if (matches.length >= 12) break;
|
|
3792
|
+
}
|
|
3793
|
+
|
|
3794
|
+
if (matches.length === 0) {
|
|
3795
|
+
results.innerHTML = '<div class="gs-empty">No results</div>';
|
|
3796
|
+
results.style.display = 'block';
|
|
3797
|
+
return;
|
|
3798
|
+
}
|
|
3799
|
+
|
|
3800
|
+
var html = '';
|
|
3801
|
+
for (var mi = 0; mi < matches.length; mi++) {
|
|
3802
|
+
var m = matches[mi];
|
|
3803
|
+
var def = getEntityDef(m.type);
|
|
3804
|
+
var label = def ? def.singular : m.type;
|
|
3805
|
+
html += '<div class="gs-item" data-type="' + escHtml(m.type) + '" data-id="' + escHtml(m.item.id) + '">' +
|
|
3806
|
+
statusIcon(m.type, m.item.status) +
|
|
3807
|
+
'<span class="gs-title">' + escHtml(m.item.title || m.item.id) + '</span>' +
|
|
3808
|
+
'<span class="gs-meta">' + escHtml(m.item.id) + '</span>' +
|
|
3809
|
+
'<span class="gs-type">' + escHtml(label) + '</span>' +
|
|
3810
|
+
'</div>';
|
|
3811
|
+
}
|
|
3812
|
+
results.innerHTML = html;
|
|
3813
|
+
results.style.display = 'block';
|
|
3814
|
+
|
|
3815
|
+
results.querySelectorAll('.gs-item').forEach(function(el) {
|
|
3816
|
+
el.addEventListener('click', function() {
|
|
3817
|
+
var type = el.dataset.type;
|
|
3818
|
+
var item = getEntities(type).find(function(i) { return i.id === el.dataset.id; });
|
|
3819
|
+
if (item) openPanel(type, item);
|
|
3820
|
+
closeGlobalSearch();
|
|
3821
|
+
document.getElementById('global-search').value = '';
|
|
3822
|
+
});
|
|
3823
|
+
});
|
|
3824
|
+
}
|
|
3825
|
+
|
|
3826
|
+
function closeGlobalSearch() {
|
|
3827
|
+
var results = document.getElementById('global-search-results');
|
|
3828
|
+
if (results) { results.innerHTML = ''; results.style.display = 'none'; }
|
|
3829
|
+
}
|
|
3830
|
+
|
|
3831
|
+
/* ── Settings ────────────────────────────────────────────── */
|
|
3236
3832
|
var _settingsState = { selectedTheme: null, originalTheme: null };
|
|
3237
3833
|
|
|
3238
3834
|
function openSettings() {
|
|
@@ -3291,10 +3887,7 @@ function renderThemeGrid() {
|
|
|
3291
3887
|
|
|
3292
3888
|
function saveTheme() {
|
|
3293
3889
|
var themeId = _settingsState.selectedTheme;
|
|
3294
|
-
if (!themeId) {
|
|
3295
|
-
closeSettings();
|
|
3296
|
-
return;
|
|
3297
|
-
}
|
|
3890
|
+
if (!themeId) { closeSettings(); return; }
|
|
3298
3891
|
var toggle = document.getElementById('theme-default-toggle');
|
|
3299
3892
|
var setDefault = toggle ? toggle.checked : false;
|
|
3300
3893
|
var css = generateThemeCss(themeId);
|
|
@@ -3305,10 +3898,10 @@ function saveTheme() {
|
|
|
3305
3898
|
body: JSON.stringify({ themeId: themeId, css: css, setDefault: setDefault })
|
|
3306
3899
|
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
3307
3900
|
if (data && data.ok) {
|
|
3308
|
-
revertThemePreview();
|
|
3309
|
-
reloadCustomCss();
|
|
3310
3901
|
D.config.theme = themeId;
|
|
3902
|
+
_settingsState.originalTheme = themeId;
|
|
3311
3903
|
_settingsState.selectedTheme = null;
|
|
3904
|
+
applyTheme(themeId);
|
|
3312
3905
|
showToast('Theme saved: ' + (THEMES[themeId] ? THEMES[themeId].name : themeId), 'success');
|
|
3313
3906
|
closeSettings();
|
|
3314
3907
|
} else {
|
|
@@ -3319,96 +3912,85 @@ function saveTheme() {
|
|
|
3319
3912
|
});
|
|
3320
3913
|
}
|
|
3321
3914
|
|
|
3322
|
-
document.getElementById('settings-toggle').addEventListener('click', function(e) {
|
|
3323
|
-
e.preventDefault();
|
|
3324
|
-
openSettings();
|
|
3325
|
-
});
|
|
3326
|
-
|
|
3327
3915
|
document.getElementById('settings-close').addEventListener('click', closeSettings);
|
|
3328
3916
|
document.getElementById('settings-overlay').addEventListener('click', closeSettings);
|
|
3329
|
-
|
|
3330
3917
|
document.getElementById('theme-grid').addEventListener('click', function(e) {
|
|
3331
3918
|
var card = e.target.closest('.theme-card');
|
|
3332
3919
|
if (!card) return;
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
applyTheme(themeId);
|
|
3920
|
+
_settingsState.selectedTheme = card.dataset.theme;
|
|
3921
|
+
applyTheme(card.dataset.theme);
|
|
3336
3922
|
renderThemeGrid();
|
|
3337
3923
|
});
|
|
3338
|
-
|
|
3339
3924
|
document.getElementById('settings-save').addEventListener('click', saveTheme);
|
|
3340
3925
|
|
|
3926
|
+
/* ── Navigation ──────────────────────────────────────────── */
|
|
3927
|
+
document.getElementById('sidebar-nav').addEventListener('click', function(e) {
|
|
3928
|
+
var a = e.target.closest('a[data-view]');
|
|
3929
|
+
if (!a) return;
|
|
3930
|
+
e.preventDefault();
|
|
3931
|
+
switchView(a.dataset.view);
|
|
3932
|
+
window.location.hash = a.dataset.view;
|
|
3933
|
+
});
|
|
3934
|
+
|
|
3935
|
+
function handleHash() {
|
|
3936
|
+
var hash = window.location.hash.replace('#', '');
|
|
3937
|
+
if (!hash) {
|
|
3938
|
+
try { hash = localStorage.getItem('mdboard-view') || ''; } catch(e) {}
|
|
3939
|
+
}
|
|
3940
|
+
if (!hash && D.config && D.config.ui && D.config.ui.tabs && D.config.ui.tabs.length > 0) {
|
|
3941
|
+
hash = D.config.ui.tabs[0].id;
|
|
3942
|
+
}
|
|
3943
|
+
return hash || 'tasks';
|
|
3944
|
+
}
|
|
3945
|
+
window.addEventListener('hashchange', function() { switchView(handleHash()); });
|
|
3946
|
+
|
|
3341
3947
|
/* ══════════════════════════════════════════════════════════════
|
|
3342
3948
|
INIT
|
|
3343
3949
|
══════════════════════════════════════════════════════════════ */
|
|
3344
3950
|
(async function() {
|
|
3345
|
-
handleHash();
|
|
3346
3951
|
await loadAll();
|
|
3347
|
-
|
|
3952
|
+
generateDynamicStyles();
|
|
3953
|
+
|
|
3954
|
+
// Determine initial tab
|
|
3955
|
+
D.activeTab = handleHash();
|
|
3348
3956
|
|
|
3349
|
-
//
|
|
3957
|
+
// Build dynamic UI
|
|
3958
|
+
buildSidebar();
|
|
3959
|
+
buildViews();
|
|
3350
3960
|
renderSidebarLogo();
|
|
3351
3961
|
|
|
3352
|
-
//
|
|
3353
|
-
|
|
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
|
-
}
|
|
3962
|
+
// Always build the source rail (switch project button)
|
|
3963
|
+
buildSourceRail();
|
|
3370
3964
|
|
|
3371
|
-
|
|
3965
|
+
// Workspace source switching
|
|
3966
|
+
if (hasWorkspace()) {
|
|
3372
3967
|
var savedSource = null;
|
|
3373
3968
|
try { savedSource = localStorage.getItem('mdboard-source'); } catch(e) {}
|
|
3374
|
-
var validSource = savedSource &&
|
|
3375
|
-
|
|
3969
|
+
var validSource = savedSource &&
|
|
3970
|
+
D.config.workspace.sources.some(function(s) { return s.name === savedSource; });
|
|
3971
|
+
var initialSource = validSource ? savedSource : D.config.workspace.sources[0].name;
|
|
3376
3972
|
|
|
3377
|
-
buildSourceRail();
|
|
3378
|
-
// switchSource skips if same as current, so set activeSource first then trigger
|
|
3379
3973
|
D.activeSource = null;
|
|
3380
3974
|
switchSource(initialSource);
|
|
3381
|
-
// Wait for switchSource data reload before continuing
|
|
3382
3975
|
await loadAll();
|
|
3383
|
-
|
|
3976
|
+
generateDynamicStyles();
|
|
3384
3977
|
}
|
|
3385
3978
|
|
|
3386
|
-
|
|
3979
|
+
// Initial render
|
|
3980
|
+
switchView(D.activeTab);
|
|
3387
3981
|
connectSSE();
|
|
3388
|
-
|
|
3389
|
-
// Auto-open history modal if no project
|
|
3390
3982
|
checkAutoOpenHistory();
|
|
3391
3983
|
|
|
3392
|
-
// Keyboard
|
|
3984
|
+
// Keyboard shortcuts
|
|
3393
3985
|
document.addEventListener('keydown', function(e) {
|
|
3394
3986
|
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
3395
3987
|
e.preventDefault();
|
|
3396
|
-
|
|
3988
|
+
var search = document.getElementById('global-search');
|
|
3989
|
+
if (search) { search.focus(); search.select(); }
|
|
3397
3990
|
}
|
|
3398
3991
|
});
|
|
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
|
-
}
|
|
3411
3992
|
})();
|
|
3993
|
+
|
|
3412
3994
|
</script>
|
|
3413
3995
|
</body>
|
|
3414
3996
|
</html>
|