@hanzlaa/rcode 3.5.0 → 3.6.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/package.json +7 -1
- package/server/dashboard.js +105 -3
- package/server/lib/html/client/agents-data.js +27 -0
- package/server/lib/html/client/app.js +15 -0
- package/server/lib/html/client/components/App.js +211 -0
- package/server/lib/html/client/components/OrchPanel.js +293 -0
- package/server/lib/html/client/components/Sidebar.js +73 -0
- package/server/lib/html/client/components/Topbar.js +53 -0
- package/server/lib/html/client/components/XtermPanel.js +220 -0
- package/server/lib/html/client/components/shared.js +330 -0
- package/server/lib/html/client/icons-client.js +85 -0
- package/server/lib/html/client/orchestrator.js +279 -0
- package/server/lib/html/client/preact.js +34 -0
- package/server/lib/html/client/store.js +91 -0
- package/server/lib/html/client/util.js +186 -0
- package/server/lib/html/client/views/AgentsView.js +83 -0
- package/server/lib/html/client/views/DecisionsView.js +102 -0
- package/server/lib/html/client/views/FilesView.js +223 -0
- package/server/lib/html/client/views/KanbanView.js +236 -0
- package/server/lib/html/client/views/MemoryView.js +157 -0
- package/server/lib/html/client/views/MilestonesView.js +136 -0
- package/server/lib/html/client/views/OrchestrationView.js +167 -0
- package/server/lib/html/client/views/OverviewView.js +221 -0
- package/server/lib/html/client/views/PhasesView.js +184 -0
- package/server/lib/html/client/views/RoadmapView.js +238 -0
- package/server/lib/html/client/views/SprintsView.js +178 -0
- package/server/lib/html/client/views/TasksView.js +148 -0
- package/server/lib/html/client.js +41 -1775
- package/server/lib/html/css.js +264 -44
- package/server/lib/html/icons.js +68 -0
- package/server/lib/html/shell.js +9 -296
- package/server/lib/scanner.js +76 -0
- package/server/orchestrator.js +237 -313
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared visual-primitive Preact components.
|
|
3
|
+
*
|
|
4
|
+
* All components are stateless unless noted. Each ports its string-template
|
|
5
|
+
* counterpart from client-render.js exactly, preserving CSS class names.
|
|
6
|
+
*
|
|
7
|
+
* Import from here; do NOT inline these in view modules.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { html, useState } from '../preact.js';
|
|
11
|
+
import { pctNum, chip as chipDesc, humanDate, pct } from '../util.js';
|
|
12
|
+
import {
|
|
13
|
+
runAndOpenTerm, isSessionRunning, runningInSprint, runningInPhase,
|
|
14
|
+
} from '../orchestrator.js';
|
|
15
|
+
import { Icon } from '../icons-client.js';
|
|
16
|
+
|
|
17
|
+
// ---- Toast helper (shared by CmdHint copy action and any view) ----
|
|
18
|
+
export function showToast(msg) {
|
|
19
|
+
const el = document.getElementById('toast');
|
|
20
|
+
if (!el) return;
|
|
21
|
+
el.textContent = msg;
|
|
22
|
+
el.classList.add('show');
|
|
23
|
+
setTimeout(() => el.classList.remove('show'), 2000);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ---- Chip ----
|
|
27
|
+
/**
|
|
28
|
+
* Status chip.
|
|
29
|
+
* @param {{ status: string }} props
|
|
30
|
+
*/
|
|
31
|
+
export function Chip({ status }) {
|
|
32
|
+
const { cls, label } = chipDesc(status);
|
|
33
|
+
return html`<span class=${'status-chip ' + cls}>● ${label}</span>`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---- Tag ----
|
|
37
|
+
/**
|
|
38
|
+
* Inline label tag.
|
|
39
|
+
* @param {{ children: any }} props
|
|
40
|
+
*/
|
|
41
|
+
export function Tag({ children }) {
|
|
42
|
+
return html`<span class="tag">${children}</span>`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---- ProgressBar ----
|
|
46
|
+
/**
|
|
47
|
+
* Horizontal progress bar matching progressBar() in client-render.js.
|
|
48
|
+
* @param {{ done: number, total: number }} props
|
|
49
|
+
*/
|
|
50
|
+
export function ProgressBar({ done, total }) {
|
|
51
|
+
const p = pctNum(done, total);
|
|
52
|
+
const color =
|
|
53
|
+
p >= 100 ? 'var(--accent-green)' :
|
|
54
|
+
p > 50 ? 'var(--accent-blue)' :
|
|
55
|
+
'var(--accent-amber)';
|
|
56
|
+
return html`
|
|
57
|
+
<div class="progress-bar">
|
|
58
|
+
<div class="progress-bar-fill" style=${'width:' + p + '%;background:' + color}></div>
|
|
59
|
+
</div>
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---- CompletionRing ----
|
|
64
|
+
/**
|
|
65
|
+
* SVG completion ring matching completionRing() in client-render.js.
|
|
66
|
+
* @param {{ done: number, total: number }} props
|
|
67
|
+
*/
|
|
68
|
+
export function CompletionRing({ done, total }) {
|
|
69
|
+
const p = pctNum(done, total);
|
|
70
|
+
const r = 28;
|
|
71
|
+
const c = 2 * Math.PI * r;
|
|
72
|
+
const offset = c - (p / 100) * c;
|
|
73
|
+
return html`
|
|
74
|
+
<div class="completion-ring">
|
|
75
|
+
<svg width="64" height="64" viewBox="0 0 64 64">
|
|
76
|
+
<circle cx="32" cy="32" r=${r} fill="none" stroke="var(--border)" stroke-width="4"/>
|
|
77
|
+
<circle cx="32" cy="32" r=${r} fill="none" stroke="var(--accent-green)" stroke-width="4"
|
|
78
|
+
stroke-dasharray=${c} stroke-dashoffset=${offset} stroke-linecap="round"/>
|
|
79
|
+
</svg>
|
|
80
|
+
<span class="ring-text">${p}%</span>
|
|
81
|
+
</div>
|
|
82
|
+
`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---- Breadcrumb ----
|
|
86
|
+
/**
|
|
87
|
+
* Back-button breadcrumb row.
|
|
88
|
+
* @param {{ items: Array<{label: string, hash: string}> }} props
|
|
89
|
+
*/
|
|
90
|
+
export function Breadcrumb({ items }) {
|
|
91
|
+
return html`
|
|
92
|
+
<div class="breadcrumb">
|
|
93
|
+
${items.map(item => html`
|
|
94
|
+
<button class="back-btn" onClick=${() => { location.hash = item.hash; }}>
|
|
95
|
+
← ${item.label}
|
|
96
|
+
</button>
|
|
97
|
+
`)}
|
|
98
|
+
</div>
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---- CmdHint + CmdHints ----
|
|
103
|
+
/**
|
|
104
|
+
* Single command hint row — copy to clipboard on click.
|
|
105
|
+
* @param {{ cmd: string, desc: string }} props
|
|
106
|
+
*/
|
|
107
|
+
export function CmdHint({ cmd, desc }) {
|
|
108
|
+
function handleClick() {
|
|
109
|
+
navigator.clipboard.writeText(cmd)
|
|
110
|
+
.then(() => showToast('Copied: ' + cmd))
|
|
111
|
+
.catch(() => {
|
|
112
|
+
const ta = document.createElement('textarea');
|
|
113
|
+
ta.value = cmd;
|
|
114
|
+
document.body.appendChild(ta);
|
|
115
|
+
ta.select();
|
|
116
|
+
document.execCommand('copy');
|
|
117
|
+
document.body.removeChild(ta);
|
|
118
|
+
showToast('Copied: ' + cmd);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return html`
|
|
122
|
+
<div class="cmd-hint-item" onClick=${handleClick}>
|
|
123
|
+
<span class="cmd-text">${cmd}</span>
|
|
124
|
+
<span class="cmd-desc">${desc}</span>
|
|
125
|
+
<${Icon} name="copy" size=${14} cls="cmd-copy"/>
|
|
126
|
+
</div>
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Collapsible command-hints accordion.
|
|
132
|
+
* @param {{ hints: Array<[string, string]> }} props — each item is [cmd, desc]
|
|
133
|
+
*/
|
|
134
|
+
export function CmdHints({ hints }) {
|
|
135
|
+
if (!hints || !hints.length) return null;
|
|
136
|
+
return html`
|
|
137
|
+
<details class="cmd-hints">
|
|
138
|
+
<summary><${Icon} name="lightbulb" size=${14}/> Commands</summary>
|
|
139
|
+
<div class="cmd-hints-list">
|
|
140
|
+
${hints.map(([cmd, desc]) => html`<${CmdHint} key=${cmd} cmd=${cmd} desc=${desc}/>`)}
|
|
141
|
+
</div>
|
|
142
|
+
</details>
|
|
143
|
+
`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---- RunBtn ----
|
|
147
|
+
/**
|
|
148
|
+
* Compact run button. Calls runAndOpenTerm from orchestrator.js.
|
|
149
|
+
* @param {{ storyId: string, cmd: string, label: string }} props
|
|
150
|
+
*/
|
|
151
|
+
export function RunBtn({ storyId, cmd, label }) {
|
|
152
|
+
function handleClick(e) {
|
|
153
|
+
e.stopPropagation();
|
|
154
|
+
runAndOpenTerm(storyId, cmd, label);
|
|
155
|
+
}
|
|
156
|
+
return html`
|
|
157
|
+
<button class="card-run-btn" title=${'Run ' + label} onClick=${handleClick}>
|
|
158
|
+
▶ Run
|
|
159
|
+
</button>
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---- RunningBadge ----
|
|
164
|
+
/**
|
|
165
|
+
* "N running" badge. Returns null when count is 0.
|
|
166
|
+
* @param {{ count: number }} props
|
|
167
|
+
*/
|
|
168
|
+
export function RunningBadge({ count }) {
|
|
169
|
+
if (!count) return null;
|
|
170
|
+
return html`<span class="run-badge">● ${count} running</span>`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---- PhaseCard ----
|
|
174
|
+
/**
|
|
175
|
+
* Clickable phase card used in Milestones, Phases, and Roadmap views.
|
|
176
|
+
* Ports phaseCard() from client-render.js:146-165.
|
|
177
|
+
* @param {{ phase: object, S: object }} props
|
|
178
|
+
*/
|
|
179
|
+
export function PhaseCard({ phase: p, S }) {
|
|
180
|
+
const sps = p.sprints || [];
|
|
181
|
+
const stories = sps.flatMap(s => s.stories || []);
|
|
182
|
+
const done = stories.filter(t => t.status === 'done' || t.status === 'completed').length;
|
|
183
|
+
const isCur = String(p.id) === String(S && S.currentPhase);
|
|
184
|
+
const running = runningInPhase(p);
|
|
185
|
+
const borderStyle = isCur ? 'border-left-color:var(--accent-amber)' : '';
|
|
186
|
+
return html`
|
|
187
|
+
<div class=${'item item-clickable'} style=${borderStyle}
|
|
188
|
+
onClick=${() => { location.hash = 'phases/' + p.id; }}>
|
|
189
|
+
<div class="item-title">
|
|
190
|
+
${sps.length ? html`<${RunBtn} storyId=${'phase-' + p.id} cmd=${'/rihal-execute ' + p.id} label=${'Phase ' + p.id}/>` : null}
|
|
191
|
+
Phase ${p.id} — ${p.name}
|
|
192
|
+
${isCur ? html`<${Tag}>current</${Tag}>` : null}
|
|
193
|
+
<${Chip} status=${p.status}/>
|
|
194
|
+
</div>
|
|
195
|
+
<div class="item-meta">
|
|
196
|
+
<${Tag}>${sps.length} sprint${sps.length !== 1 ? 's' : ''}</${Tag}>
|
|
197
|
+
<${Tag}>${done}/${stories.length} tasks</${Tag}>
|
|
198
|
+
${stories.length > 0 ? html`<${Tag}>${pct(done, stories.length)} done</${Tag}>` : null}
|
|
199
|
+
${p.completed_at ? html`
|
|
200
|
+
<span style="color:var(--text-muted);font-size:var(--text-xs);">
|
|
201
|
+
Done ${humanDate(p.completed_at)}
|
|
202
|
+
</span>
|
|
203
|
+
` : null}
|
|
204
|
+
<${RunningBadge} count=${running}/>
|
|
205
|
+
</div>
|
|
206
|
+
${stories.length > 0 ? html`
|
|
207
|
+
<div style="margin-top:6px;"><${ProgressBar} done=${done} total=${stories.length}/></div>
|
|
208
|
+
` : null}
|
|
209
|
+
${sps[0]?.goal ? html`
|
|
210
|
+
<div style="color:var(--text-secondary);font-size:var(--text-sm);margin-top:4px;">
|
|
211
|
+
${sps[0].goal}
|
|
212
|
+
</div>
|
|
213
|
+
` : null}
|
|
214
|
+
</div>
|
|
215
|
+
`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---- SprintCard ----
|
|
219
|
+
/**
|
|
220
|
+
* Clickable sprint card used in Phases, Sprints, and Roadmap views.
|
|
221
|
+
* Ports sprintCard() from client-render.js:167-188.
|
|
222
|
+
* @param {{ sprint: object, S: object }} props
|
|
223
|
+
*/
|
|
224
|
+
export function SprintCard({ sprint: s, S }) {
|
|
225
|
+
const stories = s.stories || [];
|
|
226
|
+
const done = stories.filter(t => t.status === 'done' || t.status === 'completed').length;
|
|
227
|
+
const isCur = s.id === (S && S.currentSprint);
|
|
228
|
+
const phaseId = s.phaseId || s.id || '';
|
|
229
|
+
const running = runningInSprint(s);
|
|
230
|
+
const borderStyle = isCur
|
|
231
|
+
? 'border-left-color:var(--accent-amber);background:rgba(245,158,11,0.04)'
|
|
232
|
+
: '';
|
|
233
|
+
return html`
|
|
234
|
+
<div class=${'item item-clickable' + (isCur ? ' sprint-current' : '')} style=${borderStyle}
|
|
235
|
+
onClick=${() => { location.hash = 'sprints/' + s.id; }}>
|
|
236
|
+
<div class="item-title">
|
|
237
|
+
<${RunBtn} storyId=${'sprint-' + s.id} cmd=${'/rihal-execute-sprint ' + s.id} label=${'Sprint ' + s.id}/>
|
|
238
|
+
Sprint ${s.id} — ${s.goal || 'No goal'}
|
|
239
|
+
${isCur ? html`<${Tag}>current</${Tag}>` : null}
|
|
240
|
+
<${Chip} status=${s.status}/>
|
|
241
|
+
</div>
|
|
242
|
+
<div class="item-meta">
|
|
243
|
+
${s.phaseId ? html`<${Tag}>Phase ${s.phaseId}</${Tag}>` : null}
|
|
244
|
+
<${Tag}>${done}/${stories.length} tasks</${Tag}>
|
|
245
|
+
${s.velocity_target != null ? html`<${Tag}>Target: ${s.velocity_target}pts</${Tag}>` : null}
|
|
246
|
+
${s.velocity_actual != null ? html`<${Tag}>Actual: ${s.velocity_actual}pts</${Tag}>` : null}
|
|
247
|
+
<${RunningBadge} count=${running}/>
|
|
248
|
+
</div>
|
|
249
|
+
<div style="margin-top:6px;"><${ProgressBar} done=${done} total=${stories.length}/></div>
|
|
250
|
+
${stories.length === 0 ? html`
|
|
251
|
+
<div class="empty-action" style="margin-top:var(--space-2);font-size:var(--text-xs);">
|
|
252
|
+
No tasks — run <code>/rihal-plan ${phaseId}</code> to populate
|
|
253
|
+
</div>
|
|
254
|
+
` : null}
|
|
255
|
+
${s.started_at ? html`
|
|
256
|
+
<div style="color:var(--text-muted);font-size:var(--text-xs);margin-top:4px;">
|
|
257
|
+
${humanDate(s.started_at)}${s.completed_at ? ' → ' + humanDate(s.completed_at) : ' → ongoing'}
|
|
258
|
+
</div>
|
|
259
|
+
` : null}
|
|
260
|
+
</div>
|
|
261
|
+
`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ---- TaskCard ----
|
|
265
|
+
/**
|
|
266
|
+
* Expandable task card. Expansion is component useState (replaces toggleTaskDetail).
|
|
267
|
+
* Ports taskCard() from client-render.js:190-234.
|
|
268
|
+
* @param {{ task: object }} props
|
|
269
|
+
*/
|
|
270
|
+
export function TaskCard({ task: t }) {
|
|
271
|
+
const [expanded, setExpanded] = useState(false);
|
|
272
|
+
const done = t.status === 'done' || t.status === 'completed';
|
|
273
|
+
const running = isSessionRunning(t.id);
|
|
274
|
+
|
|
275
|
+
// Build cmd hints for this task
|
|
276
|
+
const taskCmds = [];
|
|
277
|
+
if (t.id) {
|
|
278
|
+
if (!done) {
|
|
279
|
+
taskCmds.push(['/rihal-dev-story ' + t.id, 'Implement this story']);
|
|
280
|
+
taskCmds.push(['/rihal-create-story ' + (t.sprintId || ''), 'Add related story']);
|
|
281
|
+
} else {
|
|
282
|
+
taskCmds.push(['/rihal-verify-work ' + t.id, 'Verify this story']);
|
|
283
|
+
taskCmds.push(['/rihal-code-review ' + t.id, 'Review code for this story']);
|
|
284
|
+
}
|
|
285
|
+
if (t.sprintId) {
|
|
286
|
+
taskCmds.push(['/rihal-sprint-status ' + t.sprintId, 'Sprint ' + t.sprintId + ' status']);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return html`
|
|
291
|
+
<div class="item item-clickable" data-status=${t.status || ''}
|
|
292
|
+
style=${done ? 'opacity:.65' : ''}
|
|
293
|
+
onClick=${() => setExpanded(e => !e)}>
|
|
294
|
+
<div class="item-title" style=${done ? 'text-decoration:line-through' : ''}>
|
|
295
|
+
${t.id && !done ? html`<${RunBtn} storyId=${t.id} cmd=${'/rihal-dev-story ' + t.id} label=${'Story ' + t.id}/>` : null}
|
|
296
|
+
${done ? '✓ ' : ''}${t.title}
|
|
297
|
+
<${Chip} status=${t.status}/>
|
|
298
|
+
<span class="task-expand-icon">${expanded ? '▼' : '▶'}</span>
|
|
299
|
+
</div>
|
|
300
|
+
<div class="item-meta">
|
|
301
|
+
${t.points ? html`<${Tag}>${t.points}pts</${Tag}>` : null}
|
|
302
|
+
${t.id ? html`<${Tag}>${t.id}</${Tag}>` : null}
|
|
303
|
+
${t.sprintId ? html`<${Tag}>Sprint ${t.sprintId}</${Tag}>` : null}
|
|
304
|
+
${t.phaseId ? html`<${Tag}>Phase ${t.phaseId}</${Tag}>` : null}
|
|
305
|
+
${t.id && running ? html`<span class="run-badge">● running</span>` : null}
|
|
306
|
+
</div>
|
|
307
|
+
${expanded ? html`
|
|
308
|
+
<div class="task-detail">
|
|
309
|
+
${t.id ? html`<div class="task-detail-row"><strong>ID:</strong> <code>${t.id}</code></div>` : null}
|
|
310
|
+
${t.points ? html`<div class="task-detail-row"><strong>Points:</strong> ${t.points}</div>` : null}
|
|
311
|
+
<div class="task-detail-row"><strong>Status:</strong> <${Chip} status=${t.status || 'unknown'}/></div>
|
|
312
|
+
${t.sprintId ? html`<div class="task-detail-row"><strong>Sprint:</strong> ${t.sprintId}</div>` : null}
|
|
313
|
+
${t.sprintGoal ? html`<div class="task-detail-row"><strong>Sprint Goal:</strong> ${t.sprintGoal}</div>` : null}
|
|
314
|
+
${t.phaseId ? html`
|
|
315
|
+
<div class="task-detail-row">
|
|
316
|
+
<strong>Phase:</strong> P${t.phaseId}${t.phaseName ? ' — ' + t.phaseName : ''}
|
|
317
|
+
</div>
|
|
318
|
+
` : null}
|
|
319
|
+
${t.acceptance ? html`<div class="task-detail-row"><strong>Acceptance:</strong> ${t.acceptance}</div>` : null}
|
|
320
|
+
${t.assignee ? html`<div class="task-detail-row"><strong>Assignee:</strong> ${t.assignee}</div>` : null}
|
|
321
|
+
${taskCmds.length ? html`
|
|
322
|
+
<div class="task-detail-cmds">
|
|
323
|
+
${taskCmds.map(([cmd, desc]) => html`<${CmdHint} key=${cmd} cmd=${cmd} desc=${desc}/>`)}
|
|
324
|
+
</div>
|
|
325
|
+
` : null}
|
|
326
|
+
</div>
|
|
327
|
+
` : null}
|
|
328
|
+
</div>
|
|
329
|
+
`;
|
|
330
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side ESM icon set — mirrors server/lib/html/icons.js.
|
|
3
|
+
*
|
|
4
|
+
* IMPORTANT: The ICONS map below is a copy of the one in icons.js (CJS).
|
|
5
|
+
* Keep both files in sync when adding or changing icon paths.
|
|
6
|
+
* Cross-reference: server/lib/html/icons.js (server-side CJS counterpart)
|
|
7
|
+
*
|
|
8
|
+
* Why two files? icons.js uses `module.exports` for Node require() in shell.js.
|
|
9
|
+
* The browser cannot import a CJS module as ESM without a build step, so this
|
|
10
|
+
* ESM-only copy is the no-build-step cost. The data is identical; update both.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { h } from './preact.js';
|
|
14
|
+
|
|
15
|
+
// name → inner SVG markup (viewBox 0 0 24 24, stroke = currentColor)
|
|
16
|
+
// Keep in sync with server/lib/html/icons.js
|
|
17
|
+
export const ICONS = {
|
|
18
|
+
home: '<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/>',
|
|
19
|
+
activity: '<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>',
|
|
20
|
+
map: '<polygon points="3 6 9 3 15 6 21 3 21 18 15 21 9 18 3 21"/><line x1="9" y1="3" x2="9" y2="18"/><line x1="15" y1="6" x2="15" y2="21"/>',
|
|
21
|
+
target: '<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>',
|
|
22
|
+
layers: '<polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/>',
|
|
23
|
+
zap: '<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>',
|
|
24
|
+
checkSquare: '<polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>',
|
|
25
|
+
kanban: '<rect x="3" y="4" width="5" height="16" rx="1"/><rect x="10" y="4" width="5" height="11" rx="1"/><rect x="17" y="4" width="5" height="14" rx="1"/>',
|
|
26
|
+
file: '<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"/>',
|
|
27
|
+
users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
|
|
28
|
+
scale: '<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"/><line x1="8" y1="13" x2="16" y2="13"/><line x1="8" y1="17" x2="13" y2="17"/>',
|
|
29
|
+
database: '<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/><path d="M3 12c0 1.66 4 3 9 3s9-1.34 9-3"/>',
|
|
30
|
+
play: '<polygon points="6 3 20 12 6 21 6 3"/>',
|
|
31
|
+
terminal: '<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>',
|
|
32
|
+
square: '<rect x="6" y="6" width="12" height="12" rx="1"/>',
|
|
33
|
+
x: '<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>',
|
|
34
|
+
minimize: '<line x1="5" y1="14" x2="19" y2="14"/>',
|
|
35
|
+
maximize: '<path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/>',
|
|
36
|
+
clock: '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
|
|
37
|
+
eye: '<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/>',
|
|
38
|
+
filePen: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h7"/><polyline points="14 2 14 8 20 8"/><path d="M18.4 12.6a2 2 0 0 1 3 3L17 20l-4 1 1-4z"/>',
|
|
39
|
+
hourglass: '<path d="M5 22h14M5 2h14M17 22v-4.17a2 2 0 0 0-.59-1.42L12 12l-4.41 4.41A2 2 0 0 0 7 17.83V22M7 2v4.17a2 2 0 0 0 .59 1.42L12 12l4.41-4.41A2 2 0 0 0 17 6.17V2"/>',
|
|
40
|
+
|
|
41
|
+
// Added in sprint 32.2 — emoji-to-SVG sweep
|
|
42
|
+
building: '<rect x="4" y="2" width="16" height="20" rx="2"/><path d="M9 22V12h6v10"/><path d="M8 7h.01M12 7h.01M16 7h.01M8 11h.01M12 11h.01M16 11h.01"/>',
|
|
43
|
+
link: '<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>',
|
|
44
|
+
'alert-triangle':'<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>',
|
|
45
|
+
brain: '<path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96-.46 2.5 2.5 0 0 1-1.28-4.56A3 3 0 0 1 5 12c0-.56.15-1.1.42-1.57a2.5 2.5 0 0 1-.42-4.93V5.5A2.5 2.5 0 0 1 9.5 2z"/><path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96-.46 2.5 2.5 0 0 0 1.28-4.56A3 3 0 0 0 19 12c0-.56-.15-1.1-.42-1.57a2.5 2.5 0 0 0 .42-4.93V5.5A2.5 2.5 0 0 0 14.5 2z"/>',
|
|
46
|
+
'clipboard-list':'<rect x="8" y="2" width="8" height="4" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><line x1="12" y1="11" x2="12" y2="17"/><line x1="9" y1="14" x2="15" y2="14"/>',
|
|
47
|
+
flag: '<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"/>',
|
|
48
|
+
monitor: '<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/>',
|
|
49
|
+
'file-text': '<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"/><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"/>',
|
|
50
|
+
copy: '<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>',
|
|
51
|
+
lightbulb: '<line x1="9" y1="18" x2="15" y2="18"/><line x1="10" y1="22" x2="14" y2="22"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"/>',
|
|
52
|
+
'edit-3': '<path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>',
|
|
53
|
+
|
|
54
|
+
// Added in sprint 32.3 — App/Topbar theme toggle icons
|
|
55
|
+
moon: '<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>',
|
|
56
|
+
sun: '<circle cx="12" cy="12" r="4"/><line x1="12" y1="2" x2="12" y2="6"/><line x1="12" y1="18" x2="12" y2="22"/><line x1="4.22" y1="4.22" x2="7.05" y2="7.05"/><line x1="16.95" y1="16.95" x2="19.78" y2="19.78"/><line x1="2" y1="12" x2="6" y2="12"/><line x1="18" y1="12" x2="22" y2="12"/><line x1="4.22" y1="19.78" x2="7.05" y2="16.95"/><line x1="16.95" y1="7.05" x2="19.78" y2="4.22"/>',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Icon Preact component. Renders an inline SVG from the ICONS map.
|
|
61
|
+
*
|
|
62
|
+
* Props:
|
|
63
|
+
* name {string} — key in ICONS
|
|
64
|
+
* size {number} — px, default 16
|
|
65
|
+
* cls {string} — extra CSS classes
|
|
66
|
+
*
|
|
67
|
+
* Usage: html`<${Icon} name="home" size=${16} />`
|
|
68
|
+
*/
|
|
69
|
+
export function Icon({ name, size = 16, cls = '' }) {
|
|
70
|
+
const paths = ICONS[name];
|
|
71
|
+
if (!paths) return null;
|
|
72
|
+
return h('svg', {
|
|
73
|
+
class: 'ic' + (cls ? ' ' + cls : ''),
|
|
74
|
+
width: size,
|
|
75
|
+
height: size,
|
|
76
|
+
viewBox: '0 0 24 24',
|
|
77
|
+
fill: 'none',
|
|
78
|
+
stroke: 'currentColor',
|
|
79
|
+
'stroke-width': '2',
|
|
80
|
+
'stroke-linecap': 'round',
|
|
81
|
+
'stroke-linejoin': 'round',
|
|
82
|
+
'aria-hidden': 'true',
|
|
83
|
+
dangerouslySetInnerHTML: { __html: paths },
|
|
84
|
+
});
|
|
85
|
+
}
|