fenix-claude-plugin 0.1.2 → 0.2.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.
Files changed (48) hide show
  1. package/.claude-plugin/plugin.json +13 -13
  2. package/.mcp.json +11 -11
  3. package/README.md +33 -40
  4. package/artifacts/collections.html +149 -0
  5. package/artifacts/fenix-workspace.html +1285 -0
  6. package/artifacts/oa-editor.html +210 -0
  7. package/artifacts/patent-editor.html +325 -0
  8. package/artifacts/review-workspace.html +1135 -0
  9. package/commands/fenix-install.md +19 -11
  10. package/commands/fenix-setup.md +18 -13
  11. package/package.json +16 -16
  12. package/skills/fenix-application/SKILL.md +38 -11
  13. package/skills/fenix-application/VERSION +1 -1
  14. package/skills/fenix-application/references/claim1-method-drafting.md +1 -1
  15. package/skills/fenix-application/references/claims.md +4 -5
  16. package/skills/fenix-application/references/disclosure.md +116 -10
  17. package/skills/fenix-application/references/editors.md +228 -39
  18. package/skills/fenix-application/references/figure-description.md +1 -1
  19. package/skills/fenix-application/references/figure-strategy.md +166 -0
  20. package/skills/fenix-application/references/figures-guidance.md +1 -1
  21. package/skills/fenix-application/references/figures.md +60 -9
  22. package/skills/fenix-application/references/patent-review-checklist.md +3 -1
  23. package/skills/fenix-application/references/patent-review.md +6 -2
  24. package/skills/fenix-application/references/patent-safe.md +1 -1
  25. package/skills/fenix-application/references/patent.md +11 -10
  26. package/skills/fenix-application/references/spec-guidance.md +1 -1
  27. package/skills/fenix-application/references/spec.md +17 -7
  28. package/skills/fenix-application/scripts/figure_strategy_server.py +136 -0
  29. package/skills/fenix-diagram/SKILL.md +54 -0
  30. package/skills/fenix-diagram/VERSION +1 -0
  31. package/skills/fenix-diagram/references/diagram-svg-guide.md +271 -0
  32. package/skills/fenix-office-action/SKILL.md +9 -7
  33. package/skills/fenix-office-action/VERSION +1 -1
  34. package/skills/fenix-office-action/references/oa-analysis.md +14 -4
  35. package/skills/fenix-office-action/references/oa-drafting.md +144 -50
  36. package/skills/fenix-office-action/references/oa-response.md +31 -18
  37. package/skills/fenix-project/SKILL.md +4 -3
  38. package/skills/fenix-project/VERSION +1 -1
  39. package/skills/fenix-project/references/create-project.md +11 -1
  40. package/artifacts/practice-dashboard.html +0 -469
  41. package/artifacts/project-overview.html +0 -481
  42. package/artifacts/user-dashboard.html +0 -368
  43. package/skills/fenix-application/CUSTOMIZE.md +0 -1
  44. package/skills/fenix-application/assets/claims_editor.html +0 -86
  45. package/skills/fenix-application/assets/figures_editor.html +0 -86
  46. package/skills/fenix-application/assets/spec_editor.html +0 -86
  47. package/skills/fenix-office-action/CUSTOMIZE.md +0 -1
  48. package/skills/fenix-project/CUSTOMIZE.md +0 -1
@@ -1,481 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Project Overview</title>
7
- <style>
8
- :root{
9
- color-scheme: dark;
10
- --bg:#0c0e13; --card:#14171f; --card2:#191d27; --ink:#c3c8d4; --muted:#7e8598;
11
- --line:#242936; --line2:#2f3543;
12
- --accent:#818cf8; --accent2:#6366f1; --accentbg:#1c2030; --accentglow:#2a2f4a;
13
- --red:#f87171; --redbg:#2a1417; --orange:#fb923c; --orangebg:#2a1c10;
14
- --amber:#fbbf24; --amberbg:#2a230f; --green:#34d399; --greenbg:#0f2a20;
15
- --gray:#8b92a6; --graybg:#1b1f29; --purple:#a78bfa; --purplebg:#211a33;
16
- }
17
- *{box-sizing:border-box}
18
- body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
19
- .mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
20
- .wrap{max-width:980px;margin:0 auto;padding:22px 20px 80px}
21
-
22
- .top{display:flex;align-items:center;gap:13px;margin-bottom:18px}
23
- .logo{display:inline-flex;align-items:center;justify-content:center;width:46px;height:46px;border-radius:11px;border:1px solid var(--line2);background:#0a0c11;line-height:0;flex-shrink:0}
24
- .logo img{display:block;width:38px;height:38px;object-fit:contain}
25
- h1{font-size:22px;margin:0;letter-spacing:-.02em;font-weight:800}
26
- .sub{color:var(--muted);font-size:12.5px;margin-top:3px}
27
-
28
- /* combobox */
29
- .combo{position:relative;margin-bottom:20px;max-width:580px}
30
- .combo input[type=text]{width:100%;padding:10px 34px 10px 13px;border:1px solid var(--line2);border-radius:9px;background:var(--card2);color:var(--ink);font-size:13px}
31
- .combo input[type=text]::placeholder{color:var(--muted)}
32
- .combo input[type=text]:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(129,140,248,.18)}
33
- .combo .caret{position:absolute;right:13px;top:50%;transform:translateY(-50%);pointer-events:none;color:var(--muted);font-size:10px}
34
- .combo-list{position:absolute;z-index:20;left:0;right:0;top:calc(100% + 5px);background:var(--card);border:1px solid var(--line2);border-radius:10px;box-shadow:0 14px 40px rgba(0,0,0,.45);max-height:340px;overflow-y:auto;padding:5px}
35
- .combo-list[hidden]{display:none}
36
- .combo-item{padding:9px 11px;border-radius:7px;font-size:13px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
37
- .combo-item:hover,.combo-item.active{background:var(--accentbg);color:var(--ink)}
38
- .combo-empty{padding:11px;font-size:13px;color:var(--muted)}
39
-
40
- /* KPI strip */
41
- .kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:11px;margin-bottom:14px}
42
- .kpi{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:15px 16px;position:relative;overflow:hidden}
43
- .kpi .n{font-size:26px;font-weight:800;letter-spacing:-.03em;line-height:1.05;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
44
- .kpi .l{color:var(--muted);font-size:11.5px;margin-top:7px}
45
- .kpi.accent .n{color:var(--accent)}
46
- .kpi.red{background:linear-gradient(180deg,var(--redbg),var(--card));border-color:#3a1f24}
47
- .kpi.red .n{color:var(--red)}
48
- .kpi.orange .n{color:var(--orange)}
49
- .kpi.amber .n{color:var(--amber)}
50
- .kpi.green .n{color:var(--green)}
51
- .kpi .days{display:inline-block;margin-top:8px;padding:1px 8px;border-radius:6px;font-size:10.5px;font-weight:700}
52
-
53
- /* panels */
54
- .panel{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:18px;margin-bottom:14px}
55
- .panel h3{margin:0 0 14px;font-size:11.5px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-weight:700}
56
- .panel h3.mt{margin-top:18px}
57
-
58
- .matter-title{font-size:19px;font-weight:800;letter-spacing:-.01em;margin:0 0 4px}
59
- .matter-num{font-size:13px;color:var(--muted);margin:0 0 13px}
60
- .pills{display:flex;gap:8px;flex-wrap:wrap}
61
- .pill{display:inline-block;padding:2px 11px;border-radius:20px;font-size:11px;font-weight:700;white-space:nowrap;background:var(--graybg);color:var(--gray)}
62
- .pill.type{background:var(--accentbg);color:var(--accent)}
63
-
64
- .kv{display:grid;grid-template-columns:140px 1fr;gap:8px 16px;font-size:13px}
65
- .kv .k{color:var(--muted)}
66
- .kv .v{font-weight:600}
67
-
68
- /* rejections — neutral badges, distinguished by the §number itself, not by rainbow color */
69
- .rej{display:flex;align-items:flex-start;gap:13px;padding:11px 0;border-top:1px solid var(--line)}
70
- .rej:first-child{border-top:none}
71
- .statute{flex:0 0 auto;font-weight:800;font-size:12.5px;padding:5px 11px;border-radius:8px;min-width:62px;text-align:center;background:var(--card2);color:var(--accent);border:1px solid var(--line2);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
72
- .rej .claims{font-size:13px;padding-top:2px}
73
- .rej .claims .meta{color:var(--muted);font-size:11.5px;margin-top:3px}
74
-
75
- /* timeline */
76
- ol.timeline{list-style:none;margin:0;padding:0}
77
- ol.timeline li{display:flex;align-items:center;gap:13px;padding:10px 0;border-top:1px solid var(--line)}
78
- ol.timeline li:first-child{border-top:none}
79
- .dot{flex:0 0 auto;width:11px;height:11px;border-radius:50%;background:var(--gray)}
80
- .dot.active{background:var(--accent);box-shadow:0 0 8px var(--accent)}
81
- .dot.done{background:var(--green)}
82
- .tl-name{flex:1 1 auto;font-weight:600}
83
- .tl-name .st{font-weight:500;color:var(--muted);font-size:12px;margin-left:8px;text-transform:capitalize}
84
- .tl-date{flex:0 0 auto;font-size:12.5px;color:var(--muted)}
85
- .tl-date.overdue{color:var(--red);font-weight:700}
86
-
87
- /* claims / figures / sections */
88
- .claim{padding:9px 0;border-top:1px solid var(--line);font-size:13px}
89
- .claim:first-child{border-top:none}
90
- .claim .cn{font-weight:800;margin-right:6px;color:var(--accent);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
91
- .fig{padding:7px 0;font-size:13px}
92
- .sec{padding:10px 0;border-top:1px solid var(--line)}
93
- .sec:first-child{border-top:none}
94
- .sec .sname{font-weight:700;font-size:13px;margin-bottom:4px}
95
- .sec .stext{font-size:12.5px;color:var(--muted);white-space:pre-wrap;max-height:240px;overflow:auto;line-height:1.55}
96
-
97
- .empty{color:var(--muted);font-style:italic;padding:9px 0}
98
- .center{text-align:center;color:var(--muted);padding:60px 0}
99
- .err{color:var(--red)}
100
- .spin{width:26px;height:26px;border:3px solid var(--line2);border-top-color:var(--accent);border-radius:50%;animation:s .7s linear infinite;margin:0 auto 13px}
101
- @keyframes s{to{transform:rotate(360deg)}}
102
- a.dash{color:var(--accent);text-decoration:none;font-weight:700;font-size:12.5px}
103
- a.dash:hover{text-decoration:underline}
104
- @media(max-width:680px){ .kpis{grid-template-columns:repeat(2,1fr)} }
105
- </style>
106
- </head>
107
- <body>
108
- <div class="wrap">
109
- <div class="top">
110
- <span class="logo" role="img" aria-label="Fenix logo"><img src="data:image/png;base64,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" alt="Fenix logo" width="38" height="38"></span>
111
- <div>
112
- <h1>Project Overview</h1>
113
- <div class="sub">Pick any active matter to see its full record &mdash; live from Fenix, refreshed each load.</div>
114
- </div>
115
- </div>
116
-
117
- <div class="combo">
118
- <input type="text" id="combo" placeholder="Loading matters&hellip;" autocomplete="off" role="combobox" aria-expanded="false" aria-autocomplete="list" disabled>
119
- <span class="caret">&#9660;</span>
120
- <div id="combolist" class="combo-list" role="listbox" hidden></div>
121
- </div>
122
-
123
- <div id="content">
124
- <div class="center">Select a matter above to load its detail.</div>
125
- </div>
126
- </div>
127
-
128
- <script>
129
- const FENIX = {
130
- active: "mcp__25de2133-8f0a-47c4-b66d-b520d6be5fe7__list_active_projects",
131
- project: "mcp__25de2133-8f0a-47c4-b66d-b520d6be5fe7__get_project",
132
- tasks: "mcp__25de2133-8f0a-47c4-b66d-b520d6be5fe7__list_project_tasks",
133
- };
134
- const LS_KEY = "fenix_deepview_last";
135
- let ALL = [];
136
-
137
- // ---- helpers ----
138
- function unwrap(res) {
139
- if (res == null) return null;
140
- if (typeof res === "string") { try { return JSON.parse(res); } catch (e) { return res; } }
141
- if (Array.isArray(res)) return res;
142
- if (typeof res === "object" && Array.isArray(res.content)) {
143
- const t = res.content.map(c => (c && c.text) || "").join("");
144
- try { return JSON.parse(t); } catch (e) { return res; }
145
- }
146
- if (res.result !== undefined) return unwrap(res.result);
147
- return res;
148
- }
149
- async function call(name, args) {
150
- const r = await window.cowork.callMcpTool(name, args || {});
151
- return unwrap(r);
152
- }
153
- function esc(s) {
154
- return String(s == null ? "" : s).replace(/[&<>"]/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;"}[c]));
155
- }
156
- const today = (() => { const n = new Date(); return new Date(n.getFullYear(), n.getMonth(), n.getDate()); })();
157
- function pd(raw) {
158
- if (!raw) return null;
159
- let s = String(raw).trim(); if (!s) return null;
160
- s = s.replace(/(\d+)(st|nd|rd|th)/gi, "$1");
161
- let iso = s.match(/^(\d{4})-(\d{1,2})-(\d{1,2})/);
162
- if (iso) return new Date(+iso[1], +iso[2] - 1, +iso[3]);
163
- let mdy = s.match(/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2,4})$/);
164
- if (mdy) { let mo = +mdy[1], d = +mdy[2], y = +mdy[3]; if (y < 100) y += 2000; return new Date(y, mo - 1, d); }
165
- let t = Date.parse(s); if (isNaN(t)) return null;
166
- const dt = new Date(t); return new Date(dt.getFullYear(), dt.getMonth(), dt.getDate());
167
- }
168
- function dleft(d) { return d ? Math.round((d - today) / 86400000) : null; }
169
- function fmt(d) { return d ? d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) : "&mdash;"; }
170
- function urg(dl) {
171
- if (dl === null) return { bg: "var(--graybg)", fg: "var(--gray)", lab: "No date", kpi: "" };
172
- if (dl < 0) return { bg: "var(--redbg)", fg: "var(--red)", lab: Math.abs(dl) + "d overdue", kpi: "red" };
173
- if (dl <= 7) return { bg: "var(--redbg)", fg: "var(--red)", lab: dl + "d left", kpi: "red" };
174
- if (dl <= 21) return { bg: "var(--orangebg)", fg: "var(--orange)", lab: dl + "d left", kpi: "orange" };
175
- if (dl <= 45) return { bg: "var(--amberbg)", fg: "var(--amber)", lab: dl + "d left", kpi: "amber" };
176
- return { bg: "var(--greenbg)", fg: "var(--green)", lab: dl + "d left", kpi: "green" };
177
- }
178
- function nonEmpty(v) {
179
- if (v == null) return false;
180
- if (Array.isArray(v)) return v.length > 0;
181
- if (typeof v === "object") return Object.keys(v).length > 0;
182
- return String(v).trim() !== "";
183
- }
184
- function claimList(arr) {
185
- if (!nonEmpty(arr)) return "&mdash;";
186
- if (Array.isArray(arr)) return '<span class="mono">' + esc(arr.join(", ")) + "</span>";
187
- return '<span class="mono">' + esc(arr) + "</span>";
188
- }
189
- function statusPill(s) {
190
- const l = (s || "").toLowerCase();
191
- let bg = "var(--graybg)", fg = "var(--gray)", t = s || "—";
192
- if (l === "active") { bg = "var(--accentbg)"; fg = "var(--accent)"; t = "Active"; }
193
- else if (l === "in-progress") { bg = "var(--amberbg)"; fg = "var(--amber)"; t = "In progress"; }
194
- else if (l === "completed") { bg = "var(--greenbg)"; fg = "var(--green)"; t = "Completed"; }
195
- return '<span class="pill" style="background:' + bg + ';color:' + fg + '">' + esc(t) + "</span>";
196
- }
197
-
198
- // ---- combobox ----
199
- const combo = document.getElementById("combo");
200
- const combolist = document.getElementById("combolist");
201
- let comboMatches = [];
202
- let activeIdx = -1;
203
- let selectedId = "";
204
- let lastTyped = "";
205
-
206
- function openList() { combolist.hidden = false; combo.setAttribute("aria-expanded", "true"); }
207
- function closeList() { combolist.hidden = true; combo.setAttribute("aria-expanded", "false"); activeIdx = -1; }
208
- function projLabel(p) {
209
- const mn = (p.matterNum && p.matterNum !== "Default Matter") ? p.matterNum : ("(" + p.projectId.slice(-6) + ")");
210
- const ty = p.project_type ? " — " + p.project_type : "";
211
- const cl = (p.clientName && p.clientName !== "Default Client") ? " · " + p.clientName : "";
212
- return mn + ty + cl;
213
- }
214
- function rankType(t) {
215
- t = (t || "").toLowerCase();
216
- if (t.includes("office action") || t === "oa" || t === "rtoa") return 0;
217
- return 1;
218
- }
219
- function matchesFor(f) {
220
- f = (f || "").toLowerCase().trim();
221
- if (!f) return ALL.slice();
222
- return ALL.filter(p => projLabel(p).toLowerCase().includes(f));
223
- }
224
- function renderList(filter) {
225
- comboMatches = matchesFor(filter);
226
- combolist.innerHTML = "";
227
- if (!comboMatches.length) {
228
- const d = document.createElement("div");
229
- d.className = "combo-empty"; d.textContent = "No matching matters";
230
- combolist.appendChild(d);
231
- return;
232
- }
233
- comboMatches.forEach((p, i) => {
234
- const d = document.createElement("div");
235
- d.className = "combo-item" + (i === activeIdx ? " active" : "");
236
- d.setAttribute("role", "option");
237
- d.textContent = projLabel(p);
238
- d.addEventListener("mousedown", ev => { ev.preventDefault(); choose(p); });
239
- combolist.appendChild(d);
240
- });
241
- }
242
- function setActive(i) {
243
- const items = combolist.querySelectorAll(".combo-item");
244
- if (!items.length) { activeIdx = -1; return; }
245
- activeIdx = (i + items.length) % items.length;
246
- items.forEach((el, idx) => el.classList.toggle("active", idx === activeIdx));
247
- items[activeIdx].scrollIntoView({ block: "nearest" });
248
- }
249
- function choose(p) {
250
- selectedId = p.projectId;
251
- combo.value = projLabel(p);
252
- lastTyped = combo.value;
253
- closeList();
254
- loadProject(p.projectId);
255
- }
256
-
257
- async function loadPicker() {
258
- try {
259
- const raw = await call(FENIX.active, {});
260
- const arr = Array.isArray(raw) ? raw : (raw && Array.isArray(raw.projects) ? raw.projects : []);
261
- ALL = arr.filter(p => p && p.projectId);
262
- ALL.sort((a, b) => {
263
- const rt = rankType(a.project_type) - rankType(b.project_type);
264
- if (rt) return rt;
265
- const da = pd(a.due_date), db = pd(b.due_date);
266
- if (da && db) return da - db;
267
- if (da) return -1;
268
- if (db) return 1;
269
- return 0;
270
- });
271
- combo.disabled = false;
272
- combo.placeholder = "Search " + ALL.length + " matters…";
273
- renderList("");
274
- const last = localStorage.getItem(LS_KEY);
275
- const lp = last && ALL.find(p => p.projectId === last);
276
- if (lp) { selectedId = lp.projectId; combo.value = projLabel(lp); lastTyped = combo.value; loadProject(lp.projectId); }
277
- } catch (e) {
278
- combo.placeholder = "Failed to load matters";
279
- document.getElementById("content").innerHTML = '<div class="center err">Could not load the matter list: ' + esc(e.message || e) + "</div>";
280
- }
281
- }
282
-
283
- // ---- detail ----
284
- function kpiStrip(proj, full) {
285
- const oa = proj.oa_data || {};
286
- const d = pd(proj.due_date);
287
- const dl = dleft(d);
288
- const u = urg(dl);
289
- let cards = "";
290
- cards += '<div class="kpi ' + (u.kpi || "accent") + '"><div class="n">' + fmt(d) + '</div><div class="l">Due date</div>'
291
- + (dl !== null ? '<div class="days" style="background:' + u.bg + ';color:' + u.fg + '">' + u.lab + "</div>" : "") + "</div>";
292
- const totalClaims = oa.total_number_of_claims || full.claimCount;
293
- if (nonEmpty(totalClaims) && String(totalClaims) !== "0")
294
- cards += '<div class="kpi"><div class="n">' + esc(totalClaims) + '</div><div class="l">Total claims</div></div>';
295
- const indep = full.independentClaimCount || (oa.independent_claims || []).length;
296
- if (indep) cards += '<div class="kpi accent"><div class="n">' + esc(indep) + '</div><div class="l">Independent claims</div></div>';
297
- if (full.figureCount) cards += '<div class="kpi"><div class="n">' + esc(full.figureCount) + '</div><div class="l">Figures</div></div>';
298
- return '<div class="kpis">' + cards + "</div>";
299
- }
300
-
301
- function headerPanel(proj, matter) {
302
- let fields = "";
303
- const K = (k, v) => '<div class="k">' + k + '</div><div class="v">' + v + "</div>";
304
- if (nonEmpty(matter.applicationNum)) fields += K("Application No.", '<span class="mono">' + esc(matter.applicationNum) + "</span>");
305
- if (nonEmpty(matter.filingDate)) fields += K("Filed", '<span class="mono">' + fmt(pd(matter.filingDate)) + "</span>");
306
- if (nonEmpty(proj.assign_date)) fields += K("Assigned", '<span class="mono">' + fmt(pd(proj.assign_date)) + "</span>");
307
-
308
- return '<div class="panel">'
309
- + '<div class="matter-title">' + esc(matter.inventionTitle || "Untitled matter") + "</div>"
310
- + '<div class="matter-num mono">' + esc(matter.matterNum || "") + "</div>"
311
- + '<div class="pills">'
312
- + (proj.project_type ? '<span class="pill type">' + esc(proj.project_type) + "</span>" : "")
313
- + ((matter.clientName || proj.clientName) ? '<span class="pill">' + esc(matter.clientName || proj.clientName) + "</span>" : "")
314
- + (proj.status ? statusPill(proj.status) : "")
315
- + "</div>"
316
- + (fields ? '<div class="kv" style="margin-top:15px">' + fields + "</div>" : "")
317
- + "</div>";
318
- }
319
-
320
- function oaCard(oa) {
321
- let html = '<div class="panel"><h3>Office Action</h3><div class="kv">';
322
- if (nonEmpty(oa.oa_date)) html += '<div class="k">OA date</div><div class="v mono">' + fmt(pd(oa.oa_date)) + "</div>";
323
- if (nonEmpty(oa.oa_type)) html += '<div class="k">OA type</div><div class="v">' + esc(oa.oa_type) + "</div>";
324
- if (nonEmpty(oa.response_type)) html += '<div class="k">Response</div><div class="v">' + esc(oa.response_type) + "</div>";
325
- if (nonEmpty(oa.applicant)) html += '<div class="k">Applicant</div><div class="v">' + esc(oa.applicant) + "</div>";
326
- if (nonEmpty(oa.examiner_last)) html += '<div class="k">Examiner</div><div class="v">' + esc(oa.examiner_last) + "</div>";
327
- if (nonEmpty(oa.independent_claims)) html += '<div class="k">Indep. claims</div><div class="v">' + claimList(oa.independent_claims) + "</div>";
328
- html += "</div>";
329
-
330
- const rej = oa.oa_rejections || [];
331
- html += '<h3 class="mt">Rejections</h3>';
332
- if (nonEmpty(rej)) {
333
- rej.forEach(r => {
334
- const refs = (r.status_refs || []).length;
335
- html += '<div class="rej">'
336
- + '<span class="statute">&sect;' + esc(r.status_type) + "</span>"
337
- + '<div class="claims"><div>Claims ' + claimList(r.status_claims) + "</div>"
338
- + (refs ? '<div class="meta">' + refs + " cited reference" + (refs > 1 ? "s" : "") + "</div>" : "")
339
- + "</div></div>";
340
- });
341
- } else {
342
- html += '<div class="empty">No rejections recorded.</div>';
343
- }
344
-
345
- const am = oa.amendments || {};
346
- if (nonEmpty(am.amend) || nonEmpty(am.canx) || nonEmpty(am.added) || nonEmpty(am.pending_claims)) {
347
- html += '<h3 class="mt">Amendment plan</h3><div class="kv">';
348
- if (nonEmpty(am.amend)) html += '<div class="k">Amend</div><div class="v">' + claimList(am.amend) + "</div>";
349
- if (nonEmpty(am.canx)) html += '<div class="k">Cancel</div><div class="v">' + claimList(am.canx) + "</div>";
350
- if (nonEmpty(am.added)) html += '<div class="k">Add</div><div class="v">' + claimList(am.added) + "</div>";
351
- if (nonEmpty(am.pending_claims)) html += '<div class="k">Pending</div><div class="v">' + claimList(am.pending_claims) + "</div>";
352
- if (nonEmpty(am.pending_claims_ind)) html += '<div class="k">Pending indep.</div><div class="v">' + claimList(am.pending_claims_ind) + "</div>";
353
- html += "</div>";
354
- }
355
-
356
- const cd = oa.claim_dispositions || {};
357
- const dispRows = [["allowed_claims","Allowed"],["rejected_claims","Rejected"],["objected_claims","Objected"],["withdrawn_claims","Withdrawn"],["restriction_or_election","Restriction/election"]]
358
- .filter(function(row) { return nonEmpty(cd[row[0]]); });
359
- if (dispRows.length) {
360
- html += '<h3 class="mt">Claim dispositions</h3><div class="kv">';
361
- dispRows.forEach(function(row) { html += '<div class="k">' + row[1] + '</div><div class="v">' + claimList(cd[row[0]]) + "</div>"; });
362
- html += "</div>";
363
- }
364
- html += "</div>";
365
- return html;
366
- }
367
-
368
- function patentCard(full) {
369
- const claims = (full.parsedClaims && full.parsedClaims.length) ? full.parsedClaims
370
- : (Array.isArray(full.claims) ? full.claims : []);
371
- const figs = Array.isArray(full.figures) ? full.figures : [];
372
- const sections = full.sections && typeof full.sections === "object" ? full.sections : {};
373
- const secNames = full.sectionNames && full.sectionNames.length ? full.sectionNames : Object.keys(sections);
374
-
375
- let html = "";
376
- if (claims.length) {
377
- html += '<div class="panel"><h3>Claims (' + claims.length + ")</h3>";
378
- claims.forEach((c, i) => {
379
- let num, text;
380
- if (typeof c === "string") { num = i + 1; text = c; }
381
- else { num = c.number || c.num || (i + 1); text = c.text || c.claimText || c.body || JSON.stringify(c); }
382
- html += '<div class="claim"><span class="cn">' + esc(num) + ".</span>" + esc(text) + "</div>";
383
- });
384
- html += "</div>";
385
- }
386
- if (figs.length) {
387
- html += '<div class="panel"><h3>Figures (' + figs.length + ")</h3>";
388
- figs.forEach((f, i) => {
389
- let label, desc;
390
- if (typeof f === "string") { label = "Fig. " + (i + 1); desc = f; }
391
- else { label = f.label || f.name || ("Fig. " + (f.number || i + 1)); desc = f.description || f.caption || ""; }
392
- html += '<div class="fig"><strong>' + esc(label) + "</strong>" + (desc ? " — " + esc(desc) : "") + "</div>";
393
- });
394
- html += "</div>";
395
- }
396
- if (secNames.length) {
397
- html += '<div class="panel"><h3>Specification sections</h3>';
398
- secNames.forEach(n => {
399
- const v = sections[n];
400
- const txt = typeof v === "string" ? v : (v && v.text ? v.text : JSON.stringify(v));
401
- html += '<div class="sec"><div class="sname">' + esc(n) + '</div><div class="stext">' + esc(txt) + "</div></div>";
402
- });
403
- html += "</div>";
404
- }
405
- return html;
406
- }
407
-
408
- function timelineCard(tasks) {
409
- if (!nonEmpty(tasks)) return "";
410
- let html = '<div class="panel"><h3>Milestones</h3><ol class="timeline">';
411
- tasks.forEach(t => {
412
- const st = (t.status || "").toLowerCase();
413
- let dotCls = "dot", stLabel = t.status || "";
414
- if (st === "completed" || st === "complete" || st === "done") { dotCls += " done"; }
415
- else if (st === "active") { dotCls += " active"; }
416
- const d = pd(t.dueDate);
417
- const dl = dleft(d);
418
- const open = !(st === "completed" || st === "complete" || st === "done");
419
- const overdue = open && dl != null && dl < 0;
420
- html += "<li>"
421
- + '<span class="' + dotCls + '"></span>'
422
- + '<span class="tl-name">' + esc(t.name || "Task") + (stLabel ? '<span class="st">' + esc(stLabel) + "</span>" : "") + "</span>"
423
- + '<span class="tl-date mono' + (overdue ? " overdue" : "") + '">' + fmt(d) + "</span>"
424
- + "</li>";
425
- });
426
- html += "</ol></div>";
427
- return html;
428
- }
429
-
430
- async function loadProject(pid) {
431
- const c = document.getElementById("content");
432
- c.innerHTML = '<div class="center"><div class="spin"></div>Loading matter…</div>';
433
- localStorage.setItem(LS_KEY, pid);
434
- try {
435
- const [full, tasksRaw] = await Promise.all([
436
- call(FENIX.project, { projectId: pid }).catch(() => null),
437
- call(FENIX.tasks, { projectId: pid }).catch(() => null),
438
- ]);
439
- if (!full) { c.innerHTML = '<div class="center err">No data returned for this matter.</div>'; return; }
440
- const proj = full.project || {};
441
- const matter = full.matter || {};
442
- const oa = proj.oa_data || {};
443
- const tasks = (tasksRaw && Array.isArray(tasksRaw.tasks)) ? tasksRaw.tasks : (Array.isArray(tasksRaw) ? tasksRaw : []);
444
- const isOA = nonEmpty(oa.oa_date) || nonEmpty(oa.oa_rejections) || /office action|^oa$|rtoa/i.test(proj.project_type || "");
445
-
446
- let html = headerPanel(proj, matter);
447
- html += kpiStrip(proj, full);
448
- if (isOA) html += oaCard(oa);
449
- html += patentCard(full);
450
- html += timelineCard(tasks);
451
- if (nonEmpty(proj.notes)) html += '<div class="panel"><h3>Notes</h3><div class="stext">' + esc(proj.notes) + "</div></div>";
452
-
453
- const dash = full.dashboardUrl || (tasksRaw && tasksRaw.dashboardUrl);
454
- if (dash) html += '<div style="margin-top:4px"><a class="dash" href="' + esc(dash) + '" target="_blank" rel="noopener">Open in Fenix dashboard &rarr;</a></div>';
455
-
456
- c.innerHTML = html;
457
- } catch (e) {
458
- c.innerHTML = '<div class="center err">Failed to load matter: ' + esc(e.message || e) + "</div>";
459
- }
460
- }
461
-
462
- // ---- wire up ----
463
- combo.addEventListener("focus", () => { combo.select(); activeIdx = -1; renderList(""); openList(); });
464
- combo.addEventListener("input", e => { lastTyped = e.target.value; activeIdx = -1; renderList(e.target.value); openList(); });
465
- combo.addEventListener("keydown", e => {
466
- if (e.key === "ArrowDown") { e.preventDefault(); if (combolist.hidden) { renderList(lastTyped); openList(); } setActive(activeIdx + 1); }
467
- else if (e.key === "ArrowUp") { e.preventDefault(); setActive(activeIdx - 1); }
468
- else if (e.key === "Enter") { if (activeIdx >= 0 && comboMatches[activeIdx]) { e.preventDefault(); choose(comboMatches[activeIdx]); } }
469
- else if (e.key === "Escape") { closeList(); combo.blur(); }
470
- });
471
- combo.addEventListener("blur", () => {
472
- const sel = ALL.find(p => p.projectId === selectedId);
473
- if (sel && combo.value !== projLabel(sel)) combo.value = projLabel(sel);
474
- });
475
- document.addEventListener("mousedown", e => {
476
- if (!combolist.hidden && !e.target.closest(".combo")) closeList();
477
- });
478
- loadPicker();
479
- </script>
480
- </body>
481
- </html>