fenix-claude-plugin 0.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.
Files changed (40) hide show
  1. package/.claude-plugin/plugin.json +13 -0
  2. package/.mcp.json +11 -0
  3. package/README.md +55 -0
  4. package/artifacts/practice-dashboard.html +469 -0
  5. package/artifacts/project-overview.html +481 -0
  6. package/artifacts/user-dashboard.html +368 -0
  7. package/commands/fenix-install.md +15 -0
  8. package/commands/fenix-setup.md +14 -0
  9. package/package.json +16 -0
  10. package/skills/fenix-application/CUSTOMIZE.md +1 -0
  11. package/skills/fenix-application/SKILL.md +95 -0
  12. package/skills/fenix-application/VERSION +1 -0
  13. package/skills/fenix-application/assets/claims_editor.html +87 -0
  14. package/skills/fenix-application/assets/figures_editor.html +87 -0
  15. package/skills/fenix-application/assets/spec_editor.html +87 -0
  16. package/skills/fenix-application/references/claims.md +30 -0
  17. package/skills/fenix-application/references/disclosure.md +33 -0
  18. package/skills/fenix-application/references/editors.md +52 -0
  19. package/skills/fenix-application/references/figure-description.md +67 -0
  20. package/skills/fenix-application/references/figures-guidance.md +122 -0
  21. package/skills/fenix-application/references/figures.md +28 -0
  22. package/skills/fenix-application/references/patent-review-advantages.md +35 -0
  23. package/skills/fenix-application/references/patent-review-checklist.md +36 -0
  24. package/skills/fenix-application/references/patent-review-claim-formats.md +71 -0
  25. package/skills/fenix-application/references/patent-review-profanity.md +57 -0
  26. package/skills/fenix-application/references/patent-review.md +82 -0
  27. package/skills/fenix-application/references/patent-safe.md +44 -0
  28. package/skills/fenix-application/references/patent.md +18 -0
  29. package/skills/fenix-application/references/spec-guidance.md +195 -0
  30. package/skills/fenix-application/references/spec.md +48 -0
  31. package/skills/fenix-application/scripts/calculate_figure_layout.py +32 -0
  32. package/skills/fenix-application/scripts/parse_claims.py +39 -0
  33. package/skills/fenix-office-action/CUSTOMIZE.md +1 -0
  34. package/skills/fenix-office-action/SKILL.md +40 -0
  35. package/skills/fenix-office-action/VERSION +1 -0
  36. package/skills/fenix-office-action/references/oa-response.md +10 -0
  37. package/skills/fenix-project/CUSTOMIZE.md +1 -0
  38. package/skills/fenix-project/SKILL.md +41 -0
  39. package/skills/fenix-project/VERSION +1 -0
  40. package/skills/fenix-project/references/create-project.md +10 -0
@@ -0,0 +1,481 @@
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>